feat: initial commit web-cad-neu with docker-compose, frontend and backend

This commit is contained in:
2026-06-26 10:50:24 +02:00
commit 4ec76fe406
102 changed files with 25722 additions and 0 deletions
+340
View File
@@ -0,0 +1,340 @@
// web-cad v6 Interactions (theme, tabs, drawers, mobile right-tab-bar)
(function() {
'use strict';
// ===================== THEME TOGGLE =====================
const themeBtn = document.getElementById('theme-toggle');
const html = document.documentElement;
if (themeBtn) {
themeBtn.addEventListener('click', () => {
const cur = html.getAttribute('data-theme');
html.setAttribute('data-theme', cur === 'dark' ? 'light' : 'dark');
});
}
// ===================== RIGHTBAR (desktop) TAB SWITCHING =====================
document.querySelectorAll('.rightbar-tab').forEach(tab => {
tab.addEventListener('click', () => {
const panel = tab.dataset.panel;
document.querySelectorAll('.rightbar-tab').forEach(t => { t.classList.remove('active'); t.setAttribute('aria-selected', 'false'); });
document.querySelectorAll('.rightbar-panel').forEach(p => p.classList.remove('active'));
tab.classList.add('active');
tab.setAttribute('aria-selected', 'true');
const target = document.querySelector(`[data-panel-content="${panel}"]`);
if (target) target.classList.add('active');
// sync mobile drawer if open
const dTab = document.querySelector(`[data-drawer-tab="${panel}"]`);
if (dTab) dTab.click();
});
});
// ===================== DRAWER TAB SWITCHING (mobile) =====================
document.querySelectorAll('.drawer-tab').forEach(tab => {
tab.addEventListener('click', () => {
const panel = tab.dataset.drawerTab;
document.querySelectorAll('.drawer-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Re-render drawer body for this panel
renderDrawerRight(panel);
// sync desktop rightbar
const rTab = document.querySelector(`.rightbar-tab[data-panel="${panel}"]`);
if (rTab) {
document.querySelectorAll('.rightbar-tab').forEach(t => t.classList.remove('active'));
rTab.classList.add('active');
document.querySelectorAll('.rightbar-panel').forEach(p => p.classList.remove('active'));
const target = document.querySelector(`[data-panel-content="${panel}"]`);
if (target) target.classList.add('active');
}
});
});
// ===================== RIGHT-TAB-BAR (mobile vertical) =====================
document.querySelectorAll('.right-tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
const which = btn.dataset.drawer;
document.querySelectorAll('.right-tab-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// activate drawer tab + open drawer
const dTab = document.querySelector(`[data-drawer-tab="${which}"]`);
if (dTab) dTab.click();
openDrawer('right');
});
});
// ===================== DRAWER OPEN/CLOSE =====================
const scrim = document.getElementById('scrim');
const drawerLeft = document.getElementById('drawer-left');
const drawerRight = document.getElementById('drawer-right');
function openDrawer(which) {
const drawerLeftBody = document.getElementById('drawer-left-body');
const drawerRightBody = document.getElementById('drawer-right-body');
if (which === 'left' && drawerLeft) {
drawerLeft.classList.add('open');
drawerLeft.setAttribute('aria-hidden', 'false');
// Use children.length instead of innerHTML.trim() because placeholder HTML comments would prevent re-render
if (drawerLeftBody && drawerLeftBody.children.length === 0) {
renderDrawerLeft();
}
}
if (which === 'right' && drawerRight) {
drawerRight.classList.add('open');
drawerRight.setAttribute('aria-hidden', 'false');
if (drawerRightBody && drawerRightBody.children.length === 0) {
const active = document.querySelector('.drawer-tab.active');
renderDrawerRight(active ? active.dataset.drawerTab : 'tool');
}
}
if (scrim) scrim.classList.add('show');
document.body.classList.add('drawer-open');
}
function closeDrawers() {
if (drawerLeft) { drawerLeft.classList.remove('open'); drawerLeft.setAttribute('aria-hidden', 'true'); }
if (drawerRight) { drawerRight.classList.remove('open'); drawerRight.setAttribute('aria-hidden', 'true'); }
if (scrim) scrim.classList.remove('show');
document.body.classList.remove('drawer-open');
}
// Hamburger opens left drawer
const hamburger = document.getElementById('hamburger-btn');
if (hamburger) {
hamburger.addEventListener('click', () => openDrawer('left'));
}
// Close buttons
document.querySelectorAll('[data-close-drawer]').forEach(b => {
b.addEventListener('click', closeDrawers);
});
// Scrim click closes
if (scrim) scrim.addEventListener('click', closeDrawers);
// ESC key closes
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeDrawers();
});
// ===================== RENDER DRAWER LEFT (clone leftbar) =====================
function renderDrawerLeft() {
const src = document.querySelector('.leftbar');
const dst = document.getElementById('drawer-left-body');
if (!src || !dst) return;
// Clone leftbar body (skip header which has toggle)
const clone = src.cloneNode(true);
// Remove the inner toggle button (drawer has its own close)
const innerToggle = clone.querySelector('.leftbar-toggle');
if (innerToggle) innerToggle.remove();
// Remove header (drawer has its own header)
const header = clone.querySelector('.leftbar-header');
if (header) header.remove();
// Remove footer (not needed in drawer)
const footer = clone.querySelector('.leftbar-footer');
if (footer) footer.remove();
// Mark as drawer version + force display (the original .leftbar is hidden on mobile)
clone.classList.add('drawer-content-left');
clone.removeAttribute('id');
clone.style.cssText = 'display: flex; flex-direction: column; flex: 1; min-height: 0; border: none; background: transparent;';
dst.innerHTML = '';
dst.appendChild(clone);
// Wire up tool buttons inside drawer
dst.querySelectorAll('.tool-btn').forEach(b => {
b.addEventListener('click', () => {
document.querySelectorAll('.tool-btn').forEach(x => x.classList.remove('active'));
b.classList.add('active');
// Close drawer on tool selection (mobile UX)
setTimeout(() => closeDrawers(), 200);
});
});
}
// ===================== RENDER DRAWER RIGHT (clone rightbar panel) =====================
function renderDrawerRight(panel) {
const src = document.querySelector(`[data-panel-content="${panel}"]`);
const dst = document.getElementById('drawer-right-body');
const titleEl = document.getElementById('drawer-right-title');
if (!src || !dst) return;
const labels = { tool: 'Werkzeug', layer: 'Layer', library: 'Bibliothek', ki: 'KI Copilot' };
if (titleEl) titleEl.textContent = labels[panel] || 'Panel';
dst.innerHTML = '';
const clone = src.cloneNode(true);
clone.classList.add('active');
clone.style.cssText = 'display: block; width: 100%;';
dst.appendChild(clone);
// Re-wire KI chips and library categories inside drawer
dst.querySelectorAll('.ki-chip').forEach(chip => {
chip.addEventListener('click', () => {
const input = dst.querySelector('.ki-input');
if (input) { input.value = chip.textContent.trim(); input.focus(); }
});
});
dst.querySelectorAll('.lib-cat').forEach(cat => {
cat.addEventListener('click', () => {
dst.querySelectorAll('.lib-cat').forEach(c => c.classList.remove('active'));
cat.classList.add('active');
});
});
}
// ===================== RIBBON TAB SWITCHING =====================
document.querySelectorAll('.ribbon-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.ribbon-tab').forEach(t => { t.classList.remove('active'); t.setAttribute('aria-selected', 'false'); });
tab.classList.add('active');
tab.setAttribute('aria-selected', 'true');
});
});
// ===================== TOOL SELECTION (desktop) =====================
document.querySelectorAll('.leftbar .tool-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.leftbar .tool-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// ===================== LEFTBAR TOGGLE (desktop) =====================
const leftToggle = document.getElementById('leftbar-toggle');
if (leftToggle) {
leftToggle.addEventListener('click', () => {
const main = document.querySelector('.main');
if (main) main.classList.toggle('left-collapsed');
});
}
// ===================== CANVAS VIEW TABS =====================
document.querySelectorAll('.canvas-view-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.canvas-view-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
});
});
// ===================== STATUS BAR TOGGLES =====================
document.querySelectorAll('.status-item').forEach(item => {
item.addEventListener('click', () => item.classList.toggle('active'));
});
// ===================== CANVAS TOOLBAR (grid, ortho, snap) =====================
['btn-grid', 'btn-ortho', 'btn-snap'].forEach(id => {
const b = document.getElementById(id);
if (b) {
b.addEventListener('click', () => {
b.classList.toggle('active');
const pressed = b.classList.contains('active');
b.setAttribute('aria-pressed', pressed ? 'true' : 'false');
// Also toggle matching status bar item
const map = { 'btn-grid': 'GRID', 'btn-ortho': 'ORTHO', 'btn-snap': 'SNAP' };
const label = map[id];
if (label) {
document.querySelectorAll('.status-item').forEach(s => {
if (s.textContent.trim().startsWith(label)) s.classList.toggle('active', pressed);
});
}
});
}
});
// ===================== ZOOM =====================
let zoom = 100;
const zoomDisplay = document.getElementById('zoom-display');
document.getElementById('btn-zoom-in')?.addEventListener('click', () => {
zoom = Math.min(400, zoom + 25);
if (zoomDisplay) zoomDisplay.textContent = zoom + '%';
});
document.getElementById('btn-zoom-out')?.addEventListener('click', () => {
zoom = Math.max(10, zoom - 25);
if (zoomDisplay) zoomDisplay.textContent = zoom + '%';
});
document.getElementById('btn-zoom-fit')?.addEventListener('click', () => {
zoom = 100;
if (zoomDisplay) zoomDisplay.textContent = zoom + '%';
});
// ===================== LIBRARY CATEGORIES (desktop) =====================
document.querySelectorAll('.rightbar .lib-cat').forEach(cat => {
cat.addEventListener('click', () => {
document.querySelectorAll('.rightbar .lib-cat').forEach(c => c.classList.remove('active'));
cat.classList.add('active');
});
});
// ===================== KI CHIPS (desktop) =====================
document.querySelectorAll('.rightbar .ki-chip').forEach(chip => {
chip.addEventListener('click', () => {
const input = document.querySelector('.rightbar .ki-input');
if (input) { input.value = chip.textContent.trim(); input.focus(); }
});
});
// ===================== KEYBOARD SHORTCUTS =====================
const toolMap = { v: 'select', l: 'line', c: 'circle', m: 'move', p: 'pan', z: 'zoom-win', e: 'erase' };
document.addEventListener('keydown', (e) => {
if (e.target.matches('input, textarea, select')) return;
if (e.key === 'Escape') return; // handled by drawer
const k = e.key.toLowerCase();
if (toolMap[k]) {
const btn = document.querySelector(`.tool-btn[data-tool="${toolMap[k]}"]`);
if (btn) btn.click();
}
});
})();
// ===================== TREE COMPONENT TOGGLE =====================
(function initTrees() {
// Delegate click on [data-tree-toggle] rows (folder rows)
document.addEventListener('click', (e) => {
const toggleBtn = e.target.closest('.tree-toggle');
if (toggleBtn) {
const row = toggleBtn.closest('[data-tree-toggle]');
if (!row) return;
e.stopPropagation();
const node = row.parentElement;
toggleTreeNode(node);
return;
}
// Click anywhere on folder row also toggles (except the count which is passive)
const folderRow = e.target.closest('[data-tree-toggle]');
if (folderRow && !e.target.closest('.tree-count')) {
const node = folderRow.parentElement;
toggleTreeNode(node);
}
});
// Leaf selection (single-select within siblings)
document.addEventListener('click', (e) => {
const leaf = e.target.closest('[data-tree-select]');
if (!leaf) return;
e.stopPropagation();
// Remove .active from siblings within same parent tree
const tree = leaf.closest('.tree');
if (tree) {
tree.querySelectorAll('[data-tree-select].active').forEach(el => el.classList.remove('active'));
}
leaf.classList.add('active');
});
function toggleTreeNode(node) {
if (!node || !node.classList.contains('tree-node')) return;
const expanded = node.getAttribute('aria-expanded') === 'true';
node.setAttribute('aria-expanded', expanded ? 'false' : 'true');
const children = node.querySelector(':scope > .tree-children');
if (children) {
if (expanded) { children.setAttribute('hidden', ''); }
else { children.removeAttribute('hidden'); }
}
}
// Drag-and-drop for library leafs (visual only - mockup)
document.addEventListener('dragstart', (e) => {
const draggable = e.target.closest('.lib-draggable');
if (!draggable) return;
draggable.classList.add('dragging');
const name = draggable.querySelector('.tree-name')?.textContent || 'Block';
e.dataTransfer.setData('text/plain', name);
e.dataTransfer.effectAllowed = 'copy';
});
document.addEventListener('dragend', (e) => {
const draggable = e.target.closest('.lib-draggable');
if (draggable) draggable.classList.remove('dragging');
});
})();
File diff suppressed because it is too large Load Diff
+195
View File
@@ -0,0 +1,195 @@
/* web-cad v6 Design Tokens, Typography, Base (with Drawer tokens) */
:root {
--font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', ui-monospace, monospace;
--fs-xs: 11px;
--fs-sm: 12px;
--fs-md: 13px;
--fs-lg: 14px;
--fs-xl: 16px;
--fs-2xl: 20px;
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-primary-light: #dbeafe;
--color-primary-50: #eff6ff;
--color-bg: #f8fafc;
--color-surface: #ffffff;
--color-surface-2: #f1f5f9;
--color-surface-3: #e2e8f0;
--color-border: #e2e8f0;
--color-border-strong: #cbd5e1;
--color-text: #0f172a;
--color-text-muted: #64748b;
--color-text-faint: #94a3b8;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
--color-ki: #8b5cf6;
--color-ki-light: #ede9fe;
--color-canvas-bg: #0f172a;
--color-canvas-bg-2: #1e293b;
--color-grid-minor: rgba(255,255,255,0.06);
--color-grid-major: rgba(255,255,255,0.12);
--color-grid-axis: rgba(96,165,250,0.28);
--color-online: #10b981;
--radius-xs: 3px;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--spacing-xs: 4px;
--spacing-sm: 6px;
--spacing-md: 10px;
--spacing-lg: 14px;
--spacing-xl: 20px;
/* Layout */
--topbar-h: 38px;
--ribbon-h: 72px;
--leftbar-w: 200px;
--rightbar-w: 300px;
--cmdline-h: 56px;
--status-h: 26px;
--canvas-toolbar-h: 38px;
/* Mobile-specific */
--mobile-right-tab-w: 56px;
--mobile-topbar-h: 44px;
--mobile-ribbon-h: 48px;
--drawer-w: min(320px, 88vw);
--shadow-sm: 0 1px 2px 0 rgba(15,23,42,0.04);
--shadow-md: 0 2px 6px -1px rgba(15,23,42,0.08), 0 1px 3px -1px rgba(15,23,42,0.06);
--shadow-lg: 0 10px 20px -4px rgba(15,23,42,0.10), 0 4px 8px -2px rgba(15,23,42,0.06);
--shadow-drawer: -8px 0 24px -4px rgba(15,23,42,0.18);
--t-fast: 120ms cubic-bezier(.2,.0,.2,1);
--t-base: 200ms cubic-bezier(.2,.0,.2,1);
--t-drawer: 280ms cubic-bezier(.32, .72, 0, 1);
}
[data-theme="dark"] {
--color-bg: #0b0e14;
--color-surface: #11151c;
--color-surface-2: #161b24;
--color-surface-3: #1d2330;
--color-border: #232a36;
--color-border-strong: #2f3849;
--color-text: #e2e8f0;
--color-text-muted: #94a3b8;
--color-text-faint: #64748b;
--color-primary-light: #1e3a8a;
--color-primary-50: #172554;
--color-ki-light: #2e1065;
--shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.3);
--shadow-md: 0 2px 6px -1px rgba(0,0,0,0.4);
--shadow-lg: 0 10px 20px -4px rgba(0,0,0,0.5);
--shadow-drawer: -8px 0 24px -4px rgba(0,0,0,0.6);
}
* { box-sizing: border-box; }
*::before, *::after { box-sizing: border-box; }
html, body {
height: 100%;
margin: 0;
font-family: var(--font-sans);
font-size: var(--fs-md);
background: var(--color-bg);
color: var(--color-text);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.5;
overflow: hidden;
/* iOS safe areas */
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}
body {
position: fixed;
inset: 0;
width: 100%;
}
button {
font-family: inherit;
font-size: inherit;
cursor: pointer;
background: transparent;
border: none;
color: inherit;
padding: 0;
-webkit-tap-highlight-color: transparent;
}
input, select, textarea {
font-family: inherit;
font-size: inherit;
}
a { color: var(--color-primary); text-decoration: none; }
a:hover { text-decoration: underline; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background: var(--color-border-strong);
border-radius: 10px;
border: 2px solid var(--color-bg);
}
::-webkit-scrollbar-thumb:hover { background: var(--color-text-faint); }
*:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
.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;
}
.skip-link {
position: absolute;
top: -40px;
left: 8px;
background: var(--color-primary);
color: white;
padding: 8px 12px;
border-radius: var(--radius-md);
z-index: 1000;
font-weight: 500;
}
.skip-link:focus { top: 8px; }
/* Scrim (drawer backdrop) */
.scrim {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.5);
z-index: 90;
opacity: 0;
visibility: hidden;
transition: opacity var(--t-drawer), visibility 0s linear var(--t-drawer);
}
.scrim.show {
opacity: 1;
visibility: visible;
transition: opacity var(--t-drawer), visibility 0s linear 0s;
}