stitch2 0.7.3 → 0.8.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 +7 -2
- package/config.schema.json +5 -0
- package/dist/canvas.html +39 -8
- package/dist/check.js +43 -11
- package/dist/config.d.ts +5 -0
- package/dist/design-md.d.ts +5 -0
- package/dist/design-md.js +15 -0
- package/dist/serve.js +2 -1
- package/dist/typecheck.d.ts +2 -0
- package/package.json +1 -1
- package/skills/stitch2/SKILL.md +3 -0
package/README.md
CHANGED
|
@@ -7,6 +7,10 @@ agents skills that tell them how.
|
|
|
7
7
|
> stitch2 is inspired by [Google Stitch](https://stitch.withgoogle.com) and works with it, but it is an
|
|
8
8
|
> independent project, not affiliated with or endorsed by Google.
|
|
9
9
|
|
|
10
|
+
[](https://youtu.be/mIhP_umyO3o)
|
|
11
|
+
|
|
12
|
+
▶ [Watch the 80-second demo on YouTube](https://youtu.be/mIhP_umyO3o)
|
|
13
|
+
|
|
10
14
|
## Quick start with an AI agent
|
|
11
15
|
|
|
12
16
|
Paste this to your coding agent (Codex, Claude Code, Cursor or any agent that runs commands) in your project:
|
|
@@ -32,11 +36,12 @@ file that already exists. From there the agent spends its effort on the design.
|
|
|
32
36
|
- **Canvas** (`stitch2 canvas`): every screen and its versions on a pan-and-zoom board, a sidebar with search
|
|
33
37
|
and status filters, your DESIGN.md drawn as tokens and rules, the component catalog, and an inspector to
|
|
34
38
|
approve, archive or leave a note on a version. Live reload; copyable references to paste to an agent.
|
|
39
|
+
With a light and a dark theme (DESIGN.md's `light-*` colours), a switch (`T`) shows every screen in either one.
|
|
35
40
|
Pick mode (`P`) highlights any element of a screen and copies a reference with its source line, such as
|
|
36
41
|
`home mobile v1 (design/screens/home/mobile-v1.html:36) <gg-button> “Start Push”`; Shift-click collects
|
|
37
42
|
several to copy at once or add to the version's note.
|
|
38
43
|
- **Checks** (`stitch2 check`): content past the frame edge, labels that wrap, content under fixed bars, fixed bars that overlap each other, tap
|
|
39
|
-
targets (WCAG 2.2), text contrast, text that is not one of DESIGN.md's type levels, capitals without tracking,
|
|
44
|
+
targets (WCAG 2.2), text contrast in every theme, text that is not one of DESIGN.md's type levels, capitals without tracking,
|
|
40
45
|
large numbers in monospace, components built by hand, components that differ between screens, and colours
|
|
41
46
|
written as values instead of DESIGN.md tokens.
|
|
42
47
|
- **Consistency report** (`stitch2 consistency`): every shared component (tab bar, header, buttons, chips, …)
|
|
@@ -167,7 +172,7 @@ component): dialog roots are often zero-sized wrappers, which Playwright reports
|
|
|
167
172
|
| `stitch2 init --name N --primary #hex [options]` | Writes a complete DESIGN.md from a few choices |
|
|
168
173
|
| `stitch2 extract <url or file…>` | Writes a DESIGN.md measured from an existing site or screens |
|
|
169
174
|
| `stitch2 canvas` | The canvas at http://localhost:4400 (`PORT` and `HOST` to change) |
|
|
170
|
-
| `stitch2 check [filter] [--shots] [--strict]` | All checks; `--shots` saves renders to `<design>/renders/` |
|
|
175
|
+
| `stitch2 check [filter] [--shots] [--strict]` | All checks, in every theme; `--shots` saves renders to `<design>/renders/` (`--light.png` for the other theme) |
|
|
171
176
|
| `stitch2 consistency [kind…]` | The consistency report, `<design>/consistency/index.html` |
|
|
172
177
|
| `stitch2 type [filter] [--json]` | The type audit |
|
|
173
178
|
| `stitch2 screens [name] [--status s] [--json]` | Versions, statuses and notes |
|
package/config.schema.json
CHANGED
|
@@ -62,6 +62,11 @@
|
|
|
62
62
|
"description": "Tap targets smaller than this (px) get a warning.",
|
|
63
63
|
"default": 32
|
|
64
64
|
},
|
|
65
|
+
"themes": {
|
|
66
|
+
"type": "array",
|
|
67
|
+
"items": { "type": "string" },
|
|
68
|
+
"description": "The themes every screen must work in, by their data-theme value. Defaults to DESIGN.md's: dark (the plain colours), plus light when it has light-* colours. The canvas switches between them and check runs in each."
|
|
69
|
+
},
|
|
65
70
|
"skills": {
|
|
66
71
|
"type": "array",
|
|
67
72
|
"items": {
|
package/dist/canvas.html
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
.seg { background: var(--bg); border: 1px solid var(--edge); border-radius: 8px; display: flex; gap: 2px; padding: 2px; }
|
|
27
27
|
.seg button { background: none; border: 0; border-radius: 6px; color: var(--muted); cursor: pointer; flex: 1; padding: 4px 6px; white-space: nowrap; }
|
|
28
28
|
.seg button.on { background: var(--panel2); color: var(--text); }
|
|
29
|
+
.seg[hidden] { display: none; }
|
|
29
30
|
#tree { flex: 1; overflow: auto; padding: 6px 8px 16px; }
|
|
30
31
|
#tree h4 { color: var(--faint); font-size: 11px; letter-spacing: .06em; margin: 14px 6px 6px; text-transform: uppercase; }
|
|
31
32
|
.item { align-items: center; background: none; border: 0; border-radius: 6px; color: var(--muted); cursor: pointer; display: flex; gap: 8px; padding: 6px 8px; text-align: left; width: 100%; }
|
|
@@ -105,6 +106,7 @@
|
|
|
105
106
|
<input id="search" placeholder="Search screens… ( / )" />
|
|
106
107
|
<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>
|
|
107
108
|
<div class="seg" id="deviceFilter"><button type="button" data-v="all" class="on">All devices</button><button type="button" data-v="mobile">Mobile</button><button type="button" data-v="desktop">Desktop</button></div>
|
|
109
|
+
<div class="seg" id="themeFilter" hidden title="Show every screen in this theme (T)"></div>
|
|
108
110
|
</header>
|
|
109
111
|
<nav id="tree"></nav>
|
|
110
112
|
<footer>
|
|
@@ -134,7 +136,26 @@ const STATUS_HINT = {
|
|
|
134
136
|
}
|
|
135
137
|
/** What to paste to an agent: the screen, device and version, and the file. */
|
|
136
138
|
let designDir = 'design'
|
|
137
|
-
|
|
139
|
+
// Themes: every screen is shown in the one picked here (data-theme on its document); checks follow it.
|
|
140
|
+
let THEMES = ['dark']
|
|
141
|
+
let theme = localStorage.getItem('stitch2-theme') ?? 'dark'
|
|
142
|
+
const themed = () => THEMES.length > 1
|
|
143
|
+
const themeNote = () => (themed() && theme !== THEMES[0] ? ` in the ${theme} theme` : '')
|
|
144
|
+
const forTheme = (issues) => (issues ?? []).filter((i) => !i.theme || !themed() || i.theme === theme)
|
|
145
|
+
function applyTheme(f) {
|
|
146
|
+
if (!themed()) return
|
|
147
|
+
try { f.iframe.contentDocument.documentElement.dataset.theme = theme } catch {}
|
|
148
|
+
}
|
|
149
|
+
function setTheme(t) {
|
|
150
|
+
theme = t
|
|
151
|
+
localStorage.setItem('stitch2-theme', t)
|
|
152
|
+
for (const b of document.querySelectorAll('#themeFilter button')) b.classList.toggle('on', b.dataset.v === t)
|
|
153
|
+
for (const f of frames.values()) applyTheme(f)
|
|
154
|
+
for (const f of frames.values()) badge(f)
|
|
155
|
+
if (selected) inspector(selected)
|
|
156
|
+
summary()
|
|
157
|
+
}
|
|
158
|
+
const ref = (s) => (s.kind === 'components' ? `component catalog (${designDir}/${s.path})` : `${s.screen} ${s.device} ${s.version}${themeNote()} (${designDir}/${s.path})`)
|
|
138
159
|
let toastTimer
|
|
139
160
|
async function copy(text) {
|
|
140
161
|
try {
|
|
@@ -321,8 +342,8 @@ function measure(f) {
|
|
|
321
342
|
|
|
322
343
|
function badge(f) {
|
|
323
344
|
const b = f.el.querySelector('.badge')
|
|
324
|
-
|
|
325
|
-
|
|
345
|
+
if (!checks.screens?.[f.screen.path]) { b.className = 'badge'; b.textContent = 'not checked'; return }
|
|
346
|
+
const issues = forTheme(checks.screens[f.screen.path])
|
|
326
347
|
const errors = issues.filter((i) => i.severity === 'error').length
|
|
327
348
|
const warnings = issues.length - errors
|
|
328
349
|
b.className = `badge ${errors ? 'bad' : warnings ? 'warn' : 'ok'}`
|
|
@@ -346,7 +367,7 @@ function summary() {
|
|
|
346
367
|
const review = versions.filter((s) => s.status === 'review').length
|
|
347
368
|
const approved = new Set(versions.filter((s) => s.status === 'approved').map((s) => s.screen)).size
|
|
348
369
|
const names = new Set(versions.map((s) => s.screen)).size
|
|
349
|
-
const bad = versions.filter((s) => checks.screens?.[s.path]
|
|
370
|
+
const bad = versions.filter((s) => forTheme(checks.screens?.[s.path]).some((i) => i.severity === 'error')).length
|
|
350
371
|
document.getElementById('summary').textContent = `${names} screens · ${approved} approved · ${review} need review${bad ? ` · ${bad} with errors` : ''}`
|
|
351
372
|
}
|
|
352
373
|
|
|
@@ -360,7 +381,7 @@ function frameFor(s) {
|
|
|
360
381
|
iframe.style.height = `${s.width > 700 ? 800 : 844}px`
|
|
361
382
|
iframe.src = `/${s.path}?pick`
|
|
362
383
|
const f = { el, iframe, screen: s, height: s.width > 700 ? 800 : 844 }
|
|
363
|
-
iframe.addEventListener('load', () => { measure(f); setTimeout(() => measure(f), 800); setTimeout(() => measure(f), 2500) })
|
|
384
|
+
iframe.addEventListener('load', () => { applyTheme(f); measure(f); setTimeout(() => measure(f), 800); setTimeout(() => measure(f), 2500) })
|
|
364
385
|
el.querySelector('.shield').addEventListener('click', () => select(f, false))
|
|
365
386
|
el.querySelector('.shield').addEventListener('dblclick', () => { select(f, false); f.el.classList.add('live') })
|
|
366
387
|
el.querySelector('.label').addEventListener('click', () => select(f, true))
|
|
@@ -387,7 +408,7 @@ function select(f, zoom) {
|
|
|
387
408
|
function inspector(f) {
|
|
388
409
|
const box = document.getElementById('inspector')
|
|
389
410
|
const s = f.screen
|
|
390
|
-
const issues = checks.screens?.[s.path]
|
|
411
|
+
const issues = forTheme(checks.screens?.[s.path])
|
|
391
412
|
box.innerHTML = `<h3><span class="dot ${s.status}"></span><span class="h"></span><button type="button" class="copy" title="Copy a reference to paste to an agent">Copy</button><button type="button" class="close" title="Close (Esc)">×</button></h3>
|
|
392
413
|
<div class="facts"><span class="f"></span> · <a target="_blank"></a></div>
|
|
393
414
|
${s.kind === 'screen' ? `<div class="status">${['approved', 'review', 'draft', 'archived'].map((st) => `<button type="button" data-st="${st}" title="${STATUS_HINT[st]}" class="${s.status === st ? 'on' : ''}">${st === 'approved' ? 'Approve' : st === 'review' ? 'Needs review' : st === 'draft' ? 'Draft' : 'Archive'}</button>`).join('')}</div>
|
|
@@ -494,7 +515,7 @@ function elementRef(f, el) {
|
|
|
494
515
|
const d = describe(el)
|
|
495
516
|
const s = f.screen
|
|
496
517
|
const who = s.kind === 'components' ? 'component catalog' : `${s.screen} ${s.device} ${s.version}`
|
|
497
|
-
return `${who} (${designDir}/${s.path}:${d.line}) <${d.tag}>${d.text ? ` “${d.text}”` : ''}`
|
|
518
|
+
return `${who}${themeNote()} (${designDir}/${s.path}:${d.line}) <${d.tag}>${d.text ? ` “${d.text}”` : ''}`
|
|
498
519
|
}
|
|
499
520
|
function drawBox(f, el, cls, label) {
|
|
500
521
|
const r = el.getBoundingClientRect()
|
|
@@ -602,7 +623,9 @@ events.onmessage = async (e) => {
|
|
|
602
623
|
const html = paths.filter((p) => p.endsWith('.html'))
|
|
603
624
|
if (html.length) await load(false)
|
|
604
625
|
if (paths.includes('DESIGN.md') && document.getElementById('doc').classList.contains('open')) document.querySelector('#doc iframe').contentWindow.location.reload()
|
|
605
|
-
|
|
626
|
+
// Stylesheets and scripts are shared: tokens.css, tailwind.tokens.js, token exports the screens link
|
|
627
|
+
// (tokens.tailwind.css), the components.
|
|
628
|
+
const shared = paths.some((p) => /\.(css|js|mjs)$/.test(p) || p.startsWith('components/'))
|
|
606
629
|
for (const f of frames.values()) {
|
|
607
630
|
if (shared || html.includes(f.screen.path)) {
|
|
608
631
|
f.iframe.contentWindow?.location.reload()
|
|
@@ -658,6 +681,7 @@ addEventListener('keydown', (e) => {
|
|
|
658
681
|
if (e.key === 'p' || e.key === 'P') setPicking(!picking)
|
|
659
682
|
if (e.key === 'Escape') { closeDoc(); select(null) }
|
|
660
683
|
if (e.key === 'f' || e.key === 'F') fit()
|
|
684
|
+
if ((e.key === 't' || e.key === 'T') && themed()) setTheme(THEMES[(THEMES.indexOf(theme) + 1) % THEMES.length])
|
|
661
685
|
if (e.key === '0') zoomAt((innerWidth - SIDE) / 2, innerHeight / 2, 1)
|
|
662
686
|
if (e.key === '+' || e.key === '=') zoomAt((innerWidth - SIDE) / 2, innerHeight / 2, view.z * 1.2)
|
|
663
687
|
if (e.key === '-') zoomAt((innerWidth - SIDE) / 2, innerHeight / 2, view.z / 1.2)
|
|
@@ -681,6 +705,13 @@ fetch('/api/config').then((r) => r.json()).then((c) => {
|
|
|
681
705
|
ORDER = c.order ?? []
|
|
682
706
|
designDir = c.designDir ?? 'design'
|
|
683
707
|
document.getElementById('labName').textContent = c.name
|
|
708
|
+
THEMES = c.themes?.length ? c.themes : ['dark']
|
|
709
|
+
if (!THEMES.includes(theme)) theme = THEMES[0]
|
|
710
|
+
const seg = document.getElementById('themeFilter')
|
|
711
|
+
seg.hidden = !themed()
|
|
712
|
+
seg.innerHTML = THEMES.map((t) => `<button type="button" data-v="${t}">${t[0].toUpperCase()}${t.slice(1)}</button>`).join('')
|
|
713
|
+
for (const b of seg.querySelectorAll('button')) b.onclick = () => setTheme(b.dataset.v)
|
|
714
|
+
setTheme(theme)
|
|
684
715
|
document.title = c.name
|
|
685
716
|
load(true)
|
|
686
717
|
})
|
package/dist/check.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
* edge, labels wrapping onto two lines, content hidden under fixed bars, tiny tap targets, low text
|
|
4
4
|
* contrast, and text styles that are not one of DESIGN.md's type levels. Writes design/checks.json, which
|
|
5
5
|
* the canvas shows as badges.
|
|
6
|
+
* Every screen is checked in its own theme, then again in each other theme DESIGN.md defines (designThemes):
|
|
7
|
+
* colours change there, so contrast is checked again, tagged with the theme.
|
|
6
8
|
* Usage: stitch2 check [path filter…] [--shots] [--strict]
|
|
7
|
-
* --shots also save page-tall renders to design/renders/
|
|
9
|
+
* --shots also save page-tall renders to design/renders/ (one per theme)
|
|
8
10
|
* --strict exit 1 when any screen has errors (for CI and agent loops)
|
|
9
11
|
*/
|
|
10
12
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
@@ -13,7 +15,7 @@ import { chromium } from '@playwright/test';
|
|
|
13
15
|
import { colourIssues } from './colours.js';
|
|
14
16
|
import { config } from './config.js';
|
|
15
17
|
import { collectComponents, collectOptions, consistencyIssues, reuseIssues, } from './consistency.js';
|
|
16
|
-
import { typeLevels } from './design-md.js';
|
|
18
|
+
import { designThemes, typeLevels } from './design-md.js';
|
|
17
19
|
import { listScreens, ROOT } from './screens.js';
|
|
18
20
|
import { startServer } from './serve.js';
|
|
19
21
|
import { collectText, typeIssues } from './typecheck.js';
|
|
@@ -24,6 +26,7 @@ const strict = args.includes('--strict');
|
|
|
24
26
|
const filters = args.filter((a) => !a.startsWith('--'));
|
|
25
27
|
const screens = listScreens().filter((s) => !filters.length || filters.some((f) => s.path.includes(f)));
|
|
26
28
|
const levels = typeLevels();
|
|
29
|
+
const themes = designThemes();
|
|
27
30
|
const collect = collectOptions();
|
|
28
31
|
const server = await startServer(0);
|
|
29
32
|
const browser = await chromium.launch();
|
|
@@ -39,23 +42,52 @@ for (const s of screens) {
|
|
|
39
42
|
await page.waitForTimeout(600);
|
|
40
43
|
const found = await page.evaluate(collectComponents, collect);
|
|
41
44
|
components[s.path] = found.components;
|
|
45
|
+
const own = await page.evaluate(() => document.documentElement.dataset.theme || '');
|
|
46
|
+
const base = own || themes[0];
|
|
47
|
+
const layout = await page.evaluate(audit, { comfortable: config.comfortableTarget });
|
|
48
|
+
for (const i of layout)
|
|
49
|
+
if (i.type === 'contrast')
|
|
50
|
+
i.theme = base;
|
|
51
|
+
// The other themes: what the colours change (contrast), plus anything that only shows there.
|
|
52
|
+
const seen = new Set(layout.map((i) => `${i.type} ${i.selector}`));
|
|
53
|
+
const themed = [];
|
|
54
|
+
for (const theme of themes.filter((t) => t !== base)) {
|
|
55
|
+
await setTheme(theme);
|
|
56
|
+
for (const i of await page.evaluate(audit, { comfortable: config.comfortableTarget }))
|
|
57
|
+
if (i.type === 'contrast' || !seen.has(`${i.type} ${i.selector}`))
|
|
58
|
+
themed.push({ ...i, theme, message: `${theme} theme: ${i.message}` });
|
|
59
|
+
if (shots)
|
|
60
|
+
await shoot(s, theme);
|
|
61
|
+
}
|
|
62
|
+
if (themes.length > 1)
|
|
63
|
+
await setTheme(base);
|
|
42
64
|
results[s.path] = [
|
|
43
|
-
...
|
|
65
|
+
...layout,
|
|
66
|
+
...themed,
|
|
44
67
|
...typeIssues(levels, await page.evaluate(collectText)),
|
|
45
68
|
...reuseIssues(s.path, found),
|
|
46
69
|
...colourIssues(s.path),
|
|
47
70
|
];
|
|
48
|
-
if (shots)
|
|
49
|
-
|
|
50
|
-
await page.setViewportSize({ width: s.width, height: Math.max(844, Math.min(height, 6000)) });
|
|
51
|
-
mkdirSync(join(ROOT, 'renders'), { recursive: true });
|
|
52
|
-
await page.screenshot({
|
|
53
|
-
path: join(ROOT, 'renders', s.path.replaceAll('/', '__').replace(/\.html$/, '.png')),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
71
|
+
if (shots)
|
|
72
|
+
await shoot(s);
|
|
56
73
|
}
|
|
57
74
|
await browser.close();
|
|
58
75
|
server.close();
|
|
76
|
+
async function setTheme(theme) {
|
|
77
|
+
await page.evaluate((t) => {
|
|
78
|
+
document.documentElement.dataset.theme = t;
|
|
79
|
+
}, theme);
|
|
80
|
+
await page.waitForTimeout(150);
|
|
81
|
+
}
|
|
82
|
+
/** A page-tall render; renders in another theme than the screen's own get a --<theme> suffix. */
|
|
83
|
+
async function shoot(s, theme) {
|
|
84
|
+
const height = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
85
|
+
await page.setViewportSize({ width: s.width, height: Math.max(844, Math.min(height, 6000)) });
|
|
86
|
+
mkdirSync(join(ROOT, 'renders'), { recursive: true });
|
|
87
|
+
const name = s.path.replaceAll('/', '__').replace(/\.html$/, theme ? `--${theme}.png` : '.png');
|
|
88
|
+
await page.screenshot({ path: join(ROOT, 'renders', name) });
|
|
89
|
+
await page.setViewportSize({ width: s.width, height: 844 });
|
|
90
|
+
}
|
|
59
91
|
// Merge with the last results, so a filtered run still compares components with every other screen.
|
|
60
92
|
let previous = {};
|
|
61
93
|
try {
|
package/dist/config.d.ts
CHANGED
|
@@ -20,6 +20,11 @@ export interface LabConfig {
|
|
|
20
20
|
order: string[];
|
|
21
21
|
/** Tap targets smaller than this (px) get a warning; under WCAG's 24px they are errors when crowded. */
|
|
22
22
|
comfortableTarget: number;
|
|
23
|
+
/**
|
|
24
|
+
* The themes every screen must work in, by their data-theme value. Defaults to DESIGN.md's: "dark" (the plain
|
|
25
|
+
* colours) and "light" when it has light-* colours. The canvas switches between them; check runs in each.
|
|
26
|
+
*/
|
|
27
|
+
themes?: string[];
|
|
23
28
|
/** Skill folders a blind-trial sandbox copies. */
|
|
24
29
|
skills: string[];
|
|
25
30
|
/** Paths under designDir a sandbox leaves out besides screens (notes that would give the trial away). */
|
package/dist/design-md.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ export interface Design {
|
|
|
15
15
|
components: Record<string, Record<string, string>>;
|
|
16
16
|
}
|
|
17
17
|
export declare function readDesign(file?: string): Design;
|
|
18
|
+
/**
|
|
19
|
+
* The themes screens must work in (their data-theme values): the config's, or DESIGN.md's own, where the plain
|
|
20
|
+
* colours are the dark theme and light-* colours make a light one.
|
|
21
|
+
*/
|
|
22
|
+
export declare function designThemes(): string[];
|
|
18
23
|
/** A type level in pixels, for comparing with computed styles. */
|
|
19
24
|
export interface Level {
|
|
20
25
|
name: string;
|
package/dist/design-md.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { parse } from 'yaml';
|
|
8
|
+
import { config } from './config.js';
|
|
8
9
|
import { ROOT } from './screens.js';
|
|
9
10
|
export function readDesign(file = join(ROOT, 'DESIGN.md')) {
|
|
10
11
|
const text = readFileSync(file, 'utf8');
|
|
@@ -31,6 +32,20 @@ export function readDesign(file = join(ROOT, 'DESIGN.md')) {
|
|
|
31
32
|
components: raw.components ?? {},
|
|
32
33
|
};
|
|
33
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* The themes screens must work in (their data-theme values): the config's, or DESIGN.md's own, where the plain
|
|
37
|
+
* colours are the dark theme and light-* colours make a light one.
|
|
38
|
+
*/
|
|
39
|
+
export function designThemes() {
|
|
40
|
+
if (config.themes?.length)
|
|
41
|
+
return config.themes;
|
|
42
|
+
try {
|
|
43
|
+
return Object.keys(readDesign().colors).some((k) => k.startsWith('light-')) ? ['dark', 'light'] : ['dark'];
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return ['dark'];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
34
49
|
export function typeLevels(design = readDesign()) {
|
|
35
50
|
return Object.entries(design.typography).map(([name, t]) => {
|
|
36
51
|
const size = Number.parseFloat(String(t.fontSize));
|
package/dist/serve.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createServer } from 'node:http';
|
|
|
9
9
|
import { dirname, extname, join, normalize } from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import { CONFIG_FILE, config } from './config.js';
|
|
12
|
+
import { designThemes } from './design-md.js';
|
|
12
13
|
import { designPage } from './design-page.js';
|
|
13
14
|
import { annotate } from './pick.js';
|
|
14
15
|
/**
|
|
@@ -114,7 +115,7 @@ export function startServer(port) {
|
|
|
114
115
|
return;
|
|
115
116
|
}
|
|
116
117
|
if (url.pathname === '/api/config')
|
|
117
|
-
return json(config);
|
|
118
|
+
return json({ ...config, themes: designThemes() });
|
|
118
119
|
if (url.pathname === '/api/screens')
|
|
119
120
|
return json(listScreens());
|
|
120
121
|
if (url.pathname === '/api/checks') {
|
package/dist/typecheck.d.ts
CHANGED
package/package.json
CHANGED
package/skills/stitch2/SKILL.md
CHANGED
|
@@ -47,6 +47,9 @@ References, read before the first screen:
|
|
|
47
47
|
5. **Check** `stitch2 check <path> --shots --strict`. Fix every error (overflow, wrapped labels, content under
|
|
48
48
|
fixed bars, fixed bars overlapping each other, tap targets, contrast, cramped text) and every warning about type (`type-*`), hand-built
|
|
49
49
|
components (`reuse`), components that differ from the catalog (`consistency`) and colour values (`color`).
|
|
50
|
+
When DESIGN.md has a light and a dark theme, every screen is checked in both: an issue marked with a theme
|
|
51
|
+
("light theme: Low contrast…") is fixed in DESIGN.md's colours for that theme when it shows on many
|
|
52
|
+
screens, or in the screen when it is one element. Look at both renders (`--light.png`).
|
|
50
53
|
6. **Look and critique.** View the render in `<design>/renders/` (crop the first screenful). Score it with the
|
|
51
54
|
rubric in principles.md, fix what fails, repeat 5–6 at most three times.
|
|
52
55
|
7. **Hand over.** Set the version's status to `review`, point the user to it on the canvas, and report what
|