diff --git a/apps/app/components.json b/apps/app/components.json new file mode 100644 index 000000000..3781d892e --- /dev/null +++ b/apps/app/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "base-luma", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": true, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "inverted-translucent", + "menuAccent": "subtle", + "registries": {} +} diff --git a/apps/app/package.json b/apps/app/package.json index 4d32d41a5..0ea8e6dfe 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -36,11 +36,14 @@ }, "dependencies": { "@ai-sdk/react": "^3.0.148", + "@base-ui/react": "^1.4.1", "@codemirror/commands": "^6.8.0", "@codemirror/lang-markdown": "^6.3.3", "@codemirror/language": "^6.11.0", "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.38.0", + "@fontsource-variable/geist": "^5.2.8", + "@fontsource-variable/ibm-plex-sans": "^5.2.8", "@lexical/react": "^0.35.0", "@opencode-ai/sdk": "^1.4.9", "@openwork/types": "workspace:*", @@ -56,6 +59,8 @@ "@tauri-apps/plugin-process": "~2.3.1", "@tauri-apps/plugin-updater": "~2.9.0", "ai": "^6.0.146", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "fuzzysort": "^3.1.0", "jsonc-parser": "^3.2.1", "lexical": "^0.35.0", @@ -66,11 +71,15 @@ "react-markdown": "^10.1.0", "react-router-dom": "^7.14.1", "remark-gfm": "^4.0.1", + "shadcn": "^4.6.0", "streamdown": "^2.5.0", + "tailwind-merge": "^3.5.0", + "tw-animate-css": "^1.4.0", "zustand": "^5.0.12" }, "devDependencies": { "@tailwindcss/vite": "^4.1.18", + "@types/node": "^25.6.0", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "@vitejs/plugin-react": "^5.0.4", diff --git a/apps/app/src/app/index.css b/apps/app/src/app/index.css index 975208451..7a46243ed 100644 --- a/apps/app/src/app/index.css +++ b/apps/app/src/app/index.css @@ -3,38 +3,87 @@ @import "../styles/colors.css"; +@import "tw-animate-css"; + +@import "shadcn/tailwind.css"; + +@import "@fontsource-variable/geist"; + +@import "@fontsource-variable/ibm-plex-sans"; + +@custom-variant dark (&:is(.dark, .dark *, [data-theme="dark"], [data-theme="dark"] *)); + :root { color-scheme: light; - --dls-surface: #ffffff; - --dls-sidebar: #f9fafb; - --dls-app-bg: #ffffff; - --dls-border: #f3f4f6; - --dls-accent: #011627; - --dls-accent-hover: #000000; - --dls-accent-rgb: 1 22 39; - --dls-text-primary: #111827; - --dls-text-secondary: #6b7280; - --dls-hover: #f3f4f6; - --dls-active: #eef2f7; + --dls-surface: var(--slate-1); + --dls-sidebar: var(--slate-2); + --dls-app-bg: var(--slate-1); + --dls-background: var(--slate-1); + --dls-canvas: var(--slate-2); + --dls-surface-muted: var(--slate-3); + --dls-border: var(--slate-6); + --dls-accent: var(--blue-9); + --dls-accent-hover: var(--blue-10); + --dls-accent-rgb: 0 144 255; + --dls-secondary-rgb: 96 100 108; + --dls-text-primary: var(--slate-12); + --dls-text-secondary: var(--slate-11); + --dls-hover: var(--slate-3); + --dls-active: var(--slate-5); --dls-radius: 16px; --dls-radius-lg: 24px; --dls-shell-shadow: 0 10px 30px rgba(15, 23, 42, 0.06); --dls-card-shadow: 0 8px 24px rgba(15, 23, 42, 0.05); + --background: var(--slate-1); + --foreground: var(--slate-12); + --card: var(--slate-1); + --card-foreground: var(--slate-12); + --popover: var(--slate-1); + --popover-foreground: var(--slate-12); + --primary-foreground: white; + --secondary: var(--slate-3); + --secondary-foreground: var(--slate-12); + --muted: var(--slate-3); + --muted-foreground: var(--slate-11); + --accent: var(--slate-3); + --accent-foreground: var(--slate-12); + --destructive: var(--red-9); + --border: var(--slate-5); + --input: var(--slate-6); + --ring: var(--slate-8); + --chart-1: var(--sky-9); + --chart-2: var(--blue-9); + --chart-3: var(--indigo-9); + --chart-4: var(--violet-9); + --chart-5: var(--purple-9); + --radius: 0.45rem; + --sidebar: var(--slate-2); + --sidebar-foreground: var(--slate-12); + --sidebar-primary: var(--blue-9); + --sidebar-primary-foreground: white; + --sidebar-accent: var(--slate-3); + --sidebar-accent-foreground: var(--slate-12); + --sidebar-border: var(--slate-6); + --sidebar-ring: var(--slate-8); } [data-theme="dark"] { color-scheme: dark; - --dls-surface: #121212; - --dls-sidebar: #1a1a1a; - --dls-app-bg: #161616; - --dls-border: #262626; - --dls-accent: #3b82f6; - --dls-accent-hover: #2563eb; - --dls-accent-rgb: 59 130 246; - --dls-text-primary: #ededed; - --dls-text-secondary: #9ca3af; - --dls-hover: #1f1f1f; - --dls-active: #2d2d2d; + --dls-surface: var(--slate-1); + --dls-sidebar: var(--slate-2); + --dls-app-bg: var(--slate-1); + --dls-background: var(--slate-1); + --dls-canvas: var(--slate-2); + --dls-surface-muted: var(--slate-3); + --dls-border: var(--slate-6); + --dls-accent: var(--blue-9); + --dls-accent-hover: var(--blue-10); + --dls-accent-rgb: 59 142 247; + --dls-secondary-rgb: 176 180 186; + --dls-text-primary: var(--slate-12); + --dls-text-secondary: var(--slate-11); + --dls-hover: var(--slate-3); + --dls-active: var(--slate-5); --dls-shell-shadow: 0 18px 48px rgba(0, 0, 0, 0.32); --dls-card-shadow: 0 14px 36px rgba(0, 0, 0, 0.24); } @@ -42,10 +91,12 @@ html, body { height: 100%; + overflow: hidden; } #root { height: 100%; + overflow: hidden; } html { @@ -105,7 +156,7 @@ body { justify-content: center; border-radius: 9999px; background: var(--dls-accent); - color: #ffffff; + color: white; box-shadow: 0 8px 20px -16px rgba(var(--dls-accent-rgb), 0.45); } @@ -160,21 +211,21 @@ body { } .ow-status-pill-positive { - border: 1px solid rgba(16, 185, 129, 0.16); - background: #ecfdf5; - color: #047857; + border: 1px solid var(--green-a6); + background: var(--green-3); + color: var(--green-11); } .ow-status-pill-warning { - border: 1px solid rgba(245, 158, 11, 0.16); - background: #fffbeb; - color: #b45309; + border: 1px solid var(--amber-a6); + background: var(--amber-3); + color: var(--amber-11); } .ow-status-pill-neutral { - border: 1px solid #e5e7eb; - background: #f9fafb; - color: #6b7280; + border: 1px solid var(--slate-6); + background: var(--slate-2); + color: var(--slate-11); } .ow-icon-tile { @@ -182,8 +233,8 @@ body { align-items: center; justify-content: center; border-radius: 0.85rem; - background: #f4f6f8; - color: #011627; + background: var(--slate-3); + color: var(--slate-12); } .ow-icon-tile-muted { @@ -191,8 +242,8 @@ body { align-items: center; justify-content: center; border-radius: 0.85rem; - background: #f1f5f9; - color: #6b7280; + background: var(--slate-3); + color: var(--slate-11); } .ow-input { @@ -200,13 +251,13 @@ body { width: 100%; border: 0; border-radius: 0.9rem; - background: #fbfbfc; - box-shadow: inset 0 0 0 1px #eceef1; + background: var(--slate-1); + box-shadow: inset 0 0 0 1px var(--slate-6); color: var(--dls-text-primary); } .ow-input::placeholder { - color: #9ca3af; + color: var(--slate-11); } .ow-input:focus { @@ -256,22 +307,22 @@ select:disabled { /* Highlight animation for just-saved command */ @keyframes command-highlight { 0% { - box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.6); - border-color: rgba(99, 102, 241, 0.8); + box-shadow: 0 0 0 0 var(--indigo-a9); + border-color: var(--indigo-a9); } 50% { - box-shadow: 0 0 0 8px rgba(99, 102, 241, 0); - border-color: rgba(99, 102, 241, 0.4); + box-shadow: 0 0 0 8px var(--indigo-a1); + border-color: var(--indigo-a7); } 100% { - box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); - border-color: rgba(255, 255, 255, 0.08); + box-shadow: 0 0 0 0 var(--indigo-a1); + border-color: var(--slate-a3); } } .command-just-saved { animation: command-highlight 2s ease-out; - border-color: rgba(99, 102, 241, 0.8); + border-color: var(--indigo-a9); } @keyframes progress-shimmer { @@ -317,3 +368,92 @@ select:disabled { background-color: rgba(var(--dls-accent-rgb), 0.6); } } + +.dark, +[data-theme="dark"] { + --background: var(--slate-1); + --foreground: var(--slate-12); + --card: var(--slate-2); + --card-foreground: var(--slate-12); + --popover: var(--slate-2); + --popover-foreground: var(--slate-12); + --primary-foreground: var(--slate-12); + --secondary: var(--slate-3); + --secondary-foreground: var(--slate-12); + --muted: var(--slate-3); + --muted-foreground: var(--slate-10); + --accent: var(--slate-3); + --accent-foreground: var(--slate-12); + --destructive: var(--red-9); + --border: var(--slate-3); + --input: var(--slate-6); + --ring: var(--slate-8); + --chart-1: var(--sky-9); + --chart-2: var(--blue-9); + --chart-3: var(--indigo-9); + --chart-4: var(--violet-9); + --chart-5: var(--purple-9); + --sidebar: var(--slate-2); + --sidebar-foreground: var(--slate-12); + --sidebar-primary: var(--blue-9); + --sidebar-primary-foreground: var(--slate-12); + --sidebar-accent: var(--slate-3); + --sidebar-accent-foreground: var(--slate-12); + --sidebar-border: var(--slate-6); + --sidebar-ring: var(--slate-8); +} + +@theme inline { + --font-sans: 'Geist Variable', sans-serif; + --font-heading: 'IBM Plex Sans Variable', sans-serif; + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--dls-accent); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-foreground: var(--foreground); + --color-background: var(--background); + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); + --radius-2xl: calc(var(--radius) * 1.8); + --radius-3xl: calc(var(--radius) * 2.2); + --radius-4xl: calc(var(--radius) * 2.6); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + html { + @apply font-sans; + } +} diff --git a/apps/app/src/components/ui/accordion.tsx b/apps/app/src/components/ui/accordion.tsx new file mode 100644 index 000000000..2faf916dc --- /dev/null +++ b/apps/app/src/components/ui/accordion.tsx @@ -0,0 +1,75 @@ +import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion" + +import { cn } from "@/lib/utils" +import { ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) { + return ( + + ) +} + +function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: AccordionPrimitive.Trigger.Props) { + return ( + + + {children} + + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: AccordionPrimitive.Panel.Props) { + return ( + +
+ {children} +
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/apps/app/src/components/ui/button.tsx b/apps/app/src/components/ui/button.tsx new file mode 100644 index 000000000..a14483c12 --- /dev/null +++ b/apps/app/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import { Button as ButtonPrimitive } from "@base-ui/react/button" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 relative", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/80 bg-clip-padding shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", + outline: + "border-border bg-muted/20 hover:bg-muted hover:border-foreground/20 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-muted/20 dark:hover:bg-input/30 dark:hover:border-input/80 bg-clip-padding shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + ghost: + "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", + destructive: + "border-border text-destructive hover:bg-destructive/10 hover:border-destructive/40 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:border-border dark:hover:bg-destructive/10 dark:border-destructive/40 dark:focus-visible:ring-destructive/40 bg-clip-padding shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: + "h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pe-2.5 has-data-[icon=inline-start]:ps-2.5 rounded-xl", + xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1 px-3 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2 rounded-xl", + lg: "h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pe-3 has-data-[icon=inline-start]:ps-3 rounded-xl", + icon: "size-9", + "icon-xs": "size-6 [&_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", + ...props +}: ButtonPrimitive.Props & VariantProps) { + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/apps/app/src/components/ui/collapsible.tsx b/apps/app/src/components/ui/collapsible.tsx new file mode 100644 index 000000000..4b242f71d --- /dev/null +++ b/apps/app/src/components/ui/collapsible.tsx @@ -0,0 +1,19 @@ +import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible" + +function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) { + return +} + +function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) { + return ( + + ) +} + +function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) { + return ( + + ) +} + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/apps/app/src/components/ui/dialog.tsx b/apps/app/src/components/ui/dialog.tsx new file mode 100644 index 000000000..c470012e6 --- /dev/null +++ b/apps/app/src/components/ui/dialog.tsx @@ -0,0 +1,158 @@ +import * as React from "react"; +import { Dialog as DialogPrimitive } from "@base-ui/react/dialog" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { XIcon } from "lucide-react" + +function Dialog({ ...props }: DialogPrimitive.Root.Props) { + return +} + +function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) { + return +} + +function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) { + return +} + +function DialogClose({ ...props }: DialogPrimitive.Close.Props) { + return +} + +function DialogOverlay({ + className, + ...props +}: DialogPrimitive.Backdrop.Props) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: DialogPrimitive.Popup.Props & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + } + > + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ + className, + showCloseButton = false, + children, + ...props +}: React.ComponentProps<"div"> & { + showCloseButton?: boolean +}) { + return ( +
+ {children} + {showCloseButton && ( + }> + Close + + )} +
+ ) +} + +function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: DialogPrimitive.Description.Props) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/apps/app/src/components/ui/field.tsx b/apps/app/src/components/ui/field.tsx new file mode 100644 index 000000000..b2ff34345 --- /dev/null +++ b/apps/app/src/components/ui/field.tsx @@ -0,0 +1,236 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" + +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { + return ( +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className + )} + {...props} + /> + ) +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { + return ( + + ) +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +const fieldVariants = cva( + "group/field flex w-full gap-3 data-[invalid=true]:text-destructive", + { + variants: { + orientation: { + vertical: "flex-col *:w-full [&>.sr-only]:w-auto", + horizontal: + "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + responsive: + "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + }, + }, + defaultVariants: { + orientation: "vertical", + }, + } +) + +function Field({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function FieldLabel({ + className, + ...props +}: React.ComponentProps) { + return ( +