@usableapp/cardds 0.7.71 → 0.7.73

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 (49) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +24 -0
  3. package/bin/cardds.mjs +59 -3
  4. package/css/people.css +6 -0
  5. package/css/stamp.css +4 -0
  6. package/dist/actions/Chip.d.ts +7 -0
  7. package/dist/actions/Dropdown.d.ts +1 -0
  8. package/dist/actions/Fab.d.ts +3 -0
  9. package/dist/actions/IconBtnGroup.d.ts +1 -0
  10. package/dist/cardds.css +10 -0
  11. package/dist/cards/Card.d.ts +2 -0
  12. package/dist/cards/CardHead.d.ts +5 -0
  13. package/dist/cards/CardHead.js +5 -5
  14. package/dist/catalog.json +1 -0
  15. package/dist/choice/Calendar.d.ts +4 -2
  16. package/dist/choice/DayStrip.d.ts +1 -0
  17. package/dist/choice/Mood.d.ts +1 -0
  18. package/dist/choice/Slider.d.ts +2 -0
  19. package/dist/forms/Composer.d.ts +2 -0
  20. package/dist/forms/Field.d.ts +2 -0
  21. package/dist/forms/FileBtn.d.ts +1 -0
  22. package/dist/journey/NoteRow.d.ts +1 -0
  23. package/dist/journey/Route.d.ts +2 -0
  24. package/dist/layover/Callout.d.ts +4 -1
  25. package/dist/lists/Kv.d.ts +3 -0
  26. package/dist/lists/Row.d.ts +3 -1
  27. package/dist/media/Mosaic.d.ts +2 -0
  28. package/dist/media/Postcard.d.ts +4 -0
  29. package/dist/media/Tile.d.ts +6 -0
  30. package/dist/numbers/Bars.d.ts +1 -0
  31. package/dist/numbers/DotGrid.d.ts +1 -0
  32. package/dist/numbers/Picker.d.ts +1 -0
  33. package/dist/numbers/Track.d.ts +1 -0
  34. package/dist/people/Avatar.d.ts +7 -2
  35. package/dist/people/Avatar.js +4 -4
  36. package/dist/people/AvatarPick.d.ts +1 -0
  37. package/dist/post/PostLetter.d.ts +2 -1
  38. package/dist/scaffold/LayerHistory.d.ts +1 -0
  39. package/dist/scaffold/Screen.d.ts +1 -0
  40. package/dist/sheets/StackSheet.d.ts +2 -0
  41. package/dist/stamp/StampBook.d.ts +3 -1
  42. package/dist/stamp/StampBook.js +2 -2
  43. package/dist/templates/CallSheet.d.ts +2 -0
  44. package/dist/templates/Home.d.ts +2 -0
  45. package/dist/templates/Walkthrough.d.ts +5 -1
  46. package/lib/catalog.mjs +203 -0
  47. package/lib/mcp.mjs +107 -0
  48. package/lib/role.mjs +30 -0
  49. package/package.json +3 -2
