@jini-ai/ui 0.4.4 → 0.4.6

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 (165) hide show
  1. package/CHANGELOG.md +10 -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/providers/recharts/chart-tokens.d.ts +1 -1
  56. package/dist/features/interactive-ui/providers/recharts/chart-tokens.js +1 -1
  57. package/dist/features/lexical-rich-text-editor/deserialize.d.ts +0 -5
  58. package/dist/features/lexical-rich-text-editor/deserialize.js +0 -5
  59. package/dist/features/lexical-rich-text-editor/mention-node.d.ts +5 -17
  60. package/dist/features/lexical-rich-text-editor/mention-node.js +5 -17
  61. package/dist/features/lexical-rich-text-editor/mention-parser.d.ts +2 -4
  62. package/dist/features/lexical-rich-text-editor/serialize.d.ts +0 -4
  63. package/dist/features/lexical-rich-text-editor/serialize.js +0 -4
  64. package/dist/features/lexical-rich-text-editor/types.d.ts +3 -13
  65. package/dist/features/lexical-rich-text-editor/types.js +3 -13
  66. package/dist/features/mcp-ui/early-message-buffer.d.ts +1 -2
  67. package/dist/features/mcp-ui/early-message-buffer.js +1 -2
  68. package/dist/features/mcp-ui/escape.d.ts +4 -0
  69. package/dist/features/mcp-ui/escape.js +2 -9
  70. package/dist/features/mcp-ui/index.d.ts +1 -0
  71. package/dist/features/mcp-ui/index.js +1 -0
  72. package/dist/features/mcp-ui/protocol.d.ts +2 -5
  73. package/dist/features/mcp-ui/protocol.js +2 -5
  74. package/dist/features/mcp-ui/resource.d.ts +47 -7
  75. package/dist/features/mcp-ui/resource.js +5 -8
  76. package/dist/features/mcp-ui/secret-card.d.ts +115 -0
  77. package/dist/features/mcp-ui/secret-card.js +137 -0
  78. package/dist/features/mcp-ui/surfaces/confirmation.js +5 -0
  79. package/dist/features/mcp-ui/surfaces/fields.d.ts +5 -2
  80. package/dist/features/mcp-ui/surfaces/fields.js +3 -1
  81. package/dist/features/mcp-ui/surfaces/form.d.ts +4 -1
  82. package/dist/features/mcp-ui/surfaces/form.js +65 -3
  83. package/dist/features/mcp-ui/surfaces/outcome.d.ts +2 -0
  84. package/dist/features/mcp-ui/surfaces/text-input.d.ts +14 -6
  85. package/dist/features/mcp-ui/surfaces/text-input.js +16 -2
  86. package/dist/features/mcp-ui/surfaces/tokens.js +1 -1
  87. package/dist/features/memory/react/components/MemoryConnectedPanel.d.ts +1 -1
  88. package/dist/features/memory/react/components/MemoryConnectedPanel.js +0 -1
  89. package/dist/features/panel-kit/helpers/format-timestamp.d.ts +5 -3
  90. package/dist/features/panel-kit/helpers/format-timestamp.js +13 -4
  91. package/dist/features/panel-kit/helpers/permissions.js +2 -0
  92. package/dist/features/panel-kit/helpers/resolve-active-tab-id.js +2 -0
  93. package/dist/features/panel-kit/helpers/standing-draft-local-backup.d.ts +51 -0
  94. package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +77 -0
  95. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.d.ts +8 -0
  96. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +10 -0
  97. package/dist/features/panel-kit/hooks/use-async-action.hooks.d.ts +1 -0
  98. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.d.ts +1 -0
  99. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -0
  100. package/dist/features/panel-kit/hooks/use-serial-writes.hooks.d.ts +8 -0
  101. package/dist/features/panel-kit/hooks/use-serial-writes.hooks.js +3 -0
  102. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.d.ts +211 -0
  103. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +382 -0
  104. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.d.ts +179 -0
  105. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +300 -0
  106. package/dist/features/panel-kit/index.d.ts +7 -1
  107. package/dist/features/panel-kit/index.js +5 -1
  108. package/dist/features/resource-dashboard/types.d.ts +8 -41
  109. package/dist/features/resource-dashboard/types.js +3 -26
  110. package/dist/features/tab-strip/constants.d.ts +3 -4
  111. package/dist/features/tab-strip/constants.js +3 -4
  112. package/dist/features/tab-strip/dependencies.d.ts +1 -6
  113. package/dist/features/tab-strip/dependencies.js +1 -6
  114. package/dist/features/tab-strip/react/components/TabBar.d.ts +33 -1
  115. package/dist/features/tab-strip/react/components/TabBar.hooks.d.ts +57 -5
  116. package/dist/features/tab-strip/react/components/TabBar.hooks.js +63 -5
  117. package/dist/features/tab-strip/react/components/TabBar.js +12 -1
  118. package/dist/features/tab-strip/rules.d.ts +4 -17
  119. package/dist/features/tab-strip/rules.js +4 -17
  120. package/dist/features/tab-strip/types.d.ts +2 -13
  121. package/dist/icon-name.d.ts +1 -1
  122. package/dist/icon-name.js +1 -0
  123. package/dist/index.d.ts +42 -417
  124. package/dist/index.js +42 -430
  125. package/dist/react/components/CustomSelect.d.ts +6 -2
  126. package/dist/react/components/CustomSelect.js +20 -91
  127. package/dist/react/components/Icon.d.ts +6 -0
  128. package/dist/react/components/Icon.js +12 -0
  129. package/dist/react/components/select-popover.d.ts +106 -0
  130. package/dist/react/components/select-popover.js +210 -0
  131. package/dist/renderers/sandbox-scripts.d.ts +29 -0
  132. package/dist/renderers/sandbox-scripts.js +127 -0
  133. package/dist/renderers/sandboxed-document.js +3 -86
  134. package/dist/renderers/srcdoc/build.d.ts +10 -17
  135. package/dist/renderers/srcdoc/build.js +31 -234
  136. package/dist/styles/variables.css +2 -2
  137. package/dist/theme/apply.d.ts +1 -1
  138. package/dist/theme/apply.js +2 -2
  139. package/dist/theme/color-scheme.d.ts +1 -1
  140. package/dist/theme/default.d.ts +1 -3
  141. package/dist/theme/default.js +2 -10
  142. package/dist/theme/index.d.ts +3 -3
  143. package/dist/theme/index.js +2 -2
  144. package/dist/theme/stylesheet.d.ts +1 -7
  145. package/dist/theme/stylesheet.js +2 -13
  146. package/dist/theme/types.d.ts +1 -38
  147. package/dist/theme/types.js +2 -1
  148. package/dist/theme/validation.d.ts +1 -14
  149. package/dist/theme/validation.js +2 -54
  150. package/dist/utils/endpoint-policy.d.ts +21 -39
  151. package/dist/utils/endpoint-policy.js +19 -107
  152. package/dist/utils/file-system-errors.d.ts +1 -2
  153. package/dist/utils/file-system-errors.js +1 -2
  154. package/dist/utils/ime-composing.d.ts +0 -3
  155. package/dist/utils/ime-composing.js +0 -3
  156. package/dist/utils/platform.d.ts +1 -4
  157. package/dist/utils/platform.js +1 -4
  158. package/dist/utils/smooth-scroll-to-top.d.ts +1 -3
  159. package/dist/utils/smooth-scroll-to-top.js +3 -13
  160. package/dist/utils/timezone.js +1 -5
  161. package/dist/utils/uuid.d.ts +0 -3
  162. package/dist/utils/uuid.js +0 -3
  163. package/dist/utils/visual-stability.d.ts +1 -3
  164. package/dist/utils/visual-stability.js +1 -3
  165. package/package.json +74 -15
