@domandigital/craft 0.19.0 → 0.19.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @domandigital/craft
2
2
 
3
+ ## 0.19.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 4ebd2d5: The phone-first gate for portals (DOM-775). `@domandigital/craft/phone` measures a route at 375px from inside a portal's own signed-in Playwright suite (sideways scroll against the layout viewport, a missing device-width viewport meta, targets under 44px, fields under 16px, text under 12px, a cut-off title), and `craft phone-first` judges those routes and the source's max-width queries and Tailwind `max-*` variants against a baseline that only shrinks.
8
+ - 51cded2: `measurePhone` leaves out what is parked off the page until it is needed, such as a skip link before focus or a closed drawer, as create-site's measure.mjs does (DOM-775).
9
+
10
+ ## 0.19.1
11
+
12
+ ### Patch Changes
13
+
14
+ - 5056c6a: STANDARD.md: a new section 10, "The phone is the base layout" (DOM-774): base styles are the phone and wider layouts use `min-width` only, 375px is the floor every surface is designed and measured at, touch targets are at least 44 x 44 CSS px, form fields at least 16px, reviews start from the phone capture, and performance is judged on the mobile Lighthouse profile. What is not a rule is now section 11 and says layout stays free at every width; the numbers are section 12. The older sections lose their em dashes to the house copy check, with the wording unchanged. Docs only.
15
+ - 00d79a0: `craft copy` reads `title` and `description` in Markdown front matter. A dash or a stock phrase in an agent description or a page title is reported on that line, and `copy-ok` on the line or the line above still suppresses it. Other front matter keys stay unread, and fenced code stays unread.
16
+
3
17
  ## 0.19.0
4
18
 
5
19
  ### Minor Changes
package/README.md CHANGED
@@ -67,7 +67,7 @@ module.exports = { presets: [tailwindV3Preset()] };
67
67
 
68
68
  ## The standard itself
69
69
 
70
- [STANDARD.md](./STANDARD.md) is the eleven-section house standard, every numeric
70
+ [STANDARD.md](./STANDARD.md) is the twelve-section house standard, every numeric
71
71
  rule cited, including a section on what is deliberately *not* a rule. Its
72
72
  canonical numbers are compared against the code on every test run, so the
73
73
  document cannot drift from what ships.
@@ -222,6 +222,50 @@ restraint rule) and art direction (`craft direction`, `craft estate`).
222
222
  under a passing average, and a truncated report fails rather than reading as
223
223
  "no report".
224
224
 
225
+ ## The phone-first gate for portals
226
+
227
+ STANDARD.md section 10 as a gate for an app behind a sign-in, such as a client
228
+ portal (portal standard rule 10, DOM-775). create-site's `measure.mjs` and
229
+ `mobile-first.mjs` do the same job for a site; a portal's pages need a
230
+ signed-in user and fixture data, which only the portal's own Playwright suite
231
+ can set up, so the gate comes in two halves.
232
+
233
+ The portal's suite opens each key route at `PHONE` (375 x 812, touch) and
234
+ measures it:
235
+
236
+ ```ts
237
+ import { test } from "@playwright/test";
238
+ import { PHONE, measurePhone } from "@domandigital/craft/phone";
239
+
240
+ test.use(PHONE);
241
+ for (const route of ["/portal", "/portal/invoices"]) {
242
+ test(`phone first: ${route}`, async ({ page }) => {
243
+ await page.goto(route);
244
+ await measurePhone(page, { route }); // writes phone-first/measured/portal-invoices.json
245
+ });
246
+ }
247
+ ```
248
+
249
+ Then `craft phone-first --measured phone-first/measured` judges every route,
250
+ and the source folders `phone-first.json` names, against the repo's baseline:
251
+
252
+ - **Static:** a `max-width` media query (any spelling, range syntax too) or a
253
+ Tailwind `max-*:` variant fails. A genuine exception takes
254
+ `mobile-first-ok: <reason>` on its line or the line above, or the whole file
255
+ in `phone-first.json` `exceptions` as `{ "file", "reason" }`.
256
+ - **Measured at 375px:** the page scrolls sideways (against the layout
257
+ viewport: under mobile emulation `innerWidth` grows with the page, so it
258
+ never shows the scroll), a page with no `width=device-width` viewport meta,
259
+ a target under 44 x 44 (a link inside a sentence is exempt; a field's label
260
+ counts), a text field under 16px, text under 12px, a cut-off `h1`.
261
+
262
+ `phone-first.baseline.json` lists what a repo already breaks, counted per file
263
+ and per route (digits folded, so fixture dates do not split one button into
264
+ many). The gate fails on anything the baseline does not list, and on a
265
+ baseline that claims more than is there, so it only shrinks: fix a breach and
266
+ lower its count in the same PR. `--update-baseline` writes the baseline that
267
+ makes the current run pass; run it once when the gate goes in.
268
+
225
269
  ## API
