Initial Commit
All checks were successful
Build and Push Docker Image / build (push) Successful in 3m6s

This commit is contained in:
2026-05-30 12:52:07 +02:00
commit 6d6dcb66a1
62 changed files with 12208 additions and 0 deletions

13
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { Link } from '@tanstack/solid-router'
import TanStackQueryHeaderUser from '../integrations/tanstack-query/header-user.tsx'
export default function Header() {
return (
<div class="flex items-center justify-between px-4">
<Link to="/">Home</Link>
<Link to="/todo">Todo</Link>
<TanStackQueryHeaderUser />
</div>
)
}

View File

@@ -0,0 +1,12 @@
import { Link } from "@tanstack/solid-router"
function NotFound() {
return (
<div class='w-full min-h-screen grid place-items-center'>
<h1 class='text-4xl font-bold'>404: Not Found</h1>
<Link to='/'>Go to home</Link>
</div>
)
}
export default NotFound

View File

@@ -0,0 +1,182 @@
import * as React from "react"
import { SearchForm } from "~/components/search-form"
import { VersionSwitcher } from "~/components/version-switcher"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
} from "~/components/ui/sidebar"
// This is sample data.
const data = {
versions: ["1.0.1", "1.1.0-alpha", "2.0.0-beta1"],
navMain: [
{
title: "Getting Started",
url: "#",
items: [
{
title: "Installation",
url: "#",
},
{
title: "Project Structure",
url: "#",
},
],
},
{
title: "Build Your Application",
url: "#",
items: [
{
title: "Routing",
url: "#",
},
{
title: "Data Fetching",
url: "#",
isActive: true,
},
{
title: "Rendering",
url: "#",
},
{
title: "Caching",
url: "#",
},
{
title: "Styling",
url: "#",
},
{
title: "Optimizing",
url: "#",
},
{
title: "Configuring",
url: "#",
},
{
title: "Testing",
url: "#",
},
{
title: "Authentication",
url: "#",
},
{
title: "Deploying",
url: "#",
},
{
title: "Upgrading",
url: "#",
},
{
title: "Examples",
url: "#",
},
],
},
{
title: "API Reference",
url: "#",
items: [
{
title: "Components",
url: "#",
},
{
title: "File Conventions",
url: "#",
},
{
title: "Functions",
url: "#",
},
{
title: "next.config.js Options",
url: "#",
},
{
title: "CLI",
url: "#",
},
{
title: "Edge Runtime",
url: "#",
},
],
},
{
title: "Architecture",
url: "#",
items: [
{
title: "Accessibility",
url: "#",
},
{
title: "Fast Refresh",
url: "#",
},
{
title: "Next.js Compiler",
url: "#",
},
{
title: "Supported Browsers",
url: "#",
},
{
title: "Turbopack",
url: "#",
},
],
},
],
}
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar {...props}>
<SidebarHeader>
<VersionSwitcher
versions={data.versions}
defaultVersion={data.versions[0]}
/>
<SearchForm />
</SidebarHeader>
<SidebarContent>
{/* We create a SidebarGroup for each parent. */}
{data.navMain.map((item) => (
<SidebarGroup key={item.title}>
<SidebarGroupLabel>{item.title}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{item.items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild isActive={item.isActive}>
<a href={item.url}>{item.title}</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
))}
</SidebarContent>
<SidebarRail />
</Sidebar>
)
}

View File

@@ -0,0 +1,24 @@
import { createFormHookContexts, createFormHook } from '@tanstack/solid-form'
import { TextAreaField, TextField } from './text';
import { SelectField } from './select';
import { SliderField } from './slider';
// export useFieldContext for use in your custom components
export const { fieldContext, formContext, useFieldContext } =
createFormHookContexts()
const { useAppForm } = createFormHook({
fieldContext,
formContext,
// We'll learn more about these options later
fieldComponents: {
TextField,
TextAreaField,
SelectField,
SliderField
},
formComponents: {},
})
export default useAppForm;

View File

@@ -0,0 +1,34 @@
import { useFieldContext } from './appform.tsx'
import { Field, FieldDescription, FieldLabel } from '../ui/field.tsx'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select.tsx'
import { Show } from 'solid-js'
import { ClientOnly } from '@tanstack/solid-router'
export function SelectField(props: { label: string, name: string, description: string | null, placeholder: string }) {
// The `Field` infers that it should have a `value` type of `string`
const field = useFieldContext<string>()
return (
<>
<Field>
<FieldLabel for={props.name}>{props.label}</FieldLabel>
<Select
value={field().state.value}
onChange={(e) => field().handleChange(e ?? "Apple")}
options={["Apple", "Banana", "Blueberry", "Grapes", "Pineapple"]}
placeholder="Select a fruit…"
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
>
<SelectTrigger aria-label="Fruit" class="w-[180px]">
<SelectValue<string>>{(state) => state.selectedOption()}</SelectValue>
</SelectTrigger>
<SelectContent />
</Select>
<FieldDescription>
{props.description}
</FieldDescription>
</Field>
</>
)
}

View File

@@ -0,0 +1,31 @@
import { useFieldContext } from './appform.tsx'
import { Field, FieldDescription, FieldLabel } from '../ui/field.tsx'
import { ClientOnly } from '@tanstack/solid-router'
import { Slider } from '../ui/slider.tsx'
export function SliderField(props: { label: string, name: string, description: string | null, placeholder: string }) {
// The `Field` infers that it should have a `value` type of `string`
const field = useFieldContext<number[]>()
return (
<>
<Field>
<FieldLabel for={props.name}>{props.label}</FieldLabel>
<ClientOnly>
<Slider
value={field().state.value}
onChange={(e) => field().handleChange(e)}
min={0}
max={100}
step={1}
/>
</ClientOnly>
<FieldDescription>
{props.description}
</FieldDescription>
</Field>
</>
)
}

View File

@@ -0,0 +1,48 @@
import { useFieldContext } from './appform.tsx'
import { Field, FieldDescription, FieldLabel } from '../ui/field.tsx'
import { Input } from '../ui/input.tsx'
import { Textarea } from '../ui/textarea.tsx'
export function TextField(props: { label: string, name: string, description: string | null, placeholder: string }) {
// The `Field` infers that it should have a `value` type of `string`
const field = useFieldContext<string>()
return (
<>
<Field>
<FieldLabel for={props.name}>{props.label}</FieldLabel>
<Input
id={props.name}
name={props.name}
type="text" placeholder={props.placeholder}
value={field().state.value}
onChange={(e) => field().handleChange(e.target.value)}
/>
<FieldDescription>
{props.description}
</FieldDescription>
</Field>
</>
)
}
export function TextAreaField(props: { label: string, name: string, description: string | null, placeholder: string }) {
// The `Field` infers that it should have a `value` type of `string`
const field = useFieldContext<string>()
return (
<>
<Field>
<FieldLabel for={props.name}>{props.label}</FieldLabel>
<Textarea
id={props.name}
name={props.name}
placeholder={props.placeholder}
value={field().state.value}
onChange={(e) => field().handleChange(e.target.value)}
/>
<FieldDescription>
{props.description}
</FieldDescription>
</Field>
</>
)
}

