@workbench-kit/react 0.0.1-prototype.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/package.json +62 -0
- package/src/index.ts +43 -0
- package/src/layout/Panel.tsx +27 -0
- package/src/layout/SideBarViewFrame.tsx +328 -0
- package/src/modal/ConfirmDialog.tsx +53 -0
- package/src/modal/Modal.tsx +110 -0
- package/src/overlay/ContextMenu.tsx +145 -0
- package/src/primitives/Badge.tsx +12 -0
- package/src/primitives/Button.tsx +14 -0
- package/src/primitives/Checkbox.tsx +15 -0
- package/src/primitives/EmptyState.tsx +26 -0
- package/src/primitives/Field.tsx +38 -0
- package/src/primitives/IconButton.tsx +32 -0
- package/src/primitives/Select.tsx +12 -0
- package/src/primitives/TextInput.tsx +24 -0
- package/src/primitives/Toolbar.tsx +8 -0
- package/src/styles.css +1762 -0
- package/src/utils/cx.ts +3 -0
- package/src/workbench/ActivityBar.tsx +52 -0
- package/src/workbench/SplitView.tsx +136 -0
- package/src/workbench/StatusBar.tsx +123 -0
- package/src/workbench/WorkbenchShell.tsx +70 -0
- package/src/workbench/WorkbenchStandaloneShell.tsx +291 -0
- package/src/workbench/chat/ChatComposer.tsx +148 -0
- package/src/workbench/chat/ChatMessageItem.tsx +35 -0
- package/src/workbench/chat/ChatMessageList.tsx +49 -0
- package/src/workbench/chat/ChatPanel.tsx +55 -0
- package/src/workbench/chat/index.ts +9 -0
- package/src/workbench/chat/types.ts +10 -0
- package/src/workbench/commands.ts +490 -0
- package/src/workbench/index.ts +98 -0
- package/src/workbench/settings/WorkbenchSettingsModal.tsx +188 -0
- package/src/workbench/settings/WorkbenchSettingsNav.tsx +41 -0
- package/src/workbench/settings/WorkbenchSettingsSection.tsx +30 -0
- package/src/workbench/settings/index.ts +7 -0
- package/src/workbench/settings/types.ts +16 -0
- package/src/workbench/shellState.ts +203 -0
- package/src/workbench/standalone.ts +94 -0
- package/src/workbench/workspace/WorkspaceEditor.tsx +226 -0
- package/src/workbench/workspace/WorkspaceEditorPanel.tsx +353 -0
- package/src/workbench/workspace/WorkspaceExplorer.tsx +524 -0
- package/src/workbench/workspace/WorkspaceFileIcon.tsx +149 -0
- package/src/workbench/workspace/WorkspaceHighlightedText.tsx +22 -0
- package/src/workbench/workspace/WorkspaceSearchPanel.tsx +113 -0
- package/src/workbench/workspace/WorkspaceSearchResults.tsx +47 -0
- package/src/workbench/workspace/index.ts +98 -0
- package/src/workbench/workspace/path.ts +10 -0
- package/src/workbench/workspace/search.ts +6 -0
- package/src/workbench/workspace/tree.ts +1 -0
- package/src/workbench/workspace/types.ts +10 -0
- package/src/workbench/workspace/useVirtualWorkspace.ts +98 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { useId, useState, type FormEventHandler, type ReactNode } from 'react';
|
|
2
|
+
import { Modal } from '../../modal/Modal';
|
|
3
|
+
import type { ModalProps } from '../../modal/Modal';
|
|
4
|
+
import { TextInput } from '../../primitives/TextInput';
|
|
5
|
+
import { cx } from '../../utils/cx';
|
|
6
|
+
import { WorkbenchSettingsNav } from './WorkbenchSettingsNav';
|
|
7
|
+
import type { WorkbenchSettingsCategory, WorkbenchSettingsScope } from './types';
|
|
8
|
+
|
|
9
|
+
export interface WorkbenchSettingsModalProps extends Pick<
|
|
10
|
+
ModalProps,
|
|
11
|
+
'className' | 'closeLabel' | 'footer' | 'labelledBy' | 'onClose' | 'title' | 'titleSuffix'
|
|
12
|
+
> {
|
|
13
|
+
categories: WorkbenchSettingsCategory[];
|
|
14
|
+
activeCategoryId?: string;
|
|
15
|
+
activeScopeId?: string;
|
|
16
|
+
bodyClassName?: string;
|
|
17
|
+
defaultActiveCategoryId?: string;
|
|
18
|
+
defaultActiveScopeId?: string;
|
|
19
|
+
defaultSearchValue?: string;
|
|
20
|
+
emptyContent?: ReactNode;
|
|
21
|
+
onActiveCategoryIdChange?: (categoryId: string) => void;
|
|
22
|
+
onScopeChange?: (scopeId: string) => void;
|
|
23
|
+
onSearchValueChange?: (value: string) => void;
|
|
24
|
+
onSubmit?: FormEventHandler<HTMLFormElement>;
|
|
25
|
+
renderCategory?: (category: WorkbenchSettingsCategory) => ReactNode;
|
|
26
|
+
scopes?: WorkbenchSettingsScope[];
|
|
27
|
+
searchPlaceholder?: string;
|
|
28
|
+
searchValue?: string;
|
|
29
|
+
showSearch?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function firstEnabledCategory(categories: WorkbenchSettingsCategory[]) {
|
|
33
|
+
return categories.find((category) => !category.disabled) ?? categories[0];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveCategoryId(
|
|
37
|
+
categories: WorkbenchSettingsCategory[],
|
|
38
|
+
preferredCategoryId: string | undefined,
|
|
39
|
+
) {
|
|
40
|
+
const preferredCategory = categories.find(
|
|
41
|
+
(category) => category.id === preferredCategoryId && !category.disabled,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return preferredCategory?.id ?? firstEnabledCategory(categories)?.id ?? '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function firstEnabledScope(scopes: WorkbenchSettingsScope[] | undefined) {
|
|
48
|
+
return scopes?.find((scope) => !scope.disabled) ?? scopes?.[0];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function WorkbenchSettingsModal({
|
|
52
|
+
activeCategoryId,
|
|
53
|
+
activeScopeId,
|
|
54
|
+
bodyClassName,
|
|
55
|
+
categories,
|
|
56
|
+
className,
|
|
57
|
+
closeLabel = 'Close settings',
|
|
58
|
+
defaultActiveCategoryId,
|
|
59
|
+
defaultActiveScopeId,
|
|
60
|
+
defaultSearchValue = '',
|
|
61
|
+
emptyContent = null,
|
|
62
|
+
footer,
|
|
63
|
+
labelledBy,
|
|
64
|
+
onActiveCategoryIdChange,
|
|
65
|
+
onClose,
|
|
66
|
+
onScopeChange,
|
|
67
|
+
onSearchValueChange,
|
|
68
|
+
onSubmit,
|
|
69
|
+
renderCategory,
|
|
70
|
+
scopes,
|
|
71
|
+
searchPlaceholder = 'Search settings',
|
|
72
|
+
searchValue,
|
|
73
|
+
showSearch = true,
|
|
74
|
+
title,
|
|
75
|
+
titleSuffix,
|
|
76
|
+
}: WorkbenchSettingsModalProps) {
|
|
77
|
+
const generatedId = useId().replace(/:/g, '');
|
|
78
|
+
const titleId = labelledBy ?? `${generatedId}-settings-title`;
|
|
79
|
+
const [uncontrolledCategoryId, setUncontrolledCategoryId] = useState(
|
|
80
|
+
() => defaultActiveCategoryId ?? firstEnabledCategory(categories)?.id ?? '',
|
|
81
|
+
);
|
|
82
|
+
const [uncontrolledScopeId, setUncontrolledScopeId] = useState(
|
|
83
|
+
() => defaultActiveScopeId ?? firstEnabledScope(scopes)?.id ?? '',
|
|
84
|
+
);
|
|
85
|
+
const [uncontrolledSearchValue, setUncontrolledSearchValue] = useState(defaultSearchValue);
|
|
86
|
+
const selectedCategoryId = resolveCategoryId(
|
|
87
|
+
categories,
|
|
88
|
+
activeCategoryId ?? uncontrolledCategoryId,
|
|
89
|
+
);
|
|
90
|
+
const selectedCategory = categories.find((category) => category.id === selectedCategoryId);
|
|
91
|
+
const selectedScopeId =
|
|
92
|
+
activeScopeId ?? uncontrolledScopeId ?? firstEnabledScope(scopes)?.id ?? '';
|
|
93
|
+
const resolvedSearchValue = searchValue ?? uncontrolledSearchValue;
|
|
94
|
+
|
|
95
|
+
const handleSelectCategory = (categoryId: string) => {
|
|
96
|
+
const category = categories.find((candidate) => candidate.id === categoryId);
|
|
97
|
+
if (!category || category.disabled) return;
|
|
98
|
+
|
|
99
|
+
if (activeCategoryId === undefined) {
|
|
100
|
+
setUncontrolledCategoryId(categoryId);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
onActiveCategoryIdChange?.(categoryId);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleSelectScope = (scopeId: string) => {
|
|
107
|
+
const scope = scopes?.find((candidate) => candidate.id === scopeId);
|
|
108
|
+
if (!scope || scope.disabled) return;
|
|
109
|
+
|
|
110
|
+
if (activeScopeId === undefined) {
|
|
111
|
+
setUncontrolledScopeId(scopeId);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
onScopeChange?.(scopeId);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const handleSearchChange = (value: string) => {
|
|
118
|
+
if (searchValue === undefined) {
|
|
119
|
+
setUncontrolledSearchValue(value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
onSearchValueChange?.(value);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Modal
|
|
127
|
+
className={cx('workbench-settings-modal', className)}
|
|
128
|
+
bodyClassName={cx('workbench-settings-modal__body', bodyClassName)}
|
|
129
|
+
closeLabel={closeLabel}
|
|
130
|
+
footer={footer}
|
|
131
|
+
labelledBy={titleId}
|
|
132
|
+
title={title}
|
|
133
|
+
titleSuffix={titleSuffix}
|
|
134
|
+
onClose={onClose}
|
|
135
|
+
onSubmit={onSubmit}
|
|
136
|
+
>
|
|
137
|
+
{showSearch ? (
|
|
138
|
+
<div className="workbench-settings-search">
|
|
139
|
+
<TextInput
|
|
140
|
+
aria-label={searchPlaceholder}
|
|
141
|
+
controlWidth="full"
|
|
142
|
+
placeholder={searchPlaceholder}
|
|
143
|
+
value={resolvedSearchValue}
|
|
144
|
+
onChange={(event) => handleSearchChange(event.currentTarget.value)}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
) : null}
|
|
148
|
+
|
|
149
|
+
{scopes?.length ? (
|
|
150
|
+
<div className="workbench-settings-tabs" aria-label="Settings scope">
|
|
151
|
+
{scopes.map((scope) => {
|
|
152
|
+
const isActive = scope.id === selectedScopeId;
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<button
|
|
156
|
+
key={scope.id}
|
|
157
|
+
type="button"
|
|
158
|
+
className={cx(
|
|
159
|
+
'workbench-settings-tab',
|
|
160
|
+
isActive && 'workbench-settings-tab--active',
|
|
161
|
+
)}
|
|
162
|
+
disabled={scope.disabled}
|
|
163
|
+
title={scope.title}
|
|
164
|
+
onClick={() => handleSelectScope(scope.id)}
|
|
165
|
+
>
|
|
166
|
+
{scope.label}
|
|
167
|
+
</button>
|
|
168
|
+
);
|
|
169
|
+
})}
|
|
170
|
+
</div>
|
|
171
|
+
) : null}
|
|
172
|
+
|
|
173
|
+
<div className="workbench-settings-layout">
|
|
174
|
+
<WorkbenchSettingsNav
|
|
175
|
+
activeCategoryId={selectedCategoryId}
|
|
176
|
+
categories={categories}
|
|
177
|
+
onSelectCategory={handleSelectCategory}
|
|
178
|
+
/>
|
|
179
|
+
|
|
180
|
+
<main className="workbench-settings-content">
|
|
181
|
+
{selectedCategory
|
|
182
|
+
? (renderCategory?.(selectedCategory) ?? selectedCategory.content ?? emptyContent)
|
|
183
|
+
: emptyContent}
|
|
184
|
+
</main>
|
|
185
|
+
</div>
|
|
186
|
+
</Modal>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { cx } from '../../utils/cx';
|
|
2
|
+
import type { WorkbenchSettingsCategory } from './types';
|
|
3
|
+
|
|
4
|
+
export interface WorkbenchSettingsNavProps {
|
|
5
|
+
activeCategoryId: string;
|
|
6
|
+
categories: WorkbenchSettingsCategory[];
|
|
7
|
+
ariaLabel?: string;
|
|
8
|
+
onSelectCategory: (categoryId: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function WorkbenchSettingsNav({
|
|
12
|
+
activeCategoryId,
|
|
13
|
+
ariaLabel = 'Settings categories',
|
|
14
|
+
categories,
|
|
15
|
+
onSelectCategory,
|
|
16
|
+
}: WorkbenchSettingsNavProps) {
|
|
17
|
+
return (
|
|
18
|
+
<aside className="workbench-settings-sidebar" aria-label={ariaLabel}>
|
|
19
|
+
{categories.map((category) => {
|
|
20
|
+
const isActive = category.id === activeCategoryId;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<button
|
|
24
|
+
key={category.id}
|
|
25
|
+
type="button"
|
|
26
|
+
className={cx(
|
|
27
|
+
'workbench-settings-nav-item',
|
|
28
|
+
isActive && 'workbench-settings-nav-item--active',
|
|
29
|
+
)}
|
|
30
|
+
aria-current={isActive ? 'page' : undefined}
|
|
31
|
+
disabled={category.disabled}
|
|
32
|
+
title={category.title}
|
|
33
|
+
onClick={() => onSelectCategory(category.id)}
|
|
34
|
+
>
|
|
35
|
+
{category.label}
|
|
36
|
+
</button>
|
|
37
|
+
);
|
|
38
|
+
})}
|
|
39
|
+
</aside>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { cx } from '../../utils/cx';
|
|
3
|
+
|
|
4
|
+
export interface WorkbenchSettingsSectionProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
id: string;
|
|
7
|
+
title: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
description?: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function WorkbenchSettingsSection({
|
|
13
|
+
children,
|
|
14
|
+
className,
|
|
15
|
+
description,
|
|
16
|
+
id,
|
|
17
|
+
title,
|
|
18
|
+
}: WorkbenchSettingsSectionProps) {
|
|
19
|
+
return (
|
|
20
|
+
<section className={cx('workbench-settings-section', className)} aria-labelledby={id}>
|
|
21
|
+
<h2 id={id} className="workbench-settings-section__title">
|
|
22
|
+
{title}
|
|
23
|
+
</h2>
|
|
24
|
+
{description ? (
|
|
25
|
+
<p className="workbench-settings-section__description">{description}</p>
|
|
26
|
+
) : null}
|
|
27
|
+
{children}
|
|
28
|
+
</section>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { WorkbenchSettingsModal } from './WorkbenchSettingsModal';
|
|
2
|
+
export type { WorkbenchSettingsModalProps } from './WorkbenchSettingsModal';
|
|
3
|
+
export { WorkbenchSettingsNav } from './WorkbenchSettingsNav';
|
|
4
|
+
export type { WorkbenchSettingsNavProps } from './WorkbenchSettingsNav';
|
|
5
|
+
export { WorkbenchSettingsSection } from './WorkbenchSettingsSection';
|
|
6
|
+
export type { WorkbenchSettingsSectionProps } from './WorkbenchSettingsSection';
|
|
7
|
+
export type { WorkbenchSettingsCategory, WorkbenchSettingsScope } from './types';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface WorkbenchSettingsCategory {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
content?: ReactNode;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
title?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WorkbenchSettingsScope {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
title?: string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { useReducer } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface WorkbenchShellInitialState<
|
|
4
|
+
TActivityId extends string = string,
|
|
5
|
+
TTheme extends string = string,
|
|
6
|
+
> {
|
|
7
|
+
activeActivityId: TActivityId;
|
|
8
|
+
isPrimarySidebarVisible?: boolean;
|
|
9
|
+
isSettingsOpen?: boolean;
|
|
10
|
+
primarySidebarSizePercent?: number;
|
|
11
|
+
settingsCategoryId?: string;
|
|
12
|
+
settingsScopeId?: string;
|
|
13
|
+
settingsSearchValue?: string;
|
|
14
|
+
theme: TTheme;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WorkbenchShellState<
|
|
18
|
+
TActivityId extends string = string,
|
|
19
|
+
TTheme extends string = string,
|
|
20
|
+
> {
|
|
21
|
+
activeActivityId: TActivityId;
|
|
22
|
+
isPrimarySidebarVisible: boolean;
|
|
23
|
+
isSettingsOpen: boolean;
|
|
24
|
+
primarySidebarSizePercent: number;
|
|
25
|
+
settingsCategoryId: string;
|
|
26
|
+
settingsScopeId: string;
|
|
27
|
+
settingsSearchValue: string;
|
|
28
|
+
theme: TTheme;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type WorkbenchShellAction<
|
|
32
|
+
TActivityId extends string = string,
|
|
33
|
+
TTheme extends string = string,
|
|
34
|
+
> =
|
|
35
|
+
| { activityId: TActivityId; type: 'activate-activity' }
|
|
36
|
+
| { activityId: TActivityId; type: 'show-activity' }
|
|
37
|
+
| { isOpen: boolean; type: 'set-settings-open' }
|
|
38
|
+
| { percent: number; type: 'set-primary-sidebar-size' }
|
|
39
|
+
| { settingsCategoryId: string; type: 'set-settings-category' }
|
|
40
|
+
| { settingsScopeId: string; type: 'set-settings-scope' }
|
|
41
|
+
| { settingsSearchValue: string; type: 'set-settings-search' }
|
|
42
|
+
| { theme: TTheme; type: 'set-theme' }
|
|
43
|
+
| { type: 'close-settings' }
|
|
44
|
+
| { type: 'open-settings' }
|
|
45
|
+
| { type: 'toggle-primary-sidebar' }
|
|
46
|
+
| { isVisible: boolean; type: 'set-primary-sidebar-visible' };
|
|
47
|
+
|
|
48
|
+
export interface UseWorkbenchShellStateResult<
|
|
49
|
+
TActivityId extends string = string,
|
|
50
|
+
TTheme extends string = string,
|
|
51
|
+
> {
|
|
52
|
+
activateActivity: (activityId: TActivityId) => void;
|
|
53
|
+
closeSettings: () => void;
|
|
54
|
+
dispatch: (action: WorkbenchShellAction<TActivityId, TTheme>) => void;
|
|
55
|
+
openSettings: () => void;
|
|
56
|
+
setPrimarySidebarSizePercent: (percent: number) => void;
|
|
57
|
+
setPrimarySidebarVisible: (isVisible: boolean) => void;
|
|
58
|
+
setSettingsCategoryId: (settingsCategoryId: string) => void;
|
|
59
|
+
setSettingsOpen: (isOpen: boolean) => void;
|
|
60
|
+
setSettingsScopeId: (settingsScopeId: string) => void;
|
|
61
|
+
setSettingsSearchValue: (settingsSearchValue: string) => void;
|
|
62
|
+
setTheme: (theme: TTheme) => void;
|
|
63
|
+
showActivity: (activityId: TActivityId) => void;
|
|
64
|
+
state: WorkbenchShellState<TActivityId, TTheme>;
|
|
65
|
+
togglePrimarySidebar: () => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const DEFAULT_PRIMARY_SIDEBAR_SIZE_PERCENT = 24;
|
|
69
|
+
const MIN_PRIMARY_SIDEBAR_SIZE_PERCENT = 10;
|
|
70
|
+
const MAX_PRIMARY_SIDEBAR_SIZE_PERCENT = 90;
|
|
71
|
+
|
|
72
|
+
function clampPrimarySidebarSizePercent(percent: number, fallback: number) {
|
|
73
|
+
if (!Number.isFinite(percent)) return fallback;
|
|
74
|
+
return Math.min(
|
|
75
|
+
MAX_PRIMARY_SIDEBAR_SIZE_PERCENT,
|
|
76
|
+
Math.max(MIN_PRIMARY_SIDEBAR_SIZE_PERCENT, percent),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function initializeWorkbenchShellState<TActivityId extends string, TTheme extends string>({
|
|
81
|
+
activeActivityId,
|
|
82
|
+
isPrimarySidebarVisible = true,
|
|
83
|
+
isSettingsOpen = false,
|
|
84
|
+
primarySidebarSizePercent = DEFAULT_PRIMARY_SIDEBAR_SIZE_PERCENT,
|
|
85
|
+
settingsCategoryId = '',
|
|
86
|
+
settingsScopeId = '',
|
|
87
|
+
settingsSearchValue = '',
|
|
88
|
+
theme,
|
|
89
|
+
}: WorkbenchShellInitialState<TActivityId, TTheme>): WorkbenchShellState<TActivityId, TTheme> {
|
|
90
|
+
return {
|
|
91
|
+
activeActivityId,
|
|
92
|
+
isPrimarySidebarVisible,
|
|
93
|
+
isSettingsOpen,
|
|
94
|
+
primarySidebarSizePercent: clampPrimarySidebarSizePercent(
|
|
95
|
+
primarySidebarSizePercent,
|
|
96
|
+
DEFAULT_PRIMARY_SIDEBAR_SIZE_PERCENT,
|
|
97
|
+
),
|
|
98
|
+
settingsCategoryId,
|
|
99
|
+
settingsScopeId,
|
|
100
|
+
settingsSearchValue,
|
|
101
|
+
theme,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function workbenchShellStateReducer<TActivityId extends string, TTheme extends string>(
|
|
106
|
+
state: WorkbenchShellState<TActivityId, TTheme>,
|
|
107
|
+
action: WorkbenchShellAction<TActivityId, TTheme>,
|
|
108
|
+
): WorkbenchShellState<TActivityId, TTheme> {
|
|
109
|
+
if (action.type === 'activate-activity') {
|
|
110
|
+
const shouldHideSidebar =
|
|
111
|
+
action.activityId === state.activeActivityId && state.isPrimarySidebarVisible;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
...state,
|
|
115
|
+
activeActivityId: action.activityId,
|
|
116
|
+
isPrimarySidebarVisible: !shouldHideSidebar,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (action.type === 'show-activity') {
|
|
121
|
+
return {
|
|
122
|
+
...state,
|
|
123
|
+
activeActivityId: action.activityId,
|
|
124
|
+
isPrimarySidebarVisible: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (action.type === 'set-primary-sidebar-visible') {
|
|
129
|
+
return { ...state, isPrimarySidebarVisible: action.isVisible };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (action.type === 'toggle-primary-sidebar') {
|
|
133
|
+
return { ...state, isPrimarySidebarVisible: !state.isPrimarySidebarVisible };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (action.type === 'set-primary-sidebar-size') {
|
|
137
|
+
return {
|
|
138
|
+
...state,
|
|
139
|
+
primarySidebarSizePercent: clampPrimarySidebarSizePercent(
|
|
140
|
+
action.percent,
|
|
141
|
+
state.primarySidebarSizePercent,
|
|
142
|
+
),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (action.type === 'set-theme') {
|
|
147
|
+
return { ...state, theme: action.theme };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (action.type === 'open-settings') {
|
|
151
|
+
return { ...state, isSettingsOpen: true };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (action.type === 'close-settings') {
|
|
155
|
+
return { ...state, isSettingsOpen: false };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (action.type === 'set-settings-open') {
|
|
159
|
+
return { ...state, isSettingsOpen: action.isOpen };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (action.type === 'set-settings-category') {
|
|
163
|
+
return { ...state, settingsCategoryId: action.settingsCategoryId };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (action.type === 'set-settings-scope') {
|
|
167
|
+
return { ...state, settingsScopeId: action.settingsScopeId };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { ...state, settingsSearchValue: action.settingsSearchValue };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function useWorkbenchShellState<TActivityId extends string, TTheme extends string>(
|
|
174
|
+
initialState: WorkbenchShellInitialState<TActivityId, TTheme>,
|
|
175
|
+
): UseWorkbenchShellStateResult<TActivityId, TTheme> {
|
|
176
|
+
const [state, dispatch] = useReducer(
|
|
177
|
+
workbenchShellStateReducer<TActivityId, TTheme>,
|
|
178
|
+
initialState,
|
|
179
|
+
initializeWorkbenchShellState,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
activateActivity: (activityId) => dispatch({ activityId, type: 'activate-activity' }),
|
|
184
|
+
closeSettings: () => dispatch({ type: 'close-settings' }),
|
|
185
|
+
dispatch,
|
|
186
|
+
openSettings: () => dispatch({ type: 'open-settings' }),
|
|
187
|
+
setPrimarySidebarSizePercent: (percent) =>
|
|
188
|
+
dispatch({ percent, type: 'set-primary-sidebar-size' }),
|
|
189
|
+
setPrimarySidebarVisible: (isVisible) =>
|
|
190
|
+
dispatch({ isVisible, type: 'set-primary-sidebar-visible' }),
|
|
191
|
+
setSettingsCategoryId: (settingsCategoryId) =>
|
|
192
|
+
dispatch({ settingsCategoryId, type: 'set-settings-category' }),
|
|
193
|
+
setSettingsOpen: (isOpen) => dispatch({ isOpen, type: 'set-settings-open' }),
|
|
194
|
+
setSettingsScopeId: (settingsScopeId) =>
|
|
195
|
+
dispatch({ settingsScopeId, type: 'set-settings-scope' }),
|
|
196
|
+
setSettingsSearchValue: (settingsSearchValue) =>
|
|
197
|
+
dispatch({ settingsSearchValue, type: 'set-settings-search' }),
|
|
198
|
+
setTheme: (theme) => dispatch({ theme, type: 'set-theme' }),
|
|
199
|
+
showActivity: (activityId) => dispatch({ activityId, type: 'show-activity' }),
|
|
200
|
+
state,
|
|
201
|
+
togglePrimarySidebar: () => dispatch({ type: 'toggle-primary-sidebar' }),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { CommandRegistry } from '@workbench-kit/core';
|
|
2
|
+
import type {
|
|
3
|
+
SaveResult,
|
|
4
|
+
WorkspacePatchApplyResult,
|
|
5
|
+
WorkspacePatchEvent,
|
|
6
|
+
} from '@workbench-kit/contracts';
|
|
7
|
+
import type { ReactNode } from 'react';
|
|
8
|
+
import type { StatusBarSectionModel } from './StatusBar';
|
|
9
|
+
import type { WorkbenchShellCommandContext } from './commands';
|
|
10
|
+
import type { WorkspaceFile } from './workspace';
|
|
11
|
+
|
|
12
|
+
export type WorkbenchTheme = 'light' | 'dark' | (string & {});
|
|
13
|
+
|
|
14
|
+
export interface WorkbenchActivityDescriptor<TActivityId extends string = string> {
|
|
15
|
+
id: TActivityId;
|
|
16
|
+
label: string;
|
|
17
|
+
icon?: string;
|
|
18
|
+
iconNode?: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WorkbenchActivityChangeEvent<TActivityId extends string = string> {
|
|
22
|
+
nextActivityId: TActivityId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface WorkbenchWorkspaceController {
|
|
26
|
+
openFile: (path: string) => void | Promise<void>;
|
|
27
|
+
saveFile: (path: string, content: string, previousUpdatedAt?: string) => Promise<SaveResult>;
|
|
28
|
+
deleteFiles: (paths: string[]) => void | Promise<void>;
|
|
29
|
+
moveFiles?: (paths: string[], targetParentPath: string | undefined) => void | Promise<void>;
|
|
30
|
+
rename?: (path: string, nextName: string) => void | Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface WorkbenchChatController {
|
|
34
|
+
onChatSubmit: (message: string, context?: Record<string, unknown>) => void | Promise<void>;
|
|
35
|
+
onCancelChat: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface WorkbenchPatchController {
|
|
39
|
+
onPatchApply: (patch: WorkspacePatchEvent) => Promise<WorkspacePatchApplyResult> | void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface WorkbenchSaveController {
|
|
43
|
+
onSaveResult?: (result: SaveResult) => void | Promise<void>;
|
|
44
|
+
onCommitPatchResult?: (
|
|
45
|
+
patch: WorkspacePatchEvent,
|
|
46
|
+
result: WorkspacePatchApplyResult,
|
|
47
|
+
source: 'chat' | 'command',
|
|
48
|
+
) => void | Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface WorkbenchStatusController {
|
|
52
|
+
onStatusMessage?: (message: string) => void;
|
|
53
|
+
onError?: (error: Error) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface WorkbenchShellContract<TActivityId extends string = string> {
|
|
57
|
+
activities: WorkbenchActivityDescriptor<TActivityId>[];
|
|
58
|
+
commandRegistry: CommandRegistry<WorkbenchShellCommandContext<TActivityId>>;
|
|
59
|
+
initialActivityId?: TActivityId;
|
|
60
|
+
initialTheme?: WorkbenchTheme;
|
|
61
|
+
statusSections: StatusBarSectionModel[];
|
|
62
|
+
statusCompact?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface WorkbenchStandaloneEntryState<TActivityId extends string = string> {
|
|
66
|
+
activeActivityId: TActivityId;
|
|
67
|
+
isPrimarySidebarVisible: boolean;
|
|
68
|
+
primarySidebarSizePercent: number;
|
|
69
|
+
primarySidebarMinPercent?: number;
|
|
70
|
+
primarySidebarMaxPercent?: number;
|
|
71
|
+
theme: WorkbenchTheme;
|
|
72
|
+
isSettingsOpen?: boolean;
|
|
73
|
+
settingsCategoryId?: string;
|
|
74
|
+
settingsScopeId?: string;
|
|
75
|
+
settingsSearchValue?: string;
|
|
76
|
+
selectedFilePath?: string;
|
|
77
|
+
openFilePaths?: readonly string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface WorkbenchStandaloneBootstrap<TActivityId extends string = string> {
|
|
81
|
+
contract: WorkbenchShellContract<TActivityId>;
|
|
82
|
+
initialFiles: readonly WorkspaceFile[];
|
|
83
|
+
workspace: WorkbenchWorkspaceController;
|
|
84
|
+
chat: WorkbenchChatController;
|
|
85
|
+
patch: WorkbenchPatchController;
|
|
86
|
+
save: WorkbenchSaveController;
|
|
87
|
+
status: WorkbenchStatusController;
|
|
88
|
+
initialState?: Partial<WorkbenchStandaloneEntryState<TActivityId>>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type WorkbenchStandaloneBootstrapEvent<TActivityId extends string = string> =
|
|
92
|
+
| { type: 'activity-change'; payload: WorkbenchActivityChangeEvent<TActivityId> }
|
|
93
|
+
| { type: 'status-message'; message: string }
|
|
94
|
+
| { type: 'error'; error: Error };
|