@godxjp/ui 18.9.0 → 18.10.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 (85) 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/card.d.ts +3 -27
  5. package/dist/components/data-display/card.js +1 -4
  6. package/dist/components/data-display/data-table.js +5 -4
  7. package/dist/components/data-display/index.d.ts +2 -0
  8. package/dist/components/data-display/index.js +2 -0
  9. package/dist/components/data-display/permission-matrix.d.ts +26 -0
  10. package/dist/components/data-display/permission-matrix.js +219 -0
  11. package/dist/components/data-display/table.d.ts +12 -0
  12. package/dist/components/data-entry/branch-scope-picker.d.ts +43 -0
  13. package/dist/components/data-entry/branch-scope-picker.js +200 -0
  14. package/dist/components/data-entry/command-palette.d.ts +1 -42
  15. package/dist/components/data-entry/command-palette.js +2 -31
  16. package/dist/components/data-entry/index.d.ts +2 -0
  17. package/dist/components/data-entry/index.js +2 -0
  18. package/dist/components/data-entry/input-otp.d.ts +1 -1
  19. package/dist/components/feedback/alert.d.ts +2 -20
  20. package/dist/components/feedback/alert.js +1 -11
  21. package/dist/components/feedback/banner.d.ts +21 -0
  22. package/dist/components/feedback/banner.js +15 -0
  23. package/dist/components/feedback/index.d.ts +4 -2
  24. package/dist/components/feedback/index.js +2 -2
  25. package/dist/components/general/typography.d.ts +1 -0
  26. package/dist/components/general/typography.js +32 -13
  27. package/dist/components/layout/auth-shell.d.ts +2 -2
  28. package/dist/components/layout/index.d.ts +2 -2
  29. package/dist/components/layout/index.js +2 -2
  30. package/dist/components/layout/page-container.d.ts +1 -1
  31. package/dist/components/layout/page-container.js +45 -19
  32. package/dist/components/layout/service-role-panel.d.ts +25 -0
  33. package/dist/components/layout/service-role-panel.js +206 -0
  34. package/dist/components/navigation/filter-bar.d.ts +2 -2
  35. package/dist/components/navigation/filter-bar.js +113 -34
  36. package/dist/components/navigation/index.d.ts +1 -1
  37. package/dist/components/navigation/steps.d.ts +2 -2
  38. package/dist/components/navigation/steps.js +2 -4
  39. package/dist/components/ui/avatar.d.ts +1 -1
  40. package/dist/components/ui/avatar.js +1 -2
  41. package/dist/components/ui/input-otp.d.ts +3 -15
  42. package/dist/components/ui/input-otp.js +1 -2
  43. package/dist/email/tokens.generated.d.ts +2 -2
  44. package/dist/email/tokens.generated.js +2 -2
  45. package/dist/i18n/messages/en.json +56 -4
  46. package/dist/i18n/messages/ja.json +51 -5
  47. package/dist/i18n/messages/vi.json +50 -4
  48. package/dist/props/components/data-display.prop.d.ts +66 -16
  49. package/dist/props/components/data-entry.prop.d.ts +54 -7
  50. package/dist/props/components/feedback.prop.d.ts +3 -3
  51. package/dist/props/components/general.prop.d.ts +8 -1
  52. package/dist/props/components/layout.prop.d.ts +75 -54
  53. package/dist/props/components/navigation.prop.d.ts +77 -59
  54. package/dist/props/registry.d.ts +274 -73
  55. package/dist/props/registry.js +362 -93
  56. package/dist/props/vocabulary/content.prop.d.ts +5 -0
  57. package/dist/props/vocabulary/index.d.ts +1 -1
  58. package/dist/props/vocabulary/interaction.prop.d.ts +4 -7
  59. package/dist/props/vocabulary/layout.prop.d.ts +2 -2
  60. package/dist/styles/alert-layout.css +15 -32
  61. package/dist/styles/badge-layout.css +5 -0
  62. package/dist/styles/base.css +0 -28
  63. package/dist/styles/card-layout.css +13 -49
  64. package/dist/styles/control.css +2 -12
  65. package/dist/styles/data-display-layout.css +0 -16
  66. package/dist/styles/layout.css +102 -108
  67. package/dist/styles/navigation-layout.css +3 -8
  68. package/dist/styles/shell-layout.css +17 -17
  69. package/dist/styles/table-layout.css +77 -29
  70. package/dist/styles/text-layout.css +13 -0
  71. package/dist/tokens/base.css +1 -0
  72. package/dist/tokens/components/badge.css +5 -0
  73. package/dist/tokens/components/banner.css +16 -0
  74. package/dist/tokens/components/card.css +0 -12
  75. package/dist/tokens/components/control.css +0 -16
  76. package/dist/tokens/components/data-display.css +0 -12
  77. package/dist/tokens/components/email.css +2 -8
  78. package/dist/tokens/components/feedback.css +0 -20
  79. package/dist/tokens/components/navigation.css +10 -34
  80. package/dist/tokens/components/shell.css +24 -36
  81. package/dist/tokens/components/table.css +41 -22
  82. package/dist/tokens/semantic/layout.css +5 -12
  83. package/package.json +15 -2
  84. package/dist/components/layout/page-header.d.ts +0 -22
  85. package/dist/components/layout/page-header.js +0 -89
@@ -28,34 +28,6 @@
28
28
  * ───────────────────────────────────────────────────────────────────────── */
29
29
  @import "tailwindcss";
30
30
 
31
- /* ── THE LAYER CONTRACT (gh#412) ───────────────────────────────────────────
32
- * `@import "tailwindcss"` establishes `theme, base, components, utilities`. Everything this
33
- * package styles lives in `@layer components`, which is EARLIER than `utilities` — so a Tailwind
34
- * utility a component itself emits silently outranks the package rule that is supposed to own the
35
- * same property, no matter how specific that rule is. That is not a specificity bug you can fix
36
- * with another selector; layer order beats specificity outright.
37
- *
38
- * It shipped as a real defect: `<table class="… text-sm">` (utilities) vs the collection preset's
39
- * `font-size: var(--table-action-collection-font-size-compact)` (components) — the compact type
40
- * tier never applied at ANY width, so the documented token was dead and a 5–6 character Japanese
41
- * label could not fit the narrow-frame column measure (WCAG 2.2 SC 1.4.10, and only ever visible
42
- * in Japanese). Two independent consumers reported it.
43
- *
44
- * `godxjp-ui-responsive` is declared HERE, after Tailwind, so it is the LAST layer and outranks
45
- * `utilities`. It is reserved for one thing: RESPONSIVE RE-POINTS — the container/media blocks
46
- * that must win over a component's own static utility for the layout contract to hold. Nothing
47
- * static belongs in it; a rule that does not sit inside a `@container`/`@media` query goes in
48
- * `@layer components` like everything else.
49
- *
50
- * What this layer canNOT do — and the rule consumers must follow: an UNLAYERED consumer rule
51
- * outranks EVERY layer, including this one. So app CSS must theme this package through TOKENS on
52
- * a wrapper, never by writing its own selectors against `[data-slot]` / `[data-priority]`
53
- * elements. A consumer that does write such a rule must put it in a layer
54
- * (`@layer components { … }`), or it will silently kill the responsive re-points the package
55
- * makes — a column measured to 0px and wrapped one character per line is what that looks like.
56
- * See docs/TOKENS.md · "The layer contract". */
57
- @layer godxjp-ui-responsive;
58
-
59
31
  /* Per-language font wiring — token-only, NO hardcoded faces. Each locale reads
60
32
  * an OPTIONAL per-lang slot token (empty by default) and falls back to the
61
33
  * base sans. Consumers switch a locale's face by setting its slot, e.g.
@@ -51,16 +51,9 @@
51
51
  background: transparent;
52
52
  }
53
53
 
54
- /* `featured` = the brand-toned perimeter. Its colour is a role-mirror knob (rule #45): declared
55
- * `initial` in the token layer so the --primary default resolves HERE, at the call site, and a
56
- * scoped [data-tenant]/.dark override of --primary still reaches it. A service that wants the
57
- * featured ring in a non-brand tone overrides --card-featured-border-color; a card that wants a
58
- * SEMANTIC perimeter (attention / destructive / success …) uses
59
- * `accent="…" accentPlacement="perimeter"` instead — same geometry, tone-owned. */
60
54
  [data-slot="card"][data-variant="featured"] {
61
- border-color: var(--card-featured-border-color, hsl(var(--primary)));
62
- box-shadow: 0 0 0 var(--card-featured-ring-width)
63
- var(--card-featured-border-color, hsl(var(--primary)));
55
+ border-color: hsl(var(--primary));
56
+ box-shadow: 0 0 0 1px hsl(var(--primary));
64
57
  }
