@godxjp/ui 31.10.3 → 31.11.1

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.
@@ -0,0 +1,502 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { mergeRefs } from "@react-aria/utils";
5
+ import { Dialog as RacDialog, Modal, ModalOverlay } from "react-aria-components";
6
+ import {
7
+ ChevronLeft,
8
+ ChevronRight,
9
+ Eye,
10
+ FlipHorizontal2,
11
+ FlipVertical2,
12
+ RotateCcw,
13
+ RotateCw,
14
+ Undo2,
15
+ X,
16
+ ZoomIn,
17
+ ZoomOut
18
+ } from "lucide-react";
19
+ import { useTranslation } from "../../i18n/use-translation.js";
20
+ import { useOverlayPortalContainer } from "../../lib/overlay-portal.js";
21
+ import { cn } from "../../lib/utils.js";
22
+ import { useOverlayCloseFocus } from "../feedback/overlay-close-focus.js";
23
+ const ImageGroupContext = React.createContext(null);
24
+ const ImageGroupDisabledContext = React.createContext(false);
25
+ const BASE_SCALE_RATIO = 1;
26
+ const DEFAULT_SCALE_STEP = 0.5;
27
+ const DEFAULT_MIN_SCALE = 1;
28
+ const DEFAULT_MAX_SCALE = 50;
29
+ const INITIAL_TRANSFORM = {
30
+ x: 0,
31
+ y: 0,
32
+ scale: 1,
33
+ rotate: 0,
34
+ flipX: false,
35
+ flipY: false
36
+ };
37
+ function toEntry(item) {
38
+ return typeof item === "string" ? { src: item, alt: "" } : { src: item.src, alt: item.alt ?? "" };
39
+ }
40
+ function useMergedState(controlled, initial, onChange) {
41
+ const [inner, setInner] = React.useState(initial);
42
+ const value = controlled ?? inner;
43
+ const set = React.useCallback(
44
+ (next) => {
45
+ if (Object.is(next, value)) return;
46
+ if (controlled === void 0) setInner(next);
47
+ onChange?.(next, value);
48
+ },
49
+ [controlled, onChange, value]
50
+ );
51
+ return [value, set];
52
+ }
53
+ function inDocumentOrder(entries) {
54
+ return [...entries].sort((a, b) => {
55
+ const na = a.node();
56
+ const nb = b.node();
57
+ if (!na || !nb) return 0;
58
+ return na.compareDocumentPosition(nb) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
59
+ });
60
+ }
61
+ function isRtl(element) {
62
+ return element?.closest("[dir]")?.getAttribute("dir")?.toLowerCase() === "rtl";
63
+ }
64
+ function ImagePreviewDialog({
65
+ open,
66
+ onOpenChange,
67
+ items,
68
+ current,
69
+ onCurrentChange,
70
+ fallback,
71
+ scaleStep = DEFAULT_SCALE_STEP,
72
+ minScale = DEFAULT_MIN_SCALE,
73
+ maxScale = DEFAULT_MAX_SCALE
74
+ }) {
75
+ const { t } = useTranslation();
76
+ const portalContainer = useOverlayPortalContainer();
77
+ useOverlayCloseFocus(open);
78
+ const sectionRef = React.useRef(null);
79
+ React.useEffect(() => {
80
+ const section = sectionRef.current;
81
+ if (open && section && !section.contains(document.activeElement)) {
82
+ section.focus({ preventScroll: true });
83
+ }
84
+ }, [open]);
85
+ const total = items.length;
86
+ const index = total === 0 ? 0 : Math.min(Math.max(current, 0), total - 1);
87
+ const entry = items[index];
88
+ const [transform, setTransform] = React.useState(INITIAL_TRANSFORM);
89
+ const [failedSrc, setFailedSrc] = React.useState(null);
90
+ const [dragging, setDragging] = React.useState(false);
91
+ const dragStart = React.useRef(null);
92
+ React.useEffect(() => {
93
+ setTransform(INITIAL_TRANSFORM);
94
+ setFailedSrc(null);
95
+ }, [index, open]);
96
+ const zoomBy = (ratio) => setTransform((prev) => {
97
+ const scale2 = Math.min(Math.max(prev.scale * ratio, minScale), maxScale);
98
+ return scale2 <= 1 ? { ...prev, scale: scale2, x: 0, y: 0 } : { ...prev, scale: scale2 };
99
+ });
100
+ const zoomIn = () => zoomBy(BASE_SCALE_RATIO + scaleStep);
101
+ const zoomOut = () => zoomBy(BASE_SCALE_RATIO / (BASE_SCALE_RATIO + scaleStep));
102
+ const hasPrev = index > 0;
103
+ const hasNext = index < total - 1;
104
+ const goPrev = () => hasPrev && onCurrentChange(index - 1);
105
+ const goNext = () => hasNext && onCurrentChange(index + 1);
106
+ const onKeyDown = (event) => {
107
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
108
+ if (total < 2) return;
109
+ event.preventDefault();
110
+ const forward = event.key === "ArrowRight" !== isRtl(event.currentTarget);
111
+ if (forward) goNext();
112
+ else goPrev();
113
+ };
114
+ const onWheel = (event) => {
115
+ if (event.deltaY === 0) return;
116
+ if (event.deltaY < 0) zoomIn();
117
+ else zoomOut();
118
+ };
119
+ const onDoubleClick = () => {
120
+ if (transform.scale !== 1) setTransform((prev) => ({ ...prev, x: 0, y: 0, scale: 1 }));
121
+ else zoomIn();
122
+ };
123
+ const onPointerDown = (event) => {
124
+ if (event.button !== 0) return;
125
+ event.preventDefault();
126
+ event.currentTarget.setPointerCapture?.(event.pointerId);
127
+ dragStart.current = { px: event.clientX, py: event.clientY, x: transform.x, y: transform.y };
128
+ setDragging(true);
129
+ };
130
+ const onPointerMove = (event) => {
131
+ const start = dragStart.current;
132
+ if (!start) return;
133
+ setTransform((prev) => ({
134
+ ...prev,
135
+ x: start.x + event.clientX - start.px,
136
+ y: start.y + event.clientY - start.py
137
+ }));
138
+ };
139
+ const onPointerUp = () => {
140
+ if (!dragStart.current) return;
141
+ dragStart.current = null;
142
+ setDragging(false);
143
+ setTransform((prev) => prev.scale <= 1 ? { ...prev, x: 0, y: 0 } : prev);
144
+ };
145
+ const src = entry && failedSrc === entry.src && fallback ? fallback : entry?.src;
146
+ const { x, y, scale, rotate, flipX, flipY } = transform;
147
+ const imageTransform = `translate3d(${x}px, ${y}px, 0) scale3d(${flipX ? -scale : scale}, ${flipY ? -scale : scale}, 1) rotate(${rotate}deg)`;
148
+ const tools = [
149
+ {
150
+ key: "flipY",
151
+ label: t("dataDisplay.image.flipY"),
152
+ icon: /* @__PURE__ */ jsx(FlipVertical2, {}),
153
+ run: () => setTransform((p) => ({ ...p, flipY: !p.flipY }))
154
+ },
155
+ {
156
+ key: "flipX",
157
+ label: t("dataDisplay.image.flipX"),
158
+ icon: /* @__PURE__ */ jsx(FlipHorizontal2, {}),
159
+ run: () => setTransform((p) => ({ ...p, flipX: !p.flipX }))
160
+ },
161
+ {
162
+ key: "rotateLeft",
163
+ label: t("dataDisplay.image.rotateLeft"),
164
+ icon: /* @__PURE__ */ jsx(RotateCcw, {}),
165
+ run: () => setTransform((p) => ({ ...p, rotate: p.rotate - 90 }))
166
+ },
167
+ {
168
+ key: "rotateRight",
169
+ label: t("dataDisplay.image.rotateRight"),
170
+ icon: /* @__PURE__ */ jsx(RotateCw, {}),
171
+ run: () => setTransform((p) => ({ ...p, rotate: p.rotate + 90 }))
172
+ },
173
+ {
174
+ key: "zoomOut",
175
+ label: t("dataDisplay.image.zoomOut"),
176
+ icon: /* @__PURE__ */ jsx(ZoomOut, {}),
177
+ run: zoomOut,
178
+ off: scale <= minScale
179
+ },
180
+ {
181
+ key: "zoomIn",
182
+ label: t("dataDisplay.image.zoomIn"),
183
+ icon: /* @__PURE__ */ jsx(ZoomIn, {}),
184
+ run: zoomIn,
185
+ off: scale >= maxScale
186
+ },
187
+ {
188
+ key: "reset",
189
+ label: t("dataDisplay.image.reset"),
190
+ icon: /* @__PURE__ */ jsx(Undo2, {}),
191
+ run: () => setTransform(INITIAL_TRANSFORM)
192
+ }
193
+ ];
194
+ return /* @__PURE__ */ jsx(
195
+ ModalOverlay,
196
+ {
197
+ UNSTABLE_portalContainer: portalContainer,
198
+ isOpen: open,
199
+ onOpenChange,
200
+ isDismissable: false,
201
+ "data-slot": "image-preview-overlay",
202
+ className: "ui-image-preview-overlay",
203
+ children: /* @__PURE__ */ jsx(Modal, { className: "contents", children: /* @__PURE__ */ jsxs(
204
+ RacDialog,
205
+ {
206
+ "aria-label": t("dataDisplay.image.dialog"),
207
+ "data-slot": "image-preview",
208
+ className: "ui-image-preview",
209
+ render: (racProps) => {
210
+ const {
211
+ "data-rac": _rac,
212
+ ref: racRef,
213
+ ...rest
214
+ } = racProps;
215
+ return /* @__PURE__ */ jsx("section", { ...rest, ref: mergeRefs(racRef, sectionRef), onKeyDown });
216
+ },
217
+ children: [
218
+ /* @__PURE__ */ jsx(
219
+ "div",
220
+ {
221
+ className: "ui-image-preview-stage",
222
+ "data-dragging": dragging ? "" : void 0,
223
+ onWheel,
224
+ onClick: (event) => {
225
+ if (event.target === event.currentTarget) onOpenChange(false);
226
+ },
227
+ children: src ? /* @__PURE__ */ jsx(
228
+ "img",
229
+ {
230
+ "data-slot": "image-preview-img",
231
+ className: "ui-image-preview-img",
232
+ src,
233
+ alt: entry?.alt ?? "",
234
+ draggable: false,
235
+ style: { transform: imageTransform },
236
+ onError: () => entry && setFailedSrc(entry.src),
237
+ onDoubleClick,
238
+ onPointerDown,
239
+ onPointerMove,
240
+ onPointerUp,
241
+ onPointerCancel: onPointerUp
242
+ },
243
+ index
244
+ ) : null
245
+ }
246
+ ),
247
+ /* @__PURE__ */ jsx(
248
+ "button",
249
+ {
250
+ type: "button",
251
+ "data-slot": "image-preview-close",
252
+ className: "ui-image-preview-close ui-image-preview-button ui-focus-ring",
253
+ "aria-label": t("dataDisplay.image.close"),
254
+ onClick: () => onOpenChange(false),
255
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
256
+ }
257
+ ),
258
+ total > 1 ? /* @__PURE__ */ jsxs(Fragment, { children: [
259
+ /* @__PURE__ */ jsx(
260
+ "button",
261
+ {
262
+ type: "button",
263
+ "data-slot": "image-preview-previous",
264
+ className: "ui-image-preview-switch ui-image-preview-button ui-focus-ring",
265
+ "data-direction": "previous",
266
+ "aria-label": t("dataDisplay.image.previous"),
267
+ disabled: !hasPrev,
268
+ onClick: goPrev,
269
+ children: /* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": "true" })
270
+ }
271
+ ),
272
+ /* @__PURE__ */ jsx(
273
+ "button",
274
+ {
275
+ type: "button",
276
+ "data-slot": "image-preview-next",
277
+ className: "ui-image-preview-switch ui-image-preview-button ui-focus-ring",
278
+ "data-direction": "next",
279
+ "aria-label": t("dataDisplay.image.next"),
280
+ disabled: !hasNext,
281
+ onClick: goNext,
282
+ children: /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" })
283
+ }
284
+ )
285
+ ] }) : null,
286
+ /* @__PURE__ */ jsxs("div", { className: "ui-image-preview-footer", children: [
287
+ total > 1 ? /* @__PURE__ */ jsxs(Fragment, { children: [
288
+ /* @__PURE__ */ jsx(
289
+ "span",
290
+ {
291
+ "data-slot": "image-preview-counter",
292
+ className: "ui-image-preview-counter",
293
+ "aria-hidden": "true",
294
+ children: t("dataDisplay.image.counter", { current: index + 1, total })
295
+ }
296
+ ),
297
+ /* @__PURE__ */ jsx("span", { className: "sr-only", "aria-live": "polite", children: t("dataDisplay.image.position", { current: index + 1, total }) })
298
+ ] }) : null,
299
+ /* @__PURE__ */ jsx(
300
+ "div",
301
+ {
302
+ role: "group",
303
+ "aria-label": t("dataDisplay.image.toolbar"),
304
+ "data-slot": "image-preview-toolbar",
305
+ className: "ui-image-preview-toolbar",
306
+ children: tools.map((tool) => /* @__PURE__ */ jsx(
307
+ "button",
308
+ {
309
+ type: "button",
310
+ "data-action": tool.key,
311
+ className: "ui-image-preview-button ui-focus-ring",
312
+ "aria-label": tool.label,
313
+ title: tool.label,
314
+ disabled: tool.off,
315
+ onClick: tool.run,
316
+ children: /* @__PURE__ */ jsx("span", { className: "ui-image-preview-icon", "aria-hidden": "true", children: tool.icon })
317
+ },
318
+ tool.key
319
+ ))
320
+ }
321
+ )
322
+ ] })
323
+ ]
324
+ }
325
+ ) })
326
+ }
327
+ );
328
+ }
329
+ function ImagePreviewGroup({
330
+ items,
331
+ fallback,
332
+ preview = true,
333
+ children
334
+ }) {
335
+ const config = typeof preview === "object" ? preview : {};
336
+ const enabled = preview !== false;
337
+ const registry = React.useRef(/* @__PURE__ */ new Map());
338
+ const [version, setVersion] = React.useState(0);
339
+ const [visible, setVisible] = useMergedState(config.visible, false, config.onVisibleChange);
340
+ const [current, setCurrent] = useMergedState(config.current, 0, config.onChange);
341
+ const registered = React.useMemo(() => {
342
+ void version;
343
+ return inDocumentOrder(registry.current.values());
344
+ }, [version]);
345
+ const list = React.useMemo(
346
+ () => items ? items.map(toEntry) : registered,
347
+ [items, registered]
348
+ );
349
+ const latest = React.useRef({ items, setCurrent, setVisible });
350
+ latest.current = { items, setCurrent, setVisible };
351
+ const context = React.useMemo(
352
+ () => ({
353
+ register: (entry) => {
354
+ registry.current.set(entry.id, entry);
355
+ setVersion((v) => v + 1);
356
+ return () => {
357
+ registry.current.delete(entry.id);
358
+ setVersion((v) => v + 1);
359
+ };
360
+ },
361
+ open: (id) => {
362
+ const { items: explicit, setCurrent: select, setVisible: show } = latest.current;
363
+ const entry = registry.current.get(id);
364
+ const index = explicit ? explicit.map(toEntry).findIndex((item) => item.src === entry?.src) : inDocumentOrder(registry.current.values()).findIndex((item) => item.id === id);
365
+ select(Math.max(index, 0));
366
+ show(true);
367
+ }
368
+ }),
369
+ []
370
+ );
371
+ return /* @__PURE__ */ jsx(ImageGroupDisabledContext.Provider, { value: !enabled, children: /* @__PURE__ */ jsxs(ImageGroupContext.Provider, { value: enabled ? context : null, children: [
372
+ children,
373
+ enabled ? /* @__PURE__ */ jsx(
374
+ ImagePreviewDialog,
375
+ {
376
+ open: visible && list.length > 0,
377
+ onOpenChange: setVisible,
378
+ items: list,
379
+ current,
380
+ onCurrentChange: setCurrent,
381
+ fallback,
382
+ scaleStep: config.scaleStep,
383
+ minScale: config.minScale,
384
+ maxScale: config.maxScale
385
+ }
386
+ ) : null
387
+ ] }) });
388
+ }
389
+ const ImageRoot = React.forwardRef(function Image({ src, alt, fallback, placeholder, preview = true, className, onLoad, onError, ...imgProps }, ref) {
390
+ const { t } = useTranslation();
391
+ const group = React.useContext(ImageGroupContext);
392
+ const groupDisabled = React.useContext(ImageGroupDisabledContext);
393
+ const id = React.useId();
394
+ const wrapperRef = React.useRef(null);
395
+ const [load, setLoad] = React.useState({
396
+ src,
397
+ status: "loading"
398
+ });
399
+ const status = load.src === src ? load.status : "loading";
400
+ const setStatus = React.useCallback((next) => setLoad({ src, status: next }), [src]);
401
+ const imgRef = React.useRef(null);
402
+ const setImgRef = React.useCallback(
403
+ (node) => {
404
+ imgRef.current = node;
405
+ if (node?.complete && node.naturalWidth > 0) setStatus("loaded");
406
+ if (typeof ref === "function") ref(node);
407
+ else if (ref) ref.current = node;
408
+ },
409
+ [ref, setStatus]
410
+ );
411
+ const config = typeof preview === "object" ? preview : {};
412
+ const isError = status === "error";
413
+ const canPreview = preview !== false && !groupDisabled && !isError;
414
+ const previewSrc = config.src ?? src;
415
+ const inGroup = group != null && canPreview;
416
+ React.useLayoutEffect(() => {
417
+ if (!inGroup || !group) return void 0;
418
+ return group.register({ id, src: previewSrc, alt, node: () => wrapperRef.current });
419
+ }, [inGroup, group, id, previewSrc, alt]);
420
+ const [visible, setVisible] = useMergedState(config.visible, false, config.onVisibleChange);
421
+ const shownSrc = isError && fallback ? fallback : src;
422
+ const showPlaceholder = placeholder != null && placeholder !== false && status === "loading";
423
+ const picture = /* @__PURE__ */ jsxs(Fragment, { children: [
424
+ /* @__PURE__ */ jsx(
425
+ "img",
426
+ {
427
+ ...imgProps,
428
+ ref: setImgRef,
429
+ "data-slot": "image-img",
430
+ className: "ui-image-img",
431
+ src: shownSrc,
432
+ alt,
433
+ onLoad: (event) => {
434
+ setStatus("loaded");
435
+ onLoad?.(event);
436
+ },
437
+ onError: (event) => {
438
+ setStatus("error");
439
+ onError?.(event);
440
+ }
441
+ }
442
+ ),
443
+ showPlaceholder ? /* @__PURE__ */ jsx("span", { "data-slot": "image-placeholder", className: "ui-image-placeholder", "aria-hidden": "true", children: placeholder === true ? null : placeholder }) : null
444
+ ] });
445
+ if (!canPreview) {
446
+ return /* @__PURE__ */ jsx(
447
+ "span",
448
+ {
449
+ ref: wrapperRef,
450
+ "data-slot": "image",
451
+ "data-status": status,
452
+ className: cn("ui-image", className),
453
+ children: picture
454
+ }
455
+ );
456
+ }
457
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
458
+ /* @__PURE__ */ jsxs(
459
+ "button",
460
+ {
461
+ ref: (node) => {
462
+ wrapperRef.current = node;
463
+ },
464
+ type: "button",
465
+ "data-slot": "image",
466
+ "data-status": status,
467
+ "data-preview": "",
468
+ className: cn("ui-image ui-focus-ring", className),
469
+ "aria-label": alt ? t("dataDisplay.image.previewLabel", { alt }) : t("dataDisplay.image.previewUnnamed"),
470
+ "aria-haspopup": "dialog",
471
+ onClick: () => inGroup && group ? group.open(id) : setVisible(true),
472
+ children: [
473
+ picture,
474
+ /* @__PURE__ */ jsx("span", { "data-slot": "image-mask", className: "ui-image-mask", "aria-hidden": "true", children: config.mask ?? /* @__PURE__ */ jsxs(Fragment, { children: [
475
+ /* @__PURE__ */ jsx(Eye, { className: "ui-image-mask-icon" }),
476
+ t("dataDisplay.image.preview")
477
+ ] }) })
478
+ ]
479
+ }
480
+ ),
481
+ inGroup ? null : /* @__PURE__ */ jsx(
482
+ ImagePreviewDialog,
483
+ {
484
+ open: visible,
485
+ onOpenChange: setVisible,
486
+ items: [{ src: previewSrc, alt }],
487
+ current: 0,
488
+ onCurrentChange: () => void 0,
489
+ fallback,
490
+ scaleStep: config.scaleStep,
491
+ minScale: config.minScale,
492
+ maxScale: config.maxScale
493
+ }
494
+ )
495
+ ] });
496
+ });
497
+ ImageRoot.displayName = "Image";
498
+ const Image2 = Object.assign(ImageRoot, { PreviewGroup: ImagePreviewGroup });
499
+ export {
500
+ Image2 as Image,
501
+ ImagePreviewGroup
502
+ };
@@ -59,3 +59,5 @@ export { Welcome } from "./welcome.js";
59
59
  export type { WelcomeProp, WelcomeProps, WelcomeVariantProp } from "./welcome.js";
