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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190<script lang="ts"> import type { Board } from '../board.svelte'; import TileView from './TileView.svelte'; import { GUTTER, leastAlong, type Split } from './tree';
// Tiles side by side (a row) or stacked (a column). The gap between two is // the handle that resizes them, drawn as zen's desk gap is: nothing until // pointed at, then a hairline of the accent. While dragged the sizes are // only this view's; the board gets them when it ends. let { board, split }: { board: Board; split: Split } = $props();
const STEP = 0.04; let element = $state<HTMLElement>(); let live = $state<number[] | null>(null); const sizes = $derived(live ?? split.sizes); const row = $derived(split.axis === 'row');
/** The split's length less its gutters, as measured now. */ function measure(): number { const box = element?.getBoundingClientRect(); return box ? (row ? box.width : box.height) - GUTTER * (split.children.length - 1) : 0; }
/** The share child `at` may shrink to in `length`, which keeps every tile in it at LEAST or more. */ function least(at: number, length: number): number { return length > 0 ? leastAlong(split.children[at], split.axis) / length : 0.1; }
/** * Moves the boundary after child `at` so that child ends at `first` of the * pair's share. A child already smaller than its least may grow, never shrink. */ function shift(from: number[], at: number, first: number, length = measure()): number[] { const pair = from[at] + from[at + 1]; const low = Math.min(least(at, length), from[at]); const high = Math.max(pair - least(at + 1, length), from[at]); const next = [...from]; next[at] = Math.min(high, Math.max(low, first * pair)); next[at + 1] = pair - next[at]; return next; }
function grab(event: PointerEvent, at: number) { if (event.button !== 0 || !element) return; event.preventDefault(); const gutter = event.currentTarget as HTMLElement; gutter.setPointerCapture(event.pointerId); const from = [...split.sizes]; const before = gutter.previousElementSibling!.getBoundingClientRect(); const after = gutter.nextElementSibling!.getBoundingClientRect(); const start = row ? before.left : before.top; const span = row ? after.right - before.left : after.bottom - before.top; const gap = row ? gutter.offsetWidth : gutter.offsetHeight; // A split keeps its length while its gutter is dragged. const length = measure(); live = from; const move = (moved: PointerEvent) => { const along = (row ? moved.clientX : moved.clientY) - start - gap / 2; live = shift(from, at, along / (span - gap), length); }; const done = () => { if (live) board.resize(split.id, live); live = null; gutter.removeEventListener('pointermove', move); gutter.removeEventListener('pointerup', done); gutter.removeEventListener('pointercancel', done); }; gutter.addEventListener('pointermove', move); gutter.addEventListener('pointerup', done); gutter.addEventListener('pointercancel', done); }
function nudge(event: KeyboardEvent, at: number) { const keys = row ? ['ArrowLeft', 'ArrowRight'] : ['ArrowUp', 'ArrowDown']; const by = keys.indexOf(event.key); if (by < 0) return; event.preventDefault(); const pair = split.sizes[at] + split.sizes[at + 1]; board.resize(split.id, shift(split.sizes, at, split.sizes[at] / pair + (by ? STEP : -STEP) / pair)); }
function even() { board.resize( split.id, split.children.map(() => 1 / split.children.length), ); }</script>
<div class="split" class:row class:column={!row} class:resizing={live !== null} data-tile={split.id} bind:this={element}> {#each split.children as child, at (child.id)} {#if at > 0} {@const share = sizes[at - 1] / (sizes[at - 1] + sizes[at])} <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions (a focusable separator is the ARIA window splitter) --> <div class="gutter" role="separator" aria-orientation={row ? 'vertical' : 'horizontal'} aria-label="resize" aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round(share * 100)} tabindex="0" onpointerdown={(event) => grab(event, at - 1)} onkeydown={(event) => nudge(event, at - 1)} ondblclick={even} ></div> {/if} <div class="part" style:flex-grow={sizes[at]}> <TileView {board} tile={child} /> </div> {/each}</div>
<style> .split { display: flex; flex: 1; width: 100%; height: 100%; min-width: 0; min-height: 0; } .column { flex-direction: column; } .resizing { user-select: none; } .resizing.row, .resizing.row :global(*) { cursor: col-resize !important; } .resizing.column, .resizing.column :global(*) { cursor: row-resize !important; } .resizing :global(iframe) { pointer-events: none; } .part { display: flex; flex-basis: 0; flex-shrink: 1; min-width: 0; min-height: 0; } .gutter { position: relative; flex: none; touch-action: none; } .row > .gutter { width: 6px; cursor: col-resize; } .column > .gutter { height: 6px; cursor: row-resize; } .gutter::after { position: absolute; background: var(--accent); border-radius: 1px; opacity: 0; content: ''; transition: opacity 120ms ease; } .row > .gutter::after { top: 30%; bottom: 30%; left: 2px; width: 2px; } .column > .gutter::after { left: 30%; right: 30%; top: 2px; height: 2px; } .gutter:hover::after, .gutter:focus-visible::after, .resizing > .gutter:active::after { opacity: 0.55; } .gutter:focus-visible { outline: none; }</style>