@workbench-kit/react 0.0.1-prototype.0 → 0.0.2-prototype.0.1.1

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 (155) hide show
  1. package/package.json +33 -9
  2. package/src/index.ts +545 -43
  3. package/src/jdw/JdwPreview.tsx +38 -0
  4. package/src/jdw/JsonCodeEditorPane.tsx +274 -0
  5. package/src/jdw/builtins/renderBuiltinWidgetLeaf.tsx +29 -0
  6. package/src/jdw/builtins/renderBuiltinWidgetNode.tsx +126 -0
  7. package/src/jdw/createBuiltinJdwRegistry.ts +111 -0
  8. package/src/jdw/cssRenderBackend.tsx +134 -0
  9. package/src/jdw/document.ts +38 -0
  10. package/src/jdw/fixtures/jdw-fixtures.ts +52 -0
  11. package/src/jdw/fixtures/jdw-sample-screens.ts +522 -0
  12. package/src/jdw/index.ts +45 -0
  13. package/src/jdw/parse.ts +1 -0
  14. package/src/jdw/renderJdw.tsx +32 -0
  15. package/src/json-config/JsonConfigWorkbench.tsx +316 -0
  16. package/src/json-config/index.ts +6 -0
  17. package/src/json-config/json-config-editor-state.ts +42 -0
  18. package/src/layout/Panel.tsx +146 -1
  19. package/src/layout/SideBarViewFrame.tsx +367 -328
  20. package/src/layout/WorkbenchCanvas.tsx +849 -0
  21. package/src/layout/WorkbenchFullscreen.tsx +368 -0
  22. package/src/layout/WorkbenchLayout.tsx +5 -0
  23. package/src/layout/WorkbenchLayoutBase.tsx +294 -0
  24. package/src/layout/WorkbenchPropertyPanel.tsx +497 -0
  25. package/src/layout/WorkbenchSidebarActions.tsx +274 -0
  26. package/src/layout/WorkbenchTree.tsx +351 -0
  27. package/src/layout/layoutHelpers.ts +25 -0
  28. package/src/modal/ConfirmDialog.tsx +61 -53
  29. package/src/modal/Modal.tsx +295 -35
  30. package/src/overlay/ContextMenu.tsx +146 -145
  31. package/src/primitives/AbsoluteBox.tsx +46 -0
  32. package/src/primitives/Badge.tsx +12 -12
  33. package/src/primitives/Button.tsx +44 -14
  34. package/src/primitives/Checkbox.tsx +11 -5
  35. package/src/primitives/EmptyState.tsx +27 -26
  36. package/src/primitives/Field.tsx +38 -38
  37. package/src/primitives/IconButton.tsx +5 -2
  38. package/src/primitives/List.tsx +111 -0
  39. package/src/primitives/NumberInput.tsx +38 -0
  40. package/src/primitives/ScrollArea.tsx +42 -0
  41. package/src/primitives/Select.tsx +1 -12
  42. package/src/primitives/StatusBar.tsx +46 -0
  43. package/src/primitives/TextArea.tsx +36 -0
  44. package/src/primitives/TextInput.tsx +11 -2
  45. package/src/primitives/WorkbenchChrome.tsx +424 -0
  46. package/src/primitives/WorkbenchEditor.tsx +394 -0
  47. package/src/primitives/index.ts +105 -0
  48. package/src/primitives/select/Select.tsx +266 -0
  49. package/src/primitives/select/index.ts +2 -0
  50. package/src/primitives/select/options.ts +38 -0
  51. package/src/primitives/select/overlay.ts +63 -0
  52. package/src/primitives/select/select.app.css +9 -0
  53. package/src/primitives/select/select.css +140 -0
  54. package/src/primitives/select/types.ts +18 -0
  55. package/src/primitives/styles.css +955 -0
  56. package/src/scrollbars.css +69 -0
  57. package/src/styles.css +7738 -1762
  58. package/src/utils/codicon.ts +17 -0
  59. package/src/widget-asset/WidgetAssetEditor.tsx +140 -0
  60. package/src/widget-asset/WidgetAssetMetadataForm.tsx +82 -0
  61. package/src/widget-asset/WidgetAssetModeControls.tsx +42 -0
  62. package/src/widget-asset/WidgetAssetSourceEditor.tsx +50 -0
  63. package/src/widget-asset/WidgetAssetWorkbench.tsx +112 -0
  64. package/src/widget-asset/index.ts +17 -0
  65. package/src/widget-asset/widget-asset-document.ts +37 -0
  66. package/src/widget-asset/widget-asset-mode.ts +3 -0
  67. package/src/widget-studio/builtin-widget-asset-catalog.ts +12 -0
  68. package/src/widget-studio/create-widget-studio-workspace-editor.tsx +79 -0
  69. package/src/widget-studio/index.ts +7 -0
  70. package/src/widget-studio/resolve-widget-studio-asset-catalog.ts +26 -0
  71. package/src/widget-tree/WidgetAssetPalette.tsx +77 -0
  72. package/src/widget-tree/WidgetInspectorPanel.tsx +266 -0
  73. package/src/widget-tree/WidgetSourceEditor.tsx +56 -0
  74. package/src/widget-tree/WidgetTreeLab.tsx +272 -0
  75. package/src/widget-tree/WidgetTreeModeControls.tsx +44 -0
  76. package/src/widget-tree/WidgetTreeSidePanel.tsx +49 -0
  77. package/src/widget-tree/WidgetTreeView.tsx +97 -0
  78. package/src/widget-tree/WidgetTreeWorkbench.tsx +111 -0
  79. package/src/widget-tree/WidgetTreeWorkspaceShell.tsx +154 -0
  80. package/src/widget-tree/demo-registry.ts +32 -0
  81. package/src/widget-tree/demo-widget-assets.ts +4 -0
  82. package/src/widget-tree/index.ts +27 -0
  83. package/src/widget-tree/widget-tree-document.ts +20 -0
  84. package/src/widget-tree/widget-tree-layout.ts +31 -0
  85. package/src/widget-tree/widget-tree-mode.ts +7 -0
  86. package/src/widget-tree/widget-tree-play-helpers.ts +74 -0
  87. package/src/workbench/ActivityBar.tsx +52 -52
  88. package/src/workbench/ArtifactShell.tsx +397 -0
  89. package/src/workbench/CommandPalette.tsx +819 -0
  90. package/src/workbench/ConfirmationFlow.tsx +245 -0
  91. package/src/workbench/MarkdownPreview.tsx +151 -0
  92. package/src/workbench/ShortcutCommandBridge.tsx +368 -0
  93. package/src/workbench/SplitView.tsx +136 -136
  94. package/src/workbench/StatusBar.tsx +178 -123
  95. package/src/workbench/StructuredArtifactEditor.tsx +205 -0
  96. package/src/workbench/Timeline.tsx +302 -0
  97. package/src/workbench/WorkbenchCanvasShell.tsx +941 -0
  98. package/src/workbench/WorkbenchShell.tsx +74 -70
  99. package/src/workbench/WorkbenchStandaloneShell.tsx +318 -291
  100. package/src/workbench/auth/WorkbenchAuthGate.tsx +61 -0
  101. package/src/workbench/auth/WorkbenchLoginView.tsx +635 -0
  102. package/src/workbench/auth/index.ts +19 -0
  103. package/src/workbench/chat/ChatComposer.tsx +165 -148
  104. package/src/workbench/chat/ChatMessageItem.tsx +10 -1
  105. package/src/workbench/chat/index.ts +9 -0
  106. package/src/workbench/chat/slashCommand.ts +28 -0
  107. package/src/workbench/chat/useChatRuntimeState.ts +76 -0
  108. package/src/workbench/chat/useSlashCommandSuggest.ts +101 -0
  109. package/src/workbench/commands.ts +520 -490
  110. package/src/workbench/index.ts +443 -98
  111. package/src/workbench/keyboard.ts +53 -0
  112. package/src/workbench/schema/index.ts +48 -0
  113. package/src/workbench/schema/workbenchDocument.ts +31 -0
  114. package/src/workbench/schema/workbenchDocumentActions.ts +6 -0
  115. package/src/workbench/schema/workbenchDocumentAdapter.ts +9 -0
  116. package/src/workbench/schema/workbenchDocumentPatch.ts +13 -0
  117. package/src/workbench/schema/workbenchDocumentRenderer.tsx +702 -0
  118. package/src/workbench/settings/NavigationPanel.tsx +47 -0
  119. package/src/workbench/settings/SchemaForm.tsx +420 -0
  120. package/src/workbench/settings/SectionedPanel.tsx +415 -0
  121. package/src/workbench/settings/StructuredDataForm.tsx +539 -0
  122. package/src/workbench/settings/StructuredDataSchemaPanel.tsx +559 -0
  123. package/src/workbench/settings/StructuredDataSchemaPanelEmbed.tsx +21 -0
  124. package/src/workbench/settings/StructuredDataSchemaPanelFrame.tsx +76 -0
  125. package/src/workbench/settings/StructuredDataTableView.tsx +127 -0
  126. package/src/workbench/settings/WorkbenchPanelRegion.tsx +46 -0
  127. package/src/workbench/settings/WorkbenchSettingsModal.tsx +21 -12
  128. package/src/workbench/settings/WorkbenchSettingsNav.tsx +38 -22
  129. package/src/workbench/settings/index.ts +135 -0
  130. package/src/workbench/settings/panel-region.css +21 -0
  131. package/src/workbench/settings/sectionedPanelScrollSpy.ts +143 -0
  132. package/src/workbench/settings/structured-data-schema-panel.css +330 -0
  133. package/src/workbench/settings/structured-data-table-view.css +85 -0
  134. package/src/workbench/settings/structuredDataSchema.ts +886 -0
  135. package/src/workbench/shell.ts +15 -0
  136. package/src/workbench/shellState.ts +203 -203
  137. package/src/workbench/standalone.ts +101 -94
  138. package/src/workbench/status.ts +129 -0
  139. package/src/workbench/theme.ts +44 -0
  140. package/src/workbench/workspace/MultiProviderExplorer.tsx +625 -0
  141. package/src/workbench/workspace/WorkspaceDraftsContext.tsx +111 -0
  142. package/src/workbench/workspace/WorkspaceEditor.tsx +47 -12
  143. package/src/workbench/workspace/WorkspaceEditorPanel.tsx +334 -353
  144. package/src/workbench/workspace/WorkspaceExplorer.tsx +532 -524
  145. package/src/workbench/workspace/WorkspaceFileIcon.tsx +32 -2
  146. package/src/workbench/workspace/WorkspaceSearchPanel.tsx +113 -113
  147. package/src/workbench/workspace/WorkspaceSearchResults.tsx +47 -47
  148. package/src/workbench/workspace/index.ts +156 -98
  149. package/src/workbench/workspace/mimeType.ts +45 -0
  150. package/src/workbench/workspace/path.ts +10 -10
  151. package/src/workbench/workspace/search.ts +6 -6
  152. package/src/workbench/workspace/tree.ts +1 -1
  153. package/src/workbench/workspace/types.ts +10 -10
  154. package/src/workbench/workspace/useVirtualWorkspace.ts +111 -98
  155. package/src/workbench/workspace/workspaceJsonDiagnostics.ts +83 -0
