blumbaben 1.0.0

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/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ragaeeb Haq
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,452 @@
1
+ # blumbaben
2
+
3
+ A lightweight TypeScript React hook library for adding formatting toolbars to text inputs and textareas. Show contextual formatting options when users focus on input fields.
4
+
5
+ [![wakatime](https://wakatime.com/badge/user/a0b906ce-b8e7-4463-8bce-383238df6d4b/project/897368ef-62b9-48be-bba9-7c530f10e3da.svg)](https://wakatime.com/badge/user/a0b906ce-b8e7-4463-8bce-383238df6d4b/project/897368ef-62b9-48be-bba9-7c530f10e3da)
6
+ ![Bun](https://img.shields.io/badge/Bun-%23000000.svg?style=for-the-badge&logo=bun&logoColor=white)
7
+ [![Node.js CI](https://github.com/ragaeeb/blumbaben/actions/workflows/build.yml/badge.svg)](https://github.com/ragaeeb/blumbaben/actions/workflows/build.yml)
8
+ ![GitHub License](https://img.shields.io/github/license/ragaeeb/blumbaben)
9
+ ![GitHub Release](https://img.shields.io/github/v/release/ragaeeb/blumbaben)
10
+ [![Size](https://deno.bundlejs.com/badge?q=blumbaben@latest&badge=detailed)](https://bundlejs.com/?q=blumbaben%40latest)
11
+ ![typescript](https://badgen.net/badge/icon/typescript?icon=typescript&label&color=blue)
12
+ ![npm](https://img.shields.io/npm/dm/blumbaben)
13
+ ![GitHub issues](https://img.shields.io/github/issues/ragaeeb/blumbaben)
14
+ ![GitHub stars](https://img.shields.io/github/stars/ragaeeb/blumbaben?style=social)
15
+ [![npm version](https://badge.fury.io/js/blumbaben.svg)](https://badge.fury.io/js/blumbaben)
16
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
17
+
18
+ ## ✨ Features
19
+
20
+ - **🎯 Global State Management** - Single toolbar instance shared across all inputs
21
+ - **📱 Smart Positioning** - Automatic toolbar positioning with customizable placement
22
+ - **🎨 Flexible Styling** - Bring your own UI components and styles
23
+ - **⚡ TypeScript Support** - Full type safety and excellent DX
24
+ - **🪶 Lightweight** - Minimal bundle size with no external dependencies
25
+ - **🔧 Configurable** - Customizable behavior and positioning
26
+ - **♿ Accessible** - Built with accessibility in mind
27
+
28
+ ## 📦 Installation
29
+
30
+ ```bash
31
+ npm install blumbaben
32
+ ```
33
+
34
+ ```bash
35
+ bun add blumbaben
36
+ ```
37
+
38
+ ```bash
39
+ pnpm add blumbaben
40
+ ```
41
+
42
+ ## 🚀 Quick Start
43
+
44
+ ### Basic Usage with Hook
45
+
46
+ ```tsx
47
+ import React, { useState } from 'react';
48
+ import { useFormattingToolbar } from 'blumbaben';
49
+
50
+ function MyComponent() {
51
+ const [content, setContent] = useState('');
52
+ const { getInputProps, isVisible, applyFormat } = useFormattingToolbar();
53
+
54
+ return (
55
+ <div>
56
+ <textarea
57
+ {...getInputProps()}
58
+ value={content}
59
+ onChange={(e) => setContent(e.target.value)}
60
+ placeholder="Focus me to see the toolbar!"
61
+ />
62
+
63
+ {isVisible && (
64
+ <div className="toolbar">
65
+ <button onClick={() => applyFormat((text) => text.toUpperCase())}>UPPERCASE</button>
66
+ <button onClick={() => applyFormat((text) => `**${text}**`)}>Bold</button>
67
+ </div>
68
+ )}
69
+ </div>
70
+ );
71
+ }
72
+ ```
73
+
74
+ ### Using the FormattingToolbar Component
75
+
76
+ ```tsx
77
+ import React, { useState } from 'react';
78
+ import { useFormattingToolbar, FormattingToolbar } from 'blumbaben';
79
+
80
+ function MyComponent() {
81
+ const [content, setContent] = useState('');
82
+ const { getInputProps } = useFormattingToolbar();
83
+
84
+ return (
85
+ <div>
86
+ <textarea
87
+ {...getInputProps()}
88
+ value={content}
89
+ onChange={(e) => setContent(e.target.value)}
90
+ placeholder="Focus me to see the toolbar!"
91
+ />
92
+
93
+ <FormattingToolbar>
94
+ {(applyFormat) => (
95
+ <div className="flex gap-2 p-2 bg-white border rounded shadow">
96
+ <button
97
+ onClick={() => applyFormat((text) => text.toUpperCase())}
98
+ className="px-2 py-1 bg-blue-500 text-white rounded"
99
+ >
100
+ UPPERCASE
101
+ </button>
102
+ <button
103
+ onClick={() => applyFormat((text) => `**${text}**`)}
104
+ className="px-2 py-1 bg-green-500 text-white rounded"
105
+ >
106
+ Bold
107
+ </button>
108
+ <button
109
+ onClick={() => applyFormat((text) => text.replace(/\n/g, ' '))}
110
+ className="px-2 py-1 bg-red-500 text-white rounded"
111
+ >
112
+ Remove Line Breaks
113
+ </button>
114
+ </div>
115
+ )}
116
+ </FormattingToolbar>
117
+ </div>
118
+ );
119
+ }
120
+ ```
121
+
122
+ ### Using the Higher-Order Component
123
+
124
+ ```tsx
125
+ import React, { useState } from 'react';
126
+ import { withFormattingToolbar, FormattingToolbar } from 'blumbaben';
127
+
128
+ // Enhance your existing textarea component
129
+ const MyTextarea = ({ value, onChange, ...props }) => <textarea value={value} onChange={onChange} {...props} />;
130
+
131
+ const TextareaWithToolbar = withFormattingToolbar(MyTextarea);
132
+
133
+ function App() {
134
+ const [content, setContent] = useState('');
135
+
136
+ return (
137
+ <div>
138
+ <TextareaWithToolbar
139
+ value={content}
140
+ onChange={(e) => setContent(e.target.value)}
141
+ placeholder="Focus me to see the toolbar!"
142
+ />
143
+
144
+ <FormattingToolbar>
145
+ {(applyFormat) => (
146
+ <div className="toolbar-buttons">
147
+ <button onClick={() => applyFormat((text) => text.toUpperCase())}>UPPERCASE</button>
148
+ <button onClick={() => applyFormat((text) => text.toLowerCase())}>lowercase</button>
149
+ </div>
150
+ )}
151
+ </FormattingToolbar>
152
+ </div>
153
+ );
154
+ }
155
+ ```
156
+
157
+ ## 🔧 Configuration
158
+
159
+ ### Toolbar Configuration Options
160
+
161
+ ```tsx
162
+ interface ToolbarConfig {
163
+ // Custom positioning function
164
+ getPosition?: (element: TextInputElement) => ToolbarPosition;
165
+
166
+ // Delay before hiding toolbar after blur (ms)
167
+ hideDelay?: number; // default: 500
168
+
169
+ // Prevent toolbar from closing when clicked
170
+ preventCloseOnClick?: boolean; // default: true
171
+ }
172
+ ```
173
+
174
+ ### Custom Positioning
175
+
176
+ ```tsx
177
+ const { getInputProps, isVisible, applyFormat } = useFormattingToolbar({
178
+ getPosition: (element) => {
179
+ const rect = element.getBoundingClientRect();
180
+ return {
181
+ x: rect.left,
182
+ y: rect.top - 50, // Position above the element
183
+ };
184
+ },
185
+ hideDelay: 300,
186
+ preventCloseOnClick: true,
187
+ });
188
+ ```
189
+
190
+ ### Styling the Toolbar
191
+
192
+ ```tsx
193
+ <FormattingToolbar
194
+ className="my-custom-toolbar"
195
+ style={{
196
+ backgroundColor: 'white',
197
+ border: '1px solid #ccc',
198
+ borderRadius: '4px',
199
+ padding: '8px'
200
+ }}
201
+ >
202
+ {(applyFormat) => (
203
+ // Your toolbar content
204
+ )}
205
+ </FormattingToolbar>
206
+ ```
207
+
208
+ ## 📚 API Reference
209
+
210
+ ### Hooks
211
+
212
+ #### `useFormattingToolbar(config?: ToolbarConfig)`
213
+
214
+ Main hook for managing toolbar functionality.
215
+
216
+ **Returns:**
217
+
218
+ - `getInputProps()` - Props to spread on input/textarea elements
219
+ - `getToolbarProps()` - Props for toolbar container (includes positioning)
220
+ - `applyFormat(formatter)` - Apply formatting to active element
221
+ - `showToolbar(element)` - Manually show toolbar
222
+ - `hideToolbar()` - Manually hide toolbar
223
+ - `isVisible` - Whether toolbar is visible
224
+ - `toolbarState` - Current toolbar state
225
+
226
+ #### `useFormattingToolbarState()`
227
+
228
+ Lightweight hook for toolbar-only components that don't handle input events.
229
+
230
+ **Returns:**
231
+
232
+ - `applyFormat(formatter)` - Apply formatting to active element
233
+ - `isVisible` - Whether toolbar is visible
234
+ - `toolbarState` - Current toolbar state
235
+
236
+ ### Components
237
+
238
+ #### `FormattingToolbar`
239
+
240
+ Renders the toolbar when an input is focused.
241
+
242
+ **Props:**
243
+
244
+ - `children` - Render function receiving `applyFormat` callback
245
+ - `as?` - Container element type (default: 'div')
246
+ - `className?` - CSS class name
247
+ - `config?` - Toolbar configuration
248
+ - `style?` - Inline styles
249
+
250
+ #### `withFormattingToolbar(Component, config?)`
251
+
252
+ Higher-order component that adds toolbar functionality to input components.
253
+
254
+ ### Types
255
+
256
+ ```tsx
257
+ type FormatterFunction = (text: string) => string;
258
+
259
+ type TextInputElement = HTMLInputElement | HTMLTextAreaElement;
260
+
261
+ type ToolbarPosition = {
262
+ x: number;
263
+ y: number;
264
+ };
265
+
266
+ type ToolbarState = {
267
+ activeElement: TextInputElement | null;
268
+ isVisible: boolean;
269
+ position: ToolbarPosition | null;
270
+ };
271
+ ```
272
+
273
+ ## 💡 Common Formatting Functions
274
+
275
+ Here are some useful formatting functions you can use:
276
+
277
+ ```tsx
278
+ // Text transformations
279
+ const toUpperCase = (text: string) => text.toUpperCase();
280
+ const toLowerCase = (text: string) => text.toLowerCase();
281
+ const toTitleCase = (text: string) =>
282
+ text.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
283
+
284
+ // Markdown formatting
285
+ const makeBold = (text: string) => `**${text}**`;
286
+ const makeItalic = (text: string) => `*${text}*`;
287
+ const makeCode = (text: string) => `\`${text}\``;
288
+
289
+ // Text cleaning
290
+ const removeLineBreaks = (text: string) => text.replace(/\n/g, ' ');
291
+ const trimWhitespace = (text: string) => text.trim();
292
+ const removeExtraSpaces = (text: string) => text.replace(/\s+/g, ' ');
293
+
294
+ // Usage
295
+ <button onClick={() => applyFormat(makeBold)}>Bold</button>;
296
+ ```
297
+
298
+ ## 🎨 Styling Examples
299
+
300
+ ### With Tailwind CSS
301
+
302
+ ```tsx
303
+ <FormattingToolbar className="bg-white border border-gray-200 rounded-lg shadow-lg p-2">
304
+ {(applyFormat) => (
305
+ <div className="flex gap-1">
306
+ <button
307
+ onClick={() => applyFormat(toUpperCase)}
308
+ className="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600"
309
+ >
310
+ ABC
311
+ </button>
312
+ <button
313
+ onClick={() => applyFormat(toLowerCase)}
314
+ className="px-3 py-1 text-sm bg-green-500 text-white rounded hover:bg-green-600"
315
+ >
316
+ abc
317
+ </button>
318
+ </div>
319
+ )}
320
+ </FormattingToolbar>
321
+ ```
322
+
323
+ ### With CSS Modules
324
+
325
+ ```css
326
+ /* styles.module.css */
327
+ .toolbar {
328
+ background: white;
329
+ border: 1px solid #e2e8f0;
330
+ border-radius: 8px;
331
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
332
+ padding: 8px;
333
+ }
334
+
335
+ .toolbarButton {
336
+ padding: 4px 12px;
337
+ margin-right: 4px;
338
+ border: none;
339
+ border-radius: 4px;
340
+ cursor: pointer;
341
+ transition: background-color 0.2s;
342
+ }
343
+
344
+ .toolbarButton:hover {
345
+ background-color: #f1f5f9;
346
+ }
347
+ ```
348
+
349
+ ```tsx
350
+ import styles from './styles.module.css';
351
+
352
+ <FormattingToolbar className={styles.toolbar}>
353
+ {(applyFormat) => (
354
+ <div>
355
+ <button className={styles.toolbarButton} onClick={() => applyFormat(makeBold)}>
356
+ Bold
357
+ </button>
358
+ </div>
359
+ )}
360
+ </FormattingToolbar>;
361
+ ```
362
+
363
+ ## 🔍 Advanced Usage
364
+
365
+ ### Multiple Input Fields
366
+
367
+ The library automatically manages a single toolbar across multiple inputs:
368
+
369
+ ```tsx
370
+ function MultiInputForm() {
371
+ const { getInputProps } = useFormattingToolbar();
372
+ const [field1, setField1] = useState('');
373
+ const [field2, setField2] = useState('');
374
+
375
+ return (
376
+ <div>
377
+ <input
378
+ {...getInputProps()}
379
+ value={field1}
380
+ onChange={(e) => setField1(e.target.value)}
381
+ placeholder="First field"
382
+ />
383
+
384
+ <textarea
385
+ {...getInputProps()}
386
+ value={field2}
387
+ onChange={(e) => setField2(e.target.value)}
388
+ placeholder="Second field"
389
+ />
390
+
391
+ <FormattingToolbar>
392
+ {(applyFormat) => (
393
+ <div>
394
+ <button onClick={() => applyFormat(toUpperCase)}>UPPERCASE</button>
395
+ </div>
396
+ )}
397
+ </FormattingToolbar>
398
+ </div>
399
+ );
400
+ }
401
+ ```
402
+
403
+ ### Custom Formatter with Selection
404
+
405
+ ```tsx
406
+ const wrapWithQuotes = (text: string) => `"${text}"`;
407
+ const addPrefix = (text: string) => `• ${text}`;
408
+
409
+ // The library automatically handles whether text is selected or not
410
+ <button onClick={() => applyFormat(wrapWithQuotes)}>Add Quotes</button>;
411
+ ```
412
+
413
+ ### Conditional Toolbar Content
414
+
415
+ ```tsx
416
+ <FormattingToolbar>
417
+ {(applyFormat) => {
418
+ const { activeElement } = useFormattingToolbarState().toolbarState;
419
+ const isTextarea = activeElement?.tagName === 'TEXTAREA';
420
+
421
+ return (
422
+ <div>
423
+ <button onClick={() => applyFormat(toUpperCase)}>UPPERCASE</button>
424
+ {isTextarea && <button onClick={() => applyFormat(removeLineBreaks)}>Remove Line Breaks</button>}
425
+ </div>
426
+ );
427
+ }}
428
+ </FormattingToolbar>
429
+ ```
430
+
431
+ ## 🤝 Contributing
432
+
433
+ Contributions are welcome! Please feel free to submit a Pull Request.
434
+
435
+ 1. Fork the repository
436
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
437
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
438
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
439
+ 5. Open a Pull Request
440
+
441
+ ## 📄 License
442
+
443
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
444
+
445
+ ## 🙏 Acknowledgments
446
+
447
+ - Built with TypeScript and React
448
+ - Inspired by modern text editing interfaces
449
+ - Designed for developer experience and flexibility
450
+ - Asmāʾ for the project name
451
+
452
+ ---
@@ -0,0 +1,306 @@
1
+ import React$1, { JSX } from 'react';
2
+
3
+ /**
4
+ * Function type for text formatting operations.
5
+ * Takes a string input and returns a transformed string.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const boldFormatter: FormatterFunction = (text) => `**${text}**`;
10
+ * const upperCaseFormatter: FormatterFunction = (text) => text.toUpperCase();
11
+ * ```
12
+ */
13
+ type FormatterFunction = (text: string) => string;
14
+ /**
15
+ * Union type representing supported HTML input elements for text formatting.
16
+ * Includes both single-line inputs and multi-line textareas.
17
+ */
18
+ type TextInputElement = HTMLInputElement | HTMLTextAreaElement;
19
+ /**
20
+ * Configuration options for the formatting toolbar behavior and appearance.
21
+ */
22
+ type ToolbarConfig = {
23
+ /**
24
+ * Custom positioning function to determine where the toolbar appears relative to the focused element.
25
+ * If not provided, defaults to positioning below the element.
26
+ *
27
+ * @param {TextInputElement} element - The focused input or textarea element
28
+ * @returns {ToolbarPosition} Position coordinates for the toolbar
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const config: ToolbarConfig = {
33
+ * getPosition: (element) => {
34
+ * const rect = element.getBoundingClientRect();
35
+ * return { x: rect.left, y: rect.top - 50 }; // Above the element
36
+ * }
37
+ * };
38
+ * ```
39
+ */
40
+ getPosition?: (element: TextInputElement) => ToolbarPosition;
41
+ /**
42
+ * Delay in milliseconds before hiding the toolbar after the input loses focus.
43
+ * This allows users to click on toolbar buttons without the toolbar disappearing.
44
+ *
45
+ * @default 500
46
+ */
47
+ hideDelay?: number;
48
+ /**
49
+ * Whether to prevent the toolbar from closing when clicked.
50
+ * When true, clicking toolbar buttons won't cause the input to lose focus.
51
+ *
52
+ * @default true
53
+ */
54
+ preventCloseOnClick?: boolean;
55
+ };
56
+ /**
57
+ * Represents the x,y coordinates for positioning the toolbar on screen.
58
+ *
59
+ * @interface ToolbarPosition
60
+ */
61
+ type ToolbarPosition = {
62
+ /** Horizontal position in pixels from the left edge of the viewport */
63
+ x: number;
64
+ /** Vertical position in pixels from the top edge of the viewport */
65
+ y: number;
66
+ };
67
+ /**
68
+ * Current state of the formatting toolbar including visibility, position, and active element.
69
+ *
70
+ * @interface ToolbarState
71
+ */
72
+ type ToolbarState = {
73
+ /** The currently focused input/textarea element, or null if no element is active */
74
+ activeElement: null | TextInputElement;
75
+ /** Whether the toolbar is currently visible to the user */
76
+ isVisible: boolean;
77
+ /** Current position of the toolbar, or null if not positioned */
78
+ position: null | ToolbarPosition;
79
+ };
80
+
81
+ /**
82
+ * Props for the FormattingToolbar component.
83
+ *
84
+ * @interface FormattingToolbarProps
85
+ */
86
+ type FormattingToolbarProps = {
87
+ /**
88
+ * Container element type for the toolbar.
89
+ * Can be any valid HTML element tag name.
90
+ *
91
+ * @default 'div'
92
+ */
93
+ as?: keyof JSX.IntrinsicElements;
94
+ /**
95
+ * Render function that receives the applyFormat callback.
96
+ * Use this function to render your toolbar buttons and controls.
97
+ *
98
+ * @param {(formatter: FormatterFunction) => void} applyFormat - Function to apply text formatting
99
+ * @returns {React.ReactNode} The toolbar content to render
100
+ */
101
+ children: (applyFormat: (formatter: FormatterFunction) => void) => React$1.ReactNode;
102
+ /**
103
+ * Custom CSS class name for styling the toolbar container.
104
+ */
105
+ className?: string;
106
+ /**
107
+ * Toolbar configuration options.
108
+ * Merged with default configuration.
109
+ */
110
+ config?: ToolbarConfig;
111
+ /**
112
+ * Custom inline styles for the toolbar container.
113
+ * These styles are merged with the positioning styles (position, top, left, zIndex).
114
+ */
115
+ style?: React$1.CSSProperties;
116
+ };
117
+ /**
118
+ * Formatting toolbar component that renders when an input is focused.
119
+ * Automatically uses the global toolbar state - no need to pass a toolbar instance.
120
+ * Only renders when the global toolbar is visible and has a position.
121
+ *
122
+ * The toolbar automatically positions itself relative to the focused input element
123
+ * and provides an applyFormat function to child components for text transformation.
124
+ *
125
+ * @param {FormattingToolbarProps} props - Component props
126
+ * @returns {React.ReactElement | null} The toolbar element or null if not visible
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * import { FormattingToolbar } from 'blumbaben';
131
+ * import { Button } from './ui/button';
132
+ *
133
+ * function App() {
134
+ * return (
135
+ * <div>
136
+ * <textarea {...getInputProps()} />
137
+ *
138
+ * <FormattingToolbar className="my-toolbar" as="section">
139
+ * {(applyFormat) => (
140
+ * <>
141
+ * <Button onClick={() => applyFormat(text => text.toUpperCase())}>
142
+ * UPPERCASE
143
+ * </Button>
144
+ * <Button onClick={() => applyFormat(text => `**${text}**`)}>
145
+ * Bold
146
+ * </Button>
147
+ * <Button onClick={() => applyFormat(text => text.replace(/\n/g, ' '))}>
148
+ * Remove Line Breaks
149
+ * </Button>
150
+ * </>
151
+ * )}
152
+ * </FormattingToolbar>
153
+ * </div>
154
+ * );
155
+ * }
156
+ * ```
157
+ */
158
+ declare const FormattingToolbar: React$1.FC<FormattingToolbarProps>;
159
+
160
+ /**
161
+ * Return type for the useFormattingToolbar hook containing all toolbar functionality.
162
+ *
163
+ * @interface UseFormattingToolbarResult
164
+ */
165
+ type UseFormattingToolbarResult = {
166
+ /**
167
+ * Apply formatting to the currently active element.
168
+ *
169
+ * @param {FormatterFunction} formatter - Function to transform the selected or entire text
170
+ */
171
+ applyFormat: (formatter: FormatterFunction) => void;
172
+ /**
173
+ * Props to spread on your input/textarea components.
174
+ * Includes onFocus and onBlur handlers for toolbar management.
175
+ *
176
+ * @returns {object} Props object with focus and blur handlers
177
+ */
178
+ getInputProps: () => {
179
+ onBlur: (e: React.FocusEvent<TextInputElement>) => void;
180
+ onFocus: (e: React.FocusEvent<TextInputElement>) => void;
181
+ };
182
+ /**
183
+ * Props for the toolbar container element.
184
+ * Includes positioning styles and optional mouse event handlers.
185
+ *
186
+ * @returns {object} Props object with styles and event handlers
187
+ */
188
+ getToolbarProps: () => {
189
+ onMouseDown?: (e: React.MouseEvent) => void;
190
+ style: React.CSSProperties;
191
+ };
192
+ /** Function to manually hide the toolbar */
193
+ hideToolbar: () => void;
194
+ /** Whether the toolbar is currently visible */
195
+ isVisible: boolean;
196
+ /**
197
+ * Function to manually show the toolbar for a specific element.
198
+ *
199
+ * @param {TextInputElement} element - The element to show the toolbar for
200
+ */
201
+ showToolbar: (element: TextInputElement) => void;
202
+ /** Current toolbar state (shared globally across all instances) */
203
+ toolbarState: ToolbarState;
204
+ };
205
+ /**
206
+ * Hook for managing formatting toolbar functionality.
207
+ * Uses global state so all instances share the same toolbar - only one toolbar
208
+ * can be visible at a time across the entire application.
209
+ *
210
+ * @param {ToolbarConfig} [config={}] - Optional configuration for toolbar behavior
211
+ * @returns {UseFormattingToolbarResult} Object containing toolbar state and control functions
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * function MyComponent() {
216
+ * const { getInputProps, isVisible, applyFormat } = useFormattingToolbar({
217
+ * hideDelay: 300,
218
+ * getPosition: (element) => ({ x: 100, y: 200 })
219
+ * });
220
+ *
221
+ * return (
222
+ * <div>
223
+ * <textarea {...getInputProps()} />
224
+ * {isVisible && (
225
+ * <div>
226
+ * <button onClick={() => applyFormat(text => text.toUpperCase())}>
227
+ * UPPERCASE
228
+ * </button>
229
+ * </div>
230
+ * )}
231
+ * </div>
232
+ * );
233
+ * }
234
+ * ```
235
+ */
236
+ declare const useFormattingToolbar: (config?: ToolbarConfig) => UseFormattingToolbarResult;
237
+
238
+ /**
239
+ * Applies a formatting function to either selected text or entire content of an element.
240
+ * If text is selected (selectionStart !== selectionEnd), formats only the selected portion.
241
+ * If no text is selected, formats the entire content of the element.
242
+ * Returns the formatted result without modifying the original element.
243
+ *
244
+ * @param {HTMLInputElement | HTMLTextAreaElement} element - The HTML input or textarea element containing the text
245
+ * @param {(text: string) => string} formatter - Function that takes a string and returns a formatted version
246
+ * @returns {string} The formatted text with either selected portion or entire content transformed
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * const textarea = document.querySelector('textarea');
251
+ * const uppercaseFormatter = (text: string) => text.toUpperCase();
252
+ * const result = applyFormattingOnSelection(textarea, uppercaseFormatter);
253
+ * ```
254
+ */
255
+ declare const applyFormattingOnSelection: (element: HTMLInputElement | HTMLTextAreaElement, formatter: (text: string) => string) => string;
256
+ /**
257
+ * Updates the value of an input or textarea element and optionally triggers onChange event.
258
+ * Creates a synthetic React change event if onChange callback is provided.
259
+ * Useful for programmatically updating form elements while maintaining React state consistency.
260
+ *
261
+ * @param {HTMLInputElement | HTMLTextAreaElement} element - The HTML input or textarea element to update
262
+ * @param {string} newValue - The new string value to set on the element
263
+ * @param {(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void} [onChange] - Optional React onChange event handler to call after updating the value
264
+ *
265
+ * @example
266
+ * ```typescript
267
+ * const handleChange = (e) => setFormValue(e.target.value);
268
+ * updateElementValue(textareaRef.current, 'New content', handleChange);
269
+ * ```
270
+ */
271
+ declare const updateElementValue: (element: HTMLInputElement | HTMLTextAreaElement, newValue: string, onChange?: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void) => void;
272
+
273
+ /**
274
+ * Higher-order component that adds formatting toolbar functionality to input components.
275
+ * Since the library uses global state, all wrapped components automatically share the same toolbar.
276
+ * Only one toolbar will be visible at a time, appearing for whichever input is currently focused.
277
+ *
278
+ * @template P - The props type of the wrapped component
279
+ * @param {React.ComponentType<P>} Component - The input component to enhance with toolbar functionality
280
+ * @param {ToolbarConfig} [config={}] - Optional configuration for toolbar behavior
281
+ * @returns {React.ForwardRefExoticComponent} Enhanced component with toolbar functionality
282
+ *
283
+ * @example
284
+ * ```typescript
285
+ * import { Textarea } from './ui/textarea';
286
+ * import { withFormattingToolbar } from 'blumbaben';
287
+ *
288
+ * const TextareaWithToolbar = withFormattingToolbar(Textarea, {
289
+ * hideDelay: 300,
290
+ * getPosition: (element) => {
291
+ * const rect = element.getBoundingClientRect();
292
+ * return { x: rect.left, y: rect.top - 50 }; // Above the element
293
+ * }
294
+ * });
295
+ *
296
+ * // Usage
297
+ * <TextareaWithToolbar
298
+ * value={content}
299
+ * onChange={setContent}
300
+ * placeholder="Start typing..."
301
+ * />
302
+ * ```
303
+ */
304
+ declare const withFormattingToolbar: <P extends Record<string, any>>(Component: React$1.ComponentType<P>, config?: ToolbarConfig) => React$1.ForwardRefExoticComponent<React$1.PropsWithoutRef<P> & React$1.RefAttributes<TextInputElement>>;
305
+
306
+ export { type FormatterFunction, FormattingToolbar, type TextInputElement, type ToolbarConfig, type ToolbarPosition, type ToolbarState, applyFormattingOnSelection, updateElementValue, useFormattingToolbar, withFormattingToolbar };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import I from"react";import{useCallback as S,useEffect as x,useState as y}from"react";var T=(o,t)=>{let n=o.selectionEnd??0,e=o.selectionStart??0,r=o.value??"";if(n>e){let s=r.substring(0,e),a=r.substring(e,n),m=r.substring(n);return s+t(a)+m}return t(r)},v=(o,t,n)=>{o.value=t,o.dispatchEvent(new Event("input",{bubbles:!0})),n&&n({currentTarget:o,target:o})};var F=o=>{let t=o.getBoundingClientRect();return{x:t.left,y:t.bottom+5}},d=class{activeElementOnChange;hideTimeout=null;state={activeElement:null,isVisible:!1,position:null};subscribers=new Set;applyFormat(t,n){let{activeElement:e}=this.state;if(!e){console.warn("No active element found for formatting");return}let r=T(e,t),s=n||this.activeElementOnChange;v(e,r,s),e.focus()}cancelScheduledHide(){this.clearHideTimeout()}getState(){return{...this.state}}hideToolbar(){this.clearHideTimeout(),this.activeElementOnChange=void 0,this.setState({activeElement:null,isVisible:!1,position:null})}scheduleHide(t){this.clearHideTimeout(),this.hideTimeout=setTimeout(()=>this.hideToolbar(),t)}showToolbar(t,n=F){this.clearHideTimeout();let e=t.props||{};this.activeElementOnChange=e.onChange,this.setState({activeElement:t,isVisible:!0,position:n(t)})}subscribe(t){return this.subscribers.add(t),()=>{this.subscribers.delete(t)}}clearHideTimeout(){this.hideTimeout&&(clearTimeout(this.hideTimeout),this.hideTimeout=null)}setState(t){this.state={...this.state,...t},this.subscribers.forEach(n=>n(this.getState()))}},i=new d;var h=()=>{let[o,t]=y(i.getState());return x(()=>i.subscribe(t),[]),{applyFormat:S(e=>{i.applyFormat(e)},[]),isVisible:o.isVisible,toolbarState:o}};var A=({as:o="div",children:t,className:n="",config:e={},style:r={}})=>{let{applyFormat:s,toolbarState:a}=h(),{preventCloseOnClick:m=!0}=e;if(!a.isVisible||!a.position)return null;let b=m?p=>{p.preventDefault()}:void 0;return I.createElement(o,{className:n,onMouseDown:b,style:{left:a.position.x,position:"fixed",top:a.position.y,zIndex:1e3,...r}},t(s))};import{useCallback as c,useEffect as R,useState as C}from"react";var P=o=>{let t=o.getBoundingClientRect();return{x:t.left,y:t.bottom+5}},f=(o={})=>{let{getPosition:t=P,hideDelay:n=500,preventCloseOnClick:e=!0}=o,[r,s]=C(i.getState());R(()=>i.subscribe(s),[]);let a=c(l=>{i.showToolbar(l,t)},[t]),m=c(()=>{i.hideToolbar()},[]),b=c(l=>{a(l.currentTarget)},[a]),p=c(()=>{i.scheduleHide(n)},[n]),u=c(l=>{i.applyFormat(l)},[]),E=c(()=>({onBlur:p,onFocus:b}),[b,p]),g=c(()=>({style:{left:r.position?.x??0,position:"fixed",top:r.position?.y??0,zIndex:1e3},...e&&{onMouseDown:l=>{l.preventDefault(),i.cancelScheduledHide()}}}),[r.position,e]);return{applyFormat:u,getInputProps:E,getToolbarProps:g,hideToolbar:m,isVisible:r.isVisible,showToolbar:a,toolbarState:r}};import H,{forwardRef as M}from"react";var $=(o,t={})=>{let n=M((e,r)=>{let{getInputProps:s}=f(t),a=s(),p={...e,onBlur:u=>{a.onBlur(u),e.onBlur&&typeof e.onBlur=="function"&&e.onBlur(u)},onFocus:u=>{a.onFocus(u),e.onFocus&&typeof e.onFocus=="function"&&e.onFocus(u)},ref:r};return H.createElement(o,p)});return n.displayName=`withFormattingToolbar(${o.displayName||o.name})`,n};export{A as FormattingToolbar,T as applyFormattingOnSelection,v as updateElementValue,f as useFormattingToolbar,$ as withFormattingToolbar};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/formatting-toolbar.tsx","../src/hooks/useFormattingToolbarState.ts","../src/utils/domUtils.ts","../src/utils/globalToolbarManager.ts","../src/hooks/useFormattingToolbar.ts","../src/withFormattingToolbar.ts"],"sourcesContent":["import React, { type JSX } from 'react';\n\nimport type { FormatterFunction, ToolbarConfig } from './types';\n\nimport { useFormattingToolbarState } from './hooks/useFormattingToolbarState';\n\n/**\n * Props for the FormattingToolbar component.\n *\n * @interface FormattingToolbarProps\n */\ntype FormattingToolbarProps = {\n /**\n * Container element type for the toolbar.\n * Can be any valid HTML element tag name.\n *\n * @default 'div'\n */\n as?: keyof JSX.IntrinsicElements;\n\n /**\n * Render function that receives the applyFormat callback.\n * Use this function to render your toolbar buttons and controls.\n *\n * @param {(formatter: FormatterFunction) => void} applyFormat - Function to apply text formatting\n * @returns {React.ReactNode} The toolbar content to render\n */\n children: (applyFormat: (formatter: FormatterFunction) => void) => React.ReactNode;\n\n /**\n * Custom CSS class name for styling the toolbar container.\n */\n className?: string;\n\n /**\n * Toolbar configuration options.\n * Merged with default configuration.\n */\n config?: ToolbarConfig;\n\n /**\n * Custom inline styles for the toolbar container.\n * These styles are merged with the positioning styles (position, top, left, zIndex).\n */\n style?: React.CSSProperties;\n};\n\n/**\n * Formatting toolbar component that renders when an input is focused.\n * Automatically uses the global toolbar state - no need to pass a toolbar instance.\n * Only renders when the global toolbar is visible and has a position.\n *\n * The toolbar automatically positions itself relative to the focused input element\n * and provides an applyFormat function to child components for text transformation.\n *\n * @param {FormattingToolbarProps} props - Component props\n * @returns {React.ReactElement | null} The toolbar element or null if not visible\n *\n * @example\n * ```typescript\n * import { FormattingToolbar } from 'blumbaben';\n * import { Button } from './ui/button';\n *\n * function App() {\n * return (\n * <div>\n * <textarea {...getInputProps()} />\n *\n * <FormattingToolbar className=\"my-toolbar\" as=\"section\">\n * {(applyFormat) => (\n * <>\n * <Button onClick={() => applyFormat(text => text.toUpperCase())}>\n * UPPERCASE\n * </Button>\n * <Button onClick={() => applyFormat(text => `**${text}**`)}>\n * Bold\n * </Button>\n * <Button onClick={() => applyFormat(text => text.replace(/\\n/g, ' '))}>\n * Remove Line Breaks\n * </Button>\n * </>\n * )}\n * </FormattingToolbar>\n * </div>\n * );\n * }\n * ```\n */\nexport const FormattingToolbar: React.FC<FormattingToolbarProps> = ({\n as: Component = 'div',\n children,\n className = '',\n config = {},\n style = {},\n}) => {\n const { applyFormat, toolbarState } = useFormattingToolbarState();\n const { preventCloseOnClick = true } = config;\n\n if (!toolbarState.isVisible || !toolbarState.position) {\n return null;\n }\n\n const handleMouseDown = preventCloseOnClick\n ? (e: React.MouseEvent) => {\n e.preventDefault(); // Prevent blur event when clicking toolbar\n }\n : undefined;\n\n return React.createElement(\n Component,\n {\n className,\n onMouseDown: handleMouseDown,\n style: {\n left: toolbarState.position.x,\n position: 'fixed',\n top: toolbarState.position.y,\n zIndex: 1000,\n ...style,\n },\n },\n children(applyFormat),\n );\n};\n","import { useCallback, useEffect, useState } from 'react';\n\nimport type { FormatterFunction, ToolbarState } from '@/types';\n\nimport { globalToolbarManager } from '@/utils/globalToolbarManager';\n\n/**\n * Lightweight hook that only subscribes to toolbar state without creating input handlers.\n * Useful for toolbar-only components that don't need to handle input focus/blur events.\n * Perfect for creating separate toolbar components that respond to the global toolbar state.\n *\n * @returns {object} Object containing applyFormat function, visibility state, and toolbar state\n *\n * @example\n * ```typescript\n * function ToolbarComponent() {\n * const { applyFormat, isVisible, toolbarState } = useFormattingToolbarState();\n *\n * if (!isVisible) return null;\n *\n * return (\n * <div style={{ position: 'fixed', top: toolbarState.position?.y, left: toolbarState.position?.x }}>\n * <button onClick={() => applyFormat(text => `**${text}**`)}>\n * Bold\n * </button>\n * </div>\n * );\n * }\n * ```\n */\nexport const useFormattingToolbarState = () => {\n const [toolbarState, setToolbarState] = useState<ToolbarState>(globalToolbarManager.getState());\n\n useEffect(() => {\n const unsubscribe = globalToolbarManager.subscribe(setToolbarState);\n return unsubscribe;\n }, []);\n\n const applyFormat = useCallback((formatter: FormatterFunction) => {\n globalToolbarManager.applyFormat(formatter);\n }, []);\n\n return {\n applyFormat,\n isVisible: toolbarState.isVisible,\n toolbarState,\n };\n};\n","/**\n * Applies a formatting function to either selected text or entire content of an element.\n * If text is selected (selectionStart !== selectionEnd), formats only the selected portion.\n * If no text is selected, formats the entire content of the element.\n * Returns the formatted result without modifying the original element.\n *\n * @param {HTMLInputElement | HTMLTextAreaElement} element - The HTML input or textarea element containing the text\n * @param {(text: string) => string} formatter - Function that takes a string and returns a formatted version\n * @returns {string} The formatted text with either selected portion or entire content transformed\n *\n * @example\n * ```typescript\n * const textarea = document.querySelector('textarea');\n * const uppercaseFormatter = (text: string) => text.toUpperCase();\n * const result = applyFormattingOnSelection(textarea, uppercaseFormatter);\n * ```\n */\nexport const applyFormattingOnSelection = (\n element: HTMLInputElement | HTMLTextAreaElement,\n formatter: (text: string) => string,\n): string => {\n const selectionEnd = element.selectionEnd ?? 0;\n const selectionStart = element.selectionStart ?? 0;\n const value = element.value ?? '';\n\n if (selectionEnd > selectionStart) {\n // Format only selected text\n const before = value.substring(0, selectionStart);\n const selected = value.substring(selectionStart, selectionEnd);\n const after = value.substring(selectionEnd);\n\n return before + formatter(selected) + after;\n }\n\n // Format entire text if no selection\n return formatter(value);\n};\n\n/**\n * Updates the value of an input or textarea element and optionally triggers onChange event.\n * Creates a synthetic React change event if onChange callback is provided.\n * Useful for programmatically updating form elements while maintaining React state consistency.\n *\n * @param {HTMLInputElement | HTMLTextAreaElement} element - The HTML input or textarea element to update\n * @param {string} newValue - The new string value to set on the element\n * @param {(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void} [onChange] - Optional React onChange event handler to call after updating the value\n *\n * @example\n * ```typescript\n * const handleChange = (e) => setFormValue(e.target.value);\n * updateElementValue(textareaRef.current, 'New content', handleChange);\n * ```\n */\nexport const updateElementValue = (\n element: HTMLInputElement | HTMLTextAreaElement,\n newValue: string,\n onChange?: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void,\n) => {\n element.value = newValue;\n\n // Dispatch input event for React to detect the change\n element.dispatchEvent(new Event('input', { bubbles: true }));\n\n // Call onChange if provided (for controlled components)\n if (onChange) {\n const syntheticEvent = {\n currentTarget: element,\n target: element,\n } as React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>;\n onChange(syntheticEvent);\n }\n};\n","import type { FormatterFunction, TextInputElement, ToolbarPosition, ToolbarState } from '@/types';\n\nimport { applyFormattingOnSelection, updateElementValue } from './domUtils';\n\n/**\n * Default positioning function that places the toolbar below the focused element.\n * Positions the toolbar 5 pixels below the bottom edge of the element, aligned to the left.\n *\n * @param {TextInputElement} element - The focused input or textarea element\n * @returns {ToolbarPosition} Position coordinates with toolbar below the element\n */\nconst defaultGetPosition = (element: TextInputElement): ToolbarPosition => {\n const rect = element.getBoundingClientRect();\n return {\n x: rect.left,\n y: rect.bottom + 5,\n };\n};\n\n/**\n * Interface defining the contract for managing global toolbar state.\n * Provides methods for showing/hiding the toolbar and applying formatting.\n *\n * @interface ToolbarStateManager\n */\ntype ToolbarStateManager = {\n /**\n * Apply a formatting function to the currently active element.\n *\n * @param {FormatterFunction} formatter - Function to transform the text\n * @param {(e: React.ChangeEvent<TextInputElement>) => void} [onChange] - Optional change handler\n */\n applyFormat(formatter: FormatterFunction, onChange?: (e: React.ChangeEvent<TextInputElement>) => void): void;\n\n /** Cancel any scheduled toolbar hide operation */\n cancelScheduledHide(): void;\n\n /** Get the current toolbar state */\n getState(): ToolbarState;\n\n /** Immediately hide the toolbar */\n hideToolbar(): void;\n\n /**\n * Schedule the toolbar to hide after a delay.\n *\n * @param {number} delay - Delay in milliseconds\n */\n scheduleHide(delay: number): void;\n\n /**\n * Show the toolbar for a specific element.\n *\n * @param {TextInputElement} element - The element to show the toolbar for\n * @param {(element: TextInputElement) => ToolbarPosition} getPosition - Function to determine toolbar position\n */\n showToolbar(element: TextInputElement, getPosition: (element: TextInputElement) => ToolbarPosition): void;\n\n /**\n * Subscribe to toolbar state changes.\n *\n * @param {(state: ToolbarState) => void} callback - Function called when state changes\n * @returns {() => void} Unsubscribe function\n */\n subscribe(callback: (state: ToolbarState) => void): () => void;\n};\n\n/**\n * Global toolbar state manager - single source of truth for all formatting toolbars.\n * Implements the singleton pattern to ensure only one toolbar is visible at a time\n * across the entire application.\n *\n * @class GlobalToolbarManager\n * @implements {ToolbarStateManager}\n */\nclass GlobalToolbarManager implements ToolbarStateManager {\n private activeElementOnChange: ((e: React.ChangeEvent<TextInputElement>) => void) | undefined;\n\n private hideTimeout: NodeJS.Timeout | null = null;\n private state: ToolbarState = {\n activeElement: null,\n isVisible: false,\n position: null,\n };\n private subscribers: Set<(state: ToolbarState) => void> = new Set();\n\n applyFormat(formatter: FormatterFunction, onChange?: (e: React.ChangeEvent<TextInputElement>) => void): void {\n const { activeElement } = this.state;\n\n if (!activeElement) {\n console.warn('No active element found for formatting');\n return;\n }\n\n const newValue = applyFormattingOnSelection(activeElement, formatter);\n\n // Use provided onChange or the stored one from the element\n const onChangeHandler = onChange || this.activeElementOnChange;\n updateElementValue(activeElement, newValue, onChangeHandler);\n\n // Keep focus on the element after formatting\n activeElement.focus();\n }\n\n cancelScheduledHide(): void {\n this.clearHideTimeout();\n }\n\n getState(): ToolbarState {\n return { ...this.state };\n }\n\n hideToolbar(): void {\n this.clearHideTimeout();\n this.activeElementOnChange = undefined;\n this.setState({\n activeElement: null,\n isVisible: false,\n position: null,\n });\n }\n\n scheduleHide(delay: number): void {\n this.clearHideTimeout();\n this.hideTimeout = setTimeout(() => this.hideToolbar(), delay);\n }\n\n showToolbar(\n element: TextInputElement,\n getPosition: (element: TextInputElement) => ToolbarPosition = defaultGetPosition,\n ): void {\n this.clearHideTimeout();\n\n // Store the onChange handler from the element for later use\n const props = (element as any).props || {};\n this.activeElementOnChange = props.onChange;\n\n this.setState({\n activeElement: element,\n isVisible: true,\n position: getPosition(element),\n });\n }\n\n subscribe(callback: (state: ToolbarState) => void): () => void {\n this.subscribers.add(callback);\n return () => {\n this.subscribers.delete(callback);\n };\n }\n\n private clearHideTimeout(): void {\n if (this.hideTimeout) {\n clearTimeout(this.hideTimeout);\n this.hideTimeout = null;\n }\n }\n\n private setState(newState: Partial<ToolbarState>): void {\n this.state = { ...this.state, ...newState };\n this.subscribers.forEach((callback) => callback(this.getState()));\n }\n}\n\n/**\n * Global singleton instance of the toolbar manager.\n * This instance is shared across all components using the formatting toolbar.\n *\n * @example\n * ```typescript\n * import { globalToolbarManager } from './globalToolbarManager';\n *\n * // Show toolbar for an element\n * globalToolbarManager.showToolbar(textareaElement);\n *\n * // Apply formatting\n * globalToolbarManager.applyFormat((text) => text.toUpperCase());\n * ```\n */\nconst globalToolbarManager = new GlobalToolbarManager();\n\nexport { globalToolbarManager };\n","import { useCallback, useEffect, useState } from 'react';\n\nimport type { FormatterFunction, TextInputElement, ToolbarConfig, ToolbarState } from '@/types';\n\nimport { globalToolbarManager } from '@/utils/globalToolbarManager';\n\n/**\n * Default positioning function that places the toolbar below the focused element.\n *\n * @param {TextInputElement} element - The focused input or textarea element\n * @returns {ToolbarPosition} Position coordinates for the toolbar\n */\nconst defaultGetPosition = (element: TextInputElement) => {\n const rect = element.getBoundingClientRect();\n return {\n x: rect.left,\n y: rect.bottom + 5,\n };\n};\n\n/**\n * Return type for the useFormattingToolbar hook containing all toolbar functionality.\n *\n * @interface UseFormattingToolbarResult\n */\ntype UseFormattingToolbarResult = {\n /**\n * Apply formatting to the currently active element.\n *\n * @param {FormatterFunction} formatter - Function to transform the selected or entire text\n */\n applyFormat: (formatter: FormatterFunction) => void;\n\n /**\n * Props to spread on your input/textarea components.\n * Includes onFocus and onBlur handlers for toolbar management.\n *\n * @returns {object} Props object with focus and blur handlers\n */\n getInputProps: () => {\n onBlur: (e: React.FocusEvent<TextInputElement>) => void;\n onFocus: (e: React.FocusEvent<TextInputElement>) => void;\n };\n\n /**\n * Props for the toolbar container element.\n * Includes positioning styles and optional mouse event handlers.\n *\n * @returns {object} Props object with styles and event handlers\n */\n getToolbarProps: () => {\n onMouseDown?: (e: React.MouseEvent) => void;\n style: React.CSSProperties;\n };\n\n /** Function to manually hide the toolbar */\n hideToolbar: () => void;\n\n /** Whether the toolbar is currently visible */\n isVisible: boolean;\n\n /**\n * Function to manually show the toolbar for a specific element.\n *\n * @param {TextInputElement} element - The element to show the toolbar for\n */\n showToolbar: (element: TextInputElement) => void;\n\n /** Current toolbar state (shared globally across all instances) */\n toolbarState: ToolbarState;\n};\n\n/**\n * Hook for managing formatting toolbar functionality.\n * Uses global state so all instances share the same toolbar - only one toolbar\n * can be visible at a time across the entire application.\n *\n * @param {ToolbarConfig} [config={}] - Optional configuration for toolbar behavior\n * @returns {UseFormattingToolbarResult} Object containing toolbar state and control functions\n *\n * @example\n * ```typescript\n * function MyComponent() {\n * const { getInputProps, isVisible, applyFormat } = useFormattingToolbar({\n * hideDelay: 300,\n * getPosition: (element) => ({ x: 100, y: 200 })\n * });\n *\n * return (\n * <div>\n * <textarea {...getInputProps()} />\n * {isVisible && (\n * <div>\n * <button onClick={() => applyFormat(text => text.toUpperCase())}>\n * UPPERCASE\n * </button>\n * </div>\n * )}\n * </div>\n * );\n * }\n * ```\n */\nexport const useFormattingToolbar = (config: ToolbarConfig = {}): UseFormattingToolbarResult => {\n const { getPosition = defaultGetPosition, hideDelay = 500, preventCloseOnClick = true } = config;\n\n // Subscribe to global toolbar state\n const [toolbarState, setToolbarState] = useState<ToolbarState>(globalToolbarManager.getState());\n\n useEffect(() => {\n const unsubscribe = globalToolbarManager.subscribe(setToolbarState);\n return unsubscribe;\n }, []);\n\n const showToolbar = useCallback(\n (element: TextInputElement) => {\n globalToolbarManager.showToolbar(element, getPosition);\n },\n [getPosition],\n );\n\n const hideToolbar = useCallback(() => {\n globalToolbarManager.hideToolbar();\n }, []);\n\n const handleFocus = useCallback(\n (e: React.FocusEvent<TextInputElement>) => {\n showToolbar(e.currentTarget);\n },\n [showToolbar],\n );\n\n const handleBlur = useCallback(() => {\n globalToolbarManager.scheduleHide(hideDelay);\n }, [hideDelay]);\n\n const applyFormat = useCallback((formatter: FormatterFunction) => {\n globalToolbarManager.applyFormat(formatter);\n }, []);\n\n const getInputProps = useCallback(\n () => ({\n onBlur: handleBlur,\n onFocus: handleFocus,\n }),\n [handleFocus, handleBlur],\n );\n\n const getToolbarProps = useCallback(\n () => ({\n style: {\n left: toolbarState.position?.x ?? 0,\n position: 'fixed' as const,\n top: toolbarState.position?.y ?? 0,\n zIndex: 1000,\n },\n ...(preventCloseOnClick && {\n onMouseDown: (e: React.MouseEvent) => {\n e.preventDefault(); // Prevent blur event when clicking toolbar\n globalToolbarManager.cancelScheduledHide();\n },\n }),\n }),\n [toolbarState.position, preventCloseOnClick],\n );\n\n return {\n applyFormat,\n getInputProps,\n getToolbarProps,\n hideToolbar,\n isVisible: toolbarState.isVisible,\n showToolbar,\n toolbarState,\n };\n};\n","import React, { forwardRef } from 'react';\n\nimport type { TextInputElement, ToolbarConfig } from './types';\n\nimport { useFormattingToolbar } from './hooks/useFormattingToolbar';\n\n/**\n * Higher-order component that adds formatting toolbar functionality to input components.\n * Since the library uses global state, all wrapped components automatically share the same toolbar.\n * Only one toolbar will be visible at a time, appearing for whichever input is currently focused.\n *\n * @template P - The props type of the wrapped component\n * @param {React.ComponentType<P>} Component - The input component to enhance with toolbar functionality\n * @param {ToolbarConfig} [config={}] - Optional configuration for toolbar behavior\n * @returns {React.ForwardRefExoticComponent} Enhanced component with toolbar functionality\n *\n * @example\n * ```typescript\n * import { Textarea } from './ui/textarea';\n * import { withFormattingToolbar } from 'blumbaben';\n *\n * const TextareaWithToolbar = withFormattingToolbar(Textarea, {\n * hideDelay: 300,\n * getPosition: (element) => {\n * const rect = element.getBoundingClientRect();\n * return { x: rect.left, y: rect.top - 50 }; // Above the element\n * }\n * });\n *\n * // Usage\n * <TextareaWithToolbar\n * value={content}\n * onChange={setContent}\n * placeholder=\"Start typing...\"\n * />\n * ```\n */\nexport const withFormattingToolbar = <P extends Record<string, any>>(\n Component: React.ComponentType<P>,\n config: ToolbarConfig = {},\n) => {\n const WrappedComponent = forwardRef<TextInputElement, P>((props, ref) => {\n // All instances share the same global toolbar state\n const { getInputProps } = useFormattingToolbar(config);\n const toolbarProps = getInputProps();\n\n const handleFocusEvent = (e: React.FocusEvent<TextInputElement>) => {\n toolbarProps.onFocus(e);\n if (props.onFocus && typeof props.onFocus === 'function') {\n props.onFocus(e);\n }\n };\n\n const handleBlurEvent = (e: React.FocusEvent<TextInputElement>) => {\n toolbarProps.onBlur(e);\n if (props.onBlur && typeof props.onBlur === 'function') {\n props.onBlur(e);\n }\n };\n\n const enhancedProps = {\n ...props,\n onBlur: handleBlurEvent,\n onFocus: handleFocusEvent,\n ref,\n } as P & { ref: React.ForwardedRef<TextInputElement> };\n\n return React.createElement(Component, enhancedProps);\n });\n\n WrappedComponent.displayName = `withFormattingToolbar(${Component.displayName || Component.name})`;\n\n return WrappedComponent;\n};\n"],"mappings":"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","names":["React","useCallback","useEffect","useState","applyFormattingOnSelection","element","formatter","selectionEnd","selectionStart","value","before","selected","after","updateElementValue","newValue","onChange","defaultGetPosition","element","rect","GlobalToolbarManager","formatter","onChange","activeElement","newValue","applyFormattingOnSelection","onChangeHandler","updateElementValue","delay","getPosition","props","callback","newState","globalToolbarManager","useFormattingToolbarState","toolbarState","setToolbarState","useState","globalToolbarManager","useEffect","useCallback","formatter","FormattingToolbar","Component","children","className","config","style","applyFormat","toolbarState","useFormattingToolbarState","preventCloseOnClick","handleMouseDown","e","React","useCallback","useEffect","useState","defaultGetPosition","element","rect","useFormattingToolbar","config","getPosition","hideDelay","preventCloseOnClick","toolbarState","setToolbarState","useState","globalToolbarManager","useEffect","showToolbar","useCallback","hideToolbar","handleFocus","e","handleBlur","applyFormat","formatter","getInputProps","getToolbarProps","React","forwardRef","withFormattingToolbar","Component","config","WrappedComponent","forwardRef","props","ref","getInputProps","useFormattingToolbar","toolbarProps","enhancedProps","e","React"]}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "blumbaben",
3
+ "repository": {
4
+ "type": "git",
5
+ "url": "https://github.com/ragaeeb/blumbaben.git"
6
+ },
7
+ "module": "./dist/index.js",
8
+ "version": "1.0.0",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "devDependencies": {
20
+ "@eslint/js": "^9.30.1",
21
+ "@types/bun": "^1.2.17",
22
+ "@types/react": "^19.1.8",
23
+ "@types/react-dom": "^19.1.6",
24
+ "eslint": "^9.30.1",
25
+ "eslint-config-prettier": "^10.1.5",
26
+ "eslint-plugin-perfectionist": "^4.15.0",
27
+ "eslint-plugin-prettier": "^5.5.1",
28
+ "eslint-plugin-react": "^7.37.5",
29
+ "globals": "^16.3.0",
30
+ "prettier": "^3.6.2",
31
+ "semantic-release": "^24.2.6",
32
+ "tsup": "^8.5.0",
33
+ "typescript-eslint": "^8.35.1"
34
+ },
35
+ "peerDependencies": {
36
+ "react": "^19.1.0",
37
+ "react-dom": "^19.1.0"
38
+ },
39
+ "type": "module",
40
+ "bugs": {
41
+ "url": "https://github.com/ragaeeb/blumbaben/issues"
42
+ },
43
+ "description": "A lightweight TypeScript React hook to show a toolbar when an input is focused.",
44
+ "homepage": "https://github.com/ragaeeb/blumbaben",
45
+ "keywords": [
46
+ "textarea",
47
+ "multiline",
48
+ "input",
49
+ "forms",
50
+ "styling",
51
+ "formatting"
52
+ ],
53
+ "license": "MIT",
54
+ "scripts": {
55
+ "build": "tsup"
56
+ }
57
+ }