226
270
 
227
271
  | Area | Exports |
package/STANDARD.md CHANGED
@@ -1,17 +1,18 @@
1
1
  # The Doman Digital craft standard
2
2
 
3
- Eleven sections. Every numeric rule is cited, and section 10 says what is
3
+ Twelve sections. Every numeric rule is cited, and section 11 says what is
4
4
  deliberately *not* a rule.
5
5
 
6
6
  This document and the package are bound together by a test: the machine-readable
7
- block in section 11 is compared against `HOUSE_BUDGET`, `EASE` and
7
+ block in section 12 is compared against `HOUSE_BUDGET`, `EASE` and
8
8
  `DURATION_MS` on every run, so the standard cannot drift from what ships.
9
9
 
10
10
  ---
11
11
 
12
+ <!-- copy-ok: the standard names the practice each rule replaces -->
12
13
  ## 1. Colour is derived, not picked
13
14
 
14
- Anchors — colours a surface already ships — are sacred and come back byte-identical.
15
+ Anchors, the colours a surface already ships, are sacred and come back byte-identical.
15
16
  Everything else is generated in OKLCh from those anchors.
16
17
 
17
18
  - Ramps have **11 steps** (50–950).
@@ -19,7 +20,7 @@ Everything else is generated in OKLCh from those anchors.
19
20
  (CSS Color 4 §13.2). RGB clipping is rejected: it walks a saturated violet
20
21
  toward magenta.
21
22
  - Derived values standing in for a hand-picked one must be within
22
- **ΔE_OK ≤ 0.02** — the just-noticeable difference — or be declared an
23
+ **ΔE_OK ≤ 0.02** (the just-noticeable difference) or be declared an
23
24
  intentional change.
24
25
 
25
26
  *Source: Ottosson, "A perceptual color space for image processing" (2020); CSS Color 4 §13.2.*
@@ -40,9 +41,9 @@ Everything else is generated in OKLCh from those anchors.
40
41
  ## 3. Type is fluid, and always has a rem term
41
42
 
42
43
  - Steps **−2 to 5**, `clamp()` interpolated between a small and a large viewport.
43
- - The interpolation term is **`rem + vw`, never bare `vw`** — a bare-`vw` size
44
+ - The interpolation term is **`rem + vw`, never bare `vw`**. A bare-`vw` size
44
45
  ignores the browser text-size setting and fails WCAG 1.4.4 at 200% zoom.
45
- - Existing sizes are **pinned**, not regenerated, so adopting the scale moves
46
+ - Existing sizes are **pinned**, not regenerated, so adopting the scale moves <!-- copy-ok -->
46
47
  nothing.
47
48
 
48
49
  *Source: Utopia (utopia.fyi); WCAG 2.2 SC 1.4.4.*
@@ -58,7 +59,7 @@ Everything else is generated in OKLCh from those anchors.
58
59
  - Clipped display text gets a **descender mask** (`padding-bottom: .16em;
59
60
  margin-bottom: -.16em`), or it loses the tails of g, j, p, q, y.
60
61
 
61
- *Source: house habit — the −0.01em display tracking and the ch measure were arrived at independently in two client repos.*
62
+ *Source: house habit. The −0.01em display tracking and the ch measure were arrived at independently in two client repos.*
62
63
 
63
64
  ## 5. Space is fluid and proportional to type
64
65
 
@@ -66,7 +67,7 @@ Everything else is generated in OKLCh from those anchors.
66
67
  - Section rhythm has **three** sizes, not five. Five shipped with zero consumers,
67
68
  which is the signature of a scale invented ahead of a need.
68
69
 
69
- *Source: Utopia; measured — `--space-section-xs..xl` had no consumers in the monorepo.*
70
+ *Source: Utopia; measured: `--space-section-xs..xl` had no consumers in the monorepo.*
70
71
 
71
72
  ## 6. Density is a property of the surface
72
73
 
@@ -89,7 +90,7 @@ needs. Set once on a shell via `[data-density]`, inherited below.
89
90
  meaning it covers more ground per unit time arriving than it averaged.
