react-extjs 0.0.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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +36 -0
  3. package/src/components/AbsoluteLayout.tsx +206 -0
  4. package/src/components/Accordion.tsx +266 -0
  5. package/src/components/AnchorLayout.tsx +131 -0
  6. package/src/components/BorderLayout.tsx +512 -0
  7. package/src/components/Box.tsx +289 -0
  8. package/src/components/Button.tsx +300 -0
  9. package/src/components/ButtonGroup.tsx +170 -0
  10. package/src/components/CalendarPicker.tsx +431 -0
  11. package/src/components/CardLayout.tsx +122 -0
  12. package/src/components/CenterLayout.tsx +95 -0
  13. package/src/components/CheckboxGroup.tsx +200 -0
  14. package/src/components/ColorMenu.tsx +195 -0
  15. package/src/components/ColorPalette.tsx +126 -0
  16. package/src/components/ColumnLayout.tsx +183 -0
  17. package/src/components/ColumnTree.tsx +383 -0
  18. package/src/components/CycleButton.tsx +302 -0
  19. package/src/components/DataView.tsx +244 -0
  20. package/src/components/DataViewMore.tsx +337 -0
  21. package/src/components/DataViewTransition.tsx +235 -0
  22. package/src/components/DateMenu.tsx +163 -0
  23. package/src/components/DatePicker.tsx +387 -0
  24. package/src/components/DisplayField.tsx +73 -0
  25. package/src/components/Editor.tsx +214 -0
  26. package/src/components/EditorGrid.tsx +361 -0
  27. package/src/components/FieldLabeler.tsx +81 -0
  28. package/src/components/FieldReplicator.tsx +135 -0
  29. package/src/components/FieldSet.tsx +149 -0
  30. package/src/components/FitLayout.tsx +76 -0
  31. package/src/components/Form.tsx +3484 -0
  32. package/src/components/Grid.tsx +2173 -0
  33. package/src/components/GridFilters.tsx +332 -0
  34. package/src/components/GroupTab.tsx +274 -0
  35. package/src/components/HtmlEditor.tsx +391 -0
  36. package/src/components/ImageOrganizer.tsx +984 -0
  37. package/src/components/ImageOrganizerService.ts +361 -0
  38. package/src/components/ListView.tsx +221 -0
  39. package/src/components/LoadMask.tsx +74 -0
  40. package/src/components/Menu.tsx +227 -0
  41. package/src/components/MessageBox.tsx +138 -0
  42. package/src/components/MultiSlider.tsx +369 -0
  43. package/src/components/Organizer.tsx +403 -0
  44. package/src/components/PagingToolbar.tsx +255 -0
  45. package/src/components/Panel.tsx +451 -0
  46. package/src/components/ProgressBar.tsx +127 -0
  47. package/src/components/PropertyGrid.tsx +142 -0
  48. package/src/components/QuickTip.tsx +174 -0
  49. package/src/components/Resizable.tsx +241 -0
  50. package/src/components/RowLayout.tsx +126 -0
  51. package/src/components/Slider.tsx +353 -0
  52. package/src/components/SliderField.tsx +206 -0
  53. package/src/components/SpinnerField.tsx +141 -0
  54. package/src/components/SplitBar.tsx +215 -0
  55. package/src/components/Spotlight.tsx +262 -0
  56. package/src/components/StatusBar.tsx +118 -0
  57. package/src/components/TabPanel.tsx +540 -0
  58. package/src/components/TableLayout.tsx +132 -0
  59. package/src/components/ThemeSwitcher.tsx +167 -0
  60. package/src/components/ToolTip.tsx +226 -0
  61. package/src/components/Toolbar.tsx +321 -0
  62. package/src/components/Tree.tsx +809 -0
  63. package/src/components/TreeGrid.tsx +418 -0
  64. package/src/components/Viewport.tsx +218 -0
  65. package/src/components/Window.tsx +441 -0
  66. package/src/components/Writer.tsx +303 -0
  67. package/src/components/XmlTreeLoader.tsx +233 -0
  68. package/src/components/dnd/DndContext.tsx +328 -0
  69. package/src/components/dnd/DragSource.tsx +202 -0
  70. package/src/components/dnd/DragZone.tsx +66 -0
  71. package/src/components/dnd/DropTarget.tsx +86 -0
  72. package/src/components/dnd/DropZone.tsx +166 -0
  73. package/src/components/dnd/PanelProxy.tsx +81 -0
  74. package/src/components/dnd/PanelResizer.tsx +143 -0
  75. package/src/components/dnd/Portal.tsx +254 -0
  76. package/src/components/dnd/Reorderer.tsx +73 -0
  77. package/src/components/dnd/ToolbarReorderer.tsx +226 -0
  78. package/src/main.tsx +2649 -0
  79. package/src/styles/ext-theme.css +889 -0
  80. package/src/util/theme.ts +95 -0
