@usableapp/cardds 0.6.12 → 0.6.14

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,42 @@
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.6.14 — 2026-09-24
6
+
7
+ ### ActionNote (new, the bottom)
8
+ - The main button's place and size, but lying on the ground: no fill, no border, no rounding. It holds one line of muted
9
+ words, and an inline `Icon` is allowed. Nothing to press.
10
+ - For when there is nothing to press here. Example (iPhone install):
11
+ `<ActionNote>กดที่ <Icon name="share" /> ด้านล่างนี้</ActionNote>`, pointing at Safari's own share button.
12
+ - Put it in a `CardFoot` (so a `CallSheet`'s `sheet.foot`) or an `ActionBar`, in place of the button, never beside one.
13
+ A `link` / `ctaLink` still goes under it.
14
+ - One fixed shape, never styled per use.
15
+
16
+ ### Modal in Safari (iOS 18) — fixed
17
+ - Before: in Safari/WebKit the Modal stretched to the whole stage and its dark drawer (the question + buttons) was pushed out of
18
+ sight; Chrome was right. After: the same in both — the card its own height at the centre, the drawer shown.
19
+ - CSS only, nothing to change in a project: `.modal` is a flex column (was a grid of `minmax(0, 1fr) auto`, which WebKit sized
20
+ to 0) and takes its own height by `align-self: center` (was `height: fit-content`).
21
+
22
+ - Tests can run in WebKit: `CARDDS_BROWSER=webkit npm test` (the 131% text-size cases are Chrome-only — they use CDP).
23
+
24
+ ## 0.6.13 — 2026-09-24
25
+
26
+ ### `CallSheet ways`: several ways in, each calling its own sheet (new)
27
+ - Before: `CallSheet` took one `action` + one `sheet`. After: it can also take `ways: [{ action, sheet }]`. Each way gets a button
28
+ sized to its words, and the buttons wrap like a `CtaPack` (the first is primary, the rest quiet). Each button brings up its own
29
+ sheet (away → full), and only one sheet is up at a time. Each sheet holds that way's WHOLE form, with its submit on `foot`.
30
+ - `open` / `defaultOpen` = the way's index, or `null` for none (`true` = the first). `onOpenChange(open, way)` gets the index as a
31
+ second argument. `action` + `sheet` still work unchanged.
32
+ - Gallery screen **SignIn**: เข้าใช้งาน with three ways (register · code · face scan).
33
+
34
+ ### `--scrim` is always black
35
+ - Before: the scrim was 50% of `--card-3-bg` (the brand), so a coloured theme dimmed in its brand colour. After: 50% black in
36
+ every palette (dim stage, the cover and postcard gradients, the gallery drawer). A project can still set `--scrim` in its theme.css.
37
+
38
+ - Gallery only: Palette and the Layout / Boxes / Keyboard switches sit above Settings, which now holds only the base sliders. The
39
+ nav widens with the text size. The Page · OneMove story is removed (the Install screen shows it).
40
+
5
41
  ## 0.6.12 — 2026-09-23
6
42
 
7
43
  - Gallery only (nothing changes in the package's components or CSS):
package/css/actions.css CHANGED
@@ -310,3 +310,24 @@
310
310
  fixed to the viewport wherever it sits. (.screen--fill reserves nothing:
311
311
  its .step carries the bar's room itself, step.css.) */
312
312
  :has(> .action-bar) .screen:not(.screen--fill) { padding-bottom: var(--bar-reserve); }
313
+
314
+ /* ---- action note (Lh 2026-09-24): the main button's place, the main button's size, but ON the ground — no fill, no border, no
315
+ rounding — one line of muted words, an inline icon allowed; nothing to press. In a CardFoot or an ActionBar row it takes the
316
+ button's share (the whole width), so a foot or a bar keeps its height whether it holds a button or the note. ---- */
317
+ .action-note {
318
+ flex: 1 1 0;
319
+ min-width: 0;
320
+ min-height: var(--btn-h);
321
+ display: flex;
322
+ align-items: center;
323
+ justify-content: center;
324
+ gap: var(--action-note-gap, var(--sp-1));
325
+ padding-inline: var(--sp-2);
326
+ font: var(--action-note-font, var(--type-body));
327
+ color: var(--action-note-ink, var(--on-surface-muted));
328
+ text-align: center;
329
+ cursor: default;
330
+ }
331
+ .card .action-note { color: var(--action-note-ink, var(--card-muted)); }
332
+ .action-note__words { display: block; } /* ONE run of words: an icon inside sits in the line, the spaces around it kept */
333
+ .action-note__words > .icon { display: inline-block; vertical-align: -0.2em; width: 1.25em; height: 1.25em; margin-inline: 0.15em; color: inherit; }
package/css/layover.css CHANGED
@@ -34,14 +34,16 @@
34
34
  position: absolute; /* at the stage's centre, and kept there as it grows: absolute + margin auto re-centres on every height */
35
35
  inset: 0;
36
36
  margin: auto;
37
- height: fit-content; /* inset 0 alone would stretch it to the stage: its own height, then the margins share the rest */
37
+ height: auto;
38
+ align-self: center; /* inset 0 alone would stretch it to the stage: centre self-alignment gives it its own height, the margins
39
+ share the rest. NOT `height: fit-content` — WebKit/Safari sizes it wrong (Lh 2026-09-24, iOS 18.6) */
38
40
  width: calc(100% - 2 * var(--screen-pad));
39
41
  z-index: 2; /* over the dim stage's scrim (z 1) */
40
42
  max-height: calc(100cqh - 2 * var(--sp-5)); /* cqh: the stage (a size container) */
41
43
  padding: 0; /* the parts pad themselves */
42
44
  border: 0; /* no frame on the modal (Lh 2026-09-16) — its edge is the shadow */
43
- display: grid;
44
- grid-template-rows: minmax(0, 1fr) auto;
45
+ display: flex; /* a column: the lift shrinks (and scrolls) under max-height, the drawer keeps its height. NOT a grid */
46
+ flex-direction: column; /* of minmax(0, 1fr) + auto — WebKit/Safari sizes that 1fr row to 0 (Lh 2026-09-24, iOS 18.6) */
45
47
  overflow: clip;
46
48
  box-shadow: var(--modal-shadow, var(--shadow-sheet));
47
49
  }
@@ -50,11 +52,13 @@
50
52
  display: grid;
51
53
  gap: var(--card-gap);
52
54
  align-content: start;
55
+ flex: 0 1 auto;
53
56
  min-height: 0;
54
57
  overflow-y: auto; /* tall content scrolls inside; the drawer stays */
55
58
  overscroll-behavior: contain;
56
59
  }
57
60
  .modal__drawer {
61
+ flex-shrink: 0;
58
62
  height: var(--drawer-h);
59
63
  box-sizing: border-box;
60
64
  padding: var(--modal-drawer-pad, var(--sp-5));
package/css/tokens.css CHANGED
@@ -195,7 +195,7 @@
195
195
  --ink-rail: color-mix(in srgb, currentColor 30%, transparent); /* todo steps, ghost edges */
196
196
  --accent-soft: color-mix(in srgb, var(--accent) 22%, transparent); /* calendar marks */
197
197
  --accent-mid: color-mix(in srgb, var(--accent) 50%, transparent);
198
- --scrim: color-mix(in srgb, var(--card-3-bg) 50%, transparent); /* dim stage, cover gradient */
198
+ --scrim: color-mix(in srgb, #000 50%, transparent); /* dim stage, cover gradient — ALWAYS black, never the brand (Lh 2026-09-24) */
199
199
  }
200
200
 
201
201
  /* ============================================================
@@ -0,0 +1,11 @@
1
+ import type { ComponentProps } from 'react';
2
+ export interface ActionNoteProps extends ComponentProps<'div'> {
3
+ }
4
+ /**
5
+ * ActionNote — the words that stand WHERE THE MAIN BUTTON WOULD BE when there is nothing to press: a block exactly the main button's
6
+ * size (full width, `--btn-h` tall), lying on the ground — no fill, no border, no rounding — holding one line of muted words, an
7
+ * inline `Icon` allowed ("กดที่ <Icon name="share" /> ด้านล่างนี้" — the member does the thing elsewhere). It does nothing on a tap.
8
+ * A member of the BOTTOM: a `CardFoot`'s row or an `ActionBar`'s, in place of the button, never beside one; a `link` still goes under
9
+ * it (CardFoot `link`, ActionBar `ctaLink`). One fixed shape — never styled per use (Lh 2026-09-24).
10
+ */
11
+ export declare function ActionNote({ className, children, ...rest }: ActionNoteProps): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from '../cx.js';
3
+ /**
4
+ * ActionNote — the words that stand WHERE THE MAIN BUTTON WOULD BE when there is nothing to press: a block exactly the main button's
5
+ * size (full width, `--btn-h` tall), lying on the ground — no fill, no border, no rounding — holding one line of muted words, an
6
+ * inline `Icon` allowed ("กดที่ <Icon name="share" /> ด้านล่างนี้" — the member does the thing elsewhere). It does nothing on a tap.
7
+ * A member of the BOTTOM: a `CardFoot`'s row or an `ActionBar`'s, in place of the button, never beside one; a `link` still goes under
8
+ * it (CardFoot `link`, ActionBar `ctaLink`). One fixed shape — never styled per use (Lh 2026-09-24).
9
+ */
10
+ export function ActionNote({ className, children, ...rest }) {
11
+ return _jsx("div", { role: "note", className: cx('action-note', className), ...rest, children: _jsx("span", { className: "action-note__words", children: children }) });
12
+ }
package/dist/cardds.css CHANGED
@@ -236,7 +236,7 @@
236
236
  --ink-rail: color-mix(in srgb, currentColor 30%, transparent); /* todo steps, ghost edges */
237
237
  --accent-soft: color-mix(in srgb, var(--accent) 22%, transparent); /* calendar marks */
238
238
  --accent-mid: color-mix(in srgb, var(--accent) 50%, transparent);
239
- --scrim: color-mix(in srgb, var(--card-3-bg) 50%, transparent); /* dim stage, cover gradient */
239
+ --scrim: color-mix(in srgb, #000 50%, transparent); /* dim stage, cover gradient — ALWAYS black, never the brand (Lh 2026-09-24) */
240
240
  }
241
241
 
242
242
  /* ============================================================
@@ -2143,6 +2143,27 @@ button { font: inherit; cursor: pointer; }
2143
2143
  its .step carries the bar's room itself, step.css.) */
2144
2144
  :has(> .action-bar) .screen:not(.screen--fill) { padding-bottom: var(--bar-reserve); }
2145
2145
 
2146
+ /* ---- action note (Lh 2026-09-24): the main button's place, the main button's size, but ON the ground — no fill, no border, no
2147
+ rounding — one line of muted words, an inline icon allowed; nothing to press. In a CardFoot or an ActionBar row it takes the
2148
+ button's share (the whole width), so a foot or a bar keeps its height whether it holds a button or the note. ---- */
2149
+ .action-note {
2150
+ flex: 1 1 0;
2151
+ min-width: 0;
2152
+ min-height: var(--btn-h);
2153
+ display: flex;
2154
+ align-items: center;
2155
+ justify-content: center;
2156
+ gap: var(--action-note-gap, var(--sp-1));
2157
+ padding-inline: var(--sp-2);
2158
+ font: var(--action-note-font, var(--type-body));
2159
+ color: var(--action-note-ink, var(--on-surface-muted));
2160
+ text-align: center;
2161
+ cursor: default;
2162
+ }
2163
+ .card .action-note { color: var(--action-note-ink, var(--card-muted)); }
2164
+ .action-note__words { display: block; } /* ONE run of words: an icon inside sits in the line, the spaces around it kept */
2165
+ .action-note__words > .icon { display: inline-block; vertical-align: -0.2em; width: 1.25em; height: 1.25em; margin-inline: 0.15em; color: inherit; }
2166
+
2146
2167
  /* ---- css/numbers.css — ring, track, band, dot grid, picker, badge, bars ---- */
2147
2168
  /* ============================================================
2148
2169
  cardds/numbers.css — big numbers inside cards:
@@ -3357,14 +3378,16 @@ button { font: inherit; cursor: pointer; }
3357
3378
  position: absolute; /* at the stage's centre, and kept there as it grows: absolute + margin auto re-centres on every height */
3358
3379
  inset: 0;
3359
3380
  margin: auto;
3360
- height: fit-content; /* inset 0 alone would stretch it to the stage: its own height, then the margins share the rest */
3381
+ height: auto;
3382
+ align-self: center; /* inset 0 alone would stretch it to the stage: centre self-alignment gives it its own height, the margins
3383
+ share the rest. NOT `height: fit-content` — WebKit/Safari sizes it wrong (Lh 2026-09-24, iOS 18.6) */
3361
3384
  width: calc(100% - 2 * var(--screen-pad));
3362
3385
  z-index: 2; /* over the dim stage's scrim (z 1) */
3363
3386
  max-height: calc(100cqh - 2 * var(--sp-5)); /* cqh: the stage (a size container) */
3364
3387
  padding: 0; /* the parts pad themselves */
3365
3388
  border: 0; /* no frame on the modal (Lh 2026-09-16) — its edge is the shadow */
3366
- display: grid;
3367
- grid-template-rows: minmax(0, 1fr) auto;
3389
+ display: flex; /* a column: the lift shrinks (and scrolls) under max-height, the drawer keeps its height. NOT a grid */
3390
+ flex-direction: column; /* of minmax(0, 1fr) + auto — WebKit/Safari sizes that 1fr row to 0 (Lh 2026-09-24, iOS 18.6) */
3368
3391
  overflow: clip;
3369
3392
  box-shadow: var(--modal-shadow, var(--shadow-sheet));
3370
3393
  }
@@ -3373,11 +3396,13 @@ button { font: inherit; cursor: pointer; }
3373
3396
  display: grid;
3374
3397
  gap: var(--card-gap);
3375
3398
  align-content: start;
3399
+ flex: 0 1 auto;
3376
3400
  min-height: 0;
3377
3401
  overflow-y: auto; /* tall content scrolls inside; the drawer stays */
3378
3402
  overscroll-behavior: contain;
3379
3403
  }
