@usableapp/cardds 0.7.4 → 0.7.6

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,41 @@
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.6 — 2026-09-24
6
+
7
+ ### Changed
8
+ - The bottom layer of a `Screen fill` (the `ActionBar` — a Walkthrough's dots, controls, CTA row and link) now lies on
9
+ the screen's surface. Before: it had no ground, so long words scrolling under it mid-scroll showed through and could
10
+ not be read. Knob `--action-bar-bg` (default `var(--surface)`; `transparent` = the old see-through).
11
+ - `SheetSteps`: the cursor holder field's size is `1rem` (was 16px); no raw px left in sheet.css.
12
+
13
+ ## 0.7.5 — 2026-09-24
14
+
15
+ ### Errors in a form (new)
16
+ - `--signal-error` (#c62828) — the ONE colour the wireframe has; always with an icon and words, never the colour alone.
17
+ A theme may set it.
18
+ - `Field error` — the edge turns red, `aria-invalid`, and the field takes the focus with ALL its text selected (type
19
+ straight over it). `Pin error` — every cell red (edge + digit), the wrong digits stay in sight, the cursor goes back to
20
+ the first cell; the next digit starts a new code.
21
+ - `SheetStep error` / `busy` — ONE line under the step's button: an alert icon + words (`role="alert"`), or a turning
22
+ ring + words ("sending…", the step's buttons take no taps). The words are the app's.
23
+ - FORMAT checks on the client (never content — content errors come from the app through `error`): `Field format="email"
24
+ | "tel"`, `digits={n}`, `required`; `Pin match={firstPin}`; `formatError="…"` = the app's words. Checked when a field is
25
+ left and when the step's button is pressed — never while typing; a failing one holds the press back (the app's onClick
26
+ is not called). A Pin that does not match shows red for 3 s, then the steps go back one (`onAtChange(at, 'mismatch')` —
27
+ clear both codes there).
28
+ - Icons `circle-alert`, `loader-circle`.
29
+
30
+ ### Changed
31
+ - `Pin` — a typed digit is coloured words (`--pin-cell-filled-ink`, default the accent), no filled pill.
32
+ - `SheetSteps` — an ACCORDION: the old step folds while the new one opens, then the done pile scrolls (smoothly) so
33
+ 2½ done rows show above the open step; a step change never drops a phone's keyboard (the cursor waits in a holder
34
+ field while the step folds); step titles at the label size (`--sheet-step-title-font`); a step stays ✓ when the
35
+ member goes back; `finish` is always shown, disabled until every step is done or skipped; the steps never close the
36
+ sheet — when `at` reaches the count, the app sets its Sheet `away`.
37
+ - `Sheet` — on arrival the cursor goes to a `[data-autofocus]` inside it (SheetSteps marks its open step's first field),
38
+ else the handle, once the sheet has finished sliding in.
39
+
5
40
  ## 0.7.4 — 2026-09-24
6
41
 
7
42
  ### 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
@@ -380,18 +380,24 @@
380
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
381
  falls — no height is written. Nothing fades: done and next read at full ink. */
382
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; }
383
+ .sheet-steps { flex-shrink: 0; min-height: calc(100% + var(--kb, 0rem)); /* 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: 1rem; } /* holds the cursor (and the phone's keyboard) while a step folds; 1rem (16px at the browser's size) so iOS does not zoom to it */
385
+ .sheet-steps { position: relative; }
384
386
  .sheet-steps__track { list-style: none; margin: 0; padding: 0; flex: 1; display: flex; flex-direction: column; }
385
- .sheet-step--now + .sheet-step { margin-top: auto; } /* the steps not done yet wait in a pile at the bottom, clear of the one being done */
387
+
386
388
  .sheet-step {
387
389
  position: relative; min-width: 0;
388
- display: grid; grid-template-rows: auto 0fr;
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) */
389
391
  border-top: var(--rule-w) solid var(--sheet-step-border, var(--hairline));
390
- transition: grid-template-rows var(--motion-sheet) ease;
392
+ flex: 0 0 auto;
393
+ transition: grid-template-rows var(--motion-sheet) ease, flex-grow var(--motion-sheet) ease;
391
394
  }
392
- .sheet-step--now { grid-template-rows: auto 1fr; }
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; }
393
399
  .sheet-step__head { display: flex; align-items: center; gap: var(--sp-2); min-height: var(--sheet-steps-row-h, var(--tap-sm)); }
