@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,53 +1,61 @@
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
- }
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
+ confirmDisabled?: boolean;
17
+ confirmPending?: boolean;
18
+ variant?: ConfirmDialogVariant;
19
+ onCancel: () => void;
20
+ onConfirm: () => void;
21
+ }
22
+
23
+ export function ConfirmDialog({
24
+ cancelLabel = 'Cancel',
25
+ className,
26
+ closeLabel = 'Close confirmation dialog',
27
+ confirmDisabled = false,
28
+ confirmLabel = 'Confirm',
29
+ confirmPending = false,
30
+ detail,
31
+ message,
32
+ title,
33
+ variant = 'default',
34
+ onCancel,
35
+ onConfirm,
36
+ }: ConfirmDialogProps) {
37
+ return (
38
+ <Modal
39
+ title={title}
40
+ closeLabel={closeLabel}
41
+ className={cx('ui-confirm-dialog', className)}
42
+ bodyClassName="ui-confirm-dialog__body"
43
+ onClose={onCancel}
44
+ footer={
45
+ <>
46
+ <Button onClick={onCancel}>{cancelLabel}</Button>
47
+ <Button
48
+ disabled={confirmDisabled || confirmPending}
49
+ variant={variant === 'danger' ? 'danger' : 'primary'}
50
+ onClick={onConfirm}
51
+ >
52
+ {confirmPending ? 'Working…' : confirmLabel}
53
+ </Button>
54
+ </>
55
+ }
56
+ >
57
+ <p className="ui-confirm-dialog__message">{message}</p>
58
+ {detail ? <div className="ui-confirm-dialog__detail">{detail}</div> : null}
59
+ </Modal>
60
+ );
61
+ }
@@ -1,17 +1,45 @@
1
- import { useId, type FormEventHandler, type ReactNode } from 'react';
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useId,
5
+ useRef,
6
+ useState,
7
+ type CSSProperties,
8
+ type FormEventHandler,
9
+ type PointerEvent as ReactPointerEvent,
10
+ type ReactNode,
11
+ } from 'react';
2
12
  import { cx } from '../utils/cx';
3
13
 
14
+ export interface ModalPosition {
15
+ x: number;
16
+ y: number;
17
+ }
18
+
4
19
  export interface ModalProps {
5
20
  title: ReactNode;
6
- titleSuffix?: ReactNode;
21
+ titleSuffix?: ReactNode | undefined;
7
22
  children: ReactNode;
8
- footer?: ReactNode;
9
- className?: string;
10
- bodyClassName?: string;
11
- closeLabel?: string;
12
- labelledBy?: string;
23
+ footer?: ReactNode | undefined;
24
+ className?: string | undefined;
25
+ bodyClassName?: string | undefined;
26
+ closeLabel?: string | undefined;
27
+ defaultMaximized?: boolean | undefined;
28
+ labelledBy?: string | undefined;
29
+ maximizable?: boolean | undefined;
30
+ maximizeLabel?: string | undefined;
31
+ movable?: boolean | undefined;
13
32
  onClose: () => void;
14
- onSubmit?: FormEventHandler<HTMLFormElement>;
33
+ onSubmit?: FormEventHandler<HTMLFormElement> | undefined;
34
+ restoreLabel?: string | undefined;
35
+ }
36
+
37
+ interface ModalDragState {
38
+ pointerId: number;
39
+ rect: DOMRect;
40
+ startClientX: number;
41
+ startClientY: number;
42
+ startPosition: ModalPosition;
15
43
  }
16
44
 
