Feat v0.7.3 extension shell migration
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-runners (pull_request) Has been skipped
CI/CD / test-frontend (pull_request) Successful in 7s
CI/CD / test-sqlite (pull_request) Successful in 2m48s
CI/CD / test-go-pg (pull_request) Successful in 2m53s
CI/CD / build-and-deploy (pull_request) Successful in 1m8s

Migrate Chat, Notes, and Schedules from legacy sw.shell.Topbar to the
v0.7.0 shell topbar contract (sw.shell.topbar.setTitle/setSlot),
eliminating double topbars on all extension surfaces.

- Chat v0.3.0: reactive slot for thread title + People button
- Notes v0.9.0: slot for New/Import/Graph buttons
- Schedules v0.2.0: reactive slot for count + New button
- 6 new shell-topbar runner tests (2 per surface)
- Headless E2E deferred to v0.7.5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 12:52:40 +00:00
parent d6c7b21713
commit 4a6109b74f
18 changed files with 188 additions and 58 deletions

View File

@@ -35,7 +35,8 @@
var modules = [
'conversations.js',
'messaging.js'
'messaging.js',
'shell-topbar.js'
];
var loaded = 0;

View File

@@ -0,0 +1,31 @@
/**
* Chat Runner — chat/shell-topbar suite
*
* Validates the Chat surface uses the v0.7.0 shell topbar contract
* and does not render the legacy sw.shell.Topbar component.
*/
(function () {
'use strict';
sw.testing.suite('chat/shell-topbar', async function (s) {
s.test('surface JS does not reference legacy Topbar', async function (t) {
var base = window.__BASE__ || '';
var resp = await fetch(base + '/surfaces/chat/js/main.js');
t.assert.eq(resp.status, 200, 'fetched chat main.js');
var src = await resp.text();
var hasLegacy = src.indexOf('sw.shell.Topbar') !== -1;
t.assert.ok(!hasLegacy, 'no sw.shell.Topbar reference (uses shell topbar API)');
});
s.test('surface JS uses shell topbar API', async function (t) {
var base = window.__BASE__ || '';
var resp = await fetch(base + '/surfaces/chat/js/main.js');
var src = await resp.text();
var usesAPI = src.indexOf('sw.shell.topbar.setTitle') !== -1
|| src.indexOf('sw.shell.topbar.setSlot') !== -1;
t.assert.ok(usesAPI, 'uses sw.shell.topbar.setTitle or setSlot');
});
});
})();

View File

@@ -4,6 +4,6 @@
"type": "test-runner",
"title": "Chat Runner",
"auth": "admin",
"version": "0.1.0",
"version": "0.2.0",
"description": "Integration tests for Chat package — conversations, messaging, search."
}

View File