394
- .sheet-step__title { flex: 0 1 auto; min-width: 0; outline: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
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; }
395
401
  .sheet-step__summary { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
396
402
  .sheet-step__check { margin-inline-start: auto; }
397
403
  .sheet-step__body { min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
@@ -403,3 +409,5 @@
403
409
  /* the last step's finish, when the app gives one: under the steps */
404
410
  .sheet-steps__finish { display: grid; padding-top: var(--card-gap); }
405
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/step.css CHANGED
@@ -120,7 +120,11 @@
120
120
  position: absolute; inset-inline: 0; top: auto; bottom: var(--kb, 0rem);
121
121
  max-width: none;
122
122
  margin: 0;
123
- padding-top: 0; /* no ground of its own, never a solid strip */
123
+ padding-top: 0;
124
+ /* its GROUND (Lh 2026-09-24, timebank card 141 — was "no ground of its own"): words that run under the layer mid-scroll
125
+ showed through the dots, the controls and the buttons. The layer lies on the screen's own surface now; a project sets
126
+ another (or `transparent`, the old see-through) with the knob */
127
+ background: var(--action-bar-bg, var(--surface));
124
128
  }
125
129
  /* the walk's dots are the BOTTOM layer's too (Lh 2026-09-23): a row of the bar, over its buttons, centred */
126
130
  .action-bar > .pager__at, .action-bar__tier > .pager__at { align-self: center; }
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) */
@@ -1556,18 +1557,24 @@ button { font: inherit; cursor: pointer; }
1556
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
1557
1558
  falls — no height is written. Nothing fades: done and next read at full ink. */
1558
1559
  .sheet__body:has(> .sheet-steps) { display: flex; flex-direction: column; }
1559
- .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; }
1560
+ .sheet-steps { flex-shrink: 0; min-height: calc(100% + var(--kb, 0rem)); /* 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: 1rem; } /* holds the cursor (and the phone's keyboard) while a step folds; 1rem (16px at the browser's size) so iOS does not zoom to it */
1562
+ .sheet-steps { position: relative; }
1560
1563
  .sheet-steps__track { list-style: none; margin: 0; padding: 0; flex: 1; display: flex; flex-direction: column; }
1561
- .sheet-step--now + .sheet-step { margin-top: auto; } /* the steps not done yet wait in a pile at the bottom, clear of the one being done */
1564
+
1562
1565
  .sheet-step {
1563
1566
  position: relative; min-width: 0;
1564
- display: grid; grid-template-rows: auto 0fr;
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) */
1565
1568
  border-top: var(--rule-w) solid var(--sheet-step-border, var(--hairline));
1566
- transition: grid-template-rows var(--motion-sheet) ease;
1569
+ flex: 0 0 auto;
1570
+ transition: grid-template-rows var(--motion-sheet) ease, flex-grow var(--motion-sheet) ease;
1567
1571
  }
1568
- .sheet-step--now { grid-template-rows: auto 1fr; }
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; }
1569
1576
  .sheet-step__head { display: flex; align-items: center; gap: var(--sp-2); min-height: var(--sheet-steps-row-h, var(--tap-sm)); }
1570
- .sheet-step__title { flex: 0 1 auto; min-width: 0; outline: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
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; }
1571
1578
  .sheet-step__summary { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1572
1579
  .sheet-step__check { margin-inline-start: auto; }
1573
1580
  .sheet-step__body { min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
@@ -1579,6 +1586,8 @@ button { font: inherit; cursor: pointer; }
1579
1586
  /* the last step's finish, when the app gives one: under the steps */
1580
1587
  .sheet-steps__finish { display: grid; padding-top: var(--card-gap); }
1581
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; }
1582
1591
 
1583
1592
  /* ---- css/step.css — .step: one screen, one task — card, optional sheet, bar outside ---- */
1584
1593
  /* ============================================================
@@ -1703,7 +1712,11 @@ button { font: inherit; cursor: pointer; }
1703
1712
  position: absolute; inset-inline: 0; top: auto; bottom: var(--kb, 0rem);
1704
1713
  max-width: none;
1705
1714
  margin: 0;
1706
- padding-top: 0; /* no ground of its own, never a solid strip */
1715
+ padding-top: 0;
1716
+ /* its GROUND (Lh 2026-09-24, timebank card 141 — was "no ground of its own"): words that run under the layer mid-scroll
1717
+ showed through the dots, the controls and the buttons. The layer lies on the screen's own surface now; a project sets
1718
+ another (or `transparent`, the old see-through) with the knob */
1719
+ background: var(--action-bar-bg, var(--surface));
1707
1720
  }
