@cosmicdrift/kumiko-renderer 0.387.0 → 0.389.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 (210) hide show
  1. package/dist/app/action-form-body.js +10 -2
  2. package/dist/app/action-form-support.d.ts +1 -0
  3. package/dist/app/action-form-support.js +1 -1
  4. package/dist/app/base-path-context.d.ts +9 -0
  5. package/dist/app/base-path-context.js +13 -0
  6. package/dist/app/base-path.d.ts +3 -0
  7. package/dist/app/base-path.js +27 -0
  8. package/dist/app/config-edit-body.d.ts +1 -1
  9. package/dist/app/config-edit-body.js +19 -6
  10. package/dist/app/config-edit-without-secret-fields.d.ts +2 -0
  11. package/dist/app/config-edit-without-secret-fields.js +28 -0
  12. package/dist/app/entity-edit-prefill.d.ts +11 -0
  13. package/dist/app/entity-edit-prefill.js +9 -4
  14. package/dist/app/entity-list-facet-specs.d.ts +3 -0
  15. package/dist/app/entity-list-facet-specs.js +65 -0
  16. package/dist/app/entity-list-screen.js +74 -206
  17. package/dist/app/extension-sections.d.ts +5 -3
  18. package/dist/app/form-schema.js +1 -1
  19. package/dist/app/header-actions-bar.js +1 -1
  20. package/dist/app/kumiko-screen.js +6 -14
  21. package/dist/app/list-bulk-actions.d.ts +18 -0
  22. package/dist/app/list-bulk-actions.js +54 -0
  23. package/dist/app/locked-screen-header.d.ts +9 -0
  24. package/dist/app/locked-screen-header.js +33 -0
  25. package/dist/app/projection-list-body.js +51 -46
  26. package/dist/app/row-actions.d.ts +3 -0
  27. package/dist/app/row-actions.js +10 -5
  28. package/dist/app/screen-shared.js +9 -2
  29. package/dist/app/use-entity-range-facets.d.ts +24 -0
  30. package/dist/app/use-entity-range-facets.js +64 -0
  31. package/dist/app/use-projection-date-range.d.ts +12 -0
  32. package/dist/app/use-projection-date-range.js +21 -0
  33. package/dist/app/use-screen-list-rows.d.ts +37 -0
  34. package/dist/app/use-screen-list-rows.js +104 -0
  35. package/dist/app/use-screen-list-table.d.ts +24 -0
  36. package/dist/app/use-screen-list-table.js +46 -0
  37. package/dist/components/edit-mounts.d.ts +2 -2
  38. package/dist/components/embedded-list-field.js +1 -1
  39. package/dist/components/list-group-page-bridges.d.ts +3 -0
  40. package/dist/components/list-group-page-bridges.js +31 -0
  41. package/dist/components/list-state-store.d.ts +16 -0
  42. package/dist/components/list-state-store.js +31 -0
  43. package/dist/components/related-list-section.js +51 -90
  44. package/dist/components/render-edit-action-button.d.ts +1 -3
  45. package/dist/components/render-edit-action-button.js +2 -2
  46. package/dist/components/render-edit.js +11 -8
  47. package/dist/components/render-field.js +11 -7
  48. package/dist/components/render-list-toolbar-action.d.ts +27 -0
  49. package/dist/components/render-list-toolbar-action.js +40 -0
  50. package/dist/components/render-list.d.ts +13 -29
  51. package/dist/components/render-list.js +72 -67
  52. package/dist/current-user.d.ts +2 -0
  53. package/dist/data-table-aggregate-types.d.ts +47 -0
  54. package/dist/data-table-aggregate-types.js +1 -0
  55. package/dist/data-table-bulk-types.d.ts +27 -0
  56. package/dist/data-table-bulk-types.js +1 -0
  57. package/dist/data-table-facet-types.d.ts +36 -0
  58. package/dist/data-table-facet-types.js +1 -0
  59. package/dist/data-table-view-types.d.ts +73 -0
  60. package/dist/data-table-view-types.js +1 -0
  61. package/dist/flow-diagram/render.js +9 -1
  62. package/dist/headless/form/form-controller.js +8 -0
  63. package/dist/headless/form/types.d.ts +1 -0
  64. package/dist/headless/format/index.js +89 -64
  65. package/dist/headless/view-model/edit.js +5 -1
  66. package/dist/headless/view-model/list.js +184 -149
  67. package/dist/headless/view-model/types.d.ts +7 -2
  68. package/dist/hooks/list-column-layout.d.ts +19 -0
  69. package/dist/hooks/list-column-layout.js +36 -0
  70. package/dist/hooks/list-group-pages.d.ts +26 -0
  71. package/dist/hooks/list-group-pages.js +37 -0
  72. package/dist/hooks/list-reset.d.ts +8 -0
  73. package/dist/hooks/list-reset.js +15 -0
  74. package/dist/hooks/list-server-groups.d.ts +39 -0
  75. package/dist/hooks/list-server-groups.js +84 -0
  76. package/dist/hooks/list-state-schema.d.ts +28 -0
  77. package/dist/hooks/list-state-schema.js +76 -0
  78. package/dist/hooks/live-refetch-scheduler.js +4 -0
  79. package/dist/hooks/use-bulk-run.d.ts +41 -0
  80. package/dist/hooks/use-bulk-run.js +65 -0
  81. package/dist/hooks/use-group-load-state.d.ts +11 -0
  82. package/dist/hooks/use-group-load-state.js +31 -0
  83. package/dist/hooks/use-list-aggregates.d.ts +38 -0
  84. package/dist/hooks/use-list-aggregates.js +107 -0
  85. package/dist/hooks/use-list-table.d.ts +38 -0
  86. package/dist/hooks/use-list-table.js +91 -0
  87. package/dist/hooks/use-list-view-state.d.ts +43 -0
  88. package/dist/hooks/use-list-view-state.js +161 -0
  89. package/dist/hooks/use-reference-lookup.d.ts +9 -1
  90. package/dist/hooks/use-reference-lookup.js +17 -3
  91. package/dist/hooks/use-related-list-rows.d.ts +27 -0
  92. package/dist/hooks/use-related-list-rows.js +81 -0
  93. package/dist/hooks/use-related-list-state.d.ts +15 -0
  94. package/dist/hooks/use-related-list-state.js +31 -0
  95. package/dist/hooks/use-remembered-list-params.d.ts +13 -0
  96. package/dist/hooks/use-remembered-list-params.js +53 -0
  97. package/dist/hooks/use-remembered-local-list-state.d.ts +26 -0
  98. package/dist/hooks/use-remembered-local-list-state.js +63 -0
  99. package/dist/i18n-defaults.js +59 -2
  100. package/dist/index.d.ts +4 -1
  101. package/dist/index.js +3 -0
  102. package/dist/primitives.d.ts +30 -47
  103. package/dist/web/app/create-app.d.ts +5 -0
  104. package/dist/web/app/create-app.js +15 -7
  105. package/dist/web/app/dashboard-body.js +17 -1
  106. package/dist/web/app/dashboard-panels.js +12 -2
  107. package/dist/web/app/history-leave-guard.d.ts +6 -0
  108. package/dist/web/app/history-leave-guard.js +115 -0
  109. package/dist/web/app/list-state-store-boot.d.ts +6 -0
  110. package/dist/web/app/list-state-store-boot.js +15 -0
  111. package/dist/web/app/nav.js +7 -54
  112. package/dist/web/app/session-list-state-store.d.ts +15 -0
  113. package/dist/web/app/session-list-state-store.js +117 -0
  114. package/dist/web/index.d.ts +2 -1
  115. package/dist/web/index.js +2 -1
  116. package/dist/web/layout/shell-header.js +2 -1
  117. package/dist/web/layout/uniform-bar-controls.d.ts +1 -0
  118. package/dist/web/layout/uniform-bar-controls.js +4 -0
  119. package/dist/web/lib/export-xlsx-request.d.ts +11 -0
  120. package/dist/web/lib/export-xlsx-request.js +46 -0
  121. package/dist/web/lib/file-ref-download.js +4 -1
  122. package/dist/web/primitives/action-menu.js +1 -1
  123. package/dist/web/primitives/code-input.d.ts +6 -0
  124. package/dist/web/primitives/code-input.js +31 -0
  125. package/dist/web/primitives/data-table/actions-column.d.ts +16 -0
  126. package/dist/web/primitives/data-table/actions-column.js +34 -0
  127. package/dist/web/primitives/data-table/bulk-selection-bar.d.ts +20 -0
  128. package/dist/web/primitives/data-table/bulk-selection-bar.js +37 -0
  129. package/dist/web/primitives/data-table/card-sort-select.d.ts +14 -0
  130. package/dist/web/primitives/data-table/card-sort-select.js +40 -0
  131. package/dist/web/primitives/data-table/column-layout-mode.d.ts +5 -0
  132. package/dist/web/primitives/data-table/column-layout-mode.js +10 -0
  133. package/dist/web/primitives/data-table/column-picker-section.d.ts +7 -0
  134. package/dist/web/primitives/data-table/column-picker-section.js +35 -0
  135. package/dist/web/primitives/data-table/column-resize-handle.d.ts +14 -0
  136. package/dist/web/primitives/data-table/column-resize-handle.js +46 -0
  137. package/dist/web/primitives/data-table/density.d.ts +14 -0
  138. package/dist/web/primitives/data-table/density.js +20 -0
  139. package/dist/web/primitives/data-table/export-button.d.ts +5 -0
  140. package/dist/web/primitives/data-table/export-button.js +37 -0
  141. package/dist/web/primitives/data-table/facet-cluster.d.ts +8 -0
  142. package/dist/web/primitives/data-table/facet-cluster.js +37 -0
  143. package/dist/web/primitives/data-table/group-band.d.ts +28 -0
  144. package/dist/web/primitives/data-table/group-band.js +53 -0
  145. package/dist/web/primitives/data-table/measure-value.d.ts +10 -0
  146. package/dist/web/primitives/data-table/measure-value.js +28 -0
  147. package/dist/web/primitives/data-table/pin-section.d.ts +8 -0
  148. package/dist/web/primitives/data-table/pin-section.js +20 -0
  149. package/dist/web/primitives/data-table/reset-all-button.d.ts +4 -0
  150. package/dist/web/primitives/data-table/reset-all-button.js +8 -0
  151. package/dist/web/primitives/data-table/server-group-header.d.ts +11 -0
  152. package/dist/web/primitives/data-table/server-group-header.js +51 -0
  153. package/dist/web/primitives/data-table/sortable-header.d.ts +17 -0
  154. package/dist/web/primitives/data-table/sortable-header.js +41 -0
  155. package/dist/web/primitives/data-table/table-chrome.d.ts +13 -0
  156. package/dist/web/primitives/data-table/table-chrome.js +30 -0
  157. package/dist/web/primitives/data-table/table-footer.d.ts +7 -0
  158. package/dist/web/primitives/data-table/table-footer.js +20 -0
  159. package/dist/web/primitives/data-table/toolbar-end-cluster.d.ts +10 -0
  160. package/dist/web/primitives/data-table/toolbar-end-cluster.js +13 -0
  161. package/dist/web/primitives/data-table/totals-footer.d.ts +24 -0
  162. package/dist/web/primitives/data-table/totals-footer.js +48 -0
  163. package/dist/web/primitives/data-table/use-bulk-selection.d.ts +21 -0
  164. package/dist/web/primitives/data-table/use-bulk-selection.js +66 -0
  165. package/dist/web/primitives/data-table/use-column-pins.d.ts +32 -0
  166. package/dist/web/primitives/data-table/use-column-pins.js +117 -0
  167. package/dist/web/primitives/data-table/use-column-sizing.d.ts +22 -0
  168. package/dist/web/primitives/data-table/use-column-sizing.js +44 -0
  169. package/dist/web/primitives/data-table/use-measured-column-widths.d.ts +15 -0
  170. package/dist/web/primitives/data-table/use-measured-column-widths.js +83 -0
  171. package/dist/web/primitives/data-table/use-row-keyboard-navigation.d.ts +22 -0
  172. package/dist/web/primitives/data-table/use-row-keyboard-navigation.js +65 -0
  173. package/dist/web/primitives/data-table/use-row-selection.d.ts +14 -0
  174. package/dist/web/primitives/data-table/use-row-selection.js +48 -0
  175. package/dist/web/primitives/data-table/use-view-columns.d.ts +6 -0
  176. package/dist/web/primitives/data-table/use-view-columns.js +17 -0
  177. package/dist/web/primitives/data-table/view-menu.d.ts +8 -0
  178. package/dist/web/primitives/data-table/view-menu.js +45 -0
  179. package/dist/web/primitives/data-table-cell.d.ts +0 -22
  180. package/dist/web/primitives/data-table-cell.js +1 -58
  181. package/dist/web/primitives/data-table.d.ts +1 -1
  182. package/dist/web/primitives/data-table.js +125 -146
  183. package/dist/web/primitives/facet-filter.js +1 -1
  184. package/dist/web/primitives/file-thumbnail.d.ts +3 -0
  185. package/dist/web/primitives/file-thumbnail.js +5 -0
  186. package/dist/web/primitives/file-value-list.d.ts +1 -1
  187. package/dist/web/primitives/file-value-list.js +5 -5
  188. package/dist/web/primitives/input.js +6 -1
  189. package/dist/web/primitives/layout-elements.js +6 -4
  190. package/dist/web/primitives/number-range-facet.d.ts +7 -0
  191. package/dist/web/primitives/number-range-facet.js +33 -0
  192. package/dist/web/primitives/page-header.js +3 -1
  193. package/dist/web/primitives/row-actions.js +3 -3
  194. package/dist/web/primitives/use-focus-on-mount.d.ts +2 -0
  195. package/dist/web/primitives/use-focus-on-mount.js +9 -0
  196. package/dist/web/widgets/bounded-load-cache.d.ts +5 -1
  197. package/dist/web/widgets/bounded-load-cache.js +49 -21
  198. package/dist/web/widgets/choice-cards.d.ts +17 -0
  199. package/dist/web/widgets/choice-cards.js +12 -0
  200. package/dist/web/widgets/collapsible-section.js +1 -1
  201. package/dist/web/widgets/detail-list.d.ts +5 -1
  202. package/dist/web/widgets/detail-list.js +5 -1
  203. package/dist/web/widgets/index.d.ts +1 -0
  204. package/dist/web/widgets/index.js +1 -0
  205. package/dist/web/widgets/pdf-preview.js +10 -5
  206. package/dist/web/widgets/plan-card.d.ts +2 -0
  207. package/dist/web/widgets/plan-card.js +1 -1
  208. package/dist/web/widgets/upload-zone.js +13 -5
  209. package/package.json +5 -5
  210. package/src/changes.json +158 -0