60
60
  export { ThoughtChain, ThoughtChainItem } from "./thought-chain.js";
61
61
  export type { ThoughtChainProp, ThoughtChainProps, ThoughtChainItemsProp, ThoughtChainItemProp, ThoughtChainItemProps, ThoughtChainStatusProp, ThoughtChainLineProp, ThoughtChainVariantProp, } from "./thought-chain.js";
62
+ export { Image, ImagePreviewGroup } from "./image.js";
63
+ export type { ImagePreviewConfigProp, ImagePreviewGroupConfigProp, ImagePreviewGroupProp, ImagePreviewGroupProps, ImagePreviewItemProp, ImageProp, ImageProps, } from "./image.js";
@@ -63,6 +63,7 @@ import { Tree } from "./tree.js";
63
63
  import { OrgChart } from "./org-chart.js";
64
64
  import { Welcome } from "./welcome.js";
65
65
  import { ThoughtChain, ThoughtChainItem } from "./thought-chain.js";
66
+ import { Image, ImagePreviewGroup } from "./image.js";
66
67
  export {
67
68
  Accordion,
68
69
  AccordionContent,
@@ -101,6 +102,8 @@ export {
101
102
  HoverCard,
102
103
  HoverCardContent,
103
104
  HoverCardTrigger,
105
+ Image,
106
+ ImagePreviewGroup,
104
107
  Legend,
105
108
  ListRow,
106
109
  Marquee,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
3
- "version": "31.10.3",
3
+ "version": "31.11.1",
4
4
  "targetSize": {
5
5
  "standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
6
6
  "min": 24,
@@ -384,6 +384,25 @@
384
384
  "statusDone": "Completed: ",
385
385
  "statusCurrent": "Current: ",
386
386
  "statusPending": "Upcoming: "
387
+ },
388
+ "image": {
389
+ "preview": "Preview",
390
+ "previewLabel": "Preview: {alt}",
391
+ "previewUnnamed": "Preview image",
392
+ "dialog": "Image preview",
393
+ "close": "Close preview",
394
+ "previous": "Previous image",
395
+ "next": "Next image",
396
+ "counter": "{current} / {total}",
397
+ "position": "Image {current} of {total}",
398
+ "toolbar": "Image tools",
399
+ "flipX": "Flip horizontally",
400
+ "flipY": "Flip vertically",
401
+ "rotateLeft": "Rotate left",
402
+ "rotateRight": "Rotate right",
403
+ "zoomIn": "Zoom in",
404
+ "zoomOut": "Zoom out",
405
+ "reset": "Reset"
387
406
  }
388
407
  },
389
408
  "ui": {
@@ -377,6 +377,25 @@
377
377
  "statusDone": "完了: ",
378
378
  "statusCurrent": "現在: ",
379
379
  "statusPending": "予定: "
380
+ },
381
+ "image": {
382
+ "preview": "プレビュー",
383
+ "previewLabel": "プレビュー: {alt}",
384
+ "previewUnnamed": "画像をプレビュー",
385
+ "dialog": "画像プレビュー",
386
+ "close": "プレビューを閉じる",
387
+ "previous": "前の画像",
388
+ "next": "次の画像",
389
+ "counter": "{current} / {total}",
390
+ "position": "{total} 枚中 {current} 枚目",
391
+ "toolbar": "画像ツール",
392
+ "flipX": "左右反転",
393
+ "flipY": "上下反転",
394
+ "rotateLeft": "左に回転",
395
+ "rotateRight": "右に回転",
396
+ "zoomIn": "拡大",
397
+ "zoomOut": "縮小",
398
+ "reset": "リセット"
380
399
  }
381
400
  },
