@wipco/sdui 0.18.1 → 0.19.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.
@@ -2,7 +2,7 @@ import { S as SduiBoxProps } from './layout-B1QrIBm6.js';
2
2
  import React__default from 'react';
3
3
 
4
4
  interface DatePickerProps {
5
- /** 선택된 날짜 (YYYY-MM-DD). 주어지면 controlled로 동작. */
5
+ /** 선택된 날짜 (YYYY-MM-DD, `withTime`이면 YYYY-MM-DDTHH:mm). 주어지면 controlled로 동작. */
6
6
  value?: string;
7
7
  onChange?: (value: string) => void;
8
8
  /** 선택 가능 최소 날짜 (YYYY-MM-DD, 포함). */
@@ -13,10 +13,22 @@ interface DatePickerProps {
13
13
  invalid?: boolean;
14
14
  disabled?: boolean;
15
15
  size?: 'sm' | 'md' | 'lg';
16
+ /**
17
+ * 시각까지 받는다 — 값이 `YYYY-MM-DDTHH:mm`이 된다(`<input type="datetime-local">`과
18
+ * 같은 모양이라 서버로 가는 문자열은 그대로다). 칸에는 `2026-08-18 14:30`으로 보인다.
19
+ */
20
+ withTime?: boolean;
16
21
  }
17
22
  interface DateRangeValue {
18
23
  from?: string;
19
24
  to?: string;
25
+ /**
26
+ * 시작·종료일이 모두 정해지면 함께 실리는 **양끝 포함** 총 일수
27
+ * (`round((to-from)/86400000)+1`). SDUI `$calc`에는 날짜 연산이 없어 스펙이
28
+ * "총 N일" 배지를 그리려면 위젯이 일수를 값에 실어 줘야 한다. 서버 폼 제출에는
29
+ * 잉여 키로 무시된다.
30
+ */
31
+ days?: number;
20
32
  }
21
33
  interface DateRangePickerProps {
22
34
  /** 선택된 기간 (YYYY-MM-DD). 주어지면 controlled로 동작. */
@@ -26,6 +38,7 @@ interface DateRangePickerProps {
26
38
  max?: string;
27
39
  invalid?: boolean;
28
40
  disabled?: boolean;
41
+ size?: 'sm' | 'md' | 'lg';
29
42
  }
30
43
  /** 바깥에서 온 `style`·`className`을 뿌리에 얹어 내보낸다. 까닭은 {@link ./boxed.tsx}에 있다. */
31
44
  declare const DatePicker: React__default.FC<DatePickerProps & SduiBoxProps>;
@@ -1,5 +1,5 @@
1
- import { resolveTreeNodes, TreeView } from './chunk-PYSTET2C.js';
2
- import { OverlayLayer } from './chunk-XQPTSEJC.js';
1
+ import { resolveTreeNodes, TreeView } from './chunk-5H5DSIQW.js';
2
+ import { OverlayLayer } from './chunk-L6HNC4B5.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';
@@ -1,4 +1,4 @@
1
- import { buildCascadingTree } from './chunk-GZB6YLUI.js';
1
+ import { buildCascadingTree } from './chunk-VAXBRGDZ.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,4 +1,4 @@
1
- import { includesLooseSearch } from './chunk-GZB6YLUI.js';
1
+ import { includesLooseSearch } from './chunk-VAXBRGDZ.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';
@@ -66,6 +66,17 @@ function AutocompleteFieldImpl({
66
66
  const [results, setResults] = useState([]);
67
67
  const [activeIndex, setActiveIndex] = useState(0);
68
68
  const [focusPending, setFocusPending] = useState(false);
69
+ const listRef = useRef(null);
70
+ useEffect(() => {
71
+ if (!open) return;
72
+ const box = listRef.current;
73
+ const active = box?.querySelector(`#${CSS.escape(`${listId}-opt-${activeIndex}`)}`);
74
+ if (!box || !active) return;
75
+ const top = active.offsetTop;
76
+ const bottom = top + active.offsetHeight;
77
+ if (top < box.scrollTop) box.scrollTop = top;
78
+ else if (bottom > box.scrollTop + box.clientHeight) box.scrollTop = bottom - box.clientHeight;
79
+ }, [open, activeIndex, listId, results]);
69
80
  useEffect(() => {
70
81
  if (disabled || hasSelection) return;
71
82
  const keyword = query.trim();
@@ -235,9 +246,15 @@ function AutocompleteFieldImpl({
235
246
  "div",
236
247
  {
237
248
  id: listId,
249
+ ref: listRef,
238
250
  role: "listbox",
239
251
  className: "erp-popover__body",
240
- style: { maxHeight: 280, padding: "var(--wp-space-1)" },
252
+ style: {
253
+ maxHeight: 280,
254
+ overflowY: "auto",
255
+ overscrollBehavior: "contain",
256
+ padding: "var(--wp-space-1)"
257
+ },
241
258
  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: [
242
259
  /* @__PURE__ */ jsx(Icon, { name: "unplug", size: 16, style: { color: "var(--wp-text-muted)", flex: "none" } }),
243
260
  /* @__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" })
@@ -1,4 +1,5 @@
1
- import { SegmentedControl, Select, Input } from './chunk-WIDDHR7L.js';
1
+ import { DateRangePicker } from './chunk-L6HNC4B5.js';
2
+ import { SegmentedControl, Select } from './chunk-WIDDHR7L.js';
2
3
  import { boxed } from './chunk-SAN4IX6N.js';
3
4
  import { useState, useMemo } from 'react';
4
5
  import { jsxs, jsx } from 'react/jsx-runtime';
@@ -142,15 +143,11 @@ function PeriodFilterImpl({ value, onChange, years, modes }) {
142
143
  if (period.year !== void 0 && !base.includes(period.year)) base.push(period.year);
143
144
  return base.sort((a, b) => b - a).map((y) => ({ value: String(y), label: `${y}\uB144` }));
144
145
  }, [years, period.year]);
145
- const handleFromChange = (from) => {
146
- if (!isIsoDate(from)) return;
147
- const to = period.to !== void 0 && period.to >= from ? period.to : from;
148
- emit({ mode: "custom", from, to });
149
- };
150
- const handleToChange = (to) => {
151
- if (!isIsoDate(to)) return;
152
- const from = period.from !== void 0 && period.from <= to ? period.from : to;
153
- emit({ mode: "custom", from, to });
146
+ const handleRangeChange = (next) => {
147
+ const from = next.from ?? next.to;
148
+ const to = next.to ?? next.from;
149
+ if (!isIsoDate(from) || !isIsoDate(to)) return;
150
+ emit({ mode: "custom", from, to: to >= from ? to : from });
154
151
  };
155
152
  return /* @__PURE__ */ jsxs(
156
153
  "div",
@@ -210,7 +207,7 @@ function PeriodFilterImpl({ value, onChange, years, modes }) {
210
207
  onChange: (e) => emit(normalizePeriod({ ...period, month: Number(e.target.value) }))
211
208
  }
212
209
  ) }) : null,
213
- period.mode === "custom" ? /* @__PURE__ */ jsxs(
210
+ period.mode === "custom" ? /* @__PURE__ */ jsx(
214
211
  "span",
215
212
  {
216
213
  style: {
@@ -219,29 +216,13 @@ function PeriodFilterImpl({ value, onChange, years, modes }) {
219
216
  gap: "var(--wp-space-1)",
220
217
  flex: "none"
221
218
  },
222
- children: [
223
- /* @__PURE__ */ jsx("span", { style: { width: 148, flex: "none" }, children: /* @__PURE__ */ jsx(
224
- Input,
225
- {
226
- type: "date",
227
- "aria-label": "\uC2DC\uC791\uC77C",
228
- value: period.from ?? "",
229
- max: period.to,
230
- onChange: (e) => handleFromChange(e.target.value)
231
- }
232
- ) }),
233
- /* @__PURE__ */ jsx("span", { style: { color: "var(--wp-text-subtle)" }, "aria-hidden": true, children: "~" }),
234
- /* @__PURE__ */ jsx("span", { style: { width: 148, flex: "none" }, children: /* @__PURE__ */ jsx(
235
- Input,
236
- {
237
- type: "date",
238
- "aria-label": "\uC885\uB8CC\uC77C",
239
- value: period.to ?? "",
240
- min: period.from,
241
- onChange: (e) => handleToChange(e.target.value)
242
- }
243
- ) })
244
- ]
219
+ children: /* @__PURE__ */ jsx("span", { style: { width: 320, flex: "none" }, children: /* @__PURE__ */ jsx(
220
+ DateRangePicker,
221
+ {
222
+ value: { from: period.from, to: period.to },
223
+ onChange: handleRangeChange
224
+ }
225
+ ) })
245
226
  }
246
227
  ) : null
247
228
  ]