/** * — Layered tab system for rSpace. * * Each tab is a "layer" — an rApp page within the current space. * Supports two view modes: * - flat: traditional tab bar (looking down at one layer) * - stack: side view showing all layers as stacked strata with flows * * Attributes: * active — the active layer ID * space — current space slug * view-mode — "flat" | "stack" * * Events: * layer-switch — fired when user clicks a tab { detail: { layerId, moduleId } } * layer-add — fired when user clicks + to add a layer * layer-close — fired when user closes a tab { detail: { layerId } } * layer-reorder — fired on drag reorder { detail: { layerId, newIndex } } * view-toggle — fired when switching flat/stack view { detail: { mode } } * flow-select — fired when a flow is clicked in stack view { detail: { flowId } } */ import type { Layer, LayerFlow, FlowKind } from "../../lib/layer-types"; import { FLOW_COLORS, FLOW_LABELS } from "../../lib/layer-types"; // Re-export badge info so the tab bar can show module colors const MODULE_BADGES: Record = { canvas: { badge: "rS", color: "#5eead4" }, notes: { badge: "rN", color: "#fcd34d" }, pubs: { badge: "rP", color: "#fda4af" }, swag: { badge: "rSw", color: "#fda4af" }, splat: { badge: "r3", color: "#d8b4fe" }, cal: { badge: "rC", color: "#7dd3fc" }, trips: { badge: "rT", color: "#6ee7b7" }, maps: { badge: "rM", color: "#86efac" }, chats: { badge: "rCh", color: "#6ee7b7" }, inbox: { badge: "rI", color: "#a5b4fc" }, mail: { badge: "rMa", color: "#93c5fd" }, forum: { badge: "rFo", color: "#fcd34d" }, choices: { badge: "rCo", color: "#f0abfc" }, vote: { badge: "rV", color: "#c4b5fd" }, funds: { badge: "rF", color: "#bef264" }, wallet: { badge: "rW", color: "#fde047" }, cart: { badge: "rCt", color: "#fdba74" }, auctions: { badge: "rA", color: "#fca5a5" }, providers: { badge: "rPr", color: "#fdba74" }, tube: { badge: "rTu", color: "#f9a8d4" }, photos: { badge: "rPh", color: "#f9a8d4" }, network: { badge: "rNe", color: "#93c5fd" }, socials: { badge: "rSo", color: "#7dd3fc" }, files: { badge: "rFi", color: "#67e8f9" }, books: { badge: "rB", color: "#fda4af" }, data: { badge: "rD", color: "#d8b4fe" }, work: { badge: "rWo", color: "#cbd5e1" }, }; export class RStackTabBar extends HTMLElement { #shadow: ShadowRoot; #layers: Layer[] = []; #flows: LayerFlow[] = []; #viewMode: "flat" | "stack" = "flat"; #draggedTabId: string | null = null; #addMenuOpen = false; #flowDragSource: string | null = null; #flowDragTarget: string | null = null; #flowDialogOpen = false; #flowDialogSourceId = ""; #flowDialogTargetId = ""; constructor() { super(); this.#shadow = this.attachShadow({ mode: "open" }); } static get observedAttributes() { return ["active", "space", "view-mode"]; } get active(): string { return this.getAttribute("active") || ""; } set active(val: string) { this.setAttribute("active", val); } get space(): string { return this.getAttribute("space") || ""; } get viewMode(): "flat" | "stack" { return (this.getAttribute("view-mode") as "flat" | "stack") || "flat"; } set viewMode(val: "flat" | "stack") { this.setAttribute("view-mode", val); } connectedCallback() { this.#render(); } attributeChangedCallback() { this.#viewMode = this.viewMode; this.#render(); } /** Set the layer list (call from outside) */ setLayers(layers: Layer[]) { this.#layers = [...layers].sort((a, b) => a.order - b.order); this.#render(); } /** Set the inter-layer flows (for stack view) */ setFlows(flows: LayerFlow[]) { this.#flows = flows; if (this.#viewMode === "stack") this.#render(); } /** Add a single layer */ addLayer(layer: Layer) { this.#layers.push(layer); this.#layers.sort((a, b) => a.order - b.order); this.#render(); } /** Remove a layer by ID */ removeLayer(layerId: string) { this.#layers = this.#layers.filter(l => l.id !== layerId); this.#render(); } /** Update a layer */ updateLayer(layerId: string, updates: Partial) { const layer = this.#layers.find(l => l.id === layerId); if (layer) { Object.assign(layer, updates); this.#layers.sort((a, b) => a.order - b.order); this.#render(); } } // ── Render ── #render() { const active = this.active; this.#shadow.innerHTML = `
${this.#layers.map(l => this.#renderTab(l, active)).join("")} ${this.#addMenuOpen ? this.#renderAddMenu() : ""}
${this.#viewMode === "stack" ? this.#renderStackView() : ""} `; this.#attachEvents(); } #renderTab(layer: Layer, activeId: string): string { const badge = MODULE_BADGES[layer.moduleId]; const isActive = layer.id === activeId; const badgeColor = layer.color || badge?.color || "#94a3b8"; return `
${badge?.badge || layer.moduleId.slice(0, 2)} ${layer.label} ${this.#layers.length > 1 ? `` : ""}
`; } #renderAddMenu(): string { // Group available modules (show ones not yet added as layers) const existingModuleIds = new Set(this.#layers.map(l => l.moduleId)); const available = Object.entries(MODULE_BADGES) .filter(([id]) => !existingModuleIds.has(id)) .map(([id, info]) => ({ id, ...info })); if (available.length === 0) { return `
All modules added
`; } return `
${available.map(m => ` `).join("")}
`; } #renderStackView(): string { const layerCount = this.#layers.length; if (layerCount === 0) return ""; const layerHeight = 60; const layerGap = 40; const totalHeight = layerCount * layerHeight + (layerCount - 1) * layerGap + 80; const width = 600; // Build layer rects and flow arcs let layersSvg = ""; let flowsSvg = ""; const layerPositions = new Map(); this.#layers.forEach((layer, i) => { const y = 40 + i * (layerHeight + layerGap); const x = 40; const w = width - 80; const badge = MODULE_BADGES[layer.moduleId]; const color = layer.color || badge?.color || "#94a3b8"; layerPositions.set(layer.id, { x, y, w, h: layerHeight }); const isActive = layer.id === this.active; layersSvg += ` ${layer.label} ${badge?.badge || layer.moduleId} `; }); // Draw flows as curved arcs on the right side for (const flow of this.#flows) { const src = layerPositions.get(flow.sourceLayerId); const tgt = layerPositions.get(flow.targetLayerId); if (!src || !tgt) continue; const srcY = src.y + src.h / 2; const tgtY = tgt.y + tgt.h / 2; const rightX = src.x + src.w; const arcOut = 30 + flow.strength * 40; const color = flow.color || FLOW_COLORS[flow.kind] || "#94a3b8"; const strokeWidth = 1.5 + flow.strength * 3; // Going down or up — arc curves right const direction = tgtY > srcY ? 1 : -1; const midY = (srcY + tgtY) / 2; flowsSvg += ` ${flow.label ? ` ${flow.label} ` : ""} `; } // Flow kind legend const activeKinds = new Set(this.#flows.map(f => f.kind)); let legendSvg = ""; let legendX = 50; for (const kind of activeKinds) { const color = FLOW_COLORS[kind]; legendSvg += ` ${FLOW_LABELS[kind]} `; legendX += FLOW_LABELS[kind].length * 7 + 24; } // Flow creation hint const hintSvg = this.#layers.length >= 2 ? ` Drag from one layer to another to create a flow ` : ""; return `
${flowsSvg} ${layersSvg} ${legendSvg} ${hintSvg} ${this.#flowDialogOpen ? this.#renderFlowDialog() : ""}
`; } // ── Flow creation dialog ── #renderFlowDialog(): string { const srcLayer = this.#layers.find(l => l.id === this.#flowDialogSourceId); const tgtLayer = this.#layers.find(l => l.id === this.#flowDialogTargetId); if (!srcLayer || !tgtLayer) return ""; const srcBadge = MODULE_BADGES[srcLayer.moduleId]; const tgtBadge = MODULE_BADGES[tgtLayer.moduleId]; const kinds: FlowKind[] = ["economic", "trust", "data", "attention", "governance", "resource"]; return `
New Flow
${srcBadge?.badge || srcLayer.moduleId} ${tgtBadge?.badge || tgtLayer.moduleId}
${kinds.map(k => ` `).join("")}
`; } #openFlowDialog(sourceLayerId: string, targetLayerId: string) { this.#flowDialogOpen = true; this.#flowDialogSourceId = sourceLayerId; this.#flowDialogTargetId = targetLayerId; this.#render(); } #closeFlowDialog() { this.#flowDialogOpen = false; this.#flowDialogSourceId = ""; this.#flowDialogTargetId = ""; this.#render(); } // ── Events ── #attachEvents() { // Tab clicks this.#shadow.querySelectorAll(".tab").forEach(tab => { tab.addEventListener("click", (e) => { const target = e.target as HTMLElement; if (target.classList.contains("tab-close")) return; const layerId = tab.dataset.layerId!; const moduleId = tab.dataset.moduleId!; this.active = layerId; this.dispatchEvent(new CustomEvent("layer-switch", { detail: { layerId, moduleId }, bubbles: true, })); }); // Drag-and-drop reorder tab.addEventListener("dragstart", (e) => { this.#draggedTabId = tab.dataset.layerId!; tab.classList.add("dragging"); (e as DragEvent).dataTransfer!.effectAllowed = "move"; }); tab.addEventListener("dragend", () => { tab.classList.remove("dragging"); this.#draggedTabId = null; }); tab.addEventListener("dragover", (e) => { e.preventDefault(); (e as DragEvent).dataTransfer!.dropEffect = "move"; tab.classList.add("drag-over"); }); tab.addEventListener("dragleave", () => { tab.classList.remove("drag-over"); }); tab.addEventListener("drop", (e) => { e.preventDefault(); tab.classList.remove("drag-over"); if (!this.#draggedTabId || this.#draggedTabId === tab.dataset.layerId) return; const targetIdx = this.#layers.findIndex(l => l.id === tab.dataset.layerId); this.dispatchEvent(new CustomEvent("layer-reorder", { detail: { layerId: this.#draggedTabId, newIndex: targetIdx }, bubbles: true, })); }); }); // Close buttons this.#shadow.querySelectorAll(".tab-close").forEach(btn => { btn.addEventListener("click", (e) => { e.stopPropagation(); const layerId = (btn as HTMLElement).dataset.close!; this.dispatchEvent(new CustomEvent("layer-close", { detail: { layerId }, bubbles: true, })); }); }); // Add button const addBtn = this.#shadow.getElementById("add-btn"); addBtn?.addEventListener("click", (e) => { e.stopPropagation(); this.#addMenuOpen = !this.#addMenuOpen; this.#render(); }); // Add menu items this.#shadow.querySelectorAll(".add-menu-item").forEach(item => { item.addEventListener("click", (e) => { e.stopPropagation(); const moduleId = item.dataset.addModule!; this.#addMenuOpen = false; this.dispatchEvent(new CustomEvent("layer-add", { detail: { moduleId }, bubbles: true, })); }); }); // Close add menu on outside click if (this.#addMenuOpen) { const handler = () => { this.#addMenuOpen = false; this.#render(); document.removeEventListener("click", handler); }; setTimeout(() => document.addEventListener("click", handler), 0); } // View toggle const viewToggle = this.#shadow.getElementById("view-toggle"); viewToggle?.addEventListener("click", () => { const newMode = this.#viewMode === "flat" ? "stack" : "flat"; this.#viewMode = newMode; this.setAttribute("view-mode", newMode); this.dispatchEvent(new CustomEvent("view-toggle", { detail: { mode: newMode }, bubbles: true, })); this.#render(); }); // Stack view layer clicks + drag-to-connect this.#shadow.querySelectorAll(".stack-layer").forEach(g => { const layerId = g.dataset.layerId!; // Click to switch layer g.addEventListener("click", () => { if (this.#flowDragSource) return; // ignore if mid-drag const layer = this.#layers.find(l => l.id === layerId); if (layer) { this.active = layerId; this.dispatchEvent(new CustomEvent("layer-switch", { detail: { layerId, moduleId: layer.moduleId }, bubbles: true, })); } }); // Drag-to-connect: mousedown starts a flow drag g.addEventListener("mousedown", (e) => { if ((e as MouseEvent).button !== 0) return; this.#flowDragSource = layerId; g.classList.add("flow-drag-source"); }); g.addEventListener("mouseenter", () => { if (this.#flowDragSource && this.#flowDragSource !== layerId) { this.#flowDragTarget = layerId; g.classList.add("flow-drag-target"); } }); g.addEventListener("mouseleave", () => { if (this.#flowDragTarget === layerId) { this.#flowDragTarget = null; g.classList.remove("flow-drag-target"); } }); }); // Global mouseup completes the flow drag const svgEl = this.#shadow.querySelector(".stack-view svg"); if (svgEl) { svgEl.addEventListener("mouseup", () => { if (this.#flowDragSource && this.#flowDragTarget) { this.#openFlowDialog(this.#flowDragSource, this.#flowDragTarget); } // Reset drag state this.#flowDragSource = null; this.#flowDragTarget = null; this.#shadow.querySelectorAll(".flow-drag-source, .flow-drag-target").forEach(el => { el.classList.remove("flow-drag-source", "flow-drag-target"); }); }); } // Stack view flow clicks — select or delete this.#shadow.querySelectorAll(".stack-flow").forEach(g => { g.addEventListener("click", (e) => { e.stopPropagation(); const flowId = g.dataset.flowId!; this.dispatchEvent(new CustomEvent("flow-select", { detail: { flowId }, bubbles: true, })); }); // Right-click to delete flow g.addEventListener("contextmenu", (e) => { e.preventDefault(); const flowId = g.dataset.flowId!; const flow = this.#flows.find(f => f.id === flowId); if (flow && confirm(`Remove ${FLOW_LABELS[flow.kind]} flow${flow.label ? `: ${flow.label}` : ""}?`)) { this.dispatchEvent(new CustomEvent("flow-remove", { detail: { flowId }, bubbles: true, })); } }); }); // Flow dialog events if (this.#flowDialogOpen) { let selectedKind: FlowKind = "data"; this.#shadow.querySelectorAll(".flow-kind-btn").forEach(btn => { btn.addEventListener("click", () => { this.#shadow.querySelectorAll(".flow-kind-btn").forEach(b => b.classList.remove("selected")); btn.classList.add("selected"); selectedKind = btn.dataset.kind as FlowKind; }); }); // Select "data" by default const defaultBtn = this.#shadow.querySelector('.flow-kind-btn[data-kind="data"]'); defaultBtn?.classList.add("selected"); this.#shadow.getElementById("flow-cancel")?.addEventListener("click", () => { this.#closeFlowDialog(); }); this.#shadow.getElementById("flow-create")?.addEventListener("click", () => { const label = (this.#shadow.getElementById("flow-label-input") as HTMLInputElement)?.value || ""; const strength = parseFloat((this.#shadow.getElementById("flow-strength-input") as HTMLInputElement)?.value || "0.5"); const flowId = `flow-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; this.dispatchEvent(new CustomEvent("flow-create", { detail: { flow: { id: flowId, kind: selectedKind, sourceLayerId: this.#flowDialogSourceId, targetLayerId: this.#flowDialogTargetId, label: label || undefined, strength, active: true, } }, bubbles: true, })); this.#closeFlowDialog(); }); } } static define(tag = "rstack-tab-bar") { if (!customElements.get(tag)) customElements.define(tag, RStackTabBar); } } // ── SVG Icons ── const ICON_STACK = ` `; const ICON_FLAT = ` `; // ── Styles ── const STYLES = ` :host { display: block; } /* ── Tab bar (flat mode) ── */ .tab-bar { display: flex; align-items: center; gap: 0; height: 36px; padding: 0 8px; overflow: hidden; } .tabs-scroll { display: flex; align-items: center; gap: 2px; flex: 1; min-width: 0; overflow-x: auto; scrollbar-width: none; position: relative; } .tabs-scroll::-webkit-scrollbar { display: none; } .tab-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; margin-left: 4px; } /* ── Individual tab ── */ .tab { display: flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 6px 6px 0 0; cursor: pointer; white-space: nowrap; font-size: 0.8rem; font-weight: 500; transition: background 0.15s, opacity 0.15s; user-select: none; position: relative; flex-shrink: 0; } :host-context([data-theme="dark"]) .tab { color: #94a3b8; background: transparent; } :host-context([data-theme="dark"]) .tab:hover { background: rgba(255,255,255,0.05); color: #e2e8f0; } :host-context([data-theme="dark"]) .tab.active { background: rgba(255,255,255,0.08); color: #f1f5f9; } :host-context([data-theme="light"]) .tab { color: #64748b; background: transparent; } :host-context([data-theme="light"]) .tab:hover { background: rgba(0,0,0,0.04); color: #1e293b; } :host-context([data-theme="light"]) .tab.active { background: rgba(0,0,0,0.06); color: #0f172a; } /* Active indicator line at bottom */ .tab-indicator { position: absolute; bottom: 0; left: 8px; right: 8px; height: 2px; border-radius: 2px 2px 0 0; opacity: 0; transition: opacity 0.15s; } .tab.active .tab-indicator { opacity: 1; } .tab-badge { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 4px; font-size: 0.55rem; font-weight: 900; color: #0f172a; line-height: 1; flex-shrink: 0; } .tab-label { font-size: 0.8rem; } .tab-close { display: flex; align-items: center; justify-content: center; width: 16px; height: 16px; border: none; border-radius: 3px; background: transparent; color: inherit; font-size: 0.85rem; cursor: pointer; opacity: 0; transition: opacity 0.15s, background 0.15s; padding: 0; line-height: 1; } .tab:hover .tab-close { opacity: 0.5; } .tab-close:hover { opacity: 1 !important; background: rgba(239,68,68,0.2); color: #ef4444; } /* ── Drag states ── */ .tab.dragging { opacity: 0.4; } .tab.drag-over { box-shadow: inset 2px 0 0 #22d3ee; } /* ── Add button ── */ .tab-add { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: 1px dashed rgba(148,163,184,0.3); border-radius: 5px; background: transparent; color: #64748b; font-size: 0.9rem; cursor: pointer; transition: border-color 0.15s, color 0.15s, background 0.15s; flex-shrink: 0; margin-left: 4px; } .tab-add:hover { border-color: #22d3ee; color: #22d3ee; background: rgba(34,211,238,0.08); } /* ── Add menu ── */ .add-menu { position: absolute; top: 100%; left: auto; right: 0; margin-top: 4px; min-width: 180px; max-height: 300px; overflow-y: auto; border-radius: 8px; padding: 4px; z-index: 100; box-shadow: 0 8px 30px rgba(0,0,0,0.25); } :host-context([data-theme="dark"]) .add-menu { background: #1e293b; border: 1px solid rgba(255,255,255,0.1); } :host-context([data-theme="light"]) .add-menu { background: white; border: 1px solid rgba(0,0,0,0.1); } .add-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 10px; border: none; border-radius: 5px; background: transparent; color: inherit; font-size: 0.8rem; cursor: pointer; text-align: left; transition: background 0.12s; } :host-context([data-theme="dark"]) .add-menu-item:hover { background: rgba(255,255,255,0.06); } :host-context([data-theme="light"]) .add-menu-item:hover { background: rgba(0,0,0,0.04); } .add-menu-badge { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 5px; font-size: 0.55rem; font-weight: 900; color: #0f172a; flex-shrink: 0; } .add-menu-empty { padding: 12px; text-align: center; font-size: 0.75rem; opacity: 0.5; } /* ── View toggle ── */ .view-toggle { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: none; border-radius: 6px; background: transparent; color: #64748b; cursor: pointer; transition: background 0.15s, color 0.15s; } .view-toggle:hover { background: rgba(255,255,255,0.08); color: #e2e8f0; } :host-context([data-theme="light"]) .view-toggle:hover { background: rgba(0,0,0,0.05); color: #1e293b; } .view-toggle.active { color: #22d3ee; background: rgba(34,211,238,0.1); } /* ── Stack view ── */ .stack-view { padding: 12px; overflow: auto; max-height: 50vh; transition: max-height 0.3s ease; } :host-context([data-theme="dark"]) .stack-view { background: rgba(15,23,42,0.5); border-bottom: 1px solid rgba(255,255,255,0.06); } :host-context([data-theme="light"]) .stack-view { background: rgba(248,250,252,0.8); border-bottom: 1px solid rgba(0,0,0,0.06); } .stack-view svg { display: block; margin: 0 auto; } .stack-layer { cursor: pointer; } .stack-layer:hover rect { stroke-width: 2.5; } .stack-layer--active rect { stroke-dasharray: none; } /* Drag-to-connect visual states */ .stack-layer.flow-drag-source rect { stroke-dasharray: 4 2; stroke-width: 2.5; } .stack-layer.flow-drag-target rect { stroke-width: 3; filter: brightness(1.3); } .stack-flow { cursor: pointer; } .stack-flow:hover path { stroke-width: 4 !important; opacity: 1 !important; } /* ── Flow creation dialog ── */ .flow-dialog { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 280px; border-radius: 12px; padding: 16px; z-index: 50; box-shadow: 0 12px 40px rgba(0,0,0,0.4); } :host-context([data-theme="dark"]) .flow-dialog { background: #1e293b; border: 1px solid rgba(255,255,255,0.1); color: #e2e8f0; } :host-context([data-theme="light"]) .flow-dialog { background: white; border: 1px solid rgba(0,0,0,0.1); color: #1e293b; } .flow-dialog-header { font-size: 0.85rem; font-weight: 700; margin-bottom: 10px; } .flow-dialog-route { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; justify-content: center; } .flow-dialog-badge { display: inline-flex; align-items: center; justify-content: center; padding: 3px 8px; border-radius: 5px; font-size: 0.6rem; font-weight: 900; color: #0f172a; } .flow-dialog-arrow { font-size: 1rem; opacity: 0.4; } .flow-dialog-field { margin-bottom: 10px; } .flow-dialog-label { display: block; font-size: 0.6rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin-bottom: 4px; } .flow-kind-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; } .flow-kind-btn { display: flex; align-items: center; gap: 6px; padding: 5px 8px; border: 1px solid transparent; border-radius: 5px; background: transparent; color: inherit; font-size: 0.7rem; cursor: pointer; transition: background 0.12s, border-color 0.12s; } .flow-kind-btn:hover { background: rgba(255,255,255,0.05); } .flow-kind-btn.selected { border-color: var(--kind-color); background: color-mix(in srgb, var(--kind-color) 10%, transparent); } .flow-kind-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } .flow-dialog-input { width: 100%; padding: 6px 8px; border: 1px solid rgba(255,255,255,0.1); border-radius: 5px; background: rgba(255,255,255,0.04); color: inherit; font-size: 0.75rem; outline: none; } .flow-dialog-input:focus { border-color: rgba(34,211,238,0.4); } :host-context([data-theme="light"]) .flow-dialog-input { border-color: rgba(0,0,0,0.1); background: rgba(0,0,0,0.02); } .flow-dialog-range { width: 100%; accent-color: #22d3ee; } .flow-dialog-actions { display: flex; gap: 6px; justify-content: flex-end; margin-top: 4px; } .flow-dialog-cancel, .flow-dialog-create { padding: 5px 14px; border-radius: 6px; font-size: 0.7rem; font-weight: 600; cursor: pointer; } .flow-dialog-cancel { border: 1px solid rgba(255,255,255,0.1); background: transparent; color: inherit; opacity: 0.6; } .flow-dialog-cancel:hover { opacity: 1; } .flow-dialog-create { border: none; background: #22d3ee; color: #0f172a; } .flow-dialog-create:hover { opacity: 0.85; } /* ── Responsive ── */ @media (max-width: 640px) { .tab-label { display: none; } .tab { padding: 4px 8px; } .stack-view { max-height: 40vh; } .flow-dialog { width: 240px; } } `;