@usableapp/cardds 0.7.99 → 0.7.101
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 +23 -0
- package/css/card.css +43 -0
- package/dist/cardds.css +43 -0
- package/dist/cards/Card.d.ts +7 -1
- package/dist/cards/Card.js +8 -3
- package/dist/cards/CardParts.d.ts +9 -3
- package/dist/cards/CardParts.js +41 -3
- package/dist/cards/cardGo.js +1 -1
- package/dist/catalog.json +1 -1
- package/dist/post/PostDesk.d.ts +2 -1
- package/dist/post/PostDesk.js +11 -13
- package/dist/room.d.ts +12 -0
- package/dist/room.js +38 -0
- package/dist/sticky/Sticky.js +5 -38
- package/lint/rules.mjs +20 -6
- package/package.json +1 -1
package/dist/post/PostDesk.d.ts
CHANGED
|
@@ -53,7 +53,8 @@ export interface PostDeskProps extends ComponentProps<'div'> {
|
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
55
|
* PostDesk — a postcard being WRITTEN, stamped and sent. The paper is landscape and twice the screen; the phone looks at one half:
|
|
56
|
-
* the message on ruled lines (typing stops when the half is full — the paper is the limit
|
|
56
|
+
* the message on ruled lines (typing stops when the half is full — the paper is the limit: an addition that would not fit is refused
|
|
57
|
+
* where it was typed, the caret stays, the app's `onMessageChange` never hears it and `onFull` is called; the same rule as a `StickyField` on a `Sticky`), then the address with the stamp box.
|
|
57
58
|
* The other half's showing edge, or a sideways drag, slides the paper. The stamp box and the stamps under the card choose one of
|
|
58
59
|
* the sender's own stamps. Sending is the SCREEN's button, never the desk's: the app sets `state="struck"` — the postmark lands,
|
|
59
60
|
* the desk asks for `sent`, and the card leaves upward; `sent` stands in its place. Every state is the app's (`x` + `onXChange`, or `defaultX`).
|
package/dist/post/PostDesk.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useLayoutEffect, useRef } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
import { reducedMotion, useControlled, useLatest, watchSize } from '../state.js';
|
|
5
|
+
import { holdRoom } from '../room.js';
|
|
5
6
|
import { word } from '../words.js';
|
|
6
7
|
import { StampPlace } from '../stamp/Stamp.js';
|
|
7
8
|
const STRIKE = 700; // ms the struck card is seen before it asks to leave
|
|
@@ -20,7 +21,8 @@ function linesOf(ta) {
|
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
23
|
* PostDesk — a postcard being WRITTEN, stamped and sent. The paper is landscape and twice the screen; the phone looks at one half:
|
|
23
|
-
* the message on ruled lines (typing stops when the half is full — the paper is the limit
|
|
24
|
+
* the message on ruled lines (typing stops when the half is full — the paper is the limit: an addition that would not fit is refused
|
|
25
|
+
* where it was typed, the caret stays, the app's `onMessageChange` never hears it and `onFull` is called; the same rule as a `StickyField` on a `Sticky`), then the address with the stamp box.
|
|
24
26
|
* The other half's showing edge, or a sideways drag, slides the paper. The stamp box and the stamps under the card choose one of
|
|
25
27
|
* the sender's own stamps. Sending is the SCREEN's button, never the desk's: the app sets `state="struck"` — the postmark lands,
|
|
26
28
|
* the desk asks for `sent`, and the card leaves upward; `sent` stands in its place. Every state is the app's (`x` + `onXChange`, or `defaultX`).
|
|
@@ -46,10 +48,15 @@ export function PostDesk({ half, defaultHalf = 'message', onHalfChange, message,
|
|
|
46
48
|
told.current = `${left}/${fits}`;
|
|
47
49
|
lines.current?.(left, fits);
|
|
48
50
|
};
|
|
49
|
-
|
|
51
|
+
const full = useLatest(onFull);
|
|
52
|
+
const room = useRef(null);
|
|
53
|
+
useLayoutEffect(() => { measure(); room.current?.sync(); }, [text]);
|
|
50
54
|
useEffect(() => {
|
|
55
|
+
const ta = paper.current;
|
|
56
|
+
room.current = holdRoom(ta.parentElement, () => paper.current, () => ta.scrollHeight > ta.clientHeight + 1, (refused) => refused && full.current?.());
|
|
51
57
|
document.fonts?.ready.then(measure);
|
|
52
|
-
|
|
58
|
+
const stop = watchSize([ta], measure);
|
|
59
|
+
return () => { room.current?.stop(); room.current = null; stop(); };
|
|
53
60
|
}, []);
|
|
54
61
|
useEffect(() => {
|
|
55
62
|
if (at !== 'struck')
|
|
@@ -57,15 +64,6 @@ export function PostDesk({ half, defaultHalf = 'message', onHalfChange, message,
|
|
|
57
64
|
const t = setTimeout(() => askState('sent'), reducedMotion() ? 0 : STRIKE);
|
|
58
65
|
return () => clearTimeout(t);
|
|
59
66
|
}, [at, askState]);
|
|
60
|
-
const write = (e) => {
|
|
61
|
-
const ta = e.currentTarget;
|
|
62
|
-
if (ta.scrollHeight > ta.clientHeight + 1 && ta.value.length >= text.length) {
|
|
63
|
-
ta.value = text;
|
|
64
|
-
onFull?.();
|
|
65
|
-
return;
|
|
66
|
-
} // taking words away is never refused
|
|
67
|
-
askText(ta.value);
|
|
68
|
-
};
|
|
69
67
|
const from = useRef(null);
|
|
70
68
|
const drop = (e) => {
|
|
71
69
|
const dx = from.current == null ? 0 : e.clientX - from.current;
|
|
@@ -73,7 +71,7 @@ export function PostDesk({ half, defaultHalf = 'message', onHalfChange, message,
|
|
|
73
71
|
if (writing && Math.abs(dx) >= SWIPE)
|
|
74
72
|
askSide(dx < 0 ? 'address' : 'message');
|
|
75
73
|
};
|
|
76
|
-
return (_jsxs("div", { className: cx('post-desk', `post-desk--${side}`, `post-desk--${at}`, className), ...rest, children: [_jsxs("div", { className: "post-desk__window", onPointerDown: (e) => { from.current = e.clientX; }, onPointerUp: drop, children: [_jsxs("div", { className: "post-desk__card post-paper", children: [_jsx("section", { className: "post-desk__half post-desk__half--message", "aria-label": word('message'), inert: !writing || side !== 'message', children: _jsx("textarea", { ref: paper, className: "post-desk__write", "aria-label": word('message'), placeholder: placeholder, value: text, onChange:
|
|
74
|
+
return (_jsxs("div", { className: cx('post-desk', `post-desk--${side}`, `post-desk--${at}`, className), ...rest, children: [_jsxs("div", { className: "post-desk__window", onPointerDown: (e) => { from.current = e.clientX; }, onPointerUp: drop, children: [_jsxs("div", { className: "post-desk__card post-paper", children: [_jsx("section", { className: "post-desk__half post-desk__half--message", "aria-label": word('message'), inert: !writing || side !== 'message', children: _jsx("textarea", { ref: paper, className: "post-desk__write", "aria-label": word('message'), placeholder: placeholder, value: text, onChange: (e) => askText(e.currentTarget.value) }) }), _jsxs("section", { className: "post-desk__half post-desk__half--address", "aria-label": word('address'), inert: !writing || side !== 'address', children: [_jsx("div", { className: "post-desk__corner", children: _jsxs("div", { className: "post-corner", children: [!writing && postmark, _jsx("button", { className: "post-corner__stamp post-desk__slot", type: "button", "aria-pressed": chosen != null, "aria-label": word(chosen ? 'stampOff' : 'stampOn'), onClick: () => askStamp(chosen ? null : stamps[0]?.id ?? null), children: chosen ? chosen.stamp : _jsx(StampPlace, { "aria-hidden": "true", children: word('stampOn') }) })] }) }), _jsx("div", { className: "post-desk__lines", children: address })] })] }), writing && _jsx("button", { className: `post-desk__reach post-desk__reach--${side === 'message' ? 'address' : 'message'}`, type: "button", "aria-label": word(side === 'message' ? 'toAddress' : 'toMessage'), onClick: () => askSide(side === 'message' ? 'address' : 'message') }), at === 'sent' && _jsx("div", { className: "post-desk__gone", children: sent })] }), _jsx("p", { className: "post-desk__note", "aria-live": "polite", children: note }), writing && side === 'address' && stamps.length > 0 && (_jsx("div", { className: "post-desk__stamps", role: "group", "aria-label": word('stamps'), children: stamps.map((s) => (_jsx("button", { className: "post-desk__pick", type: "button", "aria-pressed": s.id === stuck, "aria-label": s.label, onClick: () => askStamp(s.id), children: s.stamp }, s.id))) }))] }));
|
|
77
75
|
}
|
|
78
76
|
/** PostDeskLine — one address line of a PostDesk (`address`): a small word and a bare input on a rule. The value is the app's, like any input's. */
|
|
79
77
|
export function PostDeskLine({ label, type = 'text', autoComplete = 'off', className, ...rest }) {
|
package/dist/room.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** A field that stops at its paper's room. An input that adds to the field's words while `over()` says they no longer fit is refused:
|
|
2
|
+
* the words go back to what they were, the caret stays where it was, and the app's `onChange` never hears it. Taking words away is
|
|
3
|
+
* never refused. `after(refused)` runs after every input on the paper, refused or not, so the holder can measure again.
|
|
4
|
+
* The listeners sit on `paper`, an element round the field: a native listener there runs before React's own at the root, so stopping
|
|
5
|
+
* the event keeps `onChange` from hearing it. A composition (Thai, Japanese) has reached the app already when it ends, so its words
|
|
6
|
+
* are put back through the prototype's setter and an input is sent, which tells the app they went back.
|
|
7
|
+
* Call `sync()` after the app sets the field's value itself (a cleared field after send); `stop()` takes the listeners off. */
|
|
8
|
+
export interface Room {
|
|
9
|
+
sync(): void;
|
|
10
|
+
stop(): void;
|
|
11
|
+
}
|
|
12
|
+
export declare function holdRoom(paper: HTMLElement, fieldOf: () => HTMLTextAreaElement | null, over: () => boolean, after?: (refused: boolean) => void): Room;
|
package/dist/room.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function holdRoom(paper, fieldOf, over, after = () => { }) {
|
|
2
|
+
let last = fieldOf()?.value ?? '', composing = false;
|
|
3
|
+
const refuse = (field, dropped) => {
|
|
4
|
+
const added = field.value.length - last.length, at = field.selectionStart - added;
|
|
5
|
+
if (dropped)
|
|
6
|
+
Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set.call(field, last);
|
|
7
|
+
else
|
|
8
|
+
field.value = last; // the prototype's setter, so React's value tracker sees the change and the input dispatched next reaches onChange
|
|
9
|
+
field.setSelectionRange(Math.max(0, at), Math.max(0, at));
|
|
10
|
+
if (dropped)
|
|
11
|
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
12
|
+
};
|
|
13
|
+
const check = (e, dropped) => {
|
|
14
|
+
const field = fieldOf();
|
|
15
|
+
if (!field || e.target !== field)
|
|
16
|
+
return after(false);
|
|
17
|
+
const refused = field.value.length >= last.length && over();
|
|
18
|
+
if (refused) {
|
|
19
|
+
if (!dropped)
|
|
20
|
+
e.stopPropagation();
|
|
21
|
+
refuse(field, dropped);
|
|
22
|
+
}
|
|
23
|
+
else
|
|
24
|
+
last = field.value;
|
|
25
|
+
after(refused);
|
|
26
|
+
};
|
|
27
|
+
const onInput = (e) => (e.isComposing ? after(false) : check(e, false));
|
|
28
|
+
const onComposing = () => { composing = true; };
|
|
29
|
+
const onComposed = (e) => { composing = false; check(e, true); };
|
|
30
|
+
paper.addEventListener('input', onInput);
|
|
31
|
+
paper.addEventListener('compositionstart', onComposing);
|
|
32
|
+
paper.addEventListener('compositionend', onComposed);
|
|
33
|
+
return {
|
|
34
|
+
sync() { const field = fieldOf(); if (field && !composing)
|
|
35
|
+
last = field.value; },
|
|
36
|
+
stop() { paper.removeEventListener('input', onInput); paper.removeEventListener('compositionstart', onComposing); paper.removeEventListener('compositionend', onComposed); },
|
|
37
|
+
};
|
|
38
|
+
}
|
package/dist/sticky/Sticky.js
CHANGED
|
@@ -4,6 +4,7 @@ import { cx } from '../cx.js';
|
|
|
4
4
|
import { pressable } from '../pressable.js';
|
|
5
5
|
import { setRefs, watchSize } from '../state.js';
|
|
6
6
|
import { word } from '../words.js';
|
|
7
|
+
import { holdRoom } from '../room.js';
|
|
7
8
|
import { fitSticky } from './fit.js';
|
|
8
9
|
/**
|
|
9
10
|
* Sticky — a paper note: a flat pastel SQUARE of one size (168, the knob `--sticky-size`), square corners, a soft shadow under it.
|
|
@@ -16,59 +17,25 @@ import { fitSticky } from './fit.js';
|
|
|
16
17
|
export function Sticky({ tone = 'custard', flat, unreadLabel, head, date, tilt, foot, corner, stuck, onClick, className, children, ref, ...rest }) {
|
|
17
18
|
const own = useRef(null);
|
|
18
19
|
const [small, setSmall] = useState(false);
|
|
19
|
-
const
|
|
20
|
-
const composing = useRef(false);
|
|
20
|
+
const room = useRef(null);
|
|
21
21
|
const fieldOf = () => own.current?.querySelector('.sticky-field') ?? null;
|
|
22
22
|
useLayoutEffect(() => {
|
|
23
23
|
const note = own.current;
|
|
24
24
|
if (!note)
|
|
25
25
|
return;
|
|
26
26
|
setSmall(fitSticky(note) !== 'full');
|
|
27
|
-
|
|
28
|
-
if (field && !composing.current)
|
|
29
|
-
last.current = field.value;
|
|
27
|
+
room.current?.sync();
|
|
30
28
|
}, [children, head, date, foot]); // a chat re-renders every note on each keystroke in its answer field; only a note whose words changed is measured again (a field is an element, new every render)
|
|
31
29
|
useEffect(() => {
|
|
32
30
|
const note = own.current, paper = note?.querySelector('.sticky__paper');
|
|
33
31
|
if (!note || !paper)
|
|
34
32
|
return;
|
|
35
33
|
const measure = () => setSmall(fitSticky(note) !== 'full');
|
|
36
|
-
|
|
37
|
-
hearing an input the note refused. `dropped` is for a composition: the app has heard it already, so it is told the words went back. */
|
|
38
|
-
const refuse = (field, dropped) => {
|
|
39
|
-
const added = field.value.length - last.current.length, at = field.selectionStart - added;
|
|
40
|
-
if (dropped)
|
|
41
|
-
Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set.call(field, last.current);
|
|
42
|
-
else
|
|
43
|
-
field.value = last.current; // the prototype's setter, so React's value tracker sees the change and the input dispatched next reaches onChange
|
|
44
|
-
field.setSelectionRange(Math.max(0, at), Math.max(0, at));
|
|
45
|
-
if (dropped)
|
|
46
|
-
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
47
|
-
};
|
|
48
|
-
const check = (e, dropped) => {
|
|
49
|
-
const field = fieldOf();
|
|
50
|
-
if (!field || e.target !== field)
|
|
51
|
-
return measure();
|
|
52
|
-
if (fitSticky(note) === 'over' && field.value.length > last.current.length) {
|
|
53
|
-
if (!dropped)
|
|
54
|
-
e.stopPropagation();
|
|
55
|
-
refuse(field, dropped);
|
|
56
|
-
fitSticky(note);
|
|
57
|
-
}
|
|
58
|
-
else
|
|
59
|
-
last.current = field.value;
|
|
60
|
-
measure();
|
|
61
|
-
};
|
|
62
|
-
const onInput = (e) => (e.isComposing ? measure() : check(e, false));
|
|
63
|
-
const onComposing = () => { composing.current = true; };
|
|
64
|
-
const onComposed = (e) => { composing.current = false; check(e, true); };
|
|
65
|
-
paper.addEventListener('input', onInput);
|
|
66
|
-
paper.addEventListener('compositionstart', onComposing);
|
|
67
|
-
paper.addEventListener('compositionend', onComposed);
|
|
34
|
+
room.current = holdRoom(paper, fieldOf, () => fitSticky(note) === 'over', measure);
|
|
68
35
|
const body = paper.querySelector('.sticky__body');
|
|
69
36
|
const stop = watchSize([body], measure);
|
|
70
37
|
document.fonts?.ready.then(measure);
|
|
71
|
-
return () => {
|
|
38
|
+
return () => { room.current?.stop(); room.current = null; stop(); };
|
|
72
39
|
}, []);
|
|
73
40
|
const notched = corner != null;
|
|
74
41
|
const { tap, Box, boxProps } = pressable(notched ? undefined : onClick, 'div', rest);
|
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'], StampOn: ['removeLabel'], PostLetter: ['backLabel', 'pictureLabel', 'playLabel', 'pauseLabel', 'toLabel'],
|
|
15
|
+
Calendar: ['prevLabel', 'nextLabel'], SheetStep: ['skipLabel'], Sticky: ['unreadLabel'], StampOn: ['removeLabel'], Card: ['removeLabel'], PostLetter: ['backLabel', 'pictureLabel', 'playLabel', 'pauseLabel', 'toLabel'],
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
const lineOf = (text, idx) => text.slice(0, idx).split('\n').length;
|
|
@@ -119,7 +119,8 @@ function* styleEntries(body, at, sep) {
|
|
|
119
119
|
export function checkSource(source, file, { classes = carddsClasses() } = {}) {
|
|
120
120
|
const findings = [];
|
|
121
121
|
const lines = source.split('\n');
|
|
122
|
-
|
|
122
|
+
// comments out, lines kept; a string is stepped over whole, so `accept="image/*"` opens no comment
|
|
123
|
+
let text = source.replace(/"[^"\n]*"|'[^'\n]*'|`[^`]*`|\/\*[\s\S]*?\*\/|(?<![:'"`\\])\/\/[^\n]*/g, (c) => /^["'`]/.test(c) ? c : c.replace(/[^\n]/g, ' '));
|
|
123
124
|
const add = (rule, level, idx, message) => {
|
|
124
125
|
const line = lineOf(text, idx);
|
|
125
126
|
if (!/cardds-ignore-next-line/.test(lines[line - 2] ?? '')) findings.push({ rule, level, file, line, message }); // `// cardds-ignore-next-line` — a deliberate case (a story that shows the wrong way)
|
|
@@ -229,25 +230,38 @@ export function checkSource(source, file, { classes = carddsClasses() } = {}) {
|
|
|
229
230
|
|
|
230
231
|
// 10 · the sheet is the paper the member fills in (Lh 2026-10-18): every control the member fills or picks stands on a Sheet's body, a SheetStep
|
|
231
232
|
// or a stack sheet; a card is for reading and pressing buttons. A Card that is a sheet of a SheetStack IS a sheet (the stack's material)
|
|
232
|
-
|
|
233
|
+
// a `<` right after a name is a type argument (`Record<Step, string>`, `useState<Step>`), not a tag; an element that never closes holds nothing
|
|
234
|
+
const span = (re) => [...text.matchAll(re)].filter((m) => !/[\w$)\].]/.test(text[m.index - 1] ?? '')).map((m) => {
|
|
235
|
+
const open = tagAt(text, m.index), el = elementAt(text, m.index), closed = open.endsWith('/>') || el.endsWith(`</${m[0].slice(1).match(/^[\w.]+/)[0]}>`);
|
|
236
|
+
return { at: m.index, body: m.index + open.length, end: m.index + (closed ? el : open).length };
|
|
237
|
+
});
|
|
233
238
|
const stacks = span(/<SheetStack\b(?![\w.])/g), cards = span(/<Card\b(?![\w.])/g);
|
|
234
239
|
const within = (at, el) => at > el.at && at < el.end;
|
|
235
240
|
const innermost = (at) => cards.filter((c) => within(at, c)).at(-1);
|
|
236
241
|
// a form is never on the ground (Lh 2026-10-18, timebank's ask steps on a bare Page): the control's nearest container written in this file decides.
|
|
237
242
|
// None in the file (a step component's fragment) is not judged here — the file that places it is
|
|
238
243
|
const places = [
|
|
239
|
-
...span(/<(?:Sheet|SheetBody|SheetStep|StackSheet|SheetStack)\b(?![\w.])/g).map((p) => ({ ...p, ground: false })),
|
|
244
|
+
...span(/<(?:Sheet|SheetBody|SheetStep|StackSheet|SheetStack|CallSheet|Modal|ModalAsk)\b(?![\w.])/g).map((p) => ({ ...p, ground: false })), // a modal is a surface over the screen too
|
|
240
245
|
...span(/<(?:ActionBarTier|FilterRow|TopBar|AppBar)\b(?![\w.])/g).map((p) => ({ ...p, ground: false })), // a bar's control: a Slider in a control tier, a Dropdown in a filter row or a top bar's actions, a Toggle in an app bar
|
|
241
246
|
...span(/<(?:Page|Pages|Step|Screen)\b(?![\w.])/g).map((p) => ({ ...p, ground: true })),
|
|
242
247
|
...cards.map((p) => ({ ...p, ground: false })), // a card round it is field-in-card's finding
|
|
243
248
|
].sort((a, b) => a.at - b.at);
|
|
244
|
-
|
|
249
|
+
// true on a sheet, false on the ground, null when this file cannot tell. A component of this file is where it is placed: the ask steps' <Page>,
|
|
250
|
+
// in a function rendered inside a <SheetBody> further up the file, slides inside that sheet
|
|
251
|
+
const componentAt = (at) => [...text.slice(0, at).matchAll(/(?:function\s+|const\s+)([A-Z]\w*)/g)].at(-1)?.[1];
|
|
252
|
+
const onSheet = (at, depth = 0) => {
|
|
253
|
+
const place = places.filter((p) => within(at, p)).at(-1);
|
|
254
|
+
if (place && !place.ground) return true;
|
|
255
|
+
const name = depth < 5 && componentAt(at);
|
|
256
|
+
if (name) for (const u of text.matchAll(new RegExp(`(?<![\\w$)\\].])<${name}\\b(?![\\w.])`, 'g'))) if (onSheet(u.index, depth + 1)) return true;
|
|
257
|
+
return place ? false : null;
|
|
258
|
+
};
|
|
245
259
|
for (const c of text.matchAll(/<(Field|Composer|ComposerArea|Pin|Dropdown|Slider|Toggle|Check|ChipPick|Calendar|DayStrip|Day|Picker|Mood|AvatarPick|FileBtn|AddRow|MapPick|Tile)\b(?![\w.])/g)) {
|
|
246
260
|
const kind = c[1] === 'Tile' ? tagAt(text, c.index).match(/\s(pick|upload)\b(?!\s*=\s*\{\s*false)/)?.[1] : null;
|
|
247
261
|
if (c[1] === 'Tile' && !kind) continue; // a plain or stat Tile is read, not filled
|
|
248
262
|
const card = innermost(c.index);
|
|
249
263
|
if (!card) {
|
|
250
|
-
if (
|
|
264
|
+
if (onSheet(c.index) === false) add('field-off-sheet', 'error', c.index, `<${kind ? `Tile ${kind}` : c[1]}> on the ground: a form is never on the ground, it sits only on a sheet (Lh 2026-10-18). A Page, Pages, Step or Screen is the base, not a sheet. Put the control on a Sheet's <SheetBody>, in a <SheetStep>, or on a stack sheet — a form over a screen is a <Sheet> in a <SheetStage over dim> (Home/AskSheet).`);
|
|
251
265
|
continue;
|
|
252
266
|
}
|
|
253
267
|
const stack = stacks.filter((s) => card.at >= s.body && card.at < s.end).at(-1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.101",
|
|
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.",
|