@wipco/sdui 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/dist/{DatePicker-C_TT_SOw.d.ts → DatePicker-DX9juElE.d.ts} +10 -1
  2. package/dist/{chunk-GZBY7I4Y.js → chunk-2KQL6DYG.js} +3 -3
  3. package/dist/{chunk-RYBBPFI7.js → chunk-2QXA52DG.js} +4 -4
  4. package/dist/{chunk-DMLDKW7K.js → chunk-37BNG4DT.js} +1 -1
  5. package/dist/{chunk-5WW4MBRO.js → chunk-3JTBOABA.js} +6 -2
  6. package/dist/{chunk-UV6HHFEW.js → chunk-4OABUPIU.js} +129 -68
  7. package/dist/{chunk-ZORQPB6B.js → chunk-53KS4E5N.js} +318 -359
  8. package/dist/{chunk-I5SD7XRA.js → chunk-6TI4WF5Y.js} +3 -3
  9. package/dist/{chunk-44TDRCKR.js → chunk-6XI2VF2O.js} +5 -5
  10. package/dist/{chunk-HBIIEYKW.js → chunk-7C54CKFZ.js} +4 -4
  11. package/dist/{chunk-IZ3JA2BR.js → chunk-7JP52QQO.js} +3 -3
  12. package/dist/{chunk-I45EUZC6.js → chunk-AY4GM6KQ.js} +4 -2
  13. package/dist/{chunk-SAPBDJJQ.js → chunk-AZSLSAJB.js} +2 -2
  14. package/dist/chunk-BITS7UJY.js +333 -0
  15. package/dist/{chunk-RNOUXUBO.js → chunk-BLFJMMBW.js} +306 -57
  16. package/dist/{chunk-YOZN35HD.js → chunk-CEVZAFPO.js} +3 -3
  17. package/dist/chunk-CI5OZ2QT.js +106 -0
  18. package/dist/{chunk-FNMS7OB7.js → chunk-CSE3PZOZ.js} +5 -5
  19. package/dist/chunk-CSZCWBOE.js +256 -0
  20. package/dist/{chunk-JIV4YHCZ.js → chunk-EJIWOD7W.js} +1 -1
  21. package/dist/{chunk-JZXU22PE.js → chunk-FEKKOBPX.js} +208 -117
  22. package/dist/chunk-GSM664TX.js +38 -0
  23. package/dist/{chunk-JYAR7D5U.js → chunk-HRHKQOOF.js} +2 -2
  24. package/dist/{chunk-CXW6KT6X.js → chunk-HXLQSO66.js} +3 -3
  25. package/dist/{chunk-O4IISNE7.js → chunk-IB2ZPUTF.js} +51 -12
  26. package/dist/{chunk-MPT7WJG2.js → chunk-IQXLNICY.js} +5 -5
  27. package/dist/{chunk-OQ52XMGG.js → chunk-K6RE23XX.js} +4 -4
  28. package/dist/{chunk-VY7MVBSE.js → chunk-KZ6U6FKB.js} +2 -2
  29. package/dist/{chunk-WPSK2CB5.js → chunk-LIZTK3HO.js} +16 -5
  30. package/dist/{chunk-Q4SWUINA.js → chunk-LKQJJSVJ.js} +1 -1
  31. package/dist/{chunk-E6ISNQK3.js → chunk-M6H3SHIS.js} +31 -8
  32. package/dist/chunk-MN4RZ2WP.js +380 -0
  33. package/dist/{chunk-GTGS2QRL.js → chunk-NA232CBX.js} +3 -3
  34. package/dist/{chunk-ZFNFPXPR.js → chunk-NAIWQQPW.js} +2 -2
  35. package/dist/{chunk-YVTV4ZV6.js → chunk-NWYYNE3Q.js} +1 -1
  36. package/dist/{chunk-DKVSM5HX.js → chunk-OUEJWC2Z.js} +6 -6
  37. package/dist/{chunk-DOL57OOP.js → chunk-OYDJHQNZ.js} +3 -3
  38. package/dist/{chunk-YWUDBLO6.js → chunk-QVS6K4FP.js} +2 -2
  39. package/dist/{chunk-N5RKM6IC.js → chunk-RU5U2YMT.js} +4 -4
  40. package/dist/{chunk-A6LAIGUS.js → chunk-V7BWMOHO.js} +7 -4
  41. package/dist/{chunk-2HLXCKK7.js → chunk-VPOOIEKT.js} +2 -2
  42. package/dist/{chunk-2YP5N4VV.js → chunk-VZOQRQAR.js} +1 -1
  43. package/dist/{chunk-GNA6SX2H.js → chunk-W4VA76QO.js} +2 -2
  44. package/dist/{chunk-BEM2YS7U.js → chunk-WEG3V7ZB.js} +3 -3
  45. package/dist/{chunk-XGFDY43I.js → chunk-WTIYILY3.js} +88 -18
  46. package/dist/{chunk-RWSH2MBU.js → chunk-X2NIRVLN.js} +73 -19
  47. package/dist/{chunk-AXGF4M57.js → chunk-XAXROQKB.js} +1 -1
  48. package/dist/{chunk-NIJUVTEW.js → chunk-XG22XKKD.js} +3 -3
  49. package/dist/{chunk-A5XQOEVH.js → chunk-YN7X7BEO.js} +4 -4
  50. package/dist/{chunk-KO6OEY3G.js → chunk-YNEEJCSZ.js} +3 -13
  51. package/dist/{chunk-7P7N43JV.js → chunk-YZTTFEL4.js} +2 -2
  52. package/dist/index.d.ts +418 -90
  53. package/dist/index.js +997 -123
  54. package/dist/{layout-C3nRXMwb.d.ts → layout-DmRikwZb.d.ts} +5 -4
  55. package/dist/meta.d.ts +1 -1
  56. package/dist/meta.js +236 -22
  57. package/dist/rich/ExcelUploadCard.d.ts +1 -1
  58. package/dist/rich/ExcelUploadCard.js +5 -4
  59. package/dist/rich.d.ts +21 -3
  60. package/dist/rich.js +277 -35
  61. package/dist/styles.css +102 -4
  62. package/dist/{types-COjGKgmd.d.ts → types-CYfIthux.d.ts} +57 -1
  63. package/dist/widgets/ApprovalLineEditor.d.ts +1 -1
  64. package/dist/widgets/ApprovalLineEditor.js +9 -8
  65. package/dist/widgets/ApprovalOverridesEditor.d.ts +1 -1
  66. package/dist/widgets/ApprovalOverridesEditor.js +11 -10
  67. package/dist/widgets/ApprovalStepper.d.ts +29 -4
  68. package/dist/widgets/ApprovalStepper.js +3 -3
  69. package/dist/widgets/AutocompleteField.d.ts +1 -1
  70. package/dist/widgets/AutocompleteField.js +9 -8
  71. package/dist/widgets/ColumnBoard.d.ts +1 -1
  72. package/dist/widgets/ColumnBoard.js +3 -3
  73. package/dist/widgets/DiffView.d.ts +1 -1
  74. package/dist/widgets/DiffView.js +3 -3
  75. package/dist/widgets/Disclosure.d.ts +1 -1
  76. package/dist/widgets/Disclosure.js +3 -3
  77. package/dist/widgets/EventCalendar.d.ts +5 -1
  78. package/dist/widgets/EventCalendar.js +5 -3
  79. package/dist/widgets/FileDropzone.d.ts +1 -1
  80. package/dist/widgets/FileDropzone.js +9 -7
  81. package/dist/widgets/ImageLightbox.d.ts +1 -1
  82. package/dist/widgets/ImageLightbox.js +7 -5
  83. package/dist/widgets/InlineEdit.d.ts +1 -1
  84. package/dist/widgets/InlineEdit.js +3 -3
  85. package/dist/widgets/JsonField.d.ts +1 -1
  86. package/dist/widgets/JsonField.js +3 -3
  87. package/dist/widgets/LiveClock.d.ts +1 -1
  88. package/dist/widgets/LiveClock.js +2 -2
  89. package/dist/widgets/OptionSchemaEditor.d.ts +1 -1
  90. package/dist/widgets/OptionSchemaEditor.js +4 -4
  91. package/dist/widgets/OptionValueFields.d.ts +1 -1
  92. package/dist/widgets/OptionValueFields.js +4 -4
  93. package/dist/widgets/OrgChart.d.ts +1 -1
  94. package/dist/widgets/OrgChart.js +4 -4
  95. package/dist/widgets/OrgChartBoard.d.ts +15 -33
  96. package/dist/widgets/OrgChartBoard.js +10 -8
  97. package/dist/widgets/PeriodFilter.d.ts +1 -1
  98. package/dist/widgets/PeriodFilter.js +6 -5
  99. package/dist/widgets/PermissionMatrixEditor.d.ts +34 -2
  100. package/dist/widgets/PermissionMatrixEditor.js +6 -4
  101. package/dist/widgets/PositionSelect.d.ts +1 -1
  102. package/dist/widgets/PositionSelect.js +5 -4
  103. package/dist/widgets/PrintSheet.d.ts +15 -2
  104. package/dist/widgets/PrintSheet.js +3 -3
  105. package/dist/widgets/ProfileEditDiffCard.d.ts +1 -1
  106. package/dist/widgets/ProfileEditDiffCard.js +5 -4
  107. package/dist/widgets/QRDisplay.d.ts +1 -1
  108. package/dist/widgets/QRDisplay.js +5 -4
  109. package/dist/widgets/RankBoard.d.ts +1 -1
  110. package/dist/widgets/RankBoard.js +8 -6
  111. package/dist/widgets/RecordSheet.d.ts +1 -1
  112. package/dist/widgets/RecordSheet.js +2 -2
  113. package/dist/widgets/RoleMemberPicker.d.ts +1 -1
  114. package/dist/widgets/RoleMemberPicker.js +6 -5
  115. package/dist/widgets/SearchableMultiPicker.d.ts +1 -1
  116. package/dist/widgets/SearchableMultiPicker.js +7 -6
  117. package/dist/widgets/SecureScreen.d.ts +1 -1
  118. package/dist/widgets/SecureScreen.js +3 -3
  119. package/dist/widgets/StackedMeter.d.ts +1 -1
  120. package/dist/widgets/StackedMeter.js +2 -2
  121. package/dist/widgets/StagePeriodEditor.d.ts +1 -1
  122. package/dist/widgets/StagePeriodEditor.js +6 -5
  123. package/dist/widgets/Strip.d.ts +1 -1
  124. package/dist/widgets/Strip.js +2 -2
  125. package/dist/widgets/SummaryTable.d.ts +1 -1
  126. package/dist/widgets/SummaryTable.js +2 -2
  127. package/dist/widgets/TagInput.d.ts +1 -1
  128. package/dist/widgets/TagInput.js +3 -3
  129. package/dist/widgets/TagMultiSelect.d.ts +1 -1
  130. package/dist/widgets/TagMultiSelect.js +5 -4
  131. package/dist/widgets/Timeline.d.ts +1 -1
  132. package/dist/widgets/Timeline.js +4 -4
  133. package/dist/widgets/TreePicker.d.ts +1 -1
  134. package/dist/widgets/TreePicker.js +8 -7
  135. package/dist/widgets/TreeView.d.ts +1 -1
  136. package/dist/widgets/TreeView.js +7 -6
  137. package/dist/widgets/index.d.ts +3 -3
  138. package/dist/widgets/index.js +84 -83
  139. package/package.json +1 -1
  140. package/dist/chunk-LFEJLOFN.js +0 -352
  141. package/dist/chunk-MMIGOPO5.js +0 -111
  142. package/dist/chunk-TFGNKFL2.js +0 -39
  143. package/dist/chunk-XQPTSEJC.js +0 -135
package/dist/index.js CHANGED
@@ -1,23 +1,26 @@
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-WPSK2CB5.js';
5
- export { Actions, Col, Divider, Grid, KeyValueList, PageHead, Row, Section, Show, Spacer, Text } from './chunk-WPSK2CB5.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-UV6HHFEW.js';
9
- export { SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue } from './chunk-UV6HHFEW.js';
10
- import { parseYmd, YMD_RE, TimePicker, DateRangePicker, DatePicker } from './chunk-RWSH2MBU.js';
11
- export { DatePicker, DateRangePicker, TimePicker } from './chunk-RWSH2MBU.js';
1
+ export { loadOptionalModule } from './chunk-NWYYNE3Q.js';
2
+ import { useSduiDispatch, SduiDialog, useSduiCan, useSduiRuntime, SduiDirtyRegistryContext, SduiLeaveRegistryContext, SduiFormScopeContext, SduiDispatchContext, SduiRuntimeContext, SduiPermissionProvider } from './chunk-CI5OZ2QT.js';
3
+ export { SduiDialog, SduiPermissionProvider, useSduiCan, useSduiDispatch, useSduiRuntime } from './chunk-CI5OZ2QT.js';
4
+ import { Text, Spacer, Show, Section, Row, PageHead, KeyValueList, Grid, Divider, Col, Actions } from './chunk-LIZTK3HO.js';
5
+ export { Actions, Col, Divider, Grid, KeyValueList, PageHead, Row, Section, Show, Spacer, Text } from './chunk-LIZTK3HO.js';
6
+ import { ImageViewer } from './chunk-AZSLSAJB.js';
7
+ export { ImageViewer } from './chunk-AZSLSAJB.js';
8
+ import { normalizeSearchFields, searchValuesFromParams, splitMultiValue, includesLooseSearch, cascadingValuePaths, SearchToolbar, searchFieldKeys, Popover } from './chunk-4OABUPIU.js';
9
+ export { SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue } from './chunk-4OABUPIU.js';
10
+ import { parseYmd, YMD_RE, TimePicker, DateRangePicker, DatePicker } from './chunk-X2NIRVLN.js';
11
+ export { DatePicker, DateRangePicker, TimePicker } from './chunk-X2NIRVLN.js';
12
+ import { OverlayContainerContext, OverlayLayer, useAnyModalOpen } from './chunk-BITS7UJY.js';
13
+ export { OverlayContainerContext, OverlayLayer, getOverlayRoot, useAnyModalOpen, useOverlayLayer } from './chunk-BITS7UJY.js';
12
14
  import { HoistedStyle } from './chunk-RIZRM7OO.js';
