@apliteni/apliteni-ui 0.32.0 → 0.33.1

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.
@@ -69,7 +69,10 @@
69
69
  flex-direction: column;
70
70
  font-family: var(--font-sans);
71
71
  background: var(--cmdk-surface);
72
- border: 1px solid var(--border);
72
+ /* The two-step edge and the drop: --border-strong outside, --border one pixel
73
+ inside it, then the broad faint drop. why: docs/specification.md#elevation */
74
+ border: 1px solid var(--border-strong);
75
+ box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
73
76
  border-radius: var(--radius-lg);
74
77
  overflow: hidden;
75
78
  opacity: 0;
@@ -79,7 +82,11 @@
79
82
  transform var(--dur-med) var(--ease);
80
83
  }
81
84
  .ui-cmdk.is-open .ui-cmdk__panel { opacity: 1; transform: translate(-50%, 0); }
82
- .ui-cmdk__panel:focus-visible { outline: none; box-shadow: var(--ring); }
85
+ /* The ring is COMPOSED with the floating treatment, not written over it: a
86
+ box-shadow list replaces the whole list, so `box-shadow: var(--ring)` alone
87
+ took the panel's edge and its drop off for as long as it held focus.
88
+ why: docs/specification.md#elevation */
89
+ .ui-cmdk__panel:focus-visible { outline: none; box-shadow: var(--ring), inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); }
83
90
 
84
91
  /* -- The text box --------------------------------------------------------- */
85
92
 
@@ -62,7 +62,10 @@
62
62
  gap: var(--space-2);
63
63
  padding: var(--space-5);
64
64
  background: var(--confirm-surface);
65
- border: 1px solid var(--border);
65
+ /* The two-step edge and the drop: --border-strong outside, --border one pixel
66
+ inside it, then the broad faint drop. why: docs/specification.md#elevation */
67
+ border: 1px solid var(--border-strong);
68
+ box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
66
69
  border-radius: var(--radius-lg);
67
70
  opacity: 0;
68
71
  transform: translateY(8px);
@@ -71,7 +74,11 @@
71
74
  transform var(--dur-med) var(--ease);
72
75
  }
73
76
  .ui-confirm.is-open .ui-confirm__panel { opacity: 1; transform: none; }
74
- .ui-confirm__panel:focus-visible { outline: none; box-shadow: var(--ring); }
77
+ /* The ring is COMPOSED with the floating treatment, not written over it: a
78
+ box-shadow list replaces the whole list, so `box-shadow: var(--ring)` alone
79
+ took the panel's edge and its drop off for as long as it held focus.
80
+ why: docs/specification.md#elevation */
81
+ .ui-confirm__panel:focus-visible { outline: none; box-shadow: var(--ring), inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); }
75
82
 
76
83
  /* -- Slots ----------------------------------------------------------------- */
77
84
  /* The question and the answers keep their size; the consequence between them is
@@ -58,9 +58,19 @@
58
58
  display: flex;
59
59
  flex-direction: column;
60
60
  background: var(--drawer-surface);
61
+ /* A drawer is flush to a screen edge, so it has one edge rather than four and the
62
+ two-step edge runs along that one: --border-strong on the edge rule below, the
63
+ inner line one pixel inside it. This is the one floating surface that writes no
64
+ inset RING — a full ring would draw 1px lines across the top and bottom of a
65
+ full-height panel, where there is no edge — so it composes its own directional
66
+ line with the drops instead. The line is a variable so the focus rule can
67
+ compose all three layers without knowing which edge it is on; its default is a
68
+ no-op layer rather than `none`, which is valid only alone and would invalidate
69
+ the list. why: docs/specification.md#elevation */
70
+ --drawer-line: 0 0 0 0 transparent;
71
+ box-shadow: var(--drawer-line), var(--elev-drop);
61
72
  transition: transform var(--dur-med) var(--ease);
62
73
  }
63
- .ui-drawer__panel:focus-visible { outline: none; box-shadow: var(--ring); }
64
74
  .ui-drawer.is-open .ui-drawer__panel { transform: none; }
65
75
 
66
76
  /* -- Edge anchoring + slide-in direction ---------------------------------- */
@@ -72,12 +82,14 @@
72
82
  }
73
83
  .ui-drawer--right .ui-drawer__panel {
74
84
  right: 0;
75
- border-left: 1px solid var(--border);
85
+ border-left: 1px solid var(--border-strong);
86
+ --drawer-line: inset 1px 0 0 var(--border);
76
87
  transform: translateX(100%);
77
88
  }
