@usableapp/cardds 0.7.79 → 0.7.81

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.
@@ -26,6 +26,8 @@ export declare function stackPaint(place: StackPlace | undefined, pulling?: bool
26
26
  style?: CSSProperties;
27
27
  busy?: true;
28
28
  };
29
+ /** the head the sheet owns: the card's own child. A card standing in the sheet's body has a head too, and it is that card's */
30
+ export declare const ownHead: (card: Element, target: Element) => Element | null;
29
31
  /**
30
32
  * The card's own pull, refresh and focus. Listeners sit on this card's head and body.
31
33
  * `--_pull` / `--_lift` are written while a finger is down, the same way a single Sheet writes `--_drag`.
@@ -36,6 +36,11 @@ const restPullPx = (card, stack) => {
36
36
  };
37
37
  const readFrame = (el) => { el.getBoundingClientRect(); };
38
38
  const along = (dir, startY, y) => (dir === 'down' ? Math.max(0, y - startY) : Math.max(0, startY - y));
39
+ /** the head the sheet owns: the card's own child. A card standing in the sheet's body has a head too, and it is that card's */
40
+ export const ownHead = (card, target) => {
41
+ const head = target.closest('.card__head');
42
+ return head?.parentElement === card ? head : null;
43
+ };
39
44
  const pullsHead = (target, head) => {
40
45
  if (!(target instanceof Element))
41
46
  return false;
@@ -168,7 +173,7 @@ export function useStackCard(ref, place, swallow) {
168
173
  return;
169
174
  if (scroller.scrollTop > 0)
170
175
  return;
171
- if (e.target instanceof Element && e.target.closest('.card__head'))
176
+ if (e.target instanceof Element && ownHead(card, e.target))
172
177
  return;
173
178
  const slop = pxFromRem(SLOP_REM);
174
179
  const commit = pxFromRem(COMMIT_REM);
@@ -237,7 +242,7 @@ export function useStackCard(ref, place, swallow) {
237
242
  const onTouchStart = (e) => {
238
243
  const t = e.touches[0];
239
244
  const ask = placeRef.current;
240
- const inHead = e.target instanceof Element && !!e.target.closest('.card__head');
245
+ const inHead = e.target instanceof Element && !!ownHead(card, e.target);
241
246
  touch = e.touches.length === 1 && t && ask?.onRefresh && !(ask.open && ask.refreshing) && scroller.scrollTop <= 0 && !inHead
242
247
  ? { x: t.clientX, y: t.clientY, ours: null } : null;
243
248
  };
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
- let cat;
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 (cat ??= catalog.load({ root })); } catch { return undefined; }
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
- cat ??= catalog.load({ root });
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.79",
3
+ "version": "0.7.81",
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,7 +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
- "ci": "npm run validate:inbox && npm run typecheck && npm run check && playwright test",
16
+ "release": "node scripts/publish.mjs",
17
+ "release:check": "node scripts/publish.mjs --check",
18
+ "ci": "node scripts/ci.mjs",
17
19
  "prepack": "npm run build",
18
20
  "knobs": "node scripts/knobs.mjs",
19
21
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p gallery/tsconfig.json --noEmit",