@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,115 @@
1
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
2
+ import { hasUnsavedInput, requestLeave } from "../../index.js";
3
+ // Back/Forward cannot be vetoed, so the browser has already moved by the time the guard asks.
4
+ // Each entry we create carries its position in history.state; with it a declined move is
5
+ // undone with history.go(-delta) and a confirmed one replayed with history.go(delta), so the
6
+ // stack keeps its shape (no duplicated or truncated entries).
7
+ const INDEX_KEY = "kumikoNavIndex";
8
+ const committed = { url: "", index: 0 };
9
+ let leaveRequestOpen = false;
10
+ // Each history.go() we issue produces one popstate that must not be treated as user navigation.
11
+ const pendingMoves = [];
12
+ // Entries without a position (written by other code) cannot be undone relatively.
13
+ let fallbackTargetUrl;
14
+ function readUrl() {
15
+ return `${window.location.pathname}${window.location.search}${window.location.hash}`;
16
+ }
17
+ function stateAt(index) {
18
+ return { [INDEX_KEY]: index };
19
+ }
20
+ function readIndex() {
21
+ const value = propertyOf(window.history.state, INDEX_KEY);
22
+ return typeof value === "number" ? value : undefined;
23
+ }
24
+ export function committedUrl() {
25
+ return committed.url;
26
+ }
27
+ /** Adopts the entry the browser currently shows as the committed one. */
28
+ export function adoptCurrentEntry() {
29
+ const index = readIndex();
30
+ committed.url = readUrl();
31
+ if (index === undefined) {
32
+ window.history.replaceState(stateAt(committed.index), "", committed.url);
33
+ }
34
+ else {
35
+ committed.index = index;
36
+ }
37
+ }
38
+ function commitCurrentUrl() {
39
+ committed.url = readUrl();
40
+ committed.index = readIndex() ?? committed.index;
41
+ }
42
+ export function pushEntry(url) {
43
+ window.history.pushState(stateAt(committed.index + 1), "", url);
44
+ committed.index += 1;
45
+ committed.url = readUrl();
46
+ }
47
+ export function replaceEntry(url) {
48
+ window.history.replaceState(stateAt(committed.index), "", url);
49
+ commitCurrentUrl();
50
+ }
51
+ export function handlePopstate(notify) {
52
+ const move = pendingMoves.shift();
53
+ if (move !== undefined) {
54
+ if (move === "replay") {
55
+ commitCurrentUrl();
56
+ notify();
57
+ }
58
+ // skip: this popstate is the echo of our own history.go()
59
+ return;
60
+ }
61
+ if (fallbackTargetUrl !== undefined) {
62
+ const target = fallbackTargetUrl;
63
+ fallbackTargetUrl = undefined;
64
+ // After a forward move the entry below our restore-push is not the target.
65
+ if (readUrl() !== target)
66
+ window.history.replaceState(stateAt(committed.index), "", target);
67
+ commitCurrentUrl();
68
+ notify();
69
+ // skip: the pending fallback move is settled
70
+ return;
71
+ }
72
+ const newIndex = readIndex();
73
+ const delta = newIndex === undefined ? 0 : newIndex - committed.index;
74
+ if (leaveRequestOpen) {
75
+ // Another Back/Forward while the dialog is open must neither stack an entry nor replace the
76
+ // open request: step back to the entry the dialog is about.
77
+ if (delta !== 0) {
78
+ pendingMoves.push("undo");
79
+ window.history.go(-delta);
80
+ }
81
+ else {
82
+ window.history.replaceState(stateAt(committed.index), "", committed.url);
83
+ }
84
+ // skip: the open leave dialog keeps the decision
85
+ return;
86
+ }
87
+ if (!hasUnsavedInput()) {
88
+ commitCurrentUrl();
89
+ notify();
90
+ // skip: nothing unsaved, the move stands
91
+ return;
92
+ }
93
+ const targetUrl = readUrl();
94
+ leaveRequestOpen = true;
95
+ const settle = () => {
96
+ leaveRequestOpen = false;
97
+ };
98
+ if (delta !== 0) {
99
+ pendingMoves.push("undo");
100
+ window.history.go(-delta);
101
+ requestLeave(() => {
102
+ settle();
103
+ pendingMoves.push("replay");
104
+ window.history.go(delta);
105
+ }, settle);
106
+ // skip: the leave request owns the rest of the move
107
+ return;
108
+ }
109
+ window.history.pushState(stateAt(committed.index), "", committed.url);
110
+ requestLeave(() => {
111
+ settle();
112
+ fallbackTargetUrl = targetUrl;
113
+ window.history.back();
114
+ }, settle);
115
+ }
@@ -0,0 +1,6 @@
1
+ import { type ReactNode } from "react";
2
+ /** Provides the tab-scoped list-state store and drops state remembered under
3
+ * another user or tenant. Mount below the auth gates so the user is known. */
4
+ export declare function ListStateStoreBoot({ children }: {
5
+ readonly children: ReactNode;
6
+ }): ReactNode;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { ListStateStoreProvider } from "../../components/list-state-store.js";
4
+ import { useOptionalCurrentUser } from "../../current-user.js";
5
+ import { resolveListStateStore } from "./session-list-state-store.js";
6
+ /** Provides the tab-scoped list-state store and drops state remembered under
7
+ * another user or tenant. Mount below the auth gates so the user is known. */
8
+ export function ListStateStoreBoot({ children }) {
9
+ const user = useOptionalCurrentUser();
10
+ const userId = user?.id;
11
+ const tenantId = user?.tenantId;
12
+ // Must run before the first list renders; an effect would let it read the previous identity's state.
13
+ const store = useMemo(() => resolveListStateStore(userId === undefined ? undefined : { userId, tenantId }), [userId, tenantId]);
14
+ return _jsx(ListStateStoreProvider, { store: store, children: children });
15
+ }
@@ -12,7 +12,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
12
12
  // notification triggers (our pushPath and the popstate event), so this is not
