@ketvietlab/ketsuite 0.1.20 → 0.1.24

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 (245) hide show
  1. package/dist/cli.js +9 -3
  2. package/dist/cli.js.map +1 -1
  3. package/dist/demo/crm.d.ts +3 -0
  4. package/dist/demo/crm.d.ts.map +1 -0
  5. package/dist/demo/crm.js +418 -0
  6. package/dist/demo/crm.js.map +1 -0
  7. package/dist/demo/partner.d.ts +8 -0
  8. package/dist/demo/partner.d.ts.map +1 -0
  9. package/dist/demo/partner.js +179 -0
  10. package/dist/demo/partner.js.map +1 -0
  11. package/dist/demo/product.d.ts +25 -0
  12. package/dist/demo/product.d.ts.map +1 -0
  13. package/dist/demo/product.js +579 -0
  14. package/dist/demo/product.js.map +1 -0
  15. package/dist/demo/purchase.d.ts +4 -0
  16. package/dist/demo/purchase.d.ts.map +1 -0
  17. package/dist/demo/purchase.js +58 -0
  18. package/dist/demo/purchase.js.map +1 -0
  19. package/dist/demo/sale.d.ts +4 -0
  20. package/dist/demo/sale.d.ts.map +1 -0
  21. package/dist/demo/sale.js +52 -0
  22. package/dist/demo/sale.js.map +1 -0
  23. package/dist/demo/seed.d.ts +14 -0
  24. package/dist/demo/seed.d.ts.map +1 -0
  25. package/dist/demo/seed.js +88 -0
  26. package/dist/demo/seed.js.map +1 -0
  27. package/dist/demo/stock.d.ts +7 -0
  28. package/dist/demo/stock.d.ts.map +1 -0
  29. package/dist/demo/stock.js +26 -0
  30. package/dist/demo/stock.js.map +1 -0
  31. package/dist/demo/user.d.ts +4 -0
  32. package/dist/demo/user.d.ts.map +1 -0
  33. package/dist/demo/user.js +41 -0
  34. package/dist/demo/user.js.map +1 -0
  35. package/dist/demo/util.d.ts +7 -0
  36. package/dist/demo/util.d.ts.map +1 -0
  37. package/dist/demo/util.js +19 -0
  38. package/dist/demo/util.js.map +1 -0
  39. package/dist/modules/backend/design/client/backend-shell.d.mts +2 -2
  40. package/dist/modules/backend/design/client/backend-shell.d.mts.map +1 -1
  41. package/dist/modules/backend/design/client/backend-shell.mjs +1 -1
  42. package/dist/modules/backend/design/client/backend-shell.mjs.map +3 -3
  43. package/dist/modules/backend/design/content.css +9 -9
  44. package/dist/modules/backend/design/controls.css +5 -5
  45. package/dist/modules/backend/design/forms.css +12 -5
  46. package/dist/modules/backend/design/foundation.css +3 -3
  47. package/dist/modules/backend/design/lists.css +23 -24
  48. package/dist/modules/backend/design/user-workflow.css +2 -2
  49. package/dist/modules/backend/screen.d.ts.map +1 -1
  50. package/dist/modules/backend/screen.js +11 -2
  51. package/dist/modules/backend/screen.js.map +1 -1
  52. package/dist/modules/crm/functions/access.d.ts +3 -0
  53. package/dist/modules/crm/functions/access.d.ts.map +1 -0
  54. package/dist/modules/crm/functions/access.js +89 -0
  55. package/dist/modules/crm/functions/access.js.map +1 -0
  56. package/dist/modules/crm/functions/activities.d.ts +3 -0
  57. package/dist/modules/crm/functions/activities.d.ts.map +1 -0
  58. package/dist/modules/crm/functions/activities.js +174 -0
  59. package/dist/modules/crm/functions/activities.js.map +1 -0
  60. package/dist/modules/crm/functions/cases.d.ts +3 -0
  61. package/dist/modules/crm/functions/cases.d.ts.map +1 -0
  62. package/dist/modules/crm/functions/cases.js +585 -0
  63. package/dist/modules/crm/functions/cases.js.map +1 -0
  64. package/dist/modules/crm/functions/configuration.d.ts +3 -0
  65. package/dist/modules/crm/functions/configuration.d.ts.map +1 -0
  66. package/dist/modules/crm/functions/configuration.js +331 -0
  67. package/dist/modules/crm/functions/configuration.js.map +1 -0
  68. package/dist/modules/crm/functions/gamification.d.ts +3 -0
  69. package/dist/modules/crm/functions/gamification.d.ts.map +1 -0
  70. package/dist/modules/crm/functions/gamification.js +58 -0
  71. package/dist/modules/crm/functions/gamification.js.map +1 -0
  72. package/dist/modules/crm/functions/index.d.ts +4 -0
  73. package/dist/modules/crm/functions/index.d.ts.map +1 -0
  74. package/dist/modules/crm/functions/index.js +33 -0
  75. package/dist/modules/crm/functions/index.js.map +1 -0
  76. package/dist/modules/crm/functions/modal-context.d.ts +3 -0
  77. package/dist/modules/crm/functions/modal-context.d.ts.map +1 -0
  78. package/dist/modules/crm/functions/modal-context.js +248 -0
  79. package/dist/modules/crm/functions/modal-context.js.map +1 -0
  80. package/dist/modules/crm/functions/previews.d.ts +3 -0
  81. package/dist/modules/crm/functions/previews.d.ts.map +1 -0
  82. package/dist/modules/crm/functions/previews.js +20 -0
  83. package/dist/modules/crm/functions/previews.js.map +1 -0
  84. package/dist/modules/crm/functions/shared.d.ts +31 -0
  85. package/dist/modules/crm/functions/shared.d.ts.map +1 -0
  86. package/dist/modules/crm/functions/shared.js +194 -0
  87. package/dist/modules/crm/functions/shared.js.map +1 -0
  88. package/dist/modules/crm/index.d.ts +1 -1
  89. package/dist/modules/crm/index.d.ts.map +1 -1
  90. package/dist/modules/crm/index.js +2 -2
  91. package/dist/modules/crm/index.js.map +1 -1
  92. package/dist/modules/crm/messages.d.ts +4 -1
  93. package/dist/modules/crm/messages.d.ts.map +1 -1
  94. package/dist/modules/crm/messages.js +6 -0
  95. package/dist/modules/crm/messages.js.map +1 -1
  96. package/dist/modules/crm/record-modal-labels.d.ts +9 -0
  97. package/dist/modules/crm/record-modal-labels.d.ts.map +1 -0
  98. package/dist/modules/crm/record-modal-labels.js +36 -0
  99. package/dist/modules/crm/record-modal-labels.js.map +1 -0
  100. package/dist/modules/crm/types.d.ts +5 -0
  101. package/dist/modules/crm/types.d.ts.map +1 -1
  102. package/dist/modules/crm/types.js +9 -0
  103. package/dist/modules/crm/types.js.map +1 -1
  104. package/dist/modules/crm_backend/client/crm-configuration-modal.d.mts +352 -0
  105. package/dist/modules/crm_backend/client/crm-configuration-modal.d.mts.map +1 -0
  106. package/dist/modules/crm_backend/client/crm-configuration-modal.mjs +2 -0
  107. package/dist/modules/crm_backend/client/crm-configuration-modal.mjs.map +7 -0
  108. package/dist/modules/crm_backend/index.d.ts.map +1 -1
  109. package/dist/modules/crm_backend/index.js +5 -1
  110. package/dist/modules/crm_backend/index.js.map +1 -1
  111. package/dist/modules/crm_backend/islands.d.ts +27 -0
  112. package/dist/modules/crm_backend/islands.d.ts.map +1 -1
  113. package/dist/modules/crm_backend/islands.js +22 -0
  114. package/dist/modules/crm_backend/islands.js.map +1 -1
  115. package/dist/modules/crm_backend/messages.d.ts +30 -0
  116. package/dist/modules/crm_backend/messages.d.ts.map +1 -1
  117. package/dist/modules/crm_backend/messages.js +30 -0
  118. package/dist/modules/crm_backend/messages.js.map +1 -1
  119. package/dist/modules/crm_backend/modal/configuration-modal-view.d.ts +55 -0
  120. package/dist/modules/crm_backend/modal/configuration-modal-view.d.ts.map +1 -0
  121. package/dist/modules/crm_backend/modal/configuration-modal-view.js +601 -0
  122. package/dist/modules/crm_backend/modal/configuration-modal-view.js.map +1 -0
  123. package/dist/modules/crm_backend/routes.d.ts.map +1 -1
  124. package/dist/modules/crm_backend/routes.js +107 -368
  125. package/dist/modules/crm_backend/routes.js.map +1 -1
  126. package/dist/modules/crm_backend/screens/configuration.d.ts +24 -32
  127. package/dist/modules/crm_backend/screens/configuration.d.ts.map +1 -1
  128. package/dist/modules/crm_backend/screens/configuration.js +183 -274
  129. package/dist/modules/crm_backend/screens/configuration.js.map +1 -1
  130. package/dist/modules/crm_backend/screens/index.d.ts +2 -2
  131. package/dist/modules/crm_backend/screens/index.d.ts.map +1 -1
  132. package/dist/modules/crm_backend/screens/index.js +1 -1
  133. package/dist/modules/crm_backend/screens/index.js.map +1 -1
  134. package/dist/modules/product/functions.d.ts +14 -1
  135. package/dist/modules/product/functions.d.ts.map +1 -1
  136. package/dist/modules/product/functions.js +36 -31
  137. package/dist/modules/product/functions.js.map +1 -1
  138. package/dist/modules/product/product-modal-context.d.ts +3 -0
  139. package/dist/modules/product/product-modal-context.d.ts.map +1 -0
  140. package/dist/modules/product/product-modal-context.js +171 -0
  141. package/dist/modules/product/product-modal-context.js.map +1 -0
  142. package/dist/modules/product_backend/client/product-modal.d.mts +143 -0
  143. package/dist/modules/product_backend/client/product-modal.d.mts.map +1 -0
  144. package/dist/modules/product_backend/client/product-modal.mjs +2 -0
  145. package/dist/modules/product_backend/client/product-modal.mjs.map +7 -0
  146. package/dist/modules/product_backend/index.d.ts.map +1 -1
  147. package/dist/modules/product_backend/index.js +16 -0
  148. package/dist/modules/product_backend/index.js.map +1 -1
  149. package/dist/modules/product_backend/islands.d.ts +1 -0
  150. package/dist/modules/product_backend/islands.d.ts.map +1 -1
  151. package/dist/modules/product_backend/islands.js +9 -0
  152. package/dist/modules/product_backend/islands.js.map +1 -1
  153. package/dist/modules/product_backend/modal/product-modal-view.d.ts +43 -0
  154. package/dist/modules/product_backend/modal/product-modal-view.d.ts.map +1 -0
  155. package/dist/modules/product_backend/modal/product-modal-view.js +602 -0
  156. package/dist/modules/product_backend/modal/product-modal-view.js.map +1 -0
  157. package/dist/modules/product_backend/routes.d.ts.map +1 -1
  158. package/dist/modules/product_backend/routes.js +44 -330
  159. package/dist/modules/product_backend/routes.js.map +1 -1
  160. package/dist/modules/product_backend/screens/detail.d.ts.map +1 -1
  161. package/dist/modules/product_backend/screens/detail.js +6 -108
  162. package/dist/modules/product_backend/screens/detail.js.map +1 -1
  163. package/dist/modules/product_backend/screens/index.d.ts +0 -1
  164. package/dist/modules/product_backend/screens/index.d.ts.map +1 -1
  165. package/dist/modules/product_backend/screens/index.js +0 -1
  166. package/dist/modules/product_backend/screens/index.js.map +1 -1
  167. package/dist/modules/product_backend/screens/list.d.ts +3 -1
  168. package/dist/modules/product_backend/screens/list.d.ts.map +1 -1
  169. package/dist/modules/product_backend/screens/list.js +6 -5
  170. package/dist/modules/product_backend/screens/list.js.map +1 -1
  171. package/dist/modules/user/authorization.d.ts.map +1 -1
  172. package/dist/modules/user/authorization.js +114 -0
  173. package/dist/modules/user/authorization.js.map +1 -1
  174. package/dist/modules/user/functions.d.ts.map +1 -1
  175. package/dist/modules/user/functions.js +2 -0
  176. package/dist/modules/user/functions.js.map +1 -1
  177. package/dist/modules/user/user-modal-context.d.ts +3 -0
  178. package/dist/modules/user/user-modal-context.d.ts.map +1 -0
  179. package/dist/modules/user/user-modal-context.js +151 -0
  180. package/dist/modules/user/user-modal-context.js.map +1 -0
  181. package/dist/modules/user_backend/client/user-modal.d.mts +72 -0
  182. package/dist/modules/user_backend/client/user-modal.d.mts.map +1 -0
  183. package/dist/modules/user_backend/client/user-modal.mjs +2 -0
  184. package/dist/modules/user_backend/client/user-modal.mjs.map +7 -0
  185. package/dist/modules/user_backend/index.d.ts.map +1 -1
  186. package/dist/modules/user_backend/index.js +9 -0
  187. package/dist/modules/user_backend/index.js.map +1 -1
  188. package/dist/modules/user_backend/islands.d.ts +15 -0
  189. package/dist/modules/user_backend/islands.d.ts.map +1 -0
  190. package/dist/modules/user_backend/islands.js +14 -0
  191. package/dist/modules/user_backend/islands.js.map +1 -0
  192. package/dist/modules/user_backend/messages.d.ts +30 -0
  193. package/dist/modules/user_backend/messages.d.ts.map +1 -1
  194. package/dist/modules/user_backend/messages.js +30 -0
  195. package/dist/modules/user_backend/messages.js.map +1 -1
  196. package/dist/modules/user_backend/modal/user-modal-view.d.ts +26 -0
  197. package/dist/modules/user_backend/modal/user-modal-view.d.ts.map +1 -0
  198. package/dist/modules/user_backend/modal/user-modal-view.js +320 -0
  199. package/dist/modules/user_backend/modal/user-modal-view.js.map +1 -0
  200. package/dist/modules/user_backend/routes.d.ts.map +1 -1
  201. package/dist/modules/user_backend/routes.js +15 -3
  202. package/dist/modules/user_backend/routes.js.map +1 -1
  203. package/dist/permission-catalogue.d.ts.map +1 -1
  204. package/dist/permission-catalogue.js +8 -0
  205. package/dist/permission-catalogue.js.map +1 -1
  206. package/dist/scaffold/index.js +1 -1
  207. package/dist/ui/client/activity.css +6 -6
  208. package/dist/ui/client/calendar.css +6 -6
  209. package/dist/ui/client/flow-app.css +1 -1
  210. package/dist/ui/client/mail.css +6 -6
  211. package/dist/ui/client/record-modal-form.d.ts +67 -0
  212. package/dist/ui/client/record-modal-form.d.ts.map +1 -0
  213. package/dist/ui/client/record-modal-form.js +45 -0
  214. package/dist/ui/client/record-modal-form.js.map +1 -0
  215. package/dist/ui/client/record-modal.d.ts +97 -6
  216. package/dist/ui/client/record-modal.d.ts.map +1 -1
  217. package/dist/ui/client/record-modal.js +330 -64
  218. package/dist/ui/client/record-modal.js.map +1 -1
  219. package/dist/ui/index.d.ts +2 -1
  220. package/dist/ui/index.d.ts.map +1 -1
  221. package/dist/ui/index.js +2 -1
  222. package/dist/ui/index.js.map +1 -1
  223. package/dist/ui/layout.d.ts.map +1 -1
  224. package/dist/ui/layout.js +5 -3
  225. package/dist/ui/layout.js.map +1 -1
  226. package/dist/ui/nav.d.ts +7 -0
  227. package/dist/ui/nav.d.ts.map +1 -1
  228. package/dist/ui/nav.js +1 -1
  229. package/dist/ui/nav.js.map +1 -1
  230. package/dist/ui/record-modal.d.ts +15 -0
  231. package/dist/ui/record-modal.d.ts.map +1 -1
  232. package/dist/ui/record-modal.js +12 -0
  233. package/dist/ui/record-modal.js.map +1 -1
  234. package/dist/ui/table.d.ts.map +1 -1
  235. package/dist/ui/table.js +1 -1
  236. package/dist/ui/table.js.map +1 -1
  237. package/package.json +4 -4
  238. package/dist/modules/crm/functions.d.ts +0 -10
  239. package/dist/modules/crm/functions.d.ts.map +0 -1
  240. package/dist/modules/crm/functions.js +0 -1394
  241. package/dist/modules/crm/functions.js.map +0 -1
  242. package/dist/modules/product_backend/screens/create.d.ts +0 -16
  243. package/dist/modules/product_backend/screens/create.d.ts.map +0 -1
  244. package/dist/modules/product_backend/screens/create.js +0 -86
  245. package/dist/modules/product_backend/screens/create.js.map +0 -1
