@qwickapps/react-framework 1.5.6 → 1.5.8
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/dist/components/AccessibilityChecker.d.ts.map +1 -1
- package/dist/components/Html.d.ts +1 -1
- package/dist/components/Html.d.ts.map +1 -1
- package/dist/components/Logo.d.ts.map +1 -1
- package/dist/components/Markdown.d.ts +2 -2
- package/dist/components/Markdown.d.ts.map +1 -1
- package/dist/components/QwickApp.d.ts.map +1 -1
- package/dist/components/SafeSpan.d.ts +1 -1
- package/dist/components/SafeSpan.d.ts.map +1 -1
- package/dist/components/base/ModelView.d.ts +1 -1
- package/dist/components/base/ModelView.d.ts.map +1 -1
- package/dist/components/blocks/Article.d.ts +1 -1
- package/dist/components/blocks/Article.d.ts.map +1 -1
- package/dist/components/blocks/CardListGrid.d.ts.map +1 -1
- package/dist/components/blocks/Code.d.ts.map +1 -1
- package/dist/components/blocks/Content.d.ts.map +1 -1
- package/dist/components/blocks/CoverImageHeader.d.ts.map +1 -1
- package/dist/components/blocks/FeatureCard.d.ts.map +1 -1
- package/dist/components/blocks/FeatureGrid.d.ts.map +1 -1
- package/dist/components/blocks/Footer.d.ts.map +1 -1
- package/dist/components/blocks/Image.d.ts.map +1 -1
- package/dist/components/blocks/PageBannerHeader.d.ts.map +1 -1
- package/dist/components/blocks/ProductCard.d.ts.map +1 -1
- package/dist/components/blocks/Section.d.ts.map +1 -1
- package/dist/components/blocks/Text.d.ts +8 -1
- package/dist/components/blocks/Text.d.ts.map +1 -1
- package/dist/components/buttons/Button.d.ts.map +1 -1
- package/dist/components/buttons/PaletteSwitcher.d.ts.map +1 -1
- package/dist/components/buttons/ThemeSwitcher.d.ts.map +1 -1
- package/dist/components/forms/FormBlock.d.ts +1 -1
- package/dist/components/forms/FormBlock.d.ts.map +1 -1
- package/dist/components/forms/SchemaFormRenderer.d.ts +28 -0
- package/dist/components/forms/SchemaFormRenderer.d.ts.map +1 -0
- package/dist/components/forms/index.d.ts +2 -0
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/input/ChoiceInputField.d.ts.map +1 -1
- package/dist/components/input/HtmlInputField.d.ts.map +1 -1
- package/dist/components/layout/CollapsibleLayout/CollapsibleLayout.d.ts.map +1 -1
- package/dist/components/layout/GridLayout.d.ts +5 -0
- package/dist/components/layout/GridLayout.d.ts.map +1 -1
- package/dist/components/plugins/DataTable.d.ts +57 -0
- package/dist/components/plugins/DataTable.d.ts.map +1 -0
- package/dist/components/plugins/StatCard.d.ts +44 -0
- package/dist/components/plugins/StatCard.d.ts.map +1 -0
- package/dist/components/plugins/index.d.ts +13 -0
- package/dist/components/plugins/index.d.ts.map +1 -0
- package/dist/components/shared/createSerializableView.d.ts.map +1 -1
- package/dist/contexts/NavigationContext.d.ts.map +1 -1
- package/dist/hooks/useBaseProps.d.ts +1 -1
- package/dist/hooks/useBaseProps.d.ts.map +1 -1
- package/dist/index.esm.js +5939 -5532
- package/dist/index.js +6028 -5618
- package/dist/palettes/manifest.json +19 -19
- package/dist/schemas/transformers/ReactNodeTransformer.d.ts.map +1 -1
- package/dist/utils/iconMap.d.ts +21 -8
- package/dist/utils/iconMap.d.ts.map +1 -1
- package/package.json +1 -2
- package/src/__tests__/utils/iconMap.test.tsx +197 -0
- package/src/components/AccessibilityChecker.tsx +10 -7
- package/src/components/ErrorBoundary.tsx +3 -3
- package/src/components/Html.tsx +17 -12
- package/src/components/Logo.tsx +1 -8
- package/src/components/Markdown.tsx +10 -10
- package/src/components/QwickApp.tsx +8 -1
- package/src/components/ResponsiveMenu.tsx +1 -1
- package/src/components/SafeSpan.tsx +9 -9
- package/src/components/Scaffold.tsx +4 -4
- package/src/components/base/ModelView.tsx +2 -2
- package/src/components/blocks/Article.tsx +7 -7
- package/src/components/blocks/CardListGrid.tsx +1 -3
- package/src/components/blocks/Code.tsx +10 -8
- package/src/components/blocks/Content.tsx +2 -4
- package/src/components/blocks/CoverImageHeader.tsx +3 -4
- package/src/components/blocks/FeatureCard.tsx +2 -4
- package/src/components/blocks/FeatureGrid.tsx +2 -4
- package/src/components/blocks/Footer.tsx +2 -4
- package/src/components/blocks/Image.tsx +8 -5
- package/src/components/blocks/PageBannerHeader.tsx +3 -4
- package/src/components/blocks/ProductCard.tsx +8 -5
- package/src/components/blocks/Section.tsx +6 -4
- package/src/components/blocks/Text.tsx +15 -7
- package/src/components/buttons/Button.tsx +8 -6
- package/src/components/buttons/PaletteSwitcher.tsx +6 -8
- package/src/components/buttons/ThemeSwitcher.tsx +8 -9
- package/src/components/forms/Captcha.tsx +1 -1
- package/src/components/forms/FormBlock.tsx +3 -5
- package/src/components/forms/FormCheckbox.tsx +1 -1
- package/src/components/forms/FormField.tsx +1 -1
- package/src/components/forms/FormSelect.tsx +1 -1
- package/src/components/forms/SchemaFormRenderer.tsx +268 -0
- package/src/components/forms/__tests__/SchemaFormRenderer.test.tsx +212 -0
- package/src/components/forms/index.ts +3 -0
- package/src/components/index.ts +1 -0
- package/src/components/input/ChoiceInputField.tsx +2 -1
- package/src/components/input/HtmlInputField.tsx +14 -9
- package/src/components/input/TextField.tsx +1 -1
- package/src/components/layout/CollapsibleLayout/CollapsibleLayout.tsx +6 -8
- package/src/components/layout/GridLayout.tsx +4 -0
- package/src/components/plugins/DataTable.tsx +259 -0
- package/src/components/plugins/StatCard.tsx +122 -0
- package/src/components/plugins/__tests__/DataTable.test.tsx +158 -0
- package/src/components/plugins/index.ts +14 -0
- package/src/components/shared/createSerializableView.tsx +8 -6
- package/src/contexts/NavigationContext.tsx +21 -15
- package/src/hooks/useBaseProps.ts +1 -1
- package/src/schemas/transformers/ReactNodeTransformer.ts +13 -10
- package/src/utils/iconMap.tsx +290 -174
- /package/dist/palettes/{palette-autumn.1.5.6.css → palette-autumn.1.5.8.css} +0 -0
- /package/dist/palettes/{palette-autumn.1.5.6.min.css → palette-autumn.1.5.8.min.css} +0 -0
- /package/dist/palettes/{palette-cosmic.1.5.6.css → palette-cosmic.1.5.8.css} +0 -0
- /package/dist/palettes/{palette-cosmic.1.5.6.min.css → palette-cosmic.1.5.8.min.css} +0 -0
- /package/dist/palettes/{palette-default.1.5.6.css → palette-default.1.5.8.css} +0 -0
- /package/dist/palettes/{palette-default.1.5.6.min.css → palette-default.1.5.8.min.css} +0 -0
- /package/dist/palettes/{palette-ocean.1.5.6.css → palette-ocean.1.5.8.css} +0 -0
- /package/dist/palettes/{palette-ocean.1.5.6.min.css → palette-ocean.1.5.8.min.css} +0 -0
- /package/dist/palettes/{palette-spring.1.5.6.css → palette-spring.1.5.8.css} +0 -0
- /package/dist/palettes/{palette-spring.1.5.6.min.css → palette-spring.1.5.8.min.css} +0 -0
- /package/dist/palettes/{palette-winter.1.5.6.css → palette-winter.1.5.8.css} +0 -0
- /package/dist/palettes/{palette-winter.1.5.6.min.css → palette-winter.1.5.8.min.css} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./manifest.schema.json",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.8",
|
|
4
4
|
"palettes": [
|
|
5
5
|
{
|
|
6
6
|
"id": "default",
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
"description": "Classic blue and neutral color scheme - the original QwickApps palette",
|
|
9
9
|
"author": "QwickApps",
|
|
10
10
|
"license": "PolyForm-Shield-1.0.0",
|
|
11
|
-
"version": "1.5.
|
|
12
|
-
"file": "palette-default.1.5.
|
|
11
|
+
"version": "1.5.8",
|
|
12
|
+
"file": "palette-default.1.5.8.css",
|
|
13
13
|
"primaryColor": "#007bff",
|
|
14
14
|
"inlined": true,
|
|
15
|
-
"fileMinified": "palette-default.1.5.
|
|
15
|
+
"fileMinified": "palette-default.1.5.8.min.css",
|
|
16
16
|
"fileLatest": "palette-default.latest.css",
|
|
17
17
|
"fileLatestMinified": "palette-default.latest.min.css"
|
|
18
18
|
},
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
"description": "Warm oranges, golden yellows, and earthy browns - inspired by fall foliage",
|
|
23
23
|
"author": "QwickApps",
|
|
24
24
|
"license": "PolyForm-Shield-1.0.0",
|
|
25
|
-
"version": "1.5.
|
|
26
|
-
"file": "palette-autumn.1.5.
|
|
25
|
+
"version": "1.5.8",
|
|
26
|
+
"file": "palette-autumn.1.5.8.css",
|
|
27
27
|
"primaryColor": "#ea580c",
|
|
28
28
|
"inlined": false,
|
|
29
|
-
"fileMinified": "palette-autumn.1.5.
|
|
29
|
+
"fileMinified": "palette-autumn.1.5.8.min.css",
|
|
30
30
|
"fileLatest": "palette-autumn.latest.css",
|
|
31
31
|
"fileLatestMinified": "palette-autumn.latest.min.css"
|
|
32
32
|
},
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"description": "Modern purple gradient for creative and tech brands - inspired by cosmic nebulae",
|
|
37
37
|
"author": "QwickApps",
|
|
38
38
|
"license": "PolyForm-Shield-1.0.0",
|
|
39
|
-
"version": "1.5.
|
|
40
|
-
"file": "palette-cosmic.1.5.
|
|
39
|
+
"version": "1.5.8",
|
|
40
|
+
"file": "palette-cosmic.1.5.8.css",
|
|
41
41
|
"primaryColor": "#8b5cf6",
|
|
42
42
|
"inlined": false,
|
|
43
|
-
"fileMinified": "palette-cosmic.1.5.
|
|
43
|
+
"fileMinified": "palette-cosmic.1.5.8.min.css",
|
|
44
44
|
"fileLatest": "palette-cosmic.latest.css",
|
|
45
45
|
"fileLatestMinified": "palette-cosmic.latest.min.css"
|
|
46
46
|
},
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
"description": "Deep blues, aqua teals, and seafoam greens - inspired by ocean depths",
|
|
51
51
|
"author": "QwickApps",
|
|
52
52
|
"license": "PolyForm-Shield-1.0.0",
|
|
53
|
-
"version": "1.5.
|
|
54
|
-
"file": "palette-ocean.1.5.
|
|
53
|
+
"version": "1.5.8",
|
|
54
|
+
"file": "palette-ocean.1.5.8.css",
|
|
55
55
|
"primaryColor": "#0891b2",
|
|
56
56
|
"inlined": false,
|
|
57
|
-
"fileMinified": "palette-ocean.1.5.
|
|
57
|
+
"fileMinified": "palette-ocean.1.5.8.min.css",
|
|
58
58
|
"fileLatest": "palette-ocean.latest.css",
|
|
59
59
|
"fileLatestMinified": "palette-ocean.latest.min.css"
|
|
60
60
|
},
|
|
@@ -64,11 +64,11 @@
|
|
|
64
64
|
"description": "Fresh greens, soft pinks, and bright yellows - inspired by spring blooms",
|
|
65
65
|
"author": "QwickApps",
|
|
66
66
|
"license": "PolyForm-Shield-1.0.0",
|
|
67
|
-
"version": "1.5.
|
|
68
|
-
"file": "palette-spring.1.5.
|
|
67
|
+
"version": "1.5.8",
|
|
68
|
+
"file": "palette-spring.1.5.8.css",
|
|
69
69
|
"primaryColor": "#16a34a",
|
|
70
70
|
"inlined": false,
|
|
71
|
-
"fileMinified": "palette-spring.1.5.
|
|
71
|
+
"fileMinified": "palette-spring.1.5.8.min.css",
|
|
72
72
|
"fileLatest": "palette-spring.latest.css",
|
|
73
73
|
"fileLatestMinified": "palette-spring.latest.min.css"
|
|
74
74
|
},
|
|
@@ -78,11 +78,11 @@
|
|
|
78
78
|
"description": "Cool blues, icy whites, and frosty grays - inspired by winter landscapes",
|
|
79
79
|
"author": "QwickApps",
|
|
80
80
|
"license": "PolyForm-Shield-1.0.0",
|
|
81
|
-
"version": "1.5.
|
|
82
|
-
"file": "palette-winter.1.5.
|
|
81
|
+
"version": "1.5.8",
|
|
82
|
+
"file": "palette-winter.1.5.8.css",
|
|
83
83
|
"primaryColor": "#0077be",
|
|
84
84
|
"inlined": false,
|
|
85
|
-
"fileMinified": "palette-winter.1.5.
|
|
85
|
+
"fileMinified": "palette-winter.1.5.8.min.css",
|
|
86
86
|
"fileLatest": "palette-winter.latest.css",
|
|
87
87
|
"fileLatestMinified": "palette-winter.latest.min.css"
|
|
88
88
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNodeTransformer.d.ts","sourceRoot":"","sources":["../../../src/schemas/transformers/ReactNodeTransformer.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAgB,SAAS,EAAiC,MAAM,OAAO,CAAC;AAG/E;;;GAGG;AACH,qBAAa,oBAAoB;IAC/B;;;;OAIG;IACH,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO;IAoE1C;;;;OAIG;IACH,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS;IA4C5C;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IAqB7B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,uBAAuB;
|
|
1
|
+
{"version":3,"file":"ReactNodeTransformer.d.ts","sourceRoot":"","sources":["../../../src/schemas/transformers/ReactNodeTransformer.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAgB,SAAS,EAAiC,MAAM,OAAO,CAAC;AAG/E;;;GAGG;AACH,qBAAa,oBAAoB;IAC/B;;;;OAIG;IACH,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO;IAoE1C;;;;OAIG;IACH,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS;IA4C5C;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IAqB7B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAoBtC;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAkB/B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;CAuBlC"}
|
package/dist/utils/iconMap.d.ts
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* Provides centralized icon mapping for both Material-UI components and emoji representations.
|
|
5
5
|
* Used across the framework for consistent icon rendering in buttons, navigation, admin UI, etc.
|
|
6
6
|
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Static map for commonly used icons with emoji support
|
|
9
|
+
* - Fallback to HelpOutline icon for unmapped icons (with console warning)
|
|
10
|
+
* - Runtime icon registration via registerIcon() for app-specific icons
|
|
11
|
+
*
|
|
7
12
|
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
8
13
|
*/
|
|
9
14
|
import React from 'react';
|
|
@@ -15,21 +20,28 @@ export interface IconMapping {
|
|
|
15
20
|
component: React.ComponentType;
|
|
16
21
|
}
|
|
17
22
|
/**
|
|
18
|
-
* Centralized icon registry mapping icon names to their representations
|
|
19
|
-
*
|
|
23
|
+
* Centralized icon registry mapping icon names to their representations.
|
|
24
|
+
* Sorted alphabetically by category, then by key within each category.
|
|
25
|
+
*
|
|
26
|
+
* For icons not in this map, getIconComponent() will return a HelpOutline fallback
|
|
27
|
+
* and log a warning. Use registerIcon() to add app-specific icons at runtime.
|
|
20
28
|
*/
|
|
21
29
|
export declare const iconMap: Record<string, IconMapping>;
|
|
22
30
|
/**
|
|
23
31
|
* Get emoji representation of an icon
|
|
24
|
-
* @param iconName - Icon name (case-insensitive)
|
|
32
|
+
* @param iconName - Icon name (case-insensitive, supports snake_case)
|
|
25
33
|
* @param fallback - Fallback emoji if icon not found (default: 🔗)
|
|
26
34
|
* @returns Emoji string
|
|
27
35
|
*/
|
|
28
36
|
export declare function getIconEmoji(iconName: string | undefined, fallback?: string): string;
|
|
29
37
|
/**
|
|
30
|
-
* Get Material-UI component representation of an icon
|
|
31
|
-
*
|
|
32
|
-
*
|
|
38
|
+
* Get Material-UI component representation of an icon.
|
|
39
|
+
*
|
|
40
|
+
* Uses the static iconMap for known icons. For unmapped icons,
|
|
41
|
+
* returns a HelpOutline fallback and logs a warning.
|
|
42
|
+
*
|
|
43
|
+
* @param iconName - Icon name (case-insensitive, supports snake_case)
|
|
44
|
+
* @returns React element (mapped icon or HelpOutline fallback), or null if no name provided
|
|
33
45
|
*/
|
|
34
46
|
export declare function getIconComponent(iconName: string | undefined): React.ReactElement | null;
|
|
35
47
|
/**
|
|
@@ -38,11 +50,12 @@ export declare function getIconComponent(iconName: string | undefined): React.Re
|
|
|
38
50
|
*/
|
|
39
51
|
export declare function registerIcon(name: string, mapping: IconMapping): void;
|
|
40
52
|
/**
|
|
41
|
-
* Check if an icon is registered
|
|
53
|
+
* Check if an icon is registered in the static map
|
|
54
|
+
* If false, getIconComponent will return HelpOutline fallback
|
|
42
55
|
*/
|
|
43
56
|
export declare function hasIcon(iconName: string): boolean;
|
|
44
57
|
/**
|
|
45
|
-
* Get all registered icon names
|
|
58
|
+
* Get all registered icon names from the static map
|
|
46
59
|
*/
|
|
47
60
|
export declare function getRegisteredIcons(): string[];
|
|
48
61
|
//# sourceMappingURL=iconMap.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"iconMap.d.ts","sourceRoot":"","sources":["../../src/utils/iconMap.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"iconMap.d.ts","sourceRoot":"","sources":["../../src/utils/iconMap.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAyG1B;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,KAAK,CAAC,aAAa,CAAC;CAChC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAmK/C,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,GAAE,MAAa,GAAG,MAAM,CAK1F;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAiBxF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAErE;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,EAAE,CAE7C"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qwickapps/react-framework",
|
|
3
|
-
"version": "1.5.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "1.5.8",
|
|
5
4
|
"description": "Complete React framework with responsive navigation, flexible layouts, theming system, and reusable components for building modern applications.",
|
|
6
5
|
"main": "dist/index.js",
|
|
7
6
|
"module": "dist/index.esm.js",
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon Map Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the icon mapping utility functions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getIconComponent,
|
|
9
|
+
getIconEmoji,
|
|
10
|
+
hasIcon,
|
|
11
|
+
registerIcon,
|
|
12
|
+
getRegisteredIcons,
|
|
13
|
+
iconMap,
|
|
14
|
+
} from '../../utils/iconMap';
|
|
15
|
+
import { Home, Star } from '@mui/icons-material';
|
|
16
|
+
|
|
17
|
+
describe('iconMap', () => {
|
|
18
|
+
describe('getIconComponent', () => {
|
|
19
|
+
it('returns null for undefined input', () => {
|
|
20
|
+
expect(getIconComponent(undefined)).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns null for empty string', () => {
|
|
24
|
+
expect(getIconComponent('')).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns correct component for mapped icon', () => {
|
|
28
|
+
const result = getIconComponent('home');
|
|
29
|
+
expect(result).not.toBeNull();
|
|
30
|
+
expect(result?.type).toBe(Home);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('is case-insensitive', () => {
|
|
34
|
+
const lower = getIconComponent('home');
|
|
35
|
+
const upper = getIconComponent('HOME');
|
|
36
|
+
const mixed = getIconComponent('Home');
|
|
37
|
+
|
|
38
|
+
expect(lower?.type).toBe(upper?.type);
|
|
39
|
+
expect(lower?.type).toBe(mixed?.type);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('supports snake_case icon names', () => {
|
|
43
|
+
const result = getIconComponent('manage_accounts');
|
|
44
|
+
expect(result).not.toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('returns HelpOutline fallback for unmapped icons', () => {
|
|
48
|
+
// Suppress console.warn for this test
|
|
49
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
50
|
+
|
|
51
|
+
const result = getIconComponent('nonexistent_icon_xyz');
|
|
52
|
+
expect(result).not.toBeNull();
|
|
53
|
+
|
|
54
|
+
warnSpy.mockRestore();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('logs warning for unmapped icons in non-production', () => {
|
|
58
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
59
|
+
|
|
60
|
+
getIconComponent('nonexistent_icon_xyz');
|
|
61
|
+
|
|
62
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
63
|
+
expect.stringContaining('nonexistent_icon_xyz')
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
warnSpy.mockRestore();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('getIconEmoji', () => {
|
|
71
|
+
it('returns fallback for undefined input', () => {
|
|
72
|
+
expect(getIconEmoji(undefined)).toBe('🔗');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns custom fallback when provided', () => {
|
|
76
|
+
expect(getIconEmoji(undefined, '❓')).toBe('❓');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('returns correct emoji for mapped icon', () => {
|
|
80
|
+
expect(getIconEmoji('home')).toBe('🏠');
|
|
81
|
+
expect(getIconEmoji('favorite')).toBe('❤️');
|
|
82
|
+
expect(getIconEmoji('star')).toBe('⭐');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('is case-insensitive', () => {
|
|
86
|
+
expect(getIconEmoji('HOME')).toBe(getIconEmoji('home'));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns fallback for unmapped icons', () => {
|
|
90
|
+
expect(getIconEmoji('nonexistent_icon')).toBe('🔗');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('hasIcon', () => {
|
|
95
|
+
it('returns true for mapped icons', () => {
|
|
96
|
+
expect(hasIcon('home')).toBe(true);
|
|
97
|
+
expect(hasIcon('settings')).toBe(true);
|
|
98
|
+
expect(hasIcon('people')).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('returns false for unmapped icons', () => {
|
|
102
|
+
expect(hasIcon('nonexistent_icon')).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('is case-insensitive', () => {
|
|
106
|
+
expect(hasIcon('HOME')).toBe(true);
|
|
107
|
+
expect(hasIcon('Home')).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('registerIcon', () => {
|
|
112
|
+
// Cleanup any test icons after each test to prevent pollution
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
delete iconMap['custom_test_icon'];
|
|
115
|
+
delete iconMap['upper_case_icon'];
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('registers new icons at runtime', () => {
|
|
119
|
+
const customIconName = 'custom_test_icon';
|
|
120
|
+
|
|
121
|
+
// Should not exist initially
|
|
122
|
+
expect(hasIcon(customIconName)).toBe(false);
|
|
123
|
+
|
|
124
|
+
// Register it
|
|
125
|
+
registerIcon(customIconName, {
|
|
126
|
+
emoji: '🎯',
|
|
127
|
+
component: Star,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Should exist now
|
|
131
|
+
expect(hasIcon(customIconName)).toBe(true);
|
|
132
|
+
expect(getIconEmoji(customIconName)).toBe('🎯');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('is case-insensitive for registration', () => {
|
|
136
|
+
registerIcon('UPPER_CASE_ICON', {
|
|
137
|
+
emoji: '🔤',
|
|
138
|
+
component: Star,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(hasIcon('upper_case_icon')).toBe(true);
|
|
142
|
+
expect(hasIcon('UPPER_CASE_ICON')).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('getRegisteredIcons', () => {
|
|
147
|
+
it('returns an array of icon names', () => {
|
|
148
|
+
const icons = getRegisteredIcons();
|
|
149
|
+
|
|
150
|
+
expect(Array.isArray(icons)).toBe(true);
|
|
151
|
+
expect(icons.length).toBeGreaterThan(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('includes common icons', () => {
|
|
155
|
+
const icons = getRegisteredIcons();
|
|
156
|
+
|
|
157
|
+
expect(icons).toContain('home');
|
|
158
|
+
expect(icons).toContain('settings');
|
|
159
|
+
expect(icons).toContain('dashboard');
|
|
160
|
+
expect(icons).toContain('people');
|
|
161
|
+
expect(icons).toContain('manage_accounts');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('iconMap coverage', () => {
|
|
166
|
+
it('has emoji and component for all entries', () => {
|
|
167
|
+
for (const [name, mapping] of Object.entries(iconMap)) {
|
|
168
|
+
expect(mapping.emoji).toBeDefined();
|
|
169
|
+
expect(typeof mapping.emoji).toBe('string');
|
|
170
|
+
expect(mapping.emoji.length).toBeGreaterThan(0);
|
|
171
|
+
|
|
172
|
+
expect(mapping.component).toBeDefined();
|
|
173
|
+
// Components can be functions or objects (React.memo, forwardRef, etc.)
|
|
174
|
+
expect(['function', 'object']).toContain(typeof mapping.component);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('includes essential icons for navigation', () => {
|
|
179
|
+
const essentialIcons = [
|
|
180
|
+
'home', 'dashboard', 'settings', 'menu',
|
|
181
|
+
'people', 'person', 'help', 'info',
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
for (const icon of essentialIcons) {
|
|
185
|
+
expect(hasIcon(icon)).toBe(true);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('includes authentication icons', () => {
|
|
190
|
+
const authIcons = ['lock', 'security', 'key', 'login', 'logout'];
|
|
191
|
+
|
|
192
|
+
for (const icon of authIcons) {
|
|
193
|
+
expect(hasIcon(icon)).toBe(true);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -23,13 +23,16 @@ import {
|
|
|
23
23
|
IconButton,
|
|
24
24
|
Divider,
|
|
25
25
|
} from '@mui/material';
|
|
26
|
-
import
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
import Accessibility from '@mui/icons-material/Accessibility';
|
|
27
|
+
import CheckCircle from '@mui/icons-material/CheckCircle';
|
|
28
|
+
import Warning from '@mui/icons-material/Warning';
|
|
29
|
+
import Error from '@mui/icons-material/Error';
|
|
30
|
+
import Close from '@mui/icons-material/Close';
|
|
31
|
+
const AccessibilityIcon = Accessibility;
|
|
32
|
+
const CheckIcon = CheckCircle;
|
|
33
|
+
const WarningIcon = Warning;
|
|
34
|
+
const ErrorIcon = Error;
|
|
35
|
+
const CloseIcon = Close;
|
|
33
36
|
import { getContrastRatio } from '@mui/material';
|
|
34
37
|
import { getCurrentPalette } from '../utils/paletteUtils';
|
|
35
38
|
import { getCurrentTheme } from '../utils/themeUtils';
|
|
@@ -68,9 +68,9 @@ export class ErrorBoundary extends Component<Props, State> {
|
|
|
68
68
|
|
|
69
69
|
// Send error to logging service if available
|
|
70
70
|
if (typeof window !== 'undefined') {
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
71
|
+
const globalWindow = window as unknown as { qwickapps?: { logError?: (error: Error, errorInfo: React.ErrorInfo) => void } };
|
|
72
|
+
if (globalWindow.qwickapps?.logError) {
|
|
73
|
+
globalWindow.qwickapps.logError(error, errorInfo);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
}
|
package/src/components/Html.tsx
CHANGED
|
@@ -58,12 +58,15 @@ function HtmlView({
|
|
|
58
58
|
stripHeaders = false,
|
|
59
59
|
placeholder,
|
|
60
60
|
component = 'div',
|
|
61
|
+
transformConfig,
|
|
62
|
+
sanitize,
|
|
63
|
+
sanitizeOptions,
|
|
61
64
|
...restProps
|
|
62
65
|
}: HtmlViewProps) {
|
|
63
66
|
const { styleProps, htmlProps, restProps: otherProps } = useBaseProps(restProps);
|
|
64
67
|
|
|
65
68
|
// Mark as QwickApp component
|
|
66
|
-
(HtmlView
|
|
69
|
+
Object.assign(HtmlView, { [QWICKAPP_COMPONENT]: true });
|
|
67
70
|
|
|
68
71
|
// Return placeholder if no HTML content
|
|
69
72
|
if (!children || !children.trim()) {
|
|
@@ -177,7 +180,7 @@ function HtmlView({
|
|
|
177
180
|
}
|
|
178
181
|
|
|
179
182
|
// Main component with data binding support and serialization capability
|
|
180
|
-
export class Html extends ModelView<HtmlProps
|
|
183
|
+
export class Html extends ModelView<HtmlProps> {
|
|
181
184
|
// Component self-declaration for serialization
|
|
182
185
|
static readonly tagName = 'Html';
|
|
183
186
|
static readonly version = '1.0.0';
|
|
@@ -191,9 +194,11 @@ export class Html extends ModelView<HtmlProps, HtmlModel> {
|
|
|
191
194
|
if (version !== Html.version) {
|
|
192
195
|
console.warn(`Version mismatch: Expected ${Html.version} but got ${version}`);
|
|
193
196
|
}
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
+
|
|
198
|
+
const typedData = (data as Record<string, unknown>) || {};
|
|
199
|
+
const { children, ...props } = typedData;
|
|
200
|
+
// Children should be a string (HTML content), not deserialized React nodes
|
|
201
|
+
return <Html {...props}>{children as string}</Html>;
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
// Component-specific serialization properties
|
|
@@ -218,14 +223,16 @@ export class Html extends ModelView<HtmlProps, HtmlModel> {
|
|
|
218
223
|
|
|
219
224
|
// Register HTML patterns that Html component can handle
|
|
220
225
|
static registerPatternHandlers(registry: Record<string, unknown>): void {
|
|
226
|
+
const typedRegistry = registry as { hasPattern?: (pattern: string) => boolean; registerPattern?: (pattern: string, handler: (element: Element) => Record<string, unknown>) => void };
|
|
227
|
+
|
|
221
228
|
// Register div elements with specific classes for Html transformation
|
|
222
|
-
if (!
|
|
223
|
-
|
|
229
|
+
if (typedRegistry.hasPattern && !typedRegistry.hasPattern('div.html-content')) {
|
|
230
|
+
typedRegistry.registerPattern?.('div.html-content', Html.transformHtmlDiv);
|
|
224
231
|
}
|
|
225
232
|
|
|
226
233
|
// Register elements with data-html attribute
|
|
227
|
-
if (!
|
|
228
|
-
|
|
234
|
+
if (typedRegistry.hasPattern && !typedRegistry.hasPattern('[data-html]')) {
|
|
235
|
+
typedRegistry.registerPattern?.('[data-html]', Html.transformDataHtml);
|
|
229
236
|
}
|
|
230
237
|
}
|
|
231
238
|
|
|
@@ -268,9 +275,7 @@ function HtmlWithDataBinding(props: HtmlProps) {
|
|
|
268
275
|
// Use data binding
|
|
269
276
|
const { loading, error, ...htmlProps } = useDataBinding<HtmlModel>(
|
|
270
277
|
dataSource!,
|
|
271
|
-
restProps as Partial<HtmlModel
|
|
272
|
-
HtmlModel.getSchema(),
|
|
273
|
-
{ cache: true, cacheTTL: 300000, strict: false, ...bindingOptions }
|
|
278
|
+
restProps as Partial<HtmlModel>
|
|
274
279
|
);
|
|
275
280
|
|
|
276
281
|
// Show loading state
|
package/src/components/Logo.tsx
CHANGED
|
@@ -430,14 +430,7 @@ function Logo(props: LogoProps) {
|
|
|
430
430
|
// Always call hooks unconditionally
|
|
431
431
|
const bindingResult = useDataBinding<LogoModel>(
|
|
432
432
|
dataSource || '',
|
|
433
|
-
|
|
434
|
-
initialData: restProps,
|
|
435
|
-
schema: LogoModel.getSchema(),
|
|
436
|
-
cache: true,
|
|
437
|
-
cacheTTL: 300000,
|
|
438
|
-
strict: false,
|
|
439
|
-
...bindingOptions
|
|
440
|
-
}
|
|
433
|
+
restProps as Partial<LogoModel>
|
|
441
434
|
);
|
|
442
435
|
|
|
443
436
|
// If no dataSource, use traditional props
|
|
@@ -35,7 +35,7 @@ type MarkdownViewProps = SchemaProps<MarkdownModel> & {
|
|
|
35
35
|
/** Custom transformation configuration for HTML conversion */
|
|
36
36
|
htmlTransformConfig?: TransformConfig;
|
|
37
37
|
/** Custom sanitization options */
|
|
38
|
-
sanitizeOptions?: unknown
|
|
38
|
+
sanitizeOptions?: Record<string, unknown>;
|
|
39
39
|
/** Container element type */
|
|
40
40
|
component?: React.ElementType;
|
|
41
41
|
/** Marked options for Markdown parsing */
|
|
@@ -78,7 +78,7 @@ function MarkdownView({
|
|
|
78
78
|
const { styleProps, htmlProps, restProps: otherProps } = useBaseProps(restProps);
|
|
79
79
|
|
|
80
80
|
// Mark as QwickApp component
|
|
81
|
-
(MarkdownView
|
|
81
|
+
Object.assign(MarkdownView, { [QWICKAPP_COMPONENT]: true });
|
|
82
82
|
|
|
83
83
|
// Return placeholder if no Markdown content
|
|
84
84
|
if (!children || !children.trim()) {
|
|
@@ -186,7 +186,7 @@ function MarkdownView({
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
// Main component with data binding support and serialization capability
|
|
189
|
-
export class Markdown extends ModelView<MarkdownProps
|
|
189
|
+
export class Markdown extends ModelView<MarkdownProps> {
|
|
190
190
|
// Component self-declaration for serialization
|
|
191
191
|
static readonly tagName = 'Markdown';
|
|
192
192
|
static readonly version = '1.0.0';
|
|
@@ -213,14 +213,16 @@ export class Markdown extends ModelView<MarkdownProps, MarkdownModel> {
|
|
|
213
213
|
|
|
214
214
|
// Register HTML patterns that Markdown component can handle
|
|
215
215
|
static registerPatternHandlers(registry: unknown): void {
|
|
216
|
+
const typedRegistry = registry as { hasPattern?: (pattern: string) => boolean; registerPattern?: (pattern: string, handler: (element: Element) => Record<string, unknown>) => void };
|
|
217
|
+
|
|
216
218
|
// Register div elements with specific classes for Markdown transformation
|
|
217
|
-
if (!
|
|
218
|
-
|
|
219
|
+
if (typedRegistry.hasPattern && !typedRegistry.hasPattern('div.markdown-content')) {
|
|
220
|
+
typedRegistry.registerPattern?.('div.markdown-content', Markdown.transformMarkdownDiv);
|
|
219
221
|
}
|
|
220
222
|
|
|
221
223
|
// Register elements with data-markdown attribute
|
|
222
|
-
if (!
|
|
223
|
-
|
|
224
|
+
if (typedRegistry.hasPattern && !typedRegistry.hasPattern('[data-markdown]')) {
|
|
225
|
+
typedRegistry.registerPattern?.('[data-markdown]', Markdown.transformDataMarkdown);
|
|
224
226
|
}
|
|
225
227
|
}
|
|
226
228
|
|
|
@@ -263,9 +265,7 @@ function MarkdownWithDataBinding(props: MarkdownProps) {
|
|
|
263
265
|
// Use data binding
|
|
264
266
|
const { loading, error, ...markdownProps } = useDataBinding<MarkdownModel>(
|
|
265
267
|
dataSource!,
|
|
266
|
-
restProps as Partial<MarkdownModel
|
|
267
|
-
MarkdownModel.getSchema(),
|
|
268
|
-
{ cache: true, cacheTTL: 300000, strict: false, ...bindingOptions }
|
|
268
|
+
restProps as Partial<MarkdownModel>
|
|
269
269
|
);
|
|
270
270
|
|
|
271
271
|
// Show loading state
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
*
|
|
35
35
|
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
36
36
|
*/
|
|
37
|
-
import React, { cloneElement, useState } from 'react';
|
|
37
|
+
import React, { cloneElement, useState, useEffect } from 'react';
|
|
38
38
|
import { DataProvider, ThemeProvider, PrintModeProvider, NavigationProvider, type ThemeMode } from '../contexts';
|
|
39
39
|
import { QwickAppContext, type QwickAppContextValue, type QwickAppProps } from '../contexts/QwickAppContext';
|
|
40
40
|
import { type TemplateResolverConfig } from '../types';
|
|
@@ -126,6 +126,13 @@ export const QwickApp: React.FC<QwickAppComponentProps> = ({
|
|
|
126
126
|
setAppConfig(prev => ({ ...prev, ...updates } as typeof prev));
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
+
// Sync logo prop changes with internal state (for dynamic logo updates)
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (resolvedConfig.logo !== appConfig.logo) {
|
|
132
|
+
setAppConfig(prev => ({ ...prev, logo: resolvedConfig.logo }));
|
|
133
|
+
}
|
|
134
|
+
}, [resolvedConfig.logo]);
|
|
135
|
+
|
|
129
136
|
const contextValue: QwickAppContextValue = {
|
|
130
137
|
appName: resolvedConfig.appName!, // Safe to use ! since we validated above
|
|
131
138
|
appId: resolvedConfig.appId,
|
|
@@ -57,7 +57,7 @@ const ResponsiveMenu: React.FC<ResponsiveMenuProps> = (props) => {
|
|
|
57
57
|
} = restProps;
|
|
58
58
|
|
|
59
59
|
// Mark as QwickApp component
|
|
60
|
-
(ResponsiveMenu
|
|
60
|
+
Object.assign(ResponsiveMenu, { [QWICKAPP_COMPONENT]: true });
|
|
61
61
|
const [screenSize, setScreenSize] = useState<ScreenSize>('desktop');
|
|
62
62
|
const [isNavRailExpanded, setIsNavRailExpanded] = useState(false);
|
|
63
63
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
|
@@ -26,7 +26,7 @@ function SafeSpanView(props: SafeSpanViewProps) {
|
|
|
26
26
|
const { styleProps, htmlProps } = useBaseProps(restProps);
|
|
27
27
|
|
|
28
28
|
// Mark as QwickApp component
|
|
29
|
-
(SafeSpanView
|
|
29
|
+
Object.assign(SafeSpanView, { [QWICKAPP_COMPONENT]: true });
|
|
30
30
|
|
|
31
31
|
// Enhanced HTML sanitization with strict security configuration
|
|
32
32
|
const sanitizeOptions = {
|
|
@@ -95,7 +95,7 @@ function SafeSpanView(props: SafeSpanViewProps) {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
// Main component with data binding support and serialization capability
|
|
98
|
-
export class SafeSpan extends ModelView<SafeSpanProps
|
|
98
|
+
export class SafeSpan extends ModelView<SafeSpanProps> {
|
|
99
99
|
// Component self-declaration for serialization
|
|
100
100
|
static readonly tagName = 'SafeSpan';
|
|
101
101
|
static readonly version = '1.0.0';
|
|
@@ -121,14 +121,16 @@ export class SafeSpan extends ModelView<SafeSpanProps, SafeSpanModel> {
|
|
|
121
121
|
|
|
122
122
|
// Register HTML patterns that SafeSpan component can handle
|
|
123
123
|
static registerPatternHandlers(registry: unknown): void {
|
|
124
|
+
const typedRegistry = registry as { hasPattern?: (pattern: string) => boolean; registerPattern?: (pattern: string, handler: (element: Element) => Record<string, unknown>) => void };
|
|
125
|
+
|
|
124
126
|
// Register span elements with specific classes or attributes
|
|
125
|
-
if (!
|
|
126
|
-
|
|
127
|
+
if (typedRegistry.hasPattern && !typedRegistry.hasPattern('span.safe-content')) {
|
|
128
|
+
typedRegistry.registerPattern?.('span.safe-content', SafeSpan.transformSafeSpan);
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
// Register span elements with data-safe attribute
|
|
130
|
-
if (!
|
|
131
|
-
|
|
132
|
+
if (typedRegistry.hasPattern && !typedRegistry.hasPattern('span[data-safe]')) {
|
|
133
|
+
typedRegistry.registerPattern?.('span[data-safe]', SafeSpan.transformSafeSpan);
|
|
132
134
|
}
|
|
133
135
|
}
|
|
134
136
|
|
|
@@ -153,9 +155,7 @@ function SafeSpanWithDataBinding(props: SafeSpanProps) {
|
|
|
153
155
|
// Use data binding
|
|
154
156
|
const { loading, error, ...safeSpanProps } = useDataBinding<SafeSpanModel>(
|
|
155
157
|
dataSource!,
|
|
156
|
-
restProps as Partial<SafeSpanModel
|
|
157
|
-
SafeSpanModel.getSchema(),
|
|
158
|
-
{ cache: true, cacheTTL: 300000, strict: false, ...bindingOptions }
|
|
158
|
+
restProps as Partial<SafeSpanModel>
|
|
159
159
|
);
|
|
160
160
|
|
|
161
161
|
// Show loading state
|