Skip to content
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
25 changes: 20 additions & 5 deletions apps/docs/.vitepress/plugins/demo-container.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {MarkdownEnv, MarkdownRenderer} from 'vitepress'
import type {RuleBlock} from 'markdown-it/lib/parser_block.mjs'
import path from 'path'
import fs from 'fs'

// This plugin is inspired by vitepress' snippet plugin and must run before it to work
// https://vitepress.dev/guide/markdown.html#import-code-snippets
Expand Down Expand Up @@ -41,18 +42,32 @@ export const demoContainer = (md: MarkdownRenderer, srcDir: string) => {
const {filepath, extension, region, lines, lang, title} = rawPathToToken(rawPath)
const component = isDemo ? `<${title.substring(0, title.indexOf('.'))}/>` : ''

// Resolve path early so it can be used in the prefix token
const {realPath, path: _path} = state.env as MarkdownEnv
const resolvedPath = path.resolve(path.dirname(realPath ?? _path), filepath)

// Read the source code file during build time
let fullFileContent = ''
try {
fullFileContent = fs.readFileSync(resolvedPath, 'utf-8')
} catch (error) {
// eslint-disable-next-line no-console
console.warn(`Failed to read source code from ${resolvedPath}:`, error)
}

// Base64 encode the full file content for StackBlitz (always complete Vue file)
// Use explicit UTF-8 encoding to preserve Unicode characters like "é", "–", etc.
const encodedFullFile = Buffer.from(fullFileContent, 'utf8').toString('base64')

state.line += 1

const prefixToken = state.push('html_block', '', 0)
prefixToken.content = `<HighlightCard>${component}<template #html>`
prefixToken.content = `<HighlightCard full-file="${encodedFullFile}" title="${title}">${component}<template #html>`

const codeToken = state.push('fence', 'code', 0)
codeToken.info = `${lang || extension}${lines ? `{${lines}}` : ''}${title ? `[${title}]` : ''}`

const {realPath, path: _path} = state.env as MarkdownEnv
const resolvedPath = path.resolve(path.dirname(realPath ?? _path), filepath)

// @ts-ignore
// @ts-expect-error - VitePress fence token type doesn't include src property
codeToken.src = [resolvedPath, region.slice(1)]
codeToken.markup = '```'
codeToken.map = [startLine, startLine + 1]
Expand Down
1 change: 1 addition & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@iconify-json/bi": "^1.2.4",
"@iconify-json/logos": "^1.2.4",
"@rushstack/eslint-patch": "^1.12.0",
"@stackblitz/sdk": "^1.11.0",
"@toycode/markdown-it-class": "^1.2.4",
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.16.5",
Expand Down
126 changes: 125 additions & 1 deletion apps/docs/src/components/HighlightCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,134 @@
<slot />
</BCardBody>
<template v-if="$slots.html">
<div v-if="$slots.default" class="html">HTML</div>
<div v-if="$slots.default" class="html d-flex justify-content-between align-items-center">
<span>HTML</span>
<BButton
v-if="hasFileContent"
size="sm"
variant="outline-primary"
:loading="isLoading"
:loading-text="'Opening...'"
title="Open in StackBlitz"
aria-label="Open example in StackBlitz"
@click="openInStackBlitz"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" class="me-1">
<path d="M10.797 14.182H3.635L16.728 0l-3.525 9.818h7.162L7.272 24l3.525-9.818z" />
</svg>
StackBlitz
</BButton>
</div>
<BCardBody class="bg-body-tertiary">
<slot name="html" />
</BCardBody>
</template>
</BCard>
</template>

<script setup lang="ts">
import {computed, ref} from 'vue'

// Import template files as raw text from the original Vite template
import indexHtml from '../../../../templates/vite/index.html?raw'
import mainTs from '../../../../templates/vite/src/main.ts?raw'
import appVue from '../../../../templates/vite/src/App.vue?raw'
import packageJson from '../../../../templates/vite/package.json?raw'
import viteConfig from '../../../../templates/vite/vite.config.mts?raw'
import tsConfig from '../../../../templates/vite/tsconfig.json?raw'
import tsConfigNode from '../../../../templates/vite/tsconfig.node.json?raw'

interface Props {
fullFile?: string
title?: string
}

const props = withDefaults(defineProps<Props>(), {
fullFile: '',
title: 'BootstrapVueNext Example',
})

// Loading state for StackBlitz button
const isLoading = ref(false)

// Decode base64 content with proper UTF-8 handling and cross-environment compatibility
const decodeFullFile = (encoded: string) => {
if (!encoded) return ''

if (typeof globalThis.atob === 'function') {
try {
const binary = globalThis.atob(encoded)
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0))
return new TextDecoder().decode(bytes)
} catch {
// fall through to the Node-friendly path
}
}

const nodeBuffer = (
globalThis as typeof globalThis & {Buffer?: typeof import('node:buffer').Buffer}
).Buffer
if (nodeBuffer) {
try {
return nodeBuffer.from(encoded, 'base64').toString('utf-8')
} catch {
// ignore and return original payload
}
}

return encoded
}

// Check if we have content to show the StackBlitz button
const hasFileContent = computed(() => Boolean(props.fullFile))

// Decoded file content for StackBlitz (reactive)
const fullFileContent = computed(() => decodeFullFile(props.fullFile))

const createProjectFiles = () => ({
'index.html': indexHtml,
'src/main.ts': mainTs,
'src/App.vue': appVue,
'src/components/Comp.vue': fullFileContent.value || '',
'package.json': packageJson,
'vite.config.ts': viteConfig,
'tsconfig.json': tsConfig,
'tsconfig.node.json': tsConfigNode,
})

const openInStackBlitz = async () => {
if (!props.fullFile || isLoading.value) return

isLoading.value = true

try {
// Dynamically import StackBlitz SDK
const {default: sdk} = await import('@stackblitz/sdk')

// Create a complete Vue project with BootstrapVueNext using node template
// This gives us full control over the file structure
const project = {
title: props.title || 'BootstrapVueNext Example',
description: 'Example from BootstrapVueNext documentation',
template: 'node' as const,
files: createProjectFiles(),
}

// Open the project in a new tab
await sdk.openProject(project, {
newWindow: true,
openFile: 'src/components/Comp.vue',
})
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to open StackBlitz project:', error)
// Fallback to basic StackBlitz
window.open('https://stackblitz.com/fork/vue-ts', '_blank')
} finally {
// Reset loading state after a short delay to show completion
setTimeout(() => {
isLoading.value = false
}, 500)
}
}
</script>
35 changes: 35 additions & 0 deletions apps/docs/src/template-types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Type declarations for template raw imports
declare module '*/templates/vite/index.html?raw' {
const content: string
export default content
}

declare module '*/templates/vite/src/main.ts?raw' {
const content: string
export default content
}

declare module '*/templates/vite/src/App.vue?raw' {
const content: string
export default content
}

declare module '*/templates/vite/package.json?raw' {
const content: string
export default content
}

declare module '*/templates/vite/vite.config.mts?raw' {
const content: string
export default content
}

declare module '*/templates/vite/tsconfig.json?raw' {
const content: string
export default content
}

declare module '*/templates/vite/tsconfig.node.json?raw' {
const content: string
export default content
}
1 change: 1 addition & 0 deletions apps/docs/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
2 changes: 1 addition & 1 deletion apps/docs/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [".vitepress/**/*", "**/*.vue", "**/*.ts"],
"include": [".vitepress/**/*", "**/*.vue", "**/*.ts", "src/template-types.d.ts"],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading