@jini-ai/ui 0.4.3 → 0.4.5

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 (169) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +20 -0
  3. package/dist/core/theme/default.d.ts +4 -0
  4. package/dist/core/theme/default.js +11 -0
  5. package/dist/core/theme/stylesheet.d.ts +8 -0
  6. package/dist/core/theme/stylesheet.js +14 -0
  7. package/dist/core/theme/types.d.ts +39 -0
  8. package/dist/core/theme/types.js +2 -0
  9. package/dist/core/theme/validation.d.ts +15 -0
  10. package/dist/core/theme/validation.js +55 -0
  11. package/dist/features/admin-widgets/components/Select/Select.d.ts +11 -3
  12. package/dist/features/admin-widgets/components/Select/Select.hooks.d.ts +8 -13
  13. package/dist/features/admin-widgets/components/Select/Select.hooks.js +37 -215
  14. package/dist/features/admin-widgets/components/Select/Select.js +4 -3
  15. package/dist/features/appearance/constants.js +1 -1
  16. package/dist/features/connectors/rules.d.ts +1 -6
  17. package/dist/features/connectors/types.d.ts +1 -6
  18. package/dist/features/connectors/types.js +1 -6
  19. package/dist/features/execution/http-adapter.d.ts +111 -0
  20. package/dist/features/execution/http-adapter.js +160 -0
  21. package/dist/features/execution/index.d.ts +4 -0
  22. package/dist/features/execution/index.js +2 -0
  23. package/dist/features/execution/react/hooks/use-execution-credential.hooks.d.ts +232 -0
  24. package/dist/features/execution/react/hooks/use-execution-credential.hooks.js +217 -0
  25. package/dist/features/html-editor/canvas-style.d.ts +1 -1
  26. package/dist/features/html-editor/canvas-style.js +2 -2
  27. package/dist/features/i18n/dictionaries/settings-dialog.ar.js +2 -0
  28. package/dist/features/i18n/dictionaries/settings-dialog.bn.js +2 -0
  29. package/dist/features/i18n/dictionaries/settings-dialog.de.js +2 -0
  30. package/dist/features/i18n/dictionaries/settings-dialog.en.js +2 -0
  31. package/dist/features/i18n/dictionaries/settings-dialog.es.js +2 -0
  32. package/dist/features/i18n/dictionaries/settings-dialog.fa.js +2 -0
  33. package/dist/features/i18n/dictionaries/settings-dialog.fr.js +2 -0
  34. package/dist/features/i18n/dictionaries/settings-dialog.hi.js +2 -0
  35. package/dist/features/i18n/dictionaries/settings-dialog.hu.js +2 -0
  36. package/dist/features/i18n/dictionaries/settings-dialog.id.js +2 -0
  37. package/dist/features/i18n/dictionaries/settings-dialog.it.js +2 -0
  38. package/dist/features/i18n/dictionaries/settings-dialog.ja.js +2 -0
  39. package/dist/features/i18n/dictionaries/settings-dialog.ko.js +2 -0
  40. package/dist/features/i18n/dictionaries/settings-dialog.pl.js +2 -0
  41. package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +2 -0
  42. package/dist/features/i18n/dictionaries/settings-dialog.ru.js +2 -0
  43. package/dist/features/i18n/dictionaries/settings-dialog.th.js +2 -0
  44. package/dist/features/i18n/dictionaries/settings-dialog.tr.js +2 -0
  45. package/dist/features/i18n/dictionaries/settings-dialog.uk.js +2 -0
  46. package/dist/features/i18n/dictionaries/settings-dialog.ur.js +2 -0
  47. package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +2 -0
  48. package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +2 -0
  49. package/dist/features/i18n/dictionary-translator.d.ts +5 -2
  50. package/dist/features/i18n/dictionary-translator.js +1 -1
  51. package/dist/features/i18n/index.d.ts +1 -1
  52. package/dist/features/i18n/index.js +1 -1
  53. package/dist/features/i18n/template-i18n.d.ts +11 -0
  54. package/dist/features/i18n/template-i18n.js +12 -0
  55. package/dist/features/interactive-ui/interactive-ui.css +1 -1
  56. package/dist/features/interactive-ui/providers/recharts/bar-chart.js +3 -2
  57. package/dist/features/interactive-ui/providers/recharts/chart-tokens.d.ts +43 -0
  58. package/dist/features/interactive-ui/providers/recharts/chart-tokens.js +44 -0
  59. package/dist/features/interactive-ui/providers/recharts/line-chart.js +3 -2
  60. package/dist/features/interactive-ui/providers/recharts/pie-chart.js +2 -2
  61. package/dist/features/lexical-rich-text-editor/deserialize.d.ts +0 -5
  62. package/dist/features/lexical-rich-text-editor/deserialize.js +0 -5
  63. package/dist/features/lexical-rich-text-editor/mention-node.d.ts +5 -17
  64. package/dist/features/lexical-rich-text-editor/mention-node.js +5 -17
  65. package/dist/features/lexical-rich-text-editor/mention-parser.d.ts +2 -4
  66. package/dist/features/lexical-rich-text-editor/serialize.d.ts +0 -4
  67. package/dist/features/lexical-rich-text-editor/serialize.js +0 -4
  68. package/dist/features/lexical-rich-text-editor/types.d.ts +3 -13
  69. package/dist/features/lexical-rich-text-editor/types.js +3 -13
  70. package/dist/features/mcp-ui/early-message-buffer.d.ts +1 -2
  71. package/dist/features/mcp-ui/early-message-buffer.js +1 -2
  72. package/dist/features/mcp-ui/escape.d.ts +4 -0
  73. package/dist/features/mcp-ui/escape.js +2 -9
  74. package/dist/features/mcp-ui/index.d.ts +1 -0
  75. package/dist/features/mcp-ui/index.js +1 -0
  76. package/dist/features/mcp-ui/protocol.d.ts +2 -5
  77. package/dist/features/mcp-ui/protocol.js +2 -5
  78. package/dist/features/mcp-ui/resource.d.ts +47 -7
  79. package/dist/features/mcp-ui/resource.js +5 -8
  80. package/dist/features/mcp-ui/secret-card.d.ts +115 -0
  81. package/dist/features/mcp-ui/secret-card.js +137 -0
  82. package/dist/features/mcp-ui/surfaces/confirmation.js +5 -0
  83. package/dist/features/mcp-ui/surfaces/fields.d.ts +5 -2
  84. package/dist/features/mcp-ui/surfaces/fields.js +3 -1
  85. package/dist/features/mcp-ui/surfaces/form.d.ts +4 -1
  86. package/dist/features/mcp-ui/surfaces/form.js +65 -3
  87. package/dist/features/mcp-ui/surfaces/outcome.d.ts +2 -0
  88. package/dist/features/mcp-ui/surfaces/text-input.d.ts +14 -6
  89. package/dist/features/mcp-ui/surfaces/text-input.js +16 -2
  90. package/dist/features/mcp-ui/surfaces/tokens.js +1 -1
  91. package/dist/features/memory/react/components/MemoryConnectedPanel.d.ts +1 -1
  92. package/dist/features/memory/react/components/MemoryConnectedPanel.js +0 -1
  93. package/dist/features/panel-kit/helpers/format-timestamp.d.ts +5 -3
  94. package/dist/features/panel-kit/helpers/format-timestamp.js +13 -4
  95. package/dist/features/panel-kit/helpers/permissions.js +2 -0
  96. package/dist/features/panel-kit/helpers/resolve-active-tab-id.js +2 -0
  97. package/dist/features/panel-kit/helpers/standing-draft-local-backup.d.ts +51 -0
  98. package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +77 -0
  99. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.d.ts +8 -0
  100. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +10 -0
  101. package/dist/features/panel-kit/hooks/use-async-action.hooks.d.ts +1 -0
  102. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.d.ts +1 -0
  103. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -0
  104. package/dist/features/panel-kit/hooks/use-serial-writes.hooks.d.ts +8 -0
  105. package/dist/features/panel-kit/hooks/use-serial-writes.hooks.js +3 -0
  106. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.d.ts +211 -0
  107. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +382 -0
  108. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.d.ts +179 -0
  109. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +300 -0
  110. package/dist/features/panel-kit/index.d.ts +7 -1
  111. package/dist/features/panel-kit/index.js +5 -1
  112. package/dist/features/resource-dashboard/types.d.ts +8 -41
  113. package/dist/features/resource-dashboard/types.js +3 -26
  114. package/dist/features/tab-strip/constants.d.ts +3 -4
  115. package/dist/features/tab-strip/constants.js +3 -4
  116. package/dist/features/tab-strip/dependencies.d.ts +1 -6
  117. package/dist/features/tab-strip/dependencies.js +1 -6
  118. package/dist/features/tab-strip/react/components/TabBar.d.ts +33 -1
  119. package/dist/features/tab-strip/react/components/TabBar.hooks.d.ts +57 -5
  120. package/dist/features/tab-strip/react/components/TabBar.hooks.js +63 -5
  121. package/dist/features/tab-strip/react/components/TabBar.js +12 -1
  122. package/dist/features/tab-strip/rules.d.ts +4 -17
  123. package/dist/features/tab-strip/rules.js +4 -17
  124. package/dist/features/tab-strip/types.d.ts +2 -13
  125. package/dist/icon-name.d.ts +1 -1
  126. package/dist/icon-name.js +1 -0
  127. package/dist/index.d.ts +42 -417
  128. package/dist/index.js +42 -430
  129. package/dist/react/components/CustomSelect.d.ts +6 -2
  130. package/dist/react/components/CustomSelect.js +20 -91
  131. package/dist/react/components/Icon.d.ts +6 -0
  132. package/dist/react/components/Icon.js +12 -0
  133. package/dist/react/components/select-popover.d.ts +106 -0
  134. package/dist/react/components/select-popover.js +210 -0
  135. package/dist/renderers/sandbox-scripts.d.ts +29 -0
  136. package/dist/renderers/sandbox-scripts.js +127 -0
  137. package/dist/renderers/sandboxed-document.js +3 -86
  138. package/dist/renderers/srcdoc/build.d.ts +10 -17
  139. package/dist/renderers/srcdoc/build.js +31 -234
  140. package/dist/styles/variables.css +30 -0
  141. package/dist/theme/apply.d.ts +1 -1
  142. package/dist/theme/apply.js +2 -2
  143. package/dist/theme/color-scheme.d.ts +1 -1
  144. package/dist/theme/default.d.ts +1 -3
  145. package/dist/theme/default.js +2 -10
  146. package/dist/theme/index.d.ts +3 -3
  147. package/dist/theme/index.js +2 -2
  148. package/dist/theme/stylesheet.d.ts +1 -7
  149. package/dist/theme/stylesheet.js +2 -13
  150. package/dist/theme/types.d.ts +1 -38
  151. package/dist/theme/types.js +2 -1
  152. package/dist/theme/validation.d.ts +1 -14
  153. package/dist/theme/validation.js +2 -54
  154. package/dist/utils/endpoint-policy.d.ts +21 -39
  155. package/dist/utils/endpoint-policy.js +19 -107
  156. package/dist/utils/file-system-errors.d.ts +1 -2
  157. package/dist/utils/file-system-errors.js +1 -2
  158. package/dist/utils/ime-composing.d.ts +0 -3
  159. package/dist/utils/ime-composing.js +0 -3
  160. package/dist/utils/platform.d.ts +1 -4
  161. package/dist/utils/platform.js +1 -4
  162. package/dist/utils/smooth-scroll-to-top.d.ts +1 -3
  163. package/dist/utils/smooth-scroll-to-top.js +3 -13
  164. package/dist/utils/timezone.js +1 -5
  165. package/dist/utils/uuid.d.ts +0 -3
  166. package/dist/utils/uuid.js +0 -3
  167. package/dist/utils/visual-stability.d.ts +1 -3
  168. package/dist/utils/visual-stability.js +1 -3
  169. package/package.json +74 -15