90
91
  - **UI durations ≤ 300ms.** Drawers and one gated reveal may exceed it.
91
92
  - **Don't animate what fires 100×/day**, and never animate a keyboard-triggered
92
- interaction — the user is moving faster than the animation.
93
+ interaction: the user is moving faster than the animation.
93
94
  - **One signature moment per page**, gated, never the LCP element.
94
95
 
95
96
  *Source: Emil Kowalski, "Animations on the Web"; Core Web Vitals (LCP).*
@@ -101,12 +102,12 @@ needs. Set once on a shell via `[data-density]`, inherited below.
101
102
 
102
103
  **Never `animation: none` inside the block.** That drops the `forwards` fill along with the
103
104
  animation, so a forwards-filled entrance never applies its end state and the
104
- element keeps `opacity: 0` from its base rule — permanently invisible, for
105
+ element keeps `opacity: 0` from its base rule, permanently invisible, for
105
106
  exactly the users who asked for less motion. This shipped.
106
107
 
107
108
  Images: AVIF and WebP, in that order.
108
109
 
109
- *Source: WCAG 2.2 SC 2.3.3; measured — a live site was stranded at opacity 0 by this exact defect.*
110
+ *Source: WCAG 2.2 SC 2.3.3; measured: a live site was stranded at opacity 0 by this exact defect.*
110
111
 
111
112
  ## 9. Restraint is the budget
112
113
 
@@ -118,11 +119,11 @@ step: `code { font-size: 0.875em }` states one relationship and applies it
118
119
  wherever code appears, rather than adding a rung to the ladder. Counting it
119
120
  against the scale is the same category error as counting the framework's theme.
120
121
  It is still counted, because eight competing ratios is as unreadable as eight
121
- competing steps — split, not dropped. A value that stops being reported is a
122
+ competing steps: split, not dropped. A value that stops being reported is a
122
123
  value that grows.
123
124
 
124
125
  Differentiation guards pass anything that differs from its siblings, however
125
- ugly. What they cannot see is accumulation — eighteen font sizes, six shadows,
126
+ ugly. What they cannot see is accumulation: eighteen font sizes, six shadows,
126
127
  four competing accents, each added reasonably, together reading as noise.
127
128
 
128
129
  The budget counts the vocabulary a surface **chose**, and the whole difficulty
@@ -141,7 +142,7 @@ second reports a pass.
141
142
 
142
143
  The line is **used versus unused**, which is not a property of the layer:
143
144
 
144
- - References are **resolved**, not skipped — through the token chain, to the
145
+ - References are **resolved**, not skipped: through the token chain, to the <!-- copy-ok -->
145
146
  value that renders.
146
147
  - A definition is only ever reached from a declaration in a kept layer, so a
147
148
  framework default nothing uses still counts for nothing.
@@ -154,7 +155,34 @@ The line is **used versus unused**, which is not a property of the layer:
154
155
 
155
156
  *Source: measured against the estate. The budget is the observed ceiling of the surfaces that read as designed; the resolution rule is what it took to measure them at all.*
156
157
 
157
- ## 10. What is deliberately not a rule
158
+ ## 10. The phone is the base layout
159
+
160
+ Mobile first is a rule about **order and floor**, not about a layout. The phone
161
+ is designed, built and measured first, and every wider layout is derived from
162
+ it. A desktop layout squeezed onto a phone keeps the desktop's priorities and
163
+ drops whatever did not fit; a phone layout widened keeps what matters and adds.
164
+
165
+ - **The phone is the base.** Base styles are the phone layout. Wider layouts
166
+ are added with `min-width` queries only (Tailwind: unprefixed is the phone,
167
+ `sm:` `md:` `lg:` add). A `max-width` query, a `width <` range or a Tailwind
168
+ `max-*:` variant needs a recorded exception with its reason.
169
+ - **375px is the floor.** Every surface is designed and measured at **375 CSS
170
+ px** first, the narrowest iPhone width in common use (SE, mini, X to 11 Pro).
171
+ Nothing scrolls sideways at 375. 390 is still measured: it is the width the
172
+ design is accepted at.
173
+ - **Touch targets at least 44 × 44 CSS px.** A link inside a sentence is exempt,
174
+ as in WCAG's own inline exception. 24px (SC 2.5.8, AA) is what an audit asks
175
+ for; 44 is the house bar.
176
+ - **Form inputs at least 16px.** Safari on iOS zooms the page when a field set
177
+ smaller gets focus, and the visitor has to pinch back out. Fix the size, never
178
+ the viewport: `maximum-scale=1` takes zoom away from people who need it.
179
+ - **Reviews look at the phone capture first**, then the desktop one.
180
+ - **Performance is judged on the mobile Lighthouse profile.** A desktop score
181
+ is not evidence: the phone is the slower device and the one Google indexes.
182
+
183
+ *Source: WCAG 2.2 SC 2.5.5 (Target Size, Enhanced), SC 2.5.8 and SC 1.4.4; Apple Human Interface Guidelines, "Accessibility" (44 × 44 pt hit targets); WebKit's focus zoom on fields under 16px; Lighthouse's default mobile emulation; Google Search Central, "Mobile-first indexing"; Luke Wroblewski, "Mobile First" (2011).*
184
+
185
+ ## 11. What is deliberately not a rule
158
186
 
