diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e4510..bbf13b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [1.1.2] - 2026-08-01 + +### Added +- Brand-accurate "Stalwart" color theme (red/pink accent from the official site tokens). The previous theme named "Stalwart" was only the neutral black/white look and is now labeled "Default". +- Changelog page in the header user dropdown, rendering this repository's `CHANGELOG.md` so release notes stay in sync with every release. +- Usage/Quota column on the Groups list (same synthetic column as Accounts); unlimited quota renders as ∞ instead of the word "Unlimited". +- Aliases count column on Accounts, Groups, Mailing Lists, and Domains (resolved from each object's real `aliases` property). +- Enabled Permissions and Disabled Permissions count columns on the Roles list. +- Client-side column sorting on Accounts, Groups, Mailing Lists, Roles, and Domains for the most useful columns (Email/Name/Description, Usage/Quota, Aliases, permission counts, Domain Name, Enabled). The backend rejects sort on these properties (`unsupportedSort`), so lists fetch then sort/paginate locally when a sortable header is clicked — same approach as mailbox hierarchy sort. +- `scripts/dev-seed` (`.sh` / `.ps1`) to populate the local test server with sample Users, Groups, Mailing Lists, and Roles after `dev-server-init` (idempotent; documented in DEVELOPMENT.md). + +### Changed +- Default color theme is now the new Stalwart theme (previously Ocean since 1.0.9); existing saved preferences are unaffected. +- Client-side sort is driven by a shared `clientSortable` column flag instead of hardcoded view/column maps, so any list can opt in without touching `DynamicList` branching. + +### Fixed +- Sidebar: expanding a collapsible group (e.g. Directory) and then navigating to an unrelated top-level link (e.g. Cluster) now collapses that group instead of leaving it open. +- Vite respects the `$PORT` environment variable so preview/dev tooling that assigns a free port can bind the same port Vite actually listens on. + ## [1.1.1] - 2026-08-01 ### Fixed diff --git a/package-lock.json b/package-lock.json index 634c10c..9061fa3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "stalwart-webui-fork", - "version": "1.1.1", + "version": "1.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stalwart-webui-fork", - "version": "1.1.1", + "version": "1.1.2", "dependencies": { "@daypicker/react": "^10.0.1", "@radix-ui/react-alert-dialog": "^1.1.23", diff --git a/package.json b/package.json index 9e7d018..d9f4434 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "stalwart-webui-fork", "private": true, - "version": "1.1.1", + "version": "1.1.2", "description": "Stalwart WebUI Fork", "type": "module", "scripts": { diff --git a/src/components/layout/TopBar.tsx b/src/components/layout/TopBar.tsx index 5c84e29..269b769 100644 --- a/src/components/layout/TopBar.tsx +++ b/src/components/layout/TopBar.tsx @@ -7,7 +7,7 @@ import { Link, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import * as LucideIcons from 'lucide-react'; -const { User, LogOut, Check, Menu, Sparkles, Search, Palette } = LucideIcons; +const { User, LogOut, Check, Menu, Sparkles, Search, Palette, ScrollText } = LucideIcons; import { Button } from '@/components/ui/button'; import { CommandPalette } from '@/components/common/CommandPalette'; import { @@ -203,6 +203,11 @@ export function TopBar() { {t('appearance.label', 'Appearance')} + navigate('/Changelog')}> + + {t('changelog.label', 'Changelog')} + + { diff --git a/src/features/changelog/ChangelogPage.tsx b/src/features/changelog/ChangelogPage.tsx new file mode 100644 index 0000000..1dda732 --- /dev/null +++ b/src/features/changelog/ChangelogPage.tsx @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +import { ExternalLink } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import ReactMarkdown from 'react-markdown'; +import type { Components } from 'react-markdown'; + +import { Card, CardContent } from '@/components/ui/card'; +import changelogSource from '../../../CHANGELOG.md?raw'; + +// Renders this fork's actual CHANGELOG.md — the single source of truth for +// release notes, kept up to date after every release — instead of a +// separately maintained copy that could drift from it. +const components: Components = { + h1: () => null, + h2: ({ children }) => ( +

{children}

+ ), + h3: ({ children }) => ( +

{children}

+ ), + p: ({ children }) =>

{children}

