268 lines
20 KiB
Markdown
268 lines
20 KiB
Markdown
---
|
||
phase: quick-260922-ge2
|
||
plan: 01
|
||
subsystem: apps/web/src/components/dashboard/widgets, apps/web/src/components/settings, apps/web/src/test
|
||
tags: [dashboard, widget, xframe, iframe, crop, zoom, pointer-events, resize-observer, tdd, i18n]
|
||
status: complete
|
||
requires:
|
||
- "quick-260921-qd3 (XFrame-Widget): Resolver, Widget, Formular, Sandbox, Neuladen"
|
||
provides:
|
||
- "XFrame-Ausschnitt: crop {x,y,w,h} in Seitenpixeln bei fester Layoutbreite 1280, in der Kachel eingepasst (contain) und zentriert, skaliert mit der Kachelgroesse"
|
||
- "Vorschau der Seite in den Einstellungen mit verschieb- und ziehbarem Rahmen (vier Ecken) plus Zahlenfelder Links/Oben/Breite/Hoehe"
|
||
- "Vergroesserung 50..150 % fuer die Ganzseiten-Ansicht ohne Ausschnitt"
|
||
- "„Nur anzeigen“: transparente Flaeche ueber dem Rahmen im Ansichtsmodus"
|
||
- "Test-Helfer stubResizeObserver({ width, height }) fuer deterministische Geometrie in jsdom"
|
||
affects:
|
||
- "apps/web/src/messages/de.json + en.json (13 neue Schluessel widgets.xframe.*)"
|
||
- "apps/web/src/messages/umlaut-dictionary.ts (Allowlist: Ausschnitt)"
|
||
tech-stack:
|
||
added: []
|
||
patterns:
|
||
- "Cross-origin laesst sich die Seite nicht von aussen scrollen: der <iframe> selbst wird mit left/top verschoben, mit transform: scale skaliert und von einem Clip-div geschnitten; feste Layoutbreite 1280 haelt den Seitenaufbau stabil"
|
||
- "EINE Klemmregel clampXframeCrop fuer Resolver, Zahlenfelder und Ziehen — Werte werden verschoben/gekappt, nie abgewiesen (T-GE2-05)"
|
||
- "Reine Geometrie (computeCropLayout, applyCropDrag) ohne React in eigenem Modul, zuerst rot getestet"
|
||
- "Pointer-Events mit Capture-Waechter (typeof setPointerCapture) — Browser faengt, jsdom nicht; Deltas statt getBoundingClientRect"
|
||
- "Kein Test-Prop im Produktionscode: Kachelgroesse im Test ueber dateiweisen ResizeObserver-Stub (ein Cast as ResizeObserverEntry)"
|
||
key-files:
|
||
created:
|
||
- apps/web/src/components/dashboard/widgets/xframe-crop.ts
|
||
- apps/web/src/components/dashboard/widgets/xframe-crop.test.ts
|
||
- apps/web/src/test/fake-resize-observer.ts
|
||
modified:
|
||
- apps/web/src/components/dashboard/widgets/xframe-config.ts
|
||
- apps/web/src/components/dashboard/widgets/xframe-config.test.ts
|
||
- apps/web/src/components/dashboard/widgets/xframe-widget.tsx
|
||
- apps/web/src/components/dashboard/widgets/xframe-widget.test.tsx
|
||
- apps/web/src/components/settings/xframe-config-form.tsx
|
||
- apps/web/src/components/settings/xframe-config-form.test.tsx
|
||
- apps/web/src/messages/de.json
|
||
- apps/web/src/messages/en.json
|
||
- apps/web/src/messages/umlaut-dictionary.ts
|
||
- CHANGELOG.md
|
||
- docs/anleitung-anwender.md
|
||
decisions:
|
||
- "Rahmen der Vorschau als <fieldset aria-label> statt <div role=\"group\">: Biome useSemanticElements meldete role=group (web 53 -> 54), biome-ignore ist verboten; das <fieldset> traegt die Gruppen-Rolle implizit, getByRole('group', { name }) findet es, Preflight nimmt Rand/Innenabstand (m-0 p-0 min-w-0 zusaetzlich)"
|
||
- "Objekt-Pruefung im Resolver ueber Typwaechter isRecord(raw): raw is Record<string, unknown> statt Zuweisung — `const r: Record<string, unknown> = raw` kompiliert unter strict nicht (object ohne Index-Signatur), ein Cast waere gegen die Regel"
|
||
- "Kopfkommentar des Test-Helfers ohne die woertliche Phrase des Zaehlers — der grep zaehlt Kommentare mit"
|
||
metrics:
|
||
duration: "ca. 10 min (10:03 bis 10:14 Uhr UTC, 22.09.2026)"
|
||
completed: 2026-09-22
|
||
actuals:
|
||
tokens: 17700
|
||
tasks: 2
|
||
commits: 2
|
||
plan_head_before: 5aa577a
|
||
---
|
||
|
||
# Quick-Aufgabe 260922-ge2: XFrame-Widget — Ausschnitt, Zoom, „Nur anzeigen“ Summary
|
||
|
||
Das XFrame-Widget zeigt auf Wunsch nur einen Ausschnitt der eingebetteten
|
||
Seite: in den Einstellungen erscheint eine Vorschau der Seite bei fester
|
||
Breite 1280 px, darueber ein Rahmen, den der Benutzer verschiebt und an
|
||
den vier Ecken zieht (oder Links/Oben/Breite/Hoehe eintippt); die Kachel
|
||
zeigt genau diesen Ausschnitt, eingepasst und zentriert, und skaliert ihn
|
||
mit der Kachelgroesse. Ohne Ausschnitt gibt es eine Vergroesserung
|
||
50–150 % fuer die ganze Seite; „Nur anzeigen“ sperrt Klicken und Scrollen
|
||
im Rahmen und wird beim Einschalten des Ausschnitts automatisch mit
|
||
gesetzt. Sandbox, no-referrer, `allow=""`, Neuladen, Kopfleiste und
|
||
Bearbeitungsflaeche sind unveraendert; der Server ruft die Adresse
|
||
weiterhin nie ab. Alle Tore gruen.
|
||
|
||
## Was gebaut wurde
|
||
|
||
**Resolver (`xframe-config.ts`, Commit 445b1d3).** Neue Exporte
|
||
`XFRAME_PAGE_WIDTH` 1280, `XFRAME_CROP_MIN_W` 100, `XFRAME_CROP_MIN_H` 60,
|
||
`XFRAME_CROP_MAX_H` 4000, `XFRAME_CROP_DEFAULT` {0,0,1280,720},
|
||
`XFRAME_ZOOM_OPTIONS` [50,60,75,90,100,125,150], `XFRAME_ZOOM_DEFAULT` 100,
|
||
`interface XframeCrop`, `clampXframeCrop` (runden; w auf [100,1280], h auf
|
||
[60,4000], x/y >= 0, dann `x + w > 1280 -> x = 1280 - w` — verschieben, nie
|
||
abweisen). `XframeConfig` um `crop: XframeCrop | null`, `zoom`, `readOnly`
|
||
erweitert; `resolveCrop` nimmt nur ein Nicht-null-Objekt mit vier endlichen
|
||
Zahlen (Typwaechter `isRecord`, kein Cast), `resolveZoom` liefert die
|
||
groesste Stufe <= n (unter 50 -> 50, nicht endlich -> 100), `resolveReadOnly`
|
||
nur bei `=== true`. Der Dateikopf erklaert, warum geklemmt und nie
|
||
abgewiesen wird (T-GE2-05) und dass `zoom` nur ohne Ausschnitt wirkt.
|
||
|
||
**Geometrie (`xframe-crop.ts`, NEU, ohne React).** `XFRAME_PREVIEW_PAGE_HEIGHT`
|
||
3000; `computeCropLayout(crop, tileW, tileH)` -> `{ scale, left, top,
|
||
frameHeight }` mit `scale = min(tileW/w, tileH/h)` (contain, darf > 1 sein),
|
||
Zentrierung im Rest, `frameHeight = max(y + h, 720)`; Nullkachel ->
|
||
`scale 0` (nichts rendern, keine Division durch 0). `applyCropDrag(mode,
|
||
start, dxPage, dyPage)`: `move` verschiebt bei fester Groesse und klemmt an
|
||
den Seitenraendern; `nw/ne/sw/se` bewegen nur die zwei Kanten ihrer Ecke,
|
||
die Gegenecke bleibt, Mindestgroesse an der bewegten Kante; Ergebnis durch
|
||
`clampXframeCrop`.
|
||
|
||
**Widget (`xframe-widget.tsx`).** Kachelkoerper mit `ref`, `overflow-hidden`
|
||
und — nur bei Ausschnitt — `data-tile-size="WxH"`; ein `useEffect` haengt
|
||
nur bei `crop !== null` einen `ResizeObserver` an (Muster dashboard-grid),
|
||
Aufraeumfunktion `disconnect()`. Drei Render-Zweige mit identischen
|
||
`frameAttrs` (src, title, sandbox, allow, referrerPolicy, loading,
|
||
data-testid, data-reload-nonce) und `key`: (1) Ausschnitt — Clip-`div`
|
||
(`xframe-crop-clip`, absolut, `overflow: hidden`, Groesse `w·scale ×
|
||
h·scale`, Position `left/top` aus dem Layout) mit dem `<iframe>` darin
|
||
(`left = -x·scale`, `top = -y·scale`, `width 1280`, `height frameHeight`,
|
||
`transform: scale(s)`, `transformOrigin 0 0`, Klasse ohne `h-full w-full`),
|
||
gerendert erst ab `scale > 0`; (2) Zoom ≠ 100 ohne Ausschnitt — absolut
|
||
positionierter Rahmen mit `width/height = 10000/z %` (60 % -> 166.67 %) und
|
||
`transform: scale(z)`; (3) 100 % — exakt wie bisher, kein `style`. Nach dem
|
||
Rahmen: `isEditMode` -> `xframe-edit-overlay`; `!isEditMode && readOnly` ->
|
||
`xframe-readonly-overlay` (`absolute inset-0`, `aria-hidden`); nie beide.
|
||
Ecksymbol-Link mit `z-10` bleibt ueber beiden. Dateikopf: warum der Rahmen
|
||
selbst verschoben und skaliert wird (cross-origin kein Scrollen von
|
||
aussen), warum die Layoutbreite fest 1280 ist, und dass readOnly
|
||
Bedienkomfort und keine Sicherheitsmassnahme ist (T-GE2-02).
|
||
|
||
**Formular (`xframe-config-form.tsx`).** Unter dem Intervall-Feld in dieser
|
||
Reihenfolge: Checkbox `xframe-crop-enable` (Einschalten -> EIN `onChange({
|
||
crop: XFRAME_CROP_DEFAULT, readOnly: true })`, Ausschalten -> `{ crop: null
|
||
}`); bei Ausschnitt Hinweis `cropPreviewHint`, `CropPreview` und
|
||
`CropNumberFields` (mit `key` aus den vier Werten -> State-Reset nach
|
||
Ziehen/Speichern); ohne Ausschnitt die Zoom-Auswahl `xframe-zoom`; Checkbox
|
||
`xframe-readonly`; dauerhaft `cropHint`. `CropPreview`: aeusserer Behaelter
|
||
420 px hoch mit eigenem Bildlauf, Breite per `ResizeObserver`, `p = width /
|
||
1280` (vor der Messung 0.5); Buehne `1280·p × 3000·p` mit dem Vorschau-
|
||
`<iframe>` (dieselbe `XFRAME_SANDBOX`, `allow=""`, `no-referrer`, `lazy`,
|
||
`width 1280`, `height 3000`, `transform: scale(p)`, `pointer-events: none`,
|
||
T-GE2-01/-03) und dem Rahmen (`<fieldset aria-label>`, `xframe-crop-rect`,
|
||
`cursor-move touch-none border-2 border-primary`) mit vier Griffen
|
||
(`xframe-crop-handle-{nw,ne,sw,se}`, `h-3 w-3 bg-primary`, Ecken-Cursor).
|
||
Pointer-Logik als Fabrik `dragProps(mode)`: `onPointerDown` nur Haupttaste,
|
||
Griffe `stopPropagation`, Zugzustand in `dragRef`, Capture per
|
||
`capturePointer` (typeof-Waechter); `onPointerMove` -> Entwurf aus
|
||
`applyCropDrag` mit Deltas `/ p`; `onPointerUp` -> Capture loesen, `onCommit`
|
||
nur bei Aenderung gegenueber `crop` (`sameCrop`); `onPointerCancel` ->
|
||
verwerfen ohne Aufruf; alle drei pruefen `drag.mode === mode`, damit das
|
||
Bubbling vom Griff zum Rahmen (jsdom ohne Capture) nichts doppelt
|
||
ausloest. Ohne Adresse statt der Buehne `xframe-crop-preview-empty`.
|
||
`CropNumberFields`: vier `type="number"`-Felder `xframe-crop-{x,y,w,h}` im
|
||
4er-Raster, Uebernahme bei Blur/Enter, `Number(draft)` endlich ->
|
||
`clampXframeCrop({ ...crop, [k]: n })`, leer/`abc`/unveraendert -> nichts;
|
||
darunter `cropUnitHint`.
|
||
|
||
**Test-Helfer (`apps/web/src/test/fake-resize-observer.ts`, NEU).**
|
||
`stubResizeObserver({ width, height })` stubbt `ResizeObserver` dateiweise
|
||
per `vi.stubGlobal` mit `DOMRectReadOnly`-`contentRect` — ein einziger Cast
|
||
`as ResizeObserverEntry`; Aufraeumen per `vi.unstubAllGlobals()`. Kein
|
||
`measuredSize`-Prop im Produktionscode.
|
||
|
||
**Uebersetzungen.** 13 neue Schluessel unter `widgets.xframe` in de UND en
|
||
(28 = 28 Schluessel gesamt, Gleichheit per Skript geprueft), Deutsch mit
|
||
„Sie“; `Ausschnitt` auf der Umlaut-Allowlist nach `neuem`.
|
||
|
||
**Doku (Commit 30fdd99).** Changelog-Stichpunkt direkt nach dem
|
||
XFrame-Stichpunkt unter „Unveroeffentlicht -> Neu“; Anwenderhandbuch: ein
|
||
Satz in der Tabellenzeile „XFrame“ und ein Satz am Ende des Absatzes
|
||
„Dashboard > Widgets“.
|
||
|
||
## Die Tests, und der Beleg dass sie rot waren
|
||
|
||
| Datei | Faelle | Rot-Lauf (vor der Umsetzung) |
|
||
|---|---:|---|
|
||
| `xframe-crop.test.ts` | 11 (neu) | `pnpm --filter @tessera/web exec vitest run src/components/dashboard/widgets/xframe-config.test.ts src/components/dashboard/widgets/xframe-crop.test.ts src/components/dashboard/widgets/xframe-widget.test.tsx` -> `Error: Failed to resolve import "./xframe-crop" from "src/components/dashboard/widgets/xframe-crop.test.ts". Does the file exist?` (0 test) |
|
||
| `xframe-config.test.ts` | 21 (12 + 9) | derselbe Lauf -> `21 tests \| 11 failed` (Tests 1 und 8 mit erweitertem `toEqual`, Tests 13–21 neu; Fehlerstellen Zeilen 26/78/97/106/111/118/128/139/146/161/172) |
|
||
| `xframe-widget.test.tsx` | 19 (12 + 7) | derselbe Lauf -> `19 tests \| 6 failed` (Tests 13, 14, 15, 16, 17, 19: `Unable to find an element by: [data-testid="xframe-crop-clip"]` bzw. `xframe-readonly-overlay`; Test 18 war schon gruen, weil er nur Abwesenheit prueft) — gesamt `Test Files 3 failed (3), Tests 17 failed \| 23 passed (40)` |
|
||
| `xframe-config-form.test.tsx` | 17 (8 + 9) | nach den Uebersetzungs-Schluesseln, vor dem Formular geschrieben: `pnpm --filter @tessera/web exec vitest run src/components/settings/xframe-config-form.test.tsx` -> `Test Files 1 failed (1), Tests 17 failed (17)` (der erweiterte `renderForm`-Helfer sucht die Labels `cropEnable`/`readOnly`, die das alte Formular nicht rendert — dadurch auch Tests 1–8 rot); erster Lauf nach der Umsetzung 17/17 |
|
||
|
||
Nach der Umsetzung: Resolver 21/21, Geometrie 11/11, Widget 19/19, Formular
|
||
17/17, `src/messages` 6/6, `widget-settings-panel.test.tsx` unveraendert
|
||
gruen (7 Dateien, 86 Faelle im `<verify>`-Lauf). Zusammen 36 neue Faelle;
|
||
Web-Tests 604 -> **640** (Plan: >= 634).
|
||
|
||
## Messungen (Endstand, HEAD 30fdd99)
|
||
|
||
| Groesse | Ausgang (5aa577a) | Jetzt |
|
||
|---|---:|---:|
|
||
| `pnpm type-check` | 4/4 | 4/4 |
|
||
| `pnpm lint` | 5/5 | 5/5 (api 74 Warnungen, web **53**, keine Stufe `error`) |
|
||
| API-Tests | 1175 | **1175** (75 Dateien) |
|
||
| Web-Tests | 604 | **640** (81 Dateien) |
|
||
| `as unknown as` in apps/api/src | 27 | 27 |
|
||
| `as unknown as` in apps/web/src | 6 | 6 (davon 1 in test/setup.ts) |
|
||
| `noNonNullAssertion` in apps/api/src (biome) | 56 | 56 |
|
||
| `noExplicitAny` in apps/api/src (biome) | 13 | 13 |
|
||
| `biome-ignore` in apps/api/src | 1 | 1 |
|
||
| `ts-expect-error` | 0 | 0 |
|
||
| `dangerouslySetInnerHTML` / `!` / `any` in den angefassten Web-Dateien | – | 0 / 0 / 0 |
|
||
| de/en-Schluesselgleichheit `widgets.xframe` | 15 = 15 | 28 = 28 |
|
||
|
||
Keine neue `any`, kein `!`, kein neues Paket (T-GE2-SC nicht ausgeloest),
|
||
kein Prisma-/Schema-Pfad, keine Aenderung an Panel, Registry, Katalog oder
|
||
API.
|
||
|
||
## Pruefliste fuer den Browser-Rundgang (Orchestrator, lokal, Playwright-MCP — NICHT Testserver)
|
||
|
||
- [x] (a) Einstellungen -> Dashboard -> XFrame mit `https://example.com`: „Nur einen Ausschnitt der Seite anzeigen“ anhaken -> genau EIN PATCH mit `crop: {x:0,y:0,w:1280,h:720}` UND `readOnly: true`; Vorschau (420 px hoch, scrollbar) zeigt die Seite bei 1280 px Breite mit blauem Rahmen ueber der ganzen Breite; Zahlenfelder zeigen 0/0/1280/720; die Zoom-Auswahl ist verschwunden
|
||
- [x] (b) Rahmen mit der Maus verschieben -> folgt fluessig, beim Loslassen genau EIN PATCH, Zahlenfelder springen auf die neuen Werte
|
||
- [x] (c) Ecke unten rechts ziehen -> Groesse aendert sich, obere linke Ecke bleibt, ein PATCH; Ecke oben links bis unter die Mindestbreite ziehen -> Rahmen bleibt 100 px breit, rechte Kante steht
|
||
- [x] (d) Zahlenfeld Breite auf 2000 + Feld verlassen -> Feld zeigt 1280, Links springt auf 0; Hoehe 10 -> 60
|
||
- [x] (e) Dashboard: die Kachel zeigt genau den Ausschnitt, eingepasst und zentriert (DOM: `xframe-crop-clip`, `data-tile-size` am Koerper, `<iframe>` mit `transform: scale(…)`); `sandbox`, `referrerpolicy=no-referrer`, `allow=""` unveraendert
|
||
- [x] (f) Kachel im Bearbeitungsmodus vergroessern/verkleinern -> nach dem Speichern skaliert der Ausschnitt mit und bleibt ganz sichtbar; Ziehen ueber dem Rahmen funktioniert weiterhin (`xframe-edit-overlay`)
|
||
- [x] (g) „Nur anzeigen“ an: Klick und Mausrad im Rahmen bewirken nichts (DOM: `xframe-readonly-overlay`), „In neuem Tab öffnen“ klickt weiterhin; aus: Seite bedienbar
|
||
- [x] (h) Ausschnitt abhaken -> PATCH `crop: null`, Zoom-Auswahl erscheint; Zoom 60 % -> Kachel zeigt die Seite verkleinert (DOM: `width: 166.67%`, `transform: scale(0.6)`), 100 % -> kein `style`
|
||
- [x] (i) verweigernde Seite (`https://www.google.com`) -> Vorschau und Kachel bleiben leer, Formular-Hinweise stehen; API-Log ohne Abruf der Fremdadresse
|
||
|
||
**Rundgang durch den Orchestrator am 22.09.2026 (lokaler Stack, Abbilder aus cf70a19, Playwright-MCP):** alle neun Punkte bestanden. Belege: (a) Anhaken → genau ein PATCH `{crop:{0,0,1280,720}, readOnly:true}`, Vorschau 403 px hoch mit Bildlaufleiste, Rahmen ueber die ganze Breite (Akzentfarbe gelb, nicht blau — die Kachelfarbe des Nutzers), Felder 0/0/1280/720, Zoom-Auswahl weg; (b) Verschieben folgt der Maus (200 px waehrend des Ziehens), beim Loslassen ein PATCH `{x:194,y:97,…}` (= 200/1,03), Felder springen mit; (c) Ecke unten rechts: obere linke Ecke bleibt (714/413), Groesse 620x310 → 739x389; Ecke oben links weit nach rechts: Breite bleibt 100 px (103 px Bildschirm), rechte Kante steht; (d) Breite 2000 → 1280 und Links → 0; Hoehe 10 → 60; (e) Kachel 933x601 zeigt genau den Ausschnitt 256/430/768/200 (`xframe-crop-clip` 933x243 bei top 179, `<iframe>` `left -311px; top -522px; scale(1.21)`), Sandbox/`no-referrer`/`allow=""` unveraendert; (f) Kachel auf 663x321 verkleinert → Ausschnitt skaliert auf 0,86 und bleibt ganz sichtbar, `xframe-edit-overlay` beim Ziehen da; (g) „Nur anzeigen“: Klick auf „Learn more“ im Rahmen bewirkt nichts (Frame bleibt example.com), `xframe-readonly-overlay` vorhanden, Link „In neuem Tab öffnen“ da; aus → Klick an der auf die Skalierung umgerechneten Stelle navigiert den Rahmen (Playwright selbst kann in einem per `transform` skalierten iframe nicht klicken — Werkzeuggrenze, per `elementFromPoint` + `mouse.click` umgangen); (h) Abhaken → PATCH `{crop:null}`, Zoom-Auswahl erscheint, 60 % → `width: 166.67%; height: 166.67%; transform: scale(0.6)`; (i) google.com → Vorschau-Rahmen und Kachel leer, beide Hinweise stehen, API-Log ohne Treffer.
|
||
|
||
**Ein Befund aus dem Rundgang, behoben in cf70a19:** die Kachel nutzte als Layouthoehe des Rahmens `max(crop.y + crop.h, 720)`, die Vorschau 3000 px. example.com setzt `margin: 15vh` — die Ueberschrift lag in der Vorschau bei y 450, in der Kachel mit 720 px Rahmen bei y 108; der in der Vorschau gewaehlte Ausschnitt haette in der Kachel etwas anderes gezeigt. Jetzt nutzt die Kachel dieselbe Layouthoehe wie die Vorschau (`XFRAME_PREVIEW_PAGE_HEIGHT`), damit vh-relative Seiten identisch umbrechen (Tests 4/13 angepasst). Zweitens ein 4-px-Querbalken in der Vorschau durch die Eckgriffe am rechten Rand → `overflow-x-hidden`. Web-Tests 640 unveraendert gruen.
|
||
|
||
## Deviations from Plan
|
||
|
||
1. **[Rule 3 - Blocking] Biome `useSemanticElements`** meldete den Rahmen
|
||
`<div role="group">` als neue Warnung (web 53 -> 54); `biome-ignore` ist
|
||
verboten. Der Rahmen ist jetzt ein `<fieldset aria-label=…>` (implizite
|
||
Gruppen-Rolle; `getByRole('group', { name })` findet ihn; Klassen
|
||
zusaetzlich `m-0 p-0 min-w-0` gegen Browser-Vorgaben). Formular-Test 10
|
||
prueft `tagName === 'FIELDSET'` statt `getAttribute('role')`. Die
|
||
Pointer-Handler nehmen `PointerEvent<HTMLElement>`. Web-Lint wieder 53.
|
||
Aufgabe 1, Commit 445b1d3.
|
||
2. **[Rule 3 - Blocking] `resolveCrop`:** die im Plan skizzierte Zuweisung
|
||
`const record: Record<string, unknown> = raw` kompiliert unter `strict`
|
||
nicht (`object` ohne Index-Signatur, TS2322). Statt eines Casts ein
|
||
Typwaechter `isRecord(raw): raw is Record<string, unknown>`. Aufgabe 1,
|
||
Commit 445b1d3.
|
||
3. **Zaehler-Falle:** der Kopfkommentar des Test-Helfers enthielt die
|
||
Phrase „No `as unknown as`“ und liess den grep-Zaehler auf 7 springen;
|
||
umformuliert („no double cast through `unknown`“). Kein Code betroffen.
|
||
4. **`onPointerCancel` prueft wie Move/Up den Zugmodus** (`drag.mode ===
|
||
mode`), damit ein Abbruch am Griff nicht zusaetzlich den Rahmen-Handler
|
||
durchlaeuft (Bubbling ohne Capture in jsdom). Verhalten im Test 14
|
||
unveraendert.
|
||
5. **Commit-Nachricht von Aufgabe 1 einmal per `--amend` korrigiert**
|
||
(vor jedem weiteren Commit, nichts baute darauf): die Testzahl stand
|
||
zunaechst als 638, gemessen sind 640. Der endgueltige Hash ist 445b1d3.
|
||
6. **Zwei Widget-Faelle mehr** als die geforderten sechs (Test 18 und 19
|
||
getrennt), ein Resolver-Fall mehr (Test 21 `clampXframeCrop`).
|
||
|
||
Nicht geaendert: `STATE.md`, `ROADMAP.md`, `.planning/**` (ausser dieser
|
||
Akte), keine neue Abhaengigkeit, kein Deploy, kein Zugriff auf den
|
||
Testserver. Ein lokaler Docker-Stack wurde nicht angefasst.
|
||
|
||
## Known Stubs
|
||
|
||
Keine. Kette verdrahtet: Checkbox -> `onChange({ crop, readOnly })` ->
|
||
`PATCH /dashboard/widgets/:id/config` (flache Zusammenfuehrung, bestehend)
|
||
-> `resolveXframeConfig` -> `crop !== null` -> `ResizeObserver` ->
|
||
`computeCropLayout` -> Clip + `<iframe style>`; Rahmen `pointerdown/move/up`
|
||
-> `applyCropDrag` -> Entwurf -> `clampXframeCrop` -> EIN `onChange`;
|
||
Zahlenfelder -> `clampXframeCrop` -> `onChange`; `zoom` -> Prozentmasse +
|
||
`scale`; `readOnly` -> Flaeche nur im Ansichtsmodus.
|
||
|
||
## Threat Flags
|
||
|
||
Keine neue Flaeche ausserhalb des `<threat_model>` des Plans: T-GE2-01
|
||
(beide Rahmen mit exakt `XFRAME_SANDBOX`, `allow=""`, no-referrer —
|
||
Widget-Test 13/15, Formular-Test 10), T-GE2-03 (`pointer-events: none` der
|
||
Vorschau — Formular-Test 10), T-GE2-04 (keine API-Aenderung — Rundgang i),
|
||
T-GE2-05 (Klemmung — Resolver-Tests 13–17, Geometrie-Test 5), T-GE2-06
|
||
(nur Zahlen/Template-Strings in Styles, Texte aus Sprachdateien), T-GE2-02
|
||
und -07 im Dateikopf des Widgets bzw. Formulars ausdruecklich als
|
||
Bedienkomfort/Capture ohne Rechte benannt.
|
||
|
||
## Self-Check: PASSED
|
||
|
||
Alle 3 neu angelegten Dateien liegen auf der Platte, die zwei Commits
|
||
445b1d3 und 30fdd99 sind in `git log` auffindbar
|
||
(`git rev-list --count 5aa577a..HEAD` = 2). Die Zahlen der Tabelle stammen
|
||
aus tatsaechlich gelaufenen Befehlen.
|