@@ -1,12 +1,12 @@
/**
* Chat — Surface Entry Point (v0.2.0)
* Chat — Surface Entry Point (v0.3.0)
*
* Messaging surface built on chat-core library:
* sw.api.ext('chat-core') — conversation/message CRUD
* sw.api.ext('chat') — typing indicators
* sw.realtime — live events
* sw.ui.* — primitive components
* sw.shell.Topbar — navigation bar
* sw.shell.topbar shell topbar API
*/
(async function () {
'use strict';
@@ -42,7 +42,6 @@
var api = sw.api.ext('chat-core');
var chatApi = sw.api.ext('chat');
var { Button, Spinner, Avatar, Dialog, Tabs } = sw.ui;
var Topbar = sw.shell.Topbar;
// Import UserPicker directly (not in sw.ui index)
var { UserPicker } = await import(base + '/js/sw/primitives/user-picker.js?v=' + ver);
@@ -831,20 +830,33 @@
var selectedConv = conversations.find(c => c.id === selectedId);
var threadTitle = selectedConv ? (selectedConv.title || 'Direct Message') : '';
// ── Shell topbar ───────────────────────────
useEffect(() => {
if (!sw.shell?.topbar) return;
sw.shell.topbar.setTitle('Chat');
}, []);
useEffect(() => {
if (!sw.shell?.topbar) return;
if (selectedId) {
sw.shell.topbar.setSlot(html`
<span class="ext-chat-topbar__thread-title">${threadTitle}</span>
<${Button} size="sm" variant="secondary"
onClick=${() => setShowParticipants(!showParticipants)}>
${showParticipants ? 'Hide' : 'People'}
<//>
`);
} else {
sw.shell.topbar.setSlot(null);
}
}, [selectedId, threadTitle, showParticipants]);
if (loading) {
return html`<div class="ext-chat-loading"><${Spinner} /></div>`;
}
return html`
<div class="ext-chat-app">
<${Topbar} title="Chat">
${selectedId && html`
<span class="ext-chat-topbar__thread-title">${threadTitle}</span>
<${Button} size="sm" variant="secondary"
onClick=${() => setShowParticipants(!showParticipants)}>
${showParticipants ? 'Hide' : 'People'}
<//>`}
<//>
<div class="ext-chat-body">
<${ConversationList}
selected=${selectedId}

View File

@@ -6,7 +6,7 @@
"route": "/s/chat",
"auth": "authenticated",
"layout": "single",
"version": "0.2.0",
"version": "0.3.0",
"icon": "\ud83d\udcac",
"description": "Chat surface — conversations, messaging, typing indicators, read receipts.",
"author": "armature",

View File

@@ -36,7 +36,8 @@
var modules = [
'notes-crud.js',
'folders.js',
'tags-search.js'
'tags-search.js',
'shell-topbar.js'
];
var loaded = 0;

View File

@@ -0,0 +1,31 @@
/**
* Notes Runner — notes/shell-topbar suite
*
* Validates the Notes surface uses the v0.7.0 shell topbar contract
* and does not render the legacy sw.shell.Topbar component.
*/
(function () {
'use strict';
sw.testing.suite('notes/shell-topbar', async function (s) {
s.test('surface JS does not reference legacy Topbar', async function (t) {
var base = window.__BASE__ || '';
var resp = await fetch(base + '/surfaces/notes/js/main.js');
t.assert.eq(resp.status, 200, 'fetched notes main.js');
var src = await resp.text();
var hasLegacy = src.indexOf('sw.shell.Topbar') !== -1;
t.assert.ok(!hasLegacy, 'no sw.shell.Topbar reference (uses shell topbar API)');
});
s.test('surface JS uses shell topbar API', async function (t) {
var base = window.__BASE__ || '';
var resp = await fetch(base + '/surfaces/notes/js/main.js');
var src = await resp.text();
var usesAPI = src.indexOf('sw.shell.topbar.setTitle') !== -1
|| src.indexOf('sw.shell.topbar.setSlot') !== -1;
t.assert.ok(usesAPI, 'uses sw.shell.topbar.setTitle or setSlot');
});
});
})();

View File

@@ -4,6 +4,6 @@
"type": "test-runner",
"title": "Notes Runner",
"auth": "admin",
"version": "0.1.0",
"version": "0.2.0",
"description": "Integration tests for Notes package — CRUD, folders, tags, backlinks, search."
}

View File

@@ -1,10 +1,10 @@
/**
* Notes — Surface Entry Point (v0.8.0)
* Notes — Surface Entry Point (v0.9.0)
*
* Markdown notes surface using the SDK:
* sw.api.ext('notes') — scoped API client
* sw.ui.* — primitive components
* sw.shell.Topbar — navigation bar
* sw.shell.topbar shell topbar API
* window.CM — CodeMirror 6 bundle (optional)
*/
(async function () {
@@ -59,7 +59,6 @@
// ── SDK modules ────────────────────────────
var api = sw.api.ext('notes');
var { Button, Spinner } = sw.ui;
var Topbar = sw.shell.Topbar;
// ── Debounce helper ────────────────────────
var _timers = {};
@@ -1729,13 +1728,19 @@
debounce('search', function() { loadNotes(); }, 300);
}
return html`
<${Topbar} title="Notes">
// ── Shell topbar ───────────────────────────
useEffect(() => {
if (!sw.shell?.topbar) return;
sw.shell.topbar.setTitle('Notes');
sw.shell.topbar.setSlot(html`
<${Button} variant="primary" size="sm" onClick=${handleNew}>+ New Note<//>
<${Button} variant="secondary" size="sm" onClick=${handleImport}>Import .md<//>
<${Button} variant=${showGraph ? 'primary' : 'secondary'} size="sm"
onClick=${function() { setShowGraph(!showGraph); }}>Graph<//>
<//>
`);
}, [showGraph]);
return html`
<div class="ext-notes-app">
<div class="ext-notes-sidebar">
<${SidebarTabs} activeTab=${sidebarTab} onTabChange=${setSidebarTab} noteSelected=${!!activeNote} />

View File

@@ -6,7 +6,7 @@
"route": "/s/notes",
"auth": "authenticated",
"layout": "single",
"version": "0.8.0",
"version": "0.9.0",
"icon": "📝",
"description": "Markdown notes surface with rich editor, folders, tags, backlinks, import/export, sidebar tabs, document outline, and note graph.",
"author": "armature",

View File

@@ -34,7 +34,8 @@
if (window.SR.base) assetBase = window.SR.base + assetBase;
var modules = [
'schedules-crud.js'
'schedules-crud.js',
'shell-topbar.js'
];
var loaded = 0;

View File

@@ -0,0 +1,31 @@
/**
* Schedules Runner — schedules/shell-topbar suite
*
* Validates the Schedules surface uses the v0.7.0 shell topbar contract
* and does not render the legacy sw.shell.Topbar component.
*/
(function () {
'use strict';
sw.testing.suite('schedules/shell-topbar', async function (s) {
s.test('surface JS does not reference legacy Topbar', async function (t) {
var base = window.__BASE__ || '';
var resp = await fetch(base + '/surfaces/schedules/js/main.js');
t.assert.eq(resp.status, 200, 'fetched schedules main.js');
var src = await resp.text();
var hasLegacy = src.indexOf('sw.shell.Topbar') !== -1;
t.assert.ok(!hasLegacy, 'no sw.shell.Topbar reference (uses shell topbar API)');
});
s.test('surface JS uses shell topbar API', async function (t) {
var base = window.__BASE__ || '';
var resp = await fetch(base + '/surfaces/schedules/js/main.js');
var src = await resp.text();
var usesAPI = src.indexOf('sw.shell.topbar.setTitle') !== -1
|| src.indexOf('sw.shell.topbar.setSlot') !== -1;
t.assert.ok(usesAPI, 'uses sw.shell.topbar.setTitle or setSlot');
});
});
})();

View File

@@ -4,6 +4,6 @@
"type": "test-runner",
"title": "Schedules Runner",
"auth": "admin",
"version": "0.1.0",
"version": "0.2.0",
"description": "Integration tests for Schedules package — CRUD, run, enable/disable."
}

View File

@@ -5,7 +5,7 @@
* this surface talks directly to the core scheduled-tasks endpoints.
*
* SDK usage:
* sw.shell.Topbar — standard navigation bar
* sw.shell.topbar — shell topbar API
* sw.ui.* — primitive components
* sw.api.* — raw REST calls to /api/v1/schedules
*/
@@ -39,7 +39,6 @@
var { useState, useEffect, useCallback } = hooks;
var { render } = preact;
var { Button, FormField, Dialog, Spinner, Banner } = sw.ui;
var Topbar = sw.shell?.Topbar;
// ── API helpers (core kernel endpoints) ────
var API = '/api/v1/schedules';
@@ -365,6 +364,16 @@
useEffect(function () { loadItems(); }, []);
// ── Shell topbar ───────────────────────────
useEffect(function () {
if (!sw.shell?.topbar) return;
sw.shell.topbar.setTitle('Schedules');
sw.shell.topbar.setSlot(html`
<span class="ext-schedules-count">${items.length} schedule${items.length !== 1 ? 's' : ''}</span>
<${Button} variant="primary" size="sm" onClick=${() => setEditing({})}>+ New Schedule<//>
`);
}, [items.length]);
function handleSave() {
setEditing(null);
loadItems();
@@ -372,17 +381,6 @@
return html`
<div class="ext-schedules-app">
${Topbar ? html`
<${Topbar} title="Schedules">
<span class="ext-schedules-count">${items.length} schedule${items.length !== 1 ? 's' : ''}</span>
<${Button} variant="primary" size="sm" onClick=${() => setEditing({})}>+ New Schedule<//>
<//>
` : html`
<div class="ext-schedules-header">
<h1>Schedules</h1>
<${Button} variant="primary" size="sm" onClick=${() => setEditing({})}>+ New Schedule<//>
</div>
`}
${error && html`<${Banner} variant="danger" text=${error} />`}

View File

@@ -2,7 +2,7 @@
"id": "schedules",
"title": "Schedules",
"type": "full",
"version": "0.1.0",
"version": "0.2.0",
"tier": "browser",
"author": "Armature",
"icon": "⏰",