@lincyaw/workbench 0.0.0-stage → 0.2.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +66 -2
  3. package/package.json +68 -3
  4. package/src/MediaClipEditor.tsx +122 -0
  5. package/src/StepProductionEditor.tsx +130 -0
  6. package/src/approvals.tsx +60 -0
  7. package/src/assistant-mascot.ts +62 -0
  8. package/src/audio-recording-messages.ts +44 -0
  9. package/src/audio-recording.ts +192 -0
  10. package/src/audio-wav.ts +25 -0
  11. package/src/audio-waveform.ts +110 -0
  12. package/src/audition.ts +84 -0
  13. package/src/authoring-client.ts +136 -0
  14. package/src/authoring-draft-fragment.ts +28 -0
  15. package/src/base-controls.tsx +47 -0
  16. package/src/caption-timing-drafts.ts +61 -0
  17. package/src/caption-timing-messages.ts +14 -0
  18. package/src/caption-timing.css +20 -0
  19. package/src/caption-timing.ts +14 -0
  20. package/src/component-draft-actions.ts +54 -0
  21. package/src/component-draft-inspection.ts +35 -0
  22. package/src/component-properties.css +24 -0
  23. package/src/component-properties.ts +45 -0
  24. package/src/component-property-drafts.ts +273 -0
  25. package/src/component-property-messages.ts +31 -0
  26. package/src/components.tsx +631 -0
  27. package/src/context.tsx +27 -0
  28. package/src/diagnostics.ts +113 -0
  29. package/src/document-draft-store.ts +179 -0
  30. package/src/document-drafts.ts +178 -0
  31. package/src/document-editor/BlockNoteSurface.tsx +209 -0
  32. package/src/document-editor/ComponentMaterials.tsx +62 -0
  33. package/src/document-editor/ComponentPropertiesDialog.tsx +282 -0
  34. package/src/document-editor/DocumentBlockFallback.tsx +31 -0
  35. package/src/document-editor/DocumentBlockPreview.tsx +239 -0
  36. package/src/document-editor/DocumentComments.tsx +282 -0
  37. package/src/document-editor/DocumentEditor.tsx +192 -0
  38. package/src/document-editor/DocumentStepNarration.tsx +93 -0
  39. package/src/document-editor/NarrationEditor.tsx +110 -0
  40. package/src/document-editor/NewCommentDrafts.tsx +224 -0
  41. package/src/document-editor/authoring.tsx +259 -0
  42. package/src/document-editor/block-playback.ts +90 -0
  43. package/src/document-editor/block-preview-messages.ts +13 -0
  44. package/src/document-editor/blocknote-messages.ts +98 -0
  45. package/src/document-editor/clipboard.ts +40 -0
  46. package/src/document-editor/colors.ts +57 -0
  47. package/src/document-editor/comment-draft-actions.ts +58 -0
  48. package/src/document-editor/comment-draft-inspection.ts +38 -0
  49. package/src/document-editor/comment-drafts.ts +361 -0
  50. package/src/document-editor/comments.ts +410 -0
  51. package/src/document-editor/file-input.ts +71 -0
  52. package/src/document-editor/focus.ts +54 -0
  53. package/src/document-editor/inline.ts +104 -0
  54. package/src/document-editor/math.tsx +63 -0
  55. package/src/document-editor/messages.ts +14 -0
  56. package/src/document-editor/narration-schema.tsx +85 -0
  57. package/src/document-editor/narration.ts +37 -0
  58. package/src/document-editor/popover.tsx +107 -0
  59. package/src/document-editor/projection.ts +423 -0
  60. package/src/document-editor/rich-text.ts +152 -0
  61. package/src/document-editor/schema.tsx +117 -0
  62. package/src/document-editor/selection.ts +152 -0
  63. package/src/document-editor/style.css +9 -0
  64. package/src/document-editor/styles.d.ts +1 -0
  65. package/src/document-editor/table-identities.ts +67 -0
  66. package/src/document-preview.ts +112 -0
  67. package/src/document-return-point.ts +59 -0
  68. package/src/document-session.ts +678 -0
  69. package/src/document-timing-drafts.ts +61 -0
  70. package/src/document-voices.ts +2 -0
  71. package/src/history.ts +167 -0
  72. package/src/index.ts +36 -0
  73. package/src/jobs.ts +111 -0
  74. package/src/mascot-preference.ts +37 -0
  75. package/src/media-drafts.ts +83 -0
  76. package/src/media-messages.ts +53 -0
  77. package/src/media-time.ts +12 -0
  78. package/src/messages.ts +781 -0
  79. package/src/outline.ts +97 -0
  80. package/src/panels/ChangeTrail.tsx +79 -0
  81. package/src/panels/DocumentNarrationPanel.tsx +27 -0
  82. package/src/panels/DocumentView.tsx +462 -0
  83. package/src/panels/DocumentVoiceDialog.tsx +179 -0
  84. package/src/panels/DocumentVoiceField.tsx +35 -0
  85. package/src/panels/ExportPanel.tsx +133 -0
  86. package/src/panels/HistoryPanel.tsx +88 -0
  87. package/src/panels/HomePanel.tsx +184 -0
  88. package/src/panels/PageDeleteDialog.tsx +33 -0
  89. package/src/panels/PageRenameDialog.tsx +27 -0
  90. package/src/panels/PreviewPanel.tsx +79 -0
  91. package/src/panels/ProductionCaptionTiming.tsx +122 -0
  92. package/src/panels/ProductionMediaClips.tsx +156 -0
  93. package/src/panels/ProductionPanel.tsx +272 -0
  94. package/src/panels/ProductionRecording.tsx +107 -0
  95. package/src/panels/ProductionRecordingRange.tsx +118 -0
  96. package/src/panels/ProductionTimeline.tsx +62 -0
  97. package/src/panels/ProductionTranscription.tsx +140 -0
  98. package/src/panels/SourceDocumentPanel.tsx +215 -0
  99. package/src/panels/SpeechSettingsPanel.tsx +64 -0
  100. package/src/panels/TranscriptionAdoption.tsx +147 -0
  101. package/src/panels/VoiceCloneDialog.tsx +152 -0
  102. package/src/panels/VoicePreviewDialog.tsx +74 -0
  103. package/src/panels/VoiceScopeField.tsx +30 -0
  104. package/src/panels/VoiceoverDialog.tsx +216 -0
  105. package/src/places.ts +102 -0
  106. package/src/preview-playback.ts +125 -0
  107. package/src/preview-pool.ts +320 -0
  108. package/src/preview.ts +110 -0
  109. package/src/production-messages.ts +151 -0
  110. package/src/production-playback.ts +109 -0
  111. package/src/production-voice-playback.ts +114 -0
  112. package/src/recording-adoption.ts +33 -0
  113. package/src/schema-form/PmSchemaForm.tsx +187 -0
  114. package/src/schema-form/context.ts +26 -0
  115. package/src/schema-form/fields.tsx +59 -0
  116. package/src/schema-form/templates.tsx +50 -0
  117. package/src/schema-form/validator.ts +36 -0
  118. package/src/schema-form/widgets.tsx +60 -0
  119. package/src/shortcuts.ts +8 -0
  120. package/src/sortable-list.tsx +201 -0
  121. package/src/speech-settings-messages.ts +40 -0
  122. package/src/step-production.css +5 -0
  123. package/src/step-production.ts +51 -0
  124. package/src/styles.css +1025 -0
  125. package/src/telemetry.ts +152 -0
  126. package/src/transcription-messages.ts +26 -0
  127. package/src/transcription-session.ts +159 -0
  128. package/src/use-recording-adoption.ts +84 -0
  129. package/src/voice-clone-messages.ts +92 -0
  130. package/src/voice-drafts.ts +89 -0
  131. package/src/voice-messages.ts +146 -0
  132. package/src/voice-snapshot.ts +55 -0
  133. package/src/voiceover-jobs.ts +87 -0
  134. package/src/workbench.ts +2328 -0
  135. package/src/workspace-client.ts +480 -0
