@apliteni/apliteni-ui 0.61.2 → 0.63.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.
@@ -210,8 +210,8 @@ export function select({ options = [], value, name, id, ariaLabel, disabled } =
210
210
  }).join('');
211
211
  return `<select class="ui-select"${id ? ` id="${esc(id)}"` : ''}${name ? ` name="${esc(name)}"` : ''}${ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ''}${disabled ? ' disabled' : ''}>${opts}</select>`;
212
212
  }
213
- export function checkbox({ label, checked, type = 'checkbox', name } = {}) {
214
- return `<label class="ui-check"><input type="${esc(type)}"${name ? ` name="${esc(name)}"` : ''}${checked ? ' checked' : ''}><span>${label}</span></label>`;
213
+ export function checkbox({ label, checked, type = 'checkbox', name, disabled = false } = {}) {
214
+ return `<label class="ui-check"><input type="${esc(type)}"${name ? ` name="${esc(name)}"` : ''}${checked ? ' checked' : ''}${disabled ? ' disabled' : ''}><span>${label}</span></label>`;
215
215
  }
216
216
  // `label` becomes the input's accessible name (a bare switch has no visible text,
217
217
  // so it needs one). Defaults to "Toggle" so a control is never left unlabelled.
@@ -135,12 +135,17 @@ export function railCollapsed(cookies) {
135
135
  // The name says what the press will do, and aria-expanded says what the rail is.
136
136
  const railName = (collapsed) => (collapsed ? 'Expand sidebar' : 'Collapse sidebar');
137
137
 
138
- // A frame that holds still and a seam that crosses it. Only the two nodes are
139
- // written here — a seam that travels has to be a child a stylesheet can reach —
140
- // and they are spliced into icon()'s own wrapper rather than a copy of it.
141
- // why: docs/specification.md#the-page-shell
138
+ // Lucide `panel-left-close`, unmodified: a frame, the seam at 9, and a chevron
139
+ // pointing the way the press moves the rail's edge. The folded rail mirrors the
140
+ // chevron about 15 in layout.css, which lands it exactly on Lucide
141
+ // `panel-left-open` — so both states are a shipped glyph and neither is drawn by
142
+ // hand. The seam does not move: it IS the rail, and the compartment it cuts off
143
+ // stays on the side the rail is on. Only the nodes are written here — a child a
144
+ // stylesheet can reach — and they are spliced into icon()'s own wrapper rather
145
+ // than a copy of it. why: docs/specification.md#the-page-shell
142
146
  const MARK = '<rect x="3" y="3" width="18" height="18" rx="2"/>'
143
- + '<path class="ui-app__fold-seam" d="M9 3v18"/>';
147
+ + '<path class="ui-app__fold-seam" d="M9 3v18"/>'
148
+ + '<path class="ui-app__fold-arrow" d="m16 15-3-3 3-3"/>';
144
149
  const railMark = () => icon('').replace('></svg>', `>${MARK}</svg>`);
145
150
 
146
151
  // The rail's own skin, outside the <nav>: folding the rail is not a place to go.
@@ -157,7 +157,9 @@
157
157
  transform: translate(-50%, -50%);
158
158
  }
159
159
  .ui-check input[type="radio"] { border-radius: 50%; }
160
- .ui-check input:hover { border-color: var(--accent); }
160
+ /* :enabled, or the accent lights the border of a box that cannot be clicked —
161
+ the same "press me" a disabled control has to stop making. #429 */
162
+ .ui-check input:enabled:hover { border-color: var(--accent); }
161
163
  .ui-check input:checked { background: var(--accent); border-color: var(--accent); }
162
164
  .ui-check input[type="checkbox"]:checked::after {
163
165
  content: "";
@@ -177,6 +179,26 @@
177
179
  }
178
180
  .ui-check input:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); }
179
181
 
