@usableapp/cardds 0.7.84 → 0.7.86

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/dist/index.d.ts CHANGED
@@ -81,6 +81,7 @@ export * from './sticky/StickyField.js';
81
81
  export * from './sticky/StickyPick.js';
82
82
  export * from './sticky/StickyThread.js';
83
83
  export * from './sticky/StickyBoard.js';
84
+ export * from './sticky/StickyPile.js';
84
85
  export * from './sticky/StickyAnswer.js';
85
86
  export * from './stamp/Stamp.js';
86
87
  export * from './stamp/StampBook.js';
package/dist/index.js CHANGED
@@ -85,6 +85,7 @@ export * from './sticky/StickyField.js';
85
85
  export * from './sticky/StickyPick.js';
86
86
  export * from './sticky/StickyThread.js';
87
87
  export * from './sticky/StickyBoard.js';
88
+ export * from './sticky/StickyPile.js';
88
89
  export * from './sticky/StickyAnswer.js';
89
90
  export * from './stamp/Stamp.js';
90
91
  export * from './stamp/StampBook.js';
@@ -15,12 +15,36 @@ export interface StampOnProps extends ComponentProps<'div'> {
15
15
  onAtChange?: (at: StampAt) => void;
16
16
  /** a draggable stamp's accessible name ("แสตมป์ของต้น") */
17
17
  label?: string;
18
+ /** the stamp can be TAKEN OFF: when a drag ends, an arrow key moves it, or (on a stamp that cannot be dragged) it is pressed, a cross stands on its top right corner for `removeFor` ms; a press on the cross — or Delete / Backspace on the focused stamp — ASKS. The app owns what is stuck (as it owns `at`): it takes the `StampOn` away. Without it nothing changes: no cross, the stamp is dragged as before and cannot be taken off. A press or a drag on a removable stamp never reaches what lies under it */
19
+ onRemove?: () => void;
20
+ /** the cross's accessible name (default: cardds's own word, "Remove" / "เอาออก" by the page's language) */
21
+ removeLabel?: string;
22
+ /** how long the cross stands after the stamp was touched, in ms (default 3000). It stays while it holds the keyboard focus; a new drag takes it away until the drag ends */
23
+ removeFor?: number;
24
+ /** the stamp at the stamp's MAIN size, 96 × 120 (`--stamp-size`), to cover what it lies on; the default is the small size, 48 × 60 (`--stamp-sm-size`), which leaves a note's words readable */
25
+ main?: boolean;
26
+ /** the stamp sticks ITSELF: as it comes onto the screen it is struck onto its place (shown large, pressed down to its size). Nobody
27
+ * picked it or pressed to stick it — the app shows it when its own move (send) begins. With `onArrived` */
28
+ arrive?: boolean;
29
+ /** an arriving stamp has LANDED (its strike ended; at once where motion is off) — the app does what follows: sends, closes */
30
+ onArrived?: () => void;
18
31
  }
