enhancement
major
minor
major
minor
Continues #29875 (wave 5a, inputs). Wave 5 is cut into four parts with a ticket each (5a inputs, 5b media, 5c planning and drag, 5d foundation and removal of tlReactControls.css), so that a release between the merges does not close the ticket of an open part.
Problem
TLAudioRecorder, TLAudioPlayer, TLPhotoCapture, the file chips, TLDownload, TLImage, TLPdfViewer, the HTML document view and TLJobStatus are styled by the old stylesheet tlReactControls.css (about 830 lines). The design system package's forecast (docs/migration/altlast.md, "Welle 5", V2, V3, V4, V10, V14, V18) found:
- Round icon buttons: recorder, player and camera draw their own 2.5rem round buttons with an own inset focus and outline: none; the photo overlay three more. Their icons are drawn from borders and backgrounds or inline SVG and do not scale with the icon system.
- Upload: six files build FormData themselves, the status formula and local status exist several times, three drop handlers (the file chips with their own look), three blob downloads next to download.ts.
- Empty, loading and error states are built five times, error lines in recorder and photo are not announced (role=alert missing), three own spinner/wobble animations.
- Floating buttons over a surface (print button of the HTML document) have their own shell.
- Animations ignore prefers-reduced-motion.
- TLCounter is dead (no Java control, only registered); the rule tlPhotoCapture__cameraBtn--active is never set.
Solution
Spec in the package: docs/superpowers/specs/2026-10-19-welle-5b-medien-design.md. No new interaction concept (that comes with the UI pass after wave 5).
- Recorder, player and camera are part buttons (icon-only, icon system glyphs, one focus ring, aria-busy while loading or uploading). A running recording is a pressed button filled red (tl-button--danger[aria-pressed]), without motion.
- The camera opens as a client-side design system dialog (portal, focus on "capture", Enter captures, Escape closes only the camera dialog even inside a window, focus returns to the camera button); mirroring is a pressed toggle. The recorder stops the microphone when it is unmounted while recording; denied access is announced.
- One upload hook useUpload and one downloadBlob replace the copies in file upload, binary field, file chips, download, recorder and camera. Dragging looks the same on the upload field and on the file chips; every upload error is announced (role="alert"); FileListControl reports error and maxUploadSize.
- Embedded content shares tl-media (image, PDF, HTML document and preview, camera video). Foreign HTML lies on surface-document, white in both modes; the print button floats over the document in tl-overlay__action. Embedded HTML reads the type scale (tl-prose).
- Wizard and job status share one step list tl-steps; the job status announces only state, current step and message, not the ticking elapsed time; errors as role="alert".
- New view elements <audio-recorder>, <audio-player>, <photo-capture>, <download> (attribute input, a channel holding the binary data) and a demo view "Media".
- TLCounter removed.
Migration
The media controls now draw with the design system package. Application stylesheets that address the old classes must move to the new ones.
Removed classes: tlAudioRecorder*, tlAudioPlayer*, tlPhotoCapture*, tlDownload*, tlFileChips*, tlBinaryField*, tlImage*, tlPdfViewer*, tlHtml*, tlJobStatus*, tlCounter*, tl-wizard__steps, tl-wizard__step-item, tl-wizard__disc, tl-wizard__step-name. The React control TLCounter is removed.
States are attributes, not classes: recording [aria-pressed="true"], uploading/loading [aria-busy="true"], dragging [data-tl-state~="dragover"], empty file [data-tl-state~="empty"], mirrored video [data-tl-state~="mirrored"], finished step [data-tl-state~="done"], current step [aria-current="step"].
Old → new:
| = Old = | = New = |
| .tlAudioRecorder, __button, --recording, --uploading, __icon | .tl-upload with part Button (tl-button--danger, [aria-pressed="true"], [aria-busy="true"]) |
| .tlAudioPlayer, __button, --playing, --loading | part Button ([aria-busy="true"]) |
| .tlPhotoCapture, __cameraBtn, __overlayContent, __overlayVideo--mirrored, __overlayToolbar | .tl-upload, .tl-dialog__surface, .tl-media__frame[data-tl-state~="mirrored"], .tl-button-bar |
| .tlAudioRecorder__status--error, .tlPhotoCapture__status--error | .tl-file-upload__status[role="alert"] |
| .tlDownload, --empty, __fileName, __fileName--empty | .tl-file, [data-tl-state~="empty"], .tl-file__name |
| .tlFileChips, --dragover, __add--uploading | .tl-chip-group, [data-tl-state~="dragover"], [aria-busy="true"] |
| .tlImage, --fit-cover, --fit-contain, __image, __placeholder | .tl-media, --cover, --contain, __frame, __placeholder |
| .tlPdfViewer, __frame, __placeholder | .tl-media, __frame, .tl-status |
| .tlHtml--document, __frame, __print, --thumbnail, __page, --inline | .tl-overlay--fill, .tl-media--document, .tl-overlay__action, .tl-media--preview, .tl-media__frame, .tl-prose |
| .tlJobStatus, __header, __phases, __phase--done/active/pending, __bar, __error | .tl-job-status, __header, .tl-steps--vertical, [data-tl-state~="done"] / [aria-current="step"], __bar, __error[role="alert"] |
| .tl-wizard__steps, __step-item, __disc, __step-name | .tl-steps, __item, __disc, __name |
The complete table is in the design system repository, docs/migration/altlast.md, section "Welle 5b".
Visible changes: media buttons are square icon buttons like every other icon button; recording shows a filled red button instead of a pulsing dot; the camera is a regular dialog with title, close button and button bar (no buttons over the video); "no file" is secondary text, no longer italic or in the disabled colour; the job status' steps use the numbered discs of the wizard (no green for finished steps); foreign HTML documents stay white in dark mode; embedded HTML uses the design system's type scale.