stitch2 0.22.12 → 0.23.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 +2 -0
- package/config.schema.json +25 -0
- package/dist/canvas.html +54 -1
- package/dist/config.d.ts +38 -0
- package/dist/config.js +38 -0
- package/dist/serve.js +3 -2
- package/dist/shots.d.ts +2 -0
- package/dist/shots.js +6 -1
- package/package.json +1 -1
- package/skills/stitch2/references/screens.md +21 -3
package/README.md
CHANGED
|
@@ -42,6 +42,8 @@ file that already exists. From there the agent spends its effort on the design.
|
|
|
42
42
|
A width switch (`W`) shows every screen at the narrowest or widest window its layout covers.
|
|
43
43
|
With languages configured, a Language switch (`L`) shows every screen in one of them, and the Languages page
|
|
44
44
|
shows a screen side by side in each.
|
|
45
|
+
With app states configured (`modes`: signed in or out, a plan, a role), a dropdown for each (`M` cycles the first)
|
|
46
|
+
shows every screen in one of them, live pages and pictures alike, or as drawn.
|
|
45
47
|
Pick mode (`P`) highlights any element of a screen and copies a reference with its source line, such as
|
|
46
48
|
`home mobile v1 (design/screens/home/mobile-v1.html:36) <gg-button> “Start Push”`; Shift-click collects
|
|
47
49
|
several to copy at once or add to the version's note.
|
package/config.schema.json
CHANGED
|
@@ -77,6 +77,31 @@
|
|
|
77
77
|
"items": { "type": "string" },
|
|
78
78
|
"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."
|
|
79
79
|
},
|
|
80
|
+
"modes": {
|
|
81
|
+
"type": "array",
|
|
82
|
+
"description": "App states every screen can be shown in, picked from the canvas sidebar: signed in or out, a plan, a role. The pick is an attribute on each screen's <html>, so the screens' own CSS and scripts answer to it. The dropdown also offers the screens as drawn, which leaves each one as its file has it.",
|
|
83
|
+
"items": {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"additionalProperties": false,
|
|
86
|
+
"required": ["name", "values"],
|
|
87
|
+
"properties": {
|
|
88
|
+
"name": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$", "description": "The id: lowercase letters, digits and hyphens. The canvas remembers the pick under it." },
|
|
89
|
+
"label": { "type": "string", "description": "Shown before each value in the dropdown. Defaults to the name, capitalized." },
|
|
90
|
+
"attribute": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$", "description": "The attribute on the screen's <html> that carries the value. Defaults to data-<name>." },
|
|
91
|
+
"values": {
|
|
92
|
+
"type": "array",
|
|
93
|
+
"minItems": 1,
|
|
94
|
+
"description": "What the canvas can show every screen as: a value, or a value with its label.",
|
|
95
|
+
"items": {
|
|
96
|
+
"oneOf": [
|
|
97
|
+
{ "type": "string" },
|
|
98
|
+
{ "type": "object", "additionalProperties": false, "required": ["value"], "properties": { "value": { "type": "string" }, "label": { "type": "string" } } }
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
80
105
|
"i18n": {
|
|
81
106
|
"type": "object",
|
|
82
107
|
"additionalProperties": false,
|
package/dist/canvas.html
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
/* Theme and language: native dropdowns drawn like the versions and variants ones. */
|
|
45
45
|
.pick { appearance: none; background: var(--bg) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5l5 5 5-5' fill='none' stroke='%23a1a1aa' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat right 10px center / 12px 8px; border: 1px solid var(--edge); border-radius: 8px; color: var(--text); cursor: pointer; font: inherit; padding: 5px 30px 5px 8px; width: 100%; }
|
|
46
46
|
.pick[hidden] { display: none; }
|
|
47
|
+
#modeFilters { display: contents; }
|
|
47
48
|
#tree { flex: 1; overflow: auto; padding: 6px 8px 16px; }
|
|
48
49
|
#tree h4 { color: var(--faint); font-size: 11px; letter-spacing: .06em; margin: 14px 6px 6px; text-transform: uppercase; }
|
|
49
50
|
.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%; }
|
|
@@ -142,6 +143,7 @@
|
|
|
142
143
|
<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>
|
|
143
144
|
<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>
|
|
144
145
|
<select id="themeFilter" class="pick" hidden title="Show every screen in this theme (T)" aria-label="Theme"></select>
|
|
146
|
+
<div id="modeFilters"></div>
|
|
145
147
|
<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>
|
|
146
148
|
<select id="langFilter" class="pick" title="Show every screen in this language (L)" aria-label="Language"></select>
|
|
147
149
|
</header>
|
|
@@ -179,13 +181,20 @@ let designDir = 'design'
|
|
|
179
181
|
let THEMES = ['dark']
|
|
180
182
|
let theme = localStorage.getItem('stitch2-theme') ?? 'dark'
|
|
181
183
|
const themed = () => THEMES.length > 1
|
|
184
|
+
// Modes: app states (signed in or out, a plan, a role) every screen can be shown in. Each sets an attribute on the
|
|
185
|
+
// screen's document, like the theme; "As drawn" leaves a screen as its file has it. Pictures of the screens that are
|
|
186
|
+
// not live ask the server for the same states (?mode=name:value).
|
|
187
|
+
let MODES = []
|
|
188
|
+
const modePick = {} // mode name -> picked value, '' for as drawn
|
|
189
|
+
const modeValueLabel = (m, v) => (v ? (m.values.find((x) => x.value === v)?.label ?? v) : 'As drawn')
|
|
190
|
+
const modeNotes = () => MODES.filter((m) => modePick[m.name]).map((m) => `${m.label}: ${modeValueLabel(m, modePick[m.name])}`)
|
|
182
191
|
// Languages: screens reload in the one picked (?lang=…); the source language shows them as written.
|
|
183
192
|
let LANGS = ['en']
|
|
184
193
|
let SOURCE = 'en'
|
|
185
194
|
let lang = localStorage.getItem('stitch2-lang') ?? 'en'
|
|
186
195
|
const langQuery = () => (lang === SOURCE ? '' : `&lang=${encodeURIComponent(lang)}`)
|
|
187
196
|
const themeNote = () =>
|
|
188
|
-
[themed() && theme !== THEMES[0] ? `in the ${theme} theme` : '', lang !== SOURCE ? `in ${lang}` : '']
|
|
197
|
+
[themed() && theme !== THEMES[0] ? `in the ${theme} theme` : '', lang !== SOURCE ? `in ${lang}` : '', ...modeNotes()]
|
|
189
198
|
.filter(Boolean)
|
|
190
199
|
.map((s) => ' ' + s)
|
|
191
200
|
.join(',')
|
|
@@ -245,6 +254,33 @@ function widthOptions() {
|
|
|
245
254
|
if (!widthModes().includes(widthMode)) widthMode = 'drawn'
|
|
246
255
|
wsel.value = widthMode
|
|
247
256
|
}
|
|
257
|
+
// The document's own value is kept the first time, so "As drawn" puts it back.
|
|
258
|
+
const authored = new WeakMap()
|
|
259
|
+
function applyModes(f) {
|
|
260
|
+
if (!f.iframe) return
|
|
261
|
+
let root
|
|
262
|
+
try { root = f.iframe.contentDocument.documentElement } catch { return }
|
|
263
|
+
if (!root) return
|
|
264
|
+
const was = authored.get(root) ?? authored.set(root, {}).get(root)
|
|
265
|
+
for (const m of MODES) {
|
|
266
|
+
if (!(m.attribute in was)) was[m.attribute] = root.getAttribute(m.attribute)
|
|
267
|
+
const value = modePick[m.name] || was[m.attribute]
|
|
268
|
+
if (value == null) root.removeAttribute(m.attribute)
|
|
269
|
+
else root.setAttribute(m.attribute, value)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function setMode(name, value) {
|
|
273
|
+
const m = MODES.find((x) => x.name === name)
|
|
274
|
+
if (!m) return
|
|
275
|
+
modePick[name] = m.values.some((x) => x.value === value) ? value : ''
|
|
276
|
+
localStorage.setItem(`stitch2-mode-${name}`, modePick[name])
|
|
277
|
+
const sel = document.querySelector(`#modeFilters [data-mode="${name}"]`)
|
|
278
|
+
if (sel) sel.value = modePick[name]
|
|
279
|
+
for (const f of frames.values()) applyModes(f)
|
|
280
|
+
if (selected) inspector(selected)
|
|
281
|
+
summary()
|
|
282
|
+
scheduleSync()
|
|
283
|
+
}
|
|
248
284
|
function applyTheme(f) {
|
|
249
285
|
if (!themed() || !f.iframe) return
|
|
250
286
|
try { f.iframe.contentDocument.documentElement.dataset.theme = theme } catch {}
|
|
@@ -640,6 +676,7 @@ function shotUrl(s) {
|
|
|
640
676
|
const q = new URLSearchParams({ path: s.path, width: String(s.width), m: String(Math.round(s.mtime)), r: String(shotRev) })
|
|
641
677
|
if (themed()) q.set('theme', theme)
|
|
642
678
|
if (lang !== SOURCE) q.set('lang', lang)
|
|
679
|
+
for (const m of MODES) if (modePick[m.name]) q.append('mode', `${m.name}:${modePick[m.name]}`)
|
|
643
680
|
return `/api/shot?${q}`
|
|
644
681
|
}
|
|
645
682
|
const pinned = (f) => f === selected || f.el.classList.contains('live') || picked.some((p) => p.f === f)
|
|
@@ -670,6 +707,7 @@ function mount(f) {
|
|
|
670
707
|
void (async () => {
|
|
671
708
|
if (!(await waitForStyles()) || f.iframe !== iframe) return
|
|
672
709
|
applyTheme(f)
|
|
710
|
+
applyModes(f)
|
|
673
711
|
measure(f)
|
|
674
712
|
clearTimeout(f.reveal)
|
|
675
713
|
// The picture stays until the complete, styled page has been measured.
|
|
@@ -1176,6 +1214,7 @@ addEventListener('keydown', (e) => {
|
|
|
1176
1214
|
if (e.key === 'Escape') { closeDoc(); select(null) }
|
|
1177
1215
|
if (e.key === 'f' || e.key === 'F') fit()
|
|
1178
1216
|
if ((e.key === 't' || e.key === 'T') && themed()) setTheme(THEMES[(THEMES.indexOf(theme) + 1) % THEMES.length])
|
|
1217
|
+
if ((e.key === 'm' || e.key === 'M') && MODES.length) { const m = MODES[0], all = ['', ...m.values.map((v) => v.value)]; setMode(m.name, all[(all.indexOf(modePick[m.name]) + 1) % all.length]) }
|
|
1179
1218
|
if (e.key === 'l' || e.key === 'L') setLang(LANGS[(LANGS.indexOf(lang) + 1) % LANGS.length])
|
|
1180
1219
|
if (e.key === 'w' || e.key === 'W') { const m = widthModes(); setWidth(m[(m.indexOf(widthMode) + 1) % m.length]) }
|
|
1181
1220
|
if (e.key === '0') zoomAt((innerWidth - SIDE) / 2, innerHeight / 2, 1)
|
|
@@ -1214,6 +1253,20 @@ fetch('/api/config').then((r) => r.json()).then((c) => {
|
|
|
1214
1253
|
pick.innerHTML = THEMES.map((t) => `<option value="${t}">${t[0].toUpperCase()}${t.slice(1)} theme</option>`).join('')
|
|
1215
1254
|
pick.onchange = () => setTheme(pick.value)
|
|
1216
1255
|
setTheme(theme)
|
|
1256
|
+
MODES = c.modes ?? []
|
|
1257
|
+
MODES.forEach((m, i) => {
|
|
1258
|
+
const stored = localStorage.getItem(`stitch2-mode-${m.name}`) ?? ''
|
|
1259
|
+
modePick[m.name] = m.values.some((v) => v.value === stored) ? stored : ''
|
|
1260
|
+
const sel = document.createElement('select')
|
|
1261
|
+
sel.className = 'pick'
|
|
1262
|
+
sel.dataset.mode = m.name
|
|
1263
|
+
sel.setAttribute('aria-label', m.label)
|
|
1264
|
+
sel.title = `Show every screen with ${m.label.toLowerCase()} set to one of these, or as drawn${i === 0 ? ' (M)' : ''}`
|
|
1265
|
+
for (const v of ['', ...m.values.map((x) => x.value)]) sel.add(new Option(`${m.label}: ${modeValueLabel(m, v)}`, v))
|
|
1266
|
+
sel.value = modePick[m.name]
|
|
1267
|
+
sel.onchange = () => setMode(m.name, sel.value)
|
|
1268
|
+
document.getElementById('modeFilters').append(sel)
|
|
1269
|
+
})
|
|
1217
1270
|
WIDTHS = c.widths ?? {}
|
|
1218
1271
|
BREAKPOINTS = c.breakpoints ?? {}
|
|
1219
1272
|
const wsel = document.getElementById('widthFilter')
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An app state every screen can be shown in, picked from the canvas sidebar: signed in or out, a plan, a role. The pick
|
|
3
|
+
* is an attribute on each screen's <html> (data-auth="user"), so the screens' own CSS and scripts answer to it.
|
|
4
|
+
*/
|
|
5
|
+
export interface ModeConfig {
|
|
6
|
+
/** The id: lowercase letters, digits and hyphens. The canvas remembers the pick under it. */
|
|
7
|
+
name: string;
|
|
8
|
+
/** Shown before each value in the dropdown; defaults to the name, capitalized. */
|
|
9
|
+
label?: string;
|
|
10
|
+
/** The attribute on the screen's <html> that carries the value; defaults to data-<name>. */
|
|
11
|
+
attribute?: string;
|
|
12
|
+
/** What the canvas can show every screen as: a value, or a value with its label. "As drawn" is always offered too. */
|
|
13
|
+
values: (string | {
|
|
14
|
+
value: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
})[];
|
|
17
|
+
}
|
|
1
18
|
export interface LabConfig {
|
|
2
19
|
/** Shown on the canvas. */
|
|
3
20
|
name: string;
|
|
@@ -41,6 +58,11 @@ export interface LabConfig {
|
|
|
41
58
|
skills: string[];
|
|
42
59
|
/** Paths under designDir a sandbox leaves out besides screens (notes that would give the trial away). */
|
|
43
60
|
sandboxExclude?: string[];
|
|
61
|
+
/**
|
|
62
|
+
* App states shown across all screens (see ModeConfig). The canvas has a dropdown for each, with the screens as drawn
|
|
63
|
+
* first; picking a value sets its attribute on every screen, live ones and the pictures of the rest.
|
|
64
|
+
*/
|
|
65
|
+
modes?: ModeConfig[];
|
|
44
66
|
/** The live app, for stitch2 compare (see compare.ts): its URL, a sign-in script and a route per screen. */
|
|
45
67
|
app?: {
|
|
46
68
|
url: string;
|
|
@@ -91,5 +113,21 @@ export declare const breakpoints: () => Record<string, number>;
|
|
|
91
113
|
* rule starts (each one is the narrowest window of its layout, where things are most likely to break).
|
|
92
114
|
*/
|
|
93
115
|
export declare function checkWidths(device: string): number[];
|
|
116
|
+
export interface Mode {
|
|
117
|
+
name: string;
|
|
118
|
+
label: string;
|
|
119
|
+
attribute: string;
|
|
120
|
+
values: {
|
|
121
|
+
value: string;
|
|
122
|
+
label: string;
|
|
123
|
+
}[];
|
|
124
|
+
}
|
|
125
|
+
/** The configured modes, checked: a name or attribute that isn't plain, a repeat, or a mode without values is left out. */
|
|
126
|
+
export declare function modes(): Mode[];
|
|
127
|
+
/**
|
|
128
|
+
* The attributes a request asks for, from its "name:value" pairs; a pair that isn't a configured mode and value is
|
|
129
|
+
* ignored, so an address can only set what the config offers.
|
|
130
|
+
*/
|
|
131
|
+
export declare function modeAttributes(pairs: string[]): Record<string, string>;
|
|
94
132
|
/** The design folder, with a trailing slash. */
|
|
95
133
|
export declare const ROOT: string;
|
package/dist/config.js
CHANGED
|
@@ -51,5 +51,43 @@ export function checkWidths(device) {
|
|
|
51
51
|
const inside = Object.values(breakpoints()).filter((b) => b > range[0] && b < range[1]);
|
|
52
52
|
return [...new Set([range[0], ...inside, range[1]])].sort((a, b) => a - b);
|
|
53
53
|
}
|
|
54
|
+
const MODE_NAME = /^[a-z][a-z0-9-]*$/;
|
|
55
|
+
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
56
|
+
/** The configured modes, checked: a name or attribute that isn't plain, a repeat, or a mode without values is left out. */
|
|
57
|
+
export function modes() {
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const m of Array.isArray(config.modes) ? config.modes : []) {
|
|
60
|
+
if (typeof m?.name !== 'string' || !MODE_NAME.test(m.name) || out.some((o) => o.name === m.name))
|
|
61
|
+
continue;
|
|
62
|
+
const attribute = m.attribute ?? `data-${m.name}`;
|
|
63
|
+
if (!MODE_NAME.test(attribute))
|
|
64
|
+
continue;
|
|
65
|
+
const values = [];
|
|
66
|
+
for (const v of Array.isArray(m.values) ? m.values : []) {
|
|
67
|
+
const value = typeof v === 'string' ? v : v?.value;
|
|
68
|
+
if (typeof value !== 'string' || !value || /[\s"'<>=&:]/.test(value) || values.some((x) => x.value === value))
|
|
69
|
+
continue;
|
|
70
|
+
values.push({ value, label: (typeof v === 'object' && v.label) || capitalize(value) });
|
|
71
|
+
}
|
|
72
|
+
if (values.length)
|
|
73
|
+
out.push({ name: m.name, label: m.label ?? capitalize(m.name), attribute, values });
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The attributes a request asks for, from its "name:value" pairs; a pair that isn't a configured mode and value is
|
|
79
|
+
* ignored, so an address can only set what the config offers.
|
|
80
|
+
*/
|
|
81
|
+
export function modeAttributes(pairs) {
|
|
82
|
+
const picked = {};
|
|
83
|
+
for (const pair of pairs) {
|
|
84
|
+
const at = pair.indexOf(':');
|
|
85
|
+
const mode = modes().find((m) => m.name === pair.slice(0, at));
|
|
86
|
+
const value = pair.slice(at + 1);
|
|
87
|
+
if (at > 0 && mode?.values.some((v) => v.value === value))
|
|
88
|
+
picked[mode.attribute] = value;
|
|
89
|
+
}
|
|
90
|
+
return picked;
|
|
91
|
+
}
|
|
54
92
|
/** The design folder, with a trailing slash. */
|
|
55
93
|
export const ROOT = join(resolve(BASE, config.designDir), '/');
|
package/dist/serve.js
CHANGED
|
@@ -9,7 +9,7 @@ import { existsSync, readdirSync, readFileSync, statSync, watch } from 'node:fs'
|
|
|
9
9
|
import { createServer } from 'node:http';
|
|
10
10
|
import { dirname, extname, join, normalize } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import { breakpoints, CONFIG_FILE, checkWidths, config, widthRanges } from './config.js';
|
|
12
|
+
import { breakpoints, CONFIG_FILE, checkWidths, config, modeAttributes, modes, widthRanges } from './config.js';
|
|
13
13
|
import { designThemes } from './design-md.js';
|
|
14
14
|
import { allLocales, localeScript, sourceLocale } from './i18n.js';
|
|
15
15
|
import { designPage } from './design-page.js';
|
|
@@ -156,7 +156,7 @@ export function startServer(port) {
|
|
|
156
156
|
return;
|
|
157
157
|
}
|
|
158
158
|
if (url.pathname === '/api/config')
|
|
159
|
-
return json({ ...config, themes: designThemes(), locales: allLocales(), sourceLocale: sourceLocale(), widths: widthRanges(), breakpoints: breakpoints(), checkWidths: Object.fromEntries(Object.keys(widthRanges()).map((d) => [d, checkWidths(d)])) });
|
|
159
|
+
return json({ ...config, modes: modes(), themes: designThemes(), locales: allLocales(), sourceLocale: sourceLocale(), widths: widthRanges(), breakpoints: breakpoints(), checkWidths: Object.fromEntries(Object.keys(widthRanges()).map((d) => [d, checkWidths(d)])) });
|
|
160
160
|
if (url.pathname === '/api/screens')
|
|
161
161
|
return json(listScreens());
|
|
162
162
|
if (url.pathname === '/api/reports')
|
|
@@ -169,6 +169,7 @@ export function startServer(port) {
|
|
|
169
169
|
width: Math.min(4000, Math.max(200, Number(q.get('width')) || 390)),
|
|
170
170
|
theme: q.get('theme') || undefined,
|
|
171
171
|
lang: q.get('lang') || undefined,
|
|
172
|
+
modes: modeAttributes(q.getAll('mode')),
|
|
172
173
|
});
|
|
173
174
|
if (!job) {
|
|
174
175
|
res.writeHead(404, { 'content-type': 'text/plain' });
|
package/dist/shots.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export interface ShotRequest {
|
|
|
3
3
|
width: number;
|
|
4
4
|
theme?: string;
|
|
5
5
|
lang?: string;
|
|
6
|
+
/** Attributes set on the page's <html> before the picture: the app states picked in the canvas (config modes). */
|
|
7
|
+
modes?: Record<string, string>;
|
|
6
8
|
}
|
|
7
9
|
/** The server's file watcher calls this: the next picture looks at the shared files again. */
|
|
8
10
|
export declare function sharedChanged(): void;
|
package/dist/shots.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* components), so a canvas with a hundred of them stalls the browser; the canvas keeps only the screens in view live
|
|
4
4
|
* and draws the rest from these. One headless Chromium renders them, three at a time, and they are kept on disk
|
|
5
5
|
* (os.tmpdir()/stitch2-shots), keyed by everything that changes the picture: the screen file, the files screens
|
|
6
|
-
* share (tokens, components, stylesheets), the theme, the language and its messages, and the width.
|
|
6
|
+
* share (tokens, components, stylesheets), the theme, the modes, the language and its messages, and the width.
|
|
7
7
|
*/
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
9
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
@@ -53,6 +53,7 @@ function keyOf(r) {
|
|
|
53
53
|
statSync(file).mtimeMs,
|
|
54
54
|
sharedStamp(),
|
|
55
55
|
r.theme ?? '',
|
|
56
|
+
JSON.stringify(Object.entries(r.modes ?? {}).sort(([a], [b]) => a.localeCompare(b))),
|
|
56
57
|
r.lang ?? '',
|
|
57
58
|
messages && existsSync(messages) ? statSync(messages).mtimeMs : '',
|
|
58
59
|
r.width,
|
|
@@ -98,6 +99,10 @@ export function shot(base, r) {
|
|
|
98
99
|
await page.goto(`${base}/${r.path}?${query}`, { waitUntil: 'networkidle', timeout: 30_000 }).catch(() => { });
|
|
99
100
|
if (r.theme)
|
|
100
101
|
await page.evaluate((t) => { document.documentElement.dataset.theme = t; }, r.theme);
|
|
102
|
+
const attributes = Object.entries(r.modes ?? {});
|
|
103
|
+
if (attributes.length)
|
|
104
|
+
await page.evaluate((list) => { for (const [name, value] of list)
|
|
105
|
+
document.documentElement.setAttribute(name, value); }, attributes);
|
|
101
106
|
await page.evaluate(() => document.fonts.ready);
|
|
102
107
|
// Tailwind's browser build and the components settle after load.
|
|
103
108
|
await page.waitForTimeout(400);
|
package/package.json
CHANGED
|
@@ -171,13 +171,31 @@ doesn't cover it is drawn at its nearest edge and faded, because the app shows a
|
|
|
171
171
|
- A desktop screen is a layout for its range, not a stretched phone: a side navigation instead of the bar,
|
|
172
172
|
list-detail with both panes, extra columns for feeds.
|
|
173
173
|
|
|
174
|
+
## App states
|
|
175
|
+
|
|
176
|
+
When the product looks different signed in or out, on another plan or for another role, do not draw a copy of each
|
|
177
|
+
screen per state. Draw it once, let its CSS and scripts answer to an attribute on `<html>`, and list the states as
|
|
178
|
+
`modes` in `stitch2.config.json`:
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
"modes": [
|
|
182
|
+
{ "name": "auth", "label": "Account", "values": [{ "value": "guest", "label": "Guest" }, { "value": "user", "label": "Signed in" }] }
|
|
183
|
+
]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The canvas sidebar then has a dropdown for each mode. A value sets the attribute (`data-auth="user"`; `attribute`
|
|
187
|
+
names another) on every screen, live frames and the pictures of the rest, and "As drawn" puts each screen back to what
|
|
188
|
+
its file says. `m` cycles the first mode. A screen is still written for the state it shows (a signed-in account page has
|
|
189
|
+
`data-auth="user"` in its file); modes show the other states of the same screen. `stitch2 check` runs on the screens as
|
|
190
|
+
drawn, not in each state, so open the other states on the canvas and look at them before hand-over.
|
|
191
|
+
|
|
174
192
|
## The canvas
|
|
175
193
|
|
|
176
194
|
`stitch2 canvas` → http://localhost:4400. The sidebar lists DESIGN.md (drawn as tokens and rules), Components
|
|
177
195
|
(the catalog for the variants the filter shows; a click steps through them), the consistency report and app
|
|
178
196
|
comparison (each with when it last ran, or how many covered screens changed since), Languages, and every screen
|
|
179
|
-
with its versions. Filters: status (active, approved, needs review, all), versions, variants, theme, width
|
|
180
|
-
language. The reports and Languages follow the filters too: they show only the screens the canvas shows. Selecting a version opens the inspector: status, note, checks, and
|
|
197
|
+
with its versions. Filters: status (active, approved, needs review, all), versions, variants, theme, width,
|
|
198
|
+
language and each app state (`modes`). The reports and Languages follow the filters too: they show only the screens the canvas shows. Selecting a version opens the inspector: status, note, checks, and
|
|
181
199
|
Copy for its reference. Pick mode (`p`) highlights the element under the pointer; a click copies its reference,
|
|
182
200
|
Shift-click collects several (Copy all, Add to note). Keys: `/` search, `j`/`k` next and previous, `a` approve,
|
|
183
|
-
`p` pick, `f` fit, `0` actual size, double-click to use a frame, `Esc` to leave.
|
|
201
|
+
`p` pick, `f` fit, `m` next app state, `0` actual size, double-click to use a frame, `Esc` to leave.
|