13
- export { OverlayContainerContext, OverlayLayer, getOverlayRoot, useOverlayLayer } from './chunk-XQPTSEJC.js';
14
- import { DEV, useSduiAdapter, hostFetchOf, isSafeNavigationTarget, useSduiSandbox, resolveAdapter, assetResolverOf, SduiAdapterContext } from './chunk-KO6OEY3G.js';
15
- export { defaultAdapter, useSduiAssetResolver, useSduiSandbox } from './chunk-KO6OEY3G.js';
16
- import { EmptyState, Button, Checkbox, Tabs, SegmentedControl, Spinner, Select, Pagination, fieldOf, StatCard, Icon, Card, Badge, ProgressBar, Tag, Toast, Textarea, Table, Switch, Input, IconButton, Field, Alert, warnOnce, isPlainObject, CALC_OPS, evaluateCalc, ICON_NODES, toNumber } from './chunk-RNOUXUBO.js';
17
- 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';
18
- import { boxed, LAYOUT_CSS, childLayoutFrom, CHILD_LAYOUT_PROPS } from './chunk-5WW4MBRO.js';
19
- export { CHILD_LAYOUT_PROPS, LAYOUT_CSS, boxed, childLayoutFrom, columnsClass, containerLayoutStyle, joinClass, mergeStyle, typographyStyle } from './chunk-5WW4MBRO.js';
20
- import React9, { createContext, useState, useMemo, useRef, useEffect, useContext, useCallback } from 'react';
15
+ import { useSduiAdapter, hostFetchOf, isSafeNavigationTarget, useSduiSandbox, resolveAdapter, assetResolverOf, SduiAdapterContext } from './chunk-YNEEJCSZ.js';
16
+ export { defaultAdapter, useSduiAssetResolver, useSduiSandbox } from './chunk-YNEEJCSZ.js';
17
+ import { DEV, useIsMobile } from './chunk-GSM664TX.js';
18
+ export { MOBILE_MEDIA_QUERY, useIsMobile } from './chunk-GSM664TX.js';
19
+ import { EmptyState, Button, Checkbox, Select, Tabs, SegmentedControl, Spinner, Pagination, fieldOf, StatCard, isZonedIsoDateTime, formatDateValue, Icon, Card, Badge, ProgressBar, Tag, Toast, Textarea, Table, Switch, Input, IconButton, Field, Alert, warnOnce, isPlainObject, CALC_OPS, evaluateCalc, Dialog, ICON_NODES, toNumber } from './chunk-BLFJMMBW.js';
20
+ 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, formatDateValue, formatDisplayValue, formatNumberValue, isZonedIsoDateTime, parseNumeric } from './chunk-BLFJMMBW.js';
21
+ import { boxed, LAYOUT_CSS, childLayoutFrom, CHILD_LAYOUT_PROPS } from './chunk-3JTBOABA.js';
22
+ export { CHILD_LAYOUT_PROPS, LAYOUT_CSS, boxed, childLayoutFrom, columnsClass, containerLayoutStyle, joinClass, mergeStyle, typographyStyle } from './chunk-3JTBOABA.js';
23
+ import React9, { createContext, useState, useMemo, useEffect, useRef, useContext, useCallback, useSyncExternalStore } from 'react';
21
24
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
22
25
 
23
26
  // src/asset-url.ts
@@ -114,6 +117,119 @@ function resolveAssetsIn(propName, value, resolve, component) {
114
117
  return out ?? record;
115
118
  }
116
119
  }
