@qwickapps/react-framework 1.3.2 → 1.3.4

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 (66) hide show
  1. package/README.md +326 -0
  2. package/dist/components/AccessibilityProvider.d.ts +64 -0
  3. package/dist/components/AccessibilityProvider.d.ts.map +1 -0
  4. package/dist/components/Breadcrumbs.d.ts +39 -0
  5. package/dist/components/Breadcrumbs.d.ts.map +1 -0
  6. package/dist/components/ErrorBoundary.d.ts +39 -0
  7. package/dist/components/ErrorBoundary.d.ts.map +1 -0
  8. package/dist/components/QwickApp.d.ts.map +1 -1
  9. package/dist/components/forms/FormBlock.d.ts +1 -1
  10. package/dist/components/forms/FormBlock.d.ts.map +1 -1
  11. package/dist/components/index.d.ts +3 -0
  12. package/dist/components/index.d.ts.map +1 -1
  13. package/dist/components/input/SwitchInputField.d.ts +28 -0
  14. package/dist/components/input/SwitchInputField.d.ts.map +1 -0
  15. package/dist/components/input/index.d.ts +2 -0
  16. package/dist/components/input/index.d.ts.map +1 -1
  17. package/dist/components/layout/CollapsibleLayout/CollapsibleLayout.d.ts +34 -0
  18. package/dist/components/layout/CollapsibleLayout/CollapsibleLayout.d.ts.map +1 -0
  19. package/dist/components/layout/CollapsibleLayout/index.d.ts +9 -0
  20. package/dist/components/layout/CollapsibleLayout/index.d.ts.map +1 -0
  21. package/dist/components/layout/index.d.ts +2 -0
  22. package/dist/components/layout/index.d.ts.map +1 -1
  23. package/dist/index.bundled.css +12 -0
  24. package/dist/index.esm.js +1678 -25
  25. package/dist/index.js +1689 -21
  26. package/dist/schemas/CollapsibleLayoutSchema.d.ts +31 -0
  27. package/dist/schemas/CollapsibleLayoutSchema.d.ts.map +1 -0
  28. package/dist/schemas/SwitchInputFieldSchema.d.ts +18 -0
  29. package/dist/schemas/SwitchInputFieldSchema.d.ts.map +1 -0
  30. package/dist/types/CollapsibleLayout.d.ts +142 -0
  31. package/dist/types/CollapsibleLayout.d.ts.map +1 -0
  32. package/dist/types/index.d.ts +1 -0
  33. package/dist/types/index.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/components/AccessibilityProvider.tsx +466 -0
  36. package/src/components/Breadcrumbs.tsx +223 -0
  37. package/src/components/ErrorBoundary.tsx +216 -0
  38. package/src/components/QwickApp.tsx +17 -11
  39. package/src/components/__tests__/AccessibilityProvider.test.tsx +330 -0
  40. package/src/components/__tests__/Breadcrumbs.test.tsx +268 -0
  41. package/src/components/__tests__/ErrorBoundary.test.tsx +163 -0
  42. package/src/components/forms/FormBlock.tsx +2 -2
  43. package/src/components/index.ts +3 -0
  44. package/src/components/input/SwitchInputField.tsx +165 -0
  45. package/src/components/input/index.ts +2 -0
  46. package/src/components/layout/CollapsibleLayout/CollapsibleLayout.tsx +554 -0
  47. package/src/components/layout/CollapsibleLayout/__tests__/CollapsibleLayout.test.tsx +1469 -0
  48. package/src/components/layout/CollapsibleLayout/index.tsx +17 -0
  49. package/src/components/layout/index.ts +4 -1
  50. package/src/components/pages/FormPage.tsx +1 -1
  51. package/src/schemas/CollapsibleLayoutSchema.ts +276 -0
  52. package/src/schemas/SwitchInputFieldSchema.ts +99 -0
  53. package/src/stories/AccessibilityProvider.stories.tsx +284 -0
  54. package/src/stories/Breadcrumbs.stories.tsx +304 -0
  55. package/src/stories/CollapsibleLayout.stories.tsx +1566 -0
  56. package/src/stories/ErrorBoundary.stories.tsx +159 -0
  57. package/src/types/CollapsibleLayout.ts +231 -0
  58. package/src/types/index.ts +1 -0
  59. package/dist/schemas/Builders.d.ts +0 -7
  60. package/dist/schemas/Builders.d.ts.map +0 -1
  61. package/dist/schemas/types.d.ts +0 -7
  62. package/dist/schemas/types.d.ts.map +0 -1
  63. package/dist/types/DataBinding.d.ts +0 -7
  64. package/dist/types/DataBinding.d.ts.map +0 -1
  65. package/dist/types/DataProvider.d.ts +0 -7
  66. package/dist/types/DataProvider.d.ts.map +0 -1
