Skip to content

Commit a03bace

Browse files
authored
feat: Blocks (#428)
* chore: build registry * feat: block preview * refactor: change to use iframe feat: add more blocks * chore: fix build * feat: add all other blocks * feat: add copy button * chore: cleanup
1 parent 8982ec3 commit a03bace

34 files changed

+3886
-233
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script setup lang="ts">
2+
import { announcementConfig } from '../config/site'
3+
import ArrowRightIcon from '~icons/radix-icons/arrow-right'
4+
</script>
5+
6+
<template>
7+
<a
8+
:href="announcementConfig.link"
9+
class="inline-flex items-center rounded-lg bg-muted px-3 py-1 text-sm font-medium"
10+
>
11+
{{ announcementConfig.icon }} <Separator class="mx-2 h-4" orientation="vertical" />
12+
<span class="sm:hidden">{{ announcementConfig.title }}</span>
13+
<span class="hidden sm:inline">
14+
{{ announcementConfig.title }}
15+
</span>
16+
<ArrowRightIcon class="ml-1 h-4 w-4" />
17+
</a>
18+
</template>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script setup lang="ts">
2+
import { useClipboard } from '@vueuse/core'
3+
import { toRefs } from 'vue'
4+
import { CheckIcon, ClipboardIcon } from '@radix-icons/vue'
5+
import { Button } from '@/lib/registry/new-york/ui/button'
6+
import {
7+
Tooltip,
8+
TooltipContent,
9+
TooltipTrigger,
10+
} from '@/lib/registry/new-york/ui/tooltip'
11+
12+
const props = withDefaults(defineProps<{
13+
code?: string
14+
}>(), {
15+
code: '',
16+
})
17+
const { code } = toRefs(props)
18+
19+
const { copy, copied } = useClipboard({ source: code })
20+
</script>
21+
22+
<template>
23+
<Tooltip :delay-duration="100">
24+
<TooltipTrigger as-child>
25+
<Button
26+
size="icon"
27+
variant="outline"
28+
class="h-7 w-7 [&_svg]:size-3.5"
29+
@click="copy()"
30+
>
31+
<span class="sr-only">Copy</span>
32+
<CheckIcon v-if="copied" />
33+
<ClipboardIcon v-else />
34+
</Button>
35+
</TooltipTrigger>
36+
<TooltipContent>Copy code</TooltipContent>
37+
</Tooltip>
38+
</template>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script setup lang="ts">
2+
import { useUrlSearchParams } from '@vueuse/core'
3+
import ComponentLoader from './ComponentLoader.vue'
4+
5+
const params = useUrlSearchParams('hash-params')
6+
</script>
7+
8+
<template>
9+
<div v-if="params.name && params.style" :class="params.containerClass">
10+
<ComponentLoader :key="params.style?.toString()" :name="params.name?.toString()" :type-name="'block'" />
11+
</div>
12+
</template>
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
<script setup lang="ts">
2+
import { CircleHelp, Info, Monitor, Smartphone, Tablet } from 'lucide-vue-next'
3+
import { reactive, ref, watch } from 'vue'
4+
import { codeToHtml } from 'shiki'
5+
import { compileScript, parse, walk } from 'vue/compiler-sfc'
6+
import MagicString from 'magic-string'
7+
import { cssVariables } from '../config/shiki'
8+
import StyleSwitcher from './StyleSwitcher.vue'
9+
import Spinner from './Spinner.vue'
10+
import BlockCopyButton from './BlockCopyButton.vue'
11+
import { useConfigStore } from '@/stores/config'
12+
13+
// import { V0Button } from '@/components/v0-button'
14+
import { Badge } from '@/lib/registry/new-york/ui/badge'
15+
import { Popover, PopoverContent, PopoverTrigger } from '@/lib/registry/new-york/ui/popover'
16+
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/lib/registry/new-york/ui/resizable'
17+
import { Separator } from '@/lib/registry/new-york/ui/separator'
18+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/lib/registry/new-york/ui/tabs'
19+
import { ToggleGroup, ToggleGroupItem } from '@/lib/registry/new-york/ui/toggle-group'
20+
21+
const props = defineProps<{
22+
name: string
23+
}>()
24+
25+
const { style, codeConfig } = useConfigStore()
26+
27+
const isLoading = ref(true)
28+
const tabValue = ref('preview')
29+
const resizableRef = ref<InstanceType<typeof ResizablePanel>>()
30+
31+
const rawString = ref('')
32+
const codeHtml = ref('')
33+
const metadata = reactive({
34+
description: null as string | null,
35+
iframeHeight: null as string | null,
36+
containerClass: null as string | null,
37+
})
38+
39+
function removeScript(code: string) {
40+
const s = new MagicString(code)
41+
const scriptTagRegex = /<script\s+lang="ts"\s*>[\s\S]+?<\/script>/g
42+
let match
43+
// eslint-disable-next-line no-cond-assign
44+
while ((match = scriptTagRegex.exec(code)) !== null) {
45+
const start = match.index
46+
const end = match.index + match[0].length
47+
s.overwrite(start, end, '') // Replace the script tag with an empty string
48+
}
49+
return s.trimStart().toString()
50+
}
51+
52+
function transformImportPath(code: string) {
53+
const s = new MagicString(code)
54+
s.replaceAll(`@/lib/registry/${style.value}`, codeConfig.value.componentsPath)
55+
s.replaceAll(`@/lib/utils`, codeConfig.value.utilsPath)
56+
return s.toString()
57+
}
58+
59+
watch([style, codeConfig], async () => {
60+
try {
61+
const baseRawString = await import(`../../../src/lib/registry/${style.value}/block/${props.name}.vue?raw`).then(res => res.default.trim())
62+
rawString.value = transformImportPath(removeScript(baseRawString))
63+
64+
if (!metadata.description) {
65+
const { descriptor } = parse(baseRawString)
66+
const ast = compileScript(descriptor, { id: '' })
67+
walk(ast.scriptAst, {
68+
enter(node: any) {
69+
const declaration = node.declaration
70+
// Check if the declaration is a variable declaration
71+
if (declaration?.type === 'VariableDeclaration') {
72+
// Extract variable names and their values
73+
declaration.declarations.forEach((decl: any) => {
74+
// @ts-expect-error ignore missing type
75+
metadata[decl.id.name] = decl.init ? decl.init.value : null
76+
})
77+
}
78+
},
79+
})
80+
}
81+
82+
codeHtml.value = await codeToHtml(rawString.value, {
83+
lang: 'vue',
84+
theme: cssVariables,
85+
})
86+
}
87+
catch (err) {
88+
console.error(err)
89+
}
90+
}, { immediate: true, deep: true })
91+
</script>
92+
93+
<template>
94+
<Tabs
95+
:id="name"
96+
v-model="tabValue"
97+
class="relative grid w-full scroll-m-20 gap-4"
98+
:style=" {
99+
'--container-height': metadata.iframeHeight ?? '600px',
100+
}"
101+
>
102+
<div class="flex flex-col items-center gap-4 sm:flex-row">
103+
<div class="flex items-center gap-2">
104+
<TabsList class="hidden sm:flex">
105+
<TabsTrigger value="preview">
106+
Preview
107+
</TabsTrigger>
108+
<TabsTrigger value="code">
109+
Code
110+
</TabsTrigger>
111+
</TabsList>
112+
<div class="hidden items-center gap-2 sm:flex">
113+
<Separator
114+
orientation="vertical"
115+
class="mx-2 hidden h-4 md:flex"
116+
/>
117+
<div class="flex items-center gap-2">
118+
<a :href="`#${name}`">
119+
<Badge variant="outline">{{ name }}</Badge>
120+
</a>
121+
<Popover>
122+
<PopoverTrigger class="hidden text-muted-foreground hover:text-foreground sm:flex">
123+
<Info class="h-3.5 w-3.5" />
124+
<span class="sr-only">Block description</span>
125+
</PopoverTrigger>
126+
<PopoverContent
127+
side="right"
128+
:side-offset="10"
129+
class="text-sm"
130+
>
131+
{{ metadata.description }}
132+
</PopoverContent>
133+
</Popover>
134+
</div>
135+
</div>
136+
</div>
137+
<div class="flex items-center gap-2 pr-[14px] sm:ml-auto">
138+
<div class="hidden h-[28px] items-center gap-1.5 rounded-md border p-[2px] shadow-sm md:flex">
139+
<ToggleGroup
140+
type="single"
141+
default-value="100"
142+
@update:model-value="(value) => {
143+
resizableRef?.resize(parseInt(value))
144+
}"
145+
>
146+
<ToggleGroupItem
147+
value="100"
148+
class="h-[22px] w-[22px] rounded-sm p-0"
149+
>
150+
<Monitor class="h-3.5 w-3.5" />
151+
</ToggleGroupItem>
152+
<ToggleGroupItem
153+
value="60"
154+
class="h-[22px] w-[22px] rounded-sm p-0"
155+
>
156+
<Tablet class="h-3.5 w-3.5" />
157+
</ToggleGroupItem>
158+
<ToggleGroupItem
159+
value="25"
160+
class="h-[22px] w-[22px] rounded-sm p-0"
161+
>
162+
<Smartphone class="h-3.5 w-3.5" />
163+
</ToggleGroupItem>
164+
</ToggleGroup>
165+
</div>
166+
<Separator
167+
orientation="vertical"
168+
class="mx-2 hidden h-4 md:flex"
169+
/>
170+
<StyleSwitcher class="h-7" />
171+
<Popover>
172+
<PopoverTrigger class="hidden text-muted-foreground hover:text-foreground sm:flex">
173+
<CircleHelp class="h-3.5 w-3.5" />
174+
<span class="sr-only">Block description</span>
175+
</PopoverTrigger>
176+
<PopoverContent
177+
side="top"
178+
:side-offset="20"
179+
class="space-y-3 rounded-[0.5rem] text-sm"
180+
>
181+
<p class="font-medium">
182+
What is the difference between the New York and Default style?
183+
</p>
184+
<p>
185+
A style comes with its own set of components, animations,
186+
icons and more.
187+
</p>
188+
<p>
189+
The <span class="font-medium">Default</span> style has
190+
larger inputs, uses lucide-react for icons and
191+
tailwindcss-animate for animations.
192+
</p>
193+
<p>
194+
The <span class="font-medium">New York</span> style ships
195+
with smaller buttons and inputs. It also uses shadows on cards
196+
and buttons.
197+
</p>
198+
</PopoverContent>
199+
</Popover>
200+
<Separator orientation="vertical" class="mx-2 h-4" />
201+
<BlockCopyButton :code="rawString" />
202+
<!-- <V0Button
203+
name="{block.name}"
204+
description="{block.description" || "Edit in v0"}
205+
code="{block.code}"
206+
style="{block.style}"
207+
/> -->
208+
</div>
209+
</div>
210+
<TabsContent
211+
v-show="tabValue === 'preview'"
212+
force-mount
213+
value="preview"
214+
class="relative after:absolute after:inset-0 after:right-3 after:z-0 after:rounded-lg after:bg-muted h-[--container-height] px-0"
215+
>
216+
<ResizablePanelGroup id="block-resizable" direction="horizontal" class="relative z-10">
217+
<ResizablePanel
218+
id="block-resizable-panel-1"
219+
ref="resizableRef"
220+
class="relative rounded-lg border bg-background transition-all "
221+
:default-size="100"
222+
:min-size="25"
223+
>
224+
<div v-if="isLoading" class="flex items-center justify-center h-full">
225+
<Spinner />
226+
</div>
227+
<iframe
228+
v-show="!isLoading"
229+
:src="`/blocks/renderer#name=${name}&style=${style}&containerClass=${encodeURIComponent(metadata.containerClass ?? '')}`"
230+
class="relative z-20 w-full bg-background h-[--container-height]"
231+
@load="isLoading = false"
232+
/>
233+
</ResizablePanel>
234+
<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" />
235+
<ResizablePanel id="block-resizable-panel-2" :default-size="0" :min-size="0" />
236+
</ResizablePanelGroup>
237+
</TabsContent>
238+
<TabsContent value="code" class="h-[--container-height]">
239+
<div
240+
class="language-vue !h-full !max-h-[none] !mt-0"
241+
v-html="codeHtml"
242+
/>
243+
</TabsContent>
244+
</Tabs>
245+
</template>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
import PageHeader from '../components/PageHeader.vue'
4+
import PageHeaderHeading from '../components/PageHeaderHeading.vue'
5+
import PageHeaderDescription from '../components/PageHeaderDescription.vue'
6+
import PageAction from '../components/PageAction.vue'
7+
import Announcement from '../components/Announcement.vue'
8+
import BlockPreview from './BlockPreview.vue'
9+
import GitHubIcon from '~icons/radix-icons/github-logo'
10+
11+
import { buttonVariants } from '@/lib/registry/new-york/ui/button'
12+
import { cn } from '@/lib/utils'
13+
14+
const blocks = ref<string[]>([])
15+
16+
import('../../../__registry__/index').then((res) => {
17+
blocks.value = Object.values(res.Index.default).filter(i => i.type === 'components:block').map(i => i.name)
18+
})
19+
</script>
20+
21+
<template>
22+
<PageHeader class="page-header pb-8">
23+
<Announcement />
24+
<PageHeaderHeading>Building Blocks for the Web</PageHeaderHeading>
25+
<PageHeaderDescription>
26+
Beautifully designed. Copy and paste into your apps. Open Source.
27+
</PageHeaderDescription>
28+
29+
<PageAction>
30+
<a
31+
href="/blocks.html#blocks"
32+
:class="cn(buttonVariants(), 'rounded-[6px]')"
33+
>
34+
Browse
35+
</a>
36+
<a
37+
href="https://github.com/radix-vue/shadcn-vue"
38+
target="_blank"
39+
:class="cn(
40+
buttonVariants({ variant: 'outline' }),
41+
'rounded-[6px]',
42+
)"
43+
>
44+
<GitHubIcon class="mr-2 h-4 w-4" />
45+
GitHub
46+
</a>
47+
</PageAction>
48+
</PageHeader>
49+
50+
<section id="blocks" class="grid scroll-mt-24 gap-24 lg:gap-48">
51+
<BlockPreview v-for="block in blocks" :key="block" :name="block" />
52+
</section>
53+
</template>

apps/www/.vitepress/theme/components/ComponentLoader.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import { useConfigStore } from '@/stores/config'
55
66
const props = defineProps<{
77
name: string
8+
typeName?: 'example' | 'block'
89
}>()
910
const { style } = useConfigStore()
1011
1112
const Component = defineAsyncComponent({
1213
loadingComponent: Spinner,
13-
loader: () => import(`../../../src/lib/registry/${style.value}/example/${props.name}.vue`),
14+
loader: () => import(`../../../src/lib/registry/${style.value}/${props.typeName}/${props.name}.vue`),
1415
timeout: 5000,
1516
})
1617
</script>

apps/www/.vitepress/theme/components/ComponentPreview.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ watch([style, codeConfig], async () => {
8383
'items-end': align === 'end',
8484
})"
8585
>
86-
<ComponentLoader v-bind="$attrs" :key="style" :name="name" />
86+
<ComponentLoader v-bind="$attrs" :key="style" :name="name" :type-name="'example'" />
8787
</div>
8888
</TabsContent>
8989
<TabsContent value="code">

0 commit comments

Comments
 (0)