From cf49742d9774802c603b4d068c2f8ac9d3ffbfd1 Mon Sep 17 00:00:00 2001
From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
Date: Tue, 18 Aug 2026 13:56:47 +0000
Subject: [PATCH] Fix the top bar styling (#7544)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Every top bar styled itself, so none of them matched the new UI. Also,
colors on the premium banner (and possibly others) clashed since the
theme changes.
## Before Example Issue
## After (all)
## What changed
- `InfoBanner` exposed 8 colour-override props (`background`,
`borderColor`, `textColor`, `iconColor`, `buttonColor`,
`buttonTextColor`, `closeIconColor`, `buttonVariant`), so every caller
invented its own look. Replaced with a closed tone set: `info` · `promo`
· `warning` · `danger`.
- Tone drives the whole bar — fill, border, icon and the button — so a
CTA can't drift from the bar it sits on. Text is neutral in every tone;
only the icon carries the tone colour.
- All colour comes from `--c-*` tokens mixed over `--c-surface`, so the
bars follow light and dark instead of ignoring them. The old bars were
hardcoded: in dark mode the two licence warnings stayed cream-on-white.
- `promo` keeps the gradient it was always meant to have, built from the
existing `--c-hue-indigo`/`--c-hue-purple` stops (documented in
`colors.css` as gradient hues, deliberately not accent-following), with
the existing `premium` button accent on it.
- Deleted the hardcoded colours from all four callers: the purple
gradient (`#667eea`→`#764ba2`), the orange soup (`#FFF4E6` / `#9A3412` /
`#EA580C`) duplicated across the urgent banner and the admin plan
section, and the fixed dark bar (`--mantine-color-dark-7`) on the team
invitation.
- `UpgradeBanner|AdminPlanSection` sat on the theme linter's exemption
list, which is how those colours survived the theme migration. Exemption
removed, so `code-colors` now guards them.
- The banner's class was colliding with `core/ui/Banner.css`'s
`.sui-banner` (16 live rules), which restyled it in the app but not in
Storybook — that's why the two disagreed on radius, border and tone.
Renamed to `.app-banner`; the two surfaces now render identically.
- Bar is square and full-bleed with a single hairline rule underneath;
button labels are optically centred.
- Added `--c-warning-subtle`, matching the existing `--c-danger-subtle`
/ `--c-success-subtle`.
- New `Shared → Top bars` story renders all six bars at once, so a
change to the shared component is visible against the whole set.
- Unrelated one-liner: `frontend/.prettierignore` now ignores the
gitignored `editor/screenshots/` capture artifacts, which were failing
`format:check` locally. Happy to drop it if you'd rather keep this PR to
the bars.
## Testing
- `task frontend:check` — typecheck, lint (oxlint + 4 theme-lint passes
+ stylelint), format, 244 files / 2119 tests.
- `frontend:storybook:a11y:changed` — clean in light and dark.
- The a11y gate caught a real defect mid-change: giving each banner
`role="region"` with the same label produced duplicate landmarks, which
the app hits for real whenever two banners show at once. Landmark
removed.
- All six bars captured in the running editor, light and dark, and
diffed against `origin/main`'s component rendered with each caller's
original props.
---
.../public/locales/en-US/translation.toml | 6 +-
frontend/editor/scripts/lint/theme-lint.mjs | 1 -
.../shared/TeamInvitationBanner.tsx | 8 +-
.../src/core/components/AppLayout.stories.tsx | 4 +-
.../src/core/components/shared/AppBanner.css | 115 ++++++++
.../components/shared/AppBanner.stories.tsx | 194 +++++++++++++
.../src/core/components/shared/AppBanner.tsx | 124 +++++++++
.../components/shared/InfoBanner.stories.tsx | 38 ---
.../src/core/components/shared/InfoBanner.tsx | 263 ------------------
frontend/editor/src/core/theme/colors.css | 5 +
.../components/shared/DefaultAppBanner.tsx | 4 +-
.../components/shared/UpgradeBanner.tsx | 22 +-
.../configSections/AdminPlanSection.tsx | 11 +-
13 files changed, 453 insertions(+), 342 deletions(-)
create mode 100644 frontend/editor/src/core/components/shared/AppBanner.css
create mode 100644 frontend/editor/src/core/components/shared/AppBanner.stories.tsx
create mode 100644 frontend/editor/src/core/components/shared/AppBanner.tsx
delete mode 100644 frontend/editor/src/core/components/shared/InfoBanner.stories.tsx
delete mode 100644 frontend/editor/src/core/components/shared/InfoBanner.tsx
diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml
index 70d3d03bf4..850f69ca02 100644
--- a/frontend/editor/public/locales/en-US/translation.toml
+++ b/frontend/editor/public/locales/en-US/translation.toml
@@ -1879,6 +1879,9 @@ width = "Width"
[app]
description = "The Free Adobe Acrobat alternative (10M+ Downloads)"
+[appBanner]
+dismiss = "Dismiss"
+
[attachments]
convertToPdfA3b = "Convert to PDF/A-3b"
convertToPdfA3bDescription = "Creates an archival PDF with embedded attachments"
@@ -4827,9 +4830,6 @@ title = "Image to PDF"
[imageToPdf]
tags = "conversion,img,jpg,picture,photo"
-[infoBanner]
-dismiss = "Dismiss"
-
[invite]
acceptError = "Failed to create account"
accountFor = "Creating account for"
diff --git a/frontend/editor/scripts/lint/theme-lint.mjs b/frontend/editor/scripts/lint/theme-lint.mjs
index 494fc03cca..97b32ef208 100644
--- a/frontend/editor/scripts/lint/theme-lint.mjs
+++ b/frontend/editor/scripts/lint/theme-lint.mjs
@@ -642,7 +642,6 @@ const CODE_EXEMPT_PATH = [
/mantineTheme|\/theme\.ts$|toolsTaxonomy|LayoutPreview|PageNumberPreview|CloudStorageIcons|BrandMarks/,
/\/onboarding\//,
/addStamp|addWatermark|\/tooltips\//,
- /UpgradeBanner|AdminPlanSection/,
// Stories are checked like app code; colour-as-data lines opt out with
// `theme-allow-color`.
/\.test\.[jt]sx?$|\/types\//,
diff --git a/frontend/editor/src/cloud/components/shared/TeamInvitationBanner.tsx b/frontend/editor/src/cloud/components/shared/TeamInvitationBanner.tsx
index 3b373e9c3b..638c752f9e 100644
--- a/frontend/editor/src/cloud/components/shared/TeamInvitationBanner.tsx
+++ b/frontend/editor/src/cloud/components/shared/TeamInvitationBanner.tsx
@@ -3,7 +3,7 @@ import { Group, Text } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import LocalIcon from "@app/components/shared/LocalIcon";
-import { InfoBanner } from "@app/components/shared/InfoBanner";
+import { AppBanner } from "@app/components/shared/AppBanner";
import { useSaaSTeam } from "@app/contexts/SaaSTeamContext";
/**
@@ -105,7 +105,7 @@ export function TeamInvitationBanner() {
);
return (
-
);
}
diff --git a/frontend/editor/src/core/components/AppLayout.stories.tsx b/frontend/editor/src/core/components/AppLayout.stories.tsx
index 4d7e6780cf..69aceef9d7 100644
--- a/frontend/editor/src/core/components/AppLayout.stories.tsx
+++ b/frontend/editor/src/core/components/AppLayout.stories.tsx
@@ -4,7 +4,7 @@ import { AppLayout } from "@app/components/AppLayout";
import { BannerProvider, useBanner } from "@app/contexts/BannerContext";
import { NavigationProvider } from "@app/contexts/NavigationContext";
import { ToolRegistryProvider } from "@app/contexts/ToolRegistryProvider";
-import { InfoBanner } from "@app/components/shared/InfoBanner";
+import { AppBanner } from "@app/components/shared/AppBanner";
const meta = {
title: "Components/AppLayout",
@@ -49,7 +49,7 @@ function BannerSetter() {
const { setBanner } = useBanner();
useEffect(() => {
setBanner(
- ;
+export default meta;
+type Story = StoryObj;
+
+export const Info: Story = {
+ args: {
+ icon: "info-rounded",
+ title: "Heads up",
+ message: "This document contains form fields that will be flattened.",
+ },
+};
+
+export const Promo: Story = {
+ args: {
+ tone: "promo",
+ icon: "stars-rounded",
+ title: "Upgrade to Server Plan",
+ message:
+ "Get the most out of Stirling PDF with unlimited users and advanced features.",
+ buttonText: "Upgrade Now",
+ buttonIcon: "upgrade-rounded",
+ onButtonClick: () => {},
+ compact: true,
+ },
+};
+
+export const Warning: Story = {
+ args: {
+ tone: "warning",
+ icon: "warning-rounded",
+ title: "Action required",
+ message: "Some pages could not be processed and were skipped.",
+ buttonText: "Review",
+ onButtonClick: () => {},
+ },
+};
+
+export const Danger: Story = {
+ args: {
+ tone: "danger",
+ icon: "warning-rounded",
+ title: "This server needs admin attention",
+ message: "Review the license requirements to keep this server compliant.",
+ buttonText: "See info",
+ buttonIcon: "info-rounded",
+ onButtonClick: () => {},
+ dismissible: false,
+ },
+};
+
+export const Compact: Story = {
+ args: {
+ compact: true,
+ icon: "info-rounded",
+ message: "Autosave is enabled for this file.",
+ dismissible: false,
+ },
+};
+
+/** Message-only, no title: the message takes the title's weight so the bar still reads. */
+export const MessageOnly: Story = {
+ args: {
+ icon: "picture-as-pdf-rounded",
+ message:
+ "Make Stirling PDF your default application for opening PDF files.",
+ buttonText: "Set Default",
+ onButtonClick: () => {},
+ secondaryButtonText: "Don't remind me again",
+ onSecondaryButtonClick: () => {},
+ },
+};
+
+function Row({ caption, children }: { caption: string; children: ReactNode }) {
+ return (
+
+
+ {caption}
+
+ {children}
+
+ );
+}
+
+/**
+ * Every top bar the app can show, in one place: each entry mirrors a real caller,
+ * so a change to the component is visible against the whole set at once. Renders a
+ * composition rather than the component, so it takes no args of its own.
+ */
+export const AllTopBars: StoryObj = {
+ render: () => (
+