@qwickapps/react-framework 1.3.1 → 1.3.3

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 (43) hide show
  1. package/README.md +123 -1
  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/index.d.ts +3 -0
  10. package/dist/components/index.d.ts.map +1 -1
  11. package/dist/index.bundled.css +12 -0
  12. package/dist/index.esm.js +910 -44
  13. package/dist/index.js +916 -47
  14. package/dist/templates/TemplateResolver.d.ts.map +1 -1
  15. package/dist/utils/htmlTransform.d.ts.map +1 -1
  16. package/dist/utils/logger.d.ts +15 -3
  17. package/dist/utils/logger.d.ts.map +1 -1
  18. package/package.json +4 -2
  19. package/src/components/AccessibilityProvider.tsx +466 -0
  20. package/src/components/Breadcrumbs.tsx +223 -0
  21. package/src/components/ErrorBoundary.tsx +216 -0
  22. package/src/components/QwickApp.tsx +17 -11
  23. package/src/components/__tests__/AccessibilityProvider.test.tsx +330 -0
  24. package/src/components/__tests__/Breadcrumbs.test.tsx +268 -0
  25. package/src/components/__tests__/ErrorBoundary.test.tsx +163 -0
  26. package/src/components/index.ts +3 -0
  27. package/src/stories/AccessibilityProvider.stories.tsx +284 -0
  28. package/src/stories/Breadcrumbs.stories.tsx +304 -0
  29. package/src/stories/ErrorBoundary.stories.tsx +159 -0
  30. package/src/stories/{form/FormComponents.stories.tsx → FormComponents.stories.tsx} +8 -8
  31. package/src/templates/TemplateResolver.ts +2 -6
  32. package/src/utils/__tests__/nested-dom-fix.test.tsx +53 -0
  33. package/src/utils/__tests__/optional-logging.test.ts +83 -0
  34. package/src/utils/htmlTransform.tsx +69 -3
  35. package/src/utils/logger.ts +60 -5
  36. package/dist/schemas/Builders.d.ts +0 -7
  37. package/dist/schemas/Builders.d.ts.map +0 -1
  38. package/dist/schemas/types.d.ts +0 -7
  39. package/dist/schemas/types.d.ts.map +0 -1
  40. package/dist/types/DataBinding.d.ts +0 -7
  41. package/dist/types/DataBinding.d.ts.map +0 -1
  42. package/dist/types/DataProvider.d.ts +0 -7
  43. package/dist/types/DataProvider.d.ts.map +0 -1