3380
3404
  .modal__drawer {
3405
+ flex-shrink: 0;
3381
3406
  height: var(--drawer-h);
3382
3407
  box-sizing: border-box;
3383
3408
  padding: var(--modal-drawer-pad, var(--sp-5));
package/dist/index.d.ts CHANGED
@@ -76,3 +76,4 @@ export * from './templates/Walkthrough.js';
76
76
  export * from './templates/Home.js';
77
77
  export * from './sheets/StackSheet.js';
78
78
  export * from './templates/CallSheet.js';
79
+ export * from './actions/ActionNote.js';
package/dist/index.js CHANGED
@@ -79,3 +79,4 @@ export * from './templates/Walkthrough.js';
79
79
  export * from './templates/Home.js';
80
80
  export * from './sheets/StackSheet.js';
81
81
  export * from './templates/CallSheet.js';
82
+ export * from './actions/ActionNote.js';
@@ -10,6 +10,13 @@ export interface CallSheetSheet {
10
10
  /** a small link UNDER that button (a `Link sm`, e.g. details for developers) */
11
11
  footLink?: ReactNode;
12
12
  }
13
+ /** one way in: its button and the sheet it calls */
14
+ export interface CallSheetWay {
15
+ /** the button's words */
16
+ action: ReactNode;
17
+ /** what that button brings up */
18
+ sheet: CallSheetSheet;
19
+ }
13
20
  export interface CallSheetProps extends Omit<ComponentProps<typeof Screen>, 'children' | 'fill' | 'top' | 'bottom' | 'title'> {
14
21
  /** a picture above the words (optional) */
15
22
  media?: ReactNode;
@@ -17,22 +24,25 @@ export interface CallSheetProps extends Omit<ComponentProps<typeof Screen>, 'chi
17
24
  heading: ReactNode;
18
25
  /** the words: a string is one paragraph */
19
26
  body?: ReactNode;
20
- /** the button that calls the sheet */
21
- action: ReactNode;
22
- /** what comes up */
23
- sheet: CallSheetSheet;
24
- /** CONTROLLED: the sheet is up — with `onOpenChange`; or `defaultOpen` */
25
- open?: boolean;
26
- defaultOpen?: boolean;
27
- onOpenChange?: (open: boolean) => void;
28
- /** the sheet's resting states once up (default "peek full") */
27
+ /** ONE way in: the button that calls the sheet (with `sheet`) */
28
+ action?: ReactNode;
29
+ /** what that button brings up */
30
+ sheet?: CallSheetSheet;
31
+ /** SEVERAL ways in (instead of `action` + `sheet`): one button each, sized to its words, wrapping like a CtaPack (the first primary, the rest quiet), each calling its own sheet */
32
+ ways?: CallSheetWay[];
33
+ /** CONTROLLED: which way's sheet is up — an index, or null for none (`true` = the first); with `onOpenChange`; or `defaultOpen` */
34
+ open?: number | boolean | null;
35
+ defaultOpen?: number | boolean | null;
36
+ onOpenChange?: (open: boolean, way: number | null) => void;
37
+ /** the sheets' resting states once up (default "peek full") */
29
38
  states?: string;
30
39
  /** layers over the screen */
31
40
  children?: ReactNode;
32
41
  }
33
42
  /**
34
43
  * CallSheet — TEMPLATE, a `Screen` with its parts filled — no top · content: a `Step` holding ONE `Page` (picture · heading (the h1) ·
35
- * words · the button) and the `Sheet` that button calls · no bottom (the sheet finishes on its own foot). The page starts at the
36
- * screen's lead (15%). The sheet waits `away`; the button brings it up full; its handle takes it down to peek.
44
+ * words · the button, or several — `ways`) and the `Sheet` each button calls · no bottom (a sheet finishes on its own foot). The page
45
+ * starts at the screen's lead (15%). The sheets wait `away`; a button brings its own up full; the handle takes it down to peek.
46
+ * One sheet up at a time — a sheet holds that way's WHOLE form.
37
47
  */
38
- export declare function CallSheet({ media, heading, body, action, sheet, open: openProp, defaultOpen, onOpenChange, states, children, ...rest }: CallSheetProps): import("react").JSX.Element;
48
+ export declare function CallSheet({ media, heading, body, action, sheet, ways, open: openProp, defaultOpen, onOpenChange, states, children, ...rest }: CallSheetProps): import("react").JSX.Element;
@@ -9,19 +9,22 @@ import { CardHead } from '../cards/CardHead.js';
9
9
  import { CardFoot } from '../cards/CardFoot.js';
10
10
  import { Text } from '../type/Text.js';
11
11
  import { Btn } from '../actions/Btn.js';
12
+ const index = (o) => (o === true ? 0 : typeof o === 'number' ? o : null);
12
13
  /**
13
14
  * CallSheet — TEMPLATE, a `Screen` with its parts filled — no top · content: a `Step` holding ONE `Page` (picture · heading (the h1) ·
14
- * words · the button) and the `Sheet` that button calls · no bottom (the sheet finishes on its own foot). The page starts at the
15
- * screen's lead (15%). The sheet waits `away`; the button brings it up full; its handle takes it down to peek.
15
+ * words · the button, or several — `ways`) and the `Sheet` each button calls · no bottom (a sheet finishes on its own foot). The page
16
+ * starts at the screen's lead (15%). The sheets wait `away`; a button brings its own up full; the handle takes it down to peek.
17
+ * One sheet up at a time — a sheet holds that way's WHOLE form.
16
18
  */
17
- export function CallSheet({ media, heading, body, action, sheet, open: openProp, defaultOpen = false, onOpenChange, states = 'peek full', children, ...rest }) {
18
- const [own, setOwn] = useState(defaultOpen);
19
- const open = openProp ?? own;
19
+ export function CallSheet({ media, heading, body, action, sheet, ways, open: openProp, defaultOpen = null, onOpenChange, states = 'peek full', children, ...rest }) {
20
+ const list = ways ?? (sheet ? [{ action, sheet }] : []);
21
+ const [own, setOwn] = useState(index(defaultOpen));
22
+ const on = openProp === undefined ? own : index(openProp);
20
23
  const [at, setAt] = useState('full');
21
24
  const set = (next) => { if (openProp === undefined)
22
- setOwn(next); onOpenChange?.(next); };
23
- 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: _jsx(Btn, { primary: true, "aria-expanded": open, onClick: () => { setAt('full'); set(true); }, children: action }) })] }), _jsxs(Sheet, { state: open ? at : 'away', onStateChange: (s) => { if (s === 'away')
24
- set(false);
25
+ setOwn(next); onOpenChange?.(next !== null, next); };
26
+ 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')
27
+ set(null);
25
28
  else
26
- setAt(s); }, states: states, children: [_jsx(CardHead, { heading: sheet.title }), _jsx(SheetBody, { children: open ? (typeof sheet.body === 'function' ? sheet.body() : sheet.body) : null }), (sheet.foot != null || sheet.footLink != null) && _jsx(CardFoot, { link: sheet.footLink, children: sheet.foot })] })] }), children] }));
29
+ setAt(s); }, states: states, children: [_jsx(CardHead, { heading: sh.title }), _jsx(SheetBody, { children: on === i ? (typeof sh.body === 'function' ? sh.body() : sh.body) : null }), (sh.foot != null || sh.footLink != null) && _jsx(CardFoot, { link: sh.footLink, children: sh.foot })] }, i)))] }), children] }));
27
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.6.12",
3
+ "version": "0.6.14",
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.",