Adopt shared WebUI structural primitives

This commit is contained in:
2026-08-18 13:17:31 +02:00
parent 178888c7ac
commit e4d6dff1d7
2 changed files with 35 additions and 115 deletions
@@ -2,15 +2,20 @@ import { useEffect, useMemo, useState } from "react";
import { Bell, Check, ExternalLink, RefreshCw, Send, XCircle } from "lucide-react"; import { Bell, Check, ExternalLink, RefreshCw, Send, XCircle } from "lucide-react";
import { import {
ActionBlockerHint, ActionBlockerHint,
ActionToolbar,
AdminIconButton, AdminIconButton,
Button, Button,
ConfirmDialog, ConfirmDialog,
CountBadge,
DismissibleAlert, DismissibleAlert,
DocumentationHelpLink, DocumentationHelpLink,
SegmentedControl, SegmentedControl,
SelectionList, SelectionList,
SelectionListItem, SelectionListItem,
StatePanel,
StatusBadge, StatusBadge,
WorkspaceFrame,
WorkspaceLayout,
hasScope, hasScope,
i18nMessage, i18nMessage,
type ApiSettings, type ApiSettings,
@@ -152,7 +157,7 @@ export default function NotificationCenterPage({ settings, auth }: { settings: A
if (!canRead) { if (!canRead) {
return ( return (
<main className="notifications-page"> <WorkspaceFrame as="main" height="viewport" surface="plain" className="notifications-page" label="Notification center">
<div className="notifications-permission-state"> <div className="notifications-permission-state">
<ActionBlockerHint <ActionBlockerHint
tone="warning" tone="warning"
@@ -167,19 +172,31 @@ export default function NotificationCenterPage({ settings, auth }: { settings: A
documentation={NOTIFICATIONS_DOCUMENTATION} documentation={NOTIFICATIONS_DOCUMENTATION}
/> />
</div> </div>
</main> </WorkspaceFrame>
); );
} }
return ( return (
<main className="notifications-page"> <WorkspaceFrame as="main" height="viewport" surface="plain" className="notifications-page" label="Notification center">
<div className="notifications-shell"> <WorkspaceLayout
<aside className="notifications-sidebar"> variant="split"
<div className="notifications-sidebar-bar"> primarySize="default"
primaryScrollable={false}
contentScrollable={false}
surface="contained"
primaryClassName="notifications-sidebar"
contentClassName="notifications-workspace"
primaryLabel="i18n:govoplan-notifications.notifications"
contentLabel="i18n:govoplan-notifications.surface.center"
interfaceId="notifications.center.workspace"
helpContextId="notifications.page.center"
helpModuleId="notifications"
primary={<>
<ActionToolbar surface="panel-header" className="notifications-sidebar-bar">
<div className="notifications-title"> <div className="notifications-title">
<Bell size={17} /> <Bell size={17} />
<strong>i18n:govoplan-notifications.notifications</strong> <strong>i18n:govoplan-notifications.notifications</strong>
{unreadCount > 0 ? <span className="notifications-count">{unreadCount}</span> : null} {unreadCount > 0 ? <CountBadge>{unreadCount}</CountBadge> : null}
</div> </div>
<AdminIconButton <AdminIconButton
label="i18n:govoplan-notifications.refresh" label="i18n:govoplan-notifications.refresh"
@@ -188,7 +205,7 @@ export default function NotificationCenterPage({ settings, auth }: { settings: A
disabled={loading || busy} disabled={loading || busy}
disabledReason={loading ? NOTIFICATIONS_I18N.loading : busy ? NOTIFICATIONS_I18N.actionActive : undefined} disabledReason={loading ? NOTIFICATIONS_I18N.loading : busy ? NOTIFICATIONS_I18N.actionActive : undefined}
/> />
</div> </ActionToolbar>
<SegmentedControl <SegmentedControl
className="notifications-status-filter" className="notifications-status-filter"
options={statusFilters.map((status) => ({ id: status, label: statusLabel(status) }))} options={statusFilters.map((status) => ({ id: status, label: statusLabel(status) }))}
@@ -201,7 +218,7 @@ export default function NotificationCenterPage({ settings, auth }: { settings: A
{loading ? <div className="notifications-note">i18n:govoplan-notifications.loading_notifications</div> : null} {loading ? <div className="notifications-note">i18n:govoplan-notifications.loading_notifications</div> : null}
{!loading && notifications.length === 0 ? <div className="notifications-note">i18n:govoplan-notifications.no_notifications</div> : null} {!loading && notifications.length === 0 ? <div className="notifications-note">i18n:govoplan-notifications.no_notifications</div> : null}
{notifications.length > 0 ? ( {notifications.length > 0 ? (
<SelectionList label="i18n:govoplan-notifications.notifications" className="notifications-selection-list"> <SelectionList variant="navigation" label="i18n:govoplan-notifications.notifications" className="notifications-selection-list">
{notifications.map((notification) => ( {notifications.map((notification) => (
<SelectionListItem <SelectionListItem
key={notification.id} key={notification.id}
@@ -222,9 +239,9 @@ export default function NotificationCenterPage({ settings, auth }: { settings: A
</SelectionList> </SelectionList>
) : null} ) : null}
</div> </div>
</aside> </>}
<section className="notifications-workspace"> >
<div className="notifications-topbar"> <ActionToolbar surface="panel-header" className="notifications-topbar">
<div className="notifications-title-line"> <div className="notifications-title-line">
<Bell size={18} /> <Bell size={18} />
<strong>{selected?.subject || selected?.event_kind || "i18n:govoplan-notifications.surface.center"}</strong> <strong>{selected?.subject || selected?.event_kind || "i18n:govoplan-notifications.surface.center"}</strong>
@@ -244,7 +261,7 @@ export default function NotificationCenterPage({ settings, auth }: { settings: A
<Send size={16} /> i18n:govoplan-notifications.dispatch_pending <Send size={16} /> i18n:govoplan-notifications.dispatch_pending
</Button> </Button>
</div> </div>
</div> </ActionToolbar>
{error ? <DismissibleAlert tone="danger" compact resetKey={error}>{error}</DismissibleAlert> : null} {error ? <DismissibleAlert tone="danger" compact resetKey={error}>{error}</DismissibleAlert> : null}
@@ -264,14 +281,9 @@ export default function NotificationCenterPage({ settings, auth }: { settings: A
) : null} ) : null}
{selected ? <NotificationDetails notification={selected} /> : ( {selected ? <NotificationDetails notification={selected} /> : (
<div className="notifications-empty-state"> <StatePanel size="fill" icon={<Bell size={22} />} title="i18n:govoplan-notifications.notifications" description="i18n:govoplan-notifications.select_notification_help" />
<Bell size={22} />
<h1>i18n:govoplan-notifications.notifications</h1>
<p>i18n:govoplan-notifications.select_notification_help</p>
</div>
)} )}
</section> </WorkspaceLayout>
</div>
<ConfirmDialog <ConfirmDialog
open={confirmingAction === "cancel"} open={confirmingAction === "cancel"}
title="i18n:govoplan-notifications.cancel_delivery_title" title="i18n:govoplan-notifications.cancel_delivery_title"
@@ -291,7 +303,7 @@ export default function NotificationCenterPage({ settings, auth }: { settings: A
onCancel={() => setConfirmingAction(null)} onCancel={() => setConfirmingAction(null)}
onConfirm={() => void confirmAction()} onConfirm={() => void confirmAction()}
/> />
</main> </WorkspaceFrame>
); );
} }
@@ -330,10 +342,10 @@ function NotificationDetails({ notification }: { notification: NotificationMessa
</section> </section>
<section className="notifications-attempts"> <section className="notifications-attempts">
<div className="notifications-section-heading"> <ActionToolbar surface="section-header" className="notifications-section-heading">
<h2>i18n:govoplan-notifications.delivery_attempts</h2> <h2>i18n:govoplan-notifications.delivery_attempts</h2>
<DocumentationHelpLink reference={NOTIFICATIONS_DELIVERY_DOCUMENTATION} /> <DocumentationHelpLink reference={NOTIFICATIONS_DELIVERY_DOCUMENTATION} />
</div> </ActionToolbar>
{notification.attempts.length === 0 ? <p className="muted">i18n:govoplan-notifications.no_delivery_attempt</p> : null} {notification.attempts.length === 0 ? <p className="muted">i18n:govoplan-notifications.no_delivery_attempt</p> : null}
{notification.attempts.map((attempt) => ( {notification.attempts.map((attempt) => (
<div className="notifications-attempt" key={attempt.id}> <div className="notifications-attempt" key={attempt.id}>
-92
View File
@@ -1,18 +1,3 @@
.notifications-page {
box-sizing: border-box;
height: calc(100vh - 115px);
min-height: 0;
overflow: hidden;
color: var(--text);
background: var(--bg);
}
.notifications-page *,
.notifications-page *::before,
.notifications-page *::after {
box-sizing: border-box;
}
.notifications-widget-actions { .notifications-widget-actions {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -21,28 +6,14 @@
margin-top: 14px; margin-top: 14px;
} }
.notifications-shell {
height: 100%;
min-height: 0;
display: grid;
grid-template-columns: minmax(270px, 340px) minmax(0, 1fr);
border: var(--border-line);
background: var(--panel);
overflow: hidden;
}
.notifications-sidebar { .notifications-sidebar {
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-right: var(--border-line);
background: var(--panel-soft);
} }
.notifications-sidebar-bar,
.notifications-title, .notifications-title,
.notifications-topbar,
.notifications-title-line, .notifications-title-line,
.notifications-actions, .notifications-actions,
.notifications-message-meta, .notifications-message-meta,
@@ -52,27 +23,6 @@
gap: 8px; gap: 8px;
} }
.notifications-sidebar-bar,
.notifications-topbar {
min-height: 54px;
justify-content: space-between;
border-bottom: var(--border-line);
background: var(--panel-header);
padding: 9px 12px;
}
.notifications-count {
min-width: 21px;
height: 21px;
display: inline-grid;
place-items: center;
border-radius: 999px;
background: var(--accent);
color: var(--on-accent);
font-size: 12px;
font-weight: 800;
}
.notifications-status-filter { .notifications-status-filter {
width: calc(100% - 16px); width: calc(100% - 16px);
margin: 8px; margin: 8px;
@@ -244,17 +194,6 @@
font-size: 15px; font-size: 15px;
} }
.notifications-section-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.notifications-section-heading h2 {
margin: 0;
}
.notifications-properties dl { .notifications-properties dl {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(180px, 1fr)); grid-template-columns: repeat(2, minmax(180px, 1fr));
@@ -307,44 +246,13 @@
color: var(--muted); color: var(--muted);
} }
.notifications-empty-state {
min-height: 100%;
display: grid;
place-items: center;
align-content: center;
gap: 8px;
padding: 32px;
text-align: center;
}
.notifications-permission-state { .notifications-permission-state {
max-width: 860px; max-width: 860px;
margin: 0 auto; margin: 0 auto;
padding: 32px 20px; padding: 32px 20px;
} }
.notifications-empty-state h1 {
margin: 0;
color: var(--text-strong);
}
.notifications-empty-state p {
max-width: 520px;
margin: 0;
color: var(--muted);
}
@media (max-width: 900px) { @media (max-width: 900px) {
.notifications-shell {
grid-template-columns: 1fr;
}
.notifications-sidebar {
min-height: 220px;
border-right: 0;
border-bottom: var(--border-line);
}
.notifications-topbar { .notifications-topbar {
align-items: flex-start; align-items: flex-start;
flex-direction: column; flex-direction: column;