View File

@@ -0,0 +1,27 @@
import { Label } from "~/components/ui/label"
import {
SidebarGroup,
SidebarGroupContent,
SidebarInput,
} from "~/components/ui/sidebar"
import { SearchIcon } from "lucide-react"
export function SearchForm({ ...props }: React.ComponentProps<"form">) {
return (
<form {...props}>
<SidebarGroup className="py-0">
<SidebarGroupContent className="relative">
<Label htmlFor="search" className="sr-only">
Search
</Label>
<SidebarInput
id="search"
placeholder="Search the docs..."
className="pl-8"
/>
<SearchIcon className="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none" />
</SidebarGroupContent>
</SidebarGroup>
</form>
)
}

View File

@@ -0,0 +1,220 @@
/* eslint-disable react-refresh/only-export-components */
import type { JSXElement } from "solid-js"
import { createContext, createEffect, createMemo, createSignal, useContext } from "solid-js"
type Theme = "dark" | "light" | "system"
type ResolvedTheme = "dark" | "light"
type ThemeProviderProps = {
children: JSXElement,
defaultTheme?: Theme
storageKey?: string
disableTransitionOnChange?: boolean
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)"
const THEME_VALUES: Theme[] = ["dark", "light", "system"]
const ThemeProviderContext = createContext<
ThemeProviderState | undefined
>(undefined)
function isTheme(value: string | null): value is Theme {
if (value === null) {
return false
}
return THEME_VALUES.includes(value as Theme)
}
function getSystemTheme(): ResolvedTheme {
if (typeof window !== "undefined" && window.matchMedia(COLOR_SCHEME_QUERY).matches) {
return "dark"
}
return "light"
}
function disableTransitionsTemporarily() {
const style = document.createElement("style")
style.appendChild(
document.createTextNode(
"*,*::before,*::after{-webkit-transition:none!important;transition:none!important}"
)
)
document.head.appendChild(style)
return () => {
window.getComputedStyle(document.body)
requestAnimationFrame(() => {
requestAnimationFrame(() => {
style.remove()
})
})
}
}
function isEditableTarget(target: EventTarget | null) {
if (!(target instanceof HTMLElement)) {
return false
}
if (target.isContentEditable) {
return true
}
const editableParent = target.closest(
"input, textarea, select, [contenteditable='true']"
)
if (editableParent) {
return true
}
return false
}
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "theme",
disableTransitionOnChange = true,
...props
}: ThemeProviderProps) {
const storedTheme = typeof localStorage !== "undefined" ? localStorage.getItem(storageKey) : null
const [theme, setThemeState] = createSignal<Theme>(isTheme(storedTheme) ? storedTheme : defaultTheme )
const setTheme = (nextTheme: Theme) => {
if (typeof localStorage !== "undefined") {
localStorage.setItem(storageKey, nextTheme)
}
setThemeState(nextTheme)
}
const applyTheme = (nextTheme: Theme) => {
const root = document.documentElement
const resolvedTheme =
nextTheme === "system" ? getSystemTheme() : nextTheme
const restoreTransitions = disableTransitionOnChange
? disableTransitionsTemporarily()
: null
root.classList.remove("light", "dark")
root.classList.add(resolvedTheme)
if (restoreTransitions) {
restoreTransitions()
}
}
createEffect(() => {
applyTheme(theme())
if (theme() !== "system") {
return undefined
}
const mediaQuery = window.matchMedia(COLOR_SCHEME_QUERY)
const handleChange = () => {
applyTheme("system")
}
mediaQuery.addEventListener("change", handleChange)
return () => {
mediaQuery.removeEventListener("change", handleChange)
}
})
createEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.repeat) {
return
}
if (event.metaKey || event.ctrlKey || event.altKey) {
return
}
if (isEditableTarget(event.target)) {
return
}
if (event.key.toLowerCase() !== "d") {
return
}
setThemeState((currentTheme) => {
const nextTheme =
currentTheme === "dark"
? "light"
: currentTheme === "light"
? "dark"
: getSystemTheme() === "dark"
? "light"
: "dark"
localStorage.setItem(storageKey, nextTheme)
return nextTheme
})
}
window.addEventListener("keydown", handleKeyDown)
return () => {
window.removeEventListener("keydown", handleKeyDown)
}
})
createEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.storageArea !== localStorage) {
return
}
if (event.key !== storageKey) {
return
}
if (isTheme(event.newValue)) {
setThemeState(event.newValue)
return
}
setThemeState(defaultTheme)
}
window.addEventListener("storage", handleStorageChange)
return () => {
window.removeEventListener("storage", handleStorageChange)
}
})
const value = createMemo(
() => ({
theme: theme(),
setTheme,
})
)
return (
<ThemeProviderContext.Provider {...props} value={value()}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider")
}
return context
}

View File