65
58
 
66
59
  /* ── Density — override the shell inset/body tokens ──────────────────────
@@ -79,39 +72,33 @@
79
72
  --card-space-body-y: var(--space-5);
80
73
  }
81
74
 
82
- /* ── Accent toneONE resolved colour per semantic accent ─────────────
83
- * `--card-accent-color` is RESOLVED from the card's own `data-accent`, so both placements read
84
- * the same value and a new tone is one line. It is deliberately NOT a service knob: it is set on
85
- * the element, so a :root / [data-tenant] override could never reach it. Retint the ROLE
86
- * (--attention, --success, …) — that is what a role is for. */
75
+ /* ── Accent edgesemantic left stripe (token width), content on shell ─ */
76
+ [data-slot="card"][data-accent] {
77
+ border-inline-start-width: var(--card-accent-rail-width);
78
+ }
79
+
87
80
  [data-slot="card"][data-accent="primary"] {
88
- --card-accent-color: hsl(var(--primary));
81
+ border-inline-start-color: hsl(var(--primary));
89
82
  }
90
83
 
91
84
  [data-slot="card"][data-accent="success"] {
92
- --card-accent-color: hsl(var(--success));
85
+ border-inline-start-color: hsl(var(--success));
93
86
  }
94
87
 
95
88
  [data-slot="card"][data-accent="warning"] {
96
- --card-accent-color: hsl(var(--warning));
89
+ border-inline-start-color: hsl(var(--warning));
97
90
  }
98
91
 
99
92
  [data-slot="card"][data-accent="info"] {
100
- --card-accent-color: hsl(var(--info));
93
+ border-inline-start-color: hsl(var(--info));
101
94
  }
102
95
 
103
96
  [data-slot="card"][data-accent="attention"] {
104
- --card-accent-color: hsl(var(--attention));
97
+ border-inline-start-color: hsl(var(--attention));
105
98
  }
106
99
 
107
100
  [data-slot="card"][data-accent="destructive"] {
108
- --card-accent-color: hsl(var(--destructive));
109
- }
110
-
111
- /* ── Accent placement `edge` (DEFAULT) — semantic left stripe (token width), content on shell ─ */
112
- [data-slot="card"][data-accent] {
113
- border-inline-start-width: var(--card-accent-rail-width);
114
- border-inline-start-color: var(--card-accent-color);
101
+ border-inline-start-color: hsl(var(--destructive));
115
102
  }
116
103
 
117
104
  [data-slot="card"][data-accent] > [data-slot="card-header"],
@@ -120,29 +107,6 @@
120
107
  padding-inline-start: calc(var(--card-space-inset) - var(--card-accent-rail-width));
121
108
  }
122
109
 
123
- /* ── Accent placement `perimeter` — the full attention border (gh#12) ───
124
- * The whole edge carries the semantic tone instead of one rail, so a card can read as
125
- * "action required" / "failed" at a glance without borrowing `--primary` the way
126
- * `variant="featured"` does. Same optical weight as `featured` (border + 1px ring), tone-owned.
127
- * Because the rail is gone, the slot padding compensation above is undone — content returns to
128
- * the normal shell column, so switching placement never shifts the text. */
129
- [data-slot="card"][data-accent][data-accent-placement="perimeter"] {
130
- border-width: var(--card-accent-perimeter-width);
131
- border-color: var(--card-accent-color);
132
- box-shadow:
133
- 0 0 0 var(--card-accent-perimeter-ring-width) var(--card-accent-color),
134
- var(--card-shadow),
135
- var(--card-glow);
136
- }
137
-
138
- [data-slot="card"][data-accent][data-accent-placement="perimeter"] > [data-slot="card-header"],
139
- [data-slot="card"][data-accent][data-accent-placement="perimeter"]
140
- > [data-slot="card-content"]:not([data-flush]),
141
- [data-slot="card"][data-accent][data-accent-placement="perimeter"]
142
- > [data-slot="card-footer"]:not([data-flush]) {
143
- padding-inline-start: var(--card-space-inset);
144
- }
145
-
146
110
  /* ── Header ──────────────────────────────────────────────────────────── */