@@ -0,0 +1,13 @@
1
+ import type { ListStateStore } from "../components/list-state-store.tsx";
2
+ import { type KnownListFields } from "./list-state-schema.ts";
3
+ type RememberedListParamsArgs = {
4
+ readonly screenId: string;
5
+ readonly enabled: boolean;
6
+ readonly store: ListStateStore;
7
+ readonly known: KnownListFields;
8
+ };
9
+ /** URL stays the source of truth: the stored list params are copied into the
10
+ * URL once when the screen opens without any, and every later URL change is
11
+ * written back. A link that already carries list params is never overridden. */
12
+ export declare function useRememberedListParams({ screenId, enabled, store, known, }: RememberedListParamsArgs): void;
13
+ export {};
@@ -0,0 +1,53 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+ import { useNav } from "../app/nav.js";
3
+ import { listParamKeys } from "./list-reset.js";
4
+ import { sanitizeStoredListState } from "./list-state-schema.js";
5
+ function stripScreenPrefix(params, screenId) {
6
+ const prefix = `${screenId}.`;
7
+ return Object.fromEntries(listParamKeys(params, screenId).flatMap((key) => {
8
+ const value = params[key];
9
+ return value === undefined ? [] : [[key.slice(prefix.length), value]];
10
+ }));
11
+ }
12
+ function sameParams(a, b) {
13
+ const keys = Object.keys(a);
14
+ return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);
15
+ }
16
+ /** URL stays the source of truth: the stored list params are copied into the
17
+ * URL once when the screen opens without any, and every later URL change is
18
+ * written back. A link that already carries list params is never overridden. */
19
+ export function useRememberedListParams({ screenId, enabled, store, known, }) {
20
+ const nav = useNav();
21
+ const current = useMemo(() => stripScreenPrefix(nav.searchParams, screenId), [nav.searchParams, screenId]);
22
+ const phaseRef = useRef({ screenId, phase: "pending" });
23
+ useEffect(() => {
24
+ // skip: remembering is not enabled
25
+ if (!enabled)
26
+ return;
27
+ if (phaseRef.current.screenId !== screenId)
28
+ phaseRef.current = { screenId, phase: "pending" };
29
+ const state = phaseRef.current;
30
+ const currentIsEmpty = Object.keys(current).length === 0;
31
+ if (state.phase === "pending" && currentIsEmpty) {
32
+ const stored = sanitizeStoredListState(store.read(screenId), known)?.params;
33
+ if (stored !== undefined && Object.keys(stored).length > 0) {
34
+ state.phase = "restoring";
35
+ nav.setSearchParams(Object.fromEntries(Object.entries(stored).map(([k, v]) => [`${screenId}.${k}`, v])));
36
+ // skip: restore started, effect reruns with the restored URL
37
+ return;
38
+ }
39
+ }
40
+ // skip: waiting for the restored URL to arrive
41
+ if (state.phase === "restoring" && currentIsEmpty)
42
+ return;
43
+ state.phase = "live";
44
+ const existing = store.read(screenId);
45
+ // skip: nothing stored and nothing to store
46
+ if (existing?.params === undefined && currentIsEmpty)
47
+ return;
48
+ // skip: stored state already matches the URL
49
+ if (existing?.params !== undefined && sameParams(existing.params, current))
50
+ return;
51
+ store.write(screenId, { ...existing, params: current });
52
+ }, [enabled, screenId, current, known, store, nav]);
53
+ }
@@ -0,0 +1,26 @@
1
+ import { type Dispatch, type SetStateAction } from "react";
2
+ import type { ListStateStore } from "../components/list-state-store.tsx";
3
+ import { type KnownListFields } from "./list-state-schema.ts";
4
+ import type { ListSort } from "./use-list-url-state.ts";
5
+ type LocalFilters = Readonly<Record<string, readonly string[]>>;
6
+ type LocalListStateArgs = {
7
+ readonly storeKey: string;
8
+ readonly store: ListStateStore;
9
+ /** `table.rememberState`: seeds from the store and writes every change back. */
10
+ readonly remember: boolean;
11
+ readonly defaultSort: ListSort | null;
12
+ readonly known: KnownListFields;
13
+ };
14
+ /** Search, facet filters and sort of a list whose state is component-local
15
+ * (relatedList has no URL namespace). Same stored `params` format as the URL lists. */
16
+ export declare function useRememberedLocalListState({ storeKey, store, remember, defaultSort, known, }: LocalListStateArgs): {
17
+ search: string;
18
+ filters: LocalFilters;
19
+ sort: ListSort | null;
20
+ setSearch: (search: string) => void;
21
+ setFilters: Dispatch<SetStateAction<Readonly<Record<string, readonly string[]>>>>;
22
+ setSort: (sort: ListSort | null) => void;
23
+ reset: () => void;
24
+ atDefault: boolean;
25
+ };
26
+ export {};
@@ -0,0 +1,63 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { sanitizeStoredListState } from "./list-state-schema.js";
3
+ const FILTER_PREFIX = "f.";
4
+ function sameSort(a, b) {
5
+ return a === b || (a !== null && b !== null && a.field === b.field && a.dir === b.dir);
6
+ }
7
+ function toParams(state, defaultSort) {
8
+ const params = {};
9
+ if (state.search !== "")
10
+ params["q"] = state.search;
11
+ if (!sameSort(state.sort, defaultSort) && state.sort !== null) {
12
+ params["sort"] = state.sort.field;
13
+ params["dir"] = state.sort.dir;
14
+ }
15
+ for (const [field, values] of Object.entries(state.filters)) {
16
+ if (values.length > 0)
17
+ params[`${FILTER_PREFIX}${field}`] = values.join(",");
18
+ }
19
+ return params;
20
+ }
21
+ function fromParams(params, defaultSort) {
22
+ const sortField = params?.["sort"];
23
+ const dir = params?.["dir"];
24
+ const sort = sortField !== undefined && (dir === "asc" || dir === "desc")
25
+ ? { field: sortField, dir }
26
+ : defaultSort;
27
+ const filters = {};
28
+ for (const [key, value] of Object.entries(params ?? {})) {
29
+ if (key.startsWith(FILTER_PREFIX)) {
30
+ filters[key.slice(FILTER_PREFIX.length)] = value.split(",").filter((v) => v !== "");
31
+ }
32
+ }
33
+ return { search: params?.["q"] ?? "", filters, sort };
34
+ }
35
+ /** Search, facet filters and sort of a list whose state is component-local
36
+ * (relatedList has no URL namespace). Same stored `params` format as the URL lists. */
37
+ export function useRememberedLocalListState({ storeKey, store, remember, defaultSort, known, }) {
38
+ const [state, setState] = useState(() => remember
39
+ ? fromParams(sanitizeStoredListState(store.read(storeKey), known)?.params, defaultSort)
40
+ : { search: "", filters: {}, sort: defaultSort });
41
+ useEffect(() => {
42
+ // skip: remembering is not enabled
43
+ if (!remember)
44
+ return;
45
+ const params = toParams(state, defaultSort);
46
+ const existing = store.read(storeKey);
47
+ // skip: nothing stored and nothing to store
48
+ if (existing?.params === undefined && Object.keys(params).length === 0)
49
+ return;
50
+ store.write(storeKey, { ...existing, params });
51
+ }, [remember, state, defaultSort, store, storeKey]);
52
+ const setSearch = useCallback((search) => setState((prev) => ({ ...prev, search })), []);
53
+ const setFilters = useCallback((next) => setState((prev) => ({
54
+ ...prev,
55
+ filters: typeof next === "function" ? next(prev.filters) : next,
56
+ })), []);
57
+ const setSort = useCallback((sort) => setState((prev) => ({ ...prev, sort })), []);
58
+ const reset = useCallback(() => setState({ search: "", filters: {}, sort: defaultSort }), [defaultSort]);
59
+ const atDefault = state.search === "" &&
60
+ Object.values(state.filters).every((values) => values.length === 0) &&
61
+ sameSort(state.sort, defaultSort);
62
+ return { ...state, setSearch, setFilters, setSort, reset, atDefault };
63
+ }
@@ -1,6 +1,10 @@
1
- // Framework default bundle. These strings are required by renderer components (Save/Cancel/Delete buttons, empty states, search placeholder, navigation-toggle ARIA labels, and validation reasons). createKumikoApp adds this as the last fallback in LocaleProvider, while apps can override individual keys via clientFeatures.translations.
1
+ // Framework default bundle. These strings are required by renderer components (Save/Cancel/Delete
2
+ // buttons, empty states, search placeholder, navigation-toggle ARIA labels, and validation
3
+ // reasons). createKumikoApp adds this as the last fallback in LocaleProvider, while apps can
4
+ // override individual keys via clientFeatures.translations.
2
5
  //
