feat(mvp): phase 5 - board, section & widget system

Add board/section/widget CRUD APIs with permission filtering, board view
page with collapsible sections and app widgets in responsive grid, form-based
board editor, and 9 Svelte components (Board, Section, Widget families).
This commit is contained in:
2026-03-24 21:05:00 +03:00
parent 4d941f566f
commit b0d77d3c29
23 changed files with 1564 additions and 27 deletions
+54
View File
@@ -0,0 +1,54 @@
<script lang="ts">
import SectionHeader from './SectionHeader.svelte';
import SectionCollapsible from './SectionCollapsible.svelte';
import WidgetGrid from '$lib/components/widget/WidgetGrid.svelte';
interface WidgetData {
id: string;
type: string;
order: number;
config: string;
appId: string | null;
app: {
id: string;
name: string;
url: string;
icon: string | null;
iconType: string;
description: string | null;
statuses: Array<{ status: string; responseTime: number | null }>;
} | null;
}
interface SectionData {
id: string;
title: string;
icon: string | null;
order: number;
isExpandedByDefault: boolean;
widgets: WidgetData[];
}
interface Props {
section: SectionData;
}
let { section }: Props = $props();
let expanded = $state(section.isExpandedByDefault);
</script>
<div class="rounded-lg border border-gray-700 bg-gray-800/30">
<SectionHeader
title={section.title}
icon={section.icon}
{expanded}
onToggle={() => (expanded = !expanded)}
/>
<SectionCollapsible {expanded}>
<div class="px-4 pb-4">
<WidgetGrid widgets={section.widgets} />
</div>
</SectionCollapsible>
</div>
@@ -0,0 +1,17 @@
<script lang="ts">
import { slide } from 'svelte/transition';
import type { Snippet } from 'svelte';
interface Props {
expanded: boolean;
children: Snippet;
}
let { expanded, children }: Props = $props();
</script>
{#if expanded}
<div transition:slide={{ duration: 200 }}>
{@render children()}
</div>
{/if}
@@ -0,0 +1,36 @@
<script lang="ts">
interface Props {
title: string;
icon: string | null;
expanded: boolean;
onToggle: () => void;
}
let { title, icon, expanded, onToggle }: Props = $props();
</script>
<button
type="button"
onclick={onToggle}
class="flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-gray-700/30"
>
<svg
class="h-4 w-4 shrink-0 text-gray-400 transition-transform duration-200"
class:rotate-90={expanded}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
{#if icon}
<span class="text-base">{icon}</span>
{/if}
<span class="font-medium text-white">{title}</span>
</button>