78
89
  .ui-drawer--left .ui-drawer__panel {
79
90
  left: 0;
80
- border-right: 1px solid var(--border);
91
+ border-right: 1px solid var(--border-strong);
92
+ --drawer-line: inset -1px 0 0 var(--border);
81
93
  transform: translateX(-100%);
82
94
  }
83
95
  .ui-drawer--top .ui-drawer__panel,
@@ -88,15 +100,23 @@
88
100
  }
89
101
  .ui-drawer--top .ui-drawer__panel {
90
102
  top: 0;
91
- border-bottom: 1px solid var(--border);
103
+ border-bottom: 1px solid var(--border-strong);
104
+ --drawer-line: inset 0 -1px 0 var(--border);
92
105
  transform: translateY(-100%);
93
106
  }
94
107
  .ui-drawer--bottom .ui-drawer__panel {
95
108
  bottom: 0;
96
- border-top: 1px solid var(--border);
109
+ border-top: 1px solid var(--border-strong);
110
+ --drawer-line: inset 0 1px 0 var(--border);
97
111
  transform: translateY(100%);
98
112
  }
99
113
 
114
+ /* The ring is COMPOSED with the floating treatment rather than written over it — a
115
+ box-shadow list replaces the whole list, so a bare `box-shadow: var(--ring)` would
116
+ take the drawer's edge and its drop off for as long as it held focus.
117
+ why: docs/specification.md#elevation */
118
+ .ui-drawer__panel:focus-visible { outline: none; box-shadow: var(--ring), var(--drawer-line), var(--elev-drop); }
119
+
100
120
  /* -- Sizes ----------------------------------------------------------------- */
101
121
  /* left|right → width; top|bottom → height. Same names, axis chosen by side. */
102
122
  .ui-drawer--right.ui-drawer--sm, .ui-drawer--left.ui-drawer--sm { --drawer-w: var(--panel-sm); }
@@ -60,11 +60,18 @@
60
60
  /* The floating step, not the sunken one — a panel held open over a card cannot be
61
61
  painted the colour of an inset. why: docs/specification.md#elevation */
62
62
  background: var(--bg-elevated);
63
- border: 1px solid var(--border);
63
+ /* The two-step edge and the drop: --border-strong outside, --border one pixel
64
+ inside it, then the broad faint drop. why: docs/specification.md#elevation */
65
+ border: 1px solid var(--border-strong);
66
+ box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
64
67
  border-radius: var(--radius-lg);
65
68
  padding: 6px;
66
69
  opacity: 0;
67
70
  visibility: hidden;
71
+ /* Drawn for the whole of the fade out, so the rows do not vanish mid-fade — and a
72
+ box that is drawn is a box that is hit. The open rules take this back.
73
+ why: docs/specification.md#the-dropdown-panel */
74
+ pointer-events: none;
68
75
  transform: translateY(-6px);
69
76
  transition:
70
77
  opacity var(--dur-med) var(--ease),
@@ -89,7 +96,13 @@
89
96
  transform: translateY(6px);
90
97
  }
91
98
 
92
- .ui-dropdown.open .ui-dropdown__panel { opacity: 1; visibility: visible; transform: translateY(0); }
99
+ .ui-dropdown.open .ui-dropdown__panel { opacity: 1; visibility: visible; pointer-events: auto; transform: translateY(0); }
100
+ /* Open, the panel is visible AT ONCE: `visibility` is off the list, so it is not
101
+ held at `hidden` for the frame a key opens the panel in — the frame the focus
102
+ call happens in. Closing still fades on every property it always did.
103
+ why: docs/specification.md#the-dropdown-panel */
104
+ .ui-dropdown.open .ui-dropdown__panel,
105
+ .ui-dropdown__panel--portal.is-open { transition-property: opacity, transform; }
93
106
 
