@wipco/sdui 0.21.0 → 0.23.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,102 +1,9 @@
1
1
  import { useDismiss, usePopoverAnchor, usePortalTo, HoistedStyle, AnchoredPopoverStyles, anchorStyle, focusStaysInside } from './chunk-XKLUCUVA.js';
2
- import { Icon } from './chunk-WIDDHR7L.js';
2
+ import { Icon, Spinner, EmptyState } from './chunk-WIDDHR7L.js';
3
3
  import { boxed } from './chunk-SAN4IX6N.js';
4
- import { createContext, useState, useRef, useEffect, useMemo, useCallback, useContext } from 'react';
4
+ import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
5
5
  import { jsxs, jsx } from 'react/jsx-runtime';
6
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
7
  var HM_RE = /^\d{2}:\d{2}$/;
101
8
  var LIST_HEIGHT = 260;
102
9
  var MINUTES_PER_DAY = 24 * 60;
@@ -287,5 +194,66 @@ var TP_CSS = `
287
194
  .wp-tp-opt--on{background:var(--wp-action-primary-bg);color:var(--wp-action-primary-fg);font-weight:500}
288
195
  `;
289
196
  var TimePicker = boxed(TimePickerImpl, "TimePicker");
197
+ function useOptionalModule(load) {
198
+ const [state, setState] = useState({ status: "loading" });
199
+ useEffect(() => {
200
+ let alive = true;
201
+ load().then(
202
+ (module) => {
203
+ if (alive) setState({ status: "ready", module });
204
+ },
205
+ (error) => {
206
+ if (!alive) return;
207
+ console.warn("[sdui] \uC120\uD0DD \uC758\uC874\uC131 \uB85C\uB4DC \uC2E4\uD328 \u2014 \uD50C\uB808\uC774\uC2A4\uD640\uB354\uB85C \uB300\uCCB4\uD569\uB2C8\uB2E4", error);
208
+ setState({ status: "missing" });
209
+ }
210
+ );
211
+ return () => {
212
+ alive = false;
213
+ };
214
+ }, [load]);
215
+ return state;
216
+ }
217
+ var optionalModuleCache = /* @__PURE__ */ new WeakMap();
218
+ function loadOptionalModule(load, packageName) {
219
+ const cached = optionalModuleCache.get(load);
220
+ if (cached) return cached;
221
+ const pending = load().catch((error) => {
222
+ console.warn(`[sdui] \uC120\uD0DD \uC758\uC874\uC131 '${packageName}' \uB85C\uB4DC \uC2E4\uD328`, error);
223
+ optionalModuleCache.delete(load);
224
+ throw new Error(`${packageName} \uD328\uD0A4\uC9C0\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4 \u2014 \uD504\uB85C\uC81D\uD2B8\uC5D0 \uC124\uCE58\uD574 \uC8FC\uC138\uC694`);
225
+ });
226
+ optionalModuleCache.set(load, pending);
227
+ return pending;
228
+ }
229
+ function DependencyPlaceholder({
230
+ packageName,
231
+ purpose,
232
+ height,
233
+ loading = false
234
+ }) {
235
+ return /* @__PURE__ */ jsx(
236
+ "div",
237
+ {
238
+ style: {
239
+ minHeight: height,
240
+ display: "flex",
241
+ alignItems: "center",
242
+ justifyContent: "center",
243
+ border: "1px solid var(--wp-border-subtle)",
244
+ borderRadius: "var(--wp-radius-md)",
245
+ background: "var(--wp-surface-sunken)"
246
+ },
247
+ children: loading ? /* @__PURE__ */ jsx(Spinner, { label: `${purpose} \uC900\uBE44 \uC911` }) : /* @__PURE__ */ jsxs(EmptyState, { tight: true, icon: "box", title: `${purpose}\uB97C \uADF8\uB9AC\uB824\uBA74 ${packageName} \uD328\uD0A4\uC9C0\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4`, children: [
248
+ "\uC120\uD0DD \uC758\uC874\uC131\uC774\uB77C \uAE30\uBCF8 \uC124\uCE58\uC5D0\uB294 \uD3EC\uD568\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 ",
249
+ /* @__PURE__ */ jsxs("code", { children: [
250
+ "pnpm add ",
251
+ packageName
252
+ ] }),
253
+ " \uD6C4 \uB2E4\uC2DC \uC5F4\uC5B4 \uC8FC\uC138\uC694."
254
+ ] })
255
+ }
256
+ );
257
+ }
290
258
 
