@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.
- package/agent/START-HERE.md +5 -5
- package/agent/components/Image.json +87 -0
- package/agent/components-index.json +5 -0
- package/agent/components.json +87 -0
- package/agent/index.json +6 -6
- package/agent/llms.txt +6 -6
- package/agent/tokens.json +72 -0
- package/dist/components/data-display/image.d.ts +21 -0
- package/dist/components/data-display/image.js +502 -0
- package/dist/components/data-display/index.d.ts +2 -0
- package/dist/components/data-display/index.js +3 -0
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +19 -0
- package/dist/i18n/messages/ja.json +19 -0
- package/dist/i18n/messages/vi.json +19 -0
- package/dist/props/components/data-display.prop.d.ts +73 -0
- package/dist/props/registry.d.ts +97 -0
- package/dist/props/registry.js +81 -0
- package/dist/styles/data-display-layout.css +203 -0
- package/dist/styles/layers.json +6 -1
- package/dist/tokens/base.css +1 -0
- package/dist/tokens/components/image.css +19 -0
- package/docs/FRAME-COVERAGE-REPORT.md +3 -2
- package/docs/data-display/image.tsx +130 -0
- package/package.json +2 -2
|
@@ -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.
|
|
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
|
+
};
|