@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.
Files changed (143) hide show
  1. package/dist/{DatePicker-C_TT_SOw.d.ts → DatePicker-DX9juElE.d.ts} +10 -1
  2. package/dist/{chunk-GZBY7I4Y.js → chunk-2KQL6DYG.js} +3 -3
  3. package/dist/{chunk-RYBBPFI7.js → chunk-2QXA52DG.js} +4 -4
  4. package/dist/{chunk-DMLDKW7K.js → chunk-37BNG4DT.js} +1 -1
  5. package/dist/{chunk-5WW4MBRO.js → chunk-3JTBOABA.js} +6 -2
  6. package/dist/{chunk-UV6HHFEW.js → chunk-4OABUPIU.js} +129 -68
  7. package/dist/{chunk-ZORQPB6B.js → chunk-53KS4E5N.js} +318 -359
  8. package/dist/{chunk-I5SD7XRA.js → chunk-6TI4WF5Y.js} +3 -3
  9. package/dist/{chunk-44TDRCKR.js → chunk-6XI2VF2O.js} +5 -5
  10. package/dist/{chunk-HBIIEYKW.js → chunk-7C54CKFZ.js} +4 -4
  11. package/dist/{chunk-IZ3JA2BR.js → chunk-7JP52QQO.js} +3 -3
  12. package/dist/{chunk-I45EUZC6.js → chunk-AY4GM6KQ.js} +4 -2
  13. package/dist/{chunk-SAPBDJJQ.js → chunk-AZSLSAJB.js} +2 -2
  14. package/dist/chunk-BITS7UJY.js +333 -0
  15. package/dist/{chunk-RNOUXUBO.js → chunk-BLFJMMBW.js} +306 -57
  16. package/dist/{chunk-YOZN35HD.js → chunk-CEVZAFPO.js} +3 -3
  17. package/dist/chunk-CI5OZ2QT.js +106 -0
  18. package/dist/{chunk-FNMS7OB7.js → chunk-CSE3PZOZ.js} +5 -5
  19. package/dist/chunk-CSZCWBOE.js +256 -0
  20. package/dist/{chunk-JIV4YHCZ.js → chunk-EJIWOD7W.js} +1 -1
  21. package/dist/{chunk-JZXU22PE.js → chunk-FEKKOBPX.js} +208 -117
  22. package/dist/chunk-GSM664TX.js +38 -0
  23. package/dist/{chunk-JYAR7D5U.js → chunk-HRHKQOOF.js} +2 -2
  24. package/dist/{chunk-CXW6KT6X.js → chunk-HXLQSO66.js} +3 -3
  25. package/dist/{chunk-O4IISNE7.js → chunk-IB2ZPUTF.js} +51 -12
  26. package/dist/{chunk-MPT7WJG2.js → chunk-IQXLNICY.js} +5 -5
  27. package/dist/{chunk-OQ52XMGG.js → chunk-K6RE23XX.js} +4 -4
  28. package/dist/{chunk-VY7MVBSE.js → chunk-KZ6U6FKB.js} +2 -2
  29. package/dist/{chunk-WPSK2CB5.js → chunk-LIZTK3HO.js} +16 -5
  30. package/dist/{chunk-Q4SWUINA.js → chunk-LKQJJSVJ.js} +1 -1
  31. package/dist/{chunk-E6ISNQK3.js → chunk-M6H3SHIS.js} +31 -8
  32. package/dist/chunk-MN4RZ2WP.js +380 -0
  33. package/dist/{chunk-GTGS2QRL.js → chunk-NA232CBX.js} +3 -3
  34. package/dist/{chunk-ZFNFPXPR.js → chunk-NAIWQQPW.js} +2 -2
  35. package/dist/{chunk-YVTV4ZV6.js → chunk-NWYYNE3Q.js} +1 -1
  36. package/dist/{chunk-DKVSM5HX.js → chunk-OUEJWC2Z.js} +6 -6
  37. package/dist/{chunk-DOL57OOP.js → chunk-OYDJHQNZ.js} +3 -3
  38. package/dist/{chunk-YWUDBLO6.js → chunk-QVS6K4FP.js} +2 -2
  39. package/dist/{chunk-N5RKM6IC.js → chunk-RU5U2YMT.js} +4 -4
  40. package/dist/{chunk-A6LAIGUS.js → chunk-V7BWMOHO.js} +7 -4
  41. package/dist/{chunk-2HLXCKK7.js → chunk-VPOOIEKT.js} +2 -2
  42. package/dist/{chunk-2YP5N4VV.js → chunk-VZOQRQAR.js} +1 -1
  43. package/dist/{chunk-GNA6SX2H.js → chunk-W4VA76QO.js} +2 -2
  44. package/dist/{chunk-BEM2YS7U.js → chunk-WEG3V7ZB.js} +3 -3
  45. package/dist/{chunk-XGFDY43I.js → chunk-WTIYILY3.js} +88 -18
  46. package/dist/{chunk-RWSH2MBU.js → chunk-X2NIRVLN.js} +73 -19
  47. package/dist/{chunk-AXGF4M57.js → chunk-XAXROQKB.js} +1 -1
  48. package/dist/{chunk-NIJUVTEW.js → chunk-XG22XKKD.js} +3 -3
  49. package/dist/{chunk-A5XQOEVH.js → chunk-YN7X7BEO.js} +4 -4
  50. package/dist/{chunk-KO6OEY3G.js → chunk-YNEEJCSZ.js} +3 -13
  51. package/dist/{chunk-7P7N43JV.js → chunk-YZTTFEL4.js} +2 -2
  52. package/dist/index.d.ts +494 -89
  53. package/dist/index.js +1098 -124
  54. package/dist/{layout-C3nRXMwb.d.ts → layout-DmRikwZb.d.ts} +5 -4
  55. package/dist/meta.d.ts +1 -1
  56. package/dist/meta.js +286 -22
  57. package/dist/rich/ExcelUploadCard.d.ts +1 -1
  58. package/dist/rich/ExcelUploadCard.js +5 -4
  59. package/dist/rich.d.ts +21 -3
  60. package/dist/rich.js +277 -35
  61. package/dist/styles.css +102 -4
  62. package/dist/{types-COjGKgmd.d.ts → types-CYfIthux.d.ts} +57 -1
  63. package/dist/widgets/ApprovalLineEditor.d.ts +1 -1
  64. package/dist/widgets/ApprovalLineEditor.js +9 -8
  65. package/dist/widgets/ApprovalOverridesEditor.d.ts +1 -1
  66. package/dist/widgets/ApprovalOverridesEditor.js +11 -10
  67. package/dist/widgets/ApprovalStepper.d.ts +29 -4
  68. package/dist/widgets/ApprovalStepper.js +3 -3
  69. package/dist/widgets/AutocompleteField.d.ts +1 -1
  70. package/dist/widgets/AutocompleteField.js +9 -8
  71. package/dist/widgets/ColumnBoard.d.ts +1 -1
  72. package/dist/widgets/ColumnBoard.js +3 -3
  73. package/dist/widgets/DiffView.d.ts +1 -1
  74. package/dist/widgets/DiffView.js +3 -3
  75. package/dist/widgets/Disclosure.d.ts +1 -1
  76. package/dist/widgets/Disclosure.js +3 -3
  77. package/dist/widgets/EventCalendar.d.ts +5 -1
  78. package/dist/widgets/EventCalendar.js +5 -3
  79. package/dist/widgets/FileDropzone.d.ts +1 -1
  80. package/dist/widgets/FileDropzone.js +9 -7
  81. package/dist/widgets/ImageLightbox.d.ts +1 -1
  82. package/dist/widgets/ImageLightbox.js +7 -5
  83. package/dist/widgets/InlineEdit.d.ts +1 -1
  84. package/dist/widgets/InlineEdit.js +3 -3
  85. package/dist/widgets/JsonField.d.ts +1 -1
  86. package/dist/widgets/JsonField.js +3 -3
  87. package/dist/widgets/LiveClock.d.ts +1 -1
  88. package/dist/widgets/LiveClock.js +2 -2
  89. package/dist/widgets/OptionSchemaEditor.d.ts +1 -1
  90. package/dist/widgets/OptionSchemaEditor.js +4 -4
  91. package/dist/widgets/OptionValueFields.d.ts +1 -1
  92. package/dist/widgets/OptionValueFields.js +4 -4
  93. package/dist/widgets/OrgChart.d.ts +1 -1
  94. package/dist/widgets/OrgChart.js +4 -4
  95. package/dist/widgets/OrgChartBoard.d.ts +15 -33
  96. package/dist/widgets/OrgChartBoard.js +10 -8
  97. package/dist/widgets/PeriodFilter.d.ts +1 -1
  98. package/dist/widgets/PeriodFilter.js +6 -5
  99. package/dist/widgets/PermissionMatrixEditor.d.ts +34 -2
  100. package/dist/widgets/PermissionMatrixEditor.js +6 -4
  101. package/dist/widgets/PositionSelect.d.ts +1 -1
  102. package/dist/widgets/PositionSelect.js +5 -4
  103. package/dist/widgets/PrintSheet.d.ts +15 -2
  104. package/dist/widgets/PrintSheet.js +3 -3
  105. package/dist/widgets/ProfileEditDiffCard.d.ts +1 -1
  106. package/dist/widgets/ProfileEditDiffCard.js +5 -4
  107. package/dist/widgets/QRDisplay.d.ts +1 -1
  108. package/dist/widgets/QRDisplay.js +5 -4
  109. package/dist/widgets/RankBoard.d.ts +1 -1
  110. package/dist/widgets/RankBoard.js +8 -6
  111. package/dist/widgets/RecordSheet.d.ts +1 -1
  112. package/dist/widgets/RecordSheet.js +2 -2
  113. package/dist/widgets/RoleMemberPicker.d.ts +1 -1
  114. package/dist/widgets/RoleMemberPicker.js +6 -5
  115. package/dist/widgets/SearchableMultiPicker.d.ts +1 -1
  116. package/dist/widgets/SearchableMultiPicker.js +7 -6
  117. package/dist/widgets/SecureScreen.d.ts +1 -1
  118. package/dist/widgets/SecureScreen.js +3 -3
  119. package/dist/widgets/StackedMeter.d.ts +1 -1
  120. package/dist/widgets/StackedMeter.js +2 -2
  121. package/dist/widgets/StagePeriodEditor.d.ts +1 -1
  122. package/dist/widgets/StagePeriodEditor.js +6 -5
  123. package/dist/widgets/Strip.d.ts +1 -1
  124. package/dist/widgets/Strip.js +2 -2
  125. package/dist/widgets/SummaryTable.d.ts +1 -1
  126. package/dist/widgets/SummaryTable.js +2 -2
  127. package/dist/widgets/TagInput.d.ts +1 -1
  128. package/dist/widgets/TagInput.js +3 -3
  129. package/dist/widgets/TagMultiSelect.d.ts +1 -1
  130. package/dist/widgets/TagMultiSelect.js +5 -4
  131. package/dist/widgets/Timeline.d.ts +1 -1
  132. package/dist/widgets/Timeline.js +4 -4
  133. package/dist/widgets/TreePicker.d.ts +1 -1
  134. package/dist/widgets/TreePicker.js +8 -7
  135. package/dist/widgets/TreeView.d.ts +1 -1
  136. package/dist/widgets/TreeView.js +7 -6
  137. package/dist/widgets/index.d.ts +3 -3
  138. package/dist/widgets/index.js +84 -83
  139. package/package.json +1 -1
  140. package/dist/chunk-LFEJLOFN.js +0 -352
  141. package/dist/chunk-MMIGOPO5.js +0 -111
  142. package/dist/chunk-TFGNKFL2.js +0 -39
  143. package/dist/chunk-XQPTSEJC.js +0 -135