94
107
  /* Portalled panel — wireDropdown() moves it onto <body>, clear of every
95
108
  ancestor's overflow and every ancestor's stacking context. The descendant
@@ -101,7 +114,7 @@
101
114
  position: fixed;
102
115
  top: auto; right: auto; bottom: auto; left: auto;
103
116
  }
104
- .ui-dropdown__panel--portal.is-open { opacity: 1; visibility: visible; transform: translateY(0); }
117
+ .ui-dropdown__panel--portal.is-open { opacity: 1; visibility: visible; pointer-events: auto; transform: translateY(0); }
105
118
 
106
119
  /* Item row — [icon] [main: label + desc] [badge] [tick]. A row is a <div>, an
107
120
  <a href> or a <button>: `is-selected` and __tick below mean a row gets
@@ -160,7 +173,7 @@
160
173
  }
161
174
  .ui-dropdown__badge.is-live { color: var(--chip-success-ink); background: var(--chip-success-fill); }
162
175
  /* The accent counter keeps the badge's flat surface and changes only the ink — the shape
163
- src/styles/nav.css:128 `.ui-nav__item.is-active .ui-nav__badge.is-accent` already holds.
176
+ src/styles/nav.css:163 `.ui-nav__item.is-active .ui-nav__badge.is-accent` already holds.
164
177
  The accent wash goes on a base surface, never a raised one, and #295 made this panel a
165
178
  raised one. why: docs/specification.md#elevation */
166
179
  .ui-dropdown__badge.is-accent { color: var(--accent); background: var(--surface-3); }
@@ -169,8 +182,12 @@
169
182
  .ui-dropdown__item.is-disabled { color: var(--disabled-ink); cursor: not-allowed; pointer-events: none; }
170
183
  .ui-dropdown__item.is-disabled .ui-dropdown__label,
171
184
  .ui-dropdown__item.is-disabled .ui-dropdown__desc { color: inherit; }
185
+ /* Quiet at rest, --pink on the way to being pressed. Both states, not hover alone:
186
+ the ring says where the reader is, not that this row ends something.
187
+ why: docs/specification.md#a-dropdown-row-is-a-div-a-link-or-a-button */
172
188
  .ui-dropdown__item.is-danger .ui-dropdown__label { color: var(--muted); }
173
- .ui-dropdown__item.is-danger:hover .ui-dropdown__label { color: var(--pink); }
189
+ .ui-dropdown__item.is-danger:hover .ui-dropdown__label,
190
+ .ui-dropdown__item.is-danger:focus-visible .ui-dropdown__label { color: var(--pink); }
174
191
 
175
192
  /* Grouped sections. `~` and :not([hidden]), because `+` counts a group a search
176
193
  query has hidden and drew the divider above the first group left showing. */
@@ -200,11 +217,6 @@
200
217
  variant emits, so the plain dropdown renders as it did.
201
218
  why: docs/specification.md#a-dropdown-with-a-search-field */
202
219
  .ui-dropdown__panel--search { min-width: 280px; }
203
- /* Open, the panel is visible at once instead of at the first step of the
204
- `visibility` transition, because a browser will not focus a field in a box
205
- that is still `hidden` and opening puts focus in the field. Closing still fades. */
206
- .ui-dropdown.open .ui-dropdown__panel--search,
207
- .ui-dropdown__panel--search.is-open { transition-property: opacity, transform; }
208
220
 
209
221
  .ui-dropdown__search { position: relative; display: flex; align-items: center; margin-bottom: 6px; }
210
222
  .ui-dropdown__search-ic {
@@ -16,6 +16,11 @@
16
16
  background: var(--bg);
17
17
  color: var(--text);
18
18
  font-family: var(--font-sans);
19
+ /* The rail's width, and the one property the fold moves: the nav's own column
20
+ plus the inset either side plus the rule, so the shell has no geometry of its
21
+ own to drift from the rows inside it. why: docs/specification.md#the-page-shell */
22
+ --ui-rail-rule: 1px;
23
+ --ui-rail-w: calc(var(--ui-nav-col) + 2 * var(--space-4) + var(--ui-rail-rule));
19
24
  }
20
25
  /* With a topbar above it, the shell starts below one. 52px is .topbar's height
21
26
  in topbar.css; the topbar sticks so the rail below it can too. */
@@ -33,21 +38,27 @@
33
38
  flex-direction: column;
34
39
  gap: var(--space-5);
35
40
  padding: var(--space-5) var(--space-4);
36
- border-right: 1px solid var(--border);
41
+ border-right: var(--ui-rail-rule) solid var(--border);
37
42
  background: var(--surface-2);
38
43
  position: sticky;
39
44
  top: var(--ui-app-top, 0px);
40
45
  height: calc(100vh - var(--ui-app-top, 0px));
46
+ width: var(--ui-rail-w);
47
+ /* hidden across, auto down: the fold clips what leaves the screen rather than
48
+ laying it out again. Two longhands, not the shorthand — the gates' CSS engine
49
+ does not read the pair. why: docs/specification.md#the-page-shell */
50
+ overflow-x: hidden;
41
51
  overflow-y: auto;
