diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4fbe356 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +target/ +.git/ +admin-ui/ +content/ +.env* +*.md +.cursor/ diff --git a/.env.docker.example b/.env.docker.example new file mode 100644 index 0000000..c65f078 --- /dev/null +++ b/.env.docker.example @@ -0,0 +1,13 @@ +# Domains – beide müssen per DNS auf den Server zeigen +RUSTYCMS_API_DOMAIN=api.example.com +RUSTYCMS_ADMIN_DOMAIN=admin.example.com + +# API Key für Schreib-Zugriff (POST/PUT/DELETE) +RUSTYCMS_API_KEY=change-me-to-something-secret + +# Sprachen (erstes = Default) +RUSTYCMS_LOCALES=de,en + +# Optionale Pfade zu Content/Schemas auf dem Host (Standard: ./content und ./types) +# RUSTYCMS_CONTENT_DIR=/home/user/mein-content/content +# RUSTYCMS_TYPES_DIR=/home/user/mein-content/types diff --git a/.env.docker.local b/.env.docker.local new file mode 100644 index 0000000..d182553 --- /dev/null +++ b/.env.docker.local @@ -0,0 +1,13 @@ +# Lokale Entwicklung – kein TLS, keine echten Domains +RUSTYCMS_API_DOMAIN=localhost +RUSTYCMS_ADMIN_DOMAIN=localhost + +# API Key (lokal kann auch ein einfacher Wert reichen) +RUSTYCMS_API_KEY=local-dev-key + +# Sprachen +RUSTYCMS_LOCALES=de,en + +# Content und Schemas vom lokalen Repo +RUSTYCMS_CONTENT_DIR=./content +RUSTYCMS_TYPES_DIR=./types diff --git a/.env.example b/.env.example index 0751c2a..5492381 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,10 @@ RUSTYCMS_API_KEY=dein-geheimes-token # Optional: Response cache for GET /api/content (TTL in seconds). 0 = off. Default: 60. # RUSTYCMS_CACHE_TTL_SECS=60 + +# Optional: Public base URL of the API (e.g. https://api.example.com). Used to expand relative /api/assets/ paths in responses. Defaults to http://host:port. +# RUSTYCMS_BASE_URL=https://api.example.com + +# Optional: Paths to types and content directories. Useful for keeping content in a separate repo. +# RUSTYCMS_TYPES_DIR=./types +# RUSTYCMS_CONTENT_DIR=./content diff --git a/.gitignore b/.gitignore index 86305b6..8e26f93 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,18 @@ Cargo.lock .DS_Store *.db content.db -content/ + +# Content: ignore all except one demo entry +content/* +!content/de/ +content/de/* +!content/de/demo/ +content/de/demo/* +!content/de/demo/demo-welcome.json5 + +# Types: ignore all except demo type +types/* +!types/demo.json5 + .history +.cursor diff --git a/.vscode/settings.json b/.vscode/settings.json index 0227c69..cd975bf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -62,6 +62,31 @@ "content/tag/*.json" ], "url": "./schemas/tag.schema.json" + }, + { + "fileMatch": [ + "content/calendar/*.json5", + "content/calendar/*.json", + "content/*/calendar/*.json5", + "content/*/calendar/*.json" + ], + "url": "./schemas/calendar.schema.json" + }, + { + "fileMatch": [ + "content/calendar_item/*.json5", + "content/calendar_item/*.json", + "content/*/calendar_item/*.json5", + "content/*/calendar_item/*.json" + ], + "url": "./schemas/calendar_item.schema.json" + }, + { + "fileMatch": [ + "content/*/translation_bundle/*.json5", + "content/*/translation_bundle/*.json" + ], + "url": "./schemas/translation_bundle.schema.json" } ] } \ No newline at end of file diff --git a/@types/Contentful_Badges.ts b/@types/Contentful_Badges.ts deleted file mode 100644 index d5dedc3..0000000 --- a/@types/Contentful_Badges.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; -import type { CF_ComponentListSkeleton } from "src/@types/Contentful_List"; - -export interface CF_ComponentBadges { - internal: string; - badges: CF_ComponentListSkeleton; - variants: "light" | "dark"; - layout?: any; -} - -export interface CF_ComponentBadgesSkeleton { - contentTypeId: CF_ContentType.badges; - fields: CF_ComponentBadges; -} diff --git a/@types/Contentful_Campaign.ts b/@types/Contentful_Campaign.ts deleted file mode 100644 index 3240f09..0000000 --- a/@types/Contentful_Campaign.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { CF_ContentType } from "./Contentful_ContentType.enum"; -import type { Asset } from "contentful"; - -export interface CF_Campaign { - campaignName: string; - urlPatter: string; - selector: string; - insertHtml: - | "afterbegin" - | "beforeend" - | "afterend" - | "beforebegin" - | "replace"; - timeUntil?: string; - javascript?: string; - medias?: Asset[]; - html?: string; - css?: string; -} - -export interface CF_CampaignSkeleton { - contentTypeId: CF_ContentType.campaign; - fields: CF_Campaign; -} diff --git a/@types/Contentful_Campaigns.ts b/@types/Contentful_Campaigns.ts deleted file mode 100644 index cf02243..0000000 --- a/@types/Contentful_Campaigns.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { CF_ContentType } from "./Contentful_ContentType.enum"; -import type { CF_CampaignSkeleton } from "./Contentful_Campaign"; - -export interface CF_Campaigns { - id: string; - campaigns: CF_CampaignSkeleton[]; - enable: boolean; -} - -export interface CF_CampaignsSkeleton { - contentTypeId: CF_ContentType.campaigns; - fields: CF_Campaigns; -} diff --git a/@types/Contentful_CloudinaryImage.ts b/@types/Contentful_CloudinaryImage.ts deleted file mode 100644 index 23ea076..0000000 --- a/@types/Contentful_CloudinaryImage.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface CF_CloudinaryImage { - bytes: number; - created_at: string; - format: string; - height: number; - original_secure_url: string; - original_url: string; - public_id: string; - resource_type: string; - secure_url: string; - type: string; - url: string; - version: number; - width: number; -} \ No newline at end of file diff --git a/@types/Contentful_Content.ts b/@types/Contentful_Content.ts deleted file mode 100644 index 1f4b0ce..0000000 --- a/@types/Contentful_Content.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { EntrySkeletonType } from "contentful"; - -export type rowJutify = - | "start" - | "end" - | "center" - | "between" - | "around" - | "evenly"; -export type rowAlignItems = "start" | "end" | "center" | "baseline" | "stretch"; - -export interface CF_Content { - row1JustifyContent: rowJutify; - row1AlignItems: rowAlignItems; - row1Content: EntrySkeletonType[]; - - row2JustifyContent: rowJutify; - row2AlignItems: rowAlignItems; - row2Content: EntrySkeletonType[]; - - row3JustifyContent: rowJutify; - row3AlignItems: rowAlignItems; - row3Content: EntrySkeletonType[]; -} diff --git a/@types/Contentful_ContentType.enum.ts b/@types/Contentful_ContentType.enum.ts deleted file mode 100644 index 7329d41..0000000 --- a/@types/Contentful_ContentType.enum.ts +++ /dev/null @@ -1,34 +0,0 @@ -export enum CF_ContentType { - "componentLinkList" = "componentLinkList", - "badges" = "badges", - "componentPostOverview" = "componentPostOverview", - "footer" = "footer", - "fullwidthBanner" = "fullwidthBanner", - "headline" = "headline", - "html" = "html", - "image" = "image", - "img" = "img", - "iframe" = "iframe", - "imgGallery" = "imageGallery", - "internalReference" = "internalComponent", - "link" = "link", - "list" = "list", - "markdown" = "markdown", - "navigation" = "navigation", - "page" = "page", - "pageConfig" = "pageConfig", - "picture" = "picture", - "post" = "post", - "postComponent" = "postComponent", - "quote" = "quoteComponent", - "richtext" = "richtext", - "row" = "row", - "rowLayout" = "rowLayout", - "tag" = "tag", - "youtubeVideo" = "youtubeVideo", - "campaign" = "campaign", - "campaigns" = "campaigns", - "topBanner" = "topBanner", - "textFragment" = "textFragment", - "componentSearchableText" = "componentSearchableText", -} diff --git a/@types/Contentful_Footer.ts b/@types/Contentful_Footer.ts deleted file mode 100644 index f57ba6b..0000000 --- a/@types/Contentful_Footer.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; -import type { CF_Content } from "./Contentful_Content"; - -export interface CF_Footer extends CF_Content { - id : string; -} -export type CF_FooterSkeleton = { - contentTypeId: CF_ContentType.footer - fields: CF_Footer -} \ No newline at end of file diff --git a/@types/Contentful_FullwidthBanner.ts b/@types/Contentful_FullwidthBanner.ts deleted file mode 100644 index 835628c..0000000 --- a/@types/Contentful_FullwidthBanner.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { CF_ComponentImgSkeleton } from "src/@types/Contentful_Img"; -import type { CF_CloudinaryImage } from "src/@types/Contentful_CloudinaryImage"; -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; - - -export enum CF_FullwidthBannerVariant { - "dark"= "dark", - "light" = "light" -} - -export interface CF_FullwidthBanner { - name: string, - variant : CF_FullwidthBannerVariant, - headline : string, - subheadline: string, - text : string, - image: CF_CloudinaryImage[]; - img: CF_ComponentImgSkeleton; -} - -export type CF_FullwidthBannerSkeleton = { - contentTypeId: CF_ContentType.fullwidthBanner - fields: CF_FullwidthBanner -} \ No newline at end of file diff --git a/@types/Contentful_Grid.ts b/@types/Contentful_Grid.ts deleted file mode 100644 index efbdf66..0000000 --- a/@types/Contentful_Grid.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { EntrySkeletonType } from "contentful"; -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; - -export type CF_justfyContent = "start" | "end" | "center" | "between" | "around" | "evenly"; -export type CF_alignItems = "start" | "end" | "center" | "baseline" | "stretch" - -export interface CF_Column_Alignment { - justifyContent: CF_justfyContent, - alignItems: CF_alignItems -} - -export interface CF_Column_Layout { - layoutMobile: T - layoutTablet: T - layoutDesktop: T -} - -export type CF_Row_1_Column_Layout = "auto" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11" | "12"; - -export interface CF_Row { - alignment: EntrySkeletonType - layout: EntrySkeletonType> - content: EntrySkeletonType[] -} - -export interface CF_RowSkeleton { - contentTypeId: CF_ContentType.row - fields: CF_Row -} \ No newline at end of file diff --git a/@types/Contentful_Headline.ts b/@types/Contentful_Headline.ts deleted file mode 100644 index 357aa43..0000000 --- a/@types/Contentful_Headline.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; -import type { CF_ComponentLayout } from "src/@types/Contentful_Layout"; - -export type CF_Component_Headline_Align = - | "left" - | "center" - | "right" - -export type CF_Component_Headline_Tag = - | "h1" - | "h2" - | "h3" - | "h4" - | "h5" - | "h6" - -export type CF_alignTextClasses = - | "text-left" - | "text-center" - | "text-right" - -export interface CF_ComponentHeadline { - internal: string; - text: string; - tag: CF_Component_Headline_Tag, - layout: CF_ComponentLayout - align?: CF_Component_Headline_Align -} - -export interface CF_ComponentHeadlineSkeleton { - contentTypeId: CF_ContentType.headline - fields: CF_ComponentHeadline -} \ No newline at end of file diff --git a/@types/Contentful_Html.ts b/@types/Contentful_Html.ts deleted file mode 100644 index 34ba4ea..0000000 --- a/@types/Contentful_Html.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { CF_ContentType } from "./Contentful_ContentType.enum"; -import type { CF_ComponentLayout } from "./Contentful_Layout"; - -export interface CF_HTML { - id: string; - html: string; - layout: CF_ComponentLayout; -} - -export type CF_HTMLSkeleton = { - contentTypeId: CF_ContentType.html; - fields: CF_HTML; -}; diff --git a/@types/Contentful_Iframe.ts b/@types/Contentful_Iframe.ts deleted file mode 100644 index 3bfd45e..0000000 --- a/@types/Contentful_Iframe.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { CF_ContentType } from "./Contentful_ContentType.enum"; -import type { CF_ComponentImgSkeleton } from "./Contentful_Img"; -import type { CF_ComponentLayout } from "./Contentful_Layout"; - -export interface CF_ComponentIframe { - name: string; - content: string; - iframe: string; - overlayImage?: CF_ComponentImgSkeleton; - layout: CF_ComponentLayout; -} - -export interface CF_ComponentIframeSkeleton { - contentTypeId: CF_ContentType.iframe; - fields: CF_ComponentIframe; -} diff --git a/@types/Contentful_Image.ts b/@types/Contentful_Image.ts deleted file mode 100644 index 9f76c45..0000000 --- a/@types/Contentful_Image.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { CF_ContentType } from "./Contentful_ContentType.enum"; -import type { CF_ComponentImgSkeleton } from "./Contentful_Img"; -import type { CF_ComponentLayout } from "./Contentful_Layout"; - -export interface CF_ComponentImage { - name: string; - image: CF_ComponentImgSkeleton; - caption: string; - layout: CF_ComponentLayout; - maxWidth?: number; - aspectRatio?: number; -} - -export interface CF_ComponentImageSkeleton { - contentTypeId: CF_ContentType.image; - fields: CF_ComponentImage; -} diff --git a/@types/Contentful_ImageGallery.ts b/@types/Contentful_ImageGallery.ts deleted file mode 100644 index 4366a38..0000000 --- a/@types/Contentful_ImageGallery.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { CF_ContentType } from "./Contentful_ContentType.enum"; -import type { CF_ComponentImgSkeleton } from "./Contentful_Img"; -import type { CF_ComponentLayout } from "./Contentful_Layout"; - -export interface CF_ImageGallery { - name: string; - images: CF_ComponentImgSkeleton[]; - layout: CF_ComponentLayout; - description?: string; -} - -export interface CF_ImageGallerySkeleton { - contentTypeId: CF_ContentType.imgGallery - fields:CF_ImageGallery -} \ No newline at end of file diff --git a/@types/Contentful_Img.ts b/@types/Contentful_Img.ts deleted file mode 100644 index faf16ef..0000000 --- a/@types/Contentful_Img.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { CF_ContentType } from "./Contentful_ContentType.enum"; - - -export interface CF_ComponentImgDetails { - size: number, - image: { - width: number, - height: number - } -} - -export interface CF_ComponentImg { - title: string; - description: string; - file: { - url: string; - details: CF_ComponentImgDetails; - fileName: string; - contentType: string; - } -} - -export interface CF_ComponentImgSkeleton { - contentTypeId: CF_ContentType.img - fields: CF_ComponentImg -} \ No newline at end of file diff --git a/@types/Contentful_InternalReference.ts b/@types/Contentful_InternalReference.ts deleted file mode 100644 index e292bbe..0000000 --- a/@types/Contentful_InternalReference.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; -import type { CF_ComponentLayout } from "src/@types/Contentful_Layout"; -import type { EntryFieldTypes } from "contentful"; - -export interface CF_internalReference { - data: EntryFieldTypes.Object, - reference: string, - layout: CF_ComponentLayout -} - -export type CF_internalReferenceSkeleton = { - contentTypeId: CF_ContentType.internalReference - fields: CF_internalReference -} \ No newline at end of file diff --git a/@types/Contentful_Layout.ts b/@types/Contentful_Layout.ts deleted file mode 100644 index a9ec491..0000000 --- a/@types/Contentful_Layout.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; - -export type CF_Component_Layout_Width = - | "1" - | "2" - | "3" - | "4" - | "5" - | "6" - | "7" - | "8" - | "9" - | "10" - | "11" - | "12"; - -export type CF_widths_mobile = - | "w-full" - | "w-1/12" - | "w-2/12" - | "w-3/12" - | "w-4/12" - | "w-5/12" - | "w-6/12" - | "w-7/12" - | "w-8/12" - | "w-9/12" - | "w-10/12" - | "w-11/12"; - -export type CF_widths_tablet = - | "" - | "md:w-full" - | "md:w-1/12" - | "md:w-2/12" - | "md:w-3/12" - | "md:w-4/12" - | "md:w-5/12" - | "md:w-6/12" - | "md:w-7/12" - | "md:w-8/12" - | "md:w-9/12" - | "md:w-10/12" - | "md:w-11/12"; - -export type CF_widths_desktop = - | "" - | "lg:w-full" - | "lg:w-1/12" - | "lg:w-2/12" - | "lg:w-3/12" - | "lg:w-4/12" - | "lg:w-5/12" - | "lg:w-6/12" - | "lg:w-7/12" - | "lg:w-8/12" - | "lg:w-9/12" - | "lg:w-10/12" - | "lg:w-11/12"; - -export type CF_Component_Layout_Space = - | 0 - | .5 - | 1 - | 1.5 - | 2 - -export type CF_Component_Space = - | "" - | "mb-[0.5rem]" - | "mb-[1rem]" - | "mb-[1.5rem]" - | "mb-[2rem]" - -export type CF_justfyContent = - | "justify-start" - | "justify-end" - | "justify-center" - | "justify-between" - | "justify-around" - | "justify-evenly"; - -export type CF_alignItems = - | "items-start" - | "items-end" - | "items-center" - | "items-baseline" - | "items-stretch"; - -export interface CF_ComponentLayout { - mobile: CF_Component_Layout_Width; - tablet?: CF_Component_Layout_Width; - desktop?: CF_Component_Layout_Width; - spaceBottom?: CF_Component_Layout_Space -} - -export interface CF_ComponentLayoutSkeleton { - contentTypeId: CF_ContentType.rowLayout - fields: CF_ComponentLayout -} \ No newline at end of file diff --git a/@types/Contentful_Link.ts b/@types/Contentful_Link.ts deleted file mode 100644 index d399d12..0000000 --- a/@types/Contentful_Link.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; - -export interface CF_Link { - name: string; - internal: string; - linkName: string; - icon?: string; - color?: string; - url: string; - newTab?: boolean; - external?: boolean; - description?: string; - alt?: string; - showText?: boolean; - author: string; - date: string; - source: string; -} - -export interface CF_LinkSkeleton { - contentTypeId: CF_ContentType.link; - fields: CF_Link; -} diff --git a/@types/Contentful_Link_List.ts b/@types/Contentful_Link_List.ts deleted file mode 100644 index 030cbd2..0000000 --- a/@types/Contentful_Link_List.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { CF_ContentType } from "./Contentful_ContentType.enum"; -import type { CF_LinkSkeleton } from "./Contentful_Link"; - -export interface CF_Link_List { - headline: string; - links: CF_LinkSkeleton[]; -} - -export type CF_LinkListSkeleton = { - contentTypeId: CF_ContentType.componentLinkList; - fields: CF_Link_List; -}; diff --git a/@types/Contentful_List.ts b/@types/Contentful_List.ts deleted file mode 100644 index c1096e6..0000000 --- a/@types/Contentful_List.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; - -export interface CF_ComponentList { - internal: string; - item: string[]; -} - -export interface CF_ComponentListSkeleton { - contentTypeId: CF_ContentType.list - fields: CF_ComponentList -} diff --git a/@types/Contentful_Markdown.ts b/@types/Contentful_Markdown.ts deleted file mode 100644 index 8c19b9b..0000000 --- a/@types/Contentful_Markdown.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; -import type { CF_ComponentLayout } from "src/@types/Contentful_Layout"; -import type { TextAlignment } from "src/@types/TextAlignment"; - -export interface CF_Markdown { - name: string, - content: string, - layout: CF_ComponentLayout - alignment: TextAlignment -} - -export type CF_MarkdownSkeleton = { - contentTypeId: CF_ContentType.markdown - fields: CF_Markdown -} \ No newline at end of file diff --git a/@types/Contentful_Names.enum.ts b/@types/Contentful_Names.enum.ts deleted file mode 100644 index e79812a..0000000 --- a/@types/Contentful_Names.enum.ts +++ /dev/null @@ -1,17 +0,0 @@ -export enum CF_Navigation_Keys { - "header" = "navigation-header", - "socialMedia" = "navigation-social-media", - "footer" = "navigation-footer", -} - -export enum CF_PageConfigKey { - "pageConfig" = "page-config", -} - -export enum CF_Footer_Keys { - "main" = "main", -} - -export enum CF_Campaigns_Keys { - "campaigns" = "campaigns", -} diff --git a/@types/Contentful_Navigation.ts b/@types/Contentful_Navigation.ts deleted file mode 100644 index 15f4d17..0000000 --- a/@types/Contentful_Navigation.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { EntrySkeletonType } from "contentful"; -import type { CF_Link } from "src/lib/contentful"; -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; -import type { CF_Page } from "./Contentful_Page"; - -export interface CF_Navigation { - name: string, - internal: string, - links: EntrySkeletonType[] -} - -export type CF_NavigationSkeleton = { - contentTypeId: CF_ContentType.navigation - fields: CF_Navigation -} \ No newline at end of file diff --git a/@types/Contentful_Page.ts b/@types/Contentful_Page.ts deleted file mode 100644 index e34d6eb..0000000 --- a/@types/Contentful_Page.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; -import type { CF_FullwidthBannerSkeleton } from "src/@types/Contentful_FullwidthBanner"; -import type { CF_Content } from "./Contentful_Content"; -import type { CF_SEO } from "./Contentful_SEO"; - -export interface CF_Page extends CF_Content, CF_SEO { - slug: string; - name: string; - linkName: string; - icon?: string; - headline: string; - subheadline: string; - topFullwidthBanner: CF_FullwidthBannerSkeleton; -} - -export type CF_PageSkeleton = { - contentTypeId: CF_ContentType.page; - fields: CF_Page; -}; diff --git a/@types/Contentful_PageConfig.ts b/@types/Contentful_PageConfig.ts deleted file mode 100644 index 5c41270..0000000 --- a/@types/Contentful_PageConfig.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; -import type { CF_ComponentImgSkeleton } from "./Contentful_Img"; - -export interface CF_PageConfig { - logo: CF_ComponentImgSkeleton; - footerText1: string; - seoTitle: string; - seoDescription: string; - blogTagPageHeadline: string; - blogPostsPageHeadline: string; - blogPostsPageSubHeadline: string; - website: string; -} - -export type CF_PageConfigSkeleton = { - contentTypeId: CF_ContentType.pageConfig; - fields: CF_PageConfig; -}; diff --git a/@types/Contentful_Page_Seo.ts b/@types/Contentful_Page_Seo.ts deleted file mode 100644 index 4b32fb0..0000000 --- a/@types/Contentful_Page_Seo.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface CF_Page_Seo { - name: "page-about-seo", - title: "about", - description: "about", - metaRobotsIndex: "index", - metaRobotsFollow: "follow" -} \ No newline at end of file diff --git a/@types/Contentful_Picture.ts b/@types/Contentful_Picture.ts deleted file mode 100644 index d8c0cb8..0000000 --- a/@types/Contentful_Picture.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { EntryFieldTypes } from "contentful"; -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; -import type { CF_CloudinaryImage } from "src/@types/Contentful_CloudinaryImage"; - - -export type CF_PictureWidths = 400 | 800 | 1200 | 1400 -export type CF_PictureFormats = "aviv" | "jpg" | "png" | "webp" -export type CF_PictureFit = "contain" | "cover" | "fill" | "inside" | "outside" -export type CF_PicturePosition = "top" - | "right top" - | "right" - | "right bottom" - | "bottom" - | "left bottom" - | "left" - | "left top" - | "north" - | "northeast" - | "east" - | "southeast" - | "south" - | "southwest" - | "west" - | "northwest" - | "center" - | "centre" - | "cover" - | "entropy" - | "attention" -export type CF_PictureAspectRatio = 'original' - | '32:9' - | '16:9' - | '5:4' - | '4:3' - | '3:2' - | '1:1' - | '2:3' - | '3:4' - | '4:5' - - -export interface CF_Picture { - name: EntryFieldTypes.Text; - image: CF_CloudinaryImage[]; - alt?: EntryFieldTypes.Text; - widths: Array; - aspectRatio: CF_PictureAspectRatio; - formats: CF_PictureFormats; - fit: CF_PictureFit; - position: CF_PicturePosition; - layout?: any; -} - -export type CF_PictureSkeleton = { - contentTypeId: CF_ContentType.picture - fields: CF_Picture -} diff --git a/@types/Contentful_Post.ts b/@types/Contentful_Post.ts deleted file mode 100644 index 7942f5b..0000000 --- a/@types/Contentful_Post.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; -import type { CF_ComponentImgSkeleton } from "./Contentful_Img"; -import type { CF_Content } from "./Contentful_Content"; -import type { CF_SEO } from "./Contentful_SEO"; -import type { CF_TagSkeleton } from "./Contentful_Tag"; - -export interface CF_Post extends CF_Content, CF_SEO { - postImage: CF_ComponentImgSkeleton; - postTag: CF_TagSkeleton[]; - slug: string; - linkName: string; - icon?: string; - headline: string; - important: boolean; - created: string; - date?: string; - subheadline: string; - excerpt: string; - content: string; - /** Show comment section (default: true) */ - showCommentSection?: boolean; -} - -export type CF_PostEntrySkeleton = { - contentTypeId: CF_ContentType.post; - fields: CF_Post; -}; diff --git a/@types/Contentful_Post_Component.ts b/@types/Contentful_Post_Component.ts deleted file mode 100644 index e2bdd98..0000000 --- a/@types/Contentful_Post_Component.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { CF_ContentType } from "./Contentful_ContentType.enum"; -import type { CF_ComponentLayout } from "./Contentful_Layout"; -import type { CF_PostEntrySkeleton } from "./Contentful_Post"; - -export interface CF_PostComponent { - id: string; - post: CF_PostEntrySkeleton; - layout: CF_ComponentLayout; -} - -export interface CF_PostComponentSkeleton { - contentTypeId: CF_ContentType.postComponent; - fields: CF_PostComponent; -} diff --git a/@types/Contentful_Post_Overview.ts b/@types/Contentful_Post_Overview.ts deleted file mode 100644 index 4e39643..0000000 --- a/@types/Contentful_Post_Overview.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; -import type { CF_Content } from "./Contentful_Content"; -import type { CF_SEO } from "src/@types/Contentful_SEO"; -import type { CF_ComponentLayout } from "src/@types/Contentful_Layout"; -import type { CF_PostEntrySkeleton } from "src/@types/Contentful_Post"; -import type { CF_TagSkeleton } from "src/@types/Contentful_Tag"; -import type { Document } from "@contentful/rich-text-types"; - -export interface CF_Post_Overview extends CF_Content, CF_SEO { - id: string; - headline: string; - text: Document; - layout: CF_ComponentLayout; - allPosts: boolean; - filterByTag: CF_TagSkeleton[]; - posts: CF_PostEntrySkeleton[]; - numberItems: number; - design?: "cards" | "list"; -} - -export type CF_Post_OverviewEntrySkeleton = { - contentTypeId: CF_ContentType.post; - fields: CF_Post_Overview; -}; diff --git a/@types/Contentful_Quote.ts b/@types/Contentful_Quote.ts deleted file mode 100644 index bc56ec5..0000000 --- a/@types/Contentful_Quote.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; -import type { CF_ComponentLayout } from "src/@types/Contentful_Layout"; - -export interface CF_Quote { - quote: string, - author: string, - variant: 'left' | 'right', - layout: CF_ComponentLayout -} - -export type CF_QuoteSkeleton = { - contentTypeId: CF_ContentType.quote - fields: CF_Quote -} \ No newline at end of file diff --git a/@types/Contentful_Richtext.ts b/@types/Contentful_Richtext.ts deleted file mode 100644 index 1af8dc8..0000000 --- a/@types/Contentful_Richtext.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; - -export interface CF_ComponentRichtext { - content?: Document; - layout?: any; -} - -export interface CF_ComponentRichtextSkeleton { - contentTypeId: CF_ContentType.richtext - fields: CF_ComponentRichtext -} \ No newline at end of file diff --git a/@types/Contentful_SEO.ts b/@types/Contentful_SEO.ts deleted file mode 100644 index 1abc4bf..0000000 --- a/@types/Contentful_SEO.ts +++ /dev/null @@ -1,8 +0,0 @@ - -export type metaRobots = "index, follow" | "noindex, follow" | "index, nofollow" | "noindex, nofollow"; - -export interface CF_SEO { - seoTitle : string, - seoMetaRobots : metaRobots, - seoDescription : string, -} \ No newline at end of file diff --git a/@types/Contentful_SearchableText.ts b/@types/Contentful_SearchableText.ts deleted file mode 100644 index d5bf02c..0000000 --- a/@types/Contentful_SearchableText.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { CF_ContentType } from "./Contentful_ContentType.enum"; -import type { CF_ComponentLayout } from "./Contentful_Layout"; -import type { CF_TextFragmentSkeleton } from "./Contentful_TextFragment"; -import type { CF_TagSkeleton } from "./Contentful_Tag"; - -export interface CF_ComponentSearchableText { - id: string; - tagWhitelist?: CF_TagSkeleton[]; - textFragments: CF_TextFragmentSkeleton[]; - title?: string; - description?: string; - layout: CF_ComponentLayout; -} - -export interface CF_ComponentSearchableTextSkeleton { - contentTypeId: CF_ContentType.componentSearchableText; - fields: CF_ComponentSearchableText; -} diff --git a/@types/Contentful_SkeletonTypes.ts b/@types/Contentful_SkeletonTypes.ts deleted file mode 100644 index 9c7cc14..0000000 --- a/@types/Contentful_SkeletonTypes.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { CF_PostEntrySkeleton } from "src/@types/Contentful_Post"; -import type { CF_NavigationSkeleton } from "src/@types/Contentful_Navigation"; -import type { CF_PageSkeleton } from "src/@types/Contentful_Page"; -import type { CF_PageConfigSkeleton } from "src/@types/Contentful_PageConfig"; - -export type CF_SkeletonTypes = CF_PostEntrySkeleton | CF_NavigationSkeleton | CF_PageSkeleton | CF_PageConfigSkeleton; diff --git a/@types/Contentful_Tag.ts b/@types/Contentful_Tag.ts deleted file mode 100644 index c6d0e58..0000000 --- a/@types/Contentful_Tag.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { CF_ContentType } from "./Contentful_ContentType.enum"; - -export interface CF_Tag { - name: string; - icon: string; -} - -export interface CF_TagSkeleton { - contentTypeId: CF_ContentType.tag; - fields: CF_Tag; -} diff --git a/@types/Contentful_TextFragment.ts b/@types/Contentful_TextFragment.ts deleted file mode 100644 index a1cde86..0000000 --- a/@types/Contentful_TextFragment.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { CF_ContentType } from "./Contentful_ContentType.enum"; -import type { CF_TagSkeleton } from "./Contentful_Tag"; - -export interface CF_TextFragment { - id: string; - tags?: CF_TagSkeleton[]; - title: string; - text: string; -} - -export interface CF_TextFragmentSkeleton { - contentTypeId: CF_ContentType.textFragment; - fields: CF_TextFragment; -} diff --git a/@types/Contentful_TopBanner.ts b/@types/Contentful_TopBanner.ts deleted file mode 100644 index de9e240..0000000 --- a/@types/Contentful_TopBanner.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { CF_ContentType } from "src/@types/Contentful_ContentType.enum"; - -export interface CF_TopBanner { - id: string; - text: string; -} - -export type CF_TopBannerSkeleton = { - contentTypeId: CF_ContentType.topBanner; - fields: CF_TopBanner; -}; diff --git a/@types/Contentful_YoutubeVideo.ts b/@types/Contentful_YoutubeVideo.ts deleted file mode 100644 index 63a72cf..0000000 --- a/@types/Contentful_YoutubeVideo.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { CF_ContentType } from "./Contentful_ContentType.enum"; -import type { CF_ComponentLayout } from "./Contentful_Layout"; - -export interface CF_YoutubeVideo { - id: string; - youtubeId: string; - params?: string; - title?: string; - description?: string; - layout: CF_ComponentLayout; -} - -export interface CF_ComponentYoutubeVideoSkeleton { - contentTypeId: CF_ContentType.youtubeVideo; - fields: CF_YoutubeVideo; -} diff --git a/@types/SeoProps.ts b/@types/SeoProps.ts deleted file mode 100644 index 3818724..0000000 --- a/@types/SeoProps.ts +++ /dev/null @@ -1,91 +0,0 @@ -export type TwitterCardType = - | "summary" - | "summary_large_image" - | "app" - | "player"; - -// Link interface matching astro-seo's Link (which extends HTMLLinkElement) -// href must be string (not URL) to match astro-seo's expectations -export interface Link { - rel?: string; - href?: string; // Only string, not URL - hreflang?: string; - media?: string; - type?: string; - sizes?: string; - prefetch?: boolean; - crossorigin?: string; - as?: string; - [key: string]: any; -} - -export interface Meta { - property?: string; - name?: string; - content?: string; - httpEquiv?: string; - charset?: string; - [key: string]: any; -} - -export interface SeoProperties { - title?: string; - titleTemplate?: string; - titleDefault?: string; - charset?: string; - description?: string; - canonical?: URL | string; - nofollow?: boolean; - noindex?: boolean; - languageAlternates?: { - href: URL | string; - hrefLang: string; - }[]; - openGraph?: { - basic: { - title: string; - type: string; - image: string; - url?: URL | string; - }; - optional?: { - audio?: string; - description?: string; - determiner?: string; - locale?: string; - localeAlternate?: string[]; - siteName?: string; - video?: string; - }; - image?: { - url?: URL | string; - secureUrl?: URL | string; - type?: string; - width?: number; - height?: number; - alt?: string; - }; - article?: { - publishedTime?: string; - modifiedTime?: string; - expirationTime?: string; - authors?: string[]; - section?: string; - tags?: string[]; - }; - }; - twitter?: { - card?: TwitterCardType; - site?: string; - creator?: string; - title?: string; - description?: string; - image?: URL | string; - imageAlt?: string; - }; - extend?: { - link?: Partial[]; - meta?: Partial[]; - }; - surpressWarnings?: boolean; -} diff --git a/@types/TextAlignment.ts b/@types/TextAlignment.ts deleted file mode 100644 index 142bf48..0000000 --- a/@types/TextAlignment.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type TextAlignment = 'left' | 'center' | 'right'; -export enum TextAlignmentClasses { - 'left' = 'text-left', - 'center' = 'text-center', - 'right' = 'text-right' -} diff --git a/@types/astro-imagetools.d.ts b/@types/astro-imagetools.d.ts deleted file mode 100644 index 0a0428e..0000000 --- a/@types/astro-imagetools.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare module "astro-imagetools/api" { - export function renderImg( - options: any - ): Promise<{ link: string; style: string; img: string } | null>; - export function importImage(src: string): Promise; -} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0974454 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,118 @@ +# RustyCMS – AI Context + +> **Für AI-Tools**: Diese Datei (CLAUDE.md) wird von Claude Code gelesen. +> Cursor liest `.cursor/rules/project.mdc`. +> Beide Dateien haben identischen Inhalt — beim Ändern bitte **beide** aktualisieren. + +--- + +## Projektstruktur + +``` +rustycms/ +├── src/ # Rust API +│ ├── api/ +│ │ ├── handlers.rs # AppState + alle Content-Handler +│ │ ├── response.rs # format_references, expand/collapse_asset_urls +│ │ ├── assets.rs # Asset-Endpoints (Upload, Serve, Delete, Folders) +│ │ └── routes.rs # Axum-Router +│ ├── schema/ +│ │ └── validator.rs # normalize_reference_arrays (single + array refs) +│ └── store/ # FileStore / SqliteStore +├── types/ # Schema-Definitionen (*.json5) +├── content/ # Inhalte als JSON5-Dateien +│ └── assets/ # Bild-Assets (mit Unterordnern) +└── admin-ui/ # Next.js Admin UI (Port 3001) + ├── src/components/ # React-Komponenten + ├── src/lib/api.ts # API-Client + └── messages/ # i18n (en.json, de.json) +``` + +## Dev starten + +```bash +./dev.sh # API (Port 3000) + Admin UI (Port 3001) +cd admin-ui && npm run dev # nur Admin UI +cargo run # nur API +``` + +## Konfiguration + +Alle Optionen per Env-Variable (`.env.example` als Vorlage) oder CLI-Flag: + +| Env-Variable | CLI-Flag | Default | Zweck | +|---|---|---|---| +| `RUSTYCMS_TYPES_DIR` | `--types-dir` | `./types` | Schema-Definitionen | +| `RUSTYCMS_CONTENT_DIR` | `--content-dir` | `./content` | Content-Dateien | +| `RUSTYCMS_BASE_URL` | — | `http://host:port` | Öffentliche API-URL (für Asset-URLs) | +| `RUSTYCMS_API_KEY` | — | unset | Auth für POST/PUT/DELETE | +| `RUSTYCMS_LOCALES` | — | unset | z.B. `de,en` (erstes = Default) | +| `RUSTYCMS_STORE` | — | `file` | `file` oder `sqlite` | +| `RUSTYCMS_CORS_ORIGIN` | — | `*` | Erlaubte CORS-Origin | +| `RUSTYCMS_CACHE_TTL_SECS` | — | `60` | Response-Cache TTL (0 = aus) | + +## Asset-URL-Strategie + +Assets werden immer mit **relativem Pfad** auf Disk gespeichert: +``` +src: "/api/assets/ordner/datei.jpg" +``` + +Die API expandiert beim Ausliefern automatisch abhängig vom Kontext: +- **GET mit `_resolve`** (Frontend-Consumer): relativ → `https://api.example.com/api/assets/...` +- **GET ohne `_resolve`** (Admin UI bearbeitet): Pfad bleibt relativ +- **POST/PUT** (Speichern): absolute URLs werden vor dem Schreiben kollabiert + +Implementierung: `src/api/response.rs` → `expand_asset_urls()` / `collapse_asset_urls()` + +## Collection-Hierarchie (wichtig!) + +| Collection | Zweck | Pflichtfelder | +|---|---|---| +| `img` | Rohes Bild-Asset | `src` (relativer Pfad), `description` | +| `image` | Layout-Komponente | `name`, `img` (Referenz auf `img`-Collection) | + +**Regel:** `postImage` → referenziert `img` direkt. `row1Content` / `row2Content` etc. → erwarten `image`-Einträge, **nicht** `img`. + +Workflow beim Hinzufügen eines Bildes zu einer Content-Row: +1. `img`-Eintrag erstellen (`src: "/api/assets/..."`) +2. `image`-Eintrag erstellen (referenziert den `img`-Eintrag) +3. `image`-Slug in `rowXContent` des Posts eintragen + +## Referenz-Handling + +### ReferenceOrInlineField (Admin UI) +- `value` ist String → Reference-Modus (Slug-Picker) +- `value` ist Objekt ohne `_slug` → Inline-Modus (Felder direkt eingeben) +- `value` ist Objekt mit `_slug` → aufgelöste Referenz vom API → wird automatisch zu Reference-Modus normalisiert + +Normalisierung: `admin-ui/src/components/ReferenceOrInlineField.tsx` via `normalizedValue` memo. + +### Normalisierung beim Schreiben (Rust) +`validator::normalize_reference_arrays()` konvertiert vor dem Speichern: +- Einzelne `reference`/`referenceOrInline`-Felder: `{_slug: "foo", ...}` → `"foo"` +- Array-Items: gleiche Normalisierung per Element +- Aufgerufen in `create_entry` und `update_entry` + +## Admin UI Konventionen + +- **i18n**: next-intl, Cookie-basiert. Neue Keys immer in **beiden** Dateien eintragen: `admin-ui/messages/en.json` + `messages/de.json` +- **Tailwind v4**: `@plugin "tailwindcss-animate"` (nicht `@import`) +- **Kein Dark Mode**: `@media (prefers-color-scheme: dark)` wurde entfernt, keine `dark:`-Klassen +- **Scroll**: `html, body { height: 100%; overflow: hidden }` — Sidebar scrollt unabhängig von der Seite +- **Neue Komponente**: immer prüfen ob Übersetzungs-Namespace in beiden Message-Dateien vorhanden + +## Axum Routing + +Literale Routen haben Vorrang vor Wildcard-Routen — Reihenfolge egal, Axum löst korrekt auf: +```rust +.route("/api/assets/folders", get(...).post(...)) // matcht vor: +.route("/api/assets/*path", get(...).delete(...)) +``` + +## Rust-spezifisch + +- `clap` braucht Feature `"env"` für Env-Var-Support bei CLI-Args +- `AppState` in `src/api/handlers.rs` — alle neuen geteilten Ressourcen hier hinzufügen +- Hot-Reload: Änderungen in `types/` werden automatisch geladen (kein Neustart nötig) +- Cache wird bei Schreib-Operationen invalidiert (`cache.invalidate_collection()`) diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..4cc02e8 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,7 @@ +{$RUSTYCMS_API_DOMAIN} { + reverse_proxy rustycms:3000 +} + +{$RUSTYCMS_ADMIN_DOMAIN} { + reverse_proxy admin-ui:3001 +} diff --git a/Cargo.toml b/Cargo.toml index 36d8bb9..c0a63a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ name = "export-json-schema" path = "src/bin/export_json_schema.rs" [dependencies] -axum = "0.7" +axum = { version = "0.7", features = ["multipart"] } tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -21,7 +21,7 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } indexmap = { version = "2", features = ["serde"] } anyhow = "1" -clap = { version = "4", features = ["derive"] } +clap = { version = "4", features = ["derive", "env"] } regex = "1" notify = "6" async-trait = "0.1" @@ -29,3 +29,4 @@ dotenvy = "0.15" sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } image = "0.25" +webpx = { version = "0.1", default-features = false, features = ["encode", "decode"] } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e9260f4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Stage 1: Build +FROM rust:1.93-slim AS builder +WORKDIR /app +# Cache dependencies +COPY Cargo.toml Cargo.lock ./ +RUN mkdir -p src/bin && echo "fn main() {}" > src/main.rs && echo "fn main() {}" > src/bin/export_json_schema.rs && cargo build --release && rm -rf src +# Build actual binary +COPY src ./src +RUN touch src/main.rs && cargo build --release + +# Stage 2: Minimal runtime image +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* +COPY --from=builder /app/target/release/rustycms /usr/local/bin/rustycms +WORKDIR /data +EXPOSE 3000 +CMD ["rustycms", "--host", "0.0.0.0"] diff --git a/README.md b/README.md index fbed6e5..acbf587 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # RustyCMS -A file-based headless CMS written in Rust. Content types are defined as JSON5 schemas, content is stored as JSON5 files, and served via a REST API. +A file-based headless CMS written in Rust. Content types are defined as JSON5 schemas, content is stored as JSON5 files, and served via a REST API. **Under development.** ## Features @@ -13,8 +13,10 @@ A file-based headless CMS written in Rust. Content types are defined as JSON5 sc - **References & _resolve**: Reference fields as `{ _type, _slug }`; embed with `?_resolve=all` - **Reusable partials**: `reusable` schemas and `useFields` for shared field groups (e.g. layout) - **Optional API auth**: Protect write access (POST/PUT/DELETE) with an API key via env +- **Admin UI**: Next.js web interface for browsing collections, creating and editing content; manage types (list, create, edit, delete) with schema files and JSON Schema export updated on save - **Swagger UI**: Interactive API docs at `/swagger-ui` -- **Image transformation**: `GET /api/transform` – load image from external URL, optional resize (w, h), aspect-ratio crop (e.g. 1:1), fit (fill/contain/cover), output formats jpeg, png, webp, avif; with response cache +- **Asset management**: Upload, serve, and delete image files (jpg, png, webp, avif, gif, svg) stored in `content/assets/` via `GET/POST/DELETE /api/assets`; served with immutable cache headers; combinable with `/api/transform` for on-the-fly resizing +- **Image transformation**: `GET /api/transform` – load image from external URL or local asset, optional resize (w, h), aspect-ratio crop (e.g. 1:1), fit (fill/contain/cover), output formats jpeg, png, webp, avif; with response cache - **JSON5**: Human-friendly format with comments, trailing commas, unquoted keys - **Editor validation**: Export JSON Schema from your types so VS Code/Cursor validate `content/*.json5` while you edit - **CORS**: Open by default for frontend development @@ -22,6 +24,7 @@ A file-based headless CMS written in Rust. Content types are defined as JSON5 sc ## Requirements - [Rust](https://www.rust-lang.org/tools/install) (1.70+) +- [Node.js](https://nodejs.org/) (for Admin UI, optional) ## Quick start @@ -32,6 +35,39 @@ cargo run The server starts at `http://127.0.0.1:3000`. +**Start API + Admin UI and open browser:** + +```bash +./dev.sh +``` + +Runs both the API and Admin UI, waits until ready, and opens `http://localhost:2001` in the default browser. Press Ctrl+C to stop both. + +### Admin UI (optional) + +A Next.js web interface for managing content. Start the API server first, then: + +```bash +cd admin-ui +npm install +npm run dev +``` + +The Admin UI runs at `http://localhost:2001` (different port to avoid conflict with the API). + +**Version control:** Only a single demo type (`types/demo.json5`) and one demo content entry (`content/de/demo/demo-welcome.json5`) are tracked. All other files under `types/` and `content/` are ignored via `.gitignore`, so you can add your own types and content without committing them. + +**Admin UI:** Dashboard → **Types** lists all content types; you can create a new type, edit (fields, description, category, strict, …) or delete a type. Edits are written to `types/*.json5` and the corresponding `schemas/*.schema.json` is updated. Content is managed under **Collections** (list, new entry, edit, with reference fields and Markdown editor). Schema and data preview are available on the relevant pages. + +**Environment variables** (in `admin-ui/.env.local` or as env vars): + +| Variable | Default | Description | +|----------|---------|-------------| +| `NEXT_PUBLIC_RUSTYCMS_API_URL` | `http://127.0.0.1:3000` | RustyCMS API base URL | +| `NEXT_PUBLIC_RUSTYCMS_API_KEY` | – | API key for write operations (same as `RUSTYCMS_API_KEY`) | + +If the API requires auth, set `NEXT_PUBLIC_RUSTYCMS_API_KEY` so the Admin UI can create, update and delete entries. + ### CLI options | Option | Default | Description | @@ -74,6 +110,11 @@ RUSTYCMS_API_KEY=your-secret-token cargo run ``` rustycms/ +├── admin-ui/ # Next.js Admin UI (optional) +│ ├── src/ +│ │ ├── app/ # Dashboard, content list/edit pages +│ │ └── components/ # ContentForm, MarkdownEditor, ReferenceArrayField, Sidebar +│ └── package.json ├── Cargo.toml ├── src/ │ ├── main.rs # Entry point, CLI, server @@ -105,6 +146,8 @@ rustycms/ │ ├── blog_post.json5 │ ├── page.json5 # extends blog_post │ └── product.json5 # strict mode, constraints, unique, pattern +├── scripts/ +│ └── contentful-to-rustycms.mjs # Migration: Contentful-Export → content/de (JSON5) ├── schemas/ # Generated JSON Schema (optional, for editor) └── content/ # Content files (layer 2) ├── blog_post/ @@ -134,6 +177,9 @@ Add a `.json5` file under `types/`: // types/blog_post.json5 { name: "blog_post", + description: "Simple blog post with title, body, tags and publish status", + tags: ["content", "blog"], + category: "content", fields: { title: { type: "string", @@ -157,15 +203,18 @@ Add a `.json5` file under `types/`: ### Schema options -| Option | Type | Description | -|-----------|-----------------|----------------------------------------------------------| -| `name` | string | **Required.** Content type name | -| `extends` | string / array | Parent type(s) to inherit from | -| `strict` | boolean | Reject unknown fields | -| `pick` | string[] | Only inherit these fields from parent (`Pick`) | -| `omit` | string[] | Exclude these fields from parent (`Omit`) | -| `partial` | boolean | Make all inherited fields optional (`Partial`) | -| `reusable`| boolean | Schema only as partial (useFields), no collection/API | +| Option | Type | Description | +|--------------|-----------------|----------------------------------------------------------| +| `name` | string | **Required.** Content type name | +| `description`| string | Human-readable description (Admin UI, Swagger) | +| `tags` | string[] | Tags for grouping/filtering (e.g. `["content","blog"]`) | +| `category` | string | Category for Admin UI (e.g. `"content"`, `"components"`) | +| `extends` | string / array | Parent type(s) to inherit from | +| `strict` | boolean | Reject unknown fields | +| `pick` | string[] | Only inherit these fields from parent (`Pick`) | +| `omit` | string[] | Exclude these fields from parent (`Omit`) | +| `partial` | boolean | Make all inherited fields optional (`Partial`) | +| `reusable` | boolean | Schema only as partial (useFields), no collection/API | ### Field options @@ -179,7 +228,7 @@ Add a `.json5` file under `types/`: | `readonly` | boolean | Field cannot be changed after creation | | `nullable` | boolean | Field allows explicit `null` | | `enum` | array | Allowed values | -| `description` | string | Description (shown in Swagger UI) | +| `description` | string | Field description (Swagger UI, Admin UI) | | `minLength` | number | Minimum string length | | `maxLength` | number | Maximum string length | | `pattern` | string | Regex pattern for strings | @@ -198,9 +247,12 @@ Add a `.json5` file under `types/`: |--------------|-----------|--------------------------------| | `string` | string | Plain text | | `richtext` | string | Markdown / rich text | +| `markdown` | string | Markdown content | +| `html` | string | Raw HTML (safely embedded) | | `number` | number | Integer or float | +| `integer` | number | Integer only | | `boolean` | boolean | true / false | -| `datetime` | string | ISO 8601 date/time | +| `datetime` | string | ISO 8601 date/time | | `array` | array | List (optional `items`) | | `object` | object | Nested object | | `reference` | string | Reference to another type | @@ -212,6 +264,9 @@ Add a `.json5` file under `types/`: ```json5 { name: "page", + description: "Page with layout, SEO and content rows", + tags: ["content", "page"], + category: "content", extends: "blog_post", fields: { nav_order: { type: "number", min: 0 }, @@ -225,6 +280,9 @@ Add a `.json5` file under `types/`: ```json5 { name: "featured_article", + description: "Featured article with expiration", + tags: ["content", "blog"], + category: "content", extends: ["blog_post", "seo_meta"], fields: { featured_until: { type: "datetime" }, @@ -278,6 +336,9 @@ In strict mode, unknown fields are rejected: ```json5 { name: "product", + description: "Product with SKU, price and optional fields", + tags: ["content", "ecommerce"], + category: "content", strict: true, fields: { title: { type: "string", required: true }, @@ -289,9 +350,20 @@ In strict mode, unknown fields are rejected: ## Creating content +### Contentful-Migration + +Ein Contentful-Export (z. B. `contentful-export.json`) kann nach `content/de/` überführt werden: + +```bash +node scripts/contentful-to-rustycms.mjs [Pfad-zum-Export] +# Default: ../www.windwiderstand.de/contentful-export.json +``` + +Es werden u. a. Pages, Posts, Markdown, Link-Listen, Fullwidth-Banner, Tags, Navigation, Footer, Page-Config und Campaigns geschrieben. Nur unterstützte Row-Komponenten (markdown, link_list, fullwidth_banner, post_overview, searchable_text) landen in `row1Content`. + ### Via files -Add a `.json5` file under `content//`. The filename becomes the slug: +Add a `.json5` file under `content//` or, for localized content, `content///` (e.g. `content/de/page/`). The filename (without extension) becomes the slug. ```json5 // content/product/laptop-pro.json5 @@ -321,7 +393,7 @@ curl -X POST http://localhost:3000/api/content/product \ `_slug` sets the filename. Fields with `default` or `auto` are set automatically. -**Note:** If `RUSTYCMS_API_KEY` is set, POST/PUT/DELETE must send the key – see section “Optional API auth”. +**Note:** If `RUSTYCMS_API_KEY` is set, POST/PUT/DELETE (including schema and content) must send the key – see section “Optional API auth”. ## REST API @@ -331,6 +403,9 @@ curl -X POST http://localhost:3000/api/content/product \ |----------|-----------------------------|--------------------------------| | `GET` | `/api/collections` | List all content types | | `GET` | `/api/collections/:type` | Get schema for a type | +| `POST` | `/api/schemas` | Create new type (Admin UI: New type) | +| `PUT` | `/api/schemas/:type` | Update type definition (Admin UI: Edit type) | +| `DELETE` | `/api/schemas/:type` | Delete type definition (Admin UI: Delete type) | | `GET` | `/api/content/:type` | List all entries | | `GET` | `/api/content/:type/:slug` | Get single entry | | `POST` | `/api/content/:type` | Create entry | @@ -339,9 +414,79 @@ curl -X POST http://localhost:3000/api/content/product \ | `GET` | `/api` or `/api/` | API index (living docs: links to Swagger UI + endpoint overview) | | `GET` | `/swagger-ui` | Swagger UI | | `GET` | `/api-docs/openapi.json` | OpenAPI 3.0 spec | +| `GET` | `/api/assets` | List all uploaded image assets | +| `POST` | `/api/assets` | Upload an image asset (multipart/form-data) | +| `GET` | `/api/assets/:filename` | Serve an image asset | +| `DELETE` | `/api/assets/:filename` | Delete an image asset | | `GET` | `/api/transform` | Transform image from external URL (resize, crop, format) | | `GET` | `/health` | Health check (e.g. for K8s/Docker), always 200 + `{"status":"ok"}` | +### Asset management + +Images and other media files can be stored in `content/assets/` and served directly via the API. This is useful for logos, hero images, icons, or any static media that belongs to your content. + +**Allowed file types:** `jpg`, `jpeg`, `png`, `webp`, `avif`, `gif`, `svg` + +Files are stored at `{content-dir}/assets/` (default: `./content/assets/`). The directory is created automatically on first use. + +#### List assets + +```bash +GET /api/assets +``` + +```json +{ + "assets": [ + { "filename": "hero.jpg", "url": "/api/assets/hero.jpg", "mime_type": "image/jpeg", "size": 102400 } + ], + "total": 1 +} +``` + +#### Upload an asset + +```bash +curl -X POST http://localhost:3000/api/assets \ + -H "X-API-Key: your-key" \ + -F "file=@/path/to/hero.jpg" +``` + +Response (201): +```json +{ "filename": "hero.jpg", "url": "/api/assets/hero.jpg", "mime_type": "image/jpeg", "size": 102400 } +``` + +- Auth required (POST/DELETE) when `RUSTYCMS_API_KEY` is set +- Filenames are lowercased and sanitized (only letters, digits, `-`, `_`, `.`) +- Uploading a file that already exists returns `409 Conflict` + +#### Serve an asset + +```bash +GET /api/assets/hero.jpg +``` + +Returns the raw image with correct `Content-Type`. Response header: +``` +Cache-Control: public, max-age=31536000, immutable +``` + +Browsers and CDNs cache the file for 1 year. Combine with `/api/transform` for on-the-fly resizing of uploaded assets: + +```bash +GET /api/transform?url=http://localhost:3000/api/assets/hero.jpg&w=400&h=300&format=webp +``` + +#### Delete an asset + +```bash +curl -X DELETE http://localhost:3000/api/assets/hero.jpg \ + -H "X-API-Key: your-key" +``` + +Returns `204 No Content`. + ### Image transformation (external URLs) `GET /api/transform` loads an image from an external URL and returns it transformed. Parameters: @@ -353,8 +498,8 @@ curl -X POST http://localhost:3000/api/content/product \ | `height` | `h` | Target height in pixels. | | `aspect_ratio`| `ar` | Aspect ratio before resize, e.g. `1:1` or `16:9` (center crop). | | `fit` | – | `fill` (exact w×h), `contain` (keep aspect ratio), `cover` (fill with crop). When **w and h** are set: default `fill`; otherwise default `contain`. | -| `format` | – | Output: `jpeg`, `png`, `webp` or `avif`. Default: `jpeg`. (WebP = lossless; AVIF at default quality.) | -| `quality` | – | JPEG quality 1–100. Default: 85. | +| `format` | – | Output: `jpeg`, `png`, `webp` or `avif`. Default: `jpeg`. WebP is lossy when `quality` is used. | +| `quality` | – | Quality 1–100 for JPEG and WebP (lossy). Default: 85. | Example: 50×50 square from any image: `GET /api/transform?url=https://example.com/image.jpg&w=50&h=50&ar=1:1` diff --git a/admin-ui/.dockerignore b/admin-ui/.dockerignore new file mode 100644 index 0000000..d4cd826 --- /dev/null +++ b/admin-ui/.dockerignore @@ -0,0 +1,5 @@ +node_modules/ +.next/ +.git/ +.env* +*.md diff --git a/admin-ui/Dockerfile b/admin-ui/Dockerfile new file mode 100644 index 0000000..4a73628 --- /dev/null +++ b/admin-ui/Dockerfile @@ -0,0 +1,28 @@ +# Stage 1: Build +FROM node:22-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 +# API URL is baked in at build time (NEXT_PUBLIC_ vars are static) +ARG NEXT_PUBLIC_RUSTYCMS_API_URL +ARG NEXT_PUBLIC_RUSTYCMS_API_KEY +ENV NEXT_PUBLIC_RUSTYCMS_API_URL=$NEXT_PUBLIC_RUSTYCMS_API_URL +ENV NEXT_PUBLIC_RUSTYCMS_API_KEY=$NEXT_PUBLIC_RUSTYCMS_API_KEY +RUN npm run build + +# Stage 2: Minimal runtime image +FROM node:22-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +USER nextjs +EXPOSE 3001 +ENV PORT=3001 +ENV HOSTNAME="0.0.0.0" +CMD ["node", "server.js"] diff --git a/admin-ui/components.json b/admin-ui/components.json new file mode 100644 index 0000000..d0a71cf --- /dev/null +++ b/admin-ui/components.json @@ -0,0 +1 @@ +{"style":"new-york","rsc":true,"tsx":true,"tailwind":{"config":"","css":"src/app/globals.css","baseColor":"neutral","cssVariables":true},"aliases":{"components":"@/components","utils":"@/lib/utils","ui":"@/components/ui","lib":"@/lib","hooks":"@/hooks"}} diff --git a/admin-ui/i18n/request.ts b/admin-ui/i18n/request.ts new file mode 100644 index 0000000..e4520b8 --- /dev/null +++ b/admin-ui/i18n/request.ts @@ -0,0 +1,11 @@ +import { getRequestConfig } from 'next-intl/server'; +import { cookies } from 'next/headers'; + +export default getRequestConfig(async () => { + const store = await cookies(); + const locale = store.get('locale')?.value ?? 'en'; + return { + locale, + messages: (await import(`../messages/${locale}.json`)).default, + }; +}); diff --git a/admin-ui/messages/de.json b/admin-ui/messages/de.json new file mode 100644 index 0000000..a9ee23d --- /dev/null +++ b/admin-ui/messages/de.json @@ -0,0 +1,263 @@ +{ + "Sidebar": { + "dashboard": "Dashboard", + "types": "Typen", + "assets": "Assets", + "searchPlaceholder": "Sammlungen suchen…", + "searchAriaLabel": "Sammlungen suchen", + "closeMenu": "Menü schließen", + "loading": "Laden…", + "errorLoading": "Fehler beim Laden der Sammlungen", + "noResults": "Keine Ergebnisse für \"{query}\"" + }, + "ContentForm": { + "slugRequired": "Slug ist erforderlich.", + "slugInUse": "Slug bereits vergeben.", + "slugMustStartWith": "Der Slug muss mit \"{prefix}\" beginnen.", + "slugPrefix": "Präfix", + "slugSuffixPlaceholder": "z. B. meine-kampagne", + "slugSuffixAriaLabel": "Slug-Suffix (Präfix ist fest)", + "slugPlaceholder": "z. B. mein-beitrag", + "slugHint": "Kleinbuchstaben (a-z), Ziffern (0-9), Bindestriche. Leerzeichen werden zu Bindestrichen.", + "savedSuccessfully": "Erfolgreich gespeichert.", + "errorSaving": "Fehler beim Speichern", + "saving": "Speichern…", + "save": "Speichern", + "backToList": "Zurück zur Liste", + "pleaseSelect": "— Bitte auswählen —", + "removeEntry": "Entfernen", + "addEntry": "+ Eintrag hinzufügen", + "keyPlaceholder": "Schlüssel", + "valuePlaceholder": "Wert" + }, + "SearchableSelect": { + "placeholder": "\u2014 Bitte ausw\u00e4hlen \u2014", + "clearLabel": "\u2014 Auswahl aufheben \u2014", + "filterPlaceholder": "Filtern\u2026", + "emptyLabel": "Keine Treffer" + }, + "ReferenceField": { + "typeLabel": "Typ: {collection}", + "typesLabel": "Typen: {collections}", + "selectType": "\u2014 Typ w\u00e4hlen \u2014", + "newEntry": "Neuer Eintrag", + "noCollection": "Keine Referenz-Collection im Schema. Setze {collectionCode} oder {collectionsCode} im Typ, oder starte die API und lade die Seite neu." + }, + "ReferenceArrayField": { + "typeLabel": "Typ: {collection}", + "typesLabel": "Typen: {collections}", + "componentType": "Komponententyp", + "selectType": "\u2014 Typ w\u00e4hlen \u2014", + "selectFromExisting": "\u2014 Aus vorhandenen w\u00e4hlen \u2014", + "filterPlaceholder": "Filtern\u2026", + "emptyLabel": "Keine Treffer", + "selectExistingAriaLabel": "Vorhandenen Eintrag zum Hinzuf\u00fcgen ausw\u00e4hlen", + "moveUp": "Nach oben", + "moveDown": "Nach unten", + "remove": "Entfernen", + "newComponent": "+ Neue {collection}-Komponente", + "createNewComponent": "+ Neue Komponente erstellen\u2026", + "openInNewTab": "In neuem Tab \u00f6ffnen; dann Seite neu laden.", + "noCollection": "Keine Referenz-Collection im Schema. Setze {collectionCode} oder {collectionsCode} im Typ, oder starte die API und lade die Seite neu." + }, + "MarkdownEditor": { + "bold": "Fett", + "italic": "Kursiv", + "code": "Code", + "link": "Link", + "bulletList": "Aufz\u00e4hlungsliste", + "bulletListButton": "\u2022 Liste", + "placeholder": "Markdown eingeben\u2026 **fett**, *kursiv*, [Link](url), - Liste", + "preview": "Vorschau", + "emptyPreview": "Leer \u2014 Vorschau erscheint beim Tippen." + }, + "PaginationLinks": { + "back": "Zur\u00fcck", + "next": "Weiter", + "pageInfo": "Seite {page} von {totalPages} ({total} Eintr\u00e4ge)" + }, + "DataPreviewPanel": { + "hide": "Daten-Vorschau ausblenden", + "show": "Daten-Vorschau", + "loading": "Laden\u2026", + "errorLoading": "Fehler beim Laden" + }, + "SchemaPanel": { + "hide": "Schema ausblenden", + "show": "Schema anzeigen" + }, + "SchemaAndEditBar": { + "editSchema": "Schema bearbeiten" + }, + "SchemaAndPreviewBar": { + "hideSchema": "Schema ausblenden", + "showSchema": "Schema anzeigen", + "editSchema": "Schema bearbeiten", + "hidePreview": "Daten-Vorschau ausblenden", + "showPreview": "Daten-Vorschau", + "loading": "Laden\u2026", + "errorLoading": "Fehler beim Laden" + }, + "ReferenceOrInlineField": { + "reference": "Referenz", + "inline": "Eingebettet", + "inlineObject": "Eingebettetes Objekt (keine Referenz)", + "noInlineSchema": "Kein Inline-Schema. Seite neu laden oder API pr\u00fcfen (useFields / collection)." + }, + "LocaleSwitcher": { + "label": "Sprache" + }, + "ContentLocaleSwitcher": { + "label": "Inhaltssprache" + }, + "Dashboard": { + "title": "Dashboard", + "subtitle": "W\u00e4hle eine Sammlung zur Inhaltsverwaltung.", + "noCollections": "Keine Sammlungen geladen. Pr\u00fcfe ob die RustyCMS-API unter {url} erreichbar ist." + }, + "TypesPage": { + "title": "Typen", + "newType": "Neuer Typ", + "description": "Inhaltstypen (Sammlungen). Schema bearbeiten oder Typ l\u00f6schen. Beim L\u00f6schen wird nur die Typdefinitionsdatei entfernt; vorhandene Inhaltseintr\u00e4ge bleiben erhalten.", + "loading": "Laden\u2026", + "errorLoading": "Fehler beim Laden der Typen: {error}", + "noTypes": "Noch keine Typen vorhanden. Erstelle einen mit \"Neuer Typ\".", + "colName": "Name", + "colDescription": "Beschreibung", + "colCategory": "Kategorie", + "colActions": "Aktionen", + "confirmDelete": "\"{name}\" l\u00f6schen?", + "confirmDeleteFinal": "\"{name}\" wirklich l\u00f6schen? Dies kann nicht r\u00fcckg\u00e4ngig gemacht werden.", + "delete": "L\u00f6schen", + "yesDelete": "Ja, l\u00f6schen", + "deleting": "\u2026", + "cancel": "Abbrechen", + "edit": "Bearbeiten" + }, + "NewTypePage": { + "title": "Neuen Typ anlegen", + "description": "Erstellt einen neuen Inhaltstyp (Sammlung). Die Schemadatei wird auf dem Server unter {path} gespeichert und per Hot-Reload geladen.", + "nameRequired": "Name ist erforderlich.", + "nameInvalid": "Name: nur Kleinbuchstaben, Ziffern und Unterstriche.", + "fieldRequired": "Mindestens ein Feld erforderlich.", + "fieldNamesUnique": "Feldnamen m\u00fcssen eindeutig sein.", + "errorCreating": "Fehler beim Erstellen des Typs.", + "nameLabel": "Name", + "namePlaceholder": "z.\u00a0B. produkt, blogbeitrag", + "nameHint": "Nur Kleinbuchstaben, Ziffern und Unterstriche.", + "descriptionLabel": "Beschreibung", + "categoryLabel": "Kategorie", + "categoryPlaceholder": "z.\u00a0B. inhalt", + "tagsLabel": "Tags (kommagetrennt)", + "tagsPlaceholder": "z.\u00a0B. inhalt, blog", + "strictLabel": "Strikt (unbekannte Felder ablehnen)", + "fieldsLabel": "Felder", + "addField": "Feld hinzuf\u00fcgen", + "fieldNamePlaceholder": "Feldname", + "required": "Pflichtfeld", + "removeField": "Feld entfernen", + "collectionPlaceholder": "Sammlung (z.\u00a0B. seite)", + "fieldDescriptionPlaceholder": "Feldbeschreibung (optional)", + "creating": "Erstellen\u2026", + "createType": "Typ erstellen", + "cancel": "Abbrechen" + }, + "EditTypePage": { + "fieldRequired": "Mindestens ein Feld erforderlich.", + "fieldNamesUnique": "Feldnamen m\u00fcssen eindeutig sein.", + "errorSaving": "Fehler beim Speichern des Typs.", + "missingName": "Typname fehlt.", + "backToTypes": "Zur\u00fcck zu Typen", + "loading": "Laden\u2026", + "errorLoading": "Fehler beim Laden des Typs: {error}", + "title": "Typ bearbeiten: {name}", + "description": "Beschreibung, Kategorie, Tags und Felder \u00e4ndern. Die Schemadatei wird auf dem Server aktualisiert.", + "nameLabel": "Name", + "descriptionLabel": "Beschreibung", + "categoryLabel": "Kategorie", + "categoryPlaceholder": "z.\u00a0B. inhalt", + "tagsLabel": "Tags (kommagetrennt)", + "tagsPlaceholder": "z.\u00a0B. inhalt, blog", + "strictLabel": "Strikt (unbekannte Felder ablehnen)", + "fieldsLabel": "Felder", + "addField": "Feld hinzuf\u00fcgen", + "fieldNamePlaceholder": "Feldname", + "required": "Pflichtfeld", + "removeField": "Feld entfernen", + "collectionPlaceholder": "Sammlung (z.\u00a0B. seite)", + "fieldDescriptionPlaceholder": "Feldbeschreibung (optional)", + "saving": "Speichern\u2026", + "save": "Speichern", + "cancel": "Abbrechen" + }, + "ErrorBoundary": { + "title": "Etwas ist schiefgelaufen", + "reload": "Seite neu laden" + }, + "Breadcrumbs": { + "ariaLabel": "Breadcrumb", + "content": "Inhalte" + }, + "ContentListPage": { + "title": "Einträge", + "newEntry": "Neuer Eintrag", + "colActions": "Aktionen", + "noEntries": "Keine Einträge.", + "noEntriesCreate": "Noch keine Einträge. Erstellen Sie den ersten.", + "edit": "Bearbeiten", + "searchPlaceholder": "Suchen…", + "loading": "Laden…", + "sortBy": "Sortieren nach {field}", + "sortAsc": "Aufsteigend", + "sortDesc": "Absteigend" + }, + "ContentNewPage": { + "breadcrumbNew": "Neu", + "title": "Neuen Eintrag anlegen" + }, + "ContentEditPage": { + "title": "Eintrag bearbeiten", + "apiLink": "API-Link (Daten-Vorschau):" + }, + "AssetsPage": { + "titleAll": "Alle Assets", + "titleRoot": "Root", + "assetCount": "{count} Bild(er)", + "upload": "Hochladen", + "uploading": "Wird hochgeladen…", + "uploadedCount": "{count} Datei(en) hochgeladen.", + "dropZoneHintRoot": "Klicken oder hierher ziehen (Root)", + "dropZoneHintFolder": "Klicken oder hierher ziehen → \"{folder}\"", + "loading": "Laden…", + "errorLoading": "Fehler beim Laden der Assets", + "noAssets": "Noch keine Assets hier.", + "urlCopied": "URL kopiert.", + "copyUrl": "URL kopieren", + "confirmDelete": "\"{filename}\" löschen?", + "confirmDeleteDesc": "Dies kann nicht rückgängig gemacht werden.", + "yesDelete": "Ja, löschen", + "deleting": "…", + "cancel": "Abbrechen", + "deleted": "\"{filename}\" gelöscht.", + "folders": "Ordner", + "all": "Alle", + "root": "Root", + "newFolder": "Neuer Ordner", + "folderNamePlaceholder": "z. B. blog", + "folderCreated": "Ordner \"{name}\" erstellt.", + "folderDeleted": "Ordner \"{name}\" gelöscht.", + "confirmDeleteFolder": "Ordner \"{name}\" löschen?", + "confirmDeleteFolderDesc": "Nur leere Ordner können gelöscht werden.", + "renameTitle": "Bild umbenennen", + "renameFilenameLabel": "Dateiname", + "rename": "Umbenennen", + "renaming": "Wird umbenannt…", + "renamed": "\"{filename}\" umbenannt.", + "copyWithTransformTitle": "Kopie mit Transformation", + "copyWithTransformDesc": "Neues Asset aus diesem Bild mit Größe/Beschnitt/Format. Gleicher Ordner.", + "copyWithTransformNewName": "Neuer Dateiname", + "copyWithTransformCreate": "Kopie erstellen", + "copyWithTransformDone": "Transformierte Kopie erstellt.", + "creating": "Wird erstellt…" + } +} diff --git a/admin-ui/messages/en.json b/admin-ui/messages/en.json new file mode 100644 index 0000000..216f25b --- /dev/null +++ b/admin-ui/messages/en.json @@ -0,0 +1,263 @@ +{ + "Sidebar": { + "dashboard": "Dashboard", + "types": "Types", + "assets": "Assets", + "searchPlaceholder": "Search collections…", + "searchAriaLabel": "Search collections", + "closeMenu": "Close menu", + "loading": "Loading…", + "errorLoading": "Error loading collections", + "noResults": "No results for \"{query}\"" + }, + "ContentForm": { + "slugRequired": "Slug is required.", + "slugInUse": "Slug already in use.", + "slugMustStartWith": "Slug must start with \"{prefix}\".", + "slugPrefix": "prefix", + "slugSuffixPlaceholder": "e.g. my-campaign", + "slugSuffixAriaLabel": "Slug suffix (prefix is fixed)", + "slugPlaceholder": "e.g. my-post", + "slugHint": "Lowercase letters (a-z), digits (0-9), hyphens. Spaces become hyphens.", + "savedSuccessfully": "Saved successfully.", + "errorSaving": "Error saving", + "saving": "Saving…", + "save": "Save", + "backToList": "Back to list", + "pleaseSelect": "— Please select —", + "removeEntry": "Remove", + "addEntry": "+ Add entry", + "keyPlaceholder": "Key", + "valuePlaceholder": "Value" + }, + "SearchableSelect": { + "placeholder": "— Please select —", + "clearLabel": "— Clear selection —", + "filterPlaceholder": "Filter…", + "emptyLabel": "No matches" + }, + "ReferenceField": { + "typeLabel": "Type: {collection}", + "typesLabel": "Types: {collections}", + "selectType": "— Select type —", + "newEntry": "New entry", + "noCollection": "No reference collection in schema. Set {collectionCode} or {collectionsCode} in the type, or start the API and reload the page." + }, + "ReferenceArrayField": { + "typeLabel": "Type: {collection}", + "typesLabel": "Types: {collections}", + "componentType": "Component type", + "selectType": "— Select type —", + "selectFromExisting": "— Select from existing —", + "filterPlaceholder": "Filter…", + "emptyLabel": "No matches", + "selectExistingAriaLabel": "Select existing entry to add", + "moveUp": "Move up", + "moveDown": "Move down", + "remove": "Remove", + "newComponent": "+ New {collection} component", + "createNewComponent": "+ Create new component…", + "openInNewTab": "Open in new tab; then reload this page.", + "noCollection": "No reference collection in schema. Set {collectionCode} or {collectionsCode} in the type, or start the API and reload the page." + }, + "MarkdownEditor": { + "bold": "Bold", + "italic": "Italic", + "code": "Code", + "link": "Link", + "bulletList": "Bullet list", + "bulletListButton": "• List", + "placeholder": "Enter markdown… **bold**, *italic*, [link](url), - list", + "preview": "Preview", + "emptyPreview": "Empty — preview appears as you type." + }, + "PaginationLinks": { + "back": "Back", + "next": "Next", + "pageInfo": "Page {page} of {totalPages} ({total} entries)" + }, + "DataPreviewPanel": { + "hide": "Hide data preview", + "show": "Data preview", + "loading": "Loading…", + "errorLoading": "Error loading" + }, + "SchemaPanel": { + "hide": "Hide schema", + "show": "Show schema" + }, + "SchemaAndEditBar": { + "editSchema": "Edit schema" + }, + "SchemaAndPreviewBar": { + "hideSchema": "Hide schema", + "showSchema": "Show schema", + "editSchema": "Edit schema", + "hidePreview": "Hide data preview", + "showPreview": "Data preview", + "loading": "Loading…", + "errorLoading": "Error loading" + }, + "ReferenceOrInlineField": { + "reference": "Reference", + "inline": "Inline", + "inlineObject": "Inline object (no reference)", + "noInlineSchema": "No inline schema. Reload or check API (useFields / collection)." + }, + "LocaleSwitcher": { + "label": "Language" + }, + "ContentLocaleSwitcher": { + "label": "Content language" + }, + "Dashboard": { + "title": "Dashboard", + "subtitle": "Choose a collection to manage content.", + "noCollections": "No collections loaded. Check that the RustyCMS API is running at {url}." + }, + "TypesPage": { + "title": "Types", + "newType": "New type", + "description": "Content types (collections). Edit the schema or delete a type. Deleting removes the type definition file; existing content entries are not removed.", + "loading": "Loading…", + "errorLoading": "Error loading types: {error}", + "noTypes": "No types yet. Create one with \"New type\".", + "colName": "Name", + "colDescription": "Description", + "colCategory": "Category", + "colActions": "Actions", + "confirmDelete": "Delete \"{name}\"?", + "confirmDeleteFinal": "Really delete \"{name}\"? This cannot be undone.", + "delete": "Delete", + "yesDelete": "Yes, delete", + "deleting": "…", + "cancel": "Cancel", + "edit": "Edit" + }, + "NewTypePage": { + "title": "Add new type", + "description": "Creates a new content type (collection). The schema file is saved on the server at {path} and loaded via hot-reload.", + "nameRequired": "Name is required.", + "nameInvalid": "Name: only lowercase letters, digits and underscores.", + "fieldRequired": "At least one field required.", + "fieldNamesUnique": "Field names must be unique.", + "errorCreating": "Error creating type.", + "nameLabel": "Name", + "namePlaceholder": "e.g. product, blog_post", + "nameHint": "Lowercase letters, digits and underscores only.", + "descriptionLabel": "Description", + "categoryLabel": "Category", + "categoryPlaceholder": "e.g. content", + "tagsLabel": "Tags (comma-separated)", + "tagsPlaceholder": "e.g. content, blog", + "strictLabel": "Strict (reject unknown fields)", + "fieldsLabel": "Fields", + "addField": "Add field", + "fieldNamePlaceholder": "Field name", + "required": "Required", + "removeField": "Remove field", + "collectionPlaceholder": "Collection (e.g. page)", + "fieldDescriptionPlaceholder": "Field description (optional)", + "creating": "Creating…", + "createType": "Create type", + "cancel": "Cancel" + }, + "EditTypePage": { + "fieldRequired": "At least one field required.", + "fieldNamesUnique": "Field names must be unique.", + "errorSaving": "Error saving type.", + "missingName": "Missing type name.", + "backToTypes": "Back to Types", + "loading": "Loading…", + "errorLoading": "Error loading type: {error}", + "title": "Edit type: {name}", + "description": "Change description, category, tags, and fields. The schema file is updated on the server.", + "nameLabel": "Name", + "descriptionLabel": "Description", + "categoryLabel": "Category", + "categoryPlaceholder": "e.g. content", + "tagsLabel": "Tags (comma-separated)", + "tagsPlaceholder": "e.g. content, blog", + "strictLabel": "Strict (reject unknown fields)", + "fieldsLabel": "Fields", + "addField": "Add field", + "fieldNamePlaceholder": "Field name", + "required": "Required", + "removeField": "Remove field", + "collectionPlaceholder": "Collection (e.g. page)", + "fieldDescriptionPlaceholder": "Field description (optional)", + "saving": "Saving…", + "save": "Save", + "cancel": "Cancel" + }, + "ErrorBoundary": { + "title": "Something went wrong", + "reload": "Reload page" + }, + "Breadcrumbs": { + "ariaLabel": "Breadcrumb", + "content": "Content" + }, + "ContentListPage": { + "title": "Entries", + "newEntry": "New entry", + "colActions": "Actions", + "noEntries": "No entries.", + "noEntriesCreate": "No entries yet. Create the first one.", + "edit": "Edit", + "searchPlaceholder": "Search…", + "loading": "Loading…", + "sortBy": "Sort by {field}", + "sortAsc": "Ascending", + "sortDesc": "Descending" + }, + "ContentNewPage": { + "breadcrumbNew": "New", + "title": "Create new entry" + }, + "ContentEditPage": { + "title": "Edit entry", + "apiLink": "API link (data preview):" + }, + "AssetsPage": { + "titleAll": "All assets", + "titleRoot": "Root", + "assetCount": "{count} image(s)", + "upload": "Upload", + "uploading": "Uploading…", + "uploadedCount": "Uploaded {count} file(s).", + "dropZoneHintRoot": "Click or drag & drop to upload to root", + "dropZoneHintFolder": "Click or drag & drop to upload to \"{folder}\"", + "loading": "Loading…", + "errorLoading": "Error loading assets", + "noAssets": "No assets here yet.", + "urlCopied": "URL copied.", + "copyUrl": "Copy URL", + "confirmDelete": "Delete \"{filename}\"?", + "confirmDeleteDesc": "This cannot be undone.", + "yesDelete": "Yes, delete", + "deleting": "…", + "cancel": "Cancel", + "deleted": "\"{filename}\" deleted.", + "folders": "Folders", + "all": "All", + "root": "Root", + "newFolder": "New folder", + "folderNamePlaceholder": "e.g. blog", + "folderCreated": "Folder \"{name}\" created.", + "folderDeleted": "Folder \"{name}\" deleted.", + "confirmDeleteFolder": "Delete folder \"{name}\"?", + "confirmDeleteFolderDesc": "Only empty folders can be deleted.", + "renameTitle": "Rename image", + "renameFilenameLabel": "Filename", + "rename": "Rename", + "renaming": "Renaming…", + "renamed": "\"{filename}\" renamed.", + "copyWithTransformTitle": "Copy with transformation", + "copyWithTransformDesc": "Create a new asset from this image with resize/crop/format. Same folder.", + "copyWithTransformNewName": "New filename", + "copyWithTransformCreate": "Create copy", + "copyWithTransformDone": "Transformed copy created.", + "creating": "Creating…" + } +} diff --git a/admin-ui/next.config.ts b/admin-ui/next.config.ts index e9ffa30..7e5afee 100644 --- a/admin-ui/next.config.ts +++ b/admin-ui/next.config.ts @@ -1,7 +1,7 @@ -import type { NextConfig } from "next"; +import createNextIntlPlugin from 'next-intl/plugin'; -const nextConfig: NextConfig = { - /* config options here */ -}; +const withNextIntl = createNextIntlPlugin('./i18n/request.ts'); -export default nextConfig; +export default withNextIntl({ + output: 'standalone', +}); diff --git a/admin-ui/package-lock.json b/admin-ui/package-lock.json index ca7a6c5..9a58f6c 100644 --- a/admin-ui/package-lock.json +++ b/admin-ui/package-lock.json @@ -8,12 +8,23 @@ "name": "admin-ui", "version": "0.1.0", "dependencies": { + "@fontsource-variable/space-grotesk": "^5.2.10", + "@iconify/react": "^6.0.2", "@tanstack/react-query": "^5.90.21", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "lucide-react": "^0.577.0", "next": "16.1.6", + "next-intl": "^4.8.3", + "radix-ui": "^1.4.3", "react": "19.2.3", "react-dom": "19.2.3", "react-hook-form": "^7.71.1", - "react-markdown": "^10.1.0" + "react-markdown": "^10.1.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -456,6 +467,105 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@fontsource-variable/space-grotesk": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@fontsource-variable/space-grotesk/-/space-grotesk-5.2.10.tgz", + "integrity": "sha512-yJQO/o35/hAP3CFnpdFTwQku2yzJOae2HIpBmqkOVoxhhXJaQP3g+b6Jrz7u+eI7A5ZdCIf88uMWpBJdFiGr5w==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-3.1.1.tgz", + "integrity": "sha512-jhZbTwda+2tcNrs4kKvxrPLPjx8QsBCLCUgrrJ/S+G9YrGHWLhAyFMMBHJBnBoOwuLHd7L14FgYudviKaxkO2Q==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.0", + "@formatjs/intl-localematcher": "0.8.1", + "decimal.js": "^10.6.0", + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.0.tgz", + "integrity": "sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.1.tgz", + "integrity": "sha512-sSDmSvmmoVQ92XqWb499KrIhv/vLisJU8ITFrx7T7NZHUmMY7EL9xgRowAosaljhqnj/5iufG24QrdzB6X3ItA==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "@formatjs/icu-skeleton-parser": "2.1.1", + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.1.tgz", + "integrity": "sha512-PSFABlcNefjI6yyk8f7nyX1DC7NHmq6WaCHZLySEXBrXuLOB2f935YsnzuPjlz+ibhb9yWTdPeVX1OVcj24w2Q==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.1.tgz", + "integrity": "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.0", + "tslib": "^2.8.1" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -508,6 +618,27 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@iconify/react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@iconify/react/-/react-6.0.2.tgz", + "integrity": "sha512-SMmC2sactfpJD427WJEDN6PMyznTFMhByK9yLW0gOTtnjzzbsi/Ke/XqsumsavFPwNiXs8jSiYeZTmLCLwO+Fg==", + "license": "MIT", + "dependencies": { + "@iconify/types": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/cyberalien" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, "node_modules/@img/colour": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", @@ -1229,6 +1360,2726 @@ "node": ">=12.4.0" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1236,6 +4087,178 @@ "dev": true, "license": "MIT" }, + "node_modules/@schummar/icu-type-parser": { + "version": "1.21.5", + "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", + "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", + "license": "MIT" + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.18.tgz", + "integrity": "sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.18.tgz", + "integrity": "sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.18.tgz", + "integrity": "sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.18.tgz", + "integrity": "sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.18.tgz", + "integrity": "sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.18.tgz", + "integrity": "sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.18.tgz", + "integrity": "sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.18.tgz", + "integrity": "sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.18.tgz", + "integrity": "sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.18.tgz", + "integrity": "sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1245,6 +4268,15 @@ "tslib": "^2.8.0" } }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tailwindcss/node": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", @@ -1638,7 +4670,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -2257,6 +5289,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -2711,12 +5755,49 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2860,6 +5941,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, "node_modules/decode-named-character-reference": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", @@ -2929,12 +6016,17 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -3871,6 +6963,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -4140,6 +7241,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/icu-minify": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.8.3.tgz", + "integrity": "sha512-65Av7FLosNk7bPbmQx5z5XG2Y3T2GFppcjiXh4z1idHeVgQxlDpAmkGoYI0eFzAvrOnjpWTL5FmPDhsdfRMPEA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/icu-messageformat-parser": "^3.4.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4198,6 +7314,18 @@ "node": ">= 0.4" } }, + "node_modules/intl-messageformat": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.1.2.tgz", + "integrity": "sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "@formatjs/fast-memoize": "3.1.0", + "@formatjs/icu-messageformat-parser": "3.5.1", + "tslib": "^2.8.1" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -4394,7 +7522,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4440,7 +7567,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -5155,6 +8281,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5864,6 +8999,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/next": { "version": "16.1.6", "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", @@ -5917,6 +9061,93 @@ } } }, + "node_modules/next-intl": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.8.3.tgz", + "integrity": "sha512-PvdBDWg+Leh7BR7GJUQbCDVVaBRn37GwDBWc9sv0rVQOJDQ5JU1rVzx9EEGuOGYo0DHAl70++9LQ7HxTawdL7w==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.8.1", + "@parcel/watcher": "^2.4.1", + "@swc/core": "^1.15.2", + "icu-minify": "^4.8.3", + "negotiator": "^1.0.0", + "next-intl-swc-plugin-extractor": "^4.8.3", + "po-parser": "^2.1.1", + "use-intl": "^4.8.3" + }, + "peerDependencies": { + "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0", + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/next-intl-swc-plugin-extractor": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.8.3.tgz", + "integrity": "sha512-YcaT+R9z69XkGhpDarVFWUprrCMbxgIQYPUaXoE6LGVnLjGdo8hu3gL6bramDVjNKViYY8a/pXPy7Bna0mXORg==", + "license": "MIT" + }, + "node_modules/next-intl/node_modules/@swc/core": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.18.tgz", + "integrity": "sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.18", + "@swc/core-darwin-x64": "1.15.18", + "@swc/core-linux-arm-gnueabihf": "1.15.18", + "@swc/core-linux-arm64-gnu": "1.15.18", + "@swc/core-linux-arm64-musl": "1.15.18", + "@swc/core-linux-x64-gnu": "1.15.18", + "@swc/core-linux-x64-musl": "1.15.18", + "@swc/core-win32-arm64-msvc": "1.15.18", + "@swc/core-win32-ia32-msvc": "1.15.18", + "@swc/core-win32-x64-msvc": "1.15.18" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/next-intl/node_modules/@swc/helpers": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -5945,6 +9176,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -6227,6 +9464,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/po-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", + "integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==", + "license": "MIT" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -6329,6 +9572,106 @@ ], "license": "MIT" }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/react": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", @@ -6400,6 +9743,75 @@ "react": ">=18" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6830,6 +10242,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -7087,13 +10509,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "dev": true, "license": "MIT" }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", @@ -7329,7 +10769,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -7552,6 +10992,79 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-intl": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.3.tgz", + "integrity": "sha512-nLxlC/RH+le6g3amA508Itnn/00mE+J22ui21QhOWo5V9hCEC43+WtnRAITbJW0ztVZphev5X9gvOf2/Dk9PLA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^3.1.0", + "@schummar/icu-type-parser": "1.21.5", + "icu-minify": "^4.8.3", + "intl-messageformat": "^11.1.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/admin-ui/package.json b/admin-ui/package.json index ed92b23..bcb2b5e 100644 --- a/admin-ui/package.json +++ b/admin-ui/package.json @@ -9,12 +9,23 @@ "lint": "eslint" }, "dependencies": { + "@fontsource-variable/space-grotesk": "^5.2.10", + "@iconify/react": "^6.0.2", "@tanstack/react-query": "^5.90.21", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "lucide-react": "^0.577.0", "next": "16.1.6", + "next-intl": "^4.8.3", + "radix-ui": "^1.4.3", "react": "19.2.3", "react-dom": "19.2.3", "react-hook-form": "^7.71.1", - "react-markdown": "^10.1.0" + "react-markdown": "^10.1.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/admin-ui/src/app/admin/new-type/page.tsx b/admin-ui/src/app/admin/new-type/page.tsx index cab051d..5f87e0b 100644 --- a/admin-ui/src/app/admin/new-type/page.tsx +++ b/admin-ui/src/app/admin/new-type/page.tsx @@ -3,21 +3,25 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; +import { Icon } from "@iconify/react"; import { useQueryClient } from "@tanstack/react-query"; +import { useTranslations } from "next-intl"; import { createSchema, type SchemaDefinition, type FieldDefinition } from "@/lib/api"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; const FIELD_TYPES = [ - "string", - "number", - "integer", - "boolean", - "datetime", - "richtext", - "html", - "markdown", - "reference", - "array", - "object", + "string", "number", "integer", "boolean", "datetime", + "richtext", "html", "markdown", "reference", "array", "object", ] as const; type FieldRow = { @@ -34,6 +38,7 @@ function nextId() { } export default function NewTypePage() { + const t = useTranslations("NewTypePage"); const router = useRouter(); const queryClient = useQueryClient(); const [name, setName] = useState(""); @@ -59,45 +64,29 @@ export default function NewTypePage() { }; const updateField = (id: string, patch: Partial) => { - setFields((prev) => - prev.map((f) => (f.id === id ? { ...f, ...patch } : f)) - ); + setFields((prev) => prev.map((f) => (f.id === id ? { ...f, ...patch } : f))); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); const nameTrim = name.trim().toLowerCase().replace(/\s+/g, "_"); - if (!nameTrim) { - setError("Name is required."); - return; - } - if (!/^[a-z0-9_]+$/.test(nameTrim)) { - setError("Name: only lowercase letters, digits and underscores."); - return; - } + if (!nameTrim) { setError(t("nameRequired")); return; } + if (!/^[a-z0-9_]+$/.test(nameTrim)) { setError(t("nameInvalid")); return; } const fieldNames = fields.map((f) => f.name.trim()).filter(Boolean); - if (fieldNames.length === 0) { - setError("At least one field required."); - return; - } - const unique = new Set(fieldNames); - if (unique.size !== fieldNames.length) { - setError("Field names must be unique."); - return; - } + if (fieldNames.length === 0) { setError(t("fieldRequired")); return; } + if (new Set(fieldNames).size !== fieldNames.length) { setError(t("fieldNamesUnique")); return; } const fieldsObj: Record = {}; for (const row of fields) { const fn = row.name.trim(); if (!fn) continue; - const def: FieldDefinition = { + fieldsObj[fn] = { type: row.type, required: row.required, description: row.description.trim() || undefined, collection: row.type === "reference" && row.collection.trim() ? row.collection.trim() : undefined, }; - fieldsObj[fn] = def; } const schema: SchemaDefinition = { @@ -117,17 +106,16 @@ export default function NewTypePage() { router.push(`/content/${nameTrim}`); router.refresh(); } catch (err) { - setError(err instanceof Error ? err.message : "Error creating type."); + setError(err instanceof Error ? err.message : t("errorCreating")); setSubmitting(false); } }; return (
-