147
111
  [data-slot="card-header"] {
148
112
  display: flex;
@@ -714,18 +714,8 @@
714
714
  .ui-otp-container {
715
715
  display: flex;
716
716
  align-items: center;
717
- justify-content: var(--otp-container-align, flex-start);
718
717
  gap: var(--space-2);
719
718
  }
720
- /* `align` on InputOTP lands on the hidden <input> (input-otp owns the container element), so the
721
- * container reads it through :has() — the same mechanism this file already uses for the invalid
722
- * and disabled states. Specificity puts the per-instance prop above the theme token. */
723
- .ui-otp-container:has(.ui-otp-input[data-align="center"]) {
724
- justify-content: center;
725
- }
726
- .ui-otp-container:has(.ui-otp-input[data-align="end"]) {
727
- justify-content: flex-end;
728
- }
729
719
  .ui-otp-input:disabled {
730
720
  cursor: not-allowed;
731
721
  }
@@ -736,8 +726,8 @@
736
726
  .ui-otp-slot {
737
727
  position: relative;
738
728
  display: flex;
739
- width: var(--otp-slot-inline-size, var(--otp-slot-size, var(--control-height)));
740
- height: var(--otp-slot-block-size, var(--otp-slot-size, var(--control-height)));
729
+ width: var(--otp-slot-size, var(--control-height));
730
+ height: var(--otp-slot-size, var(--control-height));
741
731
  align-items: center;
742
732
  justify-content: center;
743
733
  font-size: var(--font-size-base);
@@ -30,22 +30,6 @@
30
30
  color: var(--avatar-square-foreground, hsl(var(--primary-foreground)));
31
31
  }
32
32
 
33
- /* CAPABILITY MEDALLION — `<Avatar appearance="tinted">` (gh#12). The tinted companion of the
34
- * solid entity mark: same box, same radius, a soft role wash instead of a solid fill and the
35
- * glyph in the role itself. Declared AFTER the square rule (equal specificity) so it retints
36
- * either shape. The glyph rule is scoped to this appearance ON PURPOSE — a global
37
- * `.ui-avatar svg` would outrank the per-call-site icon classes existing avatars already use. */
38
- .ui-avatar[data-appearance="tinted"] {
39
- --avatar-background: var(--avatar-tinted-background, hsl(var(--primary) / 0.1));
40
- color: var(--avatar-tinted-foreground, hsl(var(--primary)));
41
- }
42
-
43
- .ui-avatar[data-appearance="tinted"] svg {
44
- inline-size: var(--avatar-tinted-glyph-size);
45
- block-size: var(--avatar-tinted-glyph-size);
46
- flex: none;
47
- }
48
-
49
33
  .ui-avatar-image {
50
34
  width: 100%;
51
35
  height: 100%;
@@ -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);
532
+ min-inline-size: 0;
533
+ }
534
+
535
+ .ui-page-header-title-row > .ui-page-title {
540
536
  min-inline-size: 0;
541
537
  }
542
538
 
543
- .ui-page-header-meta {
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
@@ -953,84 +941,6 @@
953
941
  font-size: var(--filter-label-font-size);
954
942
  }
