@godxjp/ui 18.4.0 → 18.6.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 (159) 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 +30 -0
  9. package/dist/components/charts/compact-bar-trend.js +74 -0
  10. package/dist/components/charts/index.d.ts +6 -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 +40 -4
  15. package/dist/components/data-display/data-table.js +69 -2
  16. package/dist/components/data-display/empty-state.js +1 -1
  17. package/dist/components/data-display/index.d.ts +4 -2
  18. package/dist/components/data-display/index.js +10 -1
  19. package/dist/components/data-display/list-row.d.ts +41 -4
  20. package/dist/components/data-display/list-row.js +23 -3
  21. package/dist/components/data-display/qr-code.d.ts +1 -1
  22. package/dist/components/data-display/service-launcher-card.d.ts +40 -0
  23. package/dist/components/data-display/service-launcher-card.js +97 -0
  24. package/dist/components/data-display/table.d.ts +47 -2
  25. package/dist/components/data-display/table.js +29 -4
  26. package/dist/components/data-entry/command-palette.d.ts +37 -0
  27. package/dist/components/data-entry/command-palette.js +125 -0
  28. package/dist/components/data-entry/command.d.ts +10 -10
  29. package/dist/components/data-entry/index.d.ts +3 -0
  30. package/dist/components/data-entry/index.js +2 -0
  31. package/dist/components/data-entry/input-otp.d.ts +1 -0
  32. package/dist/components/feedback/dialog.d.ts +16 -1
  33. package/dist/components/feedback/dialog.js +46 -33
  34. package/dist/components/feedback/index.d.ts +5 -2
  35. package/dist/components/feedback/index.js +8 -2
  36. package/dist/components/feedback/sheet.d.ts +26 -4
  37. package/dist/components/feedback/sheet.js +54 -7
  38. package/dist/components/feedback/two-factor-setup.d.ts +31 -0
  39. package/dist/components/feedback/two-factor-setup.js +97 -0
  40. package/dist/components/general/index.d.ts +1 -1
  41. package/dist/components/general/logo.d.ts +49 -9
  42. package/dist/components/general/logo.js +67 -14
  43. package/dist/components/layout/app-shell.d.ts +1 -1
  44. package/dist/components/layout/app-shell.js +55 -30
  45. package/dist/components/layout/auth-account-summary.d.ts +5 -0
  46. package/dist/components/layout/auth-account-summary.js +31 -0
  47. package/dist/components/layout/auth-divider.d.ts +7 -0
  48. package/dist/components/layout/auth-divider.js +21 -0
  49. package/dist/components/layout/auth-footer.d.ts +5 -0
  50. package/dist/components/layout/auth-footer.js +24 -0
  51. package/dist/components/layout/auth-identity.d.ts +5 -0
  52. package/dist/components/layout/auth-identity.js +17 -0
  53. package/dist/components/layout/auth-shell.d.ts +9 -1
  54. package/dist/components/layout/auth-shell.js +25 -6
  55. package/dist/components/layout/auth-stack.d.ts +4 -0
  56. package/dist/components/layout/auth-stack.js +8 -0
  57. package/dist/components/layout/centered-shell.d.ts +5 -2
  58. package/dist/components/layout/centered-shell.js +29 -19
  59. package/dist/components/layout/error-surface.d.ts +35 -0
  60. package/dist/components/layout/error-surface.js +155 -0
  61. package/dist/components/layout/flex.d.ts +1 -1
  62. package/dist/components/layout/flex.js +4 -0
  63. package/dist/components/layout/index.d.ts +23 -3
  64. package/dist/components/layout/index.js +21 -2
  65. package/dist/components/layout/legal-document-shell.d.ts +34 -0
  66. package/dist/components/layout/legal-document-shell.js +186 -0
  67. package/dist/components/layout/master-detail.d.ts +24 -0
  68. package/dist/components/layout/master-detail.js +48 -0
  69. package/dist/components/layout/org-switcher.d.ts +9 -0
  70. package/dist/components/layout/org-switcher.js +218 -0
  71. package/dist/components/layout/page-container.d.ts +1 -1
  72. package/dist/components/layout/page-container.js +7 -2
  73. package/dist/components/layout/sidebar-link.d.ts +43 -0
  74. package/dist/components/layout/sidebar-link.js +13 -0
  75. package/dist/components/layout/sidebar.d.ts +42 -5
  76. package/dist/components/layout/sidebar.js +170 -85
  77. package/dist/components/navigation/app-setting-picker.js +17 -6
  78. package/dist/components/navigation/filter-bar.d.ts +12 -3
  79. package/dist/components/navigation/filter-bar.js +8 -2
  80. package/dist/components/navigation/index.d.ts +2 -2
  81. package/dist/components/navigation/index.js +3 -1
  82. package/dist/components/navigation/steps.js +38 -19
  83. package/dist/components/navigation/tabs-scroll.d.ts +70 -0
  84. package/dist/components/navigation/tabs-scroll.js +76 -0
  85. package/dist/components/navigation/tabs.js +47 -23
  86. package/dist/components/ui/avatar.d.ts +2 -1
  87. package/dist/components/ui/avatar.js +2 -1
  88. package/dist/components/ui/input-otp.d.ts +4 -1
  89. package/dist/components/ui/input-otp.js +2 -1
  90. package/dist/components/ui/toggle-group.d.ts +7 -4
  91. package/dist/components/ui/toggle-group.js +32 -21
  92. package/dist/email/brand-mark.d.ts +108 -0
  93. package/dist/email/brand-mark.js +88 -0
  94. package/dist/email/color.d.ts +53 -0
  95. package/dist/email/color.js +37 -0
  96. package/dist/email/geometry.d.ts +120 -0
  97. package/dist/email/geometry.js +90 -0
  98. package/dist/email/index.d.ts +83 -0
  99. package/dist/email/index.js +82 -0
  100. package/dist/email/inline-style.d.ts +27 -0
  101. package/dist/email/inline-style.js +26 -0
  102. package/dist/email/tokens.generated.d.ts +167 -0
  103. package/dist/email/tokens.generated.js +162 -0
  104. package/dist/email/urgency.d.ts +22 -0
  105. package/dist/email/urgency.js +14 -0
  106. package/dist/i18n/messages/en.json +36 -3
  107. package/dist/i18n/messages/ja.json +36 -3
  108. package/dist/i18n/messages/vi.json +36 -3
  109. package/dist/inertia/index.d.ts +50 -0
  110. package/dist/inertia/index.js +5 -0
  111. package/dist/props/components/app.prop.d.ts +11 -1
  112. package/dist/props/components/charts.prop.d.ts +40 -1
  113. package/dist/props/components/data-display.prop.d.ts +75 -1
  114. package/dist/props/components/data-entry.prop.d.ts +6 -0
  115. package/dist/props/components/feedback.prop.d.ts +10 -0
  116. package/dist/props/components/index.d.ts +3 -3
  117. package/dist/props/components/layout.prop.d.ts +560 -10
  118. package/dist/props/components/layout.prop.js +1 -0
  119. package/dist/props/components/navigation.prop.d.ts +24 -2
  120. package/dist/props/registry.d.ts +433 -8
  121. package/dist/props/registry.js +527 -9
  122. package/dist/props/vocabulary/data.prop.d.ts +27 -0
  123. package/dist/props/vocabulary/index.d.ts +3 -3
  124. package/dist/props/vocabulary/interaction.prop.d.ts +10 -1
  125. package/dist/props/vocabulary/layout.prop.d.ts +49 -0
  126. package/dist/styles/base.css +8 -2
  127. package/dist/styles/card-layout.css +161 -7
  128. package/dist/styles/chart-layout.css +97 -0
  129. package/dist/styles/control.css +76 -2
  130. package/dist/styles/data-display-layout.css +78 -6
  131. package/dist/styles/dialog-layout.css +105 -4
  132. package/dist/styles/fonts.css +15 -4
  133. package/dist/styles/index.css +11 -3
  134. package/dist/styles/layout.css +466 -1
  135. package/dist/styles/logo-layout.css +95 -0
  136. package/dist/styles/navigation-layout.css +152 -0
  137. package/dist/styles/shell-layout.css +790 -40
  138. package/dist/styles/table-layout.css +164 -1
  139. package/dist/theme/dxs.canonical.css +77 -0
  140. package/dist/tokens/axes.css +40 -0
  141. package/dist/tokens/base.css +6 -0
  142. package/dist/tokens/components/card.css +48 -0
  143. package/dist/tokens/components/chart.css +47 -0
  144. package/dist/tokens/components/control.css +16 -0
  145. package/dist/tokens/components/data-display.css +13 -0
  146. package/dist/tokens/components/email.css +93 -0
  147. package/dist/tokens/components/error-surface.css +36 -0
  148. package/dist/tokens/components/feedback.css +17 -0
  149. package/dist/tokens/components/legal-document.css +67 -0
  150. package/dist/tokens/components/list-row.css +43 -2
  151. package/dist/tokens/components/logo.css +74 -0
  152. package/dist/tokens/components/navigation.css +28 -0
  153. package/dist/tokens/components/sheet.css +19 -0
  154. package/dist/tokens/components/shell.css +210 -0
  155. package/dist/tokens/components/sidebar.css +33 -0
  156. package/dist/tokens/components/table.css +45 -0
  157. package/dist/tokens/foundation.css +31 -2
  158. package/dist/tokens/semantic/layout.css +69 -2
  159. package/package.json +22 -5
