@ai-matrx/kit 0.5.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 +132 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/autosave.cjs +92 -0
- package/dist/autosave.cjs.map +1 -0
- package/dist/autosave.d.cts +17 -0
- package/dist/autosave.d.ts +17 -0
- package/dist/autosave.js +72 -0
- package/dist/autosave.js.map +1 -0
- package/dist/clipboard.cjs +152 -0
- package/dist/clipboard.cjs.map +1 -0
- package/dist/clipboard.d.cts +17 -0
- package/dist/clipboard.d.ts +17 -0
- package/dist/clipboard.js +132 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/color-util.cjs +790 -0
- package/dist/color-util.cjs.map +1 -0
- package/dist/color-util.d.cts +235 -0
- package/dist/color-util.d.ts +235 -0
- package/dist/color-util.js +767 -0
- package/dist/color-util.js.map +1 -0
- package/dist/concurrency.cjs +59 -0
- package/dist/concurrency.cjs.map +1 -0
- package/dist/concurrency.d.cts +19 -0
- package/dist/concurrency.d.ts +19 -0
- package/dist/concurrency.js +38 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/confirm.cjs +344 -0
- package/dist/confirm.cjs.map +1 -0
- package/dist/confirm.d.cts +132 -0
- package/dist/confirm.d.ts +132 -0
- package/dist/confirm.js +312 -0
- package/dist/confirm.js.map +1 -0
- package/dist/delimiter-guard.cjs +172 -0
- package/dist/delimiter-guard.cjs.map +1 -0
- package/dist/delimiter-guard.d.cts +119 -0
- package/dist/delimiter-guard.d.ts +119 -0
- package/dist/delimiter-guard.js +151 -0
- package/dist/delimiter-guard.js.map +1 -0
- package/dist/drafts.cjs +223 -0
- package/dist/drafts.cjs.map +1 -0
- package/dist/drafts.d.cts +62 -0
- package/dist/drafts.d.ts +62 -0
- package/dist/drafts.js +201 -0
- package/dist/drafts.js.map +1 -0
- package/dist/idb-store.cjs +178 -0
- package/dist/idb-store.cjs.map +1 -0
- package/dist/idb-store.d.cts +111 -0
- package/dist/idb-store.d.ts +111 -0
- package/dist/idb-store.js +155 -0
- package/dist/idb-store.js.map +1 -0
- package/dist/idle-scheduler.cjs +268 -0
- package/dist/idle-scheduler.cjs.map +1 -0
- package/dist/idle-scheduler.d.cts +185 -0
- package/dist/idle-scheduler.d.ts +185 -0
- package/dist/idle-scheduler.js +246 -0
- package/dist/idle-scheduler.js.map +1 -0
- package/dist/index.cjs +3113 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +897 -0
- package/dist/index.d.ts +897 -0
- package/dist/index.js +3088 -0
- package/dist/index.js.map +1 -0
- package/dist/invalidation.cjs +64 -0
- package/dist/invalidation.cjs.map +1 -0
- package/dist/invalidation.d.cts +52 -0
- package/dist/invalidation.d.ts +52 -0
- package/dist/invalidation.js +43 -0
- package/dist/invalidation.js.map +1 -0
- package/dist/json-format.cjs +319 -0
- package/dist/json-format.cjs.map +1 -0
- package/dist/json-format.d.cts +148 -0
- package/dist/json-format.d.ts +148 -0
- package/dist/json-format.js +286 -0
- package/dist/json-format.js.map +1 -0
- package/dist/latest-request.cjs +36 -0
- package/dist/latest-request.cjs.map +1 -0
- package/dist/latest-request.d.cts +29 -0
- package/dist/latest-request.d.ts +29 -0
- package/dist/latest-request.js +16 -0
- package/dist/latest-request.js.map +1 -0
- package/dist/qr.cjs +109 -0
- package/dist/qr.cjs.map +1 -0
- package/dist/qr.d.cts +47 -0
- package/dist/qr.d.ts +47 -0
- package/dist/qr.js +78 -0
- package/dist/qr.js.map +1 -0
- package/dist/react-tree.cjs +69 -0
- package/dist/react-tree.cjs.map +1 -0
- package/dist/react-tree.d.cts +38 -0
- package/dist/react-tree.d.ts +38 -0
- package/dist/react-tree.js +38 -0
- package/dist/react-tree.js.map +1 -0
- package/dist/search-scoring.cjs +116 -0
- package/dist/search-scoring.cjs.map +1 -0
- package/dist/search-scoring.d.cts +77 -0
- package/dist/search-scoring.d.ts +77 -0
- package/dist/search-scoring.js +95 -0
- package/dist/search-scoring.js.map +1 -0
- package/dist/text-case.cjs +313 -0
- package/dist/text-case.cjs.map +1 -0
- package/dist/text-case.d.cts +53 -0
- package/dist/text-case.d.ts +53 -0
- package/dist/text-case.js +292 -0
- package/dist/text-case.js.map +1 -0
- package/dist/toast.cjs +74 -0
- package/dist/toast.cjs.map +1 -0
- package/dist/toast.d.cts +92 -0
- package/dist/toast.d.ts +92 -0
- package/dist/toast.js +54 -0
- package/dist/toast.js.map +1 -0
- package/dist/url-state.cjs +212 -0
- package/dist/url-state.cjs.map +1 -0
- package/dist/url-state.d.cts +90 -0
- package/dist/url-state.d.ts +90 -0
- package/dist/url-state.js +199 -0
- package/dist/url-state.js.map +1 -0
- package/package.json +286 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.5.0 — 2026-08-29
|
|
4
|
+
|
|
5
|
+
- Added four subpaths, ported from their Matrx frontend originals:
|
|
6
|
+
- `@ai-matrx/kit/idb-store` — the typed, versioned IndexedDB wrapper
|
|
7
|
+
(`DBStoreManager` / `PublicStoreManager` / `FeatureStore`): declare object
|
|
8
|
+
stores and indexes in a `setupStores` upgrade hook; every operation resolves
|
|
9
|
+
`{ data, error }` and never throws, before or after init. Carries the `idb`
|
|
10
|
+
runtime dependency (the typed wrapper is the engine). The original's
|
|
11
|
+
static-field singleton convention is replaced by `getIdbStoreSingleton(key,
|
|
12
|
+
create)`, backed by `globalThis` under
|
|
13
|
+
`Symbol.for("ai-matrx.kit.idb-store-state")` so the ESM and CJS bundle
|
|
14
|
+
graphs share one instance registry (proven cross-graph by the tarball
|
|
15
|
+
canary).
|
|
16
|
+
- `@ai-matrx/kit/color-util` — the bidirectional Tailwind-token color
|
|
17
|
+
mapping, zero dependencies: `getColorFromTailwind` / fuzzy
|
|
18
|
+
`formatTailwindColor` (token → hex, with the verbatim palette table) and
|
|
19
|
+
`findNearestTailwindColor` (color → token). The original required a colord
|
|
20
|
+
instance; the seam is now structural — any `{ delta(hex) }` object still
|
|
21
|
+
works verbatim, and plain hex/`rgb()` strings are measured by a built-in
|
|
22
|
+
sRGB→Lab→CIEDE2000 engine reproducing the colord lab plugin's exact math
|
|
23
|
+
(cross-checked bit-identical against colord in the test suite). Also ports
|
|
24
|
+
the pure `format*` input normalizers and `createColorNormalizer({ isValid })`
|
|
25
|
+
(the `normalizeColorInput` waterfall with the validity check injected). The
|
|
26
|
+
one-line colord passthroughs (`isValidColor`, `convertColor`,
|
|
27
|
+
`getColorInfo`, `getColorString`, `getColorFormats`) deliberately stay
|
|
28
|
+
host-side — packaging them would drag colord in as a runtime dependency.
|
|
29
|
+
- `@ai-matrx/kit/react-tree` — `treeContainsComponent`, the safe React
|
|
30
|
+
children-tree scan (elements, arrays, fragments, iterables, portals; skips
|
|
31
|
+
non-renderable children with a loud dev scream instead of crashing at the
|
|
32
|
+
probe). Now the ONE home of the scanner: the `/confirm` subpath's internal
|
|
33
|
+
copy was removed and its alert-dialog imports this module.
|
|
34
|
+
- `@ai-matrx/kit/qr` — the platform QR decoder, verbatim: File/Blob,
|
|
35
|
+
`ImageData`, or `<video>`/`<canvas>` in; the encoded text or `null` out.
|
|
36
|
+
Native `BarcodeDetector` first with graceful capability detection
|
|
37
|
+
(`hasNativeQrDetector`); the `jsqr` runtime dependency is lazily imported
|
|
38
|
+
only when the native detector is absent.
|
|
39
|
+
- Fixed a defect present in the original `treeContainsComponent` (and the
|
|
40
|
+
0.3.0 confirm-internal copy): a React portal passed as a child is a valid
|
|
41
|
+
child, but it fell through to the non-renderable branch — a false-positive
|
|
42
|
+
dev scream, and the portal's content was never scanned. Portals are now
|
|
43
|
+
recognized and traversed.
|
|
44
|
+
- Fixed a defect present in the original `FeatureStore`: the constructor's
|
|
45
|
+
fire-and-forget `initDB()` left a failed `openDB` (e.g. private-mode storage
|
|
46
|
+
denial) as an unhandled promise rejection on top of the logged error. The
|
|
47
|
+
rejection is now absorbed after `initDB`'s own loud log; operations still
|
|
48
|
+
report "Database not initialized" through their `AsyncResult`.
|
|
49
|
+
- Removed (deliberate divergence, logged here rather than silently): the
|
|
50
|
+
unconditional debug `console.log`s the original color utilities emitted on
|
|
51
|
+
every `formatLabString` / `formatLchString` / `normalizeColorInput` call.
|
|
52
|
+
Matching and return behavior are unchanged.
|
|
53
|
+
|
|
54
|
+
## 0.4.0 — 2026-08-29
|
|
55
|
+
|
|
56
|
+
- Added five subpaths, ported from their Matrx frontend originals:
|
|
57
|
+
- `@ai-matrx/kit/invalidation` — the zero-import name-keyed callback registry that
|
|
58
|
+
breaks import edges into heavy chunk clusters. Generic over names: the app-specific
|
|
59
|
+
key constants stay in the host. Registry state lives on `globalThis` under
|
|
60
|
+
`Symbol.for("ai-matrx.kit.invalidation-registry")` so the ESM and CJS bundle graphs
|
|
61
|
+
share one callback map (proven by the tarball canary's cross-graph register/fire).
|
|
62
|
+
- `@ai-matrx/kit/delimiter-guard` — keeps a stray streamed `$$` or `[` from swallowing
|
|
63
|
+
a section of markdown. Verbatim port; the app's `captureError` import inverted to an
|
|
64
|
+
optional injected `capture` sink on `reportDelimiterViolations` (payload shape
|
|
65
|
+
preserved exactly; console-only loud recovery without a sink).
|
|
66
|
+
- `@ai-matrx/kit/json-format` — JSON detection (fences, surrounding prose, tolerant
|
|
67
|
+
JSON5 tier, half-streamed/unterminated fences) + the three-style writer
|
|
68
|
+
(minify / width-aware compact / pretty, key sorting, fence add/strip/preserve).
|
|
69
|
+
Verbatim port incl. the original test suite; carries the `json5` runtime dependency
|
|
70
|
+
(tolerant parsing is the product) and its own `JsonValue` types so it stands alone.
|
|
71
|
+
- `@ai-matrx/kit/idle-scheduler` — priority-aware deferred execution
|
|
72
|
+
(load → paint → true browser idle, with the hidden-tab and no-registration-page
|
|
73
|
+
fixes) plus the four React hooks. Scheduler state moved to `globalThis` under
|
|
74
|
+
`Symbol.for("ai-matrx.kit.idle-scheduler-state")` (dual-graph rule); the
|
|
75
|
+
`window.__idleSched` diagnostics probe now installs on first use instead of at
|
|
76
|
+
import (entry points stay import-inert).
|
|
77
|
+
- `@ai-matrx/kit/url-state` — the canonical URL-state core: `useUrlState` + codecs,
|
|
78
|
+
`useMirroredUrlState`, `useUrlSearchParams`, `commitUrlParams`,
|
|
79
|
+
`historyModeForParamChange`. The original imports nothing from `next/navigation`
|
|
80
|
+
(History API + sync event) and that default is ported byte-identically; new
|
|
81
|
+
OPTIONAL `setUrlStateRouter({ push, replace, getLocation? })` seam for hosts whose
|
|
82
|
+
router must own the writes, stored on `globalThis` under
|
|
83
|
+
`Symbol.for("ai-matrx.kit.url-state-router")`.
|
|
84
|
+
- Fixed a defect present in the original idle scheduler: a task registered WHILE the
|
|
85
|
+
flush loop was running landed back in the queue and silently never ran (the code's
|
|
86
|
+
own comment promised it would be scheduled post-flush). Late-during-flush
|
|
87
|
+
registrations now drain through the deferred chain after the flush completes.
|
|
88
|
+
- Fixed the "use client" stamping policy: the banner previously led EVERY dist chunk,
|
|
89
|
+
which turns a pure module's exports into client references the moment a React Server
|
|
90
|
+
Component imports it. The build now stamps only React-touching chunks (autosave,
|
|
91
|
+
latest-request, clipboard, drafts, confirm, toast, idle-scheduler, url-state, index);
|
|
92
|
+
the pure chunks (search-scoring, concurrency, text-case, invalidation,
|
|
93
|
+
delimiter-guard, json-format) ship unstamped and are RSC-safe.
|
|
94
|
+
|
|
95
|
+
## 0.3.0 — 2026-08-29
|
|
96
|
+
|
|
97
|
+
- Added `@ai-matrx/kit/confirm` — the browser-dialog replacement, ported from the Matrx
|
|
98
|
+
frontend confirm-dialog system: imperative `await confirm({...})` through a
|
|
99
|
+
mount-once `<ConfirmDialogHost/>` (pre-mount calls queue, concurrent calls present
|
|
100
|
+
sequentially, no host means the promise stays pending — never a silent default), plus
|
|
101
|
+
the declarative `<ConfirmDialog/>` for flows where in-dialog busy state matters. The
|
|
102
|
+
Radix alert-dialog wrapper is inlined; `@radix-ui/react-alert-dialog` and
|
|
103
|
+
`tailwind-merge` are this subpath's runtime dependencies. Styling keeps the Matrx
|
|
104
|
+
Tailwind semantic-token classes verbatim; hosts on other design systems override via
|
|
105
|
+
`className` / `contentClassName` (last-wins merge). The busy spinner is one inlined
|
|
106
|
+
SVG (lucide `loader-circle` v1.22.0) instead of a lucide-react dependency.
|
|
107
|
+
- Added `@ai-matrx/kit/toast` — the captured sonner wrapper as a factory:
|
|
108
|
+
`createMatrxToast({ toast, capture? })` returns a drop-in `toast` whose
|
|
109
|
+
`.error`/`.warning` also feed the injected capture sink (exact original payload
|
|
110
|
+
shape), plus `toastErrorAlreadyCaptured`. sonner is never imported — the host passes
|
|
111
|
+
its own `toast` object in and the input is typed structurally; the `sonner` peer is
|
|
112
|
+
optional and advisory-only.
|
|
113
|
+
|
|
114
|
+
## 0.2.0 — 2026-08-28
|
|
115
|
+
|
|
116
|
+
- Added four subpaths, ported verbatim from their Matrx frontend originals:
|
|
117
|
+
`/search-scoring` (relevance-weighted ranking), `/concurrency` (bounded worker
|
|
118
|
+
pool), `/text-case` (acronym-aware formatter), and `/drafts` (`useDurableDraft`
|
|
119
|
+
+ the local-drafts store, with its original test suite).
|
|
120
|
+
|
|
121
|
+
## 0.1.0 — 2026-08-28
|
|
122
|
+
|
|
123
|
+
- Created the package with the first three subpaths, ported from the Matrx frontend
|
|
124
|
+
where each primitive originated: `useAutosave`, `useLatestRequest`, `useClipboard`
|
|
125
|
+
(toast coupling inverted to an injected notifier).
|
|
126
|
+
- Fixed a defect present in the original `useAutosave`: a failed save's re-queued
|
|
127
|
+
payload was drained again immediately, producing an unbounded retry loop against a
|
|
128
|
+
persistently failing save. The package re-queues for the NEXT schedule/flush, exactly
|
|
129
|
+
as the status contract promises.
|
|
130
|
+
- React >=18 peer, zero dependencies; ESM + CommonJS with `"use client"` stamped on
|
|
131
|
+
built chunks; the release gate installs the packed tarball into an empty project and
|
|
132
|
+
loads every entry point through both `import` and `require`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AI Matrix Engine
|
|
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
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @ai-matrx/kit
|
|
2
|
+
|
|
3
|
+
The always-include AI Matrx kit: the little primitives every Matrx application speaks,
|
|
4
|
+
one capability per subpath, tree-shaken to what you use. React is the only required
|
|
5
|
+
peer; only four subpaths carry runtime dependencies, each because the dependency IS
|
|
6
|
+
the capability: `/confirm` (`@radix-ui/react-alert-dialog` + `tailwind-merge` — its
|
|
7
|
+
product is the dialog), `/json-format` (`json5` — tolerant parsing is the feature),
|
|
8
|
+
`/idb-store` (`idb` — the typed IndexedDB wrapper is the engine), and `/qr` (`jsqr`,
|
|
9
|
+
loaded lazily only when the browser has no native `BarcodeDetector`).
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @ai-matrx/kit
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Subpath | Export | What it does |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `@ai-matrx/kit/autosave` | `useAutosave` | Debounced autosave with in-flight coalescing, a UI status (`idle/unsaved/saving/saved/error`), loud non-throwing failures that re-queue the payload, and flush-on-unmount so no keystroke is ever lost. |
|
|
18
|
+
| `@ai-matrx/kit/latest-request` | `useLatestRequest` | Makes a superseded async response impossible to apply: claim an attempt, then check the predicate before every state write after an `await`. Kills the out-of-order-response bug (the wrong record under a confident label) at the only moment that matters — apply time. Render-stable by construction. |
|
|
19
|
+
| `@ai-matrx/kit/clipboard` | `useClipboard` | Copy/paste text, links, and images with the browser quirks solved: canvas→PNG re-encode for image copy, Safari `clipboard.read` feature detection, param-stripping link copy. Notifier injected — wrap your own toast. |
|
|
20
|
+
|
|
21
|
+
| `@ai-matrx/kit/search-scoring` | `filterAndSortBySearch`, `computeSearchScore`, `matchesSearch`, `idMatchesQuery` | Relevance-weighted search ranking — title above description, exact above prefix above includes, pasted-UUID matching — instead of the naive `.includes()` filter. |
|
|
22
|
+
| `@ai-matrx/kit/concurrency` | `runWithConcurrency` | Bounded worker pool over an array with per-item failure isolation and a cooperative `shouldStart()` cancel hook. |
|
|
23
|
+
| `@ai-matrx/kit/text-case` | `formatText`, `createFormatter` | Acronym-aware normalization of messy DB/API strings into title/sentence case with word-replacement overrides. |
|
|
24
|
+
| `@ai-matrx/kit/drafts` | `useDurableDraft` + the local-drafts store | User-authored text that survives anything: write-through localStorage drafts, restore on mount, key-change adoption, cleared only after content durably landed. |
|
|
25
|
+
| `@ai-matrx/kit/confirm` | `confirm`, `ConfirmDialogHost`, `ConfirmDialog` | The `window.confirm` replacement, both shapes: mount `<ConfirmDialogHost/>` once at app root, then `await confirm({ title, variant: "destructive" })` from anywhere (pre-mount calls queue; concurrent calls present one at a time) — or the declarative `<ConfirmDialog/>` when in-dialog busy state matters. Bundles `@radix-ui/react-alert-dialog`; Tailwind semantic-token styling, overridable via `className`/`contentClassName`. |
|
|
26
|
+
| `@ai-matrx/kit/toast` | `createMatrxToast` | The captured sonner wrapper as a factory: pass your sonner `toast` in, get a drop-in `toast` back whose `.error`/`.warning` also feed your injected error-capture sink, plus `toastErrorAlreadyCaptured` for notices whose failure was already captured upstream. No sonner import — the peer is advisory-only. |
|
|
27
|
+
| `@ai-matrx/kit/invalidation` | `registerInvalidationCallback`, `fireInvalidation` | Zero-import, name-keyed callback registry that breaks import edges into heavy chunk clusters: the heavy cluster registers at its own module init, the ubiquitous module fires by name. Unregistered names are a deliberate no-op; a throwing callback screams and never breaks the caller. Keep your key constants in ONE host module. |
|
|
28
|
+
| `@ai-matrx/kit/delimiter-guard` | `guardMarkdownDelimiters` (+ `guardMathDelimiters`, `guardRunawayLinks`, `reportDelimiterViolations`) | Keeps one stray streamed delimiter from swallowing a whole section: a `$$` that would turn prose into a red KaTeX error span, or an unclosed `[` that would turn a section into one giant link, gets invisibly neutralized while real math and links keep rendering. Pure; report violations loudly via the optional injected capture sink. |
|
|
29
|
+
| `@ai-matrx/kit/json-format` | `detectJson`, `formatJsonText`, `stringifyJson` | "Is this text JSON, and what shape should it be in?" Detection survives code fences, surrounding prose, and tolerant (JSON5) input — `looksLikeJson` gates offering, `ok` gates running. The writer does `minify` / width-aware `compact` (inline + pack) / `pretty`, key sorting, fence add/strip/preserve. Never throws, never mangles text it did not parse. |
|
|
30
|
+
| `@ai-matrx/kit/idle-scheduler` | `useIdleTask`, `useIdleReady`, `useIdleGate`, `useIdleRegister` + `registerIdleTask`, `onFlushComplete`, `whenPageIdle` | Priority-aware deferred execution: work waits for document load → paint → true browser idle (`scheduler.postTask` / `requestIdleCallback` / MessageChannel fallback, hidden tabs still flush), then runs 1-first…5-last. Zero re-renders to register; one re-render for the ready signal. |
|
|
31
|
+
| `@ai-matrx/kit/url-state` | `useUrlState` + codecs, `useMirroredUrlState`, `useUrlSearchParams`, `commitUrlParams` | The URL as the source of truth for view state: refresh, copied links, and Back/Forward reproduce the same view. Codecs (string/enum/boolean/positive-integer/JSON) omit defaults so pristine URLs stay clean; discrete decisions push, high-frequency text replaces; every write notifies every other URL-backed control on the page. |
|
|
32
|
+
| `@ai-matrx/kit/idb-store` | `DBStoreManager`, `PublicStoreManager`, `FeatureStore`, `getIdbStoreSingleton` | Typed, versioned IndexedDB stores over `idb`: subclass, declare your object stores/indexes in `setupStores`, and every operation resolves `{ data, error }` — never throws, before or after init. Hold each store's one instance with `getIdbStoreSingleton(key, create)` (a `globalThis` slot, so the ESM and CJS graphs share it). |
|
|
33
|
+
| `@ai-matrx/kit/color-util` | `findNearestTailwindColor`, `getColorFromTailwind`, `formatTailwindColor`, `format*` normalizers, `createColorNormalizer`, `tailwindColors` | The bidirectional Tailwind-token color mapping with zero dependencies: token → hex lookups (including fuzzy input like `"skyblue598"`), and hex/`rgb()` → nearest token by perceptual CIEDE2000 distance (bit-identical to the colord lab plugin; a colord instance also plugs straight into the structural `{ delta(hex) }` seam). Plus the pure paste-anything input normalizers and the injected-validity `normalizeColorInput` waterfall. |
|
|
34
|
+
| `@ai-matrx/kit/react-tree` | `treeContainsComponent` | Safe React children-tree scan for optional a11y children (is a `DialogDescription` anywhere under these children?): walks elements, arrays, fragments, iterables, and portals by hand, and SKIPS a non-renderable child with a loud dev scream instead of crashing at the probe like `React.Children.toArray` would. |
|
|
35
|
+
| `@ai-matrx/kit/qr` | `decodeQrFromImageFile`, `decodeQrFromElement`, `decodeQrFromImageData`, `hasNativeQrDetector` | THE QR decoder, in memory and never uploaded: a pasted screenshot, a dropped file, or a live `<video>`/`<canvas>` frame in; the encoded text (or `null`) out. Native `BarcodeDetector` first; the pure-JS `jsqr` fallback is lazily imported only in browsers that lack it. |
|
|
36
|
+
|
|
37
|
+
More subpaths are on the roadmap: copy-for-ai.
|
|
38
|
+
|
|
39
|
+
## Host wiring: `/url-state`
|
|
40
|
+
|
|
41
|
+
No wiring is required. The module writes through the History API
|
|
42
|
+
(`history.pushState`/`replaceState`) and dispatches its own sync event — this
|
|
43
|
+
is exactly how it runs inside the Next.js App Router host it was extracted
|
|
44
|
+
from; do NOT pass the Next.js router. Inject a router only when your host's
|
|
45
|
+
router must observe or perform the URL writes (react-router, a memory router
|
|
46
|
+
in tests, a native shell):
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { setUrlStateRouter } from "@ai-matrx/kit/url-state";
|
|
50
|
+
|
|
51
|
+
setUrlStateRouter({
|
|
52
|
+
push: (url) => myRouter.navigate(url), // full relative URL: pathname?query#hash
|
|
53
|
+
replace: (url) => myRouter.navigate(url, { replace: true }),
|
|
54
|
+
// Optional: where the current URL is read from (default: window.location)
|
|
55
|
+
getLocation: () => ({ pathname, search, hash }),
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Call it once at app boot (`setUrlStateRouter(null)` removes it). With no
|
|
60
|
+
router injected, behavior is byte-identical to the Matrx frontend original.
|
|
61
|
+
|
|
62
|
+
## Design
|
|
63
|
+
|
|
64
|
+
- One package, many subpaths — you always include the kit; you only bundle what you import.
|
|
65
|
+
- Injected hosts (the clipboard notifier), opinionated behavior (the autosave status
|
|
66
|
+
contract, the apply-time guard). The opinions are the product.
|
|
67
|
+
- ESM + CommonJS conditions with matching declarations, proven from the packed tarball.
|
|
68
|
+
The `"use client"` boundary is stamped per chunk: React-touching subpaths carry it,
|
|
69
|
+
pure ones (search-scoring, concurrency, text-case, invalidation, delimiter-guard,
|
|
70
|
+
json-format, idb-store, color-util, react-tree, qr) ship unstamped and RSC-safe.
|
|
71
|
+
|
|
72
|
+
MIT © AI Matrx
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
"use client";
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// src/autosave.ts
|
|
23
|
+
var autosave_exports = {};
|
|
24
|
+
__export(autosave_exports, {
|
|
25
|
+
useAutosave: () => useAutosave
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(autosave_exports);
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
function useAutosave(opts) {
|
|
30
|
+
const { save, debounceMs = 900 } = opts;
|
|
31
|
+
const [status, setStatus] = (0, import_react.useState)("idle");
|
|
32
|
+
const [lastSavedAt, setLastSavedAt] = (0, import_react.useState)(null);
|
|
33
|
+
const pendingRef = (0, import_react.useRef)(null);
|
|
34
|
+
const timerRef = (0, import_react.useRef)(null);
|
|
35
|
+
const savingRef = (0, import_react.useRef)(false);
|
|
36
|
+
const saveRef = (0, import_react.useRef)(save);
|
|
37
|
+
(0, import_react.useEffect)(() => {
|
|
38
|
+
saveRef.current = save;
|
|
39
|
+
});
|
|
40
|
+
async function saveNow() {
|
|
41
|
+
if (savingRef.current) return;
|
|
42
|
+
const pending = pendingRef.current;
|
|
43
|
+
if (!pending) return;
|
|
44
|
+
pendingRef.current = null;
|
|
45
|
+
savingRef.current = true;
|
|
46
|
+
setStatus("saving");
|
|
47
|
+
let failed = false;
|
|
48
|
+
try {
|
|
49
|
+
const res = await saveRef.current(pending.value);
|
|
50
|
+
if (res.error) {
|
|
51
|
+
failed = true;
|
|
52
|
+
pendingRef.current = pendingRef.current ?? pending;
|
|
53
|
+
setStatus("error");
|
|
54
|
+
} else {
|
|
55
|
+
setLastSavedAt(/* @__PURE__ */ new Date());
|
|
56
|
+
setStatus(pendingRef.current ? "unsaved" : "saved");
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
failed = true;
|
|
60
|
+
pendingRef.current = pendingRef.current ?? pending;
|
|
61
|
+
setStatus("error");
|
|
62
|
+
} finally {
|
|
63
|
+
savingRef.current = false;
|
|
64
|
+
if (pendingRef.current && !failed) void saveNow();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function schedule(value) {
|
|
68
|
+
pendingRef.current = { value };
|
|
69
|
+
setStatus("unsaved");
|
|
70
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
71
|
+
timerRef.current = setTimeout(() => void saveNow(), debounceMs);
|
|
72
|
+
}
|
|
73
|
+
function flush() {
|
|
74
|
+
if (timerRef.current) {
|
|
75
|
+
clearTimeout(timerRef.current);
|
|
76
|
+
timerRef.current = null;
|
|
77
|
+
}
|
|
78
|
+
void saveNow();
|
|
79
|
+
}
|
|
80
|
+
(0, import_react.useEffect)(() => {
|
|
81
|
+
return () => {
|
|
82
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
83
|
+
const pending = pendingRef.current;
|
|
84
|
+
if (pending && !savingRef.current) {
|
|
85
|
+
pendingRef.current = null;
|
|
86
|
+
void saveRef.current(pending.value);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}, []);
|
|
90
|
+
return { status, lastSavedAt, schedule, flush };
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=autosave.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/autosave.ts"],"sourcesContent":["// A small, generic autosave primitive: debounce a payload, persist it through a\n// caller-supplied async save, and expose a status a UI can show (\"Saving…\" /\n// \"Saved\" / \"Unsaved changes\" / error). Entity-agnostic — the caller owns WHAT\n// to save (the save fn) and WHAT the payload is; this owns the debounce, the\n// in-flight coalescing, the status, and the flush-on-unmount so no keystroke is\n// ever lost. (Notes have their own coupled version; this is the reusable one.)\n// Never throws: the save fn returns `{ error }` (supabase-service style) and a\n// non-null error flips status to \"error\" and re-queues, so a blocked write is\n// loud, not silent.\n// React Compiler is on: no manual useMemo / useCallback — plain closures.\n\n\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\nexport type AutosaveStatus = \"idle\" | \"unsaved\" | \"saving\" | \"saved\" | \"error\";\n\nexport interface UseAutosaveResult<T> {\n status: AutosaveStatus;\n lastSavedAt: Date | null;\n /** Queue a payload and (re)start the debounce. */\n schedule: (value: T) => void;\n /** Cancel the debounce and save the pending payload immediately. */\n flush: () => void;\n}\n\nexport function useAutosave<T>(opts: {\n save: (value: T) => Promise<{ error: string | null }>;\n debounceMs?: number;\n}): UseAutosaveResult<T> {\n const { save, debounceMs = 900 } = opts;\n const [status, setStatus] = useState<AutosaveStatus>(\"idle\");\n const [lastSavedAt, setLastSavedAt] = useState<Date | null>(null);\n\n const pendingRef = useRef<{ value: T } | null>(null);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const savingRef = useRef(false);\n // Keep the latest save fn without threading it through closures (updated in an\n // effect, never during render).\n const saveRef = useRef(save);\n useEffect(() => {\n saveRef.current = save;\n });\n\n async function saveNow(): Promise<void> {\n if (savingRef.current) return; // a later debounce flush picks up new edits\n const pending = pendingRef.current;\n if (!pending) return;\n pendingRef.current = null;\n savingRef.current = true;\n setStatus(\"saving\");\n let failed = false;\n try {\n const res = await saveRef.current(pending.value);\n if (res.error) {\n // Re-queue so the NEXT schedule/flush retries; surface loudly.\n failed = true;\n pendingRef.current = pendingRef.current ?? pending;\n setStatus(\"error\");\n } else {\n setLastSavedAt(new Date());\n setStatus(pendingRef.current ? \"unsaved\" : \"saved\");\n }\n } catch {\n failed = true;\n pendingRef.current = pendingRef.current ?? pending;\n setStatus(\"error\");\n } finally {\n savingRef.current = false;\n // If edits arrived mid-save, drain them promptly. NEVER drain after a\n // failure: the re-queued payload waits for the next schedule/flush —\n // draining it here is an unbounded immediate-retry loop against a\n // persistently failing save. (The matrx-frontend original has exactly\n // that loop; it dies with the original at the adoption swap.)\n if (pendingRef.current && !failed) void saveNow();\n }\n }\n\n function schedule(value: T): void {\n pendingRef.current = { value };\n setStatus(\"unsaved\");\n if (timerRef.current) clearTimeout(timerRef.current);\n timerRef.current = setTimeout(() => void saveNow(), debounceMs);\n }\n\n function flush(): void {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n void saveNow();\n }\n\n // Flush any pending payload on unmount so an in-progress edit is never lost.\n useEffect(() => {\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n const pending = pendingRef.current;\n if (pending && !savingRef.current) {\n pendingRef.current = null;\n void saveRef.current(pending.value);\n }\n };\n }, []);\n\n return { status, lastSavedAt, schedule, flush };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,mBAA4C;AAarC,SAAS,YAAe,MAGN;AACvB,QAAM,EAAE,MAAM,aAAa,IAAI,IAAI;AACnC,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAAyB,MAAM;AAC3D,QAAM,CAAC,aAAa,cAAc,QAAI,uBAAsB,IAAI;AAEhE,QAAM,iBAAa,qBAA4B,IAAI;AACnD,QAAM,eAAW,qBAA6C,IAAI;AAClE,QAAM,gBAAY,qBAAO,KAAK;AAG9B,QAAM,cAAU,qBAAO,IAAI;AAC3B,8BAAU,MAAM;AACd,YAAQ,UAAU;AAAA,EACpB,CAAC;AAED,iBAAe,UAAyB;AACtC,QAAI,UAAU,QAAS;AACvB,UAAM,UAAU,WAAW;AAC3B,QAAI,CAAC,QAAS;AACd,eAAW,UAAU;AACrB,cAAU,UAAU;AACpB,cAAU,QAAQ;AAClB,QAAI,SAAS;AACb,QAAI;AACF,YAAM,MAAM,MAAM,QAAQ,QAAQ,QAAQ,KAAK;AAC/C,UAAI,IAAI,OAAO;AAEb,iBAAS;AACT,mBAAW,UAAU,WAAW,WAAW;AAC3C,kBAAU,OAAO;AAAA,MACnB,OAAO;AACL,uBAAe,oBAAI,KAAK,CAAC;AACzB,kBAAU,WAAW,UAAU,YAAY,OAAO;AAAA,MACpD;AAAA,IACF,QAAQ;AACN,eAAS;AACT,iBAAW,UAAU,WAAW,WAAW;AAC3C,gBAAU,OAAO;AAAA,IACnB,UAAE;AACA,gBAAU,UAAU;AAMpB,UAAI,WAAW,WAAW,CAAC,OAAQ,MAAK,QAAQ;AAAA,IAClD;AAAA,EACF;AAEA,WAAS,SAAS,OAAgB;AAChC,eAAW,UAAU,EAAE,MAAM;AAC7B,cAAU,SAAS;AACnB,QAAI,SAAS,QAAS,cAAa,SAAS,OAAO;AACnD,aAAS,UAAU,WAAW,MAAM,KAAK,QAAQ,GAAG,UAAU;AAAA,EAChE;AAEA,WAAS,QAAc;AACrB,QAAI,SAAS,SAAS;AACpB,mBAAa,SAAS,OAAO;AAC7B,eAAS,UAAU;AAAA,IACrB;AACA,SAAK,QAAQ;AAAA,EACf;AAGA,8BAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,SAAS,QAAS,cAAa,SAAS,OAAO;AACnD,YAAM,UAAU,WAAW;AAC3B,UAAI,WAAW,CAAC,UAAU,SAAS;AACjC,mBAAW,UAAU;AACrB,aAAK,QAAQ,QAAQ,QAAQ,KAAK;AAAA,MACpC;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO,EAAE,QAAQ,aAAa,UAAU,MAAM;AAChD;","names":[]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type AutosaveStatus = "idle" | "unsaved" | "saving" | "saved" | "error";
|
|
2
|
+
interface UseAutosaveResult<T> {
|
|
3
|
+
status: AutosaveStatus;
|
|
4
|
+
lastSavedAt: Date | null;
|
|
5
|
+
/** Queue a payload and (re)start the debounce. */
|
|
6
|
+
schedule: (value: T) => void;
|
|
7
|
+
/** Cancel the debounce and save the pending payload immediately. */
|
|
8
|
+
flush: () => void;
|
|
9
|
+
}
|
|
10
|
+
declare function useAutosave<T>(opts: {
|
|
11
|
+
save: (value: T) => Promise<{
|
|
12
|
+
error: string | null;
|
|
13
|
+
}>;
|
|
14
|
+
debounceMs?: number;
|
|
15
|
+
}): UseAutosaveResult<T>;
|
|
16
|
+
|
|
17
|
+
export { type AutosaveStatus, type UseAutosaveResult, useAutosave };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type AutosaveStatus = "idle" | "unsaved" | "saving" | "saved" | "error";
|
|
2
|
+
interface UseAutosaveResult<T> {
|
|
3
|
+
status: AutosaveStatus;
|
|
4
|
+
lastSavedAt: Date | null;
|
|
5
|
+
/** Queue a payload and (re)start the debounce. */
|
|
6
|
+
schedule: (value: T) => void;
|
|
7
|
+
/** Cancel the debounce and save the pending payload immediately. */
|
|
8
|
+
flush: () => void;
|
|
9
|
+
}
|
|
10
|
+
declare function useAutosave<T>(opts: {
|
|
11
|
+
save: (value: T) => Promise<{
|
|
12
|
+
error: string | null;
|
|
13
|
+
}>;
|
|
14
|
+
debounceMs?: number;
|
|
15
|
+
}): UseAutosaveResult<T>;
|
|
16
|
+
|
|
17
|
+
export { type AutosaveStatus, type UseAutosaveResult, useAutosave };
|
package/dist/autosave.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// src/autosave.ts
|
|
5
|
+
import { useEffect, useRef, useState } from "react";
|
|
6
|
+
function useAutosave(opts) {
|
|
7
|
+
const { save, debounceMs = 900 } = opts;
|
|
8
|
+
const [status, setStatus] = useState("idle");
|
|
9
|
+
const [lastSavedAt, setLastSavedAt] = useState(null);
|
|
10
|
+
const pendingRef = useRef(null);
|
|
11
|
+
const timerRef = useRef(null);
|
|
12
|
+
const savingRef = useRef(false);
|
|
13
|
+
const saveRef = useRef(save);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
saveRef.current = save;
|
|
16
|
+
});
|
|
17
|
+
async function saveNow() {
|
|
18
|
+
if (savingRef.current) return;
|
|
19
|
+
const pending = pendingRef.current;
|
|
20
|
+
if (!pending) return;
|
|
21
|
+
pendingRef.current = null;
|
|
22
|
+
savingRef.current = true;
|
|
23
|
+
setStatus("saving");
|
|
24
|
+
let failed = false;
|
|
25
|
+
try {
|
|
26
|
+
const res = await saveRef.current(pending.value);
|
|
27
|
+
if (res.error) {
|
|
28
|
+
failed = true;
|
|
29
|
+
pendingRef.current = pendingRef.current ?? pending;
|
|
30
|
+
setStatus("error");
|
|
31
|
+
} else {
|
|
32
|
+
setLastSavedAt(/* @__PURE__ */ new Date());
|
|
33
|
+
setStatus(pendingRef.current ? "unsaved" : "saved");
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
failed = true;
|
|
37
|
+
pendingRef.current = pendingRef.current ?? pending;
|
|
38
|
+
setStatus("error");
|
|
39
|
+
} finally {
|
|
40
|
+
savingRef.current = false;
|
|
41
|
+
if (pendingRef.current && !failed) void saveNow();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function schedule(value) {
|
|
45
|
+
pendingRef.current = { value };
|
|
46
|
+
setStatus("unsaved");
|
|
47
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
48
|
+
timerRef.current = setTimeout(() => void saveNow(), debounceMs);
|
|
49
|
+
}
|
|
50
|
+
function flush() {
|
|
51
|
+
if (timerRef.current) {
|
|
52
|
+
clearTimeout(timerRef.current);
|
|
53
|
+
timerRef.current = null;
|
|
54
|
+
}
|
|
55
|
+
void saveNow();
|
|
56
|
+
}
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
return () => {
|
|
59
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
60
|
+
const pending = pendingRef.current;
|
|
61
|
+
if (pending && !savingRef.current) {
|
|
62
|
+
pendingRef.current = null;
|
|
63
|
+
void saveRef.current(pending.value);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}, []);
|
|
67
|
+
return { status, lastSavedAt, schedule, flush };
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
useAutosave
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=autosave.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/autosave.ts"],"sourcesContent":["// A small, generic autosave primitive: debounce a payload, persist it through a\n// caller-supplied async save, and expose a status a UI can show (\"Saving…\" /\n// \"Saved\" / \"Unsaved changes\" / error). Entity-agnostic — the caller owns WHAT\n// to save (the save fn) and WHAT the payload is; this owns the debounce, the\n// in-flight coalescing, the status, and the flush-on-unmount so no keystroke is\n// ever lost. (Notes have their own coupled version; this is the reusable one.)\n// Never throws: the save fn returns `{ error }` (supabase-service style) and a\n// non-null error flips status to \"error\" and re-queues, so a blocked write is\n// loud, not silent.\n// React Compiler is on: no manual useMemo / useCallback — plain closures.\n\n\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\nexport type AutosaveStatus = \"idle\" | \"unsaved\" | \"saving\" | \"saved\" | \"error\";\n\nexport interface UseAutosaveResult<T> {\n status: AutosaveStatus;\n lastSavedAt: Date | null;\n /** Queue a payload and (re)start the debounce. */\n schedule: (value: T) => void;\n /** Cancel the debounce and save the pending payload immediately. */\n flush: () => void;\n}\n\nexport function useAutosave<T>(opts: {\n save: (value: T) => Promise<{ error: string | null }>;\n debounceMs?: number;\n}): UseAutosaveResult<T> {\n const { save, debounceMs = 900 } = opts;\n const [status, setStatus] = useState<AutosaveStatus>(\"idle\");\n const [lastSavedAt, setLastSavedAt] = useState<Date | null>(null);\n\n const pendingRef = useRef<{ value: T } | null>(null);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const savingRef = useRef(false);\n // Keep the latest save fn without threading it through closures (updated in an\n // effect, never during render).\n const saveRef = useRef(save);\n useEffect(() => {\n saveRef.current = save;\n });\n\n async function saveNow(): Promise<void> {\n if (savingRef.current) return; // a later debounce flush picks up new edits\n const pending = pendingRef.current;\n if (!pending) return;\n pendingRef.current = null;\n savingRef.current = true;\n setStatus(\"saving\");\n let failed = false;\n try {\n const res = await saveRef.current(pending.value);\n if (res.error) {\n // Re-queue so the NEXT schedule/flush retries; surface loudly.\n failed = true;\n pendingRef.current = pendingRef.current ?? pending;\n setStatus(\"error\");\n } else {\n setLastSavedAt(new Date());\n setStatus(pendingRef.current ? \"unsaved\" : \"saved\");\n }\n } catch {\n failed = true;\n pendingRef.current = pendingRef.current ?? pending;\n setStatus(\"error\");\n } finally {\n savingRef.current = false;\n // If edits arrived mid-save, drain them promptly. NEVER drain after a\n // failure: the re-queued payload waits for the next schedule/flush —\n // draining it here is an unbounded immediate-retry loop against a\n // persistently failing save. (The matrx-frontend original has exactly\n // that loop; it dies with the original at the adoption swap.)\n if (pendingRef.current && !failed) void saveNow();\n }\n }\n\n function schedule(value: T): void {\n pendingRef.current = { value };\n setStatus(\"unsaved\");\n if (timerRef.current) clearTimeout(timerRef.current);\n timerRef.current = setTimeout(() => void saveNow(), debounceMs);\n }\n\n function flush(): void {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n void saveNow();\n }\n\n // Flush any pending payload on unmount so an in-progress edit is never lost.\n useEffect(() => {\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n const pending = pendingRef.current;\n if (pending && !savingRef.current) {\n pendingRef.current = null;\n void saveRef.current(pending.value);\n }\n };\n }, []);\n\n return { status, lastSavedAt, schedule, flush };\n}\n"],"mappings":";;;;AAaA,SAAS,WAAW,QAAQ,gBAAgB;AAarC,SAAS,YAAe,MAGN;AACvB,QAAM,EAAE,MAAM,aAAa,IAAI,IAAI;AACnC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAyB,MAAM;AAC3D,QAAM,CAAC,aAAa,cAAc,IAAI,SAAsB,IAAI;AAEhE,QAAM,aAAa,OAA4B,IAAI;AACnD,QAAM,WAAW,OAA6C,IAAI;AAClE,QAAM,YAAY,OAAO,KAAK;AAG9B,QAAM,UAAU,OAAO,IAAI;AAC3B,YAAU,MAAM;AACd,YAAQ,UAAU;AAAA,EACpB,CAAC;AAED,iBAAe,UAAyB;AACtC,QAAI,UAAU,QAAS;AACvB,UAAM,UAAU,WAAW;AAC3B,QAAI,CAAC,QAAS;AACd,eAAW,UAAU;AACrB,cAAU,UAAU;AACpB,cAAU,QAAQ;AAClB,QAAI,SAAS;AACb,QAAI;AACF,YAAM,MAAM,MAAM,QAAQ,QAAQ,QAAQ,KAAK;AAC/C,UAAI,IAAI,OAAO;AAEb,iBAAS;AACT,mBAAW,UAAU,WAAW,WAAW;AAC3C,kBAAU,OAAO;AAAA,MACnB,OAAO;AACL,uBAAe,oBAAI,KAAK,CAAC;AACzB,kBAAU,WAAW,UAAU,YAAY,OAAO;AAAA,MACpD;AAAA,IACF,QAAQ;AACN,eAAS;AACT,iBAAW,UAAU,WAAW,WAAW;AAC3C,gBAAU,OAAO;AAAA,IACnB,UAAE;AACA,gBAAU,UAAU;AAMpB,UAAI,WAAW,WAAW,CAAC,OAAQ,MAAK,QAAQ;AAAA,IAClD;AAAA,EACF;AAEA,WAAS,SAAS,OAAgB;AAChC,eAAW,UAAU,EAAE,MAAM;AAC7B,cAAU,SAAS;AACnB,QAAI,SAAS,QAAS,cAAa,SAAS,OAAO;AACnD,aAAS,UAAU,WAAW,MAAM,KAAK,QAAQ,GAAG,UAAU;AAAA,EAChE;AAEA,WAAS,QAAc;AACrB,QAAI,SAAS,SAAS;AACpB,mBAAa,SAAS,OAAO;AAC7B,eAAS,UAAU;AAAA,IACrB;AACA,SAAK,QAAQ;AAAA,EACf;AAGA,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,SAAS,QAAS,cAAa,SAAS,OAAO;AACnD,YAAM,UAAU,WAAW;AAC3B,UAAI,WAAW,CAAC,UAAU,SAAS;AACjC,mBAAW,UAAU;AACrB,aAAK,QAAQ,QAAQ,QAAQ,KAAK;AAAA,MACpC;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO,EAAE,QAAQ,aAAa,UAAU,MAAM;AAChD;","names":[]}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
"use client";
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// src/clipboard.ts
|
|
23
|
+
var clipboard_exports = {};
|
|
24
|
+
__export(clipboard_exports, {
|
|
25
|
+
useClipboard: () => useClipboard
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(clipboard_exports);
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
function toError(err, fallback) {
|
|
30
|
+
if (err instanceof Error) return err;
|
|
31
|
+
if (typeof err === "string" && err) return new Error(err);
|
|
32
|
+
return new Error(fallback);
|
|
33
|
+
}
|
|
34
|
+
function useClipboard(options = {}) {
|
|
35
|
+
const { notify } = options;
|
|
36
|
+
const [lastCopied, setLastCopied] = (0, import_react.useState)(null);
|
|
37
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
38
|
+
const handleSuccess = (type, successMessage) => {
|
|
39
|
+
setLastCopied(type);
|
|
40
|
+
setError(null);
|
|
41
|
+
notify?.(
|
|
42
|
+
successMessage ?? `${type.charAt(0).toUpperCase() + type.slice(1)} copied to clipboard!`,
|
|
43
|
+
"success"
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
const handleError = (err, type) => {
|
|
47
|
+
const message = `Failed to copy ${type}`;
|
|
48
|
+
setError(toError(err, message));
|
|
49
|
+
console.error(message, err);
|
|
50
|
+
notify?.(message, "error");
|
|
51
|
+
};
|
|
52
|
+
const copyText = (0, import_react.useCallback)(
|
|
53
|
+
async (text, successMessage) => {
|
|
54
|
+
try {
|
|
55
|
+
await navigator.clipboard.writeText(text);
|
|
56
|
+
handleSuccess("text", successMessage);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
handleError(err, "text");
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[notify]
|
|
62
|
+
);
|
|
63
|
+
const copyImage = (0, import_react.useCallback)(
|
|
64
|
+
async (imageSrc, successMessage) => {
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch(imageSrc);
|
|
67
|
+
if (!response.ok)
|
|
68
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
69
|
+
const blob = await response.blob();
|
|
70
|
+
const canvas = document.createElement("canvas");
|
|
71
|
+
const ctx = canvas.getContext("2d");
|
|
72
|
+
if (!ctx) throw new Error("Failed to get canvas context");
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const img = new Image();
|
|
75
|
+
img.onload = () => {
|
|
76
|
+
canvas.width = img.width;
|
|
77
|
+
canvas.height = img.height;
|
|
78
|
+
ctx.drawImage(img, 0, 0);
|
|
79
|
+
canvas.toBlob((pngBlob) => {
|
|
80
|
+
if (pngBlob) {
|
|
81
|
+
const item = new ClipboardItem({ "image/png": pngBlob });
|
|
82
|
+
navigator.clipboard.write([item]).then(
|
|
83
|
+
() => {
|
|
84
|
+
handleSuccess("image", successMessage);
|
|
85
|
+
resolve();
|
|
86
|
+
},
|
|
87
|
+
(err) => reject(
|
|
88
|
+
toError(err, "Failed to write image to clipboard")
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
} else {
|
|
92
|
+
reject(new Error("Failed to convert image to PNG"));
|
|
93
|
+
}
|
|
94
|
+
}, "image/png");
|
|
95
|
+
};
|
|
96
|
+
img.onerror = () => reject(new Error("Failed to load image"));
|
|
97
|
+
img.src = URL.createObjectURL(blob);
|
|
98
|
+
});
|
|
99
|
+
} catch (err) {
|
|
100
|
+
handleError(err, "image");
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
[notify]
|
|
104
|
+
);
|
|
105
|
+
const copyLink = (0, import_react.useCallback)(
|
|
106
|
+
async (link, stripParams = false, successMessage) => {
|
|
107
|
+
try {
|
|
108
|
+
let value = link;
|
|
109
|
+
if (stripParams) {
|
|
110
|
+
const url = new URL(link);
|
|
111
|
+
value = `${url.origin}${url.pathname}`;
|
|
112
|
+
}
|
|
113
|
+
await navigator.clipboard.writeText(value);
|
|
114
|
+
handleSuccess("link", successMessage);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
handleError(err, "link");
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
[notify]
|
|
120
|
+
);
|
|
121
|
+
const pasteText = (0, import_react.useCallback)(async () => {
|
|
122
|
+
try {
|
|
123
|
+
const text = await navigator.clipboard.readText();
|
|
124
|
+
setError(null);
|
|
125
|
+
return text;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
setError(toError(err, "Failed to paste text"));
|
|
128
|
+
console.error("Failed to paste text: ", err);
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
}, []);
|
|
132
|
+
const pasteImage = (0, import_react.useCallback)(async () => {
|
|
133
|
+
try {
|
|
134
|
+
if (!navigator.clipboard?.read) return null;
|
|
135
|
+
const items = await navigator.clipboard.read();
|
|
136
|
+
for (const item of items) {
|
|
137
|
+
const imageType = item.types.find((type) => type.startsWith("image/"));
|
|
138
|
+
if (imageType) {
|
|
139
|
+
const blob = await item.getType(imageType);
|
|
140
|
+
return new File([blob], "pasted-image.png", { type: imageType });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
} catch (err) {
|
|
145
|
+
setError(toError(err, "Failed to paste image"));
|
|
146
|
+
console.error("Failed to paste image: ", err);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}, []);
|
|
150
|
+
return { copyText, copyImage, copyLink, pasteText, pasteImage, lastCopied, error };
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=clipboard.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/clipboard.ts"],"sourcesContent":["\"use client\";\n\n/**\n * useClipboard — copy/paste text, links, and images with the browser quirks\n * already solved: image copy re-encodes through a canvas to PNG (the only type\n * `ClipboardItem` reliably accepts), paste-image feature-detects\n * `clipboard.read` (absent in Safari < 16.4) instead of throwing, and link\n * copy can strip query params.\n *\n * The notifier is INJECTED — the package does not know your toast system. The\n * host passes `notify` once (usually wrapping its toast); without it, successes\n * are silent and failures still land in `error` + the console, so nothing is\n * ever swallowed.\n */\nimport { useCallback, useState } from \"react\";\n\nexport type ClipboardNotifyKind = \"success\" | \"error\";\n\nexport interface UseClipboardOptions {\n /** Surface outcomes to the user — wrap your toast here. */\n notify?: (message: string, kind: ClipboardNotifyKind) => void;\n}\n\nexport interface UseClipboardResult {\n copyText: (text: string, successMessage?: string) => Promise<void>;\n copyImage: (imageSrc: string, successMessage?: string) => Promise<void>;\n copyLink: (\n link: string,\n stripParams?: boolean,\n successMessage?: string,\n ) => Promise<void>;\n pasteText: () => Promise<string>;\n pasteImage: () => Promise<File | null>;\n lastCopied: string | null;\n error: Error | null;\n}\n\nfunction toError(err: unknown, fallback: string): Error {\n if (err instanceof Error) return err;\n if (typeof err === \"string\" && err) return new Error(err);\n return new Error(fallback);\n}\n\nexport function useClipboard(\n options: UseClipboardOptions = {},\n): UseClipboardResult {\n const { notify } = options;\n const [lastCopied, setLastCopied] = useState<string | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n const handleSuccess = (type: string, successMessage: string | undefined) => {\n setLastCopied(type);\n setError(null);\n notify?.(\n successMessage ??\n `${type.charAt(0).toUpperCase() + type.slice(1)} copied to clipboard!`,\n \"success\",\n );\n };\n\n const handleError = (err: unknown, type: string) => {\n const message = `Failed to copy ${type}`;\n setError(toError(err, message));\n console.error(message, err);\n notify?.(message, \"error\");\n };\n\n const copyText = useCallback(\n async (text: string, successMessage?: string) => {\n try {\n await navigator.clipboard.writeText(text);\n handleSuccess(\"text\", successMessage);\n } catch (err) {\n handleError(err, \"text\");\n }\n },\n [notify],\n );\n\n const copyImage = useCallback(\n async (imageSrc: string, successMessage?: string) => {\n try {\n const response = await fetch(imageSrc);\n if (!response.ok)\n throw new Error(`HTTP error! status: ${response.status}`);\n const blob = await response.blob();\n\n const canvas = document.createElement(\"canvas\");\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) throw new Error(\"Failed to get canvas context\");\n\n return new Promise<void>((resolve, reject) => {\n const img = new Image();\n img.onload = () => {\n canvas.width = img.width;\n canvas.height = img.height;\n ctx.drawImage(img, 0, 0);\n\n canvas.toBlob((pngBlob) => {\n if (pngBlob) {\n const item = new ClipboardItem({ \"image/png\": pngBlob });\n navigator.clipboard.write([item]).then(\n () => {\n handleSuccess(\"image\", successMessage);\n resolve();\n },\n (err) =>\n reject(\n toError(err, \"Failed to write image to clipboard\"),\n ),\n );\n } else {\n reject(new Error(\"Failed to convert image to PNG\"));\n }\n }, \"image/png\");\n };\n img.onerror = () => reject(new Error(\"Failed to load image\"));\n img.src = URL.createObjectURL(blob);\n });\n } catch (err) {\n handleError(err, \"image\");\n }\n },\n [notify],\n );\n\n const copyLink = useCallback(\n async (link: string, stripParams = false, successMessage?: string) => {\n try {\n let value = link;\n if (stripParams) {\n const url = new URL(link);\n value = `${url.origin}${url.pathname}`;\n }\n await navigator.clipboard.writeText(value);\n handleSuccess(\"link\", successMessage);\n } catch (err) {\n handleError(err, \"link\");\n }\n },\n [notify],\n );\n\n const pasteText = useCallback(async () => {\n try {\n const text = await navigator.clipboard.readText();\n setError(null);\n return text;\n } catch (err) {\n setError(toError(err, \"Failed to paste text\"));\n console.error(\"Failed to paste text: \", err);\n return \"\";\n }\n }, []);\n\n const pasteImage = useCallback(async () => {\n try {\n // clipboard.read() is not available in Safari < 16.4\n if (!navigator.clipboard?.read) return null;\n const items = await navigator.clipboard.read();\n for (const item of items) {\n const imageType = item.types.find((type) => type.startsWith(\"image/\"));\n if (imageType) {\n const blob = await item.getType(imageType);\n return new File([blob], \"pasted-image.png\", { type: imageType });\n }\n }\n return null;\n } catch (err) {\n setError(toError(err, \"Failed to paste image\"));\n console.error(\"Failed to paste image: \", err);\n return null;\n }\n }, []);\n\n return { copyText, copyImage, copyLink, pasteText, pasteImage, lastCopied, error };\n}\n"],"mappings":"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","names":[]}
|