first commit

This commit is contained in:
Antoine Pelletier 2026-07-29 16:47:05 +02:00
commit 572489ed5c
176 changed files with 7171 additions and 0 deletions

2
.env Normal file
View file

@ -0,0 +1,2 @@
# This file is used by dbmate, and by the sqlx macros at compile time.
DATABASE_URL=postgres://postgres:postgres@localhost:5432/app_template?sslmode=disable

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
config.yml
config.yaml

2734
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

30
Cargo.toml Normal file
View file

@ -0,0 +1,30 @@
[package]
name = "app-template"
version = "0.1.0"
edition = "2024"
[dependencies]
aide = { version = "0.15.1", features = [
"axum-json",
"axum-query",
"http",
"macros",
"scalar",
] }
async-trait = "0.1.89"
axum = { version = "0.8.9", features = ["macros"] }
config = "0.15.23"
schemars = "0.9.0"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
sqlx = { version = "0.8.6", features = [
"postgres",
"chrono",
"json",
"runtime-tokio",
] }
thiserror = "2.0.18"
tokio = { version = "1.52.3", features = ["rt-multi-thread", "signal"] }
tower-http = { version = "0.6.10", features = ["fs"] }
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }

156
README.md Normal file
View file

@ -0,0 +1,156 @@
# App template
Starting point for an AGEPoly web project: a **Rust** backend (axum + sqlx + aide) serving a
**Vue 3** frontend (TypeScript + vue-query + shadcn-vue), on **PostgreSQL** with **dbmate**
migrations.
The template is deliberately almost empty: one table (`items`), one route
(`GET /api/items`) and one page (home) that displays it. They exist to show how the layers
fit together — rename them, or delete them once your own code is in place.
## What you get
- Layered backend (api / core / services), see [Backend](#backend)
- An OpenAPI document generated from the code, and **TypeScript types generated from it**:
the frontend cannot call an endpoint that does not exist
- i18n (fr/en) on both sides, localized strings stored as JSONB
- `dev-db/`: postgres + adminer in docker for development
- Tailwind 4 and the shadcn-vue components already vendored in `frontend/src/components/ui`
- A single binary in production: the backend serves the built frontend
There is **no authentication** in this template: add whatever the project needs.
## Bootstrap a new project
```bash
cp -r template /path/to/my-project && cd /path/to/my-project
git init
# 1. Rename the crate and the database (app-template -> my-project)
./rename.sh my-project "My Project" && rm rename.sh
# 2. Start the development database
cd dev-db && docker compose up -d && cd ..
psql -h localhost -U postgres -c 'CREATE DATABASE my_project'
dbmate up
psql "$(grep DATABASE_URL .env | cut -d= -f2-)" -f db/seed.sql # optional demo data
# 3. Configure the app
cp config.example.yml config.yml
# 4. Run the backend (port 3000)
cargo run
# 5. Run the frontend (port 5000, proxies /api to the backend)
cd frontend && npm install && npm run dev
```
Open http://localhost:5000. The api documentation is on http://localhost:3000/api/docs.
`config.yml` is gitignored: it is where the secrets go. `config.example.yml` documents every
key, keep it up to date. Every value can also come from the environment
(`APP__POSTGRES__PASSWORD=...`), which is how the app is configured in production.
## Layout
```
├── src/ backend
│ ├── api/ http layer: routes, extractors, OpenAPI
│ ├── core/ business logic, independent of axum and sqlx
│ │ ├── controller/ what the app can do
│ │ ├── models/ domain types
│ │ └── repositories/ traits describing what the core needs from the storage
│ ├── services/ implementations of the repositories (postgres/sqlx)
│ └── utils/ configuration
├── db/
│ ├── migrations/ dbmate migrations
│ ├── schema.sql dump regenerated by dbmate, do not edit by hand
│ └── seed.sql development data
├── dev-db/ postgres + adminer for development
└── frontend/
└── src/
├── components/ shared components (ui/ = shadcn-vue)
├── views/ one component per route
├── router/ route table
├── services/ api/ (one file per domain area) + i18n
├── lib/api.d.ts generated from the backend, never edited by hand
├── utils/types.ts shorthands over the generated schemas
└── locales/ fr.yml / en.yml
```
## Backend
Requests flow through three layers, each one only knowing the next:
```
api (axum handler) -> core/controller -> core/repositories -> services/database
status codes business logic trait sqlx + postgres
domain models
```
The point of the repository traits is that the core never depends on sqlx: you can add
another implementation (a mock in tests, another storage) without touching the logic.
Handlers stay thin — extract the controller, call it, map the error to a status code — and
their OpenAPI documentation sits right next to them (`fn *_docs`).
### Adding an entity
1. `dbmate n create_things` and write the migration, then `dbmate up`
2. `src/core/models/thing.rs`: the domain types
3. `src/core/repositories/things_repository.rs`: the trait, added to `DatabaseRepository`
4. `src/services/database/things.rs`: the sqlx implementation
5. `src/core/controller/things.rs`: the logic, plus a `ThingsControllerError` if needed
6. `src/api/things.rs`: the handlers and their docs, mounted in `src/api/mod.rs`
7. `cd frontend && npm run openapi` to regenerate the types, then write the service and the view
The `Item` entity follows exactly these steps: copy it.
Request bodies are best kept separate from the domain models (an `api/models.rs` holding
the `...Api` structs and their `Into<Domain>` impls), so that the public contract does not
change every time a domain model does.
### sqlx and compilation
`query!`/`query_as!` check the sql against a **real database at compile time**, so the
development database must be up and migrated for `cargo build` to work. `DATABASE_URL` is
read from `.env`.
To build without a database (CI, docker image), commit the offline data:
```bash
cargo install sqlx-cli
cargo sqlx prepare # writes .sqlx/, commit it
```
### Migrations
```bash
dbmate n add_something # creates db/migrations/<timestamp>_add_something.sql
dbmate up # applies, and regenerates db/schema.sql
dbmate rollback # undoes the last migration (write your `migrate:down`!)
```
## Frontend
```bash
npm run dev # dev server on :5000, /api proxied to the backend on :3000
npm run build # type-check + build into dist/
npm run type-check
npm run lint
npm run format
npm run openapi # regenerate src/lib/api.d.ts from the running backend
```
Rules of thumb:
- views never call `fetch`: they use a hook from `services/api/`, which returns a vue-query
query or mutation. Caching, loading and error states come for free.
- mutations update the cache in `onSuccess` so the ui reacts immediately.
- texts live in `locales/*.yml` and are used through `$t('key')`, never hardcoded.
- add a shadcn-vue component with `npx shadcn-vue@latest add <name>`.
## Production
`cargo build --release`, `npm run build`, then point `frontend_dir` at the built
`frontend/dist`: the backend serves the static files and falls back on `index.html` so the
vue router keeps working on a page reload. Only one process to deploy.

20
build.rs Normal file
View file

@ -0,0 +1,20 @@
use std::process::Command;
/// Exposes the current commit and the crate name to the code through `env!()`
fn main() {
let git_hash = Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|output| String::from_utf8(output.stdout).ok())
.map(|hash| hash.trim().chars().take(8).collect::<String>())
.unwrap_or_else(|| "unknown".to_owned());
println!("cargo:rustc-env=GIT_HASH={git_hash}");
println!(
"cargo:rustc-env=CRATE_NAME={}",
env!("CARGO_PKG_NAME").replace("-", "_")
);
println!("cargo:rerun-if-changed=.git/HEAD");
}