1708
1721
  /* the walk's dots are the BOTTOM layer's too (Lh 2026-09-23): a row of the bar, over its buttons, centred */
1709
1722
  .action-bar > .pager__at, .action-bar__tier > .pager__at { align-self: center; }
@@ -1810,6 +1823,18 @@ button { font: inherit; cursor: pointer; }
1810
1823
  }
1811
1824
  .add-row__plus .icon { width: var(--icon-xs); height: var(--icon-xs); }
1812
1825
 
1826
+ /* ---- a wrong value, and the line that says so (Lh 2026-09-24) ----
1827
+ --signal-error is the one colour the wireframe has; it never speaks alone — the line carries an icon and words.
1828
+ <p class="form-note form-note--error" role="alert"><svg class="icon icon--xs">…</svg>Not a phone number</p>
1829
+ <p class="form-note form-note--busy" role="status"><svg …/>Sending the code…</p> (the ring turns) */
1830
+ .field.field--error, [data-palette] .field.field--error { border-color: var(--field-error-border, var(--signal-error)); }
1831
+ .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 */
1832
+ .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; }
1833
+ .form-note > .icon { flex: none; }
1834
+ .form-note--error { color: var(--form-note-error-ink, var(--signal-error)); }
1835
+ @media (prefers-reduced-motion: no-preference) { .form-note--busy > .icon { animation: form-note-turn 0.9s linear infinite; } }
1836
+ @keyframes form-note-turn { to { rotate: 1turn; } }
1837
+
1813
1838
  /* ---- css/journey.css — route, tile badge, note row ---- */
