@wipco/sdui 0.50.6 → 1.0.1

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
  );
@@ -2848,6 +2521,8 @@ function SduiRenderer({
2848
2521
  const host = useMemo(() => resolveAdapter(adapter), [adapter]);
2849
2522
  const hostRef = useRef(host);
2850
2523
  hostRef.current = host;
2524
+ const invalidatePageDataRef = useRef(() => {
2525
+ });
2851
2526
  const [state, setState] = useState(() => ({ ...initialState ?? {} }));
2852
2527
  const urlSeeded = useRef(false);
2853
2528
  const urlSeedPending = useRef(null);
@@ -2930,21 +2605,25 @@ function SduiRenderer({
2930
2605
  }
2931
2606
  }, [state, page.urlState, syncUrlState]);
2932
2607
  const subscribeChannels = page.subscribe?.join(",") ?? "";
2608
+ const hasSubscribe = typeof host.subscribe === "function";
2933
2609
  useEffect(() => {
2934
- if (!subscribeChannels || !host.subscribe) return;
2610
+ if (!subscribeChannels || !hasSubscribe) return;
2611
+ const subscribe = hostRef.current.subscribe;
2612
+ if (!subscribe) return;
2935
2613
  let timer = null;
2936
- const unsubscribe = host.subscribe(subscribeChannels.split(","), () => {
2614
+ const unsubscribe = subscribe(subscribeChannels.split(","), () => {
2937
2615
  if (timer) clearTimeout(timer);
2938
2616
  timer = setTimeout(() => {
2939
- host.invalidate?.("", false);
2940
- host.refresh?.();
2617
+ hostRef.current.invalidate?.("", false);
2618
+ hostRef.current.refresh?.();
2619
+ invalidatePageDataRef.current("", false);
2941
2620
  }, 300);
2942
2621
  });
2943
2622
  return () => {
2944
2623
  if (timer) clearTimeout(timer);
2945
2624
  unsubscribe();
2946
2625
  };
2947
- }, [subscribeChannels, host]);
2626
+ }, [subscribeChannels, hasSubscribe]);
2948
2627
  const closeToast = useCallback((id) => {
2949
2628
  setToasts((prev) => prev.filter((t) => t.id !== id));
2950
2629
  }, []);
@@ -2958,14 +2637,14 @@ function SduiRenderer({
2958
2637
  );
2959
2638
  const lookupState = useCallback((key) => {
2960
2639
  const current = stateRef.current;
2961
- if (key in current) return current[key];
2640
+ if (Object.hasOwn(current, key)) return current[key];
2962
2641
  const segments = key.split(".");
2963
2642
  for (let cut = segments.length - 1; cut >= 1; cut--) {
2964
2643
  const head = segments.slice(0, cut).join(".");
2965
- if (head in current) {
2644
+ if (Object.hasOwn(current, head)) {
2966
2645
  let value = current[head];
2967
2646
  for (const segment of segments.slice(cut)) {
2968
- if (!isPlainObject5(value)) return void 0;
2647
+ if (!isPlainObject(value) || !Object.hasOwn(value, segment)) return void 0;
2969
2648
  value = value[segment];
2970
2649
  }
2971
2650
  return value;
@@ -2983,7 +2662,7 @@ function SduiRenderer({
2983
2662
  const frame = nearestItemFrame(scope);
2984
2663
  if (!frame) return null;
2985
2664
  if (value.$item === true) return frame.item;
2986
- return isPlainObject5(frame.item) ? frame.item[value.$item] ?? null : null;
2665
+ return isPlainObject(frame.item) && Object.hasOwn(frame.item, value.$item) ? frame.item[value.$item] ?? null : null;
2987
2666
  }
2988
2667
  if (isIndexRef(value)) {
2989
2668
  return nearestItemFrame(scope)?.index ?? null;
@@ -3005,14 +2684,14 @@ function SduiRenderer({
3005
2684
  const calc = value.$calc;
3006
2685
  const op = typeof calc.op === "string" ? calc.op : "";
3007
2686
  if (!Array.isArray(calc.args)) {
3008
- 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`);
2687
+ 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`);
3009
2688
  return null;
3010
2689
  }
3011
2690
  const resolved = calc.args.map((arg) => resolveValue(arg, scope));
3012
2691
  return evaluateCalc(calc, resolved, (late) => resolveValue(late, scope));
3013
2692
  }
3014
2693
  if (Array.isArray(value)) return value.map((v) => resolveValue(v, scope));
3015
- if (isPlainObject5(value)) {
2694
+ if (isPlainObject(value)) {
3016
2695
  const out = {};
3017
2696
  for (const [k, v] of Object.entries(value)) {
3018
2697
  out[k] = deferChainEntry(value, k) ? v : resolveValue(v, scope);
@@ -3034,11 +2713,25 @@ function SduiRenderer({
3034
2713
  const [dataEpochs, setDataEpochs] = useState({});
3035
2714
  const dataEpochsRef = useRef(dataEpochs);
3036
2715
  dataEpochsRef.current = dataEpochs;
2716
+ invalidatePageDataRef.current = (prefix, exact) => {
2717
+ const hit = invalidateHits(runnablePlans(dataPlanRef.current), prefix, exact);
2718
+ if (hit.length === 0) return;
2719
+ setDataEpochs((prev) => {
2720
+ const next = { ...prev };
2721
+ for (const key of hit) next[key] = (next[key] ?? 0) + 1;
2722
+ return next;
2723
+ });
2724
+ };
3037
2725
  const fetchedRef = useRef(/* @__PURE__ */ new Map());
3038
2726
  const dataEpochKey = useMemo(() => JSON.stringify(dataEpochs), [dataEpochs]);
3039
2727
  useEffect(() => {
3040
2728
  const epochs = dataEpochsRef.current;
3041
- const runnable = runnablePlans(dataPlanRef.current).filter((plan) => {
2729
+ const plans = dataPlanRef.current;
2730
+ const stale = staleLoadingKeys(plans, (key) => stateRef.current[`${key}.$loading`] === true);
2731
+ if (stale.length > 0) {
2732
+ setEntries(Object.fromEntries(stale.map((key) => [`${key}.$loading`, false])));
2733
+ }
2734
+ const runnable = runnablePlans(plans).filter((plan) => {
3042
2735
  const stamp = planStamp(plan, epochs);
3043
2736
  if (fetchedRef.current.get(plan.key) === stamp) return false;
3044
2737
  fetchedRef.current.set(plan.key, stamp);
@@ -3067,7 +2760,7 @@ function SduiRenderer({
3067
2760
  if (cancelled) return;
3068
2761
  const picked = pickPath(response, plan.select);
3069
2762
  if (picked === void 0 && plan.select !== void 0 && response !== null) {
3070
- warnOnce2(
2763
+ warnOnce(
3071
2764
  `data-select:${plan.key}`,
3072
2765
  `[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})`
3073
2766
  );
@@ -3081,7 +2774,7 @@ function SduiRenderer({
3081
2774
  } catch (error) {
3082
2775
  if (cancelled || controller.signal.aborted) return;
3083
2776
  const message = error instanceof Error ? error.message : "\uC870\uD68C\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4";
3084
- warnOnce2(`data:${plan.key}:${plan.endpoint}`, `[sdui] page.data.${plan.key} \uC870\uD68C \uC2E4\uD328 (${plan.endpoint}): ${message}`);
2777
+ warnOnce(`data:${plan.key}:${plan.endpoint}`, `[sdui] page.data.${plan.key} \uC870\uD68C \uC2E4\uD328 (${plan.endpoint}): ${message}`);
3085
2778
  settled.add(plan.key);
3086
2779
  setEntries({
3087
2780
  [plan.key]: plan.fallback ?? void 0,
@@ -3116,7 +2809,7 @@ function SduiRenderer({
3116
2809
  switch (action.action) {
3117
2810
  case "navigate":
3118
2811
  if (!isSafeNavigationTarget(action.to)) {
3119
- warnOnce2(`navigate:${String(action.to)}`, `[sdui] \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 navigate \uB300\uC0C1: ${String(action.to)}`);
2812
+ warnOnce(`navigate:${String(action.to)}`, `[sdui] \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 navigate \uB300\uC0C1: ${String(action.to)}`);
3120
2813
  break;
3121
2814
  }
3122
2815
  hostRef.current.navigate(action.to);
@@ -3133,7 +2826,7 @@ function SduiRenderer({
3133
2826
  nextValue
3134
2827
  );
3135
2828
  if (!written.ok) {
3136
- warnOnce2(
2829
+ warnOnce(
3137
2830
  `setState:${action.key}:${written.reason}`,
3138
2831
  `[sdui] setState(${action.key}) \uACBD\uB85C \uC4F0\uAE30\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4 \u2014 ${written.reason}`
3139
2832
  );
@@ -3162,7 +2855,7 @@ function SduiRenderer({
3162
2855
  if (!Array.isArray(current)) break;
3163
2856
  const index = toIndex(action.index);
3164
2857
  if (!inRange(index, current.length)) {
3165
- warnOnce2(`arrayRemove:${action.key}`, `[sdui] arrayRemove(${action.key}): \uC798\uBABB\uB41C index \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
2858
+ warnOnce(`arrayRemove:${action.key}`, `[sdui] arrayRemove(${action.key}): \uC798\uBABB\uB41C index \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
3166
2859
  break;
3167
2860
  }
3168
2861
  setEntries({ [action.key]: current.filter((_, i) => i !== index) });
@@ -3171,7 +2864,7 @@ function SduiRenderer({
3171
2864
  case "arrayMove": {
3172
2865
  const current = stateRef.current[action.key];
3173
2866
  if (!Array.isArray(current)) {
3174
- warnOnce2(
2867
+ warnOnce(
3175
2868
  `arrayMove:key:${action.key}`,
3176
2869
  `[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`
3177
2870
  );
@@ -3180,7 +2873,7 @@ function SduiRenderer({
3180
2873
  const from = toIndex(action.from);
3181
2874
  const rawTo = toIndex(action.to);
3182
2875
  if (!inRange(from, current.length) || rawTo === null) {
3183
- warnOnce2(`arrayMove:${action.key}`, `[sdui] arrayMove(${action.key}): \uC798\uBABB\uB41C from/to \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
2876
+ warnOnce(`arrayMove:${action.key}`, `[sdui] arrayMove(${action.key}): \uC798\uBABB\uB41C from/to \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
3184
2877
  break;
3185
2878
  }
3186
2879
  const to = Math.max(0, Math.min(current.length - 1, rawTo));
@@ -3196,11 +2889,11 @@ function SduiRenderer({
3196
2889
  if (!Array.isArray(current)) break;
3197
2890
  const index = toIndex(action.index);
3198
2891
  if (!inRange(index, current.length) || typeof action.field !== "string") {
3199
- warnOnce2(`arrayUpdate:${action.key}`, `[sdui] arrayUpdate(${action.key}): \uC798\uBABB\uB41C index/field \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
2892
+ warnOnce(`arrayUpdate:${action.key}`, `[sdui] arrayUpdate(${action.key}): \uC798\uBABB\uB41C index/field \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
3200
2893
  break;
3201
2894
  }
3202
2895
  const row = current[index];
3203
- if (!isPlainObject5(row)) break;
2896
+ if (!isPlainObject(row)) break;
3204
2897
  const nextValue = (action.fromEvent ? eventValue(arg) : action.value) ?? null;
3205
2898
  const next = [...current];
3206
2899
  next[index] = { ...row, [action.field]: nextValue };
@@ -3210,9 +2903,11 @@ function SduiRenderer({
3210
2903
  case "submitForm": {
3211
2904
  const formId = action.form;
3212
2905
  if (stateRef.current[`${formId}.$submitting`] === true) break;
2906
+ const pendingKey = typeof action.pendingKey === "string" && action.pendingKey !== "" ? action.pendingKey : null;
3213
2907
  void (async () => {
3214
2908
  setEntries({ [`${formId}.$submitting`]: true });
3215
2909
  let phase = "validate";
2910
+ let pendingOn = false;
3216
2911
  try {
3217
2912
  const prefix = `${formId}.`;
3218
2913
  const values = {};
@@ -3228,23 +2923,36 @@ function SduiRenderer({
3228
2923
  }
3229
2924
  setEntries({ [`${formId}.$errors`]: {}, [`${formId}.$submitting`]: true });
3230
2925
  phase = "submit";
2926
+ if (pendingKey) {
2927
+ pendingOn = true;
2928
+ setEntries({ [pendingKey]: true });
2929
+ }
3231
2930
  const response = await hostRef.current.fetch(action.endpoint, {
3232
2931
  method: action.method ?? "POST",
3233
2932
  body: values
3234
2933
  });
3235
2934
  setEntries({ [`${formId}.$submitting`]: false, [`${formId}.$resetDirty`]: true });
3236
- (action.onSuccess ?? []).forEach((a) => dispatchRef.current(a, response, scope));
2935
+ runSuccessChain("submitForm", formId, () => {
2936
+ (action.onSuccess ?? []).forEach((a) => dispatchRef.current(a, response, scope));
2937
+ });
3237
2938
  } catch (error) {
3238
2939
  setEntries({ [`${formId}.$submitting`]: false });
3239
- const message = error instanceof Error ? error.message : "\uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4";
2940
+ const failure = failureDetailOf(error);
3240
2941
  if (phase === "validate") {
3241
- warnOnce2(`validate:${formId}`, `[sdui] ${formId} \uAC80\uC99D \uADDC\uCE59 \uC624\uB958: ${message}`);
3242
- pushToast({ tone: "danger", title: "\uAC80\uC99D \uADDC\uCE59 \uC624\uB958", message });
3243
- } else if (action.onError?.length) {
3244
- action.onError.forEach((a) => dispatchRef.current(a, message, scope));
2942
+ warnOnce(`validate:${formId}`, `[sdui] ${formId} \uAC80\uC99D \uADDC\uCE59 \uC624\uB958: ${failure.message}`);
2943
+ pushToast({ tone: "danger", title: "\uAC80\uC99D \uADDC\uCE59 \uC624\uB958", message: failure.message });
3245
2944
  } else {
3246
- pushToast({ tone: "danger", title: "\uC800\uC7A5 \uC2E4\uD328", message });
2945
+ if (typeof action.errorKey === "string" && action.errorKey !== "") {
2946
+ setEntries({ [action.errorKey]: failure });
2947
+ }
2948
+ if (action.onError?.length) {
2949
+ action.onError.forEach((a) => dispatchRef.current(a, failure.message, scope));
2950
+ } else {
2951
+ pushToast({ tone: "danger", title: "\uC800\uC7A5 \uC2E4\uD328", message: failure.message });
2952
+ }
3247
2953
  }
2954
+ } finally {
2955
+ if (pendingKey && pendingOn) setEntries({ [pendingKey]: false });
3248
2956
  }
3249
2957
  })();
3250
2958
  break;
@@ -3261,18 +2969,12 @@ function SduiRenderer({
3261
2969
  case "invalidate": {
3262
2970
  hostRef.current.invalidate?.(action.prefix, action.exact ?? false);
3263
2971
  const prefix = typeof action.prefix === "string" ? action.prefix : "";
3264
- const hit = invalidateHits(runnablePlans(dataPlanRef.current), prefix, action.exact ?? false);
3265
- if (hit.length > 0) {
3266
- setDataEpochs((prev) => {
3267
- const next = { ...prev };
3268
- for (const key of hit) next[key] = (next[key] ?? 0) + 1;
3269
- return next;
3270
- });
3271
- }
2972
+ invalidatePageDataRef.current(prefix, action.exact ?? false);
3272
2973
  break;
3273
2974
  }
3274
2975
  case "refresh":
3275
2976
  hostRef.current.refresh?.();
2977
+ invalidatePageDataRef.current("", false);
3276
2978
  break;
3277
2979
  case "download": {
3278
2980
  void (async () => {
@@ -3343,4 +3045,61 @@ function SduiRenderer({
3343
3045
  );
3344
3046
  }
3345
3047
 
3346
- 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 };
3048
+ // src/spec-resolve.ts
3049
+ function isValidSpecId(id) {
3050
+ return id.length > 0 && id.split("/").every(validSegment);
3051
+ }
3052
+ function validSegment(segment) {
3053
+ if (segment.startsWith("{") && segment.endsWith("}")) {
3054
+ return /^\{[A-Za-z0-9_]+\}$/.test(segment);
3055
+ }
3056
+ return /^[A-Za-z0-9_-]+$/.test(segment);
3057
+ }
3058
+ function patternSpecCandidates(id) {
3059
+ const segments = id.split("/");
3060
+ if (segments.length < 2) return [];
3061
+ const candidates = [];
3062
+ const last = segments.length - 1;
3063
+ candidates.push({
3064
+ specId: [...segments.slice(0, last), "{id}"].join("/"),
3065
+ params: { id: segments[last] }
3066
+ });
3067
+ if (segments.length >= 3) {
3068
+ const inner = segments.length - 2;
3069
+ candidates.push({
3070
+ specId: segments.map((s, i) => i === inner ? "{id}" : s).join("/"),
3071
+ params: { id: segments[inner] }
3072
+ });
3073
+ candidates.push({
3074
+ specId: segments.map((s, i) => i === inner ? "{id}" : i === last ? "{sub}" : s).join("/"),
3075
+ params: { id: segments[inner], sub: segments[last] }
3076
+ });
3077
+ }
3078
+ if (segments.length >= 4) {
3079
+ const outer = segments.length - 3;
3080
+ candidates.push({
3081
+ specId: segments.map((s, i) => i === outer ? "{id}" : i === last ? "{sub}" : s).join("/"),
3082
+ params: { id: segments[outer], sub: segments[last] }
3083
+ });
3084
+ }
3085
+ const seen = /* @__PURE__ */ new Set([id]);
3086
+ return candidates.filter((candidate) => {
3087
+ if (seen.has(candidate.specId)) return false;
3088
+ seen.add(candidate.specId);
3089
+ return true;
3090
+ });
3091
+ }
3092
+ async function resolvePatternSpec(id, load) {
3093
+ const exact = await load(id);
3094
+ if (exact != null) return { spec: exact, specId: id, params: {} };
3095
+ for (const candidate of patternSpecCandidates(id)) {
3096
+ const spec = await load(candidate.specId);
3097
+ if (spec != null) return { spec, specId: candidate.specId, params: candidate.params };
3098
+ }
3099
+ return null;
3100
+ }
3101
+
3102
+ // src/icon-names.ts
3103
+ var ICON_NAMES = Object.keys(ICON_NODES);
3104
+
3105
+ 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 };