@usableapp/cardds 0.7.86 → 0.7.88
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 +34 -0
- package/README.md +26 -1
- package/bin/cardds.mjs +8 -3
- package/css/actions.css +1 -2
- package/css/card.css +10 -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 +2 -5
- package/css/stack.css +1 -1
- package/css/stamp.css +3 -4
- package/css/step.css +14 -3
- package/css/sticky.css +1 -1
- package/css/tokens.css +6 -0
- package/dist/cardds.css +139 -35
- package/dist/cards/Card.d.ts +10 -2
- package/dist/cards/Card.js +18 -4
- package/dist/cards/CardFeed.js +2 -9
- package/dist/catalog.json +1 -1
- package/dist/lists/Row.d.ts +2 -0
- package/dist/lists/Row.js +2 -9
- package/dist/pressable.d.ts +8 -0
- package/dist/pressable.js +6 -0
- package/dist/scaffold/EmptyState.d.ts +2 -1
- package/dist/scaffold/EmptyState.js +2 -1
- 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.js +1 -1
- package/dist/state.d.ts +5 -1
- package/dist/state.js +8 -0
- package/dist/sticky/Sticky.js +2 -3
- package/lib/catalog.mjs +24 -7
- package/lib/mcp.mjs +3 -3
- package/lint/rules.mjs +7 -0
- package/package.json +1 -1
package/dist/lists/Row.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface RowProps extends Omit<ComponentProps<'div'>, 'title'> {
|
|
|
23
23
|
done?: boolean;
|
|
24
24
|
/** div (default), a link, a label wrapping a Check/Toggle, li */
|
|
25
25
|
as?: 'div' | 'a' | 'label' | 'li';
|
|
26
|
+
/** with `as="label"`: the `id` of the control the row presses (its `Check`). Needed when the `end` holds a button BEFORE the check (a picked row's own move, "open the map"): a label presses the first control inside it, so without this a touch on the row's words would press that button */
|
|
27
|
+
htmlFor?: string;
|
|
26
28
|
/** makes the row a link (an `a`); not with `more`, whose row is a summary */
|
|
27
29
|
href?: string;
|
|
28
30
|
/** what the row EXPANDS to show under itself (who liked, the rest of a list): the row becomes a `<details>`' summary */
|
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
|
+
}
|
|
@@ -10,5 +10,6 @@ export interface EmptyStateProps extends Omit<ComponentProps<'div'>, 'title'> {
|
|
|
10
10
|
level?: 'h2' | 'h3';
|
|
11
11
|
}
|
|
12
12
|
/** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA. A component in the content: alone in a `Step` it stands at the content's human centre, 40% down (Lh 2026-09-23 — no longer a `Centre` over the screen)
|
|
13
|
-
* or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in; full ink throughout, never muted (Lh 2026-09-16).
|
|
13
|
+
* or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in; full ink throughout, never muted (Lh 2026-09-16).
|
|
14
|
+
* Knob `--empty-state-at`, a number 0–1: the share of the holder's free room that stands above it. In a `Step` the default is 0.4 (two parts above, three below); alone in a `SheetBody` it is 0 (at the top). A theme's `:root { --empty-state-at: 0.2 }` lifts it everywhere, or set it on a `Screen`, `Step` or `Sheet` for the ones inside. */
|
|
14
15
|
export declare function EmptyState({ icon, title, action, level, className, children, ...rest }: EmptyStateProps): import("react").JSX.Element;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
3
|
/** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA. A component in the content: alone in a `Step` it stands at the content's human centre, 40% down (Lh 2026-09-23 — no longer a `Centre` over the screen)
|
|
4
|
-
* or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in; full ink throughout, never muted (Lh 2026-09-16).
|
|
4
|
+
* or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in; full ink throughout, never muted (Lh 2026-09-16).
|
|
5
|
+
* Knob `--empty-state-at`, a number 0–1: the share of the holder's free room that stands above it. In a `Step` the default is 0.4 (two parts above, three below); alone in a `SheetBody` it is 0 (at the top). A theme's `:root { --empty-state-at: 0.2 }` lifts it everywhere, or set it on a `Screen`, `Step` or `Sheet` for the ones inside. */
|
|
5
6
|
export function EmptyState({ icon, title, action, level = 'h2', className, children, ...rest }) {
|
|
6
7
|
const H = level;
|
|
7
8
|
return (_jsxs("div", { className: cx('empty-state', className), ...rest, children: [icon, title != null && _jsx(H, { className: "t-h2", children: title }), typeof children === 'string' ? _jsx("p", { className: "t-body", children: children }) : children, action != null && _jsx("div", { className: "empty-state__action", children: action })] }));
|
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.js
CHANGED
|
@@ -18,7 +18,7 @@ function within(el, x, y) {
|
|
|
18
18
|
const mx = el.offsetWidth / 2 / box.clientWidth, my = el.offsetHeight / 2 / box.clientHeight;
|
|
19
19
|
return { x: round(clamp(x, mx, 1 - mx)), y: round(clamp(y, my, 1 - my)) };
|
|
20
20
|
}
|
|
21
|
-
const STRIKE = '
|
|
21
|
+
const STRIKE = 'strike'; // css/tokens.css — the strike every mark struck onto paper reads
|
|
22
22
|
const write = (el, at) => { el.style.setProperty('--stamp-on-x', String(at.x)); el.style.setProperty('--stamp-on-y', String(at.y)); };
|
|
23
23
|
/**
|
|
24
24
|
* StampOn — a Stamp (the child) stuck on a surface at the place the member chose. The surface is the nearest positioned box:
|
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);
|
package/dist/sticky/Sticky.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
+
import { pressable } from '../pressable.js';
|
|
3
4
|
import { word } from '../words.js';
|
|
4
5
|
/**
|
|
5
6
|
* Sticky — a paper note: a flat pastel SQUARE of one size (168, the knob `--sticky-size`), square corners, a soft shadow under it.
|
|
@@ -9,9 +10,7 @@ import { word } from '../words.js';
|
|
|
9
10
|
*/
|
|
10
11
|
export function Sticky({ tone = 'custard', flat, unreadLabel, head, date, tilt, foot, corner, stuck, onClick, className, children, ...rest }) {
|
|
11
12
|
const notched = corner != null;
|
|
12
|
-
const tap =
|
|
13
|
-
const Box = (tap ? 'button' : 'div');
|
|
13
|
+
const { tap, Box, boxProps } = pressable(notched ? undefined : onClick, 'div', rest);
|
|
14
14
|
const Part = tap ? 'span' : 'div';
|
|
15
|
-
const boxProps = (tap ? { type: 'button', onClick, ...rest } : rest);
|
|
16
15
|
return (_jsxs(Box, { className: cx('sticky', `sticky--${tone}`, flat && 'sticky--flat', notched && 'sticky--corner', tilt && `sticky--tilt-${tilt}`, tap && 'sticky--tap', className), ...boxProps, children: [_jsxs(Part, { className: "sticky__paper", children: [(head != null || date != null) && _jsxs(Part, { className: "sticky__head", children: [head, date] }), _jsx(Part, { className: "sticky__body", children: children }), foot != null && _jsx(Part, { className: "sticky__foot", children: foot }), !flat && !notched && _jsx("span", { className: "sticky__fold", "aria-hidden": "true" }), !flat && !notched && _jsx("span", { className: "sticky__state t-sr", children: unreadLabel ?? word('unread') })] }), stuck != null && _jsx(Part, { className: "sticky__stuck", children: stuck }), notched && _jsx("div", { className: "sticky__corner", children: corner })] }));
|
|
17
16
|
}
|
package/lib/catalog.mjs
CHANGED
|
@@ -29,8 +29,10 @@ const representsOf = ({ represents }) => represents.filter((v) => v !== '—');
|
|
|
29
29
|
|
|
30
30
|
/** the names only — the first call of nearly every session, so it stays small. `group` is the src folder; `represents`, `shape` and `interact` say what it is for */
|
|
31
31
|
export const list = (cat) => cat.components.map(({ name, group, role }) => ({ name, group, represents: representsOf(role), shape: shapeOf(role), interact: interactOf(role) }));
|
|
32
|
+
/** the finishes of the paper (`data-finish`, css/finish.css): a style apart from colour, not a component — each with what it is and its knobs */
|
|
33
|
+
export const finishes = (cat) => cat.finishes ?? [];
|
|
32
34
|
|
|
33
|
-
const sentence = (text) => {
|
|
35
|
+
export const sentence = (text) => {
|
|
34
36
|
const flat = text.replace(/\s+/g, ' ').trim();
|
|
35
37
|
const one = flat.match(/^.*?[.!?](?=\s|$)/)?.[0] ?? flat;
|
|
36
38
|
return one.length > 160 ? `${one.slice(0, 157)}…` : one;
|
|
@@ -56,7 +58,7 @@ const closest = (names, wanted) => {
|
|
|
56
58
|
return [...names].sort((a, b) => rank(a) - rank(b)).slice(0, 5);
|
|
57
59
|
};
|
|
58
60
|
|
|
59
|
-
/** one component: its props (a ReactNode prop is a slot) and its variations — the stories of its own file first, then a few that use it */
|
|
61
|
+
/** one component: its props (a ReactNode prop is a slot), what it composes and what composes it, its knobs, and its variations — the stories of its own file first, then a few that use it */
|
|
60
62
|
export function get(cat, name, { html = false, all = false } = {}) {
|
|
61
63
|
const c = cat.components.find((x) => x.name.toLowerCase() === String(name).toLowerCase());
|
|
62
64
|
if (!c) return { error: `no component "${name}"`, closest: closest(cat.components.map((x) => x.name), String(name)) };
|
|
@@ -72,6 +74,9 @@ export function get(cat, name, { html = false, all = false } = {}) {
|
|
|
72
74
|
element: c.element,
|
|
73
75
|
props: c.props.map((p) => ({ ...p, ...(isSlot(p.type) && { slot: true }) })),
|
|
74
76
|
...(c.params && { params: c.params }),
|
|
77
|
+
composes: c.composes,
|
|
78
|
+
composedBy: c.composedBy,
|
|
79
|
+
knobs: c.knobs,
|
|
75
80
|
variations: [...own, ...elsewhere.filter((s) => shown.includes(s))].map(show),
|
|
76
81
|
...(elsewhere.length > shown.length && { alsoUsedIn: elsewhere.filter((s) => !shown.includes(s)).map((s) => s.id) }),
|
|
77
82
|
};
|
|
@@ -82,6 +87,7 @@ const STOP = new Set(['the', 'and', 'for', 'with', 'from', 'that', 'this', 'into
|
|
|
82
87
|
const tokens = (query) => [...new Set(words(String(query)).filter((t) => t.length > 1 && !STOP.has(t)))];
|
|
83
88
|
const stem = (t) => (t.length > 3 && t.endsWith('s') ? t.slice(0, -1) : t);
|
|
84
89
|
const hits = (text, token) => words(text).some((w) => w.startsWith(token) || w.startsWith(stem(token)));
|
|
90
|
+
const isName = (name, token) => name.toLowerCase() === token || name.toLowerCase() === stem(token);
|
|
85
91
|
|
|
86
92
|
const TOP = 8; // answers per kind; the components that match every axis are never cut
|
|
87
93
|
|
|
@@ -119,7 +125,7 @@ const matchesUnion = (roles, want) => roles.length > 0
|
|
|
119
125
|
/** short ranked answer to "what is there for this?". `axes` ({ represents?, shape?, interact? } — what it represents, how many, what the member can do with it) are
|
|
120
126
|
* asked first: whatever matches every one of them (a component, or a screen where one component does, then a screen whose parts do between them) ranks above whatever matches only words;
|
|
121
127
|
* with axes and no words they are the whole answer. Among screens that tie on words, the one whose description says the axes comes first, then the one where a single component matches them all.
|
|
122
|
-
* Then the words: screens already drawn, then components, then what they are used with.
|
|
128
|
+
* Then the words: screens already drawn, then components, then what they are used with — a screen that answers fewer of the words than the best component comes after the components.
|
|
123
129
|
* A query that matches nothing (another language, say) returns `fallback: true`, a hint and how many screens are drawn — not their ids:
|
|
124
130
|
* they did not help to choose; ask again in English or by the axes, or `scaffold` with no id lists them. */
|
|
125
131
|
export function find(cat, query, axes = {}) {
|
|
@@ -129,11 +135,11 @@ export function find(cat, query, axes = {}) {
|
|
|
129
135
|
const roles = new Map(cat.components.map((c) => [c.name, roleOf(c)]));
|
|
130
136
|
const toks = tokens(query);
|
|
131
137
|
const axisWords = [...(asked.want.represents ?? []), ...(asked.want.interact || [])]; // the axis values as words: a screen's own description may say them
|
|
132
|
-
const rank = (items, fields, level, said = () => 0) => {
|
|
138
|
+
const rank = (items, fields, level, said = () => 0) => { // a field is [text, weight, match = hits]
|
|
133
139
|
const scored = items.map((item) => {
|
|
134
140
|
let matched = 0, score = 0;
|
|
135
141
|
for (const t of toks) {
|
|
136
|
-
const w = fields(item).filter(([text]) =>
|
|
142
|
+
const w = fields(item).filter(([text, , match = hits]) => match(text, t)).reduce((sum, [, weight]) => sum + weight, 0);
|
|
137
143
|
if (w) { matched++; score += w; }
|
|
138
144
|
}
|
|
139
145
|
return { item, matched, score, level: active ? level(item) : 0, said: active ? said(item) : 0 };
|
|
@@ -152,9 +158,20 @@ export function find(cat, query, axes = {}) {
|
|
|
152
158
|
};
|
|
153
159
|
const said = (s) => axisWords.filter((v) => hits(s.description, v)).length;
|
|
154
160
|
const [screenAxis, screenRest] = tier(rank(screens, (s) => [[s.id, 5], [s.description, 3], [s.uses.join(' '), 1]], level, said), TOP);
|
|
155
|
-
|
|
161
|
+
// a component is found by its name (the name that IS the word counts as much again as one that contains it: "card" is Card before FeedCard, whatever
|
|
162
|
+
// their other words say), its description, its role's note (what it is FOR, in the words of a request: "a setting turned on or off") and what it stands in for, then its prop names
|
|
163
|
+
const components = rank(cat.components, (c) => [[c.name, 6, isName], [c.name, 6], [c.description, 2], [c.role?.note ?? '', 2], [(c.role?.standsInFor ?? []).map((s) => `${s.name} ${s.condition}`).join(' '), 1], [c.props.map((p) => p.name).join(' '), 1]], (c) => Number(matches(roles.get(c.name), asked.want)));
|
|
156
164
|
const [componentAxis, componentRest] = tier(components, Math.max(TOP, components.axis.length));
|
|
157
|
-
|
|
165
|
+
// a screen drawn comes before the components when it answers the words as well as the best component does; one that answers FEWER of them
|
|
166
|
+
// comes after (a question by intention, "a setting turned on or off", is answered by Toggle, not by eight screens that say "off" or "or")
|
|
167
|
+
const best = componentRest[0]?.matched ?? 0;
|
|
168
|
+
// a finish asked for by its name ("printed", "sticker") or by the word finish is the thing itself, so it comes before the screens that only show it;
|
|
169
|
+
// one that answers a word of its description ("card") is an aside, after the components
|
|
170
|
+
const finishOf = ({ item: f }) => ({ kind: 'finish', name: f.name, summary: sentence(f.is), knobs: f.knobs.map((k) => k.name) });
|
|
171
|
+
const named = (text, t) => t.startsWith(text) || text.startsWith(t);
|
|
172
|
+
const finishRest = rank(finishes(cat), (f) => [[f.name, 6, named], ['finish', 6], [f.is, 2], [f.knobs.map((k) => k.name).join(' '), 1]], () => 0).rest;
|
|
173
|
+
const ranked = [...screenAxis.map(screenOf), ...componentAxis.map(componentOf), ...finishRest.filter((r) => r.score >= 6).map(finishOf), ...screenRest.filter((r) => r.matched >= best).map(screenOf),
|
|
174
|
+
...componentRest.map(componentOf), ...finishRest.filter((r) => r.score < 6).map(finishOf), ...screenRest.filter((r) => r.matched < best).map(screenOf)];
|
|
158
175
|
if (!ranked.length && active && !toks.length) return { query, results: [], hint: 'no component or screen has that role: drop an axis, or add English words' };
|
|
159
176
|
if (!ranked.length) return { query, fallback: true, hint: 'the names and descriptions are English: ask again in English words, or by the axes (represents · shape · interact); `scaffold` with no id lists the screens', screens: screens.length };
|
|
160
177
|
|
package/lib/mcp.mjs
CHANGED
|
@@ -36,19 +36,19 @@ const bool = (description) => ({ type: 'boolean', description });
|
|
|
36
36
|
const TOOLS = [
|
|
37
37
|
{
|
|
38
38
|
name: 'list_components',
|
|
39
|
-
description: 'The names of every cardds component and what each is FOR, nothing else. represents is what it stands for (person, group, time…) or, for a control that stands for no one thing, the KIND of data it carries (words, number, picture, signal); the word in parentheses after a name in the text is its represents. Every component shows information; shape says how much: "one" (one thing), "group-brief" (a group read at a glance or in a line), "group-detail" (a group read in detail), "—" (a helper function or structure, not something drawn). interact says whether the member can do something with it: false (it only shows or holds), or the kinds, any of pick, act, write, navigate — alone when shape is one (a Btn acts), as a group when shape is a group (a BtnGroup picks among several). Two kinds mean it does both. group is the src folder, not what it is for. Call this once, first, in a session that will build a UI with cardds, to learn what exists; then get_component for the ones you use.',
|
|
39
|
+
description: 'The names of every cardds component and what each is FOR, nothing else. represents is what it stands for (person, group, time…) or, for a control that stands for no one thing, the KIND of data it carries (words, number, picture, signal); the word in parentheses after a name in the text is its represents. Every component shows information; shape says how much: "one" (one thing), "group-brief" (a group read at a glance or in a line), "group-detail" (a group read in detail), "—" (a helper function or structure, not something drawn). interact says whether the member can do something with it: false (it only shows or holds), or the kinds, any of pick, act, write, navigate — alone when shape is one (a Btn acts), as a group when shape is a group (a BtnGroup picks among several). Two kinds mean it does both. group is the src folder, not what it is for. Call this once, first, in a session that will build a UI with cardds, to learn what exists; then get_component for the ones you use. The finishes of the paper (data-finish on <html>: lit, print, sticker, ink — a style apart from colour, not a component) are answered by find_components ("printed paper", "finish").',
|
|
40
40
|
inputSchema: { type: 'object', properties: {} },
|
|
41
41
|
run: (cat) => catalog.list(cat),
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
name: 'get_component',
|
|
45
|
-
description: 'One component in full: what it is, its props (a prop marked slot takes children: ReactNode), and its variations, each a working story with its code. Call it before you use a component you have not used yet, and copy a variation rather than guessing a prop. A component used everywhere (Btn, Text) comes with a few of its stories and the ids of the rest (pass all for their code). Names are case-insensitive; a miss returns the closest names.',
|
|
45
|
+
description: 'One component in full: what it is, its props (a prop marked slot takes children: ReactNode), what it composes and what composes it (composes · composedBy: the catalog names its source renders or calls, and the inverse), its knobs (the CSS custom properties a theme sets to retune it, each with its default, what it sets and the selector that reads it — joined by the classes the component emits, so a knob named for an old name is listed under the component that has it now), and its variations, each a working story with its code. Call it before you use a component you have not used yet, and copy a variation rather than guessing a prop. A component used everywhere (Btn, Text) comes with a few of its stories and the ids of the rest (pass all for their code). Names are case-insensitive; a miss returns the closest names.',
|
|
46
46
|
inputSchema: { type: 'object', properties: { name: str('component name, e.g. Menu'), html: bool('also return each variation\'s rendered HTML — only to see the markup it makes'), all: bool('include the code of every story that uses the component') }, required: ['name'] },
|
|
47
47
|
run: (cat, a) => catalog.get(cat, a.name, { html: a.html, all: a.all }),
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
name: 'find_components',
|
|
51
|
-
description: (cat) => `Find what to build with when you do not know the component name. Ask BOTH ways and read both answers: by the AXES (translated from the request) AND by the English noun of the task ("stamp", "otp", "postcard") — the screen already drawn is often found by the noun. The axes are a closed vocabulary, so ask by them in any language: represents (what it stands for, or for a control the KIND of data it carries${cat ? `: ${catalog.vocabulary(cat).represents.join(' · ')}` : ' — the word in parentheses after a name in list_components'}), shape (one · group-brief · group-detail · group = either group shape), interact (the kinds the member can do with it: ${catalog.KINDS.join(' · ')}, or "none" = it only shows). "Show all the members" is represents [person] + shape group; adding interact [pick] narrows it to the ones the member chooses from. Whatever matches every axis you give comes first, each with its role so you see why; with axes and no words they are the whole answer. Words ("stamp", "otp pin", "bottom sheet") rank what matches only by words, in ENGLISH — the names and descriptions are English, translate first. Answers short: the screens cardds has already drawn (a screen is a composition: it counts when its parts answer the axes between them, one component per axis, and one that answers them all ranks higher), then components, then the components most often used together with them. Call it at the start of a task, before composing a screen from parts: if a screen like it exists, scaffold_screen gives its code. A query that matches nothing returns fallback: true, a hint and the number of screens drawn, not their ids: ask again in English words or by the axes, or call scaffold_screen with no id to list them.`,
|
|
51
|
+
description: (cat) => `Find what to build with when you do not know the component name. Ask BOTH ways and read both answers: by the AXES (translated from the request) AND by the English noun of the task ("stamp", "otp", "postcard") — the screen already drawn is often found by the noun. The axes are a closed vocabulary, so ask by them in any language: represents (what it stands for, or for a control the KIND of data it carries${cat ? `: ${catalog.vocabulary(cat).represents.join(' · ')}` : ' — the word in parentheses after a name in list_components'}), shape (one · group-brief · group-detail · group = either group shape), interact (the kinds the member can do with it: ${catalog.KINDS.join(' · ')}, or "none" = it only shows). "Show all the members" is represents [person] + shape group; adding interact [pick] narrows it to the ones the member chooses from. Whatever matches every axis you give comes first, each with its role so you see why; with axes and no words they are the whole answer. Words ("stamp", "otp pin", "bottom sheet") rank what matches only by words, in ENGLISH — the names and descriptions are English, translate first; a component's words include its role's note and what it stands in for, so a question by intention ("a setting turned on or off", "a code digit by digit") finds the piece. Answers short: the screens cardds has already drawn (a screen is a composition: it counts when its parts answer the axes between them, one component per axis, and one that answers them all ranks higher), then components, then the components most often used together with them; a screen that answers fewer of the words than the best component comes after the components, so a question by intention ("a setting turned on or off") names the piece first. A FINISH of the paper (data-finish on <html>, beside any palette: lit, print, sticker, ink) is answered too, kind "finish", with its knobs: ask "printed paper", "sticker", or "finish" for all four. Call it at the start of a task, before composing a screen from parts: if a screen like it exists, scaffold_screen gives its code. A query that matches nothing returns fallback: true, a hint and the number of screens drawn, not their ids: ask again in English words or by the axes, or call scaffold_screen with no id to list them.`,
|
|
52
52
|
inputSchema: {
|
|
53
53
|
type: 'object',
|
|
54
54
|
properties: {
|
package/lint/rules.mjs
CHANGED
|
@@ -209,5 +209,12 @@ export function checkSource(source, file, { classes = carddsClasses() } = {}) {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
// 8 · a label presses the FIRST control inside it: a button before the row's Check takes the press meant for the row
|
|
213
|
+
for (const m of text.matchAll(/<Row\b/g)) {
|
|
214
|
+
const tag = tagAt(text, m.index);
|
|
215
|
+
if (/\sas\s*=\s*\{?\s*["']label["']/.test(tag) && /<(?:IconBtn|Btn|Link)\b/.test(tag) && !/\shtmlFor\s*=/.test(tag))
|
|
216
|
+
add('row-label-button', 'warn', m.index, '<Row as="label"> holding a button and no htmlFor: a label presses the first control inside it, so a touch on the row\'s words presses that button, not its Check. Give the Check an id and the row htmlFor={id}.');
|
|
217
|
+
}
|
|
218
|
+
|
|
212
219
|
return findings.sort((a, b) => a.line - b.line);
|
|
213
220
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.88",
|
|
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.",
|