@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 +14 -0
- package/README.md +45 -1
- package/STANDARD.md +48 -18
- package/dist/{audit-X7LGATAQ.js → audit-43PC4LMS.js} +3 -3
- package/dist/{chunk-RNHV2SXC.js → chunk-4LJFZH2I.js} +2 -2
- package/dist/{chunk-4QWEAL4G.js → chunk-CZ7KT452.js} +93 -1
- package/dist/{chunk-GNVZGG3E.js → chunk-D2PEKYKH.js} +24 -12
- package/dist/{chunk-NCBCZFDY.js → chunk-EDM4L5WJ.js} +5 -5
- package/dist/{chunk-J35UYAVV.js → chunk-EKGHL4D2.js} +4 -4
- package/dist/{chunk-W7PMRSDG.js → chunk-VDUIIWG7.js} +1 -1
- package/dist/{chunk-IHT6CJOM.js → chunk-WK2WMWTH.js} +1 -1
- package/dist/{chunk-LWKMGJSW.js → chunk-ZKZCPVQ6.js} +5 -5
- package/dist/{cli-DKI37OXK.js → cli-BZSJIUQH.js} +5 -5
- package/dist/{cli-3WWHRJRG.js → cli-GUEBFFF3.js} +4 -4
- package/dist/{cli-ZGB6MUN2.js → cli-KTN5OXHZ.js} +6 -6
- package/dist/{cli-KXVJJDN5.js → cli-OS5ESEJV.js} +9 -9
- package/dist/cli-RAH4CKYY.js +249 -0
- package/dist/{cli-U7WEVPZL.js → cli-T3IA7IQK.js} +6 -6
- package/dist/{cli-YD3VN52R.js → cli-VLNOQYEL.js} +5 -5
- package/dist/{cli-JB4X5LAG.js → cli-VPARW6FY.js} +1 -1
- package/dist/cli.js +3 -3
- package/dist/index.cjs +93 -1
- package/dist/index.js +93 -1
- package/dist/phone.cjs +275 -0
- package/dist/phone.d.cts +185 -0
- package/dist/phone.d.ts +185 -0
- package/dist/phone.js +237 -0
- package/package.json +11 -1
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
##
|
|
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-
|
|
12
|
+
} from "./chunk-VDUIIWG7.js";
|
|
13
13
|
import "./chunk-JYNXGCYZ.js";
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
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-
|
|
13
|
+
} from "./chunk-WK2WMWTH.js";
|
|
14
14
|
import {
|
|
15
15
|
TRADE_NOUNS
|
|
16
|
-
} from "./chunk-
|
|
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/,
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
653
|
+
return import("./cli-OS5ESEJV.js").then((m) => m.runReport(command, rest, io));
|
|
645
654
|
}
|
|
646
655
|
if (command === "register") {
|
|
647
|
-
return import("./cli-
|
|
656
|
+
return import("./cli-VPARW6FY.js").then((m) => m.runRegister(rest, io));
|
|
648
657
|
}
|
|
649
658
|
if (command === "estate") {
|
|
650
|
-
return import("./cli-
|
|
659
|
+
return import("./cli-T3IA7IQK.js").then((m) => m.runEstate(rest, io));
|
|
651
660
|
}
|
|
652
661
|
if (command === "null") {
|
|
653
|
-
return import("./cli-
|
|
662
|
+
return import("./cli-GUEBFFF3.js").then((m) => m.runNull(rest, io));
|
|
654
663
|
}
|
|
655
664
|
if (command === "calibrate") {
|
|
656
|
-
return import("./cli-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
25
|
+
} from "./chunk-WK2WMWTH.js";
|
|
26
26
|
import {
|
|
27
27
|
extractStrings
|
|
28
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
4
|
+
} from "./chunk-VDUIIWG7.js";
|
|
5
5
|
import {
|
|
6
6
|
describeTypicality,
|
|
7
7
|
loadNull
|
|
8
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
31
|
+
} from "./chunk-WK2WMWTH.js";
|
|
32
32
|
|
|
33
33
|
// src/audit/cli.ts
|
|
34
34
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
formatDirectionMatches,
|
|
5
5
|
summariseDirection,
|
|
6
6
|
validateDirection
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-4LJFZH2I.js";
|
|
8
8
|
import {
|
|
9
9
|
loadNull
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-EDM4L5WJ.js";
|
|
11
11
|
import {
|
|
12
12
|
parseFlags
|
|
13
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
10
|
+
} from "./chunk-4LJFZH2I.js";
|
|
11
11
|
import {
|
|
12
12
|
parseFlags
|
|
13
|
-
} from "./chunk-
|
|
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-
|
|
30
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
12
|
+
} from "./chunk-EDM4L5WJ.js";
|
|
13
13
|
import "./chunk-OQXTQQAU.js";
|
|
14
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-D2PEKYKH.js";
|
|
15
15
|
import "./chunk-QIEDBL4J.js";
|
|
16
16
|
import "./chunk-WUACK2HX.js";
|
|
17
|
-
import "./chunk-
|
|
18
|
-
import "./chunk-
|
|
17
|
+
import "./chunk-WK2WMWTH.js";
|
|
18
|
+
import "./chunk-CZ7KT452.js";
|
|
19
19
|
import "./chunk-QYBAVU3P.js";
|
|
20
20
|
export {
|
|
21
21
|
describeTypicality,
|