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

24
.cta.json Normal file
View File

@@ -0,0 +1,24 @@
{
"projectName": "solid-demo",
"mode": "file-router",
"typescript": true,
"packageManager": "npm",
"includeExamples": true,
"tailwind": true,
"addOnOptions": {},
"envVarValues": {},
"git": true,
"install": true,
"routerOnly": false,
"version": 1,
"framework": "solid",
"chosenAddOns": [
"biome",
"nitro",
"form",
"sentry",
"solid-ui",
"store",
"tanstack-query"
]
}

57
.cursorrules Normal file
View File

@@ -0,0 +1,57 @@
// Solid.js with Tailwind CSS .cursorrules
// Prefer functional components
const preferFunctionalComponents = true;
// Solid.js and Tailwind CSS best practices
const solidjsTailwindBestPractices = [
"Use createSignal() for reactive state",
"Implement Tailwind CSS classes for styling",
"Utilize TypeScript's strict mode",
"Utilize @apply directive in CSS files for reusable styles",
"Implement responsive design using Tailwind's responsive classes",
"Use Tailwind's CSS in /src/styles.css for global styles",
"Implement dark mode using Tailwind's dark variant",
];
// Additional instructions
const additionalInstructions = `
1. Use .tsx extension for files with JSX
2. Implement strict TypeScript checks
3. Implement proper Tailwind CSS purging for production builds
4. Utilize TanStack Router for routing when applicable
5. Use type-safe context with createContext
6. Implement proper typing for event handlers
7. Follow TypeScript best practices and naming conventions
8. Use type assertions sparingly and only when necessary
9. Use Tailwind's @layer directive for custom styles
10. Implement utility-first CSS approach
11. Follow both Solid.js and Tailwind naming conventions
12. Use JIT (Just-In-Time) mode for faster development
`;
We use Sentry for watching for errors in our deployed application, as well as for instrumentation of our application.
## Error collection
Error collection is automatic and configured in `src/router.tsx`.
## Instrumentation
We want our server functions intstrumented. So if you see a function name like `createServerFn`, you can instrument it with Sentry. You'll need to import `Sentry`:
```tsx
import * as Sentry from '@sentry/browser'
```
And then wrap the implementation of the server function with `Sentry.startSpan`, liks so:
```tsx
Sentry.startSpan({ name: 'Requesting all the pokemon' }, async () => {
// Some lengthy operation here
await fetch('https://api.pokemon.com/data/')
})
```

39
.github/workflows/docker.yaml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Build and Push Docker Image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
container:
image: quay.io/buildah/stable
options: --security-opt seccomp=unconfined --security-opt apparmor=unconfined --device /dev/fuse:rw --privileged
env:
BUILDAH_ISOLATION: chroot
STORAGE_DRIVER: vfs
steps:
- name: Install Node.js
run: dnf install -y nodejs git
- uses: actions/checkout@v4
with:
token: ${{ secrets.REGISTRY_PASSWORD }}
submodules: true
- name: Login to Registry
run: buildah login -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_PASSWORD }} git.kocoder.xyz
- name: Buildah Build
run: |
buildah build -t ${{ github.sha }} .
buildah tag ${{ github.sha }} latest
- name: Push Docker Images
run: |
buildah push ${{ github.sha }} docker://git.kocoder.xyz/vt/solid-demo:${{ github.sha }}
buildah push latest docker://git.kocoder.xyz/vt/solid-demo:latest

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.env
.nitro
.tanstack
.wrangler
.output
.vinxi
__unconfig*
.odo

35
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,35 @@
{
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
},
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
},
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[css]": {
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit"
}
}

33
Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
FROM node:24-alpine AS builder
# Enable pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
# Copy package files and install dependencies
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
# Copy the rest of the application code and build
COPY . .
RUN CI="true" pnpm run build
# Production stage
FROM node:24-alpine AS runner
WORKDIR /app
# Set environment variables for production
ENV NODE_ENV=production
ENV PORT=3000
# Copy only the built output from the builder stage
# Nitro typically outputs the production server to .output/
COPY --from=builder /app/.output ./.output
COPY --from=builder /app/package.json ./
EXPOSE 3000
# Start the production server
CMD ["node", ".output/server/index.mjs"]

162
README.md Normal file
View File

