dsh-claude-style 0.19.4-dev.49 → 0.19.5-dev.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/STYLE.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  Design tokens for `dsh-claude-style`, recreating the Claude Code Desktop aesthetic.
4
4
 
5
+ ## Where the values live
6
+
7
+ This page states the rule each look follows and the reason behind it. A value
8
+ lives where it is used: colours, durations, easings and layers in
9
+ `packages/client/src/theme/tokens.json`, a component's lengths in the stylesheet
10
+ beside its feature, the mascots' frames in
11
+ `packages/client/src/features/mascot/sheets.ts`, the page-scale arithmetic in
12
+ `packages/client/src/shared/page-zoom.ts` and `page-scale.ts`. A rule that turns
13
+ on one value quotes its token name; everything else is read from the file the
14
+ rule names (D48). The three tables marked generated are written from
15
+ `tokens.json` by `npm run build`; edit the JSON, not the tables.
16
+
5
17
  ## Palette
6
18
 
7
19
  Every look is a **colour card**: an accent, a canvas, a sidebar and the reader's
@@ -10,47 +22,50 @@ their own, a link per scheme. The first card is the default look. A card's dark
10
22
  outlines are drawn either as a lit highlight or as a flat surface step, and its
11
23
  brand art takes its colour from the ink or the accent (D30).
12
24
 
13
- The colour every token paints is a **tone** of one of those seeds: a lightness
14
- step from the seed, an absolute chroma or a multiple of the seed's, an optional
15
- hue shift and, on translucent surfaces, an alpha. The build resolves the tones
16
- into plain colours, so the page ships literals and nothing derives at runtime.
17
- The ladder is shared by every card; a card states only its seeds, its sidebar,
18
- its bubble and its traits.
25
+ The colour every token paints is a **tone** of one of those seeds; the build
26
+ resolves the tones into plain colours, so the page ships literals and nothing
27
+ derives at runtime. The ladder is shared by every card; a card states only its
28
+ seeds, its sidebar, its bubble and its traits.
19
29
 
20
- The reader authors a card of their own: four colours, shown and edited in
21
- the settings page beside the two brands. Every card can be picked; a brand card
30
+ The reader authors a card of their own: four colours, shown and edited in the
31
+ settings page beside the two brands. Every card can be picked; a brand card
22
32
  paints its own colours whatever the brand mark is, and a picked card offers
23
33
  「恢复跟随品牌」 to hand the colours back to the theme style's brand. The
24
- reader's palette is the same ladder written as relative colours
25
- (`oklch(from var(--dsh-claude-custom-accent) …)`), one block per brand and
26
- scheme, so the colours resolve in the page while the brand cards keep their
27
- literals; the image-colour choice (`Default`, `Accent`, `Text colour`) repaints
28
- the brand art from the same table (D30).
34
+ reader's palette is the same ladder written as relative colours, one block per
35
+ brand and scheme, so the colours resolve in the page while the brand cards keep
36
+ their literals; the image-colour choice (`Default`, `Accent`, `Text colour`)
37
+ repaints the brand art from the same table (D30).
29
38
 
30
39
  Rules:
31
40
 
32
- - Never pure black. The Claude card's light canvas is the desktop's `#FFFFFF`
33
- with a `#FAFAFA` sidebar; its dark canvas is `#191919` with a `#1F1F1F`
34
- sidebar. The DeepSeek card's light canvas is `#FBFCFF` with a `#F7F9FF`
35
- sidebar; every surface, border and ink step around them is derived from that
36
- canvas.
37
- The reader's own message plate is the card's `bubble` fill: Claude's neutral
38
- plate, DeepSeek's light blue `#ECF0FF`; the custom card derives it from its
39
- own canvas, so a conversation bubble reads as the card that painted it.
41
+ - Never pure black, never a cold gray. Each canvas pair — the Claude card's and
42
+ the DeepSeek card's — is read as one family: the canvas, its sidebar, its
43
+ borders and every ink step around them derive from that card's seed. The
44
+ reader's own message plate is the card's `bubble` fill, so a conversation
45
+ bubble reads as the card that painted it.
40
46
  - Accent usage stays under ~10% of visible elements.
41
47
  - Literal colours stay outside the ladder: the selection ring, the ripple's
42
48
  front, the peak and off-peak badges and the popover shadow.
43
49
 
44
50
  **The DeepSeek card** turns both schemes blue. Its accent is DeepSeek's brand
