Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
This commit is contained in:
18
team-portal/index.html
Normal file
18
team-portal/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DSS Team Portal POC</title>
|
||||
<link rel="stylesheet" href="src/css/style.css">
|
||||
<link rel="stylesheet" href="src/css/poc-theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="ui-container" class="ui-container">
|
||||
<h1>2D Workbench</h1>
|
||||
<button id="toggle-view-btn">Toggle View</button>
|
||||
</div>
|
||||
<canvas id="webgl-canvas"></canvas>
|
||||
<script type="module" src="src/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1006
team-portal/package-lock.json
generated
Normal file
1006
team-portal/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
team-portal/package.json
Normal file
20
team-portal/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "dss-team-portal",
|
||||
"version": "1.0.0",
|
||||
"description": "DSS Team Portal - Immersive dashboard experience",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"gsap": "^3.13.0",
|
||||
"three": "^0.160.0",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10"
|
||||
}
|
||||
}
|
||||
29
team-portal/public/favicon.svg
Normal file
29
team-portal/public/favicon.svg
Normal file
@@ -0,0 +1,29 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<linearGradient id="glow" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#00d4aa;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#7c3aed;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<filter id="blur">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="2" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Glow background -->
|
||||
<circle cx="50" cy="50" r="40" fill="url(#glow)" opacity="0.3" filter="url(#blur)"/>
|
||||
|
||||
<!-- Main circle -->
|
||||
<circle cx="50" cy="50" r="35" fill="none" stroke="url(#glow)" stroke-width="3"/>
|
||||
|
||||
<!-- Inner elements representing consciousness -->
|
||||
<circle cx="50" cy="50" r="20" fill="none" stroke="#00d4aa" stroke-width="2" opacity="0.8"/>
|
||||
<circle cx="50" cy="50" r="10" fill="none" stroke="#7c3aed" stroke-width="2" opacity="0.6"/>
|
||||
<circle cx="50" cy="50" r="4" fill="#00d4aa"/>
|
||||
|
||||
<!-- Orbital dots -->
|
||||
<circle cx="50" cy="20" r="3" fill="#00d4aa"/>
|
||||
<circle cx="75" cy="40" r="2.5" fill="#7c3aed"/>
|
||||
<circle cx="70" cy="70" r="2" fill="#00d4aa"/>
|
||||
<circle cx="35" cy="75" r="2.5" fill="#7c3aed"/>
|
||||
<circle cx="25" cy="45" r="2" fill="#00d4aa"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
108
team-portal/server.js
Normal file
108
team-portal/server.js
Normal file
@@ -0,0 +1,108 @@
|
||||
// Production server for DSS Team Portal
|
||||
import { createServer } from 'http';
|
||||
import { readFile, stat } from 'fs/promises';
|
||||
import { join, extname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const DIST_DIR = join(__dirname, 'dist');
|
||||
const PORT = process.env.PORT || 3457;
|
||||
const API_PROXY = process.env.API_PROXY || 'http://localhost:3456';
|
||||
|
||||
const MIME_TYPES = {
|
||||
'.html': 'text/html',
|
||||
'.css': 'text/css',
|
||||
'.js': 'application/javascript',
|
||||
'.json': 'application/json',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.ico': 'image/x-icon',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2'
|
||||
};
|
||||
|
||||
async function serveFile(res, filePath) {
|
||||
try {
|
||||
const content = await readFile(filePath);
|
||||
const ext = extname(filePath);
|
||||
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=31536000'
|
||||
});
|
||||
res.end(content);
|
||||
} catch (err) {
|
||||
// File not found, serve index.html for SPA routing
|
||||
const indexPath = join(DIST_DIR, 'index.html');
|
||||
const content = await readFile(indexPath);
|
||||
res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
|
||||
res.end(content);
|
||||
}
|
||||
}
|
||||
|
||||
async function proxyRequest(req, res) {
|
||||
const url = new URL(req.url, `http://localhost:${PORT}`);
|
||||
const proxyUrl = `${API_PROXY}${url.pathname}${url.search}`;
|
||||
|
||||
try {
|
||||
const proxyRes = await fetch(proxyUrl, {
|
||||
method: req.method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...Object.fromEntries(
|
||||
Object.entries(req.headers).filter(([k]) => !['host', 'connection'].includes(k))
|
||||
)
|
||||
},
|
||||
body: ['POST', 'PUT', 'PATCH'].includes(req.method)
|
||||
? await getRequestBody(req)
|
||||
: undefined
|
||||
});
|
||||
|
||||
const data = await proxyRes.text();
|
||||
res.writeHead(proxyRes.status, { 'Content-Type': 'application/json' });
|
||||
res.end(data);
|
||||
} catch (err) {
|
||||
console.error('[Proxy Error]', err.message);
|
||||
res.writeHead(502, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Proxy error', message: err.message }));
|
||||
}
|
||||
}
|
||||
|
||||
function getRequestBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk);
|
||||
req.on('end', () => resolve(body));
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
const url = new URL(req.url, `http://localhost:${PORT}`);
|
||||
|
||||
// Health check
|
||||
if (url.pathname === '/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'ok', service: 'dss-team-portal' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Proxy API requests to DSS MCP server
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
url.pathname = url.pathname.replace('/api', '');
|
||||
req.url = url.pathname + url.search;
|
||||
await proxyRequest(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// Serve static files
|
||||
const filePath = join(DIST_DIR, url.pathname === '/' ? 'index.html' : url.pathname);
|
||||
await serveFile(res, filePath);
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`[DSS Team Portal] Running on http://localhost:${PORT}`);
|
||||
console.log(`[DSS Team Portal] API Proxy: ${API_PROXY}`);
|
||||
});
|
||||
85
team-portal/src/css/base.css
Normal file
85
team-portal/src/css/base.css
Normal file
@@ -0,0 +1,85 @@
|
||||
/* Base Reset and Typography */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--dss-font-sans);
|
||||
background: var(--dss-bg-void);
|
||||
color: var(--dss-text-primary);
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--dss-accent-pulse);
|
||||
text-decoration: none;
|
||||
transition: opacity var(--dss-duration-response) var(--dss-ease-smooth);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Focus styles */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--dss-accent-pulse);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--dss-bg-space);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--dss-text-muted);
|
||||
border-radius: var(--dss-radius-full);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--dss-text-secondary);
|
||||
}
|
||||
181
team-portal/src/css/conscious.css
Normal file
181
team-portal/src/css/conscious.css
Normal file
@@ -0,0 +1,181 @@
|
||||
/* Conscious Being Animation Styles */
|
||||
#conscious-canvas {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.landing {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.landing__content {
|
||||
text-align: center;
|
||||
padding: var(--dss-space-8);
|
||||
}
|
||||
|
||||
/* Team Cards */
|
||||
.team-cards {
|
||||
display: flex;
|
||||
gap: var(--dss-space-6);
|
||||
justify-content: center;
|
||||
margin-bottom: var(--dss-space-12);
|
||||
}
|
||||
|
||||
.team-card {
|
||||
width: 200px;
|
||||
padding: var(--dss-space-8) var(--dss-space-6);
|
||||
background: rgba(var(--dss-accent-pulse-rgb), 0.05);
|
||||
border: 1px solid var(--dss-border);
|
||||
border-radius: var(--dss-radius-xl);
|
||||
cursor: pointer;
|
||||
transition: all var(--dss-duration-response) var(--dss-ease-organic);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.team-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(
|
||||
circle at center,
|
||||
rgba(var(--dss-accent-pulse-rgb), 0.15) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity var(--dss-duration-response) var(--dss-ease-smooth);
|
||||
}
|
||||
|
||||
.team-card:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: var(--dss-accent-pulse);
|
||||
box-shadow: var(--dss-glow-medium);
|
||||
}
|
||||
|
||||
.team-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.team-card__icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto var(--dss-space-4);
|
||||
color: var(--dss-accent-pulse);
|
||||
}
|
||||
|
||||
.team-card__icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.team-card h2 {
|
||||
font-size: var(--dss-text-xl);
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--dss-space-2);
|
||||
color: var(--dss-text-primary);
|
||||
}
|
||||
|
||||
.team-card p {
|
||||
font-size: var(--dss-text-sm);
|
||||
color: var(--dss-text-secondary);
|
||||
}
|
||||
|
||||
/* Team-specific colors */
|
||||
.team-card[data-team="ui"]:hover {
|
||||
--dss-accent-pulse-rgb: 0, 212, 170;
|
||||
border-color: #00d4aa;
|
||||
}
|
||||
|
||||
.team-card[data-team="ux"]:hover {
|
||||
--dss-accent-pulse-rgb: 124, 58, 237;
|
||||
border-color: #7c3aed;
|
||||
}
|
||||
|
||||
.team-card[data-team="qa"]:hover {
|
||||
--dss-accent-pulse-rgb: 245, 158, 11;
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
/* Admin Link */
|
||||
.admin-link {
|
||||
display: inline-block;
|
||||
font-size: var(--dss-text-sm);
|
||||
color: var(--dss-text-muted);
|
||||
padding: var(--dss-space-2) var(--dss-space-4);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--dss-radius);
|
||||
transition: all var(--dss-duration-response) var(--dss-ease-smooth);
|
||||
}
|
||||
|
||||
.admin-link:hover {
|
||||
color: var(--dss-text-secondary);
|
||||
border-color: var(--dss-border);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Breathing animation for the being */
|
||||
@keyframes breathe {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pulse animation for cards on hover */
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
box-shadow: var(--dss-glow-soft);
|
||||
}
|
||||
50% {
|
||||
box-shadow: var(--dss-glow-intense);
|
||||
}
|
||||
}
|
||||
|
||||
.team-card:hover {
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Page transition */
|
||||
.landing.transitioning {
|
||||
animation: fade-out var(--dss-duration-transition) var(--dss-ease-smooth) forwards;
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard.entering {
|
||||
animation: fade-in var(--dss-duration-transition) var(--dss-ease-smooth) forwards;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
477
team-portal/src/css/dashboard.css
Normal file
477
team-portal/src/css/dashboard.css
Normal file
@@ -0,0 +1,477 @@
|
||||
/* Dashboard Workbench Styles */
|
||||
.dashboard {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--dss-bg-void);
|
||||
}
|
||||
|
||||
/* Dashboard Header */
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--dss-space-4) var(--dss-space-6);
|
||||
background: var(--dss-bg-surface);
|
||||
border-bottom: 1px solid var(--dss-border);
|
||||
}
|
||||
|
||||
.dashboard-header__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--dss-space-4);
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--dss-space-2);
|
||||
padding: var(--dss-space-2) var(--dss-space-3);
|
||||
color: var(--dss-text-secondary);
|
||||
border-radius: var(--dss-radius);
|
||||
transition: all var(--dss-duration-response) var(--dss-ease-smooth);
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: var(--dss-bg-elevated);
|
||||
color: var(--dss-text-primary);
|
||||
}
|
||||
|
||||
.back-button svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.team-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--dss-space-2);
|
||||
}
|
||||
|
||||
.team-indicator__dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.team-indicator__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: var(--dss-radius-full);
|
||||
background: var(--dss-text-muted);
|
||||
transition: background var(--dss-duration-response) var(--dss-ease-smooth);
|
||||
}
|
||||
|
||||
.team-indicator__dot.active {
|
||||
background: var(--dss-accent-pulse);
|
||||
}
|
||||
|
||||
.team-indicator__name {
|
||||
font-size: var(--dss-text-lg);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.dashboard-header__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--dss-space-3);
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--dss-radius);
|
||||
color: var(--dss-text-secondary);
|
||||
transition: all var(--dss-duration-response) var(--dss-ease-smooth);
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
background: var(--dss-bg-elevated);
|
||||
color: var(--dss-text-primary);
|
||||
}
|
||||
|
||||
.icon-button svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Metrics Bar */
|
||||
.metrics-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--dss-space-8);
|
||||
padding: var(--dss-space-6);
|
||||
background: var(--dss-bg-space);
|
||||
border-bottom: 1px solid var(--dss-border);
|
||||
}
|
||||
|
||||
.metric {
|
||||
text-align: center;
|
||||
padding: var(--dss-space-4) var(--dss-space-6);
|
||||
background: var(--dss-bg-surface);
|
||||
border-radius: var(--dss-radius-lg);
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.metric__label {
|
||||
font-size: var(--dss-text-xs);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--dss-text-muted);
|
||||
margin-bottom: var(--dss-space-2);
|
||||
}
|
||||
|
||||
.metric__value {
|
||||
font-size: var(--dss-text-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--dss-text-primary);
|
||||
}
|
||||
|
||||
.metric__value--success {
|
||||
color: var(--dss-accent-pulse);
|
||||
}
|
||||
|
||||
.metric__value--warning {
|
||||
color: var(--dss-accent-alert);
|
||||
}
|
||||
|
||||
/* Action Zone */
|
||||
.action-zone {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--dss-space-8);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.action-zone__content {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-zone__title {
|
||||
font-size: var(--dss-text-3xl);
|
||||
font-weight: 300;
|
||||
color: var(--dss-text-secondary);
|
||||
margin-bottom: var(--dss-space-4);
|
||||
}
|
||||
|
||||
.action-zone__subtitle {
|
||||
font-size: var(--dss-text-base);
|
||||
color: var(--dss-text-muted);
|
||||
}
|
||||
|
||||
/* Action Dock */
|
||||
.action-dock {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--dss-space-4);
|
||||
padding: var(--dss-space-6);
|
||||
background: var(--dss-bg-surface);
|
||||
border-top: 1px solid var(--dss-border);
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--dss-space-2);
|
||||
padding: var(--dss-space-4) var(--dss-space-6);
|
||||
background: var(--dss-bg-elevated);
|
||||
border: 1px solid var(--dss-border);
|
||||
border-radius: var(--dss-radius-lg);
|
||||
color: var(--dss-text-secondary);
|
||||
min-width: 120px;
|
||||
transition: all var(--dss-duration-response) var(--dss-ease-smooth);
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background: var(--dss-bg-space);
|
||||
border-color: var(--dss-accent-pulse);
|
||||
color: var(--dss-text-primary);
|
||||
box-shadow: var(--dss-glow-soft);
|
||||
}
|
||||
|
||||
.action-button svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--dss-accent-pulse);
|
||||
}
|
||||
|
||||
.action-button__label {
|
||||
font-size: var(--dss-text-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Team-specific accent colors */
|
||||
.dashboard[data-team="ui"] {
|
||||
--dss-accent-pulse: #00d4aa;
|
||||
--dss-accent-pulse-rgb: 0, 212, 170;
|
||||
}
|
||||
|
||||
.dashboard[data-team="ux"] {
|
||||
--dss-accent-pulse: #7c3aed;
|
||||
--dss-accent-pulse-rgb: 124, 58, 237;
|
||||
}
|
||||
|
||||
.dashboard[data-team="qa"] {
|
||||
--dss-accent-pulse: #f59e0b;
|
||||
--dss-accent-pulse-rgb: 245, 158, 11;
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--dss-space-3);
|
||||
color: var(--dss-text-muted);
|
||||
}
|
||||
|
||||
.loading__spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--dss-border);
|
||||
border-top-color: var(--dss-accent-pulse);
|
||||
border-radius: var(--dss-radius-full);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Exit transitions */
|
||||
.dashboard.exiting {
|
||||
animation: dashboardExit 0.3s var(--dss-ease-smooth) forwards;
|
||||
}
|
||||
|
||||
@keyframes dashboardExit {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
/* WebGL Fallback Styles */
|
||||
.conscious-fallback {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fallback-orb {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, var(--dss-accent-pulse) 0%, transparent 70%);
|
||||
animation: fallbackPulse var(--dss-duration-breath) ease-in-out infinite;
|
||||
}
|
||||
|
||||
.fallback-ring {
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border: 2px solid var(--dss-accent-pulse);
|
||||
border-radius: 50%;
|
||||
opacity: 0.3;
|
||||
animation: fallbackRing var(--dss-duration-breath) ease-in-out infinite;
|
||||
}
|
||||
|
||||
.fallback-ring--delayed {
|
||||
animation-delay: calc(var(--dss-duration-breath) / 2);
|
||||
}
|
||||
|
||||
@keyframes fallbackPulse {
|
||||
0%, 100% {
|
||||
transform: scale(0.9);
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fallbackRing {
|
||||
0%, 100% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.2;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
/* Toast Notifications */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: var(--dss-space-6);
|
||||
right: var(--dss-space-6);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--dss-space-3);
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--dss-space-3);
|
||||
padding: var(--dss-space-4) var(--dss-space-5);
|
||||
background: var(--dss-bg-elevated);
|
||||
border: 1px solid var(--dss-border);
|
||||
border-radius: var(--dss-radius-lg);
|
||||
box-shadow: var(--dss-glow-soft);
|
||||
animation: toastIn 0.3s var(--dss-ease-organic);
|
||||
min-width: 280px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.toast--success {
|
||||
border-color: var(--dss-accent-pulse);
|
||||
}
|
||||
|
||||
.toast--error {
|
||||
border-color: var(--dss-accent-error);
|
||||
}
|
||||
|
||||
.toast--warning {
|
||||
border-color: var(--dss-accent-alert);
|
||||
}
|
||||
|
||||
.toast__icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toast--success .toast__icon {
|
||||
color: var(--dss-accent-pulse);
|
||||
}
|
||||
|
||||
.toast--error .toast__icon {
|
||||
color: var(--dss-accent-error);
|
||||
}
|
||||
|
||||
.toast--warning .toast__icon {
|
||||
color: var(--dss-accent-alert);
|
||||
}
|
||||
|
||||
.toast__message {
|
||||
flex: 1;
|
||||
font-size: var(--dss-text-sm);
|
||||
color: var(--dss-text-primary);
|
||||
}
|
||||
|
||||
.toast__close {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--dss-text-muted);
|
||||
cursor: pointer;
|
||||
transition: color var(--dss-duration-response);
|
||||
}
|
||||
|
||||
.toast__close:hover {
|
||||
color: var(--dss-text-primary);
|
||||
}
|
||||
|
||||
.toast.exiting {
|
||||
animation: toastOut 0.2s var(--dss-ease-smooth) forwards;
|
||||
}
|
||||
|
||||
@keyframes toastIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toastOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Error state for metrics */
|
||||
.metric__value--error {
|
||||
color: var(--dss-accent-error);
|
||||
}
|
||||
|
||||
/* Action button states */
|
||||
.action-button.loading {
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.action-button.loading svg {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.action-button.success {
|
||||
border-color: var(--dss-accent-pulse);
|
||||
box-shadow: var(--dss-glow-soft);
|
||||
}
|
||||
|
||||
.action-button.error {
|
||||
border-color: var(--dss-accent-error);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.team-cards {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.team-card {
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.metrics-bar {
|
||||
flex-wrap: wrap;
|
||||
gap: var(--dss-space-4);
|
||||
}
|
||||
|
||||
.metric {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.action-dock {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
25
team-portal/src/css/poc-theme.css
Normal file
25
team-portal/src/css/poc-theme.css
Normal file
@@ -0,0 +1,25 @@
|
||||
/* DSS Team Portal - POC Theme */
|
||||
|
||||
:root {
|
||||
--dss-color-primary-500: #3b82f6;
|
||||
--dss-color-neutral-100: #f3f4f6;
|
||||
--dss-color-neutral-900: #111827;
|
||||
|
||||
--color-primary: var(--dss-color-primary-500);
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--bg-glass: rgba(31, 41, 55, 0.5);
|
||||
--border-glass: rgba(255, 255, 255, 0.1);
|
||||
--text-primary: var(--dss-color-neutral-100);
|
||||
--text-secondary: #9ca3af;
|
||||
--bg-page: var(--dss-color-neutral-900);
|
||||
}
|
||||
|
||||
[data-theme='light'] {
|
||||
--bg-glass: rgba(255, 255, 255, 0.5);
|
||||
--border-glass: rgba(0, 0, 0, 0.1);
|
||||
--text-primary: var(--dss-color-neutral-900);
|
||||
--text-secondary: #4b5563;
|
||||
--bg-page: var(--dss-color-neutral-100);
|
||||
}
|
||||
32
team-portal/src/css/style.css
Normal file
32
team-portal/src/css/style.css
Normal file
@@ -0,0 +1,32 @@
|
||||
/* DSS Team Portal - Main Styles */
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
background-color: var(--bg-page);
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#webgl-canvas {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
outline: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ui-container {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 2rem;
|
||||
border-radius: 1rem;
|
||||
background-color: var(--bg-glass);
|
||||
border: 1px solid var(--border-glass);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
}
|
||||
90
team-portal/src/css/tokens.css
Normal file
90
team-portal/src/css/tokens.css
Normal file
@@ -0,0 +1,90 @@
|
||||
/* DSS Workbench Theme Tokens */
|
||||
:root {
|
||||
/* Deep Space Backgrounds */
|
||||
--dss-bg-void: #0a0a0f;
|
||||
--dss-bg-space: #0d1117;
|
||||
--dss-bg-surface: #161b22;
|
||||
--dss-bg-elevated: #1c2128;
|
||||
|
||||
/* Bioluminescent Accents */
|
||||
--dss-accent-pulse: #00d4aa;
|
||||
--dss-accent-pulse-rgb: 0, 212, 170;
|
||||
--dss-accent-aware: #7c3aed;
|
||||
--dss-accent-aware-rgb: 124, 58, 237;
|
||||
--dss-accent-alert: #f59e0b;
|
||||
--dss-accent-error: #ef4444;
|
||||
|
||||
/* Text Hierarchy */
|
||||
--dss-text-primary: #f0f6fc;
|
||||
--dss-text-secondary: #8b949e;
|
||||
--dss-text-muted: #484f58;
|
||||
|
||||
/* Borders */
|
||||
--dss-border: rgba(240, 246, 252, 0.1);
|
||||
--dss-border-focus: rgba(0, 212, 170, 0.4);
|
||||
|
||||
/* Glow Effects */
|
||||
--dss-glow-soft: 0 0 20px rgba(0, 212, 170, 0.15);
|
||||
--dss-glow-medium: 0 0 40px rgba(0, 212, 170, 0.25);
|
||||
--dss-glow-intense: 0 0 60px rgba(0, 212, 170, 0.4);
|
||||
|
||||
/* Motion */
|
||||
--dss-ease-organic: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
--dss-ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--dss-duration-breath: 4s;
|
||||
--dss-duration-response: 0.3s;
|
||||
--dss-duration-transition: 0.5s;
|
||||
|
||||
/* Spacing */
|
||||
--dss-space-1: 0.25rem;
|
||||
--dss-space-2: 0.5rem;
|
||||
--dss-space-3: 0.75rem;
|
||||
--dss-space-4: 1rem;
|
||||
--dss-space-5: 1.25rem;
|
||||
--dss-space-6: 1.5rem;
|
||||
--dss-space-8: 2rem;
|
||||
--dss-space-10: 2.5rem;
|
||||
--dss-space-12: 3rem;
|
||||
|
||||
/* Typography */
|
||||
--dss-font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--dss-font-mono: ui-monospace, 'SF Mono', 'Cascadia Code', monospace;
|
||||
--dss-text-xs: 0.75rem;
|
||||
--dss-text-sm: 0.875rem;
|
||||
--dss-text-base: 1rem;
|
||||
--dss-text-lg: 1.125rem;
|
||||
--dss-text-xl: 1.25rem;
|
||||
--dss-text-2xl: 1.5rem;
|
||||
--dss-text-3xl: 2rem;
|
||||
|
||||
/* Radius */
|
||||
--dss-radius-sm: 0.25rem;
|
||||
--dss-radius: 0.5rem;
|
||||
--dss-radius-lg: 0.75rem;
|
||||
--dss-radius-xl: 1rem;
|
||||
--dss-radius-full: 9999px;
|
||||
}
|
||||
|
||||
/* Light Mode */
|
||||
[data-theme="light"] {
|
||||
--dss-bg-void: #fafbfc;
|
||||
--dss-bg-space: #f6f8fa;
|
||||
--dss-bg-surface: #ffffff;
|
||||
--dss-bg-elevated: #ffffff;
|
||||
|
||||
--dss-accent-pulse: #059669;
|
||||
--dss-accent-pulse-rgb: 5, 150, 105;
|
||||
--dss-accent-aware: #6d28d9;
|
||||
--dss-accent-aware-rgb: 109, 40, 217;
|
||||
|
||||
--dss-text-primary: #1f2937;
|
||||
--dss-text-secondary: #4b5563;
|
||||
--dss-text-muted: #9ca3af;
|
||||
|
||||
--dss-border: rgba(31, 41, 55, 0.1);
|
||||
--dss-border-focus: rgba(5, 150, 105, 0.4);
|
||||
|
||||
--dss-glow-soft: 0 0 20px rgba(5, 150, 105, 0.1);
|
||||
--dss-glow-medium: 0 0 40px rgba(5, 150, 105, 0.15);
|
||||
--dss-glow-intense: 0 0 60px rgba(5, 150, 105, 0.25);
|
||||
}
|
||||
75
team-portal/src/js/bridge.js
Normal file
75
team-portal/src/js/bridge.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// DSS Team Portal - State Bridge & Token Synchronizer
|
||||
import { subscribe } from './store.js';
|
||||
import { camera } from './scene.js';
|
||||
import { updateCrystalColor } from './scene.js';
|
||||
import gsap from 'gsap';
|
||||
|
||||
const uiContainer = document.getElementById('ui-container');
|
||||
|
||||
function initializeBridge() {
|
||||
// 1. StateBridge: Subscribe to store changes and trigger animations
|
||||
subscribe((state) => {
|
||||
const { viewState } = state;
|
||||
|
||||
if (viewState === 'TRANSITION_TO_2D') {
|
||||
gsap.to(camera.position, {
|
||||
duration: 1.5,
|
||||
z: 10, // Dolly out
|
||||
ease: 'power2.inOut',
|
||||
});
|
||||
gsap.to(uiContainer, {
|
||||
duration: 1.5,
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
ease: 'power2.inOut',
|
||||
onComplete: () => store.getState().actions.setViewState('2D_WORKBENCH')
|
||||
});
|
||||
}
|
||||
|
||||
if (viewState === 'TRANSITION_TO_3D') {
|
||||
gsap.to(camera.position, {
|
||||
duration: 1.5,
|
||||
z: 5, // Dolly in
|
||||
ease: 'power2.inOut',
|
||||
});
|
||||
gsap.to(uiContainer, {
|
||||
duration: 1.5,
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
ease: 'power2.inOut',
|
||||
onComplete: () => store.getState().actions.setViewState('3D_HOME')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Set initial UI state
|
||||
gsap.set(uiContainer, { opacity: 0, y: 20 });
|
||||
}
|
||||
|
||||
function initializeTokenSynchronizer() {
|
||||
// 2. TokenSynchronizer: Sync theme colors to the 3D scene
|
||||
const sync = () => {
|
||||
const style = getComputedStyle(document.body);
|
||||
const primaryColor = style.getPropertyValue('--color-primary').trim();
|
||||
if (primaryColor) {
|
||||
updateCrystalColor(primaryColor);
|
||||
}
|
||||
};
|
||||
|
||||
// Sync on init
|
||||
sync();
|
||||
|
||||
// Sync when theme changes
|
||||
subscribe((state, prevState) => {
|
||||
if (state.theme !== prevState.theme) {
|
||||
document.body.dataset.theme = state.theme;
|
||||
// Allow time for CSS to apply
|
||||
setTimeout(sync, 50);
|
||||
}
|
||||
});
|
||||
|
||||
// Set initial theme
|
||||
document.body.dataset.theme = store.getState().theme;
|
||||
}
|
||||
|
||||
export { initializeBridge, initializeTokenSynchronizer };
|
||||
59
team-portal/src/js/components/ActionDock.js
Normal file
59
team-portal/src/js/components/ActionDock.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// Action Dock Component
|
||||
export function ActionDock({ actions, onAction }) {
|
||||
return `
|
||||
<nav class="action-dock">
|
||||
${actions.map(action => ActionButton(action, onAction)).join('')}
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
|
||||
export function ActionButton({ id, label, icon }) {
|
||||
return `
|
||||
<button class="action-button" data-action="${id}">
|
||||
${icon}
|
||||
<span class="action-button__label">${label}</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
// Common action icons
|
||||
export const ActionIcons = {
|
||||
extract: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<circle cx="12" cy="12" r="4"/>
|
||||
</svg>`,
|
||||
|
||||
sync: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M23 4v6h-6"/>
|
||||
<path d="M1 20v-6h6"/>
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
||||
</svg>`,
|
||||
|
||||
code: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="16 18 22 12 16 6"/>
|
||||
<polyline points="8 6 2 12 8 18"/>
|
||||
</svg>`,
|
||||
|
||||
validate: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||
</svg>`,
|
||||
|
||||
search: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>`,
|
||||
|
||||
report: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
</svg>`,
|
||||
|
||||
lightning: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
||||
</svg>`
|
||||
};
|
||||
|
||||
export default ActionDock;
|
||||
27
team-portal/src/js/components/Header.js
Normal file
27
team-portal/src/js/components/Header.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// Header Component
|
||||
export function Header({ team, onBack, onThemeToggle }) {
|
||||
return `
|
||||
<header class="dashboard-header">
|
||||
<div class="dashboard-header__left">
|
||||
<button class="back-button" onclick="${onBack}" aria-label="Back to home">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
<span>back</span>
|
||||
</button>
|
||||
<div class="team-indicator">
|
||||
<span class="team-indicator__name">${team.toUpperCase()} TEAM</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-header__right">
|
||||
<button class="icon-button" onclick="${onThemeToggle}" aria-label="Toggle theme">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
`;
|
||||
}
|
||||
|
||||
export default Header;
|
||||
28
team-portal/src/js/components/MetricsBar.js
Normal file
28
team-portal/src/js/components/MetricsBar.js
Normal file
@@ -0,0 +1,28 @@
|
||||
// Metrics Bar Component
|
||||
export function MetricsBar({ metrics }) {
|
||||
const entries = Object.entries(metrics);
|
||||
|
||||
return `
|
||||
<div class="metrics-bar">
|
||||
${entries.map(([label, value]) => `
|
||||
<div class="metric">
|
||||
<div class="metric__label">${label}</div>
|
||||
<div class="metric__value">${value}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function Metric({ label, value, status }) {
|
||||
const statusClass = status ? `metric__value--${status}` : '';
|
||||
|
||||
return `
|
||||
<div class="metric">
|
||||
<div class="metric__label">${label}</div>
|
||||
<div class="metric__value ${statusClass}">${value}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export default MetricsBar;
|
||||
47
team-portal/src/js/components/TeamCard.js
Normal file
47
team-portal/src/js/components/TeamCard.js
Normal file
@@ -0,0 +1,47 @@
|
||||
// Team Card Component
|
||||
export function TeamCard({ team, title, description, icon, onClick }) {
|
||||
return `
|
||||
<div class="team-card" data-team="${team}" onclick="${onClick}">
|
||||
<div class="team-card__icon">
|
||||
${icon}
|
||||
</div>
|
||||
<h2>${title}</h2>
|
||||
<p>${description}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Team configurations
|
||||
export const TeamConfigs = {
|
||||
ui: {
|
||||
title: 'UI Team',
|
||||
description: 'Components & Tokens',
|
||||
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
<path d="M3 9h18"/>
|
||||
<path d="M9 21V9"/>
|
||||
</svg>`,
|
||||
color: '#00d4aa'
|
||||
},
|
||||
ux: {
|
||||
title: 'UX Team',
|
||||
description: 'Consistency & Validation',
|
||||
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M12 16v-4"/>
|
||||
<path d="M12 8h.01"/>
|
||||
</svg>`,
|
||||
color: '#7c3aed'
|
||||
},
|
||||
qa: {
|
||||
title: 'QA Team',
|
||||
description: 'Quality & Testing',
|
||||
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||
</svg>`,
|
||||
color: '#f59e0b'
|
||||
}
|
||||
};
|
||||
|
||||
export default TeamCard;
|
||||
215
team-portal/src/js/conscious/Behaviors.js
Normal file
215
team-portal/src/js/conscious/Behaviors.js
Normal file
@@ -0,0 +1,215 @@
|
||||
// Particle Behaviors - Flocking, Breathing, Following
|
||||
import { lerp, distance, clamp } from '../utils/math.js';
|
||||
|
||||
// Base behavior class
|
||||
class Behavior {
|
||||
constructor() {
|
||||
this.enabled = true;
|
||||
this.strength = 1.0;
|
||||
}
|
||||
|
||||
apply(particleSystem, params) {
|
||||
// Override in subclasses
|
||||
}
|
||||
|
||||
onModeChange(newMode, oldMode) {
|
||||
// Override in subclasses
|
||||
}
|
||||
}
|
||||
|
||||
// Breathing behavior - organic pulsing
|
||||
export class BreathingBehavior extends Behavior {
|
||||
constructor() {
|
||||
super();
|
||||
this.frequency = 0.25; // Cycles per second (4s period)
|
||||
this.amplitude = 0.15; // 15% scale variation
|
||||
this.baseScale = 1.0;
|
||||
}
|
||||
|
||||
apply(particleSystem, params) {
|
||||
if (!this.enabled) return;
|
||||
|
||||
const { time } = params;
|
||||
const phase = Math.sin(time * this.frequency * Math.PI * 2 * 0.001);
|
||||
const scale = this.baseScale + phase * this.amplitude * this.strength;
|
||||
|
||||
// Apply breathing to all particles - expand/contract from center
|
||||
for (let i = 0; i < particleSystem.count; i++) {
|
||||
const pos = particleSystem.getPosition(i);
|
||||
const dist = Math.sqrt(pos.x * pos.x + pos.y * pos.y);
|
||||
|
||||
if (dist > 0.01) {
|
||||
const breathForce = (scale - 1) * 0.001;
|
||||
const forceX = (pos.x / dist) * breathForce;
|
||||
const forceY = (pos.y / dist) * breathForce;
|
||||
particleSystem.applyForce(i, forceX, forceY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onModeChange(newMode) {
|
||||
if (newMode === 'background') {
|
||||
this.strength = 0.3;
|
||||
} else {
|
||||
this.strength = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flocking behavior - boids algorithm
|
||||
export class FlockingBehavior extends Behavior {
|
||||
constructor() {
|
||||
super();
|
||||
this.separationRadius = 0.05;
|
||||
this.alignmentRadius = 0.1;
|
||||
this.cohesionRadius = 0.15;
|
||||
|
||||
this.separationWeight = 0.003;
|
||||
this.alignmentWeight = 0.001;
|
||||
this.cohesionWeight = 0.001;
|
||||
}
|
||||
|
||||
apply(particleSystem, params) {
|
||||
if (!this.enabled) return;
|
||||
|
||||
const count = particleSystem.count;
|
||||
|
||||
// Sample subset for performance
|
||||
const sampleSize = Math.min(100, count);
|
||||
const step = Math.floor(count / sampleSize);
|
||||
|
||||
for (let i = 0; i < count; i += step) {
|
||||
const pos = particleSystem.getPosition(i);
|
||||
let sepX = 0, sepY = 0, sepCount = 0;
|
||||
let alignX = 0, alignY = 0, alignCount = 0;
|
||||
let cohX = 0, cohY = 0, cohCount = 0;
|
||||
|
||||
// Check neighbors (sample for performance)
|
||||
for (let j = 0; j < count; j += step) {
|
||||
if (i === j) continue;
|
||||
|
||||
const other = particleSystem.getPosition(j);
|
||||
const dist = distance(pos.x, pos.y, other.x, other.y);
|
||||
|
||||
// Separation
|
||||
if (dist < this.separationRadius && dist > 0) {
|
||||
const factor = 1 - dist / this.separationRadius;
|
||||
sepX += (pos.x - other.x) / dist * factor;
|
||||
sepY += (pos.y - other.y) / dist * factor;
|
||||
sepCount++;
|
||||
}
|
||||
|
||||
// Alignment
|
||||
if (dist < this.alignmentRadius) {
|
||||
const vel = {
|
||||
x: particleSystem.velocities[j * 3],
|
||||
y: particleSystem.velocities[j * 3 + 1]
|
||||
};
|
||||
alignX += vel.x;
|
||||
alignY += vel.y;
|
||||
alignCount++;
|
||||
}
|
||||
|
||||
// Cohesion
|
||||
if (dist < this.cohesionRadius) {
|
||||
cohX += other.x;
|
||||
cohY += other.y;
|
||||
cohCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply forces
|
||||
let forceX = 0, forceY = 0;
|
||||
|
||||
if (sepCount > 0) {
|
||||
forceX += (sepX / sepCount) * this.separationWeight * this.strength;
|
||||
forceY += (sepY / sepCount) * this.separationWeight * this.strength;
|
||||
}
|
||||
|
||||
if (alignCount > 0) {
|
||||
const avgVelX = alignX / alignCount;
|
||||
const avgVelY = alignY / alignCount;
|
||||
forceX += avgVelX * this.alignmentWeight * this.strength;
|
||||
forceY += avgVelY * this.alignmentWeight * this.strength;
|
||||
}
|
||||
|
||||
if (cohCount > 0) {
|
||||
const centerX = cohX / cohCount;
|
||||
const centerY = cohY / cohCount;
|
||||
forceX += (centerX - pos.x) * this.cohesionWeight * this.strength;
|
||||
forceY += (centerY - pos.y) * this.cohesionWeight * this.strength;
|
||||
}
|
||||
|
||||
particleSystem.applyForce(i, forceX, forceY);
|
||||
}
|
||||
}
|
||||
|
||||
onModeChange(newMode) {
|
||||
if (newMode === 'background') {
|
||||
this.strength = 0.2;
|
||||
} else if (newMode === 'engaged') {
|
||||
this.strength = 0.5;
|
||||
} else {
|
||||
this.strength = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Follow behavior - follow mouse or target
|
||||
export class FollowBehavior extends Behavior {
|
||||
constructor() {
|
||||
super();
|
||||
this.mouseWeight = 0.0005;
|
||||
this.targetWeight = 0.002;
|
||||
this.returnWeight = 0.0001;
|
||||
}
|
||||
|
||||
apply(particleSystem, params) {
|
||||
if (!this.enabled) return;
|
||||
|
||||
const { mousePosition, target, mode } = params;
|
||||
|
||||
for (let i = 0; i < particleSystem.count; i++) {
|
||||
const pos = particleSystem.getPosition(i);
|
||||
let forceX = 0, forceY = 0;
|
||||
|
||||
// Convert mouse to scene coordinates (-1 to 1)
|
||||
const mouseX = (mousePosition.x - 0.5) * 2;
|
||||
const mouseY = (mousePosition.y - 0.5) * 2;
|
||||
|
||||
if (target && mode === 'engaged') {
|
||||
// Follow target
|
||||
const targetX = (target.x - 0.5) * 2;
|
||||
const targetY = (target.y - 0.5) * 2;
|
||||
forceX += (targetX - pos.x) * this.targetWeight * this.strength;
|
||||
forceY += (targetY - pos.y) * this.targetWeight * this.strength;
|
||||
} else if (mode !== 'background') {
|
||||
// Follow mouse
|
||||
forceX += (mouseX - pos.x) * this.mouseWeight * this.strength;
|
||||
forceY += (mouseY - pos.y) * this.mouseWeight * this.strength;
|
||||
}
|
||||
|
||||
// Return to center force (keep particles from drifting too far)
|
||||
const distFromCenter = Math.sqrt(pos.x * pos.x + pos.y * pos.y);
|
||||
if (distFromCenter > 0.5) {
|
||||
const returnStrength = (distFromCenter - 0.5) * this.returnWeight;
|
||||
forceX -= pos.x * returnStrength;
|
||||
forceY -= pos.y * returnStrength;
|
||||
}
|
||||
|
||||
particleSystem.applyForce(i, forceX, forceY);
|
||||
}
|
||||
}
|
||||
|
||||
onModeChange(newMode) {
|
||||
if (newMode === 'background') {
|
||||
this.strength = 0;
|
||||
} else if (newMode === 'engaged') {
|
||||
this.strength = 2.0;
|
||||
} else {
|
||||
this.strength = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default { BreathingBehavior, FlockingBehavior, FollowBehavior };
|
||||
201
team-portal/src/js/conscious/Being.js
Normal file
201
team-portal/src/js/conscious/Being.js
Normal file
@@ -0,0 +1,201 @@
|
||||
// The Conscious Being - Main Controller
|
||||
import { Renderer } from './Renderer.js';
|
||||
import { ParticleSystem } from './ParticleSystem.js';
|
||||
import { FlockingBehavior, BreathingBehavior, FollowBehavior } from './Behaviors.js';
|
||||
|
||||
export class Being {
|
||||
constructor(canvas) {
|
||||
this.canvas = canvas;
|
||||
this.renderer = null;
|
||||
this.particleSystem = null;
|
||||
this.behaviors = [];
|
||||
|
||||
this.mousePosition = { x: 0.5, y: 0.5 }; // Normalized 0-1
|
||||
this.target = null;
|
||||
this.mode = 'idle'; // idle, aware, engaged, background
|
||||
this.isRunning = false;
|
||||
this.lastTime = 0;
|
||||
this.initialized = false;
|
||||
|
||||
// Store bound handlers for cleanup
|
||||
this._resizeHandler = this.handleResize.bind(this);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
try {
|
||||
// Initialize renderer with WebGL error handling
|
||||
this.renderer = new Renderer(this.canvas);
|
||||
|
||||
// Initialize particle system
|
||||
this.particleSystem = new ParticleSystem(800);
|
||||
|
||||
// Initialize behaviors
|
||||
this.behaviors = [
|
||||
new BreathingBehavior(),
|
||||
new FlockingBehavior(),
|
||||
new FollowBehavior()
|
||||
];
|
||||
|
||||
// Add particle system to scene
|
||||
this.renderer.add(this.particleSystem.mesh);
|
||||
|
||||
// Handle resize
|
||||
window.addEventListener('resize', this._resizeHandler);
|
||||
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
console.error('[Being] WebGL initialization failed:', error);
|
||||
this.showFallback();
|
||||
}
|
||||
}
|
||||
|
||||
showFallback() {
|
||||
// Show CSS-based fallback animation when WebGL is unavailable
|
||||
if (this.canvas) {
|
||||
this.canvas.style.display = 'none';
|
||||
const fallback = document.createElement('div');
|
||||
fallback.className = 'conscious-fallback';
|
||||
fallback.innerHTML = `
|
||||
<div class="fallback-orb"></div>
|
||||
<div class="fallback-ring"></div>
|
||||
<div class="fallback-ring fallback-ring--delayed"></div>
|
||||
`;
|
||||
this.canvas.parentNode?.insertBefore(fallback, this.canvas);
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.isRunning || !this.initialized) return;
|
||||
this.isRunning = true;
|
||||
this.lastTime = performance.now();
|
||||
this.animate();
|
||||
console.log('[Being] Started');
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
console.log('[Being] Stopped');
|
||||
}
|
||||
|
||||
animate() {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
const currentTime = performance.now();
|
||||
const deltaTime = (currentTime - this.lastTime) / 1000; // Convert to seconds
|
||||
this.lastTime = currentTime;
|
||||
|
||||
this.update(deltaTime, currentTime);
|
||||
this.renderer.render();
|
||||
|
||||
requestAnimationFrame(() => this.animate());
|
||||
}
|
||||
|
||||
update(deltaTime, time) {
|
||||
// Apply behaviors based on mode
|
||||
const params = {
|
||||
time,
|
||||
deltaTime,
|
||||
mousePosition: this.mousePosition,
|
||||
target: this.target,
|
||||
mode: this.mode
|
||||
};
|
||||
|
||||
// Apply each behavior
|
||||
this.behaviors.forEach(behavior => {
|
||||
behavior.apply(this.particleSystem, params);
|
||||
});
|
||||
|
||||
// Update particle system
|
||||
this.particleSystem.update(deltaTime);
|
||||
|
||||
// Update uniforms
|
||||
this.particleSystem.material.uniforms.uTime.value = time * 0.001;
|
||||
this.particleSystem.material.uniforms.uMouse.value.set(
|
||||
this.mousePosition.x,
|
||||
this.mousePosition.y
|
||||
);
|
||||
}
|
||||
|
||||
updateMousePosition(x, y) {
|
||||
// Normalize to 0-1
|
||||
this.mousePosition = {
|
||||
x: x / window.innerWidth,
|
||||
y: 1 - (y / window.innerHeight) // Flip Y for WebGL
|
||||
};
|
||||
|
||||
// Update mode based on mouse activity
|
||||
if (this.mode === 'idle') {
|
||||
this.setMode('aware');
|
||||
}
|
||||
}
|
||||
|
||||
setTarget(element) {
|
||||
if (!element) {
|
||||
this.target = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
this.target = {
|
||||
x: (rect.left + rect.width / 2) / window.innerWidth,
|
||||
y: 1 - ((rect.top + rect.height / 2) / window.innerHeight),
|
||||
width: rect.width / window.innerWidth,
|
||||
height: rect.height / window.innerHeight
|
||||
};
|
||||
|
||||
this.setMode('engaged');
|
||||
}
|
||||
|
||||
clearTarget() {
|
||||
this.target = null;
|
||||
if (this.mode === 'engaged') {
|
||||
this.setMode('aware');
|
||||
}
|
||||
}
|
||||
|
||||
setMode(mode) {
|
||||
if (this.mode === mode) return;
|
||||
|
||||
const oldMode = this.mode;
|
||||
this.mode = mode;
|
||||
|
||||
console.log(`[Being] Mode: ${oldMode} -> ${mode}`);
|
||||
|
||||
// Adjust behaviors based on mode
|
||||
this.behaviors.forEach(behavior => {
|
||||
behavior.onModeChange(mode, oldMode);
|
||||
});
|
||||
|
||||
// Adjust particle appearance
|
||||
if (mode === 'background') {
|
||||
this.particleSystem.setOpacity(0.3);
|
||||
} else {
|
||||
this.particleSystem.setOpacity(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
this.renderer.handleResize();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.stop();
|
||||
|
||||
// Remove event listeners
|
||||
window.removeEventListener('resize', this._resizeHandler);
|
||||
|
||||
// Dispose of WebGL resources
|
||||
if (this.renderer) {
|
||||
this.renderer.dispose();
|
||||
}
|
||||
if (this.particleSystem) {
|
||||
this.particleSystem.dispose();
|
||||
}
|
||||
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
export default Being;
|
||||
193
team-portal/src/js/conscious/ParticleSystem.js
Normal file
193
team-portal/src/js/conscious/ParticleSystem.js
Normal file
@@ -0,0 +1,193 @@
|
||||
// WebGL Particle System with Custom Shaders
|
||||
import * as THREE from 'three';
|
||||
import { randomInRange, lerp } from '../utils/math.js';
|
||||
|
||||
export class ParticleSystem {
|
||||
constructor(count = 800) {
|
||||
this.count = count;
|
||||
this.positions = null;
|
||||
this.velocities = null;
|
||||
this.colors = null;
|
||||
this.sizes = null;
|
||||
this.geometry = null;
|
||||
this.material = null;
|
||||
this.mesh = null;
|
||||
|
||||
this.baseOpacity = 1.0;
|
||||
this.targetOpacity = 1.0;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Initialize arrays
|
||||
this.positions = new Float32Array(this.count * 3);
|
||||
this.velocities = new Float32Array(this.count * 3);
|
||||
this.colors = new Float32Array(this.count * 3);
|
||||
this.sizes = new Float32Array(this.count);
|
||||
|
||||
// Initialize particles in organic cluster
|
||||
this.initializeParticles();
|
||||
|
||||
// Create geometry
|
||||
this.geometry = new THREE.BufferGeometry();
|
||||
this.geometry.setAttribute('position', new THREE.BufferAttribute(this.positions, 3));
|
||||
this.geometry.setAttribute('color', new THREE.BufferAttribute(this.colors, 3));
|
||||
this.geometry.setAttribute('size', new THREE.BufferAttribute(this.sizes, 1));
|
||||
|
||||
// Create shader material
|
||||
this.material = this.createMaterial();
|
||||
|
||||
// Create points mesh
|
||||
this.mesh = new THREE.Points(this.geometry, this.material);
|
||||
}
|
||||
|
||||
initializeParticles() {
|
||||
const primaryColor = new THREE.Color(0x00d4aa); // Teal
|
||||
const secondaryColor = new THREE.Color(0x7c3aed); // Purple
|
||||
|
||||
for (let i = 0; i < this.count; i++) {
|
||||
const i3 = i * 3;
|
||||
|
||||
// Position in organic cluster (gaussian-like distribution)
|
||||
const radius = Math.pow(Math.random(), 0.5) * 0.5;
|
||||
const theta = Math.random() * Math.PI * 2;
|
||||
const phi = Math.acos(2 * Math.random() - 1);
|
||||
|
||||
this.positions[i3] = radius * Math.sin(phi) * Math.cos(theta);
|
||||
this.positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
|
||||
this.positions[i3 + 2] = 0; // Keep 2D for now
|
||||
|
||||
// Random velocities
|
||||
this.velocities[i3] = randomInRange(-0.001, 0.001);
|
||||
this.velocities[i3 + 1] = randomInRange(-0.001, 0.001);
|
||||
this.velocities[i3 + 2] = 0;
|
||||
|
||||
// Colors - mix between primary and secondary
|
||||
const mixRatio = Math.random();
|
||||
const color = primaryColor.clone().lerp(secondaryColor, mixRatio);
|
||||
this.colors[i3] = color.r;
|
||||
this.colors[i3 + 1] = color.g;
|
||||
this.colors[i3 + 2] = color.b;
|
||||
|
||||
// Sizes - varied for organic look
|
||||
this.sizes[i] = randomInRange(0.005, 0.02);
|
||||
}
|
||||
}
|
||||
|
||||
createMaterial() {
|
||||
const vertexShader = `
|
||||
attribute float size;
|
||||
attribute vec3 color;
|
||||
|
||||
uniform float uTime;
|
||||
uniform vec2 uMouse;
|
||||
uniform float uOpacity;
|
||||
|
||||
varying vec3 vColor;
|
||||
varying float vOpacity;
|
||||
|
||||
void main() {
|
||||
vColor = color;
|
||||
vOpacity = uOpacity;
|
||||
|
||||
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
||||
|
||||
// Size attenuation
|
||||
gl_PointSize = size * (300.0 / -mvPosition.z);
|
||||
gl_Position = projectionMatrix * mvPosition;
|
||||
}
|
||||
`;
|
||||
|
||||
const fragmentShader = `
|
||||
varying vec3 vColor;
|
||||
varying float vOpacity;
|
||||
|
||||
void main() {
|
||||
// Circular particle with soft edges
|
||||
float dist = length(gl_PointCoord - vec2(0.5));
|
||||
if (dist > 0.5) discard;
|
||||
|
||||
// Soft glow falloff
|
||||
float alpha = 1.0 - smoothstep(0.0, 0.5, dist);
|
||||
alpha *= vOpacity;
|
||||
|
||||
// Add inner glow
|
||||
float innerGlow = 1.0 - smoothstep(0.0, 0.3, dist);
|
||||
vec3 glowColor = vColor + vec3(0.2) * innerGlow;
|
||||
|
||||
gl_FragColor = vec4(glowColor, alpha * 0.8);
|
||||
}
|
||||
`;
|
||||
|
||||
return new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uMouse: { value: new THREE.Vector2(0.5, 0.5) },
|
||||
uOpacity: { value: 1.0 }
|
||||
},
|
||||
vertexShader,
|
||||
fragmentShader,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
blending: THREE.AdditiveBlending
|
||||
});
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
// Smooth opacity transition
|
||||
this.baseOpacity = lerp(this.baseOpacity, this.targetOpacity, 0.05);
|
||||
this.material.uniforms.uOpacity.value = this.baseOpacity;
|
||||
|
||||
// Update positions based on velocities
|
||||
for (let i = 0; i < this.count; i++) {
|
||||
const i3 = i * 3;
|
||||
|
||||
this.positions[i3] += this.velocities[i3];
|
||||
this.positions[i3 + 1] += this.velocities[i3 + 1];
|
||||
this.positions[i3 + 2] += this.velocities[i3 + 2];
|
||||
|
||||
// Apply friction
|
||||
this.velocities[i3] *= 0.99;
|
||||
this.velocities[i3 + 1] *= 0.99;
|
||||
this.velocities[i3 + 2] *= 0.99;
|
||||
}
|
||||
|
||||
// Mark geometry for update
|
||||
this.geometry.attributes.position.needsUpdate = true;
|
||||
}
|
||||
|
||||
setOpacity(opacity) {
|
||||
this.targetOpacity = opacity;
|
||||
}
|
||||
|
||||
applyForce(index, forceX, forceY, forceZ = 0) {
|
||||
const i3 = index * 3;
|
||||
this.velocities[i3] += forceX;
|
||||
this.velocities[i3 + 1] += forceY;
|
||||
this.velocities[i3 + 2] += forceZ;
|
||||
}
|
||||
|
||||
getPosition(index) {
|
||||
const i3 = index * 3;
|
||||
return {
|
||||
x: this.positions[i3],
|
||||
y: this.positions[i3 + 1],
|
||||
z: this.positions[i3 + 2]
|
||||
};
|
||||
}
|
||||
|
||||
setPosition(index, x, y, z = 0) {
|
||||
const i3 = index * 3;
|
||||
this.positions[i3] = x;
|
||||
this.positions[i3 + 1] = y;
|
||||
this.positions[i3 + 2] = z;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.geometry.dispose();
|
||||
this.material.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export default ParticleSystem;
|
||||
72
team-portal/src/js/conscious/Renderer.js
Normal file
72
team-portal/src/js/conscious/Renderer.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// Three.js Renderer Setup
|
||||
import * as THREE from 'three';
|
||||
|
||||
export class Renderer {
|
||||
constructor(canvas) {
|
||||
this.canvas = canvas;
|
||||
this.scene = null;
|
||||
this.camera = null;
|
||||
this.renderer = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Create scene
|
||||
this.scene = new THREE.Scene();
|
||||
|
||||
// Create camera (orthographic for 2D-like particles)
|
||||
const aspect = window.innerWidth / window.innerHeight;
|
||||
this.camera = new THREE.OrthographicCamera(
|
||||
-aspect, aspect, 1, -1, 0.1, 1000
|
||||
);
|
||||
this.camera.position.z = 1;
|
||||
|
||||
// Create renderer
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
canvas: this.canvas,
|
||||
antialias: true,
|
||||
alpha: true
|
||||
});
|
||||
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
this.renderer.setClearColor(0x000000, 0);
|
||||
|
||||
console.log('[Renderer] Initialized');
|
||||
}
|
||||
|
||||
add(object) {
|
||||
this.scene.add(object);
|
||||
}
|
||||
|
||||
remove(object) {
|
||||
this.scene.remove(object);
|
||||
}
|
||||
|
||||
render() {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
const aspect = width / height;
|
||||
|
||||
// Update camera
|
||||
this.camera.left = -aspect;
|
||||
this.camera.right = aspect;
|
||||
this.camera.updateProjectionMatrix();
|
||||
|
||||
// Update renderer
|
||||
this.renderer.setSize(width, height);
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.renderer.dispose();
|
||||
this.scene.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export default Renderer;
|
||||
177
team-portal/src/js/core/api.js
Normal file
177
team-portal/src/js/core/api.js
Normal file
@@ -0,0 +1,177 @@
|
||||
// API Client for DSS MCP Server
|
||||
const API_BASE = import.meta.env?.VITE_API_BASE || '/api';
|
||||
const isDev = import.meta.env?.DEV;
|
||||
|
||||
class APIClient {
|
||||
constructor(baseUrl = API_BASE) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.cache = new Map();
|
||||
this.cacheTimeout = 30000; // 30 seconds
|
||||
}
|
||||
|
||||
async request(endpoint, options = {}) {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
},
|
||||
...options
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, config);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new APIError(
|
||||
errorData.message || `API Error: ${response.status} ${response.statusText}`,
|
||||
response.status,
|
||||
errorData
|
||||
);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
if (error instanceof APIError) throw error;
|
||||
if (isDev) console.error('[API]', error);
|
||||
throw new APIError(error.message || 'Network error', 0, null);
|
||||
}
|
||||
}
|
||||
|
||||
async get(endpoint, useCache = false) {
|
||||
if (useCache) {
|
||||
const cached = this.cache.get(endpoint);
|
||||
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
|
||||
return cached.data;
|
||||
}
|
||||
}
|
||||
|
||||
const data = await this.request(endpoint, { method: 'GET' });
|
||||
|
||||
if (useCache) {
|
||||
this.cache.set(endpoint, { data, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async post(endpoint, data) {
|
||||
return this.request(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
// DSS-specific methods
|
||||
async getHealth() {
|
||||
return this.get('/health', true);
|
||||
}
|
||||
|
||||
async analyzeProject(path) {
|
||||
return this.post('/tools/dss_analyze_project', { path });
|
||||
}
|
||||
|
||||
async extractTokens(path, sources = ['css', 'scss']) {
|
||||
return this.post('/tools/dss_extract_tokens', { path, sources });
|
||||
}
|
||||
|
||||
async auditComponents(path) {
|
||||
return this.post('/tools/dss_audit_components', { path });
|
||||
}
|
||||
|
||||
async findQuickWins(path) {
|
||||
return this.post('/tools/dss_find_quick_wins', { path });
|
||||
}
|
||||
|
||||
async generateTheme(format, themeName = 'default') {
|
||||
return this.post('/tools/dss_generate_theme', { format, theme_name: themeName });
|
||||
}
|
||||
|
||||
async syncFigma(fileKey) {
|
||||
return this.post('/tools/dss_sync_figma', { file_key: fileKey });
|
||||
}
|
||||
|
||||
async setupStorybook(path, action = 'scan') {
|
||||
return this.post('/tools/dss_setup_storybook', { path, action });
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
return this.get('/status', true);
|
||||
}
|
||||
|
||||
async listThemes() {
|
||||
return this.get('/themes', true);
|
||||
}
|
||||
|
||||
// Aggregated metrics for dashboards
|
||||
async getTeamMetrics(team) {
|
||||
try {
|
||||
const status = await this.getStatus();
|
||||
|
||||
// Derive metrics from DSS status
|
||||
const baseMetrics = {
|
||||
tokens: status.token_count || 0,
|
||||
themes: status.themes?.length || 0,
|
||||
healthy: status.healthy || false
|
||||
};
|
||||
|
||||
switch (team) {
|
||||
case 'ui':
|
||||
return {
|
||||
components: status.component_count || 0,
|
||||
tokens: baseMetrics.tokens,
|
||||
synced: status.figma_connected || false,
|
||||
stories: status.storybook_stories || 0
|
||||
};
|
||||
case 'ux':
|
||||
return {
|
||||
consistency: status.consistency_score || 0,
|
||||
violations: status.violation_count || 0,
|
||||
patterns: status.pattern_count || 0,
|
||||
tokens: baseMetrics.tokens
|
||||
};
|
||||
case 'qa':
|
||||
return {
|
||||
health: status.health_score || 0,
|
||||
quickWins: status.quick_win_count || 0,
|
||||
unused: status.unused_count || 0,
|
||||
coverage: status.coverage_percent || 0
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
} catch (error) {
|
||||
if (isDev) console.warn('[API] Failed to fetch metrics, using defaults');
|
||||
// Return sensible defaults on error
|
||||
return this.getDefaultMetrics(team);
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultMetrics(team) {
|
||||
const defaults = {
|
||||
ui: { components: 0, tokens: 0, synced: false, stories: 0 },
|
||||
ux: { consistency: 0, violations: 0, patterns: 0, tokens: 0 },
|
||||
qa: { health: 0, quickWins: 0, unused: 0, coverage: 0 }
|
||||
};
|
||||
return defaults[team] || {};
|
||||
}
|
||||
}
|
||||
|
||||
// Custom API Error class
|
||||
class APIError extends Error {
|
||||
constructor(message, status, data) {
|
||||
super(message);
|
||||
this.name = 'APIError';
|
||||
this.status = status;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new APIClient();
|
||||
export { APIError };
|
||||
export default api;
|
||||
36
team-portal/src/js/core/router.js
Normal file
36
team-portal/src/js/core/router.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// SPA Router using History API
|
||||
let routeHandler = null;
|
||||
|
||||
export function initRouter(handler) {
|
||||
routeHandler = handler;
|
||||
|
||||
// Handle browser back/forward
|
||||
window.addEventListener('popstate', () => {
|
||||
if (routeHandler) {
|
||||
routeHandler(window.location.pathname);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle initial load
|
||||
const path = window.location.pathname;
|
||||
if (routeHandler && path !== '/') {
|
||||
routeHandler(path);
|
||||
}
|
||||
}
|
||||
|
||||
export function navigate(path) {
|
||||
if (window.location.pathname !== path) {
|
||||
window.history.pushState({}, '', path);
|
||||
if (routeHandler) {
|
||||
routeHandler(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getCurrentRoute() {
|
||||
return window.location.pathname;
|
||||
}
|
||||
|
||||
export function goBack() {
|
||||
window.history.back();
|
||||
}
|
||||
55
team-portal/src/js/core/store.js
Normal file
55
team-portal/src/js/core/store.js
Normal file
@@ -0,0 +1,55 @@
|
||||
// Zustand-like store (vanilla implementation for simplicity)
|
||||
const createStore = (initialState) => {
|
||||
let state = { ...initialState };
|
||||
const listeners = new Set();
|
||||
|
||||
const getState = () => state;
|
||||
|
||||
const setState = (partial) => {
|
||||
const nextState = typeof partial === 'function' ? partial(state) : partial;
|
||||
state = { ...state, ...nextState };
|
||||
listeners.forEach(listener => listener(state));
|
||||
};
|
||||
|
||||
const subscribe = (listener) => {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
};
|
||||
|
||||
return { getState, setState, subscribe };
|
||||
};
|
||||
|
||||
// Create the store
|
||||
const store = createStore({
|
||||
route: '/',
|
||||
team: null,
|
||||
theme: 'dark',
|
||||
beingMode: 'idle',
|
||||
metrics: {
|
||||
ui: { components: 0, tokens: 0, synced: false, stories: 0 },
|
||||
ux: { consistency: 0, violations: 0, patterns: 0, tokens: 0 },
|
||||
qa: { health: 0, quickWins: 0, unused: 0, coverage: 0 }
|
||||
}
|
||||
});
|
||||
|
||||
// Helper functions
|
||||
export const useStore = {
|
||||
getState: store.getState,
|
||||
subscribe: store.subscribe,
|
||||
|
||||
setRoute: (route) => store.setState({ route }),
|
||||
setTeam: (team) => store.setState({ team }),
|
||||
setTheme: (theme) => store.setState({ theme }),
|
||||
setBeingMode: (beingMode) => store.setState({ beingMode }),
|
||||
setMetrics: (team, metrics) => {
|
||||
const currentMetrics = store.getState().metrics;
|
||||
store.setState({
|
||||
metrics: {
|
||||
...currentMetrics,
|
||||
[team]: { ...currentMetrics[team], ...metrics }
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default store;
|
||||
37
team-portal/src/js/core/theme.js
Normal file
37
team-portal/src/js/core/theme.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// Theme Management
|
||||
import { useStore } from './store.js';
|
||||
|
||||
const THEME_KEY = 'dss-portal-theme';
|
||||
|
||||
export function initTheme() {
|
||||
// Check for saved theme or system preference
|
||||
const savedTheme = localStorage.getItem(THEME_KEY);
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const initialTheme = savedTheme || (systemPrefersDark ? 'dark' : 'light');
|
||||
|
||||
setTheme(initialTheme);
|
||||
|
||||
// Listen for system theme changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
if (!localStorage.getItem(THEME_KEY)) {
|
||||
setTheme(e.matches ? 'dark' : 'light');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function setTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem(THEME_KEY, theme);
|
||||
useStore.setTheme(theme);
|
||||
}
|
||||
|
||||
export function toggleTheme() {
|
||||
const currentTheme = useStore.getState().theme;
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
setTheme(newTheme);
|
||||
return newTheme;
|
||||
}
|
||||
|
||||
export function getTheme() {
|
||||
return useStore.getState().theme;
|
||||
}
|
||||
175
team-portal/src/js/dashboards/BaseDashboard.js
Normal file
175
team-portal/src/js/dashboards/BaseDashboard.js
Normal file
@@ -0,0 +1,175 @@
|
||||
// Base Dashboard - Shared Dashboard Logic
|
||||
import { navigate } from '../core/router.js';
|
||||
import { toggleTheme, getTheme } from '../core/theme.js';
|
||||
import { api } from '../core/api.js';
|
||||
import { UIDashboard } from './UIDashboard.js';
|
||||
import { UXDashboard } from './UXDashboard.js';
|
||||
import { QADashboard } from './QADashboard.js';
|
||||
|
||||
const dashboards = {
|
||||
ui: UIDashboard,
|
||||
ux: UXDashboard,
|
||||
qa: QADashboard
|
||||
};
|
||||
|
||||
export function renderDashboard(team, container) {
|
||||
const DashboardClass = dashboards[team];
|
||||
if (!DashboardClass) {
|
||||
console.error(`[Dashboard] Unknown team: ${team}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const dashboard = new DashboardClass(team);
|
||||
container.innerHTML = dashboard.render();
|
||||
|
||||
// Setup event listeners after render
|
||||
setupDashboardEvents(dashboard, container);
|
||||
|
||||
// Load metrics
|
||||
dashboard.loadMetrics();
|
||||
}
|
||||
|
||||
export function hideDashboard(container) {
|
||||
container.classList.add('hidden');
|
||||
container.innerHTML = '';
|
||||
}
|
||||
|
||||
function setupDashboardEvents(dashboard, container) {
|
||||
// Back button
|
||||
const backBtn = container.querySelector('.back-button');
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
container.classList.remove('entering');
|
||||
container.classList.add('exiting');
|
||||
|
||||
const handleTransitionEnd = () => {
|
||||
container.removeEventListener('transitionend', handleTransitionEnd);
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
container.addEventListener('transitionend', handleTransitionEnd);
|
||||
// Fallback in case transitionend doesn't fire
|
||||
setTimeout(() => {
|
||||
container.removeEventListener('transitionend', handleTransitionEnd);
|
||||
if (window.location.pathname !== '/') {
|
||||
navigate('/');
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
// Theme toggle
|
||||
const themeBtn = container.querySelector('.theme-toggle');
|
||||
if (themeBtn) {
|
||||
themeBtn.addEventListener('click', () => {
|
||||
const newTheme = toggleTheme();
|
||||
updateThemeIcon(themeBtn, newTheme);
|
||||
});
|
||||
updateThemeIcon(themeBtn, getTheme());
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
container.querySelectorAll('.action-button').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const action = btn.dataset.action;
|
||||
if (action && dashboard.handleAction) {
|
||||
dashboard.handleAction(action);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateThemeIcon(button, theme) {
|
||||
const icon = button.querySelector('svg');
|
||||
if (theme === 'dark') {
|
||||
icon.innerHTML = `
|
||||
<path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0z"/>
|
||||
`;
|
||||
} else {
|
||||
icon.innerHTML = `
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Base Dashboard Class
|
||||
export class BaseDashboard {
|
||||
constructor(team) {
|
||||
this.team = team;
|
||||
this.metrics = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
return `
|
||||
<header class="dashboard-header">
|
||||
<div class="dashboard-header__left">
|
||||
<button class="back-button" aria-label="Back to home">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
<span>back</span>
|
||||
</button>
|
||||
<div class="team-indicator">
|
||||
<div class="team-indicator__dots">
|
||||
<span class="team-indicator__dot ${this.team === 'ui' ? 'active' : ''}"></span>
|
||||
<span class="team-indicator__dot ${this.team === 'ux' ? 'active' : ''}"></span>
|
||||
<span class="team-indicator__dot ${this.team === 'qa' ? 'active' : ''}"></span>
|
||||
</div>
|
||||
<span class="team-indicator__name">${this.team.toUpperCase()} Team</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-header__right">
|
||||
<button class="icon-button theme-toggle" aria-label="Toggle theme">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
${this.renderMetricsBar()}
|
||||
${this.renderActionZone()}
|
||||
${this.renderActionDock()}
|
||||
`;
|
||||
}
|
||||
|
||||
renderMetricsBar() {
|
||||
// Override in subclasses
|
||||
return '<div class="metrics-bar"></div>';
|
||||
}
|
||||
|
||||
renderActionZone() {
|
||||
return `
|
||||
<main class="action-zone">
|
||||
<div class="action-zone__content">
|
||||
<h1 class="action-zone__title">Select an action</h1>
|
||||
<p class="action-zone__subtitle">Choose from the dock below to get started</p>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
}
|
||||
|
||||
renderActionDock() {
|
||||
// Override in subclasses
|
||||
return '<nav class="action-dock"></nav>';
|
||||
}
|
||||
|
||||
async loadMetrics() {
|
||||
try {
|
||||
this.metrics = await api.getTeamMetrics(this.team);
|
||||
this.updateMetricsDisplay();
|
||||
} catch (error) {
|
||||
console.error('[Dashboard] Failed to load metrics:', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateMetricsDisplay() {
|
||||
// Override in subclasses
|
||||
}
|
||||
|
||||
handleAction(action) {
|
||||
console.log(`[Dashboard] Action: ${action}`);
|
||||
// Override in subclasses
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseDashboard;
|
||||
180
team-portal/src/js/dashboards/QADashboard.js
Normal file
180
team-portal/src/js/dashboards/QADashboard.js
Normal file
@@ -0,0 +1,180 @@
|
||||
// QA Team Dashboard
|
||||
import { BaseDashboard } from './BaseDashboard.js';
|
||||
import { api } from '../core/api.js';
|
||||
|
||||
export class QADashboard extends BaseDashboard {
|
||||
constructor(team) {
|
||||
super(team);
|
||||
}
|
||||
|
||||
renderMetricsBar() {
|
||||
return `
|
||||
<div class="metrics-bar">
|
||||
<div class="metric">
|
||||
<div class="metric__label">Health Score</div>
|
||||
<div class="metric__value" id="metric-health">--%</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric__label">Quick Wins</div>
|
||||
<div class="metric__value" id="metric-quickwins">--</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric__label">Unused Styles</div>
|
||||
<div class="metric__value" id="metric-unused">--</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric__label">Coverage</div>
|
||||
<div class="metric__value" id="metric-coverage">--%</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderActionZone() {
|
||||
return `
|
||||
<main class="action-zone">
|
||||
<div class="action-zone__content">
|
||||
<h1 class="action-zone__title">Quality Assurance</h1>
|
||||
<p class="action-zone__subtitle">Find issues, validate components, and improve coverage</p>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
}
|
||||
|
||||
renderActionDock() {
|
||||
return `
|
||||
<nav class="action-dock">
|
||||
<button class="action-button" data-action="quick-wins">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
||||
</svg>
|
||||
<span class="action-button__label">Quick Wins</span>
|
||||
</button>
|
||||
<button class="action-button" data-action="find-unused">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
<line x1="8" y1="11" x2="14" y2="11"/>
|
||||
</svg>
|
||||
<span class="action-button__label">Find Unused</span>
|
||||
</button>
|
||||
<button class="action-button" data-action="audit-components">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
<path d="M3 9h18"/>
|
||||
<path d="M9 21V9"/>
|
||||
</svg>
|
||||
<span class="action-button__label">Audit Components</span>
|
||||
</button>
|
||||
<button class="action-button" data-action="coverage">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
||||
</svg>
|
||||
<span class="action-button__label">Coverage</span>
|
||||
</button>
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
|
||||
updateMetricsDisplay() {
|
||||
const { health, quickWins, unused, coverage } = this.metrics;
|
||||
|
||||
const healthEl = document.getElementById('metric-health');
|
||||
const quickWinsEl = document.getElementById('metric-quickwins');
|
||||
const unusedEl = document.getElementById('metric-unused');
|
||||
const coverageEl = document.getElementById('metric-coverage');
|
||||
|
||||
if (healthEl) {
|
||||
healthEl.textContent = `${health || 0}%`;
|
||||
healthEl.classList.toggle('metric__value--success', health >= 80);
|
||||
healthEl.classList.toggle('metric__value--warning', health < 80 && health >= 60);
|
||||
}
|
||||
if (quickWinsEl) {
|
||||
quickWinsEl.textContent = quickWins || 0;
|
||||
quickWinsEl.classList.toggle('metric__value--success', quickWins > 0);
|
||||
}
|
||||
if (unusedEl) {
|
||||
unusedEl.textContent = unused || 0;
|
||||
unusedEl.classList.toggle('metric__value--warning', unused > 10);
|
||||
}
|
||||
if (coverageEl) {
|
||||
coverageEl.textContent = `${coverage || 0}%`;
|
||||
coverageEl.classList.toggle('metric__value--success', coverage >= 80);
|
||||
}
|
||||
}
|
||||
|
||||
async handleAction(action) {
|
||||
console.log(`[QA Dashboard] Action: ${action}`);
|
||||
|
||||
switch (action) {
|
||||
case 'quick-wins':
|
||||
await this.findQuickWins();
|
||||
break;
|
||||
case 'find-unused':
|
||||
this.showMessage('Find Unused - Coming soon');
|
||||
break;
|
||||
case 'audit-components':
|
||||
await this.auditComponents();
|
||||
break;
|
||||
case 'coverage':
|
||||
this.showMessage('Coverage Analysis - Coming soon');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async findQuickWins() {
|
||||
this.showLoading('Finding quick wins...');
|
||||
try {
|
||||
const result = await api.findQuickWins('.');
|
||||
const wins = result.quick_wins?.length || 0;
|
||||
this.showMessage(`Found ${wins} quick wins`);
|
||||
await this.loadMetrics();
|
||||
} catch (error) {
|
||||
this.showError('Failed to find quick wins');
|
||||
}
|
||||
}
|
||||
|
||||
async auditComponents() {
|
||||
this.showLoading('Auditing components...');
|
||||
try {
|
||||
const result = await api.auditComponents('.');
|
||||
const issues = result.issues?.length || 0;
|
||||
this.showMessage(`Audit complete: ${issues} issues found`);
|
||||
await this.loadMetrics();
|
||||
} catch (error) {
|
||||
this.showError('Failed to audit components');
|
||||
}
|
||||
}
|
||||
|
||||
showLoading(message) {
|
||||
const zone = document.querySelector('.action-zone__content');
|
||||
if (zone) {
|
||||
zone.innerHTML = `
|
||||
<div class="loading">
|
||||
<div class="loading__spinner"></div>
|
||||
<span>${message}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
showMessage(message) {
|
||||
const zone = document.querySelector('.action-zone__content');
|
||||
if (zone) {
|
||||
zone.innerHTML = `
|
||||
<h1 class="action-zone__title">${message}</h1>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const zone = document.querySelector('.action-zone__content');
|
||||
if (zone) {
|
||||
zone.innerHTML = `
|
||||
<h1 class="action-zone__title" style="color: var(--dss-accent-error)">${message}</h1>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default QADashboard;
|
||||
174
team-portal/src/js/dashboards/UIDashboard.js
Normal file
174
team-portal/src/js/dashboards/UIDashboard.js
Normal file
@@ -0,0 +1,174 @@
|
||||
// UI Team Dashboard
|
||||
import { BaseDashboard } from './BaseDashboard.js';
|
||||
import { api } from '../core/api.js';
|
||||
|
||||
export class UIDashboard extends BaseDashboard {
|
||||
constructor(team) {
|
||||
super(team);
|
||||
}
|
||||
|
||||
renderMetricsBar() {
|
||||
return `
|
||||
<div class="metrics-bar">
|
||||
<div class="metric">
|
||||
<div class="metric__label">Components</div>
|
||||
<div class="metric__value" id="metric-components">--</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric__label">Tokens</div>
|
||||
<div class="metric__value" id="metric-tokens">--</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric__label">Figma Sync</div>
|
||||
<div class="metric__value" id="metric-synced">--</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric__label">Stories</div>
|
||||
<div class="metric__value" id="metric-stories">--</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderActionZone() {
|
||||
return `
|
||||
<main class="action-zone">
|
||||
<div class="action-zone__content">
|
||||
<h1 class="action-zone__title">Component Library</h1>
|
||||
<p class="action-zone__subtitle">Manage tokens, components, and Figma synchronization</p>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
}
|
||||
|
||||
renderActionDock() {
|
||||
return `
|
||||
<nav class="action-dock">
|
||||
<button class="action-button" data-action="extract-tokens">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<circle cx="12" cy="12" r="4"/>
|
||||
</svg>
|
||||
<span class="action-button__label">Extract Tokens</span>
|
||||
</button>
|
||||
<button class="action-button" data-action="sync-figma">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M5 5.5A3.5 3.5 0 0 1 8.5 2H12v7H8.5A3.5 3.5 0 0 1 5 5.5z"/>
|
||||
<path d="M12 2h3.5a3.5 3.5 0 1 1 0 7H12V2z"/>
|
||||
<path d="M12 12.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 1 1-7 0z"/>
|
||||
<path d="M5 19.5A3.5 3.5 0 0 1 8.5 16H12v3.5a3.5 3.5 0 1 1-7 0z"/>
|
||||
<path d="M5 12.5A3.5 3.5 0 0 1 8.5 9H12v7H8.5A3.5 3.5 0 0 1 5 12.5z"/>
|
||||
</svg>
|
||||
<span class="action-button__label">Sync Figma</span>
|
||||
</button>
|
||||
<button class="action-button" data-action="generate-code">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="16 18 22 12 16 6"/>
|
||||
<polyline points="8 6 2 12 8 18"/>
|
||||
</svg>
|
||||
<span class="action-button__label">Generate Code</span>
|
||||
</button>
|
||||
<button class="action-button" data-action="storybook">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
|
||||
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
|
||||
</svg>
|
||||
<span class="action-button__label">Storybook</span>
|
||||
</button>
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
|
||||
updateMetricsDisplay() {
|
||||
const { components, tokens, synced, stories } = this.metrics;
|
||||
|
||||
const componentsEl = document.getElementById('metric-components');
|
||||
const tokensEl = document.getElementById('metric-tokens');
|
||||
const syncedEl = document.getElementById('metric-synced');
|
||||
const storiesEl = document.getElementById('metric-stories');
|
||||
|
||||
if (componentsEl) componentsEl.textContent = components || 0;
|
||||
if (tokensEl) tokensEl.textContent = tokens || 0;
|
||||
if (syncedEl) {
|
||||
syncedEl.textContent = synced ? 'OK' : '--';
|
||||
syncedEl.classList.toggle('metric__value--success', synced);
|
||||
}
|
||||
if (storiesEl) storiesEl.textContent = stories || 0;
|
||||
}
|
||||
|
||||
async handleAction(action) {
|
||||
console.log(`[UI Dashboard] Action: ${action}`);
|
||||
|
||||
switch (action) {
|
||||
case 'extract-tokens':
|
||||
await this.extractTokens();
|
||||
break;
|
||||
case 'sync-figma':
|
||||
await this.syncFigma();
|
||||
break;
|
||||
case 'generate-code':
|
||||
this.showMessage('Generate Code - Coming soon');
|
||||
break;
|
||||
case 'storybook':
|
||||
window.open('http://localhost:6006', '_blank');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async extractTokens() {
|
||||
this.showLoading('Extracting tokens...');
|
||||
try {
|
||||
const result = await api.extractTokens('.', ['css', 'scss']);
|
||||
this.showMessage(`Extracted ${result.tokens?.length || 0} tokens`);
|
||||
await this.loadMetrics();
|
||||
} catch (error) {
|
||||
this.showError('Failed to extract tokens');
|
||||
}
|
||||
}
|
||||
|
||||
async syncFigma() {
|
||||
const fileKey = prompt('Enter Figma file key:');
|
||||
if (!fileKey) return;
|
||||
|
||||
this.showLoading('Syncing with Figma...');
|
||||
try {
|
||||
const result = await api.syncFigma(fileKey);
|
||||
this.showMessage('Figma sync complete');
|
||||
await this.loadMetrics();
|
||||
} catch (error) {
|
||||
this.showError('Failed to sync Figma');
|
||||
}
|
||||
}
|
||||
|
||||
showLoading(message) {
|
||||
const zone = document.querySelector('.action-zone__content');
|
||||
if (zone) {
|
||||
zone.innerHTML = `
|
||||
<div class="loading">
|
||||
<div class="loading__spinner"></div>
|
||||
<span>${message}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
showMessage(message) {
|
||||
const zone = document.querySelector('.action-zone__content');
|
||||
if (zone) {
|
||||
zone.innerHTML = `
|
||||
<h1 class="action-zone__title">${message}</h1>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const zone = document.querySelector('.action-zone__content');
|
||||
if (zone) {
|
||||
zone.innerHTML = `
|
||||
<h1 class="action-zone__title" style="color: var(--dss-accent-error)">${message}</h1>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UIDashboard;
|
||||
178
team-portal/src/js/dashboards/UXDashboard.js
Normal file
178
team-portal/src/js/dashboards/UXDashboard.js
Normal file
@@ -0,0 +1,178 @@
|
||||
// UX Team Dashboard
|
||||
import { BaseDashboard } from './BaseDashboard.js';
|
||||
import { api } from '../core/api.js';
|
||||
|
||||
export class UXDashboard extends BaseDashboard {
|
||||
constructor(team) {
|
||||
super(team);
|
||||
}
|
||||
|
||||
renderMetricsBar() {
|
||||
return `
|
||||
<div class="metrics-bar">
|
||||
<div class="metric">
|
||||
<div class="metric__label">Consistency</div>
|
||||
<div class="metric__value" id="metric-consistency">--%</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric__label">Violations</div>
|
||||
<div class="metric__value" id="metric-violations">--</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric__label">Patterns</div>
|
||||
<div class="metric__value" id="metric-patterns">--</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric__label">Tokens</div>
|
||||
<div class="metric__value" id="metric-tokens">--</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderActionZone() {
|
||||
return `
|
||||
<main class="action-zone">
|
||||
<div class="action-zone__content">
|
||||
<h1 class="action-zone__title">Design Consistency</h1>
|
||||
<p class="action-zone__subtitle">Validate tokens, analyze patterns, and ensure brand compliance</p>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
}
|
||||
|
||||
renderActionDock() {
|
||||
return `
|
||||
<nav class="action-dock">
|
||||
<button class="action-button" data-action="validate-tokens">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||
</svg>
|
||||
<span class="action-button__label">Validate Tokens</span>
|
||||
</button>
|
||||
<button class="action-button" data-action="analyze-patterns">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 21H3"/>
|
||||
<path d="M21 21V9l-9-6-9 6v12"/>
|
||||
<path d="M9 21V12h6v9"/>
|
||||
</svg>
|
||||
<span class="action-button__label">Style Patterns</span>
|
||||
</button>
|
||||
<button class="action-button" data-action="check-naming">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
||||
<path d="M2 17l10 5 10-5"/>
|
||||
<path d="M2 12l10 5 10-5"/>
|
||||
</svg>
|
||||
<span class="action-button__label">Check Naming</span>
|
||||
</button>
|
||||
<button class="action-button" data-action="export-report">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
</svg>
|
||||
<span class="action-button__label">Export Report</span>
|
||||
</button>
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
|
||||
updateMetricsDisplay() {
|
||||
const { consistency, violations, patterns, tokens } = this.metrics;
|
||||
|
||||
const consistencyEl = document.getElementById('metric-consistency');
|
||||
const violationsEl = document.getElementById('metric-violations');
|
||||
const patternsEl = document.getElementById('metric-patterns');
|
||||
const tokensEl = document.getElementById('metric-tokens');
|
||||
|
||||
if (consistencyEl) {
|
||||
consistencyEl.textContent = `${consistency || 0}%`;
|
||||
consistencyEl.classList.toggle('metric__value--success', consistency >= 80);
|
||||
consistencyEl.classList.toggle('metric__value--warning', consistency < 80 && consistency >= 60);
|
||||
}
|
||||
if (violationsEl) {
|
||||
violationsEl.textContent = violations || 0;
|
||||
violationsEl.classList.toggle('metric__value--warning', violations > 0);
|
||||
}
|
||||
if (patternsEl) patternsEl.textContent = patterns || 0;
|
||||
if (tokensEl) tokensEl.textContent = tokens || 0;
|
||||
}
|
||||
|
||||
async handleAction(action) {
|
||||
console.log(`[UX Dashboard] Action: ${action}`);
|
||||
|
||||
switch (action) {
|
||||
case 'validate-tokens':
|
||||
await this.validateTokens();
|
||||
break;
|
||||
case 'analyze-patterns':
|
||||
await this.analyzePatterns();
|
||||
break;
|
||||
case 'check-naming':
|
||||
this.showMessage('Naming Check - Coming soon');
|
||||
break;
|
||||
case 'export-report':
|
||||
this.showMessage('Export Report - Coming soon');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async validateTokens() {
|
||||
this.showLoading('Validating tokens...');
|
||||
try {
|
||||
const result = await api.analyzeProject('.');
|
||||
const violations = result.issues?.length || 0;
|
||||
this.showMessage(`Found ${violations} validation issues`);
|
||||
await this.loadMetrics();
|
||||
} catch (error) {
|
||||
this.showError('Failed to validate tokens');
|
||||
}
|
||||
}
|
||||
|
||||
async analyzePatterns() {
|
||||
this.showLoading('Analyzing patterns...');
|
||||
try {
|
||||
const result = await api.analyzeProject('.');
|
||||
const patterns = result.patterns?.length || 0;
|
||||
this.showMessage(`Found ${patterns} style patterns`);
|
||||
await this.loadMetrics();
|
||||
} catch (error) {
|
||||
this.showError('Failed to analyze patterns');
|
||||
}
|
||||
}
|
||||
|
||||
showLoading(message) {
|
||||
const zone = document.querySelector('.action-zone__content');
|
||||
if (zone) {
|
||||
zone.innerHTML = `
|
||||
<div class="loading">
|
||||
<div class="loading__spinner"></div>
|
||||
<span>${message}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
showMessage(message) {
|
||||
const zone = document.querySelector('.action-zone__content');
|
||||
if (zone) {
|
||||
zone.innerHTML = `
|
||||
<h1 class="action-zone__title">${message}</h1>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const zone = document.querySelector('.action-zone__content');
|
||||
if (zone) {
|
||||
zone.innerHTML = `
|
||||
<h1 class="action-zone__title" style="color: var(--dss-accent-error)">${message}</h1>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UXDashboard;
|
||||
21
team-portal/src/js/main.js
Normal file
21
team-portal/src/js/main.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// DSS Team Portal - Main Entry Point
|
||||
import { initScene } from './scene.js';
|
||||
import { actions } from './store.js';
|
||||
import { initializeBridge, initializeTokenSynchronizer } from './bridge.js';
|
||||
|
||||
// Initialize the 3D Scene
|
||||
initScene();
|
||||
|
||||
// Initialize the bridge to connect state to UI/3D
|
||||
initializeBridge();
|
||||
|
||||
// Initialize the synchronizer to connect theme to 3D
|
||||
initializeTokenSynchronizer();
|
||||
|
||||
// Add event listener for the toggle button
|
||||
const toggleButton = document.getElementById('toggle-view-btn');
|
||||
toggleButton.addEventListener('click', () => {
|
||||
actions.toggleViewState();
|
||||
});
|
||||
|
||||
console.log("DSS Team Portal POC Initialized");
|
||||
62
team-portal/src/js/scene.js
Normal file
62
team-portal/src/js/scene.js
Normal file
@@ -0,0 +1,62 @@
|
||||
// DSS Team Portal - Three.js Scene
|
||||
import * as THREE from 'three';
|
||||
|
||||
let scene, camera, renderer, crystal;
|
||||
|
||||
function init() {
|
||||
// Scene
|
||||
scene = new THREE.Scene();
|
||||
|
||||
// Camera
|
||||
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
camera.position.z = 5;
|
||||
|
||||
// Renderer
|
||||
const canvas = document.getElementById('webgl-canvas');
|
||||
renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
|
||||
// "Fake Crystal" - using a simple material for POC
|
||||
const geometry = new THREE.IcosahedronGeometry(1.5, 0);
|
||||
const material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true });
|
||||
|
||||
// Add a uniform for color control
|
||||
material.onBeforeCompile = (shader) => {
|
||||
shader.uniforms.uColor = { value: new THREE.Color(0xff0000) };
|
||||
shader.fragmentShader = `
|
||||
uniform vec3 uColor;
|
||||
${shader.fragmentShader}
|
||||
`.replace(
|
||||
`vec4 diffuseColor = vec4( diffuse, opacity );`,
|
||||
`vec4 diffuseColor = vec4( uColor, opacity );`
|
||||
);
|
||||
crystal.userData.shader = shader;
|
||||
};
|
||||
|
||||
crystal = new THREE.Mesh(geometry, material);
|
||||
scene.add(crystal);
|
||||
|
||||
// Animation Loop
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
crystal.rotation.x += 0.001;
|
||||
crystal.rotation.y += 0.001;
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
animate();
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
});
|
||||
}
|
||||
|
||||
function updateCrystalColor(color) {
|
||||
if (crystal && crystal.userData.shader) {
|
||||
crystal.userData.shader.uniforms.uColor.value.set(color);
|
||||
}
|
||||
}
|
||||
|
||||
export { init as initScene, updateCrystalColor, camera, crystal };
|
||||
28
team-portal/src/js/store.js
Normal file
28
team-portal/src/js/store.js
Normal file
@@ -0,0 +1,28 @@
|
||||
// DSS Team Portal - Zustand Store
|
||||
import { createStore } from 'zustand/vanilla';
|
||||
|
||||
const store = createStore((set) => ({
|
||||
viewState: '3D_HOME', // '3D_HOME' | 'TRANSITION_TO_2D' | '2D_WORKBENCH' | 'TRANSITION_TO_3D'
|
||||
theme: 'dark', // 'dark' | 'light'
|
||||
|
||||
actions: {
|
||||
toggleViewState: () => set((state) => {
|
||||
if (state.viewState === '3D_HOME') {
|
||||
return { viewState: 'TRANSITION_TO_2D' };
|
||||
}
|
||||
if (state.viewState === '2D_WORKBENCH') {
|
||||
return { viewState: 'TRANSITION_TO_3D' };
|
||||
}
|
||||
return {}; // Do nothing during transitions
|
||||
}),
|
||||
setViewState: (viewState) => set({ viewState }),
|
||||
toggleTheme: () => set((state) => ({ theme: state.theme === 'dark' ? 'light' : 'dark' })),
|
||||
}
|
||||
}));
|
||||
|
||||
// Export the store and actions for convenience
|
||||
const { getState, setState, subscribe } = store;
|
||||
const { actions } = store.getState();
|
||||
|
||||
export { getState, setState, subscribe, actions };
|
||||
export default store;
|
||||
85
team-portal/src/js/utils/easing.js
Normal file
85
team-portal/src/js/utils/easing.js
Normal file
@@ -0,0 +1,85 @@
|
||||
// Easing Functions
|
||||
|
||||
export const easing = {
|
||||
// Linear
|
||||
linear: t => t,
|
||||
|
||||
// Quadratic
|
||||
easeInQuad: t => t * t,
|
||||
easeOutQuad: t => t * (2 - t),
|
||||
easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
|
||||
|
||||
// Cubic
|
||||
easeInCubic: t => t * t * t,
|
||||
easeOutCubic: t => (--t) * t * t + 1,
|
||||
easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
|
||||
|
||||
// Quartic
|
||||
easeInQuart: t => t * t * t * t,
|
||||
easeOutQuart: t => 1 - (--t) * t * t * t,
|
||||
easeInOutQuart: t => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t,
|
||||
|
||||
// Sine
|
||||
easeInSine: t => 1 - Math.cos((t * Math.PI) / 2),
|
||||
easeOutSine: t => Math.sin((t * Math.PI) / 2),
|
||||
easeInOutSine: t => -(Math.cos(Math.PI * t) - 1) / 2,
|
||||
|
||||
// Exponential
|
||||
easeInExpo: t => t === 0 ? 0 : Math.pow(2, 10 * t - 10),
|
||||
easeOutExpo: t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
|
||||
easeInOutExpo: t => {
|
||||
if (t === 0) return 0;
|
||||
if (t === 1) return 1;
|
||||
if (t < 0.5) return Math.pow(2, 20 * t - 10) / 2;
|
||||
return (2 - Math.pow(2, -20 * t + 10)) / 2;
|
||||
},
|
||||
|
||||
// Elastic
|
||||
easeInElastic: t => {
|
||||
const c4 = (2 * Math.PI) / 3;
|
||||
return t === 0 ? 0 : t === 1 ? 1 : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4);
|
||||
},
|
||||
easeOutElastic: t => {
|
||||
const c4 = (2 * Math.PI) / 3;
|
||||
return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
|
||||
},
|
||||
easeInOutElastic: t => {
|
||||
const c5 = (2 * Math.PI) / 4.5;
|
||||
if (t === 0) return 0;
|
||||
if (t === 1) return 1;
|
||||
if (t < 0.5) return -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2;
|
||||
return (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1;
|
||||
},
|
||||
|
||||
// Back
|
||||
easeInBack: t => {
|
||||
const c1 = 1.70158;
|
||||
const c3 = c1 + 1;
|
||||
return c3 * t * t * t - c1 * t * t;
|
||||
},
|
||||
easeOutBack: t => {
|
||||
const c1 = 1.70158;
|
||||
const c3 = c1 + 1;
|
||||
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
|
||||
},
|
||||
|
||||
// Bounce
|
||||
easeOutBounce: t => {
|
||||
const n1 = 7.5625;
|
||||
const d1 = 2.75;
|
||||
if (t < 1 / d1) return n1 * t * t;
|
||||
if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
|
||||
if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
|
||||
return n1 * (t -= 2.625 / d1) * t + 0.984375;
|
||||
},
|
||||
|
||||
// Organic - custom for the being
|
||||
organic: t => {
|
||||
// Combination of sine and elastic for natural movement
|
||||
const sine = Math.sin(t * Math.PI);
|
||||
const elastic = Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI / 3)) + 1;
|
||||
return (sine + elastic) / 2;
|
||||
}
|
||||
};
|
||||
|
||||
export default easing;
|
||||
47
team-portal/src/js/utils/math.js
Normal file
47
team-portal/src/js/utils/math.js
Normal file
@@ -0,0 +1,47 @@
|
||||
// Math Utilities
|
||||
|
||||
export function randomInRange(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
export function lerp(start, end, t) {
|
||||
return start + (end - start) * t;
|
||||
}
|
||||
|
||||
export function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
export function distance(x1, y1, x2, y2) {
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
export function normalize(x, y) {
|
||||
const len = Math.sqrt(x * x + y * y);
|
||||
if (len === 0) return { x: 0, y: 0 };
|
||||
return { x: x / len, y: y / len };
|
||||
}
|
||||
|
||||
export function map(value, inMin, inMax, outMin, outMax) {
|
||||
return ((value - inMin) / (inMax - inMin)) * (outMax - outMin) + outMin;
|
||||
}
|
||||
|
||||
export function smoothstep(edge0, edge1, x) {
|
||||
const t = clamp((x - edge0) / (edge1 - edge0), 0, 1);
|
||||
return t * t * (3 - 2 * t);
|
||||
}
|
||||
|
||||
export function easeOutElastic(t) {
|
||||
const c4 = (2 * Math.PI) / 3;
|
||||
return t === 0
|
||||
? 0
|
||||
: t === 1
|
||||
? 1
|
||||
: Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
|
||||
}
|
||||
|
||||
export function easeInOutCubic(t) {
|
||||
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
}
|
||||
159
team-portal/src/main.js
Normal file
159
team-portal/src/main.js
Normal file
@@ -0,0 +1,159 @@
|
||||
// DSS Team Portal - Main Entry Point
|
||||
import { initRouter, navigate } from './js/core/router.js';
|
||||
import { useStore } from './js/core/store.js';
|
||||
import { initTheme } from './js/core/theme.js';
|
||||
import { Being } from './js/conscious/Being.js';
|
||||
import { renderDashboard, hideDashboard } from './js/dashboards/BaseDashboard.js';
|
||||
|
||||
// Initialize the application
|
||||
class TeamPortal {
|
||||
constructor() {
|
||||
this.being = null;
|
||||
this.canvas = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
console.log('[DSS] Initializing Team Portal...');
|
||||
|
||||
// Initialize theme
|
||||
initTheme();
|
||||
|
||||
// Initialize router
|
||||
initRouter(this.handleRoute.bind(this));
|
||||
|
||||
// Initialize canvas and being
|
||||
this.canvas = document.getElementById('conscious-canvas');
|
||||
if (this.canvas) {
|
||||
this.being = new Being(this.canvas);
|
||||
this.being.start();
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
|
||||
// Handle initial route
|
||||
this.handleRoute(window.location.pathname);
|
||||
|
||||
console.log('[DSS] Team Portal initialized');
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Team card clicks and keyboard navigation
|
||||
document.querySelectorAll('.team-card').forEach(card => {
|
||||
const handleActivation = () => {
|
||||
const team = card.dataset.team;
|
||||
this.navigateToTeam(team);
|
||||
};
|
||||
|
||||
card.addEventListener('click', handleActivation);
|
||||
|
||||
// Keyboard accessibility
|
||||
card.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleActivation();
|
||||
}
|
||||
});
|
||||
|
||||
// Mouse interaction with being
|
||||
card.addEventListener('mouseenter', () => {
|
||||
if (this.being) {
|
||||
this.being.setTarget(card);
|
||||
}
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', () => {
|
||||
if (this.being) {
|
||||
this.being.clearTarget();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Admin link - points to DSS API admin interface
|
||||
const ADMIN_URL = import.meta.env?.VITE_ADMIN_URL || '/api/admin';
|
||||
const adminLink = document.querySelector('.admin-link');
|
||||
if (adminLink) {
|
||||
adminLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
// Open API docs/admin in new tab
|
||||
window.open(ADMIN_URL, '_blank');
|
||||
});
|
||||
}
|
||||
|
||||
// Global mouse tracking for being
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (this.being) {
|
||||
this.being.updateMousePosition(e.clientX, e.clientY);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
navigateToTeam(team) {
|
||||
const landing = document.getElementById('landing');
|
||||
landing.classList.add('transitioning');
|
||||
|
||||
const handleTransitionEnd = () => {
|
||||
landing.removeEventListener('transitionend', handleTransitionEnd);
|
||||
navigate(`/${team}`);
|
||||
};
|
||||
|
||||
landing.addEventListener('transitionend', handleTransitionEnd);
|
||||
// Fallback for browsers/cases where transitionend doesn't fire
|
||||
setTimeout(() => {
|
||||
landing.removeEventListener('transitionend', handleTransitionEnd);
|
||||
if (window.location.pathname !== `/${team}`) {
|
||||
navigate(`/${team}`);
|
||||
}
|
||||
}, 400);
|
||||
}
|
||||
|
||||
handleRoute(path) {
|
||||
const store = useStore.getState();
|
||||
const landing = document.getElementById('landing');
|
||||
const dashboard = document.getElementById('dashboard');
|
||||
|
||||
if (path === '/' || path === '') {
|
||||
// Show landing
|
||||
landing.classList.remove('hidden', 'transitioning');
|
||||
dashboard.classList.add('hidden');
|
||||
dashboard.classList.remove('entering');
|
||||
store.setRoute('/');
|
||||
|
||||
if (this.being) {
|
||||
this.being.setMode('idle');
|
||||
}
|
||||
} else {
|
||||
// Extract team from path (handles /ui, /ux, /qa)
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
const team = segments[0];
|
||||
|
||||
if (['ui', 'ux', 'qa'].includes(team)) {
|
||||
// Show dashboard
|
||||
landing.classList.add('hidden');
|
||||
dashboard.classList.remove('hidden');
|
||||
dashboard.classList.add('entering');
|
||||
dashboard.dataset.team = team;
|
||||
|
||||
store.setRoute(path);
|
||||
store.setTeam(team);
|
||||
|
||||
renderDashboard(team, dashboard);
|
||||
|
||||
if (this.being) {
|
||||
this.being.setMode('background');
|
||||
}
|
||||
} else {
|
||||
// Redirect unknown routes to landing
|
||||
navigate('/');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on DOM ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const portal = new TeamPortal();
|
||||
portal.init();
|
||||
});
|
||||
|
||||
export default TeamPortal;
|
||||
20
team-portal/vite.config.js
Normal file
20
team-portal/vite.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
root: '.',
|
||||
publicDir: 'public',
|
||||
server: {
|
||||
port: 3457,
|
||||
host: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3456',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets'
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user