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.
svelte-kit-vice/web/routes/active/_components/CodeBlock.svelte

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>

Powered by TurnKey Linux.