@@ -0,0 +1,300 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { useSerialWrites } from "./use-serial-writes.hooks.js";
3
+ /** Idle debounce: fires this long after the LAST `scheduleAutosave` call with no further calls. */
4
+ const IDLE_DEBOUNCE_MS = 3000;
5
+ /** Hard ceiling while continuously typing: once this much time has passed since the first
6
+ * unflushed edit, the next tick fires immediately instead of resetting the idle window again. */
7
+ const MAX_WAIT_MS = 15000;
8
+ export function useStandingDraftAutosave(deps, _options = {}) {
9
+ const { port, backup, entryId, enabled, serverUpdatedAt } = deps;
10
+ const [backupPrincipalId] = useState(() => port.getBackupPrincipalId?.({}, {}));
11
+ const latestDraftRef = useRef(null);
12
+ const backupGenerationRef = useRef(0);
13
+ const [recoverableDraft, setRecoverableDraft] = useState(null);
14
+ const [staleBasis, setStaleBasis] = useState(null);
15
+ // Mirrors `staleBasis` for the SYNCHRONOUS gate in `scheduleAutosave`: the refusal is discovered
16
+ // inside the request chain, and a React state read there would be a render behind — the window in
17
+ // which the very ticks this gate exists to stop would still be scheduled.
18
+ const staleBasisRef = useRef(null);
19
+ // Which entry's refused text is currently mirrored in browser storage, or `null`. Deliberately
20
+ // NOT the same lifetime as the gate above (see this file's header), and deliberately an entry id
21
+ // rather than a boolean: it is the proof that a mirror this hook may drop belongs to the entry
22
+ // being edited right now, so switching entries can never delete another entry's surviving copy.
23
+ const backupEntryIdRef = useRef(null);
24
+ // Every network op (each autosave PUT, and the eventual discard) is appended here, never fired
25
+ // standalone — this IS the ordering guarantee `clearStandingDraft`'s own doc describes. Caught
26
+ // internally (see `enqueue`) so one failed request can never permanently wedge the lane.
27
+ const writes = useSerialWrites();
28
+ const idleTimerRef = useRef(null);
29
+ const firstPendingAtRef = useRef(null);
30
+ // The newest draft handed to `scheduleAutosave` that has NOT yet been written — what a flush
31
+ // sends. A ref, not the timer's own closure, precisely so an exit event can reach it: the
32
+ // closure is unreachable from outside the `setTimeout` callback, which is why cancelling the
33
+ // timer used to be indistinguishable from losing the edit.
34
+ const pendingDraftRef = useRef(null);
35
+ // The lowest `baseVersion` a write may still carry for the current entry — see `supersedeBasis`.
36
+ // `null` until the caller supersedes a basis; reset on an entry change, because another entry's
37
+ // version numbers are unrelated.
38
+ const minBaseVersionRef = useRef(null);
39
+ const clearPendingTimer = useCallback(() => {
40
+ if (idleTimerRef.current !== null)
41
+ clearTimeout(idleTimerRef.current);
42
+ idleTimerRef.current = null;
43
+ firstPendingAtRef.current = null;
44
+ pendingDraftRef.current = null;
45
+ }, []);
46
+ /** The server refused this draft. Records the basis, and mirrors the text locally — the ONLY
47
+ * place it can survive a tab close while the server keeps refusing it. @complexity O(1). */
48
+ const markStaleBasis = useCallback((id, draft) => {
49
+ const next = { baseVersion: draft.baseVersion, draft };
50
+ staleBasisRef.current = next;
51
+ setStaleBasis(next);
52
+ backup.write({ entryId: id, draft, savedAt: new Date().toISOString() }, { principalId: backupPrincipalId });
53
+ backupEntryIdRef.current = id;
54
+ }, [backupPrincipalId, backup]);
55
+ /** Lifts the gate so scheduling resumes. Does NOT touch the mirror — see this file's header for
56
+ * why those two are separate. No-op when nothing is stale, so the common path never re-renders.
57
+ * @complexity O(1). */
58
+ const liftStaleGate = useCallback(() => {
59
+ if (staleBasisRef.current === null)
60
+ return;
61
+ staleBasisRef.current = null;
62
+ setStaleBasis(null);
63
+ }, []);
64
+ /** Drops the browser-storage mirror for the entry being edited, if it has one. Called only where
65
+ * the operator's text provably survives somewhere else — the server just accepted a write (so it
66
+ * is parked server-side), or a real Save/discard superseded it. @complexity O(1). */
67
+ const dropLocalBackup = useCallback(() => {
68
+ const id = backupEntryIdRef.current;
69
+ if (id === null)
70
+ return;
71
+ backupEntryIdRef.current = null;
72
+ backup.clear({ entryId: id }, { principalId: backupPrincipalId });
73
+ }, [backupPrincipalId, backup]);
74
+ // Mount-time (and entry-change) recovery check — one read, never repeated while this entryId
75
+ // stays mounted. A stale in-flight response from a PREVIOUS entryId is dropped via `cancelled`.
76
+ useEffect(() => {
77
+ setRecoverableDraft(null);
78
+ // A different entry is a different basis. A gate recorded for the PREVIOUS entryId must never
79
+ // silence autosave for this one — their `version` numbers are very often both 1, so leaving it
80
+ // set would silently stop autosaving an entry nobody has a conflict on. In-memory only: the
81
+ // previous entry's local mirror is keyed under ITS id and stays recoverable there.
82
+ staleBasisRef.current = null;
83
+ setStaleBasis(null);
84
+ // Same reasoning for the mirror pointer, one step further: the previous entry's mirror stays in
85
+ // storage under ITS OWN key and stays recoverable there — only this hook's permission to drop a
86
+ // mirror is reset, so a later accepted write for THIS entry cannot delete that other copy.
87
+ backupEntryIdRef.current = null;
88
+ minBaseVersionRef.current = null;
89
+ latestDraftRef.current = null;
90
+ if (!enabled || !entryId)
91
+ return;
92
+ let cancelled = false;
93
+ port
94
+ .getAutosave({ id: entryId }, {})
95
+ .then((result) => {
96
+ if (cancelled)
97
+ return;
98
+ const localBackup = backup.read({ entryId }, { principalId: backupPrincipalId });
99
+ // Recorded whether or not it is the copy shown below: it exists, so a real Save or an
100
+ // explicit discard on this entry has to be able to drop it. Without this the mirror written
101
+ // before a reload would outlive every discard and re-offer itself on every later mount.
102
+ if (localBackup !== null)
103
+ backupEntryIdRef.current = entryId;
104
+ // Historically server first — a parked draft is shared across tabs. For expiry recovery,
105
+ // compare capture times instead: a 401 mirror may contain edits newer than that parked copy. The local
106
+ // mirror is the fallback for exactly the case that produced it: the other tab's real save
107
+ // cleared the server-side draft, so this tab reloads into "nothing to recover" while its
108
+ // own refused text is the thing the operator actually wants back.
109
+ const server = result.autosave;
110
+ // A 401 mirror can be newer than the last accepted server autosave. Keep the newest copy.
111
+ const newest = localBackup && (!server || localBackup.savedAt > server.savedAt) ? localBackup : server;
112
+ setRecoverableDraft(newest);
113
+ })
114
+ .catch(() => {
115
+ // A failed server recovery read must not hide the local copy rescued from an expired session.
116
+ if (cancelled)
117
+ return;
118
+ const localBackup = backup.read({ entryId }, { principalId: backupPrincipalId });
119
+ if (localBackup !== null)
120
+ backupEntryIdRef.current = entryId;
121
+ setRecoverableDraft(localBackup);
122
+ });
123
+ return () => {
124
+ cancelled = true;
125
+ };
126
+ }, [enabled, entryId, port, backupPrincipalId, backup]);
127
+ const enqueue = useCallback((op) => writes.run({ task: op }).catch(() => {
128
+ // eslint-disable-next-line no-console -- best-effort background persistence; surfaced to
129
+ // the console rather than the operator, who has nothing actionable to do about one dropped
130
+ // autosave tick, and rather than thrown, which would wedge every later tick on this chain.
131
+ console.error("standing-draft autosave: request failed");
132
+ }), [writes]);
133
+ const preserveFailedSave = useCallback(({ error, draft }, _options = {}) => {
134
+ if (!entryId || !isUnauthorizedDraftWrite(error))
135
+ return;
136
+ backup.write({ entryId, draft, savedAt: new Date().toISOString() }, { principalId: backupPrincipalId });
137
+ backupEntryIdRef.current = entryId;
138
+ }, [entryId, backupPrincipalId, backup]);
139
+ /** The one place `putAutosave` is called. Its `{ applied }` answer is acted on here rather than
140
+ * discarded — see this file's STALE BASIS note. @complexity Time/space: O(1) plus the request. */
141
+ const writeAutosave = useCallback((id, draft, keepalive) => {
142
+ const generation = backupGenerationRef.current;
143
+ return enqueue(async () => {
144
+ let result;
145
+ try {
146
+ result = await port.putAutosave({ id, draft }, { keepalive });
147
+ }
148
+ catch (error) {
149
+ // A 401 can unmount the editor before this settles; the captured identity still owns it.
150
+ // Use the newest scheduled text, since an older request may fail while typing continues.
151
+ if (generation === backupGenerationRef.current)
152
+ preserveFailedSave({ error, draft: latestDraftRef.current ?? draft });
153
+ throw error;
154
+ }
155
+ if (generation !== backupGenerationRef.current)
156
+ return;
157
+ if (!result.applied) {
158
+ // Refused because the caller already moved past this basis — nothing to pause or mirror.
159
+ if (isSupersededBasis(draft.baseVersion, minBaseVersionRef.current))
160
+ return;
161
+ markStaleBasis(id, draft);
162
+ return;
163
+ }
164
+ // Accepted — the text is parked server-side now, which is the ONLY point at which the
165
+ // mirror is redundant and may go.
166
+ liftStaleGate();
167
+ dropLocalBackup();
168
+ });
169
+ }, [dropLocalBackup, enqueue, liftStaleGate, markStaleBasis, port, preserveFailedSave]);
170
+ const scheduleAutosave = useCallback((draft) => {
171
+ if (!enabled || !entryId)
172
+ return;
173
+ latestDraftRef.current = draft;
174
+ if (isSupersededBasis(draft.baseVersion, minBaseVersionRef.current))
175
+ return;
176
+ // The gate. Same basis as the one the server already refused => this write would be refused
177
+ // too, so it is not scheduled at all; a DIFFERENT basis means the editor reloaded the row, so
178
+ // the gate lifts by itself and no caller has to remember to reset anything.
179
+ const stale = staleBasisRef.current;
180
+ if (stale !== null && stale.baseVersion === draft.baseVersion)
181
+ return;
182
+ // The mirror is deliberately left in place here: nothing has been written yet, so until the
183
+ // server accepts, that mirror is still the only copy of the refused text that outlives the tab.
184
+ liftStaleGate();
185
+ const now = Date.now();
186
+ if (firstPendingAtRef.current === null)
187
+ firstPendingAtRef.current = now;
188
+ const waitedSoFar = now - firstPendingAtRef.current;
189
+ // Clamped, not a plain "already past the ceiling?" check: without the clamp, a call arriving
190
+ // just before the ceiling would still schedule a FULL fresh `IDLE_DEBOUNCE_MS`, pushing the
191
+ // actual fire time past `MAX_WAIT_MS` under continuous typing (verified by the ceiling test —
192
+ // an unclamped version never fires within the window it claims to bound).
193
+ const delay = Math.max(0, Math.min(IDLE_DEBOUNCE_MS, MAX_WAIT_MS - waitedSoFar));
194
+ pendingDraftRef.current = draft;
195
+ if (idleTimerRef.current !== null)
196
+ clearTimeout(idleTimerRef.current);
197
+ idleTimerRef.current = setTimeout(() => {
198
+ idleTimerRef.current = null;
199
+ firstPendingAtRef.current = null;
200
+ pendingDraftRef.current = null;
201
+ // Not an exit: the page is still here, so an ordinary fetch (no 64 KiB ceiling) is right.
202
+ writeAutosave(entryId, draft, false);
203
+ }, delay);
204
+ }, [enabled, entryId, liftStaleGate, writeAutosave]);
205
+ /**
206
+ * Writes a pending (not yet fired) autosave immediately instead of waiting the debounce out —
207
+ * the half of the cadence that makes the owner's actual requirement true ("what if they reload
208
+ * the page on accident or exit out of the page? I don't want them to lose their work"). The
209
+ * debounce always leaves a window of up to {@link IDLE_DEBOUNCE_MS} in which the newest edit
210
+ * exists only in React state; without this, every exit inside that window discards it.
211
+ *
212
+ * Goes onto the same serial chain every other write uses, so it can never overtake an in-flight
213
+ * PUT or a discard already queued by {@link clearStandingDraft} — and because that function
214
+ * nulls `pendingDraftRef`, a flush after a real Save is a no-op rather than a resurrection.
215
+ *
216
+ * @complexity Time/space: O(1) — one ref read and at most one enqueued request.
217
+ */
218
+ const flushPendingAutosave = useCallback((keepalive) => {
219
+ const draft = pendingDraftRef.current;
220
+ if (draft === null || !entryId)
221
+ return;
222
+ clearPendingTimer();
223
+ writeAutosave(entryId, draft, keepalive);
224
+ }, [clearPendingTimer, entryId, writeAutosave]);
225
+ // Flush, do not cancel, when this editor goes away. An in-app navigation (the "Posts"/"Pages"
226
+ // back link, or any router move) unmounts the hook while a debounced tick is still pending, and
227
+ // the previous cancel-on-unmount is exactly how the last seconds of typing were lost — verified
228
+ // in a real browser on 2026-09-06: typed into the Pages editor, followed the in-app link one
229
+ // second later, and no PUT was ever issued. The SPA outlives the unmount, so the request started
230
+ // here completes normally. Re-running on an `entryId` change is correct too: the cleanup closes
231
+ // over the OUTGOING entry, so switching entries parks the one being left behind.
232
+ // `keepalive: false` — an unmount is an in-app navigation, not a page teardown. The SPA outlives
233
+ // it, so the request started here completes normally and must not pay keepalive's body ceiling.
234
+ useEffect(() => () => flushPendingAutosave(false), [flushPendingAutosave]);
235
+ // The exits React never sees. `pagehide` covers a tab close, a reload and a same-tab navigation;
236
+ // `visibilitychange` covers a tab switch or the OS backgrounding the browser, where `pagehide`
237
+ // may never fire; `blur` covers switching to another desktop app with the tab still visible.
238
+ // `beforeunload` is deliberately NOT one of them — `use-dirty-guard.hooks.ts` already registers
239
+ // it, and it is a PROMPT, not a persistence hook: answering "Leave" still discards whatever the
240
+ // debounce had not written. Each handler is a no-op when nothing is pending, so overlapping
241
+ // events (a close fires blur AND pagehide) cost one write, not three.
242
+ useEffect(() => {
243
+ if (!enabled || !entryId)
244
+ return;
245
+ // Wrapped rather than passed straight to `addEventListener`: a listener is called WITH the
246
+ // Event, which would arrive as `flushPendingAutosave`'s `keepalive` argument — a truthy object,
247
+ // so every flush would silently request keepalive whether or not it is an exit.
248
+ const onExit = () => flushPendingAutosave(true);
249
+ const onVisibilityChange = () => {
250
+ if (document.visibilityState === "hidden")
251
+ flushPendingAutosave(true);
252
+ };
253
+ window.addEventListener("pagehide", onExit);
254
+ window.addEventListener("blur", onExit);
255
+ document.addEventListener("visibilitychange", onVisibilityChange);
256
+ return () => {
257
+ window.removeEventListener("pagehide", onExit);
258
+ window.removeEventListener("blur", onExit);
259
+ document.removeEventListener("visibilitychange", onVisibilityChange);
260
+ };
261
+ }, [enabled, entryId, flushPendingAutosave]);
262
+ const clearStandingDraft = useCallback(async () => {
263
+ // A late 401/refusal must not resurrect a draft a successful Save or Discard superseded.
264
+ backupGenerationRef.current++;
265
+ clearPendingTimer();
266
+ setRecoverableDraft(null);
267
+ // A real Save landed (or the operator discarded): whatever was refused is superseded, so the
268
+ // gate lifts and the mirror goes with it — otherwise the next mount would offer a ghost. This
269
+ // is also the ONLY thing that clears a mirror the operator has already been shown and chose to
270
+ // discard, since by then the gate that wrote it belongs to a previous mount.
271
+ liftStaleGate();
272
+ dropLocalBackup();
273
+ if (!entryId)
274
+ return;
275
+ await enqueue(async () => {
276
+ await port.discardAutosave({ id: entryId }, {});
277
+ });
278
+ }, [clearPendingTimer, dropLocalBackup, enqueue, entryId, liftStaleGate, port]);
279
+ const dismissRecoverable = useCallback(() => setRecoverableDraft(null), []);
280
+ const supersedeBasis = useCallback((baseVersion) => {
281
+ minBaseVersionRef.current = baseVersion;
282
+ const pending = pendingDraftRef.current;
283
+ if (pending !== null && isSupersededBasis(pending.baseVersion, baseVersion))
284
+ clearPendingTimer();
285
+ const stale = staleBasisRef.current;
286
+ if (stale !== null && isSupersededBasis(stale.baseVersion, baseVersion))
287
+ liftStaleGate();
288
+ }, [clearPendingTimer, liftStaleGate]);
289
+ const comparedDraft = useMemo(() => recoverableDraft ? { ...recoverableDraft, serverUpdatedAt } : null, [recoverableDraft, serverUpdatedAt]);
290
+ return { recoverableDraft: comparedDraft, staleBasis, dismissRecoverable, scheduleAutosave, clearStandingDraft, supersedeBasis, preserveFailedSave };
291
+ }
292
+ /** Whether a write built on `baseVersion` belongs to a basis the caller has left (see
293
+ * `supersedeBasis`). @complexity O(1). */
294
+ function isSupersededBasis(baseVersion, minBaseVersion) {
295
+ return minBaseVersion !== null && baseVersion < minBaseVersion;
296
+ }
297
+ function isUnauthorizedDraftWrite(error) {
298
+ return typeof error === "object" && error !== null && "status" in error && error.status === 401;
299
+ }
300
+ //# sourceMappingURL=use-standing-draft-autosave.hooks.js.map
@@ -13,7 +13,13 @@ export { resolveActiveTabId } from './helpers/resolve-active-tab-id.js';
13
13
  export { hasPermission } from './helpers/permissions.js';
