@wakastellar/ui 1.0.12 → 2.1.0
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/README.md +163 -193
- package/dist/charts.cjs.js +1 -0
- package/dist/charts.es.js +16 -0
- package/dist/cli/commands/add.d.ts +7 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/list.d.ts +5 -0
- package/dist/cli/commands/search.d.ts +1 -0
- package/dist/cli/index.cjs +4844 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/utils/config.d.ts +29 -0
- package/dist/cli/utils/logger.d.ts +20 -0
- package/dist/cli/utils/registry.d.ts +23 -0
- package/dist/cn-B-fTneHh.js +1 -0
- package/dist/cn-DzRe1GWm.mjs +21 -0
- package/dist/components/index.d.ts +122 -0
- package/dist/components/waka-3d-pie-chart/index.d.ts +67 -0
- package/dist/components/waka-achievement-unlock/index.d.ts +83 -0
- package/dist/components/waka-activity-feed/index.d.ts +78 -0
- package/dist/components/waka-address-autocomplete/index.d.ts +124 -0
- package/dist/components/waka-alert-stack/index.d.ts +58 -0
- package/dist/components/waka-allocation-matrix/index.d.ts +193 -0
- package/dist/components/waka-approval-chain/index.d.ts +43 -0
- package/dist/components/waka-audit-log/index.d.ts +142 -0
- package/dist/components/waka-badge-showcase/index.d.ts +51 -0
- package/dist/components/waka-biometric-prompt/index.d.ts +84 -0
- package/dist/components/waka-bottom-sheet/index.d.ts +61 -0
- package/dist/components/waka-breadcrumb-path/index.d.ts +46 -0
- package/dist/components/waka-budget-burn/index.d.ts +154 -0
- package/dist/components/waka-capacity-planner/index.d.ts +132 -0
- package/dist/components/waka-cart-summary/index.d.ts +154 -0
- package/dist/components/waka-challenge-timer/index.d.ts +86 -0
- package/dist/components/waka-chat-bubble/index.d.ts +127 -0
- package/dist/components/waka-checklist/index.d.ts +123 -0
- package/dist/components/waka-checkout-stepper/index.d.ts +154 -0
- package/dist/components/waka-cohort-table/index.d.ts +130 -0
- package/dist/components/waka-combo-counter/index.d.ts +53 -0
- package/dist/components/waka-command-bar/index.d.ts +45 -0
- package/dist/components/waka-compare-period/index.d.ts +122 -0
- package/dist/components/waka-connection-matrix/index.d.ts +117 -0
- package/dist/components/waka-contribution-graph/index.d.ts +34 -0
- package/dist/components/waka-cost-breakdown/index.d.ts +50 -0
- package/dist/components/waka-coupon-input/index.d.ts +105 -0
- package/dist/components/waka-credit-card-input/index.d.ts +95 -0
- package/dist/components/waka-daily-reward/index.d.ts +76 -0
- package/dist/components/waka-deployment-lane/index.d.ts +43 -0
- package/dist/components/waka-device-trust/index.d.ts +95 -0
- package/dist/components/waka-dock/index.d.ts +44 -0
- package/dist/components/waka-empty-state/index.d.ts +85 -0
- package/dist/components/waka-error-shake/index.d.ts +49 -0
- package/dist/components/waka-feature-announcement/index.d.ts +112 -0
- package/dist/components/waka-floating-nav/index.d.ts +51 -0
- package/dist/components/waka-flow-diagram/index.d.ts +71 -0
- package/dist/components/waka-funnel-chart/index.d.ts +108 -0
- package/dist/components/waka-glow-card/index.d.ts +32 -0
- package/dist/components/waka-goal-progress/index.d.ts +139 -0
- package/dist/components/waka-haptic-button/index.d.ts +45 -0
- package/dist/components/waka-health-pulse/index.d.ts +28 -0
- package/dist/components/waka-heatmap/index.d.ts +135 -0
- package/dist/components/waka-hotspot/index.d.ts +106 -0
- package/dist/components/waka-incident-timeline/index.d.ts +38 -0
- package/dist/components/waka-invoice-preview/index.d.ts +137 -0
- package/dist/components/waka-kpi-dashboard/index.d.ts +80 -0
- package/dist/components/waka-leaderboard/index.d.ts +85 -0
- package/dist/components/waka-level-progress/index.d.ts +89 -0
- package/dist/components/waka-liquid-button/index.d.ts +41 -0
- package/dist/components/waka-loading-orbit/index.d.ts +90 -0
- package/dist/components/waka-loot-box/index.d.ts +87 -0
- package/dist/components/waka-magic-link/index.d.ts +34 -0
- package/dist/components/waka-magnetic-button/index.d.ts +56 -0
- package/dist/components/waka-mention-input/index.d.ts +106 -0
- package/dist/components/waka-metric-sparkline/index.d.ts +46 -0
- package/dist/components/waka-milestone-road/index.d.ts +91 -0
- package/dist/components/waka-morph-button/index.d.ts +62 -0
- package/dist/components/waka-network-topology/index.d.ts +35 -0
- package/dist/components/waka-orbital-menu/index.d.ts +61 -0
- package/dist/components/waka-order-tracker/index.d.ts +121 -0
- package/dist/components/waka-password-strength/index.d.ts +98 -0
- package/dist/components/waka-payment-method-picker/index.d.ts +88 -0
- package/dist/components/waka-permission-matrix/index.d.ts +197 -0
- package/dist/components/waka-phone-input/index.d.ts +93 -0
- package/dist/components/waka-pipeline-view/index.d.ts +49 -0
- package/dist/components/waka-player-card/index.d.ts +36 -0
- package/dist/components/waka-points-popup/index.d.ts +75 -0
- package/dist/components/waka-power-up/index.d.ts +103 -0
- package/dist/components/waka-presence-indicator/index.d.ts +188 -0
- package/dist/components/waka-pricing-table/index.d.ts +77 -0
- package/dist/components/waka-product-card/index.d.ts +81 -0
- package/dist/components/waka-progress-onboarding/index.d.ts +97 -0
- package/dist/components/waka-pull-to-refresh/index.d.ts +45 -0
- package/dist/components/waka-quest-card/index.d.ts +110 -0
- package/dist/components/waka-quota-bar/index.d.ts +100 -0
- package/dist/components/waka-radar-score/index.d.ts +95 -0
- package/dist/components/waka-rank-badge/index.d.ts +58 -0
- package/dist/components/waka-rating-input/index.d.ts +110 -0
- package/dist/components/waka-reaction-picker/index.d.ts +77 -0
- package/dist/components/waka-region-map/index.d.ts +27 -0
- package/dist/components/waka-resource-gauge/index.d.ts +78 -0
- package/dist/components/waka-resource-pool/index.d.ts +81 -0
- package/dist/components/waka-rollback-slider/index.d.ts +79 -0
- package/dist/components/waka-sankey-diagram/index.d.ts +120 -0
- package/dist/components/waka-schedule-picker/index.d.ts +100 -0
- package/dist/components/waka-scratch-card/index.d.ts +87 -0
- package/dist/components/waka-season-pass/index.d.ts +65 -0
- package/dist/components/waka-security-score/index.d.ts +124 -0
- package/dist/components/waka-server-rack/index.d.ts +44 -0
- package/dist/components/waka-session-manager/index.d.ts +116 -0
- package/dist/components/waka-signature-pad/index.d.ts +87 -0
- package/dist/components/waka-skeleton-wave/index.d.ts +79 -0
- package/dist/components/waka-skill-tree/index.d.ts +78 -0
- package/dist/components/waka-sla-tracker/index.d.ts +65 -0
- package/dist/components/waka-slider-range/index.d.ts +88 -0
- package/dist/components/waka-spin-wheel/index.d.ts +51 -0
- package/dist/components/waka-spotlight/index.d.ts +47 -0
- package/dist/components/waka-stats-hexagon/index.d.ts +149 -0
- package/dist/components/waka-status-matrix/index.d.ts +38 -0
- package/dist/components/waka-streak-counter/index.d.ts +27 -0
- package/dist/components/waka-success-explosion/index.d.ts +51 -0
- package/dist/components/waka-swipe-card/index.d.ts +64 -0
- package/dist/components/waka-tabs-morph/index.d.ts +66 -0
- package/dist/components/waka-tag-input/index.d.ts +134 -0
- package/dist/components/waka-team-banner/index.d.ts +122 -0
- package/dist/components/waka-terminal-output/index.d.ts +48 -0
- package/dist/components/waka-thread-view/index.d.ts +101 -0
- package/dist/components/waka-tilt-card/index.d.ts +36 -0
- package/dist/components/waka-tooltip-tour/index.d.ts +118 -0
- package/dist/components/waka-tour-guide/index.d.ts +122 -0
- package/dist/components/waka-tournament-bracket/index.d.ts +101 -0
- package/dist/components/waka-treemap-chart/index.d.ts +104 -0
- package/dist/components/waka-two-factor-setup/index.d.ts +93 -0
- package/dist/components/waka-typewriter/index.d.ts +98 -0
- package/dist/components/waka-typing-indicator/index.d.ts +64 -0
- package/dist/components/waka-versus-card/index.d.ts +117 -0
- package/dist/components/waka-video-call/index.d.ts +170 -0
- package/dist/components/waka-voice-message/index.d.ts +117 -0
- package/dist/components/waka-welcome-modal/index.d.ts +120 -0
- package/dist/components/waka-xp-bar/index.d.ts +54 -0
- package/dist/export.cjs.js +1 -0
- package/dist/export.d.ts +3 -1
- package/dist/export.es.js +5 -0
- package/dist/index-B9GTFkji.js +1 -0
- package/dist/index-c0jcWyEL.mjs +466 -0
- package/dist/index.cjs.js +2530 -22
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +72081 -19512
- package/dist/rich-text.cjs.js +1 -0
- package/dist/rich-text.es.js +4 -0
- package/dist/types-BOWIoR7j.mjs +1111 -0
- package/dist/types-D2yCJ91P.js +1 -0
- package/dist/useDataTableImport-D8R2HQl6.mjs +229 -0
- package/dist/useDataTableImport-S_hhA5Wo.js +9 -0
- package/package.json +70 -22
- package/src/blocks/activity-timeline/index.tsx +586 -0
- package/src/blocks/calendar-view/index.tsx +756 -0
- package/src/blocks/chat/index.tsx +1018 -0
- package/src/blocks/chat/widget.tsx +504 -0
- package/src/blocks/dashboard/index.tsx +522 -0
- package/src/blocks/empty-states/index.tsx +452 -0
- package/src/blocks/error-pages/index.tsx +426 -0
- package/src/blocks/faq/index.tsx +479 -0
- package/src/blocks/file-manager/index.tsx +890 -0
- package/src/blocks/footer/index.tsx +133 -0
- package/src/blocks/header/index.tsx +357 -0
- package/src/blocks/headtab/index.tsx +139 -0
- package/src/blocks/i18n-editor/index.tsx +1016 -0
- package/src/blocks/index.ts +80 -0
- package/src/blocks/kanban-board/index.tsx +779 -0
- package/src/blocks/landing/index.tsx +677 -0
- package/src/blocks/language-selector/index.tsx +88 -0
- package/src/blocks/layout/index.tsx +159 -0
- package/src/blocks/login/index.tsx +339 -0
- package/src/blocks/login/types.ts +131 -0
- package/src/blocks/pricing/index.tsx +564 -0
- package/src/blocks/profile/index.tsx +746 -0
- package/src/blocks/settings/index.tsx +558 -0
- package/src/blocks/sidebar/index.tsx +713 -0
- package/src/blocks/theme-creator-block/index.tsx +835 -0
- package/src/blocks/user-management/index.tsx +1037 -0
- package/src/blocks/wizard/index.tsx +719 -0
- package/src/components/DataTable/DataTable.tsx +406 -0
- package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
- package/src/components/DataTable/DataTableBody.tsx +216 -0
- package/src/components/DataTable/DataTableCell.tsx +172 -0
- package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
- package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
- package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
- package/src/components/DataTable/DataTableEditCell.tsx +279 -0
- package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
- package/src/components/DataTable/DataTableFilters.tsx +535 -0
- package/src/components/DataTable/DataTableGrouping.tsx +147 -0
- package/src/components/DataTable/DataTableHeader.tsx +172 -0
- package/src/components/DataTable/DataTablePagination.tsx +125 -0
- package/src/components/DataTable/DataTableSelection.tsx +269 -0
- package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
- package/src/components/DataTable/DataTableToolbar.tsx +262 -0
- package/src/components/DataTable/README.md +446 -0
- package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
- package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
- package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
- package/src/components/DataTable/examples/EditExample.tsx +166 -0
- package/src/components/DataTable/formatters/index.ts +335 -0
- package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
- package/src/components/DataTable/hooks/useDataTable.ts +145 -0
- package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
- package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
- package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
- package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
- package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
- package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
- package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
- package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
- package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
- package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
- package/src/components/DataTable/index.ts +81 -0
- package/src/components/DataTable/services/IndexedDBService.ts +504 -0
- package/src/components/DataTable/templates/index.tsx +803 -0
- package/src/components/DataTable/types.ts +504 -0
- package/src/components/DataTable/utils.ts +164 -0
- package/src/components/DataTable/workers/exportWorker.ts +213 -0
- package/src/components/accordion/index.tsx +61 -0
- package/src/components/alert/index.tsx +61 -0
- package/src/components/alert-dialog/index.tsx +146 -0
- package/src/components/aspect-ratio/index.tsx +12 -0
- package/src/components/avatar/index.tsx +54 -0
- package/src/components/badge/Badge.stories.tsx +64 -0
- package/src/components/badge/index.tsx +38 -0
- package/src/components/button/Button.stories.tsx +173 -0
- package/src/components/button/index.tsx +56 -0
- package/src/components/calendar/index.tsx +73 -0
- package/src/components/card/index.tsx +78 -0
- package/src/components/checkbox/index.tsx +34 -0
- package/src/components/code/index.tsx +229 -0
- package/src/components/collapsible/index.tsx +16 -0
- package/src/components/command/index.tsx +162 -0
- package/src/components/context-menu/index.tsx +204 -0
- package/src/components/dialog/index.tsx +126 -0
- package/src/components/dropdown-menu/index.tsx +204 -0
- package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
- package/src/components/error-boundary/index.ts +7 -0
- package/src/components/form/index.tsx +183 -0
- package/src/components/hover-card/index.tsx +33 -0
- package/src/components/index.ts +368 -0
- package/src/components/input/Input.stories.tsx +100 -0
- package/src/components/input/index.tsx +27 -0
- package/src/components/input-otp/index.tsx +277 -0
- package/src/components/label/index.tsx +30 -0
- package/src/components/language-selector/index.tsx +341 -0
- package/src/components/menubar/index.tsx +240 -0
- package/src/components/navigation-menu/index.tsx +134 -0
- package/src/components/popover/index.tsx +35 -0
- package/src/components/progress/index.tsx +32 -0
- package/src/components/radio-group/index.tsx +48 -0
- package/src/components/scroll-area/index.tsx +52 -0
- package/src/components/select/index.tsx +164 -0
- package/src/components/separator/index.tsx +35 -0
- package/src/components/sheet/index.tsx +147 -0
- package/src/components/skeleton/index.tsx +22 -0
- package/src/components/slider/index.tsx +32 -0
- package/src/components/switch/index.tsx +33 -0
- package/src/components/table/index.tsx +117 -0
- package/src/components/tabs/index.tsx +59 -0
- package/src/components/textarea/index.tsx +30 -0
- package/src/components/theme-selector/index.tsx +327 -0
- package/src/components/toast/index.tsx +133 -0
- package/src/components/toaster/index.tsx +34 -0
- package/src/components/toggle/index.tsx +49 -0
- package/src/components/tooltip/index.tsx +34 -0
- package/src/components/typography/index.tsx +276 -0
- package/src/components/waka-3d-pie-chart/index.tsx +486 -0
- package/src/components/waka-achievement-unlock/index.tsx +716 -0
- package/src/components/waka-activity-feed/index.tsx +686 -0
- package/src/components/waka-address-autocomplete/index.tsx +1202 -0
- package/src/components/waka-admincrumb/index.tsx +349 -0
- package/src/components/waka-alert-stack/index.tsx +827 -0
- package/src/components/waka-allocation-matrix/index.tsx +1278 -0
- package/src/components/waka-approval-chain/index.tsx +766 -0
- package/src/components/waka-audit-log/index.tsx +1475 -0
- package/src/components/waka-autocomplete/index.tsx +358 -0
- package/src/components/waka-badge-showcase/index.tsx +704 -0
- package/src/components/waka-barcode/index.tsx +260 -0
- package/src/components/waka-biometric-prompt/index.tsx +765 -0
- package/src/components/waka-bottom-sheet/index.tsx +495 -0
- package/src/components/waka-breadcrumb/index.tsx +376 -0
- package/src/components/waka-breadcrumb-path/index.tsx +513 -0
- package/src/components/waka-budget-burn/index.tsx +1234 -0
- package/src/components/waka-capacity-planner/index.tsx +1107 -0
- package/src/components/waka-carousel/index.tsx +893 -0
- package/src/components/waka-cart-summary/index.tsx +1055 -0
- package/src/components/waka-challenge-timer/index.tsx +1044 -0
- package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
- package/src/components/waka-charts/WakaBarChart.tsx +222 -0
- package/src/components/waka-charts/WakaChart.tsx +124 -0
- package/src/components/waka-charts/WakaLineChart.tsx +219 -0
- package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
- package/src/components/waka-charts/WakaPieChart.tsx +214 -0
- package/src/components/waka-charts/WakaSparkline.tsx +229 -0
- package/src/components/waka-charts/dataTableHelpers.ts +109 -0
- package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
- package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
- package/src/components/waka-charts/index.ts +90 -0
- package/src/components/waka-charts/types.ts +330 -0
- package/src/components/waka-chat-bubble/index.tsx +1060 -0
- package/src/components/waka-checklist/index.tsx +1067 -0
- package/src/components/waka-checkout-stepper/index.tsx +976 -0
- package/src/components/waka-cohort-table/index.tsx +1011 -0
- package/src/components/waka-color-picker/index.tsx +447 -0
- package/src/components/waka-combo-counter/index.tsx +864 -0
- package/src/components/waka-combobox/index.tsx +497 -0
- package/src/components/waka-command-bar/index.tsx +403 -0
- package/src/components/waka-compare-period/index.tsx +1230 -0
- package/src/components/waka-connection-matrix/index.tsx +1053 -0
- package/src/components/waka-contribution-graph/index.tsx +552 -0
- package/src/components/waka-cost-breakdown/index.tsx +1065 -0
- package/src/components/waka-coupon-input/index.tsx +592 -0
- package/src/components/waka-credit-card-input/index.tsx +982 -0
- package/src/components/waka-daily-reward/index.tsx +762 -0
- package/src/components/waka-date-range-picker/index.tsx +378 -0
- package/src/components/waka-datetime-picker/index.tsx +793 -0
- package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
- package/src/components/waka-deployment-lane/index.tsx +673 -0
- package/src/components/waka-device-trust/index.tsx +1259 -0
- package/src/components/waka-dock/index.tsx +285 -0
- package/src/components/waka-drawer/index.tsx +319 -0
- package/src/components/waka-empty-state/index.tsx +545 -0
- package/src/components/waka-error-shake/index.tsx +398 -0
- package/src/components/waka-feature-announcement/index.tsx +991 -0
- package/src/components/waka-file-upload/index.tsx +437 -0
- package/src/components/waka-floating-nav/index.tsx +413 -0
- package/src/components/waka-flow-diagram/index.tsx +508 -0
- package/src/components/waka-funnel-chart/index.tsx +823 -0
- package/src/components/waka-glow-card/index.tsx +246 -0
- package/src/components/waka-goal-progress/index.tsx +1025 -0
- package/src/components/waka-haptic-button/index.tsx +388 -0
- package/src/components/waka-health-pulse/index.tsx +451 -0
- package/src/components/waka-heatmap/index.tsx +1026 -0
- package/src/components/waka-hotspot/index.tsx +682 -0
- package/src/components/waka-image/index.tsx +373 -0
- package/src/components/waka-incident-timeline/index.tsx +686 -0
- package/src/components/waka-invoice-preview/index.tsx +829 -0
- package/src/components/waka-kanban/index.tsx +646 -0
- package/src/components/waka-kpi-dashboard/index.tsx +755 -0
- package/src/components/waka-leaderboard/index.tsx +746 -0
- package/src/components/waka-level-progress/index.tsx +665 -0
- package/src/components/waka-liquid-button/index.tsx +520 -0
- package/src/components/waka-loading-orbit/index.tsx +478 -0
- package/src/components/waka-loot-box/index.tsx +1091 -0
- package/src/components/waka-magic-link/index.tsx +321 -0
- package/src/components/waka-magnetic-button/index.tsx +567 -0
- package/src/components/waka-mention-input/index.tsx +953 -0
- package/src/components/waka-metric-sparkline/index.tsx +627 -0
- package/src/components/waka-milestone-road/index.tsx +1064 -0
- package/src/components/waka-modal/index.tsx +374 -0
- package/src/components/waka-morph-button/index.tsx +495 -0
- package/src/components/waka-network-topology/index.tsx +801 -0
- package/src/components/waka-notifications/index.tsx +414 -0
- package/src/components/waka-number-input/index.tsx +373 -0
- package/src/components/waka-orbital-menu/index.tsx +445 -0
- package/src/components/waka-order-tracker/index.tsx +1041 -0
- package/src/components/waka-pagination/index.tsx +393 -0
- package/src/components/waka-password-strength/index.tsx +824 -0
- package/src/components/waka-payment-method-picker/index.tsx +715 -0
- package/src/components/waka-permission-matrix/index.tsx +1302 -0
- package/src/components/waka-phone-input/index.tsx +801 -0
- package/src/components/waka-pipeline-view/index.tsx +604 -0
- package/src/components/waka-player-card/index.tsx +691 -0
- package/src/components/waka-points-popup/index.tsx +366 -0
- package/src/components/waka-power-up/index.tsx +1155 -0
- package/src/components/waka-presence-indicator/index.tsx +1181 -0
- package/src/components/waka-pricing-table/index.tsx +755 -0
- package/src/components/waka-product-card/index.tsx +786 -0
- package/src/components/waka-progress-onboarding/index.tsx +878 -0
- package/src/components/waka-pull-to-refresh/index.tsx +451 -0
- package/src/components/waka-qrcode/index.tsx +232 -0
- package/src/components/waka-quest-card/index.tsx +1275 -0
- package/src/components/waka-quota-bar/index.tsx +693 -0
- package/src/components/waka-radar-score/index.tsx +512 -0
- package/src/components/waka-rank-badge/index.tsx +813 -0
- package/src/components/waka-rating-input/index.tsx +560 -0
- package/src/components/waka-reaction-picker/index.tsx +1062 -0
- package/src/components/waka-region-map/index.tsx +730 -0
- package/src/components/waka-resource-gauge/index.tsx +654 -0
- package/src/components/waka-resource-pool/index.tsx +1035 -0
- package/src/components/waka-rich-text-editor/index.tsx +594 -0
- package/src/components/waka-rollback-slider/index.tsx +891 -0
- package/src/components/waka-sankey-diagram/index.tsx +1032 -0
- package/src/components/waka-schedule-picker/index.tsx +1060 -0
- package/src/components/waka-scratch-card/index.tsx +914 -0
- package/src/components/waka-season-pass/index.tsx +886 -0
- package/src/components/waka-security-score/index.tsx +1126 -0
- package/src/components/waka-segmented-control/index.tsx +238 -0
- package/src/components/waka-server-rack/index.tsx +764 -0
- package/src/components/waka-session-manager/index.tsx +815 -0
- package/src/components/waka-signature-pad/index.tsx +744 -0
- package/src/components/waka-skeleton-wave/index.tsx +454 -0
- package/src/components/waka-skill-tree/index.tsx +1031 -0
- package/src/components/waka-sla-tracker/index.tsx +798 -0
- package/src/components/waka-slider-range/index.tsx +765 -0
- package/src/components/waka-spin-wheel/index.tsx +671 -0
- package/src/components/waka-spinner/index.tsx +284 -0
- package/src/components/waka-spotlight/index.tsx +410 -0
- package/src/components/waka-stat/index.tsx +428 -0
- package/src/components/waka-stats-hexagon/index.tsx +824 -0
- package/src/components/waka-status-matrix/index.tsx +565 -0
- package/src/components/waka-stepper/index.tsx +489 -0
- package/src/components/waka-streak-counter/index.tsx +334 -0
- package/src/components/waka-success-explosion/index.tsx +453 -0
- package/src/components/waka-swipe-card/index.tsx +574 -0
- package/src/components/waka-tabs-morph/index.tsx +509 -0
- package/src/components/waka-tag-input/index.tsx +877 -0
- package/src/components/waka-team-banner/index.tsx +1183 -0
- package/src/components/waka-terminal-output/index.tsx +836 -0
- package/src/components/waka-theme-creator/index.tsx +762 -0
- package/src/components/waka-theme-manager/index.tsx +654 -0
- package/src/components/waka-thread-view/index.tsx +874 -0
- package/src/components/waka-tilt-card/index.tsx +250 -0
- package/src/components/waka-time-picker/index.tsx +479 -0
- package/src/components/waka-timeline/index.tsx +385 -0
- package/src/components/waka-tooltip-tour/index.tsx +855 -0
- package/src/components/waka-tour-guide/index.tsx +920 -0
- package/src/components/waka-tournament-bracket/index.tsx +1276 -0
- package/src/components/waka-tree/index.tsx +557 -0
- package/src/components/waka-treemap-chart/index.tsx +1031 -0
- package/src/components/waka-two-factor-setup/index.tsx +995 -0
- package/src/components/waka-typewriter/index.tsx +566 -0
- package/src/components/waka-typing-indicator/index.tsx +649 -0
- package/src/components/waka-versus-card/index.tsx +1026 -0
- package/src/components/waka-video/index.tsx +557 -0
- package/src/components/waka-video-call/index.tsx +1087 -0
- package/src/components/waka-virtual-list/index.tsx +327 -0
- package/src/components/waka-voice-message/index.tsx +1019 -0
- package/src/components/waka-welcome-modal/index.tsx +790 -0
- package/src/components/waka-xp-bar/index.tsx +799 -0
- package/src/styles/base.css +108 -0
- package/src/styles/code-highlight.css +82 -86
- package/src/styles/globals-v3.css +9 -0
- package/src/styles/globals.css +57 -74
- package/src/styles/tailwind.preset.js +69 -0
|
@@ -0,0 +1,1202 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import {
|
|
5
|
+
MapPin,
|
|
6
|
+
Search,
|
|
7
|
+
X,
|
|
8
|
+
Clock,
|
|
9
|
+
Navigation,
|
|
10
|
+
Loader2,
|
|
11
|
+
ChevronDown,
|
|
12
|
+
Building2,
|
|
13
|
+
Home,
|
|
14
|
+
Edit3,
|
|
15
|
+
} from "lucide-react"
|
|
16
|
+
import { cn } from "../../utils/cn"
|
|
17
|
+
|
|
18
|
+
// ============================================
|
|
19
|
+
// TYPES
|
|
20
|
+
// ============================================
|
|
21
|
+
|
|
22
|
+
export interface AddressParts {
|
|
23
|
+
/** Street address (e.g., "123 Main St") */
|
|
24
|
+
street: string
|
|
25
|
+
/** City name */
|
|
26
|
+
city: string
|
|
27
|
+
/** State/Province/Region */
|
|
28
|
+
state?: string
|
|
29
|
+
/** Postal/ZIP code */
|
|
30
|
+
zip: string
|
|
31
|
+
/** Country name */
|
|
32
|
+
country: string
|
|
33
|
+
/** Full formatted address */
|
|
34
|
+
formatted: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AddressSuggestion {
|
|
38
|
+
/** Unique identifier */
|
|
39
|
+
id: string
|
|
40
|
+
/** Primary address text */
|
|
41
|
+
primary: string
|
|
42
|
+
/** Secondary text (city, state, etc.) */
|
|
43
|
+
secondary: string
|
|
44
|
+
/** Parsed address parts */
|
|
45
|
+
parts: AddressParts
|
|
46
|
+
/** Address type */
|
|
47
|
+
type?: "home" | "business" | "other"
|
|
48
|
+
/** Distance from user (if location available) */
|
|
49
|
+
distance?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RecentAddress extends AddressSuggestion {
|
|
53
|
+
/** Timestamp of last use */
|
|
54
|
+
lastUsed: Date
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface WakaAddressAutocompleteProps {
|
|
58
|
+
/** Selected address value */
|
|
59
|
+
value?: AddressSuggestion | null
|
|
60
|
+
/** Callback when address is selected */
|
|
61
|
+
onChange?: (address: AddressSuggestion | null) => void
|
|
62
|
+
/** Callback for search queries (for custom/API implementations) */
|
|
63
|
+
onSearch?: (query: string) => Promise<AddressSuggestion[]>
|
|
64
|
+
/** Placeholder text */
|
|
65
|
+
placeholder?: string
|
|
66
|
+
/** Enable recent addresses feature */
|
|
67
|
+
showRecentAddresses?: boolean
|
|
68
|
+
/** Recent addresses list */
|
|
69
|
+
recentAddresses?: RecentAddress[]
|
|
70
|
+
/** Callback when address is added to recents */
|
|
71
|
+
onAddToRecent?: (address: AddressSuggestion) => void
|
|
72
|
+
/** Enable manual entry fallback */
|
|
73
|
+
allowManualEntry?: boolean
|
|
74
|
+
/** Callback when manual entry is requested */
|
|
75
|
+
onManualEntry?: () => void
|
|
76
|
+
/** Debounce delay in milliseconds */
|
|
77
|
+
debounceMs?: number
|
|
78
|
+
/** Minimum characters before searching */
|
|
79
|
+
minChars?: number
|
|
80
|
+
/** Maximum number of suggestions to show */
|
|
81
|
+
maxSuggestions?: number
|
|
82
|
+
/** No results message */
|
|
83
|
+
noResultsMessage?: string
|
|
84
|
+
/** Loading message */
|
|
85
|
+
loadingMessage?: string
|
|
86
|
+
/** Disabled state */
|
|
87
|
+
disabled?: boolean
|
|
88
|
+
/** Error state */
|
|
89
|
+
error?: string
|
|
90
|
+
/** Additional CSS classes */
|
|
91
|
+
className?: string
|
|
92
|
+
/** Input ID for accessibility */
|
|
93
|
+
id?: string
|
|
94
|
+
/** Input name for forms */
|
|
95
|
+
name?: string
|
|
96
|
+
/** Label text */
|
|
97
|
+
label?: string
|
|
98
|
+
/** Required field */
|
|
99
|
+
required?: boolean
|
|
100
|
+
/** Auto focus on mount */
|
|
101
|
+
autoFocus?: boolean
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface UseAddressAutocompleteOptions {
|
|
105
|
+
/** Custom search function */
|
|
106
|
+
onSearch?: (query: string) => Promise<AddressSuggestion[]>
|
|
107
|
+
/** Debounce delay in milliseconds */
|
|
108
|
+
debounceMs?: number
|
|
109
|
+
/** Minimum characters before searching */
|
|
110
|
+
minChars?: number
|
|
111
|
+
/** Maximum number of suggestions */
|
|
112
|
+
maxSuggestions?: number
|
|
113
|
+
/** Initial recent addresses */
|
|
114
|
+
initialRecentAddresses?: RecentAddress[]
|
|
115
|
+
/** Max recent addresses to store */
|
|
116
|
+
maxRecentAddresses?: number
|
|
117
|
+
/** Storage key for recent addresses */
|
|
118
|
+
storageKey?: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface UseAddressAutocompleteReturn {
|
|
122
|
+
/** Current search query */
|
|
123
|
+
query: string
|
|
124
|
+
/** Set search query */
|
|
125
|
+
setQuery: (query: string) => void
|
|
126
|
+
/** Suggestions list */
|
|
127
|
+
suggestions: AddressSuggestion[]
|
|
128
|
+
/** Loading state */
|
|
129
|
+
isLoading: boolean
|
|
130
|
+
/** Error state */
|
|
131
|
+
error: string | null
|
|
132
|
+
/** Recent addresses */
|
|
133
|
+
recentAddresses: RecentAddress[]
|
|
134
|
+
/** Add address to recents */
|
|
135
|
+
addToRecent: (address: AddressSuggestion) => void
|
|
136
|
+
/** Clear recent addresses */
|
|
137
|
+
clearRecentAddresses: () => void
|
|
138
|
+
/** Remove specific recent address */
|
|
139
|
+
removeFromRecent: (id: string) => void
|
|
140
|
+
/** Search function */
|
|
141
|
+
search: (query: string) => Promise<void>
|
|
142
|
+
/** Clear suggestions */
|
|
143
|
+
clearSuggestions: () => void
|
|
144
|
+
/** Parse address string to parts */
|
|
145
|
+
parseAddress: (addressString: string) => AddressParts
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================
|
|
149
|
+
// MOCK DATA & UTILITIES
|
|
150
|
+
// ============================================
|
|
151
|
+
|
|
152
|
+
const MOCK_ADDRESSES: AddressSuggestion[] = [
|
|
153
|
+
{
|
|
154
|
+
id: "1",
|
|
155
|
+
primary: "123 Main Street",
|
|
156
|
+
secondary: "New York, NY 10001, USA",
|
|
157
|
+
type: "home",
|
|
158
|
+
parts: {
|
|
159
|
+
street: "123 Main Street",
|
|
160
|
+
city: "New York",
|
|
161
|
+
state: "NY",
|
|
162
|
+
zip: "10001",
|
|
163
|
+
country: "USA",
|
|
164
|
+
formatted: "123 Main Street, New York, NY 10001, USA",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "2",
|
|
169
|
+
primary: "456 Oak Avenue",
|
|
170
|
+
secondary: "Los Angeles, CA 90001, USA",
|
|
171
|
+
type: "business",
|
|
172
|
+
parts: {
|
|
173
|
+
street: "456 Oak Avenue",
|
|
174
|
+
city: "Los Angeles",
|
|
175
|
+
state: "CA",
|
|
176
|
+
zip: "90001",
|
|
177
|
+
country: "USA",
|
|
178
|
+
formatted: "456 Oak Avenue, Los Angeles, CA 90001, USA",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: "3",
|
|
183
|
+
primary: "789 Pine Road",
|
|
184
|
+
secondary: "Chicago, IL 60601, USA",
|
|
185
|
+
type: "home",
|
|
186
|
+
parts: {
|
|
187
|
+
street: "789 Pine Road",
|
|
188
|
+
city: "Chicago",
|
|
189
|
+
state: "IL",
|
|
190
|
+
zip: "60601",
|
|
191
|
+
country: "USA",
|
|
192
|
+
formatted: "789 Pine Road, Chicago, IL 60601, USA",
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: "4",
|
|
197
|
+
primary: "321 Elm Boulevard",
|
|
198
|
+
secondary: "Houston, TX 77001, USA",
|
|
199
|
+
type: "other",
|
|
200
|
+
parts: {
|
|
201
|
+
street: "321 Elm Boulevard",
|
|
202
|
+
city: "Houston",
|
|
203
|
+
state: "TX",
|
|
204
|
+
zip: "77001",
|
|
205
|
+
country: "USA",
|
|
206
|
+
formatted: "321 Elm Boulevard, Houston, TX 77001, USA",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: "5",
|
|
211
|
+
primary: "555 Cedar Lane",
|
|
212
|
+
secondary: "Phoenix, AZ 85001, USA",
|
|
213
|
+
type: "home",
|
|
214
|
+
parts: {
|
|
215
|
+
street: "555 Cedar Lane",
|
|
216
|
+
city: "Phoenix",
|
|
217
|
+
state: "AZ",
|
|
218
|
+
zip: "85001",
|
|
219
|
+
country: "USA",
|
|
220
|
+
formatted: "555 Cedar Lane, Phoenix, AZ 85001, USA",
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: "6",
|
|
225
|
+
primary: "10 Downing Street",
|
|
226
|
+
secondary: "London, SW1A 2AA, UK",
|
|
227
|
+
type: "business",
|
|
228
|
+
parts: {
|
|
229
|
+
street: "10 Downing Street",
|
|
230
|
+
city: "London",
|
|
231
|
+
state: "",
|
|
232
|
+
zip: "SW1A 2AA",
|
|
233
|
+
country: "UK",
|
|
234
|
+
formatted: "10 Downing Street, London, SW1A 2AA, UK",
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: "7",
|
|
239
|
+
primary: "1600 Pennsylvania Avenue",
|
|
240
|
+
secondary: "Washington, DC 20500, USA",
|
|
241
|
+
type: "business",
|
|
242
|
+
parts: {
|
|
243
|
+
street: "1600 Pennsylvania Avenue",
|
|
244
|
+
city: "Washington",
|
|
245
|
+
state: "DC",
|
|
246
|
+
zip: "20500",
|
|
247
|
+
country: "USA",
|
|
248
|
+
formatted: "1600 Pennsylvania Avenue, Washington, DC 20500, USA",
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: "8",
|
|
253
|
+
primary: "350 Fifth Avenue",
|
|
254
|
+
secondary: "New York, NY 10118, USA",
|
|
255
|
+
type: "business",
|
|
256
|
+
distance: "2.3 mi",
|
|
257
|
+
parts: {
|
|
258
|
+
street: "350 Fifth Avenue",
|
|
259
|
+
city: "New York",
|
|
260
|
+
state: "NY",
|
|
261
|
+
zip: "10118",
|
|
262
|
+
country: "USA",
|
|
263
|
+
formatted: "350 Fifth Avenue, New York, NY 10118, USA",
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Parse a raw address string into structured parts
|
|
270
|
+
*/
|
|
271
|
+
function parseAddressString(addressString: string): AddressParts {
|
|
272
|
+
// Simple parsing logic - in production, use a proper address parser
|
|
273
|
+
const parts = addressString.split(",").map((p) => p.trim())
|
|
274
|
+
|
|
275
|
+
const street = parts[0] || ""
|
|
276
|
+
const cityStateZip = parts[1] || ""
|
|
277
|
+
const country = parts[parts.length - 1] || ""
|
|
278
|
+
|
|
279
|
+
// Try to extract state and zip from cityStateZip
|
|
280
|
+
const stateZipMatch = cityStateZip.match(/^(.+?)\s+([A-Z]{2})\s+(\d{5}(?:-\d{4})?)$/)
|
|
281
|
+
|
|
282
|
+
let city = ""
|
|
283
|
+
let state = ""
|
|
284
|
+
let zip = ""
|
|
285
|
+
|
|
286
|
+
if (stateZipMatch) {
|
|
287
|
+
city = stateZipMatch[1]
|
|
288
|
+
state = stateZipMatch[2]
|
|
289
|
+
zip = stateZipMatch[3]
|
|
290
|
+
} else {
|
|
291
|
+
city = cityStateZip
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
street,
|
|
296
|
+
city,
|
|
297
|
+
state,
|
|
298
|
+
zip,
|
|
299
|
+
country: country !== city ? country : "",
|
|
300
|
+
formatted: addressString,
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Mock search function for demonstration
|
|
306
|
+
*/
|
|
307
|
+
async function mockSearchAddresses(
|
|
308
|
+
query: string,
|
|
309
|
+
maxResults: number = 5
|
|
310
|
+
): Promise<AddressSuggestion[]> {
|
|
311
|
+
// Simulate network delay
|
|
312
|
+
await new Promise((resolve) => setTimeout(resolve, 300 + Math.random() * 200))
|
|
313
|
+
|
|
314
|
+
if (!query.trim()) return []
|
|
315
|
+
|
|
316
|
+
const queryLower = query.toLowerCase()
|
|
317
|
+
|
|
318
|
+
const results = MOCK_ADDRESSES.filter(
|
|
319
|
+
(addr) =>
|
|
320
|
+
addr.primary.toLowerCase().includes(queryLower) ||
|
|
321
|
+
addr.secondary.toLowerCase().includes(queryLower) ||
|
|
322
|
+
addr.parts.city.toLowerCase().includes(queryLower) ||
|
|
323
|
+
addr.parts.zip.includes(query)
|
|
324
|
+
).slice(0, maxResults)
|
|
325
|
+
|
|
326
|
+
return results
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ============================================
|
|
330
|
+
// HOOK: useAddressAutocomplete
|
|
331
|
+
// ============================================
|
|
332
|
+
|
|
333
|
+
export function useAddressAutocomplete(
|
|
334
|
+
options: UseAddressAutocompleteOptions = {}
|
|
335
|
+
): UseAddressAutocompleteReturn {
|
|
336
|
+
const {
|
|
337
|
+
onSearch,
|
|
338
|
+
debounceMs = 300,
|
|
339
|
+
minChars = 2,
|
|
340
|
+
maxSuggestions = 5,
|
|
341
|
+
initialRecentAddresses = [],
|
|
342
|
+
maxRecentAddresses = 5,
|
|
343
|
+
storageKey = "waka-address-recent",
|
|
344
|
+
} = options
|
|
345
|
+
|
|
346
|
+
const [query, setQuery] = React.useState("")
|
|
347
|
+
const [suggestions, setSuggestions] = React.useState<AddressSuggestion[]>([])
|
|
348
|
+
const [isLoading, setIsLoading] = React.useState(false)
|
|
349
|
+
const [error, setError] = React.useState<string | null>(null)
|
|
350
|
+
const [recentAddresses, setRecentAddresses] = React.useState<RecentAddress[]>(
|
|
351
|
+
initialRecentAddresses
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
const debounceTimerRef = React.useRef<NodeJS.Timeout | null>(null)
|
|
355
|
+
const abortControllerRef = React.useRef<AbortController | null>(null)
|
|
356
|
+
|
|
357
|
+
// Load recent addresses from storage on mount
|
|
358
|
+
React.useEffect(() => {
|
|
359
|
+
if (typeof window === "undefined") return
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const stored = localStorage.getItem(storageKey)
|
|
363
|
+
if (stored) {
|
|
364
|
+
const parsed = JSON.parse(stored) as RecentAddress[]
|
|
365
|
+
setRecentAddresses(
|
|
366
|
+
parsed.map((addr) => ({
|
|
367
|
+
...addr,
|
|
368
|
+
lastUsed: new Date(addr.lastUsed),
|
|
369
|
+
}))
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
// Ignore storage errors
|
|
374
|
+
}
|
|
375
|
+
}, [storageKey])
|
|
376
|
+
|
|
377
|
+
// Save recent addresses to storage
|
|
378
|
+
const saveRecentAddresses = React.useCallback(
|
|
379
|
+
(addresses: RecentAddress[]) => {
|
|
380
|
+
if (typeof window === "undefined") return
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
localStorage.setItem(storageKey, JSON.stringify(addresses))
|
|
384
|
+
} catch {
|
|
385
|
+
// Ignore storage errors
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
[storageKey]
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
// Search function
|
|
392
|
+
const search = React.useCallback(
|
|
393
|
+
async (searchQuery: string) => {
|
|
394
|
+
if (searchQuery.length < minChars) {
|
|
395
|
+
setSuggestions([])
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Cancel previous request
|
|
400
|
+
if (abortControllerRef.current) {
|
|
401
|
+
abortControllerRef.current.abort()
|
|
402
|
+
}
|
|
403
|
+
abortControllerRef.current = new AbortController()
|
|
404
|
+
|
|
405
|
+
setIsLoading(true)
|
|
406
|
+
setError(null)
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
const searchFn = onSearch || mockSearchAddresses
|
|
410
|
+
const results = await searchFn(searchQuery)
|
|
411
|
+
setSuggestions(results.slice(0, maxSuggestions))
|
|
412
|
+
} catch (err) {
|
|
413
|
+
if (err instanceof Error && err.name !== "AbortError") {
|
|
414
|
+
setError("Failed to search addresses. Please try again.")
|
|
415
|
+
setSuggestions([])
|
|
416
|
+
}
|
|
417
|
+
} finally {
|
|
418
|
+
setIsLoading(false)
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
[onSearch, minChars, maxSuggestions]
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
// Debounced search effect
|
|
425
|
+
React.useEffect(() => {
|
|
426
|
+
if (debounceTimerRef.current) {
|
|
427
|
+
clearTimeout(debounceTimerRef.current)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (query.length >= minChars) {
|
|
431
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
432
|
+
search(query)
|
|
433
|
+
}, debounceMs)
|
|
434
|
+
} else {
|
|
435
|
+
setSuggestions([])
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return () => {
|
|
439
|
+
if (debounceTimerRef.current) {
|
|
440
|
+
clearTimeout(debounceTimerRef.current)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}, [query, debounceMs, minChars, search])
|
|
444
|
+
|
|
445
|
+
// Add to recent addresses
|
|
446
|
+
const addToRecent = React.useCallback(
|
|
447
|
+
(address: AddressSuggestion) => {
|
|
448
|
+
setRecentAddresses((prev) => {
|
|
449
|
+
const filtered = prev.filter((addr) => addr.id !== address.id)
|
|
450
|
+
const newRecent: RecentAddress = {
|
|
451
|
+
...address,
|
|
452
|
+
lastUsed: new Date(),
|
|
453
|
+
}
|
|
454
|
+
const updated = [newRecent, ...filtered].slice(0, maxRecentAddresses)
|
|
455
|
+
saveRecentAddresses(updated)
|
|
456
|
+
return updated
|
|
457
|
+
})
|
|
458
|
+
},
|
|
459
|
+
[maxRecentAddresses, saveRecentAddresses]
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
// Clear recent addresses
|
|
463
|
+
const clearRecentAddresses = React.useCallback(() => {
|
|
464
|
+
setRecentAddresses([])
|
|
465
|
+
saveRecentAddresses([])
|
|
466
|
+
}, [saveRecentAddresses])
|
|
467
|
+
|
|
468
|
+
// Remove specific recent address
|
|
469
|
+
const removeFromRecent = React.useCallback(
|
|
470
|
+
(id: string) => {
|
|
471
|
+
setRecentAddresses((prev) => {
|
|
472
|
+
const updated = prev.filter((addr) => addr.id !== id)
|
|
473
|
+
saveRecentAddresses(updated)
|
|
474
|
+
return updated
|
|
475
|
+
})
|
|
476
|
+
},
|
|
477
|
+
[saveRecentAddresses]
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
// Clear suggestions
|
|
481
|
+
const clearSuggestions = React.useCallback(() => {
|
|
482
|
+
setSuggestions([])
|
|
483
|
+
setQuery("")
|
|
484
|
+
}, [])
|
|
485
|
+
|
|
486
|
+
// Parse address utility
|
|
487
|
+
const parseAddress = React.useCallback((addressString: string): AddressParts => {
|
|
488
|
+
return parseAddressString(addressString)
|
|
489
|
+
}, [])
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
query,
|
|
493
|
+
setQuery,
|
|
494
|
+
suggestions,
|
|
495
|
+
isLoading,
|
|
496
|
+
error,
|
|
497
|
+
recentAddresses,
|
|
498
|
+
addToRecent,
|
|
499
|
+
clearRecentAddresses,
|
|
500
|
+
removeFromRecent,
|
|
501
|
+
search,
|
|
502
|
+
clearSuggestions,
|
|
503
|
+
parseAddress,
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ============================================
|
|
508
|
+
// CSS ANIMATIONS
|
|
509
|
+
// ============================================
|
|
510
|
+
|
|
511
|
+
const dropdownAnimationStyles = `
|
|
512
|
+
@keyframes waka-address-dropdown-in {
|
|
513
|
+
from {
|
|
514
|
+
opacity: 0;
|
|
515
|
+
transform: translateY(-8px) scale(0.96);
|
|
516
|
+
}
|
|
517
|
+
to {
|
|
518
|
+
opacity: 1;
|
|
519
|
+
transform: translateY(0) scale(1);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
@keyframes waka-address-dropdown-out {
|
|
524
|
+
from {
|
|
525
|
+
opacity: 1;
|
|
526
|
+
transform: translateY(0) scale(1);
|
|
527
|
+
}
|
|
528
|
+
to {
|
|
529
|
+
opacity: 0;
|
|
530
|
+
transform: translateY(-8px) scale(0.96);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
@keyframes waka-address-highlight {
|
|
535
|
+
0% {
|
|
536
|
+
background-color: transparent;
|
|
537
|
+
}
|
|
538
|
+
50% {
|
|
539
|
+
background-color: hsl(var(--primary) / 0.1);
|
|
540
|
+
}
|
|
541
|
+
100% {
|
|
542
|
+
background-color: transparent;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
@keyframes waka-address-pulse {
|
|
547
|
+
0%, 100% {
|
|
548
|
+
opacity: 1;
|
|
549
|
+
}
|
|
550
|
+
50% {
|
|
551
|
+
opacity: 0.5;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.waka-address-dropdown-enter {
|
|
556
|
+
animation: waka-address-dropdown-in 0.2s ease-out forwards;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.waka-address-dropdown-exit {
|
|
560
|
+
animation: waka-address-dropdown-out 0.15s ease-in forwards;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.waka-address-highlight {
|
|
564
|
+
animation: waka-address-highlight 0.5s ease-out;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.waka-address-pulse {
|
|
568
|
+
animation: waka-address-pulse 1.5s ease-in-out infinite;
|
|
569
|
+
}
|
|
570
|
+
`
|
|
571
|
+
|
|
572
|
+
// ============================================
|
|
573
|
+
// SUB-COMPONENTS
|
|
574
|
+
// ============================================
|
|
575
|
+
|
|
576
|
+
interface SuggestionItemProps {
|
|
577
|
+
suggestion: AddressSuggestion
|
|
578
|
+
isHighlighted: boolean
|
|
579
|
+
onSelect: () => void
|
|
580
|
+
onMouseEnter: () => void
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function SuggestionItem({
|
|
584
|
+
suggestion,
|
|
585
|
+
isHighlighted,
|
|
586
|
+
onSelect,
|
|
587
|
+
onMouseEnter,
|
|
588
|
+
}: SuggestionItemProps) {
|
|
589
|
+
const Icon =
|
|
590
|
+
suggestion.type === "home"
|
|
591
|
+
? Home
|
|
592
|
+
: suggestion.type === "business"
|
|
593
|
+
? Building2
|
|
594
|
+
: MapPin
|
|
595
|
+
|
|
596
|
+
return (
|
|
597
|
+
<button
|
|
598
|
+
type="button"
|
|
599
|
+
role="option"
|
|
600
|
+
aria-selected={isHighlighted}
|
|
601
|
+
className={cn(
|
|
602
|
+
"w-full flex items-start gap-3 px-3 py-2.5 text-left transition-colors",
|
|
603
|
+
"hover:bg-accent focus:bg-accent focus:outline-none",
|
|
604
|
+
isHighlighted && "bg-accent"
|
|
605
|
+
)}
|
|
606
|
+
onClick={onSelect}
|
|
607
|
+
onMouseEnter={onMouseEnter}
|
|
608
|
+
>
|
|
609
|
+
<div
|
|
610
|
+
className={cn(
|
|
611
|
+
"flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center",
|
|
612
|
+
"bg-primary/10 text-primary"
|
|
613
|
+
)}
|
|
614
|
+
>
|
|
615
|
+
<Icon className="w-4 h-4" />
|
|
616
|
+
</div>
|
|
617
|
+
<div className="flex-1 min-w-0">
|
|
618
|
+
<p className="text-sm font-medium text-foreground truncate">
|
|
619
|
+
{suggestion.primary}
|
|
620
|
+
</p>
|
|
621
|
+
<p className="text-xs text-muted-foreground truncate">
|
|
622
|
+
{suggestion.secondary}
|
|
623
|
+
</p>
|
|
624
|
+
</div>
|
|
625
|
+
{suggestion.distance && (
|
|
626
|
+
<span className="flex-shrink-0 text-xs text-muted-foreground">
|
|
627
|
+
{suggestion.distance}
|
|
628
|
+
</span>
|
|
629
|
+
)}
|
|
630
|
+
</button>
|
|
631
|
+
)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
interface RecentAddressItemProps {
|
|
635
|
+
address: RecentAddress
|
|
636
|
+
isHighlighted: boolean
|
|
637
|
+
onSelect: () => void
|
|
638
|
+
onRemove: () => void
|
|
639
|
+
onMouseEnter: () => void
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function RecentAddressItem({
|
|
643
|
+
address,
|
|
644
|
+
isHighlighted,
|
|
645
|
+
onSelect,
|
|
646
|
+
onRemove,
|
|
647
|
+
onMouseEnter,
|
|
648
|
+
}: RecentAddressItemProps) {
|
|
649
|
+
return (
|
|
650
|
+
<div
|
|
651
|
+
role="option"
|
|
652
|
+
aria-selected={isHighlighted}
|
|
653
|
+
className={cn(
|
|
654
|
+
"flex items-center gap-2 px-3 py-2 transition-colors",
|
|
655
|
+
"hover:bg-accent",
|
|
656
|
+
isHighlighted && "bg-accent"
|
|
657
|
+
)}
|
|
658
|
+
onMouseEnter={onMouseEnter}
|
|
659
|
+
>
|
|
660
|
+
<button
|
|
661
|
+
type="button"
|
|
662
|
+
className="flex-1 flex items-start gap-3 text-left focus:outline-none"
|
|
663
|
+
onClick={onSelect}
|
|
664
|
+
>
|
|
665
|
+
<div className="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center bg-muted text-muted-foreground">
|
|
666
|
+
<Clock className="w-4 h-4" />
|
|
667
|
+
</div>
|
|
668
|
+
<div className="flex-1 min-w-0">
|
|
669
|
+
<p className="text-sm font-medium text-foreground truncate">
|
|
670
|
+
{address.primary}
|
|
671
|
+
</p>
|
|
672
|
+
<p className="text-xs text-muted-foreground truncate">
|
|
673
|
+
{address.secondary}
|
|
674
|
+
</p>
|
|
675
|
+
</div>
|
|
676
|
+
</button>
|
|
677
|
+
<button
|
|
678
|
+
type="button"
|
|
679
|
+
className="flex-shrink-0 p-1.5 rounded-md hover:bg-destructive/10 hover:text-destructive transition-colors"
|
|
680
|
+
onClick={(e) => {
|
|
681
|
+
e.stopPropagation()
|
|
682
|
+
onRemove()
|
|
683
|
+
}}
|
|
684
|
+
aria-label="Remove from recent"
|
|
685
|
+
>
|
|
686
|
+
<X className="w-3.5 h-3.5" />
|
|
687
|
+
</button>
|
|
688
|
+
</div>
|
|
689
|
+
)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
interface SelectedAddressDisplayProps {
|
|
693
|
+
address: AddressSuggestion
|
|
694
|
+
onClear: () => void
|
|
695
|
+
onEdit: () => void
|
|
696
|
+
disabled?: boolean
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function SelectedAddressDisplay({
|
|
700
|
+
address,
|
|
701
|
+
onClear,
|
|
702
|
+
onEdit,
|
|
703
|
+
disabled,
|
|
704
|
+
}: SelectedAddressDisplayProps) {
|
|
705
|
+
const Icon =
|
|
706
|
+
address.type === "home"
|
|
707
|
+
? Home
|
|
708
|
+
: address.type === "business"
|
|
709
|
+
? Building2
|
|
710
|
+
: MapPin
|
|
711
|
+
|
|
712
|
+
return (
|
|
713
|
+
<div
|
|
714
|
+
className={cn(
|
|
715
|
+
"flex items-start gap-3 p-3 rounded-lg border bg-card",
|
|
716
|
+
"transition-all duration-200",
|
|
717
|
+
disabled && "opacity-50 pointer-events-none"
|
|
718
|
+
)}
|
|
719
|
+
>
|
|
720
|
+
<div className="flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center bg-primary/10 text-primary">
|
|
721
|
+
<Icon className="w-5 h-5" />
|
|
722
|
+
</div>
|
|
723
|
+
<div className="flex-1 min-w-0">
|
|
724
|
+
<p className="text-sm font-medium text-foreground">{address.primary}</p>
|
|
725
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
726
|
+
{address.secondary}
|
|
727
|
+
</p>
|
|
728
|
+
<div className="flex flex-wrap gap-1.5 mt-2">
|
|
729
|
+
{address.parts.city && (
|
|
730
|
+
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-muted text-muted-foreground">
|
|
731
|
+
{address.parts.city}
|
|
732
|
+
</span>
|
|
733
|
+
)}
|
|
734
|
+
{address.parts.zip && (
|
|
735
|
+
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-muted text-muted-foreground">
|
|
736
|
+
{address.parts.zip}
|
|
737
|
+
</span>
|
|
738
|
+
)}
|
|
739
|
+
{address.parts.country && (
|
|
740
|
+
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-muted text-muted-foreground">
|
|
741
|
+
{address.parts.country}
|
|
742
|
+
</span>
|
|
743
|
+
)}
|
|
744
|
+
</div>
|
|
745
|
+
</div>
|
|
746
|
+
<div className="flex-shrink-0 flex items-center gap-1">
|
|
747
|
+
<button
|
|
748
|
+
type="button"
|
|
749
|
+
className="p-1.5 rounded-md hover:bg-accent transition-colors"
|
|
750
|
+
onClick={onEdit}
|
|
751
|
+
aria-label="Edit address"
|
|
752
|
+
>
|
|
753
|
+
<Edit3 className="w-4 h-4 text-muted-foreground" />
|
|
754
|
+
</button>
|
|
755
|
+
<button
|
|
756
|
+
type="button"
|
|
757
|
+
className="p-1.5 rounded-md hover:bg-destructive/10 hover:text-destructive transition-colors"
|
|
758
|
+
onClick={onClear}
|
|
759
|
+
aria-label="Clear address"
|
|
760
|
+
>
|
|
761
|
+
<X className="w-4 h-4" />
|
|
762
|
+
</button>
|
|
763
|
+
</div>
|
|
764
|
+
</div>
|
|
765
|
+
)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// ============================================
|
|
769
|
+
// MAIN COMPONENT
|
|
770
|
+
// ============================================
|
|
771
|
+
|
|
772
|
+
export const WakaAddressAutocomplete = React.forwardRef<
|
|
773
|
+
HTMLInputElement,
|
|
774
|
+
WakaAddressAutocompleteProps
|
|
775
|
+
>(
|
|
776
|
+
(
|
|
777
|
+
{
|
|
778
|
+
value,
|
|
779
|
+
onChange,
|
|
780
|
+
onSearch,
|
|
781
|
+
placeholder = "Search for an address...",
|
|
782
|
+
showRecentAddresses = true,
|
|
783
|
+
recentAddresses: externalRecentAddresses,
|
|
784
|
+
onAddToRecent,
|
|
785
|
+
allowManualEntry = true,
|
|
786
|
+
onManualEntry,
|
|
787
|
+
debounceMs = 300,
|
|
788
|
+
minChars = 2,
|
|
789
|
+
maxSuggestions = 5,
|
|
790
|
+
noResultsMessage = "No addresses found",
|
|
791
|
+
loadingMessage = "Searching...",
|
|
792
|
+
disabled = false,
|
|
793
|
+
error,
|
|
794
|
+
className,
|
|
795
|
+
id,
|
|
796
|
+
name,
|
|
797
|
+
label,
|
|
798
|
+
required = false,
|
|
799
|
+
autoFocus = false,
|
|
800
|
+
},
|
|
801
|
+
ref
|
|
802
|
+
) => {
|
|
803
|
+
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
804
|
+
const dropdownRef = React.useRef<HTMLDivElement>(null)
|
|
805
|
+
const [isOpen, setIsOpen] = React.useState(false)
|
|
806
|
+
const [highlightedIndex, setHighlightedIndex] = React.useState(-1)
|
|
807
|
+
const [isEditing, setIsEditing] = React.useState(!value)
|
|
808
|
+
|
|
809
|
+
// Use the hook
|
|
810
|
+
const {
|
|
811
|
+
query,
|
|
812
|
+
setQuery,
|
|
813
|
+
suggestions,
|
|
814
|
+
isLoading,
|
|
815
|
+
error: searchError,
|
|
816
|
+
recentAddresses: hookRecentAddresses,
|
|
817
|
+
addToRecent,
|
|
818
|
+
removeFromRecent,
|
|
819
|
+
} = useAddressAutocomplete({
|
|
820
|
+
onSearch,
|
|
821
|
+
debounceMs,
|
|
822
|
+
minChars,
|
|
823
|
+
maxSuggestions,
|
|
824
|
+
})
|
|
825
|
+
|
|
826
|
+
const recentAddresses = externalRecentAddresses ?? hookRecentAddresses
|
|
827
|
+
const displayError = error || searchError
|
|
828
|
+
|
|
829
|
+
// Combined ref
|
|
830
|
+
React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement)
|
|
831
|
+
|
|
832
|
+
// Inject styles
|
|
833
|
+
React.useEffect(() => {
|
|
834
|
+
if (typeof document === "undefined") return
|
|
835
|
+
|
|
836
|
+
const styleId = "waka-address-autocomplete-styles"
|
|
837
|
+
if (!document.getElementById(styleId)) {
|
|
838
|
+
const style = document.createElement("style")
|
|
839
|
+
style.id = styleId
|
|
840
|
+
style.textContent = dropdownAnimationStyles
|
|
841
|
+
document.head.appendChild(style)
|
|
842
|
+
}
|
|
843
|
+
}, [])
|
|
844
|
+
|
|
845
|
+
// Calculate what to show in dropdown
|
|
846
|
+
const showRecent =
|
|
847
|
+
showRecentAddresses &&
|
|
848
|
+
recentAddresses.length > 0 &&
|
|
849
|
+
query.length < minChars
|
|
850
|
+
const showSuggestions = suggestions.length > 0 && query.length >= minChars
|
|
851
|
+
const showNoResults =
|
|
852
|
+
!isLoading &&
|
|
853
|
+
query.length >= minChars &&
|
|
854
|
+
suggestions.length === 0
|
|
855
|
+
const showContent = showRecent || showSuggestions || showNoResults || isLoading
|
|
856
|
+
|
|
857
|
+
// All items for keyboard navigation
|
|
858
|
+
const allItems = React.useMemo(() => {
|
|
859
|
+
if (showRecent) {
|
|
860
|
+
return recentAddresses.map((addr) => ({
|
|
861
|
+
type: "recent" as const,
|
|
862
|
+
item: addr,
|
|
863
|
+
}))
|
|
864
|
+
}
|
|
865
|
+
if (showSuggestions) {
|
|
866
|
+
return suggestions.map((addr) => ({
|
|
867
|
+
type: "suggestion" as const,
|
|
868
|
+
item: addr,
|
|
869
|
+
}))
|
|
870
|
+
}
|
|
871
|
+
return []
|
|
872
|
+
}, [showRecent, showSuggestions, recentAddresses, suggestions])
|
|
873
|
+
|
|
874
|
+
// Handle selection
|
|
875
|
+
const handleSelect = React.useCallback(
|
|
876
|
+
(address: AddressSuggestion) => {
|
|
877
|
+
onChange?.(address)
|
|
878
|
+
addToRecent(address)
|
|
879
|
+
onAddToRecent?.(address)
|
|
880
|
+
setQuery("")
|
|
881
|
+
setIsOpen(false)
|
|
882
|
+
setIsEditing(false)
|
|
883
|
+
setHighlightedIndex(-1)
|
|
884
|
+
},
|
|
885
|
+
[onChange, addToRecent, onAddToRecent, setQuery]
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
// Handle clear
|
|
889
|
+
const handleClear = React.useCallback(() => {
|
|
890
|
+
onChange?.(null)
|
|
891
|
+
setQuery("")
|
|
892
|
+
setIsEditing(true)
|
|
893
|
+
inputRef.current?.focus()
|
|
894
|
+
}, [onChange, setQuery])
|
|
895
|
+
|
|
896
|
+
// Handle edit
|
|
897
|
+
const handleEdit = React.useCallback(() => {
|
|
898
|
+
setIsEditing(true)
|
|
899
|
+
setQuery(value?.parts.formatted || "")
|
|
900
|
+
setTimeout(() => inputRef.current?.focus(), 0)
|
|
901
|
+
}, [value, setQuery])
|
|
902
|
+
|
|
903
|
+
// Handle input change
|
|
904
|
+
const handleInputChange = React.useCallback(
|
|
905
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
906
|
+
setQuery(e.target.value)
|
|
907
|
+
setHighlightedIndex(-1)
|
|
908
|
+
if (!isOpen) {
|
|
909
|
+
setIsOpen(true)
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
[setQuery, isOpen]
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
// Handle focus
|
|
916
|
+
const handleFocus = React.useCallback(() => {
|
|
917
|
+
setIsOpen(true)
|
|
918
|
+
}, [])
|
|
919
|
+
|
|
920
|
+
// Handle blur
|
|
921
|
+
const handleBlur = React.useCallback((e: React.FocusEvent) => {
|
|
922
|
+
// Don't close if clicking inside dropdown
|
|
923
|
+
if (
|
|
924
|
+
dropdownRef.current &&
|
|
925
|
+
dropdownRef.current.contains(e.relatedTarget as Node)
|
|
926
|
+
) {
|
|
927
|
+
return
|
|
928
|
+
}
|
|
929
|
+
// Delay to allow click events to fire
|
|
930
|
+
setTimeout(() => setIsOpen(false), 150)
|
|
931
|
+
}, [])
|
|
932
|
+
|
|
933
|
+
// Handle keyboard navigation
|
|
934
|
+
const handleKeyDown = React.useCallback(
|
|
935
|
+
(e: React.KeyboardEvent) => {
|
|
936
|
+
if (!isOpen) {
|
|
937
|
+
if (e.key === "ArrowDown" || e.key === "Enter") {
|
|
938
|
+
setIsOpen(true)
|
|
939
|
+
e.preventDefault()
|
|
940
|
+
}
|
|
941
|
+
return
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
switch (e.key) {
|
|
945
|
+
case "ArrowDown":
|
|
946
|
+
e.preventDefault()
|
|
947
|
+
setHighlightedIndex((prev) =>
|
|
948
|
+
prev < allItems.length - 1 ? prev + 1 : prev
|
|
949
|
+
)
|
|
950
|
+
break
|
|
951
|
+
case "ArrowUp":
|
|
952
|
+
e.preventDefault()
|
|
953
|
+
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : -1))
|
|
954
|
+
break
|
|
955
|
+
case "Enter":
|
|
956
|
+
e.preventDefault()
|
|
957
|
+
if (highlightedIndex >= 0 && allItems[highlightedIndex]) {
|
|
958
|
+
handleSelect(allItems[highlightedIndex].item)
|
|
959
|
+
}
|
|
960
|
+
break
|
|
961
|
+
case "Escape":
|
|
962
|
+
e.preventDefault()
|
|
963
|
+
setIsOpen(false)
|
|
964
|
+
setHighlightedIndex(-1)
|
|
965
|
+
break
|
|
966
|
+
case "Tab":
|
|
967
|
+
setIsOpen(false)
|
|
968
|
+
setHighlightedIndex(-1)
|
|
969
|
+
break
|
|
970
|
+
}
|
|
971
|
+
},
|
|
972
|
+
[isOpen, allItems, highlightedIndex, handleSelect]
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
// Handle manual entry click
|
|
976
|
+
const handleManualEntryClick = React.useCallback(() => {
|
|
977
|
+
onManualEntry?.()
|
|
978
|
+
setIsOpen(false)
|
|
979
|
+
}, [onManualEntry])
|
|
980
|
+
|
|
981
|
+
// If we have a selected value and not editing, show the display
|
|
982
|
+
if (value && !isEditing) {
|
|
983
|
+
return (
|
|
984
|
+
<div className={cn("space-y-1.5", className)}>
|
|
985
|
+
{label && (
|
|
986
|
+
<label
|
|
987
|
+
htmlFor={id}
|
|
988
|
+
className="text-sm font-medium text-foreground"
|
|
989
|
+
>
|
|
990
|
+
{label}
|
|
991
|
+
{required && <span className="text-destructive ml-1">*</span>}
|
|
992
|
+
</label>
|
|
993
|
+
)}
|
|
994
|
+
<SelectedAddressDisplay
|
|
995
|
+
address={value}
|
|
996
|
+
onClear={handleClear}
|
|
997
|
+
onEdit={handleEdit}
|
|
998
|
+
disabled={disabled}
|
|
999
|
+
/>
|
|
1000
|
+
{name && (
|
|
1001
|
+
<input
|
|
1002
|
+
type="hidden"
|
|
1003
|
+
name={name}
|
|
1004
|
+
value={value.parts.formatted}
|
|
1005
|
+
/>
|
|
1006
|
+
)}
|
|
1007
|
+
</div>
|
|
1008
|
+
)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return (
|
|
1012
|
+
<div className={cn("relative space-y-1.5", className)}>
|
|
1013
|
+
{label && (
|
|
1014
|
+
<label
|
|
1015
|
+
htmlFor={id}
|
|
1016
|
+
className="text-sm font-medium text-foreground"
|
|
1017
|
+
>
|
|
1018
|
+
{label}
|
|
1019
|
+
{required && <span className="text-destructive ml-1">*</span>}
|
|
1020
|
+
</label>
|
|
1021
|
+
)}
|
|
1022
|
+
|
|
1023
|
+
{/* Input container */}
|
|
1024
|
+
<div className="relative">
|
|
1025
|
+
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
|
1026
|
+
{isLoading ? (
|
|
1027
|
+
<Loader2 className="w-4 h-4 text-muted-foreground animate-spin" />
|
|
1028
|
+
) : (
|
|
1029
|
+
<Search className="w-4 h-4 text-muted-foreground" />
|
|
1030
|
+
)}
|
|
1031
|
+
</div>
|
|
1032
|
+
<input
|
|
1033
|
+
ref={inputRef}
|
|
1034
|
+
type="text"
|
|
1035
|
+
id={id}
|
|
1036
|
+
name={name}
|
|
1037
|
+
role="combobox"
|
|
1038
|
+
aria-expanded={isOpen}
|
|
1039
|
+
aria-haspopup="listbox"
|
|
1040
|
+
aria-autocomplete="list"
|
|
1041
|
+
aria-controls={isOpen ? "address-listbox" : undefined}
|
|
1042
|
+
aria-activedescendant={
|
|
1043
|
+
highlightedIndex >= 0
|
|
1044
|
+
? `address-option-${highlightedIndex}`
|
|
1045
|
+
: undefined
|
|
1046
|
+
}
|
|
1047
|
+
autoComplete="off"
|
|
1048
|
+
autoFocus={autoFocus}
|
|
1049
|
+
disabled={disabled}
|
|
1050
|
+
required={required}
|
|
1051
|
+
placeholder={placeholder}
|
|
1052
|
+
value={query}
|
|
1053
|
+
onChange={handleInputChange}
|
|
1054
|
+
onFocus={handleFocus}
|
|
1055
|
+
onBlur={handleBlur}
|
|
1056
|
+
onKeyDown={handleKeyDown}
|
|
1057
|
+
className={cn(
|
|
1058
|
+
"flex h-10 w-full rounded-md border border-input bg-background",
|
|
1059
|
+
"pl-10 pr-10 py-2 text-sm",
|
|
1060
|
+
"ring-offset-background",
|
|
1061
|
+
"placeholder:text-muted-foreground",
|
|
1062
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
1063
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
1064
|
+
"transition-colors",
|
|
1065
|
+
displayError && "border-destructive focus-visible:ring-destructive"
|
|
1066
|
+
)}
|
|
1067
|
+
/>
|
|
1068
|
+
<div className="absolute inset-y-0 right-0 flex items-center pr-2">
|
|
1069
|
+
{query && (
|
|
1070
|
+
<button
|
|
1071
|
+
type="button"
|
|
1072
|
+
className="p-1 rounded-md hover:bg-accent transition-colors"
|
|
1073
|
+
onClick={() => {
|
|
1074
|
+
setQuery("")
|
|
1075
|
+
inputRef.current?.focus()
|
|
1076
|
+
}}
|
|
1077
|
+
aria-label="Clear search"
|
|
1078
|
+
>
|
|
1079
|
+
<X className="w-4 h-4 text-muted-foreground" />
|
|
1080
|
+
</button>
|
|
1081
|
+
)}
|
|
1082
|
+
<button
|
|
1083
|
+
type="button"
|
|
1084
|
+
className="p-1 rounded-md hover:bg-accent transition-colors"
|
|
1085
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
1086
|
+
aria-label="Toggle dropdown"
|
|
1087
|
+
>
|
|
1088
|
+
<ChevronDown
|
|
1089
|
+
className={cn(
|
|
1090
|
+
"w-4 h-4 text-muted-foreground transition-transform duration-200",
|
|
1091
|
+
isOpen && "rotate-180"
|
|
1092
|
+
)}
|
|
1093
|
+
/>
|
|
1094
|
+
</button>
|
|
1095
|
+
</div>
|
|
1096
|
+
</div>
|
|
1097
|
+
|
|
1098
|
+
{/* Error message */}
|
|
1099
|
+
{displayError && (
|
|
1100
|
+
<p className="text-sm text-destructive">{displayError}</p>
|
|
1101
|
+
)}
|
|
1102
|
+
|
|
1103
|
+
{/* Dropdown */}
|
|
1104
|
+
{isOpen && showContent && (
|
|
1105
|
+
<div
|
|
1106
|
+
ref={dropdownRef}
|
|
1107
|
+
id="address-listbox"
|
|
1108
|
+
role="listbox"
|
|
1109
|
+
className={cn(
|
|
1110
|
+
"absolute z-50 w-full mt-1 py-1 rounded-md border bg-popover shadow-lg",
|
|
1111
|
+
"waka-address-dropdown-enter"
|
|
1112
|
+
)}
|
|
1113
|
+
>
|
|
1114
|
+
{/* Loading state */}
|
|
1115
|
+
{isLoading && (
|
|
1116
|
+
<div className="flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground">
|
|
1117
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
1118
|
+
<span>{loadingMessage}</span>
|
|
1119
|
+
</div>
|
|
1120
|
+
)}
|
|
1121
|
+
|
|
1122
|
+
{/* Recent addresses */}
|
|
1123
|
+
{showRecent && !isLoading && (
|
|
1124
|
+
<div>
|
|
1125
|
+
<div className="px-3 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
1126
|
+
Recent Addresses
|
|
1127
|
+
</div>
|
|
1128
|
+
{recentAddresses.map((address, index) => (
|
|
1129
|
+
<RecentAddressItem
|
|
1130
|
+
key={address.id}
|
|
1131
|
+
address={address}
|
|
1132
|
+
isHighlighted={highlightedIndex === index}
|
|
1133
|
+
onSelect={() => handleSelect(address)}
|
|
1134
|
+
onRemove={() => removeFromRecent(address.id)}
|
|
1135
|
+
onMouseEnter={() => setHighlightedIndex(index)}
|
|
1136
|
+
/>
|
|
1137
|
+
))}
|
|
1138
|
+
</div>
|
|
1139
|
+
)}
|
|
1140
|
+
|
|
1141
|
+
{/* Suggestions */}
|
|
1142
|
+
{showSuggestions && !isLoading && (
|
|
1143
|
+
<div>
|
|
1144
|
+
<div className="px-3 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
1145
|
+
Suggestions
|
|
1146
|
+
</div>
|
|
1147
|
+
{suggestions.map((suggestion, index) => (
|
|
1148
|
+
<SuggestionItem
|
|
1149
|
+
key={suggestion.id}
|
|
1150
|
+
suggestion={suggestion}
|
|
1151
|
+
isHighlighted={highlightedIndex === index}
|
|
1152
|
+
onSelect={() => handleSelect(suggestion)}
|
|
1153
|
+
onMouseEnter={() => setHighlightedIndex(index)}
|
|
1154
|
+
/>
|
|
1155
|
+
))}
|
|
1156
|
+
</div>
|
|
1157
|
+
)}
|
|
1158
|
+
|
|
1159
|
+
{/* No results */}
|
|
1160
|
+
{showNoResults && (
|
|
1161
|
+
<div className="py-6 text-center">
|
|
1162
|
+
<Navigation className="w-8 h-8 mx-auto text-muted-foreground/50 mb-2" />
|
|
1163
|
+
<p className="text-sm text-muted-foreground">{noResultsMessage}</p>
|
|
1164
|
+
{allowManualEntry && onManualEntry && (
|
|
1165
|
+
<button
|
|
1166
|
+
type="button"
|
|
1167
|
+
className="mt-2 text-sm text-primary hover:underline"
|
|
1168
|
+
onClick={handleManualEntryClick}
|
|
1169
|
+
>
|
|
1170
|
+
Enter address manually
|
|
1171
|
+
</button>
|
|
1172
|
+
)}
|
|
1173
|
+
</div>
|
|
1174
|
+
)}
|
|
1175
|
+
|
|
1176
|
+
{/* Manual entry option (when showing recent/suggestions) */}
|
|
1177
|
+
{allowManualEntry && onManualEntry && !showNoResults && !isLoading && (
|
|
1178
|
+
<div className="border-t mt-1 pt-1">
|
|
1179
|
+
<button
|
|
1180
|
+
type="button"
|
|
1181
|
+
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
|
1182
|
+
onClick={handleManualEntryClick}
|
|
1183
|
+
>
|
|
1184
|
+
<Edit3 className="w-4 h-4" />
|
|
1185
|
+
<span>Enter address manually</span>
|
|
1186
|
+
</button>
|
|
1187
|
+
</div>
|
|
1188
|
+
)}
|
|
1189
|
+
</div>
|
|
1190
|
+
)}
|
|
1191
|
+
</div>
|
|
1192
|
+
)
|
|
1193
|
+
}
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
WakaAddressAutocomplete.displayName = "WakaAddressAutocomplete"
|
|
1197
|
+
|
|
1198
|
+
// ============================================
|
|
1199
|
+
// EXPORTS
|
|
1200
|
+
// ============================================
|
|
1201
|
+
|
|
1202
|
+
export default WakaAddressAutocomplete
|