stitch2 0.19.3 → 0.20.1
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 +3 -1
- package/dist/audit.js +11 -7
- package/dist/exports.d.ts +5 -0
- package/dist/exports.js +23 -2
- package/dist/states.js +7 -2
- package/dist/tokens.js +2 -1
- package/package.json +1 -1
- package/skills/stitch2/references/components.md +8 -2
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.
|
|
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/audit.js
CHANGED
|
@@ -218,6 +218,14 @@ export async function audit(opts) {
|
|
|
218
218
|
window.scrollTo(0, 0);
|
|
219
219
|
// 4. Tap targets: WCAG 2.2 asks for at least 24px; smaller than the config's comfortableTarget is a warning.
|
|
220
220
|
const interactive = all.filter((el) => el.matches('button, a[href], input:not([type=hidden]), select, textarea, [role=button], [role=tab], [role=switch], [role=checkbox]'));
|
|
221
|
+
// Fixed and sticky layers float over the scrolling page (a running-workout panel over cards): content slides
|
|
222
|
+
// under them while scrolling, so they never crowd in-flow targets. Tap spacing (6d) already compares layers.
|
|
223
|
+
const layerOf = (el) => {
|
|
224
|
+
for (let e = el; e; e = e.parentElement)
|
|
225
|
+
if (['fixed', 'sticky'].includes(getComputedStyle(e).position))
|
|
226
|
+
return e;
|
|
227
|
+
return null;
|
|
228
|
+
};
|
|
221
229
|
for (const el of interactive) {
|
|
222
230
|
if (interactive.some((o) => o !== el && o.contains(el)))
|
|
223
231
|
continue;
|
|
@@ -229,7 +237,7 @@ export async function audit(opts) {
|
|
|
229
237
|
const cx = r.left + r.width / 2;
|
|
230
238
|
const cy = r.top + r.height / 2;
|
|
231
239
|
const crowded = interactive.some((o) => {
|
|
232
|
-
if (o === el || o.contains(el) || el.contains(o))
|
|
240
|
+
if (o === el || o.contains(el) || el.contains(o) || layerOf(o) !== layerOf(el))
|
|
233
241
|
return false;
|
|
234
242
|
const q = o.getBoundingClientRect();
|
|
235
243
|
const dx = Math.max(q.left - cx, 0, cx - q.right);
|
|
@@ -250,12 +258,8 @@ export async function audit(opts) {
|
|
|
250
258
|
// 4b. Controls never overlap one another (a button squeezed over its neighbour). Controls inside one another
|
|
251
259
|
// and invisible overlays (a native picker laid over a row) are fine; fixed and sticky layers (a docked timer,
|
|
252
260
|
// a sticky Save bar floating over a list) are compared apart from the scrolling page.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (['fixed', 'sticky'].includes(getComputedStyle(e).position))
|
|
256
|
-
return e;
|
|
257
|
-
return null;
|
|
258
|
-
};
|
|
261
|
+
// layerOf is defined with the tap targets above; fixed and sticky layers are compared apart here too.
|
|
262
|
+
const layer = layerOf;
|
|
259
263
|
// The part of a control its scrolling or clipping ancestors show: a link scrolled out of a nav strip is not drawn
|
|
260
264
|
// where its box would be.
|
|
261
265
|
const visibleBox = (el) => {
|
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
|
-
|
|
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
|
@@ -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**)
|
|
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
|
|