15
config.example.yml Normal file
View file

@ -0,0 +1,15 @@
# Copy to config.yml (gitignored) and adapt.
# Every value can also be given as an environment variable, e.g. APP__SERVER__PORT=3000
server:
address: 0.0.0.0
port: 3000
postgres:
host: localhost
port: 5432
user: postgres
password: postgres
name: app_template
# Directory containing the built frontend (frontend/dist after `npm run build`)
frontend_dir: frontend/dist

View file

@ -0,0 +1,10 @@
-- migrate:up
-- Example table. Localized texts are stored as JSONB: {"fr": "...", "en": "..."}
CREATE TABLE items (
id SERIAL PRIMARY KEY,
"name" JSONB NOT NULL,
"description" JSONB NOT NULL
);
-- migrate:down
DROP TABLE IF EXISTS items;

97
db/schema.sql Normal file
View file

@ -0,0 +1,97 @@
\restrict dbmate
-- Dumped from database version 18.4
-- Dumped by pg_dump version 18.4
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET transaction_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- Name: items; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.items (
id integer NOT NULL,
name jsonb NOT NULL,
description jsonb NOT NULL
);
--
-- Name: items_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.items_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: items_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.items_id_seq OWNED BY public.items.id;
--
-- Name: schema_migrations; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.schema_migrations (
version character varying NOT NULL
);
--
-- Name: items id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.items ALTER COLUMN id SET DEFAULT nextval('public.items_id_seq'::regclass);
--
-- Name: items items_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.items
ADD CONSTRAINT items_pkey PRIMARY KEY (id);
--
-- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.schema_migrations
ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);
--
-- PostgreSQL database dump complete
--
\unrestrict dbmate
--
-- Dbmate schema migrations
--
INSERT INTO public.schema_migrations (version) VALUES
('20260101000000');

13
db/seed.sql Normal file
View file

@ -0,0 +1,13 @@
-- Development data. Apply with:
-- psql "$DATABASE_URL" -f db/seed.sql
BEGIN;
INSERT INTO public.items (id, "name", "description") VALUES
(1, '{"fr": "Premier objet", "en": "First item"}', '{"fr": "Un objet de démonstration.", "en": "A demo item."}'),
(2, '{"fr": "Deuxième objet", "en": "Second item"}', '{"fr": "Un autre objet de démonstration.", "en": "Another demo item."}')
ON CONFLICT DO NOTHING;
-- Keep the sequence in sync with the explicit ids inserted above
SELECT setval('public.items_id_seq', (SELECT COALESCE(MAX(id), 1) FROM public.items));
COMMIT;

1
dev-db/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
db

25
dev-db/docker-compose.yml Normal file
View file

@ -0,0 +1,25 @@
# This compose file should only be used in development.
# Caution: the ports of db and adminer are open without any application-side protection
services:
db:
image: postgres:18.3-alpine
ports: ["5432:5432"]
environment:
- POSTGRES_PASSWORD=postgres
volumes:
- ./db:/var/lib/postgresql
adminer:
image: adminer
ports: ["8080:8080"]
environment:
- ADMINER_SERVERS={
"dev db":{
"driver":"pgsql",
"server":"db",
"username":"postgres",
"password":"postgres",
"db":""
}
}
volumes:
- ./login-servers.php:/var/www/html/plugins-enabled/login-servers.php