13
13
  // generalizable.
14
14
  import { useCallback, useMemo, useSyncExternalStore, } from "react";
15
+ import { normalizeBasePath, prependBasePath, stripBasePath } from "../../app/base-path.js";
15
16
  import { formatPath, hasUnsavedInput, parsePath, requestLeave, resolveTarget, useNav, } from "../../index.js";
17
+ import { adoptCurrentEntry, handlePopstate, pushEntry, replaceEntry, } from "./history-leave-guard.js";
16
18
  // basePath allows the app to be mounted under a URL prefix
17
19
  // (for example, `/admin`). The read path strips the prefix before parsePath;
18
20
  // the write path prepends it before pushState/replaceState/href.
@@ -22,45 +24,11 @@ import { formatPath, hasUnsavedInput, parsePath, requestLeave, resolveTarget, us
22
24
  // returns route=undefined, and the app renders its "outside" state
23
25
  // (Not-Found, marketing layer, server-routing pickup, …). There is no
24
26
  // automatic redirect to the app root—the host app decides.
25
- function normalizeBasePath(raw) {
26
- if (raw === undefined || raw === "" || raw === "/")
27
- return "";
28
- const withLeading = raw.startsWith("/") ? raw : `/${raw}`;
29
- return withLeading.endsWith("/") ? withLeading.slice(0, -1) : withLeading;
30
- }
31
- // Returns the in-app (basePath-relative) path when `path` is under basePath;
32
- // otherwise returns `undefined`. The segment boundary is strict: "/administrator"
33
- // does not match "/admin"—otherwise a string-level prefix check would produce
34
- // false matches.
35
- function stripBasePath(path, basePath) {
36
- if (basePath === "")
37
- return path;
38
- if (path === basePath)
39
- return "/";
40
- if (path.startsWith(`${basePath}/`))
41
- return path.slice(basePath.length);
42
- return undefined;
43
- }
44
- function prependBasePath(path, basePath) {
45
- if (basePath === "")
46
- return path;
47
- // formatPath always returns an absolute in-app path: "/" or "/screen-id"
48
- // or "/screen-id/entity-id". "/" becomes basePath; otherwise, concatenate.
49
- if (path === "/" || path === "")
50
- return basePath;
51
- return `${basePath}${path}`;
52
- }
53
27
  // pushState does not fire popstate, so we maintain a listener set that
54
28
  // navigate() notifies. Back/Forward navigation goes through a window listener
55
29
  // wired exactly once.
56
30
  const listeners = new Set();
57
31
  let popstateWired = false;
58
- // URL the app currently shows; popstate has already moved the browser to the
59
- // target by the time we can ask, so a declined leave must restore this one.
60
- let committedUrl = "";
61
- function readUrl() {
62
- return `${window.location.pathname}${window.location.search}${window.location.hash}`;
63
- }
64
32
  // Stable identity for the omitted-features case — `?? []` would create a
65
33
  // fresh array every render, busting the useMemo below on every render for
66
34
  // every existing useBrowserNavApi() call site (none of which pass features).
@@ -71,19 +39,7 @@ function ensurePopstateWired() {
71
39
  if (typeof window === "undefined")
72
40
  return;
73
41
  window.addEventListener("popstate", () => {
74
- if (!hasUnsavedInput()) {
75
- committedUrl = readUrl();
76
- for (const l of listeners)
77
- l();
78
- return;
79
- }
80
- const targetUrl = readUrl();
81
- window.history.pushState(null, "", committedUrl);
82
- requestLeave(() => {
83
- // replaceState, not history.back(): the declined entry is already ours
84
- // and a forward navigation would otherwise land on the wrong entry.
85
- window.history.replaceState(null, "", targetUrl);
86
- committedUrl = targetUrl;
42
+ handlePopstate(() => {
87
43
  for (const l of listeners)
88
44
  l();
89
45
  });
@@ -99,7 +55,7 @@ function ensurePopstateWired() {
99
55
  function subscribe(listener) {
100
56
  ensurePopstateWired();
101
57
  if (typeof window !== "undefined" && listeners.size === 0)
102
- committedUrl = readUrl();
58
+ adoptCurrentEntry();
103
59
  listeners.add(listener);
104
60
  return () => {
105
61
  listeners.delete(listener);
@@ -146,8 +102,7 @@ function applySearchParamUpdates(updates) {
146
102
  if (nextSearch === window.location.search)
147
103
  return;
148
104
  const url = `${window.location.pathname}${nextSearch}${window.location.hash}`;
149
- window.history.replaceState(null, "", url);
150
- committedUrl = readUrl();
105
+ replaceEntry(url);
151
106
  for (const l of listeners)
152
107
  l();
153
108
  }
@@ -168,8 +123,7 @@ function pushPath(path, search) {
168
123
  if (unchanged)
169
124
  return;
170
125
  requestLeave(() => {
171
- window.history.pushState(null, "", `${path}${search ?? ""}`);
172
- committedUrl = readUrl();
126
+ pushEntry(`${path}${search ?? ""}`);
173
127
  for (const l of listeners)
174
128
  l();
175
129
  });
@@ -185,8 +139,7 @@ function replacePath(path, search) {
185
139
  const leavesCurrentPage = window.location.pathname !== path ||
186
140
  (search !== undefined && window.location.search !== search);
187
141
  const apply = () => {
188
- window.history.replaceState(null, "", nextUrl);
189
- committedUrl = readUrl();
142
+ replaceEntry(nextUrl);
190
143
  for (const l of listeners)
191
144
  l();
192
145
  };
@@ -0,0 +1,15 @@
1
+ import type { ListStateStore } from "../../components/list-state-store.tsx";
2
+ export type ListStateIdentity = {
3
+ readonly userId: string;
4
+ readonly tenantId?: string | undefined;
5
+ };
6
+ /** Remembered list state for the lifetime of the browser tab, keyed per tenant
7
+ * and user. Every storage failure (blocked, full) degrades to "nothing remembered". */
8
+ export declare function createSessionListStateStore(storage: Storage, identity: ListStateIdentity): ListStateStore;
9
+ /** Forgets all remembered list state and the identity marker. Logout calls it
10
+ * before a page reload, which would otherwise hide the identity change. */
11
+ export declare function clearRememberedListState(): void;
12
+ /** The store for the current identity. State remembered under another identity,
13
+ * or under one that has since gone away, is dropped first. Without an identity
14
+ * (app without auth) only the in-memory store is used and nothing is persisted. */
15
+ export declare function resolveListStateStore(identity: ListStateIdentity | undefined): ListStateStore;
@@ -0,0 +1,117 @@
1
+ import { getDefaultMemoryListStateStore } from "../../components/list-state-store.js";
2
+ import { parseStoredListStateJson } from "../../hooks/list-state-schema.js";
3
+ const KEY_PREFIX = "kumiko.list.";
4
+ const IDENTITY_KEY = `${KEY_PREFIX}identity`;
5
+ const NO_TENANT = "-";
6
+ function identityMarker(identity) {
7
+ return `${identity.tenantId ?? NO_TENANT}.${identity.userId}`;
8
+ }
9
+ function openSessionStorage() {
10
+ try {
11
+ return typeof window === "undefined" ? undefined : window.sessionStorage;
12
+ }
13
+ catch {
14
+ // Access itself throws when storage is blocked (privacy modes, sandboxed frames).
15
+ return undefined;
16
+ }
17
+ }
18
+ function prefixedKeys(storage) {
19
+ return Array.from({ length: storage.length }, (_, index) => storage.key(index)).filter((key) => key !== null && key.startsWith(KEY_PREFIX));
20
+ }
21
+ function removeAllRemembered(storage) {
22
+ for (const key of prefixedKeys(storage))
23
+ storage.removeItem(key);
24
+ }
25
+ /** Remembered list state for the lifetime of the browser tab, keyed per tenant
26
+ * and user. Every storage failure (blocked, full) degrades to "nothing remembered". */
27
+ export function createSessionListStateStore(storage, identity) {
28
+ const keyPrefix = `${KEY_PREFIX}${identityMarker(identity)}.`;
29
+ return {
30
+ read: (key) => {
31
+ try {
32
+ const raw = storage.getItem(`${keyPrefix}${key}`);
33
+ return raw === null ? undefined : parseStoredListStateJson(raw);
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ },
39
+ write: (key, state) => {
40
+ try {
41
+ storage.setItem(`${keyPrefix}${key}`, JSON.stringify(state));
42
+ }
43
+ catch {
44
+ // Quota or blocked storage: the list keeps working, it just is not remembered.
45
+ }
46
+ },
47
+ remove: (key) => {
48
+ try {
49
+ storage.removeItem(`${keyPrefix}${key}`);
50
+ }
51
+ catch {
52
+ // See write.
53
+ }
54
+ },
55
+ clear: () => {
56
+ try {
57
+ for (const key of prefixedKeys(storage).filter((k) => k !== IDENTITY_KEY)) {
58
+ storage.removeItem(key);
59
+ }
60
+ }
61
+ catch {
62
+ // See write.
63
+ }
64
+ },
65
+ };
66
+ }
67
+ /** Forgets all remembered list state and the identity marker. Logout calls it
68
+ * before a page reload, which would otherwise hide the identity change. */
69
+ export function clearRememberedListState() {
70
+ getDefaultMemoryListStateStore().clear();
71
+ lastMemoryIdentity = undefined;
72
+ const storage = openSessionStorage();
73
+ // skip: no sessionStorage available, memory store already cleared
74
+ if (storage === undefined)
75
+ return;
76
+ try {
77
+ removeAllRemembered(storage);
78
+ }
79
+ catch {
80
+ // See createSessionListStateStore.
81
+ }
82
+ }
83
+ // The in-memory store survives SPA identity changes, so it needs its own marker.
84
+ let lastMemoryIdentity;
85
+ /** The store for the current identity. State remembered under another identity,
86
+ * or under one that has since gone away, is dropped first. Without an identity
87
+ * (app without auth) only the in-memory store is used and nothing is persisted. */
88
+ export function resolveListStateStore(identity) {
89
+ const storage = openSessionStorage();
90
+ const memory = getDefaultMemoryListStateStore();
91
+ const marker = identity === undefined ? undefined : identityMarker(identity);
92
+ if (lastMemoryIdentity !== marker) {
93
+ if (lastMemoryIdentity !== undefined)
94
+ memory.clear();
95
+ lastMemoryIdentity = marker;
96
+ }
97
+ if (storage === undefined)
98
+ return memory;
99
+ try {
100
+ const storedMarker = storage.getItem(IDENTITY_KEY);
101
+ if (marker === undefined) {
102
+ if (storedMarker !== null) {
103
+ removeAllRemembered(storage);
104
+ memory.clear();
105
+ }
106
+ return memory;
107
+ }
108
+ if (storedMarker !== marker) {
109
+ removeAllRemembered(storage);
110
+ storage.setItem(IDENTITY_KEY, marker);
111
+ }
112
+ }
113
+ catch {
114
+ return memory;
115
+ }
116
+ return identity === undefined ? memory : createSessionListStateStore(storage, identity);
117
+ }
@@ -13,6 +13,7 @@ export { KumikoLink, useBrowserNavApi } from "./app/nav.tsx";
13
13
  export { PlainContentEditor } from "./app/plain-content-editor.tsx";
14
14
  export { useResolvers } from "./app/resolvers-context.tsx";
15
15
  export { RichContentEditor } from "./app/rich-content-editor.tsx";
16
+ export { clearRememberedListState } from "./app/session-list-state-store.ts";
16
17
  export type { NavIconKey } from "./icons.tsx";
17
18
  export { Icon } from "./icons.tsx";
18
19
  export type { AppLayoutProps } from "./layout/app-layout.tsx";
@@ -75,4 +76,4 @@ export { SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider } from
75
76
  export type { KumikoBuild } from "./version/update-checker.tsx";
76
77
  export { readLoadedBuild } from "./version/update-checker.tsx";
77
78
  export type { AiTextAreaProps, AiTextFieldProps, BooleanFieldProps, ComparisonMetric, DateFieldProps, DrawerProps, FeedRow, FileFieldProps, FloatingPanelGeometry, FloatingPanelProps, InfinityListProps, InfinityListSelection, ModeSwitchVariant, NumberFieldProps, PdfPreviewSource, PhotoSlotSpec, PhotoSlotsProps, PlanCardActionSlot, PlanCardLinkSlot, PlanCardPrice, PlanCardProps, PlanGridProps, ProgressListRow, PublicShellProps, PublicShellVariant, QueryTableColumn, QueryTableProps, RangeFieldProps, ResultColumn, SelectFieldProps, SideBySideTableCell, SideBySideTableColumn, SideBySideTableRow, StatDelta, StatTone, StatusBadgeTone, StatusBarEntry, StatusTone, TextareaFieldProps, TextFieldProps, TimeseriesHeight, TimeseriesPoint, TimeseriesReferenceLine, TimeseriesXAxis, TimeseriesYAxis, UploadZoneProps, } from "./widgets/index.ts";
78
- export { AiTextArea, AiTextField, BooleanField, CollapsibleSection, ComparisonTable, DateField, DetailList, Drawer, EmptyState, ErrorState, FeedList, FileField, FloatingPanel, InfinityList, LoadingState, MiniStat, ModeSwitch, MoneyField, NumberField, PdfPreview, PercentField, PhotoSlots, PlanCard, PlanGrid, ProgressBar, ProgressList, PublicShell, QueryTable, RangeField, ResultPanel, ResultTable, SectionCard, SelectField, SideBySideTable, Sparkline, STATUS_TONE_TEXT, StatCard, StatusBadge, StatusBarChart, StepBar, smoothPath, TextareaField, TextField, TimeseriesChart, UploadZone, useDraft, } from "./widgets/index.ts";
79
+ export { AiTextArea, AiTextField, BooleanField, ChoiceCards, CollapsibleSection, ComparisonTable, DateField, DetailList, Drawer, EmptyState, ErrorState, FeedList, FileField, FloatingPanel, InfinityList, LoadingState, MiniStat, ModeSwitch, MoneyField, NumberField, PdfPreview, PercentField, PhotoSlots, PlanCard, PlanGrid, ProgressBar, ProgressList, PublicShell, QueryTable, RangeField, ResultPanel, ResultTable, SectionCard, SelectField, SideBySideTable, Sparkline, STATUS_TONE_TEXT, StatCard, StatusBadge, StatusBarChart, StepBar, smoothPath, TextareaField, TextField, TimeseriesChart, UploadZone, useDraft, } from "./widgets/index.ts";
package/dist/web/index.js CHANGED
@@ -12,6 +12,7 @@ export { KumikoLink, useBrowserNavApi } from "./app/nav.js";
12
12
  export { PlainContentEditor } from "./app/plain-content-editor.js";
13
13
  export { useResolvers } from "./app/resolvers-context.js";
14
14
  export { RichContentEditor } from "./app/rich-content-editor.js";
15
+ export { clearRememberedListState } from "./app/session-list-state-store.js";
15
16
  export { Icon } from "./icons.js";
16
17
  export { AppLayout } from "./layout/app-layout.js";
17
18
  export { Avatar } from "./layout/avatar.js";
@@ -50,4 +51,4 @@ export { applyTokensToCssVars, defaultTokens, lightTokens, useBrowserTokensApi,
50
51
  export { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./ui/resizable.js";
51
52
  export { SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider } from "./ui/sidebar.js";
52
53
  export { readLoadedBuild } from "./version/update-checker.js";
53
- export { AiTextArea, AiTextField, BooleanField, CollapsibleSection, ComparisonTable, DateField, DetailList, Drawer, EmptyState, ErrorState, FeedList, FileField, FloatingPanel, InfinityList, LoadingState, MiniStat, ModeSwitch, MoneyField, NumberField, PdfPreview, PercentField, PhotoSlots, PlanCard, PlanGrid, ProgressBar, ProgressList, PublicShell, QueryTable, RangeField, ResultPanel, ResultTable, SectionCard, SelectField, SideBySideTable, Sparkline, STATUS_TONE_TEXT, StatCard, StatusBadge, StatusBarChart, StepBar, smoothPath, TextareaField, TextField, TimeseriesChart, UploadZone, useDraft, } from "./widgets/index.js";
54
+ export { AiTextArea, AiTextField, BooleanField, ChoiceCards, CollapsibleSection, ComparisonTable, DateField, DetailList, Drawer, EmptyState, ErrorState, FeedList, FileField, FloatingPanel, InfinityList, LoadingState, MiniStat, ModeSwitch, MoneyField, NumberField, PdfPreview, PercentField, PhotoSlots, PlanCard, PlanGrid, ProgressBar, ProgressList, PublicShell, QueryTable, RangeField, ResultPanel, ResultTable, SectionCard, SelectField, SideBySideTable, Sparkline, STATUS_TONE_TEXT, StatCard, StatusBadge, StatusBarChart, StepBar, smoothPath, TextareaField, TextField, TimeseriesChart, UploadZone, useDraft, } from "./widgets/index.js";
@@ -19,6 +19,7 @@ import { HeaderOverflowMenuContext, headerOverflowMenuItemClass } from "./header
19
19
  import { buildNavRegistrySliceForApp, lastSegment } from "./nav-tree.js";
20
20
  import { usePageHeaderSlot } from "./page-header-slot.js";
21
21
  import { resolveDetailBreadcrumb, screenTitleKey, } from "./shell-breadcrumb.js";
22
+ import { UNIFORM_BAR_CONTROLS_CLASSES } from "./uniform-bar-controls.js";
22
23
  export function ShellHeader({ schema, user, headerActions, }) {
23
24
  const nav = useNav();
24
25
  const t = useTranslation();
@@ -68,7 +69,7 @@ export function ShellHeader({ schema, user, headerActions, }) {
68
69
  e.preventDefault();
69
70
  nav.navigate({ screenId });
70
71
  }, children: crumb.label })) : isLast ? (_jsx("h1", { "aria-current": "page", title: titleOverride ?? crumb.label, className: "truncate text-lg font-semibold text-foreground", children: titleOverride ?? crumb.label })) : (_jsx(BreadcrumbPage, { className: "whitespace-nowrap text-foreground-secondary", children: crumb.label })) })] }, screenId ?? crumb.label));
71
- }) }) })), _jsx("div", { ref: setStatusElement, "data-kumiko-layout": "page-header-status", className: cn("flex items-center gap-2 empty:hidden", compact ? "min-w-0 overflow-hidden" : "shrink-0") })] }), _jsxs("div", { className: cn("ml-auto flex items-center gap-2", compact ? "min-w-0 max-w-[60%]" : "min-w-0 max-w-[50%] shrink-0 sm:max-w-none"), children: [_jsx("div", { ref: setActionsElement, "data-kumiko-layout": "page-header-actions", className: cn("flex min-w-0 items-center gap-2 empty:hidden [&_*]:min-w-0 [&_*]:max-w-full [&_button]:overflow-hidden [&_button]:whitespace-nowrap", compact && "overflow-hidden") }), !compact && headerActions !== undefined && (_jsx("div", { "data-kumiko-layout": "header-actions", className: "flex items-center gap-2", children: headerActions })), hasOverflow && _jsx(HeaderOverflow, { items: overflowItems, headerActions: headerActions })] })] }));
72
+ }) }) })), _jsx("div", { ref: setStatusElement, "data-kumiko-layout": "page-header-status", className: cn("flex items-center gap-2 empty:hidden", compact ? "min-w-0 overflow-hidden" : "shrink-0") })] }), _jsxs("div", { className: cn("ml-auto flex items-center gap-2", compact ? "min-w-0 max-w-[60%]" : "min-w-0 max-w-[50%] shrink-0 sm:max-w-none"), children: [_jsx("div", { ref: setActionsElement, "data-kumiko-layout": "page-header-actions", className: cn(UNIFORM_BAR_CONTROLS_CLASSES, "flex min-w-0 items-center gap-2 empty:hidden [&_*]:min-w-0 [&_*]:max-w-full [&_button]:overflow-hidden [&_button]:whitespace-nowrap", compact && "overflow-hidden") }), !compact && headerActions !== undefined && (_jsx("div", { "data-kumiko-layout": "header-actions", className: "flex items-center gap-2", children: headerActions })), hasOverflow && _jsx(HeaderOverflow, { items: overflowItems, headerActions: headerActions })] })] }));
72
73
  }
