@godxjp/ui 33.9.1 → 33.11.2

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.
Files changed (43) hide show
  1. package/DESIGN.md +10 -7
  2. package/README.md +3 -3
  3. package/agent/DESIGN.md +10 -7
  4. package/agent/START-HERE.md +4 -4
  5. package/agent/components/Cascader.json +1 -0
  6. package/agent/components/DatePicker.json +1 -0
  7. package/agent/components/Input.json +1 -0
  8. package/agent/components/NumberInput.json +1 -0
  9. package/agent/components/PasswordInput.json +1 -0
  10. package/agent/components/SearchInput.json +1 -0
  11. package/agent/components/Select.json +1 -0
  12. package/agent/components/TimePicker.json +1 -0
  13. package/agent/components/Topbar.json +8 -2
  14. package/agent/components/TopbarItem.json +7 -1
  15. package/agent/components/TreeSelect.json +1 -0
  16. package/agent/components.json +24 -3
  17. package/agent/index.json +5 -5
  18. package/agent/llms.txt +6 -6
  19. package/agent/tokens.json +54 -36
  20. package/dist/components/data-entry/input.js +16 -1
  21. package/dist/components/data-entry/search-input.js +1 -1
  22. package/dist/components/layout/topbar-item.d.ts +1 -0
  23. package/dist/components/layout/topbar-item.js +9 -4
  24. package/dist/components/layout/topbar.d.ts +1 -1
  25. package/dist/components/layout/topbar.js +25 -3
  26. package/dist/contracts/measurement.json +1 -1
  27. package/dist/props/components/layout.prop.d.ts +13 -0
  28. package/dist/props/registry.d.ts +9 -1
  29. package/dist/props/registry.js +10 -0
  30. package/dist/styles/control.css +131 -3
  31. package/dist/styles/layers.json +1 -1
  32. package/dist/styles/shell-layout.css +38 -0
  33. package/dist/styles.css +1 -1
  34. package/dist/tokens/components/card.css +3 -3
  35. package/dist/tokens/components/control.css +8 -5
  36. package/dist/tokens/components/form.css +1 -1
  37. package/dist/tokens/components/shell.css +3 -0
  38. package/dist/tokens/components/table.css +1 -1
  39. package/dist/tokens/foundation.css +11 -11
  40. package/dist/tokens/semantic/layout.css +3 -3
  41. package/docs/TOKENS.md +73 -1
  42. package/package.json +2 -2
  43. package/scripts/visual-audit.mjs +33 -3
package/agent/tokens.json CHANGED
@@ -132,10 +132,10 @@
132
132
  "value": "30 7% 93%"
133
133
  },
134
134
  {
135
- "description": "CONTROL BOUNDARY role — deliberately NOT the same value as --border (gh#315). The two roles answer different questions and are held to different bars: · --border is DECORATIVE chrome — table rules, card edges, section dividers. WCAG 2.2 SC 1.4.11 does not reach it (nothing about a divider is \"required to identify a component or its state\"), and darkening it would make the JP-dense table grid roar. It stays exactly where it is. · --input is the boundary that IS the control — the 1px edge of Input, Textarea, Select, the outline Button, TagInput, the composite date field, the topbar search. `--input` also fills the Switch's unchecked track. A text field here has NO fill of its own (`background: hsl(var(--background))`) and NO shadow to speak of, so that edge is the only thing that says \"you may type here\". That is squarely SC 1.4.11's 3:1 minimum. They shipped sharing `30 7% 83%`, which measured 1.46:1 on --background/--card — the one live WCAG failure a full DXS Platform sweep turned up. 53% clears 3:1 with margin on every surface a control actually sits on, not just the plain page (the gh#299 lesson: a value that passes on a card and fails inside a filter bar is not a line anyone can defend): --background/--card/--popover 3.47:1 · --muted/--secondary 3.18:1 · zebra stripe 3.35:1 · row hover 3.32:1. It also lifts the Switch's unchecked track from 1.46:1 to 3.47:1 against both the page and its own white thumb, so \"off\" is finally a visible state. Guarded by src/tokens/__tests__/input-boundary-contrast.test.ts. #90877f — control boundary, ≥3:1 (WCAG 2.2 SC 1.4.11)",
135
+ "description": "CONTROL BOUNDARY role — deliberately NOT the same value as --border (gh#315). The two roles answer different questions and are held to different bars: · --border is DECORATIVE chrome — table rules, card edges, section dividers. WCAG 2.2 SC 1.4.11 does not reach it (nothing about a divider is \"required to identify a component or its state\"), and darkening it would make the JP-dense table grid roar. It stays exactly where it is. · --input is the boundary that IS the control — the 1px edge of Input, Textarea, Select, the outline Button, TagInput, the composite date field, the topbar search. `--input` also fills the Switch's unchecked track. A text field here has NO fill of its own (`background: hsl(var(--background))`) and NO shadow to speak of, so that edge is the only thing that says \"you may type here\". That is squarely SC 1.4.11's 3:1 minimum. They shipped sharing `30 7% 83%`, which measured 1.46:1 on --background/--card — the one live WCAG failure a full DXS Platform sweep turned up. The modern refresh uses 51% lightness: the previous 53% passed the translucent hovered row but fell below 3:1 on the full --accent ground. The boundary test now includes that ground in both themes. Guarded by src/tokens/__tests__/input-boundary-contrast.test.ts. #8b8279 — control boundary, ≥3:1 (WCAG 2.2 SC 1.4.11)",
136
136
  "name": "--input",
137
137
  "tier": "foundation",
138
- "value": "30 7% 53%"
138
+ "value": "30 7% 51%"
139
139
  },
140
140
  {
141
141
  "description": "STATUS FILLS AND THE INK THAT RIDES ON THEM. The FILLS are the palette and they do not move: 若竹, 山吹, 群青, 朱 are named colours, every solid success/attention surface in every consumer is one of them, and a chip that changes hue on upgrade is a different design. What moved is the INK, and only where it failed to be readable (gh#643). Measured from these very tokens (src/tokens/__tests__/status-fill-contrast.test.ts, and confirmed in Chromium by `check:frame-axe` on /isolate/foundation-colors at 320/375/1440): role fill WAS near-white ink dark ink (48 8% 13%) success #68be8d 2.19:1 ✗ 7.08:1 ✓ attention #eb6101 3.32:1 ✗ 4.68:1 ✓ warning #f8b500 1.74:1 ✗ 8.91:1 ✓ (already dark — the precedent) primary #18181b 17.45:1 ✓ (the GoDX preset #008066: 4.86:1) destructive … 6.10:1 ✓ info #4c6cb3 5.00:1 ✓ Two of the eight light-theme fills carried an ink their own lightness cannot support, and the package already knew the answer twice over: --warning-foreground has always been this same near-black on the equally pale 山吹, and the DARK theme flips --success-foreground / --attention-foreground to 48 9% 9% for exactly this reason. The light block was the outlier. REJECTED — darkening 若竹 and 朱 so the near-white ink clears 4.5:1. A fill needs luminance ≤ 0.183 for that; 若竹 sits at 0.422, so \"keep the white text\" means a different green, not a darker one. That is the palette change this deliberately is not. 若竹 #68be8d",
@@ -525,13 +525,13 @@
525
525
  "description": "Foundation tokens: color, typography, raw spacing, ratio, radius, shadow.",
526
526
  "name": "--shadow-md",
527
527
  "tier": "foundation",
528
- "value": "0 4px 6px -1px rgb(var(--shadow-color) / 0.1), 0 2px 4px -2px rgb(var(--shadow-color) / 0.1)"
528
+ "value": "0 6px 16px -4px rgb(var(--shadow-color) / 0.08), 0 1px 3px 0 rgb(var(--shadow-color) / 0.08)"
529
529
  },