@@ -0,0 +1,223 @@
1
+ import React from 'react';
2
+
3
+ export interface BreadcrumbItem {
4
+ label: string;
5
+ href?: string;
6
+ icon?: React.ReactNode;
7
+ current?: boolean;
8
+ }
9
+
10
+ export interface BreadcrumbsProps {
11
+ items: BreadcrumbItem[];
12
+ separator?: React.ReactNode;
13
+ className?: string;
14
+ onNavigate?: (item: BreadcrumbItem, index: number) => void;
15
+ maxItems?: number;
16
+ showRoot?: boolean;
17
+ }
18
+
19
+ /**
20
+ * Generic Breadcrumbs component for navigation hierarchy
21
+ *
22
+ * Features:
23
+ * - Accessible navigation with proper ARIA labels
24
+ * - Customizable separators and icons
25
+ * - Responsive design with item truncation
26
+ * - Support for custom navigation handlers
27
+ * - Keyboard navigation support
28
+ * - Screen reader friendly
29
+ */
30
+ export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
31
+ items,
32
+ separator = '/',
33
+ className = '',
34
+ onNavigate,
35
+ maxItems,
36
+ showRoot = true
37
+ }) => {
38
+ // Filter and prepare items
39
+ let displayItems = showRoot ? items : items.slice(1);
40
+
41
+ // Handle max items with ellipsis
42
+ if (maxItems && displayItems.length > maxItems) {
43
+ const firstItems = displayItems.slice(0, 1);
44
+ const lastItems = displayItems.slice(-Math.max(1, maxItems - 2));
45
+ displayItems = [
46
+ ...firstItems,
47
+ { label: '...', href: undefined, current: false },
48
+ ...lastItems
49
+ ];
50
+ }
51
+
52
+ const handleItemClick = (e: React.MouseEvent, item: BreadcrumbItem, index: number) => {
53
+ if (onNavigate) {
54
+ e.preventDefault();
55
+ onNavigate(item, index);
56
+ }
57
+ };
58
+
59
+ const handleKeyDown = (e: React.KeyboardEvent, item: BreadcrumbItem, index: number) => {
60
+ if (e.key === 'Enter' || e.key === ' ') {
61
+ e.preventDefault();
62
+ if (onNavigate) {
63
+ onNavigate(item, index);
64
+ } else if (item.href) {
65
+ window.location.href = item.href;
66
+ }
67
+ }
68
+ };
69
+
70
+ if (displayItems.length <= 1) {
71
+ return null;
72
+ }
73
+
74
+ return (
75
+ <nav
76
+ className={`breadcrumbs ${className}`}
77
+ role="navigation"
78
+ aria-label="Breadcrumb navigation"
79
+ style={{
80
+ display: 'flex',
81
+ alignItems: 'center',
82
+ fontSize: '14px',
83
+ color: '#6b7280',
84
+ ...defaultStyles.nav
85
+ }}
86
+ >
87
+ <ol
88
+ style={{
89
+ display: 'flex',
90
+ alignItems: 'center',
91
+ listStyle: 'none',
92
+ margin: 0,
93
+ padding: 0,
94
+ gap: '8px'
95
+ }}
96
+ >
97
+ {displayItems.map((item, index) => {
98
+ const isLast = index === displayItems.length - 1;
99
+ const isClickable = (item.href || onNavigate) && !item.current && item.label !== '...';
100
+
101
+ return (
102
+ <li key={`${item.label}-${index}`} style={{ display: 'flex', alignItems: 'center' }}>
103
+ {isClickable ? (
104
+ <a
105
+ href={item.href}
106
+ onClick={(e) => handleItemClick(e, item, index)}
107
+ onKeyDown={(e) => handleKeyDown(e, item, index)}
108
+ style={{
109
+ ...defaultStyles.link,
110
+ color: isLast ? '#374151' : '#6b7280',
111
+ textDecoration: 'none',
112
+ display: 'flex',
113
+ alignItems: 'center',
114
+ gap: '4px'
115
+ }}
116
+ tabIndex={0}
117
+ aria-current={item.current ? 'page' : undefined}
118
+ >
119
+ {item.icon && (
120
+ <span
121
+ style={{ display: 'flex', alignItems: 'center' }}
122
+ aria-hidden="true"
123
+ >
124
+ {item.icon}
125
+ </span>
126
+ )}
127
+ <span>{item.label}</span>
128
+ </a>
129
+ ) : (
130
+ <span
131
+ style={{
132
+ ...defaultStyles.current,
133
+ color: isLast ? '#111827' : '#6b7280',
134
+ fontWeight: isLast ? 600 : 400,
135
+ display: 'flex',
136
+ alignItems: 'center',
137
+ gap: '4px'
138
+ }}
139
+ aria-current={item.current ? 'page' : undefined}
140
+ >
141
+ {item.icon && (
142
+ <span
143
+ style={{ display: 'flex', alignItems: 'center' }}
144
+ aria-hidden="true"
145
+ >
146
+ {item.icon}
147
+ </span>
148
+ )}
149
+ <span>{item.label}</span>
150
+ </span>
151
+ )}
152
+
153
+ {!isLast && (
154
+ <span
155
+ style={{
156
+ display: 'flex',
157
+ alignItems: 'center',
158
+ marginLeft: '8px',
159
+ color: '#d1d5db',
160
+ fontSize: '12px'
161
+ }}
162
+ aria-hidden="true"
163
+ >
164
+ {separator}
165
+ </span>
166
+ )}
167
+ </li>
168
+ );
169
+ })}
170
+ </ol>
171
+ </nav>
172
+ );
173
+ };
174
+
175
+ // Default styles
176
+ const defaultStyles = {
177
+ nav: {
178
+ padding: '8px 0'
179
+ },
180
+ link: {
181
+ transition: 'color 0.2s ease',
182
+ cursor: 'pointer',
183
+ borderRadius: '4px',
184
+ padding: '4px',
185
+ margin: '-4px'
186
+ },
187
+ current: {
188
+ padding: '4px'
189
+ }
190
+ } as const;
191
+
192
+ /**
193
+ * Hook for managing breadcrumb state
194
+ */
195
+ export const useBreadcrumbs = () => {
196
+ const [breadcrumbs, setBreadcrumbs] = React.useState<BreadcrumbItem[]>([]);
197
+
198
+ const addBreadcrumb = React.useCallback((item: BreadcrumbItem) => {
199
+ setBreadcrumbs(prev => [...prev, item]);
200
+ }, []);
201
+
202
+ const removeBreadcrumb = React.useCallback((index: number) => {
203
+ setBreadcrumbs(prev => prev.filter((_, i) => i !== index));
204
+ }, []);
205
+
206
+ const setBreadcrumbsCurrent = React.useCallback((items: BreadcrumbItem[]) => {
207
+ setBreadcrumbs(items);
208
+ }, []);
209
+
210
+ const clearBreadcrumbs = React.useCallback(() => {
211
+ setBreadcrumbs([]);
212
+ }, []);
213
+
214
+ return {
215
+ breadcrumbs,
216
+ addBreadcrumb,
217
+ removeBreadcrumb,
218
+ setBreadcrumbs: setBreadcrumbsCurrent,
219
+ clearBreadcrumbs
220
+ };
221
+ };
222
+
223
+ export default Breadcrumbs;
@@ -0,0 +1,216 @@
1
+ import React, { Component, ErrorInfo, ReactNode } from 'react';
2
+ import { Button } from './buttons/Button';
3
+
4
+ interface Props {
5
+ children: ReactNode;
6
+ fallback?: ReactNode;
7
+ onError?: (error: Error, errorInfo: ErrorInfo) => void;
8
+ showErrorDetails?: boolean;
9
+ }
10
+
11
+ interface State {
12
+ hasError: boolean;
13
+ error: Error | null;
14
+ errorInfo: ErrorInfo | null;
15
+ }
16
+
17
+ /**
18
+ * Generic ErrorBoundary component for catching and handling React errors
19
+ *
20
+ * Features:
21
+ * - Catches JavaScript errors anywhere in child component tree
22
+ * - Displays fallback UI with retry functionality
23
+ * - Shows error details in development mode
24
+ * - Customizable error handling and fallback UI
25
+ * - Automatic error logging
26
+ */
27
+ export class ErrorBoundary extends Component<Props, State> {
28
+ constructor(props: Props) {
29
+ super(props);
30
+ this.state = {
31
+ hasError: false,
32
+ error: null,
33
+ errorInfo: null
34
+ };
35
+ }
36
+
37
+ static getDerivedStateFromError(error: Error): State {
38
+ // Update state so the next render will show the fallback UI
39
+ return {
40
+ hasError: true,
41
+ error,
42
+ errorInfo: null
43
+ };
44
+ }
45
+
46
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
47
+ // Log error details
48
+ this.setState({
49
+ error,
50
+ errorInfo
51
+ });
52
+
53
+ // Log to console for debugging
54
+ console.error('ErrorBoundary caught an error:', error, errorInfo);
55
+
56
+ // Custom error handler
57
+ if (this.props.onError) {
58
+ this.props.onError(error, errorInfo);
59
+ }
60
+
61
+ // Send error to logging service if available
62
+ if (typeof window !== 'undefined') {
63
+ // @ts-ignore - Global error logging service
64
+ if (window.qwickapps?.logError) {
65
+ window.qwickapps.logError(error, errorInfo);
66
+ }
67
+ }
68
+ }
69
+
70
+ handleRetry = () => {
71
+ this.setState({
72
+ hasError: false,
73
+ error: null,
74
+ errorInfo: null
75
+ });
76
+ };
77
+
78
+ handleRefresh = () => {
79
+ if (typeof window !== 'undefined') {
80
+ window.location.reload();
81
+ }
82
+ };
83
+
84
+ render() {
85
+ if (this.state.hasError) {
86
+ // Custom fallback UI
87
+ if (this.props.fallback) {
88
+ return this.props.fallback;
89
+ }
90
+
91
+ // Default error UI
92
+ return (
93
+ <div
94
+ className="error-boundary"
95
+ role="alert"
96
+ style={{
97
+ padding: '2rem',
98
+ textAlign: 'center',
99
+ backgroundColor: '#fef2f2',
100
+ border: '1px solid #fecaca',
101
+ borderRadius: '8px',
102
+ margin: '1rem',
103
+ color: '#991b1b'
104
+ }}
105
+ >
106
+ <div style={{ marginBottom: '1.5rem' }}>
107
+ <h2 style={{
108
+ fontSize: '1.5rem',
109
+ fontWeight: 'bold',
110
+ marginBottom: '0.5rem',
111
+ color: '#991b1b'
112
+ }}>
113
+ Something went wrong
114
+ </h2>
115
+ <p style={{
116
+ color: '#7f1d1d',
117
+ marginBottom: '1rem'
118
+ }}>
119
+ An unexpected error occurred in the application. Please try again or refresh the page.
120
+ </p>
121
+ </div>
122
+
123
+ <div style={{
124
+ display: 'flex',
125
+ gap: '0.75rem',
126
+ justifyContent: 'center',
127
+ marginBottom: '1rem'
128
+ }}>
129
+ <Button
130
+ variant="contained"
131
+ onClick={this.handleRetry}
132
+ style={{
133
+ backgroundColor: '#dc2626',
134
+ color: 'white'
135
+ }}
136
+ >
137
+ Try Again
138
+ </Button>
139
+
140
+ <Button
141
+ variant="outlined"
142
+ onClick={this.handleRefresh}
143
+ style={{
144
+ borderColor: '#dc2626',
145
+ color: '#dc2626'
146
+ }}
147
+ >
148
+ Refresh Page
149
+ </Button>
150
+ </div>
151
+
152
+ {/* Show error details in development or when explicitly enabled */}
153
+ {(process.env.NODE_ENV === 'development' || this.props.showErrorDetails) && this.state.error && (
154
+ <details
155
+ style={{
156
+ textAlign: 'left',
157
+ marginTop: '1rem',
158
+ padding: '1rem',
159
+ backgroundColor: '#f9fafb',
160
+ border: '1px solid #d1d5db',
161
+ borderRadius: '6px'
162
+ }}
163
+ >
164
+ <summary style={{
165
+ cursor: 'pointer',
166
+ fontWeight: 'bold',
167
+ marginBottom: '0.5rem',
168
+ color: '#374151'
169
+ }}>
170
+ Error Details (Development Mode)
171
+ </summary>
172
+ <pre style={{
173
+ fontSize: '0.75rem',
174
+ color: '#374151',
175
+ whiteSpace: 'pre-wrap',
176
+ overflow: 'auto'
177
+ }}>
178
+ {this.state.error.toString()}
179
+ {this.state.errorInfo?.componentStack && (
180
+ <>
181
+ <br />
182
+ <br />
183
+ Component Stack:
184
+ {this.state.errorInfo.componentStack}
185
+ </>
186
+ )}
187
+ </pre>
188
+ </details>
189
+ )}
190
+ </div>
191
+ );
192
+ }
193
+
194
+ return this.props.children;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Higher-order component that wraps a component with ErrorBoundary
200
+ */
201
+ export function withErrorBoundary<P extends object>(
202
+ WrappedComponent: React.ComponentType<P>,
203
+ errorBoundaryProps?: Omit<Props, 'children'>
204
+ ) {
205
+ const WithErrorBoundaryComponent = (props: P) => (
206
+ <ErrorBoundary {...errorBoundaryProps}>
207
+ <WrappedComponent {...props} />
208
+ </ErrorBoundary>
209
+ );
210
+
211
+ WithErrorBoundaryComponent.displayName = `withErrorBoundary(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;
212
+
213
+ return WithErrorBoundaryComponent;
214
+ }
215
+
216
+ export default ErrorBoundary;
@@ -35,6 +35,8 @@ import { QwickAppContext, type QwickAppContextValue, type QwickAppProps } from '
35
35
  import { type TemplateResolverConfig } from '../types';
36
36
  import './QwickApp.css';
37
37
  import Scaffold from './Scaffold';
38
+ import { ErrorBoundary } from './ErrorBoundary';
39
+ import { AccessibilityProvider } from './AccessibilityProvider';
38
40
  // Auth logic moved to AuthProvider - QwickApp now focuses on app infrastructure
39
41
 
40
42
  // RouteConfig moved to AuthProvider for auth-specific routing
@@ -125,17 +127,21 @@ export const QwickApp: React.FC<QwickAppComponentProps> = ({
125
127
  ) : content;
126
128
 
127
129
  const appContent = (
128
- <div className={`qwick-app ${className || ''}`} style={style}>
129
- <ThemeProvider
130
- appId={appId}
131
- defaultTheme={defaultTheme}
132
- defaultPalette={defaultPalette}
133
- >
134
- <QwickAppContext.Provider value={contextValue}>
135
- {wrappedContent}
136
- </QwickAppContext.Provider>
137
- </ThemeProvider>
138
- </div>
130
+ <ErrorBoundary>
131
+ <AccessibilityProvider>
132
+ <div className={`qwick-app ${className || ''}`} style={style}>
133
+ <ThemeProvider
134
+ appId={appId}
135
+ defaultTheme={defaultTheme}
136
+ defaultPalette={defaultPalette}
137
+ >
138
+ <QwickAppContext.Provider value={contextValue}>
139
+ {wrappedContent}
140
+ </QwickAppContext.Provider>
141
+ </ThemeProvider>
142
+ </div>
143
+ </AccessibilityProvider>
144
+ </ErrorBoundary>
139
145
  );
140
146
 
141
147
  // If router is provided, wrap the entire app with it