@godxjp/ui 18.9.0 → 18.11.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 (73) hide show
  1. package/LICENSE +202 -0
  2. package/dist/components/data-display/badge.d.ts +7 -0
  3. package/dist/components/data-display/badge.js +1 -1
  4. package/dist/components/data-display/data-table.js +2 -2
  5. package/dist/components/data-display/index.d.ts +2 -0
  6. package/dist/components/data-display/index.js +2 -0
  7. package/dist/components/data-display/permission-matrix.d.ts +26 -0
  8. package/dist/components/data-display/permission-matrix.js +219 -0
  9. package/dist/components/data-display/table.d.ts +12 -0
  10. package/dist/components/data-entry/branch-scope-picker.d.ts +43 -0
  11. package/dist/components/data-entry/branch-scope-picker.js +200 -0
  12. package/dist/components/data-entry/index.d.ts +2 -0
  13. package/dist/components/data-entry/index.js +2 -0
  14. package/dist/components/data-entry/select.d.ts +7 -1
  15. package/dist/components/data-entry/select.js +46 -26
  16. package/dist/components/feedback/alert.d.ts +2 -20
  17. package/dist/components/feedback/alert.js +1 -11
  18. package/dist/components/feedback/banner.d.ts +21 -0
  19. package/dist/components/feedback/banner.js +19 -0
  20. package/dist/components/feedback/index.d.ts +4 -2
  21. package/dist/components/feedback/index.js +2 -2
  22. package/dist/components/general/typography.d.ts +1 -0
  23. package/dist/components/general/typography.js +32 -13
  24. package/dist/components/layout/auth-shell.d.ts +2 -2
  25. package/dist/components/layout/index.d.ts +2 -2
  26. package/dist/components/layout/index.js +2 -2
  27. package/dist/components/layout/page-container.d.ts +1 -1
  28. package/dist/components/layout/page-container.js +61 -15
  29. package/dist/components/layout/resizable.d.ts +1 -1
  30. package/dist/components/layout/resizable.js +26 -8
  31. package/dist/components/layout/service-role-panel.d.ts +25 -0
  32. package/dist/components/layout/service-role-panel.js +206 -0
  33. package/dist/components/navigation/filter-bar.d.ts +2 -2
  34. package/dist/components/navigation/filter-bar.js +113 -34
  35. package/dist/components/navigation/index.d.ts +1 -1
  36. package/dist/components/navigation/pagination.js +20 -1
  37. package/dist/components/navigation/tabs.js +6 -1
  38. package/dist/i18n/messages/en.json +59 -4
  39. package/dist/i18n/messages/ja.json +54 -5
  40. package/dist/i18n/messages/vi.json +53 -4
  41. package/dist/lib/field-a11y.d.ts +7 -0
  42. package/dist/lib/field-a11y.js +6 -0
  43. package/dist/lib/hooks.d.ts +15 -0
  44. package/dist/lib/hooks.js +37 -0
  45. package/dist/props/components/data-display.prop.d.ts +66 -0
  46. package/dist/props/components/data-entry.prop.d.ts +54 -0
  47. package/dist/props/components/feedback.prop.d.ts +3 -3
  48. package/dist/props/components/general.prop.d.ts +8 -1
  49. package/dist/props/components/layout.prop.d.ts +80 -51
  50. package/dist/props/components/navigation.prop.d.ts +77 -50
  51. package/dist/props/registry.d.ts +274 -54
  52. package/dist/props/registry.js +362 -73
  53. package/dist/props/vocabulary/content.prop.d.ts +5 -0
  54. package/dist/props/vocabulary/index.d.ts +1 -1
  55. package/dist/props/vocabulary/interaction.prop.d.ts +4 -7
  56. package/dist/props/vocabulary/layout.prop.d.ts +2 -2
  57. package/dist/styles/alert-layout.css +15 -32
  58. package/dist/styles/badge-layout.css +5 -0
  59. package/dist/styles/control.css +5 -0
  60. package/dist/styles/layout.css +118 -108
  61. package/dist/styles/shell-layout.css +16 -7
  62. package/dist/styles/table-layout.css +92 -2
  63. package/dist/styles/text-layout.css +13 -0
  64. package/dist/tokens/base.css +1 -0
  65. package/dist/tokens/components/badge.css +5 -0
  66. package/dist/tokens/components/banner.css +16 -0
  67. package/dist/tokens/components/navigation.css +10 -25
  68. package/dist/tokens/components/shell.css +20 -9
  69. package/dist/tokens/components/table.css +41 -1
  70. package/dist/tokens/semantic/layout.css +8 -7
  71. package/package.json +15 -2
  72. package/dist/components/layout/page-header.d.ts +0 -22
  73. package/dist/components/layout/page-header.js +0 -89
@@ -521,47 +521,35 @@
521
521
  width: 100%;
522
522
  }
523
523
 