Add new type

+

{t("title")}

- Creates a new content type (collection). The schema file is saved on the server at{" "} - types/<name>.json and loaded via hot-reload. + {t("description", { path: "types/.json" })}

@@ -136,78 +124,67 @@ export default function NewTypePage() { )}
- - + {t("nameLabel")} * + + setName(e.target.value)} - placeholder="e.g. product, blog_post" - className="block w-full rounded border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900" + placeholder={t("namePlaceholder")} required /> -

- Lowercase letters, digits and underscores only. -

+

{t("nameHint")}

- - {t("descriptionLabel")} + setDescription(e.target.value)} - className="block w-full rounded border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900" />
- - {t("categoryLabel")} + setCategory(e.target.value)} - placeholder="e.g. content" - className="block w-full rounded border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900" + placeholder={t("categoryPlaceholder")} />
- - {t("tagsLabel")} + setTagsStr(e.target.value)} - placeholder="e.g. content, blog" - className="block w-full rounded border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900" + placeholder={t("tagsPlaceholder")} />
- setStrict(e.target.checked)} - className="h-4 w-4 rounded border-gray-300" + onCheckedChange={(checked) => setStrict(!!checked)} />
- - + +
{fields.map((f) => ( @@ -215,56 +192,57 @@ export default function NewTypePage() { key={f.id} className="grid gap-2 rounded border border-gray-200 bg-white p-3 sm:grid-cols-[1fr_1fr_auto_auto]" > - updateField(f.id, { name: e.target.value })} - placeholder="Field name" - className="rounded border border-gray-300 px-2 py-1.5 text-sm" + placeholder={t("fieldNamePlaceholder")} + className="h-8 text-sm" /> - -
))} @@ -272,19 +250,12 @@ export default function NewTypePage() {
- - - Cancel - + +
diff --git a/admin-ui/src/app/admin/types/[name]/edit/page.tsx b/admin-ui/src/app/admin/types/[name]/edit/page.tsx new file mode 100644 index 0000000..c96e088 --- /dev/null +++ b/admin-ui/src/app/admin/types/[name]/edit/page.tsx @@ -0,0 +1,303 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter, useParams } from "next/navigation"; +import Link from "next/link"; +import { Icon } from "@iconify/react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useTranslations } from "next-intl"; +import { fetchSchema, updateSchema, type SchemaDefinition, type FieldDefinition } from "@/lib/api"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +const FIELD_TYPES = [ + "string", "number", "integer", "boolean", "datetime", + "richtext", "html", "markdown", "reference", "array", "object", + "textOrRef", "referenceOrInline", +] as const; + +type FieldRow = { + id: string; + name: string; + type: string; + required: boolean; + description: string; + collection: string; + original?: FieldDefinition; +}; + +function nextId() { + return Math.random().toString(36).slice(2, 9); +} + +function schemaToFieldRows(schema: SchemaDefinition): FieldRow[] { + const fields = schema.fields ?? {}; + return Object.entries(fields).map(([name, def]) => ({ + id: nextId(), + name, + type: def.type ?? "string", + required: !!def.required, + description: (def.description as string) ?? "", + collection: (def.collection as string) ?? "", + original: def, + })); +} + +export default function EditTypePage() { + const t = useTranslations("EditTypePage"); + const router = useRouter(); + const params = useParams(); + const name = typeof params.name === "string" ? params.name : ""; + const queryClient = useQueryClient(); + + const { data: schema, isLoading, error: fetchError } = useQuery({ + queryKey: ["schema", name], + queryFn: () => fetchSchema(name), + enabled: !!name, + }); + + const [description, setDescription] = useState(""); + const [category, setCategory] = useState(""); + const [tagsStr, setTagsStr] = useState(""); + const [strict, setStrict] = useState(false); + const [fields, setFields] = useState([]); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + if (schema) { + setDescription(schema.description ?? ""); + setCategory(schema.category ?? ""); + setTagsStr(schema.tags?.length ? schema.tags.join(", ") : ""); + setStrict(!!schema.strict); + setFields(schemaToFieldRows(schema)); + } + }, [schema]); + + const addField = () => { + setFields((prev) => [ + ...prev, + { id: nextId(), name: "", type: "string", required: false, description: "", collection: "" }, + ]); + }; + + const removeField = (id: string) => { + setFields((prev) => (prev.length <= 1 ? prev : prev.filter((f) => f.id !== id))); + }; + + const updateField = (id: string, patch: Partial) => { + setFields((prev) => prev.map((f) => (f.id === id ? { ...f, ...patch } : f))); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + const fieldNames = fields.map((f) => f.name.trim()).filter(Boolean); + if (fieldNames.length === 0) { setError(t("fieldRequired")); return; } + if (new Set(fieldNames).size !== fieldNames.length) { setError(t("fieldNamesUnique")); return; } + + const fieldsObj: Record = {}; + for (const row of fields) { + const fn = row.name.trim(); + if (!fn) continue; + const base = row.original ?? {}; + fieldsObj[fn] = { + ...base, + type: row.type, + required: row.required, + description: row.description.trim() || undefined, + collection: row.type === "reference" && row.collection.trim() ? row.collection.trim() : undefined, + }; + } + + const payload: SchemaDefinition = { + ...schema!, + name, + description: description.trim() || undefined, + category: category.trim() || undefined, + tags: tagsStr.trim() ? tagsStr.split(",").map((t) => t.trim()).filter(Boolean) : undefined, + strict, + fields: fieldsObj, + }; + + setSubmitting(true); + try { + await updateSchema(name, payload); + await queryClient.invalidateQueries({ queryKey: ["collections"] }); + await queryClient.invalidateQueries({ queryKey: ["schema", name] }); + router.push("/admin/types"); + router.refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : t("errorSaving")); + setSubmitting(false); + } + }; + + if (!name) { + return ( +
+

