stitch2 0.19.3 → 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/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/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.3",
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