3
- // All keys use the `kumiko.` prefix to avoid collisions with app keys. Subpaths group keys by area (actions / list / nav / form / validation).
6
+ // All keys use the `kumiko.` prefix to avoid collisions with app keys. Subpaths group keys by area
7
+ // (actions / list / nav / form / validation).
4
8
  export const kumikoDefaultTranslations = {
5
9
  en: {
6
10
  "kumiko.actions.save": "Save",
@@ -82,6 +86,44 @@ export const kumikoDefaultTranslations = {
82
86
  "kumiko.list.sort.unsorted": "Unsorted",
83
87
  "kumiko.list.filter.reset": "Reset",
84
88
  "kumiko.list.filter.toggle": "Filters",
89
+ "kumiko.list.reset-all": "Reset",
90
+ "kumiko.list.export": "Export",
91
+ "kumiko.list.export-busy": "Exporting…",
92
+ "kumiko.list.export-too-many": "More than {max} rows. Narrow the list with a filter and export again.",
93
+ "kumiko.list.export-failed": "The export failed. Please try again.",
94
+ "kumiko.list.view.menu": "View",
95
+ "kumiko.list.view.columns": "Columns",
96
+ "kumiko.list.view.density": "Density",
97
+ "kumiko.list.view.density.comfortable": "Comfortable",
98
+ "kumiko.list.view.density.compact": "Compact",
99
+ "kumiko.list.view.reset": "Reset view",
100
+ "kumiko.list.group.by": "Group by",
101
+ "kumiko.list.group.none": "None",
102
+ "kumiko.list.group.empty": "(empty)",
103
+ "kumiko.list.group.load-more": "Load more",
104
+ "kumiko.list.group.loading": "Loading…",
105
+ "kumiko.list.group.loaded": "{loaded} of {count}",
106
+ "kumiko.list.measure.sum": "Sum of {label}",
107
+ "kumiko.list.measure.avg": "Average {label}",
108
+ "kumiko.list.measure.count": "Count",
109
+ "kumiko.list.measure.min": "Minimum {label}",
110
+ "kumiko.list.measure.max": "Maximum {label}",
111
+ "kumiko.list.measure.kind.sum": "Sum",
112
+ "kumiko.list.measure.kind.avg": "Average",
113
+ "kumiko.list.measure.kind.count": "Count",
114
+ "kumiko.list.measure.kind.min": "Minimum",
115
+ "kumiko.list.measure.kind.max": "Maximum",
116
+ "kumiko.list.totals.label": "Total",
117
+ "kumiko.list.range.min": "From",
118
+ "kumiko.list.range.max": "To",
119
+ "kumiko.list.notice.truncated": "Not every group is shown. Please narrow the list with a filter.",
120
+ "kumiko.list.notice.searchTruncated": "The search matched too many entries; groups and totals cover only part of them. Please filter further.",
121
+ "kumiko.list.view.column.move-up": "Move {column} up",
122
+ "kumiko.list.view.pinning": "Pinned columns",
123
+ "kumiko.list.view.pin-left": "Pin {column} left",
124
+ "kumiko.list.view.pin-right": "Pin {column} right",
125
+ "kumiko.list.column.resize": "Resize {column}",
126
+ "kumiko.list.view.column.move-down": "Move {column} down",
85
127
  "kumiko.list.related-list-truncated": "Showing the first {count} entries. More are available but not loaded — this list does not paginate.",
86
128
  "kumiko.reference.system-tenant": "System",
87
129
  "kumiko.reference.system-user": "System",
@@ -107,6 +149,21 @@ export const kumikoDefaultTranslations = {
107
149
  "kumiko.theme.light": "Light theme",
108
150
  "kumiko.list.row.collapse": "Collapse {title}",
109
151
  "kumiko.list.row.expand": "Expand {title}",
152
+ "kumiko.list.bulk.selected": "{count} selected",
153
+ "kumiko.list.bulk.clear": "Clear selection",
154
+ "kumiko.list.bulk.limit": "At most {max} rows per selection",
155
+ "kumiko.list.bulk.select-all": "Select all rows on this page",
156
+ "kumiko.list.bulk.select-row": "Select {title}",
157
+ "kumiko.list.bulk.progress": "{settled} of {total} done",
158
+ "kumiko.list.bulk.cancel": "Cancel",
159
+ "kumiko.list.bulk.cancelling": "Cancelling…",
160
+ "kumiko.list.bulk.confirm": "Run “{action}” on the selection ({count})?",
161
+ "kumiko.list.bulk.result.title": "“{action}” finished",
162
+ "kumiko.list.bulk.result.succeeded": "Succeeded: {count}",
163
+ "kumiko.list.bulk.result.failed": "Failed: {count}",
164
+ "kumiko.list.bulk.result.skipped": "Skipped: {count}",
165
+ "kumiko.list.bulk.result.not-started": "Not run: {count}",
166
+ "kumiko.list.bulk.result.dismiss": "Dismiss",
110
167
  "kumiko.list.sort.by": "Sorted by {column}",
111
168
  "kumiko.form.changed": "changed",
112
169
  "kumiko.form.on-this-page": "On this page",
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ export { type Formality, formalLocaleTag } from "@cosmicdrift/kumiko-framework/u
2
2
  export { synthesizeActionFormEntity, synthesizeActionFormScreen } from "./app/action-form-shim.ts";
3
3
  export type { AppFeaturesProviderProps } from "./app/app-features-context.tsx";
4
4
  export { AppFeaturesProvider, useAppFeatures } from "./app/app-features-context.tsx";
5
+ export { normalizeBasePath, prependBasePath, stripBasePath } from "./app/base-path.ts";
6
+ export { BasePathProvider, useBasePath } from "./app/base-path-context.tsx";
5
7
  export type { ActionNotice, ActionNotifier, ClientActionHandler, ClientActionOutcome, ClientActionRuntime, ClientActionsMap, } from "./app/client-actions.tsx";
6
8
  export { ActionNoticeProvider, ClientActionsProvider, runClientAction, useClientActionHandler, useClientActionRuntime, } from "./app/client-actions.tsx";
7
9
  export type { ColumnRendererComponent, ColumnRendererProps, ColumnRenderersMap, ColumnRenderersProviderProps, } from "./app/column-renderers.tsx";
@@ -14,6 +16,7 @@ export type { CustomScreensMap, CustomScreensProviderProps } from "./app/custom-
14
16
  export { CustomScreensProvider, useCustomScreenComponent } from "./app/custom-screens.tsx";
15
17
  export type { DashboardBodyProps, DashboardBodyProviderProps } from "./app/dashboard-body.tsx";
16
18
  export { DashboardBodyProvider, useDashboardBody } from "./app/dashboard-body.tsx";
19
+ export { type TenantCurrencyResolution, useTenantCurrency } from "./app/entity-edit-prefill.ts";
17
20
  export { type ErrorNotice, ErrorReference, errorNoticeOf } from "./app/error-reference.tsx";
18
21
  export type { ExtensionFormRegistry, ExtensionFormSubmitHandler, ExtensionSubmitContext, ExtensionSubmitResult, } from "./app/extension-form-submit.tsx";
19
22
  export { ExtensionFormRegistryProvider, useExtensionFormSubmit, } from "./app/extension-form-submit.tsx";
@@ -85,7 +88,7 @@ export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.ts";
85
88
  export { hasUnsavedInput, registerUnsavedInputSource, requestLeave, setLeaveConfirmer, useUnsavedInputSource, } from "./leave-guard.ts";
86
89
  export { LeaveGuardDialog } from "./leave-guard-dialog.tsx";
87
90
  export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.ts";
88
- export type { ActionMenuItemSpec, ActionOverflowMenuProps, AppPrimitives, BannerProps, ButtonProps, CardOptions, CardProps, CardSlots, CopyButtonProps, CorePrimitives, DataTableDateRangeFacet, DataTableFacet, DataTableFacetChip, DataTableProps, DataTableRowAction, DataTableRowActionMode, DataTableRowGrouping, DataTableSort, DataTableSortDir, DialogProps, DrawerProps, EmbeddedListCellType, EmbeddedListColumn, EmbeddedListInputProps, EmbeddedListTotal, FieldCellWidth, FieldProps, FileValueListProps, FillContainerProps, FooterActionMarker, FooterActionRole, FormProps, FormSectionNavItem, FormWidth, GridCellProps, GridProps, HeadingProps, InputFillAction, InputProps, JsonViewProps, LightboxImage, LightboxProps, LinkProps, MetricBandProps, MetricProps, ModalProps, PageHeaderProps, PickedFile, PrimitivesProviderProps, PrimitivesRegistry, ProgressProps, ProgressTone, PromoPanelAction, PromoPanelProps, RuntimeRenderer, SecretRevealProps, SecretRevealValue, SectionProps, ShareButtonProps, StatusBadgeProps, StatusTone, StepBarProps, StickyActionBarProps, StickyPrimaryActionMarker, TabsProps, TextProps, TrailingInputAction, WizardStepGroupProps, } from "./primitives.tsx";
91
+ export type { ActionMenuItemSpec, ActionOverflowMenuProps, AppPrimitives, BannerProps, BulkRowOutcome, ButtonProps, CardOptions, CardProps, CardSlots, CopyButtonProps, CorePrimitives, DataTableBulk, DataTableBulkAction, DataTableDateRangeFacet, DataTableFacet, DataTableFacetChip, DataTableProps, DataTableRowAction, DataTableRowActionMode, DataTableSort, DataTableSortDir, DialogProps, DrawerProps, EmbeddedListCellType, EmbeddedListColumn, EmbeddedListInputProps, EmbeddedListTotal, FieldCellWidth, FieldProps, FileValueListProps, FillContainerProps, FooterActionMarker, FooterActionRole, FormProps, FormSectionNavItem, FormWidth, GridCellProps, GridProps, HeadingProps, InputFillAction, InputProps, JsonViewProps, LightboxImage, LightboxProps, LinkProps, MetricBandProps, MetricProps, ModalProps, PageHeaderProps, PickedFile, PrimitivesProviderProps, PrimitivesRegistry, ProgressProps, ProgressTone, PromoPanelAction, PromoPanelProps, RuntimeRenderer, SecretRevealProps, SecretRevealValue, SectionProps, ShareButtonProps, StatusBadgeProps, StatusTone, StepBarProps, StickyActionBarProps, StickyPrimaryActionMarker, TabsProps, TextProps, TrailingInputAction, WizardStepGroupProps, } from "./primitives.tsx";
89
92
  export { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, useOptionalPrimitives, usePrimitives, } from "./primitives.tsx";
90
93
  export type { SessionEndedSignal, SessionEndedSignalProviderProps, } from "./session/session-ended.tsx";
91
94
  export { createSessionEndedSignal, isSessionEndedError, SESSION_ENDED_ERROR_CODES, SessionEndedSignalProvider, useSessionEndedSignal, withSessionEndedDetection, } from "./session/session-ended.tsx";
package/dist/index.js CHANGED
@@ -11,12 +11,15 @@
11
11
  export { formalLocaleTag } from "@cosmicdrift/kumiko-framework/ui-types";
12
12
  export { synthesizeActionFormEntity, synthesizeActionFormScreen } from "./app/action-form-shim.js";
13
13
  export { AppFeaturesProvider, useAppFeatures } from "./app/app-features-context.js";
14
+ export { normalizeBasePath, prependBasePath, stripBasePath } from "./app/base-path.js";
15
+ export { BasePathProvider, useBasePath } from "./app/base-path-context.js";
14
16
  export { ActionNoticeProvider, ClientActionsProvider, runClientAction, useClientActionHandler, useClientActionRuntime, } from "./app/client-actions.js";
15
17
  export { ColumnRenderersProvider, useColumnRenderer } from "./app/column-renderers.js";
16
18
  export { CONTENT_EDITOR_ELEMENT_ID, ContentEditorsProvider, TextareaContentEditor, useContentEditor, } from "./app/content-editors.js";
17
19
  export { ContentPreview, substituteVariables } from "./app/content-preview.js";
18
20
  export { CustomScreensProvider, useCustomScreenComponent } from "./app/custom-screens.js";
19
21
  export { DashboardBodyProvider, useDashboardBody } from "./app/dashboard-body.js";
22
+ export { useTenantCurrency } from "./app/entity-edit-prefill.js";
20
23
  export { ErrorReference, errorNoticeOf } from "./app/error-reference.js";
21
24
  export { ExtensionFormRegistryProvider, useExtensionFormSubmit, } from "./app/extension-form-submit.js";
22
25
  export { ExtensionSectionsProvider, extensionSectionName, useExtensionSectionComponent, } from "./app/extension-sections.js";
@@ -1,6 +1,9 @@
1
1
  import type { ConfigCascade, ConfigScope, ConfigValueSource } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import type { FieldIconKey, FormWidth, IconKey, NavIconKey, RowActionDisplay, SelectOptionTone } from "@cosmicdrift/kumiko-framework/ui-types";
3
3
  import { type ComponentType, type FormEvent, type KeyboardEvent, type ReactNode, type Ref } from "react";
4
+ import type { DataTableBulk } from "./data-table-bulk-types.ts";
5
+ import type { DataTableDateRangeFacet, DataTableFacet } from "./data-table-facet-types.ts";
6
+ import type { DataTableTableBlock } from "./data-table-view-types.ts";
4
7
  import type { FieldIssue, ListColumnViewModel, ListRowViewModel, RuntimeRenderer } from "./headless/index.ts";
5
8
  export type { RuntimeRenderer };
6
9
  /** Escape hatch for `data-*` test hooks (e.g. an E2E selector) that don't
@@ -203,6 +206,7 @@ export type InputProps = {
203
206
  readonly name: string;
204
207
  readonly value: string;
205
208
  readonly onChange: (v: string) => void;
209
+ readonly autoFocus?: boolean;
206
210
  readonly disabled?: boolean;
207
211
  readonly required?: boolean;
208
212
  readonly hasError?: boolean;
@@ -228,6 +232,24 @@ export type InputProps = {
228
232
  /** Raw keydown access (e.g. Enter-to-submit at the field instead of
229
233
  * the surrounding Form). Forwarded to the element. */
230
234
  readonly onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
235
+ } | {
236
+ /** Fixed-length numeric one-time code. `onComplete` fires once when input,
237
+ * paste or autofill brings the value to full length; the value holds
238
+ * digits only. */
239
+ readonly kind: "code";
240
+ readonly id: string;
241
+ readonly name: string;
242
+ readonly value: string;
243
+ readonly onChange: (v: string) => void;
244
+ readonly length: number;
245
+ readonly onComplete?: (code: string) => void;
246
+ readonly autoFocus?: boolean;
247
+ readonly disabled?: boolean;
248
+ readonly required?: boolean;
249
+ readonly hasError?: boolean;
250
+ readonly testId?: string;
251
+ /** Default "one-time-code". */
252
+ readonly autoComplete?: string;
231
253
  } | {
232
254
  readonly kind: "email";
233
255
  readonly id: string;
@@ -628,55 +650,11 @@ export type DataTableRowAction = {
628
650
  export declare function shouldRenderActionsIconOnly(actions: readonly {
629
651
  readonly icon?: IconKey;
630
652
  }[]): boolean;
631
- export type DataTableFacet = {
632
- /** Entity-Feldname (= URL-State-Key + payload.filters[].field). */
633
- readonly field: string;
634
- /** Translated Button-Label ("Status", "Active"). */
635
- readonly label: string;
636
- /** Selectable values with display labels. `value` is the raw filter value
637
- * (a select option or `"true"`/`"false"`), transported as a string. */
638
- readonly options: readonly {
639
- readonly value: string;
640
- readonly label: string;
641
- }[];
642
- /** Present: render the facet as a row of single-choice toggle chips instead
643
- * of a dropdown. A chip is pressed while the selection equals its values. */
644
- readonly chips?: readonly DataTableFacetChip[];
645
- /** Chips only: show each chip's `count` next to its label. */
646
- readonly showCounts?: boolean;
647
- /** Chips only: hide chips whose `count` is 0 (unless pressed). */
648
- readonly hideEmpty?: boolean;
649
- };
650
- export type DataTableFacetChip = {
651
- readonly id: string;
652
- /** Translated chip label. */
653
- readonly label: string;
654
- /** Selection a click sets; `[]` clears the facet. */
655
- readonly values: readonly string[];
656
- /** Matching row count, undefined while unknown. */
657
- readonly count?: number;
658
- };
659
- export type DataTableDateRangeFacet = {
660
- /** The facet's namespace (its date field) — a key, not a filter value. */
661
- readonly field: string;
662
- /** Translated label ("When"). */
663
- readonly label: string;
664
- readonly from: string;
665
- readonly to: string;
666
- };
667
- /** Splits rows into collapsible groups (order of first appearance). A
668
- * renderer without grouping support shows the flat list. A group whose
669
- * `headerLabel` is undefined shows its rows without a header and never
670
- * collapses. */
671
- export type DataTableRowGrouping = {
672
- readonly keyOf: (row: ListRowViewModel) => string;
673
- readonly headerLabel: (groupKey: string, rows: readonly ListRowViewModel[]) => string | undefined;
674
- readonly startsCollapsed: (groupKey: string) => boolean;
675
- };
653
+ export type { BulkRowOutcome, DataTableBulk, DataTableBulkAction, } from "./data-table-bulk-types.ts";
654
+ export type { DataTableDateRangeFacet, DataTableFacet, DataTableFacetChip, } from "./data-table-facet-types.ts";
676
655
  export type DataTableProps = {
677
656
  readonly columns: readonly ListColumnViewModel[];
678
657
  readonly rows: readonly ListRowViewModel[];
679
- readonly rowGrouping?: DataTableRowGrouping;
680
658
  /** Tints a row. Renderers without row tinting ignore it. */
681
659
  readonly rowTone?: (row: ListRowViewModel) => SelectOptionTone | undefined;
682
660
  readonly onRowClick?: (row: ListRowViewModel) => void;
@@ -704,6 +682,8 @@ export type DataTableProps = {
704
682
  * kebab). Actions then sit at the same x-position across all rows
705
683
  * (no drifting with label width) and all lists in an app look alike. */
706
684
  readonly rowActionMode?: DataTableRowActionMode;
685
+ /** Present: row checkboxes plus a selection bar that runs these actions once per selected row. */
686
+ readonly bulk?: DataTableBulk;
707
687
  /** Custom empty-state content (e.g. Icon + Heading + CTA-Button).
708
688
  * The default renderer wraps it in a dashed-border box. */
709
689
  readonly emptyState?: ReactNode;
@@ -813,7 +793,8 @@ export type DataTableProps = {
813
793
  /** Content of the area under an expanded row. Native renderers implement
814
794
  * the same three props. */
815
795
  readonly renderExpandedRow?: (row: ListRowViewModel) => ReactNode;
816
- };
796
+ } & DataTableTableBlock;
797
+ export type DataTableTableBlockProps = Pick<DataTableProps, keyof DataTableTableBlock>;
817
798
  /** Cell type for one column of an embedded-list field. Mirrors
818
799
  * EmbeddedListCellViewModel["type"] from `@cosmicdrift/kumiko-renderer/headless`. */
819
800
  export type EmbeddedListCellType = "text" | "number" | "boolean" | "date" | "money" | "decimal" | "select" | "reference" | "timestamp";
@@ -1384,6 +1365,8 @@ export type JsonViewProps = {
1384
1365
  * PDFs, an inline preview. Used for `renderer: { format: "file" }` on a detail field. */
1385
1366
  export type FileValueListProps = {
1386
1367
  readonly fileIds: readonly string[];
1368
+ /** Starts the inline PDF preview of the first file open. */
1369
+ readonly previewOpenByDefault?: boolean;
1387
1370
  readonly testId?: string;
1388
1371
  };
1389
1372
  /** Band above the tabs of a record detail: optional subtitle line plus the
@@ -92,6 +92,11 @@ export type CreateKumikoAppOptions = {
92
92
  readonly hasWorkspaces?: boolean;
93
93
  readonly features?: readonly FeatureSchema[];
94
94
  }) => NavApi;
95
+ /** URL prefix the app is mounted under (for example "/admin"). The default nav adapter reads
96
+ * and writes URLs relative to it, and the auth gates and pages, which sit outside the
97
+ * NavProvider, use it to match and build their paths. A custom `navAdapter` handles its own
98
+ * prefix. */
99
+ readonly basePath?: string;
95
100
  /** App-wide default width for form/detail screens that don't set their
96
101
  * own `layout.width`. Default "4xl" (unchanged behavior) — a
97
102
  * consumer app that wants e.g. bundled-feature screens to match its own
@@ -3,9 +3,10 @@ import { isOpenToAllGranted } from "@cosmicdrift/kumiko-framework/ui-types";
3
3
  import { createContext, useContext, useEffect, useMemo, useRef, useState, } from "react";
4
4
  import { createRoot } from "react-dom/client";
5
5
  import { rendererError, rendererWarn } from "../../dev-console.js";
6
- import { AppFeaturesProvider, ClientActionsProvider, ColumnRenderersProvider, ContentEditorsProvider, CustomScreensProvider, createSessionEndedSignal, DashboardBodyProvider, DispatcherProvider, DraftStorageProvider, ErrorBoundary, ExtensionSectionsProvider, hasDetailScreen, KumikoScreen, kumikoDefaultTranslations, LeaveGuardDialog, LiveEventsProvider, LocaleProvider, mergeTranslations, NavProvider, navigateWithReturnTo, PrimitivesProvider, qualifyScreenId, ScreenMissingState, SessionEndedSignalProvider, TokensProvider, toAppSchema, translationsByLocaleFromKeys, useNav, usePrimitives, useTranslation, withSessionEndedDetection, } from "../../index.js";
6
+ import { AppFeaturesProvider, BasePathProvider, ClientActionsProvider, ColumnRenderersProvider, ContentEditorsProvider, CustomScreensProvider, createSessionEndedSignal, DashboardBodyProvider, DispatcherProvider, DraftStorageProvider, ErrorBoundary, ExtensionSectionsProvider, hasDetailScreen, KumikoScreen, kumikoDefaultTranslations, LeaveGuardDialog, LiveEventsProvider, LocaleProvider, mergeTranslations, NavProvider, navigateWithReturnTo, PrimitivesProvider, qualifyScreenId, ScreenMissingState, SessionEndedSignalProvider, TokensProvider, toAppSchema, translationsByLocaleFromKeys, useNav, usePrimitives, useTranslation, withSessionEndedDetection, } from "../../index.js";
7
7
  import { createLiveDispatcher } from "../../live/index.js";
8
8
  import { lastSegment } from "../layout/nav-tree.js";
9
+ import { clearFileMetaCache } from "../primitives/file-thumbnail.js";
9
10
  import { defaultPrimitives, ScreenWidthProvider } from "../primitives/index.js";
10
11
  import { ToastProvider } from "../primitives/toast.js";
11
12
  import { createEventSourceLiveEvents } from "../sse/live-events.js";
@@ -18,6 +19,7 @@ import { stackWrappers } from "./client-plugin.js";
18
19
  import { WebDashboardBody } from "./dashboard-body.js";
19
20
  import { DocumentLangSync } from "./document-lang-sync.js";
20
21
  import { createBrowserDraftStorage } from "./draft-storage.js";
22
+ import { ListStateStoreBoot } from "./list-state-store-boot.js";
21
23
  import { useBrowserNavApi } from "./nav.js";
22
24
  import { NavProvidersProvider } from "./nav-providers-context.js";
23
25
  import { ResolversProvider } from "./resolvers-context.js";
@@ -236,7 +238,7 @@ function KumikoAppRoot(props) {
236
238
  warnMissingCustomScreens(loaded, props.customScreens);
237
239
  setApp(loaded);
238
240
  }, schemaIsRoleProjected: isFetchMode, ...(isFetchMode && { onUnmountResetSchema: () => setApp(undefined) }), ...(props.translate !== undefined && { translate: props.translate }), ...(props.onRowClick !== undefined && { onRowClick: props.onRowClick }), ...(props.shell !== undefined && { shell: props.shell }) }));
239
- return (_jsx(TokensBoot, { children: _jsxs(LocaleProvider, { resolver: props.localeResolver, fallbackBundles: fallbackBundles, children: [_jsx(DocumentLangSync, { resolver: props.localeResolver }), _jsx(PrimitivesProvider, { value: props.primitives, children: _jsx(ScreenWidthProvider, { width: props.screenWidth, children: _jsx(AppFeaturesProvider, { features: app?.features ?? EMPTY_FEATURES, children: _jsx(DispatcherProvider, { dispatcher: props.dispatcher, children: _jsx(DraftStorageProvider, { value: props.draftStorage, children: _jsx(LiveEventsProvider, { value: props.liveEvents, children: _jsx(DashboardBodyProvider, { value: WebDashboardBody, children: _jsx(CustomScreensProvider, { value: props.customScreens, children: _jsx(ColumnRenderersProvider, { value: props.columnRenderers, children: _jsx(ContentEditorsProvider, { value: props.contentEditors, children: _jsx(ExtensionSectionsProvider, { value: props.extensionSectionComponents, children: _jsx(ClientActionsProvider, { value: props.clientActions, children: _jsx(NavProvidersProvider, { value: navProviders, entities: navEntities, children: _jsx(ResolversProvider, { resolvers: props.resolvers, children: _jsx(ToastProvider, { children: _jsxs(ToastActionNotifier, { children: [_jsx(UpdateChecker, {}), _jsx(LeaveGuardDialog, {}), _jsx(SessionEndedSignalProvider, { signal: props.sessionEndedSignal, children: stackWrappers(props.providers, stackWrappers(props.gates, screenNode)) })] }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) })] }) }));
241
+ return (_jsx(TokensBoot, { children: _jsxs(LocaleProvider, { resolver: props.localeResolver, fallbackBundles: fallbackBundles, children: [_jsx(DocumentLangSync, { resolver: props.localeResolver }), _jsx(PrimitivesProvider, { value: props.primitives, children: _jsx(ScreenWidthProvider, { width: props.screenWidth, children: _jsx(AppFeaturesProvider, { features: app?.features ?? EMPTY_FEATURES, children: _jsx(DispatcherProvider, { dispatcher: props.dispatcher, children: _jsx(DraftStorageProvider, { value: props.draftStorage, children: _jsx(LiveEventsProvider, { value: props.liveEvents, children: _jsx(DashboardBodyProvider, { value: WebDashboardBody, children: _jsx(CustomScreensProvider, { value: props.customScreens, children: _jsx(ColumnRenderersProvider, { value: props.columnRenderers, children: _jsx(ContentEditorsProvider, { value: props.contentEditors, children: _jsx(ExtensionSectionsProvider, { value: props.extensionSectionComponents, children: _jsx(ClientActionsProvider, { value: props.clientActions, children: _jsx(NavProvidersProvider, { value: navProviders, entities: navEntities, children: _jsx(ResolversProvider, { resolvers: props.resolvers, children: _jsx(ToastProvider, { children: _jsxs(ToastActionNotifier, { children: [_jsx(UpdateChecker, {}), _jsx(LeaveGuardDialog, {}), _jsx(SessionEndedSignalProvider, { signal: props.sessionEndedSignal, children: stackWrappers(props.providers, stackWrappers(props.gates, _jsx(ListStateStoreBoot, { children: screenNode }))) })] }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) })] }) }));
240
242
  }
241
243
  // Sits INSIDE the clientFeature gates (e.g. an auth gate) — a gate that
242
244
  // renders a login placeholder instead of children keeps this component
@@ -255,9 +257,13 @@ function AppSchemaBoundary({ app, screenQn, schemaUrl, navAdapter, onAppLoaded,
255
257
  onUnmountResetSchemaRef.current = onUnmountResetSchema;
256
258
  // A real unmount (the gate withdrawing children, e.g. on logout) resets
257
259
  // the schema so the next mount re-fetches instead of reusing this
258
- // session's — fetch-mode only, see isFetchMode in KumikoAppRoot.
260
+ // session's — fetch-mode only, see isFetchMode in KumikoAppRoot. The file
261
+ // meta cache is dropped in every mode.
259
262
  useEffect(() => {
260
- return () => onUnmountResetSchemaRef.current?.();
263
+ return () => {
264
+ clearFileMetaCache();
265
+ onUnmountResetSchemaRef.current?.();
266
+ };
261
267
  }, []);
262
268
  if (app === undefined) {
263
269
  return (_jsx(AppSchemaFetchBoot, { onLoaded: onAppLoaded, ...(schemaUrl !== undefined && { schemaUrl }) }));
@@ -335,7 +341,9 @@ export function createKumikoApp(options = {}) {
335
341
  }
336
342
  }
337
343
  const localeResolver = options.locale ?? createBrowserLocaleResolver();
338
- const navAdapter = options.navAdapter ?? useBrowserNavApi;
344
+ const { basePath } = options;
345
+ const useBrowserNavApiWithBasePath = (navOptions) => useBrowserNavApi({ ...navOptions, ...(basePath !== undefined && { basePath }) });
346
+ const navAdapter = options.navAdapter ?? useBrowserNavApiWithBasePath;
339
347
  const reporter = options.reportClientErrors === false ? undefined : createClientErrorReporter();
340
348
  reporter?.installGlobalHandlers();
341
349
  const handleBoundaryError = (error, info, context) => {
@@ -348,7 +356,7 @@ export function createKumikoApp(options = {}) {
348
356
  options.onError?.(error, info, context);
349
357
  };
350
358
  const root = createRoot(container);
351
- root.render(_jsx(ErrorBoundary, { scope: "root", onError: handleBoundaryError, fallback: ({ errorId }) => _jsx(RootErrorFallback, { errorId: errorId }), children: _jsx(BoundaryErrorHandlerContext.Provider, { value: handleBoundaryError, children: _jsx(KumikoAppRoot, { initialApp: initialApp, screenQn: options.screenQn, ...(options.schemaUrl !== undefined && { schemaUrl: options.schemaUrl }), clientFeatures: clientFeatures, providers: providers, gates: gates, customScreens: customScreens, columnRenderers: columnRenderers, extensionSectionComponents: extensionSectionComponents, clientActions: clientActions, contentEditors: contentEditors, resolvers: resolvers, dispatcher: dispatcher, sessionEndedSignal: sessionEndedSignal, draftStorage: draftStorage, primitives: primitives, liveEvents: liveEvents, localeResolver: localeResolver, navAdapter: navAdapter, screenWidth: options.screenWidth, ...(options.translate !== undefined && { translate: options.translate }), ...(options.onRowClick !== undefined && { onRowClick: options.onRowClick }), ...(options.shell !== undefined && { shell: options.shell }) }) }) }));
359
+ root.render(_jsx(ErrorBoundary, { scope: "root", onError: handleBoundaryError, fallback: ({ errorId }) => _jsx(RootErrorFallback, { errorId: errorId }), children: _jsx(BoundaryErrorHandlerContext.Provider, { value: handleBoundaryError, children: _jsx(BasePathProvider, { basePath: basePath, children: _jsx(KumikoAppRoot, { initialApp: initialApp, screenQn: options.screenQn, ...(options.schemaUrl !== undefined && { schemaUrl: options.schemaUrl }), clientFeatures: clientFeatures, providers: providers, gates: gates, customScreens: customScreens, columnRenderers: columnRenderers, extensionSectionComponents: extensionSectionComponents, clientActions: clientActions, contentEditors: contentEditors, resolvers: resolvers, dispatcher: dispatcher, sessionEndedSignal: sessionEndedSignal, draftStorage: draftStorage, primitives: primitives, liveEvents: liveEvents, localeResolver: localeResolver, navAdapter: navAdapter, screenWidth: options.screenWidth, ...(options.translate !== undefined && { translate: options.translate }), ...(options.onRowClick !== undefined && { onRowClick: options.onRowClick }), ...(options.shell !== undefined && { shell: options.shell }) }) }) }) }));
352
360
  return { root };
353
361
  }
354
362
  // TokensBoot uses the browser-backed TokensApi hook (class-based
@@ -498,7 +506,7 @@ function RoutedScreen({ app, fallbackQn, translate, onRowClick, }) {
498
506
  }, [nav]);
499
507
  if (feature === undefined || activeScreen === undefined) {
500
508
  const landingExists = findOwnerFeature(app.features, fallbackQn) !== undefined;
501
- return (_jsx(ScreenMissingState, { ...(landingExists && { homeScreenId: lastSegment(fallbackQn) }), screenQn: qn }));
509
+ return (_jsx(ScreenMissingState, { ...(landingExists && { homeScreenId: lastSegment(fallbackQn) }), screenQn: nav.route?.screenId ?? qn }));
502
510
  }
503
511
  // KumikoScreen takes a single-feature schema: the feature owning the active screen.
504
512
  const schemaForScreen = feature;
@@ -140,6 +140,20 @@ function useVisibilityGateResults() {
140
140
  }, []);
141
141
  return { results, report, forget };
142
142
  }
143
+ const NOOP_REFETCH = async () => { };
144
+ // A gate reports through an effect, so for its first commit (and after a payload change) no
145
+ // result exists yet; without a placeholder that reads as "hidden" and flashes the fallback.
146
+ function withPendingGates(results, gateKeys) {
147
+ if (gateKeys.every((key) => results.has(key)))
148
+ return results;
149
+ const merged = new Map(results);
150
+ for (const key of gateKeys) {
151
+ if (!merged.has(key)) {
152
+ merged.set(key, { data: null, error: null, loading: true, refetch: NOOP_REFETCH });
153
+ }
154
+ }
155
+ return merged;
156
+ }
143
157
  function distinctVisibilityGates(panels, screenParams) {
144
158
  const gates = new Map();
145
159
  for (const panel of panels) {
@@ -227,6 +241,8 @@ export function WebDashboardBody({ featureName, screen, translate, }) {
227
241
  const { loadedAtMs, reportLoaded } = useLoadedAt(screenParams);
228
242
  const visibilityGates = distinctVisibilityGates(screen.panels, screenParams);
229
243
  const gateResults = useVisibilityGateResults();
244
+ const gateKeysSignature = visibilityGates.map((gate) => gate.key).join("\n");
245
+ const gateResultsWithPending = useMemo(() => withPendingGates(gateResults.results, gateKeysSignature === "" ? [] : gateKeysSignature.split("\n")), [gateResults.results, gateKeysSignature]);
230
246
  // The record query is not a panel query: it needs the interval before the
231
247
  // registry context below exists, and `refresh.field` reads from its own row.
232
248
  const [refreshIntervalMs, setRefreshIntervalMs] = useState(() => resolveRefreshIntervalMs(screen.refresh, null));
@@ -271,7 +287,7 @@ export function WebDashboardBody({ featureName, screen, translate, }) {
271
287
  // The shell header owns the title; the badge and the actions sit in it there.
272
288
  const badgeInHeader = scopeBadge !== null && usesPageHeaderSlot;
273
289
  const showPageHeader = usesPageHeaderSlot && (badgeInHeader || recordTitle !== undefined || headerActionsBar !== null);
274
- return (_jsx(PanelLoadedContext.Provider, { value: reportLoaded, children: _jsx(DashboardQueryContext.Provider, { value: queryRuntime, children: _jsxs(PageSection, { className: "flex flex-col gap-4", testId: `dashboard-${screen.id}`, children: [showPageHeader && (_jsx(PageHeader, { ...(recordTitle !== undefined && { title: recordTitle }), ...(badgeInHeader && { status: scopeBadge }), ...(headerActionsBar !== null && { actions: headerActionsBar }) })), _jsxs("div", { className: "flex flex-wrap items-end justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [!usesPageHeaderSlot && recordTitle !== undefined && (_jsx(Heading, { variant: "page", testId: `dashboard-${screen.id}-record-title`, children: recordTitle })), !badgeInHeader && scopeBadge, picker] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [!usesPageHeaderSlot && headerActionsBar, screen.showUpdatedAt !== false && (_jsx("span", { "data-testid": `dashboard-${screen.id}-updated-at`, className: "text-xs tabular-nums text-muted-foreground", children: t("kumiko.dashboard.updated-at", { time: formatDateTime(loadedAtMs) }) })), rangeControl] })] }), actionError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-action-error", children: [actionError.message, _jsx(ErrorReference, { requestId: actionError.requestId })] })), recordQuery.error !== null && recordRow === null && (_jsxs("div", { role: "alert", "data-testid": `dashboard-${screen.id}-record-error`, className: "flex flex-col items-start gap-2 text-sm", children: [_jsx("span", { className: "text-status-bad", children: dispatcherErrorText(recordQuery.error, t) }), _jsx(Button, { variant: "secondary", onClick: () => void refetchRecord(), children: t("kumiko.dashboard.panel.error.retry") })] })), description !== undefined && (_jsx(Text, { variant: "muted", testId: `dashboard-${screen.id}-description`, children: description })), scope?.notice !== undefined && (_jsx(Banner, { variant: "info", testId: `dashboard-${screen.id}-scope-notice`, children: effectiveTranslate(scope.notice) })), _jsxs(VisibilityQueryContext.Provider, { value: gateResults.results, children: [_jsx(VisibilityGateQueries, { gates: visibilityGates, onResult: gateResults.report, onForget: gateResults.forget }), _jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: screen.panels.map((panel) => {
290
+ return (_jsx(PanelLoadedContext.Provider, { value: reportLoaded, children: _jsx(DashboardQueryContext.Provider, { value: queryRuntime, children: _jsxs(PageSection, { className: "flex flex-col gap-4", testId: `dashboard-${screen.id}`, children: [showPageHeader && (_jsx(PageHeader, { ...(recordTitle !== undefined && { title: recordTitle }), ...(badgeInHeader && { status: scopeBadge }), ...(headerActionsBar !== null && { actions: headerActionsBar }) })), _jsxs("div", { className: "flex flex-wrap items-end justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [!usesPageHeaderSlot && recordTitle !== undefined && (_jsx(Heading, { variant: "page", testId: `dashboard-${screen.id}-record-title`, children: recordTitle })), !badgeInHeader && scopeBadge, picker] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [!usesPageHeaderSlot && headerActionsBar, screen.showUpdatedAt !== false && (_jsx("span", { "data-testid": `dashboard-${screen.id}-updated-at`, className: "text-xs tabular-nums text-muted-foreground", children: t("kumiko.dashboard.updated-at", { time: formatDateTime(loadedAtMs) }) })), rangeControl] })] }), actionError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-action-error", children: [actionError.message, _jsx(ErrorReference, { requestId: actionError.requestId })] })), recordQuery.error !== null && recordRow === null && (_jsxs("div", { role: "alert", "data-testid": `dashboard-${screen.id}-record-error`, className: "flex flex-col items-start gap-2 text-sm", children: [_jsx("span", { className: "text-status-bad", children: dispatcherErrorText(recordQuery.error, t) }), _jsx(Button, { variant: "secondary", onClick: () => void refetchRecord(), children: t("kumiko.dashboard.panel.error.retry") })] })), description !== undefined && (_jsx(Text, { variant: "muted", testId: `dashboard-${screen.id}-description`, children: description })), scope?.notice !== undefined && (_jsx(Banner, { variant: "info", testId: `dashboard-${screen.id}-scope-notice`, children: effectiveTranslate(scope.notice) })), _jsxs(VisibilityQueryContext.Provider, { value: gateResultsWithPending, children: [_jsx(VisibilityGateQueries, { gates: visibilityGates, onResult: gateResults.report, onForget: gateResults.forget }), _jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: screen.panels.map((panel) => {
275
291
  if (panel.kind === "screen") {
276
292
  return (_jsx(ScreenPanelTile, { panel: panel, featureName: featureName, translate: effectiveTranslate }, panel.id));
277
293
  }
@@ -311,7 +311,7 @@ function TextBlockView({ block, translate, }) {
311
311
  ? "text-sm font-medium text-foreground"
312
312
  : "text-sm text-muted-foreground", children: translate(block.text) }));
313
313
  case "list":
314
- return (_jsx("ul", { "data-block": "list", className: "list-disc space-y-1 pl-5 text-sm text-muted-foreground", children: block.items.map((item) => (_jsx("li", { children: translate(item) }, item))) }));
314
+ return (_jsx("ul", { "data-block": "list", className: "list-disc space-y-1 pl-5 text-sm text-muted-foreground", children: withOccurrenceKeys(block.items, (item) => item).map(({ entry, key }) => (_jsx("li", { children: translate(entry) }, key))) }));
315
315
  case "code":
316
316
  return (_jsx("pre", { "data-block": "code", className: "overflow-x-auto rounded-md bg-muted p-3 text-xs", children: _jsx("code", { children: block.code }) }));
317
317
  case "hint":
@@ -330,6 +330,16 @@ function TextBlockLink({ block, translate, }) {
330
330
  const target = { screenId: lastSegment(block.screen) };
331
331
  return (_jsx(Link, { href: nav.hrefFor(target), onPress: () => nav.navigate(target), testId: `dashboard-text-link-${target.screenId}`, children: translate(block.text) }));
332
332
  }
333
+ // Content-derived keys collide for identical entries; the occurrence count keeps them unique.
334
+ function withOccurrenceKeys(entries, contentKey) {
335
+ const seen = new Map();
336
+ return entries.map((entry) => {
337
+ const content = contentKey(entry);
338
+ const occurrence = seen.get(content) ?? 0;
339
+ seen.set(content, occurrence + 1);
340
+ return { entry, key: `${content}#${occurrence}` };
341
+ });
342
+ }
333
343
  function textBlockKey(block) {
334
344
  switch (block.kind) {
335
345
  case "list":
@@ -341,7 +351,7 @@ function textBlockKey(block) {
341
351
  }
342
352
  }
343
353
  export function TextPanelBody({ panel, label, translate, }) {
344
- const blocks = (_jsx("div", { className: "flex flex-col gap-3", children: panel.blocks.map((block) => (_jsx(TextBlockView, { block: block, translate: translate }, textBlockKey(block)))) }));
354
+ const blocks = (_jsx("div", { className: "flex flex-col gap-3", children: withOccurrenceKeys(panel.blocks, textBlockKey).map(({ entry, key }) => (_jsx(TextBlockView, { block: entry, translate: translate }, key))) }));
345
355
  const testId = `dashboard-panel-${panel.id}`;
346
356
  if (label !== undefined) {
347
357
  return (_jsx(PanelShell, { testId: testId, title: label, children: blocks }));
@@ -0,0 +1,6 @@
1
+ export declare function committedUrl(): string;
2
+ /** Adopts the entry the browser currently shows as the committed one. */
3
+ export declare function adoptCurrentEntry(): void;
4
+ export declare function pushEntry(url: string): void;
5
+ export declare function replaceEntry(url: string): void;
6
+ export declare function handlePopstate(notify: () => void): void;