45
- blue `#4D6BFE` — the colour of its whale logo and of Deepy; the sidebar's whale
46
- and wordmark and the hero's fish take it as their ink. Its light canvas is
47
- `#FBFCFF` with a `#F7F9FF` sidebar and a light blue reader's bubble, its dark
48
- canvas a blue-black, and its dark outlines are surface steps rather than
49
- highlights. The
50
- skin's Claude marks (the account row's picture without an avatar) become
51
- DeepSeek's
52
- whale (`packages/assets/src/brand/deepseek-mark.svg`, the host's `FISH_LOGO_PATH`); the
53
- reasoning slider's accent follows whichever palette is in force.
51
+ blue, the colour of its whale logo and of Deepy; the sidebar's whale and
52
+ wordmark and the hero's fish take it as their ink. Its light canvas is a faint
53
+ blue-white with a light blue reader's bubble, its dark canvas a blue-black, and
54
+ its dark outlines are surface steps rather than highlights. The skin's Claude
55
+ marks (the account row's picture without an avatar) become DeepSeek's whale
56
+ (`packages/assets/src/brand/deepseek-mark.svg`, the host's `FISH_LOGO_PATH`);
57
+ the reasoning slider's accent follows whichever palette is in force.
58
+
59
+ **Host token bindings.** The skin supplies its palette through the host's own
60
+ alias tokens, so a host control that reads more than one token for one surface
61
+ has to be given all of them. A filled primary button is that case: the host takes
62
+ the fill from `--dsw-alias-brand-primary`, but the hover from a monochrome step
63
+ of its own scale and the label from a foreground token the skin leaves alone.
64
+ `--dsw-alias-button-primary-fill` and `--dsw-alias-button-primary-hover`
65
+ therefore state both steps in each card. Two fills are palette-specific rather
66
+ than carried over from the dark base: a solid chip, lighter than the canvas in
67
+ light, and an elevated fill, a surface above the canvas in light and a raised
68
+ gray in dark.
54
69
 
55
70
  The tables below are generated from `packages/client/src/theme/tokens.json` by `npm run build`;
56
71
  edit the JSON, not the tables. A token's row names the tone it reads and the
@@ -174,26 +189,12 @@ card leaves unset.
174
189
  | `--dsh-claude-meter-valley` | literal | `#3d7550` | `#8cbf96` | `#3d7550` | `#8cbf96` | `var(--dsw-alias-state-success-primary)` | the off-peak rate badge, and the dated promotions priced below it |
175
190
  <!-- /generated:tokens -->
176
191
 
177
- **Host token bindings.** The skin supplies its palette through the host's own
178
- alias tokens, so a host control that reads more than one token for one surface
179
- has to be given all of them. A filled primary button is that case: the host takes
180
- the fill from `--dsw-alias-brand-primary`, but the hover from a monochrome step
181
- of its own scale (light `#43454a`, dark `#ebeef2`) and the label from a
182
- foreground token the skin leaves alone. `--dsw-alias-button-primary-fill` and
183
- `--dsw-alias-button-primary-hover` therefore state both steps in each card.
184
- Two fills are palette-specific rather than carried over from the dark base:
185
- `--dsw-alias-interactive-bg-hover-solid` (a solid chip, lighter than the canvas
186
- in light) and `--dsw-alias-button-elevated-fill` (a surface above the canvas in
187
- light, a raised gray in dark).
188
-
189
192
  ## Two palettes · 两套配色归属
190
193
 
191
194
  The Colours and Typefaces rows each choose who paints: **Claude** (the default)
192
195
  or **Follow the host**. Every rule that writes a host token carries the Claude
193
- gate in its selector — `[data-dsh-claude-palette="claude"]` for colours,
194
- `[data-dsh-claude-typeface="claude"]` for `--dsw-font-*` — and so does every
195
- rule that paints the skin's canvas onto the host's frame (`<html>`, `<body>`,
196
- `#root`, the sidebar and conversation
196
+ gate in its selector, and so does every rule that paints the skin's canvas onto
197
+ the host's frame (`<html>`, `<body>`, `#root`, the sidebar and conversation
197
198
  columns); the build refuses an ungated one. Under "follow the host" those rules
198
199
  drop out: the host's own tokens stand, or another theme plugin's, such as a
199
200
  wallpaper plugin that clears the canvas and turns the overlays to glass.
@@ -202,91 +203,82 @@ The skin's own surfaces read only its private tokens and host tokens. Under the
202
203
  host palette each private token is an alias of a host token (the token table's
203
204
  Host alias column), so those surfaces follow whoever paints the host's. A new
204
205
  private colour token gets its tone and its host alias in
205
- `packages/client/src/theme/tokens.json`; the build refuses a private token without an alias.
206
- Cards take the host's overlay layer rather than its canvas: a plugin that
207
- clears the canvas still gives its overlays a readable fill, and the shared
208
- popover card blurs what lies behind it (`blur(16px) saturate(1.4)`).
206
+ `packages/client/src/theme/tokens.json`; the build refuses a private token
207
+ without an alias.
208
+
209
+ Cards take the host's overlay layer rather than its canvas: a plugin that clears
210
+ the canvas still gives its overlays a readable fill, and the shared popover card
211
+ blurs what lies behind it so its rows stay readable over a picture.
209
212
 
210
213
  Under the host typeface the skin's own faces fall back to the host's:
211
- `--dsh-claude-font-serif`, `-prose` and `-brand` to `--dsw-font-family` (the host
212
- carries no serif), `--dsh-claude-font-code` to `--ds-font-family-code`.
214
+ `--dsh-claude-font-serif`, `-prose` and `-brand` to `--dsw-font-family` (the
215
+ host carries no serif), `--dsh-claude-font-code` to `--ds-font-family-code`.
213
216
 
214
- The account-hold page keeps its own colours, reproducing a Claude screen.
215
- The reasoning slider and its trigger take the theme accent: the dot matrix and
216
- knob glow read `--dsh-claude-apex*`, and the level ramp reads
217
+ The account-hold page keeps its own colours, reproducing a Claude screen. The
218
+ reasoning slider and its trigger take the theme accent: the dot matrix and knob
219
+ glow read `--dsh-claude-apex*`, and the level ramp reads
217
220
  `--dsh-claude-effort-*`. Translucent neutral hover tints and the usage panel's
218
221
  data colours stay literal; they read on any canvas. Every other colour that
219
222
  paints a surface, a text, a border or an accent reads a token.
220
223
 
221
224
  **The effort trigger's tone.** While the Composer tab's effort-accent row is on,
222
- the effort level's text in the composer's seat box and the card's level name
223
- take their colour from the rung in force. The caption and the scale ends keep
224
- their own caption inks. Every thinking rung has
225
- a place on one ramp between two ends mixed from the theme accent in OKLCH:
226
- `--dsh-claude-effort-low` toward the raised surface (less saturated, lower
227
- contrast) and `--dsh-claude-effort-high` toward the label ink (more contrast). The rung's
228
- place is its index over the ladder's span, so a four-stop ladder reads weak,
229
- two steps between, then strong. The hue starts at the theme accent and travels
230
- slightly toward red as the rung climbs: an accent above 180° leans toward 359°,
231
- one below 180° toward 0°, by a quarter of the distance at the top, while HSL
232
- keeps the saturation and lightness of the ramp step.
233
- The switch-off level and the model's own default level have no strength the catalog
234
- names, so both keep the resting ink (`--dsw-alias-label-secondary`). The row off
235
- leaves the resting ink on every rung.
225
+ the effort level's text in the composer's seat box and the card's level name take
226
+ their colour from the rung in force; the caption and the scale ends keep their
227
+ own caption inks. Every thinking rung has one place on a single ramp between two
228
+ ends mixed from the theme accent: the low end toward the raised surface (less
229
+ saturated, lower contrast), the high end toward the label ink (more contrast),
230
+ and the rung's index over the ladder's span places it. The hue starts at the
231
+ theme accent and travels slightly toward red as the rung climbs. The switch-off
232
+ level and the model's own default level have no strength the catalog names, so
233
+ both keep the resting ink (`--dsw-alias-label-secondary`). The row off leaves the
234
+ resting ink on every rung.
236
235
 
237
236
  **The composer's seat.** The model's name and the effort level share one box in
238
237
  the row, the way the host's own seat carries both texts: the box is the row's one
239
238
  item, it stands against the row's right end and grows leftward, and both of its
240
- cards hang from that right end. The box paints the hover plate — the two texts are
241
- two hit areas of one control, not two chips side by side — and the text under the
242
- pointer takes the stronger ink, except a level wearing the accent ramp, which keeps
243
- its rung and lets the plate answer the pointer. Each text is as wide as the words
244
- it shows, so a level's name changing width moves the box's left end and nothing
245
- else.
239
+ cards hang from that right end. The box paints the hover plate — the two texts
240
+ are two hit areas of one control, not two chips side by side — and the text under
241
+ the pointer takes the stronger ink, except a level wearing the accent ramp, which
242
+ keeps its rung and lets the plate answer the pointer. Each text is as wide as the
243
+ words it shows, so a level's name changing width moves the box's left end and
244
+ nothing else.
246
245
 
247
246
  ## Windows titlebar · 桌面顶栏
248
247
 
249
- On the Windows desktop the 40px caption row has no colour of its own: the
250
- sidebar and the conversation run to the window's top edge, each in its own
251
- fill (`--dsh-claude-sidebar-canvas`, `--dsh-claude-canvas`), and the sidebar's
252
- 0.5px `--dsw-alias-border-l1` hairline runs with them from the top edge to the
253
- bottom. The conversation column and the fullscreen file panel lose the host's
254
- 16px top-left corner, so the hairline meets them straight. The three window
255
- buttons sit on the page itself: no block of their own behind them, and they dim
256
- with a modal's mask like everything else. Their hover plate is the system's, a
257
- tint of the button glyph's colour.
248
+ On the Windows desktop the caption row has no colour of its own: the sidebar and
249
+ the conversation run to the window's top edge, each in its own fill, and the
250
+ sidebar's hairline runs with them from the top edge to the bottom. The
251
+ conversation column and the fullscreen file panel lose the host's top-left
252
+ corner, so the hairline meets them straight. The three window buttons sit on the
253
+ page itself: no block of their own behind them, and they dim with a modal's mask
254
+ like everything else. Their hover plate is the system's, a tint of the button
255
+ glyph's colour.
258
256
 
259
257
  ## Right sidebar · 右侧栏
260
258
 
261
- A panel docked in the right column is a card: 8px inside the column on every
262
- side, a 16px radius, the skin's 1px `--dsw-alias-border-l1` hairline and the
263
- host's own panel elevation (`--dsw-elevation-prominent` — a `.5px` stroke, a
264
- 3px/8px drop and a 20px halo). The frame's canvas shows through the gap, and
265
- the host's `.5px` left border on the first dock column gives way to the
266
- hairline.
267
-
268
- The card hangs on what paints the column: a docked tab's pane
269
- (`[data-dockkit-host="dock"] > [data-dockkit-pane]`) or the column's own empty
270
- host (`[data-dockkit-empty]`), which the pane inside it leaves transparent. A
271
- panel pulled out of the sidebar becomes the host's own floating window
272
- (`[data-dockkit-host="float"]`) and keeps that window's frame; a split column
273
- gives each pane its own card, with the drag divider left on the seam between
274
- them.
275
-
276
- **Right sidebar card layout** (the Sidebar tab) chooses it: on is the card
277
- above; off drops every rule that draws one, so the pane stands in the host's
278
- own fill, flush with the column on all four sides, and a split's two panes meet
279
- at the dock's own resting line. Every card rule is gated on
259
+ A panel docked in the right column is a card: inset from the column on every
260
+ side, its radius from the shape scale, the skin's hairline and the host's own
261
+ panel elevation. The frame's canvas shows through the gap, and the host's left
262
+ border on the first dock column gives way to the hairline.
263
+
264
+ The card hangs on what paints the column: a docked tab's pane or the column's own
265
+ empty host, which the pane inside it leaves transparent. A panel pulled out of
266
+ the sidebar becomes the host's own floating window and keeps that window's frame;
267
+ a split column gives each pane its own card, with the drag divider left on the
268
+ seam between them.
269
+
270
+ **Right sidebar card layout** (the Sidebar tab) chooses it: on is the card above;
271
+ off drops every rule that draws one, so the pane stands in the host's own fill,
272
+ flush with the column on all four sides, and a split's two panes meet at the
273
+ dock's own resting line. Every card rule is gated on
280
274
  `<body data-dsh-claude-dock-look="card">`, so off is the host's own panel look
281
275
  with nothing added.
282
276
 
283
277
  **The start page.** The host centers its stack of entries in a box that never
284
278
  grows, so a list taller than the pane was clipped at both ends, its first rows
285
- out of reach. On the card the stack scrolls: `justify-content: flex-start`,
286
- `margin-top: auto` on the first entry and `margin-bottom: auto` on the last.
287
- The two auto margins absorb the free space exactly as `center` did while the
288
- list fits, and fall to zero once it does not, so the stack starts at the top
289
- and scrolls to its end.
279
+ out of reach. On the card the stack scrolls: the first entry's top margin and the
280
+ last entry's bottom margin take the free space while the stack fits, and fall to
281
+ zero once it does not, so the list starts at the top and scrolls to its end.
290
282
 
291
283
  ## Typography
292
284
 
@@ -294,49 +286,40 @@ and scrolls to its end.
294
286
  - **Sans UI** — chrome, body (`--dsw-font-family`).
295
287
  - **Mono** — code, technical labels (`--dsh-claude-font-code`).
296
288
 
297
-
298
289
  ## Shapes
299
290
 
300
- - Radius: 4 / 8 / 16 px; 12px for a popover card and 6px for its rows (the
301
- popover table below); pills for CTAs (`9999px`). A shape copied from a host
302
- control keeps that control's own geometry rather than the scale above: the
303
- composer card's 18px (the host's 22px less 4), the inline composer's 14px and
304
- 11px, the permission and workspace segments' 7px, the view tabs' 5px, the
305
- peak-rate and file pills' 999px.
291
+ - Radius: a ladder of 4 / 8 / 16 px, a 12px popover card with 6px rows, and
292
+ pills for CTAs. A shape copied from a host control keeps that control's own
293
+ geometry rather than the ladder, and says so in the stylesheet beside its
294
+ feature.
306
295
  - Borders: 1px, and in dark not a colour but a highlight. The three border
307
- tokens are one translucent white at `.09` / `.18` / `.30`, so a card, a panel,
308
- a code block or a keycap reads as a chamfer catching the top light over
309
- whatever fill it sits on rather than as a contour whose colour has to be
310
- re-picked per surface. The light rungs stay the warm opaque ladder. A copy of a
311
- host control keeps that control's own stroke: the inline composer's 1.6px stop
312
- square.
296
+ tokens are one translucent white at three strengths, so a card, a panel, a
297
+ code block or a keycap reads as a chamfer catching the top light over whatever
298
+ fill it sits on rather than as a contour whose colour has to be re-picked per
299
+ surface. The light rungs stay the warm opaque ladder.
313
300
  - Separator: `--dsh-claude-divider`, a rung below the lightest border, drawn at
314
301
  0.5px on every line that only separates — the sidebar's foot area, the search
315
302
  panel's foot, the context stats, the settings rows, a menu's separators, the
316
- file block's IN/OUT line and the transcript's `hr` (one device pixel on a
317
- retina display; elsewhere `0.5px` rasterises to one physical pixel). A card, a
318
- field and a popover keep their 1px border in l1, so an outline always reads
319
- heavier than a separator inside it. The split between the sidebar and the
320
- conversation stays 0.5px of l1 (`theme/chrome.css`); the Markdown table frame
321
- and the trajectory chip stay 0.5px of l2 (`theme/typography.css`,
322
- `features/chat-files`).
303
+ file block's IN/OUT line and the transcript's `hr`. The line belongs to the
304
+ layout rather than to a surface's opening animation, so one thickness covers
305
+ every separator a card holds. A card, a field and a popover keep their 1px
306
+ border in l1, so an outline always reads heavier than a separator inside it.
323
307
  - Spacing: 4 px rhythm.
324
308
  - Bottom edge: the transcript never fades behind the composer by shadow. The
325
309
  composer seat paints an opaque `var(--dsw-alias-bg-base)` bar across its own
326
310
  box (so the bar's height *is* the composer's height) and one
327
- `--dsh-composer-fade-h` (40px) gradient band above it, running to transparent.
328
- The same token is the transcript's bottom clearance, so the last turn rests
311
+ `--dsh-composer-fade-h` gradient band above it, running to transparent. The
312
+ same token is the transcript's bottom clearance, so the last turn rests
329
313
  exactly at the band's top edge. Never re-add background-coloured halo shadows
330
314
  to the card to hide the transcript: they have to be restated in every
331
315
  light / dark / focus / attachment rule, and every rule that resets the card's
332
316
  `box-shadow` inherits the job.
333
317
  - Focus: the composer field lights up — its hairline takes the input box's own
334
- shadow colour (espresso `#141413` in light) with a 1px halo in the same tone
335
- and a deeper drop shadow. Dark inverts the face: on the near-black canvas a
336
- black edge carries no cue, so the hairline and halo become bright ivory —
337
- the field visibly lights up instead of deepening. The footer tray below
338
- follows the same hairline so the two tiers stay one outline. Accent is never
339
- used for focus strokes. Every other control's keyboard ring is the host's own
318
+ shadow colour with a halo in the same tone and a deeper drop shadow; dark
319
+ inverts the face, so the hairline and halo become bright ivory rather than a
320
+ black edge on a near-black canvas. The footer tray below follows the same
321
+ hairline so the two tiers stay one outline. Accent is never used for focus
322
+ strokes. Every other control's keyboard ring is the host's own
340
323
  `:focus-visible` rule painting `--dsw-focus-ring-color`, which the skin states
341
324
  as the same ink; the skin's ring is a `:where()` fallback of no weight, so the
342
325
  rule of whichever layer owns the control — this skin's, the host's, or another
@@ -350,7 +333,7 @@ value (D9). One value per role, so a faster feedback or a calmer swap is one
350
333
  edit; the build refuses a time or a curve written anywhere else.
351
334
 
352
335
  - Feedback: a control answering hover, press or focus.
353
- - Reveal: content entering the chat area at the host's own 120 ms.
336
+ - Reveal: content entering the chat area at the host's own pace.
354
337
  - Surface: an entrance — a menu, a popover, a panel, a dialog, a toast.
355
338
  - Exit: a dismissal, deliberately quicker than the entrance.
356
339
  - Travel: something moving along its own track (a fold's door, the reasoning
@@ -364,10 +347,9 @@ edit; the build refuses a time or a curve written anywhere else.
364
347
  - The presets these tokens feed live in `packages/client/src/shared/motion.css`:
365
348
  a surface arriving or leaving, a menu growing out of its trigger, a plain
366
349
  fade, one half of a wording swap, the pulse a placeholder and the sidebar's
367
- ongoing light share, and the two paints
368
- the waiting line's words can wear (a highlight band and a colour wash). A
369
- feature that needs its own effect reads the tokens and keeps its own
370
- keyframes.
350
+ ongoing light share, and the two paints the waiting line's words can wear (a
351
+ highlight band and a colour wash). A feature that needs its own effect reads
352
+ the tokens and keeps its own keyframes.
371
353
 
372
354
  The table below is generated from `packages/client/src/theme/tokens.json` by `npm run build`;
373
355
  edit the JSON, not the table.
@@ -455,12 +437,12 @@ edit the JSON, not the table.
455
437
 
456
438
  ## Layers
457
439
 
458
- A z-index the skin writes is one of two kinds (D9). Inside one box, siblings
459
- are ordered with a literal from -1 to 9: a fill painted under its own content,
460
- a label over its sliding pill, the composer's rows over one another. Anything
461
- that has to clear what the host draws around it — the composer's own controls,
462
- the window's title row, a floating card, a surface over the whole window —
463
- stands on a layer token, and the build refuses any other number.
440
+ A z-index the skin writes is one of two kinds (D9). Inside one box, siblings are
441
+ ordered with a literal from -1 to 9: a fill painted under its own content, a
442
+ label over its sliding pill, the composer's rows over one another. Anything that
443
+ has to clear what the host draws around it — the composer's own controls, the
444
+ window's title row, a floating card, a surface over the whole window — stands on
445
+ a layer token, and the build refuses any other number.
464
446
 
465
447
  The table below is generated from `packages/client/src/theme/tokens.json` by `npm run build`;
466
448
  edit the JSON, not the table.
@@ -479,21 +461,16 @@ edit the JSON, not the table.
479
461
  ## Text selection
480
462
 
481
463
  Selection is the one surface that leaves the palette on purpose — it is a
482
- transient gesture, not part of the interface, so it uses the platform's two
483
- solid paints and looks the same on both canvases:
484
-
485
- | State | Background | Text |
486
- |---|---|---|
487
- | Window focused | `#3366D0` | `#FFFFFF` |
488
- | Window unfocused | `#C7C7C6` | `#000000` |
464
+ transient gesture, not part of the interface, so it uses the platform's two solid
465
+ paints and looks the same on both canvases.
489
466
 
490
467
  Both are solid (never translucent) and `!important`, so they override whatever
491
468
  colour the text underneath carries — links, inline code, syntax tokens. CSS
492
469
  cannot read window focus: Chromium reaches the inactive paint through its own
493
470
  internal `-internal-inactive-selection-*` properties, which a stylesheet cannot
494
- address. `packages/client/src/features/composer/selection-mirror.ts` therefore mirrors `document.hasFocus()`
495
- onto `data-dsh-window-blur` and the stylesheet switches on that attribute; the
496
- selection itself survives the blur.
471
+ address. `packages/client/src/features/composer/selection-mirror.ts` therefore
472
+ mirrors `document.hasFocus()` onto `data-dsh-window-blur` and the stylesheet
473
+ switches on that attribute; the selection itself survives the blur.
497
474
 
498
475
  The window's chrome is not text. The sidebar column and the conversation header
499
476
  take `user-select: none`: a press on a row or on the session title is a gesture
@@ -509,199 +486,140 @@ host's own crumb already carries the padding and the radius — with a tip under
509
486
  the segment naming the press, in the label ink on the canvas tone the usage
510
487
  panel's day tip uses. The segment carries the tip rather than the words because
511
488
  the host clips a crumb's overflow for its ellipsis. The press opens a field over
512
- the words' own line: canvas fill, a 1px ring in the selection blue, a 4px radius,
513
- and 1px to either side of the words with 3px above and below them, so the ring
514
- hugs the name the way Claude's own field does rather than laying a card over it.
515
- The field stands in the page's own space, so the reader's zoom keeps it on the
516
- title; a rename the host refuses keeps the field, ringed in the host's error
517
- tone.
489
+ the words' own line, in the selection's blue and in the page's own space so the
490
+ reader's zoom keeps it on the title; the field hugs the name the way Claude's own
491
+ field does rather than laying a card over it. A rename the host refuses keeps the
492
+ field, ringed in the host's error tone.
518
493
 
519
494
  ## Markdown material
520
495
 
521
496
  A quote is a **container**, not a link: it keeps the prose face on a neutral bar
522
- and wash (text `#B0AEA5`, light `#6E6A60`), and whatever sits inside it keeps its
523
- own material — links stay blue, inline-code chips stay warm red, file mentions
524
- stay link-blue. Never paint the quote itself with the link colour: the colour
525
- inherits into the block's inline code and mentions, which is exactly the leak
526
- that made every path inside a quote read as a link.
497
+ and wash, and whatever sits inside it keeps its own material — links stay blue,
498
+ inline-code chips stay warm red, file mentions stay link-blue. Never paint the
499
+ quote itself with the link colour: the colour inherits into the block's inline
500
+ code and mentions, which is exactly the leak that made every path inside a quote
501
+ read as a link.
527
502
 
528
503
  **Inline file mentions are links, not code.** The host resolves a file path
529
504
  inside an inline code span to a button (`.fileMention`, hashed — match it with
530
505
  `[class*="_fileMention"]`) and paints it with its link alias. The generic
531
- inline-code chip rule is more specific than that class, so a mention inherits
532
- the chip's warm red unless a rule names it. A mention takes the link blue, weight
533
- 500, and the link's underline (solid in the link tone at rest, solid and fully
534
- opaque on hover, same thickness and offset — the same treatment the skin gives an
535
- anchor); the chip itself is left alone — the same fill, hairline, radius and
536
- padding as any other inline code. Plain inline code keeps its warm text. Note the
537
- hover rule must set only `text-decoration-color`: the `text-decoration` shorthand
538
- resets `text-decoration-thickness` to `auto` and thins the line mid-hover.
506
+ inline-code chip rule is more specific than that class, so a mention inherits the
507
+ chip's warm red unless a rule names it. A mention takes the link blue, the link's
508
+ weight and the link's underline — solid in the link tone at rest, solid and fully
509
+ opaque on hover, same thickness and offset as an anchor; the chip itself is left
510
+ alone, with the same fill, hairline, radius and padding as any other inline code,
511
+ and plain inline code keeps its warm text. Note the hover rule must set only
512
+ `text-decoration-color`: the `text-decoration` shorthand resets
513
+ `text-decoration-thickness` to `auto` and thins the line mid-hover.
539
514
 
