@idealyst/markdown 1.2.38 → 1.2.39

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@idealyst/markdown",
3
- "version": "1.2.38",
4
- "description": "Cross-platform markdown renderer for React and React Native with theme integration",
3
+ "version": "1.2.39",
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/Editor/examples/index.ts",
34
+ "require": "./src/Editor/examples/index.ts",
35
+ "types": "./src/Editor/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
- "@idealyst/theme": "^1.2.38",
43
+ "@10play/tentap-editor": ">=0.5.0",
44
+ "@idealyst/theme": "^1.2.39",
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
- "remark-gfm": ">=4.0.0"
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
- "@idealyst/theme": "^1.2.38",
116
+ "@10play/tentap-editor": "^0.5.0",
117
+ "@idealyst/theme": "^1.2.39",
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,90 @@
1
+ import { memo } from 'react';
2
+ import { getWebProps } from 'react-native-unistyles/web';
3
+ import { editorStyles } from './MarkdownEditor.styles';
4
+ import type { ToolbarItem } from './types';
5
+ import type { Size, Intent } from '@idealyst/theme';
6
+
7
+ interface EditorToolbarProps {
8
+ items: readonly ToolbarItem[];
9
+ onAction: (action: string) => void;
10
+ isActive: (action: string) => boolean;
11
+ size: Size;
12
+ linkIntent: Intent;
13
+ }
14
+
15
+ const TOOLBAR_ICONS: Record<ToolbarItem, string> = {
16
+ bold: 'B',
17
+ italic: 'I',
18
+ underline: 'U',
19
+ strikethrough: 'S',
20
+ code: '</>',
21
+ heading1: 'H1',
22
+ heading2: 'H2',
23
+ heading3: 'H3',
24
+ bulletList: '•',
25
+ orderedList: '1.',
26
+ taskList: '☑',
27
+ blockquote: '"',
28
+ codeBlock: '{ }',
29
+ horizontalRule: '—',
30
+ link: '🔗',
31
+ image: '🖼',
32
+ undo: '↶',
33
+ redo: '↷',
34
+ };
35
+
36
+ const TOOLBAR_TITLES: Record<ToolbarItem, string> = {
37
+ bold: 'Bold (Ctrl+B)',
38
+ italic: 'Italic (Ctrl+I)',
39
+ underline: 'Underline (Ctrl+U)',
40
+ strikethrough: 'Strikethrough',
41
+ code: 'Inline Code',
42
+ heading1: 'Heading 1',
43
+ heading2: 'Heading 2',
44
+ heading3: 'Heading 3',
45
+ bulletList: 'Bullet List',
46
+ orderedList: 'Numbered List',
47
+ taskList: 'Task List',
48
+ blockquote: 'Blockquote',
49
+ codeBlock: 'Code Block',
50
+ horizontalRule: 'Horizontal Rule',
51
+ link: 'Insert Link',
52
+ image: 'Insert Image',
53
+ undo: 'Undo (Ctrl+Z)',
54
+ redo: 'Redo (Ctrl+Y)',
55
+ };
56
+
57
+ export const EditorToolbar = memo<EditorToolbarProps>(
58
+ ({ items, onAction, isActive, size, linkIntent }) => {
59
+ return (
60
+ <div
61
+ {...getWebProps([
62
+ (editorStyles.toolbar as any)({ size, linkIntent }),
63
+ ])}
64
+ role="toolbar"
65
+ aria-label="Editor formatting toolbar"
66
+ >
67
+ {items.map((item) => {
68
+ const active = isActive(item);
69
+ return (
70
+ <button
71
+ key={item}
72
+ type="button"
73
+ onClick={() => onAction(item)}
74
+ title={TOOLBAR_TITLES[item]}
75
+ aria-pressed={active}
76
+ {...getWebProps([
77
+ (editorStyles.toolbarButton as any)({ size, linkIntent }),
78
+ active && (editorStyles.toolbarButtonActive as any)({ size, linkIntent }),
79
+ ])}
80
+ >
81
+ {TOOLBAR_ICONS[item]}
82
+ </button>
83
+ );
84
+ })}
85
+ </div>
86
+ );
87
+ }
88
+ );
89
+
90
+ EditorToolbar.displayName = 'EditorToolbar';
@@ -0,0 +1,262 @@
1
+ import {
2
+ forwardRef,
3
+ useImperativeHandle,
4
+ useEffect,
5
+ useRef,
6
+ } from 'react';
7
+ import { View, StyleSheet, KeyboardAvoidingView, Platform } from 'react-native';
8
+ import {
9
+ RichText,
10
+ Toolbar,
11
+ useEditorBridge,
12
+ DEFAULT_TOOLBAR_ITEMS,
13
+ type EditorBridge,
14
+ } from '@10play/tentap-editor';
15
+ import Showdown from 'showdown';
16
+ import { editorStyles } from './MarkdownEditor.styles';
17
+ import type { MarkdownEditorProps, MarkdownEditorRef, ToolbarItem } from './types';
18
+
19
+ // Configure Showdown converter with GFM support
20
+ const showdownConverter = new Showdown.Converter({
21
+ tables: true,
22
+ strikethrough: true,
23
+ tasklists: true,
24
+ ghCodeBlocks: true,
25
+ smoothLivePreview: true,
26
+ simpleLineBreaks: false,
27
+ openLinksInNewWindow: false,
28
+ backslashEscapesHTMLTags: true,
29
+ });
30
+
31
+ // Map our toolbar items to 10tap toolbar items
32
+ const TOOLBAR_ITEM_MAP: Record<ToolbarItem, string | null> = {
33
+ bold: 'bold',
34
+ italic: 'italic',
35
+ underline: 'underline',
36
+ strikethrough: 'strikethrough',
37
+ code: 'code',
38
+ heading1: 'h1',
39
+ heading2: 'h2',
40
+ heading3: 'h3',
41
+ bulletList: 'bulletList',
42
+ orderedList: 'orderedList',
43
+ taskList: 'taskList',
44
+ blockquote: 'blockquote',
45
+ codeBlock: 'codeBlock',
46
+ horizontalRule: 'horizontalRule',
47
+ link: 'link',
48
+ image: 'image',
49
+ undo: 'undo',
50
+ redo: 'redo',
51
+ };
52
+
53
+ /**
54
+ * Convert markdown to HTML using Showdown.
55
+ */
56
+ function markdownToHtml(markdown: string): string {
57
+ return showdownConverter.makeHtml(markdown);
58
+ }
59
+
60
+ /**
61
+ * Convert HTML to markdown using Showdown.
62
+ */
63
+ function htmlToMarkdown(html: string): string {
64
+ return showdownConverter.makeMarkdown(html);
65
+ }
66
+
67
+ /**
68
+ * Markdown editor for React Native using 10tap-editor.
69
+ *
70
+ * Uses a WebView-based Tiptap editor under the hood for rich text editing
71
+ * with markdown input/output support.
72
+ */
73
+ const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>(
74
+ (
75
+ {
76
+ initialValue = '',
77
+ value,
78
+ onChange,
79
+ onFocus,
80
+ onBlur,
81
+ editable = true,
82
+ autoFocus = false,
83
+ placeholder,
84
+ toolbar = {},
85
+ size = 'md',
86
+ linkIntent = 'primary',
87
+ minHeight = 200,
88
+ maxHeight,
89
+ style,
90
+ testID,
91
+ id,
92
+ accessibilityLabel,
93
+ avoidIosKeyboard = true,
94
+ },
95
+ ref
96
+ ) => {
97
+ const isControlled = value !== undefined;
98
+ const lastValueRef = useRef<string>(value ?? initialValue);
99
+ const editorRef = useRef<EditorBridge | null>(null);
100
+
101
+ // Apply style variants
102
+ editorStyles.useVariants({
103
+ size,
104
+ linkIntent,
105
+ });
106
+
107
+ // Initialize editor with HTML content
108
+ const initialHtml = markdownToHtml(value ?? initialValue);
109
+
110
+ const editor = useEditorBridge({
111
+ autofocus: autoFocus,
112
+ avoidIosKeyboard,
113
+ initialContent: initialHtml,
114
+ editable,
115
+ onChange: async () => {
116
+ if (editorRef.current) {
117
+ try {
118
+ const html = await editorRef.current.getHTML();
119
+ const markdown = htmlToMarkdown(html);
120
+ if (markdown !== lastValueRef.current) {
121
+ lastValueRef.current = markdown;
122
+ onChange?.(markdown);
123
+ }
124
+ } catch {
125
+ // Editor might not be ready
126
+ }
127
+ }
128
+ },
129
+ });
130
+
131
+ // Store editor ref
132
+ useEffect(() => {
133
+ editorRef.current = editor;
134
+ }, [editor]);
135
+
136
+ // Handle controlled value changes
137
+ useEffect(() => {
138
+ if (isControlled && value !== lastValueRef.current && editorRef.current) {
139
+ const html = markdownToHtml(value);
140
+ editorRef.current.setContent(html);
141
+ lastValueRef.current = value;
142
+ }
143
+ }, [value, isControlled]);
144
+
145
+ // Expose ref methods
146
+ useImperativeHandle(
147
+ ref,
148
+ () => ({
149
+ getMarkdown: async () => {
150
+ if (!editorRef.current) return '';
151
+ try {
152
+ const html = await editorRef.current.getHTML();
153
+ return htmlToMarkdown(html);
154
+ } catch {
155
+ return lastValueRef.current;
156
+ }
157
+ },
158
+ setMarkdown: (markdown: string) => {
159
+ if (editorRef.current) {
160
+ const html = markdownToHtml(markdown);
161
+ editorRef.current.setContent(html);
162
+ lastValueRef.current = markdown;
163
+ }
164
+ },
165
+ focus: () => editorRef.current?.focus(),
166
+ blur: () => editorRef.current?.blur(),
167
+ isEmpty: async () => {
168
+ if (!editorRef.current) return true;
169
+ try {
170
+ const text = await editorRef.current.getText();
171
+ return !text || text.trim().length === 0;
172
+ } catch {
173
+ return true;
174
+ }
175
+ },
176
+ clear: () => {
177
+ editorRef.current?.setContent('');
178
+ lastValueRef.current = '';
179
+ },
180
+ undo: () => editorRef.current?.undo(),
181
+ redo: () => editorRef.current?.redo(),
182
+ }),
183
+ []
184
+ );
185
+
186
+ // Map toolbar items
187
+ const toolbarItems = (toolbar.items ?? [
188
+ 'bold',
189
+ 'italic',
190
+ 'underline',
191
+ 'strikethrough',
192
+ 'code',
193
+ 'heading1',
194
+ 'heading2',
195
+ 'bulletList',
196
+ 'orderedList',
197
+ 'blockquote',
198
+ 'codeBlock',
199
+ 'link',
200
+ ])
201
+ .map((item) => TOOLBAR_ITEM_MAP[item])
202
+ .filter((item): item is string => item !== null);
203
+
204
+ const showToolbar = toolbar.visible !== false && editable;
205
+ const toolbarPosition = toolbar.position ?? 'top';
206
+
207
+ const containerStyle = [
208
+ (editorStyles.container as any)({ size, linkIntent }),
209
+ style,
210
+ ];
211
+
212
+ const editorContentStyle = [
213
+ (editorStyles.editorContent as any)({ size, linkIntent }),
214
+ { minHeight },
215
+ maxHeight !== undefined && { maxHeight },
216
+ ];
217
+
218
+ const content = (
219
+ <View
220
+ style={containerStyle}
221
+ nativeID={id}
222
+ testID={testID}
223
+ accessibilityLabel={accessibilityLabel}
224
+ >
225
+ {showToolbar && toolbarPosition === 'top' && (
226
+ <Toolbar editor={editor} items={DEFAULT_TOOLBAR_ITEMS} />
227
+ )}
228
+ <View style={editorContentStyle}>
229
+ <RichText
230
+ editor={editor}
231
+ onFocus={onFocus}
232
+ onBlur={onBlur}
233
+ style={nativeStyles.richText}
234
+ />
235
+ </View>
236
+ {showToolbar && toolbarPosition === 'bottom' && (
237
+ <Toolbar editor={editor} items={DEFAULT_TOOLBAR_ITEMS} />
238
+ )}
239
+ </View>
240
+ );
241
+
242
+ if (Platform.OS === 'ios' && avoidIosKeyboard) {
243
+ return (
244
+ <KeyboardAvoidingView behavior="padding" style={{ flex: 1 }}>
245
+ {content}
246
+ </KeyboardAvoidingView>
247
+ );
248
+ }
249
+
250
+ return content;
251
+ }
252
+ );
253
+
254
+ const nativeStyles = StyleSheet.create({
255
+ richText: {
256
+ flex: 1,
257
+ },
258
+ });
259
+
260
+ MarkdownEditor.displayName = 'MarkdownEditor';
261
+
262
+ export default MarkdownEditor;
@@ -0,0 +1,227 @@
1
+ /**
2
+ * MarkdownEditor styles using defineStyle with theme integration.
3
+ *
4
+ * Provides consistent styling for the markdown editor that
5
+ * integrates with the Idealyst theme system.
6
+ */
7
+ import { StyleSheet } from 'react-native-unistyles';
8
+ import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
9
+ import type { Theme as BaseTheme, Intent, Size } from '@idealyst/theme';
10
+
11
+ // Required: Unistyles must see StyleSheet usage in original source to process this file
12
+ void StyleSheet;
13
+
14
+ // Wrap theme for $iterator support
15
+ type Theme = ThemeStyleWrapper<BaseTheme>;
16
+
17
+ export type EditorVariants = {
18
+ size: Size;
19
+ linkIntent: Intent;
20
+ };
21
+
22
+ /**
23
+ * Dynamic props passed to editor style functions.
24
+ */
25
+ export type EditorDynamicProps = {
26
+ linkIntent?: Intent;
27
+ size?: Size;
28
+ };
29
+
30
+ /**
31
+ * Editor styles with theme integration.
32
+ */
33
+ // @ts-expect-error - MarkdownEditor is not in the ComponentName union yet
34
+ export const editorStyles = defineStyle('MarkdownEditor', (theme: Theme) => ({
35
+ // Main container
36
+ container: (_props: EditorDynamicProps) => ({
37
+ borderWidth: 1,
38
+ borderColor: theme.colors.border.primary,
39
+ borderRadius: theme.radii.md,
40
+ backgroundColor: theme.colors.surface.primary,
41
+ overflow: 'hidden',
42
+ }),
43
+
44
+ // Editor content area
45
+ editorContent: (_props: EditorDynamicProps) => ({
46
+ padding: 16,
47
+ minHeight: 200,
48
+ fontSize: theme.sizes.typography.body1.fontSize,
49
+ lineHeight: theme.sizes.typography.body1.lineHeight,
50
+ color: theme.colors.text.primary,
51
+ _web: {
52
+ outline: 'none',
53
+ // Tiptap editor styles
54
+ '& .tiptap': {
55
+ outline: 'none',
56
+ minHeight: '100%',
57
+ },
58
+ '& .tiptap p.is-editor-empty:first-child::before': {
59
+ content: 'attr(data-placeholder)',
60
+ color: theme.colors.text.tertiary,
61
+ pointerEvents: 'none',
62
+ float: 'left',
63
+ height: 0,
64
+ },
65
+ // Headings
66
+ '& .tiptap h1': {
67
+ fontSize: theme.sizes.typography.h1.fontSize,
68
+ lineHeight: theme.sizes.typography.h1.lineHeight,
69
+ fontWeight: '700',
70
+ marginTop: 24,
71
+ marginBottom: 16,
72
+ },
73
+ '& .tiptap h2': {
74
+ fontSize: theme.sizes.typography.h2.fontSize,
75
+ lineHeight: theme.sizes.typography.h2.lineHeight,
76
+ fontWeight: '600',
77
+ marginTop: 20,
78
+ marginBottom: 12,
79
+ borderBottom: `1px solid ${theme.colors.border.primary}`,
80
+ paddingBottom: 8,
81
+ },
82
+ '& .tiptap h3': {
83
+ fontSize: theme.sizes.typography.h3.fontSize,
84
+ lineHeight: theme.sizes.typography.h3.lineHeight,
85
+ fontWeight: '600',
86
+ marginTop: 16,
87
+ marginBottom: 8,
88
+ },
89
+ '& .tiptap h4, & .tiptap h5, & .tiptap h6': {
90
+ fontWeight: '600',
91
+ marginTop: 12,
92
+ marginBottom: 8,
93
+ },
94
+ // Paragraphs
95
+ '& .tiptap p': {
96
+ marginVertical: 8,
97
+ },
98
+ // Links
99
+ '& .tiptap a': {
100
+ color: theme.intents?.primary?.primary ?? theme.colors.text.primary,
101
+ textDecoration: 'underline',
102
+ cursor: 'pointer',
103
+ },
104
+ // Code
105
+ '& .tiptap code': {
106
+ fontFamily: 'monospace',
107
+ backgroundColor: theme.colors.surface.secondary,
108
+ paddingHorizontal: 6,
109
+ paddingVertical: 2,
110
+ borderRadius: theme.radii.xs,
111
+ fontSize: theme.sizes.typography.caption.fontSize,
112
+ },
113
+ '& .tiptap pre': {
114
+ fontFamily: 'monospace',
115
+ backgroundColor: theme.colors.surface.secondary,
116
+ padding: 16,
117
+ borderRadius: theme.radii.md,
118
+ marginVertical: 12,
119
+ overflow: 'auto',
120
+ },
121
+ '& .tiptap pre code': {
122
+ backgroundColor: 'transparent',
123
+ padding: 0,
124
+ },
125
+ // Blockquote
126
+ '& .tiptap blockquote': {
127
+ borderLeft: `4px solid ${theme.colors.border.secondary}`,
128
+ paddingLeft: 16,
129
+ paddingVertical: 8,
130
+ marginVertical: 12,
131
+ backgroundColor: theme.colors.surface.secondary,
132
+ borderRadius: theme.radii.sm,
133
+ fontStyle: 'italic',
134
+ color: theme.colors.text.secondary,
135
+ },
136
+ // Lists
137
+ '& .tiptap ul, & .tiptap ol': {
138
+ marginVertical: 8,
139
+ paddingLeft: 24,
140
+ },
141
+ '& .tiptap li': {
142
+ marginVertical: 4,
143
+ },
144
+ // Task list
145
+ '& .tiptap ul[data-type="taskList"]': {
146
+ listStyle: 'none',
147
+ padding: 0,
148
+ },
149
+ '& .tiptap ul[data-type="taskList"] li': {
150
+ display: 'flex',
151
+ alignItems: 'flex-start',
152
+ gap: 8,
153
+ },
154
+ '& .tiptap ul[data-type="taskList"] li input': {
155
+ marginTop: 4,
156
+ },
157
+ // Horizontal rule
158
+ '& .tiptap hr': {
159
+ height: 1,
160
+ backgroundColor: theme.colors.border.secondary,
161
+ marginVertical: 24,
162
+ border: 'none',
163
+ },
164
+ // Table
165
+ '& .tiptap table': {
166
+ borderCollapse: 'collapse',
167
+ width: '100%',
168
+ marginVertical: 12,
169
+ },
170
+ '& .tiptap th, & .tiptap td': {
171
+ border: `1px solid ${theme.colors.border.primary}`,
172
+ padding: 12,
173
+ },
174
+ '& .tiptap th': {
175
+ backgroundColor: theme.colors.surface.secondary,
176
+ fontWeight: '600',
177
+ textAlign: 'left',
178
+ },
179
+ },
180
+ }),
181
+
182
+ // Toolbar container
183
+ toolbar: (_props: EditorDynamicProps) => ({
184
+ flexDirection: 'row',
185
+ flexWrap: 'wrap',
186
+ gap: 4,
187
+ padding: 8,
188
+ borderBottomWidth: 1,
189
+ borderBottomColor: theme.colors.border.primary,
190
+ backgroundColor: theme.colors.surface.secondary,
191
+ }),
192
+
193
+ // Toolbar button
194
+ toolbarButton: (_props: EditorDynamicProps) => ({
195
+ paddingHorizontal: 10,
196
+ paddingVertical: 6,
197
+ borderRadius: theme.radii.sm,
198
+ backgroundColor: 'transparent',
199
+ borderWidth: 0,
200
+ color: theme.colors.text.primary,
201
+ fontSize: 14,
202
+ fontWeight: '500',
203
+ _web: {
204
+ cursor: 'pointer',
205
+ transition: 'background-color 0.15s ease',
206
+ _hover: {
207
+ backgroundColor: theme.colors.surface.tertiary,
208
+ },
209
+ },
210
+ }),
211
+
212
+ // Toolbar button active state
213
+ toolbarButtonActive: (_props: EditorDynamicProps) => ({
214
+ backgroundColor: theme.colors.surface.tertiary,
215
+ color: theme.intents?.primary?.primary ?? theme.colors.text.primary,
216
+ }),
217
+
218
+ // Focus ring for accessibility
219
+ focusRing: (_props: EditorDynamicProps) => ({
220
+ _web: {
221
+ _focus: {
222
+ outline: `2px solid ${theme.intents?.primary?.primary ?? theme.colors.border.primary}`,
223
+ outlineOffset: 2,
224
+ },
225
+ },
226
+ }),
227
+ }));