← Whiteboard results

Whiteboard

nex agi Nex N2.5 mini GGUF · typescript

Initial view of Whiteboard

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 59,093 bytes · SHA-256 393265b36d07
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Whiteboard</title>
<style>
/* ── css: layout ── */
* { box-sizing: border-box; }
html, body, #app { width: 100%; height: 100%; margin: 0; overflow: hidden; }
body { font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #263141; background: #f7f8fb; }
#app { position: relative; isolation: isolate; }
#canvas { position: absolute; inset: 0; display: block; touch-action: none; cursor: default; }
#canvas.selecting { cursor: crosshair; }
#canvas.pen { cursor: crosshair; }
#canvas.eraser { cursor: cell; }
#canvas.text-mode { cursor: text; }

.topbar { position: absolute; z-index: 20; top: 18px; left: 20px; right: 20px; height: 58px; display: flex; align-items: center; justify-content: space-between; pointer-events: none; }
.brand, .top-actions, .toolbar, .properties, .zoom-bar, .status-pill { pointer-events: auto; box-shadow: 0 10px 30px rgba(30, 41, 59, 0.12), 0 1px 3px rgba(30, 41, 59, 0.16); backdrop-filter: blur(16px); }
.brand { display: flex; align-items: center; gap: 11px; height: 46px; padding: 0 16px 0 13px; border-radius: 14px; background: rgba(255, 255, 255, 0.82); }
.brand-mark { width: 27px; height: 27px; border-radius: 9px; display: grid; place-items: center; color: #fff; background: linear-gradient(135deg, #3157d5, #6d5ce7); font-size: 20px; line-height: 1; transform: rotate(-3deg); }
.brand strong { display: block; font-size: 14px; letter-spacing: -0.01em; color: #1f2937; }
.brand small { display: block; margin-top: 2px; color: #8491a5; font-size: 10px; }
.top-actions { display: flex; gap: 8px; }
.ghost { height: 36px; padding: 0 13px; border: 1px solid rgba(63, 78, 102, 0.12); border-radius: 10px; background: rgba(255, 255, 255, 0.88); color: #526174; font-size: 12px; font-weight: 600; cursor: pointer; transition: 160ms ease; }
.ghost:hover { border-color: rgba(49, 87, 213, 0.38); color: #3157d5; transform: translateY(-1px); }
.ghost[aria-pressed="false"] { color: #9aa6b7; }
.ghost[aria-pressed="false"]:hover { color: #526174; }

.toolbar { position: absolute; z-index: 20; top: 96px; left: 20px; width: 190px; padding: 9px; border-radius: 16px; background: rgba(255, 255, 255, 0.9); }
.tool-group { display: grid; gap: 4px; }
.tool { position: relative; display: grid; grid-template-columns: 27px 1fr 24px; align-items: center; gap: 8px; width: 100%; height: 40px; padding: 0 10px; border: 1px solid transparent; border-radius: 10px; background: transparent; color: #526174; text-align: left; cursor: pointer; font-size: 12px; font-weight: 600; transition: 140ms ease; }
.tool:hover { background: #f3f5fa; color: #263141; }
.tool.active { background: #edf1ff; border-color: rgba(49, 87, 213, 0.16); color: #3157d5; box-shadow: inset 0 0 0 1px rgba(49, 87, 213, 0.08); }
.tool .icon { color: #71809a; font-size: 17px; line-height: 1; text-align: center; }
.tool.active .icon { color: #3157d5; }
.tool kbd { justify-self: end; min-width: 20px; padding: 3px 4px; border: 1px solid #dfe4ed; border-radius: 5px; background: #fff; color: #98a3b4; font-family: inherit; font-size: 9px; line-height: 1; }
.tool.active kbd { border-color: #d5dcff; color: #7081dc; background: #f7f8ff; }
.tool-divider { height: 1px; margin: 8px 4px; background: #e9edf4; }

.properties { position: absolute; z-index: 20; left: 50%; bottom: 22px; transform: translateX(-50%); display: flex; align-items: center; gap: 14px; min-height: 48px; padding: 8px 10px; border-radius: 14px; background: rgba(255, 255, 255, 0.91); }
.property-label { color: #8996a9; font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
.color-control { position: relative; display: grid; place-items: center; width: 31px; height: 31px; border-radius: 9px; background: #f2f4f8; }
.color-control input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.swatch { width: 22px; height: 22px; border: 2px solid rgba(38, 49, 65, 0.12); border-radius: 7px; background: var(--swatch); }
.swatch.fill { background: color-mix(in srgb, var(--swatch) 32%, white); }
.width-control { display: flex; align-items: center; gap: 7px; color: #8491a5; font-size: 11px; font-weight: 600; }
.width-control input { width: 74px; accent-color: #3157d5; cursor: pointer; }
.width-control output { min-width: 14px; color: #526174; font-size: 11px; text-align: center; }
.property-spacer { width: 1px; height: 27px; background: #e9edf4; }
.icon-button { width: 34px; height: 34px; border: 1px solid #e5eaf1; border-radius: 9px; background: #fff; color: #66758b; font-size: 18px; cursor: pointer; transition: 140ms ease; }
.icon-button:hover { color: #3157d5; border-color: #d8defc; }

.zoom-bar { position: absolute; z-index: 20; right: 20px; bottom: 22px; display: flex; align-items: center; gap: 9px; height: 36px; padding: 0 12px; border-radius: 11px; background: rgba(255, 255, 255, 0.88); color: #748299; font-size: 11px; font-weight: 600; }
.zoom-bar .dot { width: 7px; height: 7px; border-radius: 50%; background: #45c48f; box-shadow: 0 0 0 3px rgba(69, 196, 143, 0.12); }
.zoom-sep { width: 1px; height: 15px; background: #e3e8ef; }
#zoom-label { color: #3b4a5f; }
.status-pill { position: absolute; z-index: 19; left: 50%; bottom: 80px; transform: translateX(-50%); padding: 8px 12px; border-radius: 9px; background: rgba(39, 49, 64, 0.82); color: rgba(255, 255, 255, 0.84); font-size: 11px; opacity: 0; pointer-events: none; transition: opacity 180ms ease; }
.status-pill.visible { opacity: 1; }
.inline-text { position: absolute; z-index: 30; resize: none; overflow: hidden; border: 1px solid #3157d5; border-radius: 4px; outline: none; padding: 0; color: #273140; background: rgba(255, 255, 255, 0.95); font: 600 22px Inter, ui-sans-serif, system-ui, sans-serif; line-height: 1.2; box-shadow: 0 8px 22px rgba(31, 41, 55, 0.13); }

@media (max-width: 760px) {
  .toolbar { top: 84px; left: 12px; width: 166px; }
  .properties { bottom: 14px; gap: 9px; }
  .property-label, .property-spacer { display: none; }
  .width-control input { width: 55px; }
  .topbar { left: 12px; right: 12px; top: 12px; }
  .brand small { display: none; }
  .zoom-bar { right: 12px; bottom: 72px; }
  .status-pill { bottom: 128px; max-width: calc(100vw - 24px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}
</style>
</head>
<body>
<div id="app"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./constants":"src/constants.ts","./board":"src/board.ts","./renderer":"src/renderer.ts","./ui":"src/ui.ts","./viewport":"src/viewport.ts"},"src/board.ts":{"./constants":"src/constants.ts"},"src/viewport.ts":{"./constants":"src/constants.ts"},"src/renderer.ts":{"./constants":"src/constants.ts"},"src/ui.ts":{"./constants":"src/constants.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const constants_1 = require("./constants");
const board_1 = require("./board");
const renderer_1 = require("./renderer");
const ui_1 = require("./ui");
const viewport_1 = require("./viewport");
function start() {
    const app = document.getElementById('app');
    const canvas = document.createElement('canvas');
    canvas.id = 'canvas';
    app.appendChild(canvas);
    const board = new board_1.BoardModel();
    const viewport = new viewport_1.ViewportController();
    const state = { tool: 'select', selectedIds: [], zoom: 1, showGrid: true, editingTextId: null, pointerDown: false };
    const draft = { mode: 'idle', pointerId: null, pointerScreen: { x: 0, y: 0 }, pointerWorld: { x: 0, y: 0 }, startScreen: { x: 0, y: 0 }, startWorld: { x: 0, y: 0 }, original: [], targetIds: [], target: null, handle: null, created: null, rubberBand: [], textObject: null };
    const pointer = { x: 0, y: 0 };
    const context = { app, canvas, viewport, board, renderer: new renderer_1.CanvasRenderer(canvas, () => board.getObjects(), () => draft, () => board.selectedIds()), state, draft, pointer, activeTool: 'select', selectedIds: [], pointerDown: false, lastPointer: { x: 0, y: 0 } };
    const setTool = (tool) => {
        if (commitTextEditing())
            return;
        context.activeTool = tool;
        state.tool = tool;
        state.selectedIds = [];
        draft.mode = 'idle';
        render();
    };
    const selectAt = (screenPoint, additive) => {
        const hit = board.findObjectAt(screenPoint, viewport.viewport, state.editingTextId);
        const id = hit?.id ?? null;
        if (!additive)
            state.selectedIds = [];
        if (id) {
            if (!state.selectedIds.includes(id))
                state.selectedIds.push(id);
        }
        else if (!additive) {
            state.selectedIds = [];
        }
        board.setSelected(state.selectedIds);
        updateChrome();
        render();
    };
    const setPointer = (event, capture = true) => {
        const rect = canvas.getBoundingClientRect();
        pointer.x = event.clientX - rect.left;
        pointer.y = event.clientY - rect.top;
        pointerScreenToWorld(pointer, viewport.viewport);
        draft.pointerScreen = { x: pointer.x, y: pointer.y };
        if (capture)
            canvas.setPointerCapture(event.pointerId);
        render();
    };
    const pointerScreenToWorld = (point, viewportArg) => {
        draft.pointerWorld = { x: (point.x - viewportArg.tx) / viewportArg.zoom, y: (point.y - viewportArg.ty) / viewportArg.zoom };
    };
    const startAction = (event) => {
        if (commitTextEditing())
            return;
        setPointer(event);
        context.pointerDown = true;
        draft.mode = 'pan';
        draft.pointerId = event.pointerId;
        draft.startScreen = { ...pointer };
        draft.startWorld = { ...draft.pointerWorld };
        draft.pointerScreen = { ...pointer };
        render();
    };
    const moveAction = (event) => {
        if (!context.pointerDown || event.pointerId !== draft.pointerId)
            return;
        const before = { ...draft.pointerWorld };
        setPointer(event);
        const dx = draft.pointerWorld.x - before.x;
        const dy = draft.pointerWorld.y - before.y;
        if (draft.mode === 'pan') {
            viewport.viewport.tx += dx;
            viewport.viewport.ty += dy;
        }
        else if (draft.mode === 'move') {
            board.updateMany(draft.targetIds, {
                ...(defaultShapePatch(draft.target, dx, dy)),
                points: draft.target?.type === 'freehand' ? draft.target.points.map((point) => ({ ...point, x: point.x + dx, y: point.y + dy })) : undefined,
            });
        }
        else if (draft.mode === 'stroke') {
            appendStrokePoint(draft.pointerWorld);
        }
        else if (draft.mode === 'rubberband') {
            // Rubber-band bounds are derived live from start and current world points.
        }
        else if (draft.mode === 'erase') {
            eraseAt(draft.pointerWorld, event.pointerId);
        }
        render();
    };
    const endAction = (event) => {
        if (!context.pointerDown || event.pointerId !== draft.pointerId)
            return;
        context.pointerDown = false;
        draft.pointerId = null;
        canvas.releasePointerCapture?.(event.pointerId);
        if (draft.mode === 'stroke')
            finishStroke();
        if (draft.mode === 'rubberband')
            finishRubberBand();
        if (draft.mode === 'move')
            board.commitTransaction();
        if (draft.mode === 'erase')
            board.commitTransaction();
        if (draft.mode === 'pan') {
            viewport.clampToBoard(board.getObjects());
            board.commitTransaction();
        }
        draft.mode = 'idle';
        render();
    };
    const wheel = (event) => {
        event.preventDefault();
        const rect = canvas.getBoundingClientRect();
        const point = { x: event.clientX - rect.left, y: event.clientY - rect.top };
        viewport.zoomBy(event.deltaY, point, canvas);
        updateZoom();
        render();
    };
    const resizeTarget = (handle) => {
        const object = board.getObject(draft.targetIds[0]);
        if (!object || !draft.target)
            return;
        const start = { x: draft.startWorld.x, y: draft.startWorld.y };
        const current = draft.pointerWorld;
        const left = handle.includes('w');
        const right = handle.includes('e');
        const top = handle.includes('n');
        const bottom = handle.includes('s');
        if (object.type === 'ellipse' || object.type === 'rectangle') {
            let x = object.x;
            let y = object.y;
            let w = object.w;
            let h = object.h;
            if (right)
                w = current.x - start.x;
            if (left) {
                w = start.x - current.x;
                x = current.x;
            }
            if (bottom)
                h = current.y - start.y;
            if (top) {
                h = start.y - current.y;
                y = current.y;
            }
            if (eventModifiers().shift && object.type === 'ellipse') {
                const side = Math.max(Math.abs(w), Math.abs(h));
                w = side;
                h = side;
            }
            board.update(object.id, { x, y, w, h });
        }
        else if (object.type === 'line') {
            board.update(object.id, { x2: current.x, y2: current.y });
        }
    };
    const handleFor = (screenPoint) => {
        const selected = board.getObject(state.selectedIds[0]);
        if (!selected)
            return null;
        const rect = shapeBounds(selected);
        const size = HANDLE_TOLERANCE();
        const corners = [['nw', { x: rect.x, y: rect.y }], ['ne', { x: rect.x + rect.w, y: rect.y }], ['sw', { x: rect.x, y: rect.y + rect.h }], ['se', { x: rect.x + rect.w, y: rect.y + rect.h }]];
        return corners.find(([_, point]) => distance(screenPoint, point) <= size)?.[0] ?? null;
    };
    const shapeBounds = (object) => {
        if (object.type === 'line' || object.type === 'freehand') {
            const points = object.type === 'line' ? [{ x: object.x, y: object.y }, { x: object.x2, y: object.y2 }] : object.points;
            let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
            for (const point of points) {
                minX = Math.min(minX, point.x);
                minY = Math.min(minY, point.y);
                maxX = Math.max(maxX, point.x);
                maxY = Math.max(maxY, point.y);
            }
            return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
        }
        return { x: object.x, y: object.y, w: object.w, h: object.h };
    };
    const eraseAt = (world, _pointerId) => {
        const hit = board.findObjectAt(screenFromWorld(world), viewport.viewport, state.editingTextId);
        if (hit)
            board.delete([hit.id]);
        updateChrome();
    };
    const screenFromWorld = (world) => ({ x: world.x * viewport.viewport.zoom + viewport.viewport.tx, y: world.y * viewport.viewport.zoom + viewport.viewport.ty });
    const appendStrokePoint = (point) => {
        if (!draft.created) {
            draft.created = strokeFromPoint(point);
        }
        else {
            const last = draft.created.points.at(-1);
            if (!last || distance(last, point) > 2 / viewport.viewport.zoom)
                draft.created.points.push(point);
        }
    };
    const strokeFromPoint = (point) => ({ id: '', type: 'freehand', points: [point], stroke: constants_1.DEFAULT_PALETTE.stroke, strokeWidth: 2.5, fill: 'none', opacity: 1, _createdOrder: 0, _selected: false });
    const finishStroke = () => {
        const object = draft.created;
        if (object.points.length < 2) {
            draft.created = null;
            return;
        }
        board.create({ ...object });
        draft.created = null;
        board.commitTransaction();
        updateChrome();
    };
    const rubberBandBounds = () => ({ x: Math.min(draft.startWorld.x, draft.pointerWorld.x), y: Math.min(draft.startWorld.y, draft.pointerWorld.y), w: Math.abs(draft.pointerWorld.x - draft.startWorld.x), h: Math.abs(draft.pointerWorld.y - draft.startWorld.y) });
    const finishRubberBand = () => {
        const bounds = rubberBandBounds();
        const hits = board.getObjects().filter((object) => intersects(bounds, shapeBounds(object)));
        board.setSelected(hits.map((object) => object.id));
        state.selectedIds = board.selectedIds();
        updateChrome();
    };
    const intersects = (a, b) => a.x <= b.x + b.w + constants_1.HIT_TOLERANCE_SCREEN / viewport.viewport.zoom && a.x + a.w + constants_1.HIT_TOLERANCE_SCREEN / viewport.viewport.zoom >= b.x && a.y <= b.y + b.h + constants_1.HIT_TOLERANCE_SCREEN / viewport.viewport.zoom && a.y + a.h + constants_1.HIT_TOLERANCE_SCREEN / viewport.viewport.zoom >= b.y;
    const commitTextEditing = () => {
        if (!state.editingTextId || !draft.textObject)
            return false;
        const editor = document.getElementById('inline-text');
        if (editor)
            draft.textObject.text = editor.value;
        board.update(state.editingTextId, { text: draft.textObject.text });
        state.editingTextId = null;
        draft.textObject = null;
        return true;
    };
    const canvasEvents = () => {
        canvas.addEventListener('pointerdown', (event) => {
            if (event.button === 1 || (event.button === 0 && context.activeTool === 'select' && (event.ctrlKey || event.metaKey))) {
                startAction(event);
                return;
            }
            if (context.activeTool === 'select') {
                setPointer(event);
                const hit = board.findObjectAt(pointer, viewport.viewport, state.editingTextId);
                if (hit) {
                    const handle = handleFor(pointer);
                    const selected = board.getObject(hit.id);
                    if (!state.selectedIds.includes(hit.id)) {
                        state.selectedIds = [hit.id];
                        board.setSelected([hit.id]);
                    }
                    if (handle) {
                        draft.mode = 'resize';
                        draft.handle = handle;
                        draft.target = selected;
                        draft.targetIds = [hit.id];
                        draft.startWorld = { ...draft.pointerWorld };
                        draft.original = [structuredClone(selected)];
                        board.beginTransaction(`Resize ${label(selected.type)}`);
                    }
                    else {
                        const wasSelected = state.selectedIds.includes(hit.id);
                        draft.mode = 'move';
                        draft.target = selected;
                        draft.targetIds = wasSelected ? [...state.selectedIds] : [hit.id];
                        draft.startWorld = { ...draft.pointerWorld };
                        draft.original = board.getObjects().filter((object) => draft.targetIds.includes(object.id)).map(structuredClone);
                        if (!wasSelected) {
                            state.selectedIds = [hit.id];
                            board.setSelected(state.selectedIds);
                        }
                        board.beginTransaction(`Move ${plural(draft.targetIds.length)} object`);
                    }
                }
                else {
                    if (!event.shiftKey) {
                        state.selectedIds = [];
                        board.setSelected([]);
                    }
                    draft.mode = 'rubberband';
                    draft.pointerId = event.pointerId;
                    draft.startWorld = { ...draft.pointerWorld };
                    draft.pointerScreen = { ...pointer };
                    board.beginTransaction('Rubber-band select');
                }
                context.pointerDown = true;
                draft.pointerId = event.pointerId;
                canvas.setPointerCapture(event.pointerId);
            }
            else if (context.activeTool === 'pen') {
                setPointer(event);
                draft.mode = 'stroke';
                draft.pointerId = event.pointerId;
                appendStrokePoint(draft.pointerWorld);
                canvas.setPointerCapture(event.pointerId);
            }
            else if (context.activeTool === 'eraser') {
                setPointer(event);
                draft.mode = 'erase';
                draft.pointerId = event.pointerId;
                eraseAt(draft.pointerWorld, event.pointerId);
                canvas.setPointerCapture(event.pointerId);
            }
            else if (context.activeTool === 'text') {
                if (commitTextEditing())
                    return;
                const object = { id: `text-${Date.now()}-${Math.random().toString(36).slice(2)}`, type: 'text', x: draft.pointerWorld.x, y: draft.pointerWorld.y, text: '', fontSize: 22, stroke: constants_1.DEFAULT_PALETTE.stroke, strokeWidth: 1, fill: 'none', opacity: 1, _createdOrder: 0, _selected: false };
                draft.textObject = object;
                state.editingTextId = object.id;
                board.setSelected([object.id]);
                draft.mode = 'text';
                const editor = document.createElement('textarea');
                editor.id = 'inline-text';
                editor.value = '';
                editor.style.left = `${object.x * viewport.viewport.zoom + viewport.viewport.tx}px`;
                editor.style.top = `${object.y * viewport.viewport.zoom + viewport.viewport.ty}px`;
                editor.style.width = `${Math.max(120, object.fontSize * 12)}px`;
                editor.style.height = '54px';
                app.appendChild(editor);
                editor.focus();
            }
            else {
                setPointer(event);
                draft.mode = 'shape';
                draft.pointerId = event.pointerId;
                draft.startWorld = { ...draft.pointerWorld };
                draft.created = shapeDraft(context.activeTool);
                canvas.setPointerCapture(event.pointerId);
            }
        });
        canvas.addEventListener('pointermove', moveAction);
        canvas.addEventListener('pointerup', endAction);
        canvas.addEventListener('pointercancel', endAction);
        canvas.addEventListener('wheel', wheel, { passive: false });
    };
    const shapeDraft = (tool) => {
        if (tool === 'line')
            return { id: '', type: 'line', x: draft.startWorld.x, y: draft.startWorld.y, x2: draft.startWorld.x, y2: draft.startWorld.y, stroke: constants_1.DEFAULT_PALETTE.stroke, strokeWidth: 2.5, fill: 'none', opacity: 1, _createdOrder: 0, _selected: false };
        if (tool === 'text')
            return { id: '', type: 'text', x: draft.startWorld.x, y: draft.startWorld.y, text: '', fontSize: 22, stroke: constants_1.DEFAULT_PALETTE.stroke, strokeWidth: 1, fill: 'none', opacity: 1, _createdOrder: 0, _selected: false };
        return { id: '', type: tool, x: draft.startWorld.x, y: draft.startWorld.y, w: 0, h: 0, stroke: constants_1.DEFAULT_PALETTE.stroke, strokeWidth: 2.5, fill: constants_1.DEFAULT_PALETTE.fill, opacity: 1, _createdOrder: 0, _selected: false };
    };
    const shapeFromDraft = () => {
        if (!draft.created)
            return { id: '', type: 'rectangle', x: draft.startWorld.x, y: draft.startWorld.y, w: 0, h: 0, stroke: constants_1.DEFAULT_PALETTE.stroke, strokeWidth: 2.5, fill: constants_1.DEFAULT_PALETTE.fill, opacity: 1, _createdOrder: 0, _selected: false };
        return draft.created;
    };
    const updateShapeDraft = () => {
        if (!draft.created)
            return;
        const object = draft.created;
        if (object.type === 'line') {
            object.x2 = draft.pointerWorld.x;
            object.y2 = draft.pointerWorld.y;
            return;
        }
        if (object.type === 'text') {
            object.text = '';
            return;
        }
        const bounds = normalizedShape(draft.startWorld, draft.pointerWorld);
        object.x = bounds.x;
        object.y = bounds.y;
        object.w = bounds.w;
        object.h = bounds.h;
    };
    const normalizedShape = (start, current) => ({ x: Math.min(start.x, current.x), y: Math.min(start.y, current.y), w: Math.abs(current.x - start.x), h: Math.abs(current.y - start.y) });
    const updateDraft = () => {
        if (draft.mode === 'shape')
            updateShapeDraft();
        if (draft.mode === 'rubberband')
            render();
    };
    const handleDrag = (event) => {
        if (draft.mode !== 'resize')
            return;
        resizeTarget(draft.handle);
        render();
    };
    const keydown = (event) => {
        if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'z') {
            event.preventDefault();
            event.shiftKey ? board.redo() : board.undo();
            updateChrome();
            render();
            return;
        }
        if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'y') {
            event.preventDefault();
            board.redo();
            updateChrome();
            render();
            return;
        }
        if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
            event.preventDefault();
            board.save();
            setStatus('Board saved locally');
            return;
        }
        if (state.editingTextId)
            return;
        const key = event.key.toLowerCase();
        if (key === 'v')
            setTool('select');
        else if (key === 'p')
            setTool('pen');
        else if (key === 'r')
            setTool('rectangle');
        else if (key === 'o')
            setTool('ellipse');
        else if (key === 'l')
            setTool('line');
        else if (key === 't')
            setTool('text');
        else if (key === 'e')
            setTool('eraser');
        else if ((key === 'delete' || key === 'backspace') && state.selectedIds.length > 0) {
            event.preventDefault();
            board.delete(state.selectedIds);
            state.selectedIds = [];
            board.setSelected([]);
            updateChrome();
            render();
        }
        else if (key === 'arrowleft' || key === 'arrowright' || key === 'arrowup' || key === 'arrowdown') {
            const step = event.shiftKey ? constants_1.NUDGE_SHIFT_AMOUNT : constants_1.NUDGE_AMOUNT;
            const dx = key === 'arrowleft' ? -step : key === 'arrowright' ? step : 0;
            const dy = key === 'arrowup' ? -step : key === 'arrowdown' ? step : 0;
            if (dx || dy) {
                event.preventDefault();
                board.nudge(dx, dy);
                updateChrome();
                render();
            }
        }
    };
    const eventModifiers = () => ({ shiftKey: false });
    const render = () => {
        window.boardViewport = viewport.viewport;
        window.selectedIds = state.selectedIds;
        renderer.requestRender();
        requestAnimationFrame(render);
    };
    const setStatus = (message) => { const status = document.getElementById('status'); if (status)
        status.textContent = message; };
    const updateZoom = () => { const label = document.getElementById('zoom-label'); if (label)
        label.textContent = `${Math.round(viewport.viewport.zoom * 100)}%`; };
    const updateChrome = () => {
        updateZoom();
        document.getElementById('object-count').textContent = `${board.getObjects().length} objects`;
        Array.from(app.querySelectorAll('.tool')).forEach((button) => button.classList.toggle('active', button.dataset.tool === state.tool));
    };
    (0, ui_1.createChrome)(app, {
        setTool,
        setColors: (stroke, fill, strokeWidth) => {
            const selected = board.getObject(state.selectedIds[0]);
            if (selected)
                board.update(state.selectedIds[0], { stroke, fill, strokeWidth });
            board.setSelected(state.selectedIds);
            updateChrome();
            render();
        },
        resetView: () => { viewport.reset(); updateZoom(); render(); setStatus('View reset'); },
        toggleGrid: () => { state.showGrid = !state.showGrid; setStatus(state.showGrid ? 'Grid on' : 'Grid off'); },
        undo: () => { if (board.undo()) {
            state.selectedIds = board.selectedIds();
            updateChrome();
            render();
            setStatus('Undo');
        } },
        redo: () => { if (board.redo()) {
            state.selectedIds = board.selectedIds();
            updateChrome();
            render();
            setStatus('Redo');
        } },
        setStatus,
        getZoom: () => viewport.viewport.zoom,
    });
    canvasEvents();
    window.addEventListener('keydown', keydown);
    updateChrome();
    render();
}
const HANDLE_TOLERANCE = () => constants_1.HIT_TOLERANCE_SCREEN;
const distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
const label = (type) => type[0].toUpperCase() + type.slice(1);
const plural = (count) => count === 1 ? '1 object' : `${count} objects`;
function defaultShapePatch(object, dx, dy) {
    if (!object)
        return { x: dx, y: dy };
    if (object.type === 'line')
        return { x2: object.x2 + dx, y2: object.y2 + dy };
    if (object.type === 'freehand')
        return {};
    return { x: object.x + dx, y: object.y + dy };
}
};

// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TOOL_ORDER = exports.DEFAULT_PALETTE = exports.NUDGE_SHIFT_AMOUNT = exports.NUDGE_AMOUNT = exports.HANDLE_SIZE_SCREEN = exports.HIT_TOLERANCE_SCREEN = exports.DEFAULT_ZOOM = exports.ZOOM_MAX = exports.ZOOM_MIN = exports.APP_KEY = void 0;
exports.APP_KEY = 'collaborative-whiteboard-prototype-v1';
exports.ZOOM_MIN = 0.25;
exports.ZOOM_MAX = 4;
exports.DEFAULT_ZOOM = 1;
exports.HIT_TOLERANCE_SCREEN = 7;
exports.HANDLE_SIZE_SCREEN = 8;
exports.NUDGE_AMOUNT = 1;
exports.NUDGE_SHIFT_AMOUNT = 10;
exports.DEFAULT_PALETTE = {
    stroke: '#273140',
    fill: 'rgba(47, 84, 150, 0.16)',
    background: '#f7f8fb',
    accent: '#3157d5',
};
exports.TOOL_ORDER = ['select', 'pen', 'rectangle', 'ellipse', 'line', 'text', 'eraser'];
};

// ── module: src/board.ts ──
__mods["src/board.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BoardModel = void 0;
const constants_1 = require("./constants");
class BoardModel {
    constructor() {
        this.objects = [];
        this.history = [];
        this.future = [];
        this.nextOrder = 1;
        this.nextId = 1;
        this.undoDepth = 0;
        this.transaction = null;
        this.objects = this.readPersisted().map((item, index) => ({ ...item, _order: index + 1, _selected: false }));
        this.nextOrder = Math.max(1, ...this.objects.map((item) => item._order)) + 1;
        this.nextId = Math.max(1, ...this.objects.map((item) => Number(String(item.id).replace(/\D/g, '')) || 0)) + 1;
    }
    readPersisted() {
        try {
            const raw = window.localStorage.getItem(constants_1.APP_KEY);
            if (!raw)
                return [];
            const parsed = JSON.parse(raw);
            if (!Array.isArray(parsed))
                return [];
            return parsed.filter(isStoredObject);
        }
        catch {
            return [];
        }
    }
    save() { try {
        window.localStorage.setItem(constants_1.APP_KEY, JSON.stringify(this.objects.map(({ _order, _selected, ...item }) => item)));
    }
    catch { /* storage unavailable */ } }
    clearHistory() { this.history = []; this.future = []; this.undoDepth = 0; }
    setObjects(objects) {
        this.objects = objects.map((item, index) => ({ ...item, _order: index + 1, _selected: false }));
        this.nextOrder = Math.max(1, ...this.objects.map((item) => item._order)) + 1;
        this.nextId = Math.max(1, ...this.objects.map((item) => Number(String(item.id).replace(/\D/g, '')) || 0)) + 1;
        this.clearHistory();
        this.save();
    }
    getObjects() { return this.objects; }
    getObject(id) { return this.objects.find((item) => item.id === id); }
    selectedIds() { return this.objects.filter((item) => item._selected).map((item) => item.id); }
    setSelected(ids) { const selected = new Set(ids); this.objects = this.objects.map((item) => ({ ...item, _selected: selected.has(item.id) })); this.save(); }
    findObjectAt(screenPoint, viewport, editingTextId) {
        const world = this.screenToWorld(screenPoint, viewport);
        return [...this.objects].sort((a, b) => b._order - a._order).find((item) => hitObject(item, world, viewport.zoom));
    }
    screenToWorld(point, viewport) {
        return { x: (point.x - viewport.tx) / viewport.zoom, y: (point.y - viewport.ty) / viewport.zoom };
    }
    worldToScreen(point, viewport) {
        return { x: point.x * viewport.zoom + viewport.tx, y: point.y * viewport.zoom + viewport.ty };
    }
    create(object) {
        const item = { ...object, id: `${Date.now().toString(36)}-${this.nextId++}`, _order: this.nextOrder++, _selected: false };
        this.beginTransaction(`Add ${labelFor(item.type)}`);
        this.objects = [...this.objects, item].sort((a, b) => a._order - b._order);
        this.commitTransaction();
        return item;
    }
    update(id, patch) {
        const index = this.objects.findIndex((item) => item.id === id);
        if (index < 0)
            return null;
        const next = { ...this.objects[index], ...patch, _order: this.objects[index]._order, _selected: this.objects[index]._selected };
        this.beginTransaction(`Edit ${labelFor(next.type)}`);
        this.objects[index] = next;
        this.commitTransaction();
        return next;
    }
    updateMany(ids, patch) {
        const found = this.objects.filter((item) => ids.includes(item.id));
        if (!found.length)
            return [];
        this.beginTransaction(`Move ${pluralized(found.length)} object`);
        this.objects = this.objects.map((item) => ids.includes(item.id) ? { ...item, ...patch, _order: item._order, _selected: item._selected } : item);
        this.commitTransaction();
        return found;
    }
    nudge(dx, dy) {
        const selectedIds = new Set(this.selectedIds());
        if (!selectedIds.size)
            return;
        this.beginTransaction(`Nudge ${pluralized(selectedIds.size)} object`);
        this.objects = this.objects.map((item) => selectedIds.has(item.id) ? {
            ...item,
            ...(item.type === 'line' ? { x2: item.x2 + dx, y2: item.y2 + dy } : item.type === 'freehand' ? { points: item.points.map((point) => ({ ...point, x: point.x + dx, y: point.y + dy })) } : { x: item.x + dx, y: item.y + dy }),
        } : item);
        this.commitTransaction();
    }
    delete(ids) {
        const found = this.objects.filter((item) => ids.includes(item.id));
        if (!found.length)
            return [];
        this.beginTransaction(`Erase ${pluralized(found.length)} object`);
        this.objects = this.objects.filter((item) => !ids.includes(item.id));
        this.commitTransaction();
        return found;
    }
    undo() {
        if (!this.undoDepth)
            return false;
        const entry = this.history[this.undoDepth - 1];
        this.future.unshift({ label: entry.label, board: structuredClone(this.objects) });
        this.objects = structuredClone(entry.board);
        this.undoDepth -= 1;
        this.save();
        return true;
    }
    redo() {
        if (!this.future.length)
            return false;
        const entry = this.future.shift();
        this.undoDepth = Math.min(this.undoDepth, this.history.length);
        this.history.push(entry);
        this.objects = structuredClone(entry.board);
        this.undoDepth += 1;
        this.save();
        return true;
    }
    canUndo() { return this.undoDepth > 0; }
    canRedo() { return this.future.length > 0; }
    lastLabel() { return this.undoDepth > 0 ? this.history[this.undoDepth - 1].label : null; }
    beginTransaction(label) { if (this.transaction)
        return; this.transaction = { label, before: structuredClone(this.objects) }; }
    commitTransaction() {
        const current = this.transaction;
        if (!current)
            return;
        this.transaction = null;
        if (!sceneEquals(current.before, this.objects))
            return;
        this.undoDepth = Math.max(0, Math.min(this.undoDepth, this.history.length));
        this.history = this.history.slice(0, this.undoDepth);
        this.history.push({ label: current.label, board: structuredClone(current.before) });
        if (this.history.length > 250)
            this.history.shift();
        this.undoDepth = this.history.length;
        this.future = [];
        this.save();
    }
    discardTransaction() { this.transaction = null; }
}
exports.BoardModel = BoardModel;
function isStoredObject(value) {
    if (!value || typeof value !== 'object')
        return false;
    const item = value;
    return typeof item.id === 'string' && ['rectangle', 'ellipse', 'line', 'freehand', 'text'].includes(item.type) && typeof item.stroke === 'string' && typeof item.fill === 'string' && typeof item.opacity === 'number';
}
function hitObject(item, world, zoom) {
    const tolerance = 7 / zoom;
    if (item.type === 'rectangle')
        return world.x >= item.x - tolerance && world.x <= item.x + item.w + tolerance && world.y >= item.y - tolerance && world.y <= item.y + item.h + tolerance;
    if (item.type === 'ellipse') {
        const rx = Math.abs(item.w) / 2 + tolerance;
        const ry = Math.abs(item.h) / 2 + tolerance;
        return (((world.x - item.x - item.w / 2) ** 2) / (rx ** 2)) + (((world.y - item.y - item.h / 2) ** 2) / (ry ** 2)) <= 1;
    }
    if (item.type === 'line')
        return pointSegmentDistance(world, { x: item.x, y: item.y }, { x: item.x2, y: item.y2 }) <= tolerance;
    if (item.type === 'freehand')
        return hitStroke(item.points, world, tolerance);
    return hitText(item, world);
}
function hitStroke(points, world, tolerance) {
    if (points.length < 2)
        return false;
    if (distance(world, points[0]) <= tolerance)
        return true;
    for (let i = 1; i < points.length; i++)
        if (pointSegmentDistance(world, points[i - 1], points[i]) <= tolerance)
            return true;
    return false;
}
function hitText(item, world) {
    if (item.text.length === 0)
        return false;
    return world.x >= item.x - 6 && world.x <= item.x + textWidth(item.text, item.fontSize) + 10 && world.y >= item.y - item.fontSize && world.y <= item.y + item.fontSize * 1.2;
}
function textWidth(text, fontSize) {
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    if (!context)
        return text.length * fontSize * 0.55;
    context.font = `600 ${fontSize}px Inter, ui-sans-serif, system-ui, sans-serif`;
    return Math.max(context.measureText(text).width, 10);
}
function pointSegmentDistance(point, start, end) {
    const dx = end.x - start.x;
    const dy = end.y - start.y;
    if (dx === 0 && dy === 0)
        return distance(point, start);
    const t = Math.max(0, Math.min(1, ((point.x - start.x) * dx + (point.y - start.y) * dy) / (dx * dx + dy * dy)));
    return distance(point, { x: start.x + t * dx, y: start.y + t * dy });
}
function distance(left, right) { return Math.hypot(left.x - right.x, left.y - right.y); }
function sceneEquals(a, b) {
    if (a.length !== b.length)
        return false;
    return a.every((item, index) => item.id === b[index].id && JSON.stringify({ ...item, _order: undefined, _selected: undefined }) === JSON.stringify({ ...b[index], _order: undefined, _selected: undefined }));
}
function labelFor(type) { return type[0].toUpperCase() + type.slice(1); }
function pluralized(count) { return count === 1 ? '1 object' : `${count} objects`; }
};

// ── module: src/viewport.ts ──
__mods["src/viewport.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ViewportController = void 0;
const constants_1 = require("./constants");
class ViewportController {
    constructor() {
        this.viewport = { zoom: 1, tx: 0, ty: 0 };
        this.lastWheel = 0;
    }
    setViewport(next) {
        this.viewport = { zoom: clamp(next.zoom, constants_1.ZOOM_MIN, constants_1.ZOOM_MAX), tx: next.tx, ty: next.ty };
    }
    setZoomAt(zoom, screenPoint, pointOnCanvas) {
        const target = clamp(zoom, constants_1.ZOOM_MIN, constants_1.ZOOM_MAX);
        const rect = pointOnCanvas.getBoundingClientRect();
        const local = { x: screenPoint.x - rect.left, y: screenPoint.y - rect.top };
        const worldBefore = { x: (local.x - this.viewport.tx) / this.viewport.zoom, y: (local.y - this.viewport.ty) / this.viewport.zoom };
        const scale = target / this.viewport.zoom;
        this.viewport = { zoom: target, tx: local.x - worldBefore.x * target, ty: local.y - worldBefore.y * target };
    }
    zoomBy(delta, screenPoint, pointOnCanvas) {
        const exponent = Math.exp(-delta * 0.0015);
        this.setZoomAt(this.viewport.zoom * exponent, screenPoint, pointOnCanvas);
    }
    reset() { this.viewport = { zoom: 1, tx: 0, ty: 0 }; }
    panBy(dx, dy) {
        this.viewport.tx += dx;
        this.viewport.ty += dy;
    }
    clampToBoard(_objects) {
        // Keep pan within generous bounds so the prototype stays usable on small screens.
        this.viewport.tx = clamp(this.viewport.tx, -18000, 18000);
        this.viewport.ty = clamp(this.viewport.ty, -18000, 18000);
    }
}
exports.ViewportController = ViewportController;
function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); }
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CanvasRenderer = void 0;
const constants_1 = require("./constants");
class CanvasRenderer {
    constructor(canvas, getObjects, getDraft, getSelectedIds) {
        this.getObjects = getObjects;
        this.getDraft = getDraft;
        this.getSelectedIds = getSelectedIds;
        this.dirty = true;
        this.canvas = canvas;
        this.context = canvas.getContext('2d');
        this.resizeObserver = new ResizeObserver(() => this.resize());
        this.resizeObserver.observe(canvas);
        this.resize();
    }
    requestRender() { this.dirty = true; }
    resize() {
        const rect = this.canvas.getBoundingClientRect();
        const dpr = window.devicePixelRatio || 1;
        this.canvas.width = Math.max(1, Math.round(rect.width * dpr));
        this.canvas.height = Math.max(1, Math.round(rect.height * dpr));
        this.canvas.style.width = `${rect.width}px`;
        this.canvas.style.height = `${rect.height}px`;
        this.context.setTransform(dpr, 0, 0, dpr, 0, 0);
        this.dirty = true;
    }
    render() {
        if (!this.dirty)
            return;
        this.dirty = false;
        const ctx = this.context;
        const width = this.canvas.clientWidth;
        const height = this.canvas.clientHeight;
        const viewport = window.boardViewport;
        if (!viewport)
            return;
        const { zoom, tx, ty } = viewport;
        ctx.clearRect(0, 0, width, height);
        ctx.fillStyle = '#f7f8fb';
        ctx.fillRect(0, 0, width, height);
        this.drawGrid(ctx, width, height, viewport);
        ctx.save();
        ctx.translate(tx, ty);
        ctx.scale(zoom, zoom);
        for (const object of this.getObjects())
            this.drawObject(ctx, object, this.getSelectedIds().includes(object.id));
        const draft = this.getDraft();
        if (draft && draft.mode !== 'idle')
            this.drawDraft(ctx, draft, zoom);
        ctx.restore();
    }
    drawGrid(ctx, width, height, viewport) {
        const zoom = viewport.zoom;
        const gridSize = 48 * (zoom >= 1 ? 1 : zoom === 0.5 ? 2 : 4);
        const startX = Math.floor((-viewport.tx / zoom) / gridSize) * gridSize * zoom;
        const startY = Math.floor((-viewport.ty / zoom) / gridSize) * gridSize * zoom;
        ctx.strokeStyle = 'rgba(45, 55, 72, 0.07)';
        ctx.lineWidth = Math.max(1, zoom * 0.8);
        ctx.beginPath();
        for (let x = startX; x < viewport.tx + width + gridSize * zoom; x += gridSize * zoom) {
            ctx.moveTo(x, viewport.ty);
            ctx.lineTo(x, viewport.ty + height);
        }
        for (let y = startY; y < viewport.ty + height + gridSize * zoom; y += gridSize * zoom) {
            ctx.moveTo(viewport.tx, y);
            ctx.lineTo(viewport.tx + width, y);
        }
        ctx.stroke();
    }
    drawObject(ctx, object, selected) {
        ctx.save();
        ctx.globalAlpha = object.opacity;
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';
        ctx.strokeStyle = object.stroke;
        ctx.fillStyle = object.fill;
        ctx.lineWidth = object.strokeWidth;
        if (object.type === 'rectangle')
            this.drawRectangle(ctx, object, selected);
        else if (object.type === 'ellipse')
            this.drawEllipse(ctx, object, selected);
        else if (object.type === 'line')
            this.drawLine(ctx, object, selected);
        else if (object.type === 'freehand')
            this.drawFreehand(ctx, object);
        else
            this.drawText(ctx, object);
        ctx.restore();
    }
    drawRectangle(ctx, object, selected) {
        const x = object.x, y = object.y, w = object.w, h = object.h;
        if (object.fill && object.fill !== 'none') {
            ctx.beginPath();
            ctx.rect(x, y, w, h);
            ctx.fill();
        }
        ctx.beginPath();
        ctx.rect(x, y, w, h);
        ctx.stroke();
        if (selected)
            this.drawSelectionBox(ctx, x, y, w, h);
    }
    drawEllipse(ctx, object, selected) {
        ctx.beginPath();
        ctx.ellipse(object.x + object.w / 2, object.y + object.h / 2, Math.abs(object.w / 2), Math.abs(object.h / 2), 0, 0, Math.PI * 2);
        if (object.fill && object.fill !== 'none')
            ctx.fill();
        ctx.stroke();
        if (selected)
            this.drawSelectionBox(ctx, object.x, object.y, object.w, object.h);
    }
    drawLine(ctx, object, selected) {
        this.drawArrow(ctx, object.x, object.y, object.x2, object.y2);
        if (selected) {
            ctx.fillStyle = '#fff';
            ctx.beginPath();
            ctx.arc(object.x, object.y, constants_1.HANDLE_SIZE_SCREEN / zoomOf(ctx), 0, Math.PI * 2);
            ctx.fill();
            ctx.beginPath();
            ctx.arc(object.x2, object.y2, constants_1.HANDLE_SIZE_SCREEN / zoomOf(ctx), 0, Math.PI * 2);
            ctx.fill();
        }
    }
    drawArrow(ctx, x1, y1, x2, y2) {
        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.stroke();
        const angle = Math.atan2(y2 - y1, x2 - x1);
        const size = Math.max(8, ctx.lineWidth * 2.4);
        ctx.beginPath();
        ctx.moveTo(x2, y2);
        ctx.lineTo(x2 - size * Math.cos(angle - Math.PI / 6), y2 - size * Math.sin(angle - Math.PI / 6));
        ctx.lineTo(x2 - size * Math.cos(angle + Math.PI / 6), y2 - size * Math.sin(angle + Math.PI / 6));
        ctx.closePath();
        ctx.fillStyle = ctx.strokeStyle;
        ctx.fill();
    }
    drawFreehand(ctx, object) {
        if (object.points.length < 2)
            return;
        ctx.beginPath();
        ctx.moveTo(object.points[0].x, object.points[0].y);
        for (let i = 1; i < object.points.length - 1; i++) {
            const current = object.points[i];
            const next = object.points[i + 1];
            const midX = (current.x + next.x) / 2;
            const midY = (current.y + next.y) / 2;
            ctx.quadraticCurveTo(current.x, current.y, midX, midY);
        }
        const last = object.points[object.points.length - 1];
        ctx.lineTo(last.x, last.y);
        ctx.stroke();
    }
    drawText(ctx, object) {
        ctx.font = `600 ${object.fontSize}px Inter, ui-sans-serif, system-ui, sans-serif`;
        ctx.fillStyle = '#273140';
        ctx.fillText(object.text, object.x, object.y);
        ctx.strokeStyle = 'rgba(39, 49, 64, 0.18)';
        ctx.lineWidth = 1;
        ctx.strokeText(object.text, object.x, object.y);
        if (selectedOf(ctx))
            this.drawTextSelection(ctx, object);
    }
    drawDraft(ctx, draft, zoom) {
        ctx.save();
        ctx.globalAlpha = 0.9;
        if (draft.mode === 'rubberband') {
            const x = Math.min(draft.startWorld.x, draft.pointerWorld.x);
            const y = Math.min(draft.startWorld.y, draft.pointerWorld.y);
            const w = Math.abs(draft.pointerWorld.x - draft.startWorld.x);
            const h = Math.abs(draft.pointerWorld.y - draft.startWorld.y);
            ctx.strokeStyle = '#3157d5';
            ctx.lineWidth = 1.5 / zoom;
            ctx.setLineDash([6 / zoom, 5 / zoom]);
            ctx.strokeRect(x, y, w, h);
            ctx.restore();
            ctx.setLineDash([]);
            return;
        }
        if (draft.mode === 'stroke') {
            const points = draft.pointerWorld ? [draft.startWorld, draft.pointerWorld] : [draft.startWorld];
            this.drawFreehandStroke(ctx, points, '#273140', 2.5);
            ctx.restore();
            return;
        }
        if (draft.created) {
            const object = draft.created;
            if (object.type === 'line') {
                this.drawArrow(ctx, object.x, object.y, object.x2, object.y2);
            }
            else if (object.type === 'ellipse') {
                ctx.beginPath();
                ctx.ellipse(object.x + object.w / 2, object.y + object.h / 2, Math.abs(object.w / 2), Math.abs(object.h / 2), 0, 0, Math.PI * 2);
                ctx.strokeStyle = object.stroke;
                ctx.lineWidth = object.strokeWidth;
                ctx.stroke();
            }
            else if (object.type === 'text') {
                ctx.font = `600 ${object.fontSize}px Inter, ui-sans-serif, system-ui, sans-serif`;
                ctx.fillStyle = '#273140';
                ctx.fillText(object.text, object.x, object.y);
            }
            else {
                ctx.beginPath();
                ctx.rect(object.x, object.y, object.w, object.h);
                if (object.fill)
                    ctx.fillStyle = object.fill;
                ctx.fill();
                ctx.strokeStyle = object.stroke;
                ctx.lineWidth = object.strokeWidth;
                ctx.stroke();
            }
        }
        ctx.restore();
    }
    drawFreehandStroke(ctx, points, stroke, width) {
        if (points.length < 2)
            return;
        ctx.beginPath();
        ctx.moveTo(points[0].x, points[0].y);
        for (let i = 1; i < points.length - 1; i++) {
            const current = points[i];
            const next = points[i + 1];
            ctx.quadraticCurveTo(current.x, current.y, (current.x + next.x) / 2, (current.y + next.y) / 2);
        }
        const last = points[points.length - 1];
        ctx.lineTo(last.x, last.y);
        ctx.strokeStyle = stroke;
        ctx.lineWidth = width;
        ctx.stroke();
    }
    drawSelectionBox(ctx, x, y, w, h) {
        const zoom = zoomOf(ctx);
        const size = constants_1.HANDLE_SIZE_SCREEN / zoom;
        ctx.strokeStyle = '#3157d5';
        ctx.lineWidth = 1.2 / zoom;
        ctx.strokeRect(x, y, w, h);
        ctx.fillStyle = '#fff';
        ctx.strokeStyle = '#3157d5';
        for (const [px, py] of [[x, y], [x + w, y], [x, y + h], [x + w, y + h]]) {
            ctx.beginPath();
            ctx.rect(px - size / 2, py - size / 2, size, size);
            ctx.fill();
            ctx.stroke();
        }
    }
    drawTextSelection(ctx, object) {
        const zoom = zoomOf(ctx);
        ctx.font = `600 ${object.fontSize}px Inter, ui-sans-serif, system-ui, sans-serif`;
        const width = ctx.measureText(object.text).width;
        const height = object.fontSize;
        const size = constants_1.HANDLE_SIZE_SCREEN / zoom;
        ctx.strokeStyle = '#3157d5';
        ctx.fillStyle = '#fff';
        ctx.beginPath();
        ctx.rect(object.x, object.y - height, width + 12, height + 14);
        ctx.stroke();
        for (const [px, py] of [[object.x, object.y - height], [object.x + width + 12, object.y - height], [object.x, object.y + 4], [object.x + width + 12, object.y + 4]]) {
            ctx.beginPath();
            ctx.rect(px - size / 2, py - size / 2, size, size);
            ctx.fill();
            ctx.stroke();
        }
    }
}
exports.CanvasRenderer = CanvasRenderer;
function zoomOf(ctx) { return window.boardViewport?.zoom ?? 1; }
function selectedOf(ctx) { return window.selectedIds?.length === 1 && ctx.measureText(''); }
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createChrome = createChrome;
const constants_1 = require("./constants");
function createChrome(app, hooks) {
    app.innerHTML = `
    <header class="topbar">
      <div class="brand"><span class="brand-mark">◫</span><div><strong>Canvas</strong><small>Collaborative whiteboard prototype</small></div></div>
      <div class="top-actions"><button id="reset-view" class="ghost">Reset view</button><button id="toggle-grid" class="ghost" aria-pressed="true">Grid</button></div>
    </header>
    <aside class="toolbar" aria-label="Drawing tools">
      <div class="tool-group">
        <button data-tool="select" class="tool active" title="Select / pan (V)"><span class="icon">↖</span><span>Select</span><kbd>V</kbd></button>
        <button data-tool="pen" class="tool" title="Freehand pen (P)"><span class="icon">⌁</span><span>Pen</span><kbd>P</kbd></button>
        <button data-tool="rectangle" class="tool" title="Rectangle (R)"><span class="icon">▭</span><span>Rectangle</span><kbd>R</kbd></button>
        <button data-tool="ellipse" class="tool" title="Ellipse (O)"><span class="icon">◯</span><span>Ellipse</span><kbd>O</kbd></button>
        <button data-tool="line" class="tool" title="Line / arrow (L)"><span class="icon">➤</span><span>Line</span><kbd>L</kbd></button>
      </div>
      <div class="tool-divider"></div>
      <div class="tool-group">
        <button data-tool="text" class="tool" title="Text (T)"><span class="icon">T</span><span>Text</span><kbd>T</kbd></button>
        <button data-tool="eraser" class="tool" title="Eraser (E)"><span class="icon">⌫</span><span>Eraser</span><kbd>E</kbd></button>
      </div>
    </aside>
    <div class="properties" id="properties">
      <div class="property-label">Properties</div>
      <label class="color-control"><span class="swatch" id="stroke-swatch" style="--swatch: ${constants_1.DEFAULT_PALETTE.stroke}"></span><input id="stroke-color" type="color" value="${constants_1.DEFAULT_PALETTE.stroke}" aria-label="Stroke color"></label>
      <label class="color-control"><span class="swatch fill" id="fill-swatch" style="--swatch: ${constants_1.DEFAULT_PALETTE.fill}"></span><input id="fill-color" type="color" value="${constants_1.DEFAULT_PALETTE.fill}" aria-label="Fill color"></label>
      <label class="width-control">Line <input id="line-width" type="range" min="1" max="8" value="2"><output id="width-output">2</output></label>
      <div class="property-spacer"></div>
      <button id="undo" class="icon-button" title="Undo (Ctrl+Z)" aria-label="Undo">↶</button>
      <button id="redo" class="icon-button" title="Redo (Ctrl+Shift+Z)" aria-label="Redo">↷</button>
    </div>
    <div class="zoom-bar"><span class="dot"></span><span id="zoom-label">100%</span><span class="zoom-sep"></span><span id="object-count">0 objects</span></div>
    <div class="status-pill" id="status">Select a tool or draw directly on the canvas</div>
  `;
    const buttons = Array.from(app.querySelectorAll('.tool'));
    buttons.forEach((button) => button.addEventListener('click', () => { hooks.setTool(button.dataset.tool); setStatusFrom(button); }));
    app.querySelector('#reset-view').addEventListener('click', () => { hooks.resetView(); setStatusFrom('View reset'); });
    app.querySelector('#toggle-grid').addEventListener('click', (event) => { const button = event.currentTarget; button.setAttribute('aria-pressed', button.getAttribute('aria-pressed') === 'true' ? 'false' : 'true'); hooks.toggleGrid(); });
    app.querySelector('#undo').addEventListener('click', () => { hooks.undo(); setStatusFrom('Undo'); });
    app.querySelector('#redo').addEventListener('click', () => { hooks.redo(); setStatusFrom('Redo'); });
    app.querySelector('#stroke-color').addEventListener('input', (event) => { const value = event.target.value; app.querySelector('#stroke-swatch').style.setProperty('--swatch', value); hooks.setColors(value, app.querySelector('#fill-color').value, Number(app.querySelector('#line-width').value)); });
    app.querySelector('#fill-color').addEventListener('input', (event) => { const value = event.target.value; app.querySelector('#fill-swatch').style.setProperty('--swatch', value); hooks.setColors(app.querySelector('#stroke-color').value, value, Number(app.querySelector('#line-width').value)); });
    app.querySelector('#line-width').addEventListener('input', (event) => { const value = Number(event.target.value); app.querySelector('#width-output').value = String(value); hooks.setColors(app.querySelector('#stroke-color').value, app.querySelector('#fill-color').value, value); });
    window.addEventListener('resize', () => { app.querySelector('#object-count').textContent = `${window.objectCount ?? 0} objects`; });
}
function setStatusFrom(source) {
    const label = typeof source === 'string' ? source : source.title.replace(/ \(.*/u, '').replace(/^Select/u, 'Select');
    if (source !== 'View reset')
        window.statusMessage = label;
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>