52
+ transition: width var(--dur-med) var(--ease);
42
53
  }
54
+ /* Each block of the rail keeps the open column while the box closes over it. The
55
+ nav declares its own; these are the two that are not the nav. */
56
+ .ui-app__head,
57
+ .ui-app__user { width: var(--ui-nav-col); flex: none; }
43
58
  /* The rail's hover WAS --surface-2, so moving the rail onto it would have made
44
- hovering a row do nothing. Scoped, so a sidebarNav() anywhere else is
45
- untouched — and the destructive row is left out, because this rule ties with
46
- nav.css's danger wash on specificity and wins on source order alone. Sign out
47
- was quietly losing the --pink wash the guideline page cites nav.css for. The
48
- current row is left out for the same reason: this rule outranks nav.css's
49
- active hover, and painting --surface-3 over a row already resting on it is
50
- what made the row you are standing on the one row that ignores the pointer. */
59
+ hovering a row do nothing. The danger row and the current row are left out by
60
+ name, because this rule outranks the states nav.css writes for them.
61
+ why: CONTRIBUTING.md#a-rule-that-outranks-another-cancels-every-state-that-other-one-writes */
51
62
  .ui-app__rail .ui-nav__item:not(.is-danger):not(.is-active):hover { background: var(--surface-3); }
52
63
  .ui-app__brand {
53
64
  display: flex;
@@ -55,6 +66,10 @@
55
66
  gap: var(--space-2);
56
67
  padding: 2px 6px;
57
68
  text-decoration: none;
69
+ /* The lockup leaves on the words' clock, `visibility` with it so the link leaves
70
+ the tab order — `linear`, as every discrete property in the kit is.
71
+ why: docs/specification.md#the-page-shell */
72
+ transition: opacity var(--dur-fast) var(--ease), visibility var(--dur-fast) linear;
58
73
  /* display: brand — the rail's lockup is the same mark topbar.css sets, and the
59
74
  two have to be the same face or the shell reads as two products. */
60
75
  font-family: var(--font-display);
@@ -66,11 +81,23 @@
66
81
  .ui-app .ui-app__brand { color: var(--strong); }
67
82
  .ui-app__brand svg { display: block; flex: none; }
68
83
 
84
+ /* The head band: the product's mark, and the rail's own control at the far end of
85
+ the same line. `min-width: 0` so a long word shrinks the lockup rather than pushing
86
+ the control off the band. why: docs/specification.md#the-page-shell */
87
+ .ui-app__head {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: var(--space-2);
91
+ padding-bottom: var(--space-3);
92
+ border-bottom: 1px solid var(--border);
93
+ }
94
+ .ui-app__head > .ui-app__brand { min-width: 0; }
95
+
69
96
  /* The nav grows, so the reader block below it pins to the bottom of a full-height
70
- rail instead of hugging the last nav row, and the nav's own footer — sign out —
71
- sits directly above it. */
97
+ rail instead of hugging the last nav row. The nav's own footer slot is unused —
98
+ sign out is in the reader's menu, because it ends a session rather than going
99
+ anywhere — so the rail's foot is the account block alone. */
72
100
  .ui-app__rail .ui-nav--side { flex: 1 1 auto; }
73
- .ui-app__rail .ui-nav__foot { margin-top: auto; }
74
101
 
75
102
  /* Which row is current is structural here, not chromatic: the resting glyphs
76
103
  step down so the current one is the brightest whatever --accent is doing, and
@@ -78,30 +105,37 @@
78
105
  smallest of three weak signals doing the most work, so it grows with the row. */
79
106
  /* Re-tuned at #295, which took the light rail down a step with the page. Below 720px
80
107
  the glyph is the whole row, so it is held at text grade rather than at 1.4.11's bar —
81
- stories/apps/shell-states.test.js:427 `a resting glyph is dimmer but still legible`. */
108
+ stories/apps/shell-states.test.js:1340 `a resting glyph is dimmer but still legible`. */
82
109
  .ui-app__rail .ui-nav__ic svg { opacity: 0.66; }
83
110
  .ui-app__rail .ui-nav__item.is-active .ui-nav__ic svg,
84
111
  .ui-app__rail .ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: currentColor; } /* motion: still — the glyph brightens with its row, whose own highlight already transitions */
85
112
  .ui-app__rail .ui-nav__item.is-active::before { width: 3px; height: 62%; }
86
113
 