{t("missingName")}

+ {t("backToTypes")} +
+ ); + } + + if (isLoading || !schema) { + return

{t("loading")}

; + } + + if (fetchError) { + return ( +
+

{t("errorLoading", { error: String(fetchError) })}

+ {t("backToTypes")} +
+ ); + } + + return ( +
+

{t("title", { name })}

+

{t("description")}

+ +
+ {error && ( +
{error}
+ )} + +
+ + +
+ +
+ + setDescription(e.target.value)} + /> +
+ +
+
+ + setCategory(e.target.value)} + placeholder={t("categoryPlaceholder")} + /> +
+
+ + setTagsStr(e.target.value)} + placeholder={t("tagsPlaceholder")} + /> +
+
+ +
+ setStrict(!!checked)} + /> + +
+ +
+
+ + +
+
+ {fields.map((f) => ( +
+ updateField(f.id, { name: e.target.value })} + placeholder={t("fieldNamePlaceholder")} + className="h-8 text-sm" + /> + + + + {f.type === "reference" && ( + updateField(f.id, { collection: e.target.value })} + placeholder={t("collectionPlaceholder")} + className="h-8 text-sm sm:col-span-2" + /> + )} + updateField(f.id, { description: e.target.value })} + placeholder={t("fieldDescriptionPlaceholder")} + className="h-8 text-sm sm:col-span-2" + /> +
+ ))} +
+
+ +
+ + +
+
+
+ ); +} diff --git a/admin-ui/src/app/admin/types/page.tsx b/admin-ui/src/app/admin/types/page.tsx new file mode 100644 index 0000000..d66a938 --- /dev/null +++ b/admin-ui/src/app/admin/types/page.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { Icon } from "@iconify/react"; +import { useTranslations } from "next-intl"; +import { toast } from "sonner"; +import { fetchCollections, deleteSchema } from "@/lib/api"; +import type { CollectionMeta } from "@/lib/api"; +import { Button } from "@/components/ui/button"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +export default function TypesPage() { + const t = useTranslations("TypesPage"); + const router = useRouter(); + const queryClient = useQueryClient(); + const [pendingDelete, setPendingDelete] = useState(null); + const [deleting, setDeleting] = useState(false); + + const { data, isLoading, error: fetchError } = useQuery({ + queryKey: ["collections"], + queryFn: fetchCollections, + }); + + const types = data?.collections ?? []; + + const handleDoDelete = async () => { + if (!pendingDelete) return; + setDeleting(true); + try { + await deleteSchema(pendingDelete); + await queryClient.invalidateQueries({ queryKey: ["collections"] }); + setPendingDelete(null); + router.refresh(); + } catch (e) { + toast.error(e instanceof Error ? e.message : "Delete failed."); + } finally { + setDeleting(false); + } + }; + + return ( +
+
+

{t("title")}

+ +
+

{t("description")}

+ + {isLoading &&

{t("loading")}

} + {fetchError && ( +

{t("errorLoading", { error: String(fetchError) })}

+ )} + {!isLoading && !fetchError && types.length === 0 && ( +

{t("noTypes")}

+ )} + + {!isLoading && !fetchError && types.length > 0 && ( +
+ + + + {t("colName")} + {t("colDescription")} + {t("colCategory")} + {t("colActions")} + + + + {types.map((c: CollectionMeta) => ( + + {c.name} + + {c.description ?? "—"} + + {c.category ?? "—"} + +
+ + +
+
+
+ ))} +
+
+
+ )} + + !o && setPendingDelete(null)}> + + + {t("confirmDelete", { name: pendingDelete ?? "" })} + + {t("confirmDeleteFinal", { name: pendingDelete ?? "" })} + + + + {t("cancel")} + + {deleting ? t("deleting") : t("yesDelete")} + + + + +
+ ); +} diff --git a/admin-ui/src/app/assets/page.tsx b/admin-ui/src/app/assets/page.tsx new file mode 100644 index 0000000..364d646 --- /dev/null +++ b/admin-ui/src/app/assets/page.tsx @@ -0,0 +1,838 @@ +"use client"; + +import { useRef, useState } from "react"; +import Image from "next/image"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { Icon } from "@iconify/react"; +import { useTranslations } from "next-intl"; +import { toast } from "sonner"; +import { + fetchAssets, + fetchFolders, + uploadAsset, + deleteAsset, + renameAsset as apiRenameAsset, + copyAssetWithTransformation, + getTransformedFilename, + createFolder, + deleteFolder, +} from "@/lib/api"; +import type { Asset, AssetFolder, TransformParams } from "@/lib/api"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +const API_BASE = process.env.NEXT_PUBLIC_RUSTYCMS_API_URL ?? "http://127.0.0.1:3000"; +const ALL = "__all__"; +const ROOT = ""; + +function formatBytes(n: number) { + if (n < 1024) return `${n} B`; + if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; + return `${(n / 1024 / 1024).toFixed(1)} MB`; +} + +function assetPath(asset: Asset) { + return asset.folder ? `${asset.folder}/${asset.filename}` : asset.filename; +} + +export default function AssetsPage() { + const t = useTranslations("AssetsPage"); + const qc = useQueryClient(); + const fileInputRef = useRef(null); + + // Folder navigation: ALL | ROOT ("") | folder name + const [selected, setSelected] = useState(ALL); + + // Upload + const [uploading, setUploading] = useState(false); + const [dragOver, setDragOver] = useState(false); + + // Delete asset + const [pendingDeleteAsset, setPendingDeleteAsset] = useState(null); + const [deletingAsset, setDeletingAsset] = useState(false); + + // Preview + const [previewAsset, setPreviewAsset] = useState(null); + + // Delete folder + const [pendingDeleteFolder, setPendingDeleteFolder] = useState(null); + const [deletingFolder, setDeletingFolder] = useState(false); + + // New folder + const [newFolderOpen, setNewFolderOpen] = useState(false); + const [newFolderName, setNewFolderName] = useState(""); + const [creatingFolder, setCreatingFolder] = useState(false); + + // Rename + const [renameTarget, setRenameTarget] = useState(null); + const [renameFilename, setRenameFilename] = useState(""); + const [renaming, setRenaming] = useState(false); + + // Copy with transformation + const [copyTransformTarget, setCopyTransformTarget] = useState(null); + const [copyTransformParams, setCopyTransformParams] = useState({ format: "webp" }); + const [copyTransformLoading, setCopyTransformLoading] = useState(false); + + // Assets query: undefined = all, "" = root, "name" = folder + const assetsFolder = selected === ALL ? undefined : selected; + const { data: assetsData, isLoading: assetsLoading, error: assetsError } = useQuery({ + queryKey: ["assets", assetsFolder], + queryFn: () => fetchAssets(assetsFolder), + }); + + const { data: foldersData, isLoading: foldersLoading } = useQuery({ + queryKey: ["folders"], + queryFn: fetchFolders, + }); + + const assets = assetsData?.assets ?? []; + const folders = foldersData?.folders ?? []; + + // ── Upload ────────────────────────────────────────────────────────────── + + async function handleFiles(files: FileList | null) { + if (!files || files.length === 0) return; + setUploading(true); + const targetFolder = selected === ALL || selected === ROOT ? undefined : selected; + let ok = 0; + for (const file of Array.from(files)) { + try { + await uploadAsset(file, targetFolder); + ok++; + } catch (e) { + toast.error(`${file.name}: ${e instanceof Error ? e.message : "Upload failed"}`); + } + } + if (ok > 0) { + toast.success(t("uploadedCount", { count: ok })); + await qc.invalidateQueries({ queryKey: ["assets"] }); + await qc.invalidateQueries({ queryKey: ["folders"] }); + } + setUploading(false); + if (fileInputRef.current) fileInputRef.current.value = ""; + } + + // ── Delete asset ───────────────────────────────────────────────────────── + + async function handleDeleteAsset() { + if (!pendingDeleteAsset) return; + setDeletingAsset(true); + try { + await deleteAsset(assetPath(pendingDeleteAsset)); + toast.success(t("deleted", { filename: pendingDeleteAsset.filename })); + await qc.invalidateQueries({ queryKey: ["assets"] }); + await qc.invalidateQueries({ queryKey: ["folders"] }); + setPendingDeleteAsset(null); + } catch (e) { + toast.error(e instanceof Error ? e.message : "Delete failed"); + } finally { + setDeletingAsset(false); + } + } + + // ── Delete folder ──────────────────────────────────────────────────────── + + async function handleDeleteFolder() { + if (!pendingDeleteFolder) return; + setDeletingFolder(true); + try { + await deleteFolder(pendingDeleteFolder.name); + toast.success(t("folderDeleted", { name: pendingDeleteFolder.name })); + await qc.invalidateQueries({ queryKey: ["folders"] }); + await qc.invalidateQueries({ queryKey: ["assets"] }); + if (selected === pendingDeleteFolder.name) setSelected(ALL); + setPendingDeleteFolder(null); + } catch (e) { + toast.error(e instanceof Error ? e.message : "Delete folder failed"); + } finally { + setDeletingFolder(false); + } + } + + // ── Create folder ──────────────────────────────────────────────────────── + + async function handleCreateFolder(e: React.FormEvent) { + e.preventDefault(); + if (!newFolderName.trim()) return; + setCreatingFolder(true); + try { + await createFolder(newFolderName.trim()); + toast.success(t("folderCreated", { name: newFolderName.trim() })); + await qc.invalidateQueries({ queryKey: ["folders"] }); + setSelected(newFolderName.trim().toLowerCase()); + setNewFolderName(""); + setNewFolderOpen(false); + } catch (e) { + toast.error(e instanceof Error ? e.message : "Create folder failed"); + } finally { + setCreatingFolder(false); + } + } + + // ── Copy URL ────────────────────────────────────────────────────────────── + + function copyUrl(asset: Asset) { + navigator.clipboard.writeText(`${API_BASE}${asset.url}`); + toast.success(t("urlCopied")); + } + + // ── Rename ─────────────────────────────────────────────────────────────── + + async function handleRename(e: React.FormEvent) { + e.preventDefault(); + if (!renameTarget || !renameFilename.trim()) return; + setRenaming(true); + try { + const path = assetPath(renameTarget); + await apiRenameAsset(path, renameFilename.trim()); + toast.success(t("renamed", { filename: renameFilename.trim() })); + await qc.invalidateQueries({ queryKey: ["assets"] }); + await qc.invalidateQueries({ queryKey: ["folders"] }); + setRenameTarget(null); + setRenameFilename(""); + } catch (err) { + toast.error(err instanceof Error ? err.message : "Rename failed"); + } finally { + setRenaming(false); + } + } + + // ── Copy with transformation ────────────────────────────────────────────── + + async function handleCopyWithTransform(e: React.FormEvent) { + e.preventDefault(); + if (!copyTransformTarget) return; + setCopyTransformLoading(true); + try { + const folder = copyTransformTarget.folder ?? undefined; + await copyAssetWithTransformation(copyTransformTarget, copyTransformParams, folder); + toast.success(t("copyWithTransformDone")); + await qc.invalidateQueries({ queryKey: ["assets"] }); + await qc.invalidateQueries({ queryKey: ["folders"] }); + setCopyTransformTarget(null); + } catch (err) { + toast.error(err instanceof Error ? err.message : "Copy failed"); + } finally { + setCopyTransformLoading(false); + } + } + + // ── Upload hint ─────────────────────────────────────────────────────────── + + const uploadHint = + selected === ALL || selected === ROOT + ? t("dropZoneHintRoot") + : t("dropZoneHintFolder", { folder: selected }); + + // ── Render ──────────────────────────────────────────────────────────────── + + const folderOptions = [ + { value: ALL, label: t("all") }, + { value: ROOT, label: t("root") }, + ...folders.map((f) => ({ value: f.name, label: f.name })), + ]; + + return ( +
+ {/* ── Folder sidebar (desktop) ── */} + + + {/* ── Mobile folder selector ── */} +
+
+ + {t("folders")}: + + + {!newFolderOpen ? ( + + ) : null} +
+ {newFolderOpen && ( +
+ setNewFolderName(e.target.value)} + placeholder={t("folderNamePlaceholder")} + className="min-h-[44px] flex-1 text-sm" + autoFocus + disabled={creatingFolder} + /> + + +
+ )} +
+ + {/* ── Main content ── */} +
+ {/* Header */} +
+
+

