stitch2 0.22.6 → 0.22.8
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 +36 -13
- package/dist/consistency.d.ts +3 -1
- package/dist/consistency.js +58 -6
- package/dist/design-md.d.ts +2 -0
- package/dist/design-md.js +38 -6
- package/dist/screens.d.ts +2 -2
- package/dist/tokens.js +2 -2
- package/package.json +1 -1
- package/skills/stitch2/references/components.md +5 -0
- package/skills/stitch2/references/screens.md +7 -1
- package/skills/stitch2/references/typography.md +3 -0
- package/skills/stitch2-consistency/SKILL.md +5 -0
package/dist/canvas.html
CHANGED
|
@@ -300,9 +300,13 @@ let reports = { consistency: null, compare: null, states: null }
|
|
|
300
300
|
let selected = null
|
|
301
301
|
const frames = new Map() // path -> { el, device, img, iframe (while live), screen, height }
|
|
302
302
|
// Heights measured before, so the layout doesn't jump while pictures and pages load.
|
|
303
|
-
|
|
303
|
+
// Height semantics changed when live frames began loading at their authored
|
|
304
|
+
// viewport. Do not carry a pre-fix cache into this layout: one stale 20k page
|
|
305
|
+
// is enough to create a giant gap before its iframe is visited.
|
|
306
|
+
const HEIGHT_KEY = 'stitch2-heights-v3'
|
|
307
|
+
const HEIGHTS = JSON.parse(localStorage.getItem(HEIGHT_KEY) ?? '{}')
|
|
304
308
|
let heightsTimer
|
|
305
|
-
const saveHeights = () => { clearTimeout(heightsTimer); heightsTimer = setTimeout(() => localStorage.setItem(
|
|
309
|
+
const saveHeights = () => { clearTimeout(heightsTimer); heightsTimer = setTimeout(() => localStorage.setItem(HEIGHT_KEY, JSON.stringify(HEIGHTS)), 1000) }
|
|
306
310
|
let layoutQueued = false
|
|
307
311
|
/** Many frames measure themselves at once while loading: one layout per animation frame. */
|
|
308
312
|
const scheduleLayout = () => { if (!layoutQueued) { layoutQueued = true; requestAnimationFrame(() => { layoutQueued = false; layout() }) } }
|
|
@@ -630,21 +634,40 @@ function mount(f) {
|
|
|
630
634
|
const iframe = document.createElement('iframe')
|
|
631
635
|
iframe.setAttribute('scrolling', 'no')
|
|
632
636
|
f.iframe = iframe
|
|
637
|
+
// A remounted frame can still have a cached full-page height. Start the
|
|
638
|
+
// document at its authored viewport so vh/svh/dvh rules are evaluated
|
|
639
|
+
// against the screen, not against the previous page height.
|
|
633
640
|
size(f)
|
|
641
|
+
iframe.style.height = `${f.screen.viewportHeight ?? 844}px`
|
|
642
|
+
const waitForStyles = async () => {
|
|
643
|
+
const doc = iframe.contentDocument
|
|
644
|
+
if (!doc || doc.readyState !== 'complete') return false
|
|
645
|
+
try { await doc.fonts?.ready } catch {}
|
|
646
|
+
// Browser-built Tailwind and late stylesheet links can settle just after
|
|
647
|
+
// load. Do not measure or reveal the live page until that layout pass.
|
|
648
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
|
649
|
+
// `load` also fires for the initial about:blank document. Compare the
|
|
650
|
+
// loaded document itself with the current iframe target (which may have
|
|
651
|
+
// changed for a language/theme reload while this mount was pending).
|
|
652
|
+
return f.iframe === iframe && iframe.contentDocument === doc && doc.readyState === 'complete' && doc.location.href === iframe.src
|
|
653
|
+
}
|
|
634
654
|
iframe.addEventListener('load', () => {
|
|
635
655
|
if (f.iframe !== iframe) return
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
// Tailwind's browser build and the components settle just after load: the picture stays until then.
|
|
640
|
-
f.reveal = setTimeout(() => {
|
|
641
|
-
if (f.iframe !== iframe) return
|
|
656
|
+
void (async () => {
|
|
657
|
+
if (!(await waitForStyles()) || f.iframe !== iframe) return
|
|
658
|
+
applyTheme(f)
|
|
642
659
|
measure(f)
|
|
643
|
-
f.
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
660
|
+
clearTimeout(f.reveal)
|
|
661
|
+
// The picture stays until the complete, styled page has been measured.
|
|
662
|
+
f.reveal = setTimeout(() => {
|
|
663
|
+
if (f.iframe !== iframe) return
|
|
664
|
+
measure(f)
|
|
665
|
+
f.el.classList.add('ready')
|
|
666
|
+
for (const go of f.waiters.splice(0)) go()
|
|
667
|
+
}, 350)
|
|
668
|
+
setTimeout(() => { if (f.iframe === iframe) measure(f) }, 1200)
|
|
669
|
+
setTimeout(() => { if (f.iframe === iframe) measure(f) }, 2500)
|
|
670
|
+
})()
|
|
648
671
|
})
|
|
649
672
|
iframe.src = frameSrc(f.screen)
|
|
650
673
|
f.device.insertBefore(iframe, f.img.nextSibling)
|
package/dist/consistency.d.ts
CHANGED
|
@@ -21,12 +21,14 @@ export interface PageComponents {
|
|
|
21
21
|
selector: string;
|
|
22
22
|
text: string;
|
|
23
23
|
}[];
|
|
24
|
+
/** Provenance declared by the page. App snapshots are faithful renders, not component implementations. */
|
|
25
|
+
source?: string;
|
|
24
26
|
}
|
|
25
27
|
export interface CollectOptions {
|
|
26
28
|
/** Component marker: elements with data-<prefix>="name". */
|
|
27
29
|
prefix: string;
|
|
28
30
|
/** DESIGN.md's primary colour as [r, g, b]: filled buttons in it are primary buttons. */
|
|
29
|
-
primary
|
|
31
|
+
primary?: number[];
|
|
30
32
|
}
|
|
31
33
|
/** The options for collectComponents from the config and DESIGN.md's primary colour. */
|
|
32
34
|
export declare function collectOptions(): CollectOptions;
|
package/dist/consistency.js
CHANGED
|
@@ -10,8 +10,31 @@ import { config } from './config.js';
|
|
|
10
10
|
import { readDesign } from './design-md.js';
|
|
11
11
|
/** The options for collectComponents from the config and DESIGN.md's primary colour. */
|
|
12
12
|
export function collectOptions() {
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const design = readDesign();
|
|
14
|
+
// DESIGN.md projects created by stitch2 have `colors.primary`; projects that use a richer palette often name
|
|
15
|
+
// the same role and reference it from components.button-primary. Resolve that reference here. Falling back to
|
|
16
|
+
// black made every transparent element look like a primary button.
|
|
17
|
+
const component = design.components['button-primary']?.backgroundColor;
|
|
18
|
+
const ref = /^\{colors\.([\w.-]+)\}$/.exec(component ?? '')?.[1];
|
|
19
|
+
const value = design.colors.primary ?? (ref ? design.colors[ref] : undefined) ?? component;
|
|
20
|
+
const primary = parseColour(value);
|
|
21
|
+
return { prefix: config.prefix, ...(primary ? { primary } : {}) };
|
|
22
|
+
}
|
|
23
|
+
/** CSS colours accepted in DESIGN.md, represented as RGB for the page-side detector. */
|
|
24
|
+
function parseColour(value) {
|
|
25
|
+
if (!value)
|
|
26
|
+
return undefined;
|
|
27
|
+
const hex = /^#([\da-f]{3}|[\da-f]{6})$/i.exec(value.trim())?.[1];
|
|
28
|
+
if (hex) {
|
|
29
|
+
const expanded = hex.length === 3 ? hex.replace(/./g, (c) => c + c) : hex;
|
|
30
|
+
return [0, 2, 4].map((i) => Number.parseInt(expanded.slice(i, i + 2), 16));
|
|
31
|
+
}
|
|
32
|
+
const rgb = /^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/i.exec(value.trim());
|
|
33
|
+
if (!rgb)
|
|
34
|
+
return undefined;
|
|
35
|
+
return [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])].every(Number.isFinite)
|
|
36
|
+
? [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])]
|
|
37
|
+
: undefined;
|
|
15
38
|
}
|
|
16
39
|
/** Runs in the page. Plain JS on purpose: Playwright sends its source to the browser. */
|
|
17
40
|
export function collectComponents(opts) {
|
|
@@ -73,11 +96,37 @@ export function collectComponents(opts) {
|
|
|
73
96
|
});
|
|
74
97
|
const iconSet = (root) => [...new Set(icons(root).map(([s]) => s))].join('+');
|
|
75
98
|
const box = (el) => el.getBoundingClientRect();
|
|
76
|
-
const ember = (el) =>
|
|
99
|
+
const ember = (el) => {
|
|
100
|
+
if (!opts.primary)
|
|
101
|
+
return false;
|
|
102
|
+
const background = getComputedStyle(el).backgroundColor.trim();
|
|
103
|
+
// Computed sRGB backgrounds are rgb()/rgba() in supported browsers. Do not extract arbitrary numbers from
|
|
104
|
+
// oklch(), color-mix() or a CSS variable that did not resolve: a false primary match is worse than no match.
|
|
105
|
+
const values = /^rgba?\(/i.test(background) ? background.match(/[\d.]+/g)?.map(Number) ?? [] : [];
|
|
106
|
+
const alpha = values.length > 3 ? values[3] : 1;
|
|
107
|
+
return (alpha >= 0.05 &&
|
|
108
|
+
values.length >= 3 &&
|
|
109
|
+
opts.primary.every((value, i) => Math.abs(value - values[i]) <= 2));
|
|
110
|
+
};
|
|
77
111
|
const visible = (el) => {
|
|
78
112
|
const r = box(el);
|
|
79
113
|
const c = getComputedStyle(el);
|
|
80
|
-
|
|
114
|
+
if (r.width <= 0 || r.height <= 0 || c.visibility === 'hidden' || c.display === 'none')
|
|
115
|
+
return false;
|
|
116
|
+
// A closed <details> exposes only its summary. Its descendants can still have boxes in a static snapshot,
|
|
117
|
+
// especially when the snapshot was captured at a responsive breakpoint.
|
|
118
|
+
const closed = el.closest('details:not([open])');
|
|
119
|
+
if (closed && !closed.querySelector(':scope > summary')?.contains(el))
|
|
120
|
+
return false;
|
|
121
|
+
// Match audit/type checks: clipped screen-reader-only nodes and transparent ancestors are not drawn.
|
|
122
|
+
if (r.width <= 1 && r.height <= 1 && c.position === 'absolute')
|
|
123
|
+
return false;
|
|
124
|
+
if (c.clip === 'rect(0px, 0px, 0px, 0px)' || c.clipPath === 'inset(100%)')
|
|
125
|
+
return false;
|
|
126
|
+
let opacity = 1;
|
|
127
|
+
for (let e = el; e; e = e.parentElement)
|
|
128
|
+
opacity *= Number(getComputedStyle(e).opacity);
|
|
129
|
+
return opacity >= 0.05;
|
|
81
130
|
};
|
|
82
131
|
const sigOf = (kind, el) => {
|
|
83
132
|
const c = getComputedStyle(el);
|
|
@@ -223,7 +272,8 @@ export function collectComponents(opts) {
|
|
|
223
272
|
el.matches('h2, h3, h4, p, span, div, label'))
|
|
224
273
|
found('section', el);
|
|
225
274
|
}
|
|
226
|
-
|
|
275
|
+
const source = document.querySelector(`meta[name="${opts.prefix}-source"]`)?.getAttribute('content') ?? undefined;
|
|
276
|
+
return { components, handBuilt, ...(source ? { source } : {}) };
|
|
227
277
|
}
|
|
228
278
|
/** The components whose look is measured and compared; the rest follow their content (see sigOf). */
|
|
229
279
|
const MEASURED = new Set(['tab-bar', 'header', 'section', 'card', 'button', 'icon-button', 'chip']);
|
|
@@ -332,7 +382,9 @@ export function consistencyIssues(known, paths) {
|
|
|
332
382
|
}
|
|
333
383
|
/** Lab screens build catalog components with the components, not by hand. */
|
|
334
384
|
export function reuseIssues(path, page) {
|
|
335
|
-
|
|
385
|
+
// App snapshots preserve the exact product markup for visual review. They are not authored by the lab and cannot
|
|
386
|
+
// truthfully be rewritten to custom elements; only hand-authored lab screens are checked for component reuse.
|
|
387
|
+
if (familyOf(path) !== 'screens' || page.source === 'app')
|
|
336
388
|
return [];
|
|
337
389
|
return page.handBuilt.slice(0, 10).map((h) => ({
|
|
338
390
|
severity: 'error',
|
package/dist/design-md.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export declare function motionDurations(design?: Design): number[];
|
|
|
24
24
|
* colours are the dark theme and light-* colours make a light one.
|
|
25
25
|
*/
|
|
26
26
|
export declare function designThemes(): string[];
|
|
27
|
+
/** Keep an authored font stack intact, quoting only a single family whose name needs it. */
|
|
28
|
+
export declare function cssFontStack(value: string | undefined): string;
|
|
27
29
|
/** A type level in pixels, for comparing with computed styles. */
|
|
28
30
|
export interface Level {
|
|
29
31
|
name: string;
|
package/dist/design-md.js
CHANGED
|
@@ -55,19 +55,51 @@ export function designThemes() {
|
|
|
55
55
|
return ['dark'];
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
/** Convert a CSS length from DESIGN.md to pixels. CSS rem uses the 16px root; em and percentages use `relative`. */
|
|
59
|
+
function cssLength(value, relative, unitless) {
|
|
60
|
+
if (value === undefined || value === null)
|
|
61
|
+
return 0;
|
|
62
|
+
if (typeof value === 'number')
|
|
63
|
+
return unitless === 'relative' ? value * relative : value;
|
|
64
|
+
const match = /^(-?(?:\d+|\d*\.\d+))\s*(px|rem|em|%)?$/i.exec(value.trim());
|
|
65
|
+
if (!match)
|
|
66
|
+
return 0;
|
|
67
|
+
const n = Number(match[1]);
|
|
68
|
+
switch ((match[2] ?? '').toLowerCase()) {
|
|
69
|
+
case 'rem':
|
|
70
|
+
return n * 16;
|
|
71
|
+
case 'em':
|
|
72
|
+
case '%':
|
|
73
|
+
return n * relative * (match[2] === '%' ? 0.01 : 1);
|
|
74
|
+
case 'px':
|
|
75
|
+
return n;
|
|
76
|
+
default:
|
|
77
|
+
return unitless === 'relative' ? n * relative : n;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Keep an authored font stack intact, quoting only a single family whose name needs it. */
|
|
81
|
+
export function cssFontStack(value) {
|
|
82
|
+
const stack = value?.trim() ?? '';
|
|
83
|
+
if (!stack)
|
|
84
|
+
return '';
|
|
85
|
+
if (stack.includes(',') || /^['"].*['"]$/.test(stack) || /^[\w-]+$/.test(stack) || /^var\(/i.test(stack))
|
|
86
|
+
return stack;
|
|
87
|
+
return `"${stack.replaceAll('"', '\\"')}"`;
|
|
88
|
+
}
|
|
58
89
|
export function typeLevels(design = readDesign()) {
|
|
59
90
|
return Object.entries(design.typography).map(([name, t]) => {
|
|
60
|
-
const size =
|
|
61
|
-
const
|
|
91
|
+
const size = cssLength(t.fontSize, 16, 'px');
|
|
92
|
+
const lineHeightValue = t.lineHeight;
|
|
93
|
+
const lineHeight = typeof lineHeightValue === 'string' && lineHeightValue.trim().toLowerCase() === 'normal'
|
|
94
|
+
? size * 1.2
|
|
95
|
+
: cssLength(lineHeightValue, size, 'relative');
|
|
62
96
|
return {
|
|
63
97
|
name,
|
|
64
98
|
mono: /mono/i.test(t.fontFamily ?? ''),
|
|
65
99
|
size,
|
|
66
100
|
weight: Number(t.fontWeight ?? 400),
|
|
67
|
-
lineHeight
|
|
68
|
-
tracking: t.letterSpacing
|
|
69
|
-
? Number.parseFloat(t.letterSpacing) * size
|
|
70
|
-
: Number.parseFloat(t.letterSpacing ?? '0') || 0,
|
|
101
|
+
lineHeight,
|
|
102
|
+
tracking: cssLength(t.letterSpacing, size, 'px'),
|
|
71
103
|
};
|
|
72
104
|
});
|
|
73
105
|
}
|
package/dist/screens.d.ts
CHANGED
|
@@ -28,8 +28,8 @@ export interface Screen {
|
|
|
28
28
|
*/
|
|
29
29
|
page: string;
|
|
30
30
|
device: 'mobile' | 'tablet' | 'desktop';
|
|
31
|
-
/** Where the version came from:
|
|
32
|
-
source: 'stitch' | 'lab' | 'trial';
|
|
31
|
+
/** Where the version came from: app snapshot, generated draft or authored design. */
|
|
32
|
+
source: 'stitch' | 'lab' | 'trial' | 'app';
|
|
33
33
|
/** Short version label: "v3", "Stitch", "Trial 2". */
|
|
34
34
|
version: string;
|
|
35
35
|
status: Status;
|
package/dist/tokens.js
CHANGED
|
@@ -10,7 +10,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { argbFromHex, Hct, hexFromArgb, MaterialDynamicColors, SchemeContent, SchemeExpressive, SchemeFidelity, SchemeFruitSalad, SchemeMonochrome, SchemeNeutral, SchemeRainbow, SchemeTonalSpot, SchemeVibrant, } from '@material/material-color-utilities';
|
|
12
12
|
import { config as lab } from './config.js';
|
|
13
|
-
import { readDesign } from './design-md.js';
|
|
13
|
+
import { cssFontStack, readDesign } from './design-md.js';
|
|
14
14
|
import { POINTER, writeExports } from './exports.js';
|
|
15
15
|
import { ROOT } from './screens.js';
|
|
16
16
|
const SCHEMES = {
|
|
@@ -71,7 +71,7 @@ const typeDecls = {};
|
|
|
71
71
|
const typeRules = Object.entries(design.typography).map(([name, t]) => {
|
|
72
72
|
const fallback = `var(--${lab.prefix}-font-${/mono/i.test(t.fontFamily ?? '') ? 'mono' : 'sans'}-fallback)`;
|
|
73
73
|
const decl = [
|
|
74
|
-
`font: ${t.fontWeight} ${t.fontSize}/${t.lineHeight}
|
|
74
|
+
`font: ${t.fontWeight} ${t.fontSize}/${t.lineHeight} ${cssFontStack(t.fontFamily) || 'sans-serif'}, ${fallback}`,
|
|
75
75
|
`letter-spacing: ${t.letterSpacing ?? '0em'}`,
|
|
76
76
|
t.fontFeature ? `font-feature-settings: ${t.fontFeature}` : '',
|
|
77
77
|
name.endsWith('-caps') ? 'text-transform: uppercase' : '',
|
package/package.json
CHANGED
|
@@ -76,6 +76,11 @@ series) is added to DESIGN.md first, with a line in the prose saying where it is
|
|
|
76
76
|
a primary-filled button, a pill chip, a header icon button, a caps label). A screen that duplicates a
|
|
77
77
|
component instead of using it is not handed over: use the component, or extend it (step 4 of the ladder)
|
|
78
78
|
when it cannot do what the screen needs.
|
|
79
|
+
- The reuse detector reports what is actually drawn: descendants of a closed `<details>`, screen-reader-only or
|
|
80
|
+
clipped nodes, and elements hidden by a transparent ancestor are ignored. This keeps collapsed menus and
|
|
81
|
+
accessibility-only labels from becoming false component errors. A faithful app snapshot marked with
|
|
82
|
+
`<meta name="<prefix>-source" content="app">` is a visual baseline, not a component implementation, so its
|
|
83
|
+
markup is not reuse-checked; make a new lab-authored version before editing the snapshot.
|
|
79
84
|
- **consistency**: a component whose size, radius, font, icon set or icons differ from the catalog's version
|
|
80
85
|
(or, without a catalog, from the majority of screens in its family), or that appears in two versions on one
|
|
81
86
|
screen. `stitch2 consistency` shows the same as cropped images; see the stitch2-consistency skill.
|
|
@@ -13,7 +13,7 @@ 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>-page` (see Pages), `<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 | app),
|
|
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
|
|
|
@@ -22,6 +22,12 @@ It is not the document height: long pages still appear whole. In canvas frames a
|
|
|
22
22
|
`vh`, `svh`, `dvh` and `lvh` keep this viewport height when the frame grows, so full-height sections
|
|
23
23
|
do not create ever-growing gaps. Width responsiveness stays active.
|
|
24
24
|
|
|
25
|
+
`source=app` means an unchanged capture of a running application's rendered DOM,
|
|
26
|
+
not a hand-authored design. Layout and visual checks still apply; the `reuse`
|
|
27
|
+
rule does not demand replacing its already-rendered framework components with
|
|
28
|
+
lab elements. Keep the import URL as provenance. A new design based on a capture
|
|
29
|
+
uses `source=lab` and the shared component catalog, not the snapshot exception.
|
|
30
|
+
|
|
25
31
|
## Pages and their screens
|
|
26
32
|
|
|
27
33
|
A place in the app often has several states or steps: the workout with its number pad, its menu and its discard
|
|
@@ -6,6 +6,9 @@ project's DESIGN.md defines the levels (front matter `typography`); `stitch2 tok
|
|
|
6
6
|
`.type-<level>` class, `stitch2 check` warns about text that is not one of them, and `stitch2 type` lists every
|
|
7
7
|
style a screen uses.
|
|
8
8
|
|
|
9
|
+
Use CSS lengths for the type scale (`px`, `rem`, `em`); checks normalize them to rendered pixels
|
|
10
|
+
using a 16px root. A font-family list stays a CSS stack, not one quoted family name.
|
|
11
|
+
|
|
9
12
|
## 1. A type scale is a short list of levels
|
|
10
13
|
|
|
11
14
|
Google's DESIGN.md format suggests 9–15 levels, each fixing family, size, line height, weight and tracking
|
|
@@ -40,6 +40,11 @@ majority of screens in the same family.
|
|
|
40
40
|
| Two versions of one component on a screen | Pick the catalog's; if both are needed, they are two variants: add the variant to the component |
|
|
41
41
|
| The catalog itself is missing the variant a screen needs | Add it to the component and the catalog first, then use it |
|
|
42
42
|
|
|
43
|
+
The reuse check only considers what is drawn. Descendants of a closed `<details>`, clipped or screen-reader-only
|
|
44
|
+
nodes, and elements hidden by a transparent ancestor are excluded. Screens imported as faithful app snapshots may
|
|
45
|
+
declare `<meta name="<prefix>-source" content="app">`; those baselines are checked visually, but are not expected to
|
|
46
|
+
contain the lab's custom elements. Create a new lab-authored version before changing one.
|
|
47
|
+
|
|
43
48
|
Never fix consistency by editing one screen to imitate another by hand, and never edit an approved version:
|
|
44
49
|
make the next version.
|
|
45
50
|
|