SvelteKit-based WebUI (#14839)
This commit is contained in:
committed by
GitHub
parent
8f8f2274ee
commit
a7a98e0fff
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config';
|
||||
import type { Component } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
icon: Component;
|
||||
tooltip: string;
|
||||
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
||||
size?: 'default' | 'sm' | 'lg' | 'icon';
|
||||
class?: string;
|
||||
disabled?: boolean;
|
||||
onclick: () => void;
|
||||
'aria-label'?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
icon,
|
||||
tooltip,
|
||||
variant = 'ghost',
|
||||
size = 'sm',
|
||||
class: className = '',
|
||||
disabled = false,
|
||||
onclick,
|
||||
'aria-label': ariaLabel
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
{variant}
|
||||
{size}
|
||||
{disabled}
|
||||
{onclick}
|
||||
class="h-6 w-6 p-0 {className}"
|
||||
aria-label={ariaLabel || tooltip}
|
||||
>
|
||||
{@const IconComponent = icon}
|
||||
<IconComponent class="h-3 w-3" />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>{tooltip}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
@@ -0,0 +1,86 @@
|
||||
<script lang="ts">
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { KeyboardShortcutInfo } from '$lib/components/app';
|
||||
import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config';
|
||||
import type { Component } from 'svelte';
|
||||
|
||||
interface ActionItem {
|
||||
icon: Component;
|
||||
label: string;
|
||||
onclick: (event: Event) => void;
|
||||
variant?: 'default' | 'destructive';
|
||||
disabled?: boolean;
|
||||
shortcut?: string[];
|
||||
separator?: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
triggerIcon: Component;
|
||||
triggerTooltip?: string;
|
||||
triggerClass?: string;
|
||||
actions: ActionItem[];
|
||||
align?: 'start' | 'center' | 'end';
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
triggerIcon,
|
||||
triggerTooltip,
|
||||
triggerClass = '',
|
||||
actions,
|
||||
align = 'end',
|
||||
open = $bindable(false)
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root bind:open>
|
||||
<DropdownMenu.Trigger
|
||||
class="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md p-0 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground {triggerClass}"
|
||||
>
|
||||
{#if triggerTooltip}
|
||||
<Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
|
||||
<Tooltip.Trigger>
|
||||
{@render iconComponent(triggerIcon, 'h-3 w-3')}
|
||||
<span class="sr-only">{triggerTooltip}</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>{triggerTooltip}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{:else}
|
||||
{@render iconComponent(triggerIcon, 'h-3 w-3')}
|
||||
{/if}
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content {align} class="z-999 w-48">
|
||||
{#each actions as action, index (action.label)}
|
||||
{#if action.separator && index > 0}
|
||||
<DropdownMenu.Separator />
|
||||
{/if}
|
||||
|
||||
<DropdownMenu.Item
|
||||
onclick={action.onclick}
|
||||
variant={action.variant}
|
||||
disabled={action.disabled}
|
||||
class="flex items-center justify-between hover:[&>kbd]:opacity-100"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
{@render iconComponent(
|
||||
action.icon,
|
||||
`h-4 w-4 ${action.variant === 'destructive' ? 'text-destructive' : ''}`
|
||||
)}
|
||||
{action.label}
|
||||
</div>
|
||||
|
||||
{#if action.shortcut}
|
||||
<KeyboardShortcutInfo keys={action.shortcut} variant={action.variant} />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
{#snippet iconComponent(IconComponent: Component, className: string)}
|
||||
<IconComponent class={className} />
|
||||
{/snippet}
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { ArrowBigUp } from '@lucide/svelte';
|
||||
|
||||
interface Props {
|
||||
keys: string[];
|
||||
variant?: 'default' | 'destructive';
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { keys, variant = 'default', class: className = '' }: Props = $props();
|
||||
|
||||
let baseClasses =
|
||||
'px-1 pointer-events-none inline-flex select-none items-center gap-0.5 font-sans text-md font-medium opacity-0 transition-opacity -my-1';
|
||||
let variantClasses = variant === 'destructive' ? 'text-destructive' : 'text-muted-foreground';
|
||||
</script>
|
||||
|
||||
<kbd class="{baseClasses} {variantClasses} {className}">
|
||||
{#each keys as key, index (index)}
|
||||
{#if key === 'shift'}
|
||||
<ArrowBigUp class="h-1 w-1 {variant === 'destructive' ? 'text-destructive' : ''} -mr-1" />
|
||||
{:else if key === 'cmd'}
|
||||
<span class={variant === 'destructive' ? 'text-destructive' : ''}>⌘</span>
|
||||
{:else}
|
||||
{key.toUpperCase()}
|
||||
{/if}
|
||||
|
||||
{#if index < keys.length - 1}
|
||||
<span> </span>
|
||||
{/if}
|
||||
{/each}
|
||||
</kbd>
|
||||
@@ -0,0 +1,625 @@
|
||||
<script lang="ts">
|
||||
import { remark } from 'remark';
|
||||
import remarkBreaks from 'remark-breaks';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import remarkRehype from 'remark-rehype';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import rehypeStringify from 'rehype-stringify';
|
||||
import { copyCodeToClipboard } from '$lib/utils/copy';
|
||||
import 'highlight.js/styles/github-dark.css';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
interface Props {
|
||||
content: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { content, class: className = '' }: Props = $props();
|
||||
|
||||
let containerRef = $state<HTMLDivElement>();
|
||||
let processedHtml = $state('');
|
||||
|
||||
let processor = $derived(() => {
|
||||
return remark()
|
||||
.use(remarkGfm) // GitHub Flavored Markdown
|
||||
.use(remarkMath) // Parse $inline$ and $$block$$ math
|
||||
.use(remarkBreaks) // Convert line breaks to <br>
|
||||
.use(remarkRehype) // Convert to rehype (HTML AST)
|
||||
.use(rehypeKatex) // Render math using KaTeX
|
||||
.use(rehypeHighlight) // Add syntax highlighting
|
||||
.use(rehypeStringify); // Convert to HTML string
|
||||
});
|
||||
|
||||
function enhanceLinks(html: string): string {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = html;
|
||||
|
||||
// Make all links open in new tabs
|
||||
const linkElements = tempDiv.querySelectorAll('a[href]');
|
||||
for (const link of linkElements) {
|
||||
link.setAttribute('target', '_blank');
|
||||
link.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
|
||||
return tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
function enhanceCodeBlocks(html: string): string {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = html;
|
||||
|
||||
const preElements = tempDiv.querySelectorAll('pre');
|
||||
|
||||
for (const [index, pre] of Array.from(preElements).entries()) {
|
||||
const codeElement = pre.querySelector('code');
|
||||
|
||||
if (!codeElement) continue;
|
||||
|
||||
let language = 'text';
|
||||
const classList = Array.from(codeElement.classList);
|
||||
|
||||
for (const className of classList) {
|
||||
if (className.startsWith('language-')) {
|
||||
language = className.replace('language-', '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const rawCode = codeElement.textContent || '';
|
||||
const codeId = `code-${Date.now()}-${index}`;
|
||||
|
||||
codeElement.setAttribute('data-code-id', codeId);
|
||||
codeElement.setAttribute('data-raw-code', rawCode);
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'code-block-wrapper';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'code-block-header';
|
||||
|
||||
const languageLabel = document.createElement('span');
|
||||
languageLabel.className = 'code-language';
|
||||
languageLabel.textContent = language;
|
||||
|
||||
const copyButton = document.createElement('button');
|
||||
copyButton.className = 'copy-code-btn';
|
||||
copyButton.setAttribute('data-code-id', codeId);
|
||||
copyButton.setAttribute('title', 'Copy code');
|
||||
copyButton.setAttribute('type', 'button');
|
||||
|
||||
copyButton.innerHTML = `
|
||||
<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="lucide lucide-copy-icon lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
||||
`;
|
||||
|
||||
header.appendChild(languageLabel);
|
||||
header.appendChild(copyButton);
|
||||
wrapper.appendChild(header);
|
||||
|
||||
const clonedPre = pre.cloneNode(true) as HTMLElement;
|
||||
wrapper.appendChild(clonedPre);
|
||||
|
||||
pre.parentNode?.replaceChild(wrapper, pre);
|
||||
}
|
||||
|
||||
return tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
async function processMarkdown(text: string): Promise<string> {
|
||||
try {
|
||||
const result = await processor().process(text);
|
||||
const html = String(result);
|
||||
const enhancedLinks = enhanceLinks(html);
|
||||
|
||||
return enhanceCodeBlocks(enhancedLinks);
|
||||
} catch (error) {
|
||||
console.error('Markdown processing error:', error);
|
||||
|
||||
// Fallback to plain text with line breaks
|
||||
return text.replace(/\n/g, '<br>');
|
||||
}
|
||||
}
|
||||
|
||||
function setupCopyButtons() {
|
||||
if (!containerRef) return;
|
||||
|
||||
const copyButtons = containerRef.querySelectorAll('.copy-code-btn');
|
||||
|
||||
for (const button of copyButtons) {
|
||||
button.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const target = e.currentTarget as HTMLButtonElement;
|
||||
const codeId = target.getAttribute('data-code-id');
|
||||
|
||||
if (!codeId) {
|
||||
console.error('No code ID found on button');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the code element within the same wrapper
|
||||
const wrapper = target.closest('.code-block-wrapper');
|
||||
if (!wrapper) {
|
||||
console.error('No wrapper found');
|
||||
return;
|
||||
}
|
||||
|
||||
const codeElement = wrapper.querySelector('code[data-code-id]');
|
||||
if (!codeElement) {
|
||||
console.error('No code element found in wrapper');
|
||||
return;
|
||||
}
|
||||
|
||||
const rawCode = codeElement.getAttribute('data-raw-code');
|
||||
if (!rawCode) {
|
||||
console.error('No raw code found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await copyCodeToClipboard(rawCode);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy code:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (content) {
|
||||
processMarkdown(content)
|
||||
.then((result) => {
|
||||
processedHtml = result;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to process markdown:', error);
|
||||
processedHtml = content.replace(/\n/g, '<br>');
|
||||
});
|
||||
} else {
|
||||
processedHtml = '';
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (containerRef && processedHtml) {
|
||||
setupCopyButtons();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={containerRef} class={className}>
|
||||
<!-- eslint-disable-next-line no-at-html-tags -->
|
||||
{@html processedHtml}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Base typography styles */
|
||||
div :global(p) {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
/* Headers with consistent spacing */
|
||||
div :global(h1) {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
margin: 1.5rem 0 0.75rem 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
div :global(h2) {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin: 1.25rem 0 0.5rem 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
div :global(h3) {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 1.5rem 0 0.5rem 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
div :global(h4) {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin: 0.75rem 0 0.25rem 0;
|
||||
}
|
||||
|
||||
div :global(h5) {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0.5rem 0 0.25rem 0;
|
||||
}
|
||||
|
||||
div :global(h6) {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin: 0.5rem 0 0.25rem 0;
|
||||
}
|
||||
|
||||
/* Text formatting */
|
||||
div :global(strong) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
div :global(em) {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
div :global(del) {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
div :global(code:not(pre code)) {
|
||||
background: var(--muted);
|
||||
color: var(--muted-foreground);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas,
|
||||
'Liberation Mono', Menlo, monospace;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
div :global(a) {
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
div :global(a:hover) {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
div :global(ul) {
|
||||
list-style-type: disc;
|
||||
margin-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
div :global(ol) {
|
||||
list-style-type: decimal;
|
||||
margin-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
div :global(li) {
|
||||
margin-bottom: 0.25rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
div :global(li::marker) {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* Nested lists */
|
||||
div :global(ul ul) {
|
||||
list-style-type: circle;
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
div :global(ol ol) {
|
||||
list-style-type: lower-alpha;
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Task lists */
|
||||
div :global(.task-list-item) {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
div :global(.task-list-item-checkbox) {
|
||||
margin-right: 0.5rem;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
/* Blockquotes */
|
||||
div :global(blockquote) {
|
||||
border-left: 4px solid var(--border);
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 1.5rem 0;
|
||||
font-style: italic;
|
||||
color: var(--muted-foreground);
|
||||
background: var(--muted);
|
||||
border-radius: 0 0.375rem 0.375rem 0;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
div :global(table) {
|
||||
width: 100%;
|
||||
margin: 1.5rem 0;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div :global(th) {
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
div :global(td) {
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
div :global(tr:nth-child(even)) {
|
||||
background: hsl(var(--muted) / 0.1);
|
||||
}
|
||||
|
||||
/* Horizontal rules */
|
||||
div :global(hr) {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
/* Images */
|
||||
div :global(img) {
|
||||
border-radius: 0.5rem;
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgb(0 0 0 / 0.1),
|
||||
0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
margin: 1.5rem 0;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
|
||||
div :global(.code-block-wrapper) {
|
||||
margin: 1.5rem 0;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--code-background);
|
||||
}
|
||||
|
||||
div :global(.code-block-header) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
div :global(.code-language) {
|
||||
color: var(--code-foreground);
|
||||
font-weight: 500;
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas,
|
||||
'Liberation Mono', Menlo, monospace;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
div :global(.copy-code-btn) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--code-foreground);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
div :global(.copy-code-btn:hover) {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
div :global(.copy-code-btn:active) {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
div :global(.code-block-wrapper pre) {
|
||||
background: transparent;
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
div :global(pre) {
|
||||
background: var(--muted);
|
||||
margin: 1.5rem 0;
|
||||
overflow-x: auto;
|
||||
border-radius: 1rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
div :global(code) {
|
||||
background: transparent;
|
||||
color: var(--code-foreground);
|
||||
}
|
||||
|
||||
/* Mentions and hashtags */
|
||||
div :global(.mention) {
|
||||
color: hsl(var(--primary));
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div :global(.mention:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div :global(.hashtag) {
|
||||
color: hsl(var(--primary));
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div :global(.hashtag:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Advanced table enhancements */
|
||||
div :global(table) {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
div :global(table:hover) {
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgb(0 0 0 / 0.1),
|
||||
0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
div :global(th:hover),
|
||||
div :global(td:hover) {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
/* Enhanced blockquotes */
|
||||
div :global(blockquote) {
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div :global(blockquote:hover) {
|
||||
border-left-width: 6px;
|
||||
background: var(--muted);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
div :global(blockquote::before) {
|
||||
content: '"';
|
||||
position: absolute;
|
||||
top: -0.5rem;
|
||||
left: 0.5rem;
|
||||
font-size: 3rem;
|
||||
color: var(--muted-foreground);
|
||||
font-family: serif;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Enhanced images */
|
||||
div :global(img) {
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div :global(img:hover) {
|
||||
transform: scale(1.02);
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgb(0 0 0 / 0.1),
|
||||
0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
/* Image zoom overlay */
|
||||
div :global(.image-zoom-overlay) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div :global(.image-zoom-overlay img) {
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||
}
|
||||
|
||||
/* Enhanced horizontal rules */
|
||||
div :global(hr) {
|
||||
border: none;
|
||||
height: 2px;
|
||||
background: linear-gradient(to right, transparent, var(--border), transparent);
|
||||
margin: 2rem 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div :global(hr::after) {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background: var(--border);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Scrollable tables */
|
||||
div :global(.table-wrapper) {
|
||||
overflow-x: auto;
|
||||
margin: 1.5rem 0;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
div :global(.table-wrapper table) {
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 640px) {
|
||||
div :global(h1) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
div :global(h2) {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
div :global(h3) {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
div :global(table) {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
div :global(th),
|
||||
div :global(td) {
|
||||
padding: 0.375rem 0.5rem;
|
||||
}
|
||||
|
||||
div :global(.table-wrapper) {
|
||||
margin: 0.5rem -1rem;
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode adjustments */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
div :global(blockquote:hover) {
|
||||
background: var(--muted);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user