Merge remote-tracking branch 'origin/dev' into pr/selemondev/821
|
|
@ -4,7 +4,6 @@ import autoprefixer from 'autoprefixer'
|
||||||
import tailwind from 'tailwindcss'
|
import tailwind from 'tailwindcss'
|
||||||
import Icons from 'unplugin-icons/vite'
|
import Icons from 'unplugin-icons/vite'
|
||||||
import { defineConfig } from 'vitepress'
|
import { defineConfig } from 'vitepress'
|
||||||
import { cssVariables } from './theme/config/shiki'
|
|
||||||
|
|
||||||
import { siteConfig } from './theme/config/site'
|
import { siteConfig } from './theme/config/site'
|
||||||
import CodeWrapperPlugin from './theme/plugins/codewrapper'
|
import CodeWrapperPlugin from './theme/plugins/codewrapper'
|
||||||
|
|
@ -31,7 +30,6 @@ export default defineConfig({
|
||||||
['meta', { name: 'og:site_name', content: siteConfig.name }],
|
['meta', { name: 'og:site_name', content: siteConfig.name }],
|
||||||
['meta', { name: 'og:image', content: siteConfig.ogImage }],
|
['meta', { name: 'og:image', content: siteConfig.ogImage }],
|
||||||
['meta', { name: 'twitter:image', content: siteConfig.ogImage }],
|
['meta', { name: 'twitter:image', content: siteConfig.ogImage }],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
sitemap: {
|
sitemap: {
|
||||||
|
|
@ -58,7 +56,6 @@ export default defineConfig({
|
||||||
|
|
||||||
srcDir: path.resolve(__dirname, '../src'),
|
srcDir: path.resolve(__dirname, '../src'),
|
||||||
markdown: {
|
markdown: {
|
||||||
theme: cssVariables,
|
|
||||||
codeTransformers: [
|
codeTransformers: [
|
||||||
transformerMetaWordHighlight(),
|
transformerMetaWordHighlight(),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
233
apps/www/.vitepress/theme/components/BlockContainer.vue
Normal 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>
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
import { useUrlSearchParams } from '@vueuse/core'
|
import { useUrlSearchParams } from '@vueuse/core'
|
||||||
import ComponentLoader from './ComponentLoader.vue'
|
import ComponentLoader from './ComponentLoader.vue'
|
||||||
|
|
||||||
const params = useUrlSearchParams('hash-params')
|
const params = useUrlSearchParams('history')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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'" />
|
<ComponentLoader :key="params.style?.toString()" :name="params.name?.toString()" :type-name="'block'" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,245 +1,44 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { computed, ref } from 'vue'
|
||||||
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 Spinner from './Spinner.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<{
|
const props = defineProps<{
|
||||||
name: string
|
name: string
|
||||||
|
styles?: string
|
||||||
|
containerClass?: string
|
||||||
|
container?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { style, codeConfig } = useConfigStore()
|
|
||||||
|
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
const tabValue = ref('preview')
|
|
||||||
const resizableRef = ref<InstanceType<typeof ResizablePanel>>()
|
|
||||||
|
|
||||||
const rawString = ref('')
|
const iframeURL = computed(() => {
|
||||||
const codeHtml = ref('')
|
// @ts-expect-error env available in import.meta
|
||||||
const metadata = reactive({
|
if (import.meta.env.SSR)
|
||||||
description: null as string | null,
|
return ''
|
||||||
iframeHeight: null as string | null,
|
|
||||||
containerClass: null as string | null,
|
const url = new URL(`${window.location.origin}/blocks/renderer`)
|
||||||
|
Object.entries(props).forEach(([key, value]) => {
|
||||||
|
if (value)
|
||||||
|
url.searchParams.append(key, value as string)
|
||||||
|
})
|
||||||
|
return url.href
|
||||||
})
|
})
|
||||||
|
|
||||||
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 = await codeToHtml(rawString.value, {
|
|
||||||
lang: 'vue',
|
|
||||||
theme: cssVariables,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}, { immediate: true, deep: true })
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Tabs
|
<div class="relative rounded-lg border overflow-hidden bg-background" :class="[container ? '' : 'aspect-[4/2.5]']">
|
||||||
:id="name"
|
<div v-if="isLoading" class="flex items-center justify-center h-full">
|
||||||
v-model="tabValue"
|
<Spinner />
|
||||||
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>
|
</div>
|
||||||
<TabsContent
|
<div
|
||||||
v-show="tabValue === 'preview'"
|
:class="[container ? 'w-full' : 'absolute inset-0 hidden w-[1600px] bg-background md:block']"
|
||||||
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">
|
<iframe
|
||||||
<ResizablePanel
|
v-show="!isLoading"
|
||||||
id="block-resizable-panel-1"
|
:src="iframeURL"
|
||||||
ref="resizableRef"
|
class="relative z-20 w-full bg-background" :class="[container ? 'h-[--container-height]' : 'size-full']"
|
||||||
class="relative rounded-lg border bg-background transition-all "
|
@load="isLoading = false"
|
||||||
:default-size="100"
|
|
||||||
:min-size="30"
|
|
||||||
>
|
|
||||||
<div v-if="isLoading" class="flex items-center justify-center h-full">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
<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]"
|
|
||||||
@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>
|
</div>
|
||||||
</Tabs>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import PageHeader from '../components/PageHeader.vue'
|
||||||
import PageHeaderDescription from '../components/PageHeaderDescription.vue'
|
import PageHeaderDescription from '../components/PageHeaderDescription.vue'
|
||||||
|
|
||||||
import PageHeaderHeading from '../components/PageHeaderHeading.vue'
|
import PageHeaderHeading from '../components/PageHeaderHeading.vue'
|
||||||
import BlockPreview from './BlockPreview.vue'
|
import BlockContainer from './BlockContainer.vue'
|
||||||
|
|
||||||
const blocks = ref<string[]>([])
|
const blocks = ref<string[]>([])
|
||||||
|
|
||||||
|
|
@ -48,6 +48,6 @@ import('../../../__registry__/index').then((res) => {
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<section id="blocks" class="grid scroll-mt-24 gap-24 lg:gap-48">
|
<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>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,8 @@ import { cn } from '@/lib/utils'
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
import { useClipboard } from '@vueuse/core'
|
import { useClipboard } from '@vueuse/core'
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
import { codeToHtml } from 'shiki'
|
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { cssVariables } from '../config/shiki'
|
import { highlight } from '../config/shiki'
|
||||||
import CodeSandbox from './CodeSandbox.vue'
|
import CodeSandbox from './CodeSandbox.vue'
|
||||||
import ComponentLoader from './ComponentLoader.vue'
|
import ComponentLoader from './ComponentLoader.vue'
|
||||||
import Stackblitz from './Stackblitz.vue'
|
import Stackblitz from './Stackblitz.vue'
|
||||||
|
|
@ -37,10 +36,7 @@ function transformImportPath(code: string) {
|
||||||
watch([style, codeConfig], async () => {
|
watch([style, codeConfig], async () => {
|
||||||
try {
|
try {
|
||||||
rawString.value = await import(`../../../src/lib/registry/${style.value}/example/${props.name}.vue?raw`).then(res => res.default.trim())
|
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, {
|
codeHtml.value = highlight(transformedRawString.value, 'vue')
|
||||||
lang: 'vue',
|
|
||||||
theme: cssVariables,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export { default as APITable } from './APITable.vue'
|
export { default as APITable } from './APITable.vue'
|
||||||
|
export { default as BlockPreview } from './BlockPreview.vue'
|
||||||
export { default as Callout } from './Callout.vue'
|
export { default as Callout } from './Callout.vue'
|
||||||
export { default as CodeWrapper } from './CodeWrapper'
|
export { default as CodeWrapper } from './CodeWrapper'
|
||||||
export { default as ComponentPreview } from './ComponentPreview.vue'
|
export { default as ComponentPreview } from './ComponentPreview.vue'
|
||||||
|
|
|
||||||
|
|
@ -128,12 +128,10 @@ export const docsConfig: DocsConfig = {
|
||||||
title: 'Auto Form',
|
title: 'Auto Form',
|
||||||
href: '/docs/components/auto-form',
|
href: '/docs/components/auto-form',
|
||||||
items: [],
|
items: [],
|
||||||
label: 'New',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Charts',
|
title: 'Charts',
|
||||||
href: '/docs/charts',
|
href: '/docs/charts',
|
||||||
label: 'New Alpha',
|
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -141,6 +139,11 @@ export const docsConfig: DocsConfig = {
|
||||||
{
|
{
|
||||||
title: 'Components',
|
title: 'Components',
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
title: 'Sidebar',
|
||||||
|
href: '/docs/components/sidebar',
|
||||||
|
label: 'New',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Accordion',
|
title: 'Accordion',
|
||||||
href: '/docs/components/accordion',
|
href: '/docs/components/accordion',
|
||||||
|
|
@ -178,7 +181,6 @@ export const docsConfig: DocsConfig = {
|
||||||
title: 'Calendar',
|
title: 'Calendar',
|
||||||
href: '/docs/components/calendar',
|
href: '/docs/components/calendar',
|
||||||
items: [],
|
items: [],
|
||||||
label: 'Updated',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Card',
|
title: 'Card',
|
||||||
|
|
@ -217,7 +219,6 @@ export const docsConfig: DocsConfig = {
|
||||||
title: 'Date Picker',
|
title: 'Date Picker',
|
||||||
href: '/docs/components/date-picker',
|
href: '/docs/components/date-picker',
|
||||||
items: [],
|
items: [],
|
||||||
label: 'Updated',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Dialog',
|
title: 'Dialog',
|
||||||
|
|
@ -259,7 +260,6 @@ export const docsConfig: DocsConfig = {
|
||||||
{
|
{
|
||||||
title: 'Number Field',
|
title: 'Number Field',
|
||||||
href: '/docs/components/number-field',
|
href: '/docs/components/number-field',
|
||||||
label: 'New Alpha',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Pagination',
|
title: 'Pagination',
|
||||||
|
|
@ -324,7 +324,6 @@ export const docsConfig: DocsConfig = {
|
||||||
{
|
{
|
||||||
title: 'Stepper',
|
title: 'Stepper',
|
||||||
href: '/docs/components/stepper',
|
href: '/docs/components/stepper',
|
||||||
label: 'New',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Switch',
|
title: 'Switch',
|
||||||
|
|
|
||||||
|
|
@ -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({
|
export const shikiThemes: ThemeOptions = {
|
||||||
variablePrefix: '--shiki-',
|
light: 'github-light-default',
|
||||||
variableDefaults: {},
|
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',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ const toggleDark = useToggle(isDark)
|
||||||
const links = [
|
const links = [
|
||||||
{
|
{
|
||||||
name: 'GitHub',
|
name: 'GitHub',
|
||||||
href: 'https://github.com/radix-vue/shadcn-vue',
|
href: 'https://github.com/unovue/shadcn-vue',
|
||||||
icon: RadixIconsGithubLogo,
|
icon: RadixIconsGithubLogo,
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
|
|
@ -220,7 +220,7 @@ watch(() => $route.path, (n) => {
|
||||||
<span class="inline-block ml-2">
|
<span class="inline-block ml-2">
|
||||||
The code source is available on
|
The code source is available on
|
||||||
<a
|
<a
|
||||||
href="https://github.com/radix-vue/shadcn-vue"
|
href="https://github.com/unovue/shadcn-vue"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="underline underline-offset-4 font-bold decoration-foreground"
|
class="underline underline-offset-4 font-bold decoration-foreground"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,16 @@
|
||||||
--ring: 240 5% 64.9%;
|
--ring: 240 5% 64.9%;
|
||||||
--radius: 0.5rem;
|
--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-primary-color: var(--primary);
|
||||||
--vis-secondary-color: 160 81% 40%;
|
--vis-secondary-color: 160 81% 40%;
|
||||||
--vis-text-color: var(--muted-foreground);
|
--vis-text-color: var(--muted-foreground);
|
||||||
|
|
@ -64,6 +74,15 @@
|
||||||
--border: 240 3.7% 15.9%;
|
--border: 240 3.7% 15.9%;
|
||||||
--input: 240 3.7% 15.9%;
|
--input: 240 3.7% 15.9%;
|
||||||
--ring: 240 4.9% 83.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%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -97,19 +116,6 @@
|
||||||
src: url("/fonts/Geist/GeistVariableVF.woff2") format("woff2");
|
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 */
|
/* Firefox */
|
||||||
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
|
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
|
||||||
|
|
@ -121,14 +127,14 @@
|
||||||
scrollbar-color: hsl(215.4 16.3% 56.9% / 0.3);
|
scrollbar-color: hsl(215.4 16.3% 56.9% / 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-scrollbar::-webkit-scrollbar {
|
html.dark .shiki,
|
||||||
display: none;
|
html.dark .shiki span {
|
||||||
}
|
color: var(--shiki-dark);
|
||||||
|
}
|
||||||
.hide-scrollbar {
|
html:not(.dark) .shiki,
|
||||||
-ms-overflow-style: none;
|
html:not(.dark) .shiki span {
|
||||||
scrollbar-width: none;
|
color: var(--shiki-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.antialised {
|
.antialised {
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
|
@ -157,7 +163,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
div[class^="language-"] {
|
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 {
|
pre {
|
||||||
@apply py-4;
|
@apply py-4;
|
||||||
|
|
|
||||||
|
|
@ -1501,6 +1501,20 @@ export const Index = {
|
||||||
component: () => import("../src/lib/registry/default/block/Dashboard07.vue").then((m) => m.default),
|
component: () => import("../src/lib/registry/default/block/Dashboard07.vue").then((m) => m.default),
|
||||||
files: ["../src/lib/registry/default/block/Dashboard07.vue"],
|
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": {
|
}, "new-york": {
|
||||||
"AccordionDemo": {
|
"AccordionDemo": {
|
||||||
name: "AccordionDemo",
|
name: "AccordionDemo",
|
||||||
|
|
@ -3000,5 +3014,19 @@ export const Index = {
|
||||||
component: () => import("../src/lib/registry/new-york/block/Dashboard07.vue").then((m) => m.default),
|
component: () => import("../src/lib/registry/new-york/block/Dashboard07.vue").then((m) => m.default),
|
||||||
files: ["../src/lib/registry/new-york/block/Dashboard07.vue"],
|
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"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,11 +68,11 @@
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"pathe": "^1.1.2",
|
"pathe": "^1.1.2",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"shiki": "^1.17.7",
|
"shiki": "^1.22.1",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss": "^3.4.12",
|
"tailwindcss": "^3.4.12",
|
||||||
"tsx": "^4.19.1",
|
"tsx": "^4.19.1",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "catalog:",
|
||||||
"unplugin-icons": "^0.19.3",
|
"unplugin-icons": "^0.19.3",
|
||||||
"vitepress": "^1.3.4",
|
"vitepress": "^1.3.4",
|
||||||
"vue-component-meta": "^2.1.6",
|
"vue-component-meta": "^2.1.6",
|
||||||
|
|
|
||||||
|
|
@ -80,10 +80,17 @@ for (const style of styles) {
|
||||||
continue
|
continue
|
||||||
|
|
||||||
const files = item.files?.map((file) => {
|
const files = item.files?.map((file) => {
|
||||||
let content = fs.readFileSync(
|
let content: string = ''
|
||||||
path.join(process.cwd(), 'src/lib/registry', style.name, file),
|
|
||||||
'utf8',
|
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
|
// Replace Windows-style newlines with Unix-style newlines
|
||||||
content = content.replace(/\r\n/g, newLine)
|
content = content.replace(/\r\n/g, newLine)
|
||||||
|
|
@ -99,7 +106,7 @@ for (const style of styles) {
|
||||||
files,
|
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(
|
fs.writeFileSync(
|
||||||
path.join(targetPath, `${item.name}.json`),
|
path.join(targetPath, `${item.name}.json`),
|
||||||
|
|
|
||||||
12
apps/www/src/content/docs/components/sidebar.md
Normal 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
|
||||||
|
```
|
||||||
|
|
@ -48,6 +48,19 @@ import { Switch } from '@/components/ui/switch'
|
||||||
</template>
|
</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
|
## Examples
|
||||||
|
|
||||||
### Form
|
### Form
|
||||||
|
|
|
||||||
|
|
@ -44,28 +44,32 @@ Install `tailwindcss` and its peer dependencies, then generate your `tailwind.co
|
||||||
|
|
||||||
#### `vite.config`
|
#### `vite.config`
|
||||||
|
|
||||||
```typescript {5,6,9-13}
|
```typescript {3,4,11-15}
|
||||||
import path from 'node:path'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
import autoprefixer from 'autoprefixer'
|
|
||||||
|
|
||||||
import tailwind from 'tailwindcss'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import { defineConfig } from 'vite'
|
import autoprefixer from 'autoprefixer'
|
||||||
|
|
||||||
export default defineConfig({
|
import tailwind from 'tailwindcss'
|
||||||
css: {
|
import { defineConfig } from 'vite'
|
||||||
postcss: {
|
|
||||||
plugins: [tailwind(), autoprefixer()],
|
// https://vite.dev/config/
|
||||||
},
|
export default defineConfig({
|
||||||
|
css: {
|
||||||
|
postcss: {
|
||||||
|
plugins: [tailwind(), autoprefixer()],
|
||||||
},
|
},
|
||||||
plugins: [vue()],
|
},
|
||||||
resolve: {
|
plugins: [
|
||||||
alias: {
|
vue(),
|
||||||
'@': path.resolve(__dirname, './src'),
|
],
|
||||||
},
|
resolve: {
|
||||||
},
|
alias: {
|
||||||
})
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
```
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
</TabMarkdown>
|
</TabMarkdown>
|
||||||
|
|
||||||
|
|
@ -121,26 +125,30 @@ Add the code below to the vite.config.ts so your app can resolve paths without e
|
||||||
npm i -D @types/node
|
npm i -D @types/node
|
||||||
```
|
```
|
||||||
|
|
||||||
```typescript {15-19}
|
```typescript {20-22}
|
||||||
import path from 'node:path'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import autoprefixer from 'autoprefixer'
|
import autoprefixer from 'autoprefixer'
|
||||||
|
|
||||||
import tailwind from 'tailwindcss'
|
import tailwind from 'tailwindcss'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
css: {
|
css: {
|
||||||
postcss: {
|
postcss: {
|
||||||
plugins: [tailwind(), autoprefixer()],
|
plugins: [tailwind(), autoprefixer()],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [vue()],
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src'),
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
217
apps/www/src/lib/registry/default/block/Sidebar01.vue
Normal 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>
|
||||||
463
apps/www/src/lib/registry/default/block/Sidebar07.vue
Normal 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>
|
||||||
|
|
@ -12,7 +12,7 @@ const props = defineProps<{
|
||||||
<li
|
<li
|
||||||
role="presentation"
|
role="presentation"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
:class="cn('[&>svg]:size-3.5', props.class)"
|
:class="cn('[&>svg]:w-3.5 [&>svg]:h-3.5', props.class)"
|
||||||
>
|
>
|
||||||
<slot>
|
<slot>
|
||||||
<ChevronRight />
|
<ChevronRight />
|
||||||
|
|
|
||||||
|
|
@ -3,26 +3,26 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
export { default as Button } from './Button.vue'
|
export { default as Button } from './Button.vue'
|
||||||
|
|
||||||
export const buttonVariants = cva(
|
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 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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
default:
|
||||||
|
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||||
destructive:
|
destructive:
|
||||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||||
outline:
|
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:
|
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',
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
link: 'text-primary underline-offset-4 hover:underline',
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: 'h-10 px-4 py-2',
|
default: 'h-9 px-4 py-2',
|
||||||
xs: 'h-7 rounded px-2',
|
sm: 'h-8 rounded-md px-3 text-xs',
|
||||||
sm: 'h-9 rounded-md px-3',
|
lg: 'h-10 rounded-md px-8',
|
||||||
lg: 'h-11 rounded-md px-8',
|
icon: 'h-9 w-9',
|
||||||
icon: 'h-10 w-10',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ const forwardedProps = useForwardProps(delegatedProps)
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
v-bind="forwardedProps"
|
v-bind="forwardedProps"
|
||||||
:class="cn(
|
: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',
|
inset && 'pl-8',
|
||||||
props.class,
|
props.class,
|
||||||
)"
|
)"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export { default as NavigationMenuItem } from './NavigationMenuItem.vue'
|
||||||
export { default as NavigationMenuLink } from './NavigationMenuLink.vue'
|
export { default as NavigationMenuLink } from './NavigationMenuLink.vue'
|
||||||
export { default as NavigationMenuList } from './NavigationMenuList.vue'
|
export { default as NavigationMenuList } from './NavigationMenuList.vue'
|
||||||
export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.vue'
|
export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.vue'
|
||||||
|
export { default as NavigationMenuViewport } from './NavigationMenuViewport.vue'
|
||||||
|
|
||||||
export const navigationMenuTriggerStyle = cva(
|
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',
|
'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',
|
||||||
|
|
|
||||||
90
apps/www/src/lib/registry/default/ui/sidebar/Sidebar.vue
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import Sheet from '@/lib/registry/default/ui/sheet/Sheet.vue'
|
||||||
|
import SheetContent from '@/lib/registry/default/ui/sheet/SheetContent.vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { SIDEBAR_WIDTH_MOBILE, useSidebar } from './utils'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
side?: 'left' | 'right'
|
||||||
|
variant?: 'sidebar' | 'floating' | 'inset'
|
||||||
|
collapsible?: 'offcanvas' | 'icon' | 'none'
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>(), {
|
||||||
|
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"
|
||||||
|
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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import Input from '@/lib/registry/default/ui/input/Input.vue'
|
||||||
|
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>
|
||||||
|
|
@ -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>
|
||||||
17
apps/www/src/lib/registry/default/ui/sidebar/SidebarMenu.vue
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Tooltip from '@/lib/registry/default/ui/tooltip/Tooltip.vue'
|
||||||
|
import TooltipContent from '@/lib/registry/default/ui/tooltip/TooltipContent.vue'
|
||||||
|
import TooltipTrigger from '@/lib/registry/default/ui/tooltip/TooltipTrigger.vue'
|
||||||
|
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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Skeleton from '@/lib/registry/default/ui/skeleton/Skeleton.vue'
|
||||||
|
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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li>
|
||||||
|
<slot />
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useEventListener, 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 = ref(false) // useIsMobile()
|
||||||
|
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(!open.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)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</template>
|
||||||
32
apps/www/src/lib/registry/default/ui/sidebar/SidebarRail.vue
Normal 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>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import Separator from '@/lib/registry/default/ui/separator/Separator.vue'
|
||||||
|
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>
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import Button from '@/lib/registry/default/ui/button/Button.vue'
|
||||||
|
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>
|
||||||
51
apps/www/src/lib/registry/default/ui/sidebar/index.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
|
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>
|
||||||
19
apps/www/src/lib/registry/default/ui/sidebar/utils.ts
Normal 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')
|
||||||
|
|
@ -31,7 +31,9 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
)"
|
)"
|
||||||
>
|
>
|
||||||
<SwitchThumb
|
<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>
|
</SwitchRoot>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
217
apps/www/src/lib/registry/new-york/block/Sidebar01.vue
Normal 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>
|
||||||
463
apps/www/src/lib/registry/new-york/block/Sidebar07.vue
Normal 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>
|
||||||
|
|
@ -18,7 +18,7 @@ const forwardedProps = useForwardProps(delegatedProps)
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
v-bind="forwardedProps"
|
v-bind="forwardedProps"
|
||||||
:class="cn(
|
: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',
|
inset && 'pl-8',
|
||||||
props.class,
|
props.class,
|
||||||
)"
|
)"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export { default as NavigationMenuItem } from './NavigationMenuItem.vue'
|
||||||
export { default as NavigationMenuLink } from './NavigationMenuLink.vue'
|
export { default as NavigationMenuLink } from './NavigationMenuLink.vue'
|
||||||
export { default as NavigationMenuList } from './NavigationMenuList.vue'
|
export { default as NavigationMenuList } from './NavigationMenuList.vue'
|
||||||
export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.vue'
|
export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.vue'
|
||||||
|
export { default as NavigationMenuViewport } from './NavigationMenuViewport.vue'
|
||||||
|
|
||||||
export const navigationMenuTriggerStyle = cva(
|
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',
|
'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',
|
||||||
|
|
|
||||||
90
apps/www/src/lib/registry/new-york/ui/sidebar/Sidebar.vue
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import Sheet from '@/lib/registry/new-york/ui/sheet/Sheet.vue'
|
||||||
|
import SheetContent from '@/lib/registry/new-york/ui/sheet/SheetContent.vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { SIDEBAR_WIDTH_MOBILE, useSidebar } from './utils'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
side?: 'left' | 'right'
|
||||||
|
variant?: 'sidebar' | 'floating' | 'inset'
|
||||||
|
collapsible?: 'offcanvas' | 'icon' | 'none'
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>(), {
|
||||||
|
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"
|
||||||
|
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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import Input from '@/lib/registry/new-york/ui/input/Input.vue'
|
||||||
|
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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Tooltip from '@/lib/registry/new-york/ui/tooltip/Tooltip.vue'
|
||||||
|
import TooltipContent from '@/lib/registry/new-york/ui/tooltip/TooltipContent.vue'
|
||||||
|
import TooltipTrigger from '@/lib/registry/new-york/ui/tooltip/TooltipTrigger.vue'
|
||||||
|
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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Skeleton from '@/lib/registry/new-york/ui/skeleton/Skeleton.vue'
|
||||||
|
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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li>
|
||||||
|
<slot />
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useEventListener, 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 = ref(false) // useIsMobile()
|
||||||
|
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(!open.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)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</template>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import Separator from '@/lib/registry/new-york/ui/separator/Separator.vue'
|
||||||
|
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>
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import Button from '@/lib/registry/new-york/ui/button/Button.vue'
|
||||||
|
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>
|
||||||
51
apps/www/src/lib/registry/new-york/ui/sidebar/index.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
|
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>
|
||||||
19
apps/www/src/lib/registry/new-york/ui/sidebar/utils.ts
Normal 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')
|
||||||
|
|
@ -32,6 +32,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
>
|
>
|
||||||
<SwitchThumb
|
<SwitchThumb
|
||||||
:class="cn('pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0')"
|
:class="cn('pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0')"
|
||||||
/>
|
>
|
||||||
|
<slot name="thumb" />
|
||||||
|
</SwitchThumb>
|
||||||
</SwitchRoot>
|
</SwitchRoot>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 49 KiB |
BIN
apps/www/src/public/images/blocks/demo-sidebar-controlled.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
apps/www/src/public/images/blocks/demo-sidebar-dark.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
apps/www/src/public/images/blocks/demo-sidebar-footer-dark.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
apps/www/src/public/images/blocks/demo-sidebar-footer.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 36 KiB |
BIN
apps/www/src/public/images/blocks/demo-sidebar-group-action.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 29 KiB |
BIN
apps/www/src/public/images/blocks/demo-sidebar-group-dark.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
apps/www/src/public/images/blocks/demo-sidebar-group.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
apps/www/src/public/images/blocks/demo-sidebar-header-dark.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
apps/www/src/public/images/blocks/demo-sidebar-header.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 46 KiB |
BIN
apps/www/src/public/images/blocks/demo-sidebar-menu-action.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 48 KiB |
BIN
apps/www/src/public/images/blocks/demo-sidebar-menu-badge.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 47 KiB |