@locusai/web 0.1.7 → 0.2.2
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/CHANGELOG.md +26 -0
- package/next.config.js +15 -2
- package/package.json +26 -3
- package/src/app/(auth)/invite/page.tsx +109 -0
- package/src/app/(auth)/layout.tsx +19 -0
- package/src/app/(auth)/login/page.tsx +65 -0
- package/src/app/(auth)/onboarding/workspace/page.tsx +46 -0
- package/src/app/(auth)/register/page.tsx +165 -0
- package/src/app/(dashboard)/activity/page.tsx +7 -0
- package/src/app/(dashboard)/backlog/page.tsx +195 -0
- package/src/app/(dashboard)/board/page.tsx +141 -0
- package/src/app/(dashboard)/layout.tsx +32 -0
- package/src/app/(dashboard)/page.tsx +14 -0
- package/src/app/(dashboard)/settings/page.tsx +161 -0
- package/src/app/(dashboard)/settings/team/page.tsx +75 -0
- package/src/app/globals.css +259 -0
- package/src/app/layout.tsx +10 -20
- package/src/app/providers.tsx +26 -3
- package/src/components/AuthLayoutUI.tsx +53 -0
- package/src/components/BoardFilter.tsx +75 -74
- package/src/components/CreateModal/CreateModal.tsx +142 -0
- package/src/components/CreateModal/index.ts +1 -0
- package/src/components/Editor.tsx +279 -0
- package/src/components/Header.tsx +99 -12
- package/src/components/PageLayout.tsx +69 -0
- package/src/components/PropertyItem.tsx +55 -9
- package/src/components/Sidebar.tsx +280 -36
- package/src/components/SprintCreateModal.tsx +84 -0
- package/src/components/TaskCard.tsx +196 -78
- package/src/components/TaskCreateModal.tsx +181 -178
- package/src/components/TaskPanel.tsx +140 -692
- package/src/components/WorkspaceCreateModal.tsx +97 -0
- package/src/components/WorkspaceProtected.tsx +91 -0
- package/src/components/auth/InviteSteps.tsx +220 -0
- package/src/components/auth/LoginSteps.tsx +86 -0
- package/src/components/auth/RegisterSteps.tsx +371 -0
- package/src/components/auth/index.ts +3 -0
- package/src/components/backlog/BacklogList.tsx +92 -0
- package/src/components/backlog/BacklogSection.tsx +137 -0
- package/src/components/backlog/CompletedSprintItem.tsx +95 -0
- package/src/components/backlog/CompletedSprintsSection.tsx +77 -0
- package/src/components/backlog/SprintSection.tsx +155 -0
- package/src/components/backlog/constants.ts +26 -0
- package/src/components/board/BoardColumn.tsx +97 -0
- package/src/components/board/BoardContent.tsx +84 -0
- package/src/components/board/BoardEmptyState.tsx +82 -0
- package/src/components/board/BoardHeader.tsx +47 -0
- package/src/components/board/SprintMindmap.tsx +65 -0
- package/src/components/board/constants.ts +40 -0
- package/src/components/board/index.ts +5 -0
- package/src/components/common/ErrorState.tsx +124 -0
- package/src/components/common/LoadingState.tsx +83 -0
- package/src/components/common/index.ts +40 -0
- package/src/components/dashboard/ActivityFeed.tsx +77 -0
- package/src/components/dashboard/ActivityItem.tsx +207 -0
- package/src/components/dashboard/QuickActions.tsx +50 -0
- package/src/components/dashboard/StatCard.tsx +79 -0
- package/src/components/dnd/index.tsx +51 -0
- package/src/components/docs/DocsEditorArea.tsx +87 -0
- package/src/components/docs/DocsHeaderActions.tsx +121 -0
- package/src/components/docs/DocsSidebar.tsx +351 -0
- package/src/components/index.ts +7 -0
- package/src/components/onboarding/StepProgress.tsx +21 -0
- package/src/components/onboarding/index.ts +1 -0
- package/src/components/settings/ApiKeyConfirmationModal.tsx +81 -0
- package/src/components/settings/ApiKeyCreatedModal.tsx +96 -0
- package/src/components/settings/ApiKeysList.tsx +143 -0
- package/src/components/settings/ApiKeysSettings.tsx +144 -0
- package/src/components/settings/InviteMemberModal.tsx +106 -0
- package/src/components/settings/ProjectSetupGuide.tsx +147 -0
- package/src/components/settings/SettingItem.tsx +32 -0
- package/src/components/settings/SettingSection.tsx +50 -0
- package/src/components/settings/TeamInvitationsList.tsx +90 -0
- package/src/components/settings/TeamMembersList.tsx +95 -0
- package/src/components/settings/index.ts +8 -0
- package/src/components/task-panel/TaskActivity.tsx +127 -0
- package/src/components/task-panel/TaskChecklist.tsx +142 -0
- package/src/components/task-panel/TaskDescription.tsx +201 -0
- package/src/components/task-panel/TaskDocs.tsx +137 -0
- package/src/components/task-panel/TaskHeader.tsx +125 -0
- package/src/components/task-panel/TaskProperties.tsx +111 -0
- package/src/components/task-panel/index.ts +12 -0
- package/src/components/typography/EmptyStateText.tsx +59 -0
- package/src/components/typography/MetadataText.tsx +65 -0
- package/src/components/typography/SecondaryText.tsx +60 -0
- package/src/components/typography/SectionLabel.tsx +60 -0
- package/src/components/typography/index.ts +14 -0
- package/src/components/typography-scales.tsx +218 -0
- package/src/components/ui/Avatar.tsx +123 -0
- package/src/components/ui/Badge.tsx +69 -2
- package/src/components/ui/Button.tsx +71 -30
- package/src/components/ui/Checkbox.tsx +34 -0
- package/src/components/ui/Dropdown.tsx +67 -1
- package/src/components/ui/EmptyState.tsx +129 -0
- package/src/components/ui/Input.tsx +53 -6
- package/src/components/ui/Modal.tsx +45 -12
- package/src/components/ui/OtpInput.tsx +148 -0
- package/src/components/ui/Skeleton.tsx +36 -0
- package/src/components/ui/Spinner.tsx +112 -0
- package/src/components/ui/Textarea.tsx +28 -3
- package/src/components/ui/Toast.tsx +99 -0
- package/src/components/ui/Toggle.tsx +63 -0
- package/src/components/ui/constants.ts +108 -0
- package/src/components/ui/index.ts +7 -0
- package/src/context/AuthContext.tsx +140 -0
- package/src/context/index.ts +1 -0
- package/src/hooks/backlog/index.ts +13 -0
- package/src/hooks/backlog/useBacklogActions.ts +144 -0
- package/src/hooks/backlog/useBacklogComposite.ts +73 -0
- package/src/hooks/backlog/useBacklogData.ts +74 -0
- package/src/hooks/backlog/useBacklogDragDrop.ts +118 -0
- package/src/hooks/backlog/useBacklogUI.ts +74 -0
- package/src/hooks/index.ts +22 -0
- package/src/hooks/task-panel/index.ts +13 -0
- package/src/hooks/task-panel/useTaskActions.ts +200 -0
- package/src/hooks/task-panel/useTaskComputedValues.ts +30 -0
- package/src/hooks/task-panel/useTaskData.ts +78 -0
- package/src/hooks/task-panel/useTaskPanelComposite.ts +161 -0
- package/src/hooks/task-panel/useTaskUIState.ts +80 -0
- package/src/hooks/useAuthLayoutLogic.ts +43 -0
- package/src/hooks/useAuthenticatedUser.ts +36 -0
- package/src/hooks/useAuthenticatedUserWithOrg.ts +41 -0
- package/src/hooks/useBacklog.ts +303 -0
- package/src/hooks/useBoard.ts +230 -0
- package/src/hooks/useDashboardLayout.ts +49 -0
- package/src/hooks/useDocs.ts +279 -0
- package/src/hooks/useDocsQuery.ts +99 -0
- package/src/hooks/useDocsSidebarState.ts +104 -0
- package/src/hooks/useFormState.ts +40 -0
- package/src/hooks/useGlobalKeydowns.ts +52 -0
- package/src/hooks/useInviteForm.ts +122 -0
- package/src/hooks/useLoginForm.ts +84 -0
- package/src/hooks/useMutationWithToast.ts +56 -0
- package/src/hooks/useOrganizationQuery.ts +55 -0
- package/src/hooks/useRegisterForm.ts +216 -0
- package/src/hooks/useSprintsQuery.ts +38 -0
- package/src/hooks/useTaskDescription.ts +102 -0
- package/src/hooks/useTaskPanel.ts +341 -0
- package/src/hooks/useTasksQuery.ts +39 -0
- package/src/hooks/useTeamManagement.ts +92 -0
- package/src/hooks/useWorkspaceCreateForm.ts +70 -0
- package/src/hooks/useWorkspaceId.ts +29 -0
- package/src/lib/api-client.ts +40 -23
- package/src/lib/config.ts +25 -0
- package/src/lib/constants.ts +83 -0
- package/src/lib/options.ts +96 -0
- package/src/lib/query-keys.ts +91 -0
- package/src/lib/typography.ts +103 -0
- package/src/lib/utils.ts +4 -0
- package/src/lib/validation.ts +192 -0
- package/src/services/index.ts +7 -3
- package/src/services/notifications.ts +80 -0
- package/src/utils/env.utils.ts +15 -0
- package/src/views/ActivityView.tsx +123 -0
- package/src/views/Dashboard.tsx +126 -0
- package/src/views/Docs.tsx +98 -612
- package/tsconfig.tsbuildinfo +1 -1
- package/.next/BUILD_ID +0 -1
- package/.next/app-build-manifest.json +0 -55
- package/.next/app-path-routes-manifest.json +0 -8
- package/.next/build-manifest.json +0 -33
- package/.next/cache/.previewinfo +0 -1
- package/.next/cache/.rscinfo +0 -1
- package/.next/cache/.tsbuildinfo +0 -1
- package/.next/cache/config.json +0 -7
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/diagnostics/build-diagnostics.json +0 -6
- package/.next/diagnostics/framework.json +0 -1
- package/.next/export-detail.json +0 -5
- package/.next/export-marker.json +0 -6
- package/.next/images-manifest.json +0 -57
- package/.next/next-minimal-server.js.nft.json +0 -1
- package/.next/next-server.js.nft.json +0 -1
- package/.next/package.json +0 -1
- package/.next/prerender-manifest.json +0 -137
- package/.next/react-loadable-manifest.json +0 -32
- package/.next/required-server-files.json +0 -324
- package/.next/routes-manifest.json +0 -77
- package/.next/server/app/_not-found/page.js +0 -2
- package/.next/server/app/_not-found/page.js.nft.json +0 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
- package/.next/server/app/_not-found.html +0 -1
- package/.next/server/app/_not-found.meta +0 -8
- package/.next/server/app/_not-found.rsc +0 -21
- package/.next/server/app/backlog/page.js +0 -2
- package/.next/server/app/backlog/page.js.nft.json +0 -1
- package/.next/server/app/backlog/page_client-reference-manifest.js +0 -1
- package/.next/server/app/backlog.html +0 -1
- package/.next/server/app/backlog.meta +0 -7
- package/.next/server/app/backlog.rsc +0 -25
- package/.next/server/app/docs/page.js +0 -97
- package/.next/server/app/docs/page.js.nft.json +0 -1
- package/.next/server/app/docs/page_client-reference-manifest.js +0 -1
- package/.next/server/app/docs.html +0 -1
- package/.next/server/app/docs.meta +0 -7
- package/.next/server/app/docs.rsc +0 -26
- package/.next/server/app/favicon.ico/route.js +0 -1
- package/.next/server/app/favicon.ico/route.js.nft.json +0 -1
- package/.next/server/app/favicon.ico.body +0 -0
- package/.next/server/app/favicon.ico.meta +0 -1
- package/.next/server/app/index.html +0 -1
- package/.next/server/app/index.meta +0 -7
- package/.next/server/app/index.rsc +0 -25
- package/.next/server/app/page.js +0 -2
- package/.next/server/app/page.js.nft.json +0 -1
- package/.next/server/app/page_client-reference-manifest.js +0 -1
- package/.next/server/app/settings/page.js +0 -2
- package/.next/server/app/settings/page.js.nft.json +0 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +0 -1
- package/.next/server/app/settings.html +0 -1
- package/.next/server/app/settings.meta +0 -7
- package/.next/server/app/settings.rsc +0 -25
- package/.next/server/app-paths-manifest.json +0 -8
- package/.next/server/chunks/496.js +0 -6
- package/.next/server/chunks/585.js +0 -1
- package/.next/server/chunks/665.js +0 -1
- package/.next/server/chunks/699.js +0 -22
- package/.next/server/chunks/852.js +0 -7
- package/.next/server/functions-config-manifest.json +0 -4
- package/.next/server/interception-route-rewrite-manifest.js +0 -1
- package/.next/server/middleware-build-manifest.js +0 -1
- package/.next/server/middleware-manifest.json +0 -6
- package/.next/server/middleware-react-loadable-manifest.js +0 -1
- package/.next/server/next-font-manifest.js +0 -1
- package/.next/server/next-font-manifest.json +0 -1
- package/.next/server/pages/404.html +0 -1
- package/.next/server/pages/500.html +0 -1
- package/.next/server/pages/_app.js +0 -1
- package/.next/server/pages/_app.js.nft.json +0 -1
- package/.next/server/pages/_document.js +0 -1
- package/.next/server/pages/_document.js.nft.json +0 -1
- package/.next/server/pages/_error.js +0 -19
- package/.next/server/pages/_error.js.nft.json +0 -1
- package/.next/server/pages-manifest.json +0 -6
- package/.next/server/server-reference-manifest.js +0 -1
- package/.next/server/server-reference-manifest.json +0 -1
- package/.next/server/webpack-runtime.js +0 -1
- package/.next/static/D0NXe04ZCLNDckV_quc8g/_buildManifest.js +0 -1
- package/.next/static/D0NXe04ZCLNDckV_quc8g/_ssgManifest.js +0 -1
- package/.next/static/chunks/138.b98511c56423f8bb.js +0 -1
- package/.next/static/chunks/146-34259952c594a3b0.js +0 -1
- package/.next/static/chunks/337-d3bb75304d130513.js +0 -1
- package/.next/static/chunks/477.1a6ecfe53375bd9c.js +0 -1
- package/.next/static/chunks/487-1808785ba665f784.js +0 -1
- package/.next/static/chunks/544.a9569941cc886e9d.js +0 -1
- package/.next/static/chunks/87c73c54-1f4741035a95c140.js +0 -1
- package/.next/static/chunks/902-d6926825a9fe8784.js +0 -1
- package/.next/static/chunks/955-c8f8f6235ae8f8c6.js +0 -1
- package/.next/static/chunks/996.e0a334e6ae90900e.js +0 -1
- package/.next/static/chunks/app/_not-found/page-44b1804abb44a34d.js +0 -1
- package/.next/static/chunks/app/backlog/page-dce1450769bfae8f.js +0 -1
- package/.next/static/chunks/app/docs/page-1efee819f25492cb.js +0 -1
- package/.next/static/chunks/app/layout-05f504c042b9f7ee.js +0 -1
- package/.next/static/chunks/app/page-3fd91aaaa4776ced.js +0 -1
- package/.next/static/chunks/app/settings/page-84e16c9638d657e4.js +0 -1
- package/.next/static/chunks/framework-152a1bc8c81c7458.js +0 -1
- package/.next/static/chunks/main-843ab130fc1be309.js +0 -1
- package/.next/static/chunks/main-app-123e879c5a937a00.js +0 -1
- package/.next/static/chunks/pages/_app-a050a8e6e4fb04cf.js +0 -1
- package/.next/static/chunks/pages/_error-3e422ffd891594de.js +0 -1
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
- package/.next/static/chunks/webpack-99a10a055b5bb9c4.js +0 -1
- package/.next/static/css/13e8617b72f9d3aa.css +0 -1
- package/.next/static/css/8aea088cdc4338f0.css +0 -1
- package/.next/static/css/b301ab0424111664.css +0 -1
- package/.next/static/media/24c15609eaa28576-s.woff2 +0 -0
- package/.next/static/media/2c07349e02a7b712-s.woff2 +0 -0
- package/.next/static/media/456105d6ea6d39e0-s.woff2 +0 -0
- package/.next/static/media/47cbc4e2adbc5db9-s.p.woff2 +0 -0
- package/.next/static/media/4f77bef990aad698-s.woff2 +0 -0
- package/.next/static/media/627d916fd739a539-s.woff2 +0 -0
- package/.next/static/media/63b255f18bea0ca9-s.woff2 +0 -0
- package/.next/static/media/70bd82ac89b4fa42-s.woff2 +0 -0
- package/.next/static/media/84602850c8fd81c3-s.woff2 +0 -0
- package/.next/trace +0 -46
- package/.next/types/app/backlog/page.ts +0 -84
- package/.next/types/app/docs/page.ts +0 -84
- package/.next/types/app/layout.ts +0 -84
- package/.next/types/app/page.ts +0 -84
- package/.next/types/app/settings/page.ts +0 -84
- package/.next/types/cache-life.d.ts +0 -141
- package/.next/types/package.json +0 -1
- package/next-env.d.ts +0 -5
- package/out/404.html +0 -1
- package/out/_next/static/D0NXe04ZCLNDckV_quc8g/_buildManifest.js +0 -1
- package/out/_next/static/D0NXe04ZCLNDckV_quc8g/_ssgManifest.js +0 -1
- package/out/_next/static/chunks/138.b98511c56423f8bb.js +0 -1
- package/out/_next/static/chunks/146-34259952c594a3b0.js +0 -1
- package/out/_next/static/chunks/337-d3bb75304d130513.js +0 -1
- package/out/_next/static/chunks/477.1a6ecfe53375bd9c.js +0 -1
- package/out/_next/static/chunks/487-1808785ba665f784.js +0 -1
- package/out/_next/static/chunks/544.a9569941cc886e9d.js +0 -1
- package/out/_next/static/chunks/87c73c54-1f4741035a95c140.js +0 -1
- package/out/_next/static/chunks/902-d6926825a9fe8784.js +0 -1
- package/out/_next/static/chunks/955-c8f8f6235ae8f8c6.js +0 -1
- package/out/_next/static/chunks/996.e0a334e6ae90900e.js +0 -1
- package/out/_next/static/chunks/app/_not-found/page-44b1804abb44a34d.js +0 -1
- package/out/_next/static/chunks/app/backlog/page-dce1450769bfae8f.js +0 -1
- package/out/_next/static/chunks/app/docs/page-1efee819f25492cb.js +0 -1
- package/out/_next/static/chunks/app/layout-05f504c042b9f7ee.js +0 -1
- package/out/_next/static/chunks/app/page-3fd91aaaa4776ced.js +0 -1
- package/out/_next/static/chunks/app/settings/page-84e16c9638d657e4.js +0 -1
- package/out/_next/static/chunks/framework-152a1bc8c81c7458.js +0 -1
- package/out/_next/static/chunks/main-843ab130fc1be309.js +0 -1
- package/out/_next/static/chunks/main-app-123e879c5a937a00.js +0 -1
- package/out/_next/static/chunks/pages/_app-a050a8e6e4fb04cf.js +0 -1
- package/out/_next/static/chunks/pages/_error-3e422ffd891594de.js +0 -1
- package/out/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
- package/out/_next/static/chunks/webpack-99a10a055b5bb9c4.js +0 -1
- package/out/_next/static/css/13e8617b72f9d3aa.css +0 -1
- package/out/_next/static/css/8aea088cdc4338f0.css +0 -1
- package/out/_next/static/css/b301ab0424111664.css +0 -1
- package/out/_next/static/media/24c15609eaa28576-s.woff2 +0 -0
- package/out/_next/static/media/2c07349e02a7b712-s.woff2 +0 -0
- package/out/_next/static/media/456105d6ea6d39e0-s.woff2 +0 -0
- package/out/_next/static/media/47cbc4e2adbc5db9-s.p.woff2 +0 -0
- package/out/_next/static/media/4f77bef990aad698-s.woff2 +0 -0
- package/out/_next/static/media/627d916fd739a539-s.woff2 +0 -0
- package/out/_next/static/media/63b255f18bea0ca9-s.woff2 +0 -0
- package/out/_next/static/media/70bd82ac89b4fa42-s.woff2 +0 -0
- package/out/_next/static/media/84602850c8fd81c3-s.woff2 +0 -0
- package/out/backlog.html +0 -1
- package/out/backlog.txt +0 -25
- package/out/docs.html +0 -1
- package/out/docs.txt +0 -26
- package/out/favicon.ico +0 -0
- package/out/index.html +0 -1
- package/out/index.txt +0 -25
- package/out/logo.png +0 -0
- package/out/settings.html +0 -1
- package/out/settings.txt +0 -25
- package/src/app/backlog/page.tsx +0 -19
- package/src/app/page.tsx +0 -16
- package/src/app/settings/page.tsx +0 -194
- package/src/hooks/useTasks.ts +0 -119
- package/src/services/doc.service.ts +0 -27
- package/src/services/sprint.service.ts +0 -24
- package/src/services/task.service.ts +0 -75
- package/src/views/Backlog.tsx +0 -691
- package/src/views/Board.tsx +0 -306
- /package/src/app/{docs → (dashboard)/docs}/page.tsx +0 -0
|
@@ -1,28 +1,57 @@
|
|
|
1
1
|
import { forwardRef, type InputHTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Input component props
|
|
6
|
+
*
|
|
7
|
+
* @property icon - Optional left icon element
|
|
8
|
+
* @property rightElement - Optional right element (e.g., clear button)
|
|
9
|
+
* @property error - Show error state
|
|
10
|
+
*/
|
|
3
11
|
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
12
|
+
/** Icon displayed on the left side of input */
|
|
4
13
|
icon?: ReactNode;
|
|
14
|
+
/** Element displayed on the right side of input */
|
|
5
15
|
rightElement?: ReactNode;
|
|
16
|
+
/** Show error state */
|
|
17
|
+
error?: boolean;
|
|
6
18
|
}
|
|
7
19
|
|
|
8
20
|
const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
9
|
-
({ className, icon, rightElement, ...props }, ref) => {
|
|
21
|
+
({ className, icon, rightElement, error, ...props }, ref) => {
|
|
10
22
|
return (
|
|
11
23
|
<div className="relative w-full group">
|
|
12
24
|
{icon && (
|
|
13
|
-
<div
|
|
25
|
+
<div
|
|
26
|
+
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground group-focus-within:text-primary transition-colors"
|
|
27
|
+
aria-hidden="true"
|
|
28
|
+
>
|
|
14
29
|
{icon}
|
|
15
30
|
</div>
|
|
16
31
|
)}
|
|
17
32
|
<input
|
|
18
33
|
ref={ref}
|
|
19
|
-
className={
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
className={cn(
|
|
35
|
+
"flex h-9 w-full rounded-lg border bg-secondary/30 px-3 py-1 text-sm transition-all",
|
|
36
|
+
"placeholder:text-muted-foreground/50",
|
|
37
|
+
"hover:border-border hover:bg-secondary/50",
|
|
38
|
+
"focus:outline-none focus:border-primary/50 focus:bg-background focus:ring-2 focus:ring-primary/10",
|
|
39
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
40
|
+
"file:border-0 file:bg-transparent file:text-sm file:font-medium",
|
|
41
|
+
error
|
|
42
|
+
? "border-red-500/50 ring-2 ring-red-500/10"
|
|
43
|
+
: "border-border/60",
|
|
44
|
+
icon && "pl-10",
|
|
45
|
+
rightElement && "pr-10",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
22
48
|
{...props}
|
|
23
49
|
/>
|
|
24
50
|
{rightElement && (
|
|
25
|
-
<div
|
|
51
|
+
<div
|
|
52
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground"
|
|
53
|
+
aria-hidden="true"
|
|
54
|
+
>
|
|
26
55
|
{rightElement}
|
|
27
56
|
</div>
|
|
28
57
|
)}
|
|
@@ -31,6 +60,24 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
31
60
|
}
|
|
32
61
|
);
|
|
33
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Input component
|
|
65
|
+
*
|
|
66
|
+
* A flexible input component with support for left/right icons and error states.
|
|
67
|
+
* Automatically adjusts padding when icons are present.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* // Basic input
|
|
71
|
+
* <Input placeholder="Enter text..." />
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* // With left icon
|
|
75
|
+
* <Input icon={<Search size={16} />} placeholder="Search..." />
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* // With error state
|
|
79
|
+
* <Input error value={value} onChange={handleChange} />
|
|
80
|
+
*/
|
|
34
81
|
Input.displayName = "Input";
|
|
35
82
|
|
|
36
83
|
export { Input };
|
|
@@ -3,13 +3,27 @@
|
|
|
3
3
|
import { X } from "lucide-react";
|
|
4
4
|
import { type ReactNode, useEffect, useRef } from "react";
|
|
5
5
|
import { createPortal } from "react-dom";
|
|
6
|
+
import { MODAL_SIZES, Z_INDEXES } from "./constants";
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Modal component props
|
|
10
|
+
*
|
|
11
|
+
* @property isOpen - Control modal visibility
|
|
12
|
+
* @property onClose - Callback when modal should close
|
|
13
|
+
* @property title - Optional modal header title
|
|
14
|
+
* @property size - Modal width size (default: "md")
|
|
15
|
+
*/
|
|
7
16
|
interface ModalProps {
|
|
17
|
+
/** Whether modal is visible */
|
|
8
18
|
isOpen: boolean;
|
|
19
|
+
/** Called when user attempts to close modal */
|
|
9
20
|
onClose: () => void;
|
|
21
|
+
/** Modal content */
|
|
10
22
|
children: ReactNode;
|
|
23
|
+
/** Optional modal header title */
|
|
11
24
|
title?: string;
|
|
12
|
-
|
|
25
|
+
/** Modal size */
|
|
26
|
+
size?: keyof typeof MODAL_SIZES;
|
|
13
27
|
}
|
|
14
28
|
|
|
15
29
|
export function Modal({
|
|
@@ -39,35 +53,37 @@ export function Modal({
|
|
|
39
53
|
|
|
40
54
|
if (!isOpen) return null;
|
|
41
55
|
|
|
42
|
-
const widths = {
|
|
43
|
-
sm: "w-[400px]",
|
|
44
|
-
md: "w-[520px]",
|
|
45
|
-
lg: "w-[680px]",
|
|
46
|
-
};
|
|
47
|
-
|
|
48
56
|
return createPortal(
|
|
49
57
|
<div
|
|
50
58
|
ref={overlayRef}
|
|
51
|
-
className=
|
|
59
|
+
className={`fixed inset-0 bg-black/40 backdrop-blur-md flex items-center justify-center ${Z_INDEXES.modalOverlay} animate-in fade-in duration-300`}
|
|
52
60
|
onClick={(e) => {
|
|
53
61
|
if (e.target === overlayRef.current) onClose();
|
|
54
62
|
}}
|
|
63
|
+
role="presentation"
|
|
55
64
|
>
|
|
56
65
|
<div
|
|
57
66
|
className={`bg-background border border-border rounded-xl shadow-lg animate-in zoom-in-95 duration-200 max-h-[90vh] overflow-hidden flex flex-col ${
|
|
58
|
-
|
|
59
|
-
} max-w-[90vw]`}
|
|
67
|
+
MODAL_SIZES[size]
|
|
68
|
+
} max-w-[90vw] ${Z_INDEXES.modal}`}
|
|
69
|
+
role="dialog"
|
|
70
|
+
aria-modal="true"
|
|
71
|
+
aria-labelledby={title ? "modal-title" : undefined}
|
|
60
72
|
>
|
|
61
73
|
{title && (
|
|
62
74
|
<div className="flex justify-between items-center p-5 border-b border-border">
|
|
63
|
-
<h3
|
|
75
|
+
<h3
|
|
76
|
+
id="modal-title"
|
|
77
|
+
className="text-lg font-semibold m-0 text-foreground"
|
|
78
|
+
>
|
|
64
79
|
{title}
|
|
65
80
|
</h3>
|
|
66
81
|
<button
|
|
67
82
|
className="bg-transparent border-none text-muted-foreground hover:text-foreground cursor-pointer p-0 transition-colors duration-200"
|
|
68
83
|
onClick={onClose}
|
|
84
|
+
aria-label="Close modal"
|
|
69
85
|
>
|
|
70
|
-
<X size={20} />
|
|
86
|
+
<X size={20} aria-hidden="true" />
|
|
71
87
|
</button>
|
|
72
88
|
</div>
|
|
73
89
|
)}
|
|
@@ -77,3 +93,20 @@ export function Modal({
|
|
|
77
93
|
document.body
|
|
78
94
|
);
|
|
79
95
|
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Modal component
|
|
99
|
+
*
|
|
100
|
+
* A accessible, dismissable modal dialog component.
|
|
101
|
+
* - Closes on Escape key
|
|
102
|
+
* - Closes on backdrop click
|
|
103
|
+
* - Prevents body scroll when open
|
|
104
|
+
* - Includes proper ARIA attributes
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* // Basic modal
|
|
108
|
+
* <Modal isOpen={isOpen} onClose={handleClose} title="Confirm Action">
|
|
109
|
+
* <p>Are you sure?</p>
|
|
110
|
+
* <Button onClick={handleConfirm}>Confirm</Button>
|
|
111
|
+
* </Modal>
|
|
112
|
+
*/
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* OTP input component props
|
|
8
|
+
*
|
|
9
|
+
* @property length - Number of input fields (default: 6)
|
|
10
|
+
* @property value - Current OTP value (e.g., "123456")
|
|
11
|
+
* @property onChange - Callback when OTP value changes
|
|
12
|
+
* @property disabled - Disable the inputs
|
|
13
|
+
*/
|
|
14
|
+
interface OtpInputProps {
|
|
15
|
+
/** Number of OTP digits (default: 6) */
|
|
16
|
+
length?: number;
|
|
17
|
+
/** Current OTP value as string */
|
|
18
|
+
value: string;
|
|
19
|
+
/** Callback when OTP changes */
|
|
20
|
+
onChange: (value: string) => void;
|
|
21
|
+
/** Disable OTP inputs */
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* OTP input component
|
|
27
|
+
*
|
|
28
|
+
* A specialized input component for one-time passwords.
|
|
29
|
+
* - Auto-focuses next field on digit entry
|
|
30
|
+
* - Supports backspace and arrow navigation
|
|
31
|
+
* - Supports paste functionality
|
|
32
|
+
* - Numeric input only
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* const [otp, setOtp] = useState("");
|
|
36
|
+
* <OtpInput
|
|
37
|
+
* length={6}
|
|
38
|
+
* value={otp}
|
|
39
|
+
* onChange={setOtp}
|
|
40
|
+
* />
|
|
41
|
+
*/
|
|
42
|
+
export function OtpInput({
|
|
43
|
+
length = 6,
|
|
44
|
+
value,
|
|
45
|
+
onChange,
|
|
46
|
+
disabled = false,
|
|
47
|
+
}: OtpInputProps) {
|
|
48
|
+
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
|
49
|
+
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
|
50
|
+
|
|
51
|
+
// Initialize refs array
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
inputRefs.current = inputRefs.current.slice(0, length);
|
|
54
|
+
}, [length]);
|
|
55
|
+
|
|
56
|
+
const handleChange = (
|
|
57
|
+
e: React.ChangeEvent<HTMLInputElement>,
|
|
58
|
+
index: number
|
|
59
|
+
) => {
|
|
60
|
+
const char = e.target.value.slice(-1); // Only take last character
|
|
61
|
+
|
|
62
|
+
// Only allow numbers
|
|
63
|
+
if (char && !/^\d$/.test(char)) return;
|
|
64
|
+
|
|
65
|
+
const newValue = value.split("");
|
|
66
|
+
newValue[index] = char;
|
|
67
|
+
const finalValue = newValue.join("");
|
|
68
|
+
onChange(finalValue);
|
|
69
|
+
|
|
70
|
+
// Focus next input
|
|
71
|
+
if (char && index < length - 1) {
|
|
72
|
+
inputRefs.current[index + 1]?.focus();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleKeyDown = (
|
|
77
|
+
e: React.KeyboardEvent<HTMLInputElement>,
|
|
78
|
+
index: number
|
|
79
|
+
) => {
|
|
80
|
+
if (e.key === "Backspace") {
|
|
81
|
+
if (!value[index] && index > 0) {
|
|
82
|
+
// If current is empty, move back and clear previous
|
|
83
|
+
const newValue = value.split("");
|
|
84
|
+
newValue[index - 1] = "";
|
|
85
|
+
onChange(newValue.join(""));
|
|
86
|
+
inputRefs.current[index - 1]?.focus();
|
|
87
|
+
} else {
|
|
88
|
+
// Just clear current
|
|
89
|
+
const newValue = value.split("");
|
|
90
|
+
newValue[index] = "";
|
|
91
|
+
onChange(newValue.join(""));
|
|
92
|
+
}
|
|
93
|
+
} else if (e.key === "ArrowLeft" && index > 0) {
|
|
94
|
+
inputRefs.current[index - 1]?.focus();
|
|
95
|
+
} else if (e.key === "ArrowRight" && index < length - 1) {
|
|
96
|
+
inputRefs.current[index + 1]?.focus();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handlePaste = (e: React.ClipboardEvent) => {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
const pastedData = e.clipboardData
|
|
103
|
+
.getData("text")
|
|
104
|
+
.slice(0, length)
|
|
105
|
+
.replace(/\D/g, "");
|
|
106
|
+
if (pastedData) {
|
|
107
|
+
onChange(pastedData);
|
|
108
|
+
// Focus the next available slot or the last one
|
|
109
|
+
const nextIndex = Math.min(pastedData.length, length - 1);
|
|
110
|
+
inputRefs.current[nextIndex]?.focus();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<fieldset
|
|
116
|
+
className="flex justify-between gap-2 sm:gap-3"
|
|
117
|
+
onPaste={handlePaste}
|
|
118
|
+
>
|
|
119
|
+
<legend className="sr-only">One-time password input</legend>
|
|
120
|
+
{Array.from({ length }).map((_, i) => (
|
|
121
|
+
<input
|
|
122
|
+
key={i}
|
|
123
|
+
ref={(el) => {
|
|
124
|
+
inputRefs.current[i] = el;
|
|
125
|
+
}}
|
|
126
|
+
type="text"
|
|
127
|
+
inputMode="numeric"
|
|
128
|
+
autoComplete="one-time-code"
|
|
129
|
+
value={value[i] || ""}
|
|
130
|
+
onChange={(e) => handleChange(e, i)}
|
|
131
|
+
onKeyDown={(e) => handleKeyDown(e, i)}
|
|
132
|
+
onFocus={() => setFocusedIndex(i)}
|
|
133
|
+
onBlur={() => setFocusedIndex(null)}
|
|
134
|
+
disabled={disabled}
|
|
135
|
+
className={cn(
|
|
136
|
+
"w-full h-12 sm:h-14 text-center text-xl font-bold rounded-xl border transition-all duration-200 outline-none",
|
|
137
|
+
"bg-secondary/20 border-border/40 text-foreground",
|
|
138
|
+
focusedIndex === i
|
|
139
|
+
? "border-primary bg-background ring-4 ring-primary/10"
|
|
140
|
+
: "hover:border-border/80",
|
|
141
|
+
disabled && "opacity-50 cursor-not-allowed"
|
|
142
|
+
)}
|
|
143
|
+
aria-label={`Digit ${i + 1}`}
|
|
144
|
+
/>
|
|
145
|
+
))}
|
|
146
|
+
</fieldset>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Skeleton component props
|
|
7
|
+
*
|
|
8
|
+
* @property className - Additional CSS classes
|
|
9
|
+
*/
|
|
10
|
+
interface SkeletonProps {
|
|
11
|
+
/** Additional CSS classes */
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Skeleton component
|
|
17
|
+
*
|
|
18
|
+
* A placeholder element with pulsing animation for loading states.
|
|
19
|
+
* Use to create skeleton screens that match your content layout.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // Skeleton for avatar
|
|
23
|
+
* <Skeleton className="h-10 w-10 rounded-full" />
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // Skeleton for text
|
|
27
|
+
* <Skeleton className="h-4 w-full rounded" />
|
|
28
|
+
*/
|
|
29
|
+
export function Skeleton({ className }: SkeletonProps) {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
className={cn("animate-pulse rounded-md bg-muted/50", className)}
|
|
33
|
+
aria-busy="true"
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { SPINNER_SIZES } from "./constants";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Spinner component props
|
|
8
|
+
*
|
|
9
|
+
* @property size - Spinner size (default: "md")
|
|
10
|
+
* @property className - Additional CSS classes
|
|
11
|
+
*/
|
|
12
|
+
interface SpinnerProps {
|
|
13
|
+
/** Additional CSS classes */
|
|
14
|
+
className?: string;
|
|
15
|
+
/** Spinner size */
|
|
16
|
+
size?: keyof typeof SPINNER_SIZES;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Spinner component
|
|
21
|
+
*
|
|
22
|
+
* A circular loading indicator with smooth animation.
|
|
23
|
+
* Used to indicate data fetching or processing.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // Default spinner
|
|
27
|
+
* <Spinner />
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Large spinner
|
|
31
|
+
* <Spinner size="lg" />
|
|
32
|
+
*/
|
|
33
|
+
export function Spinner({ className, size = "md" }: SpinnerProps) {
|
|
34
|
+
const sizeClasses = {
|
|
35
|
+
sm: "h-4 w-4 border-2",
|
|
36
|
+
md: "h-8 w-8 border-3",
|
|
37
|
+
lg: "h-12 w-12 border-4",
|
|
38
|
+
xl: "h-16 w-16 border-4",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
className={cn(
|
|
44
|
+
"animate-spin rounded-full border-primary border-t-transparent",
|
|
45
|
+
sizeClasses[size],
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Full-page loading state component
|
|
54
|
+
*
|
|
55
|
+
* Displays a centered spinner for full-page loading scenarios.
|
|
56
|
+
* Use for page transitions and initial data loading.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* <LoadingPage />
|
|
60
|
+
*/
|
|
61
|
+
export function LoadingPage() {
|
|
62
|
+
return (
|
|
63
|
+
<div className="flex items-center justify-center w-full h-full min-h-screen">
|
|
64
|
+
<Spinner size="lg" />
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Full-screen loading skeleton component
|
|
71
|
+
*
|
|
72
|
+
* Displays a full-screen loading state with sidebar placeholder
|
|
73
|
+
* to prevent layout shift during initial hydration.
|
|
74
|
+
* Use during app initialization in dashboard layout.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* <LoadingSkeleton />
|
|
78
|
+
*/
|
|
79
|
+
export function LoadingSkeleton() {
|
|
80
|
+
return (
|
|
81
|
+
<div className="flex h-screen overflow-hidden bg-background">
|
|
82
|
+
{/* Sidebar Skeleton */}
|
|
83
|
+
<aside className="w-56 border-r border-border bg-background flex flex-col">
|
|
84
|
+
<div className="flex-1" />
|
|
85
|
+
</aside>
|
|
86
|
+
|
|
87
|
+
{/* Main Content Skeleton */}
|
|
88
|
+
<main className="flex-1 overflow-auto bg-background p-6">
|
|
89
|
+
<div className="flex items-center justify-center h-full">
|
|
90
|
+
<Spinner size="lg" />
|
|
91
|
+
</div>
|
|
92
|
+
</main>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Overlay loading state component
|
|
99
|
+
*
|
|
100
|
+
* Displays a semi-transparent overlay with centered spinner.
|
|
101
|
+
* Use for asynchronous operations on pages that should remain interactive.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* {isLoading && <LoadingOverlay />}
|
|
105
|
+
*/
|
|
106
|
+
export function LoadingOverlay() {
|
|
107
|
+
return (
|
|
108
|
+
<div className="absolute inset-0 bg-background/50 backdrop-blur-sm z-50 flex items-center justify-center">
|
|
109
|
+
<Spinner size="lg" />
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -1,15 +1,40 @@
|
|
|
1
1
|
import { forwardRef, type TextareaHTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Textarea component props
|
|
6
|
+
*
|
|
7
|
+
* Extends standard HTML textarea attributes
|
|
8
|
+
*/
|
|
3
9
|
interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
|
4
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Textarea component
|
|
13
|
+
*
|
|
14
|
+
* A multi-line text input field with consistent styling.
|
|
15
|
+
* Automatically prevents resizing via CSS (use JavaScript if needed).
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // Basic textarea
|
|
19
|
+
* <Textarea placeholder="Enter description..." />
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // With value binding
|
|
23
|
+
* <Textarea
|
|
24
|
+
* value={description}
|
|
25
|
+
* onChange={(e) => setDescription(e.target.value)}
|
|
26
|
+
* disabled={isLoading}
|
|
27
|
+
* />
|
|
28
|
+
*/
|
|
5
29
|
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
6
30
|
({ className, ...props }, ref) => {
|
|
7
31
|
return (
|
|
8
32
|
<textarea
|
|
9
33
|
ref={ref}
|
|
10
|
-
className={
|
|
11
|
-
|
|
12
|
-
|
|
34
|
+
className={cn(
|
|
35
|
+
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 resize-none",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
13
38
|
{...props}
|
|
14
39
|
/>
|
|
15
40
|
);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Toaster as SonnerToaster, toast } from "sonner";
|
|
4
|
+
|
|
5
|
+
// Re-export toast for convenience
|
|
6
|
+
export { toast };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Toaster component
|
|
10
|
+
*
|
|
11
|
+
* Renders the toast notification container.
|
|
12
|
+
* Place at root level in your app (typically in layout).
|
|
13
|
+
* Handles all notification positioning and styling.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // In layout.tsx
|
|
17
|
+
* export default function Layout() {
|
|
18
|
+
* return (
|
|
19
|
+
* <>
|
|
20
|
+
* {children}
|
|
21
|
+
* <Toaster />
|
|
22
|
+
* </>
|
|
23
|
+
* );
|
|
24
|
+
* }
|
|
25
|
+
*/
|
|
26
|
+
export function Toaster() {
|
|
27
|
+
return (
|
|
28
|
+
<SonnerToaster
|
|
29
|
+
position="top-center"
|
|
30
|
+
expand={false}
|
|
31
|
+
richColors
|
|
32
|
+
theme="dark"
|
|
33
|
+
toastOptions={{
|
|
34
|
+
style: {
|
|
35
|
+
background: "rgba(23, 23, 23, 0.95)",
|
|
36
|
+
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
37
|
+
backdropFilter: "blur(12px)",
|
|
38
|
+
},
|
|
39
|
+
classNames: {
|
|
40
|
+
toast: "rounded-xl shadow-xl",
|
|
41
|
+
title: "font-semibold text-sm",
|
|
42
|
+
description: "text-muted-foreground text-xs",
|
|
43
|
+
success: "!bg-emerald-950/90 !border-emerald-500/30",
|
|
44
|
+
error: "!bg-rose-950/90 !border-rose-500/30",
|
|
45
|
+
warning: "!bg-amber-950/90 !border-amber-500/30",
|
|
46
|
+
info: "!bg-sky-950/90 !border-sky-500/30",
|
|
47
|
+
},
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Toast notification convenience wrapper
|
|
55
|
+
*
|
|
56
|
+
* Provides typed toast methods with consistent options.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* showToast.success("Saved!", "Your changes have been saved");
|
|
60
|
+
* showToast.error("Error", "Something went wrong");
|
|
61
|
+
* showToast.promise(apiCall(), {
|
|
62
|
+
* loading: "Saving...",
|
|
63
|
+
* success: "Saved!",
|
|
64
|
+
* error: "Error saving",
|
|
65
|
+
* });
|
|
66
|
+
*/
|
|
67
|
+
export const showToast = {
|
|
68
|
+
/** Success notification */
|
|
69
|
+
success: (title: string, description?: string) => {
|
|
70
|
+
toast.success(title, { description, dismissible: true });
|
|
71
|
+
},
|
|
72
|
+
/** Error notification */
|
|
73
|
+
error: (title: string, description?: string) => {
|
|
74
|
+
toast.error(title, { description, dismissible: true });
|
|
75
|
+
},
|
|
76
|
+
/** Warning notification */
|
|
77
|
+
warning: (title: string, description?: string) => {
|
|
78
|
+
toast.warning(title, { description, dismissible: true });
|
|
79
|
+
},
|
|
80
|
+
/** Info notification */
|
|
81
|
+
info: (title: string, description?: string) => {
|
|
82
|
+
toast.info(title, { description, dismissible: true });
|
|
83
|
+
},
|
|
84
|
+
/** Loading notification (returns ID for updating) */
|
|
85
|
+
loading: (title: string, description?: string) => {
|
|
86
|
+
return toast.loading(title, { description, dismissible: true });
|
|
87
|
+
},
|
|
88
|
+
/** Promise-based notification for async operations */
|
|
89
|
+
promise: <T,>(
|
|
90
|
+
promise: Promise<T>,
|
|
91
|
+
messages: {
|
|
92
|
+
loading: string;
|
|
93
|
+
success: string;
|
|
94
|
+
error: string;
|
|
95
|
+
}
|
|
96
|
+
) => {
|
|
97
|
+
return toast.promise(promise, messages);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { motion } from "framer-motion";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Toggle component props
|
|
7
|
+
*
|
|
8
|
+
* @property checked - Whether toggle is on
|
|
9
|
+
* @property onChange - Callback when toggle state changes
|
|
10
|
+
* @property disabled - Disable toggle interaction
|
|
11
|
+
*/
|
|
12
|
+
interface ToggleProps {
|
|
13
|
+
/** Whether toggle is currently on/checked */
|
|
14
|
+
checked: boolean;
|
|
15
|
+
/** Callback when toggle state changes */
|
|
16
|
+
onChange: (checked: boolean) => void;
|
|
17
|
+
/** Disable toggle */
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Toggle component
|
|
23
|
+
*
|
|
24
|
+
* An accessible, animated toggle switch component.
|
|
25
|
+
* Uses Framer Motion for smooth animations.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const [isEnabled, setIsEnabled] = useState(false);
|
|
29
|
+
* <Toggle
|
|
30
|
+
* checked={isEnabled}
|
|
31
|
+
* onChange={setIsEnabled}
|
|
32
|
+
* />
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // Disabled toggle
|
|
36
|
+
* <Toggle
|
|
37
|
+
* checked={true}
|
|
38
|
+
* onChange={handleChange}
|
|
39
|
+
* disabled={isLoading}
|
|
40
|
+
* />
|
|
41
|
+
*/
|
|
42
|
+
export function Toggle({ checked, onChange, disabled }: ToggleProps) {
|
|
43
|
+
return (
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
role="switch"
|
|
47
|
+
aria-checked={checked}
|
|
48
|
+
aria-label={checked ? "Enabled" : "Disabled"}
|
|
49
|
+
disabled={disabled}
|
|
50
|
+
onClick={() => onChange(!checked)}
|
|
51
|
+
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 ${
|
|
52
|
+
checked ? "bg-primary" : "bg-secondary"
|
|
53
|
+
} ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
|
|
54
|
+
>
|
|
55
|
+
<motion.span
|
|
56
|
+
animate={{ x: checked ? 20 : 0 }}
|
|
57
|
+
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
|
58
|
+
className="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-lg ring-0"
|
|
59
|
+
aria-hidden="true"
|
|
60
|
+
/>
|
|
61
|
+
</button>
|
|
62
|
+
);
|
|
63
|
+
}
|