@wipco/sdui 1.4.2 → 1.6.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.
- package/dist/{DatePicker-C_TT_SOw.d.ts → DatePicker-DX9juElE.d.ts} +10 -1
- package/dist/{chunk-GZBY7I4Y.js → chunk-2KQL6DYG.js} +3 -3
- package/dist/{chunk-RYBBPFI7.js → chunk-2QXA52DG.js} +4 -4
- package/dist/{chunk-DMLDKW7K.js → chunk-37BNG4DT.js} +1 -1
- package/dist/{chunk-5WW4MBRO.js → chunk-3JTBOABA.js} +6 -2
- package/dist/{chunk-UV6HHFEW.js → chunk-4OABUPIU.js} +129 -68
- package/dist/{chunk-ZORQPB6B.js → chunk-53KS4E5N.js} +318 -359
- package/dist/{chunk-I5SD7XRA.js → chunk-6TI4WF5Y.js} +3 -3
- package/dist/{chunk-44TDRCKR.js → chunk-6XI2VF2O.js} +5 -5
- package/dist/{chunk-HBIIEYKW.js → chunk-7C54CKFZ.js} +4 -4
- package/dist/{chunk-IZ3JA2BR.js → chunk-7JP52QQO.js} +3 -3
- package/dist/{chunk-I45EUZC6.js → chunk-AY4GM6KQ.js} +4 -2
- package/dist/{chunk-SAPBDJJQ.js → chunk-AZSLSAJB.js} +2 -2
- package/dist/chunk-BITS7UJY.js +333 -0
- package/dist/{chunk-RNOUXUBO.js → chunk-BLFJMMBW.js} +306 -57
- package/dist/{chunk-YOZN35HD.js → chunk-CEVZAFPO.js} +3 -3
- package/dist/chunk-CI5OZ2QT.js +106 -0
- package/dist/{chunk-FNMS7OB7.js → chunk-CSE3PZOZ.js} +5 -5
- package/dist/chunk-CSZCWBOE.js +256 -0
- package/dist/{chunk-JIV4YHCZ.js → chunk-EJIWOD7W.js} +1 -1
- package/dist/{chunk-JZXU22PE.js → chunk-FEKKOBPX.js} +208 -117
- package/dist/chunk-GSM664TX.js +38 -0
- package/dist/{chunk-JYAR7D5U.js → chunk-HRHKQOOF.js} +2 -2
- package/dist/{chunk-CXW6KT6X.js → chunk-HXLQSO66.js} +3 -3
- package/dist/{chunk-O4IISNE7.js → chunk-IB2ZPUTF.js} +51 -12
- package/dist/{chunk-MPT7WJG2.js → chunk-IQXLNICY.js} +5 -5
- package/dist/{chunk-OQ52XMGG.js → chunk-K6RE23XX.js} +4 -4
- package/dist/{chunk-VY7MVBSE.js → chunk-KZ6U6FKB.js} +2 -2
- package/dist/{chunk-WPSK2CB5.js → chunk-LIZTK3HO.js} +16 -5
- package/dist/{chunk-Q4SWUINA.js → chunk-LKQJJSVJ.js} +1 -1
- package/dist/{chunk-E6ISNQK3.js → chunk-M6H3SHIS.js} +31 -8
- package/dist/chunk-MN4RZ2WP.js +380 -0
- package/dist/{chunk-GTGS2QRL.js → chunk-NA232CBX.js} +3 -3
- package/dist/{chunk-ZFNFPXPR.js → chunk-NAIWQQPW.js} +2 -2
- package/dist/{chunk-YVTV4ZV6.js → chunk-NWYYNE3Q.js} +1 -1
- package/dist/{chunk-DKVSM5HX.js → chunk-OUEJWC2Z.js} +6 -6
- package/dist/{chunk-DOL57OOP.js → chunk-OYDJHQNZ.js} +3 -3
- package/dist/{chunk-YWUDBLO6.js → chunk-QVS6K4FP.js} +2 -2
- package/dist/{chunk-N5RKM6IC.js → chunk-RU5U2YMT.js} +4 -4
- package/dist/{chunk-A6LAIGUS.js → chunk-V7BWMOHO.js} +7 -4
- package/dist/{chunk-2HLXCKK7.js → chunk-VPOOIEKT.js} +2 -2
- package/dist/{chunk-2YP5N4VV.js → chunk-VZOQRQAR.js} +1 -1
- package/dist/{chunk-GNA6SX2H.js → chunk-W4VA76QO.js} +2 -2
- package/dist/{chunk-BEM2YS7U.js → chunk-WEG3V7ZB.js} +3 -3
- package/dist/{chunk-XGFDY43I.js → chunk-WTIYILY3.js} +88 -18
- package/dist/{chunk-RWSH2MBU.js → chunk-X2NIRVLN.js} +73 -19
- package/dist/{chunk-AXGF4M57.js → chunk-XAXROQKB.js} +1 -1
- package/dist/{chunk-NIJUVTEW.js → chunk-XG22XKKD.js} +3 -3
- package/dist/{chunk-A5XQOEVH.js → chunk-YN7X7BEO.js} +4 -4
- package/dist/{chunk-KO6OEY3G.js → chunk-YNEEJCSZ.js} +3 -13
- package/dist/{chunk-7P7N43JV.js → chunk-YZTTFEL4.js} +2 -2
- package/dist/index.d.ts +494 -89
- package/dist/index.js +1098 -124
- package/dist/{layout-C3nRXMwb.d.ts → layout-DmRikwZb.d.ts} +5 -4
- package/dist/meta.d.ts +1 -1
- package/dist/meta.js +286 -22
- package/dist/rich/ExcelUploadCard.d.ts +1 -1
- package/dist/rich/ExcelUploadCard.js +5 -4
- package/dist/rich.d.ts +21 -3
- package/dist/rich.js +277 -35
- package/dist/styles.css +102 -4
- package/dist/{types-COjGKgmd.d.ts → types-CYfIthux.d.ts} +57 -1
- package/dist/widgets/ApprovalLineEditor.d.ts +1 -1
- package/dist/widgets/ApprovalLineEditor.js +9 -8
- package/dist/widgets/ApprovalOverridesEditor.d.ts +1 -1
- package/dist/widgets/ApprovalOverridesEditor.js +11 -10
- package/dist/widgets/ApprovalStepper.d.ts +29 -4
- package/dist/widgets/ApprovalStepper.js +3 -3
- package/dist/widgets/AutocompleteField.d.ts +1 -1
- package/dist/widgets/AutocompleteField.js +9 -8
- package/dist/widgets/ColumnBoard.d.ts +1 -1
- package/dist/widgets/ColumnBoard.js +3 -3
- package/dist/widgets/DiffView.d.ts +1 -1
- package/dist/widgets/DiffView.js +3 -3
- package/dist/widgets/Disclosure.d.ts +1 -1
- package/dist/widgets/Disclosure.js +3 -3
- package/dist/widgets/EventCalendar.d.ts +5 -1
- package/dist/widgets/EventCalendar.js +5 -3
- package/dist/widgets/FileDropzone.d.ts +1 -1
- package/dist/widgets/FileDropzone.js +9 -7
- package/dist/widgets/ImageLightbox.d.ts +1 -1
- package/dist/widgets/ImageLightbox.js +7 -5
- package/dist/widgets/InlineEdit.d.ts +1 -1
- package/dist/widgets/InlineEdit.js +3 -3
- package/dist/widgets/JsonField.d.ts +1 -1
- package/dist/widgets/JsonField.js +3 -3
- package/dist/widgets/LiveClock.d.ts +1 -1
- package/dist/widgets/LiveClock.js +2 -2
- package/dist/widgets/OptionSchemaEditor.d.ts +1 -1
- package/dist/widgets/OptionSchemaEditor.js +4 -4
- package/dist/widgets/OptionValueFields.d.ts +1 -1
- package/dist/widgets/OptionValueFields.js +4 -4
- package/dist/widgets/OrgChart.d.ts +1 -1
- package/dist/widgets/OrgChart.js +4 -4
- package/dist/widgets/OrgChartBoard.d.ts +15 -33
- package/dist/widgets/OrgChartBoard.js +10 -8
- package/dist/widgets/PeriodFilter.d.ts +1 -1
- package/dist/widgets/PeriodFilter.js +6 -5
- package/dist/widgets/PermissionMatrixEditor.d.ts +34 -2
- package/dist/widgets/PermissionMatrixEditor.js +6 -4
- package/dist/widgets/PositionSelect.d.ts +1 -1
- package/dist/widgets/PositionSelect.js +5 -4
- package/dist/widgets/PrintSheet.d.ts +15 -2
- package/dist/widgets/PrintSheet.js +3 -3
- package/dist/widgets/ProfileEditDiffCard.d.ts +1 -1
- package/dist/widgets/ProfileEditDiffCard.js +5 -4
- package/dist/widgets/QRDisplay.d.ts +1 -1
- package/dist/widgets/QRDisplay.js +5 -4
- package/dist/widgets/RankBoard.d.ts +1 -1
- package/dist/widgets/RankBoard.js +8 -6
- package/dist/widgets/RecordSheet.d.ts +1 -1
- package/dist/widgets/RecordSheet.js +2 -2
- package/dist/widgets/RoleMemberPicker.d.ts +1 -1
- package/dist/widgets/RoleMemberPicker.js +6 -5
- package/dist/widgets/SearchableMultiPicker.d.ts +1 -1
- package/dist/widgets/SearchableMultiPicker.js +7 -6
- package/dist/widgets/SecureScreen.d.ts +1 -1
- package/dist/widgets/SecureScreen.js +3 -3
- package/dist/widgets/StackedMeter.d.ts +1 -1
- package/dist/widgets/StackedMeter.js +2 -2
- package/dist/widgets/StagePeriodEditor.d.ts +1 -1
- package/dist/widgets/StagePeriodEditor.js +6 -5
- package/dist/widgets/Strip.d.ts +1 -1
- package/dist/widgets/Strip.js +2 -2
- package/dist/widgets/SummaryTable.d.ts +1 -1
- package/dist/widgets/SummaryTable.js +2 -2
- package/dist/widgets/TagInput.d.ts +1 -1
- package/dist/widgets/TagInput.js +3 -3
- package/dist/widgets/TagMultiSelect.d.ts +1 -1
- package/dist/widgets/TagMultiSelect.js +5 -4
- package/dist/widgets/Timeline.d.ts +1 -1
- package/dist/widgets/Timeline.js +4 -4
- package/dist/widgets/TreePicker.d.ts +1 -1
- package/dist/widgets/TreePicker.js +8 -7
- package/dist/widgets/TreeView.d.ts +1 -1
- package/dist/widgets/TreeView.js +7 -6
- package/dist/widgets/index.d.ts +3 -3
- package/dist/widgets/index.js +84 -83
- package/package.json +1 -1
- package/dist/chunk-LFEJLOFN.js +0 -352
- package/dist/chunk-MMIGOPO5.js +0 -111
- package/dist/chunk-TFGNKFL2.js +0 -39
- package/dist/chunk-XQPTSEJC.js +0 -135
package/dist/rich.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { useOptionalModule, DependencyPlaceholder } from './chunk-
|
|
2
|
-
export { DatePicker, DateRangePicker, TimePicker } from './chunk-
|
|
1
|
+
import { useOptionalModule, DependencyPlaceholder } from './chunk-NWYYNE3Q.js';
|
|
2
|
+
export { DatePicker, DateRangePicker, TimePicker } from './chunk-X2NIRVLN.js';
|
|
3
|
+
import './chunk-BITS7UJY.js';
|
|
3
4
|
import { HoistedStyle } from './chunk-RIZRM7OO.js';
|
|
4
|
-
import './chunk-
|
|
5
|
-
import
|
|
6
|
-
import { Spinner, EmptyState } from './chunk-
|
|
7
|
-
import './chunk-
|
|
5
|
+
import { useSduiAssetResolver } from './chunk-YNEEJCSZ.js';
|
|
6
|
+
import './chunk-GSM664TX.js';
|
|
7
|
+
import { Spinner, EmptyState } from './chunk-BLFJMMBW.js';
|
|
8
|
+
import './chunk-3JTBOABA.js';
|
|
8
9
|
import { useRef, useCallback, useMemo, useState, useEffect, useLayoutEffect } from 'react';
|
|
9
10
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
10
11
|
|
|
@@ -47,24 +48,140 @@ function useSeriesPalette(ref) {
|
|
|
47
48
|
return palette;
|
|
48
49
|
}
|
|
49
50
|
var formatNumber = (v) => v.toLocaleString("ko-KR", { maximumFractionDigits: 1 });
|
|
51
|
+
var eokText = (abs) => {
|
|
52
|
+
const eok = abs / 1e8;
|
|
53
|
+
return `${eok.toLocaleString("ko-KR", { maximumFractionDigits: eok >= 100 ? 0 : 1 })}\uC5B5`;
|
|
54
|
+
};
|
|
55
|
+
var manText = (abs) => {
|
|
56
|
+
const man = abs / 1e4;
|
|
57
|
+
return `${man.toLocaleString("ko-KR", { maximumFractionDigits: man >= 100 ? 0 : 1 })}\uB9CC`;
|
|
58
|
+
};
|
|
50
59
|
var formatCurrency = (v) => {
|
|
51
60
|
const sign = v < 0 ? "-" : "";
|
|
52
61
|
const abs = Math.abs(v);
|
|
53
|
-
if (abs >= 1e8) {
|
|
54
|
-
|
|
55
|
-
return `${sign}${eok.toLocaleString("ko-KR", { maximumFractionDigits: eok >= 100 ? 0 : 1 })}\uC5B5`;
|
|
56
|
-
}
|
|
57
|
-
if (abs >= 1e4) {
|
|
58
|
-
return `${sign}${Math.round(abs / 1e4).toLocaleString("ko-KR")}\uB9CC`;
|
|
59
|
-
}
|
|
62
|
+
if (abs >= 1e8) return `${sign}${eokText(abs)}`;
|
|
63
|
+
if (abs >= 1e4) return `${sign}${manText(abs)}`;
|
|
60
64
|
return `${sign}${abs.toLocaleString("ko-KR")}`;
|
|
61
65
|
};
|
|
66
|
+
var currencyAxisFormatter = (maxAbs) => {
|
|
67
|
+
if (!(maxAbs >= 1e5)) return (v) => v.toLocaleString("ko-KR", { maximumFractionDigits: 1 });
|
|
68
|
+
return (v) => {
|
|
69
|
+
const abs = Math.abs(v);
|
|
70
|
+
if (abs === 0) return "0";
|
|
71
|
+
const sign = v < 0 ? "-" : "";
|
|
72
|
+
return `${sign}${abs >= 1e8 ? eokText(abs) : manText(abs)}`;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
62
75
|
var formatPercent = (v) => `${v.toLocaleString("ko-KR", { maximumFractionDigits: 1 })}%`;
|
|
63
76
|
var getFormatter = (format) => {
|
|
64
77
|
if (format === "currency") return formatCurrency;
|
|
65
78
|
if (format === "percent") return formatPercent;
|
|
66
79
|
return formatNumber;
|
|
67
80
|
};
|
|
81
|
+
var axisFormatter = (format, extent) => format === "currency" ? currencyAxisFormatter(extent.maxAbs) : getFormatter(format);
|
|
82
|
+
function axisExtent(rows, series) {
|
|
83
|
+
let maxAbs = 0;
|
|
84
|
+
let allInteger = true;
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
if (row === null || typeof row !== "object") continue;
|
|
87
|
+
const positive = /* @__PURE__ */ new Map();
|
|
88
|
+
const negative = /* @__PURE__ */ new Map();
|
|
89
|
+
for (const s of series) {
|
|
90
|
+
const raw = row[s.key];
|
|
91
|
+
if (raw === null || raw === void 0 || raw === "" || typeof raw === "boolean") continue;
|
|
92
|
+
const n = Number(raw);
|
|
93
|
+
if (!Number.isFinite(n)) continue;
|
|
94
|
+
if (!Number.isInteger(n)) allInteger = false;
|
|
95
|
+
if (s.stack) {
|
|
96
|
+
const bucket = n >= 0 ? positive : negative;
|
|
97
|
+
bucket.set(s.stack, (bucket.get(s.stack) ?? 0) + n);
|
|
98
|
+
} else {
|
|
99
|
+
maxAbs = Math.max(maxAbs, Math.abs(n));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const total of [...positive.values(), ...negative.values()]) {
|
|
103
|
+
maxAbs = Math.max(maxAbs, Math.abs(total));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { maxAbs, allInteger };
|
|
107
|
+
}
|
|
108
|
+
function estimateTextWidth(text, fontSize = 12) {
|
|
109
|
+
let w = 0;
|
|
110
|
+
for (const ch of text) {
|
|
111
|
+
if (/\s/.test(ch)) w += fontSize * 0.3;
|
|
112
|
+
else if (/[\u1100-\u11ff\u3000-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]/.test(ch)) w += fontSize;
|
|
113
|
+
else w += fontSize * 0.6;
|
|
114
|
+
}
|
|
115
|
+
return w;
|
|
116
|
+
}
|
|
117
|
+
function wrapLabel(label, maxWidth, measure) {
|
|
118
|
+
const text = label.trim();
|
|
119
|
+
if (!text) return [""];
|
|
120
|
+
if (!(maxWidth > 0) || measure(text) <= maxWidth) return [text];
|
|
121
|
+
const lines = [];
|
|
122
|
+
let line = "";
|
|
123
|
+
const push = () => {
|
|
124
|
+
if (line.trim()) lines.push(line.trim());
|
|
125
|
+
line = "";
|
|
126
|
+
};
|
|
127
|
+
for (const word of text.split(/(\s+)/)) {
|
|
128
|
+
if (!word) continue;
|
|
129
|
+
const candidate = line + word;
|
|
130
|
+
if (measure(candidate.trim()) <= maxWidth) {
|
|
131
|
+
line = candidate;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (/^\s+$/.test(word)) {
|
|
135
|
+
push();
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
push();
|
|
139
|
+
if (measure(word) <= maxWidth) {
|
|
140
|
+
line = word;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const chars = [...word];
|
|
144
|
+
const total = measure(word);
|
|
145
|
+
const parts = Math.ceil(total / maxWidth);
|
|
146
|
+
let acc = 0;
|
|
147
|
+
let boundary = total / parts;
|
|
148
|
+
for (const ch of chars) {
|
|
149
|
+
const w = measure(ch);
|
|
150
|
+
if (line && acc + w / 2 > boundary + 0.01) {
|
|
151
|
+
push();
|
|
152
|
+
boundary += total / parts;
|
|
153
|
+
}
|
|
154
|
+
line += ch;
|
|
155
|
+
acc += w;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
push();
|
|
159
|
+
return lines.length > 0 ? lines : [text];
|
|
160
|
+
}
|
|
161
|
+
var TICK_FONT = 12;
|
|
162
|
+
var TICK_LINE_H = 14;
|
|
163
|
+
var MAX_TICK_LINES = 3;
|
|
164
|
+
function planXTicks(labels, bandWidth, measure) {
|
|
165
|
+
if (!(bandWidth > 0) || labels.length === 0) return { mode: "skip" };
|
|
166
|
+
const room = Math.max(0, bandWidth - 6);
|
|
167
|
+
const lines = /* @__PURE__ */ new Map();
|
|
168
|
+
let maxLines = 1;
|
|
169
|
+
let fits = room >= TICK_FONT;
|
|
170
|
+
for (const label of labels) {
|
|
171
|
+
const wrapped = wrapLabel(label, room, measure);
|
|
172
|
+
lines.set(label.trim(), wrapped);
|
|
173
|
+
maxLines = Math.max(maxLines, wrapped.length);
|
|
174
|
+
if (wrapped.some((line) => measure(line) > room + TICK_FONT / 2)) fits = false;
|
|
175
|
+
}
|
|
176
|
+
if (fits && maxLines <= MAX_TICK_LINES) {
|
|
177
|
+
return { mode: "wrap", lines, height: Math.ceil(8 + maxLines * TICK_LINE_H + 4) };
|
|
178
|
+
}
|
|
179
|
+
if (bandWidth * Math.SQRT1_2 >= TICK_FONT * 1.25) {
|
|
180
|
+
const longest = Math.max(...labels.map((label) => measure(label)));
|
|
181
|
+
return { mode: "rotate", height: Math.ceil(8 + (longest + TICK_FONT) * Math.SQRT1_2 + 4) };
|
|
182
|
+
}
|
|
183
|
+
return { mode: "skip" };
|
|
184
|
+
}
|
|
68
185
|
var tooltipBoxStyle = {
|
|
69
186
|
background: "var(--wp-surface-card)",
|
|
70
187
|
border: "1px solid var(--wp-border-subtle)",
|
|
@@ -80,7 +197,7 @@ var dotStyle = (color) => ({
|
|
|
80
197
|
background: color,
|
|
81
198
|
flexShrink: 0
|
|
82
199
|
});
|
|
83
|
-
function renderTooltipContent(props,
|
|
200
|
+
function renderTooltipContent(props, formatOf) {
|
|
84
201
|
const { active, payload, label } = props;
|
|
85
202
|
if (!active || !payload || payload.length === 0) return null;
|
|
86
203
|
return /* @__PURE__ */ jsxs("div", { style: tooltipBoxStyle, children: [
|
|
@@ -88,7 +205,7 @@ function renderTooltipContent(props, format) {
|
|
|
88
205
|
/* @__PURE__ */ jsx("div", { style: { display: "grid", gap: 2 }, children: payload.filter((entry) => entry.type !== "none").map((entry, i) => {
|
|
89
206
|
const fillFromDatum = entry.payload !== null && typeof entry.payload === "object" ? entry.payload.fill : void 0;
|
|
90
207
|
const color = entry.color ?? fillFromDatum ?? "var(--wp-text-muted)";
|
|
91
|
-
const value = typeof entry.value === "number" ?
|
|
208
|
+
const value = typeof entry.value === "number" ? formatOf(entry)(entry.value) : String(entry.value ?? "");
|
|
92
209
|
return /* @__PURE__ */ jsxs(
|
|
93
210
|
"div",
|
|
94
211
|
{
|
|
@@ -169,6 +286,9 @@ function SliceLegendTable({
|
|
|
169
286
|
style: {
|
|
170
287
|
flex: "none",
|
|
171
288
|
minWidth: 148,
|
|
289
|
+
// 차트 옆 한 줄에서는 차트가 남는 폭을 다 먹어 여백이 0이다. 좁아서 아래 줄로
|
|
290
|
+
// 내려가면(wp-chart__split 이 wrap) 혼자 남은 줄에서 가운데에 선다.
|
|
291
|
+
marginInline: "auto",
|
|
172
292
|
display: "flex",
|
|
173
293
|
flexDirection: "column",
|
|
174
294
|
justifyContent: "center",
|
|
@@ -208,7 +328,60 @@ function SliceLegendTable({
|
|
|
208
328
|
}
|
|
209
329
|
);
|
|
210
330
|
}
|
|
331
|
+
var measureCanvas;
|
|
332
|
+
function makeMeasure(fontFamily) {
|
|
333
|
+
if (measureCanvas === void 0) {
|
|
334
|
+
try {
|
|
335
|
+
measureCanvas = typeof document !== "undefined" ? document.createElement("canvas").getContext("2d") : null;
|
|
336
|
+
} catch {
|
|
337
|
+
measureCanvas = null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const ctx = measureCanvas;
|
|
341
|
+
if (!ctx || !fontFamily) return (text) => estimateTextWidth(text, TICK_FONT);
|
|
342
|
+
const font = `${TICK_FONT}px ${fontFamily}`;
|
|
343
|
+
return (text) => {
|
|
344
|
+
ctx.font = font;
|
|
345
|
+
return ctx.measureText(text).width;
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function useHostBox(ref, enabled) {
|
|
349
|
+
const [box, setBox] = useState({
|
|
350
|
+
width: 0,
|
|
351
|
+
fontFamily: null
|
|
352
|
+
});
|
|
353
|
+
useLayoutEffect(() => {
|
|
354
|
+
const el = ref.current;
|
|
355
|
+
if (!enabled || el === null) return;
|
|
356
|
+
const read = () => {
|
|
357
|
+
const width = Math.round(el.getBoundingClientRect().width);
|
|
358
|
+
const fontFamily = getComputedStyle(el).fontFamily || null;
|
|
359
|
+
setBox((prev) => prev.width === width && prev.fontFamily === fontFamily ? prev : { width, fontFamily });
|
|
360
|
+
};
|
|
361
|
+
read();
|
|
362
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
363
|
+
const observer = new ResizeObserver(read);
|
|
364
|
+
observer.observe(el);
|
|
365
|
+
return () => observer.disconnect();
|
|
366
|
+
});
|
|
367
|
+
return box;
|
|
368
|
+
}
|
|
211
369
|
var axisTick = { fill: "var(--wp-text-muted)", fontSize: 12 };
|
|
370
|
+
var SIDE_LEGEND_CHART_MIN = 96;
|
|
371
|
+
function renderCategoryTick(plan) {
|
|
372
|
+
const CategoryTick = (props) => {
|
|
373
|
+
const x = Number(props.x) || 0;
|
|
374
|
+
const y = Number(props.y) || 0;
|
|
375
|
+
const label = String(props.payload?.value ?? "");
|
|
376
|
+
const textStyle = { fill: axisTick.fill, fontSize: axisTick.fontSize };
|
|
377
|
+
if (plan.mode === "rotate") {
|
|
378
|
+
return /* @__PURE__ */ jsx("g", { transform: `translate(${x},${y})`, children: /* @__PURE__ */ jsx("text", { x: 0, y: 0, dy: "0.71em", textAnchor: "end", transform: "rotate(-45)", style: textStyle, children: label }) });
|
|
379
|
+
}
|
|
380
|
+
const lines = plan.lines.get(label.trim()) ?? [label];
|
|
381
|
+
return /* @__PURE__ */ jsx("g", { transform: `translate(${x},${y})`, children: /* @__PURE__ */ jsx("text", { x: 0, y: 0, textAnchor: "middle", style: textStyle, children: lines.map((line, i) => /* @__PURE__ */ jsx("tspan", { x: 0, dy: i === 0 ? "0.71em" : TICK_LINE_H, children: line }, i)) }) });
|
|
382
|
+
};
|
|
383
|
+
return CategoryTick;
|
|
384
|
+
}
|
|
212
385
|
function Chart({
|
|
213
386
|
type,
|
|
214
387
|
data,
|
|
@@ -220,11 +393,16 @@ function Chart({
|
|
|
220
393
|
legendPlacement = "bottom",
|
|
221
394
|
legendTotal,
|
|
222
395
|
donutCenter,
|
|
223
|
-
emptyText
|
|
396
|
+
emptyText,
|
|
397
|
+
rightValueFormat = "number",
|
|
398
|
+
integer,
|
|
399
|
+
xTickWrap = false
|
|
224
400
|
}) {
|
|
225
401
|
const hostRef = useRef(null);
|
|
226
402
|
const palette = useSeriesPalette(hostRef);
|
|
227
403
|
const recharts = useOptionalModule(loadRecharts);
|
|
404
|
+
const cartesianType = type === "bar" || type === "line" || type === "composed";
|
|
405
|
+
const hostBox = useHostBox(hostRef, xTickWrap === true && cartesianType);
|
|
228
406
|
const resolveColor = useCallback(
|
|
229
407
|
(color, index) => {
|
|
230
408
|
if (!color) return palette[index % palette.length];
|
|
@@ -270,7 +448,7 @@ function Chart({
|
|
|
270
448
|
XAxis,
|
|
271
449
|
YAxis
|
|
272
450
|
} = recharts.module;
|
|
273
|
-
const tooltipContent = (props) => renderTooltipContent(props, format);
|
|
451
|
+
const tooltipContent = (props) => renderTooltipContent(props, () => format);
|
|
274
452
|
if (type === "donut" || type === "pie") {
|
|
275
453
|
const nameKey = xKey ?? "name";
|
|
276
454
|
const valueKey = seriesList[0].key;
|
|
@@ -283,7 +461,7 @@ function Chart({
|
|
|
283
461
|
const chartArea = /* @__PURE__ */ jsxs(
|
|
284
462
|
"div",
|
|
285
463
|
{
|
|
286
|
-
style: sideLegend ? { position: "relative", flex:
|
|
464
|
+
style: sideLegend ? { position: "relative", flex: `1 1 ${SIDE_LEGEND_CHART_MIN}px`, minWidth: 0, height } : { position: "relative", width: "100%", height },
|
|
287
465
|
children: [
|
|
288
466
|
/* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(PieChart, { children: [
|
|
289
467
|
/* @__PURE__ */ jsx(Tooltip, { content: tooltipContent }),
|
|
@@ -333,17 +511,24 @@ function Chart({
|
|
|
333
511
|
};
|
|
334
512
|
const denomOverride = Number(legendTotal);
|
|
335
513
|
return shell(
|
|
336
|
-
/* @__PURE__ */ jsxs(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
{
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
514
|
+
/* @__PURE__ */ jsxs(
|
|
515
|
+
"div",
|
|
516
|
+
{
|
|
517
|
+
className: "wp-chart__split",
|
|
518
|
+
style: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: "var(--wp-space-6)" },
|
|
519
|
+
children: [
|
|
520
|
+
chartArea,
|
|
521
|
+
/* @__PURE__ */ jsx(
|
|
522
|
+
SliceLegendTable,
|
|
523
|
+
{
|
|
524
|
+
rows: rows.map((row, i) => ({ ...legendItems2[i], value: sliceValue(row) })),
|
|
525
|
+
format,
|
|
526
|
+
total: legendTotal !== void 0 && Number.isFinite(denomOverride) ? denomOverride : void 0
|
|
527
|
+
}
|
|
528
|
+
)
|
|
529
|
+
]
|
|
530
|
+
}
|
|
531
|
+
)
|
|
347
532
|
);
|
|
348
533
|
}
|
|
349
534
|
return shell(
|
|
@@ -369,6 +554,25 @@ function Chart({
|
|
|
369
554
|
color: resolveColor(s.color, i),
|
|
370
555
|
dashed: kindOf(s) === "dashed-line"
|
|
371
556
|
}));
|
|
557
|
+
const onRight = (s) => s.axis === "right";
|
|
558
|
+
const dual = seriesList.some(onRight) && seriesList.some((s) => !onRight(s));
|
|
559
|
+
const axisIdOf = (s) => dual ? onRight(s) ? "right" : "left" : void 0;
|
|
560
|
+
const extentOf = (list) => axisExtent(
|
|
561
|
+
rows,
|
|
562
|
+
list.map((s) => ({ key: s.key, stack: kindOf(s) === "bar" ? s.stack : void 0 }))
|
|
563
|
+
);
|
|
564
|
+
const leftExtent = extentOf(dual ? seriesList.filter((s) => !onRight(s)) : seriesList);
|
|
565
|
+
const rightExtent = dual ? extentOf(seriesList.filter(onRight)) : null;
|
|
566
|
+
const allowDecimals = (extent) => typeof integer === "boolean" ? !integer : !extent.allInteger;
|
|
567
|
+
const tickCountFor = (extent) => !allowDecimals(extent) && extent.maxAbs < 4 ? Math.max(2, Math.ceil(extent.maxAbs) + 1) : void 0;
|
|
568
|
+
const leftTickFormat = axisFormatter(valueFormat, leftExtent);
|
|
569
|
+
const rightFormat = getFormatter(rightValueFormat);
|
|
570
|
+
const rightTickFormat = rightExtent ? axisFormatter(rightValueFormat, rightExtent) : rightFormat;
|
|
571
|
+
const rightKeys = new Set(dual ? seriesList.filter(onRight).map((s) => s.key) : []);
|
|
572
|
+
const cartesianTooltip = (props) => renderTooltipContent(
|
|
573
|
+
props,
|
|
574
|
+
(entry) => typeof entry.dataKey === "string" && rightKeys.has(entry.dataKey) ? rightFormat : format
|
|
575
|
+
);
|
|
372
576
|
const marks = seriesList.map((s, i) => {
|
|
373
577
|
const color = resolveColor(s.color, i);
|
|
374
578
|
const name = s.label ?? s.key;
|
|
@@ -380,6 +584,7 @@ function Chart({
|
|
|
380
584
|
name,
|
|
381
585
|
fill: color,
|
|
382
586
|
stackId: s.stack,
|
|
587
|
+
yAxisId: axisIdOf(s),
|
|
383
588
|
maxBarSize: 48,
|
|
384
589
|
radius: barRadius(s, i),
|
|
385
590
|
isAnimationActive: false
|
|
@@ -396,6 +601,7 @@ function Chart({
|
|
|
396
601
|
stroke: color,
|
|
397
602
|
strokeWidth: 2,
|
|
398
603
|
strokeDasharray: kindOf(s) === "dashed-line" ? "6 4" : void 0,
|
|
604
|
+
yAxisId: axisIdOf(s),
|
|
399
605
|
dot: false,
|
|
400
606
|
activeDot: { r: 4, strokeWidth: 2, stroke: "var(--wp-surface-card)" },
|
|
401
607
|
isAnimationActive: false
|
|
@@ -403,8 +609,27 @@ function Chart({
|
|
|
403
609
|
s.key
|
|
404
610
|
);
|
|
405
611
|
});
|
|
612
|
+
const margin = { top: 8, right: 8, bottom: 0, left: 0 };
|
|
613
|
+
const Y_AXIS_WIDTH = 52;
|
|
614
|
+
let xTick = {};
|
|
615
|
+
if (xTickWrap && hostBox.width > 0) {
|
|
616
|
+
const labels = rows.map((row) => String((xKey ? row[xKey] : void 0) ?? ""));
|
|
617
|
+
const plotWidth = hostBox.width - margin.left - margin.right - Y_AXIS_WIDTH * (dual ? 2 : 1);
|
|
618
|
+
const plan = planXTicks(labels, plotWidth / Math.max(1, labels.length), makeMeasure(hostBox.fontFamily));
|
|
619
|
+
if (plan.mode !== "skip") {
|
|
620
|
+
xTick = { interval: 0, height: plan.height, tick: renderCategoryTick(plan) };
|
|
621
|
+
}
|
|
622
|
+
}
|
|
406
623
|
const axes = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
407
|
-
/* @__PURE__ */ jsx(
|
|
624
|
+
/* @__PURE__ */ jsx(
|
|
625
|
+
CartesianGrid,
|
|
626
|
+
{
|
|
627
|
+
strokeDasharray: "3 3",
|
|
628
|
+
stroke: "var(--wp-divider)",
|
|
629
|
+
vertical: false,
|
|
630
|
+
yAxisId: dual ? "left" : void 0
|
|
631
|
+
}
|
|
632
|
+
),
|
|
408
633
|
/* @__PURE__ */ jsx(
|
|
409
634
|
XAxis,
|
|
410
635
|
{
|
|
@@ -412,28 +637,45 @@ function Chart({
|
|
|
412
637
|
tickLine: false,
|
|
413
638
|
axisLine: { stroke: "var(--wp-border-subtle)" },
|
|
414
639
|
tick: axisTick,
|
|
415
|
-
tickMargin: 6
|
|
640
|
+
tickMargin: 6,
|
|
641
|
+
...xTick
|
|
416
642
|
}
|
|
417
643
|
),
|
|
418
644
|
/* @__PURE__ */ jsx(
|
|
419
645
|
YAxis,
|
|
420
646
|
{
|
|
647
|
+
yAxisId: dual ? "left" : void 0,
|
|
421
648
|
tickLine: false,
|
|
422
649
|
axisLine: false,
|
|
423
650
|
tick: axisTick,
|
|
424
|
-
tickFormatter: (v) =>
|
|
425
|
-
|
|
651
|
+
tickFormatter: (v) => leftTickFormat(v),
|
|
652
|
+
allowDecimals: allowDecimals(leftExtent),
|
|
653
|
+
tickCount: tickCountFor(leftExtent),
|
|
654
|
+
width: Y_AXIS_WIDTH
|
|
426
655
|
}
|
|
427
656
|
),
|
|
657
|
+
dual && rightExtent ? /* @__PURE__ */ jsx(
|
|
658
|
+
YAxis,
|
|
659
|
+
{
|
|
660
|
+
yAxisId: "right",
|
|
661
|
+
orientation: "right",
|
|
662
|
+
tickLine: false,
|
|
663
|
+
axisLine: false,
|
|
664
|
+
tick: axisTick,
|
|
665
|
+
tickFormatter: (v) => rightTickFormat(v),
|
|
666
|
+
allowDecimals: allowDecimals(rightExtent),
|
|
667
|
+
tickCount: tickCountFor(rightExtent),
|
|
668
|
+
width: Y_AXIS_WIDTH
|
|
669
|
+
}
|
|
670
|
+
) : null,
|
|
428
671
|
/* @__PURE__ */ jsx(
|
|
429
672
|
Tooltip,
|
|
430
673
|
{
|
|
431
674
|
cursor: hasBar ? { fill: "var(--wp-surface-sunken)", fillOpacity: 0.5 } : { stroke: "var(--wp-border-subtle)", strokeDasharray: "3 3" },
|
|
432
|
-
content:
|
|
675
|
+
content: cartesianTooltip
|
|
433
676
|
}
|
|
434
677
|
)
|
|
435
678
|
] });
|
|
436
|
-
const margin = { top: 8, right: 8, bottom: 0, left: 0 };
|
|
437
679
|
const cartesian = type === "bar" ? /* @__PURE__ */ jsxs(BarChart, { data: rows, margin, children: [
|
|
438
680
|
axes,
|
|
439
681
|
marks
|
package/dist/styles.css
CHANGED
|
@@ -326,11 +326,20 @@
|
|
|
326
326
|
.wp-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
327
327
|
.wp-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
328
328
|
.wp-grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
329
|
-
|
|
330
|
-
|
|
329
|
+
/* 반응형 경계는 LAYOUT_CSS(layout.ts)·env.ts 와 같은 계약 — mobile ≤767.98 · tablet ≤1023.98.
|
|
330
|
+
`screen and` 는 인쇄 용지 폭이 max-width 에 걸리지 않게 한다. 예전 900/600 은 이 계약 이전 값. */
|
|
331
|
+
/* `.wp-grid--nocollapse`(Grid collapse:false) 는 어느 폭에서도 열을 지킨다 — 접힘 규칙 전부가 비켜선다(LAYOUT_CSS·ERP §1 과 같은 계약). */
|
|
332
|
+
@media screen and (max-width: 1023.98px) {
|
|
333
|
+
.wp-grid--3:where(:not(.wp-grid--nocollapse)), .wp-grid--4:where(:not(.wp-grid--nocollapse)) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
331
334
|
}
|
|
332
|
-
@media (max-width:
|
|
333
|
-
.wp-grid--2, .wp-grid--3, .wp-grid--4 { grid-template-columns: minmax(0, 1fr); }
|
|
335
|
+
@media screen and (max-width: 767.98px) {
|
|
336
|
+
.wp-grid--2:where(:not(.wp-grid--nocollapse)), .wp-grid--3:where(:not(.wp-grid--nocollapse)), .wp-grid--4:where(:not(.wp-grid--nocollapse)) { grid-template-columns: minmax(0, 1fr); }
|
|
337
|
+
}
|
|
338
|
+
/* Grid template(.wp-grid--custom)·nocollapse·5/6열·KV 블록·카드 포커스 링은 렌더러가 LAYOUT_CSS 로 들고
|
|
339
|
+
다닌다(SduiRenderer 가 hoist). 여기 사본은 렌더러 없이 컴포넌트를 직접 쓰는 소비자용이다. */
|
|
340
|
+
.wp-grid--custom { grid-template-columns: var(--wp-grid-template); }
|
|
341
|
+
@media screen and (max-width: 767.98px) {
|
|
342
|
+
.wp-grid--custom:where(:not(.wp-grid--nocollapse)) { grid-template-columns: minmax(0, 1fr); }
|
|
334
343
|
}
|
|
335
344
|
.wp-spacer { flex: 1; }
|
|
336
345
|
/*
|
|
@@ -361,6 +370,7 @@
|
|
|
361
370
|
.wp-kv { display: grid; grid-template-columns: minmax(96px, auto) 1fr; gap: 7px var(--wp-space-4); align-items: baseline; }
|
|
362
371
|
.wp-kv__k { font-size: 12px; font-weight: 600; color: var(--wp-text-muted); }
|
|
363
372
|
.wp-kv__v { font-size: 13px; color: var(--wp-text-default); min-width: 0; overflow-wrap: anywhere; }
|
|
373
|
+
.wp-kv > .wp-kv__v--block { grid-column: 1 / -1; text-align: left; white-space: pre-wrap; }
|
|
364
374
|
.wp-kv__tags, .wp-cell-tags { display: inline-flex; flex-wrap: wrap; gap: 4px; vertical-align: middle; }
|
|
365
375
|
|
|
366
376
|
/* ---------- 버튼 ---------- */
|
|
@@ -572,6 +582,7 @@
|
|
|
572
582
|
.wp-card--selected { border-color: var(--wp-navy-600); }
|
|
573
583
|
.wp-card--interactive { cursor: pointer; transition: box-shadow 0.12s, transform 0.12s; }
|
|
574
584
|
.wp-card--interactive:hover { box-shadow: var(--wp-shadow-md); transform: translateY(-1px); }
|
|
585
|
+
.wp-card--interactive[role=button]:focus-visible { outline: none; box-shadow: var(--wp-ring-focus); }
|
|
575
586
|
/* 표지 — 머리 위에 꽉 차게 깔린다. 카드가 overflow를 자르지 않으므로 위 두 모서리는
|
|
576
587
|
여기서 직접 깎는다(카드 곡률에서 테두리 1px을 뺀 값). */
|
|
577
588
|
.wp-card__cover {
|
|
@@ -722,6 +733,30 @@
|
|
|
722
733
|
.wp-table__num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
723
734
|
.wp-table__mono { font-family: var(--wp-font-mono); font-size: 12px; }
|
|
724
735
|
.wp-table__strong { font-weight: 600; color: var(--wp-text-strong); }
|
|
736
|
+
/*
|
|
737
|
+
* 좁은 폭의 목록 표(1.6) — ResultTable 이 비가상 표를 .wp-table-scroll 로 감싸고
|
|
738
|
+
* table.wp-table--stackable + td[data-label]·[data-mobile] 을 낸다. 데스크톱에서는 규칙이 없어
|
|
739
|
+
* 예전과 같다(상자가 overflow 가 되면 칸 안의 떠 있는 메뉴가 잘린다 — 넘칠 수 있는 폭에서만 켠다).
|
|
740
|
+
* ERP 는 이 시트를 읽지 않고 responsive.css 에 같은 규칙을 둔다 — 편집기 미리보기를 맞추는 사본이다.
|
|
741
|
+
*/
|
|
742
|
+
@media screen and (max-width: 1023.98px) {
|
|
743
|
+
.wp-table-scroll { overflow-x: auto; max-width: 100%; overscroll-behavior-x: contain; }
|
|
744
|
+
}
|
|
745
|
+
@media screen and (max-width: 767.98px) {
|
|
746
|
+
.wp-table.wp-table--stackable { display: block; }
|
|
747
|
+
.wp-table--stackable thead { display: none; }
|
|
748
|
+
.wp-table--stackable tbody { display: flex; flex-direction: column; gap: 8px; }
|
|
749
|
+
.wp-table--stackable tbody tr { position: relative; display: flex; flex-direction: column; padding: 10px 14px; background: var(--wp-surface-card); border: var(--wp-border-w) solid var(--wp-border-subtle); border-radius: var(--wp-radius-card); }
|
|
750
|
+
.wp-table--stackable.wp-table--hover tbody tr:hover { background: var(--wp-surface-card); }
|
|
751
|
+
.wp-table--stackable tbody tr[aria-selected='true'] { border-color: var(--wp-navy-600); background: var(--wp-surface-selected); }
|
|
752
|
+
.wp-table--stackable tbody td { display: flex; align-items: center; gap: 12px; min-height: 28px; padding: 3px 0; border: 0; white-space: normal; text-align: left; min-width: 0; }
|
|
753
|
+
.wp-table--stackable tbody td::before { content: attr(data-label); flex: none; width: 6em; font-size: 12px; color: var(--wp-text-subtle); }
|
|
754
|
+
.wp-table--stackable tbody td[data-label='']::before { display: none; }
|
|
755
|
+
.wp-table--stackable tbody td[data-label='']:has(.wp-check) { position: absolute; top: 6px; right: 6px; min-height: 0; padding: 0; }
|
|
756
|
+
.wp-table--stackable tbody td[data-mobile='title'] { order: -1; padding-right: 28px; font-weight: 600; color: var(--wp-text-strong); }
|
|
757
|
+
.wp-table--stackable tbody td[data-mobile='title']::before { display: none; }
|
|
758
|
+
.wp-table--stackable tbody td[data-mobile='hide'] { display: none; }
|
|
759
|
+
}
|
|
725
760
|
|
|
726
761
|
/* ---------- 스탯 ---------- */
|
|
727
762
|
.wp-stat { display: flex; flex-direction: column; gap: 4px; }
|
|
@@ -999,3 +1034,66 @@
|
|
|
999
1034
|
display: flex; align-items: center; justify-content: space-between; gap: var(--wp-space-2);
|
|
1000
1035
|
padding: var(--wp-space-3); border-top: var(--wp-border-w) solid var(--wp-divider);
|
|
1001
1036
|
}
|
|
1037
|
+
/*
|
|
1038
|
+
* 검색 팝오버의 딤(1.6) — Popover 가 열린 패널의 형제로 .wp-popover-backdrop 을 그린다.
|
|
1039
|
+
* 데스크톱에서는 없는 것처럼 두고(바깥 클릭은 문서 pointerdown 이 닫는다), 모바일에서만 화면을 덮는
|
|
1040
|
+
* 딤 + 하단 시트가 된다. 딤을 누르면 Popover 가 닫고 같은 탭의 click 을 삼킨다(밑의 행으로 새지 않게).
|
|
1041
|
+
*/
|
|
1042
|
+
.wp-popover-backdrop { display: none; }
|
|
1043
|
+
@media screen and (max-width: 767.98px) {
|
|
1044
|
+
.wp-popover-backdrop { display: block; position: fixed; inset: 0; z-index: calc(var(--wp-z-dialog) - 1); background: var(--wp-surface-overlay); touch-action: none; }
|
|
1045
|
+
.wp-popover-backdrop + .wp-popover {
|
|
1046
|
+
position: fixed; left: 0; right: 0; bottom: 0; top: auto;
|
|
1047
|
+
width: auto; min-width: 0; max-height: 85dvh;
|
|
1048
|
+
display: flex; flex-direction: column;
|
|
1049
|
+
border-radius: var(--wp-radius-lg) var(--wp-radius-lg) 0 0;
|
|
1050
|
+
z-index: var(--wp-z-dialog);
|
|
1051
|
+
}
|
|
1052
|
+
.wp-popover-backdrop + .wp-popover > .wp-popover__body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
|
|
1053
|
+
.wp-popover-backdrop + .wp-popover > .wp-popover__foot { flex: none; padding-bottom: calc(var(--wp-space-3) + env(safe-area-inset-bottom)); }
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/* ---------- 반응형 — ERP responsive.css §1·§2 사본 (편집기 미리보기용) ----------
|
|
1057
|
+
* ERP 는 이 시트를 읽지 않고 app/src/styles/css/responsive.css 를 정본으로 쓴다. 여기 규칙은 그 §1(태블릿+모바일)·
|
|
1058
|
+
* §2(태블릿 터치)를 렌더러 없이 컴포넌트를 쓰는 소비자와 편집기 미리보기에 맞추는 사본이다 — 컨테이너(.wp-content·
|
|
1059
|
+
* #wp-overlay-root) 한정만 뺐다. 브레이크포인트 계약: mobile ≤767.98 · tablet 768–1023.98 · desktop ≥1024, `screen and`.
|
|
1060
|
+
* 목록 표 상자(.wp-table-scroll)·행 카드·팝오버 딤은 위 절에 이미 있다.
|
|
1061
|
+
* 탭 인디케이터는 ERP(::after)와 달리 이 시트가 border-bottom 이라 overflow 상자 안에 들어오도록 margin-bottom 을 0 으로.
|
|
1062
|
+
*/
|
|
1063
|
+
@media screen and (max-width: 1023.98px) {
|
|
1064
|
+
/* Grid 프리셋 3·4열 → 2열 — 통계 카드 줄(StatCard 만 든 그리드)은 원래 열을 지킨다. 정확히 셋일 때만 셋째를 전폭으로.
|
|
1065
|
+
`.wp-grid--nocollapse`(collapse:false) 는 어느 폭에서도 열을 지킨다 — ERP responsive.css §1 과 같은 가드. */
|
|
1066
|
+
.wp-grid.wp-grid--3:not(.wp-grid--nocollapse),
|
|
1067
|
+
.wp-grid.wp-grid--4:not(.wp-grid--nocollapse) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
1068
|
+
.wp-grid.wp-grid--3:has(> .wp-card > .wp-card__body > .wp-stat) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
1069
|
+
.wp-grid.wp-grid--4:has(> .wp-card > .wp-card__body > .wp-stat) { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
1070
|
+
.wp-grid.wp-grid--3:not(.wp-grid--nocollapse):not(:has(> .wp-card > .wp-card__body > .wp-stat)) > :nth-child(3):last-child { grid-column: 1 / -1; }
|
|
1071
|
+
.wp-grid.wp-grid--4 > .wp-card > .wp-card__body > .wp-stat > .wp-stat__value { font-size: var(--wp-size-2xl); }
|
|
1072
|
+
/* PageHead: 제목이 180px 아래로 밀리면 액션이 다음 줄로 */
|
|
1073
|
+
.wp-page-head { flex-wrap: wrap; row-gap: 12px; }
|
|
1074
|
+
.wp-page-head__text { flex: 1 1 180px; }
|
|
1075
|
+
.wp-page-head__actions { flex: 0 1 auto; max-width: 100%; min-width: 0; flex-wrap: wrap; }
|
|
1076
|
+
/* Card 머리: 제목이 160px 아래로 줄면 액션이 다음 줄로 */
|
|
1077
|
+
.wp-card__head { flex-wrap: wrap; row-gap: 8px; }
|
|
1078
|
+
.wp-card__head > :has(> .wp-card__title) { flex: 1 1 160px; }
|
|
1079
|
+
/* Tabs: 넘치면 탭 줄만 가로 스크롤(페이지 전체가 옆으로 밀리던 것). 경계선은 inset 그림자로, 포커스 링은 안쪽으로. */
|
|
1080
|
+
.wp-tabs { overflow-x: auto; overflow-y: hidden; scrollbar-width: none; overscroll-behavior-x: contain; border-bottom: 0; box-shadow: inset 0 -1px 0 var(--wp-border-subtle); }
|
|
1081
|
+
.wp-tabs::-webkit-scrollbar { display: none; }
|
|
1082
|
+
.wp-tab { flex: none; margin-bottom: 0; }
|
|
1083
|
+
.wp-tab:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--wp-border-focus); }
|
|
1084
|
+
/* 페이지 범위 "1–10 / 68" 이 좁은 칸에서 네 줄로 꺾이던 것 */
|
|
1085
|
+
.wp-pager .wp-num { white-space: nowrap; }
|
|
1086
|
+
}
|
|
1087
|
+
/* 태블릿 + 터치 — 손가락 기기만. 시각 크기보다 누름 영역이 목적이다. */
|
|
1088
|
+
@media screen and (min-width: 768px) and (max-width: 1023.98px) and (pointer: coarse) {
|
|
1089
|
+
.wp-pager__page { min-width: 36px; height: 36px; }
|
|
1090
|
+
.wp-seg__item { height: 32px; }
|
|
1091
|
+
.wp-table__sort { min-height: 32px; }
|
|
1092
|
+
.wp-iconbtn { min-width: 36px; min-height: 36px; }
|
|
1093
|
+
/* 글자 없는 체크박스(표 선택 칸)는 16px 상자뿐 — 음수 여백으로 배치는 그대로 두고 누를 자리만 넓힌다 */
|
|
1094
|
+
.wp-check:not(:has(.wp-check__text)) { padding: 8px; margin: -8px; }
|
|
1095
|
+
}
|
|
1096
|
+
/* 모바일 행 카드의 빈 칸 — 라벨만 덩그러니 남지 않게 '—'(플레인 Table 은 데스크톱 표기를 바꾸지 않아 CSS 로만) */
|
|
1097
|
+
@media screen and (max-width: 767.98px) {
|
|
1098
|
+
.wp-table--stackable tbody td:empty::after { content: '—'; color: var(--wp-text-subtle); }
|
|
1099
|
+
}
|
|
@@ -95,8 +95,16 @@ interface SduiCalcRef {
|
|
|
95
95
|
* - `join` `sep`(기본 ',')으로 잇는다
|
|
96
96
|
* - `split` `sep`(기본 ',')으로 가른다. `take`를 주면 그 조각 하나(음수는 뒤에서)
|
|
97
97
|
* - `sortBy` `field` 기준 정렬(`desc: true`면 내림차순). 원본은 건드리지 않는다
|
|
98
|
+
* - `map` 객체 배열의 각 행을 `field` 값으로 바꾼다
|
|
98
99
|
*/
|
|
99
|
-
| 'get' | 'pluck' | 'filter' | 'some' | 'every' | 'includes' | 'distinct' | 'except' | 'groupBy' | 'groupCount' | 'entries' | 'fromEntries' | 'length' | 'join' | 'split' | 'sortBy'
|
|
100
|
+
| 'get' | 'pluck' | 'filter' | 'some' | 'every' | 'includes' | 'distinct' | 'except' | 'groupBy' | 'groupCount' | 'entries' | 'fromEntries' | 'length' | 'join' | 'split' | 'sortBy' | 'map'
|
|
101
|
+
/**
|
|
102
|
+
* 서식 — 원시 값을 사람이 읽는 글자로(KST 고정, 브라우저 시간대와 무관).
|
|
103
|
+
* - `formatDate` `[값, 'date'|'datetime'|'time']`(기본 datetime). 빈 값은 '', 못 읽는 글은 원문
|
|
104
|
+
* - `formatNumber` `[값, 소수자리?]` ko-KR 천 단위. 자리를 주면 0~6 으로 고정
|
|
105
|
+
* - `today` `['date'|'datetime'|'time']`(기본 date) 지금의 한국 시각 — 출력일 같은 자리
|
|
106
|
+
*/
|
|
107
|
+
| 'formatDate' | 'formatNumber' | 'today';
|
|
100
108
|
args: SduiPropValue[];
|
|
101
109
|
/** sum/count·목록 연산에서 객체 배열의 필드를 지정 */
|
|
102
110
|
field?: string;
|
|
@@ -305,6 +313,30 @@ type SduiAction = {
|
|
|
305
313
|
pendingKey?: string;
|
|
306
314
|
/** 성공 시 무효화할 조회 prefix — onSuccess의 invalidate 액션 단축 */
|
|
307
315
|
invalidate?: string;
|
|
316
|
+
/**
|
|
317
|
+
* 배열을 주면 **원소마다** 같은 요청을 낸다 — 선택한 N건 삭제가 이것이다.
|
|
318
|
+
* `endpoint`·`body` 안에서 원소는 `$item`(원소 전체 또는 `{"$item":"id"}`), 번호는
|
|
319
|
+
* `$index` 로 읽는다(바깥 ForEach 행보다 이 프레임이 가깝다). 그 둘은 디스패치
|
|
320
|
+
* 시점에도 원본으로 남아 원소마다 풀린다.
|
|
321
|
+
*
|
|
322
|
+
* ```json
|
|
323
|
+
* { "action": "request", "method": "DELETE",
|
|
324
|
+
* "endpoint": { "$calc": { "op": "concat", "args": ["/clients/", { "$item": true }] } },
|
|
325
|
+
* "each": { "$state": "deleteIds" }, "invalidate": "/clients", "pendingKey": "deleting",
|
|
326
|
+
* "onSuccess": [{ "action": "toast", "tone": "success", "title": "삭제되었습니다" },
|
|
327
|
+
* { "action": "close", "key": "deleteOpen" }],
|
|
328
|
+
* "onError": [{ "action": "toast", "tone": "danger",
|
|
329
|
+
* "message": { "$event": "message" } }] }
|
|
330
|
+
* ```
|
|
331
|
+
*
|
|
332
|
+
* - 동시에 3건까지 낸다. 전부 성공하면 `onSuccess` **한 번**(이벤트 값 = 응답 배열,
|
|
333
|
+
* 원소 순서). 하나라도 실패하면 `onError` 한 번 — 이벤트 값은
|
|
334
|
+
* `{ failed, succeeded, total, message, errors[] }` 라 `{"$event":"failed"}` 로 수를
|
|
335
|
+
* 읽는다(`errorKey` 에도 같은 값). 생략하면 'N건 중 M건 실패' 토스트.
|
|
336
|
+
* - `invalidate` 는 성공한 것이 하나라도 있으면 건다 — 서버는 그만큼 바뀌었다.
|
|
337
|
+
* - 빈 배열·배열 아님은 요청도 체인도 없이 끝난다. 버튼은 선택 수로 잠근다.
|
|
338
|
+
*/
|
|
339
|
+
each?: SduiPropValue;
|
|
308
340
|
}
|
|
309
341
|
/** 클라이언트 조회 캐시 무효화 — 어댑터의 invalidate로 위임 */
|
|
310
342
|
| {
|
|
@@ -405,7 +437,31 @@ interface SduiPage {
|
|
|
405
437
|
* title(문서 제목)을 포기하지 않아도 되는 자리다.
|
|
406
438
|
*/
|
|
407
439
|
title?: string;
|
|
440
|
+
/**
|
|
441
|
+
* 레코드 이름을 제목으로 — `title` 대신 **상태에서 푼** 값을 쓴다.
|
|
442
|
+
*
|
|
443
|
+
* 상세 화면의 `title`("장비 상세")은 모든 레코드에서 같아 여러 탭·방문 기록에서 구별이
|
|
444
|
+
* 안 된다. 여기에 `{"$state":"rt.asset.name"}` 이나 `$calc` 를 주면, 값이 비어 있지 않은
|
|
445
|
+
* 문자열(숫자면 문자열로)일 때 자동 페이지 헤더의 제목이 그 값이 되고 호스트에도
|
|
446
|
+
* `adapter.setTitle(제목)` 으로 알린다(문서 <title>·브레드크럼은 호스트 몫). 비어
|
|
447
|
+
* 있으면(조회 전) `title` 로 돌아간다. `title` 은 그대로 둔다 — 호스트가 스펙 JSON 만 보고
|
|
448
|
+
* 메뉴·크럼을 세우는 자리라 문자열이어야 한다.
|
|
449
|
+
*/
|
|
450
|
+
titleFrom?: SduiPropValue;
|
|
408
451
|
subtitle?: string;
|
|
452
|
+
/**
|
|
453
|
+
* 이탈 가드 — 참이면 새로고침·창 닫기(beforeunload)와 앱 안 이동 전에 확인한다.
|
|
454
|
+
*
|
|
455
|
+
* `{"$state":"notifDirty"}` 처럼 상태 식을 준다. 문자열이면 그 문구로 묻고, 그 밖의
|
|
456
|
+
* 참값은 기본 문구다. Form 을 쓰는 화면은 이것 없이 `Form.guard: true` 로 같은 통로를
|
|
457
|
+
* 탄다(작성 중일 때만, 저장 직후 이동은 묻지 않는다). 상태로 입력을 모으는 화면
|
|
458
|
+
* (알림 설정 등)이 이 자리를 쓴다.
|
|
459
|
+
*
|
|
460
|
+
* 앱 안 이동은 두 갈래다: 호스트 어댑터가 `setLeaveGuard` 를 받으면 라우터 차단은
|
|
461
|
+
* 호스트가 하고(셸 메뉴·브라우저 뒤로 가기까지), 안 받으면 렌더러가 자기 `navigate`
|
|
462
|
+
* 액션 앞에서만 확인 창을 띄운다.
|
|
463
|
+
*/
|
|
464
|
+
leaveGuard?: SduiPropValue;
|
|
409
465
|
/** 실시간 채널 구독 — 어댑터의 subscribe로 위임(이벤트 수신 시 조용한 재조회) */
|
|
410
466
|
subscribe?: string[];
|
|
411
467
|
/** URL 쿼리파라미터와 동기화할 상태 키 — 딥링크/새로고침 보존용 */
|