asciify-engine 4.12.1 → 4.14.0

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.
@@ -0,0 +1,234 @@
1
+ # Applying Asciify: the house design
2
+
3
+ How asciify.org puts characters into a real interface. Read this before choosing a component or a background, then take the call from [components.md](components.md) and its options from [kit.md](kit.md). Every rule here is one you can check in the browser.
4
+
5
+ The site's claim is the brief: ASCII that belongs in your interface. Characters are a material the interface uses for a job (showing work, confirming an outcome, moving between states, setting one ground for a page), drawn in the page's own ink, inside its own shapes, at its own pace.
6
+
7
+ ## 1. Give the characters a job
8
+
9
+ Pick by what the field does for the person, not by how it looks.
10
+
11
+ | Job | Use |
12
+ | --- | --- |
13
+ | Show that work is running | `buttonLoading`, `statusOrb`, `thinking`, `streamText`, `skeleton`, `imageGenerate`, `pullRefresh` |
14
+ | Confirm or refuse an outcome | `statusOrb` success, warning or error; `slideToConfirm`; `codeInput` `verify()`; `beam` `success()` or `error()` |
15
+ | Show a value changing | `odometer`, `pill` counts, `blockChart` |
16
+ | Move between states or views | `tabs`, `pageDissolve`, `commandPalette`, `toastStack`, `disintegrate` |
17
+ | Answer a gesture | `likeBurst`, `swipeDeck`, `compare`, `accordion`, `dropzone` |
18
+ | Carry a brand moment or media | `decode`, `wordmark`, `asciiVideo`, `artPanel`, `pricingGlow`, `fieldBackground` `notFound` |
19
+ | Set the page's one ground | `fieldBackground` `hero`, or `background()` with a preset (section 9) |
20
+
21
+ Do not use characters:
22
+
23
+ - behind body copy, forms, tables, code, docs pages or navigation;
24
+ - on every card in a grid, or as a second ground on a page that has one;
25
+ - as the only carrier of a state: the label, the live region or the HTML still says it;
26
+ - for a loader with no real work behind it, or for decoration that never answers an event.
27
+
28
+ Test: hide the canvas. If the interface loses no feedback and the field was not the page's one ground, remove it. A ground earns its place by answering the interface. On asciify.org the hero field pulses when the command is copied:
29
+
30
+ ```ts
31
+ import { fieldBackground } from 'asciify-engine/kit';
32
+
33
+ const bg = fieldBackground(hero, { preset: 'hero', clear: heroCopy });
34
+ copyButton.addEventListener('click', () => bg.pulse(copyButton));
35
+ ```
36
+
37
+ ## 2. Ink: quiet by default, colour only for meaning
38
+
39
+ Inks come from theme tokens, defined for both schemes. The kit re-reads them when `prefers-color-scheme` changes and when `class` or `data-theme` changes on `<html>` or `<body>`, so define the dark values under both selectors. These are the engine's own fallbacks and the site's graphite:
40
+
41
+ ```css
42
+ :root {
43
+ --ascii-ink: #76746e; /* grounds, rings, quiet strokes */
44
+ --ascii-ink-strong: #1d1d1b; /* a component that is the focus */
45
+ --ascii-success: #17844a; --ascii-warning: #9a6700; --ascii-error: #c93636;
46
+ }
47
+ @media (prefers-color-scheme: dark) {
48
+ :root:not([data-theme="light"]) { --ascii-ink: #7a7872; --ascii-ink-strong: #d8d6cf; --ascii-success: #3ccf7e; --ascii-warning: #f5b83d; --ascii-error: #ff6b6b; }
49
+ }
50
+ :root[data-theme="dark"] { --ascii-ink: #7a7872; --ascii-ink-strong: #d8d6cf; --ascii-success: #3ccf7e; --ascii-warning: #f5b83d; --ascii-error: #ff6b6b; }
51
+ ```
52
+
53
+ Rules:
54
+
55
+ - **Default to graphite.** Leave `ink` unset: components pick `--ascii-ink` or `--ascii-ink-strong` and background presets use their graphite. On another brand, map these tokens to its muted text colour, never to its accent.
56
+ - **Colour means something.** Green, amber and red are for outcomes only, and come from `--ascii-success`, `--ascii-warning` and `--ascii-error`. The site's phosphor marks live states only and is never a button or text.
57
+ - **Palettes are deliberate.** `beam` `colorful`, `voiceOrb` `aurora` and `fieldBackground` `brand` are chosen palettes for one moment the user asked for, not a page default.
58
+ - **Never a new colour.** No hex that is not a token, no black canvas on a light page, no white glow on a coloured image.
59
+
60
+ ```ts
61
+ import { background, halftone, statusOrb, beam } from 'asciify-engine/kit';
62
+
63
+ background(pricing, { preset: halftone, clear: pricingCopy, ink: '--ascii-ink' }); // the token, re-read per theme
64
+ const orb = statusOrb(orbSlot); // strong ink at rest; outcomes read --ascii-success, --ascii-warning, --ascii-error
65
+ beam(planCard, { palette: 'mono', mode: 'pulse' }); // the quiet beam; 'colorful' only when asked for
66
+ orb.loading();
67
+ ```
68
+
69
+ ## 3. Text over a field: clear zone plus frosted veil
70
+
71
+ Text never sits on raw characters, and never in a box. Two layers do the work:
72
+
73
+ 1. **The clear zone.** Pass the text block as `clear`; the field fades out around it (`fieldBackground` in a soft ellipse, the GPU background presets in a feathered rectangle 32px out, canvas presets in a soft ellipse).
74
+ 2. **The veil.** A frosted, radially masked wash of the ground behind the text, so characters blur and fade toward the words instead of stopping at an edge. This is asciify.org's CSS:
75
+
76
+ ```css
77
+ .hero-copy { position: relative; isolation: isolate; }
78
+ .hero-copy::before {
79
+ content: ""; position: absolute; z-index: -1; inset: -36px -64px; pointer-events: none;
80
+ background: color-mix(in oklab, var(--bg) 62%, transparent);
81
+ -webkit-backdrop-filter: blur(9px) saturate(1.1); backdrop-filter: blur(9px) saturate(1.1);
82
+ -webkit-mask-image: radial-gradient(closest-side, #000 52%, rgb(0 0 0 / .6) 74%, transparent);
83
+ mask-image: radial-gradient(closest-side, #000 52%, rgb(0 0 0 / .6) 74%, transparent);
84
+ }
85
+ /* A smaller block, such as a card's title and button: a tighter veil from the surface it sits on. */
86
+ .card-copy::before { inset: -24px -40px; background: color-mix(in oklab, var(--surface-2) 58%, transparent); }
87
+ ```
88
+
89
+ - No solid panels, cards or hard-edged scrims behind headline copy. Buttons keep their own opaque fill.
90
+ - A header over a field: mask the field to about a quarter strength under the header strip, as the site does: `mask-image: linear-gradient(to bottom, rgb(0 0 0 / .26), rgb(0 0 0 / .26) 56px, #000 152px, #000 80%, transparent)`.
91
+ - **Contrast.** Body and lede text 4.5:1, display text of 24px and up 3:1, measured against the busiest frame under the text, in both themes. Fix a failure with the clear zone, the veil, a lower `opacity` or larger cells. Never dim, recolour or shadow the text to fix it.
92
+
93
+ ## 4. Density and cell size by surface
94
+
95
+ | Surface | Cell size | Characters | Site reference |
96
+ | --- | --- | --- | --- |
97
+ | Page hero ground | 10–12px, broad shapes | Sparse: `' .:+*#'` in the mono face | `fieldBackground` `hero` (10px), `drift` (11px), `halftone` (14px dot spacing) |
98
+ | Section band or card stage | 7–10px | Fuller ramp for images (`' .:-=+*#%@'`), quiet ramp for grounds | images 7px, character fields 10px, halftone 9px |
99
+ | Control: button, pill, tabs, code input, odometer | Leave it to the kit | The kit fits cells to the element's text | Use a monospaced label for `buttonLoading` |
100
+
101
+ - One density per surface: a stage does not mix a 7px image and a 12px ground.
102
+ - Nothing under 6px on a phone; the GPU grounds already enlarge their shapes under 760px.
103
+ - Tune with `look: { cellSize }` on a component or `cellSize` on `background()`, and only after looking at it at 1x and 2x.
104
+
105
+ ```ts
106
+ import { background, drift } from 'asciify-engine/kit';
107
+
108
+ const band = background(featureBand, { preset: drift, cellSize: 12, clear: bandCopy }); // a touch quieter than the default 11
109
+ band.update({ charset: ' .:+*#' }); // the hero's sparse ramp
110
+ ```
111
+
112
+ ## 5. Radii and containment
113
+
114
+ The kit clips every field to its host's computed corner radii, so the shape must be on the host.
115
+
116
+ - Put `border-radius` on the element you pass, from the project's tokens, never a literal. asciify.org: `--r-xs` 8px, `--r-inner` 18px, `--r-panel` 24px, `--r-dialog` 28px, and every control a pill (`--r-control: 999px`).
117
+ - Nest radii: an inner frame inset by `p` takes `calc(var(--r-panel) - p)`, as the site's video frame does (`calc(var(--r-panel) - 8px)` inside an 8px padding).
118
+ - Give `overflow: hidden` and a background to hosts the kit draws inside: `artPanel`, `imageGenerate`, `compare`, `accordion`, `dropzone`.
119
+ - Some components draw past their host on purpose: `voiceOrb` 8%, `codeInput` 16px, `toastStack` 24px, `likeBurst` 40px (`outset`), `swipeDeck` 56px (`outset`). Their container must have that much room, or bound them:
120
+
121
+ ```ts
122
+ import { likeBurst, swipeDeck } from 'asciify-engine/kit';
123
+
124
+ likeBurst(likeButton, { limit: postBody, outset: 24 }); // nothing above the post body, sparks inside the card's padding
125
+ swipeDeck(stack, { outset: 32 }); // the stage's own padding, so a fling thins out before its edge
126
+ ```
127
+
128
+ Check: with the page zoomed to 200%, no character lands outside a rounded corner or past a container edge.
129
+
130
+ ## 6. Motion
131
+
132
+ - **Interface: 280ms or less.** Anything a person waits on: tab indicator (`tabs` defaults to 280), panel open (`accordion` 280), tone change (`pill` 240), poster fade (280), view swap (`pageDissolve` 140 + 160).
133
+ - **Decorative: 800ms or less.** One-shot flourishes: `disintegrate` 620, `dropzone` develop 720, the like burst. `decode` defaults to 900, so pass `duration: 800` or less.
134
+ - **Ambient loops: slow.** Keep `speed` at or below the preset's own. Motion should be noticed on the second look, not the first.
135
+ - **Easing.** CSS around the field uses the house ease-out, `--ease: cubic-bezier(.23, 1, .32, 1)`, with 140ms for control colour changes. The kit's own motion runs on `cubic-bezier(.16, 1, .3, 1)`. No linear or ease-in entries; a spring only where the material earns a settle (a like, a badge).
136
+ - **Reduced motion.** The kit draws each state settled in one frame and each background as one still; poster fades become instant. Drop your own transitions too, and never hide a playing canvas with CSS instead.
137
+ - **Off screen.** The kit pauses fields off screen and in hidden tabs. Call `pause()` on fields inside closed overlays. Give video and any field that is the content, not a texture, a visible pause control.
138
+
139
+ ```ts
140
+ import { tabs, decode } from 'asciify-engine/kit';
141
+
142
+ tabs(tablist, { duration: 280, panels: 'attribute' }); // interface
143
+ decode(headline, { duration: 800, settle: 'text' }); // decorative, under the 800ms ceiling
144
+ ```
145
+
146
+ ```css
147
+ :root { --ease: cubic-bezier(.23, 1, .32, 1); --fast: 140ms; }
148
+ @media (prefers-reduced-motion: reduce) { .tab, .panel, .btn { transition: none; animation: none; } }
149
+ ```
150
+
151
+ ## 7. Performance
152
+
153
+ - **About 12 fields per page.** At most 12 kit fields hold a WebGL context at once (`setFieldBudget` changes it). asciify.org's home page runs 12. Past that, share: one `pill()` call over a list draws every pill with one field.
154
+ - **Lazy by default.** Fields mount within half a viewport and release beyond one. Use `lazy: false` only for the hero in the first viewport.
155
+ - **Posters.** Give every field above the fold, and every field that matters without WebGL, a still per theme, and swap it on theme change with `update({ poster })` rather than remounting.
156
+ - **Canvas presets cost CPU, not contexts.** Count them toward the page anyway; one animated ground per viewport.
157
+ - **Import by name.** Presets are separate exports, so import only those you mount.
158
+ - **Destroy on unmount.** In React, create the handle in an effect and return its `destroy`.
159
+
160
+ ```ts
161
+ import { fieldBackground, setFieldBudget } from 'asciify-engine/kit';
162
+
163
+ setFieldBudget(10); // a page with heavy media of its own
164
+ const dark = matchMedia('(prefers-color-scheme: dark)');
165
+ const still = (isDark: boolean) => `/stills/hero-${isDark ? 'dark' : 'light'}.webp`;
166
+ const ground = fieldBackground(hero, { preset: 'hero', clear: heroCopy, lazy: false, poster: still(dark.matches) });
167
+ dark.addEventListener('change', event => ground.update({ poster: still(event.matches) }));
168
+ ```
169
+
170
+ ## 8. Accessibility
171
+
172
+ - **Canvases are decoration.** Kit fields are `aria-hidden` and ignore the pointer. Only the canvases that are the content take a `label`: `fieldBackground` `notFound`, `asciiVideo`, `artPanel`.
173
+ - **Real text stays in the DOM.** `decode`, `wordmark` and `streamText` keep the actual words selectable and readable; `odometer` and `pill` counts add a hidden copy; `blockChart` adds a hidden table. Do not replace text with a field.
174
+ - **Status is announced.** Pass a live region to `buttonLoading` (`announce`) and `thinking` (`label`), write `statusOrb` outcomes to one yourself, and give `pullRefresh` its `messages`. `toastStack` adds its own `role="status"` region.
175
+ - **Keyboard and focus.** The kit wires keys for `tabs`, `commandPalette`, `slideToConfirm`, `compare`, `accordion`, `swipeDeck` and `codeInput`; keep them (`manage` stays true). Keep a Refresh button beside pull to refresh. A field never covers a focus ring: `beam` draws inside the border, so a 2px outline at 3px offset stays clear.
176
+
177
+ ```ts
178
+ import { statusOrb, buttonLoading } from 'asciify-engine/kit';
179
+
180
+ // <p id="publish-status" role="status" aria-live="polite"></p>
181
+ const orb = statusOrb(orbSlot, { ring: true });
182
+ const button = buttonLoading(publishButton, { announce: publishStatus });
183
+ publishButton.addEventListener('click', async () => {
184
+ orb.loading(); button.start('Publishing');
185
+ await save().then(() => { orb.success(); button.succeed('Published'); }, () => { orb.error(); button.fail('Retry'); });
186
+ });
187
+ ```
188
+
189
+ ## 9. Backgrounds
190
+
191
+ `background(element, { preset })` draws any of the 26 presets from asciify.org/backgrounds under the element's content. Choose by surface:
192
+
193
+ | Surface | Presets | Notes |
194
+ | --- | --- | --- |
195
+ | Product hero, pricing, docs landing, behind copy | `drift`, `halftone`, or `fieldBackground` `hero` | GPU grounds that clear around `clear`. `halftone` is the calmest; `fieldBackground` `hero` is asciify.org's own hero, with ripples and `pulse()`. |
196
+ | A brand or name moment | `wordField` (id `field`), `fieldBackground` `brand` | Your word in the field; keep copy off its centre. |
197
+ | Art-first hero with little copy | `fluid` | The original Asciify hero, 7px cells at 0.7 opacity. Not behind reading text. |
198
+ | Quiet texture: section bands, footers, empty states, side panels | `caustics`, `contour`, `facet`, `etch`, `moire`, `weave`, `veil`, `dune`, `silk` | Canvas, no cursor, slow. The safest behind text after `halftone`. |
199
+ | An interactive moment the visitor plays with | `wave`, `aurora`, `noise`, `stars`, `pulse`, `voidWell` | They answer the cursor. Keep them away from forms and body copy. |
200
+ | A themed subject the page is about | `rain`, `grid`, `morph`, `fire`, `dna`, `terrain`, `circuit` | Loud. Use only when the subject matches (DNA for a biotech page), with low opacity and copy beside, not over. |
201
+
202
+ Keep them calm:
203
+
204
+ - `speed` at or below the preset's default; `cursor: false` near forms and copy.
205
+ - On canvas presets behind text, `opacity` 0.5–0.8; GPU grounds already draw quietly.
206
+ - Always `clear` the copy and add the veil from section 3; on the /backgrounds page the copy also sits on the side the field fades toward.
207
+ - One background per page section, under one `isolation: isolate` host with its own background colour.
208
+ - `setPreset()` cross-fades in place for a picker; do not mount a second background.
209
+
210
+ ```ts
211
+ import { background, contour, caustics } from 'asciify-engine/kit';
212
+
213
+ const section = background(features, { preset: contour, clear: featuresCopy, opacity: 0.6, cursor: false });
214
+ themePicker.addEventListener('change', () => section.setPreset(caustics)); // keeps clear, opacity and cursor
215
+ ```
216
+
217
+ ```ts
218
+ import { background, drift } from 'asciify-engine/kit';
219
+
220
+ const bg = background(hero, { preset: drift, clear: heroCopy, poster: '/stills/drift-light.webp' });
221
+ signUpButton.addEventListener('click', () => bg.field?.pulse({ element: signUpButton, strength: 1.2 })); // GPU presets expose the field
222
+ ```
223
+
224
+ ## 10. Checklist before you finish
225
+
226
+ - [ ] Every field has a job from section 1; hiding it loses feedback, or it is the page's one ground.
227
+ - [ ] Ink comes from tokens in both themes; colour appears only for outcomes or a requested palette.
228
+ - [ ] Text over a field has `clear` plus the veil; 4.5:1 for body, 3:1 for display, at the busiest frame, light and dark.
229
+ - [ ] Cell size matches the surface; nothing under 6px on a 390px-wide phone.
230
+ - [ ] Every host carries its radius from tokens; nothing draws past a corner or container edge at 200% zoom.
231
+ - [ ] Interface motion ≤ 280ms, decorative ≤ 800ms, on the house ease-out; reduced motion shows settled stills.
232
+ - [ ] No more than about 12 fields per page; first-viewport fields have posters per theme; everything else is lazy.
233
+ - [ ] Canvases are `aria-hidden` unless labelled content; real text is in the DOM; status reaches a live region.
234
+ - [ ] Every handle is destroyed on unmount, and the page still works with WebGL off.