@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 +169 -0
- package/README.md +449 -0
- package/bin/registry.json +20 -0
- package/bin/stickjs.js +158 -0
- package/llms.txt +244 -0
- package/package.json +38 -0
- package/stick-ui/components/accordion.html +25 -0
- package/stick-ui/components/autocomplete.html +82 -0
- package/stick-ui/components/command-palette.html +28 -0
- package/stick-ui/components/copy-button.html +12 -0
- package/stick-ui/components/data-table.html +191 -0
- package/stick-ui/components/dialog.html +23 -0
- package/stick-ui/components/dropdown.html +16 -0
- package/stick-ui/components/notification.html +11 -0
- package/stick-ui/components/skeleton.html +11 -0
- package/stick-ui/components/stepper.html +102 -0
- package/stick-ui/components/tabs.html +26 -0
- package/stick-ui/components/toast.html +10 -0
- package/stick-ui/components/toggle-group.html +16 -0
- package/stick-ui/components/toggle.html +9 -0
- package/stick-ui/components/tooltip.html +12 -0
- package/stick-ui/plugins/autocomplete.js +422 -0
- package/stick-ui/plugins/command-palette.js +289 -0
- package/stick-ui/plugins/data-table.js +426 -0
- package/stick-ui/plugins/dropdown.js +70 -0
- package/stick-ui/plugins/stepper.js +155 -0
- package/stick-ui/plugins/toast.js +51 -0
- package/stick-ui/plugins/tooltip.js +67 -0
- package/stick-ui/stick-ui.css +825 -0
- package/stick.d.ts +105 -0
- package/stick.js +655 -0
package/bin/stickjs.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
// ── Registry ────────────────────────────────────────────────────────────────
|
|
9
|
+
const registry = require('./registry.json');
|
|
10
|
+
const COMPONENTS = registry.components;
|
|
11
|
+
const NAMES = Object.keys(COMPONENTS);
|
|
12
|
+
|
|
13
|
+
// ── Paths ───────────────────────────────────────────────────────────────────
|
|
14
|
+
const PKG_ROOT = path.join(__dirname, '..', 'stick-ui'); // source in npm package
|
|
15
|
+
const DEST = path.resolve(process.cwd(), 'stick-ui'); // user's project
|
|
16
|
+
|
|
17
|
+
// ── ANSI helpers ────────────────────────────────────────────────────────────
|
|
18
|
+
const RESET = '\x1b[0m';
|
|
19
|
+
const BOLD = '\x1b[1m';
|
|
20
|
+
const DIM = '\x1b[2m';
|
|
21
|
+
const GREEN = '\x1b[32m';
|
|
22
|
+
const CYAN = '\x1b[36m';
|
|
23
|
+
const YELLOW = '\x1b[33m';
|
|
24
|
+
const RED = '\x1b[31m';
|
|
25
|
+
|
|
26
|
+
function green(s) { return GREEN + s + RESET; }
|
|
27
|
+
function cyan(s) { return CYAN + s + RESET; }
|
|
28
|
+
function yellow(s) { return YELLOW + s + RESET; }
|
|
29
|
+
function red(s) { return RED + s + RESET; }
|
|
30
|
+
function dim(s) { return DIM + s + RESET; }
|
|
31
|
+
function bold(s) { return BOLD + s + RESET; }
|
|
32
|
+
|
|
33
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
34
|
+
function copyFile(src, dest) {
|
|
35
|
+
const dir = path.dirname(dest);
|
|
36
|
+
if (!fs.existsSync(dir)) {
|
|
37
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
fs.copyFileSync(src, dest);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Commands ────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function showHelp() {
|
|
45
|
+
console.log(`
|
|
46
|
+
${bold('stickjs')} — CLI for Stick.js UI components
|
|
47
|
+
|
|
48
|
+
${bold('Usage:')}
|
|
49
|
+
npx stickjs ${cyan('list')} List available components
|
|
50
|
+
npx stickjs ${cyan('add')} <name> [name...] Add component(s) to ./stick-ui/
|
|
51
|
+
npx stickjs ${cyan('add')} ${dim('--all')} Add all components
|
|
52
|
+
npx stickjs ${cyan('add')} <name> ${dim('--force')} Overwrite existing files
|
|
53
|
+
|
|
54
|
+
${bold('Options:')}
|
|
55
|
+
${dim('--help, -h')} Show this help message
|
|
56
|
+
${dim('--all')} Add every component
|
|
57
|
+
${dim('--force')} Overwrite existing files without warning
|
|
58
|
+
|
|
59
|
+
${bold('Examples:')}
|
|
60
|
+
npx stickjs add dialog toast
|
|
61
|
+
npx stickjs add --all --force
|
|
62
|
+
`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function listComponents() {
|
|
66
|
+
console.log(`\n${bold('Available components')} ${dim('(' + NAMES.length + ')')}\n`);
|
|
67
|
+
const maxLen = Math.max(...NAMES.map(n => n.length));
|
|
68
|
+
for (const name of NAMES) {
|
|
69
|
+
const entry = COMPONENTS[name];
|
|
70
|
+
const plugin = entry.plugin ? yellow(' + plugin') : '';
|
|
71
|
+
console.log(` ${cyan(name.padEnd(maxLen))} ${dim(entry.html)}${plugin}`);
|
|
72
|
+
}
|
|
73
|
+
console.log();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function addComponents(args) {
|
|
77
|
+
const force = args.includes('--force');
|
|
78
|
+
const all = args.includes('--all');
|
|
79
|
+
const names = all
|
|
80
|
+
? NAMES
|
|
81
|
+
: args.filter(a => !a.startsWith('--'));
|
|
82
|
+
|
|
83
|
+
if (names.length === 0) {
|
|
84
|
+
console.log(red('\nError: specify at least one component name, or use --all\n'));
|
|
85
|
+
console.log(`Run ${cyan('npx stickjs list')} to see available components.\n`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Validate names
|
|
90
|
+
const invalid = names.filter(n => !COMPONENTS[n]);
|
|
91
|
+
if (invalid.length > 0) {
|
|
92
|
+
console.log(red('\nUnknown component(s): ') + invalid.join(', '));
|
|
93
|
+
console.log(`Run ${cyan('npx stickjs list')} to see available components.\n`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let added = 0;
|
|
98
|
+
let skipped = 0;
|
|
99
|
+
|
|
100
|
+
// Copy CSS on first add (if not present)
|
|
101
|
+
const cssSrc = path.join(PKG_ROOT, registry.css);
|
|
102
|
+
const cssDest = path.join(DEST, registry.css);
|
|
103
|
+
if (!fs.existsSync(cssDest)) {
|
|
104
|
+
copyFile(cssSrc, cssDest);
|
|
105
|
+
console.log(green(' + ') + dim('stick-ui.css'));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const name of names) {
|
|
109
|
+
const entry = COMPONENTS[name];
|
|
110
|
+
|
|
111
|
+
// HTML file
|
|
112
|
+
const htmlSrc = path.join(PKG_ROOT, entry.html);
|
|
113
|
+
const htmlDest = path.join(DEST, name + '.html');
|
|
114
|
+
|
|
115
|
+
if (fs.existsSync(htmlDest) && !force) {
|
|
116
|
+
console.log(yellow(' ~ ') + name + '.html' + dim(' (exists, use --force to overwrite)'));
|
|
117
|
+
skipped++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
copyFile(htmlSrc, htmlDest);
|
|
122
|
+
console.log(green(' + ') + name + '.html');
|
|
123
|
+
|
|
124
|
+
// Plugin file
|
|
125
|
+
if (entry.plugin) {
|
|
126
|
+
const pluginSrc = path.join(PKG_ROOT, entry.plugin);
|
|
127
|
+
const pluginDest = path.join(DEST, 'plugins', path.basename(entry.plugin));
|
|
128
|
+
|
|
129
|
+
if (fs.existsSync(pluginDest) && !force) {
|
|
130
|
+
console.log(yellow(' ~ ') + 'plugins/' + path.basename(entry.plugin) + dim(' (exists, use --force)'));
|
|
131
|
+
skipped++;
|
|
132
|
+
} else {
|
|
133
|
+
copyFile(pluginSrc, pluginDest);
|
|
134
|
+
console.log(green(' + ') + 'plugins/' + path.basename(entry.plugin));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
added++;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log(`\n${bold('Done:')} ${green(added + ' added')}, ${yellow(skipped + ' skipped')}\n`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
145
|
+
const args = process.argv.slice(2);
|
|
146
|
+
const command = args[0];
|
|
147
|
+
|
|
148
|
+
if (!command || command === '--help' || command === '-h') {
|
|
149
|
+
showHelp();
|
|
150
|
+
} else if (command === 'list') {
|
|
151
|
+
listComponents();
|
|
152
|
+
} else if (command === 'add') {
|
|
153
|
+
addComponents(args.slice(1));
|
|
154
|
+
} else {
|
|
155
|
+
console.log(red(`\nUnknown command: ${command}`));
|
|
156
|
+
console.log(`Run ${cyan('npx stickjs --help')} for usage.\n`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Stick.js
|
|
2
|
+
|
|
3
|
+
> Declarative behavior for HTML elements via data attributes. Zero dependencies, ~200 lines of vanilla JS.
|
|
4
|
+
|
|
5
|
+
## Core concept
|
|
6
|
+
|
|
7
|
+
Annotate any HTML element with `data-stick="event:handler:param"` to attach behavior without writing JavaScript glue code.
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<script src="stick.js"></script>
|
|
11
|
+
|
|
12
|
+
<button data-stick="click:alert:Hello!">Click me</button>
|
|
13
|
+
<input data-stick="input:set-text:{{value}}" data-stick-target="#output">
|
|
14
|
+
<div id="output">mirrored here</div>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Full syntax
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
data-stick="event:handler:param" (trailing colon optional when param is empty — "click:toggle" works)
|
|
21
|
+
data-stick="event:handler" — shorthand, same as above
|
|
22
|
+
data-stick-delegate=".sel" — event delegation: listen on el, fire only for matching descendants
|
|
23
|
+
data-stick-target="selector" — apply to another element
|
|
24
|
+
data-stick-2="event:handler:param" — stack a second behavior (up to data-stick-5)
|
|
25
|
+
data-stick-target-2="#other" — per-slot target for data-stick-2 (also -3, -4, -5)
|
|
26
|
+
data-stick-once — remove listener after first fire
|
|
27
|
+
data-stick-debounce="300" — debounce N ms
|
|
28
|
+
data-stick-throttle="300" — throttle N ms
|
|
29
|
+
data-stick-confirm="message" — require window.confirm() before running
|
|
30
|
+
data-stick-key="Enter" — only fire on matching KeyboardEvent.key (comma-separated)
|
|
31
|
+
data-stick-prevent — always call event.preventDefault() before handler
|
|
32
|
+
data-stick-stop — always call event.stopPropagation() before handler
|
|
33
|
+
data-stick-passive — addEventListener passive:true (scroll/touch perf)
|
|
34
|
+
data-stick-capture — addEventListener capture:true
|
|
35
|
+
data-stick-method="POST" — HTTP method for fetch handler
|
|
36
|
+
data-stick-swap="beforeend" — fetch insertion mode (innerHTML|outerHTML|prepend|append|beforeend|afterbegin|…)
|
|
37
|
+
data-stick-loading="Loading…" — button text while fetch is in-flight
|
|
38
|
+
data-stick-headers='{"Authorization":"Bearer TOKEN"}' — fetch headers (JSON string)
|
|
39
|
+
data-stick-json='{"key":"{{value}}"}' — JSON body for fetch POST
|
|
40
|
+
data-stick-error="#error-el" — element to show fetch errors in
|
|
41
|
+
data-stick-group="name" — mutual exclusivity: show/toggle/show-modal hides others in same group
|
|
42
|
+
data-stick-transition="name" — CSS enter/leave transitions: adds name-enter-active / name-leave-active classes with automatic cleanup
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Target selectors (data-stick-target)
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
"#id" querySelector (default)
|
|
49
|
+
"self" el itself (explicit)
|
|
50
|
+
"siblings" all sibling elements (same parent, excluding el) — enables tab patterns
|
|
51
|
+
"all:.class" all elements matching the CSS selector (querySelectorAll)
|
|
52
|
+
"next" el.nextElementSibling
|
|
53
|
+
"prev" el.previousElementSibling
|
|
54
|
+
"parent" el.parentElement
|
|
55
|
+
"closest:form" el.closest("form")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Param interpolation (resolved at event time)
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
{{value}} el.value
|
|
62
|
+
{{text}} el.textContent
|
|
63
|
+
{{id}} el.id
|
|
64
|
+
{{name}} el.name
|
|
65
|
+
{{checked}} el.checked → "true"/"false"
|
|
66
|
+
{{index}} el's position among parent.children (0-based)
|
|
67
|
+
{{length}} el.children.length
|
|
68
|
+
{{chars}} el.value.length (or textContent.length) — for character counters
|
|
69
|
+
{{url:key}} URLSearchParams value from current page URL (e.g. {{url:q}} for ?q=)
|
|
70
|
+
{{data-foo}} el.dataset.foo
|
|
71
|
+
{{href}} el.getAttribute("href") — any attribute name works
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## All built-in handlers
|
|
75
|
+
|
|
76
|
+
| handler | description |
|
|
77
|
+
|----------------|------------------------------------------------------|
|
|
78
|
+
| show | target.hidden = false; auto-manages aria-expanded on trigger |
|
|
79
|
+
| hide | target.hidden = true; auto-manages aria-expanded on trigger |
|
|
80
|
+
| toggle | toggle target.hidden; auto-manages aria-expanded on trigger |
|
|
81
|
+
| add-class | target.classList.add(param) |
|
|
82
|
+
| remove-class | target.classList.remove(param) |
|
|
83
|
+
| toggle-class | target.classList.toggle(param) |
|
|
84
|
+
| set-text | target.textContent = param |
|
|
85
|
+
| set-html | target.innerHTML = param |
|
|
86
|
+
| set-value | target.value = param |
|
|
87
|
+
| clear | target.textContent = "" |
|
|
88
|
+
| set-attr | target.setAttribute(name, value) — param: "name:val" |
|
|
89
|
+
| set-style | target.style[prop] = value — param: "property:value" |
|
|
90
|
+
| remove-attr | target.removeAttribute(param) |
|
|
91
|
+
| toggle-attr | toggle attribute presence on target (e.g. "disabled", "open") |
|
|
92
|
+
| clone-template | clone <template> matched by param; resolves {{tokens}} from el; appends to target |
|
|
93
|
+
| sort | sort target.children by textContent (default) or param key (e.g. "data-price") |
|
|
94
|
+
| count | set target.textContent to count of querySelectorAll(param) or target.children.length |
|
|
95
|
+
| animate | target.classList.add(param); auto-removes class after animationend |
|
|
96
|
+
| focus | target.focus() |
|
|
97
|
+
| scroll-to | target.scrollIntoView({ behavior: "smooth" }) |
|
|
98
|
+
| copy | navigator.clipboard.writeText(param or el.textContent)|
|
|
99
|
+
| navigate | window.location.href = param |
|
|
100
|
+
| open | window.open(param, '_blank', 'noopener') |
|
|
101
|
+
| back | window.history.back() |
|
|
102
|
+
| forward | window.history.forward() |
|
|
103
|
+
| history-push | window.history.pushState({}, '', param) — update URL without reload |
|
|
104
|
+
| scroll-top | window.scrollTo({ top: 0, behavior: 'smooth' }) — back-to-top |
|
|
105
|
+
| select | target.select() — select all text in input/textarea |
|
|
106
|
+
| show-modal | target.showModal() — native <dialog> |
|
|
107
|
+
| close-modal | target.close(param?) — native <dialog> |
|
|
108
|
+
| submit | closest form submit |
|
|
109
|
+
| reset | closest form reset |
|
|
110
|
+
| prevent | event.preventDefault() |
|
|
111
|
+
| stop | event.stopPropagation() |
|
|
112
|
+
| remove | target.remove() |
|
|
113
|
+
| disable | target.disabled = true |
|
|
114
|
+
| enable | target.disabled = false |
|
|
115
|
+
| reload | window.location.reload() |
|
|
116
|
+
| print | window.print() |
|
|
117
|
+
| log | console.log(param) |
|
|
118
|
+
| alert | window.alert(param) |
|
|
119
|
+
| dispatch | target.dispatchEvent(new CustomEvent(param, { bubbles: true, detail: { source: el } })) |
|
|
120
|
+
| emit | alias for dispatch |
|
|
121
|
+
| store | localStorage.setItem(key, value) — param: "key:value"|
|
|
122
|
+
| restore | localStorage.getItem(param) → set target value/text |
|
|
123
|
+
| fetch | fetch(param) → inject response into target |
|
|
124
|
+
| check | target.checked = true |
|
|
125
|
+
| uncheck | target.checked = false |
|
|
126
|
+
| toggle-check | toggle target.checked |
|
|
127
|
+
| set-data | target.dataset[key] = value — param: "key:value" |
|
|
128
|
+
| increment | target value/textContent += step (default 1) |
|
|
129
|
+
| decrement | target value/textContent -= step (default 1) |
|
|
130
|
+
| set-aria | set ARIA attribute — param: "name:value" (auto-prefixes aria-) |
|
|
131
|
+
| toggle-aria | flip ARIA boolean attribute true↔false — param: attribute name (auto-prefixes aria-) |
|
|
132
|
+
| intersect | fires via IntersectionObserver when el enters viewport |
|
|
133
|
+
|
|
134
|
+
## Error boundary
|
|
135
|
+
|
|
136
|
+
All handler invocations are wrapped in try/catch. A failing handler is logged to console.error but does not prevent sibling handlers on the same element from running.
|
|
137
|
+
|
|
138
|
+
## Synthetic events
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
ready — fires immediately on bind (before any user interaction)
|
|
142
|
+
watch — fires immediately on bind, then on every attribute mutation of el (MutationObserver)
|
|
143
|
+
intersect — IntersectionObserver; fires when element enters viewport
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## JavaScript API
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
Stick.version // "3.0.0"
|
|
150
|
+
Stick.add(name, fn) // register handler; fn(el, param, event, target) => void
|
|
151
|
+
Stick.remove(name) // unregister handler
|
|
152
|
+
Stick.use(plugin) // register plugin: fn(stick) | { install } | { name: fn, … }
|
|
153
|
+
Stick.debug(true?) // enable verbose console logging (dev mode)
|
|
154
|
+
Stick.unbind(el) // remove all listeners, clear data-stick-bound (allows rebind)
|
|
155
|
+
Stick.handlers // frozen snapshot of { name: fn, … }
|
|
156
|
+
Stick.init(root?) // scan and bind [data-stick] under root (default: document)
|
|
157
|
+
Stick.observe(root?) // MutationObserver auto-bind (default: document.body)
|
|
158
|
+
Stick.parse(value) // → { event, handler, param } | null
|
|
159
|
+
Stick.fire(el, handler, param?) // invoke handler programmatically (chainable)
|
|
160
|
+
Stick.on(event, fn) // lifecycle hooks: 'bind', 'unbind' (chainable)
|
|
161
|
+
Stick.bind(el) // bind a single element
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Custom handler pattern
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
Stick.add('my-handler', (el, param, event, target) => {
|
|
168
|
+
// el — element with data-stick (the trigger)
|
|
169
|
+
// param — string after second colon, with {{tokens}} already resolved
|
|
170
|
+
// event — DOM Event (or { type: 'intersect', entry } for intersect)
|
|
171
|
+
// target — resolved data-stick-target element, or el if none
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## LLM usage patterns
|
|
176
|
+
|
|
177
|
+
When generating HTML with Stick.js, map user intent to attributes:
|
|
178
|
+
|
|
179
|
+
| Intent | HTML |
|
|
180
|
+
|--------|------|
|
|
181
|
+
| "toggle sidebar on click" | `data-stick="click:toggle" data-stick-target="#sidebar"` |
|
|
182
|
+
| "add loading class while fetching" | `data-stick="click:add-class:loading" data-stick-2="click:fetch:/api/data" data-stick-target="#result"` |
|
|
183
|
+
| "search as user types, debounced" | `data-stick="input:fetch:/search?q={{value}}" data-stick-debounce="300" data-stick-target="#results"` |
|
|
184
|
+
| "confirm before delete" | `data-stick="click:fetch:/api/item/1" data-stick-method="DELETE" data-stick-confirm="Delete?" data-stick-target="parent"` |
|
|
185
|
+
| "lazy load on scroll into view" | `data-stick="intersect:fetch:/api/widget" data-stick-target="#widget" data-stick-once` |
|
|
186
|
+
| "copy link on click" | `data-stick="click:copy:https://example.com"` |
|
|
187
|
+
| "submit form via AJAX" | `data-stick="submit:prevent:" data-stick-2="submit:fetch:/api" data-stick-target="#response"` |
|
|
188
|
+
| "dispatch event for other components" | `data-stick="click:dispatch:user-selected" data-stick-target="#app"` |
|
|
189
|
+
| "increment counter on click" | `data-stick="click:increment:" data-stick-target="#count"` |
|
|
190
|
+
| "decrement with custom step" | `data-stick="click:decrement:5" data-stick-target="#count"` |
|
|
191
|
+
| "check all checkboxes in group" | `data-stick="click:check:" data-stick-target=".cb-item"` |
|
|
192
|
+
| "toggle checkbox on click" | `data-stick="click:toggle-check:" data-stick-target="#my-checkbox"` |
|
|
193
|
+
| "tag element with data attribute" | `data-stick="click:set-data:status:active" data-stick-target="#item"` |
|
|
194
|
+
| "passive scroll listener" | `data-stick="scroll:log:scrolled" data-stick-passive` |
|
|
195
|
+
| "submit form on Enter key" | `data-stick="keydown:submit:" data-stick-key="Enter"` |
|
|
196
|
+
| "close modal on Escape key" | `data-stick="keydown:close-modal:" data-stick-key="Escape" data-stick-target="#my-dialog"` |
|
|
197
|
+
| "open link in new tab" | `data-stick="click:open:https://example.com"` |
|
|
198
|
+
| "show native dialog modal" | `data-stick="click:show-modal:" data-stick-target="#my-dialog"` |
|
|
199
|
+
| "close dialog on button" | `data-stick="click:close-modal:" data-stick-target="closest:dialog"` |
|
|
200
|
+
| "go back in history" | `data-stick="click:back:"` |
|
|
201
|
+
| "add row from template" | `data-stick="click:clone-template:#row-tpl" data-stick-target="#list"` |
|
|
202
|
+
| "toggle details/open" | `data-stick="click:toggle-attr:open" data-stick-target="closest:details"` |
|
|
203
|
+
| "submit form (prevent default)" | `data-stick="submit:fetch:/api/save" data-stick-target="#result" data-stick-prevent` |
|
|
204
|
+
| "stop click propagation" | `data-stick="click:toggle-class:active" data-stick-stop` |
|
|
205
|
+
| "tab interface — active tab" | `data-stick="click:add-class:active" data-stick-2="click:remove-class:active" data-stick-target-2="siblings"` |
|
|
206
|
+
| "show one panel, hide siblings" | `data-stick="click:show" data-stick-target="#panel" data-stick-2="click:hide" data-stick-target-2="all:.panel"` |
|
|
207
|
+
| "restore theme preference on load" | `data-stick="ready:restore:theme" data-stick-target="self"` |
|
|
208
|
+
| "animate element on click" | `data-stick="click:animate:shake" data-stick-target="#icon"` |
|
|
209
|
+
| "character counter" | `data-stick="input:set-text:{{chars}}/280" data-stick-target="#count"` |
|
|
210
|
+
| "dispatch with list index" | `data-stick="click:dispatch:item-click:{{index}}"` |
|
|
211
|
+
| "register a plugin" | `Stick.use({ 'my-handler': (el, p, e, t) => { ... } })` |
|
|
212
|
+
| "unbind element for cleanup" | `Stick.unbind(el)` |
|
|
213
|
+
| "hide all matching elements" | `data-stick="click:hide:" data-stick-target="all:.notification"` |
|
|
214
|
+
| "update URL without reload" | `data-stick="click:history-push:/new-path"` |
|
|
215
|
+
| "select all text on focus" | `data-stick="focus:select" data-stick-target="self"` |
|
|
216
|
+
| "react to data-attribute changes" | `data-stick="watch:set-text:{{data-count}}" data-stick-target="#display"` |
|
|
217
|
+
| "toggle without trailing colon" | `data-stick="click:toggle" data-stick-target="#panel"` |
|
|
218
|
+
| "dynamic list — delete item" | `data-stick="click:remove" data-stick-target="parent" data-stick-delegate=".item"` |
|
|
219
|
+
| "back to top button" | `data-stick="click:scroll-top"` |
|
|
220
|
+
| "pre-fill input from URL param" | `data-stick="ready:set-value:{{url:q}}" data-stick-target="self"` |
|
|
221
|
+
| "remove item by id (delegation)" | `data-stick="click:fetch:/api/item/{{data-id}}" data-stick-method="DELETE" data-stick-delegate="[data-id]" data-stick-target="parent"` |
|
|
222
|
+
| "set aria-expanded on click" | `data-stick="click:set-aria:expanded:true" data-stick-target="#menu"` |
|
|
223
|
+
| "toggle aria-checked" | `data-stick="click:toggle-aria:checked" data-stick-target="self"` |
|
|
224
|
+
| "exclusive accordion panels" | `data-stick="click:show" data-stick-target="#panel1" data-stick-group="accordion"` |
|
|
225
|
+
| "fade-in transition on show" | `data-stick="click:show" data-stick-target="#box" data-stick-transition="fade"` |
|
|
226
|
+
| "fire handler programmatically" | `Stick.fire(el, 'toggle-class', 'active')` |
|
|
227
|
+
| "listen for bind events" | `Stick.on('bind', el => console.log('bound', el))` |
|
|
228
|
+
| "add task from input on Enter" | `data-stick="keydown:clone-template:#task-tpl" data-stick-key="Enter" data-stick-target="#list"` (template uses {{value}}) |
|
|
229
|
+
| "sort list alphabetically" | `data-stick="click:sort" data-stick-target="#list"` |
|
|
230
|
+
| "sort by data attribute" | `data-stick="click:sort:data-price" data-stick-target="#list"` |
|
|
231
|
+
| "count matching items" | `data-stick="click:count:.item" data-stick-target="#count"` |
|
|
232
|
+
| "fetch and prepend results" | `data-stick="click:fetch:/api/more" data-stick-swap="prepend" data-stick-target="#list"` |
|
|
233
|
+
|
|
234
|
+
## Install
|
|
235
|
+
|
|
236
|
+
```html
|
|
237
|
+
<script src="stick.js"></script>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Script tag placement: put `<script src="stick.js">` BEFORE any `<script>` that registers custom handlers with `Stick.add()`, so handlers are registered before the auto-init `DOMContentLoaded` fires.
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
npm install stickjs
|
|
244
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ktfth/stickjs",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Declarative behavior for HTML elements. Zero dependencies.",
|
|
5
|
+
"main": "stick.js",
|
|
6
|
+
"types": "stick.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"require": "./stick.js",
|
|
10
|
+
"import": "./stick.js",
|
|
11
|
+
"default": "./stick.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"stickjs": "./bin/stickjs.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"stick.js",
|
|
19
|
+
"stick.d.ts",
|
|
20
|
+
"llms.txt",
|
|
21
|
+
"README.md",
|
|
22
|
+
"CHANGELOG.md",
|
|
23
|
+
"bin/",
|
|
24
|
+
"stick-ui/"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"minify": "npx esbuild stick.js --bundle --minify --outfile=stick.min.js --platform=browser",
|
|
28
|
+
"test": "echo 'Open test/stick.test.html in a browser to run tests'",
|
|
29
|
+
"release": "npm run minify && npm publish --access=public && vercel --prod ./docs"
|
|
30
|
+
},
|
|
31
|
+
"keywords": ["declarative", "html", "behavior", "htmx", "vanilla", "framework", "no-js", "components", "ui", "cli"],
|
|
32
|
+
"author": "Kaique Silva <kaique.developer@gmail.com>",
|
|
33
|
+
"license": "GPL-3.0",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/kaiquekandykoga/Stickjs"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<!-- Stick UI: Accordion
|
|
2
|
+
Requires: stick.js
|
|
3
|
+
Optional: stick-ui.css (for .stk-* styles)
|
|
4
|
+
For exclusive mode, add data-stick-group="accordion-name" to each content panel.
|
|
5
|
+
-->
|
|
6
|
+
<div class="stk-accordion">
|
|
7
|
+
<div class="stk-accordion-item">
|
|
8
|
+
<button class="stk-accordion-trigger" aria-expanded="false"
|
|
9
|
+
data-stick="click:toggle" data-stick-target="next">
|
|
10
|
+
Section title
|
|
11
|
+
</button>
|
|
12
|
+
<div class="stk-accordion-content" hidden>
|
|
13
|
+
<p>Section content goes here.</p>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="stk-accordion-item">
|
|
17
|
+
<button class="stk-accordion-trigger" aria-expanded="false"
|
|
18
|
+
data-stick="click:toggle" data-stick-target="next">
|
|
19
|
+
Another section
|
|
20
|
+
</button>
|
|
21
|
+
<div class="stk-accordion-content" hidden>
|
|
22
|
+
<p>More content here.</p>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<!-- Stick UI: Autocomplete
|
|
2
|
+
Requires: stick.js + stick-ui/plugins/autocomplete.js
|
|
3
|
+
Optional: stick-ui.css
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<!-- ── Variant 1: Static options (from a hidden <ul>) ─────── -->
|
|
7
|
+
|
|
8
|
+
<div class="stk-autocomplete" style="width: 300px;">
|
|
9
|
+
<label class="stk-label" for="lang-input">Programming Language</label>
|
|
10
|
+
<input id="lang-input"
|
|
11
|
+
class="stk-autocomplete-input"
|
|
12
|
+
type="text"
|
|
13
|
+
placeholder="Search languages…"
|
|
14
|
+
data-stick="input:autocomplete:#language-list"
|
|
15
|
+
data-stk-autocomplete-min="1">
|
|
16
|
+
|
|
17
|
+
<!-- Hidden source list -->
|
|
18
|
+
<ul id="language-list" hidden>
|
|
19
|
+
<li data-value="javascript">JavaScript</li>
|
|
20
|
+
<li data-value="typescript">TypeScript</li>
|
|
21
|
+
<li data-value="python">Python</li>
|
|
22
|
+
<li data-value="rust">Rust</li>
|
|
23
|
+
<li data-value="go">Go</li>
|
|
24
|
+
<li data-value="java">Java</li>
|
|
25
|
+
<li data-value="csharp">C#</li>
|
|
26
|
+
<li data-value="cpp">C++</li>
|
|
27
|
+
<li data-value="ruby">Ruby</li>
|
|
28
|
+
<li data-value="swift">Swift</li>
|
|
29
|
+
<li data-value="kotlin">Kotlin</li>
|
|
30
|
+
<li data-value="php">PHP</li>
|
|
31
|
+
</ul>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- ── Variant 2: Remote / programmatic ───────────────────── -->
|
|
35
|
+
|
|
36
|
+
<!--
|
|
37
|
+
Remote example: set data-stk-autocomplete-url to your API endpoint.
|
|
38
|
+
The plugin appends the query string, e.g. /api/search?q=reac
|
|
39
|
+
Expects a JSON response: ["React","React Native"] or [{label,value}]
|
|
40
|
+
-->
|
|
41
|
+
<div class="stk-autocomplete" style="width: 300px; margin-top: 24px;">
|
|
42
|
+
<label class="stk-label" for="remote-input">Search Packages</label>
|
|
43
|
+
<input id="remote-input"
|
|
44
|
+
class="stk-autocomplete-input"
|
|
45
|
+
type="text"
|
|
46
|
+
placeholder="Search npm packages…"
|
|
47
|
+
data-stick="input:autocomplete"
|
|
48
|
+
data-stk-autocomplete-url="https://api.example.com/packages?q="
|
|
49
|
+
data-stk-autocomplete-min="2">
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!--
|
|
53
|
+
Programmatic example: use stkAutocomplete.setOptions() to populate
|
|
54
|
+
options from your own script or after an API call.
|
|
55
|
+
-->
|
|
56
|
+
<div class="stk-autocomplete" style="width: 300px; margin-top: 24px;">
|
|
57
|
+
<label class="stk-label" for="custom-input">City</label>
|
|
58
|
+
<input id="custom-input"
|
|
59
|
+
class="stk-autocomplete-input"
|
|
60
|
+
type="text"
|
|
61
|
+
placeholder="Type a city name…"
|
|
62
|
+
data-stick="input:autocomplete"
|
|
63
|
+
data-stk-autocomplete-min="1">
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<script>
|
|
67
|
+
// After stick.js and autocomplete.js are loaded:
|
|
68
|
+
stkAutocomplete.setOptions(document.getElementById('custom-input'), [
|
|
69
|
+
'New York',
|
|
70
|
+
'Los Angeles',
|
|
71
|
+
'Chicago',
|
|
72
|
+
'Houston',
|
|
73
|
+
'Phoenix',
|
|
74
|
+
'San Francisco',
|
|
75
|
+
'Seattle',
|
|
76
|
+
'Denver',
|
|
77
|
+
'Boston',
|
|
78
|
+
'Miami',
|
|
79
|
+
'Austin',
|
|
80
|
+
'Portland'
|
|
81
|
+
]);
|
|
82
|
+
</script>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!-- Stick UI: Command Palette
|
|
2
|
+
Requires: stick.js, stick-ui/plugins/command-palette.js
|
|
3
|
+
Optional: stick-ui.css (for styled palette)
|
|
4
|
+
Global shortcut: Ctrl+K (Cmd+K on Mac)
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
<!-- Button trigger -->
|
|
8
|
+
<button class="stk-btn" data-stick="click:command-palette">
|
|
9
|
+
Open Command Palette
|
|
10
|
+
<span class="stk-command-palette-shortcut" style="margin-left:8px">Ctrl+K</span>
|
|
11
|
+
</button>
|
|
12
|
+
|
|
13
|
+
<!-- Register commands -->
|
|
14
|
+
<script>
|
|
15
|
+
stkCommandPalette.register([
|
|
16
|
+
// Navigation group
|
|
17
|
+
{ id: 'nav-home', label: 'Go to Home', group: 'Navigation', shortcut: 'G H', action: function() { console.log('Navigate: Home'); } },
|
|
18
|
+
{ id: 'nav-settings', label: 'Go to Settings', group: 'Navigation', shortcut: 'G S', action: function() { console.log('Navigate: Settings'); } },
|
|
19
|
+
{ id: 'nav-profile', label: 'Go to Profile', group: 'Navigation', shortcut: 'G P', action: function() { console.log('Navigate: Profile'); } },
|
|
20
|
+
{ id: 'nav-docs', label: 'Open Documentation', group: 'Navigation', action: function() { console.log('Navigate: Docs'); } },
|
|
21
|
+
|
|
22
|
+
// Actions group
|
|
23
|
+
{ id: 'act-theme', label: 'Toggle Dark Mode', group: 'Actions', shortcut: 'Ctrl+D', action: function() { document.documentElement.toggleAttribute('data-theme'); } },
|
|
24
|
+
{ id: 'act-copy', label: 'Copy Current URL', group: 'Actions', shortcut: 'Ctrl+C', action: function() { navigator.clipboard.writeText(location.href); } },
|
|
25
|
+
{ id: 'act-reload', label: 'Reload Page', group: 'Actions', shortcut: 'Ctrl+R', action: function() { location.reload(); } },
|
|
26
|
+
{ id: 'act-print', label: 'Print Page', group: 'Actions', shortcut: 'Ctrl+P', action: function() { window.print(); } }
|
|
27
|
+
]);
|
|
28
|
+
</script>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!-- Stick UI: Copy Button
|
|
2
|
+
Requires: stick.js
|
|
3
|
+
Optional: stick-ui.css
|
|
4
|
+
Copies the param text. Shows "Copied!" feedback for 1.5s.
|
|
5
|
+
-->
|
|
6
|
+
<button class="stk-btn stk-btn-sm"
|
|
7
|
+
data-stick="click:copy:Text to copy"
|
|
8
|
+
data-stick-2="click:set-text:Copied!"
|
|
9
|
+
data-stick-3="click:wait:1500"
|
|
10
|
+
data-stick-4="click:set-text:Copy">
|
|
11
|
+
Copy
|
|
12
|
+
</button>
|