// Mood check-in: the COMMAND-PANEL example for member apps. `/mood [note]` // opens a panel of feeling buttons; tapping one dispatches a `mood.set` event // into your Mood thread (an action line in its chat, a row in its history) // and dismisses the panel. This is the whole "panel buttons dispatch events" // shape in ~80 lines, written against the DECLARATIVE panel form — the // command calls ctx.panel({ title, data }) and the top-level // panel(data, el, panelCtx) hook draws it — which is the one form the // member-app sandbox supports (a render closure can't cross its bridge), and // which works identically compiled-in. To try it as a member app: paste this // file into App Studio and deploy. Sandbox-legal by construction: imports // only from ../apps.js, no fetch, no host doorways. import { defineApp, addItem } from "../apps.js"; const MOODS = ["😄", "🙂", "😐", "😮‍💨", "😖"]; export const app = defineApp({ name: "mood", title: "Mood", glyph: "🌤️", initialState: () => ({ items: [] }), events: { "mood.set": (s, e) => e.id && typeof e.mood === "string" && MOODS.includes(e.mood) ? { items: addItem(s.items, { id: e.id, mood: e.mood, note: typeof e.note === "string" ? e.note.slice(0, 200) : "", by: e.name || "", ts: e.ts || 0 }) } : s, }, commands: { "/mood": { desc: "log how you're feeling", args: "[note]", run(args, ctx) { ctx.panel({ title: "How are you feeling?", data: { note: (args || "").trim() } }); }, }, }, // Companion projection: a check-in reads as an action line in the chat view. chatMessage(e) { if (e.type !== "mood.set") return null; return { text: `is feeling ${e.mood}${e.note ? " — " + e.note : ""}`, action: true }; }, // The panel: five buttons; a tap emits the event and closes. `data` is what // the command passed (the note typed after /mood); panelCtx offers // { emit, id, me, getState, close, insertText } in both worlds. panel(data, el, ctx) { const row = document.createElement("div"); row.className = "mood-row"; for (const mood of MOODS) { const b = document.createElement("button"); b.type = "button"; b.className = "mood-btn"; b.textContent = mood; b.onclick = async () => { await ctx.emit({ type: "mood.set", id: ctx.id(), mood, note: (data && data.note) || "" }); ctx.close(); }; row.appendChild(b); } el.appendChild(row); }, styles: ` .mood-row { display: flex; gap: 8px; padding: 6px 2px; } .mood-btn { border: none; background: var(--bubble-them); border-radius: 10px; font-size: 26px; line-height: 1; padding: 10px 12px; cursor: pointer; } .mood-btn:hover { outline: 2px solid var(--accent); } .mood-item { display: flex; gap: 10px; align-items: baseline; padding: 6px 2px; border-bottom: 1px solid var(--border); } .mood-note { color: var(--muted); font-size: 13px; } `, // The app-thread surface: newest first, the history of check-ins. render(state, el) { const items = [...((state && state.items) || [])].sort((a, b) => b.ts - a.ts); for (const it of items) { const row = document.createElement("div"); row.className = "mood-item"; const glyph = document.createElement("span"); glyph.textContent = it.mood; const note = document.createElement("span"); note.className = "mood-note"; note.textContent = it.note || new Date(it.ts).toLocaleString(); row.append(glyph, note); el.appendChild(row); } }, });