| 1 | // Mood check-in: the COMMAND-PANEL example for member apps. `/mood [note]` |
| 2 | // opens a panel of feeling buttons; tapping one dispatches a `mood.set` event |
| 3 | // into your Mood thread (an action line in its chat, a row in its history) |
| 4 | // and dismisses the panel. This is the whole "panel buttons dispatch events" |
| 5 | // shape in ~80 lines, written against the DECLARATIVE panel form โ the |
| 6 | // command calls ctx.panel({ title, data }) and the top-level |
| 7 | // panel(data, el, panelCtx) hook draws it โ which is the one form the |
| 8 | // member-app sandbox supports (a render closure can't cross its bridge), and |
| 9 | // which works identically compiled-in. To try it as a member app: paste this |
| 10 | // file into App Studio and deploy. Sandbox-legal by construction: imports |
| 11 | // only from ../apps.js, no fetch, no host doorways. |
| 12 | import { defineApp, addItem } from "../apps.js"; |
| 13 | |
| 14 | const MOODS = ["๐", "๐", "๐", "๐ฎโ๐จ", "๐"]; |
| 15 | |
| 16 | export const app = defineApp({ |
| 17 | name: "mood", |
| 18 | title: "Mood", |
| 19 | glyph: "๐ค๏ธ", |
| 20 | initialState: () => ({ items: [] }), |
| 21 | |
| 22 | events: { |
| 23 | "mood.set": (s, e) => |
| 24 | e.id && typeof e.mood === "string" && MOODS.includes(e.mood) |
| 25 | ? { 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 }) } |
| 26 | : s, |
| 27 | }, |
| 28 | |
| 29 | commands: { |
| 30 | "/mood": { |
| 31 | desc: "log how you're feeling", |
| 32 | args: "[note]", |
| 33 | run(args, ctx) { |
| 34 | ctx.panel({ title: "How are you feeling?", data: { note: (args || "").trim() } }); |
| 35 | }, |
| 36 | }, |
| 37 | }, |
| 38 | |
| 39 | // Companion projection: a check-in reads as an action line in the chat view. |
| 40 | chatMessage(e) { |
| 41 | if (e.type !== "mood.set") return null; |
| 42 | return { text: `is feeling ${e.mood}${e.note ? " โ " + e.note : ""}`, action: true }; |
| 43 | }, |
| 44 | |
| 45 | // The panel: five buttons; a tap emits the event and closes. `data` is what |
| 46 | // the command passed (the note typed after /mood); panelCtx offers |
| 47 | // { emit, id, me, getState, close, insertText } in both worlds. |
| 48 | panel(data, el, ctx) { |
| 49 | const row = document.createElement("div"); |
| 50 | row.className = "mood-row"; |
| 51 | for (const mood of MOODS) { |
| 52 | const b = document.createElement("button"); |
| 53 | b.type = "button"; |
| 54 | b.className = "mood-btn"; |
| 55 | b.textContent = mood; |
| 56 | b.onclick = async () => { |
| 57 | await ctx.emit({ type: "mood.set", id: ctx.id(), mood, note: (data && data.note) || "" }); |
| 58 | ctx.close(); |
| 59 | }; |
| 60 | row.appendChild(b); |
| 61 | } |
| 62 | el.appendChild(row); |
| 63 | }, |
| 64 | |
| 65 | styles: ` |
| 66 | .mood-row { display: flex; gap: 8px; padding: 6px 2px; } |
| 67 | .mood-btn { |
| 68 | border: none; background: var(--bubble-them); border-radius: 10px; |
| 69 | font-size: 26px; line-height: 1; padding: 10px 12px; cursor: pointer; |
| 70 | } |
| 71 | .mood-btn:hover { outline: 2px solid var(--accent); } |
| 72 | .mood-item { display: flex; gap: 10px; align-items: baseline; padding: 6px 2px; border-bottom: 1px solid var(--border); } |
| 73 | .mood-note { color: var(--muted); font-size: 13px; } |
| 74 | `, |
| 75 | |
| 76 | // The app-thread surface: newest first, the history of check-ins. |
| 77 | render(state, el) { |
| 78 | const items = [...((state && state.items) || [])].sort((a, b) => b.ts - a.ts); |
| 79 | for (const it of items) { |
| 80 | const row = document.createElement("div"); |
| 81 | row.className = "mood-item"; |
| 82 | const glyph = document.createElement("span"); |
| 83 | glyph.textContent = it.mood; |
| 84 | const note = document.createElement("span"); |
| 85 | note.className = "mood-note"; |
| 86 | note.textContent = it.note || new Date(it.ts).toLocaleString(); |
| 87 | row.append(glyph, note); |
| 88 | el.appendChild(row); |
| 89 | } |
| 90 | }, |
| 91 | }); |
| 92 | |