Feat v0.7.0 shell contract + surface audit + rebrand

Two-slot shell topbar (home, left, center, bell, user menu) with SDK API
(setLeft/setSlot/setTitle/hide/show). All 4 primary surfaces migrated:
Settings and Team Admin to Pattern B (flat tabs), Admin to Pattern C
(category tabs + sidebar), Docs to Pattern A (default).

Backend WS events: package.changed (broadcast), auth.changed (targeted),
notification.all_read. User menu re-fetches on package/auth changes.
Bell syncs read state across tabs.

Error handling pass with .sw-inline-error CSS primitive. Empty state
guidance for Admin Workflows/Groups. Announcement global dismiss via
localStorage. Rebrand assets deployed (both b/e icon variants, wordmarks,
full icon library). Docs outline scroll-to-heading fix.

Bug fixes: ICD security assertion tightened, workflow-demo error surfacing,
signoff display names, hello-dashboard + team-admin/groups.js deleted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 20:08:57 +00:00
parent 1236220302
commit 1fbd836c5c
150 changed files with 3250 additions and 533 deletions

BIN
src/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -292,6 +292,25 @@
.sw-dropdown__option--disabled:hover { background: transparent; }
.sw-dropdown__empty { padding: var(--sp-3); text-align: center; color: var(--text-3); font-size: 0.85rem; }
/* ── Inline Error ─────────────────────────── */
.sw-inline-error {
display: flex; align-items: center; gap: var(--sp-3);
padding: var(--sp-3) var(--sp-4);
background: var(--bg-2); border: 1px solid var(--danger);
border-radius: var(--radius); font-size: 13px; color: var(--danger);
}
/* ── Empty State ──────────────────────────── */
.sw-empty-state {
padding: var(--sp-6) var(--sp-4);
text-align: center; color: var(--text-3); font-size: 13px;
}
.sw-empty-state__text {
margin-bottom: var(--sp-3);
}
/* ── Focus Indicators ─────────────────────── */
.sw-btn:focus-visible {

View File

@@ -156,6 +156,89 @@
gap: var(--sp-2);
}
/* ── Shell Topbar — kernel-injected two-slot model ── */
.sw-topbar--shell {
gap: var(--sp-2);
}
.sw-topbar__home {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
color: var(--text-3);
text-decoration: none;
border-radius: var(--radius);
transition: color var(--transition), background var(--transition);
flex-shrink: 0;
}
.sw-topbar__home:hover {
color: var(--text);
background: var(--bg-hover);
}
.sw-topbar__left {
display: flex;
align-items: center;
gap: var(--sp-2);
flex-shrink: 0;
}
.sw-topbar__center {
flex: 1;
display: flex;
align-items: center;
min-width: 0;
overflow-x: auto;
}
/* ── Topbar Tabs — consistent tab styling in center slot ── */
.sw-topbar__tabs {
display: flex;
align-items: center;
gap: var(--sp-1);
height: 100%;
}
.sw-topbar__tab {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: var(--sp-2) var(--sp-3);
font-size: 13px;
font-weight: 500;
color: var(--text-2);
text-decoration: none;
border-radius: var(--radius-sm);
transition: color var(--transition), background var(--transition);
white-space: nowrap;
cursor: pointer;
}
.sw-topbar__tab:hover {
color: var(--text);
background: var(--bg-hover);
}
.sw-topbar__tab.active {
color: var(--text);
background: var(--bg-2);
}
@media (max-width: 768px) {
.sw-topbar__tabs {
gap: 0;
}
.sw-topbar__tab {
padding: var(--sp-2);
font-size: 12px;
}
}
/* ── User menu trigger ───────────────────── */
.sw-user-menu__trigger {

BIN
src/favicon-light-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
src/favicon-light-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

View File

@@ -1,10 +1,17 @@
<svg viewBox="0 0 520 80" xmlns="http://www.w3.org/2000/svg">
<line x1="28" y1="8" x2="6" y2="68" stroke="#3B82F6" stroke-width="3.5" stroke-linecap="round"/>
<line x1="28" y1="8" x2="50" y2="68" stroke="#3B82F6" stroke-width="3.5" stroke-linecap="round"/>
<line x1="14" y1="46" x2="42" y2="46" stroke="#3B82F6" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="28" cy="8" r="4" fill="#3B82F6"/>
<circle cx="14" cy="46" r="2.5" fill="#EF4444"/>
<circle cx="28" cy="46" r="2.5" fill="#3B82F6"/>
<circle cx="42" cy="46" r="2.5" fill="#EF4444"/>
<text x="58" y="62" fill="#222222" font-family="-apple-system, system-ui, 'Segoe UI', sans-serif" font-size="52" font-weight="300" letter-spacing="1">rmature</text>
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<rect width="200" height="200" rx="32" fill="#e8e8ee"/>
<line x1="100" y1="55" x2="142" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.18"/>
<line x1="142" y1="100" x2="100" y2="145" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.18"/>
<line x1="100" y1="145" x2="58" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.18"/>
<line x1="58" y1="100" x2="100" y2="55" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.18"/>
<line x1="58" y1="100" x2="100" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<line x1="142" y1="100" x2="100" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<line x1="100" y1="55" x2="100" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<line x1="100" y1="100" x2="100" y2="145" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<circle cx="100" cy="100" r="16" fill="#3B82F6" opacity="0.1"/>
<circle cx="100" cy="55" r="8" fill="#3B82F6"/>
<circle cx="58" cy="100" r="8" fill="#3B82F6"/>
<circle cx="142" cy="100" r="8" fill="#EF4444"/>
<circle cx="100" cy="145" r="8" fill="#3B82F6"/>
<circle cx="100" cy="100" r="10" fill="#3B82F6"/>
</svg>

Before

Width:  |  Height:  |  Size: 737 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

View File

@@ -0,0 +1,20 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<style>
@keyframes p { 0%,100%{r:2.5;opacity:1} 50%{r:3;opacity:0.8} }
.c { animation: p 2.4s ease-in-out infinite; }
</style>
<rect width="32" height="32" rx="6" fill="#14142a"/>
<line x1="16" y1="8" x2="26" y2="16" stroke="#c8c8d0" stroke-width="1" opacity="0.3"/>
<line x1="26" y1="16" x2="16" y2="24" stroke="#c8c8d0" stroke-width="1" opacity="0.3"/>
<line x1="16" y1="24" x2="6" y2="16" stroke="#c8c8d0" stroke-width="1" opacity="0.3"/>
<line x1="6" y1="16" x2="16" y2="8" stroke="#c8c8d0" stroke-width="1" opacity="0.3"/>
<line x1="6" y1="16" x2="16" y2="16" stroke="#c8c8d0" stroke-width="0.8" opacity="0.4"/>
<line x1="26" y1="16" x2="16" y2="16" stroke="#c8c8d0" stroke-width="0.8" opacity="0.4"/>
<line x1="16" y1="8" x2="16" y2="16" stroke="#c8c8d0" stroke-width="0.8" opacity="0.4"/>
<line x1="16" y1="16" x2="16" y2="24" stroke="#c8c8d0" stroke-width="0.8" opacity="0.4"/>
<circle cx="16" cy="8" r="2" fill="#3B82F6"/>
<circle cx="6" cy="16" r="2" fill="#3B82F6"/>
<circle cx="26" cy="16" r="2" fill="#EF4444"/>
<circle cx="16" cy="24" r="2" fill="#3B82F6"/>
<circle cx="16" cy="16" r="2.5" fill="#3B82F6" class="c"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

16
src/icons/favicon.svg Normal file
View File

@@ -0,0 +1,16 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="6" fill="#14142a"/>
<line x1="16" y1="7" x2="26" y2="16" stroke="#c8c8d0" stroke-width="1" opacity="0.3"/>
<line x1="26" y1="16" x2="16" y2="25" stroke="#c8c8d0" stroke-width="1" opacity="0.3"/>
<line x1="16" y1="25" x2="6" y2="16" stroke="#c8c8d0" stroke-width="1" opacity="0.3"/>
<line x1="6" y1="16" x2="16" y2="7" stroke="#c8c8d0" stroke-width="1" opacity="0.3"/>
<line x1="6" y1="16" x2="16" y2="16" stroke="#c8c8d0" stroke-width="0.8" opacity="0.4"/>
<line x1="26" y1="16" x2="16" y2="16" stroke="#c8c8d0" stroke-width="0.8" opacity="0.4"/>
<line x1="16" y1="7" x2="16" y2="16" stroke="#c8c8d0" stroke-width="0.8" opacity="0.4"/>
<line x1="16" y1="16" x2="16" y2="25" stroke="#c8c8d0" stroke-width="0.8" opacity="0.4"/>
<circle cx="16" cy="7" r="2" fill="#3B82F6"/>
<circle cx="6" cy="16" r="2" fill="#3B82F6"/>
<circle cx="26" cy="16" r="2" fill="#EF4444"/>
<circle cx="16" cy="25" r="2" fill="#3B82F6"/>
<circle cx="16" cy="16" r="2.5" fill="#3B82F6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

View File

@@ -0,0 +1,48 @@
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<style>
@keyframes center-pulse {
0%, 100% { r: 10; opacity: 1; }
50% { r: 12; opacity: 0.85; }
}
@keyframes glow-pulse {
0%, 100% { r: 16; opacity: 0.12; }
50% { r: 22; opacity: 0.06; }
}
@keyframes node-breathe {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
.center-node { animation: center-pulse 2.4s ease-in-out infinite; }
.center-glow { animation: glow-pulse 2.4s ease-in-out infinite; }
.node-tl { animation: node-breathe 2.4s ease-in-out infinite 0.3s; }
.node-tr { animation: node-breathe 2.4s ease-in-out infinite 0.6s; }
.node-bl { animation: node-breathe 2.4s ease-in-out infinite 0.9s; }
.node-br { animation: node-breathe 2.4s ease-in-out infinite 1.2s; }
</style>
<rect width="200" height="200" rx="32" fill="#14142a"/>
<!-- Diamond outline -->
<line x1="100" y1="51" x2="145" y2="100" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<line x1="145" y1="100" x2="100" y2="149" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<line x1="100" y1="149" x2="55" y2="100" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<line x1="55" y1="100" x2="100" y2="51" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<!-- Cross lines -->
<line x1="55" y1="100" x2="100" y2="100" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<line x1="145" y1="100" x2="100" y2="100" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<line x1="100" y1="51" x2="100" y2="100" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<line x1="100" y1="100" x2="100" y2="149" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<!-- Center glow -->
<circle cx="100" cy="100" r="16" fill="#3B82F6" opacity="0.12" class="center-glow"/>
<!-- Nodes -->
<circle cx="100" cy="51" r="8" fill="#3B82F6" class="node-tl"/>
<circle cx="55" cy="100" r="8" fill="#3B82F6" class="node-tr"/>
<circle cx="145" cy="100" r="8" fill="#EF4444" class="node-bl"/>
<circle cx="100" cy="149" r="8" fill="#3B82F6" class="node-br"/>
<!-- Center -->
<circle cx="100" cy="100" r="10" fill="#3B82F6" class="center-node"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

21
src/icons/icon-b-dark.svg Normal file
View File

@@ -0,0 +1,21 @@
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<rect width="200" height="200" rx="32" fill="#14142a"/>
<!-- Diamond outline -->
<line x1="100" y1="55" x2="142" y2="100" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<line x1="142" y1="100" x2="100" y2="145" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<line x1="100" y1="145" x2="58" y2="100" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<line x1="58" y1="100" x2="100" y2="55" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<!-- Cross lines -->
<line x1="58" y1="100" x2="100" y2="100" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<line x1="142" y1="100" x2="100" y2="100" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<line x1="100" y1="55" x2="100" y2="100" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<line x1="100" y1="100" x2="100" y2="145" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<!-- Center glow -->
<circle cx="100" cy="100" r="16" fill="#3B82F6" opacity="0.12"/>
<!-- Nodes -->
<circle cx="100" cy="55" r="8" fill="#3B82F6"/>
<circle cx="58" cy="100" r="8" fill="#3B82F6"/>
<circle cx="142" cy="100" r="8" fill="#EF4444"/>
<circle cx="100" cy="145" r="8" fill="#3B82F6"/>
<circle cx="100" cy="100" r="10" fill="#3B82F6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,43 @@
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<style>
@keyframes center-pulse {
0%, 100% { r: 10; opacity: 1; }
50% { r: 12; opacity: 0.85; }
}
@keyframes glow-pulse {
0%, 100% { r: 16; opacity: 0.1; }
50% { r: 22; opacity: 0.05; }
}
@keyframes node-breathe {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
.center-node { animation: center-pulse 2.4s ease-in-out infinite; }
.center-glow { animation: glow-pulse 2.4s ease-in-out infinite; }
.node-tl { animation: node-breathe 2.4s ease-in-out infinite 0.3s; }
.node-tr { animation: node-breathe 2.4s ease-in-out infinite 0.6s; }
.node-bl { animation: node-breathe 2.4s ease-in-out infinite 0.9s; }
.node-br { animation: node-breathe 2.4s ease-in-out infinite 1.2s; }
</style>
<rect width="200" height="200" rx="32" fill="#e8e8ee"/>
<line x1="100" y1="51" x2="145" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.18"/>
<line x1="145" y1="100" x2="100" y2="149" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.18"/>
<line x1="100" y1="149" x2="55" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.18"/>
<line x1="55" y1="100" x2="100" y2="51" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.18"/>
<line x1="55" y1="100" x2="100" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<line x1="145" y1="100" x2="100" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<line x1="100" y1="51" x2="100" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<line x1="100" y1="100" x2="100" y2="149" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<circle cx="100" cy="100" r="16" fill="#3B82F6" opacity="0.1" class="center-glow"/>
<circle cx="100" cy="51" r="8" fill="#3B82F6" class="node-tl"/>
<circle cx="55" cy="100" r="8" fill="#3B82F6" class="node-tr"/>
<circle cx="145" cy="100" r="8" fill="#EF4444" class="node-bl"/>
<circle cx="100" cy="149" r="8" fill="#3B82F6" class="node-br"/>
<circle cx="100" cy="100" r="10" fill="#3B82F6" class="center-node"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,17 @@
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<rect width="200" height="200" rx="32" fill="#e8e8ee"/>
<line x1="100" y1="55" x2="142" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.18"/>
<line x1="142" y1="100" x2="100" y2="145" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.18"/>
<line x1="100" y1="145" x2="58" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.18"/>
<line x1="58" y1="100" x2="100" y2="55" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.18"/>
<line x1="58" y1="100" x2="100" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<line x1="142" y1="100" x2="100" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<line x1="100" y1="55" x2="100" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<line x1="100" y1="100" x2="100" y2="145" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<circle cx="100" cy="100" r="16" fill="#3B82F6" opacity="0.1"/>
<circle cx="100" cy="55" r="8" fill="#3B82F6"/>
<circle cx="58" cy="100" r="8" fill="#3B82F6"/>
<circle cx="142" cy="100" r="8" fill="#EF4444"/>
<circle cx="100" cy="145" r="8" fill="#3B82F6"/>
<circle cx="100" cy="100" r="10" fill="#3B82F6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,67 @@
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<style>
@keyframes draw-edge {
from { stroke-dashoffset: 120; }
to { stroke-dashoffset: 0; }
}
@keyframes pop-node {
0% { r: 0; opacity: 0; }
70% { r: 8; }
100% { r: 6; opacity: 1; }
}
@keyframes pop-center {
0% { r: 0; opacity: 0; }
70% { r: 10; }
100% { r: 8; opacity: 0.7; }
}
@keyframes center-idle {
0%, 100% { r: 8; opacity: 0.7; }
50% { r: 9.5; opacity: 0.6; }
}
.edge { stroke-dasharray: 120; stroke-dashoffset: 120; }
.e-back-1 { animation: draw-edge 0.5s ease-out 0.0s forwards; }
.e-back-2 { animation: draw-edge 0.5s ease-out 0.1s forwards; }
.e-back-3 { animation: draw-edge 0.5s ease-out 0.2s forwards; }
.e-mid-1 { animation: draw-edge 0.5s ease-out 0.35s forwards; }
.e-mid-2 { animation: draw-edge 0.5s ease-out 0.45s forwards; }
.e-mid-3 { animation: draw-edge 0.5s ease-out 0.55s forwards; }
.e-frt-1 { animation: draw-edge 0.5s ease-out 0.7s forwards; }
.e-frt-2 { animation: draw-edge 0.5s ease-out 0.8s forwards; }
.e-frt-3 { animation: draw-edge 0.5s ease-out 0.9s forwards; }
.v-topback { animation: pop-node 0.3s ease-out 0.05s forwards; r: 0; opacity: 0; }
.v-backleft { animation: pop-node 0.3s ease-out 0.15s forwards; r: 0; opacity: 0; }
.v-backright { animation: pop-node 0.3s ease-out 0.25s forwards; r: 0; opacity: 0; }
.v-center { animation: pop-center 0.3s ease-out 0.5s forwards, center-idle 2.4s ease-in-out 1s infinite; r: 0; opacity: 0; }
.v-frontleft { animation: pop-node 0.3s ease-out 0.75s forwards; r: 0; opacity: 0; }
.v-frontright { animation: pop-node 0.3s ease-out 0.85s forwards; r: 0; opacity: 0; }
.v-bottom { animation: pop-node 0.3s ease-out 0.95s forwards; r: 0; opacity: 0; }
</style>
<rect width="200" height="200" rx="32" fill="#14142a"/>
<!-- Back edges -->
<line x1="100" y1="42" x2="46" y2="72" stroke="#c8c8d0" stroke-width="1.5" stroke-linecap="round" opacity="0.2" class="edge e-back-1"/>
<line x1="100" y1="42" x2="154" y2="72" stroke="#c8c8d0" stroke-width="1.5" stroke-linecap="round" opacity="0.2" class="edge e-back-2"/>
<line x1="154" y1="72" x2="154" y2="128" stroke="#c8c8d0" stroke-width="1.5" stroke-linecap="round" opacity="0.2" class="edge e-back-3"/>
<!-- Mid edges -->
<line x1="46" y1="72" x2="100" y2="100" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4" class="edge e-mid-1"/>
<line x1="154" y1="72" x2="100" y2="100" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4" class="edge e-mid-2"/>
<line x1="100" y1="100" x2="100" y2="158" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4" class="edge e-mid-3"/>
<!-- Front edges -->
<line x1="46" y1="72" x2="46" y2="128" stroke="#c8c8d0" stroke-width="2.5" stroke-linecap="round" opacity="0.9" class="edge e-frt-1"/>
<line x1="46" y1="128" x2="100" y2="158" stroke="#c8c8d0" stroke-width="2.5" stroke-linecap="round" opacity="0.9" class="edge e-frt-2"/>
<line x1="154" y1="128" x2="100" y2="158" stroke="#c8c8d0" stroke-width="2.5" stroke-linecap="round" opacity="0.9" class="edge e-frt-3"/>
<!-- Vertices -->
<circle cx="100" cy="42" fill="#3B82F6" opacity="0.55" class="v-topback"/>
<circle cx="46" cy="72" fill="#3B82F6" opacity="0.55" class="v-backleft"/>
<circle cx="154" cy="72" fill="#EF4444" opacity="0.55" class="v-backright"/>
<circle cx="100" cy="100" fill="#3B82F6" class="v-center"/>
<circle cx="46" cy="128" fill="#EF4444" class="v-frontleft"/>
<circle cx="154" cy="128" fill="#3B82F6" class="v-frontright"/>
<circle cx="100" cy="158" fill="#3B82F6" class="v-bottom"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

23
src/icons/icon-e-dark.svg Normal file
View File

@@ -0,0 +1,23 @@
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<rect width="200" height="200" rx="32" fill="#14142a"/>
<!-- Back edges -->
<line x1="100" y1="48" x2="150" y2="74" stroke="#c8c8d0" stroke-width="1.5" stroke-linecap="round" opacity="0.2"/>
<line x1="100" y1="48" x2="50" y2="74" stroke="#c8c8d0" stroke-width="1.5" stroke-linecap="round" opacity="0.2"/>
<line x1="150" y1="74" x2="150" y2="126" stroke="#c8c8d0" stroke-width="1.5" stroke-linecap="round" opacity="0.2"/>
<!-- Mid edges -->
<line x1="50" y1="74" x2="100" y2="100" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<line x1="150" y1="74" x2="100" y2="100" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<line x1="100" y1="100" x2="100" y2="152" stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<!-- Front edges -->
<line x1="50" y1="74" x2="50" y2="126" stroke="#c8c8d0" stroke-width="2.5" stroke-linecap="round" opacity="0.9"/>
<line x1="50" y1="126" x2="100" y2="152" stroke="#c8c8d0" stroke-width="2.5" stroke-linecap="round" opacity="0.9"/>
<line x1="150" y1="126" x2="100" y2="152" stroke="#c8c8d0" stroke-width="2.5" stroke-linecap="round" opacity="0.9"/>
<!-- Vertices -->
<circle cx="100" cy="48" r="6" fill="#3B82F6" opacity="0.55"/>
<circle cx="50" cy="74" r="6" fill="#3B82F6" opacity="0.55"/>
<circle cx="150" cy="74" r="6" fill="#EF4444" opacity="0.55"/>
<circle cx="100" cy="100" r="8" fill="#3B82F6" opacity="0.7"/>
<circle cx="50" cy="126" r="6" fill="#EF4444"/>
<circle cx="150" cy="126" r="6" fill="#3B82F6"/>
<circle cx="100" cy="152" r="6" fill="#3B82F6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,63 @@
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<style>
@keyframes draw-edge {
from { stroke-dashoffset: 120; }
to { stroke-dashoffset: 0; }
}
@keyframes pop-node {
0% { r: 0; opacity: 0; }
70% { r: 8; }
100% { r: 6; opacity: 1; }
}
@keyframes pop-center {
0% { r: 0; opacity: 0; }
70% { r: 10; }
100% { r: 8; opacity: 0.7; }
}
@keyframes center-idle {
0%, 100% { r: 8; opacity: 0.7; }
50% { r: 9.5; opacity: 0.6; }
}
.edge { stroke-dasharray: 120; stroke-dashoffset: 120; }
.e-back-1 { animation: draw-edge 0.5s ease-out 0.0s forwards; }
.e-back-2 { animation: draw-edge 0.5s ease-out 0.1s forwards; }
.e-back-3 { animation: draw-edge 0.5s ease-out 0.2s forwards; }
.e-mid-1 { animation: draw-edge 0.5s ease-out 0.35s forwards; }
.e-mid-2 { animation: draw-edge 0.5s ease-out 0.45s forwards; }
.e-mid-3 { animation: draw-edge 0.5s ease-out 0.55s forwards; }
.e-frt-1 { animation: draw-edge 0.5s ease-out 0.7s forwards; }
.e-frt-2 { animation: draw-edge 0.5s ease-out 0.8s forwards; }
.e-frt-3 { animation: draw-edge 0.5s ease-out 0.9s forwards; }
.v-topback { animation: pop-node 0.3s ease-out 0.05s forwards; r: 0; opacity: 0; }
.v-backleft { animation: pop-node 0.3s ease-out 0.15s forwards; r: 0; opacity: 0; }
.v-backright { animation: pop-node 0.3s ease-out 0.25s forwards; r: 0; opacity: 0; }
.v-center { animation: pop-center 0.3s ease-out 0.5s forwards, center-idle 2.4s ease-in-out 1s infinite; r: 0; opacity: 0; }
.v-frontleft { animation: pop-node 0.3s ease-out 0.75s forwards; r: 0; opacity: 0; }
.v-frontright { animation: pop-node 0.3s ease-out 0.85s forwards; r: 0; opacity: 0; }
.v-bottom { animation: pop-node 0.3s ease-out 0.95s forwards; r: 0; opacity: 0; }
</style>
<rect width="200" height="200" rx="32" fill="#e8e8ee"/>
<line x1="100" y1="42" x2="46" y2="72" stroke="#333" stroke-width="1.5" stroke-linecap="round" opacity="0.15" class="edge e-back-1"/>
<line x1="100" y1="42" x2="154" y2="72" stroke="#333" stroke-width="1.5" stroke-linecap="round" opacity="0.15" class="edge e-back-2"/>
<line x1="154" y1="72" x2="154" y2="128" stroke="#333" stroke-width="1.5" stroke-linecap="round" opacity="0.15" class="edge e-back-3"/>
<line x1="46" y1="72" x2="100" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3" class="edge e-mid-1"/>
<line x1="154" y1="72" x2="100" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3" class="edge e-mid-2"/>
<line x1="100" y1="100" x2="100" y2="158" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3" class="edge e-mid-3"/>
<line x1="46" y1="72" x2="46" y2="128" stroke="#333" stroke-width="2.5" stroke-linecap="round" opacity="0.7" class="edge e-frt-1"/>
<line x1="46" y1="128" x2="100" y2="158" stroke="#333" stroke-width="2.5" stroke-linecap="round" opacity="0.7" class="edge e-frt-2"/>
<line x1="154" y1="128" x2="100" y2="158" stroke="#333" stroke-width="2.5" stroke-linecap="round" opacity="0.7" class="edge e-frt-3"/>
<circle cx="100" cy="42" fill="#3B82F6" opacity="0.55" class="v-topback"/>
<circle cx="46" cy="72" fill="#3B82F6" opacity="0.55" class="v-backleft"/>
<circle cx="154" cy="72" fill="#EF4444" opacity="0.55" class="v-backright"/>
<circle cx="100" cy="100" fill="#3B82F6" class="v-center"/>
<circle cx="46" cy="128" fill="#EF4444" class="v-frontleft"/>
<circle cx="154" cy="128" fill="#3B82F6" class="v-frontright"/>
<circle cx="100" cy="158" fill="#3B82F6" class="v-bottom"/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,19 @@
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<rect width="200" height="200" rx="32" fill="#e8e8ee"/>
<line x1="100" y1="48" x2="150" y2="74" stroke="#333" stroke-width="1.5" stroke-linecap="round" opacity="0.15"/>
<line x1="100" y1="48" x2="50" y2="74" stroke="#333" stroke-width="1.5" stroke-linecap="round" opacity="0.15"/>
<line x1="150" y1="74" x2="150" y2="126" stroke="#333" stroke-width="1.5" stroke-linecap="round" opacity="0.15"/>
<line x1="50" y1="74" x2="100" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<line x1="150" y1="74" x2="100" y2="100" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<line x1="100" y1="100" x2="100" y2="152" stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<line x1="50" y1="74" x2="50" y2="126" stroke="#333" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>
<line x1="50" y1="126" x2="100" y2="152" stroke="#333" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>
<line x1="150" y1="126" x2="100" y2="152" stroke="#333" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>
<circle cx="100" cy="48" r="6" fill="#3B82F6" opacity="0.55"/>
<circle cx="50" cy="74" r="6" fill="#3B82F6" opacity="0.55"/>
<circle cx="150" cy="74" r="6" fill="#EF4444" opacity="0.55"/>
<circle cx="100" cy="100" r="8" fill="#3B82F6" opacity="0.7"/>
<circle cx="50" cy="126" r="6" fill="#EF4444"/>
<circle cx="150" cy="126" r="6" fill="#3B82F6"/>
<circle cx="100" cy="152" r="6" fill="#3B82F6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,174 @@
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<style>
@keyframes center-pulse {
0%, 100% { r: 8; }
50% { r: 9.5; }
}
.center-node { animation: center-pulse 2.4s ease-in-out infinite; }
</style>
<rect width="200" height="200" rx="32" fill="#14142a"/>
<!--
B-state (flat diamond) positions:
top: 100,51 left: 55,100 right: 145,100 bottom: 100,149 center: 100,100
E-state (wireframe cube) positions:
topBack: 100,42 backLeft: 46,72 backRight: 154,72
center: 100,100 frontLeft: 46,128 frontRight: 154,128 bottom: 100,158
Mapping B→E:
top → topBack (100,51 → 100,42)
left → backLeft (55,100 → 46,72) AND spawns frontLeft (55,100 → 46,128)
right → backRight (145,100 → 154,72) AND spawns frontRight (145,100 → 154,128)
bottom → bottom (100,149 → 100,158)
center → center (100,100 → 100,100) stays
Timing: 1.5s hold B, 1s morph to E, 2s hold E, 1s morph back, repeat
-->
<!-- ═══ EDGES ═══ -->
<!-- Diamond outline (B) that morphs to cube edges (E) -->
<!-- top→right becomes topBack→backRight -->
<line stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.25"
x1="100" y1="51" x2="145" y2="100">
<animate attributeName="x1" values="100;100;100;100;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y1" values="51;51;42;42;51" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="x2" values="145;145;154;154;145" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y2" values="100;100;72;72;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<!-- right→bottom becomes backRight→frontRight -->
<line stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.25"
x1="145" y1="100" x2="100" y2="149">
<animate attributeName="x1" values="145;145;154;154;145" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y1" values="100;100;72;72;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="x2" values="100;100;154;154;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y2" values="149;149;128;128;149" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<!-- bottom→left becomes frontRight→bottom (front bottom-right) -->
<line stroke="#c8c8d0" stroke-width="2.5" stroke-linecap="round" opacity="0.25"
x1="100" y1="149" x2="55" y2="100">
<animate attributeName="x1" values="100;100;154;154;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y1" values="149;149;128;128;149" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="x2" values="55;55;100;100;55" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y2" values="100;100;158;158;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.25;0.25;0.9;0.9;0.25" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<!-- left→top becomes backLeft→topBack -->
<line stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.25"
x1="55" y1="100" x2="100" y2="51">
<animate attributeName="x1" values="55;55;46;46;55" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y1" values="100;100;72;72;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="x2" values="100;100;100;100;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y2" values="51;51;42;42;51" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<!-- Cross: left→center becomes backLeft→center -->
<line stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4"
x1="55" y1="100" x2="100" y2="100">
<animate attributeName="x1" values="55;55;46;46;55" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y1" values="100;100;72;72;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<!-- Cross: right→center becomes backRight→center -->
<line stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4"
x1="145" y1="100" x2="100" y2="100">
<animate attributeName="x1" values="145;145;154;154;145" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y1" values="100;100;72;72;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<!-- Cross: center→bottom stays vertical but bottom moves -->
<line stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4"
x1="100" y1="100" x2="100" y2="149">
<animate attributeName="y2" values="149;149;158;158;149" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<!-- Cross: top→center vertical -->
<line stroke="#c8c8d0" stroke-width="2" stroke-linecap="round" opacity="0.4"
x1="100" y1="51" x2="100" y2="100">
<animate attributeName="y1" values="51;51;42;42;51" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<!-- NEW edges that appear in E-state (start invisible) -->
<!-- frontLeft→bottom -->
<line stroke="#c8c8d0" stroke-width="2.5" stroke-linecap="round"
x1="55" y1="100" x2="100" y2="149" opacity="0">
<animate attributeName="x1" values="55;55;46;46;55" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y1" values="100;100;128;128;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y2" values="149;149;158;158;149" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0;0;0.9;0.9;0" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<!-- backLeft→frontLeft (left vertical) -->
<line stroke="#c8c8d0" stroke-width="2.5" stroke-linecap="round"
x1="55" y1="100" x2="55" y2="100" opacity="0">
<animate attributeName="x1" values="55;55;46;46;55" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y1" values="100;100;72;72;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="x2" values="55;55;46;46;55" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y2" values="100;100;128;128;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0;0;0.9;0.9;0" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<!-- ═══ NODES ═══ -->
<!-- Top → TopBack -->
<circle cx="100" cy="51" r="8" fill="#3B82F6">
<animate attributeName="cy" values="51;51;42;42;51" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="1;1;0.55;0.55;1" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="r" values="8;8;6;6;8" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</circle>
<!-- Left → BackLeft -->
<circle cx="55" cy="100" r="8" fill="#3B82F6">
<animate attributeName="cx" values="55;55;46;46;55" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="cy" values="100;100;72;72;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="1;1;0.55;0.55;1" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="r" values="8;8;6;6;8" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</circle>
<!-- Right → BackRight -->
<circle cx="145" cy="100" r="8" fill="#EF4444">
<animate attributeName="cx" values="145;145;154;154;145" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="cy" values="100;100;72;72;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="1;1;0.55;0.55;1" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="r" values="8;8;6;6;8" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</circle>
<!-- Bottom → Bottom (slight shift) -->
<circle cx="100" cy="149" r="8" fill="#3B82F6">
<animate attributeName="cy" values="149;149;158;158;149" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="r" values="8;8;6;6;8" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</circle>
<!-- FrontLeft: spawns from Left position, invisible in B -->
<circle cx="55" cy="100" r="0" fill="#EF4444" opacity="0">
<animate attributeName="cx" values="55;55;46;46;55" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="cy" values="100;100;128;128;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="r" values="0;0;6;6;0" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</circle>
<!-- FrontRight: spawns from Right position, invisible in B -->
<circle cx="145" cy="100" r="0" fill="#3B82F6" opacity="0">
<animate attributeName="cx" values="145;145;154;154;145" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="cy" values="100;100;128;128;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="r" values="0;0;6;6;0" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</circle>
<!-- Center (always visible, pulse) -->
<circle cx="100" cy="100" r="10" fill="#3B82F6" class="center-node">
<animate attributeName="r" values="10;10;8;8;10" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="1;1;0.7;0.7;1" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</circle>
<!-- Center glow -->
<circle cx="100" cy="100" r="16" fill="#3B82F6" opacity="0.12">
<animate attributeName="r" values="16;16;12;12;16" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.12;0.12;0.06;0.06;0.12" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</circle>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,127 @@
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<style>
@keyframes center-pulse {
0%, 100% { r: 8; }
50% { r: 9.5; }
}
.center-node { animation: center-pulse 2.4s ease-in-out infinite; }
</style>
<rect width="200" height="200" rx="32" fill="#e8e8ee"/>
<!-- Diamond/cube edges -->
<line stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.18"
x1="100" y1="51" x2="145" y2="100">
<animate attributeName="y1" values="51;51;42;42;51" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="x2" values="145;145;154;154;145" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y2" values="100;100;72;72;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<line stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.18"
x1="145" y1="100" x2="100" y2="149">
<animate attributeName="x1" values="145;145;154;154;145" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y1" values="100;100;72;72;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="x2" values="100;100;154;154;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y2" values="149;149;128;128;149" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<line stroke="#333" stroke-width="2.5" stroke-linecap="round" opacity="0.18"
x1="100" y1="149" x2="55" y2="100">
<animate attributeName="x1" values="100;100;154;154;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y1" values="149;149;128;128;149" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="x2" values="55;55;100;100;55" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y2" values="100;100;158;158;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.18;0.18;0.7;0.7;0.18" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<line stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.18"
x1="55" y1="100" x2="100" y2="51">
<animate attributeName="x1" values="55;55;46;46;55" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y1" values="100;100;72;72;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y2" values="51;51;42;42;51" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<!-- Cross lines -->
<line stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"
x1="55" y1="100" x2="100" y2="100">
<animate attributeName="x1" values="55;55;46;46;55" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y1" values="100;100;72;72;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<line stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"
x1="145" y1="100" x2="100" y2="100">
<animate attributeName="x1" values="145;145;154;154;145" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y1" values="100;100;72;72;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<line stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"
x1="100" y1="100" x2="100" y2="149">
<animate attributeName="y2" values="149;149;158;158;149" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<line stroke="#333" stroke-width="2" stroke-linecap="round" opacity="0.3"
x1="100" y1="51" x2="100" y2="100">
<animate attributeName="y1" values="51;51;42;42;51" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<!-- E-only edges (spawn) -->
<line stroke="#333" stroke-width="2.5" stroke-linecap="round"
x1="55" y1="100" x2="100" y2="149" opacity="0">
<animate attributeName="x1" values="55;55;46;46;55" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y1" values="100;100;128;128;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y2" values="149;149;158;158;149" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0;0;0.7;0.7;0" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<line stroke="#333" stroke-width="2.5" stroke-linecap="round"
x1="55" y1="100" x2="55" y2="100" opacity="0">
<animate attributeName="x1" values="55;55;46;46;55" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y1" values="100;100;72;72;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="x2" values="55;55;46;46;55" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="y2" values="100;100;128;128;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0;0;0.7;0.7;0" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</line>
<!-- Nodes -->
<circle cx="100" cy="51" r="8" fill="#3B82F6">
<animate attributeName="cy" values="51;51;42;42;51" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="1;1;0.55;0.55;1" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="r" values="8;8;6;6;8" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</circle>
<circle cx="55" cy="100" r="8" fill="#3B82F6">
<animate attributeName="cx" values="55;55;46;46;55" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="cy" values="100;100;72;72;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="1;1;0.55;0.55;1" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="r" values="8;8;6;6;8" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</circle>
<circle cx="145" cy="100" r="8" fill="#EF4444">
<animate attributeName="cx" values="145;145;154;154;145" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="cy" values="100;100;72;72;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="1;1;0.55;0.55;1" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="r" values="8;8;6;6;8" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</circle>
<circle cx="100" cy="149" r="8" fill="#3B82F6">
<animate attributeName="cy" values="149;149;158;158;149" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="r" values="8;8;6;6;8" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</circle>
<!-- Spawn nodes -->
<circle cx="55" cy="100" r="0" fill="#EF4444" opacity="0">
<animate attributeName="cx" values="55;55;46;46;55" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="cy" values="100;100;128;128;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="r" values="0;0;6;6;0" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</circle>
<circle cx="145" cy="100" r="0" fill="#3B82F6" opacity="0">
<animate attributeName="cx" values="145;145;154;154;145" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="cy" values="100;100;128;128;100" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="r" values="0;0;6;6;0" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</circle>
<!-- Center -->
<circle cx="100" cy="100" r="10" fill="#3B82F6" class="center-node">
<animate attributeName="r" values="10;10;8;8;10" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="1;1;0.7;0.7;1" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</circle>
<circle cx="100" cy="100" r="16" fill="#3B82F6" opacity="0.1">
<animate attributeName="r" values="16;16;12;12;16" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.1;0.1;0.05;0.05;0.1" keyTimes="0;0.27;0.45;0.73;1" dur="5.5s" repeatCount="indefinite"/>
</circle>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
src/icons/preview-sheet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,12 @@
<svg viewBox="0 0 520 80" xmlns="http://www.w3.org/2000/svg">
<!-- A-frame lettermark -->
<line x1="28" y1="8" x2="6" y2="68" stroke="#3B82F6" stroke-width="3.5" stroke-linecap="round"/>
<line x1="28" y1="8" x2="50" y2="68" stroke="#3B82F6" stroke-width="3.5" stroke-linecap="round"/>
<line x1="14" y1="46" x2="42" y2="46" stroke="#3B82F6" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="28" cy="8" r="4" fill="#3B82F6"/>
<circle cx="14" cy="46" r="2.5" fill="#EF4444"/>
<circle cx="28" cy="46" r="2.5" fill="#3B82F6"/>
<circle cx="42" cy="46" r="2.5" fill="#EF4444"/>
<!-- "rmature" text -->
<text x="58" y="62" fill="#dddddd" font-family="-apple-system, system-ui, 'Segoe UI', sans-serif" font-size="52" font-weight="300" letter-spacing="1">rmature</text>
</svg>

After

Width:  |  Height:  |  Size: 793 B

View File

@@ -0,0 +1,10 @@
<svg viewBox="0 0 520 80" xmlns="http://www.w3.org/2000/svg">
<line x1="28" y1="8" x2="6" y2="68" stroke="#3B82F6" stroke-width="3.5" stroke-linecap="round"/>
<line x1="28" y1="8" x2="50" y2="68" stroke="#3B82F6" stroke-width="3.5" stroke-linecap="round"/>
<line x1="14" y1="46" x2="42" y2="46" stroke="#3B82F6" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="28" cy="8" r="4" fill="#3B82F6"/>
<circle cx="14" cy="46" r="2.5" fill="#EF4444"/>
<circle cx="28" cy="46" r="2.5" fill="#3B82F6"/>
<circle cx="42" cy="46" r="2.5" fill="#EF4444"/>
<text x="58" y="62" fill="#222222" font-family="-apple-system, system-ui, 'Segoe UI', sans-serif" font-size="52" font-weight="300" letter-spacing="1">rmature</text>
</svg>

After

Width:  |  Height:  |  Size: 737 B

View File

@@ -154,7 +154,7 @@ export async function boot() {
sw.ui = Object.freeze({});
}
// Topbar — standard navigation bar for surfaces
// Topbar — standard navigation bar for surfaces (legacy API)
try {
const topbarMod = await import('../shell/topbar.js');
_shell.Topbar = topbarMod.Topbar;
@@ -162,7 +162,16 @@ export async function boot() {
console.warn('[sw] Topbar import failed:', e.message);
}
// Freeze shell now that Topbar is loaded
// Shell Topbar — kernel-injected two-slot topbar
try {
const shellTopbarMod = await import('../shell/shell-topbar.js');
_shell.topbar = shellTopbarMod.shellTopbarAPI;
_shell._ShellTopbar = shellTopbarMod.ShellTopbar;
} catch (e) {
console.warn('[sw] ShellTopbar import failed:', e.message);
}
// Freeze shell now that all shell modules are loaded
sw.shell = Object.freeze(_shell);
// UserMenu render helper — surfaces call sw.userMenu(container, opts)
@@ -174,7 +183,7 @@ export async function boot() {
};
// Marker for idempotency
sw._sdk = '0.6.5';
sw._sdk = '0.7.0';
// 8. Expose globally
window.sw = sw;
@@ -210,7 +219,18 @@ export async function boot() {
console.warn('[sw] Imperative host mount failed:', e.message);
}
// 11. Signal ready
// 11. Auto-mount shell topbar if #shell-topbar exists
try {
const shellMount = document.getElementById('shell-topbar');
if (shellMount && sw.shell._ShellTopbar && auth.isAuthenticated) {
const { render } = preact;
render(html`<${sw.shell._ShellTopbar} />`, shellMount);
}
} catch (e) {
console.warn('[sw] Shell topbar mount failed:', e.message);
}
// 12. Signal ready
events.emit('sdk.ready', {}, { localOnly: true });
document.dispatchEvent(new CustomEvent('sw:ready', { detail: { sw } }));
console.log(`[sw] SDK v${sw._sdk} ready`);

View File

@@ -39,9 +39,35 @@ export function NotificationBell({ onNavigate } = {}) {
// ── WebSocket live updates ──────────
useEffect(() => {
if (!window.sw?.on) return;
const handler = () => reload();
window.sw.on('notification.created', handler);
return () => window.sw.off?.('notification.created', handler);
// New notification — refetch the list
const onCreated = () => reload();
// Single notification marked read (from another tab/surface)
const onRead = (e) => {
const id = e?.id;
if (!id) return;
setNotifications(prev => prev.map(n =>
n.id === id ? { ...n, read_at: n.read_at || new Date().toISOString() } : n
));
};
// All notifications marked read (from another tab/surface)
const onAllRead = () => {
setNotifications(prev => prev.map(n => ({
...n, read_at: n.read_at || new Date().toISOString()
})));
};
window.sw.on('notification.created', onCreated);
window.sw.on('notification.read', onRead);
window.sw.on('notification.all_read', onAllRead);
return () => {
window.sw.off?.('notification.created', onCreated);
window.sw.off?.('notification.read', onRead);
window.sw.off?.('notification.all_read', onAllRead);
};
}, [reload]);
// ── Close on outside click ──────────

View File

@@ -0,0 +1,95 @@
/**
* ShellTopbar — Kernel-injected topbar for all surfaces
*
* Two named slots: left (defaults to manifest title) and center (flex:1).
* Home link, notification bell, and user menu always present.
*
* Surfaces customize via sw.shell.topbar.setLeft/setSlot/setTitle/hide/show.
* This component is auto-mounted by the SDK boot if #shell-topbar exists.
*/
const { html } = window;
const { useState, useEffect } = hooks;
import { NotificationBell } from './notification-bell.js';
import { UserMenu } from './user-menu.js';
// ── Shared state — SDK API writes here, component reads ──
let _leftContent = null;
let _centerContent = null;
let _titleOverride = null;
let _visible = true;
let _listeners = new Set();
function notify() {
_listeners.forEach(fn => fn());
}
/** SDK-facing API — exported for sw.shell.topbar.* */
export const shellTopbarAPI = {
/** Override left slot with a vnode */
setLeft(vnode) {
_leftContent = vnode;
_titleOverride = null;
notify();
},
/** Set center slot content (tabs, pickers, search) */
setSlot(vnode) {
_centerContent = vnode;
notify();
},
/** Shorthand: set left slot to plain text title */
setTitle(str) {
_titleOverride = str;
_leftContent = null;
notify();
},
/** Hide the entire topbar (full-bleed surfaces) */
hide() {
_visible = false;
notify();
},
/** Show the topbar after hiding */
show() {
_visible = true;
notify();
},
};
export function ShellTopbar() {
const [, setTick] = useState(0);
useEffect(() => {
const listener = () => setTick(t => t + 1);
_listeners.add(listener);
return () => _listeners.delete(listener);
}, []);
if (!_visible) return null;
const BASE = window.__BASE__ || '';
const manifestTitle = window.__MANIFEST__?.title || '';
// Left slot: custom vnode > title override > manifest title
const leftSlot = _leftContent
? _leftContent
: html`<span class="sw-topbar__title">${_titleOverride || manifestTitle}</span>`;
return html`
<div class="sw-topbar sw-topbar--shell">
<a class="sw-topbar__home" href="${BASE}/" title="Home">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="1 8 8 1 15 8" />
<polyline points="3 10 3 15 6.5 15 6.5 11 9.5 11 9.5 15 13 15 13 10" />
</svg>
</a>
<div class="sw-topbar__left">${leftSlot}</div>
<div class="sw-topbar__center">${_centerContent}</div>
<div class="sw-topbar__right">
<${NotificationBell} />
<${UserMenu} placement="down-right" />
</div>
</div>
`;
}

View File

@@ -34,15 +34,29 @@ export function UserMenu({ placement = 'down-right', onAction, extraItems }) {
const user = sw?.auth?.user;
const authenticated = sw?.auth?.isAuthenticated;
// Fetch installed surfaces once on mount.
// Fetch installed surfaces on mount and when packages/auth change.
const CORE_IDS = new Set(['admin', 'settings', 'team-admin', 'workflow', 'workflow-landing', 'docs', 'welcome']);
useEffect(() => {
if (!sw?.api?.surfaces?.list) return;
sw.api.surfaces.list().then(data => {
const raw = Array.isArray(data) ? data : data?.data || [];
setAllSurfaces(raw);
}).catch(() => {});
function fetchSurfaces() {
sw.api.surfaces.list().then(data => {
const raw = Array.isArray(data) ? data : data?.data || [];
setAllSurfaces(raw);
}).catch(() => {});
}
fetchSurfaces();
// Re-fetch when packages or auth change (WS events from CS-1)
const off1 = sw.on?.('package.changed', fetchSurfaces);
const off2 = sw.on?.('auth.changed', fetchSurfaces);
return () => {
if (typeof off1 === 'function') off1();
if (typeof off2 === 'function') off2();
};
}, [authenticated]);
const extSurfaces = allSurfaces.filter(s => !CORE_IDS.has(s.id));

View File

@@ -15,11 +15,14 @@ export default function GroupsSection() {
const [showCreate, setShowCreate] = useState(false);
const [showAddMember, setShowAddMember] = useState(false);
const [error, setError] = useState(null);
const loadGroups = useCallback(async () => {
setError(null);
try {
const data = await sw.api.admin.groups.list();
setGroups(data || []);
} catch (e) { sw.toast(e.message, 'error'); }
} catch (e) { setError(e.message); }
finally { setLoading(false); }
}, []);
@@ -109,6 +112,13 @@ export default function GroupsSection() {
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
if (error) return html`
<div class="sw-inline-error">
<span>${error}</span>
<button class="sw-btn sw-btn--secondary sw-btn--sm" onClick=${loadGroups}>Retry</button>
</div>
`;
if (detail) return html`
<div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
@@ -194,7 +204,7 @@ export default function GroupsSection() {
`}
<div class="admin-list">
${groups.length === 0 && html`<div class="empty-hint">No groups</div>`}
${groups.length === 0 && html`<div class="sw-empty-state"><div class="sw-empty-state__text">Groups control access to surfaces and features via permissions.</div></div>`}
${groups.map(g => html`
<div class="admin-surface-row" key=${g.id} style="cursor:pointer;" onClick=${() => openDetail(g)}>
<strong style="flex:1;">${g.name}</strong>

View File

@@ -15,7 +15,6 @@ const { render } = preact;
import { ToastContainer } from '../../primitives/toast.js';
import { DialogStack } from '../../shell/dialog-stack.js';
import { UserMenu } from '../../shell/user-menu.js';
// ── Category → Section mapping ──────────────
const ADMIN_SECTIONS = {
@@ -131,36 +130,36 @@ function AdminSurface() {
if (href) location.replace(href);
}, []);
// Shell topbar: Pattern C — favicon + title in left slot, category tabs in center
useEffect(() => {
if (!window.sw?.shell?.topbar) return;
window.sw.shell.topbar.setLeft(html`
<img src="${BASE}/favicon.svg" alt="" width="18" height="18" style="vertical-align:-3px;" />
<span style="margin-left:6px;font-weight:600;font-size:0.9rem;color:var(--text);">Administration</span>
`);
window.sw.shell.topbar.setSlot(html`
<div class="sw-topbar__tabs">
${Object.entries(CATEGORY_META).map(([cat, meta]) => html`
<a key=${cat} class="sw-topbar__tab ${activeCat === cat ? 'active' : ''}"
href="${BASE}/admin/${meta.first}"
onClick=${navClick}>
<${CatIcon} paths=${meta.icon} />
${meta.label}
</a>
`)}
</div>
`);
}, [activeCat, navClick]);
const title = ADMIN_LABELS[section] || 'Admin';
const sidebarSections = ADMIN_SECTIONS[activeCat] || [];
return html`
<div class="surface-admin">
${/* Top Bar */``}
<div class="admin-topbar">
<span class="admin-topbar-title">
<img src="${BASE}/favicon.svg" alt="" width="18" height="18" style="vertical-align:-3px;" />
Administration
</span>
<div class="admin-category-tabs">
${Object.entries(CATEGORY_META).map(([cat, meta]) => html`
<a key=${cat} href="${BASE}/admin/${meta.first}"
class="admin-cat-btn ${activeCat === cat ? 'active' : ''}"
onClick=${navClick}>
<${CatIcon} paths=${meta.icon} />
${meta.label}
</a>
`)}
</div>
<div style="margin-left:auto;">
<${UserMenu} placement="down-right" />
</div>
</div>
<div class="admin-body">
${/* Left Nav */``}
${/* Left Nav — surface-owned, below topbar */``}
<div class="admin-nav">
${sidebarSections.map(key => html`
<a key=${key} href="${BASE}/admin/${key}"

View File

@@ -51,13 +51,16 @@ export default function PackagesSection() {
const BASE = window.__BASE__ || '';
const [error, setError] = useState(null);
// ── Load packages ───────────────────────────
const load = useCallback(async () => {
setError(null);
try {
const opts = typeFilter !== 'all' ? { type: typeFilter } : undefined;
const data = await sw.api.admin.packages.list(opts);
setPackages(data || []);
} catch (e) { sw.toast(e.message, 'error'); }
} catch (e) { setError(e.message); }
finally { setLoading(false); }
}, [typeFilter]);
@@ -228,6 +231,13 @@ export default function PackagesSection() {
// ── Render ───────────────────────────────────
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
if (error) return html`
<div class="sw-inline-error">
<span>${error}</span>
<button class="sw-btn sw-btn--secondary sw-btn--sm" onClick=${load}>Retry</button>
</div>
`;
const enabled = packages.filter(p => p.enabled).length;
const dormant = packages.filter(p => p.status === 'dormant').length;
const hasManifestSettings = (pkg) =>

View File

@@ -13,12 +13,14 @@ export default function WorkflowsSection() {
const [editing, setEditing] = useState(null); // null | 'new' | workflow
const [stages, setStages] = useState([]);
const [showCreate, setShowCreate] = useState(false);
const [error, setError] = useState(null);
const load = useCallback(async () => {
setError(null);
try {
const data = await sw.api.workflows.list();
setWorkflows(data || []);
} catch (e) { sw.toast(e.message, 'error'); }
} catch (e) { setError(e.message); }
finally { setLoading(false); }
}, []);
@@ -77,6 +79,13 @@ export default function WorkflowsSection() {
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
if (error) return html`
<div class="sw-inline-error">
<span>${error}</span>
<button class="sw-btn sw-btn--secondary sw-btn--sm" onClick=${load}>Retry</button>
</div>
`;
if (editing) return html`
<form onSubmit=${updateWorkflow}>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
@@ -145,7 +154,7 @@ export default function WorkflowsSection() {
`}
<div class="admin-list">
${workflows.length === 0 && html`<div class="empty-hint">No workflows</div>`}
${workflows.length === 0 && html`<div class="sw-empty-state"><div class="sw-empty-state__text">Workflows define multi-stage approval processes with team roles and SLA tracking.</div></div>`}
${workflows.map(w => html`
<div class="admin-surface-row" key=${w.id} style="cursor:pointer;" onClick=${() => openEdit(w)}>
<div style="flex:1;min-width:0;">

View File

@@ -16,8 +16,6 @@ const { render } = preact;
// Preload marked so renderSync works in useMemo
if (sw?.markdown) sw.markdown.preload();
import { Topbar } from '../../shell/topbar.js';
function DocsSurface() {
const [docs, setDocs] = useState([]);
const [active, setActive] = useState(window.__SECTION__ || 'GETTING-STARTED');
@@ -98,6 +96,14 @@ function DocsSurface() {
const el = articleRef.current;
if (!el || !renderedHtml) return;
if (sw?.renderers) sw.renderers.runPostRenderers(el);
// Inject IDs onto headings so outline links can scroll to them
el.querySelectorAll('h1, h2, h3, h4').forEach(h => {
if (!h.id) {
h.id = h.textContent.trim().toLowerCase()
.replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
}
});
}, [renderedHtml]);
function retryList() {
@@ -108,7 +114,6 @@ function DocsSurface() {
}
return html`
<${Topbar} title="Documentation" />
<div class="docs-layout">
<nav class="docs-sidebar">
<h3 class="docs-sidebar__heading">Documentation</h3>

View File

@@ -70,14 +70,12 @@ function SettingsSurface() {
if (sectionModules[section]) {
sectionModules[section]().then(mod => {
// Module exports the component as the first named export
const Comp = mod.default || Object.values(mod)[0];
setSectionComponent(() => Comp);
}).catch(e => {
console.error('[settings] Failed to load section:', section, e);
});
}
// Bridge sections are handled inline
}, [section]);
const onNavClick = useCallback((e) => {
@@ -86,86 +84,36 @@ function SettingsSurface() {
if (href) location.replace(href);
}, []);
const backClick = useCallback((e) => {
e.preventDefault();
const RETURN_KEY = 'sb_settings_return';
const returnURL = sessionStorage.getItem(RETURN_KEY);
sessionStorage.removeItem(RETURN_KEY);
location.href = returnURL || BASE + '/';
}, []);
// Stash return URL on first load
// Shell topbar: flat tabs (Pattern B)
useEffect(() => {
const RETURN_KEY = 'sb_settings_return';
if (!sessionStorage.getItem(RETURN_KEY)) {
const ref = document.referrer;
if (ref && ref.startsWith(location.origin) && !ref.includes('/settings')) {
sessionStorage.setItem(RETURN_KEY, ref);
} else {
sessionStorage.setItem(RETURN_KEY, location.origin + BASE + '/');
}
}
}, []);
if (!window.sw?.shell?.topbar) return;
const allTabs = [...NAV_ITEMS, ...EXT_NAV_ITEMS];
window.sw.shell.topbar.setSlot(html`
<div class="sw-topbar__tabs">
${allTabs.map(item => html`
<a class="sw-topbar__tab ${section === item.key ? 'active' : ''}"
href="${BASE}/settings/${item.key}"
onClick=${onNavClick}>
${item.label}
</a>
`)}
</div>
`);
}, [section, onNavClick]);
const title = SECTION_TITLES[section] || 'Settings';
return html`
<div class="surface-settings" style="flex-direction:column;">
${/* Top Bar */''}
<div class="settings-topbar">
<a href="${BASE}/" class="settings-topbar-back" onClick=${backClick}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"/>
<polyline points="12 19 5 12 12 5"/>
</svg>
Back
</a>
<div class="settings-topbar-sep"></div>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="color:var(--text-2)">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
</svg>
<span class="settings-topbar-title">Settings</span>
</div>
<div class="settings-content" style="flex:1;min-height:0;overflow:auto;">
<h2>${title}</h2>
<div style="display:flex;flex:1;min-height:0;">
${/* Left Nav */''}
<div class="settings-nav">
${NAV_ITEMS.map(item => html`
<a href="${BASE}/settings/${item.key}"
class="settings-nav-link ${section === item.key ? 'active' : ''}"
onClick=${onNavClick}>
${item.label}
</a>
`)}
${EXT_NAV_ITEMS.length > 0 && html`
<div>
<div class="settings-nav-sep"></div>
<div class="settings-nav-label" style="font-size:10px;font-weight:600;color:var(--text-3);padding:8px 12px 4px;text-transform:uppercase;letter-spacing:0.5px;">Extensions</div>
${EXT_NAV_ITEMS.map(item => html`
<a href="${BASE}/settings/${item.key}"
class="settings-nav-link ${section === item.key ? 'active' : ''}"
onClick=${onNavClick}>
${item.label}
</a>
`)}
</div>
`}
</div>
${/* Content */''}
<div class="settings-content">
<h2>${title}</h2>
${SectionComponent
? html`<${SectionComponent} />`
: html`<div class="settings-placeholder">Loading\u2026</div>`
}
</div>
${SectionComponent
? html`<${SectionComponent} />`
: html`<div class="settings-placeholder">Loading\u2026</div>`
}
</div>
</div>
<${ToastContainer} />

View File

@@ -1,37 +0,0 @@
/**
* Team Admin > Groups
*/
const { html } = window;
const { useState, useEffect, useCallback } = hooks;
export default function GroupsSection({ teamId }) {
const [groups, setGroups] = useState([]);
const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
try {
const data = await sw.api.teams.groups(teamId);
setGroups(data || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, [teamId]);
useEffect(() => { load(); }, [load]);
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
return html`
<div>
<div class="admin-list">
${groups.length === 0 && html`<div class="empty-hint">No groups</div>`}
${groups.map(g => html`
<div class="admin-surface-row" key=${g.id}>
<strong style="flex:1;">${g.name}</strong>
<span class="text-muted" style="font-size:12px;">${g.member_count || 0} members</span>
<span class="badge">${(g.permissions || []).length} permissions</span>
</div>
`)}
</div>
</div>
`;
}

View File

@@ -17,9 +17,9 @@ import { ToastContainer } from '../../primitives/toast.js';
import { DialogStack } from '../../shell/dialog-stack.js';
// ── Section config ──────────────────────────
// Groups removed (37-line dead-end — no CRUD, just "No groups" display)
const SECTIONS = [
{ key: 'members', label: 'Members' },
{ key: 'groups', label: 'Groups' },
{ key: 'connections', label: 'Connections' },
{ key: 'workflows', label: 'Workflows' },
{ key: 'settings', label: 'Settings' },
@@ -29,7 +29,6 @@ const SECTIONS = [
// ── Lazy section imports ────────────────────
const sectionModules = {
members: () => import('./members.js'),
groups: () => import('./groups.js'),
connections: () => import('./connections.js'),
workflows: () => import('./workflows.js'),
settings: () => import('./settings.js'),
@@ -79,27 +78,6 @@ function TeamAdminSurface() {
}
}, [section]);
// Back button with return URL stash
useEffect(() => {
const RETURN_KEY = 'sb_team_admin_return';
if (!sessionStorage.getItem(RETURN_KEY)) {
const ref = document.referrer;
if (ref && ref.startsWith(location.origin) && !ref.includes('/team-admin')) {
sessionStorage.setItem(RETURN_KEY, ref);
} else {
sessionStorage.setItem(RETURN_KEY, location.origin + BASE + '/');
}
}
}, []);
const backClick = useCallback((e) => {
e.preventDefault();
const RETURN_KEY = 'sb_team_admin_return';
const returnURL = sessionStorage.getItem(RETURN_KEY);
sessionStorage.removeItem(RETURN_KEY);
location.href = returnURL || BASE + '/';
}, []);
const navClick = useCallback((e) => {
e.preventDefault();
const href = e.currentTarget.getAttribute('href');
@@ -110,33 +88,52 @@ function TeamAdminSurface() {
const teamName = selectedTeam?.name || selectedTeam?.team_name || 'Team';
const sectionLabel = SECTIONS.find(s => s.key === section)?.label || 'Members';
// Shell topbar: set title and tabs
useEffect(() => {
if (!window.sw?.shell?.topbar) return;
if (selectedTeam) {
const titleContent = html`
<span style="font-weight:600;font-size:0.9rem;color:var(--text);">
Team Admin: ${teamName}
</span>
${adminTeams.length > 1 && html`
<button class="sw-btn sw-btn--ghost sw-btn--sm" style="margin-left:8px;font-size:11px;"
onClick=${() => setSelectedTeam(null)}>Switch</button>
`}
`;
window.sw.shell.topbar.setLeft(titleContent);
window.sw.shell.topbar.setSlot(html`
<div class="sw-topbar__tabs">
${SECTIONS.map(s => html`
<a key=${s.key} class="sw-topbar__tab ${section === s.key ? 'active' : ''}"
href="${BASE}/team-admin/${s.key}"
onClick=${navClick}>
${s.label}
</a>
`)}
</div>
`);
} else {
window.sw.shell.topbar.setTitle('Team Administration');
window.sw.shell.topbar.setSlot(null);
}
}, [selectedTeam, section, navClick]);
// Team picker if multiple teams
if (!selectedTeam && adminTeams.length > 1) {
return html`
<div class="surface-settings" style="flex-direction:column;">
<div class="settings-topbar">
<a href="${BASE}/" class="settings-topbar-back" onClick=${backClick}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"/>
<polyline points="12 19 5 12 12 5"/>
</svg>
Back
</a>
<div class="settings-topbar-sep"></div>
<span class="settings-topbar-title">Team Administration</span>
</div>
<div style="padding:32px;max-width:480px;margin:0 auto;">
<h3 style="margin:0 0 16px;">Select a Team</h3>
<div class="admin-list">
${adminTeams.map(t => html`
<div class="admin-surface-row" key=${t.id || t.team_id}
style="cursor:pointer;" onClick=${() => setSelectedTeam(t)}>
<strong style="flex:1;">${t.name || t.team_name}</strong>
<span class="badge">${t.my_role || 'admin'}</span>
</div>
`)}
</div>
<div style="padding:32px;max-width:480px;margin:0 auto;">
<h3 style="margin:0 0 16px;">Select a Team</h3>
<div class="admin-list">
${adminTeams.map(t => html`
<div class="admin-surface-row" key=${t.id || t.team_id}
style="cursor:pointer;" onClick=${() => setSelectedTeam(t)}>
<strong style="flex:1;">${t.name || t.team_name}</strong>
<span class="badge">${t.my_role || 'admin'}</span>
</div>
`)}
</div>
</div>
<${ToastContainer} />
@@ -147,22 +144,8 @@ function TeamAdminSurface() {
// No teams
if (!selectedTeam && adminTeams.length === 0) {
return html`
<div class="surface-settings" style="flex-direction:column;">
<div class="settings-topbar">
<a href="${BASE}/" class="settings-topbar-back" onClick=${backClick}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"/>
<polyline points="12 19 5 12 12 5"/>
</svg>
Back
</a>
<div class="settings-topbar-sep"></div>
<span class="settings-topbar-title">Team Administration</span>
</div>
<div style="padding:32px;text-align:center;">
<div class="empty-hint">You are not an admin of any team.</div>
</div>
<div style="padding:32px;text-align:center;">
<div class="empty-hint">You are not an admin of any team.</div>
</div>
<${ToastContainer} />
<${DialogStack} />
@@ -170,48 +153,12 @@ function TeamAdminSurface() {
}
return html`
<div class="surface-settings" style="flex-direction:column;">
${/* Top Bar */``}
<div class="settings-topbar">
<a href="${BASE}/" class="settings-topbar-back" onClick=${backClick}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"/>
<polyline points="12 19 5 12 12 5"/>
</svg>
Back
</a>
<div class="settings-topbar-sep"></div>
<span class="settings-topbar-title">
${teamName}
${adminTeams.length > 1 && html`
<button class="sw-btn sw-btn--ghost sw-btn--sm" style="margin-left:8px;font-size:11px;"
onClick=${() => setSelectedTeam(null)}>Switch Team</button>
`}
</span>
</div>
<div style="display:flex;flex:1;min-height:0;">
${/* Left Nav */``}
<div class="settings-nav">
${SECTIONS.map(s => html`
<a key=${s.key} href="${BASE}/team-admin/${s.key}"
class="settings-nav-link ${section === s.key ? 'active' : ''}"
onClick=${navClick}>
${s.label}
</a>
`)}
</div>
${/* Content */``}
<div class="settings-content">
<h2>${sectionLabel}</h2>
${SectionComponent && teamId
? html`<${SectionComponent} teamId=${teamId} />`
: html`<div class="settings-placeholder">Loading\u2026</div>`
}
</div>
</div>
<div style="flex:1;min-height:0;overflow:auto;padding:var(--sp-4) var(--sp-5);">
<h2>${sectionLabel}</h2>
${SectionComponent && teamId
? html`<${SectionComponent} teamId=${teamId} />`
: html`<div class="settings-placeholder">Loading\u2026</div>`
}
</div>
<${ToastContainer} />
<${DialogStack} />

View File

@@ -711,7 +711,7 @@ function SignoffPanel({ instanceId, teamId }) {
${signoffs.map(s => html`
<div key=${s.id} style="display:flex;gap:4px;align-items:center;font-size:11px;margin-top:2px;">
<span class="badge ${s.decision === 'approve' ? 'badge-success' : 'badge-danger'}">${s.decision}</span>
<span>${s.user_id}</span>
<span>${sw.users?.displayName?.(s.user_id) || s.user_id}</span>
${s.comment && html`<span class="text-muted">\u2014 ${s.comment}</span>`}
</div>
`)}

View File

@@ -1,7 +1,7 @@
{
"name": "Armature",
"short_name": "Armature",
"description": "Self-hosted AI chat — unified interface for multiple providers",
"description": "Self-hosted extension platform — build, compose, and run extensions.",
"start_url": "./",
"scope": "./",
"display": "standalone",
@@ -14,16 +14,31 @@
"sizes": "32x32",
"type": "image/png"
},
{
"src": "favicon-light-32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "favicon-256.png",
"sizes": "256x256",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "favicon-light-256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "favicon.svg",
"sizes": "any",
"type": "image/svg+xml"
},
{
"src": "favicon-light.svg",
"sizes": "any",
"type": "image/svg+xml"
}
]
}

12
src/wordmark-dark.svg Normal file
View File

@@ -0,0 +1,12 @@
<svg viewBox="0 0 520 80" xmlns="http://www.w3.org/2000/svg">
<!-- A-frame lettermark -->
<line x1="28" y1="8" x2="6" y2="68" stroke="#3B82F6" stroke-width="3.5" stroke-linecap="round"/>
<line x1="28" y1="8" x2="50" y2="68" stroke="#3B82F6" stroke-width="3.5" stroke-linecap="round"/>
<line x1="14" y1="46" x2="42" y2="46" stroke="#3B82F6" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="28" cy="8" r="4" fill="#3B82F6"/>
<circle cx="14" cy="46" r="2.5" fill="#EF4444"/>
<circle cx="28" cy="46" r="2.5" fill="#3B82F6"/>
<circle cx="42" cy="46" r="2.5" fill="#EF4444"/>
<!-- "rmature" text -->
<text x="58" y="62" fill="#dddddd" font-family="-apple-system, system-ui, 'Segoe UI', sans-serif" font-size="52" font-weight="300" letter-spacing="1">rmature</text>
</svg>

After

Width:  |  Height:  |  Size: 793 B

10
src/wordmark-light.svg Normal file
View File

@@ -0,0 +1,10 @@
<svg viewBox="0 0 520 80" xmlns="http://www.w3.org/2000/svg">
<line x1="28" y1="8" x2="6" y2="68" stroke="#3B82F6" stroke-width="3.5" stroke-linecap="round"/>
<line x1="28" y1="8" x2="50" y2="68" stroke="#3B82F6" stroke-width="3.5" stroke-linecap="round"/>
<line x1="14" y1="46" x2="42" y2="46" stroke="#3B82F6" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="28" cy="8" r="4" fill="#3B82F6"/>
<circle cx="14" cy="46" r="2.5" fill="#EF4444"/>
<circle cx="28" cy="46" r="2.5" fill="#3B82F6"/>
<circle cx="42" cy="46" r="2.5" fill="#EF4444"/>
<text x="58" y="62" fill="#222222" font-family="-apple-system, system-ui, 'Segoe UI', sans-serif" font-size="52" font-weight="300" letter-spacing="1">rmature</text>
</svg>

After

Width:  |  Height:  |  Size: 737 B

10
src/wordmark.svg Normal file
View File

@@ -0,0 +1,10 @@
<svg viewBox="0 0 520 80" xmlns="http://www.w3.org/2000/svg">
<line x1="28" y1="8" x2="6" y2="68" stroke="#3B82F6" stroke-width="3.5" stroke-linecap="round"/>
<line x1="28" y1="8" x2="50" y2="68" stroke="#3B82F6" stroke-width="3.5" stroke-linecap="round"/>
<line x1="14" y1="46" x2="42" y2="46" stroke="#3B82F6" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="28" cy="8" r="4" fill="#3B82F6"/>
<circle cx="14" cy="46" r="2.5" fill="#EF4444"/>
<circle cx="28" cy="46" r="2.5" fill="#3B82F6"/>
<circle cx="42" cy="46" r="2.5" fill="#EF4444"/>
<text x="58" y="62" fill="#222222" font-family="-apple-system, system-ui, 'Segoe UI', sans-serif" font-size="52" font-weight="300" letter-spacing="1">rmature</text>
</svg>

After

Width:  |  Height:  |  Size: 737 B