@@ -0,0 +1,273 @@
1
+ import { isRecord } from '@lincyaw/kernel'
2
+ import type { CanonicalSource, ClientComponentDraftReference, JsonObject } from '@lincyaw/kernel'
3
+ import { DocumentDraftStore } from './document-draft-store.ts'
4
+ import type { DraftStoreCommit } from './document-draft-store.ts'
5
+
6
+ export interface ComponentPropertyTarget { identity: string, memberId: string, blockId: string, stepId?: string }
7
+ export interface ComponentPropertyDraft {
8
+ format: 1
9
+ id: string
10
+ revision: number
11
+ target: ComponentPropertyTarget
12
+ source: CanonicalSource
13
+ definitionId: string
14
+ baseline: JsonObject
15
+ value: JsonObject
16
+ reset: boolean
17
+ composing: boolean
18
+ updatedAt: string
19
+ }
20
+ export interface ComponentPropertyBackup { record: ComponentPropertyDraft, token: string, key: string }
21
+ export interface ComponentPropertyBackupRead { candidates: ComponentPropertyBackup[], unreadable: number }
22
+ export interface ComponentPropertyAgentCandidate extends ComponentPropertyBackup { current: boolean, storage: 'durable' | 'session', status: 'available' | 'pending' | 'session-only' }
23
+ interface Entry { record: ComponentPropertyDraft, store: DocumentDraftStore, operation?: DraftStoreCommit, failed: boolean, origins: ComponentPropertyBackup[] }
24
+ const PREFIX = 'presmium.component-properties.v1:'
25
+ const SETTLED = 'presmium.component-properties-settled.v1:'
26
+ const MAX_CHARS = 1 << 20
27
+ const scope = (target: Pick<ComponentPropertyTarget, 'identity' | 'memberId'>) => JSON.stringify([target.identity, target.memberId])
28
+ const targetKey = (target: ComponentPropertyTarget) => JSON.stringify([target.identity, target.memberId, target.blockId, target.stepId ?? null])
29
+ const prefix = (target: ComponentPropertyTarget) => `${PREFIX}${targetKey(target)}:`
30
+ const markerPrefix = (target: ComponentPropertyTarget) => `${SETTLED}${targetKey(target)}:`
31
+ const scopedPrefix = (kind: string, identity: string, memberId: string) => `${kind}${JSON.stringify([identity, memberId]).slice(0, -1)},`
32
+ const markerOf = (target: ComponentPropertyTarget, reference: { id: string, revision: number, token: string }) => `${markerPrefix(target)}${JSON.stringify([reference.id, reference.revision, reference.token])}`
33
+ const marker = (backup: ComponentPropertyBackup) => markerOf(backup.record.target, { id: backup.record.id, revision: backup.record.revision, token: backup.token })
34
+ const keyOf = (record: ComponentPropertyDraft) => `${prefix(record.target)}${record.id}`
35
+ const tokenOf = async (raw: string) => [...new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(raw)))].map(byte => byte.toString(16).padStart(2, '0')).join('')
36
+
37
+ function valid(value: unknown, target: ComponentPropertyTarget): value is ComponentPropertyDraft {
38
+ return isRecord(value) && value.format === 1 && typeof value.id === 'string' && value.id.length > 0
39
+ && Number.isSafeInteger(value.revision) && (value.revision as number) > 0 && isRecord(value.target)
40
+ && value.target.identity === target.identity && value.target.memberId === target.memberId && value.target.blockId === target.blockId && value.target.stepId === target.stepId
41
+ && isRecord(value.source) && typeof value.source.documentId === 'string' && typeof value.source.version === 'string' && typeof value.source.fingerprint === 'string'
42
+ && typeof value.definitionId === 'string' && isRecord(value.baseline) && isRecord(value.value)
43
+ && typeof value.reset === 'boolean' && typeof value.composing === 'boolean' && typeof value.updatedAt === 'string' && Number.isFinite(Date.parse(value.updatedAt))
44
+ }
45
+
46
+ /** Unapplied form input borrows the existing strict backup actor; only lecture.json is authoring content. */
47
+ export class ComponentPropertyDrafts {
48
+ readonly #entries = new Map<string, Entry>()
49
+ readonly #listeners = new Set<() => void>()
50
+ readonly #revisions = new Map<string, number>()
51
+ readonly #settling = new Map<string, number>()
52
+ #agentReader?: { identity: string, memberId: string, store: DocumentDraftStore }
53
+ readonly #writerOwner = crypto.randomUUID()
54
+ #revision = 0
55
+ #closed = false
56
+ readonly #leave = (event: BeforeUnloadEvent) => {
57
+ if ([...this.#entries.values()].some(entry => entry.failed || entry.operation || entry.record.composing)) event.preventDefault()
58
+ }
59
+ constructor() { if (typeof window !== 'undefined') window.addEventListener('beforeunload', this.#leave) }
60
+ subscribe = (listener: () => void): (() => void) => { this.#listeners.add(listener); return () => this.#listeners.delete(listener) }
61
+ snapshot = (): number => this.#revision
62
+ revision(identity: string, memberId: string): number { return this.#revisions.get(JSON.stringify([identity, memberId])) ?? 0 }
63
+ readable(identity: string, memberId: string): boolean { return !this.#closed && !this.#settling.get(JSON.stringify([identity, memberId])) }
64
+ #settle(target: ComponentPropertyTarget): () => void {
65
+ const key = scope(target)
66
+ this.#settling.set(key, (this.#settling.get(key) ?? 0) + 1); this.#touch(target)
67
+ return () => {
68
+ const remaining = (this.#settling.get(key) ?? 1) - 1
69
+ if (remaining) this.#settling.set(key, remaining); else this.#settling.delete(key)
70
+ this.#touch(target)
71
+ }
72
+ }
73
+ #touch(target: Pick<ComponentPropertyTarget, 'identity' | 'memberId'>): void {
74
+ if (this.#closed) return
75
+ this.#revision++
76
+ this.#revisions.set(scope(target), (this.#revisions.get(scope(target)) ?? 0) + 1)
77
+ for (const listener of this.#listeners) listener()
78
+ }
79
+ get(target: ComponentPropertyTarget): ComponentPropertyDraft | undefined { return this.#entries.get(targetKey(target))?.record }
80
+ canKeep(target: ComponentPropertyTarget): boolean { return !this.#closed && (this.#entries.has(targetKey(target)) || this.#entries.size < 512) }
81
+ status(target: ComponentPropertyTarget): 'available' | 'pending' | 'session-only' {
82
+ const entry = this.#entries.get(targetKey(target))
83
+ return entry?.failed ? 'session-only' : entry?.operation ? 'pending' : 'available'
84
+ }
85
+ has(identity: string, memberId: string): boolean { return [...this.#entries.values()].some(entry => entry.record.target.identity === identity && entry.record.target.memberId === memberId) }
86
+ stopAgentRead(): void {
87
+ const reader = this.#agentReader
88
+ this.#agentReader = undefined
89
+ reader?.store.close()
90
+ if (reader) this.#touch(reader)
91
+ }
92
+
93
+ /** Explicit reads never settle backups; current input shadows older disk bytes even after admission failure. */
94
+ async readForAgent(context: { identity: string, memberId: string, source: CanonicalSource }, signal: AbortSignal, reference?: ClientComponentDraftReference): Promise<{ revision: number, candidates: ComponentPropertyAgentCandidate[], coverage: 'stored-and-session' | 'session-only', skippedInvalid: number }> {
95
+ const { identity, memberId, source } = context
96
+ signal.throwIfAborted()
97
+ if (!this.readable(identity, memberId)) throw new Error('client-component-drafts-unavailable')
98
+ if (this.#agentReader && (this.#agentReader.identity !== identity || this.#agentReader.memberId !== memberId)) this.stopAgentRead()
99
+ if (!this.#agentReader) {
100
+ const store = new DocumentDraftStore(JSON.stringify([identity, memberId]), () => { if (this.#agentReader?.store === store) this.#touch({ identity, memberId }) }, { owner: this.#writerOwner, family: 'component' })
101
+ this.#agentReader = { identity, memberId, store }
102
+ }
103
+ const reader = this.#agentReader, revision = this.revision(identity, memberId), writing = [...this.#entries.values()].filter(entry => entry.record.target.identity === identity && entry.record.target.memberId === memberId)
104
+ const target = reference ? { identity, memberId, blockId: reference.blockId, ...reference.stepId !== undefined ? { stepId: reference.stepId } : {} } : undefined
105
+ const key = target && reference ? `${prefix(target)}${reference.id}` : undefined
106
+ const known = reference && writing.find(entry => keyOf(entry.record) === key)
107
+ if (known && known.record.revision !== reference!.revision) throw new Error('client-component-draft-changed')
108
+ const current = () => { signal.throwIfAborted(); if (this.#agentReader !== reader || !this.readable(identity, memberId) || revision !== this.revision(identity, memberId)) throw new Error('client-component-drafts-changed') }
109
+ let stored: Map<string, unknown>, coverage: 'stored-and-session' | 'session-only' = 'stored-and-session'
110
+ try { stored = await reader.store.read(key && target && reference ? [key, markerOf(target, reference)] : [scopedPrefix(PREFIX, identity, memberId), scopedPrefix(SETTLED, identity, memberId)], signal, 2048) }
111
+ catch (error) {
112
+ current()
113
+ if (error instanceof Error && error.message === 'document-draft-read-limit') throw new Error('client-component-drafts-limit')
114
+ if (reference ? !known : !writing.length) throw new Error('client-component-drafts-unavailable')
115
+ stored = new Map(); coverage = 'session-only'
116
+ }
117
+ current()
118
+ const entries = new Map<string, { raw: unknown, storage: 'durable' | 'session', status: ComponentPropertyAgentCandidate['status'], current: boolean }>()
119
+ for (const [name, raw] of stored) if (name.startsWith(scopedPrefix(PREFIX, identity, memberId)) && (!key || name === key)) entries.set(name, { raw, storage: 'durable', status: 'available', current: false })
120
+ for (const entry of writing) if (!key || keyOf(entry.record) === key) {
121
+ const raw = JSON.stringify(entry.record), name = keyOf(entry.record)
122
+ entries.set(name, { raw, storage: stored.get(name) === raw ? 'durable' : 'session', status: entry.failed ? 'session-only' : entry.operation ? 'pending' : 'available', current: true })
123
+ }
124
+ const candidates: ComponentPropertyAgentCandidate[] = []; let skippedInvalid = 0
125
+ for (const [name, entry] of entries) {
126
+ try {
127
+ if (typeof entry.raw !== 'string' || entry.storage === 'durable' && entry.raw.length > MAX_CHARS) throw new Error('component-draft-data')
128
+ const value: unknown = JSON.parse(entry.raw)
129
+ if (!isRecord(value) || !isRecord(value.target) || typeof value.target.blockId !== 'string' || !value.target.blockId || value.target.blockId.length > 200
130
+ || value.target.stepId !== undefined && (typeof value.target.stepId !== 'string' || !value.target.stepId || value.target.stepId.length > 200)) throw new Error('component-draft-data')
131
+ const owned = { identity, memberId, blockId: value.target.blockId, ...value.target.stepId !== undefined ? { stepId: value.target.stepId as string } : {} }
132
+ if (!valid(value, owned) || name !== keyOf(value) || value.id.length > 200 || value.definitionId.length > 200 || !value.definitionId
133
+ || value.source.documentId !== source.documentId || !value.source.version || value.source.version.length > 200 || !value.source.fingerprint || value.source.fingerprint.length > 200) throw new Error('component-draft-data')
134
+ const token = await tokenOf(entry.raw); current()
135
+ const marked = stored.get(marker({ key: name, token, record: value })) === token
136
+ if (marked && !entry.current) continue
137
+ // Discarding a backup in another tab does not discard this tab's visible input.
138
+ const candidate: ComponentPropertyAgentCandidate = { key: name, token, record: value, current: entry.current, storage: marked ? 'session' : entry.storage, status: marked ? 'session-only' : entry.status }
139
+ if (reference && (value.id !== reference.id || value.revision !== reference.revision || token !== reference.token)) throw new Error('client-component-draft-changed')
140
+ candidates.push(candidate)
141
+ } catch { current(); if (reference) throw new Error('client-component-draft-changed'); skippedInvalid++ }
142
+ }
143
+ current()
144
+ if (reference && candidates.length !== 1) throw new Error('client-component-draft-changed')
145
+ return { revision, candidates: candidates.sort((a, b) => a.record.id.localeCompare(b.record.id)), coverage, skippedInvalid }
146
+ }
147
+
148
+ keep(target: ComponentPropertyTarget, value: { source: CanonicalSource, definitionId: string, baseline: JsonObject, value: JsonObject, reset?: boolean, composing?: boolean }, origins: ComponentPropertyBackup[] = []): ComponentPropertyDraft {
149
+ if (this.#closed) throw new Error('component-draft-closed')
150
+ const key = targetKey(target), previous = this.#entries.get(key)
151
+ if (!previous && this.#entries.size >= 512) throw new Error('component-draft-limit')
152
+ const record: ComponentPropertyDraft = { format: 1, id: previous?.record.id ?? crypto.randomUUID(), revision: (previous?.record.revision ?? 0) + 1,
153
+ target: structuredClone(target), source: structuredClone(value.source), definitionId: value.definitionId, baseline: structuredClone(value.baseline), value: structuredClone(value.value),
154
+ reset: Boolean(value.reset), composing: Boolean(value.composing), updatedAt: new Date().toISOString() }
155
+ const entry: Entry = previous ?? { record, store: new DocumentDraftStore(scope(target), undefined, { owner: this.#writerOwner, family: 'component' }), failed: false, origins: [] }
156
+ entry.record = record
157
+ for (const origin of origins) if (!entry.origins.some(candidate => candidate.key === origin.key && candidate.token === origin.token)) entry.origins.push(origin)
158
+ this.#entries.set(key, entry)
159
+ const raw = JSON.stringify(record)
160
+ const operation = raw.length <= MAX_CHARS ? entry.store.put(keyOf(record), raw) : undefined
161
+ entry.operation = operation
162
+ entry.failed = !operation
163
+ this.#touch(target)
164
+ if (operation) void operation.done.then(result => {
165
+ if (this.#closed || this.#entries.get(key) !== entry || entry.operation !== operation) return
166
+ entry.operation = undefined; entry.failed = result !== 'committed'; this.#touch(target)
167
+ })
168
+ return record
169
+ }
170
+
171
+ /** Reads never settle a foreign backup or replace current input. Recovery is an explicit caller action. */
172
+ async read(target: ComponentPropertyTarget, signal?: AbortSignal): Promise<ComponentPropertyBackupRead> {
173
+ if (this.#closed) throw new Error('component-draft-closed')
174
+ const reader = new DocumentDraftStore(scope(target), undefined, { owner: this.#writerOwner, family: 'component' })
175
+ try {
176
+ const stored = await reader.read([prefix(target), markerPrefix(target)], signal), result: ComponentPropertyBackup[] = []
177
+ if (stored.size > 1024) throw new Error('component-draft-limit')
178
+ let unreadable = 0
179
+ for (const [key, raw] of stored) {
180
+ signal?.throwIfAborted()
181
+ if (!key.startsWith(prefix(target))) continue
182
+ if (typeof raw !== 'string' || raw.length > MAX_CHARS) { unreadable++; continue }
183
+ let record: unknown
184
+ try { record = JSON.parse(raw) } catch { unreadable++; continue }
185
+ if (!valid(record, target) || key !== keyOf(record)) { unreadable++; continue }
186
+ const token = await tokenOf(raw), candidate = { record, token, key }
187
+ signal?.throwIfAborted()
188
+ if (stored.get(marker(candidate)) === token) continue
189
+ const current = this.#entries.get(targetKey(target))
190
+ if (current?.record.id === record.id || current?.origins.some(origin => origin.key === key && origin.token === token)) continue
191
+ result.push(candidate)
192
+ }
193
+ return { candidates: result.sort((a, b) => b.record.updatedAt.localeCompare(a.record.updatedAt)), unreadable }
194
+ } finally { reader.close() }
195
+ }
196
+
197
+ /** Recovery creates a confirmed branch before replacing the current in-memory draft. */
198
+ async recover(backup: ComponentPropertyBackup, expected: ComponentPropertyDraft | undefined, signal?: AbortSignal, guard?: () => void): Promise<boolean> {
199
+ const target = backup.record.target, key = targetKey(target), previous = this.#entries.get(key)
200
+ const owns = () => !this.#closed && this.#entries.get(key) === previous && previous?.record === expected
201
+ && this.status(target) === 'available' && this.canKeep(target)
202
+ signal?.throwIfAborted(); guard?.()
203
+ if (!owns()) return false
204
+ const settled = this.#settle(target)
205
+ const store = new DocumentDraftStore(scope(target), undefined, { owner: this.#writerOwner, family: 'component' })
206
+ let adopted = false
207
+ try {
208
+ const values = await store.read([backup.key, marker(backup)], signal), raw = values.get(backup.key)
209
+ if (typeof raw !== 'string' || raw.length > MAX_CHARS || await tokenOf(raw) !== backup.token || values.get(marker(backup)) === backup.token) return false
210
+ signal?.throwIfAborted()
211
+ if (!owns()) return false
212
+ let value: unknown
213
+ try { value = JSON.parse(raw) } catch { return false }
214
+ if (!valid(value, target) || backup.key !== keyOf(value)) return false
215
+ guard?.()
216
+ const record: ComponentPropertyDraft = { ...structuredClone(value), id: crypto.randomUUID(), revision: 1, composing: false, updatedAt: new Date().toISOString() }
217
+ const fork = JSON.stringify(record)
218
+ if (fork.length > MAX_CHARS) return false
219
+ const operation = store.put(keyOf(record), fork)
220
+ if (!operation || await operation.done !== 'committed') return false
221
+ signal?.throwIfAborted()
222
+ // A later keystroke retains ownership; the committed branch remains recoverable.
223
+ guard?.()
224
+ if (!owns()) return false
225
+ this.#entries.set(key, { record, store, failed: false, origins: [backup] })
226
+ previous?.store.close()
227
+ adopted = true; this.#touch(target)
228
+ return true
229
+ } finally { if (!adopted) store.close(); settled() }
230
+ }
231
+
232
+ /** Both the in-memory revision and strict storage owner must still match the acknowledged input. */
233
+ async clear(expected: ComponentPropertyDraft, options?: { preserveOrigins?: boolean }): Promise<boolean> {
234
+ const key = targetKey(expected.target), entry = this.#entries.get(key)
235
+ if (this.#closed || !entry || entry.record !== expected) return false
236
+ const settled = this.#settle(expected.target)
237
+ try {
238
+ const operation = entry.store.remove(keyOf(expected), options?.preserveOrigins ? [] : entry.origins.map(origin => [marker(origin), origin.token] as [string, string]))
239
+ if (!operation) { entry.failed = true; this.#touch(expected.target); return false }
240
+ entry.operation = operation; this.#touch(expected.target)
241
+ const result = await operation.done
242
+ if (this.#closed || this.#entries.get(key) !== entry) return false
243
+ if (entry.operation === operation) { entry.operation = undefined; entry.failed = result !== 'committed' }
244
+ if (result === 'committed' && entry.record === expected) { this.#entries.delete(key); entry.store.close(); this.#touch(expected.target); return true }
245
+ this.#touch(expected.target)
246
+ return false
247
+ } finally { settled() }
248
+ }
249
+
250
+ /** A discarded foreign revision is marked exactly; newer input under that key remains recoverable. */
251
+ async discard(backup: ComponentPropertyBackup, signal?: AbortSignal, guard?: () => void): Promise<boolean> {
252
+ signal?.throwIfAborted(); guard?.()
253
+ if (this.#closed) return false
254
+ const settled = this.#settle(backup.record.target)
255
+ const reader = new DocumentDraftStore(scope(backup.record.target), undefined, { owner: this.#writerOwner, family: 'component' })
256
+ try {
257
+ const values = await reader.read([backup.key], signal), raw = values.get(backup.key)
258
+ if (typeof raw !== 'string' || await tokenOf(raw) !== backup.token) return false
259
+ signal?.throwIfAborted(); guard?.()
260
+ const result = await reader.mark([[marker(backup), backup.token]])
261
+ this.#touch(backup.record.target)
262
+ return result
263
+ } finally { reader.close(); settled() }
264
+ }
265
+ close(): void {
266
+ if (this.#closed) return
267
+ this.#closed = true
268
+ this.stopAgentRead()
269
+ if (typeof window !== 'undefined') window.removeEventListener('beforeunload', this.#leave)
270
+ for (const entry of this.#entries.values()) entry.store.close()
271
+ this.#listeners.clear()
272
+ }
273
+ }
@@ -0,0 +1,31 @@
1
+ export const COMPONENT_PROPERTY_MESSAGES = {
2
+ 'zh-CN': {
3
+ properties: '属性', scope: '参数范围', base: '基础参数', step: '当前步骤', materials: '素材与引用', back: '返回参数',
4
+ apply: '应用', adopt: '采用', cancel: '取消', done: '完成', undo: '撤消', reset: '使用继承参数', resetPending: '将使用继承参数', edit: '继续编辑',
5
+ loading: '正在加载参数…', empty: '此内容没有公开参数。', changed: '文稿已更新,请重新检查。', recheck: '再次检查',
6
+ invalid: '请检查参数。', failed: '无法应用参数。输入已保留,请重试。', denied: '没有修改权限。', missing: '此内容已移除。',
7
+ storage: '输入仅在本次会话中保留。', storagePending: '正在备份输入…', storageRead: '无法读取备份。', limit: '无法再保留参数草稿。',
8
+ retry: '重试', backup: '参数备份', recover: '恢复草稿', discard: '放弃', discardChanges: '放弃更改', unreadable: '部分备份无法读取。',
9
+ backupChanged: '备份或输入已变化,请重新加载。', definitionChanged: '素材已改变,请核对保留的输入。',
10
+ preserved: '保留已有步骤覆盖', impact: (count: number) => `影响 ${count} 个步骤`,
11
+ search: '搜索素材', definitions: '已有素材', noDefinitions: '没有兼容素材。', results: (count: number) => `${count} 项`,
12
+ current: '当前素材', incompatible: '不适用于当前内容', module: '暂不支持此素材', references: '引用位置', locate: '定位',
13
+ actions: '素材操作', copy: '创建独立副本', remove: '删除', unavailable: '尚未提供独立副本或删除能力。', copyUnavailable: '尚未提供独立副本。', inUse: '此素材正在被引用。',
14
+ saved: '参数已应用到此内容。', resetReason: '之后将使用继承的参数。',
15
+ },
16
+ en: {
17
+ properties: 'Properties', scope: 'Parameter scope', base: 'Base parameters', step: 'Current step', materials: 'Assets and References', back: 'Back to Parameters',
18
+ apply: 'Apply', adopt: 'Use', cancel: 'Cancel', done: 'Done', undo: 'Undo', reset: 'Use Inherited Parameters', resetPending: 'Inherited parameters will be used', edit: 'Keep Editing',
19
+ loading: 'Loading parameters…', empty: 'No public parameters.', changed: 'The document changed. Review again.', recheck: 'Check Again',
20
+ invalid: 'Check the parameters.', failed: 'Couldn’t apply the parameters. Your input is kept; try again.', denied: 'Editing is not permitted.', missing: 'This content was removed.',
21
+ storage: 'Input is kept only in this session.', storagePending: 'Backing up input…', storageRead: 'Couldn’t read the backups.', limit: 'No more parameter drafts can be kept.',
22
+ retry: 'Retry', backup: 'Parameter backup', recover: 'Recover Draft', discard: 'Discard', discardChanges: 'Discard Changes', unreadable: 'Some backups could not be read.',
23
+ backupChanged: 'The backup or input changed. Reload it.', definitionChanged: 'The asset changed. Review the input that was kept.',
24
+ preserved: 'Keep existing step overrides', impact: (count: number) => `Affects ${count} ${count === 1 ? 'step' : 'steps'}`,
25
+ search: 'Search assets', definitions: 'Existing assets', noDefinitions: 'No compatible assets.', results: (count: number) => `${count} ${count === 1 ? 'item' : 'items'}`,
26
+ current: 'Current asset', incompatible: 'Not compatible with this content', module: 'This asset is not supported yet', references: 'References', locate: 'Locate',
27
+ actions: 'Asset Actions', copy: 'Make an Independent Copy', remove: 'Delete', unavailable: 'Independent copies and deletion are not available yet.', copyUnavailable: 'Independent copies are not available yet.', inUse: 'This asset is in use.',
28
+ saved: 'Parameters applied to this content.', resetReason: 'The inherited parameters will be used.',
29
+ },
30
+ }
31
+ export type ComponentPropertyMessages = typeof COMPONENT_PROPERTY_MESSAGES['en']