@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
package/dist/index.esm.js CHANGED
@@ -1,8 +1,6 @@
1
1
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
2
2
  import { CircularProgress, Button as Button$1, Paper, Typography, useTheme as useTheme$1, Box, Container as Container$8, Stack, Tooltip, IconButton, Snackbar, Alert, useMediaQuery, Avatar, Chip, Menu, MenuItem, Grid, Divider, Link, ListItemIcon, ListItemText, Card, CardContent, ButtonGroup, Collapse, TextField as TextField$1, FormControl, InputLabel, Select, FormHelperText } from '@mui/material';
3
- import React, { useMemo, useContext, useState, useEffect, createContext, useRef, useCallback, cloneElement } from 'react';
4
- import { Logger, createLogger } from '@qwickapps/logging';
5
- export * from '@qwickapps/logging';
3
+ import React, { useMemo, useContext, useState, useEffect, createContext, useRef, useCallback, Component, useReducer, cloneElement } from 'react';
6
4
  import { MustacheTemplateProvider, MemoryCacheProvider, CachedDataProvider, Field, Editor, FieldType, Schema, Model, DataType } from '@qwickapps/schema';
7
5
  import { IsOptional, IsString, IsIn, IsUrl, IsArray, ValidateNested, IsNumber, Min, Max, IsBoolean, IsNotEmpty, IsObject } from 'class-validator';
8
6
  import { Type } from 'class-transformer';
@@ -468,6 +466,63 @@ function useBaseProps(props) {
468
466
  };
469
467
  }
470
468
 
469
+ /**
470
+ * QwickApps React Framework - Logger Utility
471
+ *
472
+ * Optional logging with fallback to console when @qwickapps/logging is not available
473
+ *
474
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
475
+ */
476
+ /**
477
+ * Console-based logger fallback
478
+ */
479
+ class ConsoleLogger {
480
+ constructor(name) {
481
+ this.name = name;
482
+ }
483
+ debug(message, ...args) {
484
+ {
485
+ console.debug(`[${this.name}] ${message}`, ...args);
486
+ }
487
+ }
488
+ info(message, ...args) {
489
+ console.info(`[${this.name}] ${message}`, ...args);
490
+ }
491
+ warn(message, ...args) {
492
+ console.warn(`[${this.name}] ${message}`, ...args);
493
+ }
494
+ error(message, ...args) {
495
+ console.error(`[${this.name}] ${message}`, ...args);
496
+ }
497
+ }
498
+ /**
499
+ * Create logger with optional @qwickapps/logging dependency
500
+ */
501
+ function createLogger(name) {
502
+ try {
503
+ // Try to use @qwickapps/logging if available
504
+ const logging = require('@qwickapps/logging');
505
+ return logging.createLogger(name);
506
+ } catch {
507
+ // Fallback to console-based logger
508
+ return new ConsoleLogger(name);
509
+ }
510
+ }
511
+ /**
512
+ * Framework-specific loggers
513
+ */
514
+ const loggers = {
515
+ scaffold: createLogger('Scaffold'),
516
+ navigation: createLogger('Navigation'),
517
+ auth: createLogger('Auth'),
518
+ theme: createLogger('Theme'),
519
+ palette: createLogger('Palette'),
520
+ form: createLogger('Form'),
521
+ layout: createLogger('Layout'),
522
+ menu: createLogger('Menu'),
523
+ router: createLogger('Router')
524
+ };
525
+
471
526
  /**
472
527
  * ContentResolver provides a unified interface to fetch and render content
473
528
  * using data providers, template resolvers, and optional caching.
@@ -506,10 +561,7 @@ function useBaseProps(props) {
506
561
  */