@@ -0,0 +1,162 @@
Welcome to your new TanStack Start app!
# Getting Started
To run this application:
```bash
npm install
npm run dev
```
# Building For Production
To build this application for production:
```bash
npm run build
```
## Styling
This project uses [Tailwind CSS](https://tailwindcss.com/) for styling.
### Removing Tailwind CSS
If you prefer not to use Tailwind CSS:
1. Remove the demo pages in `src/routes/demo/`
2. Replace the Tailwind import in `src/styles.css` with your own styles
3. Remove `tailwindcss()` from the plugins array in `vite.config.ts`
4. Uninstall the packages: `npm install @tailwindcss/vite tailwindcss -D`
## Deploy with Nitro
This project uses Nitro as a generic server adapter, so it can run on any Node-compatible host.
```bash
npm run build
node dist/server/index.mjs
```
The build output is a self-contained Node server. To deploy, push the `dist/` directory to your host (Render, Fly.io, your own VPS, etc.) and run the server command above.
For host-specific presets (Vercel, Netlify, Cloudflare, AWS Lambda, etc.) and tuning, see https://v3.nitro.build/deploy.
## Solid-UI
This installation of Solid-UI follows the manual instructions but was modified to work with Tailwind V4.
To install the components, run the following command (this install button):
```bash
npx -y solidui-cli@latest add button
```
## Routing
This project uses [TanStack Router](https://tanstack.com/router) with file-based routing. Routes are managed as files in `src/routes`.
### Adding A Route
To add a new route to your application just add a new file in the `./src/routes` directory.
TanStack will automatically generate the content of the route file for you.
Now that you have two routes you can use a `Link` component to navigate between them.
### Adding Links
To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/solid-router`.
```tsx
import { Link } from "@tanstack/solid-router";
```
Then anywhere in your JSX you can use it like so:
```tsx
<Link to="/about">About</Link>
```
This will create a link that will navigate to the `/about` route.
More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/solid/api/router/linkComponent).
### Using A Layout
In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes.
More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/solid/guide/routing-concepts#layouts).
## Server Functions
TanStack Start provides server functions that allow you to write server-side code that seamlessly integrates with your client components.
```tsx
import { createServerFn } from '@tanstack/solid-start'
const getServerTime = createServerFn({
method: 'GET',
}).handler(async () => {
return new Date().toISOString()
})
```
## Data Fetching
There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered.
For example:
```tsx
import { createFileRoute } from '@tanstack/solid-router'
export const Route = createFileRoute('/people')({
loader: async () => {
const response = await fetch('https://swapi.dev/api/people')
return response.json()
},
component: PeopleComponent,
})
function PeopleComponent() {
const data = Route.useLoaderData()
return (
<ul>
<For each={data().results}>
{(person) => <li>{person.name}</li>}
</For>
</ul>
)
}
```
Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/solid/guide/data-loading#loader-parameters).
# Demo files
Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed.
## Linting & Formatting
This project uses [Biome](https://biomejs.dev/) for linting and formatting. The following scripts are available:
```bash
npm run lint
npm run format
npm run check
```
# Learn More
You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).
For TanStack Start specific documentation, visit [TanStack Start](https://tanstack.com/start).

31
biome.json Normal file
View File

@@ -0,0 +1,31 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": ["src/routeTree.gen.ts"],
"include": ["src/*", ".vscode/*", "index.html", "vite.config.ts"]
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
}
}

25
components.json Normal file
View File

@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "radix-sera",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/styles.css",
"baseColor": "mist",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "~/components",
"utils": "~/lib/utils",
"ui": "~/components/ui",
"lib": "~/lib",
"hooks": "~/hooks"
},
"menuColor": "default-translucent",
"menuAccent": "subtle",
"registries": {}
}

73
devfile.yaml Normal file
View File

@@ -0,0 +1,73 @@
commands:
- exec:
commandLine: npm install
component: runtime
group:
isDefault: true
kind: build
workingDir: ${PROJECT_SOURCE}
id: install
- exec:
commandLine: npm start
component: runtime
group:
isDefault: true
kind: run
workingDir: ${PROJECT_SOURCE}
id: run
- exec:
commandLine: npm run debug
component: runtime
group:
isDefault: true
kind: debug
workingDir: ${PROJECT_SOURCE}
id: debug
- exec:
commandLine: npm test
component: runtime
group:
isDefault: true
kind: test
workingDir: ${PROJECT_SOURCE}
id: test
components:
- container:
args:
- tail
- -f
- /dev/null
endpoints:
- name: https-node
protocol: https
targetPort: 3000
- exposure: none
name: debug
targetPort: 5858
env:
- name: DEBUG_PORT
value: "5858"
image: registry.access.redhat.com/ubi8/nodejs-18:1-32
memoryLimit: 1024Mi
mountSources: true
name: runtime
metadata:
description: Node.js 18 application
displayName: Node.js Runtime
icon: https://raw.githubusercontent.com/devfile-samples/devfile-stack-icons/main/node-js.svg
language: JavaScript
name: solid-demo
projectType: Node.js
tags:
- Node.js
- Express
- ubi8
version: 2.2.1
schemaVersion: 2.2.2
starterProjects:
- git:
checkoutFrom:
revision: main
remotes:
origin: https://github.com/nodeshift-starters/devfile-sample.git
name: nodejs-starter

57
package.json Normal file
View File

@@ -0,0 +1,57 @@
{
"name": "solid-demo",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev --port 3000",
"build": "vite build",
"start": "node .output/server/index.mjs",
"preview": "vite preview",
"test": "vitest run",
"format": "biome format",
"lint": "biome lint",
"check": "biome check"
},
"dependencies": {
"@fontsource-variable/noto-sans": "^5.2.10",
"@fontsource/inter": "^5.1.1",
"@kobalte/core": "^0.13.11",
"@sentry/solid": "^10.42.0",
"@solid-primitives/refs": "^1.1.3",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/router-plugin": "^1.133.21",
"@tanstack/solid-form": "latest",
"@tanstack/solid-query": "latest",
"@tanstack/solid-query-devtools": "latest",
"@tanstack/solid-router": "latest",
"@tanstack/solid-router-devtools": "latest",
"@tanstack/solid-router-ssr-query": "latest",
"@tanstack/solid-start": "latest",
"@tanstack/solid-store": "latest",
"@tanstack/store": "latest",
"chart.js": "^4.5.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.16.0",
"nitro": "npm:nitro-nightly@latest",
"radix-ui": "^1.4.3",
"shadcn": "^4.7.0",
"solid-js": "^1.9.12",
"srvx": "^0.11.16",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.1.18",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.4.0",
"zod": "^4.4.3"
},
"devDependencies": {
"@biomejs/biome": "2.4.5",
"@solidjs/testing-library": "^0.8.10",
"@tanstack/devtools-vite": "latest",
"jsdom": "^28.1.0",
"typescript": "^6.0.2",
"vite": "^8.0.0",
"vite-plugin-solid": "^2.11.12",
"vitest": "^4.1.5"
}
}

7925
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

5
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,5 @@
allowBuilds:
msw: true
onlyBuiltDependencies:
- esbuild
- lightningcss

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View File

@@ -0,0 +1,25 @@
{
"short_name": "TanStack App",
"name": "Create TanStack App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

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;
}
}

47
tailwind.config.js Normal file
View File

@@ -0,0 +1,47 @@
/**@type {import("tailwindcss").Config} */
module.exports = {
darkMode: ["variant", [".dark &", '[data-kb-theme="dark"] &']],
content: ["./src/**/*.{ts,tsx}"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px"
}
},
extpend: {
borderRadius: {
xl: "calc(var(--radius) + 4px)",
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)"
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--kb-accordion-content-height)" }
},
"accordion-up": {
from: { height: "var(--kb-accordion-content-height)" },
to: { height: 0 }
},
"content-show": {
from: { opacity: 0, transform: "scale(0.96)" },
to: { opacity: 1, transform: "scale(1)" }
},
"content-hide": {
from: { opacity: 1, transform: "scale(1)" },
to: { opacity: 0, transform: "scale(0.96)" }
}
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"content-show": "content-show 0.2s ease-out",
"content-hide": "content-hide 0.2s ease-out"
}
}
},
plugins: [require("tailwindcss-animate")]
}

29
tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
"target": "ES2022",
"jsx": "preserve",
"jsxImportSource": "solid-js",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
/* Linting */
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"paths": {
"~/*": ["./src/*"]
}
}
}

13
ui.config.json Normal file
View File

@@ -0,0 +1,13 @@
{
"$schema": "https://solid-ui.com/schema.json",
"tsx": true,
"tailwind": {
"css": "src/styles.css",
"config": "tailwind.config.cjs",
"prefix": ""
},
"aliases": {
"components": "~/components/ui",
"utils": "~/lib/utils"
}
}

22
vite.config.ts Normal file
View File

@@ -0,0 +1,22 @@
import { defineConfig } from 'vite'
import { devtools } from '@tanstack/devtools-vite'
import tailwindcss from '@tailwindcss/vite'
import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
import solidPlugin from 'vite-plugin-solid'
import { nitro } from 'nitro/vite'
export default defineConfig({
server: {
host: '127.0.0.1',
},
resolve: { tsconfigPaths: true },
plugins: [
devtools(),
nitro(),
tailwindcss(),
tanstackStart(),
solidPlugin({ ssr: true }),
],
})