@usableapp/cardds 0.7.73 → 0.7.74
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 +20 -0
- package/README.md +4 -1
- package/bin/cardds.mjs +4 -2
- package/css/layover.css +4 -0
- package/dist/cardds.css +4 -0
- package/dist/catalog.json +1 -1
- package/dist/choice/Calendar.d.ts +2 -2
- package/dist/post/PostLetter.d.ts +5 -5
- package/dist/sheets/Drawer.d.ts +1 -1
- package/dist/sheets/Drawer.js +1 -1
- package/dist/sheets/Sheet.d.ts +2 -1
- package/dist/sheets/Sheet.js +1 -0
- package/dist/sheets/SheetStack.d.ts +1 -0
- package/dist/sheets/SheetStack.js +1 -0
- package/dist/sheets/SheetSteps.d.ts +2 -2
- package/dist/sheets/SheetSteps.js +1 -1
- package/dist/sheets/StackSheet.d.ts +2 -2
- package/dist/sheets/StackSheet.js +1 -1
- package/dist/templates/CallSheet.d.ts +2 -2
- package/dist/templates/Home.d.ts +1 -1
- package/dist/templates/Walkthrough.d.ts +2 -2
- package/lib/catalog.mjs +21 -2
- package/lib/guide.mjs +16 -0
- package/lib/mcp.mjs +2 -1
- package/lint/eslint.mjs +2 -2
- package/lint/rules.mjs +74 -2
- package/package.json +1 -1
|
@@ -16,9 +16,9 @@ export interface CalendarProps extends Omit<ComponentProps<'div'>, 'title' | 'on
|
|
|
16
16
|
onPrev?: () => void;
|
|
17
17
|
/** the next-month button was tapped: the app turns the month, as with `onPrev` */
|
|
18
18
|
onNext?: () => void;
|
|
19
|
-
/** the previous button's accessible name
|
|
19
|
+
/** the previous button's accessible name — cardds names it in the page's language (`<html lang>`); pass it only for different words */
|
|
20
20
|
prevLabel?: string;
|
|
21
|
-
/** the next button's accessible name
|
|
21
|
+
/** the next button's accessible name — cardds names it in the page's language (`<html lang>`); pass it only for different words */
|
|
22
22
|
nextLabel?: string;
|
|
23
23
|
/** softly marked days */
|
|
24
24
|
marks?: number[];
|
|
@@ -32,15 +32,15 @@ export interface PostLetterProps extends ComponentProps<'div'> {
|
|
|
32
32
|
sign?: ReactNode;
|
|
33
33
|
/** one line under the card: the trip it made */
|
|
34
34
|
journey?: ReactNode;
|
|
35
|
-
/** the tab's words while the picture is in front
|
|
35
|
+
/** the tab's words while the picture is in front — cardds names it in the page's language (`<html lang>`); pass it only for different words */
|
|
36
36
|
backLabel?: string;
|
|
37
|
-
/** the tab's words while the back is in front */
|
|
37
|
+
/** the tab's words while the back is in front — named by cardds like `backLabel`; pass it only for different words */
|
|
38
38
|
pictureLabel?: string;
|
|
39
|
-
/** the play control's name while the note is paused
|
|
39
|
+
/** the play control's name while the note is paused — named by cardds in the page's language (`<html lang>`); pass it only for different words */
|
|
40
40
|
playLabel?: string;
|
|
41
|
-
/** the play control's name while the note plays
|
|
41
|
+
/** the play control's name while the note plays — named by cardds like `playLabel`; pass it only for different words */
|
|
42
42
|
pauseLabel?: string;
|
|
43
|
-
/** the small word before the name
|
|
43
|
+
/** the small word before the name — named by cardds in the page's language (`<html lang>`); pass it only for different words */
|
|
44
44
|
toLabel?: ReactNode;
|
|
45
45
|
}
|
|
46
46
|
/**
|
package/dist/sheets/Drawer.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface ModalProps extends ComponentProps<'article'> {
|
|
|
15
15
|
* out below while the lift moves up.
|
|
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
|
-
* the scrim, Esc).
|
|
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
19
|
*/
|
|
20
20
|
export declare function Modal({ open, onClosed, onDismiss, className, ...rest }: ModalProps): import("react").JSX.Element;
|
|
21
21
|
export interface LiftProps extends ComponentProps<'div'> {
|
package/dist/sheets/Drawer.js
CHANGED
|
@@ -8,7 +8,7 @@ import { cx } from '../cx.js';
|
|
|
8
8
|
* out below while the lift moves up.
|
|
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
|
-
* the scrim, Esc).
|
|
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
12
|
*/
|
|
13
13
|
export function Modal({ open = true, onClosed, onDismiss, className, ...rest }) {
|
|
14
14
|
const ref = useRef(null);
|
package/dist/sheets/Sheet.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface SheetProps extends Omit<ComponentProps<'article'>, 'onChange'>
|
|
|
19
19
|
onSettled?: (state: SheetState) => void;
|
|
20
20
|
/** the states a drag snaps to and a reveal may climb through — "peek half full", or an array (default all four) */
|
|
21
21
|
states?: string | readonly Resting[];
|
|
22
|
-
/** the grab handle; `false` for a sheet with no handle
|
|
22
|
+
/** the grab handle; `false` for a sheet with no handle. cardds names it in the page's language (`<html lang>`) — a string is for different words only */
|
|
23
23
|
handle?: boolean | string;
|
|
24
24
|
/** the grab mark. `line` (default) or `dots`: both sit in the card's top padding, the same distance from the edge, so the title does not move */
|
|
25
25
|
mark?: 'line' | 'dots';
|
|
@@ -50,5 +50,6 @@ export interface SheetProps extends Omit<ComponentProps<'article'>, 'onChange'>
|
|
|
50
50
|
* fixed habit of every sheet. A sheet asked up in the same box meanwhile waits for it: "one left, another came", never two sheets
|
|
51
51
|
* crossing and never one continuous move, whatever the app's wiring (both states set in one render, or the next raised from the
|
|
52
52
|
* first's `onSettled`) — the app adds no timer.
|
|
53
|
+
* The CURSOR is cardds's too: once it has come up it waits in the field marked `data-autofocus`, else the first field, else the heading — write no `autoFocus` or `.focus()` for it.
|
|
53
54
|
*/
|
|
54
55
|
export declare function Sheet({ state, defaultState, onStateChange, onSettled, states, handle, mark, refreshing, tone, paper, className, children, onFocus, ...rest }: SheetProps): import("react").JSX.Element;
|
package/dist/sheets/Sheet.js
CHANGED
|
@@ -37,6 +37,7 @@ function markLong(node, long) {
|
|
|
37
37
|
* fixed habit of every sheet. A sheet asked up in the same box meanwhile waits for it: "one left, another came", never two sheets
|
|
38
38
|
* crossing and never one continuous move, whatever the app's wiring (both states set in one render, or the next raised from the
|
|
39
39
|
* first's `onSettled`) — the app adds no timer.
|
|
40
|
+
* The CURSOR is cardds's too: once it has come up it waits in the field marked `data-autofocus`, else the first field, else the heading — write no `autoFocus` or `.focus()` for it.
|
|
40
41
|
*/
|
|
41
42
|
export function Sheet({ state, defaultState = 'half', onStateChange, onSettled, states, handle = true, mark = 'line', refreshing, tone, paper, className, children, onFocus, ...rest }) {
|
|
42
43
|
const [current, request] = useControlled(state, defaultState, onStateChange);
|
|
@@ -38,5 +38,6 @@ export interface SheetStackProps extends ComponentProps<'div'> {
|
|
|
38
38
|
* the phone's back button can open and close a section. WHAT the sheet does with that, and with `refreshing`, lives on the card (`stack`):
|
|
39
39
|
* the place classes, the grip, the pull. The stack only says which index. Sheets are rigid — the open one slides up, the ones after it leave
|
|
40
40
|
* below, the ones before tuck under nearest-first; closing reverses in two beats. Only `translate` (stack.css); never a size.
|
|
41
|
+
* The CURSOR is cardds's: a sheet that opens takes it in the field marked `data-autofocus`, else the first field, else the heading — write no `autoFocus` or `.focus()` for it.
|
|
41
42
|
*/
|
|
42
43
|
export declare function SheetStack({ mode, open, defaultOpen, onOpenChange, onRefresh, refreshing, mark, fanned, closed, rise, base, className, children, onClickCapture, onPointerDownCapture, ref: passed, ...rest }: SheetStackProps): import("react").JSX.Element;
|
|
@@ -55,6 +55,7 @@ function stamp(child, place) {
|
|
|
55
55
|
* the phone's back button can open and close a section. WHAT the sheet does with that, and with `refreshing`, lives on the card (`stack`):
|
|
56
56
|
* the place classes, the grip, the pull. The stack only says which index. Sheets are rigid — the open one slides up, the ones after it leave
|
|
57
57
|
* below, the ones before tuck under nearest-first; closing reverses in two beats. Only `translate` (stack.css); never a size.
|
|
58
|
+
* The CURSOR is cardds's: a sheet that opens takes it in the field marked `data-autofocus`, else the first field, else the heading — write no `autoFocus` or `.focus()` for it.
|
|
58
59
|
*/
|
|
59
60
|
export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChange, onRefresh, refreshing = null, mark = 'dots', fanned, closed, rise = 'under', base, className, children, onClickCapture, onPointerDownCapture, ref: passed, ...rest }) {
|
|
60
61
|
const [current, request] = useControlled(open, defaultOpen, onOpenChange);
|
|
@@ -40,7 +40,7 @@ export interface SheetStepProps extends Omit<ComponentProps<'li'>, 'title'> {
|
|
|
40
40
|
summary?: ReactNode;
|
|
41
41
|
/** the member may pass this step without doing it: a small link at its end moves on (reason `skip`) */
|
|
42
42
|
skippable?: boolean;
|
|
43
|
-
/** the skip link's words (
|
|
43
|
+
/** the skip link's words — cardds names it in the page's language (`<html lang>`); pass it only for different words */
|
|
44
44
|
skipLabel?: ReactNode;
|
|
45
45
|
/** what went wrong, in words — one line UNDER the step's button (an alert icon, `--signal-error`, read out at once): the number
|
|
46
46
|
* is not a phone number, no signal, the code could not be sent, the two PINs differ. The step stays where it is; to send the
|
|
@@ -50,5 +50,5 @@ export interface SheetStepProps extends Omit<ComponentProps<'li'>, 'title'> {
|
|
|
50
50
|
* step's buttons take no taps meanwhile. The app moves `at` when the answer is good, or sets `error`. */
|
|
51
51
|
busy?: ReactNode;
|
|
52
52
|
}
|
|
53
|
-
/** SheetStep — one step of a SheetSteps: its title, then what the member does (a Field, a Pin, a Toggle, the step's own Btn). */
|
|
53
|
+
/** SheetStep — one step of a SheetSteps: its title, then what the member does (a Field, a Pin, a Toggle, the step's own Btn). When it opens, cardds puts the cursor in the field marked `data-autofocus`, else the first field, else the title — write no focus code. */
|
|
54
54
|
export declare function SheetStep({ title, summary, skippable, skipLabel, error, busy, className, children, ...rest }: SheetStepProps): import("react").JSX.Element;
|
|
@@ -201,7 +201,7 @@ function useStepChecks(off, park) {
|
|
|
201
201
|
};
|
|
202
202
|
return { note, group, press };
|
|
203
203
|
}
|
|
204
|
-
/** SheetStep — one step of a SheetSteps: its title, then what the member does (a Field, a Pin, a Toggle, the step's own Btn). */
|
|
204
|
+
/** SheetStep — one step of a SheetSteps: its title, then what the member does (a Field, a Pin, a Toggle, the step's own Btn). When it opens, cardds puts the cursor in the field marked `data-autofocus`, else the first field, else the title — write no focus code. */
|
|
205
205
|
export function SheetStep({ title, summary, skippable, skipLabel = word('skip'), error, busy, className, children, ...rest }) {
|
|
206
206
|
const ctx = useContext(StepCtx);
|
|
207
207
|
if (!ctx)
|
|
@@ -29,7 +29,7 @@ export interface StackSheetProps extends Omit<ComponentProps<'article'>, 'title'
|
|
|
29
29
|
onTabChange?: (tab: string) => void;
|
|
30
30
|
/** the SHEET FOOT: the CTA at the sheet's bottom (one row) */
|
|
31
31
|
foot?: ReactNode;
|
|
32
|
-
/** the back button's label (shown once the sheet is open) */
|
|
32
|
+
/** the back button's label (shown once the sheet is open) — cardds names it in the page's language (`<html lang>`); pass it only for different words */
|
|
33
33
|
backLabel?: string;
|
|
34
34
|
/** where the back button stands: `none` (default — hidden for now, Lh 2026-09-29: not used in real work yet; a tap on the open sheet's title folds it) ·
|
|
35
35
|
* `end` (after the tabs, chevron DOWN: the sheet folds down) · `start` (before the title, chevron left) */
|
|
@@ -42,6 +42,6 @@ export interface StackSheetProps extends Omit<ComponentProps<'article'>, 'title'
|
|
|
42
42
|
* SHEET BODY (the children; scrolls) · SHEET FOOT (`foot`, the CTA) at the bottom. Its tabs are icon buttons. The chosen one is filled
|
|
43
43
|
* only while this sheet is open — a closed home shows every icon plain; the disc fades in with the rise and out with the fold.
|
|
44
44
|
* A count shows as a small circle over its icon. The body is the app's: render the page of the chosen tab (and nothing while the sheet
|
|
45
|
-
* is closed — see `Home`). A direct child of `SheetStack`.
|
|
45
|
+
* is closed — see `Home`). A direct child of `SheetStack`. Once open, cardds puts the cursor in the field marked `data-autofocus`, else the first field, else the heading — write no focus code.
|
|
46
46
|
*/
|
|
47
47
|
export declare function StackSheet({ title, tone, paper, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel, backAt, stack, className, children, ...rest }: StackSheetProps): import("react").JSX.Element;
|
|
@@ -17,7 +17,7 @@ import { word } from '../words.js';
|
|
|
17
17
|
* SHEET BODY (the children; scrolls) · SHEET FOOT (`foot`, the CTA) at the bottom. Its tabs are icon buttons. The chosen one is filled
|
|
18
18
|
* only while this sheet is open — a closed home shows every icon plain; the disc fades in with the rise and out with the fold.
|
|
19
19
|
* A count shows as a small circle over its icon. The body is the app's: render the page of the chosen tab (and nothing while the sheet
|
|
20
|
-
* is closed — see `Home`). A direct child of `SheetStack`.
|
|
20
|
+
* is closed — see `Home`). A direct child of `SheetStack`. Once open, cardds puts the cursor in the field marked `data-autofocus`, else the first field, else the heading — write no focus code.
|
|
21
21
|
*/
|
|
22
22
|
export function StackSheet({ title, tone, paper, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel = word('back'), backAt = 'none', stack, className, children, ...rest }) {
|
|
23
23
|
const [tab, pick] = useControlled(tabProp, defaultTab ?? tabs?.[0]?.id, onTabChange);
|
|
@@ -10,7 +10,7 @@ export interface CallSheetSheet {
|
|
|
10
10
|
foot?: ReactNode;
|
|
11
11
|
/** a small link UNDER that button (a `Link sm`, e.g. details for developers) */
|
|
12
12
|
footLink?: ReactNode;
|
|
13
|
-
/** this sheet's handle
|
|
13
|
+
/** this sheet's handle: `false` for none, a string only for words different from cardds's own (named in the page's language, `<html lang>`); overrides the CallSheet's `handle` */
|
|
14
14
|
handle?: SheetProps['handle'];
|
|
15
15
|
}
|
|
16
16
|
/** one way in: its button and the sheet it calls */
|
|
@@ -45,7 +45,7 @@ export interface CallSheetProps extends Omit<ComponentProps<typeof Screen>, 'chi
|
|
|
45
45
|
onOpenChange?: (open: boolean, way: number | null) => void;
|
|
46
46
|
/** the sheets' resting states once up (default "peek full") */
|
|
47
47
|
states?: string;
|
|
48
|
-
/** every sheet's handle:
|
|
48
|
+
/** every sheet's handle: `false` for none; cardds names it in the page's language (`<html lang>`), so pass a string only for different words. A sheet's own `handle` wins */
|
|
49
49
|
handle?: SheetProps['handle'];
|
|
50
50
|
/** layers over the screen */
|
|
51
51
|
children?: ReactNode;
|
package/dist/templates/Home.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ export interface HomeProps extends Omit<ComponentProps<typeof Screen>, 'children
|
|
|
46
46
|
base?: ReactNode;
|
|
47
47
|
/** keep a sheet's body mounted after it closes (it opens again at once, scrolled where it was); default: dropped */
|
|
48
48
|
keep?: boolean;
|
|
49
|
-
/** the open sheet's back button label */
|
|
49
|
+
/** the open sheet's back button label — cardds names it in the page's language (`<html lang>`); pass it only for different words */
|
|
50
50
|
backLabel?: string;
|
|
51
51
|
/** the open sheet's back button — see `StackSheet backAt` (default `none`: hidden for now) */
|
|
52
52
|
backAt?: StackSheetProps['backAt'];
|
|
@@ -32,9 +32,9 @@ export interface WalkthroughProps extends Omit<ComponentProps<typeof Screen>, 'c
|
|
|
32
32
|
ctaLink?: ReactNode;
|
|
33
33
|
/** layers over the screen: the app's `SheetStage dim` + `Modal` that a link or button opens (the app wires it) */
|
|
34
34
|
children?: ReactNode;
|
|
35
|
-
/** the pager's back button name
|
|
35
|
+
/** the pager's back button name — cardds names it in the page's language (`<html lang>`); pass it only for different words */
|
|
36
36
|
backLabel?: string;
|
|
37
|
-
/** the pager's next button name
|
|
37
|
+
/** the pager's next button name — cardds names it in the page's language (`<html lang>`); pass it only for different words */
|
|
38
38
|
nextLabel?: string;
|
|
39
39
|
}
|
|
40
40
|
/**
|
package/lib/catalog.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
5
5
|
import { execFileSync } from 'node:child_process';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { checkSource } from '../lint/rules.mjs';
|
|
8
|
+
import { checkSource, tagAt, CONTROL_NAMES } from '../lint/rules.mjs';
|
|
9
9
|
import { SHAPES, KINDS, shapeOf, interactOf } from './role.mjs';
|
|
10
10
|
|
|
11
11
|
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -167,6 +167,25 @@ export function find(cat, query, axes = {}) {
|
|
|
167
167
|
// the parts that put themselves where they belong by JS at runtime — over the content, open, at the edge: the static html shows them in the wrong place
|
|
168
168
|
const PLACED_BY_JS = ['Sheet', 'SheetStack', 'SheetSteps', 'SheetStage', 'Step', 'Modal', 'Menu', 'Dropdown', 'Snackbar', 'FloatBar'];
|
|
169
169
|
|
|
170
|
+
/** a story names cardds's own controls by hand where the gallery's page is not in the story's language (`backLabel="กลับ"` on an
|
|
171
|
+
* lang="en" page). In an app that is noise — and a session copies what it is handed (two prototypes did, 2026-10-08): the starting file
|
|
172
|
+
* leaves those props, and the note above them, out. The tags are read by the name the import gives them (`Home as HomeScreen`). */
|
|
173
|
+
function ownWords(tsx, imports) {
|
|
174
|
+
let out = tsx;
|
|
175
|
+
for (const [name, props] of Object.entries(CONTROL_NAMES)) {
|
|
176
|
+
const tags = [name, ...[...imports.matchAll(new RegExp(`\\b${name}\\s+as\\s+(\\w+)`, 'g'))].map((m) => m[1])];
|
|
177
|
+
for (const tag of tags) {
|
|
178
|
+
for (let at = out.indexOf(`<${tag}`); at >= 0; at = out.indexOf(`<${tag}`, at + 1)) {
|
|
179
|
+
if (/[\w.]/.test(out[at + tag.length + 1] ?? '')) continue;
|
|
180
|
+
const was = tagAt(out, at);
|
|
181
|
+
const now = props.reduce((t, prop) => t.replace(new RegExp(`(\\n[ \\t]*|[ \\t]+)${prop}\\s*=\\s*(?:"[^"]*"|'[^']*'|\\{\\s*(?:"[^"]*"|'[^']*')\\s*\\})`, 'g'), ''), was);
|
|
182
|
+
if (now !== was) out = out.slice(0, at) + now + out.slice(at + was.length);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return out.replace(/^[ \t]*(?:\/\/|\/\*|\{\/\*)[^\n]*cardds-ignore-next-line[^\n]*\n/gm, '');
|
|
187
|
+
}
|
|
188
|
+
|
|
170
189
|
/** no id: the screens already drawn. With one: a file to start from — `tsx` (the code, a string) or `html` (the look, static: `{ html, warning? }`, the warning when the picture lies) */
|
|
171
190
|
export function scaffold(cat, id, { format = 'tsx', css = CSS } = {}) {
|
|
172
191
|
if (!id) return cat.stories.filter((s) => s.screen).map((s) => ({ id: s.id, summary: sentence(s.description) }));
|
|
@@ -196,7 +215,7 @@ ${s.html}
|
|
|
196
215
|
const taken = /\bScreen\b/.test(s.imports) || /^(?:function|const|class|type|interface) Screen\b/m.test(s.tsx);
|
|
197
216
|
const as = taken ? 'MyScreen' : 'Screen';
|
|
198
217
|
const note = glue.length ? `// story glue from ./_*, not part of the package — replace or drop: ${glue.join(', ')}\n` : '';
|
|
199
|
-
return `${note}${s.imports}\n\n${s.tsx.replace(new RegExp(`^function ${s.name}\\b`, 'm'), `export default function ${as}`)}\n`;
|
|
218
|
+
return `${note}${s.imports}\n\n${ownWords(s.tsx, s.imports).replace(new RegExp(`^function ${s.name}\\b`, 'm'), `export default function ${as}`)}\n`;
|
|
200
219
|
}
|
|
201
220
|
|
|
202
221
|
/** the rules of lint/rules.mjs over a piece of code */
|
package/lib/guide.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// lib/guide.mjs — the few rules of using cardds that no checker can see, for an AI session: ONE text, two doors —
|
|
2
|
+
// `npx cardds guide` (bin/cardds.mjs) prints it, the MCP server (lib/mcp.mjs) hands it over as its `instructions` at start.
|
|
3
|
+
// Short on purpose: an MCP client may truncate server instructions, so the trigger and the order come first. A component's own rules are
|
|
4
|
+
// not here — they are in its JSDoc (get_component) and in lint/rules.mjs (check).
|
|
5
|
+
export const GUIDE = `cardds — BEFORE drawing any phone-width / mobile screen in a project with @usableapp/cardds, or that Lh asks to build "with cardds": load these tools (ToolSearch "cardds" if they are deferred) and call find_components. This holds when the request only says prototype, poc, mock-up, sketch, "show me a design", หน้า, หน้าจอ, ขึ้นหน้า, ขอดู prototype — a screen, card, bottom sheet, sheet stack, form step, sticky note / post-it, chat of notes, stamp, postcard, home. Never hand-write HTML, CSS or emoji for what it has.
|
|
6
|
+
ORDER: find → scaffold or get → write → check. First question: has cardds drawn this screen already? find BOTH ways: by the axes (represents · shape · interact) and by the English noun of the task ("sticky", "stamp", "chat").
|
|
7
|
+
TOOLS: find_components · scaffold_screen · get_component · list_components · check_source. No MCP: npx cardds find|scaffold|get|list|check (all take --json).
|
|
8
|
+
What no checker sees:
|
|
9
|
+
1. cardds is a framework you take from outside. Never edit, commit to or release cardds from a consumer project, never node_modules. What cannot be set from outside is a framework gap: STOP and tell the user exactly what you needed.
|
|
10
|
+
2. Adjust from outside only, widest first: theme.css bases → knobs (--<component>-<what>) → props/slots. The theme's values are the user's call; never pick them.
|
|
11
|
+
3. Every component is controlled like an input (state/open/value + its on…, or default…) and none knows another: the app owns the state and wires them.
|
|
12
|
+
4. Import components, never copy their markup or classes: a component is the look AND the behaviour.
|
|
13
|
+
5. Mobile only, big targets; never adapt it to desktop.
|
|
14
|
+
A prototype in HTML (scaffold --html) is a still picture to look at, never a screen; when it warns, run the tsx. check is for tsx only.
|
|
15
|
+
Everything else (a component's own rules) is in get_component and enforced by check.
|
|
16
|
+
`;
|
package/lib/mcp.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { readFileSync } from 'node:fs';
|
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import * as catalog from './catalog.mjs';
|
|
8
|
+
import { GUIDE } from './guide.mjs';
|
|
8
9
|
|
|
9
10
|
const { version } = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'));
|
|
10
11
|
const str = (description) => ({ type: 'string', description });
|
|
@@ -83,7 +84,7 @@ export function serve({ root, input = process.stdin, output = process.stdout } =
|
|
|
83
84
|
function handle(msg) {
|
|
84
85
|
const { id, method, params } = msg;
|
|
85
86
|
switch (method) {
|
|
86
|
-
case 'initialize': return reply(id, { protocolVersion: params?.protocolVersion ?? '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: 'cardds', version } });
|
|
87
|
+
case 'initialize': return reply(id, { protocolVersion: params?.protocolVersion ?? '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: 'cardds', version }, instructions: GUIDE }); // the guide: what no checker sees, handed over before the first question
|
|
87
88
|
case 'ping': return reply(id, {});
|
|
88
89
|
case 'tools/list': return reply(id, { tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description: typeof description === 'function' ? description(loaded()) : description, inputSchema })) });
|
|
89
90
|
case 'tools/call': { const r = call(params); return r ? reply(id, r) : fail(id, -32602, `unknown tool: ${params?.name}`); }
|
package/lint/eslint.mjs
CHANGED
|
@@ -18,8 +18,8 @@ const rule = (level, description) => ({
|
|
|
18
18
|
};
|
|
19
19
|
},
|
|
20
20
|
});
|
|
21
|
-
const check = rule('error', 'cardds used through its components: no hand-written cardds class
|
|
22
|
-
const advice = rule('warn', 'cardds behaviour kept: a Sheet that can move and slides out; no bar in front of a sheet');
|
|
21
|
+
const check = rule('error', 'cardds used through its components: no hand-written cardds class, no native <select>; the bar is the Screen\'s bottom');
|
|
22
|
+
const advice = rule('warn', 'cardds behaviour kept: a Sheet that can move and slides out; no bar in front of a sheet; the cursor, the control names and the look are cardds\'s and the theme\'s, not the element\'s');
|
|
23
23
|
|
|
24
24
|
const plugin = { meta: { name: '@usableapp/cardds/eslint' }, rules: { check, advice } };
|
|
25
25
|
plugin.configs = {
|
package/lint/rules.mjs
CHANGED
|
@@ -9,6 +9,12 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
|
|
10
10
|
const CSS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'css');
|
|
11
11
|
|
|
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
|
+
export const CONTROL_NAMES = {
|
|
14
|
+
Sheet: ['handle'], CallSheet: ['handle'], StackSheet: ['backLabel'], Home: ['backLabel'], Walkthrough: ['backLabel', 'nextLabel'],
|
|
15
|
+
Calendar: ['prevLabel', 'nextLabel'], SheetStep: ['skipLabel'], PostLetter: ['backLabel', 'pictureLabel', 'playLabel', 'pauseLabel', 'toLabel'],
|
|
16
|
+
};
|
|
17
|
+
|
|
12
18
|
const lineOf = (text, idx) => text.slice(0, idx).split('\n').length;
|
|
13
19
|
|
|
14
20
|
/** every class cardds's CSS styles — read from the package's own css/, so the list is never copied by hand */
|
|
@@ -29,7 +35,7 @@ export function carddsClasses(dir = CSS_DIR) {
|
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
/** the whole of a JSX opening tag from `<Name` — attributes may hold `=>` and nested braces, so a `[^>]*` regex is not enough */
|
|
32
|
-
function tagAt(text, start) {
|
|
38
|
+
export function tagAt(text, start) {
|
|
33
39
|
let depth = 0, quote = null;
|
|
34
40
|
for (let i = start + 1; i < text.length; i++) {
|
|
35
41
|
const c = text[i];
|
|
@@ -83,6 +89,23 @@ function classWords(text, at) {
|
|
|
83
89
|
return words;
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
/** a style object's or attribute's `key: value` entries from its body (`at` = where the body starts in the text): split on the commas / semicolons that are not inside a string, bracket or call */
|
|
93
|
+
function* styleEntries(body, at, sep) {
|
|
94
|
+
let depth = 0, quote = null, from = 0;
|
|
95
|
+
for (let i = 0; i <= body.length; i++) {
|
|
96
|
+
const c = body[i];
|
|
97
|
+
if (quote) { if (c === quote && body[i - 1] !== '\\') quote = null; continue; }
|
|
98
|
+
if (c === '"' || c === "'" || c === '`') quote = c;
|
|
99
|
+
else if (c === '(' || c === '[' || c === '{') depth++;
|
|
100
|
+
else if (c === ')' || c === ']' || c === '}') depth--;
|
|
101
|
+
else if ((c === sep && depth === 0) || i === body.length) {
|
|
102
|
+
const m = body.slice(from, i).match(/^\s*(?:'([^']+)'|"([^"]+)"|([\w$-]+))\s*:\s*([\s\S]*)$/);
|
|
103
|
+
if (m) yield { key: m[1] ?? m[2] ?? m[3], value: m[4], at: at + from + m[0].length - m[4].length };
|
|
104
|
+
from = i + 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
86
109
|
/**
|
|
87
110
|
* @param {string} text a source file
|
|
88
111
|
* @param {string} file its name, for the findings
|
|
@@ -91,7 +114,12 @@ function classWords(text, at) {
|
|
|
91
114
|
export function checkSource(source, file, { classes = carddsClasses() } = {}) {
|
|
92
115
|
const findings = [];
|
|
93
116
|
const lines = source.split('\n');
|
|
94
|
-
|
|
117
|
+
let text = source.replace(/\/\*[\s\S]*?\*\/|(?<![:'"`\\])\/\/[^\n]*/g, (c) => c.replace(/[^\n]/g, ' ')); // comments out, lines kept
|
|
118
|
+
// a component imported under another name (`Home as HomeScreen`) is still that component: its tags read by the name the rules know. Lines kept
|
|
119
|
+
// (seen 2026-10-08: a prototype's `<HomeScreen backLabel="กลับ">` passed control-name-repeat)
|
|
120
|
+
for (const imp of text.matchAll(/import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*['"][^'"]*cardds[^'"]*['"]/g)) {
|
|
121
|
+
for (const [, name, alias] of imp[1].matchAll(/\b([A-Z]\w*)\s+as\s+([A-Z]\w*)/g)) text = text.replace(new RegExp(`(</?)${alias}\\b`, 'g'), `$1${name}`);
|
|
122
|
+
}
|
|
95
123
|
const add = (rule, level, idx, message) => {
|
|
96
124
|
const line = lineOf(text, idx);
|
|
97
125
|
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)
|
|
@@ -131,5 +159,49 @@ export function checkSource(source, file, { classes = carddsClasses() } = {}) {
|
|
|
131
159
|
if (/<Sheet\b/.test(screen) && /<ActionBar\b/.test(screen)) add('screen.bar-with-sheet', 'warn', m.index, 'a Sheet and an ActionBar on one Screen — a screen with a sheet finishes on the sheet\'s CardFoot, no bar.');
|
|
132
160
|
}
|
|
133
161
|
|
|
162
|
+
// 4 · focus: cardds places the cursor when these parts come up — an `autoFocus` or a `.focus(` beside them fights it
|
|
163
|
+
if (/<(?:Sheet|SheetStep|SheetSteps|SheetStack|StackSheet|Home|Modal|CallSheet)\b/.test(text)) {
|
|
164
|
+
const msg = (what) => `${what} in a file that uses a Sheet, SheetStep, SheetStack or Modal: cardds already places the cursor when one comes up (the field marked data-autofocus, else the first field, else the heading) — remove it. To choose the field put data-autofocus on it; focus about something else (the field a server error points at) is the app's: then // cardds-ignore-next-line with the reason.`;
|
|
165
|
+
for (const m of text.matchAll(/(?<![\w)\]])<[A-Za-z][\w.]*/g)) {
|
|
166
|
+
const hit = tagAt(text, m.index).match(/\sautoFocus\b(?!\s*:)/);
|
|
167
|
+
if (hit) add('focus-by-hand', 'warn', m.index + hit.index + 1, msg('autoFocus'));
|
|
168
|
+
}
|
|
169
|
+
for (const m of text.matchAll(/\.focus\s*\(/g)) add('focus-by-hand', 'warn', m.index, msg('.focus()'));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 5 · control names: cardds names its controls in the page's language — a literal that only repeats them is noise (and English-only)
|
|
173
|
+
for (const [name, props] of Object.entries(CONTROL_NAMES)) {
|
|
174
|
+
for (const m of text.matchAll(new RegExp(`<${name}\\b`, 'g'))) {
|
|
175
|
+
const tag = tagAt(text, m.index);
|
|
176
|
+
for (const prop of props) {
|
|
177
|
+
const hit = tag.match(new RegExp(`\\s${prop}\\s*=\\s*(?:"[^"]*"|'[^']*'|\\{\\s*(?:"[^"]*"|'[^']*'|\`[^\`$]*\`)\\s*\\})`));
|
|
178
|
+
if (hit) add('control-name-repeat', 'warn', m.index + hit.index + 1, `${prop}="…" on <${name}>: cardds names this control itself, in the page's language (<html lang>) — drop it unless the words differ from cardds's. Another language is setWords(lang, {…}) once at app start, not a string on each component.`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 6 · a native <select> opens the platform's picker: not cardds's look, not its words — Dropdown is the one component for it
|
|
184
|
+
for (const m of text.matchAll(/<select\b/g)) add('native-select', 'error', m.index, '<select> is the platform\'s own picker: it draws outside cardds\'s look and takes none of its knobs — use <Dropdown> (one component, value controlled, the pill named by the chosen item).');
|
|
185
|
+
|
|
186
|
+
// 7 · a raw colour or px length in a style prop: the look is the theme's (theme.css · a knob · a prop); a custom property (`'--x': …`) IS a knob, so its value is the app's to set
|
|
187
|
+
if (/\.[jt]sx$/.test(file)) {
|
|
188
|
+
const raw = (value) => {
|
|
189
|
+
const v = value.replace(/var\((?:[^()]|\([^()]*\))*\)/g, '');
|
|
190
|
+
return /#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?)\(/.test(v) ? 'raw colour' : /(?<![\w.])\d*\.?\d+px\b/.test(v) ? 'raw px length' : null;
|
|
191
|
+
};
|
|
192
|
+
const report = (e) => {
|
|
193
|
+
const what = !e.key.startsWith('--') && raw(e.value);
|
|
194
|
+
if (what) add('raw-value', 'warn', e.at, `${what} in style (${e.key}): the look is set in the project's theme.css, by a knob (--<component>-<what>) or by a prop — not on the element. A hairline is var(--border-w), even 1px.`);
|
|
195
|
+
};
|
|
196
|
+
for (const m of text.matchAll(/\bstyle\s*=\s*\{\{/g)) {
|
|
197
|
+
const open = m.index + m[0].length - 2, expr = braceAt(text, open);
|
|
198
|
+
if (expr.endsWith('}}')) for (const e of styleEntries(expr.slice(2, -2), open + 2, ',')) report(e);
|
|
199
|
+
}
|
|
200
|
+
for (const m of text.matchAll(/\bstyle\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
|
|
201
|
+
const body = m[1] ?? m[2];
|
|
202
|
+
for (const e of styleEntries(body, m.index + m[0].indexOf(body), ';')) report(e);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
134
206
|
return findings.sort((a, b) => a.line - b.line);
|
|
135
207
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.74",
|
|
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.",
|