Compare commits

...

47 Commits
v0.11.0 ... dev

Author SHA1 Message Date
N1kl8s
47f53cb985 Migrate gitlab to gitea 2024-12-28 22:39:40 +01:00
1e1f335853 README.md aktualisiert 2024-12-28 22:32:52 +01:00
32d6b8b397 README.md aktualisiert 2024-12-28 22:26:05 +01:00
Boris
3eaef4a748
docs: fix broken link in footer (#950)
* Fix 404 link in footer

* Change link

Co-authored-by: zernonia <59365435+zernonia@users.noreply.github.com>

---------

Co-authored-by: zernonia <59365435+zernonia@users.noreply.github.com>
2024-12-17 16:35:47 +08:00
slow-groovin
5b1f952e0a
docs: missing .value for combobox usage (#947) 2024-12-17 12:32:11 +08:00
zernonia
693b0d2a08 fix(Sidebar): component import and registry dependencies 2024-12-17 12:21:45 +08:00
Fabian Bernhart
8a24d11a65
fix: add missing useMediaQuery in SidebarProvider.vue new-york style (#927) 2024-12-04 22:36:12 +03:30
Leo Chiu
e4fea78b33
docs: update code highlight and step configurations in vite.md (#936) 2024-12-04 10:22:52 +03:30
Stephan Koglin-Fischer
5869165a84
docs: added missing import in auto-form example (#919) 2024-12-03 12:36:41 +03:30
Damien Roche
857f10de51
docs: update responsive breadcrumb example (#933) 2024-12-03 12:26:41 +03:30
Thomas La Salmonie
5ada562803
build(cli): update proxy agent using undici agent to match ofetch requirements (#934) 2024-12-03 12:25:37 +03:30
zernonia
6c0ab55e92 chore: release v0.11.3 2024-11-19 16:13:22 +08:00
Fatih Solhan
3ddd70dd6b
docs: prevent shrinking theme customizer circles (#914)
* fix: prevent shrinking theme customizer circles

* use `shrink-0` instead of deprecated `flex-shrink-0`
2024-11-19 14:51:56 +08:00
Whbbit1999
2d5ad5b962
fix: mobile sidebar component #840 (#884)
* fix: sidebar component mobile #840

* chore: update new-york style too

* chore: run registry build

* revert: change in templates.ts

---------

Co-authored-by: zernonia <zernonia@gmail.com>
2024-11-19 14:51:24 +08:00
Leo Chiu
16f1d19460
fix(CLI): temporarily downgrade package to 3.2.0 due to TSInterfaceHeritage issue (#913)
Co-authored-by: leochiu <leochiu@hahow.in>
2024-11-19 14:31:12 +08:00
Louis Van Aken
ac69980ac9
fix: Error in the build of the auto form with Vite and TypeScript #870 (#896)
* fix: Error in the build of the auto form with Vite and TypeScript #870

* fix: new york registry
2024-11-16 17:40:29 +08:00
Titus Kirch
58fc125974
docs: fix primitive link in data-table.md (#898) 2024-11-16 17:34:26 +08:00
sinsky
383c846c02
docs: input components use "default-value" instead of "value" (#904)
* docs(example): input components used in SheetDemo use default-value instead of value

* docs(example): input components used in DialogDemo use default-value instead of value
2024-11-16 17:32:00 +08:00
sea
b7ef4653f7
docs: update * scrollbar style (#885) 2024-11-11 12:24:52 +03:30
Brian Le
384c87a91c
fix: add missing classes to button (#877) 2024-11-10 08:20:18 +03:30
Tony Zhang
75a5bce92f
docs: fix invisible code highlight in light mode (#875)
Signed-off-by: ZTL-UwU <zhangtianli2006@163.com>
2024-11-09 19:23:43 +03:30
Rhanlin
3d0db2de7b
docs: fix dialog form example onsubmit (#873) 2024-11-09 08:52:17 +03:30
Damien Roche
a5b25a9c43
fix: switch from placeholder: to data-placeholder: prefix class for SelectValue (#867) 2024-11-09 08:33:01 +03:30
zernonia
be888c80b6 chore: run changelogithub first 2024-11-04 15:20:54 +08:00
zernonia
93be758257 fix: module build 2024-11-04 15:17:59 +08:00
zernonia
26df827d33 chore: release v0.11.2 2024-11-04 14:53:22 +08:00
zernonia
f267b0ba4a chore: improve cd 2024-11-04 14:52:23 +08:00
zernonia
3c506ef188 chore: release v0.11.1 2024-11-04 14:45:18 +08:00
zernonia
df60e15a97 chore: improve cd 2024-11-04 14:43:40 +08:00
Selemon Brahanu
3646203d4f
refactor(CLI): base dependencies not installing when using shadcn-nuxt (#821)
* refactor: 815 shadcn-nuxt dependencies and CLI init

Closes: 815

* chore: remove unnecessary dependencies

* chore: update pnpm-lock.yaml file

* chore: cleanup

* chore: update pnpm-lock

---------

Co-authored-by: zernonia <zernonia@gmail.com>
2024-11-04 14:20:14 +08:00
zernonia
68c40f6908 chore: codegen 2024-11-03 23:52:53 +08:00
Hookin.
ff1b5f0a1b
fix(Sidebar): use open.value when setting the cookie (#862) 2024-11-03 17:11:46 +08:00
苗大
15f3eb305b
fix: missing NavigationMenuViewport component (#860) 2024-11-02 23:38:57 +08:00
Rnz Brngn
baceb4ce91
docs(blocks): correct tooltip prop for dynamic binding (#850) 2024-11-02 23:37:12 +08:00
IllustrisJack
2cb767122a
fix(Nuxt): component override warnings (#838) 2024-11-02 23:33:09 +08:00
Maxim Kim
eff3e75466
fix(Sidebar): missing slot into component (#847)
* fix(SidebarMenuAction): add slot into component

* fix(SidebarMenuAction): add changes in new-york style
2024-11-02 23:31:39 +08:00
xiaomo
83419c4dc3
docs: replace unexported cssVariables with highlight (#841) 2024-10-29 08:33:28 +03:30
xiaomo
52b9b20b3c
docs: add theme to highlight code block (#831) 2024-10-28 15:32:30 +03:30
zernonia
e0d4980e31
feat: Sidebar (1/2) (#827)
* feat: add default sidebar

* chore: add images, docs item

* refactor: rename and fix styling`

* feat: add new-york style

* chore: move typescript to catalog

* docs: fix block preview

* chore: build registry, add sidebar block

* docs: update sidebar demo

* chore: bump radix-vue

* chore: fix build
2024-10-28 01:05:55 +08:00
Mohammed Essam Helewa
6760ebb5ae
feat: enhance the behavior of Switch component (#835)
* Add named slot inside SwitchThumb

* Add documentation for switch thumb slot

* chore: re-build registry
2024-10-26 21:52:03 +03:30
Ciro Lo Sapio
d143272fb8
docs: update footer links (#820) 2024-10-22 20:17:17 +03:30
Otabek
4f3bb61283
docs: update vite.md for latest Vite configuration in Vue projects (#817) 2024-10-22 20:09:47 +03:30
zernonia
f9615d3657 chore: use format=cover 2024-10-17 15:22:02 +02:00
sea
d48ffcfffb
chore(vscode): add tailwindcss extensions (#810)
* chore(vscode): add tailwindcss extensions

* chore: add tailwind setting
2024-10-17 09:16:39 +03:30
sea
e63d5553e3
docs: fix navigation menu example link (#809) 2024-10-16 12:22:53 +03:30
zernonia
a01a1bd94d docs: fix carbon 2024-10-16 14:11:36 +08:00
zernonia
f5b02256bc chore: add carbon ads 2024-10-15 22:24:13 +08:00
217 changed files with 5078 additions and 1549 deletions

View File

@ -14,3 +14,4 @@ runs:
with:
node-version: lts/*
cache: pnpm
registry-url: 'https://registry.npmjs.org'

View File

@ -21,6 +21,19 @@ jobs:
- name: Setup (Install Node & pnpm)
uses: ./.github/actions/setup
- name: Install dependencies
run: pnpm i --frozen-lockfile
- run: pnpm dlx changelogithub
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Build CLI & Publish to npm
run: pnpm --filter shadcn-vue pub:release
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Build Module & Publish to npm
run: pnpm --filter shadcn-nuxt pub:release
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -1,6 +1,7 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint"
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss"
]
}

View File

@ -34,5 +34,9 @@
"json",
"jsonc",
"yaml"
],
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cn\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}

View File

@ -1,7 +1,7 @@
<p align="center">
<img align="center" src="https://raw.githubusercontent.com/radix-vue/shadcn-vue/dev/apps/www/src/public/android-chrome-192x192.png" height="96" />
<h1 align="center">
shadcn-vue
shadcn-vue by Niklas Hermanns
</h1>
</p>
@ -31,3 +31,7 @@ All credits go to these open-source works and resources
## License
Licensed under the [MIT license](https://github.com/shadcn/ui/blob/main/LICENSE.md).
## Actions
- Test

View File

@ -4,7 +4,6 @@ import autoprefixer from 'autoprefixer'
import tailwind from 'tailwindcss'
import Icons from 'unplugin-icons/vite'
import { defineConfig } from 'vitepress'
import { cssVariables } from './theme/config/shiki'
import { siteConfig } from './theme/config/site'
import CodeWrapperPlugin from './theme/plugins/codewrapper'
@ -31,7 +30,6 @@ export default defineConfig({
['meta', { name: 'og:site_name', content: siteConfig.name }],
['meta', { name: 'og:image', content: siteConfig.ogImage }],
['meta', { name: 'twitter:image', content: siteConfig.ogImage }],
],
sitemap: {
@ -50,11 +48,14 @@ export default defineConfig({
pattern: 'https://github.com/radix-vue/shadcn-vue/tree/dev/apps/www/src/:path',
text: 'Edit this page on GitHub',
},
carbonAds: {
code: 'CW7DK27U',
placement: 'wwwshadcn-vuecom',
},
},
srcDir: path.resolve(__dirname, '../src'),
markdown: {
theme: cssVariables,
codeTransformers: [
transformerMetaWordHighlight(),
],

View File

@ -0,0 +1,233 @@
<script setup lang="ts">
import { useConfigStore } from '@/stores/config'
import { CircleHelp, Info, Monitor, Smartphone, Tablet } from 'lucide-vue-next'
import MagicString from 'magic-string'
import { reactive, ref, watch } from 'vue'
import { compileScript, parse, walk } from 'vue/compiler-sfc'
import { highlight } from '../config/shiki'
import BlockCopyButton from './BlockCopyButton.vue'
import StyleSwitcher from './StyleSwitcher.vue'
// import { V0Button } from '@/components/v0-button'
import { Badge } from '@/lib/registry/new-york/ui/badge'
import { Popover, PopoverContent, PopoverTrigger } from '@/lib/registry/new-york/ui/popover'
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/lib/registry/new-york/ui/resizable'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/lib/registry/new-york/ui/tabs'
import { ToggleGroup, ToggleGroupItem } from '@/lib/registry/new-york/ui/toggle-group'
import BlockPreview from './BlockPreview.vue'
const props = defineProps<{
name: string
}>()
const { style, codeConfig } = useConfigStore()
const isLoading = ref(true)
const tabValue = ref('preview')
const resizableRef = ref<InstanceType<typeof ResizablePanel>>()
const rawString = ref('')
const codeHtml = ref('')
const metadata = reactive({
description: null as string | null,
iframeHeight: null as string | null,
containerClass: null as string | null,
})
function removeScript(code: string) {
const s = new MagicString(code)
const scriptTagRegex = /<script\s+lang="ts"\s*>[\s\S]+?<\/script>/g
let match
// eslint-disable-next-line no-cond-assign
while ((match = scriptTagRegex.exec(code)) !== null) {
const start = match.index
const end = match.index + match[0].length
s.overwrite(start, end, '') // Replace the script tag with an empty string
}
return s.trimStart().toString()
}
function transformImportPath(code: string) {
const s = new MagicString(code)
s.replaceAll(`@/lib/registry/${style.value}`, codeConfig.value.componentsPath)
s.replaceAll(`@/lib/utils`, codeConfig.value.utilsPath)
return s.toString()
}
watch([style, codeConfig], async () => {
try {
const baseRawString = await import(`../../../src/lib/registry/${style.value}/block/${props.name}.vue?raw`).then(res => res.default.trim())
rawString.value = transformImportPath(removeScript(baseRawString))
if (!metadata.description) {
const { descriptor } = parse(baseRawString)
const ast = compileScript(descriptor, { id: '' })
walk(ast.scriptAst, {
enter(node: any) {
const declaration = node.declaration
// Check if the declaration is a variable declaration
if (declaration?.type === 'VariableDeclaration') {
// Extract variable names and their values
declaration.declarations.forEach((decl: any) => {
// @ts-expect-error ignore missing type
metadata[decl.id.name] = decl.init ? decl.init.value : null
})
}
},
})
}
codeHtml.value = highlight(rawString.value, 'vue')
}
catch (err) {
console.error(err)
}
}, { immediate: true, deep: true })
</script>
<template>
<Tabs
:id="name"
v-model="tabValue"
class="relative grid w-full scroll-m-20 gap-4"
:style=" {
'--container-height': metadata.iframeHeight ?? '600px',
}"
>
<div class="flex flex-col items-center gap-4 sm:flex-row">
<div class="flex items-center gap-2">
<TabsList class="hidden sm:flex">
<TabsTrigger value="preview">
Preview
</TabsTrigger>
<TabsTrigger value="code">
Code
</TabsTrigger>
</TabsList>
<div class="hidden items-center gap-2 sm:flex">
<Separator
orientation="vertical"
class="mx-2 hidden h-4 md:flex"
/>
<div class="flex items-center gap-2">
<a :href="`#${name}`">
<Badge variant="outline">{{ name }}</Badge>
</a>
<Popover>
<PopoverTrigger class="hidden text-muted-foreground hover:text-foreground sm:flex">
<Info class="h-3.5 w-3.5" />
<span class="sr-only">Block description</span>
</PopoverTrigger>
<PopoverContent
side="right"
:side-offset="10"
class="text-sm"
>
{{ metadata.description }}
</PopoverContent>
</Popover>
</div>
</div>
</div>
<div class="flex items-center gap-2 pr-[14px] sm:ml-auto">
<div class="hidden h-[28px] items-center gap-1.5 rounded-md border p-[2px] shadow-sm md:flex">
<ToggleGroup
type="single"
default-value="100"
@update:model-value="(value) => {
resizableRef?.resize(parseInt(value as string))
}"
>
<ToggleGroupItem
value="100"
class="h-[22px] w-[22px] rounded-sm p-0"
>
<Monitor class="h-3.5 w-3.5" />
</ToggleGroupItem>
<ToggleGroupItem
value="60"
class="h-[22px] w-[22px] rounded-sm p-0"
>
<Tablet class="h-3.5 w-3.5" />
</ToggleGroupItem>
<ToggleGroupItem
value="30"
class="h-[22px] w-[22px] rounded-sm p-0"
>
<Smartphone class="h-3.5 w-3.5" />
</ToggleGroupItem>
</ToggleGroup>
</div>
<Separator
orientation="vertical"
class="mx-2 hidden h-4 md:flex"
/>
<StyleSwitcher class="h-7" />
<Popover>
<PopoverTrigger class="hidden text-muted-foreground hover:text-foreground sm:flex">
<CircleHelp class="h-3.5 w-3.5" />
<span class="sr-only">Block description</span>
</PopoverTrigger>
<PopoverContent
side="top"
:side-offset="20"
class="space-y-3 rounded-[0.5rem] text-sm"
>
<p class="font-medium">
What is the difference between the New York and Default style?
</p>
<p>
A style comes with its own set of components, animations,
icons and more.
</p>
<p>
The <span class="font-medium">Default</span> style has
larger inputs, uses lucide-vue-next for icons and
tailwindcss-animate for animations.
</p>
<p>
The <span class="font-medium">New York</span> style ships
with smaller buttons and inputs. It also uses shadows on cards
and buttons.
</p>
</PopoverContent>
</Popover>
<Separator orientation="vertical" class="mx-2 h-4" />
<BlockCopyButton :code="rawString" />
<!-- <V0Button
name="{block.name}"
description="{block.description" || "Edit in v0"}
code="{block.code}"
style="{block.style}"
/> -->
</div>
</div>
<TabsContent
v-show="tabValue === 'preview'"
force-mount
value="preview"
class="relative after:absolute after:inset-0 after:right-3 after:z-0 after:rounded-lg after:bg-muted h-[--container-height] px-0"
>
<ResizablePanelGroup id="block-resizable" direction="horizontal" class="relative z-10">
<ResizablePanel
id="block-resizable-panel-1"
ref="resizableRef"
:default-size="100"
:min-size="30"
:as-child="true"
>
<BlockPreview :name="name" styles="default" :container-class="metadata.containerClass ?? ''" container />
</ResizablePanel>
<ResizableHandle id="block-resizable-handle" class="relative hidden w-3 bg-transparent p-0 after:absolute after:right-0 after:top-1/2 after:h-8 after:w-[6px] after:-translate-y-1/2 after:translate-x-[-1px] after:rounded-full after:bg-border after:transition-all after:hover:h-10 sm:block" />
<ResizablePanel id="block-resizable-panel-2" :default-size="0" :min-size="0" />
</ResizablePanelGroup>
</TabsContent>
<TabsContent value="code" class="h-[--container-height]">
<div
class="language-vue !h-full !max-h-[none] !mt-0"
v-html="codeHtml"
/>
</TabsContent>
</Tabs>
</template>

View File

@ -2,11 +2,11 @@
import { useUrlSearchParams } from '@vueuse/core'
import ComponentLoader from './ComponentLoader.vue'
const params = useUrlSearchParams('hash-params')
const params = useUrlSearchParams('history')
</script>
<template>
<div v-if="params.name && params.style" :class="params.containerClass">
<div v-if="params.name" :class="params.containerClass">
<ComponentLoader :key="params.style?.toString()" :name="params.name?.toString()" :type-name="'block'" />
</div>
</template>

View File

@ -1,245 +1,44 @@
<script setup lang="ts">
import { useConfigStore } from '@/stores/config'
import { CircleHelp, Info, Monitor, Smartphone, Tablet } from 'lucide-vue-next'
import MagicString from 'magic-string'
import { codeToHtml } from 'shiki'
import { reactive, ref, watch } from 'vue'
import { compileScript, parse, walk } from 'vue/compiler-sfc'
import { cssVariables } from '../config/shiki'
import BlockCopyButton from './BlockCopyButton.vue'
import { computed, ref } from 'vue'
import Spinner from './Spinner.vue'
import StyleSwitcher from './StyleSwitcher.vue'
// import { V0Button } from '@/components/v0-button'
import { Badge } from '@/lib/registry/new-york/ui/badge'
import { Popover, PopoverContent, PopoverTrigger } from '@/lib/registry/new-york/ui/popover'
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/lib/registry/new-york/ui/resizable'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/lib/registry/new-york/ui/tabs'
import { ToggleGroup, ToggleGroupItem } from '@/lib/registry/new-york/ui/toggle-group'
const props = defineProps<{
name: string
styles?: string
containerClass?: string
container?: boolean
}>()
const { style, codeConfig } = useConfigStore()
const isLoading = ref(true)
const tabValue = ref('preview')
const resizableRef = ref<InstanceType<typeof ResizablePanel>>()
const rawString = ref('')
const codeHtml = ref('')
const metadata = reactive({
description: null as string | null,
iframeHeight: null as string | null,
containerClass: null as string | null,
const iframeURL = computed(() => {
// @ts-expect-error env available in import.meta
if (import.meta.env.SSR)
return ''
const url = new URL(`${window.location.origin}/blocks/renderer`)
Object.entries(props).forEach(([key, value]) => {
if (value)
url.searchParams.append(key, value as string)
})
function removeScript(code: string) {
const s = new MagicString(code)
const scriptTagRegex = /<script\s+lang="ts"\s*>[\s\S]+?<\/script>/g
let match
// eslint-disable-next-line no-cond-assign
while ((match = scriptTagRegex.exec(code)) !== null) {
const start = match.index
const end = match.index + match[0].length
s.overwrite(start, end, '') // Replace the script tag with an empty string
}
return s.trimStart().toString()
}
function transformImportPath(code: string) {
const s = new MagicString(code)
s.replaceAll(`@/lib/registry/${style.value}`, codeConfig.value.componentsPath)
s.replaceAll(`@/lib/utils`, codeConfig.value.utilsPath)
return s.toString()
}
watch([style, codeConfig], async () => {
try {
const baseRawString = await import(`../../../src/lib/registry/${style.value}/block/${props.name}.vue?raw`).then(res => res.default.trim())
rawString.value = transformImportPath(removeScript(baseRawString))
if (!metadata.description) {
const { descriptor } = parse(baseRawString)
const ast = compileScript(descriptor, { id: '' })
walk(ast.scriptAst, {
enter(node: any) {
const declaration = node.declaration
// Check if the declaration is a variable declaration
if (declaration?.type === 'VariableDeclaration') {
// Extract variable names and their values
declaration.declarations.forEach((decl: any) => {
// @ts-expect-error ignore missing type
metadata[decl.id.name] = decl.init ? decl.init.value : null
return url.href
})
}
},
})
}
codeHtml.value = await codeToHtml(rawString.value, {
lang: 'vue',
theme: cssVariables,
})
}
catch (err) {
console.error(err)
}
}, { immediate: true, deep: true })
</script>
<template>
<Tabs
:id="name"
v-model="tabValue"
class="relative grid w-full scroll-m-20 gap-4"
:style=" {
'--container-height': metadata.iframeHeight ?? '600px',
}"
>
<div class="flex flex-col items-center gap-4 sm:flex-row">
<div class="flex items-center gap-2">
<TabsList class="hidden sm:flex">
<TabsTrigger value="preview">
Preview
</TabsTrigger>
<TabsTrigger value="code">
Code
</TabsTrigger>
</TabsList>
<div class="hidden items-center gap-2 sm:flex">
<Separator
orientation="vertical"
class="mx-2 hidden h-4 md:flex"
/>
<div class="flex items-center gap-2">
<a :href="`#${name}`">
<Badge variant="outline">{{ name }}</Badge>
</a>
<Popover>
<PopoverTrigger class="hidden text-muted-foreground hover:text-foreground sm:flex">
<Info class="h-3.5 w-3.5" />
<span class="sr-only">Block description</span>
</PopoverTrigger>
<PopoverContent
side="right"
:side-offset="10"
class="text-sm"
>
{{ metadata.description }}
</PopoverContent>
</Popover>
</div>
</div>
</div>
<div class="flex items-center gap-2 pr-[14px] sm:ml-auto">
<div class="hidden h-[28px] items-center gap-1.5 rounded-md border p-[2px] shadow-sm md:flex">
<ToggleGroup
type="single"
default-value="100"
@update:model-value="(value) => {
resizableRef?.resize(parseInt(value))
}"
>
<ToggleGroupItem
value="100"
class="h-[22px] w-[22px] rounded-sm p-0"
>
<Monitor class="h-3.5 w-3.5" />
</ToggleGroupItem>
<ToggleGroupItem
value="60"
class="h-[22px] w-[22px] rounded-sm p-0"
>
<Tablet class="h-3.5 w-3.5" />
</ToggleGroupItem>
<ToggleGroupItem
value="30"
class="h-[22px] w-[22px] rounded-sm p-0"
>
<Smartphone class="h-3.5 w-3.5" />
</ToggleGroupItem>
</ToggleGroup>
</div>
<Separator
orientation="vertical"
class="mx-2 hidden h-4 md:flex"
/>
<StyleSwitcher class="h-7" />
<Popover>
<PopoverTrigger class="hidden text-muted-foreground hover:text-foreground sm:flex">
<CircleHelp class="h-3.5 w-3.5" />
<span class="sr-only">Block description</span>
</PopoverTrigger>
<PopoverContent
side="top"
:side-offset="20"
class="space-y-3 rounded-[0.5rem] text-sm"
>
<p class="font-medium">
What is the difference between the New York and Default style?
</p>
<p>
A style comes with its own set of components, animations,
icons and more.
</p>
<p>
The <span class="font-medium">Default</span> style has
larger inputs, uses lucide-vue-next for icons and
tailwindcss-animate for animations.
</p>
<p>
The <span class="font-medium">New York</span> style ships
with smaller buttons and inputs. It also uses shadows on cards
and buttons.
</p>
</PopoverContent>
</Popover>
<Separator orientation="vertical" class="mx-2 h-4" />
<BlockCopyButton :code="rawString" />
<!-- <V0Button
name="{block.name}"
description="{block.description" || "Edit in v0"}
code="{block.code}"
style="{block.style}"
/> -->
</div>
</div>
<TabsContent
v-show="tabValue === 'preview'"
force-mount
value="preview"
class="relative after:absolute after:inset-0 after:right-3 after:z-0 after:rounded-lg after:bg-muted h-[--container-height] px-0"
>
<ResizablePanelGroup id="block-resizable" direction="horizontal" class="relative z-10">
<ResizablePanel
id="block-resizable-panel-1"
ref="resizableRef"
class="relative rounded-lg border bg-background transition-all "
:default-size="100"
:min-size="30"
>
<div class="relative rounded-lg border overflow-hidden bg-background" :class="[container ? '' : 'aspect-[4/2.5]']">
<div v-if="isLoading" class="flex items-center justify-center h-full">
<Spinner />
</div>
<div
:class="[container ? 'w-full' : 'absolute inset-0 hidden w-[1600px] bg-background md:block']"
>
<iframe
v-show="!isLoading"
:src="`/blocks/renderer#name=${name}&style=${style}&containerClass=${encodeURIComponent(metadata.containerClass ?? '')}`"
class="relative z-20 w-full bg-background h-[--container-height]"
:src="iframeURL"
class="relative z-20 w-full bg-background" :class="[container ? 'h-[--container-height]' : 'size-full']"
@load="isLoading = false"
/>
</ResizablePanel>
<ResizableHandle id="block-resizable-handle" class="relative hidden w-3 bg-transparent p-0 after:absolute after:right-0 after:top-1/2 after:h-8 after:w-[6px] after:-translate-y-1/2 after:translate-x-[-1px] after:rounded-full after:bg-border after:transition-all after:hover:h-10 sm:block" />
<ResizablePanel id="block-resizable-panel-2" :default-size="0" :min-size="0" />
</ResizablePanelGroup>
</TabsContent>
<TabsContent value="code" class="h-[--container-height]">
<div
class="language-vue !h-full !max-h-[none] !mt-0"
v-html="codeHtml"
/>
</TabsContent>
</Tabs>
</div>
</div>
</template>

View File

@ -9,7 +9,7 @@ import PageHeader from '../components/PageHeader.vue'
import PageHeaderDescription from '../components/PageHeaderDescription.vue'
import PageHeaderHeading from '../components/PageHeaderHeading.vue'
import BlockPreview from './BlockPreview.vue'
import BlockContainer from './BlockContainer.vue'
const blocks = ref<string[]>([])
@ -48,6 +48,6 @@ import('../../../__registry__/index').then((res) => {
</PageHeader>
<section id="blocks" class="grid scroll-mt-24 gap-24 lg:gap-48">
<BlockPreview v-for="block in blocks" :key="block" :name="block" />
<BlockContainer v-for="block in blocks" :key="block" :name="block" />
</section>
</template>

View File

@ -0,0 +1,71 @@
<script setup lang="ts">
import { useData } from 'vitepress'
import { onMounted, ref, watch } from 'vue'
const { page, theme } = useData()
const carbonOptions = theme.value.carbonAds
const container = ref()
let isInitialized = false
function init() {
if (!isInitialized) {
isInitialized = true
const s = document.createElement('script')
s.type = 'text/javascript'
s.id = '_carbonads_js'
s.src = `//cdn.carbonads.com/carbon.js?serve=${carbonOptions.code}&placement=${carbonOptions.placement}&format=cover`
s.async = true
container.value.appendChild(s)
}
}
watch(() => page.value.relativePath, () => {
if (isInitialized) {
;(window as any)._carbonads?.refresh()
}
})
// no need to account for option changes during dev, we can just
// refresh the page
if (carbonOptions) {
onMounted(() => {
// @ts-expect-error ignoring env
if (import.meta.env.DEV)
return
// if the page is loaded when aside is active, load carbon directly.
// otherwise, only load it if the page resizes to wide enough. this avoids
// loading carbon at all on mobile where it's never shown
init()
})
}
</script>
<template>
<div
ref="container"
/>
</template>
<style>
#carbon-responsive {
@apply w-[238px] !mt-6;
}
.carbon-responsive-wrap {
@apply bg-muted/50 border border-muted p-4 rounded-md flex flex-col items-center !important;
}
.carbon-responsive-wrap .carbon-img {
@apply flex-none rounded overflow-hidden !important;
}
.carbon-responsive-wrap .carbon-text {
@apply text-muted-foreground text-sm flex-none text-center !important;
}
#carbonads .carbon-poweredby {
@apply bg-background text-muted-foreground block text-right text-[10px] uppercase no-underline !important;
}
</style>

View File

@ -4,9 +4,8 @@ import { cn } from '@/lib/utils'
import { useConfigStore } from '@/stores/config'
import { useClipboard } from '@vueuse/core'
import MagicString from 'magic-string'
import { codeToHtml } from 'shiki'
import { computed, ref, watch } from 'vue'
import { cssVariables } from '../config/shiki'
import { highlight } from '../config/shiki'
import CodeSandbox from './CodeSandbox.vue'
import ComponentLoader from './ComponentLoader.vue'
import Stackblitz from './Stackblitz.vue'
@ -37,10 +36,7 @@ function transformImportPath(code: string) {
watch([style, codeConfig], async () => {
try {
rawString.value = await import(`../../../src/lib/registry/${style.value}/example/${props.name}.vue?raw`).then(res => res.default.trim())
codeHtml.value = await codeToHtml(transformedRawString.value, {
lang: 'vue',
theme: cssVariables,
})
codeHtml.value = highlight(transformedRawString.value, 'vue')
}
catch (err) {
console.error(err)

View File

@ -5,8 +5,13 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/lib/regis
import { ScrollArea } from '@/lib/registry/default/ui/scroll-area'
import { onContentUpdated } from 'vitepress'
import { shallowRef } from 'vue'
import CarbonAds from '../components/CarbonAds.vue'
import TableOfContentTree from './TableOfContentTree.vue'
defineProps<{
showCarbonAds?: boolean
}>()
const headers = shallowRef<TableOfContents>()
function getHeadingsWithHierarchy(divId: string) {
@ -24,7 +29,7 @@ function getHeadingsWithHierarchy(divId: string) {
const level = Number.parseInt(heading.tagName.charAt(1))
if (!heading.id) {
const newId = heading.textContent
.replaceAll(/[^a-z0-9 ]/gi, '')
?.replaceAll(/[^a-z0-9 ]/gi, '')
.replaceAll(' ', '-')
.toLowerCase()
heading.id = `${newId}`
@ -63,6 +68,7 @@ onContentUpdated(() => {
On This Page
</p>
<TableOfContentTree :tree="headers" :level="1" />
<CarbonAds v-if="showCarbonAds" />
</div>
</ScrollArea>
</div>

View File

@ -43,7 +43,7 @@ const { isDark } = useData()
@click="setTheme(color)"
>
<span
class="h-5 w-5 rounded-full flex items-center justify-center"
class="h-5 w-5 rounded-full flex items-center justify-center shrink-0"
:style="{ backgroundColor: colors[color][7].rgb }"
>
<RadixIconsCheck

View File

@ -1,4 +1,5 @@
export { default as APITable } from './APITable.vue'
export { default as BlockPreview } from './BlockPreview.vue'
export { default as Callout } from './Callout.vue'
export { default as CodeWrapper } from './CodeWrapper'
export { default as ComponentPreview } from './ComponentPreview.vue'

View File

@ -128,12 +128,10 @@ export const docsConfig: DocsConfig = {
title: 'Auto Form',
href: '/docs/components/auto-form',
items: [],
label: 'New',
},
{
title: 'Charts',
href: '/docs/charts',
label: 'New Alpha',
items: [],
},
],
@ -141,6 +139,11 @@ export const docsConfig: DocsConfig = {
{
title: 'Components',
items: [
{
title: 'Sidebar',
href: '/docs/components/sidebar',
label: 'New',
},
{
title: 'Accordion',
href: '/docs/components/accordion',
@ -178,7 +181,6 @@ export const docsConfig: DocsConfig = {
title: 'Calendar',
href: '/docs/components/calendar',
items: [],
label: 'Updated',
},
{
title: 'Card',
@ -217,7 +219,6 @@ export const docsConfig: DocsConfig = {
title: 'Date Picker',
href: '/docs/components/date-picker',
items: [],
label: 'Updated',
},
{
title: 'Dialog',
@ -259,7 +260,6 @@ export const docsConfig: DocsConfig = {
{
title: 'Number Field',
href: '/docs/components/number-field',
label: 'New Alpha',
},
{
title: 'Pagination',
@ -324,7 +324,6 @@ export const docsConfig: DocsConfig = {
{
title: 'Stepper',
href: '/docs/components/stepper',
label: 'New',
},
{
title: 'Switch',

View File

@ -1,6 +1,41 @@
import { createCssVariablesTheme } from 'shiki'
import type { HighlighterCore } from 'shiki/core'
import type { ThemeOptions } from 'vitepress'
import { computedAsync } from '@vueuse/core'
import { createHighlighterCore } from 'shiki/core'
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
export const cssVariables = createCssVariablesTheme({
variablePrefix: '--shiki-',
variableDefaults: {},
export const shikiThemes: ThemeOptions = {
light: 'github-light-default',
dark: 'github-dark-default',
}
export const highlighter = computedAsync<HighlighterCore>(async (onCancel) => {
const shiki = await createHighlighterCore({
engine: createJavaScriptRegexEngine(),
themes: [
() => import('shiki/themes/github-dark-default.mjs'),
() => import('shiki/themes/github-light-default.mjs'),
],
langs: [
() => import('shiki/langs/javascript.mjs'),
() => import('shiki/langs/vue.mjs'),
],
})
onCancel(() => shiki?.dispose())
return shiki
})
export function highlight(code: string, lang: string) {
if (!highlighter.value)
return code
return highlighter.value.codeToHtml(code, {
lang,
defaultColor: false,
themes: {
dark: 'github-dark-default',
light: 'github-light-default',
},
})
}

View File

@ -5,7 +5,7 @@ import RadixIconsExternalLink from '~icons/radix-icons/external-link'
import { useData, useRoute } from 'vitepress'
import DocsBreadcrumb from '../components/DocsBreadcrumb.vue'
import EditLink from '../components/EditLink.vue'
import TableOfContentVue from '../components/TableOfContent.vue'
import TableOfContent from '../components/TableOfContent.vue'
import { docsConfig } from '../config/docs'
const $route = useRoute()
@ -62,7 +62,7 @@ const sourceLink = 'https://github.com/radix-vue/shadcn-vue/tree/dev/'
<main class="relative py-6 lg:gap-10 lg:py-8 xl:grid xl:grid-cols-[1fr_300px]">
<div class="mx-auto w-full min-w-0">
<div class="block xl:hidden">
<TableOfContentVue />
<TableOfContent />
</div>
<DocsBreadcrumb class="mb-4" />
@ -104,7 +104,7 @@ const sourceLink = 'https://github.com/radix-vue/shadcn-vue/tree/dev/'
<div class="hidden text-sm xl:block">
<div class="sticky top-16 -mt-10 h-[calc(100vh-3.5rem)] overflow-hidden pt-6">
<TableOfContentVue />
<TableOfContent show-carbon-ads />
</div>
</div>
</main>

View File

@ -41,7 +41,7 @@ const toggleDark = useToggle(isDark)
const links = [
{
name: 'GitHub',
href: 'https://github.com/radix-vue/shadcn-vue',
href: 'https://github.com/unovue/shadcn-vue',
icon: RadixIconsGithubLogo,
},
// {
@ -209,7 +209,7 @@ watch(() => $route.path, (n) => {
<span class="inline-block ml-2">
Ported to Vue by
<a
href="https://github.com/radix-vue"
href="https://github.com/unovue"
target="_blank"
class="underline underline-offset-4 font-bold decoration-foreground"
>
@ -220,7 +220,7 @@ watch(() => $route.path, (n) => {
<span class="inline-block ml-2">
The code source is available on
<a
href="https://github.com/radix-vue/shadcn-vue"
href="https://github.com/unovue/shadcn-vue"
target="_blank"
class="underline underline-offset-4 font-bold decoration-foreground"
>

View File

@ -28,6 +28,16 @@
--ring: 240 5% 64.9%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
--vis-primary-color: var(--primary);
--vis-secondary-color: 160 81% 40%;
--vis-text-color: var(--muted-foreground);
@ -64,11 +74,22 @@
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
* {
@apply border-border;
scrollbar-width: thin;
scrollbar-color: hsl(var(--border)) transparent;
}
html {
-webkit-text-size-adjust: 100%;
@ -97,19 +118,6 @@
src: url("/fonts/Geist/GeistVariableVF.woff2") format("woff2");
}
/* === Scrollbars === */
::-webkit-scrollbar {
@apply w-2;
@apply h-2;
}
::-webkit-scrollbar-track {
@apply !bg-muted;
}
::-webkit-scrollbar-thumb {
@apply rounded-sm !bg-muted-foreground/30;
}
/* Firefox */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
@ -121,13 +129,13 @@
scrollbar-color: hsl(215.4 16.3% 56.9% / 0.3);
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
html.dark .shiki,
html.dark .shiki span {
color: var(--shiki-dark);
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
html:not(.dark) .shiki,
html:not(.dark) .shiki span {
color: var(--shiki-light);
}
.antialised {
@ -157,7 +165,7 @@
}
div[class^="language-"] {
@apply mb-4 mt-6 max-h-[650px] overflow-x-auto md:rounded-lg border !bg-secondary-foreground dark:!bg-secondary
@apply mb-4 mt-6 max-h-[650px] overflow-x-auto md:rounded-lg border
}
pre {
@apply py-4;

View File

@ -350,7 +350,7 @@
padding: 0 24px; */
width: calc(100% + 2 * 24px);
display: inline-block;
@apply bg-[hsl(var(--foreground))] dark:bg-[hsl(var(--background)_/_50%)]
@apply bg-[hsl(var(--muted))] dark:bg-[hsl(var(--muted))]
}
.vp-doc [class*='language-'] code .highlighted.error {

View File

@ -1501,6 +1501,20 @@ export const Index = {
component: () => import("../src/lib/registry/default/block/Dashboard07.vue").then((m) => m.default),
files: ["../src/lib/registry/default/block/Dashboard07.vue"],
},
"Sidebar01": {
name: "Sidebar01",
type: "components:block",
registryDependencies: ["breadcrumb","dropdown-menu","label","separator","sidebar"],
component: () => import("../src/lib/registry/default/block/Sidebar01.vue").then((m) => m.default),
files: ["../src/lib/registry/default/block/Sidebar01.vue"],
},
"Sidebar07": {
name: "Sidebar07",
type: "components:block",
registryDependencies: ["avatar","breadcrumb","collapsible","dropdown-menu","separator","sidebar"],
component: () => import("../src/lib/registry/default/block/Sidebar07.vue").then((m) => m.default),
files: ["../src/lib/registry/default/block/Sidebar07.vue"],
},
}, "new-york": {
"AccordionDemo": {
name: "AccordionDemo",
@ -3000,5 +3014,19 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/block/Dashboard07.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/block/Dashboard07.vue"],
},
"Sidebar01": {
name: "Sidebar01",
type: "components:block",
registryDependencies: ["breadcrumb","dropdown-menu","label","separator","sidebar"],
component: () => import("../src/lib/registry/new-york/block/Sidebar01.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/block/Sidebar01.vue"],
},
"Sidebar07": {
name: "Sidebar07",
type: "components:block",
registryDependencies: ["avatar","breadcrumb","collapsible","dropdown-menu","separator","sidebar"],
component: () => import("../src/lib/registry/new-york/block/Sidebar07.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/block/Sidebar07.vue"],
},
},
}

View File

@ -1,7 +1,7 @@
{
"name": "www",
"type": "module",
"version": "0.11.0",
"version": "0.11.3",
"files": [
"dist"
],
@ -68,11 +68,11 @@
"markdown-it": "^14.1.0",
"pathe": "^1.1.2",
"rimraf": "^6.0.1",
"shiki": "^1.17.7",
"shiki": "^1.22.1",
"tailwind-merge": "^2.5.2",
"tailwindcss": "^3.4.12",
"tsx": "^4.19.1",
"typescript": "^5.6.2",
"typescript": "catalog:",
"unplugin-icons": "^0.19.3",
"vitepress": "^1.3.4",
"vue-component-meta": "^2.1.6",

View File

@ -80,10 +80,17 @@ for (const style of styles) {
continue
const files = item.files?.map((file) => {
let content = fs.readFileSync(
let content: string = ''
try {
content = fs.readFileSync(
path.join(process.cwd(), 'src/lib/registry', style.name, file),
'utf8',
)
}
catch (err) {
console.log(`'${file}' is missing`)
}
// Replace Windows-style newlines with Unix-style newlines
content = content.replace(/\r\n/g, newLine)
@ -99,7 +106,7 @@ for (const style of styles) {
files,
}
const payloadStr = JSON.stringify(payload, null, 2).replace(/\r\n/g, newLine)
const payloadStr = `${JSON.stringify(payload, null, 2).replace(/\r\n/g, newLine)}\n`
fs.writeFileSync(
path.join(targetPath, `${item.name}.json`),

View File

@ -15,7 +15,7 @@ npx shadcn-vue@latest init
You will be asked a few questions to configure `components.json`:
```txt:line-numbers
```ansi:line-numbers
Would you like to use TypeScript (recommended)? no / yes
Which framework are you using? Vite / Nuxt / Laravel
Which style would you like to use? Default
@ -29,7 +29,7 @@ Configure the import alias for utils: @/lib/utils
### Options
```txt
```ansi
Usage: shadcn-vue init [options]
initialize your project and install dependencies
@ -50,7 +50,7 @@ npx shadcn-vue@latest add [component]
You will be presented with a list of components to choose from:
```txt
```ansi
Which components would you like to add? Space to select. Return to submit.
◯ accordion
@ -67,7 +67,7 @@ Which components would you like to add? Space to select. Return to submit.
### Options
```txt
```ansi
Usage: shadcn-vue add [options] [components...]
add components to your project
@ -90,7 +90,7 @@ Use the `update` command to update components in your project. This will overwri
We plan on improving this command in the future to improve the update experience.
```txt
```ansi
Usage: shadcn-vue update [options] [components...]
update components in your project

View File

@ -403,6 +403,7 @@ By passing the `form` as props, you can control and use the method provided by `
```vue
<script setup lang="ts">
import { AutoForm } from '@/components/ui/auto-form'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import * as z from 'zod'

View File

@ -76,7 +76,7 @@ const value = ref('')
<CommandItem
v-for="framework in frameworks"
:key="framework.value"
:value="framework"
:value="framework.value"
@select="open = false"
>
<Check

View File

@ -1,7 +1,7 @@
---
title: Data Table
description: Powerful table and datagrids built using TanStack Table.
primitive: https://tanstack.com/table/v8/docs/guide/introduction
primitive: https://tanstack.com/table/v8/docs/introduction
---
<ComponentPreview name="DataTableDemo" />
@ -102,7 +102,7 @@ export const payments: Payment[] = [
Start by creating the following file structure:
```txt
```ansi
components
└── payments
├── columns.ts

View File

@ -0,0 +1,12 @@
---
title: Sidebar
description: A composable, themeable and customizable sidebar component.
---
<BlockPreview name="Sidebar07" ></BlockPreview>
## Installation
```bash
npx shadcn-vue@latest add sidebar
```

View File

@ -48,6 +48,19 @@ import { Switch } from '@/components/ui/switch'
</template>
```
# Add icon inside switch thumb
```vue
<template>
<Switch :checked="isDark" @update:checked="toggleTheme">
<template #thumb>
<Icon v-if="isDark" icon="lucide:moon" class="size-3" />
<Icon v-else icon="lucide:sun" class="size-3" />
</template>
</Switch>
</template>
```
## Examples
### Form

View File

@ -101,13 +101,13 @@ You can use the `pnpm --filter=[WORKSPACE]` command to start the development pro
1. To run the `shadcn-vue.com` website:
```
```bash
pnpm dev
```
2. To run the `shadcn-vue` cli package:
```
```bash
pnpm dev:cli
```

View File

@ -17,7 +17,7 @@ npm create astro@latest
You will be asked a few questions to configure your project:
```txt:line-numbers
```ansi:line-numbers
- Where should we create your new project?
./your-app-name
- How would you like to start your new project?
@ -99,7 +99,7 @@ npx shadcn-vue@latest init
You will be asked a few questions to configure `components.json`:
```txt:line-numbers
```ansi:line-numbers
Would you like to use TypeScript (recommended)? no / yes
Which framework are you using? Astro
Which style would you like to use? Default

View File

@ -25,7 +25,7 @@ npx shadcn-vue@latest init
You will be asked a few questions to configure `components.json`:
```txt:line-numbers
```ansi:line-numbers
Would you like to use TypeScript (recommended)? no / yes
Which framework are you using? Vite / Nuxt / Laravel
Which style would you like to use? Default

View File

@ -213,7 +213,7 @@ npx shadcn-vue@latest init
You will be asked a few questions to configure `components.json`:
```txt:line-numbers
```ansi:line-numbers
Would you like to use TypeScript (recommended)? no / yes
Which framework are you using? Vite / Nuxt / Laravel
Which style would you like to use? Default
@ -231,7 +231,7 @@ Write configuration to components.json. Proceed? > Y/n
Here's the default structure of Nuxt app. You can use this as a reference:
```txt {6-16,20-21}
```ansi {6-16,20-21}
.
├── pages
│ ├── index.vue

View File

@ -44,14 +44,13 @@ Install `tailwindcss` and its peer dependencies, then generate your `tailwind.co
#### `vite.config`
```typescript {5,6,9-13}
import path from 'node:path'
```typescript {2,3,8-12}
import vue from '@vitejs/plugin-vue'
import autoprefixer from 'autoprefixer'
import tailwind from 'tailwindcss'
import { defineConfig } from 'vite'
// https://vite.dev/config/
export default defineConfig({
css: {
postcss: {
@ -59,11 +58,6 @@ Install `tailwindcss` and its peer dependencies, then generate your `tailwind.co
},
},
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
```
@ -121,14 +115,14 @@ Add the code below to the vite.config.ts so your app can resolve paths without e
npm i -D @types/node
```
```typescript {15-19}
import path from 'node:path'
```typescript {1,15-19}
import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
import autoprefixer from 'autoprefixer'
import tailwind from 'tailwindcss'
import { defineConfig } from 'vite'
// https://vite.dev/config/
export default defineConfig({
css: {
postcss: {
@ -138,9 +132,9 @@ export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
```
@ -160,7 +154,7 @@ npx shadcn-vue@latest init
You will be asked a few questions to configure `components.json`:
```txt:line-numbers
```ansi:line-numbers
Would you like to use TypeScript (recommended)? no / yes
Which framework are you using? Vite / Nuxt / Laravel
Which style would you like to use? Default

View File

@ -0,0 +1,217 @@
<script lang="ts">
export const iframeHeight = '800px'
export const description
= 'A simple sidebar with navigation grouped by section.'
</script>
<script setup lang="ts">
import { Check, ChevronsUpDown, GalleryVerticalEnd, Search } from 'lucide-vue-next'
import { ref } from 'vue'
// Import components from the custom library
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/lib/registry/default/ui/breadcrumb'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/lib/registry/default/ui/dropdown-menu'
import { Label } from '@/lib/registry/default/ui/label'
import { Separator } from '@/lib/registry/default/ui/separator'
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from '@/lib/registry/default/ui/sidebar'
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: 'Building 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: '#' },
],
},
],
}
const selectedVersion = ref(data.versions[0])
const dropdownOpen = ref(false)
const search = ref('')
function toggleDropdown() {
dropdownOpen.value = !dropdownOpen.value
}
function setSelectedVersion(version: string) {
selectedVersion.value = version
}
</script>
<template>
<SidebarProvider>
<Sidebar>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
:class="{ 'data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground': dropdownOpen }"
@click="toggleDropdown"
>
<div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<GalleryVerticalEnd class="size-4" />
</div>
<div class="flex flex-col gap-0.5 leading-none">
<span class="font-semibold">Documentation</span>
<span>v{{ selectedVersion }}</span>
</div>
<ChevronsUpDown class="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
v-if="dropdownOpen"
class="w-[--radix-dropdown-menu-trigger-width]"
align="start"
>
<DropdownMenuItem
v-for="version in data.versions"
:key="version"
@click="setSelectedVersion(version)"
>
v{{ version }}
<Check v-if="version === selectedVersion" class="ml-auto" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
<form @submit.prevent>
<SidebarGroup class="py-0">
<SidebarGroupContent class="relative">
<Label for="search" class="sr-only">Search</Label>
<SidebarInput
id="search"
v-model="search"
placeholder="Search the docs..."
class="pl-8"
/>
<Search class="pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-50" />
</SidebarGroupContent>
</SidebarGroup>
</form>
</SidebarHeader>
<SidebarContent>
<SidebarGroup v-for="item in data.navMain" :key="item.title">
<SidebarGroupLabel>{{ item.title }}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem v-for="subItem in item.items" :key="subItem.title">
<SidebarMenuButton :class="{ 'is-active': subItem.isActive }" as-child>
<a :href="subItem.url">{{ subItem.title }}</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarRail />
</Sidebar>
<SidebarInset>
<header class="flex h-16 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger class="-ml-1" />
<Separator orientation="vertical" class="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem class="hidden md:block">
<BreadcrumbLink href="#">
Building Your Application
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator class="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</header>
<div class="flex flex-1 flex-col gap-4 p-4">
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
<div class="aspect-video rounded-xl bg-muted/50" />
<div class="aspect-video rounded-xl bg-muted/50" />
<div class="aspect-video rounded-xl bg-muted/50" />
</div>
<div class="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
</div>
</SidebarInset>
</SidebarProvider>
</template>

View File

@ -0,0 +1,463 @@
<script lang="ts">
export const description
= 'A sidebar that collapses to icons.'
export const iframeHeight = '800px'
export const containerClass = 'w-full h-full'
</script>
<script setup lang=ts>
import {
Avatar,
AvatarFallback,
AvatarImage,
} from '@/lib/registry/default/ui/avatar'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/lib/registry/default/ui/breadcrumb'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/lib/registry/default/ui/collapsible'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/lib/registry/default/ui/dropdown-menu'
import { Separator } from '@/lib/registry/default/ui/separator'
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from '@/lib/registry/default/ui/sidebar'
import {
AudioWaveform,
BadgeCheck,
Bell,
BookOpen,
Bot,
ChevronRight,
ChevronsUpDown,
Command,
CreditCard,
Folder,
Forward,
Frame,
GalleryVerticalEnd,
LogOut,
Map,
MoreHorizontal,
PieChart,
Plus,
Settings2,
Sparkles,
SquareTerminal,
Trash2,
} from 'lucide-vue-next'
import { ref } from 'vue'
// This is sample data.
const data = {
user: {
name: 'shadcn',
email: 'm@example.com',
avatar: '/avatars/shadcn.jpg',
},
teams: [
{
name: 'Acme Inc',
logo: GalleryVerticalEnd,
plan: 'Enterprise',
},
{
name: 'Acme Corp.',
logo: AudioWaveform,
plan: 'Startup',
},
{
name: 'Evil Corp.',
logo: Command,
plan: 'Free',
},
],
navMain: [
{
title: 'Playground',
url: '#',
icon: SquareTerminal,
isActive: true,
items: [
{
title: 'History',
url: '#',
},
{
title: 'Starred',
url: '#',
},
{
title: 'Settings',
url: '#',
},
],
},
{
title: 'Models',
url: '#',
icon: Bot,
items: [
{
title: 'Genesis',
url: '#',
},
{
title: 'Explorer',
url: '#',
},
{
title: 'Quantum',
url: '#',
},
],
},
{
title: 'Documentation',
url: '#',
icon: BookOpen,
items: [
{
title: 'Introduction',
url: '#',
},
{
title: 'Get Started',
url: '#',
},
{
title: 'Tutorials',
url: '#',
},
{
title: 'Changelog',
url: '#',
},
],
},
{
title: 'Settings',
url: '#',
icon: Settings2,
items: [
{
title: 'General',
url: '#',
},
{
title: 'Team',
url: '#',
},
{
title: 'Billing',
url: '#',
},
{
title: 'Limits',
url: '#',
},
],
},
],
projects: [
{
name: 'Design Engineering',
url: '#',
icon: Frame,
},
{
name: 'Sales & Marketing',
url: '#',
icon: PieChart,
},
{
name: 'Travel',
url: '#',
icon: Map,
},
],
}
const activeTeam = ref(data.teams[0])
function setActiveTeam(team: typeof data.teams[number]) {
activeTeam.value = team
}
</script>
<template>
<SidebarProvider>
<Sidebar collapsible="icon">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<component :is="activeTeam.logo" class="size-4" />
</div>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ activeTeam.name }}</span>
<span class="truncate text-xs">{{ activeTeam.plan }}</span>
</div>
<ChevronsUpDown class="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="start"
side="bottom"
:side-offset="4"
>
<DropdownMenuLabel class="text-xs text-muted-foreground">
Teams
</DropdownMenuLabel>
<DropdownMenuItem
v-for="(team, index) in data.teams"
:key="team.name"
class="gap-2 p-2"
@click="setActiveTeam(team)"
>
<div class="flex size-6 items-center justify-center rounded-sm border">
<component :is="team.logo" class="size-4 shrink-0" />
</div>
{{ team.name }}
<DropdownMenuShortcut>{{ index + 1 }}</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem class="gap-2 p-2">
<div class="flex size-6 items-center justify-center rounded-md border bg-background">
<Plus class="size-4" />
</div>
<div class="font-medium text-muted-foreground">
Add team
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
<Collapsible
v-for="item in data.navMain"
:key="item.title"
as-child
:default-open="item.isActive"
class="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger as-child>
<SidebarMenuButton :tooltip="item.title">
<component :is="item.icon" />
<span>{{ item.title }}</span>
<ChevronRight class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSubItem
v-for="subItem in item.items"
:key="subItem.title"
>
<SidebarMenuSubButton as-child>
<a :href="subItem.url">
<span>{{ subItem.title }}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
</SidebarMenu>
</SidebarGroup>
<SidebarGroup class="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem
v-for="item in data.projects"
:key="item.name"
>
<SidebarMenuButton as-child>
<a :href="item.url">
<component :is="item.icon" />
<span>{{ item.name }}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuAction show-on-hover>
<MoreHorizontal />
<span class="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-48 rounded-lg" side="bottom" align="end">
<DropdownMenuItem>
<Folder class="text-muted-foreground" />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Forward class="text-muted-foreground" />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 class="text-muted-foreground" />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton class="text-sidebar-foreground/70">
<MoreHorizontal class="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar class="h-8 w-8 rounded-lg">
<AvatarImage :src="data.user.avatar" :alt="data.user.name" />
<AvatarFallback class="rounded-lg">
CN
</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ data.user.name }}</span>
<span class="truncate text-xs">{{ data.user.email }}</span>
</div>
<ChevronsUpDown class="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg" side="bottom" align="end" :side-offset="4">
<DropdownMenuLabel class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar class="h-8 w-8 rounded-lg">
<AvatarImage :src="data.user.avatar" :alt="data.user.name" />
<AvatarFallback class="rounded-lg">
CN
</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ data.user.name }}</span>
<span class="truncate text-xs">{{ data.user.email }}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<SidebarInset>
<header class="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div class="flex items-center gap-2 px-4">
<SidebarTrigger class="-ml-1" />
<Separator orientation="vertical" class="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem class="hidden md:block">
<BreadcrumbLink href="#">
Building Your Application
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator class="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
<div class="aspect-video rounded-xl bg-muted/50" />
<div class="aspect-video rounded-xl bg-muted/50" />
<div class="aspect-video rounded-xl bg-muted/50" />
</div>
<div class="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
</div>
</SidebarInset>
</SidebarProvider>
</template>

View File

@ -42,7 +42,7 @@ const itemsToDisplay = 3
const firstLabel = computed(() => items.value[0]?.label)
const allButLastTwoItems = computed(() => items.value.slice(1, -2))
const remainingItems = computed(() => items.value.slice(-itemsToDisplay + 1))
const remainingItems = computed(() => items.value.slice(-Math.min(itemsToDisplay, items.value.length) + 1))
</script>
<template>

View File

@ -32,13 +32,13 @@ import { Label } from '@/lib/registry/default/ui/label'
<Label for="name" class="text-right">
Name
</Label>
<Input id="name" value="Pedro Duarte" class="col-span-3" />
<Input id="name" default-value="Pedro Duarte" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="username" class="text-right">
Username
</Label>
<Input id="username" value="@peduarte" class="col-span-3" />
<Input id="username" default-value="@peduarte" class="col-span-3" />
</div>
</div>
<DialogFooter>

View File

@ -38,7 +38,7 @@ function onSubmit(values: any) {
</script>
<template>
<Form v-slot="{ submitForm }" as="" :validation-schema="formSchema" @submit="onSubmit">
<Form v-slot="{ handleSubmit }" as="" keep-values :validation-schema="formSchema">
<Dialog>
<DialogTrigger as-child>
<Button variant="outline">
@ -53,7 +53,7 @@ function onSubmit(values: any) {
</DialogDescription>
</DialogHeader>
<form @submit="submitForm">
<form id="dialogForm" @submit="handleSubmit($event, onSubmit)">
<FormField v-slot="{ componentField }" name="username">
<FormItem>
<FormLabel>Username</FormLabel>

View File

@ -12,36 +12,36 @@ import {
const components: { title: string, href: string, description: string }[] = [
{
title: 'Alert Dialog',
href: '/docs/primitives/alert-dialog',
href: '/docs/components/alert-dialog',
description:
'A modal dialog that interrupts the user with important content and expects a response.',
},
{
title: 'Hover Card',
href: '/docs/primitives/hover-card',
href: '/docs/components/hover-card',
description:
'For sighted users to preview content available behind a link.',
},
{
title: 'Progress',
href: '/docs/primitives/progress',
href: '/docs/components/progress',
description:
'Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.',
},
{
title: 'Scroll-area',
href: '/docs/primitives/scroll-area',
href: '/docs/components/scroll-area',
description: 'Visually or semantically separates content.',
},
{
title: 'Tabs',
href: '/docs/primitives/tabs',
href: '/docs/components/tabs',
description:
'A set of layered sections of content—known as tab panels—that are displayed one at a time.',
},
{
title: 'Tooltip',
href: '/docs/primitives/tooltip',
href: '/docs/components/tooltip',
description:
'A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.',
},
@ -76,7 +76,7 @@ const components: { title: string, href: string, description: string }[] = [
<li>
<NavigationMenuLink as-child>
<a
href="/docs"
href="/docs/introduction"
class="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div class="text-sm font-medium leading-none">Introduction</div>
@ -102,7 +102,7 @@ const components: { title: string, href: string, description: string }[] = [
<li>
<NavigationMenuLink as-child>
<a
href="/docs/primitives/typography"
href="/docs/typography"
class="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div class="text-sm font-medium leading-none">Typography</div>
@ -136,7 +136,7 @@ const components: { title: string, href: string, description: string }[] = [
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink href="/docs" :class="navigationMenuTriggerStyle()">
<NavigationMenuLink href="/docs/introduction" :class="navigationMenuTriggerStyle()">
Documentation
</NavigationMenuLink>
</NavigationMenuItem>

View File

@ -33,13 +33,13 @@ import {
<Label for="name" class="text-right">
Name
</Label>
<Input id="name" value="Pedro Duarte" class="col-span-3" />
<Input id="name" default-value="Pedro Duarte" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="username" class="text-right">
Username
</Label>
<Input id="username" value="@peduarte" class="col-span-3" />
<Input id="username" default-value="@peduarte" class="col-span-3" />
</div>
</div>
<SheetFooter>

View File

@ -1,3 +1,4 @@
import type { InputComponents } from './interface'
import AutoFormFieldArray from './AutoFormFieldArray.vue'
import AutoFormFieldBoolean from './AutoFormFieldBoolean.vue'
import AutoFormFieldDate from './AutoFormFieldDate.vue'
@ -7,7 +8,7 @@ import AutoFormFieldInput from './AutoFormFieldInput.vue'
import AutoFormFieldNumber from './AutoFormFieldNumber.vue'
import AutoFormFieldObject from './AutoFormFieldObject.vue'
export const INPUT_COMPONENTS = {
export const INPUT_COMPONENTS: InputComponents = {
date: AutoFormFieldDate,
select: AutoFormFieldEnum,
radio: AutoFormFieldEnum,

View File

@ -18,6 +18,20 @@ export interface Shape {
schema?: ZodAny
}
export interface InputComponents {
date: Component;
select: Component;
radio: Component;
checkbox: Component;
switch: Component;
textarea: Component;
number: Component;
string: Component;
file: Component;
array: Component;
object: Component;
};
export interface ConfigItem {
/** Value for the `FormLabel` */
label?: string

View File

@ -12,7 +12,7 @@ const props = defineProps<{
<li
role="presentation"
aria-hidden="true"
:class="cn('[&>svg]:size-3.5', props.class)"
:class="cn('[&>svg]:w-3.5 [&>svg]:h-3.5', props.class)"
>
<slot>
<ChevronRight />

View File

@ -3,26 +3,26 @@ import { cva, type VariantProps } from 'class-variance-authority'
export { default as Button } from './Button.vue'
export const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
default:
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
xs: 'h-7 rounded px-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {

View File

@ -18,7 +18,7 @@ const forwardedProps = useForwardProps(delegatedProps)
<DropdownMenuItem
v-bind="forwardedProps"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
inset && 'pl-8',
props.class,
)"

View File

@ -6,6 +6,7 @@ export { default as NavigationMenuItem } from './NavigationMenuItem.vue'
export { default as NavigationMenuLink } from './NavigationMenuLink.vue'
export { default as NavigationMenuList } from './NavigationMenuList.vue'
export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.vue'
export { default as NavigationMenuViewport } from './NavigationMenuViewport.vue'
export const navigationMenuTriggerStyle = cva(
'group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50',

View File

@ -19,7 +19,7 @@ const forwardedProps = useForwardProps(delegatedProps)
<SelectTrigger
v-bind="forwardedProps"
:class="cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background 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 [&>span]:truncate text-start',
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
props.class,
)"
>

View File

@ -0,0 +1,85 @@
<script setup lang="ts">
import type { SidebarProps } from '.'
import { Sheet, SheetContent } from '@/lib/registry/default/ui/sheet'
import { cn } from '@/lib/utils'
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'"
:class="cn('flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground', 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-mobile="true"
:side="side"
class="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
:style="{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
}"
>
<div class="flex h-full w-full flex-col">
<slot />
</div>
</SheetContent>
</Sheet>
<div
v-else class="group peer hidden md:block"
: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(
'duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] 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)_+_theme(spacing.4))]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]',
)"
/>
<div
:class="cn(
'duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] 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)_+_theme(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="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
<slot />
</div>
</div>
</div>
</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>
<div
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,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-sidebar="footer"
:class="cn('flex flex-col gap-2 p-2', props.class)"
>
<slot />
</div>
</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>
<div
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,27 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'radix-vue'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive } from 'radix-vue'
const props = defineProps<PrimitiveProps & {
class?: HTMLAttributes['class']
}>()
</script>
<template>
<Primitive
data-sidebar="group-action"
:as="as"
:as-child="asChild"
:class="cn(
'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 after:md:hidden',
'group-data-[collapsible=icon]:hidden',
props.class,
)"
>
<slot />
</Primitive>
</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>
<div
data-sidebar="group-content"
:class="cn('w-full text-sm', props.class)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'radix-vue'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive } from 'radix-vue'
const props = defineProps<PrimitiveProps & {
class?: HTMLAttributes['class']
}>()
</script>
<template>
<Primitive
data-sidebar="group-label"
:as="as"
:as-child="asChild"
:class="cn(
'duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] 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,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-sidebar="header"
:class="cn('flex flex-col gap-2 p-2', props.class)"
>
<slot />
</div>
</template>

View File

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

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<main
:class="cn(
'relative flex min-h-svh flex-1 flex-col bg-background',
'peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
props.class,
)"
>
<slot />
</main>
</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>
<ul
data-sidebar="menu"
:class="cn('flex w-full min-w-0 flex-col gap-1', props.class)"
>
<slot />
</ul>
</template>

View File

@ -0,0 +1,34 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive, type PrimitiveProps } from 'radix-vue'
const props = withDefaults(defineProps<PrimitiveProps & {
showOnHover?: boolean
class?: HTMLAttributes['class']
}>(), {
as: 'button',
})
</script>
<template>
<Primitive
data-sidebar="menu-action"
:class="cn(
'absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 after:md:hidden',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
showOnHover
&& 'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0',
props.class,
)"
:as="as"
:as-child="asChild"
>
<slot />
</Primitive>
</template>

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-sidebar="menu-badge"
:class="cn(
'absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none',
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
props.class,
)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,49 @@
<script setup lang="ts">
import { Tooltip, TooltipContent, TooltipTrigger } from '@/lib/registry/default/ui/tooltip'
import { type Component, computed } from 'vue'
import SidebarMenuButtonChild, { type SidebarMenuButtonProps } from './SidebarMenuButtonChild.vue'
import { useSidebar } from './utils'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<SidebarMenuButtonProps & {
tooltip?: string | Component
}>(), {
as: 'button',
variant: 'default',
size: 'default',
})
const { isMobile, state } = useSidebar()
const delegatedProps = computed(() => {
const { tooltip, ...delegated } = props
return delegated
})
</script>
<template>
<SidebarMenuButtonChild v-if="!tooltip" v-bind="{ ...delegatedProps, ...$attrs }">
<slot />
</SidebarMenuButtonChild>
<Tooltip v-else>
<TooltipTrigger as-child>
<SidebarMenuButtonChild v-bind="{ ...delegatedProps, ...$attrs }">
<slot />
</SidebarMenuButtonChild>
</TooltipTrigger>
<TooltipContent
side="right"
align="center"
:hidden="state !== 'collapsed' || isMobile"
>
<template v-if="typeof tooltip === 'string'">
{{ tooltip }}
</template>
<component :is="tooltip" v-else />
</TooltipContent>
</Tooltip>
</template>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive, type PrimitiveProps } from 'radix-vue'
import { type SidebarMenuButtonVariants, sidebarMenuButtonVariants } from '.'
export interface SidebarMenuButtonProps extends PrimitiveProps {
variant?: SidebarMenuButtonVariants['variant']
size?: SidebarMenuButtonVariants['size']
isActive?: boolean
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<SidebarMenuButtonProps>(), {
as: 'button',
variant: 'default',
size: 'default',
})
</script>
<template>
<Primitive
data-sidebar="menu-button"
:data-size="size"
:data-active="isActive"
:class="cn(sidebarMenuButtonVariants({ variant, size }), props.class)"
:as="as"
:as-child="asChild"
v-bind="$attrs"
>
<slot />
</Primitive>
</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>
<li
data-sidebar="menu-item"
:class="cn('group/menu-item relative', props.class)"
>
<slot />
</li>
</template>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import { Skeleton } from '@/lib/registry/default/ui/skeleton'
import { cn } from '@/lib/utils'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<{
showIcon?: boolean
class?: HTMLAttributes['class']
}>()
const width = computed(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
})
</script>
<template>
<div
data-sidebar="menu-skeleton"
:class="cn('rounded-md h-8 flex gap-2 px-2 items-center', props.class)"
>
<Skeleton
v-if="showIcon"
class="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
<Skeleton
class="h-4 flex-1 max-w-[--skeleton-width]"
data-sidebar="menu-skeleton-text"
:style="{ '--skeleton-width': width }"
/>
</div>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<ul
data-sidebar="menu-badge"
: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',
props.class,
)"
>
<slot />
</ul>
</template>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'radix-vue'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive } from 'radix-vue'
const props = withDefaults(defineProps<PrimitiveProps & {
size?: 'sm' | 'md'
isActive?: boolean
class?: HTMLAttributes['class']
}>(), {
as: 'a',
size: 'md',
})
</script>
<template>
<Primitive
data-sidebar="menu-sub-button"
:as="as"
:as-child="asChild"
:data-size="size"
:data-active="isActive"
:class="cn(
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring 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 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === 'sm' && 'text-xs',
size === 'md' && 'text-sm',
'group-data-[collapsible=icon]:hidden',
props.class,
)"
>
<slot />
</Primitive>
</template>

View File

@ -0,0 +1,8 @@
<script setup lang="ts">
</script>
<template>
<li>
<slot />
</li>
</template>

View File

@ -0,0 +1,80 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { useEventListener, useMediaQuery, useVModel } from '@vueuse/core'
import { TooltipProvider } from 'radix-vue'
import { computed, type HTMLAttributes, type Ref, ref } from 'vue'
import { provideSidebarContext, SIDEBAR_COOKIE_MAX_AGE, SIDEBAR_COOKIE_NAME, SIDEBAR_KEYBOARD_SHORTCUT, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON } from './utils'
const props = withDefaults(defineProps<{
defaultOpen?: boolean
open?: boolean
class?: HTMLAttributes['class']
}>(), {
defaultOpen: true,
open: undefined,
})
const emits = defineEmits<{
'update:open': [open: boolean]
}>()
const isMobile = useMediaQuery('(max-width: 768px)')
const openMobile = ref(false)
const open = useVModel(props, 'open', emits, {
defaultValue: props.defaultOpen ?? false,
passive: (props.open === undefined) as false,
}) as Ref<boolean>
function setOpen(value: boolean) {
open.value = value // emits('update:open', value)
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
}
function setOpenMobile(value: boolean) {
openMobile.value = value
}
// Helper to toggle the sidebar.
function toggleSidebar() {
return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value)
}
useEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault()
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 = computed(() => open.value ? 'expanded' : 'collapsed')
provideSidebarContext({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
})
</script>
<template>
<TooltipProvider :delay-duration="0">
<div
:style="{
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
}"
:class="cn('group/sidebar-wrapper flex min-h-svh w-full text-sidebar-foreground has-[[data-variant=inset]]:bg-sidebar', props.class)"
v-bind="$attrs"
>
<slot />
</div>
</TooltipProvider>
</template>

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { useSidebar } from './utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
const { toggleSidebar } = useSidebar()
</script>
<template>
<button
data-sidebar="rail"
aria-label="Toggle Sidebar"
:tabindex="-1"
title="Toggle Sidebar"
:class="cn(
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
'[[data-side=left]_&]:cursor-w-resize [[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 group-data-[collapsible=offcanvas]:hover:bg-sidebar',
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
props.class,
)"
@click="toggleSidebar"
>
<slot />
</button>
</template>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Separator } from '@/lib/registry/default/ui/separator'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<Separator
data-sidebar="separator"
:class="cn('mx-2 w-auto bg-sidebar-border', props.class)"
>
<slot />
</Separator>
</template>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Button } from '@/lib/registry/default/ui/button'
import { cn } from '@/lib/utils'
import { PanelLeft } from 'lucide-vue-next'
import { useSidebar } from './utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
const { toggleSidebar } = useSidebar()
</script>
<template>
<Button
data-sidebar="trigger"
variant="ghost"
size="icon"
:class="cn('h-7 w-7', props.class)"
@click="toggleSidebar"
>
<PanelLeft />
<span class="sr-only">Toggle Sidebar</span>
</Button>
</template>

View File

@ -0,0 +1,59 @@
import type { HTMLAttributes } from 'vue'
import { cva, type VariantProps } from 'class-variance-authority'
export interface SidebarProps {
side?: 'left' | 'right'
variant?: 'sidebar' | 'floating' | 'inset'
collapsible?: 'offcanvas' | 'icon' | 'none'
class?: HTMLAttributes['class']
}
export { default as Sidebar } from './Sidebar.vue'
export { default as SidebarContent } from './SidebarContent.vue'
export { default as SidebarFooter } from './SidebarFooter.vue'
export { default as SidebarGroup } from './SidebarGroup.vue'
export { default as SidebarGroupAction } from './SidebarGroupAction.vue'
export { default as SidebarGroupContent } from './SidebarGroupContent.vue'
export { default as SidebarGroupLabel } from './SidebarGroupLabel.vue'
export { default as SidebarHeader } from './SidebarHeader.vue'
export { default as SidebarInput } from './SidebarInput.vue'
export { default as SidebarInset } from './SidebarInset.vue'
export { default as SidebarMenu } from './SidebarMenu.vue'
export { default as SidebarMenuAction } from './SidebarMenuAction.vue'
export { default as SidebarMenuBadge } from './SidebarMenuBadge.vue'
export { default as SidebarMenuButton } from './SidebarMenuButton.vue'
export { default as SidebarMenuItem } from './SidebarMenuItem.vue'
export { default as SidebarMenuSkeleton } from './SidebarMenuSkeleton.vue'
export { default as SidebarMenuSub } from './SidebarMenuSub.vue'
export { default as SidebarMenuSubButton } from './SidebarMenuSubButton.vue'
export { default as SidebarMenuSubItem } from './SidebarMenuSubItem.vue'
export { default as SidebarProvider } from './SidebarProvider.vue'
export { default as SidebarRail } from './SidebarRail.vue'
export { default as SidebarSeparator } from './SidebarSeparator.vue'
export { default as SidebarTrigger } from './SidebarTrigger.vue'
export { useSidebar } from './utils'
export const sidebarMenuButtonVariants = cva(
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] 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 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
{
variants: {
variant: {
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
outline:
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
},
size: {
default: 'h-8 text-sm',
sm: 'h-7 text-xs',
lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
export type SidebarMenuButtonVariants = VariantProps<typeof sidebarMenuButtonVariants>

View File

@ -0,0 +1,19 @@
import type { ComputedRef, Ref } from 'vue'
import { createContext } from 'radix-vue'
export const SIDEBAR_COOKIE_NAME = 'sidebar:state'
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
export const SIDEBAR_WIDTH = '16rem'
export const SIDEBAR_WIDTH_MOBILE = '18rem'
export const SIDEBAR_WIDTH_ICON = '3rem'
export const SIDEBAR_KEYBOARD_SHORTCUT = 'b'
export const [useSidebar, provideSidebarContext] = createContext<{
state: ComputedRef<'expanded' | 'collapsed'>
open: Ref<boolean>
setOpen: (value: boolean) => void
isMobile: Ref<boolean>
openMobile: Ref<boolean>
setOpenMobile: (value: boolean) => void
toggleSidebar: () => void
}>('Sidebar')

View File

@ -31,7 +31,9 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
)"
>
<SwitchThumb
:class="cn('pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0')"
/>
:class="cn('pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5')"
>
<slot name="thumb" />
</SwitchThumb>
</SwitchRoot>
</template>

View File

@ -0,0 +1,217 @@
<script lang="ts">
export const iframeHeight = '800px'
export const description
= 'A simple sidebar with navigation grouped by section.'
</script>
<script setup lang="ts">
import { Check, ChevronsUpDown, GalleryVerticalEnd, Search } from 'lucide-vue-next'
import { ref } from 'vue'
// Import components from the custom library
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/lib/registry/new-york/ui/breadcrumb'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/lib/registry/new-york/ui/dropdown-menu'
import { Label } from '@/lib/registry/new-york/ui/label'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from '@/lib/registry/new-york/ui/sidebar'
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: 'Building 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: '#' },
],
},
],
}
const selectedVersion = ref(data.versions[0])
const dropdownOpen = ref(false)
const search = ref('')
function toggleDropdown() {
dropdownOpen.value = !dropdownOpen.value
}
function setSelectedVersion(version: string) {
selectedVersion.value = version
}
</script>
<template>
<SidebarProvider>
<Sidebar>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger>
<SidebarMenuButton
size="lg"
:class="{ 'data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground': dropdownOpen }"
@click="toggleDropdown"
>
<div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<GalleryVerticalEnd class="size-4" />
</div>
<div class="flex flex-col gap-0.5 leading-none">
<span class="font-semibold">Documentation</span>
<span>v{{ selectedVersion }}</span>
</div>
<ChevronsUpDown class="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
v-if="dropdownOpen"
class="w-[--radix-dropdown-menu-trigger-width]"
align="start"
>
<DropdownMenuItem
v-for="version in data.versions"
:key="version"
@click="setSelectedVersion(version)"
>
v{{ version }}
<Check v-if="version === selectedVersion" class="ml-auto" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
<form @submit.prevent>
<SidebarGroup class="py-0">
<SidebarGroupContent class="relative">
<Label for="search" class="sr-only">Search</Label>
<SidebarInput
id="search"
v-model="search"
placeholder="Search the docs..."
class="pl-8"
/>
<Search class="pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-50" />
</SidebarGroupContent>
</SidebarGroup>
</form>
</SidebarHeader>
<SidebarContent>
<SidebarGroup v-for="item in data.navMain" :key="item.title">
<SidebarGroupLabel>{{ item.title }}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem v-for="subItem in item.items" :key="subItem.title">
<SidebarMenuButton :class="{ 'is-active': subItem.isActive }" as-child>
<a :href="subItem.url">{{ subItem.title }}</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarRail />
</Sidebar>
<SidebarInset>
<header class="flex h-16 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger class="-ml-1" />
<Separator orientation="vertical" class="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem class="hidden md:block">
<BreadcrumbLink href="#">
Building Your Application
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator class="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</header>
<div class="flex flex-1 flex-col gap-4 p-4">
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
<div class="aspect-video rounded-xl bg-muted/50" />
<div class="aspect-video rounded-xl bg-muted/50" />
<div class="aspect-video rounded-xl bg-muted/50" />
</div>
<div class="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
</div>
</SidebarInset>
</SidebarProvider>
</template>

View File

@ -0,0 +1,463 @@
<script lang="ts">
export const description
= 'A sidebar that collapses to icons.'
export const iframeHeight = '800px'
export const containerClass = 'w-full h-full'
</script>
<script setup lang=ts>
import {
Avatar,
AvatarFallback,
AvatarImage,
} from '@/lib/registry/new-york/ui/avatar'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/lib/registry/new-york/ui/breadcrumb'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/lib/registry/new-york/ui/collapsible'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/lib/registry/new-york/ui/dropdown-menu'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from '@/lib/registry/new-york/ui/sidebar'
import {
AudioWaveform,
BadgeCheck,
Bell,
BookOpen,
Bot,
ChevronRight,
ChevronsUpDown,
Command,
CreditCard,
Folder,
Forward,
Frame,
GalleryVerticalEnd,
LogOut,
Map,
MoreHorizontal,
PieChart,
Plus,
Settings2,
Sparkles,
SquareTerminal,
Trash2,
} from 'lucide-vue-next'
import { ref } from 'vue'
// This is sample data.
const data = {
user: {
name: 'shadcn',
email: 'm@example.com',
avatar: '/avatars/shadcn.jpg',
},
teams: [
{
name: 'Acme Inc',
logo: GalleryVerticalEnd,
plan: 'Enterprise',
},
{
name: 'Acme Corp.',
logo: AudioWaveform,
plan: 'Startup',
},
{
name: 'Evil Corp.',
logo: Command,
plan: 'Free',
},
],
navMain: [
{
title: 'Playground',
url: '#',
icon: SquareTerminal,
isActive: true,
items: [
{
title: 'History',
url: '#',
},
{
title: 'Starred',
url: '#',
},
{
title: 'Settings',
url: '#',
},
],
},
{
title: 'Models',
url: '#',
icon: Bot,
items: [
{
title: 'Genesis',
url: '#',
},
{
title: 'Explorer',
url: '#',
},
{
title: 'Quantum',
url: '#',
},
],
},
{
title: 'Documentation',
url: '#',
icon: BookOpen,
items: [
{
title: 'Introduction',
url: '#',
},
{
title: 'Get Started',
url: '#',
},
{
title: 'Tutorials',
url: '#',
},
{
title: 'Changelog',
url: '#',
},
],
},
{
title: 'Settings',
url: '#',
icon: Settings2,
items: [
{
title: 'General',
url: '#',
},
{
title: 'Team',
url: '#',
},
{
title: 'Billing',
url: '#',
},
{
title: 'Limits',
url: '#',
},
],
},
],
projects: [
{
name: 'Design Engineering',
url: '#',
icon: Frame,
},
{
name: 'Sales & Marketing',
url: '#',
icon: PieChart,
},
{
name: 'Travel',
url: '#',
icon: Map,
},
],
}
const activeTeam = ref(data.teams[0])
function setActiveTeam(team: typeof data.teams[number]) {
activeTeam.value = team
}
</script>
<template>
<SidebarProvider>
<Sidebar collapsible="icon">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<component :is="activeTeam.logo" class="size-4" />
</div>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ activeTeam.name }}</span>
<span class="truncate text-xs">{{ activeTeam.plan }}</span>
</div>
<ChevronsUpDown class="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="start"
side="bottom"
:side-offset="4"
>
<DropdownMenuLabel class="text-xs text-muted-foreground">
Teams
</DropdownMenuLabel>
<DropdownMenuItem
v-for="(team, index) in data.teams"
:key="team.name"
class="gap-2 p-2"
@click="setActiveTeam(team)"
>
<div class="flex size-6 items-center justify-center rounded-sm border">
<component :is="team.logo" class="size-4 shrink-0" />
</div>
{{ team.name }}
<DropdownMenuShortcut>{{ index + 1 }}</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem class="gap-2 p-2">
<div class="flex size-6 items-center justify-center rounded-md border bg-background">
<Plus class="size-4" />
</div>
<div class="font-medium text-muted-foreground">
Add team
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
<Collapsible
v-for="item in data.navMain"
:key="item.title"
as-child
:default-open="item.isActive"
class="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger as-child>
<SidebarMenuButton :tooltip="item.title">
<component :is="item.icon" />
<span>{{ item.title }}</span>
<ChevronRight class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSubItem
v-for="subItem in item.items"
:key="subItem.title"
>
<SidebarMenuSubButton as-child>
<a :href="subItem.url">
<span>{{ subItem.title }}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
</SidebarMenu>
</SidebarGroup>
<SidebarGroup class="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem
v-for="item in data.projects"
:key="item.name"
>
<SidebarMenuButton as-child>
<a :href="item.url">
<component :is="item.icon" />
<span>{{ item.name }}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuAction show-on-hover>
<MoreHorizontal />
<span class="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-48 rounded-lg" side="bottom" align="end">
<DropdownMenuItem>
<Folder class="text-muted-foreground" />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Forward class="text-muted-foreground" />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 class="text-muted-foreground" />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton class="text-sidebar-foreground/70">
<MoreHorizontal class="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar class="h-8 w-8 rounded-lg">
<AvatarImage :src="data.user.avatar" :alt="data.user.name" />
<AvatarFallback class="rounded-lg">
CN
</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ data.user.name }}</span>
<span class="truncate text-xs">{{ data.user.email }}</span>
</div>
<ChevronsUpDown class="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg" side="bottom" align="end" :side-offset="4">
<DropdownMenuLabel class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar class="h-8 w-8 rounded-lg">
<AvatarImage :src="data.user.avatar" :alt="data.user.name" />
<AvatarFallback class="rounded-lg">
CN
</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ data.user.name }}</span>
<span class="truncate text-xs">{{ data.user.email }}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<SidebarInset>
<header class="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div class="flex items-center gap-2 px-4">
<SidebarTrigger class="-ml-1" />
<Separator orientation="vertical" class="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem class="hidden md:block">
<BreadcrumbLink href="#">
Building Your Application
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator class="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
<div class="aspect-video rounded-xl bg-muted/50" />
<div class="aspect-video rounded-xl bg-muted/50" />
<div class="aspect-video rounded-xl bg-muted/50" />
</div>
<div class="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
</div>
</SidebarInset>
</SidebarProvider>
</template>

View File

@ -42,7 +42,7 @@ const itemsToDisplay = 3
const firstLabel = computed(() => items.value[0]?.label)
const allButLastTwoItems = computed(() => items.value.slice(1, -2))
const remainingItems = computed(() => items.value.slice(-itemsToDisplay + 1))
const remainingItems = computed(() => items.value.slice(-Math.min(itemsToDisplay, items.value.length) + 1))
</script>
<template>

View File

@ -38,7 +38,7 @@ function onSubmit(values: any) {
</script>
<template>
<Form v-slot="{ submitForm }" as="" keep-values :validation-schema="formSchema" @submit="onSubmit">
<Form v-slot="{ handleSubmit }" as="" keep-values :validation-schema="formSchema">
<Dialog>
<DialogTrigger as-child>
<Button variant="outline">
@ -53,7 +53,7 @@ function onSubmit(values: any) {
</DialogDescription>
</DialogHeader>
<form @submit="submitForm">
<form id="dialogForm" @submit="handleSubmit($event, onSubmit)">
<FormField v-slot="{ componentField }" name="username">
<FormItem>
<FormLabel>Username</FormLabel>

View File

@ -12,36 +12,36 @@ import {
const components: { title: string, href: string, description: string }[] = [
{
title: 'Alert Dialog',
href: '/docs/primitives/alert-dialog',
href: '/docs/components/alert-dialog',
description:
'A modal dialog that interrupts the user with important content and expects a response.',
},
{
title: 'Hover Card',
href: '/docs/primitives/hover-card',
href: '/docs/components/hover-card',
description:
'For sighted users to preview content available behind a link.',
},
{
title: 'Progress',
href: '/docs/primitives/progress',
href: '/docs/components/progress',
description:
'Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.',
},
{
title: 'Scroll-area',
href: '/docs/primitives/scroll-area',
href: '/docs/components/scroll-area',
description: 'Visually or semantically separates content.',
},
{
title: 'Tabs',
href: '/docs/primitives/tabs',
href: '/docs/components/tabs',
description:
'A set of layered sections of content—known as tab panels—that are displayed one at a time.',
},
{
title: 'Tooltip',
href: '/docs/primitives/tooltip',
href: '/docs/components/tooltip',
description:
'A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.',
},
@ -72,10 +72,11 @@ const components: { title: string, href: string, description: string }[] = [
</a>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink as-child>
<a
href="/docs"
href="/docs/introduction"
class="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div class="text-sm font-medium leading-none">Introduction</div>
@ -101,7 +102,7 @@ const components: { title: string, href: string, description: string }[] = [
<li>
<NavigationMenuLink as-child>
<a
href="/docs/primitives/typography"
href="/docs/typography"
class="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div class="text-sm font-medium leading-none">Typography</div>
@ -135,7 +136,7 @@ const components: { title: string, href: string, description: string }[] = [
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink href="/docs" :class="navigationMenuTriggerStyle()">
<NavigationMenuLink href="/docs/introduction" :class="navigationMenuTriggerStyle()">
Documentation
</NavigationMenuLink>
</NavigationMenuItem>

View File

@ -1,3 +1,4 @@
import type { InputComponents } from './interface'
import AutoFormFieldArray from './AutoFormFieldArray.vue'
import AutoFormFieldBoolean from './AutoFormFieldBoolean.vue'
import AutoFormFieldDate from './AutoFormFieldDate.vue'
@ -7,7 +8,7 @@ import AutoFormFieldInput from './AutoFormFieldInput.vue'
import AutoFormFieldNumber from './AutoFormFieldNumber.vue'
import AutoFormFieldObject from './AutoFormFieldObject.vue'
export const INPUT_COMPONENTS = {
export const INPUT_COMPONENTS: InputComponents = {
date: AutoFormFieldDate,
select: AutoFormFieldEnum,
radio: AutoFormFieldEnum,

View File

@ -18,6 +18,20 @@ export interface Shape {
schema?: ZodAny
}
export interface InputComponents {
date: Component;
select: Component;
radio: Component;
checkbox: Component;
switch: Component;
textarea: Component;
number: Component;
string: Component;
file: Component;
array: Component;
object: Component;
};
export interface ConfigItem {
/** Value for the `FormLabel` */
label?: string

View File

@ -3,7 +3,7 @@ import { cva, type VariantProps } from 'class-variance-authority'
export { default as Button } from './Button.vue'
export const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {

View File

@ -18,7 +18,7 @@ const forwardedProps = useForwardProps(delegatedProps)
<DropdownMenuItem
v-bind="forwardedProps"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
inset && 'pl-8',
props.class,
)"

View File

@ -6,6 +6,7 @@ export { default as NavigationMenuItem } from './NavigationMenuItem.vue'
export { default as NavigationMenuLink } from './NavigationMenuLink.vue'
export { default as NavigationMenuList } from './NavigationMenuList.vue'
export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.vue'
export { default as NavigationMenuViewport } from './NavigationMenuViewport.vue'
export const navigationMenuTriggerStyle = cva(
'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50',

View File

@ -19,7 +19,7 @@ const forwardedProps = useForwardProps(delegatedProps)
<SelectTrigger
v-bind="forwardedProps"
:class="cn(
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
props.class,
)"
>

View File

@ -0,0 +1,85 @@
<script setup lang="ts">
import type { SidebarProps } from '.'
import { Sheet, SheetContent } from '@/lib/registry/new-york/ui/sheet'
import { cn } from '@/lib/utils'
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'"
:class="cn('flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground', 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-mobile="true"
:side="side"
class="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
:style="{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
}"
>
<div class="flex h-full w-full flex-col">
<slot />
</div>
</SheetContent>
</Sheet>
<div
v-else class="group peer hidden md:block"
: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(
'duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] 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)_+_theme(spacing.4))]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]',
)"
/>
<div
:class="cn(
'duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] 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)_+_theme(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="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
<slot />
</div>
</div>
</div>
</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>
<div
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,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-sidebar="footer"
:class="cn('flex flex-col gap-2 p-2', props.class)"
>
<slot />
</div>
</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>
<div
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,27 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'radix-vue'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive } from 'radix-vue'
const props = defineProps<PrimitiveProps & {
class?: HTMLAttributes['class']
}>()
</script>
<template>
<Primitive
data-sidebar="group-action"
:as="as"
:as-child="asChild"
:class="cn(
'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 after:md:hidden',
'group-data-[collapsible=icon]:hidden',
props.class,
)"
>
<slot />
</Primitive>
</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>
<div
data-sidebar="group-content"
:class="cn('w-full text-sm', props.class)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'radix-vue'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive } from 'radix-vue'
const props = defineProps<PrimitiveProps & {
class?: HTMLAttributes['class']
}>()
</script>
<template>
<Primitive
data-sidebar="group-label"
:as="as"
:as-child="asChild"
:class="cn(
'duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] 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>

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