mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-11 23:49:01 +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
@@ -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.
|
||||
```
|
||||
Reference in New Issue
Block a user