87
- /* Sign out is a navigation row and rests like one. The danger intent is the
88
- step nav.css writes for it, which a resting --muted was pre-empting — it made
89
- the last row of the rail the quietest thing in it. Both of nav.css's danger
90
- states are named here, not hover alone: this rule outranks them, so a state
91
- it forgets is a state it silently cancels. */
114
+ /* A destructive row of the rail is a navigation row and rests like one; the danger
115
+ intent is the step nav.css writes for it. Both of nav.css's danger states are
116
+ named, not hover alone.
117
+ why: CONTRIBUTING.md#a-rule-that-outranks-another-cancels-every-state-that-other-one-writes */
92
118
  .ui-app__rail .ui-nav__item.is-danger:not(:hover):not(:focus-visible) { color: var(--text); }
93
119
 
94
- /* One rule closes the rail, not two twenty pixels apart: the nav's own footer
95
- keeps its hairline and the reader block is separated by space alone — it has
96
- an avatar and a weight change already. */
120
+ /* One rule closes the rail, and it is this one: the nav's footer slot is empty now
121
+ that sign out is in the menu this block opens, so the hairline that fenced sign
122
+ out off moved down onto the block it was fencing. Two of them twenty pixels apart
123
+ read as a third region of the rail rather than as its foot. */
97
124
  .ui-app__user {
98
125
  display: flex;
99
126
  align-items: center;
100
127
  gap: var(--space-2);
101
- padding: var(--space-5) var(--space-1) 0;
128
+ padding-top: var(--space-5);
129
+ border-top: 1px solid var(--border);
102
130
  }