159
187
  The package emits **plain CSS custom properties** precisely so these stay free:
160
188
 
@@ -162,9 +190,11 @@ The package emits **plain CSS custom properties** precisely so these stay free:
162
190
  two; the third needs nothing.
163
191
  - **The motion library.** `EASE_TUPLE` carries the same control points as `EASE`,
164
192
  so Framer Motion, GSAP and CSS agree without the package choosing one.
165
- - **Where tokens live.** A `:root` block, a generated file, a Tailwind theme —
193
+ - **Where tokens live.** A `:root` block, a generated file, a Tailwind theme:
166
194
  the package does not care.
167
- - **Layout, grid and component structure.** Not a rule. Layout *defaults*
195
+ - **Layout, grid and component structure.** Not a rule, at any width. Section
196
+ 10 fixes the order the widths are designed in and the floor they are
197
+ measured at; what the phone layout looks like stays free. Layout *defaults*
168
198
  (hero straight into a trust strip, three icon cards, a reveal on every
169
199
  section) are measured as character in [CHARACTER.md](./CHARACTER.md), as
170
200
  warnings with a declared-exception path, never as rules here.
@@ -172,7 +202,7 @@ The package emits **plain CSS custom properties** precisely so these stay free:
172
202
 
173
203
  A rule here has to be one that is wrong to break. Everything else is a default.
174
204
 
175
- ## 11. The numbers
205
+ ## 12. The numbers
176
206
 
177
207
  <!-- craft:canonical -->
