@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,142 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { SecondaryText } from "@/components/typography";
|
|
5
|
+
import { Button, Modal } from "@/components/ui";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
export interface CreateModalField {
|
|
9
|
+
/** Field identifier */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Display label */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Form component (Input, Textarea, etc) */
|
|
14
|
+
component: React.ReactNode;
|
|
15
|
+
/** Whether field is required */
|
|
16
|
+
required?: boolean;
|
|
17
|
+
/** Help text for field */
|
|
18
|
+
help?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CreateModalProps {
|
|
22
|
+
/** Whether modal is open */
|
|
23
|
+
isOpen: boolean;
|
|
24
|
+
/** Modal title */
|
|
25
|
+
title: string;
|
|
26
|
+
/** Array of form fields */
|
|
27
|
+
fields: CreateModalField[];
|
|
28
|
+
/** Form submission handler */
|
|
29
|
+
onSubmit: (e: React.FormEvent) => Promise<void> | void;
|
|
30
|
+
/** Called to close modal */
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
/** Submit button text */
|
|
33
|
+
submitText?: string;
|
|
34
|
+
/** Submit button variant */
|
|
35
|
+
submitVariant?: "primary" | "secondary";
|
|
36
|
+
/** Cancel button text */
|
|
37
|
+
cancelText?: string;
|
|
38
|
+
/** Whether submission is in progress */
|
|
39
|
+
isPending?: boolean;
|
|
40
|
+
/** Whether submit button is disabled */
|
|
41
|
+
submitDisabled?: boolean;
|
|
42
|
+
/** Optional icon for modal header */
|
|
43
|
+
icon?: React.ReactNode;
|
|
44
|
+
/** Modal size */
|
|
45
|
+
size?: "sm" | "md" | "lg";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create Modal Component
|
|
50
|
+
*
|
|
51
|
+
* Reusable compound component for create/edit modals.
|
|
52
|
+
* Provides consistent styling, field rendering, and form submission.
|
|
53
|
+
* Supports custom fields, icons, and submission handlers.
|
|
54
|
+
*
|
|
55
|
+
* Features:
|
|
56
|
+
* - Customizable form fields
|
|
57
|
+
* - Consistent styling and layout
|
|
58
|
+
* - Submit and cancel actions
|
|
59
|
+
* - Loading state handling
|
|
60
|
+
* - Optional icon display
|
|
61
|
+
* - Multiple size options
|
|
62
|
+
*
|
|
63
|
+
* @component
|
|
64
|
+
* @example
|
|
65
|
+
* <CreateModal
|
|
66
|
+
* isOpen={isOpen}
|
|
67
|
+
* title="Create Task"
|
|
68
|
+
* fields={[
|
|
69
|
+
* {
|
|
70
|
+
* name: "title",
|
|
71
|
+
* label: "Task Title",
|
|
72
|
+
* component: <Input {...titleProps} />,
|
|
73
|
+
* required: true,
|
|
74
|
+
* },
|
|
75
|
+
* ]}
|
|
76
|
+
* onSubmit={handleSubmit}
|
|
77
|
+
* onClose={handleClose}
|
|
78
|
+
* submitText="Create"
|
|
79
|
+
* isPending={isLoading}
|
|
80
|
+
* />
|
|
81
|
+
*/
|
|
82
|
+
export function CreateModal({
|
|
83
|
+
isOpen,
|
|
84
|
+
title,
|
|
85
|
+
fields,
|
|
86
|
+
onSubmit,
|
|
87
|
+
onClose,
|
|
88
|
+
submitText = "Submit",
|
|
89
|
+
submitVariant = "primary",
|
|
90
|
+
cancelText = "Cancel",
|
|
91
|
+
isPending = false,
|
|
92
|
+
submitDisabled = false,
|
|
93
|
+
icon,
|
|
94
|
+
size = "md",
|
|
95
|
+
}: CreateModalProps) {
|
|
96
|
+
return (
|
|
97
|
+
<Modal isOpen={isOpen} onClose={onClose} title={title} size={size}>
|
|
98
|
+
<form
|
|
99
|
+
onSubmit={onSubmit}
|
|
100
|
+
className={cn("space-y-6 py-2", "md:space-y-8")}
|
|
101
|
+
>
|
|
102
|
+
{fields.map((field) => (
|
|
103
|
+
<div key={field.name} className="space-y-3">
|
|
104
|
+
<SecondaryText as="label" size="xs" className="block ml-1">
|
|
105
|
+
{field.label}
|
|
106
|
+
{field.required && <span className="text-destructive">*</span>}
|
|
107
|
+
</SecondaryText>
|
|
108
|
+
{field.component}
|
|
109
|
+
{field.help && (
|
|
110
|
+
<p className="text-xs text-muted-foreground/60 ml-1">
|
|
111
|
+
{field.help}
|
|
112
|
+
</p>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
))}
|
|
116
|
+
|
|
117
|
+
<div className="flex justify-end gap-3 pt-6 border-t mt-4">
|
|
118
|
+
<Button
|
|
119
|
+
type="button"
|
|
120
|
+
onClick={onClose}
|
|
121
|
+
variant="ghost"
|
|
122
|
+
className="px-6"
|
|
123
|
+
disabled={isPending}
|
|
124
|
+
>
|
|
125
|
+
{cancelText}
|
|
126
|
+
</Button>
|
|
127
|
+
<Button
|
|
128
|
+
type="submit"
|
|
129
|
+
variant={submitVariant}
|
|
130
|
+
disabled={submitDisabled || isPending}
|
|
131
|
+
isLoading={isPending}
|
|
132
|
+
loadingText={`${submitText}...`}
|
|
133
|
+
className="px-8 shadow-lg shadow-primary/10"
|
|
134
|
+
>
|
|
135
|
+
{icon && <span className="mr-2">{icon}</span>}
|
|
136
|
+
{submitText}
|
|
137
|
+
</Button>
|
|
138
|
+
</div>
|
|
139
|
+
</form>
|
|
140
|
+
</Modal>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./CreateModal";
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rich Text Editor Component
|
|
3
|
+
*
|
|
4
|
+
* TipTap-based rich text editor with markdown support.
|
|
5
|
+
* Includes formatting toolbar and multiple editing modes.
|
|
6
|
+
* Supports headings, lists, code blocks, links, and more.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Markdown support with live preview
|
|
10
|
+
* - Rich formatting toolbar
|
|
11
|
+
* - Code syntax highlighting
|
|
12
|
+
* - Task lists and checkboxes
|
|
13
|
+
* - Link editing
|
|
14
|
+
* - Undo/redo functionality
|
|
15
|
+
* - Heading and list formatting
|
|
16
|
+
* - Quote and code block support
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* <Editor
|
|
20
|
+
* value={markdown}
|
|
21
|
+
* onChange={handleChange}
|
|
22
|
+
* placeholder="Write your documentation..."
|
|
23
|
+
* />
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
"use client";
|
|
27
|
+
|
|
28
|
+
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
|
29
|
+
import { Color } from "@tiptap/extension-color";
|
|
30
|
+
import Highlight from "@tiptap/extension-highlight";
|
|
31
|
+
import Link from "@tiptap/extension-link";
|
|
32
|
+
import Placeholder from "@tiptap/extension-placeholder";
|
|
33
|
+
import TaskItem from "@tiptap/extension-task-item";
|
|
34
|
+
import TaskList from "@tiptap/extension-task-list";
|
|
35
|
+
import { TextStyle } from "@tiptap/extension-text-style";
|
|
36
|
+
import Typography from "@tiptap/extension-typography";
|
|
37
|
+
import type { Editor as TiptapEditor } from "@tiptap/react";
|
|
38
|
+
import { EditorContent, useEditor } from "@tiptap/react";
|
|
39
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
40
|
+
import { common, createLowlight } from "lowlight";
|
|
41
|
+
import {
|
|
42
|
+
Bold,
|
|
43
|
+
CheckSquare,
|
|
44
|
+
Code as CodeIcon,
|
|
45
|
+
Heading1,
|
|
46
|
+
Heading2,
|
|
47
|
+
Heading3,
|
|
48
|
+
Highlighter,
|
|
49
|
+
Italic,
|
|
50
|
+
List,
|
|
51
|
+
ListOrdered,
|
|
52
|
+
Quote,
|
|
53
|
+
Redo,
|
|
54
|
+
Terminal,
|
|
55
|
+
Undo,
|
|
56
|
+
} from "lucide-react";
|
|
57
|
+
import { useEffect } from "react";
|
|
58
|
+
import { Markdown } from "tiptap-markdown";
|
|
59
|
+
import { cn } from "@/lib/utils";
|
|
60
|
+
|
|
61
|
+
const lowlight = createLowlight(common);
|
|
62
|
+
|
|
63
|
+
interface EditorProps {
|
|
64
|
+
value: string;
|
|
65
|
+
onChange: (value: string) => void;
|
|
66
|
+
readOnly?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const MenuBar = ({ editor }: { editor: TiptapEditor | null }) => {
|
|
70
|
+
if (!editor) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const buttons = [
|
|
75
|
+
{
|
|
76
|
+
icon: Heading1,
|
|
77
|
+
onClick: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
|
78
|
+
isActive: editor.isActive("heading", { level: 1 }),
|
|
79
|
+
label: "H1",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
icon: Heading2,
|
|
83
|
+
onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
84
|
+
isActive: editor.isActive("heading", { level: 2 }),
|
|
85
|
+
label: "H2",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
icon: Heading3,
|
|
89
|
+
onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
90
|
+
isActive: editor.isActive("heading", { level: 3 }),
|
|
91
|
+
label: "H3",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
icon: Bold,
|
|
95
|
+
onClick: () => editor.chain().focus().toggleBold().run(),
|
|
96
|
+
isActive: editor.isActive("bold"),
|
|
97
|
+
label: "Bold",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
icon: Italic,
|
|
101
|
+
onClick: () => editor.chain().focus().toggleItalic().run(),
|
|
102
|
+
isActive: editor.isActive("italic"),
|
|
103
|
+
label: "Italic",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
icon: Highlighter,
|
|
107
|
+
onClick: () => editor.chain().focus().toggleHighlight().run(),
|
|
108
|
+
isActive: editor.isActive("highlight"),
|
|
109
|
+
label: "Highlight",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
icon: CodeIcon,
|
|
113
|
+
onClick: () => editor.chain().focus().toggleCode().run(),
|
|
114
|
+
isActive: editor.isActive("code"),
|
|
115
|
+
label: "Code",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
icon: Terminal,
|
|
119
|
+
onClick: () => editor.chain().focus().toggleCodeBlock().run(),
|
|
120
|
+
isActive: editor.isActive("codeBlock"),
|
|
121
|
+
label: "Code Block",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
icon: List,
|
|
125
|
+
onClick: () => editor.chain().focus().toggleBulletList().run(),
|
|
126
|
+
isActive: editor.isActive("bulletList"),
|
|
127
|
+
label: "Bullet List",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
icon: ListOrdered,
|
|
131
|
+
onClick: () => editor.chain().focus().toggleOrderedList().run(),
|
|
132
|
+
isActive: editor.isActive("orderedList"),
|
|
133
|
+
label: "Ordered List",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
icon: CheckSquare,
|
|
137
|
+
onClick: () => editor.chain().focus().toggleTaskList().run(),
|
|
138
|
+
isActive: editor.isActive("taskList"),
|
|
139
|
+
label: "Task List",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
icon: Quote,
|
|
143
|
+
onClick: () => editor.chain().focus().toggleBlockquote().run(),
|
|
144
|
+
isActive: editor.isActive("blockquote"),
|
|
145
|
+
label: "Quote",
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div className="flex flex-wrap items-center gap-1 p-2 bg-card/50 backdrop-blur-md border-b border-border/40 sticky top-0 z-10">
|
|
151
|
+
{buttons.map((btn, i) => (
|
|
152
|
+
<button
|
|
153
|
+
key={i}
|
|
154
|
+
onClick={(e) => {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
btn.onClick();
|
|
157
|
+
}}
|
|
158
|
+
className={cn(
|
|
159
|
+
"p-2 rounded-lg transition-all hover:bg-secondary/80",
|
|
160
|
+
btn.isActive
|
|
161
|
+
? "bg-primary text-primary-foreground shadow-sm"
|
|
162
|
+
: "text-muted-foreground"
|
|
163
|
+
)}
|
|
164
|
+
title={btn.label}
|
|
165
|
+
>
|
|
166
|
+
<btn.icon size={16} />
|
|
167
|
+
</button>
|
|
168
|
+
))}
|
|
169
|
+
<div className="w-px h-6 bg-border/40 mx-1" />
|
|
170
|
+
<button
|
|
171
|
+
onClick={(e) => {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
editor.chain().focus().undo().run();
|
|
174
|
+
}}
|
|
175
|
+
disabled={!editor.can().chain().focus().undo().run()}
|
|
176
|
+
className="p-2 rounded-lg text-muted-foreground hover:bg-secondary/80 disabled:opacity-30"
|
|
177
|
+
>
|
|
178
|
+
<Undo size={16} />
|
|
179
|
+
</button>
|
|
180
|
+
<button
|
|
181
|
+
onClick={(e) => {
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
editor.chain().focus().redo().run();
|
|
184
|
+
}}
|
|
185
|
+
disabled={!editor.can().chain().focus().redo().run()}
|
|
186
|
+
className="p-2 rounded-lg text-muted-foreground hover:bg-secondary/80 disabled:opacity-30"
|
|
187
|
+
>
|
|
188
|
+
<Redo size={16} />
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export function Editor({ value, onChange, readOnly = false }: EditorProps) {
|
|
195
|
+
const editor = useEditor({
|
|
196
|
+
extensions: [
|
|
197
|
+
StarterKit.configure({
|
|
198
|
+
bulletList: {
|
|
199
|
+
keepMarks: true,
|
|
200
|
+
keepAttributes: false,
|
|
201
|
+
},
|
|
202
|
+
orderedList: {
|
|
203
|
+
keepMarks: true,
|
|
204
|
+
keepAttributes: false,
|
|
205
|
+
},
|
|
206
|
+
codeBlock: false,
|
|
207
|
+
}),
|
|
208
|
+
Placeholder.configure({
|
|
209
|
+
placeholder: "Initialize content flow...",
|
|
210
|
+
}),
|
|
211
|
+
Markdown.configure({
|
|
212
|
+
html: true,
|
|
213
|
+
tightLists: true,
|
|
214
|
+
tightListClass: "tight",
|
|
215
|
+
bulletListMarker: "-",
|
|
216
|
+
linkify: true,
|
|
217
|
+
breaks: true,
|
|
218
|
+
}),
|
|
219
|
+
Highlight.configure({ multicolor: true }),
|
|
220
|
+
TaskList,
|
|
221
|
+
TaskItem.configure({
|
|
222
|
+
nested: true,
|
|
223
|
+
}),
|
|
224
|
+
Link.configure({
|
|
225
|
+
openOnClick: false,
|
|
226
|
+
HTMLAttributes: {
|
|
227
|
+
class: "text-primary underline underline-offset-4",
|
|
228
|
+
},
|
|
229
|
+
}),
|
|
230
|
+
Typography,
|
|
231
|
+
CodeBlockLowlight.configure({
|
|
232
|
+
lowlight,
|
|
233
|
+
}),
|
|
234
|
+
TextStyle,
|
|
235
|
+
Color,
|
|
236
|
+
],
|
|
237
|
+
immediatelyRender: false,
|
|
238
|
+
content: value,
|
|
239
|
+
editable: !readOnly,
|
|
240
|
+
onUpdate: ({ editor }) => {
|
|
241
|
+
// biome-ignore lint/suspicious/noExplicitAny: Tiptap storage is not strictly typed
|
|
242
|
+
const markdown = (editor.storage as any).markdown.getMarkdown();
|
|
243
|
+
onChange(markdown);
|
|
244
|
+
},
|
|
245
|
+
editorProps: {
|
|
246
|
+
attributes: {
|
|
247
|
+
class: cn(
|
|
248
|
+
"prose prose-invert max-w-none min-h-[500px] p-8 focus:outline-none",
|
|
249
|
+
"prose-pre:bg-secondary/40 prose-pre:border prose-pre:border-border/40 prose-pre:rounded-xl",
|
|
250
|
+
"prose-p:text-muted-foreground prose-p:leading-relaxed",
|
|
251
|
+
"prose-li:text-muted-foreground",
|
|
252
|
+
"prose-strong:text-foreground prose-strong:font-bold",
|
|
253
|
+
"prose-code:text-primary prose-code:bg-primary/5 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none",
|
|
254
|
+
"prose-blockquote:border-l-primary prose-blockquote:bg-primary/5 prose-blockquote:py-2 prose-blockquote:rounded-r-xl"
|
|
255
|
+
),
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Handle value updates from outside
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (editor) {
|
|
263
|
+
// biome-ignore lint/suspicious/noExplicitAny: Tiptap storage is not strictly typed
|
|
264
|
+
const currentMarkdown = (editor.storage as any).markdown.getMarkdown();
|
|
265
|
+
if (value !== currentMarkdown) {
|
|
266
|
+
editor.commands.setContent(value);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}, [value, editor]);
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div className="w-full h-full flex flex-col">
|
|
273
|
+
{!readOnly && <MenuBar editor={editor} />}
|
|
274
|
+
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
|
275
|
+
<EditorContent editor={editor} />
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
@@ -1,21 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page Header Component
|
|
3
|
+
*
|
|
4
|
+
* Displays page title, subtitle, icon, and actions.
|
|
5
|
+
* Supports optional search with keyboard shortcut (Cmd+K).
|
|
6
|
+
* Used on main application pages for consistent header layout.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Title and subtitle display
|
|
10
|
+
* - Optional icon
|
|
11
|
+
* - Action buttons area
|
|
12
|
+
* - Search with Cmd+K keyboard shortcut
|
|
13
|
+
* - Flexible layout for custom content
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* <PageHeader
|
|
17
|
+
* title="Dashboard"
|
|
18
|
+
* subtitle="Your workspace overview"
|
|
19
|
+
* icon={<LayoutDashboard />}
|
|
20
|
+
* actions={<Button>Export</Button>}
|
|
21
|
+
* />
|
|
22
|
+
*/
|
|
23
|
+
|
|
1
24
|
"use client";
|
|
2
25
|
|
|
3
26
|
import { Search } from "lucide-react";
|
|
27
|
+
import { type ReactNode, useEffect, useRef } from "react";
|
|
28
|
+
|
|
29
|
+
interface PageHeaderProps {
|
|
30
|
+
title: string;
|
|
31
|
+
subtitle?: string;
|
|
32
|
+
icon?: ReactNode;
|
|
33
|
+
actions?: ReactNode;
|
|
34
|
+
children?: ReactNode;
|
|
35
|
+
searchValue?: string;
|
|
36
|
+
onSearchChange?: (value: string) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function PageHeader({
|
|
40
|
+
title,
|
|
41
|
+
subtitle,
|
|
42
|
+
icon,
|
|
43
|
+
actions,
|
|
44
|
+
children,
|
|
45
|
+
searchValue,
|
|
46
|
+
onSearchChange,
|
|
47
|
+
}: PageHeaderProps) {
|
|
48
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
52
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
inputRef.current?.focus();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
58
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
59
|
+
}, []);
|
|
4
60
|
|
|
5
|
-
export function Header() {
|
|
6
61
|
return (
|
|
7
|
-
<header className="
|
|
8
|
-
<div className="
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
62
|
+
<header className="mb-6">
|
|
63
|
+
<div className="flex items-start justify-between gap-4">
|
|
64
|
+
{/* Left: Title area */}
|
|
65
|
+
<div className="flex items-center gap-4">
|
|
66
|
+
{icon && (
|
|
67
|
+
<div className="p-2.5 rounded-xl bg-linear-to-br from-primary/20 to-primary/5 border border-primary/20">
|
|
68
|
+
{icon}
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
<div>
|
|
72
|
+
<h1 className="text-xl font-bold text-foreground">{title}</h1>
|
|
73
|
+
{subtitle && (
|
|
74
|
+
<p className="text-sm text-muted-foreground mt-0.5">{subtitle}</p>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{/* Right: Search + Actions */}
|
|
80
|
+
<div className="flex items-center gap-3">
|
|
81
|
+
{/* Search */}
|
|
82
|
+
{onSearchChange && (
|
|
83
|
+
<div className="relative group hidden sm:block">
|
|
84
|
+
<Search
|
|
85
|
+
size={14}
|
|
86
|
+
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground group-focus-within:text-primary transition-colors"
|
|
87
|
+
/>
|
|
88
|
+
<input
|
|
89
|
+
ref={inputRef}
|
|
90
|
+
type="text"
|
|
91
|
+
placeholder="Search... (⌘K)"
|
|
92
|
+
value={searchValue}
|
|
93
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
94
|
+
className="w-48 lg:w-56 bg-secondary/30 border border-border/40 rounded-lg pl-9 pr-3 py-2 text-xs text-foreground placeholder:text-muted-foreground/60 outline-none transition-all focus:border-primary/50 focus:ring-2 focus:ring-primary/20 focus:bg-background hover:bg-secondary/50"
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
{/* Actions */}
|
|
100
|
+
{actions}
|
|
101
|
+
</div>
|
|
18
102
|
</div>
|
|
103
|
+
|
|
104
|
+
{/* Optional children (e.g., filter bar, inline forms) */}
|
|
105
|
+
{children}
|
|
19
106
|
</header>
|
|
20
107
|
);
|
|
21
108
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page Layout Component
|
|
3
|
+
*
|
|
4
|
+
* Standard layout wrapper for content pages.
|
|
5
|
+
* Provides consistent header, title, description, and content area.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <PageLayout
|
|
9
|
+
* title="Team Management"
|
|
10
|
+
* description="Manage your organization members"
|
|
11
|
+
* actions={<Button>Invite</Button>}
|
|
12
|
+
* >
|
|
13
|
+
* <TeamList />
|
|
14
|
+
* </PageLayout>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
"use client";
|
|
18
|
+
|
|
19
|
+
import { getTypographyClass } from "@/lib/typography";
|
|
20
|
+
import { cn } from "@/lib/utils";
|
|
21
|
+
|
|
22
|
+
interface PageLayoutProps {
|
|
23
|
+
/** Page title */
|
|
24
|
+
title: string;
|
|
25
|
+
/** Page description or subtitle */
|
|
26
|
+
description?: React.ReactNode;
|
|
27
|
+
/** Action buttons/controls for header */
|
|
28
|
+
actions?: React.ReactNode;
|
|
29
|
+
/** Main page content */
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
/** Optional className override */
|
|
32
|
+
className?: string;
|
|
33
|
+
/** Optional content className override */
|
|
34
|
+
contentClassName?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function PageLayout({
|
|
38
|
+
title,
|
|
39
|
+
description,
|
|
40
|
+
actions,
|
|
41
|
+
children,
|
|
42
|
+
className,
|
|
43
|
+
contentClassName,
|
|
44
|
+
}: PageLayoutProps) {
|
|
45
|
+
return (
|
|
46
|
+
<div className={cn("flex flex-col h-full overflow-hidden", className)}>
|
|
47
|
+
<div className="flex-none pb-2">
|
|
48
|
+
<div className="flex items-center justify-between mb-2">
|
|
49
|
+
<div>
|
|
50
|
+
<h1 className={cn(getTypographyClass("h1"), "text-foreground/90")}>
|
|
51
|
+
{title}
|
|
52
|
+
</h1>
|
|
53
|
+
{description && (
|
|
54
|
+
<div className={cn(getTypographyClass("caption"), "mt-1")}>
|
|
55
|
+
{description}
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
{actions && <div className="flex items-center gap-3">{actions}</div>}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div
|
|
63
|
+
className={cn("flex-1 overflow-y-auto pb-12 mt-4", contentClassName)}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|