14
14
  export { isAbortError, retryWhileUnreachable, UNREACHABLE_RETRY_DELAYS_MS } from './helpers/retry-unreachable.js';
15
15
  export { buildAgentListHandles } from '@jini-ai/agentic';
16
- export { createDictionaryTranslator, interpolate, splitOnPlaceholders, pickPlural } from '../i18n/index.js';
16
+ export { createDictionaryTranslator, interpolate, splitOnPlaceholders, pickPlural, localeEntry } from '../i18n/index.js';
17
17
  export type { Translate, DictionaryTranslator, LocaleDictionary } from '../i18n/index.js';
18
18
  export type { DirtyGuardHostPort } from './hooks/use-dirty-guard.hooks.js';
19
+ export { useSettingsSlice, mergeSaveStates, commitQueuedSave, canAcceptRefresh, SAVE_DEBOUNCE_MS } from './hooks/use-settings-slice.hooks.js';
20
+ export type { SaveState, SettingsSlice, SettingsSliceOptions, SettingsSliceRefreshPort, SettingsRefreshScope } from './hooks/use-settings-slice.hooks.js';
21
+ export { useStandingDraftAutosave } from './hooks/use-standing-draft-autosave.hooks.js';
22
+ export type { StandingDraftAutosavePort, StandingDraftAutosaveInput, StandingDraftAutosaveSnapshot, StandingDraftAutosaveController, StandingDraftStaleBasis, StandingDraftAutosaveOptions } from './hooks/use-standing-draft-autosave.hooks.js';
23
+ export { createStandingDraftLocalBackup, LOCAL_BACKUP_PRINCIPAL_ID } from './helpers/standing-draft-local-backup.js';
24
+ export { standingDraftRecoveryMessage } from './helpers/standing-draft-recovery-message.js';
19
25
  //# sourceMappingURL=index.d.ts.map
