@usableapp/cardds 0.7.3 → 0.7.5

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,47 @@
2
2
 
3
3
  > For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap.
4
4
 
5
+ ## 0.7.5 — 2026-09-24
6
+
7
+ ### Errors in a form (new)
8
+ - `--signal-error` (#c62828) — the ONE colour the wireframe has; always with an icon and words, never the colour alone.
9
+ A theme may set it.
10
+ - `Field error` — the edge turns red, `aria-invalid`, and the field takes the focus with ALL its text selected (type
11
+ straight over it). `Pin error` — every cell red (edge + digit), the wrong digits stay in sight, the cursor goes back to
12
+ the first cell; the next digit starts a new code.
13
+ - `SheetStep error` / `busy` — ONE line under the step's button: an alert icon + words (`role="alert"`), or a turning
14
+ ring + words ("sending…", the step's buttons take no taps). The words are the app's.
15
+ - FORMAT checks on the client (never content — content errors come from the app through `error`): `Field format="email"
16
+ | "tel"`, `digits={n}`, `required`; `Pin match={firstPin}`; `formatError="…"` = the app's words. Checked when a field is
17
+ left and when the step's button is pressed — never while typing; a failing one holds the press back (the app's onClick
18
+ is not called). A Pin that does not match shows red for 3 s, then the steps go back one (`onAtChange(at, 'mismatch')` —
19
+ clear both codes there).
20
+ - Icons `circle-alert`, `loader-circle`.
21
+
22
+ ### Changed
23
+ - `Pin` — a typed digit is coloured words (`--pin-cell-filled-ink`, default the accent), no filled pill.
24
+ - `SheetSteps` — an ACCORDION: the old step folds while the new one opens, then the done pile scrolls (smoothly) so
25
+ 2½ done rows show above the open step; a step change never drops a phone's keyboard (the cursor waits in a holder
26
+ field while the step folds); step titles at the label size (`--sheet-step-title-font`); a step stays ✓ when the
27
+ member goes back; `finish` is always shown, disabled until every step is done or skipped; the steps never close the
28
+ sheet — when `at` reaches the count, the app sets its Sheet `away`.
29
+ - `Sheet` — on arrival the cursor goes to a `[data-autofocus]` inside it (SheetSteps marks its open step's first field),
30
+ else the handle, once the sheet has finished sliding in.
31
+
32
+ ## 0.7.4 — 2026-09-24
33
+
34
+ ### New
35
+ - `SheetSteps` / `SheetStep` — one task taken in steps INSIDE one sheet (phone · code · PIN · PIN again · face · notices),
36
+ in a `SheetBody`. The done steps pile on top, one short line each (title · `summary` · ✓); the current step is open
37
+ under them, its form from the top at the form's own spacing; the next steps wait in a pile at the bottom (under the
38
+ keyboard when it is up). A thin rule over every step. Controlled: `at` + `onAtChange(at, 'tap' | 'swipe' | 'skip')`, or
39
+ `defaultAt`; the APP moves `at` when a step's work is really done; `at = count` = all done — the last step's own action
40
+ sends, or pass `finish` (a `Btn`) to stand under the steps on the last one. `reversible` = a tap on a done / next step
41
+ or an up/down swipe moves; without it only forward. `SheetStep skippable skipLabel="…"` = a small link that moves on.
42
+ The cursor stands in each new step's first field; with the keyboard up the field's middle is brought to eye level
43
+ (`--screen-centre` of what shows) and the done pile glides out of the way and back. Knobs `--sheet-steps-row-h`,
44
+ `--sheet-step-border`. Stories: `/SheetSteps` (Forward · Reversible · Start · Controlled · Finish · Long).
45
+
5
46
  ## 0.7.3 — 2026-09-24
6
47
 
7
48
  ### New
package/css/choice.css CHANGED
@@ -205,7 +205,7 @@
205
205
  font: var(--pin-cell-font, var(--type-stat));
206
206
  font-variant-numeric: tabular-nums;
207
207
  }
208
- .pin__cell:not(:placeholder-shown) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
208
+ .pin__cell:not(:placeholder-shown) { color: var(--pin-cell-filled-ink, var(--accent)); } /* a typed digit: coloured words on the cell's own ground — no fill (Lh 2026-09-24; was an accent-filled pill) */
209
209
  /* the cell the cursor waits in (Lh 2026-09-22): NO outline ring — it sat outside the pill and got cropped by the sheet's
210
210
  edge. The sign is INSIDE the pill: a full-ink border and a blinking bar drawn where the digit will land (the native caret
211
211
  is hidden — its height and place are the browser's; the pill's bar is one rule). A filled cell selects its digit instead. */
package/css/forms.css CHANGED
@@ -65,3 +65,15 @@
65
65
  line-height: 1;
66
66
  }
67
67
  .add-row__plus .icon { width: var(--icon-xs); height: var(--icon-xs); }
68
+
69
+ /* ---- a wrong value, and the line that says so (Lh 2026-09-24) ----
70
+ --signal-error is the one colour the wireframe has; it never speaks alone — the line carries an icon and words.
71
+ <p class="form-note form-note--error" role="alert"><svg class="icon icon--xs">…</svg>Not a phone number</p>
72
+ <p class="form-note form-note--busy" role="status"><svg …/>Sending the code…</p> (the ring turns) */
73
+ .field.field--error, [data-palette] .field.field--error { border-color: var(--field-error-border, var(--signal-error)); }
74
+ .pin.pin--error .pin__cell, .pin.pin--error .pin__cell:focus { border-color: var(--pin-error-border, var(--signal-error)); color: var(--pin-error-ink, var(--signal-error)); } /* ALL the cells, edge and digit — the one the cursor waits in too (Lh 2026-09-24); the wrong digits stay in red until the member types again */
75
+ .form-note { display: flex; align-items: center; justify-content: center; gap: var(--sp-2); margin: 0; font: var(--form-note-font, var(--type-caption)); color: var(--form-note-ink, var(--card-muted)); text-align: center; }
76
+ .form-note > .icon { flex: none; }
77
+ .form-note--error { color: var(--form-note-error-ink, var(--signal-error)); }
78
+ @media (prefers-reduced-motion: no-preference) { .form-note--busy > .icon { animation: form-note-turn 0.9s linear infinite; } }
79
+ @keyframes form-note-turn { to { rotate: 1turn; } }
package/css/sheet.css CHANGED
@@ -371,3 +371,43 @@
371
371
  z-index: 1;
372
372
  }
373
373
  .sheet-stage--dim .sheet { z-index: 2; }