131
+ /* The avatar is the reader block's own glyph, so half the difference between the
132
+ glyph column and itself is the inset that centres it on the line every glyph above
133
+ it stands on. The arithmetic and not the 5.5 it comes to: both numbers are declared
134
+ elsewhere. why: docs/specification.md#the-page-shell */
135
+ .ui-app { --ui-app-av: 30px; }
103
136
  .ui-app__av {
104
- width: 30px; height: 30px; flex: none;
137
+ width: var(--ui-app-av); height: var(--ui-app-av); flex: none;
138
+ margin-inline-start: calc((var(--ui-nav-strip) - var(--ui-app-av)) / 2);
105
139
  border-radius: var(--radius-pill);
106
140
  background: var(--surface-3);
107
141
  color: var(--strong);
@@ -115,6 +149,118 @@
115
149
  the rail column is max-content, so a long one widens the rail instead. */
116
150
  .ui-app__who span { display: block; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
117
151
 
152
+ /* -- the reader's menu ---------------------------------------------------- */
153
+ /* The account block is a dropdown() trigger, so it takes the rail's own row skin
154
+ rather than the dropdown's pill — in place it is the last row of the rail. The
155
+ dropdown's own ring is left standing. why: docs/specification.md#the-page-shell */
156
+ .ui-app__user .ui-dropdown { width: 100%; }
157
+ .ui-app .ui-app__user-trigger {
158
+ width: 100%;
159
+ gap: var(--space-2);
160
+ /* As tall as the mark inside it plus its own padding — 30 + 4 + 4, written out
161
+ rather than as the calc() it comes from, and held to that arithmetic by
162
+ stories/apps/shell-states.test.js.
163
+ why: CONTRIBUTING.md#an-unresolved-var-measures-nothing-and-reports-green */
164
+ min-height: 38px;
165
+ padding: var(--space-1) 0;
166
+ border: 0;
167
+ border-radius: var(--radius-sm);
168
+ background: none;
169
+ color: var(--text);
170
+ font-size: inherit;
171
+ font-weight: inherit;
172
+ letter-spacing: normal;
173
+ }
174
+ .ui-app .ui-app__user-trigger:hover { background: var(--surface-3); }
175
+ .ui-app__user-trigger .ui-app__who { flex: 1 1 auto; text-align: left; }
176
+ /* The panel is portalled onto <body>, so nothing here may hang off .ui-app: by the
177
+ time it is open it is no longer inside the rail that drew it.
178
+ why: docs/specification.md#the-dropdown-panel */
179
+ .ui-app__user-panel .ui-dropdown__head { display: flex; flex-direction: column; gap: 2px; }
180
+ .ui-app__user-panel .ui-dropdown__head b { display: block; color: var(--strong); font-size: 12.5px; font-weight: var(--weight-semibold); }
181
+ /* The address wraps rather than truncating: nothing bounds this line, so
182
+ `text-overflow` would have nothing to fire against — the inert pair
183
+ .ui-app__who's own comment warns about. `anywhere` keeps an address with no
184
+ break in it from widening the panel instead. */
185
+ .ui-app__user-panel .ui-dropdown__head span { display: block; color: var(--muted); font-size: 11px; overflow-wrap: anywhere; }
186
+
187
+ /* The toggle's cell, at the far end of the brand row: the auto margin puts it there
188
+ with or without a wordmark beside it, and the offset below rides it back onto the
189
+ glyph column as the rail closes. `position: relative` and not a transform, because
190
+ the chip inside is `position: fixed`. why: docs/specification.md#the-page-shell */
191
+ .ui-app__fold-row {
192
+ flex: none;
193
+ margin-inline-start: auto;
194
+ position: relative;
195
+ inset-inline-start: 0;
196
+ transition: inset-inline-start var(--dur-med) var(--ease);
197
+ }
198
+ /* One box on the glyph column at both widths — --ui-nav-strip, the width nav.css
199
+ derives the closed rail from. `gap: 0` leaves that column the whole of the
200
+ content box, so the label below is squeezed to nothing until the chip rule lifts
201
+ it out. why: docs/specification.md#the-page-shell */
202
+ .ui-app__fold { width: var(--ui-nav-strip); gap: 0; }
203
+ /* The chip rule lifts the name out of the flow, and a 17px glyph is shorter than
204
+ the line the name occupied — so the button lost 0.39px on hover and moved every
205
+ row under it. The glyph box carries the line instead, in both states.
206
+ why: docs/specification.md#the-page-shell */
207
+ .ui-app__fold .ui-nav__ic { height: 1lh; }
208
+ .ui-app .ui-app__fold { color: var(--dim); }
209
+ .ui-app .ui-app__fold:hover,
210
+ .ui-app .ui-app__fold:focus-visible { color: var(--strong); }
211
+ /* The width's own clock, not the words', so the mark and the closing edge arrive
212
+ together; no `!important`, so the reduced-motion net still takes it. 6 is not a
213
+ number: shell.js draws the frame 3..21 and the seam at 9, and 6 mirrors it about
214
+ the centre. why: docs/specification.md#the-page-shell */
215
+ .ui-app__fold .ui-app__fold-seam { transition: transform var(--dur-med) var(--ease); }
216
+ .ui-app.is-collapsed .ui-app__fold .ui-app__fold-seam { transform: translateX(6px); }
217
+
218
+ /* -- The collapsed rail --------------------------------------------------- */
219
+ /* Each rule here has its twin in the 720px block below; nothing lays a row out
220
+ again. why: docs/specification.md#the-page-shell */
221
+ .ui-app.is-collapsed { --ui-rail-w: calc(var(--ui-nav-strip) + 2 * var(--space-4) + var(--ui-rail-rule)); }
222
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__cap,
223
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__label,
224
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__badge,
225
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__caret,
226
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-app__who,
227
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-dropdown__chevron,
228
+ :where(.ui-app.is-collapsed) .ui-app__brand span { opacity: 0; pointer-events: none; }
229
+ /* The toggle rides onto the glyph column — the two widths' own difference — and the
230
+ lockup goes whole, since that column is where its mark stood. The mark may only go
231
+ where the control arrives: below 720px the toggle is not drawn, so the 720px block
232
+ gives the lockup back rather than leaving an empty band with a hairline under it.
233
+ why: docs/specification.md#the-page-shell */
234
+ :where(.ui-app.is-collapsed) .ui-app__fold-row { inset-inline-start: calc(var(--ui-nav-strip) - var(--ui-nav-col)); }
235
+ :where(.ui-app.is-collapsed) .ui-app__brand { opacity: 0; visibility: hidden; pointer-events: none; }
236
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__sub { margin-inline-start: 0; padding-inline-start: 0; border-inline-start-width: 0; }
237
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item:not(:has(.ui-nav__ic))::after { opacity: 0.62; }
238
+ /* The label IS the chip. `fixed` is the fallback placement, out of the rail's own
239
+ clip; the @supports branch below pins it to the row and is the only one that
240
+ survives a scroll. The toggle's selector is unscoped, because it is wordless at
241
+ both widths.
242
+
243
+ It floats by role — a readout placed over the page for as long as a pointer
244
+ rests on a row — so it takes the floating treatment the hover readout takes,
245
+ which is the same shape on the same step: the two-step edge and the drops.
246
+ why: docs/specification.md#the-page-shell
247
+ why: docs/specification.md#elevation */
248
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item:is(:hover, :focus-visible) > .ui-nav__label,
249
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-app__user-trigger:is(:hover, :focus-visible) > .ui-app__who,
250
+ .ui-app__rail .ui-app__fold:is(:hover, :focus-visible) > .ui-nav__label { position: fixed; left: calc(var(--ui-rail-w) + var(--space-2)); z-index: var(--z-dropdown); width: max-content; max-width: 15rem; margin-block: -7px; padding: 6px 10px; border: 1px solid var(--border-strong); box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); border-radius: var(--radius-xs); background: var(--surface-3); color: var(--strong); opacity: 1; pointer-events: none; }
251
+ @supports (anchor-name: --a) and (anchor-scope: --a) {
252
+ .ui-app.is-collapsed,
253
+ .ui-app:has(.ui-app__fold) { anchor-scope: --ui-rail; }
254
+ :where(.ui-app.is-collapsed) .ui-app__rail,
255
+ .ui-app__rail:has(.ui-app__fold) { anchor-name: --ui-rail; }
256
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item,
257
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-app__user-trigger,
258
+ .ui-app__rail .ui-app__fold { anchor-name: --ui-rail-row; anchor-scope: --ui-rail-row; }
259
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item:is(:hover, :focus-visible) > .ui-nav__label,
260
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-app__user-trigger:is(:hover, :focus-visible) > .ui-app__who,
261
+ .ui-app__rail .ui-app__fold:is(:hover, :focus-visible) > .ui-nav__label { position-anchor: --ui-rail-row; left: calc(anchor(--ui-rail right) + var(--space-2)); top: anchor(top); bottom: anchor(bottom); align-self: center; margin-block: 0; }
262
+ }
263
+
118
264
  /* The reading column centres in the track it is given — without width: 100% it
119
265
  was hugging the rail, 519px of dead background on the right at 1728px wide.
120
266
  The width is what keeps the auto margins honest: an auto inline margin takes
@@ -166,45 +312,51 @@
166
312
  .ui-toolbar > .ui-input-group { flex: 1 1 6rem; min-width: 0; }
167
313
 
168
314
  /* -- The narrow rail ------------------------------------------------------ */
169
- /* Below this width the rail folds to an icon strip instead of disappearing.
170
- sidebarNav() names every row at every width, so folding the label out of view
171
- costs a screen reader nothing. Scoped to the shell's own rail — a sidebarNav()
172
- somewhere else keeps its labels. The 44px floor is the touch target, and the
173
- column is that floor plus the rail's own padding and border — measured, not
174
- guessed. Nothing here resizes the glyph: an icon-sizing rule inside a media
175
- query is one src/styles/icon-size.test.js cannot measure, and it says so. */
315
+ /* Below this width the rail folds to the icon strip instead of disappearing, and
316
+ it is the fold the reader's own press makes — every rule here has its twin
317
+ above. Scoped to the shell's own rail; a sidebarNav() elsewhere keeps its
318
+ labels. Nothing here resizes a glyph: an icon-sizing rule inside a media query
319
+ is one src/styles/icon-size.test.js cannot measure, and it says so. */
176
320
  @media (max-width: 720px) {
177
- .ui-app { grid-template-columns: 58px 1fr; }
178
- .ui-app__rail { padding: var(--space-3) 6px; gap: var(--space-3); }
179
- .ui-app__rail .ui-nav--side { width: auto; }
321
+ .ui-app { --ui-rail-w: calc(var(--ui-nav-strip) + 2 * var(--space-4) + var(--ui-rail-rule)); }
322
+ /* The strip is the only layout this width has, so there is nothing to fold. */
323
+ .ui-app__rail .ui-app__fold-row { display: none; }
324
+ /* …and with it the band, when the band has nothing else in it: a head with no
325
+ wordmark is the toggle alone, so at this width it is padding and a hairline
326
+ drawn over nothing. The reader's fold cannot take this rule — there the toggle
327
+ IS drawn. why: docs/specification.md#the-page-shell */
328
+ .ui-app__head:not(:has(> .ui-app__brand)) { display: none; }
329
+ /* And where the band does hold the mark, the mark stays — at this width the press
330
+ that folded the rail on a desktop still reads out of the cookie, and the control
331
+ that would undo it is the one above. A head band faded to nothing here is an empty
332
+ 41px box over a rule, with no way home. The reader's fold cannot take this rule —
333
+ there the toggle lands on the column the mark gives up.
334
+ why: docs/specification.md#the-page-shell */
335
+ .ui-app__brand { opacity: 1; visibility: visible; pointer-events: auto; }
180
336
  .ui-app__rail .ui-nav__cap,
181
337
  .ui-app__rail .ui-nav__label,
182
338
  .ui-app__rail .ui-nav__badge,
183
339
  .ui-app__rail .ui-nav__caret,
184
340
  .ui-app__rail .ui-app__who,
185
- .ui-app__brand span { display: none; }
186
- .ui-app__rail .ui-nav__item { justify-content: center; gap: 0; padding: 0; min-height: 44px; }
187
- .ui-app__rail .ui-nav__item.is-active::before { left: 3px; }
188
- .ui-app__rail .ui-nav__foot { padding-top: var(--space-2); }
189
- .ui-app__brand { justify-content: center; padding: 2px 0; }
190
- .ui-app__user { justify-content: center; padding-left: 0; padding-right: 0; }
191
- /* A group's children are rail rows at this width too. The nested list is NOT
192
- folded away: wireNav() toggles the `hidden` attribute and nothing else, so
193
- a display:none here left the toggle announcing aria-expanded over a list it
194
- could not reach — and took the row carrying aria-current with it. What goes
195
- instead is the indent and its guide hairline; a 46px column has no room. */
196
- .ui-app__rail .ui-nav__sub { margin: 0; padding-left: 0; border-left: 0; }
197
- .ui-app__rail .ui-nav__item--sub.is-active::before { left: 3px; }
198
- /* sidebarNav() takes `icon` as optional at every level, and the glyph is the
199
- whole of a row on screen here — so a row without one, at any level, would be
200
- a blank 44px target. It gets a dot in the row's own ink instead; the
201
- aria-label names it either way. */
202
- .ui-app__rail .ui-nav__item:not(:has(.ui-nav__ic))::after {
203
- content: "";
204
- width: 5px; height: 5px;
205
- border-radius: 50%;
206
- background: currentColor;
207
- opacity: 0.62;
341
+ .ui-app__rail .ui-dropdown__chevron,
342
+ .ui-app__brand span { opacity: 0; pointer-events: none; }
343
+ .ui-app__rail .ui-nav__sub { margin-inline-start: 0; padding-inline-start: 0; border-inline-start-width: 0; }
344
+ .ui-app__rail .ui-nav__item:not(:has(.ui-nav__ic))::after { opacity: 0.62; }
345
+ /* The one line of this block that is deliberately NOT the reader's fold: a finger
346
+ is the only pointer this width has, so a row and the account block are held to
347
+ 44px. Neither floor can spread to the press, where growing the box is the one
348
+ thing the travel promises nothing does. why: docs/specification.md#the-page-shell */
349
+ .ui-app__rail .ui-nav__item,
350
+ .ui-app__rail .ui-app__user-trigger { min-height: 44px; }
351
+ .ui-app__rail .ui-nav__item:is(:hover, :focus-visible) > .ui-nav__label,
352
+ .ui-app__rail .ui-app__user-trigger:is(:hover, :focus-visible) > .ui-app__who { position: fixed; left: calc(var(--ui-rail-w) + var(--space-2)); z-index: var(--z-dropdown); width: max-content; max-width: 15rem; margin-block: -7px; padding: 6px 10px; border: 1px solid var(--border-strong); box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); border-radius: var(--radius-xs); background: var(--surface-3); color: var(--strong); opacity: 1; pointer-events: none; }
353
+ @supports (anchor-name: --a) and (anchor-scope: --a) {
354
+ .ui-app { anchor-scope: --ui-rail; }
355
+ .ui-app__rail { anchor-name: --ui-rail; }
356
+ .ui-app__rail .ui-nav__item,
357
+ .ui-app__rail .ui-app__user-trigger { anchor-name: --ui-rail-row; anchor-scope: --ui-rail-row; }
358
+ .ui-app__rail .ui-nav__item:is(:hover, :focus-visible) > .ui-nav__label,
359
+ .ui-app__rail .ui-app__user-trigger:is(:hover, :focus-visible) > .ui-app__who { position-anchor: --ui-rail-row; left: calc(anchor(--ui-rail right) + var(--space-2)); top: anchor(top); bottom: anchor(bottom); align-self: center; margin-block: 0; }
208
360
  }
209
361
  .ui-app__main { padding: 28px var(--space-4) 56px; }
210
362
  .ui-app__main h1 { font-size: 25px; }