From aa4962377447c17ebf02f1f614a37ca1cc94341a Mon Sep 17 00:00:00 2001 From: Widle Date: Mon, 1 Jun 2026 16:55:18 +0530 Subject: [PATCH] Added custom loader component --- components/.DS_Store | Bin 12292 -> 12292 bytes components/custom-loader/.DS_Store | Bin 0 -> 6148 bytes components/custom-loader/README.md | 247 ++++++ components/custom-loader/metadata.json | 18 + components/custom-loader/package.json | 49 ++ components/custom-loader/preview.png | Bin 0 -> 39855 bytes .../src/component/LoaderComponent.css | 825 ++++++++++++++++++ .../src/component/LoaderComponent.tsx | 785 +++++++++++++++++ components/custom-loader/src/index.tsx | 1 + 9 files changed, 1925 insertions(+) create mode 100644 components/custom-loader/.DS_Store create mode 100644 components/custom-loader/README.md create mode 100644 components/custom-loader/metadata.json create mode 100644 components/custom-loader/package.json create mode 100644 components/custom-loader/preview.png create mode 100644 components/custom-loader/src/component/LoaderComponent.css create mode 100644 components/custom-loader/src/component/LoaderComponent.tsx create mode 100644 components/custom-loader/src/index.tsx diff --git a/components/.DS_Store b/components/.DS_Store index 346b9cb14fdaaf454c5f42ac571d38d2ff068d15..76548a13eaafef369f2cd95361c6b05ac56578d9 100644 GIT binary patch delta 366 zcmZokXi1nL#l*-lQN~!*o`HdZnSnuofgztEi6N08he4MiZL%Qa;>iX&EUb(`S(eG` z1mCh3dFJFNC*^DwWMpRB%+A5W!Ke%r*VueW=r6M-7efJ1V?IL(P;&`GIYTBxF+&MN zLw*v_2%zbVvOs&*P5vOJvH6aeKIdiy#waF6>CHR}iA?;w49N_oAT2;6av5|dpAwPM zVOS4R{~rv1CNMCdDFWM<$dJO2%1|`7BOgW18G;Ft9gLxIT#oexPiWY&per5$B+XQAPqn~ c*+XX|XF4#vl7J3Lotz*hHTjxyH5PLX0J3XXHvj+t delta 1010 zcmZokXi1nL#l+A)QN~!*iGhKEnSnuofuWQkogtM$mm!%UV`8E5WCI-*Rz{#C%j7?* zmXkdd>^6%rePrFNz{JnEnVo}$gHe5QoVwcP2(=o<$pVaTCkryFZ#EJ5%e*;N#D|kn zYBP^QBGcy0%1;Wei4> zJCx+f(|ADaIB6Q4AU5Wt8wMxm=N16H3F&per5$B>a>vJH>)PQ8JF1^xhBi;&>BB0l!+{9-0}wgaN2Vnhhp zQvBS+c9JJGi2=y+xj6>r0A_58qQ;15_vp}0cAgT&ax}Qa6I+3skwky7OV@sYJ6zei zzpnogEw(3{wq9@AHN8lYyby~w6Wh!_?z!61Ovf9Fz_P-yt7qidyb)lfnXpQ_+&uOhs35>X6zj8 z=%BI^fT+)C6WUr!XiUmjX6zg}LJyEd_1u&JnD prv`;~?-9TcJx300(8rV7^eZ!Vj+#aLEu0t+0V5<-Fz^cuyaP=uLEZoW literal 0 HcmV?d00001 diff --git a/components/custom-loader/README.md b/components/custom-loader/README.md new file mode 100644 index 0000000..273527c --- /dev/null +++ b/components/custom-loader/README.md @@ -0,0 +1,247 @@ +# ⚡ Custom Loader Component (Retool Custom Component) + +A highly customizable loading component for Retool applications featuring multiple loader types, animated progress indicators, skeleton screens, overlay modes, fullscreen loading states, and automatic query progress tracking. + +--- + +## 🚀 Features + +* âģ Multiple loader types +* ðŸŽŊ Progress tracking with percentage indicators +* 📊 Automatic query completion calculation +* ðŸĶī Table, Dashboard, and Form Skeleton loaders +* 🔄 Multiple animated spinner styles +* 📋 Step-by-step workflow visualization +* ðŸ–Ĩ Inline, Overlay, and Fullscreen display modes +* ðŸ’Ą Rotating loading tips +* ✅ Success state handling +* ❌ Error state handling +* 📭 Empty state handling +* ðŸŽĻ Light, Dark, and Auto themes +* ðŸ“ą Fully responsive design +* ⚡ Optimized for Retool applications + +--- + +## ðŸ“Ķ Inputs + +| Name | Type | Description | +| ------------------ | ------- | ------------------------------------------------------------- | +| `loaderStateInput` | String | Current loader state (`loading`, `success`, `error`, `empty`) | +| `loaderType` | String | Loader type to display | +| `theme` | String | Theme mode (`auto`, `light`, `dark`) | +| `overlayMode` | String | Display mode (`inline`, `overlay`, `fullscreen`) | +| `title` | String | Loader title text | +| `subtitle` | String | Loader subtitle text | +| `showProgress` | Boolean | Show progress bar | +| `spinnerStyle` | String | Spinner animation style | +| `progress` | Number | Manual progress value (0-100) | +| `errorMessage` | String | Message displayed for error state | +| `emptyMessage` | String | Message displayed for empty state | +| `tips` | Array | Rotating tips array | +| `steps` | Array | Step objects for step loader | +| `queryStates` | Object | Query completion status object | +| `hideDelay` | Number | Auto-hide delay in milliseconds | + +--- + +## 🎛 Loader Types + +Supported loader types: + +spinner +progress +steps +tableSkeleton +dashboardSkeleton +formSkeleton + + +### Spinner Loader + +Traditional animated loading indicator. + +### Progress Loader + +Displays animated progress bar with percentage. + +### Steps Loader + +Shows workflow progress using completed and pending steps. + +### Table Skeleton + +Simulates table rows while data loads. + +### Dashboard Skeleton + +Simulates dashboard KPIs, charts, and tables. + +### Form Skeleton + +Simulates form fields during loading. + +--- + +## ðŸŽĻ Spinner Styles + +Supported spinner styles: + +circle +dualRing +pulse +bars +ripple +heartbeat +cubeGrid +triangle +wave +dots + + +--- + +## ðŸ–Ĩ Display Modes + +| Mode | Description | +| ------------ | ---------------------------------- | +| `inline` | Displays inside the component area | +| `overlay` | Displays above component content | +| `fullscreen` | Covers the entire viewport | + +--- + +## 📊 Automatic Progress Tracking + +Pass query completion states: + +{ + "usersLoaded": true, + "ordersLoaded": true, + "reportsLoaded": false, + "analyticsLoaded": false +} + + +The component automatically calculates: 50% | completion progress. + +--- + +## 📋 Steps Configuration + +Example steps array: + +[ + { + "label": "Fetch Users", + "completed": true + }, + { + "label": "Load Reports", + "completed": true + }, + { + "label": "Generate Dashboard", + "completed": false + } +] + + +--- + +## ðŸ’Ą Loading Tips + +Example tips array: + +[ + "Loading dashboard data...", + "Fetching latest records...", + "Preparing visualizations...", + "Almost ready..." +] + + +Tips rotate automatically every few seconds. + +--- + +## ðŸ“Ī Outputs + +This component is designed primarily as a visual state component and does not expose output values. + +Progress, state transitions, and visibility are controlled through component inputs. + +--- + +## ðŸ”Ĩ Example Usage + +### Query Loading State + +{{ getUsers.isFetching ? "loading" : "success" }} + + +### Error Handling + +{{ getUsers.error ? "error" : "loading" }} + + +### Empty State + +{{ getUsers.data?.length === 0 ? "empty" : "success" }} + + +### Multi Query Progress + +{ + users: !getUsers.isFetching, + reports: !getReports.isFetching, + analytics: !getAnalytics.isFetching, + dashboard: !getDashboard.isFetching +} + + +--- + +## ðŸŽŊ Use Cases + +* Dashboard Loading Screens +* Analytics Applications +* Report Generation +* API Request Tracking +* Data Synchronization +* Workflow Automation +* Multi-Step Processes +* Table Loading States +* Form Submissions +* Background Data Processing + +--- + +## ⚙ïļ Recommended Configuration + +### Dashboard Loading + +Loader Type: dashboardSkeleton +Display Mode: overlay +Theme: auto + + +### API Progress Tracking + +Loader Type: progress +Show Progress: true + + +### Workflow Processing + +Loader Type: steps +Spinner Style: cubeGrid + + +--- + +## ðŸ‘Ļ‍ðŸ’ŧ Author + +**Widle Studio LLP** + +Built specifically for Retool applications to provide modern, customizable, and production-ready loading experiences. diff --git a/components/custom-loader/metadata.json b/components/custom-loader/metadata.json new file mode 100644 index 0000000..62c296d --- /dev/null +++ b/components/custom-loader/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "custom-loader-component", + "title": "Custom Loader", + "author": "@widlestudiollp", + "shortDescription": "A highly customizable loading component for Retool featuring spinners, progress indicators, skeleton screens, step tracking, overlay and fullscreen modes, dynamic query-state integration, success/error states, and responsive design.", + "tags": [ + "Loader", + "Loading State", + "Progress", + "Skeleton", + "UX", + "Retool", + "Dashboard", + "Spinner", + "Overlay", + "Fullscreen" + ] +} \ No newline at end of file diff --git a/components/custom-loader/package.json b/components/custom-loader/package.json new file mode 100644 index 0000000..8128cf8 --- /dev/null +++ b/components/custom-loader/package.json @@ -0,0 +1,49 @@ +{ + "name": "my-react-app", + "version": "0.1.0", + "private": true, + "dependencies": { + "@tryretool/custom-component-support": "latest", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "uuid": "^13.0.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "scripts": { + "dev": "npx retool-ccl dev", + "deploy": "npx retool-ccl deploy", + "test": "vitest" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@types/react": "^18.2.55", + "@types/react-scroll-to-bottom": "^4.2.5", + "@typescript-eslint/eslint-plugin": "^7.3.1", + "@typescript-eslint/parser": "^7.3.1", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.34.1", + "postcss-modules": "^6.0.0", + "prettier": "^3.0.3", + "vitest": "^4.0.17" + }, + "retoolCustomComponentLibraryConfig": { + "name": "CustomLoader", + "label": "Custom Loader", + "description": "Custom Loader with auto theme and dsplay options with custom loading effects.", + "entryPoint": "src/index.tsx", + "outputPath": "dist" + } +} \ No newline at end of file diff --git a/components/custom-loader/preview.png b/components/custom-loader/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..8869572493b08fd2e40109b413c07cfd07eae8df GIT binary patch literal 39855 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 CA$hd` literal 0 HcmV?d00001 diff --git a/components/custom-loader/src/component/LoaderComponent.css b/components/custom-loader/src/component/LoaderComponent.css new file mode 100644 index 0000000..eba0fb8 --- /dev/null +++ b/components/custom-loader/src/component/LoaderComponent.css @@ -0,0 +1,825 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +.loader-host { + width: 100%; + min-height: 300px; + position: relative; +} + +.loader-root { + width: 100%; + min-width: 100%; + min-height: 100%; + + display: flex; + flex-direction: column; + + overflow: hidden; + + font-family: + Inter, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; +} + +.loader-root.inline { + width: 100%; +} + +.loader-root.overlay { + position: absolute; + inset: 0; + + width: 100%; + height: 100%; + + display: flex; + align-items: center; + justify-content: center; +} + +.loader-root.light.overlay, +.loader-root.auto.overlay { + background: rgba(255, 255, 255, .75); +} + +.loader-root.dark.overlay { + background: rgba(0, 0, 0, .55); +} + +.loader-root.fullscreen { + position: fixed; + + top: 0; + left: 0; + width: 100vw; + height: 100vh; + + z-index: 999999; + + display: flex; + align-items: center; + justify-content: center; + + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} + +.loader-root.light { + --bg: #ffffff; + --bg-secondary: #f8fafc; + --border: #e2e8f0; + --text: #0f172a; + --muted: #64748b; + --primary: #2563eb; + --primary-light: #60a5fa; + --success: #10b981; + --error: #ef4444; + --shadow: + 0 10px 30px rgba(0, 0, 0, 0.08); +} + +.loader-root.dark { + --bg: #111827; + --bg-secondary: #1f2937; + --border: #374151; + --text: #f9fafb; + --muted: #94a3b8; + --primary: #3b82f6; + --primary-light: #60a5fa; + --success: #10b981; + --error: #ef4444; + --shadow: + 0 10px 40px rgba(0, 0, 0, 0.35); +} + +@media (prefers-color-scheme: dark) { + .loader-root.auto { + --bg: #111827; + --bg-secondary: #1f2937; + --border: #374151; + --text: #f9fafb; + --muted: #94a3b8; + --primary: #3b82f6; + --primary-light: #60a5fa; + --success: #10b981; + --error: #ef4444; + } +} + +@media (prefers-color-scheme: light) { + .loader-root.auto { + --bg: #ffffff; + --bg-secondary: #f8fafc; + --border: #e2e8f0; + --text: #0f172a; + --muted: #64748b; + --primary: #2563eb; + --primary-light: #60a5fa; + --success: #10b981; + --error: #ef4444; + } +} + +.loader-container { + width: 100%; + min-height: 100%; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + padding: 24px; +} + +.fullscreen .loader-container { + width: min(720px, 92vw); + + position: absolute; + top: 50%; + left: 50%; + + transform: + translate(-50%, -50%); +} + +.loader-container h2 { + margin: 20px 0 8px; + + font-size: 24px; + font-weight: 700; + + color: var(--text); +} + +.loader-container p { + margin: 0; + + color: var(--muted); + + line-height: 1.6; +} + +.spinner { + width: 72px; + height: 72px; + + border-radius: 50%; + + border: 5px solid transparent; + border-top-color: var(--primary); + + animation: + spinnerRotate 1s linear infinite; + + position: relative; +} + +.spinner::before { + content: ""; + + position: absolute; + inset: 8px; + + border-radius: 50%; + + border: 4px solid transparent; + border-top-color: var(--primary-light); + + animation: + spinnerRotateReverse 1.5s linear infinite; +} + +.spinner.small { + width: 60px; + height: 60px; +} + +.spinner.small::before { + inset: 7px; +} + +@keyframes spinnerRotate { + to { + transform: rotate(360deg); + } +} + +@keyframes spinnerRotateReverse { + to { + transform: rotate(-360deg); + } +} + +.progress-track { + width: min(500px, 100%); + height: 12px; + + margin-top: 24px; + + background: var(--bg-secondary); + + border-radius: 999px; + + overflow: hidden; +} + +.progress-fill { + height: 100%; + + border-radius: inherit; + + background: + linear-gradient(90deg, + var(--primary), + var(--primary-light)); + + transition: + width 0.4s ease; +} + +.progress-value { + margin-top: 12px; + + font-size: 14px; + font-weight: 700; + + color: var(--primary); +} + +.tip-box { + margin-top: 24px; + + width: min(500px, 100%); + + padding: 14px 16px; + + border-radius: 12px; + + background: + rgba(37, 99, 235, 0.08); + + color: var(--muted); + + font-size: 14px; + + animation: + fadeIn 0.3s ease; +} + +.steps-list { + width: min(500px, 100%); + + margin-top: 24px; + + display: flex; + flex-direction: column; + gap: 10px; +} + +.step-row { + display: flex; + align-items: center; + + gap: 12px; + + padding: 12px 16px; + + border-radius: 12px; + + background: var(--bg-secondary); + + color: var(--muted); + + text-align: left; +} + +.step-row.completed { + color: var(--success); + font-weight: 600; +} + +.state-card { + display: flex; + flex-direction: column; + align-items: center; +} + +.success-icon, +.error-icon, +.empty-icon { + width: 96px; + height: 96px; + + display: flex; + align-items: center; + justify-content: center; + + border-radius: 50%; + + font-size: 36px; + font-weight: 700; + + margin-bottom: 20px; +} + +.success-icon { + background: + rgba(16, 185, 129, 0.15); + + color: var(--success); +} + +.error-icon { + background: + rgba(239, 68, 68, 0.15); + + color: var(--error); +} + +.empty-icon { + background: + rgba(100, 116, 139, 0.15); + + color: var(--muted); +} + +.table-skeleton { + width: 100%; + max-width: 900px; + + display: flex; + flex-direction: column; + + gap: 10px; +} + +.table-row { + height: 48px; + + border-radius: 10px; + + background: + linear-gradient(90deg, + var(--bg-secondary) 25%, + rgba(255, 255, 255, 0.4) 50%, + var(--bg-secondary) 75%); + + background-size: 200% 100%; + + animation: + shimmer 1.4s infinite linear; +} + +.form-skeleton { + width: min(650px, 100%); + + display: flex; + flex-direction: column; + + gap: 18px; +} + +.form-line { + height: 52px; + + border-radius: 12px; + + background: + linear-gradient(90deg, + var(--bg-secondary) 25%, + rgba(255, 255, 255, 0.4) 50%, + var(--bg-secondary) 75%); + + background-size: 200% 100%; + + animation: + shimmer 1.4s infinite linear; +} + +.dashboard-skeleton { + width: 100%; + max-width: 1000px; +} + +.kpi-row { + display: grid; + + grid-template-columns: + repeat(4, 1fr); + + gap: 16px; +} + +.kpi-card { + height: 120px; + + border-radius: 16px; + + background: + linear-gradient(90deg, + var(--bg-secondary) 25%, + rgba(255, 255, 255, 0.4) 50%, + var(--bg-secondary) 75%); + + background-size: 200% 100%; + + animation: + shimmer 1.4s infinite linear; +} + +.chart-skeleton { + height: 260px; + + margin-top: 18px; + margin-bottom: 18px; + + border-radius: 18px; + + background: + linear-gradient(90deg, + var(--bg-secondary) 25%, + rgba(255, 255, 255, 0.4) 50%, + var(--bg-secondary) 75%); + + background-size: 200% 100%; + + animation: + shimmer 1.4s infinite linear; +} + +@keyframes shimmer { + 0% { + background-position: + 200% 0; + } + + 100% { + background-position: + -200% 0; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: + translateY(4px); + } + + to { + opacity: 1; + transform: + translateY(0); + } +} + +@media (max-width: 1024px) { + + .kpi-row { + grid-template-columns: + repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + + .loader-container { + padding: 24px; + } + + .loader-container h2 { + font-size: 20px; + } + + .spinner { + width: 64px; + height: 64px; + } + + .success-icon, + .error-icon, + .empty-icon { + width: 80px; + height: 80px; + font-size: 30px; + } + + .chart-skeleton { + height: 200px; + } +} + +@media (max-width: 576px) { + + .loader-container { + padding: 20px; + border-radius: 16px; + } + + .kpi-row { + grid-template-columns: 1fr; + } + + .spinner { + width: 56px; + height: 56px; + } + + .loader-container h2 { + font-size: 18px; + } + + .loader-container p { + font-size: 14px; + } + + .steps-list { + gap: 8px; + } + + .step-row { + padding: 10px 12px; + } + + .chart-skeleton { + height: 160px; + } +} + +.spinner-dual-ring { + width: 72px; + height: 72px; + border: 5px solid transparent; + border-top-color: var(--primary); + border-bottom-color: var(--primary); + border-radius: 50%; + animation: spinnerRotate 1s linear infinite; +} + +.spinner-pulse { + width: 72px; + height: 72px; + border-radius: 50%; + background: var(--primary); + animation: pulse 1s infinite; +} + +@keyframes pulse { + 0% { + transform: scale(.8); + opacity: .6; + } + + 50% { + transform: scale(1); + opacity: 1; + } + + 100% { + transform: scale(.8); + opacity: .6; + } +} + +.spinner-bars { + display: flex; + gap: 4px; + align-items: end; + height: 60px; +} + +.spinner-bars span { + width: 8px; + height: 20px; + background: var(--primary); + animation: bars 1s infinite; +} + +.spinner-bars span:nth-child(2) { + animation-delay: .1s; +} + +.spinner-bars span:nth-child(3) { + animation-delay: .2s; +} + +.spinner-bars span:nth-child(4) { + animation-delay: .3s; +} + +@keyframes bars { + 50% { + height: 60px; + } +} + +.spinner-dual-ring.small, +.spinner-pulse.small { + width: 60px; + height: 60px; +} + +.spinner-ripple { + position: relative; + width: 72px; + height: 72px; +} + +.spinner-ripple div { + position: absolute; + inset: 0; + border: 4px solid var(--primary); + border-radius: 50%; + animation: ripple 1.5s infinite; +} + +.spinner-ripple div:nth-child(2) { + animation-delay: .75s; +} + +@keyframes ripple { + from { + transform: scale(.2); + opacity: 1; + } + + to { + transform: scale(1); + opacity: 0; + } +} + +.spinner-dots { + display: flex; + gap: 8px; +} + +.spinner-dots span { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--primary); + animation: dots .8s infinite alternate; +} + +.spinner-dots span:nth-child(2) { + animation-delay: .2s; +} + +.spinner-dots span:nth-child(3) { + animation-delay: .4s; +} + +@keyframes dots { + to { + transform: translateY(-10px); + } +} + +.spinner-wave { + display: flex; + gap: 4px; + align-items: center; + height: 60px; +} + +.spinner-wave span { + width: 6px; + height: 20px; + background: var(--primary); + animation: wave 1s infinite; +} + +.spinner-wave span:nth-child(2) { + animation-delay: .1s; +} + +.spinner-wave span:nth-child(3) { + animation-delay: .2s; +} + +.spinner-wave span:nth-child(4) { + animation-delay: .3s; +} + +.spinner-wave span:nth-child(5) { + animation-delay: .4s; +} + +@keyframes wave { + 50% { + height: 60px; + } +} + +.spinner-grid { + width: 72px; + height: 72px; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 4px; +} + +.spinner-grid span { + background: var(--primary); + animation: gridPulse 1.2s infinite; +} + +@keyframes gridPulse { + 50% { + opacity: .3; + } +} + +.spinner-triangle { + width: 0; + height: 0; + border-left: 36px solid transparent; + border-right: 36px solid transparent; + border-bottom: 62px solid var(--primary); + animation: spinnerRotate 1s linear infinite; +} + +.spinner-heart { + width: 60px; + height: 60px; + background: var(--primary); + transform: rotate(45deg); + animation: pulse 1s infinite; + position: relative; +} + +.spinner-heart::before, +.spinner-heart::after { + content: ""; + position: absolute; + width: 60px; + height: 60px; + background: var(--primary); + border-radius: 50%; +} + +.spinner-heart::before { + top: -30px; +} + +.spinner-heart::after { + left: -30px; +} + +.spinner-ripple.small, +.spinner-grid.small, +.spinner-heart.small { + width: 60px; + height: 60px; +} + +.spinner-triangle.small { + border-left-width: 30px; + border-right-width: 30px; + border-bottom-width: 52px; +} + +.spinner-grid span:nth-child(1) { + animation-delay: .1s; +} + +.spinner-grid span:nth-child(2) { + animation-delay: .2s; +} + +.spinner-grid span:nth-child(3) { + animation-delay: .3s; +} + +.spinner-grid span:nth-child(4) { + animation-delay: .4s; +} + +.spinner-grid span:nth-child(5) { + animation-delay: .5s; +} + +.spinner-grid span:nth-child(6) { + animation-delay: .6s; +} + +.spinner-grid span:nth-child(7) { + animation-delay: .7s; +} + +.spinner-grid span:nth-child(8) { + animation-delay: .8s; +} + +.spinner-grid span:nth-child(9) { + animation-delay: .9s; +} \ No newline at end of file diff --git a/components/custom-loader/src/component/LoaderComponent.tsx b/components/custom-loader/src/component/LoaderComponent.tsx new file mode 100644 index 0000000..97792a3 --- /dev/null +++ b/components/custom-loader/src/component/LoaderComponent.tsx @@ -0,0 +1,785 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { Retool } from "@tryretool/custom-component-support"; +import "./LoaderComponent.css"; + +type LoaderState = + | "loading" + | "success" + | "error" + | "empty"; + +type LoaderType = + | "spinner" + | "progress" + | "steps" + | "tableSkeleton" + | "dashboardSkeleton" + | "formSkeleton"; + +type SpinnerStyle = + | "circle" + | "dualRing" + | "pulse" + | "bars" + | "ripple" + | "heartbeat" + | "cubeGrid" + | "triangle" + | "wave" + | "dots"; + +interface StepItem { + label: string; + completed?: boolean; +} + +export const LoaderComponent: React.FC = () => { + const [loaderStateInput] = + Retool.useStateString({ + name: "loaderStateInput", + label: "Loader State", + description: + "Bind dynamic value like {{query.isFetching ? 'loading' : 'success'}}", + initialValue: "loading" + }); + + const [loaderType] = + Retool.useStateEnumeration({ + name: "loaderType", + enumDefinition: [ + "spinner", + "progress", + "steps", + "tableSkeleton", + "dashboardSkeleton", + "formSkeleton" + ], + initialValue: "spinner", + enumLabels: { + spinner: "Spinner", + progress: "Progress", + steps: "Steps", + tableSkeleton: "Table Skeleton", + dashboardSkeleton: "Dashboard Skeleton", + formSkeleton: "Form Skeleton" + }, + inspector: "select", + label: "Loader Type", + description: + "Choose loader style" + }); + + const [theme] = Retool.useStateEnumeration({ + name: "theme", + label: "Theme", + description: + "Choose light, dark or auto theme", + inspector: "select", + initialValue: "auto", + enumDefinition: [ + "auto", + "light", + "dark" + ], + enumLabels: { + auto: "Auto", + light: "Light", + dark: "Dark" + } + }); + + const [overlayMode] = Retool.useStateEnumeration({ + name: "overlayMode", + label: "Display Mode", + description: + "Choose how the loader is displayed", + inspector: "select", + initialValue: "inline", + enumDefinition: [ + "inline", + "overlay", + "fullscreen" + ], + enumLabels: { + inline: "Inline", + overlay: "Overlay", + fullscreen: "Fullscreen" + } + }); + + const [title] = Retool.useStateString({ + name: "title", + label: "Title", + description: + "Loader heading text", + initialValue: "Loading Data" + }); + + const [subtitle] = Retool.useStateString({ + name: "subtitle", + label: "Subtitle", + description: + "Loader description text", + initialValue: "Please wait while data is loading..." + }); + + const [showProgress] = Retool.useStateBoolean({ + name: "showProgress", + label: "Show Progress", + description: + "Display progress bar", + inspector: "checkbox", + initialValue: true + }); + + const [spinnerStyle] = + Retool.useStateEnumeration({ + name: "spinnerStyle", + label: "Spinner Style", + description: + "Choose spinner animation style", + initialValue: "circle", + inspector: "select", + enumDefinition: [ + "circle", + "dualRing", + "pulse", + "bars", + "ripple", + "heartbeat", + "cubeGrid", + "triangle", + "wave", + "dots" + ], + enumLabels: { + circle: "Circle Loader", + dualRing: "Dual Ring", + pulse: "Pulse Loader", + bars: "Bars Loader", + ripple: "Ripple Loader", + heartbeat: "Heartbeat", + cubeGrid: "Cube Grid", + triangle: "Triangle", + wave: "Wave Loader", + dots: "Rotating Dots" + } + }); + + const [manualProgress] = + Retool.useStateNumber({ + name: "progress", + label: "Progress", + description: "0 - 100", + inspector: "text", + initialValue: 0 + }); + + const [errorMessage] = Retool.useStateString({ + name: "errorMessage", + label: "Error Message", + description: + "Message shown when state is error", + initialValue: "Failed to load data." + }); + + const [emptyMessage] = Retool.useStateString({ + name: "emptyMessage", + label: "Empty Message", + description: + "Message shown when state is empty", + initialValue: "No records found." + }); + + const [tips] = + Retool.useStateArray({ + name: "tips", + label: "Tips Array", + description: + "Array of rotating tips", + inspector: "text" + }); + + const [steps] = + Retool.useStateArray({ + name: "steps", + label: "Steps", + description: + "Array of step objects", + inspector: "text" + }); + + const [queryStates] = + Retool.useStateObject({ + name: "queryStates", + label: "Query Status Object", + description: + "Object used to calculate progress automatically", + inspector: "text" + }); + + const [hideDelay] = + Retool.useStateNumber({ + name: "hideDelay", + label: "Hide Delay", + description: + "Milliseconds before loader hides", + inspector: "text", + initialValue: 3000 + }); + + Retool.useComponentSettings({ + defaultWidth: 6, + defaultHeight: 8, + }); + + const loaderState = ( + loaderStateInput || "loading" + ).toLowerCase() as LoaderState; + + const spinnerStyleValue = + spinnerStyle as SpinnerStyle; + + const [animatedProgress, setAnimatedProgress] = + useState(0); + + const [loadingStartedAt] = + useState(() => Date.now()); + + const calculatedProgress = + useMemo(() => { + + if ( + queryStates && + typeof queryStates === "object" && + Object.keys(queryStates).length > 0 + ) { + + const total = + Object.keys(queryStates).length; + + const completed = + Object.values(queryStates) + .filter(Boolean).length; + + return Math.round( + (completed / total) * 100 + ); + } + + if ( + manualProgress > 0 + ) { + return Math.max( + 0, + Math.min( + 100, + manualProgress + ) + ); + } + + if ( + loaderState === "loading" + ) { + return animatedProgress; + } + + return 100; + + }, [ + queryStates, + manualProgress, + animatedProgress, + loaderState + ]); + + const [tipIndex, setTipIndex] = + useState(0); + + const [isVisible, setIsVisible] = + useState(true); + + useEffect(() => { + + if ( + !Array.isArray(tips) || + tips.length === 0 + ) return; + + const interval = + setInterval(() => { + + setTipIndex((prev) => + (prev + 1) % tips.length + ); + + }, 3000); + + return () => + clearInterval(interval); + + }, [tips]); + + useEffect(() => { + + if (loaderState !== "loading") { + return; + } + + setAnimatedProgress(5); + + const interval = setInterval(() => { + + setAnimatedProgress(prev => { + + if (prev >= 90) { + return 90; + } + + const increment = + prev < 30 + ? 3 + : prev < 60 + ? 2 + : 1; + + return Math.min( + 90, + prev + increment + ); + + }); + + }, 150); + + return () => + clearInterval(interval); + + }, [loaderState]); + + useEffect(() => { + + if ( + loaderState === "success" || + loaderState === "error" || + loaderState === "empty" + ) { + + const MIN_LOADING_TIME = 1000; + + const elapsed = + Date.now() - loadingStartedAt; + + const remaining = + Math.max( + 0, + MIN_LOADING_TIME - elapsed + ); + + const progressTimer = + setTimeout(() => { + + setAnimatedProgress(100); + + const hideTimer = + setTimeout(() => { + setIsVisible(false); + }, hideDelay); + + (window as any).__loaderHideTimer = + hideTimer; + + }, remaining); + + return () => { + + clearTimeout(progressTimer); + + clearTimeout( + (window as any).__loaderHideTimer + ); + + }; + } + + setIsVisible(true); + + }, [ + loaderState, + hideDelay, + loadingStartedAt + ]); + + const rootClass = useMemo(() => { + + let cls = "loader-root"; + + cls += ` ${theme}`; + + cls += ` ${overlayMode}`; + + return cls; + + }, [theme, overlayMode]); + + const renderSuccess = () => ( +
+ +
+ ✅ +
+ +

Success

+ +

+ Data loaded successfully. +

+ +
+ ); + const renderError = () => ( +
+ +
+ ❌ +
+ +

Error

+ +

{errorMessage}

+ +
+ ); + + const renderEmpty = () => ( +
+ +
+ 📭 +
+ +

No Data

+ +

{emptyMessage}

+ +
+ ); + + const renderSpinner = ( + small = false + ) => { + + const cls = + small + ? "small" + : ""; + + switch (spinnerStyleValue) { + + case "dualRing": + return ( +
+ ); + + case "pulse": + return ( +
+ ); + + case "bars": + return ( +
+ + + + +
+ ); + + case "ripple": + return ( +
+
+
+
+ ); + + case "heartbeat": + return ( +
+ ); + + case "cubeGrid": + return ( +
+ {[...Array(9)].map( + (_, i) => ( + + ) + )} +
+ ); + + case "triangle": + return ( +
+ ); + + case "wave": + return ( +
+ + + + + +
+ ); + + case "dots": + return ( +
+ + + +
+ ); + + default: + return ( +
+ ); + } + }; + + const renderSpinnerLoader = () => ( + <> + {renderSpinner()} + +

{title}

+ +

{subtitle}

+ + {Array.isArray(tips) && + tips.length > 0 && ( +
+ ðŸ’Ą {tips[tipIndex]} +
+ )} + + ); + + const renderProgressLoader = () => ( + <> + {renderSpinner(true)} + +

{title}

+ +

{subtitle}

+ + {showProgress && ( + <> +
+
+
+ +
+ {calculatedProgress}% +
+ + )} + + ); + + const renderStepsLoader = () => { + + const list = + Array.isArray(steps) + ? steps + : []; + + return ( + <> + {renderSpinner(true)} + +

{title}

+ +

{subtitle}

+ +
+ + {list.map( + ( + step: StepItem, + index: number + ) => ( +
+ + {step.completed + ? "✓" + : "○"} + + + + {step.label} + +
+ ) + )} + +
+ + ); + }; + + const renderTableSkeleton = + () => ( +
+ {[...Array(8)].map( + (_, i) => ( +
+ ) + )} +
+ ); + + const renderFormSkeleton = + () => ( +
+ {[...Array(6)].map( + (_, i) => ( +
+ ) + )} +
+ ); + + const renderDashboardSkeleton = + () => ( +
+ +
+ + {[...Array(4)].map( + (_, i) => ( +
+ ) + )} + +
+ +
+ +
+ {[...Array(5)].map( + (_, i) => ( +
+ ) + )} +
+ +
+ ); + + const renderLoadingContent = + () => { + + switch (loaderType) { + + case "progress": + return renderProgressLoader(); + + case "steps": + return renderStepsLoader(); + + case "tableSkeleton": + return renderTableSkeleton(); + + case "dashboardSkeleton": + return renderDashboardSkeleton(); + + case "formSkeleton": + return renderFormSkeleton(); + + default: + return renderSpinnerLoader(); + } + }; + if (!isVisible) { + return null; + } + return ( +
+
+ +
+ + {loaderState === "loading" && + renderLoadingContent()} + + {loaderState === "success" && + renderSuccess()} + + {loaderState === "error" && + renderError()} + + {loaderState === "empty" && + renderEmpty()} + +
+ +
+
+ ); +}; + +export default LoaderComponent; \ No newline at end of file diff --git a/components/custom-loader/src/index.tsx b/components/custom-loader/src/index.tsx new file mode 100644 index 0000000..78ef0d9 --- /dev/null +++ b/components/custom-loader/src/index.tsx @@ -0,0 +1 @@ +export { default as LoaderComponent } from './component/LoaderComponent' \ No newline at end of file