@@ -0,0 +1,179 @@
1
+ import type { createStandingDraftLocalBackup } from "../helpers/standing-draft-local-backup.js";
2
+ /**
3
+ * @file Standing-draft autosave (2026-09-06 dispatch, owner's own words: "even if they haven't
4
+ * saved it, temp save it whenever they make a draft... it's just saving to... the database. It
5
+ * should be a standing draft anytime something is updated"). Persists to the server
6
+ * (`server/inbound/admin-http/routes/posts/autosave.ts`, `posts.autosave_json`) so a reload or an
7
+ * accidental exit never loses unsaved edits — browser storage alone was rejected.
8
+ *
9
+ * Feature-agnostic on purpose, matching `use-dirty-guard.hooks.ts`'s own precedent: nothing here
10
+ * imports `features/posts` or `features/pages`, so `PostEditorPort`/`PageEditorPort` each satisfy
11
+ * {@link StandingDraftAutosavePort} structurally (same three method shapes, declared independently
12
+ * per that DI-seam convention) with zero cross-feature import. This is the mechanism that lets one
13
+ * implementation serve both editors with no branch between them.
14
+ *
15
+ * Two things this hook does NOT do, deliberately:
16
+ * - Never auto-applies a recovered draft. {@link StandingDraftAutosaveController.recoverableDraft}
17
+ * is exposed for the caller to render an explicit "restore or discard" banner; silently splicing
18
+ * in a recovered draft is dangerous with two tabs or two operators open on the same entry.
19
+ * - Never compares `recoverableDraft.baseVersion` against the entry's current version itself — the
20
+ * caller (which already has the freshly-loaded record) decides whether to label a recovered draft
21
+ * "outdated" because a real save happened since it was captured.
22
+ *
23
+ * STALE BASIS (added 2026-09-06, fixing silent data loss). `putAutosave` answers `{ applied }`, and
24
+ * `applied: false` means the row's `version` moved under this tab — another tab or operator saved,
25
+ * so every further write against this basis will be refused too (the guard is
26
+ * `host features/post/repo.sqlite.ts`'s `writeAutosave`). That flag used to be thrown
27
+ * away: the hook kept firing refused ticks forever, nothing was ever parked, and a reload produced
28
+ * no recovery banner because there was nothing to recover — an hour of typing could vanish with the
29
+ * operator never told anything. Now a refusal:
30
+ * 1. records {@link StandingDraftStaleBasis} (exposed so the editor can say something true),
31
+ * 2. stops scheduling further writes until the caller supplies a draft on a DIFFERENT `baseVersion`
32
+ * — i.e. until the editor reloads the row; no manual reset call is needed,
33
+ * 3. mirrors the refused text into `lib/standing-draft-local-backup.ts`, the only place it can
34
+ * survive a tab close while the server refuses it, and offers it back through the existing
35
+ * `recoverableDraft` banner on the next mount when the server has nothing parked.
36
+ * The operator's in-memory text is never touched by any of this.
37
+ *
38
+ * The gate and the mirror have DIFFERENT lifetimes on purpose. The gate lifts the instant the
39
+ * caller edits on a fresh basis; the mirror survives until the server has actually accepted a write
40
+ * (or a real Save/discard superseded it), because between those two moments nothing has been
41
+ * persisted anywhere and the mirror is still the only copy of the refused text that outlives the
42
+ * tab. Collapsing the two back into one "clear it all" step reintroduces a window in which a failed
43
+ * request loses the work.
44
+ */
45
+ /** The fields a standing draft carries — see `PostAutosaveSnapshot`'s own server-side doc for why
46
+ * `status` is deliberately absent (a draft must never change whether a row is live). */
47
+ export type StandingDraftAutosaveInput = {
48
+ bodyFormat: "doc";
49
+ bodyJson: Record<string, unknown>;
50
+ title: string;
51
+ slug: string;
52
+ baseVersion: number;
53
+ } | {
54
+ bodyFormat: "html";
55
+ bodyHtml: string;
56
+ title: string;
57
+ slug: string;
58
+ baseVersion: number;
59
+ };
60
+ export interface StandingDraftAutosaveSnapshot {
61
+ bodyFormat: "doc" | "html";
62
+ bodyJson?: Record<string, unknown>;
63
+ bodyHtml?: string;
64
+ title: string;
65
+ slug: string;
66
+ baseVersion: number;
67
+ savedAt: string;
68
+ savedByPrincipalId: string;
69
+ /** Fresh server timestamp used only for the explicit recovery comparison. */
70
+ serverUpdatedAt?: string | undefined;
71
+ }
72
+ /** What this hook needs from the outside world — see this file's header for why it is declared
73
+ * here rather than imported from either feature's own port. */
74
+ export interface StandingDraftAutosavePort {
75
+ /** Captured before expiry so late failures cannot borrow the next operator's identity. */
76
+ getBackupPrincipalId?: (requiredArgs: Record<string, never>, optionalArgs?: Record<string, never>) => string | null;
77
+ /**
78
+ * `options.keepalive` (2026-09-07) asks for an UNLOAD-SAFE transport, and is set only by the exit
79
+ * flush below. An ordinary `fetch` started from `pagehide` or `visibilitychange`→hidden is
80
+ * cancelled when the browser tears the document down — so the newest edit, the one still sitting
81
+ * in the debounce window, was precisely the one those listeners could not actually rescue.
82
+ *
83
+ * Opt-in rather than always-on because `keepalive` carries a 64 KiB body ceiling an ordinary
84
+ * request does not; an implementation that cannot honour it may ignore it (the parameter is
85
+ * optional), which is exactly the behavior this path had before.
86
+ */
87
+ putAutosave(required: {
88
+ id: string;
89
+ draft: StandingDraftAutosaveInput;
90
+ }, options?: {
91
+ keepalive?: boolean;
92
+ }): Promise<{
93
+ applied: boolean;
94
+ }>;
95
+ getAutosave(required: {
96
+ id: string;
97
+ }, optional?: Record<string, never>): Promise<{
98
+ autosave: StandingDraftAutosaveSnapshot | null;
99
+ }>;
100
+ discardAutosave(required: {
101
+ id: string;
102
+ }, optional?: Record<string, never>): Promise<{
103
+ ok: boolean;
104
+ }>;
105
+ }
106
+ /** A write the server refused because the row moved on — see this file's header. Carries the
107
+ * operator's refused text itself, not just a flag: it is the copy nothing else on the client has
108
+ * a claim to, and a conflict notice that drops the work would be worse than the silent drop. */
109
+ export interface StandingDraftStaleBasis {
110
+ /** The `baseVersion` the refused draft was built on. The gate lifts when a later
111
+ * `scheduleAutosave` arrives on a different one. */
112
+ baseVersion: number;
113
+ /** Exactly what was refused. */
114
+ draft: StandingDraftAutosaveInput;
115
+ }
116
+ export interface StandingDraftAutosaveController {
117
+ /** Non-null once the mount-time recovery check resolves and finds a parked draft. Stays exactly
118
+ * as read until {@link dismissRecoverable}/{@link clearStandingDraft} clears it — never
119
+ * overwritten by a later `scheduleAutosave` tick from THIS session, since a session that is
120
+ * actively autosaving has, by definition, already accounted for whatever it found on mount. */
121
+ recoverableDraft: StandingDraftAutosaveSnapshot | null;
122
+ /** Non-null once the server has refused a write for this entry, and until the caller edits on a
123
+ * fresh basis (or a real Save/discard runs). While it is set, `scheduleAutosave` deliberately
124
+ * sends nothing — every such write would be refused. The editor should tell the operator that
125
+ * another save happened, that reloading is what resumes autosaving, and that their text is kept.
126
+ * Purely additive: a caller that ignores it behaves exactly as before EXCEPT that refused writes
127
+ * are no longer retried forever. */
128
+ staleBasis: StandingDraftStaleBasis | null;
129
+ /** Clears `recoverableDraft` locally only (no server call) — the "Discard" affordance calls
130
+ * {@link clearStandingDraft} instead, which also tells the server; this is for a caller that
131
+ * wants to stop showing the banner without discarding anything (e.g. "Restore" applied it into
132
+ * the editor, and a real Save moments later will clear the server-side draft on its own). */
133
+ dismissRecoverable: () => void;
134
+ /** Call on every content change the caller wants protected — cheap, debounced internally
135
+ * (idle 3s / 15s hard ceiling while continuously typing), safe to call on every keystroke.
136
+ * Whatever is still sitting in the debounce window when the editor goes away is written by the
137
+ * flush this hook wires up itself — see the `flushPendingAutosave` effects below. */
138
+ scheduleAutosave: (draft: StandingDraftAutosaveInput) => void;
139
+ /**
140
+ * Clears the server-side standing draft — call after a real Save/Publish succeeds, or when the
141
+ * operator explicitly discards a recovered draft. Cancels any pending (not yet fired) debounce
142
+ * timer FIRST, then enqueues the discard onto the same serial request chain every
143
+ * `scheduleAutosave` tick uses, so a still-in-flight (already fired) autosave write is guaranteed
144
+ * to reach the server before this discard does, and no new one can be queued after it — the
145
+ * ordering guarantee that keeps a late autosave from resurrecting content a real save (or an
146
+ * explicit discard) just superseded.
147
+ */
148
+ clearStandingDraft: () => Promise<void>;
149
+ /**
150
+ * The caller replaced its basis with a newer saved row it did not write itself (an assistant
151
+ * write applied by `use-external-entry-refresh.hooks.ts`, silently or through Load latest). Every
152
+ * write built on an OLDER `baseVersion` is superseded from here on: a pending one is cancelled, a
153
+ * later `scheduleAutosave` on one is dropped (a render that committed before the replacement can
154
+ * still run its autosave effect after this call), and the refusal of one already in flight is
155
+ * ignored. Without this, that write reached the server, was refused, paused autosaving and
156
+ * mirrored text for a version the editor had already left (reviewer finding 2, 2026-09-16).
157
+ *
158
+ * A gate recorded on an older basis lifts, because the editor is no longer on it. That refusal's
159
+ * mirror is kept: nothing has accepted its text anywhere (see this file's header). Does not touch
160
+ * the server-side draft — Load latest also calls {@link clearStandingDraft} for that.
161
+ */
162
+ supersedeBasis: (baseVersion: number) => void;
163
+ preserveFailedSave: (requiredArgs: {
164
+ error: unknown;
165
+ draft: StandingDraftAutosaveInput;
166
+ }, optionalArgs?: Record<string, never>) => void;
167
+ }
168
+ export interface StandingDraftAutosaveOptions {
169
+ port: StandingDraftAutosavePort;
170
+ backup: ReturnType<typeof createStandingDraftLocalBackup>;
171
+ /** `null` before the entry has loaded (or for a not-yet-created entry) — every operation no-ops. */
172
+ entryId: string | null;
173
+ /** Gate for "this editor is in a state where autosave should run at all" (e.g. not while a save
174
+ * or delete is already in flight). Recovery-check and scheduling both respect it. */
175
+ enabled: boolean;
176
+ serverUpdatedAt?: string | undefined;
177
+ }
178
+ export declare function useStandingDraftAutosave(deps: StandingDraftAutosaveOptions, _options?: Record<string, never>): StandingDraftAutosaveController;
179
+ //# sourceMappingURL=use-standing-draft-autosave.hooks.d.ts.map
@@ -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