@wipco/sdui 0.19.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,24 @@
1
1
  import { S as SduiBoxProps } from './layout-B1QrIBm6.js';
2
2
  import React__default from 'react';
3
3
 
4
+ interface TimePickerProps {
5
+ /** 선택된 시각 (HH:MM, 24시간제). 주어지면 controlled로 동작. */
6
+ value?: string;
7
+ onChange?: (value: string) => void;
8
+ /** 고를 수 있는 최소 시각 (HH:MM, 포함). */
9
+ min?: string;
10
+ /** 고를 수 있는 최대 시각 (HH:MM, 포함). */
11
+ max?: string;
12
+ /** 목록 눈금 간격(분). 기본 30 */
13
+ step?: number;
14
+ placeholder?: string;
15
+ invalid?: boolean;
16
+ disabled?: boolean;
17
+ size?: 'sm' | 'md' | 'lg';
18
+ }
19
+ /** 바깥에서 온 `style`·`className`을 뿌리에 얹어 내보낸다. 까닭은 {@link ./boxed.tsx}에 있다. */
20
+ declare const TimePicker: React__default.FC<TimePickerProps & SduiBoxProps>;
21
+
4
22
  interface DatePickerProps {
5
23
  /** 선택된 날짜 (YYYY-MM-DD, `withTime`이면 YYYY-MM-DDTHH:mm). 주어지면 controlled로 동작. */
6
24
  value?: string;
@@ -45,4 +63,4 @@ declare const DatePicker: React__default.FC<DatePickerProps & SduiBoxProps>;
45
63
  /** 바깥에서 온 `style`·`className`을 뿌리에 얹어 내보낸다. 까닭은 {@link ./boxed.tsx}에 있다. */
46
64
  declare const DateRangePicker: React__default.FC<DateRangePickerProps & SduiBoxProps>;
47
65
 
48
- export { type DatePickerProps as D, type DateRangePickerProps as a, DatePicker as b, DateRangePicker as c, type DateRangeValue as d };
66
+ export { type DatePickerProps as D, type TimePickerProps as T, type DateRangePickerProps as a, DatePicker as b, DateRangePicker as c, type DateRangeValue as d, TimePicker as e };
@@ -1,5 +1,5 @@
1
- import { resolveTreeNodes, TreeView } from './chunk-BQ5ZGGW7.js';
2
- import { OverlayLayer } from './chunk-HPZUPYPK.js';
1
+ import { resolveTreeNodes, TreeView } from './chunk-IDUN2ESE.js';
2
+ import { OverlayLayer } from './chunk-XKLUCUVA.js';
3
3
  import { Field, Dialog, Icon, Button, Input } from './chunk-WIDDHR7L.js';
4
4
  import { boxed } from './chunk-SAN4IX6N.js';
5
5
  import { useState, useMemo, useEffect } from 'react';
@@ -0,0 +1,291 @@
1
+ import { useDismiss, usePopoverAnchor, usePortalTo, HoistedStyle, AnchoredPopoverStyles, anchorStyle, focusStaysInside } from './chunk-XKLUCUVA.js';
2
+ import { Icon } from './chunk-WIDDHR7L.js';
3
+ import { boxed } from './chunk-SAN4IX6N.js';
4
+ import { createContext, useState, useRef, useEffect, useMemo, useCallback, useContext } from 'react';
5
+ import { jsxs, jsx } from 'react/jsx-runtime';
6
+
7
+ // src/env.ts
8
+ var DEV = (() => {
9
+ try {
10
+ const meta = import.meta;
11
+ return Boolean(meta.env?.DEV);
12
+ } catch {
13
+ return false;
14
+ }
15
+ })();
16
+
17
+ // src/adapter.ts
18
+ function isSafeNavigationTarget(to) {
19
+ if (typeof to !== "string") return false;
20
+ const value = to.replace(/[\u0000-\u0020]/g, "");
21
+ if (value === "") return false;
22
+ if (value.startsWith("//")) return false;
23
+ if (value.startsWith("/") || value.startsWith("?") || value.startsWith("#")) return true;
24
+ return /^https?:\/\//i.test(value);
25
+ }
26
+ async function defaultFetch(path, init) {
27
+ const response = await fetch(path, {
28
+ method: init.method,
29
+ headers: { "content-type": "application/json", accept: "application/json" },
30
+ credentials: "include",
31
+ body: init.body === void 0 ? void 0 : JSON.stringify(init.body),
32
+ signal: init.signal
33
+ });
34
+ const text = await response.text();
35
+ let json = null;
36
+ if (text) {
37
+ try {
38
+ json = JSON.parse(text);
39
+ } catch {
40
+ json = null;
41
+ }
42
+ }
43
+ if (!response.ok) {
44
+ const message = json && typeof json === "object" && typeof json.message === "string" ? json.message : `\uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4 (${response.status})`;
45
+ throw new Error(message);
46
+ }
47
+ return json;
48
+ }
49
+ var defaultAdapter = {
50
+ navigate: (to) => {
51
+ if (!isSafeNavigationTarget(to)) {
52
+ console.error(`[sdui] \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 navigate \uB300\uC0C1: ${String(to)}`);
53
+ return;
54
+ }
55
+ window.location.assign(to);
56
+ },
57
+ fetch: defaultFetch,
58
+ rawFetch: (path) => fetch(path, { credentials: "include" }),
59
+ // 항등함수. 계약을 문서화하고, 컴포넌트가 `useSduiAssetResolver()`를 조건 없이
60
+ // 부를 수 있게 하는 것이 전부다. **이 함수 참조 자체가 표식**이기도 하다 —
61
+ // `assetResolverOf`가 이것과 같은지 보고 "주입 없음"을 판정한다.
62
+ resolveAsset: (url) => url,
63
+ invalidate: (prefix) => {
64
+ if (DEV) console.warn(`[sdui] invalidate \uC561\uC158\uC774 \uC5B4\uB311\uD130 \uC5C6\uC774 \uD638\uCD9C\uB428 (prefix: ${prefix})`);
65
+ },
66
+ refresh: () => {
67
+ if (DEV) console.warn("[sdui] refresh \uC561\uC158\uC774 \uC5B4\uB311\uD130 \uC5C6\uC774 \uD638\uCD9C\uB428");
68
+ }
69
+ };
70
+ function resolveAdapter(adapter) {
71
+ const merged = { ...defaultAdapter };
72
+ if (adapter) {
73
+ for (const [key, value] of Object.entries(adapter)) {
74
+ if (value !== void 0) merged[key] = value;
75
+ }
76
+ }
77
+ return merged;
78
+ }
79
+ var SduiAdapterContext = createContext(null);
80
+ function useSduiAdapter() {
81
+ return useContext(SduiAdapterContext);
82
+ }
83
+ function assetResolverOf(adapter) {
84
+ const resolve = adapter?.resolveAsset;
85
+ if (!resolve || resolve === defaultAdapter.resolveAsset) return null;
86
+ return resolve;
87
+ }
88
+ function useSduiAssetResolver() {
89
+ const adapter = useContext(SduiAdapterContext);
90
+ return adapter?.resolveAsset ?? defaultAdapter.resolveAsset;
91
+ }
92
+ function useSduiSandbox() {
93
+ return useContext(SduiAdapterContext)?.sandbox === true;
94
+ }
95
+ function hostFetchOf(adapter) {
96
+ const hostFetch = adapter?.fetch;
97
+ if (!hostFetch || hostFetch === defaultAdapter.fetch) return null;
98
+ return hostFetch;
99
+ }
100
+ var HM_RE = /^\d{2}:\d{2}$/;
101
+ var LIST_HEIGHT = 260;
102
+ var MINUTES_PER_DAY = 24 * 60;
103
+ function maskHm(text) {
104
+ const digits = text.replace(/\D/g, "").slice(0, 4);
105
+ if (digits.length <= 2) return digits;
106
+ return `${digits.slice(0, 2)}:${digits.slice(2)}`;
107
+ }
108
+ function parseHm(value) {
109
+ if (typeof value !== "string" || !HM_RE.test(value)) return void 0;
110
+ const h = Number(value.slice(0, 2));
111
+ const m = Number(value.slice(3, 5));
112
+ if (h > 23 || m > 59) return void 0;
113
+ return h * 60 + m;
114
+ }
115
+ function formatHm(minutes) {
116
+ const h = Math.floor(minutes / 60) % 24;
117
+ const m = minutes % 60;
118
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
119
+ }
120
+ function buildOptions(step, min, max) {
121
+ const safeStep = Number.isFinite(step) && step >= 1 ? Math.min(Math.floor(step), 720) : 30;
122
+ const from = min ?? 0;
123
+ const to = max ?? MINUTES_PER_DAY - 1;
124
+ const out = [];
125
+ for (let m = Math.ceil(from / safeStep) * safeStep; m <= to; m += safeStep) out.push(formatHm(m));
126
+ return out;
127
+ }
128
+ function TimePickerImpl({
129
+ value,
130
+ onChange,
131
+ min,
132
+ max,
133
+ step = 30,
134
+ placeholder,
135
+ invalid = false,
136
+ disabled = false,
137
+ size = "md"
138
+ }) {
139
+ const [open, setOpen] = useState(false);
140
+ const [innerValue, setInnerValue] = useState("");
141
+ const hostRef = useRef(null);
142
+ const popRef = useRef(null);
143
+ const inputRef = useRef(null);
144
+ const isControlled = value !== void 0;
145
+ const currentValue = (isControlled ? value : innerValue) ?? "";
146
+ const [text, setText] = useState(currentValue);
147
+ useEffect(() => {
148
+ setText(currentValue);
149
+ }, [currentValue]);
150
+ const minMinutes = useMemo(() => parseHm(min), [min]);
151
+ const maxMinutes = useMemo(() => parseHm(max), [max]);
152
+ const options = useMemo(() => buildOptions(step, minMinutes, maxMinutes), [step, minMinutes, maxMinutes]);
153
+ const dismiss = useCallback((reason) => {
154
+ setOpen(false);
155
+ if (reason === "escape") inputRef.current?.focus();
156
+ }, []);
157
+ useDismiss(open, hostRef, popRef, dismiss);
158
+ const anchor = usePopoverAnchor(open, hostRef, popRef, LIST_HEIGHT);
159
+ const portal = usePortalTo();
160
+ const isInvalid = Boolean(invalid);
161
+ const commit = (next) => {
162
+ if (!isControlled) setInnerValue(next);
163
+ onChange?.(next);
164
+ };
165
+ const handleTextChange = (raw) => {
166
+ const masked = maskHm(raw);
167
+ setText(masked);
168
+ if (masked === "") {
169
+ commit("");
170
+ return;
171
+ }
172
+ const minutes = parseHm(masked);
173
+ if (minutes === void 0) return;
174
+ if (minMinutes !== void 0 && minutes < minMinutes) return;
175
+ if (maxMinutes !== void 0 && minutes > maxMinutes) return;
176
+ commit(masked);
177
+ };
178
+ const focusList = () => {
179
+ const list = popRef.current;
180
+ if (!list) return;
181
+ (list.querySelector('[aria-selected="true"]') ?? list.querySelector('[role="option"]'))?.focus();
182
+ };
183
+ const handleKeyDown = (event) => {
184
+ if (event.key === "ArrowDown") {
185
+ event.preventDefault();
186
+ if (open) focusList();
187
+ else setOpen(true);
188
+ return;
189
+ }
190
+ if (event.key === "Enter" && open) {
191
+ event.preventDefault();
192
+ setOpen(false);
193
+ }
194
+ };
195
+ const handleOptionKeyDown = (index) => (event) => {
196
+ const step2 = event.key === "ArrowDown" ? 1 : event.key === "ArrowUp" ? -1 : 0;
197
+ if (step2 !== 0) {
198
+ event.preventDefault();
199
+ const next = Math.min(Math.max(index + step2, 0), options.length - 1);
200
+ popRef.current?.querySelectorAll('[role="option"]')[next]?.focus();
201
+ return;
202
+ }
203
+ if (event.key === "Home" || event.key === "End") {
204
+ event.preventDefault();
205
+ const target = event.key === "Home" ? 0 : options.length - 1;
206
+ popRef.current?.querySelectorAll('[role="option"]')[target]?.focus();
207
+ }
208
+ };
209
+ const pick = (option) => {
210
+ setText(option);
211
+ commit(option);
212
+ setOpen(false);
213
+ inputRef.current?.focus();
214
+ };
215
+ const handleBlur = (event) => {
216
+ if (focusStaysInside(event.relatedTarget, hostRef, popRef)) return;
217
+ if (text !== currentValue) setText(currentValue);
218
+ setOpen(false);
219
+ };
220
+ const classes = ["wp-input", "wp-input--mono", "wp-tp-input"];
221
+ if (size !== "md") classes.push(`wp-input--${size}`);
222
+ if (isInvalid) classes.push("wp-input--invalid");
223
+ return /* @__PURE__ */ jsxs("div", { ref: hostRef, className: "wp-tp-host", onBlur: handleBlur, children: [
224
+ /* @__PURE__ */ jsx(HoistedStyle, { id: "wp-timepicker", css: TP_CSS }),
225
+ /* @__PURE__ */ jsx(AnchoredPopoverStyles, {}),
226
+ /* @__PURE__ */ jsxs("div", { className: "wp-inputwrap wp-inputwrap--lead", children: [
227
+ /* @__PURE__ */ jsx("span", { className: "wp-inputwrap__lead", children: /* @__PURE__ */ jsx(Icon, { name: "clock", size: size === "lg" ? 16 : 15 }) }),
228
+ /* @__PURE__ */ jsx(
229
+ "input",
230
+ {
231
+ ref: inputRef,
232
+ type: "text",
233
+ className: classes.join(" "),
234
+ inputMode: "numeric",
235
+ autoComplete: "off",
236
+ spellCheck: false,
237
+ maxLength: 5,
238
+ placeholder: placeholder ?? "HH:MM",
239
+ value: text,
240
+ disabled,
241
+ "aria-invalid": isInvalid || void 0,
242
+ "aria-haspopup": "listbox",
243
+ "aria-expanded": open,
244
+ onChange: (event) => handleTextChange(event.target.value),
245
+ onKeyDown: handleKeyDown,
246
+ onPointerDown: () => {
247
+ if (!disabled && !open) setOpen(true);
248
+ }
249
+ }
250
+ )
251
+ ] }),
252
+ portal(
253
+ anchor,
254
+ /* @__PURE__ */ jsx(
255
+ "div",
256
+ {
257
+ ref: popRef,
258
+ className: "wp-anchored-pop wp-tp-pop",
259
+ style: anchorStyle(anchor),
260
+ role: "listbox",
261
+ "aria-label": "\uC2DC\uAC01 \uC120\uD0DD",
262
+ children: options.map((option, index) => /* @__PURE__ */ jsx(
263
+ "button",
264
+ {
265
+ type: "button",
266
+ role: "option",
267
+ "aria-selected": option === currentValue,
268
+ className: option === currentValue ? "wp-tp-opt wp-tp-opt--on" : "wp-tp-opt",
269
+ onKeyDown: handleOptionKeyDown(index),
270
+ onClick: () => pick(option),
271
+ children: option
272
+ },
273
+ option
274
+ ))
275
+ }
276
+ )
277
+ )
278
+ ] });
279
+ }
280
+ var TP_CSS = `
281
+ .wp-tp-host{position:relative}
282
+ .wp-tp-input{padding-right:6px;min-width:calc(5ch + 36px)}
283
+ .wp-tp-pop{padding:var(--wp-space-1);max-height:260px;overflow-y:auto;overscroll-behavior:contain}
284
+ .wp-tp-opt{display:block;width:100%;padding:6px 10px;border:none;background:transparent;border-radius:var(--wp-radius-sm);font:inherit;font-family:var(--wp-font-mono);font-size:12.5px;color:var(--wp-text-default);text-align:left;cursor:pointer}
285
+ .wp-tp-opt:hover{background:var(--wp-surface-hover)}
286
+ .wp-tp-opt:focus-visible{outline:2px solid var(--wp-border-focus);outline-offset:-2px}
287
+ .wp-tp-opt--on{background:var(--wp-action-primary-bg);color:var(--wp-action-primary-fg);font-weight:500}
288
+ `;
289
+ var TimePicker = boxed(TimePickerImpl, "TimePicker");
290
+
291
+ export { DEV, SduiAdapterContext, TimePicker, assetResolverOf, defaultAdapter, hostFetchOf, isSafeNavigationTarget, resolveAdapter, useSduiAdapter, useSduiAssetResolver, useSduiSandbox };
@@ -1,4 +1,4 @@
1
- import { buildCascadingTree } from './chunk-SUJCS7SU.js';
1
+ import { buildCascadingTree } from './chunk-U7ZK7WYH.js';
2
2
  import { Input, IconButton, EmptyState, Icon, Badge } from './chunk-WIDDHR7L.js';
3
3
  import { boxed } from './chunk-SAN4IX6N.js';
4
4
  import React, { useMemo, useState, useRef, useEffect } from 'react';
@@ -1,5 +1,5 @@
1
- import { includesLooseSearch } from './chunk-SUJCS7SU.js';
2
- import { OverlayLayer } from './chunk-HPZUPYPK.js';
1
+ import { includesLooseSearch } from './chunk-U7ZK7WYH.js';
2
+ import { OverlayLayer } from './chunk-XKLUCUVA.js';
3
3
  import { Button, Dialog, Input, Tag, Checkbox } from './chunk-WIDDHR7L.js';
4
4
  import { boxed } from './chunk-SAN4IX6N.js';
5
5
  import React, { useMemo, useState, useEffect } from 'react';
@@ -1,4 +1,4 @@
1
- import { DateRangePicker } from './chunk-HPZUPYPK.js';
1
+ import { DateRangePicker } from './chunk-XKLUCUVA.js';
2
2
  import { SegmentedControl, Select } from './chunk-WIDDHR7L.js';
3
3
  import { boxed } from './chunk-SAN4IX6N.js';
4
4
  import { useState, useMemo } from 'react';
@@ -1,4 +1,4 @@
1
- import { includesLooseSearch } from './chunk-SUJCS7SU.js';
1
+ import { includesLooseSearch } from './chunk-U7ZK7WYH.js';
2
2
  import { Icon, Button, Input, Spinner } from './chunk-WIDDHR7L.js';
3
3
  import { boxed } from './chunk-SAN4IX6N.js';
4
4
  import { useId, useRef, useState, useEffect, useMemo } from 'react';
@@ -1,4 +1,4 @@
1
- import { useOverlayLayer, DateRangePicker } from './chunk-HPZUPYPK.js';
1
+ import { useOverlayLayer, DateRangePicker } from './chunk-XKLUCUVA.js';
2
2
  import { Input, Button, IconButton, Tag, Field, Checkbox } from './chunk-WIDDHR7L.js';
3
3
  import { useRef, useEffect, useState, useMemo } from 'react';
4
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
@@ -135,6 +135,85 @@ function OverlayLayer({
135
135
  function HoistedStyle({ id, css }) {
136
136
  return /* @__PURE__ */ jsx("style", { href: id, precedence: "medium", children: css });
137
137
  }
138
+ var ANCHOR_GAP = 8;
139
+ var VIEWPORT_EDGE = 8;
140
+ function anchorTo(host, height) {
141
+ if (!host || typeof window === "undefined") return null;
142
+ const rect = host.getBoundingClientRect();
143
+ const below = window.innerHeight - rect.bottom;
144
+ const up = below < height && rect.top > below;
145
+ return {
146
+ left: rect.left,
147
+ top: up ? void 0 : rect.bottom + ANCHOR_GAP,
148
+ bottom: up ? window.innerHeight - rect.top + ANCHOR_GAP : void 0,
149
+ minWidth: rect.width
150
+ };
151
+ }
152
+ function usePopoverAnchor(open, hostRef, popRef, height) {
153
+ const [anchor, setAnchor] = useState(null);
154
+ useLayoutEffect(() => {
155
+ if (!open) {
156
+ setAnchor(null);
157
+ return;
158
+ }
159
+ const place = () => setAnchor(anchorTo(hostRef.current, height));
160
+ place();
161
+ window.addEventListener("scroll", place, true);
162
+ window.addEventListener("resize", place);
163
+ return () => {
164
+ window.removeEventListener("scroll", place, true);
165
+ window.removeEventListener("resize", place);
166
+ };
167
+ }, [open, hostRef, height]);
168
+ useLayoutEffect(() => {
169
+ const el = popRef.current;
170
+ if (!open || !el || !anchor) return;
171
+ const overflow = el.getBoundingClientRect().right - (window.innerWidth - VIEWPORT_EDGE);
172
+ if (overflow <= 1) return;
173
+ setAnchor((prev) => {
174
+ if (!prev) return prev;
175
+ const left = Math.max(VIEWPORT_EDGE, prev.left - overflow);
176
+ return Math.abs(left - prev.left) < 1 ? prev : { ...prev, left };
177
+ });
178
+ }, [open, anchor, popRef]);
179
+ return anchor;
180
+ }
181
+ function AnchoredPopoverStyles() {
182
+ return /* @__PURE__ */ jsx(HoistedStyle, { id: "wp-anchored-pop", css: ANCHORED_POP_CSS });
183
+ }
184
+ var ANCHORED_POP_CSS = `
185
+ .wp-anchored-pop{position:fixed;width:max-content;max-width:min(92vw,400px);background:var(--wp-surface-card);border:1px solid var(--wp-border-subtle);border-radius:var(--wp-radius-lg);box-shadow:var(--wp-shadow-lg);z-index:var(--wp-z-dialog);pointer-events:auto}
186
+ `;
187
+ function anchorStyle(anchor) {
188
+ if (!anchor) return void 0;
189
+ return { left: anchor.left, top: anchor.top, bottom: anchor.bottom, minWidth: anchor.minWidth };
190
+ }
191
+ function usePortalTo() {
192
+ const container = React.useContext(OverlayContainerContext);
193
+ return (anchor, node) => {
194
+ if (!anchor) return null;
195
+ const root = container ?? getOverlayRoot();
196
+ return root ? createPortal(node, root) : node;
197
+ };
198
+ }
199
+ function useDismiss(open, hostRef, popRef, onDismiss) {
200
+ useEffect(() => {
201
+ if (!open) return;
202
+ const handlePointerDown = (event) => {
203
+ const target = event.target;
204
+ if (target instanceof Node && (hostRef.current?.contains(target) || popRef.current?.contains(target))) return;
205
+ onDismiss("outside");
206
+ };
207
+ document.addEventListener("pointerdown", handlePointerDown);
208
+ return () => {
209
+ document.removeEventListener("pointerdown", handlePointerDown);
210
+ };
211
+ }, [open, hostRef, popRef, onDismiss]);
212
+ useOverlayLayer(open, () => onDismiss("escape"), hostRef);
213
+ }
214
+ function focusStaysInside(next, hostRef, popRef) {
215
+ return next instanceof Node && Boolean(hostRef.current?.contains(next) || popRef.current?.contains(next));
216
+ }
138
217
  var YMD_RE = /^\d{4}-\d{2}-\d{2}$/;
139
218
  var HM_RE = /^\d{2}:\d{2}$/;
140
219
  function parseYmd(value) {
@@ -365,20 +444,7 @@ function Calendar({
365
444
  })
366
445
  ] }) });
