@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.
Files changed (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +282 -0
  3. package/README.md +282 -0
  4. package/adapters/build.ts +184 -0
  5. package/adapters/claude.ts +111 -0
  6. package/adapters/codex.ts +68 -0
  7. package/adapters/tokens.ts +21 -0
  8. package/adapters/tool-map.json +7 -0
  9. package/bin/omd-install.ts +76 -0
  10. package/bin/omd.ts +1774 -0
  11. package/core/asset-sourcing/index.ts +187 -0
  12. package/core/coach/index.ts +98 -0
  13. package/core/composition/app-shell-workbench.md +168 -0
  14. package/core/composition/asymmetric-diagonal-grid.md +178 -0
  15. package/core/composition/bento-grid.md +223 -0
  16. package/core/composition/editorial-index-labels.md +173 -0
  17. package/core/composition/form-wizard-stepper.md +132 -0
  18. package/core/composition/master-detail-flow.md +129 -0
  19. package/core/composition/section-inversion.md +182 -0
  20. package/core/composition/sidebar-margin-annotation.md +184 -0
  21. package/core/composition/split-screen-hero.md +224 -0
  22. package/core/composition/sticky-sidebar-scroll.md +219 -0
  23. package/core/composition/typographic-hero.md +100 -0
  24. package/core/composition-contract/index.ts +233 -0
  25. package/core/composition-contract/visual-richness.ts +101 -0
  26. package/core/config/index.ts +27 -0
  27. package/core/copy/index.ts +296 -0
  28. package/core/craft/finish-pass.md +356 -0
  29. package/core/craft/index.ts +33 -0
  30. package/core/design/index.ts +581 -0
  31. package/core/design/interaction-states.ts +189 -0
  32. package/core/eval-harness/index.ts +225 -0
  33. package/core/evidence/final.ts +444 -0
  34. package/core/evidence/task.ts +459 -0
  35. package/core/figma/client.ts +312 -0
  36. package/core/figma/diff.ts +261 -0
  37. package/core/figma/responsive.ts +310 -0
  38. package/core/figma/system.ts +466 -0
  39. package/core/figma/types.ts +185 -0
  40. package/core/frame/check-ux.ts +180 -0
  41. package/core/frame/index.ts +39 -0
  42. package/core/frame/write.ts +168 -0
  43. package/core/graphics/css-illustration-primitives.md +189 -0
  44. package/core/graphics/duotone-image-presets.md +173 -0
  45. package/core/graphics/gradient-mesh.md +178 -0
  46. package/core/graphics/noise-grain-texture.md +150 -0
  47. package/core/graphics/placeholder-policy.md +185 -0
  48. package/core/graphics/svg-geometric-patterns.md +145 -0
  49. package/core/history/index.ts +39 -0
  50. package/core/install/detect.ts +32 -0
  51. package/core/install/install.ts +321 -0
  52. package/core/install/patch-claude.ts +109 -0
  53. package/core/install/patch-codex.ts +82 -0
  54. package/core/interaction/index.ts +101 -0
  55. package/core/interaction/recipes/signature-lighting.md +203 -0
  56. package/core/ir/dom.ts +352 -0
  57. package/core/ir/normalize.ts +177 -0
  58. package/core/motion/easing.md +151 -0
  59. package/core/motion/energy.ts +188 -0
  60. package/core/motion/recipes/image-hover-distortion.md +198 -0
  61. package/core/motion/recipes/magnetic-hover.md +229 -0
  62. package/core/motion/recipes/marquee.md +202 -0
  63. package/core/motion/recipes/number-counter.md +242 -0
  64. package/core/motion/recipes/page-loader.md +240 -0
  65. package/core/motion/recipes/parallax.md +233 -0
  66. package/core/motion/recipes/scroll-reveal.md +210 -0
  67. package/core/motion/recipes/section-color-inversion.md +217 -0
  68. package/core/motion/recipes/split-text-entrance.md +222 -0
  69. package/core/motion/recipes/stagger-orchestrator.md +227 -0
  70. package/core/motion/recipes/sticky-scene-transition.md +246 -0
  71. package/core/motion/recipes/view-transitions.md +239 -0
  72. package/core/probe/index.ts +186 -0
  73. package/core/probe/schema.json +46 -0
  74. package/core/protocol/composition-contract.md +192 -0
  75. package/core/protocol/copy-deck.md +96 -0
  76. package/core/protocol/human-design-loop.md +447 -0
  77. package/core/protocol/slop-review.md +71 -0
  78. package/core/ref/batch.ts +94 -0
  79. package/core/ref/blueprint.ts +199 -0
  80. package/core/ref/distance.ts +225 -0
  81. package/core/ref/invariants.ts +162 -0
  82. package/core/ref/signal.ts +37 -0
  83. package/core/ref/store.ts +122 -0
  84. package/core/render/index.ts +607 -0
  85. package/core/rules/attribution.ts +159 -0
  86. package/core/rules/builtin/contrast.yaml +8 -0
  87. package/core/rules/builtin/focus.yaml +8 -0
  88. package/core/rules/builtin/hit-area.yaml +8 -0
  89. package/core/rules/builtin/ko.yaml +6 -0
  90. package/core/rules/builtin/motion.yaml +80 -0
  91. package/core/rules/builtin/slop.yaml +365 -0
  92. package/core/rules/builtin/spacing.yaml +9 -0
  93. package/core/rules/builtin/system.yaml +42 -0
  94. package/core/rules/builtin/token.yaml +8 -0
  95. package/core/rules/builtin/ux.yaml +125 -0
  96. package/core/rules/engine.ts +88 -0
  97. package/core/rules/leakage.ts +72 -0
  98. package/core/rules/motion-spec.ts +204 -0
  99. package/core/site/index.ts +100 -0
  100. package/core/slop/index.ts +427 -0
  101. package/core/slop/text-slop.ts +220 -0
  102. package/core/source-seal/index.ts +174 -0
  103. package/core/stack/index.ts +53 -0
  104. package/core/target/index.ts +183 -0
  105. package/core/theory/color.md +285 -0
  106. package/core/theory/components.md +272 -0
  107. package/core/theory/craft.md +270 -0
  108. package/core/theory/expressive.md +213 -0
  109. package/core/theory/imagegen.md +105 -0
  110. package/core/theory/layout.md +159 -0
  111. package/core/theory/motion.md +283 -0
  112. package/core/theory/typography.md +134 -0
  113. package/core/theory/ux.md +580 -0
  114. package/core/theory/voice.md +595 -0
  115. package/core/types.ts +684 -0
  116. package/package.json +40 -0
  117. package/src/agents/composer.agent.yaml +174 -0
  118. package/src/agents/eye.agent.yaml +208 -0
  119. package/src/agents/framer.agent.yaml +52 -0
  120. package/src/agents/glance.agent.yaml +19 -0
  121. package/src/agents/hand.agent.yaml +200 -0
  122. package/src/agents/scout.agent.yaml +71 -0
  123. package/src/agents/sketch.agent.yaml +64 -0
  124. package/src/agents/typesetter.agent.yaml +52 -0
  125. package/src/agents/writer.agent.yaml +63 -0
  126. package/src/skills/omd-coach/SKILL.md +40 -0
  127. package/src/skills/omd-critique/SKILL.md +77 -0
  128. package/src/skills/omd-figma/SKILL.md +256 -0
  129. package/src/skills/omd-humanize/SKILL.md +160 -0
  130. package/src/skills/omd-scout/SKILL.md +77 -0
  131. package/src/skills/omd-ultradesign/SKILL.md +521 -0
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Asset sourcing policy core.
3
+ *
4
+ * OMD supports three asset paths, in order of precedence:
5
+ * 1. (default) User-provided assets, optionally treated with a duotone filter
6
+ * (see `core/graphics/duotone-image-presets.md`) for visual unification.
7
+ * 2. (conditional) AI-generated imagery — only when the host environment
8
+ * declares image-generation capability, and only for abstract/atmospheric
9
+ * zones. Factual carriers (team photos, product screenshots, real people,
10
+ * logos) may never be satisfied by AI-generated imagery.
11
+ * 3. (additive) WebGL/3D — gated on hand precedence (explicit user request OR
12
+ * greenfield concept necessity), a declared performance budget, and a
13
+ * non-canvas semantic fallback.
14
+ * When none of these apply, the design falls back to CSS/SVG treatments per
15
+ * `core/graphics/placeholder-policy.md` — a grey box is a defect, never the
16
+ * final answer.
17
+ *
18
+ * RUNTIME SCOPE NOTE: this module is a pure decision/gate core. It contains
19
+ * no I/O and calls no external services — it decides *whether* a path is
20
+ * permitted, never executes it. AI image generation IS wired at the agent
21
+ * layer: on a host that provides an image-generation capability (Codex,
22
+ * Claude Code), the composer/hand invoke it per `core/theory/imagegen.md`
23
+ * for image-first concept drafts and for abstract/atmospheric shipped assets,
24
+ * with this module as the admissibility gate and committed provenance recorded
25
+ * via `omd decision`. What remains environment-dependent is 3D/WebGL
26
+ * performance MEASUREMENT (render tracing, Lighthouse-style budgets): those
27
+ * adapters are intentionally NOT stubbed here and must not fabricate a fake
28
+ * "success" result for a capability the environment cannot exercise.
29
+ */
30
+
31
+ export type AssetZone = 'factual' | 'abstract' | 'atmospheric' | 'structural';
32
+
33
+ export type AssetStrategy =
34
+ | 'user-asset-duotone'
35
+ | 'ai-image'
36
+ | 'webgl-3d'
37
+ | 'css-svg-fallback';
38
+
39
+ export interface AssetStrategyInputs {
40
+ hasUserAsset: boolean;
41
+ hostHasImageGen: boolean;
42
+ explicitWebglRequest?: boolean;
43
+ greenfieldConceptNecessity?: boolean;
44
+ perfBudgetDeclared?: boolean;
45
+ zone: AssetZone;
46
+ }
47
+
48
+ export interface AssetStrategyDecision {
49
+ strategy: AssetStrategy;
50
+ reason: string;
51
+ fallbackChain: string[];
52
+ }
53
+
54
+ /**
55
+ * Decide the asset sourcing strategy for a single content slot. Pure
56
+ * decision function — no I/O, no runtime capability checks beyond the
57
+ * declared inputs.
58
+ */
59
+ export function decideAssetStrategy(inputs: AssetStrategyInputs): AssetStrategyDecision {
60
+ const { hasUserAsset, hostHasImageGen, zone } = inputs;
61
+ const explicitWebglRequest = inputs.explicitWebglRequest ?? false;
62
+ const greenfieldConceptNecessity = inputs.greenfieldConceptNecessity ?? false;
63
+ const perfBudgetDeclared = inputs.perfBudgetDeclared ?? false;
64
+
65
+ if (hasUserAsset) {
66
+ return {
67
+ strategy: 'user-asset-duotone',
68
+ reason: 'A user-provided asset is available; it is the default source and takes precedence over AI generation or WebGL.',
69
+ fallbackChain: ['user-asset-duotone', 'css-svg-fallback'],
70
+ };
71
+ }
72
+
73
+ // Factual carriers (team photos, product screenshots, real people, logos)
74
+ // may never be satisfied by AI-generated imagery, regardless of host
75
+ // capability. See `validateAiImageUsage` for the corresponding gate.
76
+ const aiImageEligibleZone = zone === 'abstract' || zone === 'atmospheric';
77
+ if (zone !== 'factual' && aiImageEligibleZone && hostHasImageGen) {
78
+ return {
79
+ strategy: 'ai-image',
80
+ reason: `No user asset is available; the "${zone}" zone permits AI-generated imagery and the host declares image-generation capability.`,
81
+ fallbackChain: ['ai-image', 'css-svg-fallback'],
82
+ };
83
+ }
84
+
85
+ const handPrecedence = explicitWebglRequest || greenfieldConceptNecessity;
86
+ if (handPrecedence && perfBudgetDeclared) {
87
+ return {
88
+ strategy: 'webgl-3d',
89
+ reason: 'No user asset or AI image path applies; WebGL/3D escalation is permitted by hand precedence and a declared performance budget.',
90
+ fallbackChain: ['webgl-3d', 'css-svg-fallback'],
91
+ };
92
+ }
93
+
94
+ return {
95
+ strategy: 'css-svg-fallback',
96
+ reason: zone === 'factual'
97
+ ? 'The zone is factual: AI-generated imagery is never permitted, no user asset was provided, and WebGL escalation was not authorised. Falling back to CSS/SVG per the placeholder policy.'
98
+ : 'No user asset, no eligible AI-image path, and no authorised WebGL escalation. Falling back to CSS/SVG per the placeholder policy.',
99
+ fallbackChain: ['css-svg-fallback'],
100
+ };
101
+ }
102
+
103
+ export interface AiImageUsageInputs {
104
+ zone: AssetZone;
105
+ hostHasImageGen: boolean;
106
+ provenance?: {
107
+ prompt?: string;
108
+ provider?: string;
109
+ };
110
+ }
111
+
112
+ export interface AiImageUsageValidation {
113
+ allowed: boolean;
114
+ violations: string[];
115
+ }
116
+
117
+ /**
118
+ * Gate for whether a specific AI-generated image is permitted to ship.
119
+ * Pure validation — the actual image generation call is a Phase 2 runtime
120
+ * adapter concern (see module header); this only decides admissibility.
121
+ */
122
+ export function validateAiImageUsage(inputs: AiImageUsageInputs): AiImageUsageValidation {
123
+ const violations: string[] = [];
124
+
125
+ if (inputs.zone === 'factual') {
126
+ violations.push('factual carrier: AI-generated imagery is never permitted for factual slots (team photos, product screenshots, real people, logos).');
127
+ } else if (inputs.zone !== 'abstract' && inputs.zone !== 'atmospheric') {
128
+ violations.push(`ineligible zone: AI-generated imagery is only permitted in abstract or atmospheric zones, not "${inputs.zone}".`);
129
+ }
130
+
131
+ if (!inputs.hostHasImageGen) {
132
+ violations.push('host capability: the host environment does not declare image-generation capability.');
133
+ }
134
+
135
+ if (!inputs.provenance?.prompt) {
136
+ violations.push('provenance missing: prompt is required to record how the image was generated.');
137
+ }
138
+
139
+ if (!inputs.provenance?.provider) {
140
+ violations.push('provenance missing: provider is required to record how the image was generated.');
141
+ }
142
+
143
+ return {
144
+ allowed: violations.length === 0,
145
+ violations,
146
+ };
147
+ }
148
+
149
+ export interface WebglEscalationInputs {
150
+ explicitUserRequest: boolean;
151
+ greenfieldConceptNecessity: boolean;
152
+ perfBudgetDeclared: boolean;
153
+ semanticFallbackPresent: boolean;
154
+ }
155
+
156
+ export interface WebglEscalationDecision {
157
+ permitted: boolean;
158
+ reasons: string[];
159
+ }
160
+
161
+ /**
162
+ * Gate for whether a WebGL/3D escalation is permitted. Pure decision —
163
+ * actual 3D performance measurement (render tracing, frame-budget
164
+ * verification) is a Phase 2 runtime adapter concern (see module header);
165
+ * this only decides admissibility from declared inputs.
166
+ */
167
+ export function evaluateWebglEscalation(inputs: WebglEscalationInputs): WebglEscalationDecision {
168
+ const reasons: string[] = [];
169
+
170
+ const handPrecedence = inputs.explicitUserRequest || inputs.greenfieldConceptNecessity;
171
+ if (!handPrecedence) {
172
+ reasons.push('hand precedence not established: neither an explicit user request nor a greenfield concept necessity was declared.');
173
+ }
174
+
175
+ if (!inputs.perfBudgetDeclared) {
176
+ reasons.push('performance budget not declared: a WebGL/3D escalation requires an explicit performance budget.');
177
+ }
178
+
179
+ if (!inputs.semanticFallbackPresent) {
180
+ reasons.push('semantic fallback missing: a non-canvas semantic fallback is required before a WebGL/3D escalation ships.');
181
+ }
182
+
183
+ return {
184
+ permitted: reasons.length === 0,
185
+ reasons,
186
+ };
187
+ }
@@ -0,0 +1,98 @@
1
+ import type { CoachReport, Category, Recurring, Run } from '../types.ts';
2
+
3
+ /** Below this many runs, a trend is noise, not a signal. */
4
+ const MIN_RUNS_FOR_CONFIDENCE = 4;
5
+
6
+ /** A rule must show up in at least this many distinct runs to be called recurring. */
7
+ const MIN_DISTINCT_RUNS = 2;
8
+
9
+ function categoryOf(ruleId: string): Category {
10
+ if (ruleId.startsWith('SLOP-')) return 'slop';
11
+ if (ruleId.startsWith('CONTRAST-') || ruleId.startsWith('HIT-')) return 'a11y';
12
+ return 'system';
13
+ }
14
+
15
+ function mean(runs: Run[], ruleId: string): number {
16
+ if (runs.length === 0) return 0;
17
+ const sum = runs.reduce((acc, r) => acc + (r.counts[ruleId] ?? 0), 0);
18
+ return sum / runs.length;
19
+ }
20
+
21
+ /**
22
+ * The change between the first and second half of the runs.
23
+ *
24
+ * A rule absent from the first half has no baseline, so a percent change does not exist.
25
+ * Reporting one — "+500%" because it went from zero to five — invents a statistic. The
26
+ * direction is knowable; the percentage is not, and `null` says so.
27
+ */
28
+ function changeFor(runs: Run[], ruleId: string): { changePct: number | null; trend: Recurring['trend'] } {
29
+ const mid = Math.floor(runs.length / 2);
30
+ const first = mean(runs.slice(0, mid), ruleId);
31
+ const second = mean(runs.slice(mid), ruleId);
32
+
33
+ if (first === 0) {
34
+ if (second === 0) return { changePct: 0, trend: 'flat' };
35
+ return { changePct: null, trend: 'worsening' };
36
+ }
37
+
38
+ const changePct = Math.round(((second - first) / first) * 100);
39
+ if (changePct <= -10) return { changePct, trend: 'improving' };
40
+ if (changePct >= 10) return { changePct, trend: 'worsening' };
41
+ return { changePct, trend: 'flat' };
42
+ }
43
+
44
+ const byCodeUnit = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
45
+
46
+ /**
47
+ * Skill, not taste. Coach only ever looks at `history` (what the linter found) and
48
+ * `decisions` (what the user overruled and why it was written down) — never
49
+ * `.omd/taste/`, which records preferences that have no right answer.
50
+ */
51
+ export function analyse(history: Run[], decisions: string[]): CoachReport {
52
+ const confident = history.length >= MIN_RUNS_FOR_CONFIDENCE;
53
+
54
+ const span = history.length === 0
55
+ ? null
56
+ : { from: history[0]!.ts, to: history[history.length - 1]!.ts };
57
+
58
+ const ruleIds = new Set<string>();
59
+ for (const run of history) for (const ruleId of Object.keys(run.counts)) ruleIds.add(ruleId);
60
+
61
+ const recurring: Recurring[] = [];
62
+ for (const ruleId of ruleIds) {
63
+ const appearances = history.filter((r) => (r.counts[ruleId] ?? 0) > 0);
64
+ if (appearances.length < MIN_DISTINCT_RUNS) continue;
65
+
66
+ const total = history.reduce((acc, r) => acc + (r.counts[ruleId] ?? 0), 0);
67
+
68
+ if (!confident) {
69
+ recurring.push({
70
+ rule: ruleId, category: categoryOf(ruleId), total, runs: appearances.length,
71
+ trend: 'insufficient', changePct: null,
72
+ });
73
+ continue;
74
+ }
75
+
76
+ const { changePct, trend } = changeFor(history, ruleId);
77
+ recurring.push({
78
+ rule: ruleId, category: categoryOf(ruleId), total, runs: appearances.length,
79
+ trend, changePct,
80
+ });
81
+ }
82
+
83
+ recurring.sort((a, b) => b.total - a.total || byCodeUnit(a.rule, b.rule));
84
+
85
+ // One decision is one overrule, however many times it names the rule. A decision that
86
+ // mentions SLOP-GRADIENT in both its title and its reason was still a single choice.
87
+ const overruleCounts = new Map<string, number>();
88
+ for (const decision of decisions) {
89
+ for (const rule of new Set(decision.match(/\bSLOP-[A-Z-]+\b/g) ?? [])) {
90
+ overruleCounts.set(rule, (overruleCounts.get(rule) ?? 0) + 1);
91
+ }
92
+ }
93
+ const overrules = [...overruleCounts.entries()]
94
+ .map(([rule, count]) => ({ rule, count }))
95
+ .sort((a, b) => b.count - a.count || byCodeUnit(a.rule, b.rule));
96
+
97
+ return { runs: history.length, span, recurring, overrules, confident };
98
+ }
@@ -0,0 +1,168 @@
1
+ # App shell workbench
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
+ The default grammar for a `product` work surface: a persistent shell (top bar and/or side
7
+ navigation) framing a dominant work region that owns the viewport. The shell answers
8
+ "where am I, where else can I go"; the work region carries the task loop. Nothing here is
9
+ a scroll narrative — the page does not argue, it operates. `core/theory/ux.md` §Surface
10
+ types names the condition; `core/theory/layout.md` §Density governs how loud the data may
11
+ be.
12
+
13
+ ## When it earns its place / When it does not
14
+
15
+ Condition: the frame's `uxSurface` is `product` and the primary screen serves a repeated
16
+ task loop over work objects — a queue, a dashboard, a console, an admin table, an editor.
17
+ The user returns to this screen many times a day; orientation cost must be near zero and
18
+ the frequent action reachable within the first viewport.
19
+
20
+ The shell earns a side navigation only when the product has 4+ peer destinations the user
21
+ switches between; below that, a top bar with inline destinations costs less horizontal
22
+ space and leaves more room for the work object (`theory/layout.md`: density follows task
23
+ frequency, not furniture convention).
24
+
25
+ Condition against: a single-surface marketing or editorial page (no destinations to hold),
26
+ or a focused flow (checkout, onboarding wizard) where global navigation invites
27
+ mid-task escape — use `form-wizard-stepper.md` there. Also against: using the shell as
28
+ decoration around content that is itself a landing page; an app shell does not convert a
29
+ persuasion page into a product.
30
+
31
+ **Anti-hero clause.** The work region's first viewport belongs to the work object at
32
+ representative data density. A display-scale headline, hero band, marketing gradient, or
33
+ illustration claiming that viewport is a grammar defect on this surface — the orientation
34
+ line (screen name + one-line state summary, e.g. "받은함 — 대기 12") is the entire
35
+ permitted "hero".
36
+
37
+ ## Parameters
38
+
39
+ ```css
40
+ :root {
41
+ /* Shell chrome sizes. The top bar stays under 56px so the work region keeps
42
+ the viewport; the side nav collapses to icons at tablet width. */
43
+ --shell-topbar-h: 52px;
44
+ --shell-sidenav-w: 220px;
45
+ --shell-sidenav-w-collapsed: 56px;
46
+
47
+ /* Work region density. Row height decides rows-per-viewport: 44-48px serves
48
+ expert repeated scanning; 56px+ serves low-frequency browsing. */
49
+ --work-row-h: 46px;
50
+
51
+ /* One accent marks the primary action and the current nav location.
52
+ Chrome stays neutral so data carries the color. */
53
+ --shell-accent: var(--accent, #1f6f5c);
54
+ --shell-chrome-bg: var(--surface, #fff);
55
+ --shell-chrome-line: var(--line, #e5e7e5);
56
+ }
57
+ ```
58
+
59
+ ## Implementation
60
+
61
+ ```html
62
+ <!-- The shell is a grid, not a scroll sequence. DOM order: shell nav first
63
+ (it is the product's map), then the work region — focus order follows. -->
64
+ <div class="app-shell">
65
+ <header class="shell-topbar">
66
+ <span class="shell-product">제품명</span>
67
+ <p class="shell-orientation">받은함 — 대기 12 · 진행 4</p>
68
+ <nav class="shell-account" aria-label="계정"><!-- profile, settings --></nav>
69
+ </header>
70
+
71
+ <nav class="shell-sidenav" aria-label="주 탐색">
72
+ <!-- The user's nouns, current location marked with aria-current. -->
73
+ <a href="/inbox" aria-current="page">받은함</a>
74
+ <a href="/customers">고객</a>
75
+ <a href="/reports">리포트</a>
76
+ <a href="/settings">설정</a>
77
+ </nav>
78
+
79
+ <main class="shell-work">
80
+ <!-- The work object owns this region: toolbar (search/filters/primary
81
+ action), then the queue/table/canvas at working density. -->
82
+ </main>
83
+ </div>
84
+ ```
85
+
86
+ ```css
87
+ .app-shell {
88
+ display: grid;
89
+ grid-template-columns: var(--shell-sidenav-w) minmax(0, 1fr);
90
+ grid-template-rows: var(--shell-topbar-h) minmax(0, 1fr);
91
+ grid-template-areas:
92
+ "topbar topbar"
93
+ "sidenav work";
94
+ min-height: 100vh;
95
+ }
96
+
97
+ .shell-topbar {
98
+ grid-area: topbar;
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 16px;
102
+ background: var(--shell-chrome-bg);
103
+ border-bottom: 1px solid var(--shell-chrome-line);
104
+ }
105
+
106
+ .shell-sidenav {
107
+ grid-area: sidenav;
108
+ border-right: 1px solid var(--shell-chrome-line);
109
+ }
110
+
111
+ .shell-sidenav a[aria-current="page"] {
112
+ color: var(--shell-accent);
113
+ font-weight: 600;
114
+ /* Location is marked structurally (inset bar), not by tint alone. */
115
+ box-shadow: inset 3px 0 0 var(--shell-accent);
116
+ }
117
+
118
+ .shell-work {
119
+ grid-area: work;
120
+ min-width: 0; /* tables and canvases must be allowed to shrink, not overflow */
121
+ }
122
+ ```
123
+
124
+ The work region's internal order is the task loop: orientation line → locate controls
125
+ (search, filters — always visible on an expert surface, never buried in a menu) → the
126
+ work object → detail/action surfaces. Reachable states (loading skeleton, empty first-run,
127
+ filtered-to-zero, error with recovery) are layouts of this same region, not separate
128
+ pages.
129
+
130
+ ## Responsive behavior
131
+
132
+ **1280px (desktop):** full shell — side nav expanded, work region at working density,
133
+ master-detail splits permitted inside the work region (see `master-detail-flow.md`).
134
+
135
+ **768px (tablet):** the side nav collapses to `--shell-sidenav-w-collapsed` icon rail with
136
+ labels on focus/hover; the work region keeps density. Two-region splits inside the work
137
+ area become stacked or overlay presentations.
138
+
139
+ **375px (mobile):** the shell inverts: top bar keeps the orientation line and a menu
140
+ disclosure; primary destinations move to a bottom tab bar within thumb reach
141
+ (`theory/ux.md` §Navigation — the frequent action must stay in the natural grip zone);
142
+ tables recompose to list rows. The frequent action stays within the first viewport —
143
+ sticky where the task demands it (e.g. a reply bar). Test 320px reflow: no horizontal
144
+ scroll except intentionally two-dimensional data.
145
+
146
+ ```css
147
+ @media (max-width: 760px) {
148
+ .app-shell {
149
+ grid-template-columns: 1fr;
150
+ grid-template-areas: "topbar" "work" "tabbar";
151
+ }
152
+ .shell-sidenav { display: none; }
153
+ }
154
+ ```
155
+
156
+ ## Do not combine with
157
+
158
+ **split-screen-hero.md / typographic-hero.md** — hero grammars claim the first viewport
159
+ that belongs to the work object; combining them recreates the landing-page-on-a-tool
160
+ defect this recipe exists to prevent.
161
+
162
+ **bento-grid.md as the work region** — a bento mosaic imposes editorial hierarchy on
163
+ peer work objects; a queue or table's hierarchy is in the data (sort order, status),
164
+ not in cell sizes. Bento is permitted only for a genuine overview dashboard where
165
+ metrics have real priority differences.
166
+
167
+ **section-inversion.md inside the work region** — alternating section backgrounds is a
168
+ scroll-narrative rhythm; a work surface has regions, not narrative sections.
@@ -0,0 +1,178 @@
1
+ # Asymmetric / diagonal 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 grid where one or more elements break from the column structure — rotated, offset, or
7
+ overlapping — to create tension and signal editorial intent. The effect reads as deliberate
8
+ when the deviation is singular; a page full of diagonal elements is not tension, it is noise.
9
+
10
+ `expressive.md` on this technique: "One diagonal or off-grid element is a decision; a page
11
+ full of diagonal elements is entropy."
12
+
13
+ ## When it earns its place / When it does not
14
+
15
+ Condition: the concept requires editorial tension — a sense that the content is not
16
+ contained, that something is pressing against its frame. Agency portfolios, magazine-format
17
+ editorial pages, brand campaign layouts with a provocative position. The asymmetry must
18
+ follow from the concept's governing metaphor; a diagonal that does not correspond to a
19
+ conceptual gesture is decoration at the scale of the layout.
20
+
21
+ `core/theory/layout.md` — grid section: "Breaking the grid is only meaningful when the
22
+ deviation is singular and intentional — a full-bleed image in an otherwise columned
23
+ editorial layout announces itself as a break precisely because everything else held the
24
+ column."
25
+
26
+ Condition against: data-heavy layouts where scanability is the primary requirement.
27
+ Tables, forms, pricing structures, documentation — any content the user reads in order or
28
+ scans for a specific item. The asymmetric element interrupts the scan path without
29
+ contributing information. `core/theory/layout.md` confirms: the F-pattern (text-heavy
30
+ pages) depends on a stable left edge; an off-grid element that cuts across that edge
31
+ disrupts the pattern.
32
+
33
+ Also: avoid when the headline is already carrying heavy typographic weight. A typographic
34
+ hero at 9rem and a diagonal element compete for the same attention budget.
35
+
36
+ ## Parameters
37
+
38
+ ```css
39
+ :root {
40
+ /* The column structure underlying the layout.
41
+ An asymmetric grid is still a grid — the off-grid element breaks one rule
42
+ in a system where all others hold. Typical: 12 columns, 24px gutters. */
43
+ --grid-columns: 12;
44
+ --grid-gutter: clamp(16px, 2vw, 24px);
45
+ --grid-margin: clamp(16px, 5vw, 80px);
46
+
47
+ /* Rotation of the off-grid element.
48
+ Small angles (3–7°) read as intentional tilt.
49
+ Larger angles (15°+) read as a different object category.
50
+ Keep this value from the board's reference study — do not pick it arbitrarily. */
51
+ --diagonal-rotate: 4deg;
52
+
53
+ /* The off-grid element's column span and offset.
54
+ Extending beyond the grid margin by a fixed amount produces the overlap signal. */
55
+ --diagonal-bleed: 5vw;
56
+ }
57
+ ```
58
+
59
+ ## Implementation
60
+
61
+ ```html
62
+ <!-- The grid provides the structure. The .diagonal element breaks it deliberately.
63
+ All other content holds the column. -->
64
+ <section class="asym-grid">
65
+ <div class="asym-grid__main">
66
+ <h2>Primary content holds the column.</h2>
67
+ <p>Body copy stays on the grid. The eye has a stable return point.</p>
68
+ </div>
69
+ <!-- This element is the single off-grid decision. -->
70
+ <div class="asym-grid__diagonal" aria-hidden="true">
71
+ <!-- Decorative content: a large number, a rotated label, a partial image. -->
72
+ <span class="diagonal-label">Editorial</span>
73
+ </div>
74
+ </section>
75
+ ```
76
+
77
+ ```css
78
+ .asym-grid {
79
+ display: grid;
80
+ grid-template-columns: repeat(var(--grid-columns), 1fr);
81
+ gap: var(--grid-gutter);
82
+ padding-inline: var(--grid-margin);
83
+ position: relative;
84
+ overflow: hidden; /* prevent bleed from creating horizontal scroll */
85
+ }
86
+
87
+ .asym-grid__main {
88
+ /* Main content occupies the left 7 columns. */
89
+ grid-column: 1 / 8;
90
+ /* Standard vertical rhythm — no transform applied to readable text. */
91
+ }
92
+
93
+ .asym-grid__diagonal {
94
+ /* Diagonal element: right side, overlapping main content zone. */
95
+ grid-column: 6 / 13;
96
+ /* Extend beyond the right margin */
97
+ margin-right: calc(-1 * var(--diagonal-bleed));
98
+ transform: rotate(var(--diagonal-rotate));
99
+ transform-origin: center left;
100
+ /* Decorative — sits behind or at same level as content depending on concept */
101
+ z-index: 0;
102
+ /* pointer-events off: the rotated element must not obstruct interactive elements */
103
+ pointer-events: none;
104
+ user-select: none;
105
+ }
106
+
107
+ .diagonal-label {
108
+ display: block;
109
+ font-size: clamp(4rem, 10vw, 8rem);
110
+ font-weight: var(--weight-display, 700);
111
+ opacity: 0.08; /* background element: presence without competition */
112
+ letter-spacing: -0.04em;
113
+ white-space: nowrap;
114
+ }
115
+ ```
116
+
117
+ ## Responsive behavior
118
+
119
+ **375px (mobile):** The 12-column grid collapses to a single column; the diagonal element
120
+ moves out of the content flow. On narrow viewports the off-grid element is not the concept
121
+ — it is a distraction from the content that must fit in a reduced space. Hide it entirely
122
+ below 600px unless it carries essential meaning.
123
+
124
+ ```css
125
+ @media (max-width: 600px) {
126
+ .asym-grid {
127
+ grid-template-columns: 1fr;
128
+ overflow: hidden;
129
+ }
130
+
131
+ .asym-grid__diagonal {
132
+ display: none; /* decorative element suppressed; content must stand without it */
133
+ }
134
+
135
+ .asym-grid__main {
136
+ grid-column: 1 / -1;
137
+ }
138
+ }
139
+ ```
140
+
141
+ **768px (tablet):** Reduce from 12 to 8 columns. The diagonal element may occupy the right
142
+ 4 columns; the main content the left 5. The rotation value stays constant — the angle
143
+ should not change with viewport width, as it reads as a different decision at each size.
144
+
145
+ ```css
146
+ @media (min-width: 601px) and (max-width: 1024px) {
147
+ .asym-grid {
148
+ grid-template-columns: repeat(8, 1fr);
149
+ }
150
+
151
+ .asym-grid__main {
152
+ grid-column: 1 / 6;
153
+ }
154
+
155
+ .asym-grid__diagonal {
156
+ grid-column: 4 / 9;
157
+ margin-right: calc(-1 * var(--diagonal-bleed));
158
+ }
159
+ }
160
+ ```
161
+
162
+ **1280px (desktop):** Full 12-column grid. The diagonal element's bleed beyond the margin
163
+ is now reading as a deliberate overextension — the composition's tension point. Ensure the
164
+ right edge of the diagonal element does not create horizontal scroll on the body; use
165
+ `overflow: hidden` on the section.
166
+
167
+ ## Do not combine with
168
+
169
+ **bento-grid.md** — both recipes alter grid structure at the layout level; two competing
170
+ spatial systems in one section produce incoherence rather than tension.
171
+
172
+ **editorial-index-labels.md** in the same section — both add large, low-opacity display
173
+ elements in the peripheral field. Combined, they produce visual noise at the edges while
174
+ the readable content fights for attention in the centre.
175
+
176
+ **sticky-sidebar-scroll.md** — the sidebar annotation system depends on a stable spatial
177
+ contract (main column + margin column); a diagonal element breaking that contract makes
178
+ the annotation impossible to anchor correctly.