@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.
Files changed (51) hide show
  1. package/package.json +62 -0
  2. package/src/index.ts +43 -0
  3. package/src/layout/Panel.tsx +27 -0
  4. package/src/layout/SideBarViewFrame.tsx +328 -0
  5. package/src/modal/ConfirmDialog.tsx +53 -0
  6. package/src/modal/Modal.tsx +110 -0
  7. package/src/overlay/ContextMenu.tsx +145 -0
  8. package/src/primitives/Badge.tsx +12 -0
  9. package/src/primitives/Button.tsx +14 -0
  10. package/src/primitives/Checkbox.tsx +15 -0
  11. package/src/primitives/EmptyState.tsx +26 -0
  12. package/src/primitives/Field.tsx +38 -0
  13. package/src/primitives/IconButton.tsx +32 -0
  14. package/src/primitives/Select.tsx +12 -0
  15. package/src/primitives/TextInput.tsx +24 -0
  16. package/src/primitives/Toolbar.tsx +8 -0
  17. package/src/styles.css +1762 -0
  18. package/src/utils/cx.ts +3 -0
  19. package/src/workbench/ActivityBar.tsx +52 -0
  20. package/src/workbench/SplitView.tsx +136 -0
  21. package/src/workbench/StatusBar.tsx +123 -0
  22. package/src/workbench/WorkbenchShell.tsx +70 -0
  23. package/src/workbench/WorkbenchStandaloneShell.tsx +291 -0
  24. package/src/workbench/chat/ChatComposer.tsx +148 -0
  25. package/src/workbench/chat/ChatMessageItem.tsx +35 -0
  26. package/src/workbench/chat/ChatMessageList.tsx +49 -0
  27. package/src/workbench/chat/ChatPanel.tsx +55 -0
  28. package/src/workbench/chat/index.ts +9 -0
  29. package/src/workbench/chat/types.ts +10 -0
  30. package/src/workbench/commands.ts +490 -0
  31. package/src/workbench/index.ts +98 -0
  32. package/src/workbench/settings/WorkbenchSettingsModal.tsx +188 -0
  33. package/src/workbench/settings/WorkbenchSettingsNav.tsx +41 -0
  34. package/src/workbench/settings/WorkbenchSettingsSection.tsx +30 -0
  35. package/src/workbench/settings/index.ts +7 -0
  36. package/src/workbench/settings/types.ts +16 -0
  37. package/src/workbench/shellState.ts +203 -0
  38. package/src/workbench/standalone.ts +94 -0
  39. package/src/workbench/workspace/WorkspaceEditor.tsx +226 -0
  40. package/src/workbench/workspace/WorkspaceEditorPanel.tsx +353 -0
  41. package/src/workbench/workspace/WorkspaceExplorer.tsx +524 -0
  42. package/src/workbench/workspace/WorkspaceFileIcon.tsx +149 -0
  43. package/src/workbench/workspace/WorkspaceHighlightedText.tsx +22 -0
  44. package/src/workbench/workspace/WorkspaceSearchPanel.tsx +113 -0
  45. package/src/workbench/workspace/WorkspaceSearchResults.tsx +47 -0
  46. package/src/workbench/workspace/index.ts +98 -0
  47. package/src/workbench/workspace/path.ts +10 -0
  48. package/src/workbench/workspace/search.ts +6 -0
  49. package/src/workbench/workspace/tree.ts +1 -0
  50. package/src/workbench/workspace/types.ts +10 -0
  51. 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
+ }