v4: /settings and /manage take after /notifications — the three pages the user menu and the bell lead to share one frame: a 680px column (PageShell's new narrow), a title with its icon (PageHeader: ⚙️, 🛠️, 🔔), then groups under a small capitalized label ruled to the column's end (PageGroup, the notifications' day label), their content on surface cards (.page-card, the notification items' surface). Settings drew big section headings over controls laid on the bare page, the full width wide, and Manage the same around the category manager; each section is now a card whose setting rows are ruled apart and keep their labels lined up (a subgrid of the card's columns). The notifications' column, centered by auto margins in a flex column, shrank to its content (380px with short ones); it fills its width now. Its own header, group and bell rules gave way to the shared ones, and Times, Brutalist and GeoCities dress .page-card as they dress the notification items. In the narrower column, Kanagawa's monospace no longer fit the six styles on one row, and French labels leave even less room: a segmented control of more than four options now makes two even rows at any width, as it did on phones. 670 frontend tests pass, and tsc, eslint, deno lint, deno fmt and doc:lint are clean; checked in screenshots in all six styles at 1280 and 390px, Smooth in light too, with the account's style and the notifications mocked so the shared test account was left as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-08 21:40:10 +00:00
parent 1fda26a399
commit 45f39a959b
11 changed files with 246 additions and 143 deletions

View File

@@ -11,7 +11,8 @@ import { can } from "../utils/auth/permissions.ts";
import { useRequiredAuth } from "../hooks/auth/useAuth.ts";
import { useDocumentTitle } from "../hooks/shared/useDocumentTitle.ts";
import { PageShell } from "../components/layout/PageShell.tsx";
import { ProfileSection } from "../components/profile/ProfileSection.tsx";
import { PageHeader } from "../components/layout/PageHeader.tsx";
import { PageGroup } from "../components/layout/PageGroup.tsx";
import { CategoryManager } from "../components/category/CategoryManager.tsx";
import { NotFound } from "./NotFound.tsx";
@@ -24,15 +25,13 @@ export function Manage(): React.JSX.Element {
const { user } = useRequiredAuth();
if (!can(user, "category:manage")) return <NotFound />;
return (
<PageShell>
<div className="page-header">
<h1 className="page-title">
<Trans>Manage</Trans>
</h1>
</div>
<ProfileSection title={<Trans>Categories</Trans>}>
<CategoryManager />
</ProfileSection>
<PageShell narrow>
<PageHeader icon="🛠️" title={<Trans>Manage</Trans>} />
<PageGroup label={<Trans>Categories</Trans>}>
<div className="page-card">
<CategoryManager />
</div>
</PageGroup>
</PageShell>
);
}

View File

@@ -47,6 +47,8 @@ import {
useScrollSave,
} from "../hooks/feed/useScrollSave.ts";
import { PageShell } from "../components/layout/PageShell.tsx";
import { PageHeader } from "../components/layout/PageHeader.tsx";
import { PageGroup } from "../components/layout/PageGroup.tsx";
import { dumpPath, playlistPath, userPath } from "../../shared/paths.ts";
type NotifIconKind =
@@ -488,63 +490,57 @@ export function Notifications(): React.JSX.Element {
const totalUnread = items.filter((n) => !n.read).length;
return (
<PageShell>
<div className="notifications-page">
<div className="page-header">
<h1 className="page-title">
<span className="notifications-title-bell" aria-hidden="true">
🔔
</span>
<Trans>Notifications</Trans>
</h1>
{totalUnread > 0 && (
<span className="notifications-unread-pill">
<Plural value={totalUnread} one="# new" other="# new" />
</span>
)}
</div>
<PageShell narrow>
<PageHeader icon="🔔" title={<Trans>Notifications</Trans>}>
{totalUnread > 0 && (
<span className="notifications-unread-pill">
<Plural value={totalUnread} one="# new" other="# new" />
</span>
)}
</PageHeader>
<ListState
state={state}
isEmpty={() => items.length === 0}
empty={
<div className="notifications-empty">
<span className="notifications-empty-icon" aria-hidden="true">
🔕
</span>
<p>
<Trans>Nothing here yet.</Trans>
</p>
<p className="notifications-empty-hint">
<Trans>
You'll be notified when someone follows your playlists,
upvotes your dumps, or posts new content.
</Trans>
</p>
</div>
}
>
{({ data }) => (
<>
{groupByDate(items).map((group) => (
<section key={group.label} className="notif-group">
<h2 className="notif-group-label">{group.label}</h2>
<ul className="notification-list">
{group.items.map((n) => (
<NotificationItem key={n.id} n={n} />
))}
</ul>
</section>
))}
<FeedFooter
hasMore={data.hasMore}
loadingMore={data.loadingMore}
sentinelRef={sentinelRef}
/>
</>
)}
</ListState>
</div>
<ListState
state={state}
isEmpty={() => items.length === 0}
empty={
<div className="notifications-empty">
<span className="notifications-empty-icon" aria-hidden="true">
🔕
</span>
<p>
<Trans>Nothing here yet.</Trans>
</p>
<p className="notifications-empty-hint">
<Trans>
You'll be notified when someone follows your playlists, upvotes
your dumps, or posts new content.
</Trans>
</p>
</div>
}
>
{({ data }) => (
<>
{groupByDate(items).map((group) => (
<PageGroup key={group.label} label={group.label}>
<ul className="notification-list">
{group.items.map((n) => (
<NotificationItem
key={n.id}
n={n}
/>
))}
</ul>
</PageGroup>
))}
<FeedFooter
hasMore={data.hasMore}
loadingMore={data.loadingMore}
sentinelRef={sentinelRef}
/>
</>
)}
</ListState>
</PageShell>
);
}

View File

@@ -22,12 +22,13 @@ import { useDocumentTitle } from "../hooks/shared/useDocumentTitle.ts";
import { EMAIL_ENABLED } from "../config/instance.ts";
import { previewChime } from "../utils/shared/sound.ts";
import { PageShell } from "../components/layout/PageShell.tsx";
import { PageHeader } from "../components/layout/PageHeader.tsx";
import { PageGroup } from "../components/layout/PageGroup.tsx";
import {
Segmented,
type SegmentedOption,
} from "../components/shared/Segmented.tsx";
import { ChangePasswordModal } from "../components/profile/ChangePasswordModal.tsx";
import { ProfileSection } from "../components/profile/ProfileSection.tsx";
import { SettingRow } from "../components/profile/SettingRow.tsx";
// The styles' names are their own, in every language.
@@ -77,14 +78,10 @@ export function Settings(): React.JSX.Element {
const [changePasswordOpen, setChangePasswordOpen] = useState(false);
return (
<PageShell>
<div className="page-header">
<h1 className="page-title">
<Trans>Settings</Trans>
</h1>
</div>
<ProfileSection title={<Trans>Account</Trans>}>
<div className="profile-appearance-grid">
<PageShell narrow>
<PageHeader icon="⚙️" title={<Trans>Settings</Trans>} />
<PageGroup label={<Trans>Account</Trans>}>
<div className="page-card profile-appearance-grid">
<SettingRow label={<Trans>Username</Trans>}>
<span className="profile-appearance-value">{user.username}</span>
</SettingRow>
@@ -98,9 +95,9 @@ export function Settings(): React.JSX.Element {
</button>
</SettingRow>
</div>
</ProfileSection>
<ProfileSection title={<Trans>Appearance</Trans>}>
<div className="profile-appearance-grid">
</PageGroup>
<PageGroup label={<Trans>Appearance</Trans>}>
<div className="page-card profile-appearance-grid">
<SettingRow label={<Trans>Style</Trans>}>
<Segmented
options={STYLE_OPTIONS}
@@ -116,9 +113,9 @@ export function Settings(): React.JSX.Element {
/>
</SettingRow>
</div>
</ProfileSection>
<ProfileSection title={<Trans>Feeds</Trans>}>
<div className="profile-appearance-grid">
</PageGroup>
<PageGroup label={<Trans>Feeds</Trans>}>
<div className="page-card profile-appearance-grid">
<SettingRow label={<Trans>Default tab</Trans>}>
<Segmented
options={FEED_TAB_OPTIONS}
@@ -127,9 +124,9 @@ export function Settings(): React.JSX.Element {
/>
</SettingRow>
</div>
</ProfileSection>
<ProfileSection title={<Trans>Notifications</Trans>}>
<div className="profile-appearance-grid">
</PageGroup>
<PageGroup label={<Trans>Notifications</Trans>}>
<div className="page-card profile-appearance-grid">
<SettingRow label={<Trans>Sounds</Trans>}>
<Segmented
options={ON_OFF_OPTIONS}
@@ -142,9 +139,9 @@ export function Settings(): React.JSX.Element {
/>
</SettingRow>
</div>
</ProfileSection>
<ProfileSection title={<Trans>Email</Trans>}>
<div className="profile-appearance-grid">
</PageGroup>
<PageGroup label={<Trans>Email</Trans>}>
<div className="page-card profile-appearance-grid">
<SettingRow label={<Message id="digest.weekly" />}>
<Segmented
options={ON_OFF_OPTIONS}
@@ -171,7 +168,7 @@ export function Settings(): React.JSX.Element {
</Trans>
</p>
)}
</ProfileSection>
</PageGroup>
{changePasswordOpen && (
<ChangePasswordModal onClose={() => setChangePasswordOpen(false)} />
)}