Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399<script lang="ts"> import type { Snippet } from 'svelte'; import { WINDOW_ZOOM } from '../board.svelte'; import { boards } from '../boards.svelte'; import { option } from '../keys'; import { dismissal } from '../popover'; import { layout } from '../layout.svelte'; import { popOut, popup } from '../popouts.svelte'; import { prefs } from '../prefs.svelte'; import ExpandIcon from '../tiles/ExpandIcon.svelte';
// The frame every window shares: a title bar to drag it by, a menu for // its zoom and pop out (and what `menu` adds for its view), focus and // close buttons, and a body that scrolls and selects text instead of // moving the canvas. On a canvas, the slot around it (SlotNode) has the // edges and corners that resize it. Later views (file, diff, terminal) supply only // the parts. let { id, selected, title, place, meta, menu, autoHeight = false, children, }: { id: string; selected: boolean; title: string; place?: string; meta?: Snippet; /** The view's own entries in the window menu, under its zoom, each a `.item` with `role="menuitem"`. */ menu?: Snippet; autoHeight?: boolean; children: Snippet; } = $props();
const zoom = $derived(boards.board?.windowZoom(id) ?? 1); const percent = $derived(Math.round(zoom * 100)); const expandable = $derived(!!boards.board?.expandable(id)); const expanded = $derived(boards.board?.expanded === id);
/** A double-click on the bar itself, not on a control in it, fills the board with the window or puts it back. */ function dblclick(event: MouseEvent) { if (expandable && !(event.target as Element).closest('button, input, .nodrag')) boards.board?.toggleExpanded(id); }
// A bar too narrow for its chips moves them into the menu, keeping room // for the title. Their width is measured while they stand in the bar. const KEEP = 150; let width = $state(0); let natural = $state(0); let shown = $state(0); const folded = $derived(natural > 0 && width > 0 && width < natural + KEEP); $effect(() => { if (!folded) natural = shown; });
let open = $state(false); let button = $state<HTMLElement>(); let pane = $state<HTMLElement>(); $effect(() => (open ? dismissal((target) => !!(button?.contains(target) || pane?.contains(target)), () => (open = false)) : undefined));</script>
<article class="window" class:selected class:auto-height={autoHeight} class:slim={prefs.compactBars}> <!-- svelte-ignore a11y_no_static_element_interactions (the focus button in it does the same from the keyboard) --> <header class="window-handle" bind:clientWidth={width} ondblclick={dblclick}> <h2 title={place ? `${title} in ${place}` : title}>{title}{#if place}<span class="in">in {place}</span>{/if}</h2> {#if meta && !folded}<span class="meta" bind:clientWidth={shown}>{@render meta()}</span>{/if} {#if percent !== 100} <button class="level nodrag" aria-label={`Window at ${percent}%, back to 100%`} title="Back to 100% (⌥0)" onclick={() => boards.board?.zoomWindow(id, 1)}>{percent}%</button> {/if} <button class="bare nodrag" bind:this={button} aria-haspopup="menu" aria-expanded={open} aria-label="Window menu" title="Window menu" onclick={() => (open = !open)}> <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><circle cx="3.5" cy="8" r="1.2" fill="currentColor" /><circle cx="8" cy="8" r="1.2" fill="currentColor" /><circle cx="12.5" cy="8" r="1.2" fill="currentColor" /></svg> </button> {#if open} <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions (the entries are buttons; this only closes the menu after one) --> <div class="pane nodrag" role="menu" tabindex="-1" bind:this={pane} onclick={(event) => (event.target as Element).closest('[role="menuitem"]') && (open = false)}> {#if meta && folded}<div class="meta folded">{@render meta()}</div>{/if} <div class="zoom" role="group" aria-label="Window zoom"> <span class="label"> <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><circle cx="7" cy="7" r="4.25" fill="none" stroke="currentColor" stroke-width="1.4" /><path d="M10.25 10.25 13.5 13.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" /></svg>zoom </span> <span class="stepper"> <button class="step" aria-label="Zoom window out" title="Zoom window out (⌥−)" disabled={zoom <= WINDOW_ZOOM.min} onclick={() => boards.board?.stepWindowZoom(id, -1)}> <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M4 8h8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" /></svg> </button> <button class="percent" aria-label={`Window at ${percent}%, back to 100%`} title="Back to 100% (⌥0)" disabled={percent === 100} onclick={() => boards.board?.zoomWindow(id, 1)}>{percent}%</button> <button class="step" aria-label="Zoom window in" title="Zoom window in (⌥+)" disabled={zoom >= WINDOW_ZOOM.max} onclick={() => boards.board?.stepWindowZoom(id, 1)}> <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M4 8h8M8 4v8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" /></svg> </button> </span> </div> {#if menu}{@render menu()}{/if} {#if !popup && !layout.phone} <button class="item" role="menuitem" onclick={() => popOut(id)}> <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M9.5 2.5h4v4M13.5 2.5 8 8M11.5 9.5v3a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1h3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" /></svg>pop out into a window </button> {/if} </div> {/if} {#if expandable} <button class="bare nodrag" aria-label={expanded ? 'Unfocus window' : 'Focus window'} aria-pressed={expanded} title={expanded ? 'Back to the board (esc)' : `Focus: fill the board with it (${option('F')})`} onclick={() => boards.board?.toggleExpanded(id)}> <ExpandIcon {expanded} /> </button> {/if} <!-- A popped-out window's page closes to put the window back on its board. --> <button class="close nodrag" aria-label={popup ? 'Back to the board' : 'Close window'} title={popup ? 'Back to the board' : 'Close window (W)'} onclick={() => (popup ? close() : boards.board?.remove(id))}> <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" /></svg> </button> </header> <div class="body nodrag" style:zoom={zoom !== 1 ? zoom : undefined} style:--window-zoom={zoom}> {@render children()} </div></article>
<style> .window { display: flex; flex-direction: column; /* What a window's em sizes (the chat's text) are relative to. */ font-size: calc(13px * var(--canvas-scale)); width: 100%; height: 100%; background: var(--surface); border-radius: 14px; box-shadow: var(--shadow); transition: box-shadow 200ms cubic-bezier(0.23, 1, 0.32, 1); } .window.selected { box-shadow: var(--shadow-raised); } /* Alone on a canvas a chip window is as tall as its list; in a tile or a group it fills its place like any other. */ :global(.window-slot[data-place='canvas']) .window.auto-height { height: auto; } :global(.window-slot[data-place='canvas']) .window.auto-height .body { overflow: visible; } header { position: relative; z-index: 10; display: flex; flex: none; align-items: center; gap: 10px; height: 42px; padding: 0 8px 0 16px; background: var(--title-bar); border-radius: 14px 14px 0 0; cursor: grab; user-select: none; } header:active { cursor: grabbing; } .selected h2 { color: var(--text); } h2 { flex: 1 1 80px; min-width: 48px; margin: 0; overflow: hidden; color: var(--muted); transition: color 160ms ease; font-size: calc(13px * var(--canvas-scale)); font-weight: 600; letter-spacing: -0.01em; white-space: nowrap; text-overflow: ellipsis; } .in { margin-left: 0.35em; color: var(--faint); font-weight: 400; } /* A compact bar: the title, the menu and close, the chips under the composer. */ .slim header { gap: 6px; height: 24px; padding: 0 4px 0 12px; } .slim h2 { font-size: calc(12px * var(--canvas-scale)); } .slim .level { height: 20px; } .slim .close { width: 20px; height: 20px; border-radius: 6px; } .meta { display: flex; flex: none; align-items: center; gap: 6px; color: var(--muted); font-size: calc(12px * var(--canvas-scale)); } .level { flex: none; height: 24px; padding: 0 7px; color: var(--accent); font: inherit; font-size: calc(11px * var(--ui-scale)); font-weight: 600; font-variant-numeric: tabular-nums; background: color-mix(in oklab, var(--accent) 14%, var(--well)); border: 0; border-radius: 7px; cursor: pointer; transition: background-color 120ms ease; } .level:hover { background: color-mix(in oklab, var(--accent) 22%, var(--well)); } :global([data-theme='dark']) .level { background: color-mix(in oklab, var(--accent) 20%, var(--well)); } :global([data-theme='dark']) .level:hover { background: color-mix(in oklab, var(--accent) 28%, var(--well)); } .bare { display: grid; flex: none; place-items: center; width: 24px; height: 24px; margin-right: -6px; padding: 0; color: var(--muted); background: none; border: 0; border-radius: 7px; cursor: pointer; transition: background-color 120ms ease, color 120ms ease; } .bare:hover, .bare[aria-expanded='true'] { color: var(--text); background: var(--hover); } .bare[aria-pressed='true'] { color: var(--text); } .bare:active { transform: scale(0.94); } .slim .bare { width: 20px; height: 20px; margin-right: -4px; border-radius: 6px; } /* Hangs from the bar, not its button, so it stays inside the window however narrow it is. */ .pane { position: absolute; top: calc(100% + 4px); right: 8px; z-index: 30; display: grid; gap: 1px; width: max-content; min-width: min(200px, 100% - 16px); max-width: calc(100% - 16px); padding: 5px; font-size: calc(12.5px * var(--ui-scale)); background: var(--surface); border-radius: 12px; box-shadow: var(--shadow-raised); cursor: auto; } .meta.folded { flex-wrap: wrap; justify-content: flex-end; padding: 3px 3px 6px; } .zoom { display: flex; align-items: center; gap: 8px; margin-bottom: 2px; padding: 4px 4px 4px 9px; color: var(--text); } /* The steps and the level in one well, a control of its own beside the entries. */ .stepper { display: flex; align-items: center; background: var(--well); border-radius: 8px; } .zoom .label { display: flex; flex: 1; align-items: center; gap: 9px; } .zoom .label svg { color: var(--muted); } .step, .percent { display: grid; place-items: center; height: 26px; padding: 0; color: var(--muted); font: inherit; font-size: calc(11.5px * var(--ui-scale)); font-variant-numeric: tabular-nums; background: none; border: 0; border-radius: 6px; cursor: pointer; transition: background-color 120ms ease, color 120ms ease; } .step { width: 26px; } .percent { min-width: 42px; } .step:hover:not(:disabled), .percent:hover:not(:disabled) { color: var(--text); background: var(--hover); } .step:disabled { opacity: 0.35; cursor: default; } .percent:disabled { cursor: default; } .pane :global(.item) { display: flex; align-items: center; gap: 9px; width: 100%; padding: 6px 9px; color: var(--text); font: inherit; text-align: left; text-decoration: none; background: none; border: 0; border-radius: 7px; cursor: pointer; } .pane :global(.item:hover) { background: var(--hover); } .pane :global(.item svg) { flex: none; color: var(--muted); } .pane :global(.item kbd) { margin-left: auto; } .close { display: grid; flex: none; place-items: center; width: 28px; height: 28px; padding: 0; color: var(--muted); background: none; border: 0; border-radius: 8px; cursor: pointer; transition: background-color 120ms ease, color 120ms ease; } .close:hover { color: var(--text); background: var(--hover); } .close:active { transform: scale(0.94); } .body { display: flex; flex: 1; flex-direction: column; min-height: 0; /* Clipped, not scrollable: focusing something sticking out (a popover's field) cannot slide the window's content. */ overflow: clip; border-radius: 0 0 14px 14px; cursor: auto; user-select: text; }</style>