52
dev-db/login-servers.php Normal file
View file

@ -0,0 +1,52 @@
<?php
/**
* Display servers list from defined ADMINER_SERVERS variable.
* @link https://www.adminer.org/plugins/#use
* @author https://github.com/garis-space
*/
class AdminerLoginServers {
/**
* Set servers from environment variable
* Example:
* $_ENV['ADMINER_SERVERS'] = '{
* "Server 1":{"driver":"pgsql","server":"","username":"","password":"","db":""},
* "Server 2":{"driver":"pgsql","server":"","username":"","password":"","db":""}
* }';
*/
function __construct() {
$this->servers = array();
if (!empty($_ENV['ADMINER_SERVERS'])) {
$this->servers = json_decode($_ENV['ADMINER_SERVERS'], true);
}
if (!empty($_POST["auth"]["custom_server"])) {
$key = $_POST["auth"]["custom_server"];
$_POST["auth"]["driver"] = $this->servers[$key]["driver"];
$_POST["auth"]["server"] = $this->servers[$key]["server"];
$_POST["auth"]["username"] = $this->servers[$key]["username"];
$_POST["auth"]["password"] = $this->servers[$key]["password"];
$_POST["auth"]["db"] = $this->servers[$key]["db"];
}
}
function loginFormField($name, $heading, $value) {
if ($name == 'driver') {
return '<tr><th>Driver<td>' . $value;
} elseif ($name == 'server') {
return '<tr><th>Host<td>' . $value;
} elseif ($name == 'db' && $_ENV['ADMINER_SERVERS'] != '') {
$out = $heading . $value;
$out .= '<tr><th><td>or';
$out .= '<tr><th>Server<td><select name="auth[custom_server]">';
$out .= '<option value="" selected>--</option>';
foreach ($this->servers as $serverName => $serverConfig) {
$out .= '<option value="' . htmlspecialchars($serverName) . '">' . htmlspecialchars($serverName) . '</option>';
}
$out .= '</select>';
return $out;
}
}
}
return new AdminerLoginServers();

8
frontend/.editorconfig Normal file
View file

@ -0,0 +1,8 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

1
frontend/.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto eol=lf

30
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View file

@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

21
frontend/components.json Normal file
View file

@ -0,0 +1,21 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "default",
"typescript": true,
"tailwind": {
"config": "",
"css": "src/styles/tailwind.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"composables": "@/composables"
},
"registries": {}
}

1
frontend/env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

32
frontend/eslint.config.ts Normal file
View file

@ -0,0 +1,32 @@
import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
skipFormatting,
{
// Must come last to win over `flat/essential`.
// shadcn-vue components (Button, Card, ...) are single word by design.
name: 'app/rules',
files: ['**/*.{ts,mts,tsx,vue}'],
rules: {
'vue/multi-word-component-names': 'off',
},
},
)

18
frontend/index.html Normal file
View file

@ -0,0 +1,18 @@
<!doctype html>
<html lang="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/src/styles/tailwind.css" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.3.2/css/flag-icons.min.css"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>App template</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

58
frontend/package.json Normal file
View file

@ -0,0 +1,58 @@
{
"name": "app-template-frontend",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"lint": "eslint . --fix",
"format": "prettier --write src/",
"openapi": "openapi-typescript http://localhost:3000/api/docs/private/api.json -o ./src/lib/api.d.ts"
},
"dependencies": {
"@lucide/vue": "^1.26.0",
"@tailwindcss/vite": "^4.3.0",
"@tanstack/vue-query": "^5.100.10",
"@tanstack/vue-query-devtools": "^6.1.29",
"@vueuse/core": "^14.3.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"http-status-ts": "^2.0.1",
"openapi-fetch": "^0.17.0",
"postcss": "^8.5.14",
"reka-ui": "^2.10.1",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.0",
"tw-animate-css": "^1.4.0",
"vue": "^3.5.18",
"vue-i18n": "^11.4.2",
"vue-router": "^5.0.7",
"vue-sonner": "^2.0.9"
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^11.2.1",
"@tsconfig/node22": "^22.0.5",
"@types/node": "^25.8.0",
"@vitejs/plugin-vue": "^6.0.7",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.7.0",
"@vue/tsconfig": "^0.9.1",
"eslint": "^10.4.0",
"eslint-plugin-vue": "~10.9.1",
"jiti": "^2.7.0",
"npm-run-all2": "^8.0.4",
"openapi-typescript": "^7.13.0",
"prettier": "3.8.3",
"typescript": "~5.9.3",
"vite": "^8.0.13",
"vite-plugin-vue-devtools": "^8.1.2",
"vue-tsc": "^3.2.9"
}
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

16
frontend/src/App.vue Normal file
View file

@ -0,0 +1,16 @@
<script setup lang="ts">
import { VueQueryDevtools } from '@tanstack/vue-query-devtools'
import { SidebarProvider } from '@/components/ui/sidebar'
import { Toaster } from '@/components/ui/sonner'
import AppSidebar from './components/AppSidebar.vue'
import AppContent from './components/AppContent.vue'
</script>
<template>
<SidebarProvider>
<AppSidebar />
<AppContent />
</SidebarProvider>
<VueQueryDevtools />
<Toaster position="top-center" />
</template>

