@papernote/ui 1.1.0 → 1.3.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.
Files changed (114) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +455 -455
  3. package/dist/components/Box.d.ts +2 -1
  4. package/dist/components/Box.d.ts.map +1 -1
  5. package/dist/components/Button.d.ts +10 -1
  6. package/dist/components/Button.d.ts.map +1 -1
  7. package/dist/components/Card.d.ts +11 -2
  8. package/dist/components/Card.d.ts.map +1 -1
  9. package/dist/components/CurrencyInput.d.ts +52 -0
  10. package/dist/components/CurrencyInput.d.ts.map +1 -0
  11. package/dist/components/DataTable.d.ts +19 -3
  12. package/dist/components/DataTable.d.ts.map +1 -1
  13. package/dist/components/EmptyState.d.ts +3 -1
  14. package/dist/components/EmptyState.d.ts.map +1 -1
  15. package/dist/components/Grid.d.ts +4 -2
  16. package/dist/components/Grid.d.ts.map +1 -1
  17. package/dist/components/Input.d.ts +2 -0
  18. package/dist/components/Input.d.ts.map +1 -1
  19. package/dist/components/Modal.d.ts.map +1 -1
  20. package/dist/components/MultiSelect.d.ts +13 -1
  21. package/dist/components/MultiSelect.d.ts.map +1 -1
  22. package/dist/components/Page.d.ts +2 -0
  23. package/dist/components/Page.d.ts.map +1 -1
  24. package/dist/components/PageLayout.d.ts +5 -1
  25. package/dist/components/PageLayout.d.ts.map +1 -1
  26. package/dist/components/Stack.d.ts +25 -5
  27. package/dist/components/Stack.d.ts.map +1 -1
  28. package/dist/components/Text.d.ts +20 -4
  29. package/dist/components/Text.d.ts.map +1 -1
  30. package/dist/components/Textarea.d.ts +2 -0
  31. package/dist/components/Textarea.d.ts.map +1 -1
  32. package/dist/components/index.d.ts +5 -3
  33. package/dist/components/index.d.ts.map +1 -1
  34. package/dist/index.d.ts +311 -49
  35. package/dist/index.esm.js +557 -224
  36. package/dist/index.esm.js.map +1 -1
  37. package/dist/index.js +555 -219
  38. package/dist/index.js.map +1 -1
  39. package/dist/styles.css +2838 -2679
  40. package/dist/utils/excelExport.d.ts +143 -0
  41. package/dist/utils/excelExport.d.ts.map +1 -0
  42. package/dist/utils/index.d.ts +2 -0
  43. package/dist/utils/index.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/components/AdminModal.css +49 -49
  46. package/src/components/Box.stories.tsx +377 -0
  47. package/src/components/Box.tsx +8 -4
  48. package/src/components/Button.tsx +23 -10
  49. package/src/components/Card.tsx +20 -5
  50. package/src/components/CurrencyInput.stories.tsx +290 -0
  51. package/src/components/CurrencyInput.tsx +193 -0
  52. package/src/components/DataTable.stories.tsx +36 -25
  53. package/src/components/DataTable.tsx +170 -16
  54. package/src/components/EmptyState.stories.tsx +124 -72
  55. package/src/components/EmptyState.tsx +10 -0
  56. package/src/components/Grid.stories.tsx +348 -0
  57. package/src/components/Grid.tsx +12 -5
  58. package/src/components/Input.tsx +12 -2
  59. package/src/components/Modal.stories.tsx +64 -0
  60. package/src/components/Modal.tsx +15 -2
  61. package/src/components/MultiSelect.tsx +41 -10
  62. package/src/components/Page.stories.tsx +76 -0
  63. package/src/components/Page.tsx +35 -3
  64. package/src/components/PageLayout.stories.tsx +75 -0
  65. package/src/components/PageLayout.tsx +28 -9
  66. package/src/components/RoleManager.css +10 -10
  67. package/src/components/Spreadsheet.css +216 -216
  68. package/src/components/Spreadsheet.stories.tsx +362 -362
  69. package/src/components/Spreadsheet.tsx +351 -351
  70. package/src/components/SpreadsheetSimple.stories.tsx +27 -27
  71. package/src/components/Stack.stories.tsx +24 -1
  72. package/src/components/Stack.tsx +40 -10
  73. package/src/components/Tabs.tsx +152 -152
  74. package/src/components/Text.stories.tsx +273 -0
  75. package/src/components/Text.tsx +33 -8
  76. package/src/components/Textarea.tsx +32 -21
  77. package/src/components/index.ts +6 -4
  78. package/src/styles/index.css +41 -4
  79. package/src/utils/excelExport.stories.tsx +535 -0
  80. package/src/utils/excelExport.ts +225 -0
  81. package/src/utils/index.ts +3 -0
  82. package/tailwind.config.js +253 -253
  83. package/dist/components/Button.stories.d.ts +0 -51
  84. package/dist/components/Button.stories.d.ts.map +0 -1
  85. package/dist/components/ChartVisualizationUI.d.ts +0 -21
  86. package/dist/components/ChartVisualizationUI.d.ts.map +0 -1
  87. package/dist/components/ChatUI.d.ts +0 -23
  88. package/dist/components/ChatUI.d.ts.map +0 -1
  89. package/dist/components/CommissionDashboardUI.d.ts +0 -25
  90. package/dist/components/CommissionDashboardUI.d.ts.map +0 -1
  91. package/dist/components/DataTable.stories.d.ts +0 -23
  92. package/dist/components/DataTable.stories.d.ts.map +0 -1
  93. package/dist/components/FormField.d.ts +0 -35
  94. package/dist/components/FormField.d.ts.map +0 -1
  95. package/dist/components/Input.stories.d.ts +0 -366
  96. package/dist/components/Input.stories.d.ts.map +0 -1
  97. package/dist/components/InsightsPanelUI.d.ts +0 -21
  98. package/dist/components/InsightsPanelUI.d.ts.map +0 -1
  99. package/dist/components/PaymentHistoryTimeline.d.ts +0 -34
  100. package/dist/components/PaymentHistoryTimeline.d.ts.map +0 -1
  101. package/dist/components/RelationshipManagerUI.d.ts +0 -60
  102. package/dist/components/RelationshipManagerUI.d.ts.map +0 -1
  103. package/dist/components/RoleManager.d.ts +0 -19
  104. package/dist/components/RoleManager.d.ts.map +0 -1
  105. package/dist/components/SplitCommissionBadge.d.ts +0 -18
  106. package/dist/components/SplitCommissionBadge.d.ts.map +0 -1
  107. package/dist/components/Spreadsheet.css +0 -216
  108. package/dist/components/Table.d.ts +0 -26
  109. package/dist/components/Table.d.ts.map +0 -1
  110. package/dist/components/__tests__/Button.test.d.ts +0 -2
  111. package/dist/components/__tests__/Button.test.d.ts.map +0 -1
  112. package/dist/components/__tests__/Input.test.d.ts +0 -2
  113. package/dist/components/__tests__/Input.test.d.ts.map +0 -1
  114. package/src/components/Table.tsx +0 -239
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { forwardRef } from 'react';
2
2
  import { Loader2 } from 'lucide-react';
