@lincyaw/workbench 0.1.0 → 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 (147) hide show
  1. package/README.md +40 -8
  2. package/package.json +27 -22
  3. package/src/MediaClipEditor.tsx +122 -0
  4. package/src/StepProductionEditor.tsx +130 -0
  5. package/src/approvals.tsx +60 -0
  6. package/src/assistant-mascot.ts +62 -0
  7. package/src/audio-recording-messages.ts +44 -0
  8. package/src/audio-recording.ts +192 -0
  9. package/src/audio-wav.ts +25 -0
  10. package/src/audio-waveform.ts +110 -0
  11. package/src/audition.ts +84 -0
  12. package/src/authoring-client.ts +136 -0
  13. package/src/authoring-draft-fragment.ts +28 -0
  14. package/src/base-controls.tsx +47 -0
  15. package/src/caption-timing-drafts.ts +61 -0
  16. package/src/caption-timing-messages.ts +14 -0
  17. package/src/caption-timing.css +20 -0
  18. package/src/caption-timing.ts +14 -0
  19. package/src/component-draft-actions.ts +54 -0
  20. package/src/component-draft-inspection.ts +35 -0
  21. package/src/component-properties.css +24 -0
  22. package/src/component-properties.ts +45 -0
  23. package/src/component-property-drafts.ts +273 -0
  24. package/src/component-property-messages.ts +31 -0
  25. package/src/components.tsx +547 -90
  26. package/src/context.tsx +27 -0
  27. package/src/diagnostics.ts +113 -0
  28. package/src/document-draft-store.ts +179 -0
  29. package/src/document-drafts.ts +178 -0
  30. package/src/document-editor/BlockNoteSurface.tsx +209 -0
  31. package/src/document-editor/ComponentMaterials.tsx +62 -0
  32. package/src/document-editor/ComponentPropertiesDialog.tsx +282 -0
  33. package/src/document-editor/DocumentBlockFallback.tsx +31 -0
  34. package/src/document-editor/DocumentBlockPreview.tsx +239 -0
  35. package/src/document-editor/DocumentComments.tsx +282 -0
  36. package/src/document-editor/DocumentEditor.tsx +192 -0
  37. package/src/document-editor/DocumentStepNarration.tsx +93 -0
  38. package/src/document-editor/NarrationEditor.tsx +110 -0
  39. package/src/document-editor/NewCommentDrafts.tsx +224 -0
  40. package/src/document-editor/authoring.tsx +259 -0
  41. package/src/document-editor/block-playback.ts +90 -0
  42. package/src/document-editor/block-preview-messages.ts +13 -0
  43. package/src/document-editor/blocknote-messages.ts +98 -0
  44. package/src/document-editor/clipboard.ts +40 -0
  45. package/src/document-editor/colors.ts +57 -0
  46. package/src/document-editor/comment-draft-actions.ts +58 -0
  47. package/src/document-editor/comment-draft-inspection.ts +38 -0
  48. package/src/document-editor/comment-drafts.ts +361 -0
  49. package/src/document-editor/comments.ts +410 -0
  50. package/src/document-editor/file-input.ts +71 -0
  51. package/src/document-editor/focus.ts +54 -0
  52. package/src/document-editor/inline.ts +104 -0
  53. package/src/document-editor/math.tsx +63 -0
  54. package/src/document-editor/messages.ts +14 -0
  55. package/src/document-editor/narration-schema.tsx +85 -0
  56. package/src/document-editor/narration.ts +37 -0
  57. package/src/document-editor/popover.tsx +107 -0
  58. package/src/document-editor/projection.ts +423 -0
  59. package/src/document-editor/rich-text.ts +152 -0
  60. package/src/document-editor/schema.tsx +117 -0
  61. package/src/document-editor/selection.ts +152 -0
  62. package/src/document-editor/style.css +9 -0
  63. package/src/document-editor/styles.d.ts +1 -0
  64. package/src/document-editor/table-identities.ts +67 -0
  65. package/src/document-preview.ts +112 -0
  66. package/src/document-return-point.ts +59 -0
  67. package/src/document-session.ts +678 -0
  68. package/src/document-timing-drafts.ts +61 -0
  69. package/src/document-voices.ts +2 -0
  70. package/src/history.ts +167 -0
  71. package/src/index.ts +33 -43
  72. package/src/jobs.ts +111 -0
  73. package/src/mascot-preference.ts +37 -0
  74. package/src/media-drafts.ts +83 -0
  75. package/src/media-messages.ts +53 -0
  76. package/src/media-time.ts +12 -0
  77. package/src/messages.ts +751 -203
  78. package/src/outline.ts +97 -0
  79. package/src/panels/ChangeTrail.tsx +79 -0
  80. package/src/panels/DocumentNarrationPanel.tsx +27 -0
  81. package/src/panels/DocumentView.tsx +456 -44
  82. package/src/panels/DocumentVoiceDialog.tsx +179 -0
  83. package/src/panels/DocumentVoiceField.tsx +35 -0
  84. package/src/panels/ExportPanel.tsx +133 -0
  85. package/src/panels/HistoryPanel.tsx +88 -0
  86. package/src/panels/HomePanel.tsx +184 -0
  87. package/src/panels/PageDeleteDialog.tsx +33 -0
  88. package/src/panels/PageRenameDialog.tsx +27 -0
  89. package/src/panels/PreviewPanel.tsx +79 -0
  90. package/src/panels/ProductionCaptionTiming.tsx +122 -0
  91. package/src/panels/ProductionMediaClips.tsx +156 -0
  92. package/src/panels/ProductionPanel.tsx +272 -0
  93. package/src/panels/ProductionRecording.tsx +107 -0
  94. package/src/panels/ProductionRecordingRange.tsx +118 -0
  95. package/src/panels/ProductionTimeline.tsx +62 -0
  96. package/src/panels/ProductionTranscription.tsx +140 -0
  97. package/src/panels/SourceDocumentPanel.tsx +215 -0
  98. package/src/panels/SpeechSettingsPanel.tsx +64 -0
  99. package/src/panels/TranscriptionAdoption.tsx +147 -0
  100. package/src/panels/VoiceCloneDialog.tsx +152 -0
  101. package/src/panels/VoicePreviewDialog.tsx +74 -0
  102. package/src/panels/VoiceScopeField.tsx +30 -0
  103. package/src/panels/VoiceoverDialog.tsx +216 -0
  104. package/src/places.ts +74 -6
  105. package/src/preview-playback.ts +125 -0
  106. package/src/preview-pool.ts +320 -0
  107. package/src/preview.ts +57 -6
  108. package/src/production-messages.ts +151 -0
  109. package/src/production-playback.ts +109 -0
  110. package/src/production-voice-playback.ts +114 -0
  111. package/src/recording-adoption.ts +33 -0
  112. package/src/schema-form/PmSchemaForm.tsx +187 -0
  113. package/src/schema-form/context.ts +26 -0
  114. package/src/schema-form/fields.tsx +59 -0
  115. package/src/schema-form/templates.tsx +50 -0
  116. package/src/schema-form/validator.ts +36 -0
  117. package/src/schema-form/widgets.tsx +60 -0
  118. package/src/shortcuts.ts +8 -0
  119. package/src/sortable-list.tsx +201 -0
  120. package/src/speech-settings-messages.ts +40 -0
  121. package/src/step-production.css +5 -0
  122. package/src/step-production.ts +51 -0
  123. package/src/styles.css +757 -361
  124. package/src/telemetry.ts +152 -0
  125. package/src/transcription-messages.ts +26 -0
  126. package/src/transcription-session.ts +159 -0
  127. package/src/use-recording-adoption.ts +84 -0
  128. package/src/voice-clone-messages.ts +92 -0
  129. package/src/voice-drafts.ts +89 -0
  130. package/src/voice-messages.ts +146 -0
  131. package/src/voice-snapshot.ts +55 -0
  132. package/src/voiceover-jobs.ts +87 -0
  133. package/src/workbench.ts +2328 -0
  134. package/src/workspace-client.ts +480 -0
  135. package/src/api.ts +0 -126
  136. package/src/editor.ts +0 -223
  137. package/src/merge.ts +0 -30
  138. package/src/panels/Changes.tsx +0 -128
  139. package/src/panels/Files.tsx +0 -73
  140. package/src/panels/Home.tsx +0 -224
  141. package/src/panels/Preview.tsx +0 -68
  142. package/src/panels/Problems.tsx +0 -42
  143. package/src/panels/Source.tsx +0 -118
  144. package/src/panels/Status.tsx +0 -26
  145. package/src/react.tsx +0 -59
  146. package/src/service.ts +0 -82
  147. package/src/store.ts +0 -599