178
208
  ```json
@@ -9,10 +9,10 @@ import {
9
9
  snapshotPage,
10
10
  snapshotUrl,
11
11
  snapshotUrls
12
- } from "./chunk-W7PMRSDG.js";
12
+ } from "./chunk-VDUIIWG7.js";
13
13
  import "./chunk-JYNXGCYZ.js";
14
- import "./chunk-IHT6CJOM.js";
15
- import "./chunk-4QWEAL4G.js";
14
+ import "./chunk-WK2WMWTH.js";
15
+ import "./chunk-CZ7KT452.js";
16
16
  import "./chunk-QYBAVU3P.js";
17
17
  export {
18
18
  PROVENANCE_BUDGET_MS,
@@ -10,10 +10,10 @@ import {
10
10
  levenshtein,
11
11
  normaliseFamily,
12
12
  parseColour
13
- } from "./chunk-IHT6CJOM.js";
13
+ } from "./chunk-WK2WMWTH.js";
14
14
  import {
15
15
  TRADE_NOUNS
16
- } from "./chunk-4QWEAL4G.js";
16
+ } from "./chunk-CZ7KT452.js";
17
17
 
18
18
  // src/direction/types.ts
19
19
  var DIRECTION_VERSION = 2;
@@ -263,8 +263,100 @@ function markupParts(file) {
263
263
  function textNodeIsProse(body) {
264
264
  return /[a-zA-Z]{2,}/.test(body) && /\s/.test(body.trim()) || /[a-zA-Z]{4,}[.!?]/.test(body);
265
265
  }
266
+ var FRONT_MATTER_PROSE = /* @__PURE__ */ new Set(["title", "description"]);
267
+ var KEY_LINE = /^(\s*)(?:-\s+)?([A-Za-z][\w-]*)[ \t]*:[ \t]*/;
268
+ var BLOCK_SCALAR = /^(?:[|>][+-]?\d*|[|>]\d+[+-]?)$/;
269
+ function readFrontKey(line) {
270
+ const m = KEY_LINE.exec(line);
271
+ if (!m) return null;
272
+ return { indent: m[1].length, name: m[2], valueAt: m[0].length, value: line.slice(m[0].length) };
273
+ }
274
+ function indentOf(line) {
275
+ let i = 0;
276
+ while (i < line.length && (line[i] === " " || line[i] === " ")) i += 1;
277
+ return i;
278
+ }
279
+ function commentAt(value) {
280
+ if (value.startsWith("#")) return 0;
281
+ const found = /[ \t]+#/.exec(value);
282
+ return found ? found.index : -1;
283
+ }
284
+ function keepSpan(line, from, to) {
285
+ const start = Math.max(0, Math.min(from, line.length));
286
+ const end = Math.max(start, Math.min(to, line.length));
287
+ return " ".repeat(start) + line.slice(start, end) + " ".repeat(line.length - end);
288
+ }
289
+ function quoteEnd(line, from, quote) {
290
+ for (let i = from; i < line.length; i += 1) {
291
+ if (quote === '"' && line[i] === "\\") {
292
+ i += 1;
293
+ continue;
294
+ }
295
+ if (quote === "'" && line[i] === "'" && line[i + 1] === "'") {
296
+ i += 1;
297
+ continue;
298
+ }
299
+ if (line[i] === quote) return i;
300
+ }
301
+ return -1;
302
+ }
303
+ function maskFrontMatter(block) {
304
+ const lines = block.split("\n");
305
+ const out = lines.map((line) => blank(line));
306
+ for (let i = 1; i < lines.length; ) {
307
+ const line = lines[i];
308
+ if (line === "---" || line === "...") break;
309
+ const key = readFrontKey(line);
310
+ if (!key || !FRONT_MATTER_PROSE.has(key.name.toLowerCase())) {
311
+ i += 1;
312
+ continue;
313
+ }
314
+ const quote = key.value.startsWith('"') ? '"' : key.value.startsWith("'") ? "'" : "";
315
+ if (quote) {
316
+ let cursor = key.valueAt + 1;
317
+ while (i < lines.length && lines[i] !== "---" && lines[i] !== "...") {
318
+ const current = lines[i];
319
+ const end = quoteEnd(current, cursor, quote);
320
+ out[i] = keepSpan(current, cursor, end === -1 ? current.length : end);
321
+ i += 1;
322
+ cursor = 0;
323
+ if (end !== -1) break;
324
+ }
325
+ continue;
326
+ }
327
+ const hash = commentAt(key.value);
328
+ const body = (hash === -1 ? key.value : key.value.slice(0, hash)).trim();
329
+ if (BLOCK_SCALAR.test(body)) {
330
+ i += 1;
331
+ while (i < lines.length && lines[i] !== "---" && lines[i] !== "...") {
332
+ const current = lines[i];
333
+ if (current.trim() !== "" && indentOf(current) <= key.indent) break;
334
+ if (current.trim()) out[i] = current;
335
+ i += 1;
336
+ }
337
+ continue;
338
+ }
339
+ if (body === "" || body === "~" || /^null$/i.test(body) || body.startsWith("[") || body.startsWith("{")) {
340
+ i += 1;
341
+ continue;
342
+ }
343
+ out[i] = keepSpan(line, key.valueAt, hash === -1 ? line.length : key.valueAt + hash);
344
+ i += 1;
345
+ while (i < lines.length && lines[i] !== "---" && lines[i] !== "...") {
346
+ const current = lines[i];
347
+ if (current.trim() === "" || indentOf(current) <= key.indent) break;
348
+ if (!/^\s*#/.test(current)) {
349
+ const cut = commentAt(current);
350
+ out[i] = cut === -1 ? current : keepSpan(current, 0, cut);
351
+ }
352
+ i += 1;
353
+ }
354
+ }
355
+ const masked = out.join("\n");
356
+ return masked.length === block.length ? masked : blank(block);
357
+ }
266
358
  function proseBlocks(file) {
267
- const masked = file.text.replace(/^(```|~~~)[\s\S]*?^\1/gm, blank).replace(/`[^`\n]*`/g, (m) => " ".repeat(m.length)).replace(/^---\n[\s\S]*?\n---\n/, blank).replace(/<!--[\s\S]*?-->/g, blank);
359
+ const masked = file.text.replace(/^(```|~~~)[\s\S]*?^\1/gm, blank).replace(/`[^`\n]*`/g, (m) => " ".repeat(m.length)).replace(/^---\n[\s\S]*?\n---\n/, (block) => maskFrontMatter(block)).replace(/<!--[\s\S]*?-->/g, blank);
268
360
  return [{ path: file.path, text: masked, offset: 0 }];
269
361
  }
270
362
  function isData(path) {
@@ -10,7 +10,7 @@ import {
10
10
  holdHouseBlocks,
11
11
  houseRule,
12
12
  scanSource
13
- } from "./chunk-IHT6CJOM.js";
13
+ } from "./chunk-WK2WMWTH.js";
14
14
  import {
15
15
  compareCompetitor,
16
16
  compareFacts,
@@ -21,7 +21,7 @@ import {
21
21
  pageCopy,
22
22
  protectedFacts,
23
23
  visibleText
24
- } from "./chunk-4QWEAL4G.js";
24
+ } from "./chunk-CZ7KT452.js";
25
25
 
26
26
  // src/character/config.ts
27
27
  var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
@@ -391,6 +391,7 @@ Usage
391
391
  craft register list [--json]
392
392
  craft register brief <plain|persuasive|warm|literary> [--json]
393
393
  craft register check <file> --as <register> [--json]
394
+ craft phone-first [--config phone-first.json] [--measured <dir>] [--update-baseline] [--json]
394
395
 
395
396
  scan Markup, component code and stylesheets (source and compiled CSS).
396
397
  --staged reads the git index, for a pre-commit hook.
@@ -445,6 +446,14 @@ register Writing in a chosen register (REGISTERS.md). brief prints what to
445
446
  do before drafting; check places a draft of five or more
446
447
  paragraphs against the human baselines for that register. Never
447
448
  fails: exit 0, or 2 on bad usage.
449
+ phone-first The phone-first gate for a portal or app (STANDARD.md section
450
+ 10). Reads phone-first.json; fails on a max-width media query or a
451
+ Tailwind max-* variant in its source folders and, with --measured,
452
+ on what @domandigital/craft/phone measured at 375px (sideways
453
+ scroll, targets under 44px, fields under 16px, text under 12px, a
454
+ cut-off title), unless phone-first.baseline.json already lists it.
455
+ A baseline that claims more than is there fails too: it only
456
+ shrinks. Exit 1 on a breach, 2 on bad usage.
448
457
 
449
458
  ci options (scan, copy, audit)
450
459
  --baseline <file> Report only findings the baseline does not already
@@ -601,7 +610,7 @@ async function pageSnapshot(target, cwd) {
601
610
  return readSnapshot(JSON.parse(readFileSync(local, "utf8")), target);
602
611
  }
603
612
  if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL nor a snapshot file`);
