@usableapp/cardds 0.7.78 → 0.7.80
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 +12 -0
- package/README.md +10 -2
- package/bin/cardds.mjs +3 -2
- package/css/base.css +2 -2
- package/css/card.css +3 -0
- package/css/stack.css +23 -3
- package/dist/actions/Btn.d.ts +2 -1
- package/dist/cardds.css +28 -5
- package/dist/cards/CardFeed.d.ts +2 -2
- package/dist/cards/CardFeed.js +1 -1
- package/dist/cards/CardFoot.d.ts +2 -1
- package/dist/cards/CardFoot.js +2 -1
- package/dist/catalog.json +1 -1
- package/dist/sheets/SheetStack.d.ts +1 -1
- package/dist/sheets/SheetStack.js +15 -2
- package/lib/mcp.mjs +26 -5
- package/package.json +4 -1
|
@@ -21,7 +21,7 @@ export interface SheetStackProps extends ComponentProps<'div'> {
|
|
|
21
21
|
closed?: boolean;
|
|
22
22
|
/** how an OPEN sheet meets the top bar of a closed stack in a `Screen fill`: `under` (default) stops under the bar · `flood` rises over it and fills the WHOLE screen (from the safe-top line down), the bar covered and hidden while a sheet is open. Only for a `closed` tap stack (warns once otherwise). Nothing resizes: the bar is a layer under the sheets */
|
|
23
23
|
rise?: 'under' | 'flood';
|
|
24
|
-
/** what the base shows in a closed stack's headroom, behind the sheets, laid out against the peeks (a picture, a greeting) */
|
|
24
|
+
/** what the base shows in a closed stack's headroom, behind the sheets, laid out against the peeks (a picture, a greeting). A room too short for it keeps the upper thing whole and scrolls: the rest lies under the heads until the member scrolls up to it */
|
|
25
25
|
base?: ReactNode;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Children, cloneElement, isValidElement, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
-
import { useControlled, warnOnce } from '../state.js';
|
|
3
|
+
import { useControlled, warnOnce, watchSize } from '../state.js';
|
|
4
4
|
import { cx } from '../cx.js';
|
|
5
5
|
import { Card } from '../cards/Card.js';
|
|
6
6
|
import { StackSheet } from './StackSheet.js';
|
|
@@ -25,6 +25,19 @@ function measureRests(stack) {
|
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
const SLOP = 8; // px a pointer may travel and still be a tap
|
|
28
|
+
/* the base's room either holds what stands in it or is too short for it (`is-long`). The CSS lays both out the same way, from the
|
|
29
|
+
room's top once there is no room to spare, so the measure does not feed back; the class only says there is something to scroll. */
|
|
30
|
+
function StackBase({ children }) {
|
|
31
|
+
const ref = useRef(null);
|
|
32
|
+
const [long, setLong] = useState(false);
|
|
33
|
+
useLayoutEffect(() => {
|
|
34
|
+
const box = ref.current;
|
|
35
|
+
if (!box)
|
|
36
|
+
return;
|
|
37
|
+
return watchSize([box, ...box.children], () => setLong(box.scrollHeight - box.clientHeight > 1));
|
|
38
|
+
});
|
|
39
|
+
return _jsx("div", { ref: ref, className: cx('sheet-stack__base', long && 'is-long'), children: children });
|
|
40
|
+
}
|
|
28
41
|
/* one wrapper around a sheet is fine (a host's own box). The props land on the Card or StackSheet inside it, so the card renders them. */
|
|
29
42
|
function stamp(child, place) {
|
|
30
43
|
if (SHEET.has(child.type))
|
|
@@ -163,5 +176,5 @@ export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChang
|
|
|
163
176
|
};
|
|
164
177
|
return stamp(child, place);
|
|
165
178
|
}) : items;
|
|
166
|
-
return (_jsxs("div", { className: cx('sheet-stack', mode !== 'static' && `sheet-stack--${mode}`, fanned && 'sheet-stack--fanned', closed && 'sheet-stack--closed', flood && 'sheet-stack--flood', className), ...rest, ref: setRef, onClickCapture: clickCapture, onPointerDownCapture: pointerDownCapture, children: [stamped, base != null && _jsx(
|
|
179
|
+
return (_jsxs("div", { className: cx('sheet-stack', mode !== 'static' && `sheet-stack--${mode}`, fanned && 'sheet-stack--fanned', closed && 'sheet-stack--closed', flood && 'sheet-stack--flood', className), ...rest, ref: setRef, onClickCapture: clickCapture, onPointerDownCapture: pointerDownCapture, children: [stamped, base != null && _jsx(StackBase, { children: base })] }));
|
|
167
180
|
}
|
package/lib/mcp.mjs
CHANGED
|
@@ -1,13 +1,35 @@
|
|
|
1
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
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
3
|
import { createInterface } from 'node:readline';
|
|
4
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { readFileSync, readdirSync, statSync } 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
8
|
import { GUIDE } from './guide.mjs';
|
|
9
9
|
|
|
10
10
|
const { version } = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'));
|
|
11
|
+
/* A server lives as long as its session — hours, across releases. What it serves must be the catalog as it is NOW (Lh 2026-10-10: a
|
|
12
|
+
session opened the day before did not know CardFeed): `keeper` asks `stamp` before every answer and loads again when it changed. */
|
|
13
|
+
const SOURCES = ['src', 'css', '.design-sync/previews', 'docs', 'lib', 'scripts'];
|
|
14
|
+
const newest = (path) => {
|
|
15
|
+
let st;
|
|
16
|
+
try { st = statSync(path); } catch { return 0; }
|
|
17
|
+
if (!st.isDirectory()) return st.mtimeMs;
|
|
18
|
+
return Math.max(0, ...readdirSync(path).map((name) => newest(join(path, name))));
|
|
19
|
+
};
|
|
20
|
+
/** what the catalog was made from: a live repo's sources (the newest change among them), or the package's own catalog file */
|
|
21
|
+
export const stampOf = (root) => root ? Math.max(...SOURCES.map((dir) => newest(join(root, dir)))) : newest(join(dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'catalog.json'));
|
|
22
|
+
/** the catalog, loaded again whenever `stamp()` answers something new. A load that fails keeps the catalog it had (a source half-edited) and is tried again on the next ask */
|
|
23
|
+
export function keeper(load, stamp) {
|
|
24
|
+
let cat, at;
|
|
25
|
+
return () => {
|
|
26
|
+
const now = stamp();
|
|
27
|
+
if (cat !== undefined && now === at) return cat;
|
|
28
|
+
try { cat = load(); at = now; } catch (e) { if (cat === undefined) throw e; }
|
|
29
|
+
return cat;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
11
33
|
const str = (description) => ({ type: 'string', description });
|
|
12
34
|
const bool = (description) => ({ type: 'boolean', description });
|
|
13
35
|
|
|
@@ -60,12 +82,12 @@ const fail = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, messag
|
|
|
60
82
|
|
|
61
83
|
/** serve the catalog until the client closes stdin; `root` serves a live repo (rebuilt first) instead of the package's */
|
|
62
84
|
export function serve({ root, input = process.stdin, output = process.stdout } = {}) {
|
|
63
|
-
|
|
85
|
+
const current = keeper(() => catalog.load({ root }), () => stampOf(root));
|
|
64
86
|
const text = (r) => typeof r === 'string' ? r : JSON.stringify(r);
|
|
65
87
|
|
|
66
88
|
// the catalog, or nothing: listing the tools must not fail because it cannot be loaded (a call says why)
|
|
67
89
|
function loaded() {
|
|
68
|
-
try { return (
|
|
90
|
+
try { return current(); } catch { return undefined; }
|
|
69
91
|
}
|
|
70
92
|
|
|
71
93
|
function call(params) {
|
|
@@ -75,8 +97,7 @@ export function serve({ root, input = process.stdin, output = process.stdout } =
|
|
|
75
97
|
const missing = tool.inputSchema.required?.find((k) => typeof args[k] !== 'string');
|
|
76
98
|
if (missing) return { content: [{ type: 'text', text: `${tool.name}: "${missing}" is required (a string)` }], isError: true };
|
|
77
99
|
try {
|
|
78
|
-
|
|
79
|
-
const result = tool.run(cat, args);
|
|
100
|
+
const result = tool.run(current(), args);
|
|
80
101
|
return { content: [{ type: 'text', text: text(result) }], ...(result?.error && { isError: true }) };
|
|
81
102
|
} catch (e) { return { content: [{ type: 'text', text: `${tool.name}: ${e.message}` }], isError: true }; }
|
|
82
103
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.80",
|
|
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.",
|
|
@@ -13,6 +13,9 @@
|
|
|
13
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
|
+
"release": "node scripts/publish.mjs",
|
|
17
|
+
"release:check": "node scripts/publish.mjs --check",
|
|
18
|
+
"ci": "node scripts/ci.mjs",
|
|
16
19
|
"prepack": "npm run build",
|
|
17
20
|
"knobs": "node scripts/knobs.mjs",
|
|
18
21
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p gallery/tsconfig.json --noEmit",
|