From c161a0d0f4050494f36e7c7fb3bddb10ada0b955 Mon Sep 17 00:00:00 2001 From: itsrubberduck Date: Sun, 26 Apr 2026 09:59:34 +0200 Subject: [PATCH] clenaeres design verbesserte views --- app/pages/copilot.vue | 1486 ++++++++++++++++++++++------------------- 1 file changed, 784 insertions(+), 702 deletions(-) diff --git a/app/pages/copilot.vue b/app/pages/copilot.vue index 5894e8e..243561f 100644 --- a/app/pages/copilot.vue +++ b/app/pages/copilot.vue @@ -3,7 +3,7 @@ import {computed, defineComponent, h, nextTick, onMounted, onUnmounted, ref, wat import {VMenu} from 'vuetify/components' import {a320Profile, glossary, scratchFields, type SopPhase, type SopStep} from '~~/shared/data/a320SopTimeline' -// Inline-Komponente für Rich-Text (Glossar-Pills + Scratchpad-Placeholder) +// RichText: rendert Text mit Glossar-Pills + Scratchpad-Placeholders const RichText = defineComponent({ name: 'RichText', props: { @@ -56,24 +56,50 @@ useHead({ link: [{rel: 'manifest', href: '/copilot.webmanifest'}], }) -const STORAGE_KEY = 'opensquawk.copilot.v2' +const STORAGE_KEY = 'opensquawk.copilot.v3' const activeProfile = ref(a320Profile) const phases = computed(() => activeProfile.value.phases) -// State const scratch = ref>({}) const variantSel = ref>({}) -const completed = ref>({}) -const expanded = ref>({}) // step.id → why-Toggle +const expanded = ref>({}) const activeStepId = ref(null) const simbriefUser = ref('') const simbriefLoading = ref(false) const simbriefError = ref('') -const showScratchpadMobile = ref(false) +const showAside = ref(false) const rightTab = ref<'scratch' | 'canvas'>('scratch') -// Glossary lookup (lange Terms zuerst → "TRANS ALT" vor "ALT") +// Quick-Jot +interface QuickNote { + text: string + time: string +} + +const quickDraft = ref('') +const quickNotes = ref([]) + +function feedQuick() { + const t = quickDraft.value.trim() + if (!t) return + const time = new Date().toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'}) + quickNotes.value.push({text: t, time}) + quickDraft.value = '' + persist() + nextTick(() => { + const f = document.querySelector('.quickjot-feed') as HTMLElement | null + if (f) f.scrollTop = f.scrollHeight + }) +} + +function clearJot() { + if (!confirm('Alle Notizen löschen?')) return + quickNotes.value = [] + persist() +} + +// Glossary const glossaryByTerm = new Map(glossary.map(g => [g.term.toUpperCase(), g])) const glossaryRegex = (() => { const sorted = [...glossary].sort((a, b) => b.term.length - a.term.length) @@ -90,7 +116,6 @@ interface Segment { function tokenize(input: string): Segment[] { if (!input) return [] - // 1. Placeholder [KEY] aufsplitten const out: Segment[] = [] const placeholderRx = /\[([A-Z0-9_ +/-]+)\]/g let last = 0 @@ -185,7 +210,7 @@ async function importSimbrief() { } } -// Steps inkl. Variants flach +// Steps mit Variants flach function stepsForPhase(phase: SopPhase): SopStep[] { const out: SopStep[] = [] for (const s of phase.steps) { @@ -200,78 +225,87 @@ function stepsForPhase(phase: SopPhase): SopStep[] { } const allSteps = computed(() => { - const out: { phase: SopPhase; step: SopStep }[] = [] - for (const p of phases.value) for (const s of stepsForPhase(p)) out.push({phase: p, step: s}) + const out: { phase: SopPhase; step: SopStep; idx: number }[] = [] + let i = 0 + for (const p of phases.value) for (const s of stepsForPhase(p)) out.push({phase: p, step: s, idx: i++}) return out }) +const activeIdx = computed(() => allSteps.value.findIndex(s => s.step.id === activeStepId.value)) +const activeStep = computed(() => allSteps.value[activeIdx.value]) + const phaseProgress = computed(() => { + const ai = activeIdx.value const out: Record = {} for (const p of phases.value) { const steps = stepsForPhase(p) - const done = steps.filter(s => completed.value[s.id]).length - out[p.id] = {done, total: steps.length, pct: steps.length ? Math.round(done / steps.length * 100) : 0} + const total = steps.length + let done = 0 + for (const s of steps) { + const idx = allSteps.value.findIndex(a => a.step.id === s.id) + if (idx >= 0 && idx < ai) done++ + } + out[p.id] = {done, total, pct: total ? Math.round(done / total * 100) : 0} } return out }) const totalProgress = computed(() => { const total = allSteps.value.length - if (!total) return 0 - const done = allSteps.value.filter(({step}) => completed.value[step.id]).length - return Math.round((done / total) * 100) + if (!total || activeIdx.value < 0) return 0 + return Math.round((activeIdx.value / total) * 100) }) -const activeStep = computed(() => allSteps.value.find(s => s.step.id === activeStepId.value)) +function positionClass(idx: number): string { + const ai = activeIdx.value + if (idx === ai) return 'is-active' + const d = idx - ai + if (d < 0) return Math.abs(d) <= 1 ? 'is-past is-near' : 'is-past' + return d <= 1 ? 'is-future is-near' : 'is-future' +} -function toggleDone(id: string) { - completed.value[id] = !completed.value[id] - persist() +let scrollLock = false + +function setActive(id: string) { + activeStepId.value = id + nextTick(() => scrollToStep(id)) +} + +function nextStep() { + if (activeIdx.value < 0 || activeIdx.value >= allSteps.value.length - 1) return + activeStepId.value = allSteps.value[activeIdx.value + 1].step.id + nextTick(() => scrollToStep(activeStepId.value!)) +} + +function prevStep() { + if (activeIdx.value <= 0) return + activeStepId.value = allSteps.value[activeIdx.value - 1].step.id + nextTick(() => scrollToStep(activeStepId.value!)) +} + +function scrollToStep(id: string) { + const el = document.querySelector(`[data-step-id="${id}"]`) as HTMLElement | null + if (!el) return + scrollLock = true + el.scrollIntoView({behavior: 'smooth', block: 'center'}) + setTimeout(() => { + scrollLock = false + }, 600) } function toggleWhy(id: string) { expanded.value[id] = !expanded.value[id] } -function setActive(id: string) { - activeStepId.value = id -} - -function nextStep() { - const list = allSteps.value - const idx = list.findIndex(({step}) => step.id === activeStepId.value) - const cur = list[idx] - if (cur) completed.value[cur.step.id] = true - const next = list[idx + 1] - if (next) { - activeStepId.value = next.step.id - nextTick(() => scrollToStep(next.step.id)) - } - persist() -} - -function prevStep() { - const list = allSteps.value - const idx = list.findIndex(({step}) => step.id === activeStepId.value) - if (idx <= 0) return - activeStepId.value = list[idx - 1].step.id - nextTick(() => scrollToStep(activeStepId.value!)) -} - -function scrollToStep(id: string) { - const el = document.querySelector(`[data-step-id="${id}"]`) as HTMLElement | null - if (el) el.scrollIntoView({behavior: 'smooth', block: 'center'}) -} - function jumpToPhase(id: string) { - const el = document.querySelector(`[data-phase-id="${id}"]`) as HTMLElement | null - if (el) el.scrollIntoView({behavior: 'smooth', block: 'start'}) + const phase = phases.value.find(p => p.id === id) + if (!phase || !phase.steps[0]) return + setActive(phase.steps[0].id) } function resetAll() { if (!confirm('Alle Eingaben & Fortschritt löschen?')) return scratch.value = {} - completed.value = {} variantSel.value = {} expanded.value = {} activeStepId.value = phases.value[0]?.steps[0]?.id || null @@ -283,38 +317,43 @@ function persist() { if (typeof localStorage === 'undefined') return localStorage.setItem(STORAGE_KEY, JSON.stringify({ scratch: scratch.value, - completed: completed.value, variantSel: variantSel.value, activeStepId: activeStepId.value, + quickNotes: quickNotes.value, })) } watch([scratch, variantSel], persist, {deep: true}) +watch(quickNotes, persist, {deep: true}) -onMounted(() => { - if (typeof localStorage !== 'undefined') { - const raw = localStorage.getItem(STORAGE_KEY) - if (raw) { - try { - const v = JSON.parse(raw) - if (v.scratch) scratch.value = v.scratch - if (v.completed) completed.value = v.completed - if (v.variantSel) variantSel.value = v.variantSel - if (v.activeStepId) activeStepId.value = v.activeStepId - } catch { - } - } +// Keyboard +function onKey(e: KeyboardEvent) { + const t = e.target as HTMLElement + if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return + if (e.key === 'ArrowDown' || e.key === 'PageDown' || e.key === ' ') { + e.preventDefault() + nextStep() + } else if (e.key === 'ArrowUp' || e.key === 'PageUp') { + e.preventDefault() + prevStep() + } else if (e.key === 'Enter') { + e.preventDefault() + nextStep() + } else if (e.key === 'i' || e.key === 'I') { + if (activeStepId.value) toggleWhy(activeStepId.value) } - setupCanvas() - setupActiveObserver() - if (!activeStepId.value && allSteps.value.length) activeStepId.value = allSteps.value[0].step.id -}) +} +// Active-Step durch Scrollen erkennen let observer: IntersectionObserver | null = null -function setupActiveObserver() { +function setupObserver() { if (typeof IntersectionObserver === 'undefined') return + observer?.disconnect() + const root = document.querySelector('.timeline') as HTMLElement | null + if (!root) return observer = new IntersectionObserver((entries) => { + if (scrollLock) return let best: { id: string; ratio: number } | null = null for (const e of entries) { if (e.isIntersecting) { @@ -323,25 +362,42 @@ function setupActiveObserver() { if (!best || e.intersectionRatio > best.ratio) best = {id, ratio: e.intersectionRatio} } } - if (best && best.ratio > 0.55) activeStepId.value = best.id - }, {threshold: [0.55, 0.8]}) - nextTick(() => { - document.querySelectorAll('[data-step-id]').forEach(el => observer?.observe(el)) - }) + if (best) activeStepId.value = best.id + }, {root, rootMargin: '-40% 0px -40% 0px', threshold: [0, 0.1, 0.5, 1]}) + document.querySelectorAll('[data-step-id]').forEach(el => observer?.observe(el)) } -watch(allSteps, () => { +watch(allSteps, () => nextTick(setupObserver)) + +onMounted(() => { + if (typeof localStorage !== 'undefined') { + const raw = localStorage.getItem(STORAGE_KEY) + if (raw) { + try { + const v = JSON.parse(raw) + if (v.scratch) scratch.value = v.scratch + if (v.variantSel) variantSel.value = v.variantSel + if (v.activeStepId) activeStepId.value = v.activeStepId + if (Array.isArray(v.quickNotes)) quickNotes.value = v.quickNotes + } catch { + } + } + } + if (!activeStepId.value && allSteps.value.length) activeStepId.value = allSteps.value[0].step.id nextTick(() => { - observer?.disconnect() - document.querySelectorAll('[data-step-id]').forEach(el => observer?.observe(el)) + setupObserver() + scrollToStep(activeStepId.value!) + setupCanvas() }) + window.addEventListener('keydown', onKey) }) onUnmounted(() => { observer?.disconnect() + window.removeEventListener('keydown', onKey) }) -// Scratchpad-Felder +// Scratchpad Felder const groupedFields = computed(() => { const groups: Record = {} for (const f of scratchFields) { @@ -363,13 +419,16 @@ const groupTitles: Record = { const canvasRef = ref(null) const drawColor = ref('#fde68a') const lineWidth = ref(2.6) +let canvasInited = false function setupCanvas() { const cv = canvasRef.value - if (!cv) return + if (!cv || canvasInited) return + canvasInited = true const dpr = window.devicePixelRatio || 1 const resize = () => { const rect = cv.getBoundingClientRect() + if (rect.width <= 0 || rect.height <= 0) return cv.width = rect.width * dpr cv.height = rect.height * dpr const ctx = cv.getContext('2d')! @@ -385,15 +444,17 @@ function setupCanvas() { let last: { x: number; y: number; p: number } | null = null const pos = (e: PointerEvent) => { const r = cv.getBoundingClientRect() - return {x: e.clientX - r.left, y: e.clientY - r.top, p: e.pressure || 0.5} + return {x: e.clientX - r.left, y: e.clientY - r.top, p: e.pressure > 0 ? e.pressure : 0.5} } cv.addEventListener('pointerdown', (e) => { + e.preventDefault() cv.setPointerCapture(e.pointerId) drawing = true last = pos(e) }) cv.addEventListener('pointermove', (e) => { if (!drawing || !last) return + e.preventDefault() const ctx = cv.getContext('2d')! const cur = pos(e) ctx.strokeStyle = drawColor.value @@ -404,15 +465,20 @@ function setupCanvas() { ctx.stroke() last = cur }) - const stop = () => { + const stop = (e: PointerEvent) => { drawing = false last = null + if (cv.hasPointerCapture(e.pointerId)) cv.releasePointerCapture(e.pointerId) } cv.addEventListener('pointerup', stop) cv.addEventListener('pointercancel', stop) cv.addEventListener('pointerleave', stop) } +watch(rightTab, (v) => { + if (v === 'canvas') nextTick(setupCanvas) +}) + function clearCanvas() { const cv = canvasRef.value if (!cv) return @@ -425,32 +491,39 @@ function clearCanvas() { ctx.scale(dpr, dpr) } -const actorMeta: Record = { - pilot: {label: 'Sagen', cls: 'tag-pilot'}, - pf: {label: 'PF', cls: 'tag-pf'}, - pm: {label: 'PM', cls: 'tag-pm'}, - atc: {label: 'ATC sagt', cls: 'tag-atc'}, - cabin: {label: 'Cabin', cls: 'tag-cabin'}, - system: {label: 'Flugzeug', cls: 'tag-system'}, +// Actor-Meta +const actorIcon: Record = { + pilot: 'mdi-radio-handheld', + pf: 'mdi-airplane', + pm: 'mdi-eye-check', + atc: 'mdi-tower-fire', + cabin: 'mdi-account-group', + system: 'mdi-cog-sync-outline', +} + +const actorLabel: Record = { + pilot: 'Funkspruch · DU', + pf: 'Pilot Flying', + pm: 'Pilot Monitoring', + atc: 'ATC sagt', + cabin: 'Cabin Crew', + system: 'Flugzeug', } @@ -789,7 +784,7 @@ const actorMeta: Record = { --text: #ffffff; --t2: rgba(255, 255, 255, .80); --t3: rgba(255, 255, 255, .60); - --t4: rgba(255, 255, 255, .40); + --t4: rgba(255, 255, 255, .38); --border: rgba(255, 255, 255, .10); --border-strong: rgba(255, 255, 255, .18); --surface: rgba(255, 255, 255, .03); @@ -798,35 +793,64 @@ const actorMeta: Record = { background: var(--bg); color: var(--text); padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); background-image: radial-gradient(900px 480px at 80% -10%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 60%), radial-gradient(680px 360px at 8% -6%, color-mix(in srgb, var(--accent2) 12%, transparent), transparent 60%); } -/* Phase accents */ .accent-cyan { --pa: #22d3ee; + --pa-soft: rgba(34, 211, 238, .12); } .accent-sky { --pa: #38bdf8; + --pa-soft: rgba(56, 189, 248, .12); } .accent-indigo { --pa: #818cf8; + --pa-soft: rgba(129, 140, 248, .12); } .accent-emerald { --pa: #34d399; + --pa-soft: rgba(52, 211, 153, .12); } .accent-amber { --pa: #fbbf24; + --pa-soft: rgba(251, 191, 36, .12); } .accent-rose { --pa: #fb7185; + --pa-soft: rgba(251, 113, 133, .12); +} + +/* Actor accents */ +.actor-pilot { + --aa: #22d3ee; +} + +.actor-pf { + --aa: #34d399; +} + +.actor-pm { + --aa: #a78bfa; +} + +.actor-atc { + --aa: #fbbf24; +} + +.actor-cabin { + --aa: #fb7185; +} + +.actor-system { + --aa: #94a3b8; } /* HUD */ @@ -843,11 +867,10 @@ const actorMeta: Record = { .hud-inner { display: flex; align-items: center; - gap: 16px; + gap: 14px; padding: 10px 14px; max-width: 1600px; margin: 0 auto; - width: 100%; } .hud-left { @@ -867,27 +890,24 @@ const actorMeta: Record = { border: 1px solid color-mix(in srgb, var(--accent) 38%, transparent); background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent); - transition: all .2s ease; flex-shrink: 0; + transition: all .2s; } .hud-logo:hover { - border-color: color-mix(in srgb, var(--accent) 54%, transparent); - background: color-mix(in srgb, var(--accent) 20%, transparent); + background: color-mix(in srgb, var(--accent) 22%, transparent); } .hud-divider { width: 1px; - height: 36px; + height: 32px; background: var(--border); - border-radius: 999px; } .hud-brand { display: flex; flex-direction: column; - line-height: 1.15; - min-width: 0; + line-height: 1.1; } .brand-name { @@ -896,14 +916,8 @@ const actorMeta: Record = { } .brand-mode { - display: inline-flex; - align-items: center; - gap: 4px; font-size: 12px; color: var(--accent); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } .hud-right { @@ -911,7 +925,6 @@ const actorMeta: Record = { display: flex; align-items: center; gap: 8px; - flex-wrap: nowrap; } .simbrief-box { @@ -934,7 +947,7 @@ const actorMeta: Record = { border: none; outline: none; color: var(--text); - width: 160px; + width: 150px; font-size: 14px; } @@ -945,7 +958,6 @@ const actorMeta: Record = { .simbrief-btn { display: inline-flex; align-items: center; - gap: 4px; padding: 6px 12px; border-radius: 9px; background: var(--accent); @@ -954,11 +966,6 @@ const actorMeta: Record = { font-weight: 600; font-size: 13px; cursor: pointer; - transition: background .2s; -} - -.simbrief-btn:hover:not(:disabled) { - background: color-mix(in srgb, var(--accent) 80%, white 20%); } .simbrief-btn:disabled { @@ -990,7 +997,7 @@ const actorMeta: Record = { display: flex; gap: 6px; overflow-x: auto; - padding: 8px 14px 10px; + padding: 0 14px 10px; max-width: 1600px; margin: 0 auto; scrollbar-width: none; @@ -1012,13 +1019,13 @@ const actorMeta: Record = { font-size: 13px; cursor: pointer; white-space: nowrap; - transition: all .2s; flex-shrink: 0; + transition: all .2s; } .phase-pill:hover { - color: var(--text); background: var(--surface-2); + color: var(--text); } .phase-pill.active { @@ -1036,7 +1043,6 @@ const actorMeta: Record = { .progress-track { height: 2px; background: var(--surface); - overflow: hidden; } .progress-fill { @@ -1053,67 +1059,74 @@ const actorMeta: Record = { text-align: center; } -/* Body Layout */ +/* Body */ .body { display: grid; grid-template-columns: minmax(0, 1fr); - gap: 16px; - padding: 14px; + gap: 0; max-width: 1600px; margin: 0 auto; + height: calc(100dvh - 132px - env(safe-area-inset-top)); } @media (min-width: 1100px) { .body { grid-template-columns: minmax(0, 1fr) 440px; + gap: 16px; + padding: 16px; + height: calc(100dvh - 148px - env(safe-area-inset-top)); } } .timeline-col { + min-width: 0; display: flex; flex-direction: column; - gap: 12px; - min-width: 0; } +/* Timeline = große Scroll-Snap-Liste, nahtlose Blöcke */ .timeline { - border-radius: 20px; - border: 1px solid var(--border); - background: var(--surface); + flex: 1; overflow-y: auto; overflow-x: hidden; scroll-snap-type: y mandatory; - max-height: calc(100dvh - 240px); - min-height: 60vh; + scroll-behavior: smooth; + overscroll-behavior: contain; } .timeline::-webkit-scrollbar { - width: 6px; + width: 0; } -.timeline::-webkit-scrollbar-thumb { - background: var(--border-strong); - border-radius: 3px; +.timeline-spacer { + height: 30vh; } -.phase { +/* Phase Banner – nahtlos, kein Card-Look */ +.phase-banner { + display: flex; + align-items: stretch; + gap: 14px; + padding: 22px 18px 16px; + background: linear-gradient(180deg, var(--pa-soft), transparent); + border-top: 1px solid color-mix(in srgb, var(--pa) 24%, transparent); scroll-snap-align: start; } -.phase-header { - position: sticky; - top: 0; - z-index: 5; - display: flex; - align-items: flex-end; - justify-content: space-between; - gap: 16px; - padding: 14px 18px 12px; - backdrop-filter: blur(10px); - background: linear-gradient(180deg, - color-mix(in srgb, var(--pa) 18%, var(--bg) 82%) 0%, - color-mix(in srgb, var(--pa) 6%, var(--bg) 94%) 100%); - border-bottom: 1px solid color-mix(in srgb, var(--pa) 30%, transparent); +.phase-banner.first { + border-top: none; +} + +.phase-banner-bar { + width: 4px; + border-radius: 4px; + background: var(--pa); + box-shadow: 0 0 12px color-mix(in srgb, var(--pa) 60%, transparent); +} + +.phase-banner-body { + flex: 1; + min-width: 0; } .phase-eyebrow { @@ -1126,217 +1139,214 @@ const actorMeta: Record = { } .phase-title { - font-size: 20px; - font-weight: 600; + font-size: 22px; + font-weight: 700; line-height: 1.15; } .phase-sub { + margin-top: 2px; font-size: 13px; color: var(--t3); - margin-top: 2px; } -.phase-progress { - text-align: right; - flex-shrink: 0; +.phase-progress-mini { + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: center; + gap: 4px; + font-size: 11.5px; + color: var(--t3); + font-variant-numeric: tabular-nums; } -.phase-progress-track { - width: 100px; - height: 4px; +.phase-progress-mini-track { + width: 72px; + height: 3px; background: var(--surface-2); border-radius: 999px; overflow: hidden; - margin-bottom: 4px; } -.phase-progress-fill { +.phase-progress-mini-fill { height: 100%; background: var(--pa); transition: width .3s; } -.phase-progress-label { - font-size: 11px; - color: var(--t3); - font-variant-numeric: tabular-nums; +/* STEP BLOCK – nahtlos, ohne Card */ +.step { + position: relative; + display: grid; + grid-template-columns: 60px minmax(0, 1fr); + align-items: stretch; + gap: 0; + padding: 16px 18px; + border-top: 1px solid var(--border); + cursor: pointer; + scroll-snap-align: center; + transition: opacity .25s, padding .2s, background .25s, transform .2s; } -.steps { - list-style: none; - margin: 0; - padding: 14px; +.step:first-of-type { + border-top: none; +} + +/* Position-States */ +.step.is-past { + opacity: .42; +} + +.step.is-past.is-near { + opacity: .55; +} + +.step.is-future { + opacity: .58; +} + +.step.is-future.is-near { + opacity: .78; +} + +.step.is-active { + opacity: 1; + background: linear-gradient(180deg, + color-mix(in srgb, var(--pa) 8%, transparent), + color-mix(in srgb, var(--pa) 4%, transparent) 60%, + transparent); + padding: 24px 18px 22px; +} + +/* Linker Rail – Actor-Indicator + Linie */ +.step-rail { + position: relative; display: flex; flex-direction: column; - gap: 12px; + align-items: center; + width: 60px; } -.step-card { - scroll-snap-align: start; - scroll-margin-top: 80px; - border-radius: 16px; - border: 1px solid var(--border); - background: linear-gradient(180deg, var(--surface-2), var(--surface)); - padding: 16px 18px; - cursor: pointer; - transition: border-color .2s, transform .15s, box-shadow .2s, opacity .2s; - position: relative; -} - -.step-card:hover { - border-color: var(--border-strong); -} - -.step-card.active { - border-color: var(--pa); - box-shadow: 0 0 0 1px color-mix(in srgb, var(--pa) 50%, transparent), - 0 18px 38px -22px color-mix(in srgb, var(--pa) 80%, transparent); -} - -.step-card.done { - opacity: .6; -} - -.step-card.done .step-title { - text-decoration: line-through; - text-decoration-color: var(--t4); -} - -.step-card.done::after { +.step-rail::before { content: ''; position: absolute; - top: 14px; - right: 14px; - width: 22px; - height: 22px; - border-radius: 50%; - background: #34d399; - background-image: linear-gradient(135deg, #34d399, #10b981); - mask: url('data:image/svg+xml;utf8,') center/14px no-repeat, - linear-gradient(black, black); - -webkit-mask-composite: source-in; - mask-composite: intersect; + top: -16px; + bottom: -16px; + left: 50%; + width: 2px; + transform: translateX(-50%); + background: var(--border); } -.step-head { - display: flex; - align-items: flex-start; - gap: 10px; - flex-wrap: wrap; +.step:first-of-type .step-rail::before { + top: 0; } -.actor-tag { - display: inline-flex; - align-items: center; - padding: 4px 10px; - border-radius: 999px; - font-size: 10.5px; - font-weight: 700; - letter-spacing: .14em; - text-transform: uppercase; - border: 1px solid; - flex-shrink: 0; +.step:last-of-type .step-rail::before { + bottom: 0; } -.tag-pilot { - background: color-mix(in srgb, #22d3ee 18%, transparent); - border-color: color-mix(in srgb, #22d3ee 40%, transparent); - color: #67e8f9; -} - -.tag-pf { - background: color-mix(in srgb, #34d399 18%, transparent); - border-color: color-mix(in srgb, #34d399 40%, transparent); - color: #6ee7b7; -} - -.tag-pm { - background: color-mix(in srgb, #a78bfa 18%, transparent); - border-color: color-mix(in srgb, #a78bfa 40%, transparent); - color: #c4b5fd; -} - -.tag-atc { - background: color-mix(in srgb, #fbbf24 18%, transparent); - border-color: color-mix(in srgb, #fbbf24 40%, transparent); - color: #fcd34d; -} - -.tag-cabin { - background: color-mix(in srgb, #fb7185 18%, transparent); - border-color: color-mix(in srgb, #fb7185 40%, transparent); - color: #fda4af; -} - -.tag-system { - background: var(--surface-2); - border-color: var(--border-strong); - color: var(--t2); -} - -.step-title { - flex: 1; - font-size: 16px; - font-weight: 600; - line-height: 1.3; - min-width: 0; -} - -.why-btn { +.actor-dot { + position: relative; + z-index: 2; display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; - border-radius: 10px; - background: transparent; + border-radius: 50%; + border: 2px solid var(--bg); + background: color-mix(in srgb, var(--aa) 22%, var(--bg)); + color: var(--aa); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--aa) 40%, transparent); + transition: transform .25s, box-shadow .25s; +} + +.step.is-active .actor-dot { + background: var(--aa); + color: #001218; + transform: scale(1.18); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--aa) 28%, transparent), + 0 8px 22px color-mix(in srgb, var(--aa) 50%, transparent); +} + +.step.is-past .actor-dot { + background: color-mix(in srgb, var(--aa) 12%, var(--bg)); + color: color-mix(in srgb, var(--aa) 60%, white 0%); +} + +.step.is-past .actor-dot::after { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + background: rgba(0, 0, 0, .25); +} + +/* Step Body */ +.step-body { + min-width: 0; + padding-left: 4px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.step-meta { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.actor-label { + font-size: 10.5px; + font-weight: 700; + letter-spacing: .14em; + text-transform: uppercase; + color: var(--aa); +} + +.look-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 6px; + background: var(--surface); border: 1px solid var(--border); color: var(--t3); - cursor: pointer; - transition: all .2s; - flex-shrink: 0; + font-size: 11.5px; } -.why-btn:hover { - background: color-mix(in srgb, var(--accent) 12%, transparent); - border-color: color-mix(in srgb, var(--accent) 32%, transparent); - color: var(--accent); +.step-title { + font-size: 15.5px; + font-weight: 600; + line-height: 1.35; + color: var(--t2); + transition: font-size .2s, color .2s; } -.why-btn.open { - background: color-mix(in srgb, var(--accent) 18%, transparent); - border-color: color-mix(in srgb, var(--accent) 40%, transparent); - color: var(--accent); -} - -.why-btn.small { - width: 30px; - height: 30px; - border-radius: 8px; +.step.is-active .step-title { + font-size: 19px; + color: var(--text); } .callout { display: flex; gap: 10px; align-items: flex-start; - margin-top: 12px; - padding: 12px 14px; - border-radius: 12px; - background: linear-gradient(180deg, rgba(34, 211, 238, .12), rgba(34, 211, 238, .05)); - border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent); + padding: 10px 12px; + border-radius: 10px; + background: color-mix(in srgb, var(--accent) 10%, transparent); + border-left: 3px solid var(--accent); font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 13.5px; line-height: 1.55; color: #cffafe; } -.callout.small { - padding: 8px 10px; - font-size: 12.5px; - margin-top: 8px; -} - .callout-icon { color: var(--accent); flex-shrink: 0; @@ -1349,23 +1359,73 @@ const actorMeta: Record = { } .step-detail { - margin-top: 10px; color: var(--t2); font-size: 14px; line-height: 1.55; } -.step-detail.small { - font-size: 13px; - margin-top: 6px; +.sim-note { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 7px; + background: color-mix(in srgb, #a78bfa 12%, transparent); + border-left: 2px solid #a78bfa; + color: #ddd6fe; + font-size: 12px; + width: fit-content; } +.input-row { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.input-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 9px; + border-radius: 7px; + background: var(--surface); + border: 1px solid var(--border); + font-size: 12px; +} + +.input-chip-label { + color: var(--t3); + font-weight: 600; + font-size: 10.5px; + letter-spacing: .08em; + text-transform: uppercase; +} + +.input-chip-value { + color: var(--t4); + font-variant-numeric: tabular-nums; +} + +.input-chip.filled { + border-color: color-mix(in srgb, var(--accent) 40%, transparent); + background: color-mix(in srgb, var(--accent) 10%, transparent); +} + +.input-chip.filled .input-chip-label { + color: var(--accent); +} + +.input-chip.filled .input-chip-value { + color: #cffafe; +} + +/* Why-Block */ .why-block { - margin-top: 12px; padding: 12px 14px; - border-radius: 12px; + border-radius: 10px; background: color-mix(in srgb, #fbbf24 8%, transparent); - border: 1px solid color-mix(in srgb, #fbbf24 24%, transparent); + border-left: 3px solid #fbbf24; } .why-label { @@ -1394,91 +1454,19 @@ const actorMeta: Record = { .why-enter-from, .why-leave-to { opacity: 0; max-height: 0; - margin-top: 0; padding-top: 0; padding-bottom: 0; - border-width: 0; } .why-enter-to, .why-leave-from { opacity: 1; - max-height: 400px; + max-height: 600px; } -.meta-row { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 10px; -} - -.meta-chip { - display: inline-flex; - align-items: center; - gap: 5px; - padding: 4px 10px; - border-radius: 8px; - background: var(--surface); - border: 1px solid var(--border); - color: var(--t3); - font-size: 12px; -} - -.meta-chip.sim { - color: #c4b5fd; - border-color: color-mix(in srgb, #a78bfa 30%, transparent); - background: color-mix(in srgb, #a78bfa 10%, transparent); -} - -.input-row { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-top: 12px; -} - -.input-chip { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 10px; - border-radius: 8px; - background: var(--surface); - border: 1px solid var(--border); - font-size: 12px; -} - -.input-chip-label { - color: var(--t3); - font-weight: 600; - font-size: 10.5px; - letter-spacing: .1em; - text-transform: uppercase; -} - -.input-chip-value { - color: var(--t4); - font-variant-numeric: tabular-nums; -} - -.input-chip.filled { - border-color: color-mix(in srgb, var(--accent) 38%, transparent); - background: color-mix(in srgb, var(--accent) 10%, transparent); -} - -.input-chip.filled .input-chip-label { - color: var(--accent); -} - -.input-chip.filled .input-chip-value { - color: #cffafe; -} - -/* Variants */ +/* Variant Tabs */ .variant-tabs { display: flex; - gap: 6px; - margin-top: 14px; + gap: 4px; padding: 4px; border-radius: 12px; background: var(--surface); @@ -1489,16 +1477,16 @@ const actorMeta: Record = { .variant-tab { flex: 1; min-width: max-content; - padding: 8px 14px; + padding: 9px 14px; border-radius: 9px; - border: 1px solid transparent; + border: none; background: transparent; color: var(--t3); font-size: 13px; font-weight: 500; cursor: pointer; - transition: all .2s; white-space: nowrap; + transition: all .2s; } .variant-tab:hover { @@ -1507,156 +1495,98 @@ const actorMeta: Record = { } .variant-tab.active { - background: color-mix(in srgb, var(--accent) 18%, transparent); - border-color: color-mix(in srgb, var(--accent) 38%, transparent); - color: var(--accent); + background: color-mix(in srgb, var(--pa) 18%, transparent); + color: var(--pa); } -.variant-steps { +/* Inline Actions */ +.inline-actions { display: flex; - flex-direction: column; gap: 8px; - margin-top: 12px; + margin-top: 6px; + align-items: stretch; } -.vstep { - padding: 12px 14px; - border-radius: 12px; - border: 1px solid var(--border); - background: rgba(0, 0, 0, .25); - cursor: pointer; - transition: border-color .2s; -} - -.vstep:hover { - border-color: var(--border-strong); -} - -.vstep.active { - border-color: var(--pa); - box-shadow: 0 0 0 1px color-mix(in srgb, var(--pa) 40%, transparent); -} - -.vstep.done { - opacity: .55; -} - -.vstep-head { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 8px; -} - -.vstep-head h4 { - font-size: 14px; - font-weight: 600; - line-height: 1.3; - flex: 1; -} - -/* Action Bar */ -.action-bar { - position: sticky; - bottom: 12px; - display: flex; - align-items: center; - gap: 10px; - padding: 10px 12px; - border-radius: 16px; - border: 1px solid var(--border); - background: color-mix(in srgb, var(--bg) 92%, transparent); - backdrop-filter: blur(14px); - box-shadow: 0 24px 48px -16px rgba(0, 0, 0, .6); -} - -.action-prev, .action-done { +.act { display: inline-flex; align-items: center; justify-content: center; - width: 48px; + gap: 6px; height: 48px; + padding: 0 16px; border-radius: 14px; border: 1px solid var(--border); background: var(--surface); color: var(--t2); + font-weight: 600; + font-size: 14px; cursor: pointer; - transition: all .2s; - flex-shrink: 0; + transition: all .15s; } -.action-prev:hover, .action-done:hover { +.act:hover:not(:disabled) { background: var(--surface-2); color: var(--text); } -.action-done.checked { - background: color-mix(in srgb, #34d399 22%, transparent); - border-color: color-mix(in srgb, #34d399 50%, transparent); - color: #6ee7b7; -} - -.action-prev:disabled, .action-done:disabled, .action-next:disabled { - opacity: .4; +.act:disabled { + opacity: .35; cursor: not-allowed; } -.action-center { - flex: 1; - min-width: 0; - text-align: center; - padding: 0 8px; +.act-back { + flex: 0 0 auto; } -.action-eyebrow { - font-size: 10.5px; - letter-spacing: .18em; - text-transform: uppercase; - color: var(--t3); - margin-bottom: 1px; +.act-why { + flex: 0 0 auto; } -.action-step { - font-size: 14px; - font-weight: 600; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; +.act-why.open { + background: color-mix(in srgb, #fbbf24 18%, transparent); + border-color: color-mix(in srgb, #fbbf24 40%, transparent); + color: #fcd34d; } -.action-next { - display: inline-flex; - align-items: center; - gap: 8px; - height: 48px; - padding: 0 18px; - border-radius: 14px; - border: none; - background: linear-gradient(135deg, var(--accent), var(--accent2)); +.act-next { + flex: 1 1 auto; + background: linear-gradient(135deg, var(--pa), color-mix(in srgb, var(--pa) 70%, var(--accent2) 30%)); + border-color: transparent; color: #001218; - font-weight: 600; - font-size: 14px; - cursor: pointer; - transition: transform .15s, box-shadow .2s; - box-shadow: 0 12px 24px -8px color-mix(in srgb, var(--accent) 50%, transparent); - flex-shrink: 0; + box-shadow: 0 12px 24px -10px color-mix(in srgb, var(--pa) 60%, transparent); } -.action-next:hover:not(:disabled) { +.act-next:hover:not(:disabled) { transform: translateY(-1px); - box-shadow: 0 18px 32px -8px color-mix(in srgb, var(--accent) 60%, transparent); + box-shadow: 0 18px 30px -10px color-mix(in srgb, var(--pa) 70%, transparent); } -@media (max-width: 640px) { - .action-next span { +@media (max-width: 480px) { + .act-back span, .act-why span { display: none; } - .action-next { - padding: 0 14px; + .act-back, .act-why { + padding: 0 12px; } } +.actions-enter-active, .actions-leave-active { + transition: opacity .2s, max-height .25s, transform .2s; + overflow: hidden; +} + +.actions-enter-from, .actions-leave-to { + opacity: 0; + max-height: 0; + transform: translateY(-4px); +} + +.actions-enter-to, .actions-leave-from { + opacity: 1; + max-height: 80px; +} + /* Aside */ .aside { display: flex; @@ -1665,6 +1595,12 @@ const actorMeta: Record = { min-width: 0; } +@media (min-width: 1100px) { + .aside { + max-height: 100%; + } +} + .aside-tabs { display: flex; gap: 4px; @@ -1688,11 +1624,6 @@ const actorMeta: Record = { font-size: 13px; font-weight: 500; cursor: pointer; - transition: all .2s; -} - -.aside-tab:hover { - color: var(--text); } .aside-tab.active { @@ -1709,7 +1640,8 @@ const actorMeta: Record = { border: 1px solid var(--border); background: var(--surface); overflow-y: auto; - max-height: calc(100dvh - 280px); + flex: 1; + min-height: 0; } .field-group-title { @@ -1751,7 +1683,6 @@ const actorMeta: Record = { color: var(--text); font-size: 14px; font-variant-numeric: tabular-nums; - transition: border-color .2s, background .2s; } .field-input::placeholder { @@ -1761,7 +1692,6 @@ const actorMeta: Record = { .field-input:focus { outline: none; border-color: color-mix(in srgb, var(--accent) 56%, transparent); - background: rgba(0, 0, 0, .35); } .canvas-wrap { @@ -1769,13 +1699,17 @@ const actorMeta: Record = { border-radius: 18px; border: 1px solid var(--border); background: var(--surface); + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; + min-height: 0; } .canvas-toolbar { display: flex; align-items: center; gap: 10px; - margin-bottom: 10px; flex-wrap: wrap; } @@ -1802,6 +1736,7 @@ const actorMeta: Record = { .line-slider { flex: 1; accent-color: var(--accent); + min-width: 80px; } .canvas-clear { @@ -1817,14 +1752,11 @@ const actorMeta: Record = { cursor: pointer; } -.canvas-clear:hover { - color: var(--text); -} - .canvas { display: block; width: 100%; - height: 60vh; + flex: 1; + min-height: 280px; border-radius: 14px; background: rgba(0, 0, 0, .35); border: 1px solid var(--border); @@ -1833,7 +1765,6 @@ const actorMeta: Record = { } .canvas-hint { - margin-top: 6px; font-size: 11.5px; color: var(--t4); } @@ -1843,10 +1774,11 @@ const actorMeta: Record = { .aside { position: fixed; inset: 0; - z-index: 60; + z-index: 70; background: var(--bg); padding: 16px; padding-top: calc(env(safe-area-inset-top) + 16px); + padding-bottom: calc(env(safe-area-inset-bottom) + 80px); transform: translateY(100%); transition: transform .25s ease; overflow-y: auto; @@ -1856,12 +1788,162 @@ const actorMeta: Record = { transform: translateY(0); } - .scratchpad { + .scratchpad, .canvas-wrap { max-height: none; } } -/* RichText – Glossar & Placeholder */ +/* QUICK-JOT (immer sichtbar unten auf Mobile) */ +.quickjot { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 50; + border-top: 1px solid var(--border); + background: color-mix(in srgb, var(--bg) 92%, transparent); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + padding-bottom: env(safe-area-inset-bottom); + display: flex; + flex-direction: column; + max-height: 50vh; +} + +@media (min-width: 1100px) { + .quickjot { + display: none; + } + + /* Auf Desktop fügen wir den Notes-Bereich später optional hinzu */ +} + +.quickjot-feed { + overflow-y: auto; + padding: 8px 14px; + display: flex; + flex-direction: column; + gap: 4px; + max-height: 36vh; +} + +.quickjot-feed::-webkit-scrollbar { + width: 4px; +} + +.quickjot-feed::-webkit-scrollbar-thumb { + background: var(--border-strong); + border-radius: 2px; +} + +.quickjot-note { + display: flex; + gap: 8px; + padding: 6px 10px; + border-radius: 8px; + background: var(--surface); + border-left: 2px solid var(--accent); + font-size: 13.5px; + color: var(--t2); + line-height: 1.4; + word-break: break-word; + animation: jot-in .25s ease; +} + +@keyframes jot-in { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.quickjot-time { + flex-shrink: 0; + font-size: 11px; + color: var(--t4); + font-variant-numeric: tabular-nums; + padding-top: 1px; +} + +.quickjot-text { + flex: 1; + min-width: 0; +} + +.quickjot-input { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border-top: 1px solid var(--border); + background: var(--bg); +} + +.quickjot-field { + flex: 1; + height: 44px; + padding: 0 14px; + border-radius: 12px; + border: 1px solid var(--border); + background: var(--surface); + color: var(--text); + font-size: 14px; +} + +.quickjot-field::placeholder { + color: var(--t4); +} + +.quickjot-field:focus { + outline: none; + border-color: color-mix(in srgb, var(--accent) 56%, transparent); + background: color-mix(in srgb, var(--accent) 6%, var(--surface)); +} + +.quickjot-clear { + width: 38px; + height: 44px; + border-radius: 11px; + border: 1px solid var(--border); + background: var(--surface); + color: var(--t3); + cursor: pointer; +} + +.quickjot-feed-btn { + width: 48px; + height: 44px; + border-radius: 12px; + border: none; + background: var(--accent); + color: #001218; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.quickjot-feed-btn:disabled { + opacity: .35; + cursor: not-allowed; +} + +/* Auf Mobile: Platz schaffen damit Quick-Jot nicht den letzten Step verdeckt */ +@media (max-width: 1099px) { + .body { + padding-bottom: 0; + } + + .timeline-spacer:last-of-type { + height: calc(40vh + 60px); + } +} + +/* RichText */ :deep(.rt-term) { display: inline-block; padding: 0 5px;