182
+ /* Off. `.ui-check` had no disabled rule at all until #429 put a disabled checkbox
183
+ on screen: the box kept --surface-2 and --border-strong, its words kept --text,
184
+ and it was the word "unavailable" in the label that carried the state. The same
185
+ flat pair the fields and the buttons take (#220, #273) — a paint on the box and
186
+ quiet ink on the words, never an `opacity`, which drags label and box toward the
187
+ ground together. A checked box drops the accent for the reason a disabled primary
188
+ button does: the accent IS the "press me". Its mark inverts to --disabled-surface,
189
+ so the tick and the dot keep the measured --disabled-ink pair in both themes. */
190
+ .ui-check input:disabled {
191
+ background: var(--disabled-surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
192
+ border-color: var(--disabled-border);
193
+ cursor: not-allowed;
194
+ }
195
+ .ui-check input:disabled:checked { background: var(--disabled-ink); border-color: var(--disabled-ink); }
196
+ .ui-check input[type="checkbox"]:disabled:checked::after { border-color: var(--disabled-surface); }
197
+ .ui-check input[type="radio"]:disabled:checked::after { background: var(--disabled-surface); /* ring-gap: inherit — the mark inside the box; the box carries the focus indicator. */ }
198
+ /* The label is the click target, so the cursor belongs on it, and the words it
199
+ holds go quiet with the box beside them. */
200
+ .ui-check:has(input:disabled) { /* muted-ink: state — unavailable control */ color: var(--disabled-ink); cursor: not-allowed; }
201
+
180
202
  /* Toggle switch */
181
203
  .ui-switch {
182
204
  --sw-w: 44px;
@@ -106,13 +106,14 @@
106
106
  /* Which row is current is structural here, not chromatic: the resting glyphs
107
107
  step down so the current one is the brightest whatever --accent is doing, and
108
108
  the bar beside it carries the hue. A 3x17px bar on a 45x44 tile was the
109
- smallest of three weak signals doing the most work, so it grows with the row. */
109
+ smallest of three weak signals doing the most work, so it grows with the row.
110
+ The current glyph's own ink is nav.css's rule now, not a copy here: since #429
111
+ EVERY rail reads it that way, so the rail's copy was the same sentence in a
112
+ second file. What is left here is the rail's own resting step. */
110
113
  /* Re-tuned at #295, which took the light rail down a step with the page. Below 720px
111
114
  the glyph is the whole row, so it is held at text grade rather than at 1.4.11's bar —
112
- stories/apps/shell-states.test.js:1456 `a resting glyph is dimmer but still legible`. */
115
+ stories/apps/shell-states.test.js:1491 `a resting glyph is dimmer but still legible`. */
113
116
  .ui-app__rail .ui-nav__ic svg { opacity: 0.66; }
114
- .ui-app__rail .ui-nav__item.is-active .ui-nav__ic svg,
115
- .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 */
116
117
  .ui-app__rail .ui-nav__item.is-active::before { width: 3px; height: 62%; }
117
118
 
118
119
  /* A destructive row of the rail is a navigation row and rests like one; the danger
@@ -224,12 +225,27 @@
224
225
  .ui-app .ui-app__fold { color: var(--text); }
225
226
  .ui-app .ui-app__fold:hover,
226
227
  .ui-app .ui-app__fold:focus-visible { color: var(--strong); }
227
- /* The width's own clock, not the words', so the mark and the closing edge arrive
228
- together; no `!important`, so the reduced-motion net still takes it. 6 is not a
229
- number: shell.js draws the frame 3..21 and the seam at 9, and 6 mirrors it about
230
- the centre. why: docs/specification.md#the-page-shell */
231
- .ui-app__fold .ui-app__fold-seam { transition: transform var(--dur-med) var(--ease); }
232
- .ui-app.is-collapsed .ui-app__fold .ui-app__fold-seam { transform: translateX(6px); }
228
+ /* The seam holds at 9 at both widths, because it is the rail: the compartment it
229
+ cuts off is the narrow one, on the side the rail is on. What travels is the
230
+ chevron, and it says what the press will do rather than repeating the state —
231
+ the same thing the control's own accessible name says. shell.js draws it at
232
+ 13..16, Lucide `panel-left-close`; mirrored about 15, the centre of the
233
+ compartment it stands in, it lands at 14..17, which is Lucide
234
+ `panel-left-open`. So the two states are two Lucide glyphs and the CSS between
235
+ them is one reflection, not a hand-drawn position. `transform-origin` is
236
+ written out because a scale, unlike the translate this replaces, is read from
237
+ wherever the origin is — left to the initial 0 0 it would fold the mark
238
+ through the viewBox's left edge.
239
+
240
+ The width's own clock, not the words', so the mark and the closing edge arrive
241
+ together; no `!important`, so the reduced-motion net still takes it.
242
+ why: docs/specification.md#the-page-shell */
243
+ .ui-app__fold .ui-app__fold-arrow {
244
+ transform-box: view-box;
245
+ transform-origin: 15px 12px;
246
+ transition: transform var(--dur-med) var(--ease);
247
+ }
248
+ .ui-app.is-collapsed .ui-app__fold .ui-app__fold-arrow { transform: scaleX(-1); }
233
249
 
234
250
  /* -- The collapsed rail --------------------------------------------------- */
235
251
  /* Each rule here has its twin in the 720px block below; nothing lays a row out
@@ -257,6 +273,16 @@
257
273
  :where(.ui-app:not(.ui-app--topbar).is-collapsed) .ui-app__brand { opacity: 0; visibility: hidden; pointer-events: none; }
258
274
  :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__sub { margin-inline-start: 0; padding-inline-start: 0; border-inline-start-width: 0; }
259
275
  :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item:not(:has(.ui-nav__ic))::after { opacity: 0.62; }
276
+ /* The current row is ONE plate on the glyph column. Same rules as nav.css writes
277
+ for a standalone folded rail, and for the same reasons: the bar stands in the
278
+ row's own left padding, which on the strip is the rail's edge; the hairline is
279
+ drawn on a row that keeps the open column, so its right edge is off the strip;
280
+ and --ui-nav-strip is twice the glyph's centre, so a box that wide at the row's
281
+ edge is centred on the glyph. why: docs/specification.md#the-page-shell */
282
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item.is-active { isolation: isolate; background: none; }
283
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item.is-active:not(:focus-visible) { box-shadow: none; }
284
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item.is-active::before { left: 0; width: var(--ui-nav-strip); height: 100%; border-radius: 10px; background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ z-index: -1; }
285
+ :where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item:hover:not(:focus-visible) { outline: none; }
260
286
  /* The label IS the chip. `fixed` is the fallback placement, out of the rail's own
261
287
  clip; the @supports branch below pins it to the row and is the only one that
262
288
  survives a scroll. The toggle's selector is unscoped, because it is wordless at
@@ -452,6 +478,10 @@
452
478
  .ui-app__brand span { opacity: 0; pointer-events: none; }
453
479
  .ui-app__rail .ui-nav__sub { margin-inline-start: 0; padding-inline-start: 0; border-inline-start-width: 0; }
454
480
  .ui-app__rail .ui-nav__item:not(:has(.ui-nav__ic))::after { opacity: 0.62; }
481
+ .ui-app__rail .ui-nav__item.is-active { isolation: isolate; background: none; }
482
+ .ui-app__rail .ui-nav__item.is-active:not(:focus-visible) { box-shadow: none; }
483
+ .ui-app__rail .ui-nav__item.is-active::before { left: 0; width: var(--ui-nav-strip); height: 100%; border-radius: 10px; background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ z-index: -1; }
484
+ .ui-app__rail .ui-nav__item:hover:not(:focus-visible) { outline: none; }
455
485
  /* The one line of this block that is deliberately NOT the reader's fold: a finger
456
486
  is the only pointer this width has, so a row and the account block are held to
457
487
  44px. Neither floor can spread to the press, where growing the box is the one
@@ -93,8 +93,12 @@
93
93
 
94
94
  .ui-nav__ic { display: inline-flex; flex: none; }
95
95
  .ui-nav__ic svg { width: 17px; height: 17px; stroke: currentColor; fill: none; stroke-width: 2.2; opacity: 0.72; }
96
+ /* The glyph brightens with its row and takes the row's ink, not the accent. One
97
+ accent signal per row: that is the marker above, which is the only thing on a
98
+ rail that means "here". A glyph in --accent beside it is the same sentence
99
+ said twice, and beside an accent counter it was said three times. See #429. */
96
100
  .ui-nav__item.is-active .ui-nav__ic svg,
97
- .ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: var(--accent); } /* motion: still — the glyph brightens with its row, whose own highlight already transitions */
101
+ .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 */
98
102
  .ui-nav__label {
99
103
  flex: 1 1 auto; min-width: 0;
100
104
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
@@ -146,16 +150,14 @@
146
150
  .ui-nav__badge.is-live { background: var(--chip-success-fill); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--chip-success-ink); }
147
151
  .ui-nav__badge.is-danger { background: var(--glow-pink); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ color: var(--pink); }
148
152
  .ui-nav__item.is-active .ui-nav__badge.is-neutral { background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--strong); }
