SvelteKit (web/): - Komplett app-skjelett med Authentik SSO (dev-bypass lokalt) - Workspace-modell med cookie-basert switching og RLS-kontekst - Komponerbare sider (PageGrid + BlockShell + block registry) - Chat med adapter-mønster: PG-polling og SpacetimeDB hybrid-adapter - Brukeridentitet fra Authentik/dev-login flyter til chat-meldinger - API-ruter for channels, messages og health SpacetimeDB (spacetimedb/): - Rust WASM-modul med ChatMessage og SyncOutbox-tabeller - send_message reducer med sync outbox for fremtidig PG-persistering - Genererte TypeScript-bindings for klient-integrasjon Infra: - SpacetimeDB lagt til i docker-compose.dev.yml Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
61 lines
1.3 KiB
Svelte
61 lines
1.3 KiB
Svelte
<script lang="ts">
|
|
import type { Component } from 'svelte';
|
|
import type { PageConfig } from '$lib/types/pages';
|
|
import { getGridColumns } from '$lib/types/pages';
|
|
import { blockRegistry } from '$lib/blocks/registry';
|
|
import BlockShell from './BlockShell.svelte';
|
|
|
|
let { page }: { page: PageConfig } = $props();
|
|
|
|
let resolved = $state<Record<string, Component>>({});
|
|
|
|
$effect(() => {
|
|
for (const block of page.blocks) {
|
|
const meta = blockRegistry[block.type];
|
|
if (meta && !resolved[block.id]) {
|
|
meta.component().then((m) => {
|
|
resolved[block.id] = m.default;
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
let gridColumns = $derived(getGridColumns(page.layout));
|
|
</script>
|
|
|
|
<div class="page-grid" style:grid-template-columns={gridColumns}>
|
|
{#each page.blocks as block (block.id)}
|
|
<BlockShell title={block.title}>
|
|
{@const BlockComponent = resolved[block.id]}
|
|
{#if BlockComponent}
|
|
<BlockComponent props={block.props ?? {}} />
|
|
{:else}
|
|
<div class="loading">Laster...</div>
|
|
{/if}
|
|
</BlockShell>
|
|
{/each}
|
|
</div>
|
|
|
|
<style>
|
|
.page-grid {
|
|
display: grid;
|
|
gap: 1rem;
|
|
height: calc(100vh - 48px - 3rem);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.page-grid {
|
|
grid-template-columns: 1fr !important;
|
|
height: auto;
|
|
}
|
|
}
|
|
|
|
.loading {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
color: #8b92a5;
|
|
font-size: 0.85rem;
|
|
}
|
|
</style>
|