@apliteni/apliteni-ui 0.92.0 → 0.94.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/guidelines/colour-and-theming.md +2 -0
  3. package/package.json +1 -1
  4. package/react/README.md +2 -2
  5. package/react/dist/index.d.ts +3 -3
  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 +54 -57
  42. package/src/styles/loading.css +1 -1
  43. package/src/styles/motion.css +1 -1
  44. package/src/styles/nav.css +39 -50
  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
package/README.md CHANGED
@@ -61,7 +61,7 @@ import { DataTable, Modal } from '@apliteni/apliteni-ui/react';
61
61
 
62
62
  The `appShell()` factory provides the complete product layout: the sticky rail, the page body
63
63
  and a topbar over it when you ask for one, so no product rebuilds it. The
64
- [component catalog](docs/library.md#component-catalog) lists this factory and every other one.
64
+ [component catalog](docs/components.md#component-catalog) lists this factory and every other one.
65
65
  Account and personal settings belong in a modal over the product rather than on a page of
66
66
  their own — see [Guidelines / Account and settings](guidelines/account-and-settings.md).
67
67
 
@@ -100,7 +100,7 @@ only `data-accent` is present, the kit uses that accent with the dark theme. An
100
100
  `data-theme` does *not* mean “follow the system”: the kit has no `prefers-color-scheme`
101
101
  rule. If the host wants to follow the operating system preference, its JavaScript must
102
102
  choose the theme and add the attribute. See
103
- [an absent attribute means dark](docs/library.md#an-absent-attribute-means-dark).
103
+ [an absent attribute means dark](docs/components.md#an-absent-attribute-means-dark).
104
104
 
105
105
  You can change the settings at runtime with `applyTheme('light')` and
106
106
  `applyAccent('phoenix')`. These functions save the choices to `localStorage`. You can also
@@ -121,12 +121,10 @@ section.
121
121
 
122
122
  | Page | What it answers |
123
123
  | --- | --- |
124
- | [specification.md](docs/specification.md) | What the kit guarantees and what it refuses to do. A gate on `npm test` holds every statement. |
125
- | [library.md](docs/library.md) | Architecture, the `src/` layout, tokens and theming, the component catalog. |
124
+ | [foundations.md](docs/foundations.md) | The tokens and the floors: widths, breakpoints, spacing, type, colour, elevation, the focus ring, the tap floor. |
125
+ | [components.md](docs/components.md) | The package: what to import, theming, the component catalog, and what each component guarantees. |
126
126
  | [react/README.md](react/README.md) | The React components, their props, and the Storybook on port 6007. |
127
- | [storybook.md](docs/storybook.md) | The workbench: config, theming toolbar, story conventions. |
128
- | [landing-page.md](docs/landing-page.md) | ui.apli.tech: the chrome, the static build, the hosting. |
129
- | [changelog.md](docs/changelog.md) | The changelog's data model and its Storybook deeplinks. |
127
+ | [contributing.md](docs/contributing.md) | Working on the kit: the repository's layout, Storybook, the guidelines collection, the site, the changelog and releases. |
130
128
 
131
129
  ## Contribute
132
130
 
@@ -66,3 +66,5 @@
66
66
  **Do:** Draw inputs with an edge. Keep grey fills for non-text states, such as disabled tracks and dividers.
67
67
 
68
68
  **Don't:** Put text on grey fields, chips, headers or footers.
69
+
70
+ **Except:** The sidebar rail's current row. Its mark may be neither an accent nor an edge — an outline there means focus — and in the page shell the rail is the card surface already. It takes the quiet fill a rung off its rail, label and all. Artur, r34 of [#475](https://github.com/apliteni/apliteni-ui/issues/475).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.92.0",
3
+ "version": "0.94.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
package/react/README.md CHANGED
@@ -246,7 +246,7 @@ this component and compares what is left.
246
246
  Two things the factory has that this does not, both deliberate. It emits no `data-dropdown`
247
247
  on the container, so a page that calls `wireDropdown(document)` cannot adopt a dropdown React
248
248
  owns — the same decision `Drawer` makes about `data-drawer`; the row and panel hooks stay,
249
- because they are the row contract `docs/library.md` publishes. And it has no `portal: true`: the
249
+ because they are the row contract `docs/components.md` publishes. And it has no `portal: true`: the
250
250
  panel is a child of the trigger's container, so a dropdown inside `.ui-app__rail`
251
251
  (`position: sticky` with `overflow-y: auto`) still wants the vanilla factory.
252
252
 
@@ -541,7 +541,7 @@ An optional `id` lets the caller choose the control ID; it must be unique.
541
541
  `type="search"` input, and no visible label, so the row keeps the height of the
542
542
  unlabelled controls beside it. `ariaLabel` is required and is the control's only
543
543
  name. It forwards its ref and the native input props, and `className` joins
544
- `.ui-input`. There is no clear button — see the specification for why.
544
+ `.ui-input`. There is no clear button — see docs/components.md#react-search-field for why.
545
545
 
546
546
  ```tsx
547
547
  <div className="ui-toolbar">
@@ -730,7 +730,7 @@ type SearchFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'ar
730
730
  * No clear button. The browser paints its own near-black on the light field and
731
731
  * white on the dark one, beside the kit's `--muted` magnifier — two glyphs, and
732
732
  * only one answers to a token — so `input.css` suppresses it.
733
- * why: docs/specification.md#react-search-field
733
+ * why: docs/components.md#react-search-field
734
734
  */
735
735
  declare const SearchField: react.ForwardRefExoticComponent<Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "aria-label"> & {
736
736
  /** The control's name. A toolbar search box shows no label, so this is the
@@ -850,7 +850,7 @@ type SuccessProps = Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'children'> &
850
850
  check?: SuccessMark;
851
851
  title?: string;
852
852
  /** One short line under the title, or nothing. There is no eyebrow tier.
853
- * why: docs/specification.md#success-confirmations */
853
+ * why: docs/components.md#success-confirmations */
854
854
  body?: string;
855
855
  /** Removed: a confirmation carries one title and at most one line. Typed as
856
856
  * `never` so a caller still passing it is told where it went rather than
@@ -869,7 +869,7 @@ declare const Success: react.ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLD
869
869
  check?: SuccessMark;
870
870
  title?: string;
871
871
  /** One short line under the title, or nothing. There is no eyebrow tier.
872
- * why: docs/specification.md#success-confirmations */
872
+ * why: docs/components.md#success-confirmations */
873
873
  body?: string;
874
874
  /** Removed: a confirmation carries one title and at most one line. Typed as
875
875
  * `never` so a caller still passing it is told where it went rather than
@@ -166,7 +166,7 @@ export const iconNames = Object.keys(ICONS);
166
166
  // `filter` — Lucide's funnel — fails the first test. Depicting a thing is not the
167
167
  // fault; `copy` is on this list and depicts one too. The kit reads the funnel alone
168
168
  // as not saying whether the control filters, sorts or exports — its own inference
169
- // from the sources in docs/specification.md, not a measured recognition finding.
169
+ // from the sources in docs/foundations.md, not a measured recognition finding.
170
170
  // See #565.
171
171
  export const iconOnlyAllowed = {
172
172
  x: 'close or dismiss',
@@ -2,8 +2,8 @@
2
2
  // that interpolates raw. It lives here rather than in shell.js because both
3
3
  // shell.js and topbar.js need it and shell.js already imports topbar.js — the
4
4
  // other direction would be a cycle. shell.js re-exports ACCOUNT_NAV, which is
5
- // the published name docs/library.md documents.
6
- // why: docs/specification.md#the-page-shell
5
+ // the published name docs/components.md documents.
6
+ // why: docs/components.md#the-page-shell
7
7
  import { esc } from './index.js';
8
8
 
9
9
  // nav.js item objects. Labels are raw text — every nav primitive escapes, so a
@@ -11,7 +11,7 @@
11
11
  // docs/reviews/270-back-control.html and the owner chose this one, so the other
12
12
  // three — a bordered button above the title, an icon-only arrow beside it, and
13
13
  // the breadcrumb trail alone — are not built here.
14
- // why: docs/specification.md#the-back-link
14
+ // why: docs/components.md#the-back-link
15
15
  import { esc, icon } from './index.js';
16
16
 
17
17
  // "Back" names a direction rather than a place. It is what a caller who names no
@@ -29,7 +29,7 @@ const TAB_OR_NEWLINE = /[\t\n\r]/g;
29
29
 
30
30
  // A label that already says "Back to Invoices" names the place after those words, or the
31
31
  // link would be read as "Back to Back to Invoices". The whole phrase, since "Backups" and
32
- // "Back office" are places too. why: docs/specification.md#the-back-link
32
+ // "Back office" are places too. why: docs/components.md#the-back-link
33
33
  const SAID = /^back\s+to(?:\s+|$)/i;
34
34
 
35
35
  const text = (v) => (typeof v === 'string' || typeof v === 'number' ? String(v).trim() : '');
@@ -9,7 +9,7 @@ export { SCORE, scoreCommand, rankCommands, rankGroups, paletteHotkey } from '..
9
9
  // The kit ships the shell and the ranking and names no result kinds; a row goes
10
10
  // somewhere, runs something, or asks a confirm first. Inertness, Escape and the
11
11
  // focus trap come from ./overlay.js, the stack the drawer and the confirm share.
12
- // why: docs/specification.md#the-command-palette
12
+ // why: docs/components.md#the-command-palette
13
13
  import { esc, icon } from './index.js';
14
14
  import { OVERLAY_LAYER, adoptOverlay, popOverlay, pushOverlay, returnFocus, syncOverlays } from './overlay.js';
15
15
  import { safeUrl } from '../html.js';
@@ -61,7 +61,7 @@ function keysFor(shortcut) {
61
61
  * the wiring, which is also what makes Cmd+Enter open it in a tab.
62
62
  *
63
63
  * A destructive item that names no confirm is rendered DISABLED. The kit's rule
64
- * is that a delete asks first (docs/specification.md#the-command-palette), and a
64
+ * is that a delete asks first (docs/components.md#the-command-palette), and a
65
65
  * palette is the one surface where a reader is typing fast and choosing from a
66
66
  * list that reorders under them.
67
67
  */
@@ -75,7 +75,7 @@ export function drawer({
75
75
  *
76
76
  * drawer({ title, body: drawerSection({ title: 'Source', rows: [['Statement', '#4102']] }) })
77
77
  *
78
- * why: docs/specification.md#the-drawer
78
+ * why: docs/components.md#the-drawer
79
79
  *
80
80
  * @param {object} [o]
81
81
  * @param {string} [o.title] group heading (escaped)
@@ -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