@ai-matrx/media 0.3.0 → 0.4.1

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 CHANGED
@@ -1,5 +1,161 @@
1
1
  # @ai-matrx/media
2
2
 
3
+ ## 0.4.1 — 2026-08-30
4
+
5
+ **Fixes a crash class in 0.4.0's `/viewers` dispatch. Every 0.4.0 consumer must
6
+ move to 0.4.1** — the bug is invisible until a real engine is registered, which
7
+ is what every host does.
8
+
9
+ ### Consumer action (C28)
10
+
11
+ - Upgrade. No API change; nothing to rewrite.
12
+
13
+ ### The defect
14
+
15
+ `MediaFileViewer` CALLED the registered viewer as a plain function
16
+ (`viewer(props)`), and `CodeViewer` did the same with the registered
17
+ highlighter. Every real engine adapter uses hooks — it resolves a display URL,
18
+ reads the host theme, joins a playback session — so those hooks landed in the
19
+ DISPATCHER's hook list instead of the body's own. A single body renders fine;
20
+ the moment the file kind changes (image → code, code → PDF) the hook count
21
+ changes and React throws "Rendered fewer hooks than expected", taking the whole
22
+ preview surface down. Both now mount through `createElement`, so each body gets
23
+ its own component instance and its own hook list.
24
+
25
+ Found while adopting 0.4.0 in matrx-frontend, before the host swap shipped.
26
+ Guarded by a new test that mounts hook-using bodies and then switches kinds
27
+ three times — the exact sequence that used to crash.
28
+
29
+ ## 0.4.0 — 2026-08-30
30
+
31
+ **The package renders FILES now, not just media elements** — and it knows what
32
+ Next.js is. Two new subpaths close this package's ALL-INCLUSIVE-LAW retrofit
33
+ row: `./next` (the framework seam every Next host was rebuilding) and
34
+ `./viewers` (the full-pane file viewers matrx-frontend owned).
35
+
36
+ ### Consumer action (C28)
37
+
38
+ - **`./next` replaces every hand-written `next/image` adapter.** A host's
39
+ `ImageComponent` port collapses to
40
+ `ImageComponent: NextMediaImage` (or `createNextMediaImageComponent({…})`).
41
+ Delete the local wrapper — including the four `as React.*Handler` casts it
42
+ needed; the package owns those now. `next` is an **optional peer** (`>=14`),
43
+ so non-Next consumers are unaffected and nothing new installs for them.
44
+ - **`./viewers` replaces host-owned text / code / SVG / HTML / generic
45
+ previewers.** A host now registers only what the package cannot ship — its
46
+ heavy engines — and renders `<MediaFileViewer>`:
47
+
48
+ ```tsx
49
+ registerMediaViewer("pdf", HostPdfViewer); // PDF.js, react-pdf…
50
+ registerMediaCodeHighlighter(PrismHighlighter); // optional: colour
51
+ <MediaFileViewer source={{ kind: "ref", ref }} fileName={name} />
52
+ ```
53
+
54
+ Delete the absorbed bodies. Anything NOT registered renders the announcing
55
+ default, never a dead pane.
56
+ - 🚨 **`formatFileSize` behavior change (`/react`).** It now accepts
57
+ `number | null | undefined` and returns `"—"` for null / undefined / NaN /
58
+ negative, where it previously returned a confident, wrong `"0 B"`. Any
59
+ consumer asserting `"0 B"` for a missing size must update. (Defect found
60
+ while absorbing the viewers: matrx-frontend's own `formatFileSize` already
61
+ had this guard and the package's did not — C22 says the package carries the
62
+ better one.)
63
+ - **No other existing export changed shape.**
64
+
65
+ ### `./next` — the Next.js binding (NEW)
66
+
67
+ - `NextMediaImage` and `createNextMediaImageComponent({ unoptimized?, quality?,
68
+ sizes?, priority?, loader?, className? })` — a `MediaHostPorts.ImageComponent`
69
+ implementation over `next/image`, with `unoptimized: true` as the default
70
+ because Matrx durable URLs are session-cookie authenticated and Next's
71
+ optimizer fetches them SERVER-side, where there is no user session.
72
+ - **The deliberate divergence from `@ai-matrx/data`'s `/next`** (which imports
73
+ no `next/*` module): `next/image` is not data crossing a seam, it IS the
74
+ renderer, so no structural seam can express it — the ALL-INCLUSIVE LAW names
75
+ this exact wrapper as package-owned. Confined to this one entry; the
76
+ package's components still reach Next only through the `ImageComponent` port.
77
+ - The port's structurally-typed handlers are narrowed to DOM handler types
78
+ here, once, instead of in every host.
79
+
80
+ ### `./viewers` — full-pane file viewers (NEW)
81
+
82
+ Absorbed from matrx-frontend `features/files/components/core/FilePreview/`:
83
+ `PreviewerSwitch` (the dispatcher) plus the `TextPreview`, `CodePreview`,
84
+ `SvgPreview`, `HtmlPreview` and `GenericPreview` bodies. Behavior ported
85
+ verbatim except where noted below.
86
+
87
+ - `MediaFileViewer` — kind dispatch with the C23 resolution order: a host
88
+ registration wins, else the package's built-in body, else the **announcing
89
+ default** that names the missing engine and the remedy and still offers
90
+ download / view-as-text. A blank pane is not a reachable state.
91
+ - Built-in bodies: `TextViewer`, `CodeViewer`, `SvgViewer` (rendered + XML
92
+ source), `HtmlViewer` (sandboxed frame + source), `GenericViewer`.
93
+ - `registerMediaViewer(kind, Component)` / `registerMediaCodeHighlighter(fn)` —
94
+ the heavy-engine seam, on `globalThis` `Symbol.for` slots so a registration
95
+ made in one loader graph is visible from the other (the tarball canary
96
+ proves it across ESM/CJS). Both return an unregister function.
97
+ - `getPreviewKind(fileName, mimeType?)` — kind derivation from the package's
98
+ own file-kind registry, so a fresh app dispatches correctly with zero setup.
99
+ A host with a richer registry passes `previewKind` explicitly.
100
+ - `MediaViewerSource` has two lanes: `{kind:"ref"}` (everything through the
101
+ MediaClient) and `{kind:"blob"}` (bytes the host already downloaded — proven
102
+ by test to make **zero** client calls).
103
+ - The markup viewers take OPTIONAL `markupControls`; a host control rail drives
104
+ mode / viewport / reload, and without one the viewer draws its own toolbar
105
+ and is fully capable (the original hard-wired itself to an app context).
106
+ - **Why `CodeViewer` ships without a highlighter** (C8, the diff-package Monaco
107
+ precedent): Prism is a ~150KB browser-only engine and a headless consumer
108
+ must not carry it. The package ships monospace, line-numbered, wrap-toggling,
109
+ copyable, truncation-honest code — everything but colour — and the engine
110
+ rides `registerMediaCodeHighlighter`. That default announces nothing because
111
+ nothing failed.
112
+ - **`HtmlViewer`'s sandbox is non-configurable**: `allow-scripts allow-popups
113
+ allow-forms`, never `allow-same-origin`, plus `referrerPolicy="no-referrer"`.
114
+ One wrong token is a real XSS against the host origin, so hosts do not get a
115
+ knob. Asserted in the source suite AND on the packed artifact.
116
+
117
+ ### Defects fixed in the port (never silently — C22)
118
+
119
+ - **A user-visible debug artifact is gone.** `GenericPreview` shipped a
120
+ fuchsia `ring-2` and a "GenericPreview" corner badge — a layering-debug
121
+ visualization commented "Rip this out when done" — to every user opening an
122
+ unpreviewable file. The package has no debug chrome.
123
+ - **`blob.text()` is no longer assumed.** All four absorbed text-reading
124
+ previewers called it directly; it does not exist in Safari < 14, older
125
+ WebViews (the Tauri shell's floor), or jsdom, and the failure surfaced to the
126
+ user as `blob.text is not a function` — as though the FILE were broken.
127
+ `readBlobText` falls back through `arrayBuffer()` and `FileReader`.
128
+ - **`formatFileSize`** — see Consumer action above.
129
+ - **Four copies of the read-cap-truncate-report machinery became one**
130
+ (`useViewerText`), as did the error card, skeleton and copy button.
131
+
132
+ ### Gate
133
+
134
+ `pnpm typecheck && pnpm test && pnpm check:package` green: 125 tests
135
+ (14 new C23 tests across three targets — the announcing default incl. its
136
+ falsifiability leg, the built-in bodies unregistered, and dispatch with zero
137
+ host configuration), publint, attw node16, and a tarball canary extended with
138
+ the `/next` no-cast `MediaHostPorts` assignment, the `/viewers` ESM+CJS export
139
+ and render legs, the packed-artifact sandbox assertion, and the viewer-registry
140
+ dual-graph proof. `/next` is not loaded in the bare-node ESM leg by design:
141
+ Next publishes `next/image` extensionless with no `exports` map, so only
142
+ bundlers and TypeScript resolve it — it is proven via attw, the type canary,
143
+ and a real `require()` in the CJS leg.
144
+
145
+ ### Deliberately NOT absorbed (recorded, not forgotten)
146
+
147
+ - **The PDF viewer tree** (`matrx-frontend/features/pdf`, 49 files): PDF.js +
148
+ annotation layer + document scanner + surface registry + server-side
149
+ extraction API. It is a FEATURE, not a viewer body, and it rides the
150
+ `registerMediaViewer("pdf", …)` seam.
151
+ - **Markdown / spreadsheet / Office viewers**: remark+rehype+KaTeX, SheetJS +
152
+ PapaParse, and a server extraction endpoint respectively — heavy engines or
153
+ host-server-coupled. Same seam.
154
+ - **Image / video / audio viewers**: they bind host domain systems (a
155
+ zoom/rotate control rail, the unified playback-session registry, a
156
+ design-system slider). They already consume `/core`; absorbing their bodies
157
+ is the next wave, not this one.
158
+
3
159
  ## 0.3.0 — 2026-08-30
4
160
 
5
161
  **A terminal media failure can never be silent again.** Born from the
package/README.md CHANGED
@@ -146,6 +146,63 @@ default, never silent. If your `MediaClient.recoverLoadError` returns a
146
146
  `secret_source: "ephemeral"`, the package immediately captures an app-wide
147
147
  warning that private renders will break across server restarts/instances.
148
148
 
149
+ ## File viewers (`/viewers`)
150
+
151
+ Full-pane rendering of ONE file — a preview pane, a file tab, a code editor's
152
+ binary-file fallback. `MediaFileViewer` dispatches on the file kind and ships
153
+ working bodies for text, code, SVG, HTML and unknown files.
154
+
155
+ ```tsx
156
+ import { MediaFileViewer } from "@ai-matrx/media/viewers";
157
+
158
+ <MediaFileViewer
159
+ source={{ kind: "ref", ref: { file_id } }}
160
+ fileName={file.name}
161
+ fileSize={file.size}
162
+ generic={{ onDownload }}
163
+ />;
164
+ ```
165
+
166
+ Kinds whose rendering IS a heavy browser-only engine — PDF, Markdown,
167
+ spreadsheets, Office documents — are yours to register, so a consumer that only
168
+ shows text files never carries a PDF engine:
169
+
170
+ ```tsx
171
+ import { registerMediaViewer, registerMediaCodeHighlighter } from "@ai-matrx/media/viewers";
172
+
173
+ registerMediaViewer("pdf", MyPdfViewer); // gets MediaViewerProps
174
+ registerMediaCodeHighlighter(MyPrismHighlighter); // optional: colour for code
175
+ ```
176
+
177
+ Until you register one, that kind renders an **announcing default**: it names
178
+ the missing engine and the remedy and still offers download / view-as-text. It
179
+ is never a blank pane. (Code is different: with no highlighter registered it
180
+ renders complete — monospace, line-numbered, copyable — just without colour,
181
+ so it announces nothing.)
182
+
183
+ Registering a kind the package already renders replaces the built-in, which is
184
+ how you plug in a domain-specific viewer of your own.
185
+
186
+ The `blob` source lane (`{ kind: "blob", blob, url }`) renders bytes you
187
+ already have without touching the `MediaClient` at all.
188
+
189
+ ## Next.js (`/next`)
190
+
191
+ The `ImageComponent` port, implemented over `next/image` so you never write
192
+ that wrapper again. `next` is an optional peer — nothing installs it for you,
193
+ and no other entry imports it.
194
+
195
+ ```tsx
196
+ import { NextMediaImage } from "@ai-matrx/media/next";
197
+
198
+ const ports: MediaHostPorts = { ImageComponent: NextMediaImage };
199
+ // or: createNextMediaImageComponent({ unoptimized: false, quality: 90 })
200
+ ```
201
+
202
+ It defaults to `unoptimized`, which is correct for Matrx media: durable file
203
+ URLs are session-cookie authenticated and Next's optimizer fetches them from
204
+ the server, where there is no user session.
205
+
149
206
  ## Styling
150
207
 
151
208
  `/react` uses Tailwind utility classes with semantic tokens (`bg-muted`,
@@ -157,9 +214,9 @@ warning that private renders will break across server restarts/instances.
157
214
 
158
215
  ## SSR / RSC
159
216
 
160
- The root entry is server-safe (types + one pure function). `/core`, `/react`
161
- and `/share` are client modules (`"use client"` is stamped per entry).
162
- Nothing touches `window` at import time.
217
+ The root entry is server-safe (types + one pure function). `/core`, `/react`,
218
+ `/share`, `/viewers` and `/next` are client modules (`"use client"` is stamped
219
+ per entry). Nothing touches `window` at import time.
163
220
 
164
221
  ## License
165
222
 
package/dist/next.cjs ADDED
@@ -0,0 +1,92 @@
1
+ "use client";
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/next.ts
32
+ var next_exports = {};
33
+ __export(next_exports, {
34
+ NextMediaImage: () => NextMediaImage,
35
+ createNextMediaImageComponent: () => createNextMediaImageComponent
36
+ });
37
+ module.exports = __toCommonJS(next_exports);
38
+
39
+ // src/next/image.tsx
40
+ var import_image = __toESM(require("next/image"), 1);
41
+ var import_jsx_runtime = require("react/jsx-runtime");
42
+ function createNextMediaImageComponent(options = {}) {
43
+ const {
44
+ unoptimized = true,
45
+ quality,
46
+ sizes,
47
+ priority,
48
+ loader,
49
+ className: baseClassName
50
+ } = options;
51
+ function NextMediaImageWithOptions({
52
+ src,
53
+ alt,
54
+ width,
55
+ height,
56
+ className,
57
+ onError,
58
+ onClick,
59
+ onKeyDown,
60
+ role,
61
+ tabIndex
62
+ }) {
63
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
64
+ import_image.default,
65
+ {
66
+ src,
67
+ alt,
68
+ width,
69
+ height,
70
+ unoptimized,
71
+ ...quality !== void 0 ? { quality } : {},
72
+ ...sizes !== void 0 ? { sizes } : {},
73
+ ...priority !== void 0 ? { priority } : {},
74
+ ...loader !== void 0 ? {
75
+ loader: ({ src: src2, width: width2, quality: loaderQuality }) => loader({ source: src2, width: width2, quality: loaderQuality })
76
+ } : {},
77
+ ...baseClassName !== void 0 || className !== void 0 ? {
78
+ className: [baseClassName, className].filter((part) => !!part).join(" ")
79
+ } : {},
80
+ ...onError !== void 0 ? { onError } : {},
81
+ ...onClick !== void 0 ? { onClick } : {},
82
+ ...onKeyDown !== void 0 ? { onKeyDown } : {},
83
+ ...role !== void 0 ? { role } : {},
84
+ ...tabIndex !== void 0 ? { tabIndex } : {}
85
+ }
86
+ );
87
+ }
88
+ NextMediaImageWithOptions.displayName = "NextMediaImage";
89
+ return NextMediaImageWithOptions;
90
+ }
91
+ var NextMediaImage = createNextMediaImageComponent();
92
+ //# sourceMappingURL=next.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/next.ts","../src/next/image.tsx"],"sourcesContent":["/**\n * `@ai-matrx/media/next` — the Next.js platform binding.\n *\n * ONE export a Next host needs, replacing the wrapper every Next app was\n * hand-writing (`MediaHostPorts.ImageComponent` over `next/image`). This is\n * the ONLY entry in the package that imports a `next/*` module; `next` is an\n * OPTIONAL peer, so no other consumer pays for it and no other entry may\n * reach for it. See the rationale block in `./next/image.tsx`.\n */\n\nexport {\n createNextMediaImageComponent,\n NextMediaImage,\n type NextMediaImageLoaderArgs,\n type NextMediaImageOptions,\n} from \"./next/image\";\n","/**\n * src/next/image.tsx — the Next.js `ImageComponent` port implementation.\n *\n * WHY THIS ENTRY IMPORTS `next/image` (the deliberate divergence from the\n * data 0.5.0 `/next` precedent, which imports NO `next/*` module):\n *\n * `@ai-matrx/data/next` could keep Next out of its source because everything\n * it needed from Next — `cookies()`, `NextResponse` — is DATA passed across a\n * structural seam. `next/image` is not data: it is the rendering component\n * itself, and its entire value (the optimizer endpoint, the srcset builder,\n * the lazy/priority machinery, the blur placeholder) lives inside the module.\n * A \"seam\" for it degenerates into exactly the thing THE ALL-INCLUSIVE LAW\n * names as the failure — \"the `next/image`/`next/link` wrappers\" that every\n * host was rebuilding by hand (matrx-frontend's was 35 lines of prop\n * forwarding with four `as` casts). So this ONE entry takes `next` as an\n * OPTIONAL peer and owns the wrapper; no other entry in the package may.\n *\n * The seam is preserved where it still buys something: the package's\n * components never import this module. They call `MediaHostPorts.ImageComponent`,\n * which defaults to a plain `<img>`. A Next host assigns `NextMediaImage` (or\n * a configured one) and nothing else in the package learns that Next exists.\n */\n\n\"use client\";\n\nimport Image from \"next/image\";\nimport type { MediaImageComponentProps } from \"../types\";\n\n/**\n * Structural mirror of Next's `ImageLoaderProps` — declared here so the\n * package's public types never re-export a `next/*` type (which would make\n * the shape of our API depend on the consumer's Next version).\n */\nexport interface NextMediaImageLoaderArgs {\n source: string;\n width: number;\n quality?: number | undefined;\n}\n\nexport interface NextMediaImageOptions {\n /**\n * Skip Next's optimizer endpoint. Defaults to `true`, which is the ONLY\n * correct default for Matrx media: durable file URLs are session-cookie\n * authenticated, and `/_next/image` fetches them from the SERVER, which\n * carries no user session — an optimized private image 404s. Set this to\n * `false` only for a host whose media is genuinely public CDN content and\n * whose `next.config` allows the remote pattern.\n */\n unoptimized?: boolean | undefined;\n quality?: number | undefined;\n sizes?: string | undefined;\n priority?: boolean | undefined;\n loader?: ((args: NextMediaImageLoaderArgs) => string) | undefined;\n /** Extra class applied under the caller's `className` (last-wins merge). */\n className?: string | undefined;\n}\n\n/**\n * Build a Next.js `ImageComponent` port with fixed options.\n *\n * ```tsx\n * const ports: MediaHostPorts = {\n * ImageComponent: createNextMediaImageComponent({ quality: 90 }),\n * };\n * ```\n */\nexport function createNextMediaImageComponent(\n options: NextMediaImageOptions = {},\n): (props: MediaImageComponentProps) => React.ReactElement {\n const {\n unoptimized = true,\n quality,\n sizes,\n priority,\n loader,\n className: baseClassName,\n } = options;\n\n function NextMediaImageWithOptions({\n src,\n alt,\n width,\n height,\n className,\n onError,\n onClick,\n onKeyDown,\n role,\n tabIndex,\n }: MediaImageComponentProps): React.ReactElement {\n return (\n <Image\n src={src}\n alt={alt}\n width={width}\n height={height}\n unoptimized={unoptimized}\n {...(quality !== undefined ? { quality } : {})}\n {...(sizes !== undefined ? { sizes } : {})}\n {...(priority !== undefined ? { priority } : {})}\n {...(loader !== undefined\n ? {\n loader: ({ src, width, quality: loaderQuality }) =>\n loader({ source: src, width, quality: loaderQuality }),\n }\n : {})}\n {...(baseClassName !== undefined || className !== undefined\n ? {\n className: [baseClassName, className]\n .filter((part): part is string => !!part)\n .join(\" \"),\n }\n : {})}\n // The port's handlers are structurally typed (`(event: unknown) => void`)\n // so the root entry stays free of React type imports. Narrowing them to\n // the DOM handler shapes is this binding's job — a host must never write\n // these casts again (C22: the massage lives in the package).\n {...(onError !== undefined\n ? { onError: onError as React.ReactEventHandler<HTMLImageElement> }\n : {})}\n {...(onClick !== undefined\n ? { onClick: onClick as React.MouseEventHandler<HTMLImageElement> }\n : {})}\n {...(onKeyDown !== undefined\n ? { onKeyDown: onKeyDown as React.KeyboardEventHandler<HTMLImageElement> }\n : {})}\n {...(role !== undefined ? { role } : {})}\n {...(tabIndex !== undefined ? { tabIndex } : {})}\n />\n );\n }\n\n NextMediaImageWithOptions.displayName = \"NextMediaImage\";\n return NextMediaImageWithOptions;\n}\n\n/**\n * The zero-configuration Next.js `ImageComponent` port — `unoptimized`, every\n * port prop forwarded. This is the whole of what a Next host needs:\n *\n * ```tsx\n * import { NextMediaImage } from \"@ai-matrx/media/next\";\n * const ports: MediaHostPorts = { ImageComponent: NextMediaImage };\n * ```\n */\nexport const NextMediaImage = createNextMediaImageComponent();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACyBA,mBAAkB;AAkEZ;AAzBC,SAAS,8BACd,UAAiC,CAAC,GACuB;AACzD,QAAM;AAAA,IACJ,cAAc;AAAA,IACd;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EACb,IAAI;AAEJ,WAAS,0BAA0B;AAAA,IACjC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAAiD;AAC/C,WACE;AAAA,MAAC,aAAAA;AAAA,MAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACC,GAAI,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC;AAAA,QAC3C,GAAI,UAAU,SAAY,EAAE,MAAM,IAAI,CAAC;AAAA,QACvC,GAAI,aAAa,SAAY,EAAE,SAAS,IAAI,CAAC;AAAA,QAC7C,GAAI,WAAW,SACZ;AAAA,UACE,QAAQ,CAAC,EAAE,KAAAC,MAAK,OAAAC,QAAO,SAAS,cAAc,MAC5C,OAAO,EAAE,QAAQD,MAAK,OAAAC,QAAO,SAAS,cAAc,CAAC;AAAA,QACzD,IACA,CAAC;AAAA,QACJ,GAAI,kBAAkB,UAAa,cAAc,SAC9C;AAAA,UACE,WAAW,CAAC,eAAe,SAAS,EACjC,OAAO,CAAC,SAAyB,CAAC,CAAC,IAAI,EACvC,KAAK,GAAG;AAAA,QACb,IACA,CAAC;AAAA,QAKJ,GAAI,YAAY,SACb,EAAE,QAA8D,IAChE,CAAC;AAAA,QACJ,GAAI,YAAY,SACb,EAAE,QAA8D,IAChE,CAAC;AAAA,QACJ,GAAI,cAAc,SACf,EAAE,UAAqE,IACvE,CAAC;AAAA,QACJ,GAAI,SAAS,SAAY,EAAE,KAAK,IAAI,CAAC;AAAA,QACrC,GAAI,aAAa,SAAY,EAAE,SAAS,IAAI,CAAC;AAAA;AAAA,IAChD;AAAA,EAEJ;AAEA,4BAA0B,cAAc;AACxC,SAAO;AACT;AAWO,IAAM,iBAAiB,8BAA8B;","names":["Image","src","width"]}
@@ -0,0 +1,74 @@
1
+ import { MediaImageComponentProps } from './index.cjs';
2
+
3
+ /**
4
+ * src/next/image.tsx — the Next.js `ImageComponent` port implementation.
5
+ *
6
+ * WHY THIS ENTRY IMPORTS `next/image` (the deliberate divergence from the
7
+ * data 0.5.0 `/next` precedent, which imports NO `next/*` module):
8
+ *
9
+ * `@ai-matrx/data/next` could keep Next out of its source because everything
10
+ * it needed from Next — `cookies()`, `NextResponse` — is DATA passed across a
11
+ * structural seam. `next/image` is not data: it is the rendering component
12
+ * itself, and its entire value (the optimizer endpoint, the srcset builder,
13
+ * the lazy/priority machinery, the blur placeholder) lives inside the module.
14
+ * A "seam" for it degenerates into exactly the thing THE ALL-INCLUSIVE LAW
15
+ * names as the failure — "the `next/image`/`next/link` wrappers" that every
16
+ * host was rebuilding by hand (matrx-frontend's was 35 lines of prop
17
+ * forwarding with four `as` casts). So this ONE entry takes `next` as an
18
+ * OPTIONAL peer and owns the wrapper; no other entry in the package may.
19
+ *
20
+ * The seam is preserved where it still buys something: the package's
21
+ * components never import this module. They call `MediaHostPorts.ImageComponent`,
22
+ * which defaults to a plain `<img>`. A Next host assigns `NextMediaImage` (or
23
+ * a configured one) and nothing else in the package learns that Next exists.
24
+ */
25
+
26
+ /**
27
+ * Structural mirror of Next's `ImageLoaderProps` — declared here so the
28
+ * package's public types never re-export a `next/*` type (which would make
29
+ * the shape of our API depend on the consumer's Next version).
30
+ */
31
+ interface NextMediaImageLoaderArgs {
32
+ source: string;
33
+ width: number;
34
+ quality?: number | undefined;
35
+ }
36
+ interface NextMediaImageOptions {
37
+ /**
38
+ * Skip Next's optimizer endpoint. Defaults to `true`, which is the ONLY
39
+ * correct default for Matrx media: durable file URLs are session-cookie
40
+ * authenticated, and `/_next/image` fetches them from the SERVER, which
41
+ * carries no user session — an optimized private image 404s. Set this to
42
+ * `false` only for a host whose media is genuinely public CDN content and
43
+ * whose `next.config` allows the remote pattern.
44
+ */
45
+ unoptimized?: boolean | undefined;
46
+ quality?: number | undefined;
47
+ sizes?: string | undefined;
48
+ priority?: boolean | undefined;
49
+ loader?: ((args: NextMediaImageLoaderArgs) => string) | undefined;
50
+ /** Extra class applied under the caller's `className` (last-wins merge). */
51
+ className?: string | undefined;
52
+ }
53
+ /**
54
+ * Build a Next.js `ImageComponent` port with fixed options.
55
+ *
56
+ * ```tsx
57
+ * const ports: MediaHostPorts = {
58
+ * ImageComponent: createNextMediaImageComponent({ quality: 90 }),
59
+ * };
60
+ * ```
61
+ */
62
+ declare function createNextMediaImageComponent(options?: NextMediaImageOptions): (props: MediaImageComponentProps) => React.ReactElement;
63
+ /**
64
+ * The zero-configuration Next.js `ImageComponent` port — `unoptimized`, every
65
+ * port prop forwarded. This is the whole of what a Next host needs:
66
+ *
67
+ * ```tsx
68
+ * import { NextMediaImage } from "@ai-matrx/media/next";
69
+ * const ports: MediaHostPorts = { ImageComponent: NextMediaImage };
70
+ * ```
71
+ */
72
+ declare const NextMediaImage: (props: MediaImageComponentProps) => React.ReactElement;
73
+
74
+ export { NextMediaImage, type NextMediaImageLoaderArgs, type NextMediaImageOptions, createNextMediaImageComponent };
package/dist/next.d.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { MediaImageComponentProps } from './index.js';
2
+
3
+ /**
4
+ * src/next/image.tsx — the Next.js `ImageComponent` port implementation.
5
+ *
6
+ * WHY THIS ENTRY IMPORTS `next/image` (the deliberate divergence from the
7
+ * data 0.5.0 `/next` precedent, which imports NO `next/*` module):
8
+ *
9
+ * `@ai-matrx/data/next` could keep Next out of its source because everything
10
+ * it needed from Next — `cookies()`, `NextResponse` — is DATA passed across a
11
+ * structural seam. `next/image` is not data: it is the rendering component
12
+ * itself, and its entire value (the optimizer endpoint, the srcset builder,
13
+ * the lazy/priority machinery, the blur placeholder) lives inside the module.
14
+ * A "seam" for it degenerates into exactly the thing THE ALL-INCLUSIVE LAW
15
+ * names as the failure — "the `next/image`/`next/link` wrappers" that every
16
+ * host was rebuilding by hand (matrx-frontend's was 35 lines of prop
17
+ * forwarding with four `as` casts). So this ONE entry takes `next` as an
18
+ * OPTIONAL peer and owns the wrapper; no other entry in the package may.
19
+ *
20
+ * The seam is preserved where it still buys something: the package's
21
+ * components never import this module. They call `MediaHostPorts.ImageComponent`,
22
+ * which defaults to a plain `<img>`. A Next host assigns `NextMediaImage` (or
23
+ * a configured one) and nothing else in the package learns that Next exists.
24
+ */
25
+
26
+ /**
27
+ * Structural mirror of Next's `ImageLoaderProps` — declared here so the
28
+ * package's public types never re-export a `next/*` type (which would make
29
+ * the shape of our API depend on the consumer's Next version).
30
+ */
31
+ interface NextMediaImageLoaderArgs {
32
+ source: string;
33
+ width: number;
34
+ quality?: number | undefined;
35
+ }
36
+ interface NextMediaImageOptions {
37
+ /**
38
+ * Skip Next's optimizer endpoint. Defaults to `true`, which is the ONLY
39
+ * correct default for Matrx media: durable file URLs are session-cookie
40
+ * authenticated, and `/_next/image` fetches them from the SERVER, which
41
+ * carries no user session — an optimized private image 404s. Set this to
42
+ * `false` only for a host whose media is genuinely public CDN content and
43
+ * whose `next.config` allows the remote pattern.
44
+ */
45
+ unoptimized?: boolean | undefined;
46
+ quality?: number | undefined;
47
+ sizes?: string | undefined;
48
+ priority?: boolean | undefined;
49
+ loader?: ((args: NextMediaImageLoaderArgs) => string) | undefined;
50
+ /** Extra class applied under the caller's `className` (last-wins merge). */
51
+ className?: string | undefined;
52
+ }
53
+ /**
54
+ * Build a Next.js `ImageComponent` port with fixed options.
55
+ *
56
+ * ```tsx
57
+ * const ports: MediaHostPorts = {
58
+ * ImageComponent: createNextMediaImageComponent({ quality: 90 }),
59
+ * };
60
+ * ```
61
+ */
62
+ declare function createNextMediaImageComponent(options?: NextMediaImageOptions): (props: MediaImageComponentProps) => React.ReactElement;
63
+ /**
64
+ * The zero-configuration Next.js `ImageComponent` port — `unoptimized`, every
65
+ * port prop forwarded. This is the whole of what a Next host needs:
66
+ *
67
+ * ```tsx
68
+ * import { NextMediaImage } from "@ai-matrx/media/next";
69
+ * const ports: MediaHostPorts = { ImageComponent: NextMediaImage };
70
+ * ```
71
+ */
72
+ declare const NextMediaImage: (props: MediaImageComponentProps) => React.ReactElement;
73
+
74
+ export { NextMediaImage, type NextMediaImageLoaderArgs, type NextMediaImageOptions, createNextMediaImageComponent };
package/dist/next.js ADDED
@@ -0,0 +1,60 @@
1
+ "use client";
2
+
3
+ // src/next/image.tsx
4
+ import Image from "next/image";
5
+ import { jsx } from "react/jsx-runtime";
6
+ function createNextMediaImageComponent(options = {}) {
7
+ const {
8
+ unoptimized = true,
9
+ quality,
10
+ sizes,
11
+ priority,
12
+ loader,
13
+ className: baseClassName
14
+ } = options;
15
+ function NextMediaImageWithOptions({
16
+ src,
17
+ alt,
18
+ width,
19
+ height,
20
+ className,
21
+ onError,
22
+ onClick,
23
+ onKeyDown,
24
+ role,
25
+ tabIndex
26
+ }) {
27
+ return /* @__PURE__ */ jsx(
28
+ Image,
29
+ {
30
+ src,
31
+ alt,
32
+ width,
33
+ height,
34
+ unoptimized,
35
+ ...quality !== void 0 ? { quality } : {},
36
+ ...sizes !== void 0 ? { sizes } : {},
37
+ ...priority !== void 0 ? { priority } : {},
38
+ ...loader !== void 0 ? {
39
+ loader: ({ src: src2, width: width2, quality: loaderQuality }) => loader({ source: src2, width: width2, quality: loaderQuality })
40
+ } : {},
41
+ ...baseClassName !== void 0 || className !== void 0 ? {
42
+ className: [baseClassName, className].filter((part) => !!part).join(" ")
43
+ } : {},
44
+ ...onError !== void 0 ? { onError } : {},
45
+ ...onClick !== void 0 ? { onClick } : {},
46
+ ...onKeyDown !== void 0 ? { onKeyDown } : {},
47
+ ...role !== void 0 ? { role } : {},
48
+ ...tabIndex !== void 0 ? { tabIndex } : {}
49
+ }
50
+ );
51
+ }
52
+ NextMediaImageWithOptions.displayName = "NextMediaImage";
53
+ return NextMediaImageWithOptions;
54
+ }
55
+ var NextMediaImage = createNextMediaImageComponent();
56
+ export {
57
+ NextMediaImage,
58
+ createNextMediaImageComponent
59
+ };
60
+ //# sourceMappingURL=next.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/next/image.tsx"],"sourcesContent":["/**\n * src/next/image.tsx — the Next.js `ImageComponent` port implementation.\n *\n * WHY THIS ENTRY IMPORTS `next/image` (the deliberate divergence from the\n * data 0.5.0 `/next` precedent, which imports NO `next/*` module):\n *\n * `@ai-matrx/data/next` could keep Next out of its source because everything\n * it needed from Next — `cookies()`, `NextResponse` — is DATA passed across a\n * structural seam. `next/image` is not data: it is the rendering component\n * itself, and its entire value (the optimizer endpoint, the srcset builder,\n * the lazy/priority machinery, the blur placeholder) lives inside the module.\n * A \"seam\" for it degenerates into exactly the thing THE ALL-INCLUSIVE LAW\n * names as the failure — \"the `next/image`/`next/link` wrappers\" that every\n * host was rebuilding by hand (matrx-frontend's was 35 lines of prop\n * forwarding with four `as` casts). So this ONE entry takes `next` as an\n * OPTIONAL peer and owns the wrapper; no other entry in the package may.\n *\n * The seam is preserved where it still buys something: the package's\n * components never import this module. They call `MediaHostPorts.ImageComponent`,\n * which defaults to a plain `<img>`. A Next host assigns `NextMediaImage` (or\n * a configured one) and nothing else in the package learns that Next exists.\n */\n\n\"use client\";\n\nimport Image from \"next/image\";\nimport type { MediaImageComponentProps } from \"../types\";\n\n/**\n * Structural mirror of Next's `ImageLoaderProps` — declared here so the\n * package's public types never re-export a `next/*` type (which would make\n * the shape of our API depend on the consumer's Next version).\n */\nexport interface NextMediaImageLoaderArgs {\n source: string;\n width: number;\n quality?: number | undefined;\n}\n\nexport interface NextMediaImageOptions {\n /**\n * Skip Next's optimizer endpoint. Defaults to `true`, which is the ONLY\n * correct default for Matrx media: durable file URLs are session-cookie\n * authenticated, and `/_next/image` fetches them from the SERVER, which\n * carries no user session — an optimized private image 404s. Set this to\n * `false` only for a host whose media is genuinely public CDN content and\n * whose `next.config` allows the remote pattern.\n */\n unoptimized?: boolean | undefined;\n quality?: number | undefined;\n sizes?: string | undefined;\n priority?: boolean | undefined;\n loader?: ((args: NextMediaImageLoaderArgs) => string) | undefined;\n /** Extra class applied under the caller's `className` (last-wins merge). */\n className?: string | undefined;\n}\n\n/**\n * Build a Next.js `ImageComponent` port with fixed options.\n *\n * ```tsx\n * const ports: MediaHostPorts = {\n * ImageComponent: createNextMediaImageComponent({ quality: 90 }),\n * };\n * ```\n */\nexport function createNextMediaImageComponent(\n options: NextMediaImageOptions = {},\n): (props: MediaImageComponentProps) => React.ReactElement {\n const {\n unoptimized = true,\n quality,\n sizes,\n priority,\n loader,\n className: baseClassName,\n } = options;\n\n function NextMediaImageWithOptions({\n src,\n alt,\n width,\n height,\n className,\n onError,\n onClick,\n onKeyDown,\n role,\n tabIndex,\n }: MediaImageComponentProps): React.ReactElement {\n return (\n <Image\n src={src}\n alt={alt}\n width={width}\n height={height}\n unoptimized={unoptimized}\n {...(quality !== undefined ? { quality } : {})}\n {...(sizes !== undefined ? { sizes } : {})}\n {...(priority !== undefined ? { priority } : {})}\n {...(loader !== undefined\n ? {\n loader: ({ src, width, quality: loaderQuality }) =>\n loader({ source: src, width, quality: loaderQuality }),\n }\n : {})}\n {...(baseClassName !== undefined || className !== undefined\n ? {\n className: [baseClassName, className]\n .filter((part): part is string => !!part)\n .join(\" \"),\n }\n : {})}\n // The port's handlers are structurally typed (`(event: unknown) => void`)\n // so the root entry stays free of React type imports. Narrowing them to\n // the DOM handler shapes is this binding's job — a host must never write\n // these casts again (C22: the massage lives in the package).\n {...(onError !== undefined\n ? { onError: onError as React.ReactEventHandler<HTMLImageElement> }\n : {})}\n {...(onClick !== undefined\n ? { onClick: onClick as React.MouseEventHandler<HTMLImageElement> }\n : {})}\n {...(onKeyDown !== undefined\n ? { onKeyDown: onKeyDown as React.KeyboardEventHandler<HTMLImageElement> }\n : {})}\n {...(role !== undefined ? { role } : {})}\n {...(tabIndex !== undefined ? { tabIndex } : {})}\n />\n );\n }\n\n NextMediaImageWithOptions.displayName = \"NextMediaImage\";\n return NextMediaImageWithOptions;\n}\n\n/**\n * The zero-configuration Next.js `ImageComponent` port — `unoptimized`, every\n * port prop forwarded. This is the whole of what a Next host needs:\n *\n * ```tsx\n * import { NextMediaImage } from \"@ai-matrx/media/next\";\n * const ports: MediaHostPorts = { ImageComponent: NextMediaImage };\n * ```\n */\nexport const NextMediaImage = createNextMediaImageComponent();\n"],"mappings":";;;AAyBA,OAAO,WAAW;AAkEZ;AAzBC,SAAS,8BACd,UAAiC,CAAC,GACuB;AACzD,QAAM;AAAA,IACJ,cAAc;AAAA,IACd;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EACb,IAAI;AAEJ,WAAS,0BAA0B;AAAA,IACjC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAAiD;AAC/C,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACC,GAAI,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC;AAAA,QAC3C,GAAI,UAAU,SAAY,EAAE,MAAM,IAAI,CAAC;AAAA,QACvC,GAAI,aAAa,SAAY,EAAE,SAAS,IAAI,CAAC;AAAA,QAC7C,GAAI,WAAW,SACZ;AAAA,UACE,QAAQ,CAAC,EAAE,KAAAA,MAAK,OAAAC,QAAO,SAAS,cAAc,MAC5C,OAAO,EAAE,QAAQD,MAAK,OAAAC,QAAO,SAAS,cAAc,CAAC;AAAA,QACzD,IACA,CAAC;AAAA,QACJ,GAAI,kBAAkB,UAAa,cAAc,SAC9C;AAAA,UACE,WAAW,CAAC,eAAe,SAAS,EACjC,OAAO,CAAC,SAAyB,CAAC,CAAC,IAAI,EACvC,KAAK,GAAG;AAAA,QACb,IACA,CAAC;AAAA,QAKJ,GAAI,YAAY,SACb,EAAE,QAA8D,IAChE,CAAC;AAAA,QACJ,GAAI,YAAY,SACb,EAAE,QAA8D,IAChE,CAAC;AAAA,QACJ,GAAI,cAAc,SACf,EAAE,UAAqE,IACvE,CAAC;AAAA,QACJ,GAAI,SAAS,SAAY,EAAE,KAAK,IAAI,CAAC;AAAA,QACrC,GAAI,aAAa,SAAY,EAAE,SAAS,IAAI,CAAC;AAAA;AAAA,IAChD;AAAA,EAEJ;AAEA,4BAA0B,cAAc;AACxC,SAAO;AACT;AAWO,IAAM,iBAAiB,8BAA8B;","names":["src","width"]}
package/dist/react.cjs CHANGED
@@ -1803,7 +1803,7 @@ var import_react11 = require("react");
1803
1803
  var import_jsx_runtime5 = require("react/jsx-runtime");
1804
1804
  var AUTO_DISMISS_MS = 1e4;
1805
1805
  function formatFileSize(bytes) {
1806
- if (!Number.isFinite(bytes) || bytes < 0) return "0 B";
1806
+ if (bytes == null || !Number.isFinite(bytes) || bytes < 0) return "\u2014";
1807
1807
  if (bytes < 1024) return `${bytes} B`;
1808
1808
  const units = ["KB", "MB", "GB", "TB"];
1809
1809
  let value = bytes;