@@ -0,0 +1,122 @@
import { cn } from "~/lib/utils"
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
import type { ComponentProps } from "solid-js"
import { Dynamic } from "solid-js/web"
function Breadcrumb({ class: className, ...props }: ComponentProps<"nav">) {
return (
<nav
aria-label="breadcrumb"
data-slot="breadcrumb"
class={cn(className)}
{...props}
/>
)
}
function BreadcrumbList({ class: className, ...props }: ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
class={cn(
"flex flex-wrap items-center gap-1.5 text-xs tracking-wide wrap-break-word text-muted-foreground uppercase sm:gap-2.5",
className
)}
{...props}
/>
)
}
function BreadcrumbItem({ class: className, ...props }: ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
class={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
}
function BreadcrumbLink({
asChild,
class: className,
...props
}: ComponentProps<"a"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "a"
return (
<Dynamic
component={Comp}
data-slot="breadcrumb-link"
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
}
function BreadcrumbPage({ class: className, ...props }: ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
class={cn("font-normal text-foreground", className)}
{...props}
/>
)
}
function BreadcrumbSeparator({
children,
class: className,
...props
}: ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
class={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? (
<ChevronRightIcon />
)}
</li>
)
}
function BreadcrumbEllipsis({
class: className,
...props
}: ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
class={cn(
"flex size-5 items-center justify-center [&>svg]:size-4",
className
)}
{...props}
>
<MoreHorizontalIcon
/>
<span class="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View File

@@ -0,0 +1,66 @@
import { cva, type VariantProps } from "class-variance-authority"
import type { ComponentProps } from "solid-js"
import { Dynamic } from "solid-js/web"
import { cn } from "~/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-semibold tracking-widest whitespace-nowrap uppercase transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-2 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-2 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-3.5",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
outline:
"border-border bg-transparent hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-input/30",
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:
"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 underline-offset-4 hover:underline",
},
size: {
default:
"h-10 gap-1.5 px-6 has-data-[icon=inline-end]:pr-4 has-data-[icon=inline-start]:pl-4",
xs: "h-7 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3",
sm: "h-9 gap-1 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
lg: "h-11 gap-1.5 px-8 has-data-[icon=inline-end]:pr-5 has-data-[icon=inline-start]:pl-5",
icon: "size-10",
"icon-xs": "size-7 [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-9",
"icon-lg": "size-11",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
class: className,
variant = "default",
size = "default",
asChild = false,
...props
}: ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Dynamic
component={Comp}
data-slot="button"
data-variant={variant}
data-size={size}
class={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

102
src/components/ui/card.tsx Normal file
View File

@@ -0,0 +1,102 @@
import type { ComponentProps } from "solid-js"
import { cn } from "~/lib/utils"
function Card({
class: className,
size = "default",
...props
}: ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
class={cn(
"group/card flex flex-col gap-8 overflow-hidden bg-card py-8 text-sm text-card-foreground shadow-sm ring-1 ring-foreground/5 has-[>img:first-child]:pt-0 data-[size=sm]:gap-5 data-[size=sm]:py-5 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none",
className
)}
{...props}
/>
)
}
function CardHeader({ class: className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="card-header"
class={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1.5 rounded-none px-8 group-data-[size=sm]/card:px-5 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-8 group-data-[size=sm]/card:[.border-b]:pb-5",
className
)}
{...props}
/>
)
}
function CardTitle({ class: className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="card-title"
class={cn(
"font-heading text-lg font-semibold tracking-wider uppercase",
className
)}
{...props}
/>
)
}
function CardDescription({ class: className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="card-description"
class={cn("text-sm leading-relaxed text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ class: className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="card-action"
class={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ class: className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="card-content"
class={cn("px-8 group-data-[size=sm]/card:px-5", className)}
{...props}
/>
)
}
function CardFooter({ class: className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
class={cn(
"flex items-center px-8 group-data-[size=sm]/card:px-5 [.border-t]:pt-8 group-data-[size=sm]/card:[.border-t]:pt-5",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@@ -0,0 +1,292 @@
import type { Component } from "solid-js"
import { createEffect, createSignal, mergeProps, on, onCleanup, onMount } from "solid-js"
import { unwrap } from "solid-js/store"
import type { Ref } from "@solid-primitives/refs"
import { mergeRefs } from "@solid-primitives/refs"
import type {
ChartComponent,
ChartData,
ChartItem,
ChartOptions,
Plugin as ChartPlugin,
ChartType,
ChartTypeRegistry,
TooltipModel
} from "chart.js"
import {
ArcElement,
BarController,
BarElement,
BubbleController,
CategoryScale,
Chart,
Colors,
DoughnutController,
Filler,
Legend,
LinearScale,
LineController,
LineElement,
PieController,
PointElement,
PolarAreaController,
RadarController,
RadialLinearScale,
ScatterController,
Tooltip
} from "chart.js"
type TypedChartProps = {
data: ChartData
options?: ChartOptions
plugins?: ChartPlugin[]
ref?: Ref<HTMLCanvasElement | null>
width?: number | undefined
height?: number | undefined
}
type ChartProps = TypedChartProps & {
type: ChartType
}
type ChartContext = {
chart: Chart
tooltip: TooltipModel<keyof ChartTypeRegistry>
}
const BaseChart: Component<ChartProps> = (rawProps) => {
const [canvasRef, setCanvasRef] = createSignal<HTMLCanvasElement | null>()
const [chart, setChart] = createSignal<Chart>()
const props = mergeProps(
{
width: 512,
height: 512,
options: { responsive: true } as ChartOptions,
plugins: [] as ChartPlugin[]
},
rawProps
)
const init = () => {
const ctx = canvasRef()?.getContext("2d") as ChartItem
const config = unwrap(props)
const chart = new Chart(ctx, {
type: config.type,
data: config.data,
options: config.options,
plugins: config.plugins
})
setChart(chart)
}
onMount(() => init())
createEffect(
on(
() => props.data,
() => {
chart()!.data = props.data
chart()!.update()
},
{ defer: true }
)
)
createEffect(
on(
() => props.options,
() => {
chart()!.options = props.options
chart()!.update()
},
{ defer: true }
)
)
createEffect(
on(
[() => props.width, () => props.height],
() => {
chart()!.resize(props.width, props.height)
},
{ defer: true }
)
)
createEffect(
on(
() => props.type,
() => {
const dimensions = [chart()!.width, chart()!.height]
chart()!.destroy()
init()
chart()!.resize(...dimensions)
},
{ defer: true }
)
)
onCleanup(() => {
chart()?.destroy()
mergeRefs(props.ref, null)
})
Chart.register(Colors, Filler, Legend, Tooltip)
return (
<canvas
ref={mergeRefs(props.ref, (el) => setCanvasRef(el))}
height={props.height}
width={props.width}
/>
)
}
function showTooltip(context: ChartContext) {
let el = document.getElementById("chartjs-tooltip")
if (!el) {
el = document.createElement("div")
el.id = "chartjs-tooltip"
document.body.appendChild(el)
}
const model = context.tooltip
if (model.opacity === 0 || !model.body) {
el.style.opacity = "0"
return
}
el.className = `p-2 bg-card text-card-foreground rounded-lg border shadow-sm text-sm ${
model.yAlign ?? `no-transform`
}`
let content = ""
model.title.forEach((title) => {
content += `<h3 class="font-semibold leading-none tracking-tight">${title}</h3>`
})
content += `<div class="mt-1 text-muted-foreground">`
const body = model.body.flatMap((body) => body.lines)
body.forEach((line, i) => {
const colors = model.labelColors[i]
content += `
<div class="flex items-center">
<span class="inline-block h-2 w-2 mr-1 rounded-full border" style="background: ${colors.backgroundColor}; border-color: ${colors.borderColor}"></span>
${line}
</div>`
})
content += `</div>`
el.innerHTML = content
const pos = context.chart.canvas.getBoundingClientRect()
el.style.opacity = "1"
el.style.position = "absolute"
el.style.left = `${pos.left + window.scrollX + model.caretX}px`
el.style.top = `${pos.top + window.scrollY + model.caretY}px`
el.style.pointerEvents = "none"
}
function createTypedChart(
type: ChartType,
components: ChartComponent[]
): Component<TypedChartProps> {
const chartsWithScales: ChartType[] = ["bar", "line", "scatter"]
const chartsWithLegends: ChartType[] = ["bar", "line"]
const options: ChartOptions = {
responsive: true,
maintainAspectRatio: false,
scales: chartsWithScales.includes(type)
? {
x: {
border: { display: false },
grid: { display: false }
},
y: {
border: {
dash: [3],
dashOffset: 3,
display: false
},
grid: {
color: "hsla(240, 3.8%, 46.1%, 0.4)"
}
}
}
: {},
plugins: {
legend: chartsWithLegends.includes(type)
? {
display: true,
align: "end",
labels: {
usePointStyle: true,
boxWidth: 6,
boxHeight: 6,
color: "hsl(240, 3.8%, 46.1%)",
font: { size: 14 }
}
}
: { display: false },
tooltip: {
enabled: false,
external: (context) => showTooltip(context)
}
}
}
Chart.register(...components)
return (props) => <BaseChart type={type} options={options} {...props} />
}
const BarChart = /* #__PURE__ */ createTypedChart("bar", [
BarController,
BarElement,
CategoryScale,
LinearScale
])
const BubbleChart = /* #__PURE__ */ createTypedChart("bubble", [
BubbleController,
PointElement,
LinearScale
])
const DonutChart = /* #__PURE__ */ createTypedChart("doughnut", [DoughnutController, ArcElement])
const LineChart = /* #__PURE__ */ createTypedChart("line", [
LineController,
LineElement,
PointElement,
CategoryScale,
LinearScale
])
const PieChart = /* #__PURE__ */ createTypedChart("pie", [PieController, ArcElement])
const PolarAreaChart = /* #__PURE__ */ createTypedChart("polarArea", [
PolarAreaController,
ArcElement,
RadialLinearScale
])
const RadarChart = /* #__PURE__ */ createTypedChart("radar", [
RadarController,
LineElement,
PointElement,
RadialLinearScale
])
const ScatterChart = /* #__PURE__ */ createTypedChart("scatter", [
ScatterController,
PointElement,
LinearScale
])
export {
BaseChart as Chart,
BarChart,
BubbleChart,
DonutChart,
LineChart,
PieChart,
PolarAreaChart,
RadarChart,
ScatterChart
}

View File

@@ -0,0 +1,30 @@
import { Checkbox as CheckboxPrimitive } from "radix-ui"
import { cn } from "~/lib/utils"
import { CheckIcon } from "lucide-react"
import type { ComponentProps } from "solid-js"
function Checkbox({
className,
...props
}: ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer relative flex size-4.5 shrink-0 items-center justify-center rounded-none border border-input bg-transparent transition-shadow outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
>
<CheckIcon />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@@ -0,0 +1,265 @@
import { cn } from "~/lib/utils"
import { CheckIcon, ChevronRightIcon } from "lucide-react"
import type { ComponentProps } from "solid-js"
function DropdownMenu({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
align = "start",
sideOffset = 4,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
align={align}
className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-48 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-none p-1.5 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!", className )}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/dropdown-menu-item relative flex cursor-default items-center gap-2.5 rounded-none px-3 py-2 text-xs font-medium tracking-wider uppercase outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-9.5 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 data-[variant=destructive]:*:[svg]:text-destructive",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
inset,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-2.5 rounded-none py-2 pr-8 pl-3 text-xs font-medium tracking-wider uppercase outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-9.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className
)}
checked={checked}
{...props}
>
<span
class="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-checkbox-item-indicator"
>
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon
/>
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
inset,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-2.5 rounded-none py-2 pr-8 pl-3 text-xs font-medium tracking-wider uppercase outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-9.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className
)}
{...props}
>
<span
class="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-radio-item-indicator"
>
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon
/>
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-3 py-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase data-inset:pl-9.5",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("-mx-1.5 my-1.5 h-px bg-border/50", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
class: className,
...props
}: ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
class={cn(
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-2 rounded-none px-3 py-2 text-xs font-medium tracking-wider uppercase outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-9.5 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn("z-50 min-w-36 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-none p-1.5 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!", className )}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

234
src/components/ui/field.tsx Normal file
View File

@@ -0,0 +1,234 @@
import { splitProps, createMemo, Show, For, type ComponentProps, type JSX } from "solid-js"
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(props: ComponentProps<"fieldset">) {
const [local, rest] = splitProps(props, ["class"])
return (
<fieldset
data-slot="field-set"
class={cn(
"flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
local.class
)}
{...rest}
/>
)
}
function FieldLegend(
props: ComponentProps<"legend"> & { variant?: "legend" | "label" }
) {
const [local, rest] = splitProps(props, ["class", "variant"])
return (
<legend
data-slot="field-legend"
data-variant={local.variant || "legend"}
class={cn(
"mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
local.class,
)}
{...rest}
/>
)
}
function FieldGroup(props: ComponentProps<"div">) {
const [local, rest] = splitProps(props, ["class"])
return (
<div
data-slot="field-group"
class={cn(
"group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
local.class,
)}
{...rest}
/>
)
}
const fieldVariants = cva(
"group/field flex w-full gap-2 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(
props: ComponentProps<"div"> & VariantProps<typeof fieldVariants>
) {
const [local, rest] = splitProps(props, ["class", "orientation"])
return (
<div
role="group"
data-slot="field"
data-orientation={local.orientation || "vertical"}
class={cn(
fieldVariants({ orientation: local.orientation || "vertical" }),
local.class,
)}
{...rest}
/>
)
}
function FieldContent(props: ComponentProps<"div">) {
const [local, rest] = splitProps(props, ["class"])
return (
<div
data-slot="field-content"
class={cn(
"group/field-content flex flex-1 flex-col gap-0.5 leading-snug",
local.class
)}
{...rest}
/>
)
}
function FieldLabel(props: ComponentProps<typeof Label>) {
const [local, rest] = splitProps(props, ["class"])
return (
<Label
data-slot="field-label"
class={cn(
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
local.class,
)}
{...rest}
/>
)
}
function FieldTitle(props: ComponentProps<"div">) {
const [local, rest] = splitProps(props, ["class"])
return (
<div
data-slot="field-label"
class={cn(
"flex w-fit items-center gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50",
local.class,
)}
{...rest}
/>
)
}
function FieldDescription(props: ComponentProps<"p">) {
const [local, rest] = splitProps(props, ["class"])
return (
<p
data-slot="field-description"
class={cn(
"text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
"last:mt-0 nth-last-2:-mt-1",
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
local.class,
)}
{...rest}
/>
)
}
function FieldSeparator(
props: ComponentProps<"div"> & {
children?: JSX.Element
}
) {
const [local, rest] = splitProps(props, ["class", "children"])
return (
<div
data-slot="field-separator"
data-content={!!local.children}
class={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
local.class,
)}
{...rest}
>
<Separator class="absolute inset-0 top-1/2" />
<Show when={local.children}>
<span
class="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
data-slot="field-separator-content"
>
{local.children}
</span>
</Show>
</div>
)
}
function FieldError(
props: ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>
}
) {
const [local, rest] = splitProps(props, ["class", "children", "errors"])
const uniqueErrors = createMemo(() => {
if (!local.errors?.length) return []
return [
...new Map(local.errors.map((error) => [error?.message, error])).values(),
]
})
return (
<Show when={local.children || uniqueErrors().length > 0}>
<div
role="alert"
data-slot="field-error"
class={cn("text-sm font-normal text-destructive", local.class)}
{...rest}
>
<Show
when={local.children}
fallback={
<Show
when={uniqueErrors().length === 1}
fallback={
<ul class="ml-4 flex list-disc flex-col gap-1">
<For each={uniqueErrors()}>
{(error) => error?.message && <li>{error.message}</li>}
</For>
</ul>
}
>
{uniqueErrors()[0]?.message}
</Show>
}
>
{local.children}
</Show>
</div>
</Show>
)
}
export {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
}

View File

@@ -0,0 +1,18 @@
import { cn } from "~/lib/utils"
import type { ComponentProps } from "solid-js"
function Input({ class: className, type, ...props }: ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
class={cn(
"h-10 w-full min-w-0 border border-transparent border-b-input bg-transparent px-0 py-1 text-base transition-[color,border-color] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-b-ring disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-b-destructive md:text-sm dark:aria-invalid:border-b-destructive/50",
className
)}
{...props}
/>
)
}
export { Input }

View File

@@ -0,0 +1,19 @@
import type { Component, ComponentProps } from "solid-js"
import { splitProps } from "solid-js"
import { cn } from "~/lib/utils"
const Label: Component<ComponentProps<"label">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<label
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
local.class
)}
{...others}
/>
)
}
export { Label }