19
32
  /**
20
33
  * StampOn — a Stamp (the child) stuck on a surface at the place the member chose. The surface is the nearest positioned box:
21
34
  * a Sticky's `stuck`, a postcard's face. `at` is the stamp's middle as a share of that surface, so the note reads the same at any size.
22
- * The stamp is the SMALL size: 48 × 60 on any surface, whatever the surface's size (`--stamp-sm-size`, one tap wide).
35
+ * The stamp is the SMALL size: 48 × 60 on any surface, whatever the surface's size (`--stamp-sm-size`, one tap wide); `main` makes it the
36
+ * stamp's main size, 96 × 120, to cover what it lies on.
23
37
  * With `onAtChange` the member drags it (finger or mouse; the arrow keys move it too): while the finger holds, the stamp writes its
24
38
  * own place, and on letting go it ASKS — the app owns `at`. It never leaves the surface.
39
+ * With `onRemove` it can be taken off: once touched (a drag let go, an arrow key, or a press on a stamp that cannot be dragged) a cross
40
+ * stands on its top right corner for `removeFor` ms; a press on the cross, or Delete / Backspace on the focused stamp, ASKS, and the app
41
+ * takes the stamp away. `onRemove` without `onAtChange` is a stamp that stays where it is and is taken off: a press shows the cross
42
+ * (a warning instead would leave the prop dead, since the cross needs a touch to appear). A press or a drag on a removable stamp or its
43
+ * cross stops there, so a pressable card or pile under it is not pressed. The cross stays upright whatever the stamp's `turn`: it is a
44
+ * button, and its 44 × 44 touch target round the 24 disc should be square to the screen.
45
+ * Inside a `StickyPile`, a stamp on a note under the top one is inert: no drag, no cross, no focus.
46
+ * With `arrive` it sticks itself: struck onto its place as it comes on, and `onArrived` tells the app it has landed. A stamp that is still
47
+ * arriving cannot be taken off: no cross, no focus, no Delete until it has landed (it is not stuck yet). `arrive` with `main` strikes
48
+ * the main size, 96 × 120, onto its place.
25
49
  */
26
- export declare function StampOn({ at, defaultAt, turn, onAtChange, label, className, style, children, ...rest }: StampOnProps): import("react").JSX.Element;
50
+ export declare function StampOn({ at, defaultAt, turn, onAtChange, label, onRemove, removeLabel, removeFor, main, arrive, onArrived, className, style, onAnimationEnd, children, ...rest }: StampOnProps): import("react").JSX.Element;
@@ -1,7 +1,10 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useRef, useState } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useContext, useEffect, useRef, useState } from 'react';
3
3
  import { cx } from '../cx.js';
4
- import { useControlled } from '../state.js';
4
+ import { reducedMotion, useControlled } from '../state.js';
5
+ import { Icon } from '../type/Icon.js';
6
+ import { word } from '../words.js';
7
+ import { UnderNotes } from './underNotes.js';
5
8
  const MIDDLE = { x: 0.5, y: 0.5 };
6
9
  const STEP = 0.02; // an arrow key's move: a share of the surface
7
10
  const MOVES = { ArrowLeft: [-STEP, 0], ArrowRight: [STEP, 0], ArrowUp: [0, -STEP], ArrowDown: [0, STEP] };
@@ -15,21 +18,42 @@ function within(el, x, y) {
15
18
  const mx = el.offsetWidth / 2 / box.clientWidth, my = el.offsetHeight / 2 / box.clientHeight;
16
19
  return { x: round(clamp(x, mx, 1 - mx)), y: round(clamp(y, my, 1 - my)) };
17
20
  }
21
+ const STRIKE = 'stamp-on-strike'; // css/stamp.css
18
22
  const write = (el, at) => { el.style.setProperty('--stamp-on-x', String(at.x)); el.style.setProperty('--stamp-on-y', String(at.y)); };
