@crossworks/share-ui 0.230.69 → 0.231.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/package.json +4 -3
- package/src/draw-presenter.tsx +9 -2
- package/src/event-presenter.tsx +22 -9
- package/src/file-presenter.tsx +41 -8
- package/src/lib/presenter-chrome.ts +37 -0
- package/src/note-presenter.tsx +20 -4
- package/src/table-presenter.tsx +17 -7
- package/src/task-presenter.tsx +56 -34
- package/src/view-payload.ts +10 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crossworks/share-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.231.0",
|
|
4
4
|
"description": "The server-rendered share surface — the /s/<token> presenters, view-payload contract, mini-app sandbox, and the few primitives they need. Lives in MANTLE (the server renders these), published for jackdaw to consume; may depend only on @mantle/{client-types,content-core} (the jackdaw-repo-split boundary).",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./app-presenter": "./src/app-presenter.tsx",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"./ui/button": "./src/ui/button.tsx",
|
|
20
20
|
"./ui/input": "./src/ui/input.tsx",
|
|
21
21
|
"./ui/label": "./src/ui/label.tsx",
|
|
22
|
+
"./lib/presenter-chrome": "./src/lib/presenter-chrome.ts",
|
|
22
23
|
"./lib/utils": "./src/lib/utils.ts",
|
|
23
24
|
"./page-outline": "./src/page-outline.tsx",
|
|
24
25
|
"./nav-items": "./src/nav-items.ts",
|
|
@@ -31,8 +32,8 @@
|
|
|
31
32
|
"./styles/app.css": "./styles/app.css"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"@mantle/client-types": "npm:@crossworks/client-types@0.
|
|
35
|
-
"@mantle/content-core": "npm:@crossworks/content-core@0.
|
|
35
|
+
"@mantle/client-types": "npm:@crossworks/client-types@0.231.0",
|
|
36
|
+
"@mantle/content-core": "npm:@crossworks/content-core@0.231.0",
|
|
36
37
|
"@radix-ui/react-label": "^2.1.12",
|
|
37
38
|
"@radix-ui/react-slot": "^1.3.0",
|
|
38
39
|
"class-variance-authority": "^0.7.1",
|
package/src/draw-presenter.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isEmbedded, type PresenterChrome } from './lib/presenter-chrome';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Public draw render — the committed SVG snapshot, static, no JS.
|
|
3
5
|
*
|
|
@@ -14,14 +16,19 @@
|
|
|
14
16
|
export function DrawPresenter({
|
|
15
17
|
view,
|
|
16
18
|
src,
|
|
19
|
+
chrome,
|
|
17
20
|
}: {
|
|
18
21
|
view: { title: string; hasSvg: boolean };
|
|
19
22
|
/** URL of the snapshot image (`/s/<token>/draw`). */
|
|
20
23
|
src: string;
|
|
24
|
+
chrome?: PresenterChrome;
|
|
21
25
|
}) {
|
|
26
|
+
const embedded = isEmbedded(chrome);
|
|
22
27
|
return (
|
|
23
|
-
<article className=
|
|
24
|
-
|
|
28
|
+
<article className={embedded ? 'w-full px-6 py-6' : 'mx-auto max-w-5xl px-6 py-12 md:py-16'}>
|
|
29
|
+
{!embedded && (
|
|
30
|
+
<h1 className="mb-8 text-3xl font-bold tracking-tight text-balance">{view.title}</h1>
|
|
31
|
+
)}
|
|
25
32
|
{view.hasSvg ? (
|
|
26
33
|
<div className="overflow-hidden rounded-lg border border-border bg-white p-3">
|
|
27
34
|
{/* eslint-disable-next-line @next/next/no-img-element -- a drawing
|
package/src/event-presenter.tsx
CHANGED
|
@@ -3,6 +3,8 @@ import remarkGfm from 'remark-gfm';
|
|
|
3
3
|
import { CalendarDays, Clock, Download, MapPin } from 'lucide-react';
|
|
4
4
|
import { formatDateTime } from '@mantle/client-types/lib/format-datetime';
|
|
5
5
|
import { buildIcsHref } from '@mantle/client-types/lib/event-time';
|
|
6
|
+
import { isEmbedded, type PresenterChrome } from './lib/presenter-chrome';
|
|
7
|
+
import { cn } from './lib/utils';
|
|
6
8
|
|
|
7
9
|
type EventView = {
|
|
8
10
|
title: string;
|
|
@@ -12,18 +14,29 @@ type EventView = {
|
|
|
12
14
|
location: string | null;
|
|
13
15
|
};
|
|
14
16
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Public event render — time, location, and an .ics link.
|
|
19
|
+
*
|
|
20
|
+
* On the standalone page this is a centred card: the card border is what tells
|
|
21
|
+
* a reader where the event begins and ends on an otherwise empty page. Embedded
|
|
22
|
+
* in a pane that already has a header rule and a border of its own, that same
|
|
23
|
+
* card is the "floating box in the middle" — so embedded drops the frame and
|
|
24
|
+
* the title, and lays the detail out on the pane. See `PresenterChrome`.
|
|
25
|
+
*/
|
|
26
|
+
export function EventPresenter({ view, chrome }: { view: EventView; chrome?: PresenterChrome }) {
|
|
17
27
|
const ics = buildIcsHref(view);
|
|
28
|
+
const embedded = isEmbedded(chrome);
|
|
18
29
|
return (
|
|
19
|
-
<div className=
|
|
20
|
-
<div className=
|
|
21
|
-
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
<div className={embedded ? 'w-full px-6 py-6' : 'mx-auto max-w-2xl px-6 py-12 md:py-16'}>
|
|
31
|
+
<div className={cn(!embedded && 'rounded-xl border border-border bg-card p-6')}>
|
|
32
|
+
{!embedded && (
|
|
33
|
+
<div className="flex items-start gap-3">
|
|
34
|
+
<CalendarDays className="mt-1 size-6 shrink-0 text-primary-ink" aria-hidden />
|
|
35
|
+
<h1 className="text-2xl font-bold tracking-tight text-balance">{view.title}</h1>
|
|
36
|
+
</div>
|
|
37
|
+
)}
|
|
25
38
|
|
|
26
|
-
<dl className=
|
|
39
|
+
<dl className={cn('space-y-2 text-sm', !embedded && 'mt-5')}>
|
|
27
40
|
{view.startsAt && (
|
|
28
41
|
<div className="flex items-center gap-2 text-muted-foreground">
|
|
29
42
|
<Clock className="size-4 shrink-0" aria-hidden />
|
package/src/file-presenter.tsx
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { Download, FileText } from 'lucide-react';
|
|
2
2
|
import { formatBytes } from '@mantle/client-types/lib/format-bytes';
|
|
3
|
+
import { isEmbedded, type PresenterChrome } from './lib/presenter-chrome';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Public file render with a media-appropriate viewer: images, PDFs, video, and
|
|
6
7
|
* audio play inline (served from the scoped asset route); everything else gets
|
|
7
|
-
* a
|
|
8
|
+
* a download card.
|
|
9
|
+
*
|
|
10
|
+
* Embedded spans the pane. This is the presenter Jason was describing — a
|
|
11
|
+
* non-previewable file drew a `max-w-md` card, which is a phone-width box
|
|
12
|
+
* marooned in the middle of a 2000px pane. Every branch here gets better with
|
|
13
|
+
* width: a PDF, an image and a video all want the space, and the download row
|
|
14
|
+
* wants to look like a row rather than an island. See `PresenterChrome`.
|
|
8
15
|
*/
|
|
9
16
|
export function FilePresenter({
|
|
10
17
|
view,
|
|
11
18
|
assetUrl,
|
|
19
|
+
chrome,
|
|
12
20
|
}: {
|
|
13
21
|
view: { fileId: string; filename: string; mimeType: string; size: number };
|
|
14
22
|
assetUrl: (fileId: string) => string;
|
|
23
|
+
chrome?: PresenterChrome;
|
|
15
24
|
}) {
|
|
16
25
|
const src = assetUrl(view.fileId);
|
|
17
26
|
const mime = view.mimeType || '';
|
|
@@ -19,12 +28,20 @@ export function FilePresenter({
|
|
|
19
28
|
const isPdf = mime === 'application/pdf';
|
|
20
29
|
const isVideo = mime.startsWith('video/');
|
|
21
30
|
const isAudio = mime.startsWith('audio/');
|
|
31
|
+
const embedded = isEmbedded(chrome);
|
|
22
32
|
|
|
23
33
|
return (
|
|
24
|
-
<div className=
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
<div className={embedded ? 'w-full px-6 py-6' : 'mx-auto max-w-5xl px-6 py-12'}>
|
|
35
|
+
{/* Embedded keeps the type/size line — the pane header carries only the
|
|
36
|
+
filename, so this is the one place a member learns what they are
|
|
37
|
+
looking at — but drops the hero title above it. */}
|
|
38
|
+
<header className={embedded ? 'mb-4' : 'mb-6 text-center'}>
|
|
39
|
+
{!embedded && <h1 className="text-xl font-semibold tracking-tight">{view.filename}</h1>}
|
|
40
|
+
<p
|
|
41
|
+
className={
|
|
42
|
+
embedded ? 'text-xs text-muted-foreground' : 'mt-1 text-xs text-muted-foreground'
|
|
43
|
+
}
|
|
44
|
+
>
|
|
28
45
|
{mime || 'file'} · {formatBytes(view.size)}
|
|
29
46
|
</p>
|
|
30
47
|
</header>
|
|
@@ -43,7 +60,11 @@ export function FilePresenter({
|
|
|
43
60
|
<iframe
|
|
44
61
|
src={src}
|
|
45
62
|
title={view.filename}
|
|
46
|
-
className=
|
|
63
|
+
className={
|
|
64
|
+
embedded
|
|
65
|
+
? 'h-[calc(100dvh-14rem)] min-h-96 w-full rounded-lg border border-border'
|
|
66
|
+
: 'h-[82vh] w-full rounded-lg border border-border'
|
|
67
|
+
}
|
|
47
68
|
/>
|
|
48
69
|
) : isVideo ? (
|
|
49
70
|
<video
|
|
@@ -52,11 +73,23 @@ export function FilePresenter({
|
|
|
52
73
|
className="mx-auto max-h-[80vh] w-full rounded-lg border border-border bg-black"
|
|
53
74
|
/>
|
|
54
75
|
) : isAudio ? (
|
|
55
|
-
<div
|
|
76
|
+
<div
|
|
77
|
+
className={
|
|
78
|
+
embedded
|
|
79
|
+
? 'rounded-lg border border-border bg-card p-4'
|
|
80
|
+
: 'mx-auto max-w-xl rounded-lg border border-border bg-card p-4'
|
|
81
|
+
}
|
|
82
|
+
>
|
|
56
83
|
<audio src={src} controls className="w-full" />
|
|
57
84
|
</div>
|
|
58
85
|
) : (
|
|
59
|
-
<div
|
|
86
|
+
<div
|
|
87
|
+
className={
|
|
88
|
+
embedded
|
|
89
|
+
? 'flex items-center gap-3 rounded-xl border border-border bg-card p-4'
|
|
90
|
+
: 'mx-auto flex max-w-md items-center gap-3 rounded-xl border border-border bg-card p-4'
|
|
91
|
+
}
|
|
92
|
+
>
|
|
60
93
|
<FileText className="size-8 shrink-0 text-muted-foreground" aria-hidden />
|
|
61
94
|
<div className="min-w-0 flex-1">
|
|
62
95
|
<p className="truncate text-sm font-medium">{view.filename}</p>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which shell a presenter is drawing inside.
|
|
3
|
+
*
|
|
4
|
+
* Every presenter here was written for ONE surface: the anonymous public `/s`
|
|
5
|
+
* page, where the presenter *is* the page. A centred column with a hero title
|
|
6
|
+
* and generous top padding is exactly right there — nothing else is on screen.
|
|
7
|
+
*
|
|
8
|
+
* `/team` then reused the same components inside a master-detail pane, and both
|
|
9
|
+
* of those choices became wrong at once. The pane already draws the title in
|
|
10
|
+
* its own header, so the hero title was the second of three on screen; and the
|
|
11
|
+
* centred cap meant dragging the divider only grew the empty margins while the
|
|
12
|
+
* content stayed a fixed narrow column. Members read that as "the drag is
|
|
13
|
+
* broken". The handle was fine — the content was ignoring it.
|
|
14
|
+
*
|
|
15
|
+
* So a presenter now says which shell it is in:
|
|
16
|
+
*
|
|
17
|
+
* - `'share'` (the default) — the standalone page. Unchanged, and it stays the
|
|
18
|
+
* default deliberately: the public page must not change because an embedder
|
|
19
|
+
* forgot a prop.
|
|
20
|
+
* - `'embedded'` — inside a shell that already owns the title and the padding.
|
|
21
|
+
* Drop the hero title, tighten the vertical rhythm, and let the content take
|
|
22
|
+
* the width it is given.
|
|
23
|
+
*
|
|
24
|
+
* ⚠ `'embedded'` is NOT a synonym for full-bleed. It means *the shell owns the
|
|
25
|
+
* chrome*; what to do with the width is still the content's call. A table, a
|
|
26
|
+
* file listing and a media viewer all get better as they get wider, so they
|
|
27
|
+
* span the pane. Prose does not — a 2000px line is unreadable no matter whose
|
|
28
|
+
* pane it is in — so a note keeps its measure and simply stops being centred
|
|
29
|
+
* under a title it no longer draws. Anything that reads as "a floating box in
|
|
30
|
+
* the middle of a big screen" is the bug; a comfortable reading measure is not.
|
|
31
|
+
*/
|
|
32
|
+
export type PresenterChrome = 'share' | 'embedded';
|
|
33
|
+
|
|
34
|
+
/** Reads better at the call sites than comparing the string in each presenter. */
|
|
35
|
+
export function isEmbedded(chrome?: PresenterChrome): boolean {
|
|
36
|
+
return chrome === 'embedded';
|
|
37
|
+
}
|
package/src/note-presenter.tsx
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
import ReactMarkdown from 'react-markdown';
|
|
2
2
|
import remarkGfm from 'remark-gfm';
|
|
3
|
+
import { isEmbedded, type PresenterChrome } from './lib/presenter-chrome';
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Public note render — markdown (GFM) in a prose column.
|
|
7
|
+
*
|
|
8
|
+
* Embedded keeps the measure. A note is prose, and prose wants a line length
|
|
9
|
+
* whoever is reading it; what it drops is the hero title the shell already
|
|
10
|
+
* draws, and the standalone page's tall top padding. See `PresenterChrome`.
|
|
11
|
+
*/
|
|
12
|
+
export function NotePresenter({
|
|
13
|
+
view,
|
|
14
|
+
chrome,
|
|
15
|
+
}: {
|
|
16
|
+
view: { title: string; content: string };
|
|
17
|
+
chrome?: PresenterChrome;
|
|
18
|
+
}) {
|
|
19
|
+
const embedded = isEmbedded(chrome);
|
|
6
20
|
return (
|
|
7
|
-
<article className=
|
|
8
|
-
|
|
21
|
+
<article className={embedded ? 'max-w-3xl px-6 py-6' : 'mx-auto max-w-3xl px-6 py-12 md:py-16'}>
|
|
22
|
+
{!embedded && (
|
|
23
|
+
<h1 className="mb-8 text-3xl font-bold tracking-tight text-balance">{view.title}</h1>
|
|
24
|
+
)}
|
|
9
25
|
<div className="prose dark:prose-invert max-w-none prose-accent prose-document">
|
|
10
26
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{view.content}</ReactMarkdown>
|
|
11
27
|
</div>
|
package/src/table-presenter.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
4
4
|
import { Loader2, Table2 } from 'lucide-react';
|
|
5
5
|
import { Button } from './ui/button';
|
|
6
6
|
import { cn } from './lib/utils';
|
|
7
|
+
import { isEmbedded, type PresenterChrome } from './lib/presenter-chrome';
|
|
7
8
|
|
|
8
9
|
type CellValue = string | number | boolean | string[] | null;
|
|
9
10
|
type PublicRow = { id: string; cells: Record<string, CellValue> };
|
|
@@ -23,6 +24,7 @@ const PAGE = 200;
|
|
|
23
24
|
export function TablePresenter({
|
|
24
25
|
view,
|
|
25
26
|
token,
|
|
27
|
+
chrome,
|
|
26
28
|
}: {
|
|
27
29
|
view: {
|
|
28
30
|
title: string;
|
|
@@ -31,6 +33,7 @@ export function TablePresenter({
|
|
|
31
33
|
legacyDoc: { columns: PublicColumn[]; rows: PublicRow[] } | null;
|
|
32
34
|
};
|
|
33
35
|
token: string;
|
|
36
|
+
chrome?: PresenterChrome;
|
|
34
37
|
}) {
|
|
35
38
|
const tabs = view.tabs ?? [];
|
|
36
39
|
const [tabId, setTabId] = useState<string | null>(tabs[0]?.id ?? null);
|
|
@@ -100,14 +103,21 @@ export function TablePresenter({
|
|
|
100
103
|
[columns],
|
|
101
104
|
);
|
|
102
105
|
|
|
106
|
+
const embedded = isEmbedded(chrome);
|
|
107
|
+
|
|
103
108
|
return (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
// A table is the clearest case for the embedded shell: every extra pixel is
|
|
110
|
+
// another column a member can read without scrolling sideways, and the
|
|
111
|
+
// centred 6xl cap was throwing that width away.
|
|
112
|
+
<div className={embedded ? 'w-full px-6 py-6' : 'mx-auto max-w-6xl px-6 py-12'}>
|
|
113
|
+
<header className={embedded ? 'mb-3' : 'mb-6 text-center'}>
|
|
114
|
+
{!embedded && (
|
|
115
|
+
<h1 className="text-xl font-semibold tracking-tight">
|
|
116
|
+
{view.icon ? `${view.icon} ` : ''}
|
|
117
|
+
{view.title}
|
|
118
|
+
</h1>
|
|
119
|
+
)}
|
|
120
|
+
<p className={cn('text-xs text-muted-foreground', !embedded && 'mt-1')}>
|
|
111
121
|
{totalRows} row{totalRows === 1 ? '' : 's'}
|
|
112
122
|
</p>
|
|
113
123
|
</header>
|
package/src/task-presenter.tsx
CHANGED
|
@@ -2,6 +2,8 @@ import ReactMarkdown from 'react-markdown';
|
|
|
2
2
|
import remarkGfm from 'remark-gfm';
|
|
3
3
|
import { Check, Circle } from 'lucide-react';
|
|
4
4
|
import { formatDateTime } from '@mantle/client-types/lib/format-datetime';
|
|
5
|
+
import { isEmbedded, type PresenterChrome } from './lib/presenter-chrome';
|
|
6
|
+
import { cn } from './lib/utils';
|
|
5
7
|
|
|
6
8
|
/** Human label per lifecycle status (raw value falls through for forward
|
|
7
9
|
* compatibility — never render a snake_case token to a reader). */
|
|
@@ -12,9 +14,19 @@ const STATUS_LABEL: Record<string, string> = {
|
|
|
12
14
|
done: 'Done',
|
|
13
15
|
};
|
|
14
16
|
|
|
15
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Public task render — a status card standalone, laid out on the pane when
|
|
19
|
+
* embedded.
|
|
20
|
+
*
|
|
21
|
+
* The status/priority/due chips survive both shells: they are the task's
|
|
22
|
+
* metadata, not decoration, and the pane header carries only the title. What
|
|
23
|
+
* embedded drops is the card frame, the hero title, and the done-tick beside it
|
|
24
|
+
* (the "Done" chip already says the same thing without a 24px glyph claiming
|
|
25
|
+
* the leftmost column). See `PresenterChrome`.
|
|
26
|
+
*/
|
|
16
27
|
export function TaskPresenter({
|
|
17
28
|
view,
|
|
29
|
+
chrome,
|
|
18
30
|
}: {
|
|
19
31
|
view: {
|
|
20
32
|
title: string;
|
|
@@ -24,48 +36,58 @@ export function TaskPresenter({
|
|
|
24
36
|
dueAt: string | null;
|
|
25
37
|
todos?: { text: string; done: boolean }[];
|
|
26
38
|
};
|
|
39
|
+
chrome?: PresenterChrome;
|
|
27
40
|
}) {
|
|
28
41
|
const done = view.status === 'done';
|
|
29
42
|
const todos = view.todos ?? [];
|
|
43
|
+
const embedded = isEmbedded(chrome);
|
|
44
|
+
const chips = (
|
|
45
|
+
<div className={cn('flex flex-wrap items-center gap-2 text-xs', !embedded && 'mt-3')}>
|
|
46
|
+
<span className="rounded-full border border-border px-2 py-0.5 text-muted-foreground">
|
|
47
|
+
{STATUS_LABEL[view.status] ?? view.status.replace(/_/g, ' ')}
|
|
48
|
+
</span>
|
|
49
|
+
<span className="rounded-full border border-border px-2 py-0.5 capitalize text-muted-foreground">
|
|
50
|
+
{view.priority} priority
|
|
51
|
+
</span>
|
|
52
|
+
{view.dueAt && (
|
|
53
|
+
<span className="rounded-full border border-border px-2 py-0.5 text-muted-foreground">
|
|
54
|
+
Due {formatDateTime(view.dueAt)}
|
|
55
|
+
</span>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
|
|
30
60
|
return (
|
|
31
|
-
<div className=
|
|
32
|
-
<div className=
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
? 'border-primary bg-primary text-primary-foreground'
|
|
39
|
-
: 'border-border text-transparent')
|
|
40
|
-
}
|
|
41
|
-
aria-hidden
|
|
42
|
-
>
|
|
43
|
-
{done ? <Check className="size-4" /> : <Circle className="size-3" />}
|
|
44
|
-
</span>
|
|
45
|
-
<div className="min-w-0 flex-1">
|
|
46
|
-
<h1
|
|
61
|
+
<div className={embedded ? 'w-full px-6 py-6' : 'mx-auto max-w-2xl px-6 py-12 md:py-16'}>
|
|
62
|
+
<div className={cn(!embedded && 'rounded-xl border border-border bg-card p-6')}>
|
|
63
|
+
{embedded ? (
|
|
64
|
+
chips
|
|
65
|
+
) : (
|
|
66
|
+
<div className="flex items-start gap-3">
|
|
67
|
+
<span
|
|
47
68
|
className={
|
|
48
|
-
'
|
|
49
|
-
(done
|
|
69
|
+
'mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full border ' +
|
|
70
|
+
(done
|
|
71
|
+
? 'border-primary bg-primary text-primary-foreground'
|
|
72
|
+
: 'border-border text-transparent')
|
|
50
73
|
}
|
|
74
|
+
aria-hidden
|
|
51
75
|
>
|
|
52
|
-
{
|
|
53
|
-
</
|
|
54
|
-
<div className="
|
|
55
|
-
<
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
</span>
|
|
65
|
-
)}
|
|
76
|
+
{done ? <Check className="size-4" /> : <Circle className="size-3" />}
|
|
77
|
+
</span>
|
|
78
|
+
<div className="min-w-0 flex-1">
|
|
79
|
+
<h1
|
|
80
|
+
className={
|
|
81
|
+
'text-2xl font-bold tracking-tight ' +
|
|
82
|
+
(done ? 'text-muted-foreground line-through' : '')
|
|
83
|
+
}
|
|
84
|
+
>
|
|
85
|
+
{view.title}
|
|
86
|
+
</h1>
|
|
87
|
+
{chips}
|
|
66
88
|
</div>
|
|
67
89
|
</div>
|
|
68
|
-
|
|
90
|
+
)}
|
|
69
91
|
{view.body && (
|
|
70
92
|
<div className="prose prose-sm dark:prose-invert mt-5 max-w-none border-t border-border pt-5">
|
|
71
93
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{view.body}</ReactMarkdown>
|
package/src/view-payload.ts
CHANGED
|
@@ -22,7 +22,16 @@ export type ShareFolderListing = {
|
|
|
22
22
|
/** ltree path currently being listed (the shared root or a descendant). */
|
|
23
23
|
currentPath: string;
|
|
24
24
|
folders: Array<{ id: string; path: string; slug: string; fileCount: number }>;
|
|
25
|
-
files: Array<{
|
|
25
|
+
files: Array<{
|
|
26
|
+
id: string;
|
|
27
|
+
filename: string;
|
|
28
|
+
mimeType: string;
|
|
29
|
+
sizeBytes: number;
|
|
30
|
+
/** ISO instant of the last write, so an embedded listing can carry a
|
|
31
|
+
* Modified column. Optional because a client pinned to an older server
|
|
32
|
+
* still gets a payload without it. */
|
|
33
|
+
updatedAt?: string;
|
|
34
|
+
}>;
|
|
26
35
|
};
|
|
27
36
|
|
|
28
37
|
export type ShareViewPayload =
|