@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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Avatar component props
|
|
8
|
+
*
|
|
9
|
+
* @property name - User name for initials fallback and alt text
|
|
10
|
+
* @property src - Optional image URL (if not provided, shows initials)
|
|
11
|
+
* @property size - Avatar size (default: "md")
|
|
12
|
+
*/
|
|
13
|
+
interface AvatarProps {
|
|
14
|
+
/** Image URL for avatar photo */
|
|
15
|
+
src?: string | null;
|
|
16
|
+
/** User name (used for initials and alt text) */
|
|
17
|
+
name: string;
|
|
18
|
+
/** Avatar size */
|
|
19
|
+
size?: "sm" | "md" | "lg";
|
|
20
|
+
/** Additional CSS classes */
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sizeClasses = {
|
|
25
|
+
sm: "h-6 w-6 text-[10px]",
|
|
26
|
+
md: "h-8 w-8 text-xs",
|
|
27
|
+
lg: "h-10 w-10 text-sm",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const imageSizes = {
|
|
31
|
+
sm: 24,
|
|
32
|
+
md: 32,
|
|
33
|
+
lg: 40,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract initials from user name
|
|
38
|
+
* @example getInitials("John Doe") // "JD"
|
|
39
|
+
*/
|
|
40
|
+
function getInitials(name: string): string {
|
|
41
|
+
return name
|
|
42
|
+
.split(" ")
|
|
43
|
+
.map((n) => n[0])
|
|
44
|
+
.join("")
|
|
45
|
+
.toUpperCase()
|
|
46
|
+
.slice(0, 2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get consistent color from user name using hash
|
|
51
|
+
* Ensures same name always gets same color
|
|
52
|
+
*/
|
|
53
|
+
function getColorFromName(name: string): string {
|
|
54
|
+
const colors = [
|
|
55
|
+
"bg-rose-500",
|
|
56
|
+
"bg-pink-500",
|
|
57
|
+
"bg-fuchsia-500",
|
|
58
|
+
"bg-purple-500",
|
|
59
|
+
"bg-violet-500",
|
|
60
|
+
"bg-indigo-500",
|
|
61
|
+
"bg-blue-500",
|
|
62
|
+
"bg-sky-500",
|
|
63
|
+
"bg-cyan-500",
|
|
64
|
+
"bg-teal-500",
|
|
65
|
+
"bg-emerald-500",
|
|
66
|
+
"bg-green-500",
|
|
67
|
+
"bg-lime-500",
|
|
68
|
+
"bg-yellow-500",
|
|
69
|
+
"bg-amber-500",
|
|
70
|
+
"bg-orange-500",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
let hash = 0;
|
|
74
|
+
for (let i = 0; i < name.length; i++) {
|
|
75
|
+
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return colors[Math.abs(hash) % colors.length];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Avatar component
|
|
83
|
+
*
|
|
84
|
+
* Displays user avatar with image or initials fallback.
|
|
85
|
+
* Automatically assigns consistent colors based on name hash.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* // With image
|
|
89
|
+
* <Avatar src="/avatar.jpg" name="John Doe" />
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* // Without image (shows initials)
|
|
93
|
+
* <Avatar name="Jane Smith" size="lg" />
|
|
94
|
+
*/
|
|
95
|
+
export function Avatar({ src, name, size = "md", className }: AvatarProps) {
|
|
96
|
+
if (src) {
|
|
97
|
+
return (
|
|
98
|
+
<Image
|
|
99
|
+
src={src}
|
|
100
|
+
alt={name}
|
|
101
|
+
width={imageSizes[size]}
|
|
102
|
+
height={imageSizes[size]}
|
|
103
|
+
className={cn(
|
|
104
|
+
"rounded-full object-cover ring-2 ring-border/50",
|
|
105
|
+
className
|
|
106
|
+
)}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div
|
|
113
|
+
className={cn(
|
|
114
|
+
"rounded-full flex items-center justify-center font-bold text-white ring-2 ring-border/50",
|
|
115
|
+
sizeClasses[size],
|
|
116
|
+
getColorFromName(name),
|
|
117
|
+
className
|
|
118
|
+
)}
|
|
119
|
+
>
|
|
120
|
+
{getInitials(name)}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -1,16 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Badge component props
|
|
3
|
+
*
|
|
4
|
+
* @property variant - Badge color variant
|
|
5
|
+
* @property size - Badge size (default: "sm")
|
|
6
|
+
*/
|
|
1
7
|
interface BadgeProps {
|
|
8
|
+
/** Badge content */
|
|
2
9
|
children: React.ReactNode;
|
|
3
|
-
|
|
10
|
+
/** Badge color variant */
|
|
11
|
+
variant?:
|
|
12
|
+
| "default"
|
|
13
|
+
| "success"
|
|
14
|
+
| "warning"
|
|
15
|
+
| "error"
|
|
16
|
+
| "info"
|
|
17
|
+
| "purple"
|
|
18
|
+
| "primary"
|
|
19
|
+
| "secondary"
|
|
20
|
+
| "outline";
|
|
21
|
+
/** Badge size */
|
|
4
22
|
size?: "sm" | "md";
|
|
23
|
+
/** Additional CSS classes */
|
|
24
|
+
className?: string;
|
|
5
25
|
}
|
|
6
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Badge component
|
|
29
|
+
*
|
|
30
|
+
* A small component used to display status, labels, or tags.
|
|
31
|
+
* Supports multiple variants and sizes.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // Basic badge
|
|
35
|
+
* <Badge>New</Badge>
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // Success badge
|
|
39
|
+
* <Badge variant="success">Active</Badge>
|
|
40
|
+
*/
|
|
7
41
|
export function Badge({
|
|
8
42
|
children,
|
|
9
43
|
variant = "default",
|
|
10
44
|
size = "sm",
|
|
45
|
+
className = "",
|
|
11
46
|
}: BadgeProps) {
|
|
12
47
|
const variants = {
|
|
13
48
|
default: "bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
|
49
|
+
primary: "bg-primary/15 text-primary border-primary/20",
|
|
50
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
51
|
+
outline: "text-foreground border-border",
|
|
14
52
|
success: "bg-status-done/15 text-status-done border-status-done/20",
|
|
15
53
|
warning:
|
|
16
54
|
"bg-status-progress/15 text-status-progress border-status-progress/20",
|
|
@@ -26,14 +64,20 @@ export function Badge({
|
|
|
26
64
|
|
|
27
65
|
return (
|
|
28
66
|
<span
|
|
29
|
-
className={`inline-flex items-center rounded-md border transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${variants[variant]} ${sizes[size]}`}
|
|
67
|
+
className={`inline-flex items-center rounded-md border transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${variants[variant]} ${sizes[size]} ${className}`}
|
|
30
68
|
>
|
|
31
69
|
{children}
|
|
32
70
|
</span>
|
|
33
71
|
);
|
|
34
72
|
}
|
|
35
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Priority badge component props
|
|
76
|
+
*
|
|
77
|
+
* @property priority - Task priority level
|
|
78
|
+
*/
|
|
36
79
|
interface PriorityBadgeProps {
|
|
80
|
+
/** Task priority level */
|
|
37
81
|
priority: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
|
|
38
82
|
}
|
|
39
83
|
|
|
@@ -44,12 +88,26 @@ const PRIORITY_CONFIG = {
|
|
|
44
88
|
CRITICAL: { label: "Critical", variant: "error" as const },
|
|
45
89
|
};
|
|
46
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Priority badge component
|
|
93
|
+
*
|
|
94
|
+
* Displays task priority with appropriate color coding.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* <PriorityBadge priority="HIGH" />
|
|
98
|
+
*/
|
|
47
99
|
export function PriorityBadge({ priority }: PriorityBadgeProps) {
|
|
48
100
|
const config = PRIORITY_CONFIG[priority];
|
|
49
101
|
return <Badge variant={config.variant}>{config.label}</Badge>;
|
|
50
102
|
}
|
|
51
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Status badge component props
|
|
106
|
+
*
|
|
107
|
+
* @property status - Task status
|
|
108
|
+
*/
|
|
52
109
|
interface StatusBadgeProps {
|
|
110
|
+
/** Task status value */
|
|
53
111
|
status: string;
|
|
54
112
|
}
|
|
55
113
|
|
|
@@ -68,6 +126,15 @@ const STATUS_CONFIG: Record<
|
|
|
68
126
|
BLOCKED: { label: "Blocked", variant: "error" },
|
|
69
127
|
};
|
|
70
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Status badge component
|
|
131
|
+
*
|
|
132
|
+
* Displays task status with appropriate color coding.
|
|
133
|
+
* Automatically maps status values to display labels and colors.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* <StatusBadge status="IN_PROGRESS" />
|
|
137
|
+
*/
|
|
71
138
|
export function StatusBadge({ status }: StatusBadgeProps) {
|
|
72
139
|
const config = STATUS_CONFIG[status] || {
|
|
73
140
|
label: status,
|
|
@@ -1,47 +1,88 @@
|
|
|
1
1
|
import { type ButtonHTMLAttributes, forwardRef } from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
import { BUTTON_SIZES, BUTTON_VARIANTS } from "./constants";
|
|
4
|
+
import { Spinner } from "./Spinner";
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Button component props
|
|
8
|
+
*
|
|
9
|
+
* @property variant - Button visual style (default: "primary")
|
|
10
|
+
* @property size - Button size (default: "md")
|
|
11
|
+
* @property isLoading - Show loading state with spinner
|
|
12
|
+
* @property loadingText - Text to show while loading (if not provided, shows original children)
|
|
13
|
+
*/
|
|
14
|
+
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
15
|
+
/** Button visual style */
|
|
16
|
+
variant?: keyof typeof BUTTON_VARIANTS;
|
|
17
|
+
/** Button size */
|
|
18
|
+
size?: keyof typeof BUTTON_SIZES;
|
|
19
|
+
/** Show loading state */
|
|
20
|
+
isLoading?: boolean;
|
|
21
|
+
/** Text to display when loading */
|
|
22
|
+
loadingText?: string;
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
15
|
-
(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
sm: "h-8 px-3 text-xs",
|
|
30
|
-
md: "h-9 px-4 py-2 text-sm",
|
|
31
|
-
lg: "h-10 px-8 text-base",
|
|
32
|
-
icon: "h-9 w-9",
|
|
33
|
-
};
|
|
26
|
+
(
|
|
27
|
+
{
|
|
28
|
+
className,
|
|
29
|
+
variant = "primary",
|
|
30
|
+
size = "md",
|
|
31
|
+
isLoading = false,
|
|
32
|
+
loadingText,
|
|
33
|
+
disabled,
|
|
34
|
+
children,
|
|
35
|
+
...props
|
|
36
|
+
},
|
|
37
|
+
ref
|
|
38
|
+
) => {
|
|
39
|
+
const isDisabled = isLoading || disabled;
|
|
34
40
|
|
|
35
41
|
return (
|
|
36
42
|
<button
|
|
37
43
|
ref={ref}
|
|
38
|
-
|
|
44
|
+
disabled={isDisabled}
|
|
45
|
+
className={cn(
|
|
46
|
+
"inline-flex items-center justify-center gap-2 font-semibold rounded-lg transition-all duration-200 active:scale-95 disabled:opacity-50 disabled:pointer-events-none",
|
|
47
|
+
BUTTON_VARIANTS[variant],
|
|
48
|
+
BUTTON_SIZES[size],
|
|
49
|
+
className
|
|
50
|
+
)}
|
|
51
|
+
aria-busy={isLoading}
|
|
39
52
|
{...props}
|
|
40
|
-
|
|
53
|
+
>
|
|
54
|
+
{isLoading ? (
|
|
55
|
+
<>
|
|
56
|
+
<Spinner size="sm" className="mr-2" aria-hidden="true" />
|
|
57
|
+
{loadingText || children}
|
|
58
|
+
</>
|
|
59
|
+
) : (
|
|
60
|
+
children
|
|
61
|
+
)}
|
|
62
|
+
</button>
|
|
41
63
|
);
|
|
42
64
|
}
|
|
43
65
|
);
|
|
44
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Button component
|
|
69
|
+
*
|
|
70
|
+
* A reusable button component with multiple variants and sizes.
|
|
71
|
+
* Supports loading state with spinner and automatic disabling.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* // Primary button
|
|
75
|
+
* <Button onClick={handleClick}>Click me</Button>
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* // Loading state
|
|
79
|
+
* <Button isLoading loadingText="Saving...">Save</Button>
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* // Different variants
|
|
83
|
+
* <Button variant="danger">Delete</Button>
|
|
84
|
+
* <Button variant="ghost">Cancel</Button>
|
|
85
|
+
*/
|
|
45
86
|
Button.displayName = "Button";
|
|
46
87
|
|
|
47
88
|
export { Button };
|
|
@@ -1,14 +1,46 @@
|
|
|
1
1
|
import { Check } from "lucide-react";
|
|
2
2
|
import { cn } from "@/lib/utils";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Checkbox component props
|
|
6
|
+
*
|
|
7
|
+
* @property checked - Whether checkbox is checked
|
|
8
|
+
* @property onChange - Callback when checkbox state changes
|
|
9
|
+
* @property label - Optional label text displayed next to checkbox
|
|
10
|
+
* @property disabled - Disable the checkbox (default: false)
|
|
11
|
+
*/
|
|
4
12
|
interface CheckboxProps {
|
|
13
|
+
/** Whether checkbox is currently checked */
|
|
5
14
|
checked: boolean;
|
|
15
|
+
/** Callback when checkbox state changes */
|
|
6
16
|
onChange: (checked: boolean) => void;
|
|
17
|
+
/** Optional label text */
|
|
7
18
|
label?: string;
|
|
19
|
+
/** Disable checkbox interaction */
|
|
8
20
|
disabled?: boolean;
|
|
21
|
+
/** Additional CSS classes */
|
|
9
22
|
className?: string;
|
|
10
23
|
}
|
|
11
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Checkbox component
|
|
27
|
+
*
|
|
28
|
+
* A custom checkbox with optional label and full accessibility support.
|
|
29
|
+
* Automatically applies line-through style to label when checked.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // Basic checkbox
|
|
33
|
+
* const [checked, setChecked] = useState(false);
|
|
34
|
+
* <Checkbox checked={checked} onChange={setChecked} />
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* // With label
|
|
38
|
+
* <Checkbox
|
|
39
|
+
* checked={isSubscribed}
|
|
40
|
+
* onChange={setIsSubscribed}
|
|
41
|
+
* label="Subscribe to newsletter"
|
|
42
|
+
* />
|
|
43
|
+
*/
|
|
12
44
|
export function Checkbox({
|
|
13
45
|
checked,
|
|
14
46
|
onChange,
|
|
@@ -30,11 +62,13 @@ export function Checkbox({
|
|
|
30
62
|
onChange={(e) => onChange(e.target.checked)}
|
|
31
63
|
disabled={disabled}
|
|
32
64
|
className="hidden"
|
|
65
|
+
aria-label={label}
|
|
33
66
|
/>
|
|
34
67
|
<span
|
|
35
68
|
className={`h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-150 flex items-center justify-center ${
|
|
36
69
|
checked ? "bg-primary text-primary-foreground" : "bg-transparent"
|
|
37
70
|
}`}
|
|
71
|
+
aria-hidden="true"
|
|
38
72
|
>
|
|
39
73
|
{checked && <Check size={12} strokeWidth={3} />}
|
|
40
74
|
</span>
|
|
@@ -3,22 +3,79 @@
|
|
|
3
3
|
import { ChevronDown } from "lucide-react";
|
|
4
4
|
import { type ReactNode, useEffect, useRef, useState } from "react";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Dropdown option configuration
|
|
8
|
+
*
|
|
9
|
+
* @property value - Unique value identifier
|
|
10
|
+
* @property label - Display label for option
|
|
11
|
+
* @property color - Optional color indicator
|
|
12
|
+
*/
|
|
6
13
|
interface DropdownOption<T extends string> {
|
|
14
|
+
/** Unique value identifier */
|
|
7
15
|
value: T;
|
|
16
|
+
/** Display label */
|
|
8
17
|
label: string;
|
|
18
|
+
/** Optional color for indicator dot */
|
|
9
19
|
color?: string;
|
|
10
20
|
}
|
|
11
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Dropdown component props
|
|
24
|
+
*
|
|
25
|
+
* @property value - Currently selected value
|
|
26
|
+
* @property onChange - Callback when selection changes
|
|
27
|
+
* @property options - Available options to select from
|
|
28
|
+
* @property placeholder - Placeholder text when no selection
|
|
29
|
+
* @property label - Optional label above dropdown
|
|
30
|
+
* @property disabled - Disable dropdown interaction
|
|
31
|
+
* @property renderOption - Custom option rendering
|
|
32
|
+
*/
|
|
12
33
|
interface DropdownProps<T extends string> {
|
|
34
|
+
/** Currently selected value */
|
|
13
35
|
value: T | undefined;
|
|
36
|
+
/** Callback when selection changes */
|
|
14
37
|
onChange: (value: T) => void;
|
|
38
|
+
/** Available options */
|
|
15
39
|
options: DropdownOption<T>[];
|
|
40
|
+
/** Placeholder text */
|
|
16
41
|
placeholder?: string;
|
|
42
|
+
/** Optional label */
|
|
17
43
|
label?: string;
|
|
44
|
+
/** Disable dropdown */
|
|
18
45
|
disabled?: boolean;
|
|
46
|
+
/** Custom option renderer */
|
|
19
47
|
renderOption?: (option: DropdownOption<T>) => ReactNode;
|
|
20
48
|
}
|
|
21
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Dropdown component
|
|
52
|
+
*
|
|
53
|
+
* A generic, type-safe dropdown select component with support for
|
|
54
|
+
* custom option rendering and color indicators.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // Basic dropdown
|
|
58
|
+
* const [status, setStatus] = useState<"pending" | "done">();
|
|
59
|
+
* <Dropdown
|
|
60
|
+
* value={status}
|
|
61
|
+
* onChange={setStatus}
|
|
62
|
+
* options={[
|
|
63
|
+
* { value: "pending", label: "Pending" },
|
|
64
|
+
* { value: "done", label: "Done" },
|
|
65
|
+
* ]}
|
|
66
|
+
* />
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // With color indicators
|
|
70
|
+
* <Dropdown
|
|
71
|
+
* value={priority}
|
|
72
|
+
* onChange={setPriority}
|
|
73
|
+
* options={[
|
|
74
|
+
* { value: "low", label: "Low", color: "#22c55e" },
|
|
75
|
+
* { value: "high", label: "High", color: "#ef4444" },
|
|
76
|
+
* ]}
|
|
77
|
+
* />
|
|
78
|
+
*/
|
|
22
79
|
export function Dropdown<T extends string>({
|
|
23
80
|
value,
|
|
24
81
|
onChange,
|
|
@@ -59,6 +116,8 @@ export function Dropdown<T extends string>({
|
|
|
59
116
|
className="w-full flex items-center justify-between gap-2 px-3.5 py-2.5 bg-background border border-input rounded-md text-foreground text-sm cursor-pointer transition-all hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
|
|
60
117
|
onClick={() => !disabled && setIsOpen(!isOpen)}
|
|
61
118
|
disabled={disabled}
|
|
119
|
+
aria-haspopup="listbox"
|
|
120
|
+
aria-expanded={isOpen}
|
|
62
121
|
>
|
|
63
122
|
<span className={selectedOption ? "" : "text-muted-foreground"}>
|
|
64
123
|
{selectedOption ? selectedOption.label : placeholder}
|
|
@@ -66,11 +125,15 @@ export function Dropdown<T extends string>({
|
|
|
66
125
|
<ChevronDown
|
|
67
126
|
size={16}
|
|
68
127
|
className={`transition-transform duration-200 text-muted-foreground ${isOpen ? "rotate-180" : "rotate-0"}`}
|
|
128
|
+
aria-hidden="true"
|
|
69
129
|
/>
|
|
70
130
|
</button>
|
|
71
131
|
|
|
72
132
|
{isOpen && (
|
|
73
|
-
<div
|
|
133
|
+
<div
|
|
134
|
+
className="absolute top-full left-0 right-0 mt-2 bg-popover border border-border rounded-md p-1 z-50 max-h-[240px] overflow-y-auto animate-in fade-in slide-in-from-top-2 duration-200 shadow-md"
|
|
135
|
+
role="listbox"
|
|
136
|
+
>
|
|
74
137
|
{options.map((option) => (
|
|
75
138
|
<button
|
|
76
139
|
key={option.value}
|
|
@@ -84,6 +147,8 @@ export function Dropdown<T extends string>({
|
|
|
84
147
|
onChange(option.value);
|
|
85
148
|
setIsOpen(false);
|
|
86
149
|
}}
|
|
150
|
+
role="option"
|
|
151
|
+
aria-selected={option.value === value}
|
|
87
152
|
>
|
|
88
153
|
{renderOption ? (
|
|
89
154
|
renderOption(option)
|
|
@@ -93,6 +158,7 @@ export function Dropdown<T extends string>({
|
|
|
93
158
|
<span
|
|
94
159
|
className="w-2 h-2 rounded-full shrink-0"
|
|
95
160
|
style={{ background: option.color }}
|
|
161
|
+
aria-hidden="true"
|
|
96
162
|
/>
|
|
97
163
|
)}
|
|
98
164
|
{option.label}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type LucideIcon, Sparkles } from "lucide-react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Empty state component props
|
|
9
|
+
*
|
|
10
|
+
* @property icon - Icon component to display
|
|
11
|
+
* @property title - Main empty state title
|
|
12
|
+
* @property description - Optional description text
|
|
13
|
+
* @property action - Optional action button/element
|
|
14
|
+
* @property variant - Display variant: default, compact, or minimal
|
|
15
|
+
*/
|
|
16
|
+
interface EmptyStateProps {
|
|
17
|
+
/** Icon component (default: Sparkles) */
|
|
18
|
+
icon?: LucideIcon;
|
|
19
|
+
/** Empty state title */
|
|
20
|
+
title: string;
|
|
21
|
+
/** Optional description text */
|
|
22
|
+
description?: string;
|
|
23
|
+
/** Optional action element (e.g., Button) */
|
|
24
|
+
action?: ReactNode;
|
|
25
|
+
/** Additional CSS classes */
|
|
26
|
+
className?: string;
|
|
27
|
+
/** Display variant */
|
|
28
|
+
variant?: "default" | "compact" | "minimal";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Empty state component
|
|
33
|
+
*
|
|
34
|
+
* Displays a friendly empty state UI for when no data is available.
|
|
35
|
+
* Supports three variants for different contexts.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // Default variant (large, centered)
|
|
39
|
+
* <EmptyState
|
|
40
|
+
* title="No tasks yet"
|
|
41
|
+
* description="Create your first task to get started"
|
|
42
|
+
* action={<Button>Create Task</Button>}
|
|
43
|
+
* />
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // Compact variant (smaller)
|
|
47
|
+
* <EmptyState
|
|
48
|
+
* variant="compact"
|
|
49
|
+
* title="No results"
|
|
50
|
+
* icon={Search}
|
|
51
|
+
* />
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* // Minimal variant (inline)
|
|
55
|
+
* <EmptyState
|
|
56
|
+
* variant="minimal"
|
|
57
|
+
* title="No items"
|
|
58
|
+
* icon={ListX}
|
|
59
|
+
* />
|
|
60
|
+
*/
|
|
61
|
+
export function EmptyState({
|
|
62
|
+
icon: Icon = Sparkles,
|
|
63
|
+
title,
|
|
64
|
+
description,
|
|
65
|
+
action,
|
|
66
|
+
className,
|
|
67
|
+
variant = "default",
|
|
68
|
+
}: EmptyStateProps) {
|
|
69
|
+
if (variant === "minimal") {
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
className={cn(
|
|
73
|
+
"flex flex-col items-center justify-center py-8 text-center",
|
|
74
|
+
className
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
<Icon
|
|
78
|
+
size={24}
|
|
79
|
+
className="text-muted-foreground/40 mb-2"
|
|
80
|
+
aria-hidden="true"
|
|
81
|
+
/>
|
|
82
|
+
<p className="text-sm text-muted-foreground/60 font-medium">{title}</p>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (variant === "compact") {
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
className={cn(
|
|
91
|
+
"flex flex-col items-center justify-center p-6 text-center bg-secondary/10 rounded-xl border border-dashed border-border/50",
|
|
92
|
+
className
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
<Icon size={20} className="text-primary/40 mb-2" aria-hidden="true" />
|
|
96
|
+
<h3 className="text-sm font-semibold text-foreground">{title}</h3>
|
|
97
|
+
{description && (
|
|
98
|
+
<p className="text-xs text-muted-foreground mt-1">{description}</p>
|
|
99
|
+
)}
|
|
100
|
+
{action && <div className="mt-3">{action}</div>}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
className={cn(
|
|
108
|
+
"flex flex-col items-center justify-center p-12 text-center",
|
|
109
|
+
className
|
|
110
|
+
)}
|
|
111
|
+
>
|
|
112
|
+
<div className="relative mb-6">
|
|
113
|
+
<div className="absolute inset-0 rounded-full" />
|
|
114
|
+
<div className="relative flex items-center justify-center w-20 h-20 rounded-2xl bg-linear-to-br from-secondary to-secondary/30 border border-border/50">
|
|
115
|
+
<Icon size={32} className="text-primary/60" aria-hidden="true" />
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<h3 className="text-xl font-bold text-foreground mb-2">{title}</h3>
|
|
120
|
+
{description && (
|
|
121
|
+
<p className="max-w-[300px] text-sm text-muted-foreground mb-8 leading-relaxed">
|
|
122
|
+
{description}
|
|
123
|
+
</p>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{action && <div>{action}</div>}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|