@wipco/sdui 0.50.1 → 1.0.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 (111) hide show
  1. package/dist/{DatePicker-BK_jsV7i.d.ts → DatePicker-C_TT_SOw.d.ts} +25 -0
  2. package/dist/{chunk-OEWTVLPZ.js → chunk-2HLXCKK7.js} +2 -2
  3. package/dist/{chunk-PWVHAUJC.js → chunk-2YP5N4VV.js} +6 -4
  4. package/dist/{chunk-QTHYLNJW.js → chunk-37SFFW37.js} +2 -13
  5. package/dist/{chunk-YQOMSWW5.js → chunk-5WW4MBRO.js} +1 -1
  6. package/dist/{chunk-LIOS3PF2.js → chunk-5XPZ3RSA.js} +3 -4
  7. package/dist/{chunk-ZD3VNVRF.js → chunk-7BGDSSAN.js} +5 -13
  8. package/dist/{chunk-Y2LIEWQX.js → chunk-7FZTUI6U.js} +6 -5
  9. package/dist/{chunk-HJ5VSXR4.js → chunk-7P7N43JV.js} +2 -2
  10. package/dist/{chunk-55VB6SDU.js → chunk-A5XQOEVH.js} +23 -20
  11. package/dist/{chunk-77LKJCMM.js → chunk-A6LAIGUS.js} +20 -108
  12. package/dist/{chunk-YXS5FXVS.js → chunk-AC5TI3BE.js} +7 -16
  13. package/dist/{chunk-AIOJYVJ2.js → chunk-AXGF4M57.js} +1 -1
  14. package/dist/{chunk-5MPK3NZK.js → chunk-B6WMC2AN.js} +3 -3
  15. package/dist/{chunk-C6ZV4VBN.js → chunk-C37U3GCC.js} +4 -4
  16. package/dist/{chunk-265YPW4W.js → chunk-DMLDKW7K.js} +1 -1
  17. package/dist/{chunk-FZ72IDVF.js → chunk-E6ISNQK3.js} +2 -2
  18. package/dist/{chunk-STX2AX6Y.js → chunk-FLAN7CV3.js} +7 -39
  19. package/dist/{chunk-IV5YTVHR.js → chunk-GE6BYNQJ.js} +18 -33
  20. package/dist/{chunk-ZX36LKJ7.js → chunk-GTGS2QRL.js} +7 -6
  21. package/dist/{chunk-NL3I6F2I.js → chunk-HBBTENSV.js} +3 -3
  22. package/dist/{chunk-RUU7WBGR.js → chunk-I45EUZC6.js} +19 -9
  23. package/dist/{chunk-JBKNIALK.js → chunk-I5SD7XRA.js} +9 -6
  24. package/dist/{chunk-X7GHY2RI.js → chunk-IFKYGJXP.js} +14 -42
  25. package/dist/{chunk-J3YMUR6R.js → chunk-IZ3JA2BR.js} +3 -3
  26. package/dist/{chunk-AUW5RP24.js → chunk-JIV4YHCZ.js} +7 -6
  27. package/dist/{chunk-M6KM3FJS.js → chunk-JYAR7D5U.js} +2 -2
  28. package/dist/{chunk-DKPNFEVU.js → chunk-KO6OEY3G.js} +19 -6
  29. package/dist/{chunk-JHHMFL5D.js → chunk-LFEJLOFN.js} +28 -16
  30. package/dist/chunk-MMIGOPO5.js +111 -0
  31. package/dist/{chunk-YSSNP6JM.js → chunk-N25YP7ID.js} +6 -21
  32. package/dist/{chunk-F2W2FWOT.js → chunk-N5RKM6IC.js} +7 -7
  33. package/dist/{chunk-C4HYWZML.js → chunk-O4IISNE7.js} +1 -1
  34. package/dist/chunk-Q4SWUINA.js +51 -0
  35. package/dist/{chunk-TUOG7EEI.js → chunk-RNOUXUBO.js} +418 -22
  36. package/dist/{chunk-4VZI3PLI.js → chunk-RYHEV5E3.js} +15 -39
  37. package/dist/{chunk-AVRNQOB5.js → chunk-SAPBDJJQ.js} +14 -13
  38. package/dist/{chunk-OPJ4DYNI.js → chunk-SJ3737RA.js} +156 -158
  39. package/dist/{chunk-UMANGYRD.js → chunk-TFGNKFL2.js} +1 -1
  40. package/dist/{chunk-KYGSMUUU.js → chunk-VY7MVBSE.js} +2 -2
  41. package/dist/{chunk-VDBUXYEB.js → chunk-WOYMEEPH.js} +8 -8
  42. package/dist/{chunk-I5Z2RVDF.js → chunk-WSLT32Q4.js} +278 -30
  43. package/dist/{chunk-NYUHSMUU.js → chunk-XGFDY43I.js} +2 -2
  44. package/dist/{chunk-ODKZF4IT.js → chunk-YOZN35HD.js} +17 -9
  45. package/dist/{chunk-EKP6Z3G6.js → chunk-YVTV4ZV6.js} +1 -1
  46. package/dist/{chunk-6BCWFPNJ.js → chunk-YWUDBLO6.js} +2 -2
  47. package/dist/chunk-Z4PYMTTS.js +41 -0
  48. package/dist/{chunk-WXIQLWIX.js → chunk-ZFNFPXPR.js} +2 -2
  49. package/dist/{chunk-7GOWOSX6.js → chunk-ZORQPB6B.js} +11 -5
  50. package/dist/index.d.ts +1096 -941
  51. package/dist/index.js +369 -610
  52. package/dist/meta.d.ts +13 -12
  53. package/dist/meta.js +84 -16
  54. package/dist/rich/ExcelUploadCard.d.ts +7 -0
  55. package/dist/rich/ExcelUploadCard.js +17 -13
  56. package/dist/rich.d.ts +1 -1
  57. package/dist/rich.js +25 -12
  58. package/dist/styles.css +10 -2
  59. package/dist/{types-D14eqLpf.d.ts → types-C7y9CpgF.d.ts} +7 -5
  60. package/dist/widgets/ApprovalLineEditor.js +7 -6
  61. package/dist/widgets/ApprovalOverridesEditor.d.ts +11 -1
  62. package/dist/widgets/ApprovalOverridesEditor.js +9 -8
  63. package/dist/widgets/ApprovalStepper.js +3 -3
  64. package/dist/widgets/AutocompleteField.d.ts +1 -0
  65. package/dist/widgets/AutocompleteField.js +7 -6
  66. package/dist/widgets/ColumnBoard.js +3 -3
  67. package/dist/widgets/DiffView.js +3 -3
  68. package/dist/widgets/Disclosure.js +3 -3
  69. package/dist/widgets/EventCalendar.d.ts +16 -1
  70. package/dist/widgets/EventCalendar.js +3 -3
  71. package/dist/widgets/FileDropzone.js +7 -6
  72. package/dist/widgets/ImageLightbox.js +5 -4
  73. package/dist/widgets/InlineEdit.js +3 -3
  74. package/dist/widgets/JsonField.js +3 -3
  75. package/dist/widgets/LiveClock.js +2 -2
  76. package/dist/widgets/OptionSchemaEditor.js +4 -4
  77. package/dist/widgets/OptionValueFields.d.ts +5 -0
  78. package/dist/widgets/OptionValueFields.js +4 -4
  79. package/dist/widgets/OrgChart.js +4 -3
  80. package/dist/widgets/OrgChartBoard.d.ts +93 -4
  81. package/dist/widgets/OrgChartBoard.js +9 -6
  82. package/dist/widgets/PeriodFilter.js +4 -4
  83. package/dist/widgets/PermissionMatrixEditor.d.ts +16 -0
  84. package/dist/widgets/PermissionMatrixEditor.js +4 -4
  85. package/dist/widgets/PositionSelect.d.ts +6 -1
  86. package/dist/widgets/PositionSelect.js +5 -4
  87. package/dist/widgets/PrintSheet.js +3 -3
  88. package/dist/widgets/ProfileEditDiffCard.d.ts +6 -0
  89. package/dist/widgets/ProfileEditDiffCard.js +4 -4
  90. package/dist/widgets/QRDisplay.js +4 -4
  91. package/dist/widgets/RankBoard.d.ts +6 -0
  92. package/dist/widgets/RankBoard.js +6 -5
  93. package/dist/widgets/RecordSheet.js +2 -2
  94. package/dist/widgets/RoleMemberPicker.d.ts +11 -5
  95. package/dist/widgets/RoleMemberPicker.js +5 -4
  96. package/dist/widgets/SearchableMultiPicker.js +5 -5
  97. package/dist/widgets/SecureScreen.d.ts +12 -0
  98. package/dist/widgets/SecureScreen.js +3 -3
  99. package/dist/widgets/StackedMeter.js +2 -2
  100. package/dist/widgets/StagePeriodEditor.js +4 -5
  101. package/dist/widgets/Strip.js +2 -2
  102. package/dist/widgets/SummaryTable.js +2 -2
  103. package/dist/widgets/TagMultiSelect.d.ts +0 -1
  104. package/dist/widgets/TagMultiSelect.js +4 -4
  105. package/dist/widgets/Timeline.js +4 -4
  106. package/dist/widgets/TreePicker.js +6 -6
  107. package/dist/widgets/TreeView.js +5 -5
  108. package/dist/widgets/index.d.ts +3 -3
  109. package/dist/widgets/index.js +81 -79
  110. package/package.json +1 -1
  111. package/dist/chunk-EKVA7T7P.js +0 -198
package/dist/index.js CHANGED
@@ -1,24 +1,22 @@
1
- export { loadOptionalModule } from './chunk-EKP6Z3G6.js';
2
- import { useSduiDispatch, SduiDialog, useSduiCan, SduiDispatchContext, SduiPermissionProvider } from './chunk-UMANGYRD.js';
3
- export { SduiDialog, SduiPermissionProvider, useSduiCan, useSduiDispatch } from './chunk-UMANGYRD.js';
4
- import { Text, Spacer, Show, Section, Row, PageHead, KeyValueList, Grid, Divider, Col, Actions } from './chunk-QTHYLNJW.js';
5
- export { Actions, Col, Divider, Grid, KeyValueList, PageHead, Row, Section, Show, Spacer, Text } from './chunk-QTHYLNJW.js';
6
- import { ImageViewer } from './chunk-AVRNQOB5.js';
7
- export { ImageViewer } from './chunk-AVRNQOB5.js';
8
- import { TimePicker } from './chunk-EKVA7T7P.js';
9
- export { TimePicker } from './chunk-EKVA7T7P.js';
10
- import { normalizeSearchFields, searchValuesFromParams, splitMultiValue, includesLooseSearch, cascadingValuePaths, SearchToolbar, searchFieldKeys, Popover } from './chunk-VDBUXYEB.js';
11
- export { SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue } from './chunk-VDBUXYEB.js';
12
- import { HoistedStyle, DateRangePicker, DatePicker } from './chunk-I5Z2RVDF.js';
13
- export { DatePicker, DateRangePicker } from './chunk-I5Z2RVDF.js';
1
+ export { loadOptionalModule } from './chunk-YVTV4ZV6.js';
2
+ import { useSduiDispatch, SduiDialog, useSduiCan, SduiDispatchContext, SduiPermissionProvider } from './chunk-TFGNKFL2.js';
3
+ export { SduiDialog, SduiPermissionProvider, useSduiCan, useSduiDispatch } from './chunk-TFGNKFL2.js';
4
+ import { Text, Spacer, Show, Section, Row, PageHead, KeyValueList, Grid, Divider, Col, Actions } from './chunk-37SFFW37.js';
5
+ export { Actions, Col, Divider, Grid, KeyValueList, PageHead, Row, Section, Show, Spacer, Text } from './chunk-37SFFW37.js';
6
+ import { ImageViewer } from './chunk-SAPBDJJQ.js';
7
+ export { ImageViewer } from './chunk-SAPBDJJQ.js';
8
+ import { normalizeSearchFields, searchValuesFromParams, splitMultiValue, includesLooseSearch, cascadingValuePaths, SearchToolbar, searchFieldKeys, Popover } from './chunk-WOYMEEPH.js';
9
+ export { SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue } from './chunk-WOYMEEPH.js';
10
+ import { HoistedStyle, parseYmd, YMD_RE, TimePicker, DateRangePicker, DatePicker } from './chunk-WSLT32Q4.js';
11
+ export { DatePicker, DateRangePicker, TimePicker } from './chunk-WSLT32Q4.js';
14
12
  export { OverlayContainerContext, OverlayLayer, getOverlayRoot, useOverlayLayer } from './chunk-XQPTSEJC.js';
