@usableapp/cardds 0.7.16 → 0.7.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +23 -20
  3. package/cardds.css +3 -3
  4. package/css/actions.css +60 -38
  5. package/css/base.css +43 -17
  6. package/css/card.css +18 -13
  7. package/css/choice.css +21 -33
  8. package/css/forms.css +0 -3
  9. package/css/journey.css +0 -2
  10. package/css/layover.css +14 -24
  11. package/css/lists.css +1 -6
  12. package/css/media.css +7 -15
  13. package/css/numbers.css +5 -21
  14. package/css/people.css +1 -8
  15. package/css/sheet.css +12 -14
  16. package/css/stack.css +17 -21
  17. package/css/step.css +7 -8
  18. package/css/tokens.css +19 -4
  19. package/dist/actions/Btn.d.ts +1 -1
  20. package/dist/actions/Dropdown.js +1 -6
  21. package/dist/actions/Fab.d.ts +1 -1
  22. package/dist/actions/Fab.js +3 -2
  23. package/dist/cardds.css +230 -232
  24. package/dist/cards/Card.d.ts +23 -7
  25. package/dist/cards/Card.js +23 -8
  26. package/dist/cards/CardHead.d.ts +0 -1
  27. package/dist/cards/CardHead.js +1 -1
  28. package/dist/choice/Calendar.js +1 -2
  29. package/dist/choice/Mood.js +2 -2
  30. package/dist/forms/Field.js +2 -3
  31. package/dist/forms/FileBtn.js +1 -1
  32. package/dist/forms/Pin.js +6 -10
  33. package/dist/index.js +2 -2
  34. package/dist/layover/Banner.d.ts +10 -3
  35. package/dist/layover/Banner.js +6 -3
  36. package/dist/lists/Row.js +3 -3
  37. package/dist/media/Postcard.js +2 -2
  38. package/dist/numbers/Band.d.ts +3 -1
  39. package/dist/numbers/Picker.js +1 -7
  40. package/dist/scaffold/ActionBar.d.ts +3 -3
  41. package/dist/scaffold/ActionBar.js +2 -2
  42. package/dist/scaffold/Centre.d.ts +1 -1
  43. package/dist/scaffold/Centre.js +1 -1
  44. package/dist/scaffold/PagerAt.d.ts +1 -1
  45. package/dist/scaffold/PagerAt.js +1 -1
  46. package/dist/scaffold/Screen.js +2 -4
  47. package/dist/sheets/Drawer.js +3 -4
  48. package/dist/sheets/Sheet.js +8 -19
  49. package/dist/sheets/SheetStack.d.ts +2 -2
  50. package/dist/sheets/SheetStack.js +18 -10
  51. package/dist/sheets/SheetSteps.js +5 -7
  52. package/dist/sheets/StackBack.js +2 -1
  53. package/dist/sheets/StackSheet.d.ts +3 -2
  54. package/dist/sheets/StackSheet.js +6 -9
  55. package/dist/state.d.ts +19 -2
  56. package/dist/state.js +47 -7
  57. package/dist/templates/CallSheet.js +1 -2
  58. package/dist/templates/Home.d.ts +3 -2
  59. package/dist/templates/Home.js +1 -2
  60. package/dist/templates/Splash.js +2 -3
  61. package/dist/templates/Walkthrough.js +2 -2
  62. package/dist/type/Text.d.ts +1 -1
  63. package/package.json +7 -4
@@ -18,7 +18,7 @@ const StepCtx = createContext(null);
18
18
  * The sheet may be dragged down to peek and back — the body keeps where it was scrolled.
19
19
  */
20
20
  export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible = false, numbered = false, finish, className, style, children, onFocus, onBlur, onPointerDown, onPointerUp, onPointerCancel, ...rest }) {
21
- const [current, setOwn] = useControlled(at, defaultAt);
21
+ const [current, request] = useControlled(at, defaultAt, onAtChange);
22
22
  const steps = Children.toArray(children).filter(isValidElement);
23
23
  const count = steps.length;
24
24
  /* 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 */
@@ -39,10 +39,8 @@ export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible
39
39
  } // from the very render that changes it: the old field's blur and the keyboard's jump come in that same commit
40
40
  const press = useRef(null);
41
41
  const go = (i, reason) => {
42
- if (i === current || i < 0 || i > count)
43
- return;
44
- setOwn(i);
45
- onAtChange?.(i, reason);
42
+ if (i >= 0 && i <= count)
43
+ request(i, reason);
46
44
  };
