@cleocode/animations 2026.5.29 → 2026.5.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,77 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@cleocode/animations` are documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+ Versions follow the **monorepo-wide CalVer** (`YYYY.M.PATCH`) — the version
7
+ shipped on npm is set by the git tag at release time via
8
+ `.github/workflows/release.yml`. **Do not bump `package.json` by hand.**
9
+
10
+ ## [2026.5.29] — initial release
11
+
12
+ Initial published version. The 2026.5.29 line on npm has three patches
13
+ because trusted-publisher setup required claiming the package name manually
14
+ before the OIDC pipeline could take over; subsequent `2026.5.30` and
15
+ `2026.5.31` patches were also hand-published while iterating on the API
16
+ surface. From the next monorepo release tag onward, the central pipeline
17
+ drives every version bump.
18
+
19
+ ### Added
20
+ - Initial port of [`gunnargray-dev/unicode-animations`](https://github.com/gunnargray-dev/unicode-animations) (MIT)
21
+ into `packages/animations/` as a workspace package.
22
+ - 18 braille spinner animations: 3 hand-curated single-char classics
23
+ (`braille`, `braillewave`, `dna`) + 15 procedurally generated grid
24
+ animations (`scan`, `rain`, `pulse`, `helix`, `cascade`, `orbit`, …).
25
+ - Grid utilities `gridToBraille(grid)` and `makeGrid(rows, cols)` for
26
+ composing custom braille animations.
27
+ - **Canon spinner aliases** — 9 workshop-vocabulary names pointing at the
28
+ same `Spinner` objects as the generic registry (aliases, not copies):
29
+ `looming → helix`, `weaving → braillewave`, `heartbeat → breathe`,
30
+ `awakening → pulse`, `sweeping → scan`, `watching → orbit`,
31
+ `cascade → cascade`, `tapestry → waverows`, `refinery → columns`.
32
+ Exposed as `canonSpinners`, `CANON_TO_GENERIC`, `resolveSpinner(name)`.
33
+ - **`AnimateContext`** — pure-data render gate consumed by every primitive.
34
+ Disables animations when `format !== 'human'`, `quiet`, `!isTTY`, or
35
+ `NO_COLOR`. Carries `reason` for diagnostics. `SILENT_CONTEXT` exported
36
+ as a frozen always-disabled context.
37
+ - **Progress bar primitives** — three canon styles via
38
+ `renderProgressBar(style, ratio, width)`:
39
+ - `tapestry` — coarse Unicode blocks (`░▒▓█`)
40
+ - `cascade` — 1/8 gradient steps (`▏▎▍▌▋▊▉█`)
41
+ - `refinery` — braille block stages (`⠀⡀⡄⡆⡇⣇⣧⣷⣿`)
42
+ - **Spark primitives** — 4 one-shot canon flares: `awaken`, `sweep`,
43
+ `cascade`, `weave`. Helper `sparkDurationMs(name)`.
44
+ - **`createSpinnerHandle(ctx, name, label, options?)`** — the canonical
45
+ owner of `\r` writes for this package. Wraps a `Spinner` with a managed
46
+ timer, cursor hide/show, exit-handler restoration, idempotent `start()` /
47
+ `stop()`, and `update(label)`. Returns a frozen no-op handle when
48
+ `AnimateContext.enabled` is `false`. Process-level `exit` / `SIGINT` /
49
+ `SIGTERM` listeners are installed exactly **once per process** via a
50
+ shared module-scoped registry, so N concurrent handles add 1 listener
51
+ per signal (not N) — well clear of Node's default 10-listener warning
52
+ threshold even with heavy orchestrator fan-out.
53
+ - **`scripts/demo.html`** — Cleo-themed self-contained vitrine page (open
54
+ in any browser, no build step). Previews every primitive animating live,
55
+ with API tables, code samples, and a light/dark theme toggle.
56
+ - **`scripts/demo.cjs`** (`cleocode-animations` bin) — terminal preview CLI
57
+ covering generic + canon spinners, sparks, and progress bars. Subcommands:
58
+ `--list` / `--list-canon` / `--list-sparks` / `--list-progress` /
59
+ `spark <name>` / `progress`.
60
+ - **`exports` map subpaths** for every public module —
61
+ `./animate-context`, `./progress`, `./spark`, `./spinner-handle`,
62
+ `./braille`, `./package.json` — for tree-shaking and discoverability.
63
+ - **`README.md`** — install, quick start, AnimateContext, registries,
64
+ canon mapping, full API surface, custom-spinner recipe, attribution.
65
+ - 153 tests covering frame-data snapshots, canon contract, AnimateContext
66
+ precedence, progress-bar boundaries, spark decay, SpinnerHandle
67
+ idempotency / cursor management / context-disabled no-op behavior, and
68
+ the process-level listener-count invariant (≤ 1 listener per signal,
69
+ regardless of how many handles run concurrently).
70
+ - LICENSE: MIT, dual-copyright (Gunnar Gray + CLEO Code).
71
+ - ESM-only, built via `tsc -p tsconfig.build.json` (matches monorepo
72
+ conventions; dropped upstream's `tsup` + CJS/IIFE bundling).
73
+ - Wired into `.github/workflows/release.yml` (version-sync iterator and
74
+ publish chain, positioned between `git-shim` and `core`) and
75
+ `scripts/execute-payload.mjs` (`PUBLISHED_PACKAGES` list).
76
+
77
+ [2026.5.29]: https://www.npmjs.com/package/@cleocode/animations/v/2026.5.29
package/README.md ADDED
@@ -0,0 +1,290 @@
1
+ # @cleocode/animations
2
+
3
+ **Unicode terminal animations for the cleo CLI and CleoOS** — woven on the LOOM, gated by LAFS.
4
+
5
+ Provides four primitive surfaces, each gated by a single `AnimateContext` so the
6
+ LAFS protocol invariant ("JSON output is the default; human rendering requires
7
+ explicit opt-in") holds uniformly:
8
+
9
+ - **18 generic braille spinners** — frame-cycled loaders ported from
10
+ [`unicode-animations`](https://github.com/gunnargray-dev/unicode-animations) (MIT, Gunnar Gray)
11
+ - **9 canon spinner aliases** — workshop vocabulary (`looming`, `weaving`, `heartbeat`, …) on the same frame data
12
+ - **3 progress bar styles** — `tapestry`, `cascade`, `refinery` (canon-themed segmented gauges)
13
+ - **4 sparks** — one-shot accents (`awaken`, `sweep`, `cascade`, `weave`)
14
+
15
+ [![npm](https://img.shields.io/npm/v/@cleocode/animations)](https://www.npmjs.com/package/@cleocode/animations)
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pnpm add @cleocode/animations
21
+ # or
22
+ npm install @cleocode/animations
23
+ ```
24
+
25
+ ESM-only. Requires Node ≥ 22.
26
+
27
+ ## Quick start
28
+
29
+ ### Spinner during async work — `createSpinnerHandle`
30
+
31
+ `createSpinnerHandle` is the canonical owner of `\r` writes for this package.
32
+ It manages the timer, hides/restores the cursor, installs an exit handler so
33
+ `Ctrl-C` doesn't leave a hidden cursor, and routes everything through the
34
+ LAFS render gate. **Calling `process.stdout.write` of a string starting with
35
+ `\r` outside this package is a contract violation** — always go through the
36
+ handle.
37
+
38
+ ```ts
39
+ import { resolveOutputFormat } from '@cleocode/lafs';
40
+ import { createAnimateContext, createSpinnerHandle } from '@cleocode/animations';
41
+
42
+ const ctx = createAnimateContext({
43
+ flagResolution: resolveOutputFormat({ humanFlag: true }),
44
+ });
45
+
46
+ const spinner = createSpinnerHandle(ctx, 'looming', 'Weaving tasks…');
47
+ spinner.start();
48
+ try {
49
+ await doWork();
50
+ spinner.stop('✔ Tapestry complete.');
51
+ } catch (err) {
52
+ spinner.stop();
53
+ throw err;
54
+ }
55
+ ```
56
+
57
+ Under `--json` / `--quiet` / non-TTY / `NO_COLOR` the handle is a frozen no-op
58
+ and emits zero output — call sites stay branch-free.
59
+
60
+ ### Progress bar with a known ratio
61
+
62
+ ```ts
63
+ import { renderProgressBar } from '@cleocode/animations';
64
+
65
+ function tick(done: number, total: number) {
66
+ const ratio = done / total;
67
+ const bar = renderProgressBar('refinery', ratio, 36);
68
+ process.stdout.write(`\r\x1B[2K ${bar} ${done}/${total}`);
69
+ }
70
+ ```
71
+
72
+ ### One-shot spark on success
73
+
74
+ ```ts
75
+ import { sparks } from '@cleocode/animations';
76
+
77
+ async function playSpark(name: 'awaken' | 'sweep' | 'cascade' | 'weave') {
78
+ const { frames, interval } = sparks[name];
79
+ for (const f of frames) {
80
+ process.stdout.write(`\r\x1B[2K ${f}`);
81
+ await new Promise(r => setTimeout(r, interval));
82
+ }
83
+ process.stdout.write('\n');
84
+ }
85
+
86
+ await shipRelease();
87
+ await playSpark('cascade');
88
+ ```
89
+
90
+ ## LAFS-aware rendering — `AnimateContext`
91
+
92
+ Every primitive routes through an `AnimateContext` so the package obeys the
93
+ LAFS protocol uniformly. The context is **pure data** — no I/O, no timers —
94
+ derived from the LAFS `FlagResolution` plus environment signals.
95
+
96
+ ```ts
97
+ import { resolveOutputFormat } from '@cleocode/lafs';
98
+ import { createAnimateContext, createSpinnerHandle } from '@cleocode/animations';
99
+
100
+ const flags = resolveOutputFormat({ humanFlag: true });
101
+ const ctx = createAnimateContext({ flagResolution: flags });
102
+
103
+ // Hand `ctx` to any primitive — they all become no-ops when `ctx.enabled === false`.
104
+ const spinner = createSpinnerHandle(ctx, 'looming', 'Loading…');
105
+
106
+ if (!ctx.enabled) {
107
+ // ctx.reason ∈ 'format-json' | 'quiet' | 'no-tty' | 'no-color' | 'enabled'
108
+ console.log(`silent: ${ctx.reason}`);
109
+ }
110
+ ```
111
+
112
+ | Signal | Source | Effect | `reason` |
113
+ |---|---|---|---|
114
+ | `format !== 'human'` | LAFS flags | Disable (machine output) | `format-json` |
115
+ | `quiet === true` | LAFS flags | Disable (script-friendly) | `quiet` |
116
+ | `!isTTY` | `process.stdout.isTTY` | Disable (piped/redirected) | `no-tty` |
117
+ | `NO_COLOR` set | `process.env.NO_COLOR` | Disable ([no-color.org](https://no-color.org)) | `no-color` |
118
+
119
+ `SILENT_CONTEXT` is exported as a frozen always-disabled context for tests and
120
+ libraries that want to opt out without constructing a full LAFS resolution.
121
+
122
+ ## Spinner registry
123
+
124
+ ### Generic — 18 braille loaders
125
+
126
+ | Name | Frames | Interval | Name | Frames | Interval |
127
+ |---|---|---|---|---|---|
128
+ | `braille` | 10 | 80ms | `cascade` | 14 | 60ms |
129
+ | `braillewave` | 8 | 100ms | `columns` | 26 | 60ms |
130
+ | `dna` | 12 | 80ms | `orbit` | 8 | 100ms |
131
+ | `scan` | 10 | 70ms | `breathe` | 17 | 100ms |
132
+ | `rain` | 12 | 100ms | `waverows` | 16 | 90ms |
133
+ | `scanline` | 6 | 120ms | `checkerboard` | 4 | 250ms |
134
+ | `pulse` | 5 | 180ms | `helix` | 16 | 80ms |
135
+ | `snake` | 16 | 80ms | `fillsweep` | 11 | 100ms |
136
+ | `sparkle` | 6 | 150ms | `diagswipe` | 16 | 60ms |
137
+
138
+ ### Canon — 9 workshop aliases
139
+
140
+ | Canon name | → Generic | Cleo lore role |
141
+ |---|---|---|
142
+ | `looming` | `helix` | Twin strands weaving — task on the LOOM |
143
+ | `weaving` | `braillewave` | Pattern threading across columns |
144
+ | `heartbeat` | `breathe` | Organic in-out pulse — Hearth presence |
145
+ | `awakening` | `pulse` | Radial bloom — first dream / `cleo init` |
146
+ | `sweeping` | `scan` | Left→right beam — BRAIN integrity Sweep |
147
+ | `watching` | `orbit` | Circular sentinel — sentient daemon tick |
148
+ | `cascade` | `cascade` | Diagonal fall — command-success accent |
149
+ | `tapestry` | `waverows` | Multi-row sinusoidal — wave of tasks shipping |
150
+ | `refinery` | `columns` | Filling stages — memory promotion pipeline |
151
+
152
+ Canon entries are **aliases**, not copies — they reference the same `Spinner`
153
+ objects as the generic registry. Renaming a generic spinner automatically
154
+ updates the canon view. The mapping is exposed as `CANON_TO_GENERIC` and
155
+ `resolveSpinner(name)` accepts either form.
156
+
157
+ ## Progress bars
158
+
159
+ `renderProgressBar(style, ratio, width)` returns a fixed-width string. Three
160
+ canon styles:
161
+
162
+ | Style | Characters | Feel |
163
+ |---|---|---|
164
+ | `tapestry` | `░ ▒ ▓ █` | Coarse blocks — woven cloth filling cell-by-cell |
165
+ | `cascade` | `▏ ▎ ▍ ▌ ▋ ▊ ▉ █` | 1/8 gradient steps — smooth waterfall edge |
166
+ | `refinery` | `⠀ ⡀ ⡄ ⡆ ⡇ ⣇ ⣧ ⣷ ⣿` | Braille block stages — BRAIN memory promotion pipeline |
167
+
168
+ Inputs outside `[0, 1]` are clamped. `width` ≤ 0 returns `''`.
169
+
170
+ ## Sparks — one-shot accents
171
+
172
+ ```ts
173
+ import { sparks, sparkDurationMs } from '@cleocode/animations';
174
+
175
+ sparkDurationMs('cascade'); // → ~980ms (frames * interval)
176
+ ```
177
+
178
+ | Spark | Frames | Duration | Played on |
179
+ |---|---|---|---|
180
+ | `awaken` | 13 × 90ms | ~1.17s | `cleo init` · first dream · sentient wake |
181
+ | `sweep` | 7 × 80ms | ~560ms | BRAIN integrity sweep complete |
182
+ | `cascade` | 14 × 70ms | ~980ms | Release shipped · task complete |
183
+ | `weave` | 18 × 70ms | ~1.26s | Playbook stage transition · CANT directive accepted |
184
+
185
+ ## Browser demo
186
+
187
+ A self-contained vitrine page ships at `scripts/demo.html`. Open it in any
188
+ browser to preview every spinner, canon alias, progress style, and spark
189
+ animating live, with API reference tables and code samples.
190
+
191
+ ```bash
192
+ # From an npm install
193
+ open node_modules/@cleocode/animations/scripts/demo.html # macOS
194
+ xdg-open node_modules/@cleocode/animations/scripts/demo.html # Linux
195
+
196
+ # From the cleo monorepo checkout
197
+ open packages/animations/scripts/demo.html # macOS
198
+ xdg-open packages/animations/scripts/demo.html # Linux
199
+ ```
200
+
201
+ The page is fully self-contained (no build step, no fetch, no `node_modules`
202
+ runtime requirement) so it can be emailed, dropped into a slide deck, or
203
+ hosted as a static asset for design reviews.
204
+
205
+ ## Terminal demo
206
+
207
+ ```bash
208
+ npx cleocode-animations # cycle through generic + canon spinners
209
+ npx cleocode-animations looming # preview one spinner (generic OR canon)
210
+ npx cleocode-animations spark cascade # play one spark and exit
211
+ npx cleocode-animations progress # loop through all 3 progress styles
212
+
213
+ npx cleocode-animations --list # full listing — spinners + sparks + progress
214
+ npx cleocode-animations --list-canon # canon aliases only
215
+ npx cleocode-animations --list-sparks # sparks only
216
+ npx cleocode-animations --list-progress # progress styles only
217
+ ```
218
+
219
+ ## API surface
220
+
221
+ ### Spinners
222
+
223
+ | Export | Type |
224
+ |---|---|
225
+ | `spinners` | `Record<BrailleSpinnerName, Spinner>` |
226
+ | `canonSpinners` | `Record<CanonSpinnerName, Spinner>` |
227
+ | `CANON_TO_GENERIC` | `Record<CanonSpinnerName, BrailleSpinnerName>` |
228
+ | `resolveSpinner(name)` | `(string) => Spinner \| undefined` |
229
+ | `gridToBraille(grid)` | `(boolean[][]) => string` |
230
+ | `makeGrid(rows, cols)` | `(number, number) => boolean[][]` |
231
+ | `Spinner` | `{ frames: readonly string[]; interval: number }` |
232
+ | `BrailleSpinnerName` · `CanonSpinnerName` | TS string-literal unions |
233
+
234
+ ### SpinnerHandle (canonical `\r` owner)
235
+
236
+ | Export | Type |
237
+ |---|---|
238
+ | `createSpinnerHandle(ctx, name, label, options?)` | `(AnimateContext, name, string, SpinnerHandleOptions?) => SpinnerHandle` |
239
+ | `SpinnerHandle` | `{ start(); stop(finalLine?); update(label); enabled: boolean }` |
240
+ | `SpinnerHandleOptions` | `{ delayMs?: number }` — defaults to `150` |
241
+
242
+ ### AnimateContext
243
+
244
+ | Export | Type |
245
+ |---|---|
246
+ | `createAnimateContext(input)` | `(AnimateContextInput) => AnimateContext` |
247
+ | `SILENT_CONTEXT` | Frozen `AnimateContext` — always disabled |
248
+ | `AnimateContext` | `{ enabled, reason, inputs }` |
249
+ | `AnimateContextInput` | `{ flagResolution, isTTY?, noColor? }` |
250
+ | `FlagResolutionLike` | `{ format: 'json' \| 'human'; quiet: boolean }` |
251
+
252
+ ### Progress + Sparks
253
+
254
+ | Export | Type |
255
+ |---|---|
256
+ | `progressBars` | `Record<ProgressBarStyle, ProgressBarRenderer>` |
257
+ | `renderProgressBar(style, ratio, width)` | `(style, number, number) => string` |
258
+ | `ProgressBarStyle` | `'tapestry' \| 'cascade' \| 'refinery'` |
259
+ | `sparks` | `Record<SparkName, Spark>` |
260
+ | `sparkDurationMs(name)` | `(SparkName) => number` |
261
+ | `SparkName` | `'awaken' \| 'sweep' \| 'cascade' \| 'weave'` |
262
+
263
+ ## Custom spinners
264
+
265
+ Every animation here is built from two primitives — compose your own:
266
+
267
+ ```ts
268
+ import { gridToBraille, makeGrid } from '@cleocode/animations';
269
+
270
+ const grid = makeGrid(4, 4);
271
+ grid[0][0] = true;
272
+ grid[1][1] = true;
273
+ grid[2][2] = true;
274
+ grid[3][3] = true;
275
+
276
+ console.log(gridToBraille(grid)); // diagonal braille pattern
277
+ ```
278
+
279
+ `makeGrid(rows, cols)` returns a `boolean[][]`. Set cells to `true` to raise
280
+ braille dots. `gridToBraille(grid)` packs them into a braille string (2 dot
281
+ columns per character, U+2800 base).
282
+
283
+ ## License
284
+
285
+ MIT — dual copyright:
286
+
287
+ - © 2024 Gunnar Gray (original `unicode-animations` project)
288
+ - © 2026 CLEO Code (`@cleocode/animations` fork)
289
+
290
+ See `LICENSE`.
@@ -0,0 +1,115 @@
1
+ /**
2
+ * AnimateContext — render-gate for terminal animations.
3
+ *
4
+ * @remarks
5
+ * Every animation primitive in this package (spinners, progress bars, sparks)
6
+ * routes its output through an {@link AnimateContext}. When the context is
7
+ * "silent" — JSON mode, --quiet, non-TTY pipes, or NO_COLOR — the primitive
8
+ * returns no-op handles so callers do not have to branch on output mode.
9
+ *
10
+ * This mirrors the LAFS protocol invariant from `@cleocode/lafs`: JSON output
11
+ * is the default, human-readable rendering requires explicit opt-in. By
12
+ * keeping the gate logic in one place we guarantee that every animation
13
+ * surface obeys the same rules:
14
+ *
15
+ * - `format === 'human'` — animations enabled
16
+ * - `format === 'json'` — animations disabled (machine output)
17
+ * - `quiet === true` — animations disabled (script-friendly)
18
+ * - `isTTY === false` — animations disabled (piped/redirected)
19
+ * - `noColor === true` — animations disabled (NO_COLOR env)
20
+ *
21
+ * The context is intentionally pure data — no I/O, no timers — so it can be
22
+ * constructed once at command entry and threaded through long-running ops.
23
+ */
24
+ /**
25
+ * Minimal subset of the LAFS `FlagResolution` shape that AnimateContext needs.
26
+ *
27
+ * @remarks
28
+ * Declared structurally rather than imported from `@cleocode/lafs` to keep
29
+ * `@cleocode/animations` zero-dep. Anything that produces a `{ format, quiet }`
30
+ * pair — including `resolveOutputFormat()` from LAFS — satisfies this contract.
31
+ */
32
+ export interface FlagResolutionLike {
33
+ /** Resolved output format. */
34
+ readonly format: 'json' | 'human';
35
+ /** When true, suppress non-essential output. */
36
+ readonly quiet: boolean;
37
+ }
38
+ /**
39
+ * Inputs to {@link createAnimateContext}.
40
+ *
41
+ * @remarks
42
+ * `flagResolution` is the load-bearing input — pass the value returned by
43
+ * `resolveOutputFormat()` from `@cleocode/lafs`. The other fields default to
44
+ * inspecting the current Node.js process environment when omitted, which is
45
+ * the right call for nearly every CLI use case.
46
+ */
47
+ export interface AnimateContextInput {
48
+ /** Resolved LAFS flags governing output format and quietness. */
49
+ readonly flagResolution: FlagResolutionLike;
50
+ /**
51
+ * Whether stdout is attached to a TTY. Defaults to `process.stdout.isTTY`.
52
+ * Pass `false` explicitly when rendering to a buffer or redirected stream.
53
+ */
54
+ readonly isTTY?: boolean;
55
+ /**
56
+ * Whether the `NO_COLOR` standard is in effect (https://no-color.org).
57
+ * Defaults to `process.env.NO_COLOR != null`. Pass `false` to override.
58
+ */
59
+ readonly noColor?: boolean;
60
+ }
61
+ /**
62
+ * Resolved render-gate context — consult `enabled` before rendering anything.
63
+ *
64
+ * @remarks
65
+ * `reason` carries diagnostic provenance (which gate disabled rendering) so
66
+ * verbose-mode callers can surface why animations are silent without
67
+ * re-implementing the gate logic.
68
+ */
69
+ export interface AnimateContext {
70
+ /** Whether animation rendering is permitted. */
71
+ readonly enabled: boolean;
72
+ /** When `enabled === false`, the rule that disabled rendering. */
73
+ readonly reason: 'enabled' | 'format-json' | 'quiet' | 'no-tty' | 'no-color';
74
+ /** Echo of the inputs used to derive this context — useful for diagnostics. */
75
+ readonly inputs: {
76
+ readonly format: 'json' | 'human';
77
+ readonly quiet: boolean;
78
+ readonly isTTY: boolean;
79
+ readonly noColor: boolean;
80
+ };
81
+ }
82
+ /**
83
+ * Construct an {@link AnimateContext} from LAFS flags + environment signals.
84
+ *
85
+ * @param input - LAFS flag resolution plus optional TTY / NO_COLOR overrides
86
+ * @returns The resolved context with `enabled` flag and diagnostic `reason`
87
+ *
88
+ * @remarks
89
+ * Precedence (first match disables): `format !== 'human'` →
90
+ * `quiet === true` → `isTTY === false` → `noColor === true`. All four checks
91
+ * fire independently; rendering is enabled only when every gate passes.
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * import { resolveOutputFormat } from '@cleocode/lafs';
96
+ * import { createAnimateContext, createSpinnerHandle } from '@cleocode/animations';
97
+ *
98
+ * const flagResolution = resolveOutputFormat({ humanFlag: true });
99
+ * const context = createAnimateContext({ flagResolution });
100
+ * const spinner = createSpinnerHandle(context, 'looming', 'Weaving tasks…');
101
+ * spinner.start();
102
+ * await doWork();
103
+ * spinner.stop('Done.');
104
+ * ```
105
+ */
106
+ export declare function createAnimateContext(input: AnimateContextInput): AnimateContext;
107
+ /**
108
+ * A silent context — guarantees every primitive returns a no-op handle.
109
+ *
110
+ * @remarks
111
+ * Useful for tests and for libraries that want to opt out of animations
112
+ * without constructing a full LAFS flag resolution.
113
+ */
114
+ export declare const SILENT_CONTEXT: AnimateContext;
115
+ //# sourceMappingURL=animate-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"animate-context.d.ts","sourceRoot":"","sources":["../../src/animate-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IACjC,8BAA8B;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,gDAAgD;IAChD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB;IAClC,iEAAiE;IACjE,QAAQ,CAAC,cAAc,EAAE,kBAAkB,CAAC;IAC5C;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,UAAU,CAAC;IAC7E,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;QAClC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;QACxB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;QACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;KAC3B,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,mBAAmB,GAAG,cAAc,CAsB/E;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,EAAE,cAS3B,CAAC"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * AnimateContext — render-gate for terminal animations.
3
+ *
4
+ * @remarks
5
+ * Every animation primitive in this package (spinners, progress bars, sparks)
6
+ * routes its output through an {@link AnimateContext}. When the context is
7
+ * "silent" — JSON mode, --quiet, non-TTY pipes, or NO_COLOR — the primitive
8
+ * returns no-op handles so callers do not have to branch on output mode.
9
+ *
10
+ * This mirrors the LAFS protocol invariant from `@cleocode/lafs`: JSON output
11
+ * is the default, human-readable rendering requires explicit opt-in. By
12
+ * keeping the gate logic in one place we guarantee that every animation
13
+ * surface obeys the same rules:
14
+ *
15
+ * - `format === 'human'` — animations enabled
16
+ * - `format === 'json'` — animations disabled (machine output)
17
+ * - `quiet === true` — animations disabled (script-friendly)
18
+ * - `isTTY === false` — animations disabled (piped/redirected)
19
+ * - `noColor === true` — animations disabled (NO_COLOR env)
20
+ *
21
+ * The context is intentionally pure data — no I/O, no timers — so it can be
22
+ * constructed once at command entry and threaded through long-running ops.
23
+ */
24
+ /**
25
+ * Construct an {@link AnimateContext} from LAFS flags + environment signals.
26
+ *
27
+ * @param input - LAFS flag resolution plus optional TTY / NO_COLOR overrides
28
+ * @returns The resolved context with `enabled` flag and diagnostic `reason`
29
+ *
30
+ * @remarks
31
+ * Precedence (first match disables): `format !== 'human'` →
32
+ * `quiet === true` → `isTTY === false` → `noColor === true`. All four checks
33
+ * fire independently; rendering is enabled only when every gate passes.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { resolveOutputFormat } from '@cleocode/lafs';
38
+ * import { createAnimateContext, createSpinnerHandle } from '@cleocode/animations';
39
+ *
40
+ * const flagResolution = resolveOutputFormat({ humanFlag: true });
41
+ * const context = createAnimateContext({ flagResolution });
42
+ * const spinner = createSpinnerHandle(context, 'looming', 'Weaving tasks…');
43
+ * spinner.start();
44
+ * await doWork();
45
+ * spinner.stop('Done.');
46
+ * ```
47
+ */
48
+ export function createAnimateContext(input) {
49
+ const format = input.flagResolution.format;
50
+ const quiet = input.flagResolution.quiet;
51
+ const isTTY = input.isTTY ?? Boolean(process.stdout.isTTY);
52
+ const noColor = input.noColor ?? process.env.NO_COLOR != null;
53
+ const inputs = { format, quiet, isTTY, noColor };
54
+ if (format !== 'human') {
55
+ return { enabled: false, reason: 'format-json', inputs };
56
+ }
57
+ if (quiet) {
58
+ return { enabled: false, reason: 'quiet', inputs };
59
+ }
60
+ if (!isTTY) {
61
+ return { enabled: false, reason: 'no-tty', inputs };
62
+ }
63
+ if (noColor) {
64
+ return { enabled: false, reason: 'no-color', inputs };
65
+ }
66
+ return { enabled: true, reason: 'enabled', inputs };
67
+ }
68
+ /**
69
+ * A silent context — guarantees every primitive returns a no-op handle.
70
+ *
71
+ * @remarks
72
+ * Useful for tests and for libraries that want to opt out of animations
73
+ * without constructing a full LAFS flag resolution.
74
+ */
75
+ export const SILENT_CONTEXT = Object.freeze({
76
+ enabled: false,
77
+ reason: 'format-json',
78
+ inputs: Object.freeze({
79
+ format: 'json',
80
+ quiet: false,
81
+ isTTY: false,
82
+ noColor: false,
83
+ }),
84
+ });
85
+ //# sourceMappingURL=animate-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"animate-context.js","sourceRoot":"","sources":["../../src/animate-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AA+DH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAA0B;IAC7D,MAAM,MAAM,GAAG,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC;IAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC;IACzC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3D,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,IAAI,CAAC;IAE9D,MAAM,MAAM,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAW,CAAC;IAE1D,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;QACvB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC;IAC3D,CAAC;IACD,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IACrD,CAAC;IACD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACtD,CAAC;IACD,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC;IACxD,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;AACtD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,cAAc,GAAmB,MAAM,CAAC,MAAM,CAAC;IAC1D,OAAO,EAAE,KAAK;IACd,MAAM,EAAE,aAAa;IACrB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM;QACd,KAAK,EAAE,KAAK;QACZ,KAAK,EAAE,KAAK;QACZ,OAAO,EAAE,KAAK;KACf,CAAC;CACH,CAAC,CAAC"}
@@ -25,4 +25,36 @@ export declare function gridToBraille(grid: boolean[][]): string;
25
25
  export declare function makeGrid(rows: number, cols: number): boolean[][];
26
26
  export declare const spinners: Record<BrailleSpinnerName, Spinner>;
27
27
  export default spinners;
28
+ /**
29
+ * Canon-themed spinner identifiers drawn from CLEO workshop vocabulary.
30
+ *
31
+ * @remarks
32
+ * Each canon name is an alias pointing at the same {@link Spinner} object
33
+ * registered in {@link spinners} under its generic name.
34
+ */
35
+ export type CanonSpinnerName = 'looming' | 'weaving' | 'heartbeat' | 'awakening' | 'sweeping' | 'watching' | 'cascade' | 'tapestry' | 'refinery';
36
+ /**
37
+ * Canon-name → generic-name lookup table.
38
+ *
39
+ * @remarks
40
+ * Exposed so consumers can render the underlying generic name in diagnostics
41
+ * (`looming → helix`) without hardcoding the relationship.
42
+ */
43
+ export declare const CANON_TO_GENERIC: Record<CanonSpinnerName, BrailleSpinnerName>;
44
+ /**
45
+ * Canon-themed spinner registry — aliases on top of {@link spinners}.
46
+ *
47
+ * @remarks
48
+ * Each entry references the same {@link Spinner} object as the generic
49
+ * registry, so frame data is never duplicated. Renaming a generic spinner
50
+ * automatically updates the canon view.
51
+ */
52
+ export declare const canonSpinners: Record<CanonSpinnerName, Spinner>;
53
+ /**
54
+ * Resolve any spinner name (generic OR canon) to its {@link Spinner}.
55
+ *
56
+ * @param name - Either a {@link BrailleSpinnerName} or a {@link CanonSpinnerName}
57
+ * @returns The matching spinner, or `undefined` if the name is not registered.
58
+ */
59
+ export declare function resolveSpinner(name: string): Spinner | undefined;
28
60
  //# sourceMappingURL=braille.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"braille.d.ts","sourceRoot":"","sources":["../../src/braille.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,kBAAkB,GAC1B,SAAS,GACT,aAAa,GACb,KAAK,GACL,MAAM,GACN,MAAM,GACN,UAAU,GACV,OAAO,GACP,OAAO,GACP,SAAS,GACT,SAAS,GACT,SAAS,GACT,OAAO,GACP,SAAS,GACT,UAAU,GACV,cAAc,GACd,OAAO,GACP,WAAW,GACX,WAAW,CAAC;AAqBhB;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,GAAG,MAAM,CAkBvD;AAED,+CAA+C;AAC/C,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,EAAE,CAGhE;AA8XD,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,kBAAkB,EAAE,OAAO,CA4CxD,CAAC;AAEF,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"braille.d.ts","sourceRoot":"","sources":["../../src/braille.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,kBAAkB,GAC1B,SAAS,GACT,aAAa,GACb,KAAK,GACL,MAAM,GACN,MAAM,GACN,UAAU,GACV,OAAO,GACP,OAAO,GACP,SAAS,GACT,SAAS,GACT,SAAS,GACT,OAAO,GACP,SAAS,GACT,UAAU,GACV,cAAc,GACd,OAAO,GACP,WAAW,GACX,WAAW,CAAC;AAqBhB;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,GAAG,MAAM,CAkBvD;AAED,+CAA+C;AAC/C,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,EAAE,CAGhE;AA8XD,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,kBAAkB,EAAE,OAAO,CA4CxD,CAAC;AAEF,eAAe,QAAQ,CAAC;AA2BxB;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,SAAS,GACT,WAAW,GACX,WAAW,GACX,UAAU,GACV,UAAU,GACV,SAAS,GACT,UAAU,GACV,UAAU,CAAC;AAEf;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,gBAAgB,EAAE,kBAAkB,CAUzE,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,gBAAgB,EAAE,OAAO,CAU3D,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAQhE"}
@@ -460,4 +460,56 @@ export const spinners = {
460
460
  diagswipe: { frames: genDiagonalSwipe(), interval: 60 },
461
461
  };
462
462
  export default spinners;
463
+ /**
464
+ * Canon-name → generic-name lookup table.
465
+ *
466
+ * @remarks
467
+ * Exposed so consumers can render the underlying generic name in diagnostics
468
+ * (`looming → helix`) without hardcoding the relationship.
469
+ */
470
+ export const CANON_TO_GENERIC = {
471
+ looming: 'helix',
472
+ weaving: 'braillewave',
473
+ heartbeat: 'breathe',
474
+ awakening: 'pulse',
475
+ sweeping: 'scan',
476
+ watching: 'orbit',
477
+ cascade: 'cascade',
478
+ tapestry: 'waverows',
479
+ refinery: 'columns',
480
+ };
481
+ /**
482
+ * Canon-themed spinner registry — aliases on top of {@link spinners}.
483
+ *
484
+ * @remarks
485
+ * Each entry references the same {@link Spinner} object as the generic
486
+ * registry, so frame data is never duplicated. Renaming a generic spinner
487
+ * automatically updates the canon view.
488
+ */
489
+ export const canonSpinners = {
490
+ looming: spinners[CANON_TO_GENERIC.looming],
491
+ weaving: spinners[CANON_TO_GENERIC.weaving],
492
+ heartbeat: spinners[CANON_TO_GENERIC.heartbeat],
493
+ awakening: spinners[CANON_TO_GENERIC.awakening],
494
+ sweeping: spinners[CANON_TO_GENERIC.sweeping],
495
+ watching: spinners[CANON_TO_GENERIC.watching],
496
+ cascade: spinners[CANON_TO_GENERIC.cascade],
497
+ tapestry: spinners[CANON_TO_GENERIC.tapestry],
498
+ refinery: spinners[CANON_TO_GENERIC.refinery],
499
+ };
500
+ /**
501
+ * Resolve any spinner name (generic OR canon) to its {@link Spinner}.
502
+ *
503
+ * @param name - Either a {@link BrailleSpinnerName} or a {@link CanonSpinnerName}
504
+ * @returns The matching spinner, or `undefined` if the name is not registered.
505
+ */
506
+ export function resolveSpinner(name) {
507
+ if (name in spinners) {
508
+ return spinners[name];
509
+ }
510
+ if (name in canonSpinners) {
511
+ return canonSpinners[name];
512
+ }
513
+ return undefined;
514
+ }
463
515
  //# sourceMappingURL=braille.js.map