@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.
- package/CHANGELOG.md +11 -0
- package/README.md +20 -0
- package/dist/core/theme/default.d.ts +4 -0
- package/dist/core/theme/default.js +11 -0
- package/dist/core/theme/stylesheet.d.ts +8 -0
- package/dist/core/theme/stylesheet.js +14 -0
- package/dist/core/theme/types.d.ts +39 -0
- package/dist/core/theme/types.js +2 -0
- package/dist/core/theme/validation.d.ts +15 -0
- package/dist/core/theme/validation.js +55 -0
- package/dist/features/admin-widgets/components/Select/Select.d.ts +11 -3
- package/dist/features/admin-widgets/components/Select/Select.hooks.d.ts +8 -13
- package/dist/features/admin-widgets/components/Select/Select.hooks.js +37 -215
- package/dist/features/admin-widgets/components/Select/Select.js +4 -3
- package/dist/features/appearance/constants.js +1 -1
- package/dist/features/connectors/rules.d.ts +1 -6
- package/dist/features/connectors/types.d.ts +1 -6
- package/dist/features/connectors/types.js +1 -6
- package/dist/features/execution/http-adapter.d.ts +111 -0
- package/dist/features/execution/http-adapter.js +160 -0
- package/dist/features/execution/index.d.ts +4 -0
- package/dist/features/execution/index.js +2 -0
- package/dist/features/execution/react/hooks/use-execution-credential.hooks.d.ts +232 -0
- package/dist/features/execution/react/hooks/use-execution-credential.hooks.js +217 -0
- package/dist/features/html-editor/canvas-style.d.ts +1 -1
- package/dist/features/html-editor/canvas-style.js +2 -2
- package/dist/features/i18n/dictionaries/settings-dialog.ar.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.bn.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.de.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.en.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.es.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.fa.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.fr.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.hi.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.hu.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.id.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.it.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ja.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ko.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.pl.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ru.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.th.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.tr.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.uk.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ur.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +2 -0
- package/dist/features/i18n/dictionary-translator.d.ts +5 -2
- package/dist/features/i18n/dictionary-translator.js +1 -1
- package/dist/features/i18n/index.d.ts +1 -1
- package/dist/features/i18n/index.js +1 -1
- package/dist/features/i18n/template-i18n.d.ts +11 -0
- package/dist/features/i18n/template-i18n.js +12 -0
- package/dist/features/interactive-ui/interactive-ui.css +1 -1
- package/dist/features/interactive-ui/providers/recharts/bar-chart.js +3 -2
- package/dist/features/interactive-ui/providers/recharts/chart-tokens.d.ts +43 -0
- package/dist/features/interactive-ui/providers/recharts/chart-tokens.js +44 -0
- package/dist/features/interactive-ui/providers/recharts/line-chart.js +3 -2
- package/dist/features/interactive-ui/providers/recharts/pie-chart.js +2 -2
- package/dist/features/lexical-rich-text-editor/deserialize.d.ts +0 -5
- package/dist/features/lexical-rich-text-editor/deserialize.js +0 -5
- package/dist/features/lexical-rich-text-editor/mention-node.d.ts +5 -17
- package/dist/features/lexical-rich-text-editor/mention-node.js +5 -17
- package/dist/features/lexical-rich-text-editor/mention-parser.d.ts +2 -4
- package/dist/features/lexical-rich-text-editor/serialize.d.ts +0 -4
- package/dist/features/lexical-rich-text-editor/serialize.js +0 -4
- package/dist/features/lexical-rich-text-editor/types.d.ts +3 -13
- package/dist/features/lexical-rich-text-editor/types.js +3 -13
- package/dist/features/mcp-ui/early-message-buffer.d.ts +1 -2
- package/dist/features/mcp-ui/early-message-buffer.js +1 -2
- package/dist/features/mcp-ui/escape.d.ts +4 -0
- package/dist/features/mcp-ui/escape.js +2 -9
- package/dist/features/mcp-ui/index.d.ts +1 -0
- package/dist/features/mcp-ui/index.js +1 -0
- package/dist/features/mcp-ui/protocol.d.ts +2 -5
- package/dist/features/mcp-ui/protocol.js +2 -5
- package/dist/features/mcp-ui/resource.d.ts +47 -7
- package/dist/features/mcp-ui/resource.js +5 -8
- package/dist/features/mcp-ui/secret-card.d.ts +115 -0
- package/dist/features/mcp-ui/secret-card.js +137 -0
- package/dist/features/mcp-ui/surfaces/confirmation.js +5 -0
- package/dist/features/mcp-ui/surfaces/fields.d.ts +5 -2
- package/dist/features/mcp-ui/surfaces/fields.js +3 -1
- package/dist/features/mcp-ui/surfaces/form.d.ts +4 -1
- package/dist/features/mcp-ui/surfaces/form.js +65 -3
- package/dist/features/mcp-ui/surfaces/outcome.d.ts +2 -0
- package/dist/features/mcp-ui/surfaces/text-input.d.ts +14 -6
- package/dist/features/mcp-ui/surfaces/text-input.js +16 -2
- package/dist/features/mcp-ui/surfaces/tokens.js +1 -1
- package/dist/features/memory/react/components/MemoryConnectedPanel.d.ts +1 -1
- package/dist/features/memory/react/components/MemoryConnectedPanel.js +0 -1
- package/dist/features/panel-kit/helpers/format-timestamp.d.ts +5 -3
- package/dist/features/panel-kit/helpers/format-timestamp.js +13 -4
- package/dist/features/panel-kit/helpers/permissions.js +2 -0
- package/dist/features/panel-kit/helpers/resolve-active-tab-id.js +2 -0
- package/dist/features/panel-kit/helpers/standing-draft-local-backup.d.ts +51 -0
- package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +77 -0
- package/dist/features/panel-kit/helpers/standing-draft-recovery-message.d.ts +8 -0
- package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +10 -0
- package/dist/features/panel-kit/hooks/use-async-action.hooks.d.ts +1 -0
- package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.d.ts +1 -0
- package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -0
- package/dist/features/panel-kit/hooks/use-serial-writes.hooks.d.ts +8 -0
- package/dist/features/panel-kit/hooks/use-serial-writes.hooks.js +3 -0
- package/dist/features/panel-kit/hooks/use-settings-slice.hooks.d.ts +211 -0
- package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +382 -0
- package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.d.ts +179 -0
- package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +300 -0
- package/dist/features/panel-kit/index.d.ts +7 -1
- package/dist/features/panel-kit/index.js +5 -1
- package/dist/features/resource-dashboard/types.d.ts +8 -41
- package/dist/features/resource-dashboard/types.js +3 -26
- package/dist/features/tab-strip/constants.d.ts +3 -4
- package/dist/features/tab-strip/constants.js +3 -4
- package/dist/features/tab-strip/dependencies.d.ts +1 -6
- package/dist/features/tab-strip/dependencies.js +1 -6
- package/dist/features/tab-strip/react/components/TabBar.d.ts +33 -1
- package/dist/features/tab-strip/react/components/TabBar.hooks.d.ts +57 -5
- package/dist/features/tab-strip/react/components/TabBar.hooks.js +63 -5
- package/dist/features/tab-strip/react/components/TabBar.js +12 -1
- package/dist/features/tab-strip/rules.d.ts +4 -17
- package/dist/features/tab-strip/rules.js +4 -17
- package/dist/features/tab-strip/types.d.ts +2 -13
- package/dist/icon-name.d.ts +1 -1
- package/dist/icon-name.js +1 -0
- package/dist/index.d.ts +42 -417
- package/dist/index.js +42 -430
- package/dist/react/components/CustomSelect.d.ts +6 -2
- package/dist/react/components/CustomSelect.js +20 -91
- package/dist/react/components/Icon.d.ts +6 -0
- package/dist/react/components/Icon.js +12 -0
- package/dist/react/components/select-popover.d.ts +106 -0
- package/dist/react/components/select-popover.js +210 -0
- package/dist/renderers/sandbox-scripts.d.ts +29 -0
- package/dist/renderers/sandbox-scripts.js +127 -0
- package/dist/renderers/sandboxed-document.js +3 -86
- package/dist/renderers/srcdoc/build.d.ts +10 -17
- package/dist/renderers/srcdoc/build.js +31 -234
- package/dist/styles/variables.css +30 -0
- package/dist/theme/apply.d.ts +1 -1
- package/dist/theme/apply.js +2 -2
- package/dist/theme/color-scheme.d.ts +1 -1
- package/dist/theme/default.d.ts +1 -3
- package/dist/theme/default.js +2 -10
- package/dist/theme/index.d.ts +3 -3
- package/dist/theme/index.js +2 -2
- package/dist/theme/stylesheet.d.ts +1 -7
- package/dist/theme/stylesheet.js +2 -13
- package/dist/theme/types.d.ts +1 -38
- package/dist/theme/types.js +2 -1
- package/dist/theme/validation.d.ts +1 -14
- package/dist/theme/validation.js +2 -54
- package/dist/utils/endpoint-policy.d.ts +21 -39
- package/dist/utils/endpoint-policy.js +19 -107
- package/dist/utils/file-system-errors.d.ts +1 -2
- package/dist/utils/file-system-errors.js +1 -2
- package/dist/utils/ime-composing.d.ts +0 -3
- package/dist/utils/ime-composing.js +0 -3
- package/dist/utils/platform.d.ts +1 -4
- package/dist/utils/platform.js +1 -4
- package/dist/utils/smooth-scroll-to-top.d.ts +1 -3
- package/dist/utils/smooth-scroll-to-top.js +3 -13
- package/dist/utils/timezone.js +1 -5
- package/dist/utils/uuid.d.ts +0 -3
- package/dist/utils/uuid.js +0 -3
- package/dist/utils/visual-stability.d.ts +1 -3
- package/dist/utils/visual-stability.js +1 -3
- 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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
/**
|
|
1
|
+
/** Drag feedback duration in milliseconds. */
|
|
2
2
|
export declare const TAB_STRIP_DRAG_HAPTIC_MS = 8;
|
|
3
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
1
|
+
/** Drag feedback duration in milliseconds. */
|
|
2
2
|
export const TAB_STRIP_DRAG_HAPTIC_MS = 8;
|
|
3
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|