540
515
  ## Inline code
541
516
 
542
- The chip hugs its glyphs. The host builds it as an `inline-flex` box that
543
- inherits the prose line box — 23px for a 15px code size — which left ~5px of
544
- empty wash above and below the text. The skin sets `line-height: 1.2` on inline
545
- code (27px → 22px, with the 1px padding as the visible inset); lower values
546
- start clipping descenders. The fill, hairline, radius and size are unchanged.
517
+ The chip hugs its glyphs. The host builds it as an `inline-flex` box that inherits
518
+ the prose line box, which left empty wash above and below the text; the skin
519
+ tightens the code's own line box so the wash sits on the text, stopping short of
520
+ the value that starts clipping descenders. The fill, hairline, radius and size
521
+ stay the host's.
547
522
 
548
523
  ## Popovers · 多选一弹层
549
524
 
550
525
  Every single-choice popover in the skin — the permission menu, the model picker,
551
526
  the account drawer, and the host's own menu primitive under the hero row's
552
- workspace and preset pickers — is meant to start from one recipe. New popovers
553
- take it rather than inventing a card.
554
-
555
- Two rules hold across all of them, both owned by `shared/popover.ts`. A
556
- pointer opens a card only after a **100 ms dwell** — long enough that crossing a
557
- 28px trigger on the way somewhere else unfolds nothing — and the card closes
558
- 100 ms after the pointer leaves. One card keeps its own number with the reason
559
- written where it is used: the model picker's levels close after 150 ms (the
560
- pointer has to cross level 1 to reach level 2). And **only one card is on screen
561
- at a time**: each popover registers its close path with `registerPopover(name,
562
- close)` and calls `closeOtherPopovers(name)` on the way open, so opening the
563
- model picker folds the permission menu, the account drawer and the host's hero
564
- menu.
565
-
566
- Every card the skin places stays inside the window: `positionAnchoredPopover`
567
- holds the resolved position inside the window's own pixels, so a card taller
568
- than the room beside its trigger begins at the window's top edge rather than
569
- drawing its lower rows past the bottom of the page.
527
+ workspace and preset pickers — starts from one recipe. New popovers take it
528
+ rather than inventing a card. The shell, its rows and the separator every card
529
+ shares are written once in `shared/popover.css`; a card's heading and footer are
530
+ its own.
531
+
532
+ Two rules hold across all of them, both owned by `shared/popover.ts`. A pointer
533
+ opens a card only after a short dwell — long enough that crossing a trigger on
534
+ the way somewhere else unfolds nothing — and the card closes a short grace after
535
+ the pointer leaves. A card that keeps its own numbers writes them where they are
536
+ used, with the reason beside them: the model picker's levels close later,
537
+ because the pointer has to cross level 1 to reach level 2. And **only one card is
538
+ on screen at a time**: opening one closes the others through the shared registry,
539
+ so opening the model picker folds the permission menu, the account drawer and the
540
+ host's hero menu.
541
+
542
+ Every card the skin places stays inside the window: the shared placement holds
543
+ the resolved position inside the window's own pixels, so a card taller than the
544
+ room beside its trigger begins at the window's top edge rather than drawing its
545
+ lower rows past the bottom of the page.
570
546
 
571
547
  The context meter's popover is the host's own panel (it portals it, places it,
572
548
  and dismisses it), so the skin drives its trigger on hover and paints nothing of
573
549
  the shell itself. What it carries is described under "Context popover" below.
574
550
 