View File

@@ -0,0 +1,181 @@
import type { JSX, ValidComponent } from "solid-js"
import { splitProps } from "solid-js"
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
import * as SelectPrimitive from "@kobalte/core/select"
import { cva } from "class-variance-authority"
import { cn } from "~/lib/utils"
const Select = SelectPrimitive.Root
const SelectValue = SelectPrimitive.Value
const SelectHiddenSelect = SelectPrimitive.HiddenSelect
type SelectTriggerProps<T extends ValidComponent = "button"> =
SelectPrimitive.SelectTriggerProps<T> & {
class?: string | undefined
children?: JSX.Element
}
const SelectTrigger = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, SelectTriggerProps<T>>
) => {
const [local, others] = splitProps(props as SelectTriggerProps, ["class", "children"])
return (
<SelectPrimitive.Trigger
class={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
local.class
)}
{...others}
>
{local.children}
<SelectPrimitive.Icon
as="svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4 opacity-50"
>
<path d="M8 9l4 -4l4 4" />
<path d="M16 15l-4 4l-4 -4" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
type SelectContentProps<T extends ValidComponent = "div"> =
SelectPrimitive.SelectContentProps<T> & { class?: string | undefined }
const SelectContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, SelectContentProps<T>>
) => {
const [local, others] = splitProps(props as SelectContentProps, ["class"])
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
class={cn(
"relative z-50 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-80",
local.class
)}
{...others}
>
<SelectPrimitive.Listbox class="m-0 p-1" />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
type SelectItemProps<T extends ValidComponent = "li"> = SelectPrimitive.SelectItemProps<T> & {
class?: string | undefined
children?: JSX.Element
}
const SelectItem = <T extends ValidComponent = "li">(
props: PolymorphicProps<T, SelectItemProps<T>>
) => {
const [local, others] = splitProps(props as SelectItemProps, ["class", "children"])
return (
<SelectPrimitive.Item
class={cn(
"relative mt-0 flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
local.class
)}
{...others}
>
<SelectPrimitive.ItemIndicator class="absolute right-2 flex size-3.5 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M5 12l5 5l10 -10" />
</svg>
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemLabel>{local.children}</SelectPrimitive.ItemLabel>
</SelectPrimitive.Item>
)
}
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
{
variants: {
variant: {
label: "data-[invalid]:text-destructive",
description: "font-normal text-muted-foreground",
error: "text-xs text-destructive"
}
},
defaultVariants: {
variant: "label"
}
}
)
type SelectLabelProps<T extends ValidComponent = "label"> = SelectPrimitive.SelectLabelProps<T> & {
class?: string | undefined
}
const SelectLabel = <T extends ValidComponent = "label">(
props: PolymorphicProps<T, SelectLabelProps<T>>
) => {
const [local, others] = splitProps(props as SelectLabelProps, ["class"])
return <SelectPrimitive.Label class={cn(labelVariants(), local.class)} {...others} />
}
type SelectDescriptionProps<T extends ValidComponent = "div"> =
SelectPrimitive.SelectDescriptionProps<T> & {
class?: string | undefined
}
const SelectDescription = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, SelectDescriptionProps<T>>
) => {
const [local, others] = splitProps(props as SelectDescriptionProps, ["class"])
return (
<SelectPrimitive.Description
class={cn(labelVariants({ variant: "description" }), local.class)}
{...others}
/>
)
}
type SelectErrorMessageProps<T extends ValidComponent = "div"> =
SelectPrimitive.SelectErrorMessageProps<T> & {
class?: string | undefined
}
const SelectErrorMessage = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, SelectErrorMessageProps<T>>
) => {
const [local, others] = splitProps(props as SelectErrorMessageProps, ["class"])
return (
<SelectPrimitive.ErrorMessage
class={cn(labelVariants({ variant: "error" }), local.class)}
{...others}
/>
)
}
export {
Select,
SelectValue,
SelectHiddenSelect,
SelectTrigger,
SelectContent,
SelectItem,
SelectLabel,
SelectDescription,
SelectErrorMessage
}

