svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export {
+ Avatar,
+ AvatarImage,
+ AvatarFallback,
+ AvatarGroup,
+ AvatarGroupCount,
+ AvatarBadge
+}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index 2dfbf6a..43416ff 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -1,31 +1,38 @@
+"use client"
+
+import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
-import { Slot as SlotPrimitive } from "radix-ui"
-import * as React from "react"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
- "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg 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/50 active: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",
{
variants: {
variant: {
- default:
- "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
- destructive:
- "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline:
- "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
+ "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
- "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
- "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
+ destructive:
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline"
},
size: {
- default: "h-9 px-4 py-2 has-[>svg]:px-3",
- sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
- icon: "size-9"
+ default:
+ "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
+ icon: "size-8",
+ "icon-xs":
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
+ "icon-sm":
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
+ "icon-lg": "size-9"
}
},
defaultVariants: {
@@ -37,18 +44,12 @@ const buttonVariants = cva(
function Button({
className,
- variant,
- size,
- asChild = false,
+ variant = "default",
+ size = "default",
...props
-}: React.ComponentProps<"button"> &
- VariantProps
& {
- asChild?: boolean
- }) {
- const Comp = asChild ? SlotPrimitive.Slot : "button"
-
+}: ButtonPrimitive.Props & VariantProps) {
return (
- ) {
- return
+function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
+ return
}
-function DropdownMenuPortal({
- ...props
-}: React.ComponentProps) {
- return (
-
- )
+function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
+ return
}
-function DropdownMenuTrigger({
- ...props
-}: React.ComponentProps) {
- return (
-
- )
+function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
+ return
}
function DropdownMenuContent({
- className,
+ align = "start",
+ alignOffset = 0,
+ side = "bottom",
sideOffset = 4,
+ className,
...props
-}: React.ComponentProps) {
+}: MenuPrimitive.Popup.Props &
+ Pick<
+ MenuPrimitive.Positioner.Props,
+ "align" | "alignOffset" | "side" | "sideOffset"
+ >) {
return (
-
-
+
-
+ >
+
+
+
)
}
-function DropdownMenuGroup({
+function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
+ return
+}
+
+function DropdownMenuLabel({
+ className,
+ inset,
...props
-}: React.ComponentProps) {
+}: MenuPrimitive.GroupLabel.Props & {
+ inset?: boolean
+}) {
return (
-
+
)
}
@@ -62,17 +79,17 @@ function DropdownMenuItem({
inset,
variant = "default",
...props
-}: React.ComponentProps & {
+}: MenuPrimitive.Item.Props & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
-
+}
+
+function DropdownMenuSubTrigger({
className,
+ inset,
children,
- checked,
...props
-}: React.ComponentProps) {
+}: MenuPrimitive.SubmenuTrigger.Props & {
+ inset?: boolean
+}) {
return (
-
-
-
-
-
-
{children}
-
+
+
)
}
-function DropdownMenuRadioGroup({
+function DropdownMenuSubContent({
+ align = "start",
+ alignOffset = -3,
+ side = "right",
+ sideOffset = 0,
+ className,
...props
-}: React.ComponentProps) {
+}: React.ComponentProps) {
return (
-
)
}
-function DropdownMenuRadioItem({
+function DropdownMenuCheckboxItem({
className,
children,
+ checked,
+ inset,
...props
-}: React.ComponentProps) {
+}: MenuPrimitive.CheckboxItem.Props & {
+ inset?: boolean
+}) {
return (
-
-
-
-
-
+
+
+
+
{children}
-
+
)
}
-function DropdownMenuLabel({
+function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
+ return (
+
+ )
+}
+
+function DropdownMenuRadioItem({
className,
+ children,
inset,
...props
-}: React.ComponentProps & {
+}: MenuPrimitive.RadioItem.Props & {
inset?: boolean
}) {
return (
-
+ >
+
+
+
+
+
+ {children}
+
)
}
function DropdownMenuSeparator({
className,
...props
-}: React.ComponentProps) {
+}: MenuPrimitive.Separator.Props) {
return (
-
- )
-}
-
-function DropdownMenuSub({
- ...props
-}: React.ComponentProps) {
- return
-}
-
-function DropdownMenuSubTrigger({
- className,
- inset,
- children,
- ...props
-}: React.ComponentProps & {
- inset?: boolean
-}) {
- return (
-
- {children}
-
-
- )
-}
-
-function DropdownMenuSubContent({
- className,
- ...props
-}: React.ComponentProps) {
- return (
- ) {
return (
- ) {
+function Label({ className, ...props }: React.ComponentProps<"label">) {
return (
-
+///
// eslint-disable-next-line import/no-unassigned-import -- side-effect: registers font
import "@fontsource-variable/geist"
// eslint-disable-next-line import/no-unassigned-import -- side-effect: registers font
diff --git a/src/routes/_main/index.tsx b/src/routes/_main/index.tsx
index b9a116a..a81f365 100644
--- a/src/routes/_main/index.tsx
+++ b/src/routes/_main/index.tsx
@@ -36,14 +36,17 @@ function RouteComponent() {
diff --git a/src/styles/app.css b/src/styles/app.css
index dca3d7e..54eaa0b 100644
--- a/src/styles/app.css
+++ b/src/styles/app.css
@@ -2,76 +2,78 @@
@import "tw-animate-css";
@import "./fonts.css";
@import "./animations.css";
+@import "shadcn/tailwind.css";
+@import "@fontsource-variable/geist";
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
- --foreground: oklch(0.141 0.005 285.823);
+ --foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
- --card-foreground: oklch(0.141 0.005 285.823);
+ --card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
- --popover-foreground: oklch(0.141 0.005 285.823);
- --primary: oklch(0.21 0.006 285.885);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
- --secondary: oklch(0.967 0.001 286.375);
- --secondary-foreground: oklch(0.21 0.006 285.885);
- --muted: oklch(0.967 0.001 286.375);
- --muted-foreground: oklch(0.552 0.016 285.938);
- --accent: oklch(0.967 0.001 286.375);
- --accent-foreground: oklch(0.21 0.006 285.885);
+ --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.92 0.004 286.32);
- --input: oklch(0.92 0.004 286.32);
- --ring: oklch(0.705 0.015 286.067);
- --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);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.87 0 0);
+ --chart-2: oklch(0.556 0 0);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.985 0 0);
- --sidebar-foreground: oklch(0.141 0.005 285.823);
- --sidebar-primary: oklch(0.21 0.006 285.885);
+ --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.967 0.001 286.375);
- --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
- --sidebar-border: oklch(0.92 0.004 286.32);
- --sidebar-ring: oklch(0.705 0.015 286.067);
+ --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 {
- --background: oklch(0.141 0.005 285.823);
+ --background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
- --card: oklch(0.21 0.006 285.885);
+ --card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.21 0.006 285.885);
+ --popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.92 0.004 286.32);
- --primary-foreground: oklch(0.21 0.006 285.885);
- --secondary: oklch(0.274 0.006 286.033);
+ --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.274 0.006 286.033);
- --muted-foreground: oklch(0.705 0.015 286.067);
- --accent: oklch(0.274 0.006 286.033);
+ --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.552 0.016 285.938);
- --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.21 0.006 285.885);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.87 0 0);
+ --chart-2: oklch(0.556 0 0);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --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.274 0.006 286.033);
+ --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.552 0.016 285.938);
+ --sidebar-ring: oklch(0.556 0 0);
}
@theme inline {
@@ -110,6 +112,11 @@
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
+ --font-heading: var(--font-sans);
+ --font-sans: "Geist Variable", sans-serif;
+ --radius-2xl: calc(var(--radius) * 1.8);
+ --radius-3xl: calc(var(--radius) * 2.2);
+ --radius-4xl: calc(var(--radius) * 2.6);
}
@layer base {
@@ -117,6 +124,9 @@
@apply border-border outline-ring/50;
}
body {
- @apply bg-background text-foreground antialiased;
+ @apply antialiased bg-background text-foreground;
+ }
+ html {
+ @apply font-sans;
}
}
diff --git a/vite.config.ts b/vite.config.ts
index 644c0cc..1a6f888 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -2,9 +2,70 @@ import { cloudflare } from "@cloudflare/vite-plugin"
import tailwindcss from "@tailwindcss/vite"
import { tanstackStart } from "@tanstack/react-start/plugin/vite"
import react from "@vitejs/plugin-react"
-import { defineConfig } from "vite"
+import { defineConfig } from "vite-plus"
export default defineConfig({
+ staged: {
+ "*": "vp check --fix"
+ },
+ fmt: {
+ semi: false,
+ singleQuote: false,
+ trailingComma: "none",
+ tabWidth: 2,
+ printWidth: 80,
+ proseWrap: "always",
+ sortImports: {},
+ sortPackageJson: true,
+ sortTsconfig: true,
+ sortTailwindConfig: true,
+ ignorePatterns: ["**/*.d.ts", "**/*.gen.ts"]
+ },
+ lint: {
+ plugins: [
+ "eslint",
+ "import",
+ "jsdoc",
+ "node",
+ "oxc",
+ "promise",
+ "react",
+ "react-perf",
+ "typescript",
+ "unicorn",
+ "vitest"
+ ],
+ categories: {
+ correctness: "error",
+ perf: "warn",
+ suspicious: "warn",
+ pedantic: "off"
+ },
+ rules: {
+ "react/react-in-jsx-scope": "off",
+ "react-perf/jsx-no-jsx-as-prop": "off",
+ "typescript/no-explicit-any": "error",
+ "typescript/no-misused-promises": "warn"
+ },
+ options: {
+ typeAware: true,
+ typeCheck: true
+ },
+ overrides: [
+ {
+ files: ["**/*.test.ts"],
+ rules: {
+ "typescript/no-explicit-any": "off"
+ }
+ },
+ {
+ files: ["**/*.d.ts"],
+ rules: {
+ "import/no-unassigned-import": "off"
+ }
+ }
+ ]
+ },
resolve: {
tsconfigPaths: true
},
diff --git a/vitest.config.ts b/vitest.config.ts
index 7cbcb4f..075ad6b 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,4 +1,4 @@
-import { defineConfig } from "vitest/config"
+import { defineConfig } from "vite-plus"
export default defineConfig({
resolve: {