@usableapp/cardds 0.7.73 → 0.7.75

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +13 -10
  3. package/bin/cardds.mjs +4 -2
  4. package/css/layover.css +5 -1
  5. package/css/lists.css +1 -1
  6. package/dist/actions/{ActionNote.d.ts → BarNote.d.ts} +3 -3
  7. package/dist/actions/{ActionNote.js → BarNote.js} +2 -2
  8. package/dist/actions/BtnGroup.d.ts +11 -0
  9. package/dist/actions/{Segment.js → BtnGroup.js} +4 -4
  10. package/dist/actions/{WaitLink.d.ts → Countdown.d.ts} +3 -3
  11. package/dist/actions/{WaitLink.js → Countdown.js} +2 -2
  12. package/dist/actions/Link.d.ts +2 -2
  13. package/dist/cardds.css +6 -2
  14. package/dist/cards/Card.d.ts +1 -1
  15. package/dist/cards/CardParts.d.ts +1 -1
  16. package/dist/cards/CardParts.js +1 -1
  17. package/dist/catalog.json +1 -1
  18. package/dist/choice/Calendar.d.ts +2 -2
  19. package/dist/index.d.ts +7 -7
  20. package/dist/index.js +7 -7
  21. package/dist/journey/{TileBadge.d.ts → JourneyBadge.d.ts} +3 -3
  22. package/dist/journey/{TileBadge.js → JourneyBadge.js} +2 -2
  23. package/dist/numbers/NumberBand.d.ts +18 -0
  24. package/dist/numbers/{Band.js → NumberBand.js} +4 -4
  25. package/dist/post/PostLetter.d.ts +5 -5
  26. package/dist/scaffold/ActionBar.d.ts +2 -2
  27. package/dist/scaffold/ActionBar.js +1 -1
  28. package/dist/scaffold/Centre.d.ts +3 -3
  29. package/dist/scaffold/Centre.js +3 -3
  30. package/dist/scaffold/Page.d.ts +1 -1
  31. package/dist/scaffold/Page.js +1 -1
  32. package/dist/scaffold/Pager.d.ts +11 -0
  33. package/dist/scaffold/Pager.js +6 -0
  34. package/dist/scaffold/Panes.d.ts +2 -2
  35. package/dist/scaffold/Panes.js +3 -3
  36. package/dist/scaffold/Screen.d.ts +1 -1
  37. package/dist/scaffold/Screen.js +1 -1
  38. package/dist/sheets/{Drawer.d.ts → Modal.d.ts} +13 -13
  39. package/dist/sheets/{Drawer.js → Modal.js} +10 -10
  40. package/dist/sheets/Sheet.d.ts +2 -1
  41. package/dist/sheets/Sheet.js +1 -0
  42. package/dist/sheets/SheetStack.d.ts +1 -0
  43. package/dist/sheets/SheetStack.js +1 -0
  44. package/dist/sheets/SheetSteps.d.ts +2 -2
  45. package/dist/sheets/SheetSteps.js +1 -1
  46. package/dist/sheets/StackSheet.d.ts +2 -2
  47. package/dist/sheets/StackSheet.js +1 -1
  48. package/dist/templates/CallSheet.d.ts +2 -2
  49. package/dist/templates/Home.d.ts +1 -1
  50. package/dist/templates/Walkthrough.d.ts +4 -4
  51. package/dist/templates/Walkthrough.js +2 -2
  52. package/lib/catalog.mjs +21 -2
  53. package/lib/guide.mjs +16 -0
  54. package/lib/mcp.mjs +3 -2
  55. package/lint/eslint.mjs +2 -2
  56. package/lint/rules.mjs +75 -2
  57. package/package.json +1 -1
  58. package/dist/actions/Segment.d.ts +0 -11
  59. package/dist/numbers/Band.d.ts +0 -18
  60. package/dist/scaffold/PagerAt.d.ts +0 -11
  61. package/dist/scaffold/PagerAt.js +0 -6
@@ -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'];
@@ -23,8 +23,8 @@ export interface WalkthroughProps extends Omit<ComponentProps<typeof Screen>, 'c
23
23
  defaultAt?: number;
24
24
  /** a page was turned to: its index */
25
25
  onAtChange?: (at: number) => void;