View file

@ -0,0 +1,11 @@
<script setup lang="ts">
import { useSidebar } from './ui/sidebar'
const { setOpen } = useSidebar()
</script>
<template>
<main @click="setOpen(false)" class="py-5 px-7 min-h-full w-full">
<RouterView />
</main>
</template>

View file

@ -0,0 +1,105 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { Home } from '@lucide/vue'
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from '@/components/ui/sidebar'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { locales, setLocale } from '@/services/i18n'
const { t, locale: currentLocale } = useI18n()
const router = useRouter()
const { isMobile, open, setOpen } = useSidebar()
// Add your routes here
const menu = [
{
title: t('sidebar.navigation'),
content: [{ title: t('sidebar.home'), icon: Home, name: 'home' }],
},
]
const selectedLangFlag = computed(
() => locales.find((l) => l.lang === currentLocale.value)?.flag ?? locales[0].flag,
)
function navigate(name: string) {
setOpen(false)
router.push({ name })
}
</script>
<template>
<Sidebar @click="setOpen(true)" collapsible="icon">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" @click.stop="navigate('home')">
<div
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
>
<Home class="size-4" />
</div>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ $t('app.title') }}</span>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger class="w-fit" as-child>
<SidebarMenuButton @click.stop class="w-[3em] justify-center">
<span :class="'fi fi-' + selectedLangFlag"></span>
</SidebarMenuButton>
<DropdownMenuContent class="w-[3em]" :side="isMobile || open ? 'bottom' : 'right'">
<DropdownMenuItem
v-for="locale in locales"
:key="locale.lang"
@click="setLocale(locale.lang)"
class="justify-center"
>
<span :class="'fi fi-' + locale.flag + ' w-fit'"></span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenuTrigger>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup v-for="menuGroup in menu" :key="menuGroup.title">
<SidebarGroupLabel>{{ menuGroup.title }}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem v-for="menuEntry in menuGroup.content" :key="menuEntry.title">
<SidebarMenuButton asChild>
<a @click.stop="navigate(menuEntry.name)" class="cursor-pointer">
<component :is="menuEntry.icon" />
<span>{{ menuEntry.title }}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
</template>