575
- **Card**
576
-
577
- | Property | Value |
578
- |---|---|
579
- | background | `var(--dsw-alias-bg-overlay)`; dark `#1e1e1d` |
580
- | border | `1px solid var(--dsw-alias-border-l1)`; dark the same stroke, which the dark ladder makes a highlight. The edge stays the card's own border: the box the placement reads and the opening animation scales keeps the paint path it has always had, and no stroke is drawn inside it |
581
- | radius | 12px |
582
- | shadow | `0 8px 24px rgba(20,20,19,.12), 0 2px 6px rgba(20,20,19,.08)`; dark `0 8px 28px rgba(0,0,0,.65), 0 2px 8px rgba(0,0,0,.45)` |
583
- | padding | 6px |
584
- | layout | flex column, `gap: 6px`; a list body stacks its rows 3px apart |
585
- | z-index | 99999, above the host's own menus (1100) |
586
- | open | `opacity 0 → 1`, `translateY(4px) scale(.98) → none`, `.15s ease`, origin on the anchor's side |
587
-
588
- **Row**
589
-
590
- | Property | Value |
591
- |---|---|
592
- | min-height | 32px — a floor, not a cap: a two-line row grows |
593
- | padding | `2px 7px` |
594
- | radius | 6px |
595
- | text | 13px / 20px, `var(--dsw-alias-label-primary)` |
596
- | hover | `var(--dsh-claude-hover-bg, rgba(0, 0, 0, .08))` |
597
- | icon | 16px, `var(--dsw-alias-label-secondary)` |
598
- | two-line row | name 13px / 16px at 500; description 11px / 14px in `label-tertiary` |
599
- | current row | trailing `IconCheckOutline16` in `var(--dsw-alias-brand-primary)` — the accent is what marks the choice |
600
- | disabled | `opacity: .4`, `cursor: not-allowed` |
601
-
602
- **Heading, separator, footer**
603
-
604
- - heading row: 11px / 16px at 600, `letter-spacing: .04em`, uppercase, `label-tertiary`, `padding: 6px 7px 2px`
605
- - separator: `0.5px` `var(--dsh-claude-divider)` — one rung below the lightest
606
- border — `margin: 2px 4px`, drawn as a top border on a zero-height box. One
607
- rule in the shared shell covers every separator in a card, the host's own menu
608
- separators included, so the line is one thickness wherever it appears and does
609
- not follow the card's opening scale
610
- - pinned footer: `margin-top` / `padding-top` 4px, and the same `0.5px` divider
611
- line above it
612
-
613
- **Host surfaces**
614
-
615
- The host's dropdown menus are one shared primitive (primitives' `Menu`), and its
616
- two class-name families hash in opposite directions: the primitive ships inside
617
- the web shell as `_<local>_<hash>_<n>` (`_itemWrap_1nxmc_92`,
618
- `_itemLabel_1nxmc_174`), while the client-ui packages hash as `<hash>_<local>`
619
- (`daogkW_itemName`, `p_FcLG_cardWorkspaceTrigger`). A substring matcher therefore
620
- takes the longest stable piece of whichever family it targets — `_itemWrap_`,
621
- `_itemLabel_`, `_viewport_` on one side, `_itemName`, `_itemDesc` on the other —
622
- never the bare local name.
551
+ **Host surfaces.** The host's dropdown menus are one shared primitive
552
+ (primitives' `Menu`), and its two class-name families hash in opposite
553
+ directions: the primitive ships inside the web shell as `_<local>_<hash>_<n>`,
554
+ while the client-ui packages hash as `<hash>_<local>`. A substring matcher
555
+ therefore takes the longest stable piece of whichever family it targets — the
556
+ local name with its underscores — never the bare local name.
623
557
 
624
558
  The hero row's two pickers are that primitive, portaled to `<body>` with no
625
- marker of their own. `packages/client/src/features/hero-menu/hero-menu.ts` stamps the open card with
626
- `data-dsh-claude-hero-menu` and `features/hero-menu/hero-menu.css` restyles it; the
627
- host's other menus (sidebar row menus, the settings permission row, submenus)
628
- keep the host's own design on purpose. What that replaces: a 20px radius card
629
- with 4px padding, 40px rows at 10px radius, and 14px text.
630
-
631
- The stamp's value names the picker (`workspace` or `preset`), because the two
632
- cards differ. The preset card keeps the popover table's 32px rows for its
633
- two-line entries. The workspace card is Claude Code's folder menu: a 180px-min
634
- card with 4px padding, 26px plain-text rows (no folder glyph in front of each
635
- folder, no `+` on the pinned add row) and the accent check on the current one.
636
-
637
- **The account card** is that primitive too, and it carries the skin's rows
638
- inside it. The host mounts it with nothing but its own three entries and places
639
- it from that geometry; `features/account/surface.ts` injects the skin's container
640
- a frame later, the card grows, and the host re-places it on the frame after the
641
- list changed. Nothing may be painted in between: the card would fade in at the
642
- height and the place it is about to leave — low, then jumping up mid-fade. So
643
- while the account row is armed the card stays unpainted (the same armed window
644
- that carries its one-shot entrance), and the feature stamps
645
- `data-dsh-claude-account-ready` once the skin's rows are inside it and its
646
- placement has been read twice with the same value. The entrance animation hangs
647
- on that stamp, so it plays on the card the reader will actually see.
648
-
649
- One footer control stays in the sidebar rather than becoming a row of that
650
- card: the update plugin's own button, which names itself with
559
+ marker of their own. `packages/client/src/features/hero-menu/hero-menu.ts` stamps
560
+ the open card with `data-dsh-claude-hero-menu` and
561
+ `features/hero-menu/hero-menu.css` restyles it; the host's other menus (sidebar
562
+ row menus, the settings permission row, submenus) keep the host's own design on
563
+ purpose. The stamp's value names the picker (`workspace` or `preset`), because
564
+ the two cards differ: the preset card keeps the recipe's rows for its two-line
565
+ entries, and the workspace card is Claude Code's folder menu — a plain-text row
566
+ per folder, no glyph in front of each, and the accent check on the current one.
567
+
568
+ **The account card** is that primitive too, and it carries the skin's rows inside
569
+ it. The host mounts it with its own three entries and places it from that
570
+ geometry; the skin's rows arrive a frame later and the card grows, so anything
571
+ painted in between would fade in at the height and the place the card is about to
572
+ leave — low, then jumping up mid-fade. The card therefore stays unpainted while
573
+ the account row is armed, and the feature stamps it ready once the skin's rows
574
+ are inside it and its placement has settled; the entrance animation hangs on that
575
+ stamp, so it plays on the card the reader will actually see.
576
+
577
+ One footer control stays in the sidebar rather than becoming a row of that card:
578
+ the update plugin's own button, which names itself with
651
579
  `data-dsh-plugin="update"` and `data-dsh-part="entry"`. The takeover leaves the
652
580
  plugin's node hidden in its slot and stands a copy of it at the right end of the
653
- account row — the plugin's words, mark, progress and press, at the pill's 22px
654
- height and 11px type — so a reader reaching for an update does not have to open
655
- the menu first. The rail has no room beside the icon and folds the copy away.
581
+ account row — the plugin's words, mark, progress and press — so a reader reaching
582
+ for an update does not have to open the menu first. The rail has no room beside
583
+ the icon and folds the copy away.
656
584
 
657
585
  ## Context popover · 上下文弹层
658
586
 
659
587
  The context meter's popover is the host's own panel: the skin opens it by
660
- pressing the host's trigger (hover, with the shared 100 ms dwell and close
661
- grace, gated on the "open popovers on hover" preference; a click still works),
662
- and paints nothing of its shell or its dismissal. It mounts
663
- instead of toggling `data-open`, so the feature stamps the panel and it takes
664
- the cards' own short rise as a one-shot animation — same 4px, same `scale(.98)`,
665
- same 0.15s — and a surface the skin opens arrives the same way wherever it is
666
- the host's.
667
-
668
- Its horizontal place is the one thing the skin takes over. ui-chat hangs the
669
- panel from the anchor's LEFT edge and only then clamps it into the viewport
670
- (`useStatDialog`, align `start`), so a trigger at the end of the composer row
671
- leaves the panel against the window's right margin instead of under the ring.
672
- `features/context-stats/stats-binding.ts` reads the meter's right edge and the
673
- panel's own layout width — `offsetWidth`, not its rect: the entrance scales the
674
- box, and a transformed rect is two per cent narrower than the one that settles —
675
- and writes `--dsh-claude-context-panel-left` with the mark that turns it on;
676
- `features/composer/inline-bar.css` reads it with `!important`, which outranks the
677
- host's inline value. The reading is re-taken when the panel's box changes (a size
678
- subscription on the observation bus) and when the viewport moves (`reposition('viewport')`).
588
+ pressing the host's trigger (hover, under the "open popovers on hover"
589
+ preference; a click still works), and paints nothing of its shell or its
590
+ dismissal. A surface the skin opens arrives the way every skin surface does — the
591
+ cards' own short rise, as a one-shot animation.
592
+
593
+ Its horizontal place is the one thing the skin takes over. The host hangs the
594
+ panel from the anchor's left edge and only then clamps it into the viewport, so a
595
+ trigger at the end of the composer row would leave the panel against the window's
596
+ right margin instead of under the meter. `features/context-stats/` reads the
597
+ meter's edge and the panel's layout width — its box width, not its scaled rect,
598
+ since the entrance scales it — hands them to `features/composer/inline-bar.css`
599
+ through a custom property that the sheet reads over the host's inline value, and
600
+ re-reads when the panel's box or the viewport moves.
679
601
 
680
602
  What the skin appends to it is the session's numbers
681
- (`features/context-stats/session-stats.ts`), read from the host's `sessionStats`
682
- and `tokenUsage` projections — the same durable whole-log values the host's own
683
- pills render — never by opening the host's stat dialogs. The labels and the
684
- duration / token templates come from the host's `chat` locale namespace, so the
685
- two surfaces read the same words; the host's small formatting rules
686
- (`formatDuration`, `formatTokensPerSecond`, `formatExactTokens`,
687
- `formatCacheHitPercent`) are mirrored so the figures match character for
688
- character.
689
-
690
- The cost is the session's own. The host half prices every settlement at its
691
- own instant and route and answers the settled total over `SESSION_COST_PATH`;
692
- the browser adds the tokens the streaming answer has grown by at the output
693
- rate in force, so the figure moves with the written-back tokens and settles
694
- with them. It writes the shorter of four significant digits and four decimals,
695
- with the symbol of the `billingCurrency` setting (`¥0.0015`, `$0.0002`,
696
- `¥2.405`): the official CNY and USD tables are stored
697
- apart, so the setting picks a table rather than converting one. A model the
698
- official table does not carry is priced from the snapshot's catalogue — its
699
- published CNY line when the vendor has one, its USD list price converted at the
700
- snapshot's fixed rate otherwise; a model neither carries keeps the segment out
701
- of the line. On the line it
702
- stands between the written-back tokens and the cache share, and it is among the
703
- figures the ladder gives up when the line needs the room for the total; in the
704
- block it is the usage section's first row.
603
+ (`features/context-stats/session-stats.ts`), read from the host's own
604
+ `sessionStats` and `tokenUsage` projections — the same durable whole-log values
605
+ the host's own pills render — never by opening the host's stat dialogs. The
606
+ labels and the duration and token templates come from the host's `chat` locale
607
+ namespace, so the two surfaces read the same words, and the host's formatting
608
+ rules are mirrored so the figures match character for character.
609
+
610
+ The cost is the session's own. The host half prices every settlement at its own
611
+ instant and route and answers the settled total; the browser adds the tokens the
612
+ streaming answer has grown by at the output rate in force, so the figure moves
613
+ with the written-back tokens and settles with them. It writes the shorter of four
614
+ significant digits and four decimals with the symbol of the `billingCurrency`
615
+ setting: the official CNY and USD tables are stored apart, so the setting picks a
616
+ table rather than converting one. A model the official table does not carry is
617
+ priced from the snapshot's catalogue — its published CNY line when the vendor has
618
+ one, its USD list price converted at the snapshot's fixed rate otherwise; a model
619
+ neither carries keeps the segment out of the line. On the line it stands between
620
+ the written-back tokens and the cache share, and it is among the figures the
621
+ ladder gives up when the line needs the room for the total; in the block it is
622
+ the usage section's first row.
705
623
 
