{
onSelect(obj.id)
if (isGeneratePage) {
diff --git a/frontend/src/components/SentencesList.tsx b/frontend/src/components/SentencesList.tsx
index addb7f6..2ef3d9c 100644
--- a/frontend/src/components/SentencesList.tsx
+++ b/frontend/src/components/SentencesList.tsx
@@ -17,14 +17,10 @@ export default function SentencesList({ sentences }: Props) {
{[...sentences].reverse().map((s, i) => (
-
- Einfach:
-
+
Einfach
{s.question_simple_en}
{s.answer_simple_en}
-
- Fortgeschritten:
-
+
Fortgeschritten
{s.question_advanced_en}
{s.answer_advanced_en}
diff --git a/frontend/src/components/Topbar.tsx b/frontend/src/components/Topbar.tsx
new file mode 100644
index 0000000..61c9ed3
--- /dev/null
+++ b/frontend/src/components/Topbar.tsx
@@ -0,0 +1,85 @@
+import { useNavigate } from 'react-router-dom'
+import { useAuth } from '../context/AuthContext'
+import { useTheme } from '../context/ThemeContext'
+import type { ReactNode } from 'react'
+
+const SunIcon = () => (
+
+)
+
+const MoonIcon = () => (
+
+)
+
+const LogoutIcon = () => (
+
+)
+
+const CrosshairIcon = () => (
+
+)
+
+interface TopbarProps {
+ page: 'draw' | 'generate'
+ center?: ReactNode
+}
+
+export default function Topbar({ page, center }: TopbarProps) {
+ const navigate = useNavigate()
+ const { logout } = useAuth()
+ const { dark, toggle } = useTheme()
+
+ return (
+
+
+
+
+
+ Content Mentor
+
+
+
+
+ {center && {center}
}
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx
new file mode 100644
index 0000000..8b27d70
--- /dev/null
+++ b/frontend/src/context/AuthContext.tsx
@@ -0,0 +1,25 @@
+import { createContext, useContext, useState, type ReactNode } from 'react'
+
+interface AuthContextType {
+ token: string | null
+ login: (token: string) => void
+ logout: () => void
+}
+
+const AuthContext = createContext
(null)
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+ const [token, setToken] = useState(null)
+
+ return (
+ setToken(null) }}>
+ {children}
+
+ )
+}
+
+export function useAuth() {
+ const ctx = useContext(AuthContext)
+ if (!ctx) throw new Error('useAuth must be used within AuthProvider')
+ return ctx
+}
diff --git a/frontend/src/context/ThemeContext.tsx b/frontend/src/context/ThemeContext.tsx
new file mode 100644
index 0000000..5a168ed
--- /dev/null
+++ b/frontend/src/context/ThemeContext.tsx
@@ -0,0 +1,26 @@
+import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
+
+interface ThemeContextType {
+ dark: boolean
+ toggle: () => void
+}
+
+const ThemeContext = createContext({ dark: false, toggle: () => {} })
+
+export function ThemeProvider({ children }: { children: ReactNode }) {
+ const [dark, setDark] = useState(
+ () => window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false
+ )
+
+ useEffect(() => {
+ document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light')
+ }, [dark])
+
+ return (
+ setDark(d => !d) }}>
+ {children}
+
+ )
+}
+
+export const useTheme = () => useContext(ThemeContext)
diff --git a/frontend/src/index.css b/frontend/src/index.css
index b3a204c..060ee04 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1,112 +1,336 @@
-body {
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+/* =====================================================
+ DESIGN TOKENS
+ ===================================================== */
+:root {
+ --font: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
+
+ --bg: #F2F4F9;
+ --canvas-bg: #E6E9F2;
+ --surface: #FFFFFF;
+ --surface-2: #F6F8FC;
+ --surface-3: #EDF0F7;
+ --border: #DDE1EE;
+ --border-focus: #6366F1;
+
+ --primary: #5C6CF6;
+ --primary-hover: #4A5AE5;
+ --primary-active: #3A4AD5;
+ --primary-muted: #EEF0FE;
+ --primary-muted-fg: #4A5AE5;
+
+ --text-1: #0D1526;
+ --text-2: #4B5568;
+ --text-3: #8C95A8;
+ --text-inv: #FFFFFF;
+
+ --success: #059669;
+ --success-bg: #ECFDF5;
+ --danger: #DC2626;
+ --danger-bg: #FEF2F2;
+ --warning: #D97706;
+
+ --shadow-xs: 0 1px 2px rgba(13,21,38,.05);
+ --shadow-sm: 0 2px 6px rgba(13,21,38,.07);
+ --shadow-md: 0 6px 20px rgba(13,21,38,.09);
+ --shadow-lg: 0 16px 40px rgba(13,21,38,.12);
+
+ --r-sm: 5px;
+ --r-md: 8px;
+ --r-lg: 12px;
+ --r-xl: 16px;
+ --r-full: 999px;
+
+ --topbar-h: 56px;
+ --sidebar-w: 252px;
+ --sidebar-rw: 276px;
+ --panel-gap: 1px;
+}
+
+[data-theme="dark"] {
+ --bg: #0C0F1A;
+ --canvas-bg: #080B14;
+ --surface: #141828;
+ --surface-2: #1A1F32;
+ --surface-3: #1F253A;
+ --border: #272D45;
+
+ --primary: #7B8BFF;
+ --primary-hover: #8D9BFF;
+ --primary-active: #6A7AEE;
+ --primary-muted: #1C2348;
+ --primary-muted-fg: #A5B4FC;
+
+ --text-1: #EDF0FA;
+ --text-2: #8892B0;
+ --text-3: #4A5270;
+ --text-inv: #0D1526;
+
+ --success: #34D399;
+ --success-bg: #052E1E;
+ --danger: #F87171;
+ --danger-bg: #3B0E0E;
+ --warning: #FBBF24;
+
+ --shadow-xs: 0 1px 2px rgba(0,0,0,.25);
+ --shadow-sm: 0 2px 6px rgba(0,0,0,.30);
+ --shadow-md: 0 6px 20px rgba(0,0,0,.40);
+ --shadow-lg: 0 16px 40px rgba(0,0,0,.55);
+}
+
+/* =====================================================
+ RESET
+ ===================================================== */
+*, *::before, *::after {
+ box-sizing: border-box;
margin: 0;
padding: 0;
- background: #f5f7fb;
- color: #222;
}
-.container {
- max-width: 1180px;
- margin: 24px auto;
- padding: 16px 20px 32px;
- background: #ffffff;
- border-radius: 12px;
- box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
+html, body, #root {
+ height: 100%;
}
-h1 {
- margin-top: 0;
- margin-bottom: 16px;
- font-size: 1.6rem;
+body {
+ font-family: var(--font);
+ font-size: 13.5px;
+ line-height: 1.5;
+ background: var(--bg);
+ color: var(--text-1);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ overflow: hidden;
}
-label {
- font-weight: 500;
-}
-
-.panel {
- margin: 12px 0;
- display: flex;
- align-items: center;
- gap: 8px;
- flex-wrap: wrap;
-}
-
-.image-nav {
- justify-content: space-between;
- gap: 8px;
-}
-
-.image-nav button {
- padding: 4px 10px;
- border-radius: 999px;
-}
-
-.image-nav-left {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.page-switch select {
- padding: 4px 8px;
- border-radius: 999px;
- border: 1px solid #cbd5f0;
- background: #f9fbff;
- font-size: 0.9rem;
- min-width: unset;
-}
-
-.main-layout {
- display: flex;
- gap: 16px;
- align-items: flex-start;
-}
-
-.objects-pane {
- flex: 0 0 260px;
-}
-
-.left-pane {
- flex: 1 1 auto;
-}
-
-.right-pane {
- flex: 0 0 280px;
+/* =====================================================
+ APP SHELL
+ ===================================================== */
+.app-shell {
display: flex;
flex-direction: column;
- gap: 12px;
+ height: 100vh;
+ overflow: hidden;
}
-.sentences-pane {
- flex: 0 0 300px;
+/* =====================================================
+ TOPBAR
+ ===================================================== */
+.topbar {
+ height: var(--topbar-h);
+ flex-shrink: 0;
+ background: var(--surface);
+ border-bottom: 1px solid var(--border);
display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-.mode-option {
- display: inline-flex;
align-items: center;
+ padding: 0 16px;
gap: 4px;
- font-weight: 400;
+ z-index: 50;
}
-select {
- min-width: 220px;
- padding: 6px 10px;
- border-radius: 8px;
- border: 1px solid #cbd5f0;
- background: #f9fbff;
+.topbar-brand {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-right: 8px;
+ user-select: none;
}
-.canvas-wrapper {
- border-radius: 12px;
- border: 1px solid #d4ddf5;
- background: #f1f5ff;
- overflow: visible;
- padding: 8px;
+.topbar-logo-icon {
+ display: flex;
+ align-items: center;
+ color: var(--primary);
+}
+
+.topbar-brand-name {
+ font-size: 14px;
+ font-weight: 650;
+ letter-spacing: -0.01em;
+ color: var(--text-1);
+}
+
+.topbar-nav {
+ display: flex;
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ border-radius: var(--r-md);
+ padding: 3px;
+ gap: 2px;
+ margin-right: 8px;
+}
+
+.topbar-tab {
+ padding: 5px 14px;
+ border-radius: 6px;
+ border: none;
+ background: transparent;
+ color: var(--text-2);
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ box-shadow: none;
+}
+
+.topbar-tab:hover:not(.active) {
+ background: var(--surface-3);
+ color: var(--text-1);
+ transform: none;
+ box-shadow: none;
+}
+
+.topbar-tab.active {
+ background: var(--surface);
+ color: var(--text-1);
+ font-weight: 600;
+ box-shadow: var(--shadow-xs);
+ transform: none;
+}
+
+.topbar-center {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.topbar-actions {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-left: auto;
+}
+
+.topbar-divider {
+ width: 1px;
+ height: 20px;
+ background: var(--border);
+ margin: 0 2px;
+}
+
+/* =====================================================
+ IMAGE NAVIGATION (in topbar)
+ ===================================================== */
+.image-nav {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ border-radius: var(--r-md);
+ padding: 5px 10px;
+}
+
+.image-counter {
+ display: flex;
+ align-items: baseline;
+ gap: 4px;
+ min-width: 80px;
+ justify-content: center;
+ font-variant-numeric: tabular-nums;
+}
+
+.image-counter-num {
+ font-size: 15px;
+ font-weight: 700;
+ color: var(--text-1);
+}
+
+.image-counter-sep {
+ font-size: 13px;
+ color: var(--text-3);
+}
+
+.image-counter-total {
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--text-2);
+}
+
+.image-counter-empty {
+ font-size: 13px;
+ color: var(--text-3);
+ font-style: italic;
+}
+
+/* =====================================================
+ WORKSPACE
+ ===================================================== */
+.workspace {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+ min-height: 0;
+}
+
+/* =====================================================
+ SIDEBAR
+ ===================================================== */
+.sidebar {
+ width: var(--sidebar-w);
+ flex-shrink: 0;
+ background: var(--surface);
+ border-right: 1px solid var(--border);
+ overflow-y: auto;
+ overflow-x: hidden;
+ display: flex;
+ flex-direction: column;
+ scrollbar-width: thin;
+ scrollbar-color: var(--border) transparent;
+}
+
+.sidebar--right {
+ width: var(--sidebar-rw);
+ border-right: none;
+ border-left: 1px solid var(--border);
+}
+
+.sidebar::-webkit-scrollbar { width: 4px; }
+.sidebar::-webkit-scrollbar-track { background: transparent; }
+.sidebar::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
+
+.sidebar-panel {
+ padding: 14px 14px;
+ border-bottom: 1px solid var(--border);
+}
+
+.sidebar-panel:last-child {
+ border-bottom: none;
+ flex: 1;
+}
+
+.sidebar-heading {
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: var(--text-3);
+ margin-bottom: 10px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+/* =====================================================
+ CANVAS AREA
+ ===================================================== */
+.canvas-area {
+ flex: 1;
+ background: var(--canvas-bg);
+ overflow: auto;
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ padding: 28px;
+ scrollbar-width: thin;
+ scrollbar-color: var(--border) transparent;
+}
+
+.canvas-frame {
+ background: var(--surface);
+ border-radius: var(--r-lg);
+ box-shadow: var(--shadow-lg);
+ overflow: hidden;
+ display: inline-flex;
+ max-width: 100%;
}
canvas {
@@ -115,84 +339,451 @@ canvas {
height: auto;
}
+/* =====================================================
+ BUTTONS
+ ===================================================== */
button {
- padding: 8px 14px;
- border-radius: 999px;
- border: none;
- background: #2563eb;
- color: white;
- font-weight: 500;
+ font-family: var(--font);
+ font-size: 13px;
cursor: pointer;
- box-shadow: 0 4px 10px rgba(37, 99, 235, 0.4);
- transition: background 0.15s ease, box-shadow 0.15s ease, transform 0.1s ease;
+ transition: all 0.15s ease;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ white-space: nowrap;
+ border: none;
+ border-radius: var(--r-md);
}
-button:disabled {
- background: #9ca3af;
- cursor: not-allowed;
- box-shadow: none;
+/* Primary */
+.btn-primary,
+button:not([class]) {
+ background: var(--primary);
+ color: var(--text-inv);
+ font-weight: 600;
+ padding: 7px 14px;
+ box-shadow: 0 1px 3px rgba(92,108,246,.35);
}
-button:not(:disabled):hover {
- background: #1d4ed8;
- box-shadow: 0 6px 18px rgba(37, 99, 235, 0.5);
+.btn-primary:hover:not(:disabled),
+button:not([class]):hover:not(:disabled) {
+ background: var(--primary-hover);
+ box-shadow: 0 3px 10px rgba(92,108,246,.45);
transform: translateY(-1px);
}
-.status {
- font-size: 0.9rem;
+.btn-primary:active:not(:disabled),
+button:not([class]):active:not(:disabled) {
+ background: var(--primary-active);
+ transform: translateY(0);
+ box-shadow: none;
}
-.status.ok {
- color: #16a34a;
+/* Ghost */
+.btn-ghost {
+ background: transparent;
+ color: var(--text-2);
+ border: 1px solid var(--border);
+ font-weight: 500;
+ padding: 7px 12px;
}
-.status.error {
- color: #dc2626;
+.btn-ghost:hover:not(:disabled) {
+ background: var(--surface-2);
+ color: var(--text-1);
+ border-color: var(--text-3);
+ transform: none;
+ box-shadow: none;
}
-.sidebar-section {
- border-radius: 10px;
- border: 1px solid #e5e7eb;
- padding: 10px 12px;
- background: #f9fafb;
+/* Danger ghost */
+.btn-danger {
+ color: var(--danger);
+ border-color: transparent;
+}
+.btn-danger:hover:not(:disabled) {
+ background: var(--danger-bg);
+ border-color: var(--danger);
+ color: var(--danger);
}
-.sidebar-section h2 {
- font-size: 1rem;
- margin: 0 0 6px;
+/* Icon button */
+.btn-icon {
+ background: transparent;
+ color: var(--text-2);
+ width: 32px;
+ height: 32px;
+ border-radius: var(--r-md);
+ padding: 0;
+ flex-shrink: 0;
}
-.sidebar-row {
- display: flex;
- flex-direction: column;
- gap: 4px;
+.btn-icon:hover:not(:disabled) {
+ background: var(--surface-2);
+ color: var(--text-1);
+ transform: none;
+ box-shadow: none;
+}
+
+.btn-icon:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+ background: transparent;
+}
+
+/* Sizes */
+.btn-sm {
+ font-size: 12.5px;
+ padding: 5px 11px;
+}
+
+.btn-block {
+ width: 100%;
margin-top: 6px;
}
-.sidebar-row input[type="text"] {
- padding: 6px 8px;
- border-radius: 6px;
- border: 1px solid #d1d5db;
- font-size: 0.9rem;
+/* Disabled state */
+button:disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+ box-shadow: none;
+ transform: none;
}
-.detail-value {
- padding: 4px 6px;
- border-radius: 6px;
- border: 1px solid #e5e7eb;
- background: #f9fafb;
- font-size: 0.9rem;
- min-height: 24px;
+/* =====================================================
+ BADGE
+ ===================================================== */
+.badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 18px;
+ height: 18px;
+ padding: 0 5px;
+ border-radius: var(--r-full);
+ background: var(--primary-muted);
+ color: var(--primary-muted-fg);
+ font-size: 11px;
+ font-weight: 700;
+ line-height: 1;
}
+/* =====================================================
+ FORM ELEMENTS
+ ===================================================== */
+.field {
+ margin-bottom: 8px;
+}
+
+.field:last-of-type {
+ margin-bottom: 0;
+}
+
+.field-label {
+ display: block;
+ font-size: 11.5px;
+ font-weight: 600;
+ color: var(--text-2);
+ margin-bottom: 4px;
+ letter-spacing: 0.01em;
+}
+
+.field-input,
+input[type="text"],
+input[type="email"],
+input[type="password"] {
+ width: 100%;
+ padding: 7px 10px;
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ border-radius: var(--r-md);
+ color: var(--text-1);
+ font-family: var(--font);
+ font-size: 13px;
+ transition: border-color 0.15s, box-shadow 0.15s;
+ outline: none;
+}
+
+.field-input:focus,
+input[type="text"]:focus,
+input[type="email"]:focus,
+input[type="password"]:focus {
+ border-color: var(--primary);
+ box-shadow: 0 0 0 3px rgba(92,108,246,.12);
+ background: var(--surface);
+}
+
+select {
+ width: 100%;
+ padding: 7px 10px;
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ border-radius: var(--r-md);
+ color: var(--text-1);
+ font-family: var(--font);
+ font-size: 13px;
+ cursor: pointer;
+ outline: none;
+ transition: border-color 0.15s;
+ min-width: unset;
+}
+
+select:focus {
+ border-color: var(--primary);
+ box-shadow: 0 0 0 3px rgba(92,108,246,.12);
+}
+
+/* =====================================================
+ MODE SELECTOR (Rect / Polygon toggle)
+ ===================================================== */
+.mode-group {
+ display: flex;
+ gap: 4px;
+}
+
+.mode-btn {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 6px 10px;
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ border-radius: var(--r-md);
+ color: var(--text-2);
+ font-size: 12.5px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s;
+ user-select: none;
+}
+
+.mode-btn input[type="radio"] {
+ display: none;
+}
+
+.mode-btn:hover {
+ background: var(--surface-3);
+ color: var(--text-1);
+}
+
+.mode-btn.active {
+ background: var(--primary-muted);
+ border-color: var(--primary);
+ color: var(--primary-muted-fg);
+ font-weight: 600;
+}
+
+/* =====================================================
+ ACTION GROUP
+ ===================================================== */
+.action-group {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ margin-top: 10px;
+}
+
+/* =====================================================
+ SELECTIONS LIST
+ ===================================================== */
+.selections-list {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ max-height: 140px;
+ overflow-y: auto;
+ margin-bottom: 2px;
+ scrollbar-width: thin;
+ scrollbar-color: var(--border) transparent;
+}
+
+.selection-chip {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 8px;
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ border-radius: var(--r-sm);
+ font-size: 12px;
+}
+
+.selection-chip-num {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background: var(--primary-muted);
+ color: var(--primary-muted-fg);
+ font-size: 10px;
+ font-weight: 700;
+ flex-shrink: 0;
+}
+
+.selection-chip-type {
+ font-weight: 600;
+ color: var(--text-1);
+}
+
+.selection-chip-info {
+ color: var(--text-3);
+ font-variant-numeric: tabular-nums;
+ margin-left: auto;
+}
+
+/* =====================================================
+ OBJECTS LIST
+ ===================================================== */
.objects-list {
display: flex;
flex-direction: column;
- gap: 8px;
- max-height: 260px;
+ gap: 4px;
+ max-height: calc(100vh - 240px);
overflow-y: auto;
- padding-right: 4px;
+ scrollbar-width: thin;
+ scrollbar-color: var(--border) transparent;
+}
+
+.object-item {
+ display: flex;
+ flex-direction: column;
+ border-radius: var(--r-md);
+ border: 1px solid var(--border);
+ background: var(--surface-2);
+ cursor: pointer;
+ transition: border-color 0.15s, background 0.15s;
+ overflow: hidden;
+}
+
+.object-item:hover {
+ background: var(--surface-3);
+ border-color: var(--text-3);
+}
+
+.object-item.selected {
+ border-color: var(--primary);
+ background: var(--primary-muted);
+}
+
+.object-item-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 7px 8px;
+ flex-wrap: nowrap;
+}
+
+.object-item-header input[type="checkbox"] {
+ width: 14px;
+ height: 14px;
+ flex-shrink: 0;
+ accent-color: var(--primary);
+ cursor: pointer;
+}
+
+.object-item img {
+ width: 36px;
+ height: 36px;
+ object-fit: cover;
+ border-radius: var(--r-sm);
+ border: 1px solid var(--border);
+ flex-shrink: 0;
+}
+
+.object-hierarchy-select {
+ width: 42px !important;
+ min-width: 0 !important;
+ padding: 3px 4px !important;
+ font-size: 11px !important;
+ border-radius: var(--r-sm) !important;
+ flex-shrink: 0;
+}
+
+.object-parent-select {
+ width: 48px !important;
+ min-width: 0 !important;
+ padding: 3px 4px !important;
+ font-size: 11px !important;
+ border-radius: var(--r-sm) !important;
+ background: var(--primary-muted) !important;
+ border-color: var(--primary) !important;
+ color: var(--primary-muted-fg) !important;
+ flex-shrink: 0;
+}
+
+.object-item-text {
+ display: flex;
+ flex-direction: column;
+ font-size: 12px;
+ flex: 1;
+ min-width: 0;
+ gap: 1px;
+}
+
+.object-item-text strong {
+ font-size: 12.5px;
+ font-weight: 600;
+ color: var(--text-1);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.object-item-text span {
+ color: var(--text-3);
+ font-size: 11px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.object-icon-button {
+ padding: 4px 6px !important;
+ font-size: 12px !important;
+ background: transparent !important;
+ color: var(--text-3) !important;
+ border: none !important;
+ box-shadow: none !important;
+ border-radius: var(--r-sm) !important;
+ flex-shrink: 0;
+}
+
+.object-icon-button:hover:not(:disabled) {
+ background: var(--surface-3) !important;
+ color: var(--text-1) !important;
+ transform: none !important;
+ box-shadow: none !important;
+}
+
+.object-item-details {
+ display: none;
+ flex-direction: column;
+ gap: 6px;
+ padding: 8px 10px;
+ border-top: 1px solid var(--border);
+ background: var(--surface);
+}
+
+.object-item-details.visible {
+ display: flex;
+}
+
+.object-item-details label {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--text-3);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+.object-item-details input[type="text"] {
+ padding: 5px 8px;
+ font-size: 12px;
+ border-radius: var(--r-sm);
}
.objects-tags {
@@ -203,180 +794,268 @@ button:not(:disabled):hover {
}
.object-tag {
- padding: 2px 6px;
- border-radius: 999px;
- background: #e5e7eb;
- font-size: 0.75rem;
+ padding: 2px 8px;
+ border-radius: var(--r-full);
+ background: var(--surface-3);
+ border: 1px solid var(--border);
+ font-size: 11px;
+ color: var(--text-2);
white-space: nowrap;
}
-.object-item {
+/* =====================================================
+ DETAILS PANEL
+ ===================================================== */
+.sidebar-section {
+ margin-bottom: 0;
+}
+
+.sidebar-section h2 {
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: var(--text-3);
+ margin-bottom: 8px;
+}
+
+.sidebar-section h3 {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--text-2);
+ margin-top: 10px;
+ margin-bottom: 6px;
+ letter-spacing: 0.03em;
+}
+
+.sidebar-row {
display: flex;
flex-direction: column;
- gap: 4px;
- padding: 4px 6px;
- border-radius: 8px;
- border: 1px solid #e5e7eb;
- background: #ffffff;
- cursor: pointer;
+ gap: 3px;
+ margin-bottom: 6px;
}
-.object-item-header {
- display: flex;
- align-items: center;
- gap: 4px;
- flex-wrap: nowrap;
+.sidebar-row label {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--text-3);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
}
-.object-hierarchy-select {
- width: 40px;
- min-width: 0;
- padding: 2px 3px;
- border-radius: 6px;
- border: 1px solid #d1d5db;
- font-size: 0.8rem;
- background: #f9fafb;
+.sidebar-row input[type="text"] {
+ padding: 6px 8px;
+ font-size: 12.5px;
+ border-radius: var(--r-sm);
}
-.object-parent-select {
- width: 50px;
- min-width: 0;
- padding: 2px 3px;
- border-radius: 6px;
- border: 1px solid #d1d5db;
- font-size: 0.8rem;
- background: #eef2ff;
-}
-
-.object-item img {
- width: 40px;
- height: 40px;
- object-fit: cover;
- border-radius: 6px;
- border: 1px solid #e5e7eb;
-}
-
-.object-item-text {
- display: flex;
- flex-direction: column;
- font-size: 0.8rem;
- flex: 1;
- min-width: 0;
-}
-
-.object-item-text strong {
- font-size: 0.85rem;
-}
-
-.object-item-details {
- padding-left: 24px;
- display: none;
- flex-direction: column;
- gap: 4px;
- margin-top: 4px;
-}
-
-.object-item-details.visible {
- display: flex;
-}
-
-.object-item-details input[type="text"] {
- padding: 4px 6px;
- border-radius: 6px;
- border: 1px solid #d1d5db;
- font-size: 0.8rem;
-}
-
-.object-item-details label {
- font-size: 0.75rem;
-}
-
-.object-icon-button {
- padding: 2px 6px;
- border-radius: 6px;
- font-size: 0.85rem;
- box-shadow: none;
- background: #e5e7eb;
- color: #374151;
- flex-shrink: 0;
-}
-
-.object-icon-button:not(:disabled):hover {
- background: #d1d5db;
- box-shadow: none;
- transform: none;
+.detail-value {
+ padding: 5px 8px;
+ border-radius: var(--r-sm);
+ border: 1px solid var(--border);
+ background: var(--surface-2);
+ font-size: 12.5px;
+ color: var(--text-1);
+ min-height: 26px;
+ word-break: break-word;
}
+/* =====================================================
+ SENTENCES LIST
+ ===================================================== */
.sentences-list {
display: flex;
flex-direction: column;
- gap: 12px;
- max-height: 70vh;
+ gap: 10px;
+ max-height: 100%;
overflow-y: auto;
- padding: 4px;
+ scrollbar-width: thin;
+ scrollbar-color: var(--border) transparent;
}
.sentence-item {
padding: 12px;
- background: #f9fbff;
- border: 1px solid #d4ddf5;
- border-radius: 8px;
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ border-radius: var(--r-md);
display: flex;
flex-direction: column;
- gap: 8px;
+ gap: 6px;
+}
+
+.sentence-level {
+ font-size: 10.5px;
+ font-weight: 700;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ color: var(--primary-muted-fg);
+ margin-bottom: 2px;
}
.sentence-item-question {
font-weight: 600;
- color: #1e40af;
- font-size: 0.95rem;
+ color: var(--text-1);
+ font-size: 13px;
}
.sentence-item-answer {
- color: #475569;
- font-size: 0.9rem;
- padding-left: 12px;
- border-left: 2px solid #cbd5f0;
+ color: var(--text-2);
+ font-size: 12.5px;
+ padding-left: 10px;
+ border-left: 2px solid var(--primary-muted);
+ line-height: 1.5;
}
-.sentence-item-empty {
- padding: 24px;
+.sentence-item-empty,
+.empty-state {
+ padding: 20px 12px;
text-align: center;
- color: #94a3b8;
+ color: var(--text-3);
+ font-size: 12.5px;
font-style: italic;
}
-.selections-list {
+/* =====================================================
+ STATUS MESSAGE
+ ===================================================== */
+.status-msg {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 8px;
+ padding: 7px 10px;
+ border-radius: var(--r-md);
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.status-msg.ok {
+ background: var(--success-bg);
+ color: var(--success);
+ border: 1px solid var(--success);
+}
+
+.status-msg.error {
+ background: var(--danger-bg);
+ color: var(--danger);
+ border: 1px solid var(--danger);
+}
+
+/* Legacy compat */
+.status { font-size: 12px; }
+.status.ok { color: var(--success); }
+.status.error { color: var(--danger); }
+
+/* =====================================================
+ AUTH / LOGIN PAGE
+ ===================================================== */
+.auth-page {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg);
+ padding: 24px;
+}
+
+.auth-card {
+ width: 100%;
+ max-width: 380px;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--r-xl);
+ box-shadow: var(--shadow-lg);
+ overflow: hidden;
+}
+
+.auth-card-header {
+ padding: 28px 28px 20px;
+ border-bottom: 1px solid var(--border);
+ background: var(--surface-2);
display: flex;
flex-direction: column;
- gap: 8px;
- max-height: 200px;
- overflow-y: auto;
- padding: 8px;
- background: #f9fbff;
- border: 1px solid #d4ddf5;
- border-radius: 8px;
- margin-bottom: 8px;
+ align-items: center;
+ gap: 10px;
}
-.selection-item {
- padding: 8px;
- background: #ffffff;
- border: 1px solid #e5e7eb;
- border-radius: 6px;
- font-size: 0.85rem;
- color: #475569;
+.auth-logo {
+ width: 44px;
+ height: 44px;
+ border-radius: var(--r-lg);
+ background: var(--primary);
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
-.selection-item strong {
- color: #1e40af;
- font-weight: 600;
+.auth-title {
+ font-size: 18px;
+ font-weight: 700;
+ color: var(--text-1);
+ letter-spacing: -0.02em;
}
-.selections-empty {
- padding: 16px;
+.auth-subtitle {
+ font-size: 13px;
+ color: var(--text-3);
text-align: center;
- color: #94a3b8;
- font-style: italic;
- font-size: 0.85rem;
+}
+
+.auth-card-body {
+ padding: 24px 28px 28px;
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+
+.auth-field {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+}
+
+.auth-field label {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-2);
+ letter-spacing: 0.02em;
+}
+
+.auth-error {
+ padding: 9px 12px;
+ background: var(--danger-bg);
+ border: 1px solid var(--danger);
+ border-radius: var(--r-md);
+ color: var(--danger);
+ font-size: 12.5px;
+ font-weight: 500;
+}
+
+.auth-submit {
+ background: var(--primary);
+ color: white;
+ font-size: 14px;
+ font-weight: 600;
+ padding: 10px 16px;
+ border-radius: var(--r-md);
+ border: none;
+ cursor: pointer;
+ width: 100%;
+ margin-top: 4px;
+ box-shadow: 0 2px 8px rgba(92,108,246,.35);
+ transition: all 0.15s;
+}
+
+.auth-submit:hover:not(:disabled) {
+ background: var(--primary-hover);
+ box-shadow: 0 4px 14px rgba(92,108,246,.45);
+ transform: translateY(-1px);
+}
+
+.auth-submit:disabled {
+ opacity: 0.55;
+ cursor: not-allowed;
+ transform: none;
+ box-shadow: none;
}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 2caafe8..5b81772 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -2,12 +2,15 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
+import { AuthProvider } from './context/AuthContext'
import './index.css'
createRoot(document.getElementById('root')!).render(
-
+
+
+
)
diff --git a/frontend/src/pages/DrawIt.tsx b/frontend/src/pages/DrawIt.tsx
index 85c2fc8..06642f9 100644
--- a/frontend/src/pages/DrawIt.tsx
+++ b/frontend/src/pages/DrawIt.tsx
@@ -1,28 +1,43 @@
import { useState, useEffect, useCallback, useRef } from 'react'
-import { useNavigate } from 'react-router-dom'
import DrawCanvas, { type DrawCanvasHandle } from '../components/DrawCanvas'
import ObjectsList from '../components/ObjectsList'
-import { getImages, getObjects, cropImage, saveImage } from '../api'
+import Topbar from '../components/Topbar'
+import { getObjects, cropImage, getDirectusPictures, directusAssetUrl, type DirectusPicture } from '../api'
+import { useAuth } from '../context/AuthContext'
import type { ObjectMeta, Selection } from '../types'
const FIELD_LABELS: Record = {
- title_de: 'Titel / title_de',
- position_de: 'Position / position_de',
- action_de: 'Status (sitzt/schwimmt/segelt) / action_de',
- condition_de: 'Zustand (alt/jung/rostig) / condition_de',
+ title_de: 'Titel',
+ position_de: 'Position',
+ action_de: 'Aktion',
+ condition_de: 'Zustand',
}
const FIELD_PLACEHOLDERS: Record = {
+ title_de: 'z.B. Hund',
+ position_de: 'z.B. links oben',
action_de: 'z.B. sitzt',
condition_de: 'z.B. rostig',
}
type FormKey = 'title_de' | 'position_de' | 'action_de' | 'condition_de'
-export default function DrawIt() {
- const navigate = useNavigate()
+const ChevronLeftIcon = () => (
+
+)
- const [imageList, setImageList] = useState([])
+const ChevronRightIcon = () => (
+
+)
+
+export default function DrawIt() {
+ const { token } = useAuth()
+
+ const [pictureList, setPictureList] = useState([])
const [currentIndex, setCurrentIndex] = useState(-1)
const [objects, setObjects] = useState([])
const [currentSelections, setCurrentSelections] = useState([])
@@ -39,32 +54,33 @@ export default function DrawIt() {
})
const canvasRef = useRef(null)
- const currentFilename = currentIndex >= 0 && currentIndex < imageList.length
- ? imageList[currentIndex]
+ const currentPicture = currentIndex >= 0 && currentIndex < pictureList.length
+ ? pictureList[currentIndex]
: null
useEffect(() => {
- getImages('draw')
- .then(imgs => {
- setImageList(imgs)
- setCurrentIndex(imgs.length - 1)
+ if (!token) return
+ getDirectusPictures(token)
+ .then(pics => {
+ setPictureList(pics)
+ setCurrentIndex(pics.length > 0 ? 0 : -1)
})
.catch(console.error)
- }, [])
+ }, [token])
useEffect(() => {
- if (!currentFilename) {
+ if (!currentPicture) {
setObjects([])
setSelectedObjectId(null)
return
}
- getObjects(currentFilename)
+ getObjects(currentPicture.id)
.then(objs => {
setObjects(objs.map(o => ({ ...o, visible: true })))
setSelectedObjectId(objs[0]?.id ?? null)
})
.catch(console.error)
- }, [currentFilename])
+ }, [currentPicture?.id])
const handleHasSelection = useCallback((has: boolean) => setHasSelection(has), [])
@@ -89,11 +105,11 @@ export default function DrawIt() {
}
const saveObject = async () => {
- if (!currentFilename || currentSelections.length === 0) return
+ if (!currentPicture || currentSelections.length === 0) return
try {
- showStatus('Speichere Objekt...')
+ showStatus('Speichere Objekt…')
const result = await cropImage({
- filename: currentFilename,
+ filename: currentPicture.id,
selections: currentSelections.map((sel, idx) => ({
number: idx + 1,
mode: sel.mode,
@@ -105,80 +121,75 @@ export default function DrawIt() {
showStatus(`Gespeichert – ID: ${result.id} (${currentSelections.length} Auswahl(en))`)
setCurrentSelections([])
setForm({ title_de: '', position_de: '', action_de: '', condition_de: '' })
- const objs = await getObjects(currentFilename)
+ const objs = await getObjects(currentPicture.id)
setObjects(objs.map(o => ({ ...o, visible: true })))
} catch (e) {
showStatus(e instanceof Error ? e.message : 'Fehler beim Speichern.', true)
}
}
- const handleSaveImage = async () => {
- if (!currentFilename) return
- try {
- showStatus('Bild wird gespeichert...')
- await saveImage(currentFilename)
- const imgs = await getImages('draw')
- setImageList(imgs)
- setCurrentIndex(imgs.length - 1)
- showStatus('Bild gespeichert.')
- } catch (e) {
- showStatus(e instanceof Error ? e.message : 'Fehler.', true)
- }
- }
+ const imageNav = (
+
+
+
+ {pictureList.length > 0 ? (
+ <>
+ {currentIndex + 1}
+ /
+ {pictureList.length}
+ >
+ ) : (
+ Keine Bilder
+ )}
+
+
+
+ )
return (
-
-
DrawIt
+
+
-
-
-
- Bild: {currentFilename || '–'}
-
-
-
-
-
-
-
-
-
- {/* Left: Objects */}
-
-
Objekte zu diesem Bild
-
- setObjects(prev => prev.map(o => o.id === id ? { ...o, visible } : o))
- }
- onObjectsChange={setObjects}
- isGeneratePage={false}
- />
-
+
+ {/* Left sidebar: Objects */}
+
{/* Center: Canvas */}
-
-
+
- {/* Right: Controls */}
-
-
-
Auswahl
-
- Auswahl-Typ (Interface / Backend):
-
-
-