291
- export { DEV, SduiAdapterContext, TimePicker, assetResolverOf, defaultAdapter, hostFetchOf, isSafeNavigationTarget, resolveAdapter, useSduiAdapter, useSduiAssetResolver, useSduiSandbox };
259
+ export { DependencyPlaceholder, TimePicker, loadOptionalModule, useOptionalModule };
@@ -0,0 +1,100 @@
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
+ // init을 반드시 흘린다 — 버리면 드롭인 소비자의 업로드가 본문 없는 GET이 된다.
57
+ rawFetch: (path, init) => fetch(path, { credentials: "include", ...init }),
58
+ // 항등함수. 계약을 문서화하고, 컴포넌트가 `useSduiAssetResolver()`를 조건 없이
59
+ // 부를 수 있게 하는 것이 전부다. **이 함수 참조 자체가 표식**이기도 하다 —
60
+ // `assetResolverOf`가 이것과 같은지 보고 "주입 없음"을 판정한다.
61
+ resolveAsset: (url) => url,
62
+ invalidate: (prefix) => {
63
+ if (DEV) console.warn(`[sdui] invalidate \uC561\uC158\uC774 \uC5B4\uB311\uD130 \uC5C6\uC774 \uD638\uCD9C\uB428 (prefix: ${prefix})`);
64
+ },
65
+ refresh: () => {
66
+ if (DEV) console.warn("[sdui] refresh \uC561\uC158\uC774 \uC5B4\uB311\uD130 \uC5C6\uC774 \uD638\uCD9C\uB428");
67
+ }
68
+ };
69
+ function resolveAdapter(adapter) {
70
+ const merged = { ...defaultAdapter };
71
+ if (adapter) {
72
+ for (const [key, value] of Object.entries(adapter)) {
73
+ if (value !== void 0) merged[key] = value;
74
+ }
75
+ }
76
+ return merged;
77
+ }
78
+ var SduiAdapterContext = createContext(null);
79
+ function useSduiAdapter() {
80
+ return useContext(SduiAdapterContext);
81
+ }
82
+ function assetResolverOf(adapter) {
83
+ const resolve = adapter?.resolveAsset;
84
+ if (!resolve || resolve === defaultAdapter.resolveAsset) return null;
85
+ return resolve;
86
+ }
87
+ function useSduiAssetResolver() {
88
+ const adapter = useContext(SduiAdapterContext);
89
+ return adapter?.resolveAsset ?? defaultAdapter.resolveAsset;
90
+ }
91
+ function useSduiSandbox() {
92
+ return useContext(SduiAdapterContext)?.sandbox === true;
93
+ }
94
+ function hostFetchOf(adapter) {
95
+ const hostFetch = adapter?.fetch;
96
+ if (!hostFetch || hostFetch === defaultAdapter.fetch) return null;
97
+ return hostFetch;
98
+ }
99
+
100
+ export { DEV, SduiAdapterContext, assetResolverOf, defaultAdapter, hostFetchOf, isSafeNavigationTarget, resolveAdapter, useSduiAdapter, useSduiAssetResolver, useSduiSandbox };
@@ -1,9 +1,24 @@
1
1
  import { includesLooseSearch } from './chunk-U7ZK7WYH.js';
2
+ import { useSduiAdapter, hostFetchOf } from './chunk-DVOPZFQS.js';
2
3
  import { Icon, Button, Input, Spinner } from './chunk-WIDDHR7L.js';