149
- /* Same shape, one row up: on an active row the accent counter takes the row's
150
- OWN surface instead of laying its wash over it. An active row is already
151
- painted --surface, so an opaque counter avoids stacking tinted fills. Every
152
- figure here is measured against the tokens this kit now ships. The accent on
153
- that pair falls under WCAG AA in four of the eight theme x accent cells —
154
- 4.08 in dark Nebula, where the flat surface reads 4.92 — and no alpha reaches
155
- it, the largest that clears being 0.06 in dark Nebula and 0.02 in light
156
- Emerald. So the accent wash is painted on a base surface, never a raised one.
157
- See #157 and docs/specification.md#colour-and-contrast. */
158
- .ui-nav__item.is-active .ui-nav__badge.is-accent { background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--accent); }
153
+ /* Same shape, one row up: on the current row the accent counter drops both the
154
+ wash and the accent and reads as the neutral one beside it. The row already
155
+ carries the one accent signal it is allowed — the marker — and the count is a
156
+ number, not a second answer to "where am I". It also retires the pair #157
157
+ recorded: accent ink on --surface fell under WCAG AA in four of the eight
158
+ theme x accent cells (4.08 in dark Nebula), and no alpha cleared it. Nothing
159
+ now paints that pair. See #429 and docs/specification.md#colour-and-contrast. */
160
+ .ui-nav__item.is-active .ui-nav__badge.is-accent { background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--strong); }
159
161
 