19
23
  /**
20
24
  * StampOn — a Stamp (the child) stuck on a surface at the place the member chose. The surface is the nearest positioned box:
21
25
  * a Sticky's `stuck`, a postcard's face. `at` is the stamp's middle as a share of that surface, so the note reads the same at any size.
22
- * The stamp is the SMALL size: 48 × 60 on any surface, whatever the surface's size (`--stamp-sm-size`, one tap wide).
26
+ * The stamp is the SMALL size: 48 × 60 on any surface, whatever the surface's size (`--stamp-sm-size`, one tap wide); `main` makes it the
27
+ * stamp's main size, 96 × 120, to cover what it lies on.
23
28
  * With `onAtChange` the member drags it (finger or mouse; the arrow keys move it too): while the finger holds, the stamp writes its
24
29
  * own place, and on letting go it ASKS — the app owns `at`. It never leaves the surface.
30
+ * With `onRemove` it can be taken off: once touched (a drag let go, an arrow key, or a press on a stamp that cannot be dragged) a cross
31
+ * stands on its top right corner for `removeFor` ms; a press on the cross, or Delete / Backspace on the focused stamp, ASKS, and the app
32
+ * takes the stamp away. `onRemove` without `onAtChange` is a stamp that stays where it is and is taken off: a press shows the cross
33
+ * (a warning instead would leave the prop dead, since the cross needs a touch to appear). A press or a drag on a removable stamp or its
34
+ * cross stops there, so a pressable card or pile under it is not pressed. The cross stays upright whatever the stamp's `turn`: it is a
35
+ * button, and its 44 × 44 touch target round the 24 disc should be square to the screen.
36
+ * Inside a `StickyPile`, a stamp on a note under the top one is inert: no drag, no cross, no focus.
37
+ * With `arrive` it sticks itself: struck onto its place as it comes on, and `onArrived` tells the app it has landed. A stamp that is still
38
+ * arriving cannot be taken off: no cross, no focus, no Delete until it has landed (it is not stuck yet). `arrive` with `main` strikes
39
+ * the main size, 96 × 120, onto its place.
25
40
  */
