🎨 Improved dark mode across Shade and Ghost admin (DES-1397)#28895
Conversation
ref https://linear.app/tryghost/issue/DES-1397/ - Added a dedicated sidebar background token so the dark sidebar reads as a distinct surface rather than pure gray-950 - Reworked the sidebar search field in dark mode: gray-900/30 fill with a gray-900/50 border, plus a lighter text and a slightly stronger border on hover so the affordance is clearer against the new background
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR adds new Shade surface and interactive design tokens, maps them into Tailwind, and updates shared UI primitives and app surfaces to use the new token-based hover, active, border, and elevation styling. It also expands token documentation and Storybook coverage, adjusts dialog tests for revised description styling, and changes Ghost admin theme switching behavior plus dark stylesheet visuals. Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
| Command | Status | Duration | Result |
|---|---|---|---|
nx run @tryghost/admin-x-settings:test:acceptance |
✅ Succeeded | 10m 48s | View ↗ |
nx run-many --target=build --projects=tag:publi... |
✅ Succeeded | 1s | View ↗ |
nx run-many -t test:unit -p @tryghost/activityp... |
✅ Succeeded | 7m 16s | View ↗ |
nx run @tryghost/admin:build |
✅ Succeeded | 4m 35s | View ↗ |
nx run ghost-admin:test |
✅ Succeeded | 2m 57s | View ↗ |
nx run-many -t lint -p @tryghost/activitypub,@t... |
✅ Succeeded | 1m 16s | View ↗ |
nx run @tryghost/activitypub:test:acceptance |
✅ Succeeded | 56s | View ↗ |
nx run ghost:build:assets |
✅ Succeeded | 2s | View ↗ |
nx run ghost:build:tsc |
✅ Succeeded | 6s | View ↗ |
💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗
☁️ Nx Cloud last updated this comment at 2026-06-30 10:35:17 UTC
ref https://linear.app/tryghost/issue/DES-1397/ - Added a dedicated sidebar border token (#25272A) so the right edge of the dark sidebar reads as a distinct surface, matching the new background tone - Tuned active and hover backgrounds: active items now use gray-900/60, non-active hover uses gray-900/30, and active-item hover stays at the active color instead of lightening - Dimmed inactive menu item text (and their icons via currentColor) from gray-100 to gray-500 so the active item reads as the primary signal
ref https://linear.app/tryghost/issue/DES-1397/ - Aligned the "create post" plus icon, sidebar count badges, and the user menu email with the ⌘K kbd colour (gray-800) in dark mode so the secondary affordances all read with the same weight against the new sidebar surface
ref https://linear.app/tryghost/issue/DES-1397/ - Applied the sidebar's active/hover palette to PageMenuItem and the button, button-sm, and pill Tabs variants in dark mode (gray-900/30 hover, gray-900/60 active, active state preserved on hover) so the Analytics navigation and in-card tabs read as part of the same surface system as the sidebar
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/shade/src/components/ui/sidebar.tsx`:
- Around line 533-537: The sidebar button styles in the component are using
hard-coded dark theme color utilities, which should be replaced with semantic
token-based classes. Update the class strings in the Sidebar menu/button styling
logic (including the variant definitions for the button styles) to remove
`dark:` color utilities like `dark:hover:bg-gray-900/30` and any similar
hard-coded dark text/background classes, and move that visual intent into
semantic tokens defined in `theme-variables.css`. Keep the component’s Tailwind
classes token-driven so the `sidebar.tsx` styles follow Shade’s color
guidelines.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d18c7cfe-a7d1-4f5e-8575-9a3c46aa8897
📒 Files selected for processing (4)
apps/admin/src/layout/app-sidebar/nav-content.tsxapps/admin/src/layout/app-sidebar/user-menu.tsxapps/shade/src/components/ui/sidebar.tsxapps/shade/theme-variables.css
✅ Files skipped from review due to trivial changes (1)
- apps/admin/src/layout/app-sidebar/nav-content.tsx
ref https://linear.app/tryghost/issue/DES-1397/ - Introduced --tab-hover and --tab-active design tokens (registered as Tailwind bg-tab-hover and bg-tab-active utilities) so the repeated gray-900/30 and gray-900/60 dark mode values for tabs and the page menu live in one place - Pointed the dark --sidebar-accent at --tab-active so the active surface value is no longer duplicated between the sidebar and tab systems
ref https://linear.app/tryghost/issue/DES-1397/ - Raised the dark mode --border from solid gray-950 to gray-900 at 50% so dividers and outlines read at the right contrast against the new panel surfaces - Pointed --sidebar-border at --border so the sidebar's right edge matches the rest of the system instead of carrying its own one-off value
ref https://linear.app/tryghost/issue/DES-1397/ - Bumped the create post + icon to white on hover in dark mode so it reads as the primary affordance against the new active surface fill
ref https://linear.app/tryghost/issue/DES-1397/ - Dropped the muted top-right gradient on the Analytics overview's "Latest post performance" card so the section sits flat against the page surface in both light and dark mode
ref https://linear.app/tryghost/issue/DES-1397/ - Added a --button-hover token (gray-100 light, gray-900/30 dark) so outline buttons, the dropdown button variant, and the SelectTrigger share one named hover surface - Switched the input-surface recipe to bg-transparent and border-border in dark mode so the Select trigger (plus Input/Textarea/InputGroup) blends into its container the way they already do in light mode and matches the lifted base border colour - Aligned outline / dropdown button borders with the recipe so all control surfaces use the same border-border value
ref https://linear.app/tryghost/issue/DES-1397/ - Pointed dark --surface-elevated and --popover at the existing --color-sidebar-bg so cards, popovers, dropdowns, and the sidebar share one elevated surface value - Bumped the sidebar user menu popover one step lighter so it reads as floating above the sidebar instead of melting into it
ref https://linear.app/tryghost/issue/DES-1397/ - Added a shared --interactive-hover token (aliased to --button-hover) so dropdown options, table rows, and the existing outline button hover all reference one named value - Routed SelectItem and every DropdownMenu item/sub-trigger through --interactive-hover so list options match the outline button hover - Switched the Shade TableCell row hover to --interactive-hover and dropped the tags list's custom row hover + cell-hover suppressor so it picks up the standard treatment - Moved --members-sticky-hover-bg into theme-variables so the members list sticky column hovers with a solid composite of the new hover colour over the page surface in both modes
ref https://linear.app/tryghost/issue/DES-1397/ - Added dark:bg-popover dark:text-popover-foreground to the Shade TooltipContent so every default tooltip lifts above the page using the elevated colour instead of inverting to a light-on-dark pill - Added dark:bg-popover to the TrendBadge's custom tooltip override so the Analytics overview trend tooltip stops melting into the page
ref https://linear.app/tryghost/issue/DES-1397/ - Added a --table-row-hover token (gray-50 light, sidebar-bg dark) and routed the Shade TableCell, the top posts list, and the members list sticky column through it so row hovers settle on the elevated colour instead of the generic interactive hover - Reused --interactive-hover for the analytics top posts overlay, comments list rows, filter dropdown options (CommandItem and the load-more button), the filter add button, and the kpis tab variant - Added a --control-border token (gray-200 light, solid gray-900 dark) for controls that were lacking contrast and applied it to outline / dropdown Button variants, the input-surface recipe, and the filter add button - Bumped --color-sidebar-bg from oklch 0.23 to 0.25 so sidebar, surface-elevated, popover, and table-row-hover lift together - Dropped the members filter add button's border-input override so it picks up the new control border
ref https://linear.app/tryghost/issue/DES-1397/ - Reworked ToggleGroup to sit on the page surface with a border and a 9px radius (--input-group-radius) so it reads as a flat control bar instead of a tinted muted block - Routed the active Toggle state through --tab-active so it visually matches the active Tabs / sidebar surface in both modes - Promoted the user menu's inline oklch(0.27 0.006 260) into a named --surface-elevated-2 token (light = gray-100, dark = oklch 0.27) so "above-elevated" popovers and controls share one named value
ref https://linear.app/tryghost/issue/DES-1397/ - Nudged --color-sidebar-bg and --surface-elevated-2 chroma from 0.006 to 0.009 so the sidebar, elevated panels, popovers, and row hover read with a subtle cool tint instead of feeling flat / dull - Swapped the Analytics top posts feature image placeholder's dark background from the inline #36373a to --surface-elevated-2 so it sits a touch darker and shares the named elevated-2 surface
ref https://linear.app/tryghost/issue/DES-1397/ - Overrode the full gray-* and grey-* scale inside .dark with the same lightness and hue but roughly half the chroma so dark mode surfaces, text, and borders feel less tinted and cleaner without affecting light mode
ref https://linear.app/tryghost/issue/DES-1397/ - Sourced --tab-active from a slightly more tinted oklch (chroma 0.012) instead of the now-desaturated --color-gray-900 so active tabs and sidebar items keep a hint of warmth against the cleaner gray scale - Dropped --control-border from solid gray-900 to gray-900 at 70% so outline buttons, inputs, and the dropdown trigger sit between the base --border and the previous solid value
ref https://linear.app/tryghost/issue/DES-1397/ - Overrode the .gh-posts-list-item-group hover background in app-dark with oklch(0.23 0.009 260) so the legacy posts list matches the Shade --table-row-hover surface used by the React tables - Locked .gh-post-list-feature-image base and hover backgrounds to oklch(0.27 0.009 260) so the legacy feature image placeholder matches the --surface-elevated-2 colour used by the analytics top posts placeholder
ref https://linear.app/tryghost/issue/DES-1397/ - Added dark:data-[state=checked]:bg-green-500 to the Switch track and dark:bg-white to the thumb so the checked Switch reads as a green toggle with a white circle in dark mode instead of an inverted white-track-with-black-thumb
ref https://linear.app/tryghost/issue/DES-1397/ - Added a global html.theme-switching kill-rule that nukes transitions and animations on every element and pseudo-element while the class is set, so the toggle paints the new theme in a single frame instead of crossfading every element with its own duration - Wrapped _setAdminTheme so it adds .theme-switching before flipping the .dark class, then releases it via a double requestAnimationFrame after the dark stylesheet finishes loading (covers both first-time lazy load and cached subsequent toggles, plus the catch path)
ref https://linear.app/tryghost/issue/DES-1397/ - Repointed --background in dark to oklch(0.178 0.006 271) (#101114) so the page sits a touch darker than the previous --color-black - Repointed --color-sidebar-bg in dark to oklch(0.204 0.007 258) (#15171A) which cascades to the sidebar, --surface-elevated, --popover, and --table-row-hover - Routed --card through --background in dark so analytics cards sit flat on the new page surface instead of lifting - Switched the analytics header and Navbar dark:bg-black to dark:bg-background so the sticky header tracks the new page colour
ref https://linear.app/tryghost/issue/DES-1397/ - Repointed --main-bg-color and --dark-main-bg-color in app-dark.css from #15171A to #101114 so the legacy Ember admin page surface matches the new React --background colour
ref https://linear.app/tryghost/issue/DES-1397/ - Repointed .gh-main-white (used by the lexical editor route) to --dark-main-bg-color so the editor canvas sits on the new page bg - Routed .gh-editor-title / .gh-editor-excerpt, .gh-btn-editor, .gh-editor-post-status, and .gh-editor-wordcount-container to the same token so the title field, header controls, post status pill, and word count chip stop reading as lighter islands inside the editor view
ref https://linear.app/tryghost/issue/DES-1397/ - Overrode .fullscreen-modal-total-overlay (publish-flow, preview, and update-flow editor modals) and .gh-publish-setting-form to use --dark-main-bg-color so they sit on the new page surface instead of the lighter --white default
ref https://linear.app/tryghost/issue/DES-1397/ - Routed the comments list row hover through bg-table-row-hover so it matches the Shade Table row hover and the top posts list instead of using the generic interactive hover - Updated the Ember post list row hover override to oklch(0.204 0.007 258) / #15171A so the legacy posts list tracks the current --table-row-hover value - Switched the Post analytics header and Navbar dark:bg-black to dark:bg-background so the sticky header matches the new page surface in dark mode
ref https://linear.app/tryghost/issue/DES-1397/ - Bumped dark --destructive from --color-red-900 to --color-red-500 so destructive menu items, links, and buttons (e.g. the bulk Delete action in the members ... menu) read clearly against the dark surfaces instead of disappearing into them
ref https://linear.app/tryghost/issue/DES-1397/ - Routed the AutomationHeader top bar and the right step sidebar to bg-surface-elevated so they lift above the canvas instead of blending into the page surface - Switched the AutomationCanvas wrapper and the React Flow --xy-background-color (dark) to --background so the canvas itself sits on the new page colour rather than the legacy --surface-page / --color-black values - Also dropped the gap-x-4 hover artifact on the automations list by promoting the row hover bg to TableRow so the bg spans the gap between cells
ref https://linear.app/tryghost/issue/DES-1397/ - Aligned DropdownMenuContent / DropdownMenuSubContent, SelectContent, and PopoverContent on a single visual: bg-surface-elevated-2, a subtler border-border/30, and the standard shadow-md so the three Radix popover surfaces read as the same component - Set the inner Command bg to bg-transparent so the filter dropdown picks up the PopoverContent surface instead of being overlaid by bg-popover (caused the filter dropdown to render at the darker --color-sidebar-bg instead of --surface-elevated-2) - Pulled --surface-elevated-2 in dark from oklch 0.27 down to 0.235 so the elevated-2 surface sits between --surface-elevated (0.204) and the previous brighter value (cascades to the user menu, the active Toggle pill, and the analytics feature image placeholder too) - Added direct .dark .shadow-xs through .dark .shadow-2xl rules with black at 0.35-0.65 alpha so every Shade consumer of the default shadow utilities gets meaningfully darker shadows in dark mode (Tailwind v4 inlines the @theme --shadow-* values at compile time so a token-level override couldn't propagate)
ref https://linear.app/tryghost/issue/DES-1397/ - Dialed avatar fill saturation from 75 to 45 across the Shade Avatar component, the Posts newsletter feedback avatar, and the legacy gh-member-avatar Ember component so members list, comments list, member details, and member activity avatars stop reading as oversaturated in both modes - Reverted light --surface-elevated-2 from --color-gray-100 back to --color-white so the dropdown surfaces (DropdownMenu, Select, Popover, plus the user menu / placeholder consumers' light variant) stop showing as a light grey block in light mode - Bumped the light mode dropdown border opacity from border-border/30 to border-border/60 while keeping dark:border-border/30 so the three dropdown surfaces have a clearer visible edge against light backgrounds
ref https://linear.app/tryghost/issue/DES-1397/ - Repointed .ember-power-select-multiple-option background from var(--white) (= #15171A, now barely distinguishable from the new page bg) to var(--lightgrey) so label and tag chips in Ember selectors lift above the surface in dark mode
…ode-fixes # Conflicts: # ghost/admin/app/services/feature.js
ref https://linear.app/tryghost/issue/DES-1397/ - Added surface elevation, interactive surfaces, form control border, and dropdown surface contract sections to tokens.mdx so the dark mode rules introduced on this branch are documented in one place - Expanded the Colors Surface story to include --surface-elevated-2 and added a new InteractiveSurfaces story documenting --interactive-hover, --button-hover, --tab-hover/active, and --table-row-hover with consumer mappings - Added --control-border to the Border story with the rule that form controls use it instead of --border-default - Annotated the Shadows story to call out the dark mode override pattern (direct .dark .shadow-* rules, since Tailwind v4 inlines @theme --shadow-* at compile time)
ref https://linear.app/tryghost/issue/DES-1397/ - Swapped DialogOverlay dark:bg-white/8 for dark:bg-black/60 so the backdrop dims the page with a transparent black instead of lifting it with a white tint, and added dark:data-[state=open]:backdrop- blur-none so the open animation doesn't blur in dark - Repointed DialogContent bg from bg-popover to bg-surface-elevated-2 so modals sit on the same lifted surface as the dropdown family in dark mode
ref https://linear.app/tryghost/issue/DES-1397/ - DialogOverlay: switched dark:bg-white/8 to dark:bg-black/60 and removed the open-state backdrop blur in both modes so the dim is just a transparent black wash - DialogContent: routed bg through bg-surface-elevated-2 so modals share the lifted surface with the dropdown family - AlertDialogOverlay: mirrored the new Dialog overlay (dark:bg-black/60, no blur) - Label picker (bulk add / remove label modals): replaced the bespoke bg-gray-100 / bg-gray-950 + green focus wrapper with the Shade input recipe (bg-surface-elevated, dark:bg-transparent, border-control-border, focus-within ring on focus-ring), and switched input + placeholder from text-sm to text-control (13px) - Bulk delete modal: dropped the text-sm wrapper so the body copy inherits the base body size; DialogContent gap-5 handles paragraph spacing - Dropped text-sm on DialogDescription so descriptions follow base body size too - Filters add button md size: explicit text-base so the Filter trigger in members / comments / analytics renders at base size, and the members consumer's lg:text-sm bumped to lg:text-base
ref https://linear.app/tryghost/issue/DES-1397/ - Updated the two Dialog Components specs that asserted the default DialogDescription className to look for text-muted-foreground only (we dropped text-sm so the description follows the base body size)
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/shade/src/components/ui/dialog.tsx`:
- Line 22: The dialog overlay in the Dialog component is hard-coding a dark-mode
backdrop with a Tailwind dark variant, which violates the semantic token
contract for Shade overlays. Update the overlay styling in the Dialog/content
wrapper to use the existing semantic overlay token instead of
`dark:bg-black/60`, and keep the class usage aligned with the token-based
pattern used by the UI layer.
In `@apps/shade/src/components/ui/dropdown-menu.tsx`:
- Line 50: The dropdown menu classes in the component should not use
`dark:border-border/30`, since Shade components must stay theme-agnostic and
rely on semantic tokens instead. Update the dropdown/menu styling in the
relevant dropdown content wrappers to express the darker border contrast through
an existing or new semantic token/class, and remove the `dark:` color override
from the affected class strings.
In `@apps/shade/src/components/ui/tooltip.tsx`:
- Line 22: The tooltip styling in the `TooltipContent` class list is using
`dark:`-specific color overrides, which should be replaced with semantic token
classes so the component stays theme-agnostic. Update the tooltip surface/text
styling in `tooltip.tsx` to use a single semantic background and foreground
token pair instead of `dark:bg-popover` and `dark:text-popover-foreground`,
keeping the rest of the animation and positioning classes unchanged.
In `@apps/shade/src/docs/tokens/colors.stories.tsx`:
- Around line 175-181: The Colors story in colors.stories.tsx currently groups
table-row-hover with the translucent overlay tokens even though it maps to an
opaque sidebar background. Update the swatches/intro in the relevant story
section to either exclude table-row-hover from the “translucent in dark mode”
description or explicitly label it as the opaque exception, using the swatches
list and surrounding description to keep the guidance accurate.
In `@apps/shade/theme-variables.css`:
- Around line 184-201: The dark shadow utility rules in theme-variables.css are
overriding Tailwind’s shadow stack by assigning box-shadow directly, which
breaks combined shadow and ring styles in dark mode. Update the dark .shadow-xs
through dark .shadow-2xl declarations to set --tw-shadow instead of box-shadow
so Tailwind can preserve ring and focus-visible ring layers. Use the existing
shadow utility selectors as the anchor for the change and keep the same shadow
values in the custom property form.
In `@ghost/admin/app/services/feature.js`:
- Around line 212-214: The `loadStyle()` flow in `feature.js` is restoring the
dark stylesheet state from a stale captured `isDark` value after
`_loadAdminThemeStylesheet()` resolves, which can overwrite a newer theme change
in `system` mode. Update the promise callback so it does not blindly reapply the
old `isDark` state; instead, re-read the current theme state or guard against
intervening `system` theme updates before toggling `link[title=dark]`. Keep the
`releaseSuppression()` path intact, and focus the fix around `loadStyle()` and
the `isDark`/stylesheet update logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b4931049-3bc7-4c63-9491-bdd4903c7c92
📒 Files selected for processing (51)
apps/admin/src/layout/app-sidebar/app-sidebar-header.tsxapps/admin/src/layout/app-sidebar/nav-content.tsxapps/admin/src/layout/app-sidebar/user-menu.tsxapps/posts/src/components/label-picker/label-picker.tsxapps/posts/src/views/Automations/components/automation-header.tsxapps/posts/src/views/Automations/components/automations-help-cards.tsxapps/posts/src/views/Automations/components/automations-list.tsxapps/posts/src/views/Automations/components/canvas/automation-canvas.tsxapps/posts/src/views/Automations/components/canvas/step-sidebar.tsxapps/posts/src/views/PostAnalytics/Newsletter/components/feedback.tsxapps/posts/src/views/PostAnalytics/components/post-analytics-header.tsxapps/posts/src/views/Tags/components/tags-list.tsxapps/posts/src/views/comments/components/comments-list.tsxapps/posts/src/views/members/components/bulk-action-modals/delete-modal.tsxapps/posts/src/views/members/components/members-filters.tsxapps/posts/src/views/members/components/members-help-cards.tsxapps/posts/src/views/members/components/members-list-item.tsxapps/shade/src/components/patterns/filters.tsxapps/shade/src/components/ui/alert-dialog.tsxapps/shade/src/components/ui/avatar.tsxapps/shade/src/components/ui/button.tsxapps/shade/src/components/ui/command.tsxapps/shade/src/components/ui/dialog.tsxapps/shade/src/components/ui/dropdown-menu.tsxapps/shade/src/components/ui/input-surface.tsapps/shade/src/components/ui/popover.tsxapps/shade/src/components/ui/select.tsxapps/shade/src/components/ui/switch.tsxapps/shade/src/components/ui/table.tsxapps/shade/src/components/ui/tabs.tsxapps/shade/src/components/ui/toggle-group.tsxapps/shade/src/components/ui/toggle.tsxapps/shade/src/components/ui/tooltip.tsxapps/shade/src/components/ui/trend-badge.tsxapps/shade/src/docs/tokens.mdxapps/shade/src/docs/tokens/colors.stories.tsxapps/shade/src/docs/tokens/shadows.stories.tsxapps/shade/tailwind.theme.cssapps/shade/test/unit/components/ui/dialog.test.tsxapps/shade/theme-variables.cssapps/stats/src/views/Stats/Overview/components/latest-post.tsxapps/stats/src/views/Stats/Overview/components/top-posts.tsxapps/stats/src/views/Stats/Overview/overview.tsxapps/stats/src/views/Stats/components/feature-image-placeholder.tsxapps/stats/src/views/Stats/layout/stats-header.tsxghost/admin/app/components/gh-member-avatar.jsghost/admin/app/services/feature.jsghost/admin/app/styles/app-dark.cssghost/admin/app/styles/app.cssghost/admin/app/styles/components/settings-menu.cssghost/admin/app/styles/patterns/forms.css
✅ Files skipped from review due to trivial changes (31)
- apps/shade/src/components/ui/trend-badge.tsx
- ghost/admin/app/styles/app.css
- apps/posts/src/views/comments/components/comments-list.tsx
- apps/shade/src/components/ui/switch.tsx
- apps/posts/src/views/Automations/components/canvas/step-sidebar.tsx
- apps/posts/src/views/Automations/components/automation-header.tsx
- apps/posts/src/views/PostAnalytics/Newsletter/components/feedback.tsx
- apps/stats/src/views/Stats/layout/stats-header.tsx
- apps/posts/src/views/PostAnalytics/components/post-analytics-header.tsx
- apps/posts/src/views/Automations/components/canvas/automation-canvas.tsx
- apps/stats/src/views/Stats/components/feature-image-placeholder.tsx
- apps/shade/src/components/ui/toggle-group.tsx
- apps/shade/src/docs/tokens/shadows.stories.tsx
- apps/admin/src/layout/app-sidebar/user-menu.tsx
- apps/shade/src/components/ui/toggle.tsx
- apps/posts/src/views/members/components/bulk-action-modals/delete-modal.tsx
- apps/shade/src/components/ui/avatar.tsx
- apps/posts/src/components/label-picker/label-picker.tsx
- apps/stats/src/views/Stats/Overview/components/latest-post.tsx
- apps/shade/src/components/ui/table.tsx
- apps/shade/src/components/ui/input-surface.ts
- apps/shade/src/components/ui/alert-dialog.tsx
- apps/shade/tailwind.theme.css
- apps/posts/src/views/Automations/components/automations-list.tsx
- apps/posts/src/views/members/components/members-help-cards.tsx
- apps/shade/src/docs/tokens.mdx
- apps/shade/src/components/ui/select.tsx
- apps/admin/src/layout/app-sidebar/nav-content.tsx
- apps/shade/src/components/ui/popover.tsx
- apps/admin/src/layout/app-sidebar/app-sidebar-header.tsx
- apps/shade/src/components/patterns/filters.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/shade/src/components/ui/tabs.tsx
| ref={ref} | ||
| className={cn( | ||
| 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', | ||
| 'z-50 min-w-[8rem] overflow-hidden rounded-md border border-border/60 bg-surface-elevated-2 dark:border-border/30 p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Keep dropdown borders token-driven instead of dark: overrides.
Lines 50 and 69 add dark:border-border/30, which reintroduces theme-specific colour branching inside a Shade component. Please express that darker border contrast through a semantic token/class so these menus stay theme-agnostic at the component layer. As per coding guidelines, "Do not add dark: Tailwind variants for colour in Shade components; use semantic tokens instead".
Also applies to: 69-69
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/shade/src/components/ui/dropdown-menu.tsx` at line 50, The dropdown menu
classes in the component should not use `dark:border-border/30`, since Shade
components must stay theme-agnostic and rely on semantic tokens instead. Update
the dropdown/menu styling in the relevant dropdown content wrappers to express
the darker border contrast through an existing or new semantic token/class, and
remove the `dark:` color override from the affected class strings.
Source: Coding guidelines
| ref={ref} | ||
| className={cn( | ||
| 'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', | ||
| 'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:bg-popover dark:text-popover-foreground', |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Keep tooltip colours semantic rather than dark:-branched.
Line 22 adds dark:bg-popover dark:text-popover-foreground, which pushes theme logic back into the component. Please move this to a single semantic tooltip surface/text token pair and keep the class list theme-agnostic. As per coding guidelines, "Do not add dark: Tailwind variants for colour in Shade components; use semantic tokens instead".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/shade/src/components/ui/tooltip.tsx` at line 22, The tooltip styling in
the `TooltipContent` class list is using `dark:`-specific color overrides, which
should be replaced with semantic token classes so the component stays
theme-agnostic. Update the tooltip surface/text styling in `tooltip.tsx` to use
a single semantic background and foreground token pair instead of
`dark:bg-popover` and `dark:text-popover-foreground`, keeping the rest of the
animation and positioning classes unchanged.
Source: Coding guidelines
ref https://linear.app/tryghost/issue/DES-1397/ - Updated the .gh-list-scrolling first-child sticky-column gradients in app-dark.css from rgba(21,23,26,...) (the old #15171A page bg) to rgba(16,17,20,...) so the fade matches the new #101114 page surface - Added a dark-mode override for the member activity row hover (oklch(0.204 0.004 258)) so the legacy table matches Shade's --table-row-hover value used by the React lists; realigned the posts-list hover override to the same chroma - Added 20px left padding on the first cell / 20px right padding on the last cell of the member activity table, scoped tightly enough to override lists.css's sticky-first-cell padding-left: 0 rule
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ghost/admin/app/styles/layouts/member-activity.css`:
- Around line 380-381: The suppression option row in the member activity layout
is too short, so the label and switch can be clipped or cramped. Update the
styling for this row in member-activity.css to restore a taller height that
accommodates the existing label padding and 1.4rem text, and verify the related
suppression option row selector still aligns correctly with the switch.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f11159f7-738c-466d-9860-b3ac9e5370ab
📒 Files selected for processing (2)
ghost/admin/app/styles/app-dark.cssghost/admin/app/styles/layouts/member-activity.css
🚧 Files skipped from review as they are similar to previous changes (1)
- ghost/admin/app/styles/app-dark.css
ref https://linear.app/tryghost/issue/DES-1397/ - Pulled the .dark .shadow-* alphas down from 0.25-0.55 to 0.15-0.4 so dropdowns, popovers, dialogs, and other shadow consumers feel less heavy in dark mode - Switched AlertDialogContent from bg-background to bg-surface-elevated-2 so alert dialogs share the same lifted surface as DialogContent and the dropdown family
ref https://linear.app/tryghost/issue/DES-1397/ - Main sticky header (header.tsx) dark:bg-black → dark:bg-background so the blur-bar matches the new page surface - Sidebar feedback box wrapper (sidebar.tsx) and the feedback box itself (feedback-box.tsx) dark:bg-black → dark:bg-background - Search modal sticky search header + Input dark:bg-(--color-popover) → dark:bg-surface-elevated-2 so the search field matches the modal surface, and added focus-visible:!ring-0 on the Input so the recipe focus ring is suppressed - Profile preview hover card: dark:bg-[#101114] → dark:bg-surface-elevated-2 and replaced the bespoke triple-stack box-shadow with shadow-lg so the card lifts above the page and picks up the dark mode shadow override - Bumped activitypub from 3.1.52 to 3.1.53 for the visual change
ref https://linear.app/tryghost/issue/DES-1397/ - Feed action buttons (heart / comment / repost) and the ... menu trigger: dropped dark:bg-black so they have no rest-state fill in dark mode, hover still uses dark:hover:bg-gray-950 - Back button: dark:bg-black/85 → dark:bg-transparent so it doesn't show a fill at rest - Profile avatar wrapper: dark:bg-black → dark:bg-background so the avatar border matches the new page surface - Profile settings avatar wrapper border: dark:border-[#101114] → dark:border-surface-elevated-2 so the avatar blends into the modal surface; Upload cover image button gets dark:bg-black/40 with dark:text-white so it reads as a translucent pill on the cover - Dialed back the "What's new?" textarea and the reply box placeholder colour in dark from gray-400 to gray-700 so it sits at a quieter dim tone - Bumped activitypub from 3.1.53 to 3.1.54 for the visual changes
ref https://linear.app/tryghost/issue/DES-1397/ - Analytics / Growth paid subscription breakdown pie chart: set the inter-slice stroke to var(--card) so the 5px separators match the card surface instead of rendering as thick white borders against the new dark page bg - Explore testimonials modal: variant order before:dark:bg-black wasn't applying in Tailwind v4 so the testimonial bubble carets stayed white in dark mode; swapped to dark:before:bg-black on all three quotes so the carets match the bubble background
ref https://linear.app/tryghost/issue/DES-1397/ - Shade SheetContent base: bg-background → bg-surface-elevated so every Sheet (left/right/top/bottom variants) sits on the lifted surface in dark mode instead of blending into the page - Updated the 8 consumer sticky SheetHeader bands (top sources / top content / growth sources / locations across Stats and PostAnalytics plus the comment thread sidebar) from bg-background/60 to bg-surface-elevated/60 so the sticky cover matches the sheet surface
ref https://linear.app/tryghost/issue/DES-1397/ - Switched the .dark .shadow-* overrides from setting box-shadow directly to overriding --tw-shadow so Tailwind v4's composed box-shadow keeps any --tw-ring-shadow / --tw-inset-shadow layers intact. Previously the direct box-shadow nuked the ring stack for any component using shadow-* together with focus-visible:ring-* (calendar trigger, toggle, sidebar user menu) in dark mode - Re-read the current isDark in the _setAdminTheme then-callback so the stale value captured before _loadAdminThemeStylesheet() doesn't stomp the system theme listener if the OS theme flips while the dark stylesheet is loading - Updated the Colors / Interactive surfaces story so it scopes the "translucent in dark mode" guidance to the overlay tokens and calls out --table-row-hover as the opaque exception (it tracks --color-sidebar-bg, used where rows sit over scrolling content)
ref https://linear.app/ghost/issue/DES-1394 - shade-component-decision: removed ListPage from Pattern examples (it belongs in page-templates/), matching the same fix already in AGENTS.md and the Storybook MDX docs. - shade-tokens-not-hex: split the border row into three distinct entries — border-border-default (opaque, default), border-border (translucent, for floating surfaces with /60-/30 opacity modifiers), and border-control-border (form controls). Also clarified the role of theme-variables.css (token values + dark overrides) vs tailwind.theme.css (Tailwind v4 @theme utility mapping + raw scale). - shade-no-dark-variants: explained why bg-foo dark:bg-foo is bad — not just verbose, but reads as if dark mode is special-cased when it isn't. - shade-dropdown-surface-contract: added `text` language tag to the fenced contract block (markdownlint MD040). Skipped two CodeRabbit notes after verification: - "dark:bg-transparent not in inputSurface base" — CodeRabbit reviewed pre-rebase; after rebasing onto main (with PR #28895 merged), base does include dark:bg-transparent. Skill is correct. - "Add .css to no-dark-variants trigger" — dark: isn't valid CSS-file syntax and Shade doesn't use @apply dark: patterns. Low value. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Summary
A pass over Ghost admin's dark mode covering the Shade design system, the React admin shell, and the legacy Ember admin. The goal was to bring every surface, control, and state up to a consistent visual standard — no more isolated islands of lighter / darker fills, no green focus rings on form controls, no oversaturated greys.
ref https://linear.app/tryghost/issue/DES-1397/
Surface palette
--backgroundis nowoklch(0.178 0.003 271)(≈#101114) — slightly darker, very low chroma.--color-sidebar-bg(oklch(0.204 0.004 258)≈#15171A) cascades to--sidebar-background,--surface-elevated,--popover, and--table-row-hoverso the sidebar, cards, popovers, and table-row hovers share one elevated value.--surface-elevated-2(oklch(0.235 0.004 260)) for surfaces that sit ABOVE an elevated surface — sidebar user menu, dropdowns, modals, automation feature image placeholder.gray-*/grey-*scale in the.darkblock so dark mode reads as neutral grey rather than tinted blue.--main-bg-color,--dark-main-bg-color,.gh-main-white, the editor title / excerpt / btn / post-status / word-count, the fullscreen modal overlay, and the publish setting form all routed to the new page surface so they stop reading as light islands inside the editor and modals.Interactive surfaces (semantic hover/active tokens)
--interactive-hover— generic catch-all hover (DropdownMenu items, SelectItem, CommandItem, filter options, load-more, etc.).--button-hover— outline / dropdown Button hover (aliased to--interactive-hover).--tab-hover/--tab-active— Tabs (button,button-sm,pill,kpis),PageMenu, sidebar menu items, active Toggle in dark.--table-row-hover— ShadeTablerow hover, also used by table-like lists (top posts overlay, comments list, members sticky cell, legacy Ember posts list hover).Borders
--control-borderfor form controls (inputs, outline button, dropdown trigger, filter add button) — lifts a step above the base border so controls keep contrast against the page surface.gray-900/50so dividers and outlines read at the right contrast against the new surfaces; sidebar right border now references--borderdirectly.Shadows
.dark .shadow-*overrides (xs→2xl) so the standardshadow-md/shadow-lgutilities pick up alpha-0.45–0.8 shadows in dark automatically. Tailwind v4 inlines@theme --shadow-*at compile time, so token-level overrides don't propagate; direct selectors do.Dropdowns (DropdownMenu, Select, Popover — one visual contract)
bg-surface-elevated-2,border border-border/60(light) /dark:border-border/30(dark),shadow-md.Commandswitched tobg-transparentso it inherits the wrapping PopoverContent surface instead of overlaying withbg-popover.Modals
DialogOverlay/AlertDialogOverlay: dark uses transparent black (bg-black/60) instead of a white tint, no backdrop blur in either mode.DialogContentrouted tobg-surface-elevated-2.text-smwrapper so the body inherits base body size.DialogDescription: droppedtext-smso descriptions follow base body size too.Sidebar
--color-sidebar-bg; right border now references--border.gray-500so the active item carries the visual weight via--sidebar-accent-foreground.gray-900/30in dark, with hover lifting border + text to gray-400.dark:hover:bg-gray-900/30; active item stays at--sidebar-accent(which references--tab-active); active-on-hover keeps the active fill (no lightening).gray-800secondary tone in dark (matches ⌘K).--surface-elevated-2so it lifts above the sidebar.Form controls (Shade input recipe + Ember inputs)
inputSurfacerecipe:border-control-border,bg-transparentin dark..gh-input/.gh-select/.gh-input-x/ settings sidebarselect/ date-time picker: transparent fill in dark with a visible--lightgrey-l2border, neutral midgrey focus border + 25% midgrey ring (no more green focus)..ember-power-select-multiple-trigger): transparent bg + visible border..gh-input-x+.ember-power-select-trigger): transparent + visible border + ring on focus.input:focusdark rule so the nested<input>inside the date/time picker doesn't double-ring.Toggle + ToggleGroup
bg-backgroundwithborder-border, 9px radius (--input-group-radius). Active usesbg-tab-active(no more invertedbg-surface-elevated-2look).Tabs
button,button-sm,pill,kpisvariants all usedark:bg-tab-hoveranddark:bg-tab-active(active-on-hover preserved).Tabscount pill,TrendBadgetooltip, defaultTooltipcontent → use--popover/--surface-elevatedso tooltips lift in dark.Avatars
75→45everywhere (ShadeAvatar, Posts newsletter feedback, legacy Embergh-member-avatar) so members list / comments / member details / member activity stop reading as oversaturated.Primary button
--primarylifted fromgray-100(~97% lightness, near-white) togray-200(~95%) so the default Button doesn't burn eyes.Destructive
--destructivelifted fromred-900tored-500so destructive menu items / buttons (e.g. bulk Delete in members…menu) read clearly.Theme switching
html.theme-switchingkill-rule (transition: none !important; animation-duration: 0s !important) wrapping the Ember_setAdminThemehandler. The class is added before the.darkflip and released via double-rAF after the dark stylesheet finishes loading. Stops the per-element crossfade so theme swaps paint in a single frame.Other surface refinements
dark:bg-black→dark:bg-backgroundso the sticky header tracks the new page surface.--surface-elevated-2.--table-row-hover.bg-surface-elevated, canvas wrapper + React Flow background →bg-background. Automations list row hover promoted to the row (covers the column gap).Ember .ember-power-select-multiple-optionchip background lifted to--lightgreyso label/tag chips are visible against the new page surface.Storybook docs
Outline / dropdown Button bg
bg-transparentso they inherit whatever surface sits behind them (no more carved rectangles over elevated surfaces).Test plan
🤖 Generated with Claude Code