1814
1839
  /* ============================================================
1815
1840
  cardds/journey.css — trip/status primitives inside cards:
@@ -2774,7 +2799,7 @@ button { font: inherit; cursor: pointer; }
2774
2799
  font: var(--pin-cell-font, var(--type-stat));
2775
2800
  font-variant-numeric: tabular-nums;
2776
2801
  }
2777
- .pin__cell:not(:placeholder-shown) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
2802
+ .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) */
2778
2803
  /* 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
2779
2804
  edge. The sign is INSIDE the pill: a full-ink border and a blinking bar drawn where the digit will land (the native caret
2780
2805
  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
+ }
@@ -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 */
@@ -1,11 +1,14 @@
1
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 */
3
- export type SheetStepsReason = 'tap' | 'swipe' | 'skip';
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';
4
5
  export interface SheetStepsProps extends ComponentProps<'div'> {
5
- /** optional: what stands under the steps on the LAST step — a `Btn` that finishes. None = the last step's own action sends. */
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. */
6
8
  finish?: ReactNode;
7
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
8
- * 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. */
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. */
9
12
  at?: number;
10
13
  /** UNCONTROLLED: the step it starts on (default 0) */
11
14
  defaultAt?: number;
@@ -32,6 +35,13 @@ export interface SheetStepProps extends Omit<ComponentProps<'li'>, 'title'> {
32
35
  skippable?: boolean;
33
36
  /** the skip link's words (default "Skip") */
34
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;
35
45
  }
36
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). */
37
- export declare function SheetStep({ title, summary, skippable, skipLabel, className, children, ...rest }: SheetStepProps): import("react").JSX.Element;
47
+ export declare function SheetStep({ title, summary, skippable, skipLabel, error, busy, className, children, ...rest }: SheetStepProps): import("react").JSX.Element;
@@ -1,8 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Children, createContext, isValidElement, useContext, useEffect, useRef, useState } from 'react';
2
+ import { Children, cloneElement, createContext, isValidElement, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
3
  import { cx } from '../cx.js';
4
4
  import { Icon } from '../type/Icon.js';
5
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
6
10
  const GLIDE_MS = 220; // the pile's slide out of the keyboard's way and back: quick, but the eye follows it
7
11
  const SWIPE_REM = 3; // an up/down move longer than this (and mostly up/down) is a swipe
8
12
  const StepCtx = createContext(null);
@@ -18,8 +22,20 @@ export function SheetSteps({ at, defaultAt = 0, onAtChange, reversible = false,
18
22
  const current = at ?? inner;
19
23
  const steps = Children.toArray(children).filter(isValidElement);
20
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);
21
30
  const ref = useRef(null);
22
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
23
39
  const press = useRef(null);
24
40
  const go = (i, reason) => {
25
41
  if (i === current || i < 0 || i > count)
@@ -28,24 +44,80 @@ export function SheetSteps({ at, defaultAt = 0, onAtChange, reversible = false,
28
44
  setInner(i);
29
45
  onAtChange?.(i, reason);
30
46
  };
31
- /* the step changed: put the cursor in it, and once the pile has moved, bring the step
32
- into view in the body. Not on arrival — the body keeps its scroll. */
33
- useEffect(() => {
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(() => {
34
87
  if (first.current) {
35
88
  first.current = false;
36
89
  return;
37
90
  }
38
- const now = ref.current?.querySelector('.sheet-step--now');
39
- // the cursor stands by: the step's first field takes the focus (the keyboard comes with it); a step with no field — its title,
40
- // so a screen reader hears where it is
41
- (now?.querySelector('.sheet-step__inner :is(input:not([type=checkbox], [type=radio], [type=hidden]), textarea)') ?? now?.querySelector('.sheet-step__title'))?.focus({ preventScroll: true });
42
- const t = setTimeout(() => now?.scrollIntoView({ block: 'nearest', behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' }), 350); // = --motion-sheet
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);
43
115
  return () => clearTimeout(t);
44
116
  }, [current]);
45
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).
46
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
47
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. */
48
- const glide = useRef(0), last = useRef(0);
120
+ const glide = useRef(0), last = useRef(0), own = useRef(0); // own: our smooth scroll is playing — not a jump to replay
49
121
  useEffect(() => {
50
122
  const el = ref.current, body = el?.closest('.sheet__body');
51
123
  if (!el || !body)
@@ -54,13 +126,14 @@ export function SheetSteps({ at, defaultAt = 0, onAtChange, reversible = false,
54
126
  const onScroll = () => {
55
127
  const d = body.scrollTop - last.current;
56
128
  last.current = body.scrollTop;
57
- if (!d || performance.now() > glide.current || matchMedia('(prefers-reduced-motion: reduce)').matches)
129
+ if (!d || performance.now() > glide.current || performance.now() < own.current || matchMedia('(prefers-reduced-motion: reduce)').matches)
58
130
  return;
59
131
  const from = parseFloat(el.style.translate.split(' ')[1] || '0') || 0;
60
132
  el.style.transition = 'none';
61
133
  el.style.translate = `0 ${from + d}px`;
62
134
  void el.offsetHeight; // the set-back lands before the ease
63
- el.style.transition = `translate ${GLIDE_MS}ms ease-out`;
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`;
64
137
  el.style.translate = '0 0';
65
138
  };
66
139
  const clean = () => { el.style.removeProperty('transition'); el.style.removeProperty('translate'); };
@@ -68,30 +141,21 @@ export function SheetSteps({ at, defaultAt = 0, onAtChange, reversible = false,
68
141
  el.addEventListener('transitionend', clean);
69
142
  return () => { body.removeEventListener('scroll', onScroll); el.removeEventListener('transitionend', clean); };
70
143
  }, []);
71
- /* a field in the current step took the focus: once the keyboard is up (the Sheet has climbed), bring the field to EYE LEVEL — its middle
72
- at the human centre (--screen-centre, 40%) of what shows between the sheet's head and the keyboard; the done pile moves up for it
73
- (never down past its own top). When the focus
74
- leaves the fields (the keyboard goes), the pile comes back down. Both moves glide. */
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. */
75
146
  const typing = (t) => t instanceof HTMLElement && t.matches('input:not([type=checkbox], [type=radio], [type=range]), textarea');
76
147
  const focusIn = (e) => {
77
148
  onFocus?.(e);
78
149
  glide.current = performance.now() + 900;
79
- const target = e.target, now = target.closest('.sheet-step--now'), body = ref.current?.closest('.sheet__body');
80
- if (!now || !body || !typing(target))
81
- return;
82
- setTimeout(() => {
83
- const control = (target.closest('.field, .pin') ?? target).getBoundingClientRect(), vv = window.visualViewport;
84
- const cut = Math.min(body.getBoundingClientRect().bottom - (parseFloat(getComputedStyle(body).scrollPaddingBottom) || 0), vv ? vv.offsetTop + vv.height : Infinity);
85
- const top = body.getBoundingClientRect().top, eye = top + (cut - top) * ((parseFloat(getComputedStyle(body).getPropertyValue('--screen-centre')) || 40) / 100);
86
- const by = Math.max((control.top + control.bottom) / 2 - eye, -body.scrollTop); // the field's middle at eye level — the human centre of what shows between the head and the keyboard
87
- if (Math.abs(by) > 1)
88
- body.scrollBy({ top: by });
89
- }, 300); // after the keyboard and the sheet's own climb
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
90
154
  };
91
155
  const focusOut = (e) => {
92
156
  onBlur?.(e);
93
- if (!typing(e.target))
94
- return;
157
+ if (!typing(e.target) || performance.now() < moving.current)
158
+ return; // the old step's field losing the cursor to the new one
95
159
  glide.current = performance.now() + 900;
96
160
  const body = ref.current?.closest('.sheet__body');
97
161
  setTimeout(() => { if (!typing(document.activeElement) && body)
@@ -115,20 +179,66 @@ export function SheetSteps({ at, defaultAt = 0, onAtChange, reversible = false,
115
179
  if (body && (dy < 0 ? body.scrollTop + body.clientHeight < body.scrollHeight - 1 : body.scrollTop > 0))
116
180
  return; // the content's own scroll
117
181
  const to = current + (dy < 0 ? 1 : -1);
118
- if (reversible)
119
- go(to, 'swipe');
182
+ if (reversible && to < count)
183
+ go(to, 'swipe'); // a swipe never passes the last step — only its own action or its skip does
120
184
  else if (dy < 0 && ref.current?.querySelector('.sheet-step--now[data-skippable]'))
121
185
  go(to, 'skip'); // forward only, and only past a step that may be skipped
122
186
  };
123
- 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("ol", { className: "sheet-steps__track", children: steps.map((step, i) => (_jsx(StepCtx.Provider, { value: { i, at: current, count, reversible, go }, children: step }, step.key ?? i))) }), finish != null && current === count - 1 && _jsx("div", { className: "sheet-steps__finish", children: finish })] }));
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 }))] }));
124
189
  }
125
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). */
126
- export function SheetStep({ title, summary, skippable, skipLabel = 'Skip', className, children, ...rest }) {
191
+ export function SheetStep({ title, summary, skippable, skipLabel = 'Skip', error, busy, className, children, ...rest }) {
127
192
  const ctx = useContext(StepCtx);
128
193
  if (!ctx)
129
194
  throw new Error('SheetStep goes inside a SheetSteps');
130
- const { i, at, count, reversible, go } = ctx;
131
- const phase = i < at ? 'done' : i === at ? 'now' : 'next';
195
+ const { i, at, furthest, count, reversible, go } = ctx;
196
+ const phase = i === at ? 'now' : i < furthest ? 'done' : 'next';
132
197
  const off = phase !== 'now';
133
- return (_jsxs("li", { className: cx('sheet-step', `sheet-step--${phase}`, className), "aria-current": phase === 'now' ? 'step' : undefined, "data-skippable": skippable || undefined, ...rest, children: [_jsxs("div", { className: "sheet-step__head", inert: off || undefined, children: [_jsx("h3", { className: "sheet-step__title t-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, children: [children, 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') }))] }));
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') }))] }));
134
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.4",
3
+ "version": "0.7.6",
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.",