955
943
 
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
944
  /* Toolbar/FilterBar `overflow="scroll"` (#216) — one bounded row that scrolls INLINE instead of
1035
945
  * wrapping onto extra rows. Below 640px the strip still stacks (the `wrap` base rules above own
1036
946
  * that): a 390px viewport must never hide a filter behind a horizontal scroll it cannot see.
@@ -1063,6 +973,90 @@
1063
973
  z-index: 1;
1064
974
  background: hsl(var(--filter-bar-sticky-background, var(--background)));
1065
975
  }
976
+
977
+ /* Typed-model reset+actions cluster gets the same viewport-pinned treatment (gh#258). */
978
+ .ui-toolbar[data-overflow="scroll"] > .ui-filter-bar-end {
979
+ position: sticky;
980
+ inset-inline-end: 0;
981
+ margin-inline-start: auto;
982
+ flex: 0 0 auto;
983
+ z-index: 1;
984
+ background: hsl(var(--filter-bar-sticky-background, var(--background)));
985
+ }
986
+ }
987
+
988
+ /* ── FilterBar typed model (gh#258) ─────────────────────────────────────────────
989
+ * Canonical model layout: strip (search → filters → children → reset → actions) →
990
+ * applied-chips row → result-count/error line. Geometry is entirely token-owned
991
+ * (`--filter-bar-*`); stacking below 640px reuses the `.ui-toolbar` base rules, so
992
+ * 1440/1024/390 behave identically to the composition form. */
993
+ .ui-filter-bar {
994
+ display: flex;
995
+ flex-direction: column;
996
+ gap: var(--filter-bar-section-gap);
997
+ }
998
+
999
+ .ui-filter-bar .ui-filter-bar-search {
1000
+ inline-size: 100%;
1001
+ }
1002
+
1003
+ .ui-filter-bar-end {
1004
+ display: flex;
1005
+ flex-wrap: wrap;
1006
+ align-items: center;
1007
+ gap: var(--space-2);
1008
+ width: 100%;
1009
+ }
1010
+
1011
+ /* Inside the end cluster the clear button gives up its own auto margin — the CLUSTER is what
1012
+ * parks at the inline end, so reset and actions stay adjacent in the keyboard/DOM order. */
1013
+ .ui-filter-bar-end .ui-toolbar-clear {
1014
+ margin-inline-start: 0;
1015
+ }
1016
+
1017
+ .ui-filter-bar-actions {
1018
+ display: flex;
1019
+ flex-wrap: wrap;
1020
+ align-items: center;
1021
+ gap: var(--space-2);
1022
+ }
1023
+
1024
+ @media (min-width: 640px) {
1025
+ .ui-filter-bar .ui-filter-bar-search {
1026
+ inline-size: min(100%, var(--filter-bar-search-width));
1027
+ }
1028
+
1029
+ .ui-filter-bar .ui-filter-bar-filter {
1030
+ min-inline-size: min(100%, var(--filter-bar-filter-width));
1031
+ }
1032
+
1033
+ .ui-filter-bar-end {
1034
+ width: auto;
1035
+ margin-inline-start: auto;
1036
+ }
1037
+ }
1038
+
1039
+ .ui-filter-bar-chips {
1040
+ display: flex;
1041
+ flex-wrap: wrap;
1042
+ align-items: center;
1043
+ gap: var(--filter-bar-chip-gap);
1044
+ }
1045
+
1046
+ .ui-filter-bar-chip {
1047
+ display: inline-flex;
1048
+ align-items: center;
1049
+ gap: var(--space-1);
1050
+ min-width: 0;
1051
+ }
1052
+
1053
+ .ui-filter-bar-meta {
1054
+ font-size: var(--filter-label-font-size);
1055
+ color: hsl(var(--muted-foreground));
1056
+ }
1057
+
1058
+ .ui-filter-bar-error {
1059
+ color: hsl(var(--destructive));
1066
1060
  }
1067
1061
 
1068
1062
  /* ── LegalDocumentShell (#222) ─────────────────────────────────────────────────
@@ -394,15 +394,10 @@
394
394
  box-shadow: 0 0 0 var(--focus-ring-width) hsl(var(--focus-ring-color, var(--ring)));
395
395
  }
396
396
 
397
- /* The step NUMBER carries the emphasis. Both knobs exist because the canonical hosted-identity
398
- * row marks the step with an accent TINT at normal weight, while the original marked it with
399
- * bold at the inherited colour — a service picks its own emphasis instead of forking this rule
400
- * (rule #45). Defaults are the original bold + inherited colour, so nothing moves. */
401
397
  .ui-steps-inline-index {
402
398
  flex: none;
403
399
  font-variant-numeric: tabular-nums;
404
- font-weight: var(--steps-inline-index-font-weight);
405
- color: var(--steps-inline-index-color, currentColor);
400
+ font-weight: var(--font-weight-bold);
406
401
  }
407
402
 
408
403
  .ui-steps-inline-title {
@@ -423,7 +418,8 @@
423
418
  color: hsl(var(--destructive));
424
419
  }
425
420
 
426
- .ui-steps-inline-item[data-status="wait"] .ui-steps-inline-control {
421
+ .ui-steps-inline-item[data-status="wait"] .ui-steps-inline-control,
422
+ .ui-steps-inline-separator {
427
423
  color: hsl(var(--muted-foreground));
428
424
  }
429
425
 
@@ -431,6 +427,5 @@
431
427
  width: var(--steps-inline-separator-size);
432
428
  height: var(--steps-inline-separator-size);
433
429
  flex: none;
434
- color: var(--steps-inline-separator-color, hsl(var(--muted-foreground)));
435
430
  }
