1286 lines · 65.1 KB · Text
RawDownload
diff --git a/dist/index.d.ts b/dist/index.d.tsindex 717aa3e9b59f30c9fcbd581a9db24a7e5aab7afc..7722709a57df23cb70e176191930d1bea0a0bf60 100644--- a/dist/index.d.ts+++ b/dist/index.d.ts@@ -57,6 +57,8 @@ type RestoreTarget = { } | {     kind: "tab";     panel: string;+    /** Lattice patch: the tab's place in the group it was hidden from. */+    index?: number; }; interface HiddenPanel {     panel: PanelNode;@@ -199,6 +201,25 @@ interface LayoutMetrics { /** World geometry of every node, as fractions of the whole layout. With metrics, each group keeps  * its children at their minimums (in its own layout space) and scales down when they can't fit. */ declare function layoutRects(node: LayoutNode | null, metrics?: LayoutMetrics): Map<string, Entry>;+/** Lattice patch: a panel header's measured parts. `chrome` is the bar without its tabs with the+ * whole action row and its modes named, the whole row, without the row's text labels, and+ * without the row. */+interface HeaderMeasure {+    chrome: readonly [number, number, number, number];+    gap: number;+    items: ReadonlyArray<{ viewId: string; atomic: true; full: number; compact: number } | { viewId: string; atomic: false; min: number; floor: number; natural: number }>;+}+/** Lattice patch: how a panel header fits `width` (see the comment in the patch). Named tabs show+ * every label, only `selected`'s, or none; `previous` is the mode the header shows now. */+declare function planHeader(header: HeaderMeasure, width: number, squeeze?: number, selected?: string, previous?: "all" | "selected" | "none"): {+    labelMode: "all" | "selected" | "none";+    labelled: number;+    /** The named tabs that show their label. */+    labels: string[];+    tools: "named" | null | "icon" | "hidden";+    tabMin: number | null;+    min: number;+};  type Cleanup = void | (() => void); interface MenuItem {@@ -440,6 +461,8 @@ interface WorkspaceSnapshot {     canGoForward: boolean;     framings: Framing[];     dragging: boolean;+    /** Lattice patch: the narrowest width the docked layout fits at full size. */+    minWidth: number; } interface WorkspaceEvents {     /** Committed layout changes only; never fires mid-drag or mid-animation. */@@ -533,4 +556,4 @@ declare function emptyDocument(): LayoutDocument; /** Repair anything inconsistent in a document from storage or a caller. */ declare function sanitize(input: LayoutDocument, isKnownType?: (type: string) => boolean): LayoutDocument; -export { type Axis, type BuiltInMenuId, type Cleanup, type Command, DEFAULT_KEYMAP, type Edge, type FloatSpec, type FloatingLayer, type FloatingPanel, type Framing, type HiddenPanel, type IframeOptions, type Keymap, type LayoutDocument, type Entry as LayoutEntry, type LayoutMetrics, type LayoutNode, type LayoutSpec, type MenuEntry, type MenuItem, type MenuRequest, type OpenOptions, type PanelMenuContext, type PanelNode, type PanelSpec, type Params, type Placement, type Rect, type RestoreTarget, type SplitNode, type SplitSpec, type StageNode, type StageSpec, type Surface, type Theme, type ViewEvents, type ViewHandle, type ViewInfo, type ViewPlacement, type ViewRecord, type ViewRules, type ViewSpec, type ViewState, type ViewTypeDefinition, type ViewTypes, type WorkspaceEvents, type WorkspaceHandle, type WorkspaceOptions, type WorkspaceSlots, type WorkspaceSnapshot, createDocument, createWorkspace, emptyDocument, formatCombo, layout, layoutRects, sanitize };+export { type Axis, type BuiltInMenuId, type Cleanup, type Command, DEFAULT_KEYMAP, type Edge, type FloatSpec, type FloatingLayer, type FloatingPanel, type Framing, type HiddenPanel, type IframeOptions, type Keymap, type LayoutDocument, type Entry as LayoutEntry, type LayoutMetrics, type LayoutNode, type LayoutSpec, type MenuEntry, type MenuItem, type MenuRequest, type OpenOptions, type PanelMenuContext, type PanelNode, type PanelSpec, type Params, type Placement, type Rect, type RestoreTarget, type SplitNode, type SplitSpec, type StageNode, type StageSpec, type Surface, type Theme, type ViewEvents, type ViewHandle, type ViewInfo, type ViewPlacement, type ViewRecord, type ViewRules, type ViewSpec, type ViewState, type ViewTypeDefinition, type ViewTypes, type WorkspaceEvents, type WorkspaceHandle, type WorkspaceOptions, type WorkspaceSlots, type WorkspaceSnapshot, type HeaderMeasure, createDocument, createWorkspace, emptyDocument, formatCombo, layout, layoutRects, planHeader, sanitize };diff --git a/dist/index.js b/dist/index.jsindex 101e9570f85e3c58ed50ba757cab4487578bbeff..fc0560e65029e9edae3348789d285c1577f45478 100644--- a/dist/index.js+++ b/dist/index.js@@ -442,22 +442,77 @@ function restorePanel(doc, panelId, fallbackLayer) {     ...doc,     hidden: doc.hidden.filter((h2) => h2.panel.id !== panelId)   };-  const { restore, panel } = hidden;-  if (restore.kind === "floating") return floatPanel(without, panel, restore.rect, restore.layer);+  return restoreAt(without, hidden.panel, hidden.restore, fallbackLayer, /* @__PURE__ */ new Set());+}+function restoreAt(doc, panel, restore, fallbackLayer, seen) {+  if (restore.kind === "floating") return floatPanel(doc, panel, restore.rect, restore.layer);+  const inPlace = restoreInPlaceOf(doc, panel, restore, fallbackLayer, seen);+  if (inPlace) return inPlace;   if (restore.kind === "tab") {-    const target = locatePanel(without, restore.panel);-    const node = findNode(without.root, restore.panel);-    if (target && target.where !== "hidden" || node?.kind === "stage")-      return insertPanel(without, panel, { into: restore.panel });+    const target = locatePanel(doc, restore.panel);+    const node = findNode(doc.root, restore.panel);+    // Lattice patch: a tab hidden out of a group comes back where it was in+    // it, not at the end (insertPanel clamps an index past the last tab).+    if (target && target.where !== "hidden" || node?.kind === "stage") {+      const next = insertPanel(doc, panel, { into: restore.panel, index: restore.index });+      return repointHidden(next, panel.id, panelOfView(next, panel.views[0])?.id);+    }   }-  if (restore.kind === "docked" && findNode(without.root, restore.beside))-    return insertPanel(without, panel, {+  if (restore.kind === "docked" && findNode(doc.root, restore.beside))+    return insertPanel(doc, panel, {       beside: restore.beside,       edge: restore.edge,       share: restore.share     });-  if (without.root) return floatPanel(without, panel, { x: 0.25, y: 0.2, w: 0.5, h: 0.6 }, fallbackLayer);-  return { ...without, root: panel };+  if (doc.root) return floatPanel(doc, panel, { x: 0.25, y: 0.2, w: 0.5, h: 0.6 }, fallbackLayer);+  return { ...doc, root: panel };+}+// Lattice patch: a panel whose place was in, or beside, a panel that is hidden+// too (a column hidden one panel at a time) used to float over the documents.+// Now, if that hidden panel will rejoin another as tabs, this one comes back in+// or beside that other one. Otherwise this one takes the hidden panel's place+// (floating, if it was a tab of a floating window) and the hidden panel is+// re-anchored to it the way this one sat against it: its tabs will rejoin this+// group, or it will dock on the opposite edge. Any show order rebuilds the+// layout the hide order took apart.+var OPPOSITE_EDGE = { left: "right", right: "left", top: "bottom", bottom: "top" };+function restoreInPlaceOf(doc, panel, restore, fallbackLayer, seen) {+  const anchorId = restore.kind === "tab" ? restore.panel : restore.beside;+  const anchor = doc.hidden.find((h2) => h2.panel.id === anchorId);+  if (!anchor || seen.has(anchorId)) return null;+  seen.add(anchorId);+  const place = anchor.restore;+  if (place.kind === "tab") {+    const through = restore.kind === "tab" ? { kind: "tab", panel: place.panel, index: place.index } : { ...restore, beside: place.panel };+    return restoreAt(doc, panel, through, fallbackLayer, seen);+  }+  if (restore.kind === "docked" && place.kind === "floating") return null;+  const next = restoreAt(doc, panel, place, fallbackLayer, seen);+  const id = panelOfView(next, panel.views[0])?.id ?? panel.id;+  const back = restore.kind === "tab" ? { kind: "tab", panel: id, index: restore.index ? 0 : void 0 } : {+    kind: "docked",+    beside: id,+    edge: OPPOSITE_EDGE[restore.edge],+    share: 1 - (restore.share ?? 0.5)+  };+  return {+    ...next,+    hidden: next.hidden.map((h2) => h2.panel.id === anchorId ? { ...h2, restore: back } : h2)+  };+}+// Lattice patch: a hidden panel that joined another as a tab is gone, so the+// panels hidden in or beside it come back in or beside the one it joined.+function repointHidden(doc, from, to) {+  if (!to || to === from) return doc;+  return {+    ...doc,+    hidden: doc.hidden.map((h2) => {+      const restore = h2.restore;+      if (restore.kind === "tab" && restore.panel === from) return { ...h2, restore: { ...restore, panel: to } };+      if (restore.kind === "docked" && restore.beside === from) return { ...h2, restore: { ...restore, beside: to } };+      return h2;+    })+  }; } function sanitize(input, isKnownType = () => true) {   const doc = {@@ -933,6 +988,8 @@ function applyDockTarget(root, target, incoming, splitId) { var DROP_SLOT = "__trellis-drop-slot"; var SOURCE_SLOT = "__trellis-source-slot"; var THRESHOLD = 6;+var STILL_PX = 3;+var STILL_MS = 120; var SETTLE_MS = 150; var PICKUP_MS = 280; var COMPACT = { w: 380, h: 260 };@@ -951,21 +1008,21 @@ function sameTarget(a, b) { var inside = (p, r) => p.x >= r.x && p.x <= r.x + r.w && p.y >= r.y && p.y <= r.y + r.h; function createDragController(host) {   let session = null;-  let pickupFrame = 0;   let cleanup = null;   let suppressClickUntil = 0;-  const local = (e) => {-    const b = host.root.getBoundingClientRect();-    return { x: e.clientX - b.left, y: e.clientY - b.top };-  };+  // Lattice patch: the root's box is read once a drag starts, not on every+  // pointer move (a read there could force the layout the frame had dirtied).+  const local = (e, b = session?.rootBox ?? host.root.getBoundingClientRect()) => ({ x: e.clientX - b.left, y: e.clientY - b.top });   function begin(e, panel, viewId) {     if (session || e.button !== 0 || e.altKey || e.shiftKey || e.ctrlKey || e.metaKey) return;     const doc = host.doc();     const float = doc.floating.find((f) => f.panel.id === panel.id);     const tab = viewId && panel.views.length > 1 ? viewId : null;     const screen = host.lastRects.get(panel.id) ?? { x: 0, y: 0, w: 0, h: 0 };-    const p = local(e);+    const rootBox = host.root.getBoundingClientRect();+    const p = local(e, rootBox);     session = {+      rootBox,       pointerId: e.pointerId,       start: { x: e.clientX, y: e.clientY },       active: false,@@ -1033,6 +1090,11 @@ function createDragController(host) {     window.addEventListener("pointermove", onMove, { signal });     window.addEventListener("pointerup", onUp, { signal });     window.addEventListener("pointercancel", onCancel, { signal });+    // Lattice patch: a press on a tab starts no text selection. WebKit+    // started one from the tab's title and stretched it over every panel the+    // pointer crossed, which the workspace hid only by restyling everything+    // in it for the drag (user-select on the root).+    window.addEventListener("selectstart", (ev) => ev.preventDefault(), { signal, capture: true });     window.addEventListener("blur", () => end(false), { signal });     window.addEventListener(       "keydown",@@ -1094,9 +1156,18 @@ function createDragController(host) {       activate(d);     }     d.pointer = local(e);+    if (!d.still || Math.hypot(d.pointer.x - d.still.x, d.pointer.y - d.still.y) > STILL_PX)+      d.still = { ...d.pointer, at: performance.now() };     retarget(d, e);     host.schedule();   }+  // Lattice patch: whether the pointer has stayed put for a moment (the+  // workspace lays the drop preview's views out to their new sizes then:+  // see placeSurface()).+  function resting() {+    const d = session;+    return !!d?.still && performance.now() - d.still.at >= STILL_MS;+  }   function activate(d) {     host.closeMenus();     d.active = true;@@ -1130,12 +1201,10 @@ function createDragController(host) {     }     host.root.setAttribute("data-dragging", "");     host.busy(true);-    const tick = () => {-      if (session !== d) return;-      host.render();-      pickupFrame = host.lifetime.frame(tick);-    };-    pickupFrame = host.lifetime.frame(tick);+    // Lattice patch: the workspace's own frame loop renders the drag (it+    // runs while a drag is active); a second loop here rendered every frame+    // twice.+    host.schedule();   }   function retarget(d, e) {     const viewport = host.viewport();@@ -1252,7 +1321,7 @@ function createDragController(host) {     if (!bar || !(p.x >= screen.x && p.x <= screen.x + screen.w && p.y >= screen.y && p.y <= screen.y + bar))       return null;     const dom = host.panelDom(panel.id);-    const rootBox = host.root.getBoundingClientRect();+    const rootBox = session?.rootBox ?? host.root.getBoundingClientRect();     let index = panel.views.length;     if (dom)       for (let i = 0; i < panel.views.length; i++) {@@ -1296,7 +1365,7 @@ function createDragController(host) {     return host.allowed(views, region) ? next : null;   }   function settle(d, next) {-    host.root.setAttribute("data-drop", next ? next.kind : "none");+    setAttr(host.root, "data-drop", next ? next.kind : "none");     if (sameTarget(next, d.candidate)) return;     d.candidate = next;     host.lifetime.clearTimeout(d.targetTimer);@@ -1389,7 +1458,7 @@ function createDragController(host) {           previous.set(DROP_SLOT, screen);         }       }-      d.dropLabel = "Add as tab";+      d.dropLabel = tl("addAsTab");     } else if (next?.kind === "stage") {       const world = d.dropBase.get(next.stage).rect;       d.layoutTargets.set(DROP_SLOT, world);@@ -1443,7 +1512,8 @@ function createDragController(host) {     const now = performance.now();     const amount = host.reduced() ? 1 : 1 - Math.exp(-(now - d.sizeTick) / 60);     d.sizeTick = now;-    d.desktopAmount += ((desktop && inside(d.pointer, desktop) ? 1 : 0) - d.desktopAmount) * amount;+    d.desktopTo = desktop && inside(d.pointer, desktop) ? 1 : 0;+    d.desktopAmount += (d.desktopTo - d.desktopAmount) * amount;     const compactTo = source && inside(d.pointer, source) || !d.fromFrame && desktop && inside(d.pointer, desktop) ? 0 : 1;     if (compactTo !== d.compactTo) {       d.compactFrom = d.compactAmount;@@ -1453,26 +1523,35 @@ function createDragController(host) {     const progress = host.reduced() ? 1 : Math.min(1, (now - d.pickupAt) / PICKUP_MS);     const eased = 1 - (1 - progress) ** 3;     d.compactAmount = d.compactFrom + (d.compactTo - d.compactFrom) * eased;-    const targetScale = Math.min(1, COMPACT.w / Math.max(1, original.w), COMPACT.h / Math.max(1, original.h));-    const scale = 1 + (targetScale - 1) * d.compactAmount;-    let w = d.fromFrame ? original.w + (COMPACT.w - original.w) * d.compactAmount : original.w * scale;-    let h2 = d.fromFrame ? original.h + (COMPACT.h - original.h) * d.compactAmount : original.h * scale;-    const min = host.minSize(d.lifted.views);-    const bar = host.tabbarHeight(d.lifted);-    w += (Math.max(w, min.w + host.inset() * 2) - w) * d.desktopAmount;-    h2 += (Math.max(h2, min.h + bar + host.inset() * 2) - h2) * d.desktopAmount;+    const { w, h: h2 } = liftedSize(d, d.compactAmount, d.desktopAmount);     const grabY = d.grabY + (Math.min(d.grabY, 18) - d.grabY) * d.compactAmount;     d.floating = { x: d.pointer.x - w * d.grabX, y: d.pointer.y - grabY, w, h: h2 };     return d.floating;   }+  function liftedSize(d, compactAmount, desktopAmount) {+    const original = d.sourceScreen;+    const targetScale = Math.min(1, COMPACT.w / Math.max(1, original.w), COMPACT.h / Math.max(1, original.h));+    const scale = 1 + (targetScale - 1) * compactAmount;+    let w = d.fromFrame ? original.w + (COMPACT.w - original.w) * compactAmount : original.w * scale;+    let h2 = d.fromFrame ? original.h + (COMPACT.h - original.h) * compactAmount : original.h * scale;+    const min = host.minSize(d.lifted.views);+    const bar = host.tabbarHeight(d.lifted);+    w += (Math.max(w, min.w + host.inset() * 2) - w) * desktopAmount;+    h2 += (Math.max(h2, min.h + bar + host.inset() * 2) - h2) * desktopAmount;+    return { w, h: h2 };+  }+  // Lattice patch: the size the lifted panel is easing to, for its view to+  // lay out at once (see the workspace's placeSurface()).+  function liftedRest() {+    const d = session;+    return liftedSize(d, d.compactTo, d.desktopTo ?? 0);+  }   const fullViewport = () => ({ x: 0, y: 0, ...host.viewport() });   function end(commit) {     const d = session;     if (!d) return;     host.lifetime.clearTimeout(d.targetTimer);     if (commit && d.active) apply(d, d.candidate);-    host.lifetime.cancelFrame(pickupFrame);-    pickupFrame = 0;     session = null;     cleanup?.();     cleanup = null;@@ -1512,7 +1591,7 @@ function createDragController(host) {     host.setDoc(d.origin);     host.commit(next, from, repositioning || target.kind === "tab" ? null : lifted.id);     host.moved(lifted, d.fromFloat && !d.tabRollback ? "floating" : "docked", target.kind);-    host.announce("Moved");+    host.announce(tl("moved"));   }   function floatRect(d, next, layer) {     const viewport = host.viewport();@@ -1583,6 +1662,8 @@ function createDragController(host) {     begin,     cancel: () => end(false),     liftedRect,+    liftedRest,+    resting,     /** World rect overrides for docked nodes and the two slots while dragging. */     layoutTargets: () => session?.active ? session.layoutTargets : null,     dropLabel: () => session?.dropLabel ?? ""@@ -1633,6 +1714,62 @@ function place(el, r, round) {   setStyle(el, "height", `${Math.max(0, h2)}px`); } +// Lattice patch: every user-facing string goes through tl(), so a host app can+// translate Trellis by assigning globalThis.__TRELLIS_LABELS__ (key -> (vars) => string).+var TRELLIS_EN = {+  addAsTab: () => "Add as tab",+  moved: () => "Moved",+  releaseToFocus: () => "Release to focus",+  releaseToFocusTarget: (v) => `Release to focus ${v.title} \xB7 Esc to cancel`,+  unavailable: () => "Unavailable",+  workspace: () => "Workspace",+  panelMenu: () => "Panel menu",+  resizePanels: () => "Resize panels",+  closeTab: (v) => `Close ${v.title}`,+  groupPanels: (v) => `${v.count} panels. Double-click to zoom in.`,+  doubleClickToZoomIn: () => "Double-click to zoom in",+  restoreSize: () => "Restore size",+  maximize: () => "Maximize",+  dockBesideStage: () => "Dock beside stage",+  dock: () => "Dock",+  float: () => "Float",+  newSplitRight: () => "New split right",+  newSplitBelow: () => "New split below",+  moveTo: (v) => `Move ${v.title} to`,+  hide: () => "Hide",+  closeView: (v) => `Close ${v.title}`,+  closeOtherTabs: () => "Close other tabs",+  theStage: () => "the stage",+  thisGroup: () => "this group"+};+function tl(key, vars = {}) {+  const own = globalThis.__TRELLIS_LABELS__?.[key];+  try {+    return (own ?? TRELLIS_EN[key])(vars);+  } catch {+    return TRELLIS_EN[key](vars);+  }+}++// Lattice patch: like place(), but with left/top. A transform would make every+// view surface the containing block of position:fixed descendants.+function placeAt(el, r, round) {+  let { x, y, w, h: h2 } = r;+  if (round) {+    const x2 = Math.round(x + w);+    const y2 = Math.round(y + h2);+    x = Math.round(x);+    y = Math.round(y);+    w = x2 - x;+    h2 = y2 - y;+  }+  setStyle(el, "transform", "");+  setStyle(el, "left", `${x}px`);+  setStyle(el, "top", `${y}px`);+  setStyle(el, "width", `${Math.max(0, w)}px`);+  setStyle(el, "height", `${Math.max(0, h2)}px`);+}+ // src/runtime/motion.ts var MOTION = {   spring: { stiffness: 210, damping: 29, maxStep: 0.032, epsilon: 1e-4 },@@ -1741,6 +1878,8 @@ var DOCK_MS = { hide: 300, restore: 380 };  // src/runtime/navigation.ts var MAX_HISTORY = 100;+// Lattice patch: where a press is not the navigator's to take (see pointerdown).+const PRESS_OWNERS = "[data-trellis-part=surface], button, [data-trellis-owns-gestures]"; function createNavigator(host) {   let entries = /* @__PURE__ */ new Map();   let framed = null;@@ -1948,8 +2087,10 @@ function createNavigator(host) {     "wheel",     (e) => {       if (!free()) return;-      if (handleHierarchyWheel(e)) return;       const target = e.target;+      // Lattice patch: see pointerdown above.+      if (target?.closest?.("[data-trellis-part=surface]") && (e.shiftKey || target.closest("[data-trellis-owns-gestures]"))) return;+      if (handleHierarchyWheel(e)) return;       if (!e.ctrlKey && !e.altKey && host.contentOwnsWheel(target, e)) return;       if (target.closest?.("[data-trellis-part=tabs]") && !e.ctrlKey) {         const list = target.closest("[data-trellis-part=tabs]");@@ -1993,7 +2134,7 @@ function createNavigator(host) {   const snapEl = h(     "div",     { "data-trellis-part": "snap-preview", "aria-hidden": "true" },-    h("span", {}, "Release to focus")+    h("span", {}, tl("releaseToFocus"))   );   host.root.append(snapEl);   host.lifetime.add(() => snapEl.remove());@@ -2023,7 +2164,7 @@ function createNavigator(host) {     if (m.candidate) {       const entry = entries.get(m.candidate);       place2(candidateEl, host.panelScreen(entry.rect));-      candidateEl.querySelector("span").textContent = `Release to focus ${host.titleOf(entry.node)} \xB7 Esc to cancel`;+      candidateEl.querySelector("span").textContent = tl("releaseToFocusTarget", { title: host.titleOf(entry.node) });     }   }   function finishMarquee(commit) {@@ -2056,6 +2197,11 @@ function createNavigator(host) {     "pointerdown",     (e) => {       if (!free() || host.busy() || e.button !== 0 || e.pointerType === "touch") return;+      // Lattice patch: a press inside view content belongs to the content+      // (Shift-click extends a selection, Alt-click places cursors), and a+      // press on a control in the chrome belongs to that control (⇧-click on+      // a header's Build is a clean rebuild).+      if (e.target?.closest?.(PRESS_OWNERS)) return;       if (e.shiftKey && !dragZoom) {         e.preventDefault();         e.stopPropagation();@@ -2117,7 +2263,7 @@ function createNavigator(host) {       host.root,       type,       (e) => {-        if (free() && e.altKey || performance.now() < suppressClickUntil) {+        if (free() && e.altKey && !e.target?.closest?.(PRESS_OWNERS) || performance.now() < suppressClickUntil) {           e.preventDefault();           e.stopPropagation();         }@@ -2329,8 +2475,9 @@ function formatCombo(combo) {     Enter: mac ? "\u21A9" : "Enter"   };   const key = arrows[c.key] ?? (c.key.length === 1 ? c.key.toUpperCase() : c.key);+  // Lattice patch: ⌘ first, as Lattice writes every combination (key-combos.ts): ⌘⇧↩, not ⇧⌘↩.   if (mac)-    return `${c.ctrl ? "\u2303" : ""}${c.alt ? "\u2325" : ""}${c.shift ? "\u21E7" : ""}${c.mod || c.meta ? "\u2318" : ""}${key}`;+    return `${c.ctrl ? "\u2303" : ""}${c.mod || c.meta ? "\u2318" : ""}${c.alt ? "\u2325" : ""}${c.shift ? "\u21E7" : ""}${key}`;   return [     c.mod || c.ctrl ? "Ctrl" : "",     c.meta ? "Meta" : "",@@ -2739,7 +2886,7 @@ var TYPE_PLACEHOLDER = (type) => ({       h(         "div",         { class: "trellis-placeholder" },-        h("strong", {}, "Unavailable"),+        h("strong", {}, tl("unavailable")),         h("span", {}, `No view type named \u201C${type}\u201D is registered.`)       )     );@@ -2752,6 +2899,70 @@ var escapeHtml = (text) => text.replace( var DEFAULT_FLOAT_SIZE = { w: 560, h: 400 }; var FRAME_ONLY = { w: 160, h: 64 }; var PANEL_MIN = { w: 80 };+// Lattice patch: the states of a header's action row, roomiest first, as the+// tab bar's data-tools: the whole row with its document modes named beside+// their icons, the whole row, its buttons without their text labels (Lattice+// CSS), then none (every header tool is also in the panel menu).+var HEADER_TOOLS = ["named", null, "icon", "hidden"];+// Lattice patch: which named tabs show their label, roomiest first, as+// planHeader's labelMode: every one; only the selected tab's (the others are+// icons, and every one is when a file tab is selected); none. There is no+// mode between: a strip never names some unselected tabs and not others.+var LABEL_MODES = ["all", "selected", "none"];+// Lattice patch: the room past its need a roomier label mode waits for before+// a header takes it again, so a width resting at the edge never flickers.+var LABEL_HYSTERESIS = 12;+// Lattice patch: how a panel's header fits `width`, so its tabs never run+// under its actions. `header.chrome` is the bar's width without its tabs in+// each HEADER_TOOLS state. Named tabs (atomic) show their whole label or only+// their icon; the other tabs have a CSS minimum (`min`) and, under it, a+// floor (`floor`: icon and close button, no title). When a header is short+// of room, in order:+//   1. every unselected named tab's label gives way at once (LABEL_MODES);+//   2. the other tabs shrink below their minimum toward their floor

Lines 1–500 of 1286.