@3xhaust/oh-my-design 0.16.1
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/LICENSE +21 -0
- package/README.ko.md +282 -0
- package/README.md +282 -0
- package/adapters/build.ts +184 -0
- package/adapters/claude.ts +111 -0
- package/adapters/codex.ts +68 -0
- package/adapters/tokens.ts +21 -0
- package/adapters/tool-map.json +7 -0
- package/bin/omd-install.ts +76 -0
- package/bin/omd.ts +1774 -0
- package/core/asset-sourcing/index.ts +187 -0
- package/core/coach/index.ts +98 -0
- package/core/composition/app-shell-workbench.md +168 -0
- package/core/composition/asymmetric-diagonal-grid.md +178 -0
- package/core/composition/bento-grid.md +223 -0
- package/core/composition/editorial-index-labels.md +173 -0
- package/core/composition/form-wizard-stepper.md +132 -0
- package/core/composition/master-detail-flow.md +129 -0
- package/core/composition/section-inversion.md +182 -0
- package/core/composition/sidebar-margin-annotation.md +184 -0
- package/core/composition/split-screen-hero.md +224 -0
- package/core/composition/sticky-sidebar-scroll.md +219 -0
- package/core/composition/typographic-hero.md +100 -0
- package/core/composition-contract/index.ts +233 -0
- package/core/composition-contract/visual-richness.ts +101 -0
- package/core/config/index.ts +27 -0
- package/core/copy/index.ts +296 -0
- package/core/craft/finish-pass.md +356 -0
- package/core/craft/index.ts +33 -0
- package/core/design/index.ts +581 -0
- package/core/design/interaction-states.ts +189 -0
- package/core/eval-harness/index.ts +225 -0
- package/core/evidence/final.ts +444 -0
- package/core/evidence/task.ts +459 -0
- package/core/figma/client.ts +312 -0
- package/core/figma/diff.ts +261 -0
- package/core/figma/responsive.ts +310 -0
- package/core/figma/system.ts +466 -0
- package/core/figma/types.ts +185 -0
- package/core/frame/check-ux.ts +180 -0
- package/core/frame/index.ts +39 -0
- package/core/frame/write.ts +168 -0
- package/core/graphics/css-illustration-primitives.md +189 -0
- package/core/graphics/duotone-image-presets.md +173 -0
- package/core/graphics/gradient-mesh.md +178 -0
- package/core/graphics/noise-grain-texture.md +150 -0
- package/core/graphics/placeholder-policy.md +185 -0
- package/core/graphics/svg-geometric-patterns.md +145 -0
- package/core/history/index.ts +39 -0
- package/core/install/detect.ts +32 -0
- package/core/install/install.ts +321 -0
- package/core/install/patch-claude.ts +109 -0
- package/core/install/patch-codex.ts +82 -0
- package/core/interaction/index.ts +101 -0
- package/core/interaction/recipes/signature-lighting.md +203 -0
- package/core/ir/dom.ts +352 -0
- package/core/ir/normalize.ts +177 -0
- package/core/motion/easing.md +151 -0
- package/core/motion/energy.ts +188 -0
- package/core/motion/recipes/image-hover-distortion.md +198 -0
- package/core/motion/recipes/magnetic-hover.md +229 -0
- package/core/motion/recipes/marquee.md +202 -0
- package/core/motion/recipes/number-counter.md +242 -0
- package/core/motion/recipes/page-loader.md +240 -0
- package/core/motion/recipes/parallax.md +233 -0
- package/core/motion/recipes/scroll-reveal.md +210 -0
- package/core/motion/recipes/section-color-inversion.md +217 -0
- package/core/motion/recipes/split-text-entrance.md +222 -0
- package/core/motion/recipes/stagger-orchestrator.md +227 -0
- package/core/motion/recipes/sticky-scene-transition.md +246 -0
- package/core/motion/recipes/view-transitions.md +239 -0
- package/core/probe/index.ts +186 -0
- package/core/probe/schema.json +46 -0
- package/core/protocol/composition-contract.md +192 -0
- package/core/protocol/copy-deck.md +96 -0
- package/core/protocol/human-design-loop.md +447 -0
- package/core/protocol/slop-review.md +71 -0
- package/core/ref/batch.ts +94 -0
- package/core/ref/blueprint.ts +199 -0
- package/core/ref/distance.ts +225 -0
- package/core/ref/invariants.ts +162 -0
- package/core/ref/signal.ts +37 -0
- package/core/ref/store.ts +122 -0
- package/core/render/index.ts +607 -0
- package/core/rules/attribution.ts +159 -0
- package/core/rules/builtin/contrast.yaml +8 -0
- package/core/rules/builtin/focus.yaml +8 -0
- package/core/rules/builtin/hit-area.yaml +8 -0
- package/core/rules/builtin/ko.yaml +6 -0
- package/core/rules/builtin/motion.yaml +80 -0
- package/core/rules/builtin/slop.yaml +365 -0
- package/core/rules/builtin/spacing.yaml +9 -0
- package/core/rules/builtin/system.yaml +42 -0
- package/core/rules/builtin/token.yaml +8 -0
- package/core/rules/builtin/ux.yaml +125 -0
- package/core/rules/engine.ts +88 -0
- package/core/rules/leakage.ts +72 -0
- package/core/rules/motion-spec.ts +204 -0
- package/core/site/index.ts +100 -0
- package/core/slop/index.ts +427 -0
- package/core/slop/text-slop.ts +220 -0
- package/core/source-seal/index.ts +174 -0
- package/core/stack/index.ts +53 -0
- package/core/target/index.ts +183 -0
- package/core/theory/color.md +285 -0
- package/core/theory/components.md +272 -0
- package/core/theory/craft.md +270 -0
- package/core/theory/expressive.md +213 -0
- package/core/theory/imagegen.md +105 -0
- package/core/theory/layout.md +159 -0
- package/core/theory/motion.md +283 -0
- package/core/theory/typography.md +134 -0
- package/core/theory/ux.md +580 -0
- package/core/theory/voice.md +595 -0
- package/core/types.ts +684 -0
- package/package.json +40 -0
- package/src/agents/composer.agent.yaml +174 -0
- package/src/agents/eye.agent.yaml +208 -0
- package/src/agents/framer.agent.yaml +52 -0
- package/src/agents/glance.agent.yaml +19 -0
- package/src/agents/hand.agent.yaml +200 -0
- package/src/agents/scout.agent.yaml +71 -0
- package/src/agents/sketch.agent.yaml +64 -0
- package/src/agents/typesetter.agent.yaml +52 -0
- package/src/agents/writer.agent.yaml +63 -0
- package/src/skills/omd-coach/SKILL.md +40 -0
- package/src/skills/omd-critique/SKILL.md +77 -0
- package/src/skills/omd-figma/SKILL.md +256 -0
- package/src/skills/omd-humanize/SKILL.md +160 -0
- package/src/skills/omd-scout/SKILL.md +77 -0
- package/src/skills/omd-ultradesign/SKILL.md +521 -0
package/core/types.ts
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
export type Hex = string;
|
|
2
|
+
|
|
3
|
+
/** A style value paired with the token it came from. `token: null` *is* the report. */
|
|
4
|
+
export interface Styled {
|
|
5
|
+
value: Hex | number;
|
|
6
|
+
token: string | null;
|
|
7
|
+
/** Set by the DOM reader: false when the browser, not the author, chose this. */
|
|
8
|
+
authored?: boolean;
|
|
9
|
+
/** Set by the DOM reader: the nearest painted ancestor fill, borrowed for contrast. */
|
|
10
|
+
inherited?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Box { x: number; y: number; w: number; h: number }
|
|
14
|
+
|
|
15
|
+
export type LayoutMode = 'VERTICAL' | 'HORIZONTAL';
|
|
16
|
+
|
|
17
|
+
export interface Layout {
|
|
18
|
+
mode: LayoutMode;
|
|
19
|
+
gap: number;
|
|
20
|
+
/** top, right, bottom, left */
|
|
21
|
+
padding: [number, number, number, number];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RawNode {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
type: 'FRAME' | 'TEXT';
|
|
28
|
+
path: string;
|
|
29
|
+
parent: string | null;
|
|
30
|
+
box: Box;
|
|
31
|
+
children: string[];
|
|
32
|
+
layout?: Layout;
|
|
33
|
+
fill?: Styled;
|
|
34
|
+
radius?: Styled;
|
|
35
|
+
color?: Hex;
|
|
36
|
+
interactive?: boolean;
|
|
37
|
+
inline?: boolean;
|
|
38
|
+
|
|
39
|
+
// Read only so that slop can be detected. Convergence to the mean is invisible
|
|
40
|
+
// node-by-node; it only shows up in the distribution.
|
|
41
|
+
/** The element's own text, trimmed, capped. Empty for containers. */
|
|
42
|
+
text?: string;
|
|
43
|
+
/** Heading level, 1-6. Set only for h1..h6. */
|
|
44
|
+
heading?: number;
|
|
45
|
+
shadow?: Styled;
|
|
46
|
+
/** The raw `background-image` when it is a gradient. */
|
|
47
|
+
gradient?: string;
|
|
48
|
+
textAlign?: 'left' | 'center' | 'right' | 'justify';
|
|
49
|
+
|
|
50
|
+
// Typography, set only on text-bearing nodes.
|
|
51
|
+
/** The first font-family in the stack, unquoted and lowercased ("inter", "georgia"). */
|
|
52
|
+
fontFamily?: string;
|
|
53
|
+
/** Computed font-size in px, rounded. */
|
|
54
|
+
fontSize?: number;
|
|
55
|
+
/** Computed font-weight, numeric. */
|
|
56
|
+
fontWeight?: number;
|
|
57
|
+
/** line-height as a ratio to font-size, 2dp. 'normal' resolves to 1.2. */
|
|
58
|
+
lineHeight?: number;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Computed word-break value, captured on text-bearing nodes.
|
|
62
|
+
* Korean may use word- or character-based breaking under KLREQ; acceptability depends on
|
|
63
|
+
* the actual copy, face, and container and is reviewed in typography proof.
|
|
64
|
+
* Set only on text-bearing nodes (where `type === 'TEXT'`).
|
|
65
|
+
*/
|
|
66
|
+
wordBreak?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Computed text-wrap value, captured on text-bearing nodes when the browser supports it
|
|
69
|
+
* (Chrome 114+, Firefox 121+). 'balance' is the recommended value for headings to prevent
|
|
70
|
+
* orphaned syllables. Absent when the browser returns an empty or unsupported value.
|
|
71
|
+
*/
|
|
72
|
+
textWrap?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Computed overflow value. Absent when 'visible' (the CSS default) to save space.
|
|
75
|
+
* Used by SYS-TEXT-CLIP to identify parents that visually clip their overflowing children.
|
|
76
|
+
* Only 'hidden' and 'clip' cut off content without providing a scrollbar escape.
|
|
77
|
+
*/
|
|
78
|
+
overflow?: string;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Set when background-clip: text is combined with a gradient on this node — the
|
|
82
|
+
* gradient-text tell: a gradient clipped to the text shape, sacrificing legibility
|
|
83
|
+
* for an effect that every generated landing page now shows.
|
|
84
|
+
*/
|
|
85
|
+
clipText?: boolean;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Whether this element is in the document tab order.
|
|
89
|
+
*
|
|
90
|
+
* true — natively focusable tag (a[href], button, input, select, textarea, summary)
|
|
91
|
+
* OR tabindex >= 0 — element is reachable by keyboard navigation.
|
|
92
|
+
* false — element is interactive (has role=button, onclick, or an INTERACTIVE tag) but
|
|
93
|
+
* carries tabindex="-1", explicitly removing it from the tab order.
|
|
94
|
+
* absent — static non-interactive element with no tabindex attribute.
|
|
95
|
+
*
|
|
96
|
+
* Cannot capture: dynamic tabindex mutations via JS, focus management via .focus() calls,
|
|
97
|
+
* CSS :focus-visible visibility (measured separately by the interaction probe in render/).
|
|
98
|
+
*/
|
|
99
|
+
focusable?: boolean;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Explicit ARIA role attribute value. Absent when no role="" attribute is set.
|
|
103
|
+
* Values of interest for rules: 'alert' (error message container), 'alertdialog',
|
|
104
|
+
* 'button' (non-button element acting as a button).
|
|
105
|
+
*
|
|
106
|
+
* Cannot capture: implicit/computed ARIA roles derived from HTML semantics (e.g.
|
|
107
|
+
* <input type="checkbox"> has an implicit role of 'checkbox'). Only explicit role=
|
|
108
|
+
* attribute values are recorded here.
|
|
109
|
+
*/
|
|
110
|
+
role?: string;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* True when aria-invalid is present and not "false". Absent otherwise.
|
|
114
|
+
* Signals a field that has failed validation per ARIA spec.
|
|
115
|
+
*
|
|
116
|
+
* Cannot capture: HTML5 constraint-validation state set via the Constraint Validation
|
|
117
|
+
* API (:invalid pseudo-class) without a corresponding aria-invalid attribute.
|
|
118
|
+
*/
|
|
119
|
+
ariaInvalid?: boolean;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Computed backdrop-filter value. Absent when 'none' or not declared.
|
|
123
|
+
* Captured to detect glassmorphism: backdrop-blur combined with a translucent surface,
|
|
124
|
+
* the frozen 2021 Dribbble aesthetic that became the new default.
|
|
125
|
+
*/
|
|
126
|
+
backdropFilter?: string;
|
|
127
|
+
|
|
128
|
+
/** Set on any node with a non-zero transition-duration or a named animation. */
|
|
129
|
+
motion?: {
|
|
130
|
+
/** ms, rounded. Transition AND animation durations both land here; zero-length transitions are dropped. */
|
|
131
|
+
durations: number[];
|
|
132
|
+
/** Named (i.e. not `none`) CSS animations running on this node. */
|
|
133
|
+
animationNames: string[];
|
|
134
|
+
/** Timing functions in play on this node, e.g. "ease-out", "cubic-bezier(...)". */
|
|
135
|
+
easings: string[];
|
|
136
|
+
/**
|
|
137
|
+
* CSS properties being transitioned (from transition-property). Only specific property
|
|
138
|
+
* names are included; 'all' and 'none' are excluded so MOTION-LAYOUT-THRASH can check
|
|
139
|
+
* for named layout properties (width/height/top/left/margin) deterministically without
|
|
140
|
+
* false-positives from the 'all' shorthand.
|
|
141
|
+
*/
|
|
142
|
+
properties?: string[];
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Everything a critic would otherwise have to count. Precomputed on purpose: a language
|
|
148
|
+
* model asked to measure a contrast ratio will be right most of the time, and a designer
|
|
149
|
+
* only has to catch it being wrong once to stop trusting the tool.
|
|
150
|
+
*/
|
|
151
|
+
export interface Computed {
|
|
152
|
+
depth: number;
|
|
153
|
+
/** null at the root. Text sits on its own surface; a container is judged against its parent. */
|
|
154
|
+
contrastWithParent: number | null;
|
|
155
|
+
/** Element-wise mode across siblings, excluding self — an outlier cannot vote for itself. */
|
|
156
|
+
siblingPaddingMode: [number, number, number, number] | null;
|
|
157
|
+
tokenCoverage: number;
|
|
158
|
+
hitArea: { w: number; h: number };
|
|
159
|
+
isInteractive: boolean;
|
|
160
|
+
/**
|
|
161
|
+
* How many siblings share this node's shape: same tag, same child count, same radius
|
|
162
|
+
* and shadow. Three of them in a row is the feature-card grid every generated landing
|
|
163
|
+
* page reaches for when nobody decided what matters most.
|
|
164
|
+
*/
|
|
165
|
+
identicalSiblings: number;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export type Node = RawNode & { computed: Computed };
|
|
169
|
+
|
|
170
|
+
export interface Stats {
|
|
171
|
+
spacingHistogram: Record<string, number>;
|
|
172
|
+
colorHistogram: Record<string, number>;
|
|
173
|
+
orphanStyles: Hex[];
|
|
174
|
+
componentReuse: Record<string, number>;
|
|
175
|
+
|
|
176
|
+
/** Distributions, because a monoculture is a property of the whole, not of any part. */
|
|
177
|
+
radiusHistogram: Record<string, number>;
|
|
178
|
+
shadowHistogram: Record<string, number>;
|
|
179
|
+
/** Fraction of text-bearing nodes that are centred. 0..1 */
|
|
180
|
+
centeredTextRatio: number;
|
|
181
|
+
/** Every gradient on the page, verbatim. */
|
|
182
|
+
gradients: string[];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface DeclaredFontFaceEvidence {
|
|
186
|
+
/** Browser-declared CSS FontFace family; this does not identify a painted glyph's face. */
|
|
187
|
+
family: string;
|
|
188
|
+
status: string;
|
|
189
|
+
style: string;
|
|
190
|
+
weight: string;
|
|
191
|
+
stretch: string;
|
|
192
|
+
/** The CSS Font Loading API does not expose the face's source URL. */
|
|
193
|
+
source: null;
|
|
194
|
+
/** Browsers do not expose a per-glyph physical-face identity through this API. */
|
|
195
|
+
glyphIdentity: null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface RawIrMeta extends Record<string, unknown> {
|
|
199
|
+
fontFaces?: DeclaredFontFaceEvidence[];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface RawIr {
|
|
203
|
+
meta?: RawIrMeta;
|
|
204
|
+
tokens?: Record<string, string>;
|
|
205
|
+
nodes: RawNode[];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface Ir extends RawIr {
|
|
209
|
+
nodes: Node[];
|
|
210
|
+
stats: Stats;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export type Severity = 'error' | 'warn';
|
|
214
|
+
|
|
215
|
+
/** Layer 1 is universal and free. Layer 2 is the team's own frame. Layer 3 never lives here. */
|
|
216
|
+
export type Layer = 1 | 2;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* `a11y` and `system` are defects: something is measurably wrong.
|
|
220
|
+
* `slop` is different. Nothing here is broken — the design has simply converged on the
|
|
221
|
+
* mean of everything the model has seen. Each rule is a heuristic and can be wrong about
|
|
222
|
+
* a deliberate choice, which is why every one of them is a warning and none is an error.
|
|
223
|
+
* `motion` covers craft defects in animation execution: missing reduced-motion support,
|
|
224
|
+
* layout thrash, uniform rhythm. Separate from `slop` because these are implementation
|
|
225
|
+
* correctness issues, not design-mean convergence failures.
|
|
226
|
+
* `ux` covers structural UX violations: primary action buried below the fold, no keyboard
|
|
227
|
+
* path into the interactive controls, frame not interrogated. These are correctness issues
|
|
228
|
+
* about task structure, not visual mean-convergence, which is why they are their own family.
|
|
229
|
+
*/
|
|
230
|
+
export type Category = 'a11y' | 'system' | 'slop' | 'motion' | 'ux';
|
|
231
|
+
|
|
232
|
+
export interface Rule {
|
|
233
|
+
id: string;
|
|
234
|
+
layer: Layer;
|
|
235
|
+
category: Category;
|
|
236
|
+
severity: Severity;
|
|
237
|
+
/** JS expression over `node`, `ir`. Truthy selects the node. */
|
|
238
|
+
when: string;
|
|
239
|
+
/** JS expression over `node`, `ir`. The quantity under test. */
|
|
240
|
+
value?: string;
|
|
241
|
+
/** JS expression over `node`, `ir`, `value`. Falsy raises a violation. */
|
|
242
|
+
assert: string;
|
|
243
|
+
message: string;
|
|
244
|
+
fix?: string;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export type RuleValue = string | number | boolean | null | number[] | { w: number; h: number };
|
|
248
|
+
|
|
249
|
+
export interface Violation {
|
|
250
|
+
id: string;
|
|
251
|
+
severity: Severity;
|
|
252
|
+
layer: Layer;
|
|
253
|
+
category: Category;
|
|
254
|
+
nodeId: string;
|
|
255
|
+
path: string;
|
|
256
|
+
value: RuleValue;
|
|
257
|
+
message: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** A record of what the problem is believed to be. The loop rewrites it; nobody signs it. */
|
|
261
|
+
export interface Frame {
|
|
262
|
+
why?: string;
|
|
263
|
+
generator?: string;
|
|
264
|
+
revision?: number;
|
|
265
|
+
writtenAt?: string;
|
|
266
|
+
reframedAt?: string;
|
|
267
|
+
body: string;
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* UX interrogation answers. Set by `omd frame set --task --frequent-action --costliest-error`.
|
|
271
|
+
* Backward compatible: frames written before these fields were introduced load without them
|
|
272
|
+
* (absence is treated as incomplete by FRAME-UX-INCOMPLETE, not as an error).
|
|
273
|
+
*
|
|
274
|
+
* Scope boundary: these fields validate that the frame ARTIFACT is complete — that the
|
|
275
|
+
* three UX questions were answered before building. The harness cannot verify whether the
|
|
276
|
+
* rendered build actually serves the named task; that requires a human or the eye agent.
|
|
277
|
+
*/
|
|
278
|
+
uxTask?: string;
|
|
279
|
+
uxFrequentAction?: string;
|
|
280
|
+
uxCostliestError?: string;
|
|
281
|
+
/**
|
|
282
|
+
* Surface classification. Set by `omd frame set --surface`. One of `marketing`,
|
|
283
|
+
* `product`, `editorial`, or `mixed` (free text is accepted; those four are the
|
|
284
|
+
* documented taxonomy — see theory/ux.md §Surface types). It selects the
|
|
285
|
+
* composition grammar downstream: a `product` work surface composes as a task
|
|
286
|
+
* loop over screen regions, never as a marketing message ladder.
|
|
287
|
+
*/
|
|
288
|
+
uxSurface?: string;
|
|
289
|
+
|
|
290
|
+
[key: string]: unknown;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* What a reference actually is: not a picture, a set of measurements.
|
|
295
|
+
*
|
|
296
|
+
* Jansson & Smith (1991) showed that designers reproduce the features of an example even
|
|
297
|
+
* after its flaws are pointed out — across four separate tasks. Hand a model a screenshot
|
|
298
|
+
* and it will produce a knockoff, which is anonymity wearing a different coat. Hand it the
|
|
299
|
+
* numbers and the reasoning behind them, and it has to think.
|
|
300
|
+
*/
|
|
301
|
+
export interface Invariants {
|
|
302
|
+
/** Spacing values carrying the design, smallest first. Zero is excluded: on a real page it is ~80% of all values. */
|
|
303
|
+
spacingLadder: number[];
|
|
304
|
+
/** Corner radii carrying the design. A single rung is a monoculture. */
|
|
305
|
+
radiusLadder: number[];
|
|
306
|
+
/**
|
|
307
|
+
* Distinct box-shadows that are actually elevation. Hairline shadows (`0 0 0 1px`) are
|
|
308
|
+
* borders drawn as shadows and are not counted — Linear has five distinct shadows and
|
|
309
|
+
* four of them are hairlines.
|
|
310
|
+
*/
|
|
311
|
+
elevationLevels: number;
|
|
312
|
+
/** Fraction of text-bearing nodes that are centred. 0..1 */
|
|
313
|
+
centeredRatio: number;
|
|
314
|
+
/** Of the styles that could carry a token, the fraction that do. 0..1 */
|
|
315
|
+
tokenCoverage: number;
|
|
316
|
+
/** Mean sum of the four paddings on nodes that declare any. Higher is airier. */
|
|
317
|
+
paddingWeight: number;
|
|
318
|
+
|
|
319
|
+
// Measurements, not vibes: a generic serif-heading dark blog is invisible to spacing
|
|
320
|
+
// and radius ladders alone. These six catch the two things that used to slip through —
|
|
321
|
+
// typography and motion.
|
|
322
|
+
/** Distinct text font sizes carrying the design, ladder-cut, ascending. */
|
|
323
|
+
typeScale: number[];
|
|
324
|
+
/** Distinct first-families found on text, sorted. */
|
|
325
|
+
fontFamilies: string[];
|
|
326
|
+
/** Distinct text font weights carrying the design, ladder-cut, ascending. */
|
|
327
|
+
weightLadder: number[];
|
|
328
|
+
/** Distinct transition/animation durations (ms) carrying the design, ladder-cut, ascending. */
|
|
329
|
+
motionDurations: number[];
|
|
330
|
+
/** Distinct timing functions in use, sorted. */
|
|
331
|
+
easingVocab: string[];
|
|
332
|
+
/** Fraction of all nodes carrying any motion (transition or animation). 0..1, 4dp. */
|
|
333
|
+
animatedShare: number;
|
|
334
|
+
|
|
335
|
+
// Interaction states are unmeasured elsewhere: a dead-on-hover button and a missing
|
|
336
|
+
// focus ring both look identical in a static screenshot. These read the page's actual
|
|
337
|
+
// behaviour, probed live (core/render/index.ts), not its resting state.
|
|
338
|
+
/** hoverResponsive / probed, 2dp. 0 when probed is 0 or the probe failed (unmeasured). */
|
|
339
|
+
hoverCoverage: number;
|
|
340
|
+
/** focusVisible / tabStops, 2dp. 0 when tabStops is 0 or the probe failed (unmeasured). */
|
|
341
|
+
focusCoverage: number;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* CSS properties actually animated, collected from the live motion probe via
|
|
345
|
+
* document.getAnimations({subtree:true}). Sorted, deduplicated.
|
|
346
|
+
*
|
|
347
|
+
* NOTE: rAF-driven libraries (GSAP, Anime.js) animate via requestAnimationFrame and
|
|
348
|
+
* do not register with the browser's Animation API. A GSAP-only page reads as [] here —
|
|
349
|
+
* that is a correct measurement of what getAnimations() can see, not a probe failure.
|
|
350
|
+
* Only CSS animations, CSS transitions (WAAPI path), and explicit WAAPI calls (including
|
|
351
|
+
* Framer Motion's WAAPI path) appear here.
|
|
352
|
+
*/
|
|
353
|
+
animatedProperties: string[];
|
|
354
|
+
/**
|
|
355
|
+
* True when at least one stylesheet contains a @media (prefers-reduced-motion) block.
|
|
356
|
+
* False when the probe was not run or found no such block.
|
|
357
|
+
*/
|
|
358
|
+
hasReducedMotion: boolean;
|
|
359
|
+
/**
|
|
360
|
+
* Scroll choreography sampled at 25%, 50%, 75%, 100% of viewport height.
|
|
361
|
+
* step: 1-indexed scroll step; fired: running animations at that position;
|
|
362
|
+
* entered: in-viewport elements with a non-zero transition or animation at that position.
|
|
363
|
+
* Empty when the probe was not run.
|
|
364
|
+
*/
|
|
365
|
+
scrollChoreography: Array<{ step: number; fired: number; entered: number }>;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Energy for a single adjacent-frame pair: how much of the image changed.
|
|
370
|
+
* Part of EnergyCurve; defined here so Reference (which embeds EnergyCurve) stays
|
|
371
|
+
* in the same file as all other core record types.
|
|
372
|
+
*/
|
|
373
|
+
export interface FrameEnergy {
|
|
374
|
+
/** 0-indexed index of the earlier frame in the pair. */
|
|
375
|
+
pairIndex: number;
|
|
376
|
+
/**
|
|
377
|
+
* Fraction of pixels whose max RGB channel difference exceeds the diff threshold.
|
|
378
|
+
* 0..1, 4dp. 0 = identical frames; 1 = every pixel changed beyond threshold.
|
|
379
|
+
*/
|
|
380
|
+
changedFraction: number;
|
|
381
|
+
/**
|
|
382
|
+
* Fraction of changed pixels per vertical third of the frame: [top, mid, bottom].
|
|
383
|
+
* Answers "where did the motion happen" without requiring layout data.
|
|
384
|
+
*/
|
|
385
|
+
regionFractions: [number, number, number];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Pixel-diff motion energy measured from a filmstrip's frame sequence.
|
|
390
|
+
*
|
|
391
|
+
* Unlike the getAnimations() probe (MotionMeasurement), this is computed from actual
|
|
392
|
+
* rendered pixels and therefore captures ALL motion including GSAP/rAF — closing the
|
|
393
|
+
* getAnimations() blind spot documented on MotionMeasurement and Invariants.animatedProperties.
|
|
394
|
+
*/
|
|
395
|
+
export interface EnergyCurve {
|
|
396
|
+
/** Number of source frames (pairs.length === frames - 1). */
|
|
397
|
+
frames: number;
|
|
398
|
+
/** Per-interval pixel-difference scores, one entry per adjacent frame pair. */
|
|
399
|
+
pairs: FrameEnergy[];
|
|
400
|
+
/** Maximum changedFraction across all pairs. 0 when pairs is empty. */
|
|
401
|
+
peakEnergy: number;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Live motion probe data attached to ir.meta.motion by probeMotion() in
|
|
406
|
+
* core/render/index.ts. Absent on IRs captured before this probe was added;
|
|
407
|
+
* all consuming code reads defensively (presence check before access).
|
|
408
|
+
*
|
|
409
|
+
* PROBE LIMIT: rAF-driven libraries (GSAP, Anime.js) animate via requestAnimationFrame
|
|
410
|
+
* and do not register with document.getAnimations(). Their effects are invisible to this
|
|
411
|
+
* probe — a GSAP-only page reports animatedProperties: []. Only CSS animations, CSS
|
|
412
|
+
* transitions exposed via WAAPI, and explicit WAAPI calls (Framer Motion's WAAPI path)
|
|
413
|
+
* are captured. This limit is surfaced in Invariants.animatedProperties.
|
|
414
|
+
*
|
|
415
|
+
* To close this blind spot, use the pixel-diff energy curve (EnergyCurve / computeEnergy
|
|
416
|
+
* in core/motion/energy.ts): it measures frame-to-frame pixel differences and sees all
|
|
417
|
+
* motion including GSAP/rAF.
|
|
418
|
+
*/
|
|
419
|
+
export interface MotionMeasurement {
|
|
420
|
+
/** Animation states sampled at t=0ms, t=500ms, t=1500ms after load. */
|
|
421
|
+
snapshots: Array<{
|
|
422
|
+
t: number;
|
|
423
|
+
animations: Array<{ duration: number; easing: string; properties: string[]; playState: string }>;
|
|
424
|
+
}>;
|
|
425
|
+
/** Union of all animated CSS properties across all snapshots, sorted. */
|
|
426
|
+
animatedProperties: string[];
|
|
427
|
+
/** True when at least one stylesheet has a @media (prefers-reduced-motion) block. */
|
|
428
|
+
hasReducedMotion: boolean;
|
|
429
|
+
/**
|
|
430
|
+
* Scroll-triggered choreography: per 25%vh step, animation count and animated-element
|
|
431
|
+
* count visible at that scroll position.
|
|
432
|
+
*/
|
|
433
|
+
scrollChoreography: Array<{ step: number; fired: number; entered: number }>;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* How closely a reference was looked at.
|
|
438
|
+
*
|
|
439
|
+
* `page` the whole thing — its overall feel, its rhythm
|
|
440
|
+
* `component` one part of it, scoped by a CSS selector: a search bar, a single button
|
|
441
|
+
* `image` a picture. A screenshot, a Pinterest pin, a photo of a book cover.
|
|
442
|
+
*/
|
|
443
|
+
export type RefKind = 'page' | 'component' | 'image';
|
|
444
|
+
|
|
445
|
+
// ── blueprint ────────────────────────────────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Semantic color role assigned to a node's fill or text color.
|
|
449
|
+
* Literal hex values never appear in a blueprint; only roles do.
|
|
450
|
+
*/
|
|
451
|
+
export type ColorRole = 'bg' | 'surface' | 'fg' | 'muted' | 'accent';
|
|
452
|
+
|
|
453
|
+
/** Structural role of a blueprint node, derived from the IR node type and computed fields. */
|
|
454
|
+
export type NodeRole = 'container' | 'heading' | 'text' | 'interactive' | 'image';
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Length class of a text node's content. Copy is never transplanted, but knowing whether
|
|
458
|
+
* a slot holds a single word, a headline, or a paragraph guides reconstruction.
|
|
459
|
+
*/
|
|
460
|
+
export type TextLengthClass = 'label' | 'phrase' | 'paragraph';
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* One node in a blueprint — a full-resolution structural measurement with the skin
|
|
464
|
+
* abstracted to color roles. Literal hex values, token names, and text content are
|
|
465
|
+
* absent; only what is measurable and transferable remains.
|
|
466
|
+
*
|
|
467
|
+
* Fields are present only when the IR extractor actually measured them — no invented
|
|
468
|
+
* values for fields the extractor cannot fill.
|
|
469
|
+
*/
|
|
470
|
+
export interface BlueprintNode {
|
|
471
|
+
id: string;
|
|
472
|
+
role: NodeRole;
|
|
473
|
+
/** IDs of direct children, in document order. */
|
|
474
|
+
children: string[];
|
|
475
|
+
/** Bounding box dimensions. Position is intentionally omitted (component-relative). */
|
|
476
|
+
box: { w: number; h: number };
|
|
477
|
+
// layout — present when the node declared flex/grid or any padding
|
|
478
|
+
padding?: [number, number, number, number];
|
|
479
|
+
/** Non-zero gap between children. Absent when 0 or unmeasured. */
|
|
480
|
+
gap?: number;
|
|
481
|
+
/** Flex/grid main axis direction. */
|
|
482
|
+
direction?: 'VERTICAL' | 'HORIZONTAL';
|
|
483
|
+
// typography — present only on text and heading nodes
|
|
484
|
+
fontSize?: number;
|
|
485
|
+
fontWeight?: number;
|
|
486
|
+
lineHeight?: number;
|
|
487
|
+
// surface geometry
|
|
488
|
+
/** Border-radius in px. Absent when 0. */
|
|
489
|
+
radius?: number;
|
|
490
|
+
/** True when the node has a non-none box-shadow. */
|
|
491
|
+
hasShadow?: boolean;
|
|
492
|
+
// color roles — no literal hex values
|
|
493
|
+
/** Semantic role of the node's background fill. Absent for transparent/inherited fills. */
|
|
494
|
+
fillRole?: ColorRole;
|
|
495
|
+
/** Semantic role of the node's text color. Absent for non-text nodes. */
|
|
496
|
+
textRole?: ColorRole;
|
|
497
|
+
// motion
|
|
498
|
+
motionDurations?: number[];
|
|
499
|
+
motionEasings?: string[];
|
|
500
|
+
// text length signal — no actual copy
|
|
501
|
+
/** Length class of the node's own text content. Absent for non-text nodes. */
|
|
502
|
+
textLength?: TextLengthClass;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Full-resolution structural measurement of one component, with the skin abstracted to
|
|
507
|
+
* color roles. The blueprint captures what the component IS structurally, not what it
|
|
508
|
+
* looks like. Transplanting a blueprint means rebuilding its structure and metrics nearly
|
|
509
|
+
* verbatim, remapping color roles to the project's own tokens, and writing fresh copy.
|
|
510
|
+
*/
|
|
511
|
+
export interface Blueprint {
|
|
512
|
+
/** The CSS selector that scoped this capture. */
|
|
513
|
+
selector: string;
|
|
514
|
+
capturedAt: string;
|
|
515
|
+
nodes: BlueprintNode[];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export interface Reference {
|
|
519
|
+
source: string;
|
|
520
|
+
component: string;
|
|
521
|
+
kind: RefKind;
|
|
522
|
+
capturedAt: string;
|
|
523
|
+
/** The CSS selector the measurements were taken from. Absent for `page` and `image`. */
|
|
524
|
+
selector?: string;
|
|
525
|
+
/**
|
|
526
|
+
* Null for an image, and the type says so on purpose. Pixels cannot be measured this way:
|
|
527
|
+
* there is no spacing ladder to read out of a JPEG. An image reference carries reasoning
|
|
528
|
+
* and nothing else, which also means `ref distance` cannot check it for cloning.
|
|
529
|
+
*/
|
|
530
|
+
invariants: Invariants | null;
|
|
531
|
+
/** Why it was built that way. Written by a model that looked, then closed the tab. */
|
|
532
|
+
principles: string[];
|
|
533
|
+
/**
|
|
534
|
+
* Number of slop-category rule violations found at capture time. Absent on references
|
|
535
|
+
* captured before this field was introduced — treat absence as 0 at the usage site.
|
|
536
|
+
* Two or more findings is a contamination signal: board only as an anti-reference.
|
|
537
|
+
*/
|
|
538
|
+
slopCount?: number;
|
|
539
|
+
/**
|
|
540
|
+
* Who provided this URL. `'user'` when the URL came directly from the brief and was added
|
|
541
|
+
* with `--from-user`; `'scout'` when the scout found it autonomously. Absent on records
|
|
542
|
+
* captured before this field was introduced — treat absence as `'scout'` at usage sites.
|
|
543
|
+
*/
|
|
544
|
+
origin?: 'user' | 'scout';
|
|
545
|
+
/**
|
|
546
|
+
* Pixel-diff motion energy curve measured during reference capture. Absent when the
|
|
547
|
+
* reference was captured before energy measurement was introduced, or when kind is
|
|
548
|
+
* `'image'` (no rendered page to film). Treat absence as null at usage sites.
|
|
549
|
+
*
|
|
550
|
+
* Unlike `ir.meta.motion` (which only sees getAnimations()), this curve captures ALL
|
|
551
|
+
* pixel-level motion including GSAP/rAF — closing the getAnimations() blind spot.
|
|
552
|
+
*/
|
|
553
|
+
energyCurve?: EnergyCurve | null;
|
|
554
|
+
/**
|
|
555
|
+
* Full-resolution structural measurement of one component, skin abstracted to color
|
|
556
|
+
* roles. Present only when `--blueprint` was passed at capture time and a `--selector`
|
|
557
|
+
* was given. Absent on references captured before this field was introduced — treat
|
|
558
|
+
* absence as undefined at usage sites.
|
|
559
|
+
*
|
|
560
|
+
* Transplanting a blueprint means rebuilding its structure and metrics nearly verbatim,
|
|
561
|
+
* remapping colorRoles to the project's own tokens, re-fitting type sizes to the
|
|
562
|
+
* project's scale (keep hierarchy ratios, not absolute px), and writing fresh copy in
|
|
563
|
+
* the project's voice. The page-distance guard still fires on a clone.
|
|
564
|
+
*/
|
|
565
|
+
blueprint?: Blueprint;
|
|
566
|
+
/**
|
|
567
|
+
* Path (relative to cwd) of the scoped component screenshot captured with
|
|
568
|
+
* `omd ref add --selector … --shot`. Pairs the component's pixels with its
|
|
569
|
+
* blueprint/invariants on one record so image-first art direction can seed
|
|
570
|
+
* from both the picture and the structural data — see `core/theory/imagegen.md`.
|
|
571
|
+
* The kinship gate (`ref distance <= 0.6`) still gates the shipped build.
|
|
572
|
+
*/
|
|
573
|
+
imagePath?: string;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** How close a page sits to a reference. 1 is identical; the warning threshold is 0.6. */
|
|
577
|
+
export interface RefDistance {
|
|
578
|
+
reference: string;
|
|
579
|
+
similarity: number;
|
|
580
|
+
/** Which invariants drove the score, most-similar first. */
|
|
581
|
+
drivers: string[];
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* One `omd check` run, appended to `.omd/history.jsonl`.
|
|
586
|
+
*
|
|
587
|
+
* Coach cannot exist without this. "You were told about contrast forty-one times" is a
|
|
588
|
+
* claim about the past, and until now nothing remembered the past: check printed and exited.
|
|
589
|
+
*/
|
|
590
|
+
export interface Run {
|
|
591
|
+
ts: string;
|
|
592
|
+
page: string;
|
|
593
|
+
/** ruleId -> how many nodes violated it in this run. */
|
|
594
|
+
counts: Record<string, number>;
|
|
595
|
+
total: number;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export interface Recurring {
|
|
599
|
+
rule: string;
|
|
600
|
+
category: Category;
|
|
601
|
+
/** Violations across every run. */
|
|
602
|
+
total: number;
|
|
603
|
+
/** How many distinct runs it showed up in. */
|
|
604
|
+
runs: number;
|
|
605
|
+
trend: 'improving' | 'worsening' | 'flat' | 'insufficient';
|
|
606
|
+
/** Percent change between the first and second half of the runs. Null when insufficient. */
|
|
607
|
+
changePct: number | null;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* What the user keeps getting wrong — skill, not taste.
|
|
612
|
+
*
|
|
613
|
+
* Taste ("you prefer dense layouts") has no right answer: professional designers agree at
|
|
614
|
+
* α = 0.248. Skill ("you keep missing contrast") has one. The two must never be mixed, so
|
|
615
|
+
* Coach never reads `.omd/taste/`.
|
|
616
|
+
*/
|
|
617
|
+
export interface CoachReport {
|
|
618
|
+
runs: number;
|
|
619
|
+
span: { from: string; to: string } | null;
|
|
620
|
+
recurring: Recurring[];
|
|
621
|
+
/** Slop rules the user overruled in decisions.md, and how often. */
|
|
622
|
+
overrules: { rule: string; count: number }[];
|
|
623
|
+
/** False when there is too little history to claim a trend. Say so rather than invent one. */
|
|
624
|
+
confident: boolean;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export interface Choice {
|
|
628
|
+
ts: string;
|
|
629
|
+
/** Legacy records omit actor/kind and normalize to unknown, never user. */
|
|
630
|
+
actor?: 'user' | 'agent' | 'unknown';
|
|
631
|
+
kind?: 'selection' | 'praise' | 'rejection' | 'overrule';
|
|
632
|
+
subject?: string;
|
|
633
|
+
evidence?: string;
|
|
634
|
+
among?: string[];
|
|
635
|
+
chose?: string;
|
|
636
|
+
why?: string | null;
|
|
637
|
+
generator?: string | null;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ── adapters ──
|
|
641
|
+
|
|
642
|
+
export type Host = 'codex' | 'claude';
|
|
643
|
+
|
|
644
|
+
export interface AbstractHook {
|
|
645
|
+
id: string;
|
|
646
|
+
event: string;
|
|
647
|
+
matcher: string;
|
|
648
|
+
command: string;
|
|
649
|
+
timeout: number;
|
|
650
|
+
statusMessage: string;
|
|
651
|
+
codexFileName: string;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export interface AbstractAgent {
|
|
655
|
+
name: string;
|
|
656
|
+
description: string;
|
|
657
|
+
reasoning: string;
|
|
658
|
+
/** No longer used by the adapters — agents inherit the session model. */
|
|
659
|
+
model?: string;
|
|
660
|
+
allow?: string[];
|
|
661
|
+
deny?: string[];
|
|
662
|
+
instructions: string;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export interface HookCommand {
|
|
666
|
+
type: 'command';
|
|
667
|
+
command: string;
|
|
668
|
+
timeout: number;
|
|
669
|
+
statusMessage?: string;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export interface HookEntry {
|
|
673
|
+
matcher: string;
|
|
674
|
+
hooks: HookCommand[];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** What a host actually loads: events -> matchers -> commands. */
|
|
678
|
+
export interface HookFile {
|
|
679
|
+
hooks: Record<string, HookEntry[]>;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export interface Emitted {
|
|
683
|
+
files: Record<string, unknown>;
|
|
684
|
+
}
|