From c7854efae381a0e6bf11adc48902ef33bac790d9 Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Tue, 2 Dec 2025 09:54:38 +0100 Subject: [PATCH] feat: redo settings and all org pages. (#601) * feat: new components, deploy storybook * refactor: simplify component field definitions and enhance button loading state - Removed unnecessary type imports and simplified the FIELDS array definitions across multiple components in Storybook. - Updated button component to use a loading state instead of isLoading, improving clarity and consistency in props. - Adjusted button stories to reflect the new loading prop and enhanced the button's loading visual feedback. - Minor CSS adjustments for better layout and styling consistency. * chore: add field, card, password components * feat: improve page component & update AuthUI path * chore: update bits-ui to version 2.14.4 and all org related UI's - Updated bits-ui dependency in pnpm-lock.yaml and package.json to version 2.14.4. - Refactored imports for RoleBasedSecurity and UpgradeBanner components to use the new ui feature directory. - Removed unused Course components and related files to clean up the codebase. - Enhanced various components to improve consistency and maintainability. * chore: move all setting buttons to the top and update auth ui styles * chore: remove svelte-loading-spinners and replace with @cio/ui/base/spinner - Removed all instances of svelte-loading-spinners from the project. - Updated components to use the new Spinner from @cio/ui/base/spinner. - Cleaned up pnpm-lock.yaml and package.json to reflect the removal of svelte-loading-spinners. - Added new CSS animations for rocket launch effects in app.css. - Enhanced email change flow with updated verification messages in translations. * chore: remove supabase file upload from dashboard and move to r2 * chore: update organization API and UI components - Added new API methods for organization plan operations and team management. - Introduced middleware for API key authentication and organization admin checks. - Refactored organization-related components to utilize the new API structure. - Updated various UI components to improve consistency and maintainability. - Enhanced error handling and validation for organization-related actions. - Removed deprecated services and streamlined the codebase. --- .github/workflows/deploy-storybook.yml | 64 ++ .gitignore | 6 +- ARCHITECHTURE.md | 809 +++++++++++++++++- apps/api/src/app.ts | 4 +- apps/api/src/config/env.ts | 4 +- apps/api/src/constants/upload.ts | 19 +- apps/api/src/middlewares/api-key.ts | 58 ++ apps/api/src/middlewares/auth-or-api-key.ts | 34 + apps/api/src/middlewares/org-admin.ts | 62 ++ apps/api/src/routes/mail/mail.ts | 1 - apps/api/src/routes/media/index.ts | 1 + apps/api/src/routes/media/media.ts | 29 + .../src/routes/organization/organization.ts | 186 +++- apps/api/src/services/index.ts | 1 + apps/api/src/services/media.ts | 64 ++ apps/api/src/services/organization.ts | 202 ++++- apps/api/src/utils/errors.ts | 4 + apps/api/src/utils/s3.ts | 9 +- apps/api/src/utils/upload.ts | 12 +- apps/dashboard/ROUTE_PAGE_SETUP.md | 288 +++++++ apps/dashboard/package.json | 3 +- apps/dashboard/src/app.css | 202 ++++- .../src/lib/components/AuthUI/index.svelte | 124 --- .../lib/components/Buttons/Close/index.svelte | 4 +- .../components/Buttons/VisitOrgSite.svelte | 9 +- .../components/Ceritficate/Design.svelte | 2 +- .../Lesson/Exercise/ViewMode.svelte | 2 +- .../components/Lesson/Exercise/index.svelte | 2 +- .../components/Lesson/Exercises/index.svelte | 28 +- .../components/Lesson/LessonList.svelte | 22 +- .../Lesson/LessonSectionList.svelte | 2 +- .../Document/AddDocumentToLesson.svelte | 2 +- .../Lesson/Materials/Video/UploadVideo.svelte | 2 +- .../components/NewsFeed/NewsFeedCard.svelte | 2 +- .../components/People/InvitationModal.svelte | 27 +- .../components/People/ShareQRImage.svelte | 8 +- .../Course/components/Settings/index.svelte | 2 +- .../components/CourseContainer/index.svelte | 4 +- .../components/Editor/CertificateForm.svelte | 8 +- .../components/Editor/InstructorForm.svelte | 9 +- .../components/Editor/ReviewFormEditor.svelte | 10 +- .../components/CourseLandingPage/index.svelte | 2 +- .../components/LMS/components/Learning.svelte | 11 +- .../components/LMS/components/Profile.svelte | 2 +- .../src/lib/components/Modal/index.svelte | 2 +- .../components/Navigation/AuthButtons.svelte | 39 +- .../components/Navigation/CustomLinks.svelte | 10 +- .../lib/components/Navigation/index.svelte | 3 - .../Org/AddOrgModal/AddOrgModal.svelte | 139 +-- .../Org/Audience/AudienceList.svelte | 128 --- .../lib/components/Org/Audience/index.svelte | 39 - .../components/Org/Community/Loader.svelte | 26 - .../Org/LandingPage/PageLoader.svelte | 8 +- .../components/Org/LandingPage/index.svelte | 297 ++++--- .../Org/Quiz/Play/ConnectToPlay.svelte | 4 +- .../lib/components/Org/Quiz/Play/index.svelte | 4 +- .../Org/Settings/CustomizeLms.svelte | 2 +- .../components/Org/Settings/Domains.svelte | 2 +- .../Org/Settings/LandingpageSettings.svelte | 4 +- .../Org/Settings/OrgSettings.svelte | 2 +- .../Org/Settings/TeamMembers.svelte | 225 ----- .../lib/components/TextEditor/index.svelte | 6 +- .../src/lib/components/Upgrade/Banner.svelte | 28 - .../src/lib/components/Upgrade/Modal.svelte | 227 ----- .../lib/components/UploadWidget/index.svelte | 20 +- .../src/lib/features/app/layout-setup.ts | 22 +- .../lib/features/audience/components/index.ts | 1 + .../features/audience/pages/audience.svelte | 133 +++ .../src/lib/features/audience/pages/index.ts | 2 + .../audience/pages/user-analytics.svelte | 206 +++++ .../lib/features/auth/api/profile.svelte.ts | 31 +- .../components/ask-community-button.svelte | 22 + .../components/community-list-loader.svelte | 22 + .../community/components/delete-modal.svelte} | 0 .../features/community/components/index.ts | 3 + .../community/pages/community.svelte} | 98 ++- .../src/lib/features/community/pages/index.ts | 1 + .../course/components/card-loader.svelte} | 2 +- .../course/components/card.svelte} | 2 +- .../components/copy-course-modal.svelte} | 2 +- .../components/create-course-button.svelte | 28 + .../lib/features/course/components/index.ts | 7 + .../course/components/list-loader.svelte} | 0 .../course/components/list.svelte} | 4 +- .../components/new-course-modal.svelte} | 4 +- .../course/pages/courses.svelte} | 107 ++- .../src/lib/features/course/pages/index.ts | 1 + .../course/utils}/constants.ts | 0 .../course/utils}/functions.ts | 0 .../course/utils}/store.ts | 0 .../lib/features/org/api/org-plan.server.ts | 77 ++ .../lib/features/org/api/org-plan.svelte.ts | 12 +- .../src/lib/features/org/api/org.server.ts | 57 ++ .../src/lib/features/org/api/org.svelte.ts | 228 ++++- .../src/lib/features/org/api/types.ts | 5 + .../src/lib/features/org/utils/types.ts | 18 +- .../settings/components/Layout/Column.svelte | 41 + .../settings/components/Layout/Grid.svelte | 12 + .../settings/components/Layout/Row.svelte | 13 + .../settings/components/Layout/index.ts | 3 + .../lib/features/settings/components/index.ts | 2 + .../components/language-picker.svelte | 39 + .../features/settings/pages/billing.svelte | 80 ++ .../settings/pages/customize-lms.svelte | 127 +++ .../features/settings/pages/domains.svelte | 360 ++++++++ .../src/lib/features/settings/pages/index.ts | 8 + .../settings/pages/integrations.svelte | 72 ++ .../settings/pages/landingpage.svelte | 762 +++++++++++++++++ .../lib/features/settings/pages/org.svelte | 256 ++++++ .../features/settings/pages/profile.svelte | 168 ++++ .../lib/features/settings/pages/teams.svelte | 158 ++++ .../src/lib/features/settings/utils/store.ts | 116 +++ .../setup/api/setup-progress.svelte.ts | 176 ++++ .../setup/components/setup-progress.svelte | 53 ++ .../src/lib/features/ui/auth-ui.svelte | 124 +++ .../src/lib/features/ui/coming-soon.svelte | 2 +- apps/dashboard/src/lib/features/ui/index.ts | 7 +- .../ui/navigation/app-breadcrumbs.svelte | 2 +- .../features/ui/navigation/app-header.svelte | 35 +- .../features/ui/navigation/app-setup.svelte | 64 ++ .../lib/features/ui/navigation/breadcrumb.ts | 2 +- .../features/ui/navigation/org-navigation.ts | 33 +- .../ui/role-based-security.svelte} | 2 +- .../ui/sidebar/org-sidebar/nav-main.svelte | 2 +- .../ui/sidebar/org-sidebar/org-sidebar.svelte | 4 +- .../org-sidebar/upgrade-trigger.svelte | 27 +- .../src/lib/features/ui/upgrade-banner.svelte | 41 + .../src/lib/features/ui/upgrade-modal.svelte | 225 +++++ .../ui/upgrade-powered-by.svelte} | 2 +- .../src/lib/utils/constants/routes.ts | 8 +- apps/dashboard/src/lib/utils/functions/app.ts | 33 +- .../src/lib/utils/functions/domain.ts | 8 +- .../src/lib/utils/services/api/index.ts | 14 +- .../src/lib/utils/services/api/server.ts | 28 + .../src/lib/utils/services/courses/index.ts | 39 +- .../src/lib/utils/services/org/index.ts | 308 ------- .../src/lib/utils/services/upload.ts | 28 + apps/dashboard/src/lib/utils/store/org.ts | 13 +- .../src/lib/utils/translations/en.json | 10 +- apps/dashboard/src/lib/utils/types/org.ts | 42 - .../routes/(auth)/auth-failed/+page.svelte | 42 + .../src/routes/(auth)/forgot/+page.svelte | 70 ++ .../routes/{ => (auth)}/login/+page.svelte | 101 ++- .../routes/{ => (auth)}/logout/+page.svelte | 2 +- .../{ => (auth)}/onboarding/+page.svelte | 97 +-- .../src/routes/(auth)/reset/+page.svelte | 110 +++ .../routes/{ => (auth)}/signup/+page.svelte | 134 +-- apps/dashboard/src/routes/+layout.server.ts | 4 +- apps/dashboard/src/routes/+layout.svelte | 16 +- apps/dashboard/src/routes/+page.svelte | 6 +- .../src/routes/api/polar/webhook/+server.ts | 44 +- .../src/routes/courses/[id]/+page.svelte | 6 +- .../courses/[id]/certificates/+page.svelte | 2 +- .../routes/courses/[id]/lessons/+page.svelte | 2 +- .../lessons/[...lessonParams]/+page.svelte | 2 +- .../routes/courses/[id]/marks/+page.svelte | 8 +- .../routes/courses/[id]/people/+layout.svelte | 2 +- .../routes/courses/[id]/people/+page.svelte | 2 +- .../courses/[id]/submissions/+page.svelte | 2 +- apps/dashboard/src/routes/forgot/+page.svelte | 66 -- .../routes/invite/s/[hash]/+layout.server.ts | 10 +- .../src/routes/invite/s/[hash]/+page.svelte | 24 +- .../routes/invite/t/[hash]/+layout.server.ts | 11 +- .../src/routes/invite/t/[hash]/+page.svelte | 29 +- apps/dashboard/src/routes/lms/+page.svelte | 2 +- .../src/routes/lms/community/+page.svelte | 4 +- .../routes/lms/community/[slug]/+page.svelte | 21 +- .../src/routes/lms/community/ask/+page.svelte | 2 +- .../src/routes/lms/explore/+page.svelte | 6 +- .../src/routes/lms/mylearning/+page.svelte | 8 +- .../src/routes/org/[slug]/+layout.svelte | 2 +- .../src/routes/org/[slug]/+page.svelte | 279 +++--- .../routes/org/[slug]/audience/+page.svelte | 41 +- .../[slug]/audience/[...params]/+page.svelte | 214 +---- .../routes/org/[slug]/community/+page.svelte | 42 +- .../org/[slug]/community/[slug]/+page.svelte | 262 +++--- .../org/[slug]/community/ask/+page.svelte | 105 +-- .../routes/org/[slug]/courses/+page.svelte | 104 +-- .../routes/org/[slug]/settings/+layout.svelte | 12 + .../routes/org/[slug]/settings/+page.svelte | 45 +- .../org/[slug]/settings/billing/+page.svelte | 20 + .../settings/customize-lms/+page.svelte | 50 +- .../org/[slug]/settings/domains/+page.svelte | 26 +- .../[slug]/settings/integrations/+page.svelte | 20 + .../[slug]/settings/landingpage/+page.svelte | 38 + .../org/[slug]/settings/org/+page.svelte | 38 + .../org/[slug]/settings/teams/+page.svelte | 28 +- .../src/routes/org/[slug]/setup/+page.svelte | 188 ++-- .../src/routes/org/[slug]/setup/+page.ts | 157 +--- apps/dashboard/src/routes/reset/+page.svelte | 96 --- packages/db/README.md | 10 + packages/db/package.json | 1 + packages/db/src/auth.ts | 5 +- packages/db/src/auth/email-verification.ts | 9 +- packages/db/src/queries/course/course.ts | 80 +- .../src/queries/organization/organization.ts | 167 +++- packages/db/src/queries/organization/types.ts | 2 +- packages/db/src/schema.ts | 462 +++------- packages/db/src/scripts/db-setup.ts | 50 ++ packages/db/src/scripts/seed.ts | 6 - packages/email/src/emails/index.ts | 3 + packages/email/src/emails/invite-teacher.ts | 26 + packages/email/src/utils/constants.ts | 2 +- packages/storybook/HOW_TO_CREATE_STORIES.md | 458 ++++++++++ packages/storybook/README.md | 34 +- packages/storybook/package.json | 14 +- .../storybook/scripts/upload-storybook.ts | 198 +++++ .../storybook/src/atoms/accordion/fields.ts | 7 +- packages/storybook/src/atoms/avatar/fields.ts | 5 +- .../src/atoms/button/button.stories.svelte | 6 +- packages/storybook/src/atoms/button/fields.ts | 5 +- .../storybook/src/atoms/checkbox/fields.ts | 7 +- .../storybook/src/atoms/collapsible/fields.ts | 7 +- .../storybook/src/atoms/command/fields.ts | 7 +- packages/storybook/src/atoms/dialog/fields.ts | 5 +- packages/storybook/src/atoms/input/fields.ts | 7 +- .../src/atoms/item/item.stories.svelte | 416 +++++++++ packages/storybook/src/atoms/label/fields.ts | 5 +- .../storybook/src/atoms/menubar/fields.ts | 7 +- .../storybook/src/atoms/pagination/fields.ts | 7 +- .../storybook/src/atoms/progress/fields.ts | 5 +- .../storybook/src/atoms/radio-group/fields.ts | 7 +- packages/storybook/src/atoms/select/fields.ts | 7 +- .../storybook/src/atoms/separator/fields.ts | 5 +- packages/storybook/src/atoms/sheet/fields.ts | 5 +- .../src/atoms/sheet/sheet.stories.svelte | 2 +- .../storybook/src/atoms/skeleton/fields.ts | 5 +- packages/storybook/src/atoms/sonner/fields.ts | 5 +- .../src/atoms/spinner/spinner.stories.svelte | 161 +++- packages/storybook/src/atoms/switch/fields.ts | 7 +- packages/storybook/src/atoms/tabs/fields.ts | 7 +- .../storybook/src/atoms/tooltip/fields.ts | 7 +- packages/storybook/src/index.css | 2 + .../button-group/button-group.stories.svelte | 400 +++++++++ .../src/molecules/button-group/fields.ts | 1 + .../src/molecules/card/card.stories.svelte | 123 +++ .../storybook/src/molecules/card/fields.ts | 1 + .../copy-button/copy-button.stories.svelte | 102 +++ .../src/molecules/copy-button/fields.ts | 11 + .../src/molecules/empty/empty.stories.svelte | 146 ++++ .../storybook/src/molecules/empty/fields.ts | 1 + .../src/molecules/field/field.stories.svelte | 508 +++++++++++ .../storybook/src/molecules/field/fields.ts | 1 + .../src/molecules/hover-card/fields.ts | 2 + .../hover-card/hover-card.stories.svelte | 121 +++ .../src/molecules/input-group/fields.ts | 2 + .../input-group/input-group.stories.svelte | 411 +++++++++ .../storybook/src/molecules/page/fields.ts | 2 + .../src/molecules/page/page.stories.svelte | 387 +++++++++ .../password/password.stories.svelte | 97 +++ packages/storybook/tsconfig.json | 9 +- packages/ui/jsrepo.json | 16 + packages/ui/package.json | 16 +- .../button-group-separator.svelte | 20 + .../button-group/button-group-text.svelte | 30 + .../src/base/button-group/button-group.svelte | 46 + packages/ui/src/base/button-group/index.ts | 13 + packages/ui/src/base/button/button.svelte | 136 +-- packages/ui/src/base/button/index.ts | 36 +- packages/ui/src/base/card/card-action.svelte | 20 + packages/ui/src/base/card/card-content.svelte | 15 + .../ui/src/base/card/card-description.svelte | 20 + packages/ui/src/base/card/card-footer.svelte | 20 + packages/ui/src/base/card/card-header.svelte | 23 + packages/ui/src/base/card/card-title.svelte | 15 + packages/ui/src/base/card/card.svelte | 23 + packages/ui/src/base/card/index.ts | 25 + .../src/base/copy-button/copy-button.svelte | 74 ++ packages/ui/src/base/copy-button/index.ts | 7 + packages/ui/src/base/copy-button/types.ts | 22 + .../ui/src/base/dark-mode/dark-mode.svelte | 4 +- .../ui/src/base/dialog/dialog-content.svelte | 2 +- .../ui/src/base/dialog/dialog-header.svelte | 2 +- .../ui/src/base/empty/empty-content.svelte | 23 + .../src/base/empty/empty-description.svelte | 23 + .../ui/src/base/empty/empty-header.svelte | 20 + packages/ui/src/base/empty/empty-media.svelte | 41 + packages/ui/src/base/empty/empty-title.svelte | 20 + packages/ui/src/base/empty/empty.svelte | 23 + packages/ui/src/base/empty/index.ts | 22 + .../ui/src/base/field/field-content.svelte | 20 + .../src/base/field/field-description.svelte | 25 + packages/ui/src/base/field/field-error.svelte | 58 ++ packages/ui/src/base/field/field-group.svelte | 23 + packages/ui/src/base/field/field-label.svelte | 21 + .../ui/src/base/field/field-legend.svelte | 29 + .../ui/src/base/field/field-separator.svelte | 35 + packages/ui/src/base/field/field-set.svelte | 24 + packages/ui/src/base/field/field-title.svelte | 23 + packages/ui/src/base/field/field.svelte | 53 ++ packages/ui/src/base/field/index.ts | 33 + .../base/hover-card/hover-card-content.svelte | 29 + .../base/hover-card/hover-card-trigger.svelte | 13 + packages/ui/src/base/hover-card/index.ts | 14 + .../input-group/input-group-button.svelte | 2 +- .../base/input-group/input-group-input.svelte | 2 +- .../input-group/input-group-textarea.svelte | 2 +- packages/ui/src/base/input/input.svelte | 6 +- packages/ui/src/base/item/item.svelte | 2 +- packages/ui/src/base/page/index.ts | 28 + packages/ui/src/base/page/page-action.svelte | 21 + .../ui/src/base/page/page-body-header.svelte | 33 + packages/ui/src/base/page/page-body.svelte | 25 + .../src/base/page/page-header-content.svelte | 20 + packages/ui/src/base/page/page-header.svelte | 17 + .../ui/src/base/page/page-subtitle.svelte | 21 + packages/ui/src/base/page/page-title.svelte | 20 + packages/ui/src/base/page/page.svelte | 20 + .../base/pagination/pagination-link.svelte | 2 +- packages/ui/src/base/password/index.ts | 7 + .../ui/src/base/password/password-copy.svelte | 21 + .../src/base/password/password-input.svelte | 45 + .../base/password/password-strength.svelte | 48 ++ .../password-toggle-visibility.svelte | 32 + .../src/base/password/password-util.svelte.ts | 157 ++++ packages/ui/src/base/password/password.svelte | 26 + packages/ui/src/base/password/types.ts | 47 + packages/ui/src/base/select/index.ts | 15 +- .../ui/src/base/sidebar/sidebar-input.svelte | 2 +- .../base/sidebar/sidebar-menu-button.svelte | 2 +- .../base/sidebar/sidebar-menu-skeleton.svelte | 2 +- .../src/base/sidebar/sidebar-provider.svelte | 2 +- .../src/base/sidebar/sidebar-separator.svelte | 2 +- .../src/base/sidebar/sidebar-trigger.svelte | 2 +- packages/ui/src/base/sidebar/sidebar.svelte | 2 +- packages/ui/src/base/sonner/sonner.svelte | 7 +- packages/ui/src/base/toggle/index.ts | 13 + packages/ui/src/base/toggle/toggle.svelte | 52 ++ .../custom/domain-input/domain-input.svelte | 34 + packages/ui/src/custom/domain-input/index.ts | 5 + packages/ui/src/custom/editor/editor.svelte | 31 +- packages/ui/src/custom/editor/index.ts | 2 +- packages/ui/src/custom/empty/.gitkeep | 0 packages/ui/src/custom/empty/empty.svelte | 67 ++ packages/ui/src/custom/empty/index.ts | 4 + packages/ui/src/custom/password/index.ts | 5 + .../ui/src/custom/password/password.svelte | 80 ++ packages/ui/src/custom/search/index.ts | 5 + packages/ui/src/custom/search/search.svelte | 32 + packages/ui/src/hooks/is-mobile.svelte.ts | 2 + packages/ui/src/hooks/use-clipboard.svelte.ts | 83 ++ packages/ui/src/index.css | 8 +- packages/ui/src/index.ts | 11 + packages/ui/src/output.css | 2 +- packages/utils/src/validation/constants.ts | 2 + packages/utils/src/validation/index.ts | 1 + packages/utils/src/validation/media/index.ts | 1 + packages/utils/src/validation/media/media.ts | 12 + .../validation/organization/organization.ts | 67 ++ pnpm-lock.yaml | 132 ++- 350 files changed, 13928 insertions(+), 3895 deletions(-) create mode 100644 .github/workflows/deploy-storybook.yml create mode 100644 apps/api/src/middlewares/api-key.ts create mode 100644 apps/api/src/middlewares/auth-or-api-key.ts create mode 100644 apps/api/src/middlewares/org-admin.ts create mode 100644 apps/api/src/routes/media/index.ts create mode 100644 apps/api/src/routes/media/media.ts create mode 100644 apps/api/src/services/media.ts create mode 100644 apps/dashboard/ROUTE_PAGE_SETUP.md delete mode 100644 apps/dashboard/src/lib/components/AuthUI/index.svelte delete mode 100644 apps/dashboard/src/lib/components/Org/Audience/AudienceList.svelte delete mode 100644 apps/dashboard/src/lib/components/Org/Audience/index.svelte delete mode 100644 apps/dashboard/src/lib/components/Org/Community/Loader.svelte delete mode 100644 apps/dashboard/src/lib/components/Org/Settings/TeamMembers.svelte delete mode 100644 apps/dashboard/src/lib/components/Upgrade/Banner.svelte delete mode 100644 apps/dashboard/src/lib/components/Upgrade/Modal.svelte create mode 100644 apps/dashboard/src/lib/features/audience/components/index.ts create mode 100644 apps/dashboard/src/lib/features/audience/pages/audience.svelte create mode 100644 apps/dashboard/src/lib/features/audience/pages/index.ts create mode 100644 apps/dashboard/src/lib/features/audience/pages/user-analytics.svelte create mode 100644 apps/dashboard/src/lib/features/community/components/ask-community-button.svelte create mode 100644 apps/dashboard/src/lib/features/community/components/community-list-loader.svelte rename apps/dashboard/src/lib/{components/Org/Community/DeleteModal.svelte => features/community/components/delete-modal.svelte} (100%) create mode 100644 apps/dashboard/src/lib/features/community/components/index.ts rename apps/dashboard/src/lib/{components/Org/Community/index.svelte => features/community/pages/community.svelte} (69%) create mode 100644 apps/dashboard/src/lib/features/community/pages/index.ts rename apps/dashboard/src/lib/{components/Courses/components/Card/Loader.svelte => features/course/components/card-loader.svelte} (84%) rename apps/dashboard/src/lib/{components/Courses/components/Card/index.svelte => features/course/components/card.svelte} (98%) rename apps/dashboard/src/lib/{components/Courses/components/CopyCourseModal/index.svelte => features/course/components/copy-course-modal.svelte} (98%) create mode 100644 apps/dashboard/src/lib/features/course/components/create-course-button.svelte create mode 100644 apps/dashboard/src/lib/features/course/components/index.ts rename apps/dashboard/src/lib/{components/Courses/components/List/Loader.svelte => features/course/components/list-loader.svelte} (100%) rename apps/dashboard/src/lib/{components/Courses/components/List/index.svelte => features/course/components/list.svelte} (96%) rename apps/dashboard/src/lib/{components/Courses/components/NewCourseModal/index.svelte => features/course/components/new-course-modal.svelte} (98%) rename apps/dashboard/src/lib/{components/Courses/index.svelte => features/course/pages/courses.svelte} (61%) create mode 100644 apps/dashboard/src/lib/features/course/pages/index.ts rename apps/dashboard/src/lib/{components/Courses => features/course/utils}/constants.ts (100%) rename apps/dashboard/src/lib/{components/Courses => features/course/utils}/functions.ts (100%) rename apps/dashboard/src/lib/{components/Courses => features/course/utils}/store.ts (100%) create mode 100644 apps/dashboard/src/lib/features/org/api/org-plan.server.ts create mode 100644 apps/dashboard/src/lib/features/org/api/org.server.ts create mode 100644 apps/dashboard/src/lib/features/org/api/types.ts create mode 100644 apps/dashboard/src/lib/features/settings/components/Layout/Column.svelte create mode 100644 apps/dashboard/src/lib/features/settings/components/Layout/Grid.svelte create mode 100644 apps/dashboard/src/lib/features/settings/components/Layout/Row.svelte create mode 100644 apps/dashboard/src/lib/features/settings/components/Layout/index.ts create mode 100644 apps/dashboard/src/lib/features/settings/components/index.ts create mode 100644 apps/dashboard/src/lib/features/settings/components/language-picker.svelte create mode 100644 apps/dashboard/src/lib/features/settings/pages/billing.svelte create mode 100644 apps/dashboard/src/lib/features/settings/pages/customize-lms.svelte create mode 100644 apps/dashboard/src/lib/features/settings/pages/domains.svelte create mode 100644 apps/dashboard/src/lib/features/settings/pages/index.ts create mode 100644 apps/dashboard/src/lib/features/settings/pages/integrations.svelte create mode 100644 apps/dashboard/src/lib/features/settings/pages/landingpage.svelte create mode 100644 apps/dashboard/src/lib/features/settings/pages/org.svelte create mode 100644 apps/dashboard/src/lib/features/settings/pages/profile.svelte create mode 100644 apps/dashboard/src/lib/features/settings/pages/teams.svelte create mode 100644 apps/dashboard/src/lib/features/settings/utils/store.ts create mode 100644 apps/dashboard/src/lib/features/setup/api/setup-progress.svelte.ts create mode 100644 apps/dashboard/src/lib/features/setup/components/setup-progress.svelte create mode 100644 apps/dashboard/src/lib/features/ui/auth-ui.svelte create mode 100644 apps/dashboard/src/lib/features/ui/navigation/app-setup.svelte rename apps/dashboard/src/lib/{components/RoleBasedSecurity/index.svelte => features/ui/role-based-security.svelte} (94%) create mode 100644 apps/dashboard/src/lib/features/ui/upgrade-banner.svelte create mode 100644 apps/dashboard/src/lib/features/ui/upgrade-modal.svelte rename apps/dashboard/src/lib/{components/Upgrade/PoweredBy.svelte => features/ui/upgrade-powered-by.svelte} (92%) create mode 100644 apps/dashboard/src/lib/utils/services/api/server.ts delete mode 100644 apps/dashboard/src/lib/utils/services/org/index.ts create mode 100644 apps/dashboard/src/lib/utils/services/upload.ts create mode 100644 apps/dashboard/src/routes/(auth)/auth-failed/+page.svelte create mode 100644 apps/dashboard/src/routes/(auth)/forgot/+page.svelte rename apps/dashboard/src/routes/{ => (auth)}/login/+page.svelte (50%) rename apps/dashboard/src/routes/{ => (auth)}/logout/+page.svelte (70%) rename apps/dashboard/src/routes/{ => (auth)}/onboarding/+page.svelte (72%) create mode 100644 apps/dashboard/src/routes/(auth)/reset/+page.svelte rename apps/dashboard/src/routes/{ => (auth)}/signup/+page.svelte (52%) delete mode 100644 apps/dashboard/src/routes/forgot/+page.svelte create mode 100644 apps/dashboard/src/routes/org/[slug]/settings/+layout.svelte create mode 100644 apps/dashboard/src/routes/org/[slug]/settings/billing/+page.svelte create mode 100644 apps/dashboard/src/routes/org/[slug]/settings/integrations/+page.svelte create mode 100644 apps/dashboard/src/routes/org/[slug]/settings/landingpage/+page.svelte create mode 100644 apps/dashboard/src/routes/org/[slug]/settings/org/+page.svelte delete mode 100644 apps/dashboard/src/routes/reset/+page.svelte create mode 100644 packages/db/src/scripts/db-setup.ts create mode 100644 packages/email/src/emails/invite-teacher.ts create mode 100644 packages/storybook/HOW_TO_CREATE_STORIES.md create mode 100644 packages/storybook/scripts/upload-storybook.ts create mode 100644 packages/storybook/src/atoms/item/item.stories.svelte create mode 100644 packages/storybook/src/molecules/button-group/button-group.stories.svelte create mode 100644 packages/storybook/src/molecules/button-group/fields.ts create mode 100644 packages/storybook/src/molecules/card/card.stories.svelte create mode 100644 packages/storybook/src/molecules/card/fields.ts create mode 100644 packages/storybook/src/molecules/copy-button/copy-button.stories.svelte create mode 100644 packages/storybook/src/molecules/copy-button/fields.ts create mode 100644 packages/storybook/src/molecules/empty/empty.stories.svelte create mode 100644 packages/storybook/src/molecules/empty/fields.ts create mode 100644 packages/storybook/src/molecules/field/field.stories.svelte create mode 100644 packages/storybook/src/molecules/field/fields.ts create mode 100644 packages/storybook/src/molecules/hover-card/fields.ts create mode 100644 packages/storybook/src/molecules/hover-card/hover-card.stories.svelte create mode 100644 packages/storybook/src/molecules/input-group/fields.ts create mode 100644 packages/storybook/src/molecules/input-group/input-group.stories.svelte create mode 100644 packages/storybook/src/molecules/page/fields.ts create mode 100644 packages/storybook/src/molecules/page/page.stories.svelte create mode 100644 packages/storybook/src/molecules/password/password.stories.svelte create mode 100644 packages/ui/jsrepo.json create mode 100644 packages/ui/src/base/button-group/button-group-separator.svelte create mode 100644 packages/ui/src/base/button-group/button-group-text.svelte create mode 100644 packages/ui/src/base/button-group/button-group.svelte create mode 100644 packages/ui/src/base/button-group/index.ts create mode 100644 packages/ui/src/base/card/card-action.svelte create mode 100644 packages/ui/src/base/card/card-content.svelte create mode 100644 packages/ui/src/base/card/card-description.svelte create mode 100644 packages/ui/src/base/card/card-footer.svelte create mode 100644 packages/ui/src/base/card/card-header.svelte create mode 100644 packages/ui/src/base/card/card-title.svelte create mode 100644 packages/ui/src/base/card/card.svelte create mode 100644 packages/ui/src/base/card/index.ts create mode 100644 packages/ui/src/base/copy-button/copy-button.svelte create mode 100644 packages/ui/src/base/copy-button/index.ts create mode 100644 packages/ui/src/base/copy-button/types.ts create mode 100644 packages/ui/src/base/empty/empty-content.svelte create mode 100644 packages/ui/src/base/empty/empty-description.svelte create mode 100644 packages/ui/src/base/empty/empty-header.svelte create mode 100644 packages/ui/src/base/empty/empty-media.svelte create mode 100644 packages/ui/src/base/empty/empty-title.svelte create mode 100644 packages/ui/src/base/empty/empty.svelte create mode 100644 packages/ui/src/base/empty/index.ts create mode 100644 packages/ui/src/base/field/field-content.svelte create mode 100644 packages/ui/src/base/field/field-description.svelte create mode 100644 packages/ui/src/base/field/field-error.svelte create mode 100644 packages/ui/src/base/field/field-group.svelte create mode 100644 packages/ui/src/base/field/field-label.svelte create mode 100644 packages/ui/src/base/field/field-legend.svelte create mode 100644 packages/ui/src/base/field/field-separator.svelte create mode 100644 packages/ui/src/base/field/field-set.svelte create mode 100644 packages/ui/src/base/field/field-title.svelte create mode 100644 packages/ui/src/base/field/field.svelte create mode 100644 packages/ui/src/base/field/index.ts create mode 100644 packages/ui/src/base/hover-card/hover-card-content.svelte create mode 100644 packages/ui/src/base/hover-card/hover-card-trigger.svelte create mode 100644 packages/ui/src/base/hover-card/index.ts create mode 100644 packages/ui/src/base/page/index.ts create mode 100644 packages/ui/src/base/page/page-action.svelte create mode 100644 packages/ui/src/base/page/page-body-header.svelte create mode 100644 packages/ui/src/base/page/page-body.svelte create mode 100644 packages/ui/src/base/page/page-header-content.svelte create mode 100644 packages/ui/src/base/page/page-header.svelte create mode 100644 packages/ui/src/base/page/page-subtitle.svelte create mode 100644 packages/ui/src/base/page/page-title.svelte create mode 100644 packages/ui/src/base/page/page.svelte create mode 100644 packages/ui/src/base/password/index.ts create mode 100644 packages/ui/src/base/password/password-copy.svelte create mode 100644 packages/ui/src/base/password/password-input.svelte create mode 100644 packages/ui/src/base/password/password-strength.svelte create mode 100644 packages/ui/src/base/password/password-toggle-visibility.svelte create mode 100644 packages/ui/src/base/password/password-util.svelte.ts create mode 100644 packages/ui/src/base/password/password.svelte create mode 100644 packages/ui/src/base/password/types.ts create mode 100644 packages/ui/src/base/toggle/index.ts create mode 100644 packages/ui/src/base/toggle/toggle.svelte create mode 100644 packages/ui/src/custom/domain-input/domain-input.svelte create mode 100644 packages/ui/src/custom/domain-input/index.ts delete mode 100644 packages/ui/src/custom/empty/.gitkeep create mode 100644 packages/ui/src/custom/empty/empty.svelte create mode 100644 packages/ui/src/custom/empty/index.ts create mode 100644 packages/ui/src/custom/password/index.ts create mode 100644 packages/ui/src/custom/password/password.svelte create mode 100644 packages/ui/src/custom/search/index.ts create mode 100644 packages/ui/src/custom/search/search.svelte create mode 100644 packages/ui/src/hooks/use-clipboard.svelte.ts create mode 100644 packages/utils/src/validation/media/index.ts create mode 100644 packages/utils/src/validation/media/media.ts diff --git a/.github/workflows/deploy-storybook.yml b/.github/workflows/deploy-storybook.yml new file mode 100644 index 00000000..700dc420 --- /dev/null +++ b/.github/workflows/deploy-storybook.yml @@ -0,0 +1,64 @@ +name: Deploy Storybook to R2 + +on: + push: + branches: [storybook-deploy] + paths: + - "packages/storybook/**" + - "packages/ui/**" + - "packages/storybook/package.json" + workflow_dispatch: + +env: + NODE_VERSION: "20" + PNPM_VERSION: "8" + +jobs: + build-and-deploy-storybook: + runs-on: ubuntu-latest + environment: production + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Setup pnpm + uses: pnpm/action-setup@v2 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v3 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm i + + - name: Build Storybook + run: | + cd packages/storybook + pnpm run build + + - name: Upload Storybook to R2 + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + run: | + cd packages/storybook + pnpm run upload:storybook + env: + CLOUDFLARE_ACCESS_KEY: ${{ secrets.CLOUDFLARE_ACCESS_KEY }} + CLOUDFLARE_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_SECRET_ACCESS_KEY }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.gitignore b/.gitignore index 0e4f23c4..22bcfeb9 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,8 @@ vite.config.ts*.mjs #Ignore vscode AI rules .github/instructions/codacy.instructions.md -tsconfig.tsbuildinfo* \ No newline at end of file +tsconfig.tsbuildinfo* + +storybook-static + +.svelte-kit/ \ No newline at end of file diff --git a/ARCHITECHTURE.md b/ARCHITECHTURE.md index 2f71a868..32035d8d 100644 --- a/ARCHITECHTURE.md +++ b/ARCHITECHTURE.md @@ -507,6 +507,22 @@ packages/utils/src/validation/ └── README.md ← Documentation ``` +**File Naming Convention**: + +All file names should use **lowercase with hyphens** (kebab-case): + +- ✅ `setup-progress.svelte` (component files) +- ✅ `org.svelte.ts` (API service files) +- ✅ `course.ts` (validation/query files) +- ✅ `user-profile.svelte` (multi-word component files) + +- ❌ `SetupProgress.svelte` (PascalCase - reserved for component names in imports) +- ❌ `Org.svelte.ts` (PascalCase) +- ❌ `userProfile.svelte` (camelCase) +- ❌ `user_profile.svelte` (snake_case) + +**Note**: Component names in imports can still use PascalCase (e.g., `import SetupProgress from './setup-progress.svelte'`), but the actual file names must be lowercase with hyphens. + **Example: Course Validations.** Schemas should be **clean** - only validation rules, no error messages: @@ -768,14 +784,30 @@ if (!result.success) { ```typescript import { authMiddleware } from '@api/middlewares/auth'; +import { apiKeyMiddleware } from '@api/middlewares/api-key'; +import { authOrApiKeyMiddleware } from '@api/middlewares/auth-or-api-key'; -// Protected route - requires authentication +// Protected route - requires user authentication (session cookies) .post('/create', authMiddleware, async (c) => { const user = c.get('user'); // Always available after authMiddleware const session = c.get('session'); // ... }); +// Protected route - requires API key (server-to-server) +.post('/webhook', apiKeyMiddleware, async (c) => { + // API key validated, proceed + // ... +}); + +// Protected route - accepts either user session OR API key +.post('/plan', authOrApiKeyMiddleware, async (c) => { + // Can be called by: + // 1. Authenticated users (via session cookies) + // 2. Server-to-server (via API key: Authorization: Bearer ) + // ... +}); + // Public endpoint - no authentication required .get('/public-courses', async (c) => { // Publicly accessible data @@ -792,6 +824,12 @@ import { authMiddleware } from '@api/middlewares/auth'; }); ``` +**API Key Authentication:** +- Add `API_KEY` to `apps/api/src/config/env.ts` environment schema +- Use `Authorization: Bearer ` header format (standard across popular APIs) +- Use `apiKeyMiddleware` for API-key-only endpoints +- Use `authOrApiKeyMiddleware` for endpoints that accept both user sessions and API keys + ### Complete Route Example Routes should be thin - they handle HTTP concerns and delegate to services: @@ -1004,6 +1042,268 @@ export class ProfileApi { --- +### Example: Creating an Organization (POST Route) + +We'll create a `POST /organization` route that creates a new organization with the current user as owner. + +--- + +### Step 1: Create Validation Schema + +**Location**: `packages/utils/src/validation/organization/organization.ts` + +```typescript +import * as z from 'zod'; + +export const ZCreateOrganization = z.object({ + name: z + .string() + .min(5) + .refine((val) => !/^[-]|[-]$/.test(val), { + message: 'Organization name cannot start or end with a hyphen' + }), + siteName: z + .string() + .min(5) + .refine((val) => !/^[-]|[-]$/.test(val), { + message: 'Site name cannot start or end with a hyphen' + }) +}); + +export type TCreateOrganization = z.infer; +``` + +**Export**: Add to `packages/utils/src/validation/organization/index.ts` and `packages/utils/src/validation/index.ts` + +--- + +### Step 2: Create or Reuse Service Function + +**Location**: `apps/api/src/services/organization.ts` + +**Option A: Reuse Existing Service** (if function exists in another service) + +```typescript +import { createOrganizationWithOwner } from '@api/services/onboarding'; + +export async function createOrg(profileId: string, data: { name: string; siteName: string }) { + // Reuse existing function from onboarding service + return await createOrganizationWithOwner(profileId, { + orgName: data.name, + siteName: data.siteName + }); +} +``` + +**Option B: Create New Service** (if you need different logic) + +```typescript +import { AppError, ErrorCodes } from '@api/utils/errors'; +import { db } from '@cio/db/drizzle'; +import { + checkSiteNameExists, + createOrganization, + createOrganizationMember, + getOrganizationByProfileId +} from '@cio/db/queries/organization'; +import { ROLE } from '@cio/utils/constants'; + +export async function createOrg(profileId: string, data: { name: string; siteName: string }) { + // Business Logic: Check sitename availability + const exists = await checkSiteNameExists(data.siteName); + if (exists) { + throw new AppError( + `Site name '${data.siteName}' already exists`, + ErrorCodes.SITENAME_EXISTS, + 409, + 'siteName' + ); + } + + // Business Logic: Create org and member in a transaction + try { + const result = await db.transaction(async (tx) => { + const organization = await createOrganization({ + name: data.name, + siteName: data.siteName + }); + + const member = await createOrganizationMember({ + organizationId: organization.id, + profileId, + roleId: ROLE.ADMIN, + verified: true + }); + + return { organization, member }; + }); + + // Fetch updated organizations list + const organizations = await getOrganizationByProfileId(profileId); + + return { + organization: result.organization, + member: result.member, + organizations + }; + } catch (error) { + // Handle database constraint violations + if (error.code === '23505') { + throw new AppError( + `Site name '${data.siteName}' already exists`, + ErrorCodes.SITENAME_EXISTS, + 409, + 'siteName' + ); + } + throw new AppError('Failed to create organization', ErrorCodes.ORG_CREATE_FAILED, 500); + } +} +``` + +--- + +### Step 3: Create Route + +**Location**: `apps/api/src/routes/organization/organization.ts` + +```typescript +import { createOrg } from '@api/services/organization'; +import { Hono } from '@api/utils/hono'; +import { ZCreateOrganization } from '@cio/utils/validation/organization'; +import { authMiddleware } from '@api/middlewares/auth'; +import { handleError } from '@api/utils/errors'; +import { zValidator } from '@hono/zod-validator'; + +export const organizationRouter = new Hono() + .post('/', authMiddleware, zValidator('json', ZCreateOrganization), async (c) => { + try { + const user = c.get('user'); + const data = c.req.valid('json'); + + const result = await createOrg(user.id, data); + + return c.json({ success: true, data: result }, 201); + } catch (error) { + return handleError(c, error, 'Failed to create organization'); + } + }); +``` + +**Note**: Use `201 Created` status code for successful POST requests that create resources. + +--- + +### Step 4: Export and Register Route + +```typescript +// apps/api/src/routes/organization/index.ts +export * from './organization'; + +// apps/api/src/app.ts +import { organizationRouter } from '@api/routes/organization'; + +export const app = new Hono() + .route('/organization', organizationRouter); // Route available at POST /organization +``` + +--- + +### Step 5: Use in Frontend + +**Location**: `apps/dashboard/src/lib/features/org/api/org.svelte.ts` + +```typescript +import { BaseApiWithErrors, classroomio } from '$lib/utils/services/api'; +import { ZCreateOrganization } from '@cio/utils/validation/organization'; +import { mapZodErrorsToTranslations } from '$lib/utils/validation'; +import { orgs, currentOrg } from '$lib/utils/store/org'; +import { snackbar } from '$lib/components/Snackbar/store'; + +class OrgApi extends BaseApiWithErrors { + async create(fields: { name: string; siteName: string }) { + // Client-side validation + const result = ZCreateOrganization.safeParse(fields); + if (!result.success) { + this.errors = mapZodErrorsToTranslations(result.error, 'organization'); + return; + } + + await this.execute({ + requestFn: () => + classroomio.organization.$post({ + json: result.data + }), + logContext: 'creating organization', + onSuccess: (response) => { + if (!response.data) return; + + const { organization, organizations } = response.data; + + // Update stores + orgs.set(organizations); + currentOrg.set(organization); + + snackbar.success('Organization created successfully'); + this.success = true; + this.errors = {}; + }, + onError: (result) => { + if (typeof result === 'string') { + snackbar.error(result); + return; + } + if ('error' in result && 'field' in result) { + // Field-specific error automatically mapped to this.errors[field] + this.errors[result.field as string] = result.error; + } + } + }); + } +} + +export const orgApi = new OrgApi(); +``` + +**Key Points:** +- Use `this.execute()` from `BaseApiWithErrors` for automatic loading/error state +- Field-specific errors are automatically mapped to `this.errors[field]` +- Update stores in `onSuccess` callback +- Handle both validation and API errors + +--- + +### Reusing Existing Service Functions + +Sometimes a service function exists in a different context (e.g., `createOrganizationWithOwner` in onboarding service). + +**When to Reuse:** +- ✅ The function does exactly what you need +- ✅ It's in the same domain (organization operations) +- ✅ You can call it from your new route without modification + +**When to Create New:** +- ❌ The existing function has domain-specific logic you don't want (e.g., onboarding-specific) +- ❌ You need different parameters or return values +- ❌ The function is in a different domain and mixing concerns would be inappropriate + +**Example: Reusing a Service** + +```typescript +// apps/api/src/services/organization.ts +import { createOrganizationWithOwner } from '@api/services/onboarding'; + +export async function createOrg(profileId: string, data: { name: string; siteName: string }) { + // Reuse existing function, just adapt the parameters + return await createOrganizationWithOwner(profileId, { + orgName: data.name, + siteName: data.siteName + }); +} +``` + +--- + ### Route Path Convention When adding routes to an existing router: @@ -1066,6 +1366,260 @@ When creating a new route, follow this checklist: --- +## Migrating Existing Supabase Calls to API Routes + +When you have existing components using direct Supabase calls, follow this migration pattern to move them to the API architecture. + +### Migration Workflow + +#### Step 1: Identify the Operation + +Analyze the existing Supabase call: +- **What database operation?** (CREATE, UPDATE, DELETE, SELECT) +- **What tables are involved?** +- **Are there related operations?** (e.g., create org + create member should be atomic) +- **What validation is currently done?** (client-side only or also server-side?) + +**Example Analysis:** +```typescript +// Current code +const { data: org, error } = await supabase + .from('organization') + .insert({ name: orgName, siteName: siteName }) + .select(); + +const { data, error } = await supabase + .from('organizationmember') + .insert({ organization_id: org[0].id, profile_id: $profile.id, role_id: 1 }); + +// Analysis: +// - Operation: CREATE (organization + member) +// - Tables: organization, organizationmember +// - Related: Yes, should be atomic (transaction) +// - Validation: Client-side only (needs server-side too) +``` + +#### Step 2: Check for Existing Services + +Search for existing service functions that might handle this: +- Check `apps/api/src/services/` for similar operations +- Look in related domains (e.g., onboarding might have org creation) +- Check if the operation exists but in a different context + +**Decision:** +- **Reuse existing service** if it does what you need +- **Create new service** if you need different logic or parameters + +#### Step 3: Create/Update API Route + +If the route doesn't exist: +- Follow the "Creating a New Route" workflow above +- Use appropriate HTTP method: `POST` (create), `PUT` (update), `DELETE` (delete) + +If the route exists: +- Add new endpoint to existing router +- Follow route path conventions + +#### Step 4: Create Frontend API Method + +Add method to existing API class (e.g., `orgApi.create()`): + +```typescript +// apps/dashboard/src/lib/features/org/api/org.svelte.ts +class OrgApi extends BaseApiWithErrors { + async create(fields: { name: string; siteName: string }) { + // Client-side validation + const result = ZCreateOrganization.safeParse(fields); + if (!result.success) { + this.errors = mapZodErrorsToTranslations(result.error, 'organization'); + return; + } + + await this.execute({ + requestFn: () => + classroomio.organization.$post({ + json: result.data + }), + logContext: 'creating organization', + onSuccess: (response) => { + // Update stores, navigate, show success + if (response.data) { + orgs.set(response.data.organizations); + currentOrg.set(response.data.organization); + } + snackbar.success('Organization created'); + this.success = true; + }, + onError: (result) => { + // Error handling (automatic for validation errors) + if (typeof result === 'string') { + snackbar.error(result); + } + } + }); + } +} +``` + +**Key Points:** +- Always use `this.execute()` from `BaseApiWithErrors` +- Client-side validation before API call +- Update stores in `onSuccess` callback +- Errors automatically handled by `BaseApiWithErrors` + +#### Step 5: Update Component + +Replace Supabase calls with API class method: + +**Before (Direct Supabase):** +```typescript + +``` + +**After (API Route):** +```typescript + +``` + +**Key Differences:** +- ✅ **No manual transaction cleanup** - Handled by service layer +- ✅ **No separate member creation** - Handled atomically by service +- ✅ **Automatic store updates** - Handled by API class `onSuccess` +- ✅ **Consistent error structure** - Field-specific errors automatically mapped +- ✅ **Type-safe** - Full TypeScript support end-to-end +- ✅ **Server-side validation** - Always validated even if client validation passes + +### Mapping API Errors to Form Fields + +The `BaseApiWithErrors` class automatically maps field-specific errors from the API response: + +**API Response Structure:** +```typescript +// Field-specific error +{ + success: false, + error: "Sitename already exists", + code: "SITENAME_EXISTS", + field: "siteName" // ← This maps to this.errors.siteName +} + +// General error +{ + success: false, + error: "Failed to create organization", + code: "ORG_CREATE_FAILED" + // No field property → maps to this.errors.general +} +``` + +**In Component:** +```typescript +await orgApi.create({ name: orgName, siteName }); + +if (!orgApi.success) { + // Field-specific error automatically in orgApi.errors.siteName + if (orgApi.errors.siteName) { + errors.siteName = orgApi.errors.siteName; + } + + // General error automatically in orgApi.errors.general + if (orgApi.errors.general) { + errors.general = orgApi.errors.general; + } +} +``` + +**How It Works:** +1. API returns error with `field` property +2. `BaseApiWithErrors.execute()` automatically sets `this.errors[field] = error` +3. Component can access `orgApi.errors.fieldName` directly +4. No manual error mapping needed + +### Migration Checklist + +When migrating a component from Supabase to API: + +- [ ] **Identify operations**: List all Supabase calls in component +- [ ] **Check existing routes**: Search `apps/api/src/routes/` for existing API routes +- [ ] **Check existing services**: Search `apps/api/src/services/` for reusable functions +- [ ] **Create missing pieces**: Follow "Creating a New Route" if route doesn't exist +- [ ] **Create API method**: Add method to appropriate API class using `execute()` pattern +- [ ] **Update component**: Replace Supabase calls with API method +- [ ] **Remove cleanup code**: Delete manual rollback/cleanup (handled by service transactions) +- [ ] **Remove separate calls**: Combine related operations (handled atomically by service) +- [ ] **Update error handling**: Use `orgApi.errors.fieldName` instead of Supabase error structure +- [ ] **Update store sync**: Remove manual store updates (handled by API class `onSuccess`) +- [ ] **Remove Supabase imports**: Delete unused `supabase` imports +- [ ] **Test**: Verify success and error cases work correctly +- [ ] **Test transactions**: Verify atomic operations (e.g., org + member creation) + +--- + ## Step 4: Export and Register Route ### Export Router @@ -1250,11 +1804,260 @@ Example with **client-side validation** before API call: ``` +### API Abstraction Pattern (API Classes) + +When creating API abstractions in `apps/dashboard/src/lib/features/{domain}/api/{entity}.svelte.ts`, **always use the `execute` method** from `BaseApi` or `BaseApiWithErrors` classes. This provides automatic loading state management, error handling, and consistent response processing. + +**Important**: +- **`.svelte.ts` files** are for **client-side use only** (Svelte components). They use Svelte reactivity (`$state`, `$derived`) and manage component state. **Never** include API key logic in these files. +- **`.server.ts` files** are for **server-side use only** (`+layout.server.ts`, `+page.server.ts`, `+server.ts`). They contain static methods with API key authentication and are automatically excluded from client bundles by SvelteKit. + +#### Using the `execute` Method + +**Pattern:** +```typescript +import { BaseApiWithErrors, classroomio } from '$lib/utils/services/api'; + +class MyApi extends BaseApiWithErrors { + async updateItem(id: string, data: Partial) { + await this.execute({ + requestFn: () => classroomio.items[':id'].$put({ + param: { id }, + json: data + }), + logContext: 'updating item', + onSuccess: (response) => { + // Update local stores with response.data + itemsStore.update((items) => { + const index = items.findIndex((i) => i.id === id); + if (index !== -1 && response.data) { + items[index] = { ...items[index], ...response.data }; + } + return items; + }); + }, + onError: (result) => { + // Optional: Custom error handling + if (typeof result === 'string') { + snackbar.error(result); + return; + } + if ('error' in result) { + this.handleValidationError(result); + } + } + }); + } +} + +export const myApi = new MyApi(); +``` + +**Key Points:** +- **Always use `this.execute`** - Never call the RPC client directly +- **Type the execute call** with `typeof classroomio.{route}.{method}` for type safety +- **Use `requestFn`** - Wrap the RPC call in a function +- **Use `logContext`** - Provide descriptive context for error logging +- **Use `onSuccess`** - Update local stores/reactivity with response data +- **Use `onError`** - Optional custom error handling (validation errors are handled automatically by `BaseApiWithErrors`) + +**Examples:** + +```typescript +// Profile API - Simple update +await this.execute({ + requestFn: () => classroomio.account.user.$put({ json: profileUpdates }), + logContext: 'updating profile', + onSuccess: (response) => { + profileStore.update((_profile) => ({ + ..._profile, + ...response.profile + })); + } +}); + +// Onboarding API - With error handling +await this.execute({ + requestFn: () => classroomio.onboarding.step1.$post({ json: data }), + logContext: 'submitting organization setup', + onSuccess: (result) => { + const { organizations } = result.data; + orgs.set(organizations); + currentOrg.set(organizations[0]); + this.errors = {}; + this.step = ONBOARDING_STEPS.USER_METADATA; + }, + onError: (result) => { + if (typeof result === 'string') { + snackbar.error(result); + return; + } + if ('error' in result) { + this.handleValidationError(result); + } + } +}); +``` + +**Benefits:** +- ✅ Automatic loading state management (`this.isLoading`) +- ✅ Automatic error state management (`this.error` or `this.errors`) +- ✅ Consistent error handling across all API calls +- ✅ Type-safe responses +- ✅ Centralized logging +- ✅ Easy to test and mock + +### Server-Side API Calls + +**For server-side files** (`+layout.server.ts`, `+page.server.ts`, `+server.ts`), use **`.server.ts` API classes** instead of calling the RPC client directly. This prevents API keys from being accidentally bundled into client-side code. + +#### Pattern for Server-Side Files + +Create a `.server.ts` file alongside your `.svelte.ts` API class with static methods that use API key authentication: + +```typescript +// apps/dashboard/src/lib/features/org/api/org.server.ts +import { classroomio } from '$lib/utils/services/api'; +import { getApiKeyHeaders } from '$lib/utils/services/api/server'; + +/** + * Server-side API methods for organization operations + * These methods use API key authentication and should only be used in server-side files + * (+server.ts, +layout.server.ts) to prevent API keys from being exposed to the client. + */ +export class OrgApiServer { + /** + * Gets organization by siteName (server-side) + * @param siteName Organization site name + * @returns Organization or null + */ + static async getOrgBySiteName(siteName: string) { + try { + const response = await classroomio.organization.$get( + { + query: { siteName } + }, + getApiKeyHeaders() + ); + + const data = await response.json(); + return data.success && data.data && data.data.length > 0 ? data.data[0] : null; + } catch (error) { + console.error('Error fetching organization by siteName (server):', error); + return null; + } + } +} +``` + +**Usage in server-side files:** + +```typescript +// apps/dashboard/src/routes/invite/t/[hash]/+layout.server.ts +import { OrgApiServer } from '$lib/features/org/api/org.server'; + +export const load = async ({ params }) => { + // Use static method from .server.ts class + const org = await OrgApiServer.getOrgBySiteName(orgSiteName); + + return { org }; +}; +``` + +#### API Key Authentication for Webhooks/External Calls + +For server-to-server communication (webhooks, external integrations), create `.server.ts` files with static methods: + +```typescript +// apps/dashboard/src/lib/features/org/api/org-plan.server.ts +import { classroomio } from '$lib/utils/services/api'; +import { getApiKeyHeaders } from '$lib/utils/services/api/server'; +import type { TCreateOrgPlan } from '@cio/utils/validation/organization'; + +/** + * Server-side API methods for organization plan operations + * These methods use API key authentication and should only be used in server-side files + * (+server.ts, +layout.server.ts) to prevent API keys from being exposed to the client. + */ +export class OrgPlanApiServer { + /** + * Creates a new organization plan (server-side) + * @param params Organization plan creation parameters + * @returns Response data or null on error + */ + static async createOrgPlan(params: TCreateOrgPlan) { + try { + const response = await classroomio.organization.plan.$post( + { + json: params + }, + getApiKeyHeaders() + ); + + const data = await response.json(); + return data.success ? data.data : null; + } catch (error) { + console.error('Error creating org plan (server):', error); + return null; + } + } +} +``` + +**Usage in webhook handlers:** + +```typescript +// apps/dashboard/src/routes/api/polar/webhook/+server.ts +import { OrgPlanApiServer } from '$lib/features/org/api/org-plan.server'; + +// Use static method from .server.ts class +const result = await OrgPlanApiServer.createOrgPlan(planData); +``` + +#### File Naming Convention + +- **`.svelte.ts`**: Client-side API classes (for Svelte components) + - Example: `org.svelte.ts`, `org-plan.svelte.ts` + - Uses Svelte reactivity (`$state`, `$derived`) + - Manages component state and loading/error states + - **Never** contains API key logic + +- **`.server.ts`**: Server-side API classes (for server-side files) + - Example: `org.server.ts`, `org-plan.server.ts` + - Contains static methods with API key authentication + - **Only** used in `+layout.server.ts`, `+page.server.ts`, `+server.ts` files + - Automatically excluded from client bundles by SvelteKit + +#### API Key Setup + +1. Add `API_KEY` to `apps/api/src/config/env.ts` environment schema +2. Set `API_KEY` environment variable in your deployment +3. Use `authOrApiKeyMiddleware` in API routes that accept both user sessions and API keys +4. Create `.server.ts` files with static methods that use `getApiKeyHeaders()` helper + +**Helper Function**: Use `getApiKeyHeaders()` from `$lib/utils/services/api/server` to get API key headers. This helper: +- Returns the correct headers format for RPC client +- Validates that `API_KEY` is configured +- Provides consistent API key authentication across all server-side files + +#### Why This Pattern? + +- ✅ **Security**: API keys are isolated in `.server.ts` files that cannot be imported on the client +- ✅ **Clear separation**: Client-side (`.svelte.ts`) vs server-side (`.server.ts`) is explicit +- ✅ **Type-safe**: Full TypeScript support end-to-end +- ✅ **Consistent**: Same API routes, same validation, same error handling +- ✅ **Maintainable**: Centralized server-side API methods in dedicated files +- ✅ **SvelteKit convention**: `.server.ts` files are automatically excluded from client bundles + ### Frontend Best Practices #### ✅ DO - **Use type-safe RPC client** for API calls (Hono RPC) +- **Always use `execute` method** in API abstraction classes (never call RPC client directly) +- **Use `.server.ts` API classes** in server-side files (`+layout.server.ts`, `+page.server.ts`, `+server.ts`) +- **Create `.server.ts` files** with static methods for server-side API calls with API key authentication +- **Use API key** for server-to-server communication (webhooks, external calls) - **Validate client-side** before API calls using shared schemas from `@cio/utils/validation` - **Use entity-specific translations** when multiple entities share field names @@ -1272,6 +2075,10 @@ Example with **client-side validation** before API call: #### ❌ DON'T +- **Call RPC client directly** in API classes (always use `execute` method) +- **Call RPC client directly in server-side files** - use `.server.ts` API classes instead +- **Use `.svelte.ts` API classes in server-side files** - they're for client-side Svelte components only +- **Include API key logic in `.svelte.ts` files** - API keys must only be in `.server.ts` files - Skip error handling assuming API calls always succeed - Display raw API error messages without formatting - Skip client-side validation (validate before API call for instant feedback) diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 67def09d..673ed943 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -9,6 +9,7 @@ import { cors } from 'hono/cors'; import { courseRouter } from '@api/routes/course'; import { logger } from 'hono/logger'; import { mailRouter } from '@api/routes/mail'; +import { mediaRouter } from '@api/routes/media'; import { onboardingRouter } from '@api/routes/onboarding'; import { organizationRouter } from '@api/routes/organization'; import { prettyJSON } from 'hono/pretty-json'; @@ -26,7 +27,7 @@ export const app = new Hono() cors({ origin: TRUSTED_ORIGINS, allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowHeaders: ['Content-Type', 'Authorization'], + allowHeaders: ['Content-Type', 'Authorization', 'Cio-org-id'], exposeHeaders: ['Content-Length', 'X-Kuma-Revision'], maxAge: 600, credentials: true @@ -71,6 +72,7 @@ export const app = new Hono() .route('/account', accountRouter) .route('/course', courseRouter) .route('/mail', mailRouter) + .route('/media', mediaRouter) .route('/organization', organizationRouter) // Error handling diff --git a/apps/api/src/config/env.ts b/apps/api/src/config/env.ts index ea03c3f3..2c651155 100644 --- a/apps/api/src/config/env.ts +++ b/apps/api/src/config/env.ts @@ -6,6 +6,7 @@ const envSchema = z.object({ CLOUDFLARE_BUCKET_DOMAIN: z.string().optional(), CLOUDFLARE_RENDERING_API_KEY: z.string().optional(), CLOUDFLARE_SECRET_ACCESS_KEY: z.string().optional(), + CLOUDFLARE_IMAGE_BUCKET_DOMAIN: z.string().optional(), NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), OPENAPI_URL: z.string().optional(), PORT: z.string().optional(), @@ -20,7 +21,8 @@ const envSchema = z.object({ SMTP_SENDER: z.string().optional(), SMTP_USER: z.string().optional(), ZOHO_TOKEN: z.string().optional(), - TRUSTED_ORIGINS: z.string().optional() + TRUSTED_ORIGINS: z.string().optional(), + SERVER_KEY: z.string().optional() }); export const env = envSchema.parse(process.env); diff --git a/apps/api/src/constants/upload.ts b/apps/api/src/constants/upload.ts index ba4b2ebb..806a88fe 100644 --- a/apps/api/src/constants/upload.ts +++ b/apps/api/src/constants/upload.ts @@ -1,20 +1,11 @@ -export const ALLOWED_CONTENT_TYPES = [ - 'video/mp4', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-matroska' -] as const; - -export const ALLOWED_DOCUMENT_TYPES = [ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx - 'application/msword' // .doc -] as const; - +// API-specific constants (file size limits) export const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB export const MAX_DOCUMENT_SIZE = 5 * 1024 * 1024; // 5MB +export const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB +// API-specific constants (bucket names) export const BUCKET_NAME = { VIDEOS: 'videos', - DOCUMENTS: 'documents' + DOCUMENTS: 'documents', + MEDIA: 'media' } as const; diff --git a/apps/api/src/middlewares/api-key.ts b/apps/api/src/middlewares/api-key.ts new file mode 100644 index 00000000..baf92322 --- /dev/null +++ b/apps/api/src/middlewares/api-key.ts @@ -0,0 +1,58 @@ +import { Context, Next } from 'hono'; + +import { env } from '@api/config/env'; + +/** + * Middleware to authenticate requests using API key + * Checks for Authorization: Bearer header + * Used for server-to-server communication (webhooks, etc.) + */ +export const apiKeyMiddleware = async (c: Context, next: Next) => { + try { + const authHeader = c.req.header('Authorization'); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return c.json( + { + success: false, + message: 'Unauthorized: Missing or invalid API key' + }, + 401 + ); + } + + const apiKey = authHeader.replace('Bearer ', '').trim(); + const expectedApiKey = env.SERVER_KEY; + + if (!expectedApiKey) { + console.error('SERVER_KEY not configured in environment'); + return c.json( + { + success: false, + message: 'Server configuration error' + }, + 500 + ); + } + + if (apiKey !== expectedApiKey) { + return c.json( + { + success: false, + message: 'Unauthorized: Invalid API key' + }, + 401 + ); + } + + // API key is valid, continue + await next(); + } catch (error) { + return c.json( + { + success: false, + message: 'Unauthorized' + }, + 401 + ); + } +}; diff --git a/apps/api/src/middlewares/auth-or-api-key.ts b/apps/api/src/middlewares/auth-or-api-key.ts new file mode 100644 index 00000000..8b219fb1 --- /dev/null +++ b/apps/api/src/middlewares/auth-or-api-key.ts @@ -0,0 +1,34 @@ +import { Context, Next } from 'hono'; + +import { apiKeyMiddleware } from './api-key'; +import { authMiddleware } from './auth'; + +/** + * Middleware that accepts either user authentication OR API key + * Used for endpoints that can be accessed by: + * 1. Authenticated users (via session cookies) + * 2. Server-to-server calls (via API key) + */ +export const authOrApiKeyMiddleware = async (c: Context, next: Next) => { + const user = c.get('user'); + const session = c.get('session'); + + if (user && session) { + await next(); + return; + } + + const authHeader = c.req.header('Authorization'); + + if (authHeader && authHeader.startsWith('Bearer ')) { + return apiKeyMiddleware(c, next); + } + + return c.json( + { + success: false, + message: 'Unauthorized: Authentication required (session or API key)' + }, + 401 + ); +}; diff --git a/apps/api/src/middlewares/org-admin.ts b/apps/api/src/middlewares/org-admin.ts new file mode 100644 index 00000000..6197c378 --- /dev/null +++ b/apps/api/src/middlewares/org-admin.ts @@ -0,0 +1,62 @@ +import { Context, Next } from 'hono'; + +import { ErrorCodes } from '@api/utils/errors'; +import { isUserOrgAdmin } from '@cio/db/queries/organization'; + +/** + * Middleware to check if the authenticated user is an admin of the organization + * Requires authMiddleware to be applied first + * Expects orgId in the 'cio-org-id' header + */ +export const orgAdminMiddleware = async (c: Context, next: Next) => { + try { + const user = c.get('user'); + if (!user) { + return c.json( + { + success: false, + error: 'Unauthorized', + code: 'UNAUTHORIZED' + }, + 401 + ); + } + + const orgId = c.req.header('cio-org-id'); + + if (!orgId) { + return c.json( + { + success: false, + error: 'Organization ID is required', + code: 'ORG_ID_REQUIRED' + }, + 400 + ); + } + + const isAdmin = await isUserOrgAdmin(orgId, user.id); + if (!isAdmin) { + return c.json( + { + success: false, + error: 'Only organization admins can perform this action', + code: ErrorCodes.ORG_TEAM_NOT_AUTHORIZED + }, + 403 + ); + } + + await next(); + } catch (error) { + console.error('Error in orgAdminMiddleware:', error); + return c.json( + { + success: false, + error: 'Failed to verify organization admin status', + code: 'ORG_ADMIN_CHECK_FAILED' + }, + 500 + ); + } +}; diff --git a/apps/api/src/routes/mail/mail.ts b/apps/api/src/routes/mail/mail.ts index 28fbf0f4..f913c9b0 100644 --- a/apps/api/src/routes/mail/mail.ts +++ b/apps/api/src/routes/mail/mail.ts @@ -8,7 +8,6 @@ export const mailRouter = new Hono().post('/send', zValidator('json', ZDeliverEm const validatedData = c.req.valid('json'); try { - // Use service to send emails const results = await deliverEmail(validatedData); const hasErrors = results.some((result) => !result.success); diff --git a/apps/api/src/routes/media/index.ts b/apps/api/src/routes/media/index.ts new file mode 100644 index 00000000..33d4737d --- /dev/null +++ b/apps/api/src/routes/media/index.ts @@ -0,0 +1 @@ +export * from './media'; diff --git a/apps/api/src/routes/media/media.ts b/apps/api/src/routes/media/media.ts new file mode 100644 index 00000000..19cba78c --- /dev/null +++ b/apps/api/src/routes/media/media.ts @@ -0,0 +1,29 @@ +import { AppError, ErrorCodes } from '@api/utils/errors'; + +import { Hono } from '@api/utils/hono'; +import { authMiddleware } from '@api/middlewares/auth'; +import { handleError } from '@api/utils/errors'; +import { uploadImage } from '@api/services/media'; + +export const mediaRouter = new Hono().post('/image/upload', authMiddleware, async (c) => { + try { + const body = await c.req.parseBody(); + + const file = body.file; + + if (!file || !(file instanceof File)) { + throw new AppError(new Error('No file provided'), ErrorCodes.VALIDATION_ERROR, 400); + } + + const result = await uploadImage(file); + + return c.json({ + success: true, + url: result.url, + fileKey: result.fileKey, + message: 'Image uploaded successfully' + }); + } catch (error) { + return handleError(c, error, 'Failed to upload image'); + } +}); diff --git a/apps/api/src/routes/organization/organization.ts b/apps/api/src/routes/organization/organization.ts index 731008ad..5e2b5e18 100644 --- a/apps/api/src/routes/organization/organization.ts +++ b/apps/api/src/routes/organization/organization.ts @@ -1,25 +1,39 @@ import { ZCancelOrgPlan, ZCreateOrgPlan, + ZCreateOrganization, ZGetCoursesBySiteName, + ZGetOrgSetup, ZGetOrganizationAudience, ZGetOrganizationTeam, ZGetOrganizations, - ZUpdateOrgPlan + ZInviteTeamMembers, + ZRemoveTeamMember, + ZUpdateOrgPlan, + ZUpdateOrganization, + ZUpdateOrganizationParam } from '@cio/utils/validation/organization'; import { cancelOrgPlan, + createOrg, createOrgPlan, getCoursesByOrgSiteName, getOrgAudience, + getOrgSetupData, getOrgTeam, getOrganizationsWithFilters, + inviteTeamMembers, + removeTeamMember, + updateOrg, updateOrgPlan } from '@api/services/organization'; import { Hono } from '@api/utils/hono'; +import { TOrganization } from '@db/types'; import { authMiddleware } from '@api/middlewares/auth'; +import { authOrApiKeyMiddleware } from '@api/middlewares/auth-or-api-key'; import { handleError } from '@api/utils/errors'; +import { orgAdminMiddleware } from '@api/middlewares/org-admin'; import { zValidator } from '@hono/zod-validator'; export const organizationRouter = new Hono() @@ -27,10 +41,12 @@ export const organizationRouter = new Hono() * GET /organization * Gets organizations with optional filters * Query params: siteName (string, optional), customDomain (string, optional), isCustomDomainVerified (boolean, optional) + * Accepts: User session OR API key (for server-side routes) */ - .get('/', zValidator('query', ZGetOrganizations), async (c) => { + .get('/', authOrApiKeyMiddleware, zValidator('query', ZGetOrganizations), async (c) => { try { const filters = c.req.valid('query'); + console.log('filters', filters); const organizations = await getOrganizationsWithFilters(filters); return c.json( @@ -41,6 +57,7 @@ export const organizationRouter = new Hono() 200 ); } catch (error) { + console.error('Error fetching organizations:', error); return handleError(c, error, 'Failed to fetch organizations'); } }) @@ -65,6 +82,63 @@ export const organizationRouter = new Hono() return handleError(c, error, 'Failed to fetch organization team'); } }) + /** + * POST /organization/:orgId/team/invite + * Invites team members to an organization + * Requires authentication and admin role + */ + .post( + '/:orgId/team/invite', + authMiddleware, + orgAdminMiddleware, + zValidator('param', ZGetOrganizationTeam), + zValidator('json', ZInviteTeamMembers), + async (c) => { + try { + const { orgId } = c.req.valid('param'); + const { emails, roleId } = c.req.valid('json'); + + const members = await inviteTeamMembers(orgId, emails, roleId); + + return c.json( + { + success: true, + data: members + }, + 201 + ); + } catch (error) { + return handleError(c, error, 'Failed to invite team members'); + } + } + ) + /** + * DELETE /organization/:orgId/team/:memberId + * Removes a team member from an organization + * Requires authentication and admin role + */ + .delete( + '/:orgId/team/:memberId', + authMiddleware, + orgAdminMiddleware, + zValidator('param', ZRemoveTeamMember), + async (c) => { + try { + const { orgId, memberId } = c.req.valid('param'); + + await removeTeamMember(orgId, memberId); + + return c.json( + { + success: true + }, + 200 + ); + } catch (error) { + return handleError(c, error, 'Failed to remove team member'); + } + } + ) /** * GET /organization/:orgId/audience * Gets organization audience (students) @@ -91,28 +165,58 @@ export const organizationRouter = new Hono() * Gets courses by organization siteName * Query params: siteName (string) */ - .get('/courses', zValidator('query', ZGetCoursesBySiteName), async (c) => { + .get( + '/courses', + // TODO: Ratelimit this endpoint + // TODO: Add cache + // TODO: Add pagination + // TODO: Add sorting + // TODO: Add filtering + zValidator('query', ZGetCoursesBySiteName), + async (c) => { + try { + const { siteName } = c.req.valid('query'); + const courses = await getCoursesByOrgSiteName(siteName); + + return c.json( + { + success: true, + data: courses + }, + 200 + ); + } catch (error) { + return handleError(c, error, 'Failed to fetch courses'); + } + } + ) + /** + * GET /organization/setup + * Gets setup data for an organization (courses, lessons, exercises, org info) + * Query params: siteName (string) + */ + .get('/setup', zValidator('query', ZGetOrgSetup), async (c) => { try { const { siteName } = c.req.valid('query'); - const courses = await getCoursesByOrgSiteName(siteName); + const setupData = await getOrgSetupData(siteName); return c.json( { success: true, - data: courses + data: setupData }, 200 ); } catch (error) { - return handleError(c, error, 'Failed to fetch courses'); + return handleError(c, error, 'Failed to fetch setup data'); } }) /** * POST /organization/plan * Creates a new organization plan - * Requires authentication + * Requires authentication (user session or API key) */ - .post('/plan', authMiddleware, zValidator('json', ZCreateOrgPlan), async (c) => { + .post('/plan', authOrApiKeyMiddleware, zValidator('json', ZCreateOrgPlan), async (c) => { try { const data = c.req.valid('json'); const plan = await createOrgPlan(data); @@ -131,9 +235,9 @@ export const organizationRouter = new Hono() /** * PUT /organization/plan * Updates an organization plan by subscription ID - * Requires authentication + * Requires authentication (user session or API key) */ - .put('/plan', authMiddleware, zValidator('json', ZUpdateOrgPlan), async (c) => { + .put('/plan', authOrApiKeyMiddleware, zValidator('json', ZUpdateOrgPlan), async (c) => { try { const { subscriptionId, payload } = c.req.valid('json'); const plan = await updateOrgPlan(subscriptionId, payload); @@ -152,9 +256,9 @@ export const organizationRouter = new Hono() /** * POST /organization/plan/cancel * Cancels an organization plan by subscription ID - * Requires authentication + * Requires authentication (user session or API key) */ - .post('/plan/cancel', authMiddleware, zValidator('json', ZCancelOrgPlan), async (c) => { + .post('/plan/cancel', authOrApiKeyMiddleware, zValidator('json', ZCancelOrgPlan), async (c) => { try { const { subscriptionId, payload } = c.req.valid('json'); const plan = await cancelOrgPlan(subscriptionId, payload); @@ -169,4 +273,60 @@ export const organizationRouter = new Hono() } catch (error) { return handleError(c, error, 'Failed to cancel organization plan'); } - }); + }) + /** + * POST /organization + * Creates a new organization with the current user as owner + * Requires authentication + */ + .post('/', authMiddleware, zValidator('json', ZCreateOrganization), async (c) => { + try { + const user = c.get('user'); + const data = c.req.valid('json'); + + const result = await createOrg(user.id, data); + + return c.json( + { + success: true, + data: result + }, + 201 + ); + } catch (error) { + return handleError(c, error, 'Failed to create organization'); + } + }) + /** + * PUT /organization/:orgId + * Updates an organization (name, avatarUrl) + * Requires authentication + */ + .put( + '/:orgId', + authMiddleware, + zValidator('param', ZUpdateOrganizationParam), + zValidator('json', ZUpdateOrganization), + async (c) => { + try { + console.log('updateOrg'); + console.log(c.req.valid('param')); + console.log(c.req.valid('json')); + const { orgId } = c.req.valid('param'); + + const data = c.req.valid('json') as Partial; + + const organization = await updateOrg(orgId, data); + + return c.json( + { + success: true, + data: organization + }, + 200 + ); + } catch (error) { + return handleError(c, error, 'Failed to update organization'); + } + } + ); diff --git a/apps/api/src/services/index.ts b/apps/api/src/services/index.ts index 2e9a7c90..4571d3d8 100644 --- a/apps/api/src/services/index.ts +++ b/apps/api/src/services/index.ts @@ -1,2 +1,3 @@ export * from './account'; +export * from './media'; export * from './onboarding'; diff --git a/apps/api/src/services/media.ts b/apps/api/src/services/media.ts new file mode 100644 index 00000000..7725d2b2 --- /dev/null +++ b/apps/api/src/services/media.ts @@ -0,0 +1,64 @@ +import { AppError, ErrorCodes } from '@api/utils/errors'; +import { BUCKET_NAME, MAX_IMAGE_SIZE } from '@api/constants/upload'; + +import { ALLOWED_IMAGE_TYPES } from '@cio/utils/validation'; +import { env } from '@api/config/env'; +import { generateFileKey } from '@api/utils/upload'; +import { uploadToS3 } from '@api/utils/s3'; + +/** + * Uploads an image file to S3 and returns the public URL + * @param file - The image file to upload + * @returns Object containing the public URL and file key + */ +export async function uploadImage(file: File) { + if (!env.CLOUDFLARE_IMAGE_BUCKET_DOMAIN) { + throw new AppError(new Error('Image bucket domain not configured'), ErrorCodes.INTERNAL_ERROR, 500); + } + + // Validate file type + if (!ALLOWED_IMAGE_TYPES.includes(file.type as any)) { + throw new AppError( + new Error(`Invalid file type. Allowed types: ${ALLOWED_IMAGE_TYPES.join(', ')}`), + ErrorCodes.VALIDATION_ERROR, + 400 + ); + } + + // Validate file size + if (file.size > MAX_IMAGE_SIZE) { + throw new AppError( + new Error(`File size exceeds maximum of ${MAX_IMAGE_SIZE / 1024 / 1024}MB`), + ErrorCodes.VALIDATION_ERROR, + 400 + ); + } + + // Generate unique file key + const fileKey = generateFileKey(file.name); + + // Convert File to Buffer + const arrayBuffer = await file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + // Upload to S3 + const uploadResult = await uploadToS3({ + Bucket: BUCKET_NAME.MEDIA, + Key: fileKey, + Body: buffer, + ContentType: file.type, + CacheControl: 'public, max-age=31536000' // Cache for 1 year + }); + + if (!uploadResult.success) { + throw new AppError(new Error(uploadResult.error || 'Failed to upload image'), ErrorCodes.INTERNAL_ERROR, 500); + } + + // Construct public URL + const publicUrl = `${env.CLOUDFLARE_IMAGE_BUCKET_DOMAIN}/${fileKey}`; + + return { + url: publicUrl, + fileKey + }; +} diff --git a/apps/api/src/services/organization.ts b/apps/api/src/services/organization.ts index 4099f114..bd596cda 100644 --- a/apps/api/src/services/organization.ts +++ b/apps/api/src/services/organization.ts @@ -1,15 +1,56 @@ import { AppError, ErrorCodes } from '@api/utils/errors'; -import type { TNewOrganizationPlan, TOrganizationPlan, TPlan } from '@db/types'; +import type { TNewOrganizationPlan, TOrganization, TOrganizationPlan, TPlan } from '@db/types'; import { cancelOrganizationPlan, + checkEmailsExistInOrg, + createOrganizationMembers, createOrganizationPlan, + deleteOrganizationMember, getOrganizationAudience, + getOrganizationById, + getOrganizationBySiteName, getOrganizationTeam, getOrganizations, + updateOrganization, updateOrganizationPlan } from '@cio/db/queries/organization'; +import { + getCoursesBySiteName, + getCoursesBySiteNameForSetup, + getExercisesBySiteName, + getLessonsBySiteName +} from '@cio/db/queries/course'; -import { getCoursesBySiteName } from '@cio/db/queries/course'; +import { createOrganizationWithOwner } from '@api/services/onboarding'; +import { env } from '@api/config/env'; +import { sendEmail } from '@cio/email'; + +/** + * Creates a new organization with the current user as owner + * @param profileId - The profile ID of the user creating the organization + * @param data - Organization creation data (name, siteName) + * @returns Created organization, member, and updated organizations list + */ +export async function createOrg(profileId: string, data: { name: string; siteName: string }) { + try { + // Reuse existing function from onboarding service + return await createOrganizationWithOwner(profileId, { + orgName: data.name, + siteName: data.siteName + }); + } catch (error) { + // Re-throw AppError as-is + if (error instanceof AppError) { + throw error; + } + // Wrap unexpected errors + throw new AppError( + error instanceof Error ? error.message : 'Failed to create organization', + ErrorCodes.ORG_CREATE_FAILED, + 500 + ); + } +} /** * Gets organizations with optional filters @@ -87,6 +128,44 @@ export async function getCoursesByOrgSiteName(siteName: string) { } } +/** + * Gets setup data for an organization + * @param siteName - The organization site name + * @returns Setup data including courses, lessons, exercises, and organization info + */ +export async function getOrgSetupData(siteName: string) { + try { + const [organization, courses, lessons, exercises] = await Promise.all([ + getOrganizationBySiteName(siteName), + getCoursesBySiteNameForSetup(siteName), + getLessonsBySiteName(siteName), + getExercisesBySiteName(siteName) + ]); + + // Check if course is published + const publishedCourse = courses.find((course) => course.isPublished === true); + + // Get organization avatar URL directly from organization + const orgHasAvatarUrl = !!organization?.avatarUrl; + + return { + isCoursePublished: !!publishedCourse, + isCourseCreated: courses.length > 0, + orgHasAvatarUrl, + courseData: courses, + lessonData: lessons, + isLessonCreated: lessons.length > 0, + isExerciseCreated: exercises.length > 0 + }; + } catch (error) { + throw new AppError( + error instanceof Error ? error.message : 'Failed to fetch setup data', + ErrorCodes.INTERNAL_ERROR, + 500 + ); + } +} + /** * Creates a new organization plan * @param data - Organization plan creation data @@ -113,6 +192,31 @@ export async function createOrgPlan(data: TNewOrganizationPlan) { } } +/** + * Updates an organization + * @param orgId - The organization ID + * @param data - Partial organization data to update + * @returns Updated organization + */ +export async function updateOrg(orgId: string, data: Partial) { + try { + const organization = await updateOrganization(orgId, data); + if (!organization) { + throw new AppError('Organization not found', ErrorCodes.ORGANIZATION_NOT_FOUND, 404); + } + return organization; + } catch (error) { + if (error instanceof AppError) { + throw error; + } + throw new AppError( + error instanceof Error ? error.message : 'Failed to update organization', + ErrorCodes.INTERNAL_ERROR, + 500 + ); + } +} + /** * Updates an organization plan * @param subscriptionId - Subscription ID @@ -163,3 +267,97 @@ export async function cancelOrgPlan(subscriptionId: string, payload: TOrganizati ); } } + +/** + * Invites team members to an organization + * @param orgId - The organization ID + * @param emails - Array of email addresses to invite + * @param roleId - Role ID to assign (ADMIN or TUTOR) + * @returns Array of created members + */ +export async function inviteTeamMembers(orgId: string, emails: string[], roleId: number) { + const organization = await getOrganizationById(orgId); + if (!organization) { + throw new AppError('Organization not found', ErrorCodes.ORGANIZATION_NOT_FOUND, 404); + } + + const normalizedEmails = [...new Set(emails.map((email) => email.toLowerCase().trim()))]; + + const existingEmails = await checkEmailsExistInOrg(orgId, normalizedEmails); + const emailsToInvite = normalizedEmails.filter((email) => !existingEmails.includes(email)); + + if (emailsToInvite.length === 0) { + return []; + } + + try { + const members = await createOrganizationMembers( + emailsToInvite.map((email) => ({ + organizationId: orgId, + email, + roleId, + verified: false + })) + ); + + const baseUrl = env.NODE_ENV === 'development' ? 'http://localhost:5173' : 'https://app.classroomio.com'; + + for (const email of emailsToInvite) { + try { + const inviteData = JSON.stringify({ + email, + orgId: organization.id, + orgSiteName: organization.siteName + }); + const inviteLink = `${baseUrl}/invite/t/${encodeURIComponent(btoa(inviteData))}`; + + await sendEmail('inviteTeacher', { + to: email, + fields: { + email, + orgName: organization.name, + orgSiteName: organization.siteName, + inviteLink + } + }); + } catch (emailError) { + console.error(`Failed to send invite email to ${email}:`, emailError); + } + } + + return members; + } catch (error) { + console.error('Error inviting team members:', error); + throw new AppError( + error instanceof Error ? error.message : 'Failed to invite team members', + ErrorCodes.ORG_TEAM_INVITE_FAILED, + 500 + ); + } +} + +/** + * Removes a team member from an organization + * @param orgId - The organization ID + * @param memberId - The member ID to remove + * @returns Deleted member + */ +export async function removeTeamMember(orgId: string, memberId: number) { + try { + const deleted = await deleteOrganizationMember(orgId, memberId); + if (!deleted) { + throw new AppError('Team member not found', ErrorCodes.ORG_TEAM_REMOVE_FAILED, 404); + } + + return deleted; + } catch (error) { + if (error instanceof AppError) { + throw error; + } + throw new AppError( + error instanceof Error ? error.message : 'Failed to remove team member', + ErrorCodes.ORG_TEAM_REMOVE_FAILED, + 500 + ); + } +} diff --git a/apps/api/src/utils/errors.ts b/apps/api/src/utils/errors.ts index 549adbba..23e99f00 100644 --- a/apps/api/src/utils/errors.ts +++ b/apps/api/src/utils/errors.ts @@ -40,6 +40,10 @@ export const ErrorCodes = { ORG_PLAN_UPDATE_FAILED: 'ORG_PLAN_UPDATE_FAILED', ORG_PLAN_CANCEL_FAILED: 'ORG_PLAN_CANCEL_FAILED', ORG_PLAN_NOT_FOUND: 'ORG_PLAN_NOT_FOUND', + ORG_TEAM_INVITE_FAILED: 'ORG_TEAM_INVITE_FAILED', + ORG_TEAM_REMOVE_FAILED: 'ORG_TEAM_REMOVE_FAILED', + ORG_TEAM_EMAIL_EXISTS: 'ORG_TEAM_EMAIL_EXISTS', + ORG_TEAM_NOT_AUTHORIZED: 'ORG_TEAM_NOT_AUTHORIZED', // Profile errors PROFILE_UPDATE_FAILED: 'PROFILE_UPDATE_FAILED', diff --git a/apps/api/src/utils/s3.ts b/apps/api/src/utils/s3.ts index 7269a0a1..400daaa3 100644 --- a/apps/api/src/utils/s3.ts +++ b/apps/api/src/utils/s3.ts @@ -22,11 +22,10 @@ export const s3Client = new S3Client({ } }); -export async function uploadToS3( - params: PutObjectCommandInput -): Promise<{ success: boolean; error?: string }> { +export async function uploadToS3(params: PutObjectCommandInput): Promise<{ success: boolean; error?: string }> { try { await s3Client.send(new PutObjectCommand(params)); + return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; @@ -44,9 +43,7 @@ export async function getFromS3( } } -export async function deleteFromS3( - params: DeleteObjectCommandInput -): Promise<{ success: boolean; error?: string }> { +export async function deleteFromS3(params: DeleteObjectCommandInput): Promise<{ success: boolean; error?: string }> { try { await s3Client.send(new DeleteObjectCommand(params)); return { success: true }; diff --git a/apps/api/src/utils/upload.ts b/apps/api/src/utils/upload.ts index 99cd0198..4891f1e8 100644 --- a/apps/api/src/utils/upload.ts +++ b/apps/api/src/utils/upload.ts @@ -11,6 +11,13 @@ export function getExtension(filename: string): string { return ext.length > 1 ? ext[ext.length - 1] : ''; } +export function removeSpacesAndSpecialCharacters(fileName: string): string { + return fileName + .replace(/\s+/g, '-') + .replace(/[^a-zA-Z0-9.-]/g, '') + .toLowerCase(); +} + /** * Generates a unique file key for storage, preserving the file extension. * Throws an error if the filename is invalid or has no extension. @@ -20,5 +27,8 @@ export function generateFileKey(fileName: string): string { if (!ext) { throw new Error('Invalid file name or missing file extension'); } - return `${nanoid()}.${ext}`; + + const cleanedFileName = removeSpacesAndSpecialCharacters(fileName); + + return `${nanoid()}-${cleanedFileName}`; } diff --git a/apps/dashboard/ROUTE_PAGE_SETUP.md b/apps/dashboard/ROUTE_PAGE_SETUP.md new file mode 100644 index 00000000..ef808659 --- /dev/null +++ b/apps/dashboard/ROUTE_PAGE_SETUP.md @@ -0,0 +1,288 @@ +# Route Page Setup Documentation + +This document outlines the pattern for setting up page routes in the dashboard application. Follow this structure to maintain consistency across the codebase. + +## Overview + +The application follows a feature-based architecture where: + +- **Route files** (`src/routes/org/[slug]/[feature]/+page.svelte`) are minimal wrappers that set up the Page structure +- **Feature pages** (`src/lib/features/[feature]/pages/[feature].svelte`) contain all business logic, state management, and UI components +- **Components** are organized in `src/lib/features/[feature]/components/` and exported via `index.ts` +- **Pages** are exported from `src/lib/features/[feature]/pages/index.ts` + +## Directory Structure + +```bash +apps/dashboard/src/ +├── routes/ +│ └── org/ +│ └── [slug]/ +│ └── [feature]/ +│ └── +page.svelte # Minimal route wrapper +└── lib/ + └── features/ + └── [feature]/ + ├── components/ + │ ├── [component].svelte + │ └── index.ts # Export all components + ├── pages/ + │ ├── [feature].svelte # Main feature page with all logic + │ └── index.ts # Export page component + ├── utils/ # Feature-specific utilities + └── api/ # Feature-specific API calls +``` + +## Route File Pattern (`+page.svelte`) + +The route file should be minimal and only contain: + +1. **Page structure** using `@cio/ui/base/page` components +2. **Import and render** the feature page component +3. **Page metadata** (title, etc.) + +### Example Route File + +```svelte + + + + Feature Name - ClassroomIO + + + + + + {$t('feature.title')} + + + + + + + {#snippet child()} + + {/snippet} + + +``` + +### Route File Rules + +- ✅ DO: Keep it minimal - only Page structure +- ✅ DO: Import feature page from `$lib/features/[feature]/pages` +- ✅ DO: Import action buttons/components from `$lib/features/[feature]/components` +- ✅ DO: Use `Page.Root`, `Page.Header`, `Page.HeaderContent`, `Page.Title`, `Page.Action`, `Page.Body` +- ❌ DON'T: Put business logic in route files +- ❌ DON'T: Put state management in route files +- ❌ DON'T: Put data fetching in route files +- ❌ DON'T: Put filters/search in route files (use `Page.BodyHeader` in feature page) + +## Feature Page Pattern (`pages/[feature].svelte`) + +The feature page contains all the business logic, state management, and UI components. + +### Feature Page Structure + +```svelte + + + + + + + + + + + + +
+ {#if isLoading} + + + + + {:else if !data.length} + + + + + {:else} + + {#each data as item} + + {/each} + {/if} +
+``` + +### Feature Page Rules + +- ✅ DO: Use `Page.BodyHeader` for filters, search, and view toggles +- ✅ DO: Use `Empty` component from `@cio/ui/custom/empty` for empty states +- ✅ DO: Use `Search` component from `@cio/ui/custom/search` for search inputs +- ✅ DO: Handle loading states with skeleton/loader components +- ✅ DO: Export the page component from `pages/index.ts` as `[Feature]Page` +- ✅ DO: Keep all business logic in the feature page +- ❌ DON'T: Use `Page.Header` or `Page.Action` in feature pages (these are in route files) +- ❌ DON'T: Put route-specific logic in feature pages + +## Component Organization + +### Components Directory + +Create reusable components specific to the feature: + +```bash +components/ +├── feature-card.svelte +├── feature-loader.svelte +├── feature-modal.svelte +└── index.ts +``` + +### Components Index (`components/index.ts`) + +```typescript +export { default as FeatureCard } from './feature-card.svelte'; +export { default as FeatureLoader } from './feature-loader.svelte'; +export { default as FeatureModal } from './feature-modal.svelte'; +export { default as FeatureActionButton } from './feature-action-button.svelte'; +``` + +## Pages Index (`pages/index.ts`) + +```typescript +export { default as FeaturePage } from './feature.svelte'; +``` + +## Complete Example + +### Route File: `routes/org/[slug]/community/+page.svelte` + +```svelte + + + + Community - ClassroomIO + + + + + + {$t('community.title')} + + + + + + + {#snippet child()} + + {/snippet} + + +``` + +### Feature Page: `lib/features/community/pages/community.svelte` + +```svelte + + + + + + + + + +{#if isLoading} + +{:else} + {#each discussions as discussion} + + {:else} + + + + {/each} +{/if} +``` + +## Key Patterns + +1. **Separation of Concerns**: Route files handle routing structure, feature pages handle business logic +2. **Reusability**: Feature pages can be imported and used in different contexts +3. **Consistency**: All pages follow the same structure for maintainability +4. **Design System**: Use `Page.BodyHeader` for filters, `Empty` for empty states, `Search` for search inputs +5. **State Management**: Keep state local to feature pages or use feature-specific stores + +## Migration Checklist + +When refactoring an existing route: + +- [ ] Create feature directory structure (`lib/features/[feature]/`) +- [ ] Move business logic from route file to feature page +- [ ] Extract components to `components/` directory +- [ ] Create `components/index.ts` with exports +- [ ] Create `pages/index.ts` with page export +- [ ] Update route file to be minimal wrapper +- [ ] Add `Page.BodyHeader` for filters/search if needed +- [ ] Replace custom empty states with `Empty` component +- [ ] Replace custom search inputs with `Search` component +- [ ] Test all functionality works correctly diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index ffcd29be..f3ab001f 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -38,7 +38,7 @@ "ai": "^2.1.31", "axios": "^1.8.4", "better-auth": "^1.3.34", - "bits-ui": "^2.14.1", + "bits-ui": "^2.14.4", "body-parser": "^1.20.2", "bufferutil": "^4.0.9", "canvas-confetti": "^1.6.0", @@ -109,7 +109,6 @@ "svelte": "^5.0.0", "svelte-dnd-action": "^0.9.24", "svelte-jester": "^3.0.0", - "svelte-loading-spinners": "^0.3.4", "svelte-preprocess": "^6.0.0", "tailwindcss": "^4.1.14", "ts-jest": "^29.1.1", diff --git a/apps/dashboard/src/app.css b/apps/dashboard/src/app.css index 12ea0b6d..2ae336a6 100644 --- a/apps/dashboard/src/app.css +++ b/apps/dashboard/src/app.css @@ -1,4 +1,6 @@ /*Write your global styles here, in PostCSS syntax */ +@import url('https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap'); + @import 'tailwindcss'; @import '@cio/ui/output.css'; @@ -44,17 +46,6 @@ html .dark { border: 2px solid var(--scrollbarBG); } -h1, -h2, -h3, -h4, -h5, -h6 { - font-size: revert; - font-weight: revert; - margin: 0.8em 0; -} - .prose h1, .prose h2, .prose h3, @@ -809,6 +800,7 @@ svg.lucide-icon:not(.custom) { --animate-roll: quizRoll 0.8s ease-in-out; --animate-gather: audienceGather 0.7s ease-in-out; --animate-setup-configure: setupConfigure 1s ease-in-out; + --animate-border: border 4s linear infinite; } @keyframes shakeAnnouncement { @@ -1152,3 +1144,191 @@ svg.lucide-icon:not(.custom) { transform: scale(1) rotate(0deg); } } + +/* Animated Gradient Border */ +@property --border-angle { + inherits: false; + initial-value: 0deg; + syntax: ''; +} + +@keyframes border { + to { + --border-angle: 360deg; + } +} + +/* Animated Gradient Border Class */ +.animate-gradient-border { + border: 1px solid transparent; + background: + linear-gradient(45deg, white, white) padding-box, + conic-gradient( + from var(--border-angle), + rgb(148 163 184 / 0.3) 75%, + rgb(37 99 235) 82%, + rgb(96 165 250) 88%, + rgb(37 99 235) 92%, + rgb(148 163 184 / 0.3) 100% + ) + border-box; + transition: --border-angle 3s ease-in-out; +} + +.animate-gradient-border:hover { + animation: border 4s ease-in-out infinite; +} + +.dark .animate-gradient-border { + background: + linear-gradient(45deg, rgb(0 0 0), rgb(0 0 0)) padding-box, + conic-gradient( + from var(--border-angle), + rgb(71 85 105 / 0.48) 75%, + rgb(37 99 235) 82%, + rgb(96 165 250) 88%, + rgb(37 99 235) 92%, + rgb(71 85 105 / 0.48) 100% + ) + border-box; + transition: --border-angle 3s ease-in-out; +} + +.dark .animate-gradient-border:hover { + animation: border 4s ease-in-out infinite; +} + +/** + Rocket launch animation. +**/ +.rocket-launch { + transform-origin: center bottom; + transform-box: fill-box; +} + +/* Full sequence runs once per hover */ +.animate-icon:hover .rocket-launch { + animation: rocket-sequence 5s ease-in-out infinite; +} + +/* Optional: flame flicker during pre-launch + launch */ +.animate-icon:hover .rocket-launch path:first-of-type { + animation: flame-sequence 5s ease-in-out infinite; +} + +/* + Rocket: + - 0–60% (~3s): jitter/pre-launch, small moves along nose direction + - 60–70%: launch along ~60° diagonal (up-right), fade out + - 70–80%: invisible pause + - 80–100%: re-enter from opposite side (down-left) +*/ +@keyframes rocket-sequence { + 0% { + transform: translate(0, 0) scale(1); + opacity: 1; + } + + 10% { + transform: translate(1px, -2px) scale(1.02); + opacity: 1; + } + + 20% { + transform: translate(0, 0) scale(1); + opacity: 1; + } + + 30% { + transform: translate(2px, -3px) scale(1.03); + opacity: 1; + } + + 40% { + transform: translate(0, 0) scale(1); + opacity: 1; + } + + 50% { + transform: translate(1px, -2px) scale(1.02); + opacity: 1; + } + + 60% { + transform: translate(0, 0) scale(1.05); + opacity: 1; + } + + /* Launch along a ~60° diagonal: up (-Y) and to the right (+X) */ + 70% { + transform: translate(100%, -173%) scale(1.1); + opacity: 0; + } + + /* Off-screen / invisible pause (~1s, 70–80%) */ + 80% { + /* Reposition off-screen in the opposite direction: down-left */ + transform: translate(-100%, 173%) scale(1.1); + opacity: 0; + } + + /* Fly back into view from below-left to original position */ + 100% { + transform: translate(0, 0) scale(1); + opacity: 1; + } +} + +/* + Flame sequence: + - Moves opposite rocket travel (down-left: -X, +Y) to match exhaust + - No vertical-only scaling; just uniform scale + diagonal translation + - Visible 0–70%, then off +*/ +@keyframes flame-sequence { + 0% { + stroke: var(--main-primary-color); + opacity: 1; + transform: translate(-0.5px, 0.9px) scale(1.02); + } + + 10% { + opacity: 1; + transform: translate(0, 0) scale(1); + } + + 20% { + opacity: 1; + transform: translate(0, 0) scale(1.02); + } + + 30% { + opacity: 0.8; + transform: translate(-0.5px, 0.9px) scale(1); + } + + 40% { + opacity: 0.8; + transform: translate(0, 0) scale(1.02); + } + + 50% { + opacity: 1; + transform: translate(-0.5px, 0.9px) scale(1); + } + + 60% { + opacity: 0.8; + transform: translate(0, 0) scale(1.02); + } + + 70% { + opacity: 0; + transform: translate(-0.5px, 0.9px) scale(1.1); + } + + 100% { + opacity: 0; + transform: translate(-1px, 1.7px) scale(1.12); + } +} diff --git a/apps/dashboard/src/lib/components/AuthUI/index.svelte b/apps/dashboard/src/lib/components/AuthUI/index.svelte deleted file mode 100644 index 63f9cd82..00000000 --- a/apps/dashboard/src/lib/components/AuthUI/index.svelte +++ /dev/null @@ -1,124 +0,0 @@ - - -
-
-
- {#if !showOnlyContent || showLogo} - - {/if} -
- {@render children?.()} -
- {#if !showOnlyContent && !hideGoogleAuth} -
-

{$t('login.signup_with')}:

- - - - {isLogin ? $t('login.login_with_google') : $t('login.signup_with_google')} - - -
- {/if} -
- {#if !showOnlyContent} -
- {#if isLogin} - {$t('login.not_registered_yet')} - {$t('login.signup')} - {:else} - {$t('login.already_have_account')} - {$t('login.login')} - {/if} -
- {/if} -
-
- - diff --git a/apps/dashboard/src/lib/components/Buttons/Close/index.svelte b/apps/dashboard/src/lib/components/Buttons/Close/index.svelte index 469fe82f..44ab8032 100644 --- a/apps/dashboard/src/lib/components/Buttons/Close/index.svelte +++ b/apps/dashboard/src/lib/components/Buttons/Close/index.svelte @@ -1,6 +1,6 @@ - + diff --git a/apps/dashboard/src/lib/components/Course/components/Ceritficate/Design.svelte b/apps/dashboard/src/lib/components/Course/components/Ceritficate/Design.svelte index f6604a82..f9825b5d 100644 --- a/apps/dashboard/src/lib/components/Course/components/Ceritficate/Design.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Ceritficate/Design.svelte @@ -16,7 +16,7 @@ import Plain from './templates/Plain.svelte'; import Professional from './templates/Professional.svelte'; import TextArea from '$lib/components/Form/TextArea.svelte'; - import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte'; + import { UpgradeBanner } from '$lib/features/ui'; import BlueBadgePattern from './templates/BlueBadgePattern.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PurpleBadgePattern from './templates/PurpleBadgePattern.svelte'; diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/ViewMode.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/ViewMode.svelte index b5ac1913..894d0e8d 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/ViewMode.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/ViewMode.svelte @@ -10,7 +10,7 @@ import TextareaQuestion from '$lib/components/Question/TextareaQuestion/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import Box from '$lib/components/Box/index.svelte'; - import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte'; + import { RoleBasedSecurity } from '$lib/features/ui'; import Progress from '$lib/components/Progress/index.svelte'; import { removeDuplicate } from '$lib/utils/functions/removeDuplicate'; import { QUESTION_TYPE } from '$lib/components/Question/constants'; diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/index.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/index.svelte index b80a71c9..e8474cd5 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/index.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/index.svelte @@ -29,7 +29,7 @@ import { IconButton } from '$lib/components/IconButton'; import UpdateDescription from './UpdateDescription.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; - import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte'; + import { RoleBasedSecurity } from '$lib/features/ui'; interface Props { exerciseId?: string; diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/Exercises/index.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/Exercises/index.svelte index dfd67add..642ebde4 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/Exercises/index.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/Exercises/index.svelte @@ -1,7 +1,7 @@
  • - +
  • {#if !disableSignup}
  • - +
  • {/if} diff --git a/apps/dashboard/src/lib/components/Navigation/CustomLinks.svelte b/apps/dashboard/src/lib/components/Navigation/CustomLinks.svelte index b5ea1297..6d0fd493 100644 --- a/apps/dashboard/src/lib/components/Navigation/CustomLinks.svelte +++ b/apps/dashboard/src/lib/components/Navigation/CustomLinks.svelte @@ -1,5 +1,6 @@ - -
    - -
    -
    - - -
    -
    - - - {#if isLoading} -
    - {#each Array(pageSize) as _} - - {/each} -
    - {:else} -
    - - - - {#each headers as header} - {header.value} - {/each} - - - - {#each paginatedRows as row} - - - - - {row.name} - - - {row.email} - {row.date_joined} - - {/each} - - -
    - {/if} - - -
    - (currentPage = page)} - > - {#snippet children({ pages, currentPage: activePage })} - - - - - {#each pages as page (page.key)} - {#if page.type === 'ellipsis'} - - - - {:else} - - - {page.value} - - - {/if} - {/each} - - - - - {/snippet} - -
    -
    diff --git a/apps/dashboard/src/lib/components/Org/Audience/index.svelte b/apps/dashboard/src/lib/components/Org/Audience/index.svelte deleted file mode 100644 index 34879c02..00000000 --- a/apps/dashboard/src/lib/components/Org/Audience/index.svelte +++ /dev/null @@ -1,39 +0,0 @@ - - -
    - {#if $orgAudience.length || isLoading} - - {:else} - - -

    {$t('audience.no_audience')}!

    -

    - {$t('audience.manage')}. -

    -
    - {/if} -
    diff --git a/apps/dashboard/src/lib/components/Org/Community/Loader.svelte b/apps/dashboard/src/lib/components/Org/Community/Loader.svelte deleted file mode 100644 index b8c25b35..00000000 --- a/apps/dashboard/src/lib/components/Org/Community/Loader.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - -
    -
    - - - -
    - -
    - -
    - -
    - - -
    - -
    - -
    -
    diff --git a/apps/dashboard/src/lib/components/Org/LandingPage/PageLoader.svelte b/apps/dashboard/src/lib/components/Org/LandingPage/PageLoader.svelte index 428e135a..fc714937 100644 --- a/apps/dashboard/src/lib/components/Org/LandingPage/PageLoader.svelte +++ b/apps/dashboard/src/lib/components/Org/LandingPage/PageLoader.svelte @@ -1,6 +1,6 @@
    @@ -19,8 +19,8 @@
    - - - + + +
    diff --git a/apps/dashboard/src/lib/components/Org/LandingPage/index.svelte b/apps/dashboard/src/lib/components/Org/LandingPage/index.svelte index 6edf28a5..2517f08c 100644 --- a/apps/dashboard/src/lib/components/Org/LandingPage/index.svelte +++ b/apps/dashboard/src/lib/components/Org/LandingPage/index.svelte @@ -12,32 +12,29 @@ import LinkedinIcon from '@lucide/svelte/icons/linkedin'; import { preventDefault } from '$lib/utils/functions/svelte'; - import Box from '$lib/components/Box/index.svelte'; - import TextArea from '$lib/components/Form/TextArea.svelte'; - import TextField from '$lib/components/Form/TextField.svelte'; import Navigation from '$lib/components/Navigation/index.svelte'; - import PoweredBy from '$lib/components/Upgrade/PoweredBy.svelte'; - import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; - import Card from '$lib/components/Courses/components/Card/index.svelte'; + import { PoweredBy } from '$lib/features/ui'; + import { CourseCard, CourseCardLoader } from '$lib/features/course/components'; import { landingPageSettings } from '$lib/components/Org/Settings/store'; import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte'; - import CardLoader from '$lib/components/Courses/components/Card/Loader.svelte'; import PageLoader from './PageLoader.svelte'; - import { Row, Grid, Column } from '../Settings/Layout'; import * as Accordion from '@cio/ui/base/accordion'; + import * as Button from '@cio/ui/base/button'; + import * as Field from '@cio/ui/base/field'; + import { Input } from '@cio/ui/base/input'; + import { Textarea } from '@cio/ui/base/textarea'; + import { Empty } from '@cio/ui/custom/empty'; import { t } from '$lib/utils/functions/translations'; - import type { CurrentOrg } from '$lib/utils/types/org'; - import { getSupabase } from '$lib/utils/functions/supabase'; - import { getCourseBySiteName } from '$lib/utils/services/org'; - import { VARIANTS } from '$lib/components/PrimaryButton/constants'; + import type { AccountOrg } from '$lib/features/app/types'; + import { orgApi } from '$lib/features/org/api/org.svelte'; import { validateEmail } from '$lib/utils/functions/validateEmail'; - import { courseMetaDeta, courses } from '$lib/components/Courses/store'; + import { courseMetaDeta, courses } from '$lib/features/course/utils/store'; import { orgLandingpageValidation } from '$lib/utils/functions/validator'; interface Props { orgSiteName?: string; - org: CurrentOrg | null; + org: AccountOrg; } let { orgSiteName = '', org }: Props = $props(); @@ -57,20 +54,11 @@ }); let contactError: Record = $state({}); - const supabase = getSupabase(); - async function handleSubmit() { if (!email || !validateEmail(email) || !org) return; isAdding = true; - const { error } = await supabase.from('organization_emaillist').insert({ - email, - organization_id: org.id - }); - - if (error) { - console.error('Error', error); - } + // TODO: Implement email list submission success = true; setTimeout(() => { @@ -89,18 +77,7 @@ } // Save to db - const { error } = await supabase.from('organization_contacts').insert({ - name: contact.name, - email: contact.email, - phone: contact.phone, - message: contact.message, - organization_id: org?.id - }); - - if (error) { - console.error('Something went wrong', error, '\n\nContact', contact); - isContactSubmiting = false; - } + // TODO: Implement contact submission isContactSubmiting = false; successContactSaved = true; @@ -123,11 +100,17 @@ untrack(async () => { try { $courseMetaDeta.isLoading = true; - const coursesResult = await getCourseBySiteName(siteName); - courses.set(coursesResult); + await orgApi.getCourseBySiteName(siteName); + if (orgApi.error) { + console.error('Error fetching courses', orgApi.error); + courses.set([]); + } else { + courses.set(orgApi.orgSiteCourses); + } $courseMetaDeta.isLoading = false; } catch (error) { console.log('error', error); + $courseMetaDeta.isLoading = false; } }); } @@ -157,18 +140,18 @@ } function getBgImage(settings) { + const defaultImg = '/images/org-landingpage-banner.jpeg'; + const { show, image } = settings.header.background || { show: true, - image: '/images/org-landingpage-banner.jpeg' + image: defaultImg }; if (!show) { return undefined; } - if (image) { - return `url('${image}')`; - } + return `url('${image || defaultImg}')`; } $effect(() => { @@ -195,7 +178,7 @@ {:else}
    -
    +
    {#if $landingPageSettings.header.banner.show}
    - { + { $landingPageSettings.header.action.redirect && goto($landingPageSettings.header.action.link); }} - /> + > + {$landingPageSettings.header.action.label} +
    @@ -275,13 +259,14 @@ {$landingPageSettings.header.subtitle}

    - { + { $landingPageSettings.header.action.redirect && goto($landingPageSettings.header.action.link); }} - /> + > + {$landingPageSettings.header.action.label} +
    {/if} @@ -322,14 +307,14 @@ {#if $courseMetaDeta.isLoading}
    - - - + + +
    {:else if $courses.length > 0}
    {#each $courses.slice(0, viewAll ? $courses.length : 3) as courseData} - {:else} - - -

    - {$t('course.navItem.landing_page.no_course_published')} -

    -

    - {$t('course.navItem.landing_page.coming_your_way')} -

    -
    + {/if} {#if $courses.length > 3}
    - (viewAll = !viewAll)} - label={viewAll ? $t('course.navItem.landing_page.view_less') : $t('course.navItem.landing_page.view_all')} - className="px-10 py-5 w-fit" - /> + (viewAll = !viewAll)} class="w-fit px-10 py-5"> + {viewAll ? $t('course.navItem.landing_page.view_less') : $t('course.navItem.landing_page.view_all')} +
    {/if} @@ -386,13 +365,11 @@ {#each $landingPageSettings.faq.questions as faq, index} - + {faq.title} -

    - {faq.content} -

    + {faq.content}
    {/each} @@ -412,30 +389,30 @@

    {$landingPageSettings.contact.subtitle}

    - +
    - - +

    {$landingPageSettings.contact.address}

    - - +

    {$landingPageSettings.contact.phone}

    - - +

    {$landingPageSettings.contact.email}

    - - +
    +
    @@ -447,48 +424,69 @@
    - - - + + + {$t('course.navItem.landing_page.name')} + + + {#if contactError.name} + {contactError.name} + {/if} + + + + {$t('course.navItem.landing_page.email')} + + + {#if contactError.email} + {contactError.email} + {/if} + + + + {$t('course.navItem.landing_page.phone')} + + + {#if contactError.phone} + {contactError.phone} + {/if} +
    - + + Submit + + +
    + {/snippet} + diff --git a/packages/storybook/src/molecules/page/fields.ts b/packages/storybook/src/molecules/page/fields.ts new file mode 100644 index 00000000..7f2a595d --- /dev/null +++ b/packages/storybook/src/molecules/page/fields.ts @@ -0,0 +1,2 @@ +export const FIELDS = ['class'] as string[]; + diff --git a/packages/storybook/src/molecules/page/page.stories.svelte b/packages/storybook/src/molecules/page/page.stories.svelte new file mode 100644 index 00000000..55ff723a --- /dev/null +++ b/packages/storybook/src/molecules/page/page.stories.svelte @@ -0,0 +1,387 @@ + + + + {#snippet template()} + + + + Courses + Manage and organize your course offerings. + + + + + + + + {#snippet child()} + + + + + + No Courses Yet + + You haven't created any courses yet. Get started by creating your first course. + + + +
    + + +
    +
    +
    + {/snippet} +
    +
    + {/snippet} +
    + + + {#snippet template()} + + + + Courses + Manage and organize your course offerings. + + + + + + + + + + + + + + + + + + + {#snippet child({ props })} + + {/snippet} + + + + + + Export Courses + + + + Share Courses + + + + Archive Selected + + + + + + + Delete Selected + + + + + + + + {#snippet child()} + + + + + + No Courses Yet + + You haven't created any courses yet. Get started by creating your first course. + + + +
    + + +
    +
    +
    + {/snippet} +
    +
    + {/snippet} +
    + + + {#snippet template()} + + + + Classes + View and manage your class schedules and sessions. + + + + + + + + + + + + {#snippet child({ props })} + + {/snippet} + + + + + + Export Schedule + + + + Class Settings + + + + + + + + + + + {#snippet child()} + + + + + + No Classes Scheduled + + Create your first class session to start teaching. Classes help you organize your teaching schedule. + + + + + + + {/snippet} + + + {/snippet} + + + + {#snippet template()} + + + + Students + Manage student enrollments and track progress. + + + + + + + + + + + + {#snippet child({ props })} + + {/snippet} + + + + + + Export Student List + + + + Generate Report + + + + Share Student List + + + + + + + + + + + {#snippet child()} + + + + + + No Students Enrolled + + Start by adding students to your courses. You can invite them via email or import from a CSV file. + + + +
    + + +
    +
    +
    + {/snippet} +
    +
    + {/snippet} +
    + + + {#snippet template()} + + + + Analytics Dashboard + View and analyze your learning management data. + + + + + + + + + + + + {#snippet child({ props })} + + {/snippet} + + + + + + Share Dashboard + + + + Dashboard Settings + + + + + + + + + + {/snippet} + diff --git a/packages/storybook/src/molecules/password/password.stories.svelte b/packages/storybook/src/molecules/password/password.stories.svelte new file mode 100644 index 00000000..db112957 --- /dev/null +++ b/packages/storybook/src/molecules/password/password.stories.svelte @@ -0,0 +1,97 @@ + + + + {#snippet template()} +
    + +

    Value: {password || '(empty)'}

    +
    + {/snippet} +
    + + + {#snippet template()} +
    + + Password + + + + Password must be more than 8 characters + +
    + {/snippet} +
    + + + {#snippet template()} +
    + + Password + + + + Password is required + +
    + {/snippet} +
    + + + {#snippet template()} +
    + + Password + + + + +
    + {/snippet} +
    + + + {#snippet template()} +
    + +
    + {/snippet} +
    + diff --git a/packages/storybook/tsconfig.json b/packages/storybook/tsconfig.json index fe87ddc5..4371798b 100644 --- a/packages/storybook/tsconfig.json +++ b/packages/storybook/tsconfig.json @@ -17,9 +17,9 @@ "noUnusedLocals": true, "noUnusedParameters": true, "paths": { - "@cio/ui": ["../ui/src"], - "@cio/ui/*": ["../ui/src/*"], - "$src/*": ["../ui/*"] + "@cio/ui": ["./node_modules/@cio/ui/src"], + "@cio/ui/*": ["./node_modules/@cio/ui/src/*"], + "$src/*": ["./node_modules/@cio/ui/*"] }, "typeRoots": ["../../node_modules/@types", "./node_modules/@types"], "types": ["node"], @@ -34,5 +34,6 @@ ] }, "include": ["src/**/*", ".storybook/**/*", "vite.config.ts"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], + "references": [{ "path": "../ui" }] } diff --git a/packages/ui/jsrepo.json b/packages/ui/jsrepo.json new file mode 100644 index 00000000..d87b0aa6 --- /dev/null +++ b/packages/ui/jsrepo.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://unpkg.com/jsrepo@2.5.2/schemas/project-config.json", + "repos": ["@ieedan/shadcn-svelte-extras"], + "includeTests": false, + "includeDocs": false, + "watermark": true, + "formatter": "prettier", + "configFiles": {}, + "paths": { + "*": "$src", + "ui": "$src/base", + "actions": "$src/actions", + "hooks": "$src/hooks", + "utils": "$src/tools" + } +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 3b4f15f5..472ef445 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -12,19 +12,24 @@ }, "devDependencies": { "@internationalized/date": "^3.8.1", - "@lucide/svelte": "^0.544.0", + "@lucide/svelte": "^0.554.0", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.5.27", "@sveltejs/vite-plugin-svelte": "^6.2.1", "@tailwindcss/typography": "^0.5.18", - "bits-ui": "^2.11.7", + "@zxcvbn-ts/core": "^3.0.4", + "@zxcvbn-ts/language-common": "^3.0.4", + "@zxcvbn-ts/language-en": "^3.0.2", + "bits-ui": "^2.14.4", "clsx": "^2.1.1", "layerchart": "2.0.0-next.27", "mode-watcher": "^1.1.0", + "runed": "^0.36.0", "svelte": "^5.39.11", "svelte-sonner": "^1.0.5", - "tailwind-merge": "^3.3.1", - "tailwind-variants": "^3.1.1", + "svelte-toolbelt": "^0.10.6", + "tailwind-merge": "^3.4.0", + "tailwind-variants": "^3.2.2", "tailwindcss": "^4.1.14", "tw-animate-css": "^1.4.0", "vaul-svelte": "1.0.0-next.7" @@ -32,7 +37,6 @@ "dependencies": { "@aarkue/tiptap-math-extension": "^1.4.0", "@floating-ui/dom": "^1.7.4", - "@lucide/svelte": "^0.546.0", "@tiptap/core": "^3.7.1", "@tiptap/extension-bubble-menu": "^3.7.1", "@tiptap/extension-code-block-lowlight": "^3.7.1", @@ -59,6 +63,6 @@ "tiptap-markdown": "^0.9.0" }, "peerDependencies": { - "bits-ui": "^2.14.1" + "bits-ui": "^2.14.4" } } diff --git a/packages/ui/src/base/button-group/button-group-separator.svelte b/packages/ui/src/base/button-group/button-group-separator.svelte new file mode 100644 index 00000000..2afa8801 --- /dev/null +++ b/packages/ui/src/base/button-group/button-group-separator.svelte @@ -0,0 +1,20 @@ + + + diff --git a/packages/ui/src/base/button-group/button-group-text.svelte b/packages/ui/src/base/button-group/button-group-text.svelte new file mode 100644 index 00000000..ed1993be --- /dev/null +++ b/packages/ui/src/base/button-group/button-group-text.svelte @@ -0,0 +1,30 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
    + {@render mergedProps.children?.()} +
    +{/if} diff --git a/packages/ui/src/base/button-group/button-group.svelte b/packages/ui/src/base/button-group/button-group.svelte new file mode 100644 index 00000000..d934ac66 --- /dev/null +++ b/packages/ui/src/base/button-group/button-group.svelte @@ -0,0 +1,46 @@ + + + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/button-group/index.ts b/packages/ui/src/base/button-group/index.ts new file mode 100644 index 00000000..7f706e3e --- /dev/null +++ b/packages/ui/src/base/button-group/index.ts @@ -0,0 +1,13 @@ +import Root from "./button-group.svelte"; +import Text from "./button-group-text.svelte"; +import Separator from "./button-group-separator.svelte"; + +export { + Root, + Text, + Separator, + // + Root as ButtonGroup, + Text as ButtonGroupText, + Separator as ButtonGroupSeparator, +}; diff --git a/packages/ui/src/base/button/button.svelte b/packages/ui/src/base/button/button.svelte index 2fadbcc9..a1714de6 100644 --- a/packages/ui/src/base/button/button.svelte +++ b/packages/ui/src/base/button/button.svelte @@ -1,21 +1,20 @@ -{#if href} - - {#if isLoading} - - {/if} - {@render children?.()} - -{:else} - -{/if} + + { + onclick?.(e); + + if (type === undefined) return; + + if (onClickPromise) { + loading = true; + + await onClickPromise(e); + + loading = false; + } + }} +> + {#if type !== undefined && loading} +
    +
    + +
    +
    + Loading + {/if} + {@render children?.()} +
    diff --git a/packages/ui/src/base/button/index.ts b/packages/ui/src/base/button/index.ts index fb585d76..5d73d59c 100644 --- a/packages/ui/src/base/button/index.ts +++ b/packages/ui/src/base/button/index.ts @@ -1,17 +1,27 @@ +/* + Installed from @ieedan/shadcn-svelte-extras +*/ + import Root, { - type ButtonProps, - type ButtonSize, - type ButtonVariant, - buttonVariants, -} from "./button.svelte"; + type ButtonProps, + type ButtonSize, + type ButtonVariant, + type AnchorElementProps, + type ButtonElementProps, + type ButtonPropsWithoutHTML, + buttonVariants +} from './button.svelte'; export { - Root, - type ButtonProps as Props, - // - Root as Button, - buttonVariants, - type ButtonProps, - type ButtonSize, - type ButtonVariant, + Root, + type ButtonProps as Props, + // + Root as Button, + buttonVariants, + type ButtonProps, + type ButtonSize, + type ButtonVariant, + type AnchorElementProps, + type ButtonElementProps, + type ButtonPropsWithoutHTML }; diff --git a/packages/ui/src/base/card/card-action.svelte b/packages/ui/src/base/card/card-action.svelte new file mode 100644 index 00000000..9b31bef4 --- /dev/null +++ b/packages/ui/src/base/card/card-action.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/card/card-content.svelte b/packages/ui/src/base/card/card-content.svelte new file mode 100644 index 00000000..b429cbcc --- /dev/null +++ b/packages/ui/src/base/card/card-content.svelte @@ -0,0 +1,15 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/card/card-description.svelte b/packages/ui/src/base/card/card-description.svelte new file mode 100644 index 00000000..a1f6da12 --- /dev/null +++ b/packages/ui/src/base/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

    + {@render children?.()} +

    diff --git a/packages/ui/src/base/card/card-footer.svelte b/packages/ui/src/base/card/card-footer.svelte new file mode 100644 index 00000000..edba0b91 --- /dev/null +++ b/packages/ui/src/base/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/card/card-header.svelte b/packages/ui/src/base/card/card-header.svelte new file mode 100644 index 00000000..487fb963 --- /dev/null +++ b/packages/ui/src/base/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/card/card-title.svelte b/packages/ui/src/base/card/card-title.svelte new file mode 100644 index 00000000..3b0c94f2 --- /dev/null +++ b/packages/ui/src/base/card/card-title.svelte @@ -0,0 +1,15 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/card/card.svelte b/packages/ui/src/base/card/card.svelte new file mode 100644 index 00000000..7133b590 --- /dev/null +++ b/packages/ui/src/base/card/card.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/card/index.ts b/packages/ui/src/base/card/index.ts new file mode 100644 index 00000000..4d3fce48 --- /dev/null +++ b/packages/ui/src/base/card/index.ts @@ -0,0 +1,25 @@ +import Root from "./card.svelte"; +import Content from "./card-content.svelte"; +import Description from "./card-description.svelte"; +import Footer from "./card-footer.svelte"; +import Header from "./card-header.svelte"; +import Title from "./card-title.svelte"; +import Action from "./card-action.svelte"; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + Action, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, + Action as CardAction, +}; diff --git a/packages/ui/src/base/copy-button/copy-button.svelte b/packages/ui/src/base/copy-button/copy-button.svelte new file mode 100644 index 00000000..57d93772 --- /dev/null +++ b/packages/ui/src/base/copy-button/copy-button.svelte @@ -0,0 +1,74 @@ + + + + + diff --git a/packages/ui/src/base/copy-button/index.ts b/packages/ui/src/base/copy-button/index.ts new file mode 100644 index 00000000..06ff7998 --- /dev/null +++ b/packages/ui/src/base/copy-button/index.ts @@ -0,0 +1,7 @@ +/* + Installed from @ieedan/shadcn-svelte-extras +*/ + +import CopyButton from './copy-button.svelte'; + +export { CopyButton }; diff --git a/packages/ui/src/base/copy-button/types.ts b/packages/ui/src/base/copy-button/types.ts new file mode 100644 index 00000000..6533d428 --- /dev/null +++ b/packages/ui/src/base/copy-button/types.ts @@ -0,0 +1,22 @@ +/* + Installed from @ieedan/shadcn-svelte-extras +*/ + +import type { WithChildren, WithoutChildren } from 'bits-ui'; + +import type { ButtonPropsWithoutHTML } from '../button'; +import type { HTMLAttributes } from 'svelte/elements'; +import type { Snippet } from 'svelte'; +import type { UseClipboard } from '../../hooks/use-clipboard.svelte'; + +export type CopyButtonPropsWithoutHTML = WithChildren< + Pick & { + ref?: HTMLButtonElement | null; + text: string; + icon?: Snippet<[]>; + animationDuration?: number; + onCopy?: (status: UseClipboard['status']) => void; + } +>; + +export type CopyButtonProps = CopyButtonPropsWithoutHTML & WithoutChildren>; diff --git a/packages/ui/src/base/dark-mode/dark-mode.svelte b/packages/ui/src/base/dark-mode/dark-mode.svelte index a26b807a..78b26be9 100644 --- a/packages/ui/src/base/dark-mode/dark-mode.svelte +++ b/packages/ui/src/base/dark-mode/dark-mode.svelte @@ -3,8 +3,8 @@ import MoonIcon from '@lucide/svelte/icons/moon'; import { resetMode, setMode } from 'mode-watcher'; - import * as DropdownMenu from '$src/base/dropdown-menu'; - import { buttonVariants } from '$src/base/button'; + import * as DropdownMenu from '../dropdown-menu'; + import { buttonVariants } from '../button'; diff --git a/packages/ui/src/base/dialog/dialog-content.svelte b/packages/ui/src/base/dialog/dialog-content.svelte index b485ad69..42d2793b 100644 --- a/packages/ui/src/base/dialog/dialog-content.svelte +++ b/packages/ui/src/base/dialog/dialog-content.svelte @@ -33,7 +33,7 @@ {@render children?.()} {#if showCloseButton} Close diff --git a/packages/ui/src/base/dialog/dialog-header.svelte b/packages/ui/src/base/dialog/dialog-header.svelte index 04b530bf..40e189ab 100644 --- a/packages/ui/src/base/dialog/dialog-header.svelte +++ b/packages/ui/src/base/dialog/dialog-header.svelte @@ -13,7 +13,7 @@
    {@render children?.()} diff --git a/packages/ui/src/base/empty/empty-content.svelte b/packages/ui/src/base/empty/empty-content.svelte new file mode 100644 index 00000000..775635ac --- /dev/null +++ b/packages/ui/src/base/empty/empty-content.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/empty/empty-description.svelte b/packages/ui/src/base/empty/empty-description.svelte new file mode 100644 index 00000000..75c90fb9 --- /dev/null +++ b/packages/ui/src/base/empty/empty-description.svelte @@ -0,0 +1,23 @@ + + +
    a:hover]:text-primary ui:text-sm/relaxed ui:[&>a]:underline ui:[&>a]:underline-offset-4', + className + )} + {...restProps} +> + {@render children?.()} +
    diff --git a/packages/ui/src/base/empty/empty-header.svelte b/packages/ui/src/base/empty/empty-header.svelte new file mode 100644 index 00000000..db2c92a0 --- /dev/null +++ b/packages/ui/src/base/empty/empty-header.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/empty/empty-media.svelte b/packages/ui/src/base/empty/empty-media.svelte new file mode 100644 index 00000000..dcf1e85f --- /dev/null +++ b/packages/ui/src/base/empty/empty-media.svelte @@ -0,0 +1,41 @@ + + + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/empty/empty-title.svelte b/packages/ui/src/base/empty/empty-title.svelte new file mode 100644 index 00000000..bac20569 --- /dev/null +++ b/packages/ui/src/base/empty/empty-title.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/empty/empty.svelte b/packages/ui/src/base/empty/empty.svelte new file mode 100644 index 00000000..1112b4f0 --- /dev/null +++ b/packages/ui/src/base/empty/empty.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/empty/index.ts b/packages/ui/src/base/empty/index.ts new file mode 100644 index 00000000..ae4c106e --- /dev/null +++ b/packages/ui/src/base/empty/index.ts @@ -0,0 +1,22 @@ +import Root from "./empty.svelte"; +import Header from "./empty-header.svelte"; +import Media from "./empty-media.svelte"; +import Title from "./empty-title.svelte"; +import Description from "./empty-description.svelte"; +import Content from "./empty-content.svelte"; + +export { + Root, + Header, + Media, + Title, + Description, + Content, + // + Root as Empty, + Header as EmptyHeader, + Media as EmptyMedia, + Title as EmptyTitle, + Description as EmptyDescription, + Content as EmptyContent, +}; diff --git a/packages/ui/src/base/field/field-content.svelte b/packages/ui/src/base/field/field-content.svelte new file mode 100644 index 00000000..148c1849 --- /dev/null +++ b/packages/ui/src/base/field/field-content.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/field/field-description.svelte b/packages/ui/src/base/field/field-description.svelte new file mode 100644 index 00000000..fff84b8b --- /dev/null +++ b/packages/ui/src/base/field/field-description.svelte @@ -0,0 +1,25 @@ + + +

    a:hover]:text-primary ui:[&>a]:underline ui:[&>a]:underline-offset-4', + className + )} + {...restProps} +> + {@render children?.()} +

    diff --git a/packages/ui/src/base/field/field-error.svelte b/packages/ui/src/base/field/field-error.svelte new file mode 100644 index 00000000..8d9626f7 --- /dev/null +++ b/packages/ui/src/base/field/field-error.svelte @@ -0,0 +1,58 @@ + + +{#if hasContent} + +{/if} diff --git a/packages/ui/src/base/field/field-group.svelte b/packages/ui/src/base/field/field-group.svelte new file mode 100644 index 00000000..cd1bcd21 --- /dev/null +++ b/packages/ui/src/base/field/field-group.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/field/field-label.svelte b/packages/ui/src/base/field/field-label.svelte new file mode 100644 index 00000000..50f20e23 --- /dev/null +++ b/packages/ui/src/base/field/field-label.svelte @@ -0,0 +1,21 @@ + + + diff --git a/packages/ui/src/base/field/field-legend.svelte b/packages/ui/src/base/field/field-legend.svelte new file mode 100644 index 00000000..0d8fe475 --- /dev/null +++ b/packages/ui/src/base/field/field-legend.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + diff --git a/packages/ui/src/base/field/field-separator.svelte b/packages/ui/src/base/field/field-separator.svelte new file mode 100644 index 00000000..8c4d08cf --- /dev/null +++ b/packages/ui/src/base/field/field-separator.svelte @@ -0,0 +1,35 @@ + + +
    + + {#if children} + + {@render children()} + + {/if} +
    diff --git a/packages/ui/src/base/field/field-set.svelte b/packages/ui/src/base/field/field-set.svelte new file mode 100644 index 00000000..a9ca81d2 --- /dev/null +++ b/packages/ui/src/base/field/field-set.svelte @@ -0,0 +1,24 @@ + + +
    [data-slot=checkbox-group]]:gap-3 ui:has-[>[data-slot=radio-group]]:gap-3', + className + )} + {...restProps} +> + {@render children?.()} +
    diff --git a/packages/ui/src/base/field/field-title.svelte b/packages/ui/src/base/field/field-title.svelte new file mode 100644 index 00000000..004dd117 --- /dev/null +++ b/packages/ui/src/base/field/field-title.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/field/field.svelte b/packages/ui/src/base/field/field.svelte new file mode 100644 index 00000000..e6dae829 --- /dev/null +++ b/packages/ui/src/base/field/field.svelte @@ -0,0 +1,53 @@ + + + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/field/index.ts b/packages/ui/src/base/field/index.ts new file mode 100644 index 00000000..ed3eb10a --- /dev/null +++ b/packages/ui/src/base/field/index.ts @@ -0,0 +1,33 @@ +import Content from './field-content.svelte'; +import Description from './field-description.svelte'; +import Error from './field-error.svelte'; +import Field from './field.svelte'; +import Group from './field-group.svelte'; +import Label from './field-label.svelte'; +import Legend from './field-legend.svelte'; +import Separator from './field-separator.svelte'; +import Set from './field-set.svelte'; +import Title from './field-title.svelte'; + +export { + Field, + Set, + Legend, + Group, + Content, + Label, + Title, + Description, + Separator, + Error, + // + Set as FieldSet, + Legend as FieldLegend, + Group as FieldGroup, + Content as FieldContent, + Label as FieldLabel, + Title as FieldTitle, + Description as FieldDescription, + Separator as FieldSeparator, + Error as FieldError +}; diff --git a/packages/ui/src/base/hover-card/hover-card-content.svelte b/packages/ui/src/base/hover-card/hover-card-content.svelte new file mode 100644 index 00000000..b53cb004 --- /dev/null +++ b/packages/ui/src/base/hover-card/hover-card-content.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/packages/ui/src/base/hover-card/hover-card-trigger.svelte b/packages/ui/src/base/hover-card/hover-card-trigger.svelte new file mode 100644 index 00000000..717571c9 --- /dev/null +++ b/packages/ui/src/base/hover-card/hover-card-trigger.svelte @@ -0,0 +1,13 @@ + + + diff --git a/packages/ui/src/base/hover-card/index.ts b/packages/ui/src/base/hover-card/index.ts new file mode 100644 index 00000000..85f3949f --- /dev/null +++ b/packages/ui/src/base/hover-card/index.ts @@ -0,0 +1,14 @@ +import { LinkPreview as HoverCardPrimitive } from "bits-ui"; +import Content from "./hover-card-content.svelte"; +import Trigger from "./hover-card-trigger.svelte"; + +const Root = HoverCardPrimitive.Root; + +export { + Root, + Content, + Trigger, + Root as HoverCard, + Content as HoverCardContent, + Trigger as HoverCardTrigger, +}; diff --git a/packages/ui/src/base/input-group/input-group-button.svelte b/packages/ui/src/base/input-group/input-group-button.svelte index e2a512d2..d2773f67 100644 --- a/packages/ui/src/base/input-group/input-group-button.svelte +++ b/packages/ui/src/base/input-group/input-group-button.svelte @@ -22,7 +22,7 @@ + +
    + {@render children?.()} +
    + diff --git a/packages/ui/src/base/page/page-body-header.svelte b/packages/ui/src/base/page/page-body-header.svelte new file mode 100644 index 00000000..6c302f4e --- /dev/null +++ b/packages/ui/src/base/page/page-body-header.svelte @@ -0,0 +1,33 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/page/page-body.svelte b/packages/ui/src/base/page/page-body.svelte new file mode 100644 index 00000000..fed505e1 --- /dev/null +++ b/packages/ui/src/base/page/page-body.svelte @@ -0,0 +1,25 @@ + + +
    + {#if child} + {@render child()} + {/if} +
    diff --git a/packages/ui/src/base/page/page-header-content.svelte b/packages/ui/src/base/page/page-header-content.svelte new file mode 100644 index 00000000..4c735b13 --- /dev/null +++ b/packages/ui/src/base/page/page-header-content.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/page/page-header.svelte b/packages/ui/src/base/page/page-header.svelte new file mode 100644 index 00000000..24a20cda --- /dev/null +++ b/packages/ui/src/base/page/page-header.svelte @@ -0,0 +1,17 @@ + + +
    +
    + {@render children?.()} +
    +
    diff --git a/packages/ui/src/base/page/page-subtitle.svelte b/packages/ui/src/base/page/page-subtitle.svelte new file mode 100644 index 00000000..3de241ef --- /dev/null +++ b/packages/ui/src/base/page/page-subtitle.svelte @@ -0,0 +1,21 @@ + + +

    + {@render children?.()} +

    + diff --git a/packages/ui/src/base/page/page-title.svelte b/packages/ui/src/base/page/page-title.svelte new file mode 100644 index 00000000..d98491f1 --- /dev/null +++ b/packages/ui/src/base/page/page-title.svelte @@ -0,0 +1,20 @@ + + +

    + {@render children?.()} +

    diff --git a/packages/ui/src/base/page/page.svelte b/packages/ui/src/base/page/page.svelte new file mode 100644 index 00000000..ae3667f9 --- /dev/null +++ b/packages/ui/src/base/page/page.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/pagination/pagination-link.svelte b/packages/ui/src/base/pagination/pagination-link.svelte index c7f82f8c..8f69c799 100644 --- a/packages/ui/src/base/pagination/pagination-link.svelte +++ b/packages/ui/src/base/pagination/pagination-link.svelte @@ -1,7 +1,7 @@ + + diff --git a/packages/ui/src/base/password/password-input.svelte b/packages/ui/src/base/password/password-input.svelte new file mode 100644 index 00000000..6632c5ea --- /dev/null +++ b/packages/ui/src/base/password/password-input.svelte @@ -0,0 +1,45 @@ + + +
    + + {@render children?.()} +
    diff --git a/packages/ui/src/base/password/password-strength.svelte b/packages/ui/src/base/password/password-strength.svelte new file mode 100644 index 00000000..c82fd84d --- /dev/null +++ b/packages/ui/src/base/password/password-strength.svelte @@ -0,0 +1,48 @@ + + + +
    + +
    + {#each Array.from({ length: 4 }) as _, i (i)} +
    + {/each} +
    +
    diff --git a/packages/ui/src/base/password/password-toggle-visibility.svelte b/packages/ui/src/base/password/password-toggle-visibility.svelte new file mode 100644 index 00000000..169c3a37 --- /dev/null +++ b/packages/ui/src/base/password/password-toggle-visibility.svelte @@ -0,0 +1,32 @@ + + + + {#if state.root.opts.hidden.current} + + {:else} + + {/if} + diff --git a/packages/ui/src/base/password/password-util.svelte.ts b/packages/ui/src/base/password/password-util.svelte.ts new file mode 100644 index 00000000..72b6def9 --- /dev/null +++ b/packages/ui/src/base/password/password-util.svelte.ts @@ -0,0 +1,157 @@ +import { Context, watch } from 'runed'; +import type { ReadableBoxedValues, WritableBoxedValues } from 'svelte-toolbelt'; +import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core'; +import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common'; +import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en'; + +const passwordOptions = { + translations: zxcvbnEnPackage.translations, + graphs: zxcvbnCommonPackage.adjacencyGraphs, + dictionary: { + ...zxcvbnCommonPackage.dictionary, + ...zxcvbnEnPackage.dictionary + } +}; + +zxcvbnOptions.setOptions(passwordOptions); + +type PasswordRootStateProps = WritableBoxedValues<{ + hidden: boolean; +}> & + ReadableBoxedValues<{ + minScore: number; + }>; + +type PasswordState = { + value: string; + copyMounted: boolean; + toggleMounted: boolean; + strengthMounted: boolean; + tainted: boolean; +}; + +const defaultPasswordState: PasswordState = { + value: '', + copyMounted: false, + toggleMounted: false, + strengthMounted: false, + tainted: false +}; + +class PasswordRootState { + passwordState = $state(defaultPasswordState); + + constructor(readonly opts: PasswordRootStateProps) {} + + // only re-run when the password changes + strength = $derived.by(() => zxcvbn(this.passwordState.value)); +} + +type PasswordInputStateProps = WritableBoxedValues<{ + value: string; +}> & + ReadableBoxedValues<{ + ref: HTMLInputElement | null; + }>; + +class PasswordInputState { + constructor( + readonly root: PasswordRootState, + readonly opts: PasswordInputStateProps + ) { + watch( + () => this.opts.value.current, + () => { + if (this.root.passwordState.value !== this.opts.value.current) { + this.root.passwordState.tainted = true; + this.root.passwordState.value = this.opts.value.current; + } + } + ); + + $effect(() => { + if (!this.root.passwordState.strengthMounted) return; + + // if the password is empty, we let the `required` attribute handle the validation + if ( + this.root.passwordState.value !== '' && + this.root.strength.score < this.root.opts.minScore.current + ) { + this.opts.ref.current?.setCustomValidity('Password is too weak'); + } else { + this.opts.ref.current?.setCustomValidity(''); + } + }); + } + + props = $derived.by(() => ({ + 'aria-invalid': + this.root.strength.score < this.root.opts.minScore.current && + this.root.passwordState.tainted && + this.root.passwordState.strengthMounted + })); +} + +class PasswordToggleVisibilityState { + constructor(readonly root: PasswordRootState) { + this.root.passwordState.toggleMounted = true; + + // this way we go back to the correct padding when toggle is unmounted + $effect(() => { + return () => { + this.root.passwordState.toggleMounted = false; + }; + }); + } +} + +class PasswordCopyState { + constructor(readonly root: PasswordRootState) { + this.root.passwordState.copyMounted = true; + + // this way we go back to the correct padding when copy is unmounted + $effect(() => { + return () => { + this.root.passwordState.copyMounted = false; + }; + }); + } +} + +class PasswordStrengthState { + constructor(readonly root: PasswordRootState) { + this.root.passwordState.strengthMounted = true; + + $effect(() => { + return () => { + this.root.passwordState.strengthMounted = false; + }; + }); + } + + get strength() { + return this.root.strength; + } +} + +const ctx = new Context('password-root-state'); + +export function usePassword(props: PasswordRootStateProps) { + return ctx.set(new PasswordRootState(props)); +} + +export function usePasswordInput(props: PasswordInputStateProps) { + return new PasswordInputState(ctx.get(), props); +} + +export function usePasswordToggleVisibility() { + return new PasswordToggleVisibilityState(ctx.get()); +} + +export function usePasswordCopy() { + return new PasswordCopyState(ctx.get()); +} + +export function usePasswordStrength() { + return new PasswordStrengthState(ctx.get()); +} diff --git a/packages/ui/src/base/password/password.svelte b/packages/ui/src/base/password/password.svelte new file mode 100644 index 00000000..c27f909c --- /dev/null +++ b/packages/ui/src/base/password/password.svelte @@ -0,0 +1,26 @@ + + +
    + {@render children?.()} +
    diff --git a/packages/ui/src/base/password/types.ts b/packages/ui/src/base/password/types.ts new file mode 100644 index 00000000..11729048 --- /dev/null +++ b/packages/ui/src/base/password/types.ts @@ -0,0 +1,47 @@ +import type { + WithChildren, + WithoutChildren, + Meter as MeterPrimitive, + Toggle as TogglePrimitive +} from 'bits-ui'; +import type { HTMLAttributes, HTMLInputAttributes } from 'svelte/elements'; +import type { CopyButtonProps } from '../copy-button/types'; +import type { ZxcvbnResult } from '@zxcvbn-ts/core'; + +export type PasswordRootPropsWithoutHTML = WithChildren<{ + ref?: HTMLDivElement | null; + hidden?: boolean; + /** The minimum acceptable score for a password. (0-4) + * + * @default 3 + */ + minScore?: 0 | 1 | 2 | 3 | 4; +}>; + +export type PasswordRootProps = WithoutChildren> & + PasswordRootPropsWithoutHTML; + +export type PasswordInputPropsWithoutHTML = WithChildren<{ + ref?: HTMLInputElement | null; + value?: string; +}>; + +export type PasswordInputProps = Omit< + WithoutChildren, + 'type' | 'files' | 'aria-invalid' | 'value' +> & + PasswordInputPropsWithoutHTML; + +export type PasswordToggleVisibilityProps = Omit< + TogglePrimitive.RootProps, + 'children' | 'pressed' | 'aria-label' | 'tabindex' +>; + +export type PasswordCopyButtonProps = Omit; + +export type PasswordStrengthPropsWithoutHTML = { + strength?: ZxcvbnResult; +}; + +export type PasswordStrengthProps = PasswordStrengthPropsWithoutHTML & + WithoutChildren; diff --git a/packages/ui/src/base/select/index.ts b/packages/ui/src/base/select/index.ts index 42de6f5c..d79ca077 100644 --- a/packages/ui/src/base/select/index.ts +++ b/packages/ui/src/base/select/index.ts @@ -1,14 +1,13 @@ -import { Select as SelectPrimitive } from 'bits-ui'; - -import Group from './select-group.svelte'; -import Label from './select-label.svelte'; -import Item from './select-item.svelte'; import Content from './select-content.svelte'; -import Trigger from './select-trigger.svelte'; -import Separator from './select-separator.svelte'; +import Group from './select-group.svelte'; +import GroupHeading from './select-group-heading.svelte'; +import Item from './select-item.svelte'; +import Label from './select-label.svelte'; import ScrollDownButton from './select-scroll-down-button.svelte'; import ScrollUpButton from './select-scroll-up-button.svelte'; -import GroupHeading from './select-group-heading.svelte'; +import { Select as SelectPrimitive } from 'bits-ui'; +import Separator from './select-separator.svelte'; +import Trigger from './select-trigger.svelte'; const Root = SelectPrimitive.Root; diff --git a/packages/ui/src/base/sidebar/sidebar-input.svelte b/packages/ui/src/base/sidebar/sidebar-input.svelte index 7ba1fbc1..10f3cb4c 100644 --- a/packages/ui/src/base/sidebar/sidebar-input.svelte +++ b/packages/ui/src/base/sidebar/sidebar-input.svelte @@ -1,6 +1,6 @@ - + diff --git a/packages/ui/src/base/toggle/index.ts b/packages/ui/src/base/toggle/index.ts new file mode 100644 index 00000000..0f18d3be --- /dev/null +++ b/packages/ui/src/base/toggle/index.ts @@ -0,0 +1,13 @@ +import Root from './toggle.svelte'; +export { + toggleVariants, + type ToggleSize, + type ToggleVariant, + type ToggleVariants +} from './toggle.svelte'; + +export { + Root, + // + Root as Toggle +}; diff --git a/packages/ui/src/base/toggle/toggle.svelte b/packages/ui/src/base/toggle/toggle.svelte new file mode 100644 index 00000000..871e0d39 --- /dev/null +++ b/packages/ui/src/base/toggle/toggle.svelte @@ -0,0 +1,52 @@ + + + + + diff --git a/packages/ui/src/custom/domain-input/domain-input.svelte b/packages/ui/src/custom/domain-input/domain-input.svelte new file mode 100644 index 00000000..70a259d5 --- /dev/null +++ b/packages/ui/src/custom/domain-input/domain-input.svelte @@ -0,0 +1,34 @@ + + + + + {prefix} + + + + {suffix} + + diff --git a/packages/ui/src/custom/domain-input/index.ts b/packages/ui/src/custom/domain-input/index.ts new file mode 100644 index 00000000..af1bbf9c --- /dev/null +++ b/packages/ui/src/custom/domain-input/index.ts @@ -0,0 +1,5 @@ +import DomainInput from './domain-input.svelte'; + +export { DomainInput }; +export { DomainInput as default }; + diff --git a/packages/ui/src/custom/editor/editor.svelte b/packages/ui/src/custom/editor/editor.svelte index d0500190..66126a35 100644 --- a/packages/ui/src/custom/editor/editor.svelte +++ b/packages/ui/src/custom/editor/editor.svelte @@ -1,5 +1,5 @@ + + + + {#if IconComponent} + + {@const Icon = IconComponent} + + + {/if} + {#if title} + {title} + {/if} + {#if description} + {description} + {/if} + + {#if children} + + {@render children()} + + {/if} + diff --git a/packages/ui/src/custom/empty/index.ts b/packages/ui/src/custom/empty/index.ts new file mode 100644 index 00000000..ca6c048a --- /dev/null +++ b/packages/ui/src/custom/empty/index.ts @@ -0,0 +1,4 @@ +import Empty from './empty.svelte'; + +export { Empty }; +export { Empty as default }; diff --git a/packages/ui/src/custom/password/index.ts b/packages/ui/src/custom/password/index.ts new file mode 100644 index 00000000..e05a9461 --- /dev/null +++ b/packages/ui/src/custom/password/index.ts @@ -0,0 +1,5 @@ +import Password from './password.svelte'; + +export { Password }; +export { Password as default }; + diff --git a/packages/ui/src/custom/password/password.svelte b/packages/ui/src/custom/password/password.svelte new file mode 100644 index 00000000..3f5164ae --- /dev/null +++ b/packages/ui/src/custom/password/password.svelte @@ -0,0 +1,80 @@ + + + + + + + + + + + + {#snippet child({ props })} + (showPassword = !showPassword)} + aria-label={ariaLabel} + {disabled} + > + {#if showPassword} + + {:else} + + {/if} + + {/snippet} + + {tooltipText} + + + + + diff --git a/packages/ui/src/custom/search/index.ts b/packages/ui/src/custom/search/index.ts new file mode 100644 index 00000000..e2f0e1da --- /dev/null +++ b/packages/ui/src/custom/search/index.ts @@ -0,0 +1,5 @@ +import Search from './search.svelte'; + +export { Search }; +export { Search as default }; + diff --git a/packages/ui/src/custom/search/search.svelte b/packages/ui/src/custom/search/search.svelte new file mode 100644 index 00000000..a18d2eb3 --- /dev/null +++ b/packages/ui/src/custom/search/search.svelte @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/packages/ui/src/hooks/is-mobile.svelte.ts b/packages/ui/src/hooks/is-mobile.svelte.ts index e3802d38..42a32e1c 100644 --- a/packages/ui/src/hooks/is-mobile.svelte.ts +++ b/packages/ui/src/hooks/is-mobile.svelte.ts @@ -7,3 +7,5 @@ export class IsMobile extends MediaQuery { super(`max-width: ${breakpoint - 1}px`); } } + +export const isMobileStore = new IsMobile(); diff --git a/packages/ui/src/hooks/use-clipboard.svelte.ts b/packages/ui/src/hooks/use-clipboard.svelte.ts new file mode 100644 index 00000000..0d33df7b --- /dev/null +++ b/packages/ui/src/hooks/use-clipboard.svelte.ts @@ -0,0 +1,83 @@ +type Options = { + /** The time before the copied status is reset. */ + delay: number; +}; + +/** Use this hook to copy text to the clipboard and show a copied state. + * + * ## Usage + * ```svelte + * + * + * + * ``` + * + */ +export class UseClipboard { + #copiedStatus = $state<'success' | 'failure'>(); + private delay: number; + private timeout: ReturnType | undefined = undefined; + + constructor({ delay = 500 }: Partial = {}) { + this.delay = delay; + } + + /** Copies the given text to the users clipboard. + * + * ## Usage + * ```ts + * clipboard.copy('Hello, World!'); + * ``` + * + * @param text + * @returns + */ + async copy(text: string) { + if (this.timeout) { + this.#copiedStatus = undefined; + clearTimeout(this.timeout); + } + + try { + await navigator.clipboard.writeText(text); + + this.#copiedStatus = 'success'; + + this.timeout = setTimeout(() => { + this.#copiedStatus = undefined; + }, this.delay); + } catch { + // an error can occur when not in the browser or if the user hasn't given clipboard access + this.#copiedStatus = 'failure'; + + this.timeout = setTimeout(() => { + this.#copiedStatus = undefined; + }, this.delay); + } + + return this.#copiedStatus; + } + + /** true when the user has just copied to the clipboard. */ + get copied() { + return this.#copiedStatus === 'success'; + } + + /** Indicates whether a copy has occurred + * and gives a status of either `success` or `failure`. */ + get status() { + return this.#copiedStatus; + } +} diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css index dbde20b9..b2abf609 100644 --- a/packages/ui/src/index.css +++ b/packages/ui/src/index.css @@ -1,5 +1,3 @@ -@import url('https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap'); - @import 'tailwindcss' prefix(ui); @import 'tw-animate-css'; @@ -134,4 +132,10 @@ ::file-selector-button { @apply ui:font-cio; } + + .toaster { + --normal-bg: var(--color-popover); + --normal-text: var(--color-popover-foreground); + --normal-border: var(--color-border); + } } diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 7b9230ca..4104eb9a 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -5,19 +5,28 @@ export * as Avatar from './base/avatar'; export * as Badge from './base/badge'; export * as Breadcrumb from './base/breadcrumb'; export * as Button from './base/button'; +export * as ButtonGroup from './base/button-group'; +export * as Card from './base/card'; +export * as Chart from './base/chart'; export * as Checkbox from './base/checkbox'; export * as Collapsible from './base/collapsible'; export * as Command from './base/command'; +export * as CopyButton from './base/copy-button'; export * as Dialog from './base/dialog'; export * as Drawer from './base/drawer'; export * as DropdownMenu from './base/dropdown-menu'; +export * as Empty from './base/empty'; +export * as Field from './base/field'; +export * as HoverCard from './base/hover-card'; export * as Input from './base/input'; export * as InputGroup from './base/input-group'; export * as Item from './base/item'; export * as Kbd from './base/kbd'; export * as Label from './base/label'; export * as Menubar from './base/menubar'; +export * as Page from './base/page'; export * as Pagination from './base/pagination'; +export * as Password from './base/password'; export * as Popover from './base/popover'; export * as Progress from './base/progress'; export * as RadioGroup from './base/radio-group'; @@ -26,11 +35,13 @@ export * as Separator from './base/separator'; export * as Sheet from './base/sheet'; export * as Sidebar from './base/sidebar'; export * as Skeleton from './base/skeleton'; +export * as Sonner from './base/sonner'; export * as Spinner from './base/spinner'; export * as Switch from './base/switch'; export * as Table from './base/table'; export * as Tabs from './base/tabs'; export * as Textarea from './base/textarea'; +export * as Toggle from './base/toggle'; export * as Tooltip from './base/tooltip'; export { Toaster } from './base/sonner'; diff --git a/packages/ui/src/output.css b/packages/ui/src/output.css index f3d3cafb..d619ed6b 100644 --- a/packages/ui/src/output.css +++ b/packages/ui/src/output.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */ -@import "https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap";@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:"";--tw-outline-style:solid;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--ui-font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--ui-font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--ui-color-green-500:oklch(72.3% .219 149.579);--ui-color-black:#000;--ui-color-white:#fff;--ui-spacing:.25rem;--ui-container-sm:24rem;--ui-container-lg:32rem;--ui-text-xs:.75rem;--ui-text-xs--line-height:calc(1/.75);--ui-text-sm:.875rem;--ui-text-sm--line-height:calc(1.25/.875);--ui-text-base:1rem;--ui-text-base--line-height:calc(1.5/1);--ui-text-lg:1.125rem;--ui-text-lg--line-height:calc(1.75/1.125);--ui-font-weight-normal:400;--ui-font-weight-medium:500;--ui-font-weight-semibold:600;--ui-tracking-widest:.1em;--ui-leading-snug:1.375;--ui-leading-normal:1.5;--ui-radius-xs:.125rem;--ui-ease-in-out:cubic-bezier(.4,0,.2,1);--ui-animate-spin:spin 1s linear infinite;--ui-animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--ui-blur-sm:8px;--ui-default-transition-duration:.15s;--ui-default-transition-timing-function:cubic-bezier(.4,0,.2,1);--ui-default-mono-font-family:var(--ui-font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--font-cio,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--ui-default-font-feature-settings,normal);font-variation-settings:var(--ui-default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--ui-default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--ui-default-mono-font-feature-settings,normal);font-variation-settings:var(--ui-default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--border);outline-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){*{outline-color:color-mix(in oklab,var(--ring)50%,transparent)}}body{background-color:var(--background);font-family:var(--font-sans);color:var(--foreground)}button,input,select,optgroup,textarea{font-family:var(--font-sans)}::file-selector-button{font-family:var(--font-sans)}}@layer components;@layer utilities{.ui\:pointer-events-none{pointer-events:none}.ui\:absolute{position:absolute}.ui\:fixed{position:fixed}.ui\:relative{position:relative}.ui\:inset-0{inset:calc(var(--ui-spacing)*0)}.ui\:inset-x-0{inset-inline:calc(var(--ui-spacing)*0)}.ui\:inset-y-0{inset-block:calc(var(--ui-spacing)*0)}.ui\:-top-2{top:calc(var(--ui-spacing)*-2)}.ui\:top-0{top:calc(var(--ui-spacing)*0)}.ui\:top-1\.5{top:calc(var(--ui-spacing)*1.5)}.ui\:top-1\/2{top:50%}.ui\:top-3\.5{top:calc(var(--ui-spacing)*3.5)}.ui\:top-4{top:calc(var(--ui-spacing)*4)}.ui\:top-\[50\%\]{top:50%}.ui\:right-0{right:calc(var(--ui-spacing)*0)}.ui\:right-1{right:calc(var(--ui-spacing)*1)}.ui\:right-2{right:calc(var(--ui-spacing)*2)}.ui\:right-3{right:calc(var(--ui-spacing)*3)}.ui\:right-4{right:calc(var(--ui-spacing)*4)}.ui\:bottom-0{bottom:calc(var(--ui-spacing)*0)}.ui\:left-0{left:calc(var(--ui-spacing)*0)}.ui\:left-1\/2{left:50%}.ui\:left-2{left:calc(var(--ui-spacing)*2)}.ui\:left-\[50\%\]{left:50%}.ui\:left-\[calc\(50\%-3rem\)\]{left:calc(50% - 3rem)}.ui\:z-10{z-index:10}.ui\:z-20{z-index:20}.ui\:z-50{z-index:50}.ui\:order-first{order:-9999}.ui\:order-last{order:9999}.ui\:m-0{margin:calc(var(--ui-spacing)*0)}.ui\:-mx-1{margin-inline:calc(var(--ui-spacing)*-1)}.ui\:mx-3\.5{margin-inline:calc(var(--ui-spacing)*3.5)}.ui\:mx-auto{margin-inline:auto}.ui\:my-0{margin-block:calc(var(--ui-spacing)*0)}.ui\:my-0\.5{margin-block:calc(var(--ui-spacing)*.5)}.ui\:my-1{margin-block:calc(var(--ui-spacing)*1)}.ui\:my-2{margin-block:calc(var(--ui-spacing)*2)}.ui\:mt-1{margin-top:calc(var(--ui-spacing)*1)}.ui\:mt-4{margin-top:calc(var(--ui-spacing)*4)}.ui\:mt-auto{margin-top:auto}.ui\:mr-1{margin-right:calc(var(--ui-spacing)*1)}.ui\:ml-auto{margin-left:auto}.ui\:line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.ui\:block{display:block}.ui\:flex{display:flex}.ui\:grid{display:grid}.ui\:hidden{display:none}.ui\:inline-flex{display:inline-flex}.ui\:field-sizing-content{field-sizing:content}.ui\:size-2{width:calc(var(--ui-spacing)*2);height:calc(var(--ui-spacing)*2)}.ui\:size-2\.5{width:calc(var(--ui-spacing)*2.5);height:calc(var(--ui-spacing)*2.5)}.ui\:size-3{width:calc(var(--ui-spacing)*3);height:calc(var(--ui-spacing)*3)}.ui\:size-3\.5{width:calc(var(--ui-spacing)*3.5);height:calc(var(--ui-spacing)*3.5)}.ui\:size-4{width:calc(var(--ui-spacing)*4);height:calc(var(--ui-spacing)*4)}.ui\:size-6{width:calc(var(--ui-spacing)*6);height:calc(var(--ui-spacing)*6)}.ui\:size-7{width:calc(var(--ui-spacing)*7);height:calc(var(--ui-spacing)*7)}.ui\:size-8{width:calc(var(--ui-spacing)*8);height:calc(var(--ui-spacing)*8)}.ui\:size-9{width:calc(var(--ui-spacing)*9);height:calc(var(--ui-spacing)*9)}.ui\:size-10{width:calc(var(--ui-spacing)*10);height:calc(var(--ui-spacing)*10)}.ui\:size-fit{width:fit-content;height:fit-content}.ui\:size-full{width:100%;height:100%}.ui\:h-\(--bits-select-anchor-height\){height:var(--bits-select-anchor-height)}.ui\:h-2{height:calc(var(--ui-spacing)*2)}.ui\:h-4{height:calc(var(--ui-spacing)*4)}.ui\:h-5{height:calc(var(--ui-spacing)*5)}.ui\:h-6{height:calc(var(--ui-spacing)*6)}.ui\:h-7{height:calc(var(--ui-spacing)*7)}.ui\:h-8{height:calc(var(--ui-spacing)*8)}.ui\:h-9{height:calc(var(--ui-spacing)*9)}.ui\:h-10{height:calc(var(--ui-spacing)*10)}.ui\:h-12{height:calc(var(--ui-spacing)*12)}.ui\:h-96{height:calc(var(--ui-spacing)*96)}.ui\:h-128{height:calc(var(--ui-spacing)*128)}.ui\:h-\[1\.2rem\]{height:1.2rem}.ui\:h-\[1\.15rem\]{height:1.15rem}.ui\:h-\[70px\]{height:70px}.ui\:h-\[calc\(100\%-1px\)\]{height:calc(100% - 1px)}.ui\:h-auto{height:auto}.ui\:h-fit{height:fit-content}.ui\:h-full{height:100%}.ui\:h-px{height:1px}.ui\:max-h-\(--bits-dropdown-menu-content-available-height\){max-height:var(--bits-dropdown-menu-content-available-height)}.ui\:max-h-\(--bits-select-content-available-height\){max-height:var(--bits-select-content-available-height)}.ui\:max-h-80{max-height:calc(var(--ui-spacing)*80)}.ui\:max-h-\[300px\]{max-height:300px}.ui\:max-h-svh{max-height:100svh}.ui\:min-h-0{min-height:calc(var(--ui-spacing)*0)}.ui\:min-h-16{min-height:calc(var(--ui-spacing)*16)}.ui\:w-\(--sidebar-width\){width:var(--sidebar-width)}.ui\:w-0{width:calc(var(--ui-spacing)*0)}.ui\:w-1{width:calc(var(--ui-spacing)*1)}.ui\:w-3\/4{width:75%}.ui\:w-4{width:calc(var(--ui-spacing)*4)}.ui\:w-5{width:calc(var(--ui-spacing)*5)}.ui\:w-8{width:calc(var(--ui-spacing)*8)}.ui\:w-40{width:calc(var(--ui-spacing)*40)}.ui\:w-72{width:calc(var(--ui-spacing)*72)}.ui\:w-\[1\.2rem\]{width:1.2rem}.ui\:w-\[25px\]{width:25px}.ui\:w-\[100px\]{width:100px}.ui\:w-fit{width:fit-content}.ui\:w-full{width:100%}.ui\:max-w-\(--skeleton-width\){max-width:var(--skeleton-width)}.ui\:max-w-80{max-width:calc(var(--ui-spacing)*80)}.ui\:max-w-\[calc\(100\%-2rem\)\]{max-width:calc(100% - 2rem)}.ui\:min-w-\(--bits-select-anchor-width\){min-width:var(--bits-select-anchor-width)}.ui\:min-w-0{min-width:calc(var(--ui-spacing)*0)}.ui\:min-w-5{min-width:calc(var(--ui-spacing)*5)}.ui\:min-w-32{min-width:calc(var(--ui-spacing)*32)}.ui\:min-w-48{min-width:calc(var(--ui-spacing)*48)}.ui\:min-w-\[9rem\]{min-width:9rem}.ui\:flex-1{flex:1}.ui\:shrink-0{flex-shrink:0}.ui\:basis-full{flex-basis:100%}.ui\:origin-\(--bits-dropdown-menu-content-transform-origin\){transform-origin:var(--bits-dropdown-menu-content-transform-origin)}.ui\:origin-\(--bits-menubar-content-transform-origin\){transform-origin:var(--bits-menubar-content-transform-origin)}.ui\:origin-\(--bits-popover-content-transform-origin\){transform-origin:var(--bits-popover-content-transform-origin)}.ui\:origin-\(--bits-select-content-transform-origin\){transform-origin:var(--bits-select-content-transform-origin)}.ui\:origin-\(--bits-tooltip-content-transform-origin\){transform-origin:var(--bits-tooltip-content-transform-origin)}.ui\:-translate-x-1\/2{--tw-translate-x:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:-translate-x-px{--tw-translate-x:-1px;translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:translate-x-\[-50\%\]{--tw-translate-x:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:translate-x-px{--tw-translate-x:1px;translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:-translate-y-1\/2{--tw-translate-y:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:translate-y-0\.5{--tw-translate-y:calc(var(--ui-spacing)*.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:translate-y-\[-50\%\]{--tw-translate-y:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:scale-0{--tw-scale-x:0%;--tw-scale-y:0%;--tw-scale-z:0%;scale:var(--tw-scale-x)var(--tw-scale-y)}.ui\:scale-100{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.ui\:rotate-0{rotate:none}.ui\:rotate-45{rotate:45deg}.ui\:rotate-90{rotate:90deg}.ui\:animate-in{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.ui\:animate-pulse{animation:var(--ui-animate-pulse)}.ui\:animate-spin{animation:var(--ui-animate-spin)}.ui\:cursor-auto{cursor:auto}.ui\:cursor-col-resize{cursor:col-resize}.ui\:cursor-default{cursor:default}.ui\:cursor-pointer{cursor:pointer}.ui\:cursor-text{cursor:text}.ui\:resize-none{resize:none}.ui\:scroll-my-1{scroll-margin-block:calc(var(--ui-spacing)*1)}.ui\:scroll-py-1{scroll-padding-block:calc(var(--ui-spacing)*1)}.ui\:flex-col{flex-direction:column}.ui\:flex-col-reverse{flex-direction:column-reverse}.ui\:flex-row{flex-direction:row}.ui\:flex-wrap{flex-wrap:wrap}.ui\:items-center{align-items:center}.ui\:items-end{align-items:flex-end}.ui\:items-start{align-items:flex-start}.ui\:items-stretch{align-items:stretch}.ui\:justify-between{justify-content:space-between}.ui\:justify-center{justify-content:center}.ui\:justify-end{justify-content:flex-end}.ui\:justify-start{justify-content:flex-start}.ui\:gap-0{gap:calc(var(--ui-spacing)*0)}.ui\:gap-0\.5{gap:calc(var(--ui-spacing)*.5)}.ui\:gap-1{gap:calc(var(--ui-spacing)*1)}.ui\:gap-1\.5{gap:calc(var(--ui-spacing)*1.5)}.ui\:gap-2{gap:calc(var(--ui-spacing)*2)}.ui\:gap-2\.5{gap:calc(var(--ui-spacing)*2.5)}.ui\:gap-3{gap:calc(var(--ui-spacing)*3)}.ui\:gap-4{gap:calc(var(--ui-spacing)*4)}.ui\:overflow-auto{overflow:auto}.ui\:overflow-hidden{overflow:hidden}.ui\:overflow-visible{overflow:visible}.ui\:overflow-x-auto{overflow-x:auto}.ui\:overflow-x-hidden{overflow-x:hidden}.ui\:overflow-y-auto{overflow-y:auto}.ui\:scroll-smooth{scroll-behavior:smooth}.ui\:rounded{border-radius:.25rem}.ui\:rounded-\[2px\]{border-radius:2px}.ui\:rounded-\[4px\]{border-radius:4px}.ui\:rounded-\[calc\(var\(--radius\)-5px\)\]{border-radius:calc(var(--radius) - 5px)}.ui\:rounded-full{border-radius:3.40282e38px}.ui\:rounded-lg{border-radius:var(--radius)}.ui\:rounded-md{border-radius:calc(var(--radius) - 2px)}.ui\:rounded-none{border-radius:0}.ui\:rounded-sm{border-radius:calc(var(--radius) - 4px)}.ui\:rounded-xl{border-radius:calc(var(--radius) + 4px)}.ui\:rounded-xs{border-radius:var(--ui-radius-xs)}.ui\:border{border-style:var(--tw-border-style);border-width:1px}.ui\:border-0{border-style:var(--tw-border-style);border-width:0}.ui\:border-2{border-style:var(--tw-border-style);border-width:2px}.ui\:border-\[1\.5px\]{border-style:var(--tw-border-style);border-width:1.5px}.ui\:border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.ui\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.ui\:border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.ui\:border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.ui\:border-dashed{--tw-border-style:dashed;border-style:dashed}.ui\:border-\(--color-border\){border-color:var(--color-border)}.ui\:border-border,.ui\:border-border\/50{border-color:var(--border)}@supports (color:color-mix(in lab, red, red)){.ui\:border-border\/50{border-color:color-mix(in oklab,var(--border)50%,transparent)}}.ui\:border-input{border-color:var(--input)}.ui\:border-sidebar-border{border-color:var(--sidebar-border)}.ui\:border-transparent{border-color:#0000}.ui\:bg-\(--color-bg\){background-color:var(--color-bg)}.ui\:bg-accent{background-color:var(--accent)}.ui\:bg-background,.ui\:bg-background\/50{background-color:var(--background)}@supports (color:color-mix(in lab, red, red)){.ui\:bg-background\/50{background-color:color-mix(in oklab,var(--background)50%,transparent)}}.ui\:bg-black\/50{background-color:var(--ui-color-black)}@supports (color:color-mix(in lab, red, red)){.ui\:bg-black\/50{background-color:color-mix(in oklab,var(--ui-color-black)50%,transparent)}}.ui\:bg-border{background-color:var(--border)}.ui\:bg-destructive{background-color:var(--destructive)}.ui\:bg-muted,.ui\:bg-muted\/50{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.ui\:bg-muted\/50{background-color:color-mix(in oklab,var(--muted)50%,transparent)}}.ui\:bg-popover{background-color:var(--popover)}.ui\:bg-primary,.ui\:bg-primary\/20{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.ui\:bg-primary\/20{background-color:color-mix(in oklab,var(--primary)20%,transparent)}}.ui\:bg-secondary,.ui\:bg-secondary\/50{background-color:var(--secondary)}@supports (color:color-mix(in lab, red, red)){.ui\:bg-secondary\/50{background-color:color-mix(in oklab,var(--secondary)50%,transparent)}}.ui\:bg-sidebar{background-color:var(--sidebar)}.ui\:bg-sidebar-border{background-color:var(--sidebar-border)}.ui\:bg-transparent{background-color:#0000}.ui\:bg-clip-padding{background-clip:padding-box}.ui\:fill-current{fill:currentColor}.ui\:fill-primary{fill:var(--primary)}.ui\:object-cover{object-fit:cover}.ui\:p-0{padding:calc(var(--ui-spacing)*0)}.ui\:p-0\.5{padding:calc(var(--ui-spacing)*.5)}.ui\:p-1{padding:calc(var(--ui-spacing)*1)}.ui\:p-2{padding:calc(var(--ui-spacing)*2)}.ui\:p-4{padding:calc(var(--ui-spacing)*4)}.ui\:p-6{padding:calc(var(--ui-spacing)*6)}.ui\:p-\[3px\]{padding:3px}.ui\:px-1{padding-inline:calc(var(--ui-spacing)*1)}.ui\:px-2{padding-inline:calc(var(--ui-spacing)*2)}.ui\:px-2\.5{padding-inline:calc(var(--ui-spacing)*2.5)}.ui\:px-3{padding-inline:calc(var(--ui-spacing)*3)}.ui\:px-4{padding-inline:calc(var(--ui-spacing)*4)}.ui\:px-6{padding-inline:calc(var(--ui-spacing)*6)}.ui\:py-0\.5{padding-block:calc(var(--ui-spacing)*.5)}.ui\:py-1{padding-block:calc(var(--ui-spacing)*1)}.ui\:py-1\.5{padding-block:calc(var(--ui-spacing)*1.5)}.ui\:py-2{padding-block:calc(var(--ui-spacing)*2)}.ui\:py-3{padding-block:calc(var(--ui-spacing)*3)}.ui\:py-4{padding-block:calc(var(--ui-spacing)*4)}.ui\:py-6{padding-block:calc(var(--ui-spacing)*6)}.ui\:ps-3{padding-inline-start:calc(var(--ui-spacing)*3)}.ui\:pe-3{padding-inline-end:calc(var(--ui-spacing)*3)}.ui\:pt-0{padding-top:calc(var(--ui-spacing)*0)}.ui\:pt-1\.5{padding-top:calc(var(--ui-spacing)*1.5)}.ui\:pt-3{padding-top:calc(var(--ui-spacing)*3)}.ui\:pr-2{padding-right:calc(var(--ui-spacing)*2)}.ui\:pr-8{padding-right:calc(var(--ui-spacing)*8)}.ui\:pb-3{padding-bottom:calc(var(--ui-spacing)*3)}.ui\:pb-4{padding-bottom:calc(var(--ui-spacing)*4)}.ui\:pl-2{padding-left:calc(var(--ui-spacing)*2)}.ui\:pl-3{padding-left:calc(var(--ui-spacing)*3)}.ui\:pl-8{padding-left:calc(var(--ui-spacing)*8)}.ui\:text-center{text-align:center}.ui\:text-left{text-align:left}.ui\:font-mono{font-family:var(--ui-font-mono)}.ui\:font-sans{font-family:var(--ui-font-sans)}.ui\:text-base{font-size:var(--ui-text-base);line-height:var(--tw-leading,var(--ui-text-base--line-height))}.ui\:text-lg{font-size:var(--ui-text-lg);line-height:var(--tw-leading,var(--ui-text-lg--line-height))}.ui\:text-sm{font-size:var(--ui-text-sm);line-height:var(--tw-leading,var(--ui-text-sm--line-height))}.ui\:text-xs{font-size:var(--ui-text-xs);line-height:var(--tw-leading,var(--ui-text-xs--line-height))}.ui\:leading-none{--tw-leading:1;line-height:1}.ui\:leading-normal{--tw-leading:var(--ui-leading-normal);line-height:var(--ui-leading-normal)}.ui\:leading-snug{--tw-leading:var(--ui-leading-snug);line-height:var(--ui-leading-snug)}.ui\:font-medium{--tw-font-weight:var(--ui-font-weight-medium);font-weight:var(--ui-font-weight-medium)}.ui\:font-normal{--tw-font-weight:var(--ui-font-weight-normal);font-weight:var(--ui-font-weight-normal)}.ui\:font-semibold{--tw-font-weight:var(--ui-font-weight-semibold);font-weight:var(--ui-font-weight-semibold)}.ui\:tracking-widest{--tw-tracking:var(--ui-tracking-widest);letter-spacing:var(--ui-tracking-widest)}.ui\:text-balance{text-wrap:balance}.ui\:break-words{overflow-wrap:break-word}.ui\:text-ellipsis{text-overflow:ellipsis}.ui\:whitespace-nowrap{white-space:nowrap}.ui\:text-current{color:currentColor}.ui\:text-destructive{color:var(--destructive)}.ui\:text-foreground{color:var(--foreground)}.ui\:text-green-500{color:var(--ui-color-green-500)}.ui\:text-muted-foreground{color:var(--muted-foreground)}.ui\:text-popover-foreground{color:var(--popover-foreground)}.ui\:text-primary{color:var(--primary)}.ui\:text-primary-foreground{color:var(--primary-foreground)}.ui\:text-secondary-foreground{color:var(--secondary-foreground)}.ui\:text-sidebar-foreground,.ui\:text-sidebar-foreground\/70{color:var(--sidebar-foreground)}@supports (color:color-mix(in lab, red, red)){.ui\:text-sidebar-foreground\/70{color:color-mix(in oklab,var(--sidebar-foreground)70%,transparent)}}.ui\:text-white{color:var(--ui-color-white)}.ui\:underline-offset-4{text-underline-offset:4px}.ui\:opacity-0{opacity:0}.ui\:opacity-50{opacity:.5}.ui\:opacity-70{opacity:.7}.ui\:shadow-\[0_0_0_1px_var\(--sidebar-border\)\]{--tw-shadow:0 0 0 1px var(--tw-shadow-color,var(--sidebar-border));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:ring-0{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:ring-sidebar-ring{--tw-ring-color:var(--sidebar-ring)}.ui\:ring-offset-background{--tw-ring-offset-color:var(--background)}.ui\:outline-hidden{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.ui\:outline-hidden{outline-offset:2px;outline:2px solid #0000}}.ui\:backdrop-blur-sm{--tw-backdrop-blur:blur(var(--ui-blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.ui\:transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-\[color\,box-shadow\]{transition-property:color,box-shadow;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-\[left\,right\,width\]{transition-property:left,right,width;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-\[margin\,opacity\]{transition-property:margin,opacity;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-\[width\,height\,padding\]{transition-property:width,height,padding;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-none{transition-property:none}.ui\:duration-100{--tw-duration:.1s;transition-duration:.1s}.ui\:duration-200{--tw-duration:.2s;transition-duration:.2s}.ui\:ease-in-out{--tw-ease:var(--ui-ease-in-out);transition-timing-function:var(--ui-ease-in-out)}.ui\:ease-linear{--tw-ease:linear;transition-timing-function:linear}.ui\:fade-in-0{--tw-enter-opacity:0}.ui\:outline-none{--tw-outline-style:none;outline-style:none}.ui\:select-none{-webkit-user-select:none;user-select:none}.ui\:zoom-in-95{--tw-enter-scale:.95}.ui\:group-has-data-\[sidebar\=menu-action\]\/menu-item\:pr-8:is(:where(.ui\:group\/menu-item):has([data-sidebar=menu-action]) *){padding-right:calc(var(--ui-spacing)*8)}.ui\:group-has-\[\[data-slot\=item-description\]\]\/item\:translate-y-0\.5:is(:where(.ui\:group\/item):has([data-slot=item-description]) *){--tw-translate-y:calc(var(--ui-spacing)*.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:group-has-\[\[data-slot\=item-description\]\]\/item\:self-start:is(:where(.ui\:group\/item):has([data-slot=item-description]) *){align-self:flex-start}.ui\:group-has-\[\>input\]\/input-group\:pt-2\.5:is(:where(.ui\:group\/input-group):has(>input) *){padding-top:calc(var(--ui-spacing)*2.5)}.ui\:group-has-\[\>input\]\/input-group\:pb-2\.5:is(:where(.ui\:group\/input-group):has(>input) *){padding-bottom:calc(var(--ui-spacing)*2.5)}.ui\:group-data-\[collapsible\=icon\]\:-mt-8:is(:where(.ui\:group)[data-collapsible=icon] *){margin-top:calc(var(--ui-spacing)*-8)}.ui\:group-data-\[collapsible\=icon\]\:hidden:is(:where(.ui\:group)[data-collapsible=icon] *){display:none}.ui\:group-data-\[collapsible\=icon\]\:size-8\!:is(:where(.ui\:group)[data-collapsible=icon] *){width:calc(var(--ui-spacing)*8)!important;height:calc(var(--ui-spacing)*8)!important}.ui\:group-data-\[collapsible\=icon\]\:w-\(--sidebar-width-icon\):is(:where(.ui\:group)[data-collapsible=icon] *){width:var(--sidebar-width-icon)}.ui\:group-data-\[collapsible\=icon\]\:w-\[calc\(var\(--sidebar-width-icon\)\+\(--spacing\(4\)\)\+2px\)\]:is(:where(.ui\:group)[data-collapsible=icon] *){width:calc(var(--sidebar-width-icon) + (calc(var(--ui-spacing)*4)) + 2px)}.ui\:group-data-\[collapsible\=icon\]\:overflow-hidden:is(:where(.ui\:group)[data-collapsible=icon] *){overflow:hidden}.ui\:group-data-\[collapsible\=icon\]\:p-0\!:is(:where(.ui\:group)[data-collapsible=icon] *){padding:calc(var(--ui-spacing)*0)!important}.ui\:group-data-\[collapsible\=icon\]\:p-2\!:is(:where(.ui\:group)[data-collapsible=icon] *){padding:calc(var(--ui-spacing)*2)!important}.ui\:group-data-\[collapsible\=icon\]\:opacity-0:is(:where(.ui\:group)[data-collapsible=icon] *){opacity:0}.ui\:group-data-\[collapsible\=offcanvas\]\:right-\[calc\(var\(--sidebar-width\)\*-1\)\]:is(:where(.ui\:group)[data-collapsible=offcanvas] *){right:calc(var(--sidebar-width)*-1)}.ui\:group-data-\[collapsible\=offcanvas\]\:left-\[calc\(var\(--sidebar-width\)\*-1\)\]:is(:where(.ui\:group)[data-collapsible=offcanvas] *){left:calc(var(--sidebar-width)*-1)}.ui\:group-data-\[collapsible\=offcanvas\]\:w-0:is(:where(.ui\:group)[data-collapsible=offcanvas] *){width:calc(var(--ui-spacing)*0)}.ui\:group-data-\[collapsible\=offcanvas\]\:translate-x-0:is(:where(.ui\:group)[data-collapsible=offcanvas] *){--tw-translate-x:calc(var(--ui-spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:group-data-\[disabled\=true\]\:pointer-events-none:is(:where(.ui\:group)[data-disabled=true] *){pointer-events:none}.ui\:group-data-\[disabled\=true\]\:opacity-50:is(:where(.ui\:group)[data-disabled=true] *),.ui\:group-data-\[disabled\=true\]\/input-group\:opacity-50:is(:where(.ui\:group\/input-group)[data-disabled=true] *){opacity:.5}.ui\:group-data-\[side\=left\]\:-right-4:is(:where(.ui\:group)[data-side=left] *){right:calc(var(--ui-spacing)*-4)}.ui\:group-data-\[side\=left\]\:border-r:is(:where(.ui\:group)[data-side=left] *){border-right-style:var(--tw-border-style);border-right-width:1px}.ui\:group-data-\[side\=right\]\:left-0:is(:where(.ui\:group)[data-side=right] *){left:calc(var(--ui-spacing)*0)}.ui\:group-data-\[side\=right\]\:rotate-180:is(:where(.ui\:group)[data-side=right] *){rotate:180deg}.ui\:group-data-\[side\=right\]\:border-l:is(:where(.ui\:group)[data-side=right] *){border-left-style:var(--tw-border-style);border-left-width:1px}.ui\:group-data-\[variant\=floating\]\:rounded-lg:is(:where(.ui\:group)[data-variant=floating] *){border-radius:var(--radius)}.ui\:group-data-\[variant\=floating\]\:border:is(:where(.ui\:group)[data-variant=floating] *){border-style:var(--tw-border-style);border-width:1px}.ui\:group-data-\[variant\=floating\]\:border-sidebar-border:is(:where(.ui\:group)[data-variant=floating] *){border-color:var(--sidebar-border)}.ui\:group-data-\[variant\=floating\]\:shadow-sm:is(:where(.ui\:group)[data-variant=floating] *){--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:group-data-\[vaul-drawer-direction\=bottom\]\/drawer-content\:block:is(:where(.ui\:group\/drawer-content)[data-vaul-drawer-direction=bottom] *){display:block}@media (hover:hover){.ui\:peer-hover\/menu-button\:text-sidebar-accent-foreground:is(:where(.ui\:peer\/menu-button):hover~*){color:var(--sidebar-accent-foreground)}}.ui\:peer-disabled\:cursor-not-allowed:is(:where(.ui\:peer):disabled~*){cursor:not-allowed}.ui\:peer-disabled\:opacity-50:is(:where(.ui\:peer):disabled~*){opacity:.5}.ui\:peer-data-\[active\=true\]\/menu-button\:text-sidebar-accent-foreground:is(:where(.ui\:peer\/menu-button)[data-active=true]~*){color:var(--sidebar-accent-foreground)}.ui\:peer-data-\[size\=default\]\/menu-button\:top-1\.5:is(:where(.ui\:peer\/menu-button)[data-size=default]~*){top:calc(var(--ui-spacing)*1.5)}.ui\:peer-data-\[size\=lg\]\/menu-button\:top-2\.5:is(:where(.ui\:peer\/menu-button)[data-size=lg]~*){top:calc(var(--ui-spacing)*2.5)}.ui\:peer-data-\[size\=sm\]\/menu-button\:top-1:is(:where(.ui\:peer\/menu-button)[data-size=sm]~*){top:calc(var(--ui-spacing)*1)}.ui\:selection\:bg-primary ::selection{background-color:var(--primary)}.ui\:selection\:bg-primary::selection{background-color:var(--primary)}.ui\:selection\:text-primary-foreground ::selection{color:var(--primary-foreground)}.ui\:selection\:text-primary-foreground::selection{color:var(--primary-foreground)}.ui\:placeholder\:text-muted-foreground::placeholder{color:var(--muted-foreground)}.ui\:after\:absolute:after{content:var(--tw-content);position:absolute}.ui\:after\:-inset-2:after{content:var(--tw-content);inset:calc(var(--ui-spacing)*-2)}.ui\:after\:inset-y-0:after{content:var(--tw-content);inset-block:calc(var(--ui-spacing)*0)}.ui\:after\:left-\[calc\(1\/2\*100\%-1px\)\]:after{content:var(--tw-content);left:calc(50% - 1px)}.ui\:after\:w-\[2px\]:after{content:var(--tw-content);width:2px}.ui\:group-data-\[collapsible\=offcanvas\]\:after\:left-full:is(:where(.ui\:group)[data-collapsible=offcanvas] *):after{content:var(--tw-content);left:100%}.ui\:last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (hover:hover){.ui\:hover\:bg-accent:hover{background-color:var(--accent)}.ui\:hover\:bg-destructive\/90:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:hover\:bg-destructive\/90:hover{background-color:color-mix(in oklab,var(--destructive)90%,transparent)}}.ui\:hover\:bg-primary\/90:hover{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.ui\:hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--primary)90%,transparent)}}.ui\:hover\:bg-secondary\/80:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab, red, red)){.ui\:hover\:bg-secondary\/80:hover{background-color:color-mix(in oklab,var(--secondary)80%,transparent)}}.ui\:hover\:bg-sidebar-accent:hover{background-color:var(--sidebar-accent)}.ui\:hover\:text-accent-foreground:hover{color:var(--accent-foreground)}.ui\:hover\:text-foreground:hover{color:var(--foreground)}.ui\:hover\:text-sidebar-accent-foreground:hover{color:var(--sidebar-accent-foreground)}.ui\:hover\:underline:hover{text-decoration-line:underline}.ui\:hover\:opacity-100:hover{opacity:1}.ui\:hover\:shadow-\[0_0_0_1px_var\(--sidebar-accent\)\]:hover{--tw-shadow:0 0 0 1px var(--tw-shadow-color,var(--sidebar-accent));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:hover\:group-data-\[collapsible\=offcanvas\]\:bg-sidebar:hover:is(:where(.ui\:group)[data-collapsible=offcanvas] *){background-color:var(--sidebar)}.ui\:hover\:after\:bg-sidebar-border:hover:after{content:var(--tw-content);background-color:var(--sidebar-border)}}.ui\:focus\:bg-accent:focus{background-color:var(--accent)}.ui\:focus\:text-accent-foreground:focus{color:var(--accent-foreground)}.ui\:focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:focus\:ring-ring:focus{--tw-ring-color:var(--ring)}.ui\:focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ui\:focus\:outline-hidden:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.ui\:focus\:outline-hidden:focus{outline-offset:2px;outline:2px solid #0000}}.ui\:focus-visible\:border-ring:focus-visible{border-color:var(--ring)}.ui\:focus-visible\:ring-0:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:focus-visible\:ring-\[3px\]:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(3px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.ui\:focus-visible\:ring-ring:focus-visible,.ui\:focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){.ui\:focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:color-mix(in oklab,var(--ring)50%,transparent)}}.ui\:focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ui\:focus-visible\:outline-hidden:focus-visible{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.ui\:focus-visible\:outline-hidden:focus-visible{outline-offset:2px;outline:2px solid #0000}}.ui\:focus-visible\:outline-1:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.ui\:focus-visible\:outline-ring:focus-visible{outline-color:var(--ring)}.ui\:active\:bg-sidebar-accent:active{background-color:var(--sidebar-accent)}.ui\:active\:text-sidebar-accent-foreground:active{color:var(--sidebar-accent-foreground)}.ui\:disabled\:pointer-events-none:disabled{pointer-events:none}.ui\:disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.ui\:disabled\:opacity-50:disabled{opacity:.5}:where([data-side=left]) .ui\:in-data-\[side\=left\]\:cursor-w-resize{cursor:w-resize}:where([data-side=right]) .ui\:in-data-\[side\=right\]\:cursor-e-resize{cursor:e-resize}:where([data-slot=tooltip-content]) .ui\:in-data-\[slot\=tooltip-content\]\:bg-background\/20{background-color:var(--background)}@supports (color:color-mix(in lab, red, red)){:where([data-slot=tooltip-content]) .ui\:in-data-\[slot\=tooltip-content\]\:bg-background\/20{background-color:color-mix(in oklab,var(--background)20%,transparent)}}:where([data-slot=tooltip-content]) .ui\:in-data-\[slot\=tooltip-content\]\:text-background{color:var(--background)}.ui\:has-data-\[variant\=inset\]\:bg-sidebar:has([data-variant=inset]){background-color:var(--sidebar)}.ui\:has-\[\[data-slot\=input-group-control\]\:focus-visible\]\:border-ring:has([data-slot=input-group-control]:focus-visible){border-color:var(--ring)}.ui\:has-\[\[data-slot\=input-group-control\]\:focus-visible\]\:ring-\[3px\]:has([data-slot=input-group-control]:focus-visible){--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(3px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:has-\[\[data-slot\=input-group-control\]\:focus-visible\]\:ring-ring\/50:has([data-slot=input-group-control]:focus-visible){--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){.ui\:has-\[\[data-slot\=input-group-control\]\:focus-visible\]\:ring-ring\/50:has([data-slot=input-group-control]:focus-visible){--tw-ring-color:color-mix(in oklab,var(--ring)50%,transparent)}}.ui\:has-\[\[data-slot\]\[aria-invalid\=true\]\]\:border-destructive:has([data-slot][aria-invalid=true]){border-color:var(--destructive)}.ui\:has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-destructive\/20:has([data-slot][aria-invalid=true]){--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-destructive\/20:has([data-slot][aria-invalid=true]){--tw-ring-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.ui\:has-\[\>\[data-align\=block-end\]\]\:h-auto:has(>[data-align=block-end]){height:auto}.ui\:has-\[\>\[data-align\=block-end\]\]\:flex-col:has(>[data-align=block-end]){flex-direction:column}.ui\:has-\[\>\[data-align\=block-start\]\]\:h-auto:has(>[data-align=block-start]){height:auto}.ui\:has-\[\>\[data-align\=block-start\]\]\:flex-col:has(>[data-align=block-start]){flex-direction:column}.ui\:has-\[\>button\]\:ms-\[-0\.45rem\]:has(>button){margin-inline-start:-.45rem}.ui\:has-\[\>button\]\:me-\[-0\.45rem\]:has(>button){margin-inline-end:-.45rem}.ui\:has-\[\>kbd\]\:ms-\[-0\.35rem\]:has(>kbd){margin-inline-start:-.35rem}.ui\:has-\[\>kbd\]\:me-\[-0\.35rem\]:has(>kbd){margin-inline-end:-.35rem}.ui\:has-\[\>svg\]\:p-0:has(>svg){padding:calc(var(--ui-spacing)*0)}.ui\:has-\[\>svg\]\:px-2:has(>svg){padding-inline:calc(var(--ui-spacing)*2)}.ui\:has-\[\>svg\]\:px-2\.5:has(>svg){padding-inline:calc(var(--ui-spacing)*2.5)}.ui\:has-\[\>svg\]\:px-3:has(>svg){padding-inline:calc(var(--ui-spacing)*3)}.ui\:has-\[\>svg\]\:px-4:has(>svg){padding-inline:calc(var(--ui-spacing)*4)}.ui\:has-\[\>textarea\]\:h-auto:has(>textarea){height:auto}.ui\:aria-disabled\:pointer-events-none[aria-disabled=true]{pointer-events:none}.ui\:aria-disabled\:opacity-50[aria-disabled=true]{opacity:.5}.ui\:aria-invalid\:border-destructive[aria-invalid=true]{border-color:var(--destructive)}.ui\:aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.ui\:aria-selected\:bg-accent[aria-selected=true]{background-color:var(--accent)}.ui\:aria-selected\:text-accent-foreground[aria-selected=true]{color:var(--accent-foreground)}:is(.ui\:\*\*\:data-command-group\:px-2 *)[data-command-group]{padding-inline:calc(var(--ui-spacing)*2)}:is(.ui\:\*\*\:data-command-input\:h-12 *)[data-command-input]{height:calc(var(--ui-spacing)*12)}:is(.ui\:\*\*\:data-command-item\:px-2 *)[data-command-item]{padding-inline:calc(var(--ui-spacing)*2)}:is(.ui\:\*\*\:data-command-item\:py-3 *)[data-command-item]{padding-block:calc(var(--ui-spacing)*3)}.ui\:data-disabled\:pointer-events-none[data-disabled]{pointer-events:none}.ui\:data-disabled\:cursor-not-allowed[data-disabled]{cursor:not-allowed}.ui\:data-disabled\:opacity-50[data-disabled]{opacity:.5}.ui\:data-highlighted\:bg-accent[data-highlighted]{background-color:var(--accent)}.ui\:data-highlighted\:text-accent-foreground[data-highlighted]{color:var(--accent-foreground)}.ui\:data-inset\:pl-8[data-inset]{padding-left:calc(var(--ui-spacing)*8)}.ui\:data-placeholder\:text-muted-foreground[data-placeholder]{color:var(--muted-foreground)}.ui\:data-\[active\=true\]\:bg-sidebar-accent[data-active=true]{background-color:var(--sidebar-accent)}.ui\:data-\[active\=true\]\:font-medium[data-active=true]{--tw-font-weight:var(--ui-font-weight-medium);font-weight:var(--ui-font-weight-medium)}.ui\:data-\[active\=true\]\:text-sidebar-accent-foreground[data-active=true]{color:var(--sidebar-accent-foreground)}.ui\:data-\[current\=true\]\:bg-muted[data-current=true]{background-color:var(--muted)}.ui\:data-\[disabled\=true\]\:pointer-events-none[data-disabled=true]{pointer-events:none}.ui\:data-\[disabled\=true\]\:opacity-50[data-disabled=true]{opacity:.5}.ui\:data-\[inset\]\:pl-8[data-inset]{padding-left:calc(var(--ui-spacing)*8)}.ui\:data-\[orientation\=horizontal\]\:h-px[data-orientation=horizontal]{height:1px}.ui\:data-\[orientation\=horizontal\]\:w-full[data-orientation=horizontal]{width:100%}.ui\:data-\[orientation\=vertical\]\:h-full[data-orientation=vertical]{height:100%}.ui\:data-\[orientation\=vertical\]\:w-px[data-orientation=vertical]{width:1px}.ui\:data-\[side\=bottom\]\:-translate-x-1\/2[data-side=bottom]{--tw-translate-x:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=bottom\]\:-translate-y-\[calc\(-50\%\+1px\)\][data-side=bottom]{--tw-translate-y:calc(calc(-50% + 1px)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y:calc(var(--ui-spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y:calc(2*var(--spacing)*-1)}.ui\:data-\[side\=left\]\:-translate-x-1[data-side=left]{--tw-translate-x:calc(var(--ui-spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=left\]\:-translate-y-\[calc\(50\%-3px\)\][data-side=left]{--tw-translate-y:calc(calc(50% - 3px)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x:calc(2*var(--spacing))}.ui\:data-\[side\=right\]\:translate-x-1[data-side=right]{--tw-translate-x:calc(var(--ui-spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=right\]\:translate-x-\[calc\(50\%\+2px\)\][data-side=right]{--tw-translate-x:calc(50% + 2px);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=right\]\:translate-y-1\/2[data-side=right]{--tw-translate-y:calc(1/2*100%);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x:calc(2*var(--spacing)*-1)}.ui\:data-\[side\=top\]\:translate-x-1\/2[data-side=top]{--tw-translate-x:calc(1/2*100%);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y:calc(var(--ui-spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=top\]\:translate-y-\[calc\(-50\%\+2px\)\][data-side=top]{--tw-translate-y:calc(-50% + 2px);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y:calc(2*var(--spacing))}.ui\:data-\[size\=default\]\:h-9[data-size=default]{height:calc(var(--ui-spacing)*9)}.ui\:data-\[size\=sm\]\:h-8[data-size=sm]{height:calc(var(--ui-spacing)*8)}:is(.ui\:\*\*\:data-\[slot\=select-value\]\:line-clamp-1 *)[data-slot=select-value]{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}:is(.ui\:\*\*\:data-\[slot\=select-value\]\:flex *)[data-slot=select-value]{display:flex}:is(.ui\:\*\*\:data-\[slot\=select-value\]\:items-center *)[data-slot=select-value]{align-items:center}:is(.ui\:\*\*\:data-\[slot\=select-value\]\:gap-2 *)[data-slot=select-value]{gap:calc(var(--ui-spacing)*2)}.ui\:data-\[state\=active\]\:bg-background[data-state=active]{background-color:var(--background)}.ui\:data-\[state\=active\]\:shadow-sm[data-state=active]{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:data-\[state\=checked\]\:translate-x-\[calc\(100\%-2px\)\][data-state=checked]{--tw-translate-x:calc(100% - 2px);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[state\=checked\]\:border-primary[data-state=checked]{border-color:var(--primary)}.ui\:data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:var(--primary)}.ui\:data-\[state\=checked\]\:text-primary-foreground[data-state=checked]{color:var(--primary-foreground)}.ui\:data-\[state\=closed\]\:animate-accordion-up[data-state=closed]{animation:accordion-up var(--tw-animation-duration,var(--tw-duration,.2s))var(--tw-ease,ease-out)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.ui\:data-\[state\=closed\]\:animate-out[data-state=closed]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.ui\:data-\[state\=closed\]\:duration-300[data-state=closed]{--tw-duration:.3s;transition-duration:.3s}.ui\:data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity:0}.ui\:data-\[state\=closed\]\:zoom-out-95[data-state=closed]{--tw-exit-scale:.95}.ui\:data-\[state\=closed\]\:slide-out-to-bottom[data-state=closed]{--tw-exit-translate-y:100%}.ui\:data-\[state\=closed\]\:slide-out-to-left[data-state=closed]{--tw-exit-translate-x:-100%}.ui\:data-\[state\=closed\]\:slide-out-to-right[data-state=closed]{--tw-exit-translate-x:100%}.ui\:data-\[state\=closed\]\:slide-out-to-top[data-state=closed]{--tw-exit-translate-y:-100%}.ui\:data-\[state\=open\]\:animate-accordion-down[data-state=open]{animation:accordion-down var(--tw-animation-duration,var(--tw-duration,.2s))var(--tw-ease,ease-out)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.ui\:data-\[state\=open\]\:animate-in[data-state=open]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.ui\:data-\[state\=open\]\:bg-accent[data-state=open]{background-color:var(--accent)}.ui\:data-\[state\=open\]\:text-accent-foreground[data-state=open]{color:var(--accent-foreground)}.ui\:data-\[state\=open\]\:opacity-100[data-state=open]{opacity:1}.ui\:data-\[state\=open\]\:duration-500[data-state=open]{--tw-duration:.5s;transition-duration:.5s}.ui\:data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity:0}.ui\:data-\[state\=open\]\:zoom-in-95[data-state=open]{--tw-enter-scale:.95}.ui\:data-\[state\=open\]\:slide-in-from-bottom[data-state=open]{--tw-enter-translate-y:100%}.ui\:data-\[state\=open\]\:slide-in-from-left[data-state=open]{--tw-enter-translate-x:-100%}.ui\:data-\[state\=open\]\:slide-in-from-right[data-state=open]{--tw-enter-translate-x:100%}.ui\:data-\[state\=open\]\:slide-in-from-top[data-state=open]{--tw-enter-translate-y:-100%}@media (hover:hover){.ui\:data-\[state\=open\]\:hover\:bg-sidebar-accent[data-state=open]:hover{background-color:var(--sidebar-accent)}.ui\:data-\[state\=open\]\:hover\:text-sidebar-accent-foreground[data-state=open]:hover{color:var(--sidebar-accent-foreground)}}.ui\:data-\[state\=selected\]\:bg-muted[data-state=selected]{background-color:var(--muted)}.ui\:data-\[state\=unchecked\]\:translate-x-0[data-state=unchecked]{--tw-translate-x:calc(var(--ui-spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[state\=unchecked\]\:bg-input[data-state=unchecked]{background-color:var(--input)}.ui\:data-\[variant\=destructive\]\:text-destructive[data-variant=destructive]{color:var(--destructive)}.ui\:data-\[variant\=destructive\]\:focus\:bg-destructive\/10[data-variant=destructive]:focus{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:data-\[variant\=destructive\]\:focus\:bg-destructive\/10[data-variant=destructive]:focus{background-color:color-mix(in oklab,var(--destructive)10%,transparent)}}.ui\:data-\[variant\=destructive\]\:focus\:text-destructive[data-variant=destructive]:focus{color:var(--destructive)}.ui\:data-\[variant\=destructive\]\:data-highlighted\:bg-destructive\/10[data-variant=destructive][data-highlighted]{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:data-\[variant\=destructive\]\:data-highlighted\:bg-destructive\/10[data-variant=destructive][data-highlighted]{background-color:color-mix(in oklab,var(--destructive)10%,transparent)}}.ui\:data-\[variant\=destructive\]\:data-highlighted\:text-destructive[data-variant=destructive][data-highlighted]{color:var(--destructive)}.ui\:data-\[vaul-drawer-direction\=bottom\]\:inset-x-0[data-vaul-drawer-direction=bottom]{inset-inline:calc(var(--ui-spacing)*0)}.ui\:data-\[vaul-drawer-direction\=bottom\]\:bottom-0[data-vaul-drawer-direction=bottom]{bottom:calc(var(--ui-spacing)*0)}.ui\:data-\[vaul-drawer-direction\=bottom\]\:mt-24[data-vaul-drawer-direction=bottom]{margin-top:calc(var(--ui-spacing)*24)}.ui\:data-\[vaul-drawer-direction\=bottom\]\:max-h-\[80vh\][data-vaul-drawer-direction=bottom]{max-height:80vh}.ui\:data-\[vaul-drawer-direction\=bottom\]\:rounded-t-lg[data-vaul-drawer-direction=bottom]{border-top-left-radius:var(--radius);border-top-right-radius:var(--radius)}.ui\:data-\[vaul-drawer-direction\=bottom\]\:border-t[data-vaul-drawer-direction=bottom]{border-top-style:var(--tw-border-style);border-top-width:1px}.ui\:data-\[vaul-drawer-direction\=left\]\:inset-y-0[data-vaul-drawer-direction=left]{inset-block:calc(var(--ui-spacing)*0)}.ui\:data-\[vaul-drawer-direction\=left\]\:start-0[data-vaul-drawer-direction=left]{inset-inline-start:calc(var(--ui-spacing)*0)}.ui\:data-\[vaul-drawer-direction\=left\]\:w-3\/4[data-vaul-drawer-direction=left]{width:75%}.ui\:data-\[vaul-drawer-direction\=left\]\:border-e[data-vaul-drawer-direction=left]{border-inline-end-style:var(--tw-border-style);border-inline-end-width:1px}.ui\:data-\[vaul-drawer-direction\=right\]\:inset-y-0[data-vaul-drawer-direction=right]{inset-block:calc(var(--ui-spacing)*0)}.ui\:data-\[vaul-drawer-direction\=right\]\:end-0[data-vaul-drawer-direction=right]{inset-inline-end:calc(var(--ui-spacing)*0)}.ui\:data-\[vaul-drawer-direction\=right\]\:w-3\/4[data-vaul-drawer-direction=right]{width:75%}.ui\:data-\[vaul-drawer-direction\=right\]\:border-s[data-vaul-drawer-direction=right]{border-inline-start-style:var(--tw-border-style);border-inline-start-width:1px}.ui\:data-\[vaul-drawer-direction\=top\]\:inset-x-0[data-vaul-drawer-direction=top]{inset-inline:calc(var(--ui-spacing)*0)}.ui\:data-\[vaul-drawer-direction\=top\]\:top-0[data-vaul-drawer-direction=top]{top:calc(var(--ui-spacing)*0)}.ui\:data-\[vaul-drawer-direction\=top\]\:mb-24[data-vaul-drawer-direction=top]{margin-bottom:calc(var(--ui-spacing)*24)}.ui\:data-\[vaul-drawer-direction\=top\]\:max-h-\[80vh\][data-vaul-drawer-direction=top]{max-height:80vh}.ui\:data-\[vaul-drawer-direction\=top\]\:rounded-b-lg[data-vaul-drawer-direction=top]{border-bottom-right-radius:var(--radius);border-bottom-left-radius:var(--radius)}.ui\:data-\[vaul-drawer-direction\=top\]\:border-b[data-vaul-drawer-direction=top]{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}@media (min-width:40rem){.ui\:sm\:flex{display:flex}.ui\:sm\:max-w-lg{max-width:var(--ui-container-lg)}.ui\:sm\:max-w-sm{max-width:var(--ui-container-sm)}.ui\:sm\:flex-row{flex-direction:row}.ui\:sm\:justify-end{justify-content:flex-end}.ui\:sm\:gap-2\.5{gap:calc(var(--ui-spacing)*2.5)}.ui\:sm\:pr-2\.5{padding-right:calc(var(--ui-spacing)*2.5)}.ui\:sm\:pl-2\.5{padding-left:calc(var(--ui-spacing)*2.5)}.ui\:sm\:text-left{text-align:left}.ui\:data-\[vaul-drawer-direction\=left\]\:sm\:max-w-sm[data-vaul-drawer-direction=left],.ui\:data-\[vaul-drawer-direction\=right\]\:sm\:max-w-sm[data-vaul-drawer-direction=right]{max-width:var(--ui-container-sm)}}@media (min-width:48rem){.ui\:md\:block{display:block}.ui\:md\:flex{display:flex}.ui\:md\:text-sm{font-size:var(--ui-text-sm);line-height:var(--tw-leading,var(--ui-text-sm--line-height))}.ui\:md\:opacity-0{opacity:0}.ui\:md\:peer-data-\[variant\=inset\]\:m-2:is(:where(.ui\:peer)[data-variant=inset]~*){margin:calc(var(--ui-spacing)*2)}.ui\:md\:peer-data-\[variant\=inset\]\:ml-0:is(:where(.ui\:peer)[data-variant=inset]~*){margin-left:calc(var(--ui-spacing)*0)}.ui\:md\:peer-data-\[variant\=inset\]\:rounded-xl:is(:where(.ui\:peer)[data-variant=inset]~*){border-radius:calc(var(--radius) + 4px)}.ui\:md\:peer-data-\[variant\=inset\]\:shadow-sm:is(:where(.ui\:peer)[data-variant=inset]~*){--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:md\:peer-data-\[variant\=inset\]\:peer-data-\[state\=collapsed\]\:ml-2:is(:where(.ui\:peer)[data-variant=inset]~*):is(:where(.ui\:peer)[data-state=collapsed]~*){margin-left:calc(var(--ui-spacing)*2)}.ui\:md\:after\:hidden:after{content:var(--tw-content);display:none}}.ui\:dark\:scale-0:where(.dark,.dark *){--tw-scale-x:0%;--tw-scale-y:0%;--tw-scale-z:0%;scale:var(--tw-scale-x)var(--tw-scale-y)}.ui\:dark\:scale-100:where(.dark,.dark *){--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.ui\:dark\:-rotate-90:where(.dark,.dark *){rotate:-90deg}.ui\:dark\:rotate-0:where(.dark,.dark *){rotate:none}.ui\:dark\:border-input:where(.dark,.dark *){border-color:var(--input)}.ui\:dark\:bg-destructive\/60:where(.dark,.dark *){background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:bg-destructive\/60:where(.dark,.dark *){background-color:color-mix(in oklab,var(--destructive)60%,transparent)}}.ui\:dark\:bg-destructive\/70:where(.dark,.dark *){background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:bg-destructive\/70:where(.dark,.dark *){background-color:color-mix(in oklab,var(--destructive)70%,transparent)}}.ui\:dark\:bg-input\/30:where(.dark,.dark *){background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:bg-input\/30:where(.dark,.dark *){background-color:color-mix(in oklab,var(--input)30%,transparent)}}.ui\:dark\:bg-transparent:where(.dark,.dark *){background-color:#0000}.ui\:dark\:text-muted-foreground:where(.dark,.dark *){color:var(--muted-foreground)}@media (hover:hover){.ui\:dark\:hover\:bg-accent\/50:where(.dark,.dark *):hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:hover\:bg-accent\/50:where(.dark,.dark *):hover{background-color:color-mix(in oklab,var(--accent)50%,transparent)}}.ui\:dark\:hover\:bg-input\/50:where(.dark,.dark *):hover{background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:hover\:bg-input\/50:where(.dark,.dark *):hover{background-color:color-mix(in oklab,var(--input)50%,transparent)}}}.ui\:dark\:focus-visible\:ring-destructive\/40:where(.dark,.dark *):focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:focus-visible\:ring-destructive\/40:where(.dark,.dark *):focus-visible{--tw-ring-color:color-mix(in oklab,var(--destructive)40%,transparent)}}:where([data-slot=tooltip-content]) .ui\:dark\:in-data-\[slot\=tooltip-content\]\:bg-background\/10:where(.dark,.dark *){background-color:var(--background)}@supports (color:color-mix(in lab, red, red)){:where([data-slot=tooltip-content]) .ui\:dark\:in-data-\[slot\=tooltip-content\]\:bg-background\/10:where(.dark,.dark *){background-color:color-mix(in oklab,var(--background)10%,transparent)}}.ui\:dark\:has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-destructive\/40:where(.dark,.dark *):has([data-slot][aria-invalid=true]){--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-destructive\/40:where(.dark,.dark *):has([data-slot][aria-invalid=true]){--tw-ring-color:color-mix(in oklab,var(--destructive)40%,transparent)}}.ui\:dark\:aria-invalid\:ring-destructive\/40:where(.dark,.dark *)[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:aria-invalid\:ring-destructive\/40:where(.dark,.dark *)[aria-invalid=true]{--tw-ring-color:color-mix(in oklab,var(--destructive)40%,transparent)}}.ui\:dark\:data-\[state\=active\]\:border-input:where(.dark,.dark *)[data-state=active]{border-color:var(--input)}.ui\:dark\:data-\[state\=active\]\:bg-input\/30:where(.dark,.dark *)[data-state=active]{background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:data-\[state\=active\]\:bg-input\/30:where(.dark,.dark *)[data-state=active]{background-color:color-mix(in oklab,var(--input)30%,transparent)}}.ui\:dark\:data-\[state\=active\]\:text-foreground:where(.dark,.dark *)[data-state=active]{color:var(--foreground)}.ui\:dark\:data-\[state\=checked\]\:bg-primary:where(.dark,.dark *)[data-state=checked]{background-color:var(--primary)}.ui\:dark\:data-\[state\=checked\]\:bg-primary-foreground:where(.dark,.dark *)[data-state=checked]{background-color:var(--primary-foreground)}.ui\:dark\:data-\[state\=unchecked\]\:bg-foreground:where(.dark,.dark *)[data-state=unchecked]{background-color:var(--foreground)}.ui\:dark\:data-\[state\=unchecked\]\:bg-input\/80:where(.dark,.dark *)[data-state=unchecked]{background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:data-\[state\=unchecked\]\:bg-input\/80:where(.dark,.dark *)[data-state=unchecked]{background-color:color-mix(in oklab,var(--input)80%,transparent)}}.ui\:\[\&_\[data-command-group\]\:not\(\[hidden\]\)_\~\[data-command-group\]\]\:pt-0 [data-command-group]:not([hidden])~[data-command-group]{padding-top:calc(var(--ui-spacing)*0)}.ui\:\[\&_\[data-command-input-wrapper\]_svg\]\:h-5 [data-command-input-wrapper] svg{height:calc(var(--ui-spacing)*5)}.ui\:\[\&_\[data-command-input-wrapper\]_svg\]\:w-5 [data-command-input-wrapper] svg{width:calc(var(--ui-spacing)*5)}.ui\:\[\&_\[data-command-item\]_svg\]\:h-5 [data-command-item] svg{height:calc(var(--ui-spacing)*5)}.ui\:\[\&_\[data-command-item\]_svg\]\:w-5 [data-command-item] svg{width:calc(var(--ui-spacing)*5)}.ui\:\[\&_img\]\:size-full img{width:100%;height:100%}.ui\:\[\&_img\]\:object-cover img{object-fit:cover}.ui\:\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.ui\:\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.ui\:\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3 svg:not([class*=size-]){width:calc(var(--ui-spacing)*3);height:calc(var(--ui-spacing)*3)}.ui\:\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 svg:not([class*=size-]){width:calc(var(--ui-spacing)*4);height:calc(var(--ui-spacing)*4)}.ui\:\[\&_svg\:not\(\[class\*\=\'text-\'\]\)\]\:text-muted-foreground svg:not([class*=text-]){color:var(--muted-foreground)}.ui\:\[\&_tr\]\:border-b tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.ui\:\[\&_tr\:last-child\]\:border-0 tr:last-child{border-style:var(--tw-border-style);border-width:0}.ui\:\[\&\+\[data-slot\=item-content\]\]\:flex-none+[data-slot=item-content]{flex:none}.ui\:\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]){padding-right:calc(var(--ui-spacing)*0)}.ui\:\[\.border-b\]\:pb-3.border-b{padding-bottom:calc(var(--ui-spacing)*3)}.ui\:\[\.border-t\]\:pt-3.border-t{padding-top:calc(var(--ui-spacing)*3)}.ui\:\[a\]\:transition-colors:is(a){transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}@media (hover:hover){.ui\:\[a\]\:hover\:bg-accent\/50:is(a):hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.ui\:\[a\]\:hover\:bg-accent\/50:is(a):hover{background-color:color-mix(in oklab,var(--accent)50%,transparent)}}}:is(.ui\:\*\:\[span\]\:last\:flex>*):is(span):last-child{display:flex}:is(.ui\:\*\:\[span\]\:last\:items-center>*):is(span):last-child{align-items:center}:is(.ui\:\*\:\[span\]\:last\:gap-2>*):is(span):last-child{gap:calc(var(--ui-spacing)*2)}:is(.ui\:data-\[variant\=destructive\]\:\*\:\[svg\]\:text-destructive\![data-variant=destructive]>*):is(svg){color:var(--destructive)!important}.ui\:\[\&\>a\]\:underline>a{text-decoration-line:underline}.ui\:\[\&\>a\]\:underline-offset-4>a{text-underline-offset:4px}.ui\:\[\&\>a\:hover\]\:text-primary>a:hover{color:var(--primary)}.ui\:\[\&\>button\]\:hidden>button{display:none}.ui\:has-\[\>\[data-align\=block-end\]\]\:\[\&\>input\]\:pt-3:has(>[data-align=block-end])>input{padding-top:calc(var(--ui-spacing)*3)}.ui\:has-\[\>\[data-align\=block-start\]\]\:\[\&\>input\]\:pb-3:has(>[data-align=block-start])>input{padding-bottom:calc(var(--ui-spacing)*3)}.ui\:has-\[\>\[data-align\=inline-end\]\]\:\[\&\>input\]\:pe-2:has(>[data-align=inline-end])>input{padding-inline-end:calc(var(--ui-spacing)*2)}.ui\:has-\[\>\[data-align\=inline-start\]\]\:\[\&\>input\]\:ps-2:has(>[data-align=inline-start])>input{padding-inline-start:calc(var(--ui-spacing)*2)}.ui\:\[\&\>kbd\]\:rounded-\[calc\(var\(--radius\)-5px\)\]>kbd{border-radius:calc(var(--radius) - 5px)}.ui\:\[\&\>span\:last-child\]\:truncate>span:last-child{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.ui\:\[\&\>svg\]\:pointer-events-none>svg{pointer-events:none}.ui\:\[\&\>svg\]\:size-2\.5>svg{width:calc(var(--ui-spacing)*2.5);height:calc(var(--ui-spacing)*2.5)}.ui\:\[\&\>svg\]\:size-3>svg{width:calc(var(--ui-spacing)*3);height:calc(var(--ui-spacing)*3)}.ui\:\[\&\>svg\]\:size-3\.5>svg{width:calc(var(--ui-spacing)*3.5);height:calc(var(--ui-spacing)*3.5)}.ui\:\[\&\>svg\]\:size-4>svg{width:calc(var(--ui-spacing)*4);height:calc(var(--ui-spacing)*4)}.ui\:\[\&\>svg\]\:shrink-0>svg{flex-shrink:0}.ui\:\[\&\>svg\]\:text-muted-foreground>svg{color:var(--muted-foreground)}.ui\:\[\&\>svg\]\:text-sidebar-accent-foreground>svg{color:var(--sidebar-accent-foreground)}.ui\:\[\&\>svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3\.5>svg:not([class*=size-]){width:calc(var(--ui-spacing)*3.5);height:calc(var(--ui-spacing)*3.5)}.ui\:\[\&\>svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4>svg:not([class*=size-]){width:calc(var(--ui-spacing)*4);height:calc(var(--ui-spacing)*4)}@media (hover:hover){:is(.ui\:hover\:\[\&\,\&\>svelte-css-wrapper\]\:\[\&\>th\,td\]\:bg-muted\/50:hover,.ui\:hover\:\[\&\,\&\>svelte-css-wrapper\]\:\[\&\>th\,td\]\:bg-muted\/50:hover>svelte-css-wrapper)>th,:is(.ui\:hover\:\[\&\,\&\>svelte-css-wrapper\]\:\[\&\>th\,td\]\:bg-muted\/50:hover,.ui\:hover\:\[\&\,\&\>svelte-css-wrapper\]\:\[\&\>th\,td\]\:bg-muted\/50:hover>svelte-css-wrapper) td{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){:is(.ui\:hover\:\[\&\,\&\>svelte-css-wrapper\]\:\[\&\>th\,td\]\:bg-muted\/50:hover,.ui\:hover\:\[\&\,\&\>svelte-css-wrapper\]\:\[\&\>th\,td\]\:bg-muted\/50:hover>svelte-css-wrapper)>th,:is(.ui\:hover\:\[\&\,\&\>svelte-css-wrapper\]\:\[\&\>th\,td\]\:bg-muted\/50:hover,.ui\:hover\:\[\&\,\&\>svelte-css-wrapper\]\:\[\&\>th\,td\]\:bg-muted\/50:hover>svelte-css-wrapper) td{background-color:color-mix(in oklab,var(--muted)50%,transparent)}}}.ui\:\[\&\>tr\]\:last\:border-b-0>tr:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.ui\:\[\&\[data-state\=open\]\>svg\]\:rotate-180[data-state=open]>svg{rotate:180deg}[data-side=left][data-collapsible=offcanvas] .ui\:\[\[data-side\=left\]\[data-collapsible\=offcanvas\]_\&\]\:-right-2{right:calc(var(--ui-spacing)*-2)}[data-side=left][data-state=collapsed] .ui\:\[\[data-side\=left\]\[data-state\=collapsed\]_\&\]\:cursor-e-resize{cursor:e-resize}[data-side=right][data-collapsible=offcanvas] .ui\:\[\[data-side\=right\]\[data-collapsible\=offcanvas\]_\&\]\:-left-2{left:calc(var(--ui-spacing)*-2)}[data-side=right][data-state=collapsed] .ui\:\[\[data-side\=right\]\[data-state\=collapsed\]_\&\]\:cursor-w-resize{cursor:w-resize}@media (hover:hover){a.ui\:\[a\&\]\:hover\:bg-accent:hover{background-color:var(--accent)}a.ui\:\[a\&\]\:hover\:bg-destructive\/90:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){a.ui\:\[a\&\]\:hover\:bg-destructive\/90:hover{background-color:color-mix(in oklab,var(--destructive)90%,transparent)}}a.ui\:\[a\&\]\:hover\:bg-primary\/90:hover{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){a.ui\:\[a\&\]\:hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--primary)90%,transparent)}}a.ui\:\[a\&\]\:hover\:bg-secondary\/90:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab, red, red)){a.ui\:\[a\&\]\:hover\:bg-secondary\/90:hover{background-color:color-mix(in oklab,var(--secondary)90%,transparent)}}a.ui\:\[a\&\]\:hover\:text-accent-foreground:hover{color:var(--accent-foreground)}}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}:root{--radius:.625rem;--background:oklch(100% 0 0);--foreground:oklch(14.7% .004 49.25);--card:oklch(100% 0 0);--card-foreground:oklch(14.7% .004 49.25);--popover:oklch(100% 0 0);--popover-foreground:oklch(14.7% .004 49.25);--primary:oklch(21.6% .006 56.043);--primary-foreground:oklch(98.5% .001 106.423);--secondary:oklch(97% .001 106.424);--secondary-foreground:oklch(21.6% .006 56.043);--muted:oklch(97% .001 106.424);--muted-foreground:oklch(55.3% .013 58.071);--accent:oklch(96.7% .001 286.375);--accent-foreground:oklch(21.6% .006 56.043);--destructive:oklch(57.7% .245 27.325);--border:oklch(92.3% .003 48.717);--input:oklch(92.3% .003 48.717);--ring:oklch(70.9% .01 56.259);--chart-1:oklch(64.6% .222 41.116);--chart-2:oklch(60% .118 184.704);--chart-3:oklch(39.8% .07 227.392);--chart-4:oklch(82.8% .189 84.429);--chart-5:oklch(76.9% .188 70.08);--sidebar:oklch(100% 0 0);--sidebar-foreground:oklch(14.7% .004 49.25);--sidebar-primary:oklch(21.6% .006 56.043);--sidebar-primary-foreground:oklch(98.5% .001 106.423);--sidebar-accent:oklch(97% .001 106.424);--sidebar-accent-foreground:oklch(21.6% .006 56.043);--sidebar-border:oklch(92.3% .003 48.717);--sidebar-ring:oklch(70.9% .01 56.259)}.dark{--background:oklch(14.7% .004 49.25);--foreground:oklch(98.5% .001 106.423);--card:oklch(21.6% .006 56.043);--card-foreground:oklch(98.5% .001 106.423);--popover:oklch(21.6% .006 56.043);--popover-foreground:oklch(98.5% .001 106.423);--primary:oklch(92.3% .003 48.717);--primary-foreground:oklch(21.6% .006 56.043);--secondary:oklch(26.8% .007 34.298);--secondary-foreground:oklch(98.5% .001 106.423);--muted:oklch(26.8% .007 34.298);--muted-foreground:oklch(70.9% .01 56.259);--accent:oklch(26.8% .007 34.298);--accent-foreground:oklch(98.5% .001 106.423);--destructive:oklch(70.4% .191 22.216);--border:oklch(100% 0 0/.1);--input:oklch(100% 0 0/.15);--ring:oklch(55.3% .013 58.071);--chart-1:oklch(48.8% .243 264.376);--chart-2:oklch(69.6% .17 162.48);--chart-3:oklch(76.9% .188 70.08);--chart-4:oklch(62.7% .265 303.9);--chart-5:oklch(64.5% .246 16.439);--sidebar:oklch(0% 0 0);--sidebar-foreground:oklch(98.5% .001 106.423);--sidebar-primary:oklch(48.8% .243 264.376);--sidebar-primary-foreground:oklch(98.5% .001 106.423);--sidebar-accent:oklch(26.8% .007 34.298);--sidebar-accent-foreground:oklch(98.5% .001 106.423);--sidebar-border:oklch(100% 0 0/.1);--sidebar-ring:oklch(55.3% .013 58.071)}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}@keyframes enter{0%{opacity:var(--tw-enter-opacity,1);transform:translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0)scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1))rotate(var(--tw-enter-rotate,0));filter:blur(var(--tw-enter-blur,0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity,1);transform:translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0)scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1))rotate(var(--tw-exit-rotate,0));filter:blur(var(--tw-exit-blur,0))}}@keyframes accordion-down{0%{height:0}to{height:var(--radix-accordion-content-height,var(--bits-accordion-content-height,var(--reka-accordion-content-height,var(--kb-accordion-content-height,var(--ngp-accordion-content-height,auto)))))}}@keyframes accordion-up{0%{height:var(--radix-accordion-content-height,var(--bits-accordion-content-height,var(--reka-accordion-content-height,var(--kb-accordion-content-height,var(--ngp-accordion-content-height,auto)))))}to{height:0}} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:"";--tw-outline-style:solid;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--ui-font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--ui-font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--ui-color-red-500:oklch(63.7% .237 25.331);--ui-color-yellow-500:oklch(79.5% .184 86.047);--ui-color-green-500:oklch(72.3% .219 149.579);--ui-color-black:#000;--ui-color-white:#fff;--ui-spacing:.25rem;--ui-container-sm:24rem;--ui-container-lg:32rem;--ui-container-3xl:48rem;--ui-text-xs:.75rem;--ui-text-xs--line-height:calc(1/.75);--ui-text-sm:.875rem;--ui-text-sm--line-height:calc(1.25/.875);--ui-text-base:1rem;--ui-text-base--line-height:calc(1.5/1);--ui-text-lg:1.125rem;--ui-text-lg--line-height:calc(1.75/1.125);--ui-text-2xl:1.5rem;--ui-text-2xl--line-height:calc(2/1.5);--ui-font-weight-normal:400;--ui-font-weight-medium:500;--ui-font-weight-semibold:600;--ui-tracking-tight:-.025em;--ui-tracking-widest:.1em;--ui-leading-snug:1.375;--ui-leading-normal:1.5;--ui-leading-relaxed:1.625;--ui-radius-xs:.125rem;--ui-ease-in-out:cubic-bezier(.4,0,.2,1);--ui-animate-spin:spin 1s linear infinite;--ui-animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--ui-blur-sm:8px;--ui-default-transition-duration:.15s;--ui-default-transition-timing-function:cubic-bezier(.4,0,.2,1);--ui-default-mono-font-family:var(--ui-font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--font-cio,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--ui-default-font-feature-settings,normal);font-variation-settings:var(--ui-default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--ui-default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--ui-default-mono-font-feature-settings,normal);font-variation-settings:var(--ui-default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--border);outline-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){*{outline-color:color-mix(in oklab,var(--ring)50%,transparent)}}body{background-color:var(--background);font-family:var(--font-sans);color:var(--foreground)}button,input,select,optgroup,textarea{font-family:var(--font-sans)}::file-selector-button{font-family:var(--font-sans)}.toaster{--normal-bg:var(--color-popover);--normal-text:var(--color-popover-foreground);--normal-border:var(--color-border)}}@layer components;@layer utilities{.ui\:\@container\/field-group{container:field-group/inline-size}.ui\:pointer-events-none{pointer-events:none}.ui\:sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.ui\:absolute{position:absolute}.ui\:fixed{position:fixed}.ui\:relative{position:relative}.ui\:inset-0{inset:calc(var(--ui-spacing)*0)}.ui\:inset-x-0{inset-inline:calc(var(--ui-spacing)*0)}.ui\:inset-y-0{inset-block:calc(var(--ui-spacing)*0)}.ui\:end-4{inset-inline-end:calc(var(--ui-spacing)*4)}.ui\:-top-2{top:calc(var(--ui-spacing)*-2)}.ui\:top-0{top:calc(var(--ui-spacing)*0)}.ui\:top-1\.5{top:calc(var(--ui-spacing)*1.5)}.ui\:top-1\/2{top:50%}.ui\:top-3\.5{top:calc(var(--ui-spacing)*3.5)}.ui\:top-4{top:calc(var(--ui-spacing)*4)}.ui\:top-\[50\%\]{top:50%}.ui\:right-0{right:calc(var(--ui-spacing)*0)}.ui\:right-1{right:calc(var(--ui-spacing)*1)}.ui\:right-2{right:calc(var(--ui-spacing)*2)}.ui\:right-3{right:calc(var(--ui-spacing)*3)}.ui\:right-4{right:calc(var(--ui-spacing)*4)}.ui\:right-9{right:calc(var(--ui-spacing)*9)}.ui\:bottom-0{bottom:calc(var(--ui-spacing)*0)}.ui\:left-0{left:calc(var(--ui-spacing)*0)}.ui\:left-1\/2{left:50%}.ui\:left-2{left:calc(var(--ui-spacing)*2)}.ui\:left-\[50\%\]{left:50%}.ui\:left-\[calc\(50\%-3rem\)\]{left:calc(50% - 3rem)}.ui\:z-10{z-index:10}.ui\:z-20{z-index:20}.ui\:z-50{z-index:50}.ui\:z-999{z-index:999}.ui\:order-first{order:-9999}.ui\:order-last{order:9999}.ui\:col-start-2{grid-column-start:2}.ui\:row-span-2{grid-row:span 2/span 2}.ui\:row-start-1{grid-row-start:1}.ui\:m-0{margin:calc(var(--ui-spacing)*0)}.ui\:m-0\!{margin:calc(var(--ui-spacing)*0)!important}.ui\:-mx-1{margin-inline:calc(var(--ui-spacing)*-1)}.ui\:mx-3\.5{margin-inline:calc(var(--ui-spacing)*3.5)}.ui\:mx-auto{margin-inline:auto}.ui\:-my-2{margin-block:calc(var(--ui-spacing)*-2)}.ui\:my-0{margin-block:calc(var(--ui-spacing)*0)}.ui\:my-0\.5{margin-block:calc(var(--ui-spacing)*.5)}.ui\:my-1{margin-block:calc(var(--ui-spacing)*1)}.ui\:my-2{margin-block:calc(var(--ui-spacing)*2)}.ui\:my-4{margin-block:calc(var(--ui-spacing)*4)}.ui\:ms-4{margin-inline-start:calc(var(--ui-spacing)*4)}.ui\:mt-1{margin-top:calc(var(--ui-spacing)*1)}.ui\:mt-3{margin-top:calc(var(--ui-spacing)*3)}.ui\:mt-4{margin-top:calc(var(--ui-spacing)*4)}.ui\:mt-auto{margin-top:auto}.ui\:mr-1{margin-right:calc(var(--ui-spacing)*1)}.ui\:mb-2{margin-bottom:calc(var(--ui-spacing)*2)}.ui\:mb-3{margin-bottom:calc(var(--ui-spacing)*3)}.ui\:ml-auto{margin-left:auto}.ui\:line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.ui\:block{display:block}.ui\:flex{display:flex}.ui\:grid{display:grid}.ui\:hidden{display:none}.ui\:inline-flex{display:inline-flex}.ui\:field-sizing-content{field-sizing:content}.ui\:size-2{width:calc(var(--ui-spacing)*2);height:calc(var(--ui-spacing)*2)}.ui\:size-2\.5{width:calc(var(--ui-spacing)*2.5);height:calc(var(--ui-spacing)*2.5)}.ui\:size-3{width:calc(var(--ui-spacing)*3);height:calc(var(--ui-spacing)*3)}.ui\:size-3\.5{width:calc(var(--ui-spacing)*3.5);height:calc(var(--ui-spacing)*3.5)}.ui\:size-4{width:calc(var(--ui-spacing)*4);height:calc(var(--ui-spacing)*4)}.ui\:size-6{width:calc(var(--ui-spacing)*6);height:calc(var(--ui-spacing)*6)}.ui\:size-7{width:calc(var(--ui-spacing)*7);height:calc(var(--ui-spacing)*7)}.ui\:size-8{width:calc(var(--ui-spacing)*8);height:calc(var(--ui-spacing)*8)}.ui\:size-9{width:calc(var(--ui-spacing)*9);height:calc(var(--ui-spacing)*9)}.ui\:size-10{width:calc(var(--ui-spacing)*10);height:calc(var(--ui-spacing)*10)}.ui\:size-fit{width:fit-content;height:fit-content}.ui\:size-full{width:100%;height:100%}.ui\:h-\(--bits-select-anchor-height\){height:var(--bits-select-anchor-height)}.ui\:h-2{height:calc(var(--ui-spacing)*2)}.ui\:h-4{height:calc(var(--ui-spacing)*4)}.ui\:h-5{height:calc(var(--ui-spacing)*5)}.ui\:h-6{height:calc(var(--ui-spacing)*6)}.ui\:h-7{height:calc(var(--ui-spacing)*7)}.ui\:h-8{height:calc(var(--ui-spacing)*8)}.ui\:h-9{height:calc(var(--ui-spacing)*9)}.ui\:h-10{height:calc(var(--ui-spacing)*10)}.ui\:h-12{height:calc(var(--ui-spacing)*12)}.ui\:h-96{height:calc(var(--ui-spacing)*96)}.ui\:h-128{height:calc(var(--ui-spacing)*128)}.ui\:h-\[1\.2rem\]{height:1.2rem}.ui\:h-\[1\.15rem\]{height:1.15rem}.ui\:h-\[6px\]{height:6px}.ui\:h-\[70px\]{height:70px}.ui\:h-\[calc\(100\%-1px\)\]{height:calc(100% - 1px)}.ui\:h-auto{height:auto}.ui\:h-fit{height:fit-content}.ui\:h-full{height:100%}.ui\:h-px{height:1px}.ui\:max-h-\(--bits-dropdown-menu-content-available-height\){max-height:var(--bits-dropdown-menu-content-available-height)}.ui\:max-h-\(--bits-select-content-available-height\){max-height:var(--bits-select-content-available-height)}.ui\:max-h-2\/4{max-height:50%}.ui\:max-h-80{max-height:calc(var(--ui-spacing)*80)}.ui\:max-h-\[300px\]{max-height:300px}.ui\:max-h-svh{max-height:100svh}.ui\:min-h-0{min-height:calc(var(--ui-spacing)*0)}.ui\:min-h-16{min-height:calc(var(--ui-spacing)*16)}.ui\:min-h-\[calc\(100vh-48px\)\]{min-height:calc(100vh - 48px)}.ui\:w-\(--sidebar-width\){width:var(--sidebar-width)}.ui\:w-0{width:calc(var(--ui-spacing)*0)}.ui\:w-1{width:calc(var(--ui-spacing)*1)}.ui\:w-1\/4{width:25%}.ui\:w-3\/4{width:75%}.ui\:w-4{width:calc(var(--ui-spacing)*4)}.ui\:w-5{width:calc(var(--ui-spacing)*5)}.ui\:w-8{width:calc(var(--ui-spacing)*8)}.ui\:w-40{width:calc(var(--ui-spacing)*40)}.ui\:w-64{width:calc(var(--ui-spacing)*64)}.ui\:w-72{width:calc(var(--ui-spacing)*72)}.ui\:w-\[1\.2rem\]{width:1.2rem}.ui\:w-\[25px\]{width:25px}.ui\:w-\[100px\]{width:100px}.ui\:w-fit{width:fit-content}.ui\:w-full{width:100%}.ui\:max-w-\(--skeleton-width\){max-width:var(--skeleton-width)}.ui\:max-w-3xl{max-width:var(--ui-container-3xl)}.ui\:max-w-6{max-width:calc(var(--ui-spacing)*6)}.ui\:max-w-80{max-width:calc(var(--ui-spacing)*80)}.ui\:max-w-\[calc\(100\%-2rem\)\]{max-width:calc(100% - 2rem)}.ui\:max-w-sm{max-width:var(--ui-container-sm)}.ui\:min-w-\(--bits-select-anchor-width\){min-width:var(--bits-select-anchor-width)}.ui\:min-w-0{min-width:calc(var(--ui-spacing)*0)}.ui\:min-w-5{min-width:calc(var(--ui-spacing)*5)}.ui\:min-w-8{min-width:calc(var(--ui-spacing)*8)}.ui\:min-w-9{min-width:calc(var(--ui-spacing)*9)}.ui\:min-w-10{min-width:calc(var(--ui-spacing)*10)}.ui\:min-w-32{min-width:calc(var(--ui-spacing)*32)}.ui\:min-w-48{min-width:calc(var(--ui-spacing)*48)}.ui\:min-w-\[9rem\]{min-width:9rem}.ui\:flex-1{flex:1}.ui\:shrink-0{flex-shrink:0}.ui\:basis-full{flex-basis:100%}.ui\:origin-\(--bits-dropdown-menu-content-transform-origin\){transform-origin:var(--bits-dropdown-menu-content-transform-origin)}.ui\:origin-\(--bits-menubar-content-transform-origin\){transform-origin:var(--bits-menubar-content-transform-origin)}.ui\:origin-\(--bits-popover-content-transform-origin\){transform-origin:var(--bits-popover-content-transform-origin)}.ui\:origin-\(--bits-select-content-transform-origin\){transform-origin:var(--bits-select-content-transform-origin)}.ui\:origin-\(--bits-tooltip-content-transform-origin\){transform-origin:var(--bits-tooltip-content-transform-origin)}.ui\:-translate-x-1\/2{--tw-translate-x:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:-translate-x-px{--tw-translate-x:-1px;translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:translate-x-\[-50\%\]{--tw-translate-x:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:translate-x-px{--tw-translate-x:1px;translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:-translate-y-1\/2{--tw-translate-y:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:translate-y-0\.5{--tw-translate-y:calc(var(--ui-spacing)*.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:translate-y-\[-50\%\]{--tw-translate-y:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:scale-0{--tw-scale-x:0%;--tw-scale-y:0%;--tw-scale-z:0%;scale:var(--tw-scale-x)var(--tw-scale-y)}.ui\:scale-100{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.ui\:rotate-0{rotate:none}.ui\:rotate-45{rotate:45deg}.ui\:rotate-90{rotate:90deg}.ui\:animate-in{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.ui\:animate-pulse{animation:var(--ui-animate-pulse)}.ui\:animate-spin{animation:var(--ui-animate-spin)}.ui\:cursor-auto{cursor:auto}.ui\:cursor-col-resize{cursor:col-resize}.ui\:cursor-default{cursor:default}.ui\:cursor-pointer{cursor:pointer}.ui\:cursor-text{cursor:text}.ui\:resize-none{resize:none}.ui\:scroll-my-1{scroll-margin-block:calc(var(--ui-spacing)*1)}.ui\:scroll-py-1{scroll-padding-block:calc(var(--ui-spacing)*1)}.ui\:list-disc{list-style-type:disc}.ui\:auto-rows-min{grid-auto-rows:min-content}.ui\:grid-rows-\[auto_auto\]{grid-template-rows:auto auto}.ui\:flex-col{flex-direction:column}.ui\:flex-col-reverse{flex-direction:column-reverse}.ui\:flex-row{flex-direction:row}.ui\:flex-wrap{flex-wrap:wrap}.ui\:place-items-center{place-items:center}.ui\:items-center{align-items:center}.ui\:items-end{align-items:flex-end}.ui\:items-start{align-items:flex-start}.ui\:items-stretch{align-items:stretch}.ui\:justify-between{justify-content:space-between}.ui\:justify-center{justify-content:center}.ui\:justify-end{justify-content:flex-end}.ui\:justify-start{justify-content:flex-start}.ui\:gap-0{gap:calc(var(--ui-spacing)*0)}.ui\:gap-0\.5{gap:calc(var(--ui-spacing)*.5)}.ui\:gap-1{gap:calc(var(--ui-spacing)*1)}.ui\:gap-1\.5{gap:calc(var(--ui-spacing)*1.5)}.ui\:gap-2{gap:calc(var(--ui-spacing)*2)}.ui\:gap-2\.5{gap:calc(var(--ui-spacing)*2.5)}.ui\:gap-3{gap:calc(var(--ui-spacing)*3)}.ui\:gap-4{gap:calc(var(--ui-spacing)*4)}.ui\:gap-6{gap:calc(var(--ui-spacing)*6)}.ui\:gap-7{gap:calc(var(--ui-spacing)*7)}.ui\:self-start{align-self:flex-start}.ui\:self-stretch{align-self:stretch}.ui\:justify-self-end{justify-self:flex-end}.ui\:overflow-auto{overflow:auto}.ui\:overflow-hidden{overflow:hidden}.ui\:overflow-visible{overflow:visible}.ui\:overflow-x-auto{overflow-x:auto}.ui\:overflow-x-hidden{overflow-x:hidden}.ui\:overflow-y-auto{overflow-y:auto}.ui\:scroll-smooth{scroll-behavior:smooth}.ui\:rounded{border-radius:.25rem}.ui\:rounded-\[2px\]{border-radius:2px}.ui\:rounded-\[4px\]{border-radius:4px}.ui\:rounded-\[calc\(var\(--radius\)-5px\)\]{border-radius:calc(var(--radius) - 5px)}.ui\:rounded-full{border-radius:3.40282e38px}.ui\:rounded-lg{border-radius:var(--radius)}.ui\:rounded-md{border-radius:calc(var(--radius) - 2px)}.ui\:rounded-none{border-radius:0}.ui\:rounded-sm{border-radius:calc(var(--radius) - 4px)}.ui\:rounded-xl{border-radius:calc(var(--radius) + 4px)}.ui\:rounded-xs{border-radius:var(--ui-radius-xs)}.ui\:border{border-style:var(--tw-border-style);border-width:1px}.ui\:border-0{border-style:var(--tw-border-style);border-width:0}.ui\:border-2{border-style:var(--tw-border-style);border-width:2px}.ui\:border-\[1\.5px\]{border-style:var(--tw-border-style);border-width:1.5px}.ui\:border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.ui\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.ui\:border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.ui\:border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.ui\:border-dashed{--tw-border-style:dashed;border-style:dashed}.ui\:border-\(--color-border\){border-color:var(--color-border)}.ui\:border-border,.ui\:border-border\/50{border-color:var(--border)}@supports (color:color-mix(in lab, red, red)){.ui\:border-border\/50{border-color:color-mix(in oklab,var(--border)50%,transparent)}}.ui\:border-input{border-color:var(--input)}.ui\:border-sidebar-border{border-color:var(--sidebar-border)}.ui\:border-transparent{border-color:#0000}.ui\:bg-\(--color-bg\){background-color:var(--color-bg)}.ui\:bg-accent{background-color:var(--accent)}.ui\:bg-background,.ui\:bg-background\/50{background-color:var(--background)}@supports (color:color-mix(in lab, red, red)){.ui\:bg-background\/50{background-color:color-mix(in oklab,var(--background)50%,transparent)}}.ui\:bg-black\/50{background-color:var(--ui-color-black)}@supports (color:color-mix(in lab, red, red)){.ui\:bg-black\/50{background-color:color-mix(in oklab,var(--ui-color-black)50%,transparent)}}.ui\:bg-border{background-color:var(--border)}.ui\:bg-card{background-color:var(--card)}.ui\:bg-destructive{background-color:var(--destructive)}.ui\:bg-green-500{background-color:var(--ui-color-green-500)}.ui\:bg-inherit{background-color:inherit}.ui\:bg-input{background-color:var(--input)}.ui\:bg-muted,.ui\:bg-muted\/50{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.ui\:bg-muted\/50{background-color:color-mix(in oklab,var(--muted)50%,transparent)}}.ui\:bg-popover{background-color:var(--popover)}.ui\:bg-primary,.ui\:bg-primary\/20{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.ui\:bg-primary\/20{background-color:color-mix(in oklab,var(--primary)20%,transparent)}}.ui\:bg-red-500{background-color:var(--ui-color-red-500)}.ui\:bg-secondary,.ui\:bg-secondary\/50{background-color:var(--secondary)}@supports (color:color-mix(in lab, red, red)){.ui\:bg-secondary\/50{background-color:color-mix(in oklab,var(--secondary)50%,transparent)}}.ui\:bg-sidebar{background-color:var(--sidebar)}.ui\:bg-sidebar-border{background-color:var(--sidebar-border)}.ui\:bg-transparent{background-color:#0000}.ui\:bg-yellow-500{background-color:var(--ui-color-yellow-500)}.ui\:bg-clip-padding{background-clip:padding-box}.ui\:fill-current{fill:currentColor}.ui\:fill-primary{fill:var(--primary)}.ui\:object-cover{object-fit:cover}.ui\:p-0{padding:calc(var(--ui-spacing)*0)}.ui\:p-0\.5{padding:calc(var(--ui-spacing)*.5)}.ui\:p-1{padding:calc(var(--ui-spacing)*1)}.ui\:p-2{padding:calc(var(--ui-spacing)*2)}.ui\:p-4{padding:calc(var(--ui-spacing)*4)}.ui\:p-6{padding:calc(var(--ui-spacing)*6)}.ui\:p-\[3px\]{padding:3px}.ui\:px-1{padding-inline:calc(var(--ui-spacing)*1)}.ui\:px-1\.5{padding-inline:calc(var(--ui-spacing)*1.5)}.ui\:px-2{padding-inline:calc(var(--ui-spacing)*2)}.ui\:px-2\.5{padding-inline:calc(var(--ui-spacing)*2.5)}.ui\:px-3{padding-inline:calc(var(--ui-spacing)*3)}.ui\:px-4{padding-inline:calc(var(--ui-spacing)*4)}.ui\:px-6{padding-inline:calc(var(--ui-spacing)*6)}.ui\:py-0\.5{padding-block:calc(var(--ui-spacing)*.5)}.ui\:py-1{padding-block:calc(var(--ui-spacing)*1)}.ui\:py-1\.5{padding-block:calc(var(--ui-spacing)*1.5)}.ui\:py-2{padding-block:calc(var(--ui-spacing)*2)}.ui\:py-3{padding-block:calc(var(--ui-spacing)*3)}.ui\:py-4{padding-block:calc(var(--ui-spacing)*4)}.ui\:py-6{padding-block:calc(var(--ui-spacing)*6)}.ui\:ps-3{padding-inline-start:calc(var(--ui-spacing)*3)}.ui\:pe-3{padding-inline-end:calc(var(--ui-spacing)*3)}.ui\:pt-0{padding-top:calc(var(--ui-spacing)*0)}.ui\:pt-1\.5{padding-top:calc(var(--ui-spacing)*1.5)}.ui\:pt-3{padding-top:calc(var(--ui-spacing)*3)}.ui\:pr-2{padding-right:calc(var(--ui-spacing)*2)}.ui\:pr-8{padding-right:calc(var(--ui-spacing)*8)}.ui\:pr-9{padding-right:calc(var(--ui-spacing)*9)}.ui\:pr-18{padding-right:calc(var(--ui-spacing)*18)}.ui\:pb-3{padding-bottom:calc(var(--ui-spacing)*3)}.ui\:pb-4{padding-bottom:calc(var(--ui-spacing)*4)}.ui\:pl-2{padding-left:calc(var(--ui-spacing)*2)}.ui\:pl-3{padding-left:calc(var(--ui-spacing)*3)}.ui\:pl-8{padding-left:calc(var(--ui-spacing)*8)}.ui\:text-center{text-align:center}.ui\:text-left{text-align:left}.ui\:font-mono{font-family:var(--ui-font-mono)}.ui\:font-sans{font-family:var(--ui-font-sans)}.ui\:text-2xl{font-size:var(--ui-text-2xl);line-height:var(--tw-leading,var(--ui-text-2xl--line-height))}.ui\:text-base{font-size:var(--ui-text-base);line-height:var(--tw-leading,var(--ui-text-base--line-height))}.ui\:text-lg{font-size:var(--ui-text-lg);line-height:var(--tw-leading,var(--ui-text-lg--line-height))}.ui\:text-sm{font-size:var(--ui-text-sm);line-height:var(--tw-leading,var(--ui-text-sm--line-height))}.ui\:text-sm\/relaxed{font-size:var(--ui-text-sm);line-height:var(--ui-leading-relaxed)}.ui\:text-xs{font-size:var(--ui-text-xs);line-height:var(--tw-leading,var(--ui-text-xs--line-height))}.ui\:leading-none{--tw-leading:1;line-height:1}.ui\:leading-normal{--tw-leading:var(--ui-leading-normal);line-height:var(--ui-leading-normal)}.ui\:leading-snug{--tw-leading:var(--ui-leading-snug);line-height:var(--ui-leading-snug)}.ui\:font-medium{--tw-font-weight:var(--ui-font-weight-medium);font-weight:var(--ui-font-weight-medium)}.ui\:font-normal{--tw-font-weight:var(--ui-font-weight-normal);font-weight:var(--ui-font-weight-normal)}.ui\:font-semibold{--tw-font-weight:var(--ui-font-weight-semibold);font-weight:var(--ui-font-weight-semibold)}.ui\:tracking-tight{--tw-tracking:var(--ui-tracking-tight);letter-spacing:var(--ui-tracking-tight)}.ui\:tracking-widest{--tw-tracking:var(--ui-tracking-widest);letter-spacing:var(--ui-tracking-widest)}.ui\:text-balance{text-wrap:balance}.ui\:break-words{overflow-wrap:break-word}.ui\:text-ellipsis{text-overflow:ellipsis}.ui\:whitespace-nowrap{white-space:nowrap}.ui\:text-card-foreground{color:var(--card-foreground)}.ui\:text-current{color:currentColor}.ui\:text-destructive{color:var(--destructive)}.ui\:text-foreground{color:var(--foreground)}.ui\:text-green-500{color:var(--ui-color-green-500)}.ui\:text-muted-foreground{color:var(--muted-foreground)}.ui\:text-popover-foreground{color:var(--popover-foreground)}.ui\:text-primary{color:var(--primary)}.ui\:text-primary-foreground{color:var(--primary-foreground)}.ui\:text-secondary-foreground{color:var(--secondary-foreground)}.ui\:text-sidebar-foreground,.ui\:text-sidebar-foreground\/70{color:var(--sidebar-foreground)}@supports (color:color-mix(in lab, red, red)){.ui\:text-sidebar-foreground\/70{color:color-mix(in oklab,var(--sidebar-foreground)70%,transparent)}}.ui\:text-white{color:var(--ui-color-white)}.ui\:underline-offset-4{text-underline-offset:4px}.ui\:opacity-0{opacity:0}.ui\:opacity-50{opacity:.5}.ui\:opacity-70{opacity:.7}.ui\:shadow-2xs{--tw-shadow:0 1px var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:shadow-\[0_0_0_1px_var\(--sidebar-border\)\]{--tw-shadow:0 0 0 1px var(--tw-shadow-color,var(--sidebar-border));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:ring-0{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:ring-3{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(3px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:ring-background{--tw-ring-color:var(--background)}.ui\:ring-sidebar-ring{--tw-ring-color:var(--sidebar-ring)}.ui\:ring-offset-background{--tw-ring-offset-color:var(--background)}.ui\:outline-hidden{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.ui\:outline-hidden{outline-offset:2px;outline:2px solid #0000}}.ui\:backdrop-blur-sm{--tw-backdrop-blur:blur(var(--ui-blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.ui\:transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-\[color\,box-shadow\]{transition-property:color,box-shadow;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-\[left\,right\,width\]{transition-property:left,right,width;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-\[margin\,opacity\]{transition-property:margin,opacity;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-\[width\,height\,padding\]{transition-property:width,height,padding;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}.ui\:transition-none{transition-property:none}.ui\:duration-100{--tw-duration:.1s;transition-duration:.1s}.ui\:duration-200{--tw-duration:.2s;transition-duration:.2s}.ui\:duration-500{--tw-duration:.5s;transition-duration:.5s}.ui\:ease-in-out{--tw-ease:var(--ui-ease-in-out);transition-timing-function:var(--ui-ease-in-out)}.ui\:ease-linear{--tw-ease:linear;transition-timing-function:linear}.ui\:fade-in-0{--tw-enter-opacity:0}.ui\:outline-none{--tw-outline-style:none;outline-style:none}.ui\:select-none{-webkit-user-select:none;user-select:none}.ui\:zoom-in-95{--tw-enter-scale:.95}:is(.ui\:\*\:w-full>*){width:100%}.ui\:group-has-data-\[orientation\=horizontal\]\/field\:text-balance:is(:where(.ui\:group\/field):has([data-orientation=horizontal]) *){text-wrap:balance}.ui\:group-has-data-\[sidebar\=menu-action\]\/menu-item\:pr-8:is(:where(.ui\:group\/menu-item):has([data-sidebar=menu-action]) *){padding-right:calc(var(--ui-spacing)*8)}.ui\:group-has-\[\[data-slot\=item-description\]\]\/item\:translate-y-0\.5:is(:where(.ui\:group\/item):has([data-slot=item-description]) *){--tw-translate-y:calc(var(--ui-spacing)*.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:group-has-\[\[data-slot\=item-description\]\]\/item\:self-start:is(:where(.ui\:group\/item):has([data-slot=item-description]) *){align-self:flex-start}.ui\:group-has-\[\>input\]\/input-group\:pt-2\.5:is(:where(.ui\:group\/input-group):has(>input) *){padding-top:calc(var(--ui-spacing)*2.5)}.ui\:group-has-\[\>input\]\/input-group\:pb-2\.5:is(:where(.ui\:group\/input-group):has(>input) *){padding-bottom:calc(var(--ui-spacing)*2.5)}.ui\:group-data-\[collapsible\=icon\]\:-mt-8:is(:where(.ui\:group)[data-collapsible=icon] *){margin-top:calc(var(--ui-spacing)*-8)}.ui\:group-data-\[collapsible\=icon\]\:hidden:is(:where(.ui\:group)[data-collapsible=icon] *){display:none}.ui\:group-data-\[collapsible\=icon\]\:size-8\!:is(:where(.ui\:group)[data-collapsible=icon] *){width:calc(var(--ui-spacing)*8)!important;height:calc(var(--ui-spacing)*8)!important}.ui\:group-data-\[collapsible\=icon\]\:w-\(--sidebar-width-icon\):is(:where(.ui\:group)[data-collapsible=icon] *){width:var(--sidebar-width-icon)}.ui\:group-data-\[collapsible\=icon\]\:w-\[calc\(var\(--sidebar-width-icon\)\+\(--spacing\(4\)\)\+2px\)\]:is(:where(.ui\:group)[data-collapsible=icon] *){width:calc(var(--sidebar-width-icon) + (calc(var(--ui-spacing)*4)) + 2px)}.ui\:group-data-\[collapsible\=icon\]\:overflow-hidden:is(:where(.ui\:group)[data-collapsible=icon] *){overflow:hidden}.ui\:group-data-\[collapsible\=icon\]\:p-0\!:is(:where(.ui\:group)[data-collapsible=icon] *){padding:calc(var(--ui-spacing)*0)!important}.ui\:group-data-\[collapsible\=icon\]\:p-2\!:is(:where(.ui\:group)[data-collapsible=icon] *){padding:calc(var(--ui-spacing)*2)!important}.ui\:group-data-\[collapsible\=icon\]\:opacity-0:is(:where(.ui\:group)[data-collapsible=icon] *){opacity:0}.ui\:group-data-\[collapsible\=offcanvas\]\:right-\[calc\(var\(--sidebar-width\)\*-1\)\]:is(:where(.ui\:group)[data-collapsible=offcanvas] *){right:calc(var(--sidebar-width)*-1)}.ui\:group-data-\[collapsible\=offcanvas\]\:left-\[calc\(var\(--sidebar-width\)\*-1\)\]:is(:where(.ui\:group)[data-collapsible=offcanvas] *){left:calc(var(--sidebar-width)*-1)}.ui\:group-data-\[collapsible\=offcanvas\]\:w-0:is(:where(.ui\:group)[data-collapsible=offcanvas] *){width:calc(var(--ui-spacing)*0)}.ui\:group-data-\[collapsible\=offcanvas\]\:translate-x-0:is(:where(.ui\:group)[data-collapsible=offcanvas] *){--tw-translate-x:calc(var(--ui-spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:group-data-\[disabled\=true\]\:pointer-events-none:is(:where(.ui\:group)[data-disabled=true] *){pointer-events:none}.ui\:group-data-\[disabled\=true\]\:opacity-50:is(:where(.ui\:group)[data-disabled=true] *),.ui\:group-data-\[disabled\=true\]\/field\:opacity-50:is(:where(.ui\:group\/field)[data-disabled=true] *),.ui\:group-data-\[disabled\=true\]\/input-group\:opacity-50:is(:where(.ui\:group\/input-group)[data-disabled=true] *){opacity:.5}.ui\:group-data-\[side\=left\]\:-right-4:is(:where(.ui\:group)[data-side=left] *){right:calc(var(--ui-spacing)*-4)}.ui\:group-data-\[side\=left\]\:border-r:is(:where(.ui\:group)[data-side=left] *){border-right-style:var(--tw-border-style);border-right-width:1px}.ui\:group-data-\[side\=right\]\:left-0:is(:where(.ui\:group)[data-side=right] *){left:calc(var(--ui-spacing)*0)}.ui\:group-data-\[side\=right\]\:rotate-180:is(:where(.ui\:group)[data-side=right] *){rotate:180deg}.ui\:group-data-\[side\=right\]\:border-l:is(:where(.ui\:group)[data-side=right] *){border-left-style:var(--tw-border-style);border-left-width:1px}.ui\:group-data-\[variant\=floating\]\:rounded-lg:is(:where(.ui\:group)[data-variant=floating] *){border-radius:var(--radius)}.ui\:group-data-\[variant\=floating\]\:border:is(:where(.ui\:group)[data-variant=floating] *){border-style:var(--tw-border-style);border-width:1px}.ui\:group-data-\[variant\=floating\]\:border-sidebar-border:is(:where(.ui\:group)[data-variant=floating] *){border-color:var(--sidebar-border)}.ui\:group-data-\[variant\=floating\]\:shadow-sm:is(:where(.ui\:group)[data-variant=floating] *){--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:group-data-\[variant\=outline\]\/field-group\:-mb-2:is(:where(.ui\:group\/field-group)[data-variant=outline] *){margin-bottom:calc(var(--ui-spacing)*-2)}.ui\:group-data-\[vaul-drawer-direction\=bottom\]\/drawer-content\:block:is(:where(.ui\:group\/drawer-content)[data-vaul-drawer-direction=bottom] *){display:block}@media (hover:hover){.ui\:peer-hover\/menu-button\:text-sidebar-accent-foreground:is(:where(.ui\:peer\/menu-button):hover~*){color:var(--sidebar-accent-foreground)}}.ui\:peer-disabled\:cursor-not-allowed:is(:where(.ui\:peer):disabled~*){cursor:not-allowed}.ui\:peer-disabled\:opacity-50:is(:where(.ui\:peer):disabled~*){opacity:.5}.ui\:peer-data-\[active\=true\]\/menu-button\:text-sidebar-accent-foreground:is(:where(.ui\:peer\/menu-button)[data-active=true]~*){color:var(--sidebar-accent-foreground)}.ui\:peer-data-\[size\=default\]\/menu-button\:top-1\.5:is(:where(.ui\:peer\/menu-button)[data-size=default]~*){top:calc(var(--ui-spacing)*1.5)}.ui\:peer-data-\[size\=lg\]\/menu-button\:top-2\.5:is(:where(.ui\:peer\/menu-button)[data-size=lg]~*){top:calc(var(--ui-spacing)*2.5)}.ui\:peer-data-\[size\=sm\]\/menu-button\:top-1:is(:where(.ui\:peer\/menu-button)[data-size=sm]~*){top:calc(var(--ui-spacing)*1)}.ui\:selection\:bg-primary ::selection{background-color:var(--primary)}.ui\:selection\:bg-primary::selection{background-color:var(--primary)}.ui\:selection\:text-primary-foreground ::selection{color:var(--primary-foreground)}.ui\:selection\:text-primary-foreground::selection{color:var(--primary-foreground)}.ui\:placeholder\:text-muted-foreground::placeholder{color:var(--muted-foreground)}.ui\:after\:absolute:after{content:var(--tw-content);position:absolute}.ui\:after\:-inset-2:after{content:var(--tw-content);inset:calc(var(--ui-spacing)*-2)}.ui\:after\:inset-y-0:after{content:var(--tw-content);inset-block:calc(var(--ui-spacing)*0)}.ui\:after\:left-\[calc\(1\/2\*100\%-1px\)\]:after{content:var(--tw-content);left:calc(50% - 1px)}.ui\:after\:w-\[2px\]:after{content:var(--tw-content);width:2px}.ui\:group-data-\[collapsible\=offcanvas\]\:after\:left-full:is(:where(.ui\:group)[data-collapsible=offcanvas] *):after{content:var(--tw-content);left:100%}.ui\:last\:mt-0:last-child{margin-top:calc(var(--ui-spacing)*0)}.ui\:last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (hover:hover){.ui\:hover\:bg-accent:hover{background-color:var(--accent)}.ui\:hover\:bg-muted:hover{background-color:var(--muted)}.ui\:hover\:bg-sidebar-accent:hover{background-color:var(--sidebar-accent)}.ui\:hover\:bg-transparent\!:hover{background-color:#0000!important}.ui\:hover\:text-accent-foreground:hover{color:var(--accent-foreground)}.ui\:hover\:text-foreground:hover{color:var(--foreground)}.ui\:hover\:text-muted-foreground:hover{color:var(--muted-foreground)}.ui\:hover\:text-sidebar-accent-foreground:hover{color:var(--sidebar-accent-foreground)}.ui\:hover\:underline:hover{text-decoration-line:underline}.ui\:hover\:opacity-100:hover{opacity:1}.ui\:hover\:shadow-\[0_0_0_1px_var\(--sidebar-accent\)\]:hover{--tw-shadow:0 0 0 1px var(--tw-shadow-color,var(--sidebar-accent));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:hover\:group-data-\[collapsible\=offcanvas\]\:bg-sidebar:hover:is(:where(.ui\:group)[data-collapsible=offcanvas] *){background-color:var(--sidebar)}.ui\:hover\:after\:bg-sidebar-border:hover:after{content:var(--tw-content);background-color:var(--sidebar-border)}}.ui\:focus\:bg-accent:focus{background-color:var(--accent)}.ui\:focus\:text-accent-foreground:focus{color:var(--accent-foreground)}.ui\:focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:focus\:ring-ring:focus{--tw-ring-color:var(--ring)}.ui\:focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ui\:focus\:outline-hidden:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.ui\:focus\:outline-hidden:focus{outline-offset:2px;outline:2px solid #0000}}.ui\:focus-visible\:border-ring:focus-visible{border-color:var(--ring)}.ui\:focus-visible\:ring-0:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:focus-visible\:ring-\[3px\]:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(3px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.ui\:focus-visible\:ring-ring:focus-visible,.ui\:focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){.ui\:focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:color-mix(in oklab,var(--ring)50%,transparent)}}.ui\:focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ui\:focus-visible\:outline-hidden:focus-visible{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.ui\:focus-visible\:outline-hidden:focus-visible{outline-offset:2px;outline:2px solid #0000}}.ui\:focus-visible\:outline-1:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.ui\:focus-visible\:outline-ring:focus-visible{outline-color:var(--ring)}.ui\:active\:bg-sidebar-accent:active{background-color:var(--sidebar-accent)}.ui\:active\:text-sidebar-accent-foreground:active{color:var(--sidebar-accent-foreground)}.ui\:disabled\:pointer-events-none:disabled{pointer-events:none}.ui\:disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.ui\:disabled\:opacity-50:disabled{opacity:.5}:where([data-side=left]) .ui\:in-data-\[side\=left\]\:cursor-w-resize{cursor:w-resize}:where([data-side=right]) .ui\:in-data-\[side\=right\]\:cursor-e-resize{cursor:e-resize}:where([data-slot=tooltip-content]) .ui\:in-data-\[slot\=tooltip-content\]\:bg-background\/20{background-color:var(--background)}@supports (color:color-mix(in lab, red, red)){:where([data-slot=tooltip-content]) .ui\:in-data-\[slot\=tooltip-content\]\:bg-background\/20{background-color:color-mix(in oklab,var(--background)20%,transparent)}}:where([data-slot=tooltip-content]) .ui\:in-data-\[slot\=tooltip-content\]\:text-background{color:var(--background)}.ui\:has-data-\[slot\=card-action\]\:grid-cols-\[1fr_auto\]:has([data-slot=card-action]){grid-template-columns:1fr auto}.ui\:has-data-\[state\=checked\]\:border-primary:has([data-state=checked]){border-color:var(--primary)}.ui\:has-data-\[state\=checked\]\:bg-primary\/5:has([data-state=checked]){background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.ui\:has-data-\[state\=checked\]\:bg-primary\/5:has([data-state=checked]){background-color:color-mix(in oklab,var(--primary)5%,transparent)}}.ui\:has-data-\[variant\=inset\]\:bg-sidebar:has([data-variant=inset]){background-color:var(--sidebar)}.ui\:has-\[\[data-slot\=input-group-control\]\:focus-visible\]\:border-ring:has([data-slot=input-group-control]:focus-visible){border-color:var(--ring)}.ui\:has-\[\[data-slot\=input-group-control\]\:focus-visible\]\:ring-\[3px\]:has([data-slot=input-group-control]:focus-visible){--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(3px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:has-\[\[data-slot\=input-group-control\]\:focus-visible\]\:ring-ring\/50:has([data-slot=input-group-control]:focus-visible){--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){.ui\:has-\[\[data-slot\=input-group-control\]\:focus-visible\]\:ring-ring\/50:has([data-slot=input-group-control]:focus-visible){--tw-ring-color:color-mix(in oklab,var(--ring)50%,transparent)}}.ui\:has-\[\[data-slot\]\[aria-invalid\=true\]\]\:border-destructive:has([data-slot][aria-invalid=true]){border-color:var(--destructive)}.ui\:has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-destructive\/20:has([data-slot][aria-invalid=true]){--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-destructive\/20:has([data-slot][aria-invalid=true]){--tw-ring-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.ui\:has-\[\>\[data-align\=block-end\]\]\:h-auto:has(>[data-align=block-end]){height:auto}.ui\:has-\[\>\[data-align\=block-end\]\]\:flex-col:has(>[data-align=block-end]){flex-direction:column}.ui\:has-\[\>\[data-align\=block-start\]\]\:h-auto:has(>[data-align=block-start]){height:auto}.ui\:has-\[\>\[data-align\=block-start\]\]\:flex-col:has(>[data-align=block-start]){flex-direction:column}.ui\:has-\[\>\[data-slot\=button-group\]\]\:gap-2:has(>[data-slot=button-group]){gap:calc(var(--ui-spacing)*2)}.ui\:has-\[\>\[data-slot\=checkbox-group\]\]\:gap-3:has(>[data-slot=checkbox-group]){gap:calc(var(--ui-spacing)*3)}.ui\:has-\[\>\[data-slot\=field-content\]\]\:items-start:has(>[data-slot=field-content]){align-items:flex-start}.ui\:has-\[\>\[data-slot\=field\]\]\:w-full:has(>[data-slot=field]){width:100%}.ui\:has-\[\>\[data-slot\=field\]\]\:flex-col:has(>[data-slot=field]){flex-direction:column}.ui\:has-\[\>\[data-slot\=field\]\]\:rounded-md:has(>[data-slot=field]){border-radius:calc(var(--radius) - 2px)}.ui\:has-\[\>\[data-slot\=field\]\]\:border:has(>[data-slot=field]){border-style:var(--tw-border-style);border-width:1px}.ui\:has-\[\>\[data-slot\=radio-group\]\]\:gap-3:has(>[data-slot=radio-group]){gap:calc(var(--ui-spacing)*3)}.ui\:has-\[\>button\]\:ms-\[-0\.45rem\]:has(>button){margin-inline-start:-.45rem}.ui\:has-\[\>button\]\:me-\[-0\.45rem\]:has(>button){margin-inline-end:-.45rem}.ui\:has-\[\>kbd\]\:ms-\[-0\.35rem\]:has(>kbd){margin-inline-start:-.35rem}.ui\:has-\[\>kbd\]\:me-\[-0\.35rem\]:has(>kbd){margin-inline-end:-.35rem}.ui\:has-\[\>svg\]\:p-0:has(>svg){padding:calc(var(--ui-spacing)*0)}.ui\:has-\[\>svg\]\:px-2:has(>svg){padding-inline:calc(var(--ui-spacing)*2)}.ui\:has-\[\>svg\]\:px-2\.5:has(>svg){padding-inline:calc(var(--ui-spacing)*2.5)}.ui\:has-\[\>svg\]\:px-3:has(>svg){padding-inline:calc(var(--ui-spacing)*3)}.ui\:has-\[\>svg\]\:px-4:has(>svg){padding-inline:calc(var(--ui-spacing)*4)}.ui\:has-\[\>textarea\]\:h-auto:has(>textarea){height:auto}.ui\:aria-disabled\:pointer-events-none[aria-disabled=true]{pointer-events:none}.ui\:aria-disabled\:opacity-50[aria-disabled=true]{opacity:.5}.ui\:aria-invalid\:border-destructive[aria-invalid=true]{border-color:var(--destructive)}.ui\:aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.ui\:aria-selected\:bg-accent[aria-selected=true]{background-color:var(--accent)}.ui\:aria-selected\:text-accent-foreground[aria-selected=true]{color:var(--accent-foreground)}:is(.ui\:\*\*\:data-command-group\:px-2 *)[data-command-group]{padding-inline:calc(var(--ui-spacing)*2)}:is(.ui\:\*\*\:data-command-input\:h-12 *)[data-command-input]{height:calc(var(--ui-spacing)*12)}:is(.ui\:\*\*\:data-command-item\:px-2 *)[data-command-item]{padding-inline:calc(var(--ui-spacing)*2)}:is(.ui\:\*\*\:data-command-item\:py-3 *)[data-command-item]{padding-block:calc(var(--ui-spacing)*3)}.ui\:data-disabled\:pointer-events-none[data-disabled]{pointer-events:none}.ui\:data-disabled\:cursor-not-allowed[data-disabled]{cursor:not-allowed}.ui\:data-disabled\:opacity-50[data-disabled]{opacity:.5}.ui\:data-highlighted\:bg-accent[data-highlighted]{background-color:var(--accent)}.ui\:data-highlighted\:text-accent-foreground[data-highlighted]{color:var(--accent-foreground)}.ui\:data-inset\:pl-8[data-inset]{padding-left:calc(var(--ui-spacing)*8)}.ui\:data-placeholder\:text-muted-foreground[data-placeholder]{color:var(--muted-foreground)}.ui\:data-\[active\=true\]\:bg-sidebar-accent[data-active=true]{background-color:var(--sidebar-accent)}.ui\:data-\[active\=true\]\:font-medium[data-active=true]{--tw-font-weight:var(--ui-font-weight-medium);font-weight:var(--ui-font-weight-medium)}.ui\:data-\[active\=true\]\:text-sidebar-accent-foreground[data-active=true]{color:var(--sidebar-accent-foreground)}.ui\:data-\[current\=true\]\:bg-muted[data-current=true]{background-color:var(--muted)}.ui\:data-\[disabled\=true\]\:pointer-events-none[data-disabled=true]{pointer-events:none}.ui\:data-\[disabled\=true\]\:opacity-50[data-disabled=true]{opacity:.5}.ui\:data-\[inset\]\:pl-8[data-inset]{padding-left:calc(var(--ui-spacing)*8)}.ui\:data-\[invalid\=true\]\:text-destructive[data-invalid=true]{color:var(--destructive)}.ui\:data-\[orientation\=horizontal\]\:h-px[data-orientation=horizontal]{height:1px}.ui\:data-\[orientation\=horizontal\]\:w-full[data-orientation=horizontal]{width:100%}.ui\:data-\[orientation\=vertical\]\:h-auto[data-orientation=vertical]{height:auto}.ui\:data-\[orientation\=vertical\]\:h-full[data-orientation=vertical]{height:100%}.ui\:data-\[orientation\=vertical\]\:w-px[data-orientation=vertical]{width:1px}.ui\:data-\[side\=bottom\]\:-translate-x-1\/2[data-side=bottom]{--tw-translate-x:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=bottom\]\:-translate-y-\[calc\(-50\%\+1px\)\][data-side=bottom]{--tw-translate-y:calc(calc(-50% + 1px)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y:calc(var(--ui-spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y:calc(2*var(--spacing)*-1)}.ui\:data-\[side\=left\]\:-translate-x-1[data-side=left]{--tw-translate-x:calc(var(--ui-spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=left\]\:-translate-y-\[calc\(50\%-3px\)\][data-side=left]{--tw-translate-y:calc(calc(50% - 3px)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=left\]\:slide-in-from-end-2[data-side=left]:where(:dir(ltr),[dir=ltr]){--tw-enter-translate-x:calc(2*var(--spacing))}.ui\:data-\[side\=left\]\:slide-in-from-end-2[data-side=left]:where(:dir(rtl),[dir=rtl]){--tw-enter-translate-x:calc(2*var(--spacing)*-1)}.ui\:data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x:calc(2*var(--spacing))}.ui\:data-\[side\=right\]\:translate-x-1[data-side=right]{--tw-translate-x:calc(var(--ui-spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=right\]\:translate-x-\[calc\(50\%\+2px\)\][data-side=right]{--tw-translate-x:calc(50% + 2px);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=right\]\:translate-y-1\/2[data-side=right]{--tw-translate-y:calc(1/2*100%);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=right\]\:slide-in-from-start-2[data-side=right]:where(:dir(ltr),[dir=ltr]){--tw-enter-translate-x:calc(2*var(--spacing)*-1)}.ui\:data-\[side\=right\]\:slide-in-from-start-2[data-side=right]:where(:dir(rtl),[dir=rtl]){--tw-enter-translate-x:calc(2*var(--spacing))}.ui\:data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x:calc(2*var(--spacing)*-1)}.ui\:data-\[side\=top\]\:translate-x-1\/2[data-side=top]{--tw-translate-x:calc(1/2*100%);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y:calc(var(--ui-spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=top\]\:translate-y-\[calc\(-50\%\+2px\)\][data-side=top]{--tw-translate-y:calc(-50% + 2px);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y:calc(2*var(--spacing))}.ui\:data-\[size\=default\]\:h-9[data-size=default]{height:calc(var(--ui-spacing)*9)}.ui\:data-\[size\=sm\]\:h-8[data-size=sm]{height:calc(var(--ui-spacing)*8)}.ui\:data-\[slot\=checkbox-group\]\:gap-3[data-slot=checkbox-group]{gap:calc(var(--ui-spacing)*3)}:is(.ui\:\*\:data-\[slot\=field\]\:p-4>*)[data-slot=field]{padding:calc(var(--ui-spacing)*4)}:is(.ui\:\*\:data-\[slot\=field-group\]\:gap-4>*)[data-slot=field-group]{gap:calc(var(--ui-spacing)*4)}:is(.ui\:\*\*\:data-\[slot\=select-value\]\:line-clamp-1 *)[data-slot=select-value]{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}:is(.ui\:\*\*\:data-\[slot\=select-value\]\:flex *)[data-slot=select-value]{display:flex}:is(.ui\:\*\*\:data-\[slot\=select-value\]\:items-center *)[data-slot=select-value]{align-items:center}:is(.ui\:\*\*\:data-\[slot\=select-value\]\:gap-2 *)[data-slot=select-value]{gap:calc(var(--ui-spacing)*2)}.ui\:data-\[state\=active\]\:bg-background[data-state=active]{background-color:var(--background)}.ui\:data-\[state\=active\]\:shadow-sm[data-state=active]{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:data-\[state\=checked\]\:translate-x-\[calc\(100\%-2px\)\][data-state=checked]{--tw-translate-x:calc(100% - 2px);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[state\=checked\]\:border-primary[data-state=checked]{border-color:var(--primary)}.ui\:data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:var(--primary)}.ui\:data-\[state\=checked\]\:text-primary-foreground[data-state=checked]{color:var(--primary-foreground)}.ui\:data-\[state\=closed\]\:animate-accordion-up[data-state=closed]{animation:accordion-up var(--tw-animation-duration,var(--tw-duration,.2s))var(--tw-ease,ease-out)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.ui\:data-\[state\=closed\]\:animate-out[data-state=closed]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.ui\:data-\[state\=closed\]\:duration-300[data-state=closed]{--tw-duration:.3s;transition-duration:.3s}.ui\:data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity:0}.ui\:data-\[state\=closed\]\:zoom-out-95[data-state=closed]{--tw-exit-scale:.95}.ui\:data-\[state\=closed\]\:slide-out-to-bottom[data-state=closed]{--tw-exit-translate-y:100%}.ui\:data-\[state\=closed\]\:slide-out-to-left[data-state=closed]{--tw-exit-translate-x:-100%}.ui\:data-\[state\=closed\]\:slide-out-to-right[data-state=closed]{--tw-exit-translate-x:100%}.ui\:data-\[state\=closed\]\:slide-out-to-top[data-state=closed]{--tw-exit-translate-y:-100%}.ui\:data-\[state\=off\]\:text-muted-foreground[data-state=off]{color:var(--muted-foreground)}@media (hover:hover){.ui\:hover\:data-\[state\=off\]\:text-accent-foreground:hover[data-state=off]{color:var(--accent-foreground)}}.ui\:data-\[state\=on\]\:bg-accent[data-state=on]{background-color:var(--accent)}.ui\:data-\[state\=on\]\:bg-transparent[data-state=on]{background-color:#0000}.ui\:data-\[state\=on\]\:text-accent-foreground[data-state=on]{color:var(--accent-foreground)}.ui\:data-\[state\=on\]\:text-muted-foreground[data-state=on]{color:var(--muted-foreground)}@media (hover:hover){.ui\:hover\:data-\[state\=on\]\:text-accent-foreground:hover[data-state=on]{color:var(--accent-foreground)}}.ui\:data-\[state\=open\]\:animate-accordion-down[data-state=open]{animation:accordion-down var(--tw-animation-duration,var(--tw-duration,.2s))var(--tw-ease,ease-out)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.ui\:data-\[state\=open\]\:animate-in[data-state=open]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.ui\:data-\[state\=open\]\:bg-accent[data-state=open]{background-color:var(--accent)}.ui\:data-\[state\=open\]\:text-accent-foreground[data-state=open]{color:var(--accent-foreground)}.ui\:data-\[state\=open\]\:opacity-100[data-state=open]{opacity:1}.ui\:data-\[state\=open\]\:duration-500[data-state=open]{--tw-duration:.5s;transition-duration:.5s}.ui\:data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity:0}.ui\:data-\[state\=open\]\:zoom-in-95[data-state=open]{--tw-enter-scale:.95}.ui\:data-\[state\=open\]\:slide-in-from-bottom[data-state=open]{--tw-enter-translate-y:100%}.ui\:data-\[state\=open\]\:slide-in-from-left[data-state=open]{--tw-enter-translate-x:-100%}.ui\:data-\[state\=open\]\:slide-in-from-right[data-state=open]{--tw-enter-translate-x:100%}.ui\:data-\[state\=open\]\:slide-in-from-top[data-state=open]{--tw-enter-translate-y:-100%}@media (hover:hover){.ui\:data-\[state\=open\]\:hover\:bg-sidebar-accent[data-state=open]:hover{background-color:var(--sidebar-accent)}.ui\:data-\[state\=open\]\:hover\:text-sidebar-accent-foreground[data-state=open]:hover{color:var(--sidebar-accent-foreground)}}.ui\:data-\[state\=selected\]\:bg-muted[data-state=selected]{background-color:var(--muted)}.ui\:data-\[state\=unchecked\]\:translate-x-0[data-state=unchecked]{--tw-translate-x:calc(var(--ui-spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.ui\:data-\[state\=unchecked\]\:bg-input[data-state=unchecked]{background-color:var(--input)}.ui\:data-\[variant\=destructive\]\:text-destructive[data-variant=destructive]{color:var(--destructive)}.ui\:data-\[variant\=destructive\]\:focus\:bg-destructive\/10[data-variant=destructive]:focus{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:data-\[variant\=destructive\]\:focus\:bg-destructive\/10[data-variant=destructive]:focus{background-color:color-mix(in oklab,var(--destructive)10%,transparent)}}.ui\:data-\[variant\=destructive\]\:focus\:text-destructive[data-variant=destructive]:focus{color:var(--destructive)}.ui\:data-\[variant\=destructive\]\:data-highlighted\:bg-destructive\/10[data-variant=destructive][data-highlighted]{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:data-\[variant\=destructive\]\:data-highlighted\:bg-destructive\/10[data-variant=destructive][data-highlighted]{background-color:color-mix(in oklab,var(--destructive)10%,transparent)}}.ui\:data-\[variant\=destructive\]\:data-highlighted\:text-destructive[data-variant=destructive][data-highlighted]{color:var(--destructive)}.ui\:data-\[variant\=label\]\:text-sm[data-variant=label]{font-size:var(--ui-text-sm);line-height:var(--tw-leading,var(--ui-text-sm--line-height))}.ui\:data-\[variant\=legend\]\:text-base[data-variant=legend]{font-size:var(--ui-text-base);line-height:var(--tw-leading,var(--ui-text-base--line-height))}.ui\:data-\[vaul-drawer-direction\=bottom\]\:inset-x-0[data-vaul-drawer-direction=bottom]{inset-inline:calc(var(--ui-spacing)*0)}.ui\:data-\[vaul-drawer-direction\=bottom\]\:bottom-0[data-vaul-drawer-direction=bottom]{bottom:calc(var(--ui-spacing)*0)}.ui\:data-\[vaul-drawer-direction\=bottom\]\:mt-24[data-vaul-drawer-direction=bottom]{margin-top:calc(var(--ui-spacing)*24)}.ui\:data-\[vaul-drawer-direction\=bottom\]\:max-h-\[80vh\][data-vaul-drawer-direction=bottom]{max-height:80vh}.ui\:data-\[vaul-drawer-direction\=bottom\]\:rounded-t-lg[data-vaul-drawer-direction=bottom]{border-top-left-radius:var(--radius);border-top-right-radius:var(--radius)}.ui\:data-\[vaul-drawer-direction\=bottom\]\:border-t[data-vaul-drawer-direction=bottom]{border-top-style:var(--tw-border-style);border-top-width:1px}.ui\:data-\[vaul-drawer-direction\=left\]\:inset-y-0[data-vaul-drawer-direction=left]{inset-block:calc(var(--ui-spacing)*0)}.ui\:data-\[vaul-drawer-direction\=left\]\:start-0[data-vaul-drawer-direction=left]{inset-inline-start:calc(var(--ui-spacing)*0)}.ui\:data-\[vaul-drawer-direction\=left\]\:w-3\/4[data-vaul-drawer-direction=left]{width:75%}.ui\:data-\[vaul-drawer-direction\=left\]\:border-e[data-vaul-drawer-direction=left]{border-inline-end-style:var(--tw-border-style);border-inline-end-width:1px}.ui\:data-\[vaul-drawer-direction\=right\]\:inset-y-0[data-vaul-drawer-direction=right]{inset-block:calc(var(--ui-spacing)*0)}.ui\:data-\[vaul-drawer-direction\=right\]\:end-0[data-vaul-drawer-direction=right]{inset-inline-end:calc(var(--ui-spacing)*0)}.ui\:data-\[vaul-drawer-direction\=right\]\:w-3\/4[data-vaul-drawer-direction=right]{width:75%}.ui\:data-\[vaul-drawer-direction\=right\]\:border-s[data-vaul-drawer-direction=right]{border-inline-start-style:var(--tw-border-style);border-inline-start-width:1px}.ui\:data-\[vaul-drawer-direction\=top\]\:inset-x-0[data-vaul-drawer-direction=top]{inset-inline:calc(var(--ui-spacing)*0)}.ui\:data-\[vaul-drawer-direction\=top\]\:top-0[data-vaul-drawer-direction=top]{top:calc(var(--ui-spacing)*0)}.ui\:data-\[vaul-drawer-direction\=top\]\:mb-24[data-vaul-drawer-direction=top]{margin-bottom:calc(var(--ui-spacing)*24)}.ui\:data-\[vaul-drawer-direction\=top\]\:max-h-\[80vh\][data-vaul-drawer-direction=top]{max-height:80vh}.ui\:data-\[vaul-drawer-direction\=top\]\:rounded-b-lg[data-vaul-drawer-direction=top]{border-bottom-right-radius:var(--radius);border-bottom-left-radius:var(--radius)}.ui\:data-\[vaul-drawer-direction\=top\]\:border-b[data-vaul-drawer-direction=top]{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.ui\:nth-last-2\:-mt-1:nth-last-child(2){margin-top:calc(var(--ui-spacing)*-1)}@media (min-width:40rem){.ui\:sm\:flex{display:flex}.ui\:sm\:max-w-lg{max-width:var(--ui-container-lg)}.ui\:sm\:max-w-sm{max-width:var(--ui-container-sm)}.ui\:sm\:flex-row{flex-direction:row}.ui\:sm\:justify-end{justify-content:flex-end}.ui\:sm\:gap-2\.5{gap:calc(var(--ui-spacing)*2.5)}.ui\:sm\:pr-2\.5{padding-right:calc(var(--ui-spacing)*2.5)}.ui\:sm\:pl-2\.5{padding-left:calc(var(--ui-spacing)*2.5)}.ui\:data-\[vaul-drawer-direction\=left\]\:sm\:max-w-sm[data-vaul-drawer-direction=left],.ui\:data-\[vaul-drawer-direction\=right\]\:sm\:max-w-sm[data-vaul-drawer-direction=right]{max-width:var(--ui-container-sm)}}@media (min-width:48rem){.ui\:md\:block{display:block}.ui\:md\:flex{display:flex}.ui\:md\:p-12{padding:calc(var(--ui-spacing)*12)}.ui\:md\:text-sm{font-size:var(--ui-text-sm);line-height:var(--tw-leading,var(--ui-text-sm--line-height))}.ui\:md\:opacity-0{opacity:0}.ui\:md\:peer-data-\[variant\=inset\]\:m-2:is(:where(.ui\:peer)[data-variant=inset]~*){margin:calc(var(--ui-spacing)*2)}.ui\:md\:peer-data-\[variant\=inset\]\:ml-0:is(:where(.ui\:peer)[data-variant=inset]~*){margin-left:calc(var(--ui-spacing)*0)}.ui\:md\:peer-data-\[variant\=inset\]\:rounded-xl:is(:where(.ui\:peer)[data-variant=inset]~*){border-radius:calc(var(--radius) + 4px)}.ui\:md\:peer-data-\[variant\=inset\]\:shadow-sm:is(:where(.ui\:peer)[data-variant=inset]~*){--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ui\:md\:peer-data-\[variant\=inset\]\:peer-data-\[state\=collapsed\]\:ml-2:is(:where(.ui\:peer)[data-variant=inset]~*):is(:where(.ui\:peer)[data-state=collapsed]~*){margin-left:calc(var(--ui-spacing)*2)}.ui\:md\:after\:hidden:after{content:var(--tw-content);display:none}}.ui\:dark\:scale-0:where(.dark,.dark *){--tw-scale-x:0%;--tw-scale-y:0%;--tw-scale-z:0%;scale:var(--tw-scale-x)var(--tw-scale-y)}.ui\:dark\:scale-100:where(.dark,.dark *){--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.ui\:dark\:-rotate-90:where(.dark,.dark *){rotate:-90deg}.ui\:dark\:rotate-0:where(.dark,.dark *){rotate:none}.ui\:dark\:border-input:where(.dark,.dark *){border-color:var(--input)}.ui\:dark\:bg-destructive\/60:where(.dark,.dark *){background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:bg-destructive\/60:where(.dark,.dark *){background-color:color-mix(in oklab,var(--destructive)60%,transparent)}}.ui\:dark\:bg-destructive\/70:where(.dark,.dark *){background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:bg-destructive\/70:where(.dark,.dark *){background-color:color-mix(in oklab,var(--destructive)70%,transparent)}}.ui\:dark\:bg-input\/30:where(.dark,.dark *){background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:bg-input\/30:where(.dark,.dark *){background-color:color-mix(in oklab,var(--input)30%,transparent)}}.ui\:dark\:bg-transparent:where(.dark,.dark *){background-color:#0000}.ui\:dark\:text-muted-foreground:where(.dark,.dark *){color:var(--muted-foreground)}@media (hover:hover){.ui\:dark\:hover\:bg-input\/50:where(.dark,.dark *):hover{background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:hover\:bg-input\/50:where(.dark,.dark *):hover{background-color:color-mix(in oklab,var(--input)50%,transparent)}}}.ui\:dark\:focus-visible\:ring-destructive\/40:where(.dark,.dark *):focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:focus-visible\:ring-destructive\/40:where(.dark,.dark *):focus-visible{--tw-ring-color:color-mix(in oklab,var(--destructive)40%,transparent)}}:where([data-slot=tooltip-content]) .ui\:dark\:in-data-\[slot\=tooltip-content\]\:bg-background\/10:where(.dark,.dark *){background-color:var(--background)}@supports (color:color-mix(in lab, red, red)){:where([data-slot=tooltip-content]) .ui\:dark\:in-data-\[slot\=tooltip-content\]\:bg-background\/10:where(.dark,.dark *){background-color:color-mix(in oklab,var(--background)10%,transparent)}}.ui\:dark\:has-data-\[state\=checked\]\:bg-primary\/10:where(.dark,.dark *):has([data-state=checked]){background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:has-data-\[state\=checked\]\:bg-primary\/10:where(.dark,.dark *):has([data-state=checked]){background-color:color-mix(in oklab,var(--primary)10%,transparent)}}.ui\:dark\:has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-destructive\/40:where(.dark,.dark *):has([data-slot][aria-invalid=true]){--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-destructive\/40:where(.dark,.dark *):has([data-slot][aria-invalid=true]){--tw-ring-color:color-mix(in oklab,var(--destructive)40%,transparent)}}.ui\:dark\:aria-invalid\:ring-destructive\/40:where(.dark,.dark *)[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:aria-invalid\:ring-destructive\/40:where(.dark,.dark *)[aria-invalid=true]{--tw-ring-color:color-mix(in oklab,var(--destructive)40%,transparent)}}.ui\:dark\:data-\[state\=active\]\:border-input:where(.dark,.dark *)[data-state=active]{border-color:var(--input)}.ui\:dark\:data-\[state\=active\]\:bg-input\/30:where(.dark,.dark *)[data-state=active]{background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:data-\[state\=active\]\:bg-input\/30:where(.dark,.dark *)[data-state=active]{background-color:color-mix(in oklab,var(--input)30%,transparent)}}.ui\:dark\:data-\[state\=active\]\:text-foreground:where(.dark,.dark *)[data-state=active]{color:var(--foreground)}.ui\:dark\:data-\[state\=checked\]\:bg-primary:where(.dark,.dark *)[data-state=checked]{background-color:var(--primary)}.ui\:dark\:data-\[state\=checked\]\:bg-primary-foreground:where(.dark,.dark *)[data-state=checked]{background-color:var(--primary-foreground)}.ui\:dark\:data-\[state\=unchecked\]\:bg-foreground:where(.dark,.dark *)[data-state=unchecked]{background-color:var(--foreground)}.ui\:dark\:data-\[state\=unchecked\]\:bg-input\/80:where(.dark,.dark *)[data-state=unchecked]{background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.ui\:dark\:data-\[state\=unchecked\]\:bg-input\/80:where(.dark,.dark *)[data-state=unchecked]{background-color:color-mix(in oklab,var(--input)80%,transparent)}}.ui\:\[\&_\[data-command-group\]\:not\(\[hidden\]\)_\~\[data-command-group\]\]\:pt-0 [data-command-group]:not([hidden])~[data-command-group]{padding-top:calc(var(--ui-spacing)*0)}.ui\:\[\&_\[data-command-input-wrapper\]_svg\]\:h-5 [data-command-input-wrapper] svg{height:calc(var(--ui-spacing)*5)}.ui\:\[\&_\[data-command-input-wrapper\]_svg\]\:w-5 [data-command-input-wrapper] svg{width:calc(var(--ui-spacing)*5)}.ui\:\[\&_\[data-command-item\]_svg\]\:h-5 [data-command-item] svg{height:calc(var(--ui-spacing)*5)}.ui\:\[\&_\[data-command-item\]_svg\]\:w-5 [data-command-item] svg{width:calc(var(--ui-spacing)*5)}.ui\:\[\&_img\]\:size-full img{width:100%;height:100%}.ui\:\[\&_img\]\:object-cover img{object-fit:cover}.ui\:\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.ui\:\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.ui\:\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3 svg:not([class*=size-]){width:calc(var(--ui-spacing)*3);height:calc(var(--ui-spacing)*3)}.ui\:\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 svg:not([class*=size-]){width:calc(var(--ui-spacing)*4);height:calc(var(--ui-spacing)*4)}.ui\:\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-6 svg:not([class*=size-]){width:calc(var(--ui-spacing)*6);height:calc(var(--ui-spacing)*6)}.ui\:\[\&_svg\:not\(\[class\*\=\'text-\'\]\)\]\:text-muted-foreground svg:not([class*=text-]){color:var(--muted-foreground)}.ui\:\[\&_tr\]\:border-b tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.ui\:\[\&_tr\:last-child\]\:border-0 tr:last-child{border-style:var(--tw-border-style);border-width:0}.ui\:\[\&\+\[data-slot\=item-content\]\]\:flex-none+[data-slot=item-content]{flex:none}.ui\:\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]){padding-right:calc(var(--ui-spacing)*0)}.ui\:\[\.border-b\]\:pb-3.border-b{padding-bottom:calc(var(--ui-spacing)*3)}.ui\:\[\.border-b\]\:pb-6.border-b{padding-bottom:calc(var(--ui-spacing)*6)}.ui\:\[\.border-t\]\:pt-3.border-t{padding-top:calc(var(--ui-spacing)*3)}.ui\:\[\.border-t\]\:pt-6.border-t{padding-top:calc(var(--ui-spacing)*6)}.ui\:\[a\]\:transition-colors:is(a){transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--ui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--ui-default-transition-duration))}@media (hover:hover){.ui\:\[a\]\:hover\:bg-accent\/50:is(a):hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.ui\:\[a\]\:hover\:bg-accent\/50:is(a):hover{background-color:color-mix(in oklab,var(--accent)50%,transparent)}}}:is(.ui\:\*\:\[span\]\:last\:flex>*):is(span):last-child{display:flex}:is(.ui\:\*\:\[span\]\:last\:items-center>*):is(span):last-child{align-items:center}:is(.ui\:\*\:\[span\]\:last\:gap-2>*):is(span):last-child{gap:calc(var(--ui-spacing)*2)}:is(.ui\:data-\[variant\=destructive\]\:\*\:\[svg\]\:text-destructive\![data-variant=destructive]>*):is(svg){color:var(--destructive)!important}.ui\:\[\&\>\*\]\:focus-visible\:relative>:focus-visible{position:relative}.ui\:\[\&\>\*\]\:focus-visible\:z-10>:focus-visible{z-index:10}.ui\:\[\&\>\*\:not\(\:first-child\)\]\:rounded-s-none>:not(:first-child){border-start-start-radius:0;border-end-start-radius:0}.ui\:\[\&\>\*\:not\(\:first-child\)\]\:rounded-t-none>:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.ui\:\[\&\>\*\:not\(\:first-child\)\]\:border-s-0>:not(:first-child){border-inline-start-style:var(--tw-border-style);border-inline-start-width:0}.ui\:\[\&\>\*\:not\(\:first-child\)\]\:border-t-0>:not(:first-child){border-top-style:var(--tw-border-style);border-top-width:0}.ui\:\[\&\>\*\:not\(\:last-child\)\]\:rounded-e-none>:not(:last-child){border-start-end-radius:0;border-end-end-radius:0}.ui\:\[\&\>\*\:not\(\:last-child\)\]\:rounded-b-none>:not(:last-child){border-bottom-right-radius:0;border-bottom-left-radius:0}.ui\:has-\[select\[aria-hidden\=true\]\:last-child\]\:\[\&\>\[data-slot\=select-trigger\]\:last-of-type\]\:rounded-e-md:has(:is(select[aria-hidden=true]:last-child))>[data-slot=select-trigger]:last-of-type{border-start-end-radius:calc(var(--radius) - 2px);border-end-end-radius:calc(var(--radius) - 2px)}.ui\:\[\&\>\[data-slot\=select-trigger\]\:not\(\[class\*\=\'w-\'\]\)\]\:w-fit>[data-slot=select-trigger]:not([class*=w-]){width:fit-content}.ui\:has-\[\>\[data-slot\=field-content\]\]\:\[\&\>\[role\=checkbox\]\,\[role\=radio\]\]\:mt-px:has(>[data-slot=field-content])>[role=checkbox],.ui\:has-\[\>\[data-slot\=field-content\]\]\:\[\&\>\[role\=checkbox\]\,\[role\=radio\]\]\:mt-px:has(>[data-slot=field-content]) [role=radio]{margin-top:1px}.ui\:\[\&\>a\]\:underline>a{text-decoration-line:underline}.ui\:\[\&\>a\]\:underline-offset-4>a{text-underline-offset:4px}.ui\:\[\&\>a\:hover\]\:text-primary>a:hover{color:var(--primary)}.ui\:\[\&\>button\]\:hidden>button{display:none}.ui\:\[\&\>input\]\:flex-1>input{flex:1}.ui\:has-\[\>\[data-align\=block-end\]\]\:\[\&\>input\]\:pt-3:has(>[data-align=block-end])>input{padding-top:calc(var(--ui-spacing)*3)}.ui\:has-\[\>\[data-align\=block-start\]\]\:\[\&\>input\]\:pb-3:has(>[data-align=block-start])>input{padding-bottom:calc(var(--ui-spacing)*3)}.ui\:has-\[\>\[data-align\=inline-end\]\]\:\[\&\>input\]\:pe-2:has(>[data-align=inline-end])>input{padding-inline-end:calc(var(--ui-spacing)*2)}.ui\:has-\[\>\[data-align\=inline-start\]\]\:\[\&\>input\]\:ps-2:has(>[data-align=inline-start])>input{padding-inline-start:calc(var(--ui-spacing)*2)}.ui\:\[\&\>kbd\]\:rounded-\[calc\(var\(--radius\)-5px\)\]>kbd{border-radius:calc(var(--radius) - 5px)}.ui\:\[\&\>span\:last-child\]\:truncate>span:last-child{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.ui\:\[\&\>svg\]\:pointer-events-none>svg{pointer-events:none}.ui\:\[\&\>svg\]\:size-2\.5>svg{width:calc(var(--ui-spacing)*2.5);height:calc(var(--ui-spacing)*2.5)}.ui\:\[\&\>svg\]\:size-3>svg{width:calc(var(--ui-spacing)*3);height:calc(var(--ui-spacing)*3)}.ui\:\[\&\>svg\]\:size-3\.5>svg{width:calc(var(--ui-spacing)*3.5);height:calc(var(--ui-spacing)*3.5)}.ui\:\[\&\>svg\]\:size-4>svg{width:calc(var(--ui-spacing)*4);height:calc(var(--ui-spacing)*4)}.ui\:\[\&\>svg\]\:shrink-0>svg{flex-shrink:0}.ui\:\[\&\>svg\]\:text-muted-foreground>svg{color:var(--muted-foreground)}.ui\:\[\&\>svg\]\:text-sidebar-accent-foreground>svg{color:var(--sidebar-accent-foreground)}.ui\:\[\&\>svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3\.5>svg:not([class*=size-]){width:calc(var(--ui-spacing)*3.5);height:calc(var(--ui-spacing)*3.5)}.ui\:\[\&\>svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4>svg:not([class*=size-]){width:calc(var(--ui-spacing)*4);height:calc(var(--ui-spacing)*4)}@media (hover:hover){:is(.ui\:hover\:\[\&\,\&\>svelte-css-wrapper\]\:\[\&\>th\,td\]\:bg-muted\/50:hover,.ui\:hover\:\[\&\,\&\>svelte-css-wrapper\]\:\[\&\>th\,td\]\:bg-muted\/50:hover>svelte-css-wrapper)>th,:is(.ui\:hover\:\[\&\,\&\>svelte-css-wrapper\]\:\[\&\>th\,td\]\:bg-muted\/50:hover,.ui\:hover\:\[\&\,\&\>svelte-css-wrapper\]\:\[\&\>th\,td\]\:bg-muted\/50:hover>svelte-css-wrapper) td{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){:is(.ui\:hover\:\[\&\,\&\>svelte-css-wrapper\]\:\[\&\>th\,td\]\:bg-muted\/50:hover,.ui\:hover\:\[\&\,\&\>svelte-css-wrapper\]\:\[\&\>th\,td\]\:bg-muted\/50:hover>svelte-css-wrapper)>th,:is(.ui\:hover\:\[\&\,\&\>svelte-css-wrapper\]\:\[\&\>th\,td\]\:bg-muted\/50:hover,.ui\:hover\:\[\&\,\&\>svelte-css-wrapper\]\:\[\&\>th\,td\]\:bg-muted\/50:hover>svelte-css-wrapper) td{background-color:color-mix(in oklab,var(--muted)50%,transparent)}}}.ui\:\[\&\>tr\]\:last\:border-b-0>tr:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.ui\:\[\&\[data-state\=open\]\>svg\]\:rotate-180[data-state=open]>svg{rotate:180deg}[data-side=left][data-collapsible=offcanvas] .ui\:\[\[data-side\=left\]\[data-collapsible\=offcanvas\]_\&\]\:-right-2{right:calc(var(--ui-spacing)*-2)}[data-side=left][data-state=collapsed] .ui\:\[\[data-side\=left\]\[data-state\=collapsed\]_\&\]\:cursor-e-resize{cursor:e-resize}[data-side=right][data-collapsible=offcanvas] .ui\:\[\[data-side\=right\]\[data-collapsible\=offcanvas\]_\&\]\:-left-2{left:calc(var(--ui-spacing)*-2)}[data-side=right][data-state=collapsed] .ui\:\[\[data-side\=right\]\[data-state\=collapsed\]_\&\]\:cursor-w-resize{cursor:w-resize}[data-variant=legend]+.ui\:\[\[data-variant\=legend\]\+\&\]\:-mt-1\.5{margin-top:calc(var(--ui-spacing)*-1.5)}@media (hover:hover){a.ui\:\[a\&\]\:hover\:bg-accent:hover{background-color:var(--accent)}a.ui\:\[a\&\]\:hover\:bg-destructive\/90:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){a.ui\:\[a\&\]\:hover\:bg-destructive\/90:hover{background-color:color-mix(in oklab,var(--destructive)90%,transparent)}}a.ui\:\[a\&\]\:hover\:bg-primary\/90:hover{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){a.ui\:\[a\&\]\:hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--primary)90%,transparent)}}a.ui\:\[a\&\]\:hover\:bg-secondary\/80:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab, red, red)){a.ui\:\[a\&\]\:hover\:bg-secondary\/80:hover{background-color:color-mix(in oklab,var(--secondary)80%,transparent)}}a.ui\:\[a\&\]\:hover\:bg-secondary\/90:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab, red, red)){a.ui\:\[a\&\]\:hover\:bg-secondary\/90:hover{background-color:color-mix(in oklab,var(--secondary)90%,transparent)}}a.ui\:\[a\&\]\:hover\:text-accent-foreground:hover{color:var(--accent-foreground)}a.ui\:\[a\&\]\:hover\:underline:hover{text-decoration-line:underline}a.ui\:dark\:\[a\&\]\:hover\:bg-accent\/50:where(.dark,.dark *):hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){a.ui\:dark\:\[a\&\]\:hover\:bg-accent\/50:where(.dark,.dark *):hover{background-color:color-mix(in oklab,var(--accent)50%,transparent)}}}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}:root{--radius:.625rem;--background:oklch(100% 0 0);--foreground:oklch(14.7% .004 49.25);--card:oklch(100% 0 0);--card-foreground:oklch(14.7% .004 49.25);--popover:oklch(100% 0 0);--popover-foreground:oklch(14.7% .004 49.25);--primary:oklch(21.6% .006 56.043);--primary-foreground:oklch(98.5% .001 106.423);--secondary:oklch(97% .001 106.424);--secondary-foreground:oklch(21.6% .006 56.043);--muted:oklch(97% .001 106.424);--muted-foreground:oklch(55.3% .013 58.071);--accent:oklch(96.7% .001 286.375);--accent-foreground:oklch(21.6% .006 56.043);--destructive:oklch(57.7% .245 27.325);--border:oklch(92.3% .003 48.717);--input:oklch(92.3% .003 48.717);--ring:oklch(70.9% .01 56.259);--chart-1:oklch(64.6% .222 41.116);--chart-2:oklch(60% .118 184.704);--chart-3:oklch(39.8% .07 227.392);--chart-4:oklch(82.8% .189 84.429);--chart-5:oklch(76.9% .188 70.08);--sidebar:oklch(100% 0 0);--sidebar-foreground:oklch(14.7% .004 49.25);--sidebar-primary:oklch(21.6% .006 56.043);--sidebar-primary-foreground:oklch(98.5% .001 106.423);--sidebar-accent:oklch(97% .001 106.424);--sidebar-accent-foreground:oklch(21.6% .006 56.043);--sidebar-border:oklch(92.3% .003 48.717);--sidebar-ring:oklch(70.9% .01 56.259)}.dark{--background:oklch(14.7% .004 49.25);--foreground:oklch(98.5% .001 106.423);--card:oklch(21.6% .006 56.043);--card-foreground:oklch(98.5% .001 106.423);--popover:oklch(21.6% .006 56.043);--popover-foreground:oklch(98.5% .001 106.423);--primary:oklch(92.3% .003 48.717);--primary-foreground:oklch(21.6% .006 56.043);--secondary:oklch(26.8% .007 34.298);--secondary-foreground:oklch(98.5% .001 106.423);--muted:oklch(26.8% .007 34.298);--muted-foreground:oklch(70.9% .01 56.259);--accent:oklch(26.8% .007 34.298);--accent-foreground:oklch(98.5% .001 106.423);--destructive:oklch(70.4% .191 22.216);--border:oklch(100% 0 0/.1);--input:oklch(100% 0 0/.15);--ring:oklch(55.3% .013 58.071);--chart-1:oklch(48.8% .243 264.376);--chart-2:oklch(69.6% .17 162.48);--chart-3:oklch(76.9% .188 70.08);--chart-4:oklch(62.7% .265 303.9);--chart-5:oklch(64.5% .246 16.439);--sidebar:oklch(0% 0 0);--sidebar-foreground:oklch(98.5% .001 106.423);--sidebar-primary:oklch(48.8% .243 264.376);--sidebar-primary-foreground:oklch(98.5% .001 106.423);--sidebar-accent:oklch(26.8% .007 34.298);--sidebar-accent-foreground:oklch(98.5% .001 106.423);--sidebar-border:oklch(100% 0 0/.1);--sidebar-ring:oklch(55.3% .013 58.071)}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}@keyframes enter{0%{opacity:var(--tw-enter-opacity,1);transform:translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0)scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1))rotate(var(--tw-enter-rotate,0));filter:blur(var(--tw-enter-blur,0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity,1);transform:translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0)scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1))rotate(var(--tw-exit-rotate,0));filter:blur(var(--tw-exit-blur,0))}}@keyframes accordion-down{0%{height:0}to{height:var(--radix-accordion-content-height,var(--bits-accordion-content-height,var(--reka-accordion-content-height,var(--kb-accordion-content-height,var(--ngp-accordion-content-height,auto)))))}}@keyframes accordion-up{0%{height:var(--radix-accordion-content-height,var(--bits-accordion-content-height,var(--reka-accordion-content-height,var(--kb-accordion-content-height,var(--ngp-accordion-content-height,auto)))))}to{height:0}} \ No newline at end of file diff --git a/packages/utils/src/validation/constants.ts b/packages/utils/src/validation/constants.ts index 6b8682b1..afdbd448 100644 --- a/packages/utils/src/validation/constants.ts +++ b/packages/utils/src/validation/constants.ts @@ -12,3 +12,5 @@ export const ALLOWED_DOCUMENT_TYPES = [ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx 'application/msword' // .doc ] as const; + +export const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'] as const; diff --git a/packages/utils/src/validation/index.ts b/packages/utils/src/validation/index.ts index a4a3851f..f39607d3 100644 --- a/packages/utils/src/validation/index.ts +++ b/packages/utils/src/validation/index.ts @@ -3,5 +3,6 @@ export * from './constants'; export * from './account'; export * from './course'; export * from './mail'; +export * from './media'; export * from './onboarding'; export * from './organization'; diff --git a/packages/utils/src/validation/media/index.ts b/packages/utils/src/validation/media/index.ts new file mode 100644 index 00000000..33d4737d --- /dev/null +++ b/packages/utils/src/validation/media/index.ts @@ -0,0 +1 @@ +export * from './media'; diff --git a/packages/utils/src/validation/media/media.ts b/packages/utils/src/validation/media/media.ts new file mode 100644 index 00000000..950de41c --- /dev/null +++ b/packages/utils/src/validation/media/media.ts @@ -0,0 +1,12 @@ +import { ALLOWED_IMAGE_TYPES } from '../constants'; +import { z } from 'zod'; + +/** + * Schema for image upload presign URL request + * Used for generating presigned URLs for image uploads (avatars, etc.) + */ +export const ZMediaImagePresignUrlUpload = z.object({ + fileName: z.string().min(1), + fileType: z.enum(ALLOWED_IMAGE_TYPES) +}); +export type TMediaImagePresignUrlUpload = z.infer; diff --git a/packages/utils/src/validation/organization/organization.ts b/packages/utils/src/validation/organization/organization.ts index 3b5b5802..d4cf7479 100644 --- a/packages/utils/src/validation/organization/organization.ts +++ b/packages/utils/src/validation/organization/organization.ts @@ -1,5 +1,7 @@ import * as z from 'zod'; +import { blockedSubdomain } from '@cio/utils/constants'; + export const ZGetOrganizations = z.object({ siteName: z.string().min(1).optional(), customDomain: z.string().min(1).optional(), @@ -14,6 +16,12 @@ export const ZGetOrganizationTeam = z.object({ export type TGetOrganizationTeam = z.infer; +export const ZUpdateOrganizationParam = z.object({ + orgId: z.uuid() +}); + +export type TUpdateOrganizationParam = z.infer; + export const ZGetOrganizationAudience = z.object({ orgId: z.uuid() }); @@ -26,6 +34,12 @@ export const ZGetCoursesBySiteName = z.object({ export type TGetCoursesBySiteName = z.infer; +export const ZGetOrgSetup = z.object({ + siteName: z.string().min(1) +}); + +export type TGetOrgSetup = z.infer; + export const ZCreateOrgPlan = z.object({ orgId: z.uuid(), planName: z.enum(['EARLY_ADOPTER', 'ENTERPRISE', 'BASIC']), @@ -49,3 +63,56 @@ export const ZCancelOrgPlan = z.object({ }); export type TCancelOrgPlan = z.infer; + +export const ZCreateOrganization = z.object({ + name: z + .string() + .min(5) + .refine((val) => !/^[-]|[-]$/.test(val), { + message: 'Organization name cannot start or end with a hyphen' + }), + siteName: z + .string() + .min(5) + .refine((val) => !/^[-]|[-]$/.test(val), { + message: 'Site name cannot start or end with a hyphen' + }) + .refine((val) => !blockedSubdomain.includes(val), { + message: 'Sitename already exists.' + }) +}); + +export type TCreateOrganization = z.infer; + +export const ZUpdateOrganization = z.object({ + name: z + .string() + .min(5) + .refine((val) => !/^[-]|[-]$/.test(val), { + message: 'Organization name cannot start or end with a hyphen' + }) + .optional(), + avatarUrl: z.url().optional(), + theme: z.string().optional(), + landingpage: z.record(z.string(), z.unknown()).optional(), + siteName: z.string().min(1).optional(), + customDomain: z.string().nullable().optional(), + isCustomDomainVerified: z.boolean().optional(), + customization: z.record(z.string(), z.unknown()).optional() +}); + +export type TUpdateOrganization = z.infer; + +export const ZInviteTeamMembers = z.object({ + emails: z.array(z.string().email()).min(1).max(50), + roleId: z.number().int().positive() +}); + +export type TInviteTeamMembers = z.infer; + +export const ZRemoveTeamMember = z.object({ + orgId: z.uuid(), + memberId: z.number().int().positive() +}); + +export type TRemoveTeamMember = z.infer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b81c5eb..04772067 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -294,8 +294,8 @@ importers: specifier: ^1.3.34 version: 1.3.34(@sveltejs/kit@2.48.4(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.43.2)(vite@5.4.21(@types/node@20.19.24)(lightningcss@1.30.2)(sass@1.93.3)))(svelte@5.43.2)(vite@5.4.21(@types/node@20.19.24)(lightningcss@1.30.2)(sass@1.93.3)))(react@18.3.1)(solid-js@1.9.10)(svelte@5.43.2)(vue@3.5.22(typescript@5.9.3)) bits-ui: - specifier: ^2.14.1 - version: 2.14.2(@internationalized/date@3.10.0)(@sveltejs/kit@2.48.4(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.43.2)(vite@5.4.21(@types/node@20.19.24)(lightningcss@1.30.2)(sass@1.93.3)))(svelte@5.43.2)(vite@5.4.21(@types/node@20.19.24)(lightningcss@1.30.2)(sass@1.93.3)))(svelte@5.43.2) + specifier: ^2.14.4 + version: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.48.4(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.43.2)(vite@5.4.21(@types/node@20.19.24)(lightningcss@1.30.2)(sass@1.93.3)))(svelte@5.43.2)(vite@5.4.21(@types/node@20.19.24)(lightningcss@1.30.2)(sass@1.93.3)))(svelte@5.43.2) body-parser: specifier: ^1.20.2 version: 1.20.3 @@ -501,9 +501,6 @@ importers: svelte-jester: specifier: ^3.0.0 version: 3.0.0(jest@29.7.0(@types/node@20.19.24)(ts-node@10.9.2(@types/node@20.19.24)(typescript@5.9.3)))(svelte@5.43.2) - svelte-loading-spinners: - specifier: ^0.3.4 - version: 0.3.6 svelte-preprocess: specifier: ^6.0.0 version: 6.0.3(@babel/core@7.28.5)(postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@20.19.24)(typescript@5.9.3)))(postcss@8.5.6)(sass@1.93.3)(svelte@5.43.2)(typescript@5.9.3) @@ -847,6 +844,9 @@ importers: specifier: workspace:* version: link:../ui devDependencies: + '@aws-sdk/client-s3': + specifier: ^3.388.0 + version: 3.922.0 '@cio/tsconfig': specifier: workspace:* version: link:../tsconfig @@ -889,6 +889,12 @@ importers: '@tiptap/pm': specifier: ^3.7.1 version: 3.10.1 + '@zxcvbn-ts/core': + specifier: ^3.0.4 + version: 3.0.4 + dotenv: + specifier: ^16.3.1 + version: 16.6.1 storybook: specifier: ^8.4.2 version: 8.6.14(bufferutil@4.0.9)(prettier@3.6.2)(utf-8-validate@6.0.5) @@ -898,6 +904,9 @@ importers: tailwindcss: specifier: ^4.1.14 version: 4.1.16 + tsx: + specifier: ^4.7.1 + version: 4.20.6 typescript-svelte-plugin: specifier: ^0.3.50 version: 0.3.50(svelte@5.43.2)(typescript@5.9.3) @@ -915,9 +924,6 @@ importers: '@floating-ui/dom': specifier: ^1.7.4 version: 1.7.4 - '@lucide/svelte': - specifier: ^0.546.0 - version: 0.546.0(svelte@5.43.2) '@tiptap/core': specifier: ^3.7.1 version: 3.10.1(@tiptap/pm@3.10.1) @@ -994,6 +1000,9 @@ importers: '@internationalized/date': specifier: ^3.8.1 version: 3.10.0 + '@lucide/svelte': + specifier: ^0.554.0 + version: 0.554.0(svelte@5.43.2) '@sveltejs/adapter-auto': specifier: ^3.0.0 version: 3.3.1(@sveltejs/kit@2.48.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1))) @@ -1006,9 +1015,18 @@ importers: '@tailwindcss/typography': specifier: ^0.5.18 version: 0.5.19(tailwindcss@4.1.16) + '@zxcvbn-ts/core': + specifier: ^3.0.4 + version: 3.0.4 + '@zxcvbn-ts/language-common': + specifier: ^3.0.4 + version: 3.0.4 + '@zxcvbn-ts/language-en': + specifier: ^3.0.2 + version: 3.0.2 bits-ui: - specifier: ^2.11.7 - version: 2.14.2(@internationalized/date@3.10.0)(@sveltejs/kit@2.48.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2) + specifier: ^2.14.4 + version: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.48.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -1018,18 +1036,24 @@ importers: mode-watcher: specifier: ^1.1.0 version: 1.1.0(svelte@5.43.2) + runed: + specifier: ^0.36.0 + version: 0.36.0(@sveltejs/kit@2.48.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(zod@4.1.12) svelte: specifier: ^5.39.11 version: 5.43.2 svelte-sonner: specifier: ^1.0.5 version: 1.0.6(svelte@5.43.2) + svelte-toolbelt: + specifier: ^0.10.6 + version: 0.10.6(@sveltejs/kit@2.48.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2) tailwind-merge: - specifier: ^3.3.1 - version: 3.3.1 + specifier: ^3.4.0 + version: 3.4.0 tailwind-variants: - specifier: ^3.1.1 - version: 3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.16) + specifier: ^3.2.2 + version: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.16) tailwindcss: specifier: ^4.1.14 version: 4.1.16 @@ -3142,8 +3166,8 @@ packages: peerDependencies: svelte: ^5 - '@lucide/svelte@0.546.0': - resolution: {integrity: sha512-qFVQ+BUJH9Swns7cag3Wik7VqKZfN2hwDZ2VazbZJc8VSqI0kgOZCZ2OVwrvNxdkl8Mw9M9H1ROB0BXh/WH/sA==} + '@lucide/svelte@0.554.0': + resolution: {integrity: sha512-CM6wLEH8uk3WBpC42t8R0hF7SlQrsYEL6qGuXdB99xKZwKglpWmX5XgYu7FIYOCBYOyC1rm4dNhIe6uF9pOXqw==} peerDependencies: svelte: ^5 @@ -5338,6 +5362,15 @@ packages: '@vue/shared@3.5.22': resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==} + '@zxcvbn-ts/core@3.0.4': + resolution: {integrity: sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==} + + '@zxcvbn-ts/language-common@3.0.4': + resolution: {integrity: sha512-viSNNnRYtc7ULXzxrQIVUNwHAPSXRtoIwy/Tq4XQQdIknBzw4vz36lQLF6mvhMlTIlpjoN/Z1GFu/fwiAlUSsw==} + + '@zxcvbn-ts/language-en@3.0.2': + resolution: {integrity: sha512-Zp+zL+I6Un2Bj0tRXNs6VUBq3Djt+hwTwUz4dkt2qgsQz47U0/XthZ4ULrT/RxjwJRl5LwiaKOOZeOtmixHnjg==} + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -5691,8 +5724,8 @@ packages: peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.118 - bits-ui@2.14.2: - resolution: {integrity: sha512-YqpAJj/nRTZjf7IlgUC3QlepVZ7YFiAQWpZaYUOAZFW5Py+g5DYkhEDTdNFI5SReo7l1rct/nRpMK4pfL9Xffw==} + bits-ui@2.14.4: + resolution: {integrity: sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg==} engines: {node: '>=20'} peerDependencies: '@internationalized/date': ^3.8.1 @@ -6994,6 +7027,10 @@ packages: resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} hasBin: true + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -9776,6 +9813,18 @@ packages: '@sveltejs/kit': optional: true + runed@0.36.0: + resolution: {integrity: sha512-CK84KPwAausPQEyWF9t6miCuNW5isAKPMswDsz7jhdueiZZ9du/UrgWc/aggLts8QuppT8KucryrHDFBAqk9Ww==} + peerDependencies: + '@sveltejs/kit': ^2.21.0 + svelte: ^5.7.0 + zod: ^4.1.8 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + zod: + optional: true + rusha@0.8.14: resolution: {integrity: sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==} @@ -10264,9 +10313,6 @@ packages: jest: '>= 27' svelte: '>= 3' - svelte-loading-spinners@0.3.6: - resolution: {integrity: sha512-mthHQ2TwiwzTWzbFry3CBnVEfzqPOD9WkVw84OfSYzHRq6N9wgQ+yv37u81uPeuLU/ZOIPqhujpXquB1aol5ZQ==} - svelte-meta-tags@3.1.4: resolution: {integrity: sha512-TUIfhut0iVeTm7f5v/ZuU/tZ9XsNig9bNN8yK0t2x2WL9qw6AxAVRe9i5XddYJE0SuVwkoDCzjoSg5hXv7oWbQ==} peerDependencies: @@ -10457,8 +10503,8 @@ packages: tailwind-merge@2.6.0: resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} - tailwind-merge@3.3.1: - resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwind-merge@3.4.0: + resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} tailwind-variants@0.2.1: resolution: {integrity: sha512-2xmhAf4UIc3PijOUcJPA1LP4AbxhpcHuHM2C26xM0k81r0maAO6uoUSHl3APmvHZcY5cZCY/bYuJdfFa4eGoaw==} @@ -10472,8 +10518,8 @@ packages: peerDependencies: tailwindcss: '*' - tailwind-variants@3.1.1: - resolution: {integrity: sha512-ftLXe3krnqkMHsuBTEmaVUXYovXtPyTK7ckEfDRXS8PBZx0bAUas+A0jYxuKA5b8qg++wvQ3d2MQ7l/xeZxbZQ==} + tailwind-variants@3.2.2: + resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==} engines: {node: '>=16.x', pnpm: '>=7.x'} peerDependencies: tailwind-merge: '>=3.0.0' @@ -13894,7 +13940,7 @@ snapshots: clsx: 2.1.1 d3-array: 3.2.4 lodash-es: 4.17.21 - tailwind-merge: 3.3.1 + tailwind-merge: 3.4.0 '@layerstack/utils@2.0.0-next.12': dependencies: @@ -13909,7 +13955,7 @@ snapshots: dependencies: svelte: 5.43.2 - '@lucide/svelte@0.546.0(svelte@5.43.2)': + '@lucide/svelte@0.554.0(svelte@5.43.2)': dependencies: svelte: 5.43.2 @@ -16651,6 +16697,14 @@ snapshots: '@vue/shared@3.5.22': {} + '@zxcvbn-ts/core@3.0.4': + dependencies: + fastest-levenshtein: 1.0.16 + + '@zxcvbn-ts/language-common@3.0.4': {} + + '@zxcvbn-ts/language-en@3.0.2': {} + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -17001,7 +17055,7 @@ snapshots: nanoid: 5.1.6 svelte: 5.43.2 - bits-ui@2.14.2(@internationalized/date@3.10.0)(@sveltejs/kit@2.48.4(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.43.2)(vite@5.4.21(@types/node@20.19.24)(lightningcss@1.30.2)(sass@1.93.3)))(svelte@5.43.2)(vite@5.4.21(@types/node@20.19.24)(lightningcss@1.30.2)(sass@1.93.3)))(svelte@5.43.2): + bits-ui@2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.48.4(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.43.2)(vite@5.4.21(@types/node@20.19.24)(lightningcss@1.30.2)(sass@1.93.3)))(svelte@5.43.2)(vite@5.4.21(@types/node@20.19.24)(lightningcss@1.30.2)(sass@1.93.3)))(svelte@5.43.2): dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/dom': 1.7.4 @@ -17014,7 +17068,7 @@ snapshots: transitivePeerDependencies: - '@sveltejs/kit' - bits-ui@2.14.2(@internationalized/date@3.10.0)(@sveltejs/kit@2.48.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2): + bits-ui@2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.48.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2): dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/dom': 1.7.4 @@ -18490,6 +18544,8 @@ snapshots: dependencies: strnum: 2.1.1 + fastest-levenshtein@1.0.16: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -21927,6 +21983,16 @@ snapshots: optionalDependencies: '@sveltejs/kit': 2.48.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)) + runed@0.36.0(@sveltejs/kit@2.48.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(zod@4.1.12): + dependencies: + dequal: 2.0.3 + esm-env: 1.2.2 + lz-string: 1.5.0 + svelte: 5.43.2 + optionalDependencies: + '@sveltejs/kit': 2.48.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.3)(tsx@4.20.6)(yaml@2.8.1)) + zod: 4.1.12 + rusha@0.8.14: {} rw@1.3.3: {} @@ -22469,8 +22535,6 @@ snapshots: jest: 29.7.0(@types/node@20.19.24)(ts-node@10.9.2(@types/node@20.19.24)(typescript@5.9.3)) svelte: 5.43.2 - svelte-loading-spinners@0.3.6: {} - svelte-meta-tags@3.1.4(svelte@4.2.20): dependencies: schema-dts: 1.1.5 @@ -22670,7 +22734,7 @@ snapshots: tailwind-merge@2.6.0: {} - tailwind-merge@3.3.1: {} + tailwind-merge@3.4.0: {} tailwind-variants@0.2.1(tailwindcss@3.4.18(ts-node@10.9.2(@types/node@22.18.13)(typescript@5.9.3))): dependencies: @@ -22682,11 +22746,11 @@ snapshots: tailwind-merge: 2.5.4 tailwindcss: 3.4.18(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) - tailwind-variants@3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.16): + tailwind-variants@3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.16): dependencies: tailwindcss: 4.1.16 optionalDependencies: - tailwind-merge: 3.3.1 + tailwind-merge: 3.4.0 tailwindcss-animate@1.0.7(tailwindcss@3.4.18(ts-node@10.9.2(@types/node@22.18.13)(typescript@5.9.3))): dependencies: