@godxjp/ui 18.4.0 → 18.5.0

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 (131) hide show
  1. package/README.md +16 -1
  2. package/dist/app/theme-axes.d.ts +11 -3
  3. package/dist/app/theme-axes.js +2 -1
  4. package/dist/components/charts/chart-frame.d.ts +17 -3
  5. package/dist/components/charts/chart-frame.js +7 -1
  6. package/dist/components/charts/chart-summary.d.ts +21 -0
  7. package/dist/components/charts/chart-summary.js +32 -5
  8. package/dist/components/charts/compact-bar-trend.d.ts +29 -0
  9. package/dist/components/charts/compact-bar-trend.js +74 -0
  10. package/dist/components/charts/index.d.ts +3 -0
  11. package/dist/components/charts/index.js +2 -0
  12. package/dist/components/data-display/badge.d.ts +8 -0
  13. package/dist/components/data-display/badge.js +7 -1
  14. package/dist/components/data-display/data-table.d.ts +16 -1
  15. package/dist/components/data-display/data-table.js +56 -0
  16. package/dist/components/data-display/index.d.ts +3 -1
  17. package/dist/components/data-display/index.js +10 -1
  18. package/dist/components/data-display/list-row.d.ts +22 -4
  19. package/dist/components/data-display/list-row.js +21 -3
  20. package/dist/components/data-display/service-launcher-card.d.ts +40 -0
  21. package/dist/components/data-display/service-launcher-card.js +97 -0
  22. package/dist/components/data-entry/command-palette.d.ts +37 -0
  23. package/dist/components/data-entry/command-palette.js +125 -0
  24. package/dist/components/data-entry/index.d.ts +2 -0
  25. package/dist/components/data-entry/index.js +2 -0
  26. package/dist/components/feedback/dialog.d.ts +2 -0
  27. package/dist/components/feedback/dialog.js +42 -33
  28. package/dist/components/feedback/index.d.ts +4 -1
  29. package/dist/components/feedback/index.js +6 -2
  30. package/dist/components/feedback/sheet.d.ts +26 -4
  31. package/dist/components/feedback/sheet.js +54 -7
  32. package/dist/components/feedback/two-factor-setup.d.ts +31 -0
  33. package/dist/components/feedback/two-factor-setup.js +97 -0
  34. package/dist/components/general/index.d.ts +1 -1
  35. package/dist/components/general/logo.d.ts +42 -8
  36. package/dist/components/general/logo.js +67 -14
  37. package/dist/components/layout/app-shell.js +21 -5
  38. package/dist/components/layout/auth-divider.d.ts +7 -0
  39. package/dist/components/layout/auth-divider.js +21 -0
  40. package/dist/components/layout/auth-footer.d.ts +5 -0
  41. package/dist/components/layout/auth-footer.js +24 -0
  42. package/dist/components/layout/auth-identity.d.ts +5 -0
  43. package/dist/components/layout/auth-identity.js +17 -0
  44. package/dist/components/layout/auth-shell.d.ts +8 -1
  45. package/dist/components/layout/auth-shell.js +25 -6
  46. package/dist/components/layout/auth-stack.d.ts +4 -0
  47. package/dist/components/layout/auth-stack.js +8 -0
  48. package/dist/components/layout/centered-shell.d.ts +5 -2
  49. package/dist/components/layout/centered-shell.js +2 -1
  50. package/dist/components/layout/index.d.ts +18 -3
  51. package/dist/components/layout/index.js +17 -2
  52. package/dist/components/layout/legal-document-shell.d.ts +34 -0
  53. package/dist/components/layout/legal-document-shell.js +186 -0
  54. package/dist/components/layout/master-detail.d.ts +24 -0
  55. package/dist/components/layout/master-detail.js +48 -0
  56. package/dist/components/layout/org-switcher.d.ts +9 -0
  57. package/dist/components/layout/org-switcher.js +218 -0
  58. package/dist/components/layout/page-container.d.ts +1 -1
  59. package/dist/components/layout/page-container.js +3 -2
  60. package/dist/components/layout/sidebar-link.d.ts +43 -0
  61. package/dist/components/layout/sidebar-link.js +13 -0
  62. package/dist/components/layout/sidebar.d.ts +42 -5
  63. package/dist/components/layout/sidebar.js +170 -85
  64. package/dist/components/navigation/app-setting-picker.js +17 -6
  65. package/dist/components/navigation/filter-bar.d.ts +12 -3
  66. package/dist/components/navigation/filter-bar.js +8 -2
  67. package/dist/components/navigation/index.d.ts +2 -2
  68. package/dist/components/navigation/index.js +3 -1
  69. package/dist/components/navigation/tabs-scroll.d.ts +70 -0
  70. package/dist/components/navigation/tabs-scroll.js +76 -0
  71. package/dist/components/navigation/tabs.js +35 -20
  72. package/dist/email/brand-mark.d.ts +92 -0
  73. package/dist/email/brand-mark.js +83 -0
  74. package/dist/email/color.d.ts +52 -0
  75. package/dist/email/color.js +37 -0
  76. package/dist/email/geometry.d.ts +112 -0
  77. package/dist/email/geometry.js +89 -0
  78. package/dist/email/index.d.ts +78 -0
  79. package/dist/email/index.js +76 -0
  80. package/dist/email/inline-style.d.ts +25 -0
  81. package/dist/email/inline-style.js +21 -0
  82. package/dist/email/tokens.generated.d.ts +147 -0
  83. package/dist/email/tokens.generated.js +142 -0
  84. package/dist/i18n/messages/en.json +21 -2
  85. package/dist/i18n/messages/ja.json +21 -2
  86. package/dist/i18n/messages/vi.json +21 -2
  87. package/dist/inertia/index.d.ts +50 -0
  88. package/dist/inertia/index.js +5 -0
  89. package/dist/props/components/app.prop.d.ts +11 -1
  90. package/dist/props/components/charts.prop.d.ts +40 -1
  91. package/dist/props/components/data-display.prop.d.ts +13 -1
  92. package/dist/props/components/feedback.prop.d.ts +10 -0
  93. package/dist/props/components/index.d.ts +2 -2
  94. package/dist/props/components/layout.prop.d.ts +373 -10
  95. package/dist/props/components/layout.prop.js +1 -0
  96. package/dist/props/components/navigation.prop.d.ts +19 -1
  97. package/dist/props/registry.d.ts +291 -7
  98. package/dist/props/registry.js +341 -7
  99. package/dist/props/vocabulary/index.d.ts +1 -1
  100. package/dist/props/vocabulary/interaction.prop.d.ts +1 -1
  101. package/dist/props/vocabulary/layout.prop.d.ts +18 -0
  102. package/dist/styles/base.css +8 -2
  103. package/dist/styles/card-layout.css +161 -7
  104. package/dist/styles/chart-layout.css +97 -0
  105. package/dist/styles/control.css +48 -2
  106. package/dist/styles/data-display-layout.css +52 -6
  107. package/dist/styles/dialog-layout.css +105 -4
  108. package/dist/styles/fonts.css +15 -4
  109. package/dist/styles/index.css +11 -3
  110. package/dist/styles/layout.css +367 -1
  111. package/dist/styles/logo-layout.css +62 -0
  112. package/dist/styles/navigation-layout.css +43 -0
  113. package/dist/styles/shell-layout.css +488 -40
  114. package/dist/theme/dxs.canonical.css +76 -0
  115. package/dist/tokens/axes.css +39 -0
  116. package/dist/tokens/base.css +5 -0
  117. package/dist/tokens/components/card.css +48 -0
  118. package/dist/tokens/components/chart.css +47 -0
  119. package/dist/tokens/components/control.css +16 -0
  120. package/dist/tokens/components/email.css +66 -0
  121. package/dist/tokens/components/feedback.css +17 -0
  122. package/dist/tokens/components/legal-document.css +67 -0
  123. package/dist/tokens/components/list-row.css +23 -2
  124. package/dist/tokens/components/logo.css +32 -0
  125. package/dist/tokens/components/navigation.css +13 -0
  126. package/dist/tokens/components/sheet.css +19 -0
  127. package/dist/tokens/components/shell.css +138 -0
  128. package/dist/tokens/components/sidebar.css +33 -0
  129. package/dist/tokens/foundation.css +5 -2
  130. package/dist/tokens/semantic/layout.css +44 -2
  131. package/package.json +16 -5