@@ -9,5 +9,9 @@ export { resolveActiveTabId } from './helpers/resolve-active-tab-id.js';
9
9
  export { hasPermission } from './helpers/permissions.js';
10
10
  export { isAbortError, retryWhileUnreachable, UNREACHABLE_RETRY_DELAYS_MS } from './helpers/retry-unreachable.js';
11
11
  export { buildAgentListHandles } from '@jini-ai/agentic';
12
- export { createDictionaryTranslator, interpolate, splitOnPlaceholders, pickPlural } from '../i18n/index.js';
12
+ export { createDictionaryTranslator, interpolate, splitOnPlaceholders, pickPlural, localeEntry } from '../i18n/index.js';
13
+ export { useSettingsSlice, mergeSaveStates, commitQueuedSave, canAcceptRefresh, SAVE_DEBOUNCE_MS } from './hooks/use-settings-slice.hooks.js';
14
+ export { useStandingDraftAutosave } from './hooks/use-standing-draft-autosave.hooks.js';
15
+ export { createStandingDraftLocalBackup, LOCAL_BACKUP_PRINCIPAL_ID } from './helpers/standing-draft-local-backup.js';
16
+ export { standingDraftRecoveryMessage } from './helpers/standing-draft-recovery-message.js';
13
17
  //# sourceMappingURL=index.js.map