View File

@@ -0,0 +1,25 @@
import type { ComponentProps } from "solid-js"
import { cn } from "~/lib/utils"
function Separator({
class: className,
orientation = "horizontal",
decorative = true,
...props
}: ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}
/>
)
}
export { Separator }

View File

148
src/components/ui/sheet.tsx Normal file
View File

@@ -0,0 +1,148 @@
import { Dialog as SheetPrimitive } from "radix-ui"
import { cn } from "~/lib/utils"
import { Button } from "~/components/ui/button"
import { XIcon } from "lucide-react"
import type { ComponentProps } from "solid-js"
function Sheet({ ...props }: ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/20 duration-100 supports-backdrop-filter:backdrop-blur-sm data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
showCloseButton = true,
...props
}: ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
showCloseButton?: boolean
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
data-side={side}
className={cn(
"fixed z-50 flex flex-col bg-popover bg-clip-padding text-sm text-popover-foreground shadow-md transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10",
className
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close data-slot="sheet-close" asChild>
<Button
variant="ghost"
class="absolute top-4 right-4 bg-secondary"
size="icon-sm"
>
<XIcon
/>
<span class="sr-only">Close</span>
</Button>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ class: className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
class={cn("flex flex-col gap-1.5 p-8", className)}
{...props}
/>
)
}
function SheetFooter({ class: className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
class={cn("mt-auto flex flex-col gap-2 p-8", className)}
{...props}
/>
)
}
function SheetTitle({
className,
...props
}: ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn(
"font-heading text-lg font-semibold tracking-wider text-foreground uppercase",
className
)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn(
"mt-0.5 text-sm leading-relaxed text-muted-foreground",
className
)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@@ -0,0 +1,703 @@
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { useIsMobile } from "~/hooks/use-mobile"
import { cn } from "~/lib/utils"
import { Button } from "~/components/ui/button"
import { Input } from "~/components/ui/input"
import { Separator } from "~/components/ui/separator"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "~/components/ui/sheet"
import { Skeleton } from "~/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "~/components/ui/tooltip"
import { PanelLeftIcon } from "lucide-react"
import { createContext, useContext, type ComponentProps } from "solid-js"
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContextProps = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = createContext<SidebarContextProps | null>(null)
function useSidebar() {
const context = useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
class: className,
style,
children,
...props
}: ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}) {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = useState(defaultOpen)
const open = openProp ?? _open
const setOpen = useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
_setOpen(openState)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
// Helper to toggle the sidebar.
const toggleSidebar = useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const contextValue = useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
<SidebarContext.Provider value={contextValue}>
<div
data-slot="sidebar-wrapper"
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as CSSProperties
}
class={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",
className
)}
{...props}
>
{children}
</div>
</SidebarContext.Provider>
)
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
class: className,
children,
dir,
...props
}: ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
<div
data-slot="sidebar"
class={cn(
"flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",
className
)}
{...props}
>
{children}
</div>
)
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
dir={dir}
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
class="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as CSSProperties
}
side={side}
>
<SheetHeader class="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div class="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
}
return (
<div
class="group peer hidden text-sidebar-foreground md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
data-slot="sidebar"
>
{/* This is what handles the sidebar gap on desktop */}
<div
data-slot="sidebar-gap"
class={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
)}
/>
<div
data-slot="sidebar-container"
data-side={side}
class={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
class="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-none group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border"
>
{children}
</div>
</div>
</div>
)
}
function SidebarTrigger({
class: className,
onClick,
...props
}: ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar()
return (
<Button
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
size="icon-sm"
class={cn(className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<PanelLeftIcon />
<span class="sr-only">Toggle Sidebar</span>
</Button>
)
}
function SidebarRail({ class: className, ...props }: ComponentProps<"button">) {
const { toggleSidebar } = useSidebar()
return (
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
class={cn(
"absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
)
}
function SidebarInset({ class: className, ...props }: ComponentProps<"main">) {
return (
<main
data-slot="sidebar-inset"
class={cn(
"relative flex w-full flex-1 flex-col bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-none md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className
)}
{...props}
/>
)
}
function SidebarInput({
class: className,
...props
}: ComponentProps<typeof Input>) {
return (
<Input
data-slot="sidebar-input"
data-sidebar="input"
class={cn(
"h-8 w-full border-input bg-transparent shadow-none",
className
)}
{...props}
/>
)
}
function SidebarHeader({ class: className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
class={cn("flex flex-col gap-2 p-2 [--radius:0]", className)}
{...props}
/>
)
}
function SidebarFooter({ class: className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
class={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
}
function SidebarSeparator({
class: className,
...props
}: ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
class={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
)
}
function SidebarContent({ class: className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="sidebar-content"
data-sidebar="content"
class={cn(
"no-scrollbar flex min-h-0 flex-1 flex-col gap-2 overflow-auto [--radius:0] group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}
/>
)
}
function SidebarGroup({ class: className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
class={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
}
function SidebarGroupLabel({
class: className,
asChild = false,
...props
}: ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "div"
return (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
class={cn(
"flex h-8 shrink-0 items-center rounded-none px-3 text-xs font-semibold tracking-wider text-sidebar-foreground/70 uppercase ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-3.5 [&>svg]:shrink-0",
className
)}
{...props}
/>
)
}
function SidebarGroupAction({
class: className,
asChild = false,
...props
}: ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
class={cn(
"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-none p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-3.5 [&>svg]:shrink-0",
className
)}
{...props}
/>
)
}
function SidebarGroupContent({
class: className,
...props
}: ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
class={cn("w-full text-sm", className)}
{...props}
/>
)
}
function SidebarMenu({ class: className, ...props }: ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
class={cn("flex w-full min-w-0 flex-col gap-0.5", className)}
{...props}
/>
)
}
function SidebarMenuItem({ class: className, ...props }: ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
class={cn("group/menu-item relative", className)}
{...props}
/>
)
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-none px-3 py-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]",
},
size: {
default: "h-9 text-sm",
sm: "h-8 text-xs",
lg: "h-14 px-3 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function SidebarMenuButton({
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
class: className,
...props
}: ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot.Root : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
data-slot="sidebar-menu-button"
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
class={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
)
}
function SidebarMenuAction({
class: className,
asChild = false,
showOnHover = false,
...props
}: ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
class={cn(
"absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-none p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-2 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-3.5 [&>svg]:shrink-0",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0",
className
)}
{...props}
/>
)
}
function SidebarMenuBadge({
class: className,
...props
}: ComponentProps<"div">) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
class={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-none px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-active/menu-button:text-sidebar-accent-foreground",
className
)}
{...props}
/>
)
}
function SidebarMenuSkeleton({
class: className,
showIcon = false,
...props
}: ComponentProps<"div"> & {
showIcon?: boolean
}) {
// Random width between 50 to 90%.
const [width] = useState(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
})
return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
class={cn("flex h-8 items-center gap-2 rounded-none px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
class="size-3.5 rounded-none"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
class="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as CSSProperties
}
/>
</div>
)
}
function SidebarMenuSub({ class: className, ...props }: ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
class={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarMenuSubItem({
class: className,
...props
}: ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
class={cn("group/menu-sub-item relative", className)}
{...props}
/>
)
}
function SidebarMenuSubButton({
asChild = false,
size = "md",
isActive = false,
class: className,
...props
}: ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
}) {
const Comp = asChild ? Slot.Root : "a"
return (
<Comp
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
class={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-none px-3 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-3.5 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
className
)}
{...props}
/>
)
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}

