blumbaben 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
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
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)
5
+ 🔭 Explore the live Storybook showcase at [blumbaben.vercel.app](https://blumbaben.vercel.app).
6
+
7
+ [![wakatime](https://wakatime.com/badge/user/a0b906ce-b8e7-4463-8bce-383238df6d4b/project/f3f86e51-f640-45b9-b5c1-8612a6c8b84c.svg)](https://wakatime.com/badge/user/a0b906ce-b8e7-4463-8bce-383238df6d4b/project/f3f86e51-f640-45b9-b5c1-8612a6c8b84c)
8
+ [![codecov](https://codecov.io/gh/ragaeeb/blumbaben/graph/badge.svg?token=Y0VF63CGJM)](https://codecov.io/gh/ragaeeb/blumbaben)
6
9
  ![Bun](https://img.shields.io/badge/Bun-%23000000.svg?style=for-the-badge&logo=bun&logoColor=white)
7
10
  [![Node.js CI](https://github.com/ragaeeb/blumbaben/actions/workflows/build.yml/badge.svg)](https://github.com/ragaeeb/blumbaben/actions/workflows/build.yml)
8
11
  ![GitHub License](https://img.shields.io/github/license/ragaeeb/blumbaben)
@@ -1,4 +1,6 @@
1
- import React$1, { JSX } from 'react';
1
+ import React$1, { JSX } from "react";
2
+
3
+ //#region src/types.d.ts
2
4
 
3
5
  /**
4
6
  * Function type for text formatting operations.
@@ -20,38 +22,38 @@ type TextInputElement = HTMLInputElement | HTMLTextAreaElement;
20
22
  * Configuration options for the formatting toolbar behavior and appearance.
21
23
  */
22
24
  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;
25
+ /**
26
+ * Custom positioning function to determine where the toolbar appears relative to the focused element.
27
+ * If not provided, defaults to positioning below the element.
28
+ *
29
+ * @param {TextInputElement} element - The focused input or textarea element
30
+ * @returns {ToolbarPosition} Position coordinates for the toolbar
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * const config: ToolbarConfig = {
35
+ * getPosition: (element) => {
36
+ * const rect = element.getBoundingClientRect();
37
+ * return { x: rect.left, y: rect.top - 50 }; // Above the element
38
+ * }
39
+ * };
40
+ * ```
41
+ */
42
+ getPosition?: (element: TextInputElement) => ToolbarPosition;
43
+ /**
44
+ * Delay in milliseconds before hiding the toolbar after the input loses focus.
45
+ * This allows users to click on toolbar buttons without the toolbar disappearing.
46
+ *
47
+ * @default 500
48
+ */
49
+ hideDelay?: number;
50
+ /**
51
+ * Whether to prevent the toolbar from closing when clicked.
52
+ * When true, clicking toolbar buttons won't cause the input to lose focus.
53
+ *
54
+ * @default true
55
+ */
56
+ preventCloseOnClick?: boolean;
55
57
  };
56
58
  /**
57
59
  * Represents the x,y coordinates for positioning the toolbar on screen.
@@ -59,10 +61,10 @@ type ToolbarConfig = {
59
61
  * @interface ToolbarPosition
60
62
  */
61
63
  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;
64
+ /** Horizontal position in pixels from the left edge of the viewport */
65
+ x: number;
66
+ /** Vertical position in pixels from the top edge of the viewport */
67
+ y: number;
66
68
  };
67
69
  /**
68
70
  * Current state of the formatting toolbar including visibility, position, and active element.
@@ -70,49 +72,50 @@ type ToolbarPosition = {
70
72
  * @interface ToolbarState
71
73
  */
72
74
  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;
75
+ /** The currently focused input/textarea element, or null if no element is active */
76
+ activeElement: null | TextInputElement;
77
+ /** Whether the toolbar is currently visible to the user */
78
+ isVisible: boolean;
79
+ /** Current position of the toolbar, or null if not positioned */
80
+ position: null | ToolbarPosition;
79
81
  };
80
-
82
+ //#endregion
83
+ //#region src/formatting-toolbar.d.ts
81
84
  /**
82
85
  * Props for the FormattingToolbar component.
83
86
  *
84
87
  * @interface FormattingToolbarProps
85
88
  */
86
89
  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;
90
+ /**
91
+ * Container element type for the toolbar.
92
+ * Can be any valid HTML element tag name.
93
+ *
94
+ * @default 'div'
95
+ */
96
+ as?: keyof JSX.IntrinsicElements;
97
+ /**
98
+ * Render function that receives the applyFormat callback.
99
+ * Use this function to render your toolbar buttons and controls.
100
+ *
101
+ * @param {(formatter: FormatterFunction) => void} applyFormat - Function to apply text formatting
102
+ * @returns {React.ReactNode} The toolbar content to render
103
+ */
104
+ children: (applyFormat: (formatter: FormatterFunction) => void) => React$1.ReactNode;
105
+ /**
106
+ * Custom CSS class name for styling the toolbar container.
107
+ */
108
+ className?: string;
109
+ /**
110
+ * Toolbar configuration options.
111
+ * Merged with default configuration.
112
+ */
113
+ config?: ToolbarConfig;
114
+ /**
115
+ * Custom inline styles for the toolbar container.
116
+ * These styles are merged with the positioning styles (position, top, left, zIndex).
117
+ */
118
+ style?: React$1.CSSProperties;
116
119
  };
117
120
  /**
118
121
  * Formatting toolbar component that renders when an input is focused.
@@ -156,51 +159,52 @@ type FormattingToolbarProps = {
156
159
  * ```
157
160
  */
158
161
  declare const FormattingToolbar: React$1.FC<FormattingToolbarProps>;
159
-
162
+ //#endregion
163
+ //#region src/hooks/useFormattingToolbar.d.ts
160
164
  /**
161
165
  * Return type for the useFormattingToolbar hook containing all toolbar functionality.
162
166
  *
163
167
  * @interface UseFormattingToolbarResult
164
168
  */
165
169
  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;
170
+ /**
171
+ * Apply formatting to the currently active element.
172
+ *
173
+ * @param {FormatterFunction} formatter - Function to transform the selected or entire text
174
+ */
175
+ applyFormat: (formatter: FormatterFunction) => void;
176
+ /**
177
+ * Props to spread on your input/textarea components.
178
+ * Includes onFocus and onBlur handlers for toolbar management.
179
+ *
180
+ * @returns {object} Props object with focus and blur handlers
181
+ */
182
+ getInputProps: () => {
183
+ onBlur: (e: React.FocusEvent<TextInputElement>) => void;
184
+ onFocus: (e: React.FocusEvent<TextInputElement>) => void;
185
+ };
186
+ /**
187
+ * Props for the toolbar container element.
188
+ * Includes positioning styles and optional mouse event handlers.
189
+ *
190
+ * @returns {object} Props object with styles and event handlers
191
+ */
192
+ getToolbarProps: () => {
193
+ onMouseDown?: (e: React.MouseEvent) => void;
194
+ style: React.CSSProperties;
195
+ };
196
+ /** Function to manually hide the toolbar */
197
+ hideToolbar: () => void;
198
+ /** Whether the toolbar is currently visible */
199
+ isVisible: boolean;
200
+ /**
201
+ * Function to manually show the toolbar for a specific element.
202
+ *
203
+ * @param {TextInputElement} element - The element to show the toolbar for
204
+ */
205
+ showToolbar: (element: TextInputElement) => void;
206
+ /** Current toolbar state (shared globally across all instances) */
207
+ toolbarState: ToolbarState;
204
208
  };
205
209
  /**
206
210
  * Hook for managing formatting toolbar functionality.
@@ -234,7 +238,8 @@ type UseFormattingToolbarResult = {
234
238
  * ```
235
239
  */
236
240
  declare const useFormattingToolbar: (config?: ToolbarConfig) => UseFormattingToolbarResult;
237
-
241
+ //#endregion
242
+ //#region src/utils/domUtils.d.ts
238
243
  /**
239
244
  * Applies a formatting function to either selected text or entire content of an element.
240
245
  * If text is selected (selectionStart !== selectionEnd), formats only the selected portion.
@@ -269,7 +274,8 @@ declare const applyFormattingOnSelection: (element: HTMLInputElement | HTMLTextA
269
274
  * ```
270
275
  */
271
276
  declare const updateElementValue: (element: HTMLInputElement | HTMLTextAreaElement, newValue: string, onChange?: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void) => void;
272
-
277
+ //#endregion
278
+ //#region src/withFormattingToolbar.d.ts
273
279
  /**
274
280
  * Higher-order component that adds formatting toolbar functionality to input components.
275
281
  * Since the library uses global state, all wrapped components automatically share the same toolbar.
@@ -301,6 +307,11 @@ declare const updateElementValue: (element: HTMLInputElement | HTMLTextAreaEleme
301
307
  * />
302
308
  * ```
303
309
  */
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 };
310
+ type FocusHandlers = {
311
+ onBlur?: (event: React$1.FocusEvent<TextInputElement>) => void;
312
+ onFocus?: (event: React$1.FocusEvent<TextInputElement>) => void;
313
+ };
314
+ declare const withFormattingToolbar: <P extends Record<string, unknown> & FocusHandlers>(Component: React$1.ComponentType<P>, config?: ToolbarConfig) => React$1.ForwardRefExoticComponent<React$1.PropsWithoutRef<P> & React$1.RefAttributes<TextInputElement>>;
315
+ //#endregion
316
+ export { FormatterFunction, FormattingToolbar, TextInputElement, ToolbarConfig, ToolbarPosition, ToolbarState, applyFormattingOnSelection, updateElementValue, useFormattingToolbar, withFormattingToolbar };
317
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import e,{forwardRef as t,useCallback as n,useEffect as r,useState as i}from"react";const a=(e,t)=>{let n=e.selectionEnd??0,r=e.selectionStart??0,i=e.value??``;if(n>r){let e=i.substring(0,r),a=i.substring(r,n),o=i.substring(n);return e+t(a)+o}return t(i)},o=(e,t,n)=>{e.value=t,e.dispatchEvent(new Event(`input`,{bubbles:!0})),n&&n({currentTarget:e,target:e})},s=e=>{let t=e.getBoundingClientRect();return{x:t.left,y:t.bottom+5}},c=new class{activeElementOnChange;hideTimeout=null;state={activeElement:null,isVisible:!1,position:null};subscribers=new Set;applyFormat(e,t){let{activeElement:n}=this.state;if(!n){console.warn(`No active element found for formatting`);return}o(n,a(n,e),t||this.activeElementOnChange),n.focus()}cancelScheduledHide(){this.clearHideTimeout()}getState(){return{...this.state}}hideToolbar(){this.clearHideTimeout(),this.activeElementOnChange=void 0,this.setState({activeElement:null,isVisible:!1,position:null})}scheduleHide(e){this.clearHideTimeout(),this.hideTimeout=setTimeout(()=>this.hideToolbar(),e)}showToolbar(e,t=s){this.clearHideTimeout(),this.activeElementOnChange=e.props?.onChange,this.setState({activeElement:e,isVisible:!0,position:t(e)})}subscribe(e){return this.subscribers.add(e),()=>{this.subscribers.delete(e)}}clearHideTimeout(){this.hideTimeout&&=(clearTimeout(this.hideTimeout),null)}setState(e){this.state={...this.state,...e},this.subscribers.forEach(e=>{e(this.getState())})}},l=()=>{let[e,t]=i(c.getState());return r(()=>c.subscribe(t),[]),{applyFormat:n(e=>{c.applyFormat(e)},[]),isVisible:e.isVisible,toolbarState:e}},u=({as:t=`div`,children:n,className:r=``,config:i={},style:a={}})=>{let{applyFormat:o,toolbarState:s}=l(),{preventCloseOnClick:c=!0}=i;if(!s.isVisible||!s.position)return null;let u=c?e=>{e.preventDefault()}:void 0;return e.createElement(t,{className:r,onMouseDown:u,style:{left:s.position.x,position:`fixed`,top:s.position.y,zIndex:1e3,...a}},n(o))},d=e=>{let t=e.getBoundingClientRect();return{x:t.left,y:t.bottom+5}},f=(e={})=>{let{getPosition:t=d,hideDelay:a=500,preventCloseOnClick:o=!0}=e,[s,l]=i(c.getState());r(()=>c.subscribe(l),[]);let u=n(e=>{c.showToolbar(e,t)},[t]),f=n(()=>{c.hideToolbar()},[]),p=n(e=>{u(e.currentTarget)},[u]),m=n(()=>{c.scheduleHide(a)},[a]);return{applyFormat:n(e=>{c.applyFormat(e)},[]),getInputProps:n(()=>({onBlur:m,onFocus:p}),[p,m]),getToolbarProps:n(()=>({style:{left:s.position?.x??0,position:`fixed`,top:s.position?.y??0,zIndex:1e3},...o&&{onMouseDown:e=>{e.preventDefault(),c.cancelScheduledHide()}}}),[s.position,o]),hideToolbar:f,isVisible:s.isVisible,showToolbar:u,toolbarState:s}},p=(n,r={})=>{let i=t((t,i)=>{let{getInputProps:a}=f(r),o=a(),s=e=>{o.onFocus(e),typeof t.onFocus==`function`&&t.onFocus(e)},c=e=>{o.onBlur(e),typeof t.onBlur==`function`&&t.onBlur(e)},l={...t,onBlur:c,onFocus:s,ref:i};return e.createElement(n,l)});return i.displayName=`withFormattingToolbar(${n.displayName||n.name})`,i};export{u as FormattingToolbar,a as applyFormattingOnSelection,o as updateElementValue,f as useFormattingToolbar,p as withFormattingToolbar};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["defaultGetPosition","FormattingToolbar: React.FC<FormattingToolbarProps>"],"sources":["../src/utils/domUtils.ts","../src/utils/globalToolbarManager.ts","../src/hooks/useFormattingToolbarState.ts","../src/formatting-toolbar.tsx","../src/hooks/useFormattingToolbar.ts","../src/withFormattingToolbar.ts"],"sourcesContent":["/**\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 React from 'react';\n\nimport 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(\n formatter: FormatterFunction,\n onChange?: (e: React.ChangeEvent<TextInputElement>) => void,\n ): 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(\n element: TextInputElement,\n getPosition: (element: TextInputElement) => ToolbarPosition,\n ): 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(\n formatter: FormatterFunction,\n onChange?: (e: React.ChangeEvent<TextInputElement>) => void,\n ): 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 elementWithProps = element as TextInputElement & {\n props?: {\n onChange?: (e: React.ChangeEvent<TextInputElement>) => void;\n };\n };\n this.activeElementOnChange = elementWithProps.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) => {\n callback(this.getState());\n });\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, 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","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, 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 */\ntype FocusHandlers = {\n onBlur?: (event: React.FocusEvent<TextInputElement>) => void;\n onFocus?: (event: React.FocusEvent<TextInputElement>) => void;\n};\n\nexport const withFormattingToolbar = <P extends Record<string, unknown> & FocusHandlers>(\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 (typeof props.onFocus === 'function') {\n props.onFocus(e);\n }\n };\n\n const handleBlurEvent = (e: React.FocusEvent<TextInputElement>) => {\n toolbarProps.onBlur(e);\n if (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 };\n\n return React.createElement(Component, enhancedProps as unknown as P);\n });\n\n WrappedComponent.displayName = `withFormattingToolbar(${Component.displayName || Component.name})`;\n\n return WrappedComponent;\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,46 +1,39 @@
1
1
  {
2
- "name": "blumbaben",
3
- "repository": {
4
- "type": "git",
5
- "url": "https://github.com/ragaeeb/blumbaben.git"
2
+ "bugs": {
3
+ "url": "https://github.com/ragaeeb/blumbaben/issues"
4
+ },
5
+ "description": "A lightweight TypeScript React hook to show a toolbar when an input is focused.",
6
+ "devDependencies": {
7
+ "@biomejs/biome": "^2.3.10",
8
+ "@chromatic-com/storybook": "^4.1.3",
9
+ "@storybook/addon-a11y": "^10.1.10",
10
+ "@storybook/addon-docs": "^10.1.10",
11
+ "@storybook/addon-vitest": "^10.1.10",
12
+ "@storybook/react-vite": "^10.1.10",
13
+ "@types/bun": "^1.3.5",
14
+ "@types/react": "^19.2.7",
15
+ "@types/react-dom": "^19.2.3",
16
+ "@vitest/browser": "^4.0.16",
17
+ "@vitest/coverage-v8": "^4.0.16",
18
+ "playwright": "^1.57.0",
19
+ "semantic-release": "^25.0.2",
20
+ "storybook": "^10.1.10",
21
+ "tsdown": "^0.18.3",
22
+ "vitest": "^4.0.16"
23
+ },
24
+ "engines": {
25
+ "bun": ">=1.3.5",
26
+ "node": ">= 24.0.0"
6
27
  },
7
- "module": "./dist/index.js",
8
- "version": "1.0.0",
9
- "types": "./dist/index.d.ts",
10
28
  "exports": {
11
29
  ".": {
12
- "import": "./dist/index.js",
13
- "types": "./dist/index.d.ts"
30
+ "import": "./dist/index.mjs",
31
+ "types": "./dist/index.d.mts"
14
32
  }
15
33
  },
16
34
  "files": [
17
35
  "dist"
18
36
  ],
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
37
  "homepage": "https://github.com/ragaeeb/blumbaben",
45
38
  "keywords": [
46
39
  "textarea",
@@ -51,7 +44,25 @@
51
44
  "formatting"
52
45
  ],
53
46
  "license": "MIT",
47
+ "module": "./dist/index.mjs",
48
+ "name": "blumbaben",
49
+ "packageManager": "bun@1.3.5",
50
+ "peerDependencies": {
51
+ "react": "^19.2.3",
52
+ "react-dom": "^19.2.3"
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "https://github.com/ragaeeb/blumbaben.git"
57
+ },
54
58
  "scripts": {
55
- "build": "tsup"
56
- }
59
+ "build": "tsdown",
60
+ "build-storybook": "storybook build",
61
+ "format": "biome format --write src .storybook",
62
+ "lint": "biome lint src .storybook",
63
+ "storybook": "storybook dev -p 6006"
64
+ },
65
+ "type": "module",
66
+ "types": "./dist/index.d.mts",
67
+ "version": "1.0.2"
57
68
  }
package/dist/index.js DELETED
@@ -1,2 +0,0 @@
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
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
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":"AAAA,OAAOA,MAAyB,QCAhC,OAAS,eAAAC,EAAa,aAAAC,EAAW,YAAAC,MAAgB,QCiB1C,IAAMC,EAA6B,CACtCC,EACAC,IACS,CACT,IAAMC,EAAeF,EAAQ,cAAgB,EACvCG,EAAiBH,EAAQ,gBAAkB,EAC3CI,EAAQJ,EAAQ,OAAS,GAE/B,GAAIE,EAAeC,EAAgB,CAE/B,IAAME,EAASD,EAAM,UAAU,EAAGD,CAAc,EAC1CG,EAAWF,EAAM,UAAUD,EAAgBD,CAAY,EACvDK,EAAQH,EAAM,UAAUF,CAAY,EAE1C,OAAOG,EAASJ,EAAUK,CAAQ,EAAIC,CAC1C,CAGA,OAAON,EAAUG,CAAK,CAC1B,EAiBaI,EAAqB,CAC9BR,EACAS,EACAC,IACC,CACDV,EAAQ,MAAQS,EAGhBT,EAAQ,cAAc,IAAI,MAAM,QAAS,CAAE,QAAS,EAAK,CAAC,CAAC,EAGvDU,GAKAA,EAJuB,CACnB,cAAeV,EACf,OAAQA,CACZ,CACuB,CAE/B,EC5DA,IAAMW,EAAsBC,GAA+C,CACvE,IAAMC,EAAOD,EAAQ,sBAAsB,EAC3C,MAAO,CACH,EAAGC,EAAK,KACR,EAAGA,EAAK,OAAS,CACrB,CACJ,EA0DMC,EAAN,KAA0D,CAC9C,sBAEA,YAAqC,KACrC,MAAsB,CAC1B,cAAe,KACf,UAAW,GACX,SAAU,IACd,EACQ,YAAkD,IAAI,IAE9D,YAAYC,EAA8BC,EAAmE,CACzG,GAAM,CAAE,cAAAC,CAAc,EAAI,KAAK,MAE/B,GAAI,CAACA,EAAe,CAChB,QAAQ,KAAK,wCAAwC,EACrD,MACJ,CAEA,IAAMC,EAAWC,EAA2BF,EAAeF,CAAS,EAG9DK,EAAkBJ,GAAY,KAAK,sBACzCK,EAAmBJ,EAAeC,EAAUE,CAAe,EAG3DH,EAAc,MAAM,CACxB,CAEA,qBAA4B,CACxB,KAAK,iBAAiB,CAC1B,CAEA,UAAyB,CACrB,MAAO,CAAE,GAAG,KAAK,KAAM,CAC3B,CAEA,aAAoB,CAChB,KAAK,iBAAiB,EACtB,KAAK,sBAAwB,OAC7B,KAAK,SAAS,CACV,cAAe,KACf,UAAW,GACX,SAAU,IACd,CAAC,CACL,CAEA,aAAaK,EAAqB,CAC9B,KAAK,iBAAiB,EACtB,KAAK,YAAc,WAAW,IAAM,KAAK,YAAY,EAAGA,CAAK,CACjE,CAEA,YACIV,EACAW,EAA8DZ,EAC1D,CACJ,KAAK,iBAAiB,EAGtB,IAAMa,EAASZ,EAAgB,OAAS,CAAC,EACzC,KAAK,sBAAwBY,EAAM,SAEnC,KAAK,SAAS,CACV,cAAeZ,EACf,UAAW,GACX,SAAUW,EAAYX,CAAO,CACjC,CAAC,CACL,CAEA,UAAUa,EAAqD,CAC3D,YAAK,YAAY,IAAIA,CAAQ,EACtB,IAAM,CACT,KAAK,YAAY,OAAOA,CAAQ,CACpC,CACJ,CAEQ,kBAAyB,CACzB,KAAK,cACL,aAAa,KAAK,WAAW,EAC7B,KAAK,YAAc,KAE3B,CAEQ,SAASC,EAAuC,CACpD,KAAK,MAAQ,CAAE,GAAG,KAAK,MAAO,GAAGA,CAAS,EAC1C,KAAK,YAAY,QAASD,GAAaA,EAAS,KAAK,SAAS,CAAC,CAAC,CACpE,CACJ,EAiBME,EAAuB,IAAIb,EFrJ1B,IAAMc,EAA4B,IAAM,CAC3C,GAAM,CAACC,EAAcC,CAAe,EAAIC,EAAuBC,EAAqB,SAAS,CAAC,EAE9F,OAAAC,EAAU,IACcD,EAAqB,UAAUF,CAAe,EAEnE,CAAC,CAAC,EAME,CACH,YALgBI,EAAaC,GAAiC,CAC9DH,EAAqB,YAAYG,CAAS,CAC9C,EAAG,CAAC,CAAC,EAID,UAAWN,EAAa,UACxB,aAAAA,CACJ,CACJ,EDyCO,IAAMO,EAAsD,CAAC,CAChE,GAAIC,EAAY,MAChB,SAAAC,EACA,UAAAC,EAAY,GACZ,OAAAC,EAAS,CAAC,EACV,MAAAC,EAAQ,CAAC,CACb,IAAM,CACF,GAAM,CAAE,YAAAC,EAAa,aAAAC,CAAa,EAAIC,EAA0B,EAC1D,CAAE,oBAAAC,EAAsB,EAAK,EAAIL,EAEvC,GAAI,CAACG,EAAa,WAAa,CAACA,EAAa,SACzC,OAAO,KAGX,IAAMG,EAAkBD,EACjBE,GAAwB,CACrBA,EAAE,eAAe,CACrB,EACA,OAEN,OAAOC,EAAM,cACTX,EACA,CACI,UAAAE,EACA,YAAaO,EACb,MAAO,CACH,KAAMH,EAAa,SAAS,EAC5B,SAAU,QACV,IAAKA,EAAa,SAAS,EAC3B,OAAQ,IACR,GAAGF,CACP,CACJ,EACAH,EAASI,CAAW,CACxB,CACJ,EI3HA,OAAS,eAAAO,EAAa,aAAAC,EAAW,YAAAC,MAAgB,QAYjD,IAAMC,EAAsBC,GAA8B,CACtD,IAAMC,EAAOD,EAAQ,sBAAsB,EAC3C,MAAO,CACH,EAAGC,EAAK,KACR,EAAGA,EAAK,OAAS,CACrB,CACJ,EAqFaC,EAAuB,CAACC,EAAwB,CAAC,IAAkC,CAC5F,GAAM,CAAE,YAAAC,EAAcL,EAAoB,UAAAM,EAAY,IAAK,oBAAAC,EAAsB,EAAK,EAAIH,EAGpF,CAACI,EAAcC,CAAe,EAAIC,EAAuBC,EAAqB,SAAS,CAAC,EAE9FC,EAAU,IACcD,EAAqB,UAAUF,CAAe,EAEnE,CAAC,CAAC,EAEL,IAAMI,EAAcC,EACfb,GAA8B,CAC3BU,EAAqB,YAAYV,EAASI,CAAW,CACzD,EACA,CAACA,CAAW,CAChB,EAEMU,EAAcD,EAAY,IAAM,CAClCH,EAAqB,YAAY,CACrC,EAAG,CAAC,CAAC,EAECK,EAAcF,EACfG,GAA0C,CACvCJ,EAAYI,EAAE,aAAa,CAC/B,EACA,CAACJ,CAAW,CAChB,EAEMK,EAAaJ,EAAY,IAAM,CACjCH,EAAqB,aAAaL,CAAS,CAC/C,EAAG,CAACA,CAAS,CAAC,EAERa,EAAcL,EAAaM,GAAiC,CAC9DT,EAAqB,YAAYS,CAAS,CAC9C,EAAG,CAAC,CAAC,EAECC,EAAgBP,EAClB,KAAO,CACH,OAAQI,EACR,QAASF,CACb,GACA,CAACA,EAAaE,CAAU,CAC5B,EAEMI,EAAkBR,EACpB,KAAO,CACH,MAAO,CACH,KAAMN,EAAa,UAAU,GAAK,EAClC,SAAU,QACV,IAAKA,EAAa,UAAU,GAAK,EACjC,OAAQ,GACZ,EACA,GAAID,GAAuB,CACvB,YAAcU,GAAwB,CAClCA,EAAE,eAAe,EACjBN,EAAqB,oBAAoB,CAC7C,CACJ,CACJ,GACA,CAACH,EAAa,SAAUD,CAAmB,CAC/C,EAEA,MAAO,CACH,YAAAY,EACA,cAAAE,EACA,gBAAAC,EACA,YAAAP,EACA,UAAWP,EAAa,UACxB,YAAAK,EACA,aAAAL,CACJ,CACJ,EC/KA,OAAOe,GAAS,cAAAC,MAAkB,QAqC3B,IAAMC,EAAwB,CACjCC,EACAC,EAAwB,CAAC,IACxB,CACD,IAAMC,EAAmBC,EAAgC,CAACC,EAAOC,IAAQ,CAErE,GAAM,CAAE,cAAAC,CAAc,EAAIC,EAAqBN,CAAM,EAC/CO,EAAeF,EAAc,EAgB7BG,EAAgB,CAClB,GAAGL,EACH,OATqBM,GAA0C,CAC/DF,EAAa,OAAOE,CAAC,EACjBN,EAAM,QAAU,OAAOA,EAAM,QAAW,YACxCA,EAAM,OAAOM,CAAC,CAEtB,EAKI,QAjBsBA,GAA0C,CAChEF,EAAa,QAAQE,CAAC,EAClBN,EAAM,SAAW,OAAOA,EAAM,SAAY,YAC1CA,EAAM,QAAQM,CAAC,CAEvB,EAaI,IAAAL,CACJ,EAEA,OAAOM,EAAM,cAAcX,EAAWS,CAAa,CACvD,CAAC,EAED,OAAAP,EAAiB,YAAc,yBAAyBF,EAAU,aAAeA,EAAU,IAAI,IAExFE,CACX","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"]}