@usableapp/cardds 0.7.85 → 0.7.87
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 +33 -0
- package/README.md +44 -1
- package/bin/cardds.mjs +8 -3
- package/css/actions.css +12 -7
- package/css/card.css +1 -2
- package/css/choice.css +2 -2
- package/css/finish.css +97 -11
- package/css/forms.css +1 -2
- package/css/numbers.css +1 -2
- package/css/post.css +1 -2
- package/css/stack.css +1 -1
- package/css/stamp.css +44 -4
- package/css/step.css +6 -0
- package/css/sticky.css +45 -2
- package/css/tokens.css +6 -0
- package/dist/cardds.css +217 -35
- package/dist/cards/CardFeed.js +2 -9
- package/dist/catalog.json +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lists/Row.js +2 -9
- package/dist/pressable.d.ts +8 -0
- package/dist/pressable.js +6 -0
- package/dist/scaffold/Screen.js +2 -9
- package/dist/sheets/StackSheet.js +3 -7
- package/dist/sheets/sheetMeasure.d.ts +1 -0
- package/dist/sheets/sheetMeasure.js +14 -9
- package/dist/sheets/stackCard.js +41 -48
- package/dist/stamp/Stamp.js +3 -6
- package/dist/stamp/StampBook.js +2 -3
- package/dist/stamp/StampOn.d.ts +21 -3
- package/dist/stamp/StampOn.js +75 -18
- package/dist/stamp/underNotes.d.ts +3 -0
- package/dist/stamp/underNotes.js +4 -0
- package/dist/state.d.ts +5 -1
- package/dist/state.js +8 -0
- package/dist/sticky/Sticky.d.ts +2 -2
- package/dist/sticky/Sticky.js +2 -3
- package/dist/sticky/StickyPile.d.ts +20 -0
- package/dist/sticky/StickyPile.js +31 -0
- package/dist/words.d.ts +1 -1
- package/dist/words.js +2 -2
- package/lib/catalog.mjs +24 -7
- package/lib/mcp.mjs +3 -3
- package/lint/rules.mjs +1 -1
- package/package.json +1 -1
package/dist/sticky/Sticky.d.ts
CHANGED
|
@@ -13,13 +13,13 @@ export interface StickyProps extends Omit<ComponentProps<'div'>, 'onClick'> {
|
|
|
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, unless its holder turns it (`StickyBoard`, `StickyThread lapped`) */
|
|
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`, `StickyPile`, `StickyThread lapped`) */
|
|
17
17
|
tilt?: 'left' | 'right';
|
|
18
18
|
/** a small muted line at the bottom ("From …") */
|
|
19
19
|
foot?: ReactNode;
|
|
20
20
|
/** a control (an IconBtn) standing on the bottom-right corner, half outside the note; the note then has no fold */
|
|
21
21
|
corner?: ReactNode;
|
|
22
|
-
/** things stuck ON the note, each placing itself over the paper (a `StampOn`) — a layer the size of the note, in front of the words */
|
|
22
|
+
/** things stuck ON the note, each placing itself over the paper (a `StampOn`: the small stamp, or `main` for the main size) — a layer the size of the note, in front of the words */
|
|
23
23
|
stuck?: ReactNode;
|
|
24
24
|
}
|
|
25
25
|
/**
|
package/dist/sticky/Sticky.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
+
import { pressable } from '../pressable.js';
|
|
3
4
|
import { word } from '../words.js';
|
|
4
5
|
/**
|
|
5
6
|
* Sticky — a paper note: a flat pastel SQUARE of one size (168, the knob `--sticky-size`), square corners, a soft shadow under it.
|
|
@@ -9,9 +10,7 @@ import { word } from '../words.js';
|
|
|
9
10
|
*/
|
|
10
11
|
export function Sticky({ tone = 'custard', flat, unreadLabel, head, date, tilt, foot, corner, stuck, onClick, className, children, ...rest }) {
|
|
11
12
|
const notched = corner != null;
|
|
12
|
-
const tap =
|
|
13
|
-
const Box = (tap ? 'button' : 'div');
|
|
13
|
+
const { tap, Box, boxProps } = pressable(notched ? undefined : onClick, 'div', rest);
|
|
14
14
|
const Part = tap ? 'span' : 'div';
|
|
15
|
-
const boxProps = (tap ? { type: 'button', onClick, ...rest } : rest);
|
|
16
15
|
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 })] }));
|
|
17
16
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ComponentProps } from 'react';
|
|
2
|
+
export interface StickyPileProps extends Omit<ComponentProps<'div'>, 'onClick'> {
|
|
3
|
+
/** the whole pile is ONE button: it renders a `<button>` over the notes and says that it was pressed — one note or many, and where the press goes, is the app's. Without it the pile only shows. A `Sticky` inside with an `onClick` of its own is misuse (two buttons for one pile): the pile warns once */
|
|
4
|
+
onClick?: () => void;
|
|
5
|
+
/** the pile's accessible name, REQUIRED with `onClick` (the notes' words are not read by the button): say how many and from whom, in the app's words. Warns once without it */
|
|
6
|
+
label?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* StickyPile — ONE to SIX `Sticky` notes lying one over another, as the newest notes of a feed do. The FIRST child is on top (the app passes
|
|
10
|
+
* the newest first); each note under it is shifted a step right and down, so a strip of it peeks out (`--sticky-pile-step-x`, 24, and
|
|
11
|
+
* `--sticky-pile-step-y`, 8) and turned a little by the same four angles as `StickyBoard` (−3° · 1.5° · 2.75° · −1.75°, by the child's place,
|
|
12
|
+
* so the top note's turn is the same whether the pile holds one note or six). Only the top note draws its `foot`: a cut time under another note is
|
|
13
|
+
* noise. A pile of one is that note, centred, with its foot. A seventh note is not drawn (a warning, once).
|
|
14
|
+
* The pile is centred in its column and never wider than it: a column too narrow for the steps shortens them (a lone note's width is the least it takes).
|
|
15
|
+
* It knows nothing of a feed or a home: in a Home's `base` it stands above a `CardFeed`, and the app writes both.
|
|
16
|
+
* With `onClick` the whole pile is one control with a name (`label`): one Tab stop, a ring round the pile, dimmed while it is held.
|
|
17
|
+
* A `StampOn` on the TOP note keeps its own touch — dragging it, or pressing its cross, never presses the pile — and the stamps on the notes under it are inert
|
|
18
|
+
* (no drag, no cross, no focus: they only show what peeks out), whatever the app passed.
|
|
19
|
+
*/
|
|
20
|
+
export declare function StickyPile({ onClick, label, className, style, children, ...rest }: StickyPileProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Children, isValidElement } from 'react';
|
|
3
|
+
import { cx } from '../cx.js';
|
|
4
|
+
import { warnOnce } from '../state.js';
|
|
5
|
+
import { UnderNotes } from '../stamp/underNotes.js';
|
|
6
|
+
/** most notes a pile draws; a seventh is dropped, as a Stamp drops a fifth picture */
|
|
7
|
+
const MOST = 6;
|
|
8
|
+
/**
|
|
9
|
+
* StickyPile — ONE to SIX `Sticky` notes lying one over another, as the newest notes of a feed do. The FIRST child is on top (the app passes
|
|
10
|
+
* the newest first); each note under it is shifted a step right and down, so a strip of it peeks out (`--sticky-pile-step-x`, 24, and
|
|
11
|
+
* `--sticky-pile-step-y`, 8) and turned a little by the same four angles as `StickyBoard` (−3° · 1.5° · 2.75° · −1.75°, by the child's place,
|
|
12
|
+
* so the top note's turn is the same whether the pile holds one note or six). Only the top note draws its `foot`: a cut time under another note is
|
|
13
|
+
* noise. A pile of one is that note, centred, with its foot. A seventh note is not drawn (a warning, once).
|
|
14
|
+
* The pile is centred in its column and never wider than it: a column too narrow for the steps shortens them (a lone note's width is the least it takes).
|
|
15
|
+
* It knows nothing of a feed or a home: in a Home's `base` it stands above a `CardFeed`, and the app writes both.
|
|
16
|
+
* With `onClick` the whole pile is one control with a name (`label`): one Tab stop, a ring round the pile, dimmed while it is held.
|
|
17
|
+
* A `StampOn` on the TOP note keeps its own touch — dragging it, or pressing its cross, never presses the pile — and the stamps on the notes under it are inert
|
|
18
|
+
* (no drag, no cross, no focus: they only show what peeks out), whatever the app passed.
|
|
19
|
+
*/
|
|
20
|
+
export function StickyPile({ onClick, label, className, style, children, ...rest }) {
|
|
21
|
+
const all = Children.toArray(children).filter((c) => isValidElement(c));
|
|
22
|
+
if (all.length > MOST)
|
|
23
|
+
warnOnce('sticky-pile-most', `cardds StickyPile: ${all.length} notes — at most six are drawn; show the rest elsewhere`);
|
|
24
|
+
const notes = all.slice(0, MOST);
|
|
25
|
+
const tap = onClick != null;
|
|
26
|
+
if (tap && label == null)
|
|
27
|
+
warnOnce('sticky-pile-label', 'cardds StickyPile: onClick without a label — the pile is one button and the notes\' words are not its name');
|
|
28
|
+
if (tap && notes.some((n) => n.props.onClick != null))
|
|
29
|
+
warnOnce('sticky-pile-tap', 'cardds StickyPile: a Sticky with its own onClick inside a pile that has one — two buttons for one pile; give the pile the onClick');
|
|
30
|
+
return (_jsx("div", { className: cx('sticky-pile', tap && 'sticky-pile--tap', className), style: { '--_n': notes.length, ...style }, ...rest, children: _jsxs("div", { className: "sticky-pile__notes", children: [notes.map((note, i) => _jsx(UnderNotes.Provider, { value: i > 0, children: note }, note.key)), tap && _jsx("button", { type: "button", className: "sticky-pile__press", "aria-label": label, onClick: onClick })] }) }));
|
|
31
|
+
}
|
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' | 'unread';
|
|
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' | 'remove';
|
|
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', unread: 'Unread' },
|
|
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', remove: 'Remove' },
|
|
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: 'เลือกแสตมป์', unread: 'ยังไม่ได้อ่าน' },
|
|
5
|
+
play: 'ฟังเสียง', pause: 'หยุดเสียง', to: 'ถึง', turnToBack: 'พลิกดูด้านหลัง', turnToPicture: 'พลิกดูรูป', message: 'ข้อความ', address: 'จ่าหน้า', toMessage: 'เลื่อนกลับไปข้อความ', toAddress: 'เลื่อนไปจ่าหน้า', stampOn: 'ติดแสตมป์', stampOff: 'แกะแสตมป์ออก', stamps: 'เลือกแสตมป์', unread: 'ยังไม่ได้อ่าน', remove: 'เอาออก' },
|
|
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
|
@@ -29,8 +29,10 @@ const representsOf = ({ represents }) => represents.filter((v) => v !== '—');
|
|
|
29
29
|
|
|
30
30
|
/** the names only — the first call of nearly every session, so it stays small. `group` is the src folder; `represents`, `shape` and `interact` say what it is for */
|
|
31
31
|
export const list = (cat) => cat.components.map(({ name, group, role }) => ({ name, group, represents: representsOf(role), shape: shapeOf(role), interact: interactOf(role) }));
|
|
32
|
+
/** the finishes of the paper (`data-finish`, css/finish.css): a style apart from colour, not a component — each with what it is and its knobs */
|
|
33
|
+
export const finishes = (cat) => cat.finishes ?? [];
|
|
32
34
|
|
|
33
|
-
const sentence = (text) => {
|
|
35
|
+
export const sentence = (text) => {
|
|
34
36
|
const flat = text.replace(/\s+/g, ' ').trim();
|
|
35
37
|
const one = flat.match(/^.*?[.!?](?=\s|$)/)?.[0] ?? flat;
|
|
36
38
|
return one.length > 160 ? `${one.slice(0, 157)}…` : one;
|
|
@@ -56,7 +58,7 @@ const closest = (names, wanted) => {
|
|
|
56
58
|
return [...names].sort((a, b) => rank(a) - rank(b)).slice(0, 5);
|
|
57
59
|
};
|
|
58
60
|
|
|
59
|
-
/** one component: its props (a ReactNode prop is a slot) and its variations — the stories of its own file first, then a few that use it */
|
|
61
|
+
/** one component: its props (a ReactNode prop is a slot), what it composes and what composes it, its knobs, and its variations — the stories of its own file first, then a few that use it */
|
|
60
62
|
export function get(cat, name, { html = false, all = false } = {}) {
|
|
61
63
|
const c = cat.components.find((x) => x.name.toLowerCase() === String(name).toLowerCase());
|
|
62
64
|
if (!c) return { error: `no component "${name}"`, closest: closest(cat.components.map((x) => x.name), String(name)) };
|
|
@@ -72,6 +74,9 @@ export function get(cat, name, { html = false, all = false } = {}) {
|
|
|
72
74
|
element: c.element,
|
|
73
75
|
props: c.props.map((p) => ({ ...p, ...(isSlot(p.type) && { slot: true }) })),
|
|
74
76
|
...(c.params && { params: c.params }),
|
|
77
|
+
composes: c.composes,
|
|
78
|
+
composedBy: c.composedBy,
|
|
79
|
+
knobs: c.knobs,
|
|
75
80
|
variations: [...own, ...elsewhere.filter((s) => shown.includes(s))].map(show),
|
|
76
81
|
...(elsewhere.length > shown.length && { alsoUsedIn: elsewhere.filter((s) => !shown.includes(s)).map((s) => s.id) }),
|
|
77
82
|
};
|
|
@@ -82,6 +87,7 @@ const STOP = new Set(['the', 'and', 'for', 'with', 'from', 'that', 'this', 'into
|
|
|
82
87
|
const tokens = (query) => [...new Set(words(String(query)).filter((t) => t.length > 1 && !STOP.has(t)))];
|
|
83
88
|
const stem = (t) => (t.length > 3 && t.endsWith('s') ? t.slice(0, -1) : t);
|
|
84
89
|
const hits = (text, token) => words(text).some((w) => w.startsWith(token) || w.startsWith(stem(token)));
|
|
90
|
+
const isName = (name, token) => name.toLowerCase() === token || name.toLowerCase() === stem(token);
|
|
85
91
|
|
|
86
92
|
const TOP = 8; // answers per kind; the components that match every axis are never cut
|
|
87
93
|
|
|
@@ -119,7 +125,7 @@ const matchesUnion = (roles, want) => roles.length > 0
|
|
|
119
125
|
/** short ranked answer to "what is there for this?". `axes` ({ represents?, shape?, interact? } — what it represents, how many, what the member can do with it) are
|
|
120
126
|
* asked first: whatever matches every one of them (a component, or a screen where one component does, then a screen whose parts do between them) ranks above whatever matches only words;
|
|
121
127
|
* with axes and no words they are the whole answer. Among screens that tie on words, the one whose description says the axes comes first, then the one where a single component matches them all.
|
|
122
|
-
* Then the words: screens already drawn, then components, then what they are used with.
|
|
128
|
+
* Then the words: screens already drawn, then components, then what they are used with — a screen that answers fewer of the words than the best component comes after the components.
|
|
123
129
|
* A query that matches nothing (another language, say) returns `fallback: true`, a hint and how many screens are drawn — not their ids:
|
|
124
130
|
* they did not help to choose; ask again in English or by the axes, or `scaffold` with no id lists them. */
|
|
125
131
|
export function find(cat, query, axes = {}) {
|
|
@@ -129,11 +135,11 @@ export function find(cat, query, axes = {}) {
|
|
|
129
135
|
const roles = new Map(cat.components.map((c) => [c.name, roleOf(c)]));
|
|
130
136
|
const toks = tokens(query);
|
|
131
137
|
const axisWords = [...(asked.want.represents ?? []), ...(asked.want.interact || [])]; // the axis values as words: a screen's own description may say them
|
|
132
|
-
const rank = (items, fields, level, said = () => 0) => {
|
|
138
|
+
const rank = (items, fields, level, said = () => 0) => { // a field is [text, weight, match = hits]
|
|
133
139
|
const scored = items.map((item) => {
|
|
134
140
|
let matched = 0, score = 0;
|
|
135
141
|
for (const t of toks) {
|
|
136
|
-
const w = fields(item).filter(([text]) =>
|
|
142
|
+
const w = fields(item).filter(([text, , match = hits]) => match(text, t)).reduce((sum, [, weight]) => sum + weight, 0);
|
|
137
143
|
if (w) { matched++; score += w; }
|
|
138
144
|
}
|
|
139
145
|
return { item, matched, score, level: active ? level(item) : 0, said: active ? said(item) : 0 };
|
|
@@ -152,9 +158,20 @@ export function find(cat, query, axes = {}) {
|
|
|
152
158
|
};
|
|
153
159
|
const said = (s) => axisWords.filter((v) => hits(s.description, v)).length;
|
|
154
160
|
const [screenAxis, screenRest] = tier(rank(screens, (s) => [[s.id, 5], [s.description, 3], [s.uses.join(' '), 1]], level, said), TOP);
|
|
155
|
-
|
|
161
|
+
// a component is found by its name (the name that IS the word counts as much again as one that contains it: "card" is Card before FeedCard, whatever
|
|
162
|
+
// their other words say), its description, its role's note (what it is FOR, in the words of a request: "a setting turned on or off") and what it stands in for, then its prop names
|
|
163
|
+
const components = rank(cat.components, (c) => [[c.name, 6, isName], [c.name, 6], [c.description, 2], [c.role?.note ?? '', 2], [(c.role?.standsInFor ?? []).map((s) => `${s.name} ${s.condition}`).join(' '), 1], [c.props.map((p) => p.name).join(' '), 1]], (c) => Number(matches(roles.get(c.name), asked.want)));
|
|
156
164
|
const [componentAxis, componentRest] = tier(components, Math.max(TOP, components.axis.length));
|
|
157
|
-
|
|
165
|
+
// a screen drawn comes before the components when it answers the words as well as the best component does; one that answers FEWER of them
|
|
166
|
+
// comes after (a question by intention, "a setting turned on or off", is answered by Toggle, not by eight screens that say "off" or "or")
|
|
167
|
+
const best = componentRest[0]?.matched ?? 0;
|
|
168
|
+
// a finish asked for by its name ("printed", "sticker") or by the word finish is the thing itself, so it comes before the screens that only show it;
|
|
169
|
+
// one that answers a word of its description ("card") is an aside, after the components
|
|
170
|
+
const finishOf = ({ item: f }) => ({ kind: 'finish', name: f.name, summary: sentence(f.is), knobs: f.knobs.map((k) => k.name) });
|
|
171
|
+
const named = (text, t) => t.startsWith(text) || text.startsWith(t);
|
|
172
|
+
const finishRest = rank(finishes(cat), (f) => [[f.name, 6, named], ['finish', 6], [f.is, 2], [f.knobs.map((k) => k.name).join(' '), 1]], () => 0).rest;
|
|
173
|
+
const ranked = [...screenAxis.map(screenOf), ...componentAxis.map(componentOf), ...finishRest.filter((r) => r.score >= 6).map(finishOf), ...screenRest.filter((r) => r.matched >= best).map(screenOf),
|
|
174
|
+
...componentRest.map(componentOf), ...finishRest.filter((r) => r.score < 6).map(finishOf), ...screenRest.filter((r) => r.matched < best).map(screenOf)];
|
|
158
175
|
if (!ranked.length && active && !toks.length) return { query, results: [], hint: 'no component or screen has that role: drop an axis, or add English words' };
|
|
159
176
|
if (!ranked.length) return { query, fallback: true, hint: 'the names and descriptions are English: ask again in English words, or by the axes (represents · shape · interact); `scaffold` with no id lists the screens', screens: screens.length };
|
|
160
177
|
|
package/lib/mcp.mjs
CHANGED
|
@@ -36,19 +36,19 @@ const bool = (description) => ({ type: 'boolean', description });
|
|
|
36
36
|
const TOOLS = [
|
|
37
37
|
{
|
|
38
38
|
name: 'list_components',
|
|
39
|
-
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.',
|
|
39
|
+
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. The finishes of the paper (data-finish on <html>: lit, print, sticker, ink — a style apart from colour, not a component) are answered by find_components ("printed paper", "finish").',
|
|
40
40
|
inputSchema: { type: 'object', properties: {} },
|
|
41
41
|
run: (cat) => catalog.list(cat),
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
name: 'get_component',
|
|
45
|
-
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.',
|
|
45
|
+
description: 'One component in full: what it is, its props (a prop marked slot takes children: ReactNode), what it composes and what composes it (composes · composedBy: the catalog names its source renders or calls, and the inverse), its knobs (the CSS custom properties a theme sets to retune it, each with its default, what it sets and the selector that reads it — joined by the classes the component emits, so a knob named for an old name is listed under the component that has it now), 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.',
|
|
46
46
|
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'] },
|
|
47
47
|
run: (cat, a) => catalog.get(cat, a.name, { html: a.html, all: a.all }),
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
name: 'find_components',
|
|
51
|
-
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.`,
|
|
51
|
+
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; a component's words include its role's note and what it stands in for, so a question by intention ("a setting turned on or off", "a code digit by digit") finds the piece. 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; a screen that answers fewer of the words than the best component comes after the components, so a question by intention ("a setting turned on or off") names the piece first. A FINISH of the paper (data-finish on <html>, beside any palette: lit, print, sticker, ink) is answered too, kind "finish", with its knobs: ask "printed paper", "sticker", or "finish" for all four. 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.`,
|
|
52
52
|
inputSchema: {
|
|
53
53
|
type: 'object',
|
|
54
54
|
properties: {
|
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'], Sticky: ['unreadLabel'], PostLetter: ['backLabel', 'pictureLabel', 'playLabel', 'pauseLabel', 'toLabel'],
|
|
15
|
+
Calendar: ['prevLabel', 'nextLabel'], SheetStep: ['skipLabel'], Sticky: ['unreadLabel'], StampOn: ['removeLabel'], PostLetter: ['backLabel', 'pictureLabel', 'playLabel', 'pauseLabel', 'toLabel'],
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
const lineOf = (text, idx) => text.slice(0, idx).split('\n').length;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.87",
|
|
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.",
|