@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
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# Finish pass — the last 5%
|
|
2
|
+
|
|
3
|
+
Done is when the build passes checks and the eye approves it. Cared for is when the cursor selects text and the highlight colour is yours, when focus rings look designed rather than browser-default, when the favicon tab reads as the brand in a row of twenty other tabs. Nobody schedules these items because none of them ships features. All of them ship the difference between a page a developer handed off and a page a designer finished.
|
|
4
|
+
|
|
5
|
+
This checklist runs after `omd check` returns clean and before handback. Walk it top to bottom. Each item is either implemented or skipped with a written reason. A page without a favicon and `::selection` colour reads as unfinished regardless of everything else.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ::selection
|
|
10
|
+
|
|
11
|
+
The browser's default selection highlight is a system blue. On a page that committed to a considered colour system, the default highlight announces that somebody stopped caring at the boundary of their CSS.
|
|
12
|
+
|
|
13
|
+
Derive the selection colour from the accent token already declared in `:root`. The background should be a mid-opacity or tinted variant of the accent — not a hardcoded blue, not a generic highlight. The text colour must maintain 4.5:1 contrast against the selection background.
|
|
14
|
+
|
|
15
|
+
```css
|
|
16
|
+
/* Derive from the accent token already declared in :root.
|
|
17
|
+
Use a transparent or tinted variant so the selected text remains readable.
|
|
18
|
+
WCAG minimum: 4.5:1 between --color-selection-text and --color-selection-bg. */
|
|
19
|
+
::selection {
|
|
20
|
+
background-color: var(--color-selection-bg, oklch(from var(--color-accent) l c h / 0.25));
|
|
21
|
+
color: var(--color-selection-text, inherit);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Declare the tokens in :root alongside the rest of the colour system. */
|
|
25
|
+
:root {
|
|
26
|
+
--color-selection-bg: oklch(from var(--color-accent) l c h / 0.2);
|
|
27
|
+
--color-selection-text: var(--color-text-primary);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For browsers without `oklch()` color relative syntax support, declare an explicit fallback:
|
|
32
|
+
|
|
33
|
+
```css
|
|
34
|
+
:root {
|
|
35
|
+
--color-selection-bg: color-mix(in oklch, var(--color-accent) 20%, transparent);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Condition for skipping: the brief specifies a monochrome print stylesheet as the only output. Otherwise, this ships.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Focus ring
|
|
44
|
+
|
|
45
|
+
Suppressing outlines — `outline: none` without a replacement — is an accessibility defect. The FOCUS rule in `omd check` (`A11Y-FOCUS-SUPPRESSED`) fires on any selector that removes the outline without providing a visible `:focus-visible` alternative. Do not suppress; design it.
|
|
46
|
+
|
|
47
|
+
`:focus-visible` is the correct pseudo-class. `:focus` fires on mouse clicks too, which produces a ring on button-press that most designs do not intend. `:focus-visible` fires only when the keyboard (or a pointing device in sequential-focus mode) is the active input — the ring appears when it is needed and disappears when it is not.
|
|
48
|
+
|
|
49
|
+
```css
|
|
50
|
+
/* Remove the default only where you are replacing it. Never remove without replacing. */
|
|
51
|
+
:focus-visible {
|
|
52
|
+
outline: none;
|
|
53
|
+
box-shadow:
|
|
54
|
+
0 0 0 2px var(--color-bg-primary, #fff),
|
|
55
|
+
0 0 0 4px var(--color-focus-ring, var(--color-accent));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Declare the focus ring token in :root. */
|
|
59
|
+
:root {
|
|
60
|
+
--color-focus-ring: var(--color-accent);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The two-layer shadow creates a ring with a gap between the element edge and the visible ring — the gap is the inner shadow matching the page background. This technique works on any background colour as long as the inner shadow is updated. For dark-background sections, override locally:
|
|
65
|
+
|
|
66
|
+
```css
|
|
67
|
+
.section--inverted :focus-visible {
|
|
68
|
+
box-shadow:
|
|
69
|
+
0 0 0 2px var(--color-bg-inverted),
|
|
70
|
+
0 0 0 4px var(--color-focus-ring-on-dark, #fff);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Minimum ring visibility: 3:1 contrast between the ring colour and the adjacent background (WCAG 2.2 §2.4.11). Run `omd check --category a11y` after setting these values — the checker measures contrast, not intent.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Scrollbar
|
|
79
|
+
|
|
80
|
+
The browser default scrollbar is a grey rectangle. It is legible and functional; it is not designed. On a page that committed to a colour system, the scrollbar is the one surface the browser owns that does not match.
|
|
81
|
+
|
|
82
|
+
Use the standards-track properties first. `scrollbar-color` and `scrollbar-width` are in the CSS Scrollbars Specification (Candidate Recommendation) and are supported in Firefox and Chrome 121+.
|
|
83
|
+
|
|
84
|
+
```css
|
|
85
|
+
/* Standards-track. Thumb colour, then track colour. */
|
|
86
|
+
:root {
|
|
87
|
+
scrollbar-color: var(--color-scrollbar-thumb) var(--color-scrollbar-track);
|
|
88
|
+
scrollbar-width: thin;
|
|
89
|
+
|
|
90
|
+
--color-scrollbar-thumb: color-mix(in oklch, var(--color-text-primary) 30%, transparent);
|
|
91
|
+
--color-scrollbar-track: transparent;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Follow with the `-webkit-` prefixed properties as the noted fallback for Safari and older browsers. These properties do not compose with `inherit` correctly — set explicit values:
|
|
96
|
+
|
|
97
|
+
```css
|
|
98
|
+
/* -webkit- fallback (Safari, older Chromium). Note: these are not in any standard.
|
|
99
|
+
They are widely supported but their future is not guaranteed; the standards-track
|
|
100
|
+
properties above take precedence wherever both are supported. */
|
|
101
|
+
::-webkit-scrollbar {
|
|
102
|
+
width: 6px;
|
|
103
|
+
height: 6px;
|
|
104
|
+
}
|
|
105
|
+
::-webkit-scrollbar-track {
|
|
106
|
+
background: var(--color-scrollbar-track, transparent);
|
|
107
|
+
}
|
|
108
|
+
::-webkit-scrollbar-thumb {
|
|
109
|
+
background: var(--color-scrollbar-thumb);
|
|
110
|
+
border-radius: 999px;
|
|
111
|
+
}
|
|
112
|
+
::-webkit-scrollbar-thumb:hover {
|
|
113
|
+
background: color-mix(in oklch, var(--color-scrollbar-thumb) 140%, transparent);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Condition for skipping: the page is a web application that deliberately defers to the OS scrollbar for platform-native feel. Record the reason.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Optical alignment
|
|
122
|
+
|
|
123
|
+
Pixel-perfect alignment is not optical alignment. A 16px icon inside a 16px × 16px bounding box sits 1px high relative to adjacent text — its visual weight lands above the centre. A button whose padding is `12px 16px` produces more apparent top weight than bottom because the ascender height of the glyph does not fill the cap-height. These are the corrections that make the difference between type that looks placed and type that looks set.
|
|
124
|
+
|
|
125
|
+
**Icon baseline nudge.** Icons whose bounding box touches the cap line rather than the baseline need a fractional downward shift. The correction is 1–2px for most icon sets at 16–20px; the correct value is measured, not assumed.
|
|
126
|
+
|
|
127
|
+
```css
|
|
128
|
+
/* Inline icons adjacent to text: nudge down by half the difference between
|
|
129
|
+
the icon's optical centre and the text baseline. Start with 1px; adjust by eye. */
|
|
130
|
+
.icon-inline {
|
|
131
|
+
vertical-align: -0.125em; /* ≈ 2px at 16px font-size — tune to match the type */
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Button padding asymmetry.** Most typefaces have a slightly higher optical midpoint than the mathematical midpoint. Equal vertical padding reads as too much bottom space on most Latin type. Subtract 1–2px from the bottom padding to correct.
|
|
136
|
+
|
|
137
|
+
```css
|
|
138
|
+
/* Start with equal padding, then reduce bottom by 1–2px until the label reads centred.
|
|
139
|
+
The correction varies by typeface — Inter and Geist need less, older serif faces more. */
|
|
140
|
+
.button {
|
|
141
|
+
padding: 11px 20px 9px; /* or: padding-top: 11px; padding-bottom: 9px */
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Number alignment in tables.** Tabular figures (`font-variant-numeric: tabular-nums`) align decimal points. Without this, proportional numerals produce ragged columns.
|
|
146
|
+
|
|
147
|
+
```css
|
|
148
|
+
.data-cell {
|
|
149
|
+
font-variant-numeric: tabular-nums;
|
|
150
|
+
text-align: right;
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
These corrections are small and cumulative. None of them will survive a linter check; they pass `omd check` before and after. They register when a human looks at the finished page and notices it is not quite right — or when they look and notice it is exactly right, without being able to say why.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Favicon
|
|
159
|
+
|
|
160
|
+
A favicon delivered as a separate asset file creates a dependency and an HTTP request. The inline SVG data-URI pattern delivers the icon with the HTML, requires no build step, and works across every browser that renders the page.
|
|
161
|
+
|
|
162
|
+
**SVG data-URI (recommended):** Build the icon as an SVG element, then encode it as a data URI. For a typographic favicon, this means a single `<text>` element with a carefully chosen character and fill colour.
|
|
163
|
+
|
|
164
|
+
```html
|
|
165
|
+
<!-- Inline SVG favicon: no asset file, no HTTP request, works in every browser.
|
|
166
|
+
The SVG viewBox is 32×32; the font-size and y offset must be tuned to keep
|
|
167
|
+
the glyph centred — exact values depend on the typeface. -->
|
|
168
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%23111'/><text x='16' y='22' font-size='18' text-anchor='middle' fill='%23fff' font-family='system-ui,sans-serif' font-weight='700'>M</text></svg>">
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Replace `%23111` with the URL-encoded hex of the brand background colour and `%23fff` with the glyph colour. The `rx='6'` rounds the favicon square — most platform launchers clip to this shape anyway; making it explicit ensures the corner doesn't read as a sharp artefact.
|
|
172
|
+
|
|
173
|
+
**Emoji favicon (minimal variant):** When the brand has no monogram but an associated emoji is genuinely appropriate, the single-character SVG is the minimal implementation:
|
|
174
|
+
|
|
175
|
+
```html
|
|
176
|
+
<!-- Emoji favicon: one character, no colour management needed.
|
|
177
|
+
The emoji renders in its platform colour — no fill attribute required.
|
|
178
|
+
Use only when the emoji is a deliberate brand choice, not a fallback decoration. -->
|
|
179
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌱</text></svg>">
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Both variants require no external asset. The inline SVG approach gives more control over the appearance at 16×16 and 32×32 display sizes; the emoji variant is correct only when the emoji reads well at small sizes (simple pictograms work; detailed emoji do not).
|
|
183
|
+
|
|
184
|
+
Provide a `<title>` with the brand name — the favicon without a page title still reads as generic in the tab.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## OG and meta
|
|
189
|
+
|
|
190
|
+
Open Graph and meta tags are copy. They are read by every person who sees the link shared in a chat, a feed, or a notification. They are the page's first impression in contexts where the design cannot speak — and they are where the same model that wrote "Unlock the power of" will try again, unchecked, if nobody explicitly closes that door.
|
|
191
|
+
|
|
192
|
+
The copy rules that apply to the page apply here without exception.
|
|
193
|
+
|
|
194
|
+
**Required tags:**
|
|
195
|
+
|
|
196
|
+
```html
|
|
197
|
+
<head>
|
|
198
|
+
<meta charset="utf-8">
|
|
199
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
200
|
+
|
|
201
|
+
<!-- Page title: brand name + one-line position, not a tagline.
|
|
202
|
+
"Moa — 목표를 위한 저축" not "Moa | Save smarter. Live better." -->
|
|
203
|
+
<title>Brand — specific position</title>
|
|
204
|
+
|
|
205
|
+
<!-- Description: one or two concrete sentences. What does it do, for whom.
|
|
206
|
+
No "powerful", "seamless", "next-level". No pink-elephant negations.
|
|
207
|
+
omd check --category slop applies to this string exactly as it applies to body copy. -->
|
|
208
|
+
<meta name="description" content="Specific, concrete description. One thing the product does.">
|
|
209
|
+
|
|
210
|
+
<!-- OG title: same rule as <title>. Match or slightly adapt — do not write a new tagline. -->
|
|
211
|
+
<meta property="og:title" content="Brand — specific position">
|
|
212
|
+
<meta property="og:description" content="Same concrete description.">
|
|
213
|
+
|
|
214
|
+
<!-- og:image policy: if a designed card image exists, use it.
|
|
215
|
+
If one does not exist, omit og:image rather than setting a broken path —
|
|
216
|
+
platforms render the URL card without an image more gracefully than they render
|
|
217
|
+
a 404. Do not set og:image to a screenshot of the page unless it was designed
|
|
218
|
+
as a social card at 1200×630. -->
|
|
219
|
+
<meta property="og:image" content="https://example.com/og-card.png"> <!-- only if the file exists -->
|
|
220
|
+
<meta property="og:type" content="website">
|
|
221
|
+
<meta property="og:url" content="https://example.com/">
|
|
222
|
+
|
|
223
|
+
<!-- Twitter/X card — summary_large_image if og:image is designed, summary if not -->
|
|
224
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
225
|
+
</head>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**OG copy rules:**
|
|
229
|
+
1. The OG title must pass SLOP-COPY: it must say something specific about this product, not any product. "모아 — 목표까지 함께하는 저축" is specific; "모아 | 더 스마트하게 저축하세요" is a placeholder with the brand name prepended.
|
|
230
|
+
2. The description must not be the tagline repeated. If the title already communicates the position, the description adds a second concrete fact — who it is for, what it costs, what the first action is.
|
|
231
|
+
3. SLOP-PINK-ELEPHANT and SLOP-COPY rules apply. Run `omd check <page> --category slop` — the checker reads meta tags. A description that passes the page body but fails the OG string is the same failure.
|
|
232
|
+
4. If the brief was in Korean, the OG strings should be in Korean.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Interaction states
|
|
237
|
+
|
|
238
|
+
A page that renders correctly when everything goes right is not a finished page. The states that matter most to users — loading, failure, nothing to show — are precisely the ones that never appear in a screenshot review. Every interactive surface must account for each applicable state before handoff.
|
|
239
|
+
|
|
240
|
+
Walk each state in the list below. For each one: either the state is implemented, or it is explicitly not applicable with a written reason.
|
|
241
|
+
|
|
242
|
+
### Loading
|
|
243
|
+
|
|
244
|
+
Any action that takes time — a form submission, a data fetch, a navigation — needs a loading signal. The minimum is a disabled submit button with a spinner or text change ("제출 중…" / "Submitting…"). The ideal is a skeleton or shimmer that matches the layout of the loaded state, so the page does not jump when data arrives.
|
|
245
|
+
|
|
246
|
+
Implementation checklist:
|
|
247
|
+
- Form submit buttons disable and show a loading indicator during in-flight requests.
|
|
248
|
+
- Data lists that load asynchronously show a skeleton with the correct number of rows.
|
|
249
|
+
- Page transitions that require data fetching show a loading bar or overlay.
|
|
250
|
+
- The loading state must be accessible: `aria-busy="true"` on the container or `role="status"` on the indicator.
|
|
251
|
+
|
|
252
|
+
```html
|
|
253
|
+
<!-- Minimum: disable + status text -->
|
|
254
|
+
<button type="submit" aria-disabled="true" aria-describedby="submit-status">Submit</button>
|
|
255
|
+
<span id="submit-status" role="status" aria-live="polite">Submitting…</span>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Empty
|
|
259
|
+
|
|
260
|
+
Every list, feed, result set, or data container that can legally contain zero items must have a designed empty state. An unstyled empty container is a layout bug that the model almost always ships and the eye almost never catches in a single-page screenshot.
|
|
261
|
+
|
|
262
|
+
The empty state must communicate three things: what is supposed to be here, why there is nothing, and what the user can do about it.
|
|
263
|
+
|
|
264
|
+
```html
|
|
265
|
+
<!-- Minimum empty state structure -->
|
|
266
|
+
<div class="list-empty" role="status" aria-live="polite">
|
|
267
|
+
<p class="list-empty__title">No results yet</p>
|
|
268
|
+
<p class="list-empty__hint">Try adjusting your filters or add your first item.</p>
|
|
269
|
+
<a href="/new" class="button">Add item</a>
|
|
270
|
+
</div>
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Condition for skipping: the list is static and never rendered with zero items (e.g., a hard-coded feature list). Record the reason.
|
|
274
|
+
|
|
275
|
+
### Error
|
|
276
|
+
|
|
277
|
+
Error states have two levels: field-level validation (which field failed and why) and page-level errors (network failure, permission denied, server error). Both must be implemented.
|
|
278
|
+
|
|
279
|
+
Field-level requirements (checked by `DESIGN-FORM-NO-ERROR`):
|
|
280
|
+
- `aria-invalid="true"` on the failed input.
|
|
281
|
+
- An associated error message element with `role="alert"` or linked via `aria-describedby`.
|
|
282
|
+
- Error copy that states what is wrong and what to do: "Email is already in use. Sign in instead." — not "Invalid email."
|
|
283
|
+
|
|
284
|
+
```html
|
|
285
|
+
<label for="email">Email</label>
|
|
286
|
+
<input id="email" type="email" aria-invalid="true" aria-describedby="email-error">
|
|
287
|
+
<p id="email-error" role="alert" class="field-error">
|
|
288
|
+
This email is already registered. <a href="/login">Sign in instead</a>.
|
|
289
|
+
</p>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Page-level requirements:
|
|
293
|
+
- Network failure: a visible message plus a retry action.
|
|
294
|
+
- Permission error: explain what the user does not have access to and how to get it.
|
|
295
|
+
- Server error: a human-readable message; never expose a stack trace to users.
|
|
296
|
+
|
|
297
|
+
### Success
|
|
298
|
+
|
|
299
|
+
Completed actions must be confirmed. Silent success — a form that submits and nothing visibly changes — is an interaction failure that generates support tickets.
|
|
300
|
+
|
|
301
|
+
Options in order of preference:
|
|
302
|
+
1. **Inline confirmation** — replace the form or button with a success state in place. No page jump.
|
|
303
|
+
2. **Toast notification** — a brief (3–5s) dismissible message in a consistent screen position. Use `role="status"` and `aria-live="polite"`.
|
|
304
|
+
3. **Page transition** — navigate to a confirmation page. Appropriate when the completed action is a significant milestone (account creation, order placed).
|
|
305
|
+
|
|
306
|
+
The confirmation copy must state the outcome specifically: "Payment of ₩35,000 received" — not "Success!" or "Done".
|
|
307
|
+
|
|
308
|
+
### Disabled
|
|
309
|
+
|
|
310
|
+
A greyed-out button with no explanation is an accessibility and usability failure. Users cannot tell whether the action is temporarily unavailable, permanently removed, or a bug. The disabled state must always communicate why.
|
|
311
|
+
|
|
312
|
+
Options:
|
|
313
|
+
- Remove the element entirely when the action is permanently unavailable in this context.
|
|
314
|
+
- Keep it visible but explain: a tooltip on hover/focus (`title` attribute minimum; `aria-describedby` preferred), or inline micro-copy.
|
|
315
|
+
- Never use `disabled` as a substitute for hidden — `display: none` is the correct tool when the action does not exist.
|
|
316
|
+
|
|
317
|
+
```html
|
|
318
|
+
<!-- Pairing disabled state with explanatory copy -->
|
|
319
|
+
<button type="button" disabled aria-describedby="publish-reason">Publish</button>
|
|
320
|
+
<p id="publish-reason" class="field-hint">Add a title before publishing.</p>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Offline
|
|
324
|
+
|
|
325
|
+
If the product has any network dependency, there must be a plan for offline states. The minimum is detecting `navigator.onLine === false` and showing a non-blocking notification that connectivity is required.
|
|
326
|
+
|
|
327
|
+
If offline capability is out of scope for this build, record it explicitly:
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
omd decision "Offline state: not implemented" \
|
|
331
|
+
--why "this is a connected-only tool; offline use is an explicit non-requirement for v1"
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
A decision to skip offline handling is valid. An implicit skip — never thinking about it — is not.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Applying the checklist
|
|
339
|
+
|
|
340
|
+
Walk each item in order. For each one, write the decision into `.omd/decisions.md`:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
omd decision "::selection bg: oklch from --color-accent at 20%" \
|
|
344
|
+
--why "accent-derived highlight; 4.5:1 verified by omd check"
|
|
345
|
+
|
|
346
|
+
omd decision "Scrollbar: standards-track scrollbar-color/scrollbar-width with webkit fallback" \
|
|
347
|
+
--why "thin scrollbar matches quiet register; track transparent"
|
|
348
|
+
|
|
349
|
+
omd decision "Favicon: inline SVG data-URI, monogram M on brand-dark background" \
|
|
350
|
+
--why "no asset file dependency; legible at 16x16"
|
|
351
|
+
|
|
352
|
+
omd decision "OG title: '모아 — 목표까지 함께하는 저축'" \
|
|
353
|
+
--why "specific claim, passes SLOP-COPY; mirrors page hero"
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
An item that is genuinely not applicable — `::selection` on a non-interactive data terminal, scrollbar styling on a page that disables scrolling by design — still gets a decision entry with the reason. The absence of a record is not the same as a decision not to implement.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export type CraftPhase = 'semantic' | 'visual';
|
|
5
|
+
export interface CraftCheckpoint {
|
|
6
|
+
ts: string;
|
|
7
|
+
phase: CraftPhase;
|
|
8
|
+
render: string;
|
|
9
|
+
observed: string;
|
|
10
|
+
changed: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const pathFor = (cwd: string): string => join(cwd, '.omd', 'craft.jsonl');
|
|
14
|
+
|
|
15
|
+
export function recordCraft(cwd: string, input: Omit<CraftCheckpoint, 'ts'>): string {
|
|
16
|
+
if (!['semantic', 'visual'].includes(input.phase)) throw new Error('craft phase must be semantic or visual');
|
|
17
|
+
if (!input.render.trim() || input.observed.trim().length < 4 || input.changed.trim().length < 4) {
|
|
18
|
+
throw new Error('--render, --observed, and --changed require concrete values');
|
|
19
|
+
}
|
|
20
|
+
if (/^(no|none|nothing|unchanged|no changes?|변경 ?없음)[.!]?$/i.test(input.changed.trim())) {
|
|
21
|
+
throw new Error('A craft checkpoint must record a concrete resulting change.');
|
|
22
|
+
}
|
|
23
|
+
const path = pathFor(cwd);
|
|
24
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
25
|
+
appendFileSync(path, `${JSON.stringify({ ts: new Date().toISOString(), ...input })}\n`);
|
|
26
|
+
return path;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readCraft(cwd: string): CraftCheckpoint[] {
|
|
30
|
+
const path = pathFor(cwd);
|
|
31
|
+
if (!existsSync(path)) return [];
|
|
32
|
+
return readFileSync(path, 'utf8').trim().split('\n').filter(Boolean).map((line) => JSON.parse(line) as CraftCheckpoint);
|
|
33
|
+
}
|