3
4
  import { boxed } from './chunk-SAN4IX6N.js';
4
- import { useId, useRef, useState, useEffect, useMemo } from 'react';
5
+ import { useId, useRef, useMemo, useState, useEffect } from 'react';
5
6
  import { jsxs, jsx } from 'react/jsx-runtime';
6
7
 
8
+ function endpointSearchFn(endpoint, searchParam, hostFetch) {
9
+ return async (keyword, signal) => {
10
+ const sep = endpoint.includes("?") ? "&" : "?";
11
+ const url = `${endpoint}${sep}${searchParam}=${encodeURIComponent(keyword)}`;
12
+ try {
13
+ return toAutocompleteOptions(await hostFetch(url, { method: "GET", signal }));
14
+ } catch (error) {
15
+ if (error?.status === 503) {
16
+ return { options: [], status: "offline" };
17
+ }
18
+ throw error;
19
+ }
20
+ };
21
+ }
7
22
  function isRecord(value) {
8
23
  return typeof value === "object" && value !== null && !Array.isArray(value);
9
24
  }
@@ -34,6 +49,8 @@ var statusRowStyle = {
34
49
  };
35
50
  function AutocompleteFieldImpl({
36
51
  search,
52
+ endpoint,
53
+ searchParam = "q",
37
54
  options,
38
55
  labelKey = "label",
39
56
  valueKey = "value",
@@ -52,9 +69,17 @@ function AutocompleteFieldImpl({
52
69
  const seqRef = useRef(0);
53
70
  const optionsRef = useRef(options);
54
71
  optionsRef.current = options;
55
- const searchRef = useRef(search);
56
- searchRef.current = search;
57
- const hasSearch = search !== void 0;
72
+ const adapter = useSduiAdapter();
73
+ const endpointSearch = useMemo(() => {
74
+ if (!endpoint) return void 0;
75
+ const hostFetch = hostFetchOf(adapter);
76
+ if (!hostFetch) return void 0;
77
+ return endpointSearchFn(endpoint, searchParam, hostFetch);
78
+ }, [endpoint, searchParam, adapter]);
79
+ const effectiveSearch = search ?? endpointSearch;
80
+ const searchRef = useRef(effectiveSearch);
81
+ searchRef.current = effectiveSearch;
82
+ const hasSearch = effectiveSearch !== void 0;
58
83
  const isControlled = value !== void 0;
59
84
  const [internalValue, setInternalValue] = useState("");
60
85
  const committed = (isControlled ? value : internalValue) ?? "";
@@ -298,4 +323,4 @@ function AutocompleteFieldImpl({
298
323
  }
299
324
  var AutocompleteField = boxed(AutocompleteFieldImpl, "AutocompleteField");
300
325
 
301
- export { AutocompleteField, toAutocompleteOptions };
326
+ export { AutocompleteField, endpointSearchFn, toAutocompleteOptions };
@@ -0,0 +1,211 @@
1
+ import { Icon, Badge, Tooltip } from './chunk-WIDDHR7L.js';
2
+ import { boxed } from './chunk-SAN4IX6N.js';
3
+ import React from 'react';
4
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+
6
+ var STATUS_META = {
7
+ approved: {
8
+ icon: "check",
9
+ label: "\uC2B9\uC778",
10
+ bg: "var(--wp-status-success-bg)",
11
+ border: "var(--wp-status-success-border)",
12
+ fg: "var(--wp-status-success-fg)"
13
+ },
14
+ rejected: {
15
+ icon: "x",
16
+ label: "\uBC18\uB824",
17
+ bg: "var(--wp-status-danger-bg)",
18
+ border: "var(--wp-status-danger-border)",
19
+ fg: "var(--wp-status-danger-fg)"
20
+ },
21
+ skipped: {
22
+ icon: "minus",
23
+ label: "\uAC74\uB108\uB700",
24
+ bg: "var(--wp-status-neutral-bg)",
25
+ border: "var(--wp-status-neutral-border)",
26
+ fg: "var(--wp-status-neutral-fg)"
27
+ },
28
+ current: {
29
+ icon: "circle-dot",
30
+ label: "\uC9C4\uD589 \uC911",
31
+ bg: "var(--wp-surface-brand-tint)",
32
+ border: "var(--wp-border-brand)",
33
+ fg: "var(--wp-text-brand)"
34
+ },
35
+ pending: {
36
+ icon: "circle-dot",
37
+ label: "\uB300\uAE30",
38
+ bg: "var(--wp-surface-card)",
39
+ border: "var(--wp-border-subtle)",
40
+ fg: "var(--wp-text-muted)"
41
+ }
42
+ };
43
+ function stepTitle(step, index, typeLabels) {
44
+ const name = step.name?.trim();
45
+ if (name) return name;
46
+ if (step.type) return typeLabels?.[step.type] ?? step.type;
47
+ return `${index + 1}\uB2E8\uACC4`;
48
+ }
49
+ function formatDateTime(value) {
50
+ const time = Date.parse(value);
51
+ if (Number.isNaN(time)) return value;
52
+ const date = new Date(time);
53
+ const pad = (part) => String(part).padStart(2, "0");
54
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
55
+ }
56
+ var circleStyle = (bg, border, fg, ring) => ({
57
+ display: "inline-flex",
58
+ alignItems: "center",
59
+ justifyContent: "center",
60
+ width: 32,
61
+ height: 32,
62
+ flex: "none",
63
+ borderRadius: "var(--wp-radius-pill)",
64
+ background: bg,
65
+ border: `var(--wp-border-w) solid ${border}`,
66
+ color: fg,
67
+ boxShadow: ring ? "var(--wp-ring-selected)" : void 0
68
+ });
69
+ var nodeStyle = {
70
+ display: "flex",
71
+ flexDirection: "column",
72
+ alignItems: "center",
73
+ gap: "var(--wp-space-1)",
74
+ flex: "none",
75
+ minWidth: 88,
76
+ maxWidth: 140,
77
+ padding: "var(--wp-space-1)"
78
+ };
79
+ var nodeTextStyle = {
80
+ display: "block",
81
+ maxWidth: "100%",
82
+ overflow: "hidden",
83
+ textOverflow: "ellipsis",
84
+ whiteSpace: "nowrap",
85
+ textAlign: "center"
86
+ };
87
+ var connectorStyle = {
88
+ flex: "none",
89
+ width: "var(--wp-space-6)",
90
+ height: 0,
91
+ borderTop: "var(--wp-border-w) solid var(--wp-divider)",
92
+ // 원(32px) 세로 중앙에 맞춘다: 노드 padding(space-1) + 16px
93
+ marginTop: "calc(var(--wp-space-1) + 16px)"
94
+ };
95
+ function stepTooltip(step, index, typeLabels) {
96
+ const meta = STATUS_META[step.status ?? "pending"];
97
+ const lines = [`${stepTitle(step, index, typeLabels)} \u2014 ${meta.label}`];
98
+ if (step.approver) lines.push(`\uCC98\uB9AC\uC790: ${step.approver}`);
99
+ if (step.processedAt) lines.push(`\uCC98\uB9AC \uC77C\uC2DC: ${formatDateTime(step.processedAt)}`);
100
+ if (step.candidates && step.candidates.length > 0) {
101
+ lines.push(`\uACB0\uC7AC \uD6C4\uBCF4: ${step.candidates.join(", ")}`);
102
+ }
103
+ return /* @__PURE__ */ jsx(Fragment, { children: lines.map((line) => /* @__PURE__ */ jsx("span", { style: { display: "block" }, children: line }, line)) });
104
+ }
105
+ function ApprovalStepperImpl({
106
+ requester,
107
+ steps,
108
+ onStepClick,
109
+ typeLabels
110
+ }) {
111
+ const safeSteps = steps ?? [];
112
+ const clickable = typeof onStepClick === "function";
113
+ const requesterNode = requester ? /* @__PURE__ */ jsxs("div", { style: nodeStyle, children: [
114
+ /* @__PURE__ */ jsx(
115
+ "span",
116
+ {
117
+ style: circleStyle(
118
+ "var(--wp-surface-sunken)",
119
+ "var(--wp-border-subtle)",
120
+ "var(--wp-text-muted)",
121
+ false
122
+ ),
123
+ children: /* @__PURE__ */ jsx(Icon, { name: "user", size: 16 })
124
+ }
125
+ ),
126
+ /* @__PURE__ */ jsx("span", { className: "wp-body-sm", style: { ...nodeTextStyle, color: "var(--wp-text-strong)" }, children: requester.name }),
127
+ /* @__PURE__ */ jsx("span", { className: "wp-caption", style: nodeTextStyle, children: requester.sub?.trim() || "\uC2E0\uCCAD\uC790" })
128
+ ] }) : null;
129
+ if (safeSteps.length === 0) {
130
+ return /* @__PURE__ */ jsxs("div", { className: "wp-row", style: { overflowX: "auto", paddingBottom: "var(--wp-space-1)" }, children: [
131
+ requesterNode,
132
+ requesterNode ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, style: connectorStyle }) : null,
133
+ /* @__PURE__ */ jsxs("div", { style: nodeStyle, children: [
134
+ /* @__PURE__ */ jsxs(Badge, { tone: "success", size: "lg", children: [
135
+ /* @__PURE__ */ jsx(Icon, { name: "check-check", size: 14, style: { marginRight: "var(--wp-space-1)" } }),
136
+ "\uC790\uB3D9 \uC2B9\uC778"
137
+ ] }),
138
+ /* @__PURE__ */ jsx("span", { className: "wp-caption", style: { textAlign: "center", whiteSpace: "nowrap" }, children: "\uACB0\uC7AC \uB2E8\uACC4\uAC00 \uC5C6\uC5B4 \uC790\uB3D9\uC73C\uB85C \uC2B9\uC778\uB429\uB2C8\uB2E4" })
139
+ ] })
140
+ ] });
141
+ }
142
+ return /* @__PURE__ */ jsxs(
143
+ "div",
144
+ {
145
+ className: "wp-row",
146
+ role: "list",
147
+ "aria-label": "\uACB0\uC7AC \uC9C4\uD589 \uB2E8\uACC4",
148
+ style: {
149
+ alignItems: "flex-start",
150
+ gap: 0,
151
+ overflowX: "auto",
152
+ paddingBottom: "var(--wp-space-1)"
153
+ },
154
+ children: [
155
+ requesterNode,
156
+ safeSteps.map((step, index) => {
157
+ const status = step.status ?? "pending";
158
+ const meta = STATUS_META[status];
159
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
160
+ /* @__PURE__ */ jsx("span", { style: circleStyle(meta.bg, meta.border, meta.fg, status === "current"), children: /* @__PURE__ */ jsx(Icon, { name: meta.icon, size: 16 }) }),
161
+ /* @__PURE__ */ jsx(
162
+ "span",
163
+ {
164
+ className: "wp-body-sm",
165
+ style: {
166
+ ...nodeTextStyle,
167
+ color: status === "skipped" ? "var(--wp-text-muted)" : "var(--wp-text-strong)"
168
+ },
169
+ children: stepTitle(step, index, typeLabels)
170
+ }
171
+ ),
172
+ /* @__PURE__ */ jsx(
173
+ "span",
174
+ {
175
+ className: "wp-caption",
176
+ style: {
177
+ ...nodeTextStyle,
178
+ color: status === "rejected" ? "var(--wp-text-danger)" : status === "current" ? "var(--wp-text-brand)" : void 0
179
+ },
180
+ children: step.approver?.trim() || meta.label
181
+ }
182
+ )
183
+ ] });
184
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
185
+ (requesterNode || index > 0) && /* @__PURE__ */ jsx("span", { "aria-hidden": true, style: connectorStyle }),
186
+ /* @__PURE__ */ jsx(Tooltip, { label: stepTooltip(step, index, typeLabels), wrap: true, placement: "top", children: clickable ? /* @__PURE__ */ jsx(
187
+ "button",
188
+ {
189
+ type: "button",
190
+ role: "listitem",
191
+ onClick: () => onStepClick?.(step),
192
+ style: {
193
+ ...nodeStyle,
194
+ background: "none",
195
+ border: 0,
196
+ font: "inherit",
197
+ cursor: "pointer",
198
+ borderRadius: "var(--wp-radius-sm)"
199
+ },
200
+ children: body
201
+ }
202
+ ) : /* @__PURE__ */ jsx("div", { role: "listitem", style: nodeStyle, children: body }) })
203
+ ] }, step.id);
204
+ })
205
+ ]
206
+ }
207
+ );
208
+ }
209
+ var ApprovalStepper = boxed(ApprovalStepperImpl, "ApprovalStepper");
210
+
211
+ export { ApprovalStepper };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { CSSProperties, ReactNode, ComponentType } from 'react';
3
- import { S as SduiPage, a as SduiAction, b as SduiPropValue, c as SduiRule } from './types-opTPYysN.js';
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';
3
+ import { S as SduiPage, a as SduiAction, b as SduiPropValue, c as SduiRule } from './types-DmHC7HCN.js';
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-DmHC7HCN.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
7
  import { D as DatePickerProps, a as DateRangePickerProps, T as TimePickerProps } from './DatePicker-D2EtpvkQ.js';
