@rslsp1/fa-app-tools 6.1.0 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-GHKN2VX3.mjs → chunk-5QA6DTJ6.mjs} +428 -15
- package/dist/{hfEventTypes-Dkfrcd_t.d.mts → hfEventTypes-BUztJySs.d.mts} +1 -11
- package/dist/{hfEventTypes-C_PX6RFR.d.ts → hfEventTypes-fAdA-0A9.d.ts} +1 -11
- package/dist/index.d.mts +18 -49
- package/dist/index.d.ts +18 -49
- package/dist/index.js +728 -1522
- package/dist/index.mjs +230 -723
- package/dist/szenen.d.mts +331 -221
- package/dist/szenen.d.ts +331 -221
- package/dist/szenen.js +121 -4
- package/dist/szenen.mjs +21 -4
- package/package.json +1 -1
- package/dist/chunk-NBUS7AWR.mjs +0 -418
- package/dist/chunk-UFXDXENC.mjs +0 -218
- package/dist/hfStateService-W7KO2FOH.mjs +0 -56
- package/dist/project-O4ORKXY5.mjs +0 -8
package/dist/index.mjs
CHANGED
|
@@ -2,37 +2,26 @@ import {
|
|
|
2
2
|
SzenenView,
|
|
3
3
|
alsFlowTextZugang,
|
|
4
4
|
applyEvents,
|
|
5
|
-
createFlowSzenenViewPort
|
|
6
|
-
} from "./chunk-GHKN2VX3.mjs";
|
|
7
|
-
import "./chunk-FUY2OCVC.mjs";
|
|
8
|
-
import {
|
|
9
|
-
exportProjectToZip,
|
|
10
|
-
importProjectFromZip,
|
|
11
|
-
injectXMPMetadata
|
|
12
|
-
} from "./chunk-UFXDXENC.mjs";
|
|
13
|
-
import {
|
|
5
|
+
createFlowSzenenViewPort,
|
|
14
6
|
getHFToken,
|
|
15
7
|
getSessionClientId,
|
|
16
8
|
hfBatchArchive,
|
|
17
9
|
hfBootstrapFromLegacy,
|
|
18
|
-
hfDeleteProject,
|
|
19
10
|
hfDownloadBinaryByPath,
|
|
20
|
-
hfDownloadProject,
|
|
21
11
|
hfListDir,
|
|
22
|
-
hfListProjects,
|
|
23
12
|
hfLoadImageAsBase64,
|
|
24
13
|
hfUploadImage,
|
|
25
|
-
hfUploadProjectForm,
|
|
26
14
|
hfUploadSmallFile,
|
|
27
15
|
loadHFState,
|
|
28
16
|
loadPendingEvents,
|
|
29
17
|
setHFToken,
|
|
30
18
|
tsFromEventPath,
|
|
31
19
|
writeHFEvent
|
|
32
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-5QA6DTJ6.mjs";
|
|
21
|
+
import "./chunk-FUY2OCVC.mjs";
|
|
33
22
|
|
|
34
23
|
// package.json
|
|
35
|
-
var version = "
|
|
24
|
+
var version = "7.2.0";
|
|
36
25
|
|
|
37
26
|
// src/hooks/useOnClickOutside.ts
|
|
38
27
|
import { useEffect } from "react";
|
|
@@ -71,7 +60,7 @@ function useKeyboardNavigation(history, currentResult, setCurrentResult) {
|
|
|
71
60
|
}
|
|
72
61
|
|
|
73
62
|
// src/features/szenen/szenenViewStyles.generated.ts
|
|
74
|
-
var SZENEN_VIEW_STYLES = '/* ==========================================================================\n SZENEN VIEW\n ========================================================================== */\n\n.szenen-view {\n display: grid;\n gap: var(--space-md);\n width: 100%;\n min-width: 0;\n font-family: var(--font-sans);\n padding: var(--space-md);\n}\n\n@media (max-width: 760px) {\n .szenen-view {\n padding: var(--space-sm);\n gap: var(--space-sm);\n }\n}\n\n.szenen-view__intro {\n display: flex;\n align-items: end;\n justify-content: space-between;\n gap: var(--space-md);\n padding: clamp(1rem, 3vw, 1.75rem);\n color: var(--color-text-primary);\n background:\n radial-gradient(circle at 85% 15%, rgba(45, 212, 191, 0.16), transparent 34%),\n linear-gradient(135deg, var(--color-bg-surface), var(--color-bg-elevated));\n border: 1px solid var(--color-border);\n border-radius: var(--radius-lg);\n}\n\n.szenen-view__intro h2,\n.szenen-view__intro p {\n margin: 0;\n}\n\n.szenen-view__intro h2 {\n margin-bottom: var(--space-xs);\n font-size: clamp(1.25rem, 4vw, 2rem);\n}\n\n.szenen-view__intro > div > p:last-child {\n max-width: 44rem;\n color: var(--color-text-secondary);\n}\n\n.szenen-view__eyebrow {\n margin-bottom: var(--space-xs) !important;\n color: var(--color-accent-teal, #2dd4bf);\n font-size: 0.72rem;\n font-weight: 800;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n}\n\n.szenen-view__step {\n flex: none;\n padding: 0.35rem 0.65rem;\n color: var(--color-text-primary);\n background: rgba(45, 212, 191, 0.12);\n border: 1px solid rgba(45, 212, 191, 0.35);\n border-radius: var(--radius-full);\n font-size: 0.78rem;\n font-weight: 700;\n}\n\n.szenen-view__nav {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-view__nav button,\n.szenen-view button {\n min-height: 2.25rem;\n padding: 0.45rem 0.75rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-view button:hover:not(:disabled) {\n color: var(--color-text-primary);\n background: var(--color-bg-surface-hover);\n border-color: var(--color-border-hover);\n}\n\n.szenen-view button:focus-visible,\n.szenen-view input:focus-visible,\n.szenen-view select:focus-visible,\n.szenen-view textarea:focus-visible {\n outline: 2px solid var(--color-accent-blue);\n outline-offset: 2px;\n}\n\n.szenen-view button:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.szenen-view__nav button.is-active,\n.szenen-view__nav button[aria-selected="true"] {\n color: var(--color-text-primary);\n background: var(--color-accent-blue-dim);\n border-color: var(--color-accent-blue);\n}\n\n.szenen-view__area {\n min-width: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-view__capability-note,\n.szenen-area__lead {\n margin: 0;\n padding: var(--space-sm) var(--space-md);\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border-left: 3px solid var(--color-accent-purple);\n border-radius: var(--radius-sm);\n}\n\n.szenen-view__host-notes {\n color: var(--color-text-secondary);\n}\n\n.szenen-view__host-notes > summary {\n cursor: pointer;\n font-size: 0.8rem;\n}\n\n/* `box-sizing` steht hier und nicht beim Host: Die React-App bringt in\n * `master.css` einen globalen `* { box-sizing: border-box }` mit, Flow **nicht**\n * \u2014 dort ragte `width: 100%` plus Innenabstand um die Randbreite aus dem\n * Elternelement heraus und erzeugte auf ~390 px einen horizontalen Scroll. Die\n * gemeinsame Ansicht darf sich auf keinen Host-Reset verlassen. */\n.szenen-view input:not([type="checkbox"]),\n.szenen-view select,\n.szenen-view textarea {\n box-sizing: border-box;\n width: 100%;\n min-height: 2.25rem;\n padding: 0.5rem 0.65rem;\n color: var(--color-text-primary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n font: inherit;\n}\n\n.szenen-view textarea {\n min-height: 7rem;\n resize: vertical;\n font-family: var(--font-mono);\n font-size: 0.8rem;\n}\n\n.szenen-view label {\n display: grid;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-size: 0.85rem;\n}\n\n.szenen-view code,\n.szenen-view pre {\n font-family: var(--font-mono);\n overflow-wrap: anywhere;\n}\n\n.szenen-view h3,\n.szenen-view h4,\n.szenen-view h5 {\n margin-top: 0;\n}\n\n.szenen-view ul,\n.szenen-view ol {\n padding-left: 1.25rem;\n}\n\n.szenen-elements,\n.szenen-detail,\n.szenen-scene,\n.szenen-setup,\n.szenen-shot,\n.szenen-media,\n.szenen-runs {\n display: grid;\n gap: var(--space-md);\n min-width: 0;\n}\n\n/* Arbeitsoberflaeche: Liste links, Bearbeitung rechts.\n *\n * Breit stehen beide Spalten nebeneinander \u2014 die Auswahl bleibt sichtbar,\n * waehrend bearbeitet wird. Schmal rendert die Komponente nur eine der beiden;\n * hier steht deshalb keine Regel, die die andere versteckt. */\n@media (min-width: 761px) {\n .szenen-elements {\n grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);\n align-items: start;\n }\n\n .szenen-elements__master,\n .szenen-elements__detail {\n min-width: 0;\n }\n}\n\n.szenen-elements__master,\n.szenen-elements__detail {\n display: grid;\n gap: var(--space-sm);\n align-content: start;\n min-width: 0;\n}\n\n.szenen-elements__toolbar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-sm);\n}\n\n.szenen-elements__heading {\n margin: 0;\n font-size: 1.05rem;\n}\n\n.szenen-elements__scope {\n display: flex;\n align-items: center;\n gap: var(--space-xs);\n min-width: 0;\n color: var(--color-text-secondary);\n font-size: 0.8rem;\n}\n\n.szenen-elements__scope > select {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.szenen-elements__new {\n flex: none;\n font-weight: 700;\n}\n\n.szenen-elements__create {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-accent-blue);\n border-radius: var(--radius-sm);\n}\n\n.szenen-elements__create label {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-elements__create-actions {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n}\n\n.szenen-elements__options > summary {\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n cursor: pointer;\n}\n\n.szenen-elements__back {\n justify-self: start;\n font-weight: 700;\n}\n\n.szenen-elements__empty-detail {\n margin: 0;\n color: var(--color-text-tertiary);\n}\n\n.szenen-elements__filter,\n.szenen-detail__actions,\n.szenen-runs__start,\n.szenen-scene-editor__add,\n.szenen-scene-editor__add-relation {\n display: flex;\n flex-wrap: wrap;\n align-items: end;\n gap: var(--space-sm);\n}\n\n.szenen-elements__filter > label,\n.szenen-runs__start > label,\n.szenen-scene-editor__add > input {\n flex: 1 1 16rem;\n}\n\n.szenen-elements__filter > label:has(input[type="checkbox"]) {\n display: flex;\n flex: 1 1 16rem;\n flex-direction: row;\n align-items: center;\n}\n\n.szenen-elements__filter input[type="checkbox"] {\n width: 1rem;\n height: 1rem;\n accent-color: var(--color-accent-blue);\n}\n\n.szenen-elements__list {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-elements__list > li > button {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs) var(--space-sm);\n width: 100%;\n height: 100%;\n text-align: left;\n}\n\n.szenen-elements__list > li > button > .szenen-elements__title {\n flex: 1 1 100%;\n}\n\n.szenen-elements__list > li > button.is-selected,\n.szenen-elements__list > li > button[aria-current="true"] {\n background: var(--color-accent-blue-dim);\n border-color: var(--color-accent-blue);\n}\n\n.szenen-elements__type {\n color: var(--color-accent-blue);\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n}\n\n.szenen-elements__title {\n color: var(--color-text-primary);\n font-size: 0.95rem;\n}\n\n.szenen-elements__subtype,\n.szenen-elements__retired {\n color: var(--color-text-secondary);\n font-size: 0.75rem;\n}\n\n.szenen-elements__retired {\n color: var(--color-warning);\n}\n\n.szenen-detail {\n margin-top: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n@media (min-width: 761px) {\n .szenen-detail {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n }\n}\n\n.szenen-detail > header,\n.szenen-detail__heads,\n.szenen-detail__draft,\n.szenen-capability {\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n/* .szenen-runs ist ein eigenst\xE4ndiger sichtbarer Bereich, beh\xE4lt Styling auf Desktop */\n@media (min-width: 761px) {\n .szenen-runs {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n }\n}\n\n@media (max-width: 760px) {\n .szenen-runs {\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n }\n}\n\n.szenen-detail__ids {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin-bottom: 0;\n color: var(--color-text-secondary);\n}\n\n.szenen-detail__heads ul,\n.szenen-scene-editor ul,\n.szenen-runs > ol,\n.szenen-capability > ul {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-detail__heads li,\n.szenen-scene-editor li,\n.szenen-runs > ol > li,\n.szenen-capability > ul > li {\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n\n/* Stufe 6.7.2 (D-126) \u2014 Advanced-/Debug-Fl\xE4che f\xFCr die rohe, schema-getriebene\n * Feldliste einer Szene. Gestrichelt wie die anderen \u201EAngebot statt\n * Auskunft"-Bereiche, aber ohne Akzentfarbe: sie ist kein Vorschlag, sondern\n * ein Eskalationsweg. */\n.szenen-detail__technical {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__technical > summary {\n margin-bottom: var(--space-sm);\n color: var(--color-text-secondary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-detail__technical[open] > summary {\n margin-bottom: var(--space-md);\n}\n\n.szenen-detail__field small,\n.szenen-capability small {\n color: var(--color-text-tertiary);\n}\n\n.szenen-detail__preview-errors,\n.szenen-detail__notice--error,\n.szenen-view [role="alert"] {\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-color: var(--color-accent-red);\n}\n\n.szenen-detail__notice {\n padding: var(--space-sm) var(--space-md);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__notice--success,\n.szenen-view [data-tone="success"] {\n color: #bbf7d0;\n background: var(--color-accent-green-dim);\n border-color: var(--color-accent-green);\n}\n\n.szenen-scene-editor {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: var(--space-md);\n margin-bottom: 0;\n}\n\n.szenen-scene-editor > section {\n min-width: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n/* \u2500\u2500 Stufe 6.7.2 (D-126): Node-/Relationskarten \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Nodes und Beziehungen sind `<details>` \u2014 dieselbe Interaktionssprache wie\n * \u201ERohantwort (Diagnose)" und \u201EBilder zu diesem Lauf" in RunsPanel.tsx. Kopf\n * (immer sichtbar) tr\xE4gt Inhalt zuerst, Technik zuletzt; Body (nur bei\n * ge\xF6ffneter Karte) tr\xE4gt die Bearbeitungscontrols. Padding wandert von der\n * `<li>` in Kopf/Body, damit die Karte keinen doppelten Rand bekommt. */\n.szenen-scene-editor li {\n padding: 0;\n overflow: hidden;\n}\n\n.szenen-node-karte__kopf,\n.szenen-relation-karte__kopf {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-xs) var(--space-sm);\n padding: var(--space-sm);\n cursor: pointer;\n list-style: none;\n}\n\n.szenen-node-karte__kopf::-webkit-details-marker,\n.szenen-relation-karte__kopf::-webkit-details-marker {\n display: none;\n}\n\n.szenen-node-karte__titel {\n flex: 1 1 12rem;\n min-width: 0;\n color: var(--color-text-primary);\n font-weight: 600;\n overflow-wrap: anywhere;\n}\n\n.szenen-node-karte__typ {\n flex: none;\n padding: 0.15rem 0.55rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-full);\n font-size: 0.72rem;\n white-space: nowrap;\n}\n\n/* node_id klein, monospace, sekund\xE4r \u2014 sichtbar auch bei zugeklappter Karte,\n * aber optisch klar hinter Beschreibung und Typ. */\n.szenen-node-karte__meta {\n flex: 1 1 100%;\n display: flex;\n flex-wrap: wrap;\n gap: 0.25rem;\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n}\n\n.szenen-node-karte__meta code {\n font-size: inherit;\n}\n\n.szenen-node-karte__body,\n.szenen-relation-karte__body {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm) var(--space-sm) var(--space-sm);\n border-top: 1px solid var(--color-border);\n}\n\n/* \xA74 \u2014 Prim\xE4raktion und destruktive Aktion optisch getrennt: \u201ENode/Beziehung\n * entfernen" bekommt einen eigenen, warnenden Ton statt gleichgewichtig neben\n * der normalen Bearbeitung zu stehen. */\n.szenen-node-karte__entfernen,\n.szenen-relation-karte__entfernen {\n justify-self: start;\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-color: var(--color-accent-red);\n}\n\n.szenen-relation-karte__kopf {\n font-size: 0.9rem;\n}\n\n.szenen-relation-karte__kopf code {\n overflow-wrap: anywhere;\n}\n\n.szenen-relation-karte__pfeil {\n flex: none;\n color: var(--color-text-secondary);\n}\n\n/* Die Composer-\xD6ffner (\u201E+ Node hinzuf\xFCgen", \u201E+ Beziehung hinzuf\xFCgen") sind\n * Angebote, keine Ausk\xFCnfte \u2014 dasselbe gestrichelte Muster wie beim Folgelauf\n * und der Fehlerrettung (\xA73.3/\xA73.5). */\n.szenen-scene-editor__oeffnen {\n width: 100%;\n border-style: dashed;\n}\n\n.szenen-refpicker {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n margin: var(--space-sm) 0;\n padding: var(--space-sm) 0;\n border: none;\n border-radius: 0;\n}\n\n.szenen-refpicker legend {\n padding: 0 var(--space-xs);\n color: var(--color-text-secondary);\n font-weight: 600;\n}\n\n.szenen-capability > ul > li {\n display: grid;\n grid-template-columns: minmax(10rem, 1fr) auto minmax(14rem, 2fr);\n align-items: start;\n gap: var(--space-sm);\n}\n\n.szenen-capability__state {\n padding: 0.1rem 0.5rem;\n border-radius: var(--radius-full);\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n}\n\n.szenen-capability__state--yes {\n color: #bbf7d0;\n background: var(--color-accent-green-dim);\n}\n\n.szenen-capability__state--no {\n color: #fecaca;\n background: var(--color-accent-red-dim);\n}\n\n.szenen-capability__state--unknown {\n color: #fef08a;\n background: rgba(234, 179, 8, 0.2);\n}\n\n.szenen-runs > ol > li {\n display: grid;\n gap: var(--space-sm);\n}\n\n/* B1 \u2014 die geschlossene Runde. Vorher stand die `run_id` als erstes und war\n die einzige Information, die niemand liest; jetzt beantwortet die Kopfzeile\n welche Runde, ob sie durchging, mit welchem Modell und was ungef\xE4hr\n herauskam. */\n.szenen-runs__kopf {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-sm);\n width: 100%;\n padding: var(--space-xs) 0;\n text-align: left;\n color: var(--color-text-secondary);\n background: transparent;\n border: none;\n cursor: pointer;\n}\n\n.szenen-runs__marke {\n font-weight: 700;\n color: var(--color-text-primary);\n}\n\n.szenen-runs__status {\n padding: 0.05rem 0.4rem;\n font-size: 0.6875rem;\n font-weight: 600;\n text-transform: uppercase;\n border-radius: var(--radius-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n}\n\n.szenen-runs__status[data-status="succeeded"] {\n border-color: var(--color-success);\n}\n\n.szenen-runs__status[data-status="failed"] {\n border-color: var(--color-danger);\n}\n\n.szenen-runs__modell {\n font-size: 0.75rem;\n}\n\n/* Der Auszug ist die Inhaltsvorschau der Runde. Er bekommt eine eigene Zeile,\n damit er nicht mit Marke und Modell um dieselbe Zeile ringt. */\n.szenen-runs__auszug {\n flex: 1 1 100%;\n min-width: 0;\n color: var(--color-text-tertiary);\n font-size: 0.8125rem;\n}\n\n/* B2 \u2014 ein Fehler steht ausserhalb der aufklappbaren Fl\xE4che: Wer eine rote\n Runde sieht, soll nicht erst klicken m\xFCssen, um zu erfahren, woran sie\n gescheitert ist. */\n.szenen-runs__fehler {\n display: grid;\n gap: var(--space-xs);\n padding: var(--space-sm);\n border-radius: var(--radius-sm);\n}\n\n.szenen-runs__fehler p {\n margin: 0;\n}\n\n.szenen-runs__inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n}\n\n.szenen-runs__prompt > h5,\n.szenen-runs__sekundaer > h5 {\n margin: 0 0 var(--space-xs);\n}\n\n.szenen-runs__prompt > p {\n margin: 0;\n white-space: pre-wrap;\n}\n\n.szenen-runs__prompt--negativ > p,\n.szenen-runs__sekundaer > p {\n margin: 0;\n color: var(--color-text-secondary);\n white-space: pre-wrap;\n}\n\n.szenen-runs__mehr {\n justify-self: start;\n margin-top: var(--space-xs);\n padding: 0;\n color: var(--color-accent-teal, var(--color-text-secondary));\n background: transparent;\n border: none;\n font-size: 0.75rem;\n text-decoration: underline;\n cursor: pointer;\n}\n\n.szenen-runs > ol > li[data-status="succeeded"] {\n border-left: 3px solid var(--color-success);\n}\n\n.szenen-runs > ol > li[data-status="failed"] {\n border-left: 3px solid var(--color-danger);\n}\n\n.szenen-runs details pre {\n max-height: 20rem;\n overflow: auto;\n padding: var(--space-sm);\n white-space: pre-wrap;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border-radius: var(--radius-sm);\n}\n\n.szenen-media > section {\n display: grid;\n gap: var(--space-sm);\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-media__lookup,\n.szenen-media__attach {\n display: flex;\n flex-wrap: wrap;\n align-items: end;\n gap: var(--space-sm);\n}\n\n.szenen-media__lookup > label,\n.szenen-media__attach > label:first-child {\n flex: 1 1 18rem;\n}\n\n.szenen-media__list {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-media__list > li {\n display: grid;\n grid-template-columns: minmax(12rem, 1fr) auto auto auto;\n align-items: center;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n@media (max-width: 760px) {\n .szenen-view {\n gap: var(--space-sm);\n }\n\n /* C1 \u2014 auf 390 px bleiben drei Kacheln je Reihe gross genug zum Erkennen\n und klein genug, dass vier Bilder eine Reihe sind und keine Seite. */\n .szenen-bildgalerie {\n grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));\n }\n\n /* Der Viewer bekommt den ganzen Bildschirm; die Bl\xE4tterkn\xF6pfe bleiben am\n Rand, wo der Daumen sie erreicht. */\n .szenen-bildviewer {\n padding: var(--space-xs);\n }\n\n .szenen-bildviewer__blaettern {\n width: 2.25rem;\n height: 4rem;\n }\n\n .szenen-view__intro {\n align-items: start;\n flex-direction: column;\n gap: var(--space-sm);\n padding: var(--space-md);\n }\n\n .szenen-view__intro h2 {\n font-size: 1.25rem;\n }\n\n .szenen-view__intro > div > p:last-child {\n font-size: 0.9rem;\n }\n\n .szenen-view__nav {\n position: sticky;\n top: 0;\n z-index: 2;\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: var(--space-xs);\n padding: var(--space-xs);\n }\n\n .szenen-view__nav button {\n width: 100%;\n min-width: 0;\n min-height: 2.5rem;\n padding: 0.35rem 0.5rem;\n }\n\n\n .szenen-scene-editor {\n grid-template-columns: 1fr;\n }\n\n .szenen-capability > ul > li {\n grid-template-columns: 1fr;\n }\n\n .szenen-derive,\n .szenen-elements,\n .szenen-scene,\n .szenen-setup,\n .szenen-shot,\n .szenen-media,\n .szenen-runs {\n gap: var(--space-sm);\n }\n\n .szenen-elements__filter {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n align-items: center;\n gap: var(--space-sm);\n }\n\n .szenen-elements__filter > label {\n width: 100%;\n min-width: 0;\n flex: none;\n }\n\n .szenen-elements__filter > label:has(input[type="checkbox"]) {\n flex: none;\n grid-column: 1 / -1;\n }\n\n /* Schmal ist die Liste eine Spalte, und die Auswahl fuehrt weiter \u2014 der\n * Eintrag ist deshalb ein Ziel fuer den Finger, keine Kachel. */\n .szenen-elements__list {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .szenen-elements__list > li > button {\n min-height: 2.75rem;\n }\n\n .szenen-elements__back {\n justify-self: stretch;\n min-height: 2.5rem;\n }\n\n .szenen-detail__actions,\n .szenen-runs__start,\n .szenen-scene-editor__add,\n .szenen-scene-editor__add-relation {\n align-items: stretch;\n flex-direction: column;\n }\n\n .szenen-runs__start > *,\n .szenen-scene-editor__add > *,\n .szenen-scene-editor__add-relation > * {\n width: 100%;\n flex: none;\n }\n}\n\n/* \u2500\u2500 Stufe 6.5: Rekonstruktion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Bereich zeigt drei Dinge nebeneinander: den Vorschlag, das nicht\n * Ableitbare und die Widerspr\xFCche. Die Diagnosen sind deshalb bewusst nicht\n * kleiner gesetzt als der Vorschlag \u2014 wer sie \xFCbersieht, \xFCbernimmt Geratenes.\n */\n\n.szenen-derive {\n display: grid;\n gap: var(--space-md);\n}\n\n.szenen-form {\n display: grid;\n gap: var(--space-sm);\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-field {\n display: grid;\n gap: var(--space-xs);\n}\n\n.szenen-field > span {\n color: var(--color-text-secondary);\n font-size: 0.875rem;\n}\n\n.szenen-error {\n padding: var(--space-sm) var(--space-md);\n color: var(--color-danger);\n background: var(--color-accent-red-dim);\n border: 1px solid var(--color-accent-red);\n border-radius: var(--radius-sm);\n}\n\n.szenen-empty {\n padding: var(--space-md);\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-diagnose {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-warning);\n border-radius: var(--radius-sm);\n}\n\n/* Ein Widerspruch ist der schwerste der drei Befunde: dort steht eine\n * Entscheidung aus, die das System bewusst nicht getroffen hat (D-74). */\n.szenen-diagnose--konflikt {\n border-left-color: var(--color-danger);\n}\n\n.szenen-diagnose > h4 {\n margin: 0 0 var(--space-sm);\n font-size: 0.9375rem;\n}\n\n.szenen-diagnose ul {\n display: grid;\n gap: var(--space-xs);\n margin: 0;\n padding-left: var(--space-md);\n}\n\n.szenen-proposal {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-proposal > h4 {\n margin: 0 0 var(--space-sm);\n font-family: var(--font-mono);\n font-size: 0.9375rem;\n}\n\n.szenen-proposal__payload {\n max-height: 18rem;\n margin: 0 0 var(--space-sm);\n padding: var(--space-sm);\n overflow: auto;\n /* Ein erzeugter Prompt ist ein einziger langer Satz \u2014 ohne Umbruch\n verschwindet er nach rechts aus dem Bild. JSON-Payloads bringen ihre\n eigenen Zeilenumbrueche mit und leiden nicht darunter. */\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n font-family: var(--font-mono);\n font-size: 0.8125rem;\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-proposal__done {\n margin: var(--space-sm) 0 0;\n color: var(--color-success);\n}\n\n/* Die Belege eines Laufs: Rohantwort und Systemprompt. Zum Lesen und Kopieren,\n nicht zum Bedienen \u2014 deshalb voll markierbar und in Monospace. */\n.szenen-beleg__text {\n width: 100%;\n margin: var(--space-sm) 0 0;\n padding: var(--space-sm);\n font-family: var(--font-mono);\n font-size: 0.8125rem;\n line-height: 1.5;\n white-space: pre;\n background: var(--color-bg-app);\n color: var(--color-text);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n resize: vertical;\n}\n\n/* Der Folgelauf (D-87): abgesetzt, damit er nicht wie Teil des Ergebnisses\n aussieht \u2014 er ist eine Frage, keine Auskunft. */\n.szenen-folgelauf {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-folgelauf > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-run__herkunft {\n margin: 0 0 var(--space-sm);\n font-size: 0.875rem;\n color: var(--color-text-muted, inherit);\n}\n\n/* Widerspricht das Modell, ist das das Wichtigste auf der Seite. */\n.szenen-diagnose--antwort {\n border-left: 3px solid var(--color-accent, var(--color-border));\n}\n\n/* Der Bildw\xE4hler (D-92) \u2014 Browser und Inspector, dem Labs-Tab nachgebildet,\n aber mit dem CSS dieser Ansicht: eine gemeinsame Komponente \xFCber zwei\n Dom\xE4nengrenzen w\xE4re eine Kopplung, die keiner gewollt hat.\n\n Der Review vom 2026-08-22 (R12.1/R12.2) hat den Ma\xDFstab gesetzt: Der Bestand\n ist ein **Browser**, kein Seiteninhalt. Vierzig Werke d\xFCrfen die Szenenseite\n nicht meterlang machen, und zw\xF6lf Varianten eines Werkes keine zw\xF6lf\n Bildschirmseiten. Deshalb steht hier alles unter zwei Regeln \u2014 begrenzte\n Fl\xE4che mit eigenem Scroll, und Kacheln statt Karten. */\n\n/* \u2500\u2500 Ebene 1: der Browser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl {\n /* Dieselbe Begr\xFCndung wie beim Inspector: Als Grid-Element w\xFCrde der\n W\xE4hler sonst so breit wie sein breitester Inhalt. */\n min-width: 0;\n}\n\n.szenen-bildwahl__browser {\n /* Die eigene Scrollfl\xE4che ist der ganze Punkt: Der Bestand w\xE4chst nach\n innen, nicht nach unten. Dieselbe Gr\xF6\xDFenordnung wie `max-h-[300px]` im\n Labs-Browser. */\n max-height: 19rem;\n overflow-y: auto;\n overscroll-behavior: contain;\n margin-top: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__raster {\n display: grid;\n /* Vier Spalten auf schmalen Displays \u2014 der gemeldete Screenshot zeigte eine\n einzige Spalte gro\xDFer Karten. Breiter wird automatisch dichter. */\n grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildwahl__karte {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n overflow: hidden;\n background: var(--color-bg-surface);\n text-align: left;\n cursor: pointer;\n}\n\n/* Das fokussierte Werk ist klar markiert \u2014 sonst wei\xDF niemand, wessen Galerie\n der Inspector unten zeigt. */\n.szenen-bildwahl__karte.is-fokus {\n border-color: var(--color-accent, var(--color-text-primary));\n box-shadow: 0 0 0 2px var(--color-accent, var(--color-text-primary));\n}\n\n/* \u2500\u2500 Die Kachel selbst \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Der Platz steht, bevor das Bild da ist \u2014 sonst springt das Raster, sobald\n die erste Vorschau eintrifft. */\n.szenen-bildkachel {\n display: block;\n width: 100%;\n aspect-ratio: 1;\n overflow: hidden;\n background: var(--color-bg-app);\n}\n\n.szenen-bildkachel img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n\n.szenen-bildkachel__leer {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n background: linear-gradient(135deg, var(--color-bg-app), var(--color-bg-surface));\n}\n\n/* R12.3: Eine fehlende Vorschau bleibt kachelgro\xDF. Vorher fiel die Karte auf\n ihren Prompttext zur\xFCck \u2014 und ein Prompt hat im echten Bestand mehrere\n hundert Zeichen. */\n.szenen-bildkachel__zeichen {\n font-size: 0.875rem;\n font-weight: 700;\n color: var(--color-text-secondary);\n opacity: 0.6;\n}\n\n/* Wie viele Varianten das Werk hat \u2014 klein und \xFCberlagernd, nie als\n dominanter Text. */\n.szenen-bildwahl__anzahl {\n position: absolute;\n top: 0.15rem;\n right: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n font-weight: 700;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* Eine Zeile, dann Schluss. Der Prompt eines Werkes ist oft mehrere hundert\n Zeichen lang und w\xE4re als Prim\xE4rdarstellung genau die Textw\xFCste, gegen die\n der Review antritt. */\n.szenen-bildwahl__kacheltitel {\n display: block;\n padding: 0.15rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.3;\n color: var(--color-text-secondary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 Ebene 2: der Inspector \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl__inspector {\n /* Ohne `min-width: 0` gewinnt der Inhalt: Der W\xE4hler ist Element eines\n Grids (`.szenen-form`), und dort ist `min-width: auto` die Vorgabe. Der\n Streifen unten ist rund 1000 px breit \u2014 gemessen am 2026-08-22 im echten\n Browser zog er die Auswahl von 374 px auf 1088 px auf und schob die\n ganze Seite waagerecht heraus, statt selbst zu scrollen. */\n min-width: 0;\n margin-top: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n /* Sichtbar abgesetzt vom Browser: zwei Ebenen, nicht eine lange Liste. */\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-accent, var(--color-border));\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__inspektorkopf {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-sm);\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bildwahl__werktitel {\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--color-text-primary);\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.szenen-bildwahl__zurueck {\n flex: 0 0 auto;\n font-size: 0.6875rem;\n padding: 0.15rem 0.5rem;\n}\n\n/* Der Streifen: waagerecht scrollend, feste kleine Kacheln \u2014 zw\xF6lf Varianten\n bleiben ein \xDCberblick statt zw\xF6lf Bildschirmseiten. */\n.szenen-bildwahl__streifen {\n /* Die Breite kommt vom Elter, nie vom Inhalt \u2014 sonst scrollt hier nichts. */\n max-width: 100%;\n display: flex;\n gap: var(--space-sm);\n margin: 0;\n padding: 0 0 0.25rem;\n list-style: none;\n overflow-x: auto;\n overscroll-behavior-x: contain;\n}\n\n.szenen-bildwahl__streifen > li {\n flex: 0 0 auto;\n width: 5rem;\n}\n\n.szenen-bildwahl__variante {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n overflow: hidden;\n background: var(--color-bg-app);\n cursor: pointer;\n}\n\n.szenen-bildwahl__variante.is-gewaehlt {\n border-color: var(--color-accent, var(--color-text-primary));\n box-shadow: 0 0 0 2px var(--color-accent, var(--color-text-primary));\n}\n\n.szenen-bildwahl__nummer {\n position: absolute;\n bottom: 0.15rem;\n left: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border-radius: var(--radius-sm);\n}\n\n/* \u2500\u2500 Das Gew\xE4hlte \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl__gewaehlt {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: var(--space-sm) 0 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildwahl__gewaehlt li {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.25rem 0.5rem;\n font-size: 0.8125rem;\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n max-width: 100%;\n}\n\n/* Das gew\xE4hlte Bild wird best\xE4tigt, nicht nur benannt: eine kleine Vorschau\n neben dem Titel. */\n.szenen-bildwahl__gewaehlt .szenen-bildkachel {\n width: 2.25rem;\n flex: 0 0 auto;\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__gewaehlt .szenen-bildwahl__kacheltitel {\n min-width: 0;\n max-width: 12rem;\n font-size: 0.75rem;\n}\n\n.szenen-beleg__kopf {\n display: flex;\n align-items: center;\n gap: var(--space-sm);\n margin-top: var(--space-sm);\n}\n\n.szenen-diagnose > details + details {\n margin-top: var(--space-sm);\n}\n\n.szenen-diagnose > details > summary {\n cursor: pointer;\n font-weight: 600;\n}\n\n/* \u2500\u2500 Die Fehlerrettung (Stufe 6.6, A5) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Sie steht im Fehlerblock und muss trotzdem als Werkzeug lesbar sein, nicht\n * als weitere Fehlermeldung. Deshalb abgesetzt wie der Folgelauf \u2014 und aus\n * demselben Grund: Beides sind Angebote, keine Ausk\xFCnfte.\n *\n * Der wichtigste optische Punkt ist die Trennung von Original und\n * Arbeitskopie. Das Original bekommt den ruhigen, \u201Efesten" Hintergrund der\n * Belege; die Kopie sieht aus wie ein normales Eingabefeld, weil sie eines\n * ist. */\n.szenen-rettung {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-rettung > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-rettung textarea[readonly] {\n background: var(--color-bg-surface);\n color: var(--color-text-secondary);\n font-family: var(--font-mono, monospace);\n font-size: 0.8125rem;\n}\n\n.szenen-rettung textarea:not([readonly]) {\n font-family: var(--font-mono, monospace);\n font-size: 0.8125rem;\n}\n\n.szenen-rettung__aktionen {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: var(--space-sm) 0;\n}\n\n/* Ein Befund tr\xE4gt Code, Bezug und Meldung. Der Code steht klein und\n * monospaced davor: Er ist die Kennung, nicht die Aussage. */\n.szenen-diagnose__liste {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding-left: var(--space-md);\n}\n\n.szenen-diagnose__liste code {\n font-size: 0.8125rem;\n}\n\n.szenen-diagnose__liste > li[data-severity=\'warning\'] {\n color: var(--color-text-secondary);\n}\n\n/* \u2500\u2500 Die Draft-Kette (Stufe 6.6, C4) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Hauptweg. Er steht nach den Einzel\xFCbernahmen und muss als das erkennbar\n * sein, was er ist: nicht eine weitere Option, sondern der k\xFCrzere Weg zum\n * selben Ziel. Deshalb durchgezogener Rahmen statt gestricheltem \u2014 er ist\n * Angebot **und** Empfehlung. */\n.szenen-kette {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-accent, var(--color-border));\n border-radius: var(--radius-sm);\n}\n\n.szenen-kette > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-kette ul {\n display: grid;\n gap: var(--space-xs);\n margin: 0 0 var(--space-sm);\n padding-left: var(--space-md);\n}\n\n.szenen-kette code {\n font-size: 0.8125rem;\n}\n\n/* Ein Hinweis, der eine Bedingung erklaert \u2014 nicht rot, weil er kein Fehler\n ist, aber abgesetzt, weil er eine Folge hat. */\n.szenen-detail__hint {\n margin: var(--space-sm) 0 0;\n padding: var(--space-sm);\n font-size: 0.875rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border-left: 3px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* \u2500\u2500 Stufe 6.7: Bilder am Prompt-Lauf (H0/H1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Bereich tr\xE4gt zwei Dinge, die man nie verwechseln darf: den **Eingang**\n * (woraus der Prompt entstand) und den **Ausgang** (was daraus wurde). Sie\n * bekommen deshalb verschiedene Akzentkanten statt nur verschiedener\n * \xDCberschriften \u2014 wer schnell liest, sieht die Trennung, bevor er sie liest.\n */\n.szenen-bilder {\n display: grid;\n gap: var(--space-md);\n margin-top: var(--space-sm);\n}\n\n.szenen-bilder__eingang,\n.szenen-bilder__ausgang {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__eingang {\n border-left: 3px solid var(--color-accent-purple, var(--color-border));\n}\n\n.szenen-bilder__ausgang {\n border-left: 3px solid var(--color-accent-teal, var(--color-border));\n}\n\n.szenen-bilder h5 {\n margin: 0;\n}\n\n.szenen-bilder__ids {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: 0;\n align-items: baseline;\n color: var(--color-text-secondary);\n font-size: 0.875rem;\n}\n\n.szenen-bilder__eingangsliste,\n.szenen-bilder__batchliste,\n.szenen-bilder__auftraege {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bilder__eingangsliste > li,\n.szenen-bilder__batchliste > li,\n.szenen-bilder__auftraege > li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n font-size: 0.8125rem;\n}\n\n/* Ein unterbrochener Auftrag ist der Fall, f\xFCr den der Auftragssatz existiert.\n Er wird deshalb markiert \u2014 nicht rot, denn er ist kein Fehler, sondern eine\n offene Entscheidung. */\n.szenen-bilder__auftraege > li[data-status="started"] {\n border-left: 3px solid var(--color-warning);\n}\n\n.szenen-bilder__auftraege > li[data-status="succeeded"] {\n border-left: 3px solid var(--color-success);\n}\n\n.szenen-bilder__auftraege > li[data-status="partial"] {\n border-left: 3px solid var(--color-accent-blue, var(--color-border));\n}\n\n.szenen-bilder__auftraege > li[data-status="failed"] {\n border-left: 3px solid var(--color-danger);\n}\n\n/* Das Eingangsbild benutzt seit C3 dieselbe Kachel wie W\xE4hler und Galerie.\n Sie ist von Haus aus so breit wie ihr Platz \u2014 in dieser Zeile bekommt sie\n deshalb eine feste Gr\xF6sse, sonst sch\xF6be sie die Metadaten heraus. */\n.szenen-bilder__eingangsliste .szenen-bildkachel {\n flex: 0 0 auto;\n width: 4rem;\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__batch {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__batchkopf {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--space-sm);\n}\n\n.szenen-bilder__herkunft {\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n}\n\n/* Review-Befund \u2014 ein transienter Bildfehler ist wiederholbar, kein\n endg\xFCltiges \u201Efehlt". Dezent wie ein Angebot, nicht wie ein Fehlerzustand. */\n.szenen-bilder__retry {\n justify-self: start;\n padding: 0.15rem 0.5rem;\n font-size: 0.75rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n/* C4 \u2014 die Provenienz vollst\xE4ndig, aber sekund\xE4r. */\n.szenen-bilder__technik > summary {\n color: var(--color-text-secondary);\n font-size: 0.75rem;\n cursor: pointer;\n}\n\n.szenen-bilder__meta {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n font-size: 0.75rem;\n color: var(--color-text-secondary);\n}\n\n/* C5 \u2014 der Verlauf verdichtet, seine Ausreisser nicht. */\n.szenen-bilder__verlauf > summary {\n color: var(--color-text-secondary);\n font-size: 0.8125rem;\n cursor: pointer;\n}\n\n.szenen-bilder__verlauf[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bilder__auffaellig {\n margin: 0;\n padding: var(--space-sm);\n border-radius: var(--radius-sm);\n font-size: 0.8125rem;\n}\n\n.szenen-bilder__auftragskennung {\n flex-basis: 100%;\n color: var(--color-text-tertiary);\n font-size: 0.6875rem;\n}\n\n/* \u2500\u2500 C1/C2 \u2014 Galerie und grosse Ansicht \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Das Raster w\xE4chst mit dem Platz: auf 390 px drei bis vier Kacheln je Reihe,\n auf dem Desktop entsprechend mehr. Vorher war jedes Bild eine Zeile aus\n Daumennagel plus vier Metadatenfeldern \u2014 vier Bilder ergaben vier Textzeilen\n und das Protokoll war die Hauptsache. */\n.szenen-bildgalerie {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr));\n gap: var(--space-sm);\n min-width: 0;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildgalerie__kachel {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-bildgalerie__kachel:hover,\n.szenen-bildgalerie__kachel:focus-visible {\n border-color: var(--color-accent-teal, var(--color-text-primary));\n}\n\n.szenen-bildgalerie__nummer {\n position: absolute;\n bottom: 0.15rem;\n left: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border-radius: var(--radius-sm);\n}\n\n/* Die grosse Ansicht liegt \xFCber allem und bringt ihren eigenen Grund mit \u2014\n `position: fixed` bezieht sich im Flow-Canvas auf dessen Rahmen, in React auf\n das Fenster; in beiden F\xE4llen genau das Gew\xFCnschte. */\n.szenen-bildviewer {\n position: fixed;\n inset: 0;\n z-index: 1000;\n display: grid;\n grid-template-rows: auto 1fr auto;\n gap: var(--space-sm);\n padding: var(--space-sm);\n /* Deckend, nicht get\xF6nt: Bei 0.92 schimmerte der Laufbereich durch und man\n las Prompttext quer \xFCber dem Bild \u2014 im Browser gemessen. */\n background: #000;\n}\n\n/* Titel, Z\xE4hlung, Schliessen \u2014 drei Spalten, damit ein langer Werktitel die\n Z\xE4hlung nicht in die n\xE4chste Zeile und \xFCber das Bild schiebt. */\n.szenen-bildviewer__kopf {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto auto;\n align-items: center;\n gap: var(--space-sm);\n color: var(--color-text-primary);\n}\n\n.szenen-bildviewer__titel {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-weight: 600;\n}\n\n.szenen-bildviewer__zaehler {\n color: var(--color-text-secondary);\n font-size: 0.8125rem;\n}\n\n.szenen-bildviewer__buehne {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--space-sm);\n min-height: 0;\n min-width: 0;\n}\n\n/* Der Rahmen nimmt den Platz, der zwischen den Bl\xE4tterkn\xF6pfen \xFCbrig ist \u2014\n `min-width: 0`, damit er auch wirklich schrumpft statt sie hinauszuschieben. */\n.szenen-bildviewer__rahmen {\n flex: 1 1 auto;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n min-height: 0;\n height: 100%;\n}\n\n.szenen-bildviewer__bild {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n}\n\n.szenen-bildviewer__leer {\n color: var(--color-text-secondary);\n text-align: center;\n}\n\n/* Die Bl\xE4tterkn\xF6pfe sind daumengross und liegen an den R\xE4ndern \u2014 auf einem\n Telefon ist der Rand erreichbar, die Bildmitte nicht. */\n.szenen-bildviewer__blaettern {\n flex: 0 0 auto;\n width: 2.75rem;\n height: 3.5rem;\n font-size: 1.75rem;\n line-height: 1;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-bildviewer__details {\n max-height: 8rem;\n overflow-y: auto;\n color: var(--color-text-secondary);\n}\n\n/* Die Aktionen stehen abgesetzt: Sie sind Angebote, keine Ausk\xFCnfte \u2014 dasselbe\n Muster wie beim Folgelauf und bei der Fehlerrettung. */\n/* D \u2014 die Aktionen stehen abgesetzt und standardm\xE4ssig kompakt: Sie sind\n Angebote, keine Ausk\xFCnfte. Vorher hielten beide Formulare dauerhaft offen und\n machten aus jedem Lauf zwei zus\xE4tzliche Bildschirmseiten. */\n.szenen-bilder__aktion {\n min-width: 0;\n padding: var(--space-sm) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__aktion > summary {\n color: var(--color-text-primary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-bilder__aktion[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bilder__aktion-inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding-bottom: var(--space-sm);\n}\n\n.szenen-bilder__aktion h5 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-bilder__aktion ol {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding-left: var(--space-md);\n font-size: 0.8125rem;\n}\n\n/* Regelwerk-UX Schritt 16 \u2014 der ZIP-Sammelimport.\n *\n * \xA78: Der gemeldete Einsatz ist mobil. Deshalb **ein** Seedfeld statt einer\n * gestapelten Eingabe je Bild, und die Zuordnung Bild -> Seed als umbrechende\n * Zeile statt als Tabelle: Eine Tabelle waere auf 390 px abgeschnitten, und\n * abgeschnitten heisst hier, dass der Nutzer die Zuordnung nicht mehr pruefen\n * kann \u2014 genau die Pruefung, wegen der die Vorschau existiert. */\n.szenen-bilder__zip {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n}\n\n.szenen-bilder__zipkopf {\n margin: 0;\n overflow-wrap: anywhere;\n}\n\n.szenen-bilder__zipvorlagen {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n margin: 0;\n}\n\n/* Eine laengere Liste bleibt scrollbar, statt die Sektion und damit den\n * Import-Knopf aus dem Blick zu schieben (\xA78, letzter Punkt). */\n.szenen-bilder__zipliste {\n max-height: 16rem;\n overflow-y: auto;\n}\n\n.szenen-bilder__zipliste > li {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-between;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-bilder__zipliste > li > span:first-child {\n overflow-wrap: anywhere;\n}\n\n.szenen-bilder__zipliste > li > span:last-child {\n color: var(--color-text-secondary);\n white-space: nowrap;\n}\n\n/* ==========================================================================\n STUFE 6.10 \u2014 ELEMENTEDITOR UND VERSIONSNAVIGATION\n ========================================================================== */\n\n/* \xA74.2 \u2014 vier Sachverhalte, vier Marken. Die Marken stehen in einer eigenen\n * Spalte, damit ANGEZEIGT + BEVORZUGT nebeneinander lesbar bleiben, statt sich\n * durch den Text zu schieben. */\n.szenen-detail__heads li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-sm);\n}\n\n.szenen-version__titel {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n min-width: 0;\n margin-right: auto;\n}\n\n/* V17 \u2014 Kennungen bleiben vollst\xE4ndig sichtbar, aber optisch sekund\xE4r. */\n.szenen-version__id,\n.szenen-ref__id,\n.szenen-detail__field-key {\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n overflow-wrap: anywhere;\n}\n\n.szenen-version__marken {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n}\n\n.szenen-version__marken > span {\n padding: 0.1rem 0.5rem;\n color: var(--color-text-primary);\n background: rgba(45, 212, 191, 0.12);\n border: 1px solid rgba(45, 212, 191, 0.35);\n border-radius: var(--radius-full);\n font-size: 0.7rem;\n font-weight: 800;\n letter-spacing: 0.08em;\n}\n\n.szenen-version--angezeigt {\n border-color: var(--color-accent-teal, #2dd4bf) !important;\n}\n\n/* V9 \u2014 der Basiswechsel ist eine Entscheidung, keine Nebenwirkung. */\n.szenen-detail__confirm {\n display: grid;\n gap: var(--space-sm);\n margin: var(--space-md) 0;\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-accent-amber, #f59e0b);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__confirm p {\n margin: 0;\n}\n\n/* Untergruppen von Objektfeldern (`prompt_render_settings`, `overrides`,\n `blender_render_settings`) \u2014 seit A1/A2 auf- und zuklappbar. Eine optionale\n Gruppe startet zu: Blender und `overrides` f\xFCllten auf einem 390-px-Display\n sonst mehrere Bildschirmseiten, obwohl die meisten Shots sie nie anfassen. */\n.szenen-detail__group {\n min-width: 0;\n margin: var(--space-md) 0;\n padding: var(--space-sm) var(--space-md);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__group > summary {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-detail__group[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n/* A2 \u2014 dass eine zugeklappte Gruppe schon Werte tr\xE4gt, muss man sehen, ohne\n sie zu \xF6ffnen. Sonst \xFCbersieht man eine gesetzte Blender-Ausgabe genau so\n lange, bis sie im Ergebnis auftaucht. */\n.szenen-detail__group-status {\n padding: 0.05rem 0.4rem;\n font-size: 0.6875rem;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--color-text-tertiary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__group[data-konfiguriert="ja"] > summary .szenen-detail__group-status {\n color: var(--color-text-primary);\n border-color: var(--color-accent-teal, var(--color-border));\n}\n\n.szenen-detail__group-inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding-bottom: var(--space-sm);\n}\n\n/* A3 \u2014 die Kernaussage steht am Feld, die Begr\xFCndung dahinter. Nicht als\n Tooltip: Auf einem Telefon gibt es kein Hover, und was dort unerreichbar ist,\n ist gestrichen \u2014 nur ohne dass es jemand entschieden h\xE4tte. */\n.szenen-detail__warum > summary {\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n cursor: pointer;\n}\n\n.szenen-detail__warum > small {\n display: block;\n padding-top: var(--space-xs);\n color: var(--color-text-tertiary);\n}\n\n.szenen-detail__fieldrow {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n margin-bottom: var(--space-md);\n}\n\n/* A4 \u2014 der technische Feldname bleibt lesbar, tritt aber hinter das fachliche\n Label zur\xFCck. */\n.szenen-detail__field-label {\n color: var(--color-text-primary);\n}\n\n.szenen-detail__field {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-detail__field > span {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n}\n\n/* Mehrfachauswahl und Listeneditoren. */\n.szenen-feld__mehrfach,\n.szenen-feld__liste,\n.szenen-feld__reflist,\n.szenen-feld__ref,\n.szenen-feld__zielmodell,\n.szenen-feld__kamera {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-feld__mehrfach > label,\n.szenen-feld__null {\n display: flex;\n align-items: center;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-size: 0.85rem;\n}\n\n.szenen-feld__liste-zeile {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n}\n\n.szenen-feld__reflist ol {\n display: grid;\n gap: var(--space-xs);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-feld__reflist li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-xs);\n padding: var(--space-xs) var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* \xA76.1 \u2014 Titel und Typ zuerst, Kennungen daneben. */\n.szenen-ref__anzeige {\n display: inline-flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-ref__typ {\n color: var(--color-text-secondary);\n font-size: 0.8rem;\n}\n\n.szenen-ref__warn {\n padding: 0.1rem 0.5rem;\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-radius: var(--radius-full);\n font-size: 0.72rem;\n font-weight: 700;\n}\n\n/* \xA76.5 \u2014 abgeleitete Angaben, ausdr\xFCcklich nur Anzeige. */\n.szenen-detail__derived {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__derived dl {\n display: grid;\n grid-template-columns: minmax(10rem, auto) 1fr;\n gap: var(--space-xs) var(--space-sm);\n margin: 0;\n}\n\n.szenen-detail__derived dt {\n color: var(--color-text-secondary);\n}\n\n.szenen-detail__derived dd {\n margin: 0;\n}\n\n.szenen-feld__abgeleitet {\n margin: 0;\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n}\n';
|
|
63
|
+
var SZENEN_VIEW_STYLES = '/* ==========================================================================\n SZENEN VIEW\n ========================================================================== */\n\n.szenen-view {\n display: grid;\n gap: var(--space-md);\n width: 100%;\n min-width: 0;\n font-family: var(--font-sans);\n padding: var(--space-md);\n}\n\n@media (max-width: 760px) {\n .szenen-view {\n padding: var(--space-sm);\n gap: var(--space-sm);\n }\n}\n\n.szenen-view__intro {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-sm);\n padding: clamp(0.6rem, 1.2vw, 0.9rem) clamp(0.75rem, 1.6vw, 1.1rem);\n color: var(--color-text-primary);\n background:\n radial-gradient(circle at 85% 15%, rgba(45, 212, 191, 0.16), transparent 34%),\n linear-gradient(135deg, var(--color-bg-surface), var(--color-bg-elevated));\n border: 1px solid var(--color-border);\n border-radius: var(--radius-lg);\n}\n\n.szenen-view__intro h2,\n.szenen-view__intro p {\n margin: 0;\n}\n\n.szenen-view__intro h2 {\n margin-bottom: 0.1rem;\n font-size: clamp(1rem, 1.9vw, 1.3rem);\n line-height: 1.2;\n}\n\n.szenen-view__intro > div > p:last-child {\n max-width: 44rem;\n color: var(--color-text-secondary);\n font-size: 0.82rem;\n line-height: 1.35;\n}\n\n.szenen-view__eyebrow {\n margin-bottom: 0.1rem !important;\n color: var(--color-accent-teal, #2dd4bf);\n font-size: 0.62rem;\n font-weight: 800;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n}\n\n.szenen-view__step {\n flex: none;\n padding: 0.25rem 0.6rem;\n color: var(--color-text-primary);\n background: rgba(45, 212, 191, 0.12);\n border: 1px solid rgba(45, 212, 191, 0.35);\n border-radius: var(--radius-full);\n font-size: 0.72rem;\n font-weight: 700;\n}\n\n.szenen-view__nav {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-view__nav button,\n.szenen-view button {\n min-height: 2.25rem;\n padding: 0.45rem 0.75rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-view button:hover:not(:disabled) {\n color: var(--color-text-primary);\n background: var(--color-bg-surface-hover);\n border-color: var(--color-border-hover);\n}\n\n.szenen-view button:focus-visible,\n.szenen-view input:focus-visible,\n.szenen-view select:focus-visible,\n.szenen-view textarea:focus-visible {\n outline: 2px solid var(--color-accent-blue);\n outline-offset: 2px;\n}\n\n.szenen-view button:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.szenen-view__nav button.is-active,\n.szenen-view__nav button[aria-selected="true"] {\n color: var(--color-text-primary);\n background: var(--color-accent-blue-dim);\n border-color: var(--color-accent-blue);\n}\n\n.szenen-view__area {\n min-width: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-view__capability-note,\n.szenen-area__lead {\n margin: 0;\n padding: var(--space-sm) var(--space-md);\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border-left: 3px solid var(--color-accent-purple);\n border-radius: var(--radius-sm);\n}\n\n.szenen-view__host-notes {\n color: var(--color-text-secondary);\n}\n\n.szenen-view__host-notes > summary {\n cursor: pointer;\n font-size: 0.8rem;\n}\n\n/* `box-sizing` steht hier und nicht beim Host: Die React-App bringt in\n * `master.css` einen globalen `* { box-sizing: border-box }` mit, Flow **nicht**\n * \u2014 dort ragte `width: 100%` plus Innenabstand um die Randbreite aus dem\n * Elternelement heraus und erzeugte auf ~390 px einen horizontalen Scroll. Die\n * gemeinsame Ansicht darf sich auf keinen Host-Reset verlassen. */\n.szenen-view input:not([type="checkbox"]),\n.szenen-view select,\n.szenen-view textarea {\n box-sizing: border-box;\n width: 100%;\n min-height: 2.25rem;\n padding: 0.5rem 0.65rem;\n color: var(--color-text-primary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n font: inherit;\n}\n\n.szenen-view textarea {\n min-height: 7rem;\n resize: vertical;\n font-family: var(--font-mono);\n font-size: 0.8rem;\n}\n\n.szenen-view label {\n display: grid;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-size: 0.85rem;\n}\n\n.szenen-view code,\n.szenen-view pre {\n font-family: var(--font-mono);\n overflow-wrap: anywhere;\n}\n\n.szenen-view h3,\n.szenen-view h4,\n.szenen-view h5 {\n margin-top: 0;\n}\n\n.szenen-view ul,\n.szenen-view ol {\n padding-left: 1.25rem;\n}\n\n.szenen-elements,\n.szenen-detail,\n.szenen-scene,\n.szenen-setup,\n.szenen-shot,\n.szenen-media,\n.szenen-runs {\n display: grid;\n gap: var(--space-md);\n min-width: 0;\n}\n\n/* Arbeitsoberflaeche: Liste links, Bearbeitung rechts.\n *\n * Breit stehen beide Spalten nebeneinander \u2014 die Auswahl bleibt sichtbar,\n * waehrend bearbeitet wird. Schmal rendert die Komponente nur eine der beiden;\n * hier steht deshalb keine Regel, die die andere versteckt. */\n@media (min-width: 761px) {\n .szenen-elements {\n grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);\n align-items: start;\n }\n\n .szenen-elements__master,\n .szenen-elements__detail {\n min-width: 0;\n }\n}\n\n.szenen-elements__master,\n.szenen-elements__detail {\n display: grid;\n gap: var(--space-sm);\n align-content: start;\n min-width: 0;\n}\n\n.szenen-elements__toolbar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-sm);\n}\n\n.szenen-elements__heading {\n margin: 0;\n font-size: 1.05rem;\n}\n\n.szenen-elements__scope {\n display: flex;\n align-items: center;\n gap: var(--space-xs);\n min-width: 0;\n color: var(--color-text-secondary);\n font-size: 0.8rem;\n}\n\n.szenen-elements__scope > select {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.szenen-elements__new {\n flex: none;\n font-weight: 700;\n}\n\n.szenen-elements__create {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-accent-blue);\n border-radius: var(--radius-sm);\n}\n\n.szenen-elements__create label {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-elements__create-actions {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n}\n\n.szenen-elements__options > summary {\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n cursor: pointer;\n}\n\n.szenen-elements__back {\n justify-self: start;\n font-weight: 700;\n}\n\n.szenen-elements__empty-detail {\n margin: 0;\n color: var(--color-text-tertiary);\n}\n\n.szenen-elements__filter,\n.szenen-detail__actions,\n.szenen-runs__start,\n.szenen-scene-editor__add,\n.szenen-scene-editor__add-relation {\n display: flex;\n flex-wrap: wrap;\n align-items: end;\n gap: var(--space-sm);\n}\n\n.szenen-elements__filter > label,\n.szenen-runs__start > label,\n.szenen-scene-editor__add > input {\n flex: 1 1 16rem;\n}\n\n.szenen-elements__filter > label:has(input[type="checkbox"]) {\n display: flex;\n flex: 1 1 16rem;\n flex-direction: row;\n align-items: center;\n}\n\n.szenen-elements__filter input[type="checkbox"] {\n width: 1rem;\n height: 1rem;\n accent-color: var(--color-accent-blue);\n}\n\n.szenen-elements__list {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-elements__list > li > button {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs) var(--space-sm);\n width: 100%;\n height: 100%;\n text-align: left;\n}\n\n.szenen-elements__list > li > button > .szenen-elements__title {\n flex: 1 1 100%;\n}\n\n.szenen-elements__list > li > button.is-selected,\n.szenen-elements__list > li > button[aria-current="true"] {\n background: var(--color-accent-blue-dim);\n border-color: var(--color-accent-blue);\n}\n\n.szenen-elements__type {\n color: var(--color-accent-blue);\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n}\n\n.szenen-elements__title {\n color: var(--color-text-primary);\n font-size: 0.95rem;\n}\n\n.szenen-elements__subtype,\n.szenen-elements__retired {\n color: var(--color-text-secondary);\n font-size: 0.75rem;\n}\n\n.szenen-elements__retired {\n color: var(--color-warning);\n}\n\n.szenen-detail {\n margin-top: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n@media (min-width: 761px) {\n .szenen-detail {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n }\n}\n\n.szenen-detail > header,\n.szenen-detail__heads,\n.szenen-detail__draft,\n.szenen-capability {\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n/* .szenen-runs ist ein eigenst\xE4ndiger sichtbarer Bereich, beh\xE4lt Styling auf Desktop */\n@media (min-width: 761px) {\n .szenen-runs {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n }\n}\n\n@media (max-width: 760px) {\n .szenen-runs {\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n }\n}\n\n.szenen-detail__ids {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin-bottom: 0;\n color: var(--color-text-secondary);\n}\n\n.szenen-detail__heads ul,\n.szenen-scene-editor ul,\n.szenen-runs > ol,\n.szenen-capability > ul {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-detail__heads li,\n.szenen-scene-editor li,\n.szenen-runs > ol > li,\n.szenen-capability > ul > li {\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n\n/* Stufe 6.7.2 (D-126) \u2014 Advanced-/Debug-Fl\xE4che f\xFCr die rohe, schema-getriebene\n * Feldliste einer Szene. Gestrichelt wie die anderen \u201EAngebot statt\n * Auskunft"-Bereiche, aber ohne Akzentfarbe: sie ist kein Vorschlag, sondern\n * ein Eskalationsweg. */\n.szenen-detail__technical {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__technical > summary {\n margin-bottom: var(--space-sm);\n color: var(--color-text-secondary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-detail__technical[open] > summary {\n margin-bottom: var(--space-md);\n}\n\n.szenen-detail__field small,\n.szenen-capability small {\n color: var(--color-text-tertiary);\n}\n\n.szenen-detail__preview-errors,\n.szenen-detail__notice--error,\n.szenen-view [role="alert"] {\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-color: var(--color-accent-red);\n}\n\n.szenen-detail__notice {\n padding: var(--space-sm) var(--space-md);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__notice--success,\n.szenen-view [data-tone="success"] {\n color: #bbf7d0;\n background: var(--color-accent-green-dim);\n border-color: var(--color-accent-green);\n}\n\n.szenen-scene-editor {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: var(--space-md);\n margin-bottom: 0;\n}\n\n.szenen-scene-editor > section {\n min-width: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n/* \u2500\u2500 Stufe 6.7.2 (D-126): Node-/Relationskarten \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Nodes und Beziehungen sind `<details>` \u2014 dieselbe Interaktionssprache wie\n * \u201ERohantwort (Diagnose)" und \u201EBilder zu diesem Lauf" in RunsPanel.tsx. Kopf\n * (immer sichtbar) tr\xE4gt Inhalt zuerst, Technik zuletzt; Body (nur bei\n * ge\xF6ffneter Karte) tr\xE4gt die Bearbeitungscontrols. Padding wandert von der\n * `<li>` in Kopf/Body, damit die Karte keinen doppelten Rand bekommt. */\n.szenen-scene-editor li {\n padding: 0;\n overflow: hidden;\n}\n\n.szenen-node-karte__kopf,\n.szenen-relation-karte__kopf {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-xs) var(--space-sm);\n padding: var(--space-sm);\n cursor: pointer;\n list-style: none;\n}\n\n.szenen-node-karte__kopf::-webkit-details-marker,\n.szenen-relation-karte__kopf::-webkit-details-marker {\n display: none;\n}\n\n.szenen-node-karte__titel {\n flex: 1 1 12rem;\n min-width: 0;\n color: var(--color-text-primary);\n font-weight: 600;\n overflow-wrap: anywhere;\n}\n\n.szenen-node-karte__typ {\n flex: none;\n padding: 0.15rem 0.55rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-full);\n font-size: 0.72rem;\n white-space: nowrap;\n}\n\n/* node_id klein, monospace, sekund\xE4r \u2014 sichtbar auch bei zugeklappter Karte,\n * aber optisch klar hinter Beschreibung und Typ. */\n.szenen-node-karte__meta {\n flex: 1 1 100%;\n display: flex;\n flex-wrap: wrap;\n gap: 0.25rem;\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n}\n\n.szenen-node-karte__meta code {\n font-size: inherit;\n}\n\n.szenen-node-karte__body,\n.szenen-relation-karte__body {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm) var(--space-sm) var(--space-sm);\n border-top: 1px solid var(--color-border);\n}\n\n/* \xA74 \u2014 Prim\xE4raktion und destruktive Aktion optisch getrennt: \u201ENode/Beziehung\n * entfernen" bekommt einen eigenen, warnenden Ton statt gleichgewichtig neben\n * der normalen Bearbeitung zu stehen. */\n.szenen-node-karte__entfernen,\n.szenen-relation-karte__entfernen {\n justify-self: start;\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-color: var(--color-accent-red);\n}\n\n.szenen-relation-karte__kopf {\n font-size: 0.9rem;\n}\n\n.szenen-relation-karte__kopf code {\n overflow-wrap: anywhere;\n}\n\n.szenen-relation-karte__pfeil {\n flex: none;\n color: var(--color-text-secondary);\n}\n\n/* Die Composer-\xD6ffner (\u201E+ Node hinzuf\xFCgen", \u201E+ Beziehung hinzuf\xFCgen") sind\n * Angebote, keine Ausk\xFCnfte \u2014 dasselbe gestrichelte Muster wie beim Folgelauf\n * und der Fehlerrettung (\xA73.3/\xA73.5). */\n.szenen-scene-editor__oeffnen {\n width: 100%;\n border-style: dashed;\n}\n\n.szenen-refpicker {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n margin: var(--space-sm) 0;\n padding: var(--space-sm) 0;\n border: none;\n border-radius: 0;\n}\n\n.szenen-refpicker legend {\n padding: 0 var(--space-xs);\n color: var(--color-text-secondary);\n font-weight: 600;\n}\n\n.szenen-capability > ul > li {\n display: grid;\n grid-template-columns: minmax(10rem, 1fr) auto minmax(14rem, 2fr);\n align-items: start;\n gap: var(--space-sm);\n}\n\n.szenen-capability__state {\n padding: 0.1rem 0.5rem;\n border-radius: var(--radius-full);\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n}\n\n.szenen-capability__state--yes {\n color: #bbf7d0;\n background: var(--color-accent-green-dim);\n}\n\n.szenen-capability__state--no {\n color: #fecaca;\n background: var(--color-accent-red-dim);\n}\n\n.szenen-capability__state--unknown {\n color: #fef08a;\n background: rgba(234, 179, 8, 0.2);\n}\n\n.szenen-runs > ol > li {\n display: grid;\n gap: var(--space-sm);\n}\n\n/* B1 \u2014 die geschlossene Runde. Vorher stand die `run_id` als erstes und war\n die einzige Information, die niemand liest; jetzt beantwortet die Kopfzeile\n welche Runde, ob sie durchging, mit welchem Modell und was ungef\xE4hr\n herauskam. */\n.szenen-runs__kopf {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-sm);\n width: 100%;\n padding: var(--space-xs) 0;\n text-align: left;\n color: var(--color-text-secondary);\n background: transparent;\n border: none;\n cursor: pointer;\n}\n\n.szenen-runs__marke {\n font-weight: 700;\n color: var(--color-text-primary);\n}\n\n.szenen-runs__status {\n padding: 0.05rem 0.4rem;\n font-size: 0.6875rem;\n font-weight: 600;\n text-transform: uppercase;\n border-radius: var(--radius-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n}\n\n.szenen-runs__status[data-status="succeeded"] {\n border-color: var(--color-success);\n}\n\n.szenen-runs__status[data-status="failed"] {\n border-color: var(--color-danger);\n}\n\n.szenen-runs__modell {\n font-size: 0.75rem;\n}\n\n/* Der Auszug ist die Inhaltsvorschau der Runde. Er bekommt eine eigene Zeile,\n damit er nicht mit Marke und Modell um dieselbe Zeile ringt. */\n.szenen-runs__auszug {\n flex: 1 1 100%;\n min-width: 0;\n color: var(--color-text-tertiary);\n font-size: 0.8125rem;\n}\n\n/* B2 \u2014 ein Fehler steht ausserhalb der aufklappbaren Fl\xE4che: Wer eine rote\n Runde sieht, soll nicht erst klicken m\xFCssen, um zu erfahren, woran sie\n gescheitert ist. */\n.szenen-runs__fehler {\n display: grid;\n gap: var(--space-xs);\n padding: var(--space-sm);\n border-radius: var(--radius-sm);\n}\n\n.szenen-runs__fehler p {\n margin: 0;\n}\n\n.szenen-runs__inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n}\n\n.szenen-runs__prompt > h5,\n.szenen-runs__sekundaer > h5 {\n margin: 0 0 var(--space-xs);\n}\n\n.szenen-runs__prompt > p {\n margin: 0;\n white-space: pre-wrap;\n}\n\n.szenen-runs__prompt--negativ > p,\n.szenen-runs__sekundaer > p {\n margin: 0;\n color: var(--color-text-secondary);\n white-space: pre-wrap;\n}\n\n.szenen-runs__mehr {\n justify-self: start;\n margin-top: var(--space-xs);\n padding: 0;\n color: var(--color-accent-teal, var(--color-text-secondary));\n background: transparent;\n border: none;\n font-size: 0.75rem;\n text-decoration: underline;\n cursor: pointer;\n}\n\n.szenen-runs > ol > li[data-status="succeeded"] {\n border-left: 3px solid var(--color-success);\n}\n\n.szenen-runs > ol > li[data-status="failed"] {\n border-left: 3px solid var(--color-danger);\n}\n\n.szenen-runs details pre {\n max-height: 20rem;\n overflow: auto;\n padding: var(--space-sm);\n white-space: pre-wrap;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border-radius: var(--radius-sm);\n}\n\n.szenen-media > section {\n display: grid;\n gap: var(--space-sm);\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-media__lookup,\n.szenen-media__attach {\n display: flex;\n flex-wrap: wrap;\n align-items: end;\n gap: var(--space-sm);\n}\n\n.szenen-media__lookup > label,\n.szenen-media__attach > label:first-child {\n flex: 1 1 18rem;\n}\n\n.szenen-media__list {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-media__list > li {\n display: grid;\n grid-template-columns: minmax(12rem, 1fr) auto auto auto;\n align-items: center;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n@media (max-width: 760px) {\n .szenen-view {\n gap: var(--space-sm);\n }\n\n /* C1 \u2014 auf 390 px bleiben drei Kacheln je Reihe gross genug zum Erkennen\n und klein genug, dass vier Bilder eine Reihe sind und keine Seite. */\n .szenen-bildgalerie {\n grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));\n }\n\n /* Der Viewer bekommt den ganzen Bildschirm; die Bl\xE4tterkn\xF6pfe bleiben am\n Rand, wo der Daumen sie erreicht. */\n .szenen-bildviewer {\n padding: var(--space-xs);\n }\n\n .szenen-bildviewer__blaettern {\n width: 2.25rem;\n height: 4rem;\n }\n\n .szenen-view__intro {\n align-items: start;\n flex-direction: column;\n gap: var(--space-xs);\n padding: var(--space-sm);\n }\n\n .szenen-view__intro h2 {\n font-size: 1.05rem;\n }\n\n .szenen-view__intro > div > p:last-child {\n font-size: 0.78rem;\n }\n\n .szenen-view__nav {\n position: sticky;\n top: 0;\n z-index: 2;\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: var(--space-xs);\n padding: var(--space-xs);\n }\n\n .szenen-view__nav button {\n width: 100%;\n min-width: 0;\n min-height: 2.5rem;\n padding: 0.35rem 0.5rem;\n }\n\n\n .szenen-scene-editor {\n grid-template-columns: 1fr;\n }\n\n .szenen-capability > ul > li {\n grid-template-columns: 1fr;\n }\n\n .szenen-derive,\n .szenen-elements,\n .szenen-scene,\n .szenen-setup,\n .szenen-shot,\n .szenen-media,\n .szenen-runs {\n gap: var(--space-sm);\n }\n\n .szenen-elements__filter {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n align-items: center;\n gap: var(--space-sm);\n }\n\n .szenen-elements__filter > label {\n width: 100%;\n min-width: 0;\n flex: none;\n }\n\n .szenen-elements__filter > label:has(input[type="checkbox"]) {\n flex: none;\n grid-column: 1 / -1;\n }\n\n /* Schmal ist die Liste eine Spalte, und die Auswahl fuehrt weiter \u2014 der\n * Eintrag ist deshalb ein Ziel fuer den Finger, keine Kachel. */\n .szenen-elements__list {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .szenen-elements__list > li > button {\n min-height: 2.75rem;\n }\n\n .szenen-elements__back {\n justify-self: stretch;\n min-height: 2.5rem;\n }\n\n .szenen-detail__actions,\n .szenen-runs__start,\n .szenen-scene-editor__add,\n .szenen-scene-editor__add-relation {\n align-items: stretch;\n flex-direction: column;\n }\n\n .szenen-runs__start > *,\n .szenen-scene-editor__add > *,\n .szenen-scene-editor__add-relation > * {\n width: 100%;\n flex: none;\n }\n}\n\n/* \u2500\u2500 Stufe 6.5: Rekonstruktion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Bereich zeigt drei Dinge nebeneinander: den Vorschlag, das nicht\n * Ableitbare und die Widerspr\xFCche. Die Diagnosen sind deshalb bewusst nicht\n * kleiner gesetzt als der Vorschlag \u2014 wer sie \xFCbersieht, \xFCbernimmt Geratenes.\n */\n\n.szenen-derive {\n display: grid;\n gap: var(--space-md);\n}\n\n.szenen-form {\n display: grid;\n gap: var(--space-sm);\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-field {\n display: grid;\n gap: var(--space-xs);\n}\n\n.szenen-field > span {\n color: var(--color-text-secondary);\n font-size: 0.875rem;\n}\n\n.szenen-error {\n padding: var(--space-sm) var(--space-md);\n color: var(--color-danger);\n background: var(--color-accent-red-dim);\n border: 1px solid var(--color-accent-red);\n border-radius: var(--radius-sm);\n}\n\n.szenen-empty {\n padding: var(--space-md);\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-diagnose {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-warning);\n border-radius: var(--radius-sm);\n}\n\n/* Ein Widerspruch ist der schwerste der drei Befunde: dort steht eine\n * Entscheidung aus, die das System bewusst nicht getroffen hat (D-74). */\n.szenen-diagnose--konflikt {\n border-left-color: var(--color-danger);\n}\n\n.szenen-diagnose > h4 {\n margin: 0 0 var(--space-sm);\n font-size: 0.9375rem;\n}\n\n.szenen-diagnose ul {\n display: grid;\n gap: var(--space-xs);\n margin: 0;\n padding-left: var(--space-md);\n}\n\n.szenen-proposal {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-proposal > h4 {\n margin: 0 0 var(--space-sm);\n font-family: var(--font-mono);\n font-size: 0.9375rem;\n}\n\n.szenen-proposal__payload {\n max-height: 18rem;\n margin: 0 0 var(--space-sm);\n padding: var(--space-sm);\n overflow: auto;\n /* Ein erzeugter Prompt ist ein einziger langer Satz \u2014 ohne Umbruch\n verschwindet er nach rechts aus dem Bild. JSON-Payloads bringen ihre\n eigenen Zeilenumbrueche mit und leiden nicht darunter. */\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n font-family: var(--font-mono);\n font-size: 0.8125rem;\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-proposal__done {\n margin: var(--space-sm) 0 0;\n color: var(--color-success);\n}\n\n/* Die Belege eines Laufs: Rohantwort und Systemprompt. Zum Lesen und Kopieren,\n nicht zum Bedienen \u2014 deshalb voll markierbar und in Monospace. */\n.szenen-beleg__text {\n width: 100%;\n margin: var(--space-sm) 0 0;\n padding: var(--space-sm);\n font-family: var(--font-mono);\n font-size: 0.8125rem;\n line-height: 1.5;\n white-space: pre;\n background: var(--color-bg-app);\n color: var(--color-text);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n resize: vertical;\n}\n\n/* Der Folgelauf (D-87): abgesetzt, damit er nicht wie Teil des Ergebnisses\n aussieht \u2014 er ist eine Frage, keine Auskunft. */\n.szenen-folgelauf {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-folgelauf > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-run__herkunft {\n margin: 0 0 var(--space-sm);\n font-size: 0.875rem;\n color: var(--color-text-muted, inherit);\n}\n\n/* Widerspricht das Modell, ist das das Wichtigste auf der Seite. */\n.szenen-diagnose--antwort {\n border-left: 3px solid var(--color-accent, var(--color-border));\n}\n\n/* Der Bildw\xE4hler (D-92) \u2014 Browser und Inspector, dem Labs-Tab nachgebildet,\n aber mit dem CSS dieser Ansicht: eine gemeinsame Komponente \xFCber zwei\n Dom\xE4nengrenzen w\xE4re eine Kopplung, die keiner gewollt hat.\n\n Der Review vom 2026-08-22 (R12.1/R12.2) hat den Ma\xDFstab gesetzt: Der Bestand\n ist ein **Browser**, kein Seiteninhalt. Vierzig Werke d\xFCrfen die Szenenseite\n nicht meterlang machen, und zw\xF6lf Varianten eines Werkes keine zw\xF6lf\n Bildschirmseiten. Deshalb steht hier alles unter zwei Regeln \u2014 begrenzte\n Fl\xE4che mit eigenem Scroll, und Kacheln statt Karten. */\n\n/* \u2500\u2500 Ebene 1: der Browser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl {\n /* Dieselbe Begr\xFCndung wie beim Inspector: Als Grid-Element w\xFCrde der\n W\xE4hler sonst so breit wie sein breitester Inhalt. */\n min-width: 0;\n}\n\n.szenen-bildwahl__browser {\n /* Die eigene Scrollfl\xE4che ist der ganze Punkt: Der Bestand w\xE4chst nach\n innen, nicht nach unten. Dieselbe Gr\xF6\xDFenordnung wie `max-h-[300px]` im\n Labs-Browser. */\n max-height: 19rem;\n overflow-y: auto;\n overscroll-behavior: contain;\n margin-top: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__raster {\n display: grid;\n /* Vier Spalten auf schmalen Displays \u2014 der gemeldete Screenshot zeigte eine\n einzige Spalte gro\xDFer Karten. Breiter wird automatisch dichter. */\n grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildwahl__karte {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n overflow: hidden;\n background: var(--color-bg-surface);\n text-align: left;\n cursor: pointer;\n}\n\n/* Das fokussierte Werk ist klar markiert \u2014 sonst wei\xDF niemand, wessen Galerie\n der Inspector unten zeigt. */\n.szenen-bildwahl__karte.is-fokus {\n border-color: var(--color-accent, var(--color-text-primary));\n box-shadow: 0 0 0 2px var(--color-accent, var(--color-text-primary));\n}\n\n/* \u2500\u2500 Die Kachel selbst \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Der Platz steht, bevor das Bild da ist \u2014 sonst springt das Raster, sobald\n die erste Vorschau eintrifft. */\n.szenen-bildkachel {\n display: block;\n width: 100%;\n aspect-ratio: 1;\n overflow: hidden;\n background: var(--color-bg-app);\n}\n\n.szenen-bildkachel img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n\n.szenen-bildkachel__leer {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n background: linear-gradient(135deg, var(--color-bg-app), var(--color-bg-surface));\n}\n\n/* R12.3: Eine fehlende Vorschau bleibt kachelgro\xDF. Vorher fiel die Karte auf\n ihren Prompttext zur\xFCck \u2014 und ein Prompt hat im echten Bestand mehrere\n hundert Zeichen. */\n.szenen-bildkachel__zeichen {\n font-size: 0.875rem;\n font-weight: 700;\n color: var(--color-text-secondary);\n opacity: 0.6;\n}\n\n/* Wie viele Varianten das Werk hat \u2014 klein und \xFCberlagernd, nie als\n dominanter Text. */\n.szenen-bildwahl__anzahl {\n position: absolute;\n top: 0.15rem;\n right: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n font-weight: 700;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* Eine Zeile, dann Schluss. Der Prompt eines Werkes ist oft mehrere hundert\n Zeichen lang und w\xE4re als Prim\xE4rdarstellung genau die Textw\xFCste, gegen die\n der Review antritt. */\n.szenen-bildwahl__kacheltitel {\n display: block;\n padding: 0.15rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.3;\n color: var(--color-text-secondary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 Ebene 2: der Inspector \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl__inspector {\n /* Ohne `min-width: 0` gewinnt der Inhalt: Der W\xE4hler ist Element eines\n Grids (`.szenen-form`), und dort ist `min-width: auto` die Vorgabe. Der\n Streifen unten ist rund 1000 px breit \u2014 gemessen am 2026-08-22 im echten\n Browser zog er die Auswahl von 374 px auf 1088 px auf und schob die\n ganze Seite waagerecht heraus, statt selbst zu scrollen. */\n min-width: 0;\n margin-top: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n /* Sichtbar abgesetzt vom Browser: zwei Ebenen, nicht eine lange Liste. */\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-accent, var(--color-border));\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__inspektorkopf {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-sm);\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bildwahl__werktitel {\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--color-text-primary);\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.szenen-bildwahl__zurueck {\n flex: 0 0 auto;\n font-size: 0.6875rem;\n padding: 0.15rem 0.5rem;\n}\n\n/* Der Streifen: waagerecht scrollend, feste kleine Kacheln \u2014 zw\xF6lf Varianten\n bleiben ein \xDCberblick statt zw\xF6lf Bildschirmseiten. */\n.szenen-bildwahl__streifen {\n /* Die Breite kommt vom Elter, nie vom Inhalt \u2014 sonst scrollt hier nichts. */\n max-width: 100%;\n display: flex;\n gap: var(--space-sm);\n margin: 0;\n padding: 0 0 0.25rem;\n list-style: none;\n overflow-x: auto;\n overscroll-behavior-x: contain;\n}\n\n.szenen-bildwahl__streifen > li {\n flex: 0 0 auto;\n width: 5rem;\n}\n\n.szenen-bildwahl__variante {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n overflow: hidden;\n background: var(--color-bg-app);\n cursor: pointer;\n}\n\n.szenen-bildwahl__variante.is-gewaehlt {\n border-color: var(--color-accent, var(--color-text-primary));\n box-shadow: 0 0 0 2px var(--color-accent, var(--color-text-primary));\n}\n\n.szenen-bildwahl__nummer {\n position: absolute;\n bottom: 0.15rem;\n left: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border-radius: var(--radius-sm);\n}\n\n/* \u2500\u2500 Das Gew\xE4hlte \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl__gewaehlt {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: var(--space-sm) 0 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildwahl__gewaehlt li {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.25rem 0.5rem;\n font-size: 0.8125rem;\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n max-width: 100%;\n}\n\n/* Das gew\xE4hlte Bild wird best\xE4tigt, nicht nur benannt: eine kleine Vorschau\n neben dem Titel. */\n.szenen-bildwahl__gewaehlt .szenen-bildkachel {\n width: 2.25rem;\n flex: 0 0 auto;\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__gewaehlt .szenen-bildwahl__kacheltitel {\n min-width: 0;\n max-width: 12rem;\n font-size: 0.75rem;\n}\n\n.szenen-beleg__kopf {\n display: flex;\n align-items: center;\n gap: var(--space-sm);\n margin-top: var(--space-sm);\n}\n\n.szenen-diagnose > details + details {\n margin-top: var(--space-sm);\n}\n\n.szenen-diagnose > details > summary {\n cursor: pointer;\n font-weight: 600;\n}\n\n/* \u2500\u2500 Die Fehlerrettung (Stufe 6.6, A5) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Sie steht im Fehlerblock und muss trotzdem als Werkzeug lesbar sein, nicht\n * als weitere Fehlermeldung. Deshalb abgesetzt wie der Folgelauf \u2014 und aus\n * demselben Grund: Beides sind Angebote, keine Ausk\xFCnfte.\n *\n * Der wichtigste optische Punkt ist die Trennung von Original und\n * Arbeitskopie. Das Original bekommt den ruhigen, \u201Efesten" Hintergrund der\n * Belege; die Kopie sieht aus wie ein normales Eingabefeld, weil sie eines\n * ist. */\n.szenen-rettung {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-rettung > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-rettung textarea[readonly] {\n background: var(--color-bg-surface);\n color: var(--color-text-secondary);\n font-family: var(--font-mono, monospace);\n font-size: 0.8125rem;\n}\n\n.szenen-rettung textarea:not([readonly]) {\n font-family: var(--font-mono, monospace);\n font-size: 0.8125rem;\n}\n\n.szenen-rettung__aktionen {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: var(--space-sm) 0;\n}\n\n/* Ein Befund tr\xE4gt Code, Bezug und Meldung. Der Code steht klein und\n * monospaced davor: Er ist die Kennung, nicht die Aussage. */\n.szenen-diagnose__liste {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding-left: var(--space-md);\n}\n\n.szenen-diagnose__liste code {\n font-size: 0.8125rem;\n}\n\n.szenen-diagnose__liste > li[data-severity=\'warning\'] {\n color: var(--color-text-secondary);\n}\n\n/* \u2500\u2500 Die Draft-Kette (Stufe 6.6, C4) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Hauptweg. Er steht nach den Einzel\xFCbernahmen und muss als das erkennbar\n * sein, was er ist: nicht eine weitere Option, sondern der k\xFCrzere Weg zum\n * selben Ziel. Deshalb durchgezogener Rahmen statt gestricheltem \u2014 er ist\n * Angebot **und** Empfehlung. */\n.szenen-kette {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-accent, var(--color-border));\n border-radius: var(--radius-sm);\n}\n\n.szenen-kette > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-kette ul {\n display: grid;\n gap: var(--space-xs);\n margin: 0 0 var(--space-sm);\n padding-left: var(--space-md);\n}\n\n.szenen-kette code {\n font-size: 0.8125rem;\n}\n\n/* Ein Hinweis, der eine Bedingung erklaert \u2014 nicht rot, weil er kein Fehler\n ist, aber abgesetzt, weil er eine Folge hat. */\n.szenen-detail__hint {\n margin: var(--space-sm) 0 0;\n padding: var(--space-sm);\n font-size: 0.875rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border-left: 3px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* \u2500\u2500 Stufe 6.7: Bilder am Prompt-Lauf (H0/H1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Bereich tr\xE4gt zwei Dinge, die man nie verwechseln darf: den **Eingang**\n * (woraus der Prompt entstand) und den **Ausgang** (was daraus wurde). Sie\n * bekommen deshalb verschiedene Akzentkanten statt nur verschiedener\n * \xDCberschriften \u2014 wer schnell liest, sieht die Trennung, bevor er sie liest.\n */\n.szenen-bilder {\n display: grid;\n gap: var(--space-md);\n margin-top: var(--space-sm);\n}\n\n.szenen-bilder__eingang,\n.szenen-bilder__ausgang {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__eingang {\n border-left: 3px solid var(--color-accent-purple, var(--color-border));\n}\n\n.szenen-bilder__ausgang {\n border-left: 3px solid var(--color-accent-teal, var(--color-border));\n}\n\n.szenen-bilder h5 {\n margin: 0;\n}\n\n.szenen-bilder__ids {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: 0;\n align-items: baseline;\n color: var(--color-text-secondary);\n font-size: 0.875rem;\n}\n\n.szenen-bilder__eingangsliste,\n.szenen-bilder__batchliste,\n.szenen-bilder__auftraege {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bilder__eingangsliste > li,\n.szenen-bilder__batchliste > li,\n.szenen-bilder__auftraege > li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n font-size: 0.8125rem;\n}\n\n/* Ein unterbrochener Auftrag ist der Fall, f\xFCr den der Auftragssatz existiert.\n Er wird deshalb markiert \u2014 nicht rot, denn er ist kein Fehler, sondern eine\n offene Entscheidung. */\n.szenen-bilder__auftraege > li[data-status="started"] {\n border-left: 3px solid var(--color-warning);\n}\n\n.szenen-bilder__auftraege > li[data-status="succeeded"] {\n border-left: 3px solid var(--color-success);\n}\n\n.szenen-bilder__auftraege > li[data-status="partial"] {\n border-left: 3px solid var(--color-accent-blue, var(--color-border));\n}\n\n.szenen-bilder__auftraege > li[data-status="failed"] {\n border-left: 3px solid var(--color-danger);\n}\n\n/* Das Eingangsbild benutzt seit C3 dieselbe Kachel wie W\xE4hler und Galerie.\n Sie ist von Haus aus so breit wie ihr Platz \u2014 in dieser Zeile bekommt sie\n deshalb eine feste Gr\xF6sse, sonst sch\xF6be sie die Metadaten heraus. */\n.szenen-bilder__eingangsliste .szenen-bildkachel {\n flex: 0 0 auto;\n width: 4rem;\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__batch {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__batchkopf {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--space-sm);\n}\n\n.szenen-bilder__herkunft {\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n}\n\n/* Review-Befund \u2014 ein transienter Bildfehler ist wiederholbar, kein\n endg\xFCltiges \u201Efehlt". Dezent wie ein Angebot, nicht wie ein Fehlerzustand. */\n.szenen-bilder__retry {\n justify-self: start;\n padding: 0.15rem 0.5rem;\n font-size: 0.75rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n/* C4 \u2014 die Provenienz vollst\xE4ndig, aber sekund\xE4r. */\n.szenen-bilder__technik > summary {\n color: var(--color-text-secondary);\n font-size: 0.75rem;\n cursor: pointer;\n}\n\n.szenen-bilder__meta {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n font-size: 0.75rem;\n color: var(--color-text-secondary);\n}\n\n/* C5 \u2014 der Verlauf verdichtet, seine Ausreisser nicht. */\n.szenen-bilder__verlauf > summary {\n color: var(--color-text-secondary);\n font-size: 0.8125rem;\n cursor: pointer;\n}\n\n.szenen-bilder__verlauf[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bilder__auffaellig {\n margin: 0;\n padding: var(--space-sm);\n border-radius: var(--radius-sm);\n font-size: 0.8125rem;\n}\n\n.szenen-bilder__auftragskennung {\n flex-basis: 100%;\n color: var(--color-text-tertiary);\n font-size: 0.6875rem;\n}\n\n/* \u2500\u2500 C1/C2 \u2014 Galerie und grosse Ansicht \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Das Raster w\xE4chst mit dem Platz: auf 390 px drei bis vier Kacheln je Reihe,\n auf dem Desktop entsprechend mehr. Vorher war jedes Bild eine Zeile aus\n Daumennagel plus vier Metadatenfeldern \u2014 vier Bilder ergaben vier Textzeilen\n und das Protokoll war die Hauptsache. */\n.szenen-bildgalerie {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr));\n gap: var(--space-sm);\n min-width: 0;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildgalerie__kachel {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-bildgalerie__kachel:hover,\n.szenen-bildgalerie__kachel:focus-visible {\n border-color: var(--color-accent-teal, var(--color-text-primary));\n}\n\n.szenen-bildgalerie__nummer {\n position: absolute;\n bottom: 0.15rem;\n left: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border-radius: var(--radius-sm);\n}\n\n/* Die grosse Ansicht liegt \xFCber allem und bringt ihren eigenen Grund mit \u2014\n `position: fixed` bezieht sich im Flow-Canvas auf dessen Rahmen, in React auf\n das Fenster; in beiden F\xE4llen genau das Gew\xFCnschte. */\n.szenen-bildviewer {\n position: fixed;\n inset: 0;\n z-index: 1000;\n display: grid;\n grid-template-rows: auto 1fr auto;\n gap: var(--space-sm);\n padding: var(--space-sm);\n /* Deckend, nicht get\xF6nt: Bei 0.92 schimmerte der Laufbereich durch und man\n las Prompttext quer \xFCber dem Bild \u2014 im Browser gemessen. */\n background: #000;\n}\n\n/* Titel, Z\xE4hlung, Schliessen \u2014 drei Spalten, damit ein langer Werktitel die\n Z\xE4hlung nicht in die n\xE4chste Zeile und \xFCber das Bild schiebt. */\n.szenen-bildviewer__kopf {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto auto;\n align-items: center;\n gap: var(--space-sm);\n color: var(--color-text-primary);\n}\n\n.szenen-bildviewer__titel {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-weight: 600;\n}\n\n.szenen-bildviewer__zaehler {\n color: var(--color-text-secondary);\n font-size: 0.8125rem;\n}\n\n.szenen-bildviewer__buehne {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--space-sm);\n min-height: 0;\n min-width: 0;\n}\n\n/* Der Rahmen nimmt den Platz, der zwischen den Bl\xE4tterkn\xF6pfen \xFCbrig ist \u2014\n `min-width: 0`, damit er auch wirklich schrumpft statt sie hinauszuschieben. */\n.szenen-bildviewer__rahmen {\n flex: 1 1 auto;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n min-height: 0;\n height: 100%;\n}\n\n.szenen-bildviewer__bild {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n}\n\n.szenen-bildviewer__leer {\n color: var(--color-text-secondary);\n text-align: center;\n}\n\n/* Die Bl\xE4tterkn\xF6pfe sind daumengross und liegen an den R\xE4ndern \u2014 auf einem\n Telefon ist der Rand erreichbar, die Bildmitte nicht. */\n.szenen-bildviewer__blaettern {\n flex: 0 0 auto;\n width: 2.75rem;\n height: 3.5rem;\n font-size: 1.75rem;\n line-height: 1;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-bildviewer__details {\n max-height: 8rem;\n overflow-y: auto;\n color: var(--color-text-secondary);\n}\n\n/* Die Aktionen stehen abgesetzt: Sie sind Angebote, keine Ausk\xFCnfte \u2014 dasselbe\n Muster wie beim Folgelauf und bei der Fehlerrettung. */\n/* D \u2014 die Aktionen stehen abgesetzt und standardm\xE4ssig kompakt: Sie sind\n Angebote, keine Ausk\xFCnfte. Vorher hielten beide Formulare dauerhaft offen und\n machten aus jedem Lauf zwei zus\xE4tzliche Bildschirmseiten. */\n.szenen-bilder__aktion {\n min-width: 0;\n padding: var(--space-sm) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__aktion > summary {\n color: var(--color-text-primary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-bilder__aktion[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bilder__aktion-inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding-bottom: var(--space-sm);\n}\n\n.szenen-bilder__aktion h5 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-bilder__aktion ol {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding-left: var(--space-md);\n font-size: 0.8125rem;\n}\n\n/* Regelwerk-UX Schritt 16 \u2014 der ZIP-Sammelimport.\n *\n * \xA78: Der gemeldete Einsatz ist mobil. Deshalb **ein** Seedfeld statt einer\n * gestapelten Eingabe je Bild, und die Zuordnung Bild -> Seed als umbrechende\n * Zeile statt als Tabelle: Eine Tabelle waere auf 390 px abgeschnitten, und\n * abgeschnitten heisst hier, dass der Nutzer die Zuordnung nicht mehr pruefen\n * kann \u2014 genau die Pruefung, wegen der die Vorschau existiert. */\n.szenen-bilder__zip {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n}\n\n.szenen-bilder__zipkopf {\n margin: 0;\n overflow-wrap: anywhere;\n}\n\n.szenen-bilder__zipvorlagen {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n margin: 0;\n}\n\n/* Eine laengere Liste bleibt scrollbar, statt die Sektion und damit den\n * Import-Knopf aus dem Blick zu schieben (\xA78, letzter Punkt). */\n.szenen-bilder__zipliste {\n max-height: 16rem;\n overflow-y: auto;\n}\n\n.szenen-bilder__zipliste > li {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-between;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-bilder__zipliste > li > span:first-child {\n overflow-wrap: anywhere;\n}\n\n.szenen-bilder__zipliste > li > span:last-child {\n color: var(--color-text-secondary);\n white-space: nowrap;\n}\n\n/* ==========================================================================\n STUFE 6.10 \u2014 ELEMENTEDITOR UND VERSIONSNAVIGATION\n ========================================================================== */\n\n/* \xA74.2 \u2014 vier Sachverhalte, vier Marken. Die Marken stehen in einer eigenen\n * Spalte, damit ANGEZEIGT + BEVORZUGT nebeneinander lesbar bleiben, statt sich\n * durch den Text zu schieben. */\n.szenen-detail__heads li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-sm);\n}\n\n.szenen-version__titel {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n min-width: 0;\n margin-right: auto;\n}\n\n/* V17 \u2014 Kennungen bleiben vollst\xE4ndig sichtbar, aber optisch sekund\xE4r. */\n.szenen-version__id,\n.szenen-ref__id,\n.szenen-detail__field-key {\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n overflow-wrap: anywhere;\n}\n\n.szenen-version__marken {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n}\n\n.szenen-version__marken > span {\n padding: 0.1rem 0.5rem;\n color: var(--color-text-primary);\n background: rgba(45, 212, 191, 0.12);\n border: 1px solid rgba(45, 212, 191, 0.35);\n border-radius: var(--radius-full);\n font-size: 0.7rem;\n font-weight: 800;\n letter-spacing: 0.08em;\n}\n\n.szenen-version--angezeigt {\n border-color: var(--color-accent-teal, #2dd4bf) !important;\n}\n\n/* V9 \u2014 der Basiswechsel ist eine Entscheidung, keine Nebenwirkung. */\n.szenen-detail__confirm {\n display: grid;\n gap: var(--space-sm);\n margin: var(--space-md) 0;\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-accent-amber, #f59e0b);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__confirm p {\n margin: 0;\n}\n\n/* Untergruppen von Objektfeldern (`prompt_render_settings`, `overrides`,\n `blender_render_settings`) \u2014 seit A1/A2 auf- und zuklappbar. Eine optionale\n Gruppe startet zu: Blender und `overrides` f\xFCllten auf einem 390-px-Display\n sonst mehrere Bildschirmseiten, obwohl die meisten Shots sie nie anfassen. */\n.szenen-detail__group {\n min-width: 0;\n margin: var(--space-md) 0;\n padding: var(--space-sm) var(--space-md);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__group > summary {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-detail__group[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n/* A2 \u2014 dass eine zugeklappte Gruppe schon Werte tr\xE4gt, muss man sehen, ohne\n sie zu \xF6ffnen. Sonst \xFCbersieht man eine gesetzte Blender-Ausgabe genau so\n lange, bis sie im Ergebnis auftaucht. */\n.szenen-detail__group-status {\n padding: 0.05rem 0.4rem;\n font-size: 0.6875rem;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--color-text-tertiary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__group[data-konfiguriert="ja"] > summary .szenen-detail__group-status {\n color: var(--color-text-primary);\n border-color: var(--color-accent-teal, var(--color-border));\n}\n\n.szenen-detail__group-inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding-bottom: var(--space-sm);\n}\n\n/* A3 \u2014 die Kernaussage steht am Feld, die Begr\xFCndung dahinter. Nicht als\n Tooltip: Auf einem Telefon gibt es kein Hover, und was dort unerreichbar ist,\n ist gestrichen \u2014 nur ohne dass es jemand entschieden h\xE4tte. */\n.szenen-detail__warum > summary {\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n cursor: pointer;\n}\n\n.szenen-detail__warum > small {\n display: block;\n padding-top: var(--space-xs);\n color: var(--color-text-tertiary);\n}\n\n.szenen-detail__fieldrow {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n margin-bottom: var(--space-md);\n}\n\n/* A4 \u2014 der technische Feldname bleibt lesbar, tritt aber hinter das fachliche\n Label zur\xFCck. */\n.szenen-detail__field-label {\n color: var(--color-text-primary);\n}\n\n.szenen-detail__field {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-detail__field > span {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n}\n\n/* Mehrfachauswahl und Listeneditoren. */\n.szenen-feld__mehrfach,\n.szenen-feld__liste,\n.szenen-feld__reflist,\n.szenen-feld__ref,\n.szenen-feld__zielmodell,\n.szenen-feld__kamera {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-feld__mehrfach > label,\n.szenen-feld__null {\n display: flex;\n align-items: center;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-size: 0.85rem;\n}\n\n.szenen-feld__liste-zeile {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n}\n\n.szenen-feld__reflist ol {\n display: grid;\n gap: var(--space-xs);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-feld__reflist li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-xs);\n padding: var(--space-xs) var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* \xA76.1 \u2014 Titel und Typ zuerst, Kennungen daneben. */\n.szenen-ref__anzeige {\n display: inline-flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-ref__typ {\n color: var(--color-text-secondary);\n font-size: 0.8rem;\n}\n\n.szenen-ref__warn {\n padding: 0.1rem 0.5rem;\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-radius: var(--radius-full);\n font-size: 0.72rem;\n font-weight: 700;\n}\n\n/* \xA76.5 \u2014 abgeleitete Angaben, ausdr\xFCcklich nur Anzeige. */\n.szenen-detail__derived {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__derived dl {\n display: grid;\n grid-template-columns: minmax(10rem, auto) 1fr;\n gap: var(--space-xs) var(--space-sm);\n margin: 0;\n}\n\n.szenen-detail__derived dt {\n color: var(--color-text-secondary);\n}\n\n.szenen-detail__derived dd {\n margin: 0;\n}\n\n.szenen-feld__abgeleitet {\n margin: 0;\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n}\n';
|
|
75
64
|
|
|
76
65
|
// src/lib/utils.ts
|
|
77
66
|
var getFormattedTimestamp = () => {
|
|
@@ -88,11 +77,6 @@ var GLOBAL_STYLES = `
|
|
|
88
77
|
.prompt-loading { background: linear-gradient(90deg, rgba(255,255,255,0.02) 25%, rgba(255,255,255,0.05) 50%, rgba(255,255,255,0.02) 75%); background-size: 200% 100%; animation: prompt-shimmer 2s infinite linear; }
|
|
89
78
|
@keyframes dropdown-enter { from { opacity: 0; transform: scale(0.95) translateY(-5px); } to { opacity: 1; transform: scale(1) translateY(0); } }
|
|
90
79
|
.animate-dropdown { animation: dropdown-enter 0.1s ease-out forwards; }
|
|
91
|
-
.flow-szenen-entry { display: grid; gap: 6px; width: 100%; max-width: 280px; text-align: center; }
|
|
92
|
-
.flow-szenen-entry button, .flow-szenen-shell__header button { min-height: 44px; padding: 0 16px; color: #e7e5e4; background: #1d4ed8; border: 1px solid #3b82f6; border-radius: 12px; font: inherit; font-weight: 700; cursor: pointer; }
|
|
93
|
-
.flow-szenen-entry button { display: flex; align-items: center; justify-content: center; gap: 8px; }
|
|
94
|
-
.flow-szenen-entry button:disabled { color: rgba(255,255,255,.3); background: transparent; border-color: rgba(255,255,255,.1); cursor: not-allowed; }
|
|
95
|
-
.flow-szenen-entry small { color: rgba(255,255,255,.35); font-size: 10px; }
|
|
96
80
|
.flow-workspace-picker { position: fixed; inset: 0; display: grid; place-items: center; padding: 16px; color: #f5f5f5; background: #050505; overflow: auto; }
|
|
97
81
|
.flow-workspace-picker__card { box-sizing: border-box; display: grid; gap: 16px; width: min(100%, 448px); padding: 24px; background: #171717; border: 1px solid #404040; border-radius: 12px; box-shadow: 0 25px 50px rgba(0,0,0,.55); }
|
|
98
82
|
.flow-workspace-picker__card header { display: grid; gap: 4px; }
|
|
@@ -103,7 +87,7 @@ var GLOBAL_STYLES = `
|
|
|
103
87
|
.flow-workspace-picker__choices button:hover { border-color: #3b82f6; background: #262626; }
|
|
104
88
|
.flow-workspace-picker__back { justify-self: start; padding: 0; color: #a3a3a3; background: transparent; border: 0; font: inherit; cursor: pointer; }
|
|
105
89
|
.flow-workspace-picker__warning { margin: 0; padding: 10px; color: #fbbf24; background: rgba(146,64,14,.2); border: 1px solid rgba(217,119,6,.4); border-radius: 8px; font-size: 12px; }
|
|
106
|
-
.flow-szenen-
|
|
90
|
+
.flow-szenen-pane {
|
|
107
91
|
--sz-bg: #0e0e0e; --sz-surface: #171717; --sz-elevated: #262626;
|
|
108
92
|
--sz-border: #404040; --sz-text: #f5f5f5; --sz-muted: #a3a3a3;
|
|
109
93
|
--color-bg-app: #000; --color-bg-surface: #0a0a0a; --color-bg-surface-hover: #171717; --color-bg-elevated: #171717;
|
|
@@ -117,17 +101,132 @@ var GLOBAL_STYLES = `
|
|
|
117
101
|
--radius-sm: .25rem; --radius-md: .5rem; --radius-lg: .75rem; --radius-full: 9999px;
|
|
118
102
|
--font-sans: system-ui, -apple-system, sans-serif;
|
|
119
103
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
120
|
-
|
|
121
|
-
color: var(--sz-text); background: var(--sz-bg); overflow:
|
|
104
|
+
flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 16px;
|
|
105
|
+
color: var(--sz-text); background: var(--sz-bg); overflow: auto;
|
|
122
106
|
}
|
|
123
|
-
.flow-szenen-
|
|
124
|
-
.flow-szenen-
|
|
125
|
-
.flow-szenen-shell__header div { display: grid; gap: 2px; }
|
|
126
|
-
.flow-szenen-shell__header small { color: var(--sz-muted); font-family: monospace; }
|
|
127
|
-
.flow-szenen-shell__content { flex: 1; min-height: 0; padding: 16px; overflow: auto; }
|
|
128
|
-
@media (max-width: 760px) { .flow-szenen-shell__content { padding: 8px; } }
|
|
107
|
+
.flow-szenen-pane--empty { align-items: center; justify-content: center; text-align: center; color: var(--sz-muted); font-size: 12px; }
|
|
108
|
+
@media (max-width: 760px) { .flow-szenen-pane { padding: 8px; } }
|
|
129
109
|
` + SZENEN_VIEW_STYLES;
|
|
130
110
|
|
|
111
|
+
// src/lib/metadata.ts
|
|
112
|
+
var crcTable = (() => {
|
|
113
|
+
let c;
|
|
114
|
+
const table = new Uint32Array(256);
|
|
115
|
+
for (let n = 0; n < 256; n++) {
|
|
116
|
+
c = n;
|
|
117
|
+
for (let k = 0; k < 8; k++) {
|
|
118
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
119
|
+
}
|
|
120
|
+
table[n] = c;
|
|
121
|
+
}
|
|
122
|
+
return table;
|
|
123
|
+
})();
|
|
124
|
+
function calculateCRC(bytes) {
|
|
125
|
+
let crc = 4294967295;
|
|
126
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
127
|
+
crc = crcTable[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
|
|
128
|
+
}
|
|
129
|
+
return (crc ^ 4294967295) >>> 0;
|
|
130
|
+
}
|
|
131
|
+
function indexOfBytes(buffer, search, start = 0) {
|
|
132
|
+
for (let i = start; i <= buffer.length - search.length; i++) {
|
|
133
|
+
let found = true;
|
|
134
|
+
for (let j = 0; j < search.length; j++) {
|
|
135
|
+
if (buffer[i + j] !== search[j]) {
|
|
136
|
+
found = false;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (found) return i;
|
|
141
|
+
}
|
|
142
|
+
return -1;
|
|
143
|
+
}
|
|
144
|
+
function createXMPPacket(prompt, seed, model, id, tags = [], hierarchy) {
|
|
145
|
+
const sanitize = (str) => (str || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
146
|
+
const escPrompt = sanitize(prompt);
|
|
147
|
+
const escModel = sanitize(model || "AI Model");
|
|
148
|
+
const escHierarchy = sanitize(hierarchy || "");
|
|
149
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
150
|
+
const allTags = [.../* @__PURE__ */ new Set([...tags, "Avatar Architect", escModel])];
|
|
151
|
+
const rdfTags = allTags.map((t) => `<rdf:li>${sanitize(t)}</rdf:li>`).join("");
|
|
152
|
+
const BOM = "\uFEFF";
|
|
153
|
+
return `<?xpacket begin="${BOM}" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
|
154
|
+
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.6-c140">
|
|
155
|
+
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
|
156
|
+
<rdf:Description rdf:about=""
|
|
157
|
+
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
|
|
158
|
+
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
159
|
+
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"
|
|
160
|
+
xmlns:ai="http://ns.flow.creative/ai/1.0/">
|
|
161
|
+
<dc:format>image/png</dc:format>
|
|
162
|
+
<dc:description><rdf:Alt><rdf:li xml:lang="x-default">${escPrompt}</rdf:li></rdf:Alt></dc:description>
|
|
163
|
+
<dc:subject><rdf:Bag>${rdfTags}</rdf:Bag></dc:subject>
|
|
164
|
+
<photoshop:Instructions>Seed: ${seed} | Model: ${escModel}</photoshop:Instructions>
|
|
165
|
+
<xmp:CreateDate>${timestamp}</xmp:CreateDate>
|
|
166
|
+
<ai:prompt>${escPrompt}</ai:prompt>
|
|
167
|
+
<ai:seed>${seed ?? "0"}</ai:seed>
|
|
168
|
+
<ai:model>${escModel}</ai:model>
|
|
169
|
+
<ai:hierarchy>${escHierarchy}</ai:hierarchy>
|
|
170
|
+
</rdf:Description>
|
|
171
|
+
</rdf:RDF>
|
|
172
|
+
</x:xmpmeta>${" ".repeat(1024)}<?xpacket end="w"?>`;
|
|
173
|
+
}
|
|
174
|
+
function bytesToBase64(bytes) {
|
|
175
|
+
let binary = "";
|
|
176
|
+
const len = bytes.byteLength;
|
|
177
|
+
for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
|
|
178
|
+
return btoa(binary);
|
|
179
|
+
}
|
|
180
|
+
function base64ToBytes(base64) {
|
|
181
|
+
const clean = base64.includes(",") ? base64.split(",")[1] : base64.trim();
|
|
182
|
+
try {
|
|
183
|
+
const binary = atob(clean);
|
|
184
|
+
const bytes = new Uint8Array(binary.length);
|
|
185
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
186
|
+
return bytes;
|
|
187
|
+
} catch (e) {
|
|
188
|
+
return new Uint8Array(0);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function injectXMPMetadata(base64, prompt, seed, model, id, tags = [], hierarchy) {
|
|
192
|
+
try {
|
|
193
|
+
const bytes = base64ToBytes(base64);
|
|
194
|
+
if (bytes.length < 8 || bytes[0] !== 137 || bytes[1] !== 80) {
|
|
195
|
+
console.warn("Metadaten-Injektion \xFCbersprungen: Datei ist kein PNG.");
|
|
196
|
+
return base64;
|
|
197
|
+
}
|
|
198
|
+
const xmpData = createXMPPacket(prompt, seed, model, id, tags, hierarchy);
|
|
199
|
+
const encoder = new TextEncoder();
|
|
200
|
+
const keyword = encoder.encode("XML:com.adobe.xmp");
|
|
201
|
+
const textBytes = encoder.encode(xmpData);
|
|
202
|
+
const chunkData = new Uint8Array(keyword.length + 5 + textBytes.length);
|
|
203
|
+
chunkData.set(keyword, 0);
|
|
204
|
+
chunkData.set(textBytes, keyword.length + 5);
|
|
205
|
+
const type = encoder.encode("iTXt");
|
|
206
|
+
const crcInput = new Uint8Array(type.length + chunkData.length);
|
|
207
|
+
crcInput.set(type, 0);
|
|
208
|
+
crcInput.set(chunkData, type.length);
|
|
209
|
+
const crc = calculateCRC(crcInput);
|
|
210
|
+
const fullChunk = new Uint8Array(4 + 4 + chunkData.length + 4);
|
|
211
|
+
const view = new DataView(fullChunk.buffer);
|
|
212
|
+
view.setUint32(0, chunkData.length);
|
|
213
|
+
fullChunk.set(type, 4);
|
|
214
|
+
fullChunk.set(chunkData, 8);
|
|
215
|
+
view.setUint32(8 + chunkData.length, crc);
|
|
216
|
+
const IDAT = encoder.encode("IDAT");
|
|
217
|
+
const idatPos = indexOfBytes(bytes, IDAT);
|
|
218
|
+
const insertPos = idatPos !== -1 ? idatPos - 4 : 8;
|
|
219
|
+
const result = new Uint8Array(bytes.length + fullChunk.length);
|
|
220
|
+
result.set(bytes.slice(0, insertPos), 0);
|
|
221
|
+
result.set(fullChunk, insertPos);
|
|
222
|
+
result.set(bytes.slice(insertPos), insertPos + fullChunk.length);
|
|
223
|
+
return bytesToBase64(result);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
console.error("Fehler beim Einbetten der Metadaten:", e);
|
|
226
|
+
return base64;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
131
230
|
// src/lib/aiHelpers.ts
|
|
132
231
|
function buildGenerationPrompt(hierarchyText, mode = "creative") {
|
|
133
232
|
const safe = hierarchyText.length > 2500 ? hierarchyText.slice(0, 2500) + "..." : hierarchyText;
|
|
@@ -1446,7 +1545,7 @@ function ListView({ nodes, edges, onNodeChange, onAddChild, onDeleteNode, onMove
|
|
|
1446
1545
|
}
|
|
1447
1546
|
|
|
1448
1547
|
// src/components/AvatarArchitectApp.tsx
|
|
1449
|
-
import { useState as useState25, useCallback as useCallback4, useMemo as useMemo6, useEffect as
|
|
1548
|
+
import { useState as useState25, useCallback as useCallback4, useMemo as useMemo6, useEffect as useEffect9, useRef as useRef10 } from "react";
|
|
1450
1549
|
|
|
1451
1550
|
// src/components/PromptTab.tsx
|
|
1452
1551
|
import { useRef as useRef5, useState as useState8 } from "react";
|
|
@@ -2033,374 +2132,50 @@ var TaskExecutor = ({ openTasks, onExecute, onTaskCompleted }) => {
|
|
|
2033
2132
|
] });
|
|
2034
2133
|
};
|
|
2035
2134
|
|
|
2036
|
-
// src/components/
|
|
2037
|
-
import {
|
|
2038
|
-
import {
|
|
2039
|
-
var
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
onWorkspaceImport,
|
|
2044
|
-
projectActionState,
|
|
2045
|
-
serverProjects,
|
|
2046
|
-
onServerSave,
|
|
2047
|
-
onServerLoad,
|
|
2048
|
-
onServerDelete,
|
|
2049
|
-
onRefreshServerProjects,
|
|
2050
|
-
onComputeSyncDiff,
|
|
2051
|
-
onExecuteSync,
|
|
2052
|
-
hfToken,
|
|
2053
|
-
onHfLoad,
|
|
2054
|
-
onProjectExportBase64,
|
|
2055
|
-
onHfInitialSync
|
|
2056
|
-
}) => {
|
|
2057
|
-
const projectInputRef = useRef7(null);
|
|
2058
|
-
const workspaceInputRef = useRef7(null);
|
|
2059
|
-
const [saveName, setSaveName] = useState10("");
|
|
2060
|
-
const [isSaving, setIsSaving] = useState10(false);
|
|
2061
|
-
const [isExporting, setIsExporting] = useState10(false);
|
|
2062
|
-
const [syncState, setSyncState] = useState10("idle");
|
|
2063
|
-
const [syncDiff, setSyncDiff] = useState10(null);
|
|
2064
|
-
const [selectedLocalIds, setSelectedLocalIds] = useState10(/* @__PURE__ */ new Set());
|
|
2065
|
-
const handleExport = async () => {
|
|
2066
|
-
if (!onProjectExport) return;
|
|
2067
|
-
setIsExporting(true);
|
|
2068
|
-
try {
|
|
2069
|
-
await onProjectExport();
|
|
2070
|
-
} finally {
|
|
2071
|
-
setIsExporting(false);
|
|
2072
|
-
}
|
|
2073
|
-
};
|
|
2074
|
-
const handleComputeDiff = async () => {
|
|
2075
|
-
if (!onComputeSyncDiff) return;
|
|
2076
|
-
setSyncState("computing");
|
|
2077
|
-
setSyncDiff(null);
|
|
2078
|
-
try {
|
|
2079
|
-
const diff = await onComputeSyncDiff();
|
|
2080
|
-
setSyncDiff(diff);
|
|
2081
|
-
setSelectedLocalIds(new Set(diff.localOnly.map((g) => g.id)));
|
|
2082
|
-
setSyncState("ready");
|
|
2083
|
-
} catch {
|
|
2084
|
-
setSyncState("error");
|
|
2085
|
-
setTimeout(() => setSyncState("idle"), 4e3);
|
|
2086
|
-
}
|
|
2087
|
-
};
|
|
2088
|
-
const handleExecuteSync = async () => {
|
|
2089
|
-
if (!onExecuteSync || !syncDiff) return;
|
|
2090
|
-
setSyncState("executing");
|
|
2091
|
-
try {
|
|
2092
|
-
await onExecuteSync([...selectedLocalIds]);
|
|
2093
|
-
setSyncState("done");
|
|
2094
|
-
setSyncDiff(null);
|
|
2095
|
-
setTimeout(() => setSyncState("idle"), 3e3);
|
|
2096
|
-
} catch {
|
|
2097
|
-
setSyncState("error");
|
|
2098
|
-
setTimeout(() => setSyncState("idle"), 4e3);
|
|
2099
|
-
}
|
|
2100
|
-
};
|
|
2101
|
-
const toggleLocalId = (id) => {
|
|
2102
|
-
setSelectedLocalIds((prev) => {
|
|
2103
|
-
const next = new Set(prev);
|
|
2104
|
-
if (next.has(id)) next.delete(id);
|
|
2105
|
-
else next.add(id);
|
|
2106
|
-
return next;
|
|
2107
|
-
});
|
|
2108
|
-
};
|
|
2109
|
-
const isWorking = projectActionState === "working" || projectActionState === "working-full";
|
|
2110
|
-
const [hfProjects, setHfProjects] = useState10([]);
|
|
2111
|
-
const [hfLoading, setHfLoading] = useState10(false);
|
|
2112
|
-
const [hfSaving, setHfSaving] = useState10(false);
|
|
2113
|
-
const [hfError, setHfError] = useState10(null);
|
|
2114
|
-
const [hfSaveName, setHfSaveName] = useState10("");
|
|
2115
|
-
const [hfSyncProgress, setHfSyncProgress] = useState10(null);
|
|
2116
|
-
const [hfSyncing, setHfSyncing] = useState10(false);
|
|
2117
|
-
const loadHfProjects = async (token) => {
|
|
2118
|
-
setHfLoading(true);
|
|
2119
|
-
setHfError(null);
|
|
2120
|
-
try {
|
|
2121
|
-
setHfProjects(await hfListProjects(token));
|
|
2122
|
-
} catch (e) {
|
|
2123
|
-
setHfError(e.message);
|
|
2124
|
-
} finally {
|
|
2125
|
-
setHfLoading(false);
|
|
2126
|
-
}
|
|
2127
|
-
};
|
|
2128
|
-
useEffect6(() => {
|
|
2129
|
-
if (hfToken) loadHfProjects(hfToken);
|
|
2130
|
-
}, [hfToken]);
|
|
2135
|
+
// src/components/HfMediaSyncTab.tsx
|
|
2136
|
+
import { useState as useState10 } from "react";
|
|
2137
|
+
import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2138
|
+
var HfMediaSyncTab = ({ topSlot, onHfInitialSync }) => {
|
|
2139
|
+
const [error, setError] = useState10(null);
|
|
2140
|
+
const [progress, setProgress] = useState10(null);
|
|
2141
|
+
const [syncing, setSyncing] = useState10(false);
|
|
2131
2142
|
return /* @__PURE__ */ jsx15("div", { className: "absolute inset-0 overflow-y-auto dark-scrollbar", children: /* @__PURE__ */ jsxs13("div", { className: "p-6 flex flex-col gap-8", children: [
|
|
2132
2143
|
topSlot && /* @__PURE__ */ jsx15("div", { children: topSlot }),
|
|
2133
|
-
|
|
2134
|
-
/* @__PURE__ */ jsx15(SectionLabel, { children: "
|
|
2144
|
+
onHfInitialSync && /* @__PURE__ */ jsxs13("div", { className: "flex flex-col gap-4", children: [
|
|
2145
|
+
/* @__PURE__ */ jsx15(SectionLabel, { children: "Hugging Face" }),
|
|
2135
2146
|
/* @__PURE__ */ jsxs13("div", { className: "flex flex-col gap-2", children: [
|
|
2136
|
-
onProjectExport && /* @__PURE__ */ jsx15(PillButton, { variant: "filled", icon: "download", loading: isExporting || isWorking, onClick: handleExport, children: "Exportieren (.zip)" }),
|
|
2137
|
-
onProjectImport && /* @__PURE__ */ jsxs13(Fragment5, { children: [
|
|
2138
|
-
/* @__PURE__ */ jsx15(PillButton, { variant: "outline", icon: "upload", onClick: () => projectInputRef.current?.click(), children: "ZIP laden" }),
|
|
2139
|
-
/* @__PURE__ */ jsx15(
|
|
2140
|
-
"input",
|
|
2141
|
-
{
|
|
2142
|
-
ref: projectInputRef,
|
|
2143
|
-
type: "file",
|
|
2144
|
-
accept: ".zip",
|
|
2145
|
-
className: "hidden",
|
|
2146
|
-
onChange: (e) => {
|
|
2147
|
-
const f = e.target.files?.[0];
|
|
2148
|
-
if (f) onProjectImport(f);
|
|
2149
|
-
e.target.value = "";
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
)
|
|
2153
|
-
] })
|
|
2154
|
-
] }),
|
|
2155
|
-
onWorkspaceImport && /* @__PURE__ */ jsxs13(Fragment5, { children: [
|
|
2156
|
-
/* @__PURE__ */ jsx15(PillButton, { variant: "outline", icon: "upload_file", onClick: () => workspaceInputRef.current?.click(), children: "Tags laden (.json)" }),
|
|
2157
|
-
/* @__PURE__ */ jsx15(
|
|
2158
|
-
"input",
|
|
2159
|
-
{
|
|
2160
|
-
ref: workspaceInputRef,
|
|
2161
|
-
type: "file",
|
|
2162
|
-
accept: ".json",
|
|
2163
|
-
className: "hidden",
|
|
2164
|
-
onChange: (e) => {
|
|
2165
|
-
const f = e.target.files?.[0];
|
|
2166
|
-
if (f) onWorkspaceImport(f);
|
|
2167
|
-
e.target.value = "";
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
)
|
|
2171
|
-
] })
|
|
2172
|
-
] }),
|
|
2173
|
-
(onServerSave || serverProjects || onServerDelete) && /* @__PURE__ */ jsxs13("div", { className: "flex flex-col gap-4", children: [
|
|
2174
|
-
/* @__PURE__ */ jsx15(SectionLabel, { children: "Server-Speicher" }),
|
|
2175
|
-
onServerSave && /* @__PURE__ */ jsxs13("div", { className: "flex gap-2", children: [
|
|
2176
|
-
/* @__PURE__ */ jsx15(
|
|
2177
|
-
"input",
|
|
2178
|
-
{
|
|
2179
|
-
type: "text",
|
|
2180
|
-
value: saveName,
|
|
2181
|
-
onChange: (e) => setSaveName(e.target.value),
|
|
2182
|
-
placeholder: "Name (optional)",
|
|
2183
|
-
className: "flex-1 bg-white/5 border border-white/10 rounded-xl px-3 text-[12px] text-white/70 outline-none placeholder:text-white/20",
|
|
2184
|
-
style: { height: 40 }
|
|
2185
|
-
}
|
|
2186
|
-
),
|
|
2187
|
-
/* @__PURE__ */ jsx15(
|
|
2188
|
-
PillButton,
|
|
2189
|
-
{
|
|
2190
|
-
variant: "filled",
|
|
2191
|
-
icon: "cloud_upload",
|
|
2192
|
-
loading: isSaving,
|
|
2193
|
-
onClick: async () => {
|
|
2194
|
-
setIsSaving(true);
|
|
2195
|
-
try {
|
|
2196
|
-
await onServerSave(saveName || void 0);
|
|
2197
|
-
setSaveName("");
|
|
2198
|
-
await onRefreshServerProjects?.();
|
|
2199
|
-
} finally {
|
|
2200
|
-
setIsSaving(false);
|
|
2201
|
-
}
|
|
2202
|
-
},
|
|
2203
|
-
children: "Speichern"
|
|
2204
|
-
}
|
|
2205
|
-
)
|
|
2206
|
-
] }),
|
|
2207
|
-
(!serverProjects || serverProjects.length === 0) && /* @__PURE__ */ jsx15("p", { className: "text-[10px] text-white/20 px-2", children: "Noch nichts gespeichert." }),
|
|
2208
|
-
serverProjects && serverProjects.length > 0 && /* @__PURE__ */ jsx15("div", { className: "flex flex-col gap-1", children: serverProjects.map((p) => /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-2 px-3 py-2 rounded-xl bg-white/5 border border-white/5", children: [
|
|
2209
|
-
/* @__PURE__ */ jsxs13("div", { className: "flex-1 min-w-0", children: [
|
|
2210
|
-
/* @__PURE__ */ jsx15("p", { className: "text-[11px] text-white/70 font-bold truncate", children: p.name }),
|
|
2211
|
-
/* @__PURE__ */ jsxs13("p", { className: "text-[9px] text-white/25", children: [
|
|
2212
|
-
new Date(p.saved_at).toLocaleString("de-DE", { dateStyle: "short", timeStyle: "short" }),
|
|
2213
|
-
" \xB7 ",
|
|
2214
|
-
(p.size / 1024).toFixed(0),
|
|
2215
|
-
" KB"
|
|
2216
|
-
] })
|
|
2217
|
-
] }),
|
|
2218
|
-
onServerLoad && /* @__PURE__ */ jsx15("button", { onClick: () => onServerLoad(p.id), className: "w-8 h-8 flex items-center justify-center text-white/30 active:text-white transition-colors", children: /* @__PURE__ */ jsx15("span", { className: "material-symbols-outlined text-[18px]", children: "cloud_download" }) }),
|
|
2219
|
-
/* @__PURE__ */ jsx15("button", { onClick: () => onServerDelete?.(p.id), className: "w-8 h-8 flex items-center justify-center text-white/20 active:text-red-400 transition-colors", children: /* @__PURE__ */ jsx15("span", { className: "material-symbols-outlined text-[18px]", children: "delete" }) })
|
|
2220
|
-
] }, p.id)) })
|
|
2221
|
-
] }),
|
|
2222
|
-
hfToken && /* @__PURE__ */ jsxs13("div", { className: "flex flex-col gap-4", children: [
|
|
2223
|
-
/* @__PURE__ */ jsxs13("div", { className: "flex items-center justify-between", children: [
|
|
2224
|
-
/* @__PURE__ */ jsx15(SectionLabel, { children: "Hugging Face" }),
|
|
2225
|
-
/* @__PURE__ */ jsx15("button", { onClick: () => loadHfProjects(hfToken), className: "text-white/30 hover:text-white/60 transition", children: /* @__PURE__ */ jsx15("span", { className: `material-symbols-outlined text-[18px]${hfLoading ? " animate-spin" : ""}`, children: "refresh" }) })
|
|
2226
|
-
] }),
|
|
2227
|
-
onProjectExportBase64 && /* @__PURE__ */ jsxs13("div", { className: "flex gap-2", children: [
|
|
2228
|
-
/* @__PURE__ */ jsx15(
|
|
2229
|
-
"input",
|
|
2230
|
-
{
|
|
2231
|
-
type: "text",
|
|
2232
|
-
value: hfSaveName,
|
|
2233
|
-
onChange: (e) => setHfSaveName(e.target.value),
|
|
2234
|
-
placeholder: "Name (optional)",
|
|
2235
|
-
className: "flex-1 bg-white/5 border border-white/10 rounded-xl px-3 text-[12px] text-white/70 outline-none placeholder:text-white/20",
|
|
2236
|
-
style: { height: 40 }
|
|
2237
|
-
}
|
|
2238
|
-
),
|
|
2239
|
-
/* @__PURE__ */ jsx15(
|
|
2240
|
-
PillButton,
|
|
2241
|
-
{
|
|
2242
|
-
variant: "filled",
|
|
2243
|
-
icon: "cloud_upload",
|
|
2244
|
-
loading: hfSaving,
|
|
2245
|
-
onClick: async () => {
|
|
2246
|
-
if (!onProjectExportBase64) return;
|
|
2247
|
-
setHfSaving(true);
|
|
2248
|
-
setHfError(null);
|
|
2249
|
-
try {
|
|
2250
|
-
const base64 = await onProjectExportBase64();
|
|
2251
|
-
const name = hfSaveName.trim() || `project_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", "_").replace(":", "")}`;
|
|
2252
|
-
await hfUploadProjectForm(base64, name, hfToken);
|
|
2253
|
-
setHfSaveName("");
|
|
2254
|
-
await loadHfProjects(hfToken);
|
|
2255
|
-
} catch (e) {
|
|
2256
|
-
setHfError(e.message);
|
|
2257
|
-
} finally {
|
|
2258
|
-
setHfSaving(false);
|
|
2259
|
-
}
|
|
2260
|
-
},
|
|
2261
|
-
children: "Speichern"
|
|
2262
|
-
}
|
|
2263
|
-
)
|
|
2264
|
-
] }),
|
|
2265
|
-
onHfInitialSync && /* @__PURE__ */ jsxs13("div", { className: "flex flex-col gap-2", children: [
|
|
2266
2147
|
/* @__PURE__ */ jsx15(
|
|
2267
2148
|
PillButton,
|
|
2268
2149
|
{
|
|
2269
2150
|
variant: "outline",
|
|
2270
2151
|
icon: "cloud_sync",
|
|
2271
|
-
loading:
|
|
2152
|
+
loading: syncing,
|
|
2272
2153
|
onClick: async () => {
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2154
|
+
setSyncing(true);
|
|
2155
|
+
setError(null);
|
|
2156
|
+
setProgress({ done: 0, total: 0 });
|
|
2276
2157
|
try {
|
|
2277
|
-
await onHfInitialSync((done, total) =>
|
|
2278
|
-
|
|
2158
|
+
await onHfInitialSync((done, total) => setProgress({ done, total }));
|
|
2159
|
+
setProgress(null);
|
|
2279
2160
|
} catch (e) {
|
|
2280
|
-
|
|
2281
|
-
|
|
2161
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
2162
|
+
setProgress(null);
|
|
2282
2163
|
} finally {
|
|
2283
|
-
|
|
2164
|
+
setSyncing(false);
|
|
2284
2165
|
}
|
|
2285
2166
|
},
|
|
2286
|
-
children:
|
|
2167
|
+
children: syncing && progress ? `${progress.done} / ${progress.total} Bilder` : "Initial-Sync nach HF"
|
|
2287
2168
|
}
|
|
2288
2169
|
),
|
|
2289
|
-
|
|
2170
|
+
syncing && progress && progress.total > 0 && /* @__PURE__ */ jsx15("div", { className: "w-full h-1 bg-white/10 rounded-full overflow-hidden", children: /* @__PURE__ */ jsx15(
|
|
2290
2171
|
"div",
|
|
2291
2172
|
{
|
|
2292
2173
|
className: "h-full bg-blue-400 rounded-full transition-all",
|
|
2293
|
-
style: { width: `${Math.round(
|
|
2174
|
+
style: { width: `${Math.round(progress.done / progress.total * 100)}%` }
|
|
2294
2175
|
}
|
|
2295
2176
|
) })
|
|
2296
2177
|
] }),
|
|
2297
|
-
|
|
2298
|
-
!hfLoading && hfProjects.length === 0 && /* @__PURE__ */ jsx15("p", { className: "text-[10px] text-white/20 px-2", children: "Noch nichts auf HF gespeichert." }),
|
|
2299
|
-
hfProjects.map((p) => /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-2 px-3 py-2 rounded-xl bg-white/5 border border-white/5", children: [
|
|
2300
|
-
/* @__PURE__ */ jsxs13("div", { className: "flex-1 min-w-0", children: [
|
|
2301
|
-
/* @__PURE__ */ jsx15("p", { className: "text-[11px] text-white/70 font-bold truncate", children: p.name }),
|
|
2302
|
-
/* @__PURE__ */ jsxs13("p", { className: "text-[9px] text-white/25", children: [
|
|
2303
|
-
(p.size / 1024 / 1024).toFixed(1),
|
|
2304
|
-
" MB",
|
|
2305
|
-
p.isLfs ? " \xB7 LFS" : ""
|
|
2306
|
-
] })
|
|
2307
|
-
] }),
|
|
2308
|
-
onHfLoad && /* @__PURE__ */ jsx15(
|
|
2309
|
-
"button",
|
|
2310
|
-
{
|
|
2311
|
-
onClick: async () => {
|
|
2312
|
-
try {
|
|
2313
|
-
const { hfDownloadProject: hfDownloadProject2 } = await import("./hfStateService-W7KO2FOH.mjs");
|
|
2314
|
-
const file = await hfDownloadProject2(p.path, hfToken);
|
|
2315
|
-
onHfLoad(file);
|
|
2316
|
-
} catch (e) {
|
|
2317
|
-
setHfError(e.message);
|
|
2318
|
-
}
|
|
2319
|
-
},
|
|
2320
|
-
className: "w-8 h-8 flex items-center justify-center text-white/30 active:text-white transition-colors",
|
|
2321
|
-
children: /* @__PURE__ */ jsx15("span", { className: "material-symbols-outlined text-[18px]", children: "cloud_download" })
|
|
2322
|
-
}
|
|
2323
|
-
),
|
|
2324
|
-
/* @__PURE__ */ jsx15(
|
|
2325
|
-
"button",
|
|
2326
|
-
{
|
|
2327
|
-
onClick: async () => {
|
|
2328
|
-
if (!confirm(`"${p.name}" auf HF l\xF6schen?`)) return;
|
|
2329
|
-
try {
|
|
2330
|
-
await hfDeleteProject(p.path, hfToken);
|
|
2331
|
-
await loadHfProjects(hfToken);
|
|
2332
|
-
} catch (e) {
|
|
2333
|
-
setHfError(e.message);
|
|
2334
|
-
}
|
|
2335
|
-
},
|
|
2336
|
-
className: "w-8 h-8 flex items-center justify-center text-white/20 active:text-red-400 transition-colors",
|
|
2337
|
-
children: /* @__PURE__ */ jsx15("span", { className: "material-symbols-outlined text-[18px]", children: "delete" })
|
|
2338
|
-
}
|
|
2339
|
-
)
|
|
2340
|
-
] }, p.id))
|
|
2341
|
-
] }),
|
|
2342
|
-
onComputeSyncDiff && onExecuteSync && /* @__PURE__ */ jsxs13("div", { className: "flex flex-col gap-4", children: [
|
|
2343
|
-
/* @__PURE__ */ jsx15(SectionLabel, { children: "Sync" }),
|
|
2344
|
-
(syncState === "idle" || syncState === "computing") && /* @__PURE__ */ jsx15(PillButton, { variant: "outline", icon: "compare_arrows", loading: syncState === "computing", onClick: handleComputeDiff, children: "Diff berechnen" }),
|
|
2345
|
-
syncState === "done" && /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-2 px-2 text-[11px] text-green-400", children: [
|
|
2346
|
-
/* @__PURE__ */ jsx15("span", { className: "material-symbols-outlined text-[16px]", children: "check_circle" }),
|
|
2347
|
-
"Sync abgeschlossen"
|
|
2348
|
-
] }),
|
|
2349
|
-
syncState === "error" && /* @__PURE__ */ jsx15("p", { className: "text-[11px] text-red-400 px-2", children: "Fehler beim Sync." }),
|
|
2350
|
-
syncDiff && (syncState === "ready" || syncState === "executing") && /* @__PURE__ */ jsxs13(Fragment5, { children: [
|
|
2351
|
-
syncDiff.localOnly.length === 0 && syncDiff.serverOnly.length === 0 && /* @__PURE__ */ jsx15("p", { className: "text-[10px] text-white/30 px-2", children: "Alles synchron \u2014 keine Unterschiede." }),
|
|
2352
|
-
syncDiff.localOnly.length > 0 && /* @__PURE__ */ jsxs13("div", { className: "flex flex-col gap-2", children: [
|
|
2353
|
-
/* @__PURE__ */ jsxs13("p", { className: "text-[9px] text-white/30 uppercase tracking-widest px-1 font-bold", children: [
|
|
2354
|
-
"Nur lokal (",
|
|
2355
|
-
syncDiff.localOnly.length,
|
|
2356
|
-
")"
|
|
2357
|
-
] }),
|
|
2358
|
-
syncDiff.localOnly.map((gen) => /* @__PURE__ */ jsxs13(
|
|
2359
|
-
"button",
|
|
2360
|
-
{
|
|
2361
|
-
onClick: () => toggleLocalId(gen.id),
|
|
2362
|
-
className: `flex items-center gap-3 px-3 py-2 rounded-xl border transition-all text-left ${selectedLocalIds.has(gen.id) ? "bg-blue-500/10 border-blue-500/30" : "bg-white/3 border-white/5 opacity-60"}`,
|
|
2363
|
-
children: [
|
|
2364
|
-
/* @__PURE__ */ jsx15("div", { className: `w-4 h-4 rounded border flex items-center justify-center flex-shrink-0 ${selectedLocalIds.has(gen.id) ? "bg-blue-500 border-blue-500" : "border-white/20"}`, children: selectedLocalIds.has(gen.id) && /* @__PURE__ */ jsx15("span", { className: "material-symbols-outlined text-[11px] text-white", children: "check" }) }),
|
|
2365
|
-
gen.base64 && /* @__PURE__ */ jsx15("img", { src: gen.base64, className: "w-10 h-10 rounded object-cover flex-shrink-0" }),
|
|
2366
|
-
/* @__PURE__ */ jsxs13("div", { className: "flex-1 min-w-0", children: [
|
|
2367
|
-
/* @__PURE__ */ jsx15("p", { className: "text-[10px] text-white/60 truncate", children: gen.prompt || "Kein Prompt" }),
|
|
2368
|
-
/* @__PURE__ */ jsx15("p", { className: "text-[9px] text-white/25", children: new Date(gen.timestamp).toLocaleString("de-DE", { dateStyle: "short", timeStyle: "short" }) })
|
|
2369
|
-
] })
|
|
2370
|
-
]
|
|
2371
|
-
},
|
|
2372
|
-
gen.id
|
|
2373
|
-
))
|
|
2374
|
-
] }),
|
|
2375
|
-
syncDiff.serverOnly.length > 0 && /* @__PURE__ */ jsxs13("div", { className: "flex flex-col gap-2", children: [
|
|
2376
|
-
/* @__PURE__ */ jsxs13("p", { className: "text-[9px] text-white/30 uppercase tracking-widest px-1 font-bold", children: [
|
|
2377
|
-
"Nur auf Server (",
|
|
2378
|
-
syncDiff.serverOnly.length,
|
|
2379
|
-
")"
|
|
2380
|
-
] }),
|
|
2381
|
-
syncDiff.serverOnly.map((gen) => /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-3 px-3 py-2 rounded-xl bg-white/3 border border-white/5 opacity-40", children: [
|
|
2382
|
-
gen.base64 && /* @__PURE__ */ jsx15("img", { src: gen.base64, className: "w-10 h-10 rounded object-cover flex-shrink-0" }),
|
|
2383
|
-
/* @__PURE__ */ jsxs13("div", { className: "flex-1 min-w-0", children: [
|
|
2384
|
-
/* @__PURE__ */ jsx15("p", { className: "text-[10px] text-white/60 truncate", children: gen.prompt || "Kein Prompt" }),
|
|
2385
|
-
/* @__PURE__ */ jsx15("p", { className: "text-[9px] text-white/25", children: new Date(gen.timestamp).toLocaleString("de-DE", { dateStyle: "short", timeStyle: "short" }) })
|
|
2386
|
-
] })
|
|
2387
|
-
] }, gen.id))
|
|
2388
|
-
] }),
|
|
2389
|
-
(syncDiff.localOnly.length > 0 || syncDiff.serverOnly.length > 0) && /* @__PURE__ */ jsxs13(
|
|
2390
|
-
PillButton,
|
|
2391
|
-
{
|
|
2392
|
-
variant: "solid",
|
|
2393
|
-
icon: "sync",
|
|
2394
|
-
loading: syncState === "executing",
|
|
2395
|
-
disabled: selectedLocalIds.size === 0,
|
|
2396
|
-
onClick: handleExecuteSync,
|
|
2397
|
-
children: [
|
|
2398
|
-
selectedLocalIds.size,
|
|
2399
|
-
" \xFCbertragen"
|
|
2400
|
-
]
|
|
2401
|
-
}
|
|
2402
|
-
)
|
|
2403
|
-
] })
|
|
2178
|
+
error && /* @__PURE__ */ jsx15("p", { className: "text-[10px] text-red-400 font-mono px-1", children: error })
|
|
2404
2179
|
] })
|
|
2405
2180
|
] }) });
|
|
2406
2181
|
};
|
|
@@ -2532,7 +2307,7 @@ function toPromptImages(images) {
|
|
|
2532
2307
|
}
|
|
2533
2308
|
|
|
2534
2309
|
// src/hooks/useHFState.ts
|
|
2535
|
-
import { useState as useState11, useEffect as
|
|
2310
|
+
import { useState as useState11, useEffect as useEffect6, useRef as useRef7, useCallback } from "react";
|
|
2536
2311
|
|
|
2537
2312
|
// src/lib/hfDag.ts
|
|
2538
2313
|
function buildDag(events) {
|
|
@@ -2622,9 +2397,9 @@ function useHFState(token, namespace) {
|
|
|
2622
2397
|
const [pendingBufferCount, setPendingBufferCount] = useState11(readOfflineBuffer().length);
|
|
2623
2398
|
const [lastEventTs, setLastEventTs] = useState11(0);
|
|
2624
2399
|
const [hasStateZip, setHasStateZip] = useState11(false);
|
|
2625
|
-
const knownEventPaths =
|
|
2626
|
-
const knownFilePaths =
|
|
2627
|
-
const allEventsRef =
|
|
2400
|
+
const knownEventPaths = useRef7(/* @__PURE__ */ new Set());
|
|
2401
|
+
const knownFilePaths = useRef7(/* @__PURE__ */ new Set());
|
|
2402
|
+
const allEventsRef = useRef7([]);
|
|
2628
2403
|
const applyNewEvents = useCallback((snapshot, newEvents) => {
|
|
2629
2404
|
if (!newEvents.length && allEventsRef.current.length === 0) {
|
|
2630
2405
|
setEventCount(0);
|
|
@@ -2701,10 +2476,10 @@ function useHFState(token, namespace) {
|
|
|
2701
2476
|
} catch {
|
|
2702
2477
|
}
|
|
2703
2478
|
}, [token, namespace, state, applyNewEvents]);
|
|
2704
|
-
|
|
2479
|
+
useEffect6(() => {
|
|
2705
2480
|
if (token && namespace) loadFull();
|
|
2706
2481
|
}, [token, namespace]);
|
|
2707
|
-
|
|
2482
|
+
useEffect6(() => {
|
|
2708
2483
|
if (!token || !namespace) return;
|
|
2709
2484
|
const id = setInterval(pollNew, POLL_INTERVAL_MS);
|
|
2710
2485
|
return () => clearInterval(id);
|
|
@@ -2759,7 +2534,7 @@ function useHFState(token, namespace) {
|
|
|
2759
2534
|
}
|
|
2760
2535
|
|
|
2761
2536
|
// src/hooks/useTagBase.ts
|
|
2762
|
-
import { useState as useState12, useEffect as
|
|
2537
|
+
import { useState as useState12, useEffect as useEffect7, useCallback as useCallback2, useRef as useRef8 } from "react";
|
|
2763
2538
|
|
|
2764
2539
|
// src/lib/hfTagBaseService.ts
|
|
2765
2540
|
import JSZip from "jszip";
|
|
@@ -2895,13 +2670,13 @@ function useTagBase(token, namespace) {
|
|
|
2895
2670
|
const [selectedContextId, setSelectedContextId] = useState12(() => readStoredId(CONTEXT_ID_KEY));
|
|
2896
2671
|
const [pickerMessage, setPickerMessage] = useState12(null);
|
|
2897
2672
|
const [pendingTags, setPendingTags] = useState12([]);
|
|
2898
|
-
const pendingIdCounter =
|
|
2899
|
-
const selectedUserIdRef =
|
|
2900
|
-
const selectedContextIdRef =
|
|
2901
|
-
|
|
2673
|
+
const pendingIdCounter = useRef8(-1);
|
|
2674
|
+
const selectedUserIdRef = useRef8(selectedUserId);
|
|
2675
|
+
const selectedContextIdRef = useRef8(selectedContextId);
|
|
2676
|
+
useEffect7(() => {
|
|
2902
2677
|
selectedUserIdRef.current = selectedUserId;
|
|
2903
2678
|
}, [selectedUserId]);
|
|
2904
|
-
|
|
2679
|
+
useEffect7(() => {
|
|
2905
2680
|
selectedContextIdRef.current = selectedContextId;
|
|
2906
2681
|
}, [selectedContextId]);
|
|
2907
2682
|
const load = useCallback2(async () => {
|
|
@@ -2946,7 +2721,7 @@ function useTagBase(token, namespace) {
|
|
|
2946
2721
|
setIsLoading(false);
|
|
2947
2722
|
}
|
|
2948
2723
|
}, [token, namespace]);
|
|
2949
|
-
|
|
2724
|
+
useEffect7(() => {
|
|
2950
2725
|
load();
|
|
2951
2726
|
}, [load]);
|
|
2952
2727
|
const selectUser = useCallback2((userId) => {
|
|
@@ -3045,7 +2820,7 @@ import { useState as useState15 } from "react";
|
|
|
3045
2820
|
|
|
3046
2821
|
// src/components/labs/LabImagePicker.tsx
|
|
3047
2822
|
import { useState as useState13 } from "react";
|
|
3048
|
-
import { Fragment as
|
|
2823
|
+
import { Fragment as Fragment5, jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
3049
2824
|
var LabImagePicker = ({
|
|
3050
2825
|
availableItems,
|
|
3051
2826
|
recentItems,
|
|
@@ -3142,7 +2917,7 @@ var LabImagePicker = ({
|
|
|
3142
2917
|
}
|
|
3143
2918
|
) }),
|
|
3144
2919
|
/* @__PURE__ */ jsxs14("div", { className: "flex-1 overflow-y-auto dark-scrollbar px-3 pb-3", children: [
|
|
3145
|
-
recentItems.length > 0 && !search && /* @__PURE__ */ jsxs14(
|
|
2920
|
+
recentItems.length > 0 && !search && /* @__PURE__ */ jsxs14(Fragment5, { children: [
|
|
3146
2921
|
/* @__PURE__ */ jsx16("p", { className: "text-[9px] font-bold uppercase tracking-widest text-white/30 mb-2 mt-1", children: "Zuletzt verwendet" }),
|
|
3147
2922
|
/* @__PURE__ */ jsx16("div", { className: "grid grid-cols-3 gap-2 mb-4", children: recentItems.slice(0, 6).map(renderItemCard) }),
|
|
3148
2923
|
/* @__PURE__ */ jsx16("p", { className: "text-[9px] font-bold uppercase tracking-widest text-white/30 mb-2", children: "Alle Bilder" })
|
|
@@ -3257,7 +3032,7 @@ function useGenerationSettings() {
|
|
|
3257
3032
|
}
|
|
3258
3033
|
|
|
3259
3034
|
// src/components/labs/LabRemix.tsx
|
|
3260
|
-
import { Fragment as
|
|
3035
|
+
import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
3261
3036
|
var LabRemix = ({ services, onResult }) => {
|
|
3262
3037
|
const [showPicker, setShowPicker] = useState15(false);
|
|
3263
3038
|
const [selected, setSelected] = useState15(null);
|
|
@@ -3413,10 +3188,10 @@ var LabRemix = ({ services, onResult }) => {
|
|
|
3413
3188
|
onClick: handleGeneratePrompt,
|
|
3414
3189
|
disabled: !selected || !instruction.trim() || isGeneratingPrompt,
|
|
3415
3190
|
className: "w-full py-3 rounded-xl bg-white/10 text-white font-bold text-[12px] uppercase tracking-wide disabled:opacity-30 active:bg-white/15 flex items-center justify-center gap-2",
|
|
3416
|
-
children: isGeneratingPrompt ? /* @__PURE__ */ jsxs16(
|
|
3191
|
+
children: isGeneratingPrompt ? /* @__PURE__ */ jsxs16(Fragment6, { children: [
|
|
3417
3192
|
/* @__PURE__ */ jsx18("div", { className: "w-3 h-3 border-t border-white rounded-full animate-spin" }),
|
|
3418
3193
|
/* @__PURE__ */ jsx18("span", { children: "Generiere Prompt..." })
|
|
3419
|
-
] }) : /* @__PURE__ */ jsxs16(
|
|
3194
|
+
] }) : /* @__PURE__ */ jsxs16(Fragment6, { children: [
|
|
3420
3195
|
/* @__PURE__ */ jsx18("span", { className: "material-symbols-outlined text-[16px]", children: "auto_fix_high" }),
|
|
3421
3196
|
/* @__PURE__ */ jsx18("span", { children: "Prompt generieren" })
|
|
3422
3197
|
] })
|
|
@@ -3463,7 +3238,7 @@ var LabRemix = ({ services, onResult }) => {
|
|
|
3463
3238
|
|
|
3464
3239
|
// src/components/labs/LabBlend.tsx
|
|
3465
3240
|
import { useState as useState16 } from "react";
|
|
3466
|
-
import { Fragment as
|
|
3241
|
+
import { Fragment as Fragment7, jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
3467
3242
|
var LabBlend = ({ services, onResult }) => {
|
|
3468
3243
|
const [showPickerFor, setShowPickerFor] = useState16(null);
|
|
3469
3244
|
const [selectedImages, setSelectedImages] = useState16([]);
|
|
@@ -3558,7 +3333,7 @@ var LabBlend = ({ services, onResult }) => {
|
|
|
3558
3333
|
/* @__PURE__ */ jsx19("p", { className: "text-[9px] font-bold uppercase tracking-widest text-white/40", children: "Bilder" }),
|
|
3559
3334
|
selectedImages.map((img, i) => /* @__PURE__ */ jsxs17("div", { className: "flex gap-3 p-3 rounded-xl border border-white/10 bg-white/5", children: [
|
|
3560
3335
|
/* @__PURE__ */ jsx19("span", { className: "text-[10px] font-bold text-white/40 w-6 shrink-0 pt-1", children: autoLabel(i) }),
|
|
3561
|
-
img ? /* @__PURE__ */ jsxs17(
|
|
3336
|
+
img ? /* @__PURE__ */ jsxs17(Fragment7, { children: [
|
|
3562
3337
|
/* @__PURE__ */ jsx19("img", { src: img.frame.base64, className: "w-12 h-12 object-cover rounded-lg shrink-0", onClick: () => setShowPickerFor(i) }),
|
|
3563
3338
|
/* @__PURE__ */ jsxs17("div", { className: "flex-1 min-w-0 flex flex-col gap-1", children: [
|
|
3564
3339
|
/* @__PURE__ */ jsx19("p", { className: "text-[10px] text-white/50 line-clamp-1", children: img.item.prompt }),
|
|
@@ -3624,10 +3399,10 @@ var LabBlend = ({ services, onResult }) => {
|
|
|
3624
3399
|
onClick: handleGeneratePrompt,
|
|
3625
3400
|
disabled: selectedImages.filter(Boolean).length < 2 || !instruction.trim() || isGeneratingPrompt,
|
|
3626
3401
|
className: "w-full py-3 rounded-xl bg-white/10 text-white font-bold text-[12px] uppercase tracking-wide disabled:opacity-30 active:bg-white/15 flex items-center justify-center gap-2",
|
|
3627
|
-
children: isGeneratingPrompt ? /* @__PURE__ */ jsxs17(
|
|
3402
|
+
children: isGeneratingPrompt ? /* @__PURE__ */ jsxs17(Fragment7, { children: [
|
|
3628
3403
|
/* @__PURE__ */ jsx19("div", { className: "w-3 h-3 border-t border-white rounded-full animate-spin" }),
|
|
3629
3404
|
/* @__PURE__ */ jsx19("span", { children: "Blend l\xE4uft..." })
|
|
3630
|
-
] }) : /* @__PURE__ */ jsxs17(
|
|
3405
|
+
] }) : /* @__PURE__ */ jsxs17(Fragment7, { children: [
|
|
3631
3406
|
/* @__PURE__ */ jsx19("span", { className: "material-symbols-outlined text-[16px]", children: "merge" }),
|
|
3632
3407
|
/* @__PURE__ */ jsx19("span", { children: "Prompts blenden" })
|
|
3633
3408
|
] })
|
|
@@ -3674,7 +3449,7 @@ var LabBlend = ({ services, onResult }) => {
|
|
|
3674
3449
|
|
|
3675
3450
|
// src/components/labs/LabCompare.tsx
|
|
3676
3451
|
import { useState as useState17 } from "react";
|
|
3677
|
-
import { Fragment as
|
|
3452
|
+
import { Fragment as Fragment8, jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
3678
3453
|
var LabCompare = ({ services, onResult }) => {
|
|
3679
3454
|
const [showPickerFor, setShowPickerFor] = useState17(null);
|
|
3680
3455
|
const [selectedImages, setSelectedImages] = useState17([]);
|
|
@@ -3756,7 +3531,7 @@ var LabCompare = ({ services, onResult }) => {
|
|
|
3756
3531
|
/* @__PURE__ */ jsx20("p", { className: "text-[9px] font-bold uppercase tracking-widest text-white/40", children: "Bilder zum Vergleichen" }),
|
|
3757
3532
|
selectedImages.map((img, i) => /* @__PURE__ */ jsxs18("div", { className: "flex gap-3 p-3 rounded-xl border border-white/10 bg-white/5", children: [
|
|
3758
3533
|
/* @__PURE__ */ jsx20("span", { className: "text-[10px] font-bold text-white/40 w-6 shrink-0 pt-1", children: autoLabel(i) }),
|
|
3759
|
-
img ? /* @__PURE__ */ jsxs18(
|
|
3534
|
+
img ? /* @__PURE__ */ jsxs18(Fragment8, { children: [
|
|
3760
3535
|
/* @__PURE__ */ jsx20("img", { src: img.frame.base64, className: "w-12 h-12 object-cover rounded-lg shrink-0 cursor-pointer", onClick: () => setShowPickerFor(i) }),
|
|
3761
3536
|
/* @__PURE__ */ jsxs18("div", { className: "flex-1 min-w-0 flex flex-col gap-1", children: [
|
|
3762
3537
|
/* @__PURE__ */ jsx20("p", { className: "text-[10px] text-white/50 line-clamp-1", children: img.item.prompt }),
|
|
@@ -3800,10 +3575,10 @@ var LabCompare = ({ services, onResult }) => {
|
|
|
3800
3575
|
onClick: handleAnalyze,
|
|
3801
3576
|
disabled: selectedImages.filter(Boolean).length < 1 || !instruction.trim() || isAnalyzing,
|
|
3802
3577
|
className: "w-full py-3 rounded-xl bg-white/10 text-white font-bold text-[12px] uppercase tracking-wide disabled:opacity-30 active:bg-white/15 flex items-center justify-center gap-2",
|
|
3803
|
-
children: isAnalyzing ? /* @__PURE__ */ jsxs18(
|
|
3578
|
+
children: isAnalyzing ? /* @__PURE__ */ jsxs18(Fragment8, { children: [
|
|
3804
3579
|
/* @__PURE__ */ jsx20("div", { className: "w-3 h-3 border-t border-white rounded-full animate-spin" }),
|
|
3805
3580
|
/* @__PURE__ */ jsx20("span", { children: "Analysiere..." })
|
|
3806
|
-
] }) : /* @__PURE__ */ jsxs18(
|
|
3581
|
+
] }) : /* @__PURE__ */ jsxs18(Fragment8, { children: [
|
|
3807
3582
|
/* @__PURE__ */ jsx20("span", { className: "material-symbols-outlined text-[16px]", children: "compare" }),
|
|
3808
3583
|
/* @__PURE__ */ jsx20("span", { children: "Analysieren" })
|
|
3809
3584
|
] })
|
|
@@ -3830,10 +3605,10 @@ var LabCompare = ({ services, onResult }) => {
|
|
|
3830
3605
|
onClick: handleGenerateImage,
|
|
3831
3606
|
disabled: !generatedPrompt.trim() || isGeneratingImage,
|
|
3832
3607
|
className: "w-full mt-2 py-3 rounded-xl bg-blue-600/80 text-white font-bold text-[12px] uppercase tracking-wide disabled:opacity-30 active:bg-blue-600 flex items-center justify-center gap-2",
|
|
3833
|
-
children: isGeneratingImage ? /* @__PURE__ */ jsxs18(
|
|
3608
|
+
children: isGeneratingImage ? /* @__PURE__ */ jsxs18(Fragment8, { children: [
|
|
3834
3609
|
/* @__PURE__ */ jsx20("div", { className: "w-3 h-3 border-t border-white rounded-full animate-spin" }),
|
|
3835
3610
|
/* @__PURE__ */ jsx20("span", { children: "Generiere..." })
|
|
3836
|
-
] }) : /* @__PURE__ */ jsxs18(
|
|
3611
|
+
] }) : /* @__PURE__ */ jsxs18(Fragment8, { children: [
|
|
3837
3612
|
/* @__PURE__ */ jsx20("span", { className: "material-symbols-outlined text-[16px]", children: "image" }),
|
|
3838
3613
|
/* @__PURE__ */ jsx20("span", { children: "Bild generieren" })
|
|
3839
3614
|
] })
|
|
@@ -4017,7 +3792,7 @@ var LabLoop = ({ services, onResult }) => {
|
|
|
4017
3792
|
};
|
|
4018
3793
|
|
|
4019
3794
|
// src/components/labs/LabFrameExtractor.tsx
|
|
4020
|
-
import { useRef as
|
|
3795
|
+
import { useRef as useRef9, useState as useState19, useCallback as useCallback3 } from "react";
|
|
4021
3796
|
import { jsx as jsx22, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
4022
3797
|
var formatTime = (s) => {
|
|
4023
3798
|
const m = Math.floor(s / 60);
|
|
@@ -4031,9 +3806,9 @@ var LabFrameExtractor = ({
|
|
|
4031
3806
|
onResult,
|
|
4032
3807
|
resolveVideoUrl
|
|
4033
3808
|
}) => {
|
|
4034
|
-
const videoRef =
|
|
4035
|
-
const canvasRef =
|
|
4036
|
-
const cancelledRef =
|
|
3809
|
+
const videoRef = useRef9(null);
|
|
3810
|
+
const canvasRef = useRef9(null);
|
|
3811
|
+
const cancelledRef = useRef9(false);
|
|
4037
3812
|
const [selectedItem, setSelectedItem] = useState19(null);
|
|
4038
3813
|
const [videoSrc, setVideoSrc] = useState19(null);
|
|
4039
3814
|
const [videoReady, setVideoReady] = useState19(false);
|
|
@@ -5425,7 +5200,7 @@ function HFTestTab({ token, namespace, galleryItems, allEvents = [], confirmedEv
|
|
|
5425
5200
|
}
|
|
5426
5201
|
|
|
5427
5202
|
// src/components/ServerTab.tsx
|
|
5428
|
-
import { useState as useState23, useEffect as
|
|
5203
|
+
import { useState as useState23, useEffect as useEffect8 } from "react";
|
|
5429
5204
|
import { jsx as jsx27, jsxs as jsxs25 } from "react/jsx-runtime";
|
|
5430
5205
|
function StarRating({ rating = 0 }) {
|
|
5431
5206
|
return /* @__PURE__ */ jsx27("div", { className: "flex gap-[2px]", children: [1, 2, 3, 4, 5].map((i) => /* @__PURE__ */ jsx27("span", { className: `material-symbols-outlined text-[12px] ${i <= rating ? "text-yellow-400" : "text-white/15"}`, children: "star" }, i)) });
|
|
@@ -5452,7 +5227,7 @@ function ServerTab({ serverBaseUrl }) {
|
|
|
5452
5227
|
const [libError, setLibError] = useState23(null);
|
|
5453
5228
|
const [activeTag, setActiveTag] = useState23(null);
|
|
5454
5229
|
const [preview, setPreview] = useState23(null);
|
|
5455
|
-
|
|
5230
|
+
useEffect8(() => {
|
|
5456
5231
|
if (!serverBaseUrl) return;
|
|
5457
5232
|
setUsersLoading(true);
|
|
5458
5233
|
setUsersError(null);
|
|
@@ -5604,7 +5379,7 @@ function ServerTab({ serverBaseUrl }) {
|
|
|
5604
5379
|
|
|
5605
5380
|
// src/components/FlowWorkspacePicker.tsx
|
|
5606
5381
|
import { useMemo as useMemo5, useState as useState24 } from "react";
|
|
5607
|
-
import { Fragment as
|
|
5382
|
+
import { Fragment as Fragment9, jsx as jsx28, jsxs as jsxs26 } from "react/jsx-runtime";
|
|
5608
5383
|
function FlowWorkspacePicker({
|
|
5609
5384
|
tagBase,
|
|
5610
5385
|
selectedUserId,
|
|
@@ -5646,7 +5421,7 @@ function FlowWorkspacePicker({
|
|
|
5646
5421
|
tagBase.meta.version,
|
|
5647
5422
|
" enth\xE4lt keine Benutzer."
|
|
5648
5423
|
] })
|
|
5649
|
-
] }) : /* @__PURE__ */ jsxs26(
|
|
5424
|
+
] }) : /* @__PURE__ */ jsxs26(Fragment9, { children: [
|
|
5650
5425
|
/* @__PURE__ */ jsx28("button", { className: "flow-workspace-picker__back", type: "button", onClick: back, children: "\u2190 Anderen Benutzer w\xE4hlen" }),
|
|
5651
5426
|
/* @__PURE__ */ jsxs26("div", { className: "flow-workspace-picker__choices", "aria-label": "Kontext w\xE4hlen", children: [
|
|
5652
5427
|
contexts.map((context) => /* @__PURE__ */ jsxs26("button", { type: "button", onClick: () => onSelectContext(context.id), children: [
|
|
@@ -5725,9 +5500,9 @@ function resolveBootstrapTarget(input) {
|
|
|
5725
5500
|
}
|
|
5726
5501
|
|
|
5727
5502
|
// src/components/AvatarArchitectApp.tsx
|
|
5728
|
-
import { Fragment as
|
|
5729
|
-
function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onSelectMedia, buildInfo, initialHfToken, hfNamespace, allowDevNamespace, requireWorkspaceSelection = false, serverBaseUrl
|
|
5730
|
-
|
|
5503
|
+
import { Fragment as Fragment10, jsx as jsx29, jsxs as jsxs27 } from "react/jsx-runtime";
|
|
5504
|
+
function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onSelectMedia, buildInfo, initialHfToken, hfNamespace, allowDevNamespace, requireWorkspaceSelection = false, serverBaseUrl }) {
|
|
5505
|
+
useEffect9(() => {
|
|
5731
5506
|
const id = "flow-styles";
|
|
5732
5507
|
if (!document.getElementById(id)) {
|
|
5733
5508
|
const style = document.createElement("style");
|
|
@@ -5737,7 +5512,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
5737
5512
|
}
|
|
5738
5513
|
}, []);
|
|
5739
5514
|
const [showStart, setShowStart] = useState25(true);
|
|
5740
|
-
const [showSzenen, setShowSzenen] = useState25(false);
|
|
5741
5515
|
const [layoutChoice, setLayoutChoice] = useState25(() => {
|
|
5742
5516
|
try {
|
|
5743
5517
|
return localStorage.getItem("aa-layout") || null;
|
|
@@ -5745,10 +5519,8 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
5745
5519
|
return null;
|
|
5746
5520
|
}
|
|
5747
5521
|
});
|
|
5748
|
-
const [projectLoaded, setProjectLoaded] = useState25(false);
|
|
5749
5522
|
const [hfToken, setHfToken] = useState25(initialHfToken || "");
|
|
5750
5523
|
const [hfTokenInput, setHfTokenInput] = useState25(initialHfToken || "");
|
|
5751
|
-
const [isLoadingFromHF, setIsLoadingFromHF] = useState25(false);
|
|
5752
5524
|
const [hfNamespaceLocal, setHfNamespaceLocal] = useState25(() => {
|
|
5753
5525
|
try {
|
|
5754
5526
|
return normalizeSelectableNamespace(localStorage.getItem("aa-hf-namespace"));
|
|
@@ -5758,7 +5530,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
5758
5530
|
});
|
|
5759
5531
|
const [hfNamespaceFromServer, setHfNamespaceFromServer] = useState25(null);
|
|
5760
5532
|
const [serverProbeFailed, setServerProbeFailed] = useState25(false);
|
|
5761
|
-
|
|
5533
|
+
useEffect9(() => {
|
|
5762
5534
|
if (hfNamespace !== void 0 || !serverBaseUrl) return;
|
|
5763
5535
|
fetch(`${serverBaseUrl}/api/status`).then((r) => r.json()).then((d) => {
|
|
5764
5536
|
if (typeof d.hfNamespace === "string" && d.hfNamespace) setHfNamespaceFromServer(d.hfNamespace);
|
|
@@ -5776,7 +5548,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
5776
5548
|
isStartDialogOpen: isDialogTarget && !targetConfirmed
|
|
5777
5549
|
});
|
|
5778
5550
|
const effectiveNamespace = bootstrapTarget.namespace ?? "";
|
|
5779
|
-
const kontextRef =
|
|
5551
|
+
const kontextRef = useRef10(null);
|
|
5780
5552
|
const szenenPort = useMemo6(() => hfToken && effectiveNamespace ? createFlowSzenenViewPort({
|
|
5781
5553
|
generateText: onGeneratePrompt,
|
|
5782
5554
|
token: hfToken,
|
|
@@ -5803,7 +5575,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
5803
5575
|
const [bootstrapLog, setBootstrapLog] = useState25([]);
|
|
5804
5576
|
const [isBootstrapping, setIsBootstrapping] = useState25(false);
|
|
5805
5577
|
const [hfMissingImages, setHfMissingImages] = useState25([]);
|
|
5806
|
-
const syncTopSlot = /* @__PURE__ */ jsxs27(
|
|
5578
|
+
const syncTopSlot = /* @__PURE__ */ jsxs27(Fragment10, { children: [
|
|
5807
5579
|
localOnlyCount > 0 && /* @__PURE__ */ jsxs27("div", { style: { background: "rgba(234,179,8,0.15)", border: "1px solid rgba(234,179,8,0.3)", padding: "4px 10px", fontSize: 11, color: "#fbbf24", borderRadius: 4, marginBottom: 4 }, children: [
|
|
5808
5580
|
"\u26A0 ",
|
|
5809
5581
|
localOnlyCount,
|
|
@@ -5852,7 +5624,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
5852
5624
|
bootstrapLog.length > 0 && /* @__PURE__ */ jsx29("div", { style: { marginTop: 6, fontFamily: "monospace", fontSize: 10, color: "#78716c", lineHeight: 1.6 }, children: bootstrapLog.map((l, i) => /* @__PURE__ */ jsx29("div", { children: l }, i)) })
|
|
5853
5625
|
] })
|
|
5854
5626
|
] });
|
|
5855
|
-
const wsInputRef = useRef11(null);
|
|
5856
5627
|
const startApp = (choice) => {
|
|
5857
5628
|
try {
|
|
5858
5629
|
localStorage.setItem("aa-layout", choice);
|
|
@@ -5866,11 +5637,11 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
5866
5637
|
const [edges, setEdges] = useState25([]);
|
|
5867
5638
|
const [history, setHistory] = useState25([]);
|
|
5868
5639
|
const [galleryItems, setGalleryItems] = useState25([]);
|
|
5869
|
-
const galleryItemsRef =
|
|
5870
|
-
|
|
5640
|
+
const galleryItemsRef = useRef10([]);
|
|
5641
|
+
useEffect9(() => {
|
|
5871
5642
|
galleryItemsRef.current = galleryItems;
|
|
5872
5643
|
}, [galleryItems]);
|
|
5873
|
-
const hfImageNotFoundRef =
|
|
5644
|
+
const hfImageNotFoundRef = useRef10(/* @__PURE__ */ new Map());
|
|
5874
5645
|
const [galleryVisibleCount, setGalleryVisibleCount] = useState25(20);
|
|
5875
5646
|
const [historyVisibleCount, setHistoryVisibleCount] = useState25(20);
|
|
5876
5647
|
const loadThumbnailsForEntries = useCallback4(async (entries) => {
|
|
@@ -5892,7 +5663,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
5892
5663
|
}
|
|
5893
5664
|
}
|
|
5894
5665
|
}, [hfToken, effectiveNamespace]);
|
|
5895
|
-
|
|
5666
|
+
useEffect9(() => {
|
|
5896
5667
|
if (!hfState) return;
|
|
5897
5668
|
if (hfState.tags?.by_category) setWorkspaceTags(hfState.tags);
|
|
5898
5669
|
const hfIds = new Set(hfState.metadata.map((m) => m.id));
|
|
@@ -5919,14 +5690,14 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
5919
5690
|
return [...localOnly, ...merged].sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0));
|
|
5920
5691
|
});
|
|
5921
5692
|
}, [hfState]);
|
|
5922
|
-
|
|
5693
|
+
useEffect9(() => {
|
|
5923
5694
|
if (!hfState) return;
|
|
5924
5695
|
const sortedMeta = [...hfState.metadata].sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0));
|
|
5925
5696
|
const skeletons = sortedMeta.map((m) => ({ id: m.id, prompt: m.prompt, timestamp: m.timestamp, status: "done", nodeId: m.id, tags: m.tags || [] }));
|
|
5926
5697
|
const repIds = new Set(groupByPrompt(skeletons).slice(0, galleryVisibleCount).map((g) => g.representative.id));
|
|
5927
5698
|
loadThumbnailsForEntries(sortedMeta.filter((e) => repIds.has(e.id)));
|
|
5928
5699
|
}, [hfState, galleryVisibleCount, loadThumbnailsForEntries]);
|
|
5929
|
-
|
|
5700
|
+
useEffect9(() => {
|
|
5930
5701
|
if (!hfState) return;
|
|
5931
5702
|
const sortedMeta = [...hfState.metadata].sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0));
|
|
5932
5703
|
loadThumbnailsForEntries(sortedMeta.slice(0, historyVisibleCount));
|
|
@@ -5987,11 +5758,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
5987
5758
|
}
|
|
5988
5759
|
});
|
|
5989
5760
|
const [isPromptCollapsed, setIsPromptCollapsed] = useState25(false);
|
|
5990
|
-
const [projectActionState, setProjectActionState] = useState25("idle");
|
|
5991
|
-
const syncServerDataRef = useRef11(null);
|
|
5992
5761
|
const [workspaceTags, setWorkspaceTags] = useState25(null);
|
|
5993
|
-
const [serverProjects, setServerProjects] = useState25([]);
|
|
5994
|
-
const [isLoadingFromServer, setIsLoadingFromServer] = useState25(false);
|
|
5995
5762
|
const [highContrast, setHighContrast] = useState25(() => {
|
|
5996
5763
|
try {
|
|
5997
5764
|
return localStorage.getItem("aa-contrast") === "high";
|
|
@@ -6006,9 +5773,9 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
6006
5773
|
const [isFullscreen, setIsFullscreen] = useState25(false);
|
|
6007
5774
|
const [zoomScale, setZoomScale] = useState25(1);
|
|
6008
5775
|
const [zoomOffset, setZoomOffset] = useState25({ x: 0, y: 0 });
|
|
6009
|
-
const lastPinchDist =
|
|
6010
|
-
const lastTapTime =
|
|
6011
|
-
const dragStart =
|
|
5776
|
+
const lastPinchDist = useRef10(null);
|
|
5777
|
+
const lastTapTime = useRef10(0);
|
|
5778
|
+
const dragStart = useRef10(null);
|
|
6012
5779
|
const openFullscreen = () => {
|
|
6013
5780
|
setIsFullscreen(true);
|
|
6014
5781
|
setZoomScale(1);
|
|
@@ -6186,7 +5953,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
6186
5953
|
const group = groups.find((gr) => gr.prompt === currentResult.prompt);
|
|
6187
5954
|
return group ? group.items : [];
|
|
6188
5955
|
}, [galleryItems, currentResult?.prompt]);
|
|
6189
|
-
|
|
5956
|
+
useEffect9(() => {
|
|
6190
5957
|
if (!currentResult?.prompt || !hfToken || !effectiveNamespace || !hfState) return;
|
|
6191
5958
|
const siblings = galleryItemsRef.current.filter(
|
|
6192
5959
|
(g) => g.prompt === currentResult.prompt && !g.base64 && !hfImageNotFoundRef.current.has(g.id)
|
|
@@ -6439,51 +6206,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
6439
6206
|
console.error("Download Error", e);
|
|
6440
6207
|
}
|
|
6441
6208
|
};
|
|
6442
|
-
const handleProjectExport = async () => {
|
|
6443
|
-
setProjectActionState("working-full");
|
|
6444
|
-
try {
|
|
6445
|
-
const { base64 } = await exportProjectToZip(nodes, edges, history, galleryItems, { aspectRatio, selectedModel, seed, seedMode }, workspaceTags, recentLabItems.map((i) => i.id));
|
|
6446
|
-
await onDownload(base64, "application/zip", `avatar_project_${getFormattedTimestamp()}.zip`);
|
|
6447
|
-
setProjectActionState("done");
|
|
6448
|
-
setTimeout(() => setProjectActionState("idle"), 3e3);
|
|
6449
|
-
} catch {
|
|
6450
|
-
setProjectActionState("error");
|
|
6451
|
-
}
|
|
6452
|
-
};
|
|
6453
|
-
const handleProjectImport = async (input) => {
|
|
6454
|
-
const file = input instanceof FileList ? input[0] : input;
|
|
6455
|
-
if (!file) return;
|
|
6456
|
-
setProjectActionState("working-full");
|
|
6457
|
-
try {
|
|
6458
|
-
const data = await importProjectFromZip(file);
|
|
6459
|
-
if (data.nodes) setNodes(data.nodes);
|
|
6460
|
-
if (data.edges) setEdges(data.edges);
|
|
6461
|
-
if (data.history) {
|
|
6462
|
-
setHistory(data.history);
|
|
6463
|
-
if (data.history.length > 0) setCurrentResult(data.history[0]);
|
|
6464
|
-
}
|
|
6465
|
-
if (data.galleryItems) setGalleryItems(data.galleryItems);
|
|
6466
|
-
if (data.settings) {
|
|
6467
|
-
setAspectRatio(data.settings.aspectRatio || "1:1");
|
|
6468
|
-
setSelectedModel(data.settings.selectedModel || "\u{1F34C} Nano Banana Pro");
|
|
6469
|
-
setSeed(data.settings.seed || 0);
|
|
6470
|
-
setSeedMode(data.settings.seedMode || "random");
|
|
6471
|
-
}
|
|
6472
|
-
if (data.workspaceTags?.by_category) setWorkspaceTags(data.workspaceTags);
|
|
6473
|
-
if (data.recentLabItemIds?.length) {
|
|
6474
|
-
const allGens = [...data.history || [], ...data.galleryItems || []];
|
|
6475
|
-
const labItems = groupGenerationsToLabItems(allGens);
|
|
6476
|
-
const recent = data.recentLabItemIds.map((id) => labItems.find((item) => item.id === id)).filter(Boolean);
|
|
6477
|
-
setRecentLabItems(recent);
|
|
6478
|
-
}
|
|
6479
|
-
setProjectActionState("done");
|
|
6480
|
-
setProjectLoaded(true);
|
|
6481
|
-
setTimeout(() => setProjectActionState("idle"), 2e3);
|
|
6482
|
-
} catch {
|
|
6483
|
-
setProjectActionState("error");
|
|
6484
|
-
setTimeout(() => setProjectActionState("idle"), 4e3);
|
|
6485
|
-
}
|
|
6486
|
-
};
|
|
6487
6209
|
const handleWorkspaceImport = (file) => {
|
|
6488
6210
|
const reader = new FileReader();
|
|
6489
6211
|
reader.onload = (ev) => {
|
|
@@ -6505,37 +6227,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
6505
6227
|
};
|
|
6506
6228
|
reader.readAsText(file);
|
|
6507
6229
|
};
|
|
6508
|
-
const fetchServerProjects = async () => {
|
|
6509
|
-
if (!onFetchServerProjects) return;
|
|
6510
|
-
try {
|
|
6511
|
-
setServerProjects(await onFetchServerProjects());
|
|
6512
|
-
} catch {
|
|
6513
|
-
}
|
|
6514
|
-
};
|
|
6515
|
-
const handleServerSave = async (name) => {
|
|
6516
|
-
if (!onServerSave) return;
|
|
6517
|
-
setProjectActionState("working-full");
|
|
6518
|
-
try {
|
|
6519
|
-
const { base64 } = await exportProjectToZip(nodes, edges, history, galleryItems, { aspectRatio, selectedModel, seed, seedMode }, workspaceTags, recentLabItems.map((i) => i.id));
|
|
6520
|
-
await onServerSave(base64, name || `avatar_project_${getFormattedTimestamp()}`);
|
|
6521
|
-
await fetchServerProjects();
|
|
6522
|
-
setProjectActionState("done");
|
|
6523
|
-
setTimeout(() => setProjectActionState("idle"), 2e3);
|
|
6524
|
-
} catch {
|
|
6525
|
-
setProjectActionState("error");
|
|
6526
|
-
setTimeout(() => setProjectActionState("idle"), 4e3);
|
|
6527
|
-
}
|
|
6528
|
-
};
|
|
6529
|
-
const handleServerLoad = async (id) => {
|
|
6530
|
-
if (!onServerLoad) return;
|
|
6531
|
-
const file = await onServerLoad(id);
|
|
6532
|
-
await handleProjectImport(file);
|
|
6533
|
-
};
|
|
6534
|
-
const handleServerDelete = async (id) => {
|
|
6535
|
-
if (!onServerDelete) return;
|
|
6536
|
-
await onServerDelete(id);
|
|
6537
|
-
await fetchServerProjects();
|
|
6538
|
-
};
|
|
6539
6230
|
const handleHfInitialSync = async (onProgress) => {
|
|
6540
6231
|
if (!hfToken || !effectiveNamespace) return;
|
|
6541
6232
|
const existingIds = new Set((hfState?.metadata || []).map((m) => m.id));
|
|
@@ -6563,45 +6254,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
6563
6254
|
onProgress(done, total);
|
|
6564
6255
|
}
|
|
6565
6256
|
};
|
|
6566
|
-
const handleComputeSyncDiff = async () => {
|
|
6567
|
-
if (!onFetchServerProjects || !onServerLoad) throw new Error("Server nicht konfiguriert");
|
|
6568
|
-
const projects = await onFetchServerProjects();
|
|
6569
|
-
if (projects.length === 0) return { localOnly: [...history], serverOnly: [] };
|
|
6570
|
-
const file = await onServerLoad(projects[0].id);
|
|
6571
|
-
const serverData = await importProjectFromZip(file);
|
|
6572
|
-
syncServerDataRef.current = serverData;
|
|
6573
|
-
const localOnly = history.filter((h) => !(serverData.history || []).find((s) => s.id === h.id));
|
|
6574
|
-
const serverOnly = (serverData.history || []).filter((s) => !history.find((h) => h.id === s.id));
|
|
6575
|
-
return { localOnly, serverOnly };
|
|
6576
|
-
};
|
|
6577
|
-
const handleExecuteSync = async (selectedLocalIds) => {
|
|
6578
|
-
const serverData = syncServerDataRef.current;
|
|
6579
|
-
if (!serverData || !onServerSave) return;
|
|
6580
|
-
setProjectActionState("working-full");
|
|
6581
|
-
try {
|
|
6582
|
-
const selectedLocal = history.filter((h) => selectedLocalIds.includes(h.id));
|
|
6583
|
-
const mergedHistory = [...serverData.history || [], ...selectedLocal];
|
|
6584
|
-
const { base64 } = await exportProjectToZip(
|
|
6585
|
-
serverData.nodes || nodes,
|
|
6586
|
-
serverData.edges || edges,
|
|
6587
|
-
mergedHistory,
|
|
6588
|
-
serverData.galleryItems || galleryItems,
|
|
6589
|
-
serverData.settings || { aspectRatio, selectedModel, seed, seedMode },
|
|
6590
|
-
serverData.workspaceTags || workspaceTags,
|
|
6591
|
-
serverData.recentLabItemIds || recentLabItems.map((i) => i.id)
|
|
6592
|
-
);
|
|
6593
|
-
await onServerSave(base64, `sync_${getFormattedTimestamp()}`);
|
|
6594
|
-
await fetchServerProjects();
|
|
6595
|
-
setProjectActionState("done");
|
|
6596
|
-
setTimeout(() => setProjectActionState("idle"), 2e3);
|
|
6597
|
-
} catch {
|
|
6598
|
-
setProjectActionState("error");
|
|
6599
|
-
setTimeout(() => setProjectActionState("idle"), 4e3);
|
|
6600
|
-
}
|
|
6601
|
-
};
|
|
6602
|
-
useEffect10(() => {
|
|
6603
|
-
if (activeTab === "setup" || activeTab === "sync") fetchServerProjects();
|
|
6604
|
-
}, [activeTab]);
|
|
6605
6257
|
const mergeWorkspaceTags = (local, remote) => {
|
|
6606
6258
|
if (!remote?.by_category) return local;
|
|
6607
6259
|
const merged = {};
|
|
@@ -6654,7 +6306,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
6654
6306
|
setZoomScale(1);
|
|
6655
6307
|
setZoomOffset({ x: 0, y: 0 });
|
|
6656
6308
|
}, className: "absolute top-4 left-4 w-10 h-10 flex items-center justify-center rounded-full bg-black/70 border border-white/20 z-10", children: /* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined text-[20px]", children: "zoom_out_map" }) }),
|
|
6657
|
-
history.length > 1 && /* @__PURE__ */ jsxs27(
|
|
6309
|
+
history.length > 1 && /* @__PURE__ */ jsxs27(Fragment10, { children: [
|
|
6658
6310
|
/* @__PURE__ */ jsx29("button", { onClick: () => {
|
|
6659
6311
|
if (currentIndex > 0) setCurrentResult(history[currentIndex - 1]);
|
|
6660
6312
|
}, disabled: currentIndex <= 0, className: "absolute left-2 top-1/2 -translate-y-1/2 w-10 h-10 flex items-center justify-center rounded-full bg-black/60 border border-white/10 disabled:opacity-0", children: /* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined text-[22px]", children: "chevron_left" }) }),
|
|
@@ -6672,31 +6324,8 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
6672
6324
|
}
|
|
6673
6325
|
);
|
|
6674
6326
|
}
|
|
6675
|
-
if (showSzenen && szenenPort) {
|
|
6676
|
-
return /* @__PURE__ */ jsxs27("main", { className: "flow-szenen-shell", children: [
|
|
6677
|
-
/* @__PURE__ */ jsxs27("header", { className: "flow-szenen-shell__header", children: [
|
|
6678
|
-
/* @__PURE__ */ jsx29("button", { type: "button", onClick: () => setShowSzenen(false), children: "Zur\xFCck" }),
|
|
6679
|
-
/* @__PURE__ */ jsxs27("div", { children: [
|
|
6680
|
-
/* @__PURE__ */ jsx29("strong", { children: "Szenen" }),
|
|
6681
|
-
/* @__PURE__ */ jsxs27("small", { children: [
|
|
6682
|
-
effectiveNamespace,
|
|
6683
|
-
" \xB7 User ",
|
|
6684
|
-
tagBaseResult.selectedUserId,
|
|
6685
|
-
" \xB7 Context ",
|
|
6686
|
-
tagBaseResult.selectedContextId
|
|
6687
|
-
] })
|
|
6688
|
-
] })
|
|
6689
|
-
] }),
|
|
6690
|
-
/* @__PURE__ */ jsx29("div", { className: "flow-szenen-shell__content", children: /* @__PURE__ */ jsx29(SzenenView, { port: szenenPort, contextId: szenenKontextId }) })
|
|
6691
|
-
] });
|
|
6692
|
-
}
|
|
6693
6327
|
if (showStart) {
|
|
6694
6328
|
return /* @__PURE__ */ jsxs27("div", { className: "fixed inset-0 bg-[#0e0e0e] flex flex-col items-center justify-center p-6", style: { gap: 28, ...hcStyle }, children: [
|
|
6695
|
-
/* @__PURE__ */ jsx29("input", { ref: wsInputRef, type: "file", accept: ".zip", className: "hidden", onChange: (e) => {
|
|
6696
|
-
const f = e.target.files?.[0];
|
|
6697
|
-
if (f) handleProjectImport(f);
|
|
6698
|
-
e.target.value = "";
|
|
6699
|
-
} }),
|
|
6700
6329
|
/* @__PURE__ */ jsxs27("div", { className: "flex flex-col items-center gap-1", children: [
|
|
6701
6330
|
/* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined text-white/15 text-[44px]", children: "palette" }),
|
|
6702
6331
|
/* @__PURE__ */ jsx29("span", { className: "text-white/25 text-[10px] font-bold uppercase tracking-[0.25em]", children: "Avatar Architect" }),
|
|
@@ -6720,21 +6349,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
6720
6349
|
]
|
|
6721
6350
|
}
|
|
6722
6351
|
),
|
|
6723
|
-
/* @__PURE__ */ jsxs27("div", { className: "flex flex-col items-center gap-2 w-full max-w-[280px]", children: [
|
|
6724
|
-
/* @__PURE__ */ jsxs27(
|
|
6725
|
-
"button",
|
|
6726
|
-
{
|
|
6727
|
-
onClick: () => wsInputRef.current?.click(),
|
|
6728
|
-
className: "w-full flex items-center justify-center gap-3 rounded-2xl font-bold text-[14px] uppercase tracking-wide text-white active:scale-95 transition-transform",
|
|
6729
|
-
style: { height: 56, background: projectLoaded ? "#16a34a" : "#0284c7" },
|
|
6730
|
-
children: [
|
|
6731
|
-
/* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined text-[22px]", children: projectLoaded ? "check_circle" : "folder_zip" }),
|
|
6732
|
-
projectLoaded ? "Projekt geladen \u2713" : "Projekt laden (.zip)"
|
|
6733
|
-
]
|
|
6734
|
-
}
|
|
6735
|
-
),
|
|
6736
|
-
!projectLoaded && /* @__PURE__ */ jsx29("span", { className: "text-white/20 text-[10px] text-center", children: "Baum, Bilder und Einstellungen wiederherstellen" })
|
|
6737
|
-
] }),
|
|
6738
6352
|
/* @__PURE__ */ jsxs27("div", { className: "flex flex-col items-center gap-2 w-full max-w-[280px]", children: [
|
|
6739
6353
|
!initialHfToken && /* @__PURE__ */ jsxs27("div", { className: "flex gap-2 w-full", children: [
|
|
6740
6354
|
/* @__PURE__ */ jsx29(
|
|
@@ -6785,79 +6399,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
6785
6399
|
},
|
|
6786
6400
|
ns
|
|
6787
6401
|
))
|
|
6788
|
-
] })
|
|
6789
|
-
hfToken && /* @__PURE__ */ jsxs27(
|
|
6790
|
-
"button",
|
|
6791
|
-
{
|
|
6792
|
-
disabled: isLoadingFromHF,
|
|
6793
|
-
onClick: async () => {
|
|
6794
|
-
setIsLoadingFromHF(true);
|
|
6795
|
-
try {
|
|
6796
|
-
const { hfListProjects: hfListProjects2, hfDownloadProject: hfDownloadProject2 } = await import("./hfStateService-W7KO2FOH.mjs");
|
|
6797
|
-
const projects = await hfListProjects2(hfToken);
|
|
6798
|
-
if (projects.length > 0) {
|
|
6799
|
-
const file = await hfDownloadProject2(projects[0].path, hfToken);
|
|
6800
|
-
await handleProjectImport(file);
|
|
6801
|
-
}
|
|
6802
|
-
} catch (e) {
|
|
6803
|
-
alert("HF Fehler: " + e.message);
|
|
6804
|
-
} finally {
|
|
6805
|
-
setIsLoadingFromHF(false);
|
|
6806
|
-
}
|
|
6807
|
-
},
|
|
6808
|
-
className: "w-full flex items-center justify-center gap-3 rounded-2xl font-bold text-[14px] uppercase tracking-wide text-white active:scale-95 transition-transform disabled:opacity-50",
|
|
6809
|
-
style: { height: 56, background: "#f59e0b" },
|
|
6810
|
-
children: [
|
|
6811
|
-
/* @__PURE__ */ jsx29("span", { className: `material-symbols-outlined text-[22px]${isLoadingFromHF ? " animate-spin" : ""}`, children: isLoadingFromHF ? "sync" : "cloud_download" }),
|
|
6812
|
-
isLoadingFromHF ? "Laden\u2026" : "Von HF laden"
|
|
6813
|
-
]
|
|
6814
|
-
}
|
|
6815
|
-
),
|
|
6816
|
-
hfToken && /* @__PURE__ */ jsx29("span", { className: "text-white/20 text-[10px] text-center", children: "Letzten Stand von Hugging Face laden" })
|
|
6817
|
-
] }),
|
|
6818
|
-
onFetchServerProjects && /* @__PURE__ */ jsxs27("div", { className: "flex flex-col items-center gap-2 w-full max-w-[280px]", children: [
|
|
6819
|
-
/* @__PURE__ */ jsxs27(
|
|
6820
|
-
"button",
|
|
6821
|
-
{
|
|
6822
|
-
disabled: isLoadingFromServer,
|
|
6823
|
-
onClick: async () => {
|
|
6824
|
-
setIsLoadingFromServer(true);
|
|
6825
|
-
try {
|
|
6826
|
-
const projects = await onFetchServerProjects();
|
|
6827
|
-
setServerProjects(projects);
|
|
6828
|
-
if (projects.length > 0 && onServerLoad) {
|
|
6829
|
-
const file = await onServerLoad(projects[0].id);
|
|
6830
|
-
await handleProjectImport(file);
|
|
6831
|
-
}
|
|
6832
|
-
} catch {
|
|
6833
|
-
} finally {
|
|
6834
|
-
setIsLoadingFromServer(false);
|
|
6835
|
-
}
|
|
6836
|
-
},
|
|
6837
|
-
className: "w-full flex items-center justify-center gap-3 rounded-2xl font-bold text-[14px] uppercase tracking-wide text-white active:scale-95 transition-transform disabled:opacity-50",
|
|
6838
|
-
style: { height: 56, background: "#7c3aed" },
|
|
6839
|
-
children: [
|
|
6840
|
-
/* @__PURE__ */ jsx29("span", { className: `material-symbols-outlined text-[22px]${isLoadingFromServer ? " animate-spin" : ""}`, children: isLoadingFromServer ? "sync" : "cloud_download" }),
|
|
6841
|
-
isLoadingFromServer ? "Laden\u2026" : "Vom Server laden"
|
|
6842
|
-
]
|
|
6843
|
-
}
|
|
6844
|
-
),
|
|
6845
|
-
/* @__PURE__ */ jsx29("span", { className: "text-white/20 text-[10px] text-center", children: "Letzten Stand vom Server wiederherstellen" })
|
|
6846
|
-
] }),
|
|
6847
|
-
/* @__PURE__ */ jsxs27("div", { className: "flow-szenen-entry", children: [
|
|
6848
|
-
/* @__PURE__ */ jsxs27(
|
|
6849
|
-
"button",
|
|
6850
|
-
{
|
|
6851
|
-
type: "button",
|
|
6852
|
-
disabled: !szenenPort || !bootstrapTarget.ready,
|
|
6853
|
-
onClick: () => setShowSzenen(true),
|
|
6854
|
-
children: [
|
|
6855
|
-
/* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined", children: "movie" }),
|
|
6856
|
-
"Szenen \xF6ffnen"
|
|
6857
|
-
]
|
|
6858
|
-
}
|
|
6859
|
-
),
|
|
6860
|
-
!szenenPort && /* @__PURE__ */ jsx29("small", { children: "F\xFCr Szenen zuerst den HF-Token setzen." })
|
|
6402
|
+
] })
|
|
6861
6403
|
] }),
|
|
6862
6404
|
/* @__PURE__ */ jsxs27("div", { className: "flex flex-col items-center gap-2 w-full max-w-[280px]", children: [
|
|
6863
6405
|
/* @__PURE__ */ jsx29("span", { className: "text-white/25 text-[10px] uppercase tracking-widest font-bold", children: "Layout w\xE4hlen & starten" }),
|
|
@@ -6926,6 +6468,11 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
6926
6468
|
);
|
|
6927
6469
|
}
|
|
6928
6470
|
}
|
|
6471
|
+
const szenenPane = szenenPort ? /* @__PURE__ */ jsx29("div", { className: "flow-szenen-pane", children: /* @__PURE__ */ jsx29(SzenenView, { port: szenenPort, contextId: szenenKontextId }) }) : /* @__PURE__ */ jsx29("div", { className: "flow-szenen-pane flow-szenen-pane--empty", children: /* @__PURE__ */ jsxs27("p", { children: [
|
|
6472
|
+
"Szenen brauchen einen HF-Token f\xFCr Namespace ",
|
|
6473
|
+
effectiveNamespace || "(noch nicht bestimmt)",
|
|
6474
|
+
" \u2014 auf der Flow-Startseite setzen."
|
|
6475
|
+
] }) });
|
|
6929
6476
|
if (layoutChoice === "mobile" || layoutChoice === "mobile-desktop") {
|
|
6930
6477
|
const mdMode = layoutChoice === "mobile-desktop";
|
|
6931
6478
|
const mdScale = mdMode ? window.innerWidth / 430 : 1;
|
|
@@ -6957,6 +6504,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
6957
6504
|
onTagMove: handleTagMove
|
|
6958
6505
|
}
|
|
6959
6506
|
) }),
|
|
6507
|
+
mobileTab === "szenen" && szenenPane,
|
|
6960
6508
|
mobileTab === "stage" && /* @__PURE__ */ jsxs27("div", { className: "flex flex-col flex-1 min-h-0", children: [
|
|
6961
6509
|
/* @__PURE__ */ jsxs27("div", { className: "flex items-center gap-2 px-3 border-b border-white/5 bg-black/30 shrink-0", style: { height: 52 }, children: [
|
|
6962
6510
|
/* @__PURE__ */ jsx29(CompactDropdown, { value: aspectRatio, onChange: setAspectRatio, options: [{ label: "1:1", value: "1:1" }, { label: "16:9", value: "16:9" }, { label: "9:16", value: "9:16" }] }),
|
|
@@ -7029,7 +6577,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
7029
6577
|
/* @__PURE__ */ jsx29("span", { className: "text-[11px] font-bold uppercase tracking-[0.2em]", children: "Avatar Architect" })
|
|
7030
6578
|
] }),
|
|
7031
6579
|
currentResult?.status === "done" && /* @__PURE__ */ jsx29("button", { onClick: openFullscreen, className: "absolute top-2 right-2 w-8 h-8 flex items-center justify-center rounded-full bg-black/60 border border-white/10 z-10", children: /* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined text-[18px]", children: "fullscreen" }) }),
|
|
7032
|
-
history.length > 1 && currentResult && /* @__PURE__ */ jsxs27(
|
|
6580
|
+
history.length > 1 && currentResult && /* @__PURE__ */ jsxs27(Fragment10, { children: [
|
|
7033
6581
|
/* @__PURE__ */ jsx29("button", { onClick: goToPrev, disabled: currentIndex <= 0, className: "absolute left-2 top-1/2 -translate-y-1/2 w-10 h-10 flex items-center justify-center rounded-full bg-black/60 border border-white/10 disabled:opacity-0 transition-opacity", children: /* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined text-[22px]", children: "chevron_left" }) }),
|
|
7034
6582
|
/* @__PURE__ */ jsx29("button", { onClick: goToNext, disabled: currentIndex >= history.length - 1, className: "absolute right-2 top-1/2 -translate-y-1/2 w-10 h-10 flex items-center justify-center rounded-full bg-black/60 border border-white/10 disabled:opacity-0 transition-opacity", children: /* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined text-[22px]", children: "chevron_right" }) }),
|
|
7035
6583
|
/* @__PURE__ */ jsxs27("div", { className: "absolute bottom-2 left-1/2 -translate-x-1/2 bg-black/60 rounded-full px-3 py-0.5 text-[10px] text-white/40 font-mono", children: [
|
|
@@ -7200,26 +6748,9 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
7200
6748
|
}, onTagCreate: handleTagCreate, onTagUpdate: handleTagUpdate, onTagDelete: handleTagDelete, onScanImage: handleScanImage, isScanning: isScanningImage }) }),
|
|
7201
6749
|
activeTab === "setup" && /* @__PURE__ */ jsx29(SetupPanel, { onWorkspaceImport: handleWorkspaceImport, buildInfo, tagBase: tagBaseResult }),
|
|
7202
6750
|
activeTab === "sync" && /* @__PURE__ */ jsx29(
|
|
7203
|
-
|
|
6751
|
+
HfMediaSyncTab,
|
|
7204
6752
|
{
|
|
7205
6753
|
topSlot: syncTopSlot,
|
|
7206
|
-
onProjectExport: handleProjectExport,
|
|
7207
|
-
onProjectImport: (f) => handleProjectImport(f),
|
|
7208
|
-
onWorkspaceImport: handleWorkspaceImport,
|
|
7209
|
-
projectActionState,
|
|
7210
|
-
serverProjects: onFetchServerProjects ? serverProjects : void 0,
|
|
7211
|
-
onServerSave: onServerSave ? handleServerSave : void 0,
|
|
7212
|
-
onServerLoad: onServerLoad ? handleServerLoad : void 0,
|
|
7213
|
-
onServerDelete: onServerDelete ? handleServerDelete : void 0,
|
|
7214
|
-
onRefreshServerProjects: onFetchServerProjects ? fetchServerProjects : void 0,
|
|
7215
|
-
onComputeSyncDiff: onFetchServerProjects && onServerLoad && onServerSave ? handleComputeSyncDiff : void 0,
|
|
7216
|
-
onExecuteSync: onFetchServerProjects && onServerLoad && onServerSave ? handleExecuteSync : void 0,
|
|
7217
|
-
hfToken: hfToken || void 0,
|
|
7218
|
-
onHfLoad: (f) => handleProjectImport(f),
|
|
7219
|
-
onProjectExportBase64: async () => {
|
|
7220
|
-
const { base64 } = await exportProjectToZip(nodes, edges, history, galleryItems, { aspectRatio, selectedModel, seed, seedMode }, workspaceTags, recentLabItems.map((i) => i.id));
|
|
7221
|
-
return base64;
|
|
7222
|
-
},
|
|
7223
6754
|
onHfInitialSync: hfToken ? handleHfInitialSync : void 0
|
|
7224
6755
|
}
|
|
7225
6756
|
),
|
|
@@ -7257,7 +6788,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
7257
6788
|
...workspaceTags ? [{ id: "tags", icon: "label", label: "Tags" }] : [],
|
|
7258
6789
|
{ id: "browse", icon: "photo_library", label: "Galerie" },
|
|
7259
6790
|
...serverBaseUrl ? [{ id: "server", icon: "storage", label: "Server" }] : []
|
|
7260
|
-
].map((tab) => /* @__PURE__ */ jsxs27("button", { onClick: () =>
|
|
6791
|
+
].map((tab) => /* @__PURE__ */ jsxs27("button", { onClick: () => setMobileTab(tab.id), className: `flex-none min-w-[64px] flex flex-col items-center justify-center gap-0.5 transition-colors ${mobileTab === tab.id ? "text-white" : "text-white/30"}`, children: [
|
|
7261
6792
|
/* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined text-[24px]", children: tab.icon }),
|
|
7262
6793
|
/* @__PURE__ */ jsx29("span", { className: "text-[10px] font-bold uppercase tracking-wide", children: tab.label })
|
|
7263
6794
|
] }, tab.id)) })
|
|
@@ -7338,7 +6869,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
7338
6869
|
/* @__PURE__ */ jsx29("span", { style: { fontSize: 11, fontWeight: "bold", textTransform: "uppercase", letterSpacing: "0.2em" }, children: "Avatar Architect" })
|
|
7339
6870
|
] }),
|
|
7340
6871
|
currentResult?.status === "done" && /* @__PURE__ */ jsx29("button", { onClick: openFullscreen, style: { position: "absolute", top: 8, right: 8, width: 32, height: 32, display: "flex", alignItems: "center", justifyContent: "center", borderRadius: "50%", background: "rgba(0,0,0,0.6)", border: "1px solid rgba(255,255,255,0.1)", cursor: "pointer", color: "#fff" }, children: /* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined", style: { fontSize: 18 }, children: "fullscreen" }) }),
|
|
7341
|
-
history.length > 1 && currentResult && /* @__PURE__ */ jsxs27(
|
|
6872
|
+
history.length > 1 && currentResult && /* @__PURE__ */ jsxs27(Fragment10, { children: [
|
|
7342
6873
|
/* @__PURE__ */ jsx29("button", { onClick: goToPrev, disabled: currentIndex <= 0, style: { position: "absolute", left: 8, top: "50%", transform: "translateY(-50%)", width: 36, height: 36, display: "flex", alignItems: "center", justifyContent: "center", borderRadius: "50%", background: "rgba(0,0,0,0.6)", border: "1px solid rgba(255,255,255,0.1)", cursor: "pointer", color: "#fff", opacity: currentIndex <= 0 ? 0 : 1, transition: "opacity 0.2s" }, children: /* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined", style: { fontSize: 20 }, children: "chevron_left" }) }),
|
|
7343
6874
|
/* @__PURE__ */ jsx29("button", { onClick: goToNext, disabled: currentIndex >= history.length - 1, style: { position: "absolute", right: 8, top: "50%", transform: "translateY(-50%)", width: 36, height: 36, display: "flex", alignItems: "center", justifyContent: "center", borderRadius: "50%", background: "rgba(0,0,0,0.6)", border: "1px solid rgba(255,255,255,0.1)", cursor: "pointer", color: "#fff", opacity: currentIndex >= history.length - 1 ? 0 : 1, transition: "opacity 0.2s" }, children: /* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined", style: { fontSize: 20 }, children: "chevron_right" }) }),
|
|
7344
6875
|
/* @__PURE__ */ jsxs27("div", { style: { position: "absolute", bottom: 8, left: "50%", transform: "translateX(-50%)", background: "rgba(0,0,0,0.6)", borderRadius: 999, padding: "2px 12px", fontSize: 10, color: "rgba(255,255,255,0.4)", fontFamily: "monospace" }, children: [
|
|
@@ -7452,6 +6983,14 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
7452
6983
|
className: `px-3 h-7 rounded-lg text-[9px] font-bold uppercase tracking-wide transition-colors ${middlePanel === "labs" ? "bg-white/10 text-white" : "text-white/30 hover:text-white/50"}`,
|
|
7453
6984
|
children: "Labs"
|
|
7454
6985
|
}
|
|
6986
|
+
),
|
|
6987
|
+
/* @__PURE__ */ jsx29(
|
|
6988
|
+
"button",
|
|
6989
|
+
{
|
|
6990
|
+
onClick: () => setMiddlePanel("szenen"),
|
|
6991
|
+
className: `px-3 h-7 rounded-lg text-[9px] font-bold uppercase tracking-wide transition-colors ${middlePanel === "szenen" ? "bg-white/10 text-white" : "text-white/30 hover:text-white/50"}`,
|
|
6992
|
+
children: "Szenen"
|
|
6993
|
+
}
|
|
7455
6994
|
)
|
|
7456
6995
|
] }),
|
|
7457
6996
|
/* @__PURE__ */ jsxs27("div", { className: "flex items-center gap-2", children: [
|
|
@@ -7474,7 +7013,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
7474
7013
|
/* @__PURE__ */ jsx29("textarea", { value: activePrompt, onChange: (e) => setActivePrompt(e.target.value), className: "w-full bg-transparent border-none outline-none text-[12px] leading-relaxed text-white/80 resize-none h-20 dark-scrollbar", placeholder: "W\xE4hle einen Knoten oder tippe einen Prompt..." }),
|
|
7475
7014
|
activePrompt && !isSynthesizing && /* @__PURE__ */ jsx29("button", { onClick: () => setActivePrompt(""), className: "absolute top-2 right-2 w-6 h-6 rounded-full bg-white/5 hover:bg-white/10 flex items-center justify-center transition-colors text-white/20 hover:text-white", children: /* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined text-[14px]", children: "close" }) })
|
|
7476
7015
|
] }) }),
|
|
7477
|
-
middlePanel === "labs" ? /* @__PURE__ */ jsx29("div", { className: "flex-1 overflow-hidden", children: /* @__PURE__ */ jsx29(LabsTab, { services: labServices, onResult: (item) => {
|
|
7016
|
+
middlePanel === "szenen" ? szenenPane : middlePanel === "labs" ? /* @__PURE__ */ jsx29("div", { className: "flex-1 overflow-hidden", children: /* @__PURE__ */ jsx29(LabsTab, { services: labServices, onResult: (item) => {
|
|
7478
7017
|
const frame = item.frames[0];
|
|
7479
7018
|
if (frame?.base64) setCurrentResult(frameToGeneration(frame, item));
|
|
7480
7019
|
} }) }) : /* @__PURE__ */ jsxs27("div", { className: "flex-1 overflow-hidden flex flex-col", children: [
|
|
@@ -7559,8 +7098,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
7559
7098
|
serverBaseUrl && /* @__PURE__ */ jsx29("button", { onClick: () => {
|
|
7560
7099
|
setActiveTab("server");
|
|
7561
7100
|
setIsRightCollapsed(false);
|
|
7562
|
-
}, className: `w-10 flex items-center justify-center relative transition-colors ${activeTab === "server" ? "text-white" : "text-white/20"}`, children: /* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined text-[20px]", children: "storage" }) })
|
|
7563
|
-
/* @__PURE__ */ jsx29("button", { onClick: () => setShowSzenen(true), className: "w-10 flex items-center justify-center relative text-white/20 hover:text-white", children: /* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined text-[20px]", children: "movie" }) })
|
|
7101
|
+
}, className: `w-10 flex items-center justify-center relative transition-colors ${activeTab === "server" ? "text-white" : "text-white/20"}`, children: /* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined text-[20px]", children: "storage" }) })
|
|
7564
7102
|
] }),
|
|
7565
7103
|
hfToken && /* @__PURE__ */ jsx29("button", { onClick: () => refreshHF(), disabled: isHfRefreshing, className: "w-10 flex items-center justify-center text-white/20 hover:text-white/60 transition-colors disabled:opacity-30", children: /* @__PURE__ */ jsx29("span", { className: `material-symbols-outlined text-[18px]${isHfRefreshing ? " animate-spin" : ""}`, children: "sync" }) }),
|
|
7566
7104
|
/* @__PURE__ */ jsx29("button", { onClick: () => setIsRightCollapsed(!isRightCollapsed), className: "w-10 flex items-center justify-center text-white/20 hover:text-white", children: /* @__PURE__ */ jsx29("span", { className: "material-symbols-outlined text-[18px]", children: isRightCollapsed ? "chevron_left" : "chevron_right" }) })
|
|
@@ -7601,26 +7139,9 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
7601
7139
|
activeTab === "inspect" && /* @__PURE__ */ jsx29(InspectPanel, { currentResult, history, onSelect: setCurrentResult }),
|
|
7602
7140
|
activeTab === "setup" && /* @__PURE__ */ jsx29(SetupPanel, { onWorkspaceImport: handleWorkspaceImport, buildInfo, tagBase: tagBaseResult }),
|
|
7603
7141
|
activeTab === "sync" && /* @__PURE__ */ jsx29(
|
|
7604
|
-
|
|
7142
|
+
HfMediaSyncTab,
|
|
7605
7143
|
{
|
|
7606
7144
|
topSlot: syncTopSlot,
|
|
7607
|
-
onProjectExport: handleProjectExport,
|
|
7608
|
-
onProjectImport: (f) => handleProjectImport(f),
|
|
7609
|
-
onWorkspaceImport: handleWorkspaceImport,
|
|
7610
|
-
projectActionState,
|
|
7611
|
-
serverProjects: onFetchServerProjects ? serverProjects : void 0,
|
|
7612
|
-
onServerSave: onServerSave ? handleServerSave : void 0,
|
|
7613
|
-
onServerLoad: onServerLoad ? handleServerLoad : void 0,
|
|
7614
|
-
onServerDelete: onServerDelete ? handleServerDelete : void 0,
|
|
7615
|
-
onRefreshServerProjects: onFetchServerProjects ? fetchServerProjects : void 0,
|
|
7616
|
-
onComputeSyncDiff: onFetchServerProjects && onServerLoad && onServerSave ? handleComputeSyncDiff : void 0,
|
|
7617
|
-
onExecuteSync: onFetchServerProjects && onServerLoad && onServerSave ? handleExecuteSync : void 0,
|
|
7618
|
-
hfToken: hfToken || void 0,
|
|
7619
|
-
onHfLoad: (f) => handleProjectImport(f),
|
|
7620
|
-
onProjectExportBase64: async () => {
|
|
7621
|
-
const { base64 } = await import("./project-O4ORKXY5.mjs").then((m) => m.exportProjectToZip(nodes, edges, history, galleryItems, { aspectRatio, selectedModel, seed, seedMode }, workspaceTags, recentLabItems.map((i) => i.id)));
|
|
7622
|
-
return base64;
|
|
7623
|
-
},
|
|
7624
7145
|
onHfInitialSync: hfToken ? handleHfInitialSync : void 0
|
|
7625
7146
|
}
|
|
7626
7147
|
),
|
|
@@ -7631,7 +7152,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
|
|
|
7631
7152
|
}
|
|
7632
7153
|
|
|
7633
7154
|
// src/components/FaApp.tsx
|
|
7634
|
-
import { useState as useState26, useEffect as
|
|
7155
|
+
import { useState as useState26, useEffect as useEffect10, useMemo as useMemo7 } from "react";
|
|
7635
7156
|
import { jsx as jsx30 } from "react/jsx-runtime";
|
|
7636
7157
|
function FaApp({
|
|
7637
7158
|
onGenerateImage,
|
|
@@ -7645,14 +7166,10 @@ function FaApp({
|
|
|
7645
7166
|
libToken,
|
|
7646
7167
|
allowDevNamespace,
|
|
7647
7168
|
serverBaseUrl,
|
|
7648
|
-
onFetchServerProjects,
|
|
7649
|
-
onServerSave,
|
|
7650
|
-
onServerLoad,
|
|
7651
|
-
onServerDelete,
|
|
7652
7169
|
buildInfo
|
|
7653
7170
|
}) {
|
|
7654
7171
|
const [hfNamespace, setHfNamespace] = useState26(void 0);
|
|
7655
|
-
|
|
7172
|
+
useEffect10(() => {
|
|
7656
7173
|
if (!serverBaseUrl) return;
|
|
7657
7174
|
fetch(`${serverBaseUrl}/api/status`).then((r) => r.json()).then((d) => {
|
|
7658
7175
|
if (typeof d.hfNamespace === "string") setHfNamespace(d.hfNamespace);
|
|
@@ -7675,10 +7192,6 @@ function FaApp({
|
|
|
7675
7192
|
allowDevNamespace: !hfNamespace && allowDevNamespace,
|
|
7676
7193
|
requireWorkspaceSelection: true,
|
|
7677
7194
|
serverBaseUrl,
|
|
7678
|
-
onFetchServerProjects,
|
|
7679
|
-
onServerSave,
|
|
7680
|
-
onServerLoad,
|
|
7681
|
-
onServerDelete,
|
|
7682
7195
|
buildInfo
|
|
7683
7196
|
}
|
|
7684
7197
|
);
|
|
@@ -8252,6 +7765,7 @@ export {
|
|
|
8252
7765
|
FaToolsBadge,
|
|
8253
7766
|
FlowWorkspacePicker,
|
|
8254
7767
|
GLOBAL_STYLES,
|
|
7768
|
+
HfMediaSyncTab,
|
|
8255
7769
|
HistoryPanel,
|
|
8256
7770
|
InspectPanel,
|
|
8257
7771
|
LIBRARY_CUTOVER_MARKER,
|
|
@@ -8268,7 +7782,6 @@ export {
|
|
|
8268
7782
|
ListView,
|
|
8269
7783
|
MediaLibrary,
|
|
8270
7784
|
PillButton,
|
|
8271
|
-
ProjectSyncTab,
|
|
8272
7785
|
PromptTab,
|
|
8273
7786
|
SCOPE_MAP,
|
|
8274
7787
|
SectionLabel,
|
|
@@ -8302,7 +7815,6 @@ export {
|
|
|
8302
7815
|
createTagsDefault,
|
|
8303
7816
|
deriveTagBaseWorkspaceTags,
|
|
8304
7817
|
deriveTagViewModel,
|
|
8305
|
-
exportProjectToZip,
|
|
8306
7818
|
formatTreeToMarkdown,
|
|
8307
7819
|
frameToGeneration,
|
|
8308
7820
|
getFormattedTimestamp,
|
|
@@ -8311,15 +7823,10 @@ export {
|
|
|
8311
7823
|
groupGenerationsToLabItems,
|
|
8312
7824
|
hfBatchArchive,
|
|
8313
7825
|
hfBootstrapFromLegacy,
|
|
8314
|
-
hfDeleteProject,
|
|
8315
|
-
hfDownloadProject,
|
|
8316
7826
|
hfListDir,
|
|
8317
|
-
hfListProjects,
|
|
8318
7827
|
hfLoadImageAsBase64,
|
|
8319
7828
|
hfUploadImage,
|
|
8320
|
-
hfUploadProjectForm,
|
|
8321
7829
|
hfUploadSmallFile,
|
|
8322
|
-
importProjectFromZip,
|
|
8323
7830
|
injectXMPMetadata,
|
|
8324
7831
|
interpretSdkError,
|
|
8325
7832
|
libraryContract,
|