436
431
  }
@@ -192,14 +192,9 @@
192
192
  text-align: center;
193
193
  }
194
194
 
195
- /* Device-authorization also owns its CODE FIELD (gh#12): the preset's subject is the code, so
196
- * the slot box comes from the preset instead of the generic square control tier. Scoped on the
197
- * shell so every OTP inside the device flow inherits it; a per-instance override still wins. */
198
195
  .ui-auth-shell[data-preset="device-authorization"] {
199
196
  --auth-shell-card-max-width: var(--auth-shell-device-card-max-width);
200
197
  --auth-shell-main-padding: var(--auth-shell-device-main-padding);
201
- --otp-slot-inline-size: var(--auth-shell-device-otp-slot-inline-size);
202
- --otp-slot-block-size: var(--auth-shell-device-otp-slot-block-size);
203
198
  }
204
199
 
205
200
  .ui-auth-shell[data-preset="context-selection"] {
@@ -343,12 +338,21 @@
343
338
  .ui-auth-shell[data-variant="canonical"] .ui-form-field {
344
339
  --field-label-gap: var(--auth-shell-field-label-gap);
345
340
  }
341
+ }
346
342
 
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
- }
343
+ /* INTENTIONALLY UNLAYERED (like density.css) — the Label primitive carries `text-sm leading-none`
344
+ * Tailwind utility classes, and the utilities layer is declared after `components`, so a
345
+ * components-layer declaration of the same properties can NEVER win regardless of specificity.
346
+ * That left `--auth-shell-field-label-font-size` / `--auth-shell-field-label-line-height` dead and
347
+ * the canonical field label rendering as a 14px/1.0 line box instead of the canonical 12px/1.5 =
348
+ * 18px, sitting the email input 4px high (gh#263). Unlayered author CSS out-cascades every layer;
349
+ * the two tokens remain the public tuning knobs. */
350
+ .ui-auth-shell[data-variant="canonical"] [data-slot="form-field-label"] [data-slot="label"] {
351
+ font-size: var(--auth-shell-field-label-font-size);
352
+ line-height: var(--auth-shell-field-label-line-height);
353
+ }
351
354
 
355
+ @layer components {
352
356
  /*
353
357
  * Compact auth card density — the two card axes are wired to SEPARATE knobs (gh#232), so a
354
358
  * service can shorten the canonical Login card without narrowing its column (or vice versa):
@@ -384,7 +388,7 @@
384
388
  gap: var(--auth-shell-divider-gap);
385
389
  color: hsl(var(--auth-shell-divider-label-color));
386
390
  font-size: var(--auth-shell-divider-label-font-size);
387
- line-height: 1;
391
+ line-height: var(--auth-shell-divider-label-line-height);
388
392
  }
389
393
 
390
394
  .ui-auth-divider-rule {
@@ -423,8 +427,8 @@
423
427
 
424
428
  /* registration = 15px inline at 390 (card x=15, width=360 — the same page rhythm as Login, so
425
429
  * 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. */
430
+ * stays deliberately larger than a bare gutter: at 390 the form is at its tallest and the
431
+ * consent/legal footer must not sit flush against the submit button. */
428
432
  .ui-auth-shell[data-preset="registration"] {
429
433
  --auth-shell-main-padding: var(--auth-shell-registration-main-padding-block-start-mobile)
430
434
  var(--auth-shell-registration-main-padding-inline-mobile)
@@ -524,11 +528,7 @@
524
528
  }
525
529
 
526
530
  /* AppShell-owned mobile drawer trigger — hidden on desktop, shown once the docked sidebar
527
- * collapses out at 900px, THE ONE AppShell breakpoint (gh#165, gh#259). NOT Tailwind's `lg`
528
- * (1024px): this comment said "below lg" for six minors while the shipped rule below and the
529
- * hamburger's own `max-[900px]:inline-flex` both fired at 900 — see the block comment on the
530
- * `@media (width <= 56.25rem)` rule for why 900 is the canonical value. The drawer nav itself is
531
- * a focus-trapped Sheet. */
531
+ * collapses out below `lg` (gh#165). The drawer nav itself is a focus-trapped Sheet. */
532
532
  .app-mobile-nav-trigger {
533
533
  display: none;
534
534
  flex: 0 0 auto;