+ {selected === ALL ? t("titleAll") : selected === ROOT ? t("titleRoot") : selected} +

+

+ {t("assetCount", { count: assetsData?.total ?? 0 })} +

+
+ +
+ + {/* Hidden file input */} + handleFiles(e.target.files)} + /> + + {/* Scrollable grid area */} +
+ {/* Drop zone */} +
{ e.preventDefault(); setDragOver(true); }} + onDragLeave={() => setDragOver(false)} + onDrop={(e) => { e.preventDefault(); setDragOver(false); handleFiles(e.dataTransfer.files); }} + onClick={() => fileInputRef.current?.click()} + className={`mb-4 flex min-h-[56px] cursor-pointer items-center justify-center rounded-lg border-2 border-dashed px-4 py-4 transition-colors md:mb-6 md:py-5 md:px-6 touch-manipulation ${ + dragOver + ? "border-primary bg-primary/5" + : "border-accent-200 hover:border-accent-300 hover:bg-accent-50" + }`} + > +
+ + {uploadHint} +
+
+ + {/* Loading / error */} + {assetsLoading &&

{t("loading")}

} + {assetsError &&

{t("errorLoading")}

} + + {/* Empty */} + {!assetsLoading && !assetsError && assets.length === 0 && ( +

{t("noAssets")}

+ )} + + {/* Grid */} + {assets.length > 0 && ( +
+ {assets.map((asset) => ( + setPreviewAsset(asset)} + onCopy={() => copyUrl(asset)} + onRename={() => { setRenameTarget(asset); setRenameFilename(asset.filename); }} + onCopyTransform={() => { setCopyTransformTarget(asset); setCopyTransformParams({ format: "webp" }); }} + onDelete={() => setPendingDeleteAsset(asset)} + /> + ))} +
+ )} +
+
+ + {/* ── Rename dialog ── */} + { if (!o) { setRenameTarget(null); setRenameFilename(""); } }}> + + + {t("renameTitle")} + +
+
+ + setRenameFilename(e.target.value)} + placeholder="image.jpg" + disabled={renaming} + /> +
+
+ + +
+
+
+
+ + {/* ── Copy with transformation dialog ── */} + { if (!o) setCopyTransformTarget(null); }}> + + + {t("copyWithTransformTitle")} + +
+

