{"_id":"@cujuju/solidjs-hooks","_rev":"2-399e28d220ce3fe97d562a47f5e80743","name":"@cujuju/solidjs-hooks","dist-tags":{"latest":"2.0.0"},"versions":{"0.1.0":{"name":"@cujuju/solidjs-hooks","version":"0.1.0","keywords":["solidjs","hooks","click-outside","hotkey","localstorage","debounce","resize-observer","intersection-observer","media-query"],"author":{"name":"Cujuju"},"license":"MIT","_id":"@cujuju/solidjs-hooks@0.1.0","maintainers":[{"name":"cujuju","email":"cujuju@pm.me"}],"homepage":"https://github.com/Cujuju/solidjs-toolkit/tree/main/packages/hooks","bugs":{"url":"https://github.com/Cujuju/solidjs-toolkit/issues"},"dist":{"shasum":"7cdfabd4711b4f27b28d36d62762d9e50b2f42d8","tarball":"https://registry.npmjs.org/@cujuju/solidjs-hooks/-/solidjs-hooks-0.1.0.tgz","fileCount":53,"integrity":"sha512-aIQY5Bwe5ElxlgSYGgZwgbQsH1+xVgF2SpqLgdaPIx7kL+3+yCZRl9k3T6vsEbVM9w4j5CKRZiXqXJ09xj2f0A==","signatures":[{"sig":"MEUCIFD7G7M6A2/wgTKyyCdIhzI8zG5v1Qhya27XLz7wpJz2AiEAhBY958DFB2jmJCTrAjBrlTE6iQqvz9+zTxDpHcDpyno=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":122308},"main":"./dist/index.js","type":"module","_from":"file:cujuju-solidjs-hooks-0.1.0.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"solid":"./src/index.ts","types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"test":"vitest run","build":"vite build","test:watch":"vitest"},"_npmUser":{"name":"cujuju","email":"cujuju@pm.me"},"_resolved":"C:\\Users\\shawn\\AppData\\Local\\Temp\\65871c4be9f3aeb0ea2edd1761b50405\\cujuju-solidjs-hooks-0.1.0.tgz","_integrity":"sha512-aIQY5Bwe5ElxlgSYGgZwgbQsH1+xVgF2SpqLgdaPIx7kL+3+yCZRl9k3T6vsEbVM9w4j5CKRZiXqXJ09xj2f0A==","repository":{"url":"git+https://github.com/Cujuju/solidjs-toolkit.git","type":"git","directory":"packages/hooks"},"_npmVersion":"11.12.1","description":"Generic SolidJS hooks — click-outside, escape-key, hotkeys, localStorage-backed signals, media queries, observers, debounce, async status. Zero deps.","directories":{},"sideEffects":false,"_nodeVersion":"25.8.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^24.0.0","vitest":"^3.2.4","@solidjs/testing-library":"^0.8.0"},"peerDependencies":{"solid-js":">=1.7.0"},"_npmOperationalInternal":{"tmp":"tmp/solidjs-hooks_0.1.0_1777341452208_0.8722911323958398","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"@cujuju/solidjs-hooks","version":"2.0.0","description":"Generic SolidJS hooks — click-outside, escape-key, hotkeys, localStorage-backed signals, media queries, observers, debounce, async status, after-paint scheduling, outside-scroll dismiss. Zero deps.","type":"module","main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","solid":"./src/index.ts","import":"./dist/index.js"}},"sideEffects":false,"license":"MIT","author":{"name":"Cujuju"},"repository":{"type":"git","url":"git+https://github.com/Cujuju/solidjs-toolkit.git","directory":"packages/hooks"},"homepage":"https://github.com/Cujuju/solidjs-toolkit/tree/main/packages/hooks","bugs":{"url":"https://github.com/Cujuju/solidjs-toolkit/issues"},"keywords":["solidjs","hooks","click-outside","hotkey","localstorage","debounce","resize-observer","intersection-observer","media-query","after-paint","scroll-dismiss"],"peerDependencies":{"solid-js":">=1.7.0"},"devDependencies":{"@solidjs/testing-library":"^0.8.0","jsdom":"^24.0.0","vitest":"^3.2.4"},"publishConfig":{"access":"public"},"scripts":{"test":"vitest run","test:watch":"vitest","build":"vite build"},"_id":"@cujuju/solidjs-hooks@2.0.0","_integrity":"sha512-GUskN6N257YM2V1OlzIxiK9AsdPHNqdr0up9S/353GJYgSNSx7aTcofbXnxbnug2CDFZkPSbATTAUc+HmE9oWA==","_resolved":"C:\\Users\\shawn\\AppData\\Local\\Temp\\4b4103bc9d1a840ee05585aa7f8bbe61\\cujuju-solidjs-hooks-2.0.0.tgz","_from":"file:cujuju-solidjs-hooks-2.0.0.tgz","_nodeVersion":"25.8.2","_npmVersion":"11.12.1","dist":{"integrity":"sha512-GUskN6N257YM2V1OlzIxiK9AsdPHNqdr0up9S/353GJYgSNSx7aTcofbXnxbnug2CDFZkPSbATTAUc+HmE9oWA==","shasum":"46ab2fec4483d2db279a8f91c4a77e21074007b1","tarball":"https://registry.npmjs.org/@cujuju/solidjs-hooks/-/solidjs-hooks-2.0.0.tgz","fileCount":59,"unpackedSize":147721,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFkv4E1/K2IpXGeAdhEfQJV46/jaGX+OsLKHO0pXh3FHAiEAwl3oJ1YakozE3zWBrcFM3rbjAJo+P6mTLhMBf/2l4ag="}]},"_npmUser":{"name":"cujuju","email":"cujuju@pm.me"},"directories":{},"maintainers":[{"name":"cujuju","email":"cujuju@pm.me"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/solidjs-hooks_2.0.0_1778521114071_0.21124995305833938"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-28T01:57:32.103Z","modified":"2026-05-11T17:38:34.369Z","0.1.0":"2026-04-28T01:57:32.355Z","2.0.0":"2026-05-11T17:38:34.264Z"},"bugs":{"url":"https://github.com/Cujuju/solidjs-toolkit/issues"},"author":{"name":"Cujuju"},"license":"MIT","homepage":"https://github.com/Cujuju/solidjs-toolkit/tree/main/packages/hooks","keywords":["solidjs","hooks","click-outside","hotkey","localstorage","debounce","resize-observer","intersection-observer","media-query","after-paint","scroll-dismiss"],"repository":{"type":"git","url":"git+https://github.com/Cujuju/solidjs-toolkit.git","directory":"packages/hooks"},"description":"Generic SolidJS hooks — click-outside, escape-key, hotkeys, localStorage-backed signals, media queries, observers, debounce, async status, after-paint scheduling, outside-scroll dismiss. Zero deps.","maintainers":[{"name":"cujuju","email":"cujuju@pm.me"}],"readme":"# @cujuju/solidjs-hooks\n\nGeneric SolidJS utility hooks — zero dependencies, accessor-style APIs.\n\n## Install\n\n```\npnpm add @cujuju/solidjs-hooks\n```\n\n## Hooks\n\n### `createClickOutside(isInside, handler, options?)`\n\nFires `handler` on `pointerdown` outside the floating UI's interactive surface.\n\nThe surface is described by a predicate `(target: Node) => boolean`. The companion\nhelper `contains(...)` covers the common ref-based cases:\n\n```tsx\nimport { createClickOutside, contains } from '@cujuju/solidjs-hooks';\n\nlet menuEl: HTMLDivElement | undefined;\nconst [open, setOpen] = createSignal(false);\n\n// Single ref\ncreateClickOutside(contains(() => menuEl), () => setOpen(false), { enabled: open });\n\n// Multi-ref (button trigger + portaled panel both count as \"inside\")\ncreateClickOutside(contains(() => [buttonEl, panelEl]), close, { enabled: open });\n\n// Selector-based (e.g., portal-rendered subtree)\ncreateClickOutside((t) => (t as Element).closest('[data-flyout]') !== null, close);\n```\n\n**Behavior:**\n- Listens on `pointerdown` (capture phase) — covers mouse, touch, and pen with one trigger.\n  Capture phase means a child calling `e.stopPropagation()` cannot silently break the hook.\n- Suppresses events whose `timeStamp` predates listener attachment — the gesture that\n  opened the floating UI (e.g., a `contextmenu` or `click`) cannot immediately close it.\n  Replaces the per-callsite `setTimeout` defenses common in hand-rolled outside-click code.\n- `options.enabled` gate is checked on every event; safe to leave the listener attached\n  for the lifetime of the component.\n\n### `createEscapeKey(handler, options?)`\n\nFires on Escape keydown.\n\n```tsx\ncreateEscapeKey(() => setOpen(false), { enabled: open });\n```\n\n### `createHotkey(combo, handler, options?)`\n\nKeyboard shortcuts. Combo syntax: modifiers (`ctrl`, `shift`, `alt`, `meta`) separated by `+`, then the key. Case-insensitive. `cmd`/`command` alias `meta`; `option` aliases `alt`. Modifier-only combos never match.\n\n```tsx\ncreateHotkey('ctrl+k', () => openSearch());\ncreateHotkey('shift+?', () => showHelp());\ncreateHotkey('escape', () => close(), { enabled: () => modalOpen() });\ncreateHotkey('up', () => focusPrev());          // alias for arrowup\ncreateHotkey('shift+plus', () => zoomIn());     // 'plus' alias — '+' is the combo separator\n```\n\n**Key aliases** (additive — direct names like `'arrowup'` / `'enter'` continue to work):\n\n| Alias | Resolves to (`KeyboardEvent.key`) |\n|---|---|\n| `up` / `down` / `left` / `right` | `ArrowUp` / `ArrowDown` / `ArrowLeft` / `ArrowRight` |\n| `return` | `Enter` |\n| `esc` | `Escape` |\n| `space` | `' '` (single space character) |\n| `plus` | `+` |\n\n### `createLocalStorage(key, defaultValue)`\n\nReactive JSON-serialized localStorage signal. Same shape as `createSignal`.\n\n```tsx\nconst [theme, setTheme] = createLocalStorage('theme', 'dark');\nsetTheme('light');\n```\n\n### `createPersistedSet<T>(key, options?)`\n\n`Set<T>` backed by localStorage. Defaults to `Set<string>`.\n\n```tsx\nconst expanded = createPersistedSet<string>('myapp:expanded');\nexpanded.toggle('panel-1');\nexpanded.set(); // accessor — reactive\n```\n\nFor non-string values, provide serializers:\n\n```tsx\nconst ids = createPersistedSet<number>('myapp:ids', { serialize: String, deserialize: Number });\n```\n\n### `createPersistedMap<K, V>(key, options?)`\n\n`Map<K, V>` backed by localStorage.\n\n```tsx\nconst prefs = createPersistedMap<string, string>('myapp:prefs');\nprefs.set('view', 'compact');\nprefs.get('view'); // 'compact'\n```\n\n### `createMediaQuery(query)`\n\nReactive media query matcher.\n\n```tsx\nconst isWide = createMediaQuery('(min-width: 768px)');\n<Show when={isWide()}>Wide layout</Show>\n```\n\n### `createResizeObserver(elAccessor, handler)`\n\nObserves element size changes.\n\n```tsx\ncreateResizeObserver(() => containerEl, (entry) => {\n  setWidth(entry.contentRect.width);\n});\n```\n\n### `createIntersectionObserver(elAccessor, handler, options?)`\n\nObserves viewport intersection.\n\n```tsx\ncreateIntersectionObserver(() => targetEl, (entry) => {\n  if (entry.isIntersecting) loadMore();\n}, { threshold: 0.5 });\n```\n\n### `createDebounce(source, ms)`\n\nReturns a debounced accessor. Rapid `source` changes reset the delay.\n\n```tsx\nconst [query, setQuery] = createSignal('');\nconst debouncedQuery = createDebounce(query, 300);\ncreateEffect(() => search(debouncedQuery()));\n```\n\n### `createDebouncedCallback(fn, ms)`\n\nWraps a callback so repeated calls collapse to one invocation after `ms` ms of stillness. Pending calls are cancelled on dispose. `isPending` is a reactive accessor — true between a `call()` and the firing of `fn` (or the intervening `cancel()` / `flush()`); useful for \"saving…\" indicators.\n\n```tsx\nconst { call, cancel, flush, isPending } = createDebouncedCallback(saveToServer, 3000);\ntextarea.addEventListener('input', (e) => call(e.target.value));\n// In JSX:\n<Show when={isPending()}><span class=\"text-muted\">Saving…</span></Show>\n```\n\n`isPending` flips back to false even if `fn` throws — it describes scheduling, not the in-flight call.\n\n### `createDocumentVisibility()`\n\nReactive `document.visibilityState`.\n\n```tsx\nconst visible = createDocumentVisibility();\ncreateEffect(() => {\n  if (visible() === 'hidden') pauseUpdates();\n});\n```\n\n### `createAsyncStatus(fn, options?)`\n\nState machine for async actions with timed auto-reset: `idle → loading → done/error → idle`. Useful for buttons that flash \"done\" or \"error\" briefly.\n\n```tsx\nconst { status, run } = createAsyncStatus(async () => {\n  return await saveData();\n}, { resetMs: 2000 });\n\n<button\n  onClick={() => run()}\n  disabled={status() === 'loading'}\n>\n  {status() === 'loading' ? 'Saving...' :\n   status() === 'done' ? 'Saved!' :\n   status() === 'error' ? 'Error' :\n   'Save'}\n</button>\n```\n\n### `createAfterPaint()`\n\nReturns `schedule(fn)` that runs `fn` on the next animation frame. A second\n`schedule()` call before the first fires cancels the pending frame (supersede).\nComponent cleanup cancels any still-pending frame.\n\nUse inside `createEffect` / `onMount` when you need post-DOM-flush measurement:\npopover positioning, focus-after-mount, scroll-indicator update,\nResizeObserver-driven reposition. Replaces bare `requestAnimationFrame(fn)`,\nwhich leaks a pending callback past component disposal.\n\n```tsx\nconst afterPaint = createAfterPaint();\ncreateEffect(() => {\n  if (open()) afterPaint(clampToViewport);\n});\n```\n\n### `createOutsideScrollDismiss(getOpen, getPanelEl, onDismiss, shouldSuppress?)`\n\nDismiss a popover/flyout when the page scrolls outside it. Anchored panels\ndesynchronize from their anchor when the page scrolls; closing on scroll is\nthe conventional fix.\n\nScrolls whose target lives inside `getPanelEl()` are ignored (so a scrollable\nlist inside the panel can wheel without dismissing). `shouldSuppress` extends\nthe in-panel filter to descendant surfaces that live OUTSIDE `panelEl` in the\nDOM (Portal'd popovers, modal dialogs) — pass a predicate matching your own\ndescendant convention.\n\n`capture: true` is used so non-window scroll containers also dismiss; the\nlistener is gated on `getOpen()` so closed panels pay zero per-frame cost.\n\n```tsx\nconst [open, setOpen] = createSignal(false);\nlet panelEl: HTMLDivElement | undefined;\ncreateOutsideScrollDismiss(open, () => panelEl, () => setOpen(false));\n```\n\n## Convention: accessor style\n\nElement-tracking hooks (`createResizeObserver`, `createIntersectionObserver`, and the `contains()` helper for `createClickOutside`) take an **element accessor** (`() => HTMLElement | undefined`), not a returned ref. The accessor is re-read on every callback, so refs that mount later (e.g., portaled panels) are picked up automatically.\n\nFor reactive gating, use `options.enabled` rather than returning `undefined` from the accessor:\n\n```tsx\ncreateClickOutside(contains(() => menuEl), close, { enabled: open });\n```\n\n## 2.0.0 migration\n\nAll hooks renamed from `use*` to `create*` to match SolidJS idiomatic convention\n(`createSignal`, `createMemo`, solid-primitives library). Drop-in rename:\n\n| Old (1.x) | New (2.x) |\n|---|---|\n| `useClickOutside` | `createClickOutside` |\n| `useEscapeKey` | `createEscapeKey` |\n| `useHotkey` | `createHotkey` |\n| `useLocalStorage` | `createLocalStorage` |\n| `useMediaQuery` | `createMediaQuery` |\n| `useResizeObserver` | `createResizeObserver` |\n| `useIntersectionObserver` | `createIntersectionObserver` |\n| `useDebounce` | `createDebounce` |\n| `useDebouncedCallback` | `createDebouncedCallback` |\n| `useDocumentVisibility` | `createDocumentVisibility` |\n| `useAsyncStatus` | `createAsyncStatus` |\n| `usePersistedSet` | `createPersistedSet` |\n| `usePersistedMap` | `createPersistedMap` |\n\nOption / return interface type names follow the same pattern: `UseHotkeyOptions` →\n`CreateHotkeyOptions`, etc.\n\nThe `contains` utility is **not** renamed (it isn't a hook).\n\n## License\n\nMIT\n","readmeFilename":"README.md"}