@orz/ui-content 0.0.0-stage → 0.0.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.
Files changed (84) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +410 -2
  3. package/dist/attachment/card-content.d.ts +26 -0
  4. package/dist/attachment/card-content.d.ts.map +1 -0
  5. package/dist/attachment/card-content.js +49 -0
  6. package/dist/attachment/card-content.js.map +1 -0
  7. package/dist/attachment/deleted.d.ts +9 -0
  8. package/dist/attachment/deleted.d.ts.map +1 -0
  9. package/dist/attachment/deleted.js +11 -0
  10. package/dist/attachment/deleted.js.map +1 -0
  11. package/dist/attachment/document-card.d.ts +32 -0
  12. package/dist/attachment/document-card.d.ts.map +1 -0
  13. package/dist/attachment/document-card.js +38 -0
  14. package/dist/attachment/document-card.js.map +1 -0
  15. package/dist/attachment/icon.d.ts +13 -0
  16. package/dist/attachment/icon.d.ts.map +1 -0
  17. package/dist/attachment/icon.js +34 -0
  18. package/dist/attachment/icon.js.map +1 -0
  19. package/dist/attachment/image-canvas.d.ts +25 -0
  20. package/dist/attachment/image-canvas.d.ts.map +1 -0
  21. package/dist/attachment/image-canvas.js +301 -0
  22. package/dist/attachment/image-canvas.js.map +1 -0
  23. package/dist/attachment/image-look.d.ts +18 -0
  24. package/dist/attachment/image-look.d.ts.map +1 -0
  25. package/dist/attachment/image-look.js +86 -0
  26. package/dist/attachment/image-look.js.map +1 -0
  27. package/dist/attachment/image-zoom.d.ts +67 -0
  28. package/dist/attachment/image-zoom.d.ts.map +1 -0
  29. package/dist/attachment/image-zoom.js +108 -0
  30. package/dist/attachment/image-zoom.js.map +1 -0
  31. package/dist/attachment/index.d.ts +12 -0
  32. package/dist/attachment/index.d.ts.map +1 -0
  33. package/dist/attachment/index.js +12 -0
  34. package/dist/attachment/index.js.map +1 -0
  35. package/dist/attachment/link.d.ts +18 -0
  36. package/dist/attachment/link.d.ts.map +1 -0
  37. package/dist/attachment/link.js +30 -0
  38. package/dist/attachment/link.js.map +1 -0
  39. package/dist/attachment/list.d.ts +26 -0
  40. package/dist/attachment/list.d.ts.map +1 -0
  41. package/dist/attachment/list.js +53 -0
  42. package/dist/attachment/list.js.map +1 -0
  43. package/dist/attachment/media-content.d.ts +21 -0
  44. package/dist/attachment/media-content.d.ts.map +1 -0
  45. package/dist/attachment/media-content.js +133 -0
  46. package/dist/attachment/media-content.js.map +1 -0
  47. package/dist/attachment/media-player.d.ts +23 -0
  48. package/dist/attachment/media-player.d.ts.map +1 -0
  49. package/dist/attachment/media-player.js +137 -0
  50. package/dist/attachment/media-player.js.map +1 -0
  51. package/dist/attachment/use-media.d.ts +27 -0
  52. package/dist/attachment/use-media.d.ts.map +1 -0
  53. package/dist/attachment/use-media.js +133 -0
  54. package/dist/attachment/use-media.js.map +1 -0
  55. package/dist/attachment/viewer.d.ts +85 -0
  56. package/dist/attachment/viewer.d.ts.map +1 -0
  57. package/dist/attachment/viewer.js +245 -0
  58. package/dist/attachment/viewer.js.map +1 -0
  59. package/dist/file-kind.d.ts +61 -0
  60. package/dist/file-kind.d.ts.map +1 -0
  61. package/dist/file-kind.js +166 -0
  62. package/dist/file-kind.js.map +1 -0
  63. package/dist/index.d.ts +6 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +5 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/registry.d.ts +44 -0
  68. package/dist/registry.d.ts.map +1 -0
  69. package/dist/registry.js +109 -0
  70. package/dist/registry.js.map +1 -0
  71. package/dist/source-text.d.ts +29 -0
  72. package/dist/source-text.d.ts.map +1 -0
  73. package/dist/source-text.js +101 -0
  74. package/dist/source-text.js.map +1 -0
  75. package/dist/title-bar.d.ts +23 -0
  76. package/dist/title-bar.d.ts.map +1 -0
  77. package/dist/title-bar.js +24 -0
  78. package/dist/title-bar.js.map +1 -0
  79. package/dist/types.d.ts +48 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/types.js +2 -0
  82. package/dist/types.js.map +1 -0
  83. package/package.json +55 -3
  84. package/styles.css +4 -0