@@ -195,6 +195,102 @@
195
195
  min-width: 0;
196
196
  }
197
197
 
198
+ /* MasterDetail owns its tracks AND its collapse threshold — both token-driven (gh#223).
199
+ *
200
+ * WHY FLEX AND NOT `@container (min-width: …)`: a media/container-query CONDITION cannot read a
201
+ * `var()` — it is resolved before custom properties, so a tokenised breakpoint is impossible
202
+ * there (the reason the first cut hard-coded `40rem`, violating rule #45). The flex threshold
203
+ * below ("Holy Albatross") puts the breakpoint in a `calc()`, where a `var()` DOES resolve, so
204
+ * `--master-detail-collapse-below` is a genuine knob a service theme can retune once globally.
205
+ *
206
+ * How it reads: the fluid region's flex-basis is `(threshold - 100%) * 999`, where 100% is the
207
+ * composition's own inline size (so, like the old container query, the decision comes from the
208
+ * space available to the layout — never the viewport).
209
+ * · width < threshold → a huge positive basis → each region takes a whole flex line and
210
+ * shrinks to it: master stacked above detail, both full width.
211
+ * · width ≥ threshold → the calc goes negative and is clamped to 0 → one line: the fluid
212
+ * region absorbs the free space while the rail keeps its token width (`max()` keeps the
213
+ * rail's basis at the preset because a negative operand can never win a max).
214
+ *
215
+ * `data-rail` picks WHICH region is the fixed rail. Default `detail` = the canonical
216
+ * "fluid list + fixed detail rail" (1fr / 320px) composition; `master` puts the fixed rail on
217
+ * the leading edge for a category/navigator list. DOM order is always master → detail, so the
218
+ * stacked order is list-then-detail either way, and the flex row follows the writing direction
219
+ * (RTL-safe without a single physical property). */
220
+ .ui-master-detail {
221
+ display: flex;
222
+ flex-wrap: wrap;
223
+ gap: var(--master-detail-gap);
224
+ --master-detail-rail-size: var(--master-detail-rail-standard);
225
+ }
226
+
227
+ .ui-master-detail[data-rail-width="compact"] {
228
+ --master-detail-rail-size: var(--master-detail-rail-compact);
229
+ }
230
+
231
+ /* Per-instance override of the token threshold (theme sets it globally, prop overrides here). */
232
+ .ui-master-detail[data-collapse-below="false"] {
233
+ --master-detail-collapse-below: 0px;
234
+ }
235
+ .ui-master-detail[data-collapse-below="sm"] {
236
+ --master-detail-collapse-below: 40rem;
237
+ }
238
+ .ui-master-detail[data-collapse-below="md"] {
239
+ --master-detail-collapse-below: 48rem;
240
+ }
241
+ .ui-master-detail[data-collapse-below="lg"] {
242
+ --master-detail-collapse-below: 64rem;
243
+ }
244
+ .ui-master-detail[data-collapse-below="xl"] {
245
+ --master-detail-collapse-below: 80rem;
246
+ }
247
+
248
+ .ui-master-detail-master,
249
+ .ui-master-detail-detail {
250
+ min-inline-size: 0;
251
+ }
252
+
253
+ /* The fluid region. */
254
+ .ui-master-detail[data-rail="detail"] > .ui-master-detail-master,
255
+ .ui-master-detail[data-rail="master"] > .ui-master-detail-detail {
256
+ flex: 1 1 calc((var(--master-detail-collapse-below) - 100%) * 999);
257
+ }
258
+
259
+ /* The fixed rail. */
260
+ .ui-master-detail[data-rail="detail"] > .ui-master-detail-detail,
261
+ .ui-master-detail[data-rail="master"] > .ui-master-detail-master {
262
+ flex: 0 1
263
+ max(var(--master-detail-rail-size), calc((var(--master-detail-collapse-below) - 100%) * 999));
264
+ }
265
+
266
+ /* Bounded master viewport (gh#231) — OPT-IN via `masterViewport`. `auto` (the default) emits no
267
+ * bound at all, so an existing composition renders byte-identically; the two presets cap the
268
+ * collection's BLOCK size with a token and scroll it INSIDE the region, which is what keeps a
269
+ * long real collection from pushing the detail thousands of pixels below the fold once the
270
+ * layout stacks. Block axis only — the inline axis stays owned by the flex threshold above, so
271
+ * the bound is writing-direction agnostic (RTL-safe).
272
+ *
273
+ * A11y: the region carries tabIndex={0} in this mode (see master-detail.tsx) so the scroll
274
+ * container is reachable and scrollable by keyboard alone, and the inset below reserves room
275
+ * for a focused row's focus ring — an overflow container clips BOTH axes, so a ring drawn at
276
+ * the region's edge would otherwise be cut. Focus is never trapped: the region is a plain tab
277
+ * stop that Tab/Shift+Tab leave normally. */
278
+ .ui-master-detail[data-master-viewport="compact"] > .ui-master-detail-master,
279
+ .ui-master-detail[data-master-viewport="standard"] > .ui-master-detail-master {
280
+ overflow-y: auto;
281
+ overscroll-behavior: contain;
282
+ padding: var(--master-detail-master-viewport-inset);
283
+ scroll-padding-block: var(--master-detail-master-viewport-inset);
284
+ }
285
+
286
+ .ui-master-detail[data-master-viewport="compact"] > .ui-master-detail-master {
287
+ max-block-size: var(--master-detail-master-viewport-compact);
288
+ }
289
+
290
+ .ui-master-detail[data-master-viewport="standard"] > .ui-master-detail-master {
291
+ max-block-size: var(--master-detail-master-viewport-standard);
292
+ }
293
+
198
294
  @container responsive-grid (min-width: 40rem) {
199
295
  .ui-responsive-grid {
200
296
  grid-template-columns: repeat(var(--responsive-grid-sm, 2), minmax(0, 1fr));
@@ -315,6 +411,29 @@
315
411
  max-width: 42rem;
316
412
  }
317
413
 
414
+ /* `measure` (gh#245 / gh#247) — a bounded page measure axis ORTHOGONAL to `variant`, so the
415
+ * quiet `ghost` chrome and a narrow/medium measure finally compose (they used to be the same
416
+ * variant axis, which is exactly why a notification feed could not have both). Unlike
417
+ * `variant="narrow"` — which caps only the body and leaves the header `extra` action stranded
418
+ * at the page edge — this caps the HEADER and the BODY to ONE shared measure, so the action
419
+ * ends on the same edge as the body surface. Both elements carry the identical page gutters,
420
+ * so their content edges line up exactly at both ends.
421
+ *
422
+ * `measure="default"` is the inert default: it emits `data-measure="default"`, which matches NO
423
+ * rule here, so every existing page renders byte-identically (the gh#231 `data-layout="stack"`
424
+ * precedent). The cap is a max, never a width — below it the page stays fluid, so 390px keeps
425
+ * the full compact-gutter surface. The footer is deliberately NOT capped: its border/background
426
+ * is page-level chrome that must span the page when `stickyFooter` pins it. */
427
+ .ui-page-container[data-measure="narrow"] .ui-page-header,
428
+ .ui-page-container[data-measure="narrow"] .ui-page-body {
429
+ max-inline-size: var(--page-measure-narrow);
430
+ }
431
+
432
+ .ui-page-container[data-measure="medium"] .ui-page-header,
433
+ .ui-page-container[data-measure="medium"] .ui-page-body {
434
+ max-inline-size: var(--page-measure-medium);
435
+ }
436
+
318
437
  /* flush = full-bleed body (DataTable); header/footer keep their inset. */
319
438
  .ui-page-container--flush .ui-page-body {
320
439
  padding-inline-start: 0;
@@ -435,6 +554,34 @@
435
554
  }
436
555
  }
437
556
 
557
+ /* `headerLayout="responsive-inline"` (gh#231): below the 640px step the DEFAULT arrangement
558
+ * drops `extra` onto its own full-width line under the subtitle, which pushes a compact header
559
+ * control (a member search, one primary action) off the title row on a 390px screen. This
560
+ * arrangement keeps it beside the title band at a token-owned measure instead, and lets the
561
+ * title/subtitle wrap into the space that is left. Scoped to the compact range only, so at
562
+ * >=640px both arrangements resolve to the identical row below — and a page that never sets
563
+ * `headerLayout` never matches these rules at all. */
564
+ @media (max-width: 639.98px) {
565
+ .ui-page-header[data-layout="responsive-inline"] .ui-page-header-row {
566
+ flex-direction: row;
567
+ align-items: flex-start;
568
+ justify-content: space-between;
569
+ gap: var(--space-inline-md);
570
+ }
571
+
572
+ .ui-page-header[data-layout="responsive-inline"] .ui-page-header-heading {
573
+ flex: 1 1 auto;
574
+ min-inline-size: 0;
575
+ }
576
+
577
+ .ui-page-header[data-layout="responsive-inline"] .ui-page-header-extra {
578
+ flex: 0 0 var(--page-header-extra-measure);
579
+ inline-size: var(--page-header-extra-measure);
580
+ max-inline-size: 100%;
581
+ justify-content: flex-end;
582
+ }
583
+ }
584
+
438
585
  @media (min-width: 640px) {
439
586
  .ui-page-header-row {
440
587
  flex-direction: row;
@@ -550,7 +697,8 @@
550
697
  .ui-empty-state-description {
551
698
  font-size: var(--font-size-base);
552
699
  color: hsl(var(--muted-foreground));
553
- max-width: 28rem;
700
+ /* Measure knob (rule #45) — default 28rem, retunable per service/locale. */
701
+ max-width: var(--empty-state-description-max-width);
554
702
  }
555
703
 
556
704
  .ui-empty-state-icon {
@@ -586,6 +734,20 @@
586
734
  }
587
735
  }
588
736
 
737
+ @layer components {
738
+ .ui-page-container[data-preset="admin-collection"] {
739
+ --control-height: var(--admin-collection-control-height);
740
+ --space-section-active: var(--admin-collection-section-gap);
741
+ --filter-bar-padding-y: var(--admin-collection-toolbar-padding-y);
742
+ --table-row-height: var(--admin-collection-table-row-height);
743
+ --table-cell-padding-y: var(--admin-collection-table-cell-padding-y);
744
+ }
745
+
746
+ .ui-page-container[data-preset="admin-collection"] .ui-toolbar .ui-search-input {
747
+ inline-size: min(100%, var(--admin-collection-search-measure));
748
+ }
749
+ }
750
+
589
751
  @layer components {
590
752
  /* Pagination is ONE horizontal row and never wraps to a second desktop line (gh#153 — the old
591
753
  * `flex-wrap: wrap` split the range / size-changer / page strip across rows). The number strip
@@ -755,4 +917,307 @@
755
917
  color: hsl(var(--muted-foreground));
756
918
  font-size: var(--filter-label-font-size);
757
919
  }
920
+
921
+ /* Toolbar/FilterBar `overflow="scroll"` (#216) — one bounded row that scrolls INLINE instead of
922
+ * wrapping onto extra rows. Below 640px the strip still stacks (the `wrap` base rules above own
923
+ * that): a 390px viewport must never hide a filter behind a horizontal scroll it cannot see.
924
+ * `overflow-x` matches the rest of the package's scroll regions (.ui-data-table-scroll, tabs
925
+ * list) and the browser already mirrors its scroll origin under `dir="rtl"`; the INSET/margin
926
+ * properties below stay logical. No `tabindex` is needed — every group holds a focusable
927
+ * control, so the region is keyboard-reachable (axe scrollable-region-focusable / WCAG 2.1.1). */
928
+ @media (min-width: 640px) {
929
+ .ui-toolbar[data-overflow="scroll"] {
930
+ flex-wrap: nowrap;
931
+ overflow-x: auto;
932
+ overscroll-behavior-x: contain;
933
+ scrollbar-gutter: stable;
934
+ padding-block-end: var(--filter-bar-scroll-padding-y);
935
+ }
936
+
937
+ .ui-toolbar[data-overflow="scroll"] > .ui-toolbar-group {
938
+ flex: 0 0 auto;
939
+ }
940
+
941
+ /* The clear action stays parked at the inline end of the VIEWPORT of the scroll region, not at
942
+ * the end of its scroll width — `margin-inline-start: auto` would push it past the fold and
943
+ * make "clear all" unreachable without scrolling. */
944
+ .ui-toolbar[data-overflow="scroll"] > .ui-toolbar-clear {
945
+ position: sticky;
946
+ inset-inline-end: 0;
947
+ margin-inline-start: auto;
948
+ flex: 0 0 auto;
949
+ /* Above the scrolling groups, and opaque, so a filter never slides underneath it. */
950
+ z-index: 1;
951
+ background: hsl(var(--filter-bar-sticky-background, var(--background)));
952
+ }
953
+ }
954
+
955
+ /* ── LegalDocumentShell (#222) ─────────────────────────────────────────────────
956
+ * The long-form legal/policy document surface: a readable, measure-capped document
957
+ * column with a STICKY table-of-contents rail beside it. Every value reads a
958
+ * `--legal-document-*` component token, so a service re-tunes the measure, the rail
959
+ * and the section rhythm from theme.css with zero consumer `.legal-*` CSS.
960
+ *
961
+ * It OWNS its query container (`container: legal-document / inline-size`, the SplitPane
962
+ * precedent — gh#165), so the one-column ⇄ two-column decision comes from the SHELL's
963
+ * own available width, not the viewport: dropped into a narrow pane on a 1440px screen
964
+ * it correctly stays single-column.
965
+ *
966
+ * DOM order is header → rail (contents) → sections → footer, which is exactly the
967
+ * mobile reading order; the desktop branch only re-places those boxes into a grid.
968
+ */
969
+ .ui-legal-document-scope {
970
+ container: legal-document / inline-size;
971
+ }
972
+
973
+ .ui-legal-document {
974
+ display: grid;
975
+ grid-template-columns: minmax(0, 1fr);
976
+ row-gap: var(--legal-document-section-gap);
977
+ align-items: start;
978
+ min-width: 0;
979
+ }
980
+
981
+ .ui-legal-document-header {
982
+ display: flex;
983
+ flex-direction: column;
984
+ gap: var(--legal-document-header-gap);
985
+ min-width: 0;
986
+ max-width: var(--legal-document-measure-max-width);
987
+ border-block-end: var(--legal-document-header-border);
988
+ }
989
+
990
+ .ui-legal-document-meta {
991
+ display: flex;
992
+ flex-wrap: wrap;
993
+ align-items: baseline;
994
+ gap: var(--legal-document-meta-gap);
995
+ font-size: var(--legal-document-meta-font-size);
996
+ color: hsl(var(--legal-document-meta-foreground, var(--muted-foreground)));
997
+ }
998
+
999
+ /* Long-form prose wrapping that holds in JA (kinsoku via `line-break: strict`), in EN
1000
+ * (long compounds / URLs via `overflow-wrap: break-word`) and in VI (diacritic-heavy
1001
+ * words must NOT be broken mid-word — `word-break: normal` keeps that true). */
1002
+ .ui-legal-document-summary,
1003
+ .ui-legal-document-section-body {
1004
+ min-width: 0;
1005
+ line-height: var(--legal-document-body-line-height);
1006
+ overflow-wrap: break-word;
1007
+ word-break: normal;
1008
+ line-break: strict;
1009
+ }
1010
+
1011
+ .ui-legal-document-summary {
1012
+ color: hsl(var(--legal-document-summary-foreground, var(--muted-foreground)));
1013
+ }
1014
+
1015
+ .ui-legal-document-rail {
1016
+ display: flex;
1017
+ flex-direction: column;
1018
+ gap: var(--legal-document-nav-gap);
1019
+ min-width: 0;
1020
+ }
1021
+
1022
+ .ui-legal-document-toc {
1023
+ display: flex;
1024
+ flex-direction: column;
1025
+ gap: var(--legal-document-toc-gap);
1026
+ min-width: 0;
1027
+ min-height: 0;
1028
+ border-inline-end: var(--legal-document-toc-border);
1029
+ }
1030
+
1031
+ .ui-legal-document-toc-title {
1032
+ font-size: var(--legal-document-toc-title-font-size);
1033
+ color: hsl(var(--legal-document-toc-foreground, var(--muted-foreground)));
1034
+ }
1035
+
1036
+ .ui-legal-document-toc-list {
1037
+ display: flex;
1038
+ flex-direction: column;
1039
+ gap: var(--legal-document-toc-gap);
1040
+ margin: 0;
1041
+ padding: 0;
1042
+ list-style: none;
1043
+ min-width: 0;
1044
+ min-height: 0;
1045
+ }
1046
+
1047
+ /* REAL anchors — `href="#<section id>"` — so the contents list is keyboard-reachable,
1048
+ * middle-clickable and deep-linkable even before JS boots. */
1049
+ .ui-legal-document-toc-link {
1050
+ display: block;
1051
+ padding: var(--legal-document-toc-item-padding);
1052
+ border-radius: var(--legal-document-toc-item-radius);
1053
+ border-inline-start: var(--legal-document-toc-marker-width) solid transparent;
1054
+ font-size: var(--legal-document-toc-font-size);
1055
+ line-height: 1.6;
1056
+ color: hsl(var(--legal-document-toc-foreground, var(--muted-foreground)));
1057
+ text-decoration: none;
1058
+ overflow-wrap: break-word;
1059
+ transition-property: color, background-color, border-color;
1060
+ transition-duration: var(--duration-fast);
1061
+ }
1062
+
1063
+ .ui-legal-document-toc-link:hover {
1064
+ color: hsl(var(--legal-document-toc-active-foreground, var(--foreground)));
1065
+ background: hsl(var(--legal-document-toc-active-background, var(--accent)) / 0.6);
1066
+ }
1067
+
1068
+ .ui-legal-document-toc-link:focus-visible {
1069
+ outline: none;
1070
+ box-shadow: 0 0 0 var(--focus-ring-width) hsl(var(--focus-ring-color, var(--ring)));
1071
+ }
1072
+
1073
+ /* Active entry: colour + a leading marker + a heavier weight, so the state survives a
1074
+ * monochrome / high-contrast rendering (never colour-only, WCAG 1.4.1). The anchor also
1075
+ * carries `aria-current="location"`. */
1076
+ .ui-legal-document-toc-link[data-active] {
1077
+ color: hsl(var(--legal-document-toc-active-foreground, var(--foreground)));
1078
+ background: hsl(var(--legal-document-toc-active-background, var(--accent)));
1079
+ border-inline-start-color: hsl(var(--legal-document-toc-marker-color, var(--primary)));
1080
+ font-weight: var(--font-weight-medium);
1081
+ }
1082
+
1083
+ .ui-legal-document-content {
1084
+ display: flex;
1085
+ flex-direction: column;
1086
+ gap: var(--legal-document-section-gap);
1087
+ min-width: 0;
1088
+ max-width: var(--legal-document-measure-max-width);
1089
+ }
1090
+
1091
+ /* `scroll-margin-block-start` (logical, so it survives vertical writing modes) is what
1092
+ * makes a hash jump AND a programmatic `scrollIntoView` land BELOW sticky chrome — no
1093
+ * JS offset arithmetic, no consumer CSS. */
1094
+ .ui-legal-document-section {
1095
+ display: flex;
1096
+ flex-direction: column;
1097
+ gap: var(--legal-document-section-title-gap);
1098
+ min-width: 0;
1099
+ scroll-margin-block-start: var(--legal-document-scroll-offset);
1100
+ }
1101
+
1102
+ /* The section is `tabIndex={-1}` and receives focus when its contents anchor is
1103
+ * activated, so the focus ring must be VISIBLE (WCAG 2.4.7 / 2.4.11). */
1104
+ .ui-legal-document-section:focus-visible {
1105
+ outline: var(--focus-ring-width) solid hsl(var(--focus-ring-color, var(--ring)));
1106
+ outline-offset: var(--space-1);
1107
+ }
1108
+
1109
+ .ui-legal-document-footer {
1110
+ display: flex;
1111
+ flex-wrap: wrap;
1112
+ align-items: center;
1113
+ gap: var(--legal-document-footer-gap);
1114
+ min-width: 0;
1115
+ max-width: var(--legal-document-measure-max-width);
1116
+ border-block-start: var(--legal-document-footer-border);
1117
+ }
1118
+
1119
+ /* Two-column mode (shell ≥56rem — a 1024px and a 1440px viewport both qualify): the
1120
+ * rail is PINNED beside a measure-capped document column and spans the header/body/
1121
+ * footer rows, so it stays in view for the whole scroll. Logical grid placement means
1122
+ * column 1 is the START side, i.e. it mirrors correctly under `dir="rtl"`.
1123
+ *
1124
+ * Below 56rem (the 390×844 case) NOTHING here applies: the shell stays a single column
1125
+ * and the rail is a STATIC compact contents block between the document header and the
1126
+ * first section — deliberately not sticky and not height-capped, so a phone never loses
1127
+ * vertical space to pinned chrome. */
1128
+ @container legal-document (min-width: 56rem) {
1129
+ .ui-legal-document {
1130
+ grid-template-columns:
1131
+ var(--legal-document-toc-width)
1132
+ minmax(0, var(--legal-document-measure-max-width));
1133
+ column-gap: var(--legal-document-column-gap);
1134
+ }
1135
+
1136
+ .ui-legal-document-header {
1137
+ grid-column: 2;
1138
+ grid-row: 1;
1139
+ }
1140
+
1141
+ .ui-legal-document-rail {
1142
+ grid-column: 1;
1143
+ grid-row: 1 / span 3;
1144
+ position: sticky;
1145
+ inset-block-start: var(--legal-document-toc-inset-block-start);
1146
+ align-self: start;
1147
+ max-height: var(--legal-document-toc-max-height);
1148
+ overflow-y: auto;
1149
+ }
1150
+
1151
+ .ui-legal-document-content {
1152
+ grid-column: 2;
1153
+ grid-row: 2;
1154
+ }
1155
+
1156
+ .ui-legal-document-footer {
1157
+ grid-column: 2;
1158
+ grid-row: 3;
1159
+ }
1160
+ }
1161
+ }
1162
+
1163
+ @layer components {
1164
+ /* ── Flex responsive region visibility (gh#252) ──────────────────────────────────────────────
1165
+ * `hideBelow` / `hideFrom` are the ONE public way to drop a layout region at a breakpoint step.
1166
+ * Without them a public header cannot hide its anchor navigation below the tablet step, or show
1167
+ * a compact-only affordance, without a page-local media query — which is exactly the API-first
1168
+ * violation gh#252 reports. The steps are the package's canonical breakpoint scale, the same
1169
+ * values `--master-detail-collapse-below` uses (sm 40rem · md 48rem · lg 64rem · xl 80rem); a
1170
+ * media query cannot read a `var()`, so the step values are the tokenized scale written out.
1171
+ *
1172
+ * Both props default to `undefined`, so NO `data-hide-*` attribute is emitted and none of these
1173
+ * selectors can match — every existing Flex is untouched (the gh#231 inert-default contract).
1174
+ * `display: none` removes the region from the a11y tree too, which is the point: the destination
1175
+ * must stay reachable from another region (the footer nav) at that width. */
1176
+ @media (width < 40rem) {
1177
+ .ui-flex[data-hide-below="sm"] {
1178
+ display: none;
1179
+ }
1180
+ }
1181
+
1182
+ @media (width < 48rem) {
1183
+ .ui-flex[data-hide-below="md"] {
1184
+ display: none;
1185
+ }
1186
+ }
1187
+
1188
+ @media (width < 64rem) {
1189
+ .ui-flex[data-hide-below="lg"] {
1190
+ display: none;
1191
+ }
1192
+ }
1193
+
1194
+ @media (width < 80rem) {
1195
+ .ui-flex[data-hide-below="xl"] {
1196
+ display: none;
1197
+ }
1198
+ }
1199
+
1200
+ @media (width >= 40rem) {
1201
+ .ui-flex[data-hide-from="sm"] {
1202
+ display: none;
1203
+ }
1204
+ }
1205
+
1206
+ @media (width >= 48rem) {
1207
+ .ui-flex[data-hide-from="md"] {
1208
+ display: none;
1209
+ }
1210
+ }
1211
+
1212
+ @media (width >= 64rem) {
1213
+ .ui-flex[data-hide-from="lg"] {
1214
+ display: none;
1215
+ }
1216
+ }
1217
+
1218
+ @media (width >= 80rem) {
1219
+ .ui-flex[data-hide-from="xl"] {
1220
+ display: none;
1221
+ }
1222
+ }
758
1223
  }
@@ -38,4 +38,99 @@
38
38
  height: var(--logo-size-lg);
39
39
  font-size: var(--logo-font-size-lg);
40
40
  }
