stitch2 0.20.0 → 0.21.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
  }
package/dist/audit.js CHANGED
@@ -218,6 +218,14 @@ export async function audit(opts) {
218
218
  window.scrollTo(0, 0);
219
219
  // 4. Tap targets: WCAG 2.2 asks for at least 24px; smaller than the config's comfortableTarget is a warning.
220
220
  const interactive = all.filter((el) => el.matches('button, a[href], input:not([type=hidden]), select, textarea, [role=button], [role=tab], [role=switch], [role=checkbox]'));
221
+ // Fixed and sticky layers float over the scrolling page (a running-workout panel over cards): content slides
222
+ // under them while scrolling, so they never crowd in-flow targets. Tap spacing (6d) already compares layers.
223
+ const layerOf = (el) => {
224
+ for (let e = el; e; e = e.parentElement)
225
+ if (['fixed', 'sticky'].includes(getComputedStyle(e).position))
226
+ return e;
227
+ return null;
228
+ };
221
229
  for (const el of interactive) {
222
230
  if (interactive.some((o) => o !== el && o.contains(el)))
223
231
  continue;
@@ -229,7 +237,7 @@ export async function audit(opts) {
229
237
  const cx = r.left + r.width / 2;
230
238
  const cy = r.top + r.height / 2;
231
239
  const crowded = interactive.some((o) => {
232
- if (o === el || o.contains(el) || el.contains(o))
240
+ if (o === el || o.contains(el) || el.contains(o) || layerOf(o) !== layerOf(el))
233
241
  return false;
234
242
  const q = o.getBoundingClientRect();
235
243
  const dx = Math.max(q.left - cx, 0, cx - q.right);
@@ -250,12 +258,8 @@ export async function audit(opts) {
250
258
  // 4b. Controls never overlap one another (a button squeezed over its neighbour). Controls inside one another
251
259
  // and invisible overlays (a native picker laid over a row) are fine; fixed and sticky layers (a docked timer,
252
260
  // a sticky Save bar floating over a list) are compared apart from the scrolling page.
253
- const layer = (el) => {
254
- for (let e = el; e; e = e.parentElement)
255
- if (['fixed', 'sticky'].includes(getComputedStyle(e).position))
256
- return e;
257
- return null;
258
- };
261
+ // layerOf is defined with the tap targets above; fixed and sticky layers are compared apart here too.
262
+ const layer = layerOf;
259
263
  // The part of a control its scrolling or clipping ancestors show: a link scrolled out of a nav strip is not drawn
260
264
  // where its box would be.
261
265
  const visibleBox = (el) => {
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.0",
3
+ "version": "0.21.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",
@@ -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