@wipco/sdui 1.9.1 → 1.9.2

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.
@@ -1,4 +1,5 @@
1
- import { ImageLightbox } from './chunk-S4CQTJC6.js';
1
+ import { ImageLightbox } from './chunk-FLBU7AVR.js';
2
+ import { looksLikeImage, fileMimeOf, FileTile, fileBadgeOf, fileDisplayNameOf, safeFileHref, FILE_TILE_WIDTH, IMAGE_EXTENSIONS } from './chunk-EKNEBGNW.js';
2
3
  import { useSduiSandbox, useSduiAdapter, hostRawFetchOf, defaultAdapter, useSduiAssetResolver } from './chunk-DE7FPWVJ.js';
3
4
  import { Spinner, Icon, IconButton } from './chunk-45GIMFRP.js';
4
5
  import { boxed } from './chunk-3JTBOABA.js';
@@ -6,91 +7,6 @@ import { useState, useRef, useEffect, useCallback } from 'react';
6
7
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
8
 
8
9
  var THUMB_SIZE = 72;
9
- var FILE_TILE_WIDTH = 168;
10
- var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
11
- "jpg",
12
- "jpeg",
13
- "jpe",
14
- "jfif",
15
- "pjpeg",
16
- "pjp",
17
- "png",
18
- "apng",
19
- "gif",
20
- "webp",
21
- "avif",
22
- "bmp",
23
- "svg",
24
- "ico",
25
- "heic",
26
- "heif",
27
- "tif",
28
- "tiff"
29
- ]);
30
- var MIME_SHAPE = /^[a-z0-9][\w.+-]*\/[\w.+-]+/i;
31
- function urlOf(item) {
32
- return typeof item.url === "string" ? item.url : "";
33
- }
34
- function itemMime(item) {
35
- const record = item;
36
- for (const key of ["mimeType", "mime_type", "type"]) {
37
- const value = record[key];
38
- if (typeof value === "string" && MIME_SHAPE.test(value.trim())) return value.trim().toLowerCase();
39
- }
40
- return null;
41
- }
42
- function extensionOfPath(path) {
43
- const segment = path.slice(Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")) + 1);
44
- const dot = segment.lastIndexOf(".");
45
- if (dot <= 0) return null;
46
- const ext = segment.slice(dot + 1).toLowerCase();
47
- return /^[a-z0-9]{1,10}$/.test(ext) ? ext : null;
48
- }
49
- function extensionOfUrl(url) {
50
- if (/^(blob|data):/i.test(url.trim())) return null;
51
- return extensionOfPath(url.split(/[?#]/, 1)[0]);
52
- }
53
- function dataUrlMime(url) {
54
- const match = /^data:([^;,]+)/i.exec(url.trim());
55
- return match && MIME_SHAPE.test(match[1]) ? match[1].toLowerCase() : null;
56
- }
57
- function itemLooksLikeImage(item, fileType) {
58
- const mime = itemMime(item) ?? (fileType && MIME_SHAPE.test(fileType) ? fileType.toLowerCase() : null) ?? dataUrlMime(urlOf(item));
59
- if (mime) return mime.startsWith("image/");
60
- const ext = extensionOfUrl(urlOf(item)) ?? (item.name ? extensionOfPath(item.name) : null);
61
- if (ext) return IMAGE_EXTENSIONS.has(ext);
62
- return true;
63
- }
64
- function fileBadgeOf(item, fileType) {
65
- const ext = (item.name ? extensionOfPath(item.name) : null) ?? extensionOfUrl(urlOf(item));
66
- if (ext) return ext.toUpperCase();
67
- const mime = itemMime(item) ?? (fileType && MIME_SHAPE.test(fileType) ? fileType.toLowerCase() : null);
68
- const subtype = mime?.split("/")[1]?.split(/[;+]/)[0];
69
- return subtype && /^[a-z0-9]{1,5}$/.test(subtype) ? subtype.toUpperCase() : null;
70
- }
71
- function displayNameOf(item) {
72
- if (item.name && item.name.trim() !== "") return item.name;
73
- const url = urlOf(item);
74
- if (!/^(blob|data):/i.test(url.trim())) {
75
- const path = url.split(/[?#]/, 1)[0];
76
- const segment = path.slice(path.lastIndexOf("/") + 1);
77
- if (segment !== "") {
78
- try {
79
- return decodeURIComponent(segment);
80
- } catch {
81
- return segment;
82
- }
83
- }
84
- }
85
- return "\uCCA8\uBD80 \uD30C\uC77C";
86
- }
87
- function safeFileHref(url) {
88
- const value = url.replace(/[\u0000-\u0020]/g, "");
89
- if (value === "") return null;
90
- const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value);
91
- if (!scheme) return url;
92
- return /^(https?|blob)$/i.test(scheme[1]) ? url : null;
93
- }
94
10
  function acceptIsImageOnly(accept) {
95
11
  const rules = accept.split(",").map((rule) => rule.trim().toLowerCase()).filter(Boolean);
96
12
  if (rules.length === 0) return false;
@@ -421,7 +337,7 @@ function FileDropzoneImpl({
421
337
  ) : null,
422
338
  items.length > 0 ? /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "var(--wp-space-2)" }, children: items.map((item) => {
423
339
  const fileType = fileTypesRef.current.get(item.id);
424
- const asImage = itemLooksLikeImage(item, fileType);
340
+ const asImage = looksLikeImage(item, fileType);
425
341
  const resolved = resolveAsset(item.url, {
426
342
  component: "FileDropzone",
427
343
  prop: asImage ? "src" : "href",
@@ -449,6 +365,7 @@ function FileDropzoneImpl({
449
365
  {
450
366
  src: resolved,
451
367
  alt: item.name,
368
+ mimeType: fileMimeOf(item) ?? fileType,
452
369
  thumbStyle: {
453
370
  width: THUMB_SIZE,
454
371
  height: THUMB_SIZE,
@@ -460,75 +377,15 @@ function FileDropzoneImpl({
460
377
  }
461
378
  );
462
379
  } else {
463
- const href = sandbox || resolved === null ? null : safeFileHref(resolved);
464
- const displayName = displayNameOf(item);
465
- const badge = fileBadgeOf(item, fileType);
466
- const tileStyle = {
467
- display: "flex",
468
- alignItems: "center",
469
- gap: "var(--wp-space-2)",
470
- boxSizing: "border-box",
471
- width: FILE_TILE_WIDTH,
472
- height: THUMB_SIZE,
473
- padding: "0 var(--wp-space-2)",
474
- borderRadius: "var(--wp-radius-md)",
475
- border: `var(--wp-border-w) solid ${hovered && href ? "var(--wp-border-strong)" : "var(--wp-border-subtle)"}`,
476
- background: "var(--wp-surface-sunken)",
477
- color: "var(--wp-text-default)",
478
- textDecoration: "none",
479
- transition: "var(--wp-transition-surface)"
480
- };
481
- const tileBody = /* @__PURE__ */ jsxs(Fragment, { children: [
482
- /* @__PURE__ */ jsxs(
483
- "span",
484
- {
485
- style: {
486
- display: "flex",
487
- flexDirection: "column",
488
- alignItems: "center",
489
- gap: 2,
490
- flex: "none",
491
- minWidth: 28,
492
- color: "var(--wp-text-muted)"
493
- },
494
- children: [
495
- /* @__PURE__ */ jsx(Icon, { name: "file-text", size: 24 }),
496
- badge ? /* @__PURE__ */ jsx("span", { className: "wp-overline", style: { lineHeight: 1 }, children: badge }) : null
497
- ]
498
- }
499
- ),
500
- /* @__PURE__ */ jsx(
501
- "span",
502
- {
503
- className: "wp-caption",
504
- style: {
505
- flex: 1,
506
- minWidth: 0,
507
- color: "var(--wp-text-default)",
508
- display: "-webkit-box",
509
- WebkitLineClamp: 2,
510
- WebkitBoxOrient: "vertical",
511
- overflow: "hidden",
512
- wordBreak: "break-all"
513
- },
514
- children: displayName
515
- }
516
- )
517
- ] });
518
- preview = href ? /* @__PURE__ */ jsx(
519
- "a",
380
+ preview = /* @__PURE__ */ jsx(
381
+ FileTile,
520
382
  {
521
- href,
522
- target: "_blank",
523
- rel: "noopener",
524
- title: displayName,
525
- "aria-label": `${displayName}${badge ? ` (${badge})` : ""} \u2014 \uC0C8 \uD0ED\uC5D0\uC11C \uC5F4\uAE30`,
526
- "data-file-tile": "",
527
- style: tileStyle,
528
- onClick: (event) => event.stopPropagation(),
529
- children: tileBody
383
+ href: sandbox || resolved === null ? null : safeFileHref(resolved),
384
+ name: fileDisplayNameOf(item),
385
+ badge: fileBadgeOf(item, fileType),
386
+ highlighted: hovered
530
387
  }
531
- ) : /* @__PURE__ */ jsx("span", { title: displayName, "data-file-tile": "", style: tileStyle, children: tileBody });
388
+ );
532
389
  }
533
390
  return /* @__PURE__ */ jsxs(
534
391
  "div",
@@ -1,9 +1,200 @@
1
1
  import { useOverlayLayer, OverlayContainerContext, getOverlayRoot } from './chunk-R2ORCOTG.js';
2
- import { IconButton } from './chunk-45GIMFRP.js';
3
- import { useState, useRef, useCallback, useEffect, useContext } from 'react';
2
+ import { useSduiSandbox } from './chunk-DE7FPWVJ.js';
3
+ import { IconButton, Icon } from './chunk-45GIMFRP.js';
4
+ import React, { useState, useRef, useCallback, useEffect, useContext } from 'react';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
6
  import { createPortal } from 'react-dom';
5
- import { jsxs, jsx } from 'react/jsx-runtime';
6
7
 
8
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
9
+ "jpg",
10
+ "jpeg",
11
+ "jpe",
12
+ "jfif",
13
+ "pjpeg",
14
+ "pjp",
15
+ "png",
16
+ "apng",
17
+ "gif",
18
+ "webp",
19
+ "avif",
20
+ "bmp",
21
+ "svg",
22
+ "ico",
23
+ "heic",
24
+ "heif",
25
+ "tif",
26
+ "tiff"
27
+ ]);
28
+ var MIME_SHAPE = /^[a-z0-9][\w.+-]*\/[\w.+-]+/i;
29
+ function asMime(value) {
30
+ return typeof value === "string" && MIME_SHAPE.test(value.trim()) ? value.trim().toLowerCase() : null;
31
+ }
32
+ function urlOf(item) {
33
+ return typeof item.url === "string" ? item.url : "";
34
+ }
35
+ function nameOf(item) {
36
+ return typeof item.name === "string" && item.name.trim() !== "" ? item.name : null;
37
+ }
38
+ function fileMimeOf(item) {
39
+ return asMime(item.mimeType) ?? asMime(item.mime_type) ?? asMime(item.type);
40
+ }
41
+ function extensionOfPath(path) {
42
+ const segment = path.slice(Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")) + 1);
43
+ const dot = segment.lastIndexOf(".");
44
+ if (dot <= 0) return null;
45
+ const ext = segment.slice(dot + 1).toLowerCase();
46
+ return /^[a-z0-9]{1,10}$/.test(ext) ? ext : null;
47
+ }
48
+ function extensionOfUrl(url) {
49
+ if (/^(blob|data):/i.test(url.trim())) return null;
50
+ return extensionOfPath(url.split(/[?#]/, 1)[0]);
51
+ }
52
+ function dataUrlMime(url) {
53
+ const match = /^data:([^;,]+)/i.exec(url.trim());
54
+ return match ? asMime(match[1]) : null;
55
+ }
56
+ function looksLikeImage(item, fileType) {
57
+ const mime = fileMimeOf(item) ?? asMime(fileType) ?? dataUrlMime(urlOf(item));
58
+ if (mime) return mime.startsWith("image/");
59
+ const name = nameOf(item);
60
+ const ext = extensionOfUrl(urlOf(item)) ?? (name ? extensionOfPath(name) : null);
61
+ if (ext) return IMAGE_EXTENSIONS.has(ext);
62
+ return true;
63
+ }
64
+ function fileBadgeOf(item, fileType) {
65
+ const name = nameOf(item);
66
+ const ext = (name ? extensionOfPath(name) : null) ?? extensionOfUrl(urlOf(item));
67
+ if (ext) return ext.toUpperCase();
68
+ const mime = fileMimeOf(item) ?? asMime(fileType);
69
+ const subtype = mime?.split("/")[1]?.split(/[;+]/)[0];
70
+ return subtype && /^[a-z0-9]{1,5}$/.test(subtype) ? subtype.toUpperCase() : null;
71
+ }
72
+ function fileDisplayNameOf(item) {
73
+ const name = nameOf(item);
74
+ if (name) return name;
75
+ const url = urlOf(item);
76
+ if (!/^(blob|data):/i.test(url.trim())) {
77
+ const path = url.split(/[?#]/, 1)[0];
78
+ const segment = path.slice(path.lastIndexOf("/") + 1);
79
+ if (segment !== "") {
80
+ try {
81
+ return decodeURIComponent(segment);
82
+ } catch {
83
+ return segment;
84
+ }
85
+ }
86
+ }
87
+ return "\uCCA8\uBD80 \uD30C\uC77C";
88
+ }
89
+ function safeFileHref(url) {
90
+ const value = url.replace(/[\u0000- ]/g, "");
91
+ if (value === "") return null;
92
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value);
93
+ if (!scheme) return url;
94
+ return /^(https?|blob)$/i.test(scheme[1]) ? url : null;
95
+ }
96
+ var FILE_TILE_WIDTH = 168;
97
+ var FILE_TILE_HEIGHT = 72;
98
+ function FileTile({
99
+ href,
100
+ name,
101
+ badge,
102
+ layout = "row",
103
+ highlighted,
104
+ style,
105
+ className
106
+ }) {
107
+ const [ownHover, setOwnHover] = React.useState(false);
108
+ const hovered = highlighted ?? ownHover;
109
+ const tile = layout === "tile";
110
+ const tileStyle = {
111
+ display: "flex",
112
+ flexDirection: tile ? "column" : void 0,
113
+ alignItems: "center",
114
+ justifyContent: tile ? "center" : void 0,
115
+ gap: tile ? 2 : "var(--wp-space-2)",
116
+ boxSizing: "border-box",
117
+ width: FILE_TILE_WIDTH,
118
+ height: FILE_TILE_HEIGHT,
119
+ padding: tile ? "var(--wp-space-1)" : "0 var(--wp-space-2)",
120
+ borderRadius: "var(--wp-radius-md)",
121
+ border: `var(--wp-border-w) solid ${hovered && href ? "var(--wp-border-strong)" : "var(--wp-border-subtle)"}`,
122
+ background: "var(--wp-surface-sunken)",
123
+ color: tile ? "var(--wp-text-muted)" : "var(--wp-text-default)",
124
+ textDecoration: "none",
125
+ transition: "var(--wp-transition-surface)",
126
+ ...style
127
+ };
128
+ const mark = /* @__PURE__ */ jsxs(Fragment, { children: [
129
+ /* @__PURE__ */ jsx(Icon, { name: "file-text", size: tile ? 28 : 24 }),
130
+ badge ? /* @__PURE__ */ jsx("span", { className: "wp-overline", style: { lineHeight: 1 }, children: badge }) : null
131
+ ] });
132
+ const body = tile ? mark : /* @__PURE__ */ jsxs(Fragment, { children: [
133
+ /* @__PURE__ */ jsx(
134
+ "span",
135
+ {
136
+ style: {
137
+ display: "flex",
138
+ flexDirection: "column",
139
+ alignItems: "center",
140
+ gap: 2,
141
+ flex: "none",
142
+ minWidth: 28,
143
+ color: "var(--wp-text-muted)"
144
+ },
145
+ children: mark
146
+ }
147
+ ),
148
+ /* @__PURE__ */ jsx(
149
+ "span",
150
+ {
151
+ className: "wp-caption",
152
+ style: {
153
+ flex: 1,
154
+ minWidth: 0,
155
+ color: "var(--wp-text-default)",
156
+ display: "-webkit-box",
157
+ WebkitLineClamp: 2,
158
+ WebkitBoxOrient: "vertical",
159
+ overflow: "hidden",
160
+ wordBreak: "break-all"
161
+ },
162
+ children: name
163
+ }
164
+ )
165
+ ] });
166
+ const hoverProps = highlighted === void 0 ? { onMouseEnter: () => setOwnHover(true), onMouseLeave: () => setOwnHover(false) } : {};
167
+ if (href) {
168
+ return /* @__PURE__ */ jsx(
169
+ "a",
170
+ {
171
+ href,
172
+ target: "_blank",
173
+ rel: "noopener",
174
+ title: name,
175
+ "aria-label": `${name}${badge ? ` (${badge})` : ""} \u2014 \uC0C8 \uD0ED\uC5D0\uC11C \uC5F4\uAE30`,
176
+ "data-file-tile": "",
177
+ className,
178
+ style: tileStyle,
179
+ onClick: (event) => event.stopPropagation(),
180
+ ...hoverProps,
181
+ children: body
182
+ }
183
+ );
184
+ }
185
+ return /* @__PURE__ */ jsx(
186
+ "span",
187
+ {
188
+ title: name,
189
+ "data-file-tile": "",
190
+ className,
191
+ style: tileStyle,
192
+ role: tile ? "img" : void 0,
193
+ "aria-label": tile ? `${name}${badge ? ` (${badge})` : ""}` : void 0,
194
+ children: body
195
+ }
196
+ );
197
+ }
7
198
  var MIN_SCALE = 1;
8
199
  var MAX_SCALE = 8;
9
200
  var BUTTON_ZOOM_STEP = 1.5;
@@ -15,6 +206,10 @@ function clamp(value, min, max) {
15
206
  function ImageViewer({ src, alt, open, onClose }) {
16
207
  const [view, setView] = useState(INITIAL_VIEW);
17
208
  const [dragging, setDragging] = useState(false);
209
+ const [failedSrc, setFailedSrc] = useState(null);
210
+ const sandbox = useSduiSandbox();
211
+ const source = typeof src === "string" ? src : "";
212
+ const showImage = looksLikeImage({ url: source }) && failedSrc !== source;
18
213
  const rootRef = useRef(null);
19
214
  const stageRef = useRef(null);
20
215
  const imgRef = useRef(null);
@@ -40,7 +235,7 @@ function ImageViewer({ src, alt, open, onClose }) {
40
235
  const zoomAt = useCallback(
41
236
  (clientX, clientY, factor) => {
42
237
  const stage = stageRef.current;
43
- if (!stage) return;
238
+ if (!stage || !imgRef.current) return;
44
239
  const prev = viewRef.current;
45
240
  const nextScale = clamp(prev.scale * factor, MIN_SCALE, MAX_SCALE);
46
241
  if (nextScale === prev.scale) return;
@@ -180,13 +375,14 @@ function ImageViewer({ src, alt, open, onClose }) {
180
375
  onPointerMove: onStagePointerMove,
181
376
  onPointerUp: endDrag,
182
377
  onPointerCancel: endDrag,
183
- children: /* @__PURE__ */ jsx(
378
+ children: showImage ? /* @__PURE__ */ jsx(
184
379
  "img",
185
380
  {
186
381
  ref: imgRef,
187
382
  src,
188
383
  alt: alt ?? "",
189
384
  draggable: false,
385
+ onError: () => setFailedSrc(source),
190
386
  style: {
191
387
  maxWidth: "calc(100vw - 96px)",
192
388
  maxHeight: "calc(100vh - 96px)",
@@ -196,6 +392,12 @@ function ImageViewer({ src, alt, open, onClose }) {
196
392
  borderRadius: view.scale === 1 ? "var(--wp-radius-sm)" : 0
197
393
  }
198
394
  }
395
+ ) : /* @__PURE__ */ jsx(
396
+ ViewerFileNotice,
397
+ {
398
+ name: alt && alt.trim() !== "" ? alt : fileDisplayNameOf({ url: source }),
399
+ href: sandbox ? null : safeFileHref(source)
400
+ }
199
401
  )
200
402
  }
201
403
  ),
@@ -238,7 +440,7 @@ function ImageViewer({ src, alt, open, onClose }) {
238
440
  icon: "zoom-out",
239
441
  label: "\uCD95\uC18C",
240
442
  size: "sm",
241
- disabled: view.scale <= MIN_SCALE,
443
+ disabled: !showImage || view.scale <= MIN_SCALE,
242
444
  onClick: () => zoomAtCenter(1 / BUTTON_ZOOM_STEP)
243
445
  }
244
446
  ),
@@ -248,7 +450,7 @@ function ImageViewer({ src, alt, open, onClose }) {
248
450
  icon: "zoom-in",
249
451
  label: "\uD655\uB300",
250
452
  size: "sm",
251
- disabled: view.scale >= MAX_SCALE,
453
+ disabled: !showImage || view.scale >= MAX_SCALE,
252
454
  onClick: () => zoomAtCenter(BUTTON_ZOOM_STEP)
253
455
  }
254
456
  ),
@@ -272,5 +474,40 @@ function ImageViewer({ src, alt, open, onClose }) {
272
474
  portalRoot
273
475
  );
274
476
  }
477
+ function ViewerFileNotice({ name, href }) {
478
+ return /* @__PURE__ */ jsxs(
479
+ "div",
480
+ {
481
+ role: "status",
482
+ style: {
483
+ display: "flex",
484
+ flexDirection: "column",
485
+ alignItems: "center",
486
+ gap: "var(--wp-space-2)",
487
+ maxWidth: "min(360px, calc(100vw - 48px))",
488
+ padding: "var(--wp-space-6)",
489
+ background: "var(--wp-surface-card)",
490
+ borderRadius: "var(--wp-radius-lg)",
491
+ boxShadow: "var(--wp-shadow-xl)",
492
+ textAlign: "center"
493
+ },
494
+ children: [
495
+ /* @__PURE__ */ jsx(Icon, { name: "file-text", size: 32, style: { color: "var(--wp-text-muted)" } }),
496
+ /* @__PURE__ */ jsx("span", { className: "wp-body-sm", style: { color: "var(--wp-text-default)", wordBreak: "break-all" }, children: name }),
497
+ /* @__PURE__ */ jsx("span", { className: "wp-caption", children: "\uC774\uBBF8\uC9C0\uB85C \uD45C\uC2DC\uD560 \uC218 \uC5C6\uB294 \uD30C\uC77C\uC785\uB2C8\uB2E4" }),
498
+ href ? /* @__PURE__ */ jsx(
499
+ "a",
500
+ {
501
+ href,
502
+ target: "_blank",
503
+ rel: "noopener",
504
+ className: "wp-btn wp-btn--secondary wp-btn--sm",
505
+ children: "\uC0C8 \uD0ED\uC5D0\uC11C \uC5F4\uAE30"
506
+ }
507
+ ) : null
508
+ ]
509
+ }
510
+ );
511
+ }
275
512
 
276
- export { ImageViewer };
513
+ export { FILE_TILE_WIDTH, FileTile, IMAGE_EXTENSIONS, ImageViewer, fileBadgeOf, fileDisplayNameOf, fileMimeOf, looksLikeImage, safeFileHref };
@@ -0,0 +1,130 @@
1
+ import { looksLikeImage, safeFileHref, fileDisplayNameOf, FileTile, fileBadgeOf, ImageViewer } from './chunk-EKNEBGNW.js';
2
+ import { useSduiSandbox } from './chunk-DE7FPWVJ.js';
3
+ import { boxed } from './chunk-3JTBOABA.js';
4
+ import { useState } from 'react';
5
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
6
+
7
+ var BOX_KEYS = [
8
+ "width",
9
+ "height",
10
+ "minWidth",
11
+ "minHeight",
12
+ "maxWidth",
13
+ "maxHeight",
14
+ "borderRadius",
15
+ "margin",
16
+ "marginTop",
17
+ "marginRight",
18
+ "marginBottom",
19
+ "marginLeft",
20
+ "flex",
21
+ "flexGrow",
22
+ "flexShrink",
23
+ "flexBasis",
24
+ "alignSelf"
25
+ ];
26
+ function tileBoxOf(thumbStyle) {
27
+ if (!thumbStyle || thumbStyle.width === void 0 && thumbStyle.height === void 0) return null;
28
+ const box = {};
29
+ for (const key of BOX_KEYS) {
30
+ if (thumbStyle[key] !== void 0) box[key] = thumbStyle[key];
31
+ }
32
+ if (box.width === void 0) box.minWidth ??= 72;
33
+ if (box.height === void 0) box.minHeight ??= 72;
34
+ return box;
35
+ }
36
+ function ImageLightboxImpl({
37
+ src,
38
+ alt,
39
+ mimeType,
40
+ thumbClassName,
41
+ thumbStyle,
42
+ children
43
+ }) {
44
+ const [open, setOpen] = useState(false);
45
+ const [failedSrc, setFailedSrc] = useState(null);
46
+ const sandbox = useSduiSandbox();
47
+ const source = typeof src === "string" ? src : "";
48
+ const item = { url: source, mimeType };
49
+ const asImage = looksLikeImage(item) && failedSrc !== source;
50
+ const href = sandbox ? null : safeFileHref(source);
51
+ const openViewer = (event) => {
52
+ event.stopPropagation();
53
+ if (asImage) {
54
+ setOpen(true);
55
+ return;
56
+ }
57
+ if (href && typeof window !== "undefined") window.open(href, "_blank", "noopener");
58
+ };
59
+ const overlay = asImage ? /* @__PURE__ */ jsx(
60
+ ImageViewer,
61
+ {
62
+ src,
63
+ alt,
64
+ open,
65
+ onClose: () => setOpen(false)
66
+ }
67
+ ) : null;
68
+ if (children) {
69
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
70
+ /* @__PURE__ */ jsx(
71
+ "span",
72
+ {
73
+ style: { display: "contents" },
74
+ onClick: openViewer,
75
+ onKeyDown: (event) => {
76
+ if (event.key === "Enter" || event.key === " ") {
77
+ event.preventDefault();
78
+ openViewer(event);
79
+ }
80
+ },
81
+ children
82
+ }
83
+ ),
84
+ overlay
85
+ ] });
86
+ }
87
+ if (!asImage) {
88
+ const box = tileBoxOf(thumbStyle);
89
+ const name = alt && alt.trim() !== "" ? alt : fileDisplayNameOf(item);
90
+ return /* @__PURE__ */ jsx(
91
+ FileTile,
92
+ {
93
+ href,
94
+ name,
95
+ badge: fileBadgeOf(item),
96
+ layout: box ? "tile" : "row",
97
+ style: box ?? void 0,
98
+ className: thumbClassName
99
+ }
100
+ );
101
+ }
102
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
103
+ /* @__PURE__ */ jsx(
104
+ "img",
105
+ {
106
+ src,
107
+ alt: alt ?? "",
108
+ role: "button",
109
+ tabIndex: 0,
110
+ className: thumbClassName,
111
+ style: { cursor: "zoom-in", ...thumbStyle },
112
+ onClick: openViewer,
113
+ onKeyDown: (event) => {
114
+ if (event.key === "Enter" || event.key === " ") {
115
+ event.preventDefault();
116
+ openViewer(event);
117
+ }
118
+ },
119
+ onError: () => setFailedSrc(source)
120
+ }
121
+ ),
122
+ overlay
123
+ ] });
124
+ }
125
+ var ImageLightbox = boxed(
126
+ ImageLightboxImpl,
127
+ "ImageLightbox"
128
+ );
129
+
130
+ export { ImageLightbox };
@@ -1,9 +1,10 @@
1
1
  import { ellipsisStyle, unwrapRows } from './chunk-HR2U2QGN.js';
2
2
  import { includesLooseSearch } from './chunk-QDCP6P3K.js';
3
+ import { useDismiss, usePopoverAnchor, usePortalTo, usePanelPointer, AnchoredPopoverStyles, focusStaysInside, anchorStyle } from './chunk-HNMBEUBO.js';
3
4
  import { useSduiAdapter, hostFetchOf } from './chunk-DE7FPWVJ.js';
4
5
  import { Icon, Input, Spinner, Button } from './chunk-45GIMFRP.js';
5
6
  import { boxed } from './chunk-3JTBOABA.js';
6
- import { useId, useRef, useMemo, useState, useEffect } from 'react';
7
+ import { useId, useRef, useMemo, useState, useEffect, useCallback } from 'react';
7
8
  import { jsxs, jsx } from 'react/jsx-runtime';
8
9
 
9
10
  function endpointSearchFn(endpoint, searchParam, hostFetch) {
@@ -32,6 +33,20 @@ function pickText(option, key) {
32
33
  var statusRowStyle = {
33
34
  padding: "var(--wp-space-2) var(--wp-space-3)"
34
35
  };
36
+ var LIST_MAX_HEIGHT = 280;
37
+ var LIST_ESTIMATE = LIST_MAX_HEIGHT + 10;
38
+ function listPopStyle(anchor) {
39
+ const base = anchorStyle(anchor);
40
+ if (!base || !anchor) return base;
41
+ return { ...base, width: anchor.minWidth, minWidth: 0, maxWidth: "calc(100vw - 16px)", boxSizing: "border-box" };
42
+ }
43
+ var listBoxStyle = {
44
+ maxHeight: `min(${LIST_MAX_HEIGHT}px, var(--wp-pop-avail, ${LIST_MAX_HEIGHT}px))`,
45
+ overflowY: "auto",
46
+ overscrollBehavior: "contain",
47
+ padding: "var(--wp-space-1)"
48
+ };
49
+ var keepFocusInField = (event) => event.preventDefault();
35
50
  function optionRowStyle(active) {
36
51
  return {
37
52
  display: "block",
@@ -102,6 +117,7 @@ function AutocompleteFieldImpl({
102
117
  const [activeIndex, setActiveIndex] = useState(0);
103
118
  const [focusPending, setFocusPending] = useState(false);
104
119
  const listRef = useRef(null);
120
+ const popRef = useRef(null);
105
121
  const [typedSeq, setTypedSeq] = useState(0);
106
122
  const [ownLinked, setOwnLinked] = useState(false);
107
123
  const linked = freeText && (selected ?? ownLinked);
@@ -109,6 +125,17 @@ function AutocompleteFieldImpl({
109
125
  useEffect(() => {
110
126
  if (linked) unlinkSentRef.current = false;
111
127
  }, [linked]);
128
+ const dismiss = useCallback(() => setOpen(false), []);
129
+ useDismiss(open, hostRef, popRef, dismiss);
130
+ const anchor = usePopoverAnchor(open, hostRef, popRef, LIST_ESTIMATE);
131
+ const portal = usePortalTo();
132
+ const pointer = usePanelPointer(popRef);
133
+ const anchored = anchor !== null;
134
+ const handleHostBlur = (event) => {
135
+ if (!open) return;
136
+ if (focusStaysInside(event.relatedTarget, hostRef, popRef, pointer)) return;
137
+ setOpen(false);
138
+ };
112
139
  useEffect(() => {
113
140
  if (!open) return;
114
141
  const box = listRef.current;
@@ -118,7 +145,7 @@ function AutocompleteFieldImpl({
118
145
  const bottom = top + active.offsetHeight;
119
146
  if (top < box.scrollTop) box.scrollTop = top;
120
147
  else if (bottom > box.scrollTop + box.clientHeight) box.scrollTop = bottom - box.clientHeight;
121
- }, [open, activeIndex, listId, results]);
148
+ }, [open, anchored, activeIndex, listId, results]);
122
149
  useEffect(() => {
123
150
  if (disabled || cardMode) return;
124
151
  const keyword = query.trim();
@@ -162,22 +189,6 @@ function AutocompleteFieldImpl({
162
189
  controller.abort();
163
190
  };
164
191
  }, [query, typedSeq, hasSearch, labelKey, subKey, minChars, debounceMs, disabled, cardMode]);
165
- useEffect(() => {
166
- if (!open) return;
167
- const closeOnOutsideClick = (event) => {
168
- if (event.target instanceof Node && hostRef.current?.contains(event.target)) return;
169
- setOpen(false);
170
- };
171
- const closeOnEscape = (event) => {
172
- if (event.key === "Escape") setOpen(false);
173
- };
174
- document.addEventListener("pointerdown", closeOnOutsideClick);
175
- document.addEventListener("keydown", closeOnEscape);
176
- return () => {
177
- document.removeEventListener("pointerdown", closeOnOutsideClick);
178
- document.removeEventListener("keydown", closeOnEscape);
179
- };
180
- }, [open]);
181
192
  useEffect(() => {
182
193
  if (!focusPending || hasSelection) return;
183
194
  hostRef.current?.querySelector("input")?.focus();
@@ -266,7 +277,6 @@ function AutocompleteFieldImpl({
266
277
  }
267
278
  return;
268
279
  }
269
- if (event.key === "Escape") setOpen(false);
270
280
  };
271
281
  const handleKeyDown = (event) => {
272
282
  if (event.key === "ArrowDown" || event.key === "ArrowUp") {
@@ -285,7 +295,6 @@ function AutocompleteFieldImpl({
285
295
  if (active) selectOption(active);
286
296
  return;
287
297
  }
288
- if (event.key === "Escape") setOpen(false);
289
298
  };
290
299
  const staticMatch = useMemo(() => {
291
300
  if (!hasSelection) return null;
@@ -297,7 +306,8 @@ function AutocompleteFieldImpl({
297
306
  const cardSub = cardOption ? pickText(cardOption, subKey) : "";
298
307
  if (freeText) {
299
308
  const createText = freeTextCreateLabel(createLabel, freeTextValue);
300
- return /* @__PURE__ */ jsxs("div", { ref: hostRef, className: "erp-popover-host", children: [
309
+ return /* @__PURE__ */ jsxs("div", { ref: hostRef, className: "erp-popover-host", onBlur: handleHostBlur, children: [
310
+ /* @__PURE__ */ jsx(AnchoredPopoverStyles, {}),
301
311
  /* @__PURE__ */ jsxs("div", { className: "wp-inputwrap wp-inputwrap--lead", children: [
302
312
  /* @__PURE__ */ jsx(
303
313
  "span",
@@ -325,65 +335,74 @@ function AutocompleteFieldImpl({
325
335
  }
326
336
  )
327
337
  ] }),
328
- open ? /* @__PURE__ */ jsx("div", { className: "erp-popover erp-popover--left", style: { width: "100%" }, children: /* @__PURE__ */ jsxs(
329
- "div",
330
- {
331
- id: listId,
332
- ref: listRef,
333
- role: "listbox",
334
- className: "erp-popover__body",
335
- style: {
336
- maxHeight: 280,
337
- overflowY: "auto",
338
- overscrollBehavior: "contain",
339
- padding: "var(--wp-space-1)"
340
- },
341
- onMouseDown: (event) => event.preventDefault(),
342
- children: [
343
- /* @__PURE__ */ jsx(
344
- "button",
338
+ open ? portal(
339
+ anchor,
340
+ /* @__PURE__ */ jsx(
341
+ "div",
342
+ {
343
+ ref: popRef,
344
+ className: "wp-anchored-pop",
345
+ style: listPopStyle(anchor),
346
+ ...pointer.panelProps,
347
+ onMouseDown: keepFocusInField,
348
+ children: /* @__PURE__ */ jsxs(
349
+ "div",
345
350
  {
346
- id: `${listId}-opt-0`,
347
- type: "button",
348
- role: "option",
349
- "aria-selected": activeIndex === 0,
350
- "data-create-row": "",
351
- onClick: () => chooseFreeRow(0),
352
- onPointerEnter: () => setActiveIndex(0),
353
- style: optionRowStyle(activeIndex === 0),
354
- children: /* @__PURE__ */ jsx("span", { className: "wp-body-sm", style: ellipsisStyle, children: createText })
351
+ id: listId,
352
+ ref: listRef,
353
+ role: "listbox",
354
+ className: "erp-popover__body",
355
+ style: listBoxStyle,
356
+ children: [
357
+ /* @__PURE__ */ jsx(
358
+ "button",
359
+ {
360
+ id: `${listId}-opt-0`,
361
+ type: "button",
362
+ tabIndex: -1,
363
+ role: "option",
364
+ "aria-selected": activeIndex === 0,
365
+ "data-create-row": "",
366
+ onClick: () => chooseFreeRow(0),
367
+ onPointerEnter: () => setActiveIndex(0),
368
+ style: optionRowStyle(activeIndex === 0),
369
+ children: /* @__PURE__ */ jsx("span", { className: "wp-body-sm", style: ellipsisStyle, children: createText })
370
+ }
371
+ ),
372
+ status === "loading" ? /* @__PURE__ */ jsx("div", { className: "wp-row", style: statusRowStyle, children: /* @__PURE__ */ jsx(Spinner, { size: "sm", label: "\uAC80\uC0C9 \uC911\u2026" }) }) : status === "offline" ? /* @__PURE__ */ jsxs("div", { className: "wp-row", style: statusRowStyle, children: [
373
+ /* @__PURE__ */ jsx(Icon, { name: "unplug", size: 16, style: { color: "var(--wp-text-muted)", flex: "none" } }),
374
+ /* @__PURE__ */ jsx("span", { className: "wp-caption", children: "\uBC31\uC5D4\uB4DC \uBBF8\uC5F0\uB3D9 \u2014 \uAC80\uC0C9 \uACB0\uACFC\uB97C \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4" })
375
+ ] }) : status === "error" ? /* @__PURE__ */ jsxs("div", { className: "wp-row", style: statusRowStyle, children: [
376
+ /* @__PURE__ */ jsx(Icon, { name: "circle-alert", size: 16, style: { color: "var(--wp-text-danger)", flex: "none" } }),
377
+ /* @__PURE__ */ jsx("span", { className: "wp-caption", children: "\uAC80\uC0C9 \uC2E4\uD328 \u2014 \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694" })
378
+ ] }) : status === "ready" && results.length === 0 ? /* @__PURE__ */ jsx("div", { className: "wp-caption", style: { ...statusRowStyle, color: "var(--wp-text-subtle)" }, children: "\uC77C\uCE58\uD558\uB294 \uD56D\uBAA9 \uC5C6\uC74C" }) : status === "ready" ? results.map((option, index) => {
379
+ const row = index + 1;
380
+ const optionSub = pickText(option, subKey);
381
+ return /* @__PURE__ */ jsxs(
382
+ "button",
383
+ {
384
+ id: `${listId}-opt-${row}`,
385
+ type: "button",
386
+ tabIndex: -1,
387
+ role: "option",
388
+ "aria-selected": row === activeIndex,
389
+ onClick: () => chooseFreeRow(row),
390
+ onPointerEnter: () => setActiveIndex(row),
391
+ style: optionRowStyle(row === activeIndex),
392
+ children: [
393
+ /* @__PURE__ */ jsx("span", { className: "wp-body-sm", style: ellipsisStyle, children: pickText(option, labelKey) || pickText(option, valueKey) }),
394
+ optionSub ? /* @__PURE__ */ jsx("span", { className: "wp-caption", style: ellipsisStyle, children: optionSub }) : null
395
+ ]
396
+ },
397
+ `${pickText(option, valueKey)}-${index}`
398
+ );
399
+ }) : null
400
+ ]
355
401
  }
356
- ),
357
- status === "loading" ? /* @__PURE__ */ jsx("div", { className: "wp-row", style: statusRowStyle, children: /* @__PURE__ */ jsx(Spinner, { size: "sm", label: "\uAC80\uC0C9 \uC911\u2026" }) }) : status === "offline" ? /* @__PURE__ */ jsxs("div", { className: "wp-row", style: statusRowStyle, children: [
358
- /* @__PURE__ */ jsx(Icon, { name: "unplug", size: 16, style: { color: "var(--wp-text-muted)", flex: "none" } }),
359
- /* @__PURE__ */ jsx("span", { className: "wp-caption", children: "\uBC31\uC5D4\uB4DC \uBBF8\uC5F0\uB3D9 \u2014 \uAC80\uC0C9 \uACB0\uACFC\uB97C \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4" })
360
- ] }) : status === "error" ? /* @__PURE__ */ jsxs("div", { className: "wp-row", style: statusRowStyle, children: [
361
- /* @__PURE__ */ jsx(Icon, { name: "circle-alert", size: 16, style: { color: "var(--wp-text-danger)", flex: "none" } }),
362
- /* @__PURE__ */ jsx("span", { className: "wp-caption", children: "\uAC80\uC0C9 \uC2E4\uD328 \u2014 \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694" })
363
- ] }) : status === "ready" && results.length === 0 ? /* @__PURE__ */ jsx("div", { className: "wp-caption", style: { ...statusRowStyle, color: "var(--wp-text-subtle)" }, children: "\uC77C\uCE58\uD558\uB294 \uD56D\uBAA9 \uC5C6\uC74C" }) : status === "ready" ? results.map((option, index) => {
364
- const row = index + 1;
365
- const optionSub = pickText(option, subKey);
366
- return /* @__PURE__ */ jsxs(
367
- "button",
368
- {
369
- id: `${listId}-opt-${row}`,
370
- type: "button",
371
- role: "option",
372
- "aria-selected": row === activeIndex,
373
- onClick: () => chooseFreeRow(row),
374
- onPointerEnter: () => setActiveIndex(row),
375
- style: optionRowStyle(row === activeIndex),
376
- children: [
377
- /* @__PURE__ */ jsx("span", { className: "wp-body-sm", style: ellipsisStyle, children: pickText(option, labelKey) || pickText(option, valueKey) }),
378
- optionSub ? /* @__PURE__ */ jsx("span", { className: "wp-caption", style: ellipsisStyle, children: optionSub }) : null
379
- ]
380
- },
381
- `${pickText(option, valueKey)}-${index}`
382
- );
383
- }) : null
384
- ]
385
- }
386
- ) }) : null
402
+ )
403
+ }
404
+ )
405
+ ) : null
387
406
  ] });
388
407
  }
389
408
  if (cardMode) {
@@ -420,7 +439,8 @@ function AutocompleteFieldImpl({
420
439
  );
421
440
  }
422
441
  const showActiveDescendant = open && status === "ready" && results.length > 0;
423
- return /* @__PURE__ */ jsxs("div", { ref: hostRef, className: "erp-popover-host", children: [
442
+ return /* @__PURE__ */ jsxs("div", { ref: hostRef, className: "erp-popover-host", onBlur: handleHostBlur, children: [
443
+ /* @__PURE__ */ jsx(AnchoredPopoverStyles, {}),
424
444
  /* @__PURE__ */ jsx(
425
445
  Input,
426
446
  {
@@ -441,47 +461,56 @@ function AutocompleteFieldImpl({
441
461
  onKeyDown: handleKeyDown
442
462
  }
443
463
  ),
444
- open ? /* @__PURE__ */ jsx("div", { className: "erp-popover erp-popover--left", style: { width: "100%" }, children: /* @__PURE__ */ jsx(
445
- "div",
446
- {
447
- id: listId,
448
- ref: listRef,
449
- role: "listbox",
450
- className: "erp-popover__body",
451
- style: {
452
- maxHeight: 280,
453
- overflowY: "auto",
454
- overscrollBehavior: "contain",
455
- padding: "var(--wp-space-1)"
456
- },
457
- children: status === "loading" ? /* @__PURE__ */ jsx("div", { className: "wp-row", style: statusRowStyle, children: /* @__PURE__ */ jsx(Spinner, { size: "sm", label: "\uAC80\uC0C9 \uC911\u2026" }) }) : status === "offline" ? /* @__PURE__ */ jsxs("div", { className: "wp-row", style: statusRowStyle, children: [
458
- /* @__PURE__ */ jsx(Icon, { name: "unplug", size: 16, style: { color: "var(--wp-text-muted)", flex: "none" } }),
459
- /* @__PURE__ */ jsx("span", { className: "wp-caption", children: "\uBC31\uC5D4\uB4DC \uBBF8\uC5F0\uB3D9 \u2014 \uAC80\uC0C9 \uACB0\uACFC\uB97C \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4" })
460
- ] }) : status === "error" ? /* @__PURE__ */ jsxs("div", { className: "wp-row", style: statusRowStyle, children: [
461
- /* @__PURE__ */ jsx(Icon, { name: "circle-alert", size: 16, style: { color: "var(--wp-text-danger)", flex: "none" } }),
462
- /* @__PURE__ */ jsx("span", { className: "wp-caption", children: "\uAC80\uC0C9 \uC2E4\uD328 \u2014 \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694" })
463
- ] }) : results.length === 0 ? /* @__PURE__ */ jsx("div", { className: "wp-caption", style: statusRowStyle, children: "\uACB0\uACFC \uC5C6\uC74C" }) : results.map((option, index) => {
464
- const optionSub = pickText(option, subKey);
465
- return /* @__PURE__ */ jsxs(
466
- "button",
464
+ open ? portal(
465
+ anchor,
466
+ /* @__PURE__ */ jsx(
467
+ "div",
468
+ {
469
+ ref: popRef,
470
+ className: "wp-anchored-pop",
471
+ style: listPopStyle(anchor),
472
+ ...pointer.panelProps,
473
+ onMouseDown: keepFocusInField,
474
+ children: /* @__PURE__ */ jsx(
475
+ "div",
467
476
  {
468
- id: `${listId}-opt-${index}`,
469
- type: "button",
470
- role: "option",
471
- "aria-selected": index === activeIndex,
472
- onClick: () => selectOption(option),
473
- onPointerEnter: () => setActiveIndex(index),
474
- style: optionRowStyle(index === activeIndex),
475
- children: [
476
- /* @__PURE__ */ jsx("span", { className: "wp-body-sm", style: ellipsisStyle, children: pickText(option, labelKey) || pickText(option, valueKey) }),
477
- optionSub ? /* @__PURE__ */ jsx("span", { className: "wp-caption", style: ellipsisStyle, children: optionSub }) : null
478
- ]
479
- },
480
- `${pickText(option, valueKey)}-${index}`
481
- );
482
- })
483
- }
484
- ) }) : null
477
+ id: listId,
478
+ ref: listRef,
479
+ role: "listbox",
480
+ className: "erp-popover__body",
481
+ style: listBoxStyle,
482
+ children: status === "loading" ? /* @__PURE__ */ jsx("div", { className: "wp-row", style: statusRowStyle, children: /* @__PURE__ */ jsx(Spinner, { size: "sm", label: "\uAC80\uC0C9 \uC911\u2026" }) }) : status === "offline" ? /* @__PURE__ */ jsxs("div", { className: "wp-row", style: statusRowStyle, children: [
483
+ /* @__PURE__ */ jsx(Icon, { name: "unplug", size: 16, style: { color: "var(--wp-text-muted)", flex: "none" } }),
484
+ /* @__PURE__ */ jsx("span", { className: "wp-caption", children: "\uBC31\uC5D4\uB4DC \uBBF8\uC5F0\uB3D9 \u2014 \uAC80\uC0C9 \uACB0\uACFC\uB97C \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4" })
485
+ ] }) : status === "error" ? /* @__PURE__ */ jsxs("div", { className: "wp-row", style: statusRowStyle, children: [
486
+ /* @__PURE__ */ jsx(Icon, { name: "circle-alert", size: 16, style: { color: "var(--wp-text-danger)", flex: "none" } }),
487
+ /* @__PURE__ */ jsx("span", { className: "wp-caption", children: "\uAC80\uC0C9 \uC2E4\uD328 \u2014 \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694" })
488
+ ] }) : results.length === 0 ? /* @__PURE__ */ jsx("div", { className: "wp-caption", style: statusRowStyle, children: "\uACB0\uACFC \uC5C6\uC74C" }) : results.map((option, index) => {
489
+ const optionSub = pickText(option, subKey);
490
+ return /* @__PURE__ */ jsxs(
491
+ "button",
492
+ {
493
+ id: `${listId}-opt-${index}`,
494
+ type: "button",
495
+ tabIndex: -1,
496
+ role: "option",
497
+ "aria-selected": index === activeIndex,
498
+ onClick: () => selectOption(option),
499
+ onPointerEnter: () => setActiveIndex(index),
500
+ style: optionRowStyle(index === activeIndex),
501
+ children: [
502
+ /* @__PURE__ */ jsx("span", { className: "wp-body-sm", style: ellipsisStyle, children: pickText(option, labelKey) || pickText(option, valueKey) }),
503
+ optionSub ? /* @__PURE__ */ jsx("span", { className: "wp-caption", style: ellipsisStyle, children: optionSub }) : null
504
+ ]
505
+ },
506
+ `${pickText(option, valueKey)}-${index}`
507
+ );
508
+ })
509
+ }
510
+ )
511
+ }
512
+ )
513
+ ) : null
485
514
  ] });
486
515
  }
487
516
  var AutocompleteField = boxed(AutocompleteFieldImpl, "AutocompleteField");
@@ -1,6 +1,6 @@
1
1
  import { parseJobPositionRow } from './chunk-XRHR452N.js';
2
2
  import { MIN_ZOOM, usePanZoomCanvas } from './chunk-XDR6HC4U.js';
3
- import { ImageLightbox } from './chunk-S4CQTJC6.js';
3
+ import { ImageLightbox } from './chunk-FLBU7AVR.js';
4
4
  import { useSduiAdapter, hostFetchOf, defaultAdapter } from './chunk-DE7FPWVJ.js';
5
5
  import { useIsTabletDown } from './chunk-TAVKGYH5.js';
6
6
  import { warnOnce, Badge, Button, IconButton, Icon } from './chunk-45GIMFRP.js';
package/dist/index.d.ts CHANGED
@@ -315,6 +315,10 @@ interface ButtonProps extends React__default.ButtonHTMLAttributes<HTMLButtonElem
315
315
  * (그래서 button일 때도 둘 다 붙인다 — 표기가 태그에 따라 달라지지 않게).
316
316
  */
317
317
  as?: 'button' | 'a' | 'span';
318
+ /** `as="a"` 링크의 주소·여는 곳·관계 — 나머지 프롭처럼 태그에 그대로 얹힌다(button 에서는 뜻이 없다). */
319
+ href?: string;
320
+ target?: string;
321
+ rel?: string;
318
322
  /**
319
323
  * 버튼 **안쪽** 내용(아이콘·글자)의 가로 정렬. 기본은 가운데다.
320
324
  *
@@ -2572,6 +2576,10 @@ interface ImageViewerProps {
2572
2576
  * **포털이지만 React 이벤트는 트리거 쪽 트리로 버블한다** — 그래서 오버레이 뿌리에서
2573
2577
  * click·pointerdown·dblclick 전파를 끊는다. 이게 없으면 목록에서 연 뷰어의 사진을
2574
2578
  * 눌렀을 때 그 행의 rowClick(상세 이동)이 함께 발동한다.
2579
+ *
2580
+ * **그림이 아니거나 못 불러오면 깨진 그림 대신 안내 상자**(1.9.2) — 문서 아이콘·이름·"새 탭에서 열기".
2581
+ * 판정은 첨부 공용 규칙(file-kind.tsx `looksLikeImage`, 주소만 본다). 트리거들(ImageLightbox·목록 셀)은
2582
+ * 이미지가 아닌 첨부로 뷰어를 열지 않지만, 확장자 없는 PDF 주소처럼 열어 봐야 아는 경우가 남는다.
2575
2583
  */
2576
2584
  declare function ImageViewer({ src, alt, open, onClose }: ImageViewerProps): React__default.ReactPortal | null;
2577
2585
 
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import { useSduiDispatch, SduiDialog, useSduiCan, useSduiRuntime, SduiDirtyRegis
3
3
  export { SduiDialog, SduiPermissionProvider, useSduiCan, useSduiDispatch, useSduiRuntime } from './chunk-77OM6OQG.js';
4
4
  import { Text, Spacer, Show, Section, Row, PageHead, KeyValueList, Grid, Divider, Col, Actions } from './chunk-PPOQY3V5.js';
5
5
  export { Actions, Col, Divider, Grid, KeyValueList, PageHead, Row, Section, Show, Spacer, Text } from './chunk-PPOQY3V5.js';
6
- import { ImageViewer } from './chunk-7NQBV77N.js';
7
- export { ImageViewer } from './chunk-7NQBV77N.js';
6
+ import { looksLikeImage, ImageViewer } from './chunk-EKNEBGNW.js';
7
+ export { ImageViewer } from './chunk-EKNEBGNW.js';
8
8
  import { normalizeSearchFields, searchValuesFromParams, splitMultiValue, includesLooseSearch, cascadingValuePaths, SearchToolbar, searchFieldKeys, Popover } from './chunk-QDCP6P3K.js';
9
9
  export { SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue } from './chunk-QDCP6P3K.js';
10
10
  import { parseYmd, YMD_RE, TimePicker, DateRangePicker, DatePicker } from './chunk-KMNC3Q4J.js';
@@ -310,7 +310,7 @@ function ImageCellView({
310
310
  const [open, setOpen] = useState(false);
311
311
  const [failed, setFailed] = useState(false);
312
312
  const box = { width, height, borderRadius: "var(--wp-radius-lg)", flex: "none" };
313
- if (!url || failed) {
313
+ if (!url || failed || !looksLikeImage({ url })) {
314
314
  return /* @__PURE__ */ jsx(
315
315
  "span",
316
316
  {
package/dist/meta.js CHANGED
@@ -503,6 +503,15 @@ var CardMeta = {
503
503
  },
504
504
  { name: "selected", kind: "boolean", label: "\uC120\uD0DD\uB428", default: false, bindable: true, group: "appearance" },
505
505
  { name: "onClick", kind: "action", label: "\uD074\uB9AD", group: "behavior" },
506
+ {
507
+ name: "aria-label",
508
+ kind: "string",
509
+ label: "\uC811\uADFC\uC131 \uB77C\uBCA8",
510
+ bindable: true,
511
+ description: '\uCE74\uB4DC \uBFCC\uB9AC(div)\uC5D0 \uADF8\uB300\uB85C \uC5B9\uD78C\uB2E4. \uB204\uB97C \uC218 \uC788\uB294 \uCE74\uB4DC(interactive + onClick \u2192 \uBC84\uD2BC \uC5ED\uD560)\uC758 \uC774\uB984\uC744 \uC81C\uBAA9\uACFC \uB2E4\uB974\uAC8C \uC77D\uD600\uC57C \uD560 \uB54C \u2014 \uC608: \uC81C\uBAA9 "\uB9E4\uC785" \uCE74\uB4DC\uB97C "\uB9E4\uC785 \uC138\uAE08\uACC4\uC0B0\uC11C \uBCF4\uAE30"\uB85C. \uD589\uB9C8\uB2E4 \uB2E4\uB974\uBA74 $calc/$item \uC73C\uB85C \uC5EE\uB294\uB2E4.',
512
+ group: "content",
513
+ advanced: true
514
+ },
506
515
  styleProp,
507
516
  classNameProp,
508
517
  growProp,
@@ -947,6 +956,35 @@ var ButtonMeta = {
947
956
  group: "behavior",
948
957
  advanced: true
949
958
  },
959
+ {
960
+ name: "href",
961
+ kind: "string",
962
+ label: "\uB9C1\uD06C \uC8FC\uC18C",
963
+ bindable: true,
964
+ description: 'as \uAC00 "a" \uC77C \uB54C \uC774\uB3D9\uD560 \uC8FC\uC18C(<a href>\uC5D0 \uADF8\uB300\uB85C \uC5B9\uD78C\uB2E4). \uCCA8\uBD80 \uD30C\uC77C \uC5F4\uAE30\uCC98\uB7FC \uD589\uB9C8\uB2E4 \uB2E4\uB974\uBA74 {"$item":"url"} \uB85C \uC5EE\uB294\uB2E4. javascript: \uAC19\uC740 \uC704\uD5D8\uD55C \uC8FC\uC18C\uB294 \uB80C\uB354\uB7EC\uAC00 \uBC84\uB9B0\uB2E4. button \uC5D0\uC11C\uB294 \uC544\uBB34 \uB370\uB3C4 \uAC00\uC9C0 \uC54A\uB294\uB2E4.',
965
+ group: "behavior",
966
+ advanced: true
967
+ },
968
+ {
969
+ name: "target",
970
+ kind: "enum",
971
+ label: "\uC5EC\uB294 \uACF3",
972
+ description: 'as "a" \uB9C1\uD06C\uB97C \uC5B4\uB514\uC11C \uC5F4\uC9C0. \uC0C8 \uD0ED(_blank)\uC774\uBA74 rel \uC5D0 "noopener noreferrer"\uB97C \uAC19\uC774 \uC900\uB2E4.',
973
+ options: [
974
+ { value: "_blank", label: "\uC0C8 \uD0ED(_blank)" },
975
+ { value: "_self", label: "\uAC19\uC740 \uD0ED(_self)" }
976
+ ],
977
+ group: "behavior",
978
+ advanced: true
979
+ },
980
+ {
981
+ name: "rel",
982
+ kind: "string",
983
+ label: "\uB9C1\uD06C \uAD00\uACC4(rel)",
984
+ description: 'as "a" \uB9C1\uD06C\uC758 rel. \uC0C8 \uD0ED\uC73C\uB85C \uC5EC\uB294 \uBC14\uAE65 \uC8FC\uC18C\uB294 "noopener noreferrer" \u2014 \uC5F4\uB9B0 \uCABD\uC774 \uC774 \uCC3D\uC744 \uC870\uC791\uD558\uC9C0 \uBABB\uD558\uAC8C \uD55C\uB2E4.',
985
+ group: "behavior",
986
+ advanced: true
987
+ },
950
988
  {
951
989
  name: "aria-label",
952
990
  kind: "string",
@@ -5534,10 +5572,26 @@ var ImageLightboxMeta = {
5534
5572
  label: "\uC774\uBBF8\uC9C0 \uD655\uB300",
5535
5573
  category: "display",
5536
5574
  icon: "image",
5537
- description: "\uC378\uB124\uC77C\uC744 \uB204\uB974\uBA74 \uC804\uCCB4 \uD654\uBA74\uC73C\uB85C \uB744\uC6B0\uACE0 \uD655\uB300\xB7\uD68C\uC804\xB7\uB2E4\uC6B4\uB85C\uB4DC\uB97C \uC81C\uACF5\uD55C\uB2E4.",
5575
+ description: "\uC378\uB124\uC77C\uC744 \uB204\uB974\uBA74 \uC804\uCCB4 \uD654\uBA74\uC73C\uB85C \uB744\uC6B0\uACE0 \uD655\uB300\xB7\uD68C\uC804\xB7\uB2E4\uC6B4\uB85C\uB4DC\uB97C \uC81C\uACF5\uD55C\uB2E4. \uC774\uBBF8\uC9C0\uAC00 \uC544\uB2CC \uCCA8\uBD80(PDF \uB4F1)\uB098 \uBABB \uBD88\uB7EC\uC628 \uC8FC\uC18C\uB294 \uC378\uB124\uC77C \uC790\uB9AC\uC5D0 \uD30C\uC77C \uD0C0\uC77C(\uBB38\uC11C \uC544\uC774\uCF58\xB7\uD655\uC7A5\uC790, \uB204\uB974\uBA74 \uC0C8 \uD0ED)\uB85C \uBCF4\uC774\uACE0 \uBDF0\uC5B4\uB97C \uC5F4\uC9C0 \uC54A\uB294\uB2E4.",
5538
5576
  props: [
5539
- { name: "src", kind: "string", label: "\uC774\uBBF8\uC9C0 \uC8FC\uC18C", required: true, bindable: true, group: "data" },
5540
- { name: "alt", kind: "string", label: "\uB300\uCCB4 \uD14D\uC2A4\uD2B8", bindable: true, group: "content" },
5577
+ {
5578
+ name: "src",
5579
+ kind: "string",
5580
+ label: "\uC774\uBBF8\uC9C0 \uC8FC\uC18C",
5581
+ required: true,
5582
+ bindable: true,
5583
+ description: "\uC774\uBBF8\uC9C0\uAC00 \uC544\uB2C8\uBA74(mimeType, \uC5C6\uC73C\uBA74 \uC8FC\uC18C \uD655\uC7A5\uC790\uB85C \uD310\uC815) \uD30C\uC77C \uD0C0\uC77C\uC774 \uB41C\uB2E4. \uB2E8\uC11C\uAC00 \uC5C6\uB294 \uC8FC\uC18C\uB294 \uADF8\uB824 \uBCF4\uACE0 \uC2E4\uD328\uD558\uBA74 \uD0C0\uC77C\uB85C \uB5A8\uC5B4\uC9C4\uB2E4.",
5584
+ group: "data"
5585
+ },
5586
+ { name: "alt", kind: "string", label: "\uB300\uCCB4 \uD14D\uC2A4\uD2B8", bindable: true, description: "\uD30C\uC77C \uD0C0\uC77C\uC5D0\uC11C\uB294 \uD30C\uC77C \uC774\uB984(title\xB7\uC811\uADFC\uC131 \uC774\uB984)\uC73C\uB85C \uC4F4\uB2E4.", group: "content" },
5587
+ {
5588
+ name: "mimeType",
5589
+ kind: "string",
5590
+ label: "MIME \uD615\uC2DD",
5591
+ bindable: true,
5592
+ description: '\uCCA8\uBD80\uC758 MIME \uD615\uC2DD(\uC608: application/pdf) \u2014 \uC788\uC73C\uBA74 \uC774\uBBF8\uC9C0/\uD30C\uC77C \uD310\uC815\uC758 1\uC21C\uC704\uB2E4. \uC11C\uBC84 \uCCA8\uBD80 \uD589\uC774\uBA74 {"$item":"mime_type"} \uC744 \uBB36\uB294\uB2E4. \uC0DD\uB7B5\uD558\uBA74 \uC8FC\uC18C \uD655\uC7A5\uC790\uB85C \uD310\uC815\uD55C\uB2E4.',
5593
+ group: "data"
5594
+ },
5541
5595
  {
5542
5596
  name: "thumbClassName",
5543
5597
  kind: "string",
@@ -1,4 +1,4 @@
1
- export { AutocompleteField, endpointSearchFn, freeTextCreateLabel, toAutocompleteOptions } from '../chunk-QHSUTI55.js';
1
+ export { AutocompleteField, endpointSearchFn, freeTextCreateLabel, toAutocompleteOptions } from '../chunk-GLAG6ZT3.js';
2
2
  import '../chunk-HR2U2QGN.js';
3
3
  import '../chunk-QDCP6P3K.js';
4
4
  import '../chunk-KMNC3Q4J.js';
@@ -9,6 +9,7 @@ type FileDropzoneItem = {
9
9
  * MIME 형식(예: `application/pdf`) — 있으면 이미지/파일 타일 판정의 1순위다.
10
10
  * 서버 행을 그대로 채우는 앱은 snake_case(`mime_type`)로 주고, File 같은 객체는
11
11
  * `type`으로 준다 — 셋 다 읽는다(`type`은 `x/y` 모양일 때만 MIME 으로 본다).
12
+ * 판정 규칙은 공용 {@link looksLikeImage}(file-kind.tsx) — 상세 사진 격자·원본 보기·목록 사진 칸과 같다.
12
13
  */
13
14
  mimeType?: string;
14
15
  mime_type?: string;
@@ -1,6 +1,6 @@
1
- export { FileDropzone, unwrapUploaded } from '../chunk-FPEOYSK4.js';
2
- import '../chunk-S4CQTJC6.js';
3
- import '../chunk-7NQBV77N.js';
1
+ export { FileDropzone, unwrapUploaded } from '../chunk-CKZ66V5Q.js';
2
+ import '../chunk-FLBU7AVR.js';
3
+ import '../chunk-EKNEBGNW.js';
4
4
  import '../chunk-R2ORCOTG.js';
5
5
  import '../chunk-RIZRM7OO.js';
6
6
  import '../chunk-DE7FPWVJ.js';
@@ -5,6 +5,11 @@ interface ImageLightboxProps {
5
5
  /** 원본 이미지 URL */
6
6
  src: string;
7
7
  alt?: string;
8
+ /**
9
+ * 첨부의 MIME 형식(예: `application/pdf`) — 있으면 이미지/파일 판정의 1순위다(서버 첨부 행의
10
+ * `mime_type` 을 그대로 묶는다). 없으면 `src` 확장자로 판정하고, 단서가 없으면 이미지로 그린다.
11
+ */
12
+ mimeType?: string;
8
13
  /** 기본 썸네일 <img>에 적용할 클래스 */
9
14
  thumbClassName?: string;
10
15
  /** 기본 썸네일 <img>에 적용할 스타일 */
@@ -1,7 +1,8 @@
1
- export { ImageLightbox } from '../chunk-S4CQTJC6.js';
2
- import '../chunk-7NQBV77N.js';
1
+ export { ImageLightbox } from '../chunk-FLBU7AVR.js';
2
+ import '../chunk-EKNEBGNW.js';
3
3
  import '../chunk-R2ORCOTG.js';
4
4
  import '../chunk-RIZRM7OO.js';
5
+ import '../chunk-DE7FPWVJ.js';
5
6
  import '../chunk-TAVKGYH5.js';
6
7
  import '../chunk-45GIMFRP.js';
7
8
  import '../chunk-3JTBOABA.js';
@@ -1,8 +1,8 @@
1
- export { OrgChartBoard, buildFlowLayout, buildModel, compareMembers, fitCanvasView, fitFlowView, flowToggleIds, isUnassignedGroup, leadershipEmployees, orgVocabOf, pickEmployees, pickGroups, pickPositions } from '../chunk-NP4VOH2M.js';
1
+ export { OrgChartBoard, buildFlowLayout, buildModel, compareMembers, fitCanvasView, fitFlowView, flowToggleIds, isUnassignedGroup, leadershipEmployees, orgVocabOf, pickEmployees, pickGroups, pickPositions } from '../chunk-QM2YJS7J.js';
2
2
  import '../chunk-XRHR452N.js';
3
3
  import '../chunk-XDR6HC4U.js';
4
- import '../chunk-S4CQTJC6.js';
5
- import '../chunk-7NQBV77N.js';
4
+ import '../chunk-FLBU7AVR.js';
5
+ import '../chunk-EKNEBGNW.js';
6
6
  import '../chunk-R2ORCOTG.js';
7
7
  import '../chunk-RIZRM7OO.js';
8
8
  import '../chunk-DE7FPWVJ.js';
@@ -42,15 +42,15 @@ import { OptionValueFields } from '../chunk-5N4ESLPX.js';
42
42
  export { OptionValueFields } from '../chunk-5N4ESLPX.js';
43
43
  import { OrgChart } from '../chunk-LRDGT4T4.js';
44
44
  export { OrgChart } from '../chunk-LRDGT4T4.js';
45
- import { OrgChartBoard } from '../chunk-NP4VOH2M.js';
46
- export { OrgChartBoard } from '../chunk-NP4VOH2M.js';
45
+ import { OrgChartBoard } from '../chunk-QM2YJS7J.js';
46
+ export { OrgChartBoard } from '../chunk-QM2YJS7J.js';
47
47
  import '../chunk-XRHR452N.js';
48
48
  import '../chunk-XDR6HC4U.js';
49
49
  import { PermissionMatrixEditor } from '../chunk-BOL536MN.js';
50
50
  export { PermissionMatrixEditor, stickyInsetOf } from '../chunk-BOL536MN.js';
51
51
  import { PeriodFilter } from '../chunk-FVONSLW3.js';
52
52
  export { PeriodFilter } from '../chunk-FVONSLW3.js';
53
- export { AutocompleteField, endpointSearchFn, toAutocompleteOptions } from '../chunk-QHSUTI55.js';
53
+ export { AutocompleteField, endpointSearchFn, toAutocompleteOptions } from '../chunk-GLAG6ZT3.js';
54
54
  import { ColumnBoard } from '../chunk-Q6V7E2H5.js';
55
55
  export { ColumnBoard } from '../chunk-Q6V7E2H5.js';
56
56
  import { DiffView } from '../chunk-QZMMW3JC.js';
@@ -59,14 +59,14 @@ import { Disclosure } from '../chunk-BBA7KKG4.js';
59
59
  export { Disclosure } from '../chunk-BBA7KKG4.js';
60
60
  import { EventCalendar } from '../chunk-ZAESY6NX.js';
61
61
  export { EventCalendar, statusToneOf } from '../chunk-ZAESY6NX.js';
62
- export { FileDropzone, unwrapUploaded } from '../chunk-FPEOYSK4.js';
63
- import { ImageLightbox } from '../chunk-S4CQTJC6.js';
64
- export { ImageLightbox } from '../chunk-S4CQTJC6.js';
62
+ export { FileDropzone, unwrapUploaded } from '../chunk-CKZ66V5Q.js';
63
+ import { ImageLightbox } from '../chunk-FLBU7AVR.js';
64
+ export { ImageLightbox } from '../chunk-FLBU7AVR.js';
65
65
  import { InlineEdit } from '../chunk-GAABHPGR.js';
66
66
  export { InlineEdit } from '../chunk-GAABHPGR.js';
67
67
  import '../chunk-77OM6OQG.js';
68
68
  import '../chunk-PPOQY3V5.js';
69
- import '../chunk-7NQBV77N.js';
69
+ import '../chunk-EKNEBGNW.js';
70
70
  import { ApprovalOverridesEditor } from '../chunk-MBROAHA6.js';
71
71
  export { ApprovalOverridesEditor } from '../chunk-MBROAHA6.js';
72
72
  import { TagMultiSelect } from '../chunk-5DLUGN5M.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipco/sdui",
3
- "version": "1.9.1",
3
+ "version": "1.9.2",
4
4
  "description": "WIPCO 서버 주도 UI(SDUI) 렌더러 — JSON 페이지 스펙을 React 컴포넌트로 렌더링",
5
5
  "keywords": [
6
6
  "sdui",
@@ -1,73 +0,0 @@
1
- import { ImageViewer } from './chunk-7NQBV77N.js';
2
- import { boxed } from './chunk-3JTBOABA.js';
3
- import { useState } from 'react';
4
- import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
5
-
6
- function ImageLightboxImpl({
7
- src,
8
- alt,
9
- thumbClassName,
10
- thumbStyle,
11
- children
12
- }) {
13
- const [open, setOpen] = useState(false);
14
- const openViewer = (event) => {
15
- event.stopPropagation();
16
- setOpen(true);
17
- };
18
- const overlay = /* @__PURE__ */ jsx(
19
- ImageViewer,
20
- {
21
- src,
22
- alt,
23
- open,
24
- onClose: () => setOpen(false)
25
- }
26
- );
27
- if (children) {
28
- return /* @__PURE__ */ jsxs(Fragment, { children: [
29
- /* @__PURE__ */ jsx(
30
- "span",
31
- {
32
- style: { display: "contents" },
33
- onClick: openViewer,
34
- onKeyDown: (event) => {
35
- if (event.key === "Enter" || event.key === " ") {
36
- event.preventDefault();
37
- openViewer(event);
38
- }
39
- },
40
- children
41
- }
42
- ),
43
- overlay
44
- ] });
45
- }
46
- return /* @__PURE__ */ jsxs(Fragment, { children: [
47
- /* @__PURE__ */ jsx(
48
- "img",
49
- {
50
- src,
51
- alt: alt ?? "",
52
- role: "button",
53
- tabIndex: 0,
54
- className: thumbClassName,
55
- style: { cursor: "zoom-in", ...thumbStyle },
56
- onClick: openViewer,
57
- onKeyDown: (event) => {
58
- if (event.key === "Enter" || event.key === " ") {
59
- event.preventDefault();
60
- openViewer(event);
61
- }
62
- }
63
- }
64
- ),
65
- overlay
66
- ] });
67
- }
68
- var ImageLightbox = boxed(
69
- ImageLightboxImpl,
70
- "ImageLightbox"
71
- );
72
-
73
- export { ImageLightbox };