@ai-matrx/media 0.3.0 → 0.4.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 CHANGED
@@ -1,5 +1,135 @@
1
1
  # @ai-matrx/media
2
2
 
3
+ ## 0.4.0 — 2026-08-30
4
+
5
+ **The package renders FILES now, not just media elements** — and it knows what
6
+ Next.js is. Two new subpaths close this package's ALL-INCLUSIVE-LAW retrofit
7
+ row: `./next` (the framework seam every Next host was rebuilding) and
8
+ `./viewers` (the full-pane file viewers matrx-frontend owned).
9
+
10
+ ### Consumer action (C28)
11
+
12
+ - **`./next` replaces every hand-written `next/image` adapter.** A host's
13
+ `ImageComponent` port collapses to
14
+ `ImageComponent: NextMediaImage` (or `createNextMediaImageComponent({…})`).
15
+ Delete the local wrapper — including the four `as React.*Handler` casts it
16
+ needed; the package owns those now. `next` is an **optional peer** (`>=14`),
17
+ so non-Next consumers are unaffected and nothing new installs for them.
18
+ - **`./viewers` replaces host-owned text / code / SVG / HTML / generic
19
+ previewers.** A host now registers only what the package cannot ship — its
20
+ heavy engines — and renders `<MediaFileViewer>`:
21
+
22
+ ```tsx
23
+ registerMediaViewer("pdf", HostPdfViewer); // PDF.js, react-pdf…
24
+ registerMediaCodeHighlighter(PrismHighlighter); // optional: colour
25
+ <MediaFileViewer source={{ kind: "ref", ref }} fileName={name} />
26
+ ```
27
+
28
+ Delete the absorbed bodies. Anything NOT registered renders the announcing
29
+ default, never a dead pane.
30
+ - 🚨 **`formatFileSize` behavior change (`/react`).** It now accepts
31
+ `number | null | undefined` and returns `"—"` for null / undefined / NaN /
32
+ negative, where it previously returned a confident, wrong `"0 B"`. Any
33
+ consumer asserting `"0 B"` for a missing size must update. (Defect found
34
+ while absorbing the viewers: matrx-frontend's own `formatFileSize` already
35
+ had this guard and the package's did not — C22 says the package carries the
36
+ better one.)
37
+ - **No other existing export changed shape.**
38
+
39
+ ### `./next` — the Next.js binding (NEW)
40
+
41
+ - `NextMediaImage` and `createNextMediaImageComponent({ unoptimized?, quality?,
42
+ sizes?, priority?, loader?, className? })` — a `MediaHostPorts.ImageComponent`
43
+ implementation over `next/image`, with `unoptimized: true` as the default
44
+ because Matrx durable URLs are session-cookie authenticated and Next's
45
+ optimizer fetches them SERVER-side, where there is no user session.
46
+ - **The deliberate divergence from `@ai-matrx/data`'s `/next`** (which imports
47
+ no `next/*` module): `next/image` is not data crossing a seam, it IS the
48
+ renderer, so no structural seam can express it — the ALL-INCLUSIVE LAW names
49
+ this exact wrapper as package-owned. Confined to this one entry; the
50
+ package's components still reach Next only through the `ImageComponent` port.
51
+ - The port's structurally-typed handlers are narrowed to DOM handler types
52
+ here, once, instead of in every host.
53
+
54
+ ### `./viewers` — full-pane file viewers (NEW)
55
+
56
+ Absorbed from matrx-frontend `features/files/components/core/FilePreview/`:
57
+ `PreviewerSwitch` (the dispatcher) plus the `TextPreview`, `CodePreview`,
58
+ `SvgPreview`, `HtmlPreview` and `GenericPreview` bodies. Behavior ported
59
+ verbatim except where noted below.
60
+
61
+ - `MediaFileViewer` — kind dispatch with the C23 resolution order: a host
62
+ registration wins, else the package's built-in body, else the **announcing
63
+ default** that names the missing engine and the remedy and still offers
64
+ download / view-as-text. A blank pane is not a reachable state.
65
+ - Built-in bodies: `TextViewer`, `CodeViewer`, `SvgViewer` (rendered + XML
66
+ source), `HtmlViewer` (sandboxed frame + source), `GenericViewer`.
67
+ - `registerMediaViewer(kind, Component)` / `registerMediaCodeHighlighter(fn)` —
68
+ the heavy-engine seam, on `globalThis` `Symbol.for` slots so a registration
69
+ made in one loader graph is visible from the other (the tarball canary
70
+ proves it across ESM/CJS). Both return an unregister function.
71
+ - `getPreviewKind(fileName, mimeType?)` — kind derivation from the package's
72
+ own file-kind registry, so a fresh app dispatches correctly with zero setup.
73
+ A host with a richer registry passes `previewKind` explicitly.
74
+ - `MediaViewerSource` has two lanes: `{kind:"ref"}` (everything through the
75
+ MediaClient) and `{kind:"blob"}` (bytes the host already downloaded — proven
76
+ by test to make **zero** client calls).
77
+ - The markup viewers take OPTIONAL `markupControls`; a host control rail drives
78
+ mode / viewport / reload, and without one the viewer draws its own toolbar
79
+ and is fully capable (the original hard-wired itself to an app context).
80
+ - **Why `CodeViewer` ships without a highlighter** (C8, the diff-package Monaco
81
+ precedent): Prism is a ~150KB browser-only engine and a headless consumer
82
+ must not carry it. The package ships monospace, line-numbered, wrap-toggling,
83
+ copyable, truncation-honest code — everything but colour — and the engine
84
+ rides `registerMediaCodeHighlighter`. That default announces nothing because
85
+ nothing failed.
86
+ - **`HtmlViewer`'s sandbox is non-configurable**: `allow-scripts allow-popups
87
+ allow-forms`, never `allow-same-origin`, plus `referrerPolicy="no-referrer"`.
88
+ One wrong token is a real XSS against the host origin, so hosts do not get a
89
+ knob. Asserted in the source suite AND on the packed artifact.
90
+
91
+ ### Defects fixed in the port (never silently — C22)
92
+
93
+ - **A user-visible debug artifact is gone.** `GenericPreview` shipped a
94
+ fuchsia `ring-2` and a "GenericPreview" corner badge — a layering-debug
95
+ visualization commented "Rip this out when done" — to every user opening an
96
+ unpreviewable file. The package has no debug chrome.
97
+ - **`blob.text()` is no longer assumed.** All four absorbed text-reading
98
+ previewers called it directly; it does not exist in Safari < 14, older
99
+ WebViews (the Tauri shell's floor), or jsdom, and the failure surfaced to the
100
+ user as `blob.text is not a function` — as though the FILE were broken.
101
+ `readBlobText` falls back through `arrayBuffer()` and `FileReader`.
102
+ - **`formatFileSize`** — see Consumer action above.
103
+ - **Four copies of the read-cap-truncate-report machinery became one**
104
+ (`useViewerText`), as did the error card, skeleton and copy button.
105
+
106
+ ### Gate
107
+
108
+ `pnpm typecheck && pnpm test && pnpm check:package` green: 125 tests
109
+ (14 new C23 tests across three targets — the announcing default incl. its
110
+ falsifiability leg, the built-in bodies unregistered, and dispatch with zero
111
+ host configuration), publint, attw node16, and a tarball canary extended with
112
+ the `/next` no-cast `MediaHostPorts` assignment, the `/viewers` ESM+CJS export
113
+ and render legs, the packed-artifact sandbox assertion, and the viewer-registry
114
+ dual-graph proof. `/next` is not loaded in the bare-node ESM leg by design:
115
+ Next publishes `next/image` extensionless with no `exports` map, so only
116
+ bundlers and TypeScript resolve it — it is proven via attw, the type canary,
117
+ and a real `require()` in the CJS leg.
118
+
119
+ ### Deliberately NOT absorbed (recorded, not forgotten)
120
+
121
+ - **The PDF viewer tree** (`matrx-frontend/features/pdf`, 49 files): PDF.js +
122
+ annotation layer + document scanner + surface registry + server-side
123
+ extraction API. It is a FEATURE, not a viewer body, and it rides the
124
+ `registerMediaViewer("pdf", …)` seam.
125
+ - **Markdown / spreadsheet / Office viewers**: remark+rehype+KaTeX, SheetJS +
126
+ PapaParse, and a server extraction endpoint respectively — heavy engines or
127
+ host-server-coupled. Same seam.
128
+ - **Image / video / audio viewers**: they bind host domain systems (a
129
+ zoom/rotate control rail, the unified playback-session registry, a
130
+ design-system slider). They already consume `/core`; absorbing their bodies
131
+ is the next wave, not this one.
132
+
3
133
  ## 0.3.0 — 2026-08-30
4
134
 
5
135
  **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;