15
- import { DEV, isSafeNavigationTarget, useSduiAdapter, hostFetchOf, useSduiSandbox, resolveAdapter, assetResolverOf, SduiAdapterContext } from './chunk-DKPNFEVU.js';
16
- export { defaultAdapter, useSduiAssetResolver, useSduiSandbox } from './chunk-DKPNFEVU.js';
17
- import { EmptyState, Button, Checkbox, SegmentedControl, Spinner, Select, Pagination, Icon, Card, Badge, ProgressBar, Tag, Toast, Textarea, Tabs, Table, Switch, StatCard, Input, IconButton, Field, Alert, warnOnce } from './chunk-TUOG7EEI.js';
18
- export { Alert, Avatar, Badge, Breadcrumb, Button, Card, Checkbox, Dialog, EmptyState, Field, Icon, IconButton, Input, Pagination, ProgressBar, Radio, SegmentedControl, Select, SidebarNav, Spinner, StatCard, Switch, Table, Tabs, Tag, Textarea, Toast, Tooltip } from './chunk-TUOG7EEI.js';
19
- import { boxed, LAYOUT_CSS, childLayoutFrom, CHILD_LAYOUT_PROPS } from './chunk-YQOMSWW5.js';
20
- export { CHILD_LAYOUT_PROPS, LAYOUT_CSS, boxed, childLayoutFrom, columnsClass, containerLayoutStyle, joinClass, mergeStyle, typographyStyle } from './chunk-YQOMSWW5.js';
21
- import React9, { createContext, useState, useMemo, useRef, useEffect, useCallback, useContext } from 'react';
13
+ import { DEV, isSafeNavigationTarget, useSduiAdapter, hostFetchOf, useSduiSandbox, resolveAdapter, assetResolverOf, SduiAdapterContext } from './chunk-KO6OEY3G.js';
14
+ export { defaultAdapter, useSduiAssetResolver, useSduiSandbox } from './chunk-KO6OEY3G.js';
15
+ import { EmptyState, Button, Checkbox, SegmentedControl, Spinner, Select, Pagination, fieldOf, Icon, Card, Badge, ProgressBar, Tag, Toast, Textarea, Tabs, Table, Switch, StatCard, Input, IconButton, Field, Alert, warnOnce, isPlainObject, CALC_OPS, evaluateCalc, ICON_NODES, toNumber } from './chunk-RNOUXUBO.js';
16
+ export { Alert, Avatar, Badge, Breadcrumb, Button, CALC_OPS, Card, Checkbox, Dialog, EmptyState, Field, Icon, IconButton, Input, Pagination, ProgressBar, Radio, SegmentedControl, Select, SidebarNav, Spinner, StatCard, Switch, Table, Tabs, Tag, Textarea, Toast, Tooltip } from './chunk-RNOUXUBO.js';
17
+ import { boxed, LAYOUT_CSS, childLayoutFrom, CHILD_LAYOUT_PROPS } from './chunk-5WW4MBRO.js';
18
+ export { CHILD_LAYOUT_PROPS, LAYOUT_CSS, boxed, childLayoutFrom, columnsClass, containerLayoutStyle, joinClass, mergeStyle, typographyStyle } from './chunk-5WW4MBRO.js';
19
+ import React9, { createContext, useState, useMemo, useRef, useEffect, useContext, useCallback } from 'react';
22
20
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
23
21
 
24
22
  // src/asset-url.ts
@@ -115,6 +113,22 @@ function resolveAssetsIn(propName, value, resolve, component) {
115
113
  return out ?? record;
116
114
  }
117
115
  }
