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 +3 -1
- 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/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
|
|