, + ul: ({ children }) => , + li: ({ children }) =>
  • {children}
  • , + a: ({ children, href }) => ( + + {children} + + ), + code: ({ children }) => {children}, +}; + +export function ChangelogPage() { + const { t } = useTranslation(); + + return ( +
    + + + +

    + {t( + 'changelog.officialNotice', + 'The official WebUI is maintained by the Stalwart team and is available at', + )}{' '} + + https://github.com/stalwartlabs/webui + + . +

    +
    +
    + +
    +

    {t('changelog.label', 'Changelog')}

    +

    + {t('changelog.description', "What's new in this fork, release by release.")} +

    +
    + + {changelogSource} +
    + ); +} diff --git a/src/i18n/en.json b/src/i18n/en.json index 3e40111..d0a5087 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -56,6 +56,11 @@ "welcome": "Welcome to Stalwart", "welcomeSubtitle": "Let's get your server set up." }, + "changelog": { + "description": "What's new in this fork, release by release.", + "label": "Changelog", + "officialNotice": "The official WebUI is maintained by the Stalwart team and is available at" + }, "common": { "apply": "Apply", "back": "Back", diff --git a/src/main.tsx b/src/main.tsx index 4dfb752..d2c2fff 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -24,8 +24,11 @@ import { getBasePath } from './lib/basePath'; const theme = state?.theme; const dark = theme === 'dark' || (theme !== 'light' && !!window.matchMedia?.('(prefers-color-scheme: dark)').matches); document.documentElement.classList.toggle('dark', dark); - const colorTheme = state?.colorTheme; + // Match uiStore defaults (stalwart + square) so the first paint is correct + // before zustand rehydrates — including brand-new sessions with no persisted state. + const colorTheme = state?.colorTheme ?? 'stalwart'; if ( + colorTheme === 'stalwart' || colorTheme === 'ocean' || colorTheme === 'forest' || colorTheme === 'violet' || @@ -35,7 +38,7 @@ import { getBasePath } from './lib/basePath'; ) { document.documentElement.dataset.theme = colorTheme; } - if (state?.radius === 'square') { + if ((state?.radius ?? 'square') === 'square') { document.documentElement.dataset.radius = 'square'; } // eslint-disable-next-line no-empty diff --git a/src/pages/AdminPanel.tsx b/src/pages/AdminPanel.tsx index 76faad3..83af03a 100644 --- a/src/pages/AdminPanel.tsx +++ b/src/pages/AdminPanel.tsx @@ -27,6 +27,7 @@ import { LoadingFallback } from '@/components/common/LoadingFallback'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; import { friendlyName } from '@/hooks/useGlobalSearch'; import { AppearancePage } from '@/features/appearance/AppearancePage'; +import { ChangelogPage } from '@/features/changelog/ChangelogPage'; import { findFirstAccessibleLinkInLayout, findFirstVisibleLinkInLayout, @@ -101,6 +102,7 @@ export default function AdminPanel() { // what the user sees in the navigation, e.g. "MTA-STS · Settings". const pageTitle = useMemo(() => { if (section === 'Appearance') return t('appearance.label', 'Appearance'); + if (section === 'Changelog') return t('changelog.label', 'Changelog'); if (!section) return t('dashboard.title', 'Dashboard'); if (!viewName) return section; const entry = @@ -220,9 +222,9 @@ export default function AdminPanel() { } } - if (section === 'Appearance') { - // Appearance is a client-side page outside the schema layouts: keep the - // sidebar bound to a real section while it is open. + if (section === 'Appearance' || section === 'Changelog') { + // Appearance and Changelog are client-side pages outside the schema + // layouts: keep the sidebar bound to a real section while open. if (!layouts.some((l) => l.name === activeSection)) { setActiveSection(layouts[0]?.name ?? ''); } @@ -337,6 +339,8 @@ export default function AdminPanel() { {section === 'Appearance' ? ( + ) : section === 'Changelog' ? ( + ) : ( )} diff --git a/src/stores/uiStore.ts b/src/stores/uiStore.ts index 3705103..41a2c28 100644 --- a/src/stores/uiStore.ts +++ b/src/stores/uiStore.ts @@ -73,7 +73,7 @@ export const useUIStore = create()( (set, get) => ({ theme: typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light', - colorTheme: 'ocean', + colorTheme: 'stalwart', radius: 'square', sidebarOpen: typeof window !== 'undefined' ? (window.matchMedia?.('(min-width: 768px)').matches ?? true) : true, activeSection: '', @@ -116,7 +116,7 @@ export const useUIStore = create()( return (state) => { if (state) { applyThemeClass(state.theme); - applyColorThemeAttribute(state.colorTheme ?? 'ocean'); + applyColorThemeAttribute(state.colorTheme ?? 'stalwart'); applyRadiusAttribute(state.radius ?? 'square'); } };