package/lib/mcp.mjs ADDED
@@ -0,0 +1,107 @@
1
+ // lib/mcp.mjs — `npx cardds mcp`: the catalog (lib/catalog.mjs) as an MCP server over stdio, so an AI session asks instead of reading src/.
2
+ // Newline-delimited JSON-RPC 2.0 written by hand: the package has no MCP SDK and takes none. stdout carries protocol messages ONLY.
3
+ import { createInterface } from 'node:readline';
4
+ import { readFileSync } from 'node:fs';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import * as catalog from './catalog.mjs';
8
+
9
+ const { version } = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'));
10
+ const str = (description) => ({ type: 'string', description });
11
+ const bool = (description) => ({ type: 'boolean', description });
12
+
13
+ const TOOLS = [
14
+ {
15
+ 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
+ inputSchema: { type: 'object', properties: {} },
18
+ run: (cat) => catalog.list(cat),
19
+ },
20
+ {
21
+ name: 'get_component',
22
+ description: 'One component in full: what it is, its props (a prop marked slot takes children: ReactNode), and its variations, each a working story with its code. Call it before you use a component you have not used yet, and copy a variation rather than guessing a prop. A component used everywhere (Btn, Text) comes with a few of its stories and the ids of the rest (pass all for their code). Names are case-insensitive; a miss returns the closest names.',
23
+ inputSchema: { type: 'object', properties: { name: str('component name, e.g. Menu'), html: bool('also return each variation\'s rendered HTML — only to see the markup it makes'), all: bool('include the code of every story that uses the component') }, required: ['name'] },
24
+ run: (cat, a) => catalog.get(cat, a.name, { html: a.html, all: a.all }),
25
+ },
26
+ {
27
+ name: 'find_components',
28
+ description: (cat) => `Find what to build with when you do not know the component name. Ask BOTH ways and read both answers: by the AXES (translated from the request) AND by the English noun of the task ("stamp", "otp", "postcard") — the screen already drawn is often found by the noun. The axes are a closed vocabulary, so ask by them in any language: represents (what it stands for, or for a control the KIND of data it carries${cat ? `: ${catalog.vocabulary(cat).represents.join(' · ')}` : ' — the word in parentheses after a name in list_components'}), shape (one · group-brief · group-detail · group = either group shape), interact (the kinds the member can do with it: ${catalog.KINDS.join(' · ')}, or "none" = it only shows). "Show all the members" is represents [person] + shape group; adding interact [pick] narrows it to the ones the member chooses from. Whatever matches every axis you give comes first, each with its role so you see why; with axes and no words they are the whole answer. Words ("stamp", "otp pin", "bottom sheet") rank what matches only by words, in ENGLISH — the names and descriptions are English, translate first. Answers short: the screens cardds has already drawn (a screen is a composition: it counts when its parts answer the axes between them, one component per axis, and one that answers them all ranks higher), then components, then the components most often used together with them. Call it at the start of a task, before composing a screen from parts: if a screen like it exists, scaffold_screen gives its code. A query that matches nothing returns fallback: true, a hint and the number of screens drawn, not their ids: ask again in English words or by the axes, or call scaffold_screen with no id to list them.`,
29
+ inputSchema: {
30
+ type: 'object',
31
+ properties: {
32
+ represents: { type: 'array', items: { type: 'string' }, description: 'what it stands for, e.g. ["person"]; a component must hold every value you give' },
33
+ shape: { type: 'string', enum: ['one', 'group-brief', 'group-detail', 'group'], description: 'how many it shows and how much reading; group = group-brief or group-detail' },
34
+ interact: { anyOf: [{ type: 'array', items: { type: 'string' }, description: 'kinds, e.g. ["pick"]; the component must have every one' }, { type: 'string', enum: ['none'], description: 'only shows, nothing to do with it' }] },
35
+ query: str('English words describing the screen or the part; optional when axes are given'),
36
+ },
37
+ },
38
+ run: (cat, a) => catalog.find(cat, a.query ?? '', { represents: a.represents, shape: a.shape, interact: a.interact === 'none' ? false : a.interact }),
39
+ },
40
+ {
41
+ name: 'scaffold_screen',
42
+ description: 'Start from a screen cardds has already drawn. With no id: the list of such screens, one line each. With an id (from find_components): a complete file to start from — format tsx (default) is the React code with its imports, edit its content for the task; format html is a static document (the look only, no behaviour) that links cardds.css, for a prototype without React — when the screen has a sheet, menu, step or the like, the file begins with a comment saying the static picture is not the layout. A tsx file may name story glue (from ./_story) that is not in the package: replace or drop it.',
43
+ inputSchema: { type: 'object', properties: { id: str('a screen id such as StampScreens/MakeStamp; omit to list the screens'), format: { type: 'string', enum: ['tsx', 'html'], description: 'tsx (default) or html' }, css: str('html only: the href of cardds.css from where the file will sit; default node_modules/@usableapp/cardds/dist/cardds.css') } },
44
+ run: (cat, a) => {
45
+ const r = catalog.scaffold(cat, a.id, { format: a.format, css: a.css });
46
+ return typeof r.html === 'string' ? `${r.warning ? `<!-- ${r.warning} -->\n` : ''}${r.html}` : r; // the warning first, where a caller reading the top of the file cannot miss it
47
+ },
48
+ },
49
+ {
50
+ name: 'check_source',
51
+ description: 'Run cardds\'s rules over code you wrote (a hand-written cardds class, a Sheet that cannot move, a bar with a sheet…). Call it on each screen before you call it done; an empty list means clean, an error must be fixed.',
52
+ inputSchema: { type: 'object', properties: { code: str('the .tsx/.jsx source'), filename: str('its file name, for the report (default screen.tsx)') }, required: ['code'] },
53
+ run: (_cat, a) => catalog.check(a.code, a.filename),
54
+ },
55
+ ];
56
+
57
+ const reply = (id, result) => ({ jsonrpc: '2.0', id, result });
58
+ const fail = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
59
+
60
+ /** serve the catalog until the client closes stdin; `root` serves a live repo (rebuilt first) instead of the package's */
61
+ export function serve({ root, input = process.stdin, output = process.stdout } = {}) {
62
+ let cat;
63
+ const text = (r) => typeof r === 'string' ? r : JSON.stringify(r);
64
+
65
+ // the catalog, or nothing: listing the tools must not fail because it cannot be loaded (a call says why)
66
+ function loaded() {
67
+ try { return (cat ??= catalog.load({ root })); } catch { return undefined; }
68
+ }
69
+
70
+ function call(params) {
71
+ const tool = TOOLS.find((t) => t.name === params?.name);
72
+ if (!tool) return null;
73
+ const args = params.arguments ?? {};
74
+ const missing = tool.inputSchema.required?.find((k) => typeof args[k] !== 'string');
75
+ if (missing) return { content: [{ type: 'text', text: `${tool.name}: "${missing}" is required (a string)` }], isError: true };
76
+ try {
77
+ cat ??= catalog.load({ root });
78
+ const result = tool.run(cat, args);
79
+ return { content: [{ type: 'text', text: text(result) }], ...(result?.error && { isError: true }) };
80
+ } catch (e) { return { content: [{ type: 'text', text: `${tool.name}: ${e.message}` }], isError: true }; }
81
+ }
82
+
83
+ function handle(msg) {
84
+ const { id, method, params } = msg;
85
+ switch (method) {
86
+ case 'initialize': return reply(id, { protocolVersion: params?.protocolVersion ?? '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: 'cardds', version } });
87
+ case 'ping': return reply(id, {});
88
+ case 'tools/list': return reply(id, { tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description: typeof description === 'function' ? description(loaded()) : description, inputSchema })) });
89
+ case 'tools/call': { const r = call(params); return r ? reply(id, r) : fail(id, -32602, `unknown tool: ${params?.name}`); }
90
+ default: return fail(id, -32601, `method not found: ${method}`);
91
+ }
92
+ }
93
+
94
+ return new Promise((done) => {
95
+ const send = (msg) => output.write(`${JSON.stringify(msg)}\n`);
96
+ const lines = createInterface({ input });
97
+ lines.on('line', (line) => {
98
+ if (!line.trim()) return;
99
+ let msg;
100
+ try { msg = JSON.parse(line); } catch { return send(fail(null, -32700, 'parse error')); }
101
+ if (!msg || typeof msg !== 'object') return send(fail(null, -32600, 'invalid request'));
102
+ if (msg.id === undefined) return; // a notification (notifications/initialized…) is never answered
103
+ send(handle(msg));
104
+ });
105
+ lines.on('close', done);
106
+ });
107
+ }
package/lib/role.mjs ADDED
@@ -0,0 +1,30 @@
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
+ // and the gallery's nav (browser) all read the table through these.
3
+ const AXES = ['represents', 'count', 'density', 'job'];
4
+ const values = (cell) => cell.trim().split(' · ').map((v) => v.trim()).filter(Boolean);
5
+
6
+ /** 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. */
8
+ export function parseRoles(doc) {
9
+ const vocabulary = Object.fromEntries([...doc.matchAll(/^- `([a-z]+)`: (.+)$/gm)].filter((m) => AXES.includes(m[1])).map((m) => [m[1], values(m[2])]));
10
+ const rows = [...doc.matchAll(/^\| (?!component )([A-Za-z]\w*) \| ([a-z]+) \|(.*)\|$/gm)].map((m) => {
11
+ const [represents, count, density, job, stands, note] = m[3].split('|').map((c) => c.trim());
12
+ return {
13
+ component: m[1],
14
+ group: m[2],
15
+ 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] ?? '' })) : [],
17
+ note: note.replace(/^\?\s*/, ''),
18
+ unsure: note.startsWith('?'),
19
+ };
20
+ });
21
+ return { vocabulary, rows };
22
+ }
23
+
24
+ // What a component is FOR, read from its role: every component shows information; what differs is how much it shows
25
+ // (`one` thing, a group in brief, a group in detail) and whether the member can do something with it (and what: pick, act, write, navigate).
26
+ export const SHAPES = ['one', 'group-brief', 'group-detail', '—'];
27
+ export const KINDS = ['pick', 'act', 'write', 'navigate'];
28
+ export const shapeOf = ({ count: [count], density: [density] }) =>
29
+ count === 'one' ? 'one' : count === 'few' || count === 'many' ? (density === 'detail' || density === 'full' ? 'group-detail' : 'group-brief') : '—'; // a holder with no density of its own claims no detail
30
+ export const interactOf = ({ job }) => job.some((j) => KINDS.includes(j)) ? job.filter((j) => KINDS.includes(j)) : false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.71",
3
+ "version": "0.7.73",
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.",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "dev": "vite --config gallery/vite.config.ts",
13
- "build": "node scripts/build-icons.mjs && tsc -p tsconfig.json && node scripts/flatten-css.mjs",
13
+ "build": "node scripts/build-icons.mjs && tsc -p tsconfig.json && node scripts/flatten-css.mjs && node scripts/build-catalog.mjs",
14
14
  "test": "playwright test",
15
15
  "validate:inbox": "node scripts/validate-inbox.mjs",
16
16
  "prepack": "npm run build",
@@ -52,6 +52,7 @@
52
52
  "files": [
53
53
  "dist",
54
54
  "lint",
55
+ "lib",
55
56
  "bin",
56
57
  "css",
57
58
  "fonts",