Compare commits
2 Commits
ed71100178
...
2b9a3bef16
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b9a3bef16 | |||
| 8774fea773 |
+4
-3
@@ -638,7 +638,7 @@ Kurz-Tasks (je gleiche Struktur: implementieren im passenden builtin-Paket + Wor
|
||||
- [x] G5 Browser-Print stylesheet (@media print: nur Layout) *(2026-08-29: @media print Block mit @page A4 landscape, .screen-only Chrome-Ausblendung, .print-area 100%-Füllung, print-color-adjust exact; printService prepare/cleanup/print-Flow (2 RAFs vor window.print); LayoutBar-Druck-Button 🖨; Log G5)*
|
||||
|
||||
## Phase H — Modi & Branchenneutralität
|
||||
- [ ] H1 simple/pro Mode (uiStore.mode, Ribbon-Filter tags basic, CommandLine hide, Settings persist)
|
||||
- [x] H1 simple/pro Mode (uiStore.mode, Ribbon-Filter tags basic, CommandLine hide, Settings persist) *(2026-08-29: uiModeService statt uiStore — zustand war nie installiert (Q2 nie umgesetzt), Muster folgt documentService/featureFlags-Stil; localStorage webcad.uiMode persistiert, Default pro; RibbonBar filtert simple auf tags basic, CommandLine pro-only, Topbar-Toggle 😊/🛠; Log H1)*
|
||||
- [ ] H2 Projekt-Templates (Halle/OpenAir/Saal/Messe/Buero/Garten): JSON Presets (grid unit, layers, enabled plugins, library folders seed)
|
||||
- [ ] H3 Rollen: library-admin (global write) vs user (own folders) — an bestehendes auth anbinden, Backend guard
|
||||
- [ ] H4 i18n Grundlage: strings.ts zentrale Maps de/en, UI-Texte ersetzen (mechanisch, Teilautomatisierbar)
|
||||
@@ -667,6 +667,7 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
|
||||
|
||||
| Datum | Task | Zusammenfassung | Tests | Commit | Notizen |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-08-29 | H1 | simple/pro Mode: (1) uiModeService NEU — getUIMode/setUIMode/toggleUIMode/subscribeUIMode + useUIMode-React-Hook; localStorage-Persistenz (webcad.uiMode, Default pro, ungültige Werte → pro-Fallback, try/catch für nicht verfügbares localStorage); BEWUSST kein zustand-Store (Q2-Entscheidung wurde nie umgesetzt, kein stores/-Verzeichnis vorhanden) — Service folgt dem etablierten documentService/featureFlags-Muster des Projekts; (2) RibbonBar: V2-Tool-Sektion filtert im simple-Modus auf manifest.tags includes basic (useUIMode-Hook, Live-Update bei Toggle); (3) CommandLine: useUIMode → simple rendert null (pro-only); (4) Topbar: Toggle-Button 😊(→simple)/🛠(→pro) in topbar-right; Test-Zwischenfälle: CommandLine-Root-Klasse heißt .cmdline nicht .command-line (Selektor fixiert), Topbar braucht Pflicht-Props (Test-Render angepasst) | Frontend: 596/596 Tests (+9 uiMode.test.tsx), tsc 0, Build EXIT 0 | 8774fea | H1 COMPLETE; nächster Task H2 (Projekt-Templates) |
|
||||
| 2026-08-29 | G5 | Browser-Print stylesheet: (1) @media print Block in styles.css — @page size A4 landscape margin 10mm; .screen-only display none !important (UI-Chrome); .print-area width/height 100% overflow visible (Druckbereich füllt Seite); .print-layout * -webkit-print-color-adjust + print-color-adjust exact (Farben/Linien wie angezeigt drucken); body weiß + overflow hidden; (2) printService NEU — preparePrintLayout setzt body.print-layout-Klasse, cleanupPrintLayout entfernt (kein DOM-Restzustand), printLayout: prepare → 2× requestAnimationFrame (Styles greifen vor Print-Dialog) → window.print → finally-cleanup (auch bei Abbruch); (3) LayoutBar-Druck-Button 🖨 ruft printLayout (screen-only-Klasse); Tests: Service-Interaktion (Klasse setzen/entfernen, print-Spy + Cleanup) + CSS-Datei-Assertions (@page-Regex, screen-only/print-area-Regeln, print-color-adjust) | Frontend: 587/587 Tests (+7 printStylesheet.test.ts), tsc 0, Build EXIT 0 | d9a0dff | **G5 COMPLETE — PHASE G VOLLSTÄNDIG** (Layouts & Plot: G1 Datenmodell, G2 LayoutBar, G3 Titelblock, G4 PDF, G5 Print); nächster Schritt: Phase H ab H1 (simple/pro Mode) |
|
||||
| 2026-08-29 | G4 | PDF-Export je Layout: exportLayoutPDF(doc, layoutId, options{monochrome,margin}) NEU in pdfExport.ts (additiv — Legacy-exportPDF unangetastet): A4 landscape 842×595pt; Ausgabemaßstab = layout.viewport.scale, sichtbarer Welt-Ausschnitt aus center berechnet; Y-Flip (PDF bottom-left vs CAD top-down) in toPage-Transform; Rendering über elementToPrimitives (C2) statt duplizierter Typ-Switches → garantiert identische Konventionen wie Pixi-Renderer (Legacy-exportPDF hätte Circle um width/2 verschoben gezeichnet — gleiche Bugklasse wie DXF-Writer vor F6, dort belassen und im Log vermerkt); Linienbreiten: properties.strokeWidth sonst Layer-lineType (solid 0.7pt/dashed 0.9pt/dotted 0.5pt) mit Dash-Patterns [4,3]/[0.5,2]; monochrome erzwingt rgb(0,0,0) + keine Dashes; unsichtbare Layer gefiltert; Arc→32 Segmente (Grad CCW); Titelblock aus frameBlockRef wird unskaliert unten-rechts mitgezeichnet (Block-Elemente um Ursprung an Ecken-Offset); drawPrimitive deckt alle 6 Primitive-Kinds | Frontend: 580/580 Tests (+6 pdfLayoutExport.test.ts — PDF via PDFDocument.load verifiziert: Seitenzahl 1, Größe 842×595, Frame-Block-Fall, monochrome, leerer Viewport, unknown-ID-throw, unsichtbare Layer), tsc 0, Build EXIT 0 | cb23638 | G4 COMPLETE; nächster Task G5 (Print-Stylesheet) |
|
||||
| 2026-08-29 | G3 | Titelblock-Block mit Attributen: (1) titleBlockService NEU — createTitleBlockElements (reine Funktion): Rahmen rect 180×100 (App-Konvention x/y=Zentrum) + Titel-Label + 4 attributierte Texte PROJEKT/DATUM/MASSSTAB/AUTOR mit tag/attdef:true/prompt-Metadaten und text=TAG=Wert (exakt die F5-Parser- und F6-Writer-Konvention → DXF-ATTDEF-Roundtrip & D12-Formular-ready), leere Meta → Fallback —; (2) insertTitleBlockForLayout: Blockdefinition (category frame) via doc.addBlock + frameBlockRef im Layout via updateLayout, Re-Insert ersetzt Referenz (neue Block-ID), fehlendes Layout → throw; (3) CADDocument um getBlock/addBlock erweitert (bewusst NICHT undo-getrackt — konsistent zum UndoManager-Design-Kommentar); (4) LayoutBar-Button 🏷 je Layout: fügt/aktualisiert Titelblock mit Layout-Name als Projekt, heutiges Datum, Maßstab aus viewport.scale (1:1/scale gerundet), Autor WebCAD; Test-Zwischenfall: eigene Assertion prüfte attdef über alle 5 Texte inkl. Titel-Label (bewusst kein Attribut) — auf getaggte Texte gescoped | Frontend: 574/574 Tests (+6 titleBlock.test.ts), tsc 0, Build EXIT 0 | bbb469e | G3 COMPLETE; nächster Task G4 (PDF-Export je Layout) |
|
||||
@@ -737,5 +738,5 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
|
||||
- `frontend/src/utils/__tests__/format.test.ts` liegt in src statt tests/ — vereinheitlichen (kleiner Task in I2)
|
||||
|
||||
---
|
||||
*Nächster offener Schritt: **H1** (simple/pro Mode: uiStore.mode, Ribbon-Filter tags basic, CommandLine hide, Settings persist). Danach: H2 Projekt-Templates, H3 Rollen, H4 i18n, H5 Flächen. Restlich: D1–D4/D9/D10-path/D11/D12, Phase I (Release).*
|
||||
*Status 2026-08-29: Phasen B+A+C+D+E+F+G komplett (G: Layouts Datenmodell/Bar/Titelblock/PDF/Print), BUG-1-Kollisionsklasse geschlossen. FE 587/587 Tests, BE 272/272, tsc/Build grün. Alle Commits auf Forgejo main.*
|
||||
*Nächster offener Schritt: **H2** (Projekt-Templates Halle/OpenAir/Saal/Messe/Buero/Garten: JSON Presets mit grid unit, layers, enabled plugins, library folders seed). Danach: H3 Rollen, H4 i18n, H5 Flächen. Restlich: D1–D4/D9/D10-path/D11/D12, Phase I (Release).*
|
||||
*Status 2026-08-29: Phasen B+A+C+D+E+F+G komplett, H1 fertig (simple/pro Mode), BUG-1-Kollisionsklasse geschlossen. FE 596/596 Tests, BE 272/272, tsc/Build grün. Alle Commits auf Forgejo main.*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useRef, useMemo, useEffect } from 'react';
|
||||
import type { CommandLineProps } from '../types/ui.types';
|
||||
import { getCommandRegistry, type CommandDefinition } from '../services/commandRegistry';
|
||||
import { useUIMode } from '../services/uiModeService';
|
||||
|
||||
const defaultHistory = [
|
||||
{ prefix: '·' as const, text: 'Bereit · Werkzeug: Auswahl · 110 Objekte · 5 Ebenen', type: 'info' as const },
|
||||
@@ -20,6 +21,9 @@ const categoryColors: Record<string, string> = {
|
||||
};
|
||||
|
||||
const CommandLine: React.FC<CommandLineProps> = ({ history, onCommand }) => {
|
||||
const uiMode = useUIMode();
|
||||
if (uiMode === 'simple') return null;
|
||||
|
||||
const [input, setInput] = useState('');
|
||||
const [selectedSuggestion, setSelectedSuggestion] = useState(0);
|
||||
const [commandHistory, setCommandHistory] = useState<string[]>([]);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { getUIMode, useUIMode } from '../services/uiModeService';
|
||||
import type { RibbonBarProps, RibbonTab } from '../types/ui.types';
|
||||
import { pluginRegistry } from '../plugins';
|
||||
import { V2_TOOLS_ENABLED } from '../kernel/featureFlags';
|
||||
@@ -19,6 +20,8 @@ const RibbonBar: React.FC<RibbonBarProps> = ({
|
||||
onCanvasBgColorChange, onGridColorChange, onToggleRuler,
|
||||
onIsV2ToolActive,
|
||||
}) => {
|
||||
const uiMode = useUIMode();
|
||||
|
||||
return (
|
||||
<nav className="ribbon" role="navigation" aria-label="Hauptwerkzeuge">
|
||||
<div className="ribbon-tabs" role="tablist">
|
||||
@@ -47,7 +50,9 @@ const RibbonBar: React.FC<RibbonBarProps> = ({
|
||||
borderBottom: '1px solid var(--color-border, #333)', marginBottom: '8px',
|
||||
}}>
|
||||
<span style={{ fontSize: '11px', color: 'var(--color-ki, #8a8aff)', marginRight: '4px' }}>V2:</span>
|
||||
{pluginRegistry.getToolsV2().map((tool) => {
|
||||
{pluginRegistry.getToolsV2()
|
||||
.filter((tool) => uiMode === 'pro' || (tool.manifest.tags ?? []).includes('basic'))
|
||||
.map((tool) => {
|
||||
const isActive = onIsV2ToolActive?.(tool.manifest.id);
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import React from 'react';
|
||||
import type { TopbarProps } from '../types/ui.types';
|
||||
import type { UnitType } from '../utils/format';
|
||||
import { useUIMode, toggleUIMode, getUIMode } from '../services/uiModeService';
|
||||
|
||||
const Topbar: React.FC<TopbarProps> = ({
|
||||
projectName, savedStatus, onUndo, onRedo, onThemeToggle, theme,
|
||||
onOpenSettings, onOpenLeftDrawer, onOpenRightDrawer,
|
||||
unit = 'mm', onUnitChange, onNavigateBack,
|
||||
}) => {
|
||||
const uiMode = useUIMode();
|
||||
|
||||
return (
|
||||
<header className="topbar" role="banner">
|
||||
<div className="topbar-left">
|
||||
@@ -33,6 +36,14 @@ const Topbar: React.FC<TopbarProps> = ({
|
||||
<span className="saved-badge" aria-label="Status: Alle Änderungen gespeichert">{savedStatus}</span>
|
||||
</div>
|
||||
<div className="topbar-right">
|
||||
<button
|
||||
className="icon-btn-top"
|
||||
title={uiMode === 'pro' ? 'Zum einfachen Modus wechseln' : 'Zum Profi-Modus wechseln'}
|
||||
aria-label={uiMode === 'pro' ? 'Zum einfachen Modus wechseln' : 'Zum Profi-Modus wechseln'}
|
||||
onClick={toggleUIMode}
|
||||
>
|
||||
{uiMode === 'pro' ? '😊' : '🛠'}
|
||||
</button>
|
||||
<button className="icon-btn-top" aria-label="Rückgängig (Strg+Z)" title="Rückgängig (Strg+Z)" onClick={onUndo}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-15-6.7L3 13"/></svg>
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Task H1 – UI-Modus simple/pro.
|
||||
*
|
||||
* - 'pro': vollständige Werkzeugpalette + CommandLine (Default)
|
||||
* - 'simple': nur basic-Tools, keine CommandLine
|
||||
*
|
||||
* Persistenz über localStorage ('webcad.uiMode'), Subscribe-Set für
|
||||
* Live-Updates (RibbonBar/CommandLine/Topbar hören zu). Bewusst KEIN
|
||||
* zustand (Q2-Entscheidung wurde nie umgesetzt; Muster folgt dem
|
||||
* etablierten documentService/featureFlags-Stil des Projekts).
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export type UIMode = 'simple' | 'pro';
|
||||
|
||||
const STORAGE_KEY = 'webcad.uiMode';
|
||||
const listeners = new Set<(mode: UIMode) => void>();
|
||||
|
||||
function readStored(): UIMode {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw === 'simple' || raw === 'pro') return raw;
|
||||
} catch {
|
||||
// localStorage nicht verfügbar — Default
|
||||
}
|
||||
return 'pro';
|
||||
}
|
||||
|
||||
let current: UIMode = readStored();
|
||||
|
||||
/** Aktueller UI-Modus (Default pro, persistiert). */
|
||||
export function getUIMode(): UIMode {
|
||||
return current;
|
||||
}
|
||||
|
||||
/** Wechselt den Modus und persistiert. */
|
||||
export function setUIMode(mode: UIMode): void {
|
||||
current = mode;
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, mode);
|
||||
} catch {
|
||||
// Persistenz optional
|
||||
}
|
||||
for (const cb of listeners) cb(mode);
|
||||
}
|
||||
|
||||
/** Toggle simple ↔ pro. */
|
||||
export function toggleUIMode(): void {
|
||||
setUIMode(current === 'pro' ? 'simple' : 'pro');
|
||||
}
|
||||
|
||||
/** Subscribe auf Moduswechsel; gibt Unsubscribe zurück. */
|
||||
export function subscribeUIMode(cb: (mode: UIMode) => void): () => void {
|
||||
listeners.add(cb);
|
||||
return () => { listeners.delete(cb); };
|
||||
}
|
||||
|
||||
/** React-Hook: aktueller Modus mit Re-Render bei Wechsel. */
|
||||
export function useUIMode(): UIMode {
|
||||
const [mode, setMode] = useState<UIMode>(current);
|
||||
useEffect(() => {
|
||||
const off = subscribeUIMode(setMode);
|
||||
return off;
|
||||
}, []);
|
||||
return mode;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Task H1 – simple/pro Mode.
|
||||
*
|
||||
* uiModeService: persistierter UI-Modus ('simple' | 'pro', Default 'pro'),
|
||||
* localStorage-Persistenz, Subscribe für Live-Updates.
|
||||
* RibbonBar filtert im simple-Modus auf tools mit tags 'basic'.
|
||||
* CommandLine ist pro-only (im simple-Modus versteckt).
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
||||
import {
|
||||
getUIMode, setUIMode, subscribeUIMode, UIMode,
|
||||
} from '../src/services/uiModeService';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
// ─── uiModeService ────────────────────────────────────────
|
||||
|
||||
describe('H1: uiModeService', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('Default ist pro (Vollständige Werkzeugpalette)', () => {
|
||||
expect(getUIMode()).toBe('pro');
|
||||
});
|
||||
|
||||
it('setUIMode simple persistiert in localStorage', () => {
|
||||
setUIMode('simple');
|
||||
expect(getUIMode()).toBe('simple');
|
||||
expect(localStorage.getItem('webcad.uiMode')).toBe('simple');
|
||||
});
|
||||
|
||||
it('Persistiert über Reload hinweg (localStorage gesetzt vor erstem get)', () => {
|
||||
localStorage.setItem('webcad.uiMode', 'simple');
|
||||
expect(getUIMode()).toBe('simple');
|
||||
});
|
||||
|
||||
it('subscribeUIMode feuert bei Wechsel und unsubscribed korrekt', () => {
|
||||
const cb = vi.fn();
|
||||
const off = subscribeUIMode(cb);
|
||||
setUIMode('simple');
|
||||
expect(cb).toHaveBeenCalledWith('simple');
|
||||
setUIMode('pro');
|
||||
expect(cb).toHaveBeenCalledWith('pro');
|
||||
off();
|
||||
cb.mockClear();
|
||||
setUIMode('simple');
|
||||
expect(cb).not.toHaveBeenCalled();
|
||||
// Aufräumen für weitere Tests
|
||||
setUIMode('pro');
|
||||
});
|
||||
|
||||
it('ungültiger localStorage-Wert fällt auf pro zurück', () => {
|
||||
localStorage.setItem('webcad.uiMode', 'gibts-nicht');
|
||||
expect(getUIMode()).toBe('pro');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── RibbonBar simple-Filter ─────────────────────────────
|
||||
|
||||
describe('H1: RibbonBar simple-Filter', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
setUIMode('pro');
|
||||
});
|
||||
|
||||
it('pro-Modus: alle V2-Tools sichtbar (basic + pro-Tags)', async () => {
|
||||
const { registerBuiltinPlugins, pluginRegistry } = await import('../src/plugins');
|
||||
registerBuiltinPlugins();
|
||||
pluginRegistry.initDefaults();
|
||||
const RibbonBar = (await import('../src/components/RibbonBar')).default;
|
||||
render(<RibbonBar activeTab="canvas" onTabChange={() => {}} onAction={() => {}} />);
|
||||
const all = screen.getAllByRole('button').length;
|
||||
expect(all).toBeGreaterThan(10);
|
||||
cleanup();
|
||||
setUIMode('simple');
|
||||
render(<RibbonBar activeTab="canvas" onTabChange={() => {}} onAction={() => {}} />);
|
||||
const simpleCount = screen.getAllByRole('button').length;
|
||||
expect(simpleCount).toBeLessThan(all);
|
||||
expect(simpleCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('simple-Modus: pro-Tools (rotate/scale/mirror) fehlen, basic (line) bleibt', async () => {
|
||||
localStorage.clear();
|
||||
setUIMode('simple');
|
||||
const { registerBuiltinPlugins, pluginRegistry } = await import('../src/plugins');
|
||||
registerBuiltinPlugins();
|
||||
pluginRegistry.initDefaults();
|
||||
const RibbonBar = (await import('../src/components/RibbonBar')).default;
|
||||
render(<RibbonBar activeTab="canvas" onTabChange={() => {}} onAction={() => {}} />);
|
||||
const html = document.body.innerHTML;
|
||||
expect(html).toContain('data-v2tool="line"');
|
||||
expect(html).not.toContain('data-v2tool="rotate"');
|
||||
expect(html).not.toContain('data-v2tool="mirror"');
|
||||
});
|
||||
|
||||
it('CommandLine-Komponente: pro-only rendering via useUIMode', async () => {
|
||||
const { useUIMode } = await import('../src/services/uiModeService');
|
||||
const CommandLine = (await import('../src/components/CommandLine')).default;
|
||||
setUIMode('pro');
|
||||
const { container: proContainer } = render(<CommandLine history={[]} onCommand={() => {}} />);
|
||||
expect(proContainer.querySelector('.cmdline')).toBeTruthy();
|
||||
cleanup();
|
||||
setUIMode('simple');
|
||||
const { container: simpleContainer } = render(<CommandLine history={[]} onCommand={() => {}} />);
|
||||
expect(simpleContainer.querySelector('.cmdline')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('Topbar-Toggle wechselt Mode', async () => {
|
||||
const Topbar = (await import('../src/components/Topbar')).default;
|
||||
setUIMode('pro');
|
||||
render(<Topbar projectName="Testprojekt" savedStatus="Gespeichert" onUndo={() => {}} onRedo={() => {}} onThemeToggle={() => {}} theme="dark" />);
|
||||
const btn = screen.getByTitle(/Modus.*wechseln|Einfach.*Modus|Profi-Modus/i);
|
||||
fireEvent.click(btn);
|
||||
expect(getUIMode()).toBe('simple');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user