26
- /** the ONE row of the bottom panel. Omitted: the round pager (back · next). A function gets `{ at, count, go }`,
27
- * e.g. the pager until the last page, then one wide Btn. */
26
+ /** the ONE row of the bottom panel. Omitted: the round pager bar (back · next). A function gets `{ at, count, go }`,
27
+ * e.g. the pager bar until the last page, then one wide Btn. */
28
28
  cta?: ReactNode | ((c: WalkthroughCta) => ReactNode);
29
29
  /** rows of controls ABOVE the CTA row (a text-size Slider…), one row each, top to bottom — keep them few */
30
30
  controls?: ReactNode[];
@@ -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 (default "Back", by `<html lang>`) */
35
+ /** the pager bar'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 (default "Next", by `<html lang>`) */
37
+ /** the pager bar'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
  /**
@@ -5,7 +5,7 @@ import { Screen } from '../scaffold/Screen.js';
5
5
  import { Step } from '../sheets/Step.js';
6
6
  import { Page } from '../scaffold/Page.js';
7
7
  import { Pages, slideTo } from '../scaffold/Centre.js';
8
- import { PagerAt } from '../scaffold/PagerAt.js';
8
+ import { Pager } from '../scaffold/Pager.js';
9
9
  import { ActionBar, ActionBarTier } from '../scaffold/ActionBar.js';
10
10
  import { CardHead } from '../cards/CardHead.js';
11
11
  import { Text } from '../type/Text.js';
@@ -50,7 +50,7 @@ export function Walkthrough({ pages, at: atProp, defaultAt = 0, onAtChange, cta,
50
50
  const row = typeof cta === 'function' ? cta({ at, count, go }) : cta;
51
51
  const custom = row !== undefined && row !== null;
52
52
  const pager = (_jsxs(_Fragment, { children: [_jsx(IconBtn, { icon: "arrow-left", label: backLabel, disabled: at === 0, onClick: () => go(at - 1) }), _jsx(IconBtn, { invert: true, icon: "arrow-right", label: nextLabel, disabled: at === count - 1, onClick: () => go(at + 1) })] }));
53
- const dots = count > 1 ? _jsx(PagerAt, { at: at + 1, of: count }) : null;
53
+ const dots = count > 1 ? _jsx(Pager, { at: at + 1, of: count }) : null;
54
54
  const bottom = dots || controls.length || ctaLink != null ? (_jsxs(ActionBar, { variant: "tiers", children: [dots, controls.map((c, i) => _jsx(ActionBarTier, { children: c }, i)), _jsx(ActionBarTier, { pager: true, children: custom ? row : pager }), ctaLink != null && _jsx(ActionBarTier, { className: "action-bar__tier--link", children: ctaLink })] })) : (_jsx(ActionBar, { variant: "pager", children: custom ? row : pager }));
55
55
  return (_jsxs(Screen, { fill: true, bottom: bottom, ...rest, children: [_jsx(Step, { ref: step, children: _jsx(Pages, { ...swipe, children: pages.map((p, i) => (_jsxs(Page, { "aria-current": i === at ? 'step' : undefined, inert: i !== at || undefined, children: [p.media, _jsx(CardHead, { level: "h1", heading: p.heading }), typeof p.body === 'string' ? _jsx(Text, { level: "body", children: p.body }) : p.body] }, i))) }) }), children] }));
56
56
  }
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 });
@@ -13,7 +14,7 @@ const bool = (description) => ({ type: 'boolean', description });
13
14
  const TOOLS = [
14
15
  {
15
16
  name: 'list_components',
16
- description: 'The names of every cardds component and what each is FOR, nothing else. represents is what it stands for (person, group, time…) or, for a control that stands for no one thing, the KIND of data it carries (words, number, picture, signal); the word in parentheses after a name in the text is its represents. Every component shows information; shape says how much: "one" (one thing), "group-brief" (a group read at a glance or in a line), "group-detail" (a group read in detail), "—" (a helper function or structure, not something drawn). interact says whether the member can do something with it: false (it only shows or holds), or the kinds, any of pick, act, write, navigate — alone when shape is one (a Btn acts), as a group when shape is a group (a Segment picks among several). Two kinds mean it does both. group is the src folder, not what it is for. Call this once, first, in a session that will build a UI with cardds, to learn what exists; then get_component for the ones you use.',
17
+ description: 'The names of every cardds component and what each is FOR, nothing else. represents is what it stands for (person, group, time…) or, for a control that stands for no one thing, the KIND of data it carries (words, number, picture, signal); the word in parentheses after a name in the text is its represents. Every component shows information; shape says how much: "one" (one thing), "group-brief" (a group read at a glance or in a line), "group-detail" (a group read in detail), "—" (a helper function or structure, not something drawn). interact says whether the member can do something with it: false (it only shows or holds), or the kinds, any of pick, act, write, navigate — alone when shape is one (a Btn acts), as a group when shape is a group (a BtnGroup picks among several). Two kinds mean it does both. group is the src folder, not what it is for. Call this once, first, in a session that will build a UI with cardds, to learn what exists; then get_component for the ones you use.',
17
18
  inputSchema: { type: 'object', properties: {} },
18
19
  run: (cat) => catalog.list(cat),
19
20
  },
@@ -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; 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');
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,12 +114,18 @@ 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
- const text = source.replace(/\/\*[\s\S]*?\*\/|(?<![:'"`\\])\/\/[^\n]*/g, (c) => c.replace(/[^\n]/g, ' ')); // comments out, lines kept
117
+ let text = source.replace(/\/\*[\s\S]*?\*\/|(?<![:'"`\\])\/\/[^\n]*/g, (c) => c.replace(/[^\n]/g, ' ')); // comments out, lines kept
95
118
  const add = (rule, level, idx, message) => {
96
119
  const line = lineOf(text, idx);
97
120
  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)
98
121
  };
99
122
 
123
+ // a component imported under another name (`Home as HomeScreen`) is still that component: its tags read by the name the rules know. Lines kept
124
+ // (seen 2026-10-08: a prototype's `<HomeScreen backLabel="กลับ">` passed control-name-repeat)
125
+ for (const imp of text.matchAll(/import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*['"][^'"]*cardds[^'"]*['"]/g)) {
126
+ 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}`);
127
+ }
128
+
100
129
  // 1 · a cardds class written by hand: the look comes without the behaviour — import the component
101
130
  for (const m of text.matchAll(/\bclass(?:Name)?(?=\s*=)/g)) {
102
131
  const hit = [...new Set(classWords(text, m.index + m[0].length).filter((w) => classes.has(w)))];
@@ -131,5 +160,49 @@ export function checkSource(source, file, { classes = carddsClasses() } = {}) {
131
160
  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
161
  }
133
162
 
163
+ // 4 · focus: cardds places the cursor when these parts come up — an `autoFocus` or a `.focus(` beside them fights it
164
+ if (/<(?:Sheet|SheetStep|SheetSteps|SheetStack|StackSheet|Home|Modal|CallSheet)\b/.test(text)) {
165
+ 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.`;
166
+ for (const m of text.matchAll(/(?<![\w)\]])<[A-Za-z][\w.]*/g)) {
167
+ const hit = tagAt(text, m.index).match(/\sautoFocus\b(?!\s*:)/);
168
+ if (hit) add('focus-by-hand', 'warn', m.index + hit.index + 1, msg('autoFocus'));
169
+ }
170
+ for (const m of text.matchAll(/\.focus\s*\(/g)) add('focus-by-hand', 'warn', m.index, msg('.focus()'));
171
+ }
172
+
173
+ // 5 · control names: cardds names its controls in the page's language — a literal that only repeats them is noise (and English-only)
174
+ for (const [name, props] of Object.entries(CONTROL_NAMES)) {
175
+ for (const m of text.matchAll(new RegExp(`<${name}\\b`, 'g'))) {
176
+ const tag = tagAt(text, m.index);
177
+ for (const prop of props) {
178
+ const hit = tag.match(new RegExp(`\\s${prop}\\s*=\\s*(?:"[^"]*"|'[^']*'|\\{\\s*(?:"[^"]*"|'[^']*'|\`[^\`$]*\`)\\s*\\})`));
179
+ 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.`);
180
+ }
181
+ }
182
+ }
183
+
184
+ // 6 · a native <select> opens the platform's picker: not cardds's look, not its words — Dropdown is the one component for it
185
+ 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).');
186
+
187
+ // 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
188
+ if (/\.[jt]sx$/.test(file)) {
189
+ const raw = (value) => {
190
+ const v = value.replace(/var\((?:[^()]|\([^()]*\))*\)/g, '');
191
+ 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;
192
+ };
193
+ const report = (e) => {
194
+ const what = !e.key.startsWith('--') && raw(e.value);
195
+ 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.`);
196
+ };
197
+ for (const m of text.matchAll(/\bstyle\s*=\s*\{\{/g)) {
198
+ const open = m.index + m[0].length - 2, expr = braceAt(text, open);
199
+ if (expr.endsWith('}}')) for (const e of styleEntries(expr.slice(2, -2), open + 2, ',')) report(e);
200
+ }
201
+ for (const m of text.matchAll(/\bstyle\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
202
+ const body = m[1] ?? m[2];
203
+ for (const e of styleEntries(body, m.index + m[0].indexOf(body), ';')) report(e);
204
+ }
205
+ }
206
+
134
207
  return findings.sort((a, b) => a.line - b.line);
135
208
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.73",
3
+ "version": "0.7.75",
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.",
@@ -1,11 +0,0 @@
1
- import type { ComponentProps } from 'react';
2
- export interface SegmentProps extends ComponentProps<'div'> {
3
- }
4
- /** Segment — a tab strip / day picker whose items share the width; children are SegmentItems. */
5
- export declare function Segment({ className, ...rest }: SegmentProps): import("react").JSX.Element;
6
- export interface SegmentItemProps extends ComponentProps<'button'> {
7
- /** the selected tab (filled) */
8
- selected?: boolean;
9
- }
10
- /** SegmentItem — one tab in a Segment. */
11
- export declare function SegmentItem({ selected, className, ...rest }: SegmentItemProps): import("react").JSX.Element;
@@ -1,18 +0,0 @@
1
- import type { ComponentProps, ReactNode } from 'react';
2
- import type { Tone } from '../cards/Card.js';
3
- export interface BandProps extends ComponentProps<'article'> {
4
- /** the two-line label (use a `<br />` to break it) */
5
- label: ReactNode;
6
- /** the giant number; wrap a unit in `<small>` */
7
- num: ReactNode;
8
- /** the footnote under the label */
9
- foot?: ReactNode;
10
- /** the paper — see `Tone` */
11
- tone?: Tone;
12
- }
13
- /** Band — a data card: 2-line label · giant number · footnote. Stack several in a BandStack for one boundary with hairlines. */
14
- export declare function Band({ label, num, foot, tone, className, ...rest }: BandProps): import("react").JSX.Element;
15
- export interface BandStackProps extends ComponentProps<'div'> {
16
- }
17
- /** BandStack — several Bands as ONE card boundary, a hairline between each. */
18
- export declare function BandStack({ className, ...rest }: BandStackProps): import("react").JSX.Element;
@@ -1,11 +0,0 @@
1
- import type { ComponentProps } from 'react';
2
- export interface PagerAtProps extends Omit<ComponentProps<'span'>, 'children'> {
3
- /** the current page, 1-based */
4
- at: number;
5
- /** how many pages */
6
- of: number;
7
- /** accessible name; defaults to "Page {at} of {of}" */
8
- label?: string;
9
- }
10
- /** PagerAt — the "where am I" dots: one accent dot at the current page. Under the card that changes (a child of Centre), or a Step's child after its Pages — pinned to the base's foot. One per walk, never inside a page; in a Walkthrough it is the bottom layer's first row (an ActionBar child, centred). */
11
- export declare function PagerAt({ at, of, label, className, ...rest }: PagerAtProps): import("react").JSX.Element;
@@ -1,6 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { cx } from '../cx.js';
3
- /** PagerAt — the "where am I" dots: one accent dot at the current page. Under the card that changes (a child of Centre), or a Step's child after its Pages — pinned to the base's foot. One per walk, never inside a page; in a Walkthrough it is the bottom layer's first row (an ActionBar child, centred). */
4
- export function PagerAt({ at, of, label, className, ...rest }) {
5
- return (_jsx("span", { className: cx('pager__at', className), role: "img", "aria-label": label ?? `Page ${at} of ${of}`, ...rest, children: Array.from({ length: of }, (_, i) => _jsx("i", { className: i + 1 === at ? 'is-at' : undefined }, i)) }));
6
- }