@orz/ui-content 0.0.0-stage → 0.1.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
@@ -0,0 +1,13 @@
1
+ import { type FileLike } from "../file-kind.ts";
2
+ /**
3
+ * One table behind every attachment icon on the page — list rows, link chips,
4
+ * document cards — so a file cannot wear two hats in one viewport.
5
+ *
6
+ * The three binary kinds are tested last, after the three that were here
7
+ * first: only files that fall through to `FileIcon` today can move. `.ts` is
8
+ * why that ordering is load-bearing rather than merely cautious — it is a
9
+ * TypeScript source and an MPEG transport stream, and `isTextFile` has to get
10
+ * the first look at it.
11
+ */
12
+ export declare function fileIcon(file: FileLike): import("lucide-react").LucideIcon;
13
+ //# sourceMappingURL=icon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icon.d.ts","sourceRoot":"","sources":["../../src/attachment/icon.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,KAAK,QAAQ,EAQd,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,qCAatC"}
@@ -0,0 +1,34 @@
1
+ import { AppWindowIcon, FileArchiveIcon, FileDiffIcon, FileHeadphoneIcon, FileIcon, FilePlayIcon, FileTextIcon, ImageIcon, } from "lucide-react";
2
+ import { isArchiveFile, isAudioFile, isHtmlFile, isImageFile, isPatchFile, isTextFile, isVideoFile, } from "../file-kind.js";
3
+ /**
4
+ * One table behind every attachment icon on the page — list rows, link chips,
5
+ * document cards — so a file cannot wear two hats in one viewport.
6
+ *
7
+ * The three binary kinds are tested last, after the three that were here
8
+ * first: only files that fall through to `FileIcon` today can move. `.ts` is
9
+ * why that ordering is load-bearing rather than merely cautious — it is a
10
+ * TypeScript source and an MPEG transport stream, and `isTextFile` has to get
11
+ * the first look at it.
12
+ */
13
+ export function fileIcon(file) {
14
+ if (isImageFile(file))
15
+ return ImageIcon;
16
+ if (isHtmlFile(file))
17
+ return AppWindowIcon;
18
+ if (isPatchFile(file))
19
+ return FileDiffIcon;
20
+ if (isTextFile(file))
21
+ return FileTextIcon;
22
+ if (isArchiveFile(file))
23
+ return FileArchiveIcon;
24
+ // `FileHeadphone` and `FilePlay` are lucide's current names for the audio
25
+ // and video file glyphs; `FileAudio` and `FileVideo` still import, as
26
+ // aliases, but render under the new names — which is what the class in the
27
+ // DOM says, and what the tests therefore have to match.
28
+ if (isAudioFile(file))
29
+ return FileHeadphoneIcon;
30
+ if (isVideoFile(file))
31
+ return FilePlayIcon;
32
+ return FileIcon;
33
+ }
34
+ //# sourceMappingURL=icon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icon.js","sourceRoot":"","sources":["../../src/attachment/icon.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAEL,aAAa,EACb,WAAW,EACX,UAAU,EACV,WAAW,EACX,WAAW,EACX,UAAU,EACV,WAAW,GACZ,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAc;IACrC,IAAI,WAAW,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACxC,IAAI,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,aAAa,CAAC;IAC3C,IAAI,WAAW,CAAC,IAAI,CAAC;QAAE,OAAO,YAAY,CAAC;IAC3C,IAAI,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,YAAY,CAAC;IAC1C,IAAI,aAAa,CAAC,IAAI,CAAC;QAAE,OAAO,eAAe,CAAC;IAChD,0EAA0E;IAC1E,sEAAsE;IACtE,2EAA2E;IAC3E,wDAAwD;IACxD,IAAI,WAAW,CAAC,IAAI,CAAC;QAAE,OAAO,iBAAiB,CAAC;IAChD,IAAI,WAAW,CAAC,IAAI,CAAC;QAAE,OAAO,YAAY,CAAC;IAC3C,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { ImageLook } from "./image-look.ts";
2
+ /**
3
+ * The attachment viewer's image: wheel or pinch zooms about the pointer, drag
4
+ * pans, a click toggles fit ↔ actual pixels, and − 100%▾ + sit in the
5
+ * viewer's title bar, the readout opening fit, 100% and the background and
6
+ * scaling choices. Mount it keyed by the image so paging starts every image
7
+ * at fit; `lastHeight` carries the canvas height across that remount, so the
8
+ * next image tweens from it. While `held` is set the canvas keeps that height
9
+ * whatever the zoom or image asks for, and moves to its own once it is
10
+ * cleared.
11
+ */
12
+ export declare function ImageCanvas({ src, alt, maximized, toolbar, look, onLookChange, lastHeight, held, className, }: {
13
+ src: string;
14
+ alt: string;
15
+ maximized: boolean;
16
+ toolbar: HTMLElement | null;
17
+ look: ImageLook;
18
+ onLookChange(look: ImageLook): void;
19
+ lastHeight: {
20
+ current: number | null;
21
+ };
22
+ held: number | null;
23
+ className?: string;
24
+ }): import("react").JSX.Element;
25
+ //# sourceMappingURL=image-canvas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-canvas.d.ts","sourceRoot":"","sources":["../../src/attachment/image-canvas.tsx"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AA+BjD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,EAC1B,GAAG,EACH,GAAG,EACH,SAAS,EACT,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,UAAU,EACV,IAAI,EACJ,SAAS,GACV,EAAE;IACD,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5B,IAAI,EAAE,SAAS,CAAC;IAChB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACpC,UAAU,EAAE;QAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACvC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,+BAyaA"}
@@ -0,0 +1,301 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button } from "@orz/ui/components/button";
3
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, } from "@orz/ui/components/dropdown-menu";
4
+ import { cn } from "@orz/ui/lib/utils";
5
+ import { ChevronDownIcon, MinusIcon, PlusIcon } from "lucide-react";
6
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState, } from "react";
7
+ import { createPortal, flushSync } from "react-dom";
8
+ import { actualAt, canvasHeight, fitScale, fitView, isAtMax, isAtMin, isFitted, panBy, reframe, toggleAt, wheelFactor, zoomAt, } from "./image-zoom.js";
9
+ // In screen pixels, never scaled with the image: at 800% a scaled checker
10
+ // would be squares bigger than the pixels it is meant to show through.
11
+ const CHECKER = "repeating-conic-gradient(#d4d4d4 0 25%, #fff 0 50%) 0 0 / 16px 16px";
12
+ /** Movement under this is still a click/tap, not a drag. */
13
+ const TAP_SLOP = 4;
14
+ const STEP = 1.25;
15
+ /** The restored canvas's height changes. */
16
+ const TWEEN = { duration: 200, easing: "cubic-bezier(0.2, 0, 0, 1)" };
17
+ /**
18
+ * The attachment viewer's image: wheel or pinch zooms about the pointer, drag
19
+ * pans, a click toggles fit ↔ actual pixels, and − 100%▾ + sit in the
20
+ * viewer's title bar, the readout opening fit, 100% and the background and
21
+ * scaling choices. Mount it keyed by the image so paging starts every image
22
+ * at fit; `lastHeight` carries the canvas height across that remount, so the
23
+ * next image tweens from it. While `held` is set the canvas keeps that height
24
+ * whatever the zoom or image asks for, and moves to its own once it is
25
+ * cleared.
26
+ */
27
+ export function ImageCanvas({ src, alt, maximized, toolbar, look, onLookChange, lastHeight, held, className, }) {
28
+ const canvasRef = useRef(null);
29
+ const [natural, setNatural] = useState(null);
30
+ const [frame, setFrame] = useState(null);
31
+ const [view, setView] = useState(null);
32
+ const [dragging, setDragging] = useState(false);
33
+ const [failed, setFailed] = useState(false);
34
+ // Kept until this image is measured, rather than collapsing to min-h-24
35
+ // while it loads and tweening up from there.
36
+ const [startHeight] = useState(() => lastHeight.current);
37
+ // Every handler reads and writes the geometry here, never the rendered
38
+ // state: one touch frame delivers a pointermove per finger before React
39
+ // re-renders, and the second must build on the first.
40
+ const live = useRef({
41
+ frame: null,
42
+ view: null,
43
+ });
44
+ const place = useCallback((f, v) => {
45
+ live.current = { frame: f, view: v };
46
+ setFrame(f);
47
+ setView(v);
48
+ }, []);
49
+ // The height this view asks for is rendered, and the ResizeObserver
50
+ // reframes the view against every step of the canvas getting there.
51
+ const apply = useCallback((next) => {
52
+ if (live.current.frame === null)
53
+ return;
54
+ live.current.view = next;
55
+ setView(next);
56
+ }, []);
57
+ useLayoutEffect(() => {
58
+ const el = canvasRef.current;
59
+ if (el === null || natural === null)
60
+ return;
61
+ const measure = () => {
62
+ const next = {
63
+ width: el.clientWidth,
64
+ height: el.clientHeight,
65
+ naturalWidth: natural.w,
66
+ naturalHeight: natural.h,
67
+ upscale: maximized,
68
+ };
69
+ const { frame: prev, view: v } = live.current;
70
+ lastHeight.current = maximized ? null : next.height;
71
+ place(next, prev === null || v === null ? fitView(next) : reframe(prev, next, v));
72
+ };
73
+ measure();
74
+ if (typeof ResizeObserver === "undefined")
75
+ return;
76
+ // Committed before the frame paints: otherwise each frame of a tween
77
+ // would show the canvas at its new height with the view placed for the
78
+ // old one, and the image would shake.
79
+ const ro = new ResizeObserver(() => flushSync(measure));
80
+ ro.observe(el);
81
+ return () => ro.disconnect();
82
+ }, [natural, maximized, place, lastHeight]);
83
+ useEffect(() => {
84
+ const el = canvasRef.current;
85
+ if (el === null)
86
+ return;
87
+ const local = (clientX, clientY) => {
88
+ const r = el.getBoundingClientRect();
89
+ return { x: clientX - r.left, y: clientY - r.top };
90
+ };
91
+ // React's onWheel is passive, so it cannot stop the page from scrolling
92
+ // or Chrome's ctrl+wheel from zooming the whole page.
93
+ const onWheel = (e) => {
94
+ const { frame: f, view: v } = live.current;
95
+ if (f === null || v === null)
96
+ return;
97
+ e.preventDefault();
98
+ const p = local(e.clientX, e.clientY);
99
+ apply(zoomAt(f, v, wheelFactor(e.deltaY, e.deltaMode, e.ctrlKey), p.x, p.y));
100
+ };
101
+ // Safari reports trackpad pinch as gesture events rather than ctrl+wheel.
102
+ let gestureStart = 1;
103
+ const onGestureStart = (e) => {
104
+ e.preventDefault();
105
+ gestureStart = live.current.view?.z ?? 1;
106
+ };
107
+ const onGestureChange = (e) => {
108
+ const { frame: f, view: v } = live.current;
109
+ if (f === null || v === null)
110
+ return;
111
+ e.preventDefault();
112
+ const g = e;
113
+ const p = local(g.clientX, g.clientY);
114
+ apply(zoomAt(f, v, (gestureStart * g.scale) / v.z, p.x, p.y));
115
+ };
116
+ el.addEventListener("wheel", onWheel, { passive: false });
117
+ el.addEventListener("gesturestart", onGestureStart);
118
+ el.addEventListener("gesturechange", onGestureChange);
119
+ return () => {
120
+ el.removeEventListener("wheel", onWheel);
121
+ el.removeEventListener("gesturestart", onGestureStart);
122
+ el.removeEventListener("gesturechange", onGestureChange);
123
+ };
124
+ }, [apply]);
125
+ const atCentre = useCallback((op) => {
126
+ const { frame: f, view: v } = live.current;
127
+ if (f === null || v === null)
128
+ return;
129
+ apply(op(f, v, f.width / 2, f.height / 2));
130
+ }, [apply]);
131
+ useEffect(() => {
132
+ const onKey = (e) => {
133
+ // Ctrl/⌘ +/−/0 stay the browser's page zoom.
134
+ if (e.ctrlKey || e.metaKey || e.altKey)
135
+ return;
136
+ if (e.key === "+" || e.key === "=")
137
+ atCentre((f, v, x, y) => zoomAt(f, v, STEP, x, y));
138
+ else if (e.key === "-")
139
+ atCentre((f, v, x, y) => zoomAt(f, v, 1 / STEP, x, y));
140
+ else if (e.key === "0")
141
+ atCentre((f) => fitView(f));
142
+ else if (e.key === "1")
143
+ atCentre(actualAt);
144
+ else
145
+ return;
146
+ e.preventDefault();
147
+ };
148
+ window.addEventListener("keydown", onKey);
149
+ return () => window.removeEventListener("keydown", onKey);
150
+ }, [atCentre]);
151
+ // Pointer bookkeeping: every active pointer's last position, plus where
152
+ // the gesture started so a click can be told apart from a drag.
153
+ const pointers = useRef(new Map());
154
+ const moved = useRef(false);
155
+ const downAt = useRef({ x: 0, y: 0 });
156
+ const localOf = (e) => {
157
+ const r = e.currentTarget.getBoundingClientRect();
158
+ return { x: e.clientX - r.left, y: e.clientY - r.top };
159
+ };
160
+ const onPointerDown = (e) => {
161
+ if (e.pointerType === "mouse" && e.button !== 0)
162
+ return;
163
+ e.currentTarget.setPointerCapture(e.pointerId);
164
+ const p = localOf(e);
165
+ pointers.current.set(e.pointerId, p);
166
+ if (pointers.current.size === 1) {
167
+ moved.current = false;
168
+ downAt.current = p;
169
+ }
170
+ else {
171
+ // A second finger makes this a pinch, never a tap.
172
+ moved.current = true;
173
+ }
174
+ };
175
+ const onPointerMove = (e) => {
176
+ const prev = pointers.current.get(e.pointerId);
177
+ const { frame, view } = live.current;
178
+ if (prev === undefined || frame === null || view === null)
179
+ return;
180
+ const p = localOf(e);
181
+ if (!moved.current &&
182
+ Math.hypot(p.x - downAt.current.x, p.y - downAt.current.y) > TAP_SLOP) {
183
+ moved.current = true;
184
+ }
185
+ if (pointers.current.size === 2) {
186
+ const [a, b] = [...pointers.current.entries()];
187
+ const other = a?.[0] === e.pointerId ? b?.[1] : a?.[1];
188
+ if (other === undefined)
189
+ return;
190
+ const before = Math.hypot(prev.x - other.x, prev.y - other.y);
191
+ const after = Math.hypot(p.x - other.x, p.y - other.y);
192
+ const mid = { x: (p.x + other.x) / 2, y: (p.y + other.y) / 2 };
193
+ const shift = { x: (p.x - prev.x) / 2, y: (p.y - prev.y) / 2 };
194
+ const zoomed = before > 0 ? zoomAt(frame, view, after / before, mid.x, mid.y) : view;
195
+ apply(panBy(frame, zoomed, shift.x, shift.y));
196
+ }
197
+ else if (pointers.current.size === 1 && moved.current) {
198
+ if (!isFitted(frame, view)) {
199
+ setDragging(true);
200
+ apply(panBy(frame, view, p.x - prev.x, p.y - prev.y));
201
+ }
202
+ }
203
+ pointers.current.set(e.pointerId, p);
204
+ };
205
+ const onPointerUp = (e) => {
206
+ if (!pointers.current.delete(e.pointerId))
207
+ return;
208
+ if (pointers.current.size > 0)
209
+ return;
210
+ setDragging(false);
211
+ const { frame, view } = live.current;
212
+ if (moved.current || frame === null || view === null)
213
+ return;
214
+ const p = localOf(e);
215
+ apply(toggleAt(frame, view, p.x, p.y));
216
+ };
217
+ const onPointerCancel = (e) => {
218
+ pointers.current.delete(e.pointerId);
219
+ if (pointers.current.size === 0)
220
+ setDragging(false);
221
+ };
222
+ const ready = frame !== null && view !== null;
223
+ const fitted = !ready || isFitted(frame, view);
224
+ const height = maximized
225
+ ? undefined
226
+ : (held ??
227
+ (ready ? canvasHeight(frame, view.z) : (startHeight ?? undefined)));
228
+ // From the last measured height, which mid-tween is the one on screen.
229
+ // Only between two restored heights at one width: maximize, restore and
230
+ // the first load land at once, and a resized window should not trail.
231
+ const tweened = useRef(null);
232
+ useLayoutEffect(() => {
233
+ const el = canvasRef.current;
234
+ const prev = tweened.current;
235
+ const from = live.current.frame;
236
+ tweened.current =
237
+ height === undefined ? null : { height, width: from?.width ?? null };
238
+ // happy-dom has no Web Animations.
239
+ if (el === null || typeof el.getAnimations !== "function")
240
+ return;
241
+ for (const a of el.getAnimations())
242
+ a.cancel();
243
+ if (from === null || prev === null || height === undefined)
244
+ return;
245
+ if (prev.width !== null && prev.width !== from.width)
246
+ return;
247
+ if (matchMedia("(prefers-reduced-motion: reduce)").matches)
248
+ return;
249
+ // The capped height, not the requested one: a tween to a height past
250
+ // the pane's cap would stop dead at the cap partway through its curve.
251
+ const to = el.clientHeight;
252
+ if (to !== from.height)
253
+ el.animate([{ height: `${from.height}px` }, { height: `${to}px` }], TWEEN);
254
+ }, [height]);
255
+ // − and + give way to pinch on a phone, where the title bar has no room
256
+ // for them beside the filename.
257
+ const controls = (_jsxs("span", { className: "inline-flex shrink-0 items-center gap-0.5", children: [_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "zoom out", disabled: !ready || isAtMin(frame, view), className: "max-sm:hidden", onClick: () => atCentre((f, v, x, y) => zoomAt(f, v, 1 / STEP, x, y)), children: _jsx(MinusIcon, {}) }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", size: "xs", variant: "ghost", disabled: !ready, "aria-label": "zoom and display options", className: "min-w-12 font-normal text-muted-foreground tabular-nums", children: [ready ? `${Math.round(view.z * 100)}%` : "–", _jsx(ChevronDownIcon, {})] }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-44", children: [_jsxs(DropdownMenuItem, { onSelect: () => atCentre((f) => fitView(f)), children: ["Fit", _jsx(DropdownMenuShortcut, { className: "pointer-coarse:hidden", children: "0" })] }), _jsxs(DropdownMenuItem, { onSelect: () => atCentre(actualAt), children: ["100%", _jsx(DropdownMenuShortcut, { className: "pointer-coarse:hidden", children: "1" })] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: "Background" }), _jsxs(DropdownMenuRadioGroup, { value: look.background, onValueChange: (v) => onLookChange({
258
+ ...look,
259
+ background: v,
260
+ }), children: [_jsx(DropdownMenuRadioItem, { value: "black", children: "Black" }), _jsx(DropdownMenuRadioItem, { value: "checker", children: "Checkerboard" })] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: "Scaling" }), _jsxs(DropdownMenuRadioGroup, { value: look.smoothing, onValueChange: (v) => onLookChange({ ...look, smoothing: v }), children: [_jsx(DropdownMenuRadioItem, { value: "smooth", children: "Smooth" }), _jsx(DropdownMenuRadioItem, { value: "pixelated", children: "Pixelated" })] })] })] }), _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "zoom in", disabled: !ready || isAtMax(frame, view), className: "max-sm:hidden", onClick: () => atCentre((f, v, x, y) => zoomAt(f, v, STEP, x, y)), children: _jsx(PlusIcon, {}) })] }));
261
+ // Nothing to measure or zoom; the static image's alt text and
262
+ // broken-image icon are the error.
263
+ if (failed) {
264
+ return (_jsx("img", { src: src, alt: alt, className: cn("mx-auto max-h-(--attachment-pane-height) rounded-md object-contain", className) }));
265
+ }
266
+ return (_jsxs(_Fragment, { children: [toolbar !== null && createPortal(controls, toolbar), _jsxs("div", { ref: canvasRef, "data-testid": "image-canvas", "data-zoom": view === null ? undefined : view.z.toFixed(4), "data-fit": frame === null ? undefined : fitScale(frame).toFixed(4), className: cn("relative touch-none overflow-hidden select-none", look.background === "black" && "bg-black", maximized ? "h-full" : "max-h-(--attachment-pane-height) min-h-24", fitted
267
+ ? "cursor-zoom-in"
268
+ : dragging
269
+ ? "cursor-grabbing"
270
+ : "cursor-grab", className), style: height === undefined ? undefined : { height }, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerCancel, children: [look.background === "checker" && natural !== null && view !== null && (_jsx("div", { "aria-hidden": true, "data-testid": "image-checker", className: "absolute", style: {
271
+ left: view.x,
272
+ top: view.y,
273
+ width: natural.w * view.z,
274
+ height: natural.h * view.z,
275
+ background: CHECKER,
276
+ borderRadius: fitted ? 6 : 0,
277
+ } })), _jsx("img", { src: src, alt: alt, draggable: false, onError: () => {
278
+ lastHeight.current = null;
279
+ setFailed(true);
280
+ }, onLoad: (e) => setNatural({
281
+ w: e.currentTarget.naturalWidth,
282
+ h: e.currentTarget.naturalHeight,
283
+ }), className: cn("absolute top-0 left-0 max-w-none origin-top-left", view === null && "invisible"), style: natural === null || view === null
284
+ ? undefined
285
+ : {
286
+ width: natural.w,
287
+ height: natural.h,
288
+ transform: `translate(${view.x}px, ${view.y}px) scale(${view.z})`,
289
+ // A static image's rounded-md, divided back out of the
290
+ // scale; zoomed in, a rounded corner would read as the
291
+ // image's own edge.
292
+ borderRadius: fitted ? 6 / view.z : 0,
293
+ // Shrunk, nearest-neighbour drops whole rows and columns;
294
+ // pixelated only means something once an image pixel is
295
+ // bigger than a screen pixel.
296
+ imageRendering: look.smoothing === "pixelated" && view.z > 1
297
+ ? "pixelated"
298
+ : undefined,
299
+ } })] })] }));
300
+ }
301
+ //# sourceMappingURL=image-canvas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-canvas.js","sourceRoot":"","sources":["../../src/attachment/image-canvas.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * How the attachment viewer paints an image: what shows through its
3
+ * transparent pixels, and whether a zoomed-in pixel stays a square.
4
+ */
5
+ export type ImageLook = {
6
+ background: "black" | "checker";
7
+ smoothing: "smooth" | "pixelated";
8
+ };
9
+ export declare const DEFAULT_IMAGE_LOOK: ImageLook;
10
+ /** Parse a stored payload; an unknown or missing field falls back to its default. */
11
+ export declare function parseImageLook(raw: string | null): ImageLook;
12
+ /**
13
+ * The viewer's image look kept in localStorage under `storageKey`, following
14
+ * other tabs. Hand the pair to the viewer as `imageLook` and
15
+ * `onImageLookChange`. Name the key after the app; there is no default.
16
+ */
17
+ export declare function useStoredImageLook(storageKey: string): [ImageLook, (look: ImageLook) => void];
18
+ //# sourceMappingURL=image-look.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-look.d.ts","sourceRoot":"","sources":["../../src/attachment/image-look.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,UAAU,EAAE,OAAO,GAAG,SAAS,CAAC;IAChC,SAAS,EAAE,QAAQ,GAAG,WAAW,CAAC;CACnC,CAAC;AAIF,eAAO,MAAM,kBAAkB,EAAE,SAGhC,CAAC;AAEF,qFAAqF;AACrF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAqB5D;AA+CD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,GACjB,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC,CAWxC"}
@@ -0,0 +1,86 @@
1
+ import { useCallback, useSyncExternalStore } from "react";
2
+ // Checkerboard under the image only: an opaque image then looks the same as
3
+ // with no background at all, and a transparent one shows where it ends.
4
+ export const DEFAULT_IMAGE_LOOK = {
5
+ background: "checker",
6
+ smoothing: "smooth",
7
+ };
8
+ /** Parse a stored payload; an unknown or missing field falls back to its default. */
9
+ export function parseImageLook(raw) {
10
+ let parsed;
11
+ try {
12
+ parsed = raw === null ? null : JSON.parse(raw);
13
+ }
14
+ catch {
15
+ parsed = null;
16
+ }
17
+ const stored = typeof parsed === "object" && parsed !== null
18
+ ? parsed
19
+ : {};
20
+ return {
21
+ background: stored.background === "black" || stored.background === "checker"
22
+ ? stored.background
23
+ : DEFAULT_IMAGE_LOOK.background,
24
+ smoothing: stored.smoothing === "smooth" || stored.smoothing === "pixelated"
25
+ ? stored.smoothing
26
+ : DEFAULT_IMAGE_LOOK.smoothing,
27
+ };
28
+ }
29
+ // One cached value per key, shared by every hook reading it, so a choice made
30
+ // in one viewer is what the next one opens with.
31
+ const looks = new Map();
32
+ const listeners = new Set();
33
+ function read(key) {
34
+ let look = looks.get(key);
35
+ if (look === undefined) {
36
+ try {
37
+ look = parseImageLook(localStorage.getItem(key));
38
+ }
39
+ catch {
40
+ // Storage may be unavailable (private mode); the defaults still render.
41
+ look = DEFAULT_IMAGE_LOOK;
42
+ }
43
+ looks.set(key, look);
44
+ }
45
+ return look;
46
+ }
47
+ function write(key, look) {
48
+ looks.set(key, look);
49
+ try {
50
+ localStorage.setItem(key, JSON.stringify(look));
51
+ }
52
+ catch {
53
+ // Unsaved, but this page still switches.
54
+ }
55
+ for (const listener of listeners)
56
+ listener();
57
+ }
58
+ // Another tab's choice applies here too.
59
+ function onStorage(e) {
60
+ if (e.key === null || !looks.has(e.key))
61
+ return;
62
+ looks.set(e.key, parseImageLook(e.newValue));
63
+ for (const listener of listeners)
64
+ listener();
65
+ }
66
+ function subscribe(listener) {
67
+ if (listeners.size === 0)
68
+ window.addEventListener("storage", onStorage);
69
+ listeners.add(listener);
70
+ return () => {
71
+ listeners.delete(listener);
72
+ if (listeners.size === 0)
73
+ window.removeEventListener("storage", onStorage);
74
+ };
75
+ }
76
+ /**
77
+ * The viewer's image look kept in localStorage under `storageKey`, following
78
+ * other tabs. Hand the pair to the viewer as `imageLook` and
79
+ * `onImageLookChange`. Name the key after the app; there is no default.
80
+ */
81
+ export function useStoredImageLook(storageKey) {
82
+ const look = useSyncExternalStore(subscribe, () => read(storageKey), () => DEFAULT_IMAGE_LOOK);
83
+ const setLook = useCallback((next) => write(storageKey, next), [storageKey]);
84
+ return [look, setLook];
85
+ }
86
+ //# sourceMappingURL=image-look.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-look.js","sourceRoot":"","sources":["../../src/attachment/image-look.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Geometry for the attachment viewer's zoomable image. Everything is
3
+ * in the canvas's own pixel space: `x`/`y` is where the image's top-left sits,
4
+ * `z` is display pixels per natural pixel.
5
+ */
6
+ export type Frame = {
7
+ /** Canvas size. */
8
+ width: number;
9
+ height: number;
10
+ /** Image's natural size. */
11
+ naturalWidth: number;
12
+ naturalHeight: number;
13
+ /** Whether fit may enlarge an image smaller than the canvas (maximized). */
14
+ upscale: boolean;
15
+ };
16
+ export type View = {
17
+ z: number;
18
+ x: number;
19
+ y: number;
20
+ };
21
+ /** Deepest zoom, in natural pixels: far enough to read a single pixel. */
22
+ export declare const MAX_NATURAL_SCALE = 8;
23
+ export declare function fitScale(f: Frame): number;
24
+ /**
25
+ * An upscaled fit can already sit past the natural-pixel cap (a 48px icon
26
+ * maximized), so the ceiling always leaves room to double whatever fit is.
27
+ */
28
+ export declare function maxScale(f: Frame): number;
29
+ /**
30
+ * Never smaller than fit, except that actual pixels stay reachable when an
31
+ * upscaled fit (maximized) is already larger than them.
32
+ */
33
+ export declare function minScale(f: Frame): number;
34
+ export declare function fitView(f: Frame): View;
35
+ /** Within a hair of fit counts as fit, so float drift cannot strand a view. */
36
+ export declare function isFitted(f: Frame, v: View): boolean;
37
+ export declare function isAtMin(f: Frame, v: View): boolean;
38
+ export declare function isAtMax(f: Frame, v: View): boolean;
39
+ export declare function clampView(f: Frame, v: View): View;
40
+ /**
41
+ * The height a restored (not maximized) canvas asks for at zoom `z`: the
42
+ * image's own, so zooming in grows the dialog until its CSS cap takes over
43
+ * and only then starts panning. Never shorter than at fit, so the request
44
+ * does not depend on the measured height it produces. Whole pixels, because
45
+ * the canvas is measured back with clientHeight.
46
+ */
47
+ export declare function canvasHeight(f: Frame, z: number): number;
48
+ /**
49
+ * Carry a view across a canvas resize. A fitted view refits; any other keeps
50
+ * whatever sat at the canvas centre there — the dialog is centred, so that
51
+ * is also what keeps the image still on screen while the canvas grows.
52
+ */
53
+ export declare function reframe(prev: Frame, next: Frame, v: View): View;
54
+ /** Scale by `factor` keeping canvas point (px, py) over the same image pixel. */
55
+ export declare function zoomAt(f: Frame, v: View, factor: number, px: number, py: number): View;
56
+ /** Actual pixels (100%), keeping canvas point (px, py) over the same image pixel. */
57
+ export declare function actualAt(f: Frame, v: View, px: number, py: number): View;
58
+ export declare function panBy(f: Frame, v: View, dx: number, dy: number): View;
59
+ /**
60
+ * Click / tap: from fit, zoom at the pointer to actual pixels or twice the
61
+ * fit, whichever is deeper — a fit of 0.8 jumping to 1.0 would look like
62
+ * nothing happened. From anything else, back to fit.
63
+ */
64
+ export declare function toggleAt(f: Frame, v: View, px: number, py: number): View;
65
+ /** Wheel delta → multiplicative factor. Pinch arrives as ctrl+wheel with small deltas. */
66
+ export declare function wheelFactor(deltaY: number, deltaMode: number, ctrlKey: boolean): number;
67
+ //# sourceMappingURL=image-zoom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-zoom.d.ts","sourceRoot":"","sources":["../../src/attachment/image-zoom.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,KAAK,GAAG;IAClB,mBAAmB;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvD,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,CAMzC;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,CAEzC;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,CAEzC;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI,CAOtC;AAED,+EAA+E;AAC/E,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,GAAG,OAAO,CAEnD;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,GAAG,OAAO,CAElD;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,GAAG,OAAO,CAElD;AASD,wBAAgB,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,GAAG,IAAI,CAOjD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAGxD;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,GAAG,IAAI,CAO/D;AAED,iFAAiF;AACjF,wBAAgB,MAAM,CACpB,CAAC,EAAE,KAAK,EACR,CAAC,EAAE,IAAI,EACP,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,GACT,IAAI,CAIN;AAED,qFAAqF;AACrF,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAExE;AAED,wBAAgB,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAErE;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAIxE;AAED,0FAA0F;AAC1F,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,OAAO,GACf,MAAM,CAIR"}