604
- const { snapshotUrl } = await import("./audit-X7LGATAQ.js");
613
+ const { snapshotUrl } = await import("./audit-43PC4LMS.js");
605
614
  return snapshotUrl(target);
606
615
  }
607
616
  async function briefFor(flags, cwd) {
@@ -612,7 +621,7 @@ async function briefFor(flags, cwd) {
612
621
  if (typeof brief === "string" && brief.trim()) return brief;
613
622
  } else if (flags.direction) throw new Error(`no such file: ${flags.direction}`);
614
623
  if (flags.null) {
615
- const { loadNull } = await import("./cli-3WWHRJRG.js");
624
+ const { loadNull } = await import("./cli-GUEBFFF3.js");
616
625
  return loadNull(flags.null, cwd).brief;
617
626
  }
618
627
  return void 0;
@@ -638,31 +647,34 @@ async function compareWithCompetitor(flags, io) {
638
647
  function run(argv, io) {
639
648
  const [command, ...rest] = argv;
640
649
  if (command === "direction") {
641
- return import("./cli-DKI37OXK.js").then((m) => m.runDirection(rest, io));
650
+ return import("./cli-BZSJIUQH.js").then((m) => m.runDirection(rest, io));
642
651
  }
643
652
  if (command === "report" || command === "retrofit") {
644
- return import("./cli-KXVJJDN5.js").then((m) => m.runReport(command, rest, io));
653
+ return import("./cli-OS5ESEJV.js").then((m) => m.runReport(command, rest, io));
645
654
  }
646
655
  if (command === "register") {
647
- return import("./cli-JB4X5LAG.js").then((m) => m.runRegister(rest, io));
656
+ return import("./cli-VPARW6FY.js").then((m) => m.runRegister(rest, io));
648
657
  }
649
658
  if (command === "estate") {
650
- return import("./cli-U7WEVPZL.js").then((m) => m.runEstate(rest, io));
659
+ return import("./cli-T3IA7IQK.js").then((m) => m.runEstate(rest, io));
651
660
  }
652
661
  if (command === "null") {
653
- return import("./cli-3WWHRJRG.js").then((m) => m.runNull(rest, io));
662
+ return import("./cli-GUEBFFF3.js").then((m) => m.runNull(rest, io));
654
663
  }
655
664
  if (command === "calibrate") {
656
- return import("./cli-YD3VN52R.js").then((m) => m.runCalibrate(rest, io));
665
+ return import("./cli-VLNOQYEL.js").then((m) => m.runCalibrate(rest, io));
657
666
  }
658
667
  if (command === "tells" && rest[0] === "harvest") {
659
- return import("./cli-3WWHRJRG.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
668
+ return import("./cli-GUEBFFF3.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
660
669
  io.err(`craft: ${error.message}`);
661
670
  return 2;
662
671
  });
663
672
  }
673
+ if (command === "phone-first") {
674
+ return import("./cli-RAH4CKYY.js").then((m) => m.runPhoneFirst(rest, io));
675
+ }
664
676
  if (command === "snapshot" || command === "audit") {
665
- return import("./cli-ZGB6MUN2.js").then((m) => m.runAudit(command, rest, io));
677
+ return import("./cli-KTN5OXHZ.js").then((m) => m.runAudit(command, rest, io));
666
678
  }
667
679
  if (!command || command === "help" || command === "--help" || command === "-h") {
668
680
  io.out(HELP);
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-OQXTQQAU.js";
4
4
  import {
5
5
  parseFlags
6
- } from "./chunk-GNVZGG3E.js";
6
+ } from "./chunk-D2PEKYKH.js";
7
7
  import {
8
8
  toJson
9
9
  } from "./chunk-QIEDBL4J.js";
@@ -22,10 +22,10 @@ import {
22
22
  hueSwatch,
23
23
  nullPrompt,
24
24
  scanSource
25
- } from "./chunk-IHT6CJOM.js";
25
+ } from "./chunk-WK2WMWTH.js";
26
26
  import {
27
27
  extractStrings
28
- } from "./chunk-4QWEAL4G.js";
28
+ } from "./chunk-CZ7KT452.js";
29
29
 
30
30
  // src/null/cli.ts
31
31
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
@@ -120,7 +120,7 @@ async function build(flags, io) {
120
120
  const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
121
121
  if (toShoot.length) {
122
122
  io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
123
- const { snapshotUrls } = await import("./audit-X7LGATAQ.js");
123
+ const { snapshotUrls } = await import("./audit-43PC4LMS.js");
124
124
  const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
125
125
  results.forEach((r, i) => {
126
126
  const id = toShoot[i];
@@ -241,7 +241,7 @@ async function importPages(flags, io) {
241
241
  const toShoot = pages.filter((p) => !existsSync(join(shots, `${p.id}.json`)));
242
242
  if (toShoot.length) {
243
243
  io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"} from ${flags.builder}`);
244
- const { snapshotUrls } = await import("./audit-X7LGATAQ.js");
244
+ const { snapshotUrls } = await import("./audit-43PC4LMS.js");
245
245
  const servers = /* @__PURE__ */ new Map();
246
246
  try {
247
247
  for (const p of toShoot) if (!servers.has(p.root)) servers.set(p.root, await serveDir(p.root));
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  snapshotUrl,
3
3
  snapshotUrls
4
- } from "./chunk-W7PMRSDG.js";
4
+ } from "./chunk-VDUIIWG7.js";
5
5
  import {
6
6
  describeTypicality,
7
7
  loadNull
8
- } from "./chunk-NCBCZFDY.js";
8
+ } from "./chunk-EDM4L5WJ.js";
9
9
  import {
10
10
  SOURCE_FILE,
11
11
  finish,
@@ -14,7 +14,7 @@ import {
14
14
  parseFlags,
15
15
  prepareReport,
16
16
  readPaths
17
- } from "./chunk-GNVZGG3E.js";
17
+ } from "./chunk-D2PEKYKH.js";
18
18
  import {
19
19
  toJson
20
20
  } from "./chunk-QIEDBL4J.js";
@@ -28,7 +28,7 @@ import {
28
28
  rhythmOf,
29
29
  scanSource,
30
30
  typicality
31
- } from "./chunk-IHT6CJOM.js";
31
+ } from "./chunk-WK2WMWTH.js";
32
32
 
33
33
  // src/audit/cli.ts
34
34
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  auditSnapshot,
6
6
  fingerprint
7
- } from "./chunk-IHT6CJOM.js";
7
+ } from "./chunk-WK2WMWTH.js";
8
8
  import {
9
9
  SNAPSHOT_VERSION
10
10
  } from "./chunk-QYBAVU3P.js";
@@ -9,7 +9,7 @@ import {
9
9
  parseFile,
10
10
  sentencesOf,
11
11
  specificity
12
- } from "./chunk-4QWEAL4G.js";
12
+ } from "./chunk-CZ7KT452.js";
13
13
  import {
14
14
  SNAPSHOT_VERSION
15
15
  } from "./chunk-QYBAVU3P.js";
@@ -4,13 +4,13 @@ import {
4
4
  formatDirectionMatches,
5
5
  summariseDirection,
6
6
  validateDirection
7
- } from "./chunk-RNHV2SXC.js";
7
+ } from "./chunk-4LJFZH2I.js";
8
8
  import {
9
9
  loadNull
10
- } from "./chunk-NCBCZFDY.js";
10
+ } from "./chunk-EDM4L5WJ.js";
11
11
  import {
12
12
  parseFlags
13
- } from "./chunk-GNVZGG3E.js";
13
+ } from "./chunk-D2PEKYKH.js";
14
14
  import {
15
15
  toJson
16
16
  } from "./chunk-QIEDBL4J.js";