@@ -19,6 +19,11 @@ interface SduiAdapter {
19
19
  * submitForm 등 JSON API 호출. 성공 시 응답 JSON을, 실패 시
20
20
  * Error(사용자에게 보여줄 메시지)를 던진다.
21
21
  *
22
+ * 던지는 오류에 **`status`(HTTP 상태)와 `body`(파싱된 응답 본문)를 얹어도 된다.**
23
+ * 필수는 아니지만, 얹으면 스펙이 상태 코드로 갈라 쓸 수 있다(`request` 액션의
24
+ * `errorKey` 참고 — 409의 위반 목록을 화면에 그리는 것이 그 자리다). 라이브러리는
25
+ * 있으면 읽고 없으면 메시지만 쓴다 — 특정 앱의 오류 타입을 알지 않는다.
26
+ *
22
27
  * `signal`은 호출부가 중단할 수 있는 조회(목록 재조회 등)에서만 넘어온다 —
23
28
  * 선택 사항이라 무시해도 계약은 성립하지만, 넘겨받은 호스트는 자기 클라이언트에
24
29
  * 그대로 흘려 보내면 파라미터가 바뀔 때 이전 요청이 실제로 끊긴다.
@@ -28,8 +33,25 @@ interface SduiAdapter {
28
33
  body?: unknown;
29
34
  signal?: AbortSignal;
30
35
  }) => Promise<unknown>;
