From 0813eed5e03fdbdd467427826dee366716a4a84a Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 25 Feb 2026 15:09:41 -0800 Subject: [PATCH] feat: consistent headers across all rApps + add mi AI assistant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Header consistency: - Fix 52px → 56px header height in 7 module CSS files (pubs, funds, providers, books, swag, choices, cart) - Remove custom header background overrides in books.css and pubs.css - All pages now use the same 3-section header layout: left (app/space switchers), center (mi), right (identity) - Add to all 4 standalone HTML pages (index, admin, create-space, canvas) and both shell renderers mi AI assistant: - New web component with search input "Ask mi anything..." - Dropdown panel with streaming chat UI, typing indicator, markdown formatting - POST /api/mi/ask endpoint: streams from Ollama with full rApp context in system prompt (all 22 modules, current space/module) - Graceful fallback to keyword-based responses when Ollama unavailable - Configurable via MI_MODEL and OLLAMA_URL env vars Co-Authored-By: Claude Opus 4.6 --- modules/books/components/books.css | 7 +- modules/cart/components/cart.css | 2 +- modules/choices/components/choices.css | 2 +- modules/funds/components/funds.css | 2 +- modules/providers/components/providers.css | 2 +- modules/pubs/components/pubs.css | 7 +- modules/swag/components/swag.css | 2 +- server/index.ts | 92 +++++++ server/shell.ts | 92 +++++++ shared/components/rstack-mi.ts | 296 +++++++++++++++++++++ website/admin.html | 3 + website/canvas.html | 5 +- website/create-space.html | 3 + website/index.html | 3 + website/public/shell.css | 33 ++- website/shell.ts | 4 + 16 files changed, 535 insertions(+), 20 deletions(-) create mode 100644 shared/components/rstack-mi.ts diff --git a/modules/books/components/books.css b/modules/books/components/books.css index 5d7f6ed..2d32fa2 100644 --- a/modules/books/components/books.css +++ b/modules/books/components/books.css @@ -5,13 +5,8 @@ body[data-theme="dark"] { background: #0f172a; } -body[data-theme="dark"] .rstack-header { - background: #0f172a; - border-bottom-color: #1e293b; -} - /* Library grid page */ body[data-theme="light"] main { background: #0f172a; - min-height: calc(100vh - 52px); + min-height: calc(100vh - 56px); } diff --git a/modules/cart/components/cart.css b/modules/cart/components/cart.css index 8808749..4f62606 100644 --- a/modules/cart/components/cart.css +++ b/modules/cart/components/cart.css @@ -1,6 +1,6 @@ /* Cart module theme */ body[data-theme="light"] main { background: #0f172a; - min-height: calc(100vh - 52px); + min-height: calc(100vh - 56px); padding: 0; } diff --git a/modules/choices/components/choices.css b/modules/choices/components/choices.css index 8d131b7..25ea14f 100644 --- a/modules/choices/components/choices.css +++ b/modules/choices/components/choices.css @@ -1,6 +1,6 @@ /* Choices module theme */ body[data-theme="light"] main { background: #0f172a; - min-height: calc(100vh - 52px); + min-height: calc(100vh - 56px); padding: 0; } diff --git a/modules/funds/components/funds.css b/modules/funds/components/funds.css index 670d7fc..78d02aa 100644 --- a/modules/funds/components/funds.css +++ b/modules/funds/components/funds.css @@ -1,7 +1,7 @@ /* ── Funds module theme ───────────────────────────────── */ body[data-theme="light"] main { background: #0f172a; - min-height: calc(100vh - 52px); + min-height: calc(100vh - 56px); padding: 0; } diff --git a/modules/providers/components/providers.css b/modules/providers/components/providers.css index cfa1c04..49892d2 100644 --- a/modules/providers/components/providers.css +++ b/modules/providers/components/providers.css @@ -1,6 +1,6 @@ /* Providers module theme */ body[data-theme="light"] main { background: #0f172a; - min-height: calc(100vh - 52px); + min-height: calc(100vh - 56px); padding: 0; } diff --git a/modules/pubs/components/pubs.css b/modules/pubs/components/pubs.css index cab4176..0ad6231 100644 --- a/modules/pubs/components/pubs.css +++ b/modules/pubs/components/pubs.css @@ -1,11 +1,6 @@ /* Pubs module — editor theme */ body[data-theme="light"] main { background: #0f172a; - min-height: calc(100vh - 52px); + min-height: calc(100vh - 56px); padding: 0; } - -body[data-theme="light"] .rstack-header { - background: #0f172a; - border-bottom-color: #1e293b; -} diff --git a/modules/swag/components/swag.css b/modules/swag/components/swag.css index 3200438..8786ac1 100644 --- a/modules/swag/components/swag.css +++ b/modules/swag/components/swag.css @@ -1,6 +1,6 @@ /* Swag module theme */ body[data-theme="light"] main { background: #0f172a; - min-height: calc(100vh - 52px); + min-height: calc(100vh - 56px); padding: 0; } diff --git a/server/index.ts b/server/index.ts index 1374215..d15b785 100644 --- a/server/index.ts +++ b/server/index.ts @@ -125,6 +125,98 @@ app.get("/.well-known/webauthn", (c) => { // ── Space registry API ── app.route("/api/spaces", spaces); +// ── mi — AI assistant endpoint ── +const MI_MODEL = process.env.MI_MODEL || "llama3.2"; +const OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434"; + +app.post("/api/mi/ask", async (c) => { + const { query, messages = [], space, module: currentModule } = await c.req.json(); + if (!query) return c.json({ error: "Query required" }, 400); + + // Build rApp context for the system prompt + const moduleList = getModuleInfoList() + .map((m) => `- **${m.name}** (${m.id}): ${m.icon} ${m.description}`) + .join("\n"); + + const systemPrompt = `You are mi, the intelligent assistant for rSpace — a self-hosted, community-run platform. +You help users navigate, understand, and get the most out of the platform's apps (rApps). + +## Available rApps +${moduleList} + +## Current Context +- Space: ${space || "none selected"} +- Active rApp: ${currentModule || "none"} + +## Guidelines +- Be concise and helpful. Keep responses short (2-4 sentences) unless the user asks for detail. +- When suggesting actions, reference specific rApps by name and explain how they connect. +- You can suggest navigating to /:space/:moduleId paths. +- If you don't know something specific about the user's data, say so honestly. +- Use a warm, knowledgeable tone. You're a guide, not a search engine.`; + + // Build conversation for Ollama + const ollamaMessages = [ + { role: "system", content: systemPrompt }, + ...messages.slice(-8).map((m: any) => ({ role: m.role, content: m.content })), + { role: "user", content: query }, + ]; + + try { + const ollamaRes = await fetch(`${OLLAMA_URL}/api/chat`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ model: MI_MODEL, messages: ollamaMessages, stream: true }), + }); + + if (!ollamaRes.ok) { + const errText = await ollamaRes.text().catch(() => ""); + console.error("mi: Ollama error:", ollamaRes.status, errText); + return c.json({ error: "AI service unavailable" }, 502); + } + + // Stream Ollama's NDJSON response directly to client + return new Response(ollamaRes.body, { + headers: { + "Content-Type": "application/x-ndjson", + "Cache-Control": "no-cache", + "Transfer-Encoding": "chunked", + }, + }); + } catch (e: any) { + console.error("mi: Failed to reach Ollama:", e.message); + // Fallback: return a static helpful response + const fallback = generateFallbackResponse(query, currentModule, space, getModuleInfoList()); + return c.json({ response: fallback }); + } +}); + +function generateFallbackResponse( + query: string, + currentModule: string, + space: string, + modules: ReturnType, +): string { + const q = query.toLowerCase(); + + // Simple keyword matching for common questions + for (const m of modules) { + if (q.includes(m.id) || q.includes(m.name.toLowerCase())) { + return `**${m.name}** ${m.icon} — ${m.description}. You can access it at /${space || "personal"}/${m.id}.`; + } + } + + if (q.includes("help") || q.includes("what can")) { + return `rSpace has ${modules.length} apps you can use. Some popular ones: **rSpace** (canvas), **rNotes** (notes), **rChat** (messaging), **rFunds** (community funding), and **rVote** (governance). What would you like to explore?`; + } + + if (q.includes("search") || q.includes("find")) { + return `You can browse your content through the app switcher (top-left dropdown), or navigate directly to any rApp. Try **rNotes** for text content, **rFiles** for documents, or **rPhotos** for images.`; + } + + return `I'm currently running in offline mode (AI service not connected). I can still help with basic navigation — ask me about any specific rApp or feature! There are ${modules.length} apps available in rSpace.`; +} + // ── Existing /api/communities/* routes (backward compatible) ── /** Resolve a community slug to SpaceAuthConfig for the SDK guard */ diff --git a/server/shell.ts b/server/shell.ts index 51fdc0b..40ba095 100644 --- a/server/shell.ts +++ b/server/shell.ts @@ -66,10 +66,16 @@ export function renderShell(opts: ShellOptions): string { +
+ +
+
+ +
${body}
@@ -77,6 +83,89 @@ export function renderShell(opts: ShellOptions): string { import '/shell.js'; // Provide module list to app switcher document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON}); + + // ── Tab bar / Layer system initialization ── + const tabBar = document.querySelector('rstack-tab-bar'); + const spaceSlug = '${escapeAttr(spaceSlug)}'; + const currentModuleId = '${escapeAttr(moduleId)}'; + + if (tabBar) { + // Default layer: current module (bootstrap if no layers saved yet) + const defaultLayer = { + id: 'layer-' + currentModuleId, + moduleId: currentModuleId, + label: ${JSON.stringify(modules.find((m: any) => m.id === moduleId)?.name || moduleId)}, + order: 0, + color: '', + visible: true, + createdAt: Date.now(), + }; + + // Set the current module as the active layer + tabBar.setLayers([defaultLayer]); + tabBar.setAttribute('active', defaultLayer.id); + + // Listen for tab events + tabBar.addEventListener('layer-switch', (e) => { + const { moduleId } = e.detail; + window.location.href = '/' + spaceSlug + '/' + moduleId; + }); + + tabBar.addEventListener('layer-add', (e) => { + const { moduleId } = e.detail; + // Navigate to the new module (layer will be persisted when sync connects) + window.location.href = '/' + spaceSlug + '/' + moduleId; + }); + + tabBar.addEventListener('layer-close', (e) => { + const { layerId } = e.detail; + tabBar.removeLayer(layerId); + // If we closed the active layer, switch to first remaining + const remaining = tabBar.querySelectorAll?.('[data-layer-id]'); + // The tab bar handles this internally + }); + + tabBar.addEventListener('view-toggle', (e) => { + const { mode } = e.detail; + // When switching to stack view, emit event for canvas to connect + document.dispatchEvent(new CustomEvent('layer-view-mode', { detail: { mode } })); + }); + + // Expose tabBar for CommunitySync integration + window.__rspaceTabBar = tabBar; + + // If CommunitySync is available, wire up layer persistence + document.addEventListener('community-sync-ready', (e) => { + const sync = e.detail?.sync; + if (!sync) return; + + // Load persisted layers + const layers = sync.getLayers(); + if (layers.length > 0) { + tabBar.setLayers(layers); + const activeId = sync.doc.activeLayerId; + if (activeId) tabBar.setAttribute('active', activeId); + tabBar.setFlows(sync.getFlows()); + } else { + // First visit: save the default layer + sync.addLayer(defaultLayer); + sync.setActiveLayer(defaultLayer.id); + } + + // Sync layer changes back to Automerge + tabBar.addEventListener('layer-switch', (e) => { + sync.setActiveLayer(e.detail.layerId); + }); + + // Listen for remote layer changes + sync.addEventListener('change', () => { + tabBar.setLayers(sync.getLayers()); + tabBar.setFlows(sync.getFlows()); + const activeId = sync.doc.activeLayerId; + if (activeId) tabBar.setAttribute('active', activeId); + }); + }); + } ${scripts} @@ -111,6 +200,9 @@ export function renderStandaloneShell(opts: { rSpace +
+ +
diff --git a/shared/components/rstack-mi.ts b/shared/components/rstack-mi.ts new file mode 100644 index 0000000..f20d037 --- /dev/null +++ b/shared/components/rstack-mi.ts @@ -0,0 +1,296 @@ +/** + * — AI-powered assistant embedded in the rSpace header. + * + * Renders a search input ("Ask mi anything..."). On submit, queries + * /api/mi/ask and streams the response into a dropdown panel. + * Supports multi-turn conversation with context. + */ + +import { getAccessToken } from "./rstack-identity"; + +interface MiMessage { + role: "user" | "assistant"; + content: string; +} + +export class RStackMi extends HTMLElement { + #shadow: ShadowRoot; + #messages: MiMessage[] = []; + #abortController: AbortController | null = null; + + constructor() { + super(); + this.#shadow = this.attachShadow({ mode: "open" }); + } + + connectedCallback() { + this.#render(); + } + + #render() { + this.#shadow.innerHTML = ` + +
+
+ + +
+
+
+
+ +

Hi, I'm mi — your guide to rSpace.

+

Ask me about any rApp, how to find things, or what you can do here.

+
+
+
+
+ `; + + const input = this.#shadow.getElementById("mi-input") as HTMLInputElement; + const panel = this.#shadow.getElementById("mi-panel")!; + const bar = this.#shadow.getElementById("mi-bar")!; + + input.addEventListener("focus", () => { + panel.classList.add("open"); + bar.classList.add("focused"); + }); + + input.addEventListener("keydown", (e) => { + if (e.key === "Enter" && input.value.trim()) { + this.#ask(input.value.trim()); + input.value = ""; + } + if (e.key === "Escape") { + panel.classList.remove("open"); + bar.classList.remove("focused"); + input.blur(); + } + }); + + document.addEventListener("click", (e) => { + if (!this.contains(e.target as Node)) { + panel.classList.remove("open"); + bar.classList.remove("focused"); + } + }); + + // Stop clicks inside the panel from closing it + panel.addEventListener("click", (e) => e.stopPropagation()); + bar.addEventListener("click", (e) => e.stopPropagation()); + } + + async #ask(query: string) { + const panel = this.#shadow.getElementById("mi-panel")!; + const messagesEl = this.#shadow.getElementById("mi-messages")!; + + panel.classList.add("open"); + this.#shadow.getElementById("mi-bar")!.classList.add("focused"); + + // Add user message + this.#messages.push({ role: "user", content: query }); + this.#renderMessages(messagesEl); + + // Add placeholder for assistant + this.#messages.push({ role: "assistant", content: "" }); + const assistantIdx = this.#messages.length - 1; + this.#renderMessages(messagesEl); + + // Abort previous + this.#abortController?.abort(); + this.#abortController = new AbortController(); + + try { + const token = getAccessToken(); + const res = await fetch("/api/mi/ask", { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ + query, + messages: this.#messages.slice(0, -1).slice(-10), + space: document.querySelector("rstack-space-switcher")?.getAttribute("current") || "", + module: document.querySelector("rstack-app-switcher")?.getAttribute("current") || "", + }), + signal: this.#abortController.signal, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ error: "Request failed" })); + throw new Error(err.error || "Request failed"); + } + + if (!res.body) throw new Error("No response stream"); + + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + for (const line of chunk.split("\n").filter(Boolean)) { + try { + const data = JSON.parse(line); + if (data.message?.content) { + this.#messages[assistantIdx].content += data.message.content; + } + // Non-streaming fallback + if (data.response) { + this.#messages[assistantIdx].content = data.response; + } + } catch { /* skip malformed lines */ } + } + this.#renderMessages(messagesEl); + } + + // If still empty after stream, show fallback + if (!this.#messages[assistantIdx].content) { + this.#messages[assistantIdx].content = "I couldn't generate a response. Please try again."; + this.#renderMessages(messagesEl); + } + } catch (e: any) { + if (e.name !== "AbortError") { + this.#messages[this.#messages.length - 1].content = + "Sorry, I'm not available right now. Please try again later."; + this.#renderMessages(messagesEl); + } + } + } + + #renderMessages(container: HTMLElement) { + container.innerHTML = this.#messages + .map( + (m) => ` +
+ ${m.role === "user" ? "You" : "✧ mi"} +
${m.content ? this.#formatContent(m.content) : ''}
+
+ `, + ) + .join(""); + container.scrollTop = container.scrollHeight; + } + + #formatContent(s: string): string { + // Escape HTML then convert markdown-like formatting + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/\*\*(.+?)\*\*/g, "$1") + .replace(/`(.+?)`/g, '$1') + .replace(/\n/g, "
"); + } + + static define(tag = "rstack-mi") { + if (!customElements.get(tag)) customElements.define(tag, RStackMi); + } +} + +const STYLES = ` +:host { display: contents; } + +.mi { position: relative; flex: 1; max-width: 480px; min-width: 0; } + +.mi-bar { + display: flex; align-items: center; gap: 8px; + padding: 6px 14px; border-radius: 10px; + transition: all 0.2s; +} +:host-context([data-theme="dark"]) .mi-bar { background: rgba(255,255,255,0.06); } +:host-context([data-theme="light"]) .mi-bar { background: rgba(0,0,0,0.04); } +:host-context([data-theme="dark"]) .mi-bar.focused { background: rgba(255,255,255,0.1); box-shadow: 0 0 0 1px rgba(6,182,212,0.3); } +:host-context([data-theme="light"]) .mi-bar.focused { background: rgba(0,0,0,0.06); box-shadow: 0 0 0 1px rgba(6,182,212,0.3); } + +.mi-icon { + font-size: 0.9rem; flex-shrink: 0; + background: linear-gradient(135deg, #06b6d4, #7c3aed); + -webkit-background-clip: text; -webkit-text-fill-color: transparent; +} + +.mi-input { + flex: 1; border: none; outline: none; background: none; + font-size: 0.85rem; min-width: 0; + font-family: inherit; +} +:host-context([data-theme="dark"]) .mi-input { color: #e2e8f0; } +:host-context([data-theme="light"]) .mi-input { color: #0f172a; } +:host-context([data-theme="dark"]) .mi-input::placeholder { color: #64748b; } +:host-context([data-theme="light"]) .mi-input::placeholder { color: #94a3b8; } + +.mi-panel { + position: absolute; top: calc(100% + 8px); left: 0; right: 0; + min-width: 360px; max-height: 420px; + border-radius: 14px; overflow: hidden; + box-shadow: 0 12px 40px rgba(0,0,0,0.3); + display: none; z-index: 300; +} +.mi-panel.open { display: flex; flex-direction: column; } +:host-context([data-theme="dark"]) .mi-panel { background: #1e293b; border: 1px solid rgba(255,255,255,0.1); } +:host-context([data-theme="light"]) .mi-panel { background: white; border: 1px solid rgba(0,0,0,0.1); } + +.mi-messages { + flex: 1; overflow-y: auto; padding: 16px; + display: flex; flex-direction: column; gap: 12px; + max-height: 400px; +} + +.mi-welcome { text-align: center; padding: 24px 16px; } +.mi-welcome-icon { + font-size: 2rem; display: block; margin-bottom: 8px; + background: linear-gradient(135deg, #06b6d4, #7c3aed); + -webkit-background-clip: text; -webkit-text-fill-color: transparent; +} +:host-context([data-theme="dark"]) .mi-welcome p { color: #e2e8f0; } +:host-context([data-theme="light"]) .mi-welcome p { color: #374151; } +.mi-welcome p { font-size: 0.9rem; line-height: 1.5; margin: 0; } +.mi-welcome-sub { font-size: 0.8rem; opacity: 0.6; margin-top: 6px !important; } + +.mi-msg { display: flex; flex-direction: column; gap: 4px; } +.mi-msg-who { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; } +:host-context([data-theme="dark"]) .mi-msg--user .mi-msg-who { color: #06b6d4; } +:host-context([data-theme="light"]) .mi-msg--user .mi-msg-who { color: #0891b2; } +.mi-msg--assistant .mi-msg-who { + background: linear-gradient(135deg, #06b6d4, #7c3aed); + -webkit-background-clip: text; -webkit-text-fill-color: transparent; +} + +.mi-msg-body { + font-size: 0.85rem; line-height: 1.6; word-break: break-word; +} +:host-context([data-theme="dark"]) .mi-msg-body { color: #cbd5e1; } +:host-context([data-theme="light"]) .mi-msg-body { color: #374151; } +:host-context([data-theme="dark"]) .mi-msg--user .mi-msg-body { color: #e2e8f0; } +:host-context([data-theme="light"]) .mi-msg--user .mi-msg-body { color: #0f172a; } + +.mi-code { + padding: 1px 5px; border-radius: 4px; font-size: 0.8rem; + font-family: 'SF Mono', Monaco, Consolas, monospace; +} +:host-context([data-theme="dark"]) .mi-code { background: rgba(255,255,255,0.08); color: #7dd3fc; } +:host-context([data-theme="light"]) .mi-code { background: rgba(0,0,0,0.06); color: #0284c7; } + +.mi-typing { display: inline-flex; gap: 4px; padding: 4px 0; } +.mi-typing span { + width: 6px; height: 6px; border-radius: 50%; + animation: miBounce 1.2s ease-in-out infinite; +} +:host-context([data-theme="dark"]) .mi-typing span { background: #64748b; } +:host-context([data-theme="light"]) .mi-typing span { background: #94a3b8; } +.mi-typing span:nth-child(2) { animation-delay: 0.15s; } +.mi-typing span:nth-child(3) { animation-delay: 0.3s; } +@keyframes miBounce { + 0%, 60%, 100% { transform: translateY(0); } + 30% { transform: translateY(-4px); } +} + +@media (max-width: 640px) { + .mi { max-width: 200px; } + .mi-panel { min-width: 300px; left: -60px; } +} +`; diff --git a/website/admin.html b/website/admin.html index bd6ee66..248fe3b 100644 --- a/website/admin.html +++ b/website/admin.html @@ -366,6 +366,9 @@ +
+ +
diff --git a/website/canvas.html b/website/canvas.html index 3be790c..5982210 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -22,7 +22,7 @@ #toolbar { position: fixed; - top: 72px; + top: 108px; /* header(56) + tab-row(36) + gap(16) */ left: 50%; transform: translateX(-50%); display: flex; @@ -570,6 +570,9 @@ +
+ +
diff --git a/website/create-space.html b/website/create-space.html index e1d7221..d30c441 100644 --- a/website/create-space.html +++ b/website/create-space.html @@ -170,6 +170,9 @@ +
+ +
diff --git a/website/index.html b/website/index.html index 595f8b7..24d389d 100644 --- a/website/index.html +++ b/website/index.html @@ -365,6 +365,9 @@ +
+ +
diff --git a/website/public/shell.css b/website/public/shell.css index 7772dbb..5416bdc 100644 --- a/website/public/shell.css +++ b/website/public/shell.css @@ -41,6 +41,14 @@ body { gap: 4px; } +.rstack-header__center { + flex: 1; + display: flex; + justify-content: center; + padding: 0 16px; + min-width: 0; +} + .rstack-header__right { display: flex; align-items: center; @@ -63,16 +71,37 @@ body { -webkit-text-fill-color: transparent; } +/* ── Tab row (below header) ── */ + +.rstack-tab-row { + position: fixed; + top: 56px; + left: 0; + right: 0; + z-index: 9998; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid rgba(128,128,128,0.1); +} + +.rstack-tab-row[data-theme="dark"] { + background: rgba(15, 23, 42, 0.8); +} + +.rstack-tab-row[data-theme="light"] { + background: rgba(255, 255, 255, 0.85); +} + /* ── Main content area ── */ #app { - padding-top: 56px; /* Below fixed header */ + padding-top: 92px; /* Below fixed header (56px) + tab row (36px) */ min-height: 100vh; } /* When canvas module is active, make it fill the viewport */ #app.canvas-layout { - padding-top: 56px; + padding-top: 92px; height: 100vh; overflow: hidden; } diff --git a/website/shell.ts b/website/shell.ts index 610e38d..d071159 100644 --- a/website/shell.ts +++ b/website/shell.ts @@ -10,11 +10,15 @@ import { RStackIdentity } from "../shared/components/rstack-identity"; import { RStackAppSwitcher } from "../shared/components/rstack-app-switcher"; import { RStackSpaceSwitcher } from "../shared/components/rstack-space-switcher"; +import { RStackTabBar } from "../shared/components/rstack-tab-bar"; +import { RStackMi } from "../shared/components/rstack-mi"; // Register all header components RStackIdentity.define(); RStackAppSwitcher.define(); RStackSpaceSwitcher.define(); +RStackTabBar.define(); +RStackMi.define(); // Reload space list when user signs in/out (to show/hide private spaces) document.addEventListener("auth-change", () => {