@zakkster/lite-ui-fx 1.0.3 → 1.0.5

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,100 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@zakkster/lite-ui-fx` are documented here.
4
+
5
+ The format follows Keep a Changelog; this project adheres to Semantic
6
+ Versioning.
7
+
8
+ ## [1.0.5] -- unreleased
9
+
10
+ Truth pass, law pass, and the torture skeleton. No runtime behaviour
11
+ changes: this release makes the package honest, lawful, and provable.
12
+
13
+ ### Fixed
14
+
15
+ - U-04 (docs sell what does not ship): recipe count corrected to 50
16
+ everywhere (`package.json` description, `llms.txt`); the recipe-guide
17
+ link now points at `recipes/UIFX-RECIPE-GUIDE.md` (was a root-level
18
+ 404); the broken import quote in `llms.txt` is repaired.
19
+
20
+ ### Changed
21
+
22
+ - U-08 (packaging law): restored `"sideEffects": false` (dropped by
23
+ mistake in 1.0.4 -- see below); added `"engines": { "node": ">=18" }`;
24
+ `UIFX-RECIPE-GUIDE.md` now ships in the package `files[]` so the
25
+ "included in the package" claim is true; `LICENSE`, `README.md`, and
26
+ `CHANGELOG.md` ship as well.
27
+ - U-08 (source law): ASCII-only sweep across the controller, recipe
28
+ volumes, guide, README, and `llms.txt` (em/en dashes, arrows,
29
+ box-drawing, emoji removed). `demo/` is exempt until U6.
30
+ - U-07 (test law): the vitest suite is ported to `node:test` +
31
+ `assert/strict`; the dead `_tickAll`/`_clear` imports are replaced by a
32
+ deterministic RAF stub. `vitest` removed from devDependencies.
33
+
34
+ ### Added
35
+
36
+ - `export const VERSION = '1.0.5'` in `UIFXController.js` (three-place
37
+ version sync: `package.json`, this const, and the `llms.txt` VERSION
38
+ line).
39
+ - `test/` node:test suite plus the torture skeleton
40
+ (`node --expose-gc test/torture.mjs`) gated by `@zakkster/lite-leak` and
41
+ `@zakkster/lite-gc-profiler`.
42
+
43
+ ### Known issues (see ROADMAP.md)
44
+
45
+ - U-01: Space cannot toggle a toggle; each press fires two spurious
46
+ `onToggle`s. (Fixed in U1.)
47
+ - U-02: one malformed recipe (no `tick`) throws inside the shared ticker
48
+ and permanently freezes every UIFX component on the page. (Fixed in U1.)
49
+ - U-03: "zero-GC in all built-in recipes" is false for 49 of 50 recipes.
50
+ (Fixed in U3.)
51
+ - U-05: recipes misrender at any non-default size (hardcoded geometry).
52
+ (Fixed in U3.)
53
+ - U-06: theming is half-built with three conventions; hardcoded fonts and
54
+ canvas labels. (Fixed in U3.)
55
+ - U-09: every slider mount leaks a `<style>` into `document.head`; never
56
+ removed on destroy. (Fixed in U1.)
57
+ - U-10: fail-open option surface -- unknown option keys silently ignored;
58
+ no initial-value options; DPR read once. (Fixed in U1.)
59
+ - U-11: `getBoundingClientRect()` runs on every pointermove. (Fixed in U1.)
60
+ - U-12: the demos reimplement the library inline instead of consuming it.
61
+ (Fixed in U6.)
62
+ - U-13: recipes are distributed as a GitHub ZIP, not shipped code.
63
+ (Fixed in U2.)
64
+
65
+ ## [1.0.4] -- 2026-03-26
66
+
67
+ ### Changed
68
+
69
+ - Metadata patch.
70
+
71
+ ### Regression
72
+
73
+ - Dropped `"sideEffects": false` from `package.json` (present in 1.0.3).
74
+ Restored in 1.0.5.
75
+
76
+ ## [1.0.3] -- 2026-03-25
77
+
78
+ ### Changed
79
+
80
+ - README-only patch.
81
+
82
+ ## [1.0.2] -- 2026-03-25
83
+
84
+ ### Changed
85
+
86
+ - README-only patch.
87
+
88
+ ## [1.0.1] -- 2026-03-25
89
+
90
+ ### Changed
91
+
92
+ - README-only patch.
93
+
94
+ ## [1.0.0] -- 2026-03-25
95
+
96
+ ### Added
97
+
98
+ - Initial release: the `mountUIFX` controller plus 50 GitHub-hosted
99
+ recipes across toggles, buttons, sliders, knobs, loaders, checkboxes,
100
+ counters, and ratings.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zahary Shinikchiev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -8,72 +8,72 @@
8
8
  ![Dependencies](https://img.shields.io/badge/dependencies-3-brightgreen)
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](https://opensource.org/licenses/MIT)
10
10
 
11
- ## What is lite-ui-fx?
11
+ ## What is lite-ui-fx?
12
12
 
13
- `@zakkster/lite-ui-fx` overlays a DPR-aware canvas on native HTML elements and renders them with pluggable, physics-driven **recipes**. The native element stays invisible but fully accessible handling focus, keyboard, and pointer events. The canvas handles all visuals.
13
+ `@zakkster/lite-ui-fx` overlays a DPR-aware canvas on native HTML elements and renders them with pluggable, physics-driven **recipes**. The native element stays invisible but fully accessible -- handling focus, keyboard, and pointer events. The canvas handles all visuals.
14
14
 
15
- ## 🎬 Live Demo (UI-FX)
15
+ ## Live Demo (UI-FX)
16
16
  https://cdpn.io/pen/debug/RNGjMjQ
17
17
 
18
- ## 🎬 Live Demo (UI-FX vol.2)
18
+ ## Live Demo (UI-FX vol.2)
19
19
  https://cdpn.io/pen/debug/yyaPKpB
20
20
 
21
- ## 🎬 Live Demo (UI-FX vol3.)
21
+ ## Live Demo (UI-FX vol3.)
22
22
  https://cdpn.io/pen/debug/YPGEaYY
23
23
 
24
24
  **50 recipes** across UI element categories:
25
25
 
26
- - 🔘 **Toggles** Swarm, Liquid, Neon Pulse, Pendulum, Circuit, Lightning, DNA
27
- - 🔲 **Buttons** Magnetic, Shatter, Confetti, Glitch, Heartbeat, Breathing, Ink Splash, Pixel Dissolve, Firework
28
- - 🎚️ **Sliders** Spark, Cosmic Void, Laser, Aurora, Wave, Elastic Band, Gravity
29
- - 🎛️ **Knobs** Volume dial, Compass needle
30
- - 📊 **Progress** Ring, Battery, Signal meter
31
- - 🔀 **Controls** Pill tabs, Stepper, Radio orbit
32
- - 📈 **Indicators** Password strength, Water level, Heat map
33
- - 🌗 **Mood** Day/night, Reaction picker, Notification bell
34
- - 💬 **Feedback** Typewriter, Sound wave, Upload progress
35
- - 🎮 **Fun** Scratch reveal, Timer countdown, Pull refresh
36
- - **Checkboxes** Ripple, Morph (X )
37
- - 🔄 **Loaders** Orbit planets, DNA helix
38
- - 🔢 **Counters** Flame heat, Glitch signal
39
- - **Rating** Bubble inflate
26
+ - **Toggles** -- Swarm, Liquid, Neon Pulse, Pendulum, Circuit, Lightning, DNA
27
+ - **Buttons** -- Magnetic, Shatter, Confetti, Glitch, Heartbeat, Breathing, Ink Splash, Pixel Dissolve, Firework
28
+ - **Sliders** -- Spark, Cosmic Void, Laser, Aurora, Wave, Elastic Band, Gravity
29
+ - **Knobs** -- Volume dial, Compass needle
30
+ - **Progress** -- Ring, Battery, Signal meter
31
+ - **Controls** -- Pill tabs, Stepper, Radio orbit
32
+ - **Indicators** -- Password strength, Water level, Heat map
33
+ - **Mood** -- Day/night, Reaction picker, Notification bell
34
+ - **Feedback** -- Typewriter, Sound wave, Upload progress
35
+ - **Fun** -- Scratch reveal, Timer countdown, Pull refresh
36
+ - **Checkboxes** -- Ripple, Morph (X to check)
37
+ - **Loaders** -- Orbit planets, DNA helix
38
+ - **Counters** -- Flame heat, Glitch signal
39
+ - **Rating** -- Bubble inflate
40
40
 
41
41
  Every recipe is zero-GC, uses `dt`-based animation, and includes accessibility indicators (focus rings, state labels).
42
42
 
43
- `@zakkster/lite-ui-fx` ships only the core controller on npm (zero bloat).
43
+ `@zakkster/lite-ui-fx` ships only the core controller on npm (zero bloat).
44
44
  All visual effects live in the GitHub repo as **recipes**.
45
45
 
46
- 📁 **Recipe Collections:**
47
- - Vol. 1 (10 recipes):
48
- https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/recipes/UIFXRecipes.js
49
- - Vol. 2 (20 recipes):
50
- https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/recipes/UIFXRecipes2.js
51
- - Vol. 3 (20 recipes):
52
- https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/recipes/UIFXRecipes3.js
46
+ **Recipe Collections:**
47
+ - Vol. 1 (10 recipes):
48
+ https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/recipes/UIFXRecipes.js
49
+ - Vol. 2 (20 recipes):
50
+ https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/recipes/UIFXRecipes2.js
51
+ - Vol. 3 (20 recipes):
52
+ https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/recipes/UIFXRecipes3.js
53
53
 
54
- 📘 **How to write your own:**
55
- https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/UIFX-RECIPE-GUIDE.md
54
+ **How to write your own:**
55
+ https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/recipes/UIFX-RECIPE-GUIDE.md
56
56
 
57
- Recipes are **optional**, **open-source**, and **not included in the npm package**
58
- to keep the install size tiny (<2 KB).
57
+ Recipes are **optional**, **open-source**, and **not included in the npm package**
58
+ to keep the install size tiny (<2 KB).
59
59
  You can copy/paste any recipe into your project or use them as inspiration.
60
60
 
61
- 📦 Download all recipes as a ZIP
61
+ Download all recipes as a ZIP
62
62
  https://github.com/PeshoVurtoleta/lite-ui-fx/archive/refs/heads/main.zip
63
63
 
64
64
  Part of the [@zakkster/lite-*](https://www.npmjs.com/org/zakkster) ecosystem.
65
65
 
66
- ## 🚀 Install
66
+ ## Install
67
67
 
68
68
  ```bash