73
74
  // A hand-rolled menu instead of Radix DropdownMenu: app headerActions are opaque
74
75
  // nodes with their own popovers, and the panel stays mounted while closed so their
@@ -0,0 +1 @@
1
+ export declare const UNIFORM_BAR_CONTROLS_CLASSES = "[&_[data-slot=button]:not([data-size^=icon])]:h-9 [&_[data-slot=input]]:h-9";
@@ -0,0 +1,4 @@
1
+ // Header and toolbar controls share one height whatever size a screen, app or
2
+ // primitive asked for: the descendant selector outranks the button's own
3
+ // size utilities. Icon-only buttons keep their square size.
4
+ export const UNIFORM_BAR_CONTROLS_CLASSES = "[&_[data-slot=button]:not([data-size^=icon])]:h-9 [&_[data-slot=input]]:h-9";
@@ -0,0 +1,11 @@
1
+ import type { DataTableExport } from "../../data-table-view-types.ts";
2
+ export type XlsxExportResult = {
3
+ readonly status: "ok";
4
+ readonly blob: Blob;
5
+ } | {
6
+ readonly status: "too-many";
7
+ readonly maxRows: number | undefined;
8
+ } | {
9
+ readonly status: "failed";
10
+ };
11
+ export declare function requestXlsxExport(source: DataTableExport): Promise<XlsxExportResult>;
@@ -0,0 +1,46 @@
1
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
2
+ import { CSRF_HEADER_NAME, LOCALE_HEADER_NAME, readActiveLocale, readCsrfToken, } from "../../live/index.js";
3
+ const TOO_MANY_ROWS_HTTP_STATUS = 413;
4
+ async function readMaxRows(res) {
5
+ try {
6
+ const body = await res.json();
7
+ const error = isPlainObject(body) ? body["error"] : undefined;
8
+ const details = isPlainObject(error) ? error["details"] : undefined;
9
+ const maxRows = isPlainObject(details) ? details["maxRows"] : undefined;
10
+ return typeof maxRows === "number" ? maxRows : undefined;
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ }
16
+ export async function requestXlsxExport(source) {
17
+ const headers = { "Content-Type": "application/json" };
18
+ const csrf = readCsrfToken();
19
+ if (csrf !== undefined)
20
+ headers[CSRF_HEADER_NAME] = csrf;
21
+ const locale = readActiveLocale();
22
+ if (locale !== undefined)
23
+ headers[LOCALE_HEADER_NAME] = locale;
24
+ try {
25
+ const res = await fetch("/api/export/xlsx", {
26
+ method: "POST",
27
+ credentials: "include",
28
+ headers,
29
+ body: JSON.stringify({
30
+ screen: source.screen,
31
+ payload: source.payload,
32
+ columns: source.columns,
33
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
34
+ }),
35
+ });
36
+ if (res.ok)
37
+ return { status: "ok", blob: await res.blob() };
38
+ if (res.status === TOO_MANY_ROWS_HTTP_STATUS) {
39
+ return { status: "too-many", maxRows: await readMaxRows(res) };
40
+ }
41
+ return { status: "failed" };
42
+ }
43
+ catch {
44
+ return { status: "failed" };
45
+ }
46
+ }
@@ -37,6 +37,9 @@ export async function fetchFileRefBlob(fileRefId) {
37
37
  fileName: fileNameFromContentDisposition(res.headers.get("content-disposition")),
38
38
  };
39
39
  }
