@ai-matrx/kit 0.7.4 → 0.9.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 +185 -0
- package/README.md +11 -4
- package/dist/confirm-opener.cjs.map +1 -1
- package/dist/confirm-opener.d.cts +6 -5
- package/dist/confirm-opener.d.ts +6 -5
- package/dist/confirm-opener.js.map +1 -1
- package/dist/format.cjs +210 -0
- package/dist/format.cjs.map +1 -0
- package/dist/format.d.cts +171 -0
- package/dist/format.d.ts +171 -0
- package/dist/format.js +189 -0
- package/dist/format.js.map +1 -0
- package/dist/html-escape.cjs +38 -0
- package/dist/html-escape.cjs.map +1 -0
- package/dist/html-escape.d.cts +40 -0
- package/dist/html-escape.d.ts +40 -0
- package/dist/html-escape.js +17 -0
- package/dist/html-escape.js.map +1 -0
- package/dist/index.cjs +53 -334
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +75 -356
- package/dist/index.js.map +1 -1
- package/dist/uuid.cjs +35 -0
- package/dist/uuid.cjs.map +1 -0
- package/dist/uuid.d.cts +38 -0
- package/dist/uuid.d.ts +38 -0
- package/dist/uuid.js +14 -0
- package/dist/uuid.js.map +1 -0
- package/package.json +39 -16
- package/dist/confirm.cjs +0 -353
- package/dist/confirm.cjs.map +0 -1
- package/dist/confirm.d.cts +0 -139
- package/dist/confirm.d.ts +0 -139
- package/dist/confirm.js +0 -321
- package/dist/confirm.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,190 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 — 2026-09-07
|
|
4
|
+
|
|
5
|
+
**THE CONFIRM DIALOG LEAVES THIS PACKAGE.** `@ai-matrx/kit/confirm` is removed.
|
|
6
|
+
The dialog body — `ConfirmDialog` and `ConfirmDialogHost` — now lives in
|
|
7
|
+
**`@ai-matrx/design-system` 0.11.0**. What stays here is
|
|
8
|
+
`@ai-matrx/kit/confirm-opener`: the pure imperative `confirm()`, the host
|
|
9
|
+
registry, and the request queue, unchanged.
|
|
10
|
+
|
|
11
|
+
**Why.** kit's confirm shipped its own inlined AlertDialog wearing
|
|
12
|
+
`data-[state=open]:animate-in … zoom-in-95 … slide-in-from-top-[48%]` — utilities
|
|
13
|
+
the `tailwindcss-animate` / `tw-animate-css` HOST plugin supplies, which a
|
|
14
|
+
node_modules package can neither ship nor require — plus a hardcoded
|
|
15
|
+
`bg-black/80` scrim. kit ships no stylesheet at all, so in three of the four
|
|
16
|
+
consumers the most consequential surface in the product (the one standing between
|
|
17
|
+
a user and a destroyed thing) appeared with no animation, no error, and a scrim
|
|
18
|
+
that ignored the theme. Its unclamped content could also push Cancel and Continue
|
|
19
|
+
below the fold of a short viewport — and an AlertDialog does not dismiss on a
|
|
20
|
+
backdrop click, so a long confirmation was a genuine dead end.
|
|
21
|
+
|
|
22
|
+
kit could not import design-system's 0.10.0 fix: **design-system depends on kit**,
|
|
23
|
+
so importing back is a cycle. Giving kit its own `styles.css` would invent a
|
|
24
|
+
second required-CSS contract no consumer performs — the same silent host contract
|
|
25
|
+
one layer down (C26). So the surface moved to the package that owns the surface,
|
|
26
|
+
exactly as the Tooltip did in tap-target 0.2.0 (census rows 19i / 20a).
|
|
27
|
+
|
|
28
|
+
**No shim.** There is no `./confirm` re-export. One surface, one owner — and the
|
|
29
|
+
packed-tarball canary now asserts the ABSENCE of `./confirm` and of both dropped
|
|
30
|
+
dependencies, so the twin cannot grow back.
|
|
31
|
+
|
|
32
|
+
### Removed
|
|
33
|
+
|
|
34
|
+
- `@ai-matrx/kit/confirm` (subpath, entry, and every export: `ConfirmDialog`,
|
|
35
|
+
`ConfirmDialogProps`, `ConfirmDialogHost`, and the `confirm` re-export).
|
|
36
|
+
- `confirm` / `ConfirmOptions` from the ROOT barrel (`@ai-matrx/kit`) — the
|
|
37
|
+
opener is not re-exported there, because the root barrel is `"use client"`
|
|
38
|
+
stamped and the opener must stay importable from Server Components, API routes
|
|
39
|
+
and Node scripts.
|
|
40
|
+
- Runtime dependencies `@radix-ui/react-alert-dialog` and `tailwind-merge`. Both
|
|
41
|
+
existed only for the dialog; kit's install is that much smaller.
|
|
42
|
+
|
|
43
|
+
### Unchanged
|
|
44
|
+
|
|
45
|
+
- `@ai-matrx/kit/confirm-opener` — `confirm`, `ConfirmOptions`, and the
|
|
46
|
+
`Symbol.for("ai-matrx.kit.confirm-opener-state")` globalThis slot the host
|
|
47
|
+
registry lives on. Every behavior is identical: pre-mount calls queue,
|
|
48
|
+
concurrent calls present one at a time, and with no host mounted a `confirm()`
|
|
49
|
+
promise stays pending forever rather than inventing a silent default.
|
|
50
|
+
|
|
51
|
+
### Consumer action (C28)
|
|
52
|
+
|
|
53
|
+
**Every consumer that mounted the kit confirm host or rendered the kit dialog
|
|
54
|
+
must repoint. Two mechanical edits, no behavior change.**
|
|
55
|
+
|
|
56
|
+
1. **Take `@ai-matrx/design-system` >= 0.11.0** alongside kit >= 0.9.0 (specs stay
|
|
57
|
+
`"latest"` / `workspace:*` per C15 — this is an install, not a spec change).
|
|
58
|
+
|
|
59
|
+
2. **The host** — wherever you mounted it:
|
|
60
|
+
|
|
61
|
+
```diff
|
|
62
|
+
- import { ConfirmDialogHost } from "@ai-matrx/kit/confirm";
|
|
63
|
+
+ import { ConfirmDialogHost } from "@ai-matrx/design-system";
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Next.js hosts that lazy-loaded it for the fragmentation law change only the
|
|
67
|
+
module specifier:
|
|
68
|
+
|
|
69
|
+
```diff
|
|
70
|
+
- dynamic(() => import("@ai-matrx/kit/confirm").then((m) => m.ConfirmDialogHost), { ssr: false })
|
|
71
|
+
+ dynamic(() => import("@ai-matrx/design-system").then((m) => m.ConfirmDialogHost), { ssr: false })
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
3. **The declarative dialog:**
|
|
75
|
+
|
|
76
|
+
```diff
|
|
77
|
+
- import { ConfirmDialog, type ConfirmDialogProps } from "@ai-matrx/kit/confirm";
|
|
78
|
+
+ import { ConfirmDialog, type ConfirmDialogProps } from "@ai-matrx/design-system";
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Props are identical. `portalContainer` still wins; with it omitted the dialog
|
|
82
|
+
now also honours design-system's `PortalContainerProvider`, so a host that
|
|
83
|
+
already provides that container can drop its own wrapper.
|
|
84
|
+
|
|
85
|
+
4. **The imperative `confirm()`** — if you imported it from `@ai-matrx/kit/confirm`
|
|
86
|
+
or from the kit ROOT, repoint to the opener (this is the light-twin entry every
|
|
87
|
+
call site should have been using anyway):
|
|
88
|
+
|
|
89
|
+
```diff
|
|
90
|
+
- import { confirm } from "@ai-matrx/kit/confirm";
|
|
91
|
+
+ import { confirm } from "@ai-matrx/kit/confirm-opener";
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
5. **Load design-system's stylesheet** if you do not already — `@ai-matrx/design-system/styles.css`
|
|
95
|
+
plus `tokens.css`. This is what the move buys you: the confirm surface finally
|
|
96
|
+
animates and scrims from your theme in EVERY consumer, not only in the one whose
|
|
97
|
+
CSS entry happened to load an animation plugin.
|
|
98
|
+
|
|
99
|
+
## 0.8.0 — 2026-09-07
|
|
100
|
+
|
|
101
|
+
**THE HOME FOR THE FLEET'S SHARED FORMATTERS.** Three new pure subpaths —
|
|
102
|
+
`./format`, `./html-escape`, `./uuid` — closing the census's headline finding
|
|
103
|
+
H1: *the packages were duplicating each other*, which is why ~65 host twins had
|
|
104
|
+
nowhere correct to point.
|
|
105
|
+
|
|
106
|
+
**Why kit is the home.** `node scripts/check_ts_sibling_graph.mjs --order`
|
|
107
|
+
reports `@ai-matrx/kit` with ZERO sibling dependencies — it is at the bottom of
|
|
108
|
+
the sibling DAG, so `meet`, `messaging`, `associations`, `diff`, `media` and
|
|
109
|
+
`print` can all depend on it without creating a cycle, and so can every host
|
|
110
|
+
app in every repo. It is also, by charter, the "little primitives every Matrx
|
|
111
|
+
app speaks" package. No new package was created (new packages need Arman's
|
|
112
|
+
approval), and nothing here re-exports from a sibling: these bodies now live in
|
|
113
|
+
exactly one place.
|
|
114
|
+
|
|
115
|
+
### `./format` — durations, relative time, byte sizes, initials
|
|
116
|
+
|
|
117
|
+
Absorbed from `@ai-matrx/meet` (`formatDuration`, `getInitials`,
|
|
118
|
+
`avatarPaletteIndex`), `@ai-matrx/messaging` (`getInitials`,
|
|
119
|
+
`avatarPaletteIndex`), `@ai-matrx/associations/react` (`formatRelativeTime`),
|
|
120
|
+
`@ai-matrx/diff` (`formatRelativeTime`, `parseTimestamp`, `formatAbsoluteDate`)
|
|
121
|
+
and `@ai-matrx/media/react` (`formatFileSize`). Every behaviour those five
|
|
122
|
+
asserted is re-asserted in `src/format.test.ts` (60 tests across the three new
|
|
123
|
+
modules).
|
|
124
|
+
|
|
125
|
+
**THE UNIT LAW.** There is deliberately no `formatDuration`. The fleet's twins
|
|
126
|
+
variously took milliseconds, seconds and minutes behind an identical signature.
|
|
127
|
+
The unit is now in the name: `formatDurationMs`, `formatDurationSeconds`,
|
|
128
|
+
`formatDurationMinutes` — plus `durationMsBetween(from, to, now)` for the
|
|
129
|
+
"start, maybe-end, else now" pattern six surfaces had rebuilt.
|
|
130
|
+
|
|
131
|
+
**The display decisions, made once and deliberately:**
|
|
132
|
+
|
|
133
|
+
- **Three duration voices**, chosen by `style`, because the fleet genuinely
|
|
134
|
+
speaks three and pretending otherwise would have been the wrong unification:
|
|
135
|
+
`"clock"` (default) `0:00` / `9:04` / `1:02:33` for anything a human watches
|
|
136
|
+
tick; `"compact"` `250ms` / `5.2s` / `5m 30s` / `1h 02m` for elapsed work;
|
|
137
|
+
`"coarse"` `45 min` / `1h 30m` for lengths read at a glance. `compact` keeps
|
|
138
|
+
one decimal below ten seconds and whole seconds above it; `coarse` prints
|
|
139
|
+
`< 1 min` rather than a false `0 min`.
|
|
140
|
+
- **A negative duration clamps to zero** unless `signed: true`. A negative
|
|
141
|
+
elapsed time is nearly always a clock bug.
|
|
142
|
+
- **Unknown is never zero.** `null` / `undefined` / `NaN` / `Infinity` take an
|
|
143
|
+
explicit `fallback` (`"0:00"` for clock, `"—"` otherwise). A confident
|
|
144
|
+
`"0 B"` or `"0:00"` for "we do not know" is a screen telling a lie.
|
|
145
|
+
- **`formatFileSize`**: binary units with short labels (`B`/`KB`/…/`PB`, never
|
|
146
|
+
`"Bytes"`), one decimal below ten in a unit and whole numbers above
|
|
147
|
+
(`2.0 KB`, `15 KB`), whole bytes below 1 KB, em-dash for unknown/negative.
|
|
148
|
+
- **`formatRelativeTime`** unifies the two packages that disagreed about who
|
|
149
|
+
owned it, via `style`: `"short"` (`2m ago`, the dense default),
|
|
150
|
+
`"long"` (`2 minutes ago`), `"intl"` (`Intl.RelativeTimeFormat` with
|
|
151
|
+
`numeric: "auto"`, which is what the comments face wanted). `fallbackToInput`
|
|
152
|
+
echoes an unparseable string instead of an em-dash — degraded, never broken.
|
|
153
|
+
It runs over `parseTimestamp`, which carries diff's real correction: a
|
|
154
|
+
zone-less Postgres `timestamp without time zone` is read as UTC, not local,
|
|
155
|
+
killing the classic "times are off by N hours" report for every consumer.
|
|
156
|
+
- **`getInitials`** takes FIRST + LAST, not first + second: `"Ana Maria
|
|
157
|
+
Rivera"` is `AR`. Both package copies did this and both were tested; six
|
|
158
|
+
matrx-frontend twins printed `AM`.
|
|
159
|
+
|
|
160
|
+
### `./html-escape` — ONE escaper, with a decided character set
|
|
161
|
+
|
|
162
|
+
Nine copies of `escapeHtml` were live across the fleet and **they did not
|
|
163
|
+
escape the same characters** — three did `& < >` only, two added `"`, four
|
|
164
|
+
added `'`. A string safe through one was an attribute break-out through
|
|
165
|
+
another. The decided set is all five: `& < > " '` →
|
|
166
|
+
`& < > " '`, always, so the result is safe as element text
|
|
167
|
+
*and* inside a quoted attribute. It lives here rather than in
|
|
168
|
+
`@ai-matrx/print/core` because an HTML escaper must not drag in `jspdf` +
|
|
169
|
+
`html2canvas`.
|
|
170
|
+
|
|
171
|
+
### `./uuid` — two predicates, named so you cannot pick the wrong one
|
|
172
|
+
|
|
173
|
+
The fleet had two different predicates both called `isUuid`, and the difference
|
|
174
|
+
was invisible at the call site. Now: `isUuidShape` (lax 8-4-4-4-12, the right
|
|
175
|
+
question for "id or slug?", accepts v7 and the nil UUID) and `isRfc4122Uuid`
|
|
176
|
+
(version 1–5 + variant 8/9/a/b, the right question for a validation door on a
|
|
177
|
+
public route, and it REJECTS the nil UUID and v6/v7/v8). A bare `isUuid` is
|
|
178
|
+
deliberately not exported — a caller must choose.
|
|
179
|
+
|
|
180
|
+
### Notes
|
|
181
|
+
|
|
182
|
+
- All three subpaths are pure and **unstamped** (no `"use client"`): they are
|
|
183
|
+
imported by API routes, Server Components and Node scripts as often as by
|
|
184
|
+
client code. For the same reason they are **not** re-exported from the root
|
|
185
|
+
barrel, which carries the banner — import them by subpath.
|
|
186
|
+
- No dependency was added. No existing export changed.
|
|
187
|
+
|
|
3
188
|
## 0.7.3 — 2026-08-30
|
|
4
189
|
|
|
5
190
|
- Peer-dependency retrofit (register row X3 / law C23): the advisory-only
|
package/README.md
CHANGED
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
The always-include AI Matrx kit: the little primitives every Matrx application speaks,
|
|
4
4
|
one capability per subpath, tree-shaken to what you use. React is the only required
|
|
5
|
-
peer; only
|
|
6
|
-
the capability: `/
|
|
7
|
-
product is the dialog), `/json-format` (`json5` — tolerant parsing is the feature),
|
|
5
|
+
peer; only three subpaths carry runtime dependencies, each because the dependency IS
|
|
6
|
+
the capability: `/json-format` (`json5` — tolerant parsing is the feature),
|
|
8
7
|
`/idb-store` (`idb` — the typed IndexedDB wrapper is the engine), and `/qr` (`jsqr`,
|
|
9
8
|
loaded lazily only when the browser has no native `BarcodeDetector`).
|
|
10
9
|
|
|
10
|
+
**The confirm DIALOG is not here — the opener is.** Since 0.9.0 the body
|
|
11
|
+
(`ConfirmDialog`, `ConfirmDialogHost`) lives in
|
|
12
|
+
[`@ai-matrx/design-system`](../design-system/README.md), which owns the AlertDialog,
|
|
13
|
+
the shipped motion layer and the overlay scrim token. Mount
|
|
14
|
+
`<ConfirmDialogHost />` from there once at your app root, then call `confirm()`
|
|
15
|
+
from `@ai-matrx/kit/confirm-opener` anywhere. One surface, one owner: kit
|
|
16
|
+
re-exports none of the dialog.
|
|
17
|
+
|
|
11
18
|
```bash
|
|
12
19
|
npm install @ai-matrx/kit
|
|
13
20
|
```
|
|
@@ -22,7 +29,7 @@ npm install @ai-matrx/kit
|
|
|
22
29
|
| `@ai-matrx/kit/concurrency` | `runWithConcurrency` | Bounded worker pool over an array with per-item failure isolation and a cooperative `shouldStart()` cancel hook. |
|
|
23
30
|
| `@ai-matrx/kit/text-case` | `formatText`, `createFormatter` | Acronym-aware normalization of messy DB/API strings into title/sentence case with word-replacement overrides. |
|
|
24
31
|
| `@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`, `
|
|
32
|
+
| `@ai-matrx/kit/confirm-opener` | `confirm`, `ConfirmOptions` | The `window.confirm` replacement's imperative half — pure TS, zero React, zero radix, no `"use client"`, so a Redux thunk, a util or a sync handler can import it at near-zero bundle cost. `await confirm({ title, description, variant: "destructive" })` resolves `true`/`false`; pre-mount calls queue and concurrent calls present one at a time; with no host mounted it stays pending forever rather than inventing a silent default. Needs `<ConfirmDialogHost />` from `@ai-matrx/design-system` mounted once — the two share one registry through a `globalThis` slot. |
|
|
26
33
|
| `@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 — and sonner is not declared as a dependency or peer at all. |
|
|
27
34
|
| `@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
35
|
| `@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. |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/confirm-opener.ts","../src/confirm/opener.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/confirm-opener — the imperative `confirm()` API alone.\n *\n *
|
|
1
|
+
{"version":3,"sources":["../src/confirm-opener.ts","../src/confirm/opener.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/confirm-opener — the imperative `confirm()` API alone.\n *\n * THE WHOLE CONFIRM SYSTEM THIS PACKAGE SHIPS (since 0.9.0, census row 19i).\n * The dialog BODY — `ConfirmDialog` and `ConfirmDialogHost` — lives in\n * `@ai-matrx/design-system`, which owns the AlertDialog, the shipped motion\n * layer and the `--matrx-overlay-scrim` token this surface needs and kit could\n * neither ship nor import (design-system depends on kit; importing back is a\n * cycle). kit re-exports none of it: one surface, one owner.\n *\n * What is here is the pure half: zero React runtime, zero radix, no\n * \"use client\" banner — statically importable from anywhere (hooks, utils,\n * Redux thunks, sync code) at near-zero bundle cost, which is what lets a host\n * keep hundreds of imperative call sites out of the dialog's chunk.\n *\n * Wiring, end to end:\n * 1. mount `<ConfirmDialogHost />` from `@ai-matrx/design-system` once, near\n * the root of every provider tree;\n * 2. `await confirm({ ... })` from anywhere, importing from HERE.\n * The two find each other through the\n * `Symbol.for(\"ai-matrx.kit.confirm-opener-state\")` globalThis slot, so they\n * observe one registration across every module graph and both packages.\n */\nexport {\n confirm,\n _registerHost,\n _unregisterHost,\n _resetConfirmOpenerState,\n type ConfirmOptions,\n} from \"./confirm/opener\";\n","/**\n * @ai-matrx/kit/confirm-opener — the imperative opener, and since 0.9.0 the\n * ONLY half of the confirm system this package ships.\n *\n * Pure-TS imperative API for the global confirm dialog. Zero React, zero\n * dialog markup — this module is statically importable from anywhere\n * (hooks, utils, Redux thunks, async handlers, sync code, anything).\n *\n * The host (`ConfirmDialogHost`, in `@ai-matrx/design-system`) registers a\n * controller on mount and\n * unregisters on unmount. Calls made before the host has hydrated queue\n * up and resolve as soon as the host is alive — so a destructive action\n * triggered in the first ~50ms after page load still gets a real\n * confirmation, never a silent default-yes/no. With no host ever mounted,\n * a `confirm()` promise stays pending forever (the original's behavior —\n * it never resolves to a silent default).\n *\n * One dialog at a time: concurrent calls queue and present sequentially.\n *\n * Ported verbatim from matrx-frontend\n * `components/dialogs/confirm/confirmDialogOpener.ts`, with ONE structural\n * inversion: the host/queue state lives on `globalThis` under a\n * `Symbol.for` slot instead of module-level variables. With the package\n * built `splitting: false` in dual ESM/CJS format, and the host now living in\n * a DIFFERENT PACKAGE, CJS/ESM each instantiate their own module graph — a module-level variable would\n * silently split the host registration from the callers (the same hazard\n * `@ai-matrx/tap-target` documents for its link registry). Behavior is\n * unchanged; never \"clean this up\" into a module local.\n */\n\nimport type { ReactNode } from \"react\";\n\nexport interface ConfirmOptions {\n title: ReactNode;\n description?: ReactNode | undefined;\n confirmLabel?: string | undefined;\n /** `null` hides the cancel button (acknowledge-only dialogs). */\n cancelLabel?: string | null | undefined;\n variant?: \"default\" | \"destructive\" | undefined;\n}\n\ntype Resolver = (confirmed: boolean) => void;\n\ninterface PendingRequest {\n opts: ConfirmOptions;\n resolve: Resolver;\n}\n\ninterface HostController {\n show: (opts: ConfirmOptions, resolve: Resolver) => void;\n}\n\ninterface OpenerState {\n host: HostController | null;\n queue: PendingRequest[];\n}\n\nconst STATE_SLOT = Symbol.for(\"ai-matrx.kit.confirm-opener-state\");\n\nfunction getState(): OpenerState {\n const holder = globalThis as Record<symbol, OpenerState | undefined>;\n let state = holder[STATE_SLOT];\n if (!state) {\n state = { host: null, queue: [] };\n holder[STATE_SLOT] = state;\n }\n return state;\n}\n\n/** @internal Called by `ConfirmDialogHost` on mount. */\nexport function _registerHost(controller: HostController): void {\n const state = getState();\n state.host = controller;\n while (state.queue.length > 0) {\n const next = state.queue.shift()!;\n controller.show(next.opts, next.resolve);\n }\n}\n\n/** @internal Called by `ConfirmDialogHost` on unmount. */\nexport function _unregisterHost(controller: HostController): void {\n const state = getState();\n if (state.host === controller) state.host = null;\n}\n\n/** @internal Test-only: drop any registered host and pending queue. */\nexport function _resetConfirmOpenerState(): void {\n const state = getState();\n state.host = null;\n state.queue.length = 0;\n}\n\n/**\n * Imperative confirm. Returns a Promise that resolves `true` if the user\n * confirms, `false` if they cancel/dismiss. Replaces `window.confirm`.\n *\n * @example\n * const ok = await confirm({\n * title: \"Delete sandbox\",\n * description: \"This cannot be undone.\",\n * variant: \"destructive\",\n * confirmLabel: \"Delete\",\n * });\n * if (!ok) return;\n */\nexport function confirm(opts: ConfirmOptions): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n const state = getState();\n if (state.host) {\n state.host.show(opts, resolve);\n } else {\n state.queue.push({ opts, resolve });\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACyDA,IAAM,aAAa,uBAAO,IAAI,mCAAmC;AAEjE,SAAS,WAAwB;AAC/B,QAAM,SAAS;AACf,MAAI,QAAQ,OAAO,UAAU;AAC7B,MAAI,CAAC,OAAO;AACV,YAAQ,EAAE,MAAM,MAAM,OAAO,CAAC,EAAE;AAChC,WAAO,UAAU,IAAI;AAAA,EACvB;AACA,SAAO;AACT;AAGO,SAAS,cAAc,YAAkC;AAC9D,QAAM,QAAQ,SAAS;AACvB,QAAM,OAAO;AACb,SAAO,MAAM,MAAM,SAAS,GAAG;AAC7B,UAAM,OAAO,MAAM,MAAM,MAAM;AAC/B,eAAW,KAAK,KAAK,MAAM,KAAK,OAAO;AAAA,EACzC;AACF;AAGO,SAAS,gBAAgB,YAAkC;AAChE,QAAM,QAAQ,SAAS;AACvB,MAAI,MAAM,SAAS,WAAY,OAAM,OAAO;AAC9C;AAGO,SAAS,2BAAiC;AAC/C,QAAM,QAAQ,SAAS;AACvB,QAAM,OAAO;AACb,QAAM,MAAM,SAAS;AACvB;AAeO,SAAS,QAAQ,MAAwC;AAC9D,SAAO,IAAI,QAAiB,CAAC,YAAY;AACvC,UAAM,QAAQ,SAAS;AACvB,QAAI,MAAM,MAAM;AACd,YAAM,KAAK,KAAK,MAAM,OAAO;AAAA,IAC/B,OAAO;AACL,YAAM,MAAM,KAAK,EAAE,MAAM,QAAQ,CAAC;AAAA,IACpC;AAAA,EACF,CAAC;AACH;","names":[]}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @ai-matrx/kit/confirm — imperative opener.
|
|
4
|
+
* @ai-matrx/kit/confirm-opener — the imperative opener, and since 0.9.0 the
|
|
5
|
+
* ONLY half of the confirm system this package ships.
|
|
5
6
|
*
|
|
6
7
|
* Pure-TS imperative API for the global confirm dialog. Zero React, zero
|
|
7
8
|
* dialog markup — this module is statically importable from anywhere
|
|
8
9
|
* (hooks, utils, Redux thunks, async handlers, sync code, anything).
|
|
9
10
|
*
|
|
10
|
-
* The host (`ConfirmDialogHost`) registers a
|
|
11
|
+
* The host (`ConfirmDialogHost`, in `@ai-matrx/design-system`) registers a
|
|
12
|
+
* controller on mount and
|
|
11
13
|
* unregisters on unmount. Calls made before the host has hydrated queue
|
|
12
14
|
* up and resolve as soon as the host is alive — so a destructive action
|
|
13
15
|
* triggered in the first ~50ms after page load still gets a real
|
|
@@ -21,9 +23,8 @@ import { ReactNode } from 'react';
|
|
|
21
23
|
* `components/dialogs/confirm/confirmDialogOpener.ts`, with ONE structural
|
|
22
24
|
* inversion: the host/queue state lives on `globalThis` under a
|
|
23
25
|
* `Symbol.for` slot instead of module-level variables. With the package
|
|
24
|
-
* built `splitting: false` in dual ESM/CJS format,
|
|
25
|
-
*
|
|
26
|
-
* each instantiate their own module graph — a module-level variable would
|
|
26
|
+
* built `splitting: false` in dual ESM/CJS format, and the host now living in
|
|
27
|
+
* a DIFFERENT PACKAGE, CJS/ESM each instantiate their own module graph — a module-level variable would
|
|
27
28
|
* silently split the host registration from the callers (the same hazard
|
|
28
29
|
* `@ai-matrx/tap-target` documents for its link registry). Behavior is
|
|
29
30
|
* unchanged; never "clean this up" into a module local.
|
package/dist/confirm-opener.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @ai-matrx/kit/confirm — imperative opener.
|
|
4
|
+
* @ai-matrx/kit/confirm-opener — the imperative opener, and since 0.9.0 the
|
|
5
|
+
* ONLY half of the confirm system this package ships.
|
|
5
6
|
*
|
|
6
7
|
* Pure-TS imperative API for the global confirm dialog. Zero React, zero
|
|
7
8
|
* dialog markup — this module is statically importable from anywhere
|
|
8
9
|
* (hooks, utils, Redux thunks, async handlers, sync code, anything).
|
|
9
10
|
*
|
|
10
|
-
* The host (`ConfirmDialogHost`) registers a
|
|
11
|
+
* The host (`ConfirmDialogHost`, in `@ai-matrx/design-system`) registers a
|
|
12
|
+
* controller on mount and
|
|
11
13
|
* unregisters on unmount. Calls made before the host has hydrated queue
|
|
12
14
|
* up and resolve as soon as the host is alive — so a destructive action
|
|
13
15
|
* triggered in the first ~50ms after page load still gets a real
|
|
@@ -21,9 +23,8 @@ import { ReactNode } from 'react';
|
|
|
21
23
|
* `components/dialogs/confirm/confirmDialogOpener.ts`, with ONE structural
|
|
22
24
|
* inversion: the host/queue state lives on `globalThis` under a
|
|
23
25
|
* `Symbol.for` slot instead of module-level variables. With the package
|
|
24
|
-
* built `splitting: false` in dual ESM/CJS format,
|
|
25
|
-
*
|
|
26
|
-
* each instantiate their own module graph — a module-level variable would
|
|
26
|
+
* built `splitting: false` in dual ESM/CJS format, and the host now living in
|
|
27
|
+
* a DIFFERENT PACKAGE, CJS/ESM each instantiate their own module graph — a module-level variable would
|
|
27
28
|
* silently split the host registration from the callers (the same hazard
|
|
28
29
|
* `@ai-matrx/tap-target` documents for its link registry). Behavior is
|
|
29
30
|
* unchanged; never "clean this up" into a module local.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/confirm/opener.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/confirm — imperative opener.\n *\n * Pure-TS imperative API for the global confirm dialog. Zero React, zero\n * dialog markup — this module is statically importable from anywhere\n * (hooks, utils, Redux thunks, async handlers, sync code, anything).\n *\n * The host (`ConfirmDialogHost`) registers a controller on mount and\n * unregisters on unmount. Calls made before the host has hydrated queue\n * up and resolve as soon as the host is alive — so a destructive action\n * triggered in the first ~50ms after page load still gets a real\n * confirmation, never a silent default-yes/no. With no host ever mounted,\n * a `confirm()` promise stays pending forever (the original's behavior —\n * it never resolves to a silent default).\n *\n * One dialog at a time: concurrent calls queue and present sequentially.\n *\n * Ported verbatim from matrx-frontend\n * `components/dialogs/confirm/confirmDialogOpener.ts`, with ONE structural\n * inversion: the host/queue state lives on `globalThis` under a\n * `Symbol.for` slot instead of module-level variables. With the package\n * built `splitting: false` in dual ESM/CJS format,
|
|
1
|
+
{"version":3,"sources":["../src/confirm/opener.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/confirm-opener — the imperative opener, and since 0.9.0 the\n * ONLY half of the confirm system this package ships.\n *\n * Pure-TS imperative API for the global confirm dialog. Zero React, zero\n * dialog markup — this module is statically importable from anywhere\n * (hooks, utils, Redux thunks, async handlers, sync code, anything).\n *\n * The host (`ConfirmDialogHost`, in `@ai-matrx/design-system`) registers a\n * controller on mount and\n * unregisters on unmount. Calls made before the host has hydrated queue\n * up and resolve as soon as the host is alive — so a destructive action\n * triggered in the first ~50ms after page load still gets a real\n * confirmation, never a silent default-yes/no. With no host ever mounted,\n * a `confirm()` promise stays pending forever (the original's behavior —\n * it never resolves to a silent default).\n *\n * One dialog at a time: concurrent calls queue and present sequentially.\n *\n * Ported verbatim from matrx-frontend\n * `components/dialogs/confirm/confirmDialogOpener.ts`, with ONE structural\n * inversion: the host/queue state lives on `globalThis` under a\n * `Symbol.for` slot instead of module-level variables. With the package\n * built `splitting: false` in dual ESM/CJS format, and the host now living in\n * a DIFFERENT PACKAGE, CJS/ESM each instantiate their own module graph — a module-level variable would\n * silently split the host registration from the callers (the same hazard\n * `@ai-matrx/tap-target` documents for its link registry). Behavior is\n * unchanged; never \"clean this up\" into a module local.\n */\n\nimport type { ReactNode } from \"react\";\n\nexport interface ConfirmOptions {\n title: ReactNode;\n description?: ReactNode | undefined;\n confirmLabel?: string | undefined;\n /** `null` hides the cancel button (acknowledge-only dialogs). */\n cancelLabel?: string | null | undefined;\n variant?: \"default\" | \"destructive\" | undefined;\n}\n\ntype Resolver = (confirmed: boolean) => void;\n\ninterface PendingRequest {\n opts: ConfirmOptions;\n resolve: Resolver;\n}\n\ninterface HostController {\n show: (opts: ConfirmOptions, resolve: Resolver) => void;\n}\n\ninterface OpenerState {\n host: HostController | null;\n queue: PendingRequest[];\n}\n\nconst STATE_SLOT = Symbol.for(\"ai-matrx.kit.confirm-opener-state\");\n\nfunction getState(): OpenerState {\n const holder = globalThis as Record<symbol, OpenerState | undefined>;\n let state = holder[STATE_SLOT];\n if (!state) {\n state = { host: null, queue: [] };\n holder[STATE_SLOT] = state;\n }\n return state;\n}\n\n/** @internal Called by `ConfirmDialogHost` on mount. */\nexport function _registerHost(controller: HostController): void {\n const state = getState();\n state.host = controller;\n while (state.queue.length > 0) {\n const next = state.queue.shift()!;\n controller.show(next.opts, next.resolve);\n }\n}\n\n/** @internal Called by `ConfirmDialogHost` on unmount. */\nexport function _unregisterHost(controller: HostController): void {\n const state = getState();\n if (state.host === controller) state.host = null;\n}\n\n/** @internal Test-only: drop any registered host and pending queue. */\nexport function _resetConfirmOpenerState(): void {\n const state = getState();\n state.host = null;\n state.queue.length = 0;\n}\n\n/**\n * Imperative confirm. Returns a Promise that resolves `true` if the user\n * confirms, `false` if they cancel/dismiss. Replaces `window.confirm`.\n *\n * @example\n * const ok = await confirm({\n * title: \"Delete sandbox\",\n * description: \"This cannot be undone.\",\n * variant: \"destructive\",\n * confirmLabel: \"Delete\",\n * });\n * if (!ok) return;\n */\nexport function confirm(opts: ConfirmOptions): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n const state = getState();\n if (state.host) {\n state.host.show(opts, resolve);\n } else {\n state.queue.push({ opts, resolve });\n }\n });\n}\n"],"mappings":";AAyDA,IAAM,aAAa,uBAAO,IAAI,mCAAmC;AAEjE,SAAS,WAAwB;AAC/B,QAAM,SAAS;AACf,MAAI,QAAQ,OAAO,UAAU;AAC7B,MAAI,CAAC,OAAO;AACV,YAAQ,EAAE,MAAM,MAAM,OAAO,CAAC,EAAE;AAChC,WAAO,UAAU,IAAI;AAAA,EACvB;AACA,SAAO;AACT;AAGO,SAAS,cAAc,YAAkC;AAC9D,QAAM,QAAQ,SAAS;AACvB,QAAM,OAAO;AACb,SAAO,MAAM,MAAM,SAAS,GAAG;AAC7B,UAAM,OAAO,MAAM,MAAM,MAAM;AAC/B,eAAW,KAAK,KAAK,MAAM,KAAK,OAAO;AAAA,EACzC;AACF;AAGO,SAAS,gBAAgB,YAAkC;AAChE,QAAM,QAAQ,SAAS;AACvB,MAAI,MAAM,SAAS,WAAY,OAAM,OAAO;AAC9C;AAGO,SAAS,2BAAiC;AAC/C,QAAM,QAAQ,SAAS;AACvB,QAAM,OAAO;AACb,QAAM,MAAM,SAAS;AACvB;AAeO,SAAS,QAAQ,MAAwC;AAC9D,SAAO,IAAI,QAAiB,CAAC,YAAY;AACvC,UAAM,QAAQ,SAAS;AACvB,QAAI,MAAM,MAAM;AACd,YAAM,KAAK,KAAK,MAAM,OAAO;AAAA,IAC/B,OAAO;AACL,YAAM,MAAM,KAAK,EAAE,MAAM,QAAQ,CAAC;AAAA,IACpC;AAAA,EACF,CAAC;AACH;","names":[]}
|
package/dist/format.cjs
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/format.ts
|
|
21
|
+
var format_exports = {};
|
|
22
|
+
__export(format_exports, {
|
|
23
|
+
avatarPaletteIndex: () => avatarPaletteIndex,
|
|
24
|
+
durationMsBetween: () => durationMsBetween,
|
|
25
|
+
formatAbsoluteDate: () => formatAbsoluteDate,
|
|
26
|
+
formatDurationMinutes: () => formatDurationMinutes,
|
|
27
|
+
formatDurationMs: () => formatDurationMs,
|
|
28
|
+
formatDurationSeconds: () => formatDurationSeconds,
|
|
29
|
+
formatFileSize: () => formatFileSize,
|
|
30
|
+
formatRelativeTime: () => formatRelativeTime,
|
|
31
|
+
getInitials: () => getInitials,
|
|
32
|
+
parseTimestamp: () => parseTimestamp
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(format_exports);
|
|
35
|
+
function hasTimezoneDesignator(value) {
|
|
36
|
+
if (/[zZ]$/.test(value)) return true;
|
|
37
|
+
if (/[+-]\d{2}(:?\d{2})?$/.test(value)) return true;
|
|
38
|
+
if (/\b(GMT|UTC)\b/i.test(value)) return true;
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
function normalizeTimestampString(raw) {
|
|
42
|
+
const value = raw.trim();
|
|
43
|
+
if (!value) return value;
|
|
44
|
+
if (!/\d{1,2}:\d{2}/.test(value)) return value;
|
|
45
|
+
if (hasTimezoneDesignator(value)) return value;
|
|
46
|
+
return `${value.replace(" ", "T")}Z`;
|
|
47
|
+
}
|
|
48
|
+
function parseTimestamp(value) {
|
|
49
|
+
if (value === null || value === void 0) return null;
|
|
50
|
+
if (value instanceof Date) {
|
|
51
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
52
|
+
}
|
|
53
|
+
if (typeof value === "number") {
|
|
54
|
+
const fromNumber = new Date(value);
|
|
55
|
+
return Number.isNaN(fromNumber.getTime()) ? null : fromNumber;
|
|
56
|
+
}
|
|
57
|
+
if (typeof value !== "string") return null;
|
|
58
|
+
const normalized = normalizeTimestampString(value);
|
|
59
|
+
if (!normalized) return null;
|
|
60
|
+
const parsed = new Date(normalized);
|
|
61
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
62
|
+
}
|
|
63
|
+
var DEFAULT_ABSOLUTE_OPTIONS = {
|
|
64
|
+
year: "numeric",
|
|
65
|
+
month: "short",
|
|
66
|
+
day: "numeric",
|
|
67
|
+
hour: "numeric",
|
|
68
|
+
minute: "2-digit"
|
|
69
|
+
};
|
|
70
|
+
function formatAbsoluteDate(value, options = DEFAULT_ABSOLUTE_OPTIONS, fallback = "\u2014") {
|
|
71
|
+
const parsed = parseTimestamp(value);
|
|
72
|
+
if (!parsed) return fallback;
|
|
73
|
+
return parsed.toLocaleString(void 0, options);
|
|
74
|
+
}
|
|
75
|
+
var RELATIVE_UNITS = [
|
|
76
|
+
{ limit: 6e4, divisor: 1e3, short: "s", long: "second" },
|
|
77
|
+
{ limit: 36e5, divisor: 6e4, short: "m", long: "minute" },
|
|
78
|
+
{ limit: 864e5, divisor: 36e5, short: "h", long: "hour" },
|
|
79
|
+
{ limit: 6048e5, divisor: 864e5, short: "d", long: "day" },
|
|
80
|
+
{ limit: 2592e6, divisor: 6048e5, short: "w", long: "week" },
|
|
81
|
+
{ limit: 31536e6, divisor: 2592e6, short: "mo", long: "month" }
|
|
82
|
+
];
|
|
83
|
+
var INTL_UNITS = [
|
|
84
|
+
{ unit: "year", ms: 365 * 24 * 60 * 60 * 1e3 },
|
|
85
|
+
{ unit: "month", ms: 30 * 24 * 60 * 60 * 1e3 },
|
|
86
|
+
{ unit: "week", ms: 7 * 24 * 60 * 60 * 1e3 },
|
|
87
|
+
{ unit: "day", ms: 24 * 60 * 60 * 1e3 },
|
|
88
|
+
{ unit: "hour", ms: 60 * 60 * 1e3 },
|
|
89
|
+
{ unit: "minute", ms: 60 * 1e3 }
|
|
90
|
+
];
|
|
91
|
+
function formatRelativeTime(value, options = {}) {
|
|
92
|
+
const style = options.style ?? "short";
|
|
93
|
+
const now = options.now ?? Date.now();
|
|
94
|
+
const parsed = parseTimestamp(value);
|
|
95
|
+
if (!parsed) {
|
|
96
|
+
if (options.fallbackToInput === true && typeof value === "string") return value;
|
|
97
|
+
return options.fallback ?? "\u2014";
|
|
98
|
+
}
|
|
99
|
+
if (style === "intl") {
|
|
100
|
+
const delta = parsed.getTime() - now;
|
|
101
|
+
const magnitude = Math.abs(delta);
|
|
102
|
+
if (magnitude < 6e4) return "just now";
|
|
103
|
+
const rtf = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
|
|
104
|
+
for (const { unit, ms } of INTL_UNITS) {
|
|
105
|
+
if (magnitude >= ms) return rtf.format(Math.trunc(delta / ms), unit);
|
|
106
|
+
}
|
|
107
|
+
return "just now";
|
|
108
|
+
}
|
|
109
|
+
const elapsed = now - parsed.getTime();
|
|
110
|
+
if (elapsed < 0) return "just now";
|
|
111
|
+
for (const unit of RELATIVE_UNITS) {
|
|
112
|
+
if (elapsed < unit.limit) {
|
|
113
|
+
const count = Math.max(1, Math.floor(elapsed / unit.divisor));
|
|
114
|
+
return style === "long" ? `${count} ${unit.long}${count === 1 ? "" : "s"} ago` : `${count}${unit.short} ago`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return formatAbsoluteDate(parsed);
|
|
118
|
+
}
|
|
119
|
+
var pad2 = (value) => value.toString().padStart(2, "0");
|
|
120
|
+
function clockBody(totalSeconds) {
|
|
121
|
+
const seconds = totalSeconds % 60;
|
|
122
|
+
const minutes = Math.floor(totalSeconds / 60) % 60;
|
|
123
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
124
|
+
return hours > 0 ? `${hours}:${pad2(minutes)}:${pad2(seconds)}` : `${minutes}:${pad2(seconds)}`;
|
|
125
|
+
}
|
|
126
|
+
function compactBody(ms) {
|
|
127
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
128
|
+
const totalSeconds = ms / 1e3;
|
|
129
|
+
if (totalSeconds < 60) {
|
|
130
|
+
return totalSeconds < 10 ? `${totalSeconds.toFixed(1)}s` : `${Math.round(totalSeconds)}s`;
|
|
131
|
+
}
|
|
132
|
+
const whole = Math.round(totalSeconds);
|
|
133
|
+
const minutes = Math.floor(whole / 60);
|
|
134
|
+
if (minutes < 60) return `${minutes}m ${pad2(whole % 60)}s`;
|
|
135
|
+
const hours = Math.floor(minutes / 60);
|
|
136
|
+
return `${hours}h ${pad2(minutes % 60)}m`;
|
|
137
|
+
}
|
|
138
|
+
function coarseBody(ms) {
|
|
139
|
+
const minutes = Math.round(ms / 6e4);
|
|
140
|
+
if (minutes < 1) return "< 1 min";
|
|
141
|
+
if (minutes < 60) return `${minutes} min`;
|
|
142
|
+
const hours = Math.floor(minutes / 60);
|
|
143
|
+
const rest = minutes % 60;
|
|
144
|
+
return rest > 0 ? `${hours}h ${rest}m` : `${hours}h`;
|
|
145
|
+
}
|
|
146
|
+
function formatDurationMs(ms, options = {}) {
|
|
147
|
+
const style = options.style ?? "clock";
|
|
148
|
+
const fallback = options.fallback ?? (style === "clock" ? "0:00" : "\u2014");
|
|
149
|
+
if (ms === null || ms === void 0 || !Number.isFinite(ms)) return fallback;
|
|
150
|
+
const negative = ms < 0;
|
|
151
|
+
if (negative && options.signed !== true) {
|
|
152
|
+
return style === "clock" ? clockBody(0) : fallback;
|
|
153
|
+
}
|
|
154
|
+
const magnitude = Math.abs(ms);
|
|
155
|
+
const body = style === "clock" ? clockBody(Math.floor(magnitude / 1e3)) : style === "compact" ? compactBody(magnitude) : coarseBody(magnitude);
|
|
156
|
+
return negative ? `-${body}` : body;
|
|
157
|
+
}
|
|
158
|
+
function formatDurationSeconds(seconds, options = {}) {
|
|
159
|
+
if (seconds === null || seconds === void 0 || !Number.isFinite(seconds)) {
|
|
160
|
+
return formatDurationMs(seconds, options);
|
|
161
|
+
}
|
|
162
|
+
return formatDurationMs(seconds * 1e3, options);
|
|
163
|
+
}
|
|
164
|
+
function formatDurationMinutes(minutes, options = {}) {
|
|
165
|
+
if (minutes === null || minutes === void 0 || !Number.isFinite(minutes)) {
|
|
166
|
+
return formatDurationMs(minutes, options);
|
|
167
|
+
}
|
|
168
|
+
return formatDurationMs(minutes * 6e4, options);
|
|
169
|
+
}
|
|
170
|
+
function durationMsBetween(from, to, now = Date.now()) {
|
|
171
|
+
const start = parseTimestamp(from);
|
|
172
|
+
if (!start) return null;
|
|
173
|
+
if (to === null || to === void 0) return Math.max(0, now - start.getTime());
|
|
174
|
+
const end = parseTimestamp(to);
|
|
175
|
+
if (!end) return null;
|
|
176
|
+
return Math.max(0, end.getTime() - start.getTime());
|
|
177
|
+
}
|
|
178
|
+
var SIZE_UNITS = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
179
|
+
function formatFileSize(bytes, options = {}) {
|
|
180
|
+
const fallback = options.fallback ?? "\u2014";
|
|
181
|
+
if (bytes === null || bytes === void 0 || !Number.isFinite(bytes) || bytes < 0) {
|
|
182
|
+
return fallback;
|
|
183
|
+
}
|
|
184
|
+
if (bytes < 1024) return `${Math.round(bytes)} B`;
|
|
185
|
+
let value = bytes;
|
|
186
|
+
let unit = 0;
|
|
187
|
+
while (value >= 1024 && unit < SIZE_UNITS.length - 1) {
|
|
188
|
+
value /= 1024;
|
|
189
|
+
unit += 1;
|
|
190
|
+
}
|
|
191
|
+
const rendered = value >= 10 ? String(Math.round(value)) : value.toFixed(1);
|
|
192
|
+
return `${rendered} ${SIZE_UNITS[unit]}`;
|
|
193
|
+
}
|
|
194
|
+
function getInitials(value, options = {}) {
|
|
195
|
+
const fallback = options.fallback ?? "?";
|
|
196
|
+
if (typeof value !== "string") return fallback;
|
|
197
|
+
const parts = value.trim().split(/\s+/).filter((part) => part.length > 0);
|
|
198
|
+
if (parts.length === 0) return fallback;
|
|
199
|
+
const first = parts[0]?.charAt(0) ?? "";
|
|
200
|
+
const last = parts.length > 1 ? parts[parts.length - 1]?.charAt(0) ?? "" : "";
|
|
201
|
+
return `${first}${last}`.toUpperCase() || fallback;
|
|
202
|
+
}
|
|
203
|
+
function avatarPaletteIndex(seed, buckets = 8) {
|
|
204
|
+
let hash = 0;
|
|
205
|
+
for (let index = 0; index < seed.length; index += 1) {
|
|
206
|
+
hash = hash * 31 + seed.charCodeAt(index) | 0;
|
|
207
|
+
}
|
|
208
|
+
return Math.abs(hash) % buckets;
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=format.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/format.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/format — THE one home for the tiny display formatters that\n * every Matrx surface, in every repo, needs and that nothing owns.\n *\n * WHY THIS FILE EXISTS (the H1 finding, 2026-09-07 duplication census). These\n * formatters were duplicated *between packages*: `@ai-matrx/meet` and\n * `@ai-matrx/messaging` each defined `formatDuration` + `getInitials`,\n * `@ai-matrx/associations` and `@ai-matrx/diff` each defined\n * `formatRelativeTime`, and `@ai-matrx/media` hid `formatFileSize` inside a\n * React upload component. Because the fleet's packages disagreed about who\n * owned them, ~65 host twins across matrx-frontend / matrx-extend /\n * matrx-local / matrx-games / aidream's two apps had *no correct package to\n * point at* — a settings page cannot take a dependency on the video-calling\n * package to print a duration.\n *\n * WHY KIT. `@ai-matrx/kit` has ZERO sibling dependencies (verified with\n * `node scripts/check_ts_sibling_graph.mjs --order`), so it sits at the very\n * bottom of the sibling DAG: meet, messaging, associations, diff, media and\n * print can all depend on it without creating a cycle, and so can every host.\n * It is also, by charter, the \"little primitives every Matrx app speaks\"\n * package. This is not a new package — new packages need Arman's approval.\n *\n * THE UNIT LAW. A duration formatter that takes a bare `number` is a bug\n * waiting to happen: the fleet's twins variously took milliseconds, seconds\n * and minutes, and two of them were wrong by 1000×-worth of confusion. There\n * is deliberately NO `formatDuration` here. The unit is in the name:\n * `formatDurationMs`, `formatDurationSeconds`, `formatDurationMinutes`.\n *\n * Everything in this module is pure, synchronous, and free of React, DOM and\n * `Intl` implicit-locale surprises except where a style explicitly asks for\n * `Intl.RelativeTimeFormat`. Every time-dependent function takes an explicit\n * `now` so tests never depend on the clock.\n */\n\n// ───────────────────────── timestamps ─────────────────────────\n\nexport type TimestampInput = string | number | Date | null | undefined;\n\n/** True when a string already carries a timezone designator (Z or ±offset). */\nfunction hasTimezoneDesignator(value: string): boolean {\n if (/[zZ]$/.test(value)) return true;\n if (/[+-]\\d{2}(:?\\d{2})?$/.test(value)) return true;\n if (/\\b(GMT|UTC)\\b/i.test(value)) return true;\n return false;\n}\n\n/**\n * Normalise a raw timestamp string so `new Date()` parses it as an absolute\n * instant.\n *\n * THE CORRECTION THIS CARRIES (from `@ai-matrx/diff`, the richest of the\n * twins). Postgres serialises `timestamp with time zone` as\n * \"2026-06-13T16:32:26+00:00\" (parses correctly) and `timestamp without time\n * zone` as \"2026-06-13T16:32:26\" — which, per the ES spec, `new Date()` parses\n * as LOCAL time. Our backend writes those columns in UTC, so a bare\n * `new Date(...)` is wrong by the viewer's offset: the classic \"times are off\n * by N hours\" report. A zone-less string that HAS a time component is treated\n * as UTC; date-only strings are left untouched (a calendar day is not a\n * timestamp).\n */\nfunction normalizeTimestampString(raw: string): string {\n const value = raw.trim();\n if (!value) return value;\n if (!/\\d{1,2}:\\d{2}/.test(value)) return value;\n if (hasTimezoneDesignator(value)) return value;\n return `${value.replace(\" \", \"T\")}Z`;\n}\n\n/** Parse any backend timestamp into a Date, or `null` when unparseable. */\nexport function parseTimestamp(value: TimestampInput): Date | null {\n if (value === null || value === undefined) return null;\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? null : value;\n }\n if (typeof value === \"number\") {\n const fromNumber = new Date(value);\n return Number.isNaN(fromNumber.getTime()) ? null : fromNumber;\n }\n if (typeof value !== \"string\") return null;\n const normalized = normalizeTimestampString(value);\n if (!normalized) return null;\n const parsed = new Date(normalized);\n return Number.isNaN(parsed.getTime()) ? null : parsed;\n}\n\nconst DEFAULT_ABSOLUTE_OPTIONS: Intl.DateTimeFormatOptions = {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n};\n\n/** Absolute local date+time, e.g. \"Jun 13, 2026, 9:32 AM\". */\nexport function formatAbsoluteDate(\n value: TimestampInput,\n options: Intl.DateTimeFormatOptions = DEFAULT_ABSOLUTE_OPTIONS,\n fallback = \"—\",\n): string {\n const parsed = parseTimestamp(value);\n if (!parsed) return fallback;\n return parsed.toLocaleString(undefined, options);\n}\n\n// ───────────────────────── relative time ─────────────────────────\n\nconst RELATIVE_UNITS: readonly {\n limit: number;\n divisor: number;\n short: string;\n long: string;\n}[] = [\n { limit: 60_000, divisor: 1000, short: \"s\", long: \"second\" },\n { limit: 3_600_000, divisor: 60_000, short: \"m\", long: \"minute\" },\n { limit: 86_400_000, divisor: 3_600_000, short: \"h\", long: \"hour\" },\n { limit: 604_800_000, divisor: 86_400_000, short: \"d\", long: \"day\" },\n { limit: 2_592_000_000, divisor: 604_800_000, short: \"w\", long: \"week\" },\n { limit: 31_536_000_000, divisor: 2_592_000_000, short: \"mo\", long: \"month\" },\n];\n\nconst INTL_UNITS: readonly { unit: Intl.RelativeTimeFormatUnit; ms: number }[] = [\n { unit: \"year\", ms: 365 * 24 * 60 * 60 * 1000 },\n { unit: \"month\", ms: 30 * 24 * 60 * 60 * 1000 },\n { unit: \"week\", ms: 7 * 24 * 60 * 60 * 1000 },\n { unit: \"day\", ms: 24 * 60 * 60 * 1000 },\n { unit: \"hour\", ms: 60 * 60 * 1000 },\n { unit: \"minute\", ms: 60 * 1000 },\n];\n\n/**\n * The three relative-time voices the fleet actually speaks. They are display\n * decisions, not implementations — one body serves all three.\n *\n * - `\"short\"` — \"2m ago\", \"3d ago\". The dense default (from `@ai-matrx/diff`).\n * - `\"long\"` — \"2 minutes ago\", \"3 days ago\".\n * - `\"intl\"` — `Intl.RelativeTimeFormat` with `numeric: \"auto\"`, so the\n * viewer's locale conventions apply and \"yesterday\" reads as \"yesterday\"\n * (from `@ai-matrx/associations`). Sub-minute reads \"just now\".\n */\nexport type RelativeTimeStyle = \"short\" | \"long\" | \"intl\";\n\nexport interface RelativeTimeOptions {\n /** Default `\"short\"`. */\n style?: RelativeTimeStyle | undefined;\n /** Injected clock. Default `Date.now()` — pass it in tests. */\n now?: number | undefined;\n /** Returned for null / unparseable input. Default `\"—\"`. */\n fallback?: string | undefined;\n /**\n * Echo the raw input instead of `fallback` when it cannot be parsed —\n * degraded, never broken. What the comments face wants: an odd server string\n * is more useful on screen than an em-dash that hides it.\n */\n fallbackToInput?: boolean | undefined;\n}\n\n/**\n * \"2m ago\" / \"2 minutes ago\" / a locale-aware \"2 minutes ago\", falling back to\n * an absolute local date past a year. Timezone-agnostic by construction (a\n * pure epoch difference over `parseTimestamp`'s corrected instant).\n *\n * Future timestamps read \"just now\" in `\"short\"`/`\"long\"` (a clock skew of a\n * few seconds must not print \"in 3 seconds\"); `\"intl\"` formats them properly\n * (\"in 5 minutes\") because that is the whole point of asking for `Intl`.\n */\nexport function formatRelativeTime(\n value: TimestampInput,\n options: RelativeTimeOptions = {},\n): string {\n const style = options.style ?? \"short\";\n const now = options.now ?? Date.now();\n const parsed = parseTimestamp(value);\n if (!parsed) {\n if (options.fallbackToInput === true && typeof value === \"string\") return value;\n return options.fallback ?? \"—\";\n }\n\n if (style === \"intl\") {\n const delta = parsed.getTime() - now;\n const magnitude = Math.abs(delta);\n if (magnitude < 60_000) return \"just now\";\n const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: \"auto\" });\n for (const { unit, ms } of INTL_UNITS) {\n if (magnitude >= ms) return rtf.format(Math.trunc(delta / ms), unit);\n }\n return \"just now\";\n }\n\n const elapsed = now - parsed.getTime();\n if (elapsed < 0) return \"just now\";\n for (const unit of RELATIVE_UNITS) {\n if (elapsed < unit.limit) {\n const count = Math.max(1, Math.floor(elapsed / unit.divisor));\n return style === \"long\"\n ? `${count} ${unit.long}${count === 1 ? \"\" : \"s\"} ago`\n : `${count}${unit.short} ago`;\n }\n }\n return formatAbsoluteDate(parsed);\n}\n\n// ───────────────────────── durations ─────────────────────────\n\n/**\n * The three duration voices the fleet actually speaks. Picked once here so a\n * call duration reads the same in the meeting, the history list and the\n * notification.\n *\n * - `\"clock\"` (default) — `0:00`, `9:04`, `1:02:33`. For anything a human\n * watches tick: recordings, calls, media players, timers.\n * - `\"compact\"` — `250ms`, `5.2s`, `5m 30s`, `1h 02m`. For elapsed work:\n * job runs, request timings, step durations. Sub-second is honest about\n * being sub-second rather than collapsing to `0:00`.\n * - `\"coarse\"` — `45 min`, `1h 30m`. For things measured in minutes and read\n * at a glance: podcast episodes, lesson lengths, estimates. Never prints\n * seconds.\n */\nexport type DurationStyle = \"clock\" | \"compact\" | \"coarse\";\n\nexport interface DurationOptions {\n /** Default `\"clock\"`. */\n style?: DurationStyle | undefined;\n /**\n * Returned for `null` / `undefined` / non-finite input. Default `\"0:00\"` for\n * `\"clock\"` and `\"—\"` for `\"compact\"` / `\"coarse\"`.\n */\n fallback?: string | undefined;\n /**\n * Keep the sign on a negative duration (`-1:23`) instead of clamping to\n * zero. Default `false` — a negative elapsed time is nearly always a clock\n * bug, and `0:00` is the honest reading. Opt in where a signed offset is the\n * actual quantity (a field format for a stored `interval`, say).\n */\n signed?: boolean | undefined;\n}\n\nconst pad2 = (value: number): string => value.toString().padStart(2, \"0\");\n\nfunction clockBody(totalSeconds: number): string {\n const seconds = totalSeconds % 60;\n const minutes = Math.floor(totalSeconds / 60) % 60;\n const hours = Math.floor(totalSeconds / 3600);\n return hours > 0\n ? `${hours}:${pad2(minutes)}:${pad2(seconds)}`\n : `${minutes}:${pad2(seconds)}`;\n}\n\nfunction compactBody(ms: number): string {\n if (ms < 1000) return `${Math.round(ms)}ms`;\n const totalSeconds = ms / 1000;\n if (totalSeconds < 60) {\n // One decimal under 10s (0.1s is a visible difference at that scale),\n // whole seconds above it (nobody reads \"43.7s\" as more precise than \"44s\").\n return totalSeconds < 10\n ? `${totalSeconds.toFixed(1)}s`\n : `${Math.round(totalSeconds)}s`;\n }\n const whole = Math.round(totalSeconds);\n const minutes = Math.floor(whole / 60);\n if (minutes < 60) return `${minutes}m ${pad2(whole % 60)}s`;\n const hours = Math.floor(minutes / 60);\n return `${hours}h ${pad2(minutes % 60)}m`;\n}\n\nfunction coarseBody(ms: number): string {\n const minutes = Math.round(ms / 60_000);\n if (minutes < 1) return \"< 1 min\";\n if (minutes < 60) return `${minutes} min`;\n const hours = Math.floor(minutes / 60);\n const rest = minutes % 60;\n return rest > 0 ? `${hours}h ${rest}m` : `${hours}h`;\n}\n\n/**\n * THE canonical duration formatter. Milliseconds in, a chosen voice out.\n *\n * `null`, `undefined`, `NaN` and `Infinity` all take the fallback — never\n * `NaN:NaN`, which is what a null start time produces, and never a confident\n * `0:00` for \"we do not know\".\n */\nexport function formatDurationMs(\n ms: number | null | undefined,\n options: DurationOptions = {},\n): string {\n const style = options.style ?? \"clock\";\n const fallback = options.fallback ?? (style === \"clock\" ? \"0:00\" : \"—\");\n if (ms === null || ms === undefined || !Number.isFinite(ms)) return fallback;\n\n const negative = ms < 0;\n if (negative && options.signed !== true) {\n return style === \"clock\" ? clockBody(0) : fallback;\n }\n const magnitude = Math.abs(ms);\n const body =\n style === \"clock\"\n ? clockBody(Math.floor(magnitude / 1000))\n : style === \"compact\"\n ? compactBody(magnitude)\n : coarseBody(magnitude);\n return negative ? `-${body}` : body;\n}\n\n/** Seconds in. See {@link formatDurationMs}. */\nexport function formatDurationSeconds(\n seconds: number | null | undefined,\n options: DurationOptions = {},\n): string {\n if (seconds === null || seconds === undefined || !Number.isFinite(seconds)) {\n return formatDurationMs(seconds as number | null | undefined, options);\n }\n return formatDurationMs(seconds * 1000, options);\n}\n\n/** Minutes in. See {@link formatDurationMs}. */\nexport function formatDurationMinutes(\n minutes: number | null | undefined,\n options: DurationOptions = {},\n): string {\n if (minutes === null || minutes === undefined || !Number.isFinite(minutes)) {\n return formatDurationMs(minutes as number | null | undefined, options);\n }\n return formatDurationMs(minutes * 60_000, options);\n}\n\n/**\n * Duration between two timestamps, in milliseconds — `null` when the start is\n * missing or either end is unparseable. An absent `to` means \"still running\",\n * so it measures to `now`. Pair with {@link formatDurationMs}.\n */\nexport function durationMsBetween(\n from: TimestampInput,\n to: TimestampInput,\n now: number = Date.now(),\n): number | null {\n const start = parseTimestamp(from);\n if (!start) return null;\n if (to === null || to === undefined) return Math.max(0, now - start.getTime());\n const end = parseTimestamp(to);\n if (!end) return null;\n return Math.max(0, end.getTime() - start.getTime());\n}\n\n// ───────────────────────── byte sizes ─────────────────────────\n\nconst SIZE_UNITS = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\"] as const;\n\nexport interface FileSizeOptions {\n /** Returned for `null` / `undefined` / non-finite / negative. Default `\"—\"`. */\n fallback?: string | undefined;\n}\n\n/**\n * `512` → `\"512 B\"`, `2048` → `\"2.0 KB\"`, `15360` → `\"15 KB\"`.\n *\n * THE DISPLAY DECISIONS, made once (unified 2026-09-07 from six host twins and\n * `@ai-matrx/media`'s copy, which was the richest):\n *\n * 1. Binary units (1024), labelled with the short SI-ish names the whole fleet\n * already used — `B`/`KB`/`MB`/`GB`/`TB`, never `\"Bytes\"`.\n * 2. One decimal below 10 in a unit (`2.0 KB`, `9.9 MB`), whole numbers at or\n * above it (`15 KB`, `340 MB`) — three significant figures is the most a\n * file size is ever worth, and `1.5 GB` versus `1536.0 MB` is the whole\n * point of the unit.\n * 3. Whole bytes below 1 KB — `\"512 B\"`, never `\"512.0 B\"`.\n * 4. `null` / `undefined` / `NaN` / `Infinity` / negative all collapse to an\n * em-dash, NOT to `\"0 B\"`. A corrupt or mid-upload `size` row rendering as\n * a confident, wrong `\"0 B\"` is a screen telling a lie; \"size unknown\" is\n * the honest reading. (`0` itself is a real size and prints `\"0 B\"`.)\n */\nexport function formatFileSize(\n bytes: number | null | undefined,\n options: FileSizeOptions = {},\n): string {\n const fallback = options.fallback ?? \"—\";\n if (bytes === null || bytes === undefined || !Number.isFinite(bytes) || bytes < 0) {\n return fallback;\n }\n if (bytes < 1024) return `${Math.round(bytes)} B`;\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < SIZE_UNITS.length - 1) {\n value /= 1024;\n unit += 1;\n }\n const rendered = value >= 10 ? String(Math.round(value)) : value.toFixed(1);\n return `${rendered} ${SIZE_UNITS[unit]}`;\n}\n\n// ───────────────────────── people ─────────────────────────\n\nexport interface InitialsOptions {\n /** Returned when nothing usable is left. Default `\"?\"`. */\n fallback?: string | undefined;\n}\n\n/**\n * `\"Ana Rivera\"` → `\"AR\"`, `\"Ana\"` → `\"A\"`, `\"Ana Maria Rivera\"` → `\"AR\"`,\n * `\"ana@example.com\"` → `\"A\"`, `\" \"` → `\"?\"`.\n *\n * THE DISPLAY DECISION, made once: a multi-part name takes FIRST + LAST, not\n * first + second. `\"Ana Maria Rivera\"` is `AR`, because the family name is the\n * half a reader recognises. (Six matrx-frontend twins took first + second and\n * printed `AM`; the two package copies both took first + last, and both were\n * tested. First + last wins.)\n *\n * Pass the email as the value when there is no name — a single token yields\n * its first character, which is exactly what the host twins did by hand.\n */\nexport function getInitials(\n value: string | null | undefined,\n options: InitialsOptions = {},\n): string {\n const fallback = options.fallback ?? \"?\";\n if (typeof value !== \"string\") return fallback;\n const parts = value.trim().split(/\\s+/).filter((part) => part.length > 0);\n if (parts.length === 0) return fallback;\n const first = parts[0]?.charAt(0) ?? \"\";\n const last = parts.length > 1 ? (parts[parts.length - 1]?.charAt(0) ?? \"\") : \"\";\n return `${first}${last}`.toUpperCase() || fallback;\n}\n\n/**\n * A stable palette index for an avatar with no image. Deterministic on the\n * seed, so the same person is the same colour on every device and every\n * reload — a random colour per render is a surprisingly loud bug.\n */\nexport function avatarPaletteIndex(seed: string, buckets = 8): number {\n let hash = 0;\n for (let index = 0; index < seed.length; index += 1) {\n hash = (hash * 31 + seed.charCodeAt(index)) | 0;\n }\n return Math.abs(hash) % buckets;\n}\n"],"mappings":"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","names":[]}
|