@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
@@ -0,0 +1,424 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import type {
3
+ CSSProperties,
4
+ ComponentPropsWithRef,
5
+ KeyboardEvent as ReactKeyboardEvent,
6
+ PointerEvent as ReactPointerEvent,
7
+ ReactNode,
8
+ } from 'react';
9
+ import { cxCodicon } from '../utils/codicon';
10
+ import { cx } from '../utils/cx';
11
+
12
+ export interface ActivityItem {
13
+ active?: boolean | undefined;
14
+ badge?: ReactNode;
15
+ icon: string;
16
+ id: string;
17
+ label: string;
18
+ }
19
+
20
+ export interface ActivityBarProps extends Omit<ComponentPropsWithRef<'nav'>, 'onSelect'> {
21
+ items: readonly ActivityItem[];
22
+ onSelect?: ((id: string) => void) | undefined;
23
+ }
24
+
25
+ export function ActivityBar({ className, items, onSelect, ...props }: ActivityBarProps) {
26
+ return (
27
+ <nav
28
+ aria-label={props['aria-label'] ?? 'Activity bar'}
29
+ className={cx('ui-activity-bar', className)}
30
+ {...props}
31
+ >
32
+ {items.map((item) => (
33
+ <button
34
+ key={item.id}
35
+ aria-label={item.label}
36
+ aria-pressed={item.active}
37
+ className={cx('ui-activity-bar__item', item.active && 'ui-activity-bar__item--active')}
38
+ onClick={() => onSelect?.(item.id)}
39
+ title={item.label}
40
+ type="button"
41
+ >
42
+ <i aria-hidden className={cxCodicon(item.icon)} />
43
+ {item.badge ? <span className="ui-activity-bar__badge">{item.badge}</span> : null}
44
+ </button>
45
+ ))}
46
+ </nav>
47
+ );
48
+ }
49
+
50
+ export interface WorkbenchShellProps {
51
+ activityBar?: ReactNode | undefined;
52
+ activityBarWidth?: number | string | undefined;
53
+ center: ReactNode;
54
+ left?: ReactNode | undefined;
55
+ leftWidth?: number | string | undefined;
56
+ resizableLeft?: boolean | undefined;
57
+ resizableRight?: boolean | undefined;
58
+ right?: ReactNode | undefined;
59
+ rightWidth?: number | string | undefined;
60
+ statusBar?: ReactNode | undefined;
61
+ style?: CSSProperties | undefined;
62
+ }
63
+
64
+ function resolveShellWidth(value: number | string | undefined, fallback: number) {
65
+ if (typeof value === 'number') return value;
66
+ if (typeof value === 'string' && value.endsWith('px')) {
67
+ const parsed = Number.parseFloat(value);
68
+ if (Number.isFinite(parsed)) return parsed;
69
+ }
70
+ return fallback;
71
+ }
72
+
73
+ function formatShellWidth(value: number | string | undefined, fallback: number) {
74
+ return typeof value === 'number' ? `${value}px` : (value ?? `${fallback}px`);
75
+ }
76
+
77
+ const SHELL_SEPARATOR_WIDTH = 1;
78
+ const SHELL_MIN_SIDE_WIDTH = 160;
79
+ const SHELL_MIN_CENTER_WIDTH = 220;
80
+ const SHELL_NARROW_CENTER_WIDTH = 180;
81
+
82
+ function clampShellSideWidth(value: number, containerWidth: number, oppositeWidth: number) {
83
+ const maxSideWidth = Math.max(
84
+ SHELL_MIN_SIDE_WIDTH,
85
+ containerWidth - oppositeWidth - SHELL_MIN_CENTER_WIDTH,
86
+ );
87
+ return Math.min(Math.max(value, SHELL_MIN_SIDE_WIDTH), maxSideWidth);
88
+ }
89
+
90
+ function clampShellResponsiveSideWidth(
91
+ value: number,
92
+ containerWidth: number | null,
93
+ usedWidth: number,
94
+ ) {
95
+ if (containerWidth === null) return value;
96
+ const available = containerWidth - usedWidth - SHELL_NARROW_CENTER_WIDTH;
97
+ return Math.min(value, Math.max(SHELL_MIN_SIDE_WIDTH, available));
98
+ }
99
+
100
+ export function WorkbenchShell({
101
+ activityBar,
102
+ activityBarWidth = 48,
103
+ center,
104
+ left,
105
+ leftWidth = 260,
106
+ resizableLeft = true,
107
+ resizableRight = true,
108
+ right,
109
+ rightWidth = 280,
110
+ statusBar,
111
+ style,
112
+ }: WorkbenchShellProps) {
113
+ const contentRef = useRef<HTMLDivElement>(null);
114
+ const [currentLeftWidth, setCurrentLeftWidth] = useState(() => resolveShellWidth(leftWidth, 260));
115
+ const [currentRightWidth, setCurrentRightWidth] = useState(() =>
116
+ resolveShellWidth(rightWidth, 280),
117
+ );
118
+ const [contentWidth, setContentWidth] = useState<number | null>(null);
119
+
120
+ useEffect(() => {
121
+ const element = contentRef.current;
122
+ if (!element) return;
123
+
124
+ const update = () => setContentWidth(element.getBoundingClientRect().width);
125
+ update();
126
+
127
+ const observer = new ResizeObserver(update);
128
+ observer.observe(element);
129
+ return () => observer.disconnect();
130
+ }, []);
131
+
132
+ const activityWidthPx = activityBar ? resolveShellWidth(activityBarWidth, 48) : 0;
133
+ const leftWidthPx = resizableLeft ? currentLeftWidth : resolveShellWidth(leftWidth, 260);
134
+ const rightWidthPx = resizableRight ? currentRightWidth : resolveShellWidth(rightWidth, 280);
135
+ const leftResizeVisible = Boolean(left && resizableLeft);
136
+ const rightMinimumWidth =
137
+ activityWidthPx +
138
+ (left ? leftWidthPx : 0) +
139
+ (leftResizeVisible ? SHELL_SEPARATOR_WIDTH : 0) +
140
+ rightWidthPx +
141
+ (resizableRight ? SHELL_SEPARATOR_WIDTH : 0) +
142
+ SHELL_MIN_CENTER_WIDTH;
143
+ const rightVisible = Boolean(
144
+ right && (contentWidth === null || contentWidth >= rightMinimumWidth),
145
+ );
146
+ const rightResizeVisible = Boolean(rightVisible && resizableRight);
147
+ const effectiveLeftWidth = clampShellResponsiveSideWidth(
148
+ leftWidthPx,
149
+ contentWidth,
150
+ activityWidthPx +
151
+ (leftResizeVisible ? SHELL_SEPARATOR_WIDTH : 0) +
152
+ (rightVisible ? rightWidthPx + (rightResizeVisible ? SHELL_SEPARATOR_WIDTH : 0) : 0),
153
+ );
154
+
155
+ const handleResizePointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
156
+ event.preventDefault();
157
+
158
+ const side = event.currentTarget.dataset.side;
159
+ if (side !== 'left' && side !== 'right') return;
160
+
161
+ const rect = contentRef.current?.getBoundingClientRect();
162
+ if (!rect) return;
163
+
164
+ const origin = event.clientX;
165
+ const startLeftWidth = currentLeftWidth;
166
+ const startRightWidth = currentRightWidth;
167
+
168
+ const handlePointerMove = (moveEvent: PointerEvent) => {
169
+ if (side === 'left') {
170
+ setCurrentLeftWidth(
171
+ clampShellSideWidth(
172
+ startLeftWidth + moveEvent.clientX - origin,
173
+ rect.width,
174
+ right ? startRightWidth : 0,
175
+ ),
176
+ );
177
+ } else {
178
+ setCurrentRightWidth(
179
+ clampShellSideWidth(
180
+ startRightWidth + origin - moveEvent.clientX,
181
+ rect.width,
182
+ left ? startLeftWidth : 0,
183
+ ),
184
+ );
185
+ }
186
+ };
187
+
188
+ const handlePointerUp = () => {
189
+ window.removeEventListener('pointermove', handlePointerMove);
190
+ window.removeEventListener('pointerup', handlePointerUp);
191
+ };
192
+
193
+ window.addEventListener('pointermove', handlePointerMove);
194
+ window.addEventListener('pointerup', handlePointerUp, { once: true });
195
+ };
196
+
197
+ const handleResizeKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {
198
+ const side = event.currentTarget.dataset.side;
199
+ if (side !== 'left' && side !== 'right') return;
200
+
201
+ const step = event.shiftKey ? 40 : 10;
202
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
203
+ event.preventDefault();
204
+
205
+ const rect = contentRef.current?.getBoundingClientRect();
206
+ if (!rect) return;
207
+
208
+ const delta = event.key === 'ArrowRight' ? step : -step;
209
+ if (side === 'left') {
210
+ setCurrentLeftWidth((width) =>
211
+ clampShellSideWidth(width + delta, rect.width, right ? currentRightWidth : 0),
212
+ );
213
+ } else {
214
+ setCurrentRightWidth((width) =>
215
+ clampShellSideWidth(width - delta, rect.width, left ? currentLeftWidth : 0),
216
+ );
217
+ }
218
+ };
219
+
220
+ return (
221
+ <div className="ui-workbench-shell" style={style}>
222
+ <div ref={contentRef} className="ui-workbench-shell__content">
223
+ {activityBar ? (
224
+ <div
225
+ className="ui-workbench-shell__activity"
226
+ style={{ flexBasis: formatShellWidth(activityBarWidth, 48) }}
227
+ >
228
+ {activityBar}
229
+ </div>
230
+ ) : null}
231
+ {left ? (
232
+ <div
233
+ className="ui-workbench-shell__side"
234
+ style={{
235
+ flexBasis:
236
+ contentWidth === null && !resizableLeft
237
+ ? formatShellWidth(leftWidth, 260)
238
+ : `${effectiveLeftWidth}px`,
239
+ }}
240
+ >
241
+ {left}
242
+ </div>
243
+ ) : null}
244
+ {left && leftResizeVisible ? (
245
+ <div
246
+ aria-label="Resize left sidebar"
247
+ aria-orientation="vertical"
248
+ className="ui-workbench-shell__separator"
249
+ data-side="left"
250
+ onKeyDown={handleResizeKeyDown}
251
+ onPointerDown={handleResizePointerDown}
252
+ role="separator"
253
+ tabIndex={0}
254
+ />
255
+ ) : null}
256
+ <div className="ui-workbench-shell__center">{center}</div>
257
+ {rightVisible && rightResizeVisible ? (
258
+ <div
259
+ aria-label="Resize right sidebar"
260
+ aria-orientation="vertical"
261
+ className="ui-workbench-shell__separator"
262
+ data-side="right"
263
+ onKeyDown={handleResizeKeyDown}
264
+ onPointerDown={handleResizePointerDown}
265
+ role="separator"
266
+ tabIndex={0}
267
+ />
268
+ ) : null}
269
+ {rightVisible ? (
270
+ <div
271
+ className="ui-workbench-shell__side"
272
+ style={{
273
+ flexBasis: resizableRight
274
+ ? `${currentRightWidth}px`
275
+ : formatShellWidth(rightWidth, 280),
276
+ }}
277
+ >
278
+ {right}
279
+ </div>
280
+ ) : null}
281
+ </div>
282
+ {statusBar}
283
+ </div>
284
+ );
285
+ }
286
+
287
+ export interface SideBarProps extends ComponentPropsWithRef<'aside'> {
288
+ side?: 'left' | 'right';
289
+ }
290
+
291
+ export function SideBar({ className, side = 'left', ...props }: SideBarProps) {
292
+ return (
293
+ <aside
294
+ className={cx('ui-sidebar', side === 'right' && 'ui-sidebar--right', className)}
295
+ {...props}
296
+ />
297
+ );
298
+ }
299
+
300
+ export interface CollapsibleProps extends Omit<
301
+ ComponentPropsWithRef<'section'>,
302
+ 'onToggle' | 'title'
303
+ > {
304
+ basis?: string | undefined;
305
+ defaultOpen?: boolean;
306
+ layout?: 'auto' | 'fill' | 'section';
307
+ onToggle?: (open: boolean) => void;
308
+ open?: boolean;
309
+ title: ReactNode;
310
+ }
311
+
312
+ export function Collapsible({
313
+ children,
314
+ basis,
315
+ className,
316
+ defaultOpen = false,
317
+ layout = 'section',
318
+ onToggle,
319
+ open,
320
+ style,
321
+ title,
322
+ ...props
323
+ }: CollapsibleProps) {
324
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
325
+ const expanded = open ?? internalOpen;
326
+
327
+ function handleToggle() {
328
+ const next = !expanded;
329
+ if (open === undefined) setInternalOpen(next);
330
+ onToggle?.(next);
331
+ }
332
+
333
+ return (
334
+ <section
335
+ className={cx(
336
+ 'ui-collapsible',
337
+ layout === 'fill' && 'ui-collapsible--fill',
338
+ layout === 'section' && 'ui-collapsible--section',
339
+ !expanded && 'ui-collapsible--closed',
340
+ className,
341
+ )}
342
+ style={{ '--ui-collapsible-basis': basis, ...style } as CSSProperties}
343
+ {...props}
344
+ >
345
+ <button
346
+ aria-expanded={expanded}
347
+ className="ui-collapsible__trigger"
348
+ onClick={handleToggle}
349
+ type="button"
350
+ >
351
+ <i
352
+ aria-hidden
353
+ className={cxCodicon(`chevron-${expanded ? 'down' : 'right'}`, 'ui-collapsible__icon')}
354
+ />
355
+ <span className="ui-collapsible__title">{title}</span>
356
+ </button>
357
+ {expanded ? <div className="ui-collapsible__body">{children}</div> : null}
358
+ </section>
359
+ );
360
+ }
361
+
362
+ export interface TabbedPanelItem {
363
+ id: string;
364
+ label: ReactNode;
365
+ panel: ReactNode;
366
+ }
367
+
368
+ export interface TabbedPanelsProps extends Omit<ComponentPropsWithRef<'div'>, 'onSelect'> {
369
+ activeId?: string;
370
+ ariaLabel?: string;
371
+ items: readonly TabbedPanelItem[];
372
+ onSelect?: (id: string) => void;
373
+ }
374
+
375
+ export function TabbedPanels({
376
+ activeId,
377
+ ariaLabel,
378
+ className,
379
+ items,
380
+ onSelect,
381
+ ...props
382
+ }: TabbedPanelsProps) {
383
+ const [internalActiveId, setInternalActiveId] = useState<string | null>(null);
384
+ const selectedId = activeId ?? internalActiveId ?? items[0]?.id;
385
+ const activeItem = items.find((item) => item.id === selectedId) ?? items[0];
386
+
387
+ return (
388
+ <div className={cx('ui-tabbed-panels', className)} {...props}>
389
+ <div
390
+ aria-label={ariaLabel ?? props['aria-label'] ?? 'Panels'}
391
+ className="ui-tabbed-panels__tabs"
392
+ role="tablist"
393
+ >
394
+ {items.map((item) => {
395
+ const selected = item.id === activeItem?.id;
396
+ return (
397
+ <button
398
+ key={item.id}
399
+ aria-selected={selected}
400
+ className={cx('ui-tabbed-panels__tab', selected && 'ui-tabbed-panels__tab--active')}
401
+ id={`${item.id}:tab`}
402
+ onClick={() => {
403
+ setInternalActiveId(item.id);
404
+ onSelect?.(item.id);
405
+ }}
406
+ role="tab"
407
+ type="button"
408
+ >
409
+ <span className="ui-tabbed-panels__tab-label">{item.label}</span>
410
+ </button>
411
+ );
412
+ })}
413
+ </div>
414
+ <div
415
+ aria-labelledby={activeItem ? `${activeItem.id}:tab` : undefined}
416
+ className="ui-tabbed-panels__panel"
417
+ id={activeItem ? `${activeItem.id}:view` : undefined}
418
+ role="tabpanel"
419
+ >
420
+ {activeItem?.panel}
421
+ </div>
422
+ </div>
423
+ );
424
+ }