@@ -1,41 +1,10 @@
1
1
  /**
2
- * Generic status-badged resource-dashboard primitives.
3
- *
4
- * Provenance: `ADS-memory/reports/jini-port/god-components-extraction-plan.md`'s "5 more
5
- * overlaps" section flagged "Resource-dashboard shell, twice, both directly
6
- * Run-vocabulary-relevant": `DesignsTab.tsx` (sub-tab sort + search +
7
- * select-mode/bulk-delete + per-item kebab menu + a grid/kanban view toggle
8
- * over a config-driven status-kanban board) and `TasksView.tsx` (hero header
9
- * with metric tiles + a flat row-list with inline per-item action buttons +
10
- * a lazy-loaded expandable per-row run-history sublist). Both were read in
11
- * full from `/tmp/od-source` (see `packages/ui/archived provenance ledger`'s
12
- * `features/resource-dashboard/` section for the full shared-vs-separate
13
- * verdict and quoted structural evidence).
14
- *
15
- * Verdict: genuinely TWO distinct composition shapes, not one. The only
16
- * piece that is actually identical between the two origins is "a status
17
- * value renders as a small badge" plus "group a list of status-bearing
18
- * items into ordered buckets" — DesignsTab uses that grouping to build a
19
- * whole-page kanban board; TasksView never groups by status at all (its
20
- * only status usage is a per-row/per-run `StatusPill`, one level deeper
21
- * than DesignsTab's top-level items). So this feature ships ONE shared
22
- * status vocabulary + `StatusPill` + `groupItemsByStatus` rule, consumed by
23
- * TWO separately composed components: `ResourceBoard` (the DesignsTab
24
- * shape) and `ResourceRowList` (the TasksView shape). Neither is generic
25
- * over the other; forcing them through one composed shell would mean
26
- * bolting kanban-grouping onto a shape that never uses it, or bolting
27
- * expandable nested history onto a shape that never had it.
28
- */
29
- /**
30
- * A host-supplied status value + its display label. Never a hardcoded
31
- * vocabulary: DesignsTab's own 6-value kanban vocabulary
32
- * (`not_started`/`running`/`awaiting_input`/`succeeded`/`failed`/`canceled`)
33
- * and TasksView's own 5-value run vocabulary
34
- * (`succeeded`/`failed`/`running`/`queued`/`canceled`) are two different,
35
- * non-identical instantiations of this same shape — see
36
- * `packages/ui/archived provenance ledger` for the note on reconciling either against
37
- * `@jini-ai/protocol`'s own `RunState` later (explicitly out of scope here).
2
+ * ResourceBoard and ResourceRowList share status badges/vocabulary and ordered grouping rules,
3
+ * but compose separate surfaces: a grid/kanban board versus rows with inline actions and nested
4
+ * lazy history. A combined shell would impose unused grouping or history on the other shape.
38
5
  */