26
- export function StampOn({ at, defaultAt = MIDDLE, turn = 0, onAtChange, label, className, style, children, ...rest }) {
41
+ export function StampOn({ at, defaultAt = MIDDLE, turn = 0, onAtChange, label, onRemove, removeLabel, removeFor = 3000, main, arrive, onArrived, className, style, onAnimationEnd, children, ...rest }) {
42
+ const under = useContext(UnderNotes);
27
43
  const [shown, request] = useControlled(at, defaultAt, onAtChange);
28
44
  const [held, setHeld] = useState(false);
45
+ const [cross, setCross] = useState(false);
29
46
  const grip = useRef(null);
30
- const moves = onAtChange != null;
47
+ const timer = useRef(undefined);
48
+ const moves = onAtChange != null && !under;
49
+ const [arrived, setArrived] = useState(false);
50
+ const removes = onRemove != null && !under && !(arrive && !arrived);
31
51
  const ask = (next) => { if (next.x !== shown.x || next.y !== shown.y)
32
52
  request(next); };
53
+ const stop = () => clearTimeout(timer.current);
54
+ const show = () => { if (!removes)
55
+ return; stop(); setCross(true); timer.current = setTimeout(() => setCross(false), removeFor); };
56
+ useEffect(() => stop, []);
33
57
  const down = (e) => {
34
58
  const el = e.currentTarget, box = el.offsetParent;
35
59
  if (!box || e.button !== 0)
@@ -38,6 +62,8 @@ export function StampOn({ at, defaultAt = MIDDLE, turn = 0, onAtChange, label, c
38
62
  grip.current = { dx: (e.clientX - r.left - box.clientLeft) / box.clientWidth - shown.x, dy: (e.clientY - r.top - box.clientTop) / box.clientHeight - shown.y, next: shown };
39
63
  el.setPointerCapture(e.pointerId);
40
64
  setHeld(true);
65
+ stop();
66
+ setCross(false); // a new drag takes the cross away until it ends
41
67
  };
42
68
  const move = (e) => {
43
69
  const el = e.currentTarget, box = el.offsetParent, g = grip.current;
@@ -54,17 +80,59 @@ export function StampOn({ at, defaultAt = MIDDLE, turn = 0, onAtChange, label, c
54
80
  grip.current = null;
55
81
  setHeld(false);
56
82
  write(e.currentTarget, shown); // back to what the app has: if it takes the new place, the next render writes it
57
- if (e.type === 'pointerup')
83
+ if (e.type === 'pointerup') {
58
84
  ask(g.next);
85
+ show();
86
+ }
59
87
  };
60
88
  const key = (e) => {
61
- const by = MOVES[e.key];
62
- if (!by)
63
- return;
64
- e.preventDefault();
65
- ask(within(e.currentTarget, shown.x + by[0], shown.y + by[1]));
89
+ if (e.target !== e.currentTarget)
90
+ return; // the cross has its own keys
91
+ const by = moves ? MOVES[e.key] : undefined;
92
+ if (by) {
93
+ e.preventDefault();
94
+ e.stopPropagation();
95
+ ask(within(e.currentTarget, shown.x + by[0], shown.y + by[1]));
96
+ show();
97
+ }
98
+ else if (removes && (e.key === 'Delete' || e.key === 'Backspace')) {
99
+ e.preventDefault();
100
+ e.stopPropagation();
101
+ stop();
102
+ setCross(false);
103
+ onRemove();
104
+ }
105
+ else if (removes && !moves && (e.key === 'Enter' || e.key === ' ')) {
106
+ e.preventDefault();
107
+ e.stopPropagation();
108
+ show();
109
+ }
110
+ };
111
+ // nothing that starts on a removable stamp or its cross reaches what lies under it: a click is the end of a press, and a drag's end is one too
112
+ const click = (e) => {
113
+ e.stopPropagation();
114
+ rest.onClick?.(e);
115
+ if (!moves)
116
+ show();
117
+ };
118
+ const landed = useRef(onArrived);
119
+ landed.current = onArrived;
120
+ const land = () => { setArrived(true); landed.current?.(); };
121
+ useEffect(() => { if (!arrive)
122
+ setArrived(false);
123
+ else if (reducedMotion()) {
124
+ setArrived(true);
125
+ landed.current?.();
126
+ } }, [arrive]); // no strike to wait for
127
+ const struck = (e) => {
128
+ onAnimationEnd?.(e);
129
+ if (arrive && e.target === e.currentTarget && e.animationName === STRIKE)
130
+ land();
66
131
  };
67
132
  const place = { '--stamp-on-x': shown.x, '--stamp-on-y': shown.y, '--stamp-on-turn': turn, ...style };
68
- const drag = moves ? { tabIndex: 0, role: 'group', 'aria-label': label, onPointerDown: down, onPointerMove: move, onPointerUp: up, onPointerCancel: up, onKeyDown: key } : null;
69
- return (_jsx("div", { className: cx('stamp-on', moves && 'stamp-on--move', held && 'is-held', className), style: place, ...rest, ...drag, children: children }));
133
+ const own = moves || removes ? { tabIndex: 0, role: 'group', 'aria-label': label, onKeyDown: key } : null;
134
+ const drag = moves ? { onPointerDown: down, onPointerMove: move, onPointerUp: up, onPointerCancel: up } : null;
135
+ const guard = removes ? { onClick: click } : null;
136
+ return (_jsxs("div", { className: cx('stamp-on', moves && 'stamp-on--move', main && 'stamp-on--main', arrive && 'stamp-on--arrive', held && 'is-held', className), style: place, onAnimationEnd: struck, ...rest, ...own, ...drag, ...guard, inert: under || undefined, children: [children, removes && cross && (_jsx("button", { type: "button", className: "stamp-on__remove", "aria-label": removeLabel ?? word('remove'), onPointerDown: (e) => e.stopPropagation(), onClick: (e) => { e.stopPropagation(); stop(); setCross(false); onRemove(); }, onFocus: stop, onBlur: () => { if (!grip.current)
137
+ show(); }, children: _jsx("span", { className: "stamp-on__remove-disc", children: _jsx(Icon, { name: "x" }) }) }))] }));
70
138
  }
@@ -0,0 +1,3 @@
1
+ /** set by a holder whose notes lie one under another (`StickyPile`): a stamp on a note that only shows what peeks out is inert — no drag, no cross, no focus.
2
+ * Not part of the package's API: a holder writes it, `StampOn` reads it. */
3
+ export declare const UnderNotes: import("react").Context<boolean>;
@@ -0,0 +1,4 @@
1
+ import { createContext } from 'react';
2
+ /** set by a holder whose notes lie one under another (`StickyPile`): a stamp on a note that only shows what peeks out is inert — no drag, no cross, no focus.
3
+ * Not part of the package's API: a holder writes it, `StampOn` reads it. */
4
+ export const UnderNotes = createContext(false);
@@ -13,13 +13,13 @@ export interface StickyProps extends Omit<ComponentProps<'div'>, 'onClick'> {
13
13
  head?: ReactNode;
14
14
  /** a StickyDate at the end of the head — the note's top-right corner */
15
15
  date?: ReactNode;
16
- /** turned a little on its middle (the one note of a start screen); a note is straight otherwise, unless its holder turns it (`StickyBoard`, `StickyThread lapped`) */
16
+ /** turned a little on its middle (the one note of a start screen); a note is straight otherwise, unless its holder turns it (`StickyBoard`, `StickyPile`, `StickyThread lapped`) */
17
17
  tilt?: 'left' | 'right';
18
18
  /** a small muted line at the bottom ("From …") */
19
19
  foot?: ReactNode;
20
20
  /** a control (an IconBtn) standing on the bottom-right corner, half outside the note; the note then has no fold */
21
21
  corner?: ReactNode;
22
- /** things stuck ON the note, each placing itself over the paper (a `StampOn`) — a layer the size of the note, in front of the words */
22
+ /** things stuck ON the note, each placing itself over the paper (a `StampOn`: the small stamp, or `main` for the main size) — a layer the size of the note, in front of the words */
23
23
  stuck?: ReactNode;
24
24
  }
25
25
  /**
@@ -0,0 +1,20 @@
1
+ import { type ComponentProps } from 'react';
2
+ export interface StickyPileProps extends Omit<ComponentProps<'div'>, 'onClick'> {
3
+ /** the whole pile is ONE button: it renders a `<button>` over the notes and says that it was pressed — one note or many, and where the press goes, is the app's. Without it the pile only shows. A `Sticky` inside with an `onClick` of its own is misuse (two buttons for one pile): the pile warns once */
4
+ onClick?: () => void;
5
+ /** the pile's accessible name, REQUIRED with `onClick` (the notes' words are not read by the button): say how many and from whom, in the app's words. Warns once without it */
6
+ label?: string;
7
+ }
8
+ /**
9
+ * StickyPile — ONE to SIX `Sticky` notes lying one over another, as the newest notes of a feed do. The FIRST child is on top (the app passes
10
+ * the newest first); each note under it is shifted a step right and down, so a strip of it peeks out (`--sticky-pile-step-x`, 24, and
11
+ * `--sticky-pile-step-y`, 8) and turned a little by the same four angles as `StickyBoard` (−3° · 1.5° · 2.75° · −1.75°, by the child's place,
12
+ * so the top note's turn is the same whether the pile holds one note or six). Only the top note draws its `foot`: a cut time under another note is
13
+ * noise. A pile of one is that note, centred, with its foot. A seventh note is not drawn (a warning, once).
14
+ * The pile is centred in its column and never wider than it: a column too narrow for the steps shortens them (a lone note's width is the least it takes).
15
+ * It knows nothing of a feed or a home: in a Home's `base` it stands above a `CardFeed`, and the app writes both.
16
+ * With `onClick` the whole pile is one control with a name (`label`): one Tab stop, a ring round the pile, dimmed while it is held.
17
+ * A `StampOn` on the TOP note keeps its own touch — dragging it, or pressing its cross, never presses the pile — and the stamps on the notes under it are inert
18
+ * (no drag, no cross, no focus: they only show what peeks out), whatever the app passed.
19
+ */
20
+ export declare function StickyPile({ onClick, label, className, style, children, ...rest }: StickyPileProps): import("react").JSX.Element;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Children, isValidElement } from 'react';
3
+ import { cx } from '../cx.js';
4
+ import { warnOnce } from '../state.js';
5
+ import { UnderNotes } from '../stamp/underNotes.js';
6
+ /** most notes a pile draws; a seventh is dropped, as a Stamp drops a fifth picture */
7
+ const MOST = 6;
8
+ /**
9
+ * StickyPile — ONE to SIX `Sticky` notes lying one over another, as the newest notes of a feed do. The FIRST child is on top (the app passes
10
+ * the newest first); each note under it is shifted a step right and down, so a strip of it peeks out (`--sticky-pile-step-x`, 24, and
11
+ * `--sticky-pile-step-y`, 8) and turned a little by the same four angles as `StickyBoard` (−3° · 1.5° · 2.75° · −1.75°, by the child's place,
12
+ * so the top note's turn is the same whether the pile holds one note or six). Only the top note draws its `foot`: a cut time under another note is
13
+ * noise. A pile of one is that note, centred, with its foot. A seventh note is not drawn (a warning, once).
14
+ * The pile is centred in its column and never wider than it: a column too narrow for the steps shortens them (a lone note's width is the least it takes).
15
+ * It knows nothing of a feed or a home: in a Home's `base` it stands above a `CardFeed`, and the app writes both.
16
+ * With `onClick` the whole pile is one control with a name (`label`): one Tab stop, a ring round the pile, dimmed while it is held.
17
+ * A `StampOn` on the TOP note keeps its own touch — dragging it, or pressing its cross, never presses the pile — and the stamps on the notes under it are inert
18
+ * (no drag, no cross, no focus: they only show what peeks out), whatever the app passed.
19
+ */
20
+ export function StickyPile({ onClick, label, className, style, children, ...rest }) {
21
+ const all = Children.toArray(children).filter((c) => isValidElement(c));
22
+ if (all.length > MOST)
23
+ warnOnce('sticky-pile-most', `cardds StickyPile: ${all.length} notes — at most six are drawn; show the rest elsewhere`);
24
+ const notes = all.slice(0, MOST);
25
+ const tap = onClick != null;
26
+ if (tap && label == null)
27
+ warnOnce('sticky-pile-label', 'cardds StickyPile: onClick without a label — the pile is one button and the notes\' words are not its name');
28
+ if (tap && notes.some((n) => n.props.onClick != null))
29
+ warnOnce('sticky-pile-tap', 'cardds StickyPile: a Sticky with its own onClick inside a pile that has one — two buttons for one pile; give the pile the onClick');
30
+ return (_jsx("div", { className: cx('sticky-pile', tap && 'sticky-pile--tap', className), style: { '--_n': notes.length, ...style }, ...rest, children: _jsxs("div", { className: "sticky-pile__notes", children: [notes.map((note, i) => _jsx(UnderNotes.Provider, { value: i > 0, children: note }, note.key)), tap && _jsx("button", { type: "button", className: "sticky-pile__press", "aria-label": label, onClick: onClick })] }) }));
31
+ }
package/dist/words.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type WordKey = 'resize' | 'back' | 'next' | 'previous' | 'skip' | 'digit' | 'progress' | 'image' | 'mood' | 'photos' | 'voiceNote' | 'play' | 'pause' | 'to' | 'turnToBack' | 'turnToPicture' | 'message' | 'address' | 'toMessage' | 'toAddress' | 'stampOn' | 'stampOff' | 'stamps' | 'unread';
1
+ export type WordKey = 'resize' | 'back' | 'next' | 'previous' | 'skip' | 'digit' | 'progress' | 'image' | 'mood' | 'photos' | 'voiceNote' | 'play' | 'pause' | 'to' | 'turnToBack' | 'turnToPicture' | 'message' | 'address' | 'toMessage' | 'toAddress' | 'stampOn' | 'stampOff' | 'stamps' | 'unread' | 'remove';
2
2
  type Words = Record<WordKey, string>;
3
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
4
  export declare function setWords(lang: string, words: Partial<Words>): void;
package/dist/words.js CHANGED
@@ -1,8 +1,8 @@
1
1
  const sets = {
2
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
- play: 'Play', pause: 'Pause', to: 'To', turnToBack: 'See the back', turnToPicture: 'See the picture', message: 'Message', address: 'Address', toMessage: 'Back to the message', toAddress: 'On to the address', stampOn: 'Stick a stamp', stampOff: 'Take the stamp off', stamps: 'Choose a stamp', unread: 'Unread' },
3
+ play: 'Play', pause: 'Pause', to: 'To', turnToBack: 'See the back', turnToPicture: 'See the picture', message: 'Message', address: 'Address', toMessage: 'Back to the message', toAddress: 'On to the address', stampOn: 'Stick a stamp', stampOff: 'Take the stamp off', stamps: 'Choose a stamp', unread: 'Unread', remove: 'Remove' },
4
4
  th: { resize: 'ปรับความสูง', back: 'กลับ', next: 'ถัดไป', previous: 'ก่อนหน้า', skip: 'ข้าม', digit: 'หลักที่', progress: 'ความคืบหน้า', image: 'รูปภาพ', mood: 'อารมณ์', photos: 'รูปภาพ', voiceNote: 'ข้อความเสียง',
5
- play: 'ฟังเสียง', pause: 'หยุดเสียง', to: 'ถึง', turnToBack: 'พลิกดูด้านหลัง', turnToPicture: 'พลิกดูรูป', message: 'ข้อความ', address: 'จ่าหน้า', toMessage: 'เลื่อนกลับไปข้อความ', toAddress: 'เลื่อนไปจ่าหน้า', stampOn: 'ติดแสตมป์', stampOff: 'แกะแสตมป์ออก', stamps: 'เลือกแสตมป์', unread: 'ยังไม่ได้อ่าน' },
5
+ play: 'ฟังเสียง', pause: 'หยุดเสียง', to: 'ถึง', turnToBack: 'พลิกดูด้านหลัง', turnToPicture: 'พลิกดูรูป', message: 'ข้อความ', address: 'จ่าหน้า', toMessage: 'เลื่อนกลับไปข้อความ', toAddress: 'เลื่อนไปจ่าหน้า', stampOn: 'ติดแสตมป์', stampOff: 'แกะแสตมป์ออก', stamps: 'เลือกแสตมป์', unread: 'ยังไม่ได้อ่าน', remove: 'เอาออก' },
6
6
  };