@@ -0,0 +1,441 @@
1
+ import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle, type ReactNode } from 'react';
2
+ import { Toolbar } from './Toolbar';
3
+
4
+ /**
5
+ * Window —— 保真复刻 Ext.Window / Ext.Panel(frame) 的 9 宫格结构。
6
+ *
7
+ * 保真契约(DOM/class 严格对齐 Ext 3.2.1 运行时产出,由 dump golden 校验):
8
+ * - 根:x-window(position:absolute;plain 时加 x-window-plain;resizable 时加 x-resizable-pinned;
9
+ * maximized 时加 x-window-maximized)
10
+ * - 顶部:x-window-tl > x-window-tr > x-window-tc > x-window-header.x-unselectable
11
+ * header 内顺序(对 golden 实测):x-tool x-tool-close → x-tool x-tool-restore(hidden)
12
+ * → x-tool x-tool-maximize → span.x-window-header-text
13
+ * (注意:Window 的 close 工具在文本「之前」;maximize 图标在 close 左侧、视觉靠右)
14
+ * - bwrap:x-window-bwrap > x-window-ml > x-window-mr > x-window-mc > x-window-body(带 inline 尺寸)
15
+ * - 底部:x-window-bl > x-window-br > x-window-bc > x-window-footer.x-panel-btns
16
+ * > Toolbar(根类 x-panel-fbar x-small-editor x-toolbar-layout-ct,按钮右对齐) + x-clear
17
+ * - 末尾:a.x-dlg-focus(focus 锚点)
18
+ * - resizable:根追加 8 个透明缩放手柄(x-resizable-handle-{edge} x-unselectable x-window-handle,
19
+ * opacity:0,不影响可见像素;edge = north/south/east/west/northeast/northwest/southeast/southwest)
20
+ * - modal:容器内含 .ext-el-mask 兄弟节点(在窗口之前),bg rgb(204,204,204) opacity .5,z-index 9020,
21
+ * 视觉被容器 overflow:hidden 裁剪(对齐 Ext 的 modal mask)
22
+ *
23
+ * Ext 运行时注入的 inline 尺寸(必须复刻,否则像素错位):
24
+ * - x-window-body: width = width - 14(左右 frame 7+7);height = height - 66(header25+footer39+frameV2)
25
+ */
26
+ export interface WindowButton {
27
+ text: string;
28
+ disabled?: boolean;
29
+ onClick?: () => void;
30
+ /** 显式宽度(如 MessageBox 按钮固定 75px) */
31
+ width?: number;
32
+ }
33
+
34
+ export interface WindowProps {
35
+ title?: string;
36
+ width?: number;
37
+ height?: number;
38
+ /** 容器内的 left(默认 0) */
39
+ left?: number;
40
+ /** 容器内的 top(默认 0) */
41
+ top?: number;
42
+ /** 显示关闭按钮(默认 true) */
43
+ closable?: boolean;
44
+ /** 透明 body 背景(默认 false,蓝灰渐变) */
45
+ plain?: boolean;
46
+ /** 底部按钮(渲染为右对齐 Toolbar) */
47
+ buttons?: WindowButton[];
48
+ onClose?: () => void;
49
+ /** 可最大化:头部渲染 maximize/restore 工具,点击切换 */
50
+ maximizable?: boolean;
51
+ onMaximize?: () => void;
52
+ onRestore?: () => void;
53
+ /** 可最小化:头部渲染 minimize 工具,点击折叠收起为仅标题栏(对齐 Ext 默认 minimize → fireEvent onMinimize,不整窗消失);标题栏展开工具或句柄 expand 可恢复 */
54
+ minimizable?: boolean;
55
+ onMinimize?: () => void;
56
+ /** 可缩放:根追加 8 个透明缩放手柄(结构保真;拖拽缩放交互见下) */
57
+ resizable?: boolean;
58
+ /** 可拖拽移动(默认 true,对齐 Ext.Window.draggable)。点击 header(非工具区)拖动 */
59
+ draggable?: boolean;
60
+ /** 最小宽/高约束(拖拽缩放时生效,避免 body 尺寸为负) */
61
+ minWidth?: number;
62
+ minHeight?: number;
63
+ /** 模态:渲染 .ext-el-mask 遮罩(覆盖容器) */
64
+ modal?: boolean;
65
+ children?: React.ReactNode;
66
+ style?: React.CSSProperties;
67
+ className?: string;
68
+ /** 高度自适应(如 MessageBox):根与 body 不固定高度,由内容撑开 */
69
+ autoHeight?: boolean;
70
+ /** 底部按钮水平对齐:默认 right(Window 习惯);MessageBox 用 center */
71
+ footerAlign?: 'left' | 'center' | 'right';
72
+ /** 初始可见性(默认 true,对齐 Ext Window 默认渲染即显示);运行时可由句柄 open/close 改写 */
73
+ visible?: boolean;
74
+ }
75
+ /** 命令式句柄:对齐 Ext.Window 的运行时 show/hide/setTitle/maximize/restore 与动态内容替换 */
76
+ export interface WindowHandle {
77
+ open: () => void;
78
+ close: () => void;
79
+ setTitle: (t: string) => void;
80
+ setContent: (node: ReactNode) => void;
81
+ maximize: () => void;
82
+ restore: () => void;
83
+ /** 最小化:折叠收起为仅标题栏(对齐 Ext 默认 minimize → fireEvent onMinimize,默认不整窗消失) */
84
+ minimize: () => void;
85
+ /** 取消最小化:恢复正文与底部工具条 */
86
+ expand: () => void;
87
+ isVisible: () => boolean;
88
+ }
89
+
90
+ // Ext 运行时注入的 frame 常数(对 golden 实测得出)
91
+ // 常规窗口 body inline 宽 = width - 14(左右 frame 7+7,body 无 padding)。
92
+ // plain 窗口(如 MessageBox)body 带左右 padding(10+10) → inline 宽 = width - 32,
93
+ // 其 offsetWidth(=inline+20) 正好填满 .x-window-mr(=width-12),无溢出(普通窗口 body 无 padding 故恰好窄 2px,对齐 Ext)。
94
+ const BODY_W_OFFSET = 14;
95
+ const BODY_H_OFFSET = 66; // header(25) + footer(39) + 上下 frame(2)
96
+
97
+ // resizable 8 向手柄(对 golden 实测:Ext 默认生成全部 8 个,opacity:0 透明)
98
+ const RESIZE_EDGES = [
99
+ 'north', 'south', 'east', 'west',
100
+ 'northeast', 'northwest', 'southeast', 'southwest',
101
+ ];
102
+
103
+ // 各边手柄的光标(对齐 Ext.Resizable 的 handle cursor)
104
+ const HANDLE_CURSOR: Record<string, string> = {
105
+ north: 'ns-resize', south: 'ns-resize',
106
+ east: 'ew-resize', west: 'ew-resize',
107
+ northeast: 'nesw-resize', southwest: 'nesw-resize',
108
+ northwest: 'nwse-resize', southeast: 'nwse-resize',
109
+ };
110
+
111
+ export const Window = forwardRef<WindowHandle, WindowProps>(
112
+ (
113
+ {
114
+ title,
115
+ width = 400,
116
+ height = 220,
117
+ left = 0,
118
+ top = 0,
119
+ closable = true,
120
+ plain = false,
121
+ buttons,
122
+ onClose,
123
+ maximizable = false,
124
+ onMaximize,
125
+ onRestore,
126
+ minimizable = false,
127
+ onMinimize,
128
+ resizable = false,
129
+ draggable = true,
130
+ minWidth = 100,
131
+ minHeight = 100,
132
+ modal = false,
133
+ children,
134
+ style,
135
+ className,
136
+ autoHeight = false,
137
+ footerAlign = 'right',
138
+ visible = true,
139
+ },
140
+ ref,
141
+ ) => {
142
+ const rootRef = useRef<HTMLDivElement>(null);
143
+ // 几何状态:maximize 时填满容器,restore 时还原
144
+ const [geom, setGeom] = useState({ width, height, left, top });
145
+ const [maximized, setMaximized] = useState(false);
146
+ // 最小化:对齐 Ext 默认 minimize 仅 fireEvent(不隐藏窗口),这里用「折叠收起」呈现可见的最小化形态
147
+ const [minimized, setMinimized] = useState(false);
148
+ // 双轨动态态:visible/title/content 既接受静态 prop,也支持运行时通过句柄改写。
149
+ const [dynVisible, setDynVisible] = useState(visible);
150
+ const [dynTitle, setDynTitle] = useState<string | undefined>(undefined);
151
+ const [dynContent, setDynContent] = useState<ReactNode | undefined>(undefined);
152
+ // 同步静态 prop 变化(演示中主要通过句柄改写,这里保证 prop 驱动也生效)
153
+ useEffect(() => setDynVisible(visible), [visible]);
154
+
155
+ const handleMaximize = () => {
156
+ const p = rootRef.current?.parentElement;
157
+ if (p) {
158
+ setGeom({ width: p.clientWidth, height: p.clientHeight, left: 0, top: 0 });
159
+ }
160
+ setMaximized(true);
161
+ setMinimized(false);
162
+ onMaximize?.();
163
+ };
164
+ const handleRestore = () => {
165
+ setGeom({ width, height, left, top });
166
+ setMaximized(false);
167
+ setMinimized(false);
168
+ onRestore?.();
169
+ };
170
+ const handleMinimize = () => {
171
+ // 对齐 Ext 默认 minimize:仅触发 onMinimize 回调(fireEvent),窗口折叠收起为仅标题栏,不整窗消失
172
+ setMinimized(true);
173
+ onMinimize?.();
174
+ };
175
+ const handleExpand = () => {
176
+ // 取消最小化:恢复正文与底部工具条
177
+ setMinimized(false);
178
+ };
179
+
180
+ useImperativeHandle(ref, () => ({
181
+ open: () => setDynVisible(true),
182
+ close: () => setDynVisible(false),
183
+ setTitle: (t: string) => setDynTitle(t),
184
+ setContent: (node: ReactNode) => setDynContent(node),
185
+ maximize: () => handleMaximize(),
186
+ restore: () => handleRestore(),
187
+ minimize: () => handleMinimize(),
188
+ expand: () => handleExpand(),
189
+ isVisible: () => dynVisible,
190
+ }));
191
+ // 拖拽描述:null 表示空闲;move 为移动,resize 带 edge
192
+ const dragRef = useRef<null | {
193
+ type: 'move' | 'resize';
194
+ edge?: string;
195
+ startX: number;
196
+ startY: number;
197
+ start: { width: number; height: number; left: number; top: number };
198
+ }>(null);
199
+
200
+ // 拖拽过程中绑定到 window 的监听(move 改几何,up 解绑)
201
+ const attachDragListeners = () => {
202
+ const onMove = (e: MouseEvent) => {
203
+ const d = dragRef.current;
204
+ if (!d) return;
205
+ const dx = e.clientX - d.startX;
206
+ const dy = e.clientY - d.startY;
207
+ if (d.type === 'move') {
208
+ setGeom((g) => ({ ...g, left: d.start.left + dx, top: d.start.top + dy }));
209
+ return;
210
+ }
211
+ // resize:按 edge 单边/双边更新,带最小尺寸约束
212
+ const edge = d.edge!;
213
+ const start = d.start;
214
+ let { width, height, left, top } = start;
215
+ const has = (s: string) => edge.includes(s);
216
+ if (has('east')) width = Math.max(minWidth, start.width + dx);
217
+ if (has('south')) height = Math.max(minHeight, start.height + dy);
218
+ if (has('west')) {
219
+ const w = Math.max(minWidth, start.width - dx);
220
+ width = w;
221
+ left = start.left + (start.width - w);
222
+ }
223
+ if (has('north')) {
224
+ const h = Math.max(minHeight, start.height - dy);
225
+ height = h;
226
+ top = start.top + (start.height - h);
227
+ }
228
+ setGeom({ width, height, left, top });
229
+ };
230
+ const onUp = () => {
231
+ dragRef.current = null;
232
+ window.removeEventListener('mousemove', onMove);
233
+ window.removeEventListener('mouseup', onUp);
234
+ };
235
+ window.addEventListener('mousemove', onMove);
236
+ window.addEventListener('mouseup', onUp);
237
+ };
238
+
239
+ const onHeaderMouseDown = (e: React.MouseEvent) => {
240
+ if (!draggable || maximized) return;
241
+ // 工具区(close/maximize/restore)点击不触发拖拽,交给工具 handler
242
+ if ((e.target as HTMLElement).closest('.x-tool')) return;
243
+ e.preventDefault();
244
+ dragRef.current = {
245
+ type: 'move',
246
+ startX: e.clientX,
247
+ startY: e.clientY,
248
+ start: { ...geom },
249
+ };
250
+ attachDragListeners();
251
+ };
252
+
253
+ const onHandleMouseDown = (e: React.MouseEvent, edge: string) => {
254
+ if (!resizable || maximized) return;
255
+ e.preventDefault();
256
+ e.stopPropagation();
257
+ dragRef.current = {
258
+ type: 'resize',
259
+ edge,
260
+ startX: e.clientX,
261
+ startY: e.clientY,
262
+ start: { ...geom },
263
+ };
264
+ attachDragListeners();
265
+ };
266
+
267
+ const rootCls = [
268
+ 'x-window',
269
+ plain ? 'x-window-plain' : '',
270
+ resizable ? 'x-resizable-pinned' : '',
271
+ maximized ? 'x-window-maximized' : '',
272
+ // 最小化:复用 Ext 折叠类 x-panel-collapsed,触发 ext-all.css 内置
273
+ // `.x-panel-collapsed .x-window-tl { border-bottom:1px solid }` 闭合标题栏(最外层顶框横跨全宽,含圆角区,不断裂)。
274
+ minimized ? 'x-panel-collapsed' : '',
275
+ className,
276
+ ]
277
+ .filter(Boolean)
278
+ .join(' ');
279
+
280
+ const headerEl = (
281
+ <div
282
+ className="x-window-header x-unselectable"
283
+ unselectable="on"
284
+ onMouseDown={onHeaderMouseDown}
285
+ style={{ cursor: draggable && !maximized && !minimized ? 'move' : undefined }}
286
+ >
287
+ {closable && (
288
+ <div className="x-tool x-tool-close" unselectable="on" onClick={onClose}>
289
+ {' '}
290
+ </div>
291
+ )}
292
+ {(maximizable || minimizable) && (
293
+ <div
294
+ className="x-tool x-tool-restore"
295
+ unselectable="on"
296
+ style={{ display: maximized || minimized ? 'block' : 'none' }}
297
+ onClick={maximized ? handleRestore : handleExpand}
298
+ >
299
+ {' '}
300
+ </div>
301
+ )}
302
+ {maximizable && (
303
+ <div
304
+ className="x-tool x-tool-maximize"
305
+ unselectable="on"
306
+ style={{ display: maximized ? 'none' : 'block' }}
307
+ onClick={handleMaximize}
308
+ >
309
+ {' '}
310
+ </div>
311
+ )}
312
+ {minimizable && (
313
+ <div
314
+ className="x-tool x-tool-minimize"
315
+ unselectable="on"
316
+ style={{ display: minimized ? 'none' : 'block' }}
317
+ onClick={handleMinimize}
318
+ >
319
+ {' '}
320
+ </div>
321
+ )}
322
+ {title && <span className="x-window-header-text">{dynTitle ?? title}</span>}
323
+ </div>
324
+ );
325
+
326
+ const bodyWidth = geom.width - (plain ? 32 : BODY_W_OFFSET);
327
+ // 无 footer 时不减 footer 高度,并改用更矮的底部圆角收口偏移(header + 底部 frame)
328
+ const bodyHeight = geom.height - (buttons?.length ? BODY_H_OFFSET : 33);
329
+
330
+ // 底部圆角框架始终渲染:对齐 Ext,无论有无 footer,窗口底部都需 x-window-bl/br/bc 收口。
331
+ // 无 footer 内容时加 x-panel-nofooter(CSS 将其压成 6px 圆角底);有 footer 时在其中渲染按钮区。
332
+ // 最小化(收起)时 footer 不渲染,故同样视为无 footer → 加 x-panel-nofooter 收口,避免底部框架塌成 0 高、边框断裂。
333
+ const noFooter = !buttons?.length || minimized;
334
+ const bottomFrame = (
335
+ <div className={`x-window-bl${noFooter ? ' x-panel-nofooter' : ''}`}>
336
+ <div className="x-window-br">
337
+ <div className="x-window-bc">
338
+ {buttons?.length && !minimized && (
339
+ <div className="x-window-footer x-panel-btns">
340
+ <Toolbar
341
+ rootClass="x-panel-fbar x-small-editor x-toolbar-layout-ct"
342
+ buttonAlign={footerAlign === 'right' ? 'left' : footerAlign}
343
+ items={
344
+ footerAlign === 'right'
345
+ ? ['->', ...buttons.map((b) => ({ text: b.text, disabled: b.disabled, onClick: b.onClick, width: b.width }))]
346
+ : buttons.map((b) => ({ text: b.text, disabled: b.disabled, onClick: b.onClick, width: b.width }))
347
+ }
348
+ />
349
+ <div className="x-clear" />
350
+ </div>
351
+ )}
352
+ </div>
353
+ </div>
354
+ </div>
355
+ );
356
+
357
+ const resizeHandles = resizable && !minimized
358
+ ? RESIZE_EDGES.map((edge) => (
359
+ <div
360
+ key={edge}
361
+ className={`x-resizable-handle x-resizable-handle-${edge} x-unselectable x-window-handle`}
362
+ unselectable="on"
363
+ onMouseDown={(e) => onHandleMouseDown(e, edge)}
364
+ style={{ cursor: HANDLE_CURSOR[edge] }}
365
+ >
366
+ {' '}
367
+ </div>
368
+ ))
369
+ : null;
370
+
371
+ // 可见性双轨:dynVisible=false 时不渲染(含 modal 遮罩一并消失),对齐 Ext Window.hide()。
372
+ if (!dynVisible) return null;
373
+
374
+ const windowEl = (
375
+ <div
376
+ ref={rootRef}
377
+ className={rootCls}
378
+ style={{
379
+ position: 'absolute',
380
+ left: geom.left,
381
+ top: geom.top,
382
+ width: geom.width,
383
+ ...(minimized ? { height: 'auto' } : autoHeight ? {} : { height: geom.height }),
384
+ ...style,
385
+ }}
386
+ >
387
+ <div className="x-window-tl">
388
+ <div className="x-window-tr">
389
+ <div className="x-window-tc">{headerEl}</div>
390
+ </div>
391
+ </div>
392
+ {!minimized && (
393
+ <div className="x-window-bwrap">
394
+ <div className="x-window-ml">
395
+ <div className="x-window-mr">
396
+ <div className="x-window-mc">
397
+ <div
398
+ className="x-window-body"
399
+ style={{
400
+ width: bodyWidth,
401
+ ...(autoHeight ? {} : { height: bodyHeight }),
402
+ }}
403
+ >
404
+ {dynContent ?? children}
405
+ </div>
406
+ </div>
407
+ </div>
408
+ </div>
409
+ {bottomFrame}
410
+ </div>
411
+ )}
412
+ <a href="#" className="x-dlg-focus" tabIndex={-1}>
413
+ {' '}
414
+ </a>
415
+ {resizeHandles}
416
+ </div>
417
+ );
418
+
419
+ // modal:渲染 .ext-el-mask 遮罩作为兄弟节点(在窗口之前),覆盖容器
420
+ if (modal) {
421
+ const maskEl = (
422
+ <div
423
+ className="ext-el-mask"
424
+ style={{
425
+ position: 'absolute',
426
+ left: 0,
427
+ top: 0,
428
+ width: '100%',
429
+ height: '100%',
430
+ zIndex: 9020,
431
+ }}
432
+ />
433
+ );
434
+ return <>{maskEl}{windowEl}</>;
435
+ }
436
+
437
+ return windowEl;
438
+ });
439
+ Window.displayName = 'Window';
440
+
441
+ export default Window;