chore(docs): eslint migration + extractor updates
Migrate docs to eslint flat config (build-script console override); doc extractor points at configs/eslint.
This commit is contained in:
@@ -4,12 +4,14 @@
|
||||
}>();
|
||||
|
||||
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',
|
||||
function: 'bg-blue-100 text-blue-700 dark:bg-blue-500/15 dark:text-blue-300',
|
||||
class: 'bg-violet-100 text-violet-700 dark:bg-violet-500/15 dark:text-violet-300',
|
||||
interface: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300',
|
||||
type: 'bg-amber-100 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300',
|
||||
enum: 'bg-rose-100 text-rose-700 dark:bg-rose-500/15 dark:text-rose-300',
|
||||
variable: 'bg-zinc-100 text-zinc-600 dark:bg-zinc-500/15 dark:text-zinc-300',
|
||||
component: 'bg-fuchsia-100 text-fuchsia-700 dark:bg-fuchsia-500/15 dark:text-fuchsia-300',
|
||||
guide: 'bg-teal-100 text-teal-700 dark:bg-teal-500/15 dark:text-teal-300',
|
||||
};
|
||||
|
||||
const kindLabels: Record<string, string> = {
|
||||
@@ -19,13 +21,15 @@ const kindLabels: Record<string, string> = {
|
||||
type: 'T',
|
||||
enum: 'E',
|
||||
variable: 'V',
|
||||
component: '◇',
|
||||
guide: '¶',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center justify-center rounded font-mono font-medium shrink-0',
|
||||
'inline-flex items-center justify-center rounded-md font-mono font-semibold shrink-0',
|
||||
kindColors[kind] ?? kindColors.variable,
|
||||
size === 'sm' ? 'w-5 h-5 text-[10px]' : 'w-6 h-6 text-xs',
|
||||
]"
|
||||
|
||||
@@ -1,27 +1,81 @@
|
||||
<script setup lang="ts">const props = defineProps<{
|
||||
code: string;
|
||||
lang?: string;
|
||||
/** Show the header bar with language label + copy button */
|
||||
bare?: boolean;
|
||||
}>();
|
||||
|
||||
const { highlight } = useShiki();
|
||||
const html = ref('');
|
||||
|
||||
onMounted(async () => {
|
||||
html.value = await highlight(props.code, props.lang ?? 'typescript');
|
||||
});
|
||||
const resolvedLang = computed(() => props.lang ?? 'typescript');
|
||||
const langLabel = computed(() => ({
|
||||
typescript: 'ts',
|
||||
javascript: 'js',
|
||||
bash: 'sh',
|
||||
vue: 'vue',
|
||||
json: 'json',
|
||||
}[resolvedLang.value] ?? resolvedLang.value));
|
||||
|
||||
watch(() => props.code, async (newCode) => {
|
||||
html.value = await highlight(newCode, props.lang ?? 'typescript');
|
||||
});
|
||||
async function render() {
|
||||
html.value = await highlight(props.code, resolvedLang.value);
|
||||
}
|
||||
|
||||
onMounted(render);
|
||||
watch(() => props.code, render);
|
||||
|
||||
const copied = ref(false);
|
||||
let copyTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
async function copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(props.code);
|
||||
copied.value = true;
|
||||
clearTimeout(copyTimer);
|
||||
copyTimer = setTimeout(() => (copied.value = false), 1500);
|
||||
}
|
||||
catch { /* clipboard unavailable */ }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="code-block relative group rounded-lg border border-(--color-border) overflow-hidden max-w-full">
|
||||
<div class="group relative rounded-xl border border-(--border) bg-(--bg-subtle) overflow-hidden max-w-full">
|
||||
<div v-if="!bare" class="flex items-center justify-between px-3 h-9 border-b border-(--border) bg-(--bg-subtle)">
|
||||
<span class="text-[11px] font-mono uppercase tracking-wider text-(--fg-subtle)">{{ langLabel }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 text-[11px] font-medium text-(--fg-subtle) hover:text-(--fg) transition-colors cursor-pointer"
|
||||
@click="copy"
|
||||
>
|
||||
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-emerald-500">
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
{{ copied ? 'Copied' : 'Copy' }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="absolute right-2 top-2 z-10 inline-flex items-center justify-center w-7 h-7 rounded-md bg-(--bg-elevated) border border-(--border) text-(--fg-subtle) opacity-0 group-hover:opacity-100 hover:text-(--fg) transition-all cursor-pointer"
|
||||
title="Copy"
|
||||
@click="copy"
|
||||
>
|
||||
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-emerald-500">
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="html"
|
||||
class="overflow-x-auto text-sm leading-relaxed [&_pre]:p-4 [&_pre]:m-0 [&_pre]:min-w-0"
|
||||
class="overflow-x-auto text-[13px] leading-relaxed [&_pre]:p-4 [&_pre]:m-0 [&_pre]:bg-transparent! [&_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>
|
||||
<pre v-else class="p-4 text-[13px] overflow-x-auto"><code>{{ code }}</code></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">import type { ComponentMeta } from '../../modules/extractor/types';
|
||||
|
||||
const props = defineProps<{
|
||||
component: ComponentMeta;
|
||||
packageName: string;
|
||||
}>();
|
||||
|
||||
const importPath = computed(() => {
|
||||
const sub = props.component.entryPoint.replace(/^\.\/?/, '');
|
||||
return sub ? `${props.packageName}/${sub}` : props.packageName;
|
||||
});
|
||||
|
||||
const partNames = computed(() => props.component.parts.map(p => p.name));
|
||||
|
||||
/** Import statement + a composition skeleton (Root wraps the remaining parts). */
|
||||
const anatomyCode = computed(() => {
|
||||
const names = partNames.value;
|
||||
if (names.length === 0) return '';
|
||||
|
||||
const imports = `import {\n${names.map(n => ` ${n},`).join('\n')}\n} from '${importPath.value}';`;
|
||||
|
||||
const [root, ...rest] = names;
|
||||
let tree: string;
|
||||
if (rest.length === 0) {
|
||||
tree = `<${root} />`;
|
||||
}
|
||||
else {
|
||||
tree = `<${root}>\n${rest.map(n => ` <${n} />`).join('\n')}\n</${root}>`;
|
||||
}
|
||||
|
||||
return `${imports}\n\n${tree}`;
|
||||
});
|
||||
|
||||
const roleColor: Record<string, string> = {
|
||||
Root: 'bg-fuchsia-100 text-fuchsia-700 dark:bg-fuchsia-500/15 dark:text-fuchsia-300',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-10">
|
||||
<!-- Anatomy snippet -->
|
||||
<section>
|
||||
<h2 class="text-xs font-semibold uppercase tracking-wider text-(--fg-subtle) mb-3">
|
||||
Anatomy
|
||||
</h2>
|
||||
<p class="text-sm text-(--fg-muted) mb-3">
|
||||
Import the parts and compose them. Each part forwards attributes to its underlying element.
|
||||
</p>
|
||||
<DocsCode :code="anatomyCode" lang="vue" />
|
||||
</section>
|
||||
|
||||
<!-- Parts -->
|
||||
<section>
|
||||
<h2 class="text-xs font-semibold uppercase tracking-wider text-(--fg-subtle) mb-4">
|
||||
API Reference
|
||||
</h2>
|
||||
<div class="space-y-8">
|
||||
<div
|
||||
v-for="part in component.parts"
|
||||
:id="part.name.toLowerCase()"
|
||||
:key="part.name"
|
||||
class="scroll-mt-20"
|
||||
>
|
||||
<div class="flex items-center gap-2.5 mb-2">
|
||||
<h3 class="font-mono text-base font-semibold text-(--fg)">{{ part.name }}</h3>
|
||||
<span
|
||||
:class="[
|
||||
'text-[11px] px-2 py-0.5 rounded-full font-medium leading-none',
|
||||
roleColor[part.role] ?? 'bg-(--bg-inset) text-(--fg-muted) border border-(--border)',
|
||||
]"
|
||||
>
|
||||
{{ part.role }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p v-if="part.description" class="text-sm text-(--fg-muted) mb-3 max-w-2xl">
|
||||
{{ part.description }}
|
||||
</p>
|
||||
|
||||
<div v-if="part.props.length > 0" class="mb-3">
|
||||
<DocsPropsTable :properties="part.props" label="Prop" />
|
||||
</div>
|
||||
|
||||
<div v-if="part.emits.length > 0" class="mb-3">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wider text-(--fg-subtle) mb-2">Emits</div>
|
||||
<DocsEmitsTable :emits="part.emits" />
|
||||
</div>
|
||||
|
||||
<p v-if="part.props.length === 0 && part.emits.length === 0" class="text-sm text-(--fg-subtle) italic">
|
||||
No props or events — renders its element and forwards attributes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -13,49 +13,30 @@ 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">
|
||||
<div class="rounded-xl border border-(--border) overflow-hidden">
|
||||
<!-- Live demo -->
|
||||
<div class="p-6 bg-(--color-bg-soft)">
|
||||
<div class="p-8 bg-(--bg-subtle) flex items-center justify-center min-h-32">
|
||||
<component :is="component" />
|
||||
</div>
|
||||
|
||||
<!-- Source toggle bar -->
|
||||
<div class="flex items-center border-t border-(--color-border) bg-(--color-bg)">
|
||||
<div class="flex items-center border-t border-(--border) bg-(--bg-elevated)">
|
||||
<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"
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 px-4 py-2.5 text-xs font-medium text-(--fg-muted) hover:text-(--fg) 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 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' : ''"
|
||||
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>
|
||||
@@ -63,8 +44,8 @@ watch(showSource, async (show) => {
|
||||
</div>
|
||||
|
||||
<!-- Source code -->
|
||||
<div v-if="showSource" class="border-t border-(--color-border)">
|
||||
<div class="overflow-x-auto text-sm" v-html="highlighted" />
|
||||
<div v-if="showSource" class="border-t border-(--border) bg-(--bg-subtle)">
|
||||
<div class="overflow-x-auto text-[13px] [&_pre]:p-4 [&_pre]:m-0 [&_pre]:bg-transparent!" v-html="highlighted" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">import type { EmitMeta } from '../../modules/extractor/types';
|
||||
|
||||
defineProps<{
|
||||
emits: EmitMeta[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="emits.length > 0" class="overflow-hidden rounded-xl border border-(--border)">
|
||||
<table class="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-(--bg-subtle) text-left">
|
||||
<th class="py-2.5 px-4 font-medium text-(--fg-muted) text-xs uppercase tracking-wider">Event</th>
|
||||
<th class="py-2.5 px-4 font-medium text-(--fg-muted) text-xs uppercase tracking-wider">Payload</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="e in emits" :key="e.name" class="border-t border-(--border) align-top">
|
||||
<td class="py-2.5 px-4 whitespace-nowrap">
|
||||
<code class="text-(--accent-text) font-mono text-[13px] font-medium">{{ e.name }}</code>
|
||||
</td>
|
||||
<td class="py-2.5 px-4">
|
||||
<code class="text-xs font-mono text-(--fg-muted) bg-(--bg-inset) px-1.5 py-0.5 rounded border border-(--border) wrap-break-word">{{ e.payload }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">const props = defineProps<{
|
||||
source: string;
|
||||
}>();
|
||||
|
||||
const { highlight } = useShiki();
|
||||
|
||||
const html = computed(() => renderMarkdown(props.source));
|
||||
const root = ref<HTMLElement | null>(null);
|
||||
|
||||
/** Replace marked's <pre><code> blocks with Shiki-highlighted markup. */
|
||||
async function highlightCodeBlocks() {
|
||||
const el = root.value;
|
||||
if (!el) return;
|
||||
|
||||
const blocks = Array.from(el.querySelectorAll('pre > code'));
|
||||
for (const code of blocks) {
|
||||
const langClass = Array.from(code.classList).find(c => c.startsWith('language-'));
|
||||
const lang = langClass?.replace('language-', '') || 'typescript';
|
||||
const supported = ['typescript', 'javascript', 'ts', 'js', 'vue', 'json', 'bash', 'sh'];
|
||||
const resolved = supported.includes(lang)
|
||||
? ({ ts: 'typescript', js: 'javascript', sh: 'bash' }[lang] ?? lang)
|
||||
: 'typescript';
|
||||
|
||||
const text = code.textContent ?? '';
|
||||
const pre = code.parentElement;
|
||||
if (!pre) continue;
|
||||
|
||||
try {
|
||||
const out = await highlight(text, resolved);
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'not-prose rounded-xl border border-(--border) bg-(--bg-subtle) overflow-x-auto text-[13px] my-5 [&_pre]:p-4 [&_pre]:m-0 [&_pre]:bg-transparent!';
|
||||
wrapper.innerHTML = out;
|
||||
pre.replaceWith(wrapper);
|
||||
}
|
||||
catch { /* leave the fallback <pre> as-is */ }
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(highlightCodeBlocks);
|
||||
watch(() => props.source, async () => {
|
||||
await nextTick();
|
||||
await highlightCodeBlocks();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="root" class="prose-docs max-w-none" v-html="html" />
|
||||
</template>
|
||||
@@ -6,23 +6,23 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="methods.length > 0" class="space-y-4">
|
||||
<div v-if="methods.length > 0" class="space-y-3">
|
||||
<div
|
||||
v-for="method in methods"
|
||||
:key="method.name"
|
||||
class="border border-(--color-border) rounded-lg p-4"
|
||||
class="rounded-xl border border-(--border) bg-(--bg-subtle) 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>
|
||||
<code class="text-sm font-mono font-semibold text-(--fg)">{{ 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)"
|
||||
class="text-[10px] uppercase px-1.5 py-0.5 rounded bg-(--bg-inset) border border-(--border) text-(--fg-subtle)"
|
||||
>
|
||||
{{ method.visibility }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p v-if="method.description" class="text-sm text-(--color-text-soft) mb-3">
|
||||
<p v-if="method.description" class="text-sm text-(--fg-muted) mb-3">
|
||||
{{ method.description }}
|
||||
</p>
|
||||
|
||||
@@ -36,9 +36,9 @@ defineProps<{
|
||||
<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>
|
||||
<span class="text-(--fg-subtle)">Returns</span>
|
||||
<code class="ml-1.5 text-xs font-mono bg-(--bg-inset) border border-(--border) px-1.5 py-0.5 rounded">{{ method.returns.type }}</code>
|
||||
<span v-if="method.returns.description" class="ml-2 text-(--fg-muted)">{{ method.returns.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,34 +6,33 @@ defineProps<{
|
||||
</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">
|
||||
<div v-if="params.length > 0" class="overflow-hidden rounded-xl border border-(--border)">
|
||||
<table class="w-full text-sm border-collapse">
|
||||
<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 class="bg-(--bg-subtle) text-left">
|
||||
<th class="py-2.5 px-4 font-medium text-(--fg-muted) text-xs uppercase tracking-wider">Parameter</th>
|
||||
<th class="py-2.5 px-4 font-medium text-(--fg-muted) text-xs uppercase tracking-wider">Type</th>
|
||||
<th class="py-2.5 px-4 font-medium text-(--fg-muted) text-xs uppercase tracking-wider hidden sm:table-cell">Default</th>
|
||||
<th class="py-2.5 px-4 font-medium text-(--fg-muted) text-xs uppercase tracking-wider">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="param in params"
|
||||
:key="param.name"
|
||||
class="border-b border-(--color-border) last:border-b-0"
|
||||
class="border-t border-(--border) align-top"
|
||||
>
|
||||
<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 class="py-2.5 px-4 whitespace-nowrap">
|
||||
<code class="text-(--accent-text) font-mono text-[13px] font-medium">{{ param.name }}</code><span v-if="param.optional" class="text-(--fg-subtle) text-xs">?</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 class="py-2.5 px-4">
|
||||
<code class="text-xs font-mono text-(--fg-muted) bg-(--bg-inset) px-1.5 py-0.5 rounded border border-(--border) wrap-break-word">{{ 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 class="py-2.5 px-4 hidden sm:table-cell">
|
||||
<code v-if="param.defaultValue" class="text-xs font-mono text-(--fg-muted)">{{ param.defaultValue }}</code>
|
||||
<span v-else class="text-(--fg-subtle)">—</span>
|
||||
</td>
|
||||
<td class="py-2 text-(--color-text-soft)">
|
||||
<td class="py-2.5 px-4 text-(--fg-muted) min-w-48">
|
||||
{{ param.description || '—' }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -2,39 +2,40 @@
|
||||
|
||||
defineProps<{
|
||||
properties: PropertyMeta[];
|
||||
/** Column label for the first column */
|
||||
label?: string;
|
||||
}>();
|
||||
</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">
|
||||
<div v-if="properties.length > 0" class="overflow-hidden rounded-xl border border-(--border)">
|
||||
<table class="w-full text-sm border-collapse">
|
||||
<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 class="bg-(--bg-subtle) text-left">
|
||||
<th class="py-2.5 px-4 font-medium text-(--fg-muted) text-xs uppercase tracking-wider">{{ label ?? 'Property' }}</th>
|
||||
<th class="py-2.5 px-4 font-medium text-(--fg-muted) text-xs uppercase tracking-wider">Type</th>
|
||||
<th class="py-2.5 px-4 font-medium text-(--fg-muted) text-xs uppercase tracking-wider hidden sm:table-cell">Default</th>
|
||||
<th class="py-2.5 px-4 font-medium text-(--fg-muted) text-xs uppercase tracking-wider">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="prop in properties"
|
||||
:key="prop.name"
|
||||
class="border-b border-(--color-border) last:border-b-0"
|
||||
class="border-t border-(--border) align-top"
|
||||
>
|
||||
<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 class="py-2.5 px-4 whitespace-nowrap">
|
||||
<code class="text-(--accent-text) font-mono text-[13px] font-medium">{{ prop.name }}</code><span v-if="prop.optional" class="text-(--fg-subtle) text-xs">?</span>
|
||||
<span v-if="prop.readonly" class="block text-[10px] text-(--fg-subtle) uppercase tracking-wide mt-0.5">readonly</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 class="py-2.5 px-4">
|
||||
<code class="text-xs font-mono text-(--fg-muted) bg-(--bg-inset) px-1.5 py-0.5 rounded border border-(--border) wrap-break-word">{{ 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 class="py-2.5 px-4 hidden sm:table-cell">
|
||||
<code v-if="prop.defaultValue" class="text-xs font-mono text-(--fg-muted)">{{ prop.defaultValue }}</code>
|
||||
<span v-else class="text-(--fg-subtle)">—</span>
|
||||
</td>
|
||||
<td class="py-2 text-(--color-text-soft)">
|
||||
<td class="py-2.5 px-4 text-(--fg-muted) min-w-48">
|
||||
{{ prop.description || '—' }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<script setup lang="ts">const { searchItems } = useDocs();
|
||||
<script setup lang="ts">const { search } = useDocs();
|
||||
|
||||
const isOpen = ref(false);
|
||||
const query = ref('');
|
||||
const activeIndex = ref(0);
|
||||
|
||||
const results = computed(() => searchItems(query.value).slice(0, 20));
|
||||
const results = computed(() => search(query.value).slice(0, 24));
|
||||
|
||||
watch(results, () => {
|
||||
activeIndex.value = 0;
|
||||
});
|
||||
|
||||
function open() {
|
||||
isOpen.value = true;
|
||||
nextTick(() => {
|
||||
const input = document.querySelector<HTMLInputElement>('[data-search-input]');
|
||||
input?.focus();
|
||||
});
|
||||
nextTick(() => document.querySelector<HTMLInputElement>('[data-search-input]')?.focus());
|
||||
}
|
||||
|
||||
function close() {
|
||||
@@ -18,96 +20,110 @@ function close() {
|
||||
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();
|
||||
}
|
||||
});
|
||||
const router = useRouter();
|
||||
function goTo(slug: string) {
|
||||
const r = results.value[activeIndex.value];
|
||||
// slug param kept for click handlers that pass an explicit target
|
||||
const target = slug || (r ? `/${r.pkg.slug}/${r.slug}` : '');
|
||||
if (target) {
|
||||
router.push(target);
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
function useEventListener(target: Window, event: string, handler: (e: any) => void) {
|
||||
onMounted(() => target.addEventListener(event, handler));
|
||||
onUnmounted(() => target.removeEventListener(event, handler));
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
if (isOpen.value) close();
|
||||
else open();
|
||||
return;
|
||||
}
|
||||
if (!isOpen.value) return;
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
close();
|
||||
}
|
||||
else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
activeIndex.value = Math.min(activeIndex.value + 1, results.value.length - 1);
|
||||
}
|
||||
else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
activeIndex.value = Math.max(activeIndex.value - 1, 0);
|
||||
}
|
||||
else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
goTo('');
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => globalThis.addEventListener('keydown', onKeydown));
|
||||
onUnmounted(() => globalThis.removeEventListener('keydown', onKeydown));
|
||||
</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"
|
||||
type="button"
|
||||
class="flex items-center gap-2 px-2.5 h-9 text-sm text-(--fg-subtle) bg-(--bg-subtle) border border-(--border) rounded-lg hover:border-(--border-strong) transition-colors w-9 sm:w-56 justify-center sm:justify-start cursor-pointer"
|
||||
@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 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="shrink-0">
|
||||
<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>
|
||||
<span class="hidden sm:inline flex-1 text-left">Search…</span>
|
||||
<kbd class="hidden sm:inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] font-mono bg-(--bg) border border-(--border) rounded text-(--fg-subtle)">⌘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"
|
||||
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 v-if="isOpen" class="fixed inset-0 z-100">
|
||||
<div class="fixed inset-0 bg-black/40 backdrop-blur-sm" @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" />
|
||||
<div class="fixed inset-x-0 top-[12vh] mx-auto max-w-xl px-4">
|
||||
<div class="bg-(--bg-elevated) rounded-2xl border border-(--border) shadow-2xl overflow-hidden">
|
||||
<div class="flex items-center px-4 border-b border-(--border)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-(--fg-subtle) 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"
|
||||
placeholder="Search across all packages…"
|
||||
class="w-full py-3.5 px-3 bg-transparent text-(--fg) placeholder:text-(--fg-subtle) focus:outline-none text-[15px]"
|
||||
>
|
||||
<kbd class="hidden sm:inline-flex items-center px-1.5 py-0.5 text-[10px] font-mono bg-(--bg-inset) border border-(--border) rounded text-(--fg-subtle)">ESC</kbd>
|
||||
</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 class="max-h-[60vh] overflow-y-auto p-2">
|
||||
<div v-if="query && results.length === 0" class="py-12 text-center text-sm text-(--fg-subtle)">
|
||||
No results for "{{ query }}"
|
||||
</div>
|
||||
<ul v-else-if="results.length > 0" class="py-2">
|
||||
<li v-for="{ pkg, item } in results" :key="`${pkg.slug}-${item.slug}`">
|
||||
<ul v-else-if="results.length > 0" class="space-y-0.5">
|
||||
<li v-for="(r, i) in results" :key="`${r.pkg.slug}-${r.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"
|
||||
:to="`/${r.pkg.slug}/${r.slug}`"
|
||||
:class="[
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors',
|
||||
i === activeIndex ? 'bg-(--accent-subtle)' : 'hover:bg-(--bg-inset)',
|
||||
]"
|
||||
@click="close"
|
||||
@mouseenter="activeIndex = i"
|
||||
>
|
||||
<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>
|
||||
<DocsBadge :kind="r.badge" size="sm" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-sm font-medium text-(--fg) truncate">{{ r.name }}</div>
|
||||
<div class="text-xs text-(--fg-subtle) truncate">{{ r.pkg.name }} · {{ r.description }}</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="py-8 text-center text-sm text-(--color-text-mute)">
|
||||
Type to search...
|
||||
<div v-else class="py-12 text-center text-sm text-(--fg-subtle)">
|
||||
Type to search functions, components & guides…
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
<script setup lang="ts">defineProps<{
|
||||
label: string;
|
||||
variant?: 'since' | 'test' | 'demo' | 'wip';
|
||||
variant?: 'since' | 'test' | 'demo' | 'wip' | 'neutral';
|
||||
}>();
|
||||
|
||||
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',
|
||||
since: 'bg-(--bg-inset) text-(--fg-muted) border border-(--border)',
|
||||
neutral: 'bg-(--bg-inset) text-(--fg-muted) border border-(--border)',
|
||||
test: 'bg-emerald-50 text-emerald-700 border border-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-300 dark:border-emerald-500/20',
|
||||
demo: 'bg-blue-50 text-blue-700 border border-blue-200 dark:bg-blue-500/10 dark:text-blue-300 dark:border-blue-500/20',
|
||||
wip: 'bg-amber-50 text-amber-700 border border-amber-200 dark:bg-amber-500/10 dark:text-amber-300 dark:border-amber-500/20',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-full',
|
||||
'inline-flex items-center px-2 py-0.5 text-[11px] font-medium rounded-full leading-none h-5',
|
||||
variantClasses[variant ?? 'since'],
|
||||
]"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">const { preference, cycle } = useTheme();
|
||||
|
||||
const label = computed(() => ({
|
||||
light: 'Light',
|
||||
dark: 'Dark',
|
||||
system: 'System',
|
||||
}[preference.value]));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
:title="`Theme: ${label} (click to change)`"
|
||||
:aria-label="`Theme: ${label}`"
|
||||
class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-(--fg-muted) hover:text-(--fg) hover:bg-(--bg-inset) transition-colors cursor-pointer"
|
||||
@click="cycle"
|
||||
>
|
||||
<ClientOnly>
|
||||
<!-- Light -->
|
||||
<svg v-if="preference === 'light'" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
|
||||
</svg>
|
||||
<!-- Dark -->
|
||||
<svg v-else-if="preference === 'dark'" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
||||
</svg>
|
||||
<!-- System -->
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" />
|
||||
<path d="M8 21h8M12 17v4" />
|
||||
</svg>
|
||||
<template #fallback>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" />
|
||||
<path d="M8 21h8M12 17v4" />
|
||||
</svg>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</button>
|
||||
</template>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">interface TocItem {
|
||||
id: string;
|
||||
text: string;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
items: TocItem[];
|
||||
}>();
|
||||
|
||||
const activeId = ref<string>('');
|
||||
let observer: IntersectionObserver | null = null;
|
||||
|
||||
function setup() {
|
||||
if (!import.meta.client || props.items.length === 0) return;
|
||||
|
||||
observer?.disconnect();
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) activeId.value = entry.target.id;
|
||||
}
|
||||
},
|
||||
{ rootMargin: '0px 0px -75% 0px', threshold: 0 },
|
||||
);
|
||||
|
||||
for (const item of props.items) {
|
||||
const el = document.getElementById(item.id);
|
||||
if (el) observer.observe(el);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => nextTick(setup));
|
||||
watch(() => props.items, () => nextTick(setup));
|
||||
onUnmounted(() => observer?.disconnect());
|
||||
|
||||
function go(id: string) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
activeId.value = id;
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
history.replaceState(null, '', `#${id}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav v-if="items.length > 0" class="text-sm">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wider text-(--fg-subtle) mb-3">
|
||||
On this page
|
||||
</div>
|
||||
<ul class="space-y-1 border-l border-(--border)">
|
||||
<li v-for="item in items" :key="item.id">
|
||||
<a
|
||||
:href="`#${item.id}`"
|
||||
:class="[
|
||||
'block py-1 -ml-px border-l-2 transition-colors',
|
||||
item.depth === 3 ? 'pl-6' : 'pl-4',
|
||||
activeId === item.id
|
||||
? 'border-(--accent) text-(--accent-text) font-medium'
|
||||
: 'border-transparent text-(--fg-muted) hover:text-(--fg)',
|
||||
]"
|
||||
@click.prevent="go(item.id)"
|
||||
>
|
||||
{{ item.text }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
Reference in New Issue
Block a user