Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Blocks #428

Merged
merged 7 commits into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/www/.vitepress/theme/components/Announcement.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script setup lang="ts">
import { announcementConfig } from '../config/site'
import ArrowRightIcon from '~icons/radix-icons/arrow-right'
</script>

<template>
<a
:href="announcementConfig.link"
class="inline-flex items-center rounded-lg bg-muted px-3 py-1 text-sm font-medium"
>
{{ announcementConfig.icon }} <Separator class="mx-2 h-4" orientation="vertical" />
<span class="sm:hidden">{{ announcementConfig.title }}</span>
<span class="hidden sm:inline">
{{ announcementConfig.title }}
</span>
<ArrowRightIcon class="ml-1 h-4 w-4" />
</a>
</template>
38 changes: 38 additions & 0 deletions apps/www/.vitepress/theme/components/BlockCopyButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
import { useClipboard } from '@vueuse/core'
import { toRefs } from 'vue'
import { CheckIcon, ClipboardIcon } from '@radix-icons/vue'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/lib/registry/new-york/ui/tooltip'

const props = withDefaults(defineProps<{
code?: string
}>(), {
code: '',
})
const { code } = toRefs(props)

const { copy, copied } = useClipboard({ source: code })
</script>

<template>
<Tooltip :delay-duration="100">
<TooltipTrigger as-child>
<Button
size="icon"
variant="outline"
class="h-7 w-7 [&_svg]:size-3.5"
@click="copy()"
>
<span class="sr-only">Copy</span>
<CheckIcon v-if="copied" />
<ClipboardIcon v-else />
</Button>
</TooltipTrigger>
<TooltipContent>Copy code</TooltipContent>
</Tooltip>
</template>
12 changes: 12 additions & 0 deletions apps/www/.vitepress/theme/components/BlockPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script setup lang="ts">
import { useUrlSearchParams } from '@vueuse/core'
import ComponentLoader from './ComponentLoader.vue'

const params = useUrlSearchParams('hash-params')
</script>

<template>
<div v-if="params.name && params.style" :class="params.containerClass">
<ComponentLoader :key="params.style?.toString()" :name="params.name?.toString()" :type-name="'block'" />
</div>
</template>
245 changes: 245 additions & 0 deletions apps/www/.vitepress/theme/components/BlockPreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
<script setup lang="ts">
import { CircleHelp, Info, Monitor, Smartphone, Tablet } from 'lucide-vue-next'
import { reactive, ref, watch } from 'vue'
import { codeToHtml } from 'shiki'
import { compileScript, parse, walk } from 'vue/compiler-sfc'
import MagicString from 'magic-string'
import { cssVariables } from '../config/shiki'
import StyleSwitcher from './StyleSwitcher.vue'
import Spinner from './Spinner.vue'
import BlockCopyButton from './BlockCopyButton.vue'
import { useConfigStore } from '@/stores/config'

// import { V0Button } from '@/components/v0-button'
import { Badge } from '@/lib/registry/new-york/ui/badge'
import { Popover, PopoverContent, PopoverTrigger } from '@/lib/registry/new-york/ui/popover'
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/lib/registry/new-york/ui/resizable'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/lib/registry/new-york/ui/tabs'
import { ToggleGroup, ToggleGroupItem } from '@/lib/registry/new-york/ui/toggle-group'

const props = defineProps<{
name: string
}>()

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 = await codeToHtml(rawString.value, {
lang: 'vue',
theme: cssVariables,
})
}
catch (err) {
console.error(err)
}
}, { immediate: true, deep: true })
</script>

