frontend/src/client/components/modal.ts — new openModal() primitive,
native <dialog>-backed. The browser handles top-layer stacking, ESC,
ARIA, and focus trap. We layer on top:
- browser back-button closes the modal (history.pushState on open +
popstate listener, matching m's Q5 lock-in)
- focus restoration to whatever was focused before open (the native
<dialog> doesn't do this)
- backdrop click closes
- close (×) button mandatory in the header, always rendered
CSS (global.css):
- dialog.modal + .modal__{header,title,close,body,footer} block. Sizes
sm/md/lg/full via data-size attr.
- Phone breakpoint (≤32rem): full-screen takeover sitting ABOVE the
PWA bottom-nav. max-height accounts for --bottom-nav-height (56px)
and margin-bottom keeps the nav visible.
- Legacy .modal-overlay / .modal-card / .modal-content / .modal stay
in place for the ~7 unmigrated modals — the new BEM-style .modal__*
avoids colliding with the legacy hierarchy. Cleanup is a follow-up
PR after the last legacy modal flips.
i18n keys + i18n-keys.ts regenerated:
- modal.close.label (DE/EN)
- approvals.suggest.section.editable / .context (DE/EN)
- approvals.suggest.context.{project,requester,requested_at,approval_status} (DE/EN)
- approvals.suggest.field.{original_due_date,warning_date,rule_code,description} (DE/EN)
- approvals.suggest.event_type_picker_unavailable (DE/EN)
(Slice C consumes the suggest.section/context/field keys; bundling them
here keeps the i18n.ts diff coherent.)