You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
292 lines
5.5 KiB
292 lines
5.5 KiB
<script lang="ts">
|
|
import { tokenize } from '../_lib/tokenize';
|
|
|
|
let {
|
|
code,
|
|
lang = 'ts',
|
|
title,
|
|
showLineNumbers = false
|
|
}: { code: string; lang?: string; title?: string; showLineNumbers?: boolean } = $props();
|
|
|
|
let copied = $state(false);
|
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
const tokens = $derived(tokenize(code, lang));
|
|
const lines = $derived(showLineNumbers ? code.split('\n') : []);
|
|
|
|
async function copy() {
|
|
try {
|
|
await navigator.clipboard.writeText(code);
|
|
copied = true;
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => (copied = false), 1400);
|
|
} catch {
|
|
/* clipboard denied */
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<figure class="block">
|
|
<header>
|
|
{#if title}
|
|
<span class="title">{title}</span>
|
|
{/if}
|
|
<span class="lang">{lang}</span>
|
|
<button
|
|
class="copy"
|
|
type="button"
|
|
onclick={copy}
|
|
aria-label={copied ? 'Copied' : 'Copy code'}
|
|
>
|
|
{#if copied}
|
|
<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true">
|
|
<path
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="m3.5 8.5 3 3 6-7"
|
|
/>
|
|
</svg>
|
|
Copied
|
|
{:else}
|
|
<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true">
|
|
<rect
|
|
x="4.5"
|
|
y="4.5"
|
|
width="8"
|
|
height="9"
|
|
rx="1.5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="1.4"
|
|
/>
|
|
<path
|
|
d="M2.5 11V3.5A1.5 1.5 0 0 1 4 2h6.5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="1.4"
|
|
stroke-linecap="round"
|
|
/>
|
|
</svg>
|
|
Copy
|
|
{/if}
|
|
</button>
|
|
</header>
|
|
<pre class:numbered={showLineNumbers}><code>{#if showLineNumbers}<span class="lines" aria-hidden="true">{#each lines as _, idx}<span class="ln">{idx + 1}
|
|
</span>{/each}</span><span class="src">{#each tokens as tok, i (i)}<span class="t-{tok.k}">{tok.t}</span>{/each}</span>{:else}{#each tokens as tok, i (i)}<span class="t-{tok.k}">{tok.t}</span>{/each}{/if}</code></pre>
|
|
</figure>
|
|
|
|
<style>
|
|
.block {
|
|
margin: 1.25rem 0;
|
|
border: 1px solid var(--c-code-border);
|
|
border-radius: var(--r-lg);
|
|
overflow: hidden;
|
|
background: var(--c-code-bg);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.4rem 0.75rem 0.4rem 1rem;
|
|
font-size: 0.75rem;
|
|
color: var(--c-fg-subtle);
|
|
background: var(--c-bg-subtle);
|
|
border-bottom: 1px solid var(--c-code-border);
|
|
}
|
|
|
|
.title {
|
|
font-family: var(--font-mono);
|
|
color: var(--c-fg-muted);
|
|
}
|
|
|
|
.lang {
|
|
margin-left: auto;
|
|
font-family: var(--font-mono);
|
|
text-transform: lowercase;
|
|
padding: 0.1rem 0.4rem;
|
|
background: var(--c-bg-muted);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: var(--r-sm);
|
|
color: var(--c-fg-subtle);
|
|
}
|
|
|
|
.copy {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.3rem;
|
|
padding: 0.25rem 0.55rem;
|
|
font-size: 0.75rem;
|
|
color: var(--c-fg-muted);
|
|
background: transparent;
|
|
border: 1px solid var(--c-border);
|
|
border-radius: var(--r-sm);
|
|
transition:
|
|
color 120ms var(--ease),
|
|
background-color 120ms var(--ease),
|
|
border-color 120ms var(--ease);
|
|
}
|
|
|
|
.copy:hover {
|
|
color: var(--c-fg);
|
|
background: var(--c-bg-muted);
|
|
border-color: var(--c-border-strong);
|
|
}
|
|
|
|
pre {
|
|
margin: 0;
|
|
border: none;
|
|
border-radius: 0;
|
|
background: transparent;
|
|
}
|
|
|
|
pre.numbered code {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
gap: 0 1rem;
|
|
}
|
|
|
|
.lines {
|
|
display: block;
|
|
text-align: right;
|
|
color: var(--c-fg-subtle);
|
|
opacity: 0.6;
|
|
user-select: none;
|
|
padding-right: 0.5rem;
|
|
border-right: 1px solid var(--c-border);
|
|
}
|
|
|
|
.ln {
|
|
display: block;
|
|
}
|
|
|
|
/* ── Token colors (light) ─────────────────────────────── */
|
|
|
|
:global([class*='t-']) {
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.t-plain {
|
|
color: var(--c-code-fg);
|
|
}
|
|
|
|
.t-keyword {
|
|
color: #a21caf; /* fuchsia-700 */
|
|
font-weight: 500;
|
|
}
|
|
|
|
.t-literal {
|
|
color: #b45309; /* amber-700 */
|
|
font-weight: 500;
|
|
}
|
|
|
|
.t-string,
|
|
.t-string-escape {
|
|
color: #15803d; /* green-700 */
|
|
}
|
|
|
|
.t-string-interp {
|
|
color: #ca8a04;
|
|
}
|
|
|
|
.t-number {
|
|
color: #c2410c; /* orange-700 */
|
|
}
|
|
|
|
.t-comment {
|
|
color: #71717a;
|
|
font-style: italic;
|
|
}
|
|
|
|
.t-type {
|
|
color: #0369a1; /* sky-700 */
|
|
}
|
|
|
|
.t-function {
|
|
color: #7e22ce; /* purple-700 */
|
|
}
|
|
|
|
.t-property {
|
|
color: #0f766e; /* teal-700 */
|
|
}
|
|
|
|
.t-operator {
|
|
color: #52525b;
|
|
}
|
|
|
|
.t-punctuation {
|
|
color: var(--c-fg);
|
|
}
|
|
|
|
.t-tag {
|
|
color: #be185d; /* pink-700 */
|
|
}
|
|
|
|
.t-tag-close {
|
|
color: #db2777;
|
|
}
|
|
|
|
.t-attr {
|
|
color: #92400e; /* amber-800 */
|
|
}
|
|
|
|
/* ── Token colors (dark) ──────────────────────────────── */
|
|
|
|
:global([data-theme='dark']) .t-plain {
|
|
color: var(--c-code-fg);
|
|
}
|
|
|
|
:global([data-theme='dark']) .t-keyword {
|
|
color: #f472b6;
|
|
}
|
|
|
|
:global([data-theme='dark']) .t-literal {
|
|
color: #fbbf24;
|
|
}
|
|
|
|
:global([data-theme='dark']) .t-string,
|
|
:global([data-theme='dark']) .t-string-escape {
|
|
color: #86efac;
|
|
}
|
|
|
|
:global([data-theme='dark']) .t-number {
|
|
color: #fdba74;
|
|
}
|
|
|
|
:global([data-theme='dark']) .t-comment {
|
|
color: #71717a;
|
|
}
|
|
|
|
:global([data-theme='dark']) .t-type {
|
|
color: #7dd3fc;
|
|
}
|
|
|
|
:global([data-theme='dark']) .t-function {
|
|
color: #c4b5fd;
|
|
}
|
|
|
|
:global([data-theme='dark']) .t-property {
|
|
color: #5eead4;
|
|
}
|
|
|
|
:global([data-theme='dark']) .t-operator {
|
|
color: #a1a1aa;
|
|
}
|
|
|
|
:global([data-theme='dark']) .t-tag {
|
|
color: #f472b6;
|
|
}
|
|
|
|
:global([data-theme='dark']) .t-tag-close {
|
|
color: #fb7185;
|
|
}
|
|
|
|
:global([data-theme='dark']) .t-attr {
|
|
color: #fbbf24;
|
|
}
|
|
</style>
|