@@ -30,7 +30,7 @@ import {
30
30
  rhythmOf,
31
31
  shapeName,
32
32
  temperatureName
33
- } from "./chunk-IHT6CJOM.js";
33
+ } from "./chunk-WK2WMWTH.js";
34
34
 
35
35
  // src/estate/cli.ts
36
36
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -133,7 +133,7 @@ async function snapshotOf(target, cwd) {
133
133
  return snap;
134
134
  }
135
135
  if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL, a snapshot file, nor a site in the register`);
136
- const { snapshotUrl } = await import("./audit-X7LGATAQ.js");
136
+ const { snapshotUrl } = await import("./audit-43PC4LMS.js");
137
137
  return snapshotUrl(target);
138
138
  }
139
139
  var line = (distance, sibling, shared) => `${distance.toFixed(2)}${sibling ? " SIBLING" : ""}${shared.length ? ` shares ${shared.join(", ")}` : ""}`;
@@ -7,10 +7,10 @@ import {
7
7
  PROPOSED,
8
8
  researchPrompt,
9
9
  validateDirection
10
- } from "./chunk-RNHV2SXC.js";
10
+ } from "./chunk-4LJFZH2I.js";
11
11
  import {
12
12
  parseFlags
13
- } from "./chunk-GNVZGG3E.js";
13
+ } from "./chunk-D2PEKYKH.js";
14
14
  import {
15
15
  toJson
16
16
  } from "./chunk-QIEDBL4J.js";
@@ -26,8 +26,8 @@ import {
26
26
  isCream,
27
27
  oklchToRgb,
28
28
  parseColour
29
- } from "./chunk-IHT6CJOM.js";
30
- import "./chunk-4QWEAL4G.js";
29
+ } from "./chunk-WK2WMWTH.js";
30
+ import "./chunk-CZ7KT452.js";
31
31
  import "./chunk-QYBAVU3P.js";
32
32
 
33
33
  // src/direction/cli.ts
@@ -208,7 +208,7 @@ var readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
208
208
  async function snapshotFrom(target, cwd) {
209
209
  if (!target) return void 0;
210
210
  if (/^https?:\/\//.test(target)) {
211
- const { snapshotUrl } = await import("./audit-X7LGATAQ.js");
211
+ const { snapshotUrl } = await import("./audit-43PC4LMS.js");
212
212
  return snapshotUrl(target);
213
213
  }
214
214
  return readSnapshot(readJson(resolve(cwd, target)), target);
@@ -9,13 +9,13 @@ import {
9
9
  runHarvest,
10
10
  runNull,
11
11
  serveDir
12
- } from "./chunk-NCBCZFDY.js";
12
+ } from "./chunk-EDM4L5WJ.js";
13
13
  import "./chunk-OQXTQQAU.js";
14
- import "./chunk-GNVZGG3E.js";
14
+ import "./chunk-D2PEKYKH.js";
15
15
  import "./chunk-QIEDBL4J.js";
16
16
  import "./chunk-WUACK2HX.js";
17
- import "./chunk-IHT6CJOM.js";
18
- import "./chunk-4QWEAL4G.js";
17
+ import "./chunk-WK2WMWTH.js";
18
+ import "./chunk-CZ7KT452.js";
19
19
  import "./chunk-QYBAVU3P.js";
20
20
  export {
21
21
  describeTypicality,