3
3
 
4
4
  /**
@@ -31,6 +31,8 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
31
31
  * A versatile button component that supports multiple visual styles, sizes, icons,
32
32
  * loading states, and notification badges.
33
33
  *
34
+ * Supports ref forwarding for DOM access.
35
+ *
34
36
  * @example Basic usage
35
37
  * ```tsx
36
38
  * <Button variant="primary">Click me</Button>
@@ -38,9 +40,9 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
38
40
  *
39
41
  * @example With icon and loading
40
42
  * ```tsx
41
- * <Button
42
- * variant="secondary"
43
- * icon={<Save />}
43
+ * <Button
44
+ * variant="secondary"
45
+ * icon={<Save />}
44
46
  * loading={isSaving}
45
47
  * >
46
48
  * Save Changes
@@ -49,16 +51,22 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
49
51
  *
50
52
  * @example Icon-only with badge
51
53
  * ```tsx
52
- * <Button
53
- * iconOnly
54
- * badge={5}
54
+ * <Button
55
+ * iconOnly
56
+ * badge={5}
55
57
  * badgeVariant="error"
56
58
  * >
57
59
  * <Bell />
58
60
  * </Button>
59
61
  * ```
62
+ *
63
+ * @example With ref
64
+ * ```tsx
65
+ * const buttonRef = useRef<HTMLButtonElement>(null);
66
+ * <Button ref={buttonRef}>Focusable</Button>
67
+ * ```
60
68
  */