View file

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { AccordionRootEmits, AccordionRootProps } from 'reka-ui'
import { AccordionRoot, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<AccordionRootProps>()
const emits = defineEmits<AccordionRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<AccordionRoot v-bind="forwarded">
<slot />
</AccordionRoot>
</template>

View file

@ -0,0 +1,22 @@
<script setup lang="ts">
import type { AccordionContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AccordionContent } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<AccordionContent
v-bind="delegatedProps"
class="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
>
<div :class="cn('pb-4 pt-0', props.class)">
<slot />
</div>
</AccordionContent>
</template>

View file

@ -0,0 +1,19 @@
<script setup lang="ts">
import type { AccordionItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AccordionItem, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<AccordionItem v-bind="forwardedProps" :class="cn('border-b', props.class)">
<slot />
</AccordionItem>
</template>

View file

@ -0,0 +1,31 @@
<script setup lang="ts">
import type { AccordionTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ChevronDown } from '@lucide/vue'
import { AccordionHeader, AccordionTrigger } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<AccordionHeader class="flex">
<AccordionTrigger
v-bind="delegatedProps"
:class="
cn(
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
props.class,
)
"
>
<slot />
<slot name="icon">
<ChevronDown class="h-4 w-4 shrink-0 transition-transform duration-200" />
</slot>
</AccordionTrigger>
</AccordionHeader>
</template>

View file

@ -0,0 +1,4 @@
export { default as Accordion } from './Accordion.vue'
export { default as AccordionContent } from './AccordionContent.vue'
export { default as AccordionItem } from './AccordionItem.vue'
export { default as AccordionTrigger } from './AccordionTrigger.vue'

View file

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { AlertDialogEmits, AlertDialogProps } from 'reka-ui'
import { AlertDialogRoot, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<AlertDialogProps>()
const emits = defineEmits<AlertDialogEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<AlertDialogRoot v-bind="forwarded">
<slot />
</AlertDialogRoot>
</template>

View file

@ -0,0 +1,18 @@
<script setup lang="ts">
import type { AlertDialogActionProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AlertDialogAction } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<AlertDialogAction v-bind="delegatedProps" :class="cn(buttonVariants(), props.class)">
<slot />
</AlertDialogAction>
</template>

View file

@ -0,0 +1,21 @@
<script setup lang="ts">
import type { AlertDialogCancelProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AlertDialogCancel } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
const props = defineProps<AlertDialogCancelProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<AlertDialogCancel
v-bind="delegatedProps"
:class="cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', props.class)"
>
<slot />
</AlertDialogCancel>
</template>

View file

@ -0,0 +1,38 @@
<script setup lang="ts">
import type { AlertDialogContentEmits, AlertDialogContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import {
AlertDialogContent,
AlertDialogOverlay,
AlertDialogPortal,
useForwardPropsEmits,
} from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<AlertDialogContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<AlertDialogContentEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<AlertDialogPortal>
<AlertDialogOverlay
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<AlertDialogContent
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg',
props.class,
)
"
>
<slot />
</AlertDialogContent>
</AlertDialogPortal>
</template>

View file

@ -0,0 +1,20 @@
<script setup lang="ts">
import type { AlertDialogDescriptionProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AlertDialogDescription } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<AlertDialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<AlertDialogDescription
v-bind="delegatedProps"
:class="cn('text-sm text-muted-foreground', props.class)"
>
<slot />
</AlertDialogDescription>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2', props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('flex flex-col gap-y-2 text-center sm:text-left', props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { AlertDialogTitleProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AlertDialogTitle } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<AlertDialogTitleProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<AlertDialogTitle v-bind="delegatedProps" :class="cn('text-lg font-semibold', props.class)">
<slot />
</AlertDialogTitle>
</template>

View file

@ -0,0 +1,12 @@
<script setup lang="ts">
import type { AlertDialogTriggerProps } from 'reka-ui'
import { AlertDialogTrigger } from 'reka-ui'
const props = defineProps<AlertDialogTriggerProps>()
</script>
<template>
<AlertDialogTrigger v-bind="props">
<slot />
</AlertDialogTrigger>
</template>

View file

@ -0,0 +1,9 @@
export { default as AlertDialog } from './AlertDialog.vue'
export { default as AlertDialogAction } from './AlertDialogAction.vue'
export { default as AlertDialogCancel } from './AlertDialogCancel.vue'
export { default as AlertDialogContent } from './AlertDialogContent.vue'
export { default as AlertDialogDescription } from './AlertDialogDescription.vue'
export { default as AlertDialogFooter } from './AlertDialogFooter.vue'
export { default as AlertDialogHeader } from './AlertDialogHeader.vue'
export { default as AlertDialogTitle } from './AlertDialogTitle.vue'
export { default as AlertDialogTrigger } from './AlertDialogTrigger.vue'

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { BadgeVariants } from '.'
import { cn } from '@/lib/utils'
import { badgeVariants } from '.'
const props = defineProps<{
variant?: BadgeVariants['variant']
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn(badgeVariants({ variant }), props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,25 @@
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
export { default as Badge } from './Badge.vue'
export const badgeVariants = cva(
'inline-flex gap-1 items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
)
export type BadgeVariants = VariantProps<typeof badgeVariants>

View file

@ -0,0 +1,29 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { ButtonVariants } from '.'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '.'
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
})
</script>
<template>
<Primitive
data-slot="button"
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>

View file

@ -0,0 +1,36 @@
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
export { default as Button } from './Button.vue'
export const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive:
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
export type ButtonVariants = VariantProps<typeof buttonVariants>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('rounded-lg border bg-card text-card-foreground shadow-sm', props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('p-6 pt-0', props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<p :class="cn('text-sm text-muted-foreground', props.class)">
<slot />
</p>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('flex items-center p-6 pt-0', props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<h3 :class="cn('text-2xl font-semibold leading-none tracking-tight', props.class)">
<slot />
</h3>
</template>

View file

@ -0,0 +1,6 @@
export { default as Card } from './Card.vue'
export { default as CardContent } from './CardContent.vue'
export { default as CardDescription } from './CardDescription.vue'
export { default as CardFooter } from './CardFooter.vue'
export { default as CardHeader } from './CardHeader.vue'
export { default as CardTitle } from './CardTitle.vue'

View file

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { ComboboxRootEmits, ComboboxRootProps } from 'reka-ui'
import { ComboboxRoot, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<ComboboxRootProps>()
const emits = defineEmits<ComboboxRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<ComboboxRoot v-bind="forwarded">
<slot />
</ComboboxRoot>
</template>

View file

@ -0,0 +1,19 @@
<script setup lang="ts">
import type { ComboboxAnchorProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ComboboxAnchor, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<ComboboxAnchorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<ComboboxAnchor v-bind="forwarded" :class="cn('w-[200px]', props.class)">
<slot />
</ComboboxAnchor>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { ComboboxEmptyProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ComboboxEmpty } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<ComboboxEmptyProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<ComboboxEmpty v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)">
<slot />
</ComboboxEmpty>
</template>

View file

@ -0,0 +1,33 @@
<script setup lang="ts">
import type { ComboboxGroupProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ComboboxGroup, ComboboxLabel } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<
ComboboxGroupProps & {
class?: HTMLAttributes['class']
heading?: string
}
>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<ComboboxGroup
v-bind="delegatedProps"
:class="
cn(
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
props.class,
)
"
>
<ComboboxLabel v-if="heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
{{ heading }}
</ComboboxLabel>
<slot />
</ComboboxGroup>
</template>

View file

@ -0,0 +1,33 @@
<script setup lang="ts">
import type { ComboboxInputEmits, ComboboxInputProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ComboboxInput, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<
ComboboxInputProps & {
class?: HTMLAttributes['class']
}
>()
const emits = defineEmits<ComboboxInputEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<ComboboxInput
v-bind="forwarded"
:class="
cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
props.class,
)
"
>
<slot />
</ComboboxInput>
</template>

View file

@ -0,0 +1,28 @@
<script setup lang="ts">
import type { ComboboxItemEmits, ComboboxItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ComboboxItem, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<ComboboxItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<ComboboxItemEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<ComboboxItem
v-bind="forwarded"
:class="
cn(
'relative flex cursor-default gap-2 select-none justify-between items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0',
props.class,
)
"
>
<slot />
</ComboboxItem>
</template>

View file

@ -0,0 +1,39 @@
<script setup lang="ts">
import type { ComboboxContentEmits, ComboboxContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ComboboxContent, ComboboxPortal, ComboboxViewport, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<ComboboxContentProps & { class?: HTMLAttributes['class'] }>(),
{
position: 'popper',
align: 'center',
sideOffset: 4,
},
)
const emits = defineEmits<ComboboxContentEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<ComboboxPortal>
<ComboboxContent
v-bind="forwarded"
:class="
cn(
'z-50 w-[200px] rounded-md border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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',
props.class,
)
"
>
<ComboboxViewport>
<slot />
</ComboboxViewport>
</ComboboxContent>
</ComboboxPortal>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { ComboboxSeparatorProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ComboboxSeparator } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<ComboboxSeparatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<ComboboxSeparator v-bind="delegatedProps" :class="cn('-mx-1 h-px bg-border', props.class)">
<slot />
</ComboboxSeparator>
</template>

View file

@ -0,0 +1,19 @@
<script setup lang="ts">
import type { ComboboxTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ComboboxTrigger, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<ComboboxTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<ComboboxTrigger v-bind="forwarded" :class="cn('', props.class)" tabindex="0">
<slot />
</ComboboxTrigger>
</template>

View file

@ -0,0 +1,11 @@
export { default as Combobox } from './Combobox.vue'
export { default as ComboboxAnchor } from './ComboboxAnchor.vue'
export { default as ComboboxEmpty } from './ComboboxEmpty.vue'
export { default as ComboboxGroup } from './ComboboxGroup.vue'
export { default as ComboboxInput } from './ComboboxInput.vue'
export { default as ComboboxItem } from './ComboboxItem.vue'
export { default as ComboboxList } from './ComboboxList.vue'
export { default as ComboboxSeparator } from './ComboboxSeparator.vue'
export { default as ComboboxTrigger } from './ComboboxTrigger.vue'
export { ComboboxCancel, ComboboxItemIndicator } from 'reka-ui'

View file

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DropdownMenuRootEmits, DropdownMenuRootProps } from 'reka-ui'
import { DropdownMenuRoot, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<DropdownMenuRootProps>()
const emits = defineEmits<DropdownMenuRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuRoot data-slot="dropdown-menu" v-bind="forwarded">
<slot />
</DropdownMenuRoot>
</template>

View file

@ -0,0 +1,35 @@
<script setup lang="ts">
import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { Check } from '@lucide/vue'
import { DropdownMenuCheckboxItem, DropdownMenuItemIndicator, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuCheckboxItem
data-slot="dropdown-menu-checkbox-item"
v-bind="forwarded"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)
"
>
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<Check class="size-4" />
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuCheckboxItem>
</template>

View file

@ -0,0 +1,36 @@
<script setup lang="ts">
import type { DropdownMenuContentEmits, DropdownMenuContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DropdownMenuContent, DropdownMenuPortal, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(),
{
sideOffset: 4,
},
)
const emits = defineEmits<DropdownMenuContentEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuPortal>
<DropdownMenuContent
data-slot="dropdown-menu-content"
v-bind="forwarded"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
props.class,
)
"
>
<slot />
</DropdownMenuContent>
</DropdownMenuPortal>
</template>

View file

@ -0,0 +1,12 @@
<script setup lang="ts">
import type { DropdownMenuGroupProps } from 'reka-ui'
import { DropdownMenuGroup } from 'reka-ui'
const props = defineProps<DropdownMenuGroupProps>()
</script>
<template>
<DropdownMenuGroup data-slot="dropdown-menu-group" v-bind="props">
<slot />
</DropdownMenuGroup>
</template>

View file

@ -0,0 +1,41 @@
<script setup lang="ts">
import type { DropdownMenuItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DropdownMenuItem, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<
DropdownMenuItemProps & {
class?: HTMLAttributes['class']
inset?: boolean
variant?: 'default' | 'destructive'
}
>(),
{
variant: 'default',
},
)
const delegatedProps = reactiveOmit(props, 'inset', 'variant', 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuItem
data-slot="dropdown-menu-item"
:data-inset="inset ? '' : undefined"
:data-variant="variant"
v-bind="forwardedProps"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)
"
>
<slot />
</DropdownMenuItem>
</template>

View file

@ -0,0 +1,25 @@
<script setup lang="ts">
import type { DropdownMenuLabelProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DropdownMenuLabel, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<
DropdownMenuLabelProps & { class?: HTMLAttributes['class']; inset?: boolean }
>()
const delegatedProps = reactiveOmit(props, 'class', 'inset')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuLabel
data-slot="dropdown-menu-label"
:data-inset="inset ? '' : undefined"
v-bind="forwardedProps"
:class="cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)"
>
<slot />
</DropdownMenuLabel>
</template>

View file

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DropdownMenuRadioGroupEmits, DropdownMenuRadioGroupProps } from 'reka-ui'
import { DropdownMenuRadioGroup, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<DropdownMenuRadioGroupProps>()
const emits = defineEmits<DropdownMenuRadioGroupEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuRadioGroup data-slot="dropdown-menu-radio-group" v-bind="forwarded">
<slot />
</DropdownMenuRadioGroup>
</template>

View file

@ -0,0 +1,36 @@
<script setup lang="ts">
import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { Circle } from '@lucide/vue'
import { DropdownMenuItemIndicator, DropdownMenuRadioItem, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DropdownMenuRadioItemEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuRadioItem
data-slot="dropdown-menu-radio-item"
v-bind="forwarded"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)
"
>
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<Circle class="size-2 fill-current" />
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuRadioItem>
</template>

View file

@ -0,0 +1,23 @@
<script setup lang="ts">
import type { DropdownMenuSeparatorProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DropdownMenuSeparator } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<
DropdownMenuSeparatorProps & {
class?: HTMLAttributes['class']
}
>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<DropdownMenuSeparator
data-slot="dropdown-menu-separator"
v-bind="delegatedProps"
:class="cn('bg-border -mx-1 my-1 h-px', props.class)"
/>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<span
data-slot="dropdown-menu-shortcut"
:class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
>
<slot />
</span>
</template>

View file

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DropdownMenuSubEmits, DropdownMenuSubProps } from 'reka-ui'
import { DropdownMenuSub, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<DropdownMenuSubProps>()
const emits = defineEmits<DropdownMenuSubEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuSub data-slot="dropdown-menu-sub" v-bind="forwarded">
<slot />
</DropdownMenuSub>
</template>

View file

@ -0,0 +1,29 @@
<script setup lang="ts">
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DropdownMenuSubContent, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DropdownMenuSubContentEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuSubContent
data-slot="dropdown-menu-sub-content"
v-bind="forwarded"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
props.class,
)
"
>
<slot />
</DropdownMenuSubContent>
</template>

View file

@ -0,0 +1,31 @@
<script setup lang="ts">
import type { DropdownMenuSubTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ChevronRight } from '@lucide/vue'
import { DropdownMenuSubTrigger, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<
DropdownMenuSubTriggerProps & { class?: HTMLAttributes['class']; inset?: boolean }
>()
const delegatedProps = reactiveOmit(props, 'class', 'inset')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuSubTrigger
data-slot="dropdown-menu-sub-trigger"
v-bind="forwardedProps"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
props.class,
)
"
>
<slot />
<ChevronRight class="ml-auto size-4" />
</DropdownMenuSubTrigger>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { DropdownMenuTriggerProps } from 'reka-ui'
import { DropdownMenuTrigger, useForwardProps } from 'reka-ui'
const props = defineProps<DropdownMenuTriggerProps>()
const forwardedProps = useForwardProps(props)
</script>
<template>
<DropdownMenuTrigger data-slot="dropdown-menu-trigger" v-bind="forwardedProps">
<slot />
</DropdownMenuTrigger>
</template>

View file

@ -0,0 +1,16 @@
export { default as DropdownMenu } from './DropdownMenu.vue'
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue'
export { default as DropdownMenuContent } from './DropdownMenuContent.vue'
export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue'
export { default as DropdownMenuItem } from './DropdownMenuItem.vue'
export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue'
export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue'
export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue'
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue'
export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue'
export { default as DropdownMenuSub } from './DropdownMenuSub.vue'
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue'
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue'
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue'
export { DropdownMenuPortal } from 'reka-ui'

View file

@ -0,0 +1,35 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { useVModel } from '@vueuse/core'
import { cn } from '@/lib/utils'
const props = defineProps<{
defaultValue?: string | number
modelValue?: string | number
class?: HTMLAttributes['class']
}>()
const emits = defineEmits<{
(e: 'update:modelValue', payload: string | number): void
}>()
const modelValue = useVModel(props, 'modelValue', emits, {
passive: true,
defaultValue: props.defaultValue,
})
</script>
<template>
<input
v-model="modelValue"
data-slot="input"
:class="
cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
props.class,
)
"
/>
</template>

View file

@ -0,0 +1 @@
export { default as Input } from './Input.vue'

View file

@ -0,0 +1,27 @@
<script setup lang="ts">
import type { SeparatorProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { Separator } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<SeparatorProps & { class?: HTMLAttributes['class'] }>(), {
orientation: 'horizontal',
decorative: true,
})
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<Separator
data-slot="separator-root"
v-bind="delegatedProps"
:class="
cn(
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
props.class,
)
"
/>
</template>

View file

@ -0,0 +1 @@
export { default as Separator } from './Separator.vue'

View file

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from 'reka-ui'
import { DialogRoot, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DialogRoot data-slot="sheet" v-bind="forwarded">
<slot />
</DialogRoot>
</template>

View file

@ -0,0 +1,12 @@
<script setup lang="ts">
import type { DialogCloseProps } from 'reka-ui'
import { DialogClose } from 'reka-ui'
const props = defineProps<DialogCloseProps>()
</script>
<template>
<DialogClose data-slot="sheet-close" v-bind="props">
<slot />
</DialogClose>
</template>

View file

@ -0,0 +1,60 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { X } from '@lucide/vue'
import { DialogClose, DialogContent, DialogPortal, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
import SheetOverlay from './SheetOverlay.vue'
interface SheetContentProps extends DialogContentProps {
class?: HTMLAttributes['class']
side?: 'top' | 'right' | 'bottom' | 'left'
}
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<SheetContentProps>(), {
side: 'right',
})
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, 'class', 'side')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<SheetOverlay />
<DialogContent
data-slot="sheet-content"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
side === 'right' &&
'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
side === 'left' &&
'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
side === 'top' &&
'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
side === 'bottom' &&
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
props.class,
)
"
v-bind="{ ...forwarded, ...$attrs }"
>
<slot />
<DialogClose
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
>
<X class="size-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>

View file

@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogDescriptionProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DialogDescription } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<DialogDescription
data-slot="sheet-description"
:class="cn('text-muted-foreground text-sm', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogDescription>
</template>

View file

@ -0,0 +1,12 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
</script>
<template>
<div data-slot="sheet-footer" :class="cn('mt-auto flex flex-col gap-2 p-4', props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,12 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
</script>
<template>
<div data-slot="sheet-header" :class="cn('flex flex-col gap-1.5 p-4', props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,26 @@
<script setup lang="ts">
import type { DialogOverlayProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DialogOverlay } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<DialogOverlay
data-slot="sheet-overlay"
:class="
cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
props.class,
)
"
v-bind="delegatedProps"
>
<slot />
</DialogOverlay>
</template>

View file

@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogTitleProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DialogTitle } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<DialogTitle
data-slot="sheet-title"
:class="cn('text-foreground font-semibold', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogTitle>
</template>

View file

@ -0,0 +1,12 @@
<script setup lang="ts">
import type { DialogTriggerProps } from 'reka-ui'
import { DialogTrigger } from 'reka-ui'
const props = defineProps<DialogTriggerProps>()
</script>
<template>
<DialogTrigger data-slot="sheet-trigger" v-bind="props">
<slot />
</DialogTrigger>
</template>

View file

@ -0,0 +1,8 @@
export { default as Sheet } from './Sheet.vue'
export { default as SheetClose } from './SheetClose.vue'
export { default as SheetContent } from './SheetContent.vue'
export { default as SheetDescription } from './SheetDescription.vue'
export { default as SheetFooter } from './SheetFooter.vue'
export { default as SheetHeader } from './SheetHeader.vue'
export { default as SheetTitle } from './SheetTitle.vue'
export { default as SheetTrigger } from './SheetTrigger.vue'

View file

@ -0,0 +1,102 @@
<script setup lang="ts">
import type { SidebarProps } from '.'
import { cn } from '@/lib/utils'
import { Sheet, SheetContent } from '@/components/ui/sheet'
import SheetDescription from '@/components/ui/sheet/SheetDescription.vue'
import SheetHeader from '@/components/ui/sheet/SheetHeader.vue'
import SheetTitle from '@/components/ui/sheet/SheetTitle.vue'
import { SIDEBAR_WIDTH_MOBILE, useSidebar } from './utils'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<SidebarProps>(), {
side: 'left',
variant: 'sidebar',
collapsible: 'offcanvas',
})
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
</script>
<template>
<div
v-if="collapsible === 'none'"
data-slot="sidebar"
:class="
cn('bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col', props.class)
"
v-bind="$attrs"
>
<slot />
</div>
<Sheet v-else-if="isMobile" :open="openMobile" v-bind="$attrs" @update:open="setOpenMobile">
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
:side="side"
class="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
:style="{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
}"
>
<SheetHeader class="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div class="flex h-full w-full flex-col">
<slot />
</div>
</SheetContent>
</Sheet>
<div
v-else
class="group peer text-sidebar-foreground hidden md:block"
data-slot="sidebar"
:data-state="state"
:data-collapsible="state === 'collapsed' ? collapsible : ''"
:data-variant="variant"
:data-side="side"
>
<!-- This is what handles the sidebar gap on desktop -->
<div
: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
:class="
cn(
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
side === 'left'
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
// 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',
props.class,
)
"
v-bind="$attrs"
>
<div
data-sidebar="sidebar"
class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
>
<slot />
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,23 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="sidebar-content"
data-sidebar="content"
:class="
cn(
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
props.class,
)
"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="sidebar-footer"
data-sidebar="footer"
:class="cn('flex flex-col gap-2 p-2', props.class)"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="sidebar-group"
data-sidebar="group"
:class="cn('relative flex w-full min-w-0 flex-col p-2', props.class)"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,31 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<
PrimitiveProps & {
class?: HTMLAttributes['class']
}
>()
</script>
<template>
<Primitive
data-slot="sidebar-group-action"
data-sidebar="group-action"
:as="as"
:as-child="asChild"
:class="
cn(
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
'after:absolute after:-inset-2 md:after:hidden',
'group-data-[collapsible=icon]:hidden',
props.class,
)
"
>
<slot />
</Primitive>
</template>

View file

@ -0,0 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
:class="cn('w-full text-sm', props.class)"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,30 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<
PrimitiveProps & {
class?: HTMLAttributes['class']
}
>()
</script>
<template>
<Primitive
data-slot="sidebar-group-label"
data-sidebar="group-label"
:as="as"
:as-child="asChild"
:class="
cn(
'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
props.class,
)
"
>
<slot />
</Primitive>
</template>

View file

@ -0,0 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="sidebar-header"
data-sidebar="header"
:class="cn('flex flex-col gap-2 p-2', props.class)"
>
<slot />
</div>
</template>

View file

@ -0,0 +1,19 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Input } from '@/components/ui/input'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<Input
data-slot="sidebar-input"
data-sidebar="input"
:class="cn('bg-background h-8 w-full shadow-none', props.class)"
>
<slot />
</Input>
</template>

Some files were not shown because too many files have changed in this diff Show more