stitch2 0.19.2 → 0.20.0

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/README.md CHANGED
@@ -69,7 +69,9 @@ file that already exists. From there the agent spends its effort on the design.
69
69
  rules for scripts without capitals) and a Tailwind config, from DESIGN.md's front matter, plus Material 3
70
70
  colour roles from a seed colour. **Exports** write the same tokens for your app, so DESIGN.md drives the
71
71
  product as well as the screens: plain CSS variables, a Tailwind v4 theme (`@theme` plus `@utility type-*`), or
72
- W3C Design Tokens JSON for Style Dictionary and native platforms. Aliases keep your app's own variable names:
72
+ W3C Design Tokens JSON for Style Dictionary and native platforms. The CSS and Tailwind exports also set the
73
+ cursor: the hand over every enabled control, not-allowed over a disabled one (Tailwind v4 gives buttons the
74
+ arrow). Aliases keep your app's own variable names:
73
75
 
74
76
  ```json
75
77
  "exports": [
package/dist/canvas.html CHANGED
@@ -139,7 +139,7 @@
139
139
  <input id="search" placeholder="Search screens… ( / )" />
140
140
  <div class="seg" id="statusFilter"><button type="button" data-v="active" class="on" title="Everything except archived versions">Active</button><button type="button" data-v="approved" title="The reference versions agents build on">Approved</button><button type="button" data-v="review" title="Finished by an agent, waiting for your decision">Needs review</button><button type="button" data-v="all" title="Including archived versions">All</button></div>
141
141
  <details id="versionFilter" class="multi" hidden title="Show only the versions you pick (several at once), or all of them"><summary>All versions</summary><div class="menu"></div></details>
142
- <details id="variantFilter" class="multi" hidden title="Show only the variants you pick: mobile, desktop (several at once), or all of them"><summary>All variants</summary><div class="menu"></div></details>
142
+ <details id="variantFilter" class="multi" hidden title="Show only the variants you pick: mobile, tablet, desktop (several at once), or all of them"><summary>All variants</summary><div class="menu"></div></details>
143
143
  <select id="themeFilter" class="pick" hidden title="Show every screen in this theme (T)" aria-label="Theme"></select>
144
144
  <select id="widthFilter" class="pick" title="Show every screen at the narrowest or widest window its variant covers, or at one breakpoint (W)" aria-label="Width"></select>
145
145
  <select id="langFilter" class="pick" title="Show every screen in this language (L)" aria-label="Language"></select>
package/dist/exports.d.ts CHANGED
@@ -21,5 +21,10 @@ export interface Tokens {
21
21
  root: Record<string, string>;
22
22
  header: string;
23
23
  }
24
+ /**
25
+ * The cursor over controls, in @layer base so any utility (cursor-grab, cursor-text) or component rule wins.
26
+ * check reports a catalog control that still shows another cursor (state-cursor, states.ts).
27
+ */
28
+ export declare const POINTER: string;
24
29
  /** Writes every export in the config; returns their paths. */
25
30
  export declare function writeExports(t: Tokens, list?: TokenExport[]): string[];
package/dist/exports.js CHANGED
@@ -8,6 +8,8 @@
8
8
  * Motion: ease-<name> classes, duration-<name> utilities, and Tailwind's default transition (the
9
9
  * plain transition, transition-colors…) set to the first duration and ease-standard.
10
10
  * dtcg W3C Design Tokens JSON, for Style Dictionary and other platforms (iOS, Android)
11
+ * css and tailwind4 also carry the pointer rule (POINTER): every enabled control shows the hand cursor, a disabled
12
+ * one shows not-allowed. Tailwind v4's preflight gives buttons the arrow, so without it nothing looks clickable.
11
13
  * "aliases" maps the app's own variable names to DESIGN.md colours ({ "background": "floor" } writes
12
14
  * --background: var(--<prefix>-floor) in each theme), so an existing theme (shadcn's, say) keeps its names.
13
15
  */
@@ -15,6 +17,25 @@ import { mkdirSync, writeFileSync } from 'node:fs';
15
17
  import { dirname, resolve } from 'node:path';
16
18
  import { BASE, config } from './config.js';
17
19
  const p = () => config.prefix;
20
+ /**
21
+ * The cursor over controls, in @layer base so any utility (cursor-grab, cursor-text) or component rule wins.
22
+ * check reports a catalog control that still shows another cursor (state-cursor, states.ts).
23
+ */
24
+ export const POINTER = [
25
+ '/* Every enabled control shows the hand cursor, a disabled one not-allowed (Tailwind v4 gives buttons the arrow). */',
26
+ '@layer base {',
27
+ ' button:not(:disabled), [role="button"]:not([aria-disabled="true"]), [role="switch"]:not([aria-disabled="true"]),',
28
+ ' [role="tab"]:not([aria-disabled="true"]), [role="checkbox"]:not([aria-disabled="true"]),',
29
+ ' [role="radio"]:not([aria-disabled="true"]), [role="option"]:not([aria-disabled="true"]),',
30
+ ' [role="menuitem"]:not([aria-disabled="true"]), summary, select:not(:disabled), label[for],',
31
+ ' input:is([type="checkbox"], [type="radio"], [type="range"], [type="file"], [type="color"], [type="submit"], [type="button"], [type="reset"]):not(:disabled) {',
32
+ ' cursor: pointer;',
33
+ ' }',
34
+ ' :disabled, [aria-disabled="true"] {',
35
+ ' cursor: not-allowed;',
36
+ ' }',
37
+ '}',
38
+ ].join('\n');
18
39
  function themeBlocks(t, e) {
19
40
  const dark = e.themes?.dark ?? ":root, [data-theme='dark']";
20
41
  const light = e.themes?.light ?? "[data-theme='light']";
@@ -35,7 +56,7 @@ function themeBlocks(t, e) {
35
56
  }
36
57
  function css(t, e) {
37
58
  const type = Object.entries(t.type).map(([n, d]) => `.type-${n} { ${d.join('; ')}; }`);
38
- return [t.header, ...themeBlocks(t, e), ...type, ''].join('\n\n');
59
+ return [t.header, ...themeBlocks(t, e), ...type, POINTER, ''].join('\n\n');
39
60
  }
40
61
  function tailwind4(t, e) {
41
62
  const families = Object.values(t.design.typography).map((x) => x.fontFamily ?? '');
@@ -52,7 +73,7 @@ function tailwind4(t, e) {
52
73
  const durations = Object.keys(t.design.motion)
53
74
  .filter((k) => k.startsWith('duration-'))
54
75
  .map((k) => `@utility ${k} {\n transition-duration: var(--${p()}-${k});\n animation-duration: var(--${p()}-${k});\n}`);
55
- return [t.header, ...themeBlocks(t, e), `@theme inline {\n${theme.join('\n')}\n}`, ...type, ...durations, ''].join('\n\n');
76
+ return [t.header, ...themeBlocks(t, e), `@theme inline {\n${theme.join('\n')}\n}`, ...type, ...durations, POINTER, ''].join('\n\n');
56
77
  }
57
78
  /** @theme entries for motion: --ease-<name> (Tailwind's ease-* classes) and the default transition. */
58
79
  function motionTheme(motion) {
package/dist/list.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Lists every screen version, variant by variant (mobile, desktop), with its status and the reviewer's note, for agents (and anyone without the
2
+ * Lists every screen version, variant by variant (mobile, tablet, desktop), with its status and the reviewer's note, for agents (and anyone without the
3
3
  * canvas open). Approved versions are the reference; review versions wait for the user; notes are feedback.
4
4
  * Ends with what makes the current version unclear (versionProblems); --strict fails on it.
5
5
  * Usage: stitch2 screens [screen name] [--status approved|review|draft|archived] [--json] [--strict]
package/dist/sandbox.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * versions, so their work is not anchored to what exists ("fresh eyes"), and where changes to the skills can
4
4
  * be tested on output that only the rules produced.
5
5
  *
6
- * stitch2 sandbox create <dir> <brief.md>… [--device mobile|desktop] [--context <screen>…]
6
+ * stitch2 sandbox create <dir> <brief.md>… [--device mobile|tablet|desktop] [--context <screen>…]
7
7
  * The project's config, its design folder without screens (DESIGN.md, tokens, components, the catalog and
8
8
  * templates), the skills from the config, a copy of stitch2 and the briefs (one screen each, named after the
9
9
  * file). --context adds the approved versions of other screens, to stay consistent with them.
@@ -34,7 +34,7 @@ if (sub === 'create') {
34
34
  const context = flag('context') ?? [];
35
35
  const [dir, ...briefs] = argv;
36
36
  if (!dir || !briefs.length) {
37
- console.error('usage: stitch2 sandbox create <dir> <brief.md>… [--device mobile|desktop] [--context <screen>…]');
37
+ console.error('usage: stitch2 sandbox create <dir> <brief.md>… [--device mobile|tablet|desktop] [--context <screen>…]');
38
38
  process.exit(1);
39
39
  }
40
40
  const out = resolve(dir);
package/dist/screens.d.ts CHANGED
@@ -25,7 +25,7 @@ export interface Screen {
25
25
  * decides it for all of its versions; without one a screen is its own page.
26
26
  */
27
27
  page: string;
28
- device: 'mobile' | 'desktop';
28
+ device: 'mobile' | 'tablet' | 'desktop';
29
29
  /** Where the version came from: stitch, lab or trial. */
30
30
  source: 'stitch' | 'lab' | 'trial';
31
31
  /** Short version label: "v3", "Stitch", "Trial 2". */
package/dist/screens.js CHANGED
@@ -41,7 +41,7 @@ export function listScreens() {
41
41
  const source = (meta(html, tag('source')) ??
42
42
  (under(path, config.stitchDir) ? 'stitch' : archive ? 'trial' : 'lab'));
43
43
  // design/screens/<screen>/<device>-v<N>.html, or older files named after the screen.
44
- const versioned = /^(mobile|desktop)-v(\d+)$/.exec(name);
44
+ const versioned = /^(mobile|tablet|desktop)-v(\d+)$/.exec(name);
45
45
  out.push({
46
46
  path,
47
47
  group,
@@ -53,7 +53,7 @@ export function listScreens() {
53
53
  screen: meta(html, tag('screen')) ?? (versioned ? group.split('/').pop() : name),
54
54
  page: meta(html, tag('page')) ?? '',
55
55
  device: (meta(html, tag('device')) ??
56
- (versioned?.[1] || (width >= 768 ? 'desktop' : 'mobile'))),
56
+ (versioned?.[1] || (width >= 1000 ? 'desktop' : width >= 600 ? 'tablet' : 'mobile'))),
57
57
  source,
58
58
  version: meta(html, tag('version')) ??
59
59
  (versioned
package/dist/states.js CHANGED
@@ -49,7 +49,8 @@ function findControls(opts) {
49
49
  h.setAttribute('data-s2-state', String(out.length));
50
50
  const field = h.matches('input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]), textarea, select, [contenteditable="true"]');
51
51
  const chosen = ['aria-pressed', 'aria-selected', 'aria-checked'].some((a) => h.getAttribute(a) === 'true') || (h.hasAttribute('aria-current') && h.getAttribute('aria-current') !== 'false');
52
- out.push({ i: out.length, name, label, selector: path(h), field, chosen });
52
+ const clickable = h.matches('button, a[href], summary, select, [role="button"], [role="switch"], [role="tab"], [role="checkbox"], [role="radio"], [role="option"], [role="menuitem"], input:is([type="checkbox"], [type="radio"], [type="range"], [type="file"], [type="color"], [type="submit"], [type="button"], [type="reset"])');
53
+ out.push({ i: out.length, name, label, selector: path(h), field, chosen, clickable });
53
54
  }
54
55
  }
55
56
  return out;
@@ -89,7 +90,7 @@ function lookOf(i) {
89
90
  const large = parseFloat(cs.fontSize) >= 24 || (parseFloat(cs.fontSize) >= 18.66 && Number(cs.fontWeight) >= 700);
90
91
  worst = Math.min(worst, ratio / (large ? 3 : 4.5));
91
92
  }
92
- return { look, contrast: worst };
93
+ return { look, contrast: worst, cursor: getComputedStyle(el).cursor };
93
94
  }
94
95
  /**
95
96
  * The states of every control in the catalog open in the page, in the current theme. With crop, saves a picture
@@ -147,6 +148,10 @@ export async function catalogStates(page, path, opts) {
147
148
  else if (noPress)
148
149
  add('state-pressed', who + ': pressing adds nothing to hover, so a tap gives no feedback (DESIGN.md, pressed)');
149
150
  }
151
+ // The hand says "this does something" before the click; the arrow over a button reads as plain text.
152
+ const cursor = looks.hover?.cursor;
153
+ if (c.clickable && cursor && cursor !== 'pointer')
154
+ add('state-cursor', who + ': the pointer shows "' + cursor + '" over it, not the hand, so it doesn\'t look clickable (stitch2 tokens writes the rule; a component overrides it)');
150
155
  for (const state of ['hover', 'focus', 'pressed'])
151
156
  if ((looks[state]?.contrast ?? 21) < 1 && (looks.rest?.contrast ?? 21) >= 1) {
152
157
  add('state-contrast', who + ': low text contrast when ' + (state === 'focus' ? 'focused' : state === 'hover' ? 'hovered' : 'pressed'));
package/dist/tokens.js CHANGED
@@ -11,7 +11,7 @@ import { join } from 'node:path';
11
11
  import { argbFromHex, Hct, hexFromArgb, MaterialDynamicColors, SchemeContent, SchemeExpressive, SchemeFidelity, SchemeFruitSalad, SchemeMonochrome, SchemeNeutral, SchemeRainbow, SchemeTonalSpot, SchemeVibrant, } from '@material/material-color-utilities';
12
12
  import { config as lab } from './config.js';
13
13
  import { readDesign } from './design-md.js';
14
- import { writeExports } from './exports.js';
14
+ import { POINTER, writeExports } from './exports.js';
15
15
  import { ROOT } from './screens.js';
16
16
  const SCHEMES = {
17
17
  fidelity: SchemeFidelity,
@@ -98,6 +98,7 @@ writeFileSync(join(ROOT, 'tokens.css'), [
98
98
  : []),
99
99
  '/* Typography levels: one class sets family, size, line height, weight, tracking and figures. */',
100
100
  typeCss.join('\n'),
101
+ POINTER,
101
102
  '',
102
103
  ].join('\n\n'));
103
104
  const colors = Object.fromEntries(Object.keys(dark).map((k) => [k, `var(--${lab.prefix}-${k})`]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitch2",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
4
4
  "description": "A design canvas, layout and type checks, component consistency and screen versioning for agent-built UI, on Google's DESIGN.md format.",
5
5
  "license": "MIT",
6
6
  "author": "ifokeev",
@@ -35,14 +35,20 @@ Every interactive component has its states, defined once in the component and de
35
35
  | Focus (keyboard) | a ring, never removed without a replacement |
36
36
  | Selected / current | shown by the accent or a raised surface; hover and press may stay as they are |
37
37
  | Disabled | faint, no hover or press change, the reason nearby |
38
+ | Cursor (pointer only) | the hand over everything that acts when clicked (buttons, links, chips, rows that open something, switches, selects); not-allowed over a disabled control; the text cursor only in fields |
39
+
40
+ The cursor comes from one rule, not from each component: `stitch2 tokens` writes it into tokens.css and every
41
+ `css` and `tailwind4` export, in `@layer base`, because Tailwind v4's preflight gives buttons the arrow. A
42
+ clickable element that is not a button or a link gets `role="button"` (and a keyboard handler), which also gives
43
+ it the hand. Override the cursor only with a reason (`cursor-grab` on a drag handle).
38
44
 
39
45
  The catalog shows each component in each of its **data states** (selected, done, active, disabled, error, with
40
46
  and without an icon). Hover, focus and pressed are not drawn by hand: `stitch2 check` on the catalog forces them on
41
47
  every control and writes the **States** sheet (canvas sidebar), each control at rest, hovered, focused and
42
48
  pressed side by side. It reports a control that gives no feedback (**state-feedback**, **state-hover**,
43
49
  **state-pressed**: pressing adds nothing to hover, so a tap shows nothing) and text that loses contrast in a state
44
- (**state-contrast**). Fix them in the component, once. Selected and current controls and text fields are exempt
45
- from the feedback checks.
50
+ (**state-contrast**), and a clickable control the pointer doesn't show as clickable (**state-cursor**). Fix them
51
+ in the component, once. Selected and current controls and text fields are exempt from the feedback checks.
46
52
 
47
53
  ## Components from the app
48
54