@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
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@workbench-kit/react",
|
|
3
|
+
"version": "0.0.1-prototype.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./workbench/chat": "./src/workbench/chat/index.ts",
|
|
9
|
+
"./workbench/settings": "./src/workbench/settings/index.ts",
|
|
10
|
+
"./workbench": "./src/workbench/index.ts",
|
|
11
|
+
"./workbench/workspace": "./src/workbench/workspace/index.ts",
|
|
12
|
+
"./styles.css": "./src/styles.css"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"!src/**/*.test.ts",
|
|
17
|
+
"!src/**/*.test.tsx",
|
|
18
|
+
"!src/**/*.stories.ts",
|
|
19
|
+
"!src/**/*.stories.tsx"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@vscode/codicons": "^0.0.45",
|
|
23
|
+
"@workbench-kit/core": "0.0.1-prototype.0",
|
|
24
|
+
"@workbench-kit/runtime": "0.0.1-prototype.0",
|
|
25
|
+
"@workbench-kit/adapters": "0.0.1-prototype.0",
|
|
26
|
+
"@workbench-kit/workspace": "0.0.1-prototype.0",
|
|
27
|
+
"@workbench-kit/tokens": "0.0.1-prototype.0",
|
|
28
|
+
"@workbench-kit/services": "0.0.1-prototype.0",
|
|
29
|
+
"@workbench-kit/contracts": "0.0.1-prototype.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@monaco-editor/react": "^4.7.0",
|
|
33
|
+
"monaco-editor": "^0.55.1",
|
|
34
|
+
"react-markdown": "^10.1.0",
|
|
35
|
+
"react": "^19.0.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"@monaco-editor/react": {
|
|
39
|
+
"optional": true
|
|
40
|
+
},
|
|
41
|
+
"monaco-editor": {
|
|
42
|
+
"optional": true
|
|
43
|
+
},
|
|
44
|
+
"react-markdown": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"description": "React primitives and workbench components for Workbench Kit.",
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public",
|
|
51
|
+
"tag": "prototype",
|
|
52
|
+
"provenance": true
|
|
53
|
+
},
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "git+https://github.com/NewChoBo/newchobo-ui-package.git",
|
|
57
|
+
"directory": "packages/react"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export { Modal } from './modal/Modal';
|
|
2
|
+
export type { ModalProps } from './modal/Modal';
|
|
3
|
+
export { commandMenuItemsToContextMenuItems } from './workbench/commands';
|
|
4
|
+
export { ConfirmDialog } from './modal/ConfirmDialog';
|
|
5
|
+
export type { ConfirmDialogProps } from './modal/ConfirmDialog';
|
|
6
|
+
export { ContextMenu } from './overlay/ContextMenu';
|
|
7
|
+
export type { ContextMenuItem, ContextMenuProps } from './overlay/ContextMenu';
|
|
8
|
+
export { Panel, PanelBody, PanelHeader } from './layout/Panel';
|
|
9
|
+
export type { PanelBodyProps, PanelHeaderProps, PanelProps } from './layout/Panel';
|
|
10
|
+
export {
|
|
11
|
+
SideBarHeaderControl,
|
|
12
|
+
SideBarList,
|
|
13
|
+
SideBarListItem,
|
|
14
|
+
SideBarViewFrame,
|
|
15
|
+
SideBarRow,
|
|
16
|
+
SideBarScrollSpacer,
|
|
17
|
+
} from './layout/SideBarViewFrame';
|
|
18
|
+
export type {
|
|
19
|
+
SideBarHeaderControlProps,
|
|
20
|
+
SideBarListItemProps,
|
|
21
|
+
SideBarListProps,
|
|
22
|
+
SideBarViewFrameProps,
|
|
23
|
+
SideBarRowProps,
|
|
24
|
+
SideBarScrollSpacerProps,
|
|
25
|
+
} from './layout/SideBarViewFrame';
|
|
26
|
+
export { Badge } from './primitives/Badge';
|
|
27
|
+
export type { BadgeProps } from './primitives/Badge';
|
|
28
|
+
export { Button } from './primitives/Button';
|
|
29
|
+
export type { ButtonProps } from './primitives/Button';
|
|
30
|
+
export { Checkbox } from './primitives/Checkbox';
|
|
31
|
+
export type { CheckboxProps } from './primitives/Checkbox';
|
|
32
|
+
export { EmptyState } from './primitives/EmptyState';
|
|
33
|
+
export type { EmptyStateProps } from './primitives/EmptyState';
|
|
34
|
+
export { Field } from './primitives/Field';
|
|
35
|
+
export type { FieldProps } from './primitives/Field';
|
|
36
|
+
export { IconButton } from './primitives/IconButton';
|
|
37
|
+
export type { IconButtonProps } from './primitives/IconButton';
|
|
38
|
+
export { Select } from './primitives/Select';
|
|
39
|
+
export type { SelectProps } from './primitives/Select';
|
|
40
|
+
export { TextInput } from './primitives/TextInput';
|
|
41
|
+
export type { TextInputProps } from './primitives/TextInput';
|
|
42
|
+
export { Toolbar } from './primitives/Toolbar';
|
|
43
|
+
export type { ToolbarProps } from './primitives/Toolbar';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef, ReactNode } from 'react';
|
|
2
|
+
import { cx } from '../utils/cx';
|
|
3
|
+
|
|
4
|
+
export type PanelProps = ComponentPropsWithRef<'div'>;
|
|
5
|
+
|
|
6
|
+
export function Panel({ className, ...props }: PanelProps) {
|
|
7
|
+
return <div className={cx('ide-panel', className)} {...props} />;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PanelHeaderProps extends ComponentPropsWithRef<'div'> {
|
|
11
|
+
actions?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function PanelHeader({ actions, children, className, ...props }: PanelHeaderProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div className={cx('panel-header', className)} {...props}>
|
|
17
|
+
<span className="ui-panel-header__title">{children}</span>
|
|
18
|
+
{actions ? <span className="ui-panel-header__actions">{actions}</span> : null}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type PanelBodyProps = ComponentPropsWithRef<'div'>;
|
|
24
|
+
|
|
25
|
+
export function PanelBody({ className, ...props }: PanelBodyProps) {
|
|
26
|
+
return <div className={cx('panel-body', className)} {...props} />;
|
|
27
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef, CSSProperties, PointerEvent, ReactNode } from 'react';
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { cx } from '../utils/cx';
|
|
4
|
+
import { Panel, PanelBody, PanelHeader, type PanelBodyProps, type PanelProps } from './Panel';
|
|
5
|
+
|
|
6
|
+
type SideBarFooterPlacement = 'static' | 'overlay';
|
|
7
|
+
|
|
8
|
+
export interface SideBarViewFrameProps extends Omit<PanelProps, 'children' | 'title'> {
|
|
9
|
+
actions?: ReactNode;
|
|
10
|
+
bodyClassName?: string;
|
|
11
|
+
bodyProps?: PanelBodyProps;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
footer?: ReactNode;
|
|
14
|
+
footerPlacement?: SideBarFooterPlacement;
|
|
15
|
+
headerAddon?: ReactNode;
|
|
16
|
+
title: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function SideBarViewFrame({
|
|
20
|
+
actions,
|
|
21
|
+
bodyClassName,
|
|
22
|
+
bodyProps,
|
|
23
|
+
children,
|
|
24
|
+
className,
|
|
25
|
+
footer,
|
|
26
|
+
footerPlacement = 'static',
|
|
27
|
+
headerAddon,
|
|
28
|
+
style,
|
|
29
|
+
title,
|
|
30
|
+
...props
|
|
31
|
+
}: SideBarViewFrameProps) {
|
|
32
|
+
const { className: bodyPropsClassName, ...resolvedBodyProps } = bodyProps ?? {};
|
|
33
|
+
const hasFooter = Boolean(footer);
|
|
34
|
+
const bodyRef = useRef<HTMLDivElement>(null);
|
|
35
|
+
const footerRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
const [scrollbar, setScrollbar] = useState({ needed: false, top: 0, height: 0 });
|
|
37
|
+
const [isDraggingScrollbar, setIsDraggingScrollbar] = useState(false);
|
|
38
|
+
const [footerHeight, setFooterHeight] = useState(0);
|
|
39
|
+
const dragStateRef = useRef<{
|
|
40
|
+
maxScrollTop: number;
|
|
41
|
+
maxThumbTop: number;
|
|
42
|
+
startScrollTop: number;
|
|
43
|
+
startY: number;
|
|
44
|
+
} | null>(null);
|
|
45
|
+
|
|
46
|
+
const updateScrollbar = useCallback(() => {
|
|
47
|
+
const element = bodyRef.current;
|
|
48
|
+
if (!element) return;
|
|
49
|
+
|
|
50
|
+
const viewportHeight = element.clientHeight;
|
|
51
|
+
const scrollHeight = element.scrollHeight;
|
|
52
|
+
const maxScrollTop = scrollHeight - viewportHeight;
|
|
53
|
+
if (viewportHeight <= 0 || maxScrollTop <= 1) {
|
|
54
|
+
setScrollbar((current) => (current.needed ? { needed: false, top: 0, height: 0 } : current));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const thumbHeight = Math.max(24, Math.round((viewportHeight / scrollHeight) * viewportHeight));
|
|
59
|
+
const maxThumbTop = Math.max(0, viewportHeight - thumbHeight);
|
|
60
|
+
const thumbTop = Math.round((element.scrollTop / maxScrollTop) * maxThumbTop);
|
|
61
|
+
|
|
62
|
+
setScrollbar((current) => {
|
|
63
|
+
if (current.needed && current.top === thumbTop && current.height === thumbHeight) {
|
|
64
|
+
return current;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { needed: true, top: thumbTop, height: thumbHeight };
|
|
68
|
+
});
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
useLayoutEffect(() => {
|
|
72
|
+
updateScrollbar();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const element = bodyRef.current;
|
|
77
|
+
if (!element) return undefined;
|
|
78
|
+
|
|
79
|
+
element.addEventListener('scroll', updateScrollbar, { passive: true });
|
|
80
|
+
|
|
81
|
+
const resizeObserver = new ResizeObserver(updateScrollbar);
|
|
82
|
+
resizeObserver.observe(element);
|
|
83
|
+
|
|
84
|
+
const mutationObserver = new MutationObserver(updateScrollbar);
|
|
85
|
+
mutationObserver.observe(element, {
|
|
86
|
+
attributes: true,
|
|
87
|
+
characterData: true,
|
|
88
|
+
childList: true,
|
|
89
|
+
subtree: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
element.removeEventListener('scroll', updateScrollbar);
|
|
94
|
+
resizeObserver.disconnect();
|
|
95
|
+
mutationObserver.disconnect();
|
|
96
|
+
};
|
|
97
|
+
}, [updateScrollbar]);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!isDraggingScrollbar) return undefined;
|
|
101
|
+
|
|
102
|
+
const handlePointerMove = (event: globalThis.PointerEvent) => {
|
|
103
|
+
const element = bodyRef.current;
|
|
104
|
+
const dragState = dragStateRef.current;
|
|
105
|
+
if (!element || !dragState || dragState.maxThumbTop <= 0) return;
|
|
106
|
+
|
|
107
|
+
const deltaY = event.clientY - dragState.startY;
|
|
108
|
+
element.scrollTop =
|
|
109
|
+
dragState.startScrollTop + (deltaY / dragState.maxThumbTop) * dragState.maxScrollTop;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handlePointerUp = () => {
|
|
113
|
+
dragStateRef.current = null;
|
|
114
|
+
setIsDraggingScrollbar(false);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
window.addEventListener('pointermove', handlePointerMove);
|
|
118
|
+
window.addEventListener('pointerup', handlePointerUp);
|
|
119
|
+
|
|
120
|
+
return () => {
|
|
121
|
+
window.removeEventListener('pointermove', handlePointerMove);
|
|
122
|
+
window.removeEventListener('pointerup', handlePointerUp);
|
|
123
|
+
};
|
|
124
|
+
}, [isDraggingScrollbar]);
|
|
125
|
+
|
|
126
|
+
const handleScrollbarTrackPointerDown = (event: PointerEvent<HTMLDivElement>) => {
|
|
127
|
+
if (event.target !== event.currentTarget) return;
|
|
128
|
+
|
|
129
|
+
const element = bodyRef.current;
|
|
130
|
+
if (!element) return;
|
|
131
|
+
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
const maxScrollTop = element.scrollHeight - element.clientHeight;
|
|
134
|
+
const maxThumbTop = element.clientHeight - scrollbar.height;
|
|
135
|
+
if (maxScrollTop <= 0 || maxThumbTop <= 0) return;
|
|
136
|
+
|
|
137
|
+
const trackTop = event.currentTarget.getBoundingClientRect().top;
|
|
138
|
+
const nextThumbTop = Math.min(
|
|
139
|
+
maxThumbTop,
|
|
140
|
+
Math.max(0, event.clientY - trackTop - scrollbar.height / 2),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
element.scrollTop = (nextThumbTop / maxThumbTop) * maxScrollTop;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const handleScrollbarThumbPointerDown = (event: PointerEvent<HTMLDivElement>) => {
|
|
147
|
+
const element = bodyRef.current;
|
|
148
|
+
if (!element) return;
|
|
149
|
+
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
event.stopPropagation();
|
|
152
|
+
|
|
153
|
+
const maxScrollTop = element.scrollHeight - element.clientHeight;
|
|
154
|
+
const maxThumbTop = element.clientHeight - scrollbar.height;
|
|
155
|
+
if (maxScrollTop <= 0 || maxThumbTop <= 0) return;
|
|
156
|
+
|
|
157
|
+
dragStateRef.current = {
|
|
158
|
+
maxScrollTop,
|
|
159
|
+
maxThumbTop,
|
|
160
|
+
startScrollTop: element.scrollTop,
|
|
161
|
+
startY: event.clientY,
|
|
162
|
+
};
|
|
163
|
+
setIsDraggingScrollbar(true);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
useLayoutEffect(() => {
|
|
167
|
+
const element = footerRef.current;
|
|
168
|
+
if (!element || footerPlacement !== 'overlay' || !hasFooter) {
|
|
169
|
+
setFooterHeight(0);
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const updateFooterHeight = () => {
|
|
174
|
+
const nextHeight = Math.ceil(element.getBoundingClientRect().height);
|
|
175
|
+
setFooterHeight((currentHeight) =>
|
|
176
|
+
currentHeight === nextHeight ? currentHeight : nextHeight,
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
updateFooterHeight();
|
|
181
|
+
const resizeObserver = new ResizeObserver(updateFooterHeight);
|
|
182
|
+
resizeObserver.observe(element);
|
|
183
|
+
|
|
184
|
+
return () => resizeObserver.disconnect();
|
|
185
|
+
}, [footerPlacement, hasFooter]);
|
|
186
|
+
|
|
187
|
+
const panelStyle = {
|
|
188
|
+
'--ui-side-bar-footer-height': `${footerHeight}px`,
|
|
189
|
+
...style,
|
|
190
|
+
} as CSSProperties;
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<Panel className={cx('ui-side-bar-view', className)} style={panelStyle} {...props}>
|
|
194
|
+
<PanelHeader actions={actions} className="ui-side-bar-view__header">
|
|
195
|
+
{title}
|
|
196
|
+
</PanelHeader>
|
|
197
|
+
{headerAddon ? <div className="ui-side-bar-view__header-addon">{headerAddon}</div> : null}
|
|
198
|
+
<div className="ui-side-bar-view__scroll-region">
|
|
199
|
+
<PanelBody
|
|
200
|
+
{...resolvedBodyProps}
|
|
201
|
+
ref={bodyRef}
|
|
202
|
+
className={cx('ui-side-bar-view__body', bodyClassName, bodyPropsClassName)}
|
|
203
|
+
>
|
|
204
|
+
{children}
|
|
205
|
+
</PanelBody>
|
|
206
|
+
{scrollbar.needed ? (
|
|
207
|
+
<div
|
|
208
|
+
aria-hidden="true"
|
|
209
|
+
className={cx(
|
|
210
|
+
'ui-side-bar-overlay-scrollbar',
|
|
211
|
+
isDraggingScrollbar && 'ui-side-bar-overlay-scrollbar--dragging',
|
|
212
|
+
)}
|
|
213
|
+
onPointerDown={handleScrollbarTrackPointerDown}
|
|
214
|
+
>
|
|
215
|
+
<div
|
|
216
|
+
className="ui-side-bar-overlay-scrollbar__thumb"
|
|
217
|
+
style={{
|
|
218
|
+
height: scrollbar.height,
|
|
219
|
+
transform: `translateY(${scrollbar.top}px)`,
|
|
220
|
+
}}
|
|
221
|
+
onPointerDown={handleScrollbarThumbPointerDown}
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
) : null}
|
|
225
|
+
</div>
|
|
226
|
+
{hasFooter ? (
|
|
227
|
+
<div
|
|
228
|
+
ref={footerRef}
|
|
229
|
+
className={cx(
|
|
230
|
+
'panel-footer',
|
|
231
|
+
'ui-side-bar-view__footer',
|
|
232
|
+
footerPlacement === 'overlay' && 'ui-side-bar-view__footer--overlay',
|
|
233
|
+
)}
|
|
234
|
+
>
|
|
235
|
+
{footer}
|
|
236
|
+
</div>
|
|
237
|
+
) : null}
|
|
238
|
+
</Panel>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface SideBarListProps extends ComponentPropsWithRef<'ul'> {
|
|
243
|
+
dropTarget?: boolean;
|
|
244
|
+
fill?: boolean;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function SideBarList({ className, dropTarget, fill, ...props }: SideBarListProps) {
|
|
248
|
+
return (
|
|
249
|
+
<ul
|
|
250
|
+
className={cx(
|
|
251
|
+
'ui-side-bar-list',
|
|
252
|
+
fill && 'ui-side-bar-list--fill',
|
|
253
|
+
dropTarget && 'ui-side-bar-list--drop-target',
|
|
254
|
+
className,
|
|
255
|
+
)}
|
|
256
|
+
{...props}
|
|
257
|
+
/>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface SideBarListItemProps extends ComponentPropsWithRef<'button'> {
|
|
262
|
+
active?: boolean;
|
|
263
|
+
depth?: number;
|
|
264
|
+
dropTarget?: boolean;
|
|
265
|
+
selected?: boolean;
|
|
266
|
+
variant?: 'default' | 'stacked';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const SideBarListItem = forwardRef<HTMLButtonElement, SideBarListItemProps>(
|
|
270
|
+
function SideBarListItem(
|
|
271
|
+
{
|
|
272
|
+
active,
|
|
273
|
+
'aria-current': ariaCurrent,
|
|
274
|
+
className,
|
|
275
|
+
depth = 0,
|
|
276
|
+
dropTarget,
|
|
277
|
+
selected,
|
|
278
|
+
style,
|
|
279
|
+
type = 'button',
|
|
280
|
+
variant = 'default',
|
|
281
|
+
...props
|
|
282
|
+
},
|
|
283
|
+
ref,
|
|
284
|
+
) {
|
|
285
|
+
const depthStyle = { '--depth': depth, ...style } as CSSProperties;
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<li className="ui-side-bar-list-entry">
|
|
289
|
+
<button
|
|
290
|
+
ref={ref}
|
|
291
|
+
type={type}
|
|
292
|
+
aria-current={ariaCurrent ?? (active ? 'true' : undefined)}
|
|
293
|
+
className={cx(
|
|
294
|
+
'ui-side-bar-list-item',
|
|
295
|
+
variant === 'stacked' && 'ui-side-bar-list-item--stacked',
|
|
296
|
+
active && 'ui-side-bar-list-item--active',
|
|
297
|
+
selected && 'ui-side-bar-list-item--selected',
|
|
298
|
+
dropTarget && 'ui-side-bar-list-item--drop-target',
|
|
299
|
+
className,
|
|
300
|
+
)}
|
|
301
|
+
data-selected={selected ? 'true' : undefined}
|
|
302
|
+
style={depthStyle}
|
|
303
|
+
{...props}
|
|
304
|
+
/>
|
|
305
|
+
</li>
|
|
306
|
+
);
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
export type SideBarRowProps = ComponentPropsWithRef<'div'>;
|
|
311
|
+
|
|
312
|
+
export function SideBarRow({ className, ...props }: SideBarRowProps) {
|
|
313
|
+
return <div className={cx('ui-side-bar-row', className)} {...props} />;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export type SideBarHeaderControlProps = ComponentPropsWithRef<'div'>;
|
|
317
|
+
|
|
318
|
+
export function SideBarHeaderControl({ className, ...props }: SideBarHeaderControlProps) {
|
|
319
|
+
return <div className={cx('ui-side-bar-header-control', className)} {...props} />;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export type SideBarScrollSpacerProps = ComponentPropsWithRef<'div'>;
|
|
323
|
+
|
|
324
|
+
export const SideBarScrollSpacer = forwardRef<HTMLDivElement, SideBarScrollSpacerProps>(
|
|
325
|
+
function SideBarScrollSpacer({ className, ...props }, ref) {
|
|
326
|
+
return <div ref={ref} className={cx('ui-side-bar-scroll-spacer', className)} {...props} />;
|
|
327
|
+
},
|
|
328
|
+
);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Button } from '../primitives/Button';
|
|
3
|
+
import { cx } from '../utils/cx';
|
|
4
|
+
import { Modal } from './Modal';
|
|
5
|
+
|
|
6
|
+
type ConfirmDialogVariant = 'default' | 'danger';
|
|
7
|
+
|
|
8
|
+
export interface ConfirmDialogProps {
|
|
9
|
+
title: ReactNode;
|
|
10
|
+
message: ReactNode;
|
|
11
|
+
detail?: ReactNode;
|
|
12
|
+
confirmLabel?: ReactNode;
|
|
13
|
+
cancelLabel?: ReactNode;
|
|
14
|
+
closeLabel?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
variant?: ConfirmDialogVariant;
|
|
17
|
+
onCancel: () => void;
|
|
18
|
+
onConfirm: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ConfirmDialog({
|
|
22
|
+
cancelLabel = 'Cancel',
|
|
23
|
+
className,
|
|
24
|
+
closeLabel = 'Close confirmation dialog',
|
|
25
|
+
confirmLabel = 'Confirm',
|
|
26
|
+
detail,
|
|
27
|
+
message,
|
|
28
|
+
title,
|
|
29
|
+
variant = 'default',
|
|
30
|
+
onCancel,
|
|
31
|
+
onConfirm,
|
|
32
|
+
}: ConfirmDialogProps) {
|
|
33
|
+
return (
|
|
34
|
+
<Modal
|
|
35
|
+
title={title}
|
|
36
|
+
closeLabel={closeLabel}
|
|
37
|
+
className={cx('ui-confirm-dialog', className)}
|
|
38
|
+
bodyClassName="ui-confirm-dialog__body"
|
|
39
|
+
onClose={onCancel}
|
|
40
|
+
footer={
|
|
41
|
+
<>
|
|
42
|
+
<Button onClick={onCancel}>{cancelLabel}</Button>
|
|
43
|
+
<Button variant={variant === 'danger' ? 'danger' : 'primary'} onClick={onConfirm}>
|
|
44
|
+
{confirmLabel}
|
|
45
|
+
</Button>
|
|
46
|
+
</>
|
|
47
|
+
}
|
|
48
|
+
>
|
|
49
|
+
<p className="ui-confirm-dialog__message">{message}</p>
|
|
50
|
+
{detail ? <div className="ui-confirm-dialog__detail">{detail}</div> : null}
|
|
51
|
+
</Modal>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useId, type FormEventHandler, type ReactNode } from 'react';
|
|
2
|
+
import { cx } from '../utils/cx';
|
|
3
|
+
|
|
4
|
+
export interface ModalProps {
|
|
5
|
+
title: ReactNode;
|
|
6
|
+
titleSuffix?: ReactNode;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
footer?: ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
bodyClassName?: string;
|
|
11
|
+
closeLabel?: string;
|
|
12
|
+
labelledBy?: string;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
onSubmit?: FormEventHandler<HTMLFormElement>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ModalContent({
|
|
18
|
+
title,
|
|
19
|
+
titleSuffix,
|
|
20
|
+
children,
|
|
21
|
+
footer,
|
|
22
|
+
bodyClassName,
|
|
23
|
+
closeLabel = 'Close modal',
|
|
24
|
+
labelledBy,
|
|
25
|
+
onClose,
|
|
26
|
+
}: Omit<ModalProps, 'className' | 'onSubmit'>) {
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<div className="ui-modal__titlebar">
|
|
30
|
+
<span id={labelledBy} className="ui-modal__title">
|
|
31
|
+
{title}
|
|
32
|
+
{titleSuffix}
|
|
33
|
+
</span>
|
|
34
|
+
<button type="button" className="ui-modal__close" aria-label={closeLabel} onClick={onClose}>
|
|
35
|
+
<i className="codicon codicon-close" />
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
<div className={cx('ui-modal__body', bodyClassName)}>{children}</div>
|
|
39
|
+
{footer && <div className="ui-modal__footer">{footer}</div>}
|
|
40
|
+
</>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function Modal({
|
|
45
|
+
title,
|
|
46
|
+
titleSuffix,
|
|
47
|
+
children,
|
|
48
|
+
footer,
|
|
49
|
+
className,
|
|
50
|
+
bodyClassName,
|
|
51
|
+
closeLabel,
|
|
52
|
+
labelledBy,
|
|
53
|
+
onClose,
|
|
54
|
+
onSubmit,
|
|
55
|
+
}: ModalProps) {
|
|
56
|
+
const modalClassName = cx('ui-modal', className);
|
|
57
|
+
const generatedLabelId = useId();
|
|
58
|
+
const resolvedLabelledBy = labelledBy ?? generatedLabelId;
|
|
59
|
+
|
|
60
|
+
if (onSubmit) {
|
|
61
|
+
return (
|
|
62
|
+
<div className="ui-modal-overlay" onClick={onClose}>
|
|
63
|
+
<form
|
|
64
|
+
className={modalClassName}
|
|
65
|
+
role="dialog"
|
|
66
|
+
aria-modal="true"
|
|
67
|
+
aria-labelledby={resolvedLabelledBy}
|
|
68
|
+
onClick={(event) => event.stopPropagation()}
|
|
69
|
+
onSubmit={onSubmit}
|
|
70
|
+
>
|
|
71
|
+
<ModalContent
|
|
72
|
+
title={title}
|
|
73
|
+
titleSuffix={titleSuffix}
|
|
74
|
+
bodyClassName={bodyClassName}
|
|
75
|
+
closeLabel={closeLabel}
|
|
76
|
+
labelledBy={resolvedLabelledBy}
|
|
77
|
+
onClose={onClose}
|
|
78
|
+
footer={footer}
|
|
79
|
+
>
|
|
80
|
+
{children}
|
|
81
|
+
</ModalContent>
|
|
82
|
+
</form>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="ui-modal-overlay" onClick={onClose}>
|
|
89
|
+
<div
|
|
90
|
+
className={modalClassName}
|
|
91
|
+
role="dialog"
|
|
92
|
+
aria-modal="true"
|
|
93
|
+
aria-labelledby={resolvedLabelledBy}
|
|
94
|
+
onClick={(event) => event.stopPropagation()}
|
|
95
|
+
>
|
|
96
|
+
<ModalContent
|
|
97
|
+
title={title}
|
|
98
|
+
titleSuffix={titleSuffix}
|
|
99
|
+
bodyClassName={bodyClassName}
|
|
100
|
+
closeLabel={closeLabel}
|
|
101
|
+
labelledBy={resolvedLabelledBy}
|
|
102
|
+
onClose={onClose}
|
|
103
|
+
footer={footer}
|
|
104
|
+
>
|
|
105
|
+
{children}
|
|
106
|
+
</ModalContent>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|