706
624
  In the toolbar's placement the segment keeps its width across a session switch:
707
625
  the session's figure is replaced while the next one is being priced, so the
@@ -711,86 +629,68 @@ extends left, and the box stands as the currency symbol and a dash in
711
629
  of the line and back, and the whole bottom line re-lays around it. The box leads
712
630
  that line, so the room an amount does not need is the line's leading edge rather
713
631
  than a gap between two figures. On the line of its own the segment hugs its
714
- amount like every other figure: a box there would stand between the written-back
715
- figure and the cost and the figures would no longer keep one gap, so the line
716
- fits its figures again for the width the arriving amount needs. The
717
- `billingCurrency` choice's Off takes the segment out of the line altogether:
718
- nothing is drawn, and the browser asks the host for neither the price table nor
719
- the session's total.
632
+ amount like every other figure: the line fits its figures again for the width the
633
+ arriving amount needs. The `billingCurrency` choice's Off takes the segment out
634
+ of the line altogether: nothing is drawn, and the browser asks the host for
635
+ neither the price table nor the session's total.
720
636
 
721
637
  The settings page's custom price table (the row under the currency) writes a row
722
- per model — `model|input|cacheRead|output`, per million tokens in that currency —
723
- and a filled bucket overrides the tables' rate while a blank one leaves it alone.
724
- The rows travel to the host with the session's cost ask, so a settlement and the
725
- answer still streaming are priced from one set of rates; a model neither the
726
- tables nor a row prices leaves the segment out of the line as before. The
727
- editor's card is the modal's own: a 720px card on the window, and a page under
728
- 760px folds each row into a card with the model above three labelled rates.
638
+ per model, per million tokens in that currency; a filled bucket overrides the
639
+ tables' rate while a blank one leaves it alone. The rows travel to the host with
640
+ the session's cost ask, so a settlement and the answer still streaming are priced
641
+ from one set of rates; a model neither the tables nor a row prices leaves the
642
+ segment out of the line as before.
729
643
 
730
644
  Which figures appear follows the host's statistics row. Its detailed form gets
731
- the whole list. Its compact form carries only the output speed and the cache-hit
732
- share on the composer line, so the block keeps the four that row leaves out —
733
- the total time, the first-token average, the output speed and the cache-hit
734
- share. The total is the one figure the host has no word for (its dialog names
735
- the model's time and the tool calls' separately): it is their sum, labelled from
736
- the skin's own copy (`contextTotalTime`).
737
-
738
- The block: a 1px `var(--dsw-alias-border-l1)` rule and 10px above it, then per
739
- section an 11px / 16px heading at 600 in `label-tertiary` and a two-column grid
740
- (6px by 10px). A row is a stack: 12px / 16px label in `label-tertiary` over a
741
- 14px / 20px value at 500 in `label-primary`, the value in tabular figures so
742
- numbers do not dance as they change. Rows follow the host's own rules — one
743
- appears only when its input exists — and the block is rewritten on every
744
- projection frame while the panel is open.
745
-
746
- While the projections have answered nothing, the block holds the numbers' place
747
- under the two real headings: bars at a row's own 37px (16 + 1 + 20) in the host's
748
- solid hover fill, pulsing over 1.6s (still under the reduced animation choice).
749
- The place is given up after 2s, so a host that serves no such projection ends at
750
- the panel it drew itself rather than keeping placeholder bars.
751
-
752
- **All five are aligned**: the account drawer was the outlier
753
- (8px row radius, 2px and 4px card gap, 8px padding, 220 / 260px min-width,
754
- z-index 1000 and 100000) and now follows the table above. The hero row's pickers are the host's own menu primitive, which differs in
755
- two ways that CSS cannot change: it mounts instead of toggling a `data-open`
756
- attribute, so it takes the same fade/scale as a one-shot `0.15s` animation; and
757
- the host places it *below* its trigger, which is where the composer sits — so
758
- `packages/client/src/features/hero-menu/hero-menu.ts` re-places it on the trigger the way the
759
- skin's own composer pickers sit: right-aligned with the trigger and opening
760
- upward by the same 6px air, flipping below only when the viewport leaves no room
761
- above, and clamped to the 8px viewport margin (`POPOVER_MARGIN`). The host
762
- re-runs its own placement from its anchor geometry on every frame while the card
763
- is open, so the position is handed over in two custom properties on the card
764
- (`--dsh-claude-hero-menu-x` / `--dsh-claude-hero-menu-y`, written by the same
765
- pass that stamps it) which `features/hero-menu/hero-menu.css` reads with
766
- `!important`; that declaration outranks the host's plain inline value.
645
+ the whole list; its compact form carries only the output speed and the cache-hit
646
+ share on the composer line, so the block keeps the four that row leaves out — the
647
+ total time, the first-token average, the output speed and the cache-hit share.
648
+ The total is the one figure the host has no word for (its dialog names the
649
+ model's time and the tool calls' separately): it is their sum, labelled from the
650
+ skin's own copy.
651
+
652
+ The block is a rule under the figures and a two-column grid of sections, each
653
+ with a heading and rows that stack a label over a value; values sit in tabular
654
+ figures so numbers do not dance as they change, and a row appears only when its
655
+ input exists. The block is rewritten on every projection frame while the panel is
656
+ open. While the projections have answered nothing, the block holds the numbers'
657
+ place under the two real headings with pulsing bars (still under the reduced
658
+ animation choice), and gives the place up rather than keeping placeholders for a
659
+ host that serves no such projection.
660
+
661
+ **The hero row's pickers.** They follow the recipe above rather than numbers of
662
+ their own, and they are the host's own menu primitive, which differs in two ways
663
+ CSS cannot change: it mounts instead of toggling an open attribute, so it takes
664
+ the same fade and scale as a one-shot animation; and the host places it below its
665
+ trigger, which is where the composer sits. `features/hero-menu/hero-menu.ts`
666
+ therefore re-places it on the trigger the way the skin's own composer pickers sit
667
+ — right-aligned with the trigger and opening upward, flipping below only when the
668
+ viewport leaves no room above — and hands the position to
669
+ `features/hero-menu/hero-menu.css` through custom properties, which the sheet
670
+ reads over the host's inline value.
767
671
 
768
672
  ## Context meter · 上下文计量
769
673
 
770
674
  **The trigger is a pie.** Where the host draws a ring of two circles, the meter
771
675
  draws two sectors on one centre: the share taken in the colour its occupancy fell
772
676
  on, the rest of the plate in the quiet tone beside it. The smaller share keeps
773
- the box's radius (8px) and the larger one steps in by up to 1.3px — half the
774
- lift — so a lopsided split reads as the smaller part standing out to the edge,
775
- 15% and 85% mirror each other, and an even split is two half discs of one radius;
776
- the step eases to nothing as the split comes even. The seam is one width, 1.2px,
777
- shared between two strokes by the split: the occupied sector's stroke runs from
778
- the whole width at nothing taken to nothing at everything taken, the plate's is
779
- the mirror, and each is half the width at the even split. Each sector is cut by
780
- the stroke around the other's path, so the seam follows the whole boundary — both
781
- radii, the outer arc and the apex — and the two edges move with the split as the
782
- reading crosses the middle. The part that stands out is painted last: the plate
783
- above the even split, the reading's colour below it. The pie is 16px across, one
784
- pixel more than the ring's 14px cell on each side, and concentric with it; the
785
- ring keeps its cell and is hidden while the pie stands, so the trigger's layout
786
- and the room the composer keeps for it are the host's. The occupied sector takes
787
- the reading's own colour (the occupancy ramp, below), the plate the quiet tone
788
- beside it.
677
+ the box's radius and the larger one steps in a little, so a lopsided split reads
678
+ as the smaller part standing out to the edge, and the step eases to nothing as
679
+ the split comes even. The seam is one width shared between two strokes by the
680
+ split: the occupied sector's stroke runs from the whole width at nothing taken to
681
+ nothing at everything taken, the plate's is the mirror, and each is half the
682
+ width at the even split. Each sector is cut by the stroke around the other's
683
+ path, so the seam follows the whole boundary — both radii, the outer arc and the
684
+ apex — and the two edges move with the split as the reading crosses the middle.
685
+ The part that stands out is painted last: the plate above the even split, the
686
+ reading's colour below it. The pie is one pixel wider than the ring's cell on
687
+ each side and concentric with it; the ring keeps its cell and is hidden while the
688
+ pie stands, so the trigger's layout and the room the composer keeps for it are
689
+ the host's.
789
690
 
790
691
  **The line of numbers.** On its own line under the card, each figure is plain
791
- text in the line's type (13px / 500, tabular figures), not a run of per-character
792
- spans: a reading that moves is chased from the value on show, closing about two
793
- thirds of the distance every 73ms and the rest within 220ms, a reading that lands
692
+ text in the line's type with tabular figures, not a run of per-character spans: a
693
+ reading that moves is chased from the value on show, a reading that lands
794
694
  mid-count becomes the new destination, and another session's numbers are chased
795
695
  from the ones on show too. Only the cache share's figure and the percent sign
796
696
  after it wear the band; the words before them keep the line's tone. A card too
@@ -800,139 +700,146 @@ figure into k / M / B.
800
700
  ## Search · 搜索
801
701
 
802
702
  **Sidebar box.** The box takes the brand's place in the logo row while the