47
45
  /* 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
46
  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
@@ -220,7 +218,7 @@ export function SheetStep({ title, summary, skippable, skipLabel = 'Skip', error
220
218
  e.preventDefault();
221
219
  };
222
220
  const press = (e) => {
223
- const btn = e.target.closest('.btn');
221
+ const btn = e.target.closest('button.btn'); // a FileBtn is a label.btn: picking a file is not the step's press
224
222
  if (!btn || btn.disabled)
225
223
  return;
226
224
  let bad = null;
@@ -242,7 +240,7 @@ export function SheetStep({ title, summary, skippable, skipLabel = 'Skip', error
242
240
  const enter = (e) => {
243
241
  if (e.key !== 'Enter' || e.nativeEvent.isComposing || !(e.target instanceof HTMLInputElement))
244
242
  return;
245
- const btn = e.currentTarget.querySelector('.btn:not(:disabled)');
243
+ const btn = e.currentTarget.querySelector('button.btn:not(:disabled)');
246
244
  if (btn) {
247
245
  e.preventDefault();
248
246
  btn.click();
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
3
  import { Icon } from '../type/Icon.js';
4
+ import { IconBtn } from '../actions/IconBtn.js';
4
5
  /** StackBack — the back button a tapped stack sheet reveals in its head; first child of that sheet's CardHead. Hidden until the sheet is open.
5
6
  * A SMALL disc (Lh 2026-09-23): 32px with a chevron — the head keeps its room for the title and the tabs; the 44px tap target stays. */
6
7
  export function StackBack({ label = 'Back', className, ...rest }) {
7
- return (_jsx("button", { type: "button", className: cx('icon-btn icon-btn--sm sheet-stack__back', className), "aria-label": label, ...rest, children: _jsx(Icon, { name: "chevron-left" }) }));
8
+ return _jsx(IconBtn, { sm: true, label: label, className: cx('sheet-stack__back', className), ...rest, children: _jsx(Icon, { name: "chevron-left" }) });
8
9
  }
@@ -1,4 +1,5 @@
1
1
  import { type ComponentProps, type ReactNode } from 'react';
2
+ import { type Tone } from '../cards/Card.js';
2
3
  import { type IconName } from '../type/Icon.js';