7
7
  /** add or retune the words for a language (`'lo'`, or `'th'` to change one) — once, at app start. Missing words fall back to English. */
8
8
  export function setWords(lang, words) {
package/lint/rules.mjs CHANGED
@@ -12,7 +12,7 @@ const CSS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'css');
12
12
  /** the props whose default is cardds's own word (src/words.ts), per component — a `label` that is a control's required accessible name (IconBtn, Fab, Pin…) is not here */
13
13
  export const CONTROL_NAMES = {
14
14
  Sheet: ['handle'], CallSheet: ['handle'], StackSheet: ['backLabel'], Home: ['backLabel'], Walkthrough: ['backLabel', 'nextLabel'],
15
- Calendar: ['prevLabel', 'nextLabel'], SheetStep: ['skipLabel'], Sticky: ['unreadLabel'], PostLetter: ['backLabel', 'pictureLabel', 'playLabel', 'pauseLabel', 'toLabel'],
15
+ Calendar: ['prevLabel', 'nextLabel'], SheetStep: ['skipLabel'], Sticky: ['unreadLabel'], StampOn: ['removeLabel'], PostLetter: ['backLabel', 'pictureLabel', 'playLabel', 'pauseLabel', 'toLabel'],
16
16
  };
17
17
 
18
18
  const lineOf = (text, idx) => text.slice(0, idx).split('\n').length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.84",
3
+ "version": "0.7.86",
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.",