@@ -6,24 +6,31 @@
6
6
  @layer components {
7
7
  .app-root {
8
8
  display: grid;
9
+ width: 100%;
10
+ max-width: 100vw;
11
+ min-width: 0;
9
12
  grid-template-areas:
10
13
  "sidebar topbar"
11
14
  "sidebar main"
12
15
  "sidebar footer";
13
- grid-template-columns: 16rem minmax(0, 1fr);
16
+ /* Rail width is a knob, never a literal (rule #45) — a service on a different design grid sets
17
+ * --app-shell-sidebar-width once (e.g. 255px) instead of forking this rule. */
18
+ grid-template-columns: var(--app-shell-sidebar-width) minmax(0, 1fr);
14
19
  grid-template-rows: var(--app-shell-bar-height) minmax(0, 1fr) auto;
15
20
  height: 100vh;
16
21
  background: hsl(var(--background));
17
22
  }
18
23
 
19
24
  .app-root[data-collapsed="true"] {
20
- grid-template-columns: 4rem minmax(0, 1fr);
25
+ grid-template-columns: var(--app-shell-rail-width) minmax(0, 1fr);
21
26
  }
22
27
 
23
28
  .app-sidebar {
24
29
  grid-area: sidebar;
25
30
  display: flex;
31
+ height: 100%;
26
32
  min-width: 0;
33
+ min-height: 0;
27
34
  flex-direction: column;
28
35
  overflow: hidden;
29
36
  border-inline-end: 1px solid hsl(var(--border));
@@ -40,9 +47,10 @@
40
47
  * set to baseline; --scaling:1 alone is not enough — see foundation.css). */
41
48
  display: flex;
42
49
  min-width: 0;
50
+ overflow: hidden;
43
51
  align-items: center;
44
- gap: var(--space-3);
45
- padding-inline: var(--space-4);
52
+ gap: var(--app-shell-bar-gap);
53
+ padding-inline: var(--app-shell-bar-inset);
46
54
  border-bottom: 1px solid hsl(var(--border));
47
55
  background: hsl(var(--card));
48
56
  /* Opt-in brand-chrome wash — invisible by default (--topbar-gradient: none). */
@@ -105,7 +113,7 @@
105
113
  flex: 1 1 auto;
106
114
  flex-direction: column;
107
115
  align-items: center;
108
- justify-content: center;
116
+ justify-content: var(--auth-shell-main-align);
109
117
  padding: var(--auth-shell-main-padding);
110
118
  }
111
119
 
@@ -120,6 +128,174 @@
120
128
  color: hsl(var(--muted-foreground));
121
129
  }
122
130
 
131
+ .ui-auth-shell[data-variant="canonical"] {
132
+ --control-height: var(--auth-shell-canonical-control-height);
133
+ --heading-h1: var(--auth-shell-canonical-heading-size);
134
+ --auth-shell-card-max-width: var(--auth-shell-canonical-card-max-width);
135
+ --auth-shell-main-padding: var(--auth-shell-canonical-main-padding);
136
+ }
137
+
138
+ /*
139
+ * NAMED FLOW PRESETS (gh#217 / gh#220) — orthogonal to `variant` and declared AFTER it (equal
140
+ * specificity, so source order wins) so a preset re-measures the page while `variant="canonical"`
141
+ * keeps owning control density and heading size. A preset owns the card measure + page gutters,
142
+ * so the consumer never overrides `--auth-shell-card-max-width` by hand.
143
+ */
144
+ .ui-auth-shell[data-preset] .ui-auth-shell-card {
145
+ display: flex;
146
+ flex-direction: column;
147
+ gap: var(--auth-shell-card-stack-gap);
148
+ }
149
+
150
+ .ui-auth-shell[data-preset="device-authorization"] {
151
+ --auth-shell-card-max-width: var(--auth-shell-device-card-max-width);
152
+ --auth-shell-main-padding: var(--auth-shell-device-main-padding);
153
+ }
154
+
155
+ .ui-auth-shell[data-preset="context-selection"] {
156
+ --auth-shell-card-max-width: var(--auth-shell-context-card-max-width);
157
+ --auth-shell-main-padding: var(--auth-shell-context-main-padding);
158
+ --auth-shell-card-stack-gap: var(--auth-shell-context-card-stack-gap);
159
+ }
160
+
161
+ /* Account-recovery preset (gh#233) — one 432px measure for BOTH SCR-008 panels (password
162
+ * recovery and the sign-in MFA challenge). Title + description live INSIDE the bordered surface
163
+ * here, so the panel is `Card > CardHeader(CardTitle + CardDescription)` with no `AuthIdentity`
164
+ * mark above it — the preset only re-measures the page, it never dictates content. */
165
+ .ui-auth-shell[data-preset="account-recovery"] {
166
+ --auth-shell-card-max-width: var(--auth-shell-recovery-card-max-width);
167
+ --auth-shell-main-padding: var(--auth-shell-recovery-main-padding);
168
+ }
169
+
170
+ .ui-auth-identity {
171
+ display: flex;
172
+ flex-direction: column;
173
+ align-items: center;
174
+ gap: var(--auth-identity-gap);
175
+ text-align: center;
176
+ }
177
+
178
+ .ui-auth-requester {
179
+ display: flex;
180
+ align-items: center;
181
+ gap: var(--auth-requester-gap);
182
+ }
183
+
184
+ .ui-auth-requester-icon {
185
+ display: inline-flex;
186
+ width: var(--auth-requester-icon-size);
187
+ height: var(--auth-requester-icon-size);
188
+ align-items: center;
189
+ justify-content: center;
190
+ border-radius: var(--radius-sm);
191
+ background: hsl(var(--accent));
192
+ color: hsl(var(--primary));
193
+ }
194
+
195
+ .ui-auth-requester-icon > svg {
196
+ width: var(--auth-requester-glyph-size);
197
+ height: var(--auth-requester-glyph-size);
198
+ }
199
+
200
+ .ui-auth-legal-footer {
201
+ display: flex;
202
+ flex-wrap: wrap;
203
+ align-items: center;
204
+ justify-content: center;
205
+ gap: var(--auth-footer-content-gap);
206
+ color: hsl(var(--muted-foreground));
207
+ font-family: var(--font-mono);
208
+ font-size: var(--auth-footer-text-font-size);
209
+ }
210
+
211
+ .ui-auth-legal-footer-item {
212
+ display: inline-flex;
213
+ align-items: center;
214
+ gap: var(--auth-footer-content-gap);
215
+ }
216
+
217
+ .ui-auth-stack {
218
+ display: flex;
219
+ flex-direction: column;
220
+ gap: var(--auth-stack-gap);
221
+ }
222
+
223
+ .ui-auth-shell[data-variant="canonical"] .ui-form-field {
224
+ --field-label-gap: var(--auth-shell-field-label-gap);
225
+ }
226
+
227
+ .ui-auth-shell[data-variant="canonical"] [data-slot="form-field-label"] [data-slot="label"] {
228
+ font-size: var(--auth-shell-field-label-font-size);
229
+ line-height: var(--auth-shell-field-label-line-height);
230
+ }
231
+
232
+ /*
233
+ * Compact auth card density — the two card axes are wired to SEPARATE knobs (gh#232), so a
234
+ * service can shorten the canonical Login card without narrowing its column (or vice versa):
235
+ * inline column ← --auth-shell-compact-card-inset → --card-space-inset
236
+ * block padding ← --auth-shell-card-padding-block-compact → --card-space-shell-y
237
+ * header↔body ← --auth-shell-card-body-gap-compact → --card-space-body-y
238
+ * solo body ← --auth-shell-card-padding-block-compact → --card-space-solo-y
239
+ * The block knob is `initial` at :root, so its default resolves HERE against the live inset —
240
+ * canonical output is byte-identical to pre-gh#232 while the knob is now actually consumed.
241
+ * --card-space-solo-y is mapped EXPLICITLY as well as via the shell-y fallback: a solo body
242
+ * takes both block edges symmetrically, and naming it here keeps that contract legible at the
243
+ * one place a reader looks for the compact card's spacing.
244
+ */
245
+ .ui-auth-shell[data-density="compact"] .ui-auth-shell-card {
246
+ --card-space-inset: var(--auth-shell-compact-card-inset);
247
+ --card-space-shell-y: var(
248
+ --auth-shell-card-padding-block-compact,
249
+ var(--auth-shell-compact-card-inset)
250
+ );
251
+ --card-space-body-y: var(--auth-shell-card-body-gap-compact);
252
+ --card-space-solo-y: var(
253
+ --auth-shell-card-padding-block-compact,
254
+ var(--auth-shell-compact-card-inset)
255
+ );
256
+ --card-space-gap: var(--auth-shell-card-gap-compact);
257
+ }
258
+
259
+ .ui-auth-divider {
260
+ display: grid;
261
+ width: 100%;
262
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
263
+ align-items: center;
264
+ gap: var(--auth-shell-divider-gap);
265
+ color: hsl(var(--auth-shell-divider-label-color));
266
+ font-size: var(--auth-shell-divider-label-font-size);
267
+ line-height: 1;
268
+ }
269
+
270
+ .ui-auth-divider-rule {
271
+ height: 1px;
272
+ background: hsl(var(--auth-shell-divider-rule-color));
273
+ }
274
+
275
+ @media (max-width: 30rem) {
276
+ .ui-auth-shell[data-variant="canonical"] {
277
+ --auth-shell-main-padding: var(--auth-shell-canonical-main-padding-mobile);
278
+ }
279
+
280
+ /* Declared after the canonical mobile inset so a preset's mobile gutter wins at equal
281
+ * specificity: device-authorization = 5px inline (card x=5, width=380 at 390px);
282
+ * context-selection = edge-to-edge (0 inline). */
283
+ .ui-auth-shell[data-preset="device-authorization"] {
284
+ --auth-shell-main-padding: var(--auth-shell-device-main-padding-mobile);
285
+ }
286
+
287
+ .ui-auth-shell[data-preset="context-selection"] {
288
+ --auth-shell-main-padding: var(--auth-shell-context-main-padding-mobile);
289
+ }
290
+
291
+ /* account-recovery = 15px inline at 390 (panel x=15, width=360). This is a DECIDED mobile
292
+ * contract — the supplied 390 canonical reference is a desktop 2x2 composite that overflows
293
+ * horizontally and is not a valid source (gh#233). */
294
+ .ui-auth-shell[data-preset="account-recovery"] {
295
+ --auth-shell-main-padding: var(--auth-shell-recovery-main-padding-mobile);
296
+ }
297
+ }
298
+
123
299
  /*
124
300
  * CENTERED SHELL — authenticated, no-sidebar, centred-column page shell (hosted-ID "My Page",
125
301
  * account / self-service, standalone settings). A flex column over the same brand-wash surface as
@@ -165,9 +341,17 @@
165
341
  .ui-centered-shell-column {
166
342
  width: 100%;
167
343
  margin-inline: auto;
344
+ /* Block offset knob — `0` (top-aligned page, the default) or `auto` (viewport-centred system
345
+ * surface via `align="center"`). Auto margins absorb the free space of the flex `main`, and
346
+ * collapse to 0 when the content overflows, so tall content still scrolls from the top. */
347
+ margin-block: var(--centered-shell-column-offset-block);
168
348
  max-width: var(--centered-shell-width-md);