803
- pointer is over the sidebar: 28px tall, 8px radius, a 1px `#E9E9E9` hairline on
804
- the white canvas fill (dark: `#232323` on `#1F1F1F`), a 16px search glyph in the
805
- secondary label, the "Search" label in the tertiary label at 14px, and the
806
- host's search shortcut as 20px keycaps at the right end. The New session row's
807
- own shortcut wears that same keycap: a 20px box with a 1px hairline, 4px radius
808
- and 12px secondary ink, and the `+` the host writes between the two keys goes,
809
- the way the box leaves it out. It is a button like the New session and Plugins
810
- rows below it: the pointer cursor, and on hover the same plate those rows take
811
- (`--dsh-claude-hover-bg`, dark `rgba(255, 255, 255, 0.08)`) laid over its
812
- opaque fill. Box and brand share one grid cell and cross-fade over 0.16s, the
813
- timing the workspace heading and its segmented control trade places with; the
814
- box is excluded from the window drag region. Standalone gives the box a row of
815
- its own under the brand row, shown without a hover; that row adds no inline
816
- padding, so the box's hairline stands on the two edges of the column's own
817
- inset and is exactly as wide as the New session plate below it. In the desktop
818
- titlebar mode the brand row rises 6px, since the sidebar column starts under
819
- the 40px titlebar — as far as it goes with the box still wholly below that
820
- edge, which clips the column. A box narrower than 200px keeps its glyph and
821
- label and leaves the keycaps out.
822
-
823
- **Palette.** A modal card set 8vh from the top, 760px wide at most, 16px radius,
824
- on the same white fill (dark `#191919`) under the host's mask. From the top: a
825
- 17px borderless input with a 28px close button; the category chips (32px tall,
826
- 8px radius, 14px, tertiary at rest and primary when current) sharing one
827
- sliding highlight at `rgba(20, 20, 19, 0.06)`; the list, capped at
828
- `min(520px, 62vh)`, with 13px tertiary section captions and 40px rows (8px
829
- radius, 15px title, an 18px glyph in the secondary label, a 13px tertiary detail
830
- after the title and an optional 13px tertiary excerpt line under it, the match
831
- inside it in the primary label at weight 500 with no fill); a hairline-topped footer
832
- of hints with 20px keycaps. The input text, the chips' text, the captions and
833
- the row glyphs all start on one vertical line 24px in from the card edge. The
834
- highlighted row takes `rgba(20, 20, 19, 0.05)` (dark `rgba(250, 249, 245, 0.07)`)
835
- and shows the Enter glyph; the others hide it. The card fades in and settles
836
- from 6px above at 98.5% scale over 0.18s; on close card and mask fade out
837
- together over 0.14s before the modal unmounts.
703
+ pointer is over the sidebar: a short field the height of the row, a hairline on
704
+ the canvas fill, a search glyph in the secondary label, the "Search" label in the
705
+ tertiary label, and the host's search shortcut as keycaps at the right end. The
706
+ New session row's own shortcut wears that same keycap, and the `+` the host
707
+ writes between the two keys goes, the way the box leaves it out. It is a button
708
+ like the New session and Plugins rows below it: the pointer cursor, and on hover
709
+ the same plate those rows take laid over its opaque fill. Box and brand share one
710
+ grid cell and cross-fade on the timing the workspace heading and its segmented
711
+ control trade places with; the box is excluded from the window drag region.
712
+ Standalone gives the box a row of its own under the brand row, shown without a
713
+ hover; that row adds no inline padding, so the box's hairline stands on the two
714
+ edges of the column's own inset and is exactly as wide as the New session plate
715
+ below it. In the desktop titlebar mode the brand row rises, as far as it goes with
716
+ the box still wholly below the titlebar edge, which clips the column. A box
717
+ narrower than its keycaps need keeps its glyph and label and leaves them out.
718
+
719
+ **Palette.** A modal card set down the window from the top, the host's mask under
720
+ it, on the same canvas fill. From the top: a borderless input with a close
721
+ button; the category chips sharing one sliding highlight; the list, capped
722
+ against the page's own height token rather than `vh` (D81), with section captions
723
+ and rows carrying a title, a glyph, a detail and an optional excerpt line, the
724
+ match in the primary label with no fill; a hairline-topped footer of hints with
725
+ keycaps. The input text, the chips' text, the captions and the row glyphs all
726
+ start on one vertical line inside the card edge. The highlighted row takes a
727
+ plate and shows the Enter glyph; the others hide it. The card fades in and settles
728
+ from above at less than full scale; on close card and mask fade out together
729
+ before the modal unmounts.
838
730
 
839
731
  ## Chat / Code switch · 聊天 / 代码切换
840
732
 
841
- With ds-bot installed, the switch is two 28×22 options on a 2px-padded plate
842
- with a 7px radius, each option 5px: chat bubbles and `</>`, 16px glyphs in the
843
- 1.3px stroke the sidebar's glyphs use. It is painted on both sides of the switch
844
- by whoever paints the host's tokens — the theme on the Code side, ds-bot on the
845
- Chat side — so the plate is a translucent tint (`rgba(20, 20, 19, 0.06)`, dark
846
- `rgba(255, 255, 255, 0.08)`), the options take the tertiary label at rest and
847
- the primary on hover, and the checked one is cut from the conversation canvas
848
- (`--dsw-alias-bg-base`) with a 1px `rgba(20, 20, 19, 0.10)` drop (dark: a
849
- `rgba(255, 255, 255, 0.14)` fill and no drop). On the desktop it stands in the
850
- caption row, its right edge 12px inside the sidebar column's, centred in the
851
- row, and leaves the screen while the column is too narrow to keep 8px clear of
852
- the controls at the row's left end; on the web it is a cell of the sidebar's
853
- first row, before the collapse toggle. It moves nothing: there is no
854
- transition, since the press hands the whole page to the other side.
733
+ With ds-bot installed, the switch is two options on a padded plate, each carrying
734
+ its glyph in the stroke the sidebar's glyphs use. It is painted on both sides of
735
+ the switch by whoever paints the host's tokens — the theme on the Code side,
736
+ ds-bot on the Chat side — so the plate is a translucent tint, the options take
737
+ the tertiary label at rest and the primary on hover, and the checked one is cut
738
+ from the conversation canvas with a hairline of its own. On the desktop it stands
739
+ in the caption row, its right edge inside the sidebar column's, centred in the
740
+ row, and leaves the screen while the column is too narrow to keep clear of the
741
+ controls at the row's left end; on the web it is a cell of the sidebar's first
742
+ row, before the collapse toggle. It moves nothing: there is no transition, since
743
+ the press hands the whole page to the other side.
855
744
 
856
745
  ## The waiting line · 模型正在做什么的那一行
857
746
 
858
747
  The host's running row, its divider and its live region stay as the host draws
859
748
  them. Its whale tail is DeepSeek's own character, so under the Claude brand the
860
- skin hides it and leads the line with its own loader: 4px dots of the accent in
861
- force in an 18 × 14 box, playing one of three motions
862
- (`features/chat-wait/wait-loader.ts`). Three dots bounce — each rising the
863
- skin's 8px and flattening against the floor as it leaves, half a second a bounce
864
- with the second dot starting two fifths of the period behind the first and the
865
- third three fifths. Three dots walk three lines through one point, 120° apart
866
- with the second line run the other way: each dot is a simple harmonic along its
867
- own line, the whole line taken in 1.25s, at the dot's own size at either end and
868
- shrinking to 0.55 as it crosses the middle, so no frame of the line motion draws
869
- a dot larger than the bouncing one. Four dots swap the corners of a 45 : 55
870
- rectangle, one pair and then the other, 2.4s a turn. The loader opens on one of
871
- the three at random and every 10s another takes over. A change never cuts and
872
- the box never empties: each dot of the motion leaving waits for its own still
873
- moment — the top of a hop, an end of a walk, a corner between two swaps — and the
874
- dot it is paired with in the motion arriving takes its place there, at its size,
875
- then travels into its own path over 480ms on the harmonic curve. The dots are
749
+ skin hides it and leads the line with its own loader: dots of the accent in force
750
+ in a small box, playing one of three motions
751
+ (`features/chat-wait/wait-loader.ts`). Three dots bounce and flatten against the
752
+ floor as they leave; three dots walk three lines through one point, each a simple
753
+ harmonic along its own line, shrinking as it crosses the middle so no frame of
754
+ the line motion draws a dot larger than the bouncing one; four dots swap the
755
+ corners of a rectangle, one pair and then the other. The loader opens on one of
756
+ the three at random and lets another take over on a slow clock. A change never
757
+ cuts and the box never empties: each dot of the motion leaving waits for its own
758
+ still moment — the top of a hop, an end of a walk, a corner between two swaps —
759
+ and the dot it is paired with in the motion arriving takes its place there, at
760
+ its size, then travels into its own path on the harmonic curve. The dots are
876
761
  paired so they travel least; from three dots to four one splits in two, and from
877
762
  four to three the spare one glides onto its partner and is gone where the two
878
- meet. Under the
879
- other brands the host's whale stands. The host's own
880
- shimmering words step aside for the skin's line beside it: the wording in the
881
- secondary label at 14px on a 24px line,
882
- the turn's clock 8px later in the caption at 13px with tabular figures, and the
883
- "No response yet" badge 8px after that (11px on a 16px line, 0 6px padding, a 5px
884
- radius, the warning label over 16% of itself). The line takes the row's own
885
- width and gives way inside it: the words are carried whole, so a clause the row
886
- has no room for is clipped where the room ends and the copy draws an ellipsis,
887
- with the loader, the clock and the badge keeping their places. The wording wears
888
- one of two paints, whichever the Motion tab's Waiting-line animation holds: a
889
- highlight band swept across it the way the host's own rows sweep — 1.5s a pass
890
- with the band resting the last third of it, in the host's shimmer tint — or a
891
- colour wash, the seven radial washes of `packages/client/src/shared/motion.css`
892
- over a grey ramp, orbiting the glyph box (5s an orbit while the hue turns a full
893
- circle over 4s), which is the effect transitions.dev ships as its Pro gradient
894
- text. A wording that changes runs both halves of the swap at once and is over in
895
- 0.2s: the old one
896
- leaves upward with a 2px blur while the new one, parked one gap, enters from 4px
897
- below; under Reduced motion the band is left out, the wash holds its still frame,
898
- and the wording is written at once. The Motion tab's preview cards
899
- wear these same two paints (`packages/client/src/features/settings/settings.css`)
900
- and take turns between a state word and the host's own word for a wait, so a card
901
- shows the wording swap as well as the paint.
763
+ meet. Under the other brands the host's whale stands.
764
+
765
+ The host's own shimmering words step aside for the skin's line beside it: the
766
+ wording in the secondary label on a line of its own, the turn's clock after it in
767
+ the caption with tabular figures, and the "No response yet" badge after that. The
768
+ line takes the row's own width and gives way inside it: the words are carried
769
+ whole, so a clause the row has no room for is clipped where the room ends and the
770
+ copy draws an ellipsis, with the loader, the clock and the badge keeping their
771
+ places. The wording wears one of two paints, whichever the Motion tab's
772
+ Waiting-line animation holds: a highlight band swept across it the way the host's
773
+ own rows sweep, or a colour wash — the radial washes of
774
+ `packages/client/src/shared/motion.css` over a grey ramp, orbiting the glyph box,
775
+ the effect transitions.dev ships as its Pro gradient text. A wording that changes
776
+ runs both halves of the swap at once: the old one leaves upward with a slight
777
+ blur while the new one enters from below; under Reduced motion the band is left
778
+ out, the wash holds its still frame, and the wording is written at once. The
779
+ Motion tab's preview cards wear these same two paints
780
+ (`packages/client/src/features/settings/settings.css`) and take turns between a
781
+ state word and the host's own word for a wait, so a card shows the wording swap
782
+ as well as the paint.
902
783
 
903
784
  With the chat area's own follow on, the row is pinned while the conversation
904
785
  follows its tail, at the place that follow's glide comes to rest — the
