@apliteni/apliteni-ui 0.91.0 → 0.93.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 (55) hide show
  1. package/README.md +5 -7
  2. package/package.json +1 -1
  3. package/react/README.md +2 -2
  4. package/react/dist/index.d.ts +3 -3
  5. package/react/dist/index.js +79 -111
  6. package/src/assets/icons.js +1 -1
  7. package/src/components/account-nav.js +2 -2
  8. package/src/components/back.js +2 -2
  9. package/src/components/command-palette.js +2 -2
  10. package/src/components/drawer.js +1 -1
  11. package/src/components/dropdown.js +12 -12
  12. package/src/components/filter-bar.js +2 -2
  13. package/src/components/footer.js +1 -1
  14. package/src/components/index.js +1 -1
  15. package/src/components/loading.js +1 -1
  16. package/src/components/shell.js +19 -19
  17. package/src/components/stat.js +3 -3
  18. package/src/components/success.js +3 -3
  19. package/src/components/tooltip.js +1 -1
  20. package/src/components/topbar.js +1 -1
  21. package/src/html.js +1 -1
  22. package/src/index.css +1 -1
  23. package/src/index.d.ts +1 -1
  24. package/src/logic/accents.js +1 -1
  25. package/src/logic/filter-bar.js +4 -4
  26. package/src/styles/back.css +3 -3
  27. package/src/styles/base.css +3 -3
  28. package/src/styles/button.css +3 -3
  29. package/src/styles/callout.css +2 -2
  30. package/src/styles/card.css +8 -8
  31. package/src/styles/command-palette.css +14 -14
  32. package/src/styles/confirm.css +7 -7
  33. package/src/styles/drawer.css +8 -8
  34. package/src/styles/dropdown.css +21 -21
  35. package/src/styles/feedback.css +2 -2
  36. package/src/styles/field-zoom.css +1 -1
  37. package/src/styles/file-drop.css +1 -1
  38. package/src/styles/filter-bar.css +6 -6
  39. package/src/styles/footer.css +1 -1
  40. package/src/styles/input.css +6 -6
  41. package/src/styles/layout.css +32 -32
  42. package/src/styles/loading.css +1 -1
  43. package/src/styles/motion.css +1 -1
  44. package/src/styles/nav.css +4 -4
  45. package/src/styles/pagination.css +2 -2
  46. package/src/styles/reduced-motion.css +2 -2
  47. package/src/styles/segmented.css +3 -3
  48. package/src/styles/stat.css +3 -3
  49. package/src/styles/success.css +1 -1
  50. package/src/styles/table.css +6 -6
  51. package/src/styles/tap-zone.css +1 -1
  52. package/src/styles/tooltip.css +3 -3
  53. package/src/styles/topbar.css +8 -8
  54. package/src/tokens/accents.css +1 -1
  55. package/src/tokens/tokens.css +25 -25