@@ -1,328 +1,367 @@
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
- );
1
+ import type {
2
+ ComponentPropsWithRef,
3
+ CSSProperties,
4
+ HTMLAttributes,
5
+ PointerEvent,
6
+ ReactNode,
7
+ } from 'react';
8
+ import { forwardRef, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
9
+ import { cx } from '../utils/cx';
10
+ import { Panel, PanelBody, PanelHeader, type PanelBodyProps, type PanelProps } from './Panel';
11
+ import { workbenchTreeIndentOffset } from './layoutHelpers';
12
+
13
+ type SideBarFooterPlacement = 'static' | 'overlay';
14
+
15
+ export interface SideBarViewFrameProps extends Omit<PanelProps, 'children' | 'title'> {
16
+ actions?: ReactNode;
17
+ bodyClassName?: string;
18
+ bodyProps?: PanelBodyProps;
19
+ children: ReactNode;
20
+ footer?: ReactNode;
21
+ footerPlacement?: SideBarFooterPlacement;
22
+ headerAddon?: ReactNode;
23
+ title: ReactNode;
24
+ }
25
+
26
+ export function SideBarViewFrame({
27
+ actions,
28
+ bodyClassName,
29
+ bodyProps,
30
+ children,
31
+ className,
32
+ footer,
33
+ footerPlacement = 'static',
34
+ headerAddon,
35
+ style,
36
+ title,
37
+ ...props
38
+ }: SideBarViewFrameProps) {
39
+ const { className: bodyPropsClassName, ...resolvedBodyProps } = bodyProps ?? {};
40
+ const hasFooter = Boolean(footer);
41
+ const bodyRef = useRef<HTMLDivElement>(null);
42
+ const footerRef = useRef<HTMLDivElement>(null);
43
+ const [scrollbar, setScrollbar] = useState({ needed: false, top: 0, height: 0 });
44
+ const [isDraggingScrollbar, setIsDraggingScrollbar] = useState(false);
45
+ const [footerHeight, setFooterHeight] = useState(0);
46
+ const dragStateRef = useRef<{
47
+ maxScrollTop: number;
48
+ maxThumbTop: number;
49
+ startScrollTop: number;
50
+ startY: number;
51
+ } | null>(null);
52
+
53
+ const updateScrollbar = useCallback(() => {
54
+ const element = bodyRef.current;
55
+ if (!element) return;
56
+
57
+ const viewportHeight = element.clientHeight;
58
+ const scrollHeight = element.scrollHeight;
59
+ const maxScrollTop = scrollHeight - viewportHeight;
60
+ if (viewportHeight <= 0 || maxScrollTop <= 1) {
61
+ setScrollbar((current) => (current.needed ? { needed: false, top: 0, height: 0 } : current));
62
+ return;
63
+ }
64
+
65
+ const thumbHeight = Math.max(24, Math.round((viewportHeight / scrollHeight) * viewportHeight));
66
+ const maxThumbTop = Math.max(0, viewportHeight - thumbHeight);
67
+ const thumbTop = Math.round((element.scrollTop / maxScrollTop) * maxThumbTop);
68
+
69
+ setScrollbar((current) => {
70
+ if (current.needed && current.top === thumbTop && current.height === thumbHeight) {
71
+ return current;
72
+ }
73
+
74
+ return { needed: true, top: thumbTop, height: thumbHeight };
75
+ });
76
+ }, []);
77
+
78
+ useLayoutEffect(() => {
79
+ updateScrollbar();
80
+ });
81
+
82
+ useEffect(() => {
83
+ const element = bodyRef.current;
84
+ if (!element) return undefined;
85
+
86
+ element.addEventListener('scroll', updateScrollbar, { passive: true });
87
+
88
+ const resizeObserver = new ResizeObserver(updateScrollbar);
89
+ resizeObserver.observe(element);
90
+
91
+ const mutationObserver = new MutationObserver(updateScrollbar);
92
+ mutationObserver.observe(element, {
93
+ attributes: true,
94
+ characterData: true,
95
+ childList: true,
96
+ subtree: true,
97
+ });
98
+
99
+ return () => {
100
+ element.removeEventListener('scroll', updateScrollbar);
101
+ resizeObserver.disconnect();
102
+ mutationObserver.disconnect();
103
+ };
104
+ }, [updateScrollbar]);
105
+
106
+ useEffect(() => {
107
+ if (!isDraggingScrollbar) return undefined;
108
+
109
+ const handlePointerMove = (event: globalThis.PointerEvent) => {
110
+ const element = bodyRef.current;
111
+ const dragState = dragStateRef.current;
112
+ if (!element || !dragState || dragState.maxThumbTop <= 0) return;
113
+
114
+ const deltaY = event.clientY - dragState.startY;
115
+ element.scrollTop =
116
+ dragState.startScrollTop + (deltaY / dragState.maxThumbTop) * dragState.maxScrollTop;
117
+ };
118
+
119
+ const handlePointerUp = () => {
120
+ dragStateRef.current = null;
121
+ setIsDraggingScrollbar(false);
122
+ };
123
+
124
+ window.addEventListener('pointermove', handlePointerMove);
125
+ window.addEventListener('pointerup', handlePointerUp);
126
+
127
+ return () => {
128
+ window.removeEventListener('pointermove', handlePointerMove);
129
+ window.removeEventListener('pointerup', handlePointerUp);
130
+ };
131
+ }, [isDraggingScrollbar]);
132
+
133
+ const handleScrollbarTrackPointerDown = (event: PointerEvent<HTMLDivElement>) => {
134
+ if (event.target !== event.currentTarget) return;
135
+
136
+ const element = bodyRef.current;
137
+ if (!element) return;
138
+
139
+ event.preventDefault();
140
+ const maxScrollTop = element.scrollHeight - element.clientHeight;
141
+ const maxThumbTop = element.clientHeight - scrollbar.height;
142
+ if (maxScrollTop <= 0 || maxThumbTop <= 0) return;
143
+
144
+ const trackTop = event.currentTarget.getBoundingClientRect().top;
145
+ const nextThumbTop = Math.min(
146
+ maxThumbTop,
147
+ Math.max(0, event.clientY - trackTop - scrollbar.height / 2),
148
+ );
149
+
150
+ element.scrollTop = (nextThumbTop / maxThumbTop) * maxScrollTop;
151
+ };
152
+
153
+ const handleScrollbarThumbPointerDown = (event: PointerEvent<HTMLDivElement>) => {
154
+ const element = bodyRef.current;
155
+ if (!element) return;
156
+
157
+ event.preventDefault();
158
+ event.stopPropagation();
159
+
160
+ const maxScrollTop = element.scrollHeight - element.clientHeight;
161
+ const maxThumbTop = element.clientHeight - scrollbar.height;
162
+ if (maxScrollTop <= 0 || maxThumbTop <= 0) return;
163
+
164
+ dragStateRef.current = {
165
+ maxScrollTop,
166
+ maxThumbTop,
167
+ startScrollTop: element.scrollTop,
168
+ startY: event.clientY,
169
+ };
170
+ setIsDraggingScrollbar(true);
171
+ };
172
+
173
+ useLayoutEffect(() => {
174
+ const element = footerRef.current;
175
+ if (!element || footerPlacement !== 'overlay' || !hasFooter) {
176
+ setFooterHeight(0);
177
+ return undefined;
178
+ }
179
+
180
+ const updateFooterHeight = () => {
181
+ const nextHeight = Math.ceil(element.getBoundingClientRect().height);
182
+ setFooterHeight((currentHeight) =>
183
+ currentHeight === nextHeight ? currentHeight : nextHeight,
184
+ );
185
+ };
186
+
187
+ updateFooterHeight();
188
+ const resizeObserver = new ResizeObserver(updateFooterHeight);
189
+ resizeObserver.observe(element);
190
+
191
+ return () => resizeObserver.disconnect();
192
+ }, [footerPlacement, hasFooter]);
193
+
194
+ const panelStyle = {
195
+ '--ui-side-bar-footer-height': `${footerHeight}px`,
196
+ ...style,
197
+ } as CSSProperties;
198
+
199
+ return (
200
+ <Panel className={cx('ui-side-bar-view', className)} style={panelStyle} {...props}>
201
+ <PanelHeader actions={actions} className="ui-side-bar-view__header">
202
+ {title}
203
+ </PanelHeader>
204
+ {headerAddon ? <div className="ui-side-bar-view__header-addon">{headerAddon}</div> : null}
205
+ <div className="ui-side-bar-view__scroll-region">
206
+ <PanelBody
207
+ {...resolvedBodyProps}
208
+ ref={bodyRef}
209
+ className={cx('ui-side-bar-view__body', bodyClassName, bodyPropsClassName)}
210
+ >
211
+ {children}
212
+ </PanelBody>
213
+ {scrollbar.needed ? (
214
+ <div
215
+ aria-hidden="true"
216
+ className={cx(
217
+ 'ui-side-bar-overlay-scrollbar',
218
+ isDraggingScrollbar && 'ui-side-bar-overlay-scrollbar--dragging',
219
+ )}
220
+ onPointerDown={handleScrollbarTrackPointerDown}
221
+ >
222
+ <div
223
+ className="ui-side-bar-overlay-scrollbar__thumb"
224
+ style={{
225
+ height: scrollbar.height,
226
+ transform: `translateY(${scrollbar.top}px)`,
227
+ }}
228
+ onPointerDown={handleScrollbarThumbPointerDown}
229
+ />
230
+ </div>
231
+ ) : null}
232
+ </div>
233
+ {hasFooter ? (
234
+ <div
235
+ ref={footerRef}
236
+ className={cx(
237
+ 'panel-footer',
238
+ 'ui-side-bar-view__footer',
239
+ footerPlacement === 'overlay' && 'ui-side-bar-view__footer--overlay',
240
+ )}
241
+ >
242
+ {footer}
243
+ </div>
244
+ ) : null}
245
+ </Panel>
246
+ );
247
+ }
248
+
249
+ export interface SideBarListProps extends ComponentPropsWithRef<'ul'> {
250
+ dropTarget?: boolean;
251
+ fill?: boolean;
252
+ }
253
+
254
+ export function SideBarList({ className, dropTarget, fill, ...props }: SideBarListProps) {
255
+ return (
256
+ <ul
257
+ className={cx(
258
+ 'ui-side-bar-list',
259
+ fill && 'ui-side-bar-list--fill',
260
+ dropTarget && 'ui-side-bar-list--drop-target',
261
+ className,
262
+ )}
263
+ {...props}
264
+ />
265
+ );
266
+ }
267
+
268
+ export interface SideBarListItemProps extends ComponentPropsWithRef<'button'> {
269
+ active?: boolean;
270
+ after?: ReactNode;
271
+ depth?: number;
272
+ dropTarget?: boolean;
273
+ noLi?: boolean;
274
+ selected?: boolean;
275
+ variant?: 'default' | 'stacked';
276
+ wrapperProps?: HTMLAttributes<HTMLLIElement>;
277
+ }
278
+
279
+ export function sideBarTreeDepthStyle(depth: number, style?: CSSProperties): CSSProperties {
280
+ return {
281
+ '--depth': depth,
282
+ '--ui-side-bar-tree-indent-offset': workbenchTreeIndentOffset(depth),
283
+ ...style,
284
+ } as CSSProperties;
285
+ }
286
+
287
+ export const SideBarListItem = forwardRef<HTMLButtonElement, SideBarListItemProps>(
288
+ function SideBarListItem(
289
+ {
290
+ active,
291
+ after,
292
+ 'aria-current': ariaCurrent,
293
+ className,
294
+ depth = 0,
295
+ dropTarget,
296
+ noLi,
297
+ selected,
298
+ style,
299
+ type = 'button',
300
+ variant = 'default',
301
+ wrapperProps,
302
+ ...props
303
+ },
304
+ ref,
305
+ ) {
306
+ const depthStyle = sideBarTreeDepthStyle(depth, style);
307
+ const { className: wrapperClassName, ...restWrapperProps } = wrapperProps ?? {};
308
+
309
+ const button = (
310
+ <button
311
+ ref={ref}
312
+ type={type}
313
+ aria-current={ariaCurrent ?? (active ? 'true' : undefined)}
314
+ className={cx(
315
+ 'ui-side-bar-list-item',
316
+ variant === 'stacked' && 'ui-side-bar-list-item--stacked',
317
+ active && 'ui-side-bar-list-item--active',
318
+ selected && 'ui-side-bar-list-item--selected',
319
+ dropTarget && 'ui-side-bar-list-item--drop-target',
320
+ className,
321
+ )}
322
+ data-selected={selected ? 'true' : undefined}
323
+ style={depthStyle}
324
+ {...props}
325
+ />
326
+ );
327
+
328
+ if (noLi) {
329
+ if (after) {
330
+ return (
331
+ <>
332
+ {button}
333
+ {after}
334
+ </>
335
+ );
336
+ }
337
+ return button;
338
+ }
339
+
340
+ return (
341
+ <li className={cx('ui-side-bar-list-entry', wrapperClassName)} {...restWrapperProps}>
342
+ {button}
343
+ {after}
344
+ </li>
345
+ );
346
+ },
347
+ );
348
+
349
+ export type SideBarRowProps = ComponentPropsWithRef<'div'>;
350
+
351
+ export function SideBarRow({ className, ...props }: SideBarRowProps) {
352
+ return <div className={cx('ui-side-bar-row', className)} {...props} />;
353
+ }
354
+
355
+ export type SideBarHeaderControlProps = ComponentPropsWithRef<'div'>;
356
+
357
+ export function SideBarHeaderControl({ className, ...props }: SideBarHeaderControlProps) {
358
+ return <div className={cx('ui-side-bar-header-control', className)} {...props} />;
359
+ }
360
+
361
+ export type SideBarScrollSpacerProps = ComponentPropsWithRef<'div'>;
362
+
363
+ export const SideBarScrollSpacer = forwardRef<HTMLDivElement, SideBarScrollSpacerProps>(
364
+ function SideBarScrollSpacer({ className, ...props }, ref) {
365
+ return <div ref={ref} className={cx('ui-side-bar-scroll-spacer', className)} {...props} />;
366
+ },
367
+ );