3
4
  export interface StackSheetTab {
4
5
  id: string;
@@ -11,8 +12,8 @@ export interface StackSheetTab {
11
12
  export interface StackSheetProps extends Omit<ComponentProps<'article'>, 'title'> {
12
13
  /** the SHEET HEAD's title — an h2 (the page's h1 is its own) */
13
14
  title: ReactNode;
14
- /** the paper: 1 light · 2 tinted · 3 dark */
15
- tone?: 1 | 2 | 3 | 4 | 5;
15
+ /** the paper — see `Tone` */
16
+ tone?: Tone;
16
17
  /** the head's end when the sheet has no tabs: an Icon, an IconRow */
17
18
  icon?: ReactNode;
18
19
  /** TABS in the head: icon buttons at the end — the sheet's pages. A tap on one in a peeked head opens the sheet on it */
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef } from 'react';
3
- import { useControlled } from '../state.js';
3
+ import { useControlled, watchSize } from '../state.js';
4
4
  import { cx } from '../cx.js';
5
5
  import { Card } from '../cards/Card.js';
6
+ import { IconBtn } from '../actions/IconBtn.js';
6
7
  import { CardHead } from '../cards/CardHead.js';
7
8
  import { CardFoot } from '../cards/CardFoot.js';
8
9
  import { SheetBody } from './SheetBody.js';
@@ -17,14 +18,13 @@ import { Badge } from '../numbers/Badge.js';
17
18
  * is closed — see `Home`). A direct child of `SheetStack`.
18
19
  */
19
20
  export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel = 'Back', className, children, ...rest }) {
20
- const [tab, setOwn] = useControlled(tabProp, defaultTab ?? tabs?.[0]?.id);
21
- const pick = (id) => { setOwn(id); onTabChange?.(id); };
21
+ const [tab, pick] = useControlled(tabProp, defaultTab ?? tabs?.[0]?.id, onTabChange);
22
22
  // a title longer than its room does not wrap: it runs sideways, like a song title in a short slot — the room it overflows by
23
23
  // is measured here (the tabs may grow with the text size) and handed to the CSS as --_over
24
24
  const titleRef = useRef(null);
25
25
  useEffect(() => {
26
26
  const h = titleRef.current, run = h?.firstElementChild;
27
- if (!h || !run || typeof ResizeObserver === 'undefined')
27
+ if (!h || !run)
28
28
  return;
29
29
  const measure = () => {
30
30
  const over = Math.max(0, Math.ceil(run.scrollWidth - h.clientWidth));
@@ -32,11 +32,8 @@ export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab,
32
32
  h.style.setProperty('--_run', `${(over / 30 + 3).toFixed(1)}s`);
33
33
  h.classList.toggle('is-long', over > 0);
34
34
  };
35
- const ro = new ResizeObserver(measure);
36
- ro.observe(h);
37
- ro.observe(run);
38
35
  measure();
39
- return () => ro.disconnect();
36
+ return watchSize([h, run], measure);
40
37
  }, [title]);
41
- return (_jsxs(Card, { tone: tone, className: cx('stack-sheet', className), ...rest, children: [_jsxs(CardHead, { children: [_jsx(StackBack, { label: backLabel }), _jsx(Text, { level: "h2", ref: titleRef, className: "stack-sheet__title", children: _jsx("span", { className: "stack-sheet__run", children: title }) }), tabs?.length ? (_jsx("span", { className: "stack-sheet__tabs", role: "tablist", children: tabs.map((t) => (_jsxs("button", { type: "button", role: "tab", "aria-selected": t.id === tab, "aria-label": t.label, className: cx('icon-btn icon-btn--sm stack-sheet__tab', t.id === tab && 'icon-btn--invert'), onClick: () => pick(t.id), children: [_jsx(Icon, { name: t.icon }), t.count ? _jsx(Badge, { className: "stack-sheet__count", children: t.count > 99 ? '99+' : t.count }) : null] }, t.id))) })) : icon] }), _jsx(SheetBody, { role: tabs?.length ? 'tabpanel' : undefined, children: children }), foot != null && _jsx(CardFoot, { children: foot })] }));
38
+ return (_jsxs(Card, { tone: tone, className: cx('stack-sheet', className), ...rest, children: [_jsxs(CardHead, { children: [_jsx(StackBack, { label: backLabel }), _jsx(Text, { level: "h2", ref: titleRef, className: "stack-sheet__title", children: _jsx("span", { className: "stack-sheet__run", children: title }) }), tabs?.length ? (_jsx("span", { className: "stack-sheet__tabs", role: "tablist", children: tabs.map((t) => (_jsxs(IconBtn, { sm: true, invert: t.id === tab, role: "tab", "aria-selected": t.id === tab, label: t.label, className: "stack-sheet__tab", onClick: () => pick(t.id), children: [_jsx(Icon, { name: t.icon }), t.count ? _jsx(Badge, { className: "stack-sheet__count", children: t.count > 99 ? '99+' : t.count }) : null] }, t.id))) })) : icon] }), _jsx(SheetBody, { role: tabs?.length ? 'tabpanel' : undefined, children: children }), foot != null && _jsx(CardFoot, { children: foot })] }));
42
39
  }
package/dist/state.d.ts CHANGED
@@ -1,7 +1,24 @@
1
+ import { type SyntheticEvent } from 'react';
1
2
  /** controlled like an input: `value` given = the app owns it; undefined = the component keeps its own, starting at `initial`.
2
- * Returns the value shown and a setter that only moves the component's own copy — the caller still ASKS the app (onXChange). */
3
- export declare function useControlled<T>(value: T | undefined, initial: T): [T, (next: T) => void];
3
+ * Returns the value shown and `request(next, …extra)`: nothing when `next` is what shows; else it moves the component's own copy
4
+ * (uncontrolled only) and ASKS the app — `onChange(next, …extra)`. `request` keeps one identity for the component's life. */
5
+ type Asks<T, A extends unknown[]> = {
6
+ ask(next: T, ...extra: A): void;
7
+ }['ask'];
8
+ export declare function useControlled<T, A extends unknown[] = []>(value: T | undefined, initial: T, onChange?: Asks<T, A>): readonly [T, (next: T, ...extra: A) => void];
9
+ /** a native `<details>` controlled like an input: `open` + `onOpenChange`, or `defaultOpen`. Spread the result on the details.
10
+ * The browser opens it on a tap by itself — a controlled one is put back until the app says so. */
11
+ export declare function useDisclosure(open: boolean | undefined, defaultOpen?: boolean, onOpenChange?: (open: boolean) => void): {
12
+ open: boolean;
13
+ onToggle: (e: SyntheticEvent<HTMLDetailsElement>) => void;
14
+ };
15
+ /** the latest value in a ref — for a handler that a timer, an observer or a listener set up once calls later */
16
+ export declare function useLatest<T>(value: T): import("react").RefObject<T>;
17
+ /** watch boxes for a size change (the keyboard, a font arriving, a wrap): `fn` on each — once at the start too, as the browser
18
+ * reports. Returns the stop, for an effect's cleanup; nothing is watched where the browser has no ResizeObserver. */
19
+ export declare function watchSize(targets: Array<Element | null | undefined>, fn: () => void): () => void;
4
20
  /** a dev hint said once per key, never again */