@@ -0,0 +1,554 @@
1
+ /**
2
+ * CollapsibleLayout Component - Advanced expandable/collapsible container
3
+ *
4
+ * Features:
5
+ * - Controlled and uncontrolled state management
6
+ * - Multiple animation styles (fade, slide, scale)
7
+ * - Customizable trigger areas (button, header, both)
8
+ * - localStorage persistence
9
+ * - Full accessibility support
10
+ * - Material-UI integration with themes
11
+ * - Multiple visual variants
12
+ *
13
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
14
+ */
15
+
16
+ import React, { useCallback, useEffect, useId, useMemo, useState } from 'react';
17
+ import {
18
+ Box,
19
+ Typography,
20
+ IconButton,
21
+ Collapse,
22
+ Divider,
23
+ Paper,
24
+ Stack,
25
+ useTheme,
26
+ SxProps,
27
+ Theme,
28
+ } from '@mui/material';
29
+ import {
30
+ ExpandMore as ExpandMoreIcon,
31
+ ExpandLess as ExpandLessIcon,
32
+ Visibility as VisibilityIcon,
33
+ VisibilityOff as VisibilityOffIcon,
34
+ } from '@mui/icons-material';
35
+ import { WithDataBinding } from '@qwickapps/schema';
36
+ import { QWICKAPP_COMPONENT, useBaseProps, useDataBinding } from '../../../hooks';
37
+ import CollapsibleLayoutModel from '../../../schemas/CollapsibleLayoutSchema';
38
+ import {
39
+ CollapsibleLayoutViewProps,
40
+ CollapsibleLayoutProps,
41
+ UseCollapsibleLayoutState,
42
+ spacingConfigs,
43
+ animationConfigs,
44
+ defaultCollapsibleLayoutProps,
45
+ } from '../../../types/CollapsibleLayout';
46
+ import Html from '../../Html';
47
+
48
+ /**
49
+ * Custom hook for managing collapsible state - simplified approach
50
+ */
51
+ function useCollapsibleState(
52
+ controlled: boolean,
53
+ collapsedProp?: boolean,
54
+ defaultCollapsedProp?: boolean,
55
+ onToggleProp?: (collapsed: boolean) => void,
56
+ persistState?: boolean,
57
+ storageKey?: string
58
+ ): UseCollapsibleLayoutState {
59
+ const id = useId();
60
+ const finalStorageKey = storageKey || `collapsible-${id}`;
61
+
62
+ // For controlled components, use the prop; for uncontrolled, use internal state
63
+ const [internalCollapsed, setInternalCollapsed] = useState<boolean>(() => {
64
+ if (controlled) {
65
+ return collapsedProp ?? false;
66
+ }
67
+
68
+ // Try localStorage first if persistence is enabled
69
+ if (persistState && typeof window !== 'undefined') {
70
+ const stored = localStorage.getItem(finalStorageKey);
71
+ if (stored !== null) {
72
+ return JSON.parse(stored);
73
+ }
74
+ }
75
+
76
+ return defaultCollapsedProp ?? false;
77
+ });
78
+
79
+ // Sync with controlled prop changes
80
+ useEffect(() => {
81
+ if (controlled && collapsedProp !== undefined) {
82
+ setInternalCollapsed(collapsedProp);
83
+ }
84
+ }, [controlled, collapsedProp]);
85
+
86
+ // Persist to localStorage for uncontrolled components
87
+ useEffect(() => {
88
+ if (!controlled && persistState && typeof window !== 'undefined') {
89
+ localStorage.setItem(finalStorageKey, JSON.stringify(internalCollapsed));
90
+ }
91
+ }, [controlled, internalCollapsed, persistState, finalStorageKey]);
92
+
93
+ const toggle = useCallback(() => {
94
+ const currentState = controlled ? (collapsedProp ?? false) : internalCollapsed;
95
+ const newCollapsed = !currentState;
96
+
97
+ // Always update internal state for visual updates
98
+ setInternalCollapsed(newCollapsed);
99
+
100
+ // Call callback to notify parent
101
+ onToggleProp?.(newCollapsed);
102
+ }, [controlled, collapsedProp, internalCollapsed, onToggleProp]);
103
+
104
+ const setCollapsed = useCallback((collapsed: boolean) => {
105
+ setInternalCollapsed(collapsed);
106
+ onToggleProp?.(collapsed);
107
+ }, [onToggleProp]);
108
+
109
+ // Return the appropriate state
110
+ const finalCollapsed = controlled ? (collapsedProp ?? false) : internalCollapsed;
111
+
112
+ return {
113
+ collapsed: finalCollapsed,
114
+ toggle,
115
+ setCollapsed,
116
+ isControlled: controlled,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Core CollapsibleLayout View component
122
+ */
123
+ function CollapsibleLayoutView({
124
+ // State props
125
+ collapsed: collapsedProp,
126
+ defaultCollapsed = false,
127
+ onToggle,
128
+
129
+ // Content props
130
+ title,
131
+ subtitle,
132
+ leadIcon,
133
+ headerActions,
134
+ collapsedView,
135
+ children,
136
+ footerView,
137
+
138
+ // Behavior props
139
+ triggerArea = 'both',
140
+ animationStyle = 'slide',
141
+ persistState = false,
142
+ storageKey,
143
+ animationDuration = 300,
144
+ disableAnimations = false,
145
+
146
+ // Icons
147
+ collapsedIcon,
148
+ expandedIcon,
149
+
150
+ // Layout props
151
+ showDivider = true,
152
+ variant = 'default',
153
+ headerSpacing = 'comfortable',
154
+ contentSpacing = 'comfortable',
155
+
156
+ // Accessibility
157
+ toggleAriaLabel = 'Toggle content visibility',
158
+ 'aria-describedby': ariaDescribedBy,
159
+ contentAriaProps = {},
160
+
161
+ // CSS classes
162
+ containerClassName,
163
+ headerClassName,
164
+ contentClassName,
165
+ footerClassName,
166
+
167
+ ...restProps
168
+ }: CollapsibleLayoutViewProps) {
169
+ const theme = useTheme();
170
+ const { styleProps, htmlProps, restProps: otherProps } = useBaseProps(restProps);
171
+
172
+ // Mark as QwickApp component
173
+ (CollapsibleLayoutView as any)[QWICKAPP_COMPONENT] = true;
174
+
175
+ // Determine controlled vs uncontrolled usage
176
+ const controlled = collapsedProp !== undefined;
177
+
178
+ // State management
179
+ const { collapsed, toggle, setCollapsed } = useCollapsibleState(
180
+ controlled,
181
+ collapsedProp,
182
+ defaultCollapsed,
183
+ onToggle,
184
+ persistState,
185
+ storageKey
186
+ );
187
+
188
+ // Get spacing configurations
189
+ const headerSpacingConfig = spacingConfigs[headerSpacing];
190
+ const contentSpacingConfig = spacingConfigs[contentSpacing];
191
+
192
+ // Get animation configuration
193
+ const animationConfig = animationConfigs[animationStyle];
194
+
195
+ // Generate unique IDs for accessibility
196
+ const headerId = useId();
197
+ const contentId = useId();
198
+
199
+ // Handle click events
200
+ const handleHeaderClick = useCallback((event: React.MouseEvent) => {
201
+ if (triggerArea === 'header' || triggerArea === 'both') {
202
+ event.preventDefault();
203
+ toggle();
204
+ }
205
+ }, [triggerArea, toggle]);
206
+
207
+ const handleButtonClick = useCallback((event: React.MouseEvent) => {
208
+ event.stopPropagation();
209
+ toggle();
210
+ }, [toggle]);
211
+
212
+ // Render icons
213
+ const renderIcon = useCallback((icon: React.ReactNode | string | undefined, defaultIcon: React.ReactNode) => {
214
+ if (React.isValidElement(icon)) {
215
+ return icon;
216
+ }
217
+ if (typeof icon === 'string') {
218
+ return <Html>{icon}</Html>;
219
+ }
220
+ return defaultIcon;
221
+ }, []);
222
+
223
+ const toggleIcon = renderIcon(
224
+ collapsed ? collapsedIcon : expandedIcon,
225
+ collapsed ? <ExpandMoreIcon /> : <ExpandLessIcon />
226
+ );
227
+
228
+ // Container styles based on variant
229
+ const containerSx: SxProps<Theme> = useMemo(() => {
230
+ const baseSx: SxProps<Theme> = {
231
+ width: '100%',
232
+ position: 'relative',
233
+ ...styleProps.sx,
234
+ };
235
+
236
+ switch (variant) {
237
+ case 'outlined':
238
+ return {
239
+ ...baseSx,
240
+ border: `1px solid ${theme.palette.divider}`,
241
+ borderRadius: 1,
242
+ backgroundColor: 'var(--theme-surface)',
243
+ };
244
+ case 'elevated':
245
+ return {
246
+ ...baseSx,
247
+ boxShadow: theme.shadows[2],
248
+ borderRadius: 1,
249
+ backgroundColor: theme.palette.background.paper,
250
+ };
251
+ case 'filled':
252
+ return {
253
+ ...baseSx,
254
+ backgroundColor: 'var(--theme-surface-variant)',
255
+ borderRadius: 1,
256
+ };
257
+ default:
258
+ return baseSx;
259
+ }
260
+ }, [variant, theme, styleProps.sx]);
261
+
262
+ // Header styles
263
+ const headerSx: SxProps<Theme> = useMemo(() => ({
264
+ ...headerSpacingConfig,
265
+ cursor: (triggerArea === 'header' || triggerArea === 'both') ? 'pointer' : 'default',
266
+ userSelect: 'none',
267
+ display: 'flex',
268
+ alignItems: 'center',
269
+ justifyContent: 'space-between',
270
+ '&:hover': (triggerArea === 'header' || triggerArea === 'both') ? {
271
+ backgroundColor: 'rgba(0, 0, 0, 0.04)',
272
+ } : {},
273
+ }), [headerSpacingConfig, triggerArea]);
274
+
275
+ // Content styles
276
+ const contentSx: SxProps<Theme> = useMemo(() => ({
277
+ ...contentSpacingConfig,
278
+ }), [contentSpacingConfig]);
279
+
280
+ // Animation props for Collapse component
281
+ const collapseProps = useMemo(() => {
282
+ if (disableAnimations) {
283
+ return { timeout: 0 };
284
+ }
285
+
286
+ const baseProps = {
287
+ timeout: animationDuration,
288
+ };
289
+
290
+ switch (animationStyle) {
291
+ case 'fade':
292
+ return {
293
+ ...baseProps,
294
+ sx: {
295
+ '& .MuiCollapse-wrapper': {
296
+ opacity: collapsed ? 0 : 1,
297
+ transition: `opacity ${animationDuration}ms ${animationConfig.easing}`,
298
+ },
299
+ },
300
+ };
301
+ case 'scale':
302
+ return {
303
+ ...baseProps,
304
+ sx: {
305
+ '& .MuiCollapse-wrapper': {
306
+ transform: collapsed ? 'scale(0.95)' : 'scale(1)',
307
+ opacity: collapsed ? 0 : 1,
308
+ transition: `all ${animationDuration}ms ${animationConfig.easing}`,
309
+ },
310
+ },
311
+ };
312
+ default: // slide
313
+ return baseProps;
314
+ }
315
+ }, [disableAnimations, animationDuration, animationStyle, animationConfig, collapsed]);
316
+
317
+ // Render content based on type
318
+ const renderContent = useCallback((content: React.ReactNode | string | undefined) => {
319
+ if (!content) return null;
320
+
321
+ if (typeof content === 'string') {
322
+ return <Html>{content}</Html>;
323
+ }
324
+
325
+ return content;
326
+ }, []);
327
+
328
+ // Container content that will be wrapped
329
+ const containerContent = (
330
+ <>
331
+ {/* Header Section */}
332
+ {(title || subtitle || leadIcon || headerActions || (triggerArea === 'button' || triggerArea === 'both')) && (
333
+ <>
334
+ <Box
335
+ id={headerId}
336
+ className={headerClassName}
337
+ sx={headerSx}
338
+ onClick={handleHeaderClick}
339
+ role={triggerArea === 'header' || triggerArea === 'both' ? 'button' : undefined}
340
+ tabIndex={triggerArea === 'header' || triggerArea === 'both' ? 0 : undefined}
341
+ aria-expanded={!collapsed}
342
+ aria-controls={contentId}
343
+ aria-describedby={ariaDescribedBy}
344
+ onKeyDown={(e) => {
345
+ if ((triggerArea === 'header' || triggerArea === 'both') && (e.key === 'Enter' || e.key === ' ')) {
346
+ e.preventDefault();
347
+ toggle();
348
+ }
349
+ }}
350
+ >
351
+ {/* Left section: Lead icon, title, subtitle */}
352
+ <Stack direction="row" spacing={2} alignItems="center" sx={{ minWidth: 0, flex: 1 }}>
353
+ {leadIcon && (
354
+ <Box sx={{ flexShrink: 0 }}>
355
+ {renderContent(leadIcon)}
356
+ </Box>
357
+ )}
358
+
359
+ {(title || subtitle) && (
360
+ <Box sx={{ minWidth: 0, flex: 1 }}>
361
+ {title && (
362
+ <Typography
363
+ variant="h6"
364
+ component="h3"
365
+ sx={{
366
+ fontWeight: 600,
367
+ lineHeight: 1.2,
368
+ ...(subtitle && { mb: 0.5 })
369
+ }}
370
+ >
371
+ {title}
372
+ </Typography>
373
+ )}
374
+ {subtitle && (
375
+ <Typography
376
+ variant="body2"
377
+ color="text.secondary"
378
+ sx={{ lineHeight: 1.3 }}
379
+ >
380
+ {subtitle}
381
+ </Typography>
382
+ )}
383
+ </Box>
384
+ )}
385
+ </Stack>
386
+
387
+ {/* Right section: Header actions and toggle button */}
388
+ <Stack direction="row" spacing={1} alignItems="center" sx={{ flexShrink: 0 }}>
389
+ {headerActions && (
390
+ <Box>
391
+ {renderContent(headerActions)}
392
+ </Box>
393
+ )}
394
+
395
+ <IconButton
396
+ onClick={handleButtonClick}
397
+ size="small"
398
+ aria-label={toggleAriaLabel}
399
+ aria-expanded={!collapsed}
400
+ aria-controls={contentId}
401
+ >
402
+ {toggleIcon}
403
+ </IconButton>
404
+ </Stack>
405
+ </Box>
406
+
407
+ {showDivider && <Divider />}
408
+ </>
409
+ )}
410
+
411
+ {/* Collapsed View (shown when collapsed) */}
412
+ {collapsed && collapsedView && (
413
+ <>
414
+ <Box
415
+ className={contentClassName}
416
+ sx={contentSx}
417
+ >
418
+ {renderContent(collapsedView)}
419
+ </Box>
420
+ {showDivider && footerView && <Divider />}
421
+ </>
422
+ )}
423
+
424
+ {/* Expanded Content (shown when not collapsed) */}
425
+ <Collapse
426
+ in={!collapsed}
427
+ {...collapseProps}
428
+ >
429
+ <Box
430
+ id={contentId}
431
+ className={contentClassName}
432
+ sx={contentSx}
433
+ role="region"
434
+ aria-labelledby={title ? headerId : undefined}
435
+ {...contentAriaProps}
436
+ >
437
+ {renderContent(children)}
438
+ </Box>
439
+ {showDivider && footerView && <Divider />}
440
+ </Collapse>
441
+
442
+ {/* Footer Section (always visible) */}
443
+ {footerView && (
444
+ <Box
445
+ className={footerClassName}
446
+ sx={contentSx}
447
+ >
448
+ {renderContent(footerView)}
449
+ </Box>
450
+ )}
451
+ </>
452
+ );
453
+
454
+ // Return appropriate container based on variant
455
+ if (variant === 'outlined') {
456
+ return (
457
+ <Paper
458
+ variant="outlined"
459
+ elevation={0}
460
+ {...htmlProps}
461
+ {...otherProps}
462
+ className={containerClassName}
463
+ sx={containerSx}
464
+ >
465
+ {containerContent}
466
+ </Paper>
467
+ );
468
+ }
469
+
470
+ if (variant === 'elevated') {
471
+ return (
472
+ <Paper
473
+ elevation={2}
474
+ {...htmlProps}
475
+ {...otherProps}
476
+ className={containerClassName}
477
+ sx={containerSx}
478
+ >
479
+ {containerContent}
480
+ </Paper>
481
+ );
482
+ }
483
+
484
+ // Default variant (default, filled)
485
+ return (
486
+ <Box
487
+ {...htmlProps}
488
+ {...otherProps}
489
+ className={containerClassName}
490
+ sx={containerSx}
491
+ >
492
+ {containerContent}
493
+ </Box>
494
+ );
495
+ }
496
+
497
+ /**
498
+ * Main CollapsibleLayout component with data binding support
499
+ */
500
+ function CollapsibleLayout(props: CollapsibleLayoutProps) {
501
+ const { dataSource, bindingOptions, ...restProps } = props;
502
+
503
+ // If no dataSource, use traditional props
504
+ if (!dataSource) {
505
+ return <CollapsibleLayoutView {...restProps} />;
506
+ }
507
+
508
+ // Use data binding
509
+ const { dataSource: _source, loading, error, cached, ...collapsibleProps } = useDataBinding<CollapsibleLayoutModel>(
510
+ dataSource,
511
+ restProps as Partial<CollapsibleLayoutModel>,
512
+ CollapsibleLayoutModel.getSchema(),
513
+ { cache: true, cacheTTL: 300000, strict: false, ...bindingOptions }
514
+ );
515
+
516
+ // Show loading state
517
+ if (loading) {
518
+ return (
519
+ <CollapsibleLayoutView
520
+ {...restProps}
521
+ title="Loading..."
522
+ variant="default"
523
+ headerSpacing="comfortable"
524
+ contentSpacing="comfortable"
525
+ />
526
+ );
527
+ }
528
+
529
+ if (error) {
530
+ console.error('Error loading collapsible layout:', error);
531
+ if (process.env.NODE_ENV !== 'production') {
532
+ return (
533
+ <CollapsibleLayoutView
534
+ {...restProps}
535
+ title="Error Loading Layout"
536
+ subtitle={error.message}
537
+ variant="outlined"
538
+ headerSpacing="comfortable"
539
+ contentSpacing="comfortable"
540
+ />
541
+ );
542
+ }
543
+ return null;
544
+ }
545
+
546
+ return <CollapsibleLayoutView {...collapsibleProps} />;
547
+ }
548
+
549
+ // Set default props
550
+ CollapsibleLayout.defaultProps = defaultCollapsibleLayoutProps;
551
+
552
+ export default CollapsibleLayout;
553
+ export { CollapsibleLayout, CollapsibleLayoutView, useCollapsibleState };
554
+ export type { CollapsibleLayoutProps, CollapsibleLayoutViewProps, UseCollapsibleLayoutState };