160
162
  /* Collapsible group */
161
163
  .ui-nav__group { display: flex; flex-direction: column; }
@@ -248,6 +250,43 @@
248
250
  }
249
251
  .ui-nav--side.is-collapsed .ui-nav__item:not(:has(.ui-nav__ic))::after { opacity: 0.62; }
250
252
 
253
+ /* The current row on a folded rail: ONE plate on the glyph column, and nothing
254
+ else. The open rail's marker is a 3px bar standing in the row's own left
255
+ padding; on the strip that padding is the rail's edge, so the bar reads as a
256
+ tick on the rail rather than a mark on a row — and a nested row puts it
257
+ --space-3 further out again, outside the rail altogether. The hairline cannot
258
+ close either: a row keeps the open column and the rail clips it, so its right
259
+ edge is drawn off the strip.
260
+
261
+ The plate is the strip. --ui-nav-strip is twice a row's own glyph centre, so a
262
+ box that wide standing at the row's edge is centred on the glyph, and the mark
263
+ lands where the reader is already looking. --surface-3 is the kit's quiet
264
+ non-text fill — a step off the rail in both themes, carrying a glyph and never
265
+ a word. The references mark current the same way and with no hue at all:
266
+ Cloudflare's docs rail paints a flat plate, and lessly-ui's rail is "a plate and
267
+ the weight step, no hue". Here the glyph's own step from 0.66 to full ink is the
268
+ second half of that pair. See #429.
269
+
270
+ `isolation` is what puts the plate behind the glyph: a positioned ::before paints
271
+ over its row's content, and a negative z-index with no stacking context of its
272
+ own would drop it behind the rail instead. */
273
+ .ui-nav--side.is-collapsed .ui-nav__item.is-active { isolation: isolate; background: none; }
274
+ /* At rest only — box-shadow carries the hairline AND the focus ring (WCAG 2.4.7). */
275
+ .ui-nav--side.is-collapsed .ui-nav__item.is-active:not(:focus-visible) { box-shadow: none; }
276
+ .ui-nav--side.is-collapsed .ui-nav__item.is-active::before {
277
+ left: 0;
278
+ width: var(--ui-nav-strip);
279
+ height: 100%;
280
+ border-radius: 10px;
281
+ background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
282
+ z-index: -1;
283
+ }
284
+ /* Hover's hairline is an outline on a row that keeps the open column, so on the
285
+ strip it too is drawn with no right edge. A folded rail answers a pointer with
286
+ the row's own ink and the name beside it, which is the whole readout at this
287
+ width. At rest only, so the focus ring is left standing. */
288
+ .ui-nav--side.is-collapsed .ui-nav__item:hover:not(:focus-visible) { outline: none; }
289
+
251
290
  /* -- Horizontal tabs ----------------------------------------------------- */
252
291
  .ui-nav--tabs {
253
292
  display: flex;