@wipco/sdui 0.31.0 → 0.33.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.
@@ -0,0 +1,122 @@
1
+ import { Text } from './chunk-QO6UJAQN.js';
2
+ import { Switch, Field, Select, Input } from './chunk-WIDDHR7L.js';
3
+ import { boxed } from './chunk-SAN4IX6N.js';
4
+ import React, { useMemo } from 'react';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
+
7
+ function normalizeSchema(schema) {
8
+ if (!Array.isArray(schema)) return [];
9
+ return schema.filter((item) => Boolean(item) && typeof item === "object").filter((item) => typeof item.key === "string" && item.key.trim() !== "");
10
+ }
11
+ function OptionValueFieldsImpl({
12
+ schema,
13
+ value,
14
+ onChange,
15
+ disabled = false,
16
+ readOnly = false
17
+ }) {
18
+ const fields = useMemo(() => normalizeSchema(schema), [schema]);
19
+ const current = useMemo(
20
+ () => value && typeof value === "object" && !Array.isArray(value) ? value : {},
21
+ [value]
22
+ );
23
+ const strays = useMemo(() => {
24
+ const known = new Set(fields.map((field) => field.key));
25
+ return Object.entries(current).filter(([key]) => !known.has(key));
26
+ }, [fields, current]);
27
+ const set = (key, next) => {
28
+ const draft = { ...current };
29
+ if (next === "" || next === void 0 || next === null) delete draft[key];
30
+ else draft[key] = next;
31
+ onChange?.(draft);
32
+ };
33
+ if (readOnly) {
34
+ const filled = fields.filter((field) => {
35
+ const raw = current[field.key];
36
+ return raw !== void 0 && raw !== null && raw !== "";
37
+ });
38
+ if (fields.length === 0 || filled.length === 0) {
39
+ return /* @__PURE__ */ jsx(Text, { preset: "body-sm", style: { color: "var(--wp-text-muted)" }, value: "\u2014" });
40
+ }
41
+ return /* @__PURE__ */ jsx("div", { className: "wp-kv", children: filled.map((field) => {
42
+ const raw = current[field.key];
43
+ const shown = field.type === "boolean" ? raw === true ? "\uC608" : "\uC544\uB2C8\uC624" : `${String(raw)}${field.unit ? ` ${field.unit}` : ""}`;
44
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
45
+ /* @__PURE__ */ jsx("span", { className: "wp-kv__k", children: field.label?.trim() || field.key }),
46
+ /* @__PURE__ */ jsx("span", { className: "wp-kv__v", children: shown })
47
+ ] }, field.key);
48
+ }) });
49
+ }
50
+ if (fields.length === 0) {
51
+ return /* @__PURE__ */ jsx(Text, { preset: "body-sm", style: { color: "var(--wp-text-muted)" }, children: "\uC774 \uC7A5\uBE44\uC5D0 \uC815\uD574 \uB454 \uC635\uC158\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uC7A5\uBE44 \uC0C1\uC138\uC758 \uC218\uC815 \u2192 \uC635\uC158 \uD0ED\uC5D0\uC11C \uBA3C\uC800 \uC11C\uC2DD\uC744 \uC815\uD558\uC138\uC694." });
52
+ }
53
+ return /* @__PURE__ */ jsxs("div", { className: "wp-col", style: { gap: "var(--wp-space-3)" }, children: [
54
+ fields.map((field) => {
55
+ const label = field.label?.trim() || field.key;
56
+ const raw = current[field.key];
57
+ if (field.type === "boolean") {
58
+ return /* @__PURE__ */ jsxs("div", { className: "wp-row", style: { alignItems: "center", gap: "var(--wp-space-3)" }, children: [
59
+ /* @__PURE__ */ jsx(Text, { preset: "body-sm", style: { flex: 1 }, value: label }),
60
+ /* @__PURE__ */ jsx(
61
+ Switch,
62
+ {
63
+ checked: raw === true,
64
+ disabled,
65
+ onChange: (event) => set(field.key, event.target.checked ? true : void 0)
66
+ }
67
+ )
68
+ ] }, field.key);
69
+ }
70
+ if (field.type === "select") {
71
+ return /* @__PURE__ */ jsx(Field, { label, children: /* @__PURE__ */ jsx(
72
+ Select,
73
+ {
74
+ value: raw === void 0 || raw === null ? "" : String(raw),
75
+ disabled,
76
+ placeholder: "\uC120\uD0DD \uC548 \uD568",
77
+ options: (field.options ?? []).map((option) => ({ value: option, label: option })),
78
+ onChange: (event) => set(field.key, event.target.value)
79
+ }
80
+ ) }, field.key);
81
+ }
82
+ if (field.type === "number") {
83
+ return /* @__PURE__ */ jsx(Field, { label: field.unit ? `${label} (${field.unit})` : label, children: /* @__PURE__ */ jsx(
84
+ Input,
85
+ {
86
+ type: "number",
87
+ value: raw === void 0 || raw === null ? "" : String(raw),
88
+ disabled,
89
+ onChange: (event) => {
90
+ const text = event.target.value;
91
+ if (text === "") return set(field.key, void 0);
92
+ const parsed = Number(text);
93
+ set(field.key, Number.isFinite(parsed) ? parsed : text);
94
+ }
95
+ }
96
+ ) }, field.key);
97
+ }
98
+ return /* @__PURE__ */ jsx(Field, { label, children: /* @__PURE__ */ jsx(
99
+ Input,
100
+ {
101
+ value: raw === void 0 || raw === null ? "" : String(raw),
102
+ disabled,
103
+ onChange: (event) => set(field.key, event.target.value)
104
+ }
105
+ ) }, field.key);
106
+ }),
107
+ strays.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "wp-col", style: { gap: "var(--wp-space-1)" }, children: [
108
+ /* @__PURE__ */ jsx(
109
+ Text,
110
+ {
111
+ preset: "caption",
112
+ value: `\uC11C\uC2DD \uBC16 \uAC12 ${strays.length}\uAC1C \u2014 \uADF8\uB300\uB85C \uBCF4\uAD00\uB429\uB2C8\uB2E4`,
113
+ style: { color: "var(--wp-text-muted)" }
114
+ }
115
+ ),
116
+ strays.map(([key, strayValue]) => /* @__PURE__ */ jsx(Text, { preset: "caption", value: `${key}: ${JSON.stringify(strayValue)}` }, key))
117
+ ] }) : null
118
+ ] });
119
+ }
120
+ var OptionValueFields = boxed(OptionValueFieldsImpl, "OptionValueFields");
121
+
122
+ export { OptionValueFields };
@@ -0,0 +1,124 @@
1
+ import { TimePicker } from './chunk-SNRKPEWT.js';
2
+ import { DatePicker } from './chunk-EFMKWPZ3.js';
3
+ import { Field } from './chunk-WIDDHR7L.js';
4
+ import { boxed } from './chunk-SAN4IX6N.js';
5
+ import { useState, useRef, useEffect, useMemo } from 'react';
6
+ import { jsxs, jsx } from 'react/jsx-runtime';
7
+
8
+ function toHm(value) {
9
+ if (typeof value !== "string") return "";
10
+ const trimmed = value.trim();
11
+ return /^\d{2}:\d{2}/.test(trimmed) ? trimmed.slice(0, 5) : trimmed;
12
+ }
13
+ function toYmd(value) {
14
+ if (typeof value !== "string") return "";
15
+ const trimmed = value.trim();
16
+ return /^\d{4}-\d{2}-\d{2}/.test(trimmed) ? trimmed.slice(0, 10) : trimmed;
17
+ }
18
+ function formFromValue(value) {
19
+ const record = value && typeof value === "object" ? value : {};
20
+ const startDate = toYmd(record.startDate);
21
+ const endDate = toYmd(record.endDate);
22
+ return {
23
+ startDate,
24
+ endDate,
25
+ // 시간은 날짜가 있어야 의미가 있다 — 저장분이 어긋나 있어도 폼에서 정합화.
26
+ startTime: startDate ? toHm(record.startTime) : "",
27
+ endTime: endDate ? toHm(record.endTime) : ""
28
+ };
29
+ }
30
+ function periodError(form) {
31
+ if (form.startDate && form.endDate && form.endDate < form.startDate) {
32
+ return "\uD3C9\uAC00 \uC885\uB8CC\uC77C\uC740 \uC2DC\uC791\uC77C\uBCF4\uB2E4 \uBE60\uB97C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.";
33
+ }
34
+ if (form.startDate && form.endDate && form.startDate === form.endDate && form.startTime && form.endTime && form.endTime <= form.startTime) {
35
+ return "\uC885\uB8CC \uC2DC\uAC04\uC740 \uC2DC\uC791 \uC2DC\uAC04\uBCF4\uB2E4 \uB2A6\uC5B4\uC57C \uD569\uB2C8\uB2E4.";
36
+ }
37
+ return null;
38
+ }
39
+ function envelopeFrom(form) {
40
+ return {
41
+ startDate: form.startDate || null,
42
+ endDate: form.endDate || null,
43
+ startTime: form.startDate && form.startTime || null,
44
+ endTime: form.endDate && form.endTime || null,
45
+ error: periodError(form)
46
+ };
47
+ }
48
+ function StagePeriodEditorImpl({ value, onChange, resetKey, disabled = false }) {
49
+ const [form, setForm] = useState(() => formFromValue(value));
50
+ const seededFor = useRef(resetKey);
51
+ useEffect(() => {
52
+ if (seededFor.current === resetKey) return;
53
+ seededFor.current = resetKey;
54
+ setForm(formFromValue(value));
55
+ }, [resetKey, value]);
56
+ const onChangeRef = useRef(onChange);
57
+ onChangeRef.current = onChange;
58
+ useEffect(() => {
59
+ onChangeRef.current?.(envelopeFrom(form));
60
+ }, [form]);
61
+ const error = useMemo(() => periodError(form), [form]);
62
+ const patch = (partial) => setForm((previous) => ({ ...previous, ...partial }));
63
+ return /* @__PURE__ */ jsxs("div", { className: "wp-col", style: { gap: "var(--wp-space-3)" }, children: [
64
+ /* @__PURE__ */ jsxs(
65
+ "div",
66
+ {
67
+ style: {
68
+ display: "grid",
69
+ gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
70
+ gap: "var(--wp-space-3)"
71
+ },
72
+ children: [
73
+ /* @__PURE__ */ jsx(Field, { label: "\uC2DC\uC791\uC77C", children: /* @__PURE__ */ jsx(
74
+ DatePicker,
75
+ {
76
+ value: form.startDate,
77
+ disabled,
78
+ max: form.endDate || void 0,
79
+ onChange: (next) => patch({
80
+ startDate: next,
81
+ // 시간은 날짜가 있어야 의미가 있으므로 날짜를 지우면 함께 비운다.
82
+ startTime: next ? form.startTime : ""
83
+ })
84
+ }
85
+ ) }),
86
+ /* @__PURE__ */ jsx(Field, { label: "\uC2DC\uC791 \uC2DC\uAC04 (\uC120\uD0DD)", children: /* @__PURE__ */ jsx(
87
+ TimePicker,
88
+ {
89
+ value: form.startTime,
90
+ disabled: disabled || !form.startDate,
91
+ onChange: (next) => patch({ startTime: next })
92
+ }
93
+ ) }),
94
+ /* @__PURE__ */ jsx(Field, { label: "\uC885\uB8CC\uC77C", children: /* @__PURE__ */ jsx(
95
+ DatePicker,
96
+ {
97
+ value: form.endDate,
98
+ disabled,
99
+ min: form.startDate || void 0,
100
+ onChange: (next) => patch({
101
+ endDate: next,
102
+ endTime: next ? form.endTime : ""
103
+ })
104
+ }
105
+ ) }),
106
+ /* @__PURE__ */ jsx(Field, { label: "\uC885\uB8CC \uC2DC\uAC04 (\uC120\uD0DD)", children: /* @__PURE__ */ jsx(
107
+ TimePicker,
108
+ {
109
+ min: form.endDate && form.endDate === form.startDate ? form.startTime || void 0 : void 0,
110
+ value: form.endTime,
111
+ disabled: disabled || !form.endDate,
112
+ onChange: (next) => patch({ endTime: next })
113
+ }
114
+ ) })
115
+ ]
116
+ }
117
+ ),
118
+ error ? /* @__PURE__ */ jsx("span", { className: "wp-caption", style: { color: "var(--wp-text-danger)" }, children: error }) : null,
119
+ /* @__PURE__ */ jsx("span", { className: "wp-caption", style: { color: "var(--wp-text-muted)" }, children: "\uB2E8\uACC4 \uAE30\uAC04\uC744 \uB530\uB85C \uC815\uD558\uC9C0 \uC54A\uC73C\uBA74 \uD68C\uCC28 \uAE30\uAC04\uC744 \uB530\uB985\uB2C8\uB2E4. \uD68C\uCC28 \uAE30\uAC04\uC744 \uBC14\uAFB8\uBA74 \uD68C\uCC28 \uAE30\uAC04\uC744 \uB530\uB974\uB358 \uB2E8\uACC4\uB9CC \uD568\uAED8 \uBC14\uB01D\uB2C8\uB2E4." })
120
+ ] });
121
+ }
122
+ var StagePeriodEditor = boxed(StagePeriodEditorImpl, "StagePeriodEditor");
123
+
124
+ export { StagePeriodEditor };
@@ -0,0 +1,109 @@
1
+ import { useSduiAssetResolver } from './chunk-DKPNFEVU.js';
2
+ import { Spinner, Icon } from './chunk-WIDDHR7L.js';
3
+ import { boxed } from './chunk-SAN4IX6N.js';
4
+ import { useState, useEffect } from 'react';
5
+ import { jsxs, jsx } from 'react/jsx-runtime';
6
+
7
+ var FALLBACK_NOTE = "QR \uC774\uBBF8\uC9C0\uB97C \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4 \u2014 \uC11C\uBC84 \uC5F0\uACB0\uC744 \uD655\uC778\uD558\uC138\uC694";
8
+ var boxStyle = (size) => ({
9
+ width: size,
10
+ height: size,
11
+ display: "flex",
12
+ flexDirection: "column",
13
+ alignItems: "center",
14
+ justifyContent: "center",
15
+ gap: "var(--wp-space-2)"
16
+ });
17
+ function QRDisplayImpl({
18
+ path,
19
+ url,
20
+ size = 128,
21
+ endpoint = "/qr",
22
+ caption
23
+ }) {
24
+ const [absoluteUrl, setAbsoluteUrl] = useState(null);
25
+ const [failed, setFailed] = useState(false);
26
+ const resolveAsset = useSduiAssetResolver();
27
+ useEffect(() => {
28
+ setFailed(false);
29
+ try {
30
+ const base = window.location.origin;
31
+ const target = url ?? path ?? window.location.href;
32
+ setAbsoluteUrl(new URL(target, base).toString());
33
+ } catch {
34
+ setAbsoluteUrl(null);
35
+ setFailed(true);
36
+ }
37
+ }, [path, url]);
38
+ const raw = absoluteUrl !== null ? `${endpoint}${endpoint.includes("?") ? "&" : "?"}data=${encodeURIComponent(absoluteUrl)}&size=${size}` : null;
39
+ const src = raw === null ? null : resolveAsset(raw, { component: "QRDisplay", prop: "src", kind: "attribute" });
40
+ const pending = absoluteUrl === null && !failed;
41
+ return /* @__PURE__ */ jsxs(
42
+ "figure",
43
+ {
44
+ style: {
45
+ display: "inline-flex",
46
+ flexDirection: "column",
47
+ alignItems: "center",
48
+ gap: "var(--wp-space-3)",
49
+ margin: 0,
50
+ padding: "var(--wp-space-4)",
51
+ background: "var(--wp-surface-card)",
52
+ border: "1px solid var(--wp-border-subtle)",
53
+ borderRadius: "var(--wp-radius-md)",
54
+ boxShadow: "var(--wp-shadow-xs)"
55
+ },
56
+ children: [
57
+ failed || src === null ? /* @__PURE__ */ jsx(
58
+ "div",
59
+ {
60
+ role: "img",
61
+ "aria-label": FALLBACK_NOTE,
62
+ style: {
63
+ ...boxStyle(size),
64
+ border: "1px dashed var(--wp-border-strong)",
65
+ borderRadius: "var(--wp-radius-md)",
66
+ background: "var(--wp-surface-sunken)",
67
+ color: "var(--wp-text-subtle)"
68
+ },
69
+ children: pending ? /* @__PURE__ */ jsx(Spinner, { label: "QR \uC900\uBE44 \uC911" }) : /* @__PURE__ */ jsx(Icon, { name: "scan", size: 24 })
70
+ }
71
+ ) : /* @__PURE__ */ jsx(
72
+ "img",
73
+ {
74
+ src,
75
+ width: size,
76
+ height: size,
77
+ alt: caption ? `QR \uCF54\uB4DC \u2014 ${caption}` : "QR \uCF54\uB4DC",
78
+ onError: () => setFailed(true),
79
+ style: {
80
+ display: "block",
81
+ borderRadius: "var(--wp-radius-xs)",
82
+ maxWidth: "100%"
83
+ }
84
+ }
85
+ ),
86
+ /* @__PURE__ */ jsxs(
87
+ "figcaption",
88
+ {
89
+ style: {
90
+ display: "flex",
91
+ flexDirection: "column",
92
+ alignItems: "center",
93
+ gap: "var(--wp-space-1)",
94
+ maxWidth: Math.max(size, 160),
95
+ textAlign: "center"
96
+ },
97
+ children: [
98
+ caption && /* @__PURE__ */ jsx("span", { className: "wp-caption", style: { color: "var(--wp-text-muted)" }, children: caption }),
99
+ failed && /* @__PURE__ */ jsx("span", { className: "wp-caption", style: { color: "var(--wp-text-subtle)" }, children: FALLBACK_NOTE })
100
+ ]
101
+ }
102
+ )
103
+ ]
104
+ }
105
+ );
106
+ }
107
+ var QRDisplay = boxed(QRDisplayImpl, "QRDisplay");
108
+
109
+ export { QRDisplay };
@@ -0,0 +1,145 @@
1
+ import { mergeStyle, containerLayoutStyle, joinClass, columnsClass, typographyStyle } from './chunk-SAN4IX6N.js';
2
+ import React from 'react';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+
5
+ function paddingStyle(padding) {
6
+ const value = padding === void 0 ? void 0 : PADDING_CSS[padding];
7
+ return value === void 0 ? {} : { padding: value };
8
+ }
9
+ var PADDING_CSS = {
10
+ "2": "2px",
11
+ "4": "4px",
12
+ "8": "8px",
13
+ "12": "12px",
14
+ "16": "16px",
15
+ "20": "20px",
16
+ "24": "24px",
17
+ "32": "32px"
18
+ };
19
+ function Grid({
20
+ columns = 3,
21
+ template,
22
+ gap,
23
+ align,
24
+ justify,
25
+ wrap,
26
+ padding,
27
+ className,
28
+ style,
29
+ children
30
+ }) {
31
+ return /* @__PURE__ */ jsx(
32
+ "div",
33
+ {
34
+ className: joinClass("wp-grid", !template && columnsClass(columns), className),
35
+ style: mergeStyle(
36
+ {
37
+ ...template ? { gridTemplateColumns: template } : null,
38
+ ...containerLayoutStyle({ gap, align, justify, wrap }),
39
+ ...paddingStyle(padding)
40
+ },
41
+ style
42
+ ),
43
+ children
44
+ }
45
+ );
46
+ }
47
+ function Row({ justify, gap, align, wrap, className, style, children }) {
48
+ return /* @__PURE__ */ jsx(
49
+ "div",
50
+ {
51
+ className: joinClass("wp-row", justify === "between" && "wp-row--between", className),
52
+ style: mergeStyle(containerLayoutStyle({ gap, align, justify, wrap }), style),
53
+ children
54
+ }
55
+ );
56
+ }
57
+ function Actions({ justify = "end", gap, align, wrap, className, style, children }) {
58
+ return /* @__PURE__ */ jsx(
59
+ "div",
60
+ {
61
+ className: joinClass("wp-row", className),
62
+ style: mergeStyle({ flex: 1, ...containerLayoutStyle({ gap, align, justify, wrap }) }, style),
63
+ children
64
+ }
65
+ );
66
+ }
67
+ function Col({ gap, align, justify, wrap, className, style, children }) {
68
+ return /* @__PURE__ */ jsx("div", { className: joinClass("wp-col", className), style: mergeStyle(containerLayoutStyle({ gap, align, justify, wrap }), style), children });
69
+ }
70
+ var SPACER_STEPS = /* @__PURE__ */ new Set([1, 2, 3, 4, 5, 6, 8]);
71
+ function Spacer({ size, className, style }) {
72
+ const step = size !== void 0 && SPACER_STEPS.has(Number(size)) ? Number(size) : void 0;
73
+ return /* @__PURE__ */ jsx(
74
+ "div",
75
+ {
76
+ className: joinClass("wp-spacer", step !== void 0 && `wp-spacer--${step}`, className),
77
+ style
78
+ }
79
+ );
80
+ }
81
+ function Section({ title, gap, align, justify, wrap, className, style, children }) {
82
+ return /* @__PURE__ */ jsxs(
83
+ "section",
84
+ {
85
+ className: joinClass("wp-col", className),
86
+ style: mergeStyle(containerLayoutStyle({ gap, align, justify, wrap }), style),
87
+ children: [
88
+ title ? /* @__PURE__ */ jsx("h2", { className: "wp-h4", children: title }) : null,
89
+ children
90
+ ]
91
+ }
92
+ );
93
+ }
94
+ var TEXT_WRAP_CSS = {
95
+ normal: {},
96
+ preserve: { whiteSpace: "pre-wrap" },
97
+ anywhere: { whiteSpace: "pre-wrap", wordBreak: "break-all" }
98
+ };
99
+ function Text({
100
+ preset = "body",
101
+ as: Tag = "p",
102
+ weight,
103
+ textAlign,
104
+ wrap,
105
+ className,
106
+ style,
107
+ value,
108
+ children
109
+ }) {
110
+ return /* @__PURE__ */ jsx(
111
+ Tag,
112
+ {
113
+ className: joinClass(`wp-${preset}`, className),
114
+ style: mergeStyle({ ...typographyStyle({ weight, textAlign }), ...TEXT_WRAP_CSS[wrap ?? "normal"] }, style),
115
+ children: value ?? children
116
+ }
117
+ );
118
+ }
119
+ function KeyValueList({ source, items, className, style }) {
120
+ return /* @__PURE__ */ jsx("div", { className: joinClass("wp-kv", className), style, children: (items ?? []).map((item, index) => {
121
+ const raw = item.key ? source?.[item.key] : item.value;
122
+ const content = raw === null || raw === void 0 || raw === "" ? "\u2014" : raw;
123
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
124
+ /* @__PURE__ */ jsx("span", { className: "wp-kv__k", children: item.label }),
125
+ /* @__PURE__ */ jsx("span", { className: joinClass("wp-kv__v", item.mono && "wp-mono"), children: content })
126
+ ] }, index);
127
+ }) });
128
+ }
129
+ function PageHead({ title, subtitle, actions, className, style }) {
130
+ return /* @__PURE__ */ jsxs("div", { className: joinClass("wp-page-head", className), style, children: [
131
+ /* @__PURE__ */ jsxs("div", { className: "wp-page-head__text", children: [
132
+ title ? /* @__PURE__ */ jsx("h1", { className: "wp-page-head__title", children: title }) : null,
133
+ subtitle ? /* @__PURE__ */ jsx("div", { className: "wp-page-head__sub", children: subtitle }) : null
134
+ ] }),
135
+ actions ? /* @__PURE__ */ jsx("div", { className: "wp-page-head__actions", children: actions }) : null
136
+ ] });
137
+ }
138
+ function Show({ when, children }) {
139
+ return when ? /* @__PURE__ */ jsx(Fragment, { children }) : null;
140
+ }
141
+ function Divider({ style, className }) {
142
+ return /* @__PURE__ */ jsx("hr", { className: joinClass("wp-divider", className), style });
143
+ }
144
+
145
+ export { Actions, Col, Divider, Grid, KeyValueList, PageHead, Row, Section, Show, Spacer, Text };
@@ -0,0 +1,140 @@
1
+ import { Text } from './chunk-QO6UJAQN.js';
2
+ import { Field, Input, Select, IconButton, Button } from './chunk-WIDDHR7L.js';
3
+ import { boxed } from './chunk-SAN4IX6N.js';
4
+ import { useMemo } from 'react';
5
+ import { jsxs, jsx } from 'react/jsx-runtime';
6
+
7
+ var OPTION_TYPES = [
8
+ { value: "text", label: "\uBB38\uC790\uC5F4" },
9
+ { value: "number", label: "\uC22B\uC790" },
10
+ { value: "select", label: "\uC120\uD0DD" },
11
+ { value: "boolean", label: "\uC608/\uC544\uB2C8\uC624" }
12
+ ];
13
+ function normalize(value) {
14
+ if (!Array.isArray(value)) return [];
15
+ return value.filter((item) => Boolean(item) && typeof item === "object").map((item) => ({
16
+ key: typeof item.key === "string" ? item.key : "",
17
+ label: typeof item.label === "string" ? item.label : void 0,
18
+ type: OPTION_TYPES.some((t) => t.value === item.type) ? item.type : "text",
19
+ options: Array.isArray(item.options) ? item.options.map((o) => String(o)) : void 0,
20
+ unit: typeof item.unit === "string" ? item.unit : void 0
21
+ }));
22
+ }
23
+ function OptionSchemaEditorImpl({ value, onChange, disabled = false }) {
24
+ const config = useMemo(
25
+ () => value && typeof value === "object" && !Array.isArray(value) ? value : {},
26
+ [value]
27
+ );
28
+ const fields = useMemo(() => normalize(config.options), [config.options]);
29
+ const duplicates = useMemo(() => {
30
+ const seen = /* @__PURE__ */ new Map();
31
+ for (const field of fields) {
32
+ const key = field.key.trim();
33
+ if (!key) continue;
34
+ seen.set(key, (seen.get(key) ?? 0) + 1);
35
+ }
36
+ return new Set([...seen.entries()].filter(([, count]) => count > 1).map(([key]) => key));
37
+ }, [fields]);
38
+ const emit = (next) => onChange?.({ ...config, options: next });
39
+ const patch = (index, change) => emit(fields.map((field, i) => i === index ? { ...field, ...change } : field));
40
+ return /* @__PURE__ */ jsxs("div", { className: "wp-col", style: { gap: "var(--wp-space-3)" }, children: [
41
+ fields.length === 0 ? /* @__PURE__ */ jsx(Text, { preset: "body-sm", style: { color: "var(--wp-text-muted)" }, children: "\uC544\uC9C1 \uC635\uC158\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uC5EC\uAE30\uC5D0 \uC815\uD574 \uB450\uBA74 \uC774 \uC7A5\uBE44\uC758 \uC720\uB2DB\uC744 \uC218\uC815\uD560 \uB54C \uADF8 \uC11C\uC2DD\uB300\uB85C \uCE78\uC774 \uB739\uB2C8\uB2E4." }) : null,
42
+ fields.map((field, index) => {
43
+ const duplicated = Boolean(field.key.trim()) && duplicates.has(field.key.trim());
44
+ return /* @__PURE__ */ jsxs(
45
+ "div",
46
+ {
47
+ className: "wp-row",
48
+ style: { gap: "var(--wp-space-2)", alignItems: "flex-start" },
49
+ children: [
50
+ /* @__PURE__ */ jsx(
51
+ Field,
52
+ {
53
+ label: index === 0 ? "\uC774\uB984(\uD0A4)" : void 0,
54
+ error: !field.key.trim() ? "\uC774\uB984\uC740 \uBE44\uC6B8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4" : duplicated ? "\uC774\uB984\uC774 \uACB9\uCE69\uB2C8\uB2E4" : void 0,
55
+ style: { flex: 1.2 },
56
+ children: /* @__PURE__ */ jsx(
57
+ Input,
58
+ {
59
+ value: field.key,
60
+ placeholder: "CPU",
61
+ disabled,
62
+ invalid: !field.key.trim() || duplicated,
63
+ onChange: (event) => patch(index, { key: event.target.value })
64
+ }
65
+ )
66
+ }
67
+ ),
68
+ /* @__PURE__ */ jsx(Field, { label: index === 0 ? "\uD45C\uC2DC \uC774\uB984" : void 0, style: { flex: 1.2 }, children: /* @__PURE__ */ jsx(
69
+ Input,
70
+ {
71
+ value: field.label ?? "",
72
+ placeholder: field.key || "\uBE44\uC6B0\uBA74 \uC774\uB984 \uADF8\uB300\uB85C",
73
+ disabled,
74
+ onChange: (event) => patch(index, { label: event.target.value || void 0 })
75
+ }
76
+ ) }),
77
+ /* @__PURE__ */ jsx(Field, { label: index === 0 ? "\uD615\uC2DD" : void 0, style: { flex: 1 }, children: /* @__PURE__ */ jsx(
78
+ Select,
79
+ {
80
+ value: field.type,
81
+ disabled,
82
+ options: OPTION_TYPES,
83
+ onChange: (event) => patch(index, {
84
+ type: event.target.value,
85
+ // 선택이 아니면 선택지는 의미가 없다 — 남겨 두면 다시 선택으로 바꿨을 때 옛 값이 살아난다
86
+ options: event.target.value === "select" ? field.options ?? [] : void 0
87
+ })
88
+ }
89
+ ) }),
90
+ field.type === "select" ? /* @__PURE__ */ jsx(Field, { label: index === 0 ? "\uC120\uD0DD\uC9C0(\uC27C\uD45C\uB85C \uAD6C\uBD84)" : void 0, style: { flex: 1.6 }, children: /* @__PURE__ */ jsx(
91
+ Input,
92
+ {
93
+ value: (field.options ?? []).join(", "),
94
+ placeholder: "8GB, 16GB, 32GB",
95
+ disabled,
96
+ onChange: (event) => patch(index, {
97
+ options: event.target.value.split(",").map((option) => option.trim()).filter(Boolean)
98
+ })
99
+ }
100
+ ) }) : null,
101
+ field.type === "number" ? /* @__PURE__ */ jsx(Field, { label: index === 0 ? "\uB2E8\uC704" : void 0, style: { flex: 0.7 }, children: /* @__PURE__ */ jsx(
102
+ Input,
103
+ {
104
+ value: field.unit ?? "",
105
+ placeholder: "GB",
106
+ disabled,
107
+ onChange: (event) => patch(index, { unit: event.target.value || void 0 })
108
+ }
109
+ ) }) : null,
110
+ /* @__PURE__ */ jsx("div", { style: { paddingTop: index === 0 ? 22 : 0, flex: "none" }, children: /* @__PURE__ */ jsx(
111
+ IconButton,
112
+ {
113
+ icon: "trash-2",
114
+ label: `${field.label || field.key || index + 1} \uC0AD\uC81C`,
115
+ variant: "ghost",
116
+ disabled,
117
+ onClick: () => emit(fields.filter((_, i) => i !== index))
118
+ }
119
+ ) })
120
+ ]
121
+ },
122
+ index
123
+ );
124
+ }),
125
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
126
+ Button,
127
+ {
128
+ size: "sm",
129
+ variant: "secondary",
130
+ icon: "plus",
131
+ disabled,
132
+ onClick: () => emit([...fields, { key: "", type: "text" }]),
133
+ children: "\uC635\uC158 \uCD94\uAC00"
134
+ }
135
+ ) })
136
+ ] });
137
+ }
138
+ var OptionSchemaEditor = boxed(OptionSchemaEditorImpl, "OptionSchemaEditor");
139
+
140
+ export { OPTION_TYPES, OptionSchemaEditor };