@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.
- package/LICENSE +29 -0
- package/README.md +410 -2
- package/dist/attachment/card-content.d.ts +26 -0
- package/dist/attachment/card-content.d.ts.map +1 -0
- package/dist/attachment/card-content.js +49 -0
- package/dist/attachment/card-content.js.map +1 -0
- package/dist/attachment/deleted.d.ts +9 -0
- package/dist/attachment/deleted.d.ts.map +1 -0
- package/dist/attachment/deleted.js +11 -0
- package/dist/attachment/deleted.js.map +1 -0
- package/dist/attachment/document-card.d.ts +32 -0
- package/dist/attachment/document-card.d.ts.map +1 -0
- package/dist/attachment/document-card.js +38 -0
- package/dist/attachment/document-card.js.map +1 -0
- package/dist/attachment/icon.d.ts +13 -0
- package/dist/attachment/icon.d.ts.map +1 -0
- package/dist/attachment/icon.js +34 -0
- package/dist/attachment/icon.js.map +1 -0
- package/dist/attachment/image-canvas.d.ts +25 -0
- package/dist/attachment/image-canvas.d.ts.map +1 -0
- package/dist/attachment/image-canvas.js +301 -0
- package/dist/attachment/image-canvas.js.map +1 -0
- package/dist/attachment/image-look.d.ts +18 -0
- package/dist/attachment/image-look.d.ts.map +1 -0
- package/dist/attachment/image-look.js +86 -0
- package/dist/attachment/image-look.js.map +1 -0
- package/dist/attachment/image-zoom.d.ts +67 -0
- package/dist/attachment/image-zoom.d.ts.map +1 -0
- package/dist/attachment/image-zoom.js +108 -0
- package/dist/attachment/image-zoom.js.map +1 -0
- package/dist/attachment/index.d.ts +12 -0
- package/dist/attachment/index.d.ts.map +1 -0
- package/dist/attachment/index.js +12 -0
- package/dist/attachment/index.js.map +1 -0
- package/dist/attachment/link.d.ts +18 -0
- package/dist/attachment/link.d.ts.map +1 -0
- package/dist/attachment/link.js +30 -0
- package/dist/attachment/link.js.map +1 -0
- package/dist/attachment/list.d.ts +26 -0
- package/dist/attachment/list.d.ts.map +1 -0
- package/dist/attachment/list.js +53 -0
- package/dist/attachment/list.js.map +1 -0
- package/dist/attachment/media-content.d.ts +21 -0
- package/dist/attachment/media-content.d.ts.map +1 -0
- package/dist/attachment/media-content.js +133 -0
- package/dist/attachment/media-content.js.map +1 -0
- package/dist/attachment/media-player.d.ts +23 -0
- package/dist/attachment/media-player.d.ts.map +1 -0
- package/dist/attachment/media-player.js +137 -0
- package/dist/attachment/media-player.js.map +1 -0
- package/dist/attachment/use-media.d.ts +27 -0
- package/dist/attachment/use-media.d.ts.map +1 -0
- package/dist/attachment/use-media.js +133 -0
- package/dist/attachment/use-media.js.map +1 -0
- package/dist/attachment/viewer.d.ts +85 -0
- package/dist/attachment/viewer.d.ts.map +1 -0
- package/dist/attachment/viewer.js +245 -0
- package/dist/attachment/viewer.js.map +1 -0
- package/dist/file-kind.d.ts +61 -0
- package/dist/file-kind.d.ts.map +1 -0
- package/dist/file-kind.js +166 -0
- package/dist/file-kind.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +44 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +109 -0
- package/dist/registry.js.map +1 -0
- package/dist/source-text.d.ts +29 -0
- package/dist/source-text.d.ts.map +1 -0
- package/dist/source-text.js +101 -0
- package/dist/source-text.js.map +1 -0
- package/dist/title-bar.d.ts +23 -0
- package/dist/title-bar.d.ts.map +1 -0
- package/dist/title-bar.js +24 -0
- package/dist/title-bar.js.map +1 -0
- package/dist/types.d.ts +48 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +55 -3
- 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
|
-
#
|
|
1
|
+
# @orz/ui-content
|
|
2
2
|
|
|
3
|
-
|
|
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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"}
|