Workspace
A dockable workspace: tabbed panels split any way, dragged anywhere, floated into windows, maximized, and remembered — and nothing inside them ever remounts.
Walkthrough
1. Basic
Panels in tabbed groups, split any way. Drag a tab onto another group's edge to split it, onto its strip to join it, or to the workspace's own edge to dock along it. Drag a divider to resize. storageKey brings the arrangement back after a reload.
+page.svelte
+layout.svelte
api.ts
app.css
README.md
<h1>Hello</h1> <p>Drag my tab somewhere.</p>
$ bun dev ready in 212 ms
Hello
Drag my tab somewhere.
<script lang="ts"> import { Workspace, WorkspacePanel, row, column, tabs, Text } from 'omaris'; import Icon from '$lib/patterns/shell/icon.svelte'; const FILES = ['+page.svelte', '+layout.svelte', 'api.ts', 'app.css', 'README.md']; const PAGE = '<h1>Hello</h1>\n<p>Drag my tab somewhere.</p>'; const API = 'export const load = () => ({ ok: true });'; const LOG = '$ bun dev\n ready in 212 ms';</script>{#snippet folder()}<Icon name="folder" />{/snippet}{#snippet code()}<Icon name="code" />{/snippet}{#snippet terminal()}<Icon name="terminal" />{/snippet}{#snippet eye()}<Icon name="eye" />{/snippet}<div class="h-[30rem] w-full"> <Workspace storageKey="docs-workspace-basic" defaultLayout={row( 'files', column(tabs('page', 'api'), 'terminal', { sizes: [65, 35] }), 'preview', { sizes: [22, 48, 30] } )} > <WorkspacePanel id="files" title="Files" icon={folder}> <ul class="flex flex-col p-2"> {#each FILES as file (file)} <li class="flex h-9 items-center rounded-shape-sm px-3"> <Text variant="body-md">{file}</Text> </li> {/each} </ul> </WorkspacePanel> <WorkspacePanel id="page" title="+page.svelte" icon={code}> <pre class="p-4 font-mono text-body-sm leading-6">{PAGE}</pre> </WorkspacePanel> <WorkspacePanel id="api" title="api.ts" icon={code}> <pre class="p-4 font-mono text-body-sm leading-6">{API}</pre> </WorkspacePanel> <WorkspacePanel id="terminal" title="Terminal" icon={terminal}> <pre class="p-3 font-mono text-body-sm leading-6 text-muted-foreground">{LOG}</pre> </WorkspacePanel> <WorkspacePanel id="preview" title="Preview" icon={eye}> <div class="grid h-full place-items-center p-6 text-center"> <div class="flex flex-col gap-1"> <Text variant="headline-sm">Hello</Text> <Text variant="body-md" tone="muted">Drag my tab somewhere.</Text> </div> </div> </WorkspacePanel> </Workspace></div> 2. Dashboard
A dashboard people arrange for themselves. bind:open on each panel drives the checkboxes in the menu; a panel switched off remembers where it was and comes back there. bind:this gives the reset.
Order #1043 paid
New customer: Lina
Refund issued for #1031
Order #1042 shipped
Stock low: Linen shirt
<script lang="ts"> import { Workspace, WorkspacePanel, StatCard, Button, Menu, MenuItem, Text, row, column } from 'omaris'; let workspace = $state<Workspace>(); const panels = $state([ { id: 'revenue', title: 'Revenue', open: true }, { id: 'orders', title: 'Orders', open: true }, { id: 'visitors', title: 'Visitors', open: true }, { id: 'activity', title: 'Activity', open: true } ]); /** The panel is the card already; the stat inside it needs no second one. */ const bare = 'h-full rounded-none border-0 bg-transparent shadow-none'; const ACTIVITY = [ 'Order #1043 paid', 'New customer: Lina', 'Refund issued for #1031', 'Order #1042 shipped', 'Stock low: Linen shirt' ];</script><div class="flex w-full flex-col gap-3 p-3"> <div class="flex flex-wrap items-center gap-2"> <Menu closeOnSelect={false} label="Panels"> {#snippet trigger(props)} <Button variant="tonal" size="sm" {...props}>Panels</Button> {/snippet} {#each panels as panel (panel.id)} <MenuItem selected={panel.open} onclick={() => (panel.open = !panel.open)}> {panel.title} </MenuItem> {/each} </Menu> <Button variant="text" size="sm" onclick={() => workspace?.reset()}>Reset layout</Button> </div> <div class="h-[28rem]"> <Workspace bind:this={workspace} defaultLayout={column(row('revenue', 'orders', 'visitors'), 'activity', { sizes: [42, 58] })} > <WorkspacePanel id="revenue" title="Revenue" bind:open={panels[0].open}> <StatCard class={bare} label="This week" value={48210} format={{ style: 'currency', currency: 'USD', maximumFractionDigits: 0 }} delta={0.124} /> </WorkspacePanel> <WorkspacePanel id="orders" title="Orders" bind:open={panels[1].open}> <StatCard class={bare} label="This week" value={1284} delta={0.061} /> </WorkspacePanel> <WorkspacePanel id="visitors" title="Visitors" bind:open={panels[2].open}> <StatCard class={bare} label="This week" value={9312} delta={-0.032} /> </WorkspacePanel> <WorkspacePanel id="activity" title="Activity" bind:open={panels[3].open} badge={ACTIVITY.length} > <ul class="flex flex-col divide-y divide-border"> {#each ACTIVITY as line (line)} <li class="px-4 py-3"><Text variant="body-md">{line}</Text></li> {/each} </ul> </WorkspacePanel> </Workspace> </div></div> 3. Floating and maximize
A panel can leave the tree for a floating window: drag it by its strip, size it from its edges, double-click the strip to dock it again. Hold Shift while dropping a tab to float it; double-click a docked strip — or its maximize button — to fill the workspace, and Escape to come back.
Background
Shape
<script lang="ts"> import { Workspace, WorkspacePanel, Button, Text, row, tabs } from 'omaris'; let workspace = $state<Workspace>();</script><div class="flex w-full flex-col gap-3 p-3"> <div class="flex flex-wrap gap-2"> <Button variant="tonal" size="sm" onclick={() => workspace?.float('layers')} >Float layers</Button > <Button variant="tonal" size="sm" onclick={() => workspace?.maximize('canvas')}> Maximize canvas </Button> </div> <div class="h-[28rem]"> <Workspace bind:this={workspace} defaultLayout={{ root: row('canvas', tabs('layers', 'history'), { sizes: [70, 30] }), floating: [{ group: tabs('color'), x: 40, y: 72, width: 240, height: 200 }] }} > <WorkspacePanel id="canvas" title="Canvas"> <div class="grid h-full place-items-center bg-surface-container-lowest"> <div class="size-32 rounded-shape-xl bg-primary-container"></div> </div> </WorkspacePanel> <WorkspacePanel id="layers" title="Layers" class="p-3"> <Text variant="body-md">Background</Text> <Text variant="body-md">Shape</Text> </WorkspacePanel> <WorkspacePanel id="history" title="History" class="p-3"> <Text variant="body-md" tone="muted">Nothing to undo.</Text> </WorkspacePanel> <WorkspacePanel id="color" title="Color" class="p-3"> <div class="grid grid-cols-4 gap-2"> {#each ['bg-primary', 'bg-secondary', 'bg-tertiary', 'bg-destructive'] as swatch (swatch)} <div class="aspect-square rounded-shape-sm {swatch}"></div> {/each} </div> </WorkspacePanel> </Workspace> </div></div> 4. Documents
Panels from data: every document is a WorkspacePanel in an {#each}, opened next to the others the moment it exists. An edited one shows the unsaved dot on its tab, and onclose returning false keeps it open until it is saved.
<script lang="ts"> import { Workspace, WorkspacePanel, Button, Textarea, row, tabs } from 'omaris'; type Doc = { id: string; name: string; text: string; saved: string }; let docs = $state<Doc[]>([ { id: 'readme', name: 'README.md', text: '# Hello', saved: '# Hello' }, { id: 'todo', name: 'TODO.md', text: '- Ship it', saved: '- Ship it' } ]); let next = 1; function create() { const n = next++; docs.push({ id: `untitled-${n}`, name: `Untitled ${n}.md`, text: '', saved: '' }); }</script><div class="flex w-full flex-col gap-3 p-3"> <div><Button variant="tonal" size="sm" onclick={create}>New document</Button></div> <div class="h-[26rem]"> <Workspace defaultLayout={row(tabs('readme', 'todo'))}> {#each docs as doc (doc.id)} <WorkspacePanel id={doc.id} title={doc.name} open placement={{ with: 'readme' }} modified={doc.text !== doc.saved} onclose={() => doc.text === doc.saved} class="flex flex-col gap-2 p-3" > <Textarea label={doc.name} bind:value={doc.text} rows={8} /> <div class="mt-auto flex justify-end"> <Button size="sm" disabled={doc.text === doc.saved} onclick={() => (doc.saved = doc.text)} > Save </Button> </div> </WorkspacePanel> {/each} </Workspace> </div></div> 5. Flush
variant="flush": edge to edge with hairlines between, the density of an IDE. Everything else — the dragging, the docking, the keyboard — is the same.
load()
actions
export async function load() {
return { items: await db.items() };
}No problems.
<script lang="ts"> import { Workspace, WorkspacePanel, Text, row, column } from 'omaris';</script><div class="h-96 w-full"> <Workspace variant="flush" defaultLayout={row('outline', column('source', 'problems', { sizes: [70, 30] }), { sizes: [25, 75] })} > <WorkspacePanel id="outline" title="Outline" class="p-3"> <Text variant="body-md">load()</Text> <Text variant="body-md">actions</Text> </WorkspacePanel> <WorkspacePanel id="source" title="+page.server.ts"> <pre class="p-4 font-mono text-body-sm leading-6">{'export async function load() {\n return { items: await db.items() };\n}'}</pre> </WorkspacePanel> <WorkspacePanel id="problems" title="Problems" badge={0} class="p-3"> <Text variant="body-md" tone="muted">No problems.</Text> </WorkspacePanel> </Workspace></div> 6. Overridden
Every part is reachable. A wider gap and a sharper radius through the custom properties, the strip and the front tab through classes, a fixed height from class, and padding on a panel's own content box. locked keeps the arrangement as it is: tabs switch, nothing moves.
A locked layout, restyled.
Left
Right
<script lang="ts"> import { Workspace, WorkspacePanel, Text, row, column } from 'omaris';</script><div class="w-full p-3"> <Workspace locked class="h-80 rounded-shape-xl [--workspace-gap:--spacing(3)] [--workspace-radius:var(--radius-shape-sm)]" classes={{ header: 'bg-primary-container text-primary-container-foreground', tab: 'data-active:bg-primary data-active:text-primary-foreground' }} defaultLayout={column('summary', row('left', 'right'), { sizes: [40, 60] })} > <WorkspacePanel id="summary" title="Summary" class="bg-surface-container-low p-4"> <Text variant="body-md">A locked layout, restyled.</Text> </WorkspacePanel> <WorkspacePanel id="left" title="Left" class="p-4"><Text>Left</Text></WorkspacePanel> <WorkspacePanel id="right" title="Right" class="p-4"><Text>Right</Text></WorkspacePanel> </Workspace></div>