374
+
375
+ /* ---- sheet steps: one task in steps inside one sheet (Lh 2026-09-24) ----
376
+ <div class="sheet-steps"> › <ol class="sheet-steps__track"> of <li class="sheet-step sheet-step--done|now|next">. The done steps
377
+ PILE UP on top, ALL of them, each folded to one short line (title · summary · ✓) — when the keyboard leaves too little room the
378
+ body scrolls them up only as far as the field needs; the current step is open under them — its title, then its form at
379
+ the form's own spacing, from the top (never spread or centred: the keyboard needs the room under it); the next steps wait in a pile at the
380
+ bottom of the body, each its title line. Every step has a thin rule on top. A step opens and folds as a 1fr ↔ 0fr grid row, so the pile rises and
381
+ falls — no height is written. Nothing fades: done and next read at full ink. */
382
+ .sheet__body:has(> .sheet-steps) { display: flex; flex-direction: column; }
383
+ .sheet-steps { flex-shrink: 0; min-height: calc(100% + var(--kb, 0px)); /* the keyboard's height more: the pile of next steps stays under the keyboard, it never rises with it */ display: flex; flex-direction: column; touch-action: pan-y; }
384
+ .sheet-steps__holder { position: absolute; top: 0; left: 0; width: 1px; height: 1px; padding: 0; border: 0; opacity: 0; pointer-events: none; font-size: 16px; } /* holds the cursor (and the phone's keyboard) while a step folds; 16px so iOS never zooms to it */
385
+ .sheet-steps { position: relative; }
386
+ .sheet-steps__track { list-style: none; margin: 0; padding: 0; flex: 1; display: flex; flex-direction: column; }
387
+
388
+ .sheet-step {
389
+ position: relative; min-width: 0;
390
+ display: grid; grid-template-rows: auto 0fr; align-content: start; /* mid-fold the rows sum to less than the box: the spare room stays BELOW, never spread into the title row (Lh: it looked like a spring) */
391
+ border-top: var(--rule-w) solid var(--sheet-step-border, var(--hairline));
392
+ flex: 0 0 auto;
393
+ transition: grid-template-rows var(--motion-sheet) ease, flex-grow var(--motion-sheet) ease;
394
+ }
395
+ /* the open step GROWS into the room left, so the steps not done yet wait in a pile at the bottom — and a move is an ACCORDION:
396
+ in one beat the step that was open folds while the new one unfolds (flex-grow and the 1fr row both ease), the piles slide
397
+ with them, nothing jumps (Lh 2026-09-24) */
398
+ .sheet-step--now { grid-template-rows: auto 1fr; flex-grow: 1; }
399
+ .sheet-step__head { display: flex; align-items: center; gap: var(--sp-2); min-height: var(--sheet-steps-row-h, var(--tap-sm)); }
400
+ .sheet-step__title { font: var(--sheet-step-title-font, var(--type-label)); margin: 0; flex: 0 1 auto; min-width: 0; outline: none; /* the label size, not a body title: a pile of rows must stay small (Lh 2026-09-24) */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
401
+ .sheet-step__summary { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
402
+ .sheet-step__check { margin-inline-start: auto; }
403
+ .sheet-step__body { min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
404
+ .sheet-step__inner { display: grid; gap: var(--card-gap); padding-bottom: var(--card-gap); }
405
+ .sheet-step__inner > * { min-width: 0; }
406
+ .sheet-step__skip { justify-self: center; }
407
+ .sheet-step:not(.sheet-step--now) > .sheet-step__body { visibility: hidden; transition: visibility 0s linear var(--motion-sheet); }
408
+ .sheet-step__go { position: absolute; inset: 0; margin: 0; padding: 0; border: 0; background: none; cursor: pointer; -webkit-tap-highlight-color: transparent; }
409
+ /* the last step's finish, when the app gives one: under the steps */
410
+ .sheet-steps__finish { display: grid; padding-top: var(--card-gap); }
411
+ @media (prefers-reduced-motion: reduce) { .sheet-step { transition: none; } }
412
+ /* the step waiting for its answer: its buttons take no taps (the app guards its own handler too) */
413
+ .sheet-step--busy .btn { pointer-events: none; opacity: 0.5; }
package/css/tokens.css CHANGED
@@ -187,6 +187,7 @@
187
187
  --neg: #3d3d3d;
188
188
  --dot-a: var(--color-brand, #141414);
189
189
  --dot-b: #8a8a8a;
190
+ --signal-error: #c62828; /* the ONE colour in the wireframe (Lh 2026-09-24): something went wrong — a field's edge, the words that say what. Always with an icon and words, never the colour alone */
190
191
 
191
192
  /* derived tints — read the element's own ink, so they work on any slot */
192
193
  --outline: var(--action-border); /* control edges (palette.css softens) */
package/dist/cardds.css CHANGED
@@ -228,6 +228,7 @@
228
228
  --neg: #3d3d3d;
229
229
  --dot-a: var(--color-brand, #141414);
230
230
  --dot-b: #8a8a8a;
231
+ --signal-error: #c62828; /* the ONE colour in the wireframe (Lh 2026-09-24): something went wrong — a field's edge, the words that say what. Always with an icon and words, never the colour alone */
231
232
 
232
233
  /* derived tints — read the element's own ink, so they work on any slot */
233
234
  --outline: var(--action-border); /* control edges (palette.css softens) */
@@ -1548,6 +1549,46 @@ button { font: inherit; cursor: pointer; }
1548
1549
  }
1549
1550
  .sheet-stage--dim .sheet { z-index: 2; }
1550
1551
 
1552
+ /* ---- sheet steps: one task in steps inside one sheet (Lh 2026-09-24) ----
1553
+ <div class="sheet-steps"> › <ol class="sheet-steps__track"> of <li class="sheet-step sheet-step--done|now|next">. The done steps
1554
+ PILE UP on top, ALL of them, each folded to one short line (title · summary · ✓) — when the keyboard leaves too little room the
1555
+ body scrolls them up only as far as the field needs; the current step is open under them — its title, then its form at
1556
+ the form's own spacing, from the top (never spread or centred: the keyboard needs the room under it); the next steps wait in a pile at the
1557
+ bottom of the body, each its title line. Every step has a thin rule on top. A step opens and folds as a 1fr ↔ 0fr grid row, so the pile rises and
1558
+ falls — no height is written. Nothing fades: done and next read at full ink. */
1559
+ .sheet__body:has(> .sheet-steps) { display: flex; flex-direction: column; }
1560
+ .sheet-steps { flex-shrink: 0; min-height: calc(100% + var(--kb, 0px)); /* the keyboard's height more: the pile of next steps stays under the keyboard, it never rises with it */ display: flex; flex-direction: column; touch-action: pan-y; }
1561
+ .sheet-steps__holder { position: absolute; top: 0; left: 0; width: 1px; height: 1px; padding: 0; border: 0; opacity: 0; pointer-events: none; font-size: 16px; } /* holds the cursor (and the phone's keyboard) while a step folds; 16px so iOS never zooms to it */
1562
+ .sheet-steps { position: relative; }
1563
+ .sheet-steps__track { list-style: none; margin: 0; padding: 0; flex: 1; display: flex; flex-direction: column; }
1564
+
1565
+ .sheet-step {
1566
+ position: relative; min-width: 0;
1567
+ display: grid; grid-template-rows: auto 0fr; align-content: start; /* mid-fold the rows sum to less than the box: the spare room stays BELOW, never spread into the title row (Lh: it looked like a spring) */
1568
+ border-top: var(--rule-w) solid var(--sheet-step-border, var(--hairline));
1569
+ flex: 0 0 auto;
1570
+ transition: grid-template-rows var(--motion-sheet) ease, flex-grow var(--motion-sheet) ease;
1571
+ }
1572
+ /* the open step GROWS into the room left, so the steps not done yet wait in a pile at the bottom — and a move is an ACCORDION:
1573
+ in one beat the step that was open folds while the new one unfolds (flex-grow and the 1fr row both ease), the piles slide
1574
+ with them, nothing jumps (Lh 2026-09-24) */
1575
+ .sheet-step--now { grid-template-rows: auto 1fr; flex-grow: 1; }
1576
+ .sheet-step__head { display: flex; align-items: center; gap: var(--sp-2); min-height: var(--sheet-steps-row-h, var(--tap-sm)); }
1577
+ .sheet-step__title { font: var(--sheet-step-title-font, var(--type-label)); margin: 0; flex: 0 1 auto; min-width: 0; outline: none; /* the label size, not a body title: a pile of rows must stay small (Lh 2026-09-24) */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1578
+ .sheet-step__summary { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1579
+ .sheet-step__check { margin-inline-start: auto; }
1580
+ .sheet-step__body { min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
1581
+ .sheet-step__inner { display: grid; gap: var(--card-gap); padding-bottom: var(--card-gap); }
1582
+ .sheet-step__inner > * { min-width: 0; }
1583
+ .sheet-step__skip { justify-self: center; }
1584
+ .sheet-step:not(.sheet-step--now) > .sheet-step__body { visibility: hidden; transition: visibility 0s linear var(--motion-sheet); }
1585
+ .sheet-step__go { position: absolute; inset: 0; margin: 0; padding: 0; border: 0; background: none; cursor: pointer; -webkit-tap-highlight-color: transparent; }
1586
+ /* the last step's finish, when the app gives one: under the steps */
1587
+ .sheet-steps__finish { display: grid; padding-top: var(--card-gap); }
1588
+ @media (prefers-reduced-motion: reduce) { .sheet-step { transition: none; } }
1589
+ /* the step waiting for its answer: its buttons take no taps (the app guards its own handler too) */
1590
+ .sheet-step--busy .btn { pointer-events: none; opacity: 0.5; }
1591
+
1551
1592
  /* ---- css/step.css — .step: one screen, one task — card, optional sheet, bar outside ---- */
1552
1593
  /* ============================================================
1553
1594
  cardds/step.css — .step: one screen, one task.
@@ -1778,6 +1819,18 @@ button { font: inherit; cursor: pointer; }
1778
1819
  }
1779
1820
  .add-row__plus .icon { width: var(--icon-xs); height: var(--icon-xs); }
1780
1821
 
1822
+ /* ---- a wrong value, and the line that says so (Lh 2026-09-24) ----
1823
+ --signal-error is the one colour the wireframe has; it never speaks alone — the line carries an icon and words.
1824
+ <p class="form-note form-note--error" role="alert"><svg class="icon icon--xs">…</svg>Not a phone number</p>
1825
+ <p class="form-note form-note--busy" role="status"><svg …/>Sending the code…</p> (the ring turns) */
1826
+ .field.field--error, [data-palette] .field.field--error { border-color: var(--field-error-border, var(--signal-error)); }
1827
+ .pin.pin--error .pin__cell, .pin.pin--error .pin__cell:focus { border-color: var(--pin-error-border, var(--signal-error)); color: var(--pin-error-ink, var(--signal-error)); } /* ALL the cells, edge and digit — the one the cursor waits in too (Lh 2026-09-24); the wrong digits stay in red until the member types again */
1828
+ .form-note { display: flex; align-items: center; justify-content: center; gap: var(--sp-2); margin: 0; font: var(--form-note-font, var(--type-caption)); color: var(--form-note-ink, var(--card-muted)); text-align: center; }
1829
+ .form-note > .icon { flex: none; }
1830
+ .form-note--error { color: var(--form-note-error-ink, var(--signal-error)); }
1831
+ @media (prefers-reduced-motion: no-preference) { .form-note--busy > .icon { animation: form-note-turn 0.9s linear infinite; } }
1832
+ @keyframes form-note-turn { to { rotate: 1turn; } }
1833
+
1781
1834
  /* ---- css/journey.css — route, tile badge, note row ---- */
1782
1835
  /* ============================================================
1783
1836
  cardds/journey.css — trip/status primitives inside cards:
@@ -2742,7 +2795,7 @@ button { font: inherit; cursor: pointer; }
2742
2795
  font: var(--pin-cell-font, var(--type-stat));
2743
2796
  font-variant-numeric: tabular-nums;
2744
2797
  }
2745
- .pin__cell:not(:placeholder-shown) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
2798
+ .pin__cell:not(:placeholder-shown) { color: var(--pin-cell-filled-ink, var(--accent)); } /* a typed digit: coloured words on the cell's own ground — no fill (Lh 2026-09-24; was an accent-filled pill) */
2746
2799
  /* the cell the cursor waits in (Lh 2026-09-22): NO outline ring — it sat outside the pill and got cropped by the sheet's
2747
2800
  edge. The sign is INSIDE the pill: a full-ink border and a blinking bar drawn where the digit will land (the native caret
2748
2801
  is hidden — its height and place are the browser's; the pill's bar is one rule). A filled cell selects its digit instead. */
@@ -1,4 +1,4 @@
1
- import type { ComponentProps, ReactNode } from 'react';
1
+ import { type ComponentProps, type ReactNode } from 'react';
2
2
  import { type IconName } from '../type/Icon.js';
3
3
  export interface FieldProps extends Omit<ComponentProps<'input'>, 'className' | 'style'> {
4
4
  /** a leading icon (phone-call, plane, calendar-days) */
@@ -7,6 +7,19 @@ export interface FieldProps extends Omit<ComponentProps<'input'>, 'className' |
7
7
  action?: ReactNode;
8
8
  /** the accessible name when the placeholder is not enough */
9
9
  label?: string;
10
+ /** the value is wrong: the edge turns `--signal-error`, the input says `aria-invalid`, and the moment it comes (or changes) the
11
+ * field takes the focus with ALL its text selected — the member types the right one straight over it. The words saying what
12
+ * is wrong are the step's (`SheetStep error`), under its button; any truthy value marks the field. */
13
+ error?: unknown;
14
+ /** a FORMAT check on the client (never whether the content is right — that is `error`, from the app): `email`, or `tel`
15
+ * (digits, spaces, - + ( ) only). Checked when the member LEAVES the field and when the step's button is pressed, never
16
+ * while typing; a wrong one is marked like `error`, and the step's button does not go on until it is fixed. */
17
+ format?: 'email' | 'tel';
18
+ /** the exact number of digits it must hold (a phone number: 10) — a format check like `format` */
19
+ digits?: number;
20
+ /** the words when the format check fails — shown under the step's button (the app's words: cardds has none). `required`
21
+ * (the attribute) makes an empty field fail when the button is pressed. */
22
+ formatError?: ReactNode;
10
23
  className?: string;
11
24
  style?: React.CSSProperties;
12
25
  }
@@ -14,4 +27,4 @@ export interface FieldProps extends Omit<ComponentProps<'input'>, 'className' |
14
27
  * Field — the flat outlined input: the LABEL IS THE PLACEHOLDER. 48px tall (56px with an action inside).
15
28
  * Fields belong in a Sheet (what the member acts on), 1–3 per step, never in a reading card.
16
29
  */
17
- export declare function Field({ icon, action, label, className, style, ...input }: FieldProps): import("react").JSX.Element;
30
+ export declare function Field({ icon, action, label, error, format, digits, formatError, className, style, onBlur, onInput, ...input }: FieldProps): import("react").JSX.Element;
@@ -1,10 +1,36 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { formatOk, useCheck } from './check.js';
2
4
  import { cx } from '../cx.js';
3
5
  import { Icon } from '../type/Icon.js';
4
6
  /**
5
7
  * Field — the flat outlined input: the LABEL IS THE PLACEHOLDER. 48px tall (56px with an action inside).
6
8
  * Fields belong in a Sheet (what the member acts on), 1–3 per step, never in a reading card.
7
9
  */
8
- export function Field({ icon, action, label, className, style, ...input }) {
9
- return (_jsxs("label", { className: cx('field', action != null && 'field--action', className), style: style, children: [icon && _jsx(Icon, { name: icon, size: "sm" }), _jsx("input", { "aria-label": label, ...input }), action] }));
10
+ export function Field({ icon, action, label, error, format, digits, formatError, className, style, onBlur, onInput, ...input }) {
11
+ const ref = useRef(null);
12
+ const [bad, setBad] = useState(false); // the format check failed, and has not been fixed since
13
+ const wrong = (error != null && error !== false && error !== '') || bad;
14
+ const checked = format != null || digits != null || input.required;
15
+ const words = formatError ?? ''; // no words given: the field is still marked, the step's line stays empty
16
+ const verdict = (strict) => {
17
+ const v = ref.current?.value ?? '';
18
+ if (!checked || (!v.trim() && !(strict && input.required)))
19
+ return null; // an untouched empty field fails only when required, on the press
20
+ return formatOk(v, format, digits) && (v.trim() || !input.required) ? null : words;
21
+ };
22
+ const { say } = useCheck((strict) => { const w = verdict(strict); if (w != null)
23
+ setBad(true); return w; }, () => { ref.current?.focus(); ref.current?.select(); });
24
+ useEffect(() => {
25
+ if (wrong) {
26
+ ref.current?.focus();
27
+ ref.current?.select();
28
+ }
29
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- when the app's error comes or changes
30
+ }, [error]);
31
+ return (_jsxs("label", { className: cx('field', action != null && 'field--action', wrong && 'field--error', className), style: style, children: [icon && _jsx(Icon, { name: icon, size: "sm" }), _jsx("input", { ref: ref, "aria-label": label, "aria-invalid": wrong || undefined, ...input, onBlur: (e) => { onBlur?.(e); if (!checked)
32
+ return; const w = verdict(false); setBad(w != null); say(w); }, onInput: (e) => { onInput?.(e); if (bad && verdict(false) == null) {
33
+ setBad(false);
34
+ say(null);
35
+ } } }), action] }));
10
36
  }
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from 'react';
2
+ /** the one line that says how a form stands — `error` (an alert icon + words, read out at once) or `busy` (a turning ring +
3
+ * words, "sending the code…"). Internal: a SheetStep draws it under its button. */
4
+ export declare function FormNote({ error, busy }: {
5
+ error?: ReactNode;
6
+ busy?: ReactNode;
7
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Icon } from '../type/Icon.js';
3
+ /** the one line that says how a form stands — `error` (an alert icon + words, read out at once) or `busy` (a turning ring +
4
+ * words, "sending the code…"). Internal: a SheetStep draws it under its button. */
5
+ export function FormNote({ error, busy }) {
6
+ if (error != null && error !== false)
7
+ return _jsxs("p", { className: "form-note form-note--error", role: "alert", children: [_jsx(Icon, { name: "circle-alert", size: "xs" }), error] });
8
+ if (busy != null && busy !== false)
9
+ return _jsxs("p", { className: "form-note form-note--busy", role: "status", children: [_jsx(Icon, { name: "loader-circle", size: "xs" }), busy] });
10
+ return null;
11
+ }
@@ -1,4 +1,4 @@
1
- import { type ComponentProps } from 'react';
1
+ import { type ReactNode, type ComponentProps } from 'react';
2
2
  export interface PinProps extends Omit<ComponentProps<'div'>, 'onChange' | 'defaultValue'> {
3
3
  /** how many digits (4 or 6) */
4
4
  length?: number;
@@ -10,6 +10,15 @@ export interface PinProps extends Omit<ComponentProps<'div'>, 'onChange' | 'defa
10
10
  label?: string;
11
11
  /** the code so far, after every change; `complete` once every cell holds a digit */
12
12
  onChange?: (code: string, complete: boolean) => void;
13
+ /** the code is wrong: the digits stay in sight in `--signal-error` (words, no fill) with red edges, the cursor goes back to the
14
+ * first cell, and the next digit typed starts a new code. The words are the step's (`SheetStep error`). */
15
+ error?: unknown;
16
+ /** a FORMAT check on the client: the code it must equal (the first PIN, when this one confirms it). Once every cell is filled
17
+ * and it differs, the Pin turns red with its digits in sight, the step shows `formatError` under it for three seconds and then
18
+ * steps BACK one step (`onAtChange(at - 1, 'mismatch')` — the app clears both codes there). */
19
+ match?: string;
20
+ /** the words when a check fails: the code does not match, or the step's button was pressed with cells still empty */
21
+ formatError?: ReactNode;
13
22
  /** focus the first empty cell on mount */
14
23
  autoFocus?: boolean;
15
24
  }
@@ -19,4 +28,4 @@ export interface PinProps extends Omit<ComponentProps<'div'>, 'onChange' | 'defa
19
28
  * arrows walk the cells, a pasted code fills them from the one it landed in; only digits stay. The first cell takes the SMS
20
29
  * one-time-code autofill. The code is a VALUE, controlled like an input: `value` + `onChange`, or `defaultValue`.
21
30
  */
22
- export declare function Pin({ length, value, defaultValue, label, onChange, autoFocus, className, ...rest }: PinProps): import("react").JSX.Element;
31
+ export declare function Pin({ length, value, defaultValue, label, onChange, autoFocus, error, match, formatError, className, ...rest }: PinProps): import("react").JSX.Element;
package/dist/forms/Pin.js CHANGED
@@ -1,19 +1,30 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
3
  import { cx } from '../cx.js';
4
+ import { useCheck } from './check.js';
4
5
  /**
5
6
  * Pin — big code entry, one digit per pill; a filled cell turns accent. Lives in a Sheet.
6
7
  * Its own behaviour, inside it: typing a digit moves to the next cell, Backspace in an empty cell moves back and clears, the
7
8
  * arrows walk the cells, a pasted code fills them from the one it landed in; only digits stay. The first cell takes the SMS
8
9
  * one-time-code autofill. The code is a VALUE, controlled like an input: `value` + `onChange`, or `defaultValue`.
9
10
  */
10
- export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onChange, autoFocus, className, ...rest }) {
11
+ export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onChange, autoFocus, error, match, formatError, className, ...rest }) {
11
12
  const ref = useRef(null);
12
13
  const [inner, setInner] = useState(defaultValue);
13
14
  const code = (value ?? inner).replace(/\D/g, '').slice(0, length);
14
15
  const cell = (i) => ref.current?.querySelectorAll('.pin__cell')[i];
15
16
  const set = (next) => {
16
17
  next = next.slice(0, length);
18
+ if (bad) {
19
+ setBad(false);
20
+ say(null);
21
+ }
22
+ if (match != null && next.length === length && next !== match) {
23
+ setBad(true);
24
+ say(words);
25
+ back();
26
+ restart.current = true;
27
+ }
17
28
  if (value === undefined)
18
29
  setInner(next);
19
30
  if (next !== code)
@@ -24,8 +35,47 @@ export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onC
24
35
  cell(Math.min(code.length, length - 1))?.focus();
25
36
  // eslint-disable-next-line react-hooks/exhaustive-deps -- on mount only
26
37
  }, [autoFocus]);
38
+ const [bad, setBad] = useState(false); // a format check failed, and nothing was typed since
39
+ const words = formatError ?? '';
40
+ const { say, back } = useCheck((strict) => {
41
+ if (strict && code.length < length) {
42
+ setBad(true);
43
+ return words;
44
+ }
45
+ if (match != null && code.length === length && code !== match) {
46
+ setBad(true);
47
+ return words;
48
+ }
49
+ return null;
50
+ }, () => cell(Math.min(code.length, length - 1))?.focus());
51
+ const wrong = (error != null && error !== false && error !== '') || bad;
52
+ const restart = useRef(false);
53
+ useEffect(() => {
54
+ if (!wrong)
55
+ return;
56
+ restart.current = true; // the wrong code stays in sight (red); the next digit typed starts a new one from the first cell
57
+ cell(0)?.focus();
58
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- each new error, once
59
+ }, [wrong, error]);
60
+ /* the app emptied the code (a new try: it went back after a mismatch): the red goes with it */
61
+ useEffect(() => {
62
+ if (code || !bad)
63
+ return;
64
+ setBad(false);
65
+ say(null);
66
+ restart.current = false;
67
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- when the code is emptied from outside
68
+ }, [code]);
27
69
  const input = (i) => (e) => {
28
70
  const digits = e.target.value.replace(/\D/g, '');
71
+ if (restart.current) {
72
+ restart.current = false;
73
+ if (digits) {
74
+ set(digits.slice(-1));
75
+ cell(1)?.focus();
76
+ }
77
+ return;
78
+ } // a new try
29
79
  const at = Math.min(i, code.length); // a code has no gaps: a digit typed further along lands in the first empty cell
30
80
  if (!digits) {
31
81
  set(code.slice(0, at) + code.slice(at + 1));
@@ -49,8 +99,9 @@ export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onC
49
99
  cell(i + 1)?.focus();
50
100
  }
51
101
  };
52
- return (_jsx("div", { ref: ref, className: cx('pin', className), ...rest, children: Array.from({ length }, (_, i) => (
102
+ return (_jsx("div", { ref: ref, className: cx('pin', wrong && 'pin--error', className), ...rest, children: Array.from({ length }, (_, i) => (
53
103
  // no maxLength: the browser would cut a pasted code before this ever saw it. A cell holds one digit — typing into a full one
54
104
  // replaces it, so its digit is selected whenever the cell takes the focus
55
- _jsx("input", { className: "pin__cell", inputMode: "numeric", pattern: "[0-9]*", placeholder: " ", value: code[i] ?? '', "aria-label": `${label} ${i + 1}`, autoComplete: i === 0 ? 'one-time-code' : 'off', onChange: input(i), onKeyDown: key(i), onFocus: (e) => e.currentTarget.select() }, i))) }));
105
+ _jsx("input", { className: "pin__cell", inputMode: "numeric", pattern: "[0-9]*", placeholder: " ", value: code[i] ?? '', "aria-label": `${label} ${i + 1}`, "aria-invalid": wrong || undefined, autoComplete: i === 0 ? 'one-time-code' : 'off', onChange: input(i), onKeyDown: key(i), onFocus: (e) => { if (!restart.current)
106
+ e.currentTarget.select(); } }, i))) }));
56
107
  }
@@ -0,0 +1,23 @@
1
+ import type { ReactNode } from 'react';
2
+ export type Check = {
3
+ /** the words when it fails now, else null. `strict` = the button was pressed: an empty control fails too */
4
+ check: (strict: boolean) => ReactNode | null;
5
+ /** take the focus, ready to be fixed */
6
+ focus: () => void;
7
+ };
8
+ export type CheckGroup = {
9
+ add: (c: Check) => () => void;
10
+ /** a control's own verdict changed (it was left, or it was fixed): its words, or null */
11
+ say: (c: Check, words: ReactNode | null) => void;
12
+ /** a check that can only be failed by going back (the second PIN does not match the first): the group shows the words for a
13
+ * moment, then steps back */
14
+ back: () => void;
15
+ };
16
+ export declare const CheckContext: import("react").Context<CheckGroup | null>;
17
+ /** join the group around (if any) with this control's check; the latest check function is always the one asked */
18
+ export declare function useCheck(check: Check['check'], focus: Check['focus']): {
19
+ say: (words: ReactNode | null) => void | undefined;
20
+ back: () => void | undefined;
21
+ };
22
+ /** a Field's format: `email`, `tel` (digits, spaces, - + ( ) only) and/or an exact count of `digits` */
23
+ export declare function formatOk(value: string, format?: 'email' | 'tel', digits?: number): boolean;
@@ -0,0 +1,24 @@
1
+ import { createContext, useContext, useEffect, useRef } from 'react';
2
+ export const CheckContext = createContext(null);
3
+ /** join the group around (if any) with this control's check; the latest check function is always the one asked */
4
+ export function useCheck(check, focus) {
5
+ const group = useContext(CheckContext);
6
+ const latest = useRef({ check, focus });
7
+ latest.current.check = check;
8
+ latest.current.focus = focus;
9
+ const self = useRef({ check: (s) => latest.current.check(s), focus: () => latest.current.focus() }).current;
10
+ useEffect(() => group?.add(self), [group, self]);
11
+ return { say: (words) => group?.say(self, words), back: () => group?.back() };
12
+ }
13
+ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
14
+ /** a Field's format: `email`, `tel` (digits, spaces, - + ( ) only) and/or an exact count of `digits` */
15
+ export function formatOk(value, format, digits) {
16
+ const v = value.trim();
17
+ if (format === 'email' && !EMAIL.test(v))
18
+ return false;
19
+ if (format === 'tel' && !/^[\d\s\-+()]*$/.test(v))
20
+ return false;
21
+ if (digits != null && v.replace(/\D/g, '').length !== digits)
22
+ return false;
23
+ return true;
24
+ }
package/dist/index.d.ts CHANGED
@@ -27,6 +27,7 @@ export * from './sheets/SheetHead.js';
27
27
  export * from './sheets/SheetBody.js';
28
28
  export * from './sheets/SheetStage.js';
29
29
  export * from './sheets/Step.js';
30
+ export * from './sheets/SheetSteps.js';
30
31
  export * from './sheets/Drawer.js';
31
32
  export * from './actions/Btn.js';
32
33
  export * from './actions/IconBtn.js';
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ export * from './sheets/SheetHead.js';
31
31
  export * from './sheets/SheetBody.js';
32
32
  export * from './sheets/SheetStage.js';
33
33
  export * from './sheets/Step.js';
34
+ export * from './sheets/SheetSteps.js';
34
35
  export * from './sheets/Drawer.js';
35
36
  export * from './actions/Btn.js';
36
37
  export * from './actions/IconBtn.js';
@@ -39,8 +39,25 @@ export function Sheet({ state, defaultState = 'half', onStateChange, states, han
39
39
  useEffect(() => {
40
40
  if (current !== 'peek' && current !== 'away')
41
41
  home.current = current;
42
- if (was.current === 'away' && current !== 'away')
43
- handleRef.current?.focus({ preventScroll: true }); // it just came onto the screen: the keyboard user is on it
42
+ if (was.current === 'away' && current !== 'away') { // it just came onto the screen: the cursor goes to what is marked to take it first (`data-autofocus` — a SheetSteps marks its open step's first field), else the handle, for the keyboard user
43
+ // …once it has finished sliding in (Lh 2026-09-24): the keyboard must not come up while the sheet still moves
44
+ const el = ref.current;
45
+ let done = false;
46
+ const land = () => {
47
+ if (done)
48
+ return;
49
+ done = true;
50
+ el?.removeEventListener('transitionend', onEnd);
51
+ const first = el?.querySelector('[data-autofocus]:not([inert] *)');
52
+ (first ?? handleRef.current)?.focus({ preventScroll: true });
53
+ };
54
+ const onEnd = (e) => { if (e.target === el && e.propertyName === 'translate')
55
+ land(); };
56
+ el?.addEventListener('transitionend', onEnd);
57
+ const t = setTimeout(land, 600); // no transition (reduced motion): land anyway
58
+ was.current = current;
59
+ return () => { clearTimeout(t); el?.removeEventListener('transitionend', onEnd); };
60
+ }
44
61
  was.current = current;
45
62
  }, [current]);
46
63
  /* geometry, read once per gesture — never stored: how far it has slid, and where each state rests, in px from its top at full */
@@ -0,0 +1,47 @@
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ /** what asked for the move: a `tap` on another step, a `swipe` across the steps, the step's `skip` link, a `mismatch` (a Pin's
3
+ * `match` failed: back one step — clear the codes there) */
4
+ export type SheetStepsReason = 'tap' | 'swipe' | 'skip' | 'mismatch';
5
+ export interface SheetStepsProps extends ComponentProps<'div'> {
6
+ /** optional: a `Btn` that finishes, under the steps — always in sight, `disabled` until EVERY step is done (its work, or
7
+ * skipped: `at` has reached `count`). None = the task ends when `at` reaches `count` — the last step's own action, or its skip. */
8
+ finish?: ReactNode;
9
+ /** CONTROLLED: the step the member is on (0-based). Steps before it are done, after it are next. The APP moves it — when a step's
10
+ * work is really finished (the code checked by the server, the two PINs match), `setAt(at + 1)`; after the last step the app sends. `at = count` = every step done —
11
+ * the app then closes its Sheet (`state="away"`, a beat after the fold) and says so on the page: the steps never close it themselves. */
12
+ at?: number;
13
+ /** UNCONTROLLED: the step it starts on (default 0) */
14
+ defaultAt?: number;
15
+ /** the steps ASK to move — a tap on another step, a swipe, a skip. Controlled: set `at` to it (or don't). */
16
+ onAtChange?: (at: number, reason: SheetStepsReason) => void;
17
+ /** true: the member may go back and forth — a tap on a done or next step opens it, an up/down swipe moves one step.
18
+ * false (default): only forward — the done steps stay in the pile, never opened again (a swipe up passes only a `skippable` step). */
19
+ reversible?: boolean;
20
+ }
21
+ /**
22
+ * SheetSteps — one task taken in steps INSIDE one sheet (phone · code · PIN · PIN again · face · notices). The done steps pile up
23
+ * on top, each one line (title · summary · ✓); the current step is open under them, its form at the form's own spacing; the next
24
+ * steps wait below as their titles. A thin rule over every step. The last step's own action sends, or a `finish` stands under the
25
+ * steps on the last one. Goes in a `SheetBody`.
26
+ * The sheet may be dragged down to peek and back — the body keeps where it was scrolled.
27
+ */
28
+ export declare function SheetSteps({ at, defaultAt, onAtChange, reversible, finish, className, style, children, onFocus, onBlur, onPointerDown, onPointerUp, onPointerCancel, ...rest }: SheetStepsProps): import("react").JSX.Element;
29
+ export interface SheetStepProps extends Omit<ComponentProps<'li'>, 'title'> {
30
+ /** the step's name — always shown: over the open step, on the folded line of a done one */
31
+ title: ReactNode;
32
+ /** the done step's one-line answer ("081-xxx-4567", "set") — shown on its folded line */
33
+ summary?: ReactNode;
34
+ /** the member may pass this step without doing it: a small link at its end moves on (reason `skip`) */
35
+ skippable?: boolean;
36
+ /** the skip link's words (default "Skip") */
37
+ skipLabel?: ReactNode;
38
+ /** what went wrong, in words — one line UNDER the step's button (an alert icon, `--signal-error`, read out at once): the number
39
+ * is not a phone number, no signal, the code could not be sent, the two PINs differ. The step stays where it is; to send the
40
+ * member back, the app sets `at` (and gives THAT step the error). Mark the wrong control with its own `error` (Field, Pin). */
41
+ error?: ReactNode;
42
+ /** the step is waiting for the answer — one line under the button: a turning ring + these words ("sending the code…"); the
43
+ * step's buttons take no taps meanwhile. The app moves `at` when the answer is good, or sets `error`. */
44
+ busy?: ReactNode;
45
+ }
46
+ /** SheetStep — one step of a SheetSteps: its title, then what the member does (a Field, a Pin, a Toggle, the step's own Btn). */
47
+ export declare function SheetStep({ title, summary, skippable, skipLabel, error, busy, className, children, ...rest }: SheetStepProps): import("react").JSX.Element;
@@ -0,0 +1,244 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Children, cloneElement, createContext, isValidElement, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
+ import { cx } from '../cx.js';
4
+ import { Icon } from '../type/Icon.js';
5
+ import { Link } from '../actions/Link.js';
6
+ import { FormNote } from '../forms/FormNote.js';
7
+ import { CheckContext } from '../forms/check.js';
8
+ const BACK_MS = 3000; // a PIN that does not match: its red digits and words stay this long, then the step goes back
9
+ const MOVE_MS = 350; // = --motion-sheet: the accordion's beat
10
+ const GLIDE_MS = 220; // the pile's slide out of the keyboard's way and back: quick, but the eye follows it
11
+ const SWIPE_REM = 3; // an up/down move longer than this (and mostly up/down) is a swipe
12
+ const StepCtx = createContext(null);
13
+ /**
14
+ * SheetSteps — one task taken in steps INSIDE one sheet (phone · code · PIN · PIN again · face · notices). The done steps pile up
15
+ * on top, each one line (title · summary · ✓); the current step is open under them, its form at the form's own spacing; the next
16
+ * steps wait below as their titles. A thin rule over every step. The last step's own action sends, or a `finish` stands under the
17
+ * steps on the last one. Goes in a `SheetBody`.
18
+ * The sheet may be dragged down to peek and back — the body keeps where it was scrolled.
19
+ */
20
+ export function SheetSteps({ at, defaultAt = 0, onAtChange, reversible = false, finish, className, style, children, onFocus, onBlur, onPointerDown, onPointerUp, onPointerCancel, ...rest }) {
21
+ const [inner, setInner] = useState(defaultAt);
22
+ const current = at ?? inner;
23
+ const steps = Children.toArray(children).filter(isValidElement);
24
+ const count = steps.length;
25
+ /* how far the member has come: a step before it was DONE (its work, or skipped) — it stays done when they go back to look */
26
+ const [furthest, setFurthest] = useState(current);
27
+ if (current > furthest)
28
+ setFurthest(current);
29
+ const reach = Math.max(furthest, current);
30
+ const ref = useRef(null);
31
+ const first = useRef(true);
32
+ const holderRef = useRef(null);
33
+ const moving = useRef(0); // until then a step change is playing its beats: focus moves do not scroll
34
+ const shown = useRef(current);
35
+ if (shown.current !== current) {
36
+ shown.current = current;
37
+ moving.current = performance.now() + MOVE_MS + 100;
38
+ } // from the very render that changes it: the old field's blur and the keyboard's jump come in that same commit
39
+ const press = useRef(null);
40
+ const go = (i, reason) => {
41
+ if (i === current || i < 0 || i > count)
42
+ return;
43
+ if (at === undefined)
44
+ setInner(i);
45
+ onAtChange?.(i, reason);
46
+ };
47
+ /* how far the body is scrolled when a step opens or a field in it takes the keyboard: the TWO latest done rows always show
48
+ above the open step, and half of the one before — older ones go up out of sight, one row at a time, so what was done never seems to vanish (Lh
49
+ 2026-09-24: moving the pile further startled; with two rows or fewer nothing moves) */
50
+ const settle = () => {
51
+ const list = ref.current, body = list?.closest('.sheet__body'), now = list?.querySelector('.sheet-step--now');
52
+ if (!list || !body || !now)
53
+ return;
54
+ const above = [];
55
+ for (let el = now.previousElementSibling; el; el = el.previousElementSibling)
56
+ above.unshift(el);
57
+ const bTop = body.getBoundingClientRect().top, at = (el) => body.scrollTop + el.getBoundingClientRect().top - bTop;
58
+ const third = above.length > 2 ? above[above.length - 3] : null; // 2½ rows show (Lh 2026-09-24): the two latest whole, the one before cut in half — a sign there is more above
59
+ let to = third ? at(third) + third.getBoundingClientRect().height / 2 : 0;
60
+ /* …unless the field with the focus would still be under the cut (a short phone, a big text size): then up as far as it needs —
61
+ but never past the LATEST done row, which always stays in sight */
62
+ const focused = document.activeElement instanceof HTMLElement && now.contains(document.activeElement) ? document.activeElement : null;
63
+ if (focused) {
64
+ const vv = window.visualViewport;
65
+ const cut = Math.min(body.getBoundingClientRect().bottom - (parseFloat(getComputedStyle(body).scrollPaddingBottom) || 0), vv ? vv.offsetTop + vv.height : Infinity);
66
+ const under = (focused.closest('.field, .pin') ?? focused).getBoundingClientRect().bottom + (body.scrollTop - to) - cut; // where its bottom will be once scrolled to `to`
67
+ const most = above.length ? at(above[above.length - 1]) : 0;
68
+ if (under > 0)
69
+ to = Math.min(to + under, Math.max(most, to));
70
+ }
71
+ if (Math.abs(to - body.scrollTop) <= 1)
72
+ return;
73
+ // the pile SCROLLS there, animated (Lh 2026-09-24) — the browser's own smooth scroll; its frames are not replayed as jumps
74
+ const smooth = !matchMedia('(prefers-reduced-motion: reduce)').matches;
75
+ own.current = performance.now() + (smooth ? 700 : 0);
76
+ body.scrollTo({ top: to, behavior: smooth ? 'smooth' : 'auto' });
77
+ };
78
+ /* the step changed, in TWO beats one right after the other: (1) the old step folds while the new one unfolds — nothing else
79
+ moves; (2) then the pile settles (the two latest done rows in sight), gliding. The cursor goes into the new step at once
80
+ (so a phone's keyboard stays up) but without scrolling. Not on arrival — the body keeps its scroll. */
81
+ /* the open step's first field is the one to take the cursor when the sheet comes onto the screen (Sheet reads `data-autofocus`) */
82
+ useLayoutEffect(() => {
83
+ ref.current?.querySelectorAll('[data-autofocus]').forEach((el) => el.removeAttribute('data-autofocus'));
84
+ ref.current?.querySelector('.sheet-step--now .sheet-step__inner :is(input:not([type=checkbox], [type=radio], [type=hidden]), textarea)')?.setAttribute('data-autofocus', '');
85
+ });
86
+ useLayoutEffect(() => {
87
+ if (first.current) {
88
+ first.current = false;
89
+ return;
90
+ }
91
+ // ONE THING AT A TIME (Lh 2026-09-24 — the old field and the new one fought over the cursor and the pile bounced):
92
+ // (1) the old field lets go — blur, at once; (2) the fold plays alone; (3) then the cursor goes into the new step and the
93
+ // pile settles, together, right after the fold. `moving` (set by the render) holds every jump until then.
94
+ // The cursor waits in a HOLDER while the fold plays — an invisible text field of the steps' own: the phone's keyboard stays up
95
+ // (a blur would drop it and the next focus bring it back — Lh 2026-09-24), and it takes the keyboard type of the field to come
96
+ const at = document.activeElement, holder = holderRef.current;
97
+ const nextField = ref.current?.querySelector('.sheet-step--now .sheet-step__inner :is(input:not([type=checkbox], [type=radio], [type=hidden]), textarea)');
98
+ if (at instanceof HTMLElement && ref.current?.contains(at)) {
99
+ if (holder && nextField && at.matches('input, textarea')) {
100
+ holder.inputMode = nextField.inputMode || 'text';
101
+ holder.value = '';
102
+ holder.focus({ preventScroll: true });
103
+ }
104
+ else
105
+ at.blur();
106
+ }
107
+ const t = setTimeout(() => {
108
+ const now = ref.current?.querySelector('.sheet-step--now');
109
+ // the cursor stands by: the step's first field takes the focus (the keyboard comes with it); a step with no field — its
110
+ // title, so a screen reader hears where it is
111
+ (now?.querySelector('.sheet-step__inner :is(input:not([type=checkbox], [type=radio], [type=hidden]), textarea)') ?? now?.querySelector('.sheet-step__title'))?.focus({ preventScroll: true });
112
+ glide.current = performance.now() + 900;
113
+ settle();
114
+ }, MOVE_MS);
115
+ return () => clearTimeout(t);
116
+ }, [current]);
117
+ /* the keyboard comes and goes: the body's scroll JUMPS (the Sheet's reveal, the browser's own, ours below, the box growing back).
118
+ For a short while after a field in the steps gains or loses the focus, every such jump is played as a quick slide (FLIP: the steps
119
+ are set back by the jump, then eased to rest) so the eye can follow the pile moving out of the way and back. */
120
+ const glide = useRef(0), last = useRef(0), own = useRef(0); // own: our smooth scroll is playing — not a jump to replay
121
+ useEffect(() => {
122
+ const el = ref.current, body = el?.closest('.sheet__body');
123
+ if (!el || !body)
124
+ return;
125
+ last.current = body.scrollTop;
126
+ const onScroll = () => {
127
+ const d = body.scrollTop - last.current;
128
+ last.current = body.scrollTop;
129
+ if (!d || performance.now() > glide.current || performance.now() < own.current || matchMedia('(prefers-reduced-motion: reduce)').matches)
130
+ return;
131
+ const from = parseFloat(el.style.translate.split(' ')[1] || '0') || 0;
132
+ el.style.transition = 'none';
133
+ el.style.translate = `0 ${from + d}px`;
134
+ void el.offsetHeight; // the set-back lands before the ease
135
+ const hold = Math.max(0, moving.current - 100 - performance.now()); // during a step change: held until the fold is done (beat 2)
136
+ el.style.transition = `translate ${GLIDE_MS}ms ease-out ${hold}ms`;
137
+ el.style.translate = '0 0';
138
+ };
139
+ const clean = () => { el.style.removeProperty('transition'); el.style.removeProperty('translate'); };
140
+ body.addEventListener('scroll', onScroll);
141
+ el.addEventListener('transitionend', clean);
142
+ return () => { body.removeEventListener('scroll', onScroll); el.removeEventListener('transitionend', clean); };
143
+ }, []);
144
+ /* a field in the current step took the focus: once the keyboard is up (the Sheet has climbed), settle the pile — the two latest
145
+ done rows in sight. When the focus leaves the fields (the keyboard goes), the pile comes back down. Both moves glide. */
146
+ const typing = (t) => t instanceof HTMLElement && t.matches('input:not([type=checkbox], [type=radio], [type=range]), textarea');
147
+ const focusIn = (e) => {
148
+ onFocus?.(e);
149
+ glide.current = performance.now() + 900;
150
+ const target = e.target;
151
+ if (!target.closest('.sheet-step--now') || !typing(target) || performance.now() < moving.current - 100)
152
+ return; // a step change settles on its own beat
153
+ setTimeout(settle, 300); // after the keyboard and the sheet's own climb
154
+ };
155
+ const focusOut = (e) => {
156
+ onBlur?.(e);
157
+ if (!typing(e.target) || performance.now() < moving.current)
158
+ return; // the old step's field losing the cursor to the new one
159
+ glide.current = performance.now() + 900;
160
+ const body = ref.current?.closest('.sheet__body');
161
+ setTimeout(() => { if (!typing(document.activeElement) && body)
162
+ body.scrollTo({ top: 0 }); }, 0); // Tab to the next field keeps it up
163
+ };
164
+ /* an up/down swipe turns the wheel one step — unless it started in content that can still scroll that way */
165
+ const down = (e) => {
166
+ onPointerDown?.(e);
167
+ press.current = e.isPrimary && !e.target.closest('input, textarea, select, .pin, .picker, .slider') ? { x: e.clientX, y: e.clientY } : null;
168
+ };
169
+ const up = (e) => {
170
+ onPointerUp?.(e);
171
+ const p = press.current;
172
+ press.current = null;
173
+ if (!p)
174
+ return;
175
+ const dx = e.clientX - p.x, dy = e.clientY - p.y;
176
+ if (Math.abs(dy) < SWIPE_REM * parseFloat(getComputedStyle(document.documentElement).fontSize) || Math.abs(dy) < 2 * Math.abs(dx))
177
+ return;
178
+ const body = e.target.closest('.sheet-step__body');
179
+ if (body && (dy < 0 ? body.scrollTop + body.clientHeight < body.scrollHeight - 1 : body.scrollTop > 0))
180
+ return; // the content's own scroll
181
+ const to = current + (dy < 0 ? 1 : -1);
182
+ if (reversible && to < count)
183
+ go(to, 'swipe'); // a swipe never passes the last step — only its own action or its skip does
184
+ else if (dy < 0 && ref.current?.querySelector('.sheet-step--now[data-skippable]'))
185
+ go(to, 'skip'); // forward only, and only past a step that may be skipped
186
+ };
187
+ return (_jsxs("div", { ref: ref, className: cx('sheet-steps', !reversible && 'sheet-steps--locked', className), style: style, onFocus: focusIn, onBlur: focusOut, onPointerDown: down, onPointerUp: up, onPointerCancel: (e) => { onPointerCancel?.(e); press.current = null; }, ...rest, children: [_jsx("input", { ref: holderRef, className: "sheet-steps__holder", "aria-hidden": true, tabIndex: -1, autoComplete: "off", onInput: (e) => { e.currentTarget.value = ''; } }), _jsx("ol", { className: "sheet-steps__track", children: steps.map((step, i) => (_jsx(StepCtx.Provider, { value: { i, at: current, furthest: reach, count, reversible, go }, children: step }, step.key ?? i))) }), finish != null && ( // always in sight; it takes taps only once every step is done (or skipped)
188
+ _jsx("div", { className: "sheet-steps__finish", children: isValidElement(finish) ? cloneElement(finish, { disabled: reach < count || finish.props.disabled }) : finish }))] }));
189
+ }
190
+ /** SheetStep — one step of a SheetSteps: its title, then what the member does (a Field, a Pin, a Toggle, the step's own Btn). */
191
+ export function SheetStep({ title, summary, skippable, skipLabel = 'Skip', error, busy, className, children, ...rest }) {
192
+ const ctx = useContext(StepCtx);
193
+ if (!ctx)
194
+ throw new Error('SheetStep goes inside a SheetSteps');
195
+ const { i, at, furthest, count, reversible, go } = ctx;
196
+ const phase = i === at ? 'now' : i < furthest ? 'done' : 'next';
197
+ const off = phase !== 'now';
198
+ /* the step is the FORM GROUP of the format checks inside it (Field format/digits/required, Pin match): the words of the first
199
+ control that fails show under the button; a press on a button of the step checks them all first and holds the press back
200
+ when one fails (the app's onClick never hears it); a Pin that does not match shows its words for three seconds, then steps back */
201
+ const goRef = useRef(go);
202
+ goRef.current = go;
203
+ const [note, setNote] = useState(null);
204
+ const checks = useRef(new Map());
205
+ const first = () => { for (const w of checks.current.values())
206
+ if (w != null)
207
+ return w; return null; };
208
+ const group = useMemo(() => ({
209
+ add: (c) => { checks.current.set(c, null); return () => { checks.current.delete(c); setNote(first()); }; },
210
+ say: (c, w) => { checks.current.set(c, w); setNote(first()); },
211
+ back: () => setTimeout(() => goRef.current(i - 1, 'mismatch'), BACK_MS),
212
+ }), [i]);
213
+ useEffect(() => { if (off) {
214
+ for (const c of checks.current.keys())
215
+ checks.current.set(c, null);
216
+ setNote(null);
217
+ } }, [off]);
218
+ /* a press on the step's button keeps the cursor where it is: the button still clicks, but the field keeps the focus, so a
219
+ phone's keyboard does not drop and come back (the pile jumped up and down with it — Lh 2026-09-24) */
220
+ const keepCursor = (e) => {
221
+ const at = document.activeElement;
222
+ if (e.target.closest('.btn') && at instanceof HTMLElement && at.matches('input, textarea') && e.currentTarget.contains(at))
223
+ e.preventDefault();
224
+ };
225
+ const press = (e) => {
226
+ const btn = e.target.closest('.btn');
227
+ if (!btn || btn.disabled)
228
+ return;
229
+ let bad = null;
230
+ for (const c of checks.current.keys()) {
231
+ const w = c.check(true);
232
+ checks.current.set(c, w);
233
+ if (w != null && !bad)
234
+ bad = c;
235
+ }
236
+ setNote(first());
237
+ if (bad) {
238
+ e.stopPropagation();
239
+ e.preventDefault();
240
+ bad.focus();
241
+ }
242
+ };
243
+ return (_jsxs("li", { className: cx('sheet-step', `sheet-step--${phase}`, busy != null && busy !== false && 'sheet-step--busy', className), "aria-current": phase === 'now' ? 'step' : undefined, "aria-busy": busy != null && busy !== false ? true : undefined, "data-skippable": skippable || undefined, ...rest, children: [_jsxs("div", { className: "sheet-step__head", inert: off || undefined, children: [_jsx("h3", { className: "sheet-step__title", tabIndex: -1, children: title }), phase === 'done' && summary != null && _jsx("span", { className: "sheet-step__summary t-caption t-muted", children: summary }), phase === 'done' && _jsx(Icon, { name: "check", size: "xs", className: "sheet-step__check" })] }), _jsx("div", { className: "sheet-step__body", children: _jsxs("div", { className: "sheet-step__inner", inert: off || undefined, onClickCapture: press, onPointerDown: keepCursor, children: [_jsx(CheckContext.Provider, { value: group, children: children }), _jsx(FormNote, { error: error ?? note, busy: busy }), skippable && _jsx(Link, { sm: true, muted: true, className: "sheet-step__skip", onClick: () => go(i + 1, 'skip'), children: skipLabel })] }) }), reversible && off && (_jsx("button", { type: "button", className: "sheet-step__go", "aria-label": typeof title === 'string' ? title : `${i + 1} / ${count}`, onClick: () => go(i, 'tap') }))] }));
244
+ }
@@ -1,4 +1,4 @@
1
- export type IconName = "arrow-left" | "arrow-right" | "arrow-up" | "arrow-up-right" | "bell" | "briefcase" | "calendar-days" | "car" | "check" | "chevron-down" | "chevron-left" | "chevron-right" | "chevron-up" | "circle-help" | "clock" | "droplet" | "ellipsis" | "file-text" | "gift" | "handshake" | "heart" | "hourglass" | "house" | "id-card" | "image" | "layout-grid" | "lightbulb" | "log-out" | "luggage" | "map" | "megaphone" | "menu" | "message-circle" | "mic" | "music" | "package" | "pencil" | "percent" | "phone-call" | "plane" | "play" | "plus" | "search" | "share" | "shopping-bag" | "siren" | "sliders" | "star" | "store" | "sun" | "thermometer" | "ticket" | "trash" | "truck" | "upload" | "user" | "users" | "wallet" | "x";
1
+ export type IconName = "arrow-left" | "arrow-right" | "arrow-up" | "arrow-up-right" | "bell" | "briefcase" | "calendar-days" | "car" | "check" | "chevron-down" | "chevron-left" | "chevron-right" | "chevron-up" | "circle-alert" | "circle-help" | "clock" | "droplet" | "ellipsis" | "file-text" | "gift" | "handshake" | "heart" | "hourglass" | "house" | "id-card" | "image" | "layout-grid" | "lightbulb" | "loader-circle" | "log-out" | "luggage" | "map" | "megaphone" | "menu" | "message-circle" | "mic" | "music" | "package" | "pencil" | "percent" | "phone-call" | "plane" | "play" | "plus" | "search" | "share" | "shopping-bag" | "siren" | "sliders" | "star" | "store" | "sun" | "thermometer" | "ticket" | "trash" | "truck" | "upload" | "user" | "users" | "wallet" | "x";
2
2
  export declare const ICON_NAMES: readonly IconName[];
3
3
  export declare const ICONS: Record<IconName, {
4
4
  attrs: Record<string, string>;
@@ -1,2 +1,2 @@
1
- export const ICON_NAMES = ["arrow-left", "arrow-right", "arrow-up", "arrow-up-right", "bell", "briefcase", "calendar-days", "car", "check", "chevron-down", "chevron-left", "chevron-right", "chevron-up", "circle-help", "clock", "droplet", "ellipsis", "file-text", "gift", "handshake", "heart", "hourglass", "house", "id-card", "image", "layout-grid", "lightbulb", "log-out", "luggage", "map", "megaphone", "menu", "message-circle", "mic", "music", "package", "pencil", "percent", "phone-call", "plane", "play", "plus", "search", "share", "shopping-bag", "siren", "sliders", "star", "store", "sun", "thermometer", "ticket", "trash", "truck", "upload", "user", "users", "wallet", "x"];
2
- export const ICONS = { "arrow-left": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m12 19-7-7 7-7\" /><path d=\"M19 12H5\" />" }, "arrow-right": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M5 12h14\"/><path d=\"m12 5 7 7-7 7\"/>" }, "arrow-up": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m5 12 7-7 7 7\"/><path d=\"M12 19V5\"/>" }, "arrow-up-right": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M7 7h10v10\" /><path d=\"M7 17 17 7\" />" }, "bell": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M10.268 21a2 2 0 0 0 3.464 0\" /><path d=\"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326\" />" }, "briefcase": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16\" /><rect width=\"20\" height=\"14\" x=\"2\" y=\"6\" rx=\"2\" />" }, "calendar-days": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M8 2v4\" /><path d=\"M16 2v4\" /><rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\" /><path d=\"M3 10h18\" /><path d=\"M8 14h.01\" /><path d=\"M12 14h.01\" /><path d=\"M16 14h.01\" /><path d=\"M8 18h.01\" /><path d=\"M12 18h.01\" /><path d=\"M16 18h.01\" />" }, "car": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2\"/><circle cx=\"7\" cy=\"17\" r=\"2\"/><path d=\"M9 17h6\"/><circle cx=\"17\" cy=\"17\" r=\"2\"/>" }, "check": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M20 6 9 17l-5-5\"/>" }, "chevron-down": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m6 9 6 6 6-6\" />" }, "chevron-left": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m15 18-6-6 6-6\"/>" }, "chevron-right": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m9 18 6-6-6-6\"/>" }, "chevron-up": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m18 15-6-6-6 6\"/>" }, "circle-help": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\"/><path d=\"M12 17h.01\"/>" }, "clock": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M12 6v6l4 2\" /><circle cx=\"12\" cy=\"12\" r=\"10\" />" }, "droplet": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z\"/>" }, "ellipsis": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/>" }, "file-text": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/><path d=\"M10 9H8\"/><path d=\"M16 13H8\"/><path d=\"M16 17H8\"/>" }, "gift": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<rect x=\"3\" y=\"8\" width=\"18\" height=\"4\" rx=\"1\"/><path d=\"M12 8v13\"/><path d=\"M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7\"/><path d=\"M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5\"/>" }, "handshake": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m11 17 2 2a1 1 0 1 0 3-3\"/><path d=\"m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 4\"/><path d=\"m21 3 1 11h-2\"/><path d=\"M3 3 2 14l6.5 6.5a1 1 0 1 0 3-3\"/><path d=\"M3 4h8\"/>" }, "heart": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z\" />" }, "hourglass": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M5 22h14\"/><path d=\"M5 2h14\"/><path d=\"M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22\"/><path d=\"M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2\"/>" }, "house": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\"/><path d=\"M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"/>" }, "id-card": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M16 10h2\"/><path d=\"M16 14h2\"/><path d=\"M6.17 15a3 3 0 0 1 5.66 0\"/><circle cx=\"9\" cy=\"11\" r=\"2\"/><rect x=\"2\" y=\"5\" width=\"20\" height=\"14\" rx=\"2\"/>" }, "image": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\"/><circle cx=\"9\" cy=\"9\" r=\"2\"/><path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\"/>" }, "layout-grid": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<rect width=\"7\" height=\"7\" x=\"3\" y=\"3\" rx=\"1\"/><rect width=\"7\" height=\"7\" x=\"14\" y=\"3\" rx=\"1\"/><rect width=\"7\" height=\"7\" x=\"14\" y=\"14\" rx=\"1\"/><rect width=\"7\" height=\"7\" x=\"3\" y=\"14\" rx=\"1\"/>" }, "lightbulb": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1.3.5 2.6 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/>" }, "log-out": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"/><polyline points=\"16 17 21 12 16 7\"/><line x1=\"21\" x2=\"9\" y1=\"12\" y2=\"12\"/>" }, "luggage": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M6 20a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2\" /><path d=\"M8 18V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v14\" /><path d=\"M10 20h4\" /><circle cx=\"16\" cy=\"20\" r=\"2\" /><circle cx=\"8\" cy=\"20\" r=\"2\" />" }, "map": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z\" /><path d=\"M15 5.764v15\" /><path d=\"M9 3.236v15\" />" }, "megaphone": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m3 11 18-5v12L3 14v-3z\"/><path d=\"M11.6 16.8a3 3 0 1 1-5.8-1.6\"/>" }, "menu": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M4 12h16\" /><path d=\"M4 18h16\" /><path d=\"M4 6h16\" />" }, "message-circle": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M7.9 20A9 9 0 1 0 4 16.1L2 22Z\"/>" }, "mic": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z\"/><path d=\"M19 10v2a7 7 0 0 1-14 0v-2\"/><line x1=\"12\" x2=\"12\" y1=\"19\" y2=\"22\"/>" }, "music": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M9 18V5l12-2v13\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/><circle cx=\"18\" cy=\"16\" r=\"3\"/>" }, "package": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m7.5 4.27 9 5.15\"/><path d=\"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z\"/><path d=\"m3.3 7 8.7 5 8.7-5\"/><path d=\"M12 22V12\"/>" }, "pencil": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z\"/><path d=\"m15 5 4 4\"/>" }, "percent": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<line x1=\"19\" x2=\"5\" y1=\"5\" y2=\"19\" /><circle cx=\"6.5\" cy=\"6.5\" r=\"2.5\" /><circle cx=\"17.5\" cy=\"17.5\" r=\"2.5\" />" }, "phone-call": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M13 2a9 9 0 0 1 9 9\" /><path d=\"M13 6a5 5 0 0 1 5 5\" /><path d=\"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384\" />" }, "plane": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z\" />" }, "play": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<polygon points=\"6 3 20 12 6 21 6 3\"/>" }, "plus": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M5 12h14\" /><path d=\"M12 5v14\" />" }, "search": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/>" }, "share": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8\"/><polyline points=\"16 6 12 2 8 6\"/><line x1=\"12\" x2=\"12\" y1=\"2\" y2=\"15\"/>" }, "shopping-bag": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M16 10a4 4 0 0 1-8 0\" /><path d=\"M3.103 6.034h17.794\" /><path d=\"M3.4 5.467a2 2 0 0 0-.4 1.2V20a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.667a2 2 0 0 0-.4-1.2l-2-2.667A2 2 0 0 0 17 2H7a2 2 0 0 0-1.6.8z\" />" }, "siren": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M7 18v-6a5 5 0 1 1 10 0v6\" /><path d=\"M5 21a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-1a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2z\" /><path d=\"M21 12h1\" /><path d=\"M18.5 4.5 18 5\" /><path d=\"M2 12h1\" /><path d=\"M12 2v1\" /><path d=\"m4.929 4.929.707.707\" /><path d=\"M12 12v6\" />" }, "sliders": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<line x1=\"21\" x2=\"14\" y1=\"4\" y2=\"4\"/><line x1=\"10\" x2=\"3\" y1=\"4\" y2=\"4\"/><line x1=\"21\" x2=\"12\" y1=\"12\" y2=\"12\"/><line x1=\"8\" x2=\"3\" y1=\"12\" y2=\"12\"/><line x1=\"21\" x2=\"16\" y1=\"20\" y2=\"20\"/><line x1=\"12\" x2=\"3\" y1=\"20\" y2=\"20\"/><line x1=\"14\" x2=\"14\" y1=\"2\" y2=\"6\"/><line x1=\"8\" x2=\"8\" y1=\"10\" y2=\"14\"/><line x1=\"16\" x2=\"16\" y1=\"18\" y2=\"22\"/>" }, "star": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<polygon points=\"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2\"/>" }, "store": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7\" /><path d=\"M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8\" /><path d=\"M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4\" /><path d=\"M2 7h20\" /><path d=\"M22 7v3a2 2 0 0 1-2 2a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12a2 2 0 0 1-2-2V7\" />" }, "sun": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2\"/><path d=\"M12 20v2\"/><path d=\"m4.93 4.93 1.41 1.41\"/><path d=\"m17.66 17.66 1.41 1.41\"/><path d=\"M2 12h2\"/><path d=\"M20 12h2\"/><path d=\"m6.34 17.66-1.41 1.41\"/><path d=\"m19.07 4.93-1.41 1.41\"/>" }, "thermometer": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z\"/>" }, "ticket": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z\" /><path d=\"M13 5v2\" /><path d=\"M13 17v2\" /><path d=\"M13 11v2\" />" }, "trash": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M3 6h18\"/><path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\"/><path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\"/><line x1=\"10\" x2=\"10\" y1=\"11\" y2=\"17\"/><line x1=\"14\" x2=\"14\" y1=\"11\" y2=\"17\"/>" }, "truck": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2\"/><path d=\"M15 18H9\"/><path d=\"M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14\"/><circle cx=\"17\" cy=\"18\" r=\"2\"/><circle cx=\"7\" cy=\"18\" r=\"2\"/>" }, "upload": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><polyline points=\"17 8 12 3 7 8\"/><line x1=\"12\" x2=\"12\" y1=\"3\" y2=\"15\"/>" }, "user": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/>" }, "users": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/><path d=\"M22 21v-2a4 4 0 0 0-3-3.87\"/><path d=\"M16 3.13a4 4 0 0 1 0 7.75\"/>" }, "wallet": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1\"/><path d=\"M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4\"/>" }, "x": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />" } };
1
+ export const ICON_NAMES = ["arrow-left", "arrow-right", "arrow-up", "arrow-up-right", "bell", "briefcase", "calendar-days", "car", "check", "chevron-down", "chevron-left", "chevron-right", "chevron-up", "circle-alert", "circle-help", "clock", "droplet", "ellipsis", "file-text", "gift", "handshake", "heart", "hourglass", "house", "id-card", "image", "layout-grid", "lightbulb", "loader-circle", "log-out", "luggage", "map", "megaphone", "menu", "message-circle", "mic", "music", "package", "pencil", "percent", "phone-call", "plane", "play", "plus", "search", "share", "shopping-bag", "siren", "sliders", "star", "store", "sun", "thermometer", "ticket", "trash", "truck", "upload", "user", "users", "wallet", "x"];
2
+ export const ICONS = { "arrow-left": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m12 19-7-7 7-7\" /><path d=\"M19 12H5\" />" }, "arrow-right": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M5 12h14\"/><path d=\"m12 5 7 7-7 7\"/>" }, "arrow-up": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m5 12 7-7 7 7\"/><path d=\"M12 19V5\"/>" }, "arrow-up-right": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M7 7h10v10\" /><path d=\"M7 17 17 7\" />" }, "bell": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M10.268 21a2 2 0 0 0 3.464 0\" /><path d=\"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326\" />" }, "briefcase": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16\" /><rect width=\"20\" height=\"14\" x=\"2\" y=\"6\" rx=\"2\" />" }, "calendar-days": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M8 2v4\" /><path d=\"M16 2v4\" /><rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\" /><path d=\"M3 10h18\" /><path d=\"M8 14h.01\" /><path d=\"M12 14h.01\" /><path d=\"M16 14h.01\" /><path d=\"M8 18h.01\" /><path d=\"M12 18h.01\" /><path d=\"M16 18h.01\" />" }, "car": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2\"/><circle cx=\"7\" cy=\"17\" r=\"2\"/><path d=\"M9 17h6\"/><circle cx=\"17\" cy=\"17\" r=\"2\"/>" }, "check": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M20 6 9 17l-5-5\"/>" }, "chevron-down": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m6 9 6 6 6-6\" />" }, "chevron-left": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m15 18-6-6 6-6\"/>" }, "chevron-right": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m9 18 6-6-6-6\"/>" }, "chevron-up": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m18 15-6-6-6 6\"/>" }, "circle-alert": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<circle cx=\"12\" cy=\"12\" r=\"10\" /><line x1=\"12\" x2=\"12\" y1=\"8\" y2=\"12\" /><line x1=\"12\" x2=\"12.01\" y1=\"16\" y2=\"16\" />" }, "circle-help": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\"/><path d=\"M12 17h.01\"/>" }, "clock": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M12 6v6l4 2\" /><circle cx=\"12\" cy=\"12\" r=\"10\" />" }, "droplet": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z\"/>" }, "ellipsis": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/>" }, "file-text": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/><path d=\"M10 9H8\"/><path d=\"M16 13H8\"/><path d=\"M16 17H8\"/>" }, "gift": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<rect x=\"3\" y=\"8\" width=\"18\" height=\"4\" rx=\"1\"/><path d=\"M12 8v13\"/><path d=\"M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7\"/><path d=\"M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5\"/>" }, "handshake": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m11 17 2 2a1 1 0 1 0 3-3\"/><path d=\"m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 4\"/><path d=\"m21 3 1 11h-2\"/><path d=\"M3 3 2 14l6.5 6.5a1 1 0 1 0 3-3\"/><path d=\"M3 4h8\"/>" }, "heart": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z\" />" }, "hourglass": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M5 22h14\"/><path d=\"M5 2h14\"/><path d=\"M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22\"/><path d=\"M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2\"/>" }, "house": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\"/><path d=\"M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"/>" }, "id-card": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M16 10h2\"/><path d=\"M16 14h2\"/><path d=\"M6.17 15a3 3 0 0 1 5.66 0\"/><circle cx=\"9\" cy=\"11\" r=\"2\"/><rect x=\"2\" y=\"5\" width=\"20\" height=\"14\" rx=\"2\"/>" }, "image": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\"/><circle cx=\"9\" cy=\"9\" r=\"2\"/><path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\"/>" }, "layout-grid": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<rect width=\"7\" height=\"7\" x=\"3\" y=\"3\" rx=\"1\"/><rect width=\"7\" height=\"7\" x=\"14\" y=\"3\" rx=\"1\"/><rect width=\"7\" height=\"7\" x=\"14\" y=\"14\" rx=\"1\"/><rect width=\"7\" height=\"7\" x=\"3\" y=\"14\" rx=\"1\"/>" }, "lightbulb": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1.3.5 2.6 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/>" }, "loader-circle": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />" }, "log-out": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"/><polyline points=\"16 17 21 12 16 7\"/><line x1=\"21\" x2=\"9\" y1=\"12\" y2=\"12\"/>" }, "luggage": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M6 20a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2\" /><path d=\"M8 18V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v14\" /><path d=\"M10 20h4\" /><circle cx=\"16\" cy=\"20\" r=\"2\" /><circle cx=\"8\" cy=\"20\" r=\"2\" />" }, "map": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z\" /><path d=\"M15 5.764v15\" /><path d=\"M9 3.236v15\" />" }, "megaphone": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m3 11 18-5v12L3 14v-3z\"/><path d=\"M11.6 16.8a3 3 0 1 1-5.8-1.6\"/>" }, "menu": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M4 12h16\" /><path d=\"M4 18h16\" /><path d=\"M4 6h16\" />" }, "message-circle": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M7.9 20A9 9 0 1 0 4 16.1L2 22Z\"/>" }, "mic": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z\"/><path d=\"M19 10v2a7 7 0 0 1-14 0v-2\"/><line x1=\"12\" x2=\"12\" y1=\"19\" y2=\"22\"/>" }, "music": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M9 18V5l12-2v13\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/><circle cx=\"18\" cy=\"16\" r=\"3\"/>" }, "package": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m7.5 4.27 9 5.15\"/><path d=\"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z\"/><path d=\"m3.3 7 8.7 5 8.7-5\"/><path d=\"M12 22V12\"/>" }, "pencil": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z\"/><path d=\"m15 5 4 4\"/>" }, "percent": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<line x1=\"19\" x2=\"5\" y1=\"5\" y2=\"19\" /><circle cx=\"6.5\" cy=\"6.5\" r=\"2.5\" /><circle cx=\"17.5\" cy=\"17.5\" r=\"2.5\" />" }, "phone-call": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M13 2a9 9 0 0 1 9 9\" /><path d=\"M13 6a5 5 0 0 1 5 5\" /><path d=\"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384\" />" }, "plane": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z\" />" }, "play": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<polygon points=\"6 3 20 12 6 21 6 3\"/>" }, "plus": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M5 12h14\" /><path d=\"M12 5v14\" />" }, "search": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/>" }, "share": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8\"/><polyline points=\"16 6 12 2 8 6\"/><line x1=\"12\" x2=\"12\" y1=\"2\" y2=\"15\"/>" }, "shopping-bag": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M16 10a4 4 0 0 1-8 0\" /><path d=\"M3.103 6.034h17.794\" /><path d=\"M3.4 5.467a2 2 0 0 0-.4 1.2V20a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.667a2 2 0 0 0-.4-1.2l-2-2.667A2 2 0 0 0 17 2H7a2 2 0 0 0-1.6.8z\" />" }, "siren": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M7 18v-6a5 5 0 1 1 10 0v6\" /><path d=\"M5 21a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-1a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2z\" /><path d=\"M21 12h1\" /><path d=\"M18.5 4.5 18 5\" /><path d=\"M2 12h1\" /><path d=\"M12 2v1\" /><path d=\"m4.929 4.929.707.707\" /><path d=\"M12 12v6\" />" }, "sliders": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<line x1=\"21\" x2=\"14\" y1=\"4\" y2=\"4\"/><line x1=\"10\" x2=\"3\" y1=\"4\" y2=\"4\"/><line x1=\"21\" x2=\"12\" y1=\"12\" y2=\"12\"/><line x1=\"8\" x2=\"3\" y1=\"12\" y2=\"12\"/><line x1=\"21\" x2=\"16\" y1=\"20\" y2=\"20\"/><line x1=\"12\" x2=\"3\" y1=\"20\" y2=\"20\"/><line x1=\"14\" x2=\"14\" y1=\"2\" y2=\"6\"/><line x1=\"8\" x2=\"8\" y1=\"10\" y2=\"14\"/><line x1=\"16\" x2=\"16\" y1=\"18\" y2=\"22\"/>" }, "star": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<polygon points=\"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2\"/>" }, "store": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7\" /><path d=\"M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8\" /><path d=\"M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4\" /><path d=\"M2 7h20\" /><path d=\"M22 7v3a2 2 0 0 1-2 2a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12a2 2 0 0 1-2-2V7\" />" }, "sun": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2\"/><path d=\"M12 20v2\"/><path d=\"m4.93 4.93 1.41 1.41\"/><path d=\"m17.66 17.66 1.41 1.41\"/><path d=\"M2 12h2\"/><path d=\"M20 12h2\"/><path d=\"m6.34 17.66-1.41 1.41\"/><path d=\"m19.07 4.93-1.41 1.41\"/>" }, "thermometer": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z\"/>" }, "ticket": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z\" /><path d=\"M13 5v2\" /><path d=\"M13 17v2\" /><path d=\"M13 11v2\" />" }, "trash": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M3 6h18\"/><path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\"/><path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\"/><line x1=\"10\" x2=\"10\" y1=\"11\" y2=\"17\"/><line x1=\"14\" x2=\"14\" y1=\"11\" y2=\"17\"/>" }, "truck": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2\"/><path d=\"M15 18H9\"/><path d=\"M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14\"/><circle cx=\"17\" cy=\"18\" r=\"2\"/><circle cx=\"7\" cy=\"18\" r=\"2\"/>" }, "upload": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><polyline points=\"17 8 12 3 7 8\"/><line x1=\"12\" x2=\"12\" y1=\"3\" y2=\"15\"/>" }, "user": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/>" }, "users": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/><path d=\"M22 21v-2a4 4 0 0 0-3-3.87\"/><path d=\"M16 3.13a4 4 0 0 1 0 7.75\"/>" }, "wallet": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1\"/><path d=\"M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4\"/>" }, "x": { "attrs": { "viewBox": "0 0 24 24", "fill": "none", "stroke": "currentColor", "strokeWidth": "2", "strokeLinecap": "round", "strokeLinejoin": "round" }, "body": "<path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />" } };
package/icons.svg CHANGED
@@ -60,4 +60,6 @@
60
60
  <symbol id="gift" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="8" width="18" height="4" rx="1"/><path d="M12 8v13"/><path d="M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7"/><path d="M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5"/></symbol>
61
61
  <symbol id="id-card" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 10h2"/><path d="M16 14h2"/><path d="M6.17 15a3 3 0 0 1 5.66 0"/><circle cx="9" cy="11" r="2"/><rect x="2" y="5" width="20" height="14" rx="2"/></symbol>
62
62
  <symbol id="handshake" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m11 17 2 2a1 1 0 1 0 3-3"/><path d="m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 4"/><path d="m21 3 1 11h-2"/><path d="M3 3 2 14l6.5 6.5a1 1 0 1 0 3-3"/><path d="M3 4h8"/></symbol>
63
+ <symbol id="circle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <line x1="12" x2="12" y1="8" y2="12" /> <line x1="12" x2="12.01" y1="16" y2="16" /></symbol>
64
+ <symbol id="loader-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></symbol>
63
65
  </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "card-first mobile design system, React-first: the components in src/ are thin wrappers over the CSS contract (css/*.css stays the only truth); gallery/ shows every story live (npm run dev), tests/ measures the geometry.",