530
530
  {
531
531
  "description": "Foundation tokens: color, typography, raw spacing, ratio, radius, shadow.",
532
532
  "name": "--shadow-lg",
533
533
  "tier": "foundation",
534
- "value": "0 10px 15px -3px rgb(var(--shadow-color) / 0.1), 0 4px 6px -4px rgb(var(--shadow-color) / 0.1)"
534
+ "value": "0 12px 28px -6px rgb(var(--shadow-color) / 0.1), 0 2px 6px -2px rgb(var(--shadow-color) / 0.08)"
535
535
  },
536
536
  {
537
537
  "description": "Foundation tokens: color, typography, raw spacing, ratio, radius, shadow.",
@@ -582,16 +582,16 @@
582
582
  "value": "initial"
583
583
  },
584
584
  {
585
- "description": "── THE SWITCH ─────────────────────────────────────────────────────────────────────────── `--focus-outline` is ONE 0/1 flag, and every painted focus length below multiplies by it: --focus-ring-width: calc(var(--focus-ring-weight) * var(--focus-outline)) — so setting it to 0 zeroes the whole indicator at once. There is deliberately no parallel mechanism: one multiplier is the only shape in which a kill switch cannot be routed around by a per-component rebind. IT SHIPS ON, as of gh#544 — it used to ship off, and that reversal is the point of this note. The old default was a real product decision: the indicator had been reported as intrusive and the owner accepted the cost, which was that this library did not satisfy WCAG 2.2 SC 2.4.7 Focus Visible (AA) and so could carry no JIS X 8341-3:2016 AA statement. What changed is not the principle but two measurements. FIRST, what the default actually produced. Counting `data-focus-outline` across the four real consumers of this package: `ql` sets it (3 files); `godx-task`, `godx-chat` and the platform app set it NOWHERE. Three of four products shipped with no keyboard-focus indicator at all — not chosen, because the switch is only discoverable by reading this file. SECOND, what the ON state now costs. It is the HAIRLINE field indicator described below, not the 3px outline the original complaint was about. The thing being turned on is no longer the thing that was turned off. The escape hatch is the same shape it always was, one attribute, now pointing the other way: <html data-focus-outline=\"off\"> (or AppProvider's equivalent) `data-focus-outline=\"on\"` still works and still means on, so every consumer that already set it is untouched. Every painted length below multiplies by `--focus-outline`, so with the switch off the outline resolves to `0px`, the halo's spread resolves to `0px`, and `--focus-field-shadow` is `none`. The `:focus-visible` selectors themselves stay exactly where they are: only the paint goes. ── THE ON WEIGHT ──────────────────────────────────────────────────────────────────────── `--focus-outline-weight` is the ON state's thickness, and it is deliberately the HAIRLINE (1px) rather than the heavy outline weight (3px). The heavy mark is what the complaint was about: a 3px opaque outline stacked on an already-shaded selected nav row reads as two treatments on one element. WCAG's AA bar for an indicator is CONTRAST (SC 1.4.11, 3:1), not thickness, so the weight can come all the way down to the FIELD indicator — one hairline of `--ring`, which measures at worst 5.30:1 light and 6.87:1 dark across every surface a control sits on — while the criterion is still met. The 3px weight is one token away: set `--focus-outline-weight: var(--stroke-lg)`. The hue is the focus hue itself, NOT `--primary-border`. That is the one place the ON state departs from the OUTLINE form, and it is forced: `--primary-border` for this seed is #a66de3, which reaches 3.49:1 on the page and 2.92:1 light / 1.03:1 dark on an accent panel, so it cannot satisfy SC 1.4.11 on every surface at any thickness. A mark that fails the criterion is not an indicator. the switch — 1 = on (shipped default since gh#544), 0 = off",
585
+ "description": "── THE SWITCH ─────────────────────────────────────────────────────────────────────────── `--focus-outline` is ONE 0/1 flag, and every painted focus length below multiplies by it: --focus-ring-width: calc(var(--focus-ring-weight) * var(--focus-outline)) — so setting it to 0 zeroes the whole indicator at once. There is deliberately no parallel mechanism: one multiplier is the only shape in which a kill switch cannot be routed around by a per-component rebind. IT SHIPS ON, as of gh#544 — it used to ship off, and that reversal is the point of this note. The old default was a real product decision: the indicator had been reported as intrusive and the owner accepted the cost, which was that this library did not satisfy WCAG 2.2 SC 2.4.7 Focus Visible (AA) and so could carry no JIS X 8341-3:2016 AA statement. What changed is not the principle but two measurements. FIRST, what the default actually produced. Counting `data-focus-outline` across the four real consumers of this package: `ql` sets it (3 files); `godx-task`, `godx-chat` and the platform app set it NOWHERE. Three of four products shipped with no keyboard-focus indicator at all — not chosen, because the switch is only discoverable by reading this file. SECOND, what the ON state now costs. It is the HAIRLINE field indicator described below, not the 3px outline the original complaint was about. The thing being turned on is no longer the thing that was turned off. The escape hatch is the same shape it always was, one attribute, now pointing the other way: <html data-focus-outline=\"off\"> (or AppProvider's equivalent) `data-focus-outline=\"on\"` still works and still means on, so every consumer that already set it is untouched. Every painted length below multiplies by `--focus-outline`, so with the switch off the outline resolves to `0px`, the halo's spread resolves to `0px`, and `--focus-field-shadow` is `none`. The `:focus-visible` selectors themselves stay exactly where they are: only the paint goes. ── THE ON WEIGHT ────────────────────────────────────────────────────── The modern default is a 2px mark with 2px offset. Hue remains --ring for at least 3:1 contrast on supported surfaces; navigation keeps its inset. Consumers can retune the two thickness knobs or use the existing opt-out. 1 = on, 0 = off",
586
586
  "name": "--focus-outline",
587
587
  "tier": "foundation",
588
588
  "value": "1"
589
589
  },
590
590
  {
591
- "description": "default = var(--stroke-hairline), resolved at the call site (gh#906)",
591
+ "description": "Foundation tokens: color, typography, raw spacing, ratio, radius, shadow.",
592
592
  "name": "--focus-outline-weight",
593
593
  "tier": "foundation",
594
- "value": "initial"
594
+ "value": "var(--stroke-md)"
595
595
  },
596
596
  {
597
597
  "description": "default = var(--focus-ring-color, var(--ring)), resolved at the focused element — a `:root` binding froze on the root's --ring, so a nested tenant scope never re-tinted the outline (gh#687; docs/TOKENS.md, the freeze rule)",
@@ -609,7 +609,7 @@
609
609
  "description": "Foundation tokens: color, typography, raw spacing, ratio, radius, shadow.",
610
610
  "name": "--focus-outline-offset",
611
611
  "tier": "foundation",
612
- "value": "0px"
612
+ "value": "2px"
613
613
  },
614
614
  {
615
615
  "description": "FOCUS RING — the two global knobs for the keyboard-focus affordance (WCAG 2.4.7 / 2.4.11). `--focus-ring-color` is the themeable hue (HSL components, defaults to --ring) and `--focus-ring-width` the standard thickness; override either ONCE — even scoped under a `[data-tenant]` — to retint or resize EVERY focus ring. `--focus-ring-color` is declared `initial` (NOT `var(--ring)`): a :root binding to a role var would freeze at the :root --ring and a scoped [data-tenant] override of --ring alone would never reach it. Every :focus-visible rule reads it with the live-role fallback `hsl(var(--focus-ring-color, var(--ring)))`, so the default re-resolves at the element that paints the ring while an explicit override still wins. default = the live --ring role",
@@ -1203,19 +1203,19 @@
1203
1203
  "description": "Motion — the primitive tier for animation, so enter/transition timing reads a token instead of a literal 0.5s / cubic-bezier(...) / translateY(10px) (cardinal rule #2). the reference design keeps motion LOW: short durations, a calm decelerate curve, a small reveal shift. Consumers honour prefers-reduced-motion at the call site.",
1204
1204
  "name": "--duration-fast",
1205
1205
  "tier": "foundation",
1206
- "value": "150ms"
1206
+ "value": "120ms"
1207
1207
  },
1208
1208
  {
1209
1209
  "description": "Foundation tokens: color, typography, raw spacing, ratio, radius, shadow.",
1210
1210
  "name": "--duration-base",
1211
1211
  "tier": "foundation",
1212
- "value": "250ms"
1212
+ "value": "180ms"
1213
1213
  },
1214
1214
  {
1215
1215
  "description": "Foundation tokens: color, typography, raw spacing, ratio, radius, shadow.",
1216
1216
  "name": "--duration-slow",
1217
1217
  "tier": "foundation",
1218
- "value": "500ms"
1218
+ "value": "320ms"
1219
1219
  },
1220
1220
  {
1221
1221
  "description": "Foundation tokens: color, typography, raw spacing, ratio, radius, shadow.",
@@ -1440,16 +1440,16 @@
1440
1440
  "value": "var(--primary)"
1441
1441
  },
1442
1442
  {
1443
- "description": "Semantic spacing — ONE coherent 4px grid (Tailwind/Material/Ant standard). All steps reference the linear `--space-*` scale (NOT the golden φ scale — golden ratio is for modular SIZE scales like type/radius, not the spacing grid; mixing the two left an incoherent density rhythm). The φ tokens remain available as an optional golden-layout utility, just not the spacing baseline. 24px — page gutter",
1443
+ "description": "Semantic spacing — ONE coherent 4px grid (Tailwind/Material/Ant standard). All steps reference the linear `--space-*` scale (NOT the golden φ scale — golden ratio is for modular SIZE scales like type/radius, not the spacing grid; mixing the two left an incoherent density rhythm). The φ tokens remain available as an optional golden-layout utility, just not the spacing baseline. 32px — page gutter",
1444
1444
  "name": "--space-page-x",
1445
1445
  "tier": "semantic",
1446
- "value": "var(--space-6)"
1446
+ "value": "var(--space-8)"
1447
1447
  },
1448
1448
  {
1449
- "description": "24px",
1449
+ "description": "32px",
1450
1450
  "name": "--space-page-y",
1451
1451
  "tier": "semantic",
1452
- "value": "var(--space-6)"
1452
+ "value": "var(--space-8)"
1453
1453
  },
1454
1454
  {
1455
1455
  "description": "16px",
@@ -1641,7 +1641,7 @@
1641
1641
  "description": "Modal scrim — the single backdrop colour shared by EVERY overlay (Dialog, AlertDialog, Sheet, Drawer, AppShell mobile nav). Themeable (rule #44) so a service tints the wash to its brand once, e.g. a navy `--overlay-background: rgb(12 26 49 / .55)`, instead of forking each overlay. HOW IT REACHES AN OVERLAY (gh#215 — it previously reached NOTHING): each surface declares its own knob `initial` and resolves a SHARE of this colour at the call site, `var(--dialog-overlay-background, color-mix(in srgb, var(--overlay-background) var(--dialog-overlay-alpha), transparent))`. That keeps the calibrated per-surface depth — a slide-in Sheet washes the page more lightly (40%) than a modal Dialog (60%) — while ONE override here retints every backdrop. Per docs/TOKENS.md the knobs must stay `initial`: a `:root` binding would freeze and a scoped `[data-tenant]`/`.dark` override of this token would never reach the overlay. Overlays are portaled, so a tenant scope must also sit on the portal container (see docs/CUSTOMER-THEMING.md).",
1642
1642
  "name": "--overlay-background",
1643
1643
  "tier": "semantic",
1644
- "value": "rgb(0 0 0 / 0.5)"
1644
+ "value": "rgb(0 0 0 / 0.4)"
1645
1645
  },
1646
1646
  {
1647
1647
  "description": "RECESSED SURFACE role (gh#399) — \"the ground a card sits on\", one step BEHIND `--card`. It is a ROLE, not a derivation: the two themes order their surfaces differently, so no single expression is correct on both. Light puts `--muted` (95%) behind `--card` (99%); dark puts it (18%) in FRONT of `--card` (12%), so the shipped `hsl(var(--muted) / 0.4)` composited to rgb(35,33,30) against a rgb(33,32,28) card — the page read 1.015:1 LIGHTER than the cards on it, i.e. the elevation inverted. No alpha fixes that: the mix runs from `--background` (correct) toward `--muted` (wrong), so anything above ~0.33 crosses the card. Each theme therefore states its own ground below. Light keeps the exact shipped composite (1.036:1 behind the card); dark takes the page `--background`, which measures 1.089:1 — matching the light theme's separation instead of inverting it.",
@@ -2415,7 +2415,7 @@
2415
2415
  "description": "Gap between the header and the body, and the body's own top padding — the breathing room under a title before content begins.",
2416
2416
  "name": "--card-space-body-y",
2417
2417
  "tier": "component",
2418
- "value": "var(--space-section-active)"
2418
+ "value": "var(--space-6)"
2419
2419
  },
