Merge pull request #67 from GeoDaoyu/feat/copy-btn-llms
feat: add copy markdown buttons
@@ -320,6 +320,9 @@ watch(sidebarCollapsed, (collapsed) => {
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
<template #doc-before>
|
||||
<CopyOrDownloadAsMarkdownButtons />
|
||||
</template>
|
||||
<template #nav-bar-content-after>
|
||||
<GitHubStars />
|
||||
<ClientOnly>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><path fill="currentColor" d="M101.228 164.247q-7.425 0-14.108-2.821a38.3 38.3 0 0 1-11.88-7.871q-5.643 1.93-11.731 1.931-9.95 0-18.414-4.901T31.433 137.22q-5.05-8.464-5.05-18.859 0-4.307 1.189-9.356-5.94-5.494-9.207-12.622-3.267-7.276-3.267-15.147 0-8.019 3.415-15.444t9.504-12.771q6.237-5.495 14.405-7.574 1.634-8.465 6.83-15.147 5.348-6.83 13.07-10.692t16.483-3.86q7.425 0 14.108 2.82a38.3 38.3 0 0 1 11.88 7.871q5.643-1.93 11.731-1.93 9.95 0 18.414 4.9t13.514 13.365q5.197 8.465 5.197 18.86 0 4.306-1.188 9.355 5.94 5.495 9.207 12.771a35.6 35.6 0 0 1 3.267 14.999q0 8.019-3.415 15.444t-9.653 12.919q-6.088 5.346-14.256 7.425-1.633 8.465-6.979 15.147-5.198 6.831-12.92 10.692t-16.483 3.861m-36.68-18.562q7.425 0 12.92-3.119l27.918-16.038q1.485-1.04 1.485-2.821v-12.771l-35.937 20.641q-3.267 1.93-6.534 0l-28.067-16.186q0 .445-.148 1.039v1.782q0 7.574 3.564 13.959 3.712 6.237 10.246 9.801 6.534 3.713 14.553 3.713m1.485-24.206q.891.446 1.634.446t1.485-.446l11.137-6.385-35.788-20.79q-3.267-1.93-3.267-5.792V56.288q-7.425 3.267-11.88 10.098-4.455 6.683-4.455 14.85 0 7.276 3.712 13.959t9.653 10.098zm35.195 32.967q7.87 0 14.256-3.564t10.098-9.801 3.712-13.959V95.046q0-1.782-1.485-2.673l-11.286-6.534v41.432q0 3.86-3.267 5.791L85.19 149.249q7.276 5.197 16.038 5.197m5.643-54.351V79.899L90.09 70.395 73.161 79.9v20.196L90.09 109.6zM63.509 52.724q0-3.861 3.267-5.792l28.066-16.186q-7.276-5.198-16.038-5.198-7.87 0-14.256 3.564-6.385 3.564-10.098 9.801-3.564 6.237-3.564 13.96V84.8q0 1.782 1.485 2.821l11.138 6.534zm75.438 70.983q7.425-3.267 11.731-10.098 4.455-6.831 4.455-14.85 0-7.276-3.712-13.96-3.713-6.681-9.653-10.097l-27.769-16.038q-.891-.594-1.634-.446-.743 0-1.485.446L99.743 64.9l35.937 20.938q1.633.891 2.376 2.376.891 1.337.891 3.267zm-29.849-75.438q3.267-2.079 6.534 0l28.215 16.483V62.08q0-7.128-3.564-13.513-3.415-6.534-9.949-10.395-6.386-3.861-14.85-3.861-7.425 0-12.92 3.118L74.646 53.466q-1.485 1.04-1.485 2.822v12.77z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>
|
||||
|
After Width: | Height: | Size: 191 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-chevron-down-icon lucide-chevron-down" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
|
After Width: | Height: | Size: 248 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" overflow="visible" viewBox="0 0 100 101"><path fill="currentColor" d="m96.138 40.515 3.5 2v1.5l-1 3.5-42.5 10-3.996-9.93zm0 0" transform-origin="50px 50px"/><path fill="currentColor" d="m80.626 11.495 4.894 1.027 1.299 1.6 1.239 3.837-.514 2.447-28.521 39-9.5-9.5 26.3-34.514zm0 0" transform-origin="50px 50px"/><path fill="currentColor" d="m56.537 5.537 3-2 2.5 1 2.5 3.5-6.849 41.162-4.65-3.162-2-5.5 3.5-31zm0 0" transform-origin="50px 50px"/><path fill="currentColor" d="m25.058 6.102 3.082-3.937 2.01-.46 3.99.584 1.968 1.54 14.345 31.804 5.19 15.11-6.071 3.376-23.139-41.987zm0 0" transform-origin="50px 50px"/><path fill="currentColor" d="m10.766 27.61-1-4.003 3-3.5 3.5.5h1l21 15.5 6.5 5 9 7-5 8.5-4.5-3.5-3-3-29-20.5zm0 0" transform-origin="50px 50px"/><path fill="currentColor" d="m4.856 53-2.263-2.5v-2.224l2.263-.776 25.5 1.5 25 2-.812 4.978L6.856 53.5zm0 0" transform-origin="50px 50px"/><path fill="currentColor" d="M19.428 78.51h-5l-1.988-2.29v-2.737l8.488-6 34.508-21.966 3.492 5.966zm0 0" transform-origin="50px 50px"/><path fill="currentColor" d="m28.59 92.082-2 .5-3-1.5.5-2.5 29.5-39 4 5.5-22 29zm0 0" transform-origin="50px 50px"/><path fill="currentColor" d="m53.09 96.91-1.5 2-3 1-2.5-2-1.5-3 7.5-40.5 4.5.5zm0 0" transform-origin="50px 50px"/><path fill="currentColor" d="M77.985 86.16v4l-.5 1.5-2 1-3.5-.466-24.033-35.77 9.533-7.264 8 14.5.75 5.25zm0 0" transform-origin="50px 50px"/><path fill="currentColor" d="m89.132 80.508.5 2.5-1.5 2-1.5-.5-8.5-6-13-11.5-10-7 3-9.5 5 3 3 5.5zm0 0" transform-origin="50px 50px"/><path fill="currentColor" d="m82.5 55.5 12.5 1 3 2 2 3v2.159L94.5 66l-28-7-11.5-.5L58 48l8 6zm0 0" transform-origin="50px 50px"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
||||
|
After Width: | Height: | Size: 287 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-file-down-icon lucide-file-down" viewBox="0 0 24 24"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5m-8 10v-6m-3 3 3 3 3-3"/></svg>
|
||||
|
After Width: | Height: | Size: 397 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="lucide lucide-arrow-up-right-icon lucide-arrow-up-right" viewBox="0 0 24 24"><path d="M7 7h10v10M7 17 17 7"/></svg>
|
||||
|
After Width: | Height: | Size: 260 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M0 8a4 4 0 0 1 4-4h16a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4zm4-2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2zm1.684 2.051A1 1 0 0 1 6.8 8.4L9 11.333 11.2 8.4A1 1 0 0 1 13 9v6a1 1 0 1 1-2 0v-3l-1.2 1.6a1 1 0 0 1-1.6 0L7 12v3a1 1 0 1 1-2 0V9a1 1 0 0 1 .684-.949M18 9a1 1 0 1 0-2 0v3.586l-.293-.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l2-2a1 1 0 0 0-1.414-1.414l-.293.293z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 541 B |
@@ -0,0 +1,340 @@
|
||||
<template>
|
||||
<div class="markdown-copy-buttons">
|
||||
<div class="markdown-copy-buttons-inner">
|
||||
<div class="dropdown-container" ref="dropdownContainer">
|
||||
<!-- Main button -->
|
||||
<div class="dropdown-trigger">
|
||||
<!-- Copy area -->
|
||||
<button class="copy-page" @click="copyAsMarkdown">
|
||||
<span v-html="copied ? iconCheck : iconCopy" class="icon"></span>
|
||||
<span class="label">
|
||||
{{ copied ? 'Copied' : 'Copy page' }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<span class="divider"></span>
|
||||
|
||||
<!-- Chevron area -->
|
||||
<button class="chevron-wrapper" @click.stop="toggleDropdown">
|
||||
<span v-html="iconChevron" class="icon chevron" :class="{ open: isOpen }"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Dropdown -->
|
||||
<div v-if="isRendered" ref="dropdownMenu" class="dropdown-menu" :class="{ open: isOpen }">
|
||||
<button class="dropdown-item" @click="viewAsMarkdown">
|
||||
<span v-html="iconMarkdown" class="icon"></span>
|
||||
View as Markdown
|
||||
<span v-html="iconExternal" class="icon external"></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-for="provider in aiProviders"
|
||||
:key="provider.name"
|
||||
class="dropdown-item"
|
||||
@click="openInAI(provider)"
|
||||
>
|
||||
<span v-html="provider.icon" class="icon"></span>
|
||||
Open in {{ provider.name }}
|
||||
<span v-html="iconExternal" class="icon external"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Download button -->
|
||||
<button class="download-btn" @click="downloadMarkdown">
|
||||
<span v-html="downloaded ? iconCheck : iconDownload" class="icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import iconChatGPT from './icons/chatgpt.svg?raw'
|
||||
import iconCheck from './icons/check.svg?raw'
|
||||
import iconChevron from './icons/chevron.svg?raw'
|
||||
import iconClaude from './icons/claude.svg?raw'
|
||||
import iconCopy from './icons/copy.svg?raw'
|
||||
import iconDownload from './icons/download.svg?raw'
|
||||
import iconExternal from './icons/external.svg?raw'
|
||||
import iconMarkdown from './icons/markdown.svg?raw'
|
||||
|
||||
import { downloadFile, resolveMarkdownPageURL } from './utils'
|
||||
|
||||
const aiProviders = [
|
||||
{ name: 'ChatGPT', icon: iconChatGPT, url: 'https://chatgpt.com/?hints=search&prompt=' },
|
||||
{ name: 'Claude', icon: iconClaude, url: 'https://claude.ai/new?q=' },
|
||||
]
|
||||
|
||||
const isOpen = ref(false)
|
||||
const copied = ref(false)
|
||||
const downloaded = ref(false)
|
||||
const dropdownContainer = ref<HTMLElement | null>(null)
|
||||
const isRendered = ref(false)
|
||||
const dropdownMenu = ref<HTMLElement | null>(null)
|
||||
|
||||
function toggleDropdown() {
|
||||
if (isOpen.value) {
|
||||
// close
|
||||
isOpen.value = false
|
||||
|
||||
const el = dropdownMenu.value
|
||||
if (!el) return
|
||||
|
||||
const onEnd = () => {
|
||||
isRendered.value = false
|
||||
el.removeEventListener('transitionend', onEnd)
|
||||
}
|
||||
|
||||
el.addEventListener('transitionend', onEnd)
|
||||
} else {
|
||||
// open
|
||||
isRendered.value = true
|
||||
requestAnimationFrame(() => {
|
||||
isOpen.value = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const currentURL = window.location.origin + window.location.pathname
|
||||
|
||||
function copyAsMarkdown() {
|
||||
fetch(resolveMarkdownPageURL(currentURL))
|
||||
.then((r) => r.text())
|
||||
.then((text) => navigator.clipboard.writeText(text))
|
||||
.then(() => {
|
||||
copied.value = true
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 2000)
|
||||
})
|
||||
.catch((e) => console.error('❌ Error:', e))
|
||||
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
function viewAsMarkdown() {
|
||||
window.open(resolveMarkdownPageURL(currentURL), '_blank')
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
function openInAI(provider: (typeof aiProviders)[0]) {
|
||||
const markdownUrl = resolveMarkdownPageURL(currentURL)
|
||||
const prompt = `Read from ${markdownUrl} so I can ask questions about it.`
|
||||
window.open(provider.url + encodeURIComponent(prompt), '_blank')
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
function downloadMarkdown() {
|
||||
fetch(resolveMarkdownPageURL(currentURL))
|
||||
.then((r) => r.text())
|
||||
.then((text) => {
|
||||
const filename = resolveMarkdownPageURL(currentURL).split('/').pop() || 'page.md'
|
||||
downloadFile(filename, text, 'text/markdown')
|
||||
downloaded.value = true
|
||||
setTimeout(() => {
|
||||
downloaded.value = false
|
||||
}, 2000)
|
||||
})
|
||||
.catch((e) => console.error('❌ Error:', e))
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownContainer.value && !dropdownContainer.value.contains(event.target as Node)) {
|
||||
isOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('click', handleClickOutside))
|
||||
onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.markdown-copy-buttons {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-copy-buttons-inner {
|
||||
margin: 16px 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
background: transparent;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 14px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.copy-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 25px;
|
||||
align-self: center;
|
||||
background: var(--vp-c-divider);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.chevron-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
min-width: 240px;
|
||||
background: var(--vp-c-bg-elv);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
z-index: 100;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
||||
|
||||
opacity: 0;
|
||||
transform: translateY(-6px) scale(0.96);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropdown-menu.open {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dropdown-item .icon.external {
|
||||
margin-left: auto;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
color: var(--vp-c-text-1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.chevron.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.dropdown-item:hover .icon.external {
|
||||
opacity: 1;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.dropdown-menu {
|
||||
transition:
|
||||
opacity 0.18s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.18s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transform-origin: top;
|
||||
}
|
||||
|
||||
/* Hover zones */
|
||||
.copy-page:hover,
|
||||
.chevron-wrapper:hover,
|
||||
.download-btn:hover {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.dropdown-trigger,
|
||||
.copy-page,
|
||||
.chevron-wrapper,
|
||||
.dropdown-item,
|
||||
.dropdown-item .icon.external,
|
||||
.download-btn {
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.dropdown-trigger:hover,
|
||||
.download-btn:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dropdown-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
background: var(--vp-c-brand-1);
|
||||
transition: width 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.dropdown-item:hover::before {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
var removeHtmlExtension = (pathSegment) => {
|
||||
const lastSlashIndex = pathSegment.lastIndexOf('/')
|
||||
const lastDotIndex = pathSegment.lastIndexOf('.')
|
||||
if (
|
||||
lastDotIndex > lastSlashIndex &&
|
||||
lastDotIndex !== -1 &&
|
||||
pathSegment.endsWith('.html')
|
||||
) {
|
||||
return pathSegment.slice(0, lastDotIndex)
|
||||
}
|
||||
return pathSegment
|
||||
}
|
||||
function cleanUrl(url) {
|
||||
const { origin, pathname } = new URL(url)
|
||||
const pathnameWithoutTrailingSlash = pathname.replace(/\/+$/, '')
|
||||
if (pathname.length > 0) {
|
||||
return origin + removeHtmlExtension(pathnameWithoutTrailingSlash)
|
||||
}
|
||||
return origin
|
||||
}
|
||||
function resolveMarkdownPageURL(url) {
|
||||
const cleanedURL = cleanUrl(url)
|
||||
return `${cleanedURL}/index.md`
|
||||
}
|
||||
function downloadFile(filename, content, blobType = 'text/plain') {
|
||||
const blob =
|
||||
content instanceof Blob ? content : new Blob([content], { type: blobType })
|
||||
const url = URL.createObjectURL(blob)
|
||||
Object.assign(document.createElement('a'), {
|
||||
href: url,
|
||||
download: filename
|
||||
}).click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
export { resolveMarkdownPageURL, downloadFile, cleanUrl }
|
||||
@@ -841,11 +841,17 @@ import ProjectArchitectureComparisonDemo from './components/appendix/project-arc
|
||||
// Appendix Navigation Component
|
||||
import AppendixFlowMap from './components/AppendixFlowMap.vue'
|
||||
|
||||
import CopyOrDownloadAsMarkdownButtons from './components/CopyOrDownloadAsMarkdownButtons/index.vue'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout,
|
||||
enhanceApp({ app }) {
|
||||
app.use(ElementPlus)
|
||||
app.component(
|
||||
'CopyOrDownloadAsMarkdownButtons',
|
||||
CopyOrDownloadAsMarkdownButtons
|
||||
)
|
||||
app.component('HomeFeatures', HomeFeatures)
|
||||
app.component('WelcomeScreen', WelcomeScreen)
|
||||
app.component('NavGrid', NavGrid)
|
||||
|
||||