Skip to content

Commit

Permalink
docs: enhance components preview with support for multiple framework (u…
Browse files Browse the repository at this point in the history
…novue#484)

* refactor: implement dynamic code snippet, prepare for more css framework

* feat: implement folder based import for code snippets

* docs: persist selected css framework

* chore: fix frozen pnpm

* docs: update accordion css styling

* docs: move component around, update alert-dialog

* chore: test component loader

* docs: add css framework for all components

* docs: improve previwer, stackblitz

* fix: dynamic vars import limitation

* fix: codeeditor can't import correct files

* docs: update css for new components
  • Loading branch information
zernonia authored Nov 1, 2023
1 parent f9b0bfe commit f85e92a
Show file tree
Hide file tree
Showing 163 changed files with 5,360 additions and 700 deletions.
6 changes: 3 additions & 3 deletions .histoire/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
"@rollup/plugin-alias": "^5.0.0",
"@vitejs/plugin-vue": "^4.1.0",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"@vueuse/components": "^10.4.1",
"@vueuse/core": "^10.4.1",
"@vueuse/shared": "^10.4.1",
"@vueuse/components": "^10.5.0",
"@vueuse/core": "^10.5.0",
"@vueuse/shared": "^10.5.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.43.0",
"histoire": "^0.16.2",
Expand Down
19 changes: 19 additions & 0 deletions docs/.vitepress/components/ComponentLoader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
import Spinner from './Spinner.vue'
const props = defineProps<{
name: string
}>()
const Component = defineAsyncComponent({
loadingComponent: Spinner,
loader: () => import(`../../components/demo/${props.name}/tailwind/index.vue`),
timeout: 5000,
suspensible: false,
})
</script>

<template>
<Component :is="Component" />
</template>
27 changes: 27 additions & 0 deletions docs/.vitepress/components/ComponentPreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script setup lang="ts">
import HeroContainer from './NewHeroContainer.vue'
import HeroCodeGroup from './NewHeroCodeGroup.vue'
import { computed } from 'vue'
import { useStorage } from '@vueuse/core'
const props = defineProps<{
name: string
files?: string
}>()
const cssFramework = useStorage<'css' | 'tailwind' | 'pinceau' >('cssFramework', 'tailwind')
const parsedFiles = computed(() => JSON.parse(decodeURIComponent(props.files ?? ''))[cssFramework.value])
</script>

<template>
<HeroContainer :folder="name" :files="parsedFiles" :css-framework="cssFramework">
<slot />

<template #codeSlot>
<HeroCodeGroup v-model="cssFramework">
<slot name="tailwind" />
<slot name="css" />
</HeroCodeGroup>
</template>
</HeroContainer>
</template>
135 changes: 135 additions & 0 deletions docs/.vitepress/components/NewHeroCodeGroup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<script setup lang="ts">
import { type VNode, capitalize, computed, ref, useSlots, watch } from 'vue'
import { SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectPortal, SelectRoot, SelectTrigger, SelectValue, SelectViewport, TabsContent, TabsList, TabsRoot, TabsTrigger } from 'radix-vue'
import { Icon } from '@iconify/vue'
import { useVModel } from '@vueuse/core'
defineOptions({
inheritAttrs: false,
})
const props = defineProps<{
modelValue: 'css' | 'tailwind' | 'pinceau'
}>()
const emits = defineEmits<{
'update:modelValue': [payload: 'css' | 'tailwind' | 'pinceau']
}>()
const cssFramework = useVModel(props, 'modelValue', emits)
const slots = useSlots()
const slotsFramework = computed(() => slots.default?.().map(slot => slot.props?.key?.toString()?.replace('_', '')) ?? [])
const cssFrameworkOptions = computed(() => [
{ label: 'TailwindCSS', value: 'tailwind' },
{ label: 'CSS', value: 'css' },
{ label: 'Pinceau', value: 'pinceau' },
].filter(i => slotsFramework.value.includes(i.value)))
const tabs = computed(
() => {
const currentFramework = slots.default?.().find(slot => slot.props?.key?.toString().includes(cssFramework.value))
const childSlots = (currentFramework?.children as VNode[]).sort((a, b) => a?.props?.title.localeCompare(b?.props?.title))
return childSlots?.map((slot, index) => {
return {
label: slot.props?.title || `${index}`,
component: slot,
}
}) || []
},
)
const open = ref(false)
const codeScrollWrapper = ref<HTMLElement | undefined>()
const buttonRef = ref<HTMLElement | undefined>()
const currentTab = ref('index.vue')
watch(open, () => {
if (!open.value) {
codeScrollWrapper.value!.scrollTo({
top: 0,
})
}
})
</script>

<template>
<TabsRoot
v-model="currentTab"
class="bg-[var(--vp-code-block-bg)] border border-neutral-700/40 rounded-b-lg overflow-hidden"
@update:model-value="open = true"
>
<div class="bg-[var(--vp-code-block-bg)] border-b-2 border-[#272727] flex pr-2">
<div class="flex justify-between items-center w-full text-[13px]">
<TabsList class="flex">
<TabsTrigger
v-for="(tab, index) in tabs"
:key="index"
:value="tab.label"
tabindex="-1"
class="text-white/70 py-2.5 px-4 border-box data-[state=active]:shadow-[0_1px_0_#10b981] data-[state=active]:font-medium data-[state=active]:text-white"
>
{{ tab.label }}
</TabsTrigger>
</TabsList>
<div>
<SelectRoot v-model="cssFramework" @update:model-value="currentTab = 'index.vue'">
<SelectTrigger class="flex items-center justify-between bg-stone-800 rounded-sm w-[115px] text-xs py-1 pl-2 pr-1">
<SelectValue />
<Icon icon="radix-icons:chevron-down" class="h-3.5 w-3.5" />
</SelectTrigger>

<SelectPortal>
<SelectContent class="border border-stone-700 min-w-[115px] bg-stone-800 rounded shadow-[0px_10px_38px_-10px_rgba(22,_23,_24,_0.35),_0px_10px_20px_-15px_rgba(22,_23,_24,_0.2)] will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade z-[100]">
<SelectViewport class="p-[5px]">
<SelectItem
v-for="framework in cssFrameworkOptions"
:key="framework.label"
class="text-xs leading-none text-grass11 rounded-[3px] flex items-center h-[25px] pl-[25px] relative select-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-green9 data-[highlighted]:text-green1"
:value="framework.value"
>
<SelectItemIndicator class="absolute left-0 w-[25px] inline-flex items-center justify-center">
<Icon icon="radix-icons:check" />
</SelectItemIndicator>

<SelectItemText>
{{ capitalize(framework.label ?? '') }}
</SelectItemText>
</SelectItem>
</SelectViewport>
</SelectContent>
</SelectPortal>
</SelectRoot>
</div>
</div>
</div>
<div
ref="codeScrollWrapper"
:key="cssFramework"
class="pb-10 block h-full"
:class="`${open ? 'overflow-scroll max-h-[80vh]' : 'overflow-hidden max-h-[150px]'}`"
>
<TabsContent v-for="tab in tabs" :key="tab.label" :value="tab.label" as-child>
<div class="relative -mt-5 text-base">
<component :is="tab.component" class="border-0" />
</div>
</TabsContent>
<div
class="bg-gradient-to-t from-[#161618FF] to-[#16161800] bottom-[1px] left-[1px] right-[1px] h-20 flex items-center justify-center absolute rounded-b-lg"
>
<button
ref="buttonRef"
class="mt-4 bg-neutral-800 hover:bg-neutral-700 px-3 py-1 rounded border-neutral-700 border"
@click="open = !open"
>
{{ open ? "Collapse code" : "Expand code" }}
</button>
</div>
</div>
</TabsRoot>
</template>

<style scoped>
:deep(*) {
color: white;
}
</style>
55 changes: 55 additions & 0 deletions docs/.vitepress/components/NewHeroContainer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<script setup lang="ts">
import CodeSandbox from '../../components/CodeSandbox.vue'
import Stackblitz from '../../components/Stackblitz.vue'
withDefaults(
defineProps<{
overflow?: boolean
folder?: string
files?: string[]
cssFramework?: string
}>(),
{ folder: '', files: () => [] },
)
</script>

<template>
<div class="relative text-[15px] text-black">
<div
class="vp-raw bg-gradient-to-br p-4 rounded-t-lg from-teal9 to-green9 w-full relative items-center justify-center flex"
:class="{ 'overflow-x-auto': overflow }"
>
<div class="w-full max-w-[700px] flex items-center py-12 sm:py-[100px] custom-justify-center z-10">
<slot />

<CodeSandbox v-if="folder" :key="cssFramework" class="hidden sm:block absolute bottom-4 right-4" :name="folder" :files="files" />
<Stackblitz v-if="folder" :key="cssFramework" class="hidden sm:block absolute bottom-4 right-12" :name="folder" :files="files" />
</div>
</div>
<slot name="codeSlot" />
</div>
</template>

<style scoped>
:deep(input) {
background-color: white;
}
:deep(li) {
margin-top: 0 !important;
}
:deep(button:focus),
:deep(button:focus-visible) {
outline: 0;
}
:deep(h3) {
margin: 0px !important;
font-weight: unset !important;
}
:deep(pre) {
z-index: 0 !important;
}
</style>
7 changes: 7 additions & 0 deletions docs/.vitepress/components/Spinner.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
</script>

<template>
<Icon icon="lucide:loader-2" class="animate-spin" />
</template>
5 changes: 5 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from './meta'
import { version } from '../../package.json'
import { teamMembers } from './contributors'
import ComponentPreviewPlugin from './plugins/preview'

// https://vitepress.dev/reference/site-config
export default defineConfig({
Expand Down Expand Up @@ -86,6 +87,10 @@ export default defineConfig({
appearance: 'dark',
markdown: {
theme: 'material-theme-palenight',

preConfig(md) {
md.use(ComponentPreviewPlugin)
},
},
transformPageData(pageData) {
if (pageData.frontmatter.sidebar != null)
Expand Down
93 changes: 93 additions & 0 deletions docs/.vitepress/plugins/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { dirname, resolve } from 'node:path'
import { readdirSync } from 'node:fs'
import type { MarkdownEnv, MarkdownRenderer } from 'vitepress'

export const rawPathRegexp
= /^(.+?(?:(?:\.([a-z0-9]+))?))(?:(#[\w-]+))?(?: ?(?:{(\d+(?:[,-]\d+)*)? ?(\S+)?}))? ?(?:\[(.+)\])?$/

function rawPathToToken(rawPath: string) {
const [
filepath = '',
extension = '',
region = '',
lines = '',
lang = '',
rawTitle = '',
] = (rawPathRegexp.exec(rawPath) || []).slice(1)

const title = rawTitle || filepath.split('/').pop() || ''

return { filepath, extension, region, lines, lang, title }
}

export default function (md: MarkdownRenderer) {
md.core.ruler.after('inline', 'component-preview', (state) => {
// Define the regular expression to match the desired pattern
const regex = /<ComponentPreview name="([^"]+)" \/>/g

// Iterate through the Markdown content and replace the pattern
state.src = state.src.replace(regex, (match, componentName) => {
const importComponent = new state.Token('html_block', '', 0)
const pathName = `../../components/demo/${componentName}`

importComponent.content = `<script setup>\nimport Demo from '${pathName}/tailwind/index.vue'\n</script>\n`
state.tokens.splice(0, 0, importComponent)

const index = state.tokens.findIndex(i => i.content.match(regex))

const { realPath, path: _path } = state.env as MarkdownEnv

const childFiles = readdirSync(resolve(dirname(realPath ?? _path), pathName), { withFileTypes: false, recursive: true })
const groupedFiles = childFiles.reduce((prev, curr) => {
if (typeof curr !== 'string')
return prev
if (!curr.includes('/')) {
prev[curr] = []
}
else {
const folder = curr.split('/')[0]
prev[folder].push(curr)
}
return prev
}, {} as { [key: string]: string[] })

state.tokens[index].content = `<ComponentPreview name="${componentName}" files="${encodeURIComponent(JSON.stringify(groupedFiles))}" ><Demo />`
const tokenArray: Array<typeof importComponent> = []

Object.entries(groupedFiles).forEach(([key, value]) => {
const templateStart = new state.Token('html_inline', '', 0)
templateStart.content = `<template #${key}>`
tokenArray.push(templateStart)

value.forEach((file) => {
const { filepath, extension, lines, lang, title } = rawPathToToken(`${pathName}/${file}`)
const resolvedPath = resolve(dirname(realPath ?? _path), filepath)

// Add code tokens for each line
const token = new state.Token('fence', 'code', 0)
token.info = `${lang || extension}${lines ? `{${lines}}` : ''}${
title ? `[${title}]` : ''
}`

token.content = `<<< ${filepath}`
// @ts-expect-error token.src is for snippets plugin to handle importing snippet
token.src = [resolvedPath]
tokenArray.push(token)
})

const templateEnd = new state.Token('html_inline', '', 0)
templateEnd.content = '</template>'
tokenArray.push(templateEnd)
})

const endTag = new state.Token('html_inline', '', 0)
endTag.content = '</ComponentPreview>'
tokenArray.push(endTag)

state.tokens.splice(index + 1, 0, ...tokenArray)

// Return an empty string to replace the original pattern
return ''
})
})
}
2 changes: 2 additions & 0 deletions docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import HomePage from '../components/HomePage.vue'
import HomePageDemo from '../components/HomePageDemo.vue'
import Annoucement from '../components/Annoucement.vue'
import EmbedIframe from '../components/EmbedIframe.vue'
import ComponentPreview from '../components/ComponentPreview.vue'
import LayoutShowcase from '../layouts/showcase.vue'
import 'vitepress/dist/client/theme-default/styles/components/vp-doc.css'
import './style.css'
Expand Down Expand Up @@ -33,5 +34,6 @@ export default {

app.component('Showcase', LayoutShowcase)
app.component('EmbedIframe', EmbedIframe)
app.component('ComponentPreview', ComponentPreview)
},
} satisfies Theme
Loading

0 comments on commit f85e92a

Please sign in to comment.