41
+
42
+ /* Identity fills — the IDENTITY role --brand (canonical GoDX emerald), NOT the --success STATUS
43
+ * green: `tone="success"` names the semantic slot, not the status role, and a badge/progress
44
+ * green must never drag the product mark with it (gh#250). Role defaults live HERE (call site),
45
+ * not on :root, so a scoped `.dark` / `[data-tenant]` override of --brand re-resolves
46
+ * (docs/TOKENS.md · role-mirror knobs).
47
+ *
48
+ * The INK is --logo-identity-foreground, NOT --brand-foreground: this branch sets caller-supplied
49
+ * TEXT on the fill, so WCAG 2.2 SC 1.4.3 applies at 4.5:1 (14px bold is not "large text"), while
50
+ * --brand-foreground is the artwork KNOCKOUT colour that tracks --background and only owes 3:1 as
51
+ * non-text (SC 1.4.11). Knockout white on the emerald measured 3.67:1 — an AA failure on every
52
+ * boxed `tone="success"` glyph. See src/tokens/components/logo.css. */
53
+ .ui-logo[data-tone="success"] {
54
+ background: hsl(var(--logo-success-background, var(--brand)));
55
+ color: hsl(var(--logo-success-foreground, var(--logo-identity-foreground)));
56
+ }
57
+
58
+ /* The identity mark honours `size` like every other Logo branch. The godx box has its OWN tier
59
+ * scale (--logo-godx-size-*) because the artwork is a horizontal capsule in a square viewBox and
60
+ * needs ~+0.25rem over the filled glyph box to read at the same weight — but a public `size` prop
61
+ * that renders identically at xs and lg is a silent no-op, and the lockup already scaled its
62
+ * wordmark per tier, so a frozen mark broke the mark↔wordmark proportion at `size="lg"`.
63
+ * These are `[data-mark][data-size]` (0,3,0) so they beat BOTH `.ui-logo[data-size]` and the
64
+ * `[data-mark="godx"]` base regardless of source order — the ordering bug that hid `size` here.
65
+ * --logo-godx-size stays the documented PIN: unset by default, and set once by a service theme it
66
+ * wins at every tier (call-site default, docs/TOKENS.md). */
67
+ .ui-logo[data-mark="godx"] {
68
+ width: var(--logo-godx-size, var(--logo-godx-size-md));
69
+ height: var(--logo-godx-size, var(--logo-godx-size-md));
70
+ border-radius: 0;
71
+ background: transparent;
72
+ color: hsl(var(--logo-godx-color, var(--brand)));
73
+ }
74
+
75
+ .ui-logo[data-mark="godx"][data-size="xs"] {
76
+ width: var(--logo-godx-size, var(--logo-godx-size-xs));
77
+ height: var(--logo-godx-size, var(--logo-godx-size-xs));
78
+ }
79
+
80
+ .ui-logo[data-mark="godx"][data-size="sm"] {
81
+ width: var(--logo-godx-size, var(--logo-godx-size-sm));
82
+ height: var(--logo-godx-size, var(--logo-godx-size-sm));
83
+ }
84
+
85
+ .ui-logo[data-mark="godx"][data-size="lg"] {
86
+ width: var(--logo-godx-size, var(--logo-godx-size-lg));
87
+ height: var(--logo-godx-size, var(--logo-godx-size-lg));
88
+ }
89
+
90
+ .ui-logo[data-mark="godx"] [data-slot="logo-artwork"] {
91
+ display: block;
92
+ width: 100%;
93
+ height: 100%;
94
+ }
95
+
96
+ /*
97
+ * LOCKUP — `<Logo wordmark="…" />`: the mark plus the readable product name as ONE element, so a
98
+ * shell header / auth brand bar never hand-rolls `inline-flex items-center gap-2` around a Logo
99
+ * and a `<Text>` (rule #46). Every knob is a token (rule #45); the wordmark colour defaults to
100
+ * the IDENTITY role and never reads --primary, so re-theming the action colour cannot recolour
101
+ * the brand — no consumer page CSS is required for the logo colour (gh#214).
102
+ */
103
+ .ui-logo-lockup {
104
+ display: inline-flex;
105
+ align-items: center;
106
+ gap: var(--logo-wordmark-gap);
107
+ }
108
+
109
+ .ui-logo-lockup .ui-logo-wordmark {
110
+ font-family: var(--logo-wordmark-font-family, var(--font-family-display));
111
+ font-size: var(--logo-wordmark-font-size-md);
112
+ font-weight: var(--logo-wordmark-font-weight);
113
+ letter-spacing: var(--logo-wordmark-letter-spacing);
114
+ line-height: 1;
115
+ color: hsl(var(--logo-wordmark-color, var(--foreground)));
116
+ white-space: nowrap;
117
+ }
118
+
119
+ .ui-logo-lockup[data-size="xs"] .ui-logo-wordmark {
120
+ font-size: var(--logo-wordmark-font-size-xs);
121
+ }
122
+
123
+ .ui-logo-lockup[data-size="sm"] .ui-logo-wordmark {
124
+ font-size: var(--logo-wordmark-font-size-sm);
125
+ }
126
+
127
+ .ui-logo-lockup[data-size="lg"] .ui-logo-wordmark {
128
+ font-size: var(--logo-wordmark-font-size-lg);
129
+ }
130
+
131
+ /* Canonical GoDX wordmark — the --brand IDENTITY role, independent of --primary AND of --success. */
132
+ .ui-logo-lockup[data-mark="godx"] .ui-logo-wordmark,
133
+ .ui-logo-lockup[data-tone="success"] .ui-logo-wordmark {
134
+ color: hsl(var(--logo-wordmark-color, var(--logo-godx-color, var(--brand))));
135
+ }
41
136
  }