61
- export default function Button({
69
+ const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
62
70
  variant = 'primary',
63
71
  size = 'md',
64
72
  loading = false,
@@ -72,7 +80,7 @@ export default function Button({
72
80
  disabled,
73
81
  className = '',
74
82
  ...props
75
- }: ButtonProps) {
83
+ }, ref) => {
76
84
  const baseStyles = 'inline-flex items-center justify-center font-medium rounded-lg border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400 disabled:opacity-40 disabled:cursor-not-allowed';
77
85
 
78
86
  const variantStyles = {
@@ -111,6 +119,7 @@ export default function Button({
111
119
 
112
120
  const buttonElement = (
113
121
  <button
122
+ ref={ref}
114
123
  className={`
115
124
  ${baseStyles}
116
125
  ${variantStyles[variant]}
@@ -163,4 +172,8 @@ export default function Button({
163
172
  </span>
164
173
  </div>
165
174
  );
166
- }
175
+ });
176
+
177
+ Button.displayName = 'Button';
178
+
179
+ export default Button;
@@ -1,10 +1,10 @@
1
- import React from 'react';
1
+ import React, { forwardRef } from 'react';
2
2
  import { Skeleton } from './Loading';
3
3
 
4
4
  /**
5
5
  * Card component props
6
6
  */
7
- export interface CardProps {
7
+ export interface CardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onClick'> {
8
8
  /** Card content */
9
9
  children: React.ReactNode;
10
10
  /** Visual style variant affecting padding and shadow */
@@ -24,6 +24,8 @@ export interface CardProps {
24
24
  /**
25
25
  * Card - Container component with paper aesthetic and subtle shadow
26
26
  *
27
+ * Supports ref forwarding for DOM access.
28
+ *
27
29
  * A content container with paper texture, border, and shadow effects. Supports
28
30
  * different sizes, variants (padding/shadow levels), and loading states.
29
31
  *
@@ -53,8 +55,14 @@ export interface CardProps {
53
55
  * <p>Content</p>
54
56
  * </Card>
55
57
  * ```
58
+ *
59
+ * @example With ref
60
+ * ```tsx
61
+ * const cardRef = useRef<HTMLDivElement>(null);
62
+ * <Card ref={cardRef}>Content</Card>
63
+ * ```
56
64
  */
57
- export default function Card({
65
+ const Card = forwardRef<HTMLDivElement, CardProps>(({
58
66
  children,
59
67
  variant = 'default',
60
68
  width = 'auto',
@@ -62,7 +70,8 @@ export default function Card({
62
70
  onClick,
63
71
  hoverable = false,
64
72
  loading = false,
65
- }: CardProps) {
73
+ ...htmlProps
74
+ }, ref) => {
66
75
  const baseStyles = 'bg-white bg-subtle-grain border-2 border-paper-300 transition-shadow duration-200';
67
76
 
68
77
  const variantStyles = {
@@ -84,6 +93,8 @@ export default function Card({
84
93
 
85
94
  return (
86
95
  <div
96
+ ref={ref}
97
+ {...htmlProps}
87
98
  className={`${baseStyles} ${variantStyles[variant]} ${widthStyles[width]} ${interactiveStyles} ${className}`}
88
99
  onClick={!loading ? onClick : undefined}
89
100
  role={onClick ? 'button' : undefined}
@@ -101,7 +112,11 @@ export default function Card({
101
112
  )}
102
113
  </div>
103
114
  );
104
- }
115
+ });
116
+
117
+ Card.displayName = 'Card';
118
+
119
+ export default Card;
105
120
 
106
121
  /**
107
122
  * CardHeader component props
@@ -0,0 +1,290 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
3
+ import CurrencyInput from './CurrencyInput';
4
+
5
+ const meta = {
6
+ title: 'Forms/CurrencyInput',
7
+ component: CurrencyInput,
8
+ parameters: {
9
+ layout: 'centered',
10
+ docs: {
11
+ description: {
12
+ component: `
13
+ Specialized input component for monetary values with automatic currency formatting.
14
+
15
+ ## Features
16
+ - **Auto-formatting**: Formats currency with proper symbols and thousands separators
17
+ - **Smart parsing**: Handles user input gracefully, removing invalid characters
18
+ - **Multiple currencies**: Supports any currency code (USD, EUR, GBP, etc.)
19
+ - **Localization**: Respects locale for number formatting
20
+ - **Validation**: Built-in min/max value constraints
21
+ - **Negative values**: Optional support for negative amounts
22
+ - **Precision control**: Configurable decimal places
23
+
24
+ ## Usage
25
+
26
+ \`\`\`tsx
27
+ import { CurrencyInput } from 'notebook-ui';
28
+
29
+ const [price, setPrice] = useState<number | null>(1234.56);
30
+
31
+ <CurrencyInput
32
+ label="Price"
33
+ value={price}
34
+ onChange={setPrice}
35
+ currency="USD"
36
+ precision={2}
37
+ />
38
+ \`\`\`
39
+ `,
40
+ },
41
+ },
42
+ },
43
+ tags: ['autodocs'],
44
+ argTypes: {
45
+ value: {
46
+ control: 'number',
47
+ description: 'Numeric value (unformatted)',
48
+ table: {
49
+ type: { summary: 'number | string' },
50
+ },
51
+ },
52
+ onChange: {
53
+ description: 'Callback when value changes (receives numeric value)',
54
+ table: {
55
+ type: { summary: '(value: number | null) => void' },
56
+ },
57
+ },
58
+ currency: {
59
+ control: 'text',
60
+ description: 'Currency code (ISO 4217)',
61
+ table: {
62
+ type: { summary: 'string' },
63
+ defaultValue: { summary: 'USD' },
64
+ },
65
+ },
66
+ locale: {
67
+ control: 'text',
68
+ description: 'Locale for formatting',
69
+ table: {
70
+ type: { summary: 'string' },
71
+ defaultValue: { summary: 'en-US' },
72
+ },
73
+ },
74
+ precision: {
75
+ control: 'number',
76
+ description: 'Number of decimal places',
77
+ table: {
78
+ type: { summary: 'number' },
79
+ defaultValue: { summary: '2' },
80
+ },
81
+ },
82
+ allowNegative: {
83
+ control: 'boolean',
84
+ description: 'Allow negative values',
85
+ table: {
86
+ type: { summary: 'boolean' },
87
+ defaultValue: { summary: 'false' },
88
+ },
89
+ },
90
+ min: {
91
+ control: 'number',
92
+ description: 'Minimum allowed value',
93
+ table: {
94
+ type: { summary: 'number' },
95
+ },
96
+ },
97
+ max: {
98
+ control: 'number',
99
+ description: 'Maximum allowed value',
100
+ table: {
101
+ type: { summary: 'number' },
102
+ },
103
+ },
104
+ },
105
+ } satisfies Meta<typeof CurrencyInput>;
106
+
107
+ export default meta;
108
+ type Story = StoryObj<typeof meta>;
109
+
110
+ export const Default: Story = {
111
+ render: () => {
112
+ const [value, setValue] = useState<number | null>(1234.56);
113
+ return (
114
+ <div style={{ width: '300px' }}>
115
+ <CurrencyInput
116
+ label="Price"
117
+ value={value}
118
+ onChange={setValue}
119
+ helperText="Enter the product price"
120
+ />
121
+ <div style={{ marginTop: '1rem', fontSize: '0.875rem', color: '#64748b' }}>
122
+ Current value: {value !== null ? `$${value.toFixed(2)}` : 'null'}
123
+ </div>
124
+ </div>
125
+ );
126
+ },
127
+ };
128
+
129
+ export const WithValidation: Story = {
130
+ render: () => {
131
+ const [value, setValue] = useState<number | null>(150);
132
+ const max = 100;
133
+ const hasError = value !== null && value > max;
134
+
135
+ return (
136
+ <div style={{ width: '300px' }}>
137
+ <CurrencyInput
138
+ label="Budget"
139
+ value={value}
140
+ onChange={setValue}
141
+ max={max}
142
+ validationState={hasError ? 'error' : value !== null && value > 0 ? 'success' : null}
143
+ validationMessage={hasError ? `Exceeds maximum budget of $${max}` : value !== null && value > 0 ? 'Within budget' : ''}
144
+ />
145
+ </div>
146
+ );
147
+ },
148
+ };
149
+
150
+ export const DifferentCurrencies: Story = {
151
+ render: () => {
152
+ const [usd, setUsd] = useState<number | null>(1234.56);
153
+ const [eur, setEur] = useState<number | null>(1234.56);
154
+ const [gbp, setGbp] = useState<number | null>(1234.56);
155
+ const [jpy, setJpy] = useState<number | null>(123456);
156
+
157
+ return (
158
+ <div style={{ width: '300px', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
159
+ <CurrencyInput
160
+ label="US Dollars"
161
+ value={usd}
162
+ onChange={setUsd}
163
+ currency="USD"
164
+ locale="en-US"
165
+ />
166
+ <CurrencyInput
167
+ label="Euros"
168
+ value={eur}
169
+ onChange={setEur}
170
+ currency="EUR"
171
+ locale="de-DE"
172
+ />
173
+ <CurrencyInput
174
+ label="British Pounds"
175
+ value={gbp}
176
+ onChange={setGbp}
177
+ currency="GBP"
178
+ locale="en-GB"
179
+ />
180
+ <CurrencyInput
181
+ label="Japanese Yen"
182
+ value={jpy}
183
+ onChange={setJpy}
184
+ currency="JPY"
185
+ locale="ja-JP"
186
+ precision={0}
187
+ />
188
+ </div>
189
+ );
190
+ },
191
+ };
192
+
193
+ export const WithNegativeValues: Story = {
194
+ render: () => {
195
+ const [value, setValue] = useState<number | null>(-500.75);
196
+ return (
197
+ <div style={{ width: '300px' }}>
198
+ <CurrencyInput
199
+ label="Profit/Loss"
200
+ value={value}
201
+ onChange={setValue}
202
+ allowNegative
203
+ helperText="Negative values allowed for losses"
204
+ />
205
+ <div style={{ marginTop: '1rem', fontSize: '0.875rem', color: '#64748b' }}>
206
+ Current value: {value !== null ? `$${value.toFixed(2)}` : 'null'}
207
+ </div>
208
+ </div>
209
+ );
210
+ },
211
+ };
212
+
213
+ export const WithMinMax: Story = {
214
+ render: () => {
215
+ const [value, setValue] = useState<number | null>(50);
216
+ const min = 10;
217
+ const max = 100;
218
+
219
+ return (
220
+ <div style={{ width: '300px' }}>
221
+ <CurrencyInput
222
+ label="Amount"
223
+ value={value}
224
+ onChange={setValue}
225
+ min={min}
226
+ max={max}
227
+ helperText={`Min: $${min}, Max: $${max}`}
228
+ />
229
+ <div style={{ marginTop: '1rem', fontSize: '0.875rem', color: '#64748b' }}>
230
+ Current value: {value !== null ? `$${value.toFixed(2)}` : 'null'}
231
+ </div>
232
+ </div>
233
+ );
234
+ },
235
+ };
236
+
237
+ export const Required: Story = {
238
+ render: () => {
239
+ const [value, setValue] = useState<number | null>(null);
240
+ const hasError = value === null;
241
+
242
+ return (
243
+ <div style={{ width: '300px' }}>
244
+ <CurrencyInput
245
+ label="Donation Amount"
246
+ value={value}
247
+ onChange={setValue}
248
+ required
249
+ validationState={hasError ? 'error' : 'success'}
250
+ validationMessage={hasError ? 'Amount is required' : 'Thank you!'}
251
+ />
252
+ </div>
253
+ );
254
+ },
255
+ };
256
+
257
+ export const Clearable: Story = {
258
+ render: () => {
259
+ const [value, setValue] = useState<number | null>(1234.56);
260
+ return (
261
+ <div style={{ width: '300px' }}>
262
+ <CurrencyInput
263
+ label="Price"
264
+ value={value}
265
+ onChange={setValue}
266
+ clearable
267
+ onClear={() => setValue(null)}
268
+ helperText="Click X to clear the value"
269
+ />
270
+ <div style={{ marginTop: '1rem', fontSize: '0.875rem', color: '#64748b' }}>
271
+ Current value: {value !== null ? `$${value.toFixed(2)}` : 'null'}
272
+ </div>
273
+ </div>
274
+ );
275
+ },
276
+ };
277
+
278
+ export const Disabled: Story = {
279
+ render: () => (
280
+ <div style={{ width: '300px' }}>
281
+ <CurrencyInput
282
+ label="Price"
283
+ value={1234.56}
284
+ onChange={() => {}}
285
+ disabled
286
+ helperText="This field is disabled"
287
+ />
288
+ </div>
289
+ ),
290
+ };
@@ -0,0 +1,193 @@
1
+ import React, { forwardRef, useState, useEffect } from 'react';
2
+ import Input, { InputProps } from './Input';
3
+
4
+ export interface CurrencyInputProps extends Omit<InputProps, 'type' | 'value' | 'onChange' | 'prefix'> {
5
+ /** Numeric value (not formatted) */
6
+ value?: number | string;
7
+ /** Callback when value changes (receives numeric value) */
8
+ onChange?: (value: number | null) => void;
9
+ /** Currency code (default: 'USD') */
10
+ currency?: string;
11
+ /** Locale for formatting (default: 'en-US') */
12
+ locale?: string;
13
+ /** Number of decimal places (default: 2) */
14
+ precision?: number;
15
+ /** Allow negative values (default: false) */
16
+ allowNegative?: boolean;
17
+ /** Minimum allowed value */
18
+ min?: number;
19
+ /** Maximum allowed value */
20
+ max?: number;
21
+ }
22
+
23
+ /**
24
+ * CurrencyInput - Specialized input for monetary values
25
+ *
26
+ * Automatically formats currency values with proper symbols and thousands separators.
27
+ * Handles parsing and validation of numeric currency input.
28
+ *
29
+ * @example Basic usage
30
+ * ```tsx
31
+ * <CurrencyInput
32
+ * label="Price"
33
+ * value={price}
34
+ * onChange={setPrice}
35
+ * currency="USD"
36
+ * />
37
+ * ```
38
+ *
39
+ * @example With validation
40
+ * ```tsx
41
+ * <CurrencyInput
42
+ * label="Budget"
43
+ * value={budget}
44
+ * onChange={setBudget}
45
+ * min={0}
46
+ * max={10000}
47
+ * validationState={budget > 10000 ? 'error' : null}
48
+ * validationMessage={budget > 10000 ? 'Exceeds maximum budget' : ''}
49
+ * />
50
+ * ```
51
+ */
52
+ const CurrencyInput = forwardRef<HTMLInputElement, CurrencyInputProps>(
53
+ (
54
+ {
55
+ value,
56
+ onChange,
57
+ currency = 'USD',
58
+ locale = 'en-US',
59
+ precision = 2,
60
+ allowNegative = false,
61
+ min,
62
+ max,
63
+ onBlur,
64
+ onFocus,
65
+ ...props
66
+ },
67
+ ref
68
+ ) => {
69
+ const [displayValue, setDisplayValue] = useState('');
70
+ const [isFocused, setIsFocused] = useState(false);
71
+
72
+ // Get currency symbol
73
+ const getCurrencySymbol = () => {
74
+ const formatter = new Intl.NumberFormat(locale, {
75
+ style: 'currency',
76
+ currency,
77
+ });
78
+ const parts = formatter.formatToParts(0);
79
+ const symbolPart = parts.find(part => part.type === 'currency');
80
+ return symbolPart?.value || '$';
81
+ };
82
+
83
+ const currencySymbol = getCurrencySymbol();
84
+
85
+ // Format number as currency
86
+ const formatCurrency = (num: number): string => {
87
+ const formatter = new Intl.NumberFormat(locale, {
88
+ minimumFractionDigits: isFocused ? 0 : precision,
89
+ maximumFractionDigits: precision,
90
+ });
91
+ return formatter.format(num);
92
+ };
93
+
94
+ // Parse display value to number
95
+ const parseValue = (str: string): number | null => {
96
+ if (!str || str === '') return null;
97
+
98
+ // Remove all non-numeric characters except decimal point and minus sign
99
+ let cleaned = str.replace(/[^\d.-]/g, '');
100
+
101
+ // Handle multiple decimal points
102
+ const parts = cleaned.split('.');
103
+ if (parts.length > 2) {
104
+ cleaned = parts[0] + '.' + parts.slice(1).join('');
105
+ }
106
+
107
+ // Handle multiple minus signs (keep only first)
108
+ const minusCount = (cleaned.match(/-/g) || []).length;
109
+ if (minusCount > 1) {
110
+ const hasLeadingMinus = cleaned.startsWith('-');
111
+ cleaned = cleaned.replace(/-/g, '');
112
+ if (hasLeadingMinus) cleaned = '-' + cleaned;
113
+ }
114
+
115
+ // Parse to float
116
+ const num = parseFloat(cleaned);
117
+
118
+ if (isNaN(num)) return null;
119
+
120
+ // Apply precision
121
+ return Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision);
122
+ };
123
+
124
+ // Update display value when external value changes
125
+ useEffect(() => {
126
+ if (value === undefined || value === null || value === '') {
127
+ setDisplayValue('');
128
+ } else {
129
+ const numValue = typeof value === 'string' ? parseFloat(value) : value;
130
+ if (!isNaN(numValue)) {
131
+ setDisplayValue(formatCurrency(numValue));
132
+ }
133
+ }
134
+ }, [value, isFocused]);
135
+
136
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
137
+ const inputValue = e.target.value;
138
+ setDisplayValue(inputValue);
139
+
140
+ const numValue = parseValue(inputValue);
141
+
142
+ // Validate constraints
143
+ if (numValue !== null) {
144
+ if (!allowNegative && numValue < 0) return;
145
+ if (min !== undefined && numValue < min) return;
146
+ if (max !== undefined && numValue > max) return;
147
+ }
148
+
149
+ onChange?.(numValue);
150
+ };
151
+
152
+ const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
153
+ setIsFocused(true);
154
+ // Remove formatting when focused for easier editing
155
+ if (value !== undefined && value !== null && value !== '') {
156
+ const numValue = typeof value === 'string' ? parseFloat(value) : value;
157
+ if (!isNaN(numValue)) {
158
+ setDisplayValue(numValue.toString());
159
+ }
160
+ }
161
+ onFocus?.(e);
162
+ };
163
+
164
+ const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
165
+ setIsFocused(false);
166
+ // Reformat on blur
167
+ const numValue = parseValue(displayValue);
168
+ if (numValue !== null) {
169
+ setDisplayValue(formatCurrency(numValue));
170
+ } else if (displayValue === '') {
171
+ setDisplayValue('');
172
+ }
173
+ onBlur?.(e);
174
+ };
175
+
176
+ return (
177
+ <Input
178
+ ref={ref}
179
+ type="text"
180
+ value={displayValue}
181
+ onChange={handleChange}
182
+ onFocus={handleFocus}
183
+ onBlur={handleBlur}
184
+ prefix={currencySymbol}
185
+ {...props}
186
+ />
187
+ );
188
+ }
189
+ );
190
+
191
+ CurrencyInput.displayName = 'CurrencyInput';
192
+
193
+ export default CurrencyInput;