5
21
  export declare const warnOnce: (key: string, text: string) => void;
6
22
  /** the member asked for less motion (false where there is no window, e.g. a server render) */
7
23
  export declare const reducedMotion: () => boolean;
24
+ export {};
package/dist/state.js CHANGED
@@ -1,11 +1,51 @@
1
- import { useState } from 'react';
2
- /** controlled like an input: `value` given = the app owns it; undefined = the component keeps its own, starting at `initial`.
3
- * Returns the value shown and a setter that only moves the component's own copy — the caller still ASKS the app (onXChange). */
4
- export function useControlled(value, initial) {
1
+ import { useCallback, useRef, useState } from 'react';
2
+ export function useControlled(value, initial, onChange) {
5
3
  const [own, setOwn] = useState(initial);
6
- const controlled = value !== undefined;
7
- return [controlled ? value : own, (next) => { if (!controlled)
8
- setOwn(next); }];
4
+ const shown = value !== undefined ? value : own;
5
+ const live = useRef({ controlled: value !== undefined, shown, onChange });
6
+ live.current = { controlled: value !== undefined, shown, onChange };
7
+ const request = useCallback((next, ...extra) => {
8
+ const { controlled, shown, onChange } = live.current;
9
+ if (Object.is(next, shown))
10
+ return;
11
+ if (!controlled)
12
+ setOwn(next);
13
+ onChange?.(next, ...extra);
14
+ }, []);
15
+ return [shown, request];
16
+ }
17
+ /** a native `<details>` controlled like an input: `open` + `onOpenChange`, or `defaultOpen`. Spread the result on the details.
18
+ * The browser opens it on a tap by itself — a controlled one is put back until the app says so. */
19
+ export function useDisclosure(open, defaultOpen = false, onOpenChange) {
20
+ const [shown, request] = useControlled(open, defaultOpen, onOpenChange);
21
+ return {
22
+ open: shown,
23
+ onToggle: (e) => {
24
+ const el = e.currentTarget;
25
+ if (el.open === shown)
26
+ return;
27
+ request(el.open);
28
+ if (open !== undefined)
29
+ el.open = open;
30
+ },
31
+ };
32
+ }
33
+ /** the latest value in a ref — for a handler that a timer, an observer or a listener set up once calls later */
34
+ export function useLatest(value) {
35
+ const ref = useRef(value);
36
+ ref.current = value;
37
+ return ref;
38
+ }
39
+ /** watch boxes for a size change (the keyboard, a font arriving, a wrap): `fn` on each — once at the start too, as the browser
40
+ * reports. Returns the stop, for an effect's cleanup; nothing is watched where the browser has no ResizeObserver. */
41
+ export function watchSize(targets, fn) {
42
+ if (typeof ResizeObserver === 'undefined')
43
+ return () => { };
44
+ const ro = new ResizeObserver(fn);
45
+ for (const t of targets)
46
+ if (t)
47
+ ro.observe(t);
48
+ return () => ro.disconnect();
9
49
  }
10
50
  const warned = new Set();
11
51
  /** a dev hint said once per key, never again */
@@ -19,9 +19,8 @@ const index = (o) => (o === true ? 0 : typeof o === 'number' ? o : null);
19
19
  */
20
20
  export function CallSheet({ media, heading, body, action, sheet, ways, open: openProp, defaultOpen = null, onOpenChange, states = 'peek full', children, ...rest }) {
21
21
  const list = ways ?? (sheet ? [{ action, sheet }] : []);
22
- const [on, setOwn] = useControlled(openProp === undefined ? undefined : index(openProp), index(defaultOpen));
22
+ const [on, set] = useControlled(openProp === undefined ? undefined : index(openProp), index(defaultOpen), onOpenChange && ((next) => onOpenChange(next !== null, next)));
23
23
  const [at, setAt] = useState('full');
24
- const set = (next) => { setOwn(next); onOpenChange?.(next !== null, next); };
25
24
  return (_jsxs(Screen, { fill: true, ...rest, children: [_jsxs(Step, { children: [_jsxs(Page, { children: [media, _jsx(CardHead, { level: "h1", heading: heading }), typeof body === 'string' ? _jsx(Text, { level: "body", children: body }) : body, _jsx(CtaPack, { children: list.map((w, i) => (_jsx(Btn, { primary: i === 0, quiet: i > 0, "aria-expanded": on === i, onClick: () => { setAt('full'); set(i); }, children: w.action }, i))) })] }), list.map(({ sheet: sh }, i) => (_jsxs(Sheet, { state: on === i ? at : 'away', onStateChange: (s) => { if (s === 'away')
26
25
  set(null);
27
26
  else
@@ -1,4 +1,5 @@
1
1
  import { type ComponentProps, type ReactNode } from 'react';
2
+ import type { Tone } from '../cards/Card.js';
2
3
  import { Screen } from '../scaffold/Screen.js';
3
4
  import { type StackSheetTab } from '../sheets/StackSheet.js';
4
5
  export interface HomeSheet {
@@ -6,8 +7,8 @@ export interface HomeSheet {
6
7
  title: ReactNode;
7
8
  /** the sheet head's end when it has no tabs: an Icon, an IconRow */
8
9
  icon?: ReactNode;
9
- /** the paper: 1 light · 2 tinted · 3 dark (default: 2, 3, 1, 2, 3 by place) */
10
- tone?: 1 | 2 | 3 | 4 | 5;
10
+ /** the paper — see `Tone` (default: 2, 3, 1, 2, 3 by place) */
11
+ tone?: Tone;
11
12
  /** the sheet's pages as icon tabs in its head (a count shows as a small circle over the icon) — see `StackSheet` */
12
13
  tabs?: StackSheetTab[];
13
14
  /** the tab shown first (default: the first) */
@@ -15,7 +15,7 @@ const LEAVE = 800; // ms: a closing sheet keeps its body until it has slid home
15
15
  * sections on one home never cost three pages (`keep` holds a body once opened).
16
16
  */
17
17
  export function Home({ top, title, sheets, open: openProp, defaultOpen = null, onOpenChange, base, keep, backLabel = 'Back', children, ...rest }) {
18
- const [open, setOwn] = useControlled(openProp, defaultOpen);
18
+ const [open, change] = useControlled(openProp, defaultOpen, onOpenChange);
19
19
  const [leaving, setLeaving] = useState(null);
20
20
  const [seen, setSeen] = useState(() => new Set(open != null ? [open] : []));
21
21
  const [was, setWas] = useState(open);
@@ -32,7 +32,6 @@ export function Home({ top, title, sheets, open: openProp, defaultOpen = null, o
32
32
  const t = setTimeout(() => setLeaving(null), LEAVE);
33
33
  return () => clearTimeout(t);
34
34
  }, [leaving]);
35
- const change = (next) => { setOwn(next); onOpenChange?.(next); };
36
35
  const [tabs, setTabs] = useState(() => Object.fromEntries(sheets.map((x, i) => [i, x.defaultTab ?? x.tabs?.[0]?.id])));
37
36
  const shows = (i) => i === open || i === leaving || (keep && seen.has(i));
38
37
  return (_jsxs(Screen, { fill: true, top: top, ...rest, children: [_jsx(Text, { level: "h1", as: "h1", sr: true, children: title }), _jsx(SheetStack, { closed: true, open: open, onOpenChange: change, base: base, children: sheets.map((s, i) => (_jsx(StackSheet, { tone: s.tone ?? TONES[i], title: s.title, icon: s.icon, tabs: s.tabs, tab: tabs[i], onTabChange: (t) => setTabs((v) => ({ ...v, [i]: t })), backLabel: backLabel, foot: typeof s.foot === 'function' ? s.foot(tabs[i]) : s.foot, children: shows(i) ? (typeof s.body === 'function' ? s.body(tabs[i]) : s.body) : null }, i))) }), children] }));
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
- import { reducedMotion } from '../state.js';
3
+ import { reducedMotion, useLatest } from '../state.js';
4
4
  import { Screen } from '../scaffold/Screen.js';
5
5
  import { cx } from '../cx.js';
6
6
  /**
@@ -13,8 +13,7 @@ import { cx } from '../cx.js';
13
13
  export function Splash({ mark, text, done, min = 0, onLeft, className, ...rest }) {
14
14
  const born = useRef(Date.now());
15
15
  const [leaving, setLeaving] = useState(false);
16
- const left = useRef(onLeft);
17
- left.current = onLeft;
16
+ const left = useLatest(onLeft);
18
17
  useEffect(() => {
19
18
  if (!done)
20
19
  return;
@@ -17,7 +17,7 @@ import { IconBtn } from '../actions/IconBtn.js';
17
17
  * first and fixed while the words scroll, one CTA row. Yours: the pages, the row, the state.
18
18
  */
19
19
  export function Walkthrough({ pages, at: atProp, defaultAt = 0, onAtChange, cta, controls = [], ctaLink, children, backLabel = 'Back', nextLabel = 'Next', ...rest }) {
20
- const [at, setOwn] = useControlled(atProp, defaultAt);
20
+ const [at, turn] = useControlled(atProp, defaultAt, onAtChange);
21
21
  const step = useRef(null);
22
22
  // a new page starts where a page starts — at its lead, not where the last one was scrolled to (Lh 2026-09-23). A LAYOUT effect:
23
23
  // it runs inside the page turn's commit, before the view transition takes its picture — only the sideways slide shows, no scroll
@@ -27,7 +27,7 @@ export function Walkthrough({ pages, at: atProp, defaultAt = 0, onAtChange, cta,
27
27
  const go = (to) => {
28
28
  if (to < 0 || to >= count || to === at)
29
29
  return;
30
- slideTo(to > at ? 'next' : 'back', () => { setOwn(to); onAtChange?.(to); }, step.current ?? document);
30
+ slideTo(to > at ? 'next' : 'back', () => turn(to), step.current ?? document);
31
31
  };
32
32
  /* a sideways SWIPE turns the page (Lh 2026-09-23): left = next, right = back. The walk still scrolls up and down itself
33
33
  (touch-action: pan-y); a swipe must be mostly sideways and long enough, so a scroll never turns a page */
@@ -14,12 +14,12 @@ export interface TextProps extends React.HTMLAttributes<HTMLElement> {
14
14
  grow?: boolean;
15
15
  /** uppercase */
16
16
  caps?: boolean;
17
- /** the element; defaults per level (h2 for display/h1/h2, p for title/body/stat, span for the rest) */
18
17
  /** READ, NOT SHOWN: the words stay in the page for a screen reader (a page's h1 that the design does not show), and take no room */
19
18
  sr?: boolean;
20
19
  /** the member may select and copy these words (a code, an address, a reference number) — everything else in a Screen is not
21
20
  * selectable, like a native app (Lh 2026-09-25) */
22
21
  selectable?: boolean;
22
+ /** the element; defaults per level (h2 for display/h1/h2, p for title/body/stat, span for the rest) */
23
23
  as?: 'h1' | 'h2' | 'h3' | 'p' | 'span' | 'strong' | 'div';
24
24
  }
25
25
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.16",
3
+ "version": "0.7.18",
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.",
@@ -14,7 +14,10 @@
14
14
  "test": "playwright test",
15
15
  "validate:inbox": "node scripts/validate-inbox.mjs",
16
16
  "prepack": "npm run build",
17
- "knobs": "node scripts/knobs.mjs"
17
+ "knobs": "node scripts/knobs.mjs",
18
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p gallery/tsconfig.json --noEmit",
19
+ "snapshot:write": "CARDDS_SNAPSHOT=write playwright test tests/style-snapshot.spec.js",
20
+ "snapshot:check": "CARDDS_SNAPSHOT=check playwright test tests/style-snapshot.spec.js"
18
21
  },
19
22
  "devDependencies": {
20
23
  "@playwright/test": "1.58.2",
@@ -52,8 +55,8 @@
52
55
  "CHANGELOG.md"
53
56
  ],
54
57
  "peerDependencies": {
55
- "react": ">=18",
56
- "react-dom": ">=18"
58
+ "react": ">=19",
59
+ "react-dom": ">=19"
57
60
  },
58
61
  "publishConfig": {
59
62
  "access": "public"