40
+ // The browser starts the download after click() returns; Safari and some
41
+ // Firefox versions fail it when the blob URL is already revoked.
42
+ const OBJECT_URL_REVOKE_DELAY_MS = 40_000;
40
43
  export function saveBlobAsFile(fileName, blob, options) {
41
44
  const payload = options?.bom === true ? new Blob([UTF8_BOM, blob], { type: blob.type }) : blob;
42
45
  const url = URL.createObjectURL(payload);
@@ -53,7 +56,7 @@ export function saveBlobAsFile(fileName, blob, options) {
53
56
  }
54
57
  }
55
58
  finally {
56
- URL.revokeObjectURL(url);
59
+ setTimeout(() => URL.revokeObjectURL(url), OBJECT_URL_REVOKE_DELAY_MS);
57
60
  }
58
61
  }
59
62
  export async function downloadFileRef(fileRefId, options) {
@@ -23,7 +23,7 @@ export function ActionMenu({ trigger, triggerLabel, items, align = "end", minWid
23
23
  return _jsx(DropdownMenuSeparator, {}, `sep-${idx}`);
24
24
  }
25
25
  if (item.kind === "label") {
26
- // biome-ignore lint/suspicious/noArrayIndexKey: Separators have no ID, so their position provides identity
26
+ // biome-ignore lint/suspicious/noArrayIndexKey: labels have no ID either, so their position provides identity
27
27
  return _jsx(DropdownMenuLabel, { children: item.label }, `label-${idx}`);
28
28
  }
29
29
  return (_jsxs(DropdownMenuItem, { onSelect: item.onSelect, disabled: item.disabled === true, "data-testid": `action-menu-item-${item.id}`, className: item.variant === "danger" ? "text-destructive focus:text-destructive" : undefined, children: [item.icon !== undefined && _jsx("span", { className: "size-4 shrink-0", children: item.icon }), _jsx("span", { className: "flex-1", children: item.label }), item.shortcut !== undefined && (_jsx("span", { className: "ml-auto text-xs text-muted-foreground tracking-wider font-mono", children: item.shortcut }))] }, item.id));
@@ -0,0 +1,6 @@
1
+ import { type ReactNode } from "react";
2
+ import type { InputProps } from "../../index.ts";
3
+ export type CodeInputProps = Extract<InputProps, {
4
+ readonly kind: "code";
5
+ }>;
6
+ export declare function CodeInput(props: CodeInputProps): ReactNode;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useContext } from "react";
3
+ import { cn } from "../lib/cn.js";
4
+ import { FieldDescriptionContext } from "./contexts.js";
5
+ import { useFocusOnMount } from "./use-focus-on-mount.js";
6
+ const MIN_LENGTH_FOR_SEPARATOR = 6;
7
+ export function CodeInput(props) {
8
+ const { length, value, hasError, disabled } = props;
9
+ const describedBy = useContext(FieldDescriptionContext);
10
+ const inputRef = useFocusOnMount(props.autoFocus);
11
+ const activeIndex = Math.min(value.length, length - 1);
12
+ const hasSeparator = length >= MIN_LENGTH_FOR_SEPARATOR && length % 2 === 0;
13
+ const handleChange = (e) => {
14
+ const next = e.target.value.replace(/\D/g, "").slice(0, length);
15
+ props.onChange(next);
16
+ if (next.length === length && next !== value)
17
+ props.onComplete?.(next);
18
+ };
19
+ const cells = [];
20
+ for (let index = 0; index < length; index++) {
21
+ const isActive = index === activeIndex;
22
+ const char = value[index];
23
+ if (hasSeparator && index === length / 2) {
24
+ cells.push(_jsx("span", { "aria-hidden": "true", className: "shrink-0 text-muted-foreground", children: "\u2013" }, "separator"));
25
+ }
26
+ cells.push(_jsxs("div", { "data-active": isActive, "aria-hidden": "true", className: cn("flex h-12 min-w-0 flex-1 items-center justify-center rounded-md border border-input bg-transparent text-xl font-semibold tabular-nums shadow-xs transition-[color,box-shadow] dark:bg-input/30", hasError === true && "border-destructive", "data-[active=true]:group-focus-within:ring-[3px]", hasError === true
27
+ ? "data-[active=true]:group-focus-within:border-destructive data-[active=true]:group-focus-within:ring-destructive/20 dark:data-[active=true]:group-focus-within:ring-destructive/40"
28
+ : "data-[active=true]:group-focus-within:border-ring data-[active=true]:group-focus-within:ring-ring/50"), children: [char, isActive && char === undefined ? (_jsx("span", { className: "hidden h-6 w-px animate-pulse bg-foreground group-focus-within:block" })) : null] }, index));
29
+ }
30
+ return (_jsxs("div", { className: cn("group relative flex w-full items-center gap-2", disabled === true && "cursor-not-allowed opacity-50"), children: [cells, _jsx("input", { ref: inputRef, type: "text", id: props.id, name: props.name, value: value, onChange: handleChange, disabled: disabled, inputMode: "numeric", autoComplete: props.autoComplete ?? "one-time-code", spellCheck: false, "data-testid": props.testId, "aria-describedby": describedBy, "aria-required": props.required, "aria-invalid": hasError === true ? true : undefined, className: "absolute inset-0 h-full w-full bg-transparent text-transparent caret-transparent outline-none selection:bg-transparent" })] }));
31
+ }
@@ -0,0 +1,16 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ export declare const ACTIONS_CONTENT_ATTRIBUTE = "data-actions-content";
3
+ export declare function stickyActionsCellClass(fillsHeight: boolean): string;
4
+ export declare function ActionsHeaderCell({ label, style, absorbsLeftover, }: {
5
+ readonly label: string;
6
+ readonly style?: CSSProperties | undefined;
7
+ /** Fixed table layout: `w-px` would pin this column to 1px, so it must size as auto and take the leftover width. */
8
+ readonly absorbsLeftover: boolean;
9
+ }): ReactNode;
10
+ export declare function ActionsBodyCell({ testId, fillsHeight, measureContent, children, }: {
11
+ readonly testId: string;
12
+ readonly fillsHeight: boolean;
13
+ /** Wraps the content so the column's natural width stays measurable under a fixed table layout. */
14
+ readonly measureContent: boolean;
15
+ readonly children: ReactNode;
16
+ }): ReactNode;
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from "../../lib/cn.js";
3
+ import { TableCell, TableHead } from "../../ui/table.js";
4
+ import { ROW_FOCUS_RING_CELL_CLASS } from "./use-row-keyboard-navigation.js";
5
+ export const ACTIONS_CONTENT_ATTRIBUTE = "data-actions-content";
6
+ export function stickyActionsCellClass(fillsHeight) {
7
+ return cn("md:sticky md:right-0 md:z-10 md:bg-background", fillsHeight && "md:bg-card");
8
+ }
9
+ export function ActionsHeaderCell({ label, style, absorbsLeftover, }) {
10
+ return (_jsx(TableHead, { "data-testid": "column-actions",
11
+ // From md: sticky right-0 + bg-muted (= header tone) so the
12
+ // action column stays at the right edge during horizontal
13
+ // scroll on wider screens. Below md, actions scroll in
14
+ // normal flow — sticky there would pin the column over the
15
+ // preceding data column instead of the (already narrow)
16
+ // page edge. No border-l: a permanent divider looks heavy;
17
+ // the sticky bg already sets the column apart during scroll.
18
+ className: "w-px text-right text-muted-foreground md:sticky md:right-0 md:z-10 md:bg-muted", "aria-label": label, style: absorbsLeftover ? { ...style, width: "auto" } : style }));
19
+ }
20
+ export function ActionsBodyCell({ testId, fillsHeight, measureContent, children, }) {
21
+ return (_jsx(TableCell, { "data-testid": testId,
22
+ // From md: sticky-right so the actions stay visible on the
23
+ // right edge during horizontal scroll. Below md, actions
24
+ // scroll with the row like any other cell — sticky there
25
+ // would hide the neighboring data column on narrow
26
+ // viewports. bg-background sets the column apart during
27
+ // scroll — no border-l (divider too heavy).
28
+ className: cn("whitespace-nowrap text-right", stickyActionsCellClass(fillsHeight), ROW_FOCUS_RING_CELL_CLASS),
29
+ // Action-cell events must not trigger the row click/activation
30
+ // (typically "Open Detail" — the user wanted the action,
31
+ // not navigation). We stopPropagation for mouse and
32
+ // keyboard so a11y stays consistent.
33
+ onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: measureContent ? (_jsx("div", { [ACTIONS_CONTENT_ATTRIBUTE]: "", className: "ml-auto w-max", children: children })) : (children) }));
34
+ }