feat(ui): one dialog frame, a shared dialog shell and three width presets (#1942)

* feat(ui): one dialog frame, a shared dialog shell and three width presets

Every dialog gets the kit frame from m3-theme.scss: a 16px panel on the
widget surface with a hairline border and the popover shadow, over a tinted
backdrop blurred at 6px. The command palette and the programme dialog keep
their own surface through their panel classes.

`app-dialog-shell` (@iptvnator/ui/components) draws the header (optional
icon tile, the heading as mat-dialog-title, subtitle, a header-end slot and
a close button), the body and a footer that renders only when an element
marked appDialogFooter wraps the actions. The close button is last in the
DOM, so first-tabbable autofocus lands in the body. Footer buttons take the
kit look: ghost, accent (new accent-button mixin) or secondary; destructive
buttons keep their tint and get the kit's 10px radius.

`dialogSize('sm' | 'md' | 'lg')` owns the width and viewport limits with a
type-only MatDialogConfig import; MAT_DIALOG_DEFAULT_OPTIONS is deliberately
not provided at the root, which would put Material's dialog back on the
startup path.

Migrated: the confirm dialog (sm), the add-playlist dialog (lg) and the
keyboard shortcuts dialog (lg, close button only). Slot wrappers render as
display: contents, because a control-flow block at the top level of the
content is projected into the body, not into a named slot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(ui): name the dialog shell's close button "Close dialog"

A confirm dialog opened with a "Close" dismiss (the download recovery
prompt, the VOD download prompt) had two buttons named "Close" once the
shell added its header ×, and the Electron download-recovery test failed
on a strict-mode violation for `getByRole('button', { name: 'Close' })`.
The × is now named by the new `CLOSE_DIALOG` key ("Close dialog") in every
locale, so a footer "Close" stays the only button of that name; the same
holds for the dialogs PR 3 migrates, whose E2E flows select "Close".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(ui): let short windows scroll the whole dialog shell

With the shell capped at the viewport, a stacked footer (the add-playlist
Xtream mode has five rows) left the body a 40px sliver at 360×480. Under
600px of viewport height the cap comes off and Material's surface scrolls
the whole dialog as the single scroll owner, which is how the dialog
behaved before the shell.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 authored and GitHub committed 2026-10-14 23:29:24 +02:00
1 parent 9b5586d328
commit 2139ddd5c0
47 files changed
+904 -451

No files matched your search

+40 -7
View File
@@ -86,6 +86,8 @@ the subsystem's document.
`libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.scss`
- Shared list selection style:
`libs/ui/styles/_nav-list.scss`
- Dialog shell and size presets:
`libs/ui/components/src/lib/dialog-shell/`
- Source row (Sources page and playlist switcher):
`libs/playlist/shared/ui/src/lib/source-row/source-row.component.html`
- UI-kit buttons, menus and segmented control:
@@ -1133,8 +1135,33 @@ per-theme tokens in `apps/web/src/_settings-theme.scss`.
strut, as the programme dialog does.
- **Order.** Dismiss first, primary last. With end alignment the primary sits
on the inline end: the right in LTR, the left in RTL.
- **One dismiss.** Offer one visible dismiss: a footer "Cancel"/"Close" or a
header close icon, not both. Escape and the backdrop still close.
- **Shell.** A dialog's markup is `app-dialog-shell`
(`@iptvnator/ui/components`): the heading (its `mat-dialog-title`, so
the container is named by it), an optional subtitle and 40px icon tile
(`iconTone="warn"` for a destructive question), the body as default
content, and a footer rendered only when an element with
`appDialogFooter` wraps the actions (`appDialogFooterStart` wraps a Clear
action or a notice at the footer's start, `appDialogHeaderEnd` a chip
before the close button); a conditional action sits inside the wrapper,
since a control-flow block at the top level is projected into the body.
Footer
buttons take the kit look from the shell: `mat-button` ghost,
`mat-flat-button` accent, `mat-flat-button.app-dialog__secondary`
secondary; `.app-destructive-button` keeps its tint. The frame itself
(16px panel on the widget surface, hairline border, popover shadow,
tinted blurred backdrop) is global in `m3-theme.scss` and reaches every
dialog; a dialog that draws its own surface strips it through its panel
class, as the command palette and the programme dialog do.
The shell caps itself at the viewport minus the preset's margins and
lets only the body scroll; under 600px of height the cap comes off and the
surface scrolls the whole dialog, so a stacked footer never squeezes the
body to a sliver.
- **Dismiss.** The shell's header × (named "Close dialog", so a footer
"Close" stays the only button of that name) and the footer
"Cancel"/"Close" both dismiss; a dialog never adds a second footer
dismiss. The × is the last
element in the DOM, so `first-tabbable` autofocus lands in the body and
Tab reaches it after the footer. Escape and the backdrop still close.
- **One row.** Action labels are short verbs ("Cancel", "Discard", "Save"),
not phrases, so the row fits one line in every locale at the dialog width.
Secondary actions tied to one part of the content stay with that content
@@ -1147,11 +1174,17 @@ per-theme tokens in `apps/web/src/_settings-theme.scss`.
locales. The programme dialog stacks its archive tools and footer the same
way, letting a long label wrap inside its button; the Electron
`epg-timeline-interaction.e2e.ts` measures it in French at 360px.
- **Width.** A dialog with several openers is opened through one helper that
owns its `MatDialogConfig`, so its width never depends on the entry point.
`EpgProgrammeDialogService` opens the programme dialog at 540px from the
timeline, list, guide and channel rows, with a panel class that scopes its
surface overrides.
- **Width.** An opener spreads a size preset first, `...dialogSize('sm' |
'md' | 'lg')` (440, 560 and 760px with the viewport limits), then its own
options. `sm` is a confirmation or a PIN prompt, `md` a form or a list,
`lg` a picker with a grid, a table or long text. The helper imports
`MatDialogConfig` as a type only, so a root service may use it without
putting Material's dialog on the startup path; `MAT_DIALOG_DEFAULT_OPTIONS`
is never provided at the root for that reason. A dialog with several
openers is opened through one helper that owns its config, so its width
never depends on the entry point: `EpgProgrammeDialogService` opens the
programme dialog at 540px from the timeline, list, guide and channel rows,
with a panel class that scopes its surface overrides.
- **Destructive actions.** Material only emits `warn` button colors for M2
themes, so the `color` input is a no-op here. A button that removes or
discards user data uses the global `.app-destructive-button` class from