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,321 @@
1
+ import React, { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react';
2
+ import { Button, type ButtonProps } from './Button';
3
+ import { type MenuItem } from './Menu';
4
+
5
+ /**
6
+ * Toolbar —— 保真复刻 Ext.Toolbar(src/widgets/Toolbar.js + layout/ToolbarLayout.js)。
7
+ *
8
+ * 保真契约(DOM/class 严格对齐源码):
9
+ * - 根:x-toolbar x-small-editor x-toolbar-layout-ct
10
+ * (onRender 加 x-small-editor;ToolbarLayout.onLayout 给 target 加 x-toolbar-layout-ct)
11
+ * - 布局表(tableHTML,ToolbarLayout 47-83 行):
12
+ * table.x-toolbar-ct
13
+ * tbody > tr
14
+ * td.x-toolbar-left[align=left|center] ← buttonAlign 决定 align
15
+ * table > tbody > tr.x-toolbar-left-row
16
+ * td.x-toolbar-cell(每个 item) ← insertCell 165 行
17
+ * td.x-toolbar-right[align=right]
18
+ * table.x-toolbar-right-ct
19
+ * tbody > tr > td
20
+ * table > tbody > tr.x-toolbar-right-row(fill '->' 之后的 item)
21
+ * td
22
+ * table > tbody > tr.x-toolbar-extras-row(overflow 'more' 按钮,默认空)
23
+ * - item 类型(lookupComponent 197-221 行):
24
+ * '-' / {xtype:'tbseparator'} → span.xtb-sep (Separator.onRender 462)
25
+ * ' ' / {xtype:'tbspacer'} → div.xtb-spacer[style width](Spacer.onRender 500)
26
+ * '->' / {xtype:'tbfill'} → 切换右侧容器(Fill 不渲染,只作为分隔标记)
27
+ * 字符串 / {xtype:'tbtext'} → div.xtb-text (TextItem.onRender 558)
28
+ * {xtype:'button'|'tbbutton'} 或带 text 的对象 → 渲染 Button 组件
29
+ * ReactNode(字段/任意元素) → 直接放入 td.x-toolbar-cell
30
+ *
31
+ * 交互(行为契约):
32
+ * - 按钮点击/菜单:委托给 Button 组件(与 buttons 用例同一实现)
33
+ * - '->' 之后项目右对齐(rightTr),之前项目按 buttonAlign 左/居中
34
+ */
35
+ export type ToolbarAlign = 'left' | 'center' | 'right';
36
+
37
+ export interface ToolbarButtonConfig extends Omit<ButtonProps, 'text'> {
38
+ text?: string;
39
+ xtype?: 'button' | 'tbbutton';
40
+ }
41
+ interface ToolbarTextConfig {
42
+ xtype: 'tbtext';
43
+ text: string;
44
+ /** 追加在 .xtb-text 之外的额外 class(如 StatusBar 的 x-status-text + iconCls) */
45
+ cls?: string;
46
+ }
47
+ interface ToolbarSeparatorConfig {
48
+ xtype: 'tbseparator';
49
+ }
50
+ interface ToolbarSpacerConfig {
51
+ xtype: 'tbspacer';
52
+ width?: number;
53
+ }
54
+ interface ToolbarFillConfig {
55
+ xtype: 'tbfill';
56
+ }
57
+
58
+ export type ToolbarItem =
59
+ | string
60
+ | ToolbarButtonConfig
61
+ | ToolbarTextConfig
62
+ | ToolbarSeparatorConfig
63
+ | ToolbarSpacerConfig
64
+ | ToolbarFillConfig
65
+ | React.ReactNode;
66
+
67
+ export interface ToolbarProps {
68
+ items: ToolbarItem[];
69
+ /** 左/中区域的水平对齐,等价 Ext buttonAlign */
70
+ buttonAlign?: ToolbarAlign;
71
+ width?: number | string;
72
+ style?: React.CSSProperties;
73
+ className?: string;
74
+ /** 数据测试 id,挂在根 div 上(供保真截图/行为测试定位) */
75
+ testId?: string;
76
+ /** 覆盖根 class(默认 x-toolbar x-small-editor x-toolbar-layout-ct)。
77
+ * 用于 Window.footer 等需要 x-panel-fbar 根类的场景。 */
78
+ rootClass?: string;
79
+ /** 溢出菜单(x-toolbar-more):为 true 时在 extras-row 渲染 more 按钮,
80
+ * 点击展开菜单显示 overflowItems。对齐 Ext.Toolbar enableOverflow。 */
81
+ enableOverflow?: boolean;
82
+ /** 收进 more 菜单的项(按钮/分隔符)。对齐 Ext 溢出检测隐藏的后缀项。 */
83
+ overflowItems?: ToolbarItem[];
84
+ /** 透传 ref:暴露 add / remove / getItems(对齐 Ext.Toolbar.add / remove)。 */
85
+ ref?: React.Ref<ToolbarHandle>;
86
+ }
87
+
88
+ /**
89
+ * Toolbar 命令句柄(对齐 Ext.Toolbar.add / remove)。
90
+ * - add(item, id?):追加一项到当前项列表尾部,返回稳定 id(便于后续 remove)。
91
+ * - remove(id):按 id 移除一项。
92
+ * - getItems():返回当前项列表(含 id)。
93
+ */
94
+ export interface ToolbarHandle {
95
+ add: (item: ToolbarItem, id?: string) => string;
96
+ remove: (id: string) => void;
97
+ getItems: () => Array<{ id: string; item: ToolbarItem }>;
98
+ }
99
+
100
+ /** 内部受管项:在原始项之外附加稳定 id 以支持动态增删。 */
101
+ interface ManagedItem {
102
+ id: string;
103
+ item: ToolbarItem;
104
+ }
105
+
106
+ /** 将溢出项转换为 Menu 的项(仅保留文本/分隔/图标/禁用,对齐 Ext.createMenuConfig) */
107
+ const toMenuItems = (items: ToolbarItem[]): MenuItem[] =>
108
+ items.map((it) => {
109
+ if (typeof it === 'string') {
110
+ if (it === '-') return { separator: true };
111
+ return { text: it };
112
+ }
113
+ if (it && typeof it === 'object' && !React.isValidElement(it)) {
114
+ const o = it as Record<string, unknown>;
115
+ if (o.xtype === 'tbseparator') return { separator: true };
116
+ return {
117
+ text: String(o.text ?? ''),
118
+ disabled: Boolean(o.disabled),
119
+ iconCls: o.iconCls as string | undefined,
120
+ handler: o.handler as (() => void) | undefined,
121
+ };
122
+ }
123
+ return { text: '' };
124
+ });
125
+
126
+ type Classified =
127
+ | { kind: 'separator' }
128
+ | { kind: 'spacer'; width?: number }
129
+ | { kind: 'fill' }
130
+ | { kind: 'text'; text: string; cls?: string }
131
+ | { kind: 'button'; config: ToolbarButtonConfig }
132
+ | { kind: 'raw'; node: React.ReactNode };
133
+
134
+ const classify = (item: ToolbarItem): Classified => {
135
+ if (React.isValidElement(item)) return { kind: 'raw', node: item };
136
+ if (typeof item === 'string') {
137
+ if (item === '-') return { kind: 'separator' };
138
+ if (item === ' ') return { kind: 'spacer' };
139
+ if (item === '->') return { kind: 'fill' };
140
+ return { kind: 'text', text: item };
141
+ }
142
+ if (item && typeof item === 'object') {
143
+ const o = item as Record<string, unknown>;
144
+ if (o.xtype === 'tbseparator') return { kind: 'separator' };
145
+ if (o.xtype === 'tbspacer') return { kind: 'spacer', width: o.width as number | undefined };
146
+ if (o.xtype === 'tbfill') return { kind: 'fill' };
147
+ if (o.xtype === 'tbtext') return { kind: 'text', text: String(o.text ?? ''), cls: o.cls as string | undefined };
148
+ if (o.xtype === 'button' || o.xtype === 'tbbutton') {
149
+ return { kind: 'button', config: item as ToolbarButtonConfig };
150
+ }
151
+ // 带 text 的对象视为按钮配置(Ext.lookupComponent 197-221 的默认分支)
152
+ if ('text' in o) return { kind: 'button', config: item as ToolbarButtonConfig };
153
+ }
154
+ return { kind: 'raw', node: item as React.ReactNode };
155
+ };
156
+
157
+ const renderItem = (c: Classified, key: number): React.ReactNode => {
158
+ switch (c.kind) {
159
+ case 'separator':
160
+ return <span key={key} className="xtb-sep" />;
161
+ case 'spacer':
162
+ return (
163
+ <div
164
+ key={key}
165
+ className="xtb-spacer"
166
+ style={c.width ? { width: `${c.width}px` } : undefined}
167
+ />
168
+ );
169
+ case 'text':
170
+ return (
171
+ <div key={key} className={c.cls ? `xtb-text ${c.cls}` : 'xtb-text'}>
172
+ {c.text}
173
+ </div>
174
+ );
175
+ case 'button':
176
+ return <Button key={key} {...c.config} />;
177
+ case 'raw':
178
+ return <React.Fragment key={key}>{c.node}</React.Fragment>;
179
+ default:
180
+ return null;
181
+ }
182
+ };
183
+
184
+ // Ext.Toolbar 通过 BoxComponent.setSize() 扣除 frame(padding+border)设置 el.style.width。
185
+ // .x-toolbar 有 padding:2px 左右 → 扣减 4px 使总宽度等于配置值。
186
+ const adjustWidth = (w?: number | string): number | string | undefined => {
187
+ if (typeof w === 'number') return w - 4;
188
+ return w;
189
+ };
190
+
191
+ export const Toolbar = forwardRef<ToolbarHandle, ToolbarProps>((
192
+ {
193
+ items,
194
+ buttonAlign = 'left',
195
+ width,
196
+ style,
197
+ className,
198
+ testId,
199
+ rootClass,
200
+ enableOverflow,
201
+ overflowItems,
202
+ },
203
+ ref,
204
+ ) => {
205
+ // 受管项:初始由 items prop 播种(id=seed-N),运行时 add/remove 维护。
206
+ // items prop 始终是静态项的来源(保持受控,PagingToolbar 等会随状态重建 items);
207
+ // 动态增删只影响独立的 dynamicItems 状态,绝不会被 prop 变更冲掉。
208
+ // 未开启动态增删时 managed === items,与原渲染完全等价(DOM 结构一致,像素基线零回归)。
209
+ const [dynamicItems, setDynamicItems] = useState<ManagedItem[]>([]);
210
+ const seqRef = useRef(0);
211
+
212
+ const managed = useMemo<ManagedItem[]>(
213
+ () => [
214
+ ...items.map((it, i) => ({ id: `seed-${i}`, item: it })),
215
+ ...dynamicItems,
216
+ ],
217
+ [items, dynamicItems],
218
+ );
219
+
220
+ useImperativeHandle(ref, () => ({
221
+ add: (item, id) => {
222
+ const newId = id ?? `tb-${++seqRef.current}`;
223
+ setDynamicItems((prev) => [...prev, { id: newId, item }]);
224
+ return newId;
225
+ },
226
+ remove: (id) => {
227
+ setDynamicItems((prev) => prev.filter((m) => m.id !== id));
228
+ },
229
+ getItems: () => managed,
230
+ }), [managed]);
231
+
232
+ const adjustedWidth = adjustWidth(width);
233
+ // 按第一个 fill('->') 切分左右区域;无 fill 则全部落入左侧
234
+ const left: ManagedItem[] = [];
235
+ const right: ManagedItem[] = [];
236
+ let onRight = false;
237
+ for (const m of managed) {
238
+ const c = classify(m.item);
239
+ if (c.kind === 'fill') {
240
+ onRight = true;
241
+ continue;
242
+ }
243
+ (onRight ? right : left).push(m);
244
+ }
245
+
246
+ const align = buttonAlign === 'center' ? 'center' : 'left';
247
+
248
+ const rootCls = rootClass
249
+ ?? [
250
+ 'x-toolbar',
251
+ 'x-small-editor',
252
+ 'x-toolbar-layout-ct',
253
+ className,
254
+ ]
255
+ .filter(Boolean)
256
+ .join(' ');
257
+
258
+ const hasOverflow = Boolean(enableOverflow && overflowItems && overflowItems.length > 0);
259
+ const moreMenuItems = hasOverflow ? toMenuItems(overflowItems!) : undefined;
260
+
261
+ const renderCells = (list: ManagedItem[]) =>
262
+ list.map((m) => (
263
+ <td key={m.id} className="x-toolbar-cell" data-tool-id={m.id}>
264
+ {renderItem(classify(m.item), 0)}
265
+ </td>
266
+ ));
267
+
268
+ return (
269
+ <div className={rootCls} data-testid={testId} style={{ width: adjustedWidth, ...style }}>
270
+ <table cellSpacing={0} className="x-toolbar-ct">
271
+ <tbody>
272
+ <tr>
273
+ <td className="x-toolbar-left" align={align}>
274
+ <table cellSpacing={0}>
275
+ <tbody>
276
+ <tr className="x-toolbar-left-row">{renderCells(left)}</tr>
277
+ </tbody>
278
+ </table>
279
+ </td>
280
+ <td className="x-toolbar-right" align="right">
281
+ <table cellSpacing={0} className="x-toolbar-right-ct">
282
+ <tbody>
283
+ <tr>
284
+ <td>
285
+ <table cellSpacing={0}>
286
+ <tbody>
287
+ <tr className="x-toolbar-right-row">{renderCells(right)}</tr>
288
+ </tbody>
289
+ </table>
290
+ </td>
291
+ <td>
292
+ <table cellSpacing={0}>
293
+ <tbody>
294
+ <tr className="x-toolbar-extras-row">
295
+ {hasOverflow && moreMenuItems && (
296
+ <td className="x-toolbar-cell">
297
+ <Button
298
+ cls="x-toolbar-more"
299
+ iconCls="x-toolbar-more-icon"
300
+ text=" "
301
+ menu={moreMenuItems}
302
+ />
303
+ </td>
304
+ )}
305
+ </tr>
306
+ </tbody>
307
+ </table>
308
+ </td>
309
+ </tr>
310
+ </tbody>
311
+ </table>
312
+ </td>
313
+ </tr>
314
+ </tbody>
315
+ </table>
316
+ </div>
317
+ );
318
+ });
319
+ Toolbar.displayName = 'Toolbar';
320
+
321
+ export default Toolbar;