@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,223 @@
|
|
|
1
|
+
# Bento grid
|
|
2
|
+
|
|
3
|
+
> Candidate hypothesis only. Revalidate its condition, values, and responsive transition
|
|
4
|
+
> against the current composition contract; do not transfer this page recipe unchanged.
|
|
5
|
+
|
|
6
|
+
A bento grid divides a section into cells of intentionally different sizes, arranged as a
|
|
7
|
+
mosaic. The cell size hierarchy communicates content hierarchy: the largest cell holds
|
|
8
|
+
the most important content, and the eye reads that before anything else. The grid earns
|
|
9
|
+
its distinctiveness from non-uniformity — if all cells are the same size, it is a card
|
|
10
|
+
grid, not a bento grid, regardless of what it is called.
|
|
11
|
+
|
|
12
|
+
## When it earns its place / When it does not
|
|
13
|
+
|
|
14
|
+
Condition: the section contains varied content types that benefit from non-uniform visual
|
|
15
|
+
weight — a primary feature with more depth, supporting features with less, and perhaps a
|
|
16
|
+
decorative or data element at a different proportion. The arrangement must be a statement
|
|
17
|
+
of hierarchy: the user can tell at a glance which cell matters most because it is largest.
|
|
18
|
+
A homepage overview where one product capability is the anchor and three or four support
|
|
19
|
+
it, a feature highlight section where capabilities are genuinely asymmetric — these earn
|
|
20
|
+
the grid.
|
|
21
|
+
|
|
22
|
+
`core/theory/layout.md` on card hierarchy: "Every card must have an internal hierarchy:
|
|
23
|
+
one primary piece of information that the eye finds first, and secondary information that
|
|
24
|
+
serves it." The bento grid applies this principle at the section level: one cell is the
|
|
25
|
+
primary; all others serve it.
|
|
26
|
+
|
|
27
|
+
**Anti-slop clause.** A bento grid that fills all cells with equal-weight cards is
|
|
28
|
+
SLOP-TRIPLE-CARD rendered at scale. The grid earns its place only when the cell-size
|
|
29
|
+
hierarchy is non-recoverable: the arrangement itself tells the reader what matters most.
|
|
30
|
+
If you can swap cells without loss of meaning, the bento grid is the wrong layout.
|
|
31
|
+
The `SLOP-TRIPLE-CARD` slop rule fires on three or more identically-treated cards
|
|
32
|
+
regardless of whether they are arranged in a standard grid or a bento mosaic — the
|
|
33
|
+
violation is in the equal treatment, not in the grid structure.
|
|
34
|
+
|
|
35
|
+
Condition against: content where all items are genuinely equal in importance — a pricing
|
|
36
|
+
table, a team member grid, a portfolio gallery. Forced hierarchy on equal content
|
|
37
|
+
misinforms the user. Also: narrow viewports where the bento mosaic must collapse to a
|
|
38
|
+
single column, losing the hierarchical arrangement that justified it. If the layout reads
|
|
39
|
+
as a simple stacked list on mobile, the bento grid is carrying the hierarchy only on
|
|
40
|
+
desktop — which means the hierarchy is not in the content, it is in the layout trick.
|
|
41
|
+
|
|
42
|
+
## Parameters
|
|
43
|
+
|
|
44
|
+
```css
|
|
45
|
+
:root {
|
|
46
|
+
/* Grid gap between cells. Consistent gap size is important — uneven gaps
|
|
47
|
+
read as different object weights. Use a single token. */
|
|
48
|
+
--bento-gap: clamp(8px, 1.5vw, 16px);
|
|
49
|
+
|
|
50
|
+
/* Base grid unit. All cell spans are multiples of this.
|
|
51
|
+
A 4-column base allows: 1 (quarter), 2 (half), 3 (three-quarter), 4 (full).
|
|
52
|
+
A 6-column base allows more granular spans. */
|
|
53
|
+
--bento-columns: 4;
|
|
54
|
+
|
|
55
|
+
/* Minimum row height. Cells taller than this are defined by row-span.
|
|
56
|
+
This sets the rhythm of the grid. */
|
|
57
|
+
--bento-row-height: clamp(160px, 22vw, 280px);
|
|
58
|
+
|
|
59
|
+
/* Cell radius: consistent across all cells.
|
|
60
|
+
Bento grids on award-winning sites use a consistent radius to read as one object.
|
|
61
|
+
Inconsistent radii break the mosaic reading. */
|
|
62
|
+
--bento-radius: var(--radius-card, 12px);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Implementation
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<!-- The grid container defines the column structure.
|
|
70
|
+
Each cell declares its own span — no JavaScript layout library needed.
|
|
71
|
+
The hierarchy is declared in the HTML: the primary cell comes first
|
|
72
|
+
and spans the largest area. -->
|
|
73
|
+
<div class="bento-grid">
|
|
74
|
+
<!-- Primary cell: largest, most important content. Spans 2 columns, 2 rows. -->
|
|
75
|
+
<div class="bento-cell bento-cell--primary">
|
|
76
|
+
<h2>The primary capability.</h2>
|
|
77
|
+
<p>The claim with the most depth, given the most space.</p>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<!-- Supporting cells: smaller, each with a distinct capability. -->
|
|
81
|
+
<div class="bento-cell bento-cell--secondary">
|
|
82
|
+
<h3>Supporting capability one.</h3>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="bento-cell bento-cell--secondary">
|
|
86
|
+
<h3>Supporting capability two.</h3>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<!-- Accent cell: a data point, a decorative element, or a CTA at smaller scale. -->
|
|
90
|
+
<div class="bento-cell bento-cell--accent">
|
|
91
|
+
<span class="bento-stat">48ms</span>
|
|
92
|
+
<span class="bento-stat-label">median response time</span>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<!-- Wide secondary: spans the full width below the primary cluster. -->
|
|
96
|
+
<div class="bento-cell bento-cell--wide">
|
|
97
|
+
<h3>The wider supporting context.</h3>
|
|
98
|
+
<p>Content that benefits from a wider canvas than the narrow supporting cells.</p>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```css
|
|
104
|
+
.bento-grid {
|
|
105
|
+
display: grid;
|
|
106
|
+
grid-template-columns: repeat(var(--bento-columns), 1fr);
|
|
107
|
+
grid-auto-rows: var(--bento-row-height);
|
|
108
|
+
gap: var(--bento-gap);
|
|
109
|
+
padding-inline: var(--space-page-margin, 5vw);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* All cells share the base styling: the radius and background.
|
|
113
|
+
Differentiation comes from content hierarchy, not from different styling. */
|
|
114
|
+
.bento-cell {
|
|
115
|
+
border-radius: var(--bento-radius);
|
|
116
|
+
background-color: var(--surface-subtle, #f5f5f3);
|
|
117
|
+
padding: var(--space-6, 1.5rem);
|
|
118
|
+
overflow: hidden;
|
|
119
|
+
/* Cells do not have individual box-shadows — the gap performs the separation. */
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Primary cell: the largest. The hierarchy is in the span, not in extra decoration. */
|
|
123
|
+
.bento-cell--primary {
|
|
124
|
+
grid-column: span 2;
|
|
125
|
+
grid-row: span 2;
|
|
126
|
+
/* The primary cell may have a distinct background only if it is the section's
|
|
127
|
+
anchor — not as generic differentiation. */
|
|
128
|
+
background-color: var(--surface-emphasis, #0f0f0f);
|
|
129
|
+
color: var(--on-surface-emphasis, #f5f5f3);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.bento-cell--secondary {
|
|
133
|
+
grid-column: span 1;
|
|
134
|
+
grid-row: span 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.bento-cell--accent {
|
|
138
|
+
grid-column: span 1;
|
|
139
|
+
grid-row: span 1;
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
align-items: flex-start;
|
|
143
|
+
justify-content: flex-end;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.bento-cell--wide {
|
|
147
|
+
grid-column: span var(--bento-columns); /* full width */
|
|
148
|
+
grid-row: span 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.bento-stat {
|
|
152
|
+
font-size: clamp(2rem, 6vw, 4rem);
|
|
153
|
+
font-weight: var(--weight-display, 700);
|
|
154
|
+
letter-spacing: -0.04em;
|
|
155
|
+
line-height: 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.bento-stat-label {
|
|
159
|
+
font-size: var(--text-xs, 0.75rem);
|
|
160
|
+
color: var(--text-secondary);
|
|
161
|
+
margin-top: var(--space-1, 0.25rem);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Responsive behavior
|
|
166
|
+
|
|
167
|
+
**375px (mobile):** The multi-column mosaic collapses to a single column. The hierarchy
|
|
168
|
+
must still read — the primary cell appears first in DOM order, retaining its importance
|
|
169
|
+
through position even when size parity is lost.
|
|
170
|
+
|
|
171
|
+
```css
|
|
172
|
+
@media (max-width: 600px) {
|
|
173
|
+
.bento-grid {
|
|
174
|
+
grid-template-columns: 1fr;
|
|
175
|
+
grid-auto-rows: auto; /* height driven by content on mobile */
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.bento-cell--primary,
|
|
179
|
+
.bento-cell--secondary,
|
|
180
|
+
.bento-cell--accent,
|
|
181
|
+
.bento-cell--wide {
|
|
182
|
+
grid-column: 1 / -1;
|
|
183
|
+
grid-row: auto;
|
|
184
|
+
min-height: var(--bento-row-height); /* preserve breathing room */
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**768px (tablet):** A two-column variant works well at tablet width. Reduce
|
|
190
|
+
`--bento-columns` to 2 and verify that the primary cell's two-column span still reads
|
|
191
|
+
as dominant — at 768px with two columns, a `span 2` cell occupies the full row, which
|
|
192
|
+
may remove the mosaic effect. Consider a `span 1` tall primary at tablet.
|
|
193
|
+
|
|
194
|
+
```css
|
|
195
|
+
@media (min-width: 601px) and (max-width: 1024px) {
|
|
196
|
+
.bento-grid {
|
|
197
|
+
--bento-columns: 2;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.bento-cell--primary {
|
|
201
|
+
grid-column: span 2;
|
|
202
|
+
grid-row: span 1; /* adjust: span 2 at 2-col is full-width */
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**1280px (desktop):** Full 4-column mosaic. The `--bento-row-height` ceiling of 280px
|
|
208
|
+
keeps cells within a readable range; taller cells are appropriate only when they hold
|
|
209
|
+
content that genuinely requires height (an image, a chart, a demonstration). An empty
|
|
210
|
+
tall cell is wasted space, not visual breathing room.
|
|
211
|
+
|
|
212
|
+
## Do not combine with
|
|
213
|
+
|
|
214
|
+
**asymmetric-diagonal-grid.md** — both techniques restructure the spatial grid at the
|
|
215
|
+
same level. Applying both in a single section produces competing spatial logics.
|
|
216
|
+
|
|
217
|
+
**section-inversion.md** inside individual bento cells — switching background colour per
|
|
218
|
+
cell introduces a fourth surface level (page → section → cell → inverted cell) that
|
|
219
|
+
obscures the hierarchy the bento grid was designed to communicate.
|
|
220
|
+
|
|
221
|
+
**A bento grid where every cell has the same span** — this collapses back into
|
|
222
|
+
`SLOP-TRIPLE-CARD` territory. The anti-slop clause above applies: if the spans are
|
|
223
|
+
uniform, use a standard grid or card layout and state the reason.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Editorial index labels (01, 02, 03)
|
|
2
|
+
|
|
3
|
+
> Candidate hypothesis only. Revalidate its condition, values, and responsive transition
|
|
4
|
+
> against the current composition contract; do not transfer this page recipe unchanged.
|
|
5
|
+
|
|
6
|
+
A large ordinal number — set at display scale, typically 10–15% opacity — anchors each
|
|
7
|
+
section to its position in a sequence. The number reads as background field before it
|
|
8
|
+
reads as a numeral; it reinforces order and creates depth without competing with the
|
|
9
|
+
section's primary content.
|
|
10
|
+
|
|
11
|
+
`expressive.md` on this technique: "The large number (set at display scale, often 10–15%
|
|
12
|
+
opacity as a background element) reinforces the ordered reading without becoming the
|
|
13
|
+
dominant visual."
|
|
14
|
+
|
|
15
|
+
## When it earns its place / When it does not
|
|
16
|
+
|
|
17
|
+
Condition: the content has a genuine sequence — steps in a process, chapters in a
|
|
18
|
+
narrative, items in an ordered argument. The numbering must correspond to a sequence the
|
|
19
|
+
user will perceive as sequential; the label confirms what the user already suspects. A
|
|
20
|
+
case study with four distinct phases, a feature walkthrough with a natural order, a
|
|
21
|
+
methodology page with numbered stages — each earns the label because the sequence is real.
|
|
22
|
+
|
|
23
|
+
`core/theory/layout.md` — von Restorff and the one memorable thing: the large number
|
|
24
|
+
differentiates each section from the others through the isolation effect. Used across all
|
|
25
|
+
sections, each number is equally differentiated, which means none of them is — the effect
|
|
26
|
+
cancels itself. Applied to a genuine sequence, the number adds the memory hook von Restorff
|
|
27
|
+
describes; applied arbitrarily, it is visual noise.
|
|
28
|
+
|
|
29
|
+
Condition against: fake sequences — content enumerated to look ordered when the items have
|
|
30
|
+
no natural sequence. A features section with three independent capabilities numbered 01–03
|
|
31
|
+
implies the user should attend to them in order; if they should not, the numbering lied.
|
|
32
|
+
`expressive.md`: "Numerical labels that carry no sequential meaning are decoration, and
|
|
33
|
+
decoration at display scale is noise." Also: navigation lists, card grids, or any context
|
|
34
|
+
where the user is scanning rather than reading in order.
|
|
35
|
+
|
|
36
|
+
This specific tell — large low-opacity ordinals on unordered sections — is a documented
|
|
37
|
+
signature of generated pages: giant faint numerals that imply sequence where none exists. The counter-condition is this recipe: ordinals on
|
|
38
|
+
sections whose content has a genuine sequence (methodology phases, walkthrough steps,
|
|
39
|
+
ordered chapters). When the label earns its place the ordinal confirms what the user
|
|
40
|
+
already suspects; when it does not, it is decoration at display scale and should be removed.
|
|
41
|
+
The SLOP-TRIPLE-CARD rule fires on the underlying grid structure that often accompanies
|
|
42
|
+
unearned numbering; adding ordinals to a triple-card grid resolves neither finding.
|
|
43
|
+
|
|
44
|
+
## Parameters
|
|
45
|
+
|
|
46
|
+
```css
|
|
47
|
+
:root {
|
|
48
|
+
/* Font size for the index number.
|
|
49
|
+
Large enough to read as a background field, not a foreground label.
|
|
50
|
+
Typical range: clamp(5rem, 15vw, 12rem). */
|
|
51
|
+
--index-font-size: clamp(5rem, 15vw, 12rem);
|
|
52
|
+
|
|
53
|
+
/* Opacity: low enough to read as depth, not text.
|
|
54
|
+
10–15% is the operative range; above 20% the number competes with the heading. */
|
|
55
|
+
--index-opacity: 0.1;
|
|
56
|
+
|
|
57
|
+
/* Position within the section grid.
|
|
58
|
+
Typically anchored to the left or top-left, offset behind the heading. */
|
|
59
|
+
--index-offset-top: -0.15em; /* nudge behind the heading's cap-height */
|
|
60
|
+
--index-offset-left: -0.05em; /* slight bleed into the margin */
|
|
61
|
+
|
|
62
|
+
/* Font weight.
|
|
63
|
+
Heavy weight at low opacity reads as texture; light weight at low opacity disappears.
|
|
64
|
+
700 or 800 is the operative range. */
|
|
65
|
+
--index-font-weight: 700;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Implementation
|
|
70
|
+
|
|
71
|
+
```html
|
|
72
|
+
<!-- Each section carries its own index number.
|
|
73
|
+
aria-hidden: the number is decorative; screen readers skip it.
|
|
74
|
+
The section heading carries the actual label for assistive technology. -->
|
|
75
|
+
<section class="indexed-section" data-index="01">
|
|
76
|
+
<span class="indexed-section__number" aria-hidden="true">01</span>
|
|
77
|
+
<div class="indexed-section__content">
|
|
78
|
+
<h2 class="indexed-section__heading">Discovery</h2>
|
|
79
|
+
<p>The phase where assumptions are tested against the real problem.</p>
|
|
80
|
+
</div>
|
|
81
|
+
</section>
|
|
82
|
+
|
|
83
|
+
<section class="indexed-section" data-index="02">
|
|
84
|
+
<span class="indexed-section__number" aria-hidden="true">02</span>
|
|
85
|
+
<div class="indexed-section__content">
|
|
86
|
+
<h2 class="indexed-section__heading">Definition</h2>
|
|
87
|
+
<p>Where the real problem is named, with evidence.</p>
|
|
88
|
+
</div>
|
|
89
|
+
</section>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```css
|
|
93
|
+
.indexed-section {
|
|
94
|
+
position: relative;
|
|
95
|
+
padding-block: var(--space-section, 6rem);
|
|
96
|
+
padding-inline: var(--space-page-margin, 5vw);
|
|
97
|
+
/* Overflow hidden: the large number must not create horizontal scroll */
|
|
98
|
+
overflow: hidden;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.indexed-section__number {
|
|
102
|
+
position: absolute;
|
|
103
|
+
top: var(--index-offset-top);
|
|
104
|
+
left: var(--index-offset-left);
|
|
105
|
+
font-size: var(--index-font-size);
|
|
106
|
+
font-weight: var(--index-font-weight);
|
|
107
|
+
font-family: var(--font-display);
|
|
108
|
+
line-height: 1;
|
|
109
|
+
letter-spacing: -0.04em;
|
|
110
|
+
opacity: var(--index-opacity);
|
|
111
|
+
/* Prevent the number from intercepting pointer events on content above */
|
|
112
|
+
pointer-events: none;
|
|
113
|
+
user-select: none;
|
|
114
|
+
/* z-index: below the content, above the background */
|
|
115
|
+
z-index: 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.indexed-section__content {
|
|
119
|
+
position: relative;
|
|
120
|
+
z-index: 1; /* above the index number */
|
|
121
|
+
/* The content column stays within the readable measure — max 72ch for body,
|
|
122
|
+
narrower for the heading. */
|
|
123
|
+
max-width: 60ch;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.indexed-section__heading {
|
|
127
|
+
font-size: var(--text-2xl, 2rem);
|
|
128
|
+
font-weight: var(--weight-heading, 600);
|
|
129
|
+
margin-bottom: var(--space-4, 1rem);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Responsive behavior
|
|
134
|
+
|
|
135
|
+
**375px (mobile):** The large number at `clamp(5rem, 15vw, 12rem)` computes to
|
|
136
|
+
`max(5rem, 56px) = 80px` at 375px — small enough to not dominate the viewport. The
|
|
137
|
+
absolute-positioned number may overlap the heading at this size; verify the heading remains
|
|
138
|
+
legible. If contrast is compromised, increase the `--index-offset-top` negative value to
|
|
139
|
+
push the number further behind or reduce `--index-opacity` to 0.06 at narrow viewports.
|
|
140
|
+
|
|
141
|
+
```css
|
|
142
|
+
@media (max-width: 600px) {
|
|
143
|
+
.indexed-section {
|
|
144
|
+
--index-opacity: 0.07; /* reduce further at narrow viewports to preserve legibility */
|
|
145
|
+
padding-block: var(--space-section-mobile, 3rem);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**768px (tablet):** The number at `15vw` computes to ~115px. The absolute positioning works
|
|
151
|
+
well; the heading and body copy are clearly in the foreground. No structural change needed;
|
|
152
|
+
adjust offset if the number's baseline aligns uncomfortably with the heading's cap height.
|
|
153
|
+
|
|
154
|
+
**1280px (desktop):** The ceiling `12rem` (192px at 16px root) governs. At this size the
|
|
155
|
+
number reads as a large background field. The content column at `max-width: 60ch` leaves
|
|
156
|
+
significant space on the right at full viewport — the index number fills that zone
|
|
157
|
+
naturally. The composition reads as editorial depth, not as an oversized ordinal.
|
|
158
|
+
|
|
159
|
+
## Do not combine with
|
|
160
|
+
|
|
161
|
+
**sidebar-margin-annotation.md** in the same section — both create competing secondary
|
|
162
|
+
reading tracks in the peripheral field (left side for the index, right margin for
|
|
163
|
+
annotations). The eye cannot establish which secondary track is the intended one.
|
|
164
|
+
|
|
165
|
+
**section-inversion.md** per section — when every section inverts, the index number's
|
|
166
|
+
opacity calibration breaks across light and dark surfaces. Calibrate separately for each
|
|
167
|
+
surface or avoid the combination.
|
|
168
|
+
|
|
169
|
+
**editorial-index-labels with fake sequences** — this bears repeating because the temptation
|
|
170
|
+
is real: do not apply index labels to a grid of independent features to make the page look
|
|
171
|
+
more structured. `expressive.md` names this directly as decoration. The slop linter's
|
|
172
|
+
`SLOP-TRIPLE-CARD` rule fires on the underlying grid; adding index numbers to that grid
|
|
173
|
+
does not resolve the finding.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Form wizard stepper
|
|
2
|
+
|
|
3
|
+
> Candidate hypothesis only. Revalidate its condition, values, and responsive transition
|
|
4
|
+
> against the current composition contract; do not transfer this page recipe unchanged.
|
|
5
|
+
|
|
6
|
+
A focused multi-step flow — onboarding, signup, checkout, complex creation — composed as
|
|
7
|
+
one narrow column with a visible progress model, one decision cluster per step, and an
|
|
8
|
+
exit from every state. The composition suppresses everything that is not the flow: no
|
|
9
|
+
global navigation pulling the user mid-task, no marketing panel repeating the pitch they
|
|
10
|
+
already accepted. `core/theory/ux.md` §Forms owns the step-count evidence (Baymard: a
|
|
11
|
+
visible progress indicator reduces perceived effort; only the steps the user's task
|
|
12
|
+
requires); `core/theory/layout.md` §Forms owns field-row grammar.
|
|
13
|
+
|
|
14
|
+
## When it earns its place / When it does not
|
|
15
|
+
|
|
16
|
+
Condition: the task has 8+ fields or genuinely dependent decisions (later fields depend
|
|
17
|
+
on earlier answers), or the user's mental load per screen must stay low (first-run
|
|
18
|
+
onboarding). Each step is one decision cluster the user can name ("계정", "팀 초대",
|
|
19
|
+
"결제") — a step that exists because an engineer needed the data, not because the user
|
|
20
|
+
makes a decision there, is merged away.
|
|
21
|
+
|
|
22
|
+
Condition against: under ~8 independent fields — a single-page form shows the full cost
|
|
23
|
+
upfront and is cheaper (`theory/ux.md` §Forms). Also against: pull-based product
|
|
24
|
+
onboarding where the product is usable immediately — prefer contextual empty-state
|
|
25
|
+
guidance over a forced wizard (§First-run experience: forced tutorials retain worse).
|
|
26
|
+
|
|
27
|
+
**Anti-trap clause.** Every step has an escape: back to the previous step without data
|
|
28
|
+
loss, save-and-exit or an explicit cancel with consequence named. A wizard whose only
|
|
29
|
+
exits are "다음" and the browser's back button is a dead end factory. Entered values
|
|
30
|
+
survive navigation in both directions and validation failure (value preservation is the
|
|
31
|
+
UX acceptance contract, not polish).
|
|
32
|
+
|
|
33
|
+
## Parameters
|
|
34
|
+
|
|
35
|
+
```css
|
|
36
|
+
:root {
|
|
37
|
+
/* One-column measure. Forms read and complete fastest in a single column
|
|
38
|
+
(theory/layout.md §Forms); 44-52ch keeps labels+inputs scannable. */
|
|
39
|
+
--wizard-measure: 48rem;
|
|
40
|
+
|
|
41
|
+
/* Progress model: steps are few, named, and visible. */
|
|
42
|
+
--wizard-step-gap: 8px;
|
|
43
|
+
--wizard-accent: var(--accent, #1f6f5c);
|
|
44
|
+
|
|
45
|
+
/* Primary action anchors the step end; back is quiet, never competing. */
|
|
46
|
+
--wizard-cta-h: 48px;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Implementation
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<main class="wizard">
|
|
54
|
+
<!-- Progress: named steps, current marked, completed steps revisitable. -->
|
|
55
|
+
<nav class="wizard-progress" aria-label="진행 단계">
|
|
56
|
+
<ol>
|
|
57
|
+
<li><a href="#step-account" class="done">계정</a></li>
|
|
58
|
+
<li aria-current="step">팀 초대</li>
|
|
59
|
+
<li>결제</li>
|
|
60
|
+
</ol>
|
|
61
|
+
</nav>
|
|
62
|
+
|
|
63
|
+
<form class="wizard-step" aria-labelledby="step-title">
|
|
64
|
+
<h1 id="step-title">팀원을 초대해요</h1>
|
|
65
|
+
<p class="wizard-why">건너뛰어도 돼요 — 나중에 설정에서 초대할 수 있어요.</p>
|
|
66
|
+
|
|
67
|
+
<!-- One decision cluster: fields in one column, labels above,
|
|
68
|
+
errors adjacent to their field, values preserved on failure. -->
|
|
69
|
+
|
|
70
|
+
<div class="wizard-actions">
|
|
71
|
+
<button type="button" class="wizard-back">이전</button>
|
|
72
|
+
<button type="submit" class="wizard-next">다음: 결제</button>
|
|
73
|
+
<button type="button" class="wizard-skip">건너뛰기</button>
|
|
74
|
+
</div>
|
|
75
|
+
</form>
|
|
76
|
+
</main>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```css
|
|
80
|
+
.wizard {
|
|
81
|
+
max-width: var(--wizard-measure);
|
|
82
|
+
margin-inline: auto;
|
|
83
|
+
padding-inline: 16px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.wizard-progress li[aria-current="step"] { color: var(--wizard-accent); font-weight: 600; }
|
|
87
|
+
|
|
88
|
+
.wizard-step { display: grid; row-gap: 16px; }
|
|
89
|
+
|
|
90
|
+
.wizard-actions {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 12px;
|
|
94
|
+
/* One primary per view: "다음" is filled; back/skip are quiet. */
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.wizard-next { min-height: var(--wizard-cta-h); }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The primary button predicts the next step by name ("다음: 결제" — a CTA predicts what
|
|
101
|
+
happens after activation, per the copy-deck protocol). The final step ends on a designed
|
|
102
|
+
confirmation that restates what was created and names the first real action in the
|
|
103
|
+
product (§Peak-end rule: the end is a deliberate moment, not a redirect).
|
|
104
|
+
|
|
105
|
+
## Responsive behavior
|
|
106
|
+
|
|
107
|
+
**1280px (desktop):** the column centers at `--wizard-measure`; surrounding space stays
|
|
108
|
+
empty — a wizard earns air (density follows task frequency; this is a low-frequency,
|
|
109
|
+
high-attention surface).
|
|
110
|
+
|
|
111
|
+
**768px (tablet):** identical structure; the progress list may compress to
|
|
112
|
+
"2/3 — 팀 초대" numeric form when horizontal room runs out, but the current step stays
|
|
113
|
+
named.
|
|
114
|
+
|
|
115
|
+
**375px (mobile):** actions dock to the bottom within thumb reach; the keyboard must not
|
|
116
|
+
cover the focused field or the primary action (test with the on-screen keyboard open —
|
|
117
|
+
the bottom-CTA-under-keyboard failure is the classic mobile wizard defect). Back remains
|
|
118
|
+
visible without scrolling. At 320px nothing two-dimensional remains.
|
|
119
|
+
|
|
120
|
+
## Do not combine with
|
|
121
|
+
|
|
122
|
+
**app-shell-workbench.md global navigation** — a focused flow suppresses global nav;
|
|
123
|
+
offering the whole product map mid-checkout invites abandonment and half-created state.
|
|
124
|
+
Show at most a logo-exit with a named consequence.
|
|
125
|
+
|
|
126
|
+
**split-screen-hero.md as a signup decoration panel** — the persuasion already happened;
|
|
127
|
+
a marketing half-panel steals half the viewport from the form on every step and doubles
|
|
128
|
+
the reflow work at 768px for nothing the task needs.
|
|
129
|
+
|
|
130
|
+
**Progress bars without step names** — a bare percentage tells the user neither where
|
|
131
|
+
they are nor what remains; the anti-pattern version of the visible-progress evidence this
|
|
132
|
+
recipe cites.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Master-detail flow
|
|
2
|
+
|
|
3
|
+
> Candidate hypothesis only. Revalidate its condition, values, and responsive transition
|
|
4
|
+
> against the current composition contract; do not transfer this page recipe unchanged.
|
|
5
|
+
|
|
6
|
+
A list (master) and the selected object's workspace (detail) presented as one continuous
|
|
7
|
+
loop: select → inspect → act → return. The composition decision is where the detail
|
|
8
|
+
lives — beside the list, over it, or as a separate screen — and that decision is made
|
|
9
|
+
from loop frequency and context needs, not from viewport fashion. `core/theory/ux.md`
|
|
10
|
+
§Flows: every step that exists only because the layout forced a round-trip is an obstacle.
|
|
11
|
+
`core/theory/layout.md` §Density governs the master's row height and scan columns.
|
|
12
|
+
|
|
13
|
+
## When it earns its place / When it does not
|
|
14
|
+
|
|
15
|
+
Condition: the primary task is processing or inspecting items from a collection —
|
|
16
|
+
tickets, orders, documents, contacts, messages — and the user alternates between
|
|
17
|
+
scanning the collection and working on one item many times per session.
|
|
18
|
+
|
|
19
|
+
Choose the detail presentation by two questions:
|
|
20
|
+
|
|
21
|
+
1. **Does acting on the item require seeing the list?** (triage, comparison, queue
|
|
22
|
+
awareness) → side-by-side split. The list keeps its scan columns visible; the detail
|
|
23
|
+
never fully covers them.
|
|
24
|
+
2. **Is the item work immersive and self-contained?** (long editing, composition) →
|
|
25
|
+
full-screen detail with an explicit return path, or an overlay panel when the visit
|
|
26
|
+
is brief (peek, quick edit).
|
|
27
|
+
|
|
28
|
+
Condition against: collections the user visits rarely or only reads linearly (an archive,
|
|
29
|
+
a blog index) — a plain list with navigation is cheaper. Also against: splitting the
|
|
30
|
+
viewport when the detail needs the full width to do its job (a canvas editor) — that is
|
|
31
|
+
an app-shell question, not master-detail.
|
|
32
|
+
|
|
33
|
+
**Anti-overlay clause.** An overlay detail that hides the list's status/assignee columns
|
|
34
|
+
while the loop still needs them breaks queue awareness — the failure seen whenever a
|
|
35
|
+
"slick" slide-over covers exactly the columns the user was triaging by. If the loop needs
|
|
36
|
+
the list, the split is side-by-side and the list keeps `minmax(0, …)` room for its scan
|
|
37
|
+
columns.
|
|
38
|
+
|
|
39
|
+
## Parameters
|
|
40
|
+
|
|
41
|
+
```css
|
|
42
|
+
:root {
|
|
43
|
+
/* Master:detail ratio. 7:5 favors scanning; 5:7 favors item work.
|
|
44
|
+
Choose from the frame's frequent action, then verify at 1280. */
|
|
45
|
+
--md-master-fr: 7fr;
|
|
46
|
+
--md-detail-fr: 5fr;
|
|
47
|
+
|
|
48
|
+
/* Minimum master width before the split collapses to stacked navigation.
|
|
49
|
+
Below this the scan columns compress into unreadability. */
|
|
50
|
+
--md-master-min: 480px;
|
|
51
|
+
|
|
52
|
+
/* Selected-row marker: structural (inset bar + tint), never tint alone. */
|
|
53
|
+
--md-selected-bg: color-mix(in srgb, var(--accent, #1f6f5c) 8%, transparent);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Implementation
|
|
58
|
+
|
|
59
|
+
```html
|
|
60
|
+
<div class="master-detail">
|
|
61
|
+
<section class="md-master" aria-label="목록">
|
|
62
|
+
<!-- Toolbar: search + filters + primary action. Then the collection at
|
|
63
|
+
working density. Selection state is visible and persistent. -->
|
|
64
|
+
<table class="md-table">
|
|
65
|
+
<!-- Scan columns: the nouns the user triages by (status, owner, time).
|
|
66
|
+
The row's open control is a real button/link for keyboard reach. -->
|
|
67
|
+
</table>
|
|
68
|
+
</section>
|
|
69
|
+
|
|
70
|
+
<section class="md-detail" aria-label="상세">
|
|
71
|
+
<!-- Item header: title + status control + explicit return path ("목록으로").
|
|
72
|
+
Then the item workspace: content, then the action surface (reply box,
|
|
73
|
+
form) at the end of the loop, sticky on mobile. -->
|
|
74
|
+
</section>
|
|
75
|
+
</div>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```css
|
|
79
|
+
.master-detail {
|
|
80
|
+
display: grid;
|
|
81
|
+
grid-template-columns: minmax(var(--md-master-min), var(--md-master-fr)) minmax(0, var(--md-detail-fr));
|
|
82
|
+
min-height: 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.md-master { border-right: 1px solid var(--line, #e5e7e5); min-width: 0; }
|
|
86
|
+
|
|
87
|
+
.md-table tr[aria-selected="true"] {
|
|
88
|
+
background: var(--md-selected-bg);
|
|
89
|
+
box-shadow: inset 3px 0 0 var(--accent, #1f6f5c);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.md-detail { min-width: 0; display: flex; flex-direction: column; }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The empty-detail state is designed, not blank: when nothing is selected, the detail
|
|
96
|
+
region says what selecting does ("왼쪽 목록에서 티켓을 열면 여기에 대화가 보여요") — a
|
|
97
|
+
dead pane teaches nothing. Acting in the detail updates the master in place (status
|
|
98
|
+
badge, ordering) so the loop's feedback is visible where the next scan happens.
|
|
99
|
+
|
|
100
|
+
## Responsive behavior
|
|
101
|
+
|
|
102
|
+
**1280px (desktop):** side-by-side split at the declared ratio. Both regions scroll
|
|
103
|
+
independently; the detail's action surface stays reachable without scrolling past the
|
|
104
|
+
full conversation when the loop is reply-heavy.
|
|
105
|
+
|
|
106
|
+
**768px (tablet):** the split usually no longer fits `--md-master-min`; collapse to
|
|
107
|
+
stacked navigation — list screen ↔ detail screen — with the selection remembered on
|
|
108
|
+
return (scroll position and selected row preserved). An overlay panel is acceptable here
|
|
109
|
+
only when the loop does not need the list's columns while acting.
|
|
110
|
+
|
|
111
|
+
**375px (mobile):** always stacked navigation: full-screen list → full-screen detail
|
|
112
|
+
with a visible "목록으로" return in the top zone and the action surface sticky at the
|
|
113
|
+
bottom within thumb reach. DOM order stays list-then-detail so focus order and back
|
|
114
|
+
navigation match the visual model. At 320px the list rows recompose (title line +
|
|
115
|
+
meta line + status mark) instead of compressing table columns.
|
|
116
|
+
|
|
117
|
+
## Do not combine with
|
|
118
|
+
|
|
119
|
+
**app-shell-workbench.md side nav at tablet width** — an expanded side nav plus a
|
|
120
|
+
side-by-side split leaves the master below its minimum; collapse the nav to the icon
|
|
121
|
+
rail before collapsing the split.
|
|
122
|
+
|
|
123
|
+
**sticky-sidebar-scroll.md** — a sticky editorial sidebar and a master pane are competing
|
|
124
|
+
persistent regions with different scroll models; a surface gets one persistent
|
|
125
|
+
navigation-like region beside the work, not two.
|
|
126
|
+
|
|
127
|
+
**Modal dialogs as the default detail** — a modal steals the list, traps scroll, and
|
|
128
|
+
forgets position; it is for confirmations and small focused inputs, not for the loop's
|
|
129
|
+
main workspace.
|