905
- composer's own height, the fade band above it and the host's 16px transcript
906
- padding — with the canvas carried on down to the composer's top, since the
907
- host's clip leaves a strip under the row where the line a stream slide brings
908
- up would otherwise show below the line. The pin stands only while the host
909
- reports the tail (or the stream glide holds the position) and steps aside for
910
- the reader's own scrolling.
786
+ composer's own height, the fade band above it and the host's transcript padding —
787
+ with the canvas carried on down to the composer's top, since the host's clip
788
+ leaves a strip under the row where the line a stream slide brings up would
789
+ otherwise show below the line. The pin stands only while the host reports the
790
+ tail (or the stream glide holds the position) and steps aside for the reader's
791
+ own scrolling.
792
+
793
+ ## Process rows · 过程行
794
+
795
+ An open process group lays its rows out one of two ways, whichever the Main
796
+ window tab's Process rows card holds. Flat is the host's own list, an icon
797
+ leading each row. Column is Claude Code's: the group's body becomes one card —
798
+ the 1px card border and the ladder's 8px radius — and each row is one line of it,
799
+ 36px tall, inset 12px, with the 0.5px divider between rows and none above the
800
+ first. No icon leads a row; a row that opens has a chevron trailing its words in
801
+ the same ink, turning down while the row stands open, and its details open
802
+ inside the card under it. The group's body has no maximum height or scrollbar;
803
+ an opened thought keeps its own `min(400px, 50vh)` scrolling area. A trailing
804
+ thought adds 6px below its line or details, leaving room above the card's bottom
805
+ edge. A thought reads as the thought itself and a generic call as its tool and
806
+ purpose, the kind word and its separator set aside; a named call keeps its verb
807
+ (查找文件 · \*.md), and a row with no description keeps its title. The header
808
+ with its counted line stays above the card, outside it. The Main window tab's
809
+ two cards draw a small group in each layout with these same
810
+ rules (`features/process-column/process-column.css`).
911
811
 
912
812
  ## Home layouts · 首页版面
913
813
 
914
814
  The new-conversation page has two arrangements. `homeLayout` (settings: Home
915
815
  layout) writes `data-dsh-claude-home-layout` onto `<body>` and
916
- `packages/client/src/features/home/home-panel.css` branches on it. Both are the host's own
917
- hero markup — the greeting, the workspace row, the dock and the composer card
918
- inside `…_composerStack …_composerHero` — so only the arrangement differs.
816
+ `packages/client/src/features/home/home-panel.css` branches on it. Both are the
817
+ host's own hero markup — the greeting, the workspace row, the dock and the
818
+ composer card inside the composer stack — so only the arrangement differs.
919
819
 
920
820
  | | Classic | Studio |
921
821
  |---|---|---|
922
- | greeting | centred, 44.2px serif, brand mark on its line | top left, one fixed line naming the user ("What's up next, …?") in the 20px sans UI face, a 21px brand mark on its line |
923
- | composer card | vertically centred in the scroll body | the conversation's single-line inline form, resting on the window's bottom edge (16px foot) |
822
+ | greeting | centred, in the display serif, brand mark on its line | top left, one fixed line naming the user, in the sans UI face, brand mark on its line |
823
+ | composer card | vertically centred in the scroll body | the conversation's single-line inline form, resting on the window's bottom edge |
924
824
  | between them | — | the usage panel |
925
825
  | permission control | the segment group | the conversation's trigger and its card |
926
826
  | workspace / preset row | the card's footer tray | hairline chips directly above the card |
927
- | column width | the hero's centred box | a 720px composer column; the greeting and the panel form a 480px block against its left edge |
827
+ | column width | the hero's centred box | a narrower composer column, with the greeting and the panel forming one block against its left edge |
828
+
829
+ The Appearance tab's cards draw these arrangements as miniature pages rather
830
+ than placeholder bars, exaggerating the content column and type for legibility
831
+ at that size. The studio greeting and dashboard stand slightly narrower than
832
+ the context row and composer. The dashboard uses the real panel, stat and
833
+ heat-cell classes, including all six stats and the seven-day-row grid; the
834
+ classic card keeps the centred brand greeting and the composer's footer tray
835
+ (`features/home/home-preview.css`).
928
836
 
929
- The studio composer is stamped `data-composer-variant="inline"` (composer.ts's
837
+ The studio composer is stamped `data-composer-variant="inline"` (the composer's
930
838
  pass reads the same preference), so the home card is drawn by the conversation's
931
- single-line stylesheet. The classic tray rules in `features/composer/card.css` are scoped
932
- away from studio with `:not([data-dsh-claude-home-layout="studio"])`. That is
933
- deliberate: they set `order: 1` / `order: 2` on the same boxes, and a competing
934
- rule would have to be out-specified rather than merely reordered — scoping them
935
- removes the contest.
839
+ single-line stylesheet. The classic tray rules in `features/composer/card.css`
840
+ are scoped away from studio on purpose: they set `order` on the same boxes, and a
841
+ competing rule would have to be out-specified rather than merely reordered —
842
+ scoping them removes the contest.
936
843
 
937
844
  **The usage panel.** It is a `conversation.input.dock` entry (the host's list
938
845
  seat between the greeting and the card; a list seat keys its entries by `id`),
@@ -943,89 +850,67 @@ workspace) has none; there the skin mounts the same component on a
943
850
  `display: contents` element of its own (`.dsh-claude-home-seat`) and drops it
944
851
  once the host's dock arrives. A window too short for the whole panel shrinks the
945
852
  panel, never the page: the greeting keeps the top edge, the chips and the card
946
- keep the bottom, and the panel scrolls between them with a 32px fade over its
947
- bottom edge that lifts over the last 32px of travel.
853
+ keep the bottom, and the panel scrolls between them under a fade over its bottom
854
+ edge.
948
855
 
949
856
  The cold start card waits for a workspace: the host marks it with a dashed ring
950
857
  and makes every control on it inert, so a press anywhere opens the workspace
951
- picker. In the studio form the ring sits on the single-line input box itself
952
- (the host's dashed stroke and colours, flat, no drop shadow), and the permission
953
- trigger fills the card's empty mode strip on the preset a new session starts in
954
- — disabled, like the rest of the card's controls.
955
-
956
- The panel is Claude Code's dashboard shape: a flat warm-gray wash (a
957
- `color-mix` of the label tone at 4% over the card tone, which flips with the
958
- theme on its own), an Overview/Models tab pair on the left of its head, and the
959
- All/30d/7d range pills on the right. The active tab and range pill are a gray
960
- chip one step below that wash — half the radius the 20px control would round to,
961
- so it reads as a rounded rectangle rather than a full pill. Overview carries six
962
- stat cells in a 3×2 grid under Claude Code's names (Sessions, Messages, Total
963
- tokens, Active days, Peak hour, Favorite model) whose tiles sit one clear step
964
- deeper than the panel (15% of the label tone) and set a 12px label over a 13px
965
- bold figure — the figure stays barely above its own label, which is also what
966
- lets a long model id such as deepseek-v4.1-flash sit on one line; the favourite
967
- model is a name, not a figure, and keeps the regular weight. Messages are the
968
- settled calls. Once the picked range's total passes one
969
- book, the yardstick line appears under the grid: sixty-five books from Tao Te
970
- Ching (7.6k tokens) to In Search of Lost Time (1.6M), the public-domain ones
971
- measured by feeding their full text to the o200k_base tokenizer (the English
972
- original or the standard English translation; Faust in German) and the rest
973
- estimated (1.3 tokens a word for English, 0.96 tokens a character for modern
974
- Chinese prose). The line states the rounded multiple as a share of the book
975
- ("~2× the tokens in Moby-Dick"), never as an excess over it.
976
- The book is drawn afresh each time the page comes back to the new-conversation
977
- hero, and a range that has not reached it steps down to the longest book it has
978
- passed, so the range pills keep the same book whenever the totals allow. The heat
979
- grid takes one
980
- equal column per week (twenty-six weeks), square cells from a 3px gutter, in
981
- Claude Code's blue data ramp (`#3b6ecf` mixed in at the shares `HOME_HEAT_RAMP`
982
- in `packages/client/src/constants.ts` lists, over the neutral empty cell); because the columns are fractions of the panel's own width, the newest
983
- week can never fall past the edge. Hovering a cell raises Claude Code's day tip
984
- after a short rest: a solid pill in the label ink with the canvas tone for text
985
- (so it inverts with the theme), 13px medium, reading the day in the shell's
986
- language and the tokens that day spent ("Sep 9 — 15.9k tokens"), the same figure
987
- the cell's heat is bucketed from. The grid carries one pill and moves it onto the
988
- day the pointer names, so a crossing between two cells changes its words in place
989
- and leaves it standing over the gutters between them rather than building and
990
- dropping a box for every cell; it is centred on its day except where its own
991
- width would carry it past the panel's edge, and there it lines up with that
992
- cell's outer edge so it stays inside the panel, and it stands above its cell
993
- except where the cell holds more room below it, and there it hangs under the cell
994
- so it never reaches over the grid's top edge across the stat cells; its width is
995
- taken from the words it carries and its height from its own box
996
- (`packages/client/src/features/home/heat-tip.ts`).
997
- Models is Claude Code's own shape: one column
998
- per day of the chart's thirty-day window, stacked from the axis up with each
999
- model's slice in its rank's colour (ranks past the ramp share its last, grey
1000
- step), four gridlines with their token labels in a 34px left gutter and every
1001
- third column's date under it in the shell's language ("Aug 26"), and beneath it
1002
- the ranked list — swatch, model name, the input/output split, and the share of
1003
- the models shown — folding past six rows behind one "show more" row, which turns
1004
- into "show less" once the list is open. The chart
1005
- and the list write counts Claude Code's way: one decimal at most, no trailing
1006
- ".0", a lowercase k ("109M", "963.6k"). The list reads the roll-up's per-model
1007
- buckets; the chart reads its per-day per-model map, so a day with nothing
1008
- attributed to a model draws no stack. Each column is sized to its day's total
1009
- against the axis top and each slice to its share of the day, so the column's
1010
- rounding sits on the top of the stack. A range window filters the
1011
- tiles (the peak hour included), the yardstick line and the model list; the heat
1012
- grid and the chart keep their own windows.
858
+ picker. In the studio form the ring sits on the single-line input box itself (the
859
+ host's dashed stroke and colours, flat, no drop shadow), and the permission
860
+ trigger fills the card's empty mode strip on the preset a new session starts in —
861
+ disabled, like the rest of the card's controls.
862
+
863
+ The panel is Claude Code's dashboard shape: a flat warm-gray wash that flips with
864
+ the theme on its own, an Overview/Models tab pair on the left of its head, and the
865
+ All/30d/7d range pills on the right. The active tab and range pill are a gray chip
866
+ one step below that wash — half the radius the control would round to, so it
867
+ reads as a rounded rectangle rather than a full pill. Overview carries six stat
868
+ cells in a 3×2 grid under Claude Code's names (Sessions, Messages, Total tokens,
869
+ Active days, Peak hour, Favorite model), whose tiles sit one clear step deeper
870
+ than the panel, a label over a bold figure that stays barely above its own label
871
+ — which is also what lets a long model id sit on one line; the favourite model is
872
+ a name, not a figure, and keeps the regular weight. Messages are the settled
873
+ calls.
874
+
875
+ Once the picked range's total passes one book, the yardstick line appears under
876
+ the grid. The books it measures and their counts are in
877
+ `packages/client/src/features/home/home-figures.ts`; the line states the rounded
878
+ multiple as a share of the book ("~2× the tokens in Moby-Dick"), never as an
879
+ excess over it. The book is drawn afresh each time the page comes back to the
880
+ new-conversation hero, and a range that has not reached it steps down to the
881
+ longest book it has passed, so the range pills keep the same book whenever the
882
+ totals allow.
883
+
884
+ The heat grid takes one equal column per week, square cells from a gutter, in
885
+ Claude Code's blue data ramp over the neutral empty cell; because the columns are
886
+ fractions of the panel's own width, the newest week can never fall past the edge.
887
+ Hovering a cell raises Claude Code's day tip after a short rest: a solid pill in
888
+ the label ink with the canvas tone for text (so it inverts with the theme),
889
+ reading the day in the shell's language and the tokens that day spent, the same
890
+ figure the cell's heat is bucketed from. The grid carries one pill and moves it
891
+ onto the day the pointer names, so a crossing between two cells changes its words
892
+ in place and leaves it standing over the gutters between them; it is centred on
893
+ its day except where its own width would carry it past the panel's edge, and
894
+ stands above its cell except where the cell holds more room below it.
895
+
896
+ Models is Claude Code's own shape: one column per day of the chart's window,
897
+ stacked from the axis up with each model's slice in its rank's colour (ranks past
898
+ the ramp share its last, grey step), gridlines with their token labels, and every
899
+ third column's date under it in the shell's language, and beneath it the ranked
900
+ list — swatch, model name, the input/output split, and the share of the models
901
+ shown — folding past a short list behind one "show more" row, which turns into
902
+ "show less" once the list is open. The chart and the list write counts Claude
903
+ Code's way: one decimal at most, no trailing ".0", a lowercase k. Each column is
904
+ sized to its day's total against the axis top and each slice to its share of the
905
+ day, so the column's rounding sits on the top of the stack. A range window filters
906
+ the tiles (the peak hour included), the yardstick line and the model list; the
907
+ heat grid and the chart keep their own windows.
1013
908
 
