feat: add better provider support, icons, regen, and a lot more

This commit is contained in:
Zoe
2026-02-12 14:56:13 +00:00
parent d5a5945c03
commit d29f95bacf
124 changed files with 6374 additions and 1861 deletions
+15
View File
@@ -19,6 +19,21 @@ const render = () => {
}
}
if (node.tagName === 'table') {
return h('div', { class: 'table-wrapper' }, [
h(
'table',
node.properties,
node.children?.map((child: any, index: number) =>
h(resolveComponent('MarkdownAstNode'), {
node: child,
key: `table-child-${index}`
})
)
)
]);
}
return h(
node.tagName,
node.properties,
+15 -3
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { RootContent } from 'hast';
import MarkdownShikiHighlight from './ShikiHighlight.vue';
const props = defineProps<{
content: string;
@@ -92,13 +93,14 @@ watch(parts, async (newParts) => {
hastParts.value = stableBase.concat(latestHast.children);
}
})
</script>
<template>
<div class="prose-wrapper">
<article class="markdown-body">
<MarkdownAstNode v-for="(node, index) in hastParts" :key="`${id}-${index}`" :node="node" />
<template v-for="(node, index) in hastParts" :key="`${id}-${index}`">
<MarkdownAstNode :node="node" v-memo="[node.type === 'text' ? node.value : node.data]" />
</template>
</article>
</div>
</template>
@@ -227,11 +229,16 @@ blockquote {
padding-left: 0.5rem;
}
.table-wrapper {
display: block;
overflow-x: auto;
margin: calc(var(--spacing) * 4) 0;
}
/* TODO: make these tables better, this is literally the first attempt from Gemini 3 flash */
table {
width: 100%;
border-collapse: collapse;
margin: calc(var(--spacing) * 4) 0;
font-size: 0.95rem;
text-align: left;
background-color: var(--color-base);
@@ -283,6 +290,11 @@ label>span {
cursor: text;
}
hr {
margin-top: 1.25rem;
margin-bottom: 1.25rem;
}
.checkbox {
width: min-content;
}
+107 -13
View File
@@ -3,51 +3,138 @@ import { hashSync } from '~/utils/hash';
const props = defineProps<{ code: string; lang: string }>();
const renderId = hashSync(props.code + props.lang);
const codeBlockRef = ref<HTMLDivElement | null>(null);
const codeHeight: Ref<string | number> = ref('auto');
const copied = ref(false);
const collapsed = ref(false);
const { data: html } = useAsyncData<string>(`shiki-${renderId}`, async () => parseCode());
const { data: parsed } = useAsyncData(`shiki-${renderId}`, async () => {
const { html, displayLang } = await parseCode();
return { html, displayLang };
});
const lineNumberWidth = computed(() => {
if (!html.value) return 1;
if (!parsed.value?.html) return 1;
// Count newlines in the generated HTML or the source code
// Using props.code is safer and faster than parsing the HTML string
return props.code.split('\n').length.toString().length;
});
watch(() => props.code, async () => {
html.value = await parseCode();
const { html: codeHtml } = await parseCode();
parsed.value = { html: codeHtml, displayLang: parsed.value!.displayLang };
});
async function parseCode() {
const shiki = await getShikiHighlighter();
let lang = props.lang.toLowerCase();
let displayLang = lang;
try {
shiki.getLanguage(lang);
const shikiLang = shiki.getLanguage(lang);
displayLang = shikiLang.name;
} catch {
lang = 'text';
}
return shiki.codeToHtml(props.code.trim(), {
const html = shiki.codeToHtml(props.code.trim(), {
lang,
themes: { dark: 'vitesse-dark', light: 'vitesse-light' },
});
return { html, displayLang };
}
let copyTimeout: NodeJS.Timeout | null = null;
function copyCode() {
copied.value = true;
navigator.clipboard.writeText(props.code);
if (copyTimeout) clearTimeout(copyTimeout);
copyTimeout = setTimeout(() => {
copied.value = false;
copyTimeout = null;
}, 2000);
}
function collapseCode() {
if (!codeBlockRef.value) return;
collapsed.value = !collapsed.value;
if (collapsed.value) {
codeHeight.value = codeBlockRef.value.scrollHeight;
nextTick(() => {
// since we are changing the height of an element, even though its
// to its own height, we are triggering a reflow, which means that
// if we didnt use requestAnimationFrame, the height would be set
// to 0 before the reflow is complete, which would cause the reflow
// to be ignored, and another one to be triggered with the new height
// of zero, causing the codeblock to snap shut immediately rather than
// animating. Not requestAnimationFrame because it doesnt work
// on firefox, but setTimeout works on both chrome and firefox
setTimeout(() => {
codeHeight.value = 0;
});
});
} else {
codeBlockRef.value!.addEventListener('transitionend', () => {
if (collapsed.value) return;
codeHeight.value = 'auto';
}, { once: true })
const targetHeight = codeBlockRef.value.scrollHeight;
codeHeight.value = targetHeight;
}
}
const codeStyle = computed(() => {
if (typeof codeHeight.value === 'number') {
return `height: ${codeHeight.value}px;`;
} else {
return `height: ${codeHeight.value};`;
}
});
onUnmounted(() => {
if (copyTimeout) clearTimeout(copyTimeout);
});
</script>
<template>
<div class="rounded-xl overflow-hidden code-container" :style="`--line-number-width: ${lineNumberWidth}ch`"
:id="`code-${renderId}`" v-html="html">
<div class="flex flex-col my-2 rounded-xl overflow-hidden">
<div class="flex items-center pl-3 pr-1.5 py-1.5 text-sm font-sans bg-[var(--color-highlight)] justify-between">
<div class="capitalize">
{{ parsed?.displayLang }}
</div>
<div class="flex gap-2">
<button @click="copyCode()"
class="flex items-center px-1 gap-0.5 rounded-md hover:bg-[var(--color-highlight)] transition-colors duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
Copy
<Icon v-if="!copied" name="mynaui:copy" class="text-4 text-[var(--color-text-subtle)]" />
<Icon v-else name="mynaui:check" class="text-4 text-emerald-500" />
</button>
<button @click="collapseCode()"
class="flex items-center justify-center h-5.5 w-5.5 rounded-md hover:bg-[var(--color-highlight)] transition-colors duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<Icon name="mynaui:chevron-down"
:class="['text-4 h-4 w-4 text-[var(--color-text-subtle)] transition-transform duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]', collapsed ? '-rotate-90' : '']" />
</button>
</div>
</div>
<div ref="codeBlockRef"
class="font-mono overflow-hidden code-container transition-height duration-300 ease-in-out"
:style="`--line-number-width: ${lineNumberWidth}ch; ${codeStyle}`" :id="`code-${renderId}`"
v-html="parsed?.html">
</div>
</div>
</template>
<style>
.code-container {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.code-container>pre {
overflow-x: auto;
scrollbar-width: thin;
padding: 1rem;
line-height: 1.625;
line-height: 0;
counter-reset: lines;
}
@@ -67,4 +154,11 @@ async function parseCode() {
.light .code-container>pre>code .line::before {
color: rgba(0, 0, 0, 0.25);
}
.code-container>pre>code {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
line-height: 0;
font-variant-ligatures: none;
}
</style>