367
446
  }
368
- function useDismiss(open, hostRef, onDismiss) {
369
- useEffect(() => {
370
- if (!open) return;
371
- const handlePointerDown = (event) => {
372
- if (event.target instanceof Node && hostRef.current?.contains(event.target)) return;
373
- onDismiss("outside");
374
- };
375
- document.addEventListener("pointerdown", handlePointerDown);
376
- return () => {
377
- document.removeEventListener("pointerdown", handlePointerDown);
378
- };
379
- }, [open, hostRef, onDismiss]);
380
- useOverlayLayer(open, () => onDismiss("escape"), hostRef);
381
- }
447
+ var CALENDAR_HEIGHT = 340;
382
448
  function DateInput({
383
449
  inputRef,
384
450
  value,
@@ -460,7 +526,9 @@ function DatePickerImpl({
460
526
  setOpen(false);
461
527
  if (reason === "escape") inputRef.current?.focus();
462
528
  }, []);
463
- useDismiss(open, hostRef, dismiss);
529
+ useDismiss(open, hostRef, popRef, dismiss);
530
+ const anchor = usePopoverAnchor(open, hostRef, popRef, CALENDAR_HEIGHT);
531
+ const portal = usePortalTo();
464
532
  const isInvalid = Boolean(invalid);
465
533
  const commit = (next) => {
466
534
  if (!isControlled) setInnerValue(next);
@@ -498,13 +566,13 @@ function DatePickerImpl({
498
566
  }
499
567
  };
500
568
  const handleBlur = (event) => {
501
- const next = event.relatedTarget;
502
- if (next instanceof Node && hostRef.current?.contains(next)) return;
569
+ if (focusStaysInside(event.relatedTarget, hostRef, popRef)) return;
503
570
  if (text !== fieldText) setText(fieldText);
504
571
  setOpen(false);
505
572
  };
506
573
  return /* @__PURE__ */ jsxs("div", { ref: hostRef, className: "wp-dp-host", onBlur: handleBlur, children: [
507
574
  /* @__PURE__ */ jsx(HoistedStyle, { id: "wp-datepicker", css: DP_CSS }),
575
+ /* @__PURE__ */ jsx(AnchoredPopoverStyles, {}),
508
576
  /* @__PURE__ */ jsx(
509
577
  DateInput,
510
578
  {
@@ -524,27 +592,40 @@ function DatePickerImpl({
524
592
  }
525
593
  }
526
594
  ),
527
- open ? /* @__PURE__ */ jsx("div", { ref: popRef, className: "wp-dp-pop", role: "dialog", "aria-label": "\uB0A0\uC9DC \uC120\uD0DD \uB2EC\uB825", children: /* @__PURE__ */ jsx("div", { className: "wp-dp-pop__body", children: /* @__PURE__ */ jsx(
528
- Calendar,
529
- {
530
- monthCount: 1,
531
- minDate,
532
- maxDate,
533
- focusDate,
534
- onFocusDateChange: setFocusDate,
535
- autoFocus: grabFocus,
536
- dayState: (date) => ({ selected: isSameDay(date, selectedDate) }),
537
- onPick: (date) => {
538
- const ymd = formatYmd(date);
539
- const time = withTime ? splitDateTime(text)[1] : "";
540
- const next = withTime ? `${ymd}T${isValidHm(time) ? time : "00:00"}` : ymd;
541
- setText(toFieldText(next, withTime));
542
- commit(next);
543
- setOpen(false);
544
- inputRef.current?.focus();
595
+ portal(
596
+ anchor,
597
+ /* @__PURE__ */ jsx(
598
+ "div",
599
+ {
600
+ ref: popRef,
601
+ className: "wp-anchored-pop wp-dp-pop",
602
+ style: anchorStyle(anchor),
603
+ role: "dialog",
604
+ "aria-label": "\uB0A0\uC9DC \uC120\uD0DD \uB2EC\uB825",
605
+ children: /* @__PURE__ */ jsx("div", { className: "wp-dp-pop__body", children: /* @__PURE__ */ jsx(
606
+ Calendar,
607
+ {
608
+ monthCount: 1,
609
+ minDate,
610
+ maxDate,
611
+ focusDate,
612
+ onFocusDateChange: setFocusDate,
613
+ autoFocus: grabFocus,
614
+ dayState: (date) => ({ selected: isSameDay(date, selectedDate) }),
615
+ onPick: (date) => {
616
+ const ymd = formatYmd(date);
617
+ const time = withTime ? splitDateTime(text)[1] : "";
618
+ const next = withTime ? `${ymd}T${isValidHm(time) ? time : "00:00"}` : ymd;
619
+ setText(toFieldText(next, withTime));
620
+ commit(next);
621
+ setOpen(false);
622
+ inputRef.current?.focus();
623
+ }
624
+ }
625
+ ) })
545
626
  }
546
- }
547
- ) }) }) : null
627
+ )
628
+ )
548
629
  ] });
549
630
  }
550
631
  function DateRangePickerImpl({
@@ -585,7 +666,9 @@ function DateRangePickerImpl({
585
666
  setOpen(false);
586
667
  if (reason === "escape") (end === "to" ? toRef : fromRef).current?.focus();
587
668
  }, [end]);
588
- useDismiss(open, hostRef, dismiss);
669
+ useDismiss(open, hostRef, popRef, dismiss);
670
+ const anchor = usePopoverAnchor(open, hostRef, popRef, CALENDAR_HEIGHT);
671
+ const portal = usePortalTo();
589
672
  const isInvalid = Boolean(invalid);
590
673
  const commit = (nextFrom, nextTo) => {
591
674
  const next = { from: nextFrom || void 0, to: nextTo || void 0 };
@@ -636,8 +719,7 @@ function DateRangePickerImpl({
636
719
  }
637
720
  };
638
721
  const handleBlur = (event) => {
639
- const next = event.relatedTarget;
640
- if (next instanceof Node && hostRef.current?.contains(next)) return;
722
+ if (focusStaysInside(event.relatedTarget, hostRef, popRef)) return;
641
723
  if (fromText !== fromValue) setFromText(fromValue);
642
724
  if (toText !== toValue) setToText(toValue);
643
725
  setOpen(false);
@@ -663,6 +745,7 @@ function DateRangePickerImpl({
663
745
  };
664
746
  return /* @__PURE__ */ jsxs("div", { ref: hostRef, className: "wp-dp-host wp-dp-host--range", onBlur: handleBlur, children: [
665
747
  /* @__PURE__ */ jsx(HoistedStyle, { id: "wp-datepicker", css: DP_CSS }),
748
+ /* @__PURE__ */ jsx(AnchoredPopoverStyles, {}),
666
749
  /* @__PURE__ */ jsxs("div", { className: "wp-dp-range", children: [
667
750
  /* @__PURE__ */ jsx(
668
751
  DateInput,
@@ -708,36 +791,48 @@ function DateRangePickerImpl({
708
791
  }
709
792
  )
710
793
  ] }),
711
- open ? /* @__PURE__ */ jsx("div", { ref: popRef, className: "wp-dp-pop wp-dp-pop--wide", role: "dialog", "aria-label": "\uAE30\uAC04 \uC120\uD0DD \uB2EC\uB825", children: /* @__PURE__ */ jsx("div", { className: "wp-dp-pop__body", children: /* @__PURE__ */ jsx(
712
- Calendar,
713
- {
714
- monthCount: 2,
715
- minDate,
716
- maxDate,
717
- focusDate,
718
- onFocusDateChange: setFocusDate,
719
- autoFocus: grabFocus,
720
- dayState: (date) => {
721
- const t = date.getTime();
722
- const start = fromDate?.getTime();
723
- const end2 = toDate?.getTime();
724
- if (start !== void 0 && end2 !== void 0 && t > start && t < end2) return { rangeMiddle: true };
725
- return {
726
- rangeStart: start !== void 0 && t === start && end2 !== void 0,
727
- rangeEnd: end2 !== void 0 && t === end2,
728
- // 한쪽 끝만 정해진 상태는 단일 선택처럼 보인다
729
- selected: start !== void 0 && t === start && end2 === void 0 || end2 !== void 0 && t === end2 && start === void 0 || void 0
730
- };
731
- },
732
- onPick: pick
733
- }
734
- ) }) }) : null
794
+ portal(
795
+ anchor,
796
+ /* @__PURE__ */ jsx(
797
+ "div",
798
+ {
799
+ ref: popRef,
800
+ className: "wp-anchored-pop wp-dp-pop wp-dp-pop--wide",
801
+ style: anchorStyle(anchor),
802
+ role: "dialog",
803
+ "aria-label": "\uAE30\uAC04 \uC120\uD0DD \uB2EC\uB825",
804
+ children: /* @__PURE__ */ jsx("div", { className: "wp-dp-pop__body", children: /* @__PURE__ */ jsx(
805
+ Calendar,
806
+ {
807
+ monthCount: 2,
808
+ minDate,
809
+ maxDate,
810
+ focusDate,
811
+ onFocusDateChange: setFocusDate,
812
+ autoFocus: grabFocus,
813
+ dayState: (date) => {
814
+ const t = date.getTime();
815
+ const start = fromDate?.getTime();
816
+ const end2 = toDate?.getTime();
817
+ if (start !== void 0 && end2 !== void 0 && t > start && t < end2) return { rangeMiddle: true };
818
+ return {
819
+ rangeStart: start !== void 0 && t === start && end2 !== void 0,
820
+ rangeEnd: end2 !== void 0 && t === end2,
821
+ // 한쪽 끝만 정해진 상태는 단일 선택처럼 보인다
822
+ selected: start !== void 0 && t === start && end2 === void 0 || end2 !== void 0 && t === end2 && start === void 0 || void 0
823
+ };
824
+ },
825
+ onPick: pick
826
+ }
827
+ ) })
828
+ }
829
+ )
830
+ )
735
831
  ] });
736
832
  }
737
833
  var DP_CSS = `
738
834
  .wp-dp-host{position:relative}
739
- .wp-dp-pop{position:absolute;top:calc(100% + var(--wp-space-2));left:0;min-width:100%;width:max-content;background:var(--wp-surface-card);border:1px solid var(--wp-border-subtle);border-radius:var(--wp-radius-lg);box-shadow:var(--wp-shadow-lg);z-index:300}
740
- .wp-dp-pop--wide{max-width:min(640px,90vw)}
835
+ .wp-dp-pop--wide{max-width:min(92vw,720px)}
741
836
  .wp-dp-pop__body{padding:var(--wp-space-4);max-height:60vh;overflow:auto}
742
837
  .wp-dp-range{display:flex;align-items:center;gap:var(--wp-space-1)}
743
838
  .wp-dp-range>*{flex:1 1 0;min-width:0}
@@ -777,4 +872,4 @@ var DP_CSS = `
777
872
  var DatePicker = boxed(DatePickerImpl, "DatePicker");
778
873
  var DateRangePicker = boxed(DateRangePickerImpl, "DateRangePicker");
779
874
 
780
- export { DatePicker, DateRangePicker, HoistedStyle, OverlayContainerContext, OverlayLayer, getOverlayRoot, useOverlayLayer };
875
+ export { AnchoredPopoverStyles, DatePicker, DateRangePicker, HoistedStyle, OverlayContainerContext, OverlayLayer, anchorStyle, focusStaysInside, getOverlayRoot, useDismiss, useOverlayLayer, usePopoverAnchor, usePortalTo };
package/dist/index.d.ts CHANGED
@@ -4,8 +4,8 @@ import { S as SduiPage, a as SduiAction, b as SduiPropValue, c as SduiRule } fro
4
4
  export { d as SduiArgRef, e as SduiCalcRef, f as SduiChild, g as SduiIndexRef, h as SduiItemRef, i as SduiNode, j as SduiPageQuery, k as SduiScalar, l as SduiStateRef } from './types-opTPYysN.js';
5
5
  import { S as SduiBoxProps, a as SduiContainerLayoutProps, b as SduiColumnsScale, c as SduiGapScale, d as SduiWeightScale, e as SduiTextAlignScale } from './layout-B1QrIBm6.js';
6
6
  export { C as CHILD_LAYOUT_PROPS, L as LAYOUT_CSS, f as SduiAlignScale, g as SduiChildLayoutProps, h as SduiSizeScale, i as SduiSpanScale, j as childLayoutFrom, k as columnsClass, l as containerLayoutStyle, m as joinClass, n as mergeStyle, t as typographyStyle } from './layout-B1QrIBm6.js';
7
- import { D as DatePickerProps, a as DateRangePickerProps } from './DatePicker-CPD2cfmG.js';
8
- export { b as DatePicker, c as DateRangePicker, d as DateRangeValue } from './DatePicker-CPD2cfmG.js';
7
+ import { D as DatePickerProps, a as DateRangePickerProps, T as TimePickerProps } from './DatePicker-D2EtpvkQ.js';
8
+ export { b as DatePicker, c as DateRangePicker, d as DateRangeValue, e as TimePicker } from './DatePicker-D2EtpvkQ.js';
9
9
 
10
10
  /**
11
11
  * 호스트 앱 연결점 — ERP에서는 라우터·erpFetch·SSE가 하던 일을
@@ -1541,6 +1541,7 @@ declare const defaultRegistry: {
1541
1541
  Checkbox: typeof Checkbox;
1542
1542
  DatePicker: React.FC<DatePickerProps & SduiBoxProps>;
1543
1543
  DateRangePicker: React.FC<DateRangePickerProps & SduiBoxProps>;
1544
+ TimePicker: React.FC<TimePickerProps & SduiBoxProps>;
1544
1545
  Dialog: typeof SduiDialog;
1545
1546
  EmptyState: typeof EmptyState;
1546
1547
  Field: typeof Field;
@@ -1830,4 +1831,4 @@ interface ImageViewerProps {
1830
1831
  */
1831
1832
  declare function ImageViewer({ src, alt, open, onClose }: ImageViewerProps): React__default.ReactPortal | null;
1832
1833
 
1833
- export { Actions, type ActionsProps, Alert, type AlertProps, Avatar, type AvatarProps, Badge, type BadgeProps, type BadgeTone, Breadcrumb, type BreadcrumbItem, type BreadcrumbProps, Button, type ButtonProps, Can, type CanProps, Card, type CardProps, type CascadingSearchOption, Checkbox, type CheckboxProps, Col, type ColProps, type CustomActionHandler, type CustomValidator, DatePickerProps, DateRangePickerProps, Dialog, type DialogProps, Divider, EmptyState, type EmptyStateProps, Field, type FieldProps, Grid, type GridCardConfig, type GridCardRow, type GridProps, Icon, IconButton, type IconButtonProps, type IconProps, ImageViewer, type ImageViewerProps, Input, type InputProps, type Justify, type KeyValueItem, KeyValueList, type KeyValueListProps, type MeterTone, NodeErrorBoundary, OverlayContainerContext, OverlayLayer, PageHead, type PageHeadProps, Pagination, type PaginationProps, ProgressBar, type ProgressBarProps, Radio, type RadioProps, RemoteSearchResults, type RemoteSearchResultsProps, ResultTable, type ResultTableColumn, type ResultTableProps, Row, type RowProps, SduiAction, type SduiAdapter, type SduiAssetContext, type SduiAssetResolver, type SduiBadgeCell, SduiBoxProps, type SduiCanFn, type SduiCanQuery, type SduiCanSetting, type SduiCell, SduiColumnsScale, SduiContainerLayoutProps, type SduiDateCell, SduiDialog, type SduiDispatch, SduiForm, type SduiFormConfig, type SduiFormProps, SduiGapScale, type SduiGridCard, type SduiGridCardAction, type SduiGridCardRow, type SduiIconCell, type SduiImageCell, type SduiLinkCell, type SduiNumberCell, SduiPage, SduiPermissionProvider, type SduiProgressCell, SduiPropValue, type SduiRegistry, SduiRenderer, type SduiRendererProps, type SduiRowCondition, SduiRule, type SduiRuntime, type SduiSearchAction, type SduiSearchColumn, SduiSearchDataProvider, SduiSearchResults, type SduiSearchResultsProps, type SduiTagCell, SduiTextAlignScale, type SduiTextCell, type SduiUseSpec, SduiWeightScale, type SearchAction, type SearchColumn, type SearchFetcher, type SearchField, type SearchFilter, type SearchOption, type SearchPage, SearchResults, type SearchResultsProps, type SearchRow, SearchToolbar, type SearchValues, type SearchViewItem, Section, type SectionProps, SegmentedControl, type SegmentedControlProps, type SegmentedItem, Select, type SelectOption, type SelectOptionRecord, type SelectProps, Show, type ShowProps, SidebarNav, type SidebarNavItem, type SidebarNavProps, type SidebarNavSection, type SortDir, SortHandle, type SortHandleProps, SortableList, type SortableListProps, Spacer, type SpacerProps, type SpecIssue, Spinner, type SpinnerProps, StatCard, type StatCardProps, Switch, type SwitchProps, type TabItem, Table, type TableColumn, type TableProps, Tabs, type TabsProps, Tag, type TagProps, Text, type TextPreset, type TextProps, Textarea, type TextareaProps, Toast, type ToastProps, Tooltip, type TooltipProps, boxed, cascadingValuePaths, cellRenderer, cleanSearchValues, createRegistry, defaultAdapter, defaultRegistry, getOverlayRoot, imageRenderer, includesLooseSearch, joinMultiValue, normalizePageSizes, normalizeSearchText, searchFieldKeys, searchValuesFromParams, sortableDropIndex, sortableMove, splitMultiValue, useOverlayLayer, useRemoteSearchPage, useSduiAssetResolver, useSduiCan, useSduiDispatch, useSduiRuntime, useSduiSandbox, useSearchFetcher, validateSpec };
1834
+ export { Actions, type ActionsProps, Alert, type AlertProps, Avatar, type AvatarProps, Badge, type BadgeProps, type BadgeTone, Breadcrumb, type BreadcrumbItem, type BreadcrumbProps, Button, type ButtonProps, Can, type CanProps, Card, type CardProps, type CascadingSearchOption, Checkbox, type CheckboxProps, Col, type ColProps, type CustomActionHandler, type CustomValidator, DatePickerProps, DateRangePickerProps, Dialog, type DialogProps, Divider, EmptyState, type EmptyStateProps, Field, type FieldProps, Grid, type GridCardConfig, type GridCardRow, type GridProps, Icon, IconButton, type IconButtonProps, type IconProps, ImageViewer, type ImageViewerProps, Input, type InputProps, type Justify, type KeyValueItem, KeyValueList, type KeyValueListProps, type MeterTone, NodeErrorBoundary, OverlayContainerContext, OverlayLayer, PageHead, type PageHeadProps, Pagination, type PaginationProps, ProgressBar, type ProgressBarProps, Radio, type RadioProps, RemoteSearchResults, type RemoteSearchResultsProps, ResultTable, type ResultTableColumn, type ResultTableProps, Row, type RowProps, SduiAction, type SduiAdapter, type SduiAssetContext, type SduiAssetResolver, type SduiBadgeCell, SduiBoxProps, type SduiCanFn, type SduiCanQuery, type SduiCanSetting, type SduiCell, SduiColumnsScale, SduiContainerLayoutProps, type SduiDateCell, SduiDialog, type SduiDispatch, SduiForm, type SduiFormConfig, type SduiFormProps, SduiGapScale, type SduiGridCard, type SduiGridCardAction, type SduiGridCardRow, type SduiIconCell, type SduiImageCell, type SduiLinkCell, type SduiNumberCell, SduiPage, SduiPermissionProvider, type SduiProgressCell, SduiPropValue, type SduiRegistry, SduiRenderer, type SduiRendererProps, type SduiRowCondition, SduiRule, type SduiRuntime, type SduiSearchAction, type SduiSearchColumn, SduiSearchDataProvider, SduiSearchResults, type SduiSearchResultsProps, type SduiTagCell, SduiTextAlignScale, type SduiTextCell, type SduiUseSpec, SduiWeightScale, type SearchAction, type SearchColumn, type SearchFetcher, type SearchField, type SearchFilter, type SearchOption, type SearchPage, SearchResults, type SearchResultsProps, type SearchRow, SearchToolbar, type SearchValues, type SearchViewItem, Section, type SectionProps, SegmentedControl, type SegmentedControlProps, type SegmentedItem, Select, type SelectOption, type SelectOptionRecord, type SelectProps, Show, type ShowProps, SidebarNav, type SidebarNavItem, type SidebarNavProps, type SidebarNavSection, type SortDir, SortHandle, type SortHandleProps, SortableList, type SortableListProps, Spacer, type SpacerProps, type SpecIssue, Spinner, type SpinnerProps, StatCard, type StatCardProps, Switch, type SwitchProps, type TabItem, Table, type TableColumn, type TableProps, Tabs, type TabsProps, Tag, type TagProps, Text, type TextPreset, type TextProps, Textarea, type TextareaProps, TimePickerProps, Toast, type ToastProps, Tooltip, type TooltipProps, boxed, cascadingValuePaths, cellRenderer, cleanSearchValues, createRegistry, defaultAdapter, defaultRegistry, getOverlayRoot, imageRenderer, includesLooseSearch, joinMultiValue, normalizePageSizes, normalizeSearchText, searchFieldKeys, searchValuesFromParams, sortableDropIndex, sortableMove, splitMultiValue, useOverlayLayer, useRemoteSearchPage, useSduiAssetResolver, useSduiCan, useSduiDispatch, useSduiRuntime, useSduiSandbox, useSearchFetcher, validateSpec };
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
- import { DEV, isSafeNavigationTarget, useSduiAdapter, hostFetchOf, useSduiSandbox, resolveAdapter, assetResolverOf, SduiAdapterContext } from './chunk-WGRB5UL4.js';
2
- export { defaultAdapter, useSduiAssetResolver, useSduiSandbox } from './chunk-WGRB5UL4.js';
1
+ import { DEV, isSafeNavigationTarget, useSduiAdapter, hostFetchOf, TimePicker, useSduiSandbox, resolveAdapter, assetResolverOf, SduiAdapterContext } from './chunk-HPZTFZZN.js';
2
+ export { TimePicker, defaultAdapter, useSduiAssetResolver, useSduiSandbox } from './chunk-HPZTFZZN.js';
3
3
  import { ImageViewer } from './chunk-THEP3VWY.js';
4
4
  export { ImageViewer } from './chunk-THEP3VWY.js';
5
- import { normalizeSearchFields, searchValuesFromParams, splitMultiValue, includesLooseSearch, cascadingValuePaths, SearchToolbar, searchFieldKeys, Popover } from './chunk-SUJCS7SU.js';
6
- export { SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue } from './chunk-SUJCS7SU.js';
7
- import { HoistedStyle, DateRangePicker, DatePicker, OverlayLayer } from './chunk-HPZUPYPK.js';
8
- export { DatePicker, DateRangePicker, OverlayContainerContext, OverlayLayer, getOverlayRoot, useOverlayLayer } from './chunk-HPZUPYPK.js';
5
+ import { normalizeSearchFields, searchValuesFromParams, splitMultiValue, includesLooseSearch, cascadingValuePaths, SearchToolbar, searchFieldKeys, Popover } from './chunk-U7ZK7WYH.js';
6
+ export { SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue } from './chunk-U7ZK7WYH.js';
7
+ import { HoistedStyle, DateRangePicker, DatePicker, OverlayLayer } from './chunk-XKLUCUVA.js';
8
+ export { DatePicker, DateRangePicker, OverlayContainerContext, OverlayLayer, getOverlayRoot, useOverlayLayer } from './chunk-XKLUCUVA.js';
9
9
  import { EmptyState, Button, Checkbox, SegmentedControl, Spinner, Select, Pagination, Icon, Card, Badge, ProgressBar, Tag, Toast, Textarea, Tabs, Table, Switch, StatCard, Input, IconButton, Field, Alert, warnOnce, Dialog } from './chunk-WIDDHR7L.js';
10
10
  export { Alert, Avatar, Badge, Breadcrumb, Button, Card, Checkbox, Dialog, EmptyState, Field, Icon, IconButton, Input, Pagination, ProgressBar, Radio, SegmentedControl, Select, SidebarNav, Spinner, StatCard, Switch, Table, Tabs, Tag, Textarea, Toast, Tooltip } from './chunk-WIDDHR7L.js';
11
11
  import { boxed, mergeStyle, containerLayoutStyle, joinClass, columnsClass, typographyStyle, LAYOUT_CSS, childLayoutFrom, CHILD_LAYOUT_PROPS } from './chunk-SAN4IX6N.js';
@@ -1725,6 +1725,7 @@ var defaultRegistry = {
1725
1725
  Checkbox,
1726
1726
  DatePicker,
1727
1727
  DateRangePicker,
1728
+ TimePicker,
1728
1729
  // Dialog는 표준 닫기 UX(Escape·배경 클릭)가 붙은 래퍼로 등록한다
1729
1730
  Dialog: SduiDialog,
1730
1731
  EmptyState,
package/dist/meta.d.ts CHANGED
@@ -285,6 +285,7 @@ declare const ButtonMeta: SduiComponentMeta;
285
285
  declare const CheckboxMeta: SduiComponentMeta;
286
286
  declare const DatePickerMeta: SduiComponentMeta;
287
287
  declare const DateRangePickerMeta: SduiComponentMeta;
288
+ declare const TimePickerMeta: SduiComponentMeta;
288
289
  declare const FieldMeta: SduiComponentMeta;
289
290
  declare const FormMeta: SduiComponentMeta;
290
291
  declare const IconButtonMeta: SduiComponentMeta;
@@ -343,6 +344,7 @@ declare const coreComponentMeta: {
343
344
  Checkbox: SduiComponentMeta;
344
345
  Col: SduiComponentMeta;
345
346
  DatePicker: SduiComponentMeta;
347
+ TimePicker: SduiComponentMeta;
346
348
  DateRangePicker: SduiComponentMeta;
347
349
  Dialog: SduiComponentMeta;
348
350
  Divider: SduiComponentMeta;
@@ -584,4 +586,4 @@ declare function paletteGroups(catalog?: Record<string, SduiComponentMeta>): Arr
584
586
  items: SduiComponentMeta[];
585
587
  }>;
586
588
 
587
- export { ActionsMeta, AlertMeta, AutocompleteFieldMeta, AvatarMeta, BadgeMeta, BreadcrumbMeta, ButtonMeta, CanMeta, CardMeta, ChartMeta, CheckboxMeta, ColMeta, ColumnBoardMeta, DatePickerMeta, DateRangePickerMeta, DialogMeta, DiffViewMeta, DisclosureMeta, DividerMeta, EmptyStateMeta, EventCalendarMeta, FieldMeta, ForEachMeta, FormMeta, FragmentMeta, GridMeta, IconButtonMeta, IconMeta, ImageLightboxMeta, InlineEditMeta, InputMeta, JsonFieldMeta, KeyValueListMeta, LiveClockMeta, MapPreviewMeta, OrgChartMeta, PageHeadMeta, PaginationMeta, PeriodFilterMeta, PrintSheetMeta, ProgressBarMeta, RadioMeta, RemoteSearchResultsMeta, RowMeta, SDUI_CATEGORIES, SELF_AXIS_PROPS, type SduiCategory, type SduiChildrenMeta, type SduiComponentMeta, type SduiDomMeta, type SduiEnumOption, type SduiPropGroup, type SduiPropKind, type SduiPropMeta, type SduiResizeMeta, type SduiSlotMeta, SearchResultsMeta, SearchableMultiPickerMeta, SectionMeta, SecureScreenMeta, SegmentedControlMeta, SelectMeta, ShowMeta, SidebarNavMeta, SortHandleMeta, SortableListMeta, SpacerMeta, SpinnerMeta, StackedMeterMeta, StatCardMeta, StripMeta, SummaryTableMeta, SwitchMeta, TableMeta, TabsMeta, TagMeta, TagMultiSelectMeta, TextMeta, TextareaMeta, TimelineMeta, ToastMeta, TooltipMeta, TreePickerMeta, TreeViewMeta, allComponentMeta, builtinComponentMeta, coreComponentMeta, dsComponentMeta, getComponentMeta, paletteGroups, richComponentMeta, slotNamesOf, widgetComponentMeta, withSelfAxis, withSelfAxisAll };
589
+ export { ActionsMeta, AlertMeta, AutocompleteFieldMeta, AvatarMeta, BadgeMeta, BreadcrumbMeta, ButtonMeta, CanMeta, CardMeta, ChartMeta, CheckboxMeta, ColMeta, ColumnBoardMeta, DatePickerMeta, DateRangePickerMeta, DialogMeta, DiffViewMeta, DisclosureMeta, DividerMeta, EmptyStateMeta, EventCalendarMeta, FieldMeta, ForEachMeta, FormMeta, FragmentMeta, GridMeta, IconButtonMeta, IconMeta, ImageLightboxMeta, InlineEditMeta, InputMeta, JsonFieldMeta, KeyValueListMeta, LiveClockMeta, MapPreviewMeta, OrgChartMeta, PageHeadMeta, PaginationMeta, PeriodFilterMeta, PrintSheetMeta, ProgressBarMeta, RadioMeta, RemoteSearchResultsMeta, RowMeta, SDUI_CATEGORIES, SELF_AXIS_PROPS, type SduiCategory, type SduiChildrenMeta, type SduiComponentMeta, type SduiDomMeta, type SduiEnumOption, type SduiPropGroup, type SduiPropKind, type SduiPropMeta, type SduiResizeMeta, type SduiSlotMeta, SearchResultsMeta, SearchableMultiPickerMeta, SectionMeta, SecureScreenMeta, SegmentedControlMeta, SelectMeta, ShowMeta, SidebarNavMeta, SortHandleMeta, SortableListMeta, SpacerMeta, SpinnerMeta, StackedMeterMeta, StatCardMeta, StripMeta, SummaryTableMeta, SwitchMeta, TableMeta, TabsMeta, TagMeta, TagMultiSelectMeta, TextMeta, TextareaMeta, TimePickerMeta, TimelineMeta, ToastMeta, TooltipMeta, TreePickerMeta, TreeViewMeta, allComponentMeta, builtinComponentMeta, coreComponentMeta, dsComponentMeta, getComponentMeta, paletteGroups, richComponentMeta, slotNamesOf, widgetComponentMeta, withSelfAxis, withSelfAxisAll };
package/dist/meta.js CHANGED
@@ -991,6 +991,46 @@ var DateRangePickerMeta = {
991
991
  },
992
992
  resize: { kind: "none" }
993
993
  };
994
+ var TimePickerMeta = {
995
+ name: "TimePicker",
996
+ label: "\uC2DC\uAC01 \uC120\uD0DD",
997
+ category: "input",
998
+ icon: "clock",
999
+ description: "\uC2DC\uAC01(HH:MM, 24\uC2DC\uAC04\uC81C) \u2014 \uD0A4\uBCF4\uB4DC\uB85C \uCCD0\uB3C4 \uB418\uACE0 \uB208\uAE08 \uBAA9\uB85D\uC5D0\uC11C \uACE8\uB77C\uB3C4 \uB41C\uB2E4.",
1000
+ props: [
1001
+ { name: "value", kind: "string", label: "\uAC12(HH:MM)", bindable: true, group: "data" },
1002
+ { name: "onChange", kind: "action", label: "\uBCC0\uACBD", eventArg: "HH:MM \uBB38\uC790\uC5F4", group: "behavior" },
1003
+ { name: "min", kind: "string", label: "\uCD5C\uC18C \uC2DC\uAC01", description: "HH:MM, \uD3EC\uD568", bindable: true, group: "validation" },
1004
+ { name: "max", kind: "string", label: "\uCD5C\uB300 \uC2DC\uAC01", description: "HH:MM, \uD3EC\uD568", bindable: true, group: "validation" },
1005
+ { name: "step", kind: "number", label: "\uB208\uAE08 \uAC04\uACA9(\uBD84)", default: 30, group: "behavior" },
1006
+ { name: "placeholder", kind: "string", label: "\uC548\uB0B4 \uBB38\uAD6C", description: "\uAE30\uBCF8\uAC12\uC740 HH:MM", group: "content" },
1007
+ { name: "invalid", kind: "boolean", label: "\uC624\uB958 \uD45C\uC2DC", default: false, bindable: true, group: "validation" },
1008
+ { name: "disabled", kind: "boolean", label: "\uBE44\uD65C\uC131", default: false, bindable: true, group: "behavior" },
1009
+ {
1010
+ name: "size",
1011
+ kind: "enum",
1012
+ label: "\uD06C\uAE30",
1013
+ default: "md",
1014
+ options: [
1015
+ { value: "sm", label: "sm 28px" },
1016
+ { value: "md", label: "md 32px" },
1017
+ { value: "lg", label: "lg 40px" }
1018
+ ],
1019
+ group: "appearance"
1020
+ }
1021
+ ],
1022
+ children: { accepts: false },
1023
+ dom: { element: "div" },
1024
+ preset: {
1025
+ component: "TimePicker",
1026
+ props: {
1027
+ placeholder: "HH:MM",
1028
+ value: { $state: "time", default: "" },
1029
+ onChange: { action: "setState", key: "time", fromEvent: true }
1030
+ }
1031
+ },
1032
+ resize: { kind: "enum", prop: "size", values: ["sm", "md", "lg"] }
1033
+ };
994
1034
  var FieldMeta = {
995
1035
  name: "Field",
996
1036
  label: "\uD3FC \uD544\uB4DC",
@@ -2665,6 +2705,7 @@ var coreComponentMeta = {
2665
2705
  Checkbox: CheckboxMeta,
2666
2706
  Col: ColMeta,
2667
2707
  DatePicker: DatePickerMeta,
2708
+ TimePicker: TimePickerMeta,
2668
2709
  DateRangePicker: DateRangePickerMeta,
2669
2710
  Dialog: DialogMeta,
2670
2711
  Divider: DividerMeta,
@@ -4579,4 +4620,4 @@ function paletteGroups(catalog = allComponentMeta) {
4579
4620
  })).filter((group) => group.items.length > 0);
4580
4621
  }
4581
4622
 
4582
- export { ActionsMeta, AlertMeta, AutocompleteFieldMeta, AvatarMeta, BadgeMeta, BreadcrumbMeta, ButtonMeta, CanMeta, CardMeta, ChartMeta, CheckboxMeta, ColMeta, ColumnBoardMeta, DatePickerMeta, DateRangePickerMeta, DialogMeta, DiffViewMeta, DisclosureMeta, DividerMeta, EmptyStateMeta, EventCalendarMeta, FieldMeta, ForEachMeta, FormMeta, FragmentMeta, GridMeta, IconButtonMeta, IconMeta, ImageLightboxMeta, InlineEditMeta, InputMeta, JsonFieldMeta, KeyValueListMeta, LiveClockMeta, MapPreviewMeta, OrgChartMeta, PageHeadMeta, PaginationMeta, PeriodFilterMeta, PrintSheetMeta, ProgressBarMeta, RadioMeta, RemoteSearchResultsMeta, RowMeta, SDUI_CATEGORIES, SELF_AXIS_PROPS, SearchResultsMeta, SearchableMultiPickerMeta, SectionMeta, SecureScreenMeta, SegmentedControlMeta, SelectMeta, ShowMeta, SidebarNavMeta, SortHandleMeta, SortableListMeta, SpacerMeta, SpinnerMeta, StackedMeterMeta, StatCardMeta, StripMeta, SummaryTableMeta, SwitchMeta, TableMeta, TabsMeta, TagMeta, TagMultiSelectMeta, TextMeta, TextareaMeta, TimelineMeta, ToastMeta, TooltipMeta, TreePickerMeta, TreeViewMeta, allComponentMeta, builtinComponentMeta, coreComponentMeta, dsComponentMeta, getComponentMeta, paletteGroups, richComponentMeta, slotNamesOf, widgetComponentMeta, withSelfAxis, withSelfAxisAll };
4623
+ export { ActionsMeta, AlertMeta, AutocompleteFieldMeta, AvatarMeta, BadgeMeta, BreadcrumbMeta, ButtonMeta, CanMeta, CardMeta, ChartMeta, CheckboxMeta, ColMeta, ColumnBoardMeta, DatePickerMeta, DateRangePickerMeta, DialogMeta, DiffViewMeta, DisclosureMeta, DividerMeta, EmptyStateMeta, EventCalendarMeta, FieldMeta, ForEachMeta, FormMeta, FragmentMeta, GridMeta, IconButtonMeta, IconMeta, ImageLightboxMeta, InlineEditMeta, InputMeta, JsonFieldMeta, KeyValueListMeta, LiveClockMeta, MapPreviewMeta, OrgChartMeta, PageHeadMeta, PaginationMeta, PeriodFilterMeta, PrintSheetMeta, ProgressBarMeta, RadioMeta, RemoteSearchResultsMeta, RowMeta, SDUI_CATEGORIES, SELF_AXIS_PROPS, SearchResultsMeta, SearchableMultiPickerMeta, SectionMeta, SecureScreenMeta, SegmentedControlMeta, SelectMeta, ShowMeta, SidebarNavMeta, SortHandleMeta, SortableListMeta, SpacerMeta, SpinnerMeta, StackedMeterMeta, StatCardMeta, StripMeta, SummaryTableMeta, SwitchMeta, TableMeta, TabsMeta, TagMeta, TagMultiSelectMeta, TextMeta, TextareaMeta, TimePickerMeta, TimelineMeta, ToastMeta, TooltipMeta, TreePickerMeta, TreeViewMeta, allComponentMeta, builtinComponentMeta, coreComponentMeta, dsComponentMeta, getComponentMeta, paletteGroups, richComponentMeta, slotNamesOf, widgetComponentMeta, withSelfAxis, withSelfAxisAll };
package/dist/rich.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import React__default from 'react';
2
- export { b as DatePicker, D as DatePickerProps, c as DateRangePicker, a as DateRangePickerProps, d as DateRangeValue } from './DatePicker-CPD2cfmG.js';
2
+ export { b as DatePicker, D as DatePickerProps, c as DateRangePicker, a as DateRangePickerProps, d as DateRangeValue, e as TimePicker, T as TimePickerProps } from './DatePicker-D2EtpvkQ.js';
3
3
  import './layout-B1QrIBm6.js';
4
4
 
5
5
  /**
package/dist/rich.js CHANGED
@@ -1,6 +1,7 @@
1
- import { useSduiAssetResolver } from './chunk-WGRB5UL4.js';
2
- import { HoistedStyle } from './chunk-HPZUPYPK.js';
3
- export { DatePicker, DateRangePicker } from './chunk-HPZUPYPK.js';
1
+ import { useSduiAssetResolver } from './chunk-HPZTFZZN.js';
2
+ export { TimePicker } from './chunk-HPZTFZZN.js';
3
+ import { HoistedStyle } from './chunk-XKLUCUVA.js';
4
+ export { DatePicker, DateRangePicker } from './chunk-XKLUCUVA.js';
4
5
  import { Spinner, EmptyState } from './chunk-WIDDHR7L.js';
5
6
  import './chunk-SAN4IX6N.js';
6
7
  import { useRef, useCallback, useMemo, useState, useEffect, useLayoutEffect } from 'react';
@@ -1,5 +1,5 @@
1
- export { AutocompleteField, toAutocompleteOptions } from '../chunk-ZODL6LE2.js';
2
- import '../chunk-SUJCS7SU.js';
3
- import '../chunk-HPZUPYPK.js';
1
+ export { AutocompleteField, toAutocompleteOptions } from '../chunk-SRGXODY2.js';
2
+ import '../chunk-U7ZK7WYH.js';
3
+ import '../chunk-XKLUCUVA.js';
4
4
  import '../chunk-WIDDHR7L.js';
5
5
  import '../chunk-SAN4IX6N.js';
@@ -1,4 +1,4 @@
1
- export { PeriodFilter } from '../chunk-IB4NDAYU.js';
2
- import '../chunk-HPZUPYPK.js';
1
+ export { PeriodFilter } from '../chunk-PFUULIZ2.js';
2
+ import '../chunk-XKLUCUVA.js';
3
3
  import '../chunk-WIDDHR7L.js';
4
4
  import '../chunk-SAN4IX6N.js';
@@ -1,5 +1,5 @@
1
- export { SearchableMultiPicker } from '../chunk-NXY6QH36.js';
2
- import '../chunk-SUJCS7SU.js';
3
- import '../chunk-HPZUPYPK.js';
1
+ export { SearchableMultiPicker } from '../chunk-N5IATDUF.js';
2
+ import '../chunk-U7ZK7WYH.js';
3
+ import '../chunk-XKLUCUVA.js';
4
4
  import '../chunk-WIDDHR7L.js';
5
5
  import '../chunk-SAN4IX6N.js';
@@ -1,6 +1,6 @@
1
- export { TreePicker } from '../chunk-YLYSOOVR.js';
2
- import '../chunk-BQ5ZGGW7.js';
3
- import '../chunk-SUJCS7SU.js';
4
- import '../chunk-HPZUPYPK.js';
1
+ export { TreePicker } from '../chunk-EQW24SJC.js';
2
+ import '../chunk-IDUN2ESE.js';
3
+ import '../chunk-U7ZK7WYH.js';
4
+ import '../chunk-XKLUCUVA.js';
5
5
  import '../chunk-WIDDHR7L.js';
6
6
  import '../chunk-SAN4IX6N.js';
@@ -1,5 +1,5 @@
1
- export { TreeView, buildTreeNodes, pruneExcluded, resolveTreeNodes } from '../chunk-BQ5ZGGW7.js';
2
- import '../chunk-SUJCS7SU.js';
3
- import '../chunk-HPZUPYPK.js';
1
+ export { TreeView, buildTreeNodes, pruneExcluded, resolveTreeNodes } from '../chunk-IDUN2ESE.js';
2
+ import '../chunk-U7ZK7WYH.js';
3
+ import '../chunk-XKLUCUVA.js';
4
4
  import '../chunk-WIDDHR7L.js';
5
5
  import '../chunk-SAN4IX6N.js';
@@ -1,5 +1,5 @@
1
- import { SearchableMultiPicker } from '../chunk-NXY6QH36.js';
2
- export { SearchableMultiPicker } from '../chunk-NXY6QH36.js';
1
+ import { SearchableMultiPicker } from '../chunk-N5IATDUF.js';
2
+ export { SearchableMultiPicker } from '../chunk-N5IATDUF.js';
3
3
  import { SecureScreen } from '../chunk-IIRENCDS.js';
4
4
  export { SecureScreen } from '../chunk-IIRENCDS.js';
5
5
  import { StackedMeter } from '../chunk-BMZKZGJF.js';
@@ -12,10 +12,10 @@ import { TagMultiSelect } from '../chunk-QDEZXA34.js';
12
12
  export { TagMultiSelect } from '../chunk-QDEZXA34.js';
13
13
  import { Timeline } from '../chunk-EFYEOT47.js';
14
14
  export { Timeline } from '../chunk-EFYEOT47.js';
15
- import { TreePicker } from '../chunk-YLYSOOVR.js';
16
- export { TreePicker } from '../chunk-YLYSOOVR.js';
17
- import { TreeView } from '../chunk-BQ5ZGGW7.js';
18
- export { TreeView, buildTreeNodes } from '../chunk-BQ5ZGGW7.js';
15
+ import { TreePicker } from '../chunk-EQW24SJC.js';
16
+ export { TreePicker } from '../chunk-EQW24SJC.js';
17
+ import { TreeView } from '../chunk-IDUN2ESE.js';
18
+ export { TreeView, buildTreeNodes } from '../chunk-IDUN2ESE.js';
19
19
  import { EventCalendar } from '../chunk-C3JY6GA3.js';
20
20
  export { EventCalendar } from '../chunk-C3JY6GA3.js';
21
21
  import { ImageLightbox } from '../chunk-XPBZMECW.js';
@@ -28,14 +28,14 @@ import { LiveClock } from '../chunk-Z3WGIFJP.js';
28
28
  export { LiveClock } from '../chunk-Z3WGIFJP.js';
29
29
  import { OrgChart } from '../chunk-QWJ3OBBL.js';
30
30
  export { OrgChart } from '../chunk-QWJ3OBBL.js';
31
- import { PeriodFilter } from '../chunk-IB4NDAYU.js';
32
- export { PeriodFilter } from '../chunk-IB4NDAYU.js';
31
+ import { PeriodFilter } from '../chunk-PFUULIZ2.js';
32
+ export { PeriodFilter } from '../chunk-PFUULIZ2.js';
33
33
  import { PrintSheet } from '../chunk-MYJ2KEIP.js';
34
34
  export { PrintSheet } from '../chunk-MYJ2KEIP.js';
35
35
  import '../chunk-THEP3VWY.js';
36
- export { AutocompleteField, toAutocompleteOptions } from '../chunk-ZODL6LE2.js';
37
- import '../chunk-SUJCS7SU.js';
38
- import '../chunk-HPZUPYPK.js';
36
+ export { AutocompleteField, toAutocompleteOptions } from '../chunk-SRGXODY2.js';
37
+ import '../chunk-U7ZK7WYH.js';
38
+ import '../chunk-XKLUCUVA.js';
39
39
  import { ColumnBoard } from '../chunk-C4JRSZGS.js';
40
40
  export { ColumnBoard } from '../chunk-C4JRSZGS.js';
41
41
  import { DiffView } from '../chunk-LDJ2CFFH.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipco/sdui",
3
- "version": "0.19.1",
3
+ "version": "0.20.0",
4
4
  "description": "WIPCO 서버 주도 UI(SDUI) 렌더러 — JSON 페이지 스펙을 React 컴포넌트로 렌더링",
5
5
  "keywords": [
6
6
  "sdui",
@@ -1,99 +0,0 @@
1
- import { createContext, useContext } from 'react';
2
-
3
- // src/adapter.ts
4
-
5
- // src/env.ts
6
- var DEV = (() => {
7
- try {
8
- const meta = import.meta;
9
- return Boolean(meta.env?.DEV);
10
- } catch {
11
- return false;
12
- }
13
- })();
14
-
15
- // src/adapter.ts
16
- function isSafeNavigationTarget(to) {
17
- if (typeof to !== "string") return false;
18
- const value = to.replace(/[\u0000-\u0020]/g, "");
19
- if (value === "") return false;
20
- if (value.startsWith("//")) return false;
21
- if (value.startsWith("/") || value.startsWith("?") || value.startsWith("#")) return true;
22
- return /^https?:\/\//i.test(value);
23
- }
24
- async function defaultFetch(path, init) {
25
- const response = await fetch(path, {
26
- method: init.method,
27
- headers: { "content-type": "application/json", accept: "application/json" },
28
- credentials: "include",
29
- body: init.body === void 0 ? void 0 : JSON.stringify(init.body),
30
- signal: init.signal
31
- });
32
- const text = await response.text();
33
- let json = null;
34
- if (text) {
35
- try {
36
- json = JSON.parse(text);
37
- } catch {
38
- json = null;
39
- }
40
- }
41
- if (!response.ok) {
42
- const message = json && typeof json === "object" && typeof json.message === "string" ? json.message : `\uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4 (${response.status})`;
43
- throw new Error(message);
44
- }
45
- return json;
46
- }
47
- var defaultAdapter = {
48
- navigate: (to) => {
49
- if (!isSafeNavigationTarget(to)) {
50
- console.error(`[sdui] \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 navigate \uB300\uC0C1: ${String(to)}`);
51
- return;
52
- }
53
- window.location.assign(to);
54
- },
55
- fetch: defaultFetch,
56
- rawFetch: (path) => fetch(path, { credentials: "include" }),
57
- // 항등함수. 계약을 문서화하고, 컴포넌트가 `useSduiAssetResolver()`를 조건 없이
58
- // 부를 수 있게 하는 것이 전부다. **이 함수 참조 자체가 표식**이기도 하다 —
59
- // `assetResolverOf`가 이것과 같은지 보고 "주입 없음"을 판정한다.
60
- resolveAsset: (url) => url,
61
- invalidate: (prefix) => {
62
- if (DEV) console.warn(`[sdui] invalidate \uC561\uC158\uC774 \uC5B4\uB311\uD130 \uC5C6\uC774 \uD638\uCD9C\uB428 (prefix: ${prefix})`);
63
- },
64
- refresh: () => {
65
- if (DEV) console.warn("[sdui] refresh \uC561\uC158\uC774 \uC5B4\uB311\uD130 \uC5C6\uC774 \uD638\uCD9C\uB428");
66
- }
67
- };
68
- function resolveAdapter(adapter) {
69
- const merged = { ...defaultAdapter };
70
- if (adapter) {
71
- for (const [key, value] of Object.entries(adapter)) {
72
- if (value !== void 0) merged[key] = value;
73
- }
74
- }
75
- return merged;
76
- }
77
- var SduiAdapterContext = createContext(null);
78
- function useSduiAdapter() {
79
- return useContext(SduiAdapterContext);
80
- }
81
- function assetResolverOf(adapter) {
82
- const resolve = adapter?.resolveAsset;
83
- if (!resolve || resolve === defaultAdapter.resolveAsset) return null;
84
- return resolve;
85
- }
86
- function useSduiAssetResolver() {
87
- const adapter = useContext(SduiAdapterContext);
88
- return adapter?.resolveAsset ?? defaultAdapter.resolveAsset;
89
- }
90
- function useSduiSandbox() {
91
- return useContext(SduiAdapterContext)?.sandbox === true;
92
- }
93
- function hostFetchOf(adapter) {
94
- const hostFetch = adapter?.fetch;
95
- if (!hostFetch || hostFetch === defaultAdapter.fetch) return null;
96
- return hostFetch;
97
- }
98
-
99
- export { DEV, SduiAdapterContext, assetResolverOf, defaultAdapter, hostFetchOf, isSafeNavigationTarget, resolveAdapter, useSduiAdapter, useSduiAssetResolver, useSduiSandbox };