507
562
  class TemplateResolver {
508
563
  constructor(config) {
509
- this.log = new Logger({
510
- namespace: 'ContentResolver',
511
- enabled: config.enableLogging || false
512
- });
564
+ this.log = createLogger('ContentResolver');
513
565
  this.enableLogging = config.enableLogging || false;
514
566
  this.templateResolver = config.templateResolver || new MustacheTemplateProvider();
515
567
  if (config.cacheProvider === true) {
@@ -13121,6 +13173,22 @@ const defaultMarkdownRules = [
13121
13173
  return null;
13122
13174
  }
13123
13175
  }];
13176
+ /**
13177
+ * Parse style string into React style object
13178
+ */
13179
+ function parseStyleString(styleStr) {
13180
+ const styles = {};
13181
+ if (!styleStr) return styles;
13182
+ styleStr.split(';').forEach(declaration => {
13183
+ const [property, value] = declaration.split(':').map(s => s.trim());
13184
+ if (property && value) {
13185
+ // Convert kebab-case to camelCase
13186
+ const camelProperty = property.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
13187
+ styles[camelProperty] = value;
13188
+ }
13189
+ });
13190
+ return styles;
13191
+ }
13124
13192
  /**
13125
13193
  * Default fallback component - renders element as-is with SafeSpan content
13126
13194
  */
@@ -13128,6 +13196,9 @@ const defaultFallback = (element, key) => {
13128
13196
  const tagName = element.tagName.toLowerCase();
13129
13197
  // Check if this is a void element (self-closing)
13130
13198
  const voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
13199
+ // Handle style attribute separately to convert to object
13200
+ const styleAttr = element.getAttribute('style');
13201
+ const style = styleAttr ? parseStyleString(styleAttr) : undefined;
13131
13202
  const props = {
13132
13203
  key,
13133
13204
  ...(element.className ? {
@@ -13136,7 +13207,10 @@ const defaultFallback = (element, key) => {
13136
13207
  ...(element.id ? {
13137
13208
  id: element.id
13138
13209
  } : {}),
13139
- ...Object.fromEntries(Array.from(element.attributes).filter(attr => !['class', 'id'].includes(attr.name)).map(attr => [attr.name, attr.value]))
13210
+ ...(style ? {
13211
+ style
13212
+ } : {}),
13213
+ ...Object.fromEntries(Array.from(element.attributes).filter(attr => !['class', 'id', 'style'].includes(attr.name)).map(attr => [attr.name, attr.value]))
13140
13214
  };
13141
13215
  if (voidElements.has(tagName)) {
13142
13216
  // Void elements can't have children
@@ -13181,6 +13255,9 @@ function transformElement(element, key, config = {
13181
13255
  const children = Array.from(element.children);
13182
13256
  const tagName = element.tagName.toLowerCase();
13183
13257
  const transformedChildren = children.map((child, index) => transformElement(child, `${key}-${index}`, config));
13258
+ // Handle style attribute separately to convert to object
13259
+ const styleAttr = element.getAttribute('style');
13260
+ const style = styleAttr ? parseStyleString(styleAttr) : undefined;
13184
13261
  return /*#__PURE__*/React.createElement(tagName, {
13185
13262
  key,
13186
13263
  ...(element.className ? {
@@ -13189,12 +13266,39 @@ function transformElement(element, key, config = {
13189
13266
  ...(element.id ? {
13190
13267
  id: element.id
13191
13268
  } : {}),
13192
- ...Object.fromEntries(Array.from(element.attributes).filter(attr => !['class', 'id'].includes(attr.name)).map(attr => [attr.name, attr.value]))
13269
+ ...(style ? {
13270
+ style
13271
+ } : {}),
13272
+ ...Object.fromEntries(Array.from(element.attributes).filter(attr => !['class', 'id', 'style'].includes(attr.name)).map(attr => [attr.name, attr.value]))
13193
13273
  }, ...transformedChildren);
13194
13274
  }
13195
13275
  // No transformation needed - use fallback
13196
13276
  return fallbackComponent(element, key);
13197
13277
  }
13278
+ /**
13279
+ * Clean up invalid DOM nesting that can cause React errors
13280
+ */
13281
+ function cleanupInvalidNesting(html) {
13282
+ const parser = new DOMParser();
13283
+ const doc = parser.parseFromString(html, 'text/html');
13284
+ // Find paragraphs that contain block-level elements (invalid nesting)
13285
+ const paragraphs = doc.querySelectorAll('p');
13286
+ const blockElements = ['div', 'section', 'article', 'aside', 'header', 'footer', 'nav', 'main', 'figure', 'blockquote', 'pre', 'table', 'form', 'fieldset', 'address'];
13287
+ paragraphs.forEach(p => {
13288
+ const hasBlockChildren = Array.from(p.children).some(child => blockElements.includes(child.tagName.toLowerCase()));
13289
+ if (hasBlockChildren) {
13290
+ // Convert paragraph to div to allow block children
13291
+ const div = doc.createElement('div');
13292
+ div.innerHTML = p.innerHTML;
13293
+ // Copy attributes
13294
+ Array.from(p.attributes).forEach(attr => {
13295
+ div.setAttribute(attr.name, attr.value);
13296
+ });
13297
+ p.parentNode?.replaceChild(div, p);
13298
+ }
13299
+ });
13300
+ return doc.body.innerHTML;
13301
+ }
13198
13302
  /**
13199
13303
  * Transform HTML string to React components
13200
13304
  */
@@ -13202,8 +13306,10 @@ function transformHtmlToReact(html, config = {
13202
13306
  rules: defaultArticleRules
13203
13307
  }) {
13204
13308
  if (!html.trim()) return [];
13309
+ // Clean up invalid DOM nesting first
13310
+ const cleanHtml = cleanupInvalidNesting(html);
13205
13311
  const parser = new DOMParser();
13206
- const doc = parser.parseFromString(html, 'text/html');
13312
+ const doc = parser.parseFromString(cleanHtml, 'text/html');
13207
13313
  return Array.from(doc.body.children).map((element, index) => transformElement(element, index.toString(), config));
13208
13314
  }
13209
13315
  /**
@@ -20128,29 +20234,6 @@ const FormPage = ({
20128
20234
  });
20129
20235
  };
20130
20236
 
20131
- /**
20132
- * QwickApps React Framework - Logger Utility
20133
- *
20134
- * Re-exports from @qwickapps/logging with framework-specific loggers
20135
- *
20136
- * Copyright (c) 2025 QwickApps.com. All rights reserved.
20137
- */
20138
- // Re-export everything from the logging package
20139
- /**
20140
- * Framework-specific loggers
20141
- */
20142
- const loggers = {
20143
- scaffold: createLogger('Scaffold'),
20144
- navigation: createLogger('Navigation'),
20145
- auth: createLogger('Auth'),
20146
- theme: createLogger('Theme'),
20147
- palette: createLogger('Palette'),
20148
- form: createLogger('Form'),
20149
- layout: createLogger('Layout'),
20150
- menu: createLogger('Menu'),
20151
- router: createLogger('Router')
20152
- };
20153
-
20154
20237
  const logger = loggers.scaffold;
20155
20238
  // Material UI breakpoints
20156
20239
  const BREAKPOINTS = {
@@ -20496,6 +20579,605 @@ const Scaffold = ({
20496
20579
 
20497
20580
  loggers.menu;
20498
20581
 
20582
+ /**
20583
+ * Generic ErrorBoundary component for catching and handling React errors
20584
+ *
20585
+ * Features:
20586
+ * - Catches JavaScript errors anywhere in child component tree
20587
+ * - Displays fallback UI with retry functionality
20588
+ * - Shows error details in development mode
20589
+ * - Customizable error handling and fallback UI
20590
+ * - Automatic error logging
20591
+ */
20592
+ class ErrorBoundary extends Component {
20593
+ constructor(props) {
20594
+ super(props);
20595
+ this.handleRetry = () => {
20596
+ this.setState({
20597
+ hasError: false,
20598
+ error: null,
20599
+ errorInfo: null
20600
+ });
20601
+ };
20602
+ this.handleRefresh = () => {
20603
+ if (typeof window !== 'undefined') {
20604
+ window.location.reload();
20605
+ }
20606
+ };
20607
+ this.state = {
20608
+ hasError: false,
20609
+ error: null,
20610
+ errorInfo: null
20611
+ };
20612
+ }
20613
+ static getDerivedStateFromError(error) {
20614
+ // Update state so the next render will show the fallback UI
20615
+ return {
20616
+ hasError: true,
20617
+ error,
20618
+ errorInfo: null
20619
+ };
20620
+ }
20621
+ componentDidCatch(error, errorInfo) {
20622
+ // Log error details
20623
+ this.setState({
20624
+ error,
20625
+ errorInfo
20626
+ });
20627
+ // Log to console for debugging
20628
+ console.error('ErrorBoundary caught an error:', error, errorInfo);
20629
+ // Custom error handler
20630
+ if (this.props.onError) {
20631
+ this.props.onError(error, errorInfo);
20632
+ }
20633
+ // Send error to logging service if available
20634
+ if (typeof window !== 'undefined') {
20635
+ // @ts-ignore - Global error logging service
20636
+ if (window.qwickapps?.logError) {
20637
+ window.qwickapps.logError(error, errorInfo);
20638
+ }
20639
+ }
20640
+ }
20641
+ render() {
20642
+ if (this.state.hasError) {
20643
+ // Custom fallback UI
20644
+ if (this.props.fallback) {
20645
+ return this.props.fallback;
20646
+ }
20647
+ // Default error UI
20648
+ return jsxs("div", {
20649
+ className: "error-boundary",
20650
+ role: "alert",
20651
+ style: {
20652
+ padding: '2rem',
20653
+ textAlign: 'center',
20654
+ backgroundColor: '#fef2f2',
20655
+ border: '1px solid #fecaca',
20656
+ borderRadius: '8px',
20657
+ margin: '1rem',
20658
+ color: '#991b1b'
20659
+ },
20660
+ children: [jsxs("div", {
20661
+ style: {
20662
+ marginBottom: '1.5rem'
20663
+ },
20664
+ children: [jsx("h2", {
20665
+ style: {
20666
+ fontSize: '1.5rem',
20667
+ fontWeight: 'bold',
20668
+ marginBottom: '0.5rem',
20669
+ color: '#991b1b'
20670
+ },
20671
+ children: "Something went wrong"
20672
+ }), jsx("p", {
20673
+ style: {
20674
+ color: '#7f1d1d',
20675
+ marginBottom: '1rem'
20676
+ },
20677
+ children: "An unexpected error occurred in the application. Please try again or refresh the page."
20678
+ })]
20679
+ }), jsxs("div", {
20680
+ style: {
20681
+ display: 'flex',
20682
+ gap: '0.75rem',
20683
+ justifyContent: 'center',
20684
+ marginBottom: '1rem'
20685
+ },
20686
+ children: [jsx(Button, {
20687
+ variant: "contained",
20688
+ onClick: this.handleRetry,
20689
+ style: {
20690
+ backgroundColor: '#dc2626',
20691
+ color: 'white'
20692
+ },
20693
+ children: "Try Again"
20694
+ }), jsx(Button, {
20695
+ variant: "outlined",
20696
+ onClick: this.handleRefresh,
20697
+ style: {
20698
+ borderColor: '#dc2626',
20699
+ color: '#dc2626'
20700
+ },
20701
+ children: "Refresh Page"
20702
+ })]
20703
+ }), this.state.error && jsxs("details", {
20704
+ style: {
20705
+ textAlign: 'left',
20706
+ marginTop: '1rem',
20707
+ padding: '1rem',
20708
+ backgroundColor: '#f9fafb',
20709
+ border: '1px solid #d1d5db',
20710
+ borderRadius: '6px'
20711
+ },
20712
+ children: [jsx("summary", {
20713
+ style: {
20714
+ cursor: 'pointer',
20715
+ fontWeight: 'bold',
20716
+ marginBottom: '0.5rem',
20717
+ color: '#374151'
20718
+ },
20719
+ children: "Error Details (Development Mode)"
20720
+ }), jsxs("pre", {
20721
+ style: {
20722
+ fontSize: '0.75rem',
20723
+ color: '#374151',
20724
+ whiteSpace: 'pre-wrap',
20725
+ overflow: 'auto'
20726
+ },
20727
+ children: [this.state.error.toString(), this.state.errorInfo?.componentStack && jsxs(Fragment, {
20728
+ children: [jsx("br", {}), jsx("br", {}), "Component Stack:", this.state.errorInfo.componentStack]
20729
+ })]
20730
+ })]
20731
+ })]
20732
+ });
20733
+ }
20734
+ return this.props.children;
20735
+ }
20736
+ }
20737
+ /**
20738
+ * Higher-order component that wraps a component with ErrorBoundary
20739
+ */
20740
+ function withErrorBoundary(WrappedComponent, errorBoundaryProps) {
20741
+ const WithErrorBoundaryComponent = props => jsx(ErrorBoundary, {
20742
+ ...errorBoundaryProps,
20743
+ children: jsx(WrappedComponent, {
20744
+ ...props
20745
+ })
20746
+ });
20747
+ WithErrorBoundaryComponent.displayName = `withErrorBoundary(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;
20748
+ return WithErrorBoundaryComponent;
20749
+ }
20750
+
20751
+ const AccessibilityContext = /*#__PURE__*/createContext(null);
20752
+ // Reducer
20753
+ const accessibilityReducer = (state, action) => {
20754
+ switch (action.type) {
20755
+ case 'SET_HIGH_CONTRAST':
20756
+ return {
20757
+ ...state,
20758
+ highContrast: action.payload
20759
+ };
20760
+ case 'SET_REDUCED_MOTION':
20761
+ return {
20762
+ ...state,
20763
+ reducedMotion: action.payload
20764
+ };
20765
+ case 'SET_LARGE_TEXT':
20766
+ return {
20767
+ ...state,
20768
+ largeText: action.payload
20769
+ };
20770
+ case 'SET_FOCUS_VISIBLE':
20771
+ return {
20772
+ ...state,
20773
+ focusVisible: action.payload
20774
+ };
20775
+ case 'SET_KEYBOARD_USER':
20776
+ return {
20777
+ ...state,
20778
+ isKeyboardUser: action.payload
20779
+ };
20780
+ case 'ADD_ISSUE':
20781
+ return {
20782
+ ...state,
20783
+ issues: [...state.issues, action.payload]
20784
+ };
20785
+ case 'CLEAR_ISSUES':
20786
+ return {
20787
+ ...state,
20788
+ issues: []
20789
+ };
20790
+ case 'SET_ANNOUNCEMENT':
20791
+ return {
20792
+ ...state,
20793
+ lastAnnouncement: action.payload
20794
+ };
20795
+ default:
20796
+ return state;
20797
+ }
20798
+ };
20799
+ // Initial state
20800
+ const initialState = {
20801
+ highContrast: false,
20802
+ reducedMotion: false,
20803
+ largeText: false,
20804
+ focusVisible: true,
20805
+ isKeyboardUser: false,
20806
+ issues: [],
20807
+ lastAnnouncement: null,
20808
+ preferences: {}
20809
+ };
20810
+ // ARIA Live Manager
20811
+ class AriaLiveManager {
20812
+ constructor() {
20813
+ this.politeRegion = null;
20814
+ this.assertiveRegion = null;
20815
+ this.createLiveRegions();
20816
+ }
20817
+ createLiveRegions() {
20818
+ if (typeof document === 'undefined') return;
20819
+ // Polite announcements
20820
+ this.politeRegion = document.createElement('div');
20821
+ this.politeRegion.setAttribute('aria-live', 'polite');
20822
+ this.politeRegion.setAttribute('aria-atomic', 'true');
20823
+ this.politeRegion.setAttribute('id', 'qwickapps-aria-live-polite');
20824
+ this.politeRegion.style.cssText = `
20825
+ position: absolute !important;
20826
+ left: -10000px !important;
20827
+ width: 1px !important;
20828
+ height: 1px !important;
20829
+ overflow: hidden !important;
20830
+ `;
20831
+ document.body.appendChild(this.politeRegion);
20832
+ // Assertive announcements
20833
+ this.assertiveRegion = document.createElement('div');
20834
+ this.assertiveRegion.setAttribute('aria-live', 'assertive');
20835
+ this.assertiveRegion.setAttribute('aria-atomic', 'true');
20836
+ this.assertiveRegion.setAttribute('id', 'qwickapps-aria-live-assertive');
20837
+ this.assertiveRegion.style.cssText = `
20838
+ position: absolute !important;
20839
+ left: -10000px !important;
20840
+ width: 1px !important;
20841
+ height: 1px !important;
20842
+ overflow: hidden !important;
20843
+ `;
20844
+ document.body.appendChild(this.assertiveRegion);
20845
+ }
20846
+ announce(message, level = 'polite') {
20847
+ if (level === 'assertive') {
20848
+ this.announceAssertive(message);
20849
+ } else {
20850
+ this.announcePolite(message);
20851
+ }
20852
+ }
20853
+ announcePolite(message) {
20854
+ if (!this.politeRegion) return;
20855
+ this.politeRegion.textContent = '';
20856
+ // Small delay ensures screen readers detect the change
20857
+ setTimeout(() => {
20858
+ if (this.politeRegion) {
20859
+ this.politeRegion.textContent = message;
20860
+ }
20861
+ }, 100);
20862
+ }
20863
+ announceAssertive(message) {
20864
+ if (!this.assertiveRegion) return;
20865
+ this.assertiveRegion.textContent = '';
20866
+ // Small delay ensures screen readers detect the change
20867
+ setTimeout(() => {
20868
+ if (this.assertiveRegion) {
20869
+ this.assertiveRegion.textContent = message;
20870
+ }
20871
+ }, 100);
20872
+ }
20873
+ }
20874
+ const ariaLiveManager = new AriaLiveManager();
20875
+ /**
20876
+ * Accessibility Provider Component
20877
+ * Provides comprehensive accessibility context and utilities
20878
+ *
20879
+ * Features:
20880
+ * - System preference detection (high contrast, reduced motion)
20881
+ * - Keyboard navigation detection
20882
+ * - ARIA live announcements
20883
+ * - Focus management
20884
+ * - Accessibility auditing
20885
+ * - Settings persistence
20886
+ */
20887
+ const AccessibilityProvider = ({
20888
+ children,
20889
+ enableAudit = "development" === 'development'
20890
+ }) => {
20891
+ const [state, dispatch] = useReducer(accessibilityReducer, initialState);
20892
+ useEffect(() => {
20893
+ // Detect user preferences from system
20894
+ detectUserPreferences();
20895
+ // Set up keyboard detection
20896
+ const keyboardCleanup = setupKeyboardDetection();
20897
+ // Initialize focus management
20898
+ initializeFocusManagement();
20899
+ // Run initial accessibility audit
20900
+ if (enableAudit) {
20901
+ runAccessibilityAudit();
20902
+ }
20903
+ // Cleanup
20904
+ return () => {
20905
+ if (keyboardCleanup) keyboardCleanup();
20906
+ };
20907
+ }, [enableAudit]);
20908
+ const detectUserPreferences = () => {
20909
+ if (typeof window === 'undefined') return;
20910
+ // High contrast mode
20911
+ if (window.matchMedia && window.matchMedia('(prefers-contrast: high)').matches) {
20912
+ dispatch({
20913
+ type: 'SET_HIGH_CONTRAST',
20914
+ payload: true
20915
+ });
20916
+ }
20917
+ // Reduced motion
20918
+ if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
20919
+ dispatch({
20920
+ type: 'SET_REDUCED_MOTION',
20921
+ payload: true
20922
+ });
20923
+ }
20924
+ // Listen for changes
20925
+ if (window.matchMedia) {
20926
+ const contrastMedia = window.matchMedia('(prefers-contrast: high)');
20927
+ const motionMedia = window.matchMedia('(prefers-reduced-motion: reduce)');
20928
+ const contrastHandler = e => {
20929
+ dispatch({
20930
+ type: 'SET_HIGH_CONTRAST',
20931
+ payload: e.matches
20932
+ });
20933
+ };
20934
+ const motionHandler = e => {
20935
+ dispatch({
20936
+ type: 'SET_REDUCED_MOTION',
20937
+ payload: e.matches
20938
+ });
20939
+ };
20940
+ contrastMedia.addEventListener('change', contrastHandler);
20941
+ motionMedia.addEventListener('change', motionHandler);
20942
+ // Return cleanup function
20943
+ return () => {
20944
+ contrastMedia.removeEventListener('change', contrastHandler);
20945
+ motionMedia.removeEventListener('change', motionHandler);
20946
+ };
20947
+ }
20948
+ };
20949
+ const setupKeyboardDetection = () => {
20950
+ if (typeof document === 'undefined') return;
20951
+ let keyboardUser = false;
20952
+ const handleKeyDown = e => {
20953
+ if (e.key === 'Tab') {
20954
+ keyboardUser = true;
20955
+ dispatch({
20956
+ type: 'SET_KEYBOARD_USER',
20957
+ payload: true
20958
+ });
20959
+ document.body.classList.add('keyboard-user');
20960
+ }
20961
+ };
20962
+ const handleMouseDown = () => {
20963
+ if (keyboardUser) {
20964
+ keyboardUser = false;
20965
+ dispatch({
20966
+ type: 'SET_KEYBOARD_USER',
20967
+ payload: false
20968
+ });
20969
+ document.body.classList.remove('keyboard-user');
20970
+ }
20971
+ };
20972
+ document.addEventListener('keydown', handleKeyDown);
20973
+ document.addEventListener('mousedown', handleMouseDown);
20974
+ return () => {
20975
+ document.removeEventListener('keydown', handleKeyDown);
20976
+ document.removeEventListener('mousedown', handleMouseDown);
20977
+ };
20978
+ };
20979
+ const initializeFocusManagement = () => {
20980
+ if (typeof document === 'undefined') return;
20981
+ // Enhanced focus indicators for keyboard users
20982
+ const style = document.createElement('style');
20983
+ style.textContent = `
20984
+ .keyboard-user *:focus {
20985
+ outline: 3px solid #005cee !important;
20986
+ outline-offset: 2px !important;
20987
+ }
20988
+
20989
+ .high-contrast *:focus {
20990
+ outline: 3px solid #ffffff !important;
20991
+ outline-offset: 2px !important;
20992
+ box-shadow: 0 0 0 1px #000000 !important;
20993
+ }
20994
+
20995
+ .reduced-motion * {
20996
+ animation-duration: 0.01ms !important;
20997
+ animation-iteration-count: 1 !important;
20998
+ transition-duration: 0.01ms !important;
20999
+ }
21000
+
21001
+ .large-text {
21002
+ font-size: 1.2em !important;
21003
+ }
21004
+ `;
21005
+ document.head.appendChild(style);
21006
+ };
21007
+ const runAccessibilityAudit = () => {
21008
+ if (typeof document === 'undefined') return;
21009
+ setTimeout(() => {
21010
+ const issues = [];
21011
+ // Check for images without alt text
21012
+ const images = document.querySelectorAll('img:not([alt])');
21013
+ images.forEach(img => {
21014
+ issues.push({
21015
+ type: 'missing-alt-text',
21016
+ message: 'Image missing alt attribute',
21017
+ level: 'error',
21018
+ element: img
21019
+ });
21020
+ });
21021
+ // Check for buttons without accessible names
21022
+ const buttons = document.querySelectorAll('button:not([aria-label]):not([title])');
21023
+ buttons.forEach(button => {
21024
+ if (!button.textContent?.trim()) {
21025
+ issues.push({
21026
+ type: 'unnamed-button',
21027
+ message: 'Button missing accessible name',
21028
+ level: 'error',
21029
+ element: button
21030
+ });
21031
+ }
21032
+ });
21033
+ // Check for form inputs without labels
21034
+ const inputs = document.querySelectorAll('input:not([aria-label]):not([title])');
21035
+ inputs.forEach(input => {
21036
+ const id = input.getAttribute('id');
21037
+ if (id) {
21038
+ const label = document.querySelector(`label[for="${id}"]`);
21039
+ if (!label) {
21040
+ issues.push({
21041
+ type: 'unlabeled-input',
21042
+ message: 'Form input missing label',
21043
+ level: 'error',
21044
+ element: input
21045
+ });
21046
+ }
21047
+ } else {
21048
+ issues.push({
21049
+ type: 'unlabeled-input',
21050
+ message: 'Form input missing label',
21051
+ level: 'error',
21052
+ element: input
21053
+ });
21054
+ }
21055
+ });
21056
+ dispatch({
21057
+ type: 'CLEAR_ISSUES'
21058
+ });
21059
+ issues.forEach(issue => {
21060
+ dispatch({
21061
+ type: 'ADD_ISSUE',
21062
+ payload: issue
21063
+ });
21064
+ });
21065
+ if (issues.length > 0) {
21066
+ console.group('🔍 Accessibility Issues Found');
21067
+ issues.forEach(issue => {
21068
+ const logMethod = issue.level === 'error' ? console.error : console.warn;
21069
+ logMethod(`${issue.type}: ${issue.message}`);
21070
+ });
21071
+ console.groupEnd();
21072
+ }
21073
+ }, 1000);
21074
+ };
21075
+ // Context value
21076
+ const contextValue = {
21077
+ ...state,
21078
+ // Actions
21079
+ setHighContrast: enabled => dispatch({
21080
+ type: 'SET_HIGH_CONTRAST',
21081
+ payload: enabled
21082
+ }),
21083
+ setReducedMotion: enabled => dispatch({
21084
+ type: 'SET_REDUCED_MOTION',
21085
+ payload: enabled
21086
+ }),
21087
+ setLargeText: enabled => dispatch({
21088
+ type: 'SET_LARGE_TEXT',
21089
+ payload: enabled
21090
+ }),
21091
+ setFocusVisible: enabled => dispatch({
21092
+ type: 'SET_FOCUS_VISIBLE',
21093
+ payload: enabled
21094
+ }),
21095
+ // Utilities
21096
+ announce: (message, level = 'polite') => {
21097
+ ariaLiveManager.announce(message, level);
21098
+ dispatch({
21099
+ type: 'SET_ANNOUNCEMENT',
21100
+ payload: {
21101
+ message,
21102
+ level,
21103
+ timestamp: Date.now()
21104
+ }
21105
+ });
21106
+ },
21107
+ announcePolite: message => {
21108
+ ariaLiveManager.announcePolite(message);
21109
+ dispatch({
21110
+ type: 'SET_ANNOUNCEMENT',
21111
+ payload: {
21112
+ message,
21113
+ level: 'polite',
21114
+ timestamp: Date.now()
21115
+ }
21116
+ });
21117
+ },
21118
+ announceAssertive: message => {
21119
+ ariaLiveManager.announceAssertive(message);
21120
+ dispatch({
21121
+ type: 'SET_ANNOUNCEMENT',
21122
+ payload: {
21123
+ message,
21124
+ level: 'assertive',
21125
+ timestamp: Date.now()
21126
+ }
21127
+ });
21128
+ },
21129
+ addIssue: issue => dispatch({
21130
+ type: 'ADD_ISSUE',
21131
+ payload: issue
21132
+ }),
21133
+ clearIssues: () => dispatch({
21134
+ type: 'CLEAR_ISSUES'
21135
+ }),
21136
+ // Audit function
21137
+ runAudit: runAccessibilityAudit
21138
+ };
21139
+ // Apply CSS classes based on preferences
21140
+ useEffect(() => {
21141
+ if (typeof document === 'undefined') return;
21142
+ const {
21143
+ highContrast,
21144
+ reducedMotion,
21145
+ largeText
21146
+ } = state;
21147
+ document.body.classList.toggle('high-contrast', highContrast);
21148
+ document.body.classList.toggle('reduced-motion', reducedMotion);
21149
+ document.body.classList.toggle('large-text', largeText);
21150
+ }, [state.highContrast, state.reducedMotion, state.largeText]);
21151
+ return jsx(AccessibilityContext.Provider, {
21152
+ value: contextValue,
21153
+ children: children
21154
+ });
21155
+ };
21156
+ /**
21157
+ * Hook to access accessibility context
21158
+ */
21159
+ const useAccessibility = () => {
21160
+ const context = useContext(AccessibilityContext);
21161
+ if (!context) {
21162
+ throw new Error('useAccessibility must be used within an AccessibilityProvider');
21163
+ }
21164
+ return context;
21165
+ };
21166
+ /**
21167
+ * Higher-Order Component for accessibility enhancements
21168
+ */
21169
+ const withAccessibility = WrappedComponent => {
21170
+ const AccessibilityEnhancedComponent = props => {
21171
+ const accessibility = useAccessibility();
21172
+ return jsx(WrappedComponent, {
21173
+ ...props,
21174
+ accessibility: accessibility
21175
+ });
21176
+ };
21177
+ AccessibilityEnhancedComponent.displayName = `withAccessibility(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;
21178
+ return AccessibilityEnhancedComponent;
21179
+ };
21180
+
20499
21181
  const QwickApp = ({
20500
21182
  children,
20501
21183
  className,
@@ -20557,16 +21239,20 @@ const QwickApp = ({
20557
21239
  dataSource: dataSource,
20558
21240
  children: content
20559
21241
  }) : content;
20560
- const appContent = jsx("div", {
20561
- className: `qwick-app ${className || ''}`,
20562
- style: style,
20563
- children: jsx(ThemeProvider, {
20564
- appId: appId,
20565
- defaultTheme: defaultTheme,
20566
- defaultPalette: defaultPalette,
20567
- children: jsx(QwickAppContext.Provider, {
20568
- value: contextValue,
20569
- children: wrappedContent
21242
+ const appContent = jsx(ErrorBoundary, {
21243
+ children: jsx(AccessibilityProvider, {
21244
+ children: jsx("div", {
21245
+ className: `qwick-app ${className || ''}`,
21246
+ style: style,
21247
+ children: jsx(ThemeProvider, {
21248
+ appId: appId,
21249
+ defaultTheme: defaultTheme,
21250
+ defaultPalette: defaultPalette,
21251
+ children: jsx(QwickAppContext.Provider, {
21252
+ value: contextValue,
21253
+ children: wrappedContent
21254
+ })
21255
+ })
20570
21256
  })
20571
21257
  })
20572
21258
  });
@@ -20674,6 +21360,186 @@ const setCSSVariable = (property, value) => {
20674
21360
  document.documentElement.style.setProperty(property, value);
20675
21361
  };
20676
21362
 
21363
+ /**
21364
+ * Generic Breadcrumbs component for navigation hierarchy
21365
+ *
21366
+ * Features:
21367
+ * - Accessible navigation with proper ARIA labels
21368
+ * - Customizable separators and icons
21369
+ * - Responsive design with item truncation
21370
+ * - Support for custom navigation handlers
21371
+ * - Keyboard navigation support
21372
+ * - Screen reader friendly
21373
+ */
21374
+ const Breadcrumbs = ({
21375
+ items,
21376
+ separator = '/',
21377
+ className = '',
21378
+ onNavigate,
21379
+ maxItems,
21380
+ showRoot = true
21381
+ }) => {
21382
+ // Filter and prepare items
21383
+ let displayItems = showRoot ? items : items.slice(1);
21384
+ // Handle max items with ellipsis
21385
+ if (maxItems && displayItems.length > maxItems) {
21386
+ const firstItems = displayItems.slice(0, 1);
21387
+ const lastItems = displayItems.slice(-Math.max(1, maxItems - 2));
21388
+ displayItems = [...firstItems, {
21389
+ label: '...',
21390
+ href: undefined,
21391
+ current: false
21392
+ }, ...lastItems];
21393
+ }
21394
+ const handleItemClick = (e, item, index) => {
21395
+ if (onNavigate) {
21396
+ e.preventDefault();
21397
+ onNavigate(item, index);
21398
+ }
21399
+ };
21400
+ const handleKeyDown = (e, item, index) => {
21401
+ if (e.key === 'Enter' || e.key === ' ') {
21402
+ e.preventDefault();
21403
+ if (onNavigate) {
21404
+ onNavigate(item, index);
21405
+ } else if (item.href) {
21406
+ window.location.href = item.href;
21407
+ }
21408
+ }
21409
+ };
21410
+ if (displayItems.length <= 1) {
21411
+ return null;
21412
+ }
21413
+ return jsx("nav", {
21414
+ className: `breadcrumbs ${className}`,
21415
+ role: "navigation",
21416
+ "aria-label": "Breadcrumb navigation",
21417
+ style: {
21418
+ display: 'flex',
21419
+ alignItems: 'center',
21420
+ fontSize: '14px',
21421
+ color: '#6b7280',
21422
+ ...defaultStyles.nav
21423
+ },
21424
+ children: jsx("ol", {
21425
+ style: {
21426
+ display: 'flex',
21427
+ alignItems: 'center',
21428
+ listStyle: 'none',
21429
+ margin: 0,
21430
+ padding: 0,
21431
+ gap: '8px'
21432
+ },
21433
+ children: displayItems.map((item, index) => {
21434
+ const isLast = index === displayItems.length - 1;
21435
+ const isClickable = (item.href || onNavigate) && !item.current && item.label !== '...';
21436
+ return jsxs("li", {
21437
+ style: {
21438
+ display: 'flex',
21439
+ alignItems: 'center'
21440
+ },
21441
+ children: [isClickable ? jsxs("a", {
21442
+ href: item.href,
21443
+ onClick: e => handleItemClick(e, item, index),
21444
+ onKeyDown: e => handleKeyDown(e, item, index),
21445
+ style: {
21446
+ ...defaultStyles.link,
21447
+ color: isLast ? '#374151' : '#6b7280',
21448
+ textDecoration: 'none',
21449
+ display: 'flex',
21450
+ alignItems: 'center',
21451
+ gap: '4px'
21452
+ },
21453
+ tabIndex: 0,
21454
+ "aria-current": item.current ? 'page' : undefined,
21455
+ children: [item.icon && jsx("span", {
21456
+ style: {
21457
+ display: 'flex',
21458
+ alignItems: 'center'
21459
+ },
21460
+ "aria-hidden": "true",
21461
+ children: item.icon
21462
+ }), jsx("span", {
21463
+ children: item.label
21464
+ })]
21465
+ }) : jsxs("span", {
21466
+ style: {
21467
+ ...defaultStyles.current,
21468
+ color: isLast ? '#111827' : '#6b7280',
21469
+ fontWeight: isLast ? 600 : 400,
21470
+ display: 'flex',
21471
+ alignItems: 'center',
21472
+ gap: '4px'
21473
+ },
21474
+ "aria-current": item.current ? 'page' : undefined,
21475
+ children: [item.icon && jsx("span", {
21476
+ style: {
21477
+ display: 'flex',
21478
+ alignItems: 'center'
21479
+ },
21480
+ "aria-hidden": "true",
21481
+ children: item.icon
21482
+ }), jsx("span", {
21483
+ children: item.label
21484
+ })]
21485
+ }), !isLast && jsx("span", {
21486
+ style: {
21487
+ display: 'flex',
21488
+ alignItems: 'center',
21489
+ marginLeft: '8px',
21490
+ color: '#d1d5db',
21491
+ fontSize: '12px'
21492
+ },
21493
+ "aria-hidden": "true",
21494
+ children: separator
21495
+ })]
21496
+ }, `${item.label}-${index}`);
21497
+ })
21498
+ })
21499
+ });
21500
+ };
21501
+ // Default styles
21502
+ const defaultStyles = {
21503
+ nav: {
21504
+ padding: '8px 0'
21505
+ },
21506
+ link: {
21507
+ transition: 'color 0.2s ease',
21508
+ cursor: 'pointer',
21509
+ borderRadius: '4px',
21510
+ padding: '4px',
21511
+ margin: '-4px'
21512
+ },
21513
+ current: {
21514
+ padding: '4px'
21515
+ }
21516
+ };
21517
+ /**
21518
+ * Hook for managing breadcrumb state
21519
+ */
21520
+ const useBreadcrumbs = () => {
21521
+ const [breadcrumbs, setBreadcrumbs] = React.useState([]);
21522
+ const addBreadcrumb = React.useCallback(item => {
21523
+ setBreadcrumbs(prev => [...prev, item]);
21524
+ }, []);
21525
+ const removeBreadcrumb = React.useCallback(index => {
21526
+ setBreadcrumbs(prev => prev.filter((_, i) => i !== index));
21527
+ }, []);
21528
+ const setBreadcrumbsCurrent = React.useCallback(items => {
21529
+ setBreadcrumbs(items);
21530
+ }, []);
21531
+ const clearBreadcrumbs = React.useCallback(() => {
21532
+ setBreadcrumbs([]);
21533
+ }, []);
21534
+ return {
21535
+ breadcrumbs,
21536
+ addBreadcrumb,
21537
+ removeBreadcrumb,
21538
+ setBreadcrumbs: setBreadcrumbsCurrent,
21539
+ clearBreadcrumbs
21540
+ };
21541
+ };
21542
+
20677
21543
  const QwickAppsLogo = props => {
20678
21544
  const {
20679
21545
  styleProps,
@@ -24140,4 +25006,4 @@ var ActionType;
24140
25006
  ActionType["CANCEL"] = "cancel";
24141
25007
  })(ActionType || (ActionType = {}));
24142
25008
 
24143
- export { AVAILABLE_PALETTES, ActionModel, ActionType, AllPalettes, Article, ArticleModel, BasePage, Button, CardListGrid, CardListGridModel, ChoiceInputField, ChoiceInputFieldModel, Code, CodeModel, Content, ContentModel, CoverImageHeader, CoverImageHeaderModel, DataProvider, DataProxy, DimensionsProvider, FeatureCard, FeatureCardActionModel, FeatureCardModel, FeatureGrid, FeatureGridModel, FeatureItemModel, Footer, FooterItemModel, FooterModel, FooterSectionModel, FormBlock, FormBlockModel, FormMethod, FormPage, GridCell, GridLayout, HeaderActionModel, HeroBlock, HeroBlockModel, Html, HtmlInputField, Logo, Markdown, MetadataItemModel, Page, PageBannerHeader, PageBannerHeaderModel, PaletteAutumn, PaletteCosmic, PaletteDefault, PaletteOcean, PaletteProvider, PaletteSpring, PaletteSwitcher, PaletteSwitcherModel, PaletteWinter, ProductCard, ProductCardActionModel, ProductCardModel, ProductModel, QWICKAPP_COMPONENT, QwickApp, QwickAppsLogo, SafeSpan, SafeSpanModel, Section, SectionModel, SelectInputField, T, TextField, TextInputField, TextInputFieldModel, ThemeProvider, ThemeSwitcher, ThemeSwitcherModel, applyCustomPalette, clearUserPalettePreference, clearUserThemePreference, createPaletteFromCurrentTheme, deleteCustomPalette, exportPalette, getCSSVariable, getComputedTheme, getCurrentPalette, getCurrentTheme, getCustomPalettes, getPaletteConfig, getPaletteName, getSystemTheme, getThemePerformanceStats, importPalette, initializePalette, initializeTheme, loadUserPalettePreference, loadUserThemePreference, logThemePerformanceStats, loggers, resetThemePerformanceStats, resolveDimension, resolveDimensions, resolveSpacing, resolveSpacingProps, saveCustomPalette, savePalettePreference, saveThemePreference, saveUserPalettePreference, saveUserThemePreference, setCSSVariable, setPalette, setTheme, t, useBaseProps, useData, useDataBinding, useDataContext, useDataProvider, useDimensions, usePalette, useQwickApp, useResolveTemplate, useSafeLocation, useSafeNavigate, useTemplate, useTheme };
25009
+ export { AVAILABLE_PALETTES, AccessibilityProvider, ActionModel, ActionType, AllPalettes, Article, ArticleModel, BasePage, Breadcrumbs, Button, CardListGrid, CardListGridModel, ChoiceInputField, ChoiceInputFieldModel, Code, CodeModel, Content, ContentModel, CoverImageHeader, CoverImageHeaderModel, DataProvider, DataProxy, DimensionsProvider, ErrorBoundary, FeatureCard, FeatureCardActionModel, FeatureCardModel, FeatureGrid, FeatureGridModel, FeatureItemModel, Footer, FooterItemModel, FooterModel, FooterSectionModel, FormBlock, FormBlockModel, FormMethod, FormPage, GridCell, GridLayout, HeaderActionModel, HeroBlock, HeroBlockModel, Html, HtmlInputField, Logo, Markdown, MetadataItemModel, Page, PageBannerHeader, PageBannerHeaderModel, PaletteAutumn, PaletteCosmic, PaletteDefault, PaletteOcean, PaletteProvider, PaletteSpring, PaletteSwitcher, PaletteSwitcherModel, PaletteWinter, ProductCard, ProductCardActionModel, ProductCardModel, ProductModel, QWICKAPP_COMPONENT, QwickApp, QwickAppsLogo, SafeSpan, SafeSpanModel, Section, SectionModel, SelectInputField, T, TextField, TextInputField, TextInputFieldModel, ThemeProvider, ThemeSwitcher, ThemeSwitcherModel, applyCustomPalette, clearUserPalettePreference, clearUserThemePreference, createLogger, createPaletteFromCurrentTheme, deleteCustomPalette, exportPalette, getCSSVariable, getComputedTheme, getCurrentPalette, getCurrentTheme, getCustomPalettes, getPaletteConfig, getPaletteName, getSystemTheme, getThemePerformanceStats, importPalette, initializePalette, initializeTheme, loadUserPalettePreference, loadUserThemePreference, logThemePerformanceStats, loggers, resetThemePerformanceStats, resolveDimension, resolveDimensions, resolveSpacing, resolveSpacingProps, saveCustomPalette, savePalettePreference, saveThemePreference, saveUserPalettePreference, saveUserThemePreference, setCSSVariable, setPalette, setTheme, t, useAccessibility, useBaseProps, useBreadcrumbs, useData, useDataBinding, useDataContext, useDataProvider, useDimensions, usePalette, useQwickApp, useResolveTemplate, useSafeLocation, useSafeNavigate, useTemplate, useTheme, withAccessibility, withErrorBoundary };