69
69
  npm i @zakkster/lite-ui-fx
70
70
  ```
71
71
 
72
- > Looking for the visual effects?
73
- > Recipes live in the GitHub repo not in the npm package to keep the library tiny.
72
+ > Looking for the visual effects?
73
+ > Recipes live in the GitHub repo -- not in the npm package -- to keep the library tiny.
74
74
 
75
75
 
76
- ## 🕹️ Quick Start
76
+ ## Quick Start
77
77
 
78
78
  ```javascript
79
79
  import { mountUIFX, UIType } from '@zakkster/lite-ui-fx';
@@ -95,7 +95,7 @@ const instance = mountUIFX(
95
95
  instance.destroy();
96
96
  ```
97
97
 
98
- ## 📦 Import Map
98
+ ## Import Map
99
99
 
100
100
  ```javascript
101
101
  // Controller (always needed)
@@ -104,40 +104,40 @@ import { mountUIFX, UIType } from '@zakkster/lite-ui-fx';
104
104
  // Recipes are NOT included in the npm package.
105
105
  // Copy them from the GitHub repo into your own ./recipes folder:
106
106
 
107
- // Vol. 1 10 recipes (toggles, buttons, sliders)
107
+ // Vol. 1 -- 10 recipes (toggles, buttons, sliders)
108
108
  import { SwarmToggle, MagneticButton, SparkSlider } from './recipes/UIFXRecipes.js';
109
109
 
110
- // Vol. 2 20 recipes (+ loaders, checkboxes, counters, rating)
110
+ // Vol. 2 -- 20 recipes (+ loaders, checkboxes, counters, rating)
111
111
  import { PendulumToggle, HeartbeatButton, RippleCheck } from './recipes/UIFXRecipes2.js';
112
112
 
113
- // Vol. 3 20 recipes (knobs, progress, controls, indicators, mood, feedback, fun)
113
+ // Vol. 3 -- 20 recipes (knobs, progress, controls, indicators, mood, feedback, fun)
114
114
  import { VolumeKnob, WaterLevel, TimerCountdown } from './recipes/UIFXRecipes3.js';
115
115
  ```
116
116
 
117
- ## 🧠 How It Works
117
+ ## How It Works
118
118
 
119
119
  ```
120
- ┌──────────────────────────────────────────────────┐
121
- mountUIFX(container, type, recipeFactory, opts)
122
-
123
- ┌──── Wrapper div ────────────────────────────┐
124
-
125
- Native element (opacity:0, z-index:2)
126
- receives pointer, keyboard, focus events │ │
127
- accessible to screen readers
128
-
129
- Canvas overlay (z-index:1, DPR-scaled)
130
- recipe.tick() renders every frame
131
- padding allows particle overflow
132
-
133
- └──────────────────────────────────────────────┘
134
-
135
- Shared Ticker (ref-counted, one RAF for all)
136
- AbortController (all events cleaned on destroy)
137
- └──────────────────────────────────────────────────┘
120
+ +--------------------------------------------------+
121
+ | mountUIFX(container, type, recipeFactory, opts) |
122
+ | |
123
+ | +---- Wrapper div ----------------------------+ |
124
+ | | | |
125
+ | | Native element (opacity:0, z-index:2) | |
126
+ | | -> receives pointer, keyboard, focus events | |
127
+ | | -> accessible to screen readers | |
128
+ | | | |
129
+ | | Canvas overlay (z-index:1, DPR-scaled) | |
130
+ | | -> recipe.tick() renders every frame | |
131
+ | | -> padding allows particle overflow | |
132
+ | | | |
133
+ | +----------------------------------------------+ |
134
+ | |
135
+ | Shared Ticker (ref-counted, one RAF for all) |
136
+ | AbortController (all events cleaned on destroy) |
137
+ +--------------------------------------------------+
138
138
  ```
139
139
 
140
- ## ⚙️ API
140
+ ## API
141
141
 
142
142
  ### `mountUIFX(container, type, recipeFactory, options?)`
143
143
 
@@ -159,7 +159,7 @@ Returns `{ el, canvas, wrapper, state, destroy() }`.
159
159
  |------|---------------|-------------|-----------|
160
160
  | `UIType.TOGGLE` | `<input type="checkbox" role="switch">` | `onToggle(checked)` | `state.toggled` |
161
161
  | `UIType.BUTTON` | `<button>` | `onClick(x, y, state)` | `state.active` |
162
- | `UIType.SLIDER` | `<input type="range">` | `onDrag(val, velocity)` | `state.val` (01) |
162
+ | `UIType.SLIDER` | `<input type="range">` | `onDrag(val, velocity)` | `state.val` (0-1) |
163
163
 
164
164
  ### State Object (provided to `tick()` every frame)
165
165
 
@@ -169,7 +169,7 @@ Returns `{ el, canvas, wrapper, state, destroy() }`.
169
169
  active: boolean; // Pointer pressed
170
170
  focused: boolean; // Keyboard focus
171
171
  toggled: boolean; // Checkbox state
172
- val: number; // Slider value (01)
172
+ val: number; // Slider value (0-1)
173
173
  w: number; // Element width
174
174
  h: number; // Element height
175
175
  padding: number; // Canvas padding
@@ -177,16 +177,16 @@ Returns `{ el, canvas, wrapper, state, destroy() }`.
177
177
  }
178
178
  ```
179
179
 
180
- ## 📊 Comparison
180
+ ## Comparison
181
181
 
182
182
  | Library | Size | Approach | Recipes | A11y | Install |
183
183
  |---------|------|----------|---------|------|---------|
184
184
  | Framer Motion | ~45 KB | React HOC | 0 | Via React | `npm i framer-motion` |
185
185
  | GSAP | ~25 KB | Timeline | 0 | Manual | `npm i gsap` |
186
186
  | Lottie | ~55 KB | JSON animation | After Effects | Manual | `npm i lottie-web` |
187
- | **lite-ui-fx** | **< 5 KB** | **Canvas hijack** | *50 built-in** | **Native + visual** | **`npm i @zakkster/lite-ui-fx`** |
187
+ | **lite-ui-fx** | **< 5 KB** | **Canvas hijack** | **50 built-in** | **Native + visual** | **`npm i @zakkster/lite-ui-fx`** |
188
188
 
189
- ## 🎨 Writing Custom Recipes
189
+ ## Writing Custom Recipes
190
190
 
191
191
  See the full [UIFX-RECIPE-GUIDE.md](recipes/UIFX-RECIPE-GUIDE.md) (included in the package).
192
192
 
@@ -212,7 +212,7 @@ export function MyButton() {
212
212
  }
213
213
  ```
214
214
 
215
- ## 📦 TypeScript
215
+ ## TypeScript
216
216
 
217
217
  Full TypeScript declarations are included for:
218
218
 
@@ -225,7 +225,7 @@ Full TypeScript declarations are included for:
225
225
  (Recipes are not part of the npm package, so their types are not included.)
226
226
 
227
227
 
228
- ## 📚 LLM-Friendly Documentation
228
+ ## LLM-Friendly Documentation
229
229
 
230
230
  See `llms.txt` for AI-optimized metadata and the complete recipe catalog.
231
231
 
@@ -1,3 +1,5 @@
1
+ export declare const VERSION: string;
2
+
1
3
  export type UITypeValue = 'button' | 'toggle' | 'slider';
2
4
 
3
5
  export declare const UIType: Readonly<{
package/UIFXController.js CHANGED
@@ -1,28 +1,32 @@
1
1
  /**
2
- * @zakkster/lite-ui-fx Canvas-Hijacked UI Components
2
+ * @zakkster/lite-ui-fx -- Canvas-Hijacked UI Components
3
3
  *
4
4
  * Overlays a DPR-aware canvas on top of native HTML elements (buttons,
5
5
  * checkboxes, sliders). The native element handles accessibility, focus,
6
6
  * and events. The canvas handles visuals via a pluggable recipe system.
7
7
  *
8
8
  * Architecture:
9
- * Native element (opacity:0, z-index:2) receives all pointer/keyboard events
10
- * Canvas overlay (z-index:1) renders the visual recipe
11
- * Recipe factory { init?, tick, onHover?, onLeave?, onClick?, onToggle?, onDrag?, destroy? }
9
+ * Native element (opacity:0, z-index:2) -- receives all pointer/keyboard events
10
+ * Canvas overlay (z-index:1) -- renders the visual recipe
11
+ * Recipe factory -> { init?, tick, onHover?, onLeave?, onClick?, onToggle?, onDrag?, destroy? }
12
12
  *
13
13
  * Uses:
14
- * @zakkster/lite-lerp interpolation in recipes
15
- * @zakkster/lite-random deterministic particle effects
16
- * @zakkster/lite-color OKLCH color math (optional per recipe)
14
+ * @zakkster/lite-lerp -- interpolation in recipes
15
+ * @zakkster/lite-random -- deterministic particle effects
16
+ * @zakkster/lite-color -- OKLCH color math (optional per recipe)
17
17
  *
18
18
  * Depends on: @zakkster/lite-ticker (shared RAF loop)
19
19
  */
20
20
 
21
21
  import { Ticker } from '@zakkster/lite-ticker';
22
22
 
23
- // ─────────────────────────────────────────────────────────
23
+ // Three-place version sync: this constant, package.json "version", and the
24
+ // VERSION line in llms.txt must always match. /release keeps them locked.
25
+ export const VERSION = '1.0.5';
26
+
27
+ // ---------------------------------------------------------
24
28
  // SHARED TICKER (ref-counted, one RAF for all UI components)
25
- // ─────────────────────────────────────────────────────────
29
+ // ---------------------------------------------------------
26
30
 
27
31
  let _sharedTicker = null;
28
32
  let _sharedRefs = 0;
@@ -46,9 +50,9 @@ function releaseTicker() {
46
50
  }
47
51
 
48
52
 
49
- // ─────────────────────────────────────────────────────────
53
+ // ---------------------------------------------------------
50
54
  // ELEMENT TYPES
51
- // ─────────────────────────────────────────────────────────
55
+ // ---------------------------------------------------------
52
56
 
53
57
  /** @enum {string} */
54
58
  export const UIType = Object.freeze({
@@ -58,9 +62,9 @@ export const UIType = Object.freeze({
58
62
  });
59
63
 
60
64
 
61
- // ═══════════════════════════════════════════════════════════
62
- // UIFXController The Canvas Hijacker
63
- // ═══════════════════════════════════════════════════════════
65
+ // =========================================================
66
+ // UIFXController -- The Canvas Hijacker
67
+ // =========================================================
64
68
 
65
69
  /**
66
70
  * Mount a canvas-rendered recipe onto a native HTML element.
@@ -81,12 +85,12 @@ export function mountUIFX(container, type, recipeFactory, {
81
85
  padding = 40,
82
86
  label = '',
83
87
  } = {}) {
84
- // ── Resolve dimensions ──
88
+ // -- Resolve dimensions --
85
89
  const w = width || (type === UIType.BUTTON ? 160 : type === UIType.SLIDER ? 200 : 64);
86
90
  const h = height || (type === UIType.BUTTON ? 48 : type === UIType.SLIDER ? 28 : 36);
87
91
  const dpr = window.devicePixelRatio || 1;
88
92
 
89
- // ── Create native element (invisible, accessible, receives events) ──
93
+ // -- Create native element (invisible, accessible, receives events) --
90
94
  let el;
91
95
  if (type === UIType.TOGGLE) {
92
96
  el = document.createElement('input');
@@ -124,7 +128,7 @@ export function mountUIFX(container, type, recipeFactory, {
124
128
  el.classList.add('uifx-slider');
125
129
  }
126
130
 
127
- // ── Create canvas overlay (DPR-aware) ──
131
+ // -- Create canvas overlay (DPR-aware) --
128
132
  const canvas = document.createElement('canvas');
129
133
  const cw = w + padding * 2;
130
134
  const ch = h + padding * 2;
@@ -140,7 +144,7 @@ export function mountUIFX(container, type, recipeFactory, {
140
144
  const ctx = canvas.getContext('2d');
141
145
  ctx.scale(dpr, dpr);
142
146
 
143
- // ── Wrapper ──
147
+ // -- Wrapper --
144
148
  const wrapper = document.createElement('div');
145
149
  Object.assign(wrapper.style, {
146
150
  position: 'relative', display: 'inline-block',
@@ -150,23 +154,23 @@ export function mountUIFX(container, type, recipeFactory, {
150
154
  wrapper.appendChild(canvas);
151
155
  container.appendChild(wrapper);
152
156
 
153
- // ── State ──
157
+ // -- State --
154
158
  const state = {
155
159
  hover: false,
156
160
  active: false, // pointer is down
157
161
  focused: false, // keyboard focus
158
162
  toggled: false, // checkbox state
159
- val: type === UIType.SLIDER ? 0.5 : 0, // slider value 01
163
+ val: type === UIType.SLIDER ? 0.5 : 0, // slider value 0-1
160
164
  w, h, padding, dpr,
161
165
  };
162
166
 
163
167
  const pointer = { x: -999, y: -999, vx: 0, vy: 0 };
164
168
 
165
- // ── Initialize recipe ──
169
+ // -- Initialize recipe --
166
170
  const recipe = recipeFactory();
167
171
  if (recipe.init) recipe.init(ctx, w, h, padding);
168
172
 
169
- // ── Events (all via AbortController) ──
173
+ // -- Events (all via AbortController) --
170
174
  const ac = new AbortController();
171
175
  const signal = ac.signal;
172
176
 
@@ -225,7 +229,7 @@ export function mountUIFX(container, type, recipeFactory, {
225
229
  }, { signal });
226
230
  }
227
231
 
228
- // ── Render loop (shared ticker) ──
232
+ // -- Render loop (shared ticker) --
229
233
  const ticker = acquireTicker();
230
234
  let destroyed = false;
231
235
 
@@ -242,7 +246,7 @@ export function mountUIFX(container, type, recipeFactory, {
242
246
  ctx.restore();
243
247
  });
244
248
 
245
- // ── Public API ──
249
+ // -- Public API --
246
250
  return {
247
251
  /** The native HTML element (for external state reads). */
248
252
  el,
package/llms.txt CHANGED
@@ -1,5 +1,7 @@
1
1
  # @zakkster/lite-ui-fx
2
- > Canvas-hijacked UI components with pluggable recipe system. 30 built-in recipes.
2
+ > Canvas-hijacked UI components with pluggable recipe system. 50 built-in recipes.
3
+
4
+ VERSION 1.0.5
3
5
 
4
6
  ## Install
5
7
  npm i @zakkster/lite-ui-fx
@@ -9,11 +11,11 @@ Native HTML element (opacity:0, z-index:2) handles events + a11y.
9
11
  Canvas overlay (z-index:1) renders visuals via a recipe factory function.
10
12
  Recipe = { tick(), init?(), onHover?(), onClick?(), onToggle?(), onDrag?(), destroy?() }
11
13
 
12
- ## Import Controller
14
+ ## Import -- Controller
13
15
  import { mountUIFX, UIType } from '@zakkster/lite-ui-fx';
14
16
 
15
- ## Import Recipes (3 volumes, 30 total)
16
- import { SwarmToggle, MagneticButton, SparkSlider } from ./recipes/UIFXRecipes.js';
17
+ ## Import -- Recipes (3 volumes, 50 total)
18
+ import { SwarmToggle, MagneticButton, SparkSlider } from './recipes/UIFXRecipes.js';
17
19
  import { PendulumToggle, HeartbeatButton, AuroraSlider } from './recipes/UIFXRecipes2.js';
18
20
  import { VolumeKnob, WaterLevel, TimerCountdown } from './recipes/UIFXRecipes3.js';
19
21
 
@@ -22,21 +24,21 @@ const instance = mountUIFX(container, UIType.TOGGLE, SwarmToggle, { label: 'Soun
22
24
  instance.destroy(); // cleanup
23
25
 
24
26
  ## Element Types
25
- UIType.TOGGLE <input type="checkbox" role="switch"> state.toggled, onToggle(checked)
26
- UIType.BUTTON <button> state.active, onClick(x, y, state)
27
- UIType.SLIDER <input type="range"> state.val (01), onDrag(val, velocity)
27
+ UIType.TOGGLE -> <input type="checkbox" role="switch"> -> state.toggled, onToggle(checked)
28
+ UIType.BUTTON -> <button> -> state.active, onClick(x, y, state)
29
+ UIType.SLIDER -> <input type="range"> -> state.val (0-1), onDrag(val, velocity)
28
30
 
29
31
  ## State Object (provided to tick every frame)
30
32
  { hover, active, focused, toggled, val, w, h, padding, dpr }
31
33
 
32
- ## 30 Built-in Recipes
34
+ ## 50 Built-in Recipes
33
35
 
34
- ### Vol. 1 (UIFXRecipes.js) 10 recipes
36
+ ### Vol. 1 (UIFXRecipes.js) -- 10 recipes
35
37
  Toggles: SwarmToggle, LiquidToggle, NeonPulseToggle
36
38
  Buttons: MagneticButton, ShatterButton, ConfettiButton, GlitchButton
37
39
  Sliders: SparkSlider, CosmicSlider, LaserSlider
38
40
 
39
- ### Vol. 2 (UIFXRecipes2.js) 20 recipes
41
+ ### Vol. 2 (UIFXRecipes2.js) -- 20 recipes
40
42
  Toggles: PendulumToggle, CircuitToggle, LightningToggle, DNAToggle
41
43
  Buttons: HeartbeatButton, BreathingButton, InkSplashButton, PixelDissolveButton, FireworkButton
42
44
  Sliders: AuroraSlider, WaveSlider, ElasticBandSlider, GravitySlider
@@ -45,7 +47,7 @@ Checkboxes: RippleCheck, MorphCheck
45
47
  Counters: FlameCounter, GlitchCounter
46
48
  Rating: BubbleRating
47
49
 
48
- ### Vol. 3 (UIFXRecipes3.js) 20 recipes
50
+ ### Vol. 3 (UIFXRecipes3.js) -- 20 recipes
49
51
  Knobs: VolumeKnob, CompassKnob
50
52
  Progress: RingProgress, BatteryGauge, SignalMeter
51
53
  Controls: PillTabs, Stepper, RadioOrbit
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@zakkster/lite-ui-fx",
3
- "version": "1.0.3",
4
- "description": "Canvas-hijacked UI components with a pluggable recipe system. 30 built-in recipes across toggles, buttons, sliders, knobs, loaders, checkboxes, counters, and ratings.",
3
+ "version": "1.0.5",
4
+ "description": "Canvas-hijacked UI components with a pluggable recipe system. 50 built-in recipes across toggles, buttons, sliders, knobs, loaders, checkboxes, counters, and ratings.",
5
5
  "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
6
6
  "license": "MIT",
7
7
  "type": "module",
8
+ "sideEffects": false,
8
9
  "main": "UIFXController.js",
9
10
  "types": "UIFXController.d.ts",
10
11
  "exports": {
@@ -16,7 +17,11 @@
16
17
  "files": [
17
18
  "UIFXController.js",
18
19
  "UIFXController.d.ts",
19
- "llms.txt"
20
+ "llms.txt",
21
+ "recipes/UIFX-RECIPE-GUIDE.md",
22
+ "CHANGELOG.md",
23
+ "README.md",
24
+ "LICENSE"
20
25
  ],
21
26
  "keywords": [
22
27
  "ui",
@@ -32,17 +37,22 @@
32
37
  "game",
33
38
  "a11y"
34
39
  ],
35
- "sideEffects": false,
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
36
43
  "dependencies": {
37
44
  "@zakkster/lite-ticker": "^1.0.0",
38
45
  "@zakkster/lite-lerp": "^1.0.0",
39
46
  "@zakkster/lite-random": "^1.0.0"
40
47
  },
41
48
  "devDependencies": {
42
- "vitest": "^3.0.0"
49
+ "@zakkster/lite-gc-profiler": "^1.16.0",
50
+ "@zakkster/lite-leak": "^1.10.0",
51
+ "@zakkster/lite-signal": "^1.5.1"
43
52
  },
44
53
  "scripts": {
45
- "test": "vitest run"
54
+ "test": "node --test test/*.test.mjs",
55
+ "torture": "node --expose-gc test/torture.mjs"
46
56
  },
47
57
  "homepage": "https://github.com/PeshoVurtoleta/lite-ui-fx#readme",
48
58
  "repository": {
@@ -0,0 +1,188 @@
1
+ # Writing a UI-FX Recipe
2
+
3
+ A recipe is a plain factory function that returns an object with a `tick()` method and optional lifecycle hooks. The UIFXController handles events, DPR scaling, and the render loop -- the recipe just draws.
4
+
5
+ ## Minimal Recipe (copy-paste starter)
6
+
7
+ ```javascript
8
+ export function MyToggle() {
9
+ let knobX = 18;
10
+
11
+ return {
12
+ tick(ctx, dt, now, state, pointer) {
13
+ const { w, h, toggled } = state;
14
+
15
+ // Animate knob position
16
+ const target = toggled ? w - 18 : 18;
17
+ knobX += (target - knobX) * dt * 12;
18
+
19
+ // Draw track
20
+ ctx.fillStyle = toggled ? 'rgba(110,231,182,.2)' : 'rgba(255,255,255,.06)';
21
+ ctx.beginPath();
22
+ ctx.roundRect(0, 0, w, h, h / 2);
23
+ ctx.fill();
24
+
25
+ // Draw knob
26
+ ctx.fillStyle = toggled ? '#6ee7b6' : '#999';
27
+ ctx.beginPath();
28
+ ctx.arc(knobX, h / 2, 14, 0, Math.PI * 2);
29
+ ctx.fill();
30
+ },
31
+ };
32
+ }
33
+ ```
34
+
35
+ ## The Recipe Interface
36
+
37
+ ```typescript
38
+ interface Recipe {
39
+ // REQUIRED -- called every frame (60fps)
40
+ tick(ctx: CanvasRenderingContext2D, dt: number, now: number, state: State, pointer: Pointer): void;
41
+
42
+ // OPTIONAL -- called once after mount
43
+ init?(ctx: CanvasRenderingContext2D, w: number, h: number, padding: number): void;
44
+
45
+ // OPTIONAL -- event hooks
46
+ onHover?(state: State, pointer: Pointer): void;
47
+ onLeave?(state: State, pointer: Pointer): void;
48
+ onClick?(x: number, y: number, state: State): void;
49
+ onToggle?(checked: boolean, state: State): void; // Toggles only
50
+ onDrag?(value: number, velocity: number, state: State): void; // Sliders only
51
+
52
+ // OPTIONAL -- cleanup
53
+ destroy?(): void;
54
+ }
55
+ ```
56
+
57
+ ## The State Object
58
+
59
+ The controller provides this every frame:
60
+
61
+ ```javascript
62
+ {
63
+ hover: boolean, // Pointer is inside the element
64
+ active: boolean, // Pointer is pressed down
65
+ focused: boolean, // Element has keyboard focus
66
+ toggled: boolean, // Checkbox checked state (toggles)
67
+ val: number, // 0--1 slider value (sliders)
68
+ w: number, // Element width in CSS pixels
69
+ h: number, // Element height
70
+ padding: number, // Canvas overflow padding
71
+ dpr: number, // Device pixel ratio
72
+ }
73
+ ```
74
+
75
+ ## The Pointer Object
76
+
77
+ ```javascript
78
+ {
79
+ x: number, // X position relative to element's top-left
80
+ y: number, // Y position relative to element's top-left
81
+ vx: number, // X velocity (pixels since last move)
82
+ vy: number, // Y velocity
83
+ }
84
+ ```
85
+
86
+ ## Coordinate System
87
+
88
+ The canvas is larger than the element (by `padding` on each side) to allow particles to overflow. The controller translates the context so that `(0, 0)` is the element's top-left corner. You draw as if the element starts at origin.
89
+
90
+ ```
91
+ Canvas memory:
92
+ +-------------------------------+
93
+ | padding |
94
+ | +-------------------+ |
95
+ | | (0,0) (w,0)| |
96
+ | | | |
97
+ | | Your drawing space | |
98
+ | | | |
99
+ | | (0,h) (w,h)| |
100
+ | +-------------------+ |
101
+ | padding |
102
+ +-------------------------------+
103
+ ```
104
+
105
+ Particles that fly outside `(0,0)--(w,h)` are visible because the canvas extends by `padding` in each direction. Default padding is 40px.
106
+
107
+ ## Mounting a Recipe
108
+
109
+ ```javascript
110
+ import { mountUIFX, UIType } from './UIFXController.js';
111
+ import { MyToggle } from './my-recipe.js';
112
+
113
+ const instance = mountUIFX(
114
+ document.getElementById('container'),
115
+ UIType.TOGGLE,
116
+ MyToggle, // Factory function (NOT called -- the controller calls it)
117
+ { label: 'Dark mode', width: 64, height: 36 }
118
+ );
119
+
120
+ // Later:
121
+ instance.destroy();
122
+ ```
123
+
124
+ ## Three Element Types
125
+
126
+ | Type | Native Element | Recipe Gets | Key State |
127
+ |------|----------------|-------------|-----------|
128
+ | `UIType.TOGGLE` | `<input type="checkbox" role="switch">` | `onToggle(checked)` | `state.toggled` |
129
+ | `UIType.BUTTON` | `<button>` | `onClick(x, y, state)` | `state.active` |
130
+ | `UIType.SLIDER` | `<input type="range">` | `onDrag(val, velocity)` | `state.val` (0--1) |
131
+
132
+ ## Using @zakkster Libraries in Recipes
133
+
134
+ Recipes can import any @zakkster library:
135
+
136
+ ```javascript
137
+ import { lerp, clamp, easeOut } from '@zakkster/lite-lerp';
138
+ import { Random } from '@zakkster/lite-random';
139
+ import { toCssOklch, lerpOklch } from '@zakkster/lite-color';
140
+
141
+ export function OklchToggle() {
142
+ const rng = new Random(42);
143
+ const off = { l: 0.4, c: 0.05, h: 250 };
144
+ const on = { l: 0.7, c: 0.2, h: 160 };
145
+ let t = 0;
146
+
147
+ return {
148
+ tick(ctx, dt, now, state) {
149
+ t = lerp(t, state.toggled ? 1 : 0, dt * 8);
150
+ const color = lerpOklch(off, on, easeOut(t));
151
+
152
+ ctx.fillStyle = toCssOklch(color);
153
+ ctx.beginPath();
154
+ ctx.roundRect(0, 0, state.w, state.h, state.h / 2);
155
+ ctx.fill();
156
+ // ...
157
+ },
158
+ };
159
+ }
160
+ ```
161
+
162
+ ## Accessibility Checklist
163
+
164
+ The controller makes the native element invisible but fully accessible. Your recipe should add visual feedback:
165
+
166
+ 1. **Focus ring** -- draw a dashed outline when `state.focused` is true (keyboard users)
167
+ 2. **State label** -- show "ON"/"OFF" or a value percentage so the user sees the state
168
+ 3. **Press feedback** -- scale down on `state.active`, spring back on release
169
+ 4. **Hover feedback** -- change color/glow when `state.hover` is true
170
+
171
+ ```javascript
172
+ // Focus ring helper
173
+ if (state.focused) {
174
+ ctx.strokeStyle = 'rgba(110,231,182,.6)';
175
+ ctx.lineWidth = 2;
176
+ ctx.setLineDash([4, 3]);
177
+ ctx.strokeRect(-2, -2, state.w + 4, state.h + 4);
178
+ ctx.setLineDash([]);
179
+ }
180
+ ```
181
+
182
+ ## Performance Rules
183
+
184
+ 1. **Never allocate in `tick()`** -- pre-allocate TypedArrays in the factory closure or `init()`
185
+ 2. **Use `splice()` sparingly** -- for small particle arrays (< 100) it's fine. For larger pools, use a dead-flag pattern
186
+ 3. **Reset composite operation** -- if you set `ctx.globalCompositeOperation = 'screen'`, reset to `'source-over'` before returning
187
+ 4. **Reset shadow** -- `ctx.shadowBlur = 0` after drawing glowing elements
188
+ 5. **Use the `dt` parameter** -- all motion must be `value * dt`, not `value` per frame. This ensures consistent speed regardless of frame rate.