@usableapp/cardds 0.7.81 → 0.7.83
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 +40 -0
- package/README.md +46 -4
- package/bin/cardds.mjs +21 -10
- package/css/actions.css +2 -0
- package/css/layover.css +5 -0
- package/css/post.css +1 -1
- package/css/stamp.css +15 -10
- package/css/sticky.css +139 -28
- package/dist/actions/Chip.d.ts +4 -2
- package/dist/actions/Chip.js +3 -3
- package/dist/cardds.css +162 -39
- package/dist/catalog.json +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/sheets/Modal.d.ts +1 -1
- package/dist/sheets/Modal.js +1 -1
- package/dist/stamp/Stamp.d.ts +3 -2
- package/dist/stamp/Stamp.js +3 -2
- 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/sheets/Modal.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface ModalAskProps extends Omit<ComponentProps<'div'>, 'title'> {
|
|
|
30
30
|
/** 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
31
|
light?: boolean;
|
|
32
32
|
}
|
|
33
|
-
/** ModalAsk — the dark end of a `Modal` (`light` = paper and dark words): the question, its fine print, and a `BtnTrio`
|
|
33
|
+
/** 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
34
|
* 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
35
|
* to show above it — the ModalAsk is the whole card and comes in with it in one beat. */
|
|
36
36
|
export declare function ModalAsk({ title, caption, light, className, children, ...rest }: ModalAskProps): import("react").JSX.Element;
|
package/dist/sheets/Modal.js
CHANGED
|
@@ -58,7 +58,7 @@ export function Modal({ open = true, onClosed, onDismiss, className, ...rest })
|
|
|
58
58
|
export function ModalBody({ className, ...rest }) {
|
|
59
59
|
return _jsx("div", { className: cx('modal__lift', className), ...rest });
|
|
60
60
|
}
|
|
61
|
-
/** ModalAsk — the dark end of a `Modal` (`light` = paper and dark words): the question, its fine print, and a `BtnTrio`
|
|
61
|
+
/** 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
62
|
* 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
63
|
* to show above it — the ModalAsk is the whole card and comes in with it in one beat. */
|
|
64
64
|
export function ModalAsk({ title, caption, light, className, children, ...rest }) {
|
package/dist/stamp/Stamp.d.ts
CHANGED
|
@@ -29,7 +29,8 @@ export interface StampPlaceProps extends Omit<ComponentProps<'span'>, 'onClick'>
|
|
|
29
29
|
onClick?: () => void;
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
-
* StampPlace — an empty place for a stamp: a
|
|
33
|
-
* The
|
|
32
|
+
* StampPlace — an empty place for a stamp: a 4:5 frame of round dots the size a Stamp would be, a word or an Icon inside.
|
|
33
|
+
* 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
|
+
* and a `--stamp-place-dash` above 0 stretches each dot into a dash.
|
|
34
35
|
*/
|
|
35
36
|
export declare function StampPlace({ faint, onClick, className, children, ...rest }: StampPlaceProps): import("react").JSX.Element;
|
package/dist/stamp/Stamp.js
CHANGED
|
@@ -13,8 +13,9 @@ export function Stamp({ pictures, tone = 'custard', name, words, bare, tilt, onC
|
|
|
13
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 })] }))] }) }) }));
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* StampPlace — an empty place for a stamp: a
|
|
17
|
-
* The
|
|
16
|
+
* StampPlace — an empty place for a stamp: a 4:5 frame of round dots the size a Stamp would be, a word or an Icon inside.
|
|
17
|
+
* 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
|
+
* and a `--stamp-place-dash` above 0 stretches each dot into a dash.
|
|
18
19
|
*/
|
|
19
20
|
export function StampPlace({ faint, onClick, className, children, ...rest }) {
|
|
20
21
|
const tap = onClick != null;
|
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.83",
|
|
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.",
|