stitch2 0.20.1 → 0.22.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/approve.js CHANGED
@@ -18,7 +18,8 @@ if (!targets.length) {
18
18
  // Oldest first, so approving several versions of one screen leaves the latest current.
19
19
  targets.sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true }));
20
20
  for (const t of targets) {
21
- const archived = approve(t.path);
21
+ const { archived, relinked } = approve(t.path);
22
22
  const note = archived.length ? `; archived ${archived.map((s) => s.version).join(', ')}` : '';
23
- console.log(`approved ${t.screen} ${t.version} ${t.device}${note}`);
23
+ const links = relinked.length ? `; relinked ${relinked.join(', ')}` : '';
24
+ console.log(`approved ${t.screen} ${t.version} ${t.device}${note}${links}`);
24
25
  }
@@ -335,7 +335,7 @@ export function reuseIssues(path, page) {
335
335
  if (familyOf(path) !== 'screens')
336
336
  return [];
337
337
  return page.handBuilt.slice(0, 10).map((h) => ({
338
- severity: 'warning',
338
+ severity: 'error',
339
339
  type: 'reuse',
340
340
  message: `${nameOf(h.kind)} built by hand; use <${NAMES[h.kind]?.[1] ?? 'the catalog component'}> (see the component catalog, ${config.catalogDir})`,
341
341
  selector: h.selector,
package/dist/list.js CHANGED
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Lists every screen version, variant by variant (mobile, tablet, desktop), with its status and the reviewer's note, for agents (and anyone without the
3
3
  * canvas open). Approved versions are the reference; review versions wait for the user; notes are feedback.
4
- * Ends with what makes the current version unclear (versionProblems); --strict fails on it.
4
+ * Ends with what makes the current version unclear (versionProblems) and links to a previous variant
5
+ * (staleLinks); --strict fails on either.
5
6
  * Usage: stitch2 screens [screen name] [--status approved|review|draft|archived] [--json] [--strict]
6
7
  */
7
- import { listScreens, stateOf, versionProblems } from './screens.js';
8
+ import { listScreens, staleLinks, stateOf, versionProblems } from './screens.js';
8
9
  const args = process.argv.slice(2);
9
10
  const status = args.includes('--status') ? args[args.indexOf('--status') + 1] : undefined;
10
11
  const name = args.find((a, i) => !a.startsWith('--') && args[i - 1] !== '--status');
@@ -35,5 +36,11 @@ if (problems.length && !args.includes('--json')) {
35
36
  for (const p of problems)
36
37
  console.log(` ${p}`);
37
38
  }
38
- if (problems.length && args.includes('--strict'))
39
+ const links = staleLinks().filter((p) => !name || p.startsWith(`${name}/`) || p.includes(` ${name} `));
40
+ if (links.length && !args.includes('--json')) {
41
+ console.log(`\nLinks to a previous variant (${links.length}):`);
42
+ for (const p of links)
43
+ console.log(` ${p}`);
44
+ }
45
+ if ((problems.length || links.length) && args.includes('--strict'))
39
46
  process.exit(1);
package/dist/screens.d.ts CHANGED
@@ -40,13 +40,43 @@ export declare function listScreens(): Screen[];
40
40
  export declare const stateOf: (s: Pick<Screen, 'screen' | 'page'>) => string;
41
41
  /** Sets (or removes, with undefined) <prefix>-* meta tags in a screen file, keeping the rest of it untouched. */
42
42
  export declare function writeMeta(path: string, values: Record<string, string | undefined>): void;
43
+ /**
44
+ * Links between screens: a screen embeds or points at another screen's variant,
45
+ * `src="/screens/workout/mobile-v3.html"`. Such a link always names the current approved version of that
46
+ * screen on that device; approval moves every link that names an earlier version to the approved one.
47
+ */
48
+ /** Every src/href reference to a screen variant inside a screen file. */
49
+ export interface ScreenRef {
50
+ screen: string;
51
+ device: 'mobile' | 'tablet' | 'desktop';
52
+ version: number;
53
+ }
54
+ export declare function findScreenRefs(html: string): ScreenRef[];
55
+ /** Bumps every reference to an earlier version of one screen variant to the given version. */
56
+ export declare function rewriteScreenRefs(html: string, screen: string, device: string, version: number): string;
57
+ /**
58
+ * Moves every link that names an earlier version of the target's screen on its device to the target's
59
+ * version, in every screen file. Returns the files it changed.
60
+ */
61
+ export declare function rewriteStaleLinks(target: Pick<Screen, 'screen' | 'device' | 'version'>): string[];
62
+ /**
63
+ * Links that name a variant which is not current: the file is gone, the variant is archived, or a newer
64
+ * version is approved on that screen and device. A link to a version still in review or a draft is a
65
+ * deliberate preview of a proposal, not stale. One line each; empty when every link is current.
66
+ */
67
+ export declare function staleLinks(screens?: Screen[]): string[];
43
68
  /**
44
69
  * Approves a variant. Approval makes it the one current version of its screen on its device: every earlier
45
70
  * version there (approved, in review or a draft) and any other approved one become archived, so nobody can mistake
46
71
  * an older one for what is implemented. Later versions (a newer proposal) keep their status.
47
- * Returns the variants it archived.
72
+ * Links from other screens to an earlier version of this screen on this device move to the approved one,
73
+ * so no screen keeps pointing at a previous variant.
74
+ * Returns the variants it archived and the files whose links moved.
48
75
  */
49
- export declare function approve(path: string): Screen[];
76
+ export declare function approve(path: string): {
77
+ archived: Screen[];
78
+ relinked: string[];
79
+ };
50
80
  /**
51
81
  * What makes it unclear which version of a screen is current: two approved on one device, or an earlier version
52
82
  * still in review or a draft next to an approved later one. One line each; empty when every screen is clear.
package/dist/screens.js CHANGED
@@ -93,11 +93,85 @@ export function writeMeta(path, values) {
93
93
  }
94
94
  writeFileSync(file, html);
95
95
  }
96
+ const refRe = /(?:src|href)="[^"]*?(?:screens\/)?([a-z0-9-]+)\/(mobile|tablet|desktop)-v(\d+)\.html/g;
97
+ export function findScreenRefs(html) {
98
+ const out = [];
99
+ for (const m of html.matchAll(refRe))
100
+ out.push({ screen: m[1], device: m[2], version: Number(m[3]) });
101
+ return out;
102
+ }
103
+ /** Bumps every reference to an earlier version of one screen variant to the given version. */
104
+ export function rewriteScreenRefs(html, screen, device, version) {
105
+ return html.replace(refRe, (m, s, d, v) => s === screen && d === device && Number(v) < version ? m.replace(`-v${v}.html`, `-v${version}.html`) : m);
106
+ }
107
+ /**
108
+ * Moves every link that names an earlier version of the target's screen on its device to the target's
109
+ * version, in every screen file. Returns the files it changed.
110
+ */
111
+ export function rewriteStaleLinks(target) {
112
+ const n = versionNumber(target);
113
+ if (Number.isNaN(n))
114
+ return [];
115
+ const changed = [];
116
+ for (const dir of config.screenDirs) {
117
+ for (const file of htmlFiles(join(ROOT, dir))) {
118
+ const html = readFileSync(file, 'utf8');
119
+ const next = rewriteScreenRefs(html, target.screen, target.device, n);
120
+ if (next !== html) {
121
+ writeFileSync(file, next);
122
+ changed.push(relative(ROOT, file));
123
+ }
124
+ }
125
+ }
126
+ return changed.sort();
127
+ }
128
+ /**
129
+ * Links that name a variant which is not current: the file is gone, the variant is archived, or a newer
130
+ * version is approved on that screen and device. A link to a version still in review or a draft is a
131
+ * deliberate preview of a proposal, not stale. One line each; empty when every link is current.
132
+ */
133
+ export function staleLinks(screens = listScreens()) {
134
+ const out = [];
135
+ const current = new Map();
136
+ for (const s of screens) {
137
+ if (s.kind !== 'screen' || s.status !== 'approved')
138
+ continue;
139
+ const key = `${s.screen} ${s.device}`;
140
+ const n = versionNumber(s);
141
+ if (!Number.isNaN(n))
142
+ current.set(key, Math.max(current.get(key) ?? -1, n));
143
+ }
144
+ const byVariant = new Map(screens.map((s) => [`${s.screen} ${s.device} v${versionNumber(s)}`, s]));
145
+ const seen = new Set();
146
+ for (const dir of config.screenDirs) {
147
+ for (const file of htmlFiles(join(ROOT, dir))) {
148
+ const rel = relative(ROOT, file);
149
+ const html = readFileSync(file, 'utf8');
150
+ for (const r of findScreenRefs(html)) {
151
+ const key = `${r.screen} ${r.device}`;
152
+ const cur = current.get(key);
153
+ if (cur === undefined || r.version >= cur)
154
+ continue;
155
+ const hit = byVariant.get(`${key} v${r.version}`);
156
+ if (hit && hit.status !== 'approved' && hit.status !== 'archived')
157
+ continue;
158
+ const line = `${rel}: points at ${r.screen} ${r.device}-v${r.version} (${hit ? hit.status : 'missing'}); current is v${cur}`;
159
+ if (!seen.has(line)) {
160
+ seen.add(line);
161
+ out.push(line);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ return out.sort();
167
+ }
96
168
  /**
97
169
  * Approves a variant. Approval makes it the one current version of its screen on its device: every earlier
98
170
  * version there (approved, in review or a draft) and any other approved one become archived, so nobody can mistake
99
171
  * an older one for what is implemented. Later versions (a newer proposal) keep their status.
100
- * Returns the variants it archived.
172
+ * Links from other screens to an earlier version of this screen on this device move to the approved one,
173
+ * so no screen keeps pointing at a previous variant.
174
+ * Returns the variants it archived and the files whose links moved.
101
175
  */
102
176
  export function approve(path) {
103
177
  const all = listScreens();
@@ -114,7 +188,7 @@ export function approve(path) {
114
188
  (s.status === 'approved' || versionNumber(s) < n));
115
189
  for (const s of outdated)
116
190
  writeMeta(s.path, { [tag('status')]: 'archived' });
117
- return outdated;
191
+ return { archived: outdated, relinked: rewriteStaleLinks(target) };
118
192
  }
119
193
  /**
120
194
  * What makes it unclear which version of a screen is current: two approved on one device, or an earlier version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitch2",
3
- "version": "0.20.1",
3
+ "version": "0.22.0",
4
4
  "description": "A design canvas, layout and type checks, component consistency and screen versioning for agent-built UI, on Google's DESIGN.md format.",
5
5
  "license": "MIT",
6
6
  "author": "ifokeev",
@@ -54,8 +54,9 @@ References, read before the first screen:
54
54
  and translate it with **stitch2-translate**. Never add classes to a component to change how it looks, and never write a colour value (hex,
55
55
  `rgb()`, `hsl()`): only DESIGN.md's colours, as token classes or `var(--<prefix>-<name>)`.
56
56
  5. **Check** `stitch2 check <path> --shots --strict`. Fix every error (overflow, wrapped labels, content under
57
- fixed bars, fixed bars overlapping each other, tap targets, contrast, cramped text) and every warning about type (`type-*`), hand-built
58
- components (`reuse`), components that differ from the catalog (`consistency`) and colour values (`color`).
57
+ fixed bars, fixed bars overlapping each other, tap targets, contrast, cramped text), a hand-built copy of a
58
+ catalog component (`reuse`: use the component or extend it, never rebuild it), and every warning about type
59
+ (`type-*`), components that differ from the catalog (`consistency`) and colour values (`color`).
59
60
  Text running into other text (`text-overlap`) or out of its card (`overflow`) is an error; a short label cut
60
61
  short with an ellipsis (`truncated`) is a warning to fix with room, not a smaller size. Google's rules are
61
62
  warnings too (`text-small`, `edge-margin`, `measure`, `tap-spacing`, `placeholder-label`, `unnamed-control`,
@@ -11,6 +11,8 @@ colour, icons and states. Users learn an element once; every variant costs atten
11
11
 
12
12
  ## The ladder
13
13
 
14
+ Open the catalog first (on the canvas, or its render under `<design>/renders/`): every element of the
15
+ screen is mapped to a component before any markup is written, and the mapping is part of the hand-over.
14
16
  Go down only when the step above cannot work:
15
17
 
16
18
  1. **Use** a component as it is.
@@ -69,9 +71,11 @@ series) is added to DESIGN.md first, with a line in the prose saying where it is
69
71
 
70
72
  ## What the checks report
71
73
 
72
- - **reuse**: on a lab screen, an element that looks like a catalog component but was built by hand (a bottom
73
- bar with three or more buttons, an h1, a primary-filled button, a pill chip, a header icon button, a caps
74
- label).
74
+ - **reuse** (an error: it fails `check --strict` and blocks the hand-over): on a lab screen, an element that
75
+ looks like a catalog component but was built by hand (a bottom bar with three or more buttons, an h1,
76
+ a primary-filled button, a pill chip, a header icon button, a caps label). A screen that duplicates a
77
+ component instead of using it is not handed over: use the component, or extend it (step 4 of the ladder)
78
+ when it cannot do what the screen needs.
75
79
  - **consistency**: a component whose size, radius, font, icon set or icons differ from the catalog's version
76
80
  (or, without a catalog, from the majority of screens in its family), or that appears in two versions on one
77
81
  screen. `stitch2 consistency` shows the same as cropped images; see the stitch2-consistency skill.
@@ -86,6 +86,15 @@ The canvas shows each version with its variants side by side under one heading,
86
86
  needs review while any variant does. **Approve version** approves every variant in review at once; a single
87
87
  variant can still be approved, archived or noted on its own.
88
88
 
89
+ ## Links between screens
90
+
91
+ A screen that embeds or points at another screen names its file
92
+ (`src="/screens/workout/mobile-v3.html"`): always the current approved version of that screen on that
93
+ device, never a previous one. Approving a variant moves every such link to it at once (the canvas Approve
94
+ does the same as `stitch2 approve`), and `stitch2 screens` lists any link that still names a previous variant
95
+ — fix it, or move it to the current version yourself when the link was deliberate. A link to a version still
96
+ in review or a draft is a preview of a proposal and stays.
97
+
89
98
  ## How to use them
90
99
 
91
100
  1. Before designing, run `stitch2 screens` (a name or `--status approved` narrows it) and open the approved