View File

@@ -0,0 +1,14 @@
import type { ComponentProps } from "solid-js"
import { cn } from "~/lib/utils"
function Skeleton({ class: className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
class={cn("animate-pulse bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@@ -0,0 +1,60 @@
"use client"
import { createMemo, For, type ComponentProps } from "solid-js"
import { cn } from "~/lib/utils"
import { Slider as SliderPrimitive } from "@kobalte/core/slider"
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}: ComponentProps<typeof SliderPrimitive>) {
const _values = createMemo(
() =>
Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max]
)
return (
<SliderPrimitive
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
className={cn(
"relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:min-h-40 data-vertical:w-auto data-vertical:flex-col",
className
)}
{...props}
>
<SliderPrimitive.Track
data-slot="slider-track"
class="relative grow overflow-hidden rounded-full bg-muted data-horizontal:h-1 data-horizontal:w-full data-vertical:h-full data-vertical:w-1"
>
<SliderPrimitive.Fill
data-slot="slider-range"
class="absolute bg-primary select-none data-horizontal:h-full data-vertical:w-full"
/>
</SliderPrimitive.Track>
<For each={_values()}>
{(_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
class="relative block size-3 shrink-0 rounded-full border border-ring bg-white ring-ring/50 transition-[color,box-shadow] select-none after:absolute after:-inset-2 hover:ring-3 focus-visible:ring-3 focus-visible:outline-hidden active:ring-3 disabled:pointer-events-none disabled:opacity-50"
>
<SliderPrimitive.Input></SliderPrimitive.Input>
</SliderPrimitive.Thumb>
)}
</For>
</SliderPrimitive>
)
}
export { Slider }

View File

@@ -0,0 +1,17 @@
import type { ComponentProps } from "solid-js"
import { cn } from "~/lib/utils"
function Textarea({ class: className, ...props }: ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
class={cn(
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@@ -0,0 +1,55 @@
import { Tooltip as TooltipPrimitive } from "radix-ui"
import type { ComponentProps } from "solid-js"
import { cn } from "~/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: ComponentProps<typeof TooltipPrimitive.Root>) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
}
function TooltipTrigger({
...props
}: ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-none bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-none data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-none bg-foreground fill-foreground" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }

View File

@@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "~/components/ui/sidebar"
import { GalleryVerticalEndIcon, ChevronsUpDownIcon, CheckIcon } from "lucide-react"
export function VersionSwitcher({
versions,
defaultVersion,
}: {
versions: string[]
defaultVersion: string
}) {
const [selectedVersion, setSelectedVersion] = React.useState(defaultVersion)
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<GalleryVerticalEndIcon className="size-4" />
</div>
<div className="flex flex-col gap-0.5 leading-none">
<span className="font-medium">Documentation</span>
<span className="">v{selectedVersion}</span>
</div>
<ChevronsUpDownIcon className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width)"
align="start"
>
{versions.map((version) => (
<DropdownMenuItem
key={version}
onSelect={() => setSelectedVersion(version)}
>
v{version}{" "}
{version === selectedVersion && (
<CheckIcon className="ml-auto" />
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}

19
src/hooks/use-mobile.ts Normal file
View File

@@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}

View File

@@ -0,0 +1,5 @@
import { SolidQueryDevtools } from '@tanstack/solid-query-devtools'
export default function AppTanstackQueryHeaderUser() {
return <SolidQueryDevtools buttonPosition="bottom-right" />
}

View File

@@ -0,0 +1,8 @@
import { QueryClient } from '@tanstack/solid-query'
export function getContext() {
const queryClient = new QueryClient()
return {
queryClient,
}
}

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

86
src/routeTree.gen.ts Normal file
View File

@@ -0,0 +1,86 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as TodoRouteImport } from './routes/todo'
import { Route as IndexRouteImport } from './routes/index'
const TodoRoute = TodoRouteImport.update({
id: '/todo',
path: '/todo',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/todo': typeof TodoRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/todo': typeof TodoRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/todo': typeof TodoRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/todo'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/todo'
id: '__root__' | '/' | '/todo'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
TodoRoute: typeof TodoRoute
}
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
'/todo': {
id: '/todo'
path: '/todo'
fullPath: '/todo'
preLoaderRoute: typeof TodoRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
TodoRoute: TodoRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/solid-start'
declare module '@tanstack/solid-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}

40
src/router.tsx Normal file
View File

@@ -0,0 +1,40 @@
import { createRouter as createTanStackRouter } from '@tanstack/solid-router'
import { routeTree } from './routeTree.gen'
// import { getContext } from './integrations/tanstack-query/provider'
import NotFound from './components/NotFound'
import { QueryClient } from '@tanstack/solid-query'
import { ErrorComponent } from './routes/__root'
import { getContext } from './integrations/tanstack-query/provider'
import { setupRouterSsrQueryIntegration } from '@tanstack/solid-router-ssr-query'
const queryClient = new QueryClient()
export function getRouter() {
const router = createTanStackRouter({
routeTree,
context: getContext(),
scrollRestoration: true,
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
defaultNotFoundComponent: () => <NotFound />,
defaultErrorComponent: ({ error }) => <ErrorComponent error={error} />,
})
setupRouterSsrQueryIntegration({
router,
queryClient
})
return router
}
declare module '@tanstack/solid-router' {
interface Register {
router: ReturnType<typeof getRouter>
}
}

51
src/routes/__root.tsx Normal file
View File

@@ -0,0 +1,51 @@
import {
HeadContent,
Outlet,
Scripts,
createRootRouteWithContext,
} from "@tanstack/solid-router";
import { TanStackRouterDevtools } from "@tanstack/solid-router-devtools";
import "@fontsource/inter/400.css";
import { HydrationScript } from "solid-js/web";
import { Suspense } from "solid-js";
import styleCss from "~/styles.css?url";
import Header from "~/components/Header";
import { ThemeProvider } from "~/components/theme-provider";
export function ErrorComponent({ error }: { error: Error }) {
console.log(error);
return <div>Error: {error.message}</div>;
}
export const Route = createRootRouteWithContext()({
head: () => ({
links: [{ rel: "stylesheet", href: styleCss }],
}),
shellComponent: RootComponent,
errorComponent: ErrorComponent,
});
function RootComponent() {
return (
<html>
<head>
<HydrationScript />
<HeadContent />
</head>
<body>
<Suspense>
<ThemeProvider>
<Header />
<Outlet />
</ThemeProvider>
<TanStackRouterDevtools />
</Suspense>
<Scripts />
</body>
</html>
);
}

75
src/routes/index.tsx Normal file
View File

@@ -0,0 +1,75 @@
import { createFileRoute } from '@tanstack/solid-router'
import { Button } from '~/components/ui/button'
import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '~/components/ui/card'
import { Input } from '~/components/ui/input'
import { Label } from '~/components/ui/label'
export const Route = createFileRoute('/')({ component: App })
function App() {
return (
<div class="flex min-h-svh p-6">
<div class="flex max-w-md min-w-0 flex-col gap-4 text-sm leading-loose">
<div>
<h1 class="font-medium">Project ready!</h1>
<p>You may now add components and start building.</p>
<p>We&apos;ve already added the button component for you.</p>
<Button class="mt-2">Button</Button>
</div>
<div class="font-mono text-xs text-muted-foreground">
(Press <kbd>d</kbd> to toggle dark mode)
</div>
<Card class="w-full max-w-sm">
<CardHeader>
<CardTitle>Login to your account</CardTitle>
<CardDescription>
Enter your email below to login to your account
</CardDescription>
<CardAction>
<Button variant="link">Sign Up</Button>
</CardAction>
</CardHeader>
<CardContent>
<form>
<div class="flex flex-col gap-6">
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
/>
</div>
<div class="grid gap-2">
<div class="flex items-center">
<Label for="password">Password</Label>
<a
href="#"
class="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</a>
</div>
<Input id="password" type="password" required />
</div>
</div>
</form>
</CardContent>
<CardFooter class="flex-col gap-2">
<Button type="submit" class="w-full">
Login
</Button>
<Button variant="outline" class="w-full">
Login with Google
</Button>
</CardFooter>
</Card>
</div>
</div>
)
}

53
src/routes/todo.tsx Normal file
View File

@@ -0,0 +1,53 @@
import { queryOptions, useQuery } from "@tanstack/solid-query";
import { createFileRoute } from "@tanstack/solid-router";
import { For, Match, Switch } from "solid-js";
export const Route = createFileRoute("/todo")({
component: RouteComponent,
});
function RouteComponent() {
const query = useQuery(() =>
queryOptions({
queryKey: ["todo"],
queryFn: () =>
fetch("https://jsonplaceholder.typicode.com/todos").then((res) =>
res.json(),
),
}),
);
return (
<div>
<Switch fallback={<>Loading...</>}>
<Match when={query.isError}>
<div>Error: {query.error?.message}</div>
</Match>
<Match when={query.isLoading}>
<div>Loading...</div>
</Match>
<Match when={query.data}>
<For each={query.data}>{(item) => <TodoItem item={item} />}</For>
</Match>
</Switch>
</div>
);
}
interface TodoItemProps {
item: {
userId: number;
id: number;
title: string;
completed: boolean;
};
}
function TodoItem({ item }: TodoItemProps) {
return (
<div class="p-2">
<div>{item.title}</div>
<div>{item.completed ? "Completed" : "Pending"}</div>
</div>
);
}

10
src/server.ts Normal file
View File

@@ -0,0 +1,10 @@
import { FastResponse } from "srvx"
import handler, { createServerEntry } from '@tanstack/solid-start/server-entry'
globalThis.Response = FastResponse;
export default createServerEntry({
fetch(request) {
return handler.fetch(request)
},
})

95
src/shadcn-tailwind.css Normal file
View File

@@ -0,0 +1,95 @@
@theme inline {
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(
--radix-accordion-content-height,
var(--accordion-panel-height, auto)
);
}
}
@keyframes accordion-up {
from {
height: var(
--radix-accordion-content-height,
var(--accordion-panel-height, auto)
);
}
to {
height: 0;
}
}
}
/* Custom variants */
@custom-variant data-open {
&:where([data-state="open"]),
&:where([data-open]:not([data-open="false"])) {
@slot;
}
}
@custom-variant data-closed {
&:where([data-state="closed"]),
&:where([data-closed]:not([data-closed="false"])) {
@slot;
}
}
@custom-variant data-checked {
&:where([data-state="checked"]),
&:where([data-checked]:not([data-checked="false"])) {
@slot;
}
}
@custom-variant data-unchecked {
&:where([data-state="unchecked"]),
&:where([data-unchecked]:not([data-unchecked="false"])) {
@slot;
}
}
@custom-variant data-selected {
&:where([data-selected="true"]) {
@slot;
}
}
@custom-variant data-disabled {
&:where([data-disabled="true"]),
&:where([data-disabled]:not([data-disabled="false"])) {
@slot;
}
}
@custom-variant data-active {
&:where([data-state="active"]),
&:where([data-active]:not([data-active="false"])) {
@slot;
}
}
@custom-variant data-horizontal {
&:where([data-orientation="horizontal"]) {
@slot;
}
}
@custom-variant data-vertical {
&:where([data-orientation="vertical"]) {
@slot;
}
}
@utility no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}

133
src/styles.css Normal file
View File

@@ -0,0 +1,133 @@
@import "tailwindcss";
/* @import "tw-animate-css"; */
@import "./shadcn-tailwind.css";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@fontsource-variable/noto-sans";
/* @import "@fontsource-variable/noto-sans"; */
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-heading: var(--font-sans);
--font-sans: 'Noto 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(--primary);
--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);
}
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.148 0.004 228.8);
--card: oklch(1 0 0);
--card-foreground: oklch(0.148 0.004 228.8);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.148 0.004 228.8);
--primary: oklch(0.508 0.118 165.612);
--primary-foreground: oklch(0.979 0.021 166.113);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.963 0.002 197.1);
--muted-foreground: oklch(0.56 0.021 213.5);
--accent: oklch(0.963 0.002 197.1);
--accent-foreground: oklch(0.218 0.008 223.9);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.925 0.005 214.3);
--input: oklch(0.925 0.005 214.3);
--ring: oklch(0.723 0.014 214.4);
--chart-1: oklch(0.785 0.115 274.713);
--chart-2: oklch(0.585 0.233 277.117);
--chart-3: oklch(0.511 0.262 276.966);
--chart-4: oklch(0.457 0.24 277.023);
--chart-5: oklch(0.398 0.195 277.366);
--radius: 0.625rem;
--sidebar: oklch(0.987 0.002 197.1);
--sidebar-foreground: oklch(0.148 0.004 228.8);
--sidebar-primary: oklch(0.596 0.145 163.225);
--sidebar-primary-foreground: oklch(0.979 0.021 166.113);
--sidebar-accent: oklch(0.963 0.002 197.1);
--sidebar-accent-foreground: oklch(0.218 0.008 223.9);
--sidebar-border: oklch(0.925 0.005 214.3);
--sidebar-ring: oklch(0.723 0.014 214.4);
}
.dark {
--background: oklch(0.148 0.004 228.8);
--foreground: oklch(0.987 0.002 197.1);
--card: oklch(0.218 0.008 223.9);
--card-foreground: oklch(0.987 0.002 197.1);
--popover: oklch(0.218 0.008 223.9);
--popover-foreground: oklch(0.987 0.002 197.1);
--primary: oklch(0.432 0.095 166.913);
--primary-foreground: oklch(0.979 0.021 166.113);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.275 0.011 216.9);
--muted-foreground: oklch(0.723 0.014 214.4);
--accent: oklch(0.275 0.011 216.9);
--accent-foreground: oklch(0.987 0.002 197.1);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.56 0.021 213.5);
--chart-1: oklch(0.785 0.115 274.713);
--chart-2: oklch(0.585 0.233 277.117);
--chart-3: oklch(0.511 0.262 276.966);
--chart-4: oklch(0.457 0.24 277.023);
--chart-5: oklch(0.398 0.195 277.366);
--sidebar: oklch(0.218 0.008 223.9);
--sidebar-foreground: oklch(0.987 0.002 197.1);
--sidebar-primary: oklch(0.696 0.17 162.48);
--sidebar-primary-foreground: oklch(0.262 0.051 172.552);
--sidebar-accent: oklch(0.275 0.011 216.9);
--sidebar-accent-foreground: oklch(0.987 0.002 197.1);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.56 0.021 213.5);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}