2420
2420
  {
2421
2421
  "description": "Symmetric block padding for a SOLO content slot, so a composed shell can tune block and inline spacing independently without targeting CardContent. Declared `initial` for the SAME reason as --card-space-shell-y above: written as `var(--card-space-inset)` here it would freeze at the :root inset, and a card carrying [data-density=\"tight\"|\"cozy\"] (which override --card-space-inset ON THE CARD) would keep the :root value. The default now resolves at the call site through the chain solo-y → shell-y → inset, so a solo body follows the general block knob when only that is set, and an explicit solo-y still wins. default = var(--card-space-shell-y, var(--card-space-inset))",
@@ -2445,7 +2445,7 @@
2445
2445
  "description": "Card component tokens: card chrome derives from semantic layout tokens.",
2446
2446
  "name": "--card-title-font-size",
2447
2447
  "tier": "component",
2448
- "value": "var(--font-size-base)"
2448
+ "value": "calc(var(--font-size-base) * var(--font-size-ratio))"
2449
2449
  },
2450
2450
  {
2451
2451
  "description": "Card component tokens: card chrome derives from semantic layout tokens.",
@@ -2508,10 +2508,10 @@
2508
2508
  "value": "initial"
2509
2509
  },
2510
2510
  {
2511
- "description": "The DXS hi-fi baseline uses a quiet 10px data surface with one shadow-sm elevation layer. Consumers can still flatten or lift cards through this knob. `initial`, NOT `var(--shadow-sm)` — a role-mirror knob written here binds at `<html>` and a scope below root that restates the elevation ramp can never reach it (docs/TOKEN-RESOLUTION.md §3). The default now resolves on `.ui-card`, so a `[data-tenant]`/`[data-theme]` region that sets `--shadow-sm` moves its own cards and nothing else. gh#880 item 7. default = var(--shadow-sm), resolved at the call site",
2511
+ "description": "Flat resting cards: the hairline defines the surface; elevation is reserved for overlays. Set --card-shadow to a shadow value for opt-in elevation, or to initial to restore the live --shadow-sm fallback at the call site (including scoped shadow ramps).",
2512
2512
  "name": "--card-shadow",
2513
2513
  "tier": "component",
2514
- "value": "initial"
2514
+ "value": "none"
2515
2515
  },
2516
2516
  {
2517
2517
  "description": "Brand glow layer — invisible no-op at rest (rule #44). Paired AFTER --card-shadow in the surface box-shadow so a service can wash every card with the global glow, e.g. --card-glow: var(--shadow-glow), with no markup change.",
@@ -3147,7 +3147,7 @@
3147
3147
  "description": "Control primitive tokens: heights, horizontal padding, adjacent control sizes.",
3148
3148
  "name": "--control-height-default",
3149
3149
  "tier": "component",
3150
- "value": "var(--band-height-md)"
3150
+ "value": "var(--band-height-lg)"
3151
3151
  },
3152
3152
  {
3153
3153
  "description": "Control primitive tokens: heights, horizontal padding, adjacent control sizes.",
@@ -3234,10 +3234,10 @@
3234
3234
  "value": "var(--radius)"
3235
3235
  },
3236
3236
  {
3237
- "description": "Button corner radius — defaults to the button's historical `rounded-md` so nothing changes by default, but is its OWN knob so a service theme can retune the button radius INDEPENDENTLY of input/control radius (issue #124). The `var(--radius-md, …)` fallback is gh#888, NOT the same fix as this token's own freeze: Button reads this value through `rounded-[var(--button-radius)]`, a Tailwind utility (`@layer utilities`) that this package cannot give a fallback to from CSS — so this knob is deliberately NOT flipped to `initial` (that would still be unreachable from a scoped theme, gh#824 measured `--control-radius: 37px` on `:root` moving 0 buttons). What DOES need the fallback is the read of `--radius-md` itself: gh#888 unfreezes the φ tier globally, so without this fallback `--radius-md` resolves to nothing and `--button-radius` — and therefore every Button's corner — would go to 0. The formula keeps this declaration byte-identical to before gh#888 (still frozen at :root against --radius, same historical limitation).",
3237
+ "description": "Match the default 6px field corner while retaining an independent Button knob. The utility consumes a concrete value, so this remains a root alias; scoped services override --button-radius directly. Pill and sharp shapes keep their own tokens.",
3238
3238
  "name": "--button-radius",
3239
3239
  "tier": "component",
3240
- "value": "var(--radius-md, calc(var(--radius) / var(--radius-ratio)))"
3240
+ "value": "var(--radius)"
3241
3241
  },
3242
3242
  {
3243
3243
  "description": "THE BUTTON'S OWN SURFACE — one knob per painted variant (gh#880, standard §1). Every variant painted a bare role (`hsl(var(--primary))`, `hsl(var(--background))`, `hsl(var(--secondary))`, `hsl(var(--destructive))`) with no knob of its own, so the only way to retint a button was to retint the ROLE — and `--primary` is also the link ink, the focus mark and a chart series. There was no way to say \"this button is translucent\" without saying it about eight other things. WHY BUTTONS AT ALL. namethatui's source draws the line this library needed: glassmorphism is a decorative skin for ANY surface including controls, while Apple's Liquid Glass reserves glass for the floating control layer above opaque content. A glass page with a solid primary button on it is the Liquid Glass split applied backwards. FOUR FILLS, NOT SIX. `ghost` and `link` paint no resting surface, so they have nothing to knob. The HOVER fills are deliberately not knobs either: they already resolve from roles at their own call sites (`--primary-hover`, `--accent`, `--secondary`), so a theme reaches them today — the documented consequence being that a glass RESTING fill with an untouched hover gives a button that turns opaque under the pointer, and a theme that wants glass on both moves the role too. ONE BLUR FOR THE WHOLE COMPONENT, because the material is one decision per component and the variants are one component. `initial`, so unset there is no blur and no backdrop root. default = hsl(var(--primary)) at the call site",
@@ -3309,7 +3309,7 @@
3309
3309
  "description": "Control primitive tokens: heights, horizontal padding, adjacent control sizes.",
3310
3310
  "name": "--control-shadow",
3311
3311
  "tier": "component",
3312
- "value": "var(--shadow-xs)"
3312
+ "value": "none"
3313
3313
  },
3314
3314
  {
3315
3315
  "description": "Control primitive tokens: heights, horizontal padding, adjacent control sizes.",
@@ -3588,10 +3588,10 @@
3588
3588
  "value": "1"
3589
3589
  },
