@qwickapps/react-framework 1.4.8 → 1.4.9
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 +13 -3
- package/dist/index.css +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +255 -54
- package/dist/index.js +249 -48
- package/dist/src/components/Logo.d.ts.map +1 -1
- package/dist/src/components/ResponsiveMenu.d.ts.map +1 -1
- package/dist/src/components/SafeSpan.d.ts.map +1 -1
- package/dist/src/components/Scaffold.d.ts.map +1 -1
- package/dist/src/components/blocks/Article.d.ts.map +1 -1
- package/dist/src/components/blocks/Footer.d.ts.map +1 -1
- package/dist/src/components/blocks/Text.d.ts.map +1 -1
- package/dist/src/components/buttons/Button.d.ts +26 -10
- package/dist/src/components/buttons/Button.d.ts.map +1 -1
- package/dist/src/components/menu/MenuItem.d.ts +2 -2
- package/dist/src/components/menu/MenuItem.d.ts.map +1 -1
- package/dist/src/schemas/ButtonSchema.d.ts +3 -0
- package/dist/src/schemas/ButtonSchema.d.ts.map +1 -1
- package/dist/src/schemas/transformers/ComponentTransformer.d.ts.map +1 -1
- package/dist/src/schemas/transformers/ReactNodeTransformer.d.ts.map +1 -1
- package/package.json +9 -1
- package/src/components/Html.tsx +1 -1
- package/src/components/Logo.tsx +2 -4
- package/src/components/QwickApp.css +19 -13
- package/src/components/ResponsiveMenu.tsx +12 -1
- package/src/components/SafeSpan.tsx +0 -1
- package/src/components/Scaffold.css +14 -0
- package/src/components/Scaffold.tsx +16 -2
- package/src/components/blocks/Article.tsx +78 -7
- package/src/components/blocks/CoverImageHeader.tsx +1 -1
- package/src/components/blocks/Footer.tsx +23 -23
- package/src/components/blocks/PageBannerHeader.tsx +1 -1
- package/src/components/blocks/Text.tsx +7 -4
- package/src/components/buttons/Button.tsx +189 -15
- package/src/components/menu/MenuItem.tsx +2 -2
- package/src/contexts/ThemeContext.tsx +1 -1
- package/src/schemas/ButtonSchema.ts +33 -0
- package/src/schemas/ViewSchema.ts +1 -1
- package/src/schemas/transformers/ComponentTransformer.ts +2 -8
- package/src/schemas/transformers/ReactNodeTransformer.ts +1 -2
- package/src/stories/Article.stories.tsx +1 -1
- package/src/stories/ChoiceInputField.stories.tsx +2 -2
|
@@ -16,9 +16,106 @@
|
|
|
16
16
|
|
|
17
17
|
import React, { ReactElement } from 'react';
|
|
18
18
|
import { Button as MuiButton, ButtonProps as MuiButtonProps, CircularProgress } from '@mui/material';
|
|
19
|
+
import {
|
|
20
|
+
Home,
|
|
21
|
+
Download,
|
|
22
|
+
Settings,
|
|
23
|
+
Dashboard,
|
|
24
|
+
Info,
|
|
25
|
+
Help,
|
|
26
|
+
Add,
|
|
27
|
+
Edit,
|
|
28
|
+
Delete,
|
|
29
|
+
Check,
|
|
30
|
+
Close,
|
|
31
|
+
ArrowForward,
|
|
32
|
+
ArrowBack,
|
|
33
|
+
Menu,
|
|
34
|
+
Search,
|
|
35
|
+
Favorite,
|
|
36
|
+
Star,
|
|
37
|
+
Share,
|
|
38
|
+
CloudUpload,
|
|
39
|
+
CloudDownload,
|
|
40
|
+
Save,
|
|
41
|
+
Send,
|
|
42
|
+
Email,
|
|
43
|
+
Phone,
|
|
44
|
+
Person,
|
|
45
|
+
Group,
|
|
46
|
+
Business,
|
|
47
|
+
ShoppingCart,
|
|
48
|
+
AttachMoney,
|
|
49
|
+
Lock,
|
|
50
|
+
LockOpen,
|
|
51
|
+
Visibility,
|
|
52
|
+
VisibilityOff,
|
|
53
|
+
} from '@mui/icons-material';
|
|
19
54
|
import ButtonModel from '../../schemas/ButtonSchema';
|
|
20
55
|
import { createSerializableView, SerializableComponent } from '../shared/createSerializableView';
|
|
21
56
|
import { ViewProps } from '../shared/viewProps';
|
|
57
|
+
import type { SchemaProps } from '@qwickapps/schema/src/types/ModelProps';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Icon name to Material-UI icon component mapping
|
|
61
|
+
* Used by finalize function to transform icon string names to React components
|
|
62
|
+
*/
|
|
63
|
+
const iconRegistry: Record<string, React.ComponentType> = {
|
|
64
|
+
home: Home,
|
|
65
|
+
download: Download,
|
|
66
|
+
clouddownload: CloudDownload,
|
|
67
|
+
cloudupload: CloudUpload,
|
|
68
|
+
settings: Settings,
|
|
69
|
+
dashboard: Dashboard,
|
|
70
|
+
info: Info,
|
|
71
|
+
help: Help,
|
|
72
|
+
add: Add,
|
|
73
|
+
edit: Edit,
|
|
74
|
+
delete: Delete,
|
|
75
|
+
check: Check,
|
|
76
|
+
close: Close,
|
|
77
|
+
arrowforward: ArrowForward,
|
|
78
|
+
arrowback: ArrowBack,
|
|
79
|
+
menu: Menu,
|
|
80
|
+
search: Search,
|
|
81
|
+
favorite: Favorite,
|
|
82
|
+
star: Star,
|
|
83
|
+
share: Share,
|
|
84
|
+
save: Save,
|
|
85
|
+
send: Send,
|
|
86
|
+
email: Email,
|
|
87
|
+
phone: Phone,
|
|
88
|
+
person: Person,
|
|
89
|
+
group: Group,
|
|
90
|
+
business: Business,
|
|
91
|
+
shoppingcart: ShoppingCart,
|
|
92
|
+
attachmoney: AttachMoney,
|
|
93
|
+
lock: Lock,
|
|
94
|
+
lockopen: LockOpen,
|
|
95
|
+
visibility: Visibility,
|
|
96
|
+
visibilityoff: VisibilityOff,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get icon component from icon name string
|
|
101
|
+
* Exported for use by other components (Scaffold, ResponsiveMenu, etc.)
|
|
102
|
+
*/
|
|
103
|
+
export function getIconComponent(iconName: string | undefined): React.ReactElement | null {
|
|
104
|
+
if (!iconName) return null;
|
|
105
|
+
|
|
106
|
+
const IconComponent = iconRegistry[iconName.toLowerCase()];
|
|
107
|
+
if (!IconComponent) {
|
|
108
|
+
console.warn(`[Button] Icon "${iconName}" not found in registry`);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return <IconComponent />;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Export icon registry for extension by applications
|
|
117
|
+
*/
|
|
118
|
+
export { iconRegistry };
|
|
22
119
|
|
|
23
120
|
// Action serialization pattern for button clicks
|
|
24
121
|
export interface ButtonAction {
|
|
@@ -29,27 +126,28 @@ export interface ButtonAction {
|
|
|
29
126
|
customHandler?: string;
|
|
30
127
|
}
|
|
31
128
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Additional props for enhanced functionality
|
|
129
|
+
/**
|
|
130
|
+
* Props interface for Button component
|
|
131
|
+
* Uses SchemaProps<typeof ButtonSchema> for schema-driven typing
|
|
132
|
+
* Icons are transformed from strings to React components by finalize function
|
|
133
|
+
*
|
|
134
|
+
* Note: We omit 'icon' and 'endIcon' from schema props because they are strings in the schema
|
|
135
|
+
* but get transformed to ReactElements by the finalize function.
|
|
136
|
+
*/
|
|
137
|
+
export type ButtonProps = ViewProps & Omit<SchemaProps<typeof ButtonModel>, 'icon' | 'endIcon'> & {
|
|
138
|
+
// Runtime props (transformed from schema strings by finalize function)
|
|
43
139
|
icon?: React.ReactNode;
|
|
44
140
|
endIcon?: React.ReactNode;
|
|
141
|
+
// Additional runtime-only props
|
|
45
142
|
action?: ButtonAction;
|
|
46
|
-
}
|
|
143
|
+
};
|
|
47
144
|
|
|
48
145
|
// View component - handles the actual rendering
|
|
49
146
|
const ButtonView = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
|
|
50
147
|
const {
|
|
51
148
|
label,
|
|
52
149
|
variant = 'primary',
|
|
150
|
+
color,
|
|
53
151
|
buttonSize = 'medium',
|
|
54
152
|
icon,
|
|
55
153
|
endIcon,
|
|
@@ -82,8 +180,14 @@ const ButtonView = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref)
|
|
|
82
180
|
}
|
|
83
181
|
};
|
|
84
182
|
|
|
85
|
-
//
|
|
86
|
-
const
|
|
183
|
+
// Get theme color name - prioritize explicit color prop over variant-derived color
|
|
184
|
+
const getThemeColorName = (): string => {
|
|
185
|
+
// If color prop is explicitly provided, use it
|
|
186
|
+
if (color) {
|
|
187
|
+
return color;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Otherwise derive from variant
|
|
87
191
|
switch (variant) {
|
|
88
192
|
case 'primary':
|
|
89
193
|
return 'primary';
|
|
@@ -100,6 +204,54 @@ const ButtonView = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref)
|
|
|
100
204
|
}
|
|
101
205
|
};
|
|
102
206
|
|
|
207
|
+
// Get CSS theme variable styles based on variant and color
|
|
208
|
+
const getColorStyles = () => {
|
|
209
|
+
const muiVariant = getMuiVariant();
|
|
210
|
+
const colorName = getThemeColorName();
|
|
211
|
+
|
|
212
|
+
// For contained buttons: use solid background
|
|
213
|
+
if (muiVariant === 'contained') {
|
|
214
|
+
return {
|
|
215
|
+
backgroundColor: `var(--theme-${colorName})`,
|
|
216
|
+
color: `var(--theme-on-${colorName})`,
|
|
217
|
+
'&:hover': {
|
|
218
|
+
backgroundColor: `var(--theme-${colorName}-dark)`,
|
|
219
|
+
},
|
|
220
|
+
'&.Mui-disabled': {
|
|
221
|
+
backgroundColor: 'var(--theme-text-disabled)',
|
|
222
|
+
color: 'var(--theme-on-surface)',
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// For outlined buttons: border and text color
|
|
228
|
+
if (muiVariant === 'outlined') {
|
|
229
|
+
return {
|
|
230
|
+
borderColor: `var(--theme-${colorName})`,
|
|
231
|
+
color: `var(--theme-${colorName})`,
|
|
232
|
+
'&:hover': {
|
|
233
|
+
borderColor: `var(--theme-${colorName}-dark)`,
|
|
234
|
+
backgroundColor: `var(--theme-${colorName}-light)`,
|
|
235
|
+
},
|
|
236
|
+
'&.Mui-disabled': {
|
|
237
|
+
borderColor: 'var(--theme-border-main)',
|
|
238
|
+
color: 'var(--theme-text-disabled)',
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// For text buttons: just text color
|
|
244
|
+
return {
|
|
245
|
+
color: `var(--theme-${colorName})`,
|
|
246
|
+
'&:hover': {
|
|
247
|
+
backgroundColor: `var(--theme-${colorName}-light)`,
|
|
248
|
+
},
|
|
249
|
+
'&.Mui-disabled': {
|
|
250
|
+
color: 'var(--theme-text-disabled)',
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
|
|
103
255
|
// Handle action serialization pattern for onClick
|
|
104
256
|
const handleActionClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
|
|
105
257
|
if (action) {
|
|
@@ -163,12 +315,13 @@ const ButtonView = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref)
|
|
|
163
315
|
// Base button props
|
|
164
316
|
const baseProps: Partial<MuiButtonProps> = {
|
|
165
317
|
variant: getMuiVariant(),
|
|
166
|
-
color: getMuiColor(),
|
|
167
318
|
size: buttonSize,
|
|
168
319
|
disabled: disabled || loading,
|
|
169
320
|
fullWidth,
|
|
170
321
|
...restProps,
|
|
171
322
|
sx: {
|
|
323
|
+
// Apply CSS theme variable colors
|
|
324
|
+
...getColorStyles(),
|
|
172
325
|
// Ensure consistent text transform
|
|
173
326
|
textTransform: 'none',
|
|
174
327
|
// Loading state adjustments
|
|
@@ -177,6 +330,7 @@ const ButtonView = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref)
|
|
|
177
330
|
marginRight: 1,
|
|
178
331
|
},
|
|
179
332
|
}),
|
|
333
|
+
// Merge user-provided sx prop last to allow overrides
|
|
180
334
|
...(restProps.sx || {}),
|
|
181
335
|
},
|
|
182
336
|
startIcon: loading ? (
|
|
@@ -229,6 +383,26 @@ export const Button: SerializableComponent<ButtonProps> = createSerializableView
|
|
|
229
383
|
role: 'view',
|
|
230
384
|
View: ButtonView,
|
|
231
385
|
// Button component uses default react-children strategy for potential child content
|
|
386
|
+
finalize: (props: ButtonProps) => {
|
|
387
|
+
// Transform icon string names to React icon components
|
|
388
|
+
const transformedProps = { ...props };
|
|
389
|
+
|
|
390
|
+
if (typeof (props as any).icon === 'string') {
|
|
391
|
+
const iconComponent = getIconComponent((props as any).icon);
|
|
392
|
+
if (iconComponent) {
|
|
393
|
+
transformedProps.icon = iconComponent;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (typeof (props as any).endIcon === 'string') {
|
|
398
|
+
const endIconComponent = getIconComponent((props as any).endIcon);
|
|
399
|
+
if (endIconComponent) {
|
|
400
|
+
transformedProps.endIcon = endIconComponent;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return transformedProps;
|
|
405
|
+
}
|
|
232
406
|
});
|
|
233
407
|
|
|
234
408
|
// Register HTML patterns that Button component can handle
|
|
@@ -13,8 +13,8 @@ export interface MenuItem {
|
|
|
13
13
|
id: string;
|
|
14
14
|
/** Display label for the menu item */
|
|
15
15
|
label: string;
|
|
16
|
-
/** Icon
|
|
17
|
-
icon?: React.ReactNode;
|
|
16
|
+
/** Icon name (string) or icon component (React.ReactNode) to display */
|
|
17
|
+
icon?: string | React.ReactNode;
|
|
18
18
|
/** Click handler for the menu item */
|
|
19
19
|
onClick?: () => void;
|
|
20
20
|
/** External link URL (if this is a link) */
|
|
@@ -242,7 +242,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
|
|
242
242
|
const surfaceMain = getCSSCustomProperty('--theme-surface') || (isDark ? '#1e1e1e' : '#ffffff');
|
|
243
243
|
|
|
244
244
|
const textPrimary = getCSSCustomProperty('--theme-text-primary') || (isDark ? '#ffffff' : '#000000');
|
|
245
|
-
const textSecondary = getCSSCustomProperty('--theme-text-secondary') || (isDark ? '
|
|
245
|
+
const textSecondary = getCSSCustomProperty('--theme-text-secondary') || (isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.6)');
|
|
246
246
|
const textDisabled = getCSSCustomProperty('--theme-on-disabled') || (isDark ? 'rgba(255, 255, 255, 0.38)' : 'rgba(0, 0, 0, 0.38)');
|
|
247
247
|
|
|
248
248
|
return createTheme({
|
|
@@ -33,6 +33,17 @@ export class ButtonModel extends ViewSchema {
|
|
|
33
33
|
@IsIn(['primary', 'secondary', 'outlined', 'text', 'contained'])
|
|
34
34
|
variant?: 'primary' | 'secondary' | 'outlined' | 'text' | 'contained';
|
|
35
35
|
|
|
36
|
+
@Field()
|
|
37
|
+
@Editor({
|
|
38
|
+
field_type: FieldType.SELECT,
|
|
39
|
+
label: 'Button Color',
|
|
40
|
+
description: 'Color theme for the button (overrides variant color)'
|
|
41
|
+
})
|
|
42
|
+
@IsOptional()
|
|
43
|
+
@IsString()
|
|
44
|
+
@IsIn(['primary', 'secondary', 'error', 'warning', 'info', 'success'])
|
|
45
|
+
color?: 'primary' | 'secondary' | 'error' | 'warning' | 'info' | 'success';
|
|
46
|
+
|
|
36
47
|
@Field({ defaultValue: 'medium' })
|
|
37
48
|
@Editor({
|
|
38
49
|
field_type: FieldType.SELECT,
|
|
@@ -95,6 +106,28 @@ export class ButtonModel extends ViewSchema {
|
|
|
95
106
|
@IsOptional()
|
|
96
107
|
@IsBoolean()
|
|
97
108
|
fullWidth?: boolean;
|
|
109
|
+
|
|
110
|
+
@Field()
|
|
111
|
+
@Editor({
|
|
112
|
+
field_type: FieldType.TEXT,
|
|
113
|
+
label: 'Icon',
|
|
114
|
+
description: 'Icon name to display before button text (e.g., "home", "download", "settings")',
|
|
115
|
+
placeholder: 'Icon name...'
|
|
116
|
+
})
|
|
117
|
+
@IsOptional()
|
|
118
|
+
@IsString()
|
|
119
|
+
icon?: string;
|
|
120
|
+
|
|
121
|
+
@Field()
|
|
122
|
+
@Editor({
|
|
123
|
+
field_type: FieldType.TEXT,
|
|
124
|
+
label: 'End Icon',
|
|
125
|
+
description: 'Icon name to display after button text',
|
|
126
|
+
placeholder: 'Icon name...'
|
|
127
|
+
})
|
|
128
|
+
@IsOptional()
|
|
129
|
+
@IsString()
|
|
130
|
+
endIcon?: string;
|
|
98
131
|
}
|
|
99
132
|
|
|
100
133
|
export default ButtonModel;
|
|
@@ -516,7 +516,7 @@ export class ViewSchema extends Model {
|
|
|
516
516
|
field_type: FieldType.TEXTAREA,
|
|
517
517
|
label: 'Click Handler',
|
|
518
518
|
description: 'JavaScript function for click events',
|
|
519
|
-
placeholder: 'function(event) { console.
|
|
519
|
+
placeholder: 'function(event) { console.debug("clicked"); }'
|
|
520
520
|
})
|
|
521
521
|
@IsOptional()
|
|
522
522
|
@IsString()
|
|
@@ -85,7 +85,6 @@ export class ComponentTransformer {
|
|
|
85
85
|
console.warn(`Component '${tagName}' is already registered. Overwriting existing registration.`);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
console.log(`TEST: Registering component: ${tagName} (version ${version})`);
|
|
89
88
|
componentRegistry.set(tagName, componentClass);
|
|
90
89
|
|
|
91
90
|
// Register HTML patterns if component supports them
|
|
@@ -210,14 +209,10 @@ export class ComponentTransformer {
|
|
|
210
209
|
if (typeof componentType === 'function') {
|
|
211
210
|
const tagName = ComponentTransformer.findTagNameForComponent(componentType);
|
|
212
211
|
if (tagName) {
|
|
213
|
-
console.log(`TEST: Serializing component instance of ${tagName}:`, element.props);
|
|
214
212
|
const componentClass = componentRegistry.get(tagName)!;
|
|
215
213
|
|
|
216
214
|
let serializedData: any = null;
|
|
217
|
-
console.log(`TEST: Element type:`, typeof (element as any).toJson);
|
|
218
|
-
console.log(`TEST: Component type:`, typeof (componentClass as any).toJson);
|
|
219
215
|
if (typeof (componentClass as any).toJson === 'function') {
|
|
220
|
-
console.log(`TEST: Serializing via component toJson method ${tagName}:`, element.props);
|
|
221
216
|
serializedData = (componentClass as any).toJson(element.props);
|
|
222
217
|
}
|
|
223
218
|
|
|
@@ -231,7 +226,7 @@ export class ComponentTransformer {
|
|
|
231
226
|
...key,
|
|
232
227
|
...serializedData
|
|
233
228
|
};
|
|
234
|
-
|
|
229
|
+
|
|
235
230
|
return result;
|
|
236
231
|
}
|
|
237
232
|
} else if (strictMode) {
|
|
@@ -251,8 +246,7 @@ export class ComponentTransformer {
|
|
|
251
246
|
version: FALLBACK_VERSION,
|
|
252
247
|
data: ReactNodeTransformer.serialize(node)
|
|
253
248
|
};
|
|
254
|
-
|
|
255
|
-
console.log(`TEST: Component Registry:`, componentRegistry);
|
|
249
|
+
|
|
256
250
|
return result;
|
|
257
251
|
}
|
|
258
252
|
|
|
@@ -45,7 +45,6 @@ export class ReactNodeTransformer {
|
|
|
45
45
|
// Handle React elements
|
|
46
46
|
if (isValidElement(node)) {
|
|
47
47
|
const element = node as ReactElement;
|
|
48
|
-
console.log(`TEST: isValidElement:`, element);
|
|
49
48
|
|
|
50
49
|
const comp: any = element.type;
|
|
51
50
|
const rawName =
|
|
@@ -176,7 +175,7 @@ export class ReactNodeTransformer {
|
|
|
176
175
|
// Handle HTML elements
|
|
177
176
|
if (typeof elementType === 'string') {
|
|
178
177
|
const deserializedProps = ReactNodeTransformer.deserializeProps(props);
|
|
179
|
-
|
|
178
|
+
|
|
180
179
|
return createElement(elementType, { key, ...deserializedProps });
|
|
181
180
|
}
|
|
182
181
|
} catch (error) {
|
|
@@ -58,7 +58,7 @@ function MyApp() {
|
|
|
58
58
|
<ProductCard dataSource="products.featured" />
|
|
59
59
|
</QwickApp></code></pre>
|
|
60
60
|
|
|
61
|
-
<p>You can also use inline code like <code>console.
|
|
61
|
+
<p>You can also use inline code like <code>console.debug('hello')</code> within paragraphs.</p>
|
|
62
62
|
|
|
63
63
|
<h2>Complex Multi-line Code</h2>
|
|
64
64
|
<code>
|
|
@@ -198,12 +198,12 @@ function InteractiveChoiceExample({
|
|
|
198
198
|
const newOptions = [...options];
|
|
199
199
|
newOptions[optionIndex] = value;
|
|
200
200
|
setOptions(newOptions);
|
|
201
|
-
console.
|
|
201
|
+
console.debug('Option changed:', { optionIndex, value, allOptions: newOptions });
|
|
202
202
|
};
|
|
203
203
|
|
|
204
204
|
const handleAddOption = () => {
|
|
205
205
|
setOptions([...options, '']);
|
|
206
|
-
console.
|
|
206
|
+
console.debug('Added new option, total:', options.length + 1);
|
|
207
207
|
};
|
|
208
208
|
|
|
209
209
|
return (
|