6
+ /** Host-supplied status and label. Board and row/run vocabularies can differ; reconciling them
7
+ * with protocol RunState is a separate decision, not implicit in this display primitive. */
39
8
  export interface ResourceStatusOption {
40
9
  value: string;
41
10
  label: string;
@@ -63,10 +32,7 @@ export interface ResourceMenuActionSpec {
63
32
  * option needs (DesignsTab's `recent` sorts by `updatedAt`, `yours` by
64
33
  * `createdAt` — two different fields on the same origin `Project` type) —
65
34
  * this primitive never assumes a single fixed timestamp field.
66
- * `body` is a host-supplied render slot for anything else (DesignsTab's
67
- * cover-thumbnail resolution across html/image/video/logo/brand kinds stays
68
- * entirely host-owned, never ported into this generic primitive — see
69
- * archived provenance ledger's "Dropped" list).
35
+ * `body` is a host-supplied render slot; content-kind-specific thumbnail resolution is host-owned.
70
36
  */
71
37
  export interface ResourceBoardItem<TBody = unknown> {
72
38
  id: string;
@@ -110,7 +76,8 @@ export interface ResourceRowItem {
110
76
  paused?: boolean;
111
77
  actions: ResourceRowAction[];
112
78
  }
113
- /** One entry in a row's expandable run-history sublist (TasksView's `RoutineRun`, lazy-fetched on first expand). `actions` generalizes the origin's per-run "crystallize"/"view progress" buttons — see archived provenance ledger for what was dropped (the crystallize automation-evolution workflow is OD-specific and not ported; "open" is the one generic action kept). */
79
+ /** Entry in an expandable run-history list. Action kinds are host-owned; the primitive does
80
+ * not implement a product's automation-evolution workflow. */
114
81
  export interface ResourceRunHistoryItem {
115
82
  id: string;
116
83
  status: string;
@@ -1,30 +1,7 @@
1
1
  /**
2
- * Generic status-badged resource-dashboard primitives.
3
- *
4
- * Provenance: `ADS-memory/reports/jini-port/god-components-extraction-plan.md`'s "5 more
5
- * overlaps" section flagged "Resource-dashboard shell, twice, both directly
6
- * Run-vocabulary-relevant": `DesignsTab.tsx` (sub-tab sort + search +
7
- * select-mode/bulk-delete + per-item kebab menu + a grid/kanban view toggle
8
- * over a config-driven status-kanban board) and `TasksView.tsx` (hero header
9
- * with metric tiles + a flat row-list with inline per-item action buttons +
10
- * a lazy-loaded expandable per-row run-history sublist). Both were read in
11
- * full from `/tmp/od-source` (see `packages/ui/archived provenance ledger`'s
12
- * `features/resource-dashboard/` section for the full shared-vs-separate
13
- * verdict and quoted structural evidence).
14
- *
15
- * Verdict: genuinely TWO distinct composition shapes, not one. The only
16
- * piece that is actually identical between the two origins is "a status
17
- * value renders as a small badge" plus "group a list of status-bearing
18
- * items into ordered buckets" — DesignsTab uses that grouping to build a
19
- * whole-page kanban board; TasksView never groups by status at all (its
20
- * only status usage is a per-row/per-run `StatusPill`, one level deeper
21
- * than DesignsTab's top-level items). So this feature ships ONE shared
22
- * status vocabulary + `StatusPill` + `groupItemsByStatus` rule, consumed by
23
- * TWO separately composed components: `ResourceBoard` (the DesignsTab
24
- * shape) and `ResourceRowList` (the TasksView shape). Neither is generic
25
- * over the other; forcing them through one composed shell would mean
26
- * bolting kanban-grouping onto a shape that never uses it, or bolting
27
- * expandable nested history onto a shape that never had it.
2
+ * ResourceBoard and ResourceRowList share status badges/vocabulary and ordered grouping rules,
3
+ * but compose separate surfaces: a grid/kanban board versus rows with inline actions and nested
4
+ * lazy history. A combined shell would impose unused grouping or history on the other shape.
28
5
  */
29
6
  export {};
30
7
  //# sourceMappingURL=types.js.map
@@ -1,8 +1,7 @@
1
- /** Ported verbatim from `WorkspaceTabsBar.tsx`'s `TAB_DRAG_HAPTIC_MS`. */
1
+ /** Drag feedback duration in milliseconds. */
2
2
  export declare const TAB_STRIP_DRAG_HAPTIC_MS = 8;
3
- /** Ported verbatim from `WorkspaceTabsBar.tsx`'s `TAB_DROP_HAPTIC_MS`. */
3
+ /** Drop feedback duration in milliseconds. */
4
4
  export declare const TAB_STRIP_DROP_HAPTIC_MS = 12;
5
- /** DOM attribute used for strip-level drop-target hit-testing (the generic
6
- * form of `WorkspaceTabsBar.tsx`'s `data-workspace-tab-id`). */
5
+ /** DOM attribute used for strip-level drop-target hit-testing. */
7
6
  export declare const TAB_STRIP_ITEM_ID_ATTRIBUTE = "data-tab-strip-item-id";
8
7
  //# sourceMappingURL=constants.d.ts.map
@@ -1,8 +1,7 @@
1
- /** Ported verbatim from `WorkspaceTabsBar.tsx`'s `TAB_DRAG_HAPTIC_MS`. */
1
+ /** Drag feedback duration in milliseconds. */
2
2
  export const TAB_STRIP_DRAG_HAPTIC_MS = 8;
3
- /** Ported verbatim from `WorkspaceTabsBar.tsx`'s `TAB_DROP_HAPTIC_MS`. */
3
+ /** Drop feedback duration in milliseconds. */
4
4
  export const TAB_STRIP_DROP_HAPTIC_MS = 12;
5
- /** DOM attribute used for strip-level drop-target hit-testing (the generic
6
- * form of `WorkspaceTabsBar.tsx`'s `data-workspace-tab-id`). */
5
+ /** DOM attribute used for strip-level drop-target hit-testing. */
7
6
  export const TAB_STRIP_ITEM_ID_ATTRIBUTE = 'data-tab-strip-item-id';
8
7
  //# sourceMappingURL=constants.js.map
@@ -1,9 +1,4 @@
1
1
  import type { TabStripHapticsPort } from './ports.js';
2
- /**
3
- * Real browser haptics implementation — ported from `WorkspaceTabsBar.tsx`'s
4
- * `pulseTabDragHaptic`: SSR-guarded, opportunistic (a `navigator.vibrate`
5
- * failure/absence is silently swallowed, matching the source's own
6
- * "unsupported environments should keep dragging normally" comment).
7
- */
2
+ /** SSR-safe, opportunistic browser haptics. Unavailable or failed vibration must never stop dragging. */
8
3
  export declare function createBrowserTabStripHaptics(): TabStripHapticsPort;
9
4
  //# sourceMappingURL=dependencies.d.ts.map
@@ -1,9 +1,4 @@
1
- /**
2
- * Real browser haptics implementation — ported from `WorkspaceTabsBar.tsx`'s
3
- * `pulseTabDragHaptic`: SSR-guarded, opportunistic (a `navigator.vibrate`
4
- * failure/absence is silently swallowed, matching the source's own
5
- * "unsupported environments should keep dragging normally" comment).
6
- */
1
+ /** SSR-safe, opportunistic browser haptics. Unavailable or failed vibration must never stop dragging. */
7
2
  export function createBrowserTabStripHaptics() {
8
3
  return {
9
4
  pulse(durationMs) {
@@ -1,14 +1,45 @@
1
1
  import type { ReactNode } from "react";
2
+ /**
3
+ * @file Controlled horizontal tabs: id/label pairs, active id and change callback, with no routing,
4
+ * persistence or panel lifecycle. SettingsDialogShell also owns a header/sidebar/dialog lifecycle;
5
+ * hosts needing only a tab row can use this standalone component.
6
+ * Optional agent handles tag only elements whose handles are supplied.
7
+ */
2
8
  export interface TabBarTab {
3
9
  readonly id: string;
4
10
  readonly label: string;
11
+ /** Inline icon before the label. Always aria-hidden because adjacent text already names the
12
+ * tab. Omit for the plain text layout; this slot does not require SettingsDialogShell. */
5
13
  readonly icon?: ReactNode | undefined;
14
+ /** Shown next to the label when present (e.g. a theme count per tier). Omit to show none. */
6
15
  readonly count?: number | undefined;
16
+ /** Pill after the label, using the admin Sidebar's shared "Soon" styling. A tagged tab stays
17
+ * clickable so its panel can explain status; use disabled when there is no destination. */
7
18
  readonly tag?: string | undefined;
19
+ /**
20
+ * Renders this tab greyed out and non-interactive: no `onChange` call, not part of the tab
21
+ * order (native `disabled`), `aria-disabled` set for assistive tech. For scaffolding a future
22
+ * tab (e.g. a "Marketplace (soon)" placeholder with no real destination yet) that should be
23
+ * visibly present without inviting a click into nothing.
24
+ */
8
25
  readonly disabled?: boolean | undefined;
26
+ /** Publishes this tab button as agent-addressable via `agentHandle()` (`@jini-ai/agentic`). Omit
27
+ * to leave the tab untagged. */
9
28
  readonly handle?: string | undefined;
29
+ /** Plain-English description of what switching to this tab does, paired with {@link handle}.
30
+ * Defaults to `label` (untranslated tab labels, e.g. Deployment's, still read fine as a
31
+ * description; a caller with a translated `label` should pass its own). Ignored if `handle` is
32
+ * omitted. */
10
33
  readonly handleLabel?: string | undefined;
34
+ /**
35
+ * A filled dot before the label exposes configuration state across panels shown one at a time,
36
+ * without making the tab row denser as providers are added. Meaning comes from presence rather
37
+ * than color alone. The dot is aria-hidden; dotLabel supplies its text equivalent as a suffix
38
+ * to the tab's accessible name.
39
+ */
11
40
  readonly dot?: boolean | undefined;
41
+ /** Visually-hidden text appended to this tab's accessible name when {@link dot} is set — e.g.
42
+ * "Connected". Required whenever `dot` is true; ignored otherwise. */
12
43
  readonly dotLabel?: string | undefined;
13
44
  }
14
45
  export interface TabBarProps {
@@ -16,8 +47,9 @@ export interface TabBarProps {
16
47
  activeId: string;
17
48
  onChange: (id: string) => void;
18
49
  ariaLabel: string;
50
+ /** Publishes the tablist container itself as agent-addressable. Omit to leave it untagged. */
19
51
  containerHandle?: string | undefined;
20
52
  }
21
53
  /** Controlled button-based selection strip, including roving focus and optional agent handles. */
22
- export declare function TabBar({ tabs, activeId, onChange, ariaLabel, containerHandle }: TabBarProps): import("react").JSX.Element;
54
+ export declare function TabBar({ tabs, activeId, onChange, ariaLabel, containerHandle }: TabBarProps, _optional?: Record<string, never>): import("react").JSX.Element;
23
55
  //# sourceMappingURL=TabBar.d.ts.map
@@ -1,25 +1,77 @@
1
1
  import type { KeyboardEvent as ReactKeyboardEvent } from "react";
2
2
  import type { TabBarTab } from "./TabBar.js";
3
+ /**
4
+ * @file TabBar's derived props and WAI-ARIA automatic-activation keyboard behavior.
5
+ * Data and event logic live here; TabBar.tsx keeps the JSX render helpers. Arrow keys activate
6
+ * immediately and move focus, while disabled tabs are skipped and non-tab controls keep their keys.
7
+ */
8
+ /** The `TabBarButton` component's own `agentHandle()` spread, as a plain function rather than an
9
+ * inline ternary in its JSX — one more small piece pulled out of `TabBar.tsx` for the same
10
+ * complexity-gate reason that component's own doc comment gives.
11
+ * @example tabHandleProps({ tab: { id: "a", label: "A", handle: "tab-a" } });
12
+ */
3
13
  export declare function tabHandleProps({ tab }: {
4
14
  tab: TabBarTab;
5
- }): {};
6
- /** Resolve arrow/Home/End navigation in enabled-tab order; null means no navigation. */
15
+ }, _optional?: Record<string, never>): {};
16
+ /**
17
+ * Resolve arrow/Home/End navigation in enabled-tab order; null means no navigation.
18
+ *
19
+ * Resolves which tab a keydown on the tablist should move to, for the WAI-ARIA "automatic
20
+ * activation" tabs pattern (arrow keys change the selection immediately, not just focus).
21
+ *
22
+ * @param input.tabs - The tab row in DOM order.
23
+ * @param input.activeId - The currently active tab's id.
24
+ * @param input.key - `KeyboardEvent.key` from the tablist's own `onKeyDown`.
25
+ * @returns The target tab's index into `tabs`, or `null` when `key` isn't a nav key or every tab
26
+ * is disabled. Disabled tabs are never a target. Home/End always resolve to the first/last
27
+ * enabled tab. When `activeId` matches no enabled tab (unset, unknown id, or a disabled tab),
28
+ * ArrowRight/Home resolve to the first enabled tab and ArrowLeft/End resolve to the last —
29
+ * the same "nothing to move from" case Home/End already answer explicitly.
30
+ * @complexity O(n²) time, O(n) space (the enabled-index reducer copies its accumulator) in `tabs.length`.
31
+ * @example resolveTabBarKeyTarget({ tabs, activeId: "a", key: "ArrowRight" });
32
+ */
7
33
  export declare function resolveTabBarKeyTarget({ tabs, activeId, key }: {
8
34
  tabs: readonly TabBarTab[];
9
35
  activeId: string;
10
36
  key: string;
11
- }): number | null;
37
+ }, _optional?: Record<string, never>): number | null;
38
+ /**
39
+ * Resolves one tab's roving-tabindex value — exactly one tab in a `TabBar` is ever in the native
40
+ * Tab order at a time (WAI-ARIA APG roving tabindex), so arrowing between tabs never lets a plain
41
+ * Tab keypress land on more than one of them.
42
+ *
43
+ * @param input.tabs - The tab row in DOM order.
44
+ * @param input.activeId - The currently active tab's id.
45
+ * @param input.tab - The tab being rendered.
46
+ * @returns `0` for the enabled active tab, or for the first enabled tab when `activeId` matches
47
+ * no enabled tab; `-1` otherwise, including every disabled tab.
48
+ * @complexity O(n) in `tabs.length`.
49
+ * @example resolveTabBarTabIndex({ tabs, activeId: "a", tab: tabs[0]! });
50
+ */
12
51
  export declare function resolveTabBarTabIndex({ tabs, activeId, tab }: {
13
52
  tabs: readonly TabBarTab[];
14
53
  activeId: string;
15
54
  tab: TabBarTab;
16
- }): 0 | -1;
55
+ }, _optional?: Record<string, never>): 0 | -1;
17
56
  export interface TabBarKeyboardHandlers {
18
57
  onKeyDown: (e: ReactKeyboardEvent<HTMLDivElement>) => void;
19
58
  }
59
+ /**
60
+ * Wires {@link resolveTabBarKeyTarget} into a tablist's `onKeyDown`: on a nav key, changes the
61
+ * active tab and moves DOM focus to the new tab button, so focus and selection never drift apart.
62
+ * Keydowns that did not originate inside a `[role="tab"]` are ignored, so a non-tab control hosted
63
+ * in the same tablist keeps its own arrow/Home/End keys.
64
+ *
65
+ * @param input.tabs - The tab row in DOM order.
66
+ * @param input.activeId - The currently active tab's id.
67
+ * @param input.onChange - `TabBarProps.onChange`, called with the target tab's id.
68
+ * @returns `onKeyDown`, for the tablist container's own React event prop.
69
+ * @complexity O(n²) time, O(n) space (the enabled-index reducer copies its accumulator) in `tabs.length` per keydown (bounded by the on-screen tab count).
70
+ * @example useTabBarKeyboard({ tabs, activeId, onChange });
71
+ */
20
72
  export declare function useTabBarKeyboard({ tabs, activeId, onChange }: {
21
73
  tabs: readonly TabBarTab[];
22
74
  activeId: string;
23
75
  onChange: (id: string) => void;
24
- }): TabBarKeyboardHandlers;
76
+ }, _optional?: Record<string, never>): TabBarKeyboardHandlers;
25
77
  //# sourceMappingURL=TabBar.hooks.d.ts.map