382
401
  "ui": {
@@ -378,6 +378,25 @@
378
378
  "statusDone": "Đã xong: ",
379
379
  "statusCurrent": "Hiện tại: ",
380
380
  "statusPending": "Sắp tới: "
381
+ },
382
+ "image": {
383
+ "preview": "Xem",
384
+ "previewLabel": "Xem ảnh: {alt}",
385
+ "previewUnnamed": "Xem ảnh",
386
+ "dialog": "Xem ảnh",
387
+ "close": "Đóng xem ảnh",
388
+ "previous": "Ảnh trước",
389
+ "next": "Ảnh sau",
390
+ "counter": "{current} / {total}",
391
+ "position": "Ảnh {current} trên {total}",
392
+ "toolbar": "Công cụ ảnh",
393
+ "flipX": "Lật ngang",
394
+ "flipY": "Lật dọc",
395
+ "rotateLeft": "Xoay trái",
396
+ "rotateRight": "Xoay phải",
397
+ "zoomIn": "Phóng to",
398
+ "zoomOut": "Thu nhỏ",
399
+ "reset": "Đặt lại"
381
400
  }
382
401
  },
383
402
  "ui": {
@@ -1195,3 +1195,76 @@ export type OrgChartProp = {
1195
1195
  className?: ClassNameProp;
1196
1196
  id?: IdProp;
1197
1197
  };
1198
+ /**
1199
+ * @see ImagePreviewGroup — one picture of a group's `items`. A plain string is its `src` (antd
1200
+ * `PreviewGroup items: string[] | { src, … }[]`).
1201
+ */
1202
+ export type ImagePreviewItemProp = string | {
1203
+ src: string;
1204
+ alt?: string;
1205
+ };
1206
+ /**
1207
+ * @see Image — the object form of `preview` (antd `Image preview: PreviewType`).
1208
+ *
1209
+ * `visible` / `onVisibleChange(visible, prevVisible)` keep antd's names on purpose: the whole
1210
+ * component is a port of antd `Image`, and a Markdown renderer ported from an antd screen passes
1211
+ * these through unchanged.
1212
+ */
1213
+ export type ImagePreviewConfigProp = {
1214
+ /** Controlled: is the preview open. */
1215
+ visible?: boolean;
1216
+ /** Called with the next and the previous open state (antd order). */
1217
+ onVisibleChange?: (visible: boolean, prevVisible: boolean) => void;
1218
+ /** A different (usually larger) file to show in the preview than the one on the page. */
1219
+ src?: string;
1220
+ /** Replaces the hover/focus veil's content (default: an eye + "Preview"). */
1221
+ mask?: React.ReactNode;
1222
+ /** Zoom factor per step, as a fraction: `0.5` means ×1.5 per zoom-in. Default `0.5`. */
1223
+ scaleStep?: number;
1224
+ /** Smallest zoom. Default `1`. */
1225
+ minScale?: number;
1226
+ /** Largest zoom. Default `50`. */
1227
+ maxScale?: number;
1228
+ };
1229
+ /** @see ImagePreviewGroup — the group's `preview` object (antd `PreviewGroup preview`). */
1230
+ export type ImagePreviewGroupConfigProp = Omit<ImagePreviewConfigProp, "src" | "mask"> & {
1231
+ /** Controlled: index of the picture on show. */
1232
+ current?: number;
1233
+ /** Called with the next and the previous index (antd order). */
1234
+ onChange?: (current: number, prevCurrent: number) => void;
1235
+ };
1236
+ /**
1237
+ * @see Image — a picture that opens large in a full-viewport preview (antd `Image`): zoom, rotate,
1238
+ * flip, and — inside an `ImagePreviewGroup` — prev/next across every picture of the group.
1239
+ */
1240
+ export type ImageProp = Omit<React.ImgHTMLAttributes<HTMLImageElement>, "alt" | "placeholder"> & {
1241
+ /** Image URL. */
1242
+ src: string;
1243
+ /**
1244
+ * Required, as on `Thumbnail`: `""` is a decision, a missing `alt` an omission (WCAG 1.1.1). It
1245
+ * also names the preview trigger ("Preview: {alt}").
1246
+ */
1247
+ alt: string;
1248
+ /** Shown instead when `src` fails to load. A failed picture does not open a preview (antd). */
1249
+ fallback?: string;
1250
+ /** Painted over the frame until the picture loads. `true` = the plain muted block. */
1251
+ placeholder?: React.ReactNode | boolean;
1252
+ /** `false` = a plain picture, not clickable, not in its group. Default `true`. */
1253
+ preview?: boolean | ImagePreviewConfigProp;
1254
+ className?: ClassNameProp;
1255
+ };
1256
+ /**
1257
+ * @see ImagePreviewGroup — pages the preview across a set of pictures (antd `Image.PreviewGroup`).
1258
+ *
1259
+ * Every `Image` rendered anywhere under it joins the group in DOCUMENT order, however deep — so a
1260
+ * Markdown renderer's `img` override is enough to make a whole article one gallery.
1261
+ */
1262
+ export type ImagePreviewGroupProp = {
1263
+ /** An explicit list instead of the pictures rendered under the group. */
1264
+ items?: readonly ImagePreviewItemProp[];
1265
+ /** Shown in the preview instead of a picture that fails to load. */
1266
+ fallback?: string;
1267
+ /** `false` turns the whole group's previews off; an object controls it (antd shape). */
1268
+ preview?: boolean | ImagePreviewGroupConfigProp;
1269
+ children?: React.ReactNode;
1270
+ };