mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 14:38:59 +02:00
feat(design): carve UX doctrine + Pretext patterns into read-on-demand sections
design-html 57→49KB, design-shotgun 53→50KB. Sections wrap
{{UX_PRINCIPLES}} so scripts/resolvers/design.ts stays the source of
truth; the pretext-patterns STOP sits at the top of Step 3 so the read
provably precedes the Write.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
eae484c0f7
commit
4fce22d708
+19
-238
@@ -402,6 +402,20 @@ approximations. Computed layout via Pretext. Text reflows on resize, heights adj
|
||||
to content, cards size themselves, chat bubbles shrinkwrap, editorial spreads flow
|
||||
around obstacles.
|
||||
|
||||
---
|
||||
|
||||
## Section index — Read each section when its situation applies
|
||||
|
||||
This skill is a decision-tree skeleton. The steps below point to on-demand
|
||||
sections. Read a section in full before doing its step; do not work from memory.
|
||||
|
||||
| When | Read this section |
|
||||
|------|-------------------|
|
||||
| analyzing the design or making any layout/visual decision (Step 1 onward) — the UX-principles doctrine governs every design choice | `sections/doctrine.md` |
|
||||
| writing the finalized HTML in Step 3 — the Pretext wiring patterns and API cheatsheet are the required reference for all text-layout code | `sections/pretext-patterns.md` |
|
||||
|
||||
---
|
||||
|
||||
## DESIGN SETUP (run this check BEFORE any design mockup command)
|
||||
|
||||
```bash
|
||||
@@ -445,90 +459,8 @@ MUST be saved to `~/.gstack/projects/$SLUG/designs/`, NEVER to `.context/`,
|
||||
`docs/designs/`, `/tmp/`, or any project-local directory. Design artifacts are USER
|
||||
data, not project files. They persist across branches, conversations, and workspaces.
|
||||
|
||||
## UX Principles: How Users Actually Behave
|
||||
|
||||
These principles govern how real humans interact with interfaces. They are observed
|
||||
behavior, not preferences. Apply them before, during, and after every design decision.
|
||||
|
||||
### The Three Laws of Usability
|
||||
|
||||
1. **Don't make me think.** Every page should be self-evident. If a user stops
|
||||
to think "What do I click?" or "What does this mean?", the design has failed.
|
||||
Self-evident > self-explanatory > requires explanation.
|
||||
|
||||
2. **Clicks don't matter, thinking does.** Three mindless, unambiguous clicks
|
||||
beat one click that requires thought. Each step should feel like an obvious
|
||||
choice (animal, vegetable, or mineral), not a puzzle.
|
||||
|
||||
3. **Omit, then omit again.** Get rid of half the words on each page, then get
|
||||
rid of half of what's left. Happy talk (self-congratulatory text) must die.
|
||||
Instructions must die. If they need reading, the design has failed.
|
||||
|
||||
### How Users Actually Behave
|
||||
|
||||
- **Users scan, they don't read.** Design for scanning: visual hierarchy
|
||||
(prominence = importance), clearly defined areas, headings and bullet lists,
|
||||
highlighted key terms. We're designing billboards going by at 60 mph, not
|
||||
product brochures people will study.
|
||||
- **Users satisfice.** They pick the first reasonable option, not the best.
|
||||
Make the right choice the most visible choice.
|
||||
- **Users muddle through.** They don't figure out how things work. They wing
|
||||
it. If they accomplish their goal by accident, they won't seek the "right" way.
|
||||
Once they find something that works, no matter how badly, they stick to it.
|
||||
- **Users don't read instructions.** They dive in. Guidance must be brief,
|
||||
timely, and unavoidable, or it won't be seen.
|
||||
|
||||
### Billboard Design for Interfaces
|
||||
|
||||
- **Use conventions.** Logo top-left, nav top/left, search = magnifying glass.
|
||||
Don't innovate on navigation to be clever. Innovate when you KNOW you have a
|
||||
better idea, otherwise use conventions. Even across languages and cultures,
|
||||
web conventions let people identify the logo, nav, search, and main content.
|
||||
- **Visual hierarchy is everything.** Related things are visually grouped. Nested
|
||||
things are visually contained. More important = more prominent. If everything
|
||||
shouts, nothing is heard. Start with the assumption everything is visual noise,
|
||||
guilty until proven innocent.
|
||||
- **Make clickable things obviously clickable.** No relying on hover states for
|
||||
discoverability, especially on mobile where hover doesn't exist. Shape, location,
|
||||
and formatting (color, underlining) must signal clickability without interaction.
|
||||
- **Eliminate noise.** Three sources: too many things shouting for attention
|
||||
(shouting), things not organized logically (disorganization), and too much stuff
|
||||
(clutter). Fix noise by removal, not addition.
|
||||
- **Clarity trumps consistency.** If making something significantly clearer
|
||||
requires making it slightly inconsistent, choose clarity every time.
|
||||
|
||||
### Navigation as Wayfinding
|
||||
|
||||
Users on the web have no sense of scale, direction, or location. Navigation
|
||||
must always answer: What site is this? What page am I on? What are the major
|
||||
sections? What are my options at this level? Where am I? How can I search?
|
||||
|
||||
Persistent navigation on every page. Breadcrumbs for deep hierarchies.
|
||||
Current section visually indicated. The "trunk test": cover everything except
|
||||
the navigation. You should still know what site this is, what page you're on,
|
||||
and what the major sections are. If not, the navigation has failed.
|
||||
|
||||
### The Goodwill Reservoir
|
||||
|
||||
Users start with a reservoir of goodwill. Every friction point depletes it.
|
||||
|
||||
**Deplete faster:** Hiding info users want (pricing, contact, shipping). Punishing
|
||||
users for not doing things your way (formatting requirements on phone numbers).
|
||||
Asking for unnecessary information. Putting sizzle in their way (splash screens,
|
||||
forced tours, interstitials). Unprofessional or sloppy appearance.
|
||||
|
||||
**Replenish:** Know what users want to do and make it obvious. Tell them what they
|
||||
want to know upfront. Save them steps wherever possible. Make it easy to recover
|
||||
from errors. When in doubt, apologize.
|
||||
|
||||
### Mobile: Same Rules, Higher Stakes
|
||||
|
||||
All the above applies on mobile, just more so. Real estate is scarce, but never
|
||||
sacrifice usability for space savings. Affordances must be VISIBLE: no cursor
|
||||
means no hover-to-discover. Touch targets must be big enough (44px minimum).
|
||||
Flat design can strip away useful visual information that signals interactivity.
|
||||
Prioritize ruthlessly: things needed in a hurry go close at hand, everything
|
||||
else a few taps away with an obvious path to get there.
|
||||
> **STOP.** Before analyzing the design or making any layout/visual decision (Step 1 onward) — the UX-principles doctrine governs every design choice, Read `~/.claude/skills/gstack/design-html/sections/doctrine.md` and execute it
|
||||
> in full. Do not work from memory — that section is the source of truth for this step.
|
||||
|
||||
## SETUP (run this check BEFORE any browse command)
|
||||
|
||||
@@ -741,6 +673,9 @@ If no framework detected: default to vanilla HTML, no question needed.
|
||||
|
||||
## Step 3: Generate Pretext-Native HTML
|
||||
|
||||
> **STOP.** Before writing the finalized HTML in Step 3 — the Pretext wiring patterns and API cheatsheet are the required reference for all text-layout code, Read `~/.claude/skills/gstack/design-html/sections/pretext-patterns.md` and execute it
|
||||
> in full. Do not work from memory — that section is the source of truth for this step.
|
||||
|
||||
### Pretext Source Embedding
|
||||
|
||||
For **vanilla HTML output**, check for the vendored Pretext bundle:
|
||||
@@ -802,160 +737,6 @@ For framework output, save to:
|
||||
- Generic testimonial sections
|
||||
- Cookie-cutter hero sections with left-text right-image
|
||||
|
||||
### Pretext Wiring Patterns
|
||||
|
||||
Use these patterns based on the tier selected in Step 2. These are the correct
|
||||
Pretext API usage patterns. Follow them exactly.
|
||||
|
||||
**Pattern 1: Basic height computation (Simple layout, Card/grid)**
|
||||
```js
|
||||
import { prepare, layout } from './pretext-inline.js'
|
||||
// Or if inlined: const { prepare, layout } = window.Pretext
|
||||
|
||||
// 1. PREPARE — one-time, after fonts load
|
||||
await document.fonts.ready
|
||||
const elements = document.querySelectorAll('[data-pretext]')
|
||||
const prepared = new Map()
|
||||
|
||||
for (const el of elements) {
|
||||
const text = el.textContent
|
||||
const font = getComputedStyle(el).font
|
||||
prepared.set(el, prepare(text, font))
|
||||
}
|
||||
|
||||
// 2. LAYOUT — cheap, call on every resize
|
||||
function relayout() {
|
||||
for (const [el, handle] of prepared) {
|
||||
const { height } = layout(handle, el.clientWidth, parseFloat(getComputedStyle(el).lineHeight))
|
||||
el.style.height = `${height}px`
|
||||
}
|
||||
}
|
||||
|
||||
// 3. RESIZE-AWARE
|
||||
new ResizeObserver(() => relayout()).observe(document.body)
|
||||
relayout()
|
||||
|
||||
// 4. CONTENT-EDITABLE — re-prepare when text changes
|
||||
for (const el of elements) {
|
||||
if (el.contentEditable === 'true') {
|
||||
new MutationObserver(() => {
|
||||
const font = getComputedStyle(el).font
|
||||
prepared.set(el, prepare(el.textContent, font))
|
||||
relayout()
|
||||
}).observe(el, { characterData: true, subtree: true, childList: true })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 2: Shrinkwrap / tight-fit containers (Chat bubbles)**
|
||||
```js
|
||||
import { prepareWithSegments, walkLineRanges } from './pretext-inline.js'
|
||||
|
||||
// Find the tightest width that produces the same line count
|
||||
function shrinkwrap(text, font, maxWidth, lineHeight) {
|
||||
const segs = prepareWithSegments(text, font)
|
||||
let bestWidth = maxWidth
|
||||
walkLineRanges(segs, maxWidth, (lineCount, startIdx, endIdx) => {
|
||||
// walkLineRanges calls back with progressively narrower widths
|
||||
// The first call gives us the line count at maxWidth
|
||||
// We want the narrowest width that still produces this line count
|
||||
})
|
||||
// Binary search for tightest width with same line count
|
||||
const { lineCount: targetLines } = layout(prepare(text, font), maxWidth, lineHeight)
|
||||
let lo = 0, hi = maxWidth
|
||||
while (hi - lo > 1) {
|
||||
const mid = (lo + hi) / 2
|
||||
const { lineCount } = layout(prepare(text, font), mid, lineHeight)
|
||||
if (lineCount === targetLines) hi = mid
|
||||
else lo = mid
|
||||
}
|
||||
return hi
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 3: Text around obstacles (Editorial layout)**
|
||||
```js
|
||||
import { prepareWithSegments, layoutNextLine } from './pretext-inline.js'
|
||||
|
||||
function layoutAroundObstacles(text, font, containerWidth, lineHeight, obstacles) {
|
||||
const segs = prepareWithSegments(text, font)
|
||||
let state = null
|
||||
let y = 0
|
||||
const lines = []
|
||||
|
||||
while (true) {
|
||||
// Calculate available width at current y position, accounting for obstacles
|
||||
let availWidth = containerWidth
|
||||
for (const obs of obstacles) {
|
||||
if (y >= obs.top && y < obs.top + obs.height) {
|
||||
availWidth -= obs.width
|
||||
}
|
||||
}
|
||||
|
||||
const result = layoutNextLine(segs, state, availWidth, lineHeight)
|
||||
if (!result) break
|
||||
|
||||
lines.push({ text: result.text, width: result.width, x: 0, y })
|
||||
state = result.state
|
||||
y += lineHeight
|
||||
}
|
||||
|
||||
return { lines, totalHeight: y }
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 4: Full line-by-line rendering (Complex editorial)**
|
||||
```js
|
||||
import { prepareWithSegments, layoutWithLines } from './pretext-inline.js'
|
||||
|
||||
const segs = prepareWithSegments(text, font)
|
||||
const { lines, height } = layoutWithLines(segs, containerWidth, lineHeight)
|
||||
|
||||
// lines = [{ text, width, x, y }, ...]
|
||||
// Use for Canvas/SVG rendering or custom DOM positioning
|
||||
for (const line of lines) {
|
||||
const span = document.createElement('span')
|
||||
span.textContent = line.text
|
||||
span.style.position = 'absolute'
|
||||
span.style.left = `${line.x}px`
|
||||
span.style.top = `${line.y}px`
|
||||
container.appendChild(span)
|
||||
}
|
||||
```
|
||||
|
||||
### Pretext API Reference
|
||||
|
||||
```
|
||||
PRETEXT API CHEATSHEET:
|
||||
|
||||
prepare(text, font) → handle
|
||||
One-time text measurement. Call after document.fonts.ready.
|
||||
Font: CSS shorthand like '16px Inter' or 'bold 24px Georgia'.
|
||||
|
||||
layout(prepared, maxWidth, lineHeight) → { height, lineCount }
|
||||
Fast layout computation. Call on every resize. Sub-millisecond.
|
||||
|
||||
prepareWithSegments(text, font) → handle
|
||||
Like prepare() but enables line-level APIs below.
|
||||
|
||||
layoutWithLines(segs, maxWidth, lineHeight) → { lines: [{text, width, x, y}...], height }
|
||||
Full line-by-line breakdown. For Canvas/SVG rendering.
|
||||
|
||||
walkLineRanges(segs, maxWidth, onLine) → void
|
||||
Calls onLine(lineCount, startIdx, endIdx) for each possible layout.
|
||||
Find minimum width for N lines. For tight-fit containers.
|
||||
|
||||
layoutNextLine(segs, state, maxWidth, lineHeight) → { text, width, state } | null
|
||||
Iterator. Different maxWidth per line = text around obstacles.
|
||||
Pass null as initial state. Returns null when text is exhausted.
|
||||
|
||||
clearCache() → void
|
||||
Clears internal measurement caches. Use when cycling many fonts.
|
||||
|
||||
setLocale(locale?) → void
|
||||
Retargets word segmenter for future prepare() calls.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3.5: Live Reload Server
|
||||
|
||||
+9
-155
@@ -39,9 +39,15 @@ approximations. Computed layout via Pretext. Text reflows on resize, heights adj
|
||||
to content, cards size themselves, chat bubbles shrinkwrap, editorial spreads flow
|
||||
around obstacles.
|
||||
|
||||
---
|
||||
|
||||
{{SECTION_INDEX:design-html}}
|
||||
|
||||
---
|
||||
|
||||
{{DESIGN_SETUP}}
|
||||
|
||||
{{UX_PRINCIPLES}}
|
||||
{{SECTION:doctrine}}
|
||||
|
||||
{{BROWSE_SETUP}}
|
||||
|
||||
@@ -220,6 +226,8 @@ If no framework detected: default to vanilla HTML, no question needed.
|
||||
|
||||
## Step 3: Generate Pretext-Native HTML
|
||||
|
||||
{{SECTION:pretext-patterns}}
|
||||
|
||||
### Pretext Source Embedding
|
||||
|
||||
For **vanilla HTML output**, check for the vendored Pretext bundle:
|
||||
@@ -281,160 +289,6 @@ For framework output, save to:
|
||||
- Generic testimonial sections
|
||||
- Cookie-cutter hero sections with left-text right-image
|
||||
|
||||
### Pretext Wiring Patterns
|
||||
|
||||
Use these patterns based on the tier selected in Step 2. These are the correct
|
||||
Pretext API usage patterns. Follow them exactly.
|
||||
|
||||
**Pattern 1: Basic height computation (Simple layout, Card/grid)**
|
||||
```js
|
||||
import { prepare, layout } from './pretext-inline.js'
|
||||
// Or if inlined: const { prepare, layout } = window.Pretext
|
||||
|
||||
// 1. PREPARE — one-time, after fonts load
|
||||
await document.fonts.ready
|
||||
const elements = document.querySelectorAll('[data-pretext]')
|
||||
const prepared = new Map()
|
||||
|
||||
for (const el of elements) {
|
||||
const text = el.textContent
|
||||
const font = getComputedStyle(el).font
|
||||
prepared.set(el, prepare(text, font))
|
||||
}
|
||||
|
||||
// 2. LAYOUT — cheap, call on every resize
|
||||
function relayout() {
|
||||
for (const [el, handle] of prepared) {
|
||||
const { height } = layout(handle, el.clientWidth, parseFloat(getComputedStyle(el).lineHeight))
|
||||
el.style.height = `${height}px`
|
||||
}
|
||||
}
|
||||
|
||||
// 3. RESIZE-AWARE
|
||||
new ResizeObserver(() => relayout()).observe(document.body)
|
||||
relayout()
|
||||
|
||||
// 4. CONTENT-EDITABLE — re-prepare when text changes
|
||||
for (const el of elements) {
|
||||
if (el.contentEditable === 'true') {
|
||||
new MutationObserver(() => {
|
||||
const font = getComputedStyle(el).font
|
||||
prepared.set(el, prepare(el.textContent, font))
|
||||
relayout()
|
||||
}).observe(el, { characterData: true, subtree: true, childList: true })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 2: Shrinkwrap / tight-fit containers (Chat bubbles)**
|
||||
```js
|
||||
import { prepareWithSegments, walkLineRanges } from './pretext-inline.js'
|
||||
|
||||
// Find the tightest width that produces the same line count
|
||||
function shrinkwrap(text, font, maxWidth, lineHeight) {
|
||||
const segs = prepareWithSegments(text, font)
|
||||
let bestWidth = maxWidth
|
||||
walkLineRanges(segs, maxWidth, (lineCount, startIdx, endIdx) => {
|
||||
// walkLineRanges calls back with progressively narrower widths
|
||||
// The first call gives us the line count at maxWidth
|
||||
// We want the narrowest width that still produces this line count
|
||||
})
|
||||
// Binary search for tightest width with same line count
|
||||
const { lineCount: targetLines } = layout(prepare(text, font), maxWidth, lineHeight)
|
||||
let lo = 0, hi = maxWidth
|
||||
while (hi - lo > 1) {
|
||||
const mid = (lo + hi) / 2
|
||||
const { lineCount } = layout(prepare(text, font), mid, lineHeight)
|
||||
if (lineCount === targetLines) hi = mid
|
||||
else lo = mid
|
||||
}
|
||||
return hi
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 3: Text around obstacles (Editorial layout)**
|
||||
```js
|
||||
import { prepareWithSegments, layoutNextLine } from './pretext-inline.js'
|
||||
|
||||
function layoutAroundObstacles(text, font, containerWidth, lineHeight, obstacles) {
|
||||
const segs = prepareWithSegments(text, font)
|
||||
let state = null
|
||||
let y = 0
|
||||
const lines = []
|
||||
|
||||
while (true) {
|
||||
// Calculate available width at current y position, accounting for obstacles
|
||||
let availWidth = containerWidth
|
||||
for (const obs of obstacles) {
|
||||
if (y >= obs.top && y < obs.top + obs.height) {
|
||||
availWidth -= obs.width
|
||||
}
|
||||
}
|
||||
|
||||
const result = layoutNextLine(segs, state, availWidth, lineHeight)
|
||||
if (!result) break
|
||||
|
||||
lines.push({ text: result.text, width: result.width, x: 0, y })
|
||||
state = result.state
|
||||
y += lineHeight
|
||||
}
|
||||
|
||||
return { lines, totalHeight: y }
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 4: Full line-by-line rendering (Complex editorial)**
|
||||
```js
|
||||
import { prepareWithSegments, layoutWithLines } from './pretext-inline.js'
|
||||
|
||||
const segs = prepareWithSegments(text, font)
|
||||
const { lines, height } = layoutWithLines(segs, containerWidth, lineHeight)
|
||||
|
||||
// lines = [{ text, width, x, y }, ...]
|
||||
// Use for Canvas/SVG rendering or custom DOM positioning
|
||||
for (const line of lines) {
|
||||
const span = document.createElement('span')
|
||||
span.textContent = line.text
|
||||
span.style.position = 'absolute'
|
||||
span.style.left = `${line.x}px`
|
||||
span.style.top = `${line.y}px`
|
||||
container.appendChild(span)
|
||||
}
|
||||
```
|
||||
|
||||
### Pretext API Reference
|
||||
|
||||
```
|
||||
PRETEXT API CHEATSHEET:
|
||||
|
||||
prepare(text, font) → handle
|
||||
One-time text measurement. Call after document.fonts.ready.
|
||||
Font: CSS shorthand like '16px Inter' or 'bold 24px Georgia'.
|
||||
|
||||
layout(prepared, maxWidth, lineHeight) → { height, lineCount }
|
||||
Fast layout computation. Call on every resize. Sub-millisecond.
|
||||
|
||||
prepareWithSegments(text, font) → handle
|
||||
Like prepare() but enables line-level APIs below.
|
||||
|
||||
layoutWithLines(segs, maxWidth, lineHeight) → { lines: [{text, width, x, y}...], height }
|
||||
Full line-by-line breakdown. For Canvas/SVG rendering.
|
||||
|
||||
walkLineRanges(segs, maxWidth, onLine) → void
|
||||
Calls onLine(lineCount, startIdx, endIdx) for each possible layout.
|
||||
Find minimum width for N lines. For tight-fit containers.
|
||||
|
||||
layoutNextLine(segs, state, maxWidth, lineHeight) → { text, width, state } | null
|
||||
Iterator. Different maxWidth per line = text around obstacles.
|
||||
Pass null as initial state. Returns null when text is exhausted.
|
||||
|
||||
clearCache() → void
|
||||
Clears internal measurement caches. Use when cycling many fonts.
|
||||
|
||||
setLocale(locale?) → void
|
||||
Retargets word segmenter for future prepare() calls.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3.5: Live Reload Server
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- AUTO-GENERATED from doctrine.md.tmpl — do not edit directly -->
|
||||
<!-- Regenerate: bun run gen:skill-docs -->
|
||||
## UX Principles: How Users Actually Behave
|
||||
|
||||
These principles govern how real humans interact with interfaces. They are observed
|
||||
behavior, not preferences. Apply them before, during, and after every design decision.
|
||||
|
||||
### The Three Laws of Usability
|
||||
|
||||
1. **Don't make me think.** Every page should be self-evident. If a user stops
|
||||
to think "What do I click?" or "What does this mean?", the design has failed.
|
||||
Self-evident > self-explanatory > requires explanation.
|
||||
|
||||
2. **Clicks don't matter, thinking does.** Three mindless, unambiguous clicks
|
||||
beat one click that requires thought. Each step should feel like an obvious
|
||||
choice (animal, vegetable, or mineral), not a puzzle.
|
||||
|
||||
3. **Omit, then omit again.** Get rid of half the words on each page, then get
|
||||
rid of half of what's left. Happy talk (self-congratulatory text) must die.
|
||||
Instructions must die. If they need reading, the design has failed.
|
||||
|
||||
### How Users Actually Behave
|
||||
|
||||
- **Users scan, they don't read.** Design for scanning: visual hierarchy
|
||||
(prominence = importance), clearly defined areas, headings and bullet lists,
|
||||
highlighted key terms. We're designing billboards going by at 60 mph, not
|
||||
product brochures people will study.
|
||||
- **Users satisfice.** They pick the first reasonable option, not the best.
|
||||
Make the right choice the most visible choice.
|
||||
- **Users muddle through.** They don't figure out how things work. They wing
|
||||
it. If they accomplish their goal by accident, they won't seek the "right" way.
|
||||
Once they find something that works, no matter how badly, they stick to it.
|
||||
- **Users don't read instructions.** They dive in. Guidance must be brief,
|
||||
timely, and unavoidable, or it won't be seen.
|
||||
|
||||
### Billboard Design for Interfaces
|
||||
|
||||
- **Use conventions.** Logo top-left, nav top/left, search = magnifying glass.
|
||||
Don't innovate on navigation to be clever. Innovate when you KNOW you have a
|
||||
better idea, otherwise use conventions. Even across languages and cultures,
|
||||
web conventions let people identify the logo, nav, search, and main content.
|
||||
- **Visual hierarchy is everything.** Related things are visually grouped. Nested
|
||||
things are visually contained. More important = more prominent. If everything
|
||||
shouts, nothing is heard. Start with the assumption everything is visual noise,
|
||||
guilty until proven innocent.
|
||||
- **Make clickable things obviously clickable.** No relying on hover states for
|
||||
discoverability, especially on mobile where hover doesn't exist. Shape, location,
|
||||
and formatting (color, underlining) must signal clickability without interaction.
|
||||
- **Eliminate noise.** Three sources: too many things shouting for attention
|
||||
(shouting), things not organized logically (disorganization), and too much stuff
|
||||
(clutter). Fix noise by removal, not addition.
|
||||
- **Clarity trumps consistency.** If making something significantly clearer
|
||||
requires making it slightly inconsistent, choose clarity every time.
|
||||
|
||||
### Navigation as Wayfinding
|
||||
|
||||
Users on the web have no sense of scale, direction, or location. Navigation
|
||||
must always answer: What site is this? What page am I on? What are the major
|
||||
sections? What are my options at this level? Where am I? How can I search?
|
||||
|
||||
Persistent navigation on every page. Breadcrumbs for deep hierarchies.
|
||||
Current section visually indicated. The "trunk test": cover everything except
|
||||
the navigation. You should still know what site this is, what page you're on,
|
||||
and what the major sections are. If not, the navigation has failed.
|
||||
|
||||
### The Goodwill Reservoir
|
||||
|
||||
Users start with a reservoir of goodwill. Every friction point depletes it.
|
||||
|
||||
**Deplete faster:** Hiding info users want (pricing, contact, shipping). Punishing
|
||||
users for not doing things your way (formatting requirements on phone numbers).
|
||||
Asking for unnecessary information. Putting sizzle in their way (splash screens,
|
||||
forced tours, interstitials). Unprofessional or sloppy appearance.
|
||||
|
||||
**Replenish:** Know what users want to do and make it obvious. Tell them what they
|
||||
want to know upfront. Save them steps wherever possible. Make it easy to recover
|
||||
from errors. When in doubt, apologize.
|
||||
|
||||
### Mobile: Same Rules, Higher Stakes
|
||||
|
||||
All the above applies on mobile, just more so. Real estate is scarce, but never
|
||||
sacrifice usability for space savings. Affordances must be VISIBLE: no cursor
|
||||
means no hover-to-discover. Touch targets must be big enough (44px minimum).
|
||||
Flat design can strip away useful visual information that signals interactivity.
|
||||
Prioritize ruthlessly: things needed in a hurry go close at hand, everything
|
||||
else a few taps away with an obvious path to get there.
|
||||
@@ -0,0 +1 @@
|
||||
{{UX_PRINCIPLES}}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://gstack.dev/schemas/section-manifest.json",
|
||||
"skill": "design-html",
|
||||
"version": 1,
|
||||
"note": "PASSIVE registry (v2 plan T9 / CM2). Fields are IDs, file paths, human titles, and human-readable trigger text ONLY. The skeleton's decision-tree prose is the ONLY place that decides WHEN to read a section; required-reads live in the E2E fixtures. No machine predicate here — see docs/designs/v2_PLAN.md:663.",
|
||||
"sections": [
|
||||
{
|
||||
"id": "doctrine",
|
||||
"file": "doctrine.md",
|
||||
"title": "UX principles doctrine (how users actually behave)",
|
||||
"trigger": "analyzing the design or making any layout/visual decision (Step 1 onward) — the UX-principles doctrine governs every design choice"
|
||||
},
|
||||
{
|
||||
"id": "pretext-patterns",
|
||||
"file": "pretext-patterns.md",
|
||||
"title": "Pretext wiring patterns + API reference",
|
||||
"trigger": "writing the finalized HTML in Step 3 — the Pretext wiring patterns and API cheatsheet are the required reference for all text-layout code"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<!-- AUTO-GENERATED from pretext-patterns.md.tmpl — do not edit directly -->
|
||||
<!-- Regenerate: bun run gen:skill-docs -->
|
||||
### Pretext Wiring Patterns
|
||||
|
||||
Use these patterns based on the tier selected in Step 2. These are the correct
|
||||
Pretext API usage patterns. Follow them exactly.
|
||||
|
||||
**Pattern 1: Basic height computation (Simple layout, Card/grid)**
|
||||
```js
|
||||
import { prepare, layout } from './pretext-inline.js'
|
||||
// Or if inlined: const { prepare, layout } = window.Pretext
|
||||
|
||||
// 1. PREPARE — one-time, after fonts load
|
||||
await document.fonts.ready
|
||||
const elements = document.querySelectorAll('[data-pretext]')
|
||||
const prepared = new Map()
|
||||
|
||||
for (const el of elements) {
|
||||
const text = el.textContent
|
||||
const font = getComputedStyle(el).font
|
||||
prepared.set(el, prepare(text, font))
|
||||
}
|
||||
|
||||
// 2. LAYOUT — cheap, call on every resize
|
||||
function relayout() {
|
||||
for (const [el, handle] of prepared) {
|
||||
const { height } = layout(handle, el.clientWidth, parseFloat(getComputedStyle(el).lineHeight))
|
||||
el.style.height = `${height}px`
|
||||
}
|
||||
}
|
||||
|
||||
// 3. RESIZE-AWARE
|
||||
new ResizeObserver(() => relayout()).observe(document.body)
|
||||
relayout()
|
||||
|
||||
// 4. CONTENT-EDITABLE — re-prepare when text changes
|
||||
for (const el of elements) {
|
||||
if (el.contentEditable === 'true') {
|
||||
new MutationObserver(() => {
|
||||
const font = getComputedStyle(el).font
|
||||
prepared.set(el, prepare(el.textContent, font))
|
||||
relayout()
|
||||
}).observe(el, { characterData: true, subtree: true, childList: true })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 2: Shrinkwrap / tight-fit containers (Chat bubbles)**
|
||||
```js
|
||||
import { prepareWithSegments, walkLineRanges } from './pretext-inline.js'
|
||||
|
||||
// Find the tightest width that produces the same line count
|
||||
function shrinkwrap(text, font, maxWidth, lineHeight) {
|
||||
const segs = prepareWithSegments(text, font)
|
||||
let bestWidth = maxWidth
|
||||
walkLineRanges(segs, maxWidth, (lineCount, startIdx, endIdx) => {
|
||||
// walkLineRanges calls back with progressively narrower widths
|
||||
// The first call gives us the line count at maxWidth
|
||||
// We want the narrowest width that still produces this line count
|
||||
})
|
||||
// Binary search for tightest width with same line count
|
||||
const { lineCount: targetLines } = layout(prepare(text, font), maxWidth, lineHeight)
|
||||
let lo = 0, hi = maxWidth
|
||||
while (hi - lo > 1) {
|
||||
const mid = (lo + hi) / 2
|
||||
const { lineCount } = layout(prepare(text, font), mid, lineHeight)
|
||||
if (lineCount === targetLines) hi = mid
|
||||
else lo = mid
|
||||
}
|
||||
return hi
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 3: Text around obstacles (Editorial layout)**
|
||||
```js
|
||||
import { prepareWithSegments, layoutNextLine } from './pretext-inline.js'
|
||||
|
||||
function layoutAroundObstacles(text, font, containerWidth, lineHeight, obstacles) {
|
||||
const segs = prepareWithSegments(text, font)
|
||||
let state = null
|
||||
let y = 0
|
||||
const lines = []
|
||||
|
||||
while (true) {
|
||||
// Calculate available width at current y position, accounting for obstacles
|
||||
let availWidth = containerWidth
|
||||
for (const obs of obstacles) {
|
||||
if (y >= obs.top && y < obs.top + obs.height) {
|
||||
availWidth -= obs.width
|
||||
}
|
||||
}
|
||||
|
||||
const result = layoutNextLine(segs, state, availWidth, lineHeight)
|
||||
if (!result) break
|
||||
|
||||
lines.push({ text: result.text, width: result.width, x: 0, y })
|
||||
state = result.state
|
||||
y += lineHeight
|
||||
}
|
||||
|
||||
return { lines, totalHeight: y }
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 4: Full line-by-line rendering (Complex editorial)**
|
||||
```js
|
||||
import { prepareWithSegments, layoutWithLines } from './pretext-inline.js'
|
||||
|
||||
const segs = prepareWithSegments(text, font)
|
||||
const { lines, height } = layoutWithLines(segs, containerWidth, lineHeight)
|
||||
|
||||
// lines = [{ text, width, x, y }, ...]
|
||||
// Use for Canvas/SVG rendering or custom DOM positioning
|
||||
for (const line of lines) {
|
||||
const span = document.createElement('span')
|
||||
span.textContent = line.text
|
||||
span.style.position = 'absolute'
|
||||
span.style.left = `${line.x}px`
|
||||
span.style.top = `${line.y}px`
|
||||
container.appendChild(span)
|
||||
}
|
||||
```
|
||||
|
||||
### Pretext API Reference
|
||||
|
||||
```
|
||||
PRETEXT API CHEATSHEET:
|
||||
|
||||
prepare(text, font) → handle
|
||||
One-time text measurement. Call after document.fonts.ready.
|
||||
Font: CSS shorthand like '16px Inter' or 'bold 24px Georgia'.
|
||||
|
||||
layout(prepared, maxWidth, lineHeight) → { height, lineCount }
|
||||
Fast layout computation. Call on every resize. Sub-millisecond.
|
||||
|
||||
prepareWithSegments(text, font) → handle
|
||||
Like prepare() but enables line-level APIs below.
|
||||
|
||||
layoutWithLines(segs, maxWidth, lineHeight) → { lines: [{text, width, x, y}...], height }
|
||||
Full line-by-line breakdown. For Canvas/SVG rendering.
|
||||
|
||||
walkLineRanges(segs, maxWidth, onLine) → void
|
||||
Calls onLine(lineCount, startIdx, endIdx) for each possible layout.
|
||||
Find minimum width for N lines. For tight-fit containers.
|
||||
|
||||
layoutNextLine(segs, state, maxWidth, lineHeight) → { text, width, state } | null
|
||||
Iterator. Different maxWidth per line = text around obstacles.
|
||||
Pass null as initial state. Returns null when text is exhausted.
|
||||
|
||||
clearCache() → void
|
||||
Clears internal measurement caches. Use when cycling many fonts.
|
||||
|
||||
setLocale(locale?) → void
|
||||
Retargets word segmenter for future prepare() calls.
|
||||
```
|
||||
@@ -0,0 +1,153 @@
|
||||
### Pretext Wiring Patterns
|
||||
|
||||
Use these patterns based on the tier selected in Step 2. These are the correct
|
||||
Pretext API usage patterns. Follow them exactly.
|
||||
|
||||
**Pattern 1: Basic height computation (Simple layout, Card/grid)**
|
||||
```js
|
||||
import { prepare, layout } from './pretext-inline.js'
|
||||
// Or if inlined: const { prepare, layout } = window.Pretext
|
||||
|
||||
// 1. PREPARE — one-time, after fonts load
|
||||
await document.fonts.ready
|
||||
const elements = document.querySelectorAll('[data-pretext]')
|
||||
const prepared = new Map()
|
||||
|
||||
for (const el of elements) {
|
||||
const text = el.textContent
|
||||
const font = getComputedStyle(el).font
|
||||
prepared.set(el, prepare(text, font))
|
||||
}
|
||||
|
||||
// 2. LAYOUT — cheap, call on every resize
|
||||
function relayout() {
|
||||
for (const [el, handle] of prepared) {
|
||||
const { height } = layout(handle, el.clientWidth, parseFloat(getComputedStyle(el).lineHeight))
|
||||
el.style.height = `${height}px`
|
||||
}
|
||||
}
|
||||
|
||||
// 3. RESIZE-AWARE
|
||||
new ResizeObserver(() => relayout()).observe(document.body)
|
||||
relayout()
|
||||
|
||||
// 4. CONTENT-EDITABLE — re-prepare when text changes
|
||||
for (const el of elements) {
|
||||
if (el.contentEditable === 'true') {
|
||||
new MutationObserver(() => {
|
||||
const font = getComputedStyle(el).font
|
||||
prepared.set(el, prepare(el.textContent, font))
|
||||
relayout()
|
||||
}).observe(el, { characterData: true, subtree: true, childList: true })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 2: Shrinkwrap / tight-fit containers (Chat bubbles)**
|
||||
```js
|
||||
import { prepareWithSegments, walkLineRanges } from './pretext-inline.js'
|
||||
|
||||
// Find the tightest width that produces the same line count
|
||||
function shrinkwrap(text, font, maxWidth, lineHeight) {
|
||||
const segs = prepareWithSegments(text, font)
|
||||
let bestWidth = maxWidth
|
||||
walkLineRanges(segs, maxWidth, (lineCount, startIdx, endIdx) => {
|
||||
// walkLineRanges calls back with progressively narrower widths
|
||||
// The first call gives us the line count at maxWidth
|
||||
// We want the narrowest width that still produces this line count
|
||||
})
|
||||
// Binary search for tightest width with same line count
|
||||
const { lineCount: targetLines } = layout(prepare(text, font), maxWidth, lineHeight)
|
||||
let lo = 0, hi = maxWidth
|
||||
while (hi - lo > 1) {
|
||||
const mid = (lo + hi) / 2
|
||||
const { lineCount } = layout(prepare(text, font), mid, lineHeight)
|
||||
if (lineCount === targetLines) hi = mid
|
||||
else lo = mid
|
||||
}
|
||||
return hi
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 3: Text around obstacles (Editorial layout)**
|
||||
```js
|
||||
import { prepareWithSegments, layoutNextLine } from './pretext-inline.js'
|
||||
|
||||
function layoutAroundObstacles(text, font, containerWidth, lineHeight, obstacles) {
|
||||
const segs = prepareWithSegments(text, font)
|
||||
let state = null
|
||||
let y = 0
|
||||
const lines = []
|
||||
|
||||
while (true) {
|
||||
// Calculate available width at current y position, accounting for obstacles
|
||||
let availWidth = containerWidth
|
||||
for (const obs of obstacles) {
|
||||
if (y >= obs.top && y < obs.top + obs.height) {
|
||||
availWidth -= obs.width
|
||||
}
|
||||
}
|
||||
|
||||
const result = layoutNextLine(segs, state, availWidth, lineHeight)
|
||||
if (!result) break
|
||||
|
||||
lines.push({ text: result.text, width: result.width, x: 0, y })
|
||||
state = result.state
|
||||
y += lineHeight
|
||||
}
|
||||
|
||||
return { lines, totalHeight: y }
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 4: Full line-by-line rendering (Complex editorial)**
|
||||
```js
|
||||
import { prepareWithSegments, layoutWithLines } from './pretext-inline.js'
|
||||
|
||||
const segs = prepareWithSegments(text, font)
|
||||
const { lines, height } = layoutWithLines(segs, containerWidth, lineHeight)
|
||||
|
||||
// lines = [{ text, width, x, y }, ...]
|
||||
// Use for Canvas/SVG rendering or custom DOM positioning
|
||||
for (const line of lines) {
|
||||
const span = document.createElement('span')
|
||||
span.textContent = line.text
|
||||
span.style.position = 'absolute'
|
||||
span.style.left = `${line.x}px`
|
||||
span.style.top = `${line.y}px`
|
||||
container.appendChild(span)
|
||||
}
|
||||
```
|
||||
|
||||
### Pretext API Reference
|
||||
|
||||
```
|
||||
PRETEXT API CHEATSHEET:
|
||||
|
||||
prepare(text, font) → handle
|
||||
One-time text measurement. Call after document.fonts.ready.
|
||||
Font: CSS shorthand like '16px Inter' or 'bold 24px Georgia'.
|
||||
|
||||
layout(prepared, maxWidth, lineHeight) → { height, lineCount }
|
||||
Fast layout computation. Call on every resize. Sub-millisecond.
|
||||
|
||||
prepareWithSegments(text, font) → handle
|
||||
Like prepare() but enables line-level APIs below.
|
||||
|
||||
layoutWithLines(segs, maxWidth, lineHeight) → { lines: [{text, width, x, y}...], height }
|
||||
Full line-by-line breakdown. For Canvas/SVG rendering.
|
||||
|
||||
walkLineRanges(segs, maxWidth, onLine) → void
|
||||
Calls onLine(lineCount, startIdx, endIdx) for each possible layout.
|
||||
Find minimum width for N lines. For tight-fit containers.
|
||||
|
||||
layoutNextLine(segs, state, maxWidth, lineHeight) → { text, width, state } | null
|
||||
Iterator. Different maxWidth per line = text around obstacles.
|
||||
Pass null as initial state. Returns null when text is exhausted.
|
||||
|
||||
clearCache() → void
|
||||
Clears internal measurement caches. Use when cycling many fonts.
|
||||
|
||||
setLocale(locale?) → void
|
||||
Retargets word segmenter for future prepare() calls.
|
||||
```
|
||||
Reference in New Issue
Block a user