Files
web-app-launcher/src/lib/components/widget/MarkdownWidget.svelte
T
alexei.dolgolyov 1c0a7cb850 feat: Phases 4-7 — Full Feature Expansion (26 features)
Phase 4 — New Widget Types:
- Clock/Weather, System Stats, RSS/Feed, Calendar, Markdown,
  Metric/Counter, Link Group, Camera/Stream widgets
- Backend services with caching for each data source
- Full creation form with dynamic config fields per type

Phase 5 — Visual & Styling Enhancements:
- Glassmorphism card style (solid/glass/outline)
- Board-level themes with per-board hue/saturation
- Animated SVG status rings replacing static dots
- Card size options (compact/medium/large)
- Custom CSS injection (admin + per-board, sanitized)
- Wallpaper backgrounds with blur/overlay/parallax

Phase 6 — Functional Features:
- Favorites bar with drag-and-drop reordering
- Recent apps tracking with privacy toggle
- Uptime dashboard page (/status, guest-accessible)
- Notifications system (Discord/Slack/Telegram/HTTP webhooks)
- App tags with filtering in board view
- Multi-URL app cards with expandable sub-links
- Personal API tokens with scoped permissions
- Audit log with retention and admin viewer

Phase 7 — Quality of Life:
- Onboarding wizard (5-step first-launch setup)
- App URL health preview with favicon/title detection
- Board templates (4 built-in + custom import/export)
- Keyboard shortcut overlay (j/k nav, 1-9 boards, ? help)

212 files changed, 15641 insertions, 980 deletions.
Build, lint, type check, and 222 tests all pass.
2026-03-25 14:18:10 +03:00

102 lines
2.8 KiB
Svelte

<script lang="ts">
import { marked } from 'marked';
import DOMPurify from 'isomorphic-dompurify';
import { Pencil, Eye } from 'lucide-svelte';
import type { MarkdownWidgetConfig } from '$lib/types/widget.js';
interface Props {
config: MarkdownWidgetConfig;
widgetId?: string;
}
let { config, widgetId }: Props = $props();
let editMode = $state(false);
let editContent = $state(config.content ?? '');
let saving = $state(false);
marked.setOptions({
breaks: true,
gfm: true
});
const renderedHtml = $derived.by(() => {
const source = editMode ? editContent : (config.content ?? '');
const raw = marked.parse(source, { async: false }) as string;
return DOMPurify.sanitize(raw);
});
async function saveContent() {
if (!widgetId) return;
saving = true;
try {
const newConfig = { ...config, content: editContent };
await fetch(`/api/widgets/${widgetId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config: JSON.stringify(newConfig) })
});
} catch {
// Silently fail — user can retry
} finally {
saving = false;
}
}
function toggleEdit() {
if (editMode) {
saveContent();
} else {
editContent = config.content ?? '';
}
editMode = !editMode;
}
</script>
<div class="flex h-full flex-col rounded-xl border border-border bg-card">
<!-- Toolbar -->
<div class="flex items-center justify-end border-b border-border px-3 py-1.5">
<button
type="button"
onclick={toggleEdit}
class="flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
disabled={saving}
>
{#if editMode}
<Eye class="h-3.5 w-3.5" />
<span>{saving ? 'Saving...' : 'Preview'}</span>
{:else}
<Pencil class="h-3.5 w-3.5" />
<span>Edit</span>
{/if}
</button>
</div>
{#if editMode}
<!-- Split pane: editor + preview -->
<div class="flex flex-1 divide-x divide-border overflow-hidden">
<div class="flex-1 overflow-hidden">
<textarea
bind:value={editContent}
class="h-full w-full resize-none border-0 bg-background p-3 font-mono text-sm text-foreground placeholder:text-muted-foreground focus:outline-none"
placeholder="Write markdown here..."
></textarea>
</div>
<div class="flex-1 overflow-auto p-3">
<div class="prose prose-sm prose-invert max-w-none text-foreground">
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized with DOMPurify -->
{@html renderedHtml}
</div>
</div>
</div>
{:else}
<!-- View mode -->
<div class="flex-1 overflow-auto p-4">
<div class="prose prose-sm prose-invert max-w-none text-foreground">
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized with DOMPurify -->
{@html renderedHtml}
</div>
</div>
{/if}
</div>