@@ -80,7 +80,7 @@ function ddBody({ items, sections }, listbox, sx) {
80
80
  // The match is a substring of the label, anywhere in it, ignoring case and
81
81
  // accents; rows keep their order. The factory and the wiring both ask
82
82
  // dropdownMatch(), so a preset query and a typed one hide the same rows.
83
- // why: docs/specification.md#a-dropdown-with-a-search-field
83
+ // why: docs/components.md#a-dropdown-with-a-search-field
84
84
  // NFD takes the mark off é or ö; ł, ø, đ and the rest are letters of their own
85
85
  // with nothing to take off, so they are mapped by hand.
86
86
  const ddIsRow = (it) => it && it !== '---' && !it.separator;
@@ -207,7 +207,7 @@ export function dropdown({
207
207
 
208
208
  // The block the sheet bleeds to the panel's bottom edge. It sits OUTSIDE the
209
209
  // unwrapped `footer`, because the block that bleeds is the one that has to
210
- // touch the edge. why: docs/specification.md#the-dropdown-panel
210
+ // touch the edge. why: docs/components.md#the-dropdown-panel
211
211
  const footBlock = foot ? `<div class="ui-dropdown__foot">${foot}</div>` : '';
212
212
 
213
213
  const ddAttrs = 'data-dropdown'
@@ -236,7 +236,7 @@ export function dropdown({
236
236
  // Every wired container, so the close handlers can reach a dropdown wherever it
237
237
  // was drawn: `document.querySelectorAll` enters no shadow root and sees no other
238
238
  // document. A Set and not a WeakSet, because this has to be walked.
239
- // why: docs/specification.md#the-dropdown-panel
239
+ // why: docs/components.md#the-dropdown-panel
240
240
  const _ddAll = new Set();
241
241
  // One pair of close handlers per document that holds a dropdown, the same way
242
242
  // wireShell() listens once per document it is handed.
@@ -276,7 +276,7 @@ function ddItemsOf(dd) {
276
276
  }
277
277
 
278
278
  // ---- Search wiring ---------------------------------------------------------
279
- // why: docs/specification.md#a-dropdown-with-a-search-field
279
+ // why: docs/components.md#a-dropdown-with-a-search-field
280
280
  const ddSearchOf = (dd) => ddPanelOf(dd)?.querySelector('[data-dd-search]') || null;
281
281
  const ddActiveOf = (dd) => ddPanelOf(dd)?.querySelector('[data-dd-item].is-active') || null;
282
282
  const ddComposing = (e) => e.isComposing || e.keyCode === 229;
@@ -342,7 +342,7 @@ export const DD_MENU_FLOOR = 240;
342
342
  * A panel is anchored at one edge of its dropdown — its inline end when it
343
343
  * carries `is-end` — so room is measured from that edge and the slide goes the
344
344
  * other way. Measuring an end-anchored panel forwards put one off the page.
345
- * why: docs/specification.md#a-filter-row-holds-its-panels
345
+ * why: docs/components.md#a-filter-row-holds-its-panels
346
346
  *
347
347
  * @param {Element} dd a `.ui-dropdown` that may be inside a filter row
348
348
  * @param {number} [floor] the width to reach for
@@ -405,7 +405,7 @@ function ddFitFilterPanel(dd, panel) {
405
405
  * the old width too, and inline beats the sheet, so it is read again off the
406
406
  * newly bounded box; only when the fit moved, because re-reading it is itself a
407
407
  * write.
408
- * why: docs/specification.md#a-filter-row-holds-its-panels */
408
+ * why: docs/components.md#a-filter-row-holds-its-panels */
409
409
  function ddRefitFilterPanel(dd, panel) {
410
410
  const fit = panel?.style ? filterPanelFit(dd) : null;
411
411
  if (!fit || !ddWriteFit(panel, fit)) return;
@@ -424,7 +424,7 @@ function ddRowOf(dd) {
424
424
  * `position: absolute`, so nothing in it can move a chip along its row. Any
425
425
  * panel, not only the one being fitted: two open menus in one row would
426
426
  * otherwise answer each other a mutation at a time.
427
- * why: docs/specification.md#a-filter-row-holds-its-panels */
427
+ * why: docs/components.md#a-filter-row-holds-its-panels */
428
428
  function ddOurs(node) {
429
429
  const el = node?.nodeType === 1 ? node : node?.parentElement;
430
430
  return !!el?.closest?.('[data-dropdown-panel]');
@@ -443,7 +443,7 @@ function ddOurs(node) {
443
443
  * is laid out.
444
444
  *
445
445
  * Why those two cover the arithmetic, and the one case they do not:
446
- * why: docs/specification.md#a-filter-row-holds-its-panels
446
+ * why: docs/components.md#a-filter-row-holds-its-panels
447
447
  */
448
448
  function ddWatchRow(dd, panel) {
449
449
  ddUnwatchRow(dd);
@@ -664,7 +664,7 @@ function selectOption(dd, item) {
664
664
  // Click-outside, Escape and the repositioning sweep, registered once per document
665
665
  // that holds a dropdown — the same shape wireShell()'s listen() has, and for the
666
666
  // same reason: a frame is its own document and a listener on the page's never
667
- // fires there. why: docs/specification.md#the-dropdown-panel
667
+ // fires there. why: docs/components.md#the-dropdown-panel
668
668
  function ddListen(doc) {
669
669
  if (!doc || _ddWiredDocs.has(doc)) return;
670
670
  _ddWiredDocs.add(doc);
@@ -683,7 +683,7 @@ function ddListen(doc) {
683
683
  };
684
684
  /* The fallback re-fit, for a view with no ResizeObserver. Not taken as well where
685
685
  * ddWatchRow() is watching: a `resize` event is the worse measurement of the two.
686
- * why: docs/specification.md#a-filter-row-holds-its-panels */
686
+ * why: docs/components.md#a-filter-row-holds-its-panels */
687
687
  const refit = () => {
688
688
  for (const dd of ddLive()) {
689
689
  if (dd.classList.contains('open') && !dd.__ddPanel && !dd.__ddRowFit) {
@@ -712,11 +712,11 @@ export function wireDropdown(root = document) {
712
712
  // Portal: lift the panel onto <body>. An ancestor whose overflow is not
713
713
  // `visible` clips it on both axes, and one that is `position: sticky` opens
714
714
  // a stacking context whatever z-index the panel carries — the app rail is
715
- // both at once. why: docs/specification.md#the-dropdown-panel
715
+ // both at once. why: docs/components.md#the-dropdown-panel
716
716
  if (panel && dd.hasAttribute('data-dropdown-portal')) {
717
717
  // The tree the trigger is in, not the page's: a panel lifted out of a
718
718
  // frame or a shadow root into the top document leaves its stylesheet and
719
- // its close handler behind. why: docs/specification.md#the-dropdown-panel
719
+ // its close handler behind. why: docs/components.md#the-dropdown-panel
720
720
  const host = ddHostOf(dd);
721
721
  sweepOrphanPanels(host);
722
722
  panel.setAttribute('data-dropdown-portal', '');
@@ -9,7 +9,7 @@ export function filterBar({ filters = [], label = 'Filters', clearLabel = 'Clear
9
9
  + `<legend class="ui-filter-bar__legend">${esc(filter.label)}</legend>`
10
10
  + dropdown({ items: filterChipItems(filter), variant: 'select',
11
11
  // The chip prints one line; the field's name reaches a reader through the
12
- // trigger's name and the chip's own legend. why: docs/specification.md#dense-financial-tables
12
+ // trigger's name and the chip's own legend. why: docs/components.md#dense-financial-tables
13
13
  triggerContent: `<span class="${filterChipUnset(filter) ? 'ui-dropdown__value is-placeholder' : 'ui-dropdown__value'}">${esc(filterChipText(filter))}</span>`,
14
14
  ariaLabel: filterChipName(filter), open: !!filter.open && !disabled && !busy && !filter.disabled })
15
15
  + `<button type="button" class="ui-filter-bar__remove" data-filter-remove aria-label="${esc(`Remove ${filter.label} filter`)}">×</button></fieldset>`).join('')
@@ -19,7 +19,7 @@ export function filterBar({ filters = [], label = 'Filters', clearLabel = 'Clear
19
19
  // rule — a control with no action to offer is not shown. It returns with
20
20
  // the first chip, in the bordered skin rather than the ghost one, because a
21
21
  // live action beside two chips has to read as live.
22
- // why: docs/specification.md#a-filter-row-holds-its-panels
22
+ // why: docs/components.md#a-filter-row-holds-its-panels
23
23
  + (filters.length
24
24
  ? `<span data-filter-clear>${button({ label: clearLabel, size: 'sm' })}</span>`
25
25
  : '')
@@ -36,7 +36,7 @@ function linkList(links = []) {
36
36
  // matter, and h2 is the one rank that cannot skip whatever heading came before
37
37
  // it. It was an h4 — the only h4 the kit drew — which read h2 → h4 on the
38
38
  // landing page and left a rank a reader hears missing. The look is the class's,
39
- // not the tag's. why: docs/specification.md#the-page
39
+ // not the tag's. why: guidelines/the-page.md#the-page
40
40
  function column({ title, links = [] } = {}) {
41
41
  return `<div class="ui-footer__col">` +
42
42
  (title ? `<h2 class="ui-footer__col-title">${esc(title)}</h2>` : '') +
@@ -61,7 +61,7 @@ export function statusDot(live = false) {
61
61
 
62
62
  // ---- Card ----------------------------------------------------------------
63
63
  // The title is a heading one level under the page's h1 unless `level` says
64
- // otherwise. why: docs/specification.md#labels-and-titles
64
+ // otherwise. why: docs/foundations.md#labels-and-titles
65
65
  export function card({ title, sub, body = '', variant, pad, icon: ic, level = 2 } = {}) {
66
66
  const cls = cx('ui-card', variant && `ui-card--${variant}`, pad && `ui-card--pad-${pad}`);
67
67
  const h = [2, 3, 4, 5, 6].includes(Number(level)) ? `h${Number(level)}` : 'h2';
@@ -5,7 +5,7 @@
5
5
  // Three things land in that body: skeleton() while it fetches, your markup once
6
6
  // the rows arrive, deniedState() when the answer came back 403.
7
7
  //
8
- // why: docs/specification.md#pending-and-denied-states
8
+ // why: docs/components.md#pending-and-denied-states
9
9
  import { icon } from '../assets/icons.js';
10
10
  import { button, esc } from './index.js';
11
11
  import { playEntrance } from '../motion.js';
@@ -1,7 +1,7 @@
1
1
  // The kit's one page shell: a full-height rail beside one <main>, in two layouts.
2
2
  // `topbar` switches the product band on; it is off unless the caller hands one over.
3
3
  // wireShell() once after mounting wires the fold, the nav's groups and the reader's menu.
4
- // why: docs/specification.md#the-page-shell
4
+ // why: docs/components.md#the-page-shell
5
5
  import { topbar as productTopbar } from './topbar.js';
6
6
  import { esc, icon } from './index.js';
7
7
  import { sidebarNav, breadcrumbs, wireNav } from './nav.js';
@@ -12,7 +12,7 @@ import { ACCOUNT_NAV, toMenuTuple, initials } from './account-nav.js';
12
12
  import { paletteHotkey } from './command-palette.js';
13
13
  import { safeUrl } from '../html.js';
14
14
  // The one account navigation definition lives in account-nav.js because topbar.js needs
15
- // it too; re-exported here so the name docs/library.md publishes keeps working.
15
+ // it too; re-exported here so the name docs/components.md publishes keeps working.
16
16
  export { ACCOUNT_NAV };
17
17
 
18
18
  const str = (v) => (v == null ? '' : String(v));
@@ -44,7 +44,7 @@ const toCrumbs = (crumbs) => (Array.isArray(crumbs) ? crumbs : [])
44
44
 
45
45
  // A back link replaces the trail rather than joining it: the two would name the same parent
46
46
  // twice. Anything but a record is no back link; its fields go through as given, so a back
47
- // backLink() refuses leaves the trail standing. why: docs/specification.md#the-back-link
47
+ // backLink() refuses leaves the trail standing. why: docs/components.md#the-back-link
48
48
  const toBack = (b) => (isRecord(b) && !Array.isArray(b) ? { href: b.href, label: b.label } : null);
49
49
 
50
50
  // The reader, as two strings. railUser() and initials() both read them, and an
@@ -83,15 +83,15 @@ const toTopbar = (t) => {
83
83
  //
84
84
  // Each is a name the kit knows or the default — read strictly, and not through
85
85
  // String(), which turned `['topbar']` into a layout and drew half a page.
86
- // why: docs/specification.md#the-page-shell
86
+ // why: docs/components.md#the-page-shell
87
87
  const toLayout = (v) => (v === 'topbar' ? 'topbar' : 'rail');
88
88
 
89
- // why: docs/specification.md#widths
89
+ // why: docs/foundations.md#widths
90
90
  const toWidth = (v) => (v === 'wide' ? 'wide' : 'centered');
91
91
 
92
92
  // `search: 'palette-id'`, or the same id in `{ palette }` with a placeholder beside
93
93
  // it. No palette to open, no field — the argument `signOutHref` takes.
94
- // why: docs/specification.md#the-page-shell
94
+ // why: docs/components.md#the-page-shell
95
95
  const toSearch = (v) => {
96
96
  const given = typeof v === 'string' ? { palette: v } : (isRecord(v) && !Array.isArray(v) ? v : null);
97
97
  const palette = given && typeof given.palette === 'string' ? given.palette.trim() : '';
@@ -104,7 +104,7 @@ const toSearch = (v) => {
104
104
  // attribute, falling through to --measure. It must REMOVE the property rather than pass
105
105
  // a bad value on: a custom property accepts any token stream, so garbage is a valid
106
106
  // declaration that drops the column to `none`, the full track.
107
- // why: docs/specification.md#widths
107
+ // why: docs/foundations.md#widths
108
108
  const LENGTH = /^(?:\d+|\d*\.\d+)(?:px|rem|em|ch|%|vw)$/;
109
109
  const mainMax = (v) => {
110
110
  const s = str(v).trim();
@@ -114,7 +114,7 @@ const mainMax = (v) => {
114
114
  // ---- the fold, and where the reader's choice is kept ---------------------
115
115
  //
116
116
  // A cookie rather than localStorage, because a server can read one and paint the
117
- // rail at the width the reader left it. why: docs/specification.md#the-page-shell
117
+ // rail at the width the reader left it. why: docs/components.md#the-page-shell
118
118
  export const RAIL_COOKIE = 'apliteni-ui-rail';
119
119
  const RAIL_MAX_AGE = 60 * 60 * 24 * 365;
120
120
  const RAIL_VALUE = new RegExp(`(?:^|;\\s*)${RAIL_COOKIE}=(collapsed|expanded)(?:;|$)`);
@@ -142,7 +142,7 @@ const railName = (collapsed) => (collapsed ? 'Expand sidebar' : 'Collapse sideba
142
142
  // hand. The seam does not move: it IS the rail, and the compartment it cuts off
143
143
  // stays on the side the rail is on. Only the nodes are written here — a child a
144
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
145
+ // than a copy of it. why: docs/components.md#the-page-shell
146
146
  const MARK = '<rect x="3" y="3" width="18" height="18" rx="2"/>'
147
147
  + '<path class="ui-app__fold-seam" d="M9 3v18"/>'
148
148
  + '<path class="ui-app__fold-arrow" d="m16 15-3-3 3-3"/>';
@@ -151,7 +151,7 @@ const railMark = () => icon('').replace('></svg>', `>${MARK}</svg>`);
151
151
  // The rail's own skin, outside the <nav>: folding the rail is not a place to go.
152
152
  // It stands at the far end of the head band's brand row, and its name is written
153
153
  // out rather than put in a tooltip, because the name IS the chip layout.css lands
154
- // beside the glyph — at both widths. why: docs/specification.md#the-page-shell
154
+ // beside the glyph — at both widths. why: docs/components.md#the-page-shell
155
155
  const railToggle = (collapsed) =>
156
156
  `<div class="ui-app__fold-row">`
157
157
  + `<button type="button" class="ui-nav__item ui-app__fold" data-rail-toggle`
@@ -165,7 +165,7 @@ const SHAPES = {
165
165
  nav: toItems, crumbs: toCrumbs, back: toBack, account: toReader, maxWidth: mainMax, topbar: toTopbar,
166
166
  layout: toLayout, width: toWidth, search: toSearch,
167
167
  // Drawn by default; `collapsible: false` is the way out, for a page that will
168
- // never call wireShell(). why: docs/specification.md#the-page-shell
168
+ // never call wireShell(). why: docs/components.md#the-page-shell
169
169
  collapsible: (v) => v !== false,
170
170
  // A boolean is the caller's answer. Anything else leaves it to the reader.
171
171
  collapsed: (v) => (typeof v === 'boolean' ? v : null),
@@ -186,7 +186,7 @@ function settle(options) {
186
186
  // The face of the reader block: the initials, and the two lines beside them that
187
187
  // the fold takes away. `named` is the accessible name when nothing else carries
188
188
  // one; under the menu trigger it is null, because the button is named by the words
189
- // inside it. why: docs/specification.md#the-page-shell
189
+ // inside it. why: docs/components.md#the-page-shell
190
190
  const readerFace = (name, email, named, markOnly = false) =>
191
191
  `<span class="ui-app__av"${named ? ` role="img" aria-label="Signed in as ${esc(named)}"` : ' aria-hidden="true"'}>`
192
192
  + `${esc(initials(name, email))}</span>`
@@ -235,7 +235,7 @@ function readerBlock({ name, email }, signOutHref, { band = false } = {}) {
235
235
 
236
236
  // A field to look at, a button to press. `data-cmdk-open` and the cap's class are the
237
237
  // palette's own, and the cap is NOT aria-hidden: it is half the button's name.
238
- // why: docs/specification.md#the-page-shell
238
+ // why: docs/components.md#the-page-shell
239
239
  const searchField = ({ palette, placeholder }) =>
240
240
  `<button type="button" class="ui-app__search" data-cmdk-open="${esc(palette)}" aria-haspopup="dialog">`
241
241
  + `<span class="ui-app__search-ic" aria-hidden="true">${icon('search')}</span>`
@@ -244,7 +244,7 @@ const searchField = ({ palette, placeholder }) =>
244
244
  + `</button>`;
245
245
 
246
246
  // The band: search at its start, the reader at its end, beside the rail rather than
247
- // across the top of both. why: docs/specification.md#the-page-shell
247
+ // across the top of both. why: docs/components.md#the-page-shell
248
248
  const shellBar = (search, account, signOutHref) =>
249
249
  `<header class="ui-app__bar">`
250
250
  + (search ? searchField(search) : '')
@@ -292,13 +292,13 @@ export function appShell(options = {}) {
292
292
  ariaLabel: navLabel,
293
293
  });
294
294
  // One band over a page, never two: the banded layout draws its own, so the
295
- // compatibility bag is not drawn beside it. why: docs/specification.md#the-page-shell
295
+ // compatibility bag is not drawn beside it. why: docs/components.md#the-page-shell
296
296
  const compat = banded ? null : topbar;
297
297
  // The topbar already says the product word, so the rail head steps aside when there is
298
298
  // one. The word is the link's only text and the narrow rail folds it out of view, so
299
299
  // the name is written out — the mark itself is aria-hidden. The banded layout's own
300
300
  // bar says no word, so there the rail keeps the lockup: that layout's head band IS
301
- // the product's mark. why: docs/specification.md#the-page-shell
301
+ // the product's mark. why: docs/components.md#the-page-shell
302
302
  const brand = compat ? '' : `<a class="ui-app__brand" href="${esc(safeUrl(brandHref))}" aria-label="${esc(word)}">`
303
303
  + `${prism(`appb-${++_shellUid}`, 24)}<span>${esc(word)}</span></a>`;
304
304
  // A <div>, not an <aside>: <aside> is the `complementary` landmark, and this holds the
@@ -319,7 +319,7 @@ export function appShell(options = {}) {
319
319
  ? `<div class="ui-app__head">${brand}${inHead}</div>`
320
320
  : '';
321
321
  // The rail's foot: the reader, or the control that took their place. One block
322
- // either way. why: docs/specification.md#the-page-shell
322
+ // either way. why: docs/components.md#the-page-shell
323
323
  const foot = banded
324
324
  ? (fold ? `<div class="ui-app__foot">${fold}</div>` : '')
325
325
  : readerBlock(account, signOutHref);
@@ -357,7 +357,7 @@ const _unpersisted = new WeakSet();
357
357
  // path is written once — the listener, the reflector and wireShell() all have to mean
358
358
  // the same control. A descendant and not a child, because the two layouts stand the
359
359
  // row in two places: the head band, or the rail's foot.
360
- // why: docs/specification.md#the-page-shell
360
+ // why: docs/components.md#the-page-shell
361
361
  const FOLD_PATH = '.ui-app__fold-row > [data-rail-toggle]';
362
362
  const RAIL_FOLD = `.ui-app__rail ${FOLD_PATH}`;
363
363
 
@@ -413,7 +413,7 @@ export function wireShell(root = document, { persist } = {}) {
413
413
  // other. Idempotent, and a shell with no account draws none to find.
414
414
  wireDropdown(root);
415
415
  // A server cannot know which key the reader holds, so the browser corrects it here —
416
- // off the ROOT's window, not the global one. why: docs/specification.md#the-page-shell
416
+ // off the ROOT's window, not the global one. why: docs/components.md#the-page-shell
417
417
  const here = root.nodeType === 9 ? root : root.ownerDocument;
418
418
  const view = here && here.defaultView;
419
419
  const key = paletteHotkey(view && view.navigator ? view.navigator.platform : '');
@@ -1,5 +1,5 @@
1
1
  // Stat band — a row of key figures, as an HTML string.
2
- // why: docs/specification.md#stat-bands
2
+ // why: docs/components.md#stat-bands
3
3
  //
4
4
  // A band is a <dl>: each figure is a <div> holding its label as a <dt> and its
5
5
  // value, change and trend as <dd>s, which is the grouping HTML allows inside a
@@ -24,7 +24,7 @@ const GLYPH = { up: 'arrowUp', down: 'arrowDown', flat: 'minus' };
24
24
  const hasChange = (delta) => delta && delta.value != null && delta.value !== '';
25
25
 
26
26
  // One row and one line: the caption, the change, then what it is measured
27
- // against. Nothing to compare draws no row. why: docs/specification.md#stat-bands
27
+ // against. Nothing to compare draws no row. why: docs/components.md#stat-bands
28
28
  const contextRow = (caption, delta, basisId) => {
29
29
  if (!hasChange(delta)) return caption ? `<dd class="ui-stat__caption">${esc(caption)}</dd>` : '';
30
30
  // The space is read where the CSS gap is only drawn: "of income+1.2 pts" else.
@@ -58,7 +58,7 @@ const figure = ({ label = '', value = '', caption = '', delta, trend = '' }, til
58
58
  // `basis` is the band's caption: what every change is measured against, said
59
59
  // once before the list the way a table's <caption> is, in every layout — under
60
60
  // a row of tiles it would read as a note on the last card.
61
- // why: docs/specification.md#stat-bands
61
+ // why: docs/components.md#stat-bands
62
62
  export function statBand({ stats = [], variant = 'tiles', basis = '', label, id } = {}) {
63
63
  const v = STAT_VARIANTS.includes(variant) ? variant : 'tiles';
64
64
  const base = id ? esc(id) : `ui-stats-${++seq}`;
@@ -10,7 +10,7 @@ import { esc, button } from './index.js';
10
10
 
11
11
  // 'line' is the bare Lucide `check`, 'circled' is Lucide circle-check-big. Which
12
12
  // is the default, and why that departs from Guidelines / Iconography:
13
- // why: docs/specification.md#success-confirmations
13
+ // why: docs/components.md#success-confirmations
14
14
  // Each is written whole because the icon-sizing and glyph-stroke gates find their
15
15
  // subjects by scanning source for a class on an `<svg …>`: a tag split across
16
16
  // string pieces leaves both silent rather than failing.
@@ -66,7 +66,7 @@ function countdownEl({ seconds = 5, label = 'Redirecting' } = {}) {
66
66
  // Both carry one title and at most one short line under it. There is no eyebrow
67
67
  // tier: a confirmation stacking a label, a headline and a paragraph reads as
68
68
  // three competing voices for one outcome.
69
- // why: docs/specification.md#success-confirmations
69
+ // why: docs/components.md#success-confirmations
70
70
  export function success({
71
71
  layout = 'hero', // 'hero' | 'split' | 'compact'
72
72
  level, // heading level of the title; see the note below
@@ -88,7 +88,7 @@ export function success({
88
88
  // takes h2. It was an h3 either way, which left a page whose whole content is
89
89
  // a success() with no h1 at all — the fault Guidelines / The page names.
90
90
  // A caller who knows better passes `level`. The look is the class's.
91
- // why: docs/specification.md#the-page
91
+ // why: guidelines/the-page.md#the-page
92
92
  const rank = [1, 2, 3, 4, 5, 6].includes(Number(level)) ? Number(level) : (layout === 'compact' ? 2 : 1);
93
93
  const h = `h${rank}`;
94
94
 
@@ -11,7 +11,7 @@
11
11
  // A mark carries `data-tip-value`, with `data-tip-label` and `data-tip-detail`;
12
12
  // a `[data-tip-anchor]` inside it is where the readout opens. Under a finger
13
13
  // there is no hover, so a tap is the switch: see Pointer kinds below.
14
- // why: docs/specification.md#the-hover-readout
14
+ // why: docs/components.md#the-hover-readout
15
15
  import { esc } from './index.js';
16
16
 
17
17
  const cx = (...a) => a.filter(Boolean).join(' ');
@@ -30,7 +30,7 @@ export function deckTextSwitch(active = 'deck') {
30
30
  // bespoke .vsw/.vopt classes for a pixel-identical look, but emits the generic
31
31
  // [data-dropdown] hooks so there's ONE open/close/keyboard implementation.
32
32
  // `badge` is a tone key and the kit writes the word for it; the stylesheet used
33
- // to uppercase the key itself. why: docs/specification.md#labels-and-titles
33
+ // to uppercase the key itself. why: docs/foundations.md#labels-and-titles
34
34
  const VBADGE = { live: 'Live', archive: 'Archive' };
35
35
  export function versionSwitcher(versions = [], activeIdx = 0) {
36
36
  const cur = versions[activeIdx]?.label || '';
package/src/html.js CHANGED
@@ -5,7 +5,7 @@ export const esc = value => String(value == null ? '' : value).replace(/[&<>"]/g
5
5
  // Legacy topbar/brand slots accept already escaped HTML. Preserve their entities.
6
6
  export const trustedAttr = value => String(value ?? '').replace(/[<>"]/g, c => ENTITIES[c]);
7
7
 
8
- // why: docs/specification.md#vanilla-html-boundaries
8
+ // why: docs/components.md#escaping-and-url-slots
9
9
  export function safeUrl(value, fallback = '#') {
10
10
  const raw = String(value ?? '');
11
11
  const scheme = raw.replace(/[\t\n\r]/g, '').replace(/^[\u0000-\u0020]+/, '');
package/src/index.css CHANGED
@@ -9,7 +9,7 @@
9
9
  * (--font-sans, everything else). Weights 300-700 in both. Neither is
10
10
  * bundled; a family that never loads falls back to the system stack behind
11
11
  * it, in silence.
12
- * why: docs/specification.md#typefaces
12
+ * why: docs/foundations.md#typefaces
13
13
  * ========================================================================== */
14
14
  @import "./tokens/brand.generated.css";
15
15
  @import "./tokens/tokens.css";
package/src/index.d.ts CHANGED
@@ -41,7 +41,7 @@ export declare const DD_MENU_FLOOR: number;
41
41
  * the width it reached for, and whether it is anchored at its inline end.
42
42
  * `null` unless the dropdown is inside a `.ui-filter-bar__chip`: the slide is a
43
43
  * chip's offset along its row, so a panel anchored to the row itself is not this
44
- * function's subject and sizes itself. why: docs/library.md */
44
+ * function's subject and sizes itself. why: docs/components.md */
45
45
  export declare function filterPanelFit(
46
46
  dd: Element | null | undefined,
47
47
  floor?: number,
@@ -5,7 +5,7 @@
5
5
  // react/src/AccentPicker.tsx call accentSwatchStyle(); site/chrome.mjs and
6
6
  // site/index.html still hand-keep the same four gradients. Held by
7
7
  // stories/accent-swatch.test.js, which derives both sides out of src/tokens/.
8
- // why: docs/specification.md#react-accent-picker. See issues #190 and #429.
8
+ // why: docs/components.md#page-furniture. See issues #190 and #429.
9
9
  export const ACCENTS = ['default', 'phoenix', 'ocean', 'emerald'];
10
10
 
11
11
  // Per accent, the gradient's two stops — each the value src/tokens/ declares for
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * `value` is display text: it is the chip's only visible line, so a consumer
8
8
  * answers a change with text a reader can read, not a raw code.
9
- * why: docs/specification.md#dense-financial-tables
9
+ * why: docs/components.md#dense-financial-tables
10
10
  */
11
11
 
12
12
  const text = (v) => (v == null ? '' : String(v));
@@ -71,7 +71,7 @@ function stopsAround(host) {
71
71
  * order, else the nearest one behind it. A filter bar that has just dropped its
72
72
  * last control keeps no box the focus ring can sit on, so the focus has to
73
73
  * leave the bar rather than land on a line with no height.
74
- * why: docs/specification.md#a-filter-row-holds-its-panels */
74
+ * why: docs/components.md#a-filter-row-holds-its-panels */
75
75
  export function nextFocusStop(host) {
76
76
  return stopsAround(host)[0] || null;
77
77
  }
@@ -88,7 +88,7 @@ export function nextFocusStop(host) {
88
88
  * Being reachable is not the same as taking the focus: a control can be visible
89
89
  * and enabled and still refuse, so each candidate is asked and then checked, and
90
90
  * the next one tried.
91
- * why: docs/specification.md#a-filter-row-holds-its-panels */
91
+ * why: docs/components.md#a-filter-row-holds-its-panels */
92
92
  export function focusNextStop(host) {
93
93
  const doc = host && host.ownerDocument;
94
94
  if (!doc) return null;
@@ -114,7 +114,7 @@ export function focusNextStop(host) {
114
114
  * consumer wrote them left whatever they had marked standing — a menu checking
115
115
  * `All`, `aria-selected="true"` on it, under a chip printing `Asia`. The
116
116
  * consumer's own array and objects are not written to. #550
117
- * why: docs/specification.md#a-filter-row-holds-its-panels */
117
+ * why: docs/components.md#a-filter-row-holds-its-panels */
118
118
  export function filterChipItems(filter = {}) {
119
119
  const items = filter.items || [];
120
120
  const chosen = text(filter.value);
@@ -3,7 +3,7 @@
3
3
  * The way up from a page to the page it sits under. It stands above the page
4
4
  * title, in the slot a breadcrumb trail would take, and it is quiet at rest: it
5
5
  * is on every page below a list, and it is never what the page is for.
6
- * why: docs/specification.md#the-back-link
6
+ * why: docs/components.md#the-back-link
7
7
  * ========================================================================== */
8
8
 
9
9
  /* Its own line, and only as wide as its words, so the hover ground does not run
@@ -36,7 +36,7 @@
36
36
 
37
37
  /* 16px, with the stroke raised to match: the glyph set draws at 1.7 on a 24-unit
38
38
  box, which is 1.13 CSS px at this size and under the kit's 1.5 for a stroked
39
- mark. 2.4 paints 1.6. why: docs/specification.md#icons-and-glyphs */
39
+ mark. 2.4 paints 1.6. why: docs/foundations.md#icons-and-glyphs */
40
40
  .ui-back svg { width: 16px; height: 16px; flex: none; stroke-width: 2.4; }
41
41
 
42
42
  /* In the page shell it takes the trail's place, and the trail's distance from
@@ -44,7 +44,7 @@
44
44
  .ui-app__main > .ui-back { margin-bottom: var(--space-5); }
45
45
 
46
46
  /* The one part that can run long. It clips rather than wrapping, as .ui-nav__label
47
- does. why: docs/specification.md#the-back-link */
47
+ does. why: docs/components.md#the-back-link */
48
48
  .ui-back__label {
49
49
  min-width: 0;
50
50
  overflow: hidden;
@@ -43,7 +43,7 @@ a {
43
43
  * reader notices. A component that wants a heading tag set in the text face
44
44
  * says so on its own rule, which outranks this one at (0,1,0); .ui-card__title,
45
45
  * .ui-drawer__title and .ui-confirm__title are the three that do.
46
- * why: docs/specification.md#typefaces */
46
+ * why: docs/foundations.md#typefaces */
47
47
  h1, h2, h3, h4, h5, h6 {
48
48
  font-family: var(--font-display);
49
49
  }
@@ -52,7 +52,7 @@ h1, h2, h3, h4, h5, h6 {
52
52
  * chat feeds run at, 700 stops reading as emphasis and starts reading as a
53
53
  * filled-in shape. The semibold step is the emphasis; the bold step stays
54
54
  * available to anything that asks for it by name.
55
- * why: docs/specification.md#typefaces */
55
+ * why: docs/foundations.md#typefaces */
56
56
  b,
57
57
  strong {
58
58
  font-weight: var(--weight-semibold);
@@ -115,7 +115,7 @@ strong {
115
115
  .ui-stack > * + * { margin-top: var(--space-4); }
116
116
 
117
117
  /* Eyebrow — the label above a title or a figure, in sentence case like every
118
- label in the kit. why: docs/specification.md#labels-and-titles */
118
+ label in the kit. why: docs/foundations.md#labels-and-titles */
119
119
  .ui-eyebrow {
120
120
  /* rank: label */
121
121
  font-size: var(--text-sm);
@@ -20,7 +20,7 @@
20
20
  padding: var(--btn-pad-y) var(--btn-pad-x);
21
21
  cursor: pointer;
22
22
  /* Light needs a stronger line here: --border on this fill measures 1.047:1.
23
- why: docs/specification.md#elevation */
23
+ why: docs/foundations.md#elevation */
24
24
  border: 1px solid var(--control-edge);
25
25
  background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
26
26
  color: var(--text);
@@ -74,7 +74,7 @@
74
74
  }
75
75
 
76
76
  /* -- Sizes -------------------------------------------------------------- */
77
- /* why: docs/specification.md#extra-small-buttons */
77
+ /* why: docs/components.md#extra-small-buttons */
78
78
  .ui-btn--xs { --btn-pad-y: 4.5px; --btn-pad-x: 8px; --btn-font: var(--text-xs); min-width: 24px; min-height: 24px; }
79
79
  .ui-btn--xs svg { width: 13px; height: 13px; stroke-width: 2.8; }
80
80
  .ui-btn--sm { --btn-pad-y: 6px; --btn-pad-x: 12px; --btn-font: calc(var(--text-sm) - 0.5px); }
@@ -123,7 +123,7 @@
123
123
  contrast table shows that. The box was reverted and this ink shipped.
124
124
  Held by src/styles/button-disabled.test.js. */
125
125
 
126
- /* why: docs/specification.md#busy-button-labels */
126
+ /* why: docs/components.md#busy-button-labels */
127
127
  .ui-btn { position: relative; }
128
128
  .ui-btn[aria-busy="true"] { pointer-events: none; }
129
129
  .ui-btn[aria-busy="true"][data-btn-wired] {
@@ -15,7 +15,7 @@
15
15
  .ui-callout__body { min-width: 0; overflow-wrap: break-word; }
16
16
  .ui-callout__actions { margin-top: 15px; }
17
17
  .ui-callout__icon { width: 18px; height: 18px; flex: none; margin-top: 1px; /* muted-ink: glyph — nonverbal callout icon */ color: var(--muted); }
18
- .ui-callout__icon svg { width: 100%; height: 100%; stroke: currentColor; fill: none; stroke-width: 2.1; } /* 2.1 of a 24 box drawn at 18px = 1.58 CSS px, which is what buys the 3:1 graphic bar rather than the 4.5:1 text one. docs/specification.md#icons-and-glyphs */
18
+ .ui-callout__icon svg { width: 100%; height: 100%; stroke: currentColor; fill: none; stroke-width: 2.1; } /* 2.1 of a 24 box drawn at 18px = 1.58 CSS px, which is what buys the 3:1 graphic bar rather than the 4.5:1 text one. docs/foundations.md#icons-and-glyphs */
19
19
  .ui-callout b { color: var(--text); font-weight: var(--weight-semibold); }
20
20
  .ui-callout code { font-family: var(--font-mono); font-size: 0.92em; color: var(--text); }
21
21
 
@@ -133,7 +133,7 @@
133
133
  the 20px success() draws it at, rather than stretching a status mark to fill the
134
134
  line mark's box. The mark fills the box, so the box is what decides. The modifier
135
135
  is written by successPanel(), as success() writes ui-sx--check-circled on its root.
136
- why: docs/specification.md#success-confirmations */
136
+ why: docs/components.md#success-confirmations */
137
137
  .ui-success__check--circled { width: 20px; height: 20px; }
138
138
  .ui-success__title { color: var(--strong); font-size: var(--text-lg); font-weight: var(--weight-semibold); margin-bottom: 5px; }
139
139
  .ui-success__sub { color: var(--text); font-size: var(--text-base); }
@@ -6,14 +6,14 @@
6
6
  /* The surface is the step; what makes it legible depends on the theme. Dark edges the
7
7
  card with the hairline (--card-edge). Light draws no line at all and casts --elev-rest
8
8
  instead — a 22px penumbra off a 10px offset, the width Artur's reference uses on a
9
- card. Chosen by him on #490 round t3. why: docs/specification.md#elevation */
9
+ card. Chosen by him on #490 round t3. why: docs/foundations.md#elevation */
10
10
  .ui-card {
11
11
  background: var(--surface); --ring-gap: var(--surface);
12
12
  border: 1px solid var(--card-edge);
13
13
  box-shadow: var(--elev-rest);
14
14
  border-radius: var(--radius-xl);
15
15
  /* Was `var(--space-6) 26px`; 26 is off the scale and broke the column's text edge.
16
- why: docs/specification.md#spacing-and-rhythm */
16
+ why: docs/foundations.md#spacing-and-rhythm */
17
17
  padding: var(--space-6);
18
18
  position: relative;
19
19
  }
@@ -26,7 +26,7 @@
26
26
  answered with the native outline. It is a scroll region, so it takes --ring-scroll:
27
27
  the band drawn inward, inside the card's 24px padding and clear of the table.
28
28
  --elev-rest is left alone: the band is an outline, not a layer of the shadow list.
29
- why: docs/specification.md#the-focus-ring */
29
+ why: docs/foundations.md#the-focus-ring */
30
30
  .ui-card:has(> .ui-table):focus-visible { outline: var(--ring-scroll); outline-offset: var(--ring-scroll-offset); }
31
31
 
32
32
  /* Vertical stacks space cards with the container's gap — see .ui-card-stack.
@@ -36,7 +36,7 @@
36
36
  /* A padding modifier moves the vertical rhythm and keeps the text edge, so cards of
37
37
  different padding line up in one column. --pad-lg is the centred landing card, never
38
38
  stacked with plain ones, and keeps its own inset.
39
- why: docs/specification.md#spacing-and-rhythm */
39
+ why: docs/foundations.md#spacing-and-rhythm */
40
40
  .ui-card--pad-sm { padding: var(--space-5) var(--space-6); }
41
41
  .ui-card--pad-lg { padding: var(--space-8) var(--space-10); }
42
42
 
@@ -44,7 +44,7 @@
44
44
  the card's own hairline re-coloured: an inset ring over it would draw two. #295
45
45
  The strength is --card-tint, per theme: light mixes down from white, so a tint that
46
46
  works in dark sinks a light card below the page. Each variant edges itself in its own
47
- colour. why: docs/specification.md#elevation */
47
+ colour. why: docs/foundations.md#elevation */
48
48
  .ui-card--accent {
49
49
  background: color-mix(in srgb, var(--accent) var(--card-tint), var(--surface)); --ring-gap: color-mix(in srgb, var(--accent) var(--card-tint), var(--surface));
50
50
  border-color: color-mix(in srgb, var(--accent) 22%, transparent);
@@ -66,12 +66,12 @@
66
66
  transition: transform var(--dur-fast) var(--ease);
67
67
  }
68
68
  /* The lift alone: the card's own rung is what says it is raised, and hover is not a
69
- change of level. why: docs/specification.md#elevation */
69
+ change of level. why: docs/foundations.md#elevation */
70
70
  .ui-card--interactive:hover { transform: translateY(-2px); }
71
71
  .ui-card--interactive:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); }
72
72
 
73
73
  /* card() emits the title as an h2, so the text face is named here rather than
74
- left to the element. why: docs/specification.md#labels-and-titles */
74
+ left to the element. why: docs/foundations.md#labels-and-titles */
75
75
  .ui-card__title {
76
76
  color: var(--strong);
77
77
  font-family: var(--font-sans);
@@ -82,7 +82,7 @@
82
82
  display: flex;
83
83
  align-items: center;
84
84
  gap: 10px;
85
- /* Title to description: the step, not the 5px it was. why: docs/specification.md#spacing-and-rhythm */
85
+ /* Title to description: the step, not the 5px it was. why: docs/foundations.md#spacing-and-rhythm */
86
86
  margin: 0 0 var(--space-2);
87
87
  }
88
88
  .ui-card__sub {