{t("copyWithTransformDesc")}

+
+
+ + setCopyTransformParams((p) => ({ ...p, w: e.target.value ? Number(e.target.value) : undefined }))} + className="h-8 text-sm" + /> +
+
+ + setCopyTransformParams((p) => ({ ...p, h: e.target.value ? Number(e.target.value) : undefined }))} + className="h-8 text-sm" + /> +
+
+ + setCopyTransformParams((p) => ({ ...p, ar: e.target.value || undefined }))} + className="h-8 text-sm" + /> +
+
+ + +
+
+ + +
+
+ {copyTransformTarget && ( +

+ {t("copyWithTransformNewName")}: {getTransformedFilename(copyTransformTarget.filename, copyTransformParams)} +

+ )} +
+ + +
+
+
+
+ + {/* ── Preview dialog ── */} + { if (!o) setPreviewAsset(null); }}> + + {previewAsset && ( + <> + + + {previewAsset.filename} + + +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {previewAsset.filename} +
+

+ {formatBytes(previewAsset.size)} + {previewAsset.folder && ( + + {previewAsset.folder} + + )} +

+ +
+ + )} +
+
+ + {/* ── Delete asset dialog ── */} + { if (!o) setPendingDeleteAsset(null); }} + > + + + {t("confirmDelete", { filename: pendingDeleteAsset?.filename ?? "" })} + {t("confirmDeleteDesc")} + + + {t("cancel")} + + {deletingAsset ? t("deleting") : t("yesDelete")} + + + + + + {/* ── Delete folder dialog ── */} + { if (!o) setPendingDeleteFolder(null); }} + > + + + {t("confirmDeleteFolder", { name: pendingDeleteFolder?.name ?? "" })} + {t("confirmDeleteFolderDesc")} + + + {t("cancel")} + + {deletingFolder ? t("deleting") : t("yesDelete")} + + + + +
+ ); +} + +// ── Sub-components ────────────────────────────────────────────────────────── + +function FolderItem({ + label, + icon, + count, + active, + onClick, + onDelete, +}: { + label: string; + icon: string; + count?: number; + active: boolean; + onClick: () => void; + onDelete?: () => void; +}) { + return ( +
+ + + {label} + + + {count !== undefined && ( + {count} + )} + {onDelete && ( + + )} + +
+ ); +} + +function AssetCard({ + asset, + showFolder, + onPreview, + onCopy, + onRename, + onCopyTransform, + onDelete, +}: { + asset: Asset; + showFolder: boolean; + onPreview: () => void; + onCopy: () => void; + onRename: () => void; + onCopyTransform: () => void; + onDelete: () => void; +}) { + return ( +
+ {/* Thumbnail (click to preview) */} + + + {/* Info */} +
+

+ {asset.filename} +

+

+ {showFolder && asset.folder && ( + {asset.folder} + )} + {formatBytes(asset.size)} +

+
+ + {/* Mobile: always-visible action row (touch-friendly) */} +
+ + + + +
+ + {/* Hover actions – stopPropagation so click doesn’t trigger thumbnail preview */} +
+ + + + +
+
+ ); +} diff --git a/admin-ui/src/app/content/[collection]/[slug]/page.tsx b/admin-ui/src/app/content/[collection]/[slug]/page.tsx new file mode 100644 index 0000000..056b91d --- /dev/null +++ b/admin-ui/src/app/content/[collection]/[slug]/page.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { useEffect } from "react"; +import Link from "next/link"; +import { useParams, useSearchParams } from "next/navigation"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { Icon } from "@iconify/react"; +import { useTranslations } from "next-intl"; +import { fetchSchema, fetchEntry, fetchLocales } from "@/lib/api"; +import { ContentForm } from "@/components/ContentForm"; +import { SchemaAndPreviewBar } from "@/components/SchemaAndPreviewBar"; +import { ContentLocaleSwitcher } from "@/components/ContentLocaleSwitcher"; +import { Breadcrumbs } from "@/components/Breadcrumbs"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function ContentEditPage() { + const t = useTranslations("ContentEditPage"); + const tList = useTranslations("ContentForm"); + const params = useParams(); + const searchParams = useSearchParams(); + const queryClient = useQueryClient(); + const collection = typeof params.collection === "string" ? params.collection : ""; + const slug = typeof params.slug === "string" ? params.slug : ""; + const locale = searchParams.get("_locale") ?? undefined; + + const { data: schema, isLoading: schemaLoading, error: schemaError } = useQuery({ + queryKey: ["schema", collection], + queryFn: () => fetchSchema(collection), + enabled: !!collection, + }); + + const { + data: entry, + isLoading: entryLoading, + error: entryError, + } = useQuery({ + queryKey: ["entry", collection, slug, locale ?? ""], + queryFn: () => + fetchEntry(collection, slug, { + ...(locale ? { _locale: locale } : {}), + }), + enabled: !!collection && !!slug, + }); + + const { data: localesData } = useQuery({ + queryKey: ["locales"], + queryFn: fetchLocales, + }); + const locales = localesData?.locales ?? []; + const defaultLocale = localesData?.default ?? null; + + const onSuccess = () => { + void queryClient.invalidateQueries({ queryKey: ["entry", collection, slug] }); + void queryClient.invalidateQueries({ queryKey: ["content", collection] }); + }; + + if (!collection || !slug) { + return ( +
+ Missing collection or slug. +
+ ); + } + + const localeQ = locale ? `?_locale=${locale}` : ""; + const listHref = `/content/${collection}${localeQ}`; + const isLoading = schemaLoading || entryLoading; + const error = schemaError ?? entryError; + const tBread = useTranslations("Breadcrumbs"); + + useEffect(() => { + document.title = schema && entry + ? `${slug} — ${collection} — RustyCMS Admin` + : "RustyCMS Admin"; + return () => { + document.title = "RustyCMS Admin"; + }; + }, [collection, slug, schema, entry]); + + return ( +
+ +
+
+ + +
+ +
+ +

+ {t("title")} — {slug} +

+ + {isLoading && ( +
+ + + + +
+ )} + {error && ( +

+ {error instanceof Error ? error.message : String(error)} +

+ )} + + {!isLoading && !error && schema && entry && ( + } + slug={slug} + locale={locale} + onSuccess={onSuccess} + /> + )} + + {!isLoading && !error && schema && !entry && ( +

+ Entry not found. It may have been deleted or the slug is wrong. +

+ )} +
+ ); +} diff --git a/admin-ui/src/app/content/[collection]/new/page.tsx b/admin-ui/src/app/content/[collection]/new/page.tsx new file mode 100644 index 0000000..a53a6a0 --- /dev/null +++ b/admin-ui/src/app/content/[collection]/new/page.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { useEffect } from "react"; +import Link from "next/link"; +import { useParams, useSearchParams } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; +import { Icon } from "@iconify/react"; +import { useTranslations } from "next-intl"; +import { fetchSchema, fetchLocales } from "@/lib/api"; +import { ContentForm } from "@/components/ContentForm"; +import { SchemaAndEditBar } from "@/components/SchemaAndEditBar"; +import { ContentLocaleSwitcher } from "@/components/ContentLocaleSwitcher"; +import { Breadcrumbs } from "@/components/Breadcrumbs"; +import { Button } from "@/components/ui/button"; + +export default function ContentNewPage() { + const t = useTranslations("ContentNewPage"); + const tList = useTranslations("ContentForm"); + const params = useParams(); + const searchParams = useSearchParams(); + const collection = typeof params.collection === "string" ? params.collection : ""; + const locale = searchParams.get("_locale") ?? undefined; + + const { data: schema, isLoading: schemaLoading, error: schemaError } = useQuery({ + queryKey: ["schema", collection], + queryFn: () => fetchSchema(collection), + enabled: !!collection, + }); + + const { data: localesData } = useQuery({ + queryKey: ["locales"], + queryFn: fetchLocales, + }); + const locales = localesData?.locales ?? []; + const defaultLocale = localesData?.default ?? null; + + if (!collection) { + return ( +
+ Missing collection name. +
+ ); + } + + const localeQ = locale ? `?_locale=${locale}` : ""; + const listHref = `/content/${collection}${localeQ}`; + const tBread = useTranslations("Breadcrumbs"); + const tNew = useTranslations("ContentNewPage"); + + useEffect(() => { + document.title = collection ? `New — ${collection} — RustyCMS Admin` : "RustyCMS Admin"; + return () => { + document.title = "RustyCMS Admin"; + }; + }, [collection]); + + return ( +
+ +
+
+ + +
+ +
+ +

+ {t("title")} — {collection} +

+ + {schemaLoading &&

Loading…

} + {schemaError && ( +

+ {schemaError instanceof Error ? schemaError.message : String(schemaError)} +

+ )} + {!schemaLoading && !schemaError && schema && ( + + )} +
+ ); +} diff --git a/admin-ui/src/app/content/[collection]/page.tsx b/admin-ui/src/app/content/[collection]/page.tsx new file mode 100644 index 0000000..a2c9416 --- /dev/null +++ b/admin-ui/src/app/content/[collection]/page.tsx @@ -0,0 +1,233 @@ +"use client"; + +import { useEffect } from "react"; +import Link from "next/link"; +import { useParams, useSearchParams } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; +import { Icon } from "@iconify/react"; +import { useTranslations } from "next-intl"; +import { fetchContentList, fetchLocales } from "@/lib/api"; +import { Button } from "@/components/ui/button"; +import { SearchBar } from "@/components/SearchBar"; +import { PaginationLinks } from "@/components/PaginationLinks"; +import { ContentLocaleSwitcher } from "@/components/ContentLocaleSwitcher"; +import { Breadcrumbs } from "@/components/Breadcrumbs"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Skeleton } from "@/components/ui/skeleton"; + +const PER_PAGE = 20; + +export default function ContentListPage() { + const t = useTranslations("ContentListPage"); + const params = useParams(); + const searchParams = useSearchParams(); + const collection = typeof params.collection === "string" ? params.collection : ""; + + const page = Math.max(1, parseInt(searchParams.get("_page") ?? "1", 10) || 1); + const sort = searchParams.get("_sort") ?? undefined; + const order = (searchParams.get("_order") ?? "asc") as "asc" | "desc"; + const q = searchParams.get("_q") ?? undefined; + const locale = searchParams.get("_locale") ?? undefined; + + const { data: localesData } = useQuery({ + queryKey: ["locales"], + queryFn: fetchLocales, + }); + const locales = localesData?.locales ?? []; + const defaultLocale = localesData?.default ?? null; + + const listParams = { + _page: page, + _per_page: PER_PAGE, + ...(sort ? { _sort: sort, _order: order } : {}), + ...(q?.trim() ? { _q: q.trim() } : {}), + ...(locale ? { _locale: locale } : {}), + }; + + const { data, isLoading, error } = useQuery({ + queryKey: ["content", collection, listParams], + queryFn: () => fetchContentList(collection, listParams), + enabled: !!collection, + }); + + if (!collection) { + return ( +
+ Missing collection name. +
+ ); + } + + const items = data?.items ?? []; + const total = data?.total ?? 0; + const totalPages = data?.total_pages ?? 1; + const localeQ = locale ? `_locale=${locale}` : ""; + const baseQuery = new URLSearchParams(); + if (locale) baseQuery.set("_locale", locale); + if (q?.trim()) baseQuery.set("_q", q.trim()); + const sortQuery = (field: string, order: "asc" | "desc") => { + const p = new URLSearchParams(baseQuery); + p.set("_sort", field); + p.set("_order", order); + return p.toString(); + }; + const isSortSlug = sort === "_slug" || !sort; + const nextSlugOrder = isSortSlug && order === "asc" ? "desc" : "asc"; + + const tBread = useTranslations("Breadcrumbs"); + + useEffect(() => { + document.title = collection ? `${collection} — RustyCMS Admin` : "RustyCMS Admin"; + return () => { + document.title = "RustyCMS Admin"; + }; + }, [collection]); + + return ( +
+ +
+

+ {collection} +

+
+ + + +
+
+ + {isLoading && ( +
+ +
+ + + + _slug + {t("colActions")} + + + + {[1, 2, 3, 4, 5].map((i) => ( + + + + + ))} + +
+
+
+ )} + {error && ( +

+ {error instanceof Error ? error.message : String(error)} +

+ )} + + {!isLoading && !error && items.length === 0 && ( +
+

+ {t("noEntriesCreate")} +

+
+ +
+
+ )} + + {!isLoading && !error && items.length > 0 && ( + <> +
+ + + + + + _slug + {isSortSlug && ( + + )} + + + {t("colActions")} + + + + {items.map((entry: Record) => { + const slug = entry._slug as string | undefined; + if (slug == null) return null; + const editHref = `/content/${collection}/${encodeURIComponent(slug)}${localeQ ? `?${localeQ}` : ""}`; + return ( + + {slug} + + + + + ); + })} + +
+
+ + + )} +
+ ); +} diff --git a/admin-ui/src/app/globals.css b/admin-ui/src/app/globals.css index 3693591..ac8f568 100644 --- a/admin-ui/src/app/globals.css +++ b/admin-ui/src/app/globals.css @@ -1,34 +1,125 @@ @import "tailwindcss"; +@plugin "tailwindcss-animate"; +@import "@fontsource-variable/space-grotesk"; :root { --background: #ffffff; --foreground: #171717; + --font-sans: "Space Grotesk Variable", ui-sans-serif, system-ui, sans-serif; + + /* Rose/Rost Accent-Skala (bleibt erhalten für bestehende Tailwind-Utilities) */ + --accent-50: #fff1f2; + --accent-100: #ffe4e6; + --accent-200: #fecdd3; + --accent-300: #fda4af; + --accent-400: #fb7185; + --accent-500: #f43f5e; + --accent-600: #e11d48; + --accent-700: #be123c; + --accent-800: #9f1239; + --accent-900: #881337; + + /* shadcn semantische Variablen — primary = rose */ + --primary: #e11d48; + --primary-foreground: #ffffff; + --secondary: #fff1f2; + --secondary-foreground: #9f1239; + --muted: #f9fafb; + --muted-foreground: #6b7280; + --card: #ffffff; + --card-foreground: #171717; + --popover: #ffffff; + --popover-foreground: #171717; + --border: #d1d5db; + --input: #d1d5db; + --ring: #fda4af; + --destructive: #dc2626; + --destructive-foreground: #ffffff; + --radius: 0.5rem; + /* shadcn accent = subtiler Hover-Hintergrund (z.B. SelectItem, CommandItem) */ + --shadcn-accent: #fff1f2; + --shadcn-accent-foreground: #881337; } + @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-sans); + + /* Accent-Skala für bestehende Utilities */ + --color-accent-50: var(--accent-50); + --color-accent-100: var(--accent-100); + --color-accent-200: var(--accent-200); + --color-accent-300: var(--accent-300); + --color-accent-400: var(--accent-400); + --color-accent-500: var(--accent-500); + --color-accent-600: var(--accent-600); + --color-accent-700: var(--accent-700); + --color-accent-800: var(--accent-800); + --color-accent-900: var(--accent-900); + + /* shadcn semantische Tokens */ + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + /* shadcn accent Utility-Klassen (bg-accent, text-accent-foreground) */ + --color-accent: var(--shadcn-accent); + --color-accent-foreground: var(--shadcn-accent-foreground); + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +html, body { + height: 100%; + overflow: hidden; +} + +/* Safe area for notched devices (e.g. iPhone) */ +@supports (padding: env(safe-area-inset-top)) { + body { + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); } } body { background: var(--background); color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + font-family: var(--font-sans); } -/* Lesbare Eingabefelder: dunkler Text, heller Hintergrund */ -input:not([type="checkbox"]):not([type="radio"]), -textarea, -select { - color: #111827; /* gray-900 */ - background-color: #ffffff; +/* Links: exclude elements used as buttons (Slot.Root forwards data-slot) */ +a:not([data-slot="button"]) { + color: var(--accent-700); + text-decoration: underline; + text-underline-offset: 2px; +} +a:not([data-slot="button"]):hover { + color: var(--accent-800); +} + +/* Text-Links (Rostrot) */ +.link-accent { + color: var(--accent-700); + text-decoration: underline; + text-underline-offset: 2px; +} +.link-accent:hover { + color: var(--accent-800); } diff --git a/admin-ui/src/app/globals.css.bak b/admin-ui/src/app/globals.css.bak new file mode 100644 index 0000000..81895e3 --- /dev/null +++ b/admin-ui/src/app/globals.css.bak @@ -0,0 +1,110 @@ +@import "tailwindcss"; +@import "@fontsource-variable/space-grotesk"; + +/* Accent (Rost/Rose) – zentral für Buttons, Links, Hover; hier anpassen für neues Look */ +:root { + --background: #ffffff; + --foreground: #171717; + --font-sans: "Space Grotesk Variable", ui-sans-serif, system-ui, sans-serif; + --accent-50: #fff1f2; + --accent-100: #ffe4e6; + --accent-200: #fecdd3; + --accent-300: #fda4af; + --accent-400: #fb7185; + --accent-500: #f43f5e; + --accent-600: #e11d48; + --accent-700: #be123c; + --accent-800: #9f1239; + --accent-900: #881337; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-sans); + --font-mono: var(--font-geist-mono); + --color-accent-50: var(--accent-50); + --color-accent-100: var(--accent-100); + --color-accent-200: var(--accent-200); + --color-accent-300: var(--accent-300); + --color-accent-400: var(--accent-400); + --color-accent-500: var(--accent-500); + --color-accent-600: var(--accent-600); + --color-accent-700: var(--accent-700); + --color-accent-800: var(--accent-800); + --color-accent-900: var(--accent-900); +} + +/* Primär-Button (z.B. „Speichern“, „New entry“) */ +.btn-primary { + @apply shrink-0 whitespace-nowrap rounded-lg px-4 py-2 text-sm font-medium text-white transition-colors; + background-color: var(--accent-600); +} +.btn-primary:hover:not(:disabled) { + background-color: var(--accent-700); +} +.btn-primary:disabled { + @apply opacity-50; +} + +/* Sekundär-Button / Outline (z.B. „Abbrechen“, „Back“) */ +.btn-secondary { + @apply rounded-lg border px-4 py-2 text-sm font-medium transition-colors; + border-color: var(--accent-200); + color: var(--accent-800); + background-color: #fff; +} +.btn-secondary:hover { + background-color: var(--accent-50); +} + +/* Kleine Outline-Buttons / Links (z.B. Schema bearbeiten, Pagination) */ +.btn-outline { + @apply inline-flex items-center gap-2 rounded border px-3 py-1.5 text-sm font-medium transition-colors; + border-color: var(--accent-200); + color: var(--accent-800); + background-color: #fff; +} +.btn-outline:hover { + background-color: var(--accent-50); +} + +/* Text-Links (Rostrot, zentral steuerbar) – Klasse für Inline-Links */ +.link-accent { + color: var(--accent-700); + text-decoration: underline; + text-underline-offset: 2px; +} +.link-accent:hover { + color: var(--accent-800); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: var(--font-sans); +} + +a { + color: var(--accent-700); + text-decoration: underline; + text-underline-offset: 2px; +} +a:hover { + color: var(--accent-800); +} + +/* Lesbare Eingabefelder: dunkler Text, heller Hintergrund */ +input:not([type="checkbox"]):not([type="radio"]), +textarea, +select { + color: #111827; /* gray-900 */ + background-color: #ffffff; +} diff --git a/admin-ui/src/app/layout.tsx b/admin-ui/src/app/layout.tsx index 768f9f3..50f4100 100644 --- a/admin-ui/src/app/layout.tsx +++ b/admin-ui/src/app/layout.tsx @@ -1,14 +1,12 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import type { Metadata, Viewport } from "next"; +import { Geist_Mono } from "next/font/google"; +import { NextIntlClientProvider } from "next-intl"; +import { getLocale, getMessages } from "next-intl/server"; import { Providers } from "./providers"; -import { Sidebar } from "@/components/Sidebar"; +import { AppShell } from "@/components/AppShell"; +import { Toaster } from "sonner"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"], @@ -19,22 +17,29 @@ export const metadata: Metadata = { description: "RustyCMS admin interface", }; -export default function RootLayout({ +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + viewportFit: "cover", +}; + +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const locale = await getLocale(); + const messages = await getMessages(); + return ( - - - -
- -
{children}
-
-
+ + + + + {children} + + + ); diff --git a/admin-ui/src/app/page.tsx b/admin-ui/src/app/page.tsx index 1c3e86b..47e17a4 100644 --- a/admin-ui/src/app/page.tsx +++ b/admin-ui/src/app/page.tsx @@ -1,7 +1,9 @@ import Link from "next/link"; +import { getTranslations } from "next-intl/server"; import { fetchCollections } from "@/lib/api"; export default async function DashboardPage() { + const t = await getTranslations("Dashboard"); let collections: { name: string }[] = []; try { const res = await fetchCollections(); @@ -12,16 +14,16 @@ export default async function DashboardPage() { return (
-

Dashboard

+

{t("title")}

- Choose a collection to manage content. + {t("subtitle")}

    {collections.map((c) => (
  • {c.name} @@ -30,12 +32,9 @@ export default async function DashboardPage() {
{collections.length === 0 && (

- No collections loaded. Check that the RustyCMS API is running at{" "} - - {process.env.NEXT_PUBLIC_RUSTYCMS_API_URL ?? - "http://127.0.0.1:3000"} - - . + {t("noCollections", { + url: process.env.NEXT_PUBLIC_RUSTYCMS_API_URL ?? "http://127.0.0.1:3000", + })}

)}
diff --git a/admin-ui/src/components/AppShell.tsx b/admin-ui/src/components/AppShell.tsx new file mode 100644 index 0000000..738597d --- /dev/null +++ b/admin-ui/src/components/AppShell.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { useState } from "react"; +import { Sidebar } from "@/components/Sidebar"; +import { ErrorBoundary } from "@/components/ErrorBoundary"; + +type AppShellProps = { + locale: string; + children: React.ReactNode; +}; + +export function AppShell({ locale, children }: AppShellProps) { + const [sidebarOpen, setSidebarOpen] = useState(false); + + return ( +
+ {/* Mobile backdrop */} + {sidebarOpen && ( + + RustyCMS Admin + +
+ {children} +
+
+ + ); +} diff --git a/admin-ui/src/components/BackButton.tsx b/admin-ui/src/components/BackButton.tsx new file mode 100644 index 0000000..fb01b8f --- /dev/null +++ b/admin-ui/src/components/BackButton.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { Icon } from "@iconify/react"; + +export function BackButton({ label = "Back" }: { label?: string }) { + const router = useRouter(); + return ( + + ); +} diff --git a/admin-ui/src/components/Breadcrumbs.tsx b/admin-ui/src/components/Breadcrumbs.tsx new file mode 100644 index 0000000..87cd61e --- /dev/null +++ b/admin-ui/src/components/Breadcrumbs.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { useTranslations } from "next-intl"; + +export type BreadcrumbItem = { label: string; href?: string }; + +type Props = { items: BreadcrumbItem[] }; + +export function Breadcrumbs({ items }: Props) { + if (items.length === 0) return null; + const t = useTranslations("Breadcrumbs"); + + return ( + + ); +} diff --git a/admin-ui/src/components/ContentForm.tsx b/admin-ui/src/components/ContentForm.tsx index dab9838..8bd1611 100644 --- a/admin-ui/src/components/ContentForm.tsx +++ b/admin-ui/src/components/ContentForm.tsx @@ -1,12 +1,30 @@ "use client"; -import { useId } from "react"; +import { useId, useEffect, useRef } from "react"; import { useRouter } from "next/navigation"; import { useForm, Controller } from "react-hook-form"; +import { Icon } from "@iconify/react"; +import { useTranslations } from "next-intl"; +import { toast } from "sonner"; import type { SchemaDefinition, FieldDefinition } from "@/lib/api"; import { checkSlug } from "@/lib/api"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { ReferenceArrayField } from "./ReferenceArrayField"; +import { ReferenceField } from "./ReferenceField"; +import { ReferenceOrInlineField } from "./ReferenceOrInlineField"; import { MarkdownEditor } from "./MarkdownEditor"; +import { CollapsibleSection } from "./ui/collapsible"; type Props = { collection: string; @@ -17,34 +35,75 @@ type Props = { onSuccess?: () => void; }; -/** _slug field with format and duplicate check via API. */ +/** Slug prefix for new entries: collection name with underscores → hyphens, plus trailing hyphen (e.g. calendar_item → calendar-item-). */ +export function slugPrefixForCollection(collection: string): string { + return collection.replace(/_/g, "-") + "-"; +} + +/** _slug field with format and duplicate check via API. When slugPrefix is set, shows prefix as fixed text + input for the rest. */ function SlugField({ collection, currentSlug, + slugPrefix, locale, register, + setValue, setError, clearErrors, error, + slugValue, }: { collection: string; currentSlug?: string; + slugPrefix?: string; locale?: string; register: ReturnType["register"]; + setValue: ReturnType["setValue"]; setError: ReturnType["setError"]; clearErrors: ReturnType["clearErrors"]; error: ReturnType["formState"]["errors"]["_slug"]; + slugValue: string | undefined; }) { - const { ref, onChange, onBlur, name } = register("_slug", { - required: "Slug is required.", + const t = useTranslations("ContentForm"); + const registered = register("_slug", { + required: t("slugRequired"), + validate: + slugPrefix && !currentSlug + ? (v) => { + const val = (v ?? "").trim(); + if (!val) return true; + const normalized = val.toLowerCase(); + if (!normalized.startsWith(slugPrefix.toLowerCase())) { + return t("slugMustStartWith", { prefix: slugPrefix }); + } + return true; + } + : undefined, }); + const fullSlug = (slugValue ?? "").trim(); + const suffix = + slugPrefix && fullSlug.toLowerCase().startsWith(slugPrefix.toLowerCase()) + ? fullSlug.slice(slugPrefix.length) + : slugPrefix + ? fullSlug + : fullSlug; + + const handleSuffixChange = (e: React.ChangeEvent) => { + const v = e.target.value; + const normalized = slugPrefix + ? v.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "") + : v; + setValue("_slug", slugPrefix ? slugPrefix + normalized : v, { shouldValidate: true }); + }; + const handleBlur = async (e: React.FocusEvent) => { - onBlur(e); - const value = e.target.value?.trim(); - if (!value) return; + const value = slugPrefix ? slugPrefix + (e.target.value ?? "") : (e.target.value ?? ""); + const full = value.trim(); + if (!full) return; + if (slugPrefix && !full.toLowerCase().startsWith(slugPrefix.toLowerCase())) return; try { - const res = await checkSlug(collection, value, { + const res = await checkSlug(collection, full, { exclude: currentSlug ?? undefined, _locale: locale, }); @@ -53,36 +112,60 @@ function SlugField({ return; } if (!res.available) { - setError("_slug", { - type: "server", - message: "Slug already in use.", - }); + setError("_slug", { type: "server", message: t("slugInUse") }); return; } clearErrors("_slug"); } catch { - // Network error etc. – no setError, user will see it on submit + // Network error etc. } }; + const suffixPlaceholder = slugPrefix ? t("slugSuffixPlaceholder") : t("slugPlaceholder"); + + if (slugPrefix) { + return ( +
+ +
+ + {slugPrefix} + + +
+

{t("slugHint")}

+ {error ? ( +

{String(error.message)}

+ ) : null} +
+ ); + } + return (
-