stitch2 0.16.0 → 0.17.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/dist/canvas.html +74 -15
- package/dist/list.js +17 -6
- package/dist/screens.d.ts +8 -0
- package/dist/screens.js +10 -0
- package/dist/setup.js +1 -1
- package/package.json +1 -1
- package/skills/stitch2/references/screens.md +14 -1
package/dist/canvas.html
CHANGED
|
@@ -58,6 +58,11 @@
|
|
|
58
58
|
.screen-name { align-items: center; border-radius: 6px; color: var(--text); cursor: pointer; display: flex; font-weight: 600; gap: 6px; padding: 8px 8px 2px; }
|
|
59
59
|
.screen-name:hover { background: var(--panel2); }
|
|
60
60
|
.screen-name .meta { color: var(--faint); font-size: 11px; font-weight: 400; margin-left: auto; }
|
|
61
|
+
.page-name .chev { color: var(--faint); display: inline-block; font-size: 10px; transition: transform .15s; width: 10px; }
|
|
62
|
+
.page-name.open .chev { transform: rotate(90deg); }
|
|
63
|
+
.screen-name.state { font-weight: 500; padding-left: 26px; }
|
|
64
|
+
.item.version.nested { padding-left: 30px; }
|
|
65
|
+
.item.variant.nested { padding-left: 52px; }
|
|
61
66
|
.version { padding-left: 20px; }
|
|
62
67
|
.dot { background: var(--faint); border-radius: 50%; flex: none; height: 8px; width: 8px; }
|
|
63
68
|
.dot.approved { background: var(--good); } .dot.review { background: var(--warn); } .dot.draft { background: var(--muted); } .dot.archived { background: transparent; border: 1px solid var(--faint); }
|
|
@@ -151,6 +156,8 @@ const viewport = document.getElementById('viewport')
|
|
|
151
156
|
const world = document.getElementById('world')
|
|
152
157
|
const SIDE = 288, GAP_X = 72, GAP_Y = 150, GROUP_GAP = 200, LINE_W = 3400, ROW_LABEL_H = 52, LABEL_H = 38
|
|
153
158
|
let ORDER = [] // screen order, from the config
|
|
159
|
+
// Pages whose screens are unfolded in the sidebar.
|
|
160
|
+
const openPages = new Set((() => { try { return JSON.parse(localStorage.getItem('stitch2-open-pages') ?? '[]') } catch { return [] } })())
|
|
154
161
|
const STATUS_RANK = { approved: 0, review: 1, draft: 2, archived: 3 }
|
|
155
162
|
const STATUS_LABEL = { approved: 'approved', review: 'needs review', draft: 'draft', archived: 'archived' }
|
|
156
163
|
const STATUS_HINT = {
|
|
@@ -289,7 +296,19 @@ const zoomAt = (cx, cy, z) => {
|
|
|
289
296
|
// version its variants: the layouts for each device (mobile, desktop), files <device>-v<N> sharing N ----
|
|
290
297
|
const rowKey = (s) => (s.kind === 'components' ? '~components' : s.screen)
|
|
291
298
|
const rowRank = (k) => (k === '~components' ? -1 : ORDER.includes(k) ? ORDER.indexOf(k) : 100)
|
|
292
|
-
|
|
299
|
+
/** What a screen is within its page: its name without the page's ("entry" for workout-entry), or "main". */
|
|
300
|
+
const stateOf = (screen, page) => (screen === page ? 'main' : screen.startsWith(`${page}-`) ? screen.slice(page.length + 1) : screen)
|
|
301
|
+
// Pages keep their screens together: a page sorts where the first of its screens (or the page itself) is in the
|
|
302
|
+
// config's order, its main screen first, the rest in order.
|
|
303
|
+
const sortRows = (keys, pageOf) => {
|
|
304
|
+
const pageRank = new Map()
|
|
305
|
+
for (const k of keys) { const p = pageOf(k); pageRank.set(p, Math.min(pageRank.get(p) ?? 100, rowRank(k), rowRank(p))) }
|
|
306
|
+
return keys.sort((a, b) => {
|
|
307
|
+
const pa = pageOf(a), pb = pageOf(b)
|
|
308
|
+
if (pa !== pb) return pageRank.get(pa) - pageRank.get(pb) || pa.localeCompare(pb)
|
|
309
|
+
return (a === pa ? -1 : b === pb ? 1 : 0) || rowRank(a) - rowRank(b) || a.localeCompare(b)
|
|
310
|
+
})
|
|
311
|
+
}
|
|
293
312
|
const versionRank = (v) => { const m = /^v(\d+)$/.exec(v); return m ? Number(m[1]) : 0 }
|
|
294
313
|
const DEVICE_RANK = { mobile: 0, tablet: 1, desktop: 2 }
|
|
295
314
|
const sortVersions = (list) =>
|
|
@@ -314,16 +333,31 @@ function shown(s) {
|
|
|
314
333
|
const rows = () => {
|
|
315
334
|
const by = new Map()
|
|
316
335
|
for (const s of screens) by.set(rowKey(s), [...(by.get(rowKey(s)) ?? []), s])
|
|
317
|
-
|
|
336
|
+
const pageOf = (k) => (k === '~components' ? '~components' : by.get(k)[0].page || k)
|
|
337
|
+
const sizes = new Map()
|
|
338
|
+
for (const k of by.keys()) sizes.set(pageOf(k), (sizes.get(pageOf(k)) ?? 0) + 1)
|
|
339
|
+
return sortRows([...by.keys()], pageOf).map((k) => ({
|
|
340
|
+
key: k,
|
|
341
|
+
page: pageOf(k),
|
|
342
|
+
// A page with several screens: each is one of its states (entry, menu, empty…).
|
|
343
|
+
state: sizes.get(pageOf(k)) > 1 ? stateOf(k, pageOf(k)) : '',
|
|
344
|
+
all: sortVersions(by.get(k)),
|
|
345
|
+
visible: sortVersions(by.get(k).filter(shown)),
|
|
346
|
+
}))
|
|
318
347
|
}
|
|
319
348
|
|
|
320
349
|
// Screens flow left to right and wrap, like a design canvas; each screen's versions stay together.
|
|
321
350
|
function layout() {
|
|
322
351
|
for (const n of world.querySelectorAll('.row-label')) n.remove()
|
|
323
|
-
let x = 0, y = 0, lineH = 0
|
|
324
|
-
|
|
352
|
+
let x = 0, y = 0, lineH = 0, page = ''
|
|
353
|
+
const all = rows()
|
|
354
|
+
const paged = new Set(all.filter((r) => r.state).map((r) => r.page))
|
|
355
|
+
for (const row of all) {
|
|
325
356
|
for (const s of row.all) frames.get(s.path).el.style.display = row.visible.includes(s) ? '' : 'none'
|
|
326
357
|
if (!row.visible.length) continue
|
|
358
|
+
// Each page with several screens starts its own line, so its states read together.
|
|
359
|
+
if (x > 0 && row.page !== page && (paged.has(row.page) || paged.has(page))) { x = 0; y += lineH + GAP_Y; lineH = 0 }
|
|
360
|
+
page = row.page
|
|
327
361
|
const groups = versionsOf(row.visible)
|
|
328
362
|
const widthOf = (g) => g.variants.reduce((w, s) => w + s.width, 0) + GAP_X * (g.variants.length - 1)
|
|
329
363
|
const groupW = groups.reduce((w, g) => w + widthOf(g), 0) + VERSION_GAP * (groups.length - 1)
|
|
@@ -332,7 +366,7 @@ function layout() {
|
|
|
332
366
|
label.className = 'row-label'
|
|
333
367
|
const approved = row.all.filter((s) => s.status === 'approved').map((s) => `${s.version} ${s.device}`)
|
|
334
368
|
label.innerHTML = '<b></b><span></span>'
|
|
335
|
-
label.querySelector('b').textContent = row.key === '~components' ? 'Components' : row.key
|
|
369
|
+
label.querySelector('b').textContent = row.key === '~components' ? 'Components' : row.state ? `${row.page} › ${row.state}` : row.key
|
|
336
370
|
label.querySelector('span').textContent = row.key === '~components' ? 'the catalog every screen is built from' : approved.length ? `approved: ${approved.join(', ')}` : 'nothing approved yet'
|
|
337
371
|
label.style.transform = `translate(${x}px, ${y}px)`
|
|
338
372
|
label.style.maxWidth = `${groupW}px`
|
|
@@ -438,26 +472,50 @@ function tree() {
|
|
|
438
472
|
report('Consistency report', '/consistency/index.html', reports.consistency, 'stitch2 consistency')
|
|
439
473
|
report('App comparison', '/compare/index.html', reports.compare, 'stitch2 compare')
|
|
440
474
|
item('Languages', 'screens × languages', docOpen('/languages'), () => openDoc('/languages'))
|
|
441
|
-
const all = rows()
|
|
475
|
+
const all = rows().filter((r) => r.key !== '~components' && r.visible.length)
|
|
442
476
|
head('Screens')
|
|
443
|
-
|
|
444
|
-
|
|
477
|
+
const dotOf = (list) => (list.some((s) => s.status === 'approved') ? 'approved' : list.some((s) => s.status === 'review') ? 'review' : 'draft')
|
|
478
|
+
const screenRow = (row, nested) => {
|
|
445
479
|
const name = document.createElement('div')
|
|
446
|
-
name.className =
|
|
447
|
-
|
|
448
|
-
name.
|
|
449
|
-
name.children[1].textContent = row.key
|
|
480
|
+
name.className = `screen-name${nested ? ' state' : ''}`
|
|
481
|
+
name.innerHTML = `<span class="dot ${dotOf(row.all)}"></span><span></span><span class="meta"></span>`
|
|
482
|
+
name.children[1].textContent = nested ? row.state : row.key
|
|
450
483
|
const vs = versionsOf(row.visible)
|
|
451
484
|
name.querySelector('.meta').textContent = `${vs.length} version${vs.length > 1 ? 's' : ''}`
|
|
452
485
|
name.onclick = () => select(frames.get(row.visible[0].path), true)
|
|
453
486
|
name.append(copyButton(row.key, 'Copy the screen name'))
|
|
454
487
|
nav.append(name)
|
|
488
|
+
const sub = nested ? ' nested' : ''
|
|
455
489
|
for (const g of vs) {
|
|
456
490
|
const one = g.variants.length === 1
|
|
457
491
|
const first = g.variants[0]
|
|
458
|
-
item(one ? `${g.version} · ${first.device}` : g.version, one ? STATUS_LABEL[first.status] : `${STATUS_LABEL[g.status]} · ${g.variants.length} variants`, one && selected?.screen.path === first.path, () => select(frames.get(first.path), true), g.status,
|
|
459
|
-
if (!one) for (const s of g.variants) item(s.device, STATUS_LABEL[s.status], selected?.screen.path === s.path, () => select(frames.get(s.path), true), s.status,
|
|
492
|
+
item(one ? `${g.version} · ${first.device}` : g.version, one ? STATUS_LABEL[first.status] : `${STATUS_LABEL[g.status]} · ${g.variants.length} variants`, one && selected?.screen.path === first.path, () => select(frames.get(first.path), true), g.status, `version${sub}`, one ? ref(first) : `${row.key} ${g.version}`)
|
|
493
|
+
if (!one) for (const s of g.variants) item(s.device, STATUS_LABEL[s.status], selected?.screen.path === s.path, () => select(frames.get(s.path), true), s.status, `variant${sub}`, ref(s))
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
// A page with several screens folds them under its name; it opens on a click, while one of them is selected, or
|
|
497
|
+
// while searching.
|
|
498
|
+
const searching = !!document.getElementById('search').value.trim()
|
|
499
|
+
for (let i = 0; i < all.length; ) {
|
|
500
|
+
const page = all[i].page
|
|
501
|
+
const group = []
|
|
502
|
+
while (i < all.length && all[i].page === page) group.push(all[i++])
|
|
503
|
+
if (!group[0].state) { screenRow(group[0], false); continue }
|
|
504
|
+
const open = searching || openPages.has(page) || group.some((r) => r.all.some((s) => s.path === selected?.screen.path))
|
|
505
|
+
const pageRow = document.createElement('div')
|
|
506
|
+
pageRow.className = `screen-name page-name${open ? ' open' : ''}`
|
|
507
|
+
pageRow.innerHTML = `<span class="chev">▶</span><span class="dot ${dotOf(group.flatMap((r) => r.all))}"></span><span></span><span class="meta"></span>`
|
|
508
|
+
pageRow.children[2].textContent = page
|
|
509
|
+
pageRow.querySelector('.meta').textContent = `${group.length} screens`
|
|
510
|
+
pageRow.title = group.map((r) => r.state).join(', ')
|
|
511
|
+
pageRow.onclick = () => {
|
|
512
|
+
if (openPages.has(page)) openPages.delete(page)
|
|
513
|
+
else openPages.add(page)
|
|
514
|
+
localStorage.setItem('stitch2-open-pages', JSON.stringify([...openPages]))
|
|
515
|
+
tree()
|
|
460
516
|
}
|
|
517
|
+
nav.append(pageRow)
|
|
518
|
+
if (open) for (const row of group) screenRow(row, true)
|
|
461
519
|
}
|
|
462
520
|
}
|
|
463
521
|
|
|
@@ -527,8 +585,9 @@ function summary() {
|
|
|
527
585
|
const review = versions.filter((s) => s.status === 'review').length
|
|
528
586
|
const approved = new Set(versions.filter((s) => s.status === 'approved').map((s) => s.screen)).size
|
|
529
587
|
const names = new Set(versions.map((s) => s.screen)).size
|
|
588
|
+
const pages = new Set(versions.map((s) => s.page || s.screen)).size
|
|
530
589
|
const bad = versions.filter((s) => forTheme(checks.screens?.[s.path]).some((i) => i.severity === 'error')).length
|
|
531
|
-
document.getElementById('summary').textContent = `${names} screens · ${approved} approved · ${review} need review${bad ? ` · ${bad} with errors` : ''}`
|
|
590
|
+
document.getElementById('summary').textContent = `${pages < names ? `${pages} pages · ` : ''}${names} screens · ${approved} approved · ${review} need review${bad ? ` · ${bad} with errors` : ''}`
|
|
532
591
|
}
|
|
533
592
|
|
|
534
593
|
function frameFor(s) {
|
package/dist/list.js
CHANGED
|
@@ -4,20 +4,31 @@
|
|
|
4
4
|
* Ends with what makes the current version unclear (versionProblems); --strict fails on it.
|
|
5
5
|
* Usage: stitch2 screens [screen name] [--status approved|review|draft|archived] [--json] [--strict]
|
|
6
6
|
*/
|
|
7
|
-
import { listScreens, versionProblems } from './screens.js';
|
|
7
|
+
import { listScreens, stateOf, versionProblems } from './screens.js';
|
|
8
8
|
const args = process.argv.slice(2);
|
|
9
9
|
const status = args.includes('--status') ? args[args.indexOf('--status') + 1] : undefined;
|
|
10
10
|
const name = args.find((a, i) => !a.startsWith('--') && args[i - 1] !== '--status');
|
|
11
11
|
const rows = listScreens()
|
|
12
|
-
.filter((s) => s.kind === 'screen' && (!status || s.status === status) && (!name || s.screen === name))
|
|
13
|
-
.sort((a, b) => a.
|
|
12
|
+
.filter((s) => s.kind === 'screen' && (!status || s.status === status) && (!name || s.screen === name || s.page === name))
|
|
13
|
+
.sort((a, b) => a.page.localeCompare(b.page) ||
|
|
14
|
+
(a.screen === a.page ? -1 : b.screen === b.page ? 1 : 0) ||
|
|
15
|
+
a.screen.localeCompare(b.screen) ||
|
|
14
16
|
a.version.localeCompare(b.version, undefined, { numeric: true }) ||
|
|
15
17
|
b.device.localeCompare(a.device));
|
|
16
18
|
if (args.includes('--json'))
|
|
17
19
|
console.log(JSON.stringify(rows, null, 2));
|
|
18
|
-
else
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
else {
|
|
21
|
+
let page = '';
|
|
22
|
+
for (const s of rows) {
|
|
23
|
+
// A page with several screens gets a heading; its screens are listed under it by their state.
|
|
24
|
+
const several = rows.some((r) => r.page === s.page && r.screen !== s.screen);
|
|
25
|
+
if (several && s.page !== page)
|
|
26
|
+
console.log(`\n${s.page} (page)`);
|
|
27
|
+
page = s.page;
|
|
28
|
+
const label = several ? ` ${stateOf(s)}` : s.screen;
|
|
29
|
+
console.log(`${label.padEnd(20)} ${s.version.padEnd(8)} ${s.device.padEnd(8)} ${s.status.padEnd(9)} ${s.path}${s.note ? `\n${' '.repeat(39)}note: ${s.note}` : ''}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
21
32
|
const problems = versionProblems().filter((p) => !name || p.startsWith(`${name} `));
|
|
22
33
|
if (problems.length && !args.includes('--json')) {
|
|
23
34
|
console.log(`\nWhich version is current is unclear (${problems.length}):`);
|
package/dist/screens.d.ts
CHANGED
|
@@ -19,6 +19,12 @@ export interface Screen {
|
|
|
19
19
|
kind: 'screen' | 'components';
|
|
20
20
|
/** Which app screen this file is a version of (<meta name="<prefix>-screen">), e.g. "home". */
|
|
21
21
|
screen: string;
|
|
22
|
+
/**
|
|
23
|
+
* The page it belongs to (<meta name="<prefix>-page">): one place in the app whose states and steps are screens of
|
|
24
|
+
* their own, e.g. "workout" for workout, workout-entry and workout-menu. A screen's latest version that names one
|
|
25
|
+
* decides it for all of its versions; without one a screen is its own page.
|
|
26
|
+
*/
|
|
27
|
+
page: string;
|
|
22
28
|
device: 'mobile' | 'desktop';
|
|
23
29
|
/** Where the version came from: stitch, lab or trial. */
|
|
24
30
|
source: 'stitch' | 'lab' | 'trial';
|
|
@@ -30,6 +36,8 @@ export interface Screen {
|
|
|
30
36
|
}
|
|
31
37
|
export declare const meta: (html: string, name: string) => string | undefined;
|
|
32
38
|
export declare function listScreens(): Screen[];
|
|
39
|
+
/** What a screen is within its page: its name without the page's ("entry" for workout-entry), or "main". */
|
|
40
|
+
export declare const stateOf: (s: Pick<Screen, 'screen' | 'page'>) => string;
|
|
33
41
|
/** Sets (or removes, with undefined) <prefix>-* meta tags in a screen file, keeping the rest of it untouched. */
|
|
34
42
|
export declare function writeMeta(path: string, values: Record<string, string | undefined>): void;
|
|
35
43
|
/**
|
package/dist/screens.js
CHANGED
|
@@ -51,6 +51,7 @@ export function listScreens() {
|
|
|
51
51
|
mtime: statSync(file).mtimeMs,
|
|
52
52
|
kind: under(path, config.catalogDir) ? 'components' : 'screen',
|
|
53
53
|
screen: meta(html, tag('screen')) ?? (versioned ? group.split('/').pop() : name),
|
|
54
|
+
page: meta(html, tag('page')) ?? '',
|
|
54
55
|
device: (meta(html, tag('device')) ??
|
|
55
56
|
(versioned?.[1] || (width >= 768 ? 'desktop' : 'mobile'))),
|
|
56
57
|
source,
|
|
@@ -65,8 +66,17 @@ export function listScreens() {
|
|
|
65
66
|
});
|
|
66
67
|
}
|
|
67
68
|
}
|
|
69
|
+
// One page per screen: the latest version that names one.
|
|
70
|
+
const pages = new Map();
|
|
71
|
+
for (const s of [...out].sort((a, b) => versionNumber(b) - versionNumber(a) || b.mtime - a.mtime))
|
|
72
|
+
if (s.page && !pages.has(s.screen))
|
|
73
|
+
pages.set(s.screen, s.page);
|
|
74
|
+
for (const s of out)
|
|
75
|
+
s.page = pages.get(s.screen) ?? s.screen;
|
|
68
76
|
return out;
|
|
69
77
|
}
|
|
78
|
+
/** What a screen is within its page: its name without the page's ("entry" for workout-entry), or "main". */
|
|
79
|
+
export const stateOf = (s) => s.screen === s.page ? 'main' : s.screen.startsWith(`${s.page}-`) ? s.screen.slice(s.page.length + 1) : s.screen;
|
|
70
80
|
/** Sets (or removes, with undefined) <prefix>-* meta tags in a screen file, keeping the rest of it untouched. */
|
|
71
81
|
export function writeMeta(path, values) {
|
|
72
82
|
const file = join(ROOT, path);
|
package/dist/setup.js
CHANGED
|
@@ -92,7 +92,7 @@ ${extra}<meta charset="utf-8" />
|
|
|
92
92
|
<script src="/components/${prefix}.js" defer></script>
|
|
93
93
|
</head>
|
|
94
94
|
`;
|
|
95
|
-
write(join(design, 'screens/_template.html'), `${head('Screen name', `<meta name="${prefix}-screen" content="screen-name" />\n<meta name="${prefix}-device" content="mobile" />\n<meta name="${prefix}-status" content="draft" />\n`)}<!--
|
|
95
|
+
write(join(design, 'screens/_template.html'), `${head('Screen name', `<meta name="${prefix}-screen" content="screen-name" />\n<meta name="${prefix}-page" content="screen-name" />\n<meta name="${prefix}-device" content="mobile" />\n<meta name="${prefix}-status" content="draft" />\n`)}<!--
|
|
96
96
|
Template for ${name} screens. Copy it to ${design}/screens/<screen>/<device>-v<N>.html and set the ${prefix}-* meta
|
|
97
97
|
tags (desktop: ${prefix}-device desktop and ${prefix}-frame-width 1280); status draft while you work, review when
|
|
98
98
|
done. Files starting with _ are not shown on the canvas. Build with the components in the catalog
|
package/package.json
CHANGED
|
@@ -13,10 +13,23 @@ and build on it. Meta tag names below use the project's prefix (`<p>`, from `sti
|
|
|
13
13
|
(`mobile-v3.html` and `desktop-v3.html` are v3). A new version takes the next free N across the screen's
|
|
14
14
|
variants; a variant added to an existing version takes that version's N (a desktop for v3 is
|
|
15
15
|
`desktop-v3.html`, never v4). A version may start with one variant and get the others later.
|
|
16
|
-
Its head carries `<p>-screen`, `<p>-device` (mobile | desktop), `<p>-status`, `<p>-source` (stitch | lab),
|
|
16
|
+
Its head carries `<p>-screen`, `<p>-page` (see Pages), `<p>-device` (mobile | desktop), `<p>-status`, `<p>-source` (stitch | lab),
|
|
17
17
|
`<p>-frame-width` (390 mobile, 1280 desktop) and, written by the user, `<p>-note`. Older files elsewhere
|
|
18
18
|
carry the same tags.
|
|
19
19
|
|
|
20
|
+
## Pages and their screens
|
|
21
|
+
|
|
22
|
+
A place in the app often has several states or steps: the workout with its number pad, its menu and its discard
|
|
23
|
+
question; an exercise list with its detail sheet and a three-step form. Each is a **screen** of its own (it has its own
|
|
24
|
+
versions and approval), and they belong to one **page**: `<p>-page` in the head, the page's name (`workout`). The page's
|
|
25
|
+
main screen has the page's name; the others are named `<page>-<state>` (`workout-entry`, `workout-menu`,
|
|
26
|
+
`create-exercise-1`) and carry the same `<p>-page`. The canvas folds a page's screens under its name and lays them out
|
|
27
|
+
together; `stitch2 screens <page>` lists them. A screen without `<p>-page` is a page of its own.
|
|
28
|
+
|
|
29
|
+
- A new state of an existing place (a sheet, a dialog, an empty, loading or error state, a step) is a new screen in
|
|
30
|
+
that page, not a new page.
|
|
31
|
+
- Copy `<p>-page` along with the other tags into each new version; the latest version that has it decides the page.
|
|
32
|
+
|
|
20
33
|
## New version or edit?
|
|
21
34
|
|
|
22
35
|
- **New version** for every new attempt: a new direction, a regeneration, an export from a generator, a
|