package/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright 2026 the orzui team
4
+ Copyright 2026 the todou team
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md CHANGED
@@ -1,3 +1,411 @@
1
- # Temporary Holding Version
1
+ # @orz/ui-content
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A registry where content packages contribute renderers and from which any surface asks for content
4
+ to be drawn. It is what lets markdown, code, diagrams and an attachment viewer render one another
5
+ without importing one another: each of them peers on this package, and the app decides at runtime
6
+ which renderers exist.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pnpm add @orz/ui-content react
12
+ ```
13
+
14
+ The root entry has no runtime dependencies and needs no CSS. Its fallbacks are a plain `<pre>` and
15
+ a plain link, styled by whatever typography surrounds them. Attachment viewing, under
16
+ `@orz/ui-content/attachment`, needs more; see [Attachments](#attachments).
17
+
18
+ ## The contract
19
+
20
+ ```ts
21
+ type ContentSource =
22
+ | { kind: "inline"; text: string; lang?: string }
23
+ | {
24
+ kind: "file";
25
+ name: string;
26
+ mime?: string;
27
+ size?: number;
28
+ url?: string; // where the bytes live: an <img>, an <iframe>, the download link
29
+ load?: (signal: AbortSignal) => Promise<string>; // the text, for renderers that need it
30
+ deleted?: boolean; // links and embeds draw it struck through, as deleted
31
+ };
32
+
33
+ type Placement = "document" | "viewer"; // inside a flow of content, or owning the surface
34
+
35
+ type ContentRenderer = {
36
+ name: string;
37
+ accepts(source: ContentSource, placement: Placement): boolean;
38
+ display?: "block" | "inline"; // default "block"
39
+ Component: ComponentType<ContentProps & { Next: ComponentType<ContentProps> }>;
40
+ };
41
+ ```
42
+
43
+ `accepts` must be synchronous and pure, decided from the descriptor alone. Callers make layout
44
+ decisions from it during render (a paragraph holding a block embed becomes a `<div>`), and those
45
+ must not flip when data arrives later.
46
+
47
+ ## Registering and drawing
48
+
49
+ ```tsx
50
+ import { Content, ContentRenderers, type ContentRenderer } from "@orz/ui-content";
51
+
52
+ const shout: ContentRenderer = {
53
+ name: "shout",
54
+ accepts: (source) => source.kind === "inline" && source.lang === "shout",
55
+ Component: ({ source, className }) => (
56
+ <strong className={className}>
57
+ {source.kind === "inline" && source.text.toUpperCase()}
58
+ </strong>
59
+ ),
60
+ };
61
+
62
+ const framed: ContentRenderer = {
63
+ name: "framed",
64
+ accepts: (source) => source.kind === "file",
65
+ Component: ({ Next, ...props }) => (
66
+ <figure>
67
+ <Next {...props} />
68
+ <figcaption>{props.source.kind === "file" && props.source.name}</figcaption>
69
+ </figure>
70
+ ),
71
+ };
72
+
73
+ const renderers = [shout, framed];
74
+
75
+ <ContentRenderers renderers={renderers}>
76
+ <Content source={{ kind: "inline", text: "hi", lang: "shout" }} placement="document" />
77
+ </ContentRenderers>;
78
+ ```
79
+
80
+ - **Order.** The list is ordered and the first renderer that accepts a source draws it, so a narrow
81
+ renderer goes before a broad one (markdown files before any text file). Renderers do not exclude
82
+ one another's kinds; the app's order decides.
83
+ - **`Next`.** A renderer's `Next` draws the same source with the renderers after it, which is how a
84
+ frame wraps whatever would have drawn the file anyway. Past the end of the list comes the
85
+ fallback: `<pre><code>` for inline text, and a download link (or the bare name, when there is no
86
+ `url`) for a file. `ContentFallback` is the same fallback as a component, for a renderer that
87
+ decides to step aside.
88
+ - **Nesting.** A nested `<ContentRenderers>` puts its renderers in front of the outer ones, so a
89
+ surface can override one kind locally.
90
+ - **Stable lists.** Keep `renderers` a module constant or a memo. Each position's `Next` is built
91
+ once per list, so a renderer's subtree keeps its identity across re-renders. A new array rebuilds
92
+ them, and React remounts whatever the renderers drew, collapsing any live text selection inside.
93
+ - **`className`** goes on the renderer's root element. A decoration pass that marked the element a
94
+ source was written as (an embedded `<img>`) needs that class to survive the swap.
95
+
96
+ `useContentDisplay(source, placement)` answers `"block"`, `"inline"` or `null` (nothing registered
97
+ accepts it) without rendering anything. `useContentDisplayOf()` returns the same question as a
98
+ function, for a caller asking about several sources in one render.
99
+
100
+ ## Depth
101
+
102
+ A file source drawn by a renderer raises `useContentDepth()` by one for everything below it, once
103
+ per `<Content>`. A document that embeds documents reads its depth to stop recursing: past its limit
104
+ it draws `ContentFallback`, a link, instead of the document. Inline sources never raise it, so a
105
+ code fence inside an embedded document still renders.
106
+
107
+ `<ContentRenderers maxDepth>` stores a limit for renderers to read with `useContentMaxDepth()`.
108
+ `<Content>` does not enforce it; each renderer that recurses decides what its limit means.
109
+
110
+ ## Title bar
111
+
112
+ A surface with a title bar of its own (a card, a dialog) can lend a spot in it to the renderer
113
+ drawing its file, for controls that belong next to the surface's own:
114
+
115
+ ```tsx
116
+ import { ContentTitleBar, useContentTitleBar } from "@orz/ui-content";
117
+
118
+ // The surface: a spot in its bar, and the slot around its body.
119
+ const [bar, setBar] = useState<HTMLElement | null>(null);
120
+ <span ref={setBar} className="contents" />
121
+ <ContentTitleBar source={file} target={bar}>
122
+ <Content source={file} placement="viewer" />
123
+ </ContentTitleBar>
124
+
125
+ // A renderer: portal the controls there, or draw a bar of its own.
126
+ const bar = useContentTitleBar(source);
127
+ if (bar === undefined) return <FramedWithOwnBar />;
128
+ return <>{bar !== null && createPortal(controls, bar)}{body}</>;
129
+ ```
130
+
131
+ `useContentTitleBar` returns the element, `null` while the bar is still mounting, or `undefined`
132
+ when no surface lent one for this source. The slot is keyed by the source object, not by nesting:
133
+ only the renderer drawing that exact source sees it, so a fence or an embed inside the file, being
134
+ a source of its own, never takes over the surface's bar. `attachmentCard` lends its card's bar.
135
+
136
+ ## Loading text
137
+
138
+ ```tsx
139
+ import { ContentFallback, useSourceText, type ContentProps } from "@orz/ui-content";
140
+
141
+ function TextFile(props: ContentProps) {
142
+ const text = useSourceText(props.source);
143
+ if (text.status === "loading") return <ContentFallback {...props} />;
144
+ if (text.status === "error") {
145
+ return (
146
+ <p>
147
+ {text.error.message} <button onClick={text.retry}>Retry</button>
148
+ </p>
149
+ );
150
+ }
151
+ return <pre>{text.text}</pre>;
152
+ }
153
+ ```
154
+
155
+ Inline text is ready at once. A file calls `load` when it has one and fetches `url` otherwise, and
156
+ the load is aborted on unmount and when the source changes; an aborted load is not reported as an
157
+ error. `retry()` runs the load again.
158
+
159
+ A file counts as changed when its `name` or `url` does, or, for a file with no `url`, when its `load`
160
+ function does. Keep such a `load` stable. A `load` beside a `url` is read from the latest render, so
161
+ a resolver building a fresh closure each render does not restart the load.
162
+
163
+ Size caps are the caller's: check `size` before asking for the text.
164
+
165
+ ## File kinds
166
+
167
+ ```ts
168
+ import { isImageFile, isTextFile, isMarkdownFile, formatSize } from "@orz/ui-content";
169
+
170
+ isImageFile(f) isTextFile(f) isMarkdownFile(f) isHtmlFile(f)
171
+ isPdfFile(f) isArchiveFile(f) isAudioFile(f) isVideoFile(f)
172
+ isPatchFile(f) // text/x-patch, text/x-diff, or .patch / .diff under a generic type
173
+ isTextEmbed(f) // a text file that is not also an image (.svg is both)
174
+ formatSize(bytes) // "512 B", "1.5 KB", "20 KB"
175
+ TEXT_PREVIEW_MAX_BYTES // 1 MiB: text past this previews only when asked, and plain
176
+ TEXT_PREVIEW_HARD_MAX_BYTES // 10 MiB: text past this never previews
177
+ textPreviewGate({ name, mime?, size }, { max?, hardMax? }) // "auto" | "ask" | "refuse"
178
+ ```
179
+
180
+ `textPreviewGate` is the rule the viewer and `attachmentCard` apply: a text file up to `max`
181
+ previews on its own, one up to `hardMax` waits for "Preview anyway", and a larger one never
182
+ previews. A patch skips the asking, since its view is drawn per file.
183
+
184
+ Each predicate takes `{ name, mime? }`, so a file `ContentSource` passes straight in. A declared
185
+ type outranks the filename: the extension only counts when `mime` is missing, empty or
186
+ `application/octet-stream`. A `.png` declared `text/plain` is text, and a `.ts` uploaded as
187
+ octet-stream is TypeScript, never an MPEG transport stream. `mime` is read without its parameters
188
+ or case, so `Text/HTML; charset=utf-8` is `text/html`.
189
+
190
+ Renderers use these in `accepts` rather than matching extensions of their own, so that every
191
+ package draws the same file as the same kind.
192
+
193
+ ## Attachments
194
+
195
+ `@orz/ui-content/attachment` draws files: renderers for images, HTML, PDF, video and audio, a frame
196
+ for text files embedded in a document, one viewer dialog, and the links and lists that open it. It
197
+ peers on `@orz/ui`, `lucide-react` and `react-dom` as well:
198
+
199
+ ```bash
200
+ pnpm add @orz/ui-content @orz/ui lucide-react react react-dom
201
+ ```
202
+
203
+ ```css
204
+ @import "tailwindcss";
205
+ @import "@orz/ui/styles.css";
206
+ @import "@orz/ui-content/styles.css"; /* after @orz/ui's; only when ./attachment is used */
207
+ ```
208
+
209
+ The stylesheet registers the package's shipped JS with Tailwind, so the classes the attachment
210
+ components use are generated.
211
+
212
+ ### Registering the renderers
213
+
214
+ ```tsx
215
+ import { ContentRenderers } from "@orz/ui-content";
216
+ import {
217
+ AttachmentViewerProvider,
218
+ attachmentCard,
219
+ audioContent,
220
+ htmlContent,
221
+ imageContent,
222
+ pdfContent,
223
+ videoContent,
224
+ } from "@orz/ui-content/attachment";
225
+
226
+ const RENDERERS = [
227
+ attachmentCard(),
228
+ imageContent(),
229
+ htmlContent(),
230
+ pdfContent(),
231
+ videoContent(),
232
+ audioContent(),
233
+ mermaidContent,
234
+ markdownContent(),
235
+ patchContent,
236
+ codeContent,
237
+ ];
238
+
239
+ <ContentRenderers renderers={RENDERERS}>
240
+ <AttachmentViewerProvider viewHref={viewHref} group={group}>
241
+ <App />
242
+ </AttachmentViewerProvider>
243
+ </ContentRenderers>;
244
+ ```
245
+
246
+ The order matters, because the first renderer that accepts a source draws it:
247
+
248
+ - **`attachmentCard` first**, so a text file embedded in a document is framed before anything draws
249
+ it. It hands the file on through `Next`, and the renderers after it draw the frame's body.
250
+ - **Images before text**, because an `.svg` is both, and it should be drawn as an image.
251
+ - **Markdown and diagrams before a renderer that takes every text file**, or that renderer would
252
+ draw them as source.
253
+
254
+ | Renderer | Accepts | Placement | Draws |
255
+ |---|---|---|---|
256
+ | `attachmentCard({ maxBytes?, maxDepth?, collapsedClassName? })` | text files that are not images | `document` | `DocumentCard` around `Next` |
257
+ | `imageContent()` | images with a `url` | both | `<img>`; in a document, a click opens the viewer; in the viewer, a zoomable canvas |
258
+ | `htmlContent({ src? })` | HTML with a `url` | `viewer` | `<iframe sandbox="allow-scripts">` |
259
+ | `pdfContent({ src? })` | PDF with a `url` | `viewer` | `<iframe>`, unsandboxed |
260
+ | `videoContent()` | video with a `url` | both | `VideoPlayer`; in a document, under a header with the name and Download |
261
+ | `audioContent()` | audio with a `url` | both | `AudioPlayer`; in a document, under a header with the file's icon, name and Download |
262
+
263
+ A file with no `url` is never accepted by the binary renderers, and falls through to the
264
+ registry's fallback. In a document an HTML file is text, so `attachmentCard` frames its source; a
265
+ PDF embed is the fallback's download link.
266
+
267
+ **Never add `allow-same-origin` to the HTML frame.** A file served from the app's own origin could
268
+ then run scripts that call the app's API with the reader's cookies. `allow-scripts` alone keeps the
269
+ document on an opaque origin. The PDF frame carries no sandbox because Chromium's PDF viewer refuses
270
+ to run in a sandboxed frame, so serve PDFs from a URL that cannot be mistaken for the app.
271
+
272
+ A video or audio file the browser cannot play (an `.avi`, a `.wma`) falls through to the renderers
273
+ after `videoContent`/`audioContent`, usually the download link, with "This browser can't play this
274
+ file." under it.
275
+
276
+ ### Video and audio players
277
+
278
+ `VideoPlayer` and `AudioPlayer` are the players the renderers draw, for use on their own: a native
279
+ `<video>`/`<audio>` without native controls, an optional `header` above, and `MediaControls` from
280
+ `@orz/ui` underneath.
281
+
282
+ ```tsx
283
+ import { VideoPlayer } from "@orz/ui-content/attachment";
284
+
285
+ <VideoPlayer
286
+ src={url}
287
+ title="clip.webm"
288
+ header={<MyHeader />}
289
+ onError={() => setUnplayable(true)}
290
+ />;
291
+ ```
292
+
293
+ - Props: `src`, `title` (the accessible name and the frame's `title`), `header`, `autoPlay`,
294
+ `loop`, `speed` (the initial speed) and `className`. The video preloads metadata only.
295
+ - `onError` is called when the element cannot load or decode `src`, and the player then draws
296
+ nothing; without it, the player says "This browser can't play this file." in place of the
297
+ picture (for audio, the bar).
298
+ - A video adds picture-in-picture and fullscreen where the document offers them. Fullscreen takes
299
+ the whole frame, so the bar stays.
300
+ - Keys, anywhere in the frame unless the focused control uses the key itself (a slider its arrows,
301
+ a button Space): Space or `k` plays and pauses, ← → seek 5 s, `0`–`9` jump to tenths of the
302
+ duration, `m` mutes, `f` toggles fullscreen. A handled key is `preventDefault()`ed, so inside the
303
+ viewer an arrow key on the focused picture seeks rather than paging.
304
+
305
+ `attachmentCard`:
306
+
307
+ - past the depth limit (`maxDepth`, else the provider's `maxDepth`, else 1, counted as
308
+ `markdownContent` counts it) it draws an `AttachmentLink` chip instead of a frame, so a document
309
+ inside a card shows its own documents as links;
310
+ - over `maxBytes` (default `TEXT_PREVIEW_MAX_BYTES`, 1 MiB) it draws the frame with "Large files
311
+ aren't previewed automatically." and a Preview anyway button, then the body once asked (a patch
312
+ is not asked about); over `hardMaxBytes` (default `TEXT_PREVIEW_HARD_MAX_BYTES`, 10 MiB) it draws
313
+ the frame with a note and no body. A file of unknown size is not capped here; the renderer `Next`
314
+ reaches decides once the text is loaded (`codeContent` draws text over 1 MiB without
315
+ highlighting);
316
+ - otherwise it draws the frame with the size, a download button and, inside a provider, an expand
317
+ button that opens the viewer. Loading and load errors are drawn by the renderer `Next` reaches,
318
+ which may also put controls in the frame's title bar (`useContentTitleBar`).
319
+
320
+ ### The viewer
321
+
322
+ ```ts
323
+ <AttachmentViewerProvider
324
+ viewHref={(file) => string | undefined} // where a browser tab shows the file
325
+ group={(file) => ({ items, index }) | null} // the set paging covers when one file is opened
326
+ textPreviewMaxBytes={1024 * 1024} // text above this waits for "Preview anyway"
327
+ textPreviewHardMaxBytes={10 * 1024 * 1024} // text above this never previews
328
+ imageLook={{ background, smoothing }} // controlled image look; default: held in memory
329
+ onImageLookChange={(look) => void}
330
+ >
331
+
332
+ useAttachmentViewer(): { open(items, index), show(file) } | null // null outside a provider
333
+ useCanPreview(file): boolean // whether a plain click should open the viewer
334
+ useViewHref(file): string // viewHref(file) ?? file.url ?? ""
335
+ useStoredImageLook(storageKey): [ImageLook, (look: ImageLook) => void]
336
+ ```
337
+
338
+ One provider owns one dialog, and everything below it opens that one. A nested provider shadows
339
+ the outer one, so a surface can page through its own set.
340
+
341
+ - **`viewHref`** is for an app that serves a separate inline-view URL beside the download URL.
342
+ Anchors, the HTML and PDF frames and "Open in new tab" use it; Download always uses `url`. When it
343
+ returns undefined, "Open in new tab" is left out.
344
+ - **`group`** lets a click on one file page through the set it belongs to, positioned on it. A file
345
+ it returns null for opens alone.
346
+ - **`useCanPreview`** is true when some registered renderer accepts the file in `"viewer"`
347
+ placement and, for a text file, its size is known. A text file over `textPreviewMaxBytes` opens
348
+ on "Large files aren't previewed automatically." with Preview anyway, which holds until the
349
+ viewer pages away; one over `textPreviewHardMaxBytes` opens on "This file is too large to
350
+ preview.", with the footer's Download. A patch is not asked about. HTML is not capped: the
351
+ browser streams it into the frame rather than this tab drawing it as text. HTML that no frame
352
+ takes (no `htmlContent`, or no `url`) reaches the text renderers, which apply their own cap. `attachmentCard` draws
353
+ its expand button only when this holds.
354
+
355
+ The dialog shows the name, ‹ n / m › for a set (← and → page, wrapping), a maximize toggle that
356
+ fills the window and stays a modal, and the close button, all in one row. Its body is `<Content placement="viewer">`, or a download pane
357
+ for a file nothing can draw. The body sets `--attachment-pane-height` (`75vh`, or the remaining
358
+ height when maximized); a renderer in `"viewer"` placement sizes to it, as in
359
+ `max-h-(--attachment-pane-height)`. The title bar lends the renderer drawing the file a slot
360
+ between the name and the pager, through `ContentTitleBar`, for controls of its own.
361
+ `AttachmentViewer` is the same dialog, controlled, for an app that holds `{ items, index } | null`
362
+ itself, and takes the same `imageLook` props.
363
+
364
+ An image in the viewer sits on a canvas. The wheel zooms about the pointer, as do a trackpad pinch
365
+ (ctrl+wheel, or Safari's gesture events) and a two-finger pinch on a phone; dragging pans once
366
+ zoomed; a click toggles between fit and actual pixels (or twice the fit, if that is deeper) at the
367
+ pointer. Zoom runs from fit, or 100% if that is smaller, to 8× natural pixels, and panning never
368
+ pulls an image edge inside the canvas. Restored, fit never enlarges an image; maximized, it fills
369
+ the window. Paging starts every image at fit; maximize keeps the zoom and the point at the centre.
370
+ Restored, the canvas asks for the image's height at its zoom, so zooming in grows the dialog up to
371
+ the pane's height before it pans, and the height tweens as it changes (not under
372
+ `prefers-reduced-motion`). While the mouse is over the zoom controls or ‹ ›, the canvas keeps the
373
+ height it had when the pointer arrived, so the controls do not slide out from under it; it lets go
374
+ 400 ms after the pointer leaves, or at once on a key, a press or a wheel.
375
+
376
+ In the title bar, − and + step the zoom (hidden on a phone, where pinch does it), and the readout
377
+ opens a menu: Fit (key `0`), 100% (key `1`), a black or checkerboard background, and smooth or
378
+ pixelated scaling. `+` `-` `0` `1` work from the keyboard; with Ctrl/⌘ they stay the browser's page
379
+ zoom. Black fills the canvas; the checkerboard sits under the image only, so an opaque image looks
380
+ the same and a transparent one shows where it ends. Pixelated applies only above 100%. An image that
381
+ fails to load is a plain `<img>`, with no controls.
382
+
383
+ The background and scaling (`ImageLook`, default checkerboard and smooth) are the viewer's, not
384
+ the image's, so paging and reopening keep them. Without `imageLook` the viewer holds them in memory
385
+ until the page reloads. `useStoredImageLook(storageKey)` keeps them in localStorage and follows
386
+ other tabs; hand its pair to `imageLook` and `onImageLookChange`, and name the key after the app.
387
+
388
+ ### Links, lists, cards and icons
389
+
390
+ - **`<AttachmentLink file boxed? className?>`** is an inline link wearing the file's type icon. It is
391
+ a real anchor to `useViewHref(file)`, so copy-link and middle-click work; a plain left click on a
392
+ file `useCanPreview` accepts opens the viewer. `boxed` draws it as a bordered chip. It is a
393
+ `LinkChip` from `@orz/ui/components/link-chip`, which also exports the chip's classes. A file
394
+ marked `deleted` is its name struck through, with no link.
395
+ - **`<AttachmentList items actions? title? id? className?>`** lists files (`FileSource` plus a
396
+ `key` and an optional ISO `createdAt`) with their icon, size, time and a download button.
397
+ `actions(item)` draws the app's own controls at the end of a row, such as a delete button. A list
398
+ of eight or more keeps its newest five rows and folds the rest behind a toggle. A plain click on a
399
+ row opens the viewer on the whole list. An empty list renders nothing.
400
+ - **`<DeletedAttachmentMark name className?>`** is what a document shows for a link or embed of a
401
+ deleted file: the name struck through, with the word "deleted". `@orz/ui-markdown`'s
402
+ `attachmentLinks` and `markdownEmbeds` draw it for a file their `resolve` marks `deleted`.
403
+ - **`<DocumentCard name icon? meta? headerActions? titleBarFor? downloadUrl? onExpand? collapsedClassName?>`**
404
+ is the frame `attachmentCard` draws, for anything else that wants it: a header with the name, a
405
+ muted note, extra actions, download and expand, around a height-capped body that offers "Show full
406
+ document" when it overflows. Without `onExpand` it draws no expand controls. With `titleBarFor`,
407
+ it lends a spot in the header, after the name, to the renderer drawing that source.
408
+ - **`fileIcon(file)`** is the lucide icon for a file's kind: image, HTML, patch, text, archive,
409
+ audio, video, else a generic file.
410
+
411
+ `@orz/ui-markdown`'s `attachmentLinks` plugin draws markdown links to files as `AttachmentLink`s.
@@ -0,0 +1,26 @@
1
+ import type { ContentRenderer } from "../types.ts";
2
+ export type AttachmentCardOptions = {
3
+ /**
4
+ * Text files above this wait for "Preview anyway", and the renderer then draws them plain.
5
+ * Default 1 MiB.
6
+ */
7
+ maxBytes?: number;
8
+ /** Text files above this draw a note instead of their contents. Default 10 MiB. */
9
+ hardMaxBytes?: number;
10
+ /**
11
+ * How many embedded files deep a card is still drawn; past it, the file is a link chip.
12
+ * Defaults to the provider's `maxDepth`, else 1, as `markdownContent` counts it, so the two
13
+ * agree on which level turns into a link.
14
+ */
15
+ maxDepth?: number;
16
+ /** Height cap for the card's collapsed body. Default `max-h-80`. */
17
+ collapsedClassName?: string;
18
+ };
19
+ /**
20
+ * Frames a text file embedded in a document: a `DocumentCard` whose body is drawn by the
21
+ * renderers after this one. Register it first, so a text embed is framed before anything draws it.
22
+ *
23
+ * Returns a new renderer on every call, so call it once, where the renderer list is built.
24
+ */
25
+ export declare function attachmentCard(options?: AttachmentCardOptions): ContentRenderer;
26
+ //# sourceMappingURL=card-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card-content.d.ts","sourceRoot":"","sources":["../../src/attachment/card-content.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,aAAa,CAAC;AASjE,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,OAAO,GAAE,qBAA0B,GAClC,eAAe,CAgFjB"}
@@ -0,0 +1,49 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { formatSize, isTextEmbed, textPreviewGate } from "../file-kind.js";
4
+ import { useContentDepth, useContentMaxDepth } from "../registry.js";
5
+ import { DocumentCard } from "./document-card.js";
6
+ import { AttachmentLink } from "./link.js";
7
+ import { useAttachmentViewer, useCanPreview, } from "./viewer.js";
8
+ /**
9
+ * Frames a text file embedded in a document: a `DocumentCard` whose body is drawn by the
10
+ * renderers after this one. Register it first, so a text embed is framed before anything draws it.
11
+ *
12
+ * Returns a new renderer on every call, so call it once, where the renderer list is built.
13
+ */
14
+ export function attachmentCard(options = {}) {
15
+ const limits = { max: options.maxBytes, hardMax: options.hardMaxBytes };
16
+ function AttachmentCard({ source, className, Next, }) {
17
+ const depth = useContentDepth();
18
+ const providerMaxDepth = useContentMaxDepth();
19
+ const viewer = useAttachmentViewer();
20
+ const canPreview = useCanPreview(source);
21
+ const [asked, setAsked] = useState(false);
22
+ if (source.kind !== "file")
23
+ return null;
24
+ const maxDepth = options.maxDepth ?? providerMaxDepth ?? 1;
25
+ // A document inside a document card shows its own documents as links,
26
+ // which is what stops a document that embeds itself.
27
+ if (depth > maxDepth) {
28
+ return _jsx(AttachmentLink, { file: source, className: className });
29
+ }
30
+ const meta = source.size === undefined ? undefined : formatSize(source.size);
31
+ const gate = source.size === undefined
32
+ ? "auto"
33
+ : textPreviewGate({ ...source, size: source.size }, limits);
34
+ if (gate === "refuse" || (gate === "ask" && !asked)) {
35
+ return (_jsx(DocumentCard, { name: source.name, meta: meta, downloadUrl: source.url, className: className, collapsedClassName: options.collapsedClassName, children: _jsx("p", { className: "text-sm text-muted-foreground", children: gate === "refuse" ? ("This file is too large to preview.") : (_jsxs(_Fragment, { children: ["Large files aren't previewed automatically.", " ", _jsx("button", { type: "button", onClick: () => setAsked(true), className: "cursor-pointer font-medium text-foreground hover:underline", children: "Preview anyway" })] })) }) }));
36
+ }
37
+ return (_jsx(DocumentCard, { name: source.name, meta: meta, downloadUrl: source.url, titleBarFor: source,
38
+ // Only when the viewer would draw it: a text file of unknown size
39
+ // would open on the download pane.
40
+ onExpand: viewer !== null && canPreview ? () => viewer.show(source) : undefined, className: className, collapsedClassName: options.collapsedClassName, children: _jsx(Next, { source: source, placement: "document" }) }));
41
+ }
42
+ return {
43
+ name: "attachment-card",
44
+ accepts: (source, placement) => source.kind === "file" && placement === "document" && isTextEmbed(source),
45
+ display: "block",
46
+ Component: AttachmentCard,
47
+ };
48
+ }
49
+ //# sourceMappingURL=card-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card-content.js","sourceRoot":"","sources":["../../src/attachment/card-content.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Where a document links or embeds a file that was deleted: the name it was linked under, struck
3
+ * through, with the word a broken image would otherwise leave the reader to guess.
4
+ */
5
+ export declare function DeletedAttachmentMark({ name, className, }: {
6
+ name: string;
7
+ className?: string;
8
+ }): import("react").JSX.Element;
9
+ //# sourceMappingURL=deleted.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deleted.d.ts","sourceRoot":"","sources":["../../src/attachment/deleted.tsx"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,IAAI,EACJ,SAAS,GACV,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,+BAcA"}
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "@orz/ui/lib/utils";
3
+ import { FileXIcon } from "lucide-react";
4
+ /**
5
+ * Where a document links or embeds a file that was deleted: the name it was linked under, struck
6
+ * through, with the word a broken image would otherwise leave the reader to guess.
7
+ */
8
+ export function DeletedAttachmentMark({ name, className, }) {
9
+ return (_jsxs("span", { className: cn("inline-flex items-center gap-1 rounded border border-dashed px-1.5 text-sm text-muted-foreground", className), "data-attachment-deleted": "", children: [_jsx(FileXIcon, { "aria-hidden": true, className: "size-3.5 shrink-0" }), _jsx("span", { className: "line-through", children: name }), _jsx("span", { children: "deleted" })] }));
10
+ }
11
+ //# sourceMappingURL=deleted.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deleted.js","sourceRoot":"","sources":["../../src/attachment/deleted.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,mBAAmB,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,EACpC,IAAI,EACJ,SAAS,GAIV;IACC,OAAO,CACL,gBACE,SAAS,EAAE,EAAE,CACX,kGAAkG,EAClG,SAAS,CACV,6BACuB,EAAE,aAE1B,KAAC,SAAS,yBAAa,SAAS,EAAC,mBAAmB,GAAG,EACvD,eAAM,SAAS,EAAC,cAAc,YAAE,IAAI,GAAQ,EAC5C,qCAAoB,IACf,CACR,CAAC;AACJ,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { type ComponentType, type ReactNode } from "react";
2
+ import type { ContentSource } from "../types.ts";
3
+ export type DocumentCardProps = {
4
+ name: string;
5
+ /** Default: `fileIcon({ name })`. */
6
+ icon?: ComponentType<{
7
+ className?: string;
8
+ }>;
9
+ /** Short muted note next to the name (e.g. a size). */
10
+ meta?: ReactNode;
11
+ /** Extra header controls, rendered before download/expand. */
12
+ headerActions?: ReactNode;
13
+ /**
14
+ * The source the body draws, when its renderer may put controls in the header
15
+ * (`useContentTitleBar`). They sit after the name, before `headerActions`.
16
+ */
17
+ titleBarFor?: ContentSource;
18
+ downloadUrl?: string;
19
+ /** Opens the full document; without it the card draws no expand controls. */
20
+ onExpand?: () => void;
21
+ /** Height cap for the collapsed body. */
22
+ collapsedClassName?: string;
23
+ className?: string;
24
+ children: ReactNode;
25
+ };
26
+ /**
27
+ * A bordered card with a filename header and a height-capped body. Generic on
28
+ * purpose: an attachment embed puts the drawn file in it, and an app can put
29
+ * anything else there, with version chips or review state in `headerActions`.
30
+ */
31
+ export declare function DocumentCard({ name, icon, meta, headerActions, titleBarFor, downloadUrl, onExpand, collapsedClassName, className, children, }: DocumentCardProps): import("react").JSX.Element;
32
+ //# sourceMappingURL=document-card.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-card.d.ts","sourceRoot":"","sources":["../../src/attachment/document-card.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,SAAS,EAIf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,IAAI,CAAC,EAAE,aAAa,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7C,uDAAuD;IACvD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B;;;OAGG;IACH,WAAW,CAAC,EAAE,aAAa,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,yCAAyC;IACzC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAC3B,IAAI,EACJ,IAAI,EACJ,IAAI,EACJ,aAAa,EACb,WAAW,EACX,WAAW,EACX,QAAQ,EACR,kBAA+B,EAC/B,SAAS,EACT,QAAQ,GACT,EAAE,iBAAiB,+BA4GnB"}
@@ -0,0 +1,38 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "@orz/ui/lib/utils";
3
+ import { DownloadIcon, Maximize2Icon } from "lucide-react";
4
+ import { useEffect, useRef, useState, } from "react";
5
+ import { ContentTitleBar } from "../title-bar.js";
6
+ import { fileIcon } from "./icon.js";
7
+ /**
8
+ * A bordered card with a filename header and a height-capped body. Generic on
9
+ * purpose: an attachment embed puts the drawn file in it, and an app can put
10
+ * anything else there, with version chips or review state in `headerActions`.
11
+ */
12
+ export function DocumentCard({ name, icon, meta, headerActions, titleBarFor, downloadUrl, onExpand, collapsedClassName = "max-h-80", className, children, }) {
13
+ const outerRef = useRef(null);
14
+ const innerRef = useRef(null);
15
+ const [overflowing, setOverflowing] = useState(false);
16
+ const [titleBar, setTitleBar] = useState(null);
17
+ // The cap sits on the outer box, so growth of the inner content (lazy
18
+ // code block upgrade, images loading) never resizes the observed border
19
+ // box — observe the inner one.
20
+ useEffect(() => {
21
+ const outer = outerRef.current;
22
+ const inner = innerRef.current;
23
+ if (outer === null || inner === null)
24
+ return;
25
+ const check = () => setOverflowing(inner.offsetHeight > outer.clientHeight + 1);
26
+ check();
27
+ if (typeof ResizeObserver === "undefined")
28
+ return;
29
+ const observer = new ResizeObserver(check);
30
+ observer.observe(inner);
31
+ return () => observer.disconnect();
32
+ }, []);
33
+ const Icon = icon ?? fileIcon({ name });
34
+ const title = (_jsxs(_Fragment, { children: [_jsx(Icon, { className: "size-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: cn("truncate font-medium", onExpand && "hover:underline"), children: name }), meta !== undefined && (_jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: meta }))] }));
35
+ return (_jsxs("section", { className: cn("my-2 overflow-hidden rounded-lg border", className), children: [_jsxs("div", { className: "flex items-center gap-1 border-b bg-muted/40 py-1 pr-1.5 pl-3 text-sm", children: [onExpand ? (_jsx("button", { type: "button", onClick: onExpand, className: "flex min-w-0 flex-1 cursor-pointer items-center gap-2 text-left", children: title })) : (_jsx("span", { className: "flex min-w-0 flex-1 items-center gap-2 py-1", children: title })), titleBarFor !== undefined && (_jsx("span", { ref: setTitleBar, className: "contents" })), headerActions, downloadUrl !== undefined && (_jsx("a", { href: downloadUrl, download: name, "aria-label": `download ${name}`, className: "rounded-md p-1.5 text-muted-foreground hover:bg-muted hover:text-foreground", children: _jsx(DownloadIcon, { className: "size-3.5" }) })), onExpand && (_jsx("button", { type: "button", onClick: onExpand, "aria-label": `expand ${name}`, className: "cursor-pointer rounded-md p-1.5 text-muted-foreground hover:bg-muted hover:text-foreground", children: _jsx(Maximize2Icon, { className: "size-3.5" }) }))] }), _jsxs("div", { ref: outerRef, className: cn("relative overflow-hidden", collapsedClassName), children: [_jsx("div", { ref: innerRef, className: "p-3", children: titleBarFor === undefined ? (children) : (_jsx(ContentTitleBar, { source: titleBarFor, target: titleBar, children: children })) }), overflowing &&
36
+ (onExpand ? (_jsx("button", { type: "button", onClick: onExpand, className: "absolute inset-x-0 bottom-0 flex h-16 cursor-pointer items-end justify-center bg-gradient-to-t from-background to-transparent pb-1.5 text-xs font-medium text-muted-foreground hover:text-foreground", children: "Show full document" })) : (_jsx("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-background to-transparent" })))] })] }));
37
+ }
38
+ //# sourceMappingURL=document-card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-card.js","sourceRoot":"","sources":["../../src/attachment/document-card.tsx"],"names":[],"mappings":"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"}