1014
909
  Two sources, in this order: the host half's usage route, then the session list's
1015
- own projection block (`tokenUsage`, `modelSelection`, `sessionListMetadata`).
1016
- The second answers in a few milliseconds and carries per-model totals without the
1017
- four buckets, which is what the list falls back to when the first cannot answer.
1018
- The first reads the cost-meter ledger for the days it knows — its
1019
- `<provider>:<model>` split becomes the per-model cells, one model across providers
1020
- merged into one — takes the dates past the ledger's newest day from the accurate
1021
- per-event fold, and keeps the two apart by date rather than adding them; the
1022
- sessions behind the ledger's days and the logs written on the folded ones are one
1023
- union. Both carry each day's session ids, which a range window unions into one
1024
- distinct session count. Only the fold knows the settlement hours: it keeps one
1025
- hour histogram per day, which a range window sums into its own peak hour, and the
1026
- ledger's own days carry none — the ledger's figures land first, and the peak hour
1027
- of the folded days lands a moment later. The panel names
1028
- which one it drew from, and a figure neither can answer is a dash.
910
+ own projection block (`tokenUsage`, `modelSelection`, `sessionListMetadata`). The
911
+ session list carries per-model totals without the four buckets, which is what the
912
+ list falls back to when the route cannot answer. The panel names which one it
913
+ drew from, and a figure neither can answer is a dash.
1029
914
 
1030
915
  The skeleton keeps the frame's geometry — six fixed-size stat cells, a heat grid
1031
916
  of a fixed cell count, and on the models tab the chart's frame with three
@@ -1038,69 +923,39 @@ placeholder, a zero day draws the grid's own base tone.
1038
923
  Claude Code's pixel crab is the mascot under the Claude brand (or when picked in
1039
924
  the Mascot row). It stands where Deepy does — the composer card's top edge on
1040
925
  both home layouts, the input area or the panel that replaces its card in a
1041
- conversation (unless Where it appears keeps it to the home page) — and follows
1042
- the same states with the same keys. It is drawn on a 52×36 grid of cells at 2px
1043
- a cell, crisp (`pixelated`, 104×72px): feet on the bottom row, on the edge of
1044
- what it stands on; the right claw four cells in from the grid's right edge, so
1045
- it stands 8px inside the card's right edge, where the 18px corner starts to
1046
- round, and the four spare cells take a lean, a note or a sparkle. The resting
1047
- crab — shell 16×12 cells, claws 4×4, eyes 2×2, four legs 2×4 — is 48×32px
1048
- (columns 24–47, rows 20–35) and only it takes the pointer. The shell is the clay
1049
- accent `#d97757` in both themes, the side-on back `#b9603f`, the eyes `#141413`;
1050
- the props (the laptop, the thought bubble, letters, notes, the hard hat) are an
1051
- ink mask filled with the tertiary label ink. Every frame lasts 80ms, Claude
1052
- Code's pace. Its laptop frames are Claude Code's own routine; the rest are drawn
1053
- by `scripts/draw-crab.py` after the same character. With the animation choice
1054
- resolved to "reduced" each state holds its still frame; a click or a pull still
1055
- plays its reaction.
1056
-
1057
- | State | Animation | Still frame |
1058
- |---|---|---|
1059
- | idle, every 20–40 s a look around, a claw wave or Claude Code's whole laptop routine | `idle`, `idle-look`, `idle-wave`, `idle-laptop` | 0 |
1060
- | the model reasons or has not answered: eyes up to a thought bubble | `thinking` | 18 |
1061
- | the model writes or tools run: 1 / 2 / 3+ sessions at work (typing / headphones / typing in a hard hat) | `typing` / `music` / `building` | 0 / 0 / 0 |
1062
- | subagents running: 1 / 2+ (headphones / conducting) | `music` / `conducting` | 0 / 0 |
1063
- | a compaction runs: squashed | `compacting` | 3 |
1064
- | an approval, a question or a plan review waits: an exclamation mark | `notification` | 0 |
1065
- | a tool call or a turn failed (4.8 s): crossed eyes, a shake | `error` | 4 |
1066
- | a turn or a compaction finished (5.2 s): hops, claws up | `happy` | 3 |
1067
- | a quiet minute / the next pointer move or key | `sleeping` / `waking` | 0 / 11 |
1068
- | a click on the left / right half, four quick clicks, a pull | `poke-left` / `poke-right`, `tickle`, `drag` | 0 |
926
+ conversation (unless Where it appears keeps it to the home page) — and follows the
927
+ same states with the same keys. Its frames, its crop boxes and the frame each
928
+ state holds under reduced motion are in
929
+ `packages/client/src/features/mascot/sheets.ts`, the table the build cuts the
930
+ sheets from; the art is drawn by `scripts/draw-crab.py` after Claude Code's
931
+ character. The shell keeps the clay accent in both themes and the props (the
932
+ laptop, the thought bubble, letters, notes, the hard hat) are an ink mask filled
933
+ with the tertiary label ink, so the mascot reads as part of the card it stands on.
934
+
935
+ Only the resting crab takes the pointer. With the animation choice resolved to
936
+ "reduced" each state holds its still frame; a click or a pull still plays its
937
+ reaction.
1069
938
 
1070
939
  ## Deepy · 小鲸鱼
1071
940
 
1072
- Under the DeepSeek brand (or when picked in the Mascot row) the mascot is
1073
- Deepy, the pixel whale of the Deepy theme pack: a 52×52 grid of logical pixels drawn at 2px a pixel (a 104px
1074
- square), its ground line — row 48.5, the middle of its shadow — on the top edge
1075
- of what it stands on. Every sheet's crop box reaches 3px past that ground line,
1076
- and the whale paints over the host's cards, so its box is lifted by those 3px:
1077
- the sprite's last row lands on the card's top edge, and the shadow's tail stays
1078
- off the card instead of being stamped across it. Its box is 8px in from the
1079
- right end of the card it stands over. On the home page that is the composer card
1080
- (both layouts); in a conversation it is the whole input area, and the panel that
1081
- replaces the card while the reader is asked for something (those panels start
1082
- 6–8px above their card). Every frame lasts 50ms. The sheets carry five device
1083
- pixels to a logical pixel and are scaled down smoothly, so the whale stays crisp
1084
- from 1× to 2× screens without `image-rendering: pixelated`, which would drop
1085
- rows on a downscale. The body is Deepy's blue `#4E6FFF` with a navy `#142660`
1086
- outline and a white belly in both themes; on the dark canvas the outline and the
1087
- soft shadow recede and the blue body carries the shape. Only the resting body
1088
- (columns 12–44, rows 30–48) takes the pointer. With the animation choice
1089
- resolved to "reduced" each state holds its still frame; a click or a pull still
1090
- plays its reaction.
1091
-
1092
- | State | Animation | Still frame |
1093
- |---|---|---|
1094
- | idle, every 20–40 s a look around or a spout | `idle`, `idle-look`, `idle-spout` | 0 |
1095
- | the model reasons or has not answered | `thinking` | 20 |
1096
- | the model writes or tools run: 1 / 2 / 3+ sessions at work | `typing` / `music` / `building` | 16 / 0 / 0 |
1097
- | subagents running: 1 / 2+ | `music` / `conducting` | 0 / 6 |
1098
- | a compaction runs | `compacting` | 20 |
1099
- | an approval, a question or a plan review waits | `notification` | 12 |
1100
- | a tool call or a turn failed (4.8 s) | `error` | 24 |
1101
- | a turn or a compaction finished (5.2 s) | `happy` | 44 |
1102
- | a quiet minute (no work, no pointer move or key) / the next pointer move or key | `sleeping` / `waking` | 10 / 29 |
1103
- | a click on the face / the tail, four quick clicks, a pull | `poke-left` / `poke-right`, `tickle`, `drag` | 0 |
941
+ Under the DeepSeek brand (or when picked in the Mascot row) the mascot is Deepy,
942
+ the pixel whale of the Deepy theme pack. Its sheets, crop boxes and still frames
943
+ are in `features/mascot/sheets.ts`, like the crab's, and the two share one state
944
+ machine, so a state added for one is a state for both. Its body is Deepy's blue
945
+ with a navy outline and a white belly in both themes; on the dark canvas the
946
+ outline and the soft shadow recede and the body carries the shape.
947
+
948
+ Its ground line sits on the top edge of what it stands on, and its crop box
949
+ reaches past that line, so the box is lifted by that margin: the sprite's last
950
+ row lands on the card's top edge and the shadow's tail stays off the card instead
951
+ of being stamped across it. Its box is inset from the right end of the card it
952
+ stands over — on the home page the composer card under both layouts, in a
953
+ conversation the whole input area, and the panel that replaces the card while the
954
+ reader is asked for something. The sheets are scaled down smoothly so the whale
955
+ stays crisp from 1× to 2× screens without `image-rendering: pixelated`, which
956
+ would drop rows on a downscale. Only the resting body takes the pointer. With the
957
+ animation choice resolved to "reduced" each state holds its still frame; a click
958
+ or a pull still plays its reaction.
1104
959
 
1105
960
  ## Implementation notes
1106
961
 
@@ -1118,4 +973,3 @@ plays its reaction.
1118
973
  whole: `packages/client/src/shared/page-scale.ts` gives each one a space one pixel to one
1119
974
  pixel with the window (D81), and a rule that caps a card against the window reads
1120
975
  `--dsh-claude-page-height` / `--dsh-claude-page-width` rather than `vh` / `vw`.
1121
-