169
349
  }
170
350
 
351
+ .ui-centered-shell-column[data-align="center"] {
352
+ --centered-shell-column-offset-block: auto;
353
+ }
354
+
171
355
  .ui-centered-shell-column[data-width="sm"] {
172
356
  max-width: var(--centered-shell-width-sm);
173
357
  }
@@ -211,11 +395,36 @@
211
395
  margin-inline-end: var(--space-1);
212
396
  }
213
397
 
398
+ /* Drawer scrim = a SHARE of the ONE shared semantic scrim (--overlay-background), resolved at the
399
+ * call site so a scoped [data-tenant]/.dark override of --overlay-background reaches the portaled
400
+ * drawer (docs/TOKENS.md · "Role-mirror knobs MUST be `initial`"). Default resolves to the same
401
+ * rgb(0 0 0 / 0.2) it has always been; setting --app-shell-mobile-nav-background still wins. */
402
+ [data-slot="sheet-overlay"].app-mobile-nav-overlay {
403
+ background-color: var(
404
+ --app-shell-mobile-nav-background,
405
+ color-mix(in srgb, var(--overlay-background) var(--app-shell-mobile-nav-alpha), transparent)
406
+ );
407
+ }
408
+
409
+ .app-mobile-nav-drawer {
410
+ padding-block-start: max(var(--sheet-pad-y), env(safe-area-inset-top));
411
+ padding-block-end: max(var(--sheet-pad-y), env(safe-area-inset-bottom));
412
+ padding-inline-start: max(var(--sheet-pad-x), env(safe-area-inset-left));
413
+ padding-inline-end: max(var(--sheet-pad-x), env(safe-area-inset-right));
414
+ overscroll-behavior: contain;
415
+ }
416
+
214
417
  /* The drawer nav reuses the docked Sidebar node; its `.sb-root { display: contents }` flattens
215
- * into the scrollable SheetBody so sections/footer flow naturally in the panel. */
418
+ * into the scrollable SheetBody so sections/footer flow naturally in the panel.
419
+ * The body's INLINE inset is tokenized on the component itself
420
+ * (`px-[var(--app-shell-mobile-nav-inset)]` in AppShell, which wins over SheetBody's generic
421
+ * `px-[var(--sheet-pad-x)]` because both are utilities) — the nav owns its edge instead of being
422
+ * double-padded (gh#211). Do NOT set `padding-inline` here: this file is `@layer components`, so
423
+ * a Tailwind utility on the element always wins. */
216
424
  .app-mobile-nav-body {
217
425
  display: flex;
218
426
  flex-direction: column;
427
+ overscroll-behavior: contain;
219
428
  }
220
429
 
221
430
  .app-topbar-spacer {
@@ -229,6 +438,21 @@
229
438
  font-size: var(--font-size-base);
230
439
  }
231
440
 
441
+ /*
442
+ * THE ONE AppShell breakpoint — 900px (56.25rem), the DXS canonical value. It is stated in exactly
443
+ * two places and they must agree: this rule and `max-[900px]:inline-flex` on the hamburger in
444
+ * `app-shell.tsx` (a CSS @media cannot read a custom property, so the trigger's visibility has to
445
+ * repeat the number; making it a public knob needs a `data-shell-mode` attribute — tracked in the
446
+ * #213 audit, out of scope here).
447
+ *
448
+ * There used to be a SECOND, conflicting `@media (max-width: 768px)` block further down that also
449
+ * restructured `.app-root`. It (a) dropped the `"footer"` grid area and set `.app-footer{display:
450
+ * none}`, silently deleting whatever a consumer passed to AppShell's `footer` slot on a phone, and
451
+ * (b) re-declared the grid ROW template with a 3rem literal, overriding --app-shell-bar-height so
452
+ * a themed bar height was ignored below 768px only. Between 768 and 900 the sidebar was hidden by
453
+ * THIS rule while the grid still reserved a sidebar track — incoherent. Both are gone; the
454
+ * footer row and the bar-height token now hold at every width.
455
+ */
232
456
  @media (width <= 56.25rem) {
233
457
  .app-root,
234
458
  .app-root[data-collapsed="true"] {
@@ -243,6 +467,11 @@
243
467
  display: none;
244
468
  }
245
469
 
470
+ /* The docked rail is gone, so the bar tightens against the viewport edge (knob, not a literal). */
471
+ .app-topbar {
472
+ padding-inline: var(--app-shell-bar-inset-compact);
473
+ }
474
+
246
475
  .app-topbar-left:has(.app-mobile-sidebar-trigger) {
247
476
  display: flex;
248
477
  }
@@ -256,10 +485,132 @@
256
485
  display: contents;
257
486
  }
258
487
 
488
+ /*
489
+ * ORG SWITCHER — shared Console/Admin tenant control. Widths, insets and state geometry stay
490
+ * inside the library so a consumer never needs `w-64` or descendant-selector overrides.
491
+ */
492
+ .ui-org-switcher {
493
+ width: 100%;
494
+ min-width: 0;
495
+ }
496
+
497
+ .ui-org-switcher[data-collapsed="true"] {
498
+ display: flex;
499
+ justify-content: center;
500
+ }
501
+
502
+ .ui-org-switcher-trigger {
503
+ width: 100%;
504
+ height: var(--org-switcher-trigger-height);
505
+ min-width: 0;
506
+ justify-content: flex-start;
507
+ padding-inline: var(--org-switcher-trigger-padding-x);
508
+ }
509
+
510
+ .ui-org-switcher-trigger[data-collapsed="true"] {
511
+ width: var(--org-switcher-trigger-height);
512
+ padding: 0;
513
+ }
514
+
515
+ .ui-org-switcher-avatar {
516
+ display: grid;
517
+ width: var(--org-switcher-avatar-size);
518
+ height: var(--org-switcher-avatar-size);
519
+ flex: 0 0 auto;
520
+ place-items: center;
521
+ overflow: hidden;
522
+ border-radius: var(--radius);
523
+ background: hsl(var(--secondary));
524
+ color: hsl(var(--secondary-foreground));
525
+ font-size: var(--sidebar-avatar-font-size);
526
+ font-weight: var(--font-weight-semibold);
527
+ }
528
+
529
+ .ui-org-switcher-avatar > * {
530
+ width: 100%;
531
+ height: 100%;
532
+ }
533
+
534
+ .ui-org-switcher-current,
535
+ .ui-org-switcher-option {
536
+ display: grid;
537
+ min-width: 0;
538
+ flex: 1;
539
+ text-align: start;
540
+ }
541
+
542
+ .ui-org-switcher-name,
543
+ .ui-org-switcher-meta {
544
+ overflow: hidden;
545
+ line-height: 1.25;
546
+ text-overflow: ellipsis;
547
+ white-space: nowrap;
548
+ }
549
+
550
+ .ui-org-switcher-name {
551
+ font-size: var(--sidebar-user-name-font-size);
552
+ font-weight: var(--font-weight-medium);
553
+ }
554
+
555
+ .ui-org-switcher-meta {
556
+ color: hsl(var(--org-switcher-meta-foreground));
557
+ font-size: var(--sidebar-user-role-font-size);
558
+ }
559
+
560
+ .ui-org-switcher-chevron,
561
+ .ui-org-switcher-check,
562
+ .ui-org-switcher-spinner {
563
+ width: 1rem;
564
+ height: 1rem;
565
+ flex: 0 0 auto;
566
+ }
567
+
568
+ .ui-org-switcher-chevron {
569
+ margin-inline-start: auto;
570
+ color: hsl(var(--muted-foreground));
571
+ }
572
+
573
+ .ui-org-switcher-check {
574
+ margin-inline-start: auto;
575
+ }
576
+
577
+ .ui-org-switcher-popover {
578
+ width: min(var(--org-switcher-menu-width), calc(100vw - var(--space-4)));
579
+ padding: 0;
580
+ }
581
+
582
+ .ui-org-switcher-sheet {
583
+ max-height: var(--org-switcher-sheet-max-height);
584
+ border-start-start-radius: var(--radius-lg);
585
+ border-start-end-radius: var(--radius-lg);
586
+ }
587
+
588
+ .ui-org-switcher-command {
589
+ width: 100%;
590
+ }
591
+
592
+ .ui-org-switcher-state {
593
+ display: flex;
594
+ min-height: var(--org-switcher-state-min-height);
595
+ flex-direction: column;
596
+ align-items: center;
597
+ justify-content: center;
598
+ gap: var(--space-3);
599
+ padding: var(--space-5);
600
+ color: hsl(var(--muted-foreground));
601
+ text-align: center;
602
+ }
603
+
604
+ .ui-org-switcher-spinner {
605
+ animation: spin 1s linear infinite;
606
+ }
607
+
259
608
  .sb-product,
260
609
  .sb-brand {
261
610
  display: flex;
611
+ min-width: 0;
262
612
  min-height: var(--app-shell-bar-height);
613
+ flex: 0 0 auto;
263
614
  align-items: center;
264
615
  gap: var(--space-2);
265
616
  padding-inline: var(--space-4);
@@ -337,8 +688,10 @@
337
688
 
338
689
  .sb-nav-scroll {
339
690
  flex: 1;
691
+ min-height: 0;
692
+ overscroll-behavior: contain;
340
693
  overflow-y: auto;
341
- padding: var(--space-3) var(--space-2);
694
+ padding: var(--sidebar-nav-scroll-padding);
342
695
  }
343
696
 
344
697
  .sb-section {
@@ -346,11 +699,11 @@
346
699
  }
347
700
 
348
701
  .sb-section + .sb-section {
349
- margin-top: var(--space-4);
702
+ margin-top: var(--sidebar-section-gap);
350
703
  }
351
704
 
352
705
  .sb-section-label {
353
- padding: 0 var(--space-2) var(--space-1);
706
+ padding: 0 var(--sidebar-section-label-padding-x) var(--sidebar-section-label-padding-bottom);
354
707
  color: hsl(var(--muted-foreground));
355
708
  font-size: var(--sidebar-section-label-font-size);
356
709
  font-weight: 700;
@@ -362,7 +715,7 @@
362
715
  .sb-nav {
363
716
  display: flex;
364
717
  flex-direction: column;
365
- gap: 2px;
718
+ gap: var(--sidebar-nav-gap);
366
719
  }
367
720
 
368
721
  .sb-nav-item {
@@ -370,22 +723,27 @@
370
723
  width: 100%;
371
724
  height: var(--sidebar-nav-item-height);
372
725
  min-width: 0;
726
+ flex: 0 0 auto;
373
727
  align-items: center;
374
- gap: 0.625rem;
375
- padding-inline: 0.625rem;
728
+ gap: var(--sidebar-nav-item-gap);
729
+ padding-inline: var(--sidebar-nav-item-padding-x);
376
730
  border: 0;
377
731
  border-radius: calc(var(--radius) - 1px);
378
732
  background: transparent;
379
- color: hsl(var(--muted-foreground));
733
+ /* gh#228 — the row/label foreground is its own knob (default = the muted role, unchanged). The
734
+ * icon reads a SEPARATE knob on `.sb-icon` below, so a service can darken 16px nav icons
735
+ * without re-tinting every muted text. */
736
+ color: var(--sidebar-nav-item-foreground, hsl(var(--muted-foreground)));
380
737
  cursor: pointer;
381
738
  font-family: inherit;
382
739
  font-size: var(--sidebar-nav-item-font-size);
740
+ line-height: 1;
383
741
  text-align: start;
384
742
  }
385
743
 
386
744
  .sb-nav-item:hover {
387
745
  background: hsl(var(--accent));
388
- color: hsl(var(--foreground));
746
+ color: var(--sidebar-nav-item-hover-foreground, hsl(var(--foreground)));
389
747
  }
390
748
 
391
749
  /* Active item reads its own bg/fg tokens (defaults = the hover look) so a service can give the
@@ -399,15 +757,24 @@
399
757
 
400
758
  .sb-icon {
401
759
  display: grid;
402
- width: 1rem;
403
- height: 1rem;
760
+ width: var(--sidebar-nav-icon-size);
761
+ height: var(--sidebar-nav-icon-size);
404
762
  flex: 0 0 auto;
763
+ line-height: 0;
405
764
  place-items: center;
765
+ /* gh#228 — the nav ICON owns its own foreground knob; the default `currentColor` resolves to the
766
+ * row colour, so the resting look is byte-identical to before. A service sets
767
+ * `--sidebar-nav-icon-foreground` alone to get canonical darker icons beside muted labels — no
768
+ * page-local selector, no global `--muted-foreground` re-tint. Colour only: size/geometry stay
769
+ * on --sidebar-nav-icon-size. */
770
+ color: var(--sidebar-nav-icon-foreground, currentColor);
406
771
  }
407
772
 
408
773
  .sb-icon svg {
409
- width: 1rem;
410
- height: 1rem;
774
+ display: block;
775
+ width: var(--sidebar-nav-icon-size);
776
+ height: var(--sidebar-nav-icon-size);
777
+ flex: 0 0 auto;
411
778
  }
412
779
 
413
780
  .sb-label {
@@ -464,7 +831,9 @@
464
831
  .sb-nav-item--sub {
465
832
  height: 1.75rem;
466
833
  font-size: var(--sidebar-nav-sub-font-size);
467
- color: hsl(var(--muted-foreground));
834
+ /* Same row knob as a top-level row (gh#228) — one override themes the whole nav, sub rows
835
+ * included. Default unchanged. */
836
+ color: var(--sidebar-nav-item-foreground, hsl(var(--muted-foreground)));
468
837
  }
469
838
 
470
839
  .sb-nav-item--sub::before {
@@ -479,7 +848,7 @@
479
848
 
480
849
  .sb-nav-item--sub:hover {
481
850
  background: transparent;
482
- color: hsl(var(--foreground));
851
+ color: var(--sidebar-nav-item-hover-foreground, hsl(var(--foreground)));
483
852
  }
484
853
 
485
854
  .sb-nav-item--sub[data-active="true"] {
@@ -493,6 +862,42 @@
493
862
  opacity: 1;
494
863
  }
495
864
 
865
+ /* gh#228 — nav ICON state knobs. Declared AFTER the row-state rules so they win at equal
866
+ * specificity, in the same order the row states resolve (hover → active → disabled). Each falls
867
+ * back to the base icon knob and finally to `currentColor` (= the row colour for that state), so
868
+ * the defaults are byte-identical to the pre-gh#228 rendering and setting only
869
+ * `--sidebar-nav-icon-foreground` themes every state at once. Colour only — no geometry. */
870
+ .sb-nav-item:hover .sb-icon {
871
+ color: var(
872
+ --sidebar-nav-icon-hover-foreground,
873
+ var(--sidebar-nav-icon-foreground, currentColor)
874
+ );
875
+ }
876
+
877
+ .sb-nav-item[data-active="true"] .sb-icon {
878
+ color: var(
879
+ --sidebar-nav-icon-active-foreground,
880
+ var(--sidebar-nav-icon-foreground, currentColor)
881
+ );
882
+ }
883
+
884
+ /* Disabled rows (`aria-disabled="true"`, set by SidebarItem for item.disabled). Defaults resolve
885
+ * to the RESTING row/icon colour, so a disabled row reads as inert in every state; a service dims
886
+ * it further with the two disabled knobs. */
887
+ .sb-nav-item[aria-disabled="true"] {
888
+ color: var(
889
+ --sidebar-nav-item-disabled-foreground,
890
+ var(--sidebar-nav-item-foreground, hsl(var(--muted-foreground)))
891
+ );
892
+ }
893
+
894
+ .sb-nav-item[aria-disabled="true"] .sb-icon {
895
+ color: var(
896
+ --sidebar-nav-icon-disabled-foreground,
897
+ var(--sidebar-nav-icon-foreground, currentColor)
898
+ );
899
+ }
900
+
496
901
  /* Collapsed-rail flyout — PORTALED (Radix Popover) so it escapes the sidebar's overflow and is
497
902
  * never clipped: names a leaf item (tooltip) or reveals a group's submenu on hover/focus. */
498
903
  .sb-flyout-pop {
@@ -640,12 +1045,40 @@
640
1045
  }
641
1046
 
642
1047
  /* Topbar — a pure slot bar. start (inline-start) · center (grows, centered) · end (inline-end).
643
- * The library owns ONLY this positioning; every control inside is the consumer's to compose. */
1048
+ * The library owns ONLY this positioning; every control inside is the consumer's to compose.
1049
+ *
1050
+ * SHRINK CONTRACT (gh#226) — a hosted console packs an intrinsically wide brand/tenant string
1051
+ * into `start` and a fixed-width search trigger into `center`; at a compact desktop width
1052
+ * (1024px minus a 16rem sidebar) that content used to push `end` (locale picker, user menu) past
1053
+ * the bar and out of the viewport, and leak a horizontal document scroll. The contract is:
1054
+ * root — never wider than its AppShell grid/flex allocation (`max-width:100%` +
1055
+ * `min-width:0`, so it shrinks whether the parent is a grid track or a flex line);
1056
+ * start — shrinks first, its content clipped INSIDE the slot (never spills over center/end);
1057
+ * center — `flex-basis:0`, so it yields all of its box before `start` gives up a pixel;
1058
+ * end — `flex:0 0 auto`, so it keeps its natural width and stays anchored inline-end.
1059
+ * Slots clip with `clip`, NOT `hidden`: a `hidden` box is still a scroll container, so focusing a
1060
+ * clipped control would scroll the slot and shove its leading content out of view. The clip
1061
+ * margin keeps the focus ring of an edge control paintable (WCAG 2.4.11 / 2.4.13).
1062
+ *
1063
+ * BOX KNOBS (rule #45) — --topbar-height / --topbar-inset / --topbar-gap. Their defaults are the
1064
+ * QUIET ones (`auto` / `0` / the previous 8px), i.e. byte-identical rendering to before they
1065
+ * existed: inside AppShell the `.app-topbar` grid row still owns the height and inset. A service
1066
+ * that mounts <Topbar> DIRECTLY on a page (no AppShell) now sizes it from the theme —
1067
+ * `--topbar-height: 3.5rem; --topbar-inset: var(--space-4)` — instead of an app-local class.
1068
+ * `--topbar-gap` re-rhythms the whole bar at once: it is both the gap BETWEEN the three clusters
1069
+ * and the gap INSIDE each of them, so a denser bar stays internally consistent.
1070
+ * The shrink contract above is unaffected — none of these touch flex/overflow. */
644
1071
  .ui-topbar {
645
1072
  display: flex;
646
- width: 100%;
1073
+ height: var(--topbar-height);
1074
+ width: auto;
1075
+ max-width: 100%;
1076
+ min-width: 0;
1077
+ flex: 1 1 0%;
647
1078
  align-items: center;
648
- gap: var(--space-2);
1079
+ gap: var(--topbar-gap);
1080
+ padding-inline: var(--topbar-inset);
1081
+ overflow: hidden;
649
1082
  }
650
1083
  .ui-topbar-start,
651
1084
  .ui-topbar-center,
@@ -653,13 +1086,20 @@
653
1086
  display: flex;
654
1087
  min-width: 0;
655
1088
  align-items: center;
656
- gap: var(--space-2);
1089
+ gap: var(--topbar-gap);
1090
+ overflow: clip;
1091
+ overflow-clip-margin: var(--focus-ring-width);
1092
+ }
1093
+ .ui-topbar-start {
1094
+ flex: 0 1 auto;
657
1095
  }
658
1096
  .ui-topbar-center {
659
- flex: 1 1 auto;
1097
+ flex: 1 1 0%;
660
1098
  justify-content: center;
661
1099
  }
662
1100
  .ui-topbar-end {
1101
+ max-width: 100%;
1102
+ flex: 0 0 auto;
663
1103
  margin-inline-start: auto;
664
1104
  }
665
1105
 
@@ -807,27 +1247,18 @@
807
1247
  background: hsl(var(--attention));
808
1248
  }
809
1249
 
1250
+ /* TOPBAR CHIP/SEARCH DENSITY ONLY — this is NOT a second shell breakpoint. The `.app-root`
1251
+ * restructure that used to live here (a 768px duplicate of the 900px rule that also deleted the
1252
+ * footer row and re-declared the grid rows with a 3rem literal) is gone; see the note on the one
1253
+ * `@media (width <= 56.25rem)` block above. What remains is purely the optional `.tb-*` console
1254
+ * chrome collapsing to icon-only on a phone, which is independent of where the shell switches to
1255
+ * its drawer. */
810
1256
  @media (max-width: 768px) {
811
- .app-root,
812
- .app-root[data-collapsed="true"] {
813
- grid-template-areas:
814
- "topbar"
815
- "main";
816
- grid-template-columns: minmax(0, 1fr);
817
- grid-template-rows: 3rem minmax(0, 1fr);
818
- }
819
-
820
- .app-sidebar,
821
- .app-footer,
822
1257
  .tb-chip-sep,
823
1258
  .tb-search .kbd {
824
1259
  display: none;
825
1260
  }
826
1261
 
827
- .app-topbar {
828
- padding-inline: var(--space-3);
829
- }
830
-
831
1262
  .tb-search {
832
1263
  width: auto;
833
1264
  min-width: var(--control-height);
@@ -838,3 +1269,20 @@
838
1269
  }
839
1270
  }
840
1271
  }
1272
+
1273
+ /*
1274
+ * REDUCED MOTION for the AppShell mobile drawer (WCAG 2.3.3 / 2.2.2). The drawer is a Sheet, so it
1275
+ * animates via Tailwind `data-[state]:animate-in|animate-out` + `slide-in-from-*` utilities.
1276
+ * DELIBERATELY UNLAYERED: Tailwind v4 emits those into `@layer utilities`, which beats any rule in
1277
+ * `@layer components` regardless of specificity — this gate was previously written inside the
1278
+ * components layer above and could never win. Same reasoning as the overlay gate in
1279
+ * dialog-layout.css (which also covers this drawer via [data-slot="sheet-*"]); kept here as well so
1280
+ * a consumer importing only `styles/shell-layout` still gets it.
1281
+ */
1282
+ @media (prefers-reduced-motion: reduce) {
1283
+ .app-mobile-nav-overlay,
1284
+ .app-mobile-nav-drawer {
1285
+ animation: none;
1286
+ transition: none;
1287
+ }
1288
+ }