@usableapp/cardds 0.7.85 → 0.7.87
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 +33 -0
- package/README.md +44 -1
- package/bin/cardds.mjs +8 -3
- package/css/actions.css +12 -7
- package/css/card.css +1 -2
- package/css/choice.css +2 -2
- package/css/finish.css +97 -11
- package/css/forms.css +1 -2
- package/css/numbers.css +1 -2
- package/css/post.css +1 -2
- package/css/stack.css +1 -1
- package/css/stamp.css +44 -4
- package/css/step.css +6 -0
- package/css/sticky.css +45 -2
- package/css/tokens.css +6 -0
- package/dist/cardds.css +217 -35
- package/dist/cards/CardFeed.js +2 -9
- package/dist/catalog.json +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lists/Row.js +2 -9
- package/dist/pressable.d.ts +8 -0
- package/dist/pressable.js +6 -0
- package/dist/scaffold/Screen.js +2 -9
- package/dist/sheets/StackSheet.js +3 -7
- package/dist/sheets/sheetMeasure.d.ts +1 -0
- package/dist/sheets/sheetMeasure.js +14 -9
- package/dist/sheets/stackCard.js +41 -48
- package/dist/stamp/Stamp.js +3 -6
- package/dist/stamp/StampBook.js +2 -3
- package/dist/stamp/StampOn.d.ts +21 -3
- package/dist/stamp/StampOn.js +75 -18
- package/dist/stamp/underNotes.d.ts +3 -0
- package/dist/stamp/underNotes.js +4 -0
- package/dist/state.d.ts +5 -1
- package/dist/state.js +8 -0
- package/dist/sticky/Sticky.d.ts +2 -2
- package/dist/sticky/Sticky.js +2 -3
- package/dist/sticky/StickyPile.d.ts +20 -0
- package/dist/sticky/StickyPile.js +31 -0
- package/dist/words.d.ts +1 -1
- package/dist/words.js +2 -2
- package/lib/catalog.mjs +24 -7
- package/lib/mcp.mjs +3 -3
- package/lint/rules.mjs +1 -1
- package/package.json +1 -1
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';
|
package/dist/lists/Row.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { cloneElement, createElement, isValidElement, useId, useLayoutEffect, useRef } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
import { Icon } from '../type/Icon.js';
|
|
5
|
-
import { reducedMotion, useDisclosure } from '../state.js';
|
|
5
|
+
import { reducedMotion, setRefs, useDisclosure } from '../state.js';
|
|
6
6
|
/**
|
|
7
7
|
* Row — a frameless row inside a card: lead · title + meta · end. 48px+ tall, a tap target.
|
|
8
8
|
* Several go in Rows (hairlines between). A row that IS a card is `Card row`.
|
|
@@ -56,12 +56,5 @@ export function Rows({ heading, className, children, ref, ...rest }) {
|
|
|
56
56
|
was.current = at;
|
|
57
57
|
order.current = rows;
|
|
58
58
|
});
|
|
59
|
-
|
|
60
|
-
own.current = node;
|
|
61
|
-
if (typeof ref === 'function')
|
|
62
|
-
ref(node);
|
|
63
|
-
else if (ref)
|
|
64
|
-
ref.current = node;
|
|
65
|
-
};
|
|
66
|
-
return (_jsxs("div", { ref: setRef, className: cx('rows', className), role: named ? 'group' : undefined, "aria-labelledby": named ? id : undefined, ...rest, children: [named && (isValidElement(heading) ? cloneElement(heading, { id }) : _jsx("span", { className: "rows__head", id: id, children: heading })), children] }));
|
|
59
|
+
return (_jsxs("div", { ref: setRefs(own, ref), className: cx('rows', className), role: named ? 'group' : undefined, "aria-labelledby": named ? id : undefined, ...rest, children: [named && (isValidElement(heading) ? cloneElement(heading, { id }) : _jsx("span", { className: "rows__head", id: id, children: heading })), children] }));
|
|
67
60
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
/** a box that is a `<button>` when it can be pressed (`onClick` given), else `still` — the tag to render and what to spread on it:
|
|
3
|
+
* `type="button"`, the onClick and `extra` (attributes a button alone takes, `aria-pressed`) before `rest`. `tap` says which it is */
|
|
4
|
+
export declare function pressable<E extends 'span' | 'div'>(onClick: (() => void) | undefined, still: E, rest: ComponentProps<E>, extra?: ComponentProps<'button'>): {
|
|
5
|
+
tap: boolean;
|
|
6
|
+
Box: E;
|
|
7
|
+
boxProps: ComponentProps<E>;
|
|
8
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** a box that is a `<button>` when it can be pressed (`onClick` given), else `still` — the tag to render and what to spread on it:
|
|
2
|
+
* `type="button"`, the onClick and `extra` (attributes a button alone takes, `aria-pressed`) before `rest`. `tap` says which it is */
|
|
3
|
+
export function pressable(onClick, still, rest, extra) {
|
|
4
|
+
const tap = onClick != null;
|
|
5
|
+
return { tap, Box: (tap ? 'button' : still), boxProps: (tap ? { type: 'button', onClick, ...extra, ...rest } : rest) };
|
|
6
|
+
}
|
package/dist/scaffold/Screen.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createElement, useEffect, useRef } from 'react';
|
|
2
|
-
import { warnOnce, watchSize } from '../state.js';
|
|
2
|
+
import { setRefs, warnOnce, watchSize } from '../state.js';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
/**
|
|
5
5
|
* Screen — the mobile screen in THREE parts: **top** and **content** (the children) in one flex column, the content down to the
|
|
@@ -39,12 +39,5 @@ export function Screen({ fill, palette, as = 'div', top, bottom, ready = true, c
|
|
|
39
39
|
}
|
|
40
40
|
return watchSize([lead], () => el.style.setProperty('--top-h', `${lead.offsetHeight}px`));
|
|
41
41
|
}, [top, ready]);
|
|
42
|
-
|
|
43
|
-
own.current = node;
|
|
44
|
-
if (typeof ref === 'function')
|
|
45
|
-
ref(node);
|
|
46
|
-
else if (ref)
|
|
47
|
-
ref.current = node;
|
|
48
|
-
};
|
|
49
|
-
return createElement(as, { className: cx('screen', fill && 'screen--fill', top === 'lead' && 'screen--lead', className), 'data-palette': palette, 'data-pending': ready ? undefined : '', 'aria-busy': ready ? undefined : true, ref: setRef, ...rest }, ready && top !== 'lead' ? top : null, ready ? children : null, ready ? bottom : null);
|
|
42
|
+
return createElement(as, { className: cx('screen', fill && 'screen--fill', top === 'lead' && 'screen--lead', className), 'data-palette': palette, 'data-pending': ready ? undefined : '', 'aria-busy': ready ? undefined : true, ref: setRefs(own, ref), ...rest }, ready && top !== 'lead' ? top : null, ready ? children : null, ready ? bottom : null);
|
|
50
43
|
}
|
|
@@ -12,6 +12,7 @@ import { Text } from '../type/Text.js';
|
|
|
12
12
|
import { Icon } from '../type/Icon.js';
|
|
13
13
|
import { Badge } from '../numbers/Badge.js';
|
|
14
14
|
import { word } from '../words.js';
|
|
15
|
+
import { runOver } from './sheetMeasure.js';
|
|
15
16
|
/**
|
|
16
17
|
* StackSheet — ONE sheet of a `SheetStack`, in the one sheet shape: SHEET HEAD (back · title · tabs or an icon) pinned at the top ·
|
|
17
18
|
* SHEET BODY (the children; scrolls) · SHEET FOOT (`foot`, the CTA) at the bottom. Its tabs are icon buttons. The chosen one is filled
|
|
@@ -22,19 +23,14 @@ import { word } from '../words.js';
|
|
|
22
23
|
export function StackSheet({ title, tone, paper, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel = word('back'), backAt = 'none', stack, className, children, ...rest }) {
|
|
23
24
|
const [tab, pick] = useControlled(tabProp, defaultTab ?? tabs?.[0]?.id, onTabChange);
|
|
24
25
|
// 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
|
|
25
|
-
// is measured here (the tabs may grow with the text size) and handed to the CSS as --_over
|
|
26
|
+
// is measured here (the tabs may grow with the text size) and handed to the CSS as --_over / --_run, the Sheet's measurer
|
|
26
27
|
const titleRef = useRef(null);
|
|
27
28
|
const [long, setLong] = useState(false);
|
|
28
29
|
useEffect(() => {
|
|
29
30
|
const h = titleRef.current, run = h?.firstElementChild;
|
|
30
31
|
if (!h || !run)
|
|
31
32
|
return;
|
|
32
|
-
const measure = () =>
|
|
33
|
-
const over = Math.max(0, Math.ceil(run.scrollWidth - h.clientWidth));
|
|
34
|
-
h.style.setProperty('--_over', `${over}px`);
|
|
35
|
-
h.style.setProperty('--_run', `${(over / 30 + 3).toFixed(1)}s`);
|
|
36
|
-
setLong(over > 0);
|
|
37
|
-
};
|
|
33
|
+
const measure = () => setLong(runOver(h, run));
|
|
38
34
|
measure();
|
|
39
35
|
return watchSize([h, run], measure);
|
|
40
36
|
}, [title]);
|
|
@@ -5,6 +5,7 @@ type SheetRef = RefObject<HTMLElement | null>;
|
|
|
5
5
|
export declare const slid: (el: HTMLElement) => number;
|
|
6
6
|
export declare const stopsOf: (el: HTMLElement) => Record<Resting, number>;
|
|
7
7
|
export declare function usePeekStrip(ref: SheetRef): void;
|
|
8
|
+
export declare function runOver(line: HTMLElement, words?: Node): boolean;
|
|
8
9
|
export declare function useTitleRun(ref: SheetRef, onLong: (long: boolean) => void): void;
|
|
9
10
|
export declare function useFootRoom(ref: SheetRef): void;
|
|
10
11
|
export {};
|
|
@@ -38,9 +38,19 @@ export function usePeekStrip(ref) {
|
|
|
38
38
|
return () => { stop(); box.style.removeProperty('--sheet-peek'); };
|
|
39
39
|
}, []);
|
|
40
40
|
}
|
|
41
|
-
/*
|
|
42
|
-
words overflow the line
|
|
43
|
-
|
|
41
|
+
/* a TITLE is one line; too long for it, it runs sideways like a song title (Lh 2026-09-24 for the StackSheet, 2026-10-03 for the Sheet):
|
|
42
|
+
how far its `words` overflow the `line` they stand on, published on the line for the CSS — `--_over` (px) and `--_run` (30px a
|
|
43
|
+
second plus the pauses). Measured on the words themselves (a range), so the running indent or translate never feeds back into
|
|
44
|
+
the number. Returns whether it is long; the heading renders `.is-long` from that. */
|
|
45
|
+
export function runOver(line, words = line) {
|
|
46
|
+
const range = document.createRange();
|
|
47
|
+
range.selectNodeContents(words);
|
|
48
|
+
const over = Math.max(0, Math.ceil(range.getBoundingClientRect().width - line.clientWidth));
|
|
49
|
+
line.style.setProperty('--_over', `${over}px`);
|
|
50
|
+
line.style.setProperty('--_run', `${(over / 30 + 3).toFixed(1)}s`);
|
|
51
|
+
return over > 0;
|
|
52
|
+
}
|
|
53
|
+
/* the Sheet's title: the app's own heading, measured whole (sheet.css runs it with text-indent) */
|
|
44
54
|
export function useTitleRun(ref, onLong) {
|
|
45
55
|
useEffect(() => {
|
|
46
56
|
const el = ref.current, title = el?.querySelector(':scope > .card__head > :is(h1, h2, h3):first-child, :scope > .sheet__head .sheet__title');
|
|
@@ -48,12 +58,7 @@ export function useTitleRun(ref, onLong) {
|
|
|
48
58
|
return;
|
|
49
59
|
let was = false;
|
|
50
60
|
const measure = () => {
|
|
51
|
-
const
|
|
52
|
-
words.selectNodeContents(title);
|
|
53
|
-
const over = Math.max(0, Math.ceil(words.getBoundingClientRect().width - title.clientWidth));
|
|
54
|
-
title.style.setProperty('--_over', `${over}px`);
|
|
55
|
-
title.style.setProperty('--_run', `${(over / 30 + 3).toFixed(1)}s`);
|
|
56
|
-
const long = over > 0;
|
|
61
|
+
const long = runOver(title);
|
|
57
62
|
if (long !== was) {
|
|
58
63
|
was = long;
|
|
59
64
|
onLong(long);
|
package/dist/sheets/stackCard.js
CHANGED
|
@@ -46,6 +46,31 @@ const pullsHead = (target, head) => {
|
|
|
46
46
|
return false;
|
|
47
47
|
return target === head || !!target.closest('.sheet-stack__back') || !!target.closest('.card__head > :is(h1, h2, h3)');
|
|
48
48
|
};
|
|
49
|
+
/* once a finger is down the DOCUMENT hears it, not the head or the body it started on: the finger leaves them while it drags.
|
|
50
|
+
One set of move / up / cancel at a time; `stop` takes the set off, and is safe to call with none on */
|
|
51
|
+
const docPointer = () => {
|
|
52
|
+
let stop = () => { };
|
|
53
|
+
return {
|
|
54
|
+
listen: (move, up, cancel) => {
|
|
55
|
+
stop();
|
|
56
|
+
document.addEventListener('pointermove', move, { passive: false });
|
|
57
|
+
document.addEventListener('pointerup', up);
|
|
58
|
+
document.addEventListener('pointercancel', cancel);
|
|
59
|
+
stop = () => {
|
|
60
|
+
document.removeEventListener('pointermove', move);
|
|
61
|
+
document.removeEventListener('pointerup', up);
|
|
62
|
+
document.removeEventListener('pointercancel', cancel);
|
|
63
|
+
stop = () => { };
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
stop: () => stop(),
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/* a pull that moved ends in a click the browser still fires: the card swallows it for one frame */
|
|
70
|
+
const armSwallow = (swallow) => {
|
|
71
|
+
swallow.current = true;
|
|
72
|
+
requestAnimationFrame(() => { swallow.current = false; });
|
|
73
|
+
};
|
|
49
74
|
/**
|
|
50
75
|
* The card's own pull, refresh and focus. Listeners sit on this card's head and body.
|
|
51
76
|
* `--_pull` / `--_lift` are written while a finger is down, the same way a single Sheet writes `--_drag`.
|
|
@@ -86,24 +111,8 @@ export function useStackCard(ref, place, swallow) {
|
|
|
86
111
|
const stack = card.closest('.sheet-stack');
|
|
87
112
|
const head = card.querySelector(':scope > .card__head');
|
|
88
113
|
const scroller = card.querySelector(':scope > .sheet__body') ?? card;
|
|
89
|
-
|
|
90
|
-
const listen = (move, up, cancel) => {
|
|
91
|
-
stopDoc();
|
|
92
|
-
document.addEventListener('pointermove', move, { passive: false });
|
|
93
|
-
document.addEventListener('pointerup', up);
|
|
94
|
-
document.addEventListener('pointercancel', cancel);
|
|
95
|
-
stopDoc = () => {
|
|
96
|
-
document.removeEventListener('pointermove', move);
|
|
97
|
-
document.removeEventListener('pointerup', up);
|
|
98
|
-
document.removeEventListener('pointercancel', cancel);
|
|
99
|
-
stopDoc = () => { };
|
|
100
|
-
};
|
|
101
|
-
};
|
|
114
|
+
const doc = docPointer();
|
|
102
115
|
const primary = (e) => e.isPrimary && e.button === 0;
|
|
103
|
-
const armSwallow = () => {
|
|
104
|
-
swallow.current = true;
|
|
105
|
-
requestAnimationFrame(() => { swallow.current = false; });
|
|
106
|
-
};
|
|
107
116
|
const release = (prop) => {
|
|
108
117
|
live.current = null;
|
|
109
118
|
card.classList.remove('is-dragging');
|
|
@@ -144,7 +153,7 @@ export function useStackCard(ref, place, swallow) {
|
|
|
144
153
|
const dy = Math.max(pull.last, along('down', pull.startY, ev.clientY));
|
|
145
154
|
const moved = pull.moved || dy >= pull.slop;
|
|
146
155
|
headPull.current = null;
|
|
147
|
-
|
|
156
|
+
doc.stop();
|
|
148
157
|
if (moved && dy >= pull.commit) {
|
|
149
158
|
card.classList.remove('is-dragging');
|
|
150
159
|
readFrame(card);
|
|
@@ -156,16 +165,16 @@ export function useStackCard(ref, place, swallow) {
|
|
|
156
165
|
else
|
|
157
166
|
release('--_pull');
|
|
158
167
|
if (moved)
|
|
159
|
-
armSwallow();
|
|
168
|
+
armSwallow(swallow);
|
|
160
169
|
};
|
|
161
170
|
const onCancel = (ev) => {
|
|
162
171
|
if (ev.pointerId !== pull.pointerId || headPull.current !== pull)
|
|
163
172
|
return;
|
|
164
173
|
headPull.current = null;
|
|
165
|
-
|
|
174
|
+
doc.stop();
|
|
166
175
|
release('--_pull');
|
|
167
176
|
};
|
|
168
|
-
listen(onMove, onUp, onCancel);
|
|
177
|
+
doc.listen(onMove, onUp, onCancel);
|
|
169
178
|
};
|
|
170
179
|
const onBodyDown = (e) => {
|
|
171
180
|
const ask = placeRef.current;
|
|
@@ -220,22 +229,22 @@ export function useStackCard(ref, place, swallow) {
|
|
|
220
229
|
const dy = Math.max(pull.lastDy, Math.max(0, ev.clientY - pull.startY));
|
|
221
230
|
const go = pull.moved && !pull.drop && scroller.scrollTop <= 0 && dy >= pull.commit;
|
|
222
231
|
bodyPull.current = null;
|
|
223
|
-
|
|
232
|
+
doc.stop();
|
|
224
233
|
readFrame(scroller);
|
|
225
234
|
finish();
|
|
226
235
|
if (go)
|
|
227
236
|
placeRef.current?.onRefresh?.();
|
|
228
237
|
if (pull.moved)
|
|
229
|
-
armSwallow();
|
|
238
|
+
armSwallow(swallow);
|
|
230
239
|
};
|
|
231
240
|
const onCancel = (ev) => {
|
|
232
241
|
if (ev.pointerId !== pull.pointerId || bodyPull.current !== pull)
|
|
233
242
|
return;
|
|
234
243
|
bodyPull.current = null;
|
|
235
|
-
|
|
244
|
+
doc.stop();
|
|
236
245
|
finish();
|
|
237
246
|
};
|
|
238
|
-
listen(onMove, onUp, onCancel);
|
|
247
|
+
doc.listen(onMove, onUp, onCancel);
|
|
239
248
|
};
|
|
240
249
|
/* a finger: the browser takes a touch for its own scroll at the first move unless that move is refused here */
|
|
241
250
|
let touch = null;
|
|
@@ -264,7 +273,7 @@ export function useStackCard(ref, place, swallow) {
|
|
|
264
273
|
return () => {
|
|
265
274
|
scroller.removeEventListener('touchstart', onTouchStart);
|
|
266
275
|
scroller.removeEventListener('touchmove', onTouchMove);
|
|
267
|
-
|
|
276
|
+
doc.stop();
|
|
268
277
|
headPull.current = null;
|
|
269
278
|
bodyPull.current = null;
|
|
270
279
|
scroller.style.removeProperty('--_refresh');
|
|
@@ -285,23 +294,7 @@ export function useStackCard(ref, place, swallow) {
|
|
|
285
294
|
const head = card.querySelector(':scope > .card__head');
|
|
286
295
|
if (!head || !stack)
|
|
287
296
|
return;
|
|
288
|
-
|
|
289
|
-
const listen = (move, up, cancel) => {
|
|
290
|
-
stopDoc();
|
|
291
|
-
document.addEventListener('pointermove', move, { passive: false });
|
|
292
|
-
document.addEventListener('pointerup', up);
|
|
293
|
-
document.addEventListener('pointercancel', cancel);
|
|
294
|
-
stopDoc = () => {
|
|
295
|
-
document.removeEventListener('pointermove', move);
|
|
296
|
-
document.removeEventListener('pointerup', up);
|
|
297
|
-
document.removeEventListener('pointercancel', cancel);
|
|
298
|
-
stopDoc = () => { };
|
|
299
|
-
};
|
|
300
|
-
};
|
|
301
|
-
const armSwallow = () => {
|
|
302
|
-
swallow.current = true;
|
|
303
|
-
requestAnimationFrame(() => { swallow.current = false; });
|
|
304
|
-
};
|
|
297
|
+
const doc = docPointer();
|
|
305
298
|
const release = () => {
|
|
306
299
|
live.current = null;
|
|
307
300
|
card.classList.remove('is-dragging');
|
|
@@ -365,7 +358,7 @@ export function useStackCard(ref, place, swallow) {
|
|
|
365
358
|
const upPx = Math.max(pull.last, along('up', pull.startY, ev.clientY));
|
|
366
359
|
const moved = pull.moved || upPx >= pull.slop;
|
|
367
360
|
headPull.current = null;
|
|
368
|
-
|
|
361
|
+
doc.stop();
|
|
369
362
|
if (moved && upPx >= pull.commit) {
|
|
370
363
|
card.classList.remove('is-dragging');
|
|
371
364
|
readFrame(card);
|
|
@@ -377,20 +370,20 @@ export function useStackCard(ref, place, swallow) {
|
|
|
377
370
|
else
|
|
378
371
|
release();
|
|
379
372
|
if (moved)
|
|
380
|
-
armSwallow();
|
|
373
|
+
armSwallow(swallow);
|
|
381
374
|
};
|
|
382
375
|
const onCancel = (ev) => {
|
|
383
376
|
if (ev.pointerId !== pull.pointerId || headPull.current !== pull)
|
|
384
377
|
return;
|
|
385
378
|
headPull.current = null;
|
|
386
|
-
|
|
379
|
+
doc.stop();
|
|
387
380
|
release();
|
|
388
381
|
};
|
|
389
|
-
listen(onMove, onUp, onCancel);
|
|
382
|
+
doc.listen(onMove, onUp, onCancel);
|
|
390
383
|
};
|
|
391
384
|
head.addEventListener('pointerdown', onDown);
|
|
392
385
|
return () => {
|
|
393
|
-
|
|
386
|
+
doc.stop();
|
|
394
387
|
headPull.current = null;
|
|
395
388
|
if (live.current?.prop === '--_lift')
|
|
396
389
|
release();
|
package/dist/stamp/Stamp.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
+
import { pressable } from '../pressable.js';
|
|
3
4
|
/**
|
|
4
5
|
* Stamp — a perforated paper stamp, 4:5: the pictures fill the inner frame on a tone, the words sit on the picture's foot.
|
|
5
6
|
* Every stamp is ONE size wherever it stands (`--stamp-size`, two taps wide: 96 × 120) — what holds it only places it; a holder narrower than that shows it whole, smaller.
|
|
@@ -8,9 +9,7 @@ import { cx } from '../cx.js';
|
|
|
8
9
|
*/
|
|
9
10
|
export function Stamp({ pictures, tone = 'custard', name, words, bare, tilt, onClick, className, ...rest }) {
|
|
10
11
|
const shown = pictures.slice(0, 4);
|
|
11
|
-
const tap = onClick
|
|
12
|
-
const Box = (tap ? 'button' : 'span');
|
|
13
|
-
const boxProps = (tap ? { type: 'button', onClick, ...rest } : rest);
|
|
12
|
+
const { tap, Box, boxProps } = pressable(onClick, 'span', rest);
|
|
14
13
|
return (_jsx(Box, { className: cx('stamp', `stamp--${shown.length}`, bare && 'stamp--bare', tilt && `stamp--tilt-${tilt}`, tap && 'stamp--tap', className), ...boxProps, children: _jsx("span", { className: "stamp__paper", children: _jsxs("span", { className: cx('stamp__pic', `sticky--${tone}`), children: [shown.map((picture, i) => _jsx("span", { className: "stamp__art", "aria-hidden": "true", children: picture }, i)), (name != null || words != null) && (_jsxs("span", { className: "stamp__cap", children: [words != null && _jsx("span", { className: "stamp__words", children: words }), name != null && _jsx("span", { className: "stamp__name", children: name })] }))] }) }) }));
|
|
15
14
|
}
|
|
16
15
|
/**
|
|
@@ -19,8 +18,6 @@ export function Stamp({ pictures, tone = 'custard', name, words, bare, tilt, onC
|
|
|
19
18
|
* and a `--stamp-place-dash` above 0 stretches each dot into a dash.
|
|
20
19
|
*/
|
|
21
20
|
export function StampPlace({ faint, onClick, className, children, ...rest }) {
|
|
22
|
-
const
|
|
23
|
-
const Box = (tap ? 'button' : 'span');
|
|
24
|
-
const boxProps = (tap ? { type: 'button', onClick, ...rest } : rest);
|
|
21
|
+
const { Box, boxProps } = pressable(onClick, 'span', rest);
|
|
25
22
|
return (_jsxs(Box, { className: cx('stamp-place', faint && 'stamp-place--faint', className), ...boxProps, children: [_jsx("svg", { className: "stamp-place__edge", "aria-hidden": "true", focusable: "false", children: _jsx("rect", {}) }), children] }));
|
|
26
23
|
}
|
package/dist/stamp/StampBook.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Children } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
|
+
import { pressable } from '../pressable.js';
|
|
4
5
|
import { Icon } from '../type/Icon.js';
|
|
5
6
|
import { StampPlace } from './Stamp.js';
|
|
6
7
|
/** StampBook — the album page: StampCells three across on an album ground, the free places drawn empty up to `places`. The book places its stamps and never sizes them: each is the one size of a stamp, in the middle of its third. */
|
|
@@ -10,8 +11,6 @@ export function StampBook({ places = 0, strip, bare, className, children, ...res
|
|
|
10
11
|
}
|
|
11
12
|
/** StampCell — one place of a StampBook: a Stamp (the child) and a short label under it. */
|
|
12
13
|
export function StampCell({ label, onClick, on, className, children, ...rest }) {
|
|
13
|
-
const
|
|
14
|
-
const Box = (tap ? 'button' : 'span');
|
|
15
|
-
const boxProps = (tap ? { type: 'button', onClick, 'aria-pressed': on, ...rest } : rest);
|
|
14
|
+
const { Box, boxProps } = pressable(onClick, 'span', rest, { 'aria-pressed': on });
|
|
16
15
|
return (_jsxs(Box, { className: cx('stamp-cell', className), ...boxProps, children: [children, label != null && _jsx("span", { className: "stamp-cell__label", children: label }), on && _jsx("span", { className: "stamp-cell__on", "aria-hidden": "true", children: _jsx(Icon, { name: "check" }) })] }));
|
|
17
16
|
}
|
package/dist/stamp/StampOn.d.ts
CHANGED
|
@@ -15,6 +15,14 @@ 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;
|
|
18
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
|
|
19
27
|
* picked it or pressed to stick it — the app shows it when its own move (send) begins. With `onArrived` */
|
|
20
28
|
arrive?: boolean;
|
|
@@ -24,9 +32,19 @@ export interface StampOnProps extends ComponentProps<'div'> {
|
|
|
24
32
|
/**
|
|
25
33
|
* StampOn — a Stamp (the child) stuck on a surface at the place the member chose. The surface is the nearest positioned box:
|
|
26
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.
|
|
27
|
-
* 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.
|
|
28
37
|
* With `onAtChange` the member drags it (finger or mouse; the arrow keys move it too): while the finger holds, the stamp writes its
|
|
29
38
|
* own place, and on letting go it ASKS — the app owns `at`. It never leaves the surface.
|
|
30
|
-
* With `
|
|
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.
|
|
31
49
|
*/
|
|
32
|
-
export declare function StampOn({ at, defaultAt, turn, onAtChange, label, arrive, onArrived, className, style, onAnimationEnd, 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;
|
package/dist/stamp/StampOn.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, 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
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,23 +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
|
}
|
|
18
|
-
const STRIKE = '
|
|
21
|
+
const STRIKE = 'strike'; // css/tokens.css — the strike every mark struck onto paper reads
|
|
19
22
|
const write = (el, at) => { el.style.setProperty('--stamp-on-x', String(at.x)); el.style.setProperty('--stamp-on-y', String(at.y)); };
|
|
20
23
|
/**
|
|
21
24
|
* StampOn — a Stamp (the child) stuck on a surface at the place the member chose. The surface is the nearest positioned box:
|
|
22
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.
|
|
23
|
-
* 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.
|
|
24
28
|
* With `onAtChange` the member drags it (finger or mouse; the arrow keys move it too): while the finger holds, the stamp writes its
|
|
25
29
|
* own place, and on letting go it ASKS — the app owns `at`. It never leaves the surface.
|
|
26
|
-
* With `
|
|
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.
|
|
27
40
|
*/
|
|
28
|
-
export function StampOn({ at, defaultAt = MIDDLE, turn = 0, onAtChange, label, arrive, onArrived, className, style, onAnimationEnd, 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);
|
|
29
43
|
const [shown, request] = useControlled(at, defaultAt, onAtChange);
|
|
30
44
|
const [held, setHeld] = useState(false);
|
|
45
|
+
const [cross, setCross] = useState(false);
|
|
31
46
|
const grip = useRef(null);
|
|
32
|
-
const
|
|
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);
|
|
33
51
|
const ask = (next) => { if (next.x !== shown.x || next.y !== shown.y)
|
|
34
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, []);
|
|
35
57
|
const down = (e) => {
|
|
36
58
|
const el = e.currentTarget, box = el.offsetParent;
|
|
37
59
|
if (!box || e.button !== 0)
|
|
@@ -40,6 +62,8 @@ export function StampOn({ at, defaultAt = MIDDLE, turn = 0, onAtChange, label, a
|
|
|
40
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 };
|
|
41
63
|
el.setPointerCapture(e.pointerId);
|
|
42
64
|
setHeld(true);
|
|
65
|
+
stop();
|
|
66
|
+
setCross(false); // a new drag takes the cross away until it ends
|
|
43
67
|
};
|
|
44
68
|
const move = (e) => {
|
|
45
69
|
const el = e.currentTarget, box = el.offsetParent, g = grip.current;
|
|
@@ -56,26 +80,59 @@ export function StampOn({ at, defaultAt = MIDDLE, turn = 0, onAtChange, label, a
|
|
|
56
80
|
grip.current = null;
|
|
57
81
|
setHeld(false);
|
|
58
82
|
write(e.currentTarget, shown); // back to what the app has: if it takes the new place, the next render writes it
|
|
59
|
-
if (e.type === 'pointerup')
|
|
83
|
+
if (e.type === 'pointerup') {
|
|
60
84
|
ask(g.next);
|
|
85
|
+
show();
|
|
86
|
+
}
|
|
61
87
|
};
|
|
62
88
|
const key = (e) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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();
|
|
68
117
|
};
|
|
69
118
|
const landed = useRef(onArrived);
|
|
70
119
|
landed.current = onArrived;
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
73
127
|
const struck = (e) => {
|
|
74
128
|
onAnimationEnd?.(e);
|
|
75
129
|
if (arrive && e.target === e.currentTarget && e.animationName === STRIKE)
|
|
76
|
-
|
|
130
|
+
land();
|
|
77
131
|
};
|
|
78
132
|
const place = { '--stamp-on-x': shown.x, '--stamp-on-y': shown.y, '--stamp-on-turn': turn, ...style };
|
|
79
|
-
const
|
|
80
|
-
|
|
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" }) }) }))] }));
|
|
81
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);
|
package/dist/state.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SyntheticEvent } from 'react';
|
|
1
|
+
import { type Ref, type SyntheticEvent } from 'react';
|
|
2
2
|
/** controlled like an input: `value` given = the app owns it; undefined = the component keeps its own, starting at `initial`.
|
|
3
3
|
* Returns the value shown and `request(next, …extra)`: nothing when `next` is what shows; else it moves the component's own copy
|
|
4
4
|
* (uncontrolled only) and ASKS the app — `onChange(next, …extra)`. `request` keeps one identity for the component's life. */
|
|
@@ -12,6 +12,10 @@ export declare function useDisclosure(open: boolean | undefined, defaultOpen?: b
|
|
|
12
12
|
open: boolean;
|
|
13
13
|
onToggle: (e: SyntheticEvent<HTMLDetailsElement>) => void;
|
|
14
14
|
};
|
|
15
|
+
/** the node to the component's own ref AND the caller's `ref` (a function or an object: React 19 passes it as a prop). Give the result to the element's `ref` */
|
|
16
|
+
export declare const setRefs: <T extends Element>(own: {
|
|
17
|
+
current: Element | null;
|
|
18
|
+
}, ref: Ref<T> | undefined) => (node: T | null) => void;
|
|
15
19
|
/** the latest value in a ref — for a handler that a timer, an observer or a listener set up once calls later */
|
|
16
20
|
export declare function useLatest<T>(value: T): import("react").RefObject<T>;
|
|
17
21
|
/** watch boxes for a size change (the keyboard, a font arriving, a wrap): `fn` on each — once at the start too, as the browser
|
package/dist/state.js
CHANGED
|
@@ -30,6 +30,14 @@ export function useDisclosure(open, defaultOpen = false, onOpenChange) {
|
|
|
30
30
|
},
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
/** the node to the component's own ref AND the caller's `ref` (a function or an object: React 19 passes it as a prop). Give the result to the element's `ref` */
|
|
34
|
+
export const setRefs = (own, ref) => (node) => {
|
|
35
|
+
own.current = node;
|
|
36
|
+
if (typeof ref === 'function')
|
|
37
|
+
ref(node);
|
|
38
|
+
else if (ref)
|
|
39
|
+
ref.current = node;
|
|
40
|
+
};
|
|
33
41
|
/** the latest value in a ref — for a handler that a timer, an observer or a listener set up once calls later */
|
|
34
42
|
export function useLatest(value) {
|
|
35
43
|
const ref = useRef(value);
|