diff --git a/components.json b/components.json new file mode 100644 index 0000000..edcaef2 --- /dev/null +++ b/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/package.json b/package.json index 5421704..9228506 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,30 @@ "start": "next start" }, "dependencies": { + "@base-ui/react": "^1.1.0", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/postcss": "^4.1.18", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dayjs": "^1.11.19", + "lucide-react": "^0.563.0", "next": "^16.0.7", "nextra": "^4.6.1", "nextra-theme-docs": "^4.6.1", "postcss": "^8.5.6", "react": "19.1.0", "react-dom": "19.1.0", + "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18" }, "devDependencies": { "@types/node": "25.0.3", "@types/react": "19.2.7", + "gray-matter": "^4.0.3", "pagefind": "^1.3.0", + "tw-animate-css": "^1.4.0", "typescript": "5.9.3" } } diff --git a/src/app/_components/CardInfo.jsx b/src/app/_components/CardInfo.jsx new file mode 100644 index 0000000..12b2fbd --- /dev/null +++ b/src/app/_components/CardInfo.jsx @@ -0,0 +1,20 @@ +export const CardInfo = ({ + classNames = { + container: "", + title: "", + description: "", + }, + title, + children, +}) => { + return ( +
+ {title && ( +

{title}

+ )} + +
+ ) +} diff --git a/src/app/_meta.js b/src/app/_meta.js index 08358c8..531e141 100644 --- a/src/app/_meta.js +++ b/src/app/_meta.js @@ -1,17 +1,24 @@ export default { index: { - display: 'hidden' + display: "hidden", }, docs: { - type: 'page', - title: 'Documentation' + type: "page", + title: "Documentation", }, blog: { - type: 'page', - title: 'Blog' + type: "page", + title: "Blog", }, community: { - type: 'page', - title: 'Community' - } + type: "page", + title: "Community", + }, + troubleshooting: { + theme: { + sidebar: false, // Hide sidebar on this page + toc: false, + }, + type: "page", + }, } diff --git a/src/app/globals.css b/src/app/globals.css index f7a01b9..55fcea7 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,15 +1,80 @@ @import "tailwindcss"; +@custom-variant dark (&:is(.dark *)); + @variant dark (&:where(.dark *)); /* SpaceDF Theme Color Configuration */ :root { --nextra-primary-hue: 255deg; --nextra-primary-saturation: 84%; --nextra-primary-lightness: 80%; + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } .dark { --nextra-primary-lightness: 80%; + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); } /* Custom SpaceDF primary color #4006AA overrides */ @@ -54,3 +119,71 @@ html[class~="dark"] .dark\:nx-text-primary-600 { .dark .svg-icon { filter: brightness(0) invert(1); } + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --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-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + +html { + font-size: 18px; +} + +@media (max-width: 768px) { + html { + font-size: 14px; + } + + h1 { + font-size: 24px; + } + + h2 { + font-size: 20px; + } +} diff --git a/src/app/layout.jsx b/src/app/layout.jsx index 4e5d361..265c9b6 100644 --- a/src/app/layout.jsx +++ b/src/app/layout.jsx @@ -36,7 +36,7 @@ export default async function RootLayout({ children }) { const navbar = ( +
SpaceDF + { + const searchRef = React.useRef(null) + + const handleResetFilter = () => { + setFilters({ + source: [], + tags: [], + errorCode: [], + q: "", + }) + + searchRef.current.value = "" + } + + return ( +
+
+ +
+ + + + + + + + + + + +
+ ) +} + +const FiltersCombobox = ({ + filterValues, + setFilters, + ref, + tagOptions = [], + sourceOptions = [], + errorCodeOptions = [], +}) => { + const { source, errorCode, tags } = filterValues + return ( +
+ + setFilters({ ...filterValues, source: values }) + } + options={sourceOptions} + /> + + + setFilters({ ...filterValues, errorCode: values }) + } + options={errorCodeOptions} + /> + + + setFilters({ ...filterValues, tags: values }) + } + options={tagOptions} + /> + + { + setFilters({ ...filterValues, q }) + }} + ref={ref} + /> +
+ ) +} + +const SourceCombobox = ({ values = [], onValueChange, options = [] }) => { + const anchor = useComboboxAnchor() + + return ( + onValueChange?.(values)} + > + + + {(values) => ( + + {values.map((value) => ( + {value} + ))} + + + )} + + + + No items found. + + {(item) => ( + + {item} + + )} + + + + ) +} + +const ErrorCodeCombobox = ({ values = [], onValueChange, options = [] }) => { + const anchor = useComboboxAnchor() + + return ( + onValueChange?.(values)} + > + + + {(values) => ( + + {values.map((value) => ( + {value} + ))} + + + )} + + + + No items found. + + {(item) => ( + + {item} + + )} + + + + ) +} + +const TagsCodeCombobox = ({ values = [], onValueChange, options = [] }) => { + const anchor = useComboboxAnchor() + + return ( + onValueChange?.(values)} + > + + + {(values) => ( + + {values.map((value) => ( + {value} + ))} + + + )} + + + + No items found. + + {(item) => ( + + {item} + + )} + + + + ) +} + +const InputSearch = ({ onSearchChange, ref }) => { + const debounceId = React.useRef(null) + + return ( + + { + if (debounceId) { + clearTimeout(debounceId.current) + } + + debounceId.current = setTimeout(() => { + onSearchChange?.(event.target.value) + }, 500) + }} + /> + + + + + ) +} diff --git a/src/app/troubleshooting/_containers/IssueList.jsx b/src/app/troubleshooting/_containers/IssueList.jsx new file mode 100644 index 0000000..4582e1a --- /dev/null +++ b/src/app/troubleshooting/_containers/IssueList.jsx @@ -0,0 +1,47 @@ +import Link from "next/link" + +export const IssueList = ({ list = [], isLoading }) => { + return ( +
+ {!list.length && ( +
No issues found
+ )} + + {!!list.length && + list.map((issue, index) => ( +
+
+
+ + {issue.title} + +

+ {(issue.errorCode || []).join(", ")} +

+
+ +
+ {[...issue.source, ...issue.tags].map((t, index) => ( +
+ {t} +
+ ))} +
+ +
+ {issue.dateTime} +
+
+ +
+
+ ))} +
+ ) +} diff --git a/src/app/troubleshooting/_containers/index.jsx b/src/app/troubleshooting/_containers/index.jsx new file mode 100644 index 0000000..33ac5d0 --- /dev/null +++ b/src/app/troubleshooting/_containers/index.jsx @@ -0,0 +1,79 @@ +"use client" + +import { useEffect, useMemo, useState, useTransition } from "react" +import { Filters } from "./Filters" +import { IssueList } from "./IssueList" + +const TroubleshootingContainer = ({ issues }) => { + const [issuesDisplay, setIssuesDisplay] = useState(issues) + + const [filters, setFilters] = useState({ + source: [], + tags: [], + errorCode: [], + q: "", + }) + + const { tagOptions, sourceOptions, errorCodeOptions } = useMemo(() => { + const tags = new Map() + const sources = new Map() + const errorCodes = new Map() + + issues.forEach((issue) => { + issue.tags.forEach((tag) => { + tags.set(tag, tag) + }) + + issue.source.forEach((source) => { + sources.set(source, source) + }) + + issue.errorCode.forEach((errorCode) => { + errorCodes.set(errorCode, errorCode) + }) + }) + + return { + tagOptions: Array.from(tags.keys()), + sourceOptions: Array.from(sources.keys()), + errorCodeOptions: Array.from(errorCodes.keys()), + } + }, []) + + useEffect(() => { + const filterEntries = Object.entries(filters).filter( + ([key, value]) => value.length > 0, + ) + + if (!filterEntries.length) { + setIssuesDisplay(issues) + } else { + const issuesFiltered = issues.filter((issue) => { + return filterEntries.every(([key, value]) => { + if (typeof value !== "string") + return value.some((item) => issue[key].includes(item)) + + return issue.title.toLowerCase().includes(value.toLowerCase()) + }) + }) + + setIssuesDisplay(issuesFiltered) + } + }, [filters]) + + return ( +
+
+ + +
+ ) +} + +export default TroubleshootingContainer diff --git a/src/app/troubleshooting/index.jsx b/src/app/troubleshooting/index.jsx new file mode 100644 index 0000000..234f279 --- /dev/null +++ b/src/app/troubleshooting/index.jsx @@ -0,0 +1,70 @@ +import TroubleshootingContainer from "./_containers" +import fs from "fs" +import path from "path" +import matter from "gray-matter" +import dayjs from "dayjs" +import utc from "dayjs/plugin/utc" + +dayjs.extend(utc) + +export const metadata = { + title: "Troubleshooting", + description: + "Search or browse our troubleshooting guides for solutions to common SpaceDF issues.", +} + +const getTroubleshootingFiles = async () => { + const DOCS_DIR = path.join(process.cwd(), "src/app/troubleshooting/issues") + const results = [] + + const walk = (dir) => { + const files = fs.readdirSync(dir) + for (const file of files) { + const fullPath = path.join(dir, file, "page.mdx") + + const raw = fs.readFileSync(fullPath, "utf-8") + const stats = fs.statSync(fullPath) + + const timeUnix = stats.mtimeMs + const dateTime = dayjs.utc(timeUnix).format("MMM DD, YYYY") + + if (!raw) continue + + const { data } = matter(raw) + + results.push({ + title: data?.title || "", + source: data?.source || [], + errorCode: data.errorCode || [], + tags: data?.tags || [], + href: `/troubleshooting/issues/${file}`, + dateTime, + timeUnix, + }) + } + + return results.sort((a, b) => b.timeUnix - a.timeUnix) + } + + return { + issues: walk(DOCS_DIR), + } +} + +export default async function TroubleshootingPage() { + const troubleshootingFiles = await getTroubleshootingFiles() + + return ( +
+

+ Troubleshooting +

+

+ Search or browse our troubleshooting guides for solutions to common + SpaceDF issues. +

+ + +
+ ) +} diff --git a/src/app/troubleshooting/issues/google-oauth-redirect-uri-mismatch/page.mdx b/src/app/troubleshooting/issues/google-oauth-redirect-uri-mismatch/page.mdx new file mode 100644 index 0000000..4b1cf8b --- /dev/null +++ b/src/app/troubleshooting/issues/google-oauth-redirect-uri-mismatch/page.mdx @@ -0,0 +1,105 @@ +--- +title: "Google OAuth: redirect_uri_mismatch" +source: ["Auth"] +errorCode: ["401", "500", "unauthenticated", "server_error"] +tags: ["Self-hosting"] +--- +import { Steps } from 'nextra/components' +import { + NumberOfContent, + NumberWithMdx +} from "@/app/_components/NumberOfContent.jsx" +import { CardInfo } from "@/app/_components/CardInfo.jsx" +import { Callout } from 'nextra/components' + + +# Google OAuth: `redirect_uri_mismatch` +> This error occurs when the **redirect URI used by SpaceDF does not exactly match** the Authorized Redirect URI configured in the Google Cloud Console. + +## Symptoms +Google shows an error page with: + +```text +Error 400: redirect_uri_mismatch +``` + +Google login works in one environment (local or production) but fails in another. + +The login flow redirects to Google, then immediately fails. + +## Common causes (SpaceDF-specific) +- `GOOGLE_CALLBACK_URL` in `.env` does not match the redirect URI configured in Google Cloud Console. +- The redirect URI is correct, but: + - Protocol is different (`http` vs `https`) + - Port is different (`3000` vs `80`) + - Trailing slash mismatch +- Production domain is not added to Google OAuth settings. +- Switching from **Quick Start** to **Advanced Setup** without updating OAuth settings. + +## Fix + + +### Verify `GOOGLE_CALLBACK_URL` +Check your .env file: + +```bash copy +# Development +GOOGLE_CALLBACK_URL=http://localhost:3000 + +# Production +GOOGLE_CALLBACK_URL=https://your-domain.com +``` + +> Do not include `/auth/google/callback` in `GOOGLE_CALLBACK_URL` +SpaceDF appends it automatically. + +### Update Google Cloud Console + + Go to the [Google Cloud Console](https://console.cloud.google.com/) + + + Navigate to **APIs & Services → Credentials** + + + Select your **OAuth 2.0 Client ID**. + + + Under **Authorized redirect URIs**, add: +```bash +# Development +http://localhost:3000/auth/google/callback + +# Production +https://your-domain.com/auth/google/callback +``` +> The URI must match exactly, including protocol, domain, port, and path. + + + +### Restart SpaceDF services +After updating `.env`, restart all services: + +```bash copy +docker compose down +docker compose up -d +``` + + + + ❌ Using localhost in production
+ ❌ Mixing HTTP and HTTPS
+ ❌ Missing the `/auth/google/callback path` in `Google Console`
+ ❌ Adding trailing slashes inconsistently
+ ❌ Forgetting to restart services after changing `.env`
+
+ + +***Notes*** +- Google OAuth is **not enabled in Quick Start**. +- This error only applies when using **Advanced Setup**. +- Each environment (local, staging, production) requires its own redirect URI entry. + \ No newline at end of file diff --git a/src/app/troubleshooting/issues/rabbitmq-existing-setup-with-different-credentials/page.mdx b/src/app/troubleshooting/issues/rabbitmq-existing-setup-with-different-credentials/page.mdx new file mode 100644 index 0000000..5f5fd36 --- /dev/null +++ b/src/app/troubleshooting/issues/rabbitmq-existing-setup-with-different-credentials/page.mdx @@ -0,0 +1,80 @@ +--- +title: "RabbitMQ: Existing setup with different credentials" +source: ["RabbitMQ", "Messaging"] +errorCode: ["ACCESS_REFUSED", "AUTHENTICATION_FAILED"] +tags: ["Self-hosting", "rabbitmq", "credentials", "volume_conflict", "docker"] +--- + +import { Callout } from 'nextra/components' +import { Steps } from 'nextra/components' + +# RabbitMQ: Existing setup with different credentials + +> This issue occurs when a **previous RabbitMQ setup already exists** on the machine and was initialized with different credentials than the ones currently configured for SpaceDF. + +## Symptoms +- SpaceDF services fail to start or keep restarting +- Logs show RabbitMQ authentication errors such as: + - `ACCESS_REFUSED` + - `authentication failed` +- Updating `RABBITMQ_DEFAULT_USER` or `RABBITMQ_DEFAULT_PASS` does not fix the issue + +## Cause +RabbitMQ **stores credentials in persistent volumes** on first startup. + +If RabbitMQ was previously started with different credentials: +- Updating environment variables alone is **not enough** +- RabbitMQ will continue using the **old credentials stored in volumes** +- This results in a credential mismatch between SpaceDF services and RabbitMQ + +## How to verify + +Check if an existing RabbitMQ container or volume is present: +```bash copy +docker ps -a | grep rabbitmq +``` + +```bash copy +docker volume ls | grep rabbitmq +``` +If RabbitMQ volumes exist, they may contain credentials from a previous setup. + +## Fix (reset RabbitMQ credentials) + + +This will remove all RabbitMQ data. +Only assume safe in development or fresh setups. + + + +### Stop all SpaceDF services: + +```bash copy +docker compose down +``` + +### Remove existing RabbitMQ volumes: +```bash copy +docker volume rm +``` + +### Verify credentials in `.env`: +```bash copy +RABBITMQ_DEFAULT_USER=your_username +RABBITMQ_DEFAULT_PASS=your_password +``` + +### Restart SpaceDF: +```bash copy +docker compose up -d +``` + +RabbitMQ will be re-initialized using the new credentials. + + + +***Notes*** +- Changing RabbitMQ credentials **always requires resetting volumes** +- Do not reuse RabbitMQ credentials across projects on the same host +- For production systems, plan credential changes carefully to avoid data loss + \ No newline at end of file diff --git a/src/app/troubleshooting/page.mdx b/src/app/troubleshooting/page.mdx index fa45a99..2770e4c 100644 --- a/src/app/troubleshooting/page.mdx +++ b/src/app/troubleshooting/page.mdx @@ -1,30 +1,8 @@ -# Troubleshooting - -Common issues and solutions when working with SpaceDF. - -## Getting Started Issues - -### Installation Problems - -If you're having trouble installing SpaceDF, check: -- System requirements are met -- Network connectivity -- Required permissions - -### Connection Issues - -Problems connecting to SpaceDF: -- Verify network settings -- Check firewall configuration -- Ensure proper authentication - -## Advanced Troubleshooting - -For more complex issues, please: -1. Check the logs -2. Review configuration files -3. Contact support through [Community](/community) - -## FAQ - -Coming soon... \ No newline at end of file +--- +title: Troubleshooting +sidebar: true +asIndexPage: true +--- +import TroubleshootingPage from "./index" + + \ No newline at end of file diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..915ea2a --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx new file mode 100644 index 0000000..2f9df6d --- /dev/null +++ b/src/components/ui/combobox.tsx @@ -0,0 +1,310 @@ +"use client" + +import * as React from "react" +import { Combobox as ComboboxPrimitive } from "@base-ui/react" +import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, +} from "@/components/ui/input-group" + +const Combobox = ComboboxPrimitive.Root + +function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { + return +} + +function ComboboxTrigger({ + className, + children, + ...props +}: ComboboxPrimitive.Trigger.Props) { + return ( + + {children} + + + ) +} + +function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { + return ( + } + className={cn(className)} + {...props} + > + + + ) +} + +function ComboboxInput({ + className, + children, + disabled = false, + showTrigger = true, + showClear = false, + ...props +}: ComboboxPrimitive.Input.Props & { + showTrigger?: boolean + showClear?: boolean +}) { + return ( + + } + {...props} + /> + + {showTrigger && ( + + + + )} + {showClear && } + + {children} + + ) +} + +function ComboboxContent({ + className, + side = "bottom", + sideOffset = 6, + align = "start", + alignOffset = 0, + anchor, + ...props +}: ComboboxPrimitive.Popup.Props & + Pick< + ComboboxPrimitive.Positioner.Props, + "side" | "align" | "sideOffset" | "alignOffset" | "anchor" + >) { + return ( + + + + + + ) +} + +function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { + return ( + + ) +} + +function ComboboxItem({ + className, + children, + ...props +}: ComboboxPrimitive.Item.Props) { + return ( + + {children} + + } + > + + + + ) +} + +function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { + return ( + + ) +} + +function ComboboxLabel({ + className, + ...props +}: ComboboxPrimitive.GroupLabel.Props) { + return ( + + ) +} + +function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) { + return ( + + ) +} + +function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { + return ( +