Changeset 0.37.15 (#227)

This commit is contained in:
2026-03-23 19:58:17 +00:00
parent b7746c3004
commit d005e8a30f
23 changed files with 1764 additions and 170 deletions

View File

@@ -0,0 +1,221 @@
/**
* Team Activity Log — Surface Entry Point
*
* Demonstrates:
* - SDK boot + sw.* API access
* - Preact rendering into #extension-mount
* - Calling Starlark-backed ext API routes (/s/:slug/api/*)
* - sw.toast, sw.auth, sw.on (WS event subscription)
* - sw.userMenu for shell integration
* - Platform CSS variables for theming
*/
(async function () {
'use strict';
var mount = document.getElementById('extension-mount');
if (!mount) return;
// ── Boot SDK (extension surfaces don't auto-load it) ──
var base = window.__BASE__ || '';
var ver = window.__VERSION__ || '0';
var slug = (window.__MANIFEST__ || {}).id || 'team-activity-log';
try {
if (!window.preact) {
var { h, render } = await import(base + '/js/sw/vendor/preact.module.js');
var hooksModule = await import(base + '/js/sw/vendor/hooks.module.js');
var htmModule = await import(base + '/js/sw/vendor/htm.module.js');
window.preact = { h, render };
window.hooks = hooksModule;
window.html = htmModule.default.bind(h);
}
var sdk = await import(base + '/js/sw/sdk/index.js?v=' + ver);
await sdk.boot();
} catch (e) {
mount.innerHTML = '<p style="color:var(--danger);padding:24px;">SDK boot failed: ' + e.message + '</p>';
return;
}
var { html } = window;
var { useState, useEffect, useCallback, useRef } = hooks;
var { render } = preact;
// ── API helper: call Starlark backend ──
var API_BASE = base + '/s/' + slug + '/api';
async function api(method, path, body) {
var token = sw.auth._getToken();
var opts = {
method: method,
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
};
if (body) opts.body = JSON.stringify(body);
var resp = await fetch(API_BASE + path, opts);
var text = await resp.text();
try { return JSON.parse(text); } catch (_) { return { _raw: text, _status: resp.status }; }
}
// ── Components ─────────────────────────────
var CATEGORIES = ['note', 'update', 'decision', 'action-item'];
function App() {
var [entries, setEntries] = useState([]);
var [loading, setLoading] = useState(true);
var [filter, setFilter] = useState('');
var menuRef = useRef(null);
var load = useCallback(async function () {
var path = '/entries' + (filter ? '?category=' + encodeURIComponent(filter) : '');
var d = await api('GET', path);
setEntries(d.data || []);
setLoading(false);
}, [filter]);
useEffect(function () { load(); }, [load]);
// Mount user menu
useEffect(function () {
if (menuRef.current && sw.userMenu) {
sw.userMenu(menuRef.current, { placement: 'down-right' });
}
}, []);
async function addEntry(category, message) {
var user = sw.auth.user;
var result = await api('POST', '/entries', {
category: category,
message: message,
username: user ? (user.display_name || user.username) : '',
created_at: new Date().toISOString(),
});
if (result.error) {
sw.toast(result.error, 'error');
return;
}
sw.toast('Entry added', 'success');
load();
}
async function deleteEntry(id) {
await api('DELETE', '/entries/' + id);
sw.toast('Entry deleted', 'info');
load();
}
return html`
<div class="tal-shell">
<header class="tal-header">
<div class="tal-header__left">
<h1 class="tal-title">Activity Log</h1>
<span class="tal-subtitle">Team activity tracker</span>
</div>
<div class="tal-header__right">
<div ref=${menuRef}></div>
</div>
</header>
<${EntryForm} onSubmit=${addEntry} />
<div class="tal-filters">
<button class="tal-filter ${filter === '' ? 'active' : ''}"
onClick=${() => setFilter('')}>All</button>
${CATEGORIES.map(c => html`
<button key=${c}
class="tal-filter ${filter === c ? 'active' : ''}"
onClick=${() => setFilter(c)}>
${c}
</button>
`)}
<span class="tal-count">${entries.length} entries</span>
</div>
${loading
? html`<div class="tal-loading">Loading…</div>`
: html`<${EntryList} entries=${entries} onDelete=${deleteEntry} />`
}
</div>
`;
}
function EntryForm({ onSubmit }) {
var [message, setMessage] = useState('');
var [category, setCategory] = useState('note');
var [submitting, setSubmitting] = useState(false);
async function submit(e) {
e.preventDefault();
if (!message.trim()) return;
setSubmitting(true);
await onSubmit(category, message.trim());
setMessage('');
setSubmitting(false);
}
return html`
<form class="tal-form" onSubmit=${submit}>
<select class="tal-form__select" value=${category}
onChange=${e => setCategory(e.target.value)}>
${CATEGORIES.map(c => html`<option key=${c} value=${c}>${c}</option>`)}
</select>
<input class="tal-form__input" type="text"
placeholder="What happened?"
value=${message}
onInput=${e => setMessage(e.target.value)}
disabled=${submitting} />
<button class="tal-form__btn" type="submit" disabled=${submitting || !message.trim()}>
${submitting ? '…' : 'Log'}
</button>
</form>
`;
}
function EntryList({ entries, onDelete }) {
if (!entries.length) {
return html`<div class="tal-empty">No entries yet. Log your first activity above.</div>`;
}
return html`
<div class="tal-entries">
${entries.map(e => html`
<div key=${e.id} class="tal-entry">
<div class="tal-entry__header">
<span class="tal-entry__category badge">${e.category}</span>
<span class="tal-entry__user">${esc(e.username || 'unknown')}</span>
<span class="tal-entry__time">${timeAgo(e.created_at)}</span>
<button class="tal-entry__delete" onClick=${() => onDelete(e.id)}
title="Delete">×</button>
</div>
<div class="tal-entry__message">${esc(e.message)}</div>
</div>
`)}
</div>
`;
}
// ── Utilities ──────────────────────────────
function esc(s) {
var el = document.createElement('span');
el.textContent = String(s || '');
return el.innerHTML;
}
function timeAgo(iso) {
if (!iso) return '';
var ms = Date.now() - new Date(iso).getTime();
var sec = Math.floor(ms / 1000);
if (sec < 60) return 'just now';
var min = Math.floor(sec / 60);
if (min < 60) return min + 'm ago';
var hr = Math.floor(min / 60);
if (hr < 24) return hr + 'h ago';
var d = Math.floor(hr / 24);
return d + 'd ago';
}
// ── Mount ──────────────────────────────────
render(html`<${App} />`, mount);
console.log('[team-activity-log] Surface mounted');
})();