mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 10:54:44 +00:00
feat(docs): add document generator
This commit is contained in:
37
docs/app/components/DocsBadge.vue
Normal file
37
docs/app/components/DocsBadge.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
kind: string;
|
||||
size?: 'sm' | 'md';
|
||||
}>();
|
||||
|
||||
const kindColors: Record<string, string> = {
|
||||
function: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300',
|
||||
class: 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300',
|
||||
interface: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300',
|
||||
type: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
|
||||
enum: 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-300',
|
||||
variable: 'bg-slate-100 text-slate-700 dark:bg-slate-800/50 dark:text-slate-300',
|
||||
};
|
||||
|
||||
const kindLabels: Record<string, string> = {
|
||||
function: 'fn',
|
||||
class: 'C',
|
||||
interface: 'I',
|
||||
type: 'T',
|
||||
enum: 'E',
|
||||
variable: 'V',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center justify-center rounded font-mono font-medium shrink-0',
|
||||
kindColors[kind] ?? kindColors.variable,
|
||||
size === 'sm' ? 'w-5 h-5 text-[10px]' : 'w-6 h-6 text-xs',
|
||||
]"
|
||||
:title="kind"
|
||||
>
|
||||
{{ kindLabels[kind] ?? '?' }}
|
||||
</span>
|
||||
</template>
|
||||
28
docs/app/components/DocsCode.vue
Normal file
28
docs/app/components/DocsCode.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
code: string;
|
||||
lang?: string;
|
||||
}>();
|
||||
|
||||
const { highlight } = useShiki();
|
||||
const html = ref('');
|
||||
|
||||
onMounted(async () => {
|
||||
html.value = await highlight(props.code, props.lang ?? 'typescript');
|
||||
});
|
||||
|
||||
watch(() => props.code, async (newCode) => {
|
||||
html.value = await highlight(newCode, props.lang ?? 'typescript');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="code-block relative group rounded-lg border border-(--color-border) overflow-hidden max-w-full">
|
||||
<div
|
||||
v-if="html"
|
||||
class="overflow-x-auto text-sm leading-relaxed [&_pre]:p-4 [&_pre]:m-0 [&_pre]:min-w-0"
|
||||
v-html="html"
|
||||
/>
|
||||
<pre v-else class="p-4 text-sm bg-(--color-bg-soft) overflow-x-auto"><code>{{ code }}</code></pre>
|
||||
</div>
|
||||
</template>
|
||||
71
docs/app/components/DocsDemo.vue
Normal file
71
docs/app/components/DocsDemo.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
component: Component;
|
||||
source: string;
|
||||
}>();
|
||||
|
||||
const showSource = ref(false);
|
||||
|
||||
const { highlighted, highlightReactive } = useShiki();
|
||||
|
||||
watch(showSource, async (show) => {
|
||||
if (show && !highlighted.value) {
|
||||
await highlightReactive(props.source, 'vue');
|
||||
}
|
||||
}, { immediate: false });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border border-(--color-border) rounded-lg overflow-hidden">
|
||||
<!-- Live demo -->
|
||||
<div class="p-6 bg-(--color-bg-soft)">
|
||||
<component :is="component" />
|
||||
</div>
|
||||
|
||||
<!-- Source toggle bar -->
|
||||
<div class="flex items-center border-t border-(--color-border) bg-(--color-bg)">
|
||||
<button
|
||||
class="flex items-center gap-1.5 px-4 py-2 text-xs font-medium text-(--color-text-mute) hover:text-(--color-text) transition-colors cursor-pointer"
|
||||
@click="showSource = !showSource"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="16 18 22 12 16 6" />
|
||||
<polyline points="8 6 2 12 8 18" />
|
||||
</svg>
|
||||
{{ showSource ? 'Hide source' : 'View source' }}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="transition-transform"
|
||||
:class="showSource ? 'rotate-180' : ''"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Source code -->
|
||||
<div v-if="showSource" class="border-t border-(--color-border)">
|
||||
<div class="overflow-x-auto text-sm" v-html="highlighted" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
46
docs/app/components/DocsMethodsList.vue
Normal file
46
docs/app/components/DocsMethodsList.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import type { MethodMeta } from '../../modules/extractor/types';
|
||||
|
||||
defineProps<{
|
||||
methods: MethodMeta[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="methods.length > 0" class="space-y-4">
|
||||
<div
|
||||
v-for="method in methods"
|
||||
:key="method.name"
|
||||
class="border border-(--color-border) rounded-lg p-4"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<code class="text-sm font-mono font-semibold text-(--color-text)">{{ method.name }}</code>
|
||||
<span
|
||||
v-if="method.visibility !== 'public'"
|
||||
class="text-[10px] uppercase px-1.5 py-0.5 rounded bg-(--color-bg-mute) text-(--color-text-mute)"
|
||||
>
|
||||
{{ method.visibility }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p v-if="method.description" class="text-sm text-(--color-text-soft) mb-3">
|
||||
{{ method.description }}
|
||||
</p>
|
||||
|
||||
<DocsCode
|
||||
v-for="(sig, i) in method.signatures"
|
||||
:key="i"
|
||||
:code="sig"
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
<DocsParamsTable v-if="method.params.length > 0" :params="method.params" />
|
||||
|
||||
<div v-if="method.returns" class="mt-2 text-sm">
|
||||
<span class="text-(--color-text-mute)">Returns:</span>
|
||||
<code class="ml-1 text-xs font-mono bg-(--color-bg-mute) px-1.5 py-0.5 rounded">{{ method.returns.type }}</code>
|
||||
<span v-if="method.returns.description" class="ml-2 text-(--color-text-soft)">{{ method.returns.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
44
docs/app/components/DocsParamsTable.vue
Normal file
44
docs/app/components/DocsParamsTable.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import type { ParamMeta } from '../../modules/extractor/types';
|
||||
|
||||
defineProps<{
|
||||
params: ParamMeta[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="params.length > 0" class="overflow-x-auto max-w-full">
|
||||
<table class="w-full text-sm border-collapse table-fixed">
|
||||
<thead>
|
||||
<tr class="border-b border-(--color-border)">
|
||||
<th class="text-left py-2 pr-4 font-medium text-(--color-text-soft)">Parameter</th>
|
||||
<th class="text-left py-2 pr-4 font-medium text-(--color-text-soft)">Type</th>
|
||||
<th class="text-left py-2 pr-4 font-medium text-(--color-text-soft)">Default</th>
|
||||
<th class="text-left py-2 font-medium text-(--color-text-soft)">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="param in params"
|
||||
:key="param.name"
|
||||
class="border-b border-(--color-border) last:border-b-0"
|
||||
>
|
||||
<td class="py-2 pr-4">
|
||||
<code class="text-brand-600 font-mono text-xs">{{ param.name }}</code>
|
||||
<span v-if="param.optional" class="text-(--color-text-mute) text-xs ml-1">?</span>
|
||||
</td>
|
||||
<td class="py-2 pr-4 max-w-48 overflow-hidden">
|
||||
<code class="text-xs font-mono text-(--color-text-soft) bg-(--color-bg-mute) px-1.5 py-0.5 rounded break-all">{{ param.type }}</code>
|
||||
</td>
|
||||
<td class="py-2 pr-4">
|
||||
<code v-if="param.defaultValue" class="text-xs font-mono text-(--color-text-mute)">{{ param.defaultValue }}</code>
|
||||
<span v-else class="text-(--color-text-mute)">—</span>
|
||||
</td>
|
||||
<td class="py-2 text-(--color-text-soft)">
|
||||
{{ param.description || '—' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
45
docs/app/components/DocsPropsTable.vue
Normal file
45
docs/app/components/DocsPropsTable.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { PropertyMeta } from '../../modules/extractor/types';
|
||||
|
||||
defineProps<{
|
||||
properties: PropertyMeta[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="properties.length > 0" class="overflow-x-auto max-w-full">
|
||||
<table class="w-full text-sm border-collapse table-fixed">
|
||||
<thead>
|
||||
<tr class="border-b border-(--color-border)">
|
||||
<th class="text-left py-2 pr-4 font-medium text-(--color-text-soft)">Property</th>
|
||||
<th class="text-left py-2 pr-4 font-medium text-(--color-text-soft)">Type</th>
|
||||
<th class="text-left py-2 pr-4 font-medium text-(--color-text-soft)">Default</th>
|
||||
<th class="text-left py-2 font-medium text-(--color-text-soft)">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="prop in properties"
|
||||
:key="prop.name"
|
||||
class="border-b border-(--color-border) last:border-b-0"
|
||||
>
|
||||
<td class="py-2 pr-4">
|
||||
<code class="text-brand-600 font-mono text-xs">{{ prop.name }}</code>
|
||||
<span v-if="prop.readonly" class="text-(--color-text-mute) text-[10px] ml-1 uppercase">readonly</span>
|
||||
<span v-if="prop.optional" class="text-(--color-text-mute) text-xs ml-1">?</span>
|
||||
</td>
|
||||
<td class="py-2 pr-4 max-w-48 overflow-hidden">
|
||||
<code class="text-xs font-mono text-(--color-text-soft) bg-(--color-bg-mute) px-1.5 py-0.5 rounded break-all">{{ prop.type }}</code>
|
||||
</td>
|
||||
<td class="py-2 pr-4">
|
||||
<code v-if="prop.defaultValue" class="text-xs font-mono text-(--color-text-mute)">{{ prop.defaultValue }}</code>
|
||||
<span v-else class="text-(--color-text-mute)">—</span>
|
||||
</td>
|
||||
<td class="py-2 text-(--color-text-soft)">
|
||||
{{ prop.description || '—' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
120
docs/app/components/DocsSearch.vue
Normal file
120
docs/app/components/DocsSearch.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
const { searchItems } = useDocs();
|
||||
|
||||
const isOpen = ref(false);
|
||||
const query = ref('');
|
||||
|
||||
const results = computed(() => searchItems(query.value).slice(0, 20));
|
||||
|
||||
function open() {
|
||||
isOpen.value = true;
|
||||
nextTick(() => {
|
||||
const input = document.querySelector<HTMLInputElement>('[data-search-input]');
|
||||
input?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function close() {
|
||||
isOpen.value = false;
|
||||
query.value = '';
|
||||
}
|
||||
|
||||
// Keyboard shortcut: Cmd+K / Ctrl+K
|
||||
if (import.meta.client) {
|
||||
useEventListener(window, 'keydown', (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
if (isOpen.value) close();
|
||||
else open();
|
||||
}
|
||||
if (e.key === 'Escape' && isOpen.value) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function useEventListener(target: Window, event: string, handler: (e: any) => void) {
|
||||
onMounted(() => target.addEventListener(event, handler));
|
||||
onUnmounted(() => target.removeEventListener(event, handler));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<button
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-sm text-(--color-text-mute) bg-(--color-bg-mute) border border-(--color-border) rounded-lg hover:border-(--color-text-mute) transition-colors"
|
||||
@click="open"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Search...</span>
|
||||
<kbd class="hidden sm:inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] font-mono bg-(--color-bg) border border-(--color-border) rounded">
|
||||
<span>⌘</span>K
|
||||
</kbd>
|
||||
</button>
|
||||
|
||||
<!-- Search modal -->
|
||||
<Teleport to="body">
|
||||
<Transition
|
||||
enter-active-class="duration-200 ease-out"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="duration-150 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div v-if="isOpen" class="fixed inset-0 z-50">
|
||||
<div class="fixed inset-0 bg-black/50" @click="close" />
|
||||
|
||||
<div class="fixed inset-x-0 top-[10vh] mx-auto max-w-lg px-4">
|
||||
<div class="bg-(--color-bg) rounded-xl border border-(--color-border) shadow-2xl overflow-hidden">
|
||||
<div class="flex items-center px-4 border-b border-(--color-border)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-(--color-text-mute) shrink-0">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
<input
|
||||
v-model="query"
|
||||
data-search-input
|
||||
type="text"
|
||||
placeholder="Search documentation..."
|
||||
class="w-full py-3 px-3 bg-transparent text-(--color-text) placeholder:text-(--color-text-mute) focus:outline-none"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="max-h-80 overflow-y-auto">
|
||||
<div v-if="query && results.length === 0" class="py-8 text-center text-sm text-(--color-text-mute)">
|
||||
No results found
|
||||
</div>
|
||||
<ul v-else-if="results.length > 0" class="py-2">
|
||||
<li v-for="{ pkg, item } in results" :key="`${pkg.slug}-${item.slug}`">
|
||||
<NuxtLink
|
||||
:to="`/${pkg.slug}/${item.slug}`"
|
||||
class="flex items-center gap-3 px-4 py-2.5 hover:bg-(--color-bg-mute) transition-colors"
|
||||
@click="close"
|
||||
>
|
||||
<DocsBadge :kind="item.kind" size="sm" />
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-medium text-(--color-text) truncate">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div class="text-xs text-(--color-text-mute) truncate">
|
||||
{{ pkg.name }} · {{ item.description }}
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="py-8 text-center text-sm text-(--color-text-mute)">
|
||||
Type to search...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
24
docs/app/components/DocsTag.vue
Normal file
24
docs/app/components/DocsTag.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string;
|
||||
variant?: 'since' | 'test' | 'demo' | 'wip';
|
||||
}>();
|
||||
|
||||
const variantClasses: Record<string, string> = {
|
||||
since: 'bg-(--color-bg-mute) text-(--color-text-mute)',
|
||||
test: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400',
|
||||
demo: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400',
|
||||
wip: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-full',
|
||||
variantClasses[variant ?? 'since'],
|
||||
]"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
</template>
|
||||
Reference in New Issue
Block a user