31
- /** download 액션용 원시 fetch. 기본: fetch(path, { credentials: 'include' }) */
32
- rawFetch?: (path: string) => Promise<Response>;
36
+ /**
37
+ * 원시 fetch — **직렬화하지 않는 통로**다. `fetch`가 본문을 JSON으로 굳혀 보내는 것과
38
+ * 정반대이고, 그 차이가 이 항목의 존재 이유다: 업로드의 `FormData`는 경계 문자열
39
+ * (boundary)을 브라우저가 붙여야 하는데 `JSON.stringify`를 태우면 `"{}"`가 된다.
40
+ *
41
+ * `init`을 받는다. 예전에는 경로 하나만 받아 download 액션의 GET만 낼 수 있었고,
42
+ * multipart POST나 DELETE를 실을 자리가 없었다.
43
+ *
44
+ * ⚠ **호스트 구현을 1인자로 남겨 두면 안 된다.** `(path) => myFetch(path)`처럼 쓰면
45
+ * 타입은 통과하는데 `init`이 조용히 버려져 POST가 본문 없는 GET으로 나간다.
46
+ * 타입 검사로도 {@link hostRawFetchOf}로도 잡히지 않는 자리라 여기 적어 둔다.
47
+ */
48
+ rawFetch?: (path: string, init?: {
49
+ method?: string;
50
+ /** 직렬화하지 않고 그대로 흘린다 — FormData·Blob·string 전부 가능. */
51
+ body?: BodyInit;
52
+ headers?: Record<string, string>;
53
+ signal?: AbortSignal;
54
+ }) => Promise<Response>;
33
55
  /** invalidate 액션 — 클라이언트 조회 캐시 무효화 (TanStack Query 등) */
34
56
  invalidate?: (prefix: string, exact: boolean) => void;
35
57
  /** refresh 액션 — 페이지 스펙 재조회 */
@@ -1677,6 +1699,23 @@ interface SduiRendererProps {
1677
1699
  */
1678
1700
  declare function SduiRenderer({ page, registry, adapter, syncUrlState, initialState, can, onCustomAction, customValidators, }: SduiRendererProps): React__default.JSX.Element;
1679
1701
 
1702
+ /**
1703
+ * 어휘 전체. **렌더러와 편집기 검증기가 같은 이 집합을 본다** — 목록이 두 벌이면
1704
+ * 한쪽에만 연산을 더했을 때 "렌더는 되는데 저장이 거부되는" 스펙이 생긴다.
1705
+ */
1706
+ declare const CALC_OPS: ReadonlySet<string>;
1707
+
1708
+ /**
1709
+ * 선택적 peer 의존성(recharts · ol)을 런타임에 끌어오는 공용 장치.
1710
+ *
1711
+ * 이 진입점(@wipco/sdui/rich)의 위젯은 무거운 라이브러리를 쓰지만, 그 라이브러리는
1712
+ * optional peerDependency다 — 안 깔려 있어도 렌더가 죽으면 안 된다. 그래서
1713
+ * 모듈은 동적 import로만 만지고, 로드에 실패하면 예외를 그대로 흘리지 않고
1714
+ * "무엇을 설치해야 하는지" 알려주는 플레이스홀더로 떨어진다.
1715
+ */
1716
+
1717
+ declare function loadOptionalModule<T>(load: () => Promise<T>, packageName: string): Promise<T>;
1718
+
1680
1719
  /**
1681
1720
  * 스펙 구조 검증기 — 서버(또는 편집기)가 만든 스펙의 형태 오류를 렌더 전에 찾아낸다.
1682
1721
  * 렌더러는 개발 모드에서 이 결과를 콘솔에 경고하고, 관리자 편집기는
@@ -1831,4 +1870,4 @@ interface ImageViewerProps {
1831
1870
  */
1832
1871
  declare function ImageViewer({ src, alt, open, onClose }: ImageViewerProps): React__default.ReactPortal | null;
1833
1872
 
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 };
1873
+ export { Actions, type ActionsProps, Alert, type AlertProps, Avatar, type AvatarProps, Badge, type BadgeProps, type BadgeTone, Breadcrumb, type BreadcrumbItem, type BreadcrumbProps, Button, type ButtonProps, CALC_OPS, 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, loadOptionalModule, normalizePageSizes, normalizeSearchText, searchFieldKeys, searchValuesFromParams, sortableDropIndex, sortableMove, splitMultiValue, useOverlayLayer, useRemoteSearchPage, useSduiAssetResolver, useSduiCan, useSduiDispatch, useSduiRuntime, useSduiSandbox, useSearchFetcher, validateSpec };