17
45
  function ModalContent({
@@ -22,18 +50,63 @@ function ModalContent({
22
50
  bodyClassName,
23
51
  closeLabel = 'Close modal',
24
52
  labelledBy,
53
+ maximizable,
54
+ maximized,
55
+ maximizeLabel = 'Maximize modal',
56
+ movable,
25
57
  onClose,
26
- }: Omit<ModalProps, 'className' | 'onSubmit'>) {
58
+ onTitlebarDoubleClick,
59
+ onTitlebarPointerDown,
60
+ onToggleMaximized,
61
+ restoreLabel = 'Restore modal',
62
+ }: Omit<ModalProps, 'className' | 'defaultMaximized' | 'onSubmit'> & {
63
+ maximized: boolean;
64
+ onTitlebarDoubleClick: () => void;
65
+ onTitlebarPointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;
66
+ onToggleMaximized: () => void;
67
+ }) {
68
+ const maximizeTitle = maximized ? restoreLabel : maximizeLabel;
69
+
27
70
  return (
28
71
  <>
29
- <div className="ui-modal__titlebar">
72
+ <div
73
+ className="ui-modal__titlebar"
74
+ data-draggable={movable && !maximized ? 'true' : undefined}
75
+ onDoubleClick={onTitlebarDoubleClick}
76
+ onPointerDown={onTitlebarPointerDown}
77
+ >
30
78
  <span id={labelledBy} className="ui-modal__title">
31
79
  {title}
32
80
  {titleSuffix}
33
81
  </span>
34
- <button type="button" className="ui-modal__close" aria-label={closeLabel} onClick={onClose}>
35
- <i className="codicon codicon-close" />
36
- </button>
82
+ <div className="ui-modal__controls">
83
+ {maximizable ? (
84
+ <button
85
+ type="button"
86
+ className="ui-modal__control"
87
+ aria-label={maximizeTitle}
88
+ title={maximizeTitle}
89
+ onClick={onToggleMaximized}
90
+ >
91
+ <i
92
+ aria-hidden="true"
93
+ className={cx(
94
+ 'codicon',
95
+ maximized ? 'codicon-chrome-restore' : 'codicon-chrome-maximize',
96
+ )}
97
+ />
98
+ </button>
99
+ ) : null}
100
+ <button
101
+ type="button"
102
+ className="ui-modal__control ui-modal__close"
103
+ aria-label={closeLabel}
104
+ title={closeLabel}
105
+ onClick={onClose}
106
+ >
107
+ <i aria-hidden="true" className="codicon codicon-close" />
108
+ </button>
109
+ </div>
37
110
  </div>
38
111
  <div className={cx('ui-modal__body', bodyClassName)}>{children}</div>
39
112
  {footer && <div className="ui-modal__footer">{footer}</div>}
@@ -49,36 +122,185 @@ export function Modal({
49
122
  className,
50
123
  bodyClassName,
51
124
  closeLabel,
125
+ defaultMaximized = false,
52
126
  labelledBy,
127
+ maximizable = false,
128
+ maximizeLabel,
129
+ movable = false,
53
130
  onClose,
54
131
  onSubmit,
132
+ restoreLabel,
55
133
  }: ModalProps) {
56
134
  const modalClassName = cx('ui-modal', className);
57
135
  const generatedLabelId = useId();
58
136
  const resolvedLabelledBy = labelledBy ?? generatedLabelId;
137
+ const modalRef = useRef<HTMLDivElement | HTMLFormElement>(null);
138
+ const dragStateRef = useRef<ModalDragState | null>(null);
139
+ const [position, setPosition] = useState<ModalPosition>({ x: 0, y: 0 });
140
+ const [dragging, setDragging] = useState(false);
141
+ const [maximized, setMaximized] = useState(defaultMaximized);
142
+ const modalStyle = createModalPositionStyle(position, maximized);
143
+
144
+ const handleToggleMaximized = useCallback(() => {
145
+ if (!maximizable) {
146
+ return;
147
+ }
148
+
149
+ setMaximized((current) => !current);
150
+ setDragging(false);
151
+ }, [maximizable]);
152
+
153
+ const handleFormRef = useCallback((node: HTMLFormElement | null) => {
154
+ modalRef.current = node;
155
+ }, []);
156
+
157
+ const handleFrameRef = useCallback((node: HTMLDivElement | null) => {
158
+ modalRef.current = node;
159
+ }, []);
160
+
161
+ const handleTitlebarDoubleClick = useCallback(() => {
162
+ handleToggleMaximized();
163
+ }, [handleToggleMaximized]);
164
+
165
+ const handleTitlebarPointerDown = useCallback(
166
+ (event: ReactPointerEvent<HTMLDivElement>) => {
167
+ if (!movable || maximized || event.button !== 0 || isModalTitlebarControl(event.target)) {
168
+ return;
169
+ }
170
+
171
+ const modal = modalRef.current;
172
+ if (!modal) {
173
+ return;
174
+ }
175
+
176
+ dragStateRef.current = {
177
+ pointerId: event.pointerId,
178
+ rect: modal.getBoundingClientRect(),
179
+ startClientX: event.clientX,
180
+ startClientY: event.clientY,
181
+ startPosition: position,
182
+ };
183
+
184
+ event.preventDefault();
185
+ try {
186
+ event.currentTarget.setPointerCapture(event.pointerId);
187
+ } catch {
188
+ // Some test and embedded browser environments do not expose pointer capture.
189
+ }
190
+ setDragging(true);
191
+ },
192
+ [maximized, movable, position],
193
+ );
194
+
195
+ useEffect(() => {
196
+ if (!dragging) {
197
+ return undefined;
198
+ }
199
+
200
+ const previousCursor = document.body.style.cursor;
201
+ const previousUserSelect = document.body.style.userSelect;
202
+
203
+ document.body.style.cursor = 'move';
204
+ document.body.style.userSelect = 'none';
205
+
206
+ const handlePointerMove = (event: PointerEvent) => {
207
+ const dragState = dragStateRef.current;
208
+ if (!dragState || event.pointerId !== dragState.pointerId) {
209
+ return;
210
+ }
211
+
212
+ event.preventDefault();
213
+ const nextPosition = {
214
+ x: dragState.startPosition.x + event.clientX - dragState.startClientX,
215
+ y: dragState.startPosition.y + event.clientY - dragState.startClientY,
216
+ };
217
+
218
+ setPosition(clampModalPosition(nextPosition, dragState.startPosition, dragState.rect));
219
+ };
220
+
221
+ const handlePointerUp = (event: PointerEvent) => {
222
+ const dragState = dragStateRef.current;
223
+ if (dragState && event.pointerId !== dragState.pointerId) {
224
+ return;
225
+ }
226
+
227
+ dragStateRef.current = null;
228
+ setDragging(false);
229
+ };
230
+
231
+ window.addEventListener('pointermove', handlePointerMove, { passive: false });
232
+ window.addEventListener('pointerup', handlePointerUp);
233
+ window.addEventListener('pointercancel', handlePointerUp);
234
+
235
+ return () => {
236
+ document.body.style.cursor = previousCursor;
237
+ document.body.style.userSelect = previousUserSelect;
238
+ window.removeEventListener('pointermove', handlePointerMove);
239
+ window.removeEventListener('pointerup', handlePointerUp);
240
+ window.removeEventListener('pointercancel', handlePointerUp);
241
+ };
242
+ }, [dragging]);
243
+
244
+ useEffect(() => {
245
+ if (typeof window === 'undefined' || maximized) {
246
+ return undefined;
247
+ }
248
+
249
+ const handleResize = () => {
250
+ const modal = modalRef.current;
251
+ if (!modal) {
252
+ return;
253
+ }
254
+
255
+ setPosition((current) => clampModalPosition(current, current, modal.getBoundingClientRect()));
256
+ };
257
+
258
+ window.addEventListener('resize', handleResize);
259
+
260
+ return () => {
261
+ window.removeEventListener('resize', handleResize);
262
+ };
263
+ }, [maximized]);
264
+
265
+ const contentProps = {
266
+ title,
267
+ titleSuffix,
268
+ bodyClassName,
269
+ closeLabel,
270
+ labelledBy: resolvedLabelledBy,
271
+ maximizable,
272
+ maximized,
273
+ maximizeLabel,
274
+ movable,
275
+ onClose,
276
+ onTitlebarDoubleClick: handleTitlebarDoubleClick,
277
+ onTitlebarPointerDown: handleTitlebarPointerDown,
278
+ onToggleMaximized: handleToggleMaximized,
279
+ restoreLabel,
280
+ footer,
281
+ } satisfies Omit<ModalProps, 'className' | 'children' | 'defaultMaximized' | 'onSubmit'> & {
282
+ maximized: boolean;
283
+ onTitlebarDoubleClick: () => void;
284
+ onTitlebarPointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;
285
+ onToggleMaximized: () => void;
286
+ };
59
287
 
60
288
  if (onSubmit) {
61
289
  return (
62
290
  <div className="ui-modal-overlay" onClick={onClose}>
63
291
  <form
292
+ ref={handleFormRef}
64
293
  className={modalClassName}
65
294
  role="dialog"
66
295
  aria-modal="true"
67
296
  aria-labelledby={resolvedLabelledBy}
297
+ data-dragging={dragging ? 'true' : undefined}
298
+ data-maximized={maximized ? 'true' : undefined}
68
299
  onClick={(event) => event.stopPropagation()}
69
300
  onSubmit={onSubmit}
301
+ style={modalStyle}
70
302
  >
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>
303
+ <ModalContent {...contentProps}>{children}</ModalContent>
82
304
  </form>
83
305
  </div>
84
306
  );
@@ -87,24 +309,62 @@ export function Modal({
87
309
  return (
88
310
  <div className="ui-modal-overlay" onClick={onClose}>
89
311
  <div
312
+ ref={handleFrameRef}
90
313
  className={modalClassName}
91
314
  role="dialog"
92
315
  aria-modal="true"
93
316
  aria-labelledby={resolvedLabelledBy}
317
+ data-dragging={dragging ? 'true' : undefined}
318
+ data-maximized={maximized ? 'true' : undefined}
94
319
  onClick={(event) => event.stopPropagation()}
320
+ style={modalStyle}
95
321
  >
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>
322
+ <ModalContent {...contentProps}>{children}</ModalContent>
107
323
  </div>
108
324
  </div>
109
325
  );
110
326
  }
327
+
328
+ function createModalPositionStyle(
329
+ position: ModalPosition,
330
+ maximized: boolean,
331
+ ): CSSProperties | undefined {
332
+ if (maximized || (position.x === 0 && position.y === 0)) {
333
+ return undefined;
334
+ }
335
+
336
+ return {
337
+ transform: `translate3d(${position.x}px, ${position.y}px, 0)`,
338
+ };
339
+ }
340
+
341
+ function clampModalPosition(
342
+ nextPosition: ModalPosition,
343
+ basePosition: ModalPosition,
344
+ rect: DOMRect,
345
+ ): ModalPosition {
346
+ if (typeof window === 'undefined') {
347
+ return nextPosition;
348
+ }
349
+
350
+ const padding = 8;
351
+ const deltaX = nextPosition.x - basePosition.x;
352
+ const deltaY = nextPosition.y - basePosition.y;
353
+ const minDeltaX = padding - rect.left;
354
+ const maxDeltaX = window.innerWidth - padding - rect.right;
355
+ const minDeltaY = padding - rect.top;
356
+ const maxDeltaY = window.innerHeight - padding - rect.bottom;
357
+
358
+ return {
359
+ x: basePosition.x + clamp(deltaX, minDeltaX, maxDeltaX),
360
+ y: basePosition.y + clamp(deltaY, minDeltaY, maxDeltaY),
361
+ };
362
+ }
363
+
364
+ function clamp(value: number, min: number, max: number): number {
365
+ return Math.min(Math.max(value, min), max);
366
+ }
367
+
368
+ function isModalTitlebarControl(target: EventTarget | null): boolean {
369
+ return target instanceof Element && Boolean(target.closest('.ui-modal__controls'));
370
+ }