mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-11 09:49:41 +02:00
feat(tabs): Tab Layout setting with by case (A) and ledger (B), reversible state order, cleaner tiles (#426)
Tab Grouping becomes Tab Layout (tabArrangement: 'state' | 'case' | 'ledger' | 'classic', default 'state'), the first row of App Settings, Appearance, Tabs, so the old and the new strip are one choice apart. - By case (option A): each case's tabs sit in one .tab-cluster box in first-appearance order, labelled with the case and its count, coloured by a stable hash into the session palette. Membership is _mobileOverviewCaseFor(), the home screens' own match. Inside a box with company a generated w75-api-gateway reads w75; the -<case> stays in the DOM in a .tab-name-case span only .tabs-clusters hides. The incremental render path rebuilds only when the cluster structure key changes. The rail and the sidebar get a labelled section per case; phones dissolve the boxes into the chip row. Drag stays inside one box. - Ledger (option B): CSS only on .tabs-ledger, desktop header strip: an auto-fill column grid of equal cells in mono type with a 3px status bar. Its markup is identical to classic's. - State Order (tabStateOrder: 'urgent-first' | 'urgent-last'): flips the by-state groups so needs you can be the bottom row. - By state: the label column is measured to the widest label on screen and the labels are right-aligned in it, instead of a fixed 92px gutter that left short labels far from their tabs. - Tiles: a three-row grid (label, value, bar) with pixel line-heights in the bundled JetBrains Mono, 36px like the header. The bar used to lie over a fixed 28px tile, and a taller system mono (SF Mono) pushed the value into it. The WS tile's grid moved onto an inner .connection-tile span because JS writes the indicator's display inline. Compact uses the same font and a matched WS size. Tests: test/tab-clusters.test.ts (new), plus the rename and the reversed order in test/tab-triage.test.ts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+244
-34
@@ -19790,7 +19790,7 @@ html .toolbar .btn-git-status[aria-expanded='true'] {
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Tab grouping by state (tabGrouping: 'state', the default;
|
||||
Tab grouping by state (tabArrangement: 'state', the default;
|
||||
Discussion #426 option C)
|
||||
============================================================
|
||||
app.js writes a flex `order` per tab plus one heading and one row break per
|
||||
@@ -19848,14 +19848,19 @@ html .toolbar .btn-git-status[aria-expanded='true'] {
|
||||
/* Header strip, wrapping (desktop): one row per state. Each heading sits in a
|
||||
left gutter, so a group too long for one line continues under its own tabs
|
||||
rather than under its heading, and each row ends with a full-width break. */
|
||||
/* The gutter is as wide as the widest label on screen (app.js measures it into
|
||||
--tab-triage-gutter whenever the labels change; 92px is only the first-paint
|
||||
fallback), and every label is right-aligned in it, so each one sits right
|
||||
next to its own tabs while the rows still start on one line. */
|
||||
.session-tabs-host > .session-tabs.tabs-triage:is(.tabs-auto-wrap, .tabs-two-rows) {
|
||||
--tab-triage-gutter: 92px;
|
||||
padding-left: calc(0.25rem + var(--tab-triage-gutter));
|
||||
padding-left: calc(0.25rem + var(--tab-triage-gutter, 92px));
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-triage:is(.tabs-auto-wrap, .tabs-two-rows) > .tab-triage-head {
|
||||
width: var(--tab-triage-gutter);
|
||||
margin-left: calc(-1 * var(--tab-triage-gutter));
|
||||
justify-content: flex-end;
|
||||
width: var(--tab-triage-gutter, 92px);
|
||||
margin-left: calc(-1 * var(--tab-triage-gutter, 92px));
|
||||
padding: 0 8px 0 0;
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-triage:is(.tabs-auto-wrap, .tabs-two-rows) > .tab-triage-break {
|
||||
@@ -19877,6 +19882,183 @@ html[data-session-list='sidebar'][data-sidebar='collapsed'] .session-sidebar .ta
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Tab clusters by case (tabArrangement: 'case'; Discussion #426 option A)
|
||||
============================================================
|
||||
app.js wraps each case's tabs in a .tab-cluster box (_renderTabClusters):
|
||||
a labelled box per case with two or more tabs, an unlabelled box with a
|
||||
colour swatch per case with one, and a box per web tab. --cluster-color is
|
||||
the case's colour from the session palette. Tabs inside keep their own
|
||||
markup; only the `-<case>` part of a generated name (.tab-name-case) is
|
||||
hidden, and only here. */
|
||||
.session-tabs.tabs-clusters .tab-name-case {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-cluster-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: none;
|
||||
padding: 0 6px 0 2px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
color: var(--text-dim);
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tab-cluster-count {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.tab-cluster-swatch {
|
||||
flex: none;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
background: var(--cluster-color, var(--text-muted));
|
||||
}
|
||||
|
||||
/* Header strip: each cluster is an inline box that wraps inside itself when a
|
||||
case has more tabs than fit on a line. */
|
||||
.session-tabs-host > .session-tabs.tabs-clusters {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-clusters > .tab-cluster {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 1px 3px 1px 8px;
|
||||
border: 1px solid var(--control-border);
|
||||
border-left: 3px solid var(--cluster-color, var(--control-border));
|
||||
border-radius: 8px;
|
||||
background: var(--control-bg);
|
||||
}
|
||||
|
||||
/* A case with one tab and a web tab are boxed but not labelled: the single
|
||||
case keeps its swatch, a web tab has no case at all. */
|
||||
.session-tabs-host > .session-tabs.tabs-clusters > .tab-cluster--single {
|
||||
border-left-width: 1px;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-clusters > .tab-cluster--single .tab-cluster-name,
|
||||
.session-tabs-host > .session-tabs.tabs-clusters > .tab-cluster--single .tab-cluster-count {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-clusters > .tab-cluster--single .tab-cluster-label {
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-clusters > .tab-cluster--web {
|
||||
padding-left: 3px;
|
||||
}
|
||||
|
||||
/* Side rail and sidebar: every case is a section with its label on top
|
||||
(vertical space is cheap there, so a one-tab case is labelled too). The
|
||||
label goes first by `order`, because a sorted rail orders the rows inside
|
||||
each cluster. */
|
||||
html[data-tab-orientation='vertical'] .tab-rail .session-tabs.tabs-clusters > .tab-cluster,
|
||||
html[data-session-list='sidebar'] .session-sidebar .session-tabs.tabs-clusters > .tab-cluster {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
html[data-tab-orientation='vertical'] .tab-rail .session-tabs.tabs-clusters > .tab-cluster + .tab-cluster,
|
||||
html[data-session-list='sidebar'] .session-sidebar .session-tabs.tabs-clusters > .tab-cluster + .tab-cluster {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
html[data-tab-orientation='vertical'] .tab-rail .session-tabs.tabs-clusters .tab-cluster-label,
|
||||
html[data-session-list='sidebar'] .session-sidebar .session-tabs.tabs-clusters .tab-cluster-label {
|
||||
order: -1;
|
||||
padding: 6px 6px 3px;
|
||||
}
|
||||
|
||||
/* The collapsed sidebar is a 44px icon rail with no room for a label. */
|
||||
html[data-session-list='sidebar'][data-sidebar='collapsed'] .session-sidebar .tab-cluster-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Ledger (tabArrangement: 'ledger'; Discussion #426 option B)
|
||||
============================================================
|
||||
The flat list, in tab order, on an aligned column grid: equal cells, mono
|
||||
type, a 3px status bar on the left instead of the dot. Desktop header strip
|
||||
only (updateTabOverflowMode() keeps it wrapping); narrower strips, the rail
|
||||
and the sidebar keep the plain list. Nothing here touches the markup. */
|
||||
@media (min-width: 768px) {
|
||||
.session-tabs-host > .session-tabs.tabs-ledger {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(176px, 1fr));
|
||||
align-content: start;
|
||||
gap: 4px 6px;
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-ledger > .session-tab {
|
||||
--ledger-bar: var(--green);
|
||||
min-width: 0;
|
||||
gap: 7px;
|
||||
padding: 4px 8px 4px 11px;
|
||||
border: 1px solid var(--control-border);
|
||||
border-radius: 4px;
|
||||
background: var(--control-bg);
|
||||
box-shadow: inset 3px 0 0 var(--ledger-bar);
|
||||
font-family: 'JetBrains Mono', var(--font-mono, monospace);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-ledger > .session-tab .tab-info {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-ledger > .session-tab .tab-number {
|
||||
width: auto;
|
||||
min-width: 1ch;
|
||||
height: auto;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: var(--text-muted);
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-ledger > .session-tab .tab-status {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-ledger > .session-tab.tab-alert-idle {
|
||||
--ledger-bar: var(--yellow);
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-ledger > .session-tab:is(.tab-alert-action, :has(> .tab-status.error)) {
|
||||
--ledger-bar: var(--red);
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-ledger > .session-tab:is(.tab-agent-exited, .session-tab--web, :has(> .tab-status.stopped)) {
|
||||
--ledger-bar: var(--text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Header stats styles (headerStatsStyle; Discussion #426 option G)
|
||||
============================================================
|
||||
@@ -19890,8 +20072,7 @@ html[data-session-list='sidebar'][data-sidebar='collapsed'] .session-sidebar .ta
|
||||
|
||||
/* Parts only the two new styles draw. */
|
||||
.stat-spark,
|
||||
.connection-tile-label,
|
||||
.connection-tile-value,
|
||||
.connection-tile,
|
||||
.header-plan-usage .pu-ring,
|
||||
.header-plan-usage .pu-meter {
|
||||
display: none;
|
||||
@@ -19912,9 +20093,21 @@ html[data-header-stats='compact'] .header-system-stats {
|
||||
}
|
||||
|
||||
html[data-header-stats='compact'] .header-system-stats > .connection-indicator {
|
||||
font-size: inherit;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
html[data-header-stats='compact'] .header-system-stats > .connection-indicator > .connection-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
/* Same bundled face as Tiles, so the pills read the same on every machine. */
|
||||
html[data-header-stats='compact'] .header-system-stats,
|
||||
html[data-header-stats='compact'] .header-plan-usage {
|
||||
font-family: 'JetBrains Mono', var(--font-mono, monospace);
|
||||
}
|
||||
|
||||
html[data-header-stats='compact'] .header-system-stats > .connection-indicator::after,
|
||||
html[data-header-stats='compact'] .header-system-stats > .stat-item + .stat-item::before {
|
||||
content: '·';
|
||||
@@ -20009,31 +20202,37 @@ html[data-header-stats='compact'] .header-plan-usage .pu-ring {
|
||||
}
|
||||
|
||||
/* --- Tiles: label over value, a bar underneath, no icons ----------------- */
|
||||
/* Each tile is a three-row grid (label, value, bar) with pixel line-heights,
|
||||
so its height comes from the layout, not from whichever monospace face the
|
||||
machine falls back to. The bar is a real row, never laid over the bottom
|
||||
edge: an absolutely placed bar under a fixed-height tile is what put the
|
||||
value on top of it with a taller system font (SF Mono). The bundled
|
||||
JetBrains Mono comes first so every machine draws the same tile. */
|
||||
html[data-header-stats='tiles'] .header-system-stats {
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html[data-header-stats='tiles'] .header-system-stats > .connection-indicator,
|
||||
html[data-header-stats='tiles'] .header-system-stats > .stat-item,
|
||||
html[data-header-stats='tiles'] .header-plan-usage .pu-win {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 1px;
|
||||
min-width: 44px;
|
||||
height: 28px;
|
||||
padding: 0 8px 3px;
|
||||
html[data-header-stats='tiles'] .header-plan-usage .pu-win,
|
||||
html[data-header-stats='tiles'] .header-system-stats > .connection-indicator > .connection-tile {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: 9px 14px 2px;
|
||||
row-gap: 2px;
|
||||
align-content: center;
|
||||
justify-items: start;
|
||||
min-width: 54px;
|
||||
/* 2 + 9 + 2 + 14 + 2 + 2 + 3 plus the 1px borders: 36px, the header's own
|
||||
height, so the tiles never make the header grow. */
|
||||
padding: 2px 10px 3px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 6px;
|
||||
border-radius: 7px;
|
||||
background: var(--control-bg);
|
||||
border: 1px solid var(--control-border);
|
||||
overflow: hidden;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
line-height: 1.05;
|
||||
font-family: 'JetBrains Mono', var(--font-mono, monospace);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* The tile label and value: the stat items already carry a label and a value,
|
||||
@@ -20043,10 +20242,12 @@ html[data-header-stats='tiles'] .header-system-stats .stat-label,
|
||||
html[data-header-stats='tiles'] .connection-tile-label,
|
||||
html[data-header-stats='tiles'] .header-plan-usage .pu-label {
|
||||
display: block;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
font-size: 0.55rem;
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
opacity: 1;
|
||||
@@ -20056,10 +20257,13 @@ html[data-header-stats='tiles'] .header-system-stats .stat-value,
|
||||
html[data-header-stats='tiles'] .connection-tile-value,
|
||||
html[data-header-stats='tiles'] .header-plan-usage .pu-val {
|
||||
display: block;
|
||||
grid-row: 2;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
font-size: 0.72rem;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Plan values keep their green/yellow/red; the stat values read as plain text
|
||||
@@ -20069,6 +20273,13 @@ html[data-header-stats='tiles'] .connection-tile-value {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* The WS tile: JS sets the indicator's `display` inline (flex, or none when
|
||||
there is nothing to report), so the grid lives on the .connection-tile span
|
||||
inside it and the indicator itself only keeps the flex box it already has. */
|
||||
html[data-header-stats='tiles'] .header-system-stats > .connection-indicator {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html[data-header-stats='tiles'] .header-system-stats > .connection-indicator > .connection-dot,
|
||||
html[data-header-stats='tiles'] .header-system-stats > .connection-indicator > .connection-text {
|
||||
display: none;
|
||||
@@ -20088,14 +20299,13 @@ html[data-header-stats='tiles'] .connection-tile-value.offline {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
/* The bar runs along the bottom edge of the tile. */
|
||||
/* The bar is the third row, full tile width. The WS tile has no bar, and its
|
||||
empty third row keeps it the same height as its neighbours. */
|
||||
html[data-header-stats='tiles'] .header-system-stats .stat-bar,
|
||||
html[data-header-stats='tiles'] .header-plan-usage .pu-meter {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
bottom: 3px;
|
||||
grid-row: 3;
|
||||
justify-self: stretch;
|
||||
width: auto;
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
@@ -20149,11 +20359,11 @@ html[data-header-stats='tiles'] .header-plan-usage .pu-win-idle {
|
||||
of its own so it does not read as a stray mark between the tiles. */
|
||||
html[data-header-stats='tiles'] .header-plan-usage:not(:has(.pu-row)) {
|
||||
justify-content: center;
|
||||
min-width: 44px;
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
min-width: 54px;
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 6px;
|
||||
border-radius: 7px;
|
||||
background: var(--control-bg);
|
||||
border: 1px solid var(--control-border);
|
||||
color: var(--text-muted);
|
||||
|
||||
Reference in New Issue
Block a user