@idealyst/markdown 1.2.38 → 1.2.40
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.
- package/package.json +74 -5
- package/src/Editor/EditorToolbar.web.tsx +470 -0
- package/src/Editor/MarkdownEditor.native.tsx +277 -0
- package/src/Editor/MarkdownEditor.styles.ts +337 -0
- package/src/Editor/MarkdownEditor.web.tsx +388 -0
- package/src/Editor/examples/MarkdownEditorExamples.tsx +235 -0
- package/src/Editor/examples/index.ts +1 -0
- package/src/Editor/index.native.ts +23 -0
- package/src/Editor/index.ts +43 -0
- package/src/Editor/types.ts +202 -0
- package/src/examples/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/markdown",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "Cross-platform markdown renderer for React and React Native with theme integration",
|
|
3
|
+
"version": "1.2.40",
|
|
4
|
+
"description": "Cross-platform markdown renderer and editor for React and React Native with theme integration",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"module": "src/index.ts",
|
|
7
7
|
"types": "src/index.ts",
|
|
@@ -22,6 +22,17 @@
|
|
|
22
22
|
"import": "./src/index.ts",
|
|
23
23
|
"require": "./src/index.ts",
|
|
24
24
|
"types": "./src/index.ts"
|
|
25
|
+
},
|
|
26
|
+
"./editor": {
|
|
27
|
+
"react-native": "./src/Editor/index.native.ts",
|
|
28
|
+
"import": "./src/Editor/index.ts",
|
|
29
|
+
"require": "./src/Editor/index.ts",
|
|
30
|
+
"types": "./src/Editor/index.ts"
|
|
31
|
+
},
|
|
32
|
+
"./examples": {
|
|
33
|
+
"import": "./src/examples/index.ts",
|
|
34
|
+
"require": "./src/examples/index.ts",
|
|
35
|
+
"types": "./src/examples/index.ts"
|
|
25
36
|
}
|
|
26
37
|
},
|
|
27
38
|
"scripts": {
|
|
@@ -29,18 +40,53 @@
|
|
|
29
40
|
"publish:npm": "npm publish"
|
|
30
41
|
},
|
|
31
42
|
"peerDependencies": {
|
|
32
|
-
"@
|
|
43
|
+
"@10play/tentap-editor": ">=0.5.0",
|
|
44
|
+
"@idealyst/theme": "^1.2.40",
|
|
45
|
+
"@tiptap/extension-link": ">=2.0.0",
|
|
46
|
+
"@tiptap/extension-placeholder": ">=2.0.0",
|
|
47
|
+
"@tiptap/extension-task-item": ">=2.0.0",
|
|
48
|
+
"@tiptap/extension-task-list": ">=2.0.0",
|
|
49
|
+
"@tiptap/extension-underline": ">=2.0.0",
|
|
50
|
+
"@tiptap/react": ">=2.0.0",
|
|
51
|
+
"@tiptap/starter-kit": ">=2.0.0",
|
|
33
52
|
"react": ">=16.8.0",
|
|
34
53
|
"react-markdown": ">=9.0.0",
|
|
35
54
|
"react-native": ">=0.60.0",
|
|
36
55
|
"react-native-markdown-display": ">=7.0.0",
|
|
37
56
|
"react-native-unistyles": ">=3.0.0",
|
|
38
|
-
"
|
|
57
|
+
"react-native-webview": ">=13.0.0",
|
|
58
|
+
"remark-gfm": ">=4.0.0",
|
|
59
|
+
"showdown": ">=2.0.0",
|
|
60
|
+
"tiptap-markdown": ">=0.8.0"
|
|
39
61
|
},
|
|
40
62
|
"peerDependenciesMeta": {
|
|
63
|
+
"@10play/tentap-editor": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
41
66
|
"@idealyst/theme": {
|
|
42
67
|
"optional": true
|
|
43
68
|
},
|
|
69
|
+
"@tiptap/extension-link": {
|
|
70
|
+
"optional": true
|
|
71
|
+
},
|
|
72
|
+
"@tiptap/extension-placeholder": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
75
|
+
"@tiptap/extension-task-item": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"@tiptap/extension-task-list": {
|
|
79
|
+
"optional": true
|
|
80
|
+
},
|
|
81
|
+
"@tiptap/extension-underline": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"@tiptap/react": {
|
|
85
|
+
"optional": true
|
|
86
|
+
},
|
|
87
|
+
"@tiptap/starter-kit": {
|
|
88
|
+
"optional": true
|
|
89
|
+
},
|
|
44
90
|
"react-markdown": {
|
|
45
91
|
"optional": true
|
|
46
92
|
},
|
|
@@ -53,18 +99,39 @@
|
|
|
53
99
|
"react-native-unistyles": {
|
|
54
100
|
"optional": true
|
|
55
101
|
},
|
|
102
|
+
"react-native-webview": {
|
|
103
|
+
"optional": true
|
|
104
|
+
},
|
|
56
105
|
"remark-gfm": {
|
|
57
106
|
"optional": true
|
|
107
|
+
},
|
|
108
|
+
"showdown": {
|
|
109
|
+
"optional": true
|
|
110
|
+
},
|
|
111
|
+
"tiptap-markdown": {
|
|
112
|
+
"optional": true
|
|
58
113
|
}
|
|
59
114
|
},
|
|
60
115
|
"devDependencies": {
|
|
61
|
-
"@
|
|
116
|
+
"@10play/tentap-editor": "^0.5.0",
|
|
117
|
+
"@idealyst/theme": "^1.2.40",
|
|
118
|
+
"@tiptap/extension-link": "^2.11.0",
|
|
119
|
+
"@tiptap/extension-placeholder": "^2.11.0",
|
|
120
|
+
"@tiptap/extension-task-item": "^2.11.0",
|
|
121
|
+
"@tiptap/extension-task-list": "^2.11.0",
|
|
122
|
+
"@tiptap/extension-underline": "^2.11.0",
|
|
123
|
+
"@tiptap/react": "^2.11.0",
|
|
124
|
+
"@tiptap/starter-kit": "^2.11.0",
|
|
62
125
|
"@types/react": "^19.1.0",
|
|
63
126
|
"react": "^19.1.0",
|
|
64
127
|
"react-markdown": "^9.0.0",
|
|
65
128
|
"react-native": "^0.80.1",
|
|
66
129
|
"react-native-unistyles": "^3.0.10",
|
|
130
|
+
"react-native-webview": "^13.0.0",
|
|
67
131
|
"remark-gfm": "^4.0.0",
|
|
132
|
+
"showdown": "^2.1.0",
|
|
133
|
+
"@types/showdown": "^2.0.0",
|
|
134
|
+
"tiptap-markdown": "^0.8.0",
|
|
68
135
|
"typescript": "^5.0.0"
|
|
69
136
|
},
|
|
70
137
|
"files": [
|
|
@@ -75,6 +142,8 @@
|
|
|
75
142
|
"react",
|
|
76
143
|
"react-native",
|
|
77
144
|
"markdown",
|
|
145
|
+
"editor",
|
|
146
|
+
"tiptap",
|
|
78
147
|
"cross-platform",
|
|
79
148
|
"idealyst"
|
|
80
149
|
]
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { memo, useState, useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { UnistylesRuntime } from 'react-native-unistyles';
|
|
3
|
+
import Icon from '@mdi/react';
|
|
4
|
+
import {
|
|
5
|
+
mdiFormatBold,
|
|
6
|
+
mdiFormatItalic,
|
|
7
|
+
mdiFormatUnderline,
|
|
8
|
+
mdiFormatStrikethrough,
|
|
9
|
+
mdiCodeTags,
|
|
10
|
+
mdiFormatHeader1,
|
|
11
|
+
mdiFormatHeader2,
|
|
12
|
+
mdiFormatHeader3,
|
|
13
|
+
mdiFormatHeader4,
|
|
14
|
+
mdiFormatHeader5,
|
|
15
|
+
mdiFormatHeader6,
|
|
16
|
+
mdiFormatHeaderPound,
|
|
17
|
+
mdiFormatListBulleted,
|
|
18
|
+
mdiFormatListNumbered,
|
|
19
|
+
mdiFormatListChecks,
|
|
20
|
+
mdiFormatQuoteClose,
|
|
21
|
+
mdiCodeBlockBraces,
|
|
22
|
+
mdiMinus,
|
|
23
|
+
mdiLink,
|
|
24
|
+
mdiImage,
|
|
25
|
+
mdiUndo,
|
|
26
|
+
mdiRedo,
|
|
27
|
+
mdiMenuDown,
|
|
28
|
+
} from '@mdi/js';
|
|
29
|
+
import type { ToolbarItem } from './types';
|
|
30
|
+
import type { Size, Intent } from '@idealyst/theme';
|
|
31
|
+
import type { Editor } from '@tiptap/react';
|
|
32
|
+
|
|
33
|
+
interface EditorToolbarProps {
|
|
34
|
+
editor: Editor | null;
|
|
35
|
+
items: readonly ToolbarItem[];
|
|
36
|
+
disabledItems?: readonly ToolbarItem[];
|
|
37
|
+
onAction: (action: string) => void;
|
|
38
|
+
isActive: (action: string) => boolean;
|
|
39
|
+
size: Size;
|
|
40
|
+
linkIntent: Intent;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const TOOLBAR_ICONS: Record<string, string> = {
|
|
44
|
+
bold: mdiFormatBold,
|
|
45
|
+
italic: mdiFormatItalic,
|
|
46
|
+
underline: mdiFormatUnderline,
|
|
47
|
+
strikethrough: mdiFormatStrikethrough,
|
|
48
|
+
code: mdiCodeTags,
|
|
49
|
+
heading: mdiFormatHeaderPound,
|
|
50
|
+
heading1: mdiFormatHeader1,
|
|
51
|
+
heading2: mdiFormatHeader2,
|
|
52
|
+
heading3: mdiFormatHeader3,
|
|
53
|
+
heading4: mdiFormatHeader4,
|
|
54
|
+
heading5: mdiFormatHeader5,
|
|
55
|
+
heading6: mdiFormatHeader6,
|
|
56
|
+
bulletList: mdiFormatListBulleted,
|
|
57
|
+
orderedList: mdiFormatListNumbered,
|
|
58
|
+
taskList: mdiFormatListChecks,
|
|
59
|
+
blockquote: mdiFormatQuoteClose,
|
|
60
|
+
codeBlock: mdiCodeBlockBraces,
|
|
61
|
+
horizontalRule: mdiMinus,
|
|
62
|
+
link: mdiLink,
|
|
63
|
+
image: mdiImage,
|
|
64
|
+
undo: mdiUndo,
|
|
65
|
+
redo: mdiRedo,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const TOOLBAR_TITLES: Record<string, string> = {
|
|
69
|
+
bold: 'Bold (Ctrl+B)',
|
|
70
|
+
italic: 'Italic (Ctrl+I)',
|
|
71
|
+
underline: 'Underline (Ctrl+U)',
|
|
72
|
+
strikethrough: 'Strikethrough',
|
|
73
|
+
code: 'Inline Code',
|
|
74
|
+
heading: 'Heading',
|
|
75
|
+
heading1: 'Heading 1',
|
|
76
|
+
heading2: 'Heading 2',
|
|
77
|
+
heading3: 'Heading 3',
|
|
78
|
+
heading4: 'Heading 4',
|
|
79
|
+
heading5: 'Heading 5',
|
|
80
|
+
heading6: 'Heading 6',
|
|
81
|
+
bulletList: 'Bullet List',
|
|
82
|
+
orderedList: 'Numbered List',
|
|
83
|
+
taskList: 'Task List',
|
|
84
|
+
blockquote: 'Blockquote',
|
|
85
|
+
codeBlock: 'Code Block',
|
|
86
|
+
horizontalRule: 'Horizontal Rule',
|
|
87
|
+
link: 'Insert Link',
|
|
88
|
+
image: 'Insert Image',
|
|
89
|
+
undo: 'Undo (Ctrl+Z)',
|
|
90
|
+
redo: 'Redo (Ctrl+Y)',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const HEADING_ITEMS = ['heading1', 'heading2', 'heading3', 'heading4', 'heading5', 'heading6'] as const;
|
|
94
|
+
|
|
95
|
+
export const EditorToolbar = memo<EditorToolbarProps>(
|
|
96
|
+
({ editor, items, disabledItems = [], onAction, isActive, size, linkIntent }) => {
|
|
97
|
+
// Force re-render when editor state changes
|
|
98
|
+
const [, setUpdateCounter] = useState(0);
|
|
99
|
+
|
|
100
|
+
// Access theme directly
|
|
101
|
+
const theme = UnistylesRuntime.getTheme();
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!editor) return;
|
|
105
|
+
|
|
106
|
+
const handleUpdate = () => {
|
|
107
|
+
setUpdateCounter((c) => c + 1);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
editor.on('selectionUpdate', handleUpdate);
|
|
111
|
+
editor.on('transaction', handleUpdate);
|
|
112
|
+
|
|
113
|
+
return () => {
|
|
114
|
+
editor.off('selectionUpdate', handleUpdate);
|
|
115
|
+
editor.off('transaction', handleUpdate);
|
|
116
|
+
};
|
|
117
|
+
}, [editor]);
|
|
118
|
+
|
|
119
|
+
// Generate toolbar styles from theme
|
|
120
|
+
const toolbarStyle: React.CSSProperties = useMemo(() => ({
|
|
121
|
+
display: 'flex',
|
|
122
|
+
flexDirection: 'row',
|
|
123
|
+
flexWrap: 'wrap',
|
|
124
|
+
gap: 4,
|
|
125
|
+
padding: 8,
|
|
126
|
+
borderBottom: `1px solid ${theme.colors.border.primary}`,
|
|
127
|
+
backgroundColor: theme.colors.surface.secondary,
|
|
128
|
+
}), [theme]);
|
|
129
|
+
|
|
130
|
+
// Check if any heading is active
|
|
131
|
+
const activeHeading = HEADING_ITEMS.find((h) => isActive(h));
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div
|
|
135
|
+
style={toolbarStyle}
|
|
136
|
+
role="toolbar"
|
|
137
|
+
aria-label="Editor formatting toolbar"
|
|
138
|
+
>
|
|
139
|
+
{items.map((item) => {
|
|
140
|
+
// Render heading dropdown
|
|
141
|
+
if (item === 'heading') {
|
|
142
|
+
const disabled = disabledItems.includes(item);
|
|
143
|
+
return (
|
|
144
|
+
<HeadingDropdown
|
|
145
|
+
key={item}
|
|
146
|
+
activeHeading={activeHeading}
|
|
147
|
+
disabled={disabled}
|
|
148
|
+
onAction={onAction}
|
|
149
|
+
theme={theme}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Skip individual heading items if we're using the dropdown
|
|
155
|
+
if (item.startsWith('heading') && items.includes('heading')) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const active = isActive(item);
|
|
160
|
+
const disabled = disabledItems.includes(item);
|
|
161
|
+
return (
|
|
162
|
+
<ToolbarButton
|
|
163
|
+
key={item}
|
|
164
|
+
item={item}
|
|
165
|
+
active={active}
|
|
166
|
+
disabled={disabled}
|
|
167
|
+
onAction={onAction}
|
|
168
|
+
theme={theme}
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
})}
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
EditorToolbar.displayName = 'EditorToolbar';
|
|
178
|
+
|
|
179
|
+
// Heading dropdown component
|
|
180
|
+
const HeadingDropdown = memo<{
|
|
181
|
+
activeHeading: string | undefined;
|
|
182
|
+
disabled: boolean;
|
|
183
|
+
onAction: (action: string) => void;
|
|
184
|
+
theme: any;
|
|
185
|
+
}>(({ activeHeading, disabled, onAction, theme }) => {
|
|
186
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
187
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
188
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
189
|
+
|
|
190
|
+
// Close dropdown when clicking outside
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
193
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
194
|
+
setIsOpen(false);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
if (isOpen) {
|
|
199
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
200
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
201
|
+
}
|
|
202
|
+
}, [isOpen]);
|
|
203
|
+
|
|
204
|
+
const primaryColor = theme.intents?.primary?.primary ?? '#6366f1';
|
|
205
|
+
const textColor = theme.colors.text.primary;
|
|
206
|
+
const disabledColor = theme.colors.text.tertiary ?? 'rgba(0, 0, 0, 0.3)';
|
|
207
|
+
const activeBg = theme.colors.surface.tertiary ?? 'rgba(0, 0, 0, 0.08)';
|
|
208
|
+
const hoverBg = theme.colors.surface.tertiary ?? 'rgba(0, 0, 0, 0.05)';
|
|
209
|
+
const surfacePrimary = theme.colors.surface.primary ?? '#ffffff';
|
|
210
|
+
const borderColor = theme.colors.border.primary ?? 'rgba(0, 0, 0, 0.1)';
|
|
211
|
+
|
|
212
|
+
const buttonStyle: React.CSSProperties = useMemo(() => {
|
|
213
|
+
const base: React.CSSProperties = {
|
|
214
|
+
display: 'flex',
|
|
215
|
+
alignItems: 'center',
|
|
216
|
+
justifyContent: 'center',
|
|
217
|
+
gap: 2,
|
|
218
|
+
padding: '6px 8px',
|
|
219
|
+
borderRadius: theme.radii?.sm ?? 4,
|
|
220
|
+
backgroundColor: 'transparent',
|
|
221
|
+
color: textColor,
|
|
222
|
+
minWidth: 48,
|
|
223
|
+
height: 32,
|
|
224
|
+
border: 'none',
|
|
225
|
+
cursor: 'pointer',
|
|
226
|
+
transition: 'all 0.15s ease',
|
|
227
|
+
outline: 'none',
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (disabled) {
|
|
231
|
+
return {
|
|
232
|
+
...base,
|
|
233
|
+
color: disabledColor,
|
|
234
|
+
cursor: 'not-allowed',
|
|
235
|
+
opacity: 0.5,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (activeHeading || isOpen) {
|
|
240
|
+
return {
|
|
241
|
+
...base,
|
|
242
|
+
backgroundColor: activeBg,
|
|
243
|
+
color: primaryColor,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (isHovered) {
|
|
248
|
+
return {
|
|
249
|
+
...base,
|
|
250
|
+
backgroundColor: hoverBg,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return base;
|
|
255
|
+
}, [activeHeading, isOpen, disabled, isHovered, theme, primaryColor, textColor, disabledColor, activeBg, hoverBg]);
|
|
256
|
+
|
|
257
|
+
const dropdownStyle: React.CSSProperties = {
|
|
258
|
+
position: 'absolute',
|
|
259
|
+
top: '100%',
|
|
260
|
+
left: 0,
|
|
261
|
+
marginTop: 4,
|
|
262
|
+
backgroundColor: surfacePrimary,
|
|
263
|
+
border: `1px solid ${borderColor}`,
|
|
264
|
+
borderRadius: theme.radii?.md ?? 8,
|
|
265
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
266
|
+
zIndex: 1000,
|
|
267
|
+
minWidth: 120,
|
|
268
|
+
overflow: 'hidden',
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const handleToggle = () => {
|
|
272
|
+
if (!disabled) {
|
|
273
|
+
setIsOpen(!isOpen);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const handleSelect = (heading: string) => {
|
|
278
|
+
onAction(heading);
|
|
279
|
+
setIsOpen(false);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Determine which icon to show - default to H1
|
|
283
|
+
const currentIcon = activeHeading ? TOOLBAR_ICONS[activeHeading] : TOOLBAR_ICONS.heading1;
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<div ref={dropdownRef} style={{ position: 'relative' }}>
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
onClick={handleToggle}
|
|
290
|
+
title={TOOLBAR_TITLES.heading}
|
|
291
|
+
aria-expanded={isOpen}
|
|
292
|
+
aria-haspopup="listbox"
|
|
293
|
+
disabled={disabled}
|
|
294
|
+
style={buttonStyle}
|
|
295
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
296
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
297
|
+
>
|
|
298
|
+
<Icon path={currentIcon} size={0.75} />
|
|
299
|
+
<Icon path={mdiMenuDown} size={0.5} />
|
|
300
|
+
</button>
|
|
301
|
+
{isOpen && (
|
|
302
|
+
<div style={dropdownStyle} role="listbox">
|
|
303
|
+
{HEADING_ITEMS.map((heading) => (
|
|
304
|
+
<HeadingOption
|
|
305
|
+
key={heading}
|
|
306
|
+
heading={heading}
|
|
307
|
+
isActive={activeHeading === heading}
|
|
308
|
+
onSelect={handleSelect}
|
|
309
|
+
theme={theme}
|
|
310
|
+
/>
|
|
311
|
+
))}
|
|
312
|
+
</div>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
HeadingDropdown.displayName = 'HeadingDropdown';
|
|
319
|
+
|
|
320
|
+
// Individual heading option in the dropdown
|
|
321
|
+
const HeadingOption = memo<{
|
|
322
|
+
heading: string;
|
|
323
|
+
isActive: boolean;
|
|
324
|
+
onSelect: (heading: string) => void;
|
|
325
|
+
theme: any;
|
|
326
|
+
}>(({ heading, isActive, onSelect, theme }) => {
|
|
327
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
328
|
+
|
|
329
|
+
const primaryColor = theme.intents?.primary?.primary ?? '#6366f1';
|
|
330
|
+
const textColor = theme.colors.text.primary;
|
|
331
|
+
const hoverBg = theme.colors.surface.secondary ?? 'rgba(0, 0, 0, 0.05)';
|
|
332
|
+
const activeBg = theme.colors.surface.tertiary ?? 'rgba(0, 0, 0, 0.08)';
|
|
333
|
+
|
|
334
|
+
const optionStyle: React.CSSProperties = useMemo(() => {
|
|
335
|
+
const base: React.CSSProperties = {
|
|
336
|
+
display: 'flex',
|
|
337
|
+
alignItems: 'center',
|
|
338
|
+
gap: 8,
|
|
339
|
+
padding: '8px 12px',
|
|
340
|
+
backgroundColor: 'transparent',
|
|
341
|
+
color: textColor,
|
|
342
|
+
border: 'none',
|
|
343
|
+
cursor: 'pointer',
|
|
344
|
+
width: '100%',
|
|
345
|
+
textAlign: 'left',
|
|
346
|
+
transition: 'background-color 0.15s ease',
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
if (isActive) {
|
|
350
|
+
return {
|
|
351
|
+
...base,
|
|
352
|
+
backgroundColor: activeBg,
|
|
353
|
+
color: primaryColor,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (isHovered) {
|
|
358
|
+
return {
|
|
359
|
+
...base,
|
|
360
|
+
backgroundColor: hoverBg,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return base;
|
|
365
|
+
}, [isActive, isHovered, primaryColor, textColor, hoverBg, activeBg]);
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<button
|
|
369
|
+
type="button"
|
|
370
|
+
onClick={() => onSelect(heading)}
|
|
371
|
+
style={optionStyle}
|
|
372
|
+
role="option"
|
|
373
|
+
aria-selected={isActive}
|
|
374
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
375
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
376
|
+
>
|
|
377
|
+
<Icon path={TOOLBAR_ICONS[heading]} size={0.75} />
|
|
378
|
+
<span style={{ fontSize: 14 }}>{TOOLBAR_TITLES[heading]}</span>
|
|
379
|
+
</button>
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
HeadingOption.displayName = 'HeadingOption';
|
|
384
|
+
|
|
385
|
+
// Separate button component to properly manage styles per button
|
|
386
|
+
const ToolbarButton = memo<{
|
|
387
|
+
item: ToolbarItem;
|
|
388
|
+
active: boolean;
|
|
389
|
+
disabled: boolean;
|
|
390
|
+
onAction: (action: string) => void;
|
|
391
|
+
theme: any;
|
|
392
|
+
}>(({ item, active, disabled, onAction, theme }) => {
|
|
393
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
394
|
+
|
|
395
|
+
// Get colors from theme
|
|
396
|
+
const primaryColor = theme.intents?.primary?.primary ?? '#6366f1';
|
|
397
|
+
const textColor = theme.colors.text.primary;
|
|
398
|
+
const disabledColor = theme.colors.text.tertiary ?? 'rgba(0, 0, 0, 0.3)';
|
|
399
|
+
const activeBg = theme.colors.surface.tertiary ?? 'rgba(0, 0, 0, 0.08)';
|
|
400
|
+
const hoverBg = theme.colors.surface.tertiary ?? 'rgba(0, 0, 0, 0.05)';
|
|
401
|
+
|
|
402
|
+
// Build button style based on state
|
|
403
|
+
const buttonStyle: React.CSSProperties = useMemo(() => {
|
|
404
|
+
const base: React.CSSProperties = {
|
|
405
|
+
display: 'flex',
|
|
406
|
+
alignItems: 'center',
|
|
407
|
+
justifyContent: 'center',
|
|
408
|
+
padding: '6px',
|
|
409
|
+
borderRadius: theme.radii?.sm ?? 4,
|
|
410
|
+
backgroundColor: 'transparent',
|
|
411
|
+
color: textColor,
|
|
412
|
+
minWidth: 32,
|
|
413
|
+
height: 32,
|
|
414
|
+
border: 'none',
|
|
415
|
+
cursor: 'pointer',
|
|
416
|
+
transition: 'all 0.15s ease',
|
|
417
|
+
outline: 'none',
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
if (disabled) {
|
|
421
|
+
return {
|
|
422
|
+
...base,
|
|
423
|
+
color: disabledColor,
|
|
424
|
+
cursor: 'not-allowed',
|
|
425
|
+
opacity: 0.5,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (active) {
|
|
430
|
+
return {
|
|
431
|
+
...base,
|
|
432
|
+
backgroundColor: activeBg,
|
|
433
|
+
color: primaryColor,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (isHovered) {
|
|
438
|
+
return {
|
|
439
|
+
...base,
|
|
440
|
+
backgroundColor: hoverBg,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return base;
|
|
445
|
+
}, [active, disabled, isHovered, theme, primaryColor, textColor, disabledColor, activeBg, hoverBg]);
|
|
446
|
+
|
|
447
|
+
const handleClick = () => {
|
|
448
|
+
if (!disabled) {
|
|
449
|
+
onAction(item);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<button
|
|
455
|
+
type="button"
|
|
456
|
+
onClick={handleClick}
|
|
457
|
+
title={TOOLBAR_TITLES[item]}
|
|
458
|
+
aria-pressed={active}
|
|
459
|
+
aria-disabled={disabled}
|
|
460
|
+
disabled={disabled}
|
|
461
|
+
style={buttonStyle}
|
|
462
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
463
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
464
|
+
>
|
|
465
|
+
<Icon path={TOOLBAR_ICONS[item]} size={0.75} />
|
|
466
|
+
</button>
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
ToolbarButton.displayName = 'ToolbarButton';
|