524
- .ui-page-title {
525
- font-size: var(--page-title-font-size);
526
- font-weight: var(--font-weight-medium);
527
- letter-spacing: var(--letter-spacing-tight);
528
- line-height: var(--line-height-tight);
529
- overflow-wrap: anywhere;
530
- }
531
-
532
- /* Title + `meta` share one baseline-aligned band (gh#255). The band only exists when `meta` is
533
- * passed, so a header without it keeps its historical bare-`<h1>` DOM. `wrap` is what keeps a
534
- * long JA/VI title and a status Badge legible at 390px instead of squeezing the title column. */
535
- .ui-page-title-band {
524
+ /* Title line + status/meta band (PageContainer `status`, godxjp-ui#255). The band sits on
525
+ * the title's baseline row at the token-owned gap and WRAPS under the title when the
526
+ * viewport (390px) or a long JA/VI title leaves no room — never clips, never overlaps. */
527
+ .ui-page-header-title-row {
536
528
  display: flex;
537
529
  flex-wrap: wrap;
538
- align-items: baseline;
539
- gap: var(--page-header-meta-gap);
530
+ align-items: center;
531
+ gap: var(--page-header-status-gap);
540
532
  min-inline-size: 0;
541
533
  }
542
534
 
543
- .ui-page-header-meta {
535
+ .ui-page-header-title-row > .ui-page-title {
536
+ min-inline-size: 0;
537
+ }
538
+
539
+ .ui-page-header-status {
544
540
  display: flex;
545
541
  flex-wrap: wrap;
546
542
  align-items: center;
547
- gap: var(--page-header-meta-gap);
543
+ gap: var(--page-header-status-gap);
548
544
  min-inline-size: 0;
549
545
  }
550
546
 
551
- /* `loading` placeholders — the real `<h1>`/`<p>` wearing the shared `.ui-skeleton-block` skin, so
552
- * the pending band occupies the same rhythm the settled band will. `max-inline-size: 100%` keeps
553
- * them inside a 390px column; the block size replaces the line box the (sr-only) text no longer
554
- * fills. */
555
- .ui-page-title-placeholder {
556
- inline-size: var(--page-title-placeholder-measure);
557
- max-inline-size: 100%;
558
- min-block-size: var(--page-title-placeholder-block-size);
559
- }
560
-
561
- .ui-page-subtitle-placeholder {
562
- inline-size: var(--page-subtitle-placeholder-measure);
563
- max-inline-size: 100%;
564
- min-block-size: var(--page-subtitle-placeholder-block-size);
547
+ .ui-page-title {
548
+ font-size: var(--page-title-font-size);
549
+ font-weight: var(--font-weight-medium);
550
+ letter-spacing: var(--letter-spacing-tight);
551
+ line-height: var(--line-height-tight);
552
+ overflow-wrap: anywhere;
565
553
  }
566
554
 
567
555
  /* Compact viewports use a responsive type step, not compact density: body/control text and
@@ -638,6 +626,22 @@
638
626
  margin-top: var(--space-1);
639
627
  }
640
628
 
629
+ /* `headerLoading` placeholders — the real `<h1>`/`<p>` wearing the shared `.ui-skeleton-block`
630
+ * skin, so the pending band occupies the same rhythm the settled band will. `max-inline-size:
631
+ * 100%` keeps them inside a 390px column; the block size replaces the line box the (sr-only)
632
+ * text no longer fills. */
633
+ .ui-page-title-placeholder {
634
+ inline-size: var(--page-title-placeholder-measure);
635
+ max-inline-size: 100%;
636
+ min-block-size: var(--page-title-placeholder-block-size);
637
+ }
638
+
639
+ .ui-page-subtitle-placeholder {
640
+ inline-size: var(--page-subtitle-placeholder-measure);
641
+ max-inline-size: 100%;
642
+ min-block-size: var(--page-subtitle-placeholder-block-size);
643
+ }
644
+
641
645
  /* Default: content-height + top-packed. The container's min-height:100% keeps
642
646
  * the page background spanning the viewport-tall shell, but the body itself is
643
647
  * NOT stretched — so short pages no longer leave a jarring void below their
@@ -953,84 +957,6 @@
953
957
  font-size: var(--filter-label-font-size);
954
958
  }
955
959
 
956
- /* ── FilterBar typed model (#258) ──────────────────────────────────────────────────────────
957
- * Every rule below is scoped to a region the bar only emits when its prop is passed, so a bar
958
- * built the old way (children + onClear) matches none of them and is geometrically untouched.
959
- *
960
- * SEARCH — full-width while the strip is stacked (a bounded search box on a 390px screen wastes
961
- * the row), then one shared token-owned measure from the 640px step up. This single measure is
962
- * what the issue was filed against: as a plain child, every page picked its own width. */
963
- .ui-filter-bar-search {
964
- width: 100%;
965
- min-width: 0;
966
- }
967
-
968
- @media (min-width: 640px) {
969
- .ui-filter-bar-search {
970
- width: var(--filter-bar-search-width);
971
- max-width: 100%;
972
- flex: 0 0 auto;
973
- }
974
- }
975
-
976
- /* CHIPS — always their own full-width line. Applied filters are a record of state, not another
977
- * control in the row; wrapping them in beside the pickers makes both harder to scan. */
978
- .ui-filter-bar-chips {
979
- display: flex;
980
- width: 100%;
981
- flex-basis: 100%;
982
- flex-wrap: wrap;
983
- align-items: center;
984
- gap: var(--filter-bar-chip-row-gap);
985
- min-width: 0;
986
- }
987
-
988
- .ui-filter-bar-chip {
989
- display: inline-flex;
990
- max-width: 100%;
991
- align-items: center;
992
- gap: var(--filter-bar-chip-gap);
993
- padding: var(--filter-bar-chip-padding-block) var(--filter-bar-chip-padding-inline);
994
- border-radius: var(--filter-bar-chip-radius);
995
- background: hsl(var(--filter-bar-chip-background, var(--muted)));
996
- color: hsl(var(--filter-bar-chip-foreground, var(--muted-foreground)));
997
- font-size: var(--filter-bar-chip-font-size);
998
- }
999
-
1000
- /* A long JA/VI chip value wraps inside the chip rather than pushing the row into a horizontal
1001
- * scroll — the remove control must stay reachable at 390px. */
1002
- .ui-filter-bar-chip-label {
1003
- min-width: 0;
1004
- overflow-wrap: anywhere;
1005
- }
1006
-
1007
- .ui-filter-bar-chip-remove {
1008
- flex: none;
1009
- }
1010
-
1011
- .ui-filter-bar-count {
1012
- color: hsl(var(--filter-bar-count-foreground, var(--muted-foreground)));
1013
- font-size: var(--filter-bar-count-font-size);
1014
- /* The count is a number that changes in place — tabular figures stop the row from twitching
1015
- * as it goes 9 → 10 → 100. */
1016
- font-variant-numeric: tabular-nums;
1017
- }
1018
-
1019
- .ui-filter-bar-actions {
1020
- display: flex;
1021
- width: 100%;
1022
- flex-wrap: wrap;
1023
- align-items: center;
1024
- gap: var(--space-inline-sm);
1025
- }
1026
-
1027
- @media (min-width: 640px) {
1028
- .ui-filter-bar-actions {
1029
- width: auto;
1030
- flex: 0 0 auto;
1031
- }
1032
- }
1033
-
1034
960
  /* Toolbar/FilterBar `overflow="scroll"` (#216) — one bounded row that scrolls INLINE instead of
1035
961
  * wrapping onto extra rows. Below 640px the strip still stacks (the `wrap` base rules above own
1036
962
  * that): a 390px viewport must never hide a filter behind a horizontal scroll it cannot see.
@@ -1063,6 +989,90 @@
1063
989
  z-index: 1;
1064
990
  background: hsl(var(--filter-bar-sticky-background, var(--background)));
1065
991
  }
992
+
993
+ /* Typed-model reset+actions cluster gets the same viewport-pinned treatment (gh#258). */
994
+ .ui-toolbar[data-overflow="scroll"] > .ui-filter-bar-end {
995
+ position: sticky;
996
+ inset-inline-end: 0;
997
+ margin-inline-start: auto;
998
+ flex: 0 0 auto;
999
+ z-index: 1;
1000
+ background: hsl(var(--filter-bar-sticky-background, var(--background)));
1001
+ }
1002
+ }
1003
+
1004
+ /* ── FilterBar typed model (gh#258) ─────────────────────────────────────────────
1005
+ * Canonical model layout: strip (search → filters → children → reset → actions) →
1006
+ * applied-chips row → result-count/error line. Geometry is entirely token-owned
1007
+ * (`--filter-bar-*`); stacking below 640px reuses the `.ui-toolbar` base rules, so
1008
+ * 1440/1024/390 behave identically to the composition form. */
1009
+ .ui-filter-bar {
1010
+ display: flex;
1011
+ flex-direction: column;
1012
+ gap: var(--filter-bar-section-gap);
1013
+ }
1014
+
1015
+ .ui-filter-bar .ui-filter-bar-search {
1016
+ inline-size: 100%;
1017
+ }
1018
+
1019
+ .ui-filter-bar-end {
1020
+ display: flex;
1021
+ flex-wrap: wrap;
1022
+ align-items: center;
1023
+ gap: var(--space-2);
1024
+ width: 100%;
1025
+ }
1026
+
1027
+ /* Inside the end cluster the clear button gives up its own auto margin — the CLUSTER is what
1028
+ * parks at the inline end, so reset and actions stay adjacent in the keyboard/DOM order. */
1029
+ .ui-filter-bar-end .ui-toolbar-clear {
1030
+ margin-inline-start: 0;
1031
+ }
1032
+
1033
+ .ui-filter-bar-actions {
1034
+ display: flex;
1035
+ flex-wrap: wrap;
1036
+ align-items: center;
1037
+ gap: var(--space-2);
1038
+ }
1039
+
1040
+ @media (min-width: 640px) {
1041
+ .ui-filter-bar .ui-filter-bar-search {
1042
+ inline-size: min(100%, var(--filter-bar-search-width));
1043
+ }
1044
+
1045
+ .ui-filter-bar .ui-filter-bar-filter {
1046
+ min-inline-size: min(100%, var(--filter-bar-filter-width));
1047
+ }
1048
+
1049
+ .ui-filter-bar-end {
1050
+ width: auto;
1051
+ margin-inline-start: auto;
1052
+ }
1053
+ }
1054
+
1055
+ .ui-filter-bar-chips {
1056
+ display: flex;
1057
+ flex-wrap: wrap;
1058
+ align-items: center;
1059
+ gap: var(--filter-bar-chip-gap);
1060
+ }
1061
+
1062
+ .ui-filter-bar-chip {
1063
+ display: inline-flex;
1064
+ align-items: center;
1065
+ gap: var(--space-1);
1066
+ min-width: 0;
1067
+ }
1068
+
1069
+ .ui-filter-bar-meta {
1070
+ font-size: var(--filter-label-font-size);
1071
+ color: hsl(var(--muted-foreground));
1072
+ }
1073
+
1074
+ .ui-filter-bar-error {
1075
+ color: hsl(var(--destructive));
1066
1076
  }
1067
1077
 
1068
1078
  /* ── LegalDocumentShell (#222) ─────────────────────────────────────────────────
@@ -343,12 +343,21 @@
343
343
  .ui-auth-shell[data-variant="canonical"] .ui-form-field {
344
344
  --field-label-gap: var(--auth-shell-field-label-gap);
345
345
  }
346
+ }
346
347
 
347
- .ui-auth-shell[data-variant="canonical"] [data-slot="form-field-label"] [data-slot="label"] {
348
- font-size: var(--auth-shell-field-label-font-size);
349
- line-height: var(--auth-shell-field-label-line-height);
350
- }
348
+ /* INTENTIONALLY UNLAYERED (like density.css) — the Label primitive carries `text-sm leading-none`
349
+ * Tailwind utility classes, and the utilities layer is declared after `components`, so a
350
+ * components-layer declaration of the same properties can NEVER win regardless of specificity.
351
+ * That left `--auth-shell-field-label-font-size` / `--auth-shell-field-label-line-height` dead and
352
+ * the canonical field label rendering as a 14px/1.0 line box instead of the canonical 12px/1.5 =
353
+ * 18px, sitting the email input 4px high (gh#263). Unlayered author CSS out-cascades every layer;
354
+ * the two tokens remain the public tuning knobs. */
355
+ .ui-auth-shell[data-variant="canonical"] [data-slot="form-field-label"] [data-slot="label"] {
356
+ font-size: var(--auth-shell-field-label-font-size);
357
+ line-height: var(--auth-shell-field-label-line-height);
358
+ }
351
359
 
360
+ @layer components {
352
361
  /*
353
362
  * Compact auth card density — the two card axes are wired to SEPARATE knobs (gh#232), so a
354
363
  * service can shorten the canonical Login card without narrowing its column (or vice versa):
@@ -384,7 +393,7 @@
384
393
  gap: var(--auth-shell-divider-gap);
385
394
  color: hsl(var(--auth-shell-divider-label-color));
386
395
  font-size: var(--auth-shell-divider-label-font-size);
387
- line-height: 1;
396
+ line-height: var(--auth-shell-divider-label-line-height);
388
397
  }
389
398
 
390
399
  .ui-auth-divider-rule {
@@ -423,8 +432,8 @@
423
432
 
424
433
  /* registration = 15px inline at 390 (card x=15, width=360 — the same page rhythm as Login, so
425
434
  * sign-in → sign-up never jumps on a phone). The block insets tighten but the footer clearance
426
- * stays deliberately larger than the block-start inset: at 390 the form is at its tallest and
427
- * the consent/legal footer must not sit flush against the submit button. */
435
+ * stays deliberately larger than a bare gutter: at 390 the form is at its tallest and the
436
+ * consent/legal footer must not sit flush against the submit button. */
428
437
  .ui-auth-shell[data-preset="registration"] {
429
438
  --auth-shell-main-padding: var(--auth-shell-registration-main-padding-block-start-mobile)
430
439
  var(--auth-shell-registration-main-padding-inline-mobile)
@@ -186,6 +186,21 @@
186
186
  padding-inline: 0;
187
187
  }
188
188
 
189
+ /* Same reset, same reason, for a table sitting straight on a FLUSH page. The bleed above exists
190
+ * to cancel the page gutter so the scroll region reaches the page edges — but
191
+ * `.ui-page-container--flush .ui-page-body` zeroes that gutter, leaving nothing to bleed into.
192
+ * The -16px then escapes the page: nothing clips it, so the whole page gained 16px of
193
+ * horizontal scroll at 320/375/390 (WCAG 2.2 SC 1.4.10 Reflow). Scoped by child combinator so a
194
+ * table inside a Card keeps its own bleed — the card supplies the padding that one compensates. */
195
+ .ui-page-container--flush > .ui-page-body > .ui-data-table-root > .ui-data-table-scroll {
196
+ margin-inline: 0;
197
+ padding-inline: 0;
198
+ }
199
+
200
+ .ui-page-container--flush > .ui-page-body > .ui-data-table-root > .ui-data-table-scroll::after {
201
+ inset-inline-end: 0;
202
+ }
203
+
189
204
  [data-slot="card-content"][data-flush] .ui-data-table-scroll::after {
190
205
  inset-inline-end: 0;
191
206
  }
@@ -284,8 +299,23 @@
284
299
 
285
300
  .ui-table-collection :is([data-slot="table-head"], [data-slot="table-cell"]) {
286
301
  /* The column measure comes from `priority`; an unmarked column takes what is left (`auto`),
287
- * which is the free-text column. */
288
- inline-size: var(--table-action-collection-column-width, auto);
302
+ * which is the free-text column — EXCEPT past the seven-column budget below the collapse
303
+ * step, where the `:has()` tier re-points the middle fallback at the flex floor (gh#262):
304
+ * the fixed algorithm hands an auto column 0px once the marked columns over-constrain the
305
+ * table, which is the one-character-per-line shredding this preset must never produce.
306
+ * There every column carries a rem floor-measure; once the floors outgrow the container the
307
+ * table's used width grows past its specified 100% (CSS 2.1 §17.5.2.1) and the DIRECT
308
+ * wrapper's keyboard-reachable overflow-x region scrolls — the SC 1.4.10-permitted
309
+ * one-dimensional scroll, instead of unreadable shredding (see tokens/components/table.css). */
310
+ inline-size: var(
311
+ --table-action-collection-column-width,
312
+ var(--table-action-collection-flex-width, auto)
313
+ );
314
+ /* Wrapping guard, considered for gh#262: this stays `overflow-wrap: anywhere`, NOT
315
+ * `word-break: keep-all`. Under `table-layout: fixed` a column cannot grow to fit an
316
+ * unbreakable CJK run, so keep-all would overflow/clip a long JA header instead of widening
317
+ * its column. With the floors guaranteeing a ~5-glyph measure, a wrapped JA header is two
318
+ * readable lines — never a vertical character column — and losing no text beats clipping. */
289
319
  overflow-wrap: anywhere;
290
320
  white-space: normal;
291
321
  vertical-align: top;
@@ -346,6 +376,21 @@
346
376
  [data-collapse-below="sm"] [data-slot="table-cell"] * {
347
377
  white-space: normal;
348
378
  }
379
+
380
+ /* SEVEN columns and up — the wide-collection floor tier (gh#262). Percent shares are
381
+ * guaranteed to sum past 100% here and fixed layout would normalize them into sub-glyph
382
+ * columns, so every column (the unmarked free-text one included) takes its rem floor
383
+ * instead; the over-constrained table grows past 100% and its wrapper scrolls. Up to six
384
+ * columns this rule never matches and the percent tier above keeps the canonical queues
385
+ * scroll-free — the preset's documented column-count ceiling. */
386
+ [data-collapse-below="sm"] [data-slot="table"]:has([data-slot="table-head"]:nth-child(7)) {
387
+ --table-action-collection-primary-width: var(--table-action-collection-primary-width-floor);
388
+ --table-action-collection-secondary-width: var(
389
+ --table-action-collection-secondary-width-floor
390
+ );
391
+ --table-action-collection-meta-width: var(--table-action-collection-meta-width-floor);
392
+ --table-action-collection-flex-width: var(--table-action-collection-flex-width-floor);
393
+ }
349
394
  }
350
395
 
351
396
  @container ui-table-collection (width < 48rem) {
@@ -371,6 +416,21 @@
371
416
  [data-collapse-below="md"] [data-slot="table-cell"] * {
372
417
  white-space: normal;
373
418
  }
419
+
420
+ /* SEVEN columns and up — the wide-collection floor tier (gh#262). Percent shares are
421
+ * guaranteed to sum past 100% here and fixed layout would normalize them into sub-glyph
422
+ * columns, so every column (the unmarked free-text one included) takes its rem floor
423
+ * instead; the over-constrained table grows past 100% and its wrapper scrolls. Up to six
424
+ * columns this rule never matches and the percent tier above keeps the canonical queues
425
+ * scroll-free — the preset's documented column-count ceiling. */
426
+ [data-collapse-below="md"] [data-slot="table"]:has([data-slot="table-head"]:nth-child(7)) {
427
+ --table-action-collection-primary-width: var(--table-action-collection-primary-width-floor);
428
+ --table-action-collection-secondary-width: var(
429
+ --table-action-collection-secondary-width-floor
430
+ );
431
+ --table-action-collection-meta-width: var(--table-action-collection-meta-width-floor);
432
+ --table-action-collection-flex-width: var(--table-action-collection-flex-width-floor);
433
+ }
374
434
  }
375
435
 
376
436
  @container ui-table-collection (width < 64rem) {
@@ -396,6 +456,21 @@
396
456
  [data-collapse-below="lg"] [data-slot="table-cell"] * {
397
457
  white-space: normal;
398
458
  }
459
+
460
+ /* SEVEN columns and up — the wide-collection floor tier (gh#262). Percent shares are
461
+ * guaranteed to sum past 100% here and fixed layout would normalize them into sub-glyph
462
+ * columns, so every column (the unmarked free-text one included) takes its rem floor
463
+ * instead; the over-constrained table grows past 100% and its wrapper scrolls. Up to six
464
+ * columns this rule never matches and the percent tier above keeps the canonical queues
465
+ * scroll-free — the preset's documented column-count ceiling. */
466
+ [data-collapse-below="lg"] [data-slot="table"]:has([data-slot="table-head"]:nth-child(7)) {
467
+ --table-action-collection-primary-width: var(--table-action-collection-primary-width-floor);
468
+ --table-action-collection-secondary-width: var(
469
+ --table-action-collection-secondary-width-floor
470
+ );
471
+ --table-action-collection-meta-width: var(--table-action-collection-meta-width-floor);
472
+ --table-action-collection-flex-width: var(--table-action-collection-flex-width-floor);
473
+ }
399
474
  }
400
475
 
401
476
  @container ui-table-collection (width < 80rem) {
@@ -421,5 +496,20 @@
421
496
  [data-collapse-below="xl"] [data-slot="table-cell"] * {
422
497
  white-space: normal;
423
498
  }
499
+
500
+ /* SEVEN columns and up — the wide-collection floor tier (gh#262). Percent shares are
501
+ * guaranteed to sum past 100% here and fixed layout would normalize them into sub-glyph
502
+ * columns, so every column (the unmarked free-text one included) takes its rem floor
503
+ * instead; the over-constrained table grows past 100% and its wrapper scrolls. Up to six
504
+ * columns this rule never matches and the percent tier above keeps the canonical queues
505
+ * scroll-free — the preset's documented column-count ceiling. */
506
+ [data-collapse-below="xl"] [data-slot="table"]:has([data-slot="table-head"]:nth-child(7)) {
507
+ --table-action-collection-primary-width: var(--table-action-collection-primary-width-floor);
508
+ --table-action-collection-secondary-width: var(
509
+ --table-action-collection-secondary-width-floor
510
+ );
511
+ --table-action-collection-meta-width: var(--table-action-collection-meta-width-floor);
512
+ --table-action-collection-flex-width: var(--table-action-collection-flex-width-floor);
513
+ }
424
514
  }
425
515
  }
@@ -107,6 +107,19 @@
107
107
  * having to add `min-w-0` — the documented flex-truncate idiom (issue #114). */
108
108
  min-width: 0;
109
109
  }
110
+ /* Multi-line clamp (`clamp` prop, issue #261) — the token-owned line-clamp so consumers never
111
+ * write a page-local `line-clamp-N` utility. Visual-only: the full text stays in the DOM and
112
+ * the accessible name. `--text-clamp` is set inline by the component from the prop value. */
113
+ [data-slot="text"][data-clamp] {
114
+ display: -webkit-box;
115
+ -webkit-box-orient: vertical;
116
+ -webkit-line-clamp: var(--text-clamp, 2);
117
+ line-clamp: var(--text-clamp, 2);
118
+ overflow: hidden;
119
+ /* Same flex-shrink contract as truncate (issue #114): clamps inside a flex row without
120
+ * the consumer adding `min-w-0`. */
121
+ min-width: 0;
122
+ }
110
123
  [data-slot="text"][data-tabular] {
111
124
  font-variant-numeric: tabular-nums;
112
125
  }
@@ -13,6 +13,7 @@
13
13
  @import "./components/descriptions.css";
14
14
  @import "./components/table.css";
15
15
  @import "./components/feedback.css";
16
+ @import "./components/banner.css";
16
17
  @import "./components/sheet.css";
17
18
  @import "./components/badge.css";
18
19
  @import "./components/navigation.css";
@@ -7,4 +7,9 @@
7
7
  /* Small-by-design (badge/pill/counter). A knob (rule #45) so a service can
8
8
  * re-tune badge text without touching the global --font-size-xs step. */
9
9
  --badge-font-size: var(--font-size-xs);
10
+ /* Companion to --badge-font-size (gh#260). The cva's old `text-xs` utility also set
11
+ * line-height via Tailwind's default `--text-xs--line-height: calc(1 / 0.75)` (the theme
12
+ * remaps --text-xs but never that companion). Same unitless ratio here keeps the default
13
+ * badge pixel-identical now that badge-layout.css owns the type metrics. */
14
+ --badge-line-height: calc(1 / 0.75);
10
15
  }
@@ -0,0 +1,16 @@
1
+ /* Banner — the full-bleed attention strip (<Banner> = Alert variant="banner").
2
+ * Colour stays with the alert tone rules; these tokens own ONLY the strip geometry,
3
+ * so a service retunes the banner once here instead of forking alert-layout.css
4
+ * (rule #45 — every service-tunable constant gets a knob). */
5
+
6
+ :root {
7
+ /* Square corners — a strip spans its container edge-to-edge, so it carries no radius. */
8
+ --banner-radius: 0;
9
+ /* The strip keeps a single hairline block-end rule (tone-coloured via the alert
10
+ * border rules); inline/block-start edges are open so it reads as part of the frame. */
11
+ --banner-border-width: 1px;
12
+ /* Strip inset. Block is denser than the inline Alert card; inline defaults to the
13
+ * page gutter so banner text aligns with page content under AppShell/PageContainer. */
14
+ --banner-space-inset-block: var(--space-3);
15
+ --banner-space-inset-inline: var(--space-page-active-x, var(--space-6));
16
+ }
@@ -65,29 +65,14 @@
65
65
  * (0 on an overlay-scrollbar platform). */
66
66
  --filter-bar-scroll-padding-y: var(--space-1);
67
67
 
68
- /* FilterBar typed model (#258) — the search / chip / result-count regions the bar now OWNS, so a
69
- * list page never re-decides them. Every constant is a knob (rule #45): a service matches its own
70
- * grid by retuning these once instead of per page.
71
- *
72
- * Search MEASURE — the whole point of the `search` slot. Below the 640px step the control is
73
- * full-width (a bounded search box on a 390px screen wastes the row), so this binds only at and
74
- * above it. 18rem = 288px: comfortable for a JA/VI query without crowding the filter groups. */
75
- --filter-bar-search-width: 18rem;
76
-
77
- /* Applied-filter chips. */
78
- --filter-bar-chip-gap: var(--space-inline-xs);
79
- --filter-bar-chip-row-gap: var(--space-inline-xs);
80
- --filter-bar-chip-padding-inline: var(--space-2);
81
- --filter-bar-chip-padding-block: var(--space-1);
82
- --filter-bar-chip-radius: var(--radius-sm);
83
- --filter-bar-chip-font-size: var(--font-size-xs);
84
- /* Role-mirror knobs (docs/TOKENS.md) — `initial` at :root so --muted / --muted-foreground
85
- * re-resolve at the CALL SITE under a scoped [data-tenant]/.dark theme. Binding a role var here
86
- * would freeze the chip at the :root palette and a tenant override would never reach it. */
87
- --filter-bar-chip-background: initial;
88
- --filter-bar-chip-foreground: initial;
89
-
90
- /* Result-count live region. */
91
- --filter-bar-count-font-size: var(--font-size-xs);
92
- --filter-bar-count-foreground: initial;
68
+ /* FilterBar typed model (gh#258) — token-owned geometry so every list page gets the SAME
69
+ * search width, filter width, chip rhythm and section stacking without page-local CSS.
70
+ * All are rule-#45 knobs: a service theme retunes them once, props/className override per
71
+ * instance. Below 640px the strip stacks (search/filters full-width); the widths apply from
72
+ * the 640px breakpoint up, always clamped by min(100%, …) so a narrow container never
73
+ * overflows at 390px with long JA/EN/VI labels. */
74
+ --filter-bar-search-width: 20rem;
75
+ --filter-bar-filter-width: var(--filter-picker-width-md);
76
+ --filter-bar-chip-gap: var(--space-2);
77
+ --filter-bar-section-gap: var(--space-2);
93
78
  }
@@ -183,9 +183,11 @@
183
183
  --auth-shell-login-main-padding-block-end: 1rem;
184
184
  --auth-shell-login-identity-slot-block-size: 7rem;
185
185
  --auth-shell-login-card-stack-gap: 1.25rem;
186
- --auth-shell-login-card-padding-block-compact: calc(
187
- var(--auth-shell-compact-card-inset) + var(--control-border-width) + var(--control-border-width)
188
- );
186
+ /* Canonical SCR-001 block inset is a FLAT 24px — the same measure as the inline inset. The card
187
+ * draws its own 1px border and a border-box rect already contains it, so the former
188
+ * `+ 2 x --control-border-width` double-counted the border and pushed the passkey CTA to y=390
189
+ * instead of the canonical y=388 (gh#263). */
190
+ --auth-shell-login-card-padding-block-compact: var(--auth-shell-compact-card-inset);
189
191
 
190
192
  /* Device-authorization preset — 380px card measure, 15px block · 5px inline mobile gutter
191
193
  * (at a 390px viewport the card is x=5px, width=380px). */
@@ -256,14 +258,19 @@
256
258
  --auth-shell-registration-main-padding-block-end-mobile: 2rem;
257
259
  --auth-shell-registration-card-stack-gap: 1.25rem;
258
260
  /* Fixed identity track, the same 112px `login` proved (gh#237). Without it the card anchor moves
259
- * with the title/requester line count measured here at 82.69px for one wrapped requester so
260
- * the canonical y above would hold for exactly one copy length and drift for every other. The
261
- * slot absorbs absent / one-line / two-line identity content instead. */
261
+ * with the title/requester line count, so the canonical y above would hold for exactly one copy
262
+ * length and drift for every other. The slot absorbs absent / one-line / two-line identity
263
+ * content instead. */
262
264
  --auth-shell-registration-identity-slot-block-size: 7rem;
263
265
 
264
- /* Labelled auth divider geometry. */
266
+ /* Labelled auth divider geometry. The canonical SCR-001 divider row is 19px tall at the 11px
267
+ * label (the artboard label inherits the page's line-height instead of collapsing to a 1.0 line
268
+ * box). 19/11 pins the row at exactly 19px and keeps it proportional if a service retunes the
269
+ * label font-size. The previous hardcoded `line-height: 1` rendered an 11px row, sitting the
270
+ * whole lower half of the Login card 8px high (gh#263). */
265
271
  --auth-shell-divider-gap: 0.625rem;
266
272
  --auth-shell-divider-label-font-size: 0.6875rem;
273
+ --auth-shell-divider-label-line-height: calc(19 / 11);
267
274
  --auth-shell-divider-rule-color: var(--border);
268
275
  --auth-shell-divider-label-color: var(--muted-foreground);
269
276
  --auth-identity-gap: 0.375rem;
@@ -281,8 +288,12 @@
281
288
  --auth-footer-text-font-size: 0.6875rem;
282
289
  --auth-stack-gap: var(--space-3);
283
290
  --auth-shell-field-label-gap: var(--space-1);
284
- --auth-shell-field-label-font-size: var(--font-size-xs);
285
- --auth-shell-field-label-line-height: 1.25;
291
+ /* Canonical SCR-001 field label: a LITERAL 12px (the golden-ratio --font-size-xs ≈ 12.47px would
292
+ * drift the 18px line box to 18.7px and push the email input off its y=489 anchor) with a 1.5
293
+ * line box = 18px. The former var(--font-size-xs)/1.25 pair rendered a 14px box, sitting the
294
+ * email input 4px high (gh#263). */
295
+ --auth-shell-field-label-font-size: 0.75rem;
296
+ --auth-shell-field-label-line-height: 1.5;
286
297
 
287
298
  /* CenteredShell — authenticated, no-sidebar, centred-column page shell (hosted-ID "My Page",
288
299
  * account, standalone settings). The bar mirrors AppShell's `.app-topbar` chrome (fixed height +
@@ -59,11 +59,51 @@
59
59
  --table-action-collection-secondary-width: 22%; /* target / resource */
60
60
  --table-action-collection-meta-width: 12%; /* timestamps, ids — lowest priority */
61
61
  --table-action-collection-actions-width: 3.5rem; /* row-action affordance, never clipped */
62
- /* Compact tier, applied below the collapse step (the 390px acceptance frame). */
62
+ /* Compact tier, applied below the collapse step (the 390px acceptance frame). Percentages,
63
+ * like the desktop tier: while the priority ratios FIT (the canonical queues up to the
64
+ * six-column budget below), they keep the table filling its container edge-to-edge with no
65
+ * horizontal scroll — the gh#253 acceptance. */
63
66
  --table-action-collection-primary-width-compact: 24%;
64
67
  --table-action-collection-secondary-width-compact: 22%;
65
68
  --table-action-collection-meta-width-compact: 20%;
66
69
  --table-action-collection-actions-width-compact: 2.75rem;
70
+ /* Wide-collection FLOORS — the compact measures from SEVEN columns up (gh#262).
71
+ *
72
+ * Percentage measures stop working once a queue carries enough columns for the shares to sum
73
+ * past 100%: `table-layout: fixed` NORMALIZES them back into the table's specified width, so at
74
+ * ~10 columns on a 390px frame a "24%" column really renders at a tenth of the frame, every
75
+ * column drops below one CJK glyph, and JA headers shred into a vertical one-character-per-line
76
+ * column (the failure mode WCAG 2.2 SC 1.4.10 Reflow exists to prevent). Cell `min-inline-size`
77
+ * and `max(%, length)` widths are equally inert in the fixed algorithm, and the intrinsic
78
+ * (max-content) size of a fixed-layout table is degenerate — all verified in Chromium — so a
79
+ * LENGTH is the only floor an engine respects: length columns are never squeezed, and when
80
+ * their sum outgrows the container the table's used width grows past its specified 100%
81
+ * (CSS 2.1 §17.5.2.1) into the keyboard-reachable overflow-x wrapper, which scrolls.
82
+ * ONE-dimensional scrolling of a data table is explicitly permitted by SC 1.4.10;
83
+ * one-character-per-line is not readable at all.
84
+ *
85
+ * The stylesheet switches tiers on a COLUMN BUDGET, not on width: up to six columns the
86
+ * percentage ratios above still resolve to readable measures at every acceptance artboard
87
+ * (320 · 375 · 390), so the canonical five-column approval queue keeps its documented
88
+ * scroll-free 390 frame byte-for-byte; from seven columns the shares are guaranteed to
89
+ * over-sum and the floors + intentional scroll take over. That budget is the preset's
90
+ * documented column-count ceiling (gh#262 expectation 3): past six columns the preset stops
91
+ * compressing and starts scrolling.
92
+ *
93
+ * Units are rem, not ch: `ch` tracks the "0" glyph (~half a CJK em), so a ch-denominated floor
94
+ * would silently halve for the JA copy these floors exist to protect. At the compact type tier
95
+ * (font-size-xs = 0.75rem, space-2 inline padding each side) the 5rem meta floor keeps a ~4rem
96
+ * text measure = 5 full-width glyphs per line — a JA header wraps to two readable lines, never
97
+ * a character column. */
98
+ --table-action-collection-primary-width-floor: 6rem;
99
+ --table-action-collection-secondary-width-floor: 5.5rem;
100
+ --table-action-collection-meta-width-floor: 5rem;
101
+ /* Floor for a column with NO `priority` (the free-text column, `auto` up to the budget). It
102
+ * cannot stay `auto` past the budget: once the marked columns over-constrain the table, the
103
+ * fixed algorithm hands an auto column exactly 0px (verified in Chromium) — the same
104
+ * shredding, moved to the free-text column. The actions floor is simply
105
+ * `--table-action-collection-actions-width-compact`, already an absolute measure. */
106
+ --table-action-collection-flex-width-floor: 5rem;
67
107
  --table-action-collection-font-size-compact: var(--font-size-xs);
68
108
  --table-action-collection-cell-space-x-compact: var(--space-2);
69
109
  --table-action-collection-cell-padding-y-compact: var(--space-2);