@@ -11,8 +11,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "@ketvietlab/k
11
11
  // Views are render-pure: they read the context and return design-system markup.
12
12
  // They never fetch, never touch history and never query the document.
13
13
  import { signal } from '@ketvietlab/ketjs-view';
14
- import { LoadingState, ModalSheet, Notice, Tabs, Button } from '@ketvietlab/design-system';
15
- import { RECORD_PARAM, RECORD_TAB_PARAM, readRecordModalTarget, recordModalClosedHref, recordModalHost, recordModalHref, } from '../record-modal.js';
14
+ import { Button, LoadingState, ModalSheet, Notice, TabbedView } from '@ketvietlab/design-system';
15
+ import { RECORD_NEW_ID, RECORD_PARAM, RECORD_TAB_PARAM, readRecordModalTarget, recordModalClosedHref, recordModalCreateHref, recordModalHost, recordModalHref, } from '../record-modal.js';
16
16
  const issuesOf = (source) => {
17
17
  const holder = (source ?? {});
18
18
  const list = Array.isArray(holder.errors)
@@ -62,6 +62,8 @@ export const callRecordFunction = async (name, input, options = {}) => {
62
62
  return { ok: false, issues: issuesOf(value), message: null, status: response.status };
63
63
  return { ok: true, value: payload.value };
64
64
  };
65
+ /** How many record contexts one island keeps for instant reopening. */
66
+ export const RECORD_MODAL_CACHE_SIZE = 30;
65
67
  /**
66
68
  * English defaults for every label the runtime itself shows. A module passes its
67
69
  * own language through `labels`; these only guarantee a reader never sees a key.
@@ -74,6 +76,8 @@ export const RECORD_MODAL_LABELS = Object.freeze({
74
76
  'recordModal.retry': 'Retry',
75
77
  'recordModal.errorTitle': 'Not saved',
76
78
  'recordModal.saveFailed': 'That did not work. Try again.',
79
+ 'recordModal.savedTitle': 'Saved',
80
+ 'recordModal.saved': 'The change is in.',
77
81
  'recordModal.unsaved': 'Discard what you typed?',
78
82
  'recordModal.uploadFailed': 'The file could not be uploaded. Try again.',
79
83
  });
@@ -151,6 +155,56 @@ const inertOutside = (keep) => {
151
155
  const uuid = () => typeof crypto !== 'undefined' && 'randomUUID' in crypto
152
156
  ? crypto.randomUUID()
153
157
  : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
158
+ const emptyDraftState = () => ({ values: {}, checks: {} });
159
+ const draftCheckKey = (name, value) => `${name}\u0000${value}`;
160
+ const after_ = (command) => command.after ?? 'close';
161
+ /**
162
+ * How long a command may run before its button says so. Below this the answer
163
+ * arrives while the reader is still lifting their finger, and a spinner shown
164
+ * and withdrawn inside that window is noise; above it, silence would read as a
165
+ * click that did nothing.
166
+ */
167
+ export const BUSY_AFTER_MS = 400;
168
+ /**
169
+ * A flag that turns on late and off at once. Work that finishes inside the
170
+ * window never raises it, so a progress indicator bound to it appears only when
171
+ * there is progress to report, and never flashes on its way back out.
172
+ */
173
+ export const delayedFlag = (show, after = BUSY_AFTER_MS) => {
174
+ let timer;
175
+ const stop = () => {
176
+ clearTimeout(timer);
177
+ timer = undefined;
178
+ };
179
+ return {
180
+ set: (running) => {
181
+ stop();
182
+ if (!running) {
183
+ show(false);
184
+ return;
185
+ }
186
+ timer = setTimeout(() => show(true), after);
187
+ },
188
+ stop,
189
+ };
190
+ };
191
+ /** Controls inside a row do their own thing; the row's destination is for the rest of it. */
192
+ const rowControl = 'a, button, input, select, textarea, label, summary, details, [data-ui="select-cell"]';
193
+ /**
194
+ * Where a click is asking to go: the link it landed on, or the row it landed
195
+ * in. A table row carries its destination on the row itself (`rowLink: false`),
196
+ * so the whole row is one target rather than a link around the first cell. The
197
+ * shell would navigate such a row through the navigation layer, which fetches
198
+ * the collection again and leaves the modal unopened, so this reads it first.
199
+ */
200
+ export const openerHref = (element) => {
201
+ const anchor = element?.closest('a[href]');
202
+ if (anchor)
203
+ return anchor.target && anchor.target !== '_self' ? null : anchor.href;
204
+ if (element?.closest(rowControl))
205
+ return null;
206
+ return element?.closest('[data-row-href]')?.getAttribute('data-row-href') ?? null;
207
+ };
154
208
  /**
155
209
  * Create the island controller for one record kind.
156
210
  *
@@ -163,15 +217,45 @@ export const createRecordModal = (definition) => () => {
163
217
  const envelope = signal(null);
164
218
  const failure = signal(null);
165
219
  const issues = signal([]);
166
- const drafts = signal({});
220
+ const recordDrafts = signal(emptyDraftState());
221
+ const dialogDrafts = signal(emptyDraftState());
222
+ // `running` is the guard — one command at a time — and `busy` is what the
223
+ // views show. They are not the same thing: a save the server answers in
224
+ // twenty milliseconds would otherwise flash the button through its spinner
225
+ // and back, which reads as a glitch rather than as progress. The spinner
226
+ // waits; the guard does not.
227
+ const running = signal(false);
167
228
  const busy = signal(false);
229
+ const showBusy = delayedFlag((value) => busy.set(value));
230
+ // A command that succeeded while the modal stayed open. Saving is usually
231
+ // answered before the button could say anything, and a record that looks
232
+ // the same afterwards leaves the reader unsure anything happened, so the
233
+ // form says so where it would have said the opposite.
234
+ const saved = signal(false);
235
+ const setRunning = (value) => {
236
+ running.set(value);
237
+ showBusy.set(value);
238
+ if (value)
239
+ saved.set(false);
240
+ };
168
241
  const dialog = signal(null);
169
242
  const version = signal(0);
170
243
  const viewState = signal({});
244
+ // Contexts this island has read, newest last: reopening a record (or the create
245
+ // form) renders immediately and revalidates in place. Bounded and page-scoped;
246
+ // a successful command or `ket:records-changed` for this kind drops the entries.
247
+ const cache = new Map();
248
+ const remember = (id, value) => {
249
+ cache.delete(id);
250
+ cache.set(id, value);
251
+ while (cache.size > RECORD_MODAL_CACHE_SIZE)
252
+ cache.delete(cache.keys().next().value);
253
+ };
171
254
  let root = null;
172
255
  let request = null;
173
256
  let pushed = false;
174
257
  let returnFocus = null;
258
+ let dialogReturnFocus = null;
175
259
  let releaseInert = null;
176
260
  // Messages of the last record this modal loaded: opening the next record clears
177
261
  // the envelope, and its loading state still needs words rather than keys.
@@ -183,10 +267,12 @@ export const createRecordModal = (definition) => () => {
183
267
  labels: labels(),
184
268
  }), params);
185
269
  const visibleTabs = (context) => (definition.tabs ?? []).filter((tab) => tab.visible?.(context) ?? true);
186
- const contextFor = (current, data) => {
270
+ const contextFor = (current, data, scope = 'record') => {
271
+ const draftState = () => (scope === 'dialog' ? dialogDrafts() : recordDrafts());
187
272
  const base = {
188
273
  kind: definition.kind,
189
274
  id: current.id,
275
+ creating: current.id === RECORD_NEW_ID,
190
276
  tab: current.tab,
191
277
  data,
192
278
  t,
@@ -194,7 +280,8 @@ export const createRecordModal = (definition) => () => {
194
280
  const hit = issues().find((issue) => issue.field === name);
195
281
  return hit ? (hit.message ?? t(hit.code, hit.params)) : null;
196
282
  },
197
- draft: (name, fallback = '') => drafts()[name] ?? fallback,
283
+ draft: (name, fallback = '') => draftState().values[name] ?? fallback,
284
+ draftChecked: (name, value = '1', fallback = false) => draftState().checks[draftCheckKey(name, value)] ?? fallback,
198
285
  busy: busy(),
199
286
  dialog: dialog(),
200
287
  href: (tab) => recordModalHref(location.href, { kind: definition.kind, id: current.id, tab }),
@@ -209,11 +296,25 @@ export const createRecordModal = (definition) => () => {
209
296
  request?.abort();
210
297
  const controller = new AbortController();
211
298
  request = controller;
299
+ // A record opened before shows at once from the in-memory cache and is read
300
+ // again quietly behind it; the reader never waits on a frame they have seen.
301
+ const cached = definition.cache === false ? undefined : cache.get(id);
302
+ if (cached) {
303
+ previousMessages = cached.messages ?? previousMessages;
304
+ envelope.set(cached);
305
+ status.set('ready');
306
+ quiet = true;
307
+ }
212
308
  if (!quiet)
213
309
  status.set('loading');
214
310
  failure.set(null);
215
311
  try {
216
- const input = definition.context.input ? definition.context.input(id) : { id };
312
+ const creating = id === RECORD_NEW_ID;
313
+ const input = definition.context.input
314
+ ? definition.context.input(id, creating)
315
+ : creating
316
+ ? {}
317
+ : { id };
217
318
  const result = await callRecordFunction(definition.context.fn, input, {
218
319
  signal: controller.signal,
219
320
  });
@@ -225,6 +326,8 @@ export const createRecordModal = (definition) => () => {
225
326
  return;
226
327
  }
227
328
  previousMessages = result.value.messages ?? previousMessages;
329
+ if (definition.cache !== false)
330
+ remember(id, result.value);
228
331
  envelope.set(result.value);
229
332
  status.set('ready');
230
333
  }
@@ -235,30 +338,64 @@ export const createRecordModal = (definition) => () => {
235
338
  status.set('error');
236
339
  }
237
340
  };
238
- const layer = () => root?.querySelector('[data-ui="modal-layer"][data-client-modal="true"]') ?? null;
239
- const keepDrafts = () => {
240
- const current = layer();
241
- if (!current || !recordLayerHasDraft(current))
341
+ const layers = () => root ? [...root.querySelectorAll('[data-ui="modal-layer"][data-client-modal="true"]')] : [];
342
+ const recordLayer = () => layers()[0] ?? null;
343
+ const topLayer = () => layers().at(-1) ?? null;
344
+ const keepDrafts = (current, scope) => {
345
+ if (!current)
242
346
  return;
243
- const kept = {};
244
- for (const form of current.querySelectorAll('form'))
245
- for (const [key, value] of new FormData(form))
246
- if (typeof value === 'string' && key !== RECORD_COMMAND_FIELD)
247
- kept[key] = value;
248
- drafts.set({ ...drafts(), ...kept });
347
+ const previous = scope === 'dialog' ? dialogDrafts() : recordDrafts();
348
+ const kept = {
349
+ values: { ...previous.values },
350
+ checks: { ...previous.checks },
351
+ };
352
+ for (const control of current.querySelectorAll('input[name], select[name], textarea[name]')) {
353
+ if (control.disabled || control.name === RECORD_COMMAND_FIELD)
354
+ continue;
355
+ if (control instanceof HTMLInputElement) {
356
+ if (control.type === 'file' || control.type === 'hidden')
357
+ continue;
358
+ if (control.type === 'checkbox' || control.type === 'radio') {
359
+ kept.checks[draftCheckKey(control.name, control.value)] = control.checked;
360
+ if (control.type === 'checkbox')
361
+ kept.values[control.name] = control.checked ? control.value : '';
362
+ else if (control.checked)
363
+ kept.values[control.name] = control.value;
364
+ continue;
365
+ }
366
+ }
367
+ kept.values[control.name] = control.value;
368
+ }
369
+ if (scope === 'dialog')
370
+ dialogDrafts.set(kept);
371
+ else
372
+ recordDrafts.set(kept);
373
+ };
374
+ const keepAllDrafts = () => {
375
+ const currentLayers = layers();
376
+ keepDrafts(currentLayers[0] ?? null, 'record');
377
+ if (currentLayers.length > 1)
378
+ keepDrafts(currentLayers.at(-1) ?? null, 'dialog');
249
379
  };
250
- const mayDiscard = () => {
251
- const current = layer();
380
+ const mayDiscard = (current) => {
252
381
  if (!current || !recordLayerHasDraft(current))
253
382
  return true;
254
383
  return globalThis.confirm(t('recordModal.unsaved'));
255
384
  };
256
- const afterRender = () => {
385
+ const afterRender = (preferred) => {
257
386
  requestAnimationFrame(() => {
258
- const layers = root?.querySelectorAll('[data-ui="modal-layer"][data-client-modal="true"]');
259
- const top = layers?.[layers.length - 1];
387
+ const currentLayers = layers();
388
+ const record = currentLayers[0];
389
+ const top = currentLayers.at(-1);
260
390
  if (!top)
261
391
  return;
392
+ if (record)
393
+ record.inert = currentLayers.length > 1;
394
+ const target = preferred?.();
395
+ if (target?.isConnected && top.contains(target)) {
396
+ target.focus();
397
+ return;
398
+ }
262
399
  if (top.contains(document.activeElement))
263
400
  return;
264
401
  const sheet = top.querySelector('[data-ui="modal-sheet"]');
@@ -266,22 +403,30 @@ export const createRecordModal = (definition) => () => {
266
403
  (first ?? sheet)?.focus();
267
404
  });
268
405
  };
269
- const show = (id, tab, how) => {
406
+ const show = (id, tab, how, preferredFocus) => {
270
407
  const current = open();
271
408
  const sameRecord = current?.id === id;
272
409
  const nextTab = tab ?? (sameRecord ? current.tab : '');
273
410
  if (!sameRecord) {
274
411
  returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
275
412
  issues.set([]);
276
- drafts.set({});
413
+ saved.set(false);
414
+ recordDrafts.set(emptyDraftState());
415
+ dialogDrafts.set(emptyDraftState());
277
416
  viewState.set({});
278
417
  dialog.set(null);
418
+ dialogReturnFocus = null;
279
419
  envelope.set(null);
280
420
  void load(id);
281
421
  }
282
422
  open.set({ id, tab: nextTab });
283
423
  const href = recordModalHref(location.href, { kind: definition.kind, id, tab: nextTab || null });
284
424
  if (how === 'push') {
425
+ // The shell's own navigation snapshots scroll onto the entry it leaves
426
+ // before pushing (`saveScroll` in packages/ketjs/src/server/http.ts) so
427
+ // going back restores it; this push must do the same, or closing the
428
+ // modal later restores no scroll and the page it sits over jumps to top.
429
+ history.replaceState({ ...(history.state ?? {}), __ketScroll: [window.scrollX, window.scrollY] }, '', location.href);
285
430
  history.pushState({ ...(history.state ?? {}), __ketRecordModal: definition.kind }, '', href);
286
431
  pushed = true;
287
432
  }
@@ -289,14 +434,16 @@ export const createRecordModal = (definition) => () => {
289
434
  history.replaceState(history.state ?? {}, '', href);
290
435
  if (root && !releaseInert)
291
436
  releaseInert = inertOutside(root);
292
- afterRender();
437
+ afterRender(preferredFocus);
293
438
  };
294
439
  const hide = (how) => {
295
440
  request?.abort();
296
441
  open.set(null);
297
442
  dialog.set(null);
298
443
  issues.set([]);
299
- drafts.set({});
444
+ saved.set(false);
445
+ recordDrafts.set(emptyDraftState());
446
+ dialogDrafts.set(emptyDraftState());
300
447
  viewState.set({});
301
448
  status.set('idle');
302
449
  envelope.set(null);
@@ -312,19 +459,23 @@ export const createRecordModal = (definition) => () => {
312
459
  }
313
460
  const target = returnFocus;
314
461
  returnFocus = null;
462
+ dialogReturnFocus = null;
315
463
  if (target?.isConnected)
316
464
  requestAnimationFrame(() => target.focus());
317
465
  };
318
466
  const close = () => {
319
467
  if (dialog()) {
320
- if (!mayDiscard())
468
+ if (!mayDiscard(topLayer()))
321
469
  return;
470
+ const target = dialogReturnFocus;
471
+ dialogReturnFocus = null;
322
472
  dialog.set(null);
473
+ dialogDrafts.set(emptyDraftState());
323
474
  issues.set([]);
324
- afterRender();
475
+ afterRender(() => (target?.isConnected ? target : null));
325
476
  return;
326
477
  }
327
- if (!mayDiscard())
478
+ if (!mayDiscard(recordLayer()))
328
479
  return;
329
480
  hide('history');
330
481
  };
@@ -332,11 +483,19 @@ export const createRecordModal = (definition) => () => {
332
483
  const command = definition.commands?.[name];
333
484
  const current = open();
334
485
  const data = envelope()?.data;
335
- if (!command || !current || data === undefined || busy())
486
+ if (!command || !current || data === undefined || running())
336
487
  return;
337
488
  const formData = new FormData(form, submitter instanceof HTMLButtonElement ? submitter : null);
338
- const context = contextFor(current, data);
339
- busy.set(true);
489
+ const currentLayer = form.closest('[data-ui="modal-layer"][data-client-modal="true"]');
490
+ const scope = dialog() && currentLayer === topLayer() ? 'dialog' : 'record';
491
+ keepDrafts(currentLayer, scope);
492
+ const context = contextFor(current, data, scope);
493
+ if (command.confirm) {
494
+ const message = command.confirm(context);
495
+ if (message && !globalThis.confirm(message))
496
+ return;
497
+ }
498
+ setRunning(true);
340
499
  issues.set([]);
341
500
  try {
342
501
  const uploads = {};
@@ -366,16 +525,20 @@ export const createRecordModal = (definition) => () => {
366
525
  name: typeof stored.name === 'string' ? stored.name : null,
367
526
  };
368
527
  }
369
- const result = await callRecordFunction(command.fn, command.input(formData, context, uploads), {
370
- idempotencyKey: uuid(),
371
- });
528
+ const invoke = (fn, input) => callRecordFunction(fn, input, { idempotencyKey: uuid() });
529
+ let result = await invoke(command.fn, command.input(formData, context, uploads));
530
+ // Further calls run only once the first succeeds, and stop at the first
531
+ // one that does not — a later step's success never papers over an
532
+ // earlier failure.
533
+ for (const step of result.ok ? (command.also ?? []) : []) {
534
+ if (step.when && !step.when(context))
535
+ continue;
536
+ result = await invoke(step.fn, step.input(formData, context, uploads));
537
+ if (!result.ok)
538
+ break;
539
+ }
372
540
  if (!result.ok) {
373
- // Keep what was typed: the view reads it back through `draft`.
374
- const kept = {};
375
- for (const [key, value] of formData)
376
- if (typeof value === 'string')
377
- kept[key] = value;
378
- drafts.set({ ...drafts(), ...kept });
541
+ // The layer was snapshotted before the busy state rendered, including unchecked controls.
379
542
  issues.set(result.issues.length
380
543
  ? result.issues
381
544
  : [
@@ -388,35 +551,77 @@ export const createRecordModal = (definition) => () => {
388
551
  ]);
389
552
  return;
390
553
  }
391
- drafts.set({});
554
+ if (scope === 'dialog')
555
+ dialogDrafts.set(emptyDraftState());
556
+ else
557
+ recordDrafts.set(emptyDraftState());
392
558
  form.reset();
393
- document.dispatchEvent(new CustomEvent('ket:records-changed', { detail: { kind: definition.kind, ids: [current.id] } }));
394
- const after = command.after ?? 'close';
559
+ const value = result.value;
560
+ const createdId = after_(command) === 'open'
561
+ ? (command.created?.(result.value) ??
562
+ (value && typeof value === 'object' && value.id != null ? String(value.id) : null))
563
+ : null;
564
+ // What the command changed is no longer what the cache holds.
565
+ cache.delete(current.id);
566
+ if (createdId)
567
+ cache.delete(createdId);
568
+ const announce = () => {
569
+ document.dispatchEvent(new CustomEvent('ket:records-changed', {
570
+ detail: { kind: definition.kind, ids: [createdId ?? current.id] },
571
+ }));
572
+ };
573
+ const after = after_(command);
574
+ // A modal that closes says so by closing; one that stays owes an answer.
575
+ if (after !== 'close')
576
+ saved.set(true);
577
+ if (after === 'open') {
578
+ dialog.set(null);
579
+ dialogDrafts.set(emptyDraftState());
580
+ // Replace `:new` in the address bar before the collection refreshes: the
581
+ // shell re-fetches `location.href`, which must already name the new record.
582
+ if (createdId)
583
+ show(createdId, command.openTab ?? null, 'replace');
584
+ else
585
+ hide('history');
586
+ announce();
587
+ return;
588
+ }
589
+ announce();
395
590
  if (after === 'close')
396
591
  hide('history');
397
592
  else if (after === 'reload') {
593
+ const target = dialogReturnFocus;
594
+ dialogReturnFocus = null;
398
595
  dialog.set(null);
596
+ dialogDrafts.set(emptyDraftState());
399
597
  await load(current.id);
598
+ afterRender(() => (target?.isConnected ? target : null));
400
599
  }
401
600
  else if (after === 'refresh')
402
601
  await load(current.id, true);
403
602
  else if (after === 'stay')
404
603
  version.set(version() + 1);
405
604
  else if ('tab' in after) {
605
+ dialogReturnFocus = null;
406
606
  dialog.set(null);
607
+ dialogDrafts.set(emptyDraftState());
407
608
  show(current.id, after.tab, 'replace');
408
609
  await load(current.id);
409
610
  }
410
611
  else {
612
+ const target = after.dialog ? null : dialogReturnFocus;
613
+ dialogReturnFocus = after.dialog && submitter instanceof HTMLElement ? submitter : null;
411
614
  dialog.set(after.dialog ? { name: after.dialog, params: {} } : null);
615
+ dialogDrafts.set(emptyDraftState());
412
616
  await load(current.id, true);
617
+ afterRender(() => (target?.isConnected ? target : null));
413
618
  }
414
619
  }
415
620
  catch {
416
621
  issues.set([{ field: null, code: 'recordModal.saveFailed', message: null, params: {} }]);
417
622
  }
418
623
  finally {
419
- busy.set(false);
624
+ setRunning(false);
420
625
  }
421
626
  };
422
627
  const formIssues = (context) => {
@@ -424,7 +629,13 @@ export const createRecordModal = (definition) => () => {
424
629
  const fieldless = issues().filter((issue) => issue.field && !root?.querySelector(`[name="${CSS.escape(issue.field)}"]`));
425
630
  const all = [...general, ...fieldless];
426
631
  if (!all.length)
427
- return '';
632
+ return saved()
633
+ ? Notice({
634
+ title: context.t('recordModal.savedTitle'),
635
+ message: context.t('recordModal.saved'),
636
+ tone: 'positive',
637
+ })
638
+ : '';
428
639
  return Notice({
429
640
  title: context.t('recordModal.errorTitle'),
430
641
  message: all.map((issue) => issue.message ?? context.t(issue.code, issue.params)).join(' · '),
@@ -447,17 +658,23 @@ export const createRecordModal = (definition) => () => {
447
658
  const context = contextFor(current, data);
448
659
  const tabs = visibleTabs(context);
449
660
  const active = tabs.find((tab) => tab.id === context.tab);
450
- return (_jsxs(_Fragment, { children: [definition.header?.(context) ?? '', formIssues(context), tabs.length
451
- ? Tabs({
452
- label: definition.title(context),
453
- items: tabs.map((tab) => ({
454
- id: tab.id,
455
- label: tab.label(context),
456
- href: context.href(tab.id),
457
- active: tab.id === context.tab,
458
- })),
459
- })
460
- : '', active ? active.view(context) : (definition.body?.(context) ?? '')] }));
661
+ const header = definition.header?.(context) ?? '';
662
+ const issueNotice = formIssues(context);
663
+ const body = active ? active.view(context) : (definition.body?.(context) ?? '');
664
+ if (tabs.length)
665
+ return TabbedView({
666
+ id: `record-view-${definition.kind.replaceAll('.', '-')}`,
667
+ label: definition.title(context),
668
+ items: tabs.map((tab) => ({
669
+ id: tab.id,
670
+ label: tab.label(context),
671
+ href: context.href(tab.id),
672
+ active: tab.id === context.tab,
673
+ })),
674
+ context: header !== '' || issueNotice !== '' ? (_jsxs(_Fragment, { children: [header, issueNotice] })) : undefined,
675
+ body,
676
+ });
677
+ return (_jsxs(_Fragment, { children: [header, issueNotice, body] }));
461
678
  };
462
679
  const dialogLayer = () => {
463
680
  const current = open();
@@ -468,7 +685,7 @@ export const createRecordModal = (definition) => () => {
468
685
  const spec = definition.dialogs?.[opened.name];
469
686
  if (!spec)
470
687
  return '';
471
- const context = contextFor(current, data);
688
+ const context = contextFor(current, data, 'dialog');
472
689
  return ModalSheet({
473
690
  id: `record-dialog-${definition.kind.replaceAll('.', '-')}-${opened.name}`,
474
691
  mode: 'client',
@@ -492,8 +709,14 @@ export const createRecordModal = (definition) => () => {
492
709
  mode: 'client',
493
710
  presentation: 'dialog',
494
711
  size: definition.size ?? 'default',
712
+ // Tabs have different heights; a fixed dialog does not jump when the reader switches
713
+ // tabs, nor when the loading state gives way to the record.
714
+ height: (definition.tabs?.length ?? 0) > 1 ? 'fixed' : 'content',
715
+ fixedHeight: definition.fixedHeight,
495
716
  title: context ? definition.title(context) : t('recordModal.loading'),
496
717
  description: context ? (definition.description?.(context) ?? null) : null,
718
+ status: context ? definition.status?.(context) : undefined,
719
+ actions: context ? definition.actions?.(context) : undefined,
497
720
  closeLabel: t('recordModal.close'),
498
721
  body: recordBody(),
499
722
  }), dialogLayer()] }));
@@ -513,7 +736,11 @@ export const createRecordModal = (definition) => () => {
513
736
  const element = event.target instanceof Element ? event.target : null;
514
737
  const inModal = element?.closest('[data-ui="modal-layer"][data-client-modal="true"]');
515
738
  if (inModal && root?.contains(inModal)) {
516
- if (element?.closest('[data-ui="modal-close"], [data-ui="modal-backdrop"]')) {
739
+ // `data-ui="modal-close"` is the chrome's own × control; `data-record-close`
740
+ // marks a module's own labeled close button placed elsewhere in the body
741
+ // (the two can't share one attribute — a labeled button needs `data-ui="action"`
742
+ // for its styling, not the icon-only close control's).
743
+ if (element?.closest('[data-ui="modal-close"], [data-ui="modal-backdrop"], [data-record-close]')) {
517
744
  event.preventDefault();
518
745
  close();
519
746
  return;
@@ -521,6 +748,7 @@ export const createRecordModal = (definition) => () => {
521
748
  const stateControl = element?.closest('button[data-record-state], a[data-record-state]');
522
749
  if (stateControl) {
523
750
  event.preventDefault();
751
+ keepAllDrafts();
524
752
  viewState.set({
525
753
  ...viewState(),
526
754
  [stateControl.getAttribute('data-record-state') ?? '']: stateControl.getAttribute('data-record-value') ?? '',
@@ -530,6 +758,12 @@ export const createRecordModal = (definition) => () => {
530
758
  const opener = element?.closest(`[${RECORD_DIALOG_ATTRIBUTE}]`);
531
759
  if (opener) {
532
760
  event.preventDefault();
761
+ const focused = document.activeElement;
762
+ dialogReturnFocus =
763
+ focused instanceof HTMLElement && opener.contains(focused)
764
+ ? focused
765
+ : opener.querySelector(focusable);
766
+ dialogDrafts.set(emptyDraftState());
533
767
  const params = {};
534
768
  for (const [key, value] of Object.entries(opener.dataset))
535
769
  if (key.startsWith('recordParam') && value !== undefined)
@@ -547,10 +781,10 @@ export const createRecordModal = (definition) => () => {
547
781
  return;
548
782
  }
549
783
  }
550
- const anchor = element?.closest('a[href]');
551
- if (!anchor || (anchor.target && anchor.target !== '_self'))
784
+ const href = openerHref(element);
785
+ if (!href)
552
786
  return;
553
- const url = new URL(anchor.href, location.href);
787
+ const url = new URL(href, location.href);
554
788
  if (url.origin !== location.origin || url.pathname !== location.pathname)
555
789
  return;
556
790
  const target = readRecordModalTarget(url);
@@ -563,9 +797,9 @@ export const createRecordModal = (definition) => () => {
563
797
  return;
564
798
  // Switching tab is not discarding: what was typed rides along as drafts,
565
799
  // which the views read back, so no prompt stands between two tabs.
566
- keepDrafts();
800
+ keepDrafts(recordLayer(), 'record');
567
801
  issues.set([]);
568
- show(target.id, target.tab ?? null, 'replace');
802
+ show(target.id, target.tab ?? null, 'replace', () => recordLayer()?.querySelector('[data-ui="tab"][data-active="true"]') ?? null);
569
803
  return;
570
804
  }
571
805
  show(target.id, target.tab ?? null, 'push');
@@ -573,6 +807,24 @@ export const createRecordModal = (definition) => () => {
573
807
  // Capture: the navigation layer listens on the document too, and was
574
808
  // installed first, so a bubbling listener would see the page fetched.
575
809
  { signal: lifetime, capture: true });
810
+ // A row is reached by keyboard, not by pointer alone: Enter and Space on
811
+ // the focused row open what clicking it opens.
812
+ document.addEventListener('keydown', (event) => {
813
+ if (event.defaultPrevented || (event.key !== 'Enter' && event.key !== ' '))
814
+ return;
815
+ const element = event.target instanceof Element ? event.target : null;
816
+ if (!element?.matches('[data-row-href][tabindex="0"]'))
817
+ return;
818
+ const href = element.getAttribute('data-row-href');
819
+ const url = href ? new URL(href, location.href) : null;
820
+ if (!url || url.origin !== location.origin || url.pathname !== location.pathname)
821
+ return;
822
+ const target = readRecordModalTarget(url);
823
+ if (!target || target.kind !== definition.kind)
824
+ return;
825
+ event.preventDefault();
826
+ show(target.id, target.tab ?? null, 'push');
827
+ }, { signal: lifetime, capture: true });
576
828
  document.addEventListener('submit', (event) => {
577
829
  const form = event.target instanceof HTMLFormElement ? event.target : null;
578
830
  if (!form || !root?.contains(form))
@@ -625,6 +877,7 @@ export const createRecordModal = (definition) => () => {
625
877
  return;
626
878
  if ((control instanceof HTMLSelectElement || control instanceof HTMLInputElement) &&
627
879
  control.hasAttribute('data-record-state')) {
880
+ keepAllDrafts();
628
881
  viewState.set({
629
882
  ...viewState(),
630
883
  [control.getAttribute('data-record-state') ?? '']: control.value,
@@ -692,6 +945,18 @@ export const createRecordModal = (definition) => () => {
692
945
  });
693
946
  observer.observe(root, { childList: true, subtree: true });
694
947
  lifetime.addEventListener('abort', () => observer.disconnect());
948
+ // Another island (or this one) changed records of this kind: drop them so the
949
+ // next opening reads fresh data first.
950
+ document.addEventListener('ket:records-changed', (event) => {
951
+ const detail = event.detail;
952
+ if (detail?.kind !== definition.kind)
953
+ return;
954
+ if (!Array.isArray(detail.ids) || !detail.ids.length)
955
+ cache.clear();
956
+ else
957
+ for (const id of detail.ids)
958
+ cache.delete(String(id));
959
+ }, { signal: lifetime });
695
960
  // Back and forward over entries this modal created change only the modal.
696
961
  document.addEventListener('ket:popstate', (event) => {
697
962
  const target = readRecordModalTarget(location.href);
@@ -709,6 +974,7 @@ export const createRecordModal = (definition) => () => {
709
974
  show(initial.id, initial.tab ?? null, 'none');
710
975
  lifetime.addEventListener('abort', () => {
711
976
  request?.abort();
977
+ showBusy.stop();
712
978
  releaseInert?.();
713
979
  releaseInert = null;
714
980
  root = null;
@@ -721,5 +987,5 @@ export const createRecordModal = (definition) => () => {
721
987
  * appears and disposes it when the host leaves; props must be JSON.
722
988
  */
723
989
  export const recordIsland = (name, props) => (_jsx("ket-island", { "data-island": name, "data-props": JSON.stringify(props) }));
724
- export { RECORD_PARAM, RECORD_TAB_PARAM, recordModalHref, recordModalClosedHref, readRecordModalTarget };
990
+ export { RECORD_NEW_ID, RECORD_PARAM, RECORD_TAB_PARAM, recordModalHref, recordModalCreateHref, recordModalClosedHref, readRecordModalTarget, };
725
991
  //# sourceMappingURL=record-modal.js.map