From 81a2215049eb51dd0155001c1487038dd5e41d28 Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Thu, 13 Aug 2026 10:54:07 +0200 Subject: [PATCH 1/2] Improved form and container border contrast (#29929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What changed - Set Shade's light-mode `--control-border` to a 50% OKLab mix between `gray-200` and `gray-300`. - Applied the same intermediate value to light-mode `--border-default` for bordered containers. - Routed `Card`, `EmptyCard`, and plain-card separators through `border-border-default` explicitly. - Kept the existing dark-mode values unchanged because they are tuned separately for dark surfaces. The shared control token applies consistently to text inputs, textareas, selects, comboboxes, input groups, and other composed form controls. The default border token covers framed containers and dividers, while Card now opts into that semantic border deliberately. ## Why The original gray-200 borders lacked definition, while gray-300 was visually too heavy. The midpoint preserves a quieter hierarchy while still increasing separation from white surfaces. ## Impact This is a backward-compatible visual adjustment. No component APIs, token names, focus states, or dark-mode values change. ## Validation - `pnpm --filter @tryghost/shade lint` - `pnpm --filter @tryghost/shade test` — 296 tests passed - `pnpm --filter @tryghost/shade build-storybook` - Shade token and merge-gate scans [DES-1474](https://linear.app/ghost/issue/DES-1474/bump-form-input-border-color-for-better-contrast) --- apps/shade/src/components/ui/card.tsx | 8 ++++---- .../test/unit/components/ui/card.test.tsx | 18 +++++++++++++----- apps/shade/theme-variables.css | 4 ++-- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/apps/shade/src/components/ui/card.tsx b/apps/shade/src/components/ui/card.tsx index 06c90daec1a..5f9e051cd1e 100644 --- a/apps/shade/src/components/ui/card.tsx +++ b/apps/shade/src/components/ui/card.tsx @@ -14,7 +14,7 @@ const cardVariants = cva( { variants: { variant: { - outline: 'rounded-xl border transition-all hover:shadow-xs', + outline: 'rounded-xl border border-border-default transition-all hover:shadow-xs', plain: '' } }, @@ -44,7 +44,7 @@ const cardHeaderVariants = cva( variants: { variant: { outline: 'p-6', - plain: 'border-b py-5' + plain: 'border-b border-border-default py-5' } }, defaultVariants: { @@ -98,7 +98,7 @@ const cardContentVariants = cva( variants: { variant: { outline: 'p-6 pt-0', - plain: 'border-b' + plain: 'border-b border-border-default' } }, defaultVariants: { @@ -159,7 +159,7 @@ const EmptyCard = React.forwardRef< >(({className, ...props}, ref) => (
)); diff --git a/apps/shade/test/unit/components/ui/card.test.tsx b/apps/shade/test/unit/components/ui/card.test.tsx index 9079efeb274..096928a1545 100644 --- a/apps/shade/test/unit/components/ui/card.test.tsx +++ b/apps/shade/test/unit/components/ui/card.test.tsx @@ -7,7 +7,8 @@ import { CardFooter, CardTitle, CardDescription, - CardContent + CardContent, + EmptyCard } from '../../../../src/components/ui/card'; import {render} from '../../utils/test-utils'; @@ -18,7 +19,7 @@ describe('Card Components', () => { assert.ok(card, 'Card should be rendered'); assert.equal(card.textContent, 'Card Content', 'Card should render its content'); - assert.ok(card.className.includes('rounded-xl border'), 'Should have outline variant styling'); + assert.ok(card.className.includes('rounded-xl border border-border-default'), 'Should have outline variant styling'); }); it('renders Card with plain variant', () => { @@ -36,6 +37,13 @@ describe('Card Components', () => { assert.ok(card.className.includes('custom-card-class'), 'Should have custom class'); }); + it('renders EmptyCard with the default container border', () => { + render(Empty Card); + + const card = screen.getByTestId('empty-card'); + assert.ok(card.className.includes('border border-border-default'), 'Should use the default container border'); + }); + it('renders CardHeader with correct styling based on Card variant', () => { render( @@ -57,7 +65,7 @@ describe('Card Components', () => { ); const header = screen.getByTestId('card-header'); - assert.ok(header.className.includes('border-b py-5'), 'Should have plain variant styling'); + assert.ok(header.className.includes('border-b border-border-default py-5'), 'Should have plain variant styling'); }); it('renders CardTitle with correct styling', () => { @@ -99,7 +107,7 @@ describe('Card Components', () => { ); const content = screen.getByTestId('card-content'); - assert.ok(content.className.includes('border-b'), 'Should have plain variant styling'); + assert.ok(content.className.includes('border-b border-border-default'), 'Should have plain variant styling'); }); it('renders CardFooter with correct styling based on Card variant', () => { @@ -145,4 +153,4 @@ describe('Card Components', () => { assert.ok(screen.getByTestId('card-content'), 'CardContent should be rendered'); assert.ok(screen.getByTestId('card-footer'), 'CardFooter should be rendered'); }); -}); \ No newline at end of file +}); diff --git a/apps/shade/theme-variables.css b/apps/shade/theme-variables.css index b1ceabd79c7..0321ca9046b 100644 --- a/apps/shade/theme-variables.css +++ b/apps/shade/theme-variables.css @@ -39,7 +39,7 @@ --text-tertiary: var(--color-gray-400); --text-inverse: var(--color-white); --border-subtle: var(--color-gray-200); - --border-default: var(--color-gray-200); + --border-default: color-mix(in oklab, var(--color-gray-200), var(--color-gray-300)); --border-strong: var(--color-gray-500); --focus-ring: var(--color-gray-600); --state-info: var(--color-blue-500); @@ -83,7 +83,7 @@ --control-surface: var(--surface-elevated); --control-readonly-surface: var(--color-gray-50); --control-disabled-surface: var(--color-gray-100); - --control-border: var(--color-gray-200); + --control-border: color-mix(in oklab, var(--color-gray-200), var(--color-gray-300)); --table-row-hover: var(--color-gray-50); --members-sticky-hover-bg: var(--table-row-hover); --mobile-navbar-height: 64px; From b91694f57119f203e00d8d8190bef74024919cdf Mon Sep 17 00:00:00 2001 From: louisghost Date: Thu, 13 Aug 2026 11:40:02 +0200 Subject: [PATCH 2/2] Update AGENTS.md (#29930) --- AGENTS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 4ee650360a1..42b6f935639 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -174,6 +174,7 @@ Conventions: - **Use:** `admin-x-framework` for API hooks (`useBrowse`, `useEdit`, etc.) - **Use:** `shade` design system for new components (not admin-x-design-system) - **Translations:** Add to `packages/i18n/locales/en/ghost.json` +- **Deploy skew:** Ghost Admin and Ghost core deploy independently. New admin UI that depends on new settings, endpoints, or config must feature-detect backend support (e.g. settings-key presence in the browse response, as in `social-accounts.tsx`) and hide or no-op when absent. Labs flags alone are not a deploy-skew guard. Add an acceptance test for the “backend not deployed yet” case. ### When Working on Public UI - **Edit:** `apps/portal`, `apps/comments-ui`, etc. @@ -187,6 +188,8 @@ Conventions: - **Services:** `ghost/core/core/server/services/` - **Models:** `ghost/core/core/server/models/` - **Frontend & theme rendering:** `ghost/core/core/frontend/` +- **TypeScript by default:** New code under `ghost/core/core/server/services/` is TypeScript unless extending an existing JS module. Follow the gifts/donations pattern: domain logic as `.ts` with named exports; thin CJS `index.js` / `*-wrapper.js` only where boot/`require` still needs them. +- **Service init:** New services get an explicit `init()` call from `ghost/core/core/boot.js` (same Promise.all as donations/gifts). Keep the wrapper’s `init()` idempotent so early callers are safe, but boot owns construction — not first request. ### Design System Usage - **New components:** Use `shade` (shadcn/ui-inspired)