116
+ function SpecErrorCard({ children }) {
117
+ return /* @__PURE__ */ jsx(
118
+ "div",
119
+ {
120
+ role: "alert",
121
+ className: "wp-caption",
122
+ style: {
123
+ color: "var(--wp-text-danger)",
124
+ border: "1px dashed var(--wp-border-danger)",
125
+ borderRadius: "var(--wp-radius-md)",
126
+ padding: "var(--wp-space-3)"
127
+ },
128
+ children
129
+ }
130
+ );
131
+ }
118
132
  var NodeErrorBoundary = class extends React9.Component {
119
133
  state = { error: null };
120
134
  static getDerivedStateFromError(error) {
@@ -132,25 +146,12 @@ var NodeErrorBoundary = class extends React9.Component {
132
146
  }
133
147
  render() {
134
148
  if (this.state.error) {
135
- return /* @__PURE__ */ jsxs(
136
- "div",
137
- {
138
- role: "alert",
139
- className: "wp-caption",
140
- style: {
141
- color: "var(--wp-text-danger)",
142
- border: "1px dashed var(--wp-border-danger)",
143
- borderRadius: "var(--wp-radius-md)",
144
- padding: "var(--wp-space-3)"
145
- },
146
- children: [
147
- "[",
148
- this.props.name,
149
- "] \uB80C\uB354 \uC624\uB958: ",
150
- this.state.error.message
151
- ]
152
- }
153
- );
149
+ return /* @__PURE__ */ jsxs(SpecErrorCard, { children: [
150
+ "[",
151
+ this.props.name,
152
+ "] \uB80C\uB354 \uC624\uB958: ",
153
+ this.state.error.message
154
+ ] });
154
155
  }
155
156
  return this.props.children;
156
157
  }
@@ -165,17 +166,20 @@ function Can({ entity, action = "read", scope, fallback = null, children }) {
165
166
  }
166
167
  function imageRenderer(cell, key, width = cell.width ?? 72) {
167
168
  const height = cell.height ?? 52;
168
- return (row) => /* @__PURE__ */ jsx(
169
- ImageCellView,
170
- {
171
- url: typeof row[key] === "string" ? row[key] : "",
172
- alt: cell.altKey ? String(row[cell.altKey] ?? "") : "",
173
- width,
174
- height,
175
- fit: cell.fit ?? "cover",
176
- fallbackIcon: cell.fallbackIcon ?? "image"
177
- }
178
- );
169
+ return (row) => {
170
+ const url = fieldOf(row, key);
171
+ return /* @__PURE__ */ jsx(
172
+ ImageCellView,
173
+ {
174
+ url: typeof url === "string" ? url : "",
175
+ alt: cell.altKey ? String(fieldOf(row, cell.altKey) ?? "") : "",
176
+ width,
177
+ height,
178
+ fit: cell.fit ?? "cover",
179
+ fallbackIcon: cell.fallbackIcon ?? "image"
180
+ }
181
+ );
182
+ };
179
183
  }
180
184
  function ImageCellView({
181
185
  url,
@@ -229,7 +233,7 @@ function ImageCellView({
229
233
  /* @__PURE__ */ jsx(ImageViewer, { src: url, alt, open, onClose: () => setOpen(false) })
230
234
  ] });
231
235
  }
232
- function toNumber(value) {
236
+ function toNumber2(value) {
233
237
  if (typeof value === "number") return Number.isFinite(value) ? value : null;
234
238
  if (typeof value === "string") {
235
239
  const plain = value.replace(/,/g, "").trim();
@@ -240,7 +244,7 @@ function toNumber(value) {
240
244
  function fillTemplate(template, row) {
241
245
  return template.replace(
242
246
  /\{([\w.-]+)\}/g,
243
- (_match, key) => encodeURIComponent(String(row[key] ?? ""))
247
+ (_match, key) => encodeURIComponent(String(fieldOf(row, key) ?? ""))
244
248
  );
245
249
  }
246
250
  var pad2 = (n) => String(n).padStart(2, "0");
@@ -250,7 +254,9 @@ function formatDateCellValue(date) {
250
254
  return `${ymdOf(date)} ${hmOf(date)}`;
251
255
  }
252
256
  function formatDate(raw, format) {
253
- const date = new Date(raw);
257
+ const dateOnly = parseYmd(raw);
258
+ if (!dateOnly && YMD_RE.test(raw)) return raw;
259
+ const date = dateOnly ? new Date(dateOnly.getFullYear(), dateOnly.getMonth(), dateOnly.getDate()) : new Date(raw);
254
260
  if (Number.isNaN(date.getTime())) return raw;
255
261
  if (format === "datetime") return formatDateCellValue(date);
256
262
  if (format === "time") return hmOf(date);
@@ -261,19 +267,19 @@ function cellRenderer(cell, key, dispatch) {
261
267
  switch (cell.type) {
262
268
  case "badge":
263
269
  return (row) => {
264
- const value = String(row[key] ?? "");
270
+ const value = String(fieldOf(row, key) ?? "");
265
271
  if (!value) return null;
266
272
  return /* @__PURE__ */ jsx(Badge, { tone: cell.toneMap?.[value] ?? "neutral", children: cell.labelMap?.[value] ?? value });
267
273
  };
268
274
  case "tag":
269
275
  return (row) => {
270
- const value = String(row[key] ?? "");
276
+ const value = String(fieldOf(row, key) ?? "");
271
277
  if (!value) return null;
272
278
  return /* @__PURE__ */ jsx(Tag, { color: cell.colorMap?.[value], filled: cell.filled, children: value });
273
279
  };
274
280
  case "link":
275
281
  return (row) => {
276
- const label = String(row[cell.labelKey ?? key] ?? "");
282
+ const label = String(fieldOf(row, cell.labelKey ?? key) ?? "");
277
283
  if (!label) return null;
278
284
  const to = fillTemplate(cell.to ?? "", row);
279
285
  return (
@@ -299,14 +305,15 @@ function cellRenderer(cell, key, dispatch) {
299
305
  };
300
306
  case "progress":
301
307
  return (row) => {
302
- const value = toNumber(row[key]);
303
- if (value === null) return String(row[key] ?? "");
304
- const max = (cell.maxKey ? toNumber(row[cell.maxKey]) : null) ?? cell.max ?? 100;
308
+ const raw = fieldOf(row, key);
309
+ const value = toNumber2(raw);
310
+ if (value === null) return String(raw ?? "");
311
+ const max = (cell.maxKey ? toNumber2(fieldOf(row, cell.maxKey)) : null) ?? cell.max ?? 100;
305
312
  return /* @__PURE__ */ jsx(ProgressBar, { value, max: max || 100, tone: cell.tone, showValue: cell.showValue, size: "sm" });
306
313
  };
307
314
  case "icon":
308
315
  return (row) => {
309
- const value = String(row[key] ?? "");
316
+ const value = String(fieldOf(row, key) ?? "");
310
317
  const name = cell.iconMap?.[value] ?? cell.icon;
311
318
  if (!name) return value;
312
319
  return (
@@ -319,8 +326,9 @@ function cellRenderer(cell, key, dispatch) {
319
326
  };
320
327
  case "number":
321
328
  return (row) => {
322
- const value = toNumber(row[key]);
323
- if (value === null) return String(row[key] ?? "");
329
+ const raw = fieldOf(row, key);
330
+ const value = toNumber2(raw);
331
+ if (value === null) return String(raw ?? "");
324
332
  const digits = Number.isFinite(cell.digits) ? Math.max(0, Math.min(6, Number(cell.digits))) : 0;
325
333
  const text = new Intl.NumberFormat("ko-KR", {
326
334
  minimumFractionDigits: digits,
@@ -330,7 +338,7 @@ function cellRenderer(cell, key, dispatch) {
330
338
  };
331
339
  case "date":
332
340
  return (row) => {
333
- const raw = String(row[key] ?? "");
341
+ const raw = String(fieldOf(row, key) ?? "");
334
342
  return raw ? formatDate(raw, cell.format) : "";
335
343
  };
336
344
  case "image":
@@ -352,7 +360,10 @@ function toPage(body) {
352
360
  var defaultFetcher = async (endpoint, params, signal) => {
353
361
  const response = await fetch(searchUrl(endpoint, params), {
354
362
  headers: { accept: "application/json" },
355
- credentials: "include",
363
+ // same-origin — endpoint는 런타임 JSON(스펙)에서 오므로, 어댑터 없이 쓰는 앱에서
364
+ // 스펙이 외부 origin을 가리켜도 세션 쿠키가 제3자에게 나가면 안 된다.
365
+ // 쿠키를 실어야 하는 교차 출처 조회는 어댑터(fetch) 주입으로 자기 정책을 편다.
366
+ credentials: "same-origin",
356
367
  signal
357
368
  });
358
369
  const body = await response.json().catch(() => null);
@@ -374,6 +385,31 @@ function SduiSearchDataProvider({
374
385
  }) {
375
386
  return /* @__PURE__ */ jsx(SearchDataContext.Provider, { value: fetcher, children });
376
387
  }
388
+ function createSearchInvalidator() {
389
+ const listeners = /* @__PURE__ */ new Set();
390
+ return {
391
+ invalidate(prefix = "", exact = false) {
392
+ for (const entry of [...listeners]) {
393
+ const hit = prefix === "" ? true : exact ? entry.endpoint === prefix : entry.endpoint.startsWith(prefix);
394
+ if (hit) entry.reload();
395
+ }
396
+ },
397
+ subscribe(endpoint, reload) {
398
+ const entry = { endpoint, reload };
399
+ listeners.add(entry);
400
+ return () => {
401
+ listeners.delete(entry);
402
+ };
403
+ }
404
+ };
405
+ }
406
+ var SearchInvalidationContext = createContext(null);
407
+ function SduiSearchInvalidationProvider({
408
+ invalidator,
409
+ children
410
+ }) {
411
+ return /* @__PURE__ */ jsx(SearchInvalidationContext.Provider, { value: invalidator, children });
412
+ }
377
413
  function useSearchFetcher() {
378
414
  const injected = useContext(SearchDataContext);
379
415
  const adapter = useSduiAdapter();
@@ -386,10 +422,15 @@ function useSearchFetcher() {
386
422
  var EMPTY_PAGE = { data: [], total: 0 };
387
423
  function useRemoteSearchPage(endpoint, params) {
388
424
  const fetcher = useSearchFetcher();
425
+ const invalidator = useContext(SearchInvalidationContext);
389
426
  const [page, setPage] = useState(EMPTY_PAGE);
390
427
  const [isLoading, setIsLoading] = useState(true);
391
428
  const [error, setError] = useState(null);
392
429
  const [reloadKey, setReloadKey] = useState(0);
430
+ useEffect(() => {
431
+ if (!invalidator) return void 0;
432
+ return invalidator.subscribe(endpoint, () => setReloadKey((value) => value + 1));
433
+ }, [invalidator, endpoint]);
393
434
  const query = params.toString();
394
435
  const fetcherRef = useRef(fetcher);
395
436
  fetcherRef.current = fetcher;
@@ -494,7 +535,10 @@ function ResultTable({
494
535
  if (column.align === "right") cls.push("wp-table__num");
495
536
  if (column.mono) cls.push("wp-table__mono");
496
537
  if (column.strong) cls.push("wp-table__strong");
497
- return /* @__PURE__ */ jsx("td", { className: cls.join(" ") || void 0, children: column.render ? column.render(row, index) : row[column.key] }, column.key);
538
+ return /* @__PURE__ */ jsx("td", { className: cls.join(" ") || void 0, children: column.render ? column.render(row, index) : (
539
+ // 평면 키 우선, 없으면 점 경로 — 셀 렌더러(cells.tsx)와 같은 조회 규칙이다
540
+ fieldOf(row, column.key)
541
+ ) }, column.key);
498
542
  })
499
543
  },
500
544
  row.id ?? index
@@ -612,7 +656,7 @@ function SearchResults({
612
656
  }, [leftFilters, rightFilters, searchParams]);
613
657
  const hasAllRows = item.data.length >= item.total;
614
658
  const processed = useMemo(() => {
615
- const cell = (row, key) => row[key];
659
+ const cell = (row, key) => fieldOf(row, key);
616
660
  let rows = item.data;
617
661
  if (hasAllRows) {
618
662
  if (appliedSearch) {
@@ -894,13 +938,8 @@ function SearchResults({
894
938
  ] });
895
939
  }
896
940
  function numericValue(value) {
897
- if (typeof value === "number") return Number.isFinite(value) ? value : null;
898
941
  if (value instanceof Date) return value.getTime();
899
- if (typeof value === "string") {
900
- const plain = value.replace(/,/g, "").trim();
901
- if (plain !== "" && /^-?\d+(\.\d+)?$/.test(plain)) return Number(plain);
902
- }
903
- return null;
942
+ return toNumber2(value);
904
943
  }
905
944
  function formatCell(value) {
906
945
  if (value == null) return "";
@@ -1005,7 +1044,7 @@ function mapColumns(columns, dispatch) {
1005
1044
  }
1006
1045
  function matchesCondition(condition, row) {
1007
1046
  if (!condition || typeof condition.key !== "string") return true;
1008
- const value = row[condition.key];
1047
+ const value = fieldOf(row, condition.key);
1009
1048
  if (condition.equals !== void 0) return String(value ?? "") === String(condition.equals);
1010
1049
  if (Array.isArray(condition.in)) return condition.in.some((v) => String(v) === String(value ?? ""));
1011
1050
  if (condition.notEmpty === false) return true;
@@ -1017,7 +1056,7 @@ function mapCardRows(rows, dispatch) {
1017
1056
  return {
1018
1057
  icon: row.icon ? /* @__PURE__ */ jsx(Icon, { name: row.icon, size: 14 }) : void 0,
1019
1058
  label: row.label,
1020
- value: render ?? ((item) => String(item[row.valueKey] ?? "")),
1059
+ value: render ?? ((item) => String(fieldOf(item, row.valueKey) ?? "")),
1021
1060
  show: row.showWhen ? (item) => matchesCondition(row.showWhen, item) : void 0
1022
1061
  };
1023
1062
  });
@@ -1034,10 +1073,10 @@ function mapGridCard(gridCard, dispatch) {
1034
1073
  gridCard.imageKey,
1035
1074
  "100%"
1036
1075
  ) : void 0,
1037
- title: (row) => String(row[gridCard.titleKey] ?? ""),
1038
- subtitle: gridCard.subtitleKey ? (row) => String(row[gridCard.subtitleKey] ?? "") : void 0,
1076
+ title: (row) => String(fieldOf(row, gridCard.titleKey) ?? ""),
1077
+ subtitle: gridCard.subtitleKey ? (row) => String(fieldOf(row, gridCard.subtitleKey) ?? "") : void 0,
1039
1078
  badge: gridCard.badge ? (row) => {
1040
- const value = String(row[gridCard.badge.key] ?? "");
1079
+ const value = String(fieldOf(row, gridCard.badge.key) ?? "");
1041
1080
  if (!value) return null;
1042
1081
  return {
1043
1082
  text: gridCard.badge.labelMap?.[value] ?? value,
@@ -1047,7 +1086,7 @@ function mapGridCard(gridCard, dispatch) {
1047
1086
  rows: mapCardRows(gridCard.rows, dispatch),
1048
1087
  leftRows: mapCardRows(gridCard.leftRows, dispatch),
1049
1088
  rightRows: mapCardRows(gridCard.rightRows, dispatch),
1050
- note: gridCard.noteKey ? (row) => String(row[gridCard.noteKey] ?? "") || null : void 0,
1089
+ note: gridCard.noteKey ? (row) => String(fieldOf(row, gridCard.noteKey) ?? "") || null : void 0,
1051
1090
  actions: gridCard.actions?.length ? (row) => (
1052
1091
  // 카드 전체가 클릭 대상(rowClick)일 수 있으므로 버튼 클릭은 여기서 전파를 끊는다
1053
1092
  /* @__PURE__ */ jsx("span", { className: "wp-row", onClick: (e) => e.stopPropagation(), children: gridCard.actions.map((action) => /* @__PURE__ */ jsx(
@@ -1148,15 +1187,22 @@ function RemoteSearchResultsImpl({
1148
1187
  rowClick,
1149
1188
  headerActions,
1150
1189
  virtual,
1151
- pageSize = 10
1190
+ pageSize,
1191
+ searchParams: controlledParams,
1192
+ onParamsChange
1152
1193
  }) {
1153
1194
  const dispatch = useSduiDispatch();
1154
- const [params, setParams] = useState(() => {
1195
+ const defaultPageSize = normalizePageSizes(pageSizes)?.[0] ?? 10;
1196
+ const [localParams, setLocalParams] = useState(() => {
1155
1197
  const initial = new URLSearchParams();
1156
- initial.set("limit", String(pageSize));
1198
+ initial.set("limit", String(pageSize ?? defaultPageSize));
1157
1199
  return initial;
1158
1200
  });
1159
- const defaultPageSize = normalizePageSizes(pageSizes)?.[0] ?? 10;
1201
+ const params = controlledParams ?? localParams;
1202
+ const handleParamsChange = (next) => {
1203
+ if (controlledParams === void 0) setLocalParams(next);
1204
+ onParamsChange?.(next);
1205
+ };
1160
1206
  const query = useMemo(() => {
1161
1207
  const next = new URLSearchParams(params);
1162
1208
  if (!next.has("page")) next.set("page", "1");
@@ -1197,7 +1243,7 @@ function RemoteSearchResultsImpl({
1197
1243
  isLoading,
1198
1244
  onRowClick: rowClick ? (row) => toList(rowClick).forEach((a) => dispatch(a, row)) : void 0,
1199
1245
  searchParams: params,
1200
- onParamsChange: setParams
1246
+ onParamsChange: handleParamsChange
1201
1247
  }
1202
1248
  );
1203
1249
  }
@@ -1209,6 +1255,16 @@ function useSduiRuntime() {
1209
1255
  if (!runtime) throw new Error("useSduiRuntime\uC740 SduiRenderer \uB0B4\uBD80\uC5D0\uC11C\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
1210
1256
  return runtime;
1211
1257
  }
1258
+ function prefillParams(search, hash) {
1259
+ const params = new URLSearchParams(search);
1260
+ const q = hash.indexOf("?");
1261
+ if (q !== -1) {
1262
+ new URLSearchParams(hash.slice(q + 1)).forEach((value, key) => {
1263
+ if (!params.has(key)) params.append(key, value);
1264
+ });
1265
+ }
1266
+ return params;
1267
+ }
1212
1268
  function SduiForm({
1213
1269
  id,
1214
1270
  initial,
@@ -1240,7 +1296,7 @@ function SduiForm({
1240
1296
  if (runtime.state[`${id}.${field}`] === void 0) entries[`${id}.${field}`] = value;
1241
1297
  }
1242
1298
  if (initialFromUrl && typeof window !== "undefined") {
1243
- const params = new URLSearchParams(window.location.search);
1299
+ const params = prefillParams(window.location.search, window.location.hash);
1244
1300
  for (const [field, param] of Object.entries(initialFromUrl)) {
1245
1301
  const raw = params.get(param);
1246
1302
  if (raw !== null && runtime.state[`${id}.${field}`] === void 0) {
@@ -1551,8 +1607,6 @@ var SORTABLE_CSS = `
1551
1607
  .wp-sortable__status{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
1552
1608
  overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
1553
1609
  `;
1554
-
1555
- // src/registry.tsx
1556
1610
  var defaultRegistry = {
1557
1611
  // 기본 컴포넌트
1558
1612
  Alert,
@@ -1619,399 +1673,39 @@ var defaultRegistry = {
1619
1673
  function createRegistry(extensions) {
1620
1674
  return { ...defaultRegistry, ...extensions };
1621
1675
  }
1622
-
1623
- // src/calc.ts
1624
- function isPlainObject(v) {
1625
- return typeof v === "object" && v !== null && !Array.isArray(v);
1626
- }
1627
- function toNumber2(v) {
1628
- if (typeof v === "number") return v;
1629
- if (typeof v === "string") {
1630
- const n = Number(v.replace(/,/g, ""));
1631
- return Number.isFinite(n) ? n : 0;
1632
- }
1633
- return 0;
1634
- }
1635
- function isNumericText(v) {
1636
- if (typeof v === "number") return true;
1637
- if (typeof v !== "string" || v === "") return false;
1638
- return Number.isFinite(Number(v.replace(/,/g, "")));
1639
- }
1640
- function orderingCompare(a, b) {
1641
- if (typeof a === "string" && typeof b === "string" && !isNumericText(a) && !isNumericText(b)) {
1642
- return a < b ? -1 : a > b ? 1 : 0;
1643
- }
1644
- const x = toNumber2(a);
1645
- const y = toNumber2(b);
1646
- return x < y ? -1 : x > y ? 1 : 0;
1647
- }
1648
- function looseEquals(a, b) {
1649
- return String(a ?? "") === String(b ?? "");
1650
- }
1651
- function isBlank(v) {
1652
- return v === null || v === void 0 || v === "";
1653
- }
1654
- function isEmptyValue(v) {
1655
- if (isBlank(v)) return true;
1656
- if (Array.isArray(v)) return v.length === 0;
1657
- if (isPlainObject(v)) return Object.keys(v).length === 0;
1658
- return false;
1659
- }
1660
- function fieldOf(row, field) {
1661
- if (!field) return row;
1662
- if (!isPlainObject(row)) return void 0;
1663
- if (Object.hasOwn(row, field)) return row[field];
1664
- if (!field.includes(".")) return void 0;
1665
- let current = row;
1666
- for (const segment of field.split(".")) {
1667
- if (!isPlainObject(current) || !Object.hasOwn(current, segment)) return void 0;
1668
- current = current[segment];
1669
- }
1670
- return current;
1671
- }
1672
- var FIELD_OPS = /* @__PURE__ */ new Set([
1673
- "eq",
1674
- "ne",
1675
- "gt",
1676
- "gte",
1677
- "lt",
1678
- "lte",
1679
- "in",
1680
- "notIn",
1681
- "contains",
1682
- "startsWith",
1683
- "endsWith",
1684
- "isNull",
1685
- "isEmpty",
1686
- "intersects"
1687
- ]);
1688
- function isFieldOpObject(v) {
1689
- return isPlainObject(v) && Object.keys(v).some((key) => FIELD_OPS.has(key));
1690
- }
1691
- function matchesFieldOps(actual, ops, resolve) {
1692
- return Object.entries(ops).every(([op, rawExpected]) => {
1693
- if (!FIELD_OPS.has(op)) return true;
1694
- const expected = resolve(rawExpected);
1695
- switch (op) {
1696
- case "eq":
1697
- return looseEquals(actual, expected);
1698
- case "ne":
1699
- return !looseEquals(actual, expected);
1700
- case "gt":
1701
- return orderingCompare(actual, expected) > 0;
1702
- case "gte":
1703
- return orderingCompare(actual, expected) >= 0;
1704
- case "lt":
1705
- return orderingCompare(actual, expected) < 0;
1706
- case "lte":
1707
- return orderingCompare(actual, expected) <= 0;
1708
- case "in":
1709
- return Array.isArray(expected) && expected.some((item) => looseEquals(actual, item));
1710
- case "notIn":
1711
- return !Array.isArray(expected) || !expected.some((item) => looseEquals(actual, item));
1712
- case "contains":
1713
- return String(actual ?? "").includes(String(expected ?? ""));
1714
- case "startsWith":
1715
- return String(actual ?? "").startsWith(String(expected ?? ""));
1716
- case "endsWith":
1717
- return String(actual ?? "").endsWith(String(expected ?? ""));
1718
- case "isNull":
1719
- return isBlank(actual) === Boolean(expected);
1720
- case "intersects": {
1721
- const mine = Array.isArray(actual) ? actual : [];
1722
- const theirs = Array.isArray(expected) ? expected : [];
1723
- if (mine.length === 0 || theirs.length === 0) return false;
1724
- const set = new Set(theirs.map((item) => String(item ?? "")));
1725
- return mine.some((item) => set.has(String(item ?? "")));
1726
- }
1727
- case "isEmpty":
1728
- return isEmptyValue(actual) === Boolean(expected);
1729
- default:
1730
- return true;
1731
- }
1732
- });
1733
- }
1734
- function matchesWhere(row, where, resolve) {
1735
- if (!isPlainObject(where)) return true;
1736
- return Object.entries(where).every(([key, condition]) => {
1737
- if (key === "AND") {
1738
- return Array.isArray(condition) && condition.every((sub) => matchesWhere(row, sub, resolve));
1739
- }
1740
- if (key === "OR") {
1741
- return Array.isArray(condition) && condition.some((sub) => matchesWhere(row, sub, resolve));
1742
- }
1743
- if (key === "NOT") {
1744
- return !matchesWhere(row, condition, resolve);
1745
- }
1746
- const actual = fieldOf(row, key);
1747
- if (isFieldOpObject(condition)) return matchesFieldOps(actual, condition, resolve);
1748
- return looseEquals(actual, resolve(condition));
1749
- });
1750
- }
1751
- var CALC_OPS = /* @__PURE__ */ new Set([
1752
- // 산술 (기존)
1753
- "sum",
1754
- "sub",
1755
- "mul",
1756
- "div",
1757
- "percent",
1758
- "count",
1759
- // 문자 (기존)
1760
- "concat",
1761
- "encode",
1762
- // 비교·논리 (기존)
1763
- "eq",
1764
- "gt",
1765
- "gte",
1766
- "lt",
1767
- "lte",
1768
- "not",
1769
- // 논리 (신규) — 예전에는 스펙이 eq(concat(eq(a),eq(b)),"falsefalse")로 우회했다
1770
- "and",
1771
- "or",
1772
- // 값 고르기 (신규)
1773
- "if",
1774
- "match",
1775
- "coalesce",
1776
- // 목록·객체 (신규)
1777
- "get",
1778
- "pluck",
1779
- "filter",
1780
- "some",
1781
- "every",
1782
- "includes",
1783
- "distinct",
1784
- "except",
1785
- "groupBy",
1786
- "groupCount",
1787
- "entries",
1788
- "fromEntries",
1789
- "length",
1790
- "join",
1791
- "split",
1792
- "sortBy",
1793
- "map"
1794
- ]);
1795
- function isFieldRef(value) {
1796
- return isPlainObject(value) && "$field" in value;
1797
- }
1798
- function substituteFields(template, row) {
1799
- if (isFieldRef(template)) {
1800
- return template.$field === true ? row : fieldOf(row, String(template.$field)) ?? null;
1801
- }
1802
- if (Array.isArray(template)) return template.map((item) => substituteFields(item, row));
1803
- if (isPlainObject(template)) {
1804
- const nested = isPlainObject(template.$calc) ? template.$calc : null;
1805
- if (nested && nested.op === "map") {
1806
- const inner = {};
1807
- for (const [key, value] of Object.entries(nested)) {
1808
- inner[key] = key === "select" ? value : substituteFields(value, row);
1809
- }
1810
- return { ...template, $calc: inner };
1811
- }
1812
- const out = {};
1813
- for (const [key, value] of Object.entries(template)) out[key] = substituteFields(value, row);
1814
- return out;
1815
- }
1816
- return template;
1817
- }
1818
- function evaluateCalc(calc, resolved, resolve) {
1819
- const op = typeof calc.op === "string" ? calc.op : "";
1820
- const field = typeof calc.field === "string" ? calc.field : void 0;
1821
- const first = resolved[0];
1822
- const list = Array.isArray(first) ? first : void 0;
1823
- const listOrEmpty = () => {
1824
- if (list) return list;
1825
- if (first !== void 0 && first !== null) {
1826
- warnOnce(
1827
- `calc:${op}:list`,
1828
- `[sdui] $calc(${op}): \uCCAB \uC778\uC790\uAC00 \uBC30\uC5F4\uC774 \uC544\uB2D9\uB2C8\uB2E4 \u2014 \uBE48 \uBAA9\uB85D\uC73C\uB85C \uCC98\uB9AC\uD569\uB2C8\uB2E4`
1829
- );
1830
- }
1831
- return [];
1832
- };
1833
- const nums = (v) => {
1834
- if (Array.isArray(v))
1835
- return v.map((row) => field && isPlainObject(row) ? toNumber2(row[field]) : toNumber2(row));
1836
- return [toNumber2(v)];
1837
- };
1838
- switch (op) {
1839
- /* ── 산술 ── */
1840
- case "sum":
1841
- return resolved.flatMap(nums).reduce((a, b) => a + b, 0);
1842
- case "count":
1843
- return resolved.reduce(
1844
- (a, v) => a + (Array.isArray(v) ? v.length : v == null ? 0 : 1),
1845
- 0
1846
- );
1847
- case "sub": {
1848
- const items = resolved.map((v) => toNumber2(v));
1849
- return items.length === 0 ? 0 : items.reduce((a, b) => a - b);
1850
- }
1851
- case "mul":
1852
- return resolved.map((v) => toNumber2(v)).reduce((a, b) => a * b, 1);
1853
- case "div": {
1854
- const [a, b] = resolved.map((v) => toNumber2(v));
1855
- return b === 0 ? 0 : a / b;
1856
- }
1857
- case "percent": {
1858
- const [a, b] = resolved.map((v) => toNumber2(v));
1859
- return b === 0 ? 0 : Math.round(a / b * 1e3) / 10;
1860
- }
1861
- /* ── 문자 ── */
1862
- case "concat":
1863
- return resolved.map((v) => v == null ? "" : String(v)).join("");
1864
- case "encode":
1865
- return encodeURIComponent(resolved.map((v) => v == null ? "" : String(v)).join(""));
1866
- /* ── 비교·논리 ── */
1867
- case "eq":
1868
- return looseEquals(resolved[0], resolved[1]);
1869
- case "gt":
1870
- return orderingCompare(resolved[0], resolved[1]) > 0;
1871
- case "gte":
1872
- return orderingCompare(resolved[0], resolved[1]) >= 0;
1873
- case "lt":
1874
- return orderingCompare(resolved[0], resolved[1]) < 0;
1875
- case "lte":
1876
- return orderingCompare(resolved[0], resolved[1]) <= 0;
1877
- case "not":
1878
- return !resolved[0];
1879
- case "and":
1880
- return resolved.every(Boolean);
1881
- case "or":
1882
- return resolved.some(Boolean);
1883
- /* ── 값 고르기 ── */
1884
- case "if":
1885
- return resolved[0] ? resolve(calc.then ?? null) : resolve(calc.else ?? null);
1886
- case "match": {
1887
- const key = String(resolved[0] ?? "");
1888
- const cases = isPlainObject(calc.cases) ? calc.cases : {};
1889
- if (Object.hasOwn(cases, key)) return resolve(cases[key]);
1890
- return resolve(calc.default ?? null);
1891
- }
1892
- case "coalesce":
1893
- return resolved.find((v) => !isBlank(v)) ?? null;
1894
- /* ── 목록·객체 ── */
1895
- case "get": {
1896
- const [container, rawKey] = resolved;
1897
- if (Array.isArray(container)) {
1898
- if (typeof rawKey !== "number" && (typeof rawKey !== "string" || rawKey.trim() === "")) {
1899
- return null;
1900
- }
1901
- const index = Number(rawKey);
1902
- if (!Number.isInteger(index) || index < 0 || index >= container.length) return null;
1903
- return container[index] ?? null;
1904
- }
1905
- if (isPlainObject(container)) {
1906
- const key = rawKey == null ? "" : String(rawKey);
1907
- return Object.hasOwn(container, key) ? container[key] ?? null : null;
1908
- }
1909
- return null;
1910
- }
1911
- case "pluck":
1912
- return listOrEmpty().map((row) => fieldOf(row, field)).filter((v) => v !== null && v !== void 0);
1913
- case "filter":
1914
- return listOrEmpty().filter((row) => matchesWhere(row, calc.where, resolve));
1915
- case "some":
1916
- return listOrEmpty().some((row) => matchesWhere(row, calc.where, resolve));
1917
- case "every":
1918
- return listOrEmpty().every((row) => matchesWhere(row, calc.where, resolve));
1919
- case "includes": {
1920
- const needle = resolved[1];
1921
- if (typeof first === "string") return first.includes(String(needle ?? ""));
1922
- return listOrEmpty().some((row) => looseEquals(fieldOf(row, field), needle));
1923
- }
1924
- case "distinct": {
1925
- const seen = /* @__PURE__ */ new Set();
1926
- return listOrEmpty().filter((row) => {
1927
- const key = String(fieldOf(row, field) ?? "");
1928
- if (seen.has(key)) return false;
1929
- seen.add(key);
1930
- return true;
1931
- });
1932
- }
1933
- case "except": {
1934
- const remove = new Set(
1935
- (Array.isArray(resolved[1]) ? resolved[1] : []).map(
1936
- (row) => String(fieldOf(row, field) ?? "")
1937
- )
1938
- );
1939
- return listOrEmpty().filter(
1940
- (row) => !remove.has(String(fieldOf(row, field) ?? ""))
1941
- );
1942
- }
1943
- case "groupBy":
1944
- case "groupCount": {
1945
- const groups = /* @__PURE__ */ new Map();
1946
- for (const row of listOrEmpty()) {
1947
- const key = String(fieldOf(row, field) ?? "");
1948
- const bucket = groups.get(key);
1949
- if (bucket) bucket.push(row);
1950
- else groups.set(key, [row]);
1951
- }
1952
- const out = [...groups.entries()].map(
1953
- ([key, items]) => op === "groupCount" ? { key, count: items.length } : { key, items }
1954
- );
1955
- return out;
1956
- }
1957
- case "entries": {
1958
- if (!isPlainObject(first)) return [];
1959
- return Object.entries(first).map(([key, value]) => ({
1960
- key,
1961
- value
1962
- }));
1963
- }
1964
- case "fromEntries": {
1965
- const out = /* @__PURE__ */ Object.create(null);
1966
- for (const row of listOrEmpty()) {
1967
- if (!isPlainObject(row)) continue;
1968
- const key = row.key;
1969
- if (key === null || key === void 0 || key === "") continue;
1970
- out[String(key)] = row.value ?? null;
1971
- }
1972
- return { ...out };
1973
- }
1974
- case "length":
1975
- if (Array.isArray(first)) return first.length;
1976
- if (typeof first === "string") return first.length;
1977
- return 0;
1978
- case "join": {
1979
- const sep = typeof calc.sep === "string" ? calc.sep : ",";
1980
- return listOrEmpty().map((row) => {
1981
- const value = fieldOf(row, field);
1982
- return value == null ? "" : String(value);
1983
- }).join(sep);
1984
- }
1985
- case "split": {
1986
- const sep = typeof calc.sep === "string" ? calc.sep : ",";
1987
- let parts = String(first ?? "").split(sep);
1988
- if (calc.trim === true) parts = parts.map((part) => part.trim());
1989
- if (calc.take === void 0) return parts;
1990
- const index = toNumber2(calc.take);
1991
- const at = index < 0 ? parts.length + index : index;
1992
- return parts[at] ?? null;
1993
- }
1994
- case "map": {
1995
- const template = calc.select;
1996
- if (template === void 0) return listOrEmpty();
1997
- return listOrEmpty().map((row) => resolve(substituteFields(template, row)));
1998
- }
1999
- case "sortBy": {
2000
- const desc = Boolean(calc.desc);
2001
- return [...listOrEmpty()].sort((a, b) => {
2002
- const order = orderingCompare(fieldOf(a, field), fieldOf(b, field));
2003
- return desc ? -order : order;
2004
- });
2005
- }
2006
- default:
2007
- return null;
1676
+ function withProps(Component, defaults) {
1677
+ function WithProps(props) {
1678
+ const merged = { ...defaults, ...props };
1679
+ return /* @__PURE__ */ jsx(Component, { ...merged });
2008
1680
  }
1681
+ const label = Component;
1682
+ WithProps.displayName = `withProps(${label.displayName ?? label.name ?? "Component"})`;
1683
+ return WithProps;
2009
1684
  }
2010
1685
 
2011
1686
  // src/request-action.ts
2012
1687
  function deferChainEntry(container, key) {
2013
1688
  return typeof container.action === "string" && (key === "onSuccess" || key === "onError");
2014
1689
  }
1690
+ function failureDetailOf(error) {
1691
+ const message = error instanceof Error ? error.message : "\uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4";
1692
+ const carrier = error;
1693
+ const detail = { message };
1694
+ if (typeof carrier?.status === "number") detail.status = carrier.status;
1695
+ if (carrier?.body !== void 0) detail.body = carrier.body;
1696
+ return detail;
1697
+ }
1698
+ function runSuccessChain(kind, id, run) {
1699
+ try {
1700
+ run();
1701
+ } catch (chainError) {
1702
+ const detail = chainError instanceof Error ? chainError.message : String(chainError);
1703
+ warnOnce(
1704
+ `${kind}:chain:${id}`,
1705
+ `[sdui] ${kind}(${id}) \uC131\uACF5 \uD6C4 onSuccess \uCCB4\uC778 \uC2E4\uD589 \uC911 \uC624\uB958: ${detail}`
1706
+ );
1707
+ }
1708
+ }
2015
1709
  async function runRequestAction(action, deps) {
2016
1710
  const pendingKey = typeof action.pendingKey === "string" && action.pendingKey !== "" ? action.pendingKey : null;
2017
1711
  let pendingOn = false;
@@ -2027,27 +1721,25 @@ async function runRequestAction(action, deps) {
2027
1721
  method: action.method ?? "POST",
2028
1722
  ...action.body === void 0 ? {} : { body: action.body }
2029
1723
  });
2030
- if (typeof action.invalidate === "string" && action.invalidate !== "") {
2031
- deps.dispatch({ action: "invalidate", prefix: action.invalidate });
2032
- }
2033
- (action.onSuccess ?? []).forEach((a) => deps.dispatch(a, response));
1724
+ runSuccessChain("request", action.endpoint, () => {
1725
+ if (typeof action.invalidate === "string" && action.invalidate !== "") {
1726
+ deps.dispatch({ action: "invalidate", prefix: action.invalidate });
1727
+ }
1728
+ (action.onSuccess ?? []).forEach((a) => deps.dispatch(a, response));
1729
+ });
2034
1730
  } catch (error) {
2035
- const message = error instanceof Error ? error.message : "\uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4";
1731
+ const failure = failureDetailOf(error);
2036
1732
  if (typeof action.errorKey === "string" && action.errorKey !== "") {
2037
- const detail = error;
2038
- const value = { message };
2039
- if (typeof detail?.status === "number") value.status = detail.status;
2040
- if (detail?.body !== void 0) value.body = detail.body;
2041
1733
  deps.dispatch({
2042
1734
  action: "setState",
2043
1735
  key: action.errorKey,
2044
- value
1736
+ value: failure
2045
1737
  });
2046
1738
  }
2047
1739
  if (action.onError?.length) {
2048
- action.onError.forEach((a) => deps.dispatch(a, message));
1740
+ action.onError.forEach((a) => deps.dispatch(a, failure.message));
2049
1741
  } else {
2050
- deps.toast({ tone: "danger", title: "\uC694\uCCAD \uC2E4\uD328", message });
1742
+ deps.toast({ tone: "danger", title: "\uC694\uCCAD \uC2E4\uD328", message: failure.message });
2051
1743
  }
2052
1744
  } finally {
2053
1745
  if (pendingKey && pendingOn) deps.dispatch({ action: "setState", key: pendingKey, value: false });
@@ -2056,9 +1748,7 @@ async function runRequestAction(action, deps) {
2056
1748
 
2057
1749
  // src/state-path.ts
2058
1750
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2059
- function isPlainObject2(value) {
2060
- return typeof value === "object" && value !== null && !Array.isArray(value);
2061
- }
1751
+ var isPlainObject2 = (value) => isPlainObject(value);
2062
1752
  function normalizeSegment(raw) {
2063
1753
  if (typeof raw === "number") {
2064
1754
  return Number.isInteger(raw) && raw >= 0 ? raw : null;
@@ -2170,9 +1860,6 @@ var DENIED_PROPS = /* @__PURE__ */ new Set([
2170
1860
  "constructor",
2171
1861
  "prototype"
2172
1862
  ]);
2173
- function isPlainObject3(v) {
2174
- return typeof v === "object" && v !== null && !Array.isArray(v);
2175
- }
2176
1863
  function checkUrlValue(key, value, path, issues) {
2177
1864
  if (typeof value !== "string" || !URL_PROPS.has(key)) return;
2178
1865
  if (!isDangerousUrlValue(value)) return;
@@ -2180,7 +1867,7 @@ function checkUrlValue(key, value, path, issues) {
2180
1867
  }
2181
1868
  function validateSpec(page, knownComponents, slotNamesOf) {
2182
1869
  const issues = [];
2183
- if (!isPlainObject3(page)) return [{ path: "$", message: "\uC2A4\uD399\uC774 \uAC1D\uCCB4\uAC00 \uC544\uB2D9\uB2C8\uB2E4" }];
1870
+ if (!isPlainObject(page)) return [{ path: "$", message: "\uC2A4\uD399\uC774 \uAC1D\uCCB4\uAC00 \uC544\uB2D9\uB2C8\uB2E4" }];
2184
1871
  if (page.version !== 1) issues.push({ path: "$.version", message: "version\uC740 1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4" });
2185
1872
  validatePageData(page.data, issues);
2186
1873
  if (!Array.isArray(page.body)) {
@@ -2194,7 +1881,7 @@ function validateSpec(page, knownComponents, slotNamesOf) {
2194
1881
  }
2195
1882
  function validatePageData(data, issues) {
2196
1883
  if (data === void 0) return;
2197
- if (!isPlainObject3(data)) {
1884
+ if (!isPlainObject(data)) {
2198
1885
  issues.push({ path: "$.data", message: "data\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
2199
1886
  return;
2200
1887
  }
@@ -2204,7 +1891,7 @@ function validatePageData(data, issues) {
2204
1891
  issues.push({ path, message: "\uC0C1\uD0DC \uD0A4\uB294 \uBE44\uC5B4 \uC788\uC9C0 \uC54A\uC544\uC57C \uD558\uACE0 $\uB85C \uC2DC\uC791\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4" });
2205
1892
  continue;
2206
1893
  }
2207
- if (!isPlainObject3(raw)) {
1894
+ if (!isPlainObject(raw)) {
2208
1895
  issues.push({ path, message: "\uC870\uD68C\uB294 { endpoint, select?, when?, fallback? } \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
2209
1896
  continue;
2210
1897
  }
@@ -2216,7 +1903,7 @@ function validatePageData(data, issues) {
2216
1903
  } else if (!raw.endpoint.startsWith("/")) {
2217
1904
  issues.push({ path: `${path}.endpoint`, message: "endpoint\uB294 /\uB85C \uC2DC\uC791\uD558\uB294 \uACBD\uB85C\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
2218
1905
  }
2219
- } else if (!isPlainObject3(raw.endpoint)) {
1906
+ } else if (!isPlainObject(raw.endpoint)) {
2220
1907
  issues.push({ path: `${path}.endpoint`, message: "endpoint\uB294 \uBB38\uC790\uC5F4\uC774\uAC70\uB098 \uAC12 \uCC38\uC870\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
2221
1908
  } else {
2222
1909
  walkValue(raw.endpoint, `${path}.endpoint`, issues, false);
@@ -2239,7 +1926,7 @@ function validatePageData(data, issues) {
2239
1926
  }
2240
1927
  function walkChild(child, path, issues, known, slotNamesOf) {
2241
1928
  if (typeof child === "string") return;
2242
- if (!isPlainObject3(child)) {
1929
+ if (!isPlainObject(child)) {
2243
1930
  issues.push({ path, message: "children \uD56D\uBAA9\uC740 \uBB38\uC790\uC5F4 \uB610\uB294 \uB178\uB4DC \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
2244
1931
  return;
2245
1932
  }
@@ -2251,7 +1938,7 @@ function walkChild(child, path, issues, known, slotNamesOf) {
2251
1938
  if (known && !RENDERER_BUILTINS.has(node.component) && !known.has(node.component)) {
2252
1939
  issues.push({ path, message: `\uB4F1\uB85D\uB418\uC9C0 \uC54A\uC740 \uCEF4\uD3EC\uB10C\uD2B8: ${node.component}` });
2253
1940
  }
2254
- if (node.props !== void 0 && !isPlainObject3(node.props)) {
1941
+ if (node.props !== void 0 && !isPlainObject(node.props)) {
2255
1942
  issues.push({ path: `${path}.props`, message: "props\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
2256
1943
  }
2257
1944
  for (const [name, value] of Object.entries(node.props ?? {})) {
@@ -2265,9 +1952,9 @@ function walkChild(child, path, issues, known, slotNamesOf) {
2265
1952
  checkUrlValue(name, value, `${path}.props.${name}`, issues);
2266
1953
  walkValue(value, `${path}.props.${name}`, issues, name.startsWith("on") || name === "rowClick");
2267
1954
  }
2268
- if (node.component === "Form" && isPlainObject3(node.props) && node.props.rules !== void 0) {
1955
+ if (node.component === "Form" && isPlainObject(node.props) && node.props.rules !== void 0) {
2269
1956
  const rules = node.props.rules;
2270
- if (!isPlainObject3(rules)) {
1957
+ if (!isPlainObject(rules)) {
2271
1958
  issues.push({ path: `${path}.props.rules`, message: "rules\uB294 { \uD544\uB4DC: \uADDC\uCE59\uBC30\uC5F4 } \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
2272
1959
  } else {
2273
1960
  for (const [field, list] of Object.entries(rules)) {
@@ -2278,15 +1965,15 @@ function walkChild(child, path, issues, known, slotNamesOf) {
2278
1965
  }
2279
1966
  }
2280
1967
  if (node.component === "Fragment") {
2281
- const id = isPlainObject3(node.props) ? node.props.id : void 0;
2282
- if ((typeof id !== "string" || id === "") && !isPlainObject3(id)) {
1968
+ const id = isPlainObject(node.props) ? node.props.id : void 0;
1969
+ if ((typeof id !== "string" || id === "") && !isPlainObject(id)) {
2283
1970
  issues.push({
2284
1971
  path: `${path}.props.id`,
2285
1972
  message: "Fragment.id\uB294 \uBE44\uC5B4 \uC788\uC9C0 \uC54A\uC740 \uBB38\uC790\uC5F4(\uB610\uB294 \uAC12 \uCC38\uC870)\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4"
2286
1973
  });
2287
1974
  }
2288
- const args = isPlainObject3(node.props) ? node.props.args : void 0;
2289
- if (args !== void 0 && !isPlainObject3(args)) {
1975
+ const args = isPlainObject(node.props) ? node.props.args : void 0;
1976
+ if (args !== void 0 && !isPlainObject(args)) {
2290
1977
  issues.push({
2291
1978
  path: `${path}.props.args`,
2292
1979
  message: "Fragment.args\uB294 { \uC774\uB984: \uAC12 } \uD3C9\uBA74 \uC624\uBE0C\uC81D\uD2B8\uC5EC\uC57C \uD569\uB2C8\uB2E4"
@@ -2300,7 +1987,7 @@ function walkChild(child, path, issues, known, slotNamesOf) {
2300
1987
  }
2301
1988
  }
2302
1989
  if (node.slots !== void 0) {
2303
- if (!isPlainObject3(node.slots)) {
1990
+ if (!isPlainObject(node.slots)) {
2304
1991
  issues.push({ path: `${path}.slots`, message: "slots\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
2305
1992
  } else {
2306
1993
  const slotNames = slotNamesOf?.(node.component) ?? null;
@@ -2336,7 +2023,7 @@ function walkValue(value, path, issues, actionEligible) {
2336
2023
  value.forEach((v, i) => walkValue(v, `${path}[${i}]`, issues, actionEligible));
2337
2024
  return;
2338
2025
  }
2339
- if (!isPlainObject3(value)) return;
2026
+ if (!isPlainObject(value)) return;
2340
2027
  if (typeof value.action === "string" && (actionEligible || ACTION_NAMES.has(value.action))) {
2341
2028
  if (actionEligible && !ACTION_NAMES.has(value.action)) {
2342
2029
  issues.push({ path, message: `\uC54C \uC218 \uC5C6\uB294 \uC561\uC158: ${value.action}` });
@@ -2353,6 +2040,17 @@ function walkValue(value, path, issues, actionEligible) {
2353
2040
  }
2354
2041
  }
2355
2042
  }
2043
+ if (typeof value.endpoint === "string" && isDangerousUrlValue(value.endpoint)) {
2044
+ issues.push({ path: `${path}.endpoint`, message: `\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 endpoint: ${value.endpoint}` });
2045
+ }
2046
+ if (value.action === "navigate" && typeof value.to === "string" && isDangerousUrlValue(value.to)) {
2047
+ issues.push({ path: `${path}.to`, message: `\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 navigate \uB300\uC0C1: ${value.to}` });
2048
+ }
2049
+ for (const [k, v] of Object.entries(value)) {
2050
+ if (k === "action" || k === "onSuccess" || k === "onError") continue;
2051
+ checkUrlValue(k, v, `${path}.${k}`, issues);
2052
+ walkValue(v, `${path}.${k}`, issues, false);
2053
+ }
2356
2054
  return;
2357
2055
  }
2358
2056
  if ("$state" in value && typeof value.$state !== "string") {
@@ -2363,7 +2061,7 @@ function walkValue(value, path, issues, actionEligible) {
2363
2061
  }
2364
2062
  if ("$calc" in value) {
2365
2063
  const calc = value.$calc;
2366
- if (!isPlainObject3(calc) || typeof calc.op !== "string" || !CALC_OPS.has(calc.op)) {
2064
+ if (!isPlainObject(calc) || typeof calc.op !== "string" || !CALC_OPS.has(calc.op)) {
2367
2065
  issues.push({ path, message: "$calc.op\uC774 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4" });
2368
2066
  } else if (!Array.isArray(calc.args)) {
2369
2067
  issues.push({ path: `${path}.$calc.args`, message: "args\uB294 \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4" });
@@ -2392,12 +2090,9 @@ function walkValue(value, path, issues, actionEligible) {
2392
2090
  }
2393
2091
 
2394
2092
  // src/page-data.ts
2395
- function isPlainObject4(value) {
2396
- return typeof value === "object" && value !== null && !Array.isArray(value);
2397
- }
2398
2093
  function planPageData(data, resolve) {
2399
2094
  if (data === void 0) return [];
2400
- if (!isPlainObject4(data)) {
2095
+ if (!isPlainObject(data)) {
2401
2096
  warnOnce("page-data:root", "[sdui] page.data\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4 \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4");
2402
2097
  return [];
2403
2098
  }
@@ -2407,7 +2102,7 @@ function planPageData(data, resolve) {
2407
2102
  warnOnce(`page-data:${key}`, `[sdui] page.data\uC758 \uC0C1\uD0DC \uD0A4 "${key}"\uB294 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uAC74\uB108\uB701\uB2C8\uB2E4`);
2408
2103
  continue;
2409
2104
  }
2410
- if (!isPlainObject4(raw)) {
2105
+ if (!isPlainObject(raw)) {
2411
2106
  warnOnce(`page-data:${key}`, `[sdui] page.data.${key}\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4 \u2014 \uAC74\uB108\uB701\uB2C8\uB2E4`);
2412
2107
  continue;
2413
2108
  }
@@ -2433,6 +2128,9 @@ function planSignature(plans) {
2433
2128
  function runnablePlans(plans) {
2434
2129
  return plans.filter((plan) => plan.enabled && plan.endpoint !== "");
2435
2130
  }
2131
+ function staleLoadingKeys(plans, isLoading) {
2132
+ return plans.filter((plan) => (!plan.enabled || plan.endpoint === "") && isLoading(plan.key)).map((plan) => plan.key);
2133
+ }
2436
2134
  function invalidateHits(plans, prefix, exact) {
2437
2135
  return plans.filter(
2438
2136
  (plan) => prefix === "" ? true : exact ? plan.endpoint === prefix : plan.endpoint.startsWith(prefix)
@@ -2442,7 +2140,7 @@ function pickPath(source, path) {
2442
2140
  if (path === void 0 || path === "") return source;
2443
2141
  let value = source;
2444
2142
  for (const segment of path.split(".")) {
2445
- if (!isPlainObject4(value)) return void 0;
2143
+ if (!isPlainObject(value) || !Object.hasOwn(value, segment)) return void 0;
2446
2144
  value = value[segment];
2447
2145
  }
2448
2146
  return value;
@@ -2483,37 +2181,34 @@ function nearestArgsFrame(scope) {
2483
2181
  }
2484
2182
  return void 0;
2485
2183
  }
2486
- function isPlainObject5(v) {
2487
- return typeof v === "object" && v !== null && !Array.isArray(v);
2488
- }
2489
2184
  function isStateRef(v) {
2490
- return isPlainObject5(v) && typeof v.$state === "string";
2185
+ return isPlainObject(v) && typeof v.$state === "string";
2491
2186
  }
2492
2187
  function isItemRef(v) {
2493
- return isPlainObject5(v) && "$item" in v;
2188
+ return isPlainObject(v) && "$item" in v;
2494
2189
  }
2495
2190
  function isIndexRef(v) {
2496
- return isPlainObject5(v) && "$index" in v;
2191
+ return isPlainObject(v) && "$index" in v;
2497
2192
  }
2498
2193
  function isEventRef(v) {
2499
- return isPlainObject5(v) && "$event" in v;
2194
+ return isPlainObject(v) && "$event" in v;
2500
2195
  }
2501
2196
  function readEventPath(source, path) {
2502
2197
  let current = source;
2503
2198
  for (const segment of path.split(".")) {
2504
- if (!isPlainObject5(current) || !Object.hasOwn(current, segment)) return null;
2199
+ if (!isPlainObject(current) || !Object.hasOwn(current, segment)) return null;
2505
2200
  current = current[segment];
2506
2201
  }
2507
2202
  return current ?? null;
2508
2203
  }
2509
2204
  function isCalcRef(v) {
2510
- return isPlainObject5(v) && isPlainObject5(v.$calc);
2205
+ return isPlainObject(v) && isPlainObject(v.$calc);
2511
2206
  }
2512
2207
  function isArgRef(v) {
2513
- return isPlainObject5(v) && typeof v.$arg === "string";
2208
+ return isPlainObject(v) && typeof v.$arg === "string";
2514
2209
  }
2515
2210
  function isAction(v) {
2516
- return isPlainObject5(v) && typeof v.action === "string";
2211
+ return isPlainObject(v) && typeof v.action === "string";
2517
2212
  }
2518
2213
  function isActionList(v) {
2519
2214
  return Array.isArray(v) && v.length > 0 && v.every(isAction);
@@ -2533,11 +2228,11 @@ function expandActionEntries(raw, scope) {
2533
2228
  }
2534
2229
  if (isAction(raw)) return [{ action: raw, scope }];
2535
2230
  if (raw === void 0 || raw === null) return [];
2536
- warnOnce2(`action-entry:${typeof raw}`, "[sdui] \uC561\uC158 \uC790\uB9AC\uC5D0 \uC561\uC158\uC774 \uC544\uB2CC \uAC12\uC774 \uC788\uC2B5\uB2C8\uB2E4 \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4");
2231
+ warnOnce(`action-entry:${typeof raw}`, "[sdui] \uC561\uC158 \uC790\uB9AC\uC5D0 \uC561\uC158\uC774 \uC544\uB2CC \uAC12\uC774 \uC788\uC2B5\uB2C8\uB2E4 \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4");
2537
2232
  return [];
2538
2233
  }
2539
2234
  function eventValue(arg) {
2540
- if (isPlainObject5(arg) && "target" in arg && typeof arg.preventDefault === "function") {
2235
+ if (isPlainObject(arg) && "target" in arg && typeof arg.preventDefault === "function") {
2541
2236
  const target = arg.target;
2542
2237
  if (target.type === "checkbox") return target.checked;
2543
2238
  if (target.type === "number" || target.type === "range") {
@@ -2557,12 +2252,6 @@ function toIndex(v) {
2557
2252
  function inRange(index, length) {
2558
2253
  return index !== null && index >= 0 && index < length;
2559
2254
  }
2560
- var warned = /* @__PURE__ */ new Set();
2561
- function warnOnce2(key, message) {
2562
- if (warned.has(key)) return;
2563
- warned.add(key);
2564
- console.error(message);
2565
- }
2566
2255
  function validateForm(values, config, customValidators) {
2567
2256
  const errors = {};
2568
2257
  if (!config) return errors;
@@ -2586,9 +2275,9 @@ function ruleError(rule, value, values, label, customValidators) {
2586
2275
  case "required":
2587
2276
  return empty || value === false ? rule.message ?? `${label}\uC744(\uB97C) \uC785\uB825\uD558\uC138\uC694` : null;
2588
2277
  case "min":
2589
- return !empty && toNumber2(value) < rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value} \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4` : null;
2278
+ return !empty && toNumber(value) < rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value} \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4` : null;
2590
2279
  case "max":
2591
- return !empty && toNumber2(value) > rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value} \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4` : null;
2280
+ return !empty && toNumber(value) > rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value} \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4` : null;
2592
2281
  case "minLength":
2593
2282
  return !empty && String(value).length < rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value}\uC790 \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4` : null;
2594
2283
  case "maxLength":
@@ -2615,22 +2304,6 @@ function ruleError(rule, value, values, label, customValidators) {
2615
2304
  }
2616
2305
  }
2617
2306
  var EMPTY_SCOPE = [];
2618
- function SpecErrorCard({ children }) {
2619
- return /* @__PURE__ */ jsx(
2620
- "div",
2621
- {
2622
- role: "alert",
2623
- className: "wp-caption",
2624
- style: {
2625
- color: "var(--wp-text-danger)",
2626
- border: "1px dashed var(--wp-border-danger)",
2627
- borderRadius: "var(--wp-radius-md)",
2628
- padding: "var(--wp-space-3)"
2629
- },
2630
- children
2631
- }
2632
- );
2633
- }
2634
2307
  var DENIED_PROPS2 = /* @__PURE__ */ new Set([
2635
2308
  "dangerouslySetInnerHTML",
2636
2309
  "ref",
@@ -2642,13 +2315,13 @@ var DENIED_PROPS2 = /* @__PURE__ */ new Set([
2642
2315
  function childrenOf(node) {
2643
2316
  if (node.children === void 0) return void 0;
2644
2317
  if (Array.isArray(node.children)) return node.children;
2645
- warnOnce2(`children:${node.component}`, `[sdui] <${node.component}>.children\uC740 \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4`);
2318
+ warnOnce(`children:${node.component}`, `[sdui] <${node.component}>.children\uC740 \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4`);
2646
2319
  return typeof node.children === "string" ? [node.children] : void 0;
2647
2320
  }
2648
2321
  function renderChild(child, index, scope, engine) {
2649
2322
  if (typeof child === "string") return child;
2650
- if (!isPlainObject5(child)) {
2651
- warnOnce2(`child:${String(child)}`, `[sdui] \uC798\uBABB\uB41C \uB178\uB4DC(\uBB38\uC790\uC5F4 \uB610\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4): ${String(child)}`);
2323
+ if (!isPlainObject(child)) {
2324
+ warnOnce(`child:${String(child)}`, `[sdui] \uC798\uBABB\uB41C \uB178\uB4DC(\uBB38\uC790\uC5F4 \uB610\uB294 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4): ${String(child)}`);
2652
2325
  return /* @__PURE__ */ jsx(SpecErrorCard, { children: "[sdui] \uC798\uBABB\uB41C \uB178\uB4DC \u2014 \uBB38\uC790\uC5F4 \uB610\uB294 \uB178\uB4DC \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4" }, index);
2653
2326
  }
2654
2327
  const node = child;
@@ -2662,11 +2335,11 @@ function SduiNodeRenderer({
2662
2335
  engine
2663
2336
  }) {
2664
2337
  const { registry, resolveValue, dispatch, resolveAsset } = engine;
2665
- const slots = isPlainObject5(node.slots) ? node.slots : void 0;
2338
+ const slots = isPlainObject(node.slots) ? node.slots : void 0;
2666
2339
  const childList = childrenOf(node);
2667
2340
  if (node.component === "ForEach") {
2668
2341
  const source = resolveValue(
2669
- isPlainObject5(node.props) ? node.props.source ?? null : null,
2342
+ isPlainObject(node.props) ? node.props.source ?? null : null,
2670
2343
  scope
2671
2344
  );
2672
2345
  const rows = Array.isArray(source) ? source : [];
@@ -2676,7 +2349,7 @@ function SduiNodeRenderer({
2676
2349
  }
2677
2350
  return /* @__PURE__ */ jsx(Fragment, { children: rows.map((item, rowIndex) => {
2678
2351
  const rowScope = [...scope, { item, index: rowIndex }];
2679
- const rowKey = isPlainObject5(item) && typeof item.id === "string" ? item.id : rowIndex;
2352
+ const rowKey = isPlainObject(item) && typeof item.id === "string" ? item.id : rowIndex;
2680
2353
  return /* @__PURE__ */ jsx(React9.Fragment, { children: childList?.map((child, i) => renderChild(child, i, rowScope, engine)) }, rowKey);
2681
2354
  }) });
2682
2355
  }
@@ -2685,7 +2358,7 @@ function SduiNodeRenderer({
2685
2358
  }
2686
2359
  const Component = typeof node.component === "string" && Object.hasOwn(registry, node.component) ? registry[node.component] : void 0;
2687
2360
  if (!Component) {
2688
- warnOnce2(
2361
+ warnOnce(
2689
2362
  `component:${String(node.component)}`,
2690
2363
  `[sdui] \uB4F1\uB85D\uB418\uC9C0 \uC54A\uC740 \uCEF4\uD3EC\uB10C\uD2B8: ${String(node.component)} \u2014 \uC774 \uB178\uB4DC\uC640 \uC790\uC2DD\uC740 \uB80C\uB354\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4`
2691
2364
  );
@@ -2695,9 +2368,9 @@ function SduiNodeRenderer({
2695
2368
  ] });
2696
2369
  }
2697
2370
  const props = {};
2698
- for (const [name, raw] of Object.entries(isPlainObject5(node.props) ? node.props : {})) {
2371
+ for (const [name, raw] of Object.entries(isPlainObject(node.props) ? node.props : {})) {
2699
2372
  if (DENIED_PROPS2.has(name)) {
2700
- warnOnce2(`prop:${name}`, `[sdui] \uC2A4\uD399\uC5D0\uC11C \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD504\uB86D: ${name} \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
2373
+ warnOnce(`prop:${name}`, `[sdui] \uC2A4\uD399\uC5D0\uC11C \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD504\uB86D: ${name} \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
2701
2374
  continue;
2702
2375
  }
2703
2376
  if (name.startsWith("on") && (isAction(raw) || isArgRef(raw) || isActionEntryList(raw))) {
@@ -2715,7 +2388,7 @@ function SduiNodeRenderer({
2715
2388
  else if (name === "checked") resolved = false;
2716
2389
  }
2717
2390
  if (URL_PROPS.has(name) && typeof resolved === "string" && isDangerousUrlValue(resolved)) {
2718
- warnOnce2(`url:${name}:${resolved}`, `[sdui] \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 ${name} \uAC12: ${resolved} \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
2391
+ warnOnce(`url:${name}:${resolved}`, `[sdui] \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 ${name} \uAC12: ${resolved} \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
2719
2392
  continue;
2720
2393
  }
2721
2394
  if (resolveAsset) {
@@ -2730,7 +2403,7 @@ function SduiNodeRenderer({
2730
2403
  if (childLayout) {
2731
2404
  for (const name of CHILD_LAYOUT_PROPS) delete props[name];
2732
2405
  if (childLayout.style) {
2733
- props.style = isPlainObject5(props.style) ? { ...childLayout.style, ...props.style } : childLayout.style;
2406
+ props.style = isPlainObject(props.style) ? { ...childLayout.style, ...props.style } : childLayout.style;
2734
2407
  }
2735
2408
  if (childLayout.className) {
2736
2409
  props.className = typeof props.className === "string" && props.className ? `${childLayout.className} ${props.className}` : childLayout.className;
@@ -2738,7 +2411,7 @@ function SduiNodeRenderer({
2738
2411
  }
2739
2412
  for (const [name, slotChildren] of Object.entries(slots ?? {})) {
2740
2413
  if (!Array.isArray(slotChildren)) {
2741
- warnOnce2(
2414
+ warnOnce(
2742
2415
  `slot:${node.component}.${name}`,
2743
2416
  `[sdui] <${node.component}>.slots.${name}\uC740 \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4 \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`
2744
2417
  );
@@ -2754,33 +2427,33 @@ var FRAGMENT_MAX_DEPTH = 4;
2754
2427
  function SduiFragment({ node, scope, engine }) {
2755
2428
  const adapter = useSduiAdapter();
2756
2429
  const chain = React9.useContext(SduiFragmentChainContext);
2757
- const raw = isPlainObject5(node.props) ? node.props.id : void 0;
2430
+ const raw = isPlainObject(node.props) ? node.props.id : void 0;
2758
2431
  const id = engine.resolveValue(raw, scope);
2759
2432
  if (typeof id !== "string" || id === "") {
2760
- warnOnce2("fragment:id", "[sdui] <Fragment>\uC5D0 \uBB38\uC790\uC5F4 id\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uB80C\uB354\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
2433
+ warnOnce("fragment:id", "[sdui] <Fragment>\uC5D0 \uBB38\uC790\uC5F4 id\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uB80C\uB354\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
2761
2434
  return null;
2762
2435
  }
2763
2436
  if (chain.includes(id)) {
2764
- warnOnce2(
2437
+ warnOnce(
2765
2438
  `fragment:cycle:${id}`,
2766
2439
  `[sdui] Fragment \uC21C\uD658 \uCC38\uC870: ${[...chain, id].join(" \u2192 ")} \u2014 \uC7AC\uC9C4\uC785\uC744 \uAC70\uBD80\uD569\uB2C8\uB2E4`
2767
2440
  );
2768
2441
  return null;
2769
2442
  }
2770
2443
  if (chain.length >= FRAGMENT_MAX_DEPTH) {
2771
- warnOnce2(
2444
+ warnOnce(
2772
2445
  `fragment:depth:${id}`,
2773
2446
  `[sdui] Fragment \uC911\uCCA9\uC774 ${FRAGMENT_MAX_DEPTH}\uB2E8\uC744 \uB118\uC2B5\uB2C8\uB2E4(${[...chain, id].join(" \u2192 ")}) \u2014 \uB80C\uB354\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4`
2774
2447
  );
2775
2448
  return null;
2776
2449
  }
2777
- const rawArgs = isPlainObject5(node.props) ? node.props.args : void 0;
2778
- if (rawArgs !== void 0 && !isPlainObject5(rawArgs)) {
2779
- warnOnce2(`fragment:args:${id}`, `[sdui] <Fragment ${id}>\uC758 args\uB294 \uD3C9\uBA74 \uC624\uBE0C\uC81D\uD2B8\uC5EC\uC57C \uD569\uB2C8\uB2E4 \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
2450
+ const rawArgs = isPlainObject(node.props) ? node.props.args : void 0;
2451
+ if (rawArgs !== void 0 && !isPlainObject(rawArgs)) {
2452
+ warnOnce(`fragment:args:${id}`, `[sdui] <Fragment ${id}>\uC758 args\uB294 \uD3C9\uBA74 \uC624\uBE0C\uC81D\uD2B8\uC5EC\uC57C \uD569\uB2C8\uB2E4 \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
2780
2453
  }
2781
2454
  const useSpec = adapter?.useSpec;
2782
2455
  if (!useSpec) {
2783
- warnOnce2(
2456
+ warnOnce(
2784
2457
  "fragment:no-useSpec",
2785
2458
  "[sdui] Fragment\uB97C \uC4F0\uB824\uBA74 \uC5B4\uB311\uD130\uC5D0 useSpec(id)\uC774 \uD544\uC694\uD569\uB2C8\uB2E4 \u2014 \uB80C\uB354\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4"
2786
2459
  );
@@ -2790,7 +2463,7 @@ function SduiFragment({ node, scope, engine }) {
2790
2463
  SduiFragmentBody,
2791
2464
  {
2792
2465
  id,
2793
- args: isPlainObject5(rawArgs) ? rawArgs : void 0,
2466
+ args: isPlainObject(rawArgs) ? rawArgs : void 0,
2794
2467
  useSpec,
2795
2468
  chain,
2796
2469
  scope,
@@ -2811,19 +2484,19 @@ function SduiFragmentBody({
2811
2484
  const nextChain = React9.useMemo(() => [...chain, id], [chain, id]);
2812
2485
  const bodyScope = React9.useMemo(() => [...scope, { args, callSiteScope: scope }], [scope, args]);
2813
2486
  React9.useEffect(() => {
2814
- if (!DEV || !isPlainObject5(spec) || !Array.isArray(spec.body)) return;
2487
+ if (!DEV || !isPlainObject(spec) || !Array.isArray(spec.body)) return;
2815
2488
  validateSpec(spec, new Set(Object.keys(engine.registry))).forEach(
2816
2489
  (issue) => console.warn(`[sdui:spec:${id}] ${issue.path} \u2014 ${issue.message}`)
2817
2490
  );
2818
2491
  }, [spec, engine.registry, id]);
2819
2492
  if (spec === void 0) return null;
2820
- if (spec === null || !isPlainObject5(spec) || !Array.isArray(spec.body)) {
2821
- warnOnce2(`fragment:missing:${id}`, `[sdui] Fragment \uC2A4\uD399\uC744 \uD574\uC11D\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${id} \u2014 \uB80C\uB354\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4`);
2493
+ if (spec === null || !isPlainObject(spec) || !Array.isArray(spec.body)) {
2494
+ warnOnce(`fragment:missing:${id}`, `[sdui] Fragment \uC2A4\uD399\uC744 \uD574\uC11D\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${id} \u2014 \uB80C\uB354\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4`);
2822
2495
  return null;
2823
2496
  }
2824
2497
  const page = spec;
2825
2498
  if (page.data || page.urlState || page.subscribe) {
2826
- warnOnce2(
2499
+ warnOnce(
2827
2500
  `fragment:page-features:${id}`,
2828
2501
  `[sdui] Fragment \uC2A4\uD399 ${id}\uC758 data/urlState/subscribe\uB294 \uBB34\uC2DC\uB429\uB2C8\uB2E4 \u2014 \uC870\uAC01\uC740 body\uB9CC \uB80C\uB354\uD569\uB2C8\uB2E4`
2829
2502
  );
@@ -2843,6 +2516,8 @@ function SduiRenderer({
2843
2516
  const host = useMemo(() => resolveAdapter(adapter), [adapter]);
2844
2517
  const hostRef = useRef(host);
2845
2518
  hostRef.current = host;
2519
+ const invalidatePageDataRef = useRef(() => {
2520
+ });
2846
2521
  const [state, setState] = useState(() => ({ ...initialState ?? {} }));
2847
2522
  const urlSeeded = useRef(false);
2848
2523
  const [toasts, setToasts] = useState([]);
@@ -2912,21 +2587,25 @@ function SduiRenderer({
2912
2587
  }
2913
2588
  }, [state, page.urlState, syncUrlState]);
2914
2589
  const subscribeChannels = page.subscribe?.join(",") ?? "";
2590
+ const hasSubscribe = typeof host.subscribe === "function";
2915
2591
  useEffect(() => {
2916
- if (!subscribeChannels || !host.subscribe) return;
2592
+ if (!subscribeChannels || !hasSubscribe) return;
2593
+ const subscribe = hostRef.current.subscribe;
2594
+ if (!subscribe) return;
2917
2595
  let timer = null;
2918
- const unsubscribe = host.subscribe(subscribeChannels.split(","), () => {
2596
+ const unsubscribe = subscribe(subscribeChannels.split(","), () => {
2919
2597
  if (timer) clearTimeout(timer);
2920
2598
  timer = setTimeout(() => {
2921
- host.invalidate?.("", false);
2922
- host.refresh?.();
2599
+ hostRef.current.invalidate?.("", false);
2600
+ hostRef.current.refresh?.();
2601
+ invalidatePageDataRef.current("", false);
2923
2602
  }, 300);
2924
2603
  });
2925
2604
  return () => {
2926
2605
  if (timer) clearTimeout(timer);
2927
2606
  unsubscribe();
2928
2607
  };
2929
- }, [subscribeChannels, host]);
2608
+ }, [subscribeChannels, hasSubscribe]);
2930
2609
  const closeToast = useCallback((id) => {
2931
2610
  setToasts((prev) => prev.filter((t) => t.id !== id));
2932
2611
  }, []);
@@ -2940,14 +2619,14 @@ function SduiRenderer({
2940
2619
  );
2941
2620
  const lookupState = useCallback((key) => {
2942
2621
  const current = stateRef.current;
2943
- if (key in current) return current[key];
2622
+ if (Object.hasOwn(current, key)) return current[key];
2944
2623
  const segments = key.split(".");
2945
2624
  for (let cut = segments.length - 1; cut >= 1; cut--) {
2946
2625
  const head = segments.slice(0, cut).join(".");
2947
- if (head in current) {
2626
+ if (Object.hasOwn(current, head)) {
2948
2627
  let value = current[head];
2949
2628
  for (const segment of segments.slice(cut)) {
2950
- if (!isPlainObject5(value)) return void 0;
2629
+ if (!isPlainObject(value) || !Object.hasOwn(value, segment)) return void 0;
2951
2630
  value = value[segment];
2952
2631
  }
2953
2632
  return value;
@@ -2965,7 +2644,7 @@ function SduiRenderer({
2965
2644
  const frame = nearestItemFrame(scope);
2966
2645
  if (!frame) return null;
2967
2646
  if (value.$item === true) return frame.item;
2968
- return isPlainObject5(frame.item) ? frame.item[value.$item] ?? null : null;
2647
+ return isPlainObject(frame.item) && Object.hasOwn(frame.item, value.$item) ? frame.item[value.$item] ?? null : null;
2969
2648
  }
2970
2649
  if (isIndexRef(value)) {
2971
2650
  return nearestItemFrame(scope)?.index ?? null;
@@ -2987,14 +2666,14 @@ function SduiRenderer({
2987
2666
  const calc = value.$calc;
2988
2667
  const op = typeof calc.op === "string" ? calc.op : "";
2989
2668
  if (!Array.isArray(calc.args)) {
2990
- warnOnce2(`calc:${op}`, `[sdui] $calc(${op || "?"}): args\uB294 \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4 \u2014 \uAC12 \uC5C6\uC74C\uC73C\uB85C \uCC98\uB9AC\uD569\uB2C8\uB2E4`);
2669
+ warnOnce(`calc:${op}`, `[sdui] $calc(${op || "?"}): args\uB294 \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4 \u2014 \uAC12 \uC5C6\uC74C\uC73C\uB85C \uCC98\uB9AC\uD569\uB2C8\uB2E4`);
2991
2670
  return null;
2992
2671
  }
2993
2672
  const resolved = calc.args.map((arg) => resolveValue(arg, scope));
2994
2673
  return evaluateCalc(calc, resolved, (late) => resolveValue(late, scope));
2995
2674
  }
2996
2675
  if (Array.isArray(value)) return value.map((v) => resolveValue(v, scope));
2997
- if (isPlainObject5(value)) {
2676
+ if (isPlainObject(value)) {
2998
2677
  const out = {};
2999
2678
  for (const [k, v] of Object.entries(value)) {
3000
2679
  out[k] = deferChainEntry(value, k) ? v : resolveValue(v, scope);
@@ -3016,11 +2695,25 @@ function SduiRenderer({
3016
2695
  const [dataEpochs, setDataEpochs] = useState({});
3017
2696
  const dataEpochsRef = useRef(dataEpochs);
3018
2697
  dataEpochsRef.current = dataEpochs;
2698
+ invalidatePageDataRef.current = (prefix, exact) => {
2699
+ const hit = invalidateHits(runnablePlans(dataPlanRef.current), prefix, exact);
2700
+ if (hit.length === 0) return;
2701
+ setDataEpochs((prev) => {
2702
+ const next = { ...prev };
2703
+ for (const key of hit) next[key] = (next[key] ?? 0) + 1;
2704
+ return next;
2705
+ });
2706
+ };
3019
2707
  const fetchedRef = useRef(/* @__PURE__ */ new Map());
3020
2708
  const dataEpochKey = useMemo(() => JSON.stringify(dataEpochs), [dataEpochs]);
3021
2709
  useEffect(() => {
3022
2710
  const epochs = dataEpochsRef.current;
3023
- const runnable = runnablePlans(dataPlanRef.current).filter((plan) => {
2711
+ const plans = dataPlanRef.current;
2712
+ const stale = staleLoadingKeys(plans, (key) => stateRef.current[`${key}.$loading`] === true);
2713
+ if (stale.length > 0) {
2714
+ setEntries(Object.fromEntries(stale.map((key) => [`${key}.$loading`, false])));
2715
+ }
2716
+ const runnable = runnablePlans(plans).filter((plan) => {
3024
2717
  const stamp = planStamp(plan, epochs);
3025
2718
  if (fetchedRef.current.get(plan.key) === stamp) return false;
3026
2719
  fetchedRef.current.set(plan.key, stamp);
@@ -3049,7 +2742,7 @@ function SduiRenderer({
3049
2742
  if (cancelled) return;
3050
2743
  const picked = pickPath(response, plan.select);
3051
2744
  if (picked === void 0 && plan.select !== void 0 && response !== null) {
3052
- warnOnce2(
2745
+ warnOnce(
3053
2746
  `data-select:${plan.key}`,
3054
2747
  `[sdui] page.data.${plan.key}: \uC751\uB2F5\uC5D0 "${plan.select}" \uACBD\uB85C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uAC12 \uC5C6\uC74C\uC73C\uB85C \uB461\uB2C8\uB2E4 (${plan.endpoint})`
3055
2748
  );
@@ -3063,7 +2756,7 @@ function SduiRenderer({
3063
2756
  } catch (error) {
3064
2757
  if (cancelled || controller.signal.aborted) return;
3065
2758
  const message = error instanceof Error ? error.message : "\uC870\uD68C\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4";
3066
- warnOnce2(`data:${plan.key}:${plan.endpoint}`, `[sdui] page.data.${plan.key} \uC870\uD68C \uC2E4\uD328 (${plan.endpoint}): ${message}`);
2759
+ warnOnce(`data:${plan.key}:${plan.endpoint}`, `[sdui] page.data.${plan.key} \uC870\uD68C \uC2E4\uD328 (${plan.endpoint}): ${message}`);
3067
2760
  settled.add(plan.key);
3068
2761
  setEntries({
3069
2762
  [plan.key]: plan.fallback ?? void 0,
@@ -3098,7 +2791,7 @@ function SduiRenderer({
3098
2791
  switch (action.action) {
3099
2792
  case "navigate":
3100
2793
  if (!isSafeNavigationTarget(action.to)) {
3101
- warnOnce2(`navigate:${String(action.to)}`, `[sdui] \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 navigate \uB300\uC0C1: ${String(action.to)}`);
2794
+ warnOnce(`navigate:${String(action.to)}`, `[sdui] \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 navigate \uB300\uC0C1: ${String(action.to)}`);
3102
2795
  break;
3103
2796
  }
3104
2797
  hostRef.current.navigate(action.to);
@@ -3115,7 +2808,7 @@ function SduiRenderer({
3115
2808
  nextValue
3116
2809
  );
3117
2810
  if (!written.ok) {
3118
- warnOnce2(
2811
+ warnOnce(
3119
2812
  `setState:${action.key}:${written.reason}`,
3120
2813
  `[sdui] setState(${action.key}) \uACBD\uB85C \uC4F0\uAE30\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4 \u2014 ${written.reason}`
3121
2814
  );
@@ -3144,7 +2837,7 @@ function SduiRenderer({
3144
2837
  if (!Array.isArray(current)) break;
3145
2838
  const index = toIndex(action.index);
3146
2839
  if (!inRange(index, current.length)) {
3147
- warnOnce2(`arrayRemove:${action.key}`, `[sdui] arrayRemove(${action.key}): \uC798\uBABB\uB41C index \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
2840
+ warnOnce(`arrayRemove:${action.key}`, `[sdui] arrayRemove(${action.key}): \uC798\uBABB\uB41C index \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
3148
2841
  break;
3149
2842
  }
3150
2843
  setEntries({ [action.key]: current.filter((_, i) => i !== index) });
@@ -3153,7 +2846,7 @@ function SduiRenderer({
3153
2846
  case "arrayMove": {
3154
2847
  const current = stateRef.current[action.key];
3155
2848
  if (!Array.isArray(current)) {
3156
- warnOnce2(
2849
+ warnOnce(
3157
2850
  `arrayMove:key:${action.key}`,
3158
2851
  `[sdui] arrayMove(${action.key}): \uC0C1\uD0DC\uAC00 \uBC30\uC5F4\uC774 \uC544\uB2D9\uB2C8\uB2E4 \u2014 \uC911\uCCA9 \uACBD\uB85C\uC774\uAC70\uB098 default\uB85C\uB9CC \uADF8\uB824\uC9C4 \uBC30\uC5F4\uC785\uB2C8\uB2E4`
3159
2852
  );
@@ -3162,7 +2855,7 @@ function SduiRenderer({
3162
2855
  const from = toIndex(action.from);
3163
2856
  const rawTo = toIndex(action.to);
3164
2857
  if (!inRange(from, current.length) || rawTo === null) {
3165
- warnOnce2(`arrayMove:${action.key}`, `[sdui] arrayMove(${action.key}): \uC798\uBABB\uB41C from/to \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
2858
+ warnOnce(`arrayMove:${action.key}`, `[sdui] arrayMove(${action.key}): \uC798\uBABB\uB41C from/to \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
3166
2859
  break;
3167
2860
  }
3168
2861
  const to = Math.max(0, Math.min(current.length - 1, rawTo));
@@ -3178,11 +2871,11 @@ function SduiRenderer({
3178
2871
  if (!Array.isArray(current)) break;
3179
2872
  const index = toIndex(action.index);
3180
2873
  if (!inRange(index, current.length) || typeof action.field !== "string") {
3181
- warnOnce2(`arrayUpdate:${action.key}`, `[sdui] arrayUpdate(${action.key}): \uC798\uBABB\uB41C index/field \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
2874
+ warnOnce(`arrayUpdate:${action.key}`, `[sdui] arrayUpdate(${action.key}): \uC798\uBABB\uB41C index/field \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
3182
2875
  break;
3183
2876
  }
3184
2877
  const row = current[index];
3185
- if (!isPlainObject5(row)) break;
2878
+ if (!isPlainObject(row)) break;
3186
2879
  const nextValue = (action.fromEvent ? eventValue(arg) : action.value) ?? null;
3187
2880
  const next = [...current];
3188
2881
  next[index] = { ...row, [action.field]: nextValue };
@@ -3192,9 +2885,11 @@ function SduiRenderer({
3192
2885
  case "submitForm": {
3193
2886
  const formId = action.form;
3194
2887
  if (stateRef.current[`${formId}.$submitting`] === true) break;
2888
+ const pendingKey = typeof action.pendingKey === "string" && action.pendingKey !== "" ? action.pendingKey : null;
3195
2889
  void (async () => {
3196
2890
  setEntries({ [`${formId}.$submitting`]: true });
3197
2891
  let phase = "validate";
2892
+ let pendingOn = false;
3198
2893
  try {
3199
2894
  const prefix = `${formId}.`;
3200
2895
  const values = {};
@@ -3210,23 +2905,36 @@ function SduiRenderer({
3210
2905
  }
3211
2906
  setEntries({ [`${formId}.$errors`]: {}, [`${formId}.$submitting`]: true });
3212
2907
  phase = "submit";
2908
+ if (pendingKey) {
2909
+ pendingOn = true;
2910
+ setEntries({ [pendingKey]: true });
2911
+ }
3213
2912
  const response = await hostRef.current.fetch(action.endpoint, {
3214
2913
  method: action.method ?? "POST",
3215
2914
  body: values
3216
2915
  });
3217
2916
  setEntries({ [`${formId}.$submitting`]: false, [`${formId}.$resetDirty`]: true });
3218
- (action.onSuccess ?? []).forEach((a) => dispatchRef.current(a, response, scope));
2917
+ runSuccessChain("submitForm", formId, () => {
2918
+ (action.onSuccess ?? []).forEach((a) => dispatchRef.current(a, response, scope));
2919
+ });
3219
2920
  } catch (error) {
3220
2921
  setEntries({ [`${formId}.$submitting`]: false });
3221
- const message = error instanceof Error ? error.message : "\uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4";
2922
+ const failure = failureDetailOf(error);
3222
2923
  if (phase === "validate") {
3223
- warnOnce2(`validate:${formId}`, `[sdui] ${formId} \uAC80\uC99D \uADDC\uCE59 \uC624\uB958: ${message}`);
3224
- pushToast({ tone: "danger", title: "\uAC80\uC99D \uADDC\uCE59 \uC624\uB958", message });
3225
- } else if (action.onError?.length) {
3226
- action.onError.forEach((a) => dispatchRef.current(a, message, scope));
2924
+ warnOnce(`validate:${formId}`, `[sdui] ${formId} \uAC80\uC99D \uADDC\uCE59 \uC624\uB958: ${failure.message}`);
2925
+ pushToast({ tone: "danger", title: "\uAC80\uC99D \uADDC\uCE59 \uC624\uB958", message: failure.message });
3227
2926
  } else {
3228
- pushToast({ tone: "danger", title: "\uC800\uC7A5 \uC2E4\uD328", message });
2927
+ if (typeof action.errorKey === "string" && action.errorKey !== "") {
2928
+ setEntries({ [action.errorKey]: failure });
2929
+ }
2930
+ if (action.onError?.length) {
2931
+ action.onError.forEach((a) => dispatchRef.current(a, failure.message, scope));
2932
+ } else {
2933
+ pushToast({ tone: "danger", title: "\uC800\uC7A5 \uC2E4\uD328", message: failure.message });
2934
+ }
3229
2935
  }
2936
+ } finally {
2937
+ if (pendingKey && pendingOn) setEntries({ [pendingKey]: false });
3230
2938
  }
3231
2939
  })();
3232
2940
  break;
@@ -3243,18 +2951,12 @@ function SduiRenderer({
3243
2951
  case "invalidate": {
3244
2952
  hostRef.current.invalidate?.(action.prefix, action.exact ?? false);
3245
2953
  const prefix = typeof action.prefix === "string" ? action.prefix : "";
3246
- const hit = invalidateHits(runnablePlans(dataPlanRef.current), prefix, action.exact ?? false);
3247
- if (hit.length > 0) {
3248
- setDataEpochs((prev) => {
3249
- const next = { ...prev };
3250
- for (const key of hit) next[key] = (next[key] ?? 0) + 1;
3251
- return next;
3252
- });
3253
- }
2954
+ invalidatePageDataRef.current(prefix, action.exact ?? false);
3254
2955
  break;
3255
2956
  }
3256
2957
  case "refresh":
3257
2958
  hostRef.current.refresh?.();
2959
+ invalidatePageDataRef.current("", false);
3258
2960
  break;
3259
2961
  case "download": {
3260
2962
  void (async () => {
@@ -3325,4 +3027,61 @@ function SduiRenderer({
3325
3027
  );
3326
3028
  }
3327
3029
 
3328
- export { CALC_OPS, Can, NodeErrorBoundary, RemoteSearchResults, ResultTable, SduiForm, SduiRenderer, SduiSearchDataProvider, SduiSearchResults, SearchResults, SortHandle, SortableList, cellRenderer, createRegistry, defaultRegistry, imageRenderer, normalizePageSizes, sortableDropIndex, sortableMove, useRemoteSearchPage, useSduiRuntime, useSearchFetcher, validateSpec };
3030
+ // src/spec-resolve.ts
3031
+ function isValidSpecId(id) {
3032
+ return id.length > 0 && id.split("/").every(validSegment);
3033
+ }
3034
+ function validSegment(segment) {
3035
+ if (segment.startsWith("{") && segment.endsWith("}")) {
3036
+ return /^\{[A-Za-z0-9_]+\}$/.test(segment);
3037
+ }
3038
+ return /^[A-Za-z0-9_-]+$/.test(segment);
3039
+ }
3040
+ function patternSpecCandidates(id) {
3041
+ const segments = id.split("/");
3042
+ if (segments.length < 2) return [];
3043
+ const candidates = [];
3044
+ const last = segments.length - 1;
3045
+ candidates.push({
3046
+ specId: [...segments.slice(0, last), "{id}"].join("/"),
3047
+ params: { id: segments[last] }
3048
+ });
3049
+ if (segments.length >= 3) {
3050
+ const inner = segments.length - 2;
3051
+ candidates.push({
3052
+ specId: segments.map((s, i) => i === inner ? "{id}" : s).join("/"),
3053
+ params: { id: segments[inner] }
3054
+ });
3055
+ candidates.push({
3056
+ specId: segments.map((s, i) => i === inner ? "{id}" : i === last ? "{sub}" : s).join("/"),
3057
+ params: { id: segments[inner], sub: segments[last] }
3058
+ });
3059
+ }
3060
+ if (segments.length >= 4) {
3061
+ const outer = segments.length - 3;
3062
+ candidates.push({
3063
+ specId: segments.map((s, i) => i === outer ? "{id}" : i === last ? "{sub}" : s).join("/"),
3064
+ params: { id: segments[outer], sub: segments[last] }
3065
+ });
3066
+ }
3067
+ const seen = /* @__PURE__ */ new Set([id]);
3068
+ return candidates.filter((candidate) => {
3069
+ if (seen.has(candidate.specId)) return false;
3070
+ seen.add(candidate.specId);
3071
+ return true;
3072
+ });
3073
+ }
3074
+ async function resolvePatternSpec(id, load) {
3075
+ const exact = await load(id);
3076
+ if (exact != null) return { spec: exact, specId: id, params: {} };
3077
+ for (const candidate of patternSpecCandidates(id)) {
3078
+ const spec = await load(candidate.specId);
3079
+ if (spec != null) return { spec, specId: candidate.specId, params: candidate.params };
3080
+ }
3081
+ return null;
3082
+ }
3083
+
3084
+ // src/icon-names.ts
3085
+ var ICON_NAMES = Object.keys(ICON_NODES);
3086
+
3087
+ export { Can, ICON_NAMES, NodeErrorBoundary, RemoteSearchResults, ResultTable, SduiForm, SduiRenderer, SduiSearchDataProvider, SduiSearchInvalidationProvider, SduiSearchResults, SearchResults, SortHandle, SortableList, cellRenderer, createRegistry, createSearchInvalidator, defaultRegistry, imageRenderer, isValidSpecId, mapActions, mapCardRows, mapColumns, mapGridCard, mapViews, matchesCondition, normalizePageSizes, patternSpecCandidates, resolvePatternSpec, sortableDropIndex, sortableMove, useRemoteSearchPage, useSduiRuntime, useSearchFetcher, validateSpec, withProps };