3590
3590
  {
3591
- "description": "Mark width for a BORDERED FIELD. This knob exists because a field is the one control that already owns a boundary, so it is the one place the mark could plausibly be dropped and the recoloured border left to carry the state — which is the established treatment for a bordered field: a primary border plus a translucent halo, nothing in between. It ships at the same step as `--focus-outline-weight`, the hairline stroke, so a field and a button carry the same weight. A gate asserts the two stay equal, because this token cannot literally READ the global one without freezing at :root (docs/TOKENS.md, the freeze rule). It feeds `--focus-ring-weight`, never `--focus-ring-width`. The width is `weight × --focus-outline`, so a field's mark is still switched off with everything else — a knob that bypassed the switch would be a hole in it. If a service sets this, note the unit: the halo's spread is summed with a length in `calc()`, and CSS refuses to add a unitless number to a length — a bare `0` makes the whole box-shadow invalid at computed-value time and it resolves to NONE. Write `0px`. default = var(--stroke-hairline), resolved at the call site (gh#906)",
3591
+ "description": "Mark width for a BORDERED FIELD. This knob exists because a field is the one control that already owns a boundary, so it is the one place the mark could plausibly be dropped and the recoloured border left to carry the state — which is the established treatment for a bordered field: a primary border plus a translucent halo, nothing in between. It ships at the same step as `--focus-outline-weight`, the hairline stroke, so a field and a button carry the same weight. A gate asserts the two stay equal, because this token cannot literally READ the global one without freezing at :root (docs/TOKENS.md, the freeze rule). It feeds `--focus-ring-weight`, never `--focus-ring-width`. The width is `weight × --focus-outline`, so a field's mark is still switched off with everything else — a knob that bypassed the switch would be a hole in it. If a service sets this, note the unit: the halo's spread is summed with a length in `calc()`, and CSS refuses to add a unitless number to a length — a bare `0` makes the whole box-shadow invalid at computed-value time and it resolves to NONE. Write `0px`. modern 2px mark, matching global focus",
3592
3592
  "name": "--control-focus-ring-width",
3593
3593
  "tier": "component",
3594
- "value": "initial"
3594
+ "value": "var(--stroke-md)"
3595
3595
  },
3596
3596
  {
3597
3597
  "description": "Outline-form gaps — marks where a radius-hugging ring would touch the glyph.",
@@ -4067,6 +4067,12 @@
4067
4067
  "tier": "component",
4068
4068
  "value": "1.5rem"
4069
4069
  },
4070
+ {
4071
+ "description": "Shared coarse-pointer floor; independent of density and the desktop glyph size.",
4072
+ "name": "--control-affix-touch-target-size",
4073
+ "tier": "component",
4074
+ "value": "var(--band-height-xl)"
4075
+ },
4070
4076
  {
4071
4077
  "description": "gh#888 — φ tier freezes at :root; default = calc(var(--radius) / var(--radius-ratio) / var(--radius-ratio))",
4072
4078
  "name": "--control-affix-action-radius",
@@ -5139,7 +5145,7 @@
5139
5145
  "description": "SearchInput's text stops at the clear ✕'s 44px TARGET, not at its 24px box (gh#1148): edge inset + half the box (where the glyph's centre is) + half the target.",
5140
5146
  "name": "--search-input-end-padding",
5141
5147
  "tier": "component",
5142
- "value": "calc( var(--search-input-edge-inset) + var(--touch-target-min) / 2 + var(--band-height-xl) / 2 )"
5148
+ "value": "calc( var(--search-input-edge-inset) + var(--touch-target-min) / 2 + var(--control-affix-touch-target-size) / 2 )"
5143
5149
  },
5144
5150
  {
5145
5151
  "description": "Control primitive tokens: heights, horizontal padding, adjacent control sizes.",
@@ -7032,13 +7038,13 @@
7032
7038
  "value": "22.5rem"
7033
7039
  },
7034
7040
  {
7035
- "description": "Overlay scrims — `initial` so the shared semantic --overlay-background re-resolves at the CALL SITE under a scoped [data-tenant]/.dark theme (a :root binding to it freezes at :root and a scoped override never reaches the overlay). Each surface takes a SHARE of that one scrim rather than a private literal, so a service retints every backdrop with one --overlay-background while the calibrated per-surface depth survives: a slide-in Sheet washes the page more lightly than a modal Dialog. Defaults resolve to rgb(0 0 0 / 0.2) (sheet) and rgb(0 0 0 / 0.3) (dialog).",
7041
+ "description": "Overlay scrims — `initial` so the shared semantic --overlay-background re-resolves at the CALL SITE under a scoped [data-tenant]/.dark theme (a :root binding to it freezes at :root and a scoped override never reaches the overlay). Each surface takes a SHARE of that one scrim rather than a private literal, so a service retints every backdrop with one --overlay-background while the calibrated per-surface depth survives: a slide-in Sheet washes the page more lightly than a modal Dialog. Defaults resolve to rgb(0 0 0 / 0.16) (sheet) and rgb(0 0 0 / 0.24) (dialog).",
7036
7042
  "name": "--sheet-overlay-background",
7037
7043
  "tier": "component",
7038
7044
  "value": "initial"
7039
7045
  },
7040
7046
  {
7041
- "description": "A SHARE OF `--overlay-background`, NOT THE RESULTING ALPHA (gh#886). The base is a finished colour that already carries `/0.5`, so `40%` composites to **0.20**, and a theme author who sets `62%` gets 0.31. That is the library-wide convention for a `--*-alpha` knob in the `color-mix(in srgb, <base> <alpha>, transparent)` share form — 100% means \"the base as authored\", 0% means gone — and it is deliberate: the calibrated depth lives ONCE in `--overlay-background` so a service retints every scrim with one line. Want an absolute opacity instead? Set `--sheet-overlay-background` directly; it wins outright. Pinned by src/styles/__tests__/overlay-alpha-share-886.test.ts. 40% of rgb(0 0 0 / 0.5) = rgb(0 0 0 / 0.20)",
7047
+ "description": "A SHARE OF `--overlay-background`, NOT THE RESULTING ALPHA (gh#886). The base is a finished colour that already carries `/0.4`, so `40%` composites to **0.16**, and a theme author who sets `62%` gets 0.248. That is the library-wide convention for a `--*-alpha` knob in the `color-mix(in srgb, <base> <alpha>, transparent)` share form — 100% means \"the base as authored\", 0% means gone — and it is deliberate: the calibrated depth lives ONCE in `--overlay-background` so a service retints every scrim with one line. Want an absolute opacity instead? Set `--sheet-overlay-background` directly; it wins outright. Pinned by src/styles/__tests__/overlay-alpha-share-886.test.ts. 40% of rgb(0 0 0 / 0.4) = rgb(0 0 0 / 0.16)",
7042
7048
  "name": "--sheet-overlay-alpha",
7043
7049
  "tier": "component",
7044
7050
  "value": "40%"
@@ -7170,7 +7176,7 @@
7170
7176
  "value": "initial"
7171
7177
  },
7172
7178
  {
7173
- "description": "A SHARE OF `--overlay-background`, NOT THE RESULTING ALPHA (gh#886) — the miss that was reported, because this knob carried no note of its own and `60%` reads as \"0.6\". The base is a finished colour with `/0.5` baked in, so the product is **0.30**, and `62%` gives 0.31 rather than the darker scrim that was asked for. See `--sheet-overlay-alpha` above for the convention and for the escape hatch: `--dialog-overlay-background` is an absolute colour and wins outright. Pinned by src/styles/__tests__/overlay-alpha-share-886.test.ts. 60% of rgb(0 0 0 / 0.5) = rgb(0 0 0 / 0.30)",
7179
+ "description": "A SHARE OF `--overlay-background`, NOT THE RESULTING ALPHA (gh#886) — the miss that was reported, because this knob carried no note of its own and `60%` reads as \"0.6\". The base is a finished colour with `/0.4` baked in, so the product is **0.24**, and `62%` gives 0.248 rather than the darker scrim that was asked for. See `--sheet-overlay-alpha` above for the convention and for the escape hatch: `--dialog-overlay-background` is an absolute colour and wins outright. Pinned by src/styles/__tests__/overlay-alpha-share-886.test.ts. 60% of rgb(0 0 0 / 0.4) = rgb(0 0 0 / 0.24)",
7174
7180
  "name": "--dialog-overlay-alpha",
7175
7181
  "tier": "component",
7176
7182
  "value": "60%"
@@ -7956,10 +7962,10 @@
7956
7962
  "value": "var(--control-label-font-size, var(--text-sm))"
7957
7963
  },
