@usableapp/cardds 0.7.82 → 0.7.84
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 +47 -0
- package/README.md +72 -12
- package/bin/cardds.mjs +21 -10
- package/css/actions.css +2 -0
- package/css/card.css +4 -4
- package/css/layover.css +5 -0
- package/css/people.css +3 -2
- package/css/post.css +16 -4
- package/css/stamp.css +23 -21
- package/css/sticky.css +138 -28
- package/dist/actions/Chip.d.ts +4 -2
- package/dist/actions/Chip.js +3 -3
- package/dist/cardds.css +191 -59
- package/dist/catalog.json +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/post/PostBox.d.ts +1 -1
- package/dist/post/PostDesk.d.ts +2 -2
- package/dist/post/PostLetter.d.ts +1 -1
- package/dist/post/Postmark.d.ts +1 -1
- package/dist/post/Postmark.js +1 -1
- package/dist/post/postCorner.js +1 -1
- package/dist/scaffold/LayerHistory.d.ts +2 -0
- package/dist/scaffold/LayerHistory.js +2 -0
- package/dist/sheets/Modal.d.ts +4 -1
- package/dist/sheets/Modal.js +4 -1
- package/dist/stamp/Stamp.d.ts +4 -3
- package/dist/stamp/Stamp.js +3 -2
- package/dist/stamp/StampBook.d.ts +3 -3
- package/dist/stamp/StampBook.js +1 -1
- package/dist/stamp/StampOn.d.ts +1 -0
- package/dist/stamp/StampOn.js +1 -0
- package/dist/sticky/Sticky.d.ts +8 -6
- package/dist/sticky/Sticky.js +6 -3
- package/dist/sticky/StickyAnswer.d.ts +23 -0
- package/dist/sticky/StickyAnswer.js +35 -0
- package/dist/sticky/StickyBoard.d.ts +12 -0
- package/dist/sticky/StickyBoard.js +13 -0
- package/dist/sticky/StickyThread.d.ts +12 -6
- package/dist/sticky/StickyThread.js +8 -4
- package/dist/words.d.ts +1 -1
- package/dist/words.js +2 -2
- package/lib/catalog.mjs +1 -0
- package/lib/mcp.mjs +1 -1
- package/lib/role.mjs +11 -3
- package/lint/rules.mjs +7 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export * from './sticky/StickyDate.js';
|
|
|
80
80
|
export * from './sticky/StickyField.js';
|
|
81
81
|
export * from './sticky/StickyPick.js';
|
|
82
82
|
export * from './sticky/StickyThread.js';
|
|
83
|
+
export * from './sticky/StickyBoard.js';
|
|
84
|
+
export * from './sticky/StickyAnswer.js';
|
|
83
85
|
export * from './stamp/Stamp.js';
|
|
84
86
|
export * from './stamp/StampBook.js';
|
|
85
87
|
export * from './stamp/StampOn.js';
|
package/dist/index.js
CHANGED
|
@@ -84,6 +84,8 @@ export * from './sticky/StickyDate.js';
|
|
|
84
84
|
export * from './sticky/StickyField.js';
|
|
85
85
|
export * from './sticky/StickyPick.js';
|
|
86
86
|
export * from './sticky/StickyThread.js';
|
|
87
|
+
export * from './sticky/StickyBoard.js';
|
|
88
|
+
export * from './sticky/StickyAnswer.js';
|
|
87
89
|
export * from './stamp/Stamp.js';
|
|
88
90
|
export * from './stamp/StampBook.js';
|
|
89
91
|
export * from './stamp/StampOn.js';
|
package/dist/post/PostBox.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface PostBoxCardProps extends Omit<ComponentProps<'li'>, 'id'> {
|
|
|
20
20
|
from: ReactNode;
|
|
21
21
|
/** where and when — the edge's small line ("เชียงใหม่ · 3 ม.ค. 69") */
|
|
22
22
|
when?: ReactNode;
|
|
23
|
-
/** the corner's stamp — a `Stamp` */
|
|
23
|
+
/** the corner's stamp — a `Stamp`, shown at the postcard's own size (72 × 90, `--post-corner-stamp-w`) */
|
|
24
24
|
stamp?: ReactNode;
|
|
25
25
|
/** the mark struck over the stamp — a `Postmark`; behind another card only its place shows */
|
|
26
26
|
postmark?: ReactNode;
|
package/dist/post/PostDesk.d.ts
CHANGED
|
@@ -30,9 +30,9 @@ export interface PostDeskProps extends ComponentProps<'div'> {
|
|
|
30
30
|
placeholder?: string;
|
|
31
31
|
/** the address lines — `PostDeskLine`s; their values are the app's, like any input's */
|
|
32
32
|
address?: ReactNode;
|
|
33
|
-
/** the sender's own stamps, shown under the card on the address half */
|
|
33
|
+
/** the sender's own stamps, shown under the card on the address half: each the standard size (96 × 120, `--stamp-size`), in one line that scrolls sideways when they outrun the screen */
|
|
34
34
|
stamps?: PostDeskStamp[];
|
|
35
|
-
/** CONTROLLED: the `id` of the stamp in the stamp box, `null` = none — with `onStampChange` */
|
|
35
|
+
/** CONTROLLED: the `id` of the stamp in the stamp box, `null` = none — with `onStampChange`. The box and its stamp are the postcard's own size (72 × 90, `--post-corner-stamp-w`); the empty box shows its word */
|
|
36
36
|
stamp?: string | null;
|
|
37
37
|
/** UNCONTROLLED: the stamp it starts with */
|
|
38
38
|
defaultStamp?: string | null;
|
|
@@ -24,7 +24,7 @@ export interface PostLetterProps extends ComponentProps<'div'> {
|
|
|
24
24
|
kicker?: ReactNode;
|
|
25
25
|
/** the address lines, top down: the first is the name, the rest where they live */
|
|
26
26
|
to?: ReactNode[];
|
|
27
|
-
/** the corner's stamp — a `Stamp` */
|
|
27
|
+
/** the corner's stamp — a `Stamp`, shown at the postcard's own size (72 × 90, `--post-corner-stamp-w`) */
|
|
28
28
|
stamp?: ReactNode;
|
|
29
29
|
/** the mark struck over the stamp — a `Postmark` */
|
|
30
30
|
postmark?: ReactNode;
|
package/dist/post/Postmark.d.ts
CHANGED
|
@@ -7,6 +7,6 @@ export interface PostmarkProps extends ComponentProps<'div'> {
|
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Postmark — the round mark the post strikes: where and when, two rings, turned a little, its cancel lines running on to the side.
|
|
10
|
-
* On a card's corner (`postmark` of PostLetter · PostDesk · PostBoxCard) it lies over the edge of the stamp. Information: nothing to press.
|
|
10
|
+
* On a card's corner (`postmark` of PostLetter · PostDesk · PostBoxCard) it lies over the edge of the stamp, by 0.4 of its own size. Information: nothing to press.
|
|
11
11
|
*/
|
|
12
12
|
export declare function Postmark({ place, date, className, ...rest }: PostmarkProps): import("react").JSX.Element;
|
package/dist/post/Postmark.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
3
|
/**
|
|
4
4
|
* Postmark — the round mark the post strikes: where and when, two rings, turned a little, its cancel lines running on to the side.
|
|
5
|
-
* On a card's corner (`postmark` of PostLetter · PostDesk · PostBoxCard) it lies over the edge of the stamp. Information: nothing to press.
|
|
5
|
+
* On a card's corner (`postmark` of PostLetter · PostDesk · PostBoxCard) it lies over the edge of the stamp, by 0.4 of its own size. Information: nothing to press.
|
|
6
6
|
*/
|
|
7
7
|
export function Postmark({ place, date, className, ...rest }) {
|
|
8
8
|
return (_jsxs("div", { className: cx('postmark', className), ...rest, children: [_jsx("span", { children: place }), date != null && _jsx("span", { children: date })] }));
|
package/dist/post/postCorner.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/* the corner of a card that proves the journey: the stamp (a `Stamp`, the app's), the postmark struck over its edge.
|
|
3
|
-
A part of the three post views, never exported: a stamp here shows its pictures only (stamp.css). */
|
|
3
|
+
A part of the three post views, never exported: a stamp here is the postcard's own size, `--post-corner-stamp-w` (72 × 90, outside the stamp's two sizes), and shows its pictures only (stamp.css, post.css). */
|
|
4
4
|
export function PostCorner({ stamp, postmark, ...rest }) {
|
|
5
5
|
if (stamp == null && postmark == null)
|
|
6
6
|
return null;
|
|
@@ -41,6 +41,8 @@ export interface LayerApi {
|
|
|
41
41
|
* `const way = useLayer('way')` → `<CallSheet open={way.value} onOpenChange={(up, i) => (up ? way.open(i!) : way.close())}>`.
|
|
42
42
|
* A Modal: `open={ask.value !== null}` + the app's own `mounted` flag cleared in `onClosed`. Opened again a layer starts anew —
|
|
43
43
|
* give what is inside a new `key` per opening.
|
|
44
|
+
* ONE LAYER PER TICK: `open` builds the new state from the layers read at render, so two layers opened in the same handler lose the
|
|
45
|
+
* first — `a.open(1); b.open(2)` gives `a.value` null and `b.value` 2, and two new entries. Open the second in a later tick.
|
|
44
46
|
*/
|
|
45
47
|
export declare function useLayer(name: string): LayerApi;
|
|
46
48
|
/** useOptionalLayer — the same layer as `useLayer`, or `null` when no `LayerHistory` is above. A missing provider does not warn. */
|
|
@@ -58,6 +58,8 @@ const layerApi = (name, handle) => {
|
|
|
58
58
|
* `const way = useLayer('way')` → `<CallSheet open={way.value} onOpenChange={(up, i) => (up ? way.open(i!) : way.close())}>`.
|
|
59
59
|
* A Modal: `open={ask.value !== null}` + the app's own `mounted` flag cleared in `onClosed`. Opened again a layer starts anew —
|
|
60
60
|
* give what is inside a new `key` per opening.
|
|
61
|
+
* ONE LAYER PER TICK: `open` builds the new state from the layers read at render, so two layers opened in the same handler lose the
|
|
62
|
+
* first — `a.open(1); b.open(2)` gives `a.value` null and `b.value` 2, and two new entries. Open the second in a later tick.
|
|
61
63
|
*/
|
|
62
64
|
export function useLayer(name) {
|
|
63
65
|
const handle = useHistory();
|
package/dist/sheets/Modal.d.ts
CHANGED
|
@@ -16,6 +16,9 @@ export interface ModalProps extends ComponentProps<'article'> {
|
|
|
16
16
|
* A COMPONENT, not a wiring: it does not know its buttons. They are plain `IconBtn`s / `Btn`s with the app's `onClick` — the app
|
|
17
17
|
* records the answer and sets `open={false}`; the modal's one behaviour is how it leaves. `onDismiss` asks to leave unanswered (a tap on
|
|
18
18
|
* the scrim, Esc). When it has arrived, cardds puts the cursor in the field marked `data-autofocus`, else the first field, else the question — write no focus code.
|
|
19
|
+
* MOUNT IT ONLY WHEN IT IS TO SHOW. First mounted with `open={false}` it is drawn in place, then leaves, and calls `onClosed` after about 700ms
|
|
20
|
+
* (measured at 375, 2026-10-11). KNOWN FAULT, not fixed: under React StrictMode `onClosed` is never called for a Modal mounted that way — the
|
|
21
|
+
* effect's first run sets `shown` to false, its cleanup drops the wait, and the second run returns early.
|
|
19
22
|
*/
|
|
20
23
|
export declare function Modal({ open, onClosed, onDismiss, className, ...rest }: ModalProps): import("react").JSX.Element;
|
|
21
24
|
export interface ModalBodyProps extends ComponentProps<'div'> {
|
|
@@ -30,7 +33,7 @@ export interface ModalAskProps extends Omit<ComponentProps<'div'>, 'title'> {
|
|
|
30
33
|
/** a LIGHT ModalAsk: the card's paper and dark words instead of the dark end (Lh 2026-09-29) — same layout, same trio; the one solid button turns dark */
|
|
31
34
|
light?: boolean;
|
|
32
35
|
}
|
|
33
|
-
/** ModalAsk — the dark end of a `Modal` (`light` = paper and dark words): the question, its fine print, and a `BtnTrio`
|
|
36
|
+
/** ModalAsk — the dark end of a `Modal` (`light` = paper and dark words): the question, its fine print, and the answers: a `BtnTrio` of round icon buttons, or a `BtnRow` of word `Btn`s (it fills the card, each button's words on one line; a `Btn quiet` there is edged in the ask's own ink) — plain buttons
|
|
34
37
|
* with the app's `onClick`; the ModalAsk does nothing with them. A `Modal` holding ONLY a ModalAsk (no `ModalBody`) is the CONFIRM: a question with nothing
|
|
35
38
|
* to show above it — the ModalAsk is the whole card and comes in with it in one beat. */
|
|
36
39
|
export declare function ModalAsk({ title, caption, light, className, children, ...rest }: ModalAskProps): import("react").JSX.Element;
|
package/dist/sheets/Modal.js
CHANGED
|
@@ -9,6 +9,9 @@ import { cx } from '../cx.js';
|
|
|
9
9
|
* A COMPONENT, not a wiring: it does not know its buttons. They are plain `IconBtn`s / `Btn`s with the app's `onClick` — the app
|
|
10
10
|
* records the answer and sets `open={false}`; the modal's one behaviour is how it leaves. `onDismiss` asks to leave unanswered (a tap on
|
|
11
11
|
* the scrim, Esc). When it has arrived, cardds puts the cursor in the field marked `data-autofocus`, else the first field, else the question — write no focus code.
|
|
12
|
+
* MOUNT IT ONLY WHEN IT IS TO SHOW. First mounted with `open={false}` it is drawn in place, then leaves, and calls `onClosed` after about 700ms
|
|
13
|
+
* (measured at 375, 2026-10-11). KNOWN FAULT, not fixed: under React StrictMode `onClosed` is never called for a Modal mounted that way — the
|
|
14
|
+
* effect's first run sets `shown` to false, its cleanup drops the wait, and the second run returns early.
|
|
12
15
|
*/
|
|
13
16
|
export function Modal({ open = true, onClosed, onDismiss, className, ...rest }) {
|
|
14
17
|
const ref = useRef(null);
|
|
@@ -58,7 +61,7 @@ export function Modal({ open = true, onClosed, onDismiss, className, ...rest })
|
|
|
58
61
|
export function ModalBody({ className, ...rest }) {
|
|
59
62
|
return _jsx("div", { className: cx('modal__lift', className), ...rest });
|
|
60
63
|
}
|
|
61
|
-
/** ModalAsk — the dark end of a `Modal` (`light` = paper and dark words): the question, its fine print, and a `BtnTrio`
|
|
64
|
+
/** ModalAsk — the dark end of a `Modal` (`light` = paper and dark words): the question, its fine print, and the answers: a `BtnTrio` of round icon buttons, or a `BtnRow` of word `Btn`s (it fills the card, each button's words on one line; a `Btn quiet` there is edged in the ask's own ink) — plain buttons
|
|
62
65
|
* with the app's `onClick`; the ModalAsk does nothing with them. A `Modal` holding ONLY a ModalAsk (no `ModalBody`) is the CONFIRM: a question with nothing
|
|
63
66
|
* to show above it — the ModalAsk is the whole card and comes in with it in one beat. */
|
|
64
67
|
export function ModalAsk({ title, caption, light, className, children, ...rest }) {
|
package/dist/stamp/Stamp.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface StampProps extends Omit<ComponentProps<'span'>, 'onClick'> {
|
|
|
7
7
|
tone?: StickyTone;
|
|
8
8
|
/** the big line at the foot, on the picture — whose stamp it is */
|
|
9
9
|
name?: ReactNode;
|
|
10
|
-
/** a small line above the name (the pictures' words)
|
|
10
|
+
/** a small line above the name (the pictures' words). It shows only on a stamp WIDER than the standard size (`--stamp-size` set above 6rem): at 96 it would be under 5px, so it is not drawn and the name is larger */
|
|
11
11
|
words?: ReactNode;
|
|
12
12
|
/** the pictures alone: `name` and `words` are not drawn. For a stamp stuck on something (a postcard's corner); in a `StampOn` every stamp is bare already. The words are for the album */
|
|
13
13
|
bare?: boolean;
|
|
@@ -18,7 +18,8 @@ export interface StampProps extends Omit<ComponentProps<'span'>, 'onClick'> {
|
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* Stamp — a perforated paper stamp, 4:5: the pictures fill the inner frame on a tone, the words sit on the picture's foot.
|
|
21
|
-
*
|
|
21
|
+
* 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.
|
|
22
|
+
* The one small stamp is a stamp stuck on something (`StampOn`). Every inner length follows the stamp's width.
|
|
22
23
|
* The outer box holds the size and the shadow; the paper inside is the one whose edge is punched (a mask would cut the shadow too).
|
|
23
24
|
*/
|
|
24
25
|
export declare function Stamp({ pictures, tone, name, words, bare, tilt, onClick, className, ...rest }: StampProps): import("react").JSX.Element;
|
|
@@ -29,7 +30,7 @@ export interface StampPlaceProps extends Omit<ComponentProps<'span'>, 'onClick'>
|
|
|
29
30
|
onClick?: () => void;
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
32
|
-
* StampPlace — an empty place for a stamp: a 4:5 frame of round dots the size a Stamp
|
|
33
|
+
* StampPlace — an empty place for a stamp: a 4:5 frame of round dots the size a Stamp is (`--stamp-size`), a word or an Icon inside.
|
|
33
34
|
* The dots are a stroke, not a border: `--stamp-place-dot` is their width, `--stamp-place-gap` the distance from one centre to the next,
|
|
34
35
|
* and a `--stamp-place-dash` above 0 stretches each dot into a dash.
|
|
35
36
|
*/
|
package/dist/stamp/Stamp.js
CHANGED
|
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
3
|
/**
|
|
4
4
|
* 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
|
-
*
|
|
5
|
+
* 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.
|
|
6
|
+
* The one small stamp is a stamp stuck on something (`StampOn`). Every inner length follows the stamp's width.
|
|
6
7
|
* The outer box holds the size and the shadow; the paper inside is the one whose edge is punched (a mask would cut the shadow too).
|
|
7
8
|
*/
|
|
8
9
|
export function Stamp({ pictures, tone = 'custard', name, words, bare, tilt, onClick, className, ...rest }) {
|
|
@@ -13,7 +14,7 @@ export function Stamp({ pictures, tone = 'custard', name, words, bare, tilt, onC
|
|
|
13
14
|
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 })] }))] }) }) }));
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
|
-
* StampPlace — an empty place for a stamp: a 4:5 frame of round dots the size a Stamp
|
|
17
|
+
* StampPlace — an empty place for a stamp: a 4:5 frame of round dots the size a Stamp is (`--stamp-size`), a word or an Icon inside.
|
|
17
18
|
* The dots are a stroke, not a border: `--stamp-place-dot` is their width, `--stamp-place-gap` the distance from one centre to the next,
|
|
18
19
|
* and a `--stamp-place-dash` above 0 stretches each dot into a dash.
|
|
19
20
|
*/
|
|
@@ -2,12 +2,12 @@ import { type ComponentProps, type ReactNode } from 'react';
|
|
|
2
2
|
export interface StampBookProps extends ComponentProps<'div'> {
|
|
3
3
|
/** how many places the page shows: the ones no StampCell fills are drawn empty */
|
|
4
4
|
places?: number;
|
|
5
|
-
/** ONE line of
|
|
5
|
+
/** ONE line of stamps that scrolls sideways, no ground — the tray a member picks from. Each stamp is its own size, as everywhere */
|
|
6
6
|
strip?: boolean;
|
|
7
|
-
/** the page with NO ground — no fill, no padding, no round:
|
|
7
|
+
/** the page with NO ground — no fill, no padding, no round: the stamps lie on what holds the book, three across, each its own size in the middle of its third. Put the name INSIDE the stamp (`Stamp name`), not under it */
|
|
8
8
|
bare?: boolean;
|
|
9
9
|
}
|
|
10
|
-
/** StampBook — the album page: StampCells three across on an album ground, the free places drawn empty up to `places`. */
|
|
10
|
+
/** 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. */
|
|
11
11
|
export declare function StampBook({ places, strip, bare, className, children, ...rest }: StampBookProps): import("react").JSX.Element;
|
|
12
12
|
export interface StampCellProps extends Omit<ComponentProps<'span'>, 'onClick'> {
|
|
13
13
|
/** a short line under the stamp — who it is from */
|
package/dist/stamp/StampBook.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Children } from 'react';
|
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
import { Icon } from '../type/Icon.js';
|
|
5
5
|
import { StampPlace } from './Stamp.js';
|
|
6
|
-
/** StampBook — the album page: StampCells three across on an album ground, the free places drawn empty up to `places`. */
|
|
6
|
+
/** 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. */
|
|
7
7
|
export function StampBook({ places = 0, strip, bare, className, children, ...rest }) {
|
|
8
8
|
const free = Math.max(0, places - Children.toArray(children).length); // toArray drops null / undefined / booleans: `{owned && <StampCell/>}` that is false holds no place (Children.count counts it)
|
|
9
9
|
return (_jsxs("div", { className: cx('stamp-book', strip && 'stamp-book--strip', bare && 'stamp-book--bare', className), ...rest, children: [children, Array.from({ length: free }, (_, i) => _jsx(StampPlace, { faint: true, "aria-hidden": "true" }, i))] }));
|
package/dist/stamp/StampOn.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface StampOnProps extends ComponentProps<'div'> {
|
|
|
19
19
|
/**
|
|
20
20
|
* StampOn — a Stamp (the child) stuck on a surface at the place the member chose. The surface is the nearest positioned box:
|
|
21
21
|
* a Sticky's `stuck`, a postcard's face. `at` is the stamp's middle as a share of that surface, so the note reads the same at any size.
|
|
22
|
+
* The stamp is the SMALL size: 48 × 60 on any surface, whatever the surface's size (`--stamp-sm-size`, one tap wide).
|
|
22
23
|
* With `onAtChange` the member drags it (finger or mouse; the arrow keys move it too): while the finger holds, the stamp writes its
|
|
23
24
|
* own place, and on letting go it ASKS — the app owns `at`. It never leaves the surface.
|
|
24
25
|
*/
|
package/dist/stamp/StampOn.js
CHANGED
|
@@ -19,6 +19,7 @@ const write = (el, at) => { el.style.setProperty('--stamp-on-x', String(at.x));
|
|
|
19
19
|
/**
|
|
20
20
|
* StampOn — a Stamp (the child) stuck on a surface at the place the member chose. The surface is the nearest positioned box:
|
|
21
21
|
* a Sticky's `stuck`, a postcard's face. `at` is the stamp's middle as a share of that surface, so the note reads the same at any size.
|
|
22
|
+
* The stamp is the SMALL size: 48 × 60 on any surface, whatever the surface's size (`--stamp-sm-size`, one tap wide).
|
|
22
23
|
* With `onAtChange` the member drags it (finger or mouse; the arrow keys move it too): while the finger holds, the stamp writes its
|
|
23
24
|
* own place, and on letting go it ASKS — the app owns `at`. It never leaves the surface.
|
|
24
25
|
*/
|
package/dist/sticky/Sticky.d.ts
CHANGED
|
@@ -5,15 +5,15 @@ export interface StickyProps extends Omit<ComponentProps<'div'>, 'onClick'> {
|
|
|
5
5
|
onClick?: () => void;
|
|
6
6
|
/** the paper's colour — one of STICKY_TONES (default custard) */
|
|
7
7
|
tone?: StickyTone;
|
|
8
|
-
/** no folded corner */
|
|
8
|
+
/** the note has been READ: no folded corner. A folded corner (the default) says the note is new — not read yet */
|
|
9
9
|
flat?: boolean;
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/** what a folded note says to a screen reader after its words (default: cardds's own word, "Unread" / "ยังไม่ได้อ่าน" by the page's language) */
|
|
11
|
+
unreadLabel?: string;
|
|
12
12
|
/** a row at the top of the note — a date, a small picture, a label */
|
|
13
13
|
head?: ReactNode;
|
|
14
14
|
/** a StickyDate at the end of the head — the note's top-right corner */
|
|
15
15
|
date?: ReactNode;
|
|
16
|
-
/** turned a little on its middle (the one note of a start screen); a note is straight otherwise */
|
|
16
|
+
/** turned a little on its middle (the one note of a start screen); a note is straight otherwise, unless its holder turns it (`StickyBoard`, `StickyThread lapped`) */
|
|
17
17
|
tilt?: 'left' | 'right';
|
|
18
18
|
/** a small muted line at the bottom ("From …") */
|
|
19
19
|
foot?: ReactNode;
|
|
@@ -23,7 +23,9 @@ export interface StickyProps extends Omit<ComponentProps<'div'>, 'onClick'> {
|
|
|
23
23
|
stuck?: ReactNode;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* Sticky — a paper note: a flat pastel
|
|
26
|
+
* Sticky — a paper note: a flat pastel SQUARE of one size (168, the knob `--sticky-size`), square corners, a soft shadow under it.
|
|
27
|
+
* Every note is that one square whatever it says: words it cannot hold are cut, never the note grown.
|
|
28
|
+
* THE FOLD SAYS UNREAD: the bottom-right corner folded over = not read yet; `flat` = read. A folded note says so to a screen reader too (`unreadLabel`).
|
|
27
29
|
* The outer box holds the tone and the shadow; the paper inside is the part that is cut (a clip would cut the shadow too).
|
|
28
30
|
*/
|
|
29
|
-
export declare function Sticky({ tone, flat,
|
|
31
|
+
export declare function Sticky({ tone, flat, unreadLabel, head, date, tilt, foot, corner, stuck, onClick, className, children, ...rest }: StickyProps): import("react").JSX.Element;
|
package/dist/sticky/Sticky.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
+
import { word } from '../words.js';
|
|
3
4
|
/**
|
|
4
|
-
* Sticky — a paper note: a flat pastel
|
|
5
|
+
* Sticky — a paper note: a flat pastel SQUARE of one size (168, the knob `--sticky-size`), square corners, a soft shadow under it.
|
|
6
|
+
* Every note is that one square whatever it says: words it cannot hold are cut, never the note grown.
|
|
7
|
+
* THE FOLD SAYS UNREAD: the bottom-right corner folded over = not read yet; `flat` = read. A folded note says so to a screen reader too (`unreadLabel`).
|
|
5
8
|
* The outer box holds the tone and the shadow; the paper inside is the part that is cut (a clip would cut the shadow too).
|
|
6
9
|
*/
|
|
7
|
-
export function Sticky({ tone = 'custard', flat,
|
|
10
|
+
export function Sticky({ tone = 'custard', flat, unreadLabel, head, date, tilt, foot, corner, stuck, onClick, className, children, ...rest }) {
|
|
8
11
|
const notched = corner != null;
|
|
9
12
|
const tap = onClick != null && !notched;
|
|
10
13
|
const Box = (tap ? 'button' : 'div');
|
|
11
14
|
const Part = tap ? 'span' : 'div';
|
|
12
15
|
const boxProps = (tap ? { type: 'button', onClick, ...rest } : rest);
|
|
13
|
-
return (_jsxs(Box, { className: cx('sticky', `sticky--${tone}`, flat && 'sticky--flat',
|
|
16
|
+
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 })] }));
|
|
14
17
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
export type StickyAnswerReason = 'focus' | 'outside' | 'escape';
|
|
3
|
+
export interface StickyAnswerProps extends ComponentProps<'div'> {
|
|
4
|
+
/** CONTROLLED: the note is being written on (at the top, the screen behind washed out) — with `onWritingChange`; or `defaultWriting` */
|
|
5
|
+
writing?: boolean;
|
|
6
|
+
/** UNCONTROLLED: starts being written on (default false: the note waits at the foot) */
|
|
7
|
+
defaultWriting?: boolean;
|
|
8
|
+
/** it asks: to write — the cursor went into the note (`focus`); to stop — a touch outside (`outside`) or Escape (`escape`). The phone's back and sending are the app's: it sets `writing` */
|
|
9
|
+
onWritingChange?: (writing: boolean, reason: StickyAnswerReason) => void;
|
|
10
|
+
/** the row under the note while it is written on: the paper chooser at the start (a `StickyPick`), the send button at the end (an `IconBtn`) */
|
|
11
|
+
tools?: ReactNode;
|
|
12
|
+
/** what stands under that row — the stamp the writer may stick on the note (a `StampCell on` button), a line of words */
|
|
13
|
+
tray?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* StickyAnswer — the note you answer on, and its two places. The child is the note itself: a `Sticky flat` holding a `StickyField` (give it
|
|
17
|
+
* a `placeholder`: the pale word). AT REST it waits at the screen's foot, near the right, three quarters of it in sight. A touch on it puts
|
|
18
|
+
* the cursor in the field, and the holder ASKS to write: the note goes to the top of the screen, the screen behind is washed pale and takes
|
|
19
|
+
* no touch, and `tools` and `tray` wake under it (inert until then). The note keeps the one size in both places. A child of the `Screen`,
|
|
20
|
+
* a layer over it. Controlled like an input: `writing` + `onWritingChange(writing, reason)`, or `defaultWriting`; the words, the paper and
|
|
21
|
+
* what is stuck on the note are the app's, on the parts it passes in. When writing ends the cursor leaves the note, so the keyboard goes down.
|
|
22
|
+
*/
|
|
23
|
+
export declare function StickyAnswer({ writing, defaultWriting, onWritingChange, tools, tray, className, children, onKeyDown, ...rest }: StickyAnswerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { cx } from '../cx.js';
|
|
4
|
+
import { useControlled } from '../state.js';
|
|
5
|
+
/**
|
|
6
|
+
* StickyAnswer — the note you answer on, and its two places. The child is the note itself: a `Sticky flat` holding a `StickyField` (give it
|
|
7
|
+
* a `placeholder`: the pale word). AT REST it waits at the screen's foot, near the right, three quarters of it in sight. A touch on it puts
|
|
8
|
+
* the cursor in the field, and the holder ASKS to write: the note goes to the top of the screen, the screen behind is washed pale and takes
|
|
9
|
+
* no touch, and `tools` and `tray` wake under it (inert until then). The note keeps the one size in both places. A child of the `Screen`,
|
|
10
|
+
* a layer over it. Controlled like an input: `writing` + `onWritingChange(writing, reason)`, or `defaultWriting`; the words, the paper and
|
|
11
|
+
* what is stuck on the note are the app's, on the parts it passes in. When writing ends the cursor leaves the note, so the keyboard goes down.
|
|
12
|
+
*/
|
|
13
|
+
export function StickyAnswer({ writing, defaultWriting = false, onWritingChange, tools, tray, className, children, onKeyDown, ...rest }) {
|
|
14
|
+
const [shown, request] = useControlled(writing, defaultWriting, onWritingChange);
|
|
15
|
+
const box = useRef(null);
|
|
16
|
+
const note = useRef(null);
|
|
17
|
+
const was = useRef(shown);
|
|
18
|
+
/* the cursor follows the state when the app moved it (never on mount: a page of stories must not lose its cursor to one) */
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (was.current === shown)
|
|
21
|
+
return;
|
|
22
|
+
was.current = shown;
|
|
23
|
+
const held = document.activeElement;
|
|
24
|
+
if (shown && !note.current?.contains(held))
|
|
25
|
+
note.current?.querySelector('textarea, input')?.focus();
|
|
26
|
+
if (!shown && held instanceof HTMLElement && box.current?.contains(held))
|
|
27
|
+
held.blur();
|
|
28
|
+
}, [shown]);
|
|
29
|
+
const key = (e) => {
|
|
30
|
+
onKeyDown?.(e);
|
|
31
|
+
if (e.key === 'Escape' && shown)
|
|
32
|
+
request(false, 'escape');
|
|
33
|
+
};
|
|
34
|
+
return (_jsxs("div", { ref: box, className: cx('sticky-answer', shown && 'sticky-answer--writing', className), onKeyDown: key, ...rest, children: [_jsx("div", { className: "sticky-answer__scrim", onClick: () => request(false, 'outside') }), _jsx("div", { ref: note, className: "sticky-answer__note", onFocus: () => request(true, 'focus'), children: children }), (tools != null || tray != null) && (_jsxs("div", { className: "sticky-answer__under", inert: !shown, children: [tools != null && _jsx("div", { className: "sticky-answer__tools", children: tools }), tray != null && _jsx("div", { className: "sticky-answer__tray", children: tray })] }))] }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
export interface StickyBoardProps extends ComponentProps<'div'> {
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* StickyBoard — Sticky notes stuck on a page: rows that wrap by the width (two on a phone, three on a wider screen), edge to edge,
|
|
6
|
+
* each note turned a little by one of four unlike angles in turn, so no two notes that touch lean alike. LATER = ON TOP: where two cross, the right one
|
|
7
|
+
* lies over the left and the lower over the upper — the order of the children is the order they lie in. A note here is the same note as
|
|
8
|
+
* where it is opened: its words, breaking exactly as they do there, the stamps really stuck on it, and — laid over the note's foot, taking no room from
|
|
9
|
+
* the words — its `foot`: whose it is as a small label at the start (`Chip sm`) and the time at the end. Knobs: `--sticky-board-lap` (how far each lies over the one
|
|
10
|
+
* before, 0), `--sticky-board-turn-1` … `-4` (the four angles), `--sticky-board-drop`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function StickyBoard({ className, ...rest }: StickyBoardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from '../cx.js';
|
|
3
|
+
/**
|
|
4
|
+
* StickyBoard — Sticky notes stuck on a page: rows that wrap by the width (two on a phone, three on a wider screen), edge to edge,
|
|
5
|
+
* each note turned a little by one of four unlike angles in turn, so no two notes that touch lean alike. LATER = ON TOP: where two cross, the right one
|
|
6
|
+
* lies over the left and the lower over the upper — the order of the children is the order they lie in. A note here is the same note as
|
|
7
|
+
* where it is opened: its words, breaking exactly as they do there, the stamps really stuck on it, and — laid over the note's foot, taking no room from
|
|
8
|
+
* the words — its `foot`: whose it is as a small label at the start (`Chip sm`) and the time at the end. Knobs: `--sticky-board-lap` (how far each lies over the one
|
|
9
|
+
* before, 0), `--sticky-board-turn-1` … `-4` (the four angles), `--sticky-board-drop`.
|
|
10
|
+
*/
|
|
11
|
+
export function StickyBoard({ className, ...rest }) {
|
|
12
|
+
return _jsx("div", { className: cx('sticky-board', className), ...rest });
|
|
13
|
+
}
|
|
@@ -2,21 +2,27 @@ import type { ComponentProps, ReactNode } from 'react';
|
|
|
2
2
|
export interface StickyThreadProps extends ComponentProps<'div'> {
|
|
3
3
|
/** a dotted line running down under the avatars, post to post (a feed) */
|
|
4
4
|
rail?: boolean;
|
|
5
|
+
/** a chat with NO head rows: theirs at the left, mine at the right, each turn lying over a corner of the one before, turned a little; a post's `time` stands close under its note, its `who` is not drawn and its `name` is only heard. Not with `rail` */
|
|
6
|
+
lapped?: boolean;
|
|
5
7
|
}
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* StickyThread — a stack of StickyPosts: a chat or a feed told in notes. Two ways: head rows (avatar · name · time above each turn; `rail` for a feed),
|
|
10
|
+
* or `lapped` — the chat of paper notes lying over one another, the name once in the screen's top bar. Later turns lie on top.
|
|
11
|
+
*/
|
|
12
|
+
export declare function StickyThread({ rail, lapped, className, ...rest }: StickyThreadProps): import("react").JSX.Element;
|
|
8
13
|
export interface StickyPostProps extends ComponentProps<'div'> {
|
|
9
|
-
/** who wrote it (an Avatar) */
|
|
14
|
+
/** who wrote it (an Avatar); not drawn in a `lapped` thread */
|
|
10
15
|
who?: ReactNode;
|
|
11
|
-
/** their name, beside the avatar */
|
|
16
|
+
/** their name, beside the avatar; in a `lapped` thread it is only heard by a screen reader */
|
|
12
17
|
name?: ReactNode;
|
|
13
|
-
/** when, at the far end of the head row */
|
|
18
|
+
/** when, at the far end of the head row; in a `lapped` thread, close under the note on its own side */
|
|
14
19
|
time?: ReactNode;
|
|
15
20
|
/** whose: theirs on the left (default), mine on the right */
|
|
16
21
|
side?: 'left' | 'right';
|
|
17
22
|
}
|
|
18
23
|
/**
|
|
19
24
|
* StickyPost — one person's turn: a head row (avatar · name · time at the far end) and, under the name, their Sticky notes.
|
|
20
|
-
* `side="right"` mirrors it. The rail mark is always there; only a `StickyThread rail` draws it.
|
|
25
|
+
* `side="right"` mirrors it. The rail mark is always there; only a `StickyThread rail` draws it. In a `StickyThread lapped` the same
|
|
26
|
+
* turn is the note alone, at its side, with the time under it.
|
|
21
27
|
*/
|
|
22
28
|
export declare function StickyPost({ who, name, time, side, className, children, ...rest }: StickyPostProps): import("react").JSX.Element;
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* StickyThread — a stack of StickyPosts: a chat or a feed told in notes. Two ways: head rows (avatar · name · time above each turn; `rail` for a feed),
|
|
5
|
+
* or `lapped` — the chat of paper notes lying over one another, the name once in the screen's top bar. Later turns lie on top.
|
|
6
|
+
*/
|
|
7
|
+
export function StickyThread({ rail, lapped, className, ...rest }) {
|
|
8
|
+
return _jsx("div", { className: cx('sticky-thread', rail && !lapped && 'sticky-thread--rail', lapped && 'sticky-thread--lapped', className), ...rest });
|
|
6
9
|
}
|
|
7
10
|
/**
|
|
8
11
|
* StickyPost — one person's turn: a head row (avatar · name · time at the far end) and, under the name, their Sticky notes.
|
|
9
|
-
* `side="right"` mirrors it. The rail mark is always there; only a `StickyThread rail` draws it.
|
|
12
|
+
* `side="right"` mirrors it. The rail mark is always there; only a `StickyThread rail` draws it. In a `StickyThread lapped` the same
|
|
13
|
+
* turn is the note alone, at its side, with the time under it.
|
|
10
14
|
*/
|
|
11
15
|
export function StickyPost({ who, name, time, side = 'left', className, children, ...rest }) {
|
|
12
16
|
return (_jsxs("div", { className: cx('sticky-post', side === 'right' && 'sticky-post--right', className), ...rest, children: [who != null && _jsx("span", { className: "sticky-post__who", children: who }), _jsxs("div", { className: "sticky-post__head", children: [name != null && _jsx("span", { className: "sticky-post__name", children: name }), time != null && _jsx("span", { className: "sticky-post__time", children: time })] }), _jsx("span", { className: "sticky-post__rail", "aria-hidden": "true" }), _jsx("div", { className: "sticky-post__notes", children: children })] }));
|
package/dist/words.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type WordKey = 'resize' | 'back' | 'next' | 'previous' | 'skip' | 'digit' | 'progress' | 'image' | 'mood' | 'photos' | 'voiceNote' | 'play' | 'pause' | 'to' | 'turnToBack' | 'turnToPicture' | 'message' | 'address' | 'toMessage' | 'toAddress' | 'stampOn' | 'stampOff' | 'stamps';
|
|
1
|
+
export type WordKey = 'resize' | 'back' | 'next' | 'previous' | 'skip' | 'digit' | 'progress' | 'image' | 'mood' | 'photos' | 'voiceNote' | 'play' | 'pause' | 'to' | 'turnToBack' | 'turnToPicture' | 'message' | 'address' | 'toMessage' | 'toAddress' | 'stampOn' | 'stampOff' | 'stamps' | 'unread';
|
|
2
2
|
type Words = Record<WordKey, string>;
|
|
3
3
|
/** add or retune the words for a language (`'lo'`, or `'th'` to change one) — once, at app start. Missing words fall back to English. */
|
|
4
4
|
export declare function setWords(lang: string, words: Partial<Words>): void;
|
package/dist/words.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const sets = {
|
|
2
2
|
en: { resize: 'Resize', back: 'Back', next: 'Next', previous: 'Previous', skip: 'Skip', digit: 'Digit', progress: 'Progress', image: 'Image', mood: 'Mood', photos: 'Photos', voiceNote: 'Voice note',
|
|
3
|
-
play: 'Play', pause: 'Pause', to: 'To', turnToBack: 'See the back', turnToPicture: 'See the picture', message: 'Message', address: 'Address', toMessage: 'Back to the message', toAddress: 'On to the address', stampOn: 'Stick a stamp', stampOff: 'Take the stamp off', stamps: 'Choose a stamp' },
|
|
3
|
+
play: 'Play', pause: 'Pause', to: 'To', turnToBack: 'See the back', turnToPicture: 'See the picture', message: 'Message', address: 'Address', toMessage: 'Back to the message', toAddress: 'On to the address', stampOn: 'Stick a stamp', stampOff: 'Take the stamp off', stamps: 'Choose a stamp', unread: 'Unread' },
|
|
4
4
|
th: { resize: 'ปรับความสูง', back: 'กลับ', next: 'ถัดไป', previous: 'ก่อนหน้า', skip: 'ข้าม', digit: 'หลักที่', progress: 'ความคืบหน้า', image: 'รูปภาพ', mood: 'อารมณ์', photos: 'รูปภาพ', voiceNote: 'ข้อความเสียง',
|
|
5
|
-
play: 'ฟังเสียง', pause: 'หยุดเสียง', to: 'ถึง', turnToBack: 'พลิกดูด้านหลัง', turnToPicture: 'พลิกดูรูป', message: 'ข้อความ', address: 'จ่าหน้า', toMessage: 'เลื่อนกลับไปข้อความ', toAddress: 'เลื่อนไปจ่าหน้า', stampOn: 'ติดแสตมป์', stampOff: 'แกะแสตมป์ออก', stamps: 'เลือกแสตมป์' },
|
|
5
|
+
play: 'ฟังเสียง', pause: 'หยุดเสียง', to: 'ถึง', turnToBack: 'พลิกดูด้านหลัง', turnToPicture: 'พลิกดูรูป', message: 'ข้อความ', address: 'จ่าหน้า', toMessage: 'เลื่อนกลับไปข้อความ', toAddress: 'เลื่อนไปจ่าหน้า', stampOn: 'ติดแสตมป์', stampOff: 'แกะแสตมป์ออก', stamps: 'เลือกแสตมป์', unread: 'ยังไม่ได้อ่าน' },
|
|
6
6
|
};
|
|
7
7
|
/** add or retune the words for a language (`'lo'`, or `'th'` to change one) — once, at app start. Missing words fall back to English. */
|
|
8
8
|
export function setWords(lang, words) {
|
package/lib/catalog.mjs
CHANGED
|
@@ -71,6 +71,7 @@ export function get(cat, name, { html = false, all = false } = {}) {
|
|
|
71
71
|
...(c.role && { role: c.role }),
|
|
72
72
|
element: c.element,
|
|
73
73
|
props: c.props.map((p) => ({ ...p, ...(isSlot(p.type) && { slot: true }) })),
|
|
74
|
+
...(c.params && { params: c.params }),
|
|
74
75
|
variations: [...own, ...elsewhere.filter((s) => shown.includes(s))].map(show),
|
|
75
76
|
...(elsewhere.length > shown.length && { alsoUsedIn: elsewhere.filter((s) => !shown.includes(s)).map((s) => s.id) }),
|
|
76
77
|
};
|
package/lib/mcp.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { GUIDE } from './guide.mjs';
|
|
|
10
10
|
const { version } = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'));
|
|
11
11
|
/* A server lives as long as its session — hours, across releases. What it serves must be the catalog as it is NOW (Lh 2026-10-10: a
|
|
12
12
|
session opened the day before did not know CardFeed): `keeper` asks `stamp` before every answer and loads again when it changed. */
|
|
13
|
-
const SOURCES = ['src', 'css', '.design-sync/previews', 'docs', 'lib', 'scripts'];
|
|
13
|
+
const SOURCES = ['src', 'css', '.design-sync/previews', 'docs', 'lib', 'scripts', 'package.json', 'gallery/vite.config.ts']; // the last two are read by the build too: the version, and how the stories render
|
|
14
14
|
const newest = (path) => {
|
|
15
15
|
let st;
|
|
16
16
|
try { st = statSync(path); } catch { return 0; }
|
package/lib/role.mjs
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
// lib/role.mjs — the role table (docs/roles.md) read, and what a role says a component is FOR. Pure, no fs: the catalog (node), scripts/roles.mjs
|
|
2
2
|
// and the gallery's nav (browser) all read the table through these.
|
|
3
3
|
const AXES = ['represents', 'count', 'density', 'job'];
|
|
4
|
+
/** @returns {never} */
|
|
5
|
+
const fail = (component, stands) => { throw new Error(`roles: ${component}: cannot read "stands in for": ${stands}`); };
|
|
4
6
|
const values = (cell) => cell.trim().split(' · ').map((v) => v.trim()).filter(Boolean);
|
|
5
7
|
|
|
6
8
|
/** the text of docs/roles.md → `vocabulary` (each axis's closed values) and `rows` (one per component). Hand-written; the vocabulary is the
|
|
7
|
-
* table's own definition lines, so a value outside it is the test's to catch, not this reader's.
|
|
9
|
+
* table's own definition lines, so a value outside it is the test's to catch, not this reader's.
|
|
10
|
+
* @param {string} doc */
|
|
8
11
|
export function parseRoles(doc) {
|
|
9
12
|
const vocabulary = Object.fromEntries([...doc.matchAll(/^- `([a-z]+)`: (.+)$/gm)].filter((m) => AXES.includes(m[1])).map((m) => [m[1], values(m[2])]));
|
|
10
|
-
|
|
13
|
+
// every line of the table is a row, its header or its rule: one that is none of them is an error — a regex over the whole text would drop it
|
|
14
|
+
// and the component would simply have no role.
|
|
15
|
+
const ROW = /^\| ([A-Za-z]\w*) \| ([a-z]+) \|(.*)\|$/;
|
|
16
|
+
const rows = doc.split('\n').filter((line) => line.startsWith('|') && !/^\| component \|/.test(line) && !/^\|[-| ]+$/.test(line)).map((line) => {
|
|
17
|
+
const m = ROW.exec(line);
|
|
18
|
+
if (!m) throw new Error(`roles: cannot read this table line: ${line}`);
|
|
11
19
|
const [represents, count, density, job, stands, note] = m[3].split('|').map((c) => c.trim());
|
|
12
20
|
return {
|
|
13
21
|
component: m[1],
|
|
14
22
|
group: m[2],
|
|
15
23
|
represents: values(represents), count: values(count), density: values(density), job: values(job),
|
|
16
|
-
standsInFor: stands ? stands.split(';').map((s) => s.trim().match(/^(\w+)(?: \((.*)\))?$/)).map((x) => ({ name: x[1], condition: x[2] ?? '' })) : [],
|
|
24
|
+
standsInFor: stands ? stands.split(';').map((s) => s.trim().match(/^(\w+)(?: \((.*)\))?$/) ?? fail(m[1], stands)).map((x) => ({ name: x[1], condition: x[2] ?? '' })) : [],
|
|
17
25
|
note: note.replace(/^\?\s*/, ''),
|
|
18
26
|
unsure: note.startsWith('?'),
|
|
19
27
|
};
|
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'], PostLetter: ['backLabel', 'pictureLabel', 'playLabel', 'pauseLabel', 'toLabel'],
|
|
15
|
+
Calendar: ['prevLabel', 'nextLabel'], SheetStep: ['skipLabel'], Sticky: ['unreadLabel'], PostLetter: ['backLabel', 'pictureLabel', 'playLabel', 'pauseLabel', 'toLabel'],
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
const lineOf = (text, idx) => text.slice(0, idx).split('\n').length;
|
|
@@ -28,7 +28,12 @@ export function carddsClasses(dir = CSS_DIR) {
|
|
|
28
28
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
29
29
|
.replace(/url\([^)]*\)/g, '')
|
|
30
30
|
.replace(/"[^"]*"|'[^']*'/g, '');
|
|
31
|
-
|
|
31
|
+
// only a selector can name a class: the text in front of a `{`. A `.` after a word character is a class too (`.field.field--error`) — the lookbehind this
|
|
32
|
+
// used to have for numbers (`1.5rem`) is not needed here, and it cost every class that only ever stands second in a compound selector.
|
|
33
|
+
for (const sel of css.matchAll(/([^{};]+)\{/g)) {
|
|
34
|
+
if (sel[1].trimStart().startsWith('@')) continue;
|
|
35
|
+
for (const m of sel[1].matchAll(/(?<!\\)\.(-?[a-zA-Z_][\w-]*)/g)) if (!/^(is|has)-/.test(m[1])) set.add(m[1]); // a bare state word (`is-open`) is every project's too — it only matters beside a cardds class, which is caught
|
|
36
|
+
}
|
|
32
37
|
}
|
|
33
38
|
if (dir === CSS_DIR) classes = set;
|
|
34
39
|
return set;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.84",
|
|
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.",
|