@usableapp/cardds 0.7.31 → 0.7.33
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 +16 -0
- package/README.md +4 -1
- package/css/stack.css +8 -5
- package/css/tokens.css +3 -1
- package/dist/cardds.css +11 -6
- package/dist/cards/Placeholder.js +2 -1
- package/dist/choice/Calendar.js +2 -1
- package/dist/choice/Mood.js +2 -1
- package/dist/forms/Pin.js +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/media/Mosaic.js +3 -2
- package/dist/numbers/Track.js +2 -1
- package/dist/sheets/Sheet.js +2 -1
- package/dist/sheets/SheetSteps.js +2 -1
- package/dist/sheets/StackBack.js +2 -1
- package/dist/sheets/StackSheet.js +2 -1
- package/dist/templates/CallSheet.d.ts +6 -1
- package/dist/templates/CallSheet.js +2 -2
- package/dist/templates/Home.js +2 -1
- package/dist/templates/Walkthrough.js +2 -1
- package/dist/words.d.ts +7 -0
- package/dist/words.js +14 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
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.33 — 2026-10-01
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **A StackSheet tab's count grows with the text** (Lh 2026-10-01): the red circle was a fixed 20px while its number followed `--fs-base` — at a large text size the number ran over it. The circle is now cut from its own number's size (20px at the default, 32px at 160%); a long count ("10", "99+") makes it a capsule. The number sits on the circle's middle (it sat low), and the circle stands 8px out of the tab's corner instead of 4 — beside the icon, not on the disc's edge. Nothing to change in a screen.
|
|
9
|
+
- **A screen fills the phone, edge to edge** (Lh 2026-10-01): the column's cap `--screen-w` is `30rem` (480px), was `25rem` (400) — a phone 412–448px wide showed a strip of bare ground each side of the screen, its bars and its sheets. No phone reaches the cap now; it only keeps a tablet / desktop window to a phone-sized column. Nothing to change in a screen; a project that set `--screen-w` itself keeps its value.
|
|
10
|
+
### Added
|
|
11
|
+
- Knobs `--stack-sheet-count-pad-x` (the room beside the number), `--stack-sheet-count-lift` (the room under it — retune for another font), `--stack-sheet-count-out` (how far it stands out of the corner). `--stack-sheet-count-h` as before; its default is `1.175em`, was `1.25rem`.
|
|
12
|
+
|
|
13
|
+
## 0.7.32 — 2026-09-29
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **cardds names its own controls, in the page's language** (Lh 2026-09-29): the words a control reads out when the app gives none — a sheet's handle, a back / next / previous button, Skip, a Pin's cells, Progress, Image, Mood, Photos, Voice note — follow `<html lang>`. `th` ships (`ปรับความสูง`, `กลับ`, `ถัดไป`, `หลักที่`…), anything else is English as before. An app passes NOTHING per component: remove `handle="ปรับความสูง"`, `backLabel="กลับ"`, `label="หลักที่"`… where they only repeated these words. A prop still wins where a screen needs other words.
|
|
17
|
+
### Added
|
|
18
|
+
- `setWords(lang, { … })` — a project in another language adds its set once at app start (missing words fall back to English); `word(key)` reads one.
|
|
19
|
+
- `CallSheet handle` (+ a way's `sheet.handle`): override the sheets' handle name, or `false` for none.
|
|
20
|
+
|
|
5
21
|
## 0.7.31 — 2026-09-29
|
|
6
22
|
|
|
7
23
|
### Changed
|
package/README.md
CHANGED
|
@@ -306,6 +306,9 @@ whose markup has rules, because the sheet's box is the step itself:
|
|
|
306
306
|
opened, a `SheetStack` sheet opened, a `Modal` arrived. Once it has settled: the field marked `data-autofocus`, else its
|
|
307
307
|
first field, else its heading (a step's title, a modal's question) or, for a sheet come onto the screen, its handle. A field
|
|
308
308
|
that already has the cursor keeps it. One mechanic for all of them (`focusWhenActive`, src/state.ts); an app writes no focus code.
|
|
309
|
+
- **cardds names its own controls, in the page's language** (Lh 2026-09-29): a handle, a back button, a Pin's cells… read
|
|
310
|
+
their name from `src/words.ts` by `<html lang>` (en, th ship; `setWords(lang, {…})` adds one). An app passes no label that
|
|
311
|
+
only repeats these words; a prop still wins.
|
|
309
312
|
|
|
310
313
|
## Centred card markup
|
|
311
314
|
|
|
@@ -647,7 +650,7 @@ Every length token resolves to rem — a multiplier × one of four rem bases
|
|
|
647
650
|
(`--sp-4` = `--sp-base` × 4 = 1rem, `--tap` = `--tap-base` = 3rem, `--r-card` =
|
|
648
651
|
`--r-base` × 2, `--fs-h1` = `--fs-base` × 1.2; see "A project's theme") — so the
|
|
649
652
|
whole system scales with the root font size. `base.css` keeps it at `100%` (the browser's setting, 16px by
|
|
650
|
-
default) and steps narrow phones (≤360px) to `93.75%`, which keeps the
|
|
653
|
+
default) and steps narrow phones (≤360px) to `93.75%`, which keeps the
|
|
651
654
|
column, its 48px taps and its type in proportion on 320–360 screens. Only
|
|
652
655
|
`--border-w`, `--rule-w` and the focus ring stay in px, so edges stay crisp.
|
|
653
656
|
An app with its own text-size setting calls `setTextScale(percent)`, which writes
|
package/css/stack.css
CHANGED
|
@@ -292,11 +292,14 @@
|
|
|
292
292
|
.stack-sheet__tab { position: relative; }
|
|
293
293
|
.stack-sheet__tab > .stack-sheet__count { /* outranks .badge (numbers.css loads after this file) */
|
|
294
294
|
position: absolute;
|
|
295
|
-
top: calc(-1 * var(--sp-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
295
|
+
top: calc(-1 * var(--stack-sheet-count-out, var(--sp-2))); /* how far it stands OUT of the tab's corner, up and to the right: at sp-1 it
|
|
296
|
+
sat exactly on the chosen tab's disc edge — it reads as a mark beside the icon, not part of the disc (Lh 2026-10-01) */
|
|
297
|
+
right: calc(-1 * var(--stack-sheet-count-out, var(--sp-2)));
|
|
298
|
+
min-width: var(--stack-sheet-count-h, 1.175em); /* em, not rem (Lh 2026-10-01): the circle is cut from ITS OWN number's size — at a
|
|
299
|
+
large text size the number grew and a 20px circle did not; 1.175em of a 17px caption = the 20px it always was */
|
|
300
|
+
height: var(--stack-sheet-count-h, 1.175em);
|
|
301
|
+
padding: 0 var(--stack-sheet-count-pad-x, 0.25em) var(--stack-sheet-count-lift, 0.1em); /* "99+" keeps its room at every text size;
|
|
302
|
+
the room UNDER the number lifts it onto the circle's middle: a digit has no descender, so a centred line box sets it low (Lh 2026-10-01) */
|
|
300
303
|
font: var(--type-caption);
|
|
301
304
|
line-height: 1;
|
|
302
305
|
pointer-events: none;
|
package/css/tokens.css
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
:root {
|
|
14
14
|
/* ---- layout ---- */
|
|
15
|
-
--screen-w:
|
|
15
|
+
--screen-w: 30rem; /* 480: the column's CAP — .screen, .action-bar, .sheet-stage. A phone never reaches it (the widest are ~450 CSS px):
|
|
16
|
+
on a phone the screen is the device, edge to edge; the cap only keeps a tablet / desktop window to a phone-sized
|
|
17
|
+
column. Was 25rem: a 412px phone showed a strip each side (Lh 2026-10-01) */
|
|
16
18
|
--screen-centre: 40%; /* the human centre of a screen: the middle of its top four fifths — the bottom fifth is the thumb's (Lh, 2026-09-10). .card--centre sits here */
|
|
17
19
|
|
|
18
20
|
/* ---- the four bases: what a project retunes (Lh, 2026-09-20) ----
|
package/dist/cardds.css
CHANGED
|
@@ -53,7 +53,9 @@
|
|
|
53
53
|
|
|
54
54
|
:root {
|
|
55
55
|
/* ---- layout ---- */
|
|
56
|
-
--screen-w:
|
|
56
|
+
--screen-w: 30rem; /* 480: the column's CAP — .screen, .action-bar, .sheet-stage. A phone never reaches it (the widest are ~450 CSS px):
|
|
57
|
+
on a phone the screen is the device, edge to edge; the cap only keeps a tablet / desktop window to a phone-sized
|
|
58
|
+
column. Was 25rem: a 412px phone showed a strip each side (Lh 2026-10-01) */
|
|
57
59
|
--screen-centre: 40%; /* the human centre of a screen: the middle of its top four fifths — the bottom fifth is the thumb's (Lh, 2026-09-10). .card--centre sits here */
|
|
58
60
|
|
|
59
61
|
/* ---- the four bases: what a project retunes (Lh, 2026-09-20) ----
|
|
@@ -1249,11 +1251,14 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1249
1251
|
.stack-sheet__tab { position: relative; }
|
|
1250
1252
|
.stack-sheet__tab > .stack-sheet__count { /* outranks .badge (numbers.css loads after this file) */
|
|
1251
1253
|
position: absolute;
|
|
1252
|
-
top: calc(-1 * var(--sp-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1254
|
+
top: calc(-1 * var(--stack-sheet-count-out, var(--sp-2))); /* how far it stands OUT of the tab's corner, up and to the right: at sp-1 it
|
|
1255
|
+
sat exactly on the chosen tab's disc edge — it reads as a mark beside the icon, not part of the disc (Lh 2026-10-01) */
|
|
1256
|
+
right: calc(-1 * var(--stack-sheet-count-out, var(--sp-2)));
|
|
1257
|
+
min-width: var(--stack-sheet-count-h, 1.175em); /* em, not rem (Lh 2026-10-01): the circle is cut from ITS OWN number's size — at a
|
|
1258
|
+
large text size the number grew and a 20px circle did not; 1.175em of a 17px caption = the 20px it always was */
|
|
1259
|
+
height: var(--stack-sheet-count-h, 1.175em);
|
|
1260
|
+
padding: 0 var(--stack-sheet-count-pad-x, 0.25em) var(--stack-sheet-count-lift, 0.1em); /* "99+" keeps its room at every text size;
|
|
1261
|
+
the room UNDER the number lifts it onto the circle's middle: a digit has no descender, so a centred line box sets it low (Lh 2026-10-01) */
|
|
1257
1262
|
font: var(--type-caption);
|
|
1258
1263
|
line-height: 1;
|
|
1259
1264
|
pointer-events: none;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
+
import { word } from '../words.js';
|
|
3
4
|
/** Placeholder — the wireframe's crossed box standing in for a photo or illustration (currentColor, survives any palette). */
|
|
4
|
-
export function Placeholder({ size, label = '
|
|
5
|
+
export function Placeholder({ size, label = word('image'), className, ...rest }) {
|
|
5
6
|
return _jsx("div", { className: cx('ph', size && `ph--${size}`, className), role: "img", "aria-label": label, ...rest });
|
|
6
7
|
}
|
package/dist/choice/Calendar.js
CHANGED
|
@@ -2,9 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useControlled } from '../state.js';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
import { IconBtn } from '../actions/IconBtn.js';
|
|
5
|
+
import { word } from '../words.js';
|
|
5
6
|
/** Calendar — a month grid of 44px round cells: one accent day, optional marks; prev/next in the head. It shows ONE month and knows no dates:
|
|
6
7
|
* the app owns which month (`title`, `days`, `start` + `onPrev` / `onNext`) and which day (`selected` + `onSelect`, or `defaultSelected`). */
|
|
7
|
-
export function Calendar({ title, days = 30, start = 1, selected, defaultSelected, onSelect, onPrev, onNext, prevLabel = '
|
|
8
|
+
export function Calendar({ title, days = 30, start = 1, selected, defaultSelected, onSelect, onPrev, onNext, prevLabel = word('previous'), nextLabel = word('next'), marks = [], marks2 = [], disabled = [], weekdays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], nav, className, ...rest }) {
|
|
8
9
|
const [day, pick] = useControlled(selected, defaultSelected, onSelect);
|
|
9
10
|
return (_jsxs("div", { className: cx('calendar', className), ...rest, children: [_jsxs("div", { className: "calendar__head", children: [_jsx("span", { className: "t-title", children: title }), nav ?? (_jsxs("span", { className: "icon-row", children: [_jsx(IconBtn, { sm: true, icon: "chevron-left", label: prevLabel, onClick: onPrev }), _jsx(IconBtn, { sm: true, icon: "chevron-right", label: nextLabel, onClick: onNext })] }))] }), _jsxs("div", { className: "calendar__grid", children: [weekdays.map((d) => _jsx("span", { className: "calendar__wd", children: d }, d)), Array.from({ length: days }, (_, i) => i + 1).map((d) => (_jsx("button", { type: "button", className: cx('calendar__day', marks.includes(d) && 'calendar__day--mark', marks2.includes(d) && 'calendar__day--mark2'), "aria-selected": d === day ? 'true' : undefined, onClick: () => pick(d), disabled: disabled.includes(d), style: d === 1 ? { '--start': start } : undefined, children: d }, d)))] })] }));
|
|
10
11
|
}
|
package/dist/choice/Mood.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
3
|
import { useControlled } from '../state.js';
|
|
4
|
+
import { word } from '../words.js';
|
|
4
5
|
/** Mood — five dots in a pill from bad to good; the picked one grows. */
|
|
5
|
-
export function Mood({ name = 'mood', value, defaultValue, label = '
|
|
6
|
+
export function Mood({ name = 'mood', value, defaultValue, label = word('mood'), onChange, className, ...rest }) {
|
|
6
7
|
const [picked, pick] = useControlled(value, defaultValue, onChange);
|
|
7
8
|
return (_jsx("fieldset", { className: cx('mood', className), "aria-label": label, ...rest, children: [1, 2, 3, 4, 5].map((n) => (_jsxs("label", { className: "mood__opt", children: [_jsx("input", { type: "radio", name: name, value: n, checked: n === picked, onChange: () => pick(n) }), _jsx("span", { className: "mood__dot" })] }, n))) }));
|
|
8
9
|
}
|
package/dist/forms/Pin.js
CHANGED
|
@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
3
3
|
import { useControlled, useLatest } from '../state.js';
|
|
4
4
|
import { cx } from '../cx.js';
|
|
5
5
|
import { useCheck } from './check.js';
|
|
6
|
+
import { word } from '../words.js';
|
|
6
7
|
const RESET_MS = 900; // a code that does not match: red + shake this long, then the cells empty for a new try
|
|
7
8
|
/**
|
|
8
9
|
* Pin — big code entry, one digit per pill; a filled cell turns accent. Lives in a Sheet.
|
|
@@ -10,7 +11,7 @@ const RESET_MS = 900; // a code that does not match: red + shake this long, then
|
|
|
10
11
|
* arrows walk the cells, a pasted code fills them from the one it landed in; only digits stay. The first cell takes the SMS
|
|
11
12
|
* one-time-code autofill. The code is a VALUE, controlled like an input: `value` + `onChange`, or `defaultValue`.
|
|
12
13
|
*/
|
|
13
|
-
export function Pin({ length = 6, value, defaultValue = '', label = '
|
|
14
|
+
export function Pin({ length = 6, value, defaultValue = '', label = word('digit'), onChange, autoFocus, error, match, formatError, secret, className, onAnimationEnd, ...rest }) {
|
|
14
15
|
const ref = useRef(null);
|
|
15
16
|
const [typed, request] = useControlled(value, defaultValue, onChange);
|
|
16
17
|
const code = typed.replace(/\D/g, '').slice(0, length);
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from './type/Icon.js';
|
|
|
5
5
|
export { ICON_NAMES } from './type/icons.js';
|
|
6
6
|
export * from './type/Text.js';
|
|
7
7
|
export { setTextScale } from './type/textScale.js';
|
|
8
|
+
export { setWords, word, type WordKey } from './words.js';
|
|
8
9
|
export * from './scaffold/Screen.js';
|
|
9
10
|
export * from './scaffold/AppBar.js';
|
|
10
11
|
export * from './scaffold/TopBar.js';
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export * from './type/Icon.js';
|
|
|
9
9
|
export { ICON_NAMES } from './type/icons.js';
|
|
10
10
|
export * from './type/Text.js';
|
|
11
11
|
export { setTextScale } from './type/textScale.js';
|
|
12
|
+
export { setWords, word } from './words.js';
|
|
12
13
|
export * from './scaffold/Screen.js';
|
|
13
14
|
export * from './scaffold/AppBar.js';
|
|
14
15
|
export * from './scaffold/TopBar.js';
|
package/dist/media/Mosaic.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
+
import { word } from '../words.js';
|
|
3
4
|
/** Mosaic — one big photo + two small in a card (three Placeholders or imgs). */
|
|
4
|
-
export function Mosaic({ label = '
|
|
5
|
+
export function Mosaic({ label = word('photos'), className, ...rest }) {
|
|
5
6
|
return _jsx("div", { className: cx('mosaic', className), role: "img", "aria-label": label, ...rest });
|
|
6
7
|
}
|
|
7
8
|
/* a MOCK voice shape (Lh 2026-09-23): short and tall rounded bars, loud in bursts, quiet between — not a real recording */
|
|
8
9
|
const MOCK = [0.25, 0.45, 0.3, 0.6, 0.4, 0.2, 0.5, 0.85, 0.35, 0.7, 1, 0.55, 0.3, 0.75, 0.9, 0.45, 0.25, 0.6, 0.8, 0.4,
|
|
9
10
|
0.2, 0.5, 0.7, 0.35, 0.95, 0.6, 0.3, 0.45, 0.8, 0.5, 0.25, 0.65, 0.9, 0.4, 0.2, 0.55, 0.75, 0.35, 0.5, 0.3];
|
|
10
11
|
/** Wave — a voice note's waveform: rounded bars, short and tall, filling a Row's body. `levels` draws a real one; without it, a mock-up. */
|
|
11
|
-
export function Wave({ label = '
|
|
12
|
+
export function Wave({ label = word('voiceNote'), levels = MOCK, className, ...rest }) {
|
|
12
13
|
return (_jsx("span", { className: cx('wave', className), role: "img", "aria-label": label, ...rest, children: levels.map((v, i) => _jsx("i", { style: { '--wave-at': Math.max(0.12, Math.min(1, v)) } }, i)) }));
|
|
13
14
|
}
|
package/dist/numbers/Track.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
+
import { word } from '../words.js';
|
|
3
4
|
/** Track — step progress in a dark pill: done · done · todo; children are TrackSteps holding an Icon each. */
|
|
4
|
-
export function Track({ label = '
|
|
5
|
+
export function Track({ label = word('progress'), className, ...rest }) {
|
|
5
6
|
return _jsx("ol", { className: cx('track', className), "aria-label": label, ...rest });
|
|
6
7
|
}
|
|
7
8
|
/** TrackStep — one step of a Track: an Icon (children) or its number (`num`) in a round; lit when done. */
|
package/dist/sheets/Sheet.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
3
|
import { focusWhenActive, useControlled, useLatest, warnOnce, watchSize } from '../state.js';
|
|
4
4
|
import { cx } from '../cx.js';
|
|
5
|
+
import { word } from '../words.js';
|
|
5
6
|
const ORDER = ['peek', 'half', '3q', 'full']; // low → high; `away` is below them all and outside them
|
|
6
7
|
const RANK = { away: 0, peek: 1, half: 2, '3q': 3, full: 4 };
|
|
7
8
|
const TAP_REM = 0.5; // = --sp-2: a move shorter than this is a tap, not a drag
|
|
@@ -225,5 +226,5 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
|
|
|
225
226
|
skipClick.current = true;
|
|
226
227
|
requestAnimationFrame(() => { skipClick.current = false; });
|
|
227
228
|
};
|
|
228
|
-
return (_jsxs("article", { ref: ref, className: cx('card sheet', `sheet--${shown}`, className), inert: current === 'away' || undefined, onFocus: focus, ...rest, children: [handle !== false && (_jsx("button", { ref: handleRef, className: "sheet__handle", type: "button", "aria-label": typeof handle === 'string' ? handle : '
|
|
229
|
+
return (_jsxs("article", { ref: ref, className: cx('card sheet', `sheet--${shown}`, className), inert: current === 'away' || undefined, onFocus: focus, ...rest, children: [handle !== false && (_jsx("button", { ref: handleRef, className: "sheet__handle", type: "button", "aria-label": typeof handle === 'string' ? handle : word('resize'), onClick: tap, onPointerDown: down, onPointerMove: move, onPointerUp: up, onPointerCancel: up })), children] }));
|
|
229
230
|
}
|
|
@@ -6,6 +6,7 @@ import { Icon } from '../type/Icon.js';
|
|
|
6
6
|
import { Link } from '../actions/Link.js';
|
|
7
7
|
import { FormNote } from '../forms/FormNote.js';
|
|
8
8
|
import { CheckContext } from '../forms/check.js';
|
|
9
|
+
import { word } from '../words.js';
|
|
9
10
|
const MOVE_MS = 350; // = --motion-sheet: the accordion's beat
|
|
10
11
|
const GLIDE_MS = 220; // the pile's slide out of the keyboard's way and back: quick, but the eye follows it
|
|
11
12
|
const SWIPE_REM = 3; // an up/down move longer than this (and mostly up/down) is a swipe
|
|
@@ -183,7 +184,7 @@ export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible
|
|
|
183
184
|
_jsx("div", { className: "sheet-steps__finish", children: isValidElement(finish) ? cloneElement(finish, { disabled: reach < count || finish.props.disabled }) : finish }))] }));
|
|
184
185
|
}
|
|
185
186
|
/** SheetStep — one step of a SheetSteps: its title, then what the member does (a Field, a Pin, a Toggle, the step's own Btn). */
|
|
186
|
-
export function SheetStep({ title, summary, skippable, skipLabel = '
|
|
187
|
+
export function SheetStep({ title, summary, skippable, skipLabel = word('skip'), error, busy, className, children, ...rest }) {
|
|
187
188
|
const ctx = useContext(StepCtx);
|
|
188
189
|
if (!ctx)
|
|
189
190
|
throw new Error('SheetStep goes inside a SheetSteps');
|
package/dist/sheets/StackBack.js
CHANGED
|
@@ -2,8 +2,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
3
|
import { Icon } from '../type/Icon.js';
|
|
4
4
|
import { IconBtn } from '../actions/IconBtn.js';
|
|
5
|
+
import { word } from '../words.js';
|
|
5
6
|
/** 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.
|
|
6
7
|
* 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. */
|
|
7
|
-
export function StackBack({ label = '
|
|
8
|
+
export function StackBack({ label = word('back'), end, className, ...rest }) {
|
|
8
9
|
return _jsx(IconBtn, { sm: true, label: label, className: cx('sheet-stack__back', end && 'sheet-stack__back--end', className), ...rest, children: _jsx(Icon, { name: end ? 'chevron-down' : 'chevron-left' }) });
|
|
9
10
|
}
|
|
@@ -11,13 +11,14 @@ import { StackBack } from './StackBack.js';
|
|
|
11
11
|
import { Text } from '../type/Text.js';
|
|
12
12
|
import { Icon } from '../type/Icon.js';
|
|
13
13
|
import { Badge } from '../numbers/Badge.js';
|
|
14
|
+
import { word } from '../words.js';
|
|
14
15
|
/**
|
|
15
16
|
* StackSheet — ONE sheet of a `SheetStack`, in the one sheet shape: SHEET HEAD (back · title · tabs or an icon) pinned at the top ·
|
|
16
17
|
* SHEET BODY (the children; scrolls) · SHEET FOOT (`foot`, the CTA) at the bottom. Its tabs are icon buttons, the chosen one filled;
|
|
17
18
|
* a count shows as a small circle over its icon. The body is the app's: render the page of the chosen tab (and nothing while the sheet
|
|
18
19
|
* is closed — see `Home`). A direct child of `SheetStack`.
|
|
19
20
|
*/
|
|
20
|
-
export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel = '
|
|
21
|
+
export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel = word('back'), backAt = 'none', className, children, ...rest }) {
|
|
21
22
|
const [tab, pick] = useControlled(tabProp, defaultTab ?? tabs?.[0]?.id, onTabChange);
|
|
22
23
|
// 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
24
|
// is measured here (the tabs may grow with the text size) and handed to the CSS as --_over
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
2
|
import { Screen } from '../scaffold/Screen.js';
|
|
3
|
+
import { type SheetProps } from '../sheets/Sheet.js';
|
|
3
4
|
export interface CallSheetSheet {
|
|
4
5
|
/** the sheet head's title */
|
|
5
6
|
title: ReactNode;
|
|
@@ -9,6 +10,8 @@ export interface CallSheetSheet {
|
|
|
9
10
|
foot?: ReactNode;
|
|
10
11
|
/** a small link UNDER that button (a `Link sm`, e.g. details for developers) */
|
|
11
12
|
footLink?: ReactNode;
|
|
13
|
+
/** this sheet's handle — its accessible name (a string), or `false` for none; overrides the CallSheet's `handle` */
|
|
14
|
+
handle?: SheetProps['handle'];
|
|
12
15
|
}
|
|
13
16
|
/** one way in: its button and the sheet it calls */
|
|
14
17
|
export interface CallSheetWay {
|
|
@@ -36,6 +39,8 @@ export interface CallSheetProps extends Omit<ComponentProps<typeof Screen>, 'chi
|
|
|
36
39
|
onOpenChange?: (open: boolean, way: number | null) => void;
|
|
37
40
|
/** the sheets' resting states once up (default "peek full") */
|
|
38
41
|
states?: string;
|
|
42
|
+
/** every sheet's handle: its accessible name in the app's language (a string — default "Resize"), or `false` for none. A sheet's own `handle` wins */
|
|
43
|
+
handle?: SheetProps['handle'];
|
|
39
44
|
/** layers over the screen */
|
|
40
45
|
children?: ReactNode;
|
|
41
46
|
}
|
|
@@ -45,4 +50,4 @@ export interface CallSheetProps extends Omit<ComponentProps<typeof Screen>, 'chi
|
|
|
45
50
|
* 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
51
|
* One sheet up at a time — a sheet holds that way's WHOLE form.
|
|
47
52
|
*/
|
|
48
|
-
export declare function CallSheet({ media, heading, body, action, sheet, ways, open: openProp, defaultOpen, onOpenChange, states, children, ...rest }: CallSheetProps): import("react").JSX.Element;
|
|
53
|
+
export declare function CallSheet({ media, heading, body, action, sheet, ways, open: openProp, defaultOpen, onOpenChange, states, handle, children, ...rest }: CallSheetProps): import("react").JSX.Element;
|
|
@@ -17,12 +17,12 @@ const index = (o) => (o === true ? 0 : typeof o === 'number' ? o : null);
|
|
|
17
17
|
* 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.
|
|
18
18
|
* One sheet up at a time — a sheet holds that way's WHOLE form.
|
|
19
19
|
*/
|
|
20
|
-
export function CallSheet({ media, heading, body, action, sheet, ways, open: openProp, defaultOpen = null, onOpenChange, states = 'peek full', children, ...rest }) {
|
|
20
|
+
export function CallSheet({ media, heading, body, action, sheet, ways, open: openProp, defaultOpen = null, onOpenChange, states = 'peek full', handle, children, ...rest }) {
|
|
21
21
|
const list = ways ?? (sheet ? [{ action, sheet }] : []);
|
|
22
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
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')
|
|
25
25
|
set(null);
|
|
26
26
|
else
|
|
27
|
-
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
|
+
setAt(s); }, states: states, handle: sh.handle ?? handle, 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] }));
|
|
28
28
|
}
|
package/dist/templates/Home.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Screen } from '../scaffold/Screen.js';
|
|
|
5
5
|
import { SheetStack } from '../sheets/SheetStack.js';
|
|
6
6
|
import { StackSheet } from '../sheets/StackSheet.js';
|
|
7
7
|
import { Text } from '../type/Text.js';
|
|
8
|
+
import { word } from '../words.js';
|
|
8
9
|
const TONES = [2, 3, 1, 2, 3];
|
|
9
10
|
const LEAVE = 800; // ms: a closing sheet keeps its body until it has slid home
|
|
10
11
|
/**
|
|
@@ -14,7 +15,7 @@ const LEAVE = 800; // ms: a closing sheet keeps its body until it has slid home
|
|
|
14
15
|
* at the bottom). A tap on a head brings that sheet up to fill everything under the top. Only the open sheet's body is rendered — three
|
|
15
16
|
* sections on one home never cost three pages (`keep` holds a body once opened).
|
|
16
17
|
*/
|
|
17
|
-
export function Home({ top, title, sheets, open: openProp, defaultOpen = null, onOpenChange, base, keep, backLabel = '
|
|
18
|
+
export function Home({ top, title, sheets, open: openProp, defaultOpen = null, onOpenChange, base, keep, backLabel = word('back'), backAt, children, ...rest }) {
|
|
18
19
|
const [open, change] = useControlled(openProp, defaultOpen, onOpenChange);
|
|
19
20
|
const [leaving, setLeaving] = useState(null);
|
|
20
21
|
const [seen, setSeen] = useState(() => new Set(open != null ? [open] : []));
|
|
@@ -10,13 +10,14 @@ import { ActionBar, ActionBarTier } from '../scaffold/ActionBar.js';
|
|
|
10
10
|
import { CardHead } from '../cards/CardHead.js';
|
|
11
11
|
import { Text } from '../type/Text.js';
|
|
12
12
|
import { IconBtn } from '../actions/IconBtn.js';
|
|
13
|
+
import { word } from '../words.js';
|
|
13
14
|
/**
|
|
14
15
|
* Walkthrough — TEMPLATE, a `Screen` with its three parts filled — no top · content: `Pages` (down to the screen's foot; each Page padded by the lead above and below, the whole run scrolls) · bottom: a LAYER over it — the dots, then the `ActionBar` rows.
|
|
15
16
|
* A walk of pages that tell (onboarding, an introduction). Each page = a picture, a heading,
|
|
16
17
|
* the words; the dots at the foot; one row of calls to action at the bottom. No top bar. Fixed: the order, the picture
|
|
17
18
|
* first and fixed while the words scroll, one CTA row. Yours: the pages, the row, the state.
|
|
18
19
|
*/
|
|
19
|
-
export function Walkthrough({ pages, at: atProp, defaultAt = 0, onAtChange, cta, controls = [], ctaLink, children, backLabel = '
|
|
20
|
+
export function Walkthrough({ pages, at: atProp, defaultAt = 0, onAtChange, cta, controls = [], ctaLink, children, backLabel = word('back'), nextLabel = word('next'), ...rest }) {
|
|
20
21
|
const [at, turn] = useControlled(atProp, defaultAt, onAtChange);
|
|
21
22
|
const step = useRef(null);
|
|
22
23
|
// 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:
|
package/dist/words.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type WordKey = 'resize' | 'back' | 'next' | 'previous' | 'skip' | 'digit' | 'progress' | 'image' | 'mood' | 'photos' | 'voiceNote';
|
|
2
|
+
type Words = Record<WordKey, string>;
|
|
3
|
+
/** add or retune the words for a language (`'lo'`, or `'th'` to change one) — once, at app start. Missing words fall back to English. */
|
|
4
|
+
export declare function setWords(lang: string, words: Partial<Words>): void;
|
|
5
|
+
/** the word for `key` in the page's language (`<html lang>`: "th", "th-TH" → th); English where the page names none cardds knows */
|
|
6
|
+
export declare function word(key: WordKey): string;
|
|
7
|
+
export {};
|
package/dist/words.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const sets = {
|
|
2
|
+
en: { resize: 'Resize', back: 'Back', next: 'Next', previous: 'Previous', skip: 'Skip', digit: 'Digit', progress: 'Progress', image: 'Image', mood: 'Mood', photos: 'Photos', voiceNote: 'Voice note' },
|
|
3
|
+
th: { resize: 'ปรับความสูง', back: 'กลับ', next: 'ถัดไป', previous: 'ก่อนหน้า', skip: 'ข้าม', digit: 'หลักที่', progress: 'ความคืบหน้า', image: 'รูปภาพ', mood: 'อารมณ์', photos: 'รูปภาพ', voiceNote: 'ข้อความเสียง' },
|
|
4
|
+
};
|
|
5
|
+
/** add or retune the words for a language (`'lo'`, or `'th'` to change one) — once, at app start. Missing words fall back to English. */
|
|
6
|
+
export function setWords(lang, words) {
|
|
7
|
+
const key = lang.toLowerCase();
|
|
8
|
+
sets[key] = { ...sets.en, ...sets[key], ...words };
|
|
9
|
+
}
|
|
10
|
+
/** the word for `key` in the page's language (`<html lang>`: "th", "th-TH" → th); English where the page names none cardds knows */
|
|
11
|
+
export function word(key) {
|
|
12
|
+
const lang = typeof document === 'undefined' ? '' : document.documentElement.lang.toLowerCase();
|
|
13
|
+
return (sets[lang] ?? sets[lang.split('-')[0]] ?? sets.en)[key];
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.33",
|
|
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.",
|