package/dist/rich.js CHANGED
@@ -1,10 +1,11 @@
1
- import { useOptionalModule, DependencyPlaceholder } from './chunk-YVTV4ZV6.js';
2
- export { DatePicker, DateRangePicker, TimePicker } from './chunk-RWSH2MBU.js';
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-XQPTSEJC.js';
5
- import { useSduiAssetResolver } from './chunk-KO6OEY3G.js';
6
- import { Spinner, EmptyState } from './chunk-RNOUXUBO.js';
7
- import './chunk-5WW4MBRO.js';
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
- const eok = abs / 1e8;
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, format) {
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" ? format(entry.value) : String(entry.value ?? "");
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: "1 1 0%", minWidth: 0, height } : { position: "relative", width: "100%", height },
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("div", { style: { display: "flex", alignItems: "center", gap: "var(--wp-space-6)" }, children: [
337
- chartArea,
338
- /* @__PURE__ */ jsx(
339
- SliceLegendTable,
340
- {
341
- rows: rows.map((row, i) => ({ ...legendItems2[i], value: sliceValue(row) })),
342
- format,
343
- total: legendTotal !== void 0 && Number.isFinite(denomOverride) ? denomOverride : void 0
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(CartesianGrid, { strokeDasharray: "3 3", stroke: "var(--wp-divider)", vertical: false }),
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) => format(v),
425
- width: 52
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: tooltipContent
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
- @media (max-width: 900px) {
330
- .wp-grid--3, .wp-grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
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: 600px) {
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 쿼리파라미터와 동기화할 상태 키 — 딥링크/새로고침 보존용 */
@@ -1,4 +1,4 @@
1
- import { S as SduiBoxProps } from '../layout-C3nRXMwb.js';
1
+ import { S as SduiBoxProps } from '../layout-DmRikwZb.js';
2
2
  import React__default from 'react';
3
3
 
4
4
  /** 저장 JSON 계약(approval-line) — 키 이름 변경 금지. */