<template>
<Tabs
:id="name"
v-model="tabValue"
class="relative grid w-full scroll-m-20 gap-4"
:style=" {
'--container-height': metadata.iframeHeight ?? '600px',
}"
>
<div class="flex flex-col items-center gap-4 sm:flex-row">
<div class="flex items-center gap-2">
<TabsList class="hidden sm:flex">
<TabsTrigger value="preview">
Preview
</TabsTrigger>
<TabsTrigger value="code">
Code
</TabsTrigger>
</TabsList>
<div class="hidden items-center gap-2 sm:flex">
<Separator
orientation="vertical"
class="mx-2 hidden h-4 md:flex"
/>
<div class="flex items-center gap-2">
<a :href="`#${name}`">
<Badge variant="outline">{{ name }}</Badge>
</a>
<Popover>
<PopoverTrigger class="hidden text-muted-foreground hover:text-foreground sm:flex">
<Info class="h-3.5 w-3.5" />
<span class="sr-only">Block description</span>
</PopoverTrigger>
<PopoverContent
side="right"
:side-offset="10"
class="text-sm"
>
{{ metadata.description }}
</PopoverContent>
</Popover>
</div>
</div>
</div>
<div class="flex items-center gap-2 pr-[14px] sm:ml-auto">
<div class="hidden h-[28px] items-center gap-1.5 rounded-md border p-[2px] shadow-sm md:flex">
<ToggleGroup
type="single"
default-value="100"
@update:model-value="(value) => {
resizableRef?.resize(parseInt(value))
}"
>
<ToggleGroupItem
value="100"
class="h-[22px] w-[22px] rounded-sm p-0"
>
<Monitor class="h-3.5 w-3.5" />
</ToggleGroupItem>
<ToggleGroupItem
value="60"
class="h-[22px] w-[22px] rounded-sm p-0"
>
<Tablet class="h-3.5 w-3.5" />
</ToggleGroupItem>
<ToggleGroupItem
value="25"
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-react for icons and
tailwindcss-animate for animations.
</p>
<p>
The <span class="font-medium">New York</span> style ships
with smaller buttons and inputs. It also uses shadows on cards
and buttons.
</p>
</PopoverContent>
</Popover>
<Separator orientation="vertical" class="mx-2 h-4" />
<BlockCopyButton :code="rawString" />
<!-- <V0Button
name="{block.name}"
description="{block.description" || "Edit in v0"}
code="{block.code}"
style="{block.style}"
/> -->
</div>
</div>
<TabsContent
v-show="tabValue === 'preview'"
force-mount
value="preview"
class="relative after:absolute after:inset-0 after:right-3 after:z-0 after:rounded-lg after:bg-muted h-[--container-height] px-0"
>
<ResizablePanelGroup id="block-resizable" direction="horizontal" class="relative z-10">
<ResizablePanel
id="block-resizable-panel-1"
ref="resizableRef"
class="relative rounded-lg border bg-background transition-all "
:default-size="100"
:min-size="25"
>
<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>
</Tabs>
</template>
53 changes: 53 additions & 0 deletions apps/www/.vitepress/theme/components/Blocks.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script setup lang="ts">
import { ref } from 'vue'
import PageHeader from '../components/PageHeader.vue'
import PageHeaderHeading from '../components/PageHeaderHeading.vue'
import PageHeaderDescription from '../components/PageHeaderDescription.vue'
import PageAction from '../components/PageAction.vue'
import Announcement from '../components/Announcement.vue'
import BlockPreview from './BlockPreview.vue'
import GitHubIcon from '~icons/radix-icons/github-logo'

import { buttonVariants } from '@/lib/registry/new-york/ui/button'
import { cn } from '@/lib/utils'

const blocks = ref<string[]>([])

import('../../../__registry__/index').then((res) => {
blocks.value = Object.values(res.Index.default).filter(i => i.type === 'components:block').map(i => i.name)
})
</script>

<template>
<PageHeader class="page-header pb-8">
<Announcement />
<PageHeaderHeading>Building Blocks for the Web</PageHeaderHeading>
<PageHeaderDescription>
Beautifully designed. Copy and paste into your apps. Open Source.
</PageHeaderDescription>

<PageAction>
<a
href="/blocks.html#blocks"
:class="cn(buttonVariants(), 'rounded-[6px]')"
>
Browse
</a>
<a
href="https://github.com/radix-vue/shadcn-vue"
target="_blank"
:class="cn(
buttonVariants({ variant: 'outline' }),
'rounded-[6px]',
)"
>
<GitHubIcon class="mr-2 h-4 w-4" />
GitHub
</a>
</PageAction>
</PageHeader>

<section id="blocks" class="grid scroll-mt-24 gap-24 lg:gap-48">
<BlockPreview v-for="block in blocks" :key="block" :name="block" />
</section>
</template>
3 changes: 2 additions & 1 deletion apps/www/.vitepress/theme/components/ComponentLoader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { useConfigStore } from '@/stores/config'

const props = defineProps<{
name: string
typeName?: 'example' | 'block'
}>()
const { style } = useConfigStore()

const Component = defineAsyncComponent({
loadingComponent: Spinner,
loader: () => import(`../../../src/lib/registry/${style.value}/example/${props.name}.vue`),
loader: () => import(`../../../src/lib/registry/${style.value}/${props.typeName}/${props.name}.vue`),
timeout: 5000,
})
</script>
Expand Down
2 changes: 1 addition & 1 deletion apps/www/.vitepress/theme/components/ComponentPreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ watch([style, codeConfig], async () => {
'items-end': align === 'end',
})"
>
<ComponentLoader v-bind="$attrs" :key="style" :name="name" />
<ComponentLoader v-bind="$attrs" :key="style" :name="name" :type-name="'example'" />
</div>
</TabsContent>
<TabsContent value="code">
Expand Down
Loading
Loading