@ktfth/stickjs 3.0.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,169 @@
1
+ # Changelog
2
+
3
+ All notable changes to Stick.js will be documented here.
4
+
5
+ ## v3.1.0 — CLI + CDN + Vercel docs
6
+
7
+ ### New features
8
+ - **CLI** — `npx stickjs add <component>` copies component files to `./stick-ui/` (shadcn/ui-style)
9
+ - **`npx stickjs list`** — list all 15 available components
10
+ - **`npx stickjs add --all`** — add all components at once
11
+ - **`--force` flag** — overwrite existing files
12
+ - **CDN** — all files available via jsdelivr and unpkg after npm publish
13
+ - **Release script** — `npm run release` minifies, publishes to npm, and deploys docs to Vercel
14
+
15
+ ### Infrastructure
16
+ - Vercel deployment for documentation site (`docs/vercel.json`)
17
+ - `bin/registry.json` component manifest
18
+ - `bin/stickjs.js` zero-dependency CLI (Node built-ins only)
19
+
20
+ ## v3.0.0 — core hardening + a11y + transitions
21
+
22
+ ### New features
23
+ - **Error boundary** — handler failures are caught and logged; one failing handler no longer breaks siblings on the same element
24
+ - **`Stick.fire(el, handler, param?)`** — invoke any handler programmatically without events (chainable)
25
+ - **`Stick.on(event, fn)`** — lifecycle hooks for `bind` and `unbind` events (chainable)
26
+ - **`data-stick-group="name"`** — mutual exclusivity: `show`/`toggle`/`show-modal` hides others in the same group
27
+ - **`data-stick-transition="name"`** — CSS enter/leave transitions: adds `name-enter-active` / `name-leave-active` classes with automatic cleanup
28
+ - **`set-aria` handler** — set ARIA attributes declaratively, auto-prefixes `aria-`
29
+ - **`toggle-aria` handler** — flip ARIA boolean attributes (`true` ↔ `false`)
30
+
31
+ ### Improvements
32
+ - `show`/`hide`/`toggle` auto-manage `aria-expanded` on trigger element
33
+ - All handler invocations wrapped in try/catch (error boundary)
34
+
35
+ ## [2.9.0] — 2026-02-19
36
+
37
+ ### Added (iter 20)
38
+ - `clone-template` now resolves `{{tokens}}` from the trigger element inside cloned text nodes and attributes — dynamic list generation fully declarative
39
+ - `sort` handler — sort `target.children` alphabetically by textContent (default) or any `data-*`/attribute key (numeric-aware)
40
+ - `count` handler — set `target.textContent` to count of elements matching CSS selector param, or `target.children.length` when param is empty
41
+ - `data-stick-swap` now accepts `"prepend"` (alias for `afterbegin`) and `"append"` (alias for `beforeend`) for more natural naming
42
+ - 6 new browser tests
43
+
44
+ ## [2.8.0] — 2026-02-19
45
+
46
+ ### Added (iter 19)
47
+ - `data-stick-delegate="selector"` — event delegation: attach one listener to a container, fire for matching descendants. `el` in the handler is the matched child. Relative targets (`parent`, `next`, `closest:`) resolve from the matched child.
48
+ - `{{url:key}}` interpolation — reads `URLSearchParams` from the current page URL at event time
49
+ - `scroll-top` handler — `window.scrollTo({ top: 0, behavior: 'smooth' })` — back-to-top in one attribute
50
+ - 9 new browser tests
51
+
52
+ ## [2.7.0] — 2026-02-19
53
+
54
+ ### Added (iter 18)
55
+ - `parse()` shorthand — `"event:handler"` (no trailing colon) now valid; trailing colon remains optional
56
+ - `watch` synthetic event — fires immediately on bind, then on every attribute mutation of `el` (MutationObserver)
57
+ - `select` handler — `target.select()` — focus and select all text in an input/textarea
58
+ - `history-push` handler — `window.history.pushState({}, '', param)` — update URL without page reload
59
+ - 8 new browser tests covering all new features
60
+
61
+ ## [2.6.0] — 2026-02-20
62
+
63
+ ### Added (iter 17)
64
+ - `{{chars}}` interpolation — `el.value.length` (character counter for inputs/textareas)
65
+ - `Stick.debug(true?)` — enable verbose console logging (bind + fire events); chainable
66
+ - Comprehensive demo rewrite (`index.html`) — 16 sections covering every major feature:
67
+ tabs with siblings, native dialog, checkboxes, counters, clone-template,
68
+ animate, store+ready, dispatch, plugin system, multi-target, intersect, observe()
69
+ - 4 new browser tests for `{{chars}}` and `Stick.debug()`
70
+
71
+ ## [2.5.0] — 2026-02-20
72
+
73
+ ### Added (iter 16)
74
+ - `Stick.use(plugin)` — plugin system: fn(stick) | { install } | { name: fn, … }
75
+ - `Stick.unbind(el)` — remove all listeners from el, clear data-stick-bound (enables clean SPA unmount)
76
+ - `{{index}}` interpolation — element's position among parent's children (0-based)
77
+ - `{{length}}` interpolation — `el.children.length`
78
+ - `{{attr}}` interpolation — any attribute via `el.getAttribute(attr)` (always worked, now documented)
79
+ - Updated `stick.d.ts` — full TypeScript coverage: `StickPlugin`, `StickSyntheticEvent`, `use`, `unbind`
80
+ - Internal: `listenerMap` WeakMap tracks attached listeners for `unbind` cleanup
81
+ - 12 new browser tests
82
+
83
+ ## [2.4.0] — 2026-02-20
84
+
85
+ ### Added (iter 15)
86
+ - Multi-target support: `"siblings"` and `"all:sel"` target keywords — handler called once per matched element
87
+ - `ready` synthetic event — fires immediately on `bind()`, before any user interaction
88
+ - `animate` handler — adds CSS class, auto-removes after `animationend`
89
+ - Internal: `resolveTarget` → `resolveTargets` (returns array); enables true multi-target dispatch
90
+ - 8 new browser tests (siblings, all:, ready, animate)
91
+
92
+ ## [2.3.0] — 2026-02-20
93
+
94
+ ### Added (iter 14)
95
+ - `toggle-attr` handler — toggle attribute presence (great for `open`, `disabled`, `aria-expanded`)
96
+ - `clone-template` handler — clone `<template>` content into target; auto-binds new elements
97
+ - `data-stick-prevent` modifier — always call `event.preventDefault()` (saves a stacked slot on forms)
98
+ - `data-stick-stop` modifier — always call `event.stopPropagation()`
99
+ - `"self"` target keyword — explicit self-reference in `data-stick-target`
100
+ - `Stick.remove(name)` API — unregister a handler, chainable
101
+ - 10 new browser tests
102
+
103
+ ## [2.2.0] — 2026-02-20
104
+
105
+ ### Added (iter 13)
106
+ - `data-stick-key="Enter"` modifier — filter keyboard events by key name (comma-separated)
107
+ - `open` handler — `window.open(param, '_blank', 'noopener')`
108
+ - `back` / `forward` handlers — history navigation
109
+ - `show-modal` / `close-modal` handlers — native `<dialog>` support
110
+ - 10 new browser tests
111
+
112
+ ## [2.1.0] — 2026-02-20
113
+
114
+ ### Added (iter 12)
115
+ - `check`, `uncheck`, `toggle-check` handlers — checkbox control
116
+ - `increment`, `decrement` handlers — counter mutation on text/input targets (optional step param)
117
+ - `set-data` handler — `target.dataset[key] = value` (param: `"key:value"`)
118
+ - `data-stick-passive` modifier — `addEventListener` with `passive: true` (performance for scroll/touch)
119
+ - `data-stick-capture` modifier — `addEventListener` with `capture: true`
120
+ - 13 new browser tests covering all new handlers and modifiers
121
+
122
+ ## [2.0.0] — 2026-02-20
123
+
124
+ Complete rewrite. Zero dependencies. Vanilla JS.
125
+
126
+ ### Added
127
+ - `data-stick="event:handler:param"` core syntax
128
+ - `data-stick-target` — apply handler to another element
129
+ - Relative targets: `"next"`, `"prev"`, `"parent"`, `"closest:sel"`
130
+ - `data-stick-2/3/4/5` — stack multiple behaviors on one element
131
+ - `data-stick-target-2/3/4/5` — per-slot targets for stacked behaviors
132
+ - `data-stick-once` — remove listener after first fire
133
+ - `data-stick-debounce="ms"` — debounce handler
134
+ - `data-stick-throttle="ms"` — throttle handler
135
+ - `data-stick-confirm="msg"` — window.confirm gate
136
+ - `data-stick-bound` attribute — prevents double-binding; remove to rebind
137
+ - `{{value}}`, `{{text}}`, `{{id}}`, `{{checked}}`, `{{data-*}}` param interpolation
138
+ - `intersect` synthetic event (IntersectionObserver)
139
+ - `Stick.observe()` — MutationObserver auto-bind
140
+ - `Stick.version`, `Stick.handlers`, `Stick.parse()`, `Stick.bind()`
141
+ - UMD wrapper — works as `<script>`, `require()`, or `import`
142
+ - TypeScript types (`stick.d.ts`)
143
+ - `llms.txt` — machine-readable API reference for LLMs
144
+
145
+ ### Built-in handlers (40)
146
+ `show`, `hide`, `toggle`, `add-class`, `remove-class`, `toggle-class`,
147
+ `set-text`, `set-html`, `set-value`, `clear`, `set-attr`, `remove-attr`,
148
+ `set-style`, `focus`, `scroll-to`, `copy`, `navigate`, `reload`, `print`,
149
+ `submit`, `reset`, `prevent`, `stop`, `dispatch`, `emit`, `remove`,
150
+ `disable`, `enable`, `store`, `restore`, `fetch`, `log`, `alert`, `wait`
151
+
152
+ ### fetch handler options
153
+ - `data-stick-method` — HTTP method (default: GET)
154
+ - `data-stick-swap` — insertion mode (innerHTML, outerHTML, beforeend, …)
155
+ - `data-stick-loading` — button label while in-flight
156
+ - `data-stick-headers` — JSON string of fetch headers
157
+ - `data-stick-json` — JSON body template with interpolation (auto-sets Content-Type)
158
+ - `data-stick-error` — element to display fetch errors
159
+
160
+ ### Removed
161
+ - jQuery dependency
162
+ - Old `$.handler:param` syntax
163
+ - `build/` directory (Ant/Java build tools)
164
+ - All HTML5 Boilerplate scaffolding
165
+
166
+ ## [1.x] — 2011
167
+
168
+ Original experimental release. jQuery-based.
169
+ Syntax: `data-stick="$.handler:param"`. Handlers: alert, confirm, log, validate.
package/README.md ADDED
@@ -0,0 +1,449 @@
1
+ # Stick.js
2
+
3
+ **Declarative behavior for HTML elements.** Zero dependencies, ~220 lines of vanilla JS.
4
+
5
+ Drop in one `<script>` and annotate your HTML — no JavaScript glue required for the most common UI interactions.
6
+
7
+ ```html
8
+ <script src="stick.js"></script>
9
+
10
+ <button data-stick="click:toggle" data-stick-target="#menu">Menu</button>
11
+ <input data-stick="input:fetch:/api/search?q={{value}}"
12
+ data-stick-target="#results" data-stick-debounce="300">
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Install
18
+
19
+ **Script tag** (put before your custom-handler `<script>`):
20
+ ```html
21
+ <script src="stick.js"></script>
22
+ ```
23
+
24
+ **npm:**
25
+ ```bash
26
+ npm install stickjs
27
+ ```
28
+
29
+ **ESM / CommonJS:**
30
+ ```js
31
+ import Stick from 'stickjs';
32
+ const Stick = require('stickjs');
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Syntax
38
+
39
+ ```
40
+ data-stick="event:handler:param"
41
+ ```
42
+
43
+ | Attribute | Purpose |
44
+ |-----------|---------|
45
+ | `data-stick="event:handler:param"` | Primary behavior |
46
+ | `data-stick-2="event:handler:param"` | Stack a 2nd behavior (up to `data-stick-5`) |
47
+ | `data-stick-target="#sel"` | Apply handler to another element |
48
+ | `data-stick-target-2="#other"` | Per-slot target for `data-stick-2` (also `-3`, `-4`, `-5`) |
49
+ | `data-stick-once` | Remove listener after first fire |
50
+ | `data-stick-debounce="300"` | Debounce handler by N ms |
51
+ | `data-stick-throttle="300"` | Throttle handler by N ms |
52
+ | `data-stick-confirm="message"` | `window.confirm()` gate before handler |
53
+ | `data-stick-key="Enter"` | Only fire when `event.key` matches (comma-separated, e.g. `"Enter,Escape"`) |
54
+ | `data-stick-prevent` | Always call `event.preventDefault()` before handler (e.g. on `<form>`) |
55
+ | `data-stick-stop` | Always call `event.stopPropagation()` before handler |
56
+ | `data-stick-passive` | Add event listener as passive (use for `scroll`/`touchmove`) |
57
+ | `data-stick-capture` | Add event listener in capture phase |
58
+ | `data-stick-delegate=".item"` | Event delegation — listen on `el`, fire only for matching descendant elements. Handler receives the matched child as `el`. |
59
+ | `data-stick-method="POST"` | HTTP method for `fetch` handler |
60
+ | `data-stick-swap="beforeend"` | Fetch insertion mode (default: `innerHTML`). Aliases: `prepend` = `afterbegin`, `append` = `beforeend` |
61
+ | `data-stick-loading="Loading…"` | Button label while fetch is in-flight |
62
+ | `data-stick-headers='{"Authorization":"Bearer TOKEN"}'` | Fetch headers (JSON string) |
63
+ | `data-stick-json='{"key":"{{value}}"}'` | JSON body for fetch POST (auto-sets Content-Type) |
64
+ | `data-stick-error="#el"` | Element to display fetch errors |
65
+
66
+ ### Target selectors
67
+
68
+ `data-stick-target` accepts:
69
+
70
+ | Value | Resolves to |
71
+ |-------|------------|
72
+ | `"#id"` or any CSS selector | `document.querySelector(sel)` |
73
+ | `"self"` | The trigger element itself (explicit) |
74
+ | `"siblings"` | All sibling elements (same parent, excluding el) |
75
+ | `"all:.cls"` | All elements matching a CSS selector (`querySelectorAll`) |
76
+ | `"next"` | `el.nextElementSibling` |
77
+ | `"prev"` | `el.previousElementSibling` |
78
+ | `"parent"` | `el.parentElement` |
79
+ | `"closest:form"` | `el.closest("form")` |
80
+
81
+ ### Param interpolation
82
+
83
+ Tokens in `param` are resolved at event time from the **trigger element**:
84
+
85
+ | Token | Value |
86
+ |-------|-------|
87
+ | `{{value}}` | `el.value` |
88
+ | `{{text}}` | `el.textContent` |
89
+ | `{{id}}` | `el.id` |
90
+ | `{{name}}` | `el.name` |
91
+ | `{{checked}}` | `"true"` or `"false"` |
92
+ | `{{index}}` | Position among parent's children (0-based) |
93
+ | `{{length}}` | `el.children.length` |
94
+ | `{{chars}}` | `el.value.length` or `textContent.length` — character count |
95
+ | `{{data-foo}}` | `el.dataset.foo` |
96
+ | `{{url:key}}` | `URLSearchParams` value from the current page URL (e.g. `{{url:q}}` → `?q=value`) |
97
+ | `{{href}}`, `{{src}}`, etc. | Any attribute via `el.getAttribute(name)` |
98
+
99
+ ### Synthetic events
100
+
101
+ | Event | Trigger |
102
+ |-------|---------|
103
+ | `ready` | Fires immediately on bind — use to initialize state |
104
+ | `watch` | Fires immediately on bind, then on every attribute mutation of `el` (MutationObserver) |
105
+ | `intersect` | Element enters the viewport (IntersectionObserver) |
106
+
107
+ ---
108
+
109
+ ## Built-in handlers
110
+
111
+ ### Visibility
112
+
113
+ | Handler | Effect |
114
+ |---------|--------|
115
+ | `show` | `target.hidden = false` |
116
+ | `hide` | `target.hidden = true` |
117
+ | `toggle` | Toggle `target.hidden` |
118
+
119
+ ### CSS classes
120
+
121
+ | Handler | Effect |
122
+ |---------|--------|
123
+ | `add-class` | `target.classList.add(param)` |
124
+ | `remove-class` | `target.classList.remove(param)` |
125
+ | `toggle-class` | `target.classList.toggle(param)` |
126
+
127
+ ### Content
128
+
129
+ | Handler | Effect |
130
+ |---------|--------|
131
+ | `set-text` | `target.textContent = param` |
132
+ | `set-html` | `target.innerHTML = param` |
133
+ | `set-value` | `target.value = param` |
134
+ | `clear` | `target.textContent = ""` |
135
+
136
+ ### Attributes & styles
137
+
138
+ | Handler | Effect |
139
+ |---------|--------|
140
+ | `set-attr` | `target.setAttribute(name, val)` — param: `"name:value"` |
141
+ | `remove-attr` | `target.removeAttribute(param)` |
142
+ | `toggle-attr` | Toggle attribute presence — param is attr name (e.g. `"disabled"`, `"open"`) |
143
+ | `set-style` | `target.style[prop] = val` — param: `"property:value"` |
144
+
145
+ ### Focus, scroll & selection
146
+
147
+ | Handler | Effect |
148
+ |---------|--------|
149
+ | `focus` | `target.focus()` |
150
+ | `scroll-to` | `target.scrollIntoView({ behavior: 'smooth' })` |
151
+ | `select` | `target.select()` — select all text in an input/textarea |
152
+
153
+ ### Clipboard & navigation
154
+
155
+ | Handler | Effect |
156
+ |---------|--------|
157
+ | `copy` | `navigator.clipboard.writeText(param \|\| el.textContent)` |
158
+ | `navigate` | `window.location.href = param` |
159
+ | `open` | `window.open(param, '_blank', 'noopener')` |
160
+ | `back` | `window.history.back()` |
161
+ | `forward` | `window.history.forward()` |
162
+ | `history-push` | `window.history.pushState({}, '', param)` — update URL without reload |
163
+ | `scroll-top` | `window.scrollTo({ top: 0, behavior: 'smooth' })` — back-to-top |
164
+ | `reload` | `window.location.reload()` |
165
+ | `print` | `window.print()` |
166
+
167
+ ### Forms
168
+
169
+ | Handler | Effect |
170
+ |---------|--------|
171
+ | `submit` | Submit closest `<form>` |
172
+ | `reset` | Reset closest `<form>` |
173
+
174
+ ### Events
175
+
176
+ | Handler | Effect |
177
+ |---------|--------|
178
+ | `prevent` | `event.preventDefault()` |
179
+ | `stop` | `event.stopPropagation()` |
180
+ | `dispatch` / `emit` | `target.dispatchEvent(new CustomEvent(param, { bubbles: true, detail: { source: el } }))` |
181
+
182
+ ### DOM
183
+
184
+ | Handler | Effect |
185
+ |---------|--------|
186
+ | `remove` | `target.remove()` |
187
+ | `disable` | `target.disabled = true` |
188
+ | `enable` | `target.disabled = false` |
189
+
190
+ ### Checkboxes
191
+
192
+ | Handler | Effect |
193
+ |---------|--------|
194
+ | `check` | `target.checked = true` |
195
+ | `uncheck` | `target.checked = false` |
196
+ | `toggle-check` | Toggle `target.checked` |
197
+
198
+ ### Counters
199
+
200
+ | Handler | Effect |
201
+ |---------|--------|
202
+ | `increment` | Add `param` (default `1`) to `target` value or textContent |
203
+ | `decrement` | Subtract `param` (default `1`) from `target` value or textContent |
204
+
205
+ ### Data attributes
206
+
207
+ | Handler | Effect |
208
+ |---------|--------|
209
+ | `set-data` | `target.dataset[key] = val` — param: `"key:value"` |
210
+
211
+ ### Native dialog
212
+
213
+ | Handler | Effect |
214
+ |---------|--------|
215
+ | `show-modal` | `target.showModal()` — open native `<dialog>` as modal |
216
+ | `close-modal` | `target.close(param?)` — close native `<dialog>` |
217
+
218
+ ### Storage
219
+
220
+ | Handler | Effect |
221
+ |---------|--------|
222
+ | `store` | `localStorage.setItem(key, val)` — param: `"key:value"` |
223
+ | `restore` | `localStorage.getItem(param)` → set target `.value` or `.textContent` |
224
+
225
+ ### Templates & animation
226
+
227
+ | Handler | Effect |
228
+ |---------|--------|
229
+ | `clone-template` | Clone `<template>` matched by param selector, resolve `{{tokens}}` from `el`, append to target. Auto-binds new elements. |
230
+ | `animate` | `target.classList.add(param)` — removes the class automatically after `animationend` |
231
+ | `sort` | Sort `target.children` by textContent (default) or param key (e.g. `"data-price"`) — numeric-aware |
232
+ | `count` | Set `target.textContent` to count of elements matching CSS param, or `target.children.length` when empty |
233
+
234
+ ### Network
235
+
236
+ | Handler | Notes |
237
+ |---------|-------|
238
+ | `fetch` | Fetch `param` URL, inject response HTML into `target`. Supports `data-stick-method`, `data-stick-swap`, `data-stick-loading`, `data-stick-headers`, `data-stick-json`, `data-stick-error`. |
239
+
240
+ ### Debug
241
+
242
+ | Handler | Effect |
243
+ |---------|--------|
244
+ | `log` | `console.log('[Stick]', param)` |
245
+ | `alert` | `window.alert(param)` |
246
+
247
+ ---
248
+
249
+ ## Examples
250
+
251
+ ```html
252
+ <!-- Toggle menu open/closed -->
253
+ <button data-stick="click:toggle-class:open" data-stick-target="#menu">Menu</button>
254
+
255
+ <!-- Show panel on click, only once -->
256
+ <button data-stick="click:show" data-stick-target="#welcome" data-stick-once>
257
+ Show welcome
258
+ </button>
259
+
260
+ <!-- Search with debounce -->
261
+ <input data-stick="input:fetch:/api/search?q={{value}}"
262
+ data-stick-target="#results"
263
+ data-stick-debounce="300"
264
+ placeholder="Search…">
265
+
266
+ <!-- Fetch + append (load more) -->
267
+ <button data-stick="click:fetch:/api/posts?page={{data-page}}"
268
+ data-stick-target="#list"
269
+ data-stick-swap="beforeend"
270
+ data-stick-loading="Loading…"
271
+ data-page="2">Load more</button>
272
+
273
+ <!-- Confirm before destructive action -->
274
+ <button data-stick="click:fetch:/api/item/42"
275
+ data-stick-method="DELETE"
276
+ data-stick-target="parent"
277
+ data-stick-confirm="Delete this item?">Delete</button>
278
+
279
+ <!-- POST JSON -->
280
+ <button data-stick="click:fetch:/api/users"
281
+ data-stick-method="POST"
282
+ data-stick-json='{"name":"{{value}}"}'
283
+ data-stick-target="#result">Create user</button>
284
+
285
+ <!-- Lazy-load on scroll into view -->
286
+ <section data-stick="intersect:fetch:/api/widget"
287
+ data-stick-target="#widget-output"
288
+ data-stick-once>Loading widget…</section>
289
+
290
+ <!-- Stack behaviors with different targets -->
291
+ <button
292
+ data-stick="click:add-class:loading" data-stick-target="#submit-btn"
293
+ data-stick-2="click:show" data-stick-target-2="#spinner"
294
+ data-stick-3="click:fetch:/api/save" data-stick-target-3="#response">
295
+ Save
296
+ </button>
297
+
298
+ <!-- Dispatch event for other components to react -->
299
+ <li data-stick="click:emit:item-selected" data-id="42">Item 42</li>
300
+
301
+ <!-- Navigate to sibling (no ID needed) -->
302
+ <button data-stick="click:toggle" data-stick-target="next">Toggle next</button>
303
+ <div>This toggles</div>
304
+
305
+ <!-- Native dialog modal -->
306
+ <button data-stick="click:show-modal:" data-stick-target="#my-dialog">Open</button>
307
+ <dialog id="my-dialog">
308
+ <p>Hello from dialog</p>
309
+ <button data-stick="click:close-modal:" data-stick-target="closest:dialog">Close</button>
310
+ </dialog>
311
+
312
+ <!-- Submit form on Enter key -->
313
+ <input data-stick="keydown:submit:" data-stick-key="Enter">
314
+
315
+ <!-- Increment / decrement counter -->
316
+ <button data-stick="click:decrement:" data-stick-target="#count">−</button>
317
+ <span id="count">0</span>
318
+ <button data-stick="click:increment:" data-stick-target="#count">+</button>
319
+
320
+ <!-- Tab interface — no JS required -->
321
+ <nav>
322
+ <button class="tab active"
323
+ data-stick="click:add-class:active"
324
+ data-stick-2="click:remove-class:active" data-stick-target-2="siblings">Tab 1</button>
325
+ <button class="tab"
326
+ data-stick="click:add-class:active"
327
+ data-stick-2="click:remove-class:active" data-stick-target-2="siblings">Tab 2</button>
328
+ </nav>
329
+
330
+ <!-- Restore stored preference on page load -->
331
+ <select data-stick="ready:restore:theme-pref" data-stick-target="self"
332
+ data-stick-2="change:store:theme-pref:{{value}}">
333
+ <option>light</option><option>dark</option>
334
+ </select>
335
+
336
+ <!-- Animate on click -->
337
+ <button data-stick="click:animate:bounce" data-stick-target="#icon">Bounce</button>
338
+ <span id="icon">🎉</span>
339
+
340
+ <!-- clone-template with interpolation — add items from input, no JS needed -->
341
+ <template id="task-tpl">
342
+ <li>{{value}} <button data-stick="click:remove" data-stick-target="parent">×</button></li>
343
+ </template>
344
+ <input id="task-input" placeholder="New task"
345
+ data-stick="keydown:clone-template:#task-tpl"
346
+ data-stick-key="Enter"
347
+ data-stick-target="#task-list"
348
+ data-stick-2="keydown:set-value:"
349
+ data-stick-target-2="self">
350
+ <ul id="task-list"></ul>
351
+
352
+ <!-- Sort a list alphabetically -->
353
+ <button data-stick="click:sort" data-stick-target="#task-list">Sort A–Z</button>
354
+
355
+ <!-- Count items -->
356
+ <span data-stick="ready:count:.task" data-stick-target="self"></span>
357
+
358
+ <!-- Event delegation — single listener handles dynamic list items -->
359
+ <ul data-stick="click:remove" data-stick-target="parent" data-stick-delegate=".item">
360
+ <li class="item">Task A <button>×</button></li>
361
+ <li class="item">Task B <button>×</button></li>
362
+ </ul>
363
+
364
+ <!-- Back to top -->
365
+ <button data-stick="click:scroll-top">Back to top</button>
366
+
367
+ <!-- Pre-fill search from URL ?q= param -->
368
+ <input data-stick="ready:set-value:{{url:q}}" data-stick-target="self">
369
+ ```
370
+
371
+ ---
372
+
373
+ ## JavaScript API
374
+
375
+ ```js
376
+ Stick.version // "2.9.0"
377
+
378
+ Stick.add(name, fn) // register a handler — chainable
379
+ Stick.remove(name) // unregister a handler — chainable
380
+ Stick.use(plugin) // register a plugin — chainable
381
+ Stick.debug(true?) // enable verbose console logging — chainable
382
+ // plugin: fn(stick) | { install(stick) } | { 'name': fn, … }
383
+ Stick.unbind(el) // remove all listeners from el, clear data-stick-bound — chainable
384
+ // fn signature: (el, param, event, target) => void | Promise<void>
385
+ // el — trigger element (has data-stick)
386
+ // param — string after second colon, {{tokens}} already resolved
387
+ // event — DOM Event or synthetic { type: 'intersect', entry }
388
+ // target — resolved data-stick-target element, or el
389
+
390
+ Stick.handlers // frozen snapshot: { name: fn, … }
391
+
392
+ Stick.init(root?) // scan and bind [data-stick] under root — chainable
393
+ // default: document
394
+
395
+ Stick.observe(root?) // MutationObserver: auto-bind new [data-stick] elements
396
+ // called automatically on load; default: document.body
397
+
398
+ Stick.parse(value) // parse "event:handler:param" → { event, handler, param } | null
399
+
400
+ Stick.bind(el) // bind a single element
401
+ ```
402
+
403
+ ### Rebinding
404
+
405
+ Elements are marked `data-stick-bound` after binding to prevent double-bind. Remove it to force rebind:
406
+
407
+ ```js
408
+ el.removeAttribute('data-stick-bound');
409
+ Stick.bind(el);
410
+ ```
411
+
412
+ ### Script placement
413
+
414
+ ```html
415
+ <script src="stick.js"></script> ← here
416
+ <script>
417
+ Stick.add('my-handler', fn); ← register before DOMContentLoaded fires
418
+ </script>
419
+ <!-- data-stick elements anywhere in body -->
420
+ ```
421
+
422
+ `stick.js` queues `Stick.init()` on `DOMContentLoaded`, so any `Stick.add()` calls in subsequent `<script>` tags are picked up automatically.
423
+
424
+ ---
425
+
426
+ ## Testing
427
+
428
+ Open `test/stick.test.html` in a browser. Covers 55+ assertions across all handlers, modifiers, and edge cases.
429
+
430
+ ---
431
+
432
+ ## LLM-friendly
433
+
434
+ See `llms.txt` for a machine-readable reference optimised for LLM context windows.
435
+
436
+ | Intent | HTML |
437
+ |--------|------|
438
+ | "toggle sidebar on click" | `data-stick="click:toggle" data-stick-target="#sidebar"` |
439
+ | "search as you type, wait 300ms" | `data-stick="input:fetch:/api/search?q={{value}}" data-stick-debounce="300" data-stick-target="#results"` |
440
+ | "confirm before delete" | `data-stick="click:fetch:/api/items/1" data-stick-method="DELETE" data-stick-confirm="Delete?" data-stick-target="parent"` |
441
+ | "lazy-load when visible" | `data-stick="intersect:fetch:/api/widget" data-stick-once data-stick-target="#out"` |
442
+ | "copy link on click" | `data-stick="click:copy:https://example.com"` |
443
+ | "two behaviors, two targets" | `data-stick="click:show" data-stick-target="#modal" data-stick-2="click:add-class:open" data-stick-target-2="#overlay"` |
444
+
445
+ ---
446
+
447
+ ## License
448
+
449
+ GPL-3.0 © 2011–2026 Kaique Silva
@@ -0,0 +1,20 @@
1
+ {
2
+ "components": {
3
+ "accordion": { "html": "components/accordion.html", "plugin": null },
4
+ "tabs": { "html": "components/tabs.html", "plugin": null },
5
+ "toggle": { "html": "components/toggle.html", "plugin": null },
6
+ "notification": { "html": "components/notification.html", "plugin": null },
7
+ "copy-button": { "html": "components/copy-button.html", "plugin": null },
8
+ "skeleton": { "html": "components/skeleton.html", "plugin": null },
9
+ "dialog": { "html": "components/dialog.html", "plugin": null },
10
+ "toast": { "html": "components/toast.html", "plugin": "plugins/toast.js" },
11
+ "dropdown": { "html": "components/dropdown.html", "plugin": "plugins/dropdown.js" },
12
+ "tooltip": { "html": "components/tooltip.html", "plugin": "plugins/tooltip.js" },
13
+ "toggle-group": { "html": "components/toggle-group.html", "plugin": null },
14
+ "command-palette": { "html": "components/command-palette.html", "plugin": "plugins/command-palette.js" },
15
+ "data-table": { "html": "components/data-table.html", "plugin": "plugins/data-table.js" },
16
+ "stepper": { "html": "components/stepper.html", "plugin": "plugins/stepper.js" },
17
+ "autocomplete": { "html": "components/autocomplete.html", "plugin": "plugins/autocomplete.js" }
18
+ },
19
+ "css": "stick-ui.css"
20
+ }