7958
7964
  {
7959
- "description": "Row rhythm between a Form's own top-level blocks (gh#295) — e.g. the field group's CardContent and its CardFooter action row, or two FormFields that happen to be Form's OWN direct children (no Card framing). Unchanged from the historical value. 16px",
7965
+ "description": "Row rhythm between a Form's own top-level blocks (gh#295) — e.g. the field group's CardContent and its CardFooter action row, or two FormFields that happen to be Form's OWN direct children (no Card framing). Modern block spacing is 24px. 24px",
7960
7966
  "name": "--form-block-gap",
7961
7967
  "tier": "component",
7962
- "value": "var(--space-4)"
7968
+ "value": "var(--space-6)"
7963
7969
  },
7964
7970
  {
7965
7971
  "description": "THE VALIDATION FEEDBACK GLYPH. It was `controlIconClass`, which is `size-[length:var(--control-height)]` — the height of a whole CONTROL, meant for something that fills a control-sized box like the ColorPicker swatch. On a `text-xs` feedback line that rendered a ~32px icon beside ~12px text, which is what a reader actually saw and reported. A glyph that sits inline with text belongs on the ICON scale, one step above the text it accompanies so it reads as a mark rather than a letter. 14px beside 12px text",
@@ -7968,7 +7974,7 @@
7968
7974
  "value": "var(--icon-size-sm)"
7969
7975
  },
7970
7976
  {
7971
- "description": "Field-to-field row rhythm (gh#295) — mirrors --descriptions-row-gap so a read-only value mixed in via `FormField.staticText` (gh#294) and a real Descriptions block share ONE canonical rhythm. Applied via margin-based sibling spacing on FormField itself (not Form's flex `gap`, which only reaches DIRECT children — dead in the common `Form > CardContent > FormField*` composition every real Save-button form needs), so it holds at any DOM depth relative to Form. 12px",
7977
+ "description": "Field-to-field row rhythm (gh#295) — mirrors --descriptions-row-gap so a read-only value mixed in via `FormField.staticText` (gh#294) and a real Descriptions block share ONE canonical rhythm. Applied via margin-based sibling spacing on FormField itself (not Form's flex `gap`, which only reaches DIRECT children — dead in the common `Form > CardContent > FormField*` composition every real Save-button form needs), so it holds at any DOM depth relative to Form. 12px — shared with Descriptions",
7972
7978
  "name": "--form-field-row-gap",
7973
7979
  "tier": "component",
7974
7980
  "value": "var(--space-3)"
@@ -10722,13 +10728,13 @@
10722
10728
  "value": "22.5rem"
10723
10729
  },
10724
10730
  {
10725
- "description": "Mobile-drawer scrim — `initial` so the shared --overlay-background default re-resolves at the CALL SITE under a scoped [data-tenant]/.dark theme (a :root binding freezes at :root). Default = var(--app-shell-mobile-nav-alpha) of the global scrim, i.e. rgb(0 0 0 / 0.2) out of the box.",
10731
+ "description": "Mobile-drawer scrim — `initial` so the shared --overlay-background default re-resolves at the CALL SITE under a scoped [data-tenant]/.dark theme (a :root binding freezes at :root). Default = var(--app-shell-mobile-nav-alpha) of the global scrim, i.e. rgb(0 0 0 / 0.16) out of the box.",
10726
10732
  "name": "--app-shell-mobile-nav-background",
10727
10733
  "tier": "component",
10728
10734
  "value": "initial"
10729
10735
  },
10730
10736
  {
10731
- "description": "Share of the global --overlay-background this drawer's scrim uses. A slide-in drawer washes the page more lightly than a modal dialog; keeping it a RATIO means a service retints the whole system with one --overlay-background and every overlay keeps its calibrated depth. A SHARE, not the resulting alpha (gh#886): the base already carries `/0.5`, so the product is 0.20. 40% of rgb(0 0 0 / 0.5) = rgb(0 0 0 / 0.20)",
10737
+ "description": "Share of the global --overlay-background this drawer's scrim uses. A slide-in drawer washes the page more lightly than a modal dialog; keeping it a RATIO means a service retints the whole system with one --overlay-background and every overlay keeps its calibrated depth. A SHARE, not the resulting alpha (gh#886): the base already carries `/0.4`, so the product is 0.16. 40% of rgb(0 0 0 / 0.4) = rgb(0 0 0 / 0.16)",
10732
10738
  "name": "--app-shell-mobile-nav-alpha",
10733
10739
  "tier": "component",
10734
10740
  "value": "40%"
@@ -10895,6 +10901,18 @@
10895
10901
  "tier": "component",
10896
10902
  "value": "var(--space-2)"
10897
10903
  },
10904
+ {
10905
+ "description": "Shared-device identity gets a bounded share, not the primary controls' width.",
10906
+ "name": "--topbar-identity-max-inline-size",
10907
+ "tier": "component",
10908
+ "value": "40%"
10909
+ },
10910
+ {
10911
+ "description": "Shell (sidebar / topbar / kbd) component tokens — small-by-design text knobs (rule #45/#46). A service re-tunes chrome text without moving the global scale.",
10912
+ "name": "--topbar-identity-min-text-inline-size",
10913
+ "tier": "component",
10914
+ "value": "3ch"
10915
+ },
10898
10916
  {
10899
10917
  "description": "THE GLASS BAR (gh#831) — the same two declarations two unrelated website showcases each hand-wrote on their own sticky header: a translucent page fill plus a backdrop blur. Both knobs are `initial`, i.e. QUIET by default in exactly the sense the paragraph above describes: a guaranteed-invalid value makes the whole declaration at the call site invalid-at-computed-value time, so `background-color` falls back to its own initial `transparent` and `backdrop-filter` to `none`. The shipped bar therefore paints nothing, creates no backdrop root, and is byte-identical to the pure slot bar it was. A service opts in with `--topbar-background-alpha: 85%` / `--topbar-backdrop-blur-size: 10px`. The TINT is NOT a knob: `.ui-topbar` reads `--background` directly at the call site, so a dark or tenant-scoped subtree re-tints the glass instead of inheriting :root's answer (docs/TOKENS.md · the `:root` freeze rule). Precedent for the blur name: --app-launcher-launchpad-backdrop-blur-size. default = none — the bar paints no fill",
10900
10918
  "name": "--topbar-background-alpha",
@@ -12195,7 +12213,7 @@
12195
12213
  "description": "Table component tokens: row height, cell padding.",
12196
12214
  "name": "--table-row-height-default",
12197
12215
  "tier": "component",
12198
- "value": "var(--band-height-md)"
12216
+ "value": "var(--band-height-lg)"
12199
12217
  },