package/src/editor.ts DELETED
@@ -1,223 +0,0 @@
1
- import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
2
- import { markdown } from '@codemirror/lang-markdown'
3
- import { yaml, yamlFrontmatter } from '@codemirror/lang-yaml'
4
- import { bracketMatching, HighlightStyle, indentOnInput, syntaxHighlighting } from '@codemirror/language'
5
- import { lintGutter, setDiagnostics } from '@codemirror/lint'
6
- import type { Diagnostic } from '@codemirror/lint'
7
- import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
8
- import { EditorSelection, EditorState, StateEffect, StateField } from '@codemirror/state'
9
- import type { Extension } from '@codemirror/state'
10
- import { Decoration, drawSelection, EditorView, highlightActiveLine, highlightActiveLineGutter, keymap, lineNumbers, MatchDecorator, ViewPlugin } from '@codemirror/view'
11
- import type { DecorationSet, ViewUpdate } from '@codemirror/view'
12
- import { tags } from '@lezer/highlight'
13
-
14
- export interface EditorOptions {
15
- label: string
16
- save(): void
17
- changed(path: string, text: string): void
18
- moved(line: number): void
19
- selected?(selection: { startLine: number, endLine: number, text: string }): void
20
- }
21
-
22
- export interface Problem {
23
- line: number
24
- severity: 'error' | 'warning' | 'info'
25
- message: string
26
- }
27
-
28
- export interface SourceEditor {
29
- show(path: string, text: string): void
30
- text(path: string): string | undefined
31
- replace(path: string, text: string, fresh: boolean): void
32
- reveal(line: number, endLine?: number, mark?: boolean): void
33
- problems(path: string, problems: Problem[]): void
34
- paths(): string[]
35
- forget(path: string): void
36
- focus(): void
37
- destroy(): void
38
- }
39
-
40
- const MYST = new MatchDecorator({
41
- regexp: /^(?:`{3,}|:{3,})\{[\w.:-]+\}.*$|^:[\w-]+:(?=\s|$)|^\([^)\s]+\)=\s*$|\{[\w.:-]+\}`[^`]*`/g,
42
- decoration: match => Decoration.mark({ class: match[0].startsWith('(') ? 'pm-cm-myst-label' : 'pm-cm-myst' }),
43
- })
44
- const mystMarks = ViewPlugin.fromClass(class {
45
- decorations: DecorationSet
46
- constructor(view: EditorView) {
47
- this.decorations = MYST.createDeco(view)
48
- }
49
-
50
- update(update: ViewUpdate) {
51
- this.decorations = MYST.updateDeco(update, this.decorations)
52
- }
53
- }, { decorations: plugin => plugin.decorations })
54
-
55
- const HIGHLIGHT = HighlightStyle.define([
56
- { tag: tags.heading, color: 'var(--pm-color-accent)', fontWeight: '600' },
57
- { tag: [tags.processingInstruction, tags.meta, tags.contentSeparator, tags.comment, tags.punctuation], color: 'var(--pm-color-muted)' },
58
- { tag: tags.emphasis, fontStyle: 'italic' },
59
- { tag: tags.strong, fontWeight: '600' },
60
- { tag: [tags.link, tags.url], color: 'var(--pm-color-accent)' },
61
- { tag: tags.monospace, color: 'var(--pm-color-syntax-string)' },
62
- { tag: [tags.propertyName, tags.definition(tags.propertyName), tags.attributeName, tags.labelName], color: 'var(--pm-color-syntax-key)' },
63
- { tag: [tags.string, tags.special(tags.string)], color: 'var(--pm-color-syntax-string)' },
64
- { tag: [tags.keyword, tags.bool, tags.null, tags.number, tags.atom], color: 'var(--pm-color-syntax-keyword)' },
65
- ])
66
-
67
- const THEME = EditorView.theme({
68
- '&': { height: '100%', backgroundColor: 'var(--pm-color-code)', color: 'var(--pm-color-ink)', fontSize: '0.84375rem' },
69
- '&.cm-focused': { outline: 'none' },
70
- '.cm-scroller': { fontFamily: 'var(--pm-font-mono)', lineHeight: '1.75' },
71
- '.cm-content': { caretColor: 'var(--pm-color-ink)', padding: '14px 0' },
72
- '.cm-line': { padding: '0 14px 0 6px' },
73
- '.cm-gutters': { backgroundColor: 'var(--pm-color-code)', color: 'var(--pm-color-muted)', border: 'none' },
74
- '.cm-lineNumbers .cm-gutterElement': { padding: '0 4px 0 12px', minWidth: '2.5em' },
75
- '.cm-activeLineGutter': { backgroundColor: 'transparent', color: 'var(--pm-color-ink)' },
76
- '.cm-activeLine': { backgroundColor: 'color-mix(in srgb, var(--pm-color-selected) 55%, transparent)' },
77
- '&.cm-focused .cm-cursor': { borderLeftColor: 'var(--pm-color-ink)' },
78
- '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': { backgroundColor: 'var(--pm-color-accent-soft)' },
79
- '.cm-line.pm-cm-picked': { backgroundColor: 'var(--pm-color-selected)', boxShadow: 'inset 2px 0 0 var(--pm-color-accent)' },
80
- '.pm-cm-myst': { color: 'var(--pm-color-syntax-key)' },
81
- '.pm-cm-myst-label': { color: 'var(--pm-color-muted)' },
82
- '.cm-lintRange-error, .cm-lintRange-warning, .cm-lintRange-info': { backgroundImage: 'none', textDecoration: 'underline wavy', textUnderlineOffset: '3px' },
83
- '.cm-lintRange-error': { textDecorationColor: 'var(--pm-color-danger)' },
84
- '.cm-lintRange-warning': { textDecorationColor: 'var(--pm-color-warn)' },
85
- '.cm-lintRange-info': { textDecorationColor: 'var(--pm-color-accent)' },
86
- '.cm-tooltip': { border: '1px solid var(--pm-color-line)', backgroundColor: 'var(--pm-color-surface)', color: 'var(--pm-color-ink)', borderRadius: 'var(--pm-radius-small)' },
87
- '.cm-panels': { backgroundColor: 'var(--pm-color-sunk)', color: 'var(--pm-color-ink)' },
88
- '.cm-searchMatch': { backgroundColor: 'var(--pm-color-warn-soft)' },
89
- '.cm-matchingBracket, &.cm-focused .cm-matchingBracket': { backgroundColor: 'var(--pm-color-accent-soft)' },
90
- })
91
-
92
- const pick = StateEffect.define<{ from: number, to: number } | null>()
93
- const picked = StateField.define<DecorationSet>({
94
- create: () => Decoration.none,
95
- update(marks, transaction) {
96
- marks = marks.map(transaction.changes)
97
- for (const effect of transaction.effects) {
98
- if (!effect.is(pick)) continue
99
- if (!effect.value) return Decoration.none
100
- const { doc } = transaction.state
101
- const lines = []
102
- for (let n = effect.value.from; n <= effect.value.to && n <= doc.lines; n++) lines.push(Decoration.line({ class: 'pm-cm-picked' }).range(doc.line(n).from))
103
- return Decoration.set(lines)
104
- }
105
- return marks
106
- },
107
- provide: field => EditorView.decorations.from(field),
108
- })
109
-
110
- function language(path: string): Extension {
111
- if (/\.(?:md|markdown)$/i.test(path)) return [yamlFrontmatter({ content: markdown() }), mystMarks]
112
- if (/\.ya?ml$/i.test(path)) return yaml()
113
- return []
114
- }
115
-
116
- export function createEditor(parent: HTMLElement, options: EditorOptions): SourceEditor {
117
- const states = new Map<string, EditorState>()
118
- const problems = new Map<string, Problem[]>()
119
- let current: string | undefined
120
- const listener = EditorView.updateListener.of((update) => {
121
- if (!current) return
122
- if (update.docChanged) options.changed(current, update.state.doc.toString())
123
- if (update.docChanged || update.selectionSet) options.moved(update.state.doc.lineAt(update.state.selection.main.head).number)
124
- const selection = update.state.selection.main
125
- if (update.view.hasFocus && update.selectionSet && !selection.empty) {
126
- options.selected?.({
127
- startLine: update.state.doc.lineAt(selection.from).number,
128
- endLine: update.state.doc.lineAt(Math.max(selection.from, selection.to - 1)).number,
129
- text: update.state.sliceDoc(selection.from, selection.to),
130
- })
131
- }
132
- })
133
- const extensions = (path: string): Extension => [
134
- lineNumbers(),
135
- highlightActiveLineGutter(),
136
- highlightActiveLine(),
137
- history(),
138
- drawSelection(),
139
- indentOnInput(),
140
- bracketMatching(),
141
- highlightSelectionMatches(),
142
- lintGutter(),
143
- picked,
144
- EditorView.lineWrapping,
145
- syntaxHighlighting(HIGHLIGHT),
146
- THEME,
147
- language(path),
148
- keymap.of([
149
- { key: 'Mod-s', preventDefault: true, run: () => {
150
- options.save()
151
- return true
152
- } },
153
- ...defaultKeymap,
154
- ...historyKeymap,
155
- ...searchKeymap,
156
- ]),
157
- EditorView.contentAttributes.of({ 'aria-label': options.label }),
158
- listener,
159
- ]
160
- const view = new EditorView({ parent, state: EditorState.create({ doc: '', extensions: [THEME, EditorState.readOnly.of(true)] }) })
161
-
162
- const diagnose = () => {
163
- if (!current) return
164
- const { doc } = view.state
165
- const diagnostics: Diagnostic[] = (problems.get(current) ?? []).filter(problem => problem.line >= 1 && problem.line <= doc.lines).map((problem) => {
166
- const line = doc.line(problem.line)
167
- return { from: line.from, to: line.to, severity: problem.severity, message: problem.message }
168
- })
169
- view.dispatch(setDiagnostics(view.state, diagnostics))
170
- }
171
-
172
- return {
173
- show(path, text) {
174
- if (current === path) return
175
- if (current) states.set(current, view.state)
176
- current = path
177
- view.setState(states.get(path) ?? EditorState.create({ doc: text, extensions: extensions(path) }))
178
- diagnose()
179
- options.moved(view.state.doc.lineAt(view.state.selection.main.head).number)
180
- },
181
- text(path) {
182
- return (path === current ? view.state : states.get(path))?.doc.toString()
183
- },
184
- replace(path, text, fresh) {
185
- if (path !== current) {
186
- states.set(path, EditorState.create({ doc: text, extensions: extensions(path) }))
187
- return
188
- }
189
- if (fresh) {
190
- const head = Math.min(view.state.selection.main.head, text.length)
191
- view.setState(EditorState.create({ doc: text, selection: EditorSelection.cursor(head), extensions: extensions(path) }))
192
- diagnose()
193
- return
194
- }
195
- view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text } })
196
- },
197
- reveal(line, endLine, mark) {
198
- const { doc } = view.state
199
- const first = Math.min(Math.max(line, 1), doc.lines)
200
- const last = Math.min(Math.max(endLine ?? first, first), doc.lines)
201
- const position = doc.line(first).from
202
- view.dispatch({
203
- selection: { anchor: position },
204
- effects: [EditorView.scrollIntoView(position, { y: 'center' }), pick.of(mark ? { from: first, to: last } : null)],
205
- })
206
- },
207
- problems(path, list) {
208
- problems.set(path, list)
209
- if (path === current) diagnose()
210
- },
211
- paths: () => [...new Set([...states.keys(), ...current ? [current] : []])],
212
- forget(path) {
213
- states.delete(path)
214
- problems.delete(path)
215
- if (path === current) {
216
- current = undefined
217
- view.setState(EditorState.create({ doc: '', extensions: [THEME, EditorState.readOnly.of(true)] }))
218
- }
219
- },
220
- focus: () => view.focus(),
221
- destroy: () => view.destroy(),
222
- }
223
- }
package/src/merge.ts DELETED
@@ -1,30 +0,0 @@
1
- import { diff3Merge } from 'node-diff3'
2
-
3
- export interface Merged {
4
- text: string
5
- conflicts: number
6
- }
7
-
8
- export interface MergeLabels {
9
- mine: string
10
- theirs: string
11
- }
12
-
13
- export function mergeTexts(mine: string, base: string, theirs: string, labels: MergeLabels): Merged {
14
- const lines: string[] = []
15
- let conflicts = 0
16
- for (const region of diff3Merge(mine.split('\n'), base.split('\n'), theirs.split('\n'), { excludeFalseConflicts: true })) {
17
- if (region.ok) {
18
- lines.push(...region.ok)
19
- continue
20
- }
21
- const { a, o, b } = region.conflict!
22
- if (o.length === 0) {
23
- lines.push(...b, ...a)
24
- continue
25
- }
26
- conflicts++
27
- lines.push(`<<<<<<< ${labels.mine}`, ...a, '=======', ...b, `>>>>>>> ${labels.theirs}`)
28
- }
29
- return { text: lines.join('\n'), conflicts }
30
- }
@@ -1,128 +0,0 @@
1
- import { useState } from 'react'
2
- import type { FormEvent } from 'react'
3
- import { PmBadge, PmButton, PmDialog, PmIconButton, PmNotice } from '../components.tsx'
4
- import { useMessages, useWorkbench, useWorkbenchStore } from '../react.tsx'
5
-
6
- function Counts({ added, removed }: { added?: number, removed?: number }) {
7
- return (
8
- <span className="pmw-changes__counts">
9
- {added !== undefined && <span data-sign="added">+{added}</span>}
10
- {removed !== undefined && <span data-sign="removed">−{removed}</span>}
11
- </span>
12
- )
13
- }
14
-
15
- export function ChangesPanel() {
16
- const t = useMessages()
17
- const workbench = useWorkbenchStore()
18
- const locale = useWorkbench(state => state.locale)
19
- const review = useWorkbench(state => state.review)
20
- const changed = useWorkbench(state => state.changed)
21
- const history = useWorkbench(state => state.history)
22
- const failure = useWorkbench(state => state.changesFailure)
23
- const dir = useWorkbench(state => state.document)
24
- const short = (path: string) => (dir && path.startsWith(`${dir}/`) ? path.slice(dir.length + 1) : path)
25
- const [reviewMessage, setReviewMessage] = useState('')
26
- const [commitMessage, setCommitMessage] = useState('')
27
- const [discarding, setDiscarding] = useState(false)
28
- const pending = new Set(review?.changes.map(change => change.path))
29
- const own = changed?.filter(file => !pending.has(file.path))
30
- const submit = (work: () => Promise<boolean>, done: () => void) => async (event: FormEvent) => {
31
- event.preventDefault()
32
- if (await work()) done()
33
- }
34
- return (
35
- <section className="pmw-changes" aria-label={t.changes}>
36
- {failure && <PmNotice tone="danger">{t.changesFailed(failure)}</PmNotice>}
37
- {review && (
38
- <section className="pmw-changes__group" aria-label={t.review} data-group="review">
39
- <h2 className="pmw-changes__head">{t.review}<PmBadge tone="accent">{review.changes.length}</PmBadge></h2>
40
- <p className="pmw-muted">{review.running ? t.reviewRunning : t.reviewWaiting(review.changes.length)}</p>
41
- <ul className="pmw-changes__list">
42
- {review.changes.map(change => (
43
- <li key={change.path} className="pmw-changes__file">
44
- <details>
45
- <summary><span className="pmw-changes__path" data-kind={change.kind} title={change.path}>{short(change.path)}</span><Counts added={change.added} removed={change.removed} /></summary>
46
- {change.diff && <pre className="pmw-changes__diff">{change.diff}</pre>}
47
- </details>
48
- <PmIconButton icon="undo" small label={t.rejectFile(short(change.path))} disabled={review.running} onClick={() => void workbench.rejectReview([change.path])} />
49
- </li>
50
- ))}
51
- </ul>
52
- {review.steps.length > 1 && (
53
- <details className="pmw-changes__steps">
54
- <summary>{t.checkpoints}</summary>
55
- <ol>
56
- {review.steps.map(step => (
57
- <li key={step.id}>
58
- <span>{step.label}</span>
59
- <PmButton small disabled={review.running} onClick={() => void workbench.restoreReview(step.id)}>{t.restore}</PmButton>
60
- </li>
61
- ))}
62
- </ol>
63
- </details>
64
- )}
65
- <form className="pmw-changes__form" onSubmit={submit(() => workbench.acceptReview(reviewMessage.trim() || undefined), () => setReviewMessage(''))}>
66
- <input className="pm-ui-input" aria-label={t.optionalMessage} placeholder={t.optionalMessage} value={reviewMessage} onChange={event => setReviewMessage(event.target.value)} />
67
- <PmButton type="submit" variant="primary" icon="check" disabled={review.running}>{t.accept}</PmButton>
68
- <PmButton variant="danger" disabled={review.running} onClick={() => void workbench.rejectReview()}>{t.rejectAll}</PmButton>
69
- </form>
70
- </section>
71
- )}
72
- <section className="pmw-changes__group" aria-label={t.uncommittedFiles} data-group="uncommitted">
73
- <h2 className="pmw-changes__head">{t.uncommittedFiles}{own?.length ? <PmBadge>{own.length}</PmBadge> : null}</h2>
74
- {own?.length === 0 && <p className="pmw-muted">{t.noUncommitted}</p>}
75
- {own && own.length > 0 && (
76
- <>
77
- <ul className="pmw-changes__list">
78
- {own.map(file => (
79
- <li key={file.path} className="pmw-changes__file">
80
- <span className="pmw-changes__path" data-kind={file.kind} title={file.path}>{short(file.path)}</span>
81
- <Counts added={file.added} removed={file.removed} />
82
- </li>
83
- ))}
84
- </ul>
85
- <form className="pmw-changes__form" onSubmit={submit(() => workbench.commitChanges(commitMessage.trim()), () => setCommitMessage(''))}>
86
- <input className="pm-ui-input" required aria-label={t.commitMessage} placeholder={t.commitMessage} value={commitMessage} onChange={event => setCommitMessage(event.target.value)} />
87
- <PmButton type="submit" variant="primary" disabled={Boolean(review)}>{t.commit}</PmButton>
88
- <PmButton disabled={Boolean(review)} onClick={() => setDiscarding(true)}>{t.discard}</PmButton>
89
- </form>
90
- </>
91
- )}
92
- </section>
93
- <section className="pmw-changes__group" aria-label={t.history} data-group="history">
94
- <h2 className="pmw-changes__head">{t.history}</h2>
95
- {history?.length === 0 && <p className="pmw-muted">{t.noHistory}</p>}
96
- <ol className="pmw-changes__list">
97
- {history?.map(revision => (
98
- <li key={revision.id} className="pmw-changes__revision" data-revision={revision.id}>
99
- <span className="pmw-changes__subject">{revision.message.split('\n')[0]}</span>
100
- <span className="pmw-muted pmw-changes__meta">
101
- <PmBadge>{t.origins[revision.origin.kind] ?? revision.origin.kind}</PmBadge>
102
- {revision.author.replace(/\s*<[^>]*>$/, '')} · {new Date(revision.date).toLocaleString(locale, { dateStyle: 'short', timeStyle: 'short' })}
103
- </span>
104
- {revision.revertedBy
105
- ? <span className="pmw-muted">{t.reverted}</span>
106
- : <PmButton small icon="undo" onClick={() => void workbench.revert(revision.id)}>{t.revert}</PmButton>}
107
- </li>
108
- ))}
109
- </ol>
110
- </section>
111
- <PmDialog open={discarding} label={t.discard} closeLabel={t.close} onClose={() => setDiscarding(false)}>
112
- <p>{t.confirmDiscard}</p>
113
- <div className="pmw-operation__actions">
114
- <PmButton onClick={() => setDiscarding(false)}>{t.cancel}</PmButton>
115
- <PmButton
116
- variant="danger"
117
- onClick={() => {
118
- setDiscarding(false)
119
- void workbench.discardChanges()
120
- }}
121
- >
122
- {t.discard}
123
- </PmButton>
124
- </div>
125
- </PmDialog>
126
- </section>
127
- )
128
- }
@@ -1,73 +0,0 @@
1
- import { Tree } from 'react-arborist'
2
- import type { NodeRendererProps } from 'react-arborist'
3
- import { useMemo } from 'react'
4
- import type { SourceFile } from '../api.ts'
5
- import { PmIcon, PmIconButton, PmNotice } from '../components.tsx'
6
- import { useMessages, useWorkbench, useWorkbenchStore } from '../react.tsx'
7
-
8
- interface FileNode {
9
- id: string
10
- name: string
11
- file?: SourceFile
12
- children?: FileNode[]
13
- }
14
-
15
- function fileTree(files: SourceFile[], document: string): FileNode[] {
16
- const root: FileNode = { id: '', name: '', children: [] }
17
- for (const file of files) {
18
- const parts = (document && file.path.startsWith(`${document}/`) ? file.path.slice(document.length + 1) : file.path).split('/')
19
- let parent = root
20
- for (const [i, name] of parts.entries()) {
21
- const leaf = i === parts.length - 1
22
- const id = leaf ? file.path : `directory:${parts.slice(0, i + 1).join('/')}`
23
- let child = parent.children!.find(node => node.id === id)
24
- if (!child) {
25
- child = { id, name, ...leaf ? { file } : { children: [] } }
26
- parent.children!.push(child)
27
- }
28
- parent = child
29
- }
30
- }
31
- return root.children!
32
- }
33
-
34
- function FileRow({ node, style }: NodeRendererProps<FileNode>) {
35
- return (
36
- <button type="button" tabIndex={-1} style={style} className="pmw-tree__file" data-path={node.data.file?.path} data-active={node.isSelected || undefined} disabled={node.data.file?.binary} onClick={() => node.isInternal ? node.toggle() : node.activate()}>
37
- <PmIcon name={node.isInternal ? 'folder' : 'document'} size={15} />{node.data.name}
38
- </button>
39
- )
40
- }
41
-
42
- export function FilesPanel({ onOpen }: { onOpen?(path: string): void }) {
43
- const t = useMessages()
44
- const workbench = useWorkbenchStore()
45
- const document = useWorkbench(state => state.document)
46
- const files = useWorkbench(state => state.files)
47
- const active = useWorkbench(state => state.active)
48
- const documents = useWorkbench(state => state.documents)
49
- const failure = useWorkbench(state => state.openFailure)
50
- const data = useMemo(() => fileTree(files?.files ?? [], files?.home ? '' : document ?? ''), [files, document])
51
- if (document === undefined) return null
52
- const title = files?.home ? documents?.title ?? t.workspace : documents?.documents.find(doc => doc.dir === document)?.title ?? (document || documents?.title || t.workspace)
53
- return (
54
- <nav className="pmw-files" aria-label={t.filesOf(title)}>
55
- <div className="pmw-files__head">
56
- <PmIconButton icon="arrow-left" label={t.backToDocuments} small onClick={() => workbench.closeDocument()} />
57
- <h2 className="pmw-files__title">{title}</h2>
58
- </div>
59
- {failure && <PmNotice tone="danger">{t.openFailed(failure)}</PmNotice>}
60
- {files && files.files.length === 0 && <p className="pmw-muted">{t.noFiles}</p>}
61
- {files && (
62
- <Tree<FileNode> className="pmw-tree" data={data} selection={active} width="100%" height={Math.min(480, Math.max(96, (files.files.length + data.length) * 32))} rowHeight={32} indent={16} openByDefault disableDrag disableDrop disableEdit disableMultiSelection disableSelect={node => Boolean(node.file?.binary)} aria-label={t.files} onActivate={node => {
63
- if (!node.data.file || node.data.file.binary) return
64
- void workbench.openFile(node.data.file.path)
65
- onOpen?.(node.data.file.path)
66
- }}>
67
- {FileRow}
68
- </Tree>
69
- )}
70
- {files?.truncated && <p className="pmw-muted">{t.truncated}</p>}
71
- </nav>
72
- )
73
- }