V0.7.0 shell contract (#54)
All checks were successful
BIN
src/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
@@ -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 {
|
||||
|
||||
@@ -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
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/favicon-light-32.png
Normal file
|
After Width: | Height: | Size: 332 B |
@@ -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 |
BIN
src/icons/apple-touch-icon-b-dark.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/icons/apple-touch-icon-b-light.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/icons/apple-touch-icon-e-dark.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/icons/apple-touch-icon-e-light.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/icons/favicon-16-b-dark.png
Normal file
|
After Width: | Height: | Size: 218 B |
BIN
src/icons/favicon-16-b-light.png
Normal file
|
After Width: | Height: | Size: 214 B |
BIN
src/icons/favicon-16-e-dark.png
Normal file
|
After Width: | Height: | Size: 304 B |
BIN
src/icons/favicon-16-e-light.png
Normal file
|
After Width: | Height: | Size: 312 B |
BIN
src/icons/favicon-32-b-dark.png
Normal file
|
After Width: | Height: | Size: 363 B |
BIN
src/icons/favicon-32-b-light.png
Normal file
|
After Width: | Height: | Size: 332 B |
BIN
src/icons/favicon-32-e-dark.png
Normal file
|
After Width: | Height: | Size: 457 B |
BIN
src/icons/favicon-32-e-light.png
Normal file
|
After Width: | Height: | Size: 459 B |
20
src/icons/favicon-animated.svg
Normal 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
@@ -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 |
BIN
src/icons/icon-128-b-dark.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/icons/icon-128-b-light.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/icons/icon-128-e-dark.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/icons/icon-128-e-light.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/icons/icon-192-b-dark.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/icons/icon-192-b-light.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/icons/icon-192-e-dark.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/icons/icon-192-e-light.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/icons/icon-256-b-dark.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/icons/icon-256-b-light.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/icons/icon-256-e-dark.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/icons/icon-256-e-light.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/icons/icon-48-b-dark.png
Normal file
|
After Width: | Height: | Size: 496 B |
BIN
src/icons/icon-48-b-light.png
Normal file
|
After Width: | Height: | Size: 492 B |
BIN
src/icons/icon-48-e-dark.png
Normal file
|
After Width: | Height: | Size: 564 B |
BIN
src/icons/icon-48-e-light.png
Normal file
|
After Width: | Height: | Size: 559 B |
BIN
src/icons/icon-512-b-dark.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
src/icons/icon-512-b-light.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
src/icons/icon-512-e-dark.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src/icons/icon-512-e-light.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src/icons/icon-64-b-dark.png
Normal file
|
After Width: | Height: | Size: 626 B |
BIN
src/icons/icon-64-b-light.png
Normal file
|
After Width: | Height: | Size: 620 B |
BIN
src/icons/icon-64-e-dark.png
Normal file
|
After Width: | Height: | Size: 711 B |
BIN
src/icons/icon-64-e-light.png
Normal file
|
After Width: | Height: | Size: 707 B |
48
src/icons/icon-b-dark-animated.svg
Normal 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
@@ -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 |
43
src/icons/icon-b-light-animated.svg
Normal 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 |
17
src/icons/icon-b-light.svg
Normal 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 |
67
src/icons/icon-e-dark-animated.svg
Normal 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
@@ -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 |
63
src/icons/icon-e-light-animated.svg
Normal 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 |
19
src/icons/icon-e-light.svg
Normal 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 |
174
src/icons/icon-morph-dark-animated.svg
Normal 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 |
127
src/icons/icon-morph-light-animated.svg
Normal 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
|
After Width: | Height: | Size: 15 KiB |
12
src/icons/wordmark-dark.svg
Normal 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/icons/wordmark-light.svg
Normal 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 |
@@ -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`);
|
||||
|
||||
@@ -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 ──────────
|
||||
|
||||
95
src/js/sw/shell/shell-topbar.js
Normal 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>
|
||||
`;
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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;">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
`)}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 |