12200
12218
  {
12201
12219
  "description": "Table component tokens: row height, cell padding.",
@@ -72,6 +72,21 @@ const Input = React.forwardRef(
72
72
  },
73
73
  [ref]
74
74
  );
75
+ const setTrailingRef = React.useCallback((node) => {
76
+ if (!node) return;
77
+ const wrapper = node.parentElement;
78
+ const sync = () => wrapper.style.setProperty(
79
+ "--input-trailing-measured-size",
80
+ `${node.getBoundingClientRect().width}px`
81
+ );
82
+ sync();
83
+ const observer = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(sync);
84
+ observer?.observe(node);
85
+ return () => {
86
+ observer?.disconnect();
87
+ wrapper.style.removeProperty("--input-trailing-measured-size");
88
+ };
89
+ }, []);
75
90
  const [text, setText] = React.useState(() => String(value ?? defaultValue ?? ""));
76
91
  const hasText = text.length > 0;
77
92
  React.useEffect(() => {
@@ -167,7 +182,7 @@ const Input = React.forwardRef(
167
182
  ...appearance
168
183
  }
169
184
  ),
170
- trailing != null || counter !== null ? /* @__PURE__ */ jsxs("span", { className: "ui-input-trailing", children: [
185
+ trailing != null || counter !== null ? /* @__PURE__ */ jsxs("span", { className: "ui-input-trailing", ref: setTrailingRef, children: [
171
186
  counter ? /* @__PURE__ */ jsx(
172
187
  "span",
173
188
  {
@@ -104,7 +104,7 @@ function SearchInput({
104
104
  // gh#1250 — while the key cap shows, the text stops before it instead of running under it.
105
105
  // gh#1285 — written FROM --search-input-end-padding (minus the ✕ slot it reserves, plus the
106
106
  // cap), so a theme that retunes that token still moves this padding (default: edge inset + 6ch, as before).
107
- showShortcut ? "!pe-[calc(var(--search-input-end-padding)_-_var(--control-icon-size)_-_var(--control-gap)_+_6ch)]" : "!pe-[var(--search-input-end-padding)]",
107
+ showShortcut ? "!pe-[calc(var(--search-input-end-padding)_-_var(--control-icon-size)_-_var(--control-gap)_+_6ch)]" : void 0,
108
108
  inputClassName
109
109
  ),
110
110
  disabled
@@ -15,6 +15,7 @@ export declare const TopbarItem: React.ForwardRefExoticComponent<Omit<React.Butt
15
15
  icon?: React.ReactNode;
16
16
  iconHideFrom?: import("../../props/index.js").BreakpointProp;
17
17
  labelHideBelow?: import("../../props/index.js").BreakpointProp;
18
+ truncate?: boolean;
18
19
  badge?: React.ReactNode;
19
20
  badgeTone?: import("../../props/components/layout.prop.js").SidebarBadgeToneProp;
20
21
  asChild?: boolean;
@@ -10,6 +10,7 @@ const TopbarItem = React.forwardRef(function TopbarItem2({
10
10
  icon,
11
11
  iconHideFrom,
12
12
  labelHideBelow,
13
+ truncate = false,
13
14
  badge,
14
15
  badgeTone,
15
16
  children,
@@ -26,13 +27,14 @@ const TopbarItem = React.forwardRef(function TopbarItem2({
26
27
  children: icon
27
28
  }
28
29
  ) : null;
29
- const labelOf = (label) => labelHideBelow === void 0 || label === void 0 || label === null || label === false ? label : /* @__PURE__ */ jsx(
30
+ const labelOf = (label2) => !truncate && labelHideBelow === void 0 || label2 === void 0 || label2 === null || label2 === false ? label2 : /* @__PURE__ */ jsx(
30
31
  "span",
31
32
  {
33
+ dir: truncate ? "auto" : void 0,
32
34
  "data-slot": "topbar-item-label",
33
35
  className: "ui-topbar-item-label",
34
- "data-hide-below": labelHideBelow,
35
- children: label
36
+ "data-hide-below": truncate ? void 0 : labelHideBelow,
37
+ children: label2
36
38
  }
37
39
  );
38
40
  const badgeNode = showBadge ? /* @__PURE__ */ jsx(
@@ -44,18 +46,21 @@ const TopbarItem = React.forwardRef(function TopbarItem2({
44
46
  children: badge
45
47
  }
46
48
  ) : null;
47
- const slotted = asChild && (iconNode !== null || labelHideBelow !== void 0 || badgeNode !== null) && React.isValidElement(children) ? React.cloneElement(
49
+ const slotted = asChild && (iconNode !== null || truncate || labelHideBelow !== void 0 || badgeNode !== null) && React.isValidElement(children) ? React.cloneElement(
48
50
  children,
49
51
  void 0,
50
52
  iconNode,
51
53
  labelOf(children.props.children),
52
54
  badgeNode
53
55
  ) : children;
56
+ const label = asChild && React.isValidElement(children) ? children.props.children : children;
54
57
  return /* @__PURE__ */ jsx(
55
58
  Comp,
56
59
  {
57
60
  ref,
58
61
  "data-slot": "topbar-item",
62
+ "data-truncate": truncate ? "true" : void 0,
63
+ title: truncate && typeof label === "string" ? label : void 0,
59
64
  "data-hide-below": hideBelow,
60
65
  className: cn("ui-topbar-item ui-focus-ring", className),
61
66
  type: asChild ? void 0 : type ?? "button",
@@ -5,4 +5,4 @@ export type { TopbarProp, TopbarProp as TopbarProps } from "../../props/componen
5
5
  * Topbar — a PURE SLOT bar. It positions three clusters (`start` / `center` / `end`) and owns the
6
6
  * bar's flex layout; it does NOT bake any chrome.
7
7
  */
8
- export declare function Topbar({ start, center, end, className, children, height, overflow, pad, style, ...props }: TopbarProp): React.JSX.Element;
8
+ export declare function Topbar({ start, center, end, identity, className, children, height, overflow, pad, style, ...props }: TopbarProp): React.JSX.Element;
@@ -11,6 +11,7 @@ function Topbar({
11
11
  start,
12
12
  center,
13
13
  end,
14
+ identity,
14
15
  className,
15
16
  children,
16
17
  height,
@@ -20,8 +21,10 @@ function Topbar({
20
21
  ...props
21
22
  }) {
22
23
  const barRef = React.useRef(null);
24
+ const controlsRef = React.useRef(null);
25
+ const hasIdentity = identity != null && children == null;
23
26
  const foldable = overflow === "menu" && children == null;
24
- const collapsed = useCollapsesToMenu(barRef, foldable);
27
+ const collapsed = useCollapsesToMenu(hasIdentity ? controlsRef : barRef, foldable);
25
28
  const clusters = /* @__PURE__ */ jsxs(Fragment, { children: [
26
29
  start != null ? /* @__PURE__ */ jsx("div", { "data-slot": "topbar-start", className: "ui-topbar-start", children: start }) : null,
27
30
  center != null ? /* @__PURE__ */ jsx("div", { "data-slot": "topbar-center", className: "ui-topbar-center", children: center }) : null,
@@ -32,13 +35,26 @@ function Topbar({
32
35
  {
33
36
  ref: barRef,
34
37
  "data-height": height,
38
+ "data-identity": hasIdentity ? "true" : void 0,
35
39
  "data-overflow": overflow,
36
40
  "data-collapsed": foldable ? String(collapsed) : void 0,
37
41
  style: { ...style, ...padStyle(pad, void 0) },
38
42
  "data-slot": "topbar",
39
43
  className: cn("ui-topbar", className),
40
44
  ...props,
41
- children: children ?? (collapsed ? /* @__PURE__ */ jsx(TopbarOverflowMenu, { children: clusters }) : clusters)
45
+ children: children ?? (hasIdentity ? /* @__PURE__ */ jsxs(Fragment, { children: [
46
+ /* @__PURE__ */ jsx(
47
+ "div",
48
+ {
49
+ ref: controlsRef,
50
+ "data-slot": "topbar-controls",
51
+ "data-overflow": overflow,
52
+ className: "ui-topbar ui-topbar-controls",
53
+ children: collapsed ? /* @__PURE__ */ jsx(TopbarOverflowMenu, { children: clusters }) : clusters
54
+ }
55
+ ),
56
+ /* @__PURE__ */ jsx("div", { "data-slot": "topbar-identity", className: "ui-topbar-identity", children: identity })
57
+ ] }) : collapsed ? /* @__PURE__ */ jsx(TopbarOverflowMenu, { children: clusters }) : clusters)
42
58
  }
43
59
  );
44
60
  }
@@ -69,7 +85,13 @@ function useCollapsesToMenu(ref, enabled) {
69
85
  if (typeof ResizeObserver === "undefined") return void 0;
70
86
  const observer = new ResizeObserver(update);
71
87
  observer.observe(bar);
72
- if (!collapsed) for (const cluster of bar.children) observer.observe(cluster);
88
+ if (!collapsed)
89
+ for (const cluster of bar.children) {
90
+ observer.observe(cluster);
91
+ if (cluster.getAttribute("data-slot") === "topbar-start") {
92
+ for (const cell of cluster.children) observer.observe(cell);
93
+ }
94
+ }
73
95
  return () => observer.disconnect();
74
96
  }, [ref, enabled, collapsed]);
75
97
  return enabled && collapsed;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
3
- "version": "33.9.1",
3
+ "version": "33.11.2",
4
4
  "targetSize": {
5
5
  "standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
6
6
  "min": 24,
@@ -1545,6 +1545,14 @@ export type TopbarProp = Omit<React.HTMLAttributes<HTMLDivElement>, "children">
1545
1545
  center?: ReactNode;
1546
1546
  /** Inline-end cluster — settings pickers, notifications, the user menu. */
1547
1547
  end?: ReactNode;
1548
+ /**
1549
+ * Opt-in shared-device identity, pinned outside scrolling/overflow menus. Pass one
1550
+ * TopbarItem with `truncate` and an Avatar in `icon`; use a plain string name so its full
1551
+ * title is automatic. Never combine with hideBelow/labelHideBelow/iconHideFrom.
1552
+ * Capped by --topbar-identity-max-inline-size (40%); primary controls retain their floors.
1553
+ * At extreme widths only the other clusters scroll or move to the menu. Ignored with children.
1554
+ */
1555
+ identity?: ReactNode;
1548
1556
  /**
1549
1557
  * What a bar that does not fit does with the cells that do not fit (gh#728).
1550
1558
  *
@@ -1643,6 +1651,11 @@ export type TopbarItemProp = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>,
1643
1651
  * accessible name. Under `asChild` the child element's own children are the label.
1644
1652
  */
1645
1653
  labelHideBelow?: BreakpointProp;
1654
+ /** Ellipse the label in the available width, keeping the icon and control minimum.
1655
+ * Same boolean vocabulary as Text.truncate. Overrides labelHideBelow; the complete text
1656
+ * remains the accessible name. String children automatically supply title (asChild too).
1657
+ */
1658
+ truncate?: boolean;
1646
1659
  /**
1647
1660
  * Unread count OVERLAID on the cell's glyph — the notification-bell affordance the cell's own use
1648
1661
  * cases name (gh#398). Pass the CONTENT ONLY, exactly like `SidebarItemProp.badge`: a number, a
@@ -1433,7 +1433,11 @@ export declare const COMPONENT_PROP_REGISTRY: {
1433
1433
  readonly TopbarProp: {
1434
1434
  readonly group: "layout";
1435
1435
  readonly file: "components/layout.prop.ts";
1436
- readonly vocabulary: readonly ["ChildrenProp", {
1436
+ readonly vocabulary: readonly [{
1437
+ readonly field: "identity";
1438
+ readonly local: true;
1439
+ readonly reason: "Reserved shared-device account slot; bounded and outside the overflow policy.";
1440
+ }, "ChildrenProp", {
1437
1441
  readonly field: "overflow";
1438
1442
  readonly local: true;
1439
1443
  readonly reason: "The bar's overflow contract — `scroll` (default) keeps every cell reachable, `clip` is the pre-gh#728 slicing.";
@@ -1443,6 +1447,10 @@ export declare const COMPONENT_PROP_REGISTRY: {
1443
1447
  readonly group: "layout";
1444
1448
  readonly file: "components/layout.prop.ts";
1445
1449
  readonly vocabulary: readonly ["ChildrenProp", "ClassNameProp", {
1450
+ readonly field: "truncate";
1451
+ readonly local: true;
1452
+ readonly reason: "Text.truncate boolean vocabulary applied to the bar cell label.";
1453
+ }, {
1446
1454
  readonly field: "asChild";
1447
1455
  readonly local: true;
1448
1456
  readonly reason: "Radix Slot passthrough — merges onto the child";
@@ -1551,6 +1551,11 @@ const COMPONENT_PROP_REGISTRY = {
1551
1551
  group: "layout",
1552
1552
  file: "components/layout.prop.ts",
1553
1553
  vocabulary: [
1554
+ {
1555
+ field: "identity",
1556
+ local: true,
1557
+ reason: "Reserved shared-device account slot; bounded and outside the overflow policy."
1558
+ },
1554
1559
  "ChildrenProp",
1555
1560
  {
1556
1561
  field: "overflow",
@@ -1565,6 +1570,11 @@ const COMPONENT_PROP_REGISTRY = {
1565
1570
  vocabulary: [
1566
1571
  "ChildrenProp",
1567
1572
  "ClassNameProp",
1573
+ {
1574
+ field: "truncate",
1575
+ local: true,
1576
+ reason: "Text.truncate boolean vocabulary applied to the bar cell label."
1577
+ },
1568
1578
  { field: "asChild", local: true, reason: "Radix Slot passthrough \u2014 merges onto the child" },
1569
1579
  "BreakpointProp",
1570
1580
  {