@bendyline/docblocks-react 1.1.1 → 2.0.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.
- package/README.md +50 -15
- package/dist/AppMenu/AppMenu.d.ts +29 -2
- package/dist/AppMenu/AppMenu.d.ts.map +1 -1
- package/dist/AppMenu/AppMenu.js +41 -2
- package/dist/AppMenu/AppMenu.js.map +1 -1
- package/dist/AppMenu/index.d.ts +1 -1
- package/dist/AppMenu/index.d.ts.map +1 -1
- package/dist/DocBlocksShell/DocBlocksShell.d.ts +17 -4
- package/dist/DocBlocksShell/DocBlocksShell.d.ts.map +1 -1
- package/dist/DocBlocksShell/DocBlocksShell.js +1710 -299
- package/dist/DocBlocksShell/DocBlocksShell.js.map +1 -1
- package/dist/DocBlocksShell/LazyEditorShell.d.ts +3 -0
- package/dist/DocBlocksShell/LazyEditorShell.d.ts.map +1 -0
- package/dist/DocBlocksShell/LazyEditorShell.js +3 -0
- package/dist/DocBlocksShell/LazyEditorShell.js.map +1 -0
- package/dist/DocBlocksShell/UpdateAvailableNotice.d.ts +13 -0
- package/dist/DocBlocksShell/UpdateAvailableNotice.d.ts.map +1 -0
- package/dist/DocBlocksShell/UpdateAvailableNotice.js +16 -0
- package/dist/DocBlocksShell/UpdateAvailableNotice.js.map +1 -0
- package/dist/DocBlocksShell/document-title.d.ts +3 -0
- package/dist/DocBlocksShell/document-title.d.ts.map +1 -0
- package/dist/DocBlocksShell/document-title.js +17 -0
- package/dist/DocBlocksShell/document-title.js.map +1 -0
- package/dist/DocBlocksShell/workspace-authority-barrier.d.ts +14 -0
- package/dist/DocBlocksShell/workspace-authority-barrier.d.ts.map +1 -0
- package/dist/DocBlocksShell/workspace-authority-barrier.js +23 -0
- package/dist/DocBlocksShell/workspace-authority-barrier.js.map +1 -0
- package/dist/Export/DeferredExportToolbarControls.d.ts +5 -0
- package/dist/Export/DeferredExportToolbarControls.d.ts.map +1 -0
- package/dist/Export/DeferredExportToolbarControls.js +10 -0
- package/dist/Export/DeferredExportToolbarControls.js.map +1 -0
- package/dist/Export/ExportDialog.d.ts +10 -1
- package/dist/Export/ExportDialog.d.ts.map +1 -1
- package/dist/Export/ExportDialog.js +39 -20
- package/dist/Export/ExportDialog.js.map +1 -1
- package/dist/Export/ExportToolbarControls.d.ts +21 -1
- package/dist/Export/ExportToolbarControls.d.ts.map +1 -1
- package/dist/Export/ExportToolbarControls.js +161 -26
- package/dist/Export/ExportToolbarControls.js.map +1 -1
- package/dist/Export/export-destination.d.ts +3 -0
- package/dist/Export/export-destination.d.ts.map +1 -0
- package/dist/Export/export-destination.js +32 -0
- package/dist/Export/export-destination.js.map +1 -0
- package/dist/Export/index.d.ts +8 -4
- package/dist/Export/index.d.ts.map +1 -1
- package/dist/Export/index.js +4 -1
- package/dist/Export/index.js.map +1 -1
- package/dist/Export/public-api.d.ts +12 -0
- package/dist/Export/public-api.d.ts.map +1 -0
- package/dist/Export/public-api.js +9 -0
- package/dist/Export/public-api.js.map +1 -0
- package/dist/Export/run-export.d.ts +3 -1
- package/dist/Export/run-export.d.ts.map +1 -1
- package/dist/Export/run-export.js +58 -25
- package/dist/Export/run-export.js.map +1 -1
- package/dist/Export/transform-summaries.d.ts +7 -0
- package/dist/Export/transform-summaries.d.ts.map +1 -0
- package/dist/Export/transform-summaries.js +6 -0
- package/dist/Export/transform-summaries.js.map +1 -0
- package/dist/ExportDialog-MEUISWZX.js +9 -0
- package/dist/ExportDialog-MEUISWZX.js.map +1 -0
- package/dist/ExportToolbarControls-D2YDWOXW.js +373 -0
- package/dist/ExportToolbarControls-D2YDWOXW.js.map +1 -0
- package/dist/FileExplorer/FileExplorer.d.ts +26 -4
- package/dist/FileExplorer/FileExplorer.d.ts.map +1 -1
- package/dist/FileExplorer/FileExplorer.js +204 -29
- package/dist/FileExplorer/FileExplorer.js.map +1 -1
- package/dist/FileExplorer/FileTreeNode.d.ts +24 -3
- package/dist/FileExplorer/FileTreeNode.d.ts.map +1 -1
- package/dist/FileExplorer/FileTreeNode.js +27 -8
- package/dist/FileExplorer/FileTreeNode.js.map +1 -1
- package/dist/FileExplorer/index.d.ts +1 -1
- package/dist/FileExplorer/index.d.ts.map +1 -1
- package/dist/FileExplorer/useFileTree.d.ts +5 -3
- package/dist/FileExplorer/useFileTree.d.ts.map +1 -1
- package/dist/FileExplorer/useFileTree.js +173 -13
- package/dist/FileExplorer/useFileTree.js.map +1 -1
- package/dist/Git/GitAuthGuidanceDialog.d.ts +18 -0
- package/dist/Git/GitAuthGuidanceDialog.d.ts.map +1 -0
- package/dist/Git/GitAuthGuidanceDialog.js +27 -0
- package/dist/Git/GitAuthGuidanceDialog.js.map +1 -0
- package/dist/Git/GitBranchesDialog.d.ts +13 -0
- package/dist/Git/GitBranchesDialog.d.ts.map +1 -0
- package/dist/Git/GitBranchesDialog.js +59 -0
- package/dist/Git/GitBranchesDialog.js.map +1 -0
- package/dist/Git/GitCloneDialog.d.ts +14 -0
- package/dist/Git/GitCloneDialog.d.ts.map +1 -0
- package/dist/Git/GitCloneDialog.js +79 -0
- package/dist/Git/GitCloneDialog.js.map +1 -0
- package/dist/Git/GitCommitDialog.d.ts +12 -0
- package/dist/Git/GitCommitDialog.d.ts.map +1 -0
- package/dist/Git/GitCommitDialog.js +38 -0
- package/dist/Git/GitCommitDialog.js.map +1 -0
- package/dist/Git/GitConflictsDialog.d.ts +12 -0
- package/dist/Git/GitConflictsDialog.d.ts.map +1 -0
- package/dist/Git/GitConflictsDialog.js +17 -0
- package/dist/Git/GitConflictsDialog.js.map +1 -0
- package/dist/Git/GitContext.d.ts +89 -0
- package/dist/Git/GitContext.d.ts.map +1 -0
- package/dist/Git/GitContext.js +15 -0
- package/dist/Git/GitContext.js.map +1 -0
- package/dist/Git/GitDiffDialog.d.ts +20 -0
- package/dist/Git/GitDiffDialog.d.ts.map +1 -0
- package/dist/Git/GitDiffDialog.js +100 -0
- package/dist/Git/GitDiffDialog.js.map +1 -0
- package/dist/Git/GitDiffView.d.ts +23 -0
- package/dist/Git/GitDiffView.d.ts.map +1 -0
- package/dist/Git/GitDiffView.js +77 -0
- package/dist/Git/GitDiffView.js.map +1 -0
- package/dist/Git/GitHistoryDialog.d.ts +15 -0
- package/dist/Git/GitHistoryDialog.d.ts.map +1 -0
- package/dist/Git/GitHistoryDialog.js +107 -0
- package/dist/Git/GitHistoryDialog.js.map +1 -0
- package/dist/Git/GitStatusBar.d.ts +8 -0
- package/dist/Git/GitStatusBar.d.ts.map +1 -0
- package/dist/Git/GitStatusBar.js +49 -0
- package/dist/Git/GitStatusBar.js.map +1 -0
- package/dist/Git/GitToolbarControl.d.ts +15 -0
- package/dist/Git/GitToolbarControl.d.ts.map +1 -0
- package/dist/Git/GitToolbarControl.js +51 -0
- package/dist/Git/GitToolbarControl.js.map +1 -0
- package/dist/Git/GitUI.d.ts +14 -0
- package/dist/Git/GitUI.d.ts.map +1 -0
- package/dist/Git/GitUI.js +21 -0
- package/dist/Git/GitUI.js.map +1 -0
- package/dist/Git/commit-selection.d.ts +22 -0
- package/dist/Git/commit-selection.d.ts.map +1 -0
- package/dist/Git/commit-selection.js +47 -0
- package/dist/Git/commit-selection.js.map +1 -0
- package/dist/Git/git-clone-utils.d.ts +8 -0
- package/dist/Git/git-clone-utils.d.ts.map +1 -0
- package/dist/Git/git-clone-utils.js +26 -0
- package/dist/Git/git-clone-utils.js.map +1 -0
- package/dist/Git/git-status.d.ts +24 -0
- package/dist/Git/git-status.d.ts.map +1 -0
- package/dist/Git/git-status.js +105 -0
- package/dist/Git/git-status.js.map +1 -0
- package/dist/Git/useGit.d.ts +15 -0
- package/dist/Git/useGit.d.ts.map +1 -0
- package/dist/Git/useGit.js +206 -0
- package/dist/Git/useGit.js.map +1 -0
- package/dist/Git/useGitStatus.d.ts +17 -0
- package/dist/Git/useGitStatus.d.ts.map +1 -0
- package/dist/Git/useGitStatus.js +63 -0
- package/dist/Git/useGitStatus.js.map +1 -0
- package/dist/GitToolbarControl-LMIR4R7I.js +117 -0
- package/dist/GitToolbarControl-LMIR4R7I.js.map +1 -0
- package/dist/GitUI-KGF5MSNV.js +1048 -0
- package/dist/GitUI-KGF5MSNV.js.map +1 -0
- package/dist/LazyEditorShell-EF2BWRXU.js +6 -0
- package/dist/LazyEditorShell-EF2BWRXU.js.map +1 -0
- package/dist/Settings/Settings.d.ts +23 -0
- package/dist/Settings/Settings.d.ts.map +1 -0
- package/dist/Settings/Settings.js +22 -0
- package/dist/Settings/Settings.js.map +1 -0
- package/dist/Settings/public-api.d.ts +3 -0
- package/dist/Settings/public-api.d.ts.map +1 -0
- package/dist/Settings/public-api.js +2 -0
- package/dist/Settings/public-api.js.map +1 -0
- package/dist/WorkspacePicker/WorkspacePicker.d.ts +6 -1
- package/dist/WorkspacePicker/WorkspacePicker.d.ts.map +1 -1
- package/dist/WorkspacePicker/WorkspacePicker.js +16 -5
- package/dist/WorkspacePicker/WorkspacePicker.js.map +1 -1
- package/dist/chunk-3SAXDU2U.js +157 -0
- package/dist/chunk-3SAXDU2U.js.map +1 -0
- package/dist/chunk-4XXSWN3T.js +13 -0
- package/dist/chunk-4XXSWN3T.js.map +1 -0
- package/dist/chunk-5A4KEWKT.js +97 -0
- package/dist/chunk-5A4KEWKT.js.map +1 -0
- package/dist/chunk-AOVTHZU7.js +321 -0
- package/dist/chunk-AOVTHZU7.js.map +1 -0
- package/dist/chunk-DD4VFJQI.js +51 -0
- package/dist/chunk-DD4VFJQI.js.map +1 -0
- package/dist/chunk-GSNZSCIK.js +55 -0
- package/dist/chunk-GSNZSCIK.js.map +1 -0
- package/dist/chunk-VHDP4IPL.js +70 -0
- package/dist/chunk-VHDP4IPL.js.map +1 -0
- package/dist/chunk-ZI56BNB7.js +270 -0
- package/dist/chunk-ZI56BNB7.js.map +1 -0
- package/dist/components/Dialog.d.ts +22 -0
- package/dist/components/Dialog.d.ts.map +1 -0
- package/dist/components/Dialog.js +33 -0
- package/dist/components/Dialog.js.map +1 -0
- package/dist/editor.d.ts +10 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +70 -0
- package/dist/editor.js.map +1 -0
- package/dist/export/index.d.ts +87 -0
- package/dist/export/index.js +26 -0
- package/dist/export/index.js.map +1 -0
- package/dist/hooks/index.d.ts +2 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useDocumentSession.d.ts +16 -0
- package/dist/hooks/useDocumentSession.d.ts.map +1 -0
- package/dist/hooks/useDocumentSession.js +18 -0
- package/dist/hooks/useDocumentSession.js.map +1 -0
- package/dist/icons.d.ts +11 -6
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +21 -20
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/preferences/theme.d.ts +18 -0
- package/dist/preferences/theme.d.ts.map +1 -0
- package/dist/preferences/theme.js +63 -0
- package/dist/preferences/theme.js.map +1 -0
- package/dist/provider-lease.d.ts +10 -0
- package/dist/provider-lease.d.ts.map +1 -0
- package/dist/provider-lease.js +42 -0
- package/dist/provider-lease.js.map +1 -0
- package/dist/run-export-ICTZITXC.js +10 -0
- package/dist/run-export-ICTZITXC.js.map +1 -0
- package/dist/settings/index.d.ts +28 -0
- package/dist/settings/index.js +12 -0
- package/dist/settings/index.js.map +1 -0
- package/package.json +27 -10
- package/src/AppMenu/AppMenu.tsx +169 -99
- package/src/AppMenu/index.ts +1 -1
- package/src/DocBlocksShell/DocBlocksShell.tsx +2183 -516
- package/src/DocBlocksShell/LazyEditorShell.ts +2 -0
- package/src/DocBlocksShell/UpdateAvailableNotice.tsx +60 -0
- package/src/DocBlocksShell/document-title.ts +19 -0
- package/src/DocBlocksShell/workspace-authority-barrier.ts +26 -0
- package/src/Export/DeferredExportToolbarControls.tsx +23 -0
- package/src/Export/ExportDialog.tsx +102 -27
- package/src/Export/ExportToolbarControls.tsx +343 -57
- package/src/Export/export-destination.ts +30 -0
- package/src/Export/index.ts +18 -4
- package/src/Export/public-api.ts +17 -0
- package/src/Export/run-export.ts +66 -29
- package/src/Export/transform-summaries.ts +14 -0
- package/src/FileExplorer/FileExplorer.tsx +284 -38
- package/src/FileExplorer/FileTreeNode.tsx +100 -8
- package/src/FileExplorer/index.ts +1 -1
- package/src/FileExplorer/useFileTree.ts +190 -16
- package/src/Git/GitAuthGuidanceDialog.tsx +84 -0
- package/src/Git/GitBranchesDialog.tsx +144 -0
- package/src/Git/GitCloneDialog.tsx +200 -0
- package/src/Git/GitCommitDialog.tsx +119 -0
- package/src/Git/GitConflictsDialog.tsx +62 -0
- package/src/Git/GitContext.tsx +88 -0
- package/src/Git/GitDiffDialog.tsx +147 -0
- package/src/Git/GitDiffView.tsx +105 -0
- package/src/Git/GitHistoryDialog.tsx +195 -0
- package/src/Git/GitStatusBar.tsx +157 -0
- package/src/Git/GitToolbarControl.tsx +123 -0
- package/src/Git/GitUI.tsx +68 -0
- package/src/Git/commit-selection.ts +63 -0
- package/src/Git/git-clone-utils.ts +23 -0
- package/src/Git/git-status.ts +113 -0
- package/src/Git/useGit.ts +279 -0
- package/src/Git/useGitStatus.ts +78 -0
- package/src/Settings/Settings.tsx +118 -0
- package/src/Settings/public-api.ts +8 -0
- package/src/WorkspacePicker/WorkspacePicker.tsx +62 -8
- package/src/components/Dialog.tsx +81 -0
- package/src/editor.ts +72 -0
- package/src/hooks/index.ts +2 -1
- package/src/hooks/useDocumentSession.ts +37 -0
- package/src/icons.tsx +33 -55
- package/src/index.ts +35 -5
- package/src/preferences/theme.ts +67 -0
- package/src/provider-lease.ts +52 -0
- package/src/styles/docblocks.css +2247 -77
- package/src/types/pwa-dom.d.ts +31 -0
- package/dist/hooks/useAutoSave.d.ts +0 -15
- package/dist/hooks/useAutoSave.d.ts.map +0 -1
- package/dist/hooks/useAutoSave.js +0 -83
- package/dist/hooks/useAutoSave.js.map +0 -1
- package/src/fonts/fonts.css +0 -362
- package/src/fonts/licenses/LICENSE-cormorant-garamond.txt +0 -93
- package/src/fonts/licenses/LICENSE-crimson-text.txt +0 -93
- package/src/fonts/licenses/LICENSE-dm-sans.txt +0 -93
- package/src/fonts/licenses/LICENSE-dm-serif-display.txt +0 -93
- package/src/fonts/licenses/LICENSE-hanken-grotesk.txt +0 -94
- package/src/fonts/licenses/LICENSE-ibm-plex-sans.txt +0 -93
- package/src/fonts/licenses/LICENSE-inter.txt +0 -93
- package/src/fonts/licenses/LICENSE-jetbrains-mono.txt +0 -93
- package/src/fonts/licenses/LICENSE-lora.txt +0 -93
- package/src/fonts/licenses/LICENSE-merriweather.txt +0 -93
- package/src/fonts/licenses/LICENSE-oswald.txt +0 -93
- package/src/fonts/licenses/LICENSE-playfair-display.txt +0 -93
- package/src/fonts/licenses/LICENSE-pt-serif.txt +0 -93
- package/src/fonts/licenses/LICENSE-roboto.txt +0 -93
- package/src/fonts/licenses/LICENSE-source-serif-4.txt +0 -93
- package/src/fonts/woff2/cormorant-garamond-400.woff2 +0 -0
- package/src/fonts/woff2/cormorant-garamond-700.woff2 +0 -0
- package/src/fonts/woff2/crimson-text-400.woff2 +0 -0
- package/src/fonts/woff2/crimson-text-400i.woff2 +0 -0
- package/src/fonts/woff2/crimson-text-700.woff2 +0 -0
- package/src/fonts/woff2/dm-sans-400.woff2 +0 -0
- package/src/fonts/woff2/dm-sans-400i.woff2 +0 -0
- package/src/fonts/woff2/dm-sans-500.woff2 +0 -0
- package/src/fonts/woff2/dm-sans-700.woff2 +0 -0
- package/src/fonts/woff2/dm-serif-display-400.woff2 +0 -0
- package/src/fonts/woff2/hanken-grotesk-400.woff2 +0 -0
- package/src/fonts/woff2/hanken-grotesk-400i.woff2 +0 -0
- package/src/fonts/woff2/hanken-grotesk-500.woff2 +0 -0
- package/src/fonts/woff2/hanken-grotesk-600.woff2 +0 -0
- package/src/fonts/woff2/hanken-grotesk-700.woff2 +0 -0
- package/src/fonts/woff2/ibm-plex-sans-400.woff2 +0 -0
- package/src/fonts/woff2/ibm-plex-sans-400i.woff2 +0 -0
- package/src/fonts/woff2/ibm-plex-sans-500.woff2 +0 -0
- package/src/fonts/woff2/ibm-plex-sans-600.woff2 +0 -0
- package/src/fonts/woff2/ibm-plex-sans-700.woff2 +0 -0
- package/src/fonts/woff2/inter-400.woff2 +0 -0
- package/src/fonts/woff2/inter-400i.woff2 +0 -0
- package/src/fonts/woff2/inter-500.woff2 +0 -0
- package/src/fonts/woff2/inter-600.woff2 +0 -0
- package/src/fonts/woff2/inter-700.woff2 +0 -0
- package/src/fonts/woff2/jetbrains-mono-400.woff2 +0 -0
- package/src/fonts/woff2/jetbrains-mono-400i.woff2 +0 -0
- package/src/fonts/woff2/jetbrains-mono-700.woff2 +0 -0
- package/src/fonts/woff2/lora-400.woff2 +0 -0
- package/src/fonts/woff2/lora-400i.woff2 +0 -0
- package/src/fonts/woff2/lora-700.woff2 +0 -0
- package/src/fonts/woff2/merriweather-400.woff2 +0 -0
- package/src/fonts/woff2/merriweather-700.woff2 +0 -0
- package/src/fonts/woff2/oswald-400.woff2 +0 -0
- package/src/fonts/woff2/oswald-700.woff2 +0 -0
- package/src/fonts/woff2/playfair-display-400.woff2 +0 -0
- package/src/fonts/woff2/playfair-display-700.woff2 +0 -0
- package/src/fonts/woff2/pt-serif-400.woff2 +0 -0
- package/src/fonts/woff2/pt-serif-700.woff2 +0 -0
- package/src/fonts/woff2/roboto-400.woff2 +0 -0
- package/src/fonts/woff2/roboto-400i.woff2 +0 -0
- package/src/fonts/woff2/roboto-700.woff2 +0 -0
- package/src/fonts/woff2/source-serif-4-400.woff2 +0 -0
- package/src/fonts/woff2/source-serif-4-400i.woff2 +0 -0
- package/src/fonts/woff2/source-serif-4-700.woff2 +0 -0
- package/src/hooks/useAutoSave.ts +0 -116
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit-dialog selection state — pure helpers, unit-testable.
|
|
3
|
+
*
|
|
4
|
+
* All changed files are included by default. While a merge is being
|
|
5
|
+
* concluded, conflicted files are force-included: committing a subset that
|
|
6
|
+
* omits them would not resolve the merge.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { GitFileChange, GitStatus } from '@bendyline/docblocks/host';
|
|
10
|
+
|
|
11
|
+
export interface CommitSelection {
|
|
12
|
+
/** Slash-prefixed path → include in the commit. */
|
|
13
|
+
included: Map<string, boolean>;
|
|
14
|
+
/** Paths the user cannot exclude (conflicted files during a merge). */
|
|
15
|
+
locked: Set<string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalise(path: string): string {
|
|
19
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function initSelection(
|
|
23
|
+
changes: readonly GitFileChange[],
|
|
24
|
+
merging: boolean,
|
|
25
|
+
): CommitSelection {
|
|
26
|
+
const included = new Map<string, boolean>();
|
|
27
|
+
const locked = new Set<string>();
|
|
28
|
+
for (const change of changes) {
|
|
29
|
+
const path = normalise(change.path);
|
|
30
|
+
included.set(path, true);
|
|
31
|
+
if (merging && change.conflicted) locked.add(path);
|
|
32
|
+
}
|
|
33
|
+
return { included, locked };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function toggleSelection(selection: CommitSelection, path: string): CommitSelection {
|
|
37
|
+
const key = normalise(path);
|
|
38
|
+
if (selection.locked.has(key)) return selection;
|
|
39
|
+
const included = new Map(selection.included);
|
|
40
|
+
included.set(key, !included.get(key));
|
|
41
|
+
return { included, locked: selection.locked };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function setAllSelected(selection: CommitSelection, value: boolean): CommitSelection {
|
|
45
|
+
const included = new Map<string, boolean>();
|
|
46
|
+
for (const key of selection.included.keys()) {
|
|
47
|
+
included.set(key, selection.locked.has(key) ? true : value);
|
|
48
|
+
}
|
|
49
|
+
return { included, locked: selection.locked };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function selectedPaths(selection: CommitSelection): string[] {
|
|
53
|
+
return [...selection.included.entries()].filter(([, on]) => on).map(([path]) => path);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function canCommit(message: string, selection: CommitSelection): boolean {
|
|
57
|
+
return message.trim().length > 0 && selectedPaths(selection).length > 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** True when committing concludes an in-progress merge. */
|
|
61
|
+
export function isMergeCommit(status: GitStatus | null): boolean {
|
|
62
|
+
return status?.operation === 'merge';
|
|
63
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clone-dialog helpers — pure URL utilities, unit-testable.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Folder name a clone of this URL will produce (mirrors the host's rule). */
|
|
6
|
+
export function deriveCloneFolderName(url: string): string {
|
|
7
|
+
const trimmed = url.trim().replace(/\/+$/, '');
|
|
8
|
+
const lastSegment = trimmed.split(/[/:\\]/).pop() ?? '';
|
|
9
|
+
const withoutGit = lastSegment.replace(/\.git$/i, '');
|
|
10
|
+
const sanitized = withoutGit.replace(/[^A-Za-z0-9._-]/g, '-');
|
|
11
|
+
return sanitized.length > 0 ? sanitized : 'repository';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Loose shape check for enabling the Clone button — the host re-validates. */
|
|
15
|
+
export function isLikelyGitUrl(url: string): boolean {
|
|
16
|
+
const trimmed = url.trim();
|
|
17
|
+
if (trimmed.length === 0 || /\s/.test(trimmed)) return false;
|
|
18
|
+
if (trimmed.startsWith('-') || trimmed.toLowerCase().startsWith('file:')) return false;
|
|
19
|
+
if (/^(https?|ssh|git):\/\/\S+\/\S+/i.test(trimmed)) return true;
|
|
20
|
+
// scp-like: user@host:owner/repo
|
|
21
|
+
if (/^[\w.-]+@[\w.-]+:\S+/.test(trimmed)) return true;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers mapping GitStatus onto UI concerns — file-tree badges,
|
|
3
|
+
* ahead/behind chips, dirty summaries. No React, no host access.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { GitFileChange, GitStatus } from '@bendyline/docblocks/host';
|
|
7
|
+
|
|
8
|
+
export type GitBadgeKind =
|
|
9
|
+
| 'modified'
|
|
10
|
+
| 'added'
|
|
11
|
+
| 'untracked'
|
|
12
|
+
| 'renamed'
|
|
13
|
+
| 'deleted'
|
|
14
|
+
| 'conflicted'
|
|
15
|
+
| 'dir';
|
|
16
|
+
|
|
17
|
+
export const BADGE_GLYPHS: Record<GitBadgeKind, string> = {
|
|
18
|
+
modified: 'M',
|
|
19
|
+
added: 'A',
|
|
20
|
+
untracked: 'U',
|
|
21
|
+
renamed: 'R',
|
|
22
|
+
deleted: 'D',
|
|
23
|
+
conflicted: '!',
|
|
24
|
+
dir: '•',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const BADGE_LABELS: Record<GitBadgeKind, string> = {
|
|
28
|
+
modified: 'modified',
|
|
29
|
+
added: 'added',
|
|
30
|
+
untracked: 'untracked',
|
|
31
|
+
renamed: 'renamed',
|
|
32
|
+
deleted: 'deleted',
|
|
33
|
+
conflicted: 'has conflicts',
|
|
34
|
+
dir: 'contains changes',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function normalise(path: string): string {
|
|
38
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** True when any path segment is a dot-entry (.git, .github, .gitignore…). */
|
|
42
|
+
function isDotPath(path: string): boolean {
|
|
43
|
+
return normalise(path)
|
|
44
|
+
.split('/')
|
|
45
|
+
.some((segment) => segment.startsWith('.'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function badgeKindFor(change: GitFileChange): GitBadgeKind {
|
|
49
|
+
if (change.conflicted) return 'conflicted';
|
|
50
|
+
if (change.worktree === 'untracked') return 'untracked';
|
|
51
|
+
if (change.index === 'deleted' || change.worktree === 'deleted') return 'deleted';
|
|
52
|
+
if (change.index === 'renamed' || change.worktree === 'renamed') return 'renamed';
|
|
53
|
+
if (change.index === 'added' || change.worktree === 'added') return 'added';
|
|
54
|
+
return 'modified';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Slash-prefixed path → badge kind, including directory rollups: any
|
|
59
|
+
* ancestor directory of a changed file gets a 'dir' dot (or 'conflicted'
|
|
60
|
+
* when a conflicted descendant wins). Deleted files aren't in the tree, so
|
|
61
|
+
* they contribute only their ancestor rollup. Dot-paths are excluded so a
|
|
62
|
+
* `.gitignore` edit doesn't inexplicably dot the root.
|
|
63
|
+
*/
|
|
64
|
+
export function buildBadgeMap(changes: readonly GitFileChange[]): Map<string, GitBadgeKind> {
|
|
65
|
+
const badges = new Map<string, GitBadgeKind>();
|
|
66
|
+
const dirKinds = new Map<string, GitBadgeKind>();
|
|
67
|
+
|
|
68
|
+
for (const change of changes) {
|
|
69
|
+
if (isDotPath(change.path)) continue;
|
|
70
|
+
const filePath = normalise(change.path);
|
|
71
|
+
const kind = badgeKindFor(change);
|
|
72
|
+
if (kind !== 'deleted') {
|
|
73
|
+
badges.set(filePath, kind);
|
|
74
|
+
}
|
|
75
|
+
const segments = filePath.split('/').slice(1, -1);
|
|
76
|
+
let dir = '';
|
|
77
|
+
for (const segment of segments) {
|
|
78
|
+
dir += `/${segment}`;
|
|
79
|
+
const rollup = kind === 'conflicted' ? 'conflicted' : 'dir';
|
|
80
|
+
if (dirKinds.get(dir) !== 'conflicted') dirKinds.set(dir, rollup);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const [dir, kind] of dirKinds) {
|
|
84
|
+
if (!badges.has(dir)) badges.set(dir, kind);
|
|
85
|
+
}
|
|
86
|
+
return badges;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** "2↑ 1↓" — empty string when neither applies. */
|
|
90
|
+
export function formatAheadBehind(status: Pick<GitStatus, 'ahead' | 'behind'>): string {
|
|
91
|
+
const parts: string[] = [];
|
|
92
|
+
if (status.ahead > 0) parts.push(`${status.ahead}↑`);
|
|
93
|
+
if (status.behind > 0) parts.push(`${status.behind}↓`);
|
|
94
|
+
return parts.join(' ');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function isFileDirty(status: GitStatus | null, path: string | null): boolean {
|
|
98
|
+
if (!status || !path) return false;
|
|
99
|
+
const target = normalise(path);
|
|
100
|
+
return status.changes.some((c) => normalise(c.path) === target);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function conflictedPaths(status: GitStatus | null): string[] {
|
|
104
|
+
if (!status) return [];
|
|
105
|
+
return status.changes.filter((c) => c.conflicted).map((c) => normalise(c.path));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** "3 changes" / "1 change" / "" when clean. */
|
|
109
|
+
export function summarizeDirty(status: GitStatus | null): string {
|
|
110
|
+
const count = status?.changes.length ?? 0;
|
|
111
|
+
if (count === 0) return '';
|
|
112
|
+
return count === 1 ? '1 change' : `${count} changes`;
|
|
113
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGit — the single hook DocBlocksShell calls to power all git UI.
|
|
3
|
+
*
|
|
4
|
+
* Tier 1 (`available`): running under the Electron host with a working git —
|
|
5
|
+
* gates only the clone entry. Tier 2 (`repo`): the active workspace is an
|
|
6
|
+
* electron-native folder that is a git repository — gates everything else.
|
|
7
|
+
* On the site / VS Code webview / store builds every surface null-renders.
|
|
8
|
+
*
|
|
9
|
+
* Actions never throw: typed GitErrors are routed to the auth-guidance or
|
|
10
|
+
* conflicts dialogs when actionable, otherwise to the aria-live result line.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
14
|
+
import type {
|
|
15
|
+
DocBlocksHostGitAPI,
|
|
16
|
+
GitCapabilities,
|
|
17
|
+
GitError,
|
|
18
|
+
GitRepoDetection,
|
|
19
|
+
GitRemoteInfo,
|
|
20
|
+
} from '@bendyline/docblocks/host';
|
|
21
|
+
import { maybeGetDocBlocksHost } from '@bendyline/docblocks/host';
|
|
22
|
+
import type { FileSystemProvider } from '@bendyline/docblocks/filesystem';
|
|
23
|
+
import { buildBadgeMap, conflictedPaths } from './git-status.js';
|
|
24
|
+
import { useGitStatus } from './useGitStatus.js';
|
|
25
|
+
import type { GitBusyKind, GitDialogState, GitInlineResult, GitValue } from './GitContext.js';
|
|
26
|
+
|
|
27
|
+
/** Capabilities can't change within a session — probe once per page load. */
|
|
28
|
+
let capabilitiesPromise: Promise<GitCapabilities> | null = null;
|
|
29
|
+
function capabilitiesOnce(gitApi: DocBlocksHostGitAPI): Promise<GitCapabilities> {
|
|
30
|
+
capabilitiesPromise ??= gitApi.capabilities();
|
|
31
|
+
return capabilitiesPromise;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function describeError(error: GitError): string {
|
|
35
|
+
return error.message || 'Git operation failed';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useGit(
|
|
39
|
+
provider: FileSystemProvider | null,
|
|
40
|
+
requestedWorkspaceId: string | null,
|
|
41
|
+
theme: 'light' | 'dark',
|
|
42
|
+
): GitValue {
|
|
43
|
+
const host = maybeGetDocBlocksHost();
|
|
44
|
+
const gitApi = host?.git ?? null;
|
|
45
|
+
const workspaceId = gitApi ? requestedWorkspaceId : null;
|
|
46
|
+
|
|
47
|
+
const [capabilities, setCapabilities] = useState<GitCapabilities | null>(null);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!gitApi) return;
|
|
50
|
+
let cancelled = false;
|
|
51
|
+
void capabilitiesOnce(gitApi).then((caps) => {
|
|
52
|
+
if (!cancelled) setCapabilities(caps);
|
|
53
|
+
});
|
|
54
|
+
return () => {
|
|
55
|
+
cancelled = true;
|
|
56
|
+
};
|
|
57
|
+
}, [gitApi]);
|
|
58
|
+
|
|
59
|
+
const available = gitApi !== null && capabilities?.gitAvailable === true;
|
|
60
|
+
|
|
61
|
+
const [repo, setRepo] = useState<GitRepoDetection | null>(null);
|
|
62
|
+
const [remoteWeb, setRemoteWeb] = useState<GitValue['remoteWeb']>(null);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
setRepo(null);
|
|
65
|
+
setRemoteWeb(null);
|
|
66
|
+
if (!gitApi || !workspaceId || !available) return;
|
|
67
|
+
let cancelled = false;
|
|
68
|
+
void gitApi.detectRepo(workspaceId).then((result) => {
|
|
69
|
+
if (cancelled || !result.ok || !result.value.isRepo || !result.value.repositoryId) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
setRepo(result.value);
|
|
73
|
+
void gitApi.listRemotes(result.value.repositoryId).then((remotes) => {
|
|
74
|
+
if (cancelled || !remotes.ok) return;
|
|
75
|
+
const first: GitRemoteInfo | undefined = remotes.value[0];
|
|
76
|
+
setRemoteWeb(first?.web ?? null);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
return () => {
|
|
80
|
+
cancelled = true;
|
|
81
|
+
};
|
|
82
|
+
}, [gitApi, workspaceId, available]);
|
|
83
|
+
|
|
84
|
+
const repositoryId = repo?.repositoryId ?? null;
|
|
85
|
+
const isRepo = repositoryId !== null;
|
|
86
|
+
const { status, refresh, scheduleRefresh } = useGitStatus(gitApi, repositoryId, isRepo);
|
|
87
|
+
const badges = useMemo(() => buildBadgeMap(status?.changes ?? []), [status]);
|
|
88
|
+
|
|
89
|
+
const [busy, setBusy] = useState<GitBusyKind>(null);
|
|
90
|
+
const [lastResult, setLastResult] = useState<GitInlineResult | null>(null);
|
|
91
|
+
const [dialog, setDialog] = useState<GitDialogState>({ kind: 'none' });
|
|
92
|
+
|
|
93
|
+
const openDialog = useCallback((next: GitDialogState) => setDialog(next), []);
|
|
94
|
+
const closeDialog = useCallback(() => setDialog({ kind: 'none' }), []);
|
|
95
|
+
|
|
96
|
+
// Route a failed operation somewhere useful.
|
|
97
|
+
const routeError = useCallback(
|
|
98
|
+
(operation: 'push' | 'pull' | 'fetch' | 'commit' | 'branch' | 'pr', error: GitError) => {
|
|
99
|
+
if (error.code === 'auth-failed') {
|
|
100
|
+
setDialog({
|
|
101
|
+
kind: 'auth-guidance',
|
|
102
|
+
operation: operation === 'commit' || operation === 'branch' ? 'push' : operation,
|
|
103
|
+
detail: error.message,
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
setLastResult({ tone: 'error', message: describeError(error) });
|
|
108
|
+
},
|
|
109
|
+
[],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const statusRef = useRef(status);
|
|
113
|
+
statusRef.current = status;
|
|
114
|
+
|
|
115
|
+
const runAction = useCallback(
|
|
116
|
+
async <T>(
|
|
117
|
+
kind: Exclude<GitBusyKind, null>,
|
|
118
|
+
operation: 'push' | 'pull' | 'fetch' | 'commit' | 'branch' | 'pr',
|
|
119
|
+
action: (
|
|
120
|
+
api: DocBlocksHostGitAPI,
|
|
121
|
+
repository: string,
|
|
122
|
+
) => Promise<{ ok: true; value: T } | { ok: false; error: GitError }>,
|
|
123
|
+
successMessage?: string,
|
|
124
|
+
): Promise<boolean> => {
|
|
125
|
+
if (!gitApi || !repositoryId) return false;
|
|
126
|
+
setBusy(kind);
|
|
127
|
+
setLastResult(null);
|
|
128
|
+
try {
|
|
129
|
+
const result = await action(gitApi, repositoryId);
|
|
130
|
+
if (result.ok) {
|
|
131
|
+
if (successMessage) setLastResult({ tone: 'info', message: successMessage });
|
|
132
|
+
refresh();
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (operation === 'pull' && result.error.code === 'merge-conflict') {
|
|
136
|
+
// Surface the conflicted files; the fresh status carries them.
|
|
137
|
+
const fresh = await gitApi.status(repositoryId);
|
|
138
|
+
const paths = fresh.ok ? conflictedPaths(fresh.value) : [];
|
|
139
|
+
refresh();
|
|
140
|
+
setDialog({ kind: 'conflicts', paths });
|
|
141
|
+
setLastResult({ tone: 'error', message: 'Pull hit merge conflicts' });
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
routeError(operation, result.error);
|
|
145
|
+
refresh();
|
|
146
|
+
return false;
|
|
147
|
+
} finally {
|
|
148
|
+
setBusy(null);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
[gitApi, repositoryId, refresh, routeError],
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const commit = useCallback(
|
|
155
|
+
(message: string, paths: string[]) =>
|
|
156
|
+
runAction(
|
|
157
|
+
'commit',
|
|
158
|
+
'commit',
|
|
159
|
+
(api, repository) => api.commit(repository, message, paths),
|
|
160
|
+
'Committed',
|
|
161
|
+
),
|
|
162
|
+
[runAction],
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const push = useCallback(async () => {
|
|
166
|
+
const setUpstream = statusRef.current?.upstream === null;
|
|
167
|
+
await runAction(
|
|
168
|
+
'push',
|
|
169
|
+
'push',
|
|
170
|
+
(api, repository) => api.push(repository, setUpstream ? { setUpstream: true } : undefined),
|
|
171
|
+
setUpstream ? 'Branch published' : 'Pushed',
|
|
172
|
+
);
|
|
173
|
+
}, [runAction]);
|
|
174
|
+
|
|
175
|
+
const pull = useCallback(async () => {
|
|
176
|
+
await runAction('pull', 'pull', (api, repository) => api.pull(repository), 'Pulled');
|
|
177
|
+
}, [runAction]);
|
|
178
|
+
|
|
179
|
+
const fetchRemote = useCallback(async () => {
|
|
180
|
+
await runAction('fetch', 'fetch', (api, repository) => api.fetch(repository), 'Fetched');
|
|
181
|
+
}, [runAction]);
|
|
182
|
+
|
|
183
|
+
const createBranch = useCallback(
|
|
184
|
+
(name: string) =>
|
|
185
|
+
runAction(
|
|
186
|
+
'branch',
|
|
187
|
+
'branch',
|
|
188
|
+
(api, repository) => api.createBranch(repository, name, { checkout: true }),
|
|
189
|
+
`Switched to new branch ${name}`,
|
|
190
|
+
),
|
|
191
|
+
[runAction],
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const switchBranch = useCallback(
|
|
195
|
+
(name: string) =>
|
|
196
|
+
runAction(
|
|
197
|
+
'branch',
|
|
198
|
+
'branch',
|
|
199
|
+
(api, repository) => api.checkoutBranch(repository, name),
|
|
200
|
+
`Switched to ${name}`,
|
|
201
|
+
),
|
|
202
|
+
[runAction],
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const openOnRemote = useCallback(
|
|
206
|
+
(filePath?: string) => {
|
|
207
|
+
if (!remoteWeb || !host) return;
|
|
208
|
+
let url = remoteWeb.webUrl;
|
|
209
|
+
// Only GitHub's blob-path shape is a safe bet; other hosts get the repo.
|
|
210
|
+
if (filePath && remoteWeb.host === 'github.com' && statusRef.current?.branch) {
|
|
211
|
+
const rel = filePath.replace(/^\/+/, '');
|
|
212
|
+
url = `${remoteWeb.webUrl}/blob/${encodeURIComponent(statusRef.current.branch)}/${rel
|
|
213
|
+
.split('/')
|
|
214
|
+
.map(encodeURIComponent)
|
|
215
|
+
.join('/')}`;
|
|
216
|
+
}
|
|
217
|
+
void host.shell.openExternal(url);
|
|
218
|
+
},
|
|
219
|
+
[remoteWeb, host],
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const createPullRequest = useCallback(async () => {
|
|
223
|
+
if (!gitApi || !repositoryId) return;
|
|
224
|
+
setBusy('pr');
|
|
225
|
+
setLastResult(null);
|
|
226
|
+
try {
|
|
227
|
+
const result = await gitApi.createPullRequest(repositoryId);
|
|
228
|
+
if (result.ok) {
|
|
229
|
+
setLastResult({ tone: 'info', message: 'Opening pull request in browser' });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (result.error.code === 'no-upstream') {
|
|
233
|
+
setLastResult({
|
|
234
|
+
tone: 'error',
|
|
235
|
+
message: 'Push the branch first, then create the pull request',
|
|
236
|
+
});
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (result.error.code === 'gh-not-available') {
|
|
240
|
+
setLastResult({
|
|
241
|
+
tone: 'error',
|
|
242
|
+
message: 'Install the GitHub CLI (gh) to create pull requests',
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
routeError('pr', result.error);
|
|
247
|
+
} finally {
|
|
248
|
+
setBusy(null);
|
|
249
|
+
}
|
|
250
|
+
}, [gitApi, repositoryId, routeError]);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
available,
|
|
254
|
+
capabilities,
|
|
255
|
+
repo,
|
|
256
|
+
repositoryId,
|
|
257
|
+
remoteWeb,
|
|
258
|
+
gitApi,
|
|
259
|
+
provider,
|
|
260
|
+
theme,
|
|
261
|
+
status,
|
|
262
|
+
badges,
|
|
263
|
+
busy,
|
|
264
|
+
lastResult,
|
|
265
|
+
refresh,
|
|
266
|
+
scheduleRefresh,
|
|
267
|
+
commit,
|
|
268
|
+
push,
|
|
269
|
+
pull,
|
|
270
|
+
fetchRemote,
|
|
271
|
+
createBranch,
|
|
272
|
+
switchBranch,
|
|
273
|
+
openOnRemote,
|
|
274
|
+
createPullRequest,
|
|
275
|
+
dialog,
|
|
276
|
+
openDialog,
|
|
277
|
+
closeDialog,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGitStatus — subscribes to the host's owner-scoped repository stream.
|
|
3
|
+
*
|
|
4
|
+
* Dedupes identical payloads so a permanently-dirty working tree (normal
|
|
5
|
+
* under the manual-commit model) doesn't repaint the tree on every push
|
|
6
|
+
* from the main process.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
10
|
+
import type { DocBlocksHostGitAPI, GitStatus } from '@bendyline/docblocks/host';
|
|
11
|
+
|
|
12
|
+
const SCHEDULE_REFRESH_MS = 1500;
|
|
13
|
+
|
|
14
|
+
function statusKey(status: GitStatus): string {
|
|
15
|
+
return JSON.stringify(status);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UseGitStatusResult {
|
|
19
|
+
status: GitStatus | null;
|
|
20
|
+
/** Fetch a fresh status immediately (after a renderer-initiated git op). */
|
|
21
|
+
refresh: () => void;
|
|
22
|
+
/** Trailing-debounced refresh (after autosave writes). */
|
|
23
|
+
scheduleRefresh: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useGitStatus(
|
|
27
|
+
gitApi: DocBlocksHostGitAPI | null,
|
|
28
|
+
repositoryId: string | null,
|
|
29
|
+
enabled: boolean,
|
|
30
|
+
): UseGitStatusResult {
|
|
31
|
+
const [status, setStatus] = useState<GitStatus | null>(null);
|
|
32
|
+
const lastKeyRef = useRef<string | null>(null);
|
|
33
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
34
|
+
|
|
35
|
+
const apply = useCallback((next: GitStatus) => {
|
|
36
|
+
const key = statusKey(next);
|
|
37
|
+
if (key === lastKeyRef.current) return;
|
|
38
|
+
lastKeyRef.current = key;
|
|
39
|
+
setStatus(next);
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
lastKeyRef.current = null;
|
|
44
|
+
setStatus(null);
|
|
45
|
+
if (!gitApi || !repositoryId || !enabled) return;
|
|
46
|
+
let cancelled = false;
|
|
47
|
+
const unsubscribe = gitApi.onStatusChanged(repositoryId, (next) => {
|
|
48
|
+
if (!cancelled) apply(next);
|
|
49
|
+
});
|
|
50
|
+
return () => {
|
|
51
|
+
cancelled = true;
|
|
52
|
+
unsubscribe();
|
|
53
|
+
};
|
|
54
|
+
}, [gitApi, repositoryId, enabled, apply]);
|
|
55
|
+
|
|
56
|
+
const refresh = useCallback(() => {
|
|
57
|
+
if (!gitApi || !repositoryId || !enabled) return;
|
|
58
|
+
void gitApi.status(repositoryId).then((result) => {
|
|
59
|
+
if (result.ok) apply(result.value);
|
|
60
|
+
});
|
|
61
|
+
}, [gitApi, repositoryId, enabled, apply]);
|
|
62
|
+
|
|
63
|
+
const scheduleRefresh = useCallback(() => {
|
|
64
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
65
|
+
timerRef.current = setTimeout(() => {
|
|
66
|
+
timerRef.current = null;
|
|
67
|
+
refresh();
|
|
68
|
+
}, SCHEDULE_REFRESH_MS);
|
|
69
|
+
}, [refresh]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
return () => {
|
|
73
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
74
|
+
};
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
return { status, refresh, scheduleRefresh };
|
|
78
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** Shared controls used by the app and host-specific DocBlocks settings dialogs. */
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { Dialog } from '../components/Dialog.js';
|
|
5
|
+
import { ACCENT_COLORS, type AccentColor, type ThemePreference } from '../preferences/theme.js';
|
|
6
|
+
|
|
7
|
+
export type { AccentColor, ThemePreference } from '../preferences/theme.js';
|
|
8
|
+
|
|
9
|
+
const ACCENT_LABELS: Record<AccentColor, string> = {
|
|
10
|
+
brown: 'Brown',
|
|
11
|
+
green: 'Green',
|
|
12
|
+
blue: 'Blue',
|
|
13
|
+
purple: 'Purple',
|
|
14
|
+
maroon: 'Maroon',
|
|
15
|
+
orange: 'Orange',
|
|
16
|
+
gray: 'Gray',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface SettingsDialogProps {
|
|
20
|
+
title?: string;
|
|
21
|
+
onClose: () => void;
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function SettingsDialog({ title = 'Settings', onClose, children }: SettingsDialogProps) {
|
|
26
|
+
return (
|
|
27
|
+
<Dialog title={title} onClose={onClose}>
|
|
28
|
+
{children}
|
|
29
|
+
</Dialog>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ThemeSettingsProps {
|
|
34
|
+
value: ThemePreference;
|
|
35
|
+
onChange: (theme: ThemePreference) => void;
|
|
36
|
+
name?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ThemeSettings({ value, onChange, name = 'theme' }: ThemeSettingsProps) {
|
|
40
|
+
return (
|
|
41
|
+
<fieldset className="db-settings-fieldset">
|
|
42
|
+
<legend className="db-settings-legend">Theme</legend>
|
|
43
|
+
<label className="db-settings-radio">
|
|
44
|
+
<input
|
|
45
|
+
type="radio"
|
|
46
|
+
name={name}
|
|
47
|
+
value="auto"
|
|
48
|
+
checked={value === 'auto'}
|
|
49
|
+
onChange={() => onChange('auto')}
|
|
50
|
+
/>
|
|
51
|
+
System default
|
|
52
|
+
</label>
|
|
53
|
+
<label className="db-settings-radio">
|
|
54
|
+
<input
|
|
55
|
+
type="radio"
|
|
56
|
+
name={name}
|
|
57
|
+
value="light"
|
|
58
|
+
checked={value === 'light'}
|
|
59
|
+
onChange={() => onChange('light')}
|
|
60
|
+
/>
|
|
61
|
+
Light
|
|
62
|
+
</label>
|
|
63
|
+
<label className="db-settings-radio">
|
|
64
|
+
<input
|
|
65
|
+
type="radio"
|
|
66
|
+
name={name}
|
|
67
|
+
value="dark"
|
|
68
|
+
checked={value === 'dark'}
|
|
69
|
+
onChange={() => onChange('dark')}
|
|
70
|
+
/>
|
|
71
|
+
Dark
|
|
72
|
+
</label>
|
|
73
|
+
</fieldset>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface AccentColorSettingsProps {
|
|
78
|
+
value: AccentColor;
|
|
79
|
+
onChange: (color: AccentColor) => void;
|
|
80
|
+
name?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function AccentColorSettings({
|
|
84
|
+
value,
|
|
85
|
+
onChange,
|
|
86
|
+
name = 'accent-color',
|
|
87
|
+
}: AccentColorSettingsProps) {
|
|
88
|
+
return (
|
|
89
|
+
<fieldset className="db-settings-fieldset">
|
|
90
|
+
<legend className="db-settings-legend">Accent color</legend>
|
|
91
|
+
<p className="db-settings-hint">Used in both light and dark appearances.</p>
|
|
92
|
+
<div className="db-settings-accent-grid">
|
|
93
|
+
{ACCENT_COLORS.map((color) => (
|
|
94
|
+
<label
|
|
95
|
+
className={`db-settings-accent${
|
|
96
|
+
value === color ? ' db-settings-accent--selected' : ''
|
|
97
|
+
}`}
|
|
98
|
+
key={color}
|
|
99
|
+
>
|
|
100
|
+
<input
|
|
101
|
+
className="db-settings-accent-input"
|
|
102
|
+
type="radio"
|
|
103
|
+
name={name}
|
|
104
|
+
value={color}
|
|
105
|
+
checked={value === color}
|
|
106
|
+
onChange={() => onChange(color)}
|
|
107
|
+
/>
|
|
108
|
+
<span
|
|
109
|
+
className={`db-settings-accent-swatch db-settings-accent-swatch--${color}`}
|
|
110
|
+
aria-hidden="true"
|
|
111
|
+
/>
|
|
112
|
+
<span>{ACCENT_LABELS[color]}</span>
|
|
113
|
+
</label>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
</fieldset>
|
|
117
|
+
);
|
|
118
|
+
}
|