120
+
121
+ // src/josa.ts
122
+ var HANGUL_FIRST = 44032;
123
+ var HANGUL_LAST = 55203;
124
+ var JONG_RIEUL = 8;
125
+ var DIGIT_JONG = {
126
+ "0": 21,
127
+ "1": JONG_RIEUL,
128
+ "2": 0,
129
+ "3": 16,
130
+ "4": 0,
131
+ "5": 0,
132
+ "6": 1,
133
+ "7": JONG_RIEUL,
134
+ "8": JONG_RIEUL,
135
+ "9": 0
136
+ };
137
+ function finalConsonant(word) {
138
+ let text = word.trim();
139
+ for (; ; ) {
140
+ const next = text.replace(/\s*[([([][^()[\]()[]]*[)\])]]$/u, "").replace(/["'”’」』]+$/u, "").trim();
141
+ if (next === text) break;
142
+ text = next;
143
+ }
144
+ const last = text.slice(-1);
145
+ if (!last) return null;
146
+ const code = last.charCodeAt(0);
147
+ if (code >= HANGUL_FIRST && code <= HANGUL_LAST) return (code - HANGUL_FIRST) % 28;
148
+ if (last in DIGIT_JONG) return DIGIT_JONG[last];
149
+ return null;
150
+ }
151
+ function josaOf(word, pair) {
152
+ const [withFinal, withoutFinal] = pair.split("/");
153
+ const jong = finalConsonant(word);
154
+ if (jong === null) return `${withFinal}(${withoutFinal})`;
155
+ if (pair === "\uC73C\uB85C/\uB85C") return jong === 0 || jong === JONG_RIEUL ? withoutFinal : withFinal;
156
+ return jong === 0 ? withoutFinal : withFinal;
157
+ }
158
+ function josa(word, pair) {
159
+ return `${word}${josaOf(word, pair)}`;
160
+ }
161
+
162
+ // src/toast-store.ts
163
+ var TOAST_LIFETIME_MS = 4e3;
164
+ var TOAST_UNSHOWN_TTL_MS = 1e4;
165
+ var entries = [];
166
+ var hosts = [];
167
+ var nextToastId = 0;
168
+ var nextHostId = 0;
169
+ var version = { entries, hosts };
170
+ var listeners = /* @__PURE__ */ new Set();
171
+ function later(fn, ms) {
172
+ const timer = setTimeout(fn, ms);
173
+ timer.unref?.();
174
+ }
175
+ function emit() {
176
+ version = { entries, hosts };
177
+ listeners.forEach((fn) => fn());
178
+ }
179
+ function subscribeToasts(fn) {
180
+ listeners.add(fn);
181
+ return () => {
182
+ listeners.delete(fn);
183
+ };
184
+ }
185
+ function toastSnapshot() {
186
+ return version;
187
+ }
188
+ var EMPTY_SNAPSHOT = { entries: [], hosts: [] };
189
+ function toastServerSnapshot() {
190
+ return EMPTY_SNAPSHOT;
191
+ }
192
+ function allocateToastHostId() {
193
+ nextHostId += 1;
194
+ return nextHostId;
195
+ }
196
+ function registerToastHost(id, channel) {
197
+ hosts = [...hosts.filter((h) => h.id !== id), { id, channel }];
198
+ emit();
199
+ return () => {
200
+ hosts = hosts.filter((h) => h.id !== id);
201
+ emit();
202
+ };
203
+ }
204
+ function pushToastEntry(owner, channel, toast) {
205
+ nextToastId += 1;
206
+ const id = nextToastId;
207
+ entries = [...entries, { ...toast, id, owner, channel, shown: false }];
208
+ emit();
209
+ later(() => {
210
+ const entry = entries.find((e) => e.id === id);
211
+ if (entry && !entry.shown) closeToastEntry(id);
212
+ }, TOAST_UNSHOWN_TTL_MS);
213
+ return id;
214
+ }
215
+ function closeToastEntry(id) {
216
+ if (!entries.some((e) => e.id === id)) return;
217
+ entries = entries.filter((e) => e.id !== id);
218
+ emit();
219
+ }
220
+ function markToastsShown(ids) {
221
+ const fresh = ids.filter((id) => entries.some((e) => e.id === id && !e.shown));
222
+ if (fresh.length === 0) return;
223
+ entries = entries.map((e) => fresh.includes(e.id) ? { ...e, shown: true } : e);
224
+ for (const id of fresh) later(() => closeToastEntry(id), TOAST_LIFETIME_MS);
225
+ }
226
+ function toastHostOf(entry, liveHosts) {
227
+ if (liveHosts.some((h) => h.id === entry.owner)) return entry.owner;
228
+ for (let i = liveHosts.length - 1; i >= 0; i -= 1) {
229
+ if (liveHosts[i].channel === entry.channel) return liveHosts[i].id;
230
+ }
231
+ return void 0;
232
+ }
117
233
  function SpecErrorCard({ children }) {
118
234
  return /* @__PURE__ */ jsx(
119
235
  "div",
@@ -361,13 +477,85 @@ function cellRenderer(cell, key, dispatch) {
361
477
  return void 0;
362
478
  }
363
479
  }
480
+
481
+ // src/search/sort.ts
482
+ var SORT_PARAM = "sort";
483
+ var ORDER_PARAM = "order";
484
+ function sortFromParams(params) {
485
+ const key = params.get(SORT_PARAM);
486
+ if (!key) return null;
487
+ return { key, dir: params.get(ORDER_PARAM) === "desc" ? "desc" : "asc" };
488
+ }
489
+ function nextSort(current, key) {
490
+ if (!current || current.key !== key) return { key, dir: "asc" };
491
+ return current.dir === "asc" ? { key, dir: "desc" } : null;
492
+ }
493
+ function sortEchoOf(body) {
494
+ if (!body || typeof body !== "object") return null;
495
+ const sort = body.sort;
496
+ if (!sort || typeof sort !== "object") return null;
497
+ const { field, order } = sort;
498
+ if (typeof field !== "string" || !field) return null;
499
+ return { field, order: order === "desc" ? "desc" : "asc" };
500
+ }
501
+ var NONE = Object.freeze([]);
502
+ function endpointPath(endpoint) {
503
+ const cut = endpoint.indexOf("?");
504
+ return cut >= 0 ? endpoint.slice(0, cut) : endpoint;
505
+ }
506
+ function createServerSortMemory() {
507
+ const entries2 = /* @__PURE__ */ new Map();
508
+ const entry = (endpoint) => {
509
+ const path = endpointPath(endpoint);
510
+ let found = entries2.get(path);
511
+ if (!found) {
512
+ found = { supported: /* @__PURE__ */ new Set(), unsupported: [], all: false };
513
+ entries2.set(path, found);
514
+ }
515
+ return found;
516
+ };
517
+ const unsortable = (endpoint) => {
518
+ const e = entries2.get(endpointPath(endpoint));
519
+ if (!e) return NONE;
520
+ return e.all ? "all" : e.unsupported.length > 0 ? e.unsupported : NONE;
521
+ };
522
+ return {
523
+ record(endpoint, requested, echo) {
524
+ const e = entry(endpoint);
525
+ if (echo) {
526
+ e.supported.add(echo.field);
527
+ e.all = false;
528
+ if (echo.field === requested.key) return true;
529
+ }
530
+ if (e.supported.size === 0) e.all = true;
531
+ else if (!e.unsupported.includes(requested.key)) e.unsupported = [...e.unsupported, requested.key];
532
+ return false;
533
+ },
534
+ unsortable,
535
+ allows(endpoint, key) {
536
+ const verdict = unsortable(endpoint);
537
+ return verdict !== "all" && !verdict.includes(key);
538
+ }
539
+ };
540
+ }
541
+ var serverSortMemory = createServerSortMemory();
542
+ function withoutSort(params) {
543
+ const next = new URLSearchParams(params);
544
+ next.delete(SORT_PARAM);
545
+ next.delete(ORDER_PARAM);
546
+ return next;
547
+ }
364
548
  function searchUrl(endpoint, params) {
365
549
  const query = params.toString();
366
550
  return `${endpoint}${query ? endpoint.includes("?") ? "&" : "?" : ""}${query}`;
367
551
  }
368
552
  function toPage(body) {
369
553
  const rows = Array.isArray(body?.data) ? body.data : [];
370
- return { data: rows, total: typeof body?.total === "number" ? body.total : rows.length };
554
+ return {
555
+ data: rows,
556
+ total: typeof body?.total === "number" ? body.total : rows.length,
557
+ sort: sortEchoOf(body)
558
+ };
371
559
  }
372
560
  var defaultFetcher = async (endpoint, params, signal) => {
373
561
  const response = await fetch(searchUrl(endpoint, params), {
@@ -398,19 +586,19 @@ function SduiSearchDataProvider({
398
586
  return /* @__PURE__ */ jsx(SearchDataContext.Provider, { value: fetcher, children });
399
587
  }
400
588
  function createSearchInvalidator() {
401
- const listeners = /* @__PURE__ */ new Set();
589
+ const listeners2 = /* @__PURE__ */ new Set();
402
590
  return {
403
591
  invalidate(prefix = "", exact = false) {
404
- for (const entry of [...listeners]) {
592
+ for (const entry of [...listeners2]) {
405
593
  const hit = prefix === "" ? true : exact ? entry.endpoint === prefix : entry.endpoint.startsWith(prefix);
406
594
  if (hit) entry.reload();
407
595
  }
408
596
  },
409
597
  subscribe(endpoint, reload) {
410
598
  const entry = { endpoint, reload };
411
- listeners.add(entry);
599
+ listeners2.add(entry);
412
600
  return () => {
413
- listeners.delete(entry);
601
+ listeners2.delete(entry);
414
602
  };
415
603
  }
416
604
  };
@@ -436,6 +624,7 @@ function useRemoteSearchPage(endpoint, params) {
436
624
  const fetcher = useSearchFetcher();
437
625
  const invalidator = useContext(SearchInvalidationContext);
438
626
  const [page, setPage] = useState(EMPTY_PAGE);
627
+ const [pageQuery, setPageQuery] = useState(null);
439
628
  const [isLoading, setIsLoading] = useState(true);
440
629
  const [error, setError] = useState(null);
441
630
  const [reloadKey, setReloadKey] = useState(0);
@@ -454,6 +643,7 @@ function useRemoteSearchPage(endpoint, params) {
454
643
  (next) => {
455
644
  if (!alive) return;
456
645
  setPage(next);
646
+ setPageQuery(query);
457
647
  setError(null);
458
648
  setIsLoading(false);
459
649
  },
@@ -469,7 +659,7 @@ function useRemoteSearchPage(endpoint, params) {
469
659
  };
470
660
  }, [endpoint, query, reloadKey]);
471
661
  const reload = useCallback(() => setReloadKey((value) => value + 1), []);
472
- return { page, isLoading, error, reload };
662
+ return { page, pageQuery, isLoading, error, reload };
473
663
  }
474
664
  function useRemoteSearchSummary(endpoint, params) {
475
665
  const adapter = useSduiAdapter();
@@ -523,6 +713,8 @@ function summaryParams(params) {
523
713
  const next = new URLSearchParams(params);
524
714
  next.delete("page");
525
715
  next.delete("limit");
716
+ next.delete("sort");
717
+ next.delete("order");
526
718
  return next;
527
719
  }
528
720
  function plainValue(value) {
@@ -545,6 +737,9 @@ function SearchSummaryCards({ items, values, dispatch }) {
545
737
  }
546
738
  var OVERSCAN = 10;
547
739
  var CLIP_CSS = `.wp-table--clip tbody td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}`;
740
+ function isBlankCell(value) {
741
+ return value === null || value === void 0 || value === "";
742
+ }
548
743
  function ResultTable({
549
744
  columns,
550
745
  rows,
@@ -555,7 +750,8 @@ function ResultTable({
555
750
  empty,
556
751
  virtual = false,
557
752
  height = 560,
558
- rowHeight = 41
753
+ rowHeight = 41,
754
+ stackable = true
559
755
  }) {
560
756
  const scrollRef = useRef(null);
561
757
  const firstRowRef = useRef(null);
@@ -579,13 +775,14 @@ function ResultTable({
579
775
  const table = /* @__PURE__ */ jsxs(
580
776
  "table",
581
777
  {
582
- className: `wp-table wp-table--hover${virtual ? " wp-table--sticky wp-table--clip" : ""}`,
778
+ className: `wp-table wp-table--hover${virtual ? " wp-table--sticky wp-table--clip" : stackable ? " wp-table--stackable" : ""}`,
583
779
  style: virtual ? { tableLayout: "fixed" } : void 0,
584
780
  children: [
585
781
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: columns.map((column) => /* @__PURE__ */ jsx(
586
782
  "th",
587
783
  {
588
784
  style: { width: column.width, textAlign: column.align === "right" ? "right" : void 0 },
785
+ "data-mobile": column.mobile,
589
786
  children: column.sortable && onSort ? (
590
787
  // 인라인 style로는 :hover를 표현할 수 없다 — 색 피드백은 .wp-table__sort:hover가 낸다
591
788
  /* @__PURE__ */ jsxs("button", { type: "button", className: "wp-table__sort", onClick: () => onSort(column.key), children: [
@@ -619,10 +816,20 @@ function ResultTable({
619
816
  if (column.align === "right") cls.push("wp-table__num");
620
817
  if (column.mono) cls.push("wp-table__mono");
621
818
  if (column.strong) cls.push("wp-table__strong");
622
- return /* @__PURE__ */ jsx("td", { className: cls.join(" ") || void 0, children: column.render ? column.render(row, index) : (
819
+ const value = column.render ? column.render(row, index) : (
623
820
  // 평면 키 우선, 없으면 점 경로 — 셀 렌더러(cells.tsx)와 같은 조회 규칙이다
624
821
  fieldOf(row, column.key)
625
- ) }, column.key);
822
+ );
823
+ return /* @__PURE__ */ jsx(
824
+ "td",
825
+ {
826
+ className: cls.join(" ") || void 0,
827
+ "data-label": labelOf(column),
828
+ "data-mobile": column.mobile,
829
+ children: isBlankCell(value) ? /* @__PURE__ */ jsx("span", { style: { color: "var(--wp-text-subtle)" }, children: "\u2014" }) : value
830
+ },
831
+ column.key
832
+ );
626
833
  })
627
834
  },
628
835
  row.id ?? index
@@ -633,14 +840,33 @@ function ResultTable({
633
840
  ]
634
841
  }
635
842
  );
636
- if (!virtual) return table;
843
+ if (!virtual) {
844
+ return /* @__PURE__ */ jsx("div", { className: "wp-table-scroll", children: table });
845
+ }
637
846
  return /* @__PURE__ */ jsxs("div", { ref: scrollRef, onScroll: handleScroll, style: { height, overflowY: "auto" }, children: [
638
847
  /* @__PURE__ */ jsx(HoistedStyle, { id: "wp-table-clip", css: CLIP_CSS }),
639
848
  table
640
849
  ] });
641
850
  }
642
- function ResultEmptyState({ title }) {
643
- return /* @__PURE__ */ jsx(EmptyState, { icon: "scan-search", title: `${title}\uC774(\uAC00) \uC5C6\uC2B5\uB2C8\uB2E4`, tight: true, children: "\uAC80\uC0C9 \uC870\uAC74\uC744 \uBC14\uAFB8\uAC70\uB098 \uCD08\uAE30\uD654\uD574 \uBCF4\uC138\uC694." });
851
+ function labelOf(column) {
852
+ if (typeof column.label === "string") return column.label;
853
+ return typeof column.header === "string" ? column.header : "";
854
+ }
855
+ function ResultEmptyState({ title, filtered = false, emptyTitle, emptyText, scoped = false, onReset }) {
856
+ const name = String(title ?? "").trim() || "\uD56D\uBAA9";
857
+ if (filtered) {
858
+ return /* @__PURE__ */ jsx(
859
+ EmptyState,
860
+ {
861
+ icon: "scan-search",
862
+ title: `\uC870\uAC74\uC5D0 \uB9DE\uB294 ${name}${josaOf(name, "\uC774/\uAC00")} \uC5C6\uC2B5\uB2C8\uB2E4`,
863
+ tight: true,
864
+ actions: onReset ? /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", icon: "refresh-cw", onClick: onReset, children: "\uC870\uAC74 \uCD08\uAE30\uD654" }) : void 0,
865
+ children: "\uAC80\uC0C9\uC5B4\uB098 \uC870\uAC74\uC744 \uBC14\uAFD4 \uBCF4\uC138\uC694."
866
+ }
867
+ );
868
+ }
869
+ return /* @__PURE__ */ jsx(EmptyState, { title: emptyTitle || `${scoped ? "" : "\uC544\uC9C1 "}${name}${josaOf(name, "\uC774/\uAC00")} \uC5C6\uC2B5\uB2C8\uB2E4`, tight: true, children: emptyText || void 0 });
644
870
  }
645
871
  var EMPTY_FILTERS = [];
646
872
  var EMPTY_FIELDS = [];
@@ -649,6 +875,29 @@ var DEFAULT_VIEWS = [
649
875
  { value: "list", icon: "list", label: "\uBAA9\uB85D" },
650
876
  { value: "grid", icon: "layout-grid", label: "\uCE74\uB4DC" }
651
877
  ];
878
+ var VIEW_STORAGE_PREFIX = "wp-sdui:view:";
879
+ var viewMemory = /* @__PURE__ */ new Map();
880
+ function readStoredView(title) {
881
+ const inMemory = viewMemory.get(title);
882
+ if (inMemory) return inMemory;
883
+ try {
884
+ if (typeof window === "undefined") return null;
885
+ const stored = window.sessionStorage.getItem(VIEW_STORAGE_PREFIX + title);
886
+ return stored === "grid" || stored === "list" ? stored : null;
887
+ } catch {
888
+ return null;
889
+ }
890
+ }
891
+ function writeStoredView(title, view) {
892
+ viewMemory.set(title, view);
893
+ try {
894
+ if (typeof window !== "undefined") window.sessionStorage.setItem(VIEW_STORAGE_PREFIX + title, view);
895
+ } catch {
896
+ }
897
+ }
898
+ function looksLikeLocator(value) {
899
+ return /^(https?:)?\/\//.test(value) || value.startsWith("/") || value.startsWith("data:");
900
+ }
652
901
  function normalizePageSizes(sizes) {
653
902
  if (!Array.isArray(sizes)) return void 0;
654
903
  const clean = [...new Set(sizes.map(Number).filter((n) => Number.isFinite(n) && n > 0).map(Math.floor))].sort(
@@ -697,9 +946,15 @@ function SearchResults({
697
946
  hideHeader = false,
698
947
  advancedSearchFields: rawAdvancedSearchFields = EMPTY_FIELDS,
699
948
  searchPlaceholder,
700
- virtual
949
+ virtual,
950
+ serverUnsortable,
951
+ primaryKey,
952
+ emptyTitle,
953
+ emptyText,
954
+ serverFiltered = false
701
955
  }) {
702
956
  const hasActions = actions.length > 0;
957
+ const isMobile = useIsMobile();
703
958
  const advancedSearchFields = useMemo(
704
959
  () => normalizeSearchFields(rawAdvancedSearchFields),
705
960
  [rawAdvancedSearchFields]
@@ -715,12 +970,18 @@ function SearchResults({
715
970
  );
716
971
  const applyViewConstraints = (candidate) => viewItems.some((v) => v.value === candidate) ? candidate : viewItems[0].value;
717
972
  const appliedSearch = searchParams.get("search") ?? "";
718
- const [view, setView] = useState(() => applyViewConstraints(initialView));
973
+ const [chosenView, setChosenView] = useState(() => readStoredView(title));
719
974
  const [search, setSearch] = useState(appliedSearch);
720
- const [sort, setSort] = useState(null);
721
975
  const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
722
976
  const selectedRowsRef = useRef(/* @__PURE__ */ new Map());
723
- const activeView = applyViewConstraints(view);
977
+ const hasGridView = viewItems.some((v) => v.value === "grid");
978
+ const defaultView = isMobile && hasGridView ? "grid" : initialView;
979
+ const activeView = applyViewConstraints(chosenView ?? defaultView);
980
+ const chooseView = (next) => {
981
+ const view = applyViewConstraints(next);
982
+ writeStoredView(title, view);
983
+ setChosenView(view);
984
+ };
724
985
  useEffect(() => {
725
986
  setSearch(appliedSearch);
726
987
  }, [appliedSearch]);
@@ -737,33 +998,43 @@ function SearchResults({
737
998
  onParamsChange(next);
738
999
  };
739
1000
  const activeFilters = useMemo(() => {
740
- const entries = [];
1001
+ const entries2 = [];
741
1002
  for (const filter of [...leftFilters, ...rightFilters]) {
742
1003
  const values = splitMultiValue(searchParams.get(filter.key) ?? "");
743
- if (values.length > 0) entries.push({ key: filter.key, values });
1004
+ if (values.length > 0) entries2.push({ key: filter.key, values });
744
1005
  }
745
1006
  if (tabs && typeof tabs.key === "string") {
746
1007
  const values = splitMultiValue(searchParams.get(tabs.key) ?? "");
747
- if (values.length > 0) entries.push({ key: tabs.key, values });
1008
+ if (values.length > 0) entries2.push({ key: tabs.key, values });
748
1009
  }
749
- return entries;
1010
+ return entries2;
750
1011
  }, [leftFilters, rightFilters, tabs, searchParams]);
751
1012
  const hasAllRows = item.data.length >= item.total;
1013
+ const sortAllowed = (key) => hasAllRows || !(serverUnsortable === "all" || Array.isArray(serverUnsortable) && serverUnsortable.includes(key));
1014
+ const requestedSort = sortFromParams(searchParams);
1015
+ const sortKey = requestedSort && sortAllowed(requestedSort.key) ? requestedSort.key : null;
1016
+ const sortDir = requestedSort?.dir ?? "asc";
1017
+ const sort = useMemo(() => sortKey ? { key: sortKey, dir: sortDir } : null, [sortKey, sortDir]);
752
1018
  const processed = useMemo(() => {
753
1019
  const cell = (row, key) => fieldOf(row, key);
754
1020
  let rows = item.data;
755
- if (hasAllRows) {
1021
+ if (hasAllRows && !serverFiltered) {
1022
+ const source = item.data;
1023
+ const known = (key) => source.some((row) => cell(row, key) !== void 0);
756
1024
  if (appliedSearch) {
757
1025
  rows = rows.filter((row) => includesLooseSearch(localSearchText(row), appliedSearch));
758
1026
  }
759
1027
  for (const { key, values } of activeFilters) {
1028
+ if (!known(key)) continue;
760
1029
  rows = rows.filter((row) => values.includes(String(cell(row, key))));
761
1030
  }
762
1031
  for (const field of advancedSearchFields) {
763
1032
  if (field.type === "select") {
1033
+ if (!known(field.key)) continue;
764
1034
  const wanted = splitMultiValue(advancedValues[field.key] ?? "");
765
1035
  if (wanted.length > 0) rows = rows.filter((row) => wanted.includes(String(cell(row, field.key))));
766
1036
  } else if (field.type === "cascading") {
1037
+ if (!known(field.key)) continue;
767
1038
  const wanted = splitMultiValue(advancedValues[field.key] ?? "");
768
1039
  if (wanted.length > 0) {
769
1040
  const lineages = cascadingValuePaths(field.options);
@@ -774,12 +1045,14 @@ function SearchResults({
774
1045
  });
775
1046
  }
776
1047
  } else if (field.type === "dateRange") {
1048
+ if (!known(field.key)) continue;
777
1049
  const from = advancedValues[field.fromKey ?? `${field.key}From`];
778
1050
  const to = advancedValues[field.toKey ?? `${field.key}To`];
779
- const datePart = (v) => String(v ?? "").slice(0, 10);
1051
+ const datePart = (v) => isZonedIsoDateTime(v) ? formatDateValue(v, "date") : String(v ?? "").slice(0, 10);
780
1052
  if (from) rows = rows.filter((row) => datePart(cell(row, field.key)) >= from.slice(0, 10));
781
1053
  if (to) rows = rows.filter((row) => datePart(cell(row, field.key)) <= to.slice(0, 10));
782
1054
  } else if (field.type === "numberRange") {
1055
+ if (!known(field.key)) continue;
783
1056
  const min = numericValue(advancedValues[field.fromKey ?? `${field.key}Min`]);
784
1057
  const max = numericValue(advancedValues[field.toKey ?? `${field.key}Max`]);
785
1058
  if (min !== null) rows = rows.filter((row) => (numericValue(cell(row, field.key)) ?? -Infinity) >= min);
@@ -788,11 +1061,13 @@ function SearchResults({
788
1061
  const keyword = advancedValues[field.key];
789
1062
  const suffix = ["_contains", "Contains"].find((s) => field.key.endsWith(s));
790
1063
  const baseKey = suffix ? field.key.slice(0, -suffix.length) : field.key;
791
- if (keyword) rows = rows.filter((row) => includesLooseSearch(String(cell(row, baseKey) ?? ""), keyword));
1064
+ if (keyword && known(baseKey)) {
1065
+ rows = rows.filter((row) => includesLooseSearch(String(cell(row, baseKey) ?? ""), keyword));
1066
+ }
792
1067
  }
793
1068
  }
794
1069
  }
795
- if (sort) {
1070
+ if (sort && hasAllRows) {
796
1071
  const column = columns.find((c) => c.key === sort.key);
797
1072
  const dirFactor = sort.dir === "asc" ? 1 : -1;
798
1073
  rows = [...rows].sort((a, b) => {
@@ -807,7 +1082,7 @@ function SearchResults({
807
1082
  });
808
1083
  }
809
1084
  return rows;
810
- }, [item.data, hasAllRows, appliedSearch, activeFilters, advancedSearchFields, advancedValues, sort, columns]);
1085
+ }, [item.data, hasAllRows, serverFiltered, appliedSearch, activeFilters, advancedSearchFields, advancedValues, sort, columns]);
811
1086
  const clientPaginated = hasAllRows;
812
1087
  const totalCount = clientPaginated ? processed.length : item.total;
813
1088
  const pageCount = Math.max(1, Math.ceil(totalCount / pageSize));
@@ -839,7 +1114,50 @@ function SearchResults({
839
1114
  });
840
1115
  };
841
1116
  const selectedItems = Array.from(selected).map((id) => selectedRowsRef.current.get(id)).filter((row) => row !== void 0);
1117
+ const paramsKey = searchParams.toString();
1118
+ const seenRef = useRef(null);
1119
+ const answersRef = useRef(/* @__PURE__ */ new Map());
1120
+ useEffect(() => {
1121
+ const settle = (key, data) => {
1122
+ const answers = answersRef.current;
1123
+ const nextIds = new Set(data.map((row) => row.id));
1124
+ const prev = answers.get(key);
1125
+ answers.delete(key);
1126
+ answers.set(key, nextIds);
1127
+ if (answers.size > 64) answers.delete(answers.keys().next().value);
1128
+ if (!prev) return;
1129
+ const vanished = [...prev].filter((id) => !nextIds.has(id));
1130
+ if (vanished.length === 0) return;
1131
+ vanished.forEach((id) => selectedRowsRef.current.delete(id));
1132
+ setSelected((current) => {
1133
+ let changed = false;
1134
+ const next = new Set(current);
1135
+ for (const id of vanished) if (next.delete(id)) changed = true;
1136
+ return changed ? next : current;
1137
+ });
1138
+ };
1139
+ const seen = seenRef.current;
1140
+ seenRef.current = { key: paramsKey, data: item.data };
1141
+ if (!seen) {
1142
+ settle(paramsKey, item.data);
1143
+ return;
1144
+ }
1145
+ if (seen.data === item.data) return;
1146
+ settle(paramsKey, item.data);
1147
+ }, [paramsKey, item.data]);
842
1148
  const ratioSum = columns.reduce((sum, c) => sum + (c.ratio ?? 0), 0);
1149
+ const titleColumnKey = columns.find((c) => c.mobile === "title")?.key ?? (primaryKey && columns.some((c) => c.key === primaryKey) ? primaryKey : void 0) ?? columns.find((c) => c.strong && c.mobile !== "hide")?.key;
1150
+ const rowLabelOf = (row) => {
1151
+ const keys = [primaryKey, titleColumnKey, ...columns.map((c) => c.key)].filter(
1152
+ (key) => typeof key === "string" && key.length > 0
1153
+ );
1154
+ for (const key of keys) {
1155
+ const value = fieldOf(row, key);
1156
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
1157
+ if (typeof value === "string" && value.trim() && !looksLikeLocator(value.trim())) return value.trim();
1158
+ }
1159
+ return "\uD589";
1160
+ };
843
1161
  const tableColumns = [
844
1162
  ...hasActions ? [
845
1163
  {
@@ -854,6 +1172,7 @@ function SearchResults({
854
1172
  }
855
1173
  ),
856
1174
  width: "36px",
1175
+ label: "",
857
1176
  render: (row) => (
858
1177
  // 셀 어디를 눌러도 행 클릭(onRowClick)으로 번지지 않게 래퍼에서 전파를 끊는다
859
1178
  /* @__PURE__ */ jsx("span", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
@@ -861,7 +1180,7 @@ function SearchResults({
861
1180
  {
862
1181
  checked: selected.has(row.id),
863
1182
  onChange: (e) => toggleSelect(row, e.target.checked),
864
- "aria-label": `${row.id} \uC120\uD0DD`
1183
+ "aria-label": `${rowLabelOf(row)} \uC120\uD0DD`
865
1184
  }
866
1185
  ) })
867
1186
  )
@@ -870,7 +1189,9 @@ function SearchResults({
870
1189
  ...columns.map((column) => ({
871
1190
  key: column.key,
872
1191
  header: column.title,
873
- sortable: column.sortable,
1192
+ label: column.title,
1193
+ mobile: column.mobile ?? (column.key === titleColumnKey ? "title" : void 0),
1194
+ sortable: column.sortable && sortAllowed(column.key),
874
1195
  align: column.textAlign === "right" ? "right" : "left",
875
1196
  mono: column.mono,
876
1197
  strong: column.strong,
@@ -879,9 +1200,51 @@ function SearchResults({
879
1200
  }))
880
1201
  ];
881
1202
  const handleSort = (key) => {
882
- setSort((prev) => prev?.key === key ? prev.dir === "asc" ? { key, dir: "desc" } : null : { key, dir: "asc" });
1203
+ const next = nextSort(sort, key);
1204
+ updateParams({ [SORT_PARAM]: next?.key ?? null, [ORDER_PARAM]: next?.dir ?? null, page: null });
883
1205
  };
884
- const empty = /* @__PURE__ */ jsx(ResultEmptyState, { title });
1206
+ const sortChoices = columns.filter((column) => column.sortable && sortAllowed(column.key));
1207
+ const mobileSort = isMobile && sortChoices.length > 0 ? /* @__PURE__ */ jsx(
1208
+ Select,
1209
+ {
1210
+ size: "sm",
1211
+ "aria-label": `${title} \uC815\uB82C`,
1212
+ value: sort ? `${sort.key}:${sort.dir}` : "",
1213
+ options: [
1214
+ { value: "", label: "\uAE30\uBCF8 \uC815\uB82C" },
1215
+ ...sortChoices.flatMap((column) => [
1216
+ { value: `${column.key}:asc`, label: `${column.title} \uC624\uB984\uCC28\uC21C` },
1217
+ { value: `${column.key}:desc`, label: `${column.title} \uB0B4\uB9BC\uCC28\uC21C` }
1218
+ ])
1219
+ ],
1220
+ onChange: (e) => {
1221
+ const cut = e.target.value.lastIndexOf(":");
1222
+ const key = cut > 0 ? e.target.value.slice(0, cut) : "";
1223
+ const dir = e.target.value.slice(cut + 1) === "desc" ? "desc" : "asc";
1224
+ updateParams({ [SORT_PARAM]: key || null, [ORDER_PARAM]: key ? dir : null, page: null });
1225
+ }
1226
+ }
1227
+ ) : null;
1228
+ const chipKeys = [...leftFilters, ...rightFilters].map((filter) => filter.key);
1229
+ const filtered = Boolean(appliedSearch) || Object.values(advancedValues).some((value) => Boolean(value)) || chipKeys.some((key) => Boolean(searchParams.get(key)));
1230
+ const scoped = Boolean(tabs && typeof tabs.key === "string" && searchParams.get(tabs.key));
1231
+ const resetConditions = () => {
1232
+ const patch = { search: null, page: null };
1233
+ for (const key of searchFieldKeys(advancedSearchFields)) patch[key] = null;
1234
+ for (const key of chipKeys) patch[key] = null;
1235
+ updateParams(patch);
1236
+ };
1237
+ const empty = /* @__PURE__ */ jsx(
1238
+ ResultEmptyState,
1239
+ {
1240
+ title,
1241
+ filtered,
1242
+ scoped,
1243
+ emptyTitle,
1244
+ emptyText,
1245
+ onReset: resetConditions
1246
+ }
1247
+ );
885
1248
  return /* @__PURE__ */ jsxs("div", { className: ["wp-col", className].filter(Boolean).join(" "), style, "aria-label": title, children: [
886
1249
  summary ?? null,
887
1250
  tabs && Array.isArray(tabs.items) && tabs.items.length > 0 ? /* @__PURE__ */ jsx(
@@ -921,9 +1284,10 @@ function SearchResults({
921
1284
  {
922
1285
  items: viewItems,
923
1286
  value: activeView,
924
- onChange: (next) => setView(applyViewConstraints(next))
1287
+ onChange: (next) => chooseView(next)
925
1288
  }
926
1289
  ) : null,
1290
+ mobileSort,
927
1291
  headerActions
928
1292
  ] })
929
1293
  ] }) : null,
@@ -1004,7 +1368,7 @@ function SearchResults({
1004
1368
  virtual: virtualListActive
1005
1369
  }
1006
1370
  ) : visibleItems.length === 0 ? empty : /* @__PURE__ */ jsx("div", { className: "wp-grid wp-grid--3", children: visibleItems.map((row) => /* @__PURE__ */ jsx(GridCard, { item: row, config: gridConfig, onClick: onRowClick }, row.id)) }),
1007
- virtualListActive && clientPaginated ? null : /* @__PURE__ */ jsxs("div", { className: "wp-row wp-row--between", children: [
1371
+ virtualListActive && clientPaginated || totalCount === 0 && !isLoading ? null : /* @__PURE__ */ jsxs("div", { className: "wp-row wp-row--between", children: [
1008
1372
  /* @__PURE__ */ jsxs("div", { className: "wp-row", style: { flex: "none" }, children: [
1009
1373
  /* @__PURE__ */ jsx("span", { className: "wp-caption", style: { whiteSpace: "nowrap" }, children: "\uD398\uC774\uC9C0\uB2F9" }),
1010
1374
  /* @__PURE__ */ jsx(
@@ -1046,6 +1410,9 @@ function formatCell(value) {
1046
1410
  if (value instanceof Date) return formatDateCellValue(value);
1047
1411
  return value;
1048
1412
  }
1413
+ function blankAsDash(value) {
1414
+ return value === null || value === void 0 || value === "" ? /* @__PURE__ */ jsx("span", { style: { color: "var(--wp-text-subtle)" }, children: "\u2014" }) : value;
1415
+ }
1049
1416
  function resolveRowPart(part, item) {
1050
1417
  return typeof part === "function" ? part(item) : part;
1051
1418
  }
@@ -1061,7 +1428,7 @@ function GridCard({
1061
1428
  return /* @__PURE__ */ jsxs("div", { className: "wp-row", children: [
1062
1429
  resolveRowPart(row.icon, item),
1063
1430
  row.label != null ? /* @__PURE__ */ jsx("span", { className: "wp-caption", children: resolveRowPart(row.label, item) }) : null,
1064
- /* @__PURE__ */ jsx("span", { className: "wp-body-sm", children: resolveRowPart(row.value, item) })
1431
+ /* @__PURE__ */ jsx("span", { className: "wp-body-sm", children: blankAsDash(resolveRowPart(row.value, item)) })
1065
1432
  ] }, idx);
1066
1433
  }) });
1067
1434
  const note = config.note?.(item);
@@ -1130,6 +1497,22 @@ function FilterMultiSelect({
1130
1497
  function toList(action) {
1131
1498
  return Array.isArray(action) ? action : [action];
1132
1499
  }
1500
+ function conditionParams(params) {
1501
+ const next = new URLSearchParams(params);
1502
+ next.delete("page");
1503
+ next.delete("limit");
1504
+ return next.toString();
1505
+ }
1506
+ function useQueryStateMirror(dispatch, key, params) {
1507
+ const value = conditionParams(params);
1508
+ const lastRef = useRef(null);
1509
+ useEffect(() => {
1510
+ if (typeof key !== "string" || key === "") return;
1511
+ if (lastRef.current === value) return;
1512
+ lastRef.current = value;
1513
+ dispatch({ action: "setState", key, value });
1514
+ }, [dispatch, key, value]);
1515
+ }
1133
1516
  function mapColumns(columns, dispatch) {
1134
1517
  return (columns ?? []).map((column) => ({
1135
1518
  key: column.key,
@@ -1139,6 +1522,7 @@ function mapColumns(columns, dispatch) {
1139
1522
  ratio: column.ratio,
1140
1523
  mono: column.mono,
1141
1524
  strong: column.strong,
1525
+ mobile: column.mobile === "title" || column.mobile === "hide" ? column.mobile : void 0,
1142
1526
  render: cellRenderer(column.cell, column.key, dispatch)
1143
1527
  }));
1144
1528
  }
@@ -1240,11 +1624,15 @@ function SduiSearchResultsImpl({
1240
1624
  searchPlaceholder,
1241
1625
  rowClick,
1242
1626
  headerActions,
1243
- virtual
1627
+ virtual,
1628
+ emptyTitle,
1629
+ emptyText,
1630
+ queryStateKey
1244
1631
  }) {
1245
1632
  const dispatch = useSduiDispatch();
1246
1633
  const [params, setParams] = useState(() => new URLSearchParams());
1247
1634
  const rows = Array.isArray(data) ? data : [];
1635
+ useQueryStateMirror(dispatch, queryStateKey, params);
1248
1636
  return /* @__PURE__ */ jsx(
1249
1637
  SearchResults,
1250
1638
  {
@@ -1265,6 +1653,9 @@ function SduiSearchResultsImpl({
1265
1653
  searchPlaceholder,
1266
1654
  headerActions,
1267
1655
  virtual,
1656
+ primaryKey: gridCard?.titleKey,
1657
+ emptyTitle,
1658
+ emptyText,
1268
1659
  onRowClick: rowClick ? (row) => toList(rowClick).forEach((a) => dispatch(a, row)) : void 0,
1269
1660
  searchParams: params,
1270
1661
  onParamsChange: setParams
@@ -1293,7 +1684,10 @@ function RemoteSearchResultsImpl({
1293
1684
  pageSize,
1294
1685
  searchParams: controlledParams,
1295
1686
  onParamsChange,
1296
- summary
1687
+ summary,
1688
+ emptyTitle,
1689
+ emptyText,
1690
+ queryStateKey
1297
1691
  }) {
1298
1692
  const dispatch = useSduiDispatch();
1299
1693
  const defaultPageSize = normalizePageSizes(pageSizes)?.[0] ?? 10;
@@ -1313,8 +1707,18 @@ function RemoteSearchResultsImpl({
1313
1707
  if (!next.has("limit")) next.set("limit", String(defaultPageSize));
1314
1708
  return next;
1315
1709
  }, [params, defaultPageSize]);
1316
- const { page, isLoading, error, reload } = useRemoteSearchPage(endpoint, query);
1710
+ const { page, pageQuery, isLoading, error, reload } = useRemoteSearchPage(endpoint, query);
1711
+ const requestedSort = sortFromParams(params);
1712
+ const queryString = query.toString();
1713
+ const settledForQuery = pageQuery === queryString;
1714
+ useEffect(() => {
1715
+ if (!requestedSort || !settledForQuery || page.data.length >= page.total) return;
1716
+ if (!serverSortMemory.record(endpoint, requestedSort, page.sort ?? null)) {
1717
+ handleParamsChange(withoutSort(params));
1718
+ }
1719
+ }, [page, settledForQuery, endpoint, requestedSort?.key, requestedSort?.dir]);
1317
1720
  const summaryQuery = useMemo(() => summaryParams(params), [params]);
1721
+ useQueryStateMirror(dispatch, queryStateKey, params);
1318
1722
  const summaryState = useRemoteSearchSummary(
1319
1723
  summary && typeof summary.endpoint === "string" ? summary.endpoint : null,
1320
1724
  summaryQuery
@@ -1351,6 +1755,11 @@ function RemoteSearchResultsImpl({
1351
1755
  headerActions,
1352
1756
  virtual,
1353
1757
  isLoading,
1758
+ serverUnsortable: serverSortMemory.unsortable(endpoint),
1759
+ serverFiltered: true,
1760
+ primaryKey: gridCard?.titleKey,
1761
+ emptyTitle,
1762
+ emptyText,
1354
1763
  summary: summary ? /* @__PURE__ */ jsx(SearchSummaryCards, { items: summary.items, values: summaryState.values, dispatch }) : void 0,
1355
1764
  onRowClick: rowClick ? (row) => toList(rowClick).forEach((a) => dispatch(a, row)) : void 0,
1356
1765
  searchParams: params,
@@ -1360,12 +1769,6 @@ function RemoteSearchResultsImpl({
1360
1769
  }
1361
1770
  var SduiSearchResults = boxed(SduiSearchResultsImpl, "SduiSearchResults");
1362
1771
  var RemoteSearchResults = boxed(RemoteSearchResultsImpl, "RemoteSearchResults");
1363
- var SduiRuntimeContext = createContext(null);
1364
- function useSduiRuntime() {
1365
- const runtime = useContext(SduiRuntimeContext);
1366
- if (!runtime) throw new Error("useSduiRuntime\uC740 SduiRenderer \uB0B4\uBD80\uC5D0\uC11C\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
1367
- return runtime;
1368
- }
1369
1772
  function prefillParams(search, hash) {
1370
1773
  const params = new URLSearchParams(search);
1371
1774
  const q = hash.indexOf("?");
@@ -1402,21 +1805,21 @@ function SduiForm({
1402
1805
  return JSON.stringify(keys.map((key) => [key, state[key]]));
1403
1806
  };
1404
1807
  useEffect(() => {
1405
- const entries = {};
1808
+ const entries2 = {};
1406
1809
  for (const [field, value] of Object.entries(initial ?? {})) {
1407
- if (runtime.state[`${id}.${field}`] === void 0) entries[`${id}.${field}`] = value;
1810
+ if (runtime.state[`${id}.${field}`] === void 0) entries2[`${id}.${field}`] = value;
1408
1811
  }
1409
1812
  if (initialFromUrl && typeof window !== "undefined") {
1410
1813
  const params = prefillParams(window.location.search, window.location.hash);
1411
1814
  for (const [field, param] of Object.entries(initialFromUrl)) {
1412
1815
  const raw = params.get(param);
1413
1816
  if (raw !== null && runtime.state[`${id}.${field}`] === void 0) {
1414
- entries[`${id}.${field}`] = raw;
1817
+ entries2[`${id}.${field}`] = raw;
1415
1818
  }
1416
1819
  }
1417
1820
  }
1418
- if (Object.keys(entries).length > 0) runtime.setEntries(entries);
1419
- initialSnapshot.current = sliceOf({ ...runtime.state, ...entries });
1821
+ if (Object.keys(entries2).length > 0) runtime.setEntries(entries2);
1822
+ initialSnapshot.current = sliceOf({ ...runtime.state, ...entries2 });
1420
1823
  }, [id]);
1421
1824
  useEffect(() => {
1422
1825
  return () => {
@@ -1437,6 +1840,25 @@ function SduiForm({
1437
1840
  }
1438
1841
  }, [slice, id, runtime]);
1439
1842
  const dirty = Boolean(runtime.state[`${id}.$dirty`]);
1843
+ const latestState = useRef(runtime.state);
1844
+ latestState.current = runtime.state;
1845
+ const dirtyRegistry = useContext(SduiDirtyRegistryContext);
1846
+ useEffect(
1847
+ () => dirtyRegistry?.register(
1848
+ () => initialSnapshot.current !== null && sliceOf(latestState.current) !== initialSnapshot.current
1849
+ ),
1850
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1851
+ [dirtyRegistry, id]
1852
+ );
1853
+ const leaveRegistry = useContext(SduiLeaveRegistryContext);
1854
+ useEffect(() => {
1855
+ if (!guard || !leaveRegistry) return;
1856
+ return leaveRegistry.register(
1857
+ id,
1858
+ () => initialSnapshot.current !== null && sliceOf(latestState.current) !== initialSnapshot.current
1859
+ );
1860
+ }, [guard, leaveRegistry, id]);
1861
+ const scope = useMemo(() => ({ id, rules: JSON.parse(rulesJson) }), [id, rulesJson]);
1440
1862
  useEffect(() => {
1441
1863
  if (!guard || !dirty) return;
1442
1864
  const onBeforeUnload = (event) => {
@@ -1451,12 +1873,13 @@ function SduiForm({
1451
1873
  {
1452
1874
  className: ["wp-col", className].filter(Boolean).join(" "),
1453
1875
  style,
1876
+ "data-sdui-form": id,
1454
1877
  onSubmit: (event) => {
1455
1878
  event.preventDefault();
1456
1879
  onSubmit?.(event);
1457
1880
  },
1458
1881
  noValidate: true,
1459
- children
1882
+ children: /* @__PURE__ */ jsx(SduiFormScopeContext.Provider, { value: scope, children })
1460
1883
  }
1461
1884
  );
1462
1885
  }
@@ -1798,6 +2221,9 @@ function withProps(Component, defaults) {
1798
2221
  function deferChainEntry(container, key) {
1799
2222
  return typeof container.action === "string" && (key === "onSuccess" || key === "onError");
1800
2223
  }
2224
+ function deferEachTarget(container, key) {
2225
+ return container.action === "request" && container.each !== void 0 && (key === "endpoint" || key === "body");
2226
+ }
1801
2227
  function failureDetailOf(error) {
1802
2228
  const message = error instanceof Error ? error.message : "\uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4";
1803
2229
  const carrier = error;
@@ -1817,12 +2243,14 @@ function runSuccessChain(kind, id, run) {
1817
2243
  );
1818
2244
  }
1819
2245
  }
2246
+ var EMPTY_ENDPOINT_MESSAGE = "request \uC561\uC158\uC758 endpoint\uAC00 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4 \u2014 $state \uCC38\uC870\uC5D0 \uC544\uC9C1 \uAC12\uC774 \uC5C6\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694";
1820
2247
  async function runRequestAction(action, deps) {
2248
+ if (action.each !== void 0) return runRequestEach(action, deps);
1821
2249
  const pendingKey = typeof action.pendingKey === "string" && action.pendingKey !== "" ? action.pendingKey : null;
1822
2250
  let pendingOn = false;
1823
2251
  try {
1824
2252
  if (typeof action.endpoint !== "string" || action.endpoint === "") {
1825
- throw new Error("request \uC561\uC158\uC758 endpoint\uAC00 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4 \u2014 $state \uCC38\uC870\uC5D0 \uC544\uC9C1 \uAC12\uC774 \uC5C6\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694");
2253
+ throw new Error(EMPTY_ENDPOINT_MESSAGE);
1826
2254
  }
1827
2255
  if (pendingKey) {
1828
2256
  pendingOn = true;
@@ -1856,6 +2284,91 @@ async function runRequestAction(action, deps) {
1856
2284
  if (pendingKey && pendingOn) deps.dispatch({ action: "setState", key: pendingKey, value: false });
1857
2285
  }
1858
2286
  }
2287
+ var REQUEST_EACH_CONCURRENCY = 3;
2288
+ async function mapWithLimit(items, limit, fn) {
2289
+ const results = new Array(items.length);
2290
+ let next = 0;
2291
+ const worker = async () => {
2292
+ while (next < items.length) {
2293
+ const index = next;
2294
+ next += 1;
2295
+ results[index] = await fn(items[index], index);
2296
+ }
2297
+ };
2298
+ const width = Math.max(1, Math.min(limit, items.length));
2299
+ await Promise.all(Array.from({ length: width }, () => worker()));
2300
+ return results;
2301
+ }
2302
+ function eachFailureMessage(total, failed, first) {
2303
+ const head = `${total}\uAC74 \uC911 ${failed}\uAC74 \uC2E4\uD328`;
2304
+ return first ? `${head} \u2014 ${first}` : head;
2305
+ }
2306
+ async function runRequestEach(action, deps) {
2307
+ const list = action.each;
2308
+ if (!Array.isArray(list) || list.length === 0) {
2309
+ warnOnce(
2310
+ "request:each:empty",
2311
+ "[sdui] request \uC561\uC158\uC758 each \uAC00 \uBC30\uC5F4\uC774 \uC544\uB2C8\uAC70\uB098 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4 \u2014 \uC694\uCCAD\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 (\uBC84\uD2BC\uC744 \uC120\uD0DD \uC218\uB85C \uC7A0\uADF8\uC138\uC694)",
2312
+ "warn"
2313
+ );
2314
+ return;
2315
+ }
2316
+ const pendingKey = typeof action.pendingKey === "string" && action.pendingKey !== "" ? action.pendingKey : null;
2317
+ const resolveFor = deps.resolveFor ?? ((value) => value);
2318
+ const method = action.method ?? "POST";
2319
+ if (pendingKey) deps.dispatch({ action: "setState", key: pendingKey, value: true });
2320
+ try {
2321
+ const outcomes = await mapWithLimit(list, REQUEST_EACH_CONCURRENCY, async (item, index) => {
2322
+ try {
2323
+ const endpoint = resolveFor(action.endpoint, item, index);
2324
+ if (typeof endpoint !== "string" || endpoint === "") throw new Error(EMPTY_ENDPOINT_MESSAGE);
2325
+ const body = action.body === void 0 ? void 0 : resolveFor(action.body, item, index);
2326
+ const value = await deps.fetch(endpoint, { method, ...body === void 0 ? {} : { body } });
2327
+ return { ok: true, value };
2328
+ } catch (error) {
2329
+ return { ok: false, error };
2330
+ }
2331
+ });
2332
+ const errors = [];
2333
+ outcomes.forEach((outcome, index) => {
2334
+ if (!outcome.ok) errors.push({ index, item: list[index], ...failureDetailOf(outcome.error) });
2335
+ });
2336
+ const succeeded = outcomes.length - errors.length;
2337
+ const invalidate = typeof action.invalidate === "string" && action.invalidate !== "" ? action.invalidate : null;
2338
+ if (errors.length === 0) {
2339
+ runSuccessChain("request", `each:${list.length}`, () => {
2340
+ if (invalidate) deps.dispatch({ action: "invalidate", prefix: invalidate });
2341
+ const responses = outcomes.map((outcome) => outcome.ok ? outcome.value : null);
2342
+ (action.onSuccess ?? []).forEach((a) => deps.dispatch(a, responses));
2343
+ });
2344
+ return;
2345
+ }
2346
+ if (succeeded > 0 && invalidate) {
2347
+ runSuccessChain("request", `each:${list.length}`, () => deps.dispatch({ action: "invalidate", prefix: invalidate }));
2348
+ }
2349
+ const failure = {
2350
+ failed: errors.length,
2351
+ succeeded,
2352
+ total: list.length,
2353
+ message: eachFailureMessage(list.length, errors.length, errors[0]?.message),
2354
+ errors
2355
+ };
2356
+ if (typeof action.errorKey === "string" && action.errorKey !== "") {
2357
+ deps.dispatch({ action: "setState", key: action.errorKey, value: failure });
2358
+ }
2359
+ if (action.onError?.length) {
2360
+ action.onError.forEach((a) => deps.dispatch(a, failure));
2361
+ } else {
2362
+ deps.toast({
2363
+ tone: "danger",
2364
+ title: `${list.length}\uAC74 \uC911 ${errors.length}\uAC74 \uC2E4\uD328`,
2365
+ message: errors[0]?.message
2366
+ });
2367
+ }
2368
+ } finally {
2369
+ if (pendingKey) deps.dispatch({ action: "setState", key: pendingKey, value: false });
2370
+ }
2371
+ }
1859
2372
 
1860
2373
  // src/state-path.ts
1861
2374
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
@@ -2292,6 +2805,33 @@ function nearestArgsFrame(scope) {
2292
2805
  }
2293
2806
  return void 0;
2294
2807
  }
2808
+ function toastText(value) {
2809
+ if (typeof value === "string") return value;
2810
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
2811
+ if (isPlainObject(value) && typeof value.message === "string") return value.message;
2812
+ return void 0;
2813
+ }
2814
+ var DEFAULT_LEAVE_MESSAGE = "\uC800\uC7A5\uD558\uC9C0 \uC54A\uC740 \uBCC0\uACBD \uC0AC\uD56D\uC774 \uC788\uC2B5\uB2C8\uB2E4. \uC774 \uD654\uBA74\uC744 \uB5A0\uB098\uBA74 \uC0AC\uB77C\uC9D1\uB2C8\uB2E4.";
2815
+ function leaveMessageOf(value) {
2816
+ if (typeof value === "string") return value.trim() === "" ? null : value;
2817
+ return value ? DEFAULT_LEAVE_MESSAGE : null;
2818
+ }
2819
+ function titleText(value) {
2820
+ if (typeof value === "string") return value.trim() === "" ? null : value;
2821
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
2822
+ return null;
2823
+ }
2824
+ function collectFormValues(state, formId) {
2825
+ const prefix = `${formId}.`;
2826
+ const values = {};
2827
+ for (const [key, value] of Object.entries(state)) {
2828
+ if (key.startsWith(prefix) && !key.slice(prefix.length).startsWith("$") && value !== void 0) {
2829
+ values[key.slice(prefix.length)] = value;
2830
+ }
2831
+ }
2832
+ return values;
2833
+ }
2834
+ var TOAST_MODAL_CSS = ".wp-toast-region.wp-toast-region--modal{top:16px;bottom:auto}@media screen and (max-width:767.98px){.wp-toast-region.wp-toast-region--modal{top:calc(12px + env(safe-area-inset-top));left:12px;right:12px}}";
2295
2835
  function hasExplicitPageHead(body) {
2296
2836
  return Array.isArray(body) && body.some((child) => isPlainObject(child) && child.component === "PageHead");
2297
2837
  }
@@ -2369,45 +2909,76 @@ function toIndex(v) {
2369
2909
  function inRange(index, length) {
2370
2910
  return index !== null && index >= 0 && index < length;
2371
2911
  }
2372
- function validateForm(values, config, customValidators) {
2373
- const errors = {};
2374
- if (!config) return errors;
2912
+ var CHOICE_KINDS = /* @__PURE__ */ new Set([
2913
+ "Select",
2914
+ "AutocompleteField",
2915
+ "PositionSelect",
2916
+ "TagMultiSelect",
2917
+ "TreePicker",
2918
+ "SearchableMultiPicker",
2919
+ "RoleMemberPicker",
2920
+ "SegmentedControl",
2921
+ "Radio",
2922
+ "DatePicker",
2923
+ "DateRangePicker",
2924
+ "TimePicker"
2925
+ ]);
2926
+ function requiredVerb(kind) {
2927
+ if (kind && CHOICE_KINDS.has(kind)) return "\uC120\uD0DD\uD558\uC138\uC694";
2928
+ if (kind === "FileDropzone") return "\uCCA8\uBD80\uD558\uC138\uC694";
2929
+ if (kind === "Checkbox" || kind === "Switch") return "\uD655\uC778\uD558\uC138\uC694";
2930
+ return "\uC785\uB825\uD558\uC138\uC694";
2931
+ }
2932
+ function validateForm(values, config, customValidators, fieldInfo) {
2933
+ const out = { errors: {}, failed: {} };
2934
+ if (!config) return out;
2935
+ const labelOf2 = (field) => config.labels[field] ?? fieldInfo?.get(field)?.label;
2375
2936
  for (const [field, rules] of Object.entries(config.rules)) {
2376
2937
  const value = values[field];
2377
- const label = config.labels[field] ?? field;
2938
+ const context = {
2939
+ label: labelOf2(field),
2940
+ kind: fieldInfo?.get(field)?.kind,
2941
+ labelOf: labelOf2
2942
+ };
2378
2943
  for (const rule of rules) {
2379
- const message = ruleError(rule, value, values, label, customValidators);
2944
+ const message = ruleError(rule, value, values, context, customValidators);
2380
2945
  if (message) {
2381
- errors[field] = message;
2946
+ out.errors[field] = message;
2947
+ out.failed[field] = rule.rule;
2382
2948
  break;
2383
2949
  }
2384
2950
  }
2385
2951
  }
2386
- return errors;
2952
+ return out;
2387
2953
  }
2388
- function ruleError(rule, value, values, label, customValidators) {
2954
+ function ruleError(rule, value, values, { label, kind, labelOf: labelOf2 }, customValidators) {
2389
2955
  const empty = value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0 || // DateRangePicker의 {} 같은 "값 없는 객체"도 미입력으로 본다
2390
2956
  typeof value === "object" && !Array.isArray(value) && Object.values(value).every((v) => v === void 0 || v === null || v === "");
2957
+ const subject = (pair) => label ? `${josa(label, pair)} ` : "";
2391
2958
  switch (rule.rule) {
2392
2959
  case "required":
2393
- return empty || value === false ? rule.message ?? `${label}\uC744(\uB97C) \uC785\uB825\uD558\uC138\uC694` : null;
2960
+ return empty || value === false ? rule.message ?? (label ? `${josa(label, "\uC744/\uB97C")} ${requiredVerb(kind)}` : "\uD544\uC218 \uD56D\uBAA9\uC785\uB2C8\uB2E4") : null;
2394
2961
  case "min":
2395
- return !empty && toNumber(value) < rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value} \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4` : null;
2962
+ return !empty && toNumber(value) < rule.value ? rule.message ?? `${subject("\uC740/\uB294")}${rule.value} \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4` : null;
2396
2963
  case "max":
2397
- return !empty && toNumber(value) > rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value} \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4` : null;
2964
+ return !empty && toNumber(value) > rule.value ? rule.message ?? `${subject("\uC740/\uB294")}${rule.value} \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4` : null;
2398
2965
  case "minLength":
2399
- return !empty && String(value).length < rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value}\uC790 \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4` : null;
2966
+ return !empty && String(value).length < rule.value ? rule.message ?? `${subject("\uC740/\uB294")}${rule.value}\uC790 \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4` : null;
2400
2967
  case "maxLength":
2401
- return !empty && String(value).length > rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value}\uC790 \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4` : null;
2968
+ return !empty && String(value).length > rule.value ? rule.message ?? `${subject("\uC740/\uB294")}${rule.value}\uC790 \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4` : null;
2402
2969
  case "pattern":
2403
- return !empty && !new RegExp(rule.value).test(String(value)) ? rule.message ?? `${label} \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4` : null;
2970
+ return !empty && !new RegExp(rule.value).test(String(value)) ? rule.message ?? (label ? `${label} \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4` : "\uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4") : null;
2404
2971
  case "notBeforeField": {
2405
2972
  const other = values[rule.field];
2406
2973
  if (empty || other === void 0 || other === null || other === "") return null;
2407
2974
  const a = Number(String(value).replace(/,/g, ""));
2408
2975
  const b = Number(String(other).replace(/,/g, ""));
2409
2976
  const before = Number.isFinite(a) && Number.isFinite(b) && String(value).trim() !== "" ? a < b : String(value) < String(other);
2410
- return before ? rule.message ?? `${label}\uC774(\uAC00) \uC21C\uC11C\uC5D0 \uB9DE\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4` : null;
2977
+ if (!before) return null;
2978
+ if (rule.message) return rule.message;
2979
+ const otherLabel = labelOf2(rule.field);
2980
+ if (label && otherLabel) return `${josa(label, "\uC740/\uB294")} ${otherLabel}\uBCF4\uB2E4 \uC55E\uC124 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`;
2981
+ return `${subject("\uC774/\uAC00")}\uC21C\uC11C\uC5D0 \uB9DE\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4`;
2411
2982
  }
2412
2983
  case "custom": {
2413
2984
  const validator = customValidators?.[rule.name];
@@ -2420,6 +2991,122 @@ function ruleError(rule, value, values, label, customValidators) {
2420
2991
  }
2421
2992
  }
2422
2993
  }
2994
+ function invalidSummary(result, labelOf2, order = []) {
2995
+ const rank = (field) => {
2996
+ const index = order.indexOf(field);
2997
+ return index === -1 ? Number.MAX_SAFE_INTEGER : index;
2998
+ };
2999
+ const fields = Object.keys(result.errors).sort((a, b) => rank(a) - rank(b));
3000
+ const count = fields.length;
3001
+ const allRequired = fields.every((field) => result.failed[field] === "required");
3002
+ const labels = fields.map(labelOf2).filter((l) => typeof l === "string" && l !== "");
3003
+ if (count === 1 && labels.length === 1) return { title: result.errors[fields[0]] };
3004
+ const title = allRequired ? `\uD544\uC218 \uD56D\uBAA9 ${count}\uAC1C\uB97C \uCC44\uC6CC \uC8FC\uC138\uC694` : `\uC785\uB825\uAC12 ${count}\uAC1C\uB97C \uD655\uC778\uD574 \uC8FC\uC138\uC694`;
3005
+ if (labels.length === 0) return { title, message: count === 1 ? void 0 : result.errors[fields[0]] };
3006
+ const shown = labels.slice(0, 4).join(", ");
3007
+ const rest = labels.length - 4;
3008
+ return { title, message: rest > 0 ? `${shown} \uC678 ${rest}\uAC1C` : shown };
3009
+ }
3010
+ var FOCUSABLE_IN_FIELD = 'input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])';
3011
+ function formElementOf(formId) {
3012
+ if (typeof document === "undefined") return void 0;
3013
+ return Array.from(document.querySelectorAll("form[data-sdui-form]")).find(
3014
+ (el) => el.getAttribute("data-sdui-form") === formId && el.isConnected
3015
+ );
3016
+ }
3017
+ function fieldOrderOf(formId) {
3018
+ const form = formElementOf(formId);
3019
+ if (!form) return [];
3020
+ return Array.from(form.querySelectorAll("[data-sdui-field]")).map(
3021
+ (el) => el.getAttribute("data-sdui-field") ?? ""
3022
+ );
3023
+ }
3024
+ function focusFirstInvalid(formId, fields) {
3025
+ const form = formElementOf(formId);
3026
+ if (!form) return;
3027
+ const wanted = new Set(fields);
3028
+ const candidates = form.querySelectorAll('[data-sdui-field], [aria-invalid]:not([aria-invalid="false"])');
3029
+ const hit = Array.from(candidates).find((el) => {
3030
+ const field = el.getAttribute("data-sdui-field");
3031
+ return field !== null ? wanted.has(field) : true;
3032
+ });
3033
+ if (!hit) return;
3034
+ const target = hit.matches(FOCUSABLE_IN_FIELD) ? hit : hit.querySelector(FOCUSABLE_IN_FIELD);
3035
+ hit.scrollIntoView?.({ block: "center", inline: "nearest" });
3036
+ target?.focus({ preventScroll: true });
3037
+ }
3038
+ var INVALID_PROP_COMPONENTS = /* @__PURE__ */ new Set([
3039
+ "Input",
3040
+ "Textarea",
3041
+ "Select",
3042
+ "DatePicker",
3043
+ "DateRangePicker",
3044
+ "TimePicker",
3045
+ "AutocompleteField",
3046
+ "JsonField",
3047
+ "PositionSelect"
3048
+ ]);
3049
+ var BINDING_PROPS = ["value", "checked", "values"];
3050
+ function formKeyOf(value, formId) {
3051
+ if (!isStateRef(value)) return null;
3052
+ const prefix = `${formId}.`;
3053
+ if (!value.$state.startsWith(prefix)) return null;
3054
+ const key = value.$state.slice(prefix.length);
3055
+ return key === "" || key.startsWith("$") ? null : key;
3056
+ }
3057
+ function boundInputOf(node, formId, depth = 0) {
3058
+ if (!Array.isArray(node.children)) return null;
3059
+ for (const child of node.children) {
3060
+ if (!isPlainObject(child)) continue;
3061
+ const childNode = child;
3062
+ const props = isPlainObject(childNode.props) ? childNode.props : {};
3063
+ for (const name of BINDING_PROPS) {
3064
+ const key = formKeyOf(props[name], formId);
3065
+ if (key) return { key, kind: String(childNode.component) };
3066
+ }
3067
+ const component = childNode.component;
3068
+ if (component === "Field" || component === "Form" || component === "ForEach" || component === "Fragment") continue;
3069
+ if (depth < 4) {
3070
+ const found = boundInputOf(childNode, formId, depth + 1);
3071
+ if (found) return found;
3072
+ }
3073
+ }
3074
+ return null;
3075
+ }
3076
+ function hasRequiredRule(rules) {
3077
+ return Array.isArray(rules) && rules.some((rule) => isPlainObject(rule) && rule.rule === "required");
3078
+ }
3079
+ function applyFormBinding(node, props, form, engine) {
3080
+ const raw = isPlainObject(node.props) ? node.props : {};
3081
+ const errorOf = (key) => {
3082
+ const errors = engine.resolveValue({ $state: `${form.id}.$errors` }, EMPTY_SCOPE);
3083
+ const message = isPlainObject(errors) ? errors[key] : void 0;
3084
+ return typeof message === "string" && message !== "" ? message : void 0;
3085
+ };
3086
+ if (node.component === "Field") {
3087
+ const bound = boundInputOf(node, form.id);
3088
+ if (!bound) return;
3089
+ if (!Object.hasOwn(raw, "required") && hasRequiredRule(form.rules[bound.key])) props.required = true;
3090
+ if (!Object.hasOwn(raw, "error")) {
3091
+ const message = errorOf(bound.key);
3092
+ if (message) props.error = message;
3093
+ }
3094
+ props["data-sdui-field"] = bound.key;
3095
+ engine.noteField(form.id, bound.key, {
3096
+ label: typeof props.label === "string" && props.label !== "" ? props.label : void 0,
3097
+ kind: bound.kind
3098
+ });
3099
+ return;
3100
+ }
3101
+ if (typeof node.component !== "string" || !INVALID_PROP_COMPONENTS.has(node.component)) return;
3102
+ if (Object.hasOwn(raw, "invalid")) return;
3103
+ for (const name of BINDING_PROPS) {
3104
+ const key = formKeyOf(raw[name], form.id);
3105
+ if (!key) continue;
3106
+ if (errorOf(key)) props.invalid = true;
3107
+ return;
3108
+ }
3109
+ }
2423
3110
  var EMPTY_SCOPE = [];
2424
3111
  var DENIED_PROPS2 = /* @__PURE__ */ new Set([
2425
3112
  "dangerouslySetInnerHTML",
@@ -2452,6 +3139,7 @@ function SduiNodeRenderer({
2452
3139
  engine
2453
3140
  }) {
2454
3141
  const { registry, resolveValue, dispatch, resolveAsset } = engine;
3142
+ const formScope = React9.useContext(SduiFormScopeContext);
2455
3143
  const slots = isPlainObject(node.slots) ? node.slots : void 0;
2456
3144
  const childList = childrenOf(node);
2457
3145
  if (node.component === "ForEach") {
@@ -2516,6 +3204,10 @@ function SduiNodeRenderer({
2516
3204
  props[name] = resolved;
2517
3205
  }
2518
3206
  }
3207
+ if (formScope) applyFormBinding(node, props, formScope, engine);
3208
+ if (typeof node.component === "string" && INVALID_PROP_COMPONENTS.has(node.component) && props.invalid !== void 0 && typeof props.invalid !== "boolean") {
3209
+ props.invalid = Boolean(props.invalid);
3210
+ }
2519
3211
  const childLayout = childLayoutFrom(props);
2520
3212
  if (childLayout) {
2521
3213
  for (const name of CHILD_LAYOUT_PROPS) delete props[name];
@@ -2612,14 +3304,30 @@ function SduiFragmentBody({
2612
3304
  return null;
2613
3305
  }
2614
3306
  const page = spec;
2615
- if (page.data || page.urlState || page.subscribe) {
3307
+ if (page.data || page.urlState || page.subscribe || page.leaveGuard !== void 0 || page.titleFrom !== void 0) {
2616
3308
  warnOnce(
2617
3309
  `fragment:page-features:${id}`,
2618
- `[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`
3310
+ `[sdui] Fragment \uC2A4\uD399 ${id}\uC758 data/urlState/subscribe/leaveGuard/titleFrom\uC740 \uBB34\uC2DC\uB429\uB2C8\uB2E4 \u2014 \uC870\uAC01\uC740 body\uB9CC \uB80C\uB354\uD569\uB2C8\uB2E4`
2619
3311
  );
2620
3312
  }
2621
3313
  return /* @__PURE__ */ jsx(SduiFragmentChainContext.Provider, { value: nextChain, children: page.body.map((child, i) => renderChild(child, i, bodyScope, engine)) });
2622
3314
  }
3315
+ function SduiToastRegion({ hostId }) {
3316
+ const board = useSyncExternalStore(subscribeToasts, toastSnapshot, toastServerSnapshot);
3317
+ const toasts = useMemo(
3318
+ () => board.entries.filter((entry) => toastHostOf(entry, board.hosts) === hostId),
3319
+ [board, hostId]
3320
+ );
3321
+ useEffect(() => {
3322
+ if (toasts.length > 0) markToastsShown(toasts.map((t) => t.id));
3323
+ }, [toasts]);
3324
+ const modalOpen = useAnyModalOpen();
3325
+ if (toasts.length === 0) return null;
3326
+ return /* @__PURE__ */ jsxs("div", { className: modalOpen ? "wp-toast-region wp-toast-region--modal" : "wp-toast-region", children: [
3327
+ /* @__PURE__ */ jsx(HoistedStyle, { id: "wp-sdui-toast-modal", css: TOAST_MODAL_CSS }),
3328
+ toasts.map((t) => /* @__PURE__ */ jsx(Toast, { tone: t.tone, title: t.title, onClose: () => closeToastEntry(t.id), children: t.message }, t.id))
3329
+ ] });
3330
+ }
2623
3331
  function urlSeedSettled(state, seeded, baseline) {
2624
3332
  return Object.keys(seeded).every(
2625
3333
  (key) => Object.is(state[key], seeded[key]) || !Object.is(state[key], baseline[key])
@@ -2644,24 +3352,43 @@ function SduiRenderer({
2644
3352
  const urlSeeded = useRef(false);
2645
3353
  const urlSeedPending = useRef(null);
2646
3354
  const urlHomePath = useRef(typeof window !== "undefined" ? window.location.pathname : "");
2647
- const [toasts, setToasts] = useState([]);
2648
- const toastSeq = useRef(0);
2649
- const toastTimers = useRef([]);
2650
3355
  const stateRef = useRef(state);
2651
3356
  stateRef.current = state;
2652
3357
  const formsRef = useRef(/* @__PURE__ */ new Map());
2653
- useEffect(() => {
2654
- const timers = toastTimers.current;
2655
- return () => timers.forEach(clearTimeout);
3358
+ const fieldInfoRef = useRef(/* @__PURE__ */ new Map());
3359
+ const noteField = useCallback((formId, key, info) => {
3360
+ let fields = fieldInfoRef.current.get(formId);
3361
+ if (!fields) {
3362
+ fields = /* @__PURE__ */ new Map();
3363
+ fieldInfoRef.current.set(formId, fields);
3364
+ }
3365
+ fields.set(key, info);
2656
3366
  }, []);
3367
+ const [toastHostId] = useState(allocateToastHostId);
3368
+ const toastChannel = React9.useContext(OverlayContainerContext);
3369
+ useEffect(() => registerToastHost(toastHostId, toastChannel), [toastHostId, toastChannel]);
2657
3370
  useEffect(() => {
2658
3371
  if (!DEV) return;
2659
3372
  const issues = validateSpec(page, new Set(Object.keys(registry)));
2660
3373
  issues.forEach((issue) => console.warn(`[sdui:spec] ${issue.path} \u2014 ${issue.message}`));
2661
3374
  }, [page, registry]);
2662
- const setEntries = useCallback((entries) => {
2663
- stateRef.current = { ...stateRef.current, ...entries };
2664
- setState((prev) => ({ ...prev, ...entries }));
3375
+ const leaveFormsRef = useRef(/* @__PURE__ */ new Map());
3376
+ const leaveRegistry = useMemo(
3377
+ () => ({
3378
+ register: (formId, isDirty) => {
3379
+ leaveFormsRef.current.set(formId, isDirty);
3380
+ return () => {
3381
+ if (leaveFormsRef.current.get(formId) === isDirty) leaveFormsRef.current.delete(formId);
3382
+ };
3383
+ }
3384
+ }),
3385
+ []
3386
+ );
3387
+ const leavingRef = useRef(false);
3388
+ const setEntries = useCallback((entries2) => {
3389
+ stateRef.current = { ...stateRef.current, ...entries2 };
3390
+ leavingRef.current = false;
3391
+ setState((prev) => ({ ...prev, ...entries2 }));
2665
3392
  }, []);
2666
3393
  useEffect(() => {
2667
3394
  if (!syncUrlState || urlSeeded.current || !page.urlState?.length || typeof window === "undefined") return;
@@ -2685,6 +3412,16 @@ function SduiRenderer({
2685
3412
  setEntries(seeded);
2686
3413
  }
2687
3414
  }, [page.urlState, setEntries, syncUrlState]);
3415
+ const [urlResync, setUrlResync] = useState(0);
3416
+ const hasUrlState = Boolean(page.urlState?.length);
3417
+ useEffect(() => {
3418
+ if (!syncUrlState || !hasUrlState || typeof window === "undefined") return;
3419
+ const onPop = () => {
3420
+ if (window.location.pathname === urlHomePath.current) setUrlResync((n) => n + 1);
3421
+ };
3422
+ window.addEventListener("popstate", onPop);
3423
+ return () => window.removeEventListener("popstate", onPop);
3424
+ }, [syncUrlState, hasUrlState]);
2688
3425
  useEffect(() => {
2689
3426
  if (!syncUrlState || !page.urlState?.length || !urlSeeded.current || typeof window === "undefined") return;
2690
3427
  if (window.location.pathname !== urlHomePath.current) return;
@@ -2720,7 +3457,7 @@ function SduiRenderer({
2720
3457
  );
2721
3458
  }
2722
3459
  }
2723
- }, [state, page.urlState, syncUrlState]);
3460
+ }, [state, page.urlState, syncUrlState, urlResync]);
2724
3461
  const subscribeChannels = page.subscribe?.join(",") ?? "";
2725
3462
  const hasSubscribe = typeof host.subscribe === "function";
2726
3463
  useEffect(() => {
@@ -2741,16 +3478,11 @@ function SduiRenderer({
2741
3478
  unsubscribe();
2742
3479
  };
2743
3480
  }, [subscribeChannels, hasSubscribe]);
2744
- const closeToast = useCallback((id) => {
2745
- setToasts((prev) => prev.filter((t) => t.id !== id));
2746
- }, []);
2747
3481
  const pushToast = useCallback(
2748
3482
  (toast) => {
2749
- const id = ++toastSeq.current;
2750
- setToasts((prev) => [...prev, { ...toast, id }]);
2751
- toastTimers.current.push(setTimeout(() => closeToast(id), 4e3));
3483
+ pushToastEntry(toastHostId, toastChannel, toast);
2752
3484
  },
2753
- [closeToast]
3485
+ [toastHostId, toastChannel]
2754
3486
  );
2755
3487
  const lookupState = useCallback((key) => {
2756
3488
  const current = stateRef.current;
@@ -2811,7 +3543,7 @@ function SduiRenderer({
2811
3543
  if (isPlainObject(value)) {
2812
3544
  const out = {};
2813
3545
  for (const [k, v] of Object.entries(value)) {
2814
- out[k] = deferChainEntry(value, k) || deferRunEntry(k, v) ? v : resolveValue(v, scope);
3546
+ out[k] = deferChainEntry(value, k) || deferRunEntry(k, v) || deferEachTarget(value, k) ? v : resolveValue(v, scope);
2815
3547
  }
2816
3548
  return out;
2817
3549
  }
@@ -2819,6 +3551,76 @@ function SduiRenderer({
2819
3551
  },
2820
3552
  [lookupState]
2821
3553
  );
3554
+ const leaveMessageNow = useCallback(() => {
3555
+ if (leavingRef.current) return null;
3556
+ if (page.leaveGuard !== void 0) {
3557
+ const message = leaveMessageOf(resolveValue(page.leaveGuard, EMPTY_SCOPE));
3558
+ if (message) return message;
3559
+ }
3560
+ for (const [formId, isDirty] of leaveFormsRef.current) {
3561
+ if (stateRef.current[`${formId}.$resetDirty`] === true) continue;
3562
+ if (isDirty()) return DEFAULT_LEAVE_MESSAGE;
3563
+ }
3564
+ return null;
3565
+ }, [page.leaveGuard, resolveValue]);
3566
+ const leaveSentRef = useRef(null);
3567
+ const unloadHandlerRef = useRef(null);
3568
+ const syncLeaveGuard = useCallback(() => {
3569
+ const message = leaveMessageNow();
3570
+ if (message === leaveSentRef.current) return message;
3571
+ leaveSentRef.current = message;
3572
+ hostRef.current.setLeaveGuard?.(message);
3573
+ if (typeof window === "undefined") return message;
3574
+ if (message !== null && !unloadHandlerRef.current) {
3575
+ const handler = (event) => {
3576
+ event.preventDefault();
3577
+ event.returnValue = "";
3578
+ };
3579
+ window.addEventListener("beforeunload", handler);
3580
+ unloadHandlerRef.current = handler;
3581
+ } else if (message === null && unloadHandlerRef.current) {
3582
+ window.removeEventListener("beforeunload", unloadHandlerRef.current);
3583
+ unloadHandlerRef.current = null;
3584
+ }
3585
+ return message;
3586
+ }, [leaveMessageNow]);
3587
+ useEffect(() => {
3588
+ syncLeaveGuard();
3589
+ });
3590
+ useEffect(
3591
+ () => () => {
3592
+ if (leaveSentRef.current !== null) {
3593
+ leaveSentRef.current = null;
3594
+ hostRef.current.setLeaveGuard?.(null);
3595
+ }
3596
+ if (unloadHandlerRef.current && typeof window !== "undefined") {
3597
+ window.removeEventListener("beforeunload", unloadHandlerRef.current);
3598
+ unloadHandlerRef.current = null;
3599
+ }
3600
+ },
3601
+ []
3602
+ );
3603
+ const [pendingLeave, setPendingLeave] = useState(null);
3604
+ const titleFrom = page.titleFrom;
3605
+ const resolvedTitle = useMemo(
3606
+ () => titleFrom === void 0 ? null : titleText(resolveValue(titleFrom, EMPTY_SCOPE)),
3607
+ // state 가 바뀌어야 다시 푼다 — resolveValue 는 ref 를 읽어 정체성이 안 바뀐다.
3608
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3609
+ [titleFrom, resolveValue, state]
3610
+ );
3611
+ const effectiveTitle = resolvedTitle ?? page.title;
3612
+ useEffect(() => {
3613
+ if (titleFrom === void 0) return;
3614
+ hostRef.current.setTitle?.(effectiveTitle ?? null);
3615
+ }, [titleFrom, effectiveTitle]);
3616
+ useEffect(
3617
+ () => () => {
3618
+ if (titleFrom !== void 0) hostRef.current.setTitle?.(null);
3619
+ },
3620
+ // 마운트 1회 계약 — 라우트가 바뀌면 호스트가 key 로 갈아 끼운다.
3621
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3622
+ []
3623
+ );
2822
3624
  const dataPlan = useMemo(
2823
3625
  () => planPageData(page.data, (value) => resolveValue(value, EMPTY_SCOPE)),
2824
3626
  // state를 의존성에 두어야 $state를 참조하는 경로가 값 변화를 따라간다.
@@ -2908,6 +3710,34 @@ function SduiRenderer({
2908
3710
  revertUnsettledStamps(fetchedRef.current, runnable, epochs, settled);
2909
3711
  };
2910
3712
  }, [dataPlanKey, dataEpochKey, setEntries]);
3713
+ const revalidateField = useCallback(
3714
+ (key) => {
3715
+ for (const [formId, config] of formsRef.current) {
3716
+ const prefix = `${formId}.`;
3717
+ if (!key.startsWith(prefix)) continue;
3718
+ const field = key.slice(prefix.length);
3719
+ if (field === "" || field.startsWith("$")) continue;
3720
+ const errors = stateRef.current[`${formId}.$errors`];
3721
+ if (!isPlainObject(errors) || !Object.hasOwn(errors, field)) continue;
3722
+ const next = { ...errors };
3723
+ try {
3724
+ const result = validateForm(
3725
+ collectFormValues(stateRef.current, formId),
3726
+ config,
3727
+ customValidators,
3728
+ fieldInfoRef.current.get(formId)
3729
+ );
3730
+ const message = result.errors[field];
3731
+ if (message) next[field] = message;
3732
+ else delete next[field];
3733
+ } catch {
3734
+ delete next[field];
3735
+ }
3736
+ setEntries({ [`${formId}.$errors`]: next });
3737
+ }
3738
+ },
3739
+ [customValidators, setEntries]
3740
+ );
2911
3741
  const dispatchRef = useRef(() => {
2912
3742
  });
2913
3743
  const dispatch = useCallback(
@@ -2924,17 +3754,24 @@ function SduiRenderer({
2924
3754
  eventScope
2925
3755
  );
2926
3756
  switch (action.action) {
2927
- case "navigate":
3757
+ case "navigate": {
2928
3758
  if (!isSafeNavigationTarget(action.to)) {
2929
3759
  warnOnce(`navigate:${String(action.to)}`, `[sdui] \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 navigate \uB300\uC0C1: ${String(action.to)}`);
2930
3760
  break;
2931
3761
  }
3762
+ const leaveMessage = syncLeaveGuard();
3763
+ if (leaveMessage !== null && typeof hostRef.current.setLeaveGuard !== "function") {
3764
+ setPendingLeave({ to: action.to, message: leaveMessage });
3765
+ break;
3766
+ }
2932
3767
  hostRef.current.navigate(action.to);
2933
3768
  break;
3769
+ }
2934
3770
  case "setState": {
2935
3771
  const nextValue = action.fromEvent ? eventValue(arg) : action.value;
2936
3772
  if (!Array.isArray(action.path) || action.path.length === 0) {
2937
3773
  setEntries({ [action.key]: nextValue });
3774
+ revalidateField(action.key);
2938
3775
  break;
2939
3776
  }
2940
3777
  const written = setAtPath(
@@ -2950,6 +3787,7 @@ function SduiRenderer({
2950
3787
  break;
2951
3788
  }
2952
3789
  setEntries({ [action.key]: written.next });
3790
+ revalidateField(action.key);
2953
3791
  break;
2954
3792
  }
2955
3793
  case "open":
@@ -2959,7 +3797,7 @@ function SduiRenderer({
2959
3797
  setEntries({ [action.key]: false });
2960
3798
  break;
2961
3799
  case "toast":
2962
- pushToast({ tone: action.tone ?? "info", title: action.title, message: action.message });
3800
+ pushToast({ tone: action.tone ?? "info", title: toastText(action.title), message: toastText(action.message) });
2963
3801
  break;
2964
3802
  case "arrayAppend": {
2965
3803
  const current = stateRef.current[action.key];
@@ -3026,16 +3864,21 @@ function SduiRenderer({
3026
3864
  let phase = "validate";
3027
3865
  let pendingOn = false;
3028
3866
  try {
3029
- const prefix = `${formId}.`;
3030
- const values = {};
3031
- for (const [key, value] of Object.entries(stateRef.current)) {
3032
- if (key.startsWith(prefix) && !key.slice(prefix.length).startsWith("$") && value !== void 0) {
3033
- values[key.slice(prefix.length)] = value;
3034
- }
3035
- }
3036
- const errors = validateForm(values, formsRef.current.get(formId), customValidators);
3867
+ const values = collectFormValues(stateRef.current, formId);
3868
+ const config = formsRef.current.get(formId);
3869
+ const fieldInfo = fieldInfoRef.current.get(formId);
3870
+ const result = validateForm(values, config, customValidators, fieldInfo);
3871
+ const errors = result.errors;
3037
3872
  if (Object.keys(errors).length > 0) {
3038
3873
  setEntries({ [`${formId}.$errors`]: errors, [`${formId}.$submitting`]: false });
3874
+ const summary = invalidSummary(
3875
+ result,
3876
+ (field) => config?.labels[field] ?? fieldInfo?.get(field)?.label,
3877
+ fieldOrderOf(formId)
3878
+ );
3879
+ pushToast({ tone: "warning", title: summary.title, message: summary.message });
3880
+ const fields = Object.keys(errors);
3881
+ setTimeout(() => focusFirstInvalid(formId, fields), 0);
3039
3882
  return;
3040
3883
  }
3041
3884
  setEntries({ [`${formId}.$errors`]: {}, [`${formId}.$submitting`]: true });
@@ -3080,7 +3923,10 @@ function SduiRenderer({
3080
3923
  // scope 를 잇는다 — request 의 onSuccess/onError 도 같은 자리의 텍스트다.
3081
3924
  // {"$arg"} 체인 항목은 dispatch 진입부가 이 스코프로 펼친다(submitForm과 동일).
3082
3925
  dispatch: (a, value) => dispatchRef.current(a, value, scope),
3083
- toast: pushToast
3926
+ toast: pushToast,
3927
+ // each: 원소를 행 프레임으로 얹어 endpoint·body 를 원소마다 푼다 — 같은 자리의
3928
+ // $state·$arg·$event 는 그대로 보이고, $item/$index 만 이 원소를 가리킨다.
3929
+ resolveFor: (value, item, index) => resolveValue(value, [...eventScope, { item, index }])
3084
3930
  });
3085
3931
  break;
3086
3932
  case "invalidate": {
@@ -3127,12 +3973,12 @@ function SduiRenderer({
3127
3973
  break;
3128
3974
  }
3129
3975
  },
3130
- [pushToast, onCustomAction, customValidators, setEntries, resolveValue]
3976
+ [pushToast, onCustomAction, customValidators, setEntries, resolveValue, revalidateField, syncLeaveGuard]
3131
3977
  );
3132
3978
  dispatchRef.current = dispatch;
3133
3979
  const engine = useMemo(
3134
- () => ({ registry, resolveValue, dispatch, resolveAsset: assetResolverOf(host) }),
3135
- [registry, resolveValue, dispatch, host]
3980
+ () => ({ registry, resolveValue, dispatch, resolveAsset: assetResolverOf(host), noteField }),
3981
+ [registry, resolveValue, dispatch, host, noteField]
3136
3982
  );
3137
3983
  const runtime = useMemo(
3138
3984
  () => ({
@@ -3150,15 +3996,43 @@ function SduiRenderer({
3150
3996
  return (
3151
3997
  // 해석된 어댑터를 트리에 흘려보낸다 — dispatch를 거치지 않고 스스로 요청을 내는
3152
3998
  // 컴포넌트(목록 조회 등)도 호스트가 건 잠금 안에서 움직이게 하기 위해서다(adapter.ts 주석).
3153
- /* @__PURE__ */ jsx(SduiAdapterContext.Provider, { value: host, children: /* @__PURE__ */ jsx(SduiDispatchContext.Provider, { value: dispatch, children: /* @__PURE__ */ jsx(SduiRuntimeContext.Provider, { value: runtime, children: /* @__PURE__ */ jsx(SduiPermissionProvider, { can, children: /* @__PURE__ */ jsxs("div", { className: "sdui-root", children: [
3999
+ /* @__PURE__ */ jsx(SduiAdapterContext.Provider, { value: host, children: /* @__PURE__ */ jsx(SduiDispatchContext.Provider, { value: dispatch, children: /* @__PURE__ */ jsx(SduiRuntimeContext.Provider, { value: runtime, children: /* @__PURE__ */ jsx(SduiPermissionProvider, { can, children: /* @__PURE__ */ jsx(SduiLeaveRegistryContext.Provider, { value: leaveRegistry, children: /* @__PURE__ */ jsxs("div", { className: "sdui-root", children: [
3154
4000
  /* @__PURE__ */ jsx(HoistedStyle, { id: "wp-sdui-layout", css: LAYOUT_CSS }),
3155
- !hasExplicitPageHead(page.body) && (page.title || page.subtitle) ? /* @__PURE__ */ jsx("div", { className: "wp-page-head", children: /* @__PURE__ */ jsxs("div", { className: "wp-page-head__text", children: [
3156
- page.title ? /* @__PURE__ */ jsx("h1", { className: "wp-page-head__title", children: page.title }) : null,
4001
+ !hasExplicitPageHead(page.body) && (effectiveTitle || page.subtitle) ? /* @__PURE__ */ jsx("div", { className: "wp-page-head", children: /* @__PURE__ */ jsxs("div", { className: "wp-page-head__text", children: [
4002
+ effectiveTitle ? /* @__PURE__ */ jsx("h1", { className: "wp-page-head__title", children: effectiveTitle }) : null,
3157
4003
  page.subtitle ? /* @__PURE__ */ jsx("div", { className: "wp-page-head__sub", children: page.subtitle }) : null
3158
4004
  ] }) }) : null,
3159
4005
  /* @__PURE__ */ jsx("div", { className: "wp-col", children: Array.isArray(page.body) ? page.body.map((child, i) => renderChild(child, i, EMPTY_SCOPE, engine)) : /* @__PURE__ */ jsx(SpecErrorCard, { children: "[sdui] page.body\uB294 \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4" }) }),
3160
- toasts.length > 0 ? /* @__PURE__ */ jsx("div", { className: "wp-toast-region", children: toasts.map((t) => /* @__PURE__ */ jsx(Toast, { tone: t.tone, title: t.title, onClose: () => closeToast(t.id), children: t.message }, t.id)) }) : null
3161
- ] }) }) }) }) })
4006
+ /* @__PURE__ */ jsx(SduiToastRegion, { hostId: toastHostId }),
4007
+ pendingLeave ? /* @__PURE__ */ jsx(OverlayLayer, { onClose: () => setPendingLeave(null), children: /* @__PURE__ */ jsx(
4008
+ Dialog,
4009
+ {
4010
+ size: "sm",
4011
+ icon: "triangle-alert",
4012
+ tone: "danger",
4013
+ title: "\uC800\uC7A5\uD558\uC9C0 \uC54A\uACE0 \uB098\uAC08\uAE4C\uC694?",
4014
+ onClose: () => setPendingLeave(null),
4015
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [
4016
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", "data-autofocus": "", onClick: () => setPendingLeave(null), children: "\uACC4\uC18D \uC791\uC131" }),
4017
+ /* @__PURE__ */ jsx(
4018
+ Button,
4019
+ {
4020
+ variant: "danger",
4021
+ onClick: () => {
4022
+ const { to } = pendingLeave;
4023
+ setPendingLeave(null);
4024
+ leavingRef.current = true;
4025
+ syncLeaveGuard();
4026
+ hostRef.current.navigate(to);
4027
+ },
4028
+ children: "\uB098\uAC00\uAE30"
4029
+ }
4030
+ )
4031
+ ] }),
4032
+ children: pendingLeave.message
4033
+ }
4034
+ ) }) : null
4035
+ ] }) }) }) }) }) })
3162
4036
  );
3163
4037
  }
3164
4038
 
@@ -3219,4 +4093,4 @@ async function resolvePatternSpec(id, load) {
3219
4093
  // src/icon-names.ts
3220
4094
  var ICON_NAMES = Object.keys(ICON_NODES);
3221
4095
 
3222
- export { Can, ICON_NAMES, NodeErrorBoundary, RemoteSearchResults, ResultTable, SduiForm, SduiRenderer, SduiSearchDataProvider, SduiSearchInvalidationProvider, SduiSearchResults, SearchResults, SearchSummaryCards, SortHandle, SortableList, cellRenderer, createRegistry, createSearchInvalidator, defaultRegistry, imageRenderer, isValidSpecId, mapActions, mapCardRows, mapColumns, mapGridCard, mapViews, matchesCondition, normalizePageSizes, patternSpecCandidates, resolvePatternSpec, sortableDropIndex, sortableMove, summaryParams, useRemoteSearchPage, useRemoteSearchSummary, useSduiRuntime, useSearchFetcher, validateSpec, withProps };
4096
+ export { Can, ICON_NAMES, NodeErrorBoundary, ORDER_PARAM, RemoteSearchResults, ResultEmptyState, ResultTable, SORT_PARAM, SduiForm, SduiRenderer, SduiSearchDataProvider, SduiSearchInvalidationProvider, SduiSearchResults, SearchResults, SearchSummaryCards, SortHandle, SortableList, cellRenderer, conditionParams, createRegistry, createSearchInvalidator, createServerSortMemory, defaultRegistry, imageRenderer, isValidSpecId, mapActions, mapCardRows, mapColumns, mapGridCard, mapViews, matchesCondition, nextSort, normalizePageSizes, patternSpecCandidates, resolvePatternSpec, serverSortMemory, sortEchoOf, sortFromParams, sortableDropIndex, sortableMove, summaryParams, useRemoteSearchPage, useRemoteSearchSummary, useSearchFetcher, validateSpec, withProps, withoutSort };