From 91cb7b89b6a8e32220006d8e468ce3ef106ea5f5 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 16:32:35 -0800 Subject: [PATCH 01/23] =?UTF-8?q?Retune=20the=20slate=20palette=20to=20Cra?= =?UTF-8?q?ft=205=E2=80=99s=20grays?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slate’s color keys are now Craft 5’s gray-050 through gray-900, so base, which shares them, and the surfaces built on it, take Craft 5’s blue-gray. base-200’s ratio drops to 1.1 to land on Craft 5’s gray-100. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- .../src/components/radio-group/RadioGroup.mdx | 2 +- .../radio-group/radio-group.browser.test.ts | 6 +- .../src/styles/color-definitions.js | 4 +- .../src/styles/shared/color-palette.css | 734 +++++++++--------- 4 files changed, 373 insertions(+), 373 deletions(-) diff --git a/packages/craftcms-ui/src/components/radio-group/RadioGroup.mdx b/packages/craftcms-ui/src/components/radio-group/RadioGroup.mdx index ab489aed8ef..99c0ba20ebc 100644 --- a/packages/craftcms-ui/src/components/radio-group/RadioGroup.mdx +++ b/packages/craftcms-ui/src/components/radio-group/RadioGroup.mdx @@ -64,7 +64,7 @@ Icons on a card are 60px tall by default (`--c-radio-card-icon-size`), and are s /> ``` -The tokens are palette stops, so the illustration follows the theme, and its outlines turn blue when its option is selected. Put the tokens in `style` rather than in the `fill` and `stroke` attributes, so any `fill` or `stroke` a drawing tool exported can stay: `style` overrides it in the control panel, and it still shows when the file is opened on its own. To make the file preview correctly without one, add the light-theme color as a fallback, as in `var(--c-illustration-figure, #6f7c94)`. +The tokens are palette stops, so the illustration follows the theme, and its outlines turn blue when its option is selected. Put the tokens in `style` rather than in the `fill` and `stroke` attributes, so any `fill` or `stroke` a drawing tool exported can stay: `style` overrides it in the control panel, and it still shows when the file is opened on its own. To make the file preview correctly without one, add the light-theme color as a fallback, as in `var(--c-illustration-figure, #727d8b)`. Each row below draws the same sample illustration with everything but that role faded, in the current theme. Roles that are hard to spot are outlined in dashed blue, which isn't part of the palette. diff --git a/packages/craftcms-ui/src/components/radio-group/radio-group.browser.test.ts b/packages/craftcms-ui/src/components/radio-group/radio-group.browser.test.ts index ea1ee15055e..3410a4ef1ec 100644 --- a/packages/craftcms-ui/src/components/radio-group/radio-group.browser.test.ts +++ b/packages/craftcms-ui/src/components/radio-group/radio-group.browser.test.ts @@ -423,9 +423,9 @@ it('marks the selected card with a thicker border and a bold label', async () => }); it('draws the selected illustration in blue, following the selection and the theme', async () => { - const BLUE_500 = 'rgb(62, 116, 242)'; - const SLATE_500 = 'rgb(111, 124, 148)'; - const SLATE_500_DARK = 'rgb(143, 155, 175)'; + const BLUE_500 = 'rgb(62, 117, 242)'; + const SLATE_500 = 'rgb(114, 125, 139)'; + const SLATE_500_DARK = 'rgb(144, 155, 169)'; const {radios, inputs} = await renderCards('40rem'); const hairline = (radio: CraftRadio) => getComputedStyle( diff --git a/packages/craftcms-ui/src/styles/color-definitions.js b/packages/craftcms-ui/src/styles/color-definitions.js index fcb11824292..5d6fff6fa6d 100644 --- a/packages/craftcms-ui/src/styles/color-definitions.js +++ b/packages/craftcms-ui/src/styles/color-definitions.js @@ -4,10 +4,10 @@ const contrastRatios = { static: [1.45, 1.78, 2.22, 2.78, 3.54, 4.61, 5.92, 7.81, 10.21, 13.01, 15.91], light: [1.08, 1.33, 1.58, 2.39, 3.01, 3.87, 5.07, 6.72, 8.84, 11.31, 13.94], dark: [1.08, 1.58, 1.96, 2.45, 3.09, 3.9, 5.07, 6.02, 7.34, 8.77, 10.18], - base: [-1.2, 1, 1.2, 1.4, 2, 4, 5, 6.5, 10.21, 13.58, 17.04], + base: [-1.2, 1, 1.1, 1.4, 2, 4, 5, 6.5, 10.21, 13.58, 17.04], }; -const slateColorKeys = ['#f2f5f8', '#cbd5e0', '#64728b', '#48546a', '#1f283a']; +const slateColorKeys = ['#f3f7fc', '#cdd8e4', '#616d7b', '#3f4d5a', '#1f2933']; const colorKeys = { gray: ['#e4e6ea', '#f9fafb', '#9aa0ad', '#4c5463', '#6b7180', '#101727'], diff --git a/packages/craftcms-ui/src/styles/shared/color-palette.css b/packages/craftcms-ui/src/styles/shared/color-palette.css index 1b56b7f1937..7f4d2b8233d 100644 --- a/packages/craftcms-ui/src/styles/shared/color-palette.css +++ b/packages/craftcms-ui/src/styles/shared/color-palette.css @@ -3,259 +3,259 @@ :root, :host, [data-theme='light'] { - --color-gray-50: #eaecef; + --color-gray-50: #ecedf0; --color-gray-100: #d4d7dd; --color-gray-200: #c2c6ce; - --color-gray-300: #9ba1ae; - --color-gray-400: #888e9c; - --color-gray-500: #757b8a; - --color-gray-600: #626978; - --color-gray-700: #4f5665; + --color-gray-300: #9ca1ae; + --color-gray-400: #898f9c; + --color-gray-500: #767c8b; + --color-gray-600: #636978; + --color-gray-700: #505766; --color-gray-800: #3e4555; --color-gray-900: #2e3545; - --color-gray-950: #1e2635; - --color-red-50: #fae8e8; + --color-gray-950: #1f2736; + --color-red-50: #fbeaea; --color-red-100: #f6cdcc; - --color-red-200: #f4b6b4; - --color-red-300: #ee817e; - --color-red-400: #e8645f; - --color-red-500: #dc463e; - --color-red-600: #c72c22; - --color-red-700: #a32720; - --color-red-800: #80231f; - --color-red-900: #631b18; - --color-red-950: #4b110f; - --color-orange-50: #fbead2; - --color-orange-100: #f7d0a1; - --color-orange-200: #f4ba7c; - --color-orange-300: #ed8633; - --color-orange-400: #e6692c; - --color-orange-500: #d45124; - --color-orange-600: #b5441d; - --color-orange-700: #963816; - --color-orange-800: #792c12; - --color-orange-900: #5e220e; - --color-orange-950: #44180b; - --color-amber-50: #fbecaf; - --color-amber-100: #f7d36f; - --color-amber-200: #f4bd44; - --color-amber-300: #df8e32; - --color-amber-400: #ca7c2c; - --color-amber-500: #b56825; - --color-amber-600: #a0551e; - --color-amber-700: #8a4217; - --color-amber-800: #703411; - --color-amber-900: #58270b; - --color-amber-950: #411b06; + --color-red-200: #f4b8b7; + --color-red-300: #ee8280; + --color-red-400: #e86561; + --color-red-500: #dd473f; + --color-red-600: #c82c22; + --color-red-700: #a42720; + --color-red-800: #82231f; + --color-red-900: #651b19; + --color-red-950: #4c120f; + --color-orange-50: #fbecd6; + --color-orange-100: #f7d2a3; + --color-orange-200: #f5bb7e; + --color-orange-300: #ed8833; + --color-orange-400: #e66a2c; + --color-orange-500: #d55124; + --color-orange-600: #b6441d; + --color-orange-700: #983816; + --color-orange-800: #792d12; + --color-orange-900: #5f220e; + --color-orange-950: #45190b; + --color-amber-50: #fceeb7; + --color-amber-100: #f7d470; + --color-amber-200: #f4be46; + --color-amber-300: #e08f33; + --color-amber-400: #cb7c2c; + --color-amber-500: #b66925; + --color-amber-600: #a0561e; + --color-amber-700: #8c4217; + --color-amber-800: #713511; + --color-amber-900: #58280c; + --color-amber-950: #421c06; --color-emerald-50: #cff6e1; --color-emerald-100: #9ae7be; --color-emerald-200: #77d9a6; - --color-emerald-300: #51b37f; - --color-emerald-400: #479e70; - --color-emerald-500: #3d8961; - --color-emerald-600: #327555; - --color-emerald-700: #286048; + --color-emerald-300: #51b380; + --color-emerald-400: #479f70; + --color-emerald-500: #3d8b62; + --color-emerald-600: #337555; + --color-emerald-700: #286149; --color-emerald-800: #1e4d3c; - --color-emerald-900: #173b2e; - --color-emerald-950: #112a21; - --color-blue-50: #e2edfd; + --color-emerald-900: #173c2f; + --color-emerald-950: #112b22; + --color-blue-50: #e4effd; --color-blue-100: #c1d9fb; --color-blue-200: #a5c8fa; - --color-blue-300: #68a2f7; - --color-blue-400: #518cf6; - --color-blue-500: #3e74f2; - --color-blue-600: #305ce7; - --color-blue-700: #2244d7; - --color-blue-800: #2138a7; - --color-blue-900: #1d2d7a; - --color-blue-950: #192250; + --color-blue-300: #69a3f7; + --color-blue-400: #528cf6; + --color-blue-500: #3e75f2; + --color-blue-600: #305de7; + --color-blue-700: #2244d9; + --color-blue-800: #2139a7; + --color-blue-900: #1d2d7c; + --color-blue-950: #192351; --color-yellow-50: #fcef90; - --color-yellow-100: #f7d358; - --color-yellow-200: #eebf40; - --color-yellow-300: #ce9733; - --color-yellow-400: #bd822c; - --color-yellow-500: #aa6f24; - --color-yellow-600: #945c1d; - --color-yellow-700: #7c4b16; - --color-yellow-800: #633c12; - --color-yellow-900: #4c2e0e; - --color-yellow-950: #37210a; - --color-slate-50: #e9eef3; - --color-slate-100: #ced7e2; - --color-slate-200: #bdc7d4; - --color-slate-300: #95a2b4; - --color-slate-400: #828fa4; - --color-slate-500: #6f7c94; - --color-slate-600: #5c6982; - --color-slate-700: #4a576d; - --color-slate-800: #3a455a; - --color-slate-900: #2b3549; - --color-slate-950: #1e2637; + --color-yellow-100: #f7d55c; + --color-yellow-200: #efc041; + --color-yellow-300: #cf9734; + --color-yellow-400: #be832d; + --color-yellow-500: #aa6f25; + --color-yellow-600: #955c1d; + --color-yellow-700: #7c4c16; + --color-yellow-800: #633d12; + --color-yellow-900: #4c2f0e; + --color-yellow-950: #37220a; + --color-slate-50: #e8eef5; + --color-slate-100: #cfd9e5; + --color-slate-200: #bcc7d3; + --color-slate-300: #97a3b0; + --color-slate-400: #84909d; + --color-slate-500: #727d8b; + --color-slate-600: #5e6b78; + --color-slate-700: #4b5866; + --color-slate-800: #3a4754; + --color-slate-900: #2b3642; + --color-slate-950: #1e2731; --color-lime-50: #ddf7a2; - --color-lime-100: #b4e758; + --color-lime-100: #b7e85d; --color-lime-200: #9ed742; - --color-lime-300: #7ab034; - --color-lime-400: #6a9c2d; - --color-lime-500: #5d8726; + --color-lime-300: #7bb134; + --color-lime-400: #6a9d2d; + --color-lime-500: #5e8826; --color-lime-600: #50731f; - --color-lime-700: #435e18; - --color-lime-800: #344c13; - --color-lime-900: #273a0e; - --color-lime-950: #1b2a09; + --color-lime-700: #445f19; + --color-lime-800: #354d13; + --color-lime-900: #283b0e; + --color-lime-950: #1c2b0a; --color-green-50: #cef7d8; --color-green-100: #96e9a8; --color-green-200: #6add82; - --color-green-300: #52b555; - --color-green-400: #48a04a; - --color-green-500: #3e8b42; - --color-green-600: #34763b; - --color-green-700: #2b6133; - --color-green-800: #234e2b; - --color-green-900: #1b3b21; + --color-green-300: #52b656; + --color-green-400: #48a14a; + --color-green-500: #3e8c43; + --color-green-600: #34773b; + --color-green-700: #2c6234; + --color-green-800: #234e2c; + --color-green-900: #1b3c22; --color-green-950: #142b18; --color-teal-50: #c7f7ec; --color-teal-100: #90e7d6; - --color-teal-200: #6dd7c4; - --color-teal-300: #4fb0a1; - --color-teal-400: #459c90; - --color-teal-500: #3b877f; - --color-teal-600: #31736d; + --color-teal-200: #70d8c6; + --color-teal-300: #50b1a2; + --color-teal-400: #469d91; + --color-teal-500: #3c8880; + --color-teal-600: #31746d; --color-teal-700: #275f5b; - --color-teal-800: #1e4c49; - --color-teal-900: #163a39; - --color-teal-950: #0f2a2a; + --color-teal-800: #1e4c4a; + --color-teal-900: #173b3a; + --color-teal-950: #0f2b2b; --color-cyan-50: #c9f5fc; --color-cyan-100: #7fe6fa; --color-cyan-200: #66d4f1; - --color-cyan-300: #4dacce; - --color-cyan-400: #4397bb; - --color-cyan-500: #3983a5; - --color-cyan-600: #2f6f8d; - --color-cyan-700: #265c74; - --color-cyan-800: #1e4a5e; - --color-cyan-900: #17384b; - --color-cyan-950: #102836; - --color-sky-50: #dceffc; - --color-sky-100: #aaddfb; - --color-sky-200: #84cefa; - --color-sky-300: #4ca8eb; - --color-sky-400: #4193d9; - --color-sky-500: #377fc5; - --color-sky-600: #2e6ca7; - --color-sky-700: #25598a; - --color-sky-800: #1d476f; - --color-sky-900: #163655; - --color-sky-950: #10273e; + --color-cyan-300: #4eacce; + --color-cyan-400: #4498bc; + --color-cyan-500: #3a84a6; + --color-cyan-600: #30708e; + --color-cyan-700: #265d75; + --color-cyan-800: #1e4b60; + --color-cyan-900: #17394c; + --color-cyan-950: #112938; + --color-sky-50: #e0f0fc; + --color-sky-100: #aedefb; + --color-sky-200: #86cffa; + --color-sky-300: #4ca9eb; + --color-sky-400: #4294da; + --color-sky-500: #387fc7; + --color-sky-600: #2e6ca8; + --color-sky-700: #255a8a; + --color-sky-800: #1d486f; + --color-sky-900: #163757; + --color-sky-950: #102840; --color-zinc-50: #ededee; - --color-zinc-100: #d6d6da; - --color-zinc-200: #c5c5ca; - --color-zinc-300: #a0a0a7; - --color-zinc-400: #8d8d95; - --color-zinc-500: #7b7b83; + --color-zinc-100: #d7d7db; + --color-zinc-200: #c6c6cb; + --color-zinc-300: #a1a1a8; + --color-zinc-400: #8e8e96; + --color-zinc-500: #7b7b84; --color-zinc-600: #686872; - --color-zinc-700: #55555f; + --color-zinc-700: #565660; --color-zinc-800: #44444c; - --color-zinc-900: #34343b; - --color-zinc-950: #25252b; + --color-zinc-900: #35353b; + --color-zinc-950: #26262c; --color-violet-50: #eeebfd; - --color-violet-100: #d8d2fb; - --color-violet-200: #c9bdfa; - --color-violet-300: #a991f8; - --color-violet-400: #9979f7; - --color-violet-500: #8b5df5; - --color-violet-600: #7c3cf4; - --color-violet-700: #6a1be3; - --color-violet-800: #5518b5; - --color-violet-900: #401889; - --color-violet-950: #2d0f69; - --color-purple-50: #f2eafd; - --color-purple-100: #e2cefb; - --color-purple-200: #d6b9f9; - --color-purple-300: #be88f8; - --color-purple-400: #b16df7; - --color-purple-500: #a24ff6; - --color-purple-600: #902bf2; - --color-purple-700: #7815d6; - --color-purple-800: #6118a6; - --color-purple-900: #4b167c; - --color-purple-950: #380963; - --color-fuchsia-50: #f7e7fd; - --color-fuchsia-100: #eecafb; - --color-fuchsia-200: #e9b2f9; - --color-fuchsia-300: #dd78f7; - --color-fuchsia-400: #d15bed; - --color-fuchsia-500: #c23be0; - --color-fuchsia-600: #ac22c8; - --color-fuchsia-700: #8e1ea3; - --color-fuchsia-800: #721c7f; - --color-fuchsia-900: #571861; - --color-fuchsia-950: #401146; - --color-pink-50: #f9e7f2; - --color-pink-100: #f4cbe4; - --color-pink-200: #f1b3d8; - --color-pink-300: #eb7bba; - --color-pink-400: #e65ba6; - --color-pink-500: #db3b88; - --color-pink-600: #c2296a; - --color-pink-700: #a22054; - --color-pink-800: #801d44; - --color-pink-900: #631735; - --color-pink-950: #481127; - --color-rose-50: #fbe8ea; - --color-rose-100: #f7ccd0; - --color-rose-200: #f3b6bd; - --color-rose-300: #ee7f8f; - --color-rose-400: #ec5e74; - --color-rose-500: #e43852; - --color-rose-600: #c7293f; - --color-rose-700: #a52138; - --color-rose-800: #851930; - --color-rose-900: #681225; - --color-rose-950: #4e0b1b; + --color-violet-100: #d9d3fb; + --color-violet-200: #cbbffa; + --color-violet-300: #a992f8; + --color-violet-400: #9a79f7; + --color-violet-500: #8b5ef5; + --color-violet-600: #7d3df4; + --color-violet-700: #6b1be4; + --color-violet-800: #5518b7; + --color-violet-900: #41188a; + --color-violet-950: #2e106a; + --color-purple-50: #f3ebfd; + --color-purple-100: #e3d0fb; + --color-purple-200: #d7bbfa; + --color-purple-300: #be89f8; + --color-purple-400: #b16ff7; + --color-purple-500: #a251f6; + --color-purple-600: #902df2; + --color-purple-700: #7916d6; + --color-purple-800: #6218a7; + --color-purple-900: #4b177e; + --color-purple-950: #380a64; + --color-fuchsia-50: #f7e9fd; + --color-fuchsia-100: #eeccfb; + --color-fuchsia-200: #eab3f9; + --color-fuchsia-300: #de79f7; + --color-fuchsia-400: #d25bee; + --color-fuchsia-500: #c23de0; + --color-fuchsia-600: #ad23c9; + --color-fuchsia-700: #8f1ea4; + --color-fuchsia-800: #731c80; + --color-fuchsia-900: #581862; + --color-fuchsia-950: #411148; + --color-pink-50: #f9eaf3; + --color-pink-100: #f4cce4; + --color-pink-200: #f1b5d8; + --color-pink-300: #eb7cbb; + --color-pink-400: #e65da8; + --color-pink-500: #db3c89; + --color-pink-600: #c3296b; + --color-pink-700: #a32055; + --color-pink-800: #811d44; + --color-pink-900: #641836; + --color-pink-950: #491128; + --color-rose-50: #fbeaec; + --color-rose-100: #f7ced2; + --color-rose-200: #f4b7be; + --color-rose-300: #ef808f; + --color-rose-400: #ec5f75; + --color-rose-500: #e53954; + --color-rose-600: #c72a3f; + --color-rose-700: #a62138; + --color-rose-800: #861931; + --color-rose-900: #691226; + --color-rose-950: #4f0b1c; --color-neutral-50: #ededed; --color-neutral-100: #d7d7d7; - --color-neutral-200: #c5c5c5; - --color-neutral-300: #a0a0a0; - --color-neutral-400: #8e8e8e; + --color-neutral-200: #c6c6c6; + --color-neutral-300: #a1a1a1; + --color-neutral-400: #8f8f8f; --color-neutral-500: #7c7c7c; - --color-neutral-600: #686868; - --color-neutral-700: #565656; - --color-neutral-800: #444444; - --color-neutral-900: #343434; - --color-neutral-950: #262626; + --color-neutral-600: #696969; + --color-neutral-700: #575757; + --color-neutral-800: #454545; + --color-neutral-900: #353535; + --color-neutral-950: #272727; --color-stone-50: #eeedec; --color-stone-100: #d8d7d5; - --color-stone-200: #c8c5c2; - --color-stone-300: #a4a09b; - --color-stone-400: #928e88; - --color-stone-500: #807a75; - --color-stone-600: #6d6862; - --color-stone-700: #5a5550; - --color-stone-800: #48443f; - --color-stone-900: #373430; - --color-stone-950: #282523; - --color-indigo-50: #e7ebfd; + --color-stone-200: #c8c6c3; + --color-stone-300: #a5a19c; + --color-stone-400: #938e89; + --color-stone-500: #817b76; + --color-stone-600: #6e6863; + --color-stone-700: #5b5651; + --color-stone-800: #494540; + --color-stone-900: #383531; + --color-stone-950: #292623; + --color-indigo-50: #e9edfd; --color-indigo-100: #cfd5fb; - --color-indigo-200: #bbc2fa; - --color-indigo-300: #9198f8; - --color-indigo-400: #7b83f6; - --color-indigo-500: #6b6cec; - --color-indigo-600: #5a54e1; - --color-indigo-700: #483bd6; - --color-indigo-800: #3a2eaf; - --color-indigo-900: #2e2681; - --color-indigo-950: #221e56; + --color-indigo-200: #bcc3fa; + --color-indigo-300: #9299f8; + --color-indigo-400: #7c84f6; + --color-indigo-500: #6c6dec; + --color-indigo-600: #5b55e2; + --color-indigo-700: #493cd6; + --color-indigo-800: #3a2eb1; + --color-indigo-900: #2e2781; + --color-indigo-950: #221e58; --color-base-50: #ffffff; - --color-base-100: #f2f5f8; - --color-base-200: #dae1e9; - --color-base-300: #c8d2de; - --color-base-400: #a6b1c1; - --color-base-500: #6c7a92; - --color-base-600: #5d6a83; - --color-base-700: #4d5970; - --color-base-800: #313c50; - --color-base-900: #1f283b; - --color-base-950: #0f141c; + --color-base-100: #f2f6fb; + --color-base-200: #e6ecf4; + --color-base-300: #c9d4e0; + --color-base-400: #a7b2bf; + --color-base-500: #6f7b89; + --color-base-600: #5f6c79; + --color-base-700: #4d5b68; + --color-base-800: #313e49; + --color-base-900: #1f2934; + --color-base-950: #10151b; /* Static colors */ --color-static-gray-50: #d3d6dc; @@ -335,17 +335,17 @@ --color-static-yellow-800: #5d3811; --color-static-yellow-900: #462b0d; --color-static-yellow-950: #311e09; - --color-static-slate-50: #ced7e2; + --color-static-slate-50: #ccd7e3; --color-static-slate-100: #b8c3d0; - --color-static-slate-200: #a4afc0; - --color-static-slate-300: #909caf; - --color-static-slate-400: #7c899f; - --color-static-slate-500: #68758e; - --color-static-slate-600: #57647c; - --color-static-slate-700: #475268; - --color-static-slate-800: #374256; - --color-static-slate-900: #283144; - --color-static-slate-950: #1a2232; + --color-static-slate-200: #a4afbc; + --color-static-slate-300: #909ca9; + --color-static-slate-400: #7e8997; + --color-static-slate-500: #6b7684; + --color-static-slate-600: #596573; + --color-static-slate-700: #465361; + --color-static-slate-800: #36424f; + --color-static-slate-900: #27323d; + --color-static-slate-950: #1a232b; --color-static-lime-50: #b4e758; --color-static-lime-100: #9bd340; --color-static-lime-200: #87be39; @@ -501,16 +501,16 @@ --color-static-indigo-900: #2b2474; --color-static-indigo-950: #1e1b4d; --color-static-base-50: #ffffff; - --color-static-base-100: #fefefe; - --color-static-base-200: #e5ebf0; - --color-static-base-300: #d2dbe4; - --color-static-base-400: #acb8c7; - --color-static-base-500: #728097; - --color-static-base-600: #627089; - --color-static-base-700: #525e76; - --color-static-base-800: #374256; - --color-static-base-900: #252e41; - --color-static-base-950: #161c29; + --color-static-base-100: #fdfefe; + --color-static-base-200: #f0f4fa; + --color-static-base-300: #d1dbe6; + --color-static-base-400: #adb8c5; + --color-static-base-500: #75808e; + --color-static-base-600: #65717e; + --color-static-base-700: #535f6d; + --color-static-base-800: #36424f; + --color-static-base-900: #252f3a; + --color-static-base-950: #161c23; } /* What's in a black card, (filled) chip, or badge is themed dark, so it holds @@ -523,125 +523,125 @@ ) { --color-gray-50: #3b4252; --color-gray-100: #535a69; - --color-gray-200: #626877; + --color-gray-200: #616877; --color-gray-300: #717786; --color-gray-400: #828896; --color-gray-500: #949aa8; - --color-gray-600: #acb1bc; + --color-gray-600: #abb1bb; --color-gray-700: #bcc0c9; --color-gray-800: #d0d4da; - --color-gray-900: #e5e6ea; - --color-gray-950: #f6f7f9; + --color-gray-900: #e4e6ea; + --color-gray-950: #f5f6f8; --color-red-50: #7b221f; - --color-red-100: #ab2821; + --color-red-100: #aa2821; --color-red-200: #c62b22; - --color-red-300: #da3e35; - --color-red-400: #e55b56; + --color-red-300: #d93e35; + --color-red-400: #e45a55; --color-red-500: #ed7775; - --color-red-600: #f19997; - --color-red-700: #f3afae; - --color-red-800: #f6c9c8; - --color-red-900: #f9e1e0; + --color-red-600: #f19996; + --color-red-700: #f3afad; + --color-red-800: #f6c8c7; + --color-red-900: #f9e0e0; --color-red-950: #fdf5f5; --color-orange-50: #732a11; --color-orange-100: #9c3a17; --color-orange-200: #b3441d; --color-orange-300: #cd4e22; --color-orange-400: #e35f29; - --color-orange-500: #eb7d31; + --color-orange-500: #ea7d30; --color-orange-600: #f19e4d; --color-orange-700: #f4b371; - --color-orange-800: #f7cc9b; - --color-orange-900: #fae3c4; - --color-orange-950: #fdf6eb; - --color-amber-50: #6c3210; - --color-amber-100: #8f4618; + --color-orange-800: #f7cc9a; + --color-orange-900: #fae2c3; + --color-orange-950: #fdf5ea; + --color-amber-50: #6b3110; + --color-amber-100: #8f4518; --color-amber-200: #9f541d; - --color-amber-300: #b16423; + --color-amber-300: #b06423; --color-amber-400: #c47529; --color-amber-500: #d88830; - --color-amber-600: #f0a038; - --color-amber-700: #f3b63f; - --color-amber-800: #f7cf67; - --color-amber-900: #fae694; - --color-amber-950: #fdf7e1; - --color-emerald-50: #1d4a3a; - --color-emerald-100: #2a654b; + --color-amber-600: #f09f38; + --color-amber-700: #f3b53e; + --color-amber-800: #f7cf66; + --color-amber-900: #fae692; + --color-amber-950: #fdf7dd; + --color-emerald-50: #1d4939; + --color-emerald-100: #2a644a; --color-emerald-200: #327454; --color-emerald-300: #3a855f; - --color-emerald-400: #44986b; + --color-emerald-400: #44986a; --color-emerald-500: #4dac7a; --color-emerald-600: #59c48d; - --color-emerald-700: #6bd49e; + --color-emerald-700: #6ad49e; --color-emerald-800: #94e4ba; --color-emerald-900: #bdf2d5; - --color-emerald-950: #eafbf2; + --color-emerald-950: #e8fbf1; --color-blue-50: #20369e; - --color-blue-100: #2448de; + --color-blue-100: #2347de; --color-blue-200: #2f5be7; --color-blue-300: #3b6ff0; --color-blue-400: #4a84f6; - --color-blue-500: #609bf7; - --color-blue-600: #83b3f8; + --color-blue-500: #609af7; + --color-blue-600: #83b2f8; --color-blue-700: #9dc2f9; --color-blue-800: #bcd5fb; --color-blue-900: #d9e7fc; - --color-blue-950: #f3f8fe; - --color-yellow-50: #5f3a11; - --color-yellow-100: #814f17; - --color-yellow-200: #945b1d; + --color-blue-950: #f2f7fe; + --color-yellow-50: #5e3911; + --color-yellow-100: #804e17; + --color-yellow-200: #935b1c; --color-yellow-300: #a56a23; --color-yellow-400: #b77c2a; - --color-yellow-500: #c99031; + --color-yellow-500: #c98f31; --color-yellow-600: #dca839; - --color-yellow-700: #eaba3f; + --color-yellow-700: #e9ba3f; --color-yellow-800: #f6d054; - --color-yellow-900: #fbe782; - --color-yellow-950: #fdf8d0; - --color-slate-50: #374257; - --color-slate-100: #4e5b71; - --color-slate-200: #5c6981; - --color-slate-300: #6b7890; - --color-slate-400: #7c899f; - --color-slate-500: #8f9baf; - --color-slate-600: #a6b2c2; - --color-slate-700: #b7c1cf; - --color-slate-800: #cbd5e0; - --color-slate-900: #e1e7ee; - --color-slate-950: #f5f7fa; + --color-yellow-900: #fae781; + --color-yellow-950: #fdf8cf; + --color-slate-50: #36434f; + --color-slate-100: #4e5b69; + --color-slate-200: #5d6977; + --color-slate-300: #6d7886; + --color-slate-400: #7e8997; + --color-slate-500: #909ba9; + --color-slate-600: #a6b2be; + --color-slate-700: #b6c1ce; + --color-slate-800: #c9d5e1; + --color-slate-900: #dfe7ef; + --color-slate-950: #f3f7fc; --color-lime-50: #324912; - --color-lime-100: #46631a; + --color-lime-100: #466219; --color-lime-200: #50721f; --color-lime-300: #5a8324; - --color-lime-400: #66962b; + --color-lime-400: #66952a; --color-lime-500: #74a931; - --color-lime-600: #8ac13a; - --color-lime-700: #9ad140; - --color-lime-800: #aee54d; - --color-lime-900: #d2f28e; - --color-lime-950: #f2fcd7; + --color-lime-600: #8ac03a; + --color-lime-700: #99d13f; + --color-lime-800: #aee44d; + --color-lime-900: #d2f28d; + --color-lime-950: #f1fbd6; --color-green-50: #214a29; - --color-green-100: #2d6635; - --color-green-200: #34753a; - --color-green-300: #3b8740; - --color-green-400: #449a47; - --color-green-500: #4fae51; - --color-green-600: #5ac760; + --color-green-100: #2d6534; + --color-green-200: #33753a; + --color-green-300: #3b8640; + --color-green-400: #449947; + --color-green-500: #4eae51; + --color-green-600: #5ac660; --color-green-700: #63d878; - --color-green-800: #8fe7a2; + --color-green-800: #8ee6a1; --color-green-900: #bcf3c9; - --color-green-950: #ebfbee; + --color-green-950: #eafbee; --color-teal-50: #1d4946; --color-teal-100: #29635e; --color-teal-200: #31726c; --color-teal-300: #39837b; - --color-teal-400: #42968b; + --color-teal-400: #42958b; --color-teal-500: #4ca99c; - --color-teal-600: #58c2b0; + --color-teal-600: #58c1b0; --color-teal-700: #63d2c0; - --color-teal-800: #8ae4d3; - --color-teal-900: #aef4e5; - --color-teal-950: #e8fbf6; + --color-teal-800: #88e4d2; + --color-teal-900: #acf4e4; + --color-teal-950: #e7fbf6; --color-cyan-50: #1c465a; --color-cyan-100: #276079; --color-cyan-200: #2f6f8c; @@ -649,31 +649,31 @@ --color-cyan-400: #4091b5; --color-cyan-500: #4aa5c7; --color-cyan-600: #56bdde; - --color-cyan-700: #5fceee; - --color-cyan-800: #7be2f8; - --color-cyan-900: #b2f1fb; - --color-cyan-950: #e5fbfe; + --color-cyan-700: #5eceed; + --color-cyan-800: #7ae2f8; + --color-cyan-900: #b2f0fb; + --color-cyan-950: #e4fbfd; --color-sky-50: #1c446a; --color-sky-100: #275d8f; --color-sky-200: #2e6ba5; - --color-sky-300: #357bbf; - --color-sky-400: #3e8dd4; - --color-sky-500: #48a1e5; + --color-sky-300: #357abf; + --color-sky-400: #3e8cd4; + --color-sky-500: #48a0e5; --color-sky-600: #55b9f9; - --color-sky-700: #7ac9fa; - --color-sky-800: #a5dbfb; - --color-sky-900: #d0eafc; + --color-sky-700: #79c9fa; + --color-sky-800: #a5dafb; + --color-sky-900: #ceeafc; --color-sky-950: #f0f8fe; --color-zinc-50: #414149; --color-zinc-100: #595962; --color-zinc-200: #676771; - --color-zinc-300: #777780; - --color-zinc-400: #88888f; - --color-zinc-500: #9a9aa1; - --color-zinc-600: #b1b1b6; - --color-zinc-700: #c0c0c5; - --color-zinc-800: #d4d4d7; - --color-zinc-900: #e6e6e8; + --color-zinc-300: #76767f; + --color-zinc-400: #87878f; + --color-zinc-500: #9999a0; + --color-zinc-600: #b0b0b6; + --color-zinc-700: #bfbfc4; + --color-zinc-800: #d3d3d7; + --color-zinc-900: #e5e5e8; --color-zinc-950: #f6f6f7; --color-violet-50: #5118ac; --color-violet-100: #6e20e9; @@ -682,51 +682,51 @@ --color-violet-400: #9570f6; --color-violet-500: #a389f7; --color-violet-600: #b7a5f9; - --color-violet-700: #c5b8f9; + --color-violet-700: #c5b7f9; --color-violet-800: #d6cefb; --color-violet-900: #e7e3fc; - --color-violet-950: #f7f6fe; + --color-violet-950: #f7f5fe; --color-purple-50: #5c199c; --color-purple-100: #7d18dd; - --color-purple-200: #8f2af1; + --color-purple-200: #8f29f1; --color-purple-300: #9e48f5; - --color-purple-400: #ac65f7; + --color-purple-400: #ac64f7; --color-purple-500: #b97ff7; --color-purple-600: #c99ef8; - --color-purple-700: #d3b3f9; + --color-purple-700: #d3b2f9; --color-purple-800: #e0cbfb; --color-purple-900: #ede1fd; --color-purple-950: #f9f5fe; --color-fuchsia-50: #6d1c79; --color-fuchsia-100: #951eab; --color-fuchsia-200: #ab22c6; - --color-fuchsia-300: #be34dd; - --color-fuchsia-400: #cd51e9; + --color-fuchsia-300: #be33dc; + --color-fuchsia-400: #cc51e9; --color-fuchsia-500: #da6df5; --color-fuchsia-600: #e392f8; --color-fuchsia-700: #e8aaf9; --color-fuchsia-800: #edc6fa; --color-fuchsia-900: #f4dffc; - --color-fuchsia-950: #fbf5fe; + --color-fuchsia-950: #fbf4fe; --color-pink-50: #7a1d42; - --color-pink-100: #a92259; + --color-pink-100: #a82158; --color-pink-200: #c12869; - --color-pink-300: #d73681; + --color-pink-300: #d7347f; --color-pink-400: #e4509e; --color-pink-500: #e971b5; - --color-pink-600: #ee95c8; + --color-pink-600: #ed95c8; --color-pink-700: #f0acd4; --color-pink-800: #f3c7e2; - --color-pink-900: #f7e0ee; - --color-pink-950: #fcf5f9; + --color-pink-900: #f7dfee; + --color-pink-950: #fcf4f9; --color-rose-50: #80172e; --color-rose-100: #ac2239; - --color-rose-200: #c6293f; - --color-rose-300: #df344c; - --color-rose-400: #eb526b; - --color-rose-500: #ee7586; + --color-rose-200: #c5293e; + --color-rose-300: #df334c; + --color-rose-400: #eb516a; + --color-rose-500: #ee7486; --color-rose-600: #f098a3; - --color-rose-700: #f2afb6; + --color-rose-700: #f2aeb6; --color-rose-800: #f6c8cd; --color-rose-900: #f9e0e3; --color-rose-950: #fdf5f5; @@ -741,37 +741,37 @@ --color-neutral-800: #d4d4d4; --color-neutral-900: #e6e6e6; --color-neutral-950: #f6f6f6; - --color-stone-50: #45423d; - --color-stone-100: #5e5954; - --color-stone-200: #6d6761; - --color-stone-300: #7c7771; + --color-stone-50: #44413c; + --color-stone-100: #5d5953; + --color-stone-200: #6c6761; + --color-stone-300: #7c7671; --color-stone-400: #8d8782; - --color-stone-500: #9e9a95; - --color-stone-600: #b4b0ac; - --color-stone-700: #c3c0bd; + --color-stone-500: #9e9994; + --color-stone-600: #b3b0ac; + --color-stone-700: #c2c0bc; --color-stone-800: #d5d3d1; - --color-stone-900: #e7e6e5; - --color-stone-950: #f7f7f6; + --color-stone-900: #e6e6e5; + --color-stone-950: #f7f6f5; --color-indigo-50: #372da6; - --color-indigo-100: #4c41d9; - --color-indigo-200: #5a54e1; + --color-indigo-100: #4c40d8; + --color-indigo-200: #5953e1; --color-indigo-300: #6767ea; - --color-indigo-400: #777cf3; - --color-indigo-500: #8a91f8; - --color-indigo-600: #a4abf9; - --color-indigo-700: #b6bcfa; + --color-indigo-400: #767cf3; + --color-indigo-500: #8991f8; + --color-indigo-600: #a3abf9; + --color-indigo-700: #b5bcfa; --color-indigo-800: #cbd2fb; --color-indigo-900: #e0e5fc; - --color-indigo-950: #f5f7fe; - --color-base-50: #273043; - --color-base-100: #333d51; - --color-base-200: #3e495e; - --color-base-300: #475369; - --color-base-400: #5d6a82; - --color-base-500: #919db0; - --color-base-600: #a5b0c1; - --color-base-700: #bec9d5; - --color-base-800: #f5f7fa; + --color-indigo-950: #f5f6fe; + --color-base-50: #26313c; + --color-base-100: #313e49; + --color-base-200: #374450; + --color-base-300: #465461; + --color-base-400: #5e6b78; + --color-base-500: #929dab; + --color-base-600: #a5b0bd; + --color-base-700: #bdc9d5; + --color-base-800: #f4f7fc; --color-base-900: #ffffff; --color-base-950: #ffffff; } From ceb384979b153f176cbcbb2c07d2ab19b47defa6 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 16:32:37 -0800 Subject: [PATCH 02/23] =?UTF-8?q?Move=20the=20blue=20palette=20to=20Craft?= =?UTF-8?q?=205=E2=80=99s=20221=C2=B0=20hue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The accent blue (Save, selected states, lightswitches, links) matches Craft 5’s link blue. Leonardo keeps each stop’s contrast. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- .../radio-group/radio-group.browser.test.ts | 2 +- .../src/styles/color-definitions.js | 14 ++-- .../src/styles/shared/color-palette.css | 66 +++++++++---------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/packages/craftcms-ui/src/components/radio-group/radio-group.browser.test.ts b/packages/craftcms-ui/src/components/radio-group/radio-group.browser.test.ts index 3410a4ef1ec..95a8d8bb390 100644 --- a/packages/craftcms-ui/src/components/radio-group/radio-group.browser.test.ts +++ b/packages/craftcms-ui/src/components/radio-group/radio-group.browser.test.ts @@ -423,7 +423,7 @@ it('marks the selected card with a thicker border and a bold label', async () => }); it('draws the selected illustration in blue, following the selection and the theme', async () => { - const BLUE_500 = 'rgb(62, 117, 242)'; + const BLUE_500 = 'rgb(60, 117, 241)'; const SLATE_500 = 'rgb(114, 125, 139)'; const SLATE_500_DARK = 'rgb(144, 155, 169)'; const {radios, inputs} = await renderCards('40rem'); diff --git a/packages/craftcms-ui/src/styles/color-definitions.js b/packages/craftcms-ui/src/styles/color-definitions.js index 5d6fff6fa6d..7cc237657d9 100644 --- a/packages/craftcms-ui/src/styles/color-definitions.js +++ b/packages/craftcms-ui/src/styles/color-definitions.js @@ -16,13 +16,13 @@ const colorKeys = { amber: ['#fefaec', '#401b06', '#fae692', '#8b4217', '#f4bb40', '#f09e38'], emerald: ['#effcf5', '#b5f1d0', '#5fd096', '#439669', '#1e4d3c', '#347857'], blue: [ - '#f0f6fe', - '#437df6', - '#223bb1', - '#2245dd', - '#c3dafb', - '#65a0f7', - '#192352', + '#f0f4fe', + '#437cf6', + '#224fb1', + '#225ddd', + '#c3d5fb', + '#6593f7', + '#192b52', ], yellow: [ '#fefce9', diff --git a/packages/craftcms-ui/src/styles/shared/color-palette.css b/packages/craftcms-ui/src/styles/shared/color-palette.css index 7f4d2b8233d..7a9fbec313d 100644 --- a/packages/craftcms-ui/src/styles/shared/color-palette.css +++ b/packages/craftcms-ui/src/styles/shared/color-palette.css @@ -58,17 +58,17 @@ --color-emerald-800: #1e4d3c; --color-emerald-900: #173c2f; --color-emerald-950: #112b22; - --color-blue-50: #e4effd; - --color-blue-100: #c1d9fb; - --color-blue-200: #a5c8fa; - --color-blue-300: #69a3f7; - --color-blue-400: #528cf6; - --color-blue-500: #3e75f2; - --color-blue-600: #305de7; - --color-blue-700: #2244d9; - --color-blue-800: #2139a7; - --color-blue-900: #1d2d7c; - --color-blue-950: #192351; + --color-blue-50: #e7eefd; + --color-blue-100: #c6d7fb; + --color-blue-200: #afc7fa; + --color-blue-300: #78a0f8; + --color-blue-400: #5a8bf7; + --color-blue-500: #3c75f1; + --color-blue-600: #2660e0; + --color-blue-700: #2251b6; + --color-blue-800: #1f418d; + --color-blue-900: #1b3368; + --color-blue-950: #162547; --color-yellow-50: #fcef90; --color-yellow-100: #f7d55c; --color-yellow-200: #efc041; @@ -313,17 +313,17 @@ --color-static-emerald-800: #1c4838; --color-static-emerald-900: #15372b; --color-static-emerald-950: #0f261e; - --color-static-blue-50: #c0d8fb; - --color-static-blue-100: #a0c4f9; - --color-static-blue-200: #7eb0f8; - --color-static-blue-300: #609bf7; - --color-static-blue-400: #4a84f6; - --color-static-blue-500: #396cee; - --color-static-blue-600: #2c55e4; - --color-static-blue-700: #2241cb; - --color-static-blue-800: #20369b; - --color-static-blue-900: #1c2a70; - --color-static-blue-950: #161e47; + --color-static-blue-50: #c4d6fb; + --color-static-blue-100: #a8c2fa; + --color-static-blue-200: #8aadf9; + --color-static-blue-300: #6e99f7; + --color-static-blue-400: #4e83f6; + --color-static-blue-500: #346eea; + --color-static-blue-600: #225bd7; + --color-static-blue-700: #214caa; + --color-static-blue-800: #1e3e83; + --color-static-blue-900: #1a2f5e; + --color-static-blue-950: #132140; --color-static-yellow-50: #f7d358; --color-static-yellow-100: #ebbb3f; --color-static-yellow-200: #daa538; @@ -576,17 +576,17 @@ --color-emerald-800: #94e4ba; --color-emerald-900: #bdf2d5; --color-emerald-950: #e8fbf1; - --color-blue-50: #20369e; - --color-blue-100: #2347de; - --color-blue-200: #2f5be7; - --color-blue-300: #3b6ff0; - --color-blue-400: #4a84f6; - --color-blue-500: #609af7; - --color-blue-600: #83b2f8; - --color-blue-700: #9dc2f9; - --color-blue-800: #bcd5fb; - --color-blue-900: #d9e7fc; - --color-blue-950: #f2f7fe; + --color-blue-50: #1e3e83; + --color-blue-100: #2253bd; + --color-blue-200: #245fdf; + --color-blue-300: #3670ec; + --color-blue-400: #4e83f6; + --color-blue-500: #6c98f7; + --color-blue-600: #8eb0f9; + --color-blue-700: #a6c0fa; + --color-blue-800: #c1d4fb; + --color-blue-900: #dce6fd; + --color-blue-950: #f3f6fe; --color-yellow-50: #5e3911; --color-yellow-100: #804e17; --color-yellow-200: #935b1c; From d7eca67bca313dd33d86734fedc9731c9d111920 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 16:32:38 -0800 Subject: [PATCH 03/23] Give the page and details pane their own surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds --c-surface-page (Craft 5’s gray-100) and --c-surface-details (a lighter gray-050), and a --cp-details-bg variable the details pane, its overlay panels, and the Activity and Ask AI composer fades share. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- .../craftcms-ui/src/stories/tokens/surface.stories.ts | 2 ++ packages/craftcms-ui/src/styles/shared/base.css | 5 +++-- packages/craftcms-ui/src/styles/shared/tokens.css | 8 ++++++++ resources/js/common/components/CpSidebar.vue | 2 +- resources/js/common/components/ScreenDetailsLayout.vue | 3 ++- resources/js/common/layouts/screens/PageScreen.vue | 5 +++-- .../js/modules/activity/components/ActivityTimeline.vue | 8 ++++++-- resources/js/modules/ai/assistant/AssistantChat.vue | 8 ++++++-- 8 files changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/craftcms-ui/src/stories/tokens/surface.stories.ts b/packages/craftcms-ui/src/stories/tokens/surface.stories.ts index 3eec093d16c..6eec00c963a 100644 --- a/packages/craftcms-ui/src/stories/tokens/surface.stories.ts +++ b/packages/craftcms-ui/src/stories/tokens/surface.stories.ts @@ -16,6 +16,8 @@ const surfaces = [ {name: '--c-surface-sunken', var: 'var(--c-surface-sunken)'}, {name: '--c-surface-overlay', var: 'var(--c-surface-overlay)'}, {name: '--c-surface-form', var: 'var(--c-surface-form)'}, + {name: '--c-surface-page', var: 'var(--c-surface-page)'}, + {name: '--c-surface-details', var: 'var(--c-surface-details)'}, ]; export const Surfaces: Story = { diff --git a/packages/craftcms-ui/src/styles/shared/base.css b/packages/craftcms-ui/src/styles/shared/base.css index c3852e6627f..0f5ca382a2c 100644 --- a/packages/craftcms-ui/src/styles/shared/base.css +++ b/packages/craftcms-ui/src/styles/shared/base.css @@ -1,6 +1,7 @@ html, -body { - background-color: var(--c-surface-sunken); +body, +:root[data-theme] { + background-color: var(--c-surface-page); } body { diff --git a/packages/craftcms-ui/src/styles/shared/tokens.css b/packages/craftcms-ui/src/styles/shared/tokens.css index 6c8db217927..62beffdf06b 100644 --- a/packages/craftcms-ui/src/styles/shared/tokens.css +++ b/packages/craftcms-ui/src/styles/shared/tokens.css @@ -38,6 +38,12 @@ --c-surface-sunken: var(--color-base-100); --c-surface-form: var(--color-base-100); --c-surface-overlay: var(--color-base-50); + --c-surface-page: var(--color-base-200); + --c-surface-details: color-mix( + in oklab, + var(--color-base-100), + var(--color-base-50) 40% + ); --c-surface-shade: rgba(0 0 0 / 25%); --c-table-border-color: color-mix( var(--c-color-neutral-border-quiet) 60%, @@ -393,6 +399,8 @@ var(--color-black) 25% ); --c-surface-overlay: var(--c-surface-default); + --c-surface-page: var(--c-surface-sunken); + --c-surface-details: var(--c-surface-default); /* Shadows read fainter against dark surfaces. */ --c-shadow-multiplier: 1.5; diff --git a/resources/js/common/components/CpSidebar.vue b/resources/js/common/components/CpSidebar.vue index 538bb4c76db..774f5a272f9 100644 --- a/resources/js/common/components/CpSidebar.vue +++ b/resources/js/common/components/CpSidebar.vue @@ -150,7 +150,7 @@ } .cp-sidebar[data-mode='docked'] { - --badge-border-color: var(--c-surface-sunken); + --badge-border-color: var(--c-surface-page); height: calc( 100dvh - var(--cp-top-bar-visible-height, 0px) - var( --cp-debug-bar-height, diff --git a/resources/js/common/components/ScreenDetailsLayout.vue b/resources/js/common/components/ScreenDetailsLayout.vue index df921e9d961..4e2de6bfe9a 100644 --- a/resources/js/common/components/ScreenDetailsLayout.vue +++ b/resources/js/common/components/ScreenDetailsLayout.vue @@ -106,6 +106,7 @@ } .screen-details-layout__details { + --cp-details-bg: var(--c-surface-overlay); display: grid; gap: var(--c-spacing-md, 1rem); border-inline-start: 1px solid var(--c-color-border-quiet); @@ -139,7 +140,7 @@ off rather than against the content. */ inline-size: clamp(calc(300rem / 16), 80cqi, calc(400rem / 16)); max-inline-size: 80cqi; - background-color: var(--c-surface-overlay); + background-color: var(--cp-details-bg); border-inline-start: 1px solid var(--c-color-border-quiet); box-shadow: var(--c-shadow-overlay); } diff --git a/resources/js/common/layouts/screens/PageScreen.vue b/resources/js/common/layouts/screens/PageScreen.vue index 68436cd4ac1..7e3f0151dbe 100644 --- a/resources/js/common/layouts/screens/PageScreen.vue +++ b/resources/js/common/layouts/screens/PageScreen.vue @@ -566,7 +566,7 @@ Shell padding-inline-end: var(--cp-scrollbar-width, 0px); /* The floating sidebar's row stays empty, so the main row takes the room. */ grid-template-rows: auto 1fr; - background-color: var(--c-surface-sunken); + background-color: var(--c-surface-page); @media (width >= var(--breakpoint-lg)) { grid-template-rows: none; @@ -784,7 +784,8 @@ Content: the secondary nav, content, and details panes grid-area: details; z-index: var(--c-layer-sticky); border-inline-start: 1px solid var(--c-color-border-quiet); - background-color: var(--c-surface-default); + --cp-details-bg: var(--c-surface-details); + background-color: var(--cp-details-bg); /* A dragged width outlives the layout it was dragged in, so cap it here. */ max-inline-size: var(--cp-content-details-max, 90cqi); /* Contained in both axes, so the row sets its height rather than the pane: diff --git a/resources/js/modules/activity/components/ActivityTimeline.vue b/resources/js/modules/activity/components/ActivityTimeline.vue index 4e04de9ba54..19f5fee749a 100644 --- a/resources/js/modules/activity/components/ActivityTimeline.vue +++ b/resources/js/modules/activity/components/ActivityTimeline.vue @@ -177,8 +177,12 @@ padding-inline: var(--cp-container-padding); background: linear-gradient( to bottom, - color-mix(in srgb, var(--c-surface-default) 0%, transparent), - var(--c-surface-default) var(--c-spacing-lg) + color-mix( + in srgb, + var(--cp-details-bg, var(--c-surface-default)) 0%, + transparent + ), + var(--cp-details-bg, var(--c-surface-default)) var(--c-spacing-lg) ); position: relative; z-index: 1; diff --git a/resources/js/modules/ai/assistant/AssistantChat.vue b/resources/js/modules/ai/assistant/AssistantChat.vue index 62cf4051194..36682f9ac8c 100644 --- a/resources/js/modules/ai/assistant/AssistantChat.vue +++ b/resources/js/modules/ai/assistant/AssistantChat.vue @@ -1259,8 +1259,12 @@ padding: var(--c-spacing-lg); background: linear-gradient( to bottom, - color-mix(in srgb, var(--c-surface-default) 0%, transparent), - var(--c-surface-default) var(--c-spacing-lg) + color-mix( + in srgb, + var(--cp-details-bg, var(--c-surface-default)) 0%, + transparent + ), + var(--cp-details-bg, var(--c-surface-default)) var(--c-spacing-lg) ); } From ba0b33e1b0f62efe6c91d3605bd53bc0ee14fe3a Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 16:32:41 -0800 Subject: [PATCH 04/23] Give the details pane a subtle inset edge shadow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drawn by an overlay on the pane, so the composers’ fades can’t cover it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- resources/js/common/layouts/screens/PageScreen.vue | 14 ++++++++++++++ .../layouts/screens/page/ContentDetailsAside.vue | 10 ++++++++++ 2 files changed, 24 insertions(+) diff --git a/resources/js/common/layouts/screens/PageScreen.vue b/resources/js/common/layouts/screens/PageScreen.vue index 7e3f0151dbe..02e0f322715 100644 --- a/resources/js/common/layouts/screens/PageScreen.vue +++ b/resources/js/common/layouts/screens/PageScreen.vue @@ -716,6 +716,7 @@ Content: the secondary nav, content, and details panes --resize-handle-display: none; .cp-content__details { + --cp-details-edge-shadow: none; border-inline-start: none; box-shadow: none; } @@ -785,7 +786,18 @@ Content: the secondary nav, content, and details panes z-index: var(--c-layer-sticky); border-inline-start: 1px solid var(--c-color-border-quiet); --cp-details-bg: var(--c-surface-details); + --_details-inset-shadow: rgb( + var(--c-shadow-tint) / calc(0.08 * var(--c-shadow-multiplier)) + ); + /* Drawn by the pane inside (see ContentDetailsAside), above the composers + whose opaque fades would otherwise cover it. */ + --cp-details-edge-shadow: inset 6px 0 6px -6px var(--_details-inset-shadow); background-color: var(--cp-details-bg); + + &:dir(rtl) { + --cp-details-edge-shadow: inset -6px 0 6px -6px + var(--_details-inset-shadow); + } /* A dragged width outlives the layout it was dragged in, so cap it here. */ max-inline-size: var(--cp-content-details-max, 90cqi); /* Contained in both axes, so the row sets its height rather than the pane: @@ -860,6 +872,7 @@ Folds .cp-content--details > .cp-content__details { inline-size: var(--cp-content-details-width); justify-self: end; + --cp-details-edge-shadow: none; box-shadow: var(--c-shadow-md); } } @@ -904,6 +917,7 @@ Folds .cp-content--sidebar.cp-content--details > .cp-content__details { inline-size: var(--cp-content-details-width); justify-self: end; + --cp-details-edge-shadow: none; box-shadow: var(--c-shadow-md); } } diff --git a/resources/js/common/layouts/screens/page/ContentDetailsAside.vue b/resources/js/common/layouts/screens/page/ContentDetailsAside.vue index b128dd0ffef..980bb496141 100644 --- a/resources/js/common/layouts/screens/page/ContentDetailsAside.vue +++ b/resources/js/common/layouts/screens/page/ContentDetailsAside.vue @@ -81,4 +81,14 @@ var(--cp-details-end-gap, 0px) ); } + + /* Over the panels, so a composer's opaque fade can't cover the edge. */ + .cp-content__details-pane::after { + content: ''; + position: absolute; + inset: 0; + z-index: 2; + pointer-events: none; + box-shadow: var(--cp-details-edge-shadow, none); + } From 458400fa04f4ff8d3f72062f000465a92d080775 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 16:32:42 -0800 Subject: [PATCH 05/23] Match dashboard widgets to the details surface Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- .../modules/dashboard/craft-support/Index.vue | 26 ++++++++++++++----- resources/js/pages/Dashboard.vue | 7 +++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/resources/js/modules/dashboard/craft-support/Index.vue b/resources/js/modules/dashboard/craft-support/Index.vue index 2ce8ccd89dc..942803e4449 100644 --- a/resources/js/modules/dashboard/craft-support/Index.vue +++ b/resources/js/modules/dashboard/craft-support/Index.vue @@ -116,12 +116,26 @@ border-radius: 0; white-space: normal; } - /* At rest, tiles match the other widgets' bodies (a gray card's fill). */ - .support-tile:not(:hover, :active) { - background-color: color-mix( - var(--c-color-gray-fill-quiet), - transparent 70% - ); + /* At rest, tiles match the other widgets (see Dashboard.vue), and darken + from there, as nav items do. */ + .support-tile { + background-color: var(--c-surface-details); + + &:hover { + background-color: color-mix( + in srgb, + var(--c-surface-details), + currentColor 5% + ); + } + + &:active { + background-color: color-mix( + in srgb, + var(--c-surface-details), + currentColor 10% + ); + } } .support-tile:first-child { border-start-start-radius: var(--c-card-radius, var(--c-radius-md)); diff --git a/resources/js/pages/Dashboard.vue b/resources/js/pages/Dashboard.vue index b57272344f0..18f0e6276ca 100644 --- a/resources/js/pages/Dashboard.vue +++ b/resources/js/pages/Dashboard.vue @@ -119,4 +119,11 @@ .item { margin-block-end: 14px; } + + /* Widgets take the details pane's surface, the page's other secondary + surface. */ + .dashboard-grid :deep(craft-card[data-color='gray']::part(base)), + .dashboard-grid :deep(craft-card[data-color='gray']::part(header)) { + background-color: var(--c-surface-details); + } From adee8d87031a61533cd9d61d8a480e311693d70e Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 16:32:44 -0800 Subject: [PATCH 06/23] =?UTF-8?q?Make=20Ask=20AI=E2=80=99s=20input=20trans?= =?UTF-8?q?parent,=20like=20the=20comment=20box?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- resources/js/modules/ai/assistant/AssistantChat.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/modules/ai/assistant/AssistantChat.vue b/resources/js/modules/ai/assistant/AssistantChat.vue index 36682f9ac8c..2b25ff7140d 100644 --- a/resources/js/modules/ai/assistant/AssistantChat.vue +++ b/resources/js/modules/ai/assistant/AssistantChat.vue @@ -1278,8 +1278,8 @@ border: var(--c-form-control-border-width) solid var(--c-form-control-border-color); border-radius: var(--c-radius-lg); - background-color: var(--c-surface-white); - box-shadow: var(--c-shadow-md); + background-color: var(--c-input-fill, var(--c-form-control-fill)); + box-shadow: var(--c-input-shadow, 0 0 transparent); } .assistant-chat__card { From 10478ccb730fabb9ca4679f1b425a8345ac57619 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 16:32:46 -0800 Subject: [PATCH 07/23] Lighten breadcrumb separators Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- .../src/components/breadcrumb-item/breadcrumb-item.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/craftcms-ui/src/components/breadcrumb-item/breadcrumb-item.ts b/packages/craftcms-ui/src/components/breadcrumb-item/breadcrumb-item.ts index f828651c2e8..074a2dead22 100644 --- a/packages/craftcms-ui/src/components/breadcrumb-item/breadcrumb-item.ts +++ b/packages/craftcms-ui/src/components/breadcrumb-item/breadcrumb-item.ts @@ -70,7 +70,9 @@ export default class CraftBreadcrumbItem extends LitElement { /* With the crumb's gap, its margin puts as much space before it as the list's gap puts after it (see craft-breadcrumbs), centering it. */ slot[name='separator']::slotted(*) { - color: var(--c-text-quiet); + color: var(--color-slate-300); + font-size: var(--c-text-base); + font-weight: var(--font-weight-light); margin-block: 0; margin-inline: var(--c-spacing-sm) 0; } From 97bb549c0a4e162a4677f5cfdd69c84a416eb268 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 16:32:47 -0800 Subject: [PATCH 08/23] =?UTF-8?q?Even=20out=20folder=20breadcrumbs?= =?UTF-8?q?=E2=80=99=20separator=20spacing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit craft-breadcrumbs now spaces its separators evenly, so the end margin these added doubled the gap after each one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- .../js/modules/element-selector-modal/ElementPicker.vue | 7 ------- resources/js/pages/assets/Index.vue | 9 --------- 2 files changed, 16 deletions(-) diff --git a/resources/js/modules/element-selector-modal/ElementPicker.vue b/resources/js/modules/element-selector-modal/ElementPicker.vue index 188b14d05e6..65a69c5bf62 100644 --- a/resources/js/modules/element-selector-modal/ElementPicker.vue +++ b/resources/js/modules/element-selector-modal/ElementPicker.vue @@ -216,13 +216,6 @@ overflow: hidden; } - // Even on both sides of each separator, matching the header's breadcrumbs: - // the crumb's own gap already sits before it, so the end margin makes up for - // it. - .folder-breadcrumbs :deep(craft-breadcrumb-item > [slot='separator']) { - margin-inline: var(--c-spacing-sm) var(--c-spacing-md); - } - // A folder listing has no columns besides the folder's own, so it takes the // width the title column would otherwise leave to the checkbox column. .modal-element-index--folders :deep(.cp-table-cell--title) { diff --git a/resources/js/pages/assets/Index.vue b/resources/js/pages/assets/Index.vue index 0807f694fad..5e868e278fd 100644 --- a/resources/js/pages/assets/Index.vue +++ b/resources/js/pages/assets/Index.vue @@ -326,12 +326,3 @@ - - From 38986620cd519bb399970be565e749d60ab7be8c Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 16:32:49 -0800 Subject: [PATCH 09/23] Keep nav icons in place as the sidebar collapses Expanded rows with an icon now pad by the border less, so their icons sit where collapsed rows center them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- .../src/components/nav-item/nav-item.styles.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/craftcms-ui/src/components/nav-item/nav-item.styles.ts b/packages/craftcms-ui/src/components/nav-item/nav-item.styles.ts index 250192aee17..fa1c0cd4579 100644 --- a/packages/craftcms-ui/src/components/nav-item/nav-item.styles.ts +++ b/packages/craftcms-ui/src/components/nav-item/nav-item.styles.ts @@ -20,6 +20,9 @@ export default css` transparent border to keep room for its focus state. */ --_rail-border: 1px; --_rail-size: var(--c-size-touch-target); + /* A row with an icon, less its border, so the icon sits where a collapsed + row centers it. */ + --_prefixed-padding-inline: calc(var(--c-spacing-sm) - var(--_rail-border)); border-radius: var(--c-radius-md); } @@ -105,7 +108,7 @@ export default css` } .nav-item--prefixed { - padding-inline: var(--c-spacing-sm); + padding-inline: var(--_prefixed-padding-inline); grid-template-columns: var(--_prefix-size) 1fr auto; /* Pull the label back across the row's gap, to the tighter prefix gap. */ @@ -245,8 +248,9 @@ export default css` */ :host(:not([group])) .subnav { --_subnav-indent: calc( - var(--c-spacing-sm) + var(--_prefix-size) + var(--_prefix-gap) - - var(--_subnav-rule) - var(--c-spacing-sm) - var(--_padding-inline) + var(--_prefixed-padding-inline) + var(--_prefix-size) + + var(--_prefix-gap) - var(--_subnav-rule) - var(--c-spacing-sm) - + var(--_padding-inline) ); position: relative; margin-block-start: var(--c-spacing-sm); From 23decd9f010da7d9308ebd600da6642b391b93b3 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 16:32:50 -0800 Subject: [PATCH 10/23] Keep a nav item hovered while its flyout is open Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- .../src/components/nav-item/nav-item.styles.ts | 9 ++++++--- packages/craftcms-ui/src/components/nav-item/nav-item.ts | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/craftcms-ui/src/components/nav-item/nav-item.styles.ts b/packages/craftcms-ui/src/components/nav-item/nav-item.styles.ts index fa1c0cd4579..21809e8005a 100644 --- a/packages/craftcms-ui/src/components/nav-item/nav-item.styles.ts +++ b/packages/craftcms-ui/src/components/nav-item/nav-item.styles.ts @@ -193,9 +193,11 @@ export default css` --c-color-neutral-on-normal: var(--c-color-accent-on-normal); } - /* Not on the page you're on: its selected fill is the state to show. */ + /* Not on the page you're on: its selected fill is the state to show. A row + with its flyout open looks hovered while the pointer is in the flyout. */ :host(:not([current])) - .nav-item:not(.nav-item--static):hover:not(:has(craft-button:hover)) { + .nav-item:not(.nav-item--static):hover:not(:has(craft-button:hover)), + :host(:not([current])) .nav-item--flyout-open { background-color: color-mix(in srgb, currentColor, transparent 95%); } @@ -216,7 +218,8 @@ export default css` /* A selected or hovered row's icon takes the row's own color. */ :host([active]) .nav-item__prefix, - .nav-item:not(.nav-item--static):hover .nav-item__prefix { + .nav-item:not(.nav-item--static):hover .nav-item__prefix, + .nav-item--flyout-open .nav-item__prefix { color: inherit; } diff --git a/packages/craftcms-ui/src/components/nav-item/nav-item.ts b/packages/craftcms-ui/src/components/nav-item/nav-item.ts index 93436be77a4..12f6fa58d06 100644 --- a/packages/craftcms-ui/src/components/nav-item/nav-item.ts +++ b/packages/craftcms-ui/src/components/nav-item/nav-item.ts @@ -501,6 +501,7 @@ export default class CraftNavItem extends LitElement { 'nav-item': true, 'nav-item--icon': true, 'nav-item--static': this.isStatic(useFlyout), + 'nav-item--flyout-open': useFlyout && this.flyoutOpen, })}" id="${this.itemId}" type="${this.buttonType(useFlyout)}" @@ -735,6 +736,7 @@ export default class CraftNavItem extends LitElement { 'nav-item--prefixed': hasPrefix, 'nav-item--flush': this.flush, 'nav-item--static': this.isStatic(useFlyout), + 'nav-item--flyout-open': useFlyout && this.flyoutOpen, })}" id="${this.itemId}" > From 6da0e2d3d9908928bf09bbf46958a299e9bd4673 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 16:32:51 -0800 Subject: [PATCH 11/23] Keep menu buttons hovered while their menus are open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applies to buttons slotted as an action menu’s or popover’s invoker. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- .../src/components/button/button.styles.ts | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/craftcms-ui/src/components/button/button.styles.ts b/packages/craftcms-ui/src/components/button/button.styles.ts index c9cb3e516b1..8d9a4d1b4e7 100644 --- a/packages/craftcms-ui/src/components/button/button.styles.ts +++ b/packages/craftcms-ui/src/components/button/button.styles.ts @@ -371,7 +371,10 @@ export default css` :host([variant~='primary']:hover), :host([variant~='danger']:hover), - :host([variant~='solid']:hover) { + :host([variant~='solid']:hover), + :host([variant~='primary'][slot='invoker'][aria-expanded='true']), + :host([variant~='danger'][slot='invoker'][aria-expanded='true']), + :host([variant~='solid'][slot='invoker'][aria-expanded='true']) { background-color: hsl( from var(--c-color-fill-loud, var(--c-color-neutral-fill-loud)) h s calc(l - 5) @@ -402,7 +405,8 @@ export default css` color: var(--c-color-on-normal, var(--c-color-neutral-on-normal)); } - :host([variant~='fill']:hover) { + :host([variant~='fill']:hover), + :host([variant~='fill'][slot='invoker'][aria-expanded='true']) { background-color: hsl( from var(--c-color-fill-quiet, var(--c-color-neutral-fill-quiet)) h s calc(l - 5) @@ -430,7 +434,16 @@ export default css` :host([variant~='outline']:not(:disabled):not(.loading):hover), :host([variant~='dashed']:not(:disabled):not(.loading):hover), :host([variant~='danger-plain']:not(:disabled):not(.loading):hover), - :host([variant~='plain']:not(:disabled):not(.loading):hover) { + :host([variant~='plain']:not(:disabled):not(.loading):hover), + /* A menu or popover's button keeps its hover state while it's open. */ + :host( + :is( + [variant~='outline'], + [variant~='dashed'], + [variant~='danger-plain'], + [variant~='plain'] + )[slot='invoker'][aria-expanded='true'] + ) { background-color: color-mix( in oklab, var(--c-color-fill-quiet, var(--c-color-neutral-fill-quiet)), @@ -541,7 +554,8 @@ export default css` } :host([variant~='plain'][inherit]:not(:disabled):not(.loading):active), - :host([variant~='plain'][inherit].is-active:not(:disabled):not(.loading)) { + :host([variant~='plain'][inherit].is-active:not(:disabled):not(.loading)), + :host([variant~='plain'][inherit][slot='invoker'][aria-expanded='true']) { color: inherit; } From 22fb5ec6e95db16b897b5c59052a66c340834e63 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 16:32:53 -0800 Subject: [PATCH 12/23] Blend the global header into the page The header drops its dark theme, fill, shadow, and borders (keeping a bottom border on mobile), takes in the space above the body panel, lines the system info up with the nav items (truncating long names, and keeping the name when collapsed), and centers the avatar on the details rail with the last icon ending where the details pane does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- .../js/common/components/CpHeaderBar.vue | 110 +++++++++++++----- resources/js/common/components/CpSidebar.vue | 20 +--- resources/js/common/components/SystemInfo.vue | 5 +- .../composables/useCpBreakpoints.test.ts | 3 - .../js/common/layouts/screens/PageScreen.vue | 5 +- .../components/cp-notification-center.ts | 7 ++ 6 files changed, 98 insertions(+), 52 deletions(-) diff --git a/resources/js/common/components/CpHeaderBar.vue b/resources/js/common/components/CpHeaderBar.vue index 5100020bca7..b5e4d87ec8d 100644 --- a/resources/js/common/components/CpHeaderBar.vue +++ b/resources/js/common/components/CpHeaderBar.vue @@ -38,7 +38,12 @@ : ['start', 'indicators', 'end', 'breadcrumbs'] ); - const {toggle: toggleSidebar, toggleButton} = useGlobalSidebar(); + const { + toggle: toggleSidebar, + toggleButton, + collapsed: sidebarCollapsed, + width: sidebarWidth, + } = useGlobalSidebar(); // The composable returns focus here when the floating sidebar closes, and it // can't reach into a template of its own to find the button. @@ -81,7 +86,10 @@ diff --git a/resources/js/modules/notifications/components/cp-notification-center.ts b/resources/js/modules/notifications/components/cp-notification-center.ts index cdb1a4f744e..47a6fbaf88f 100644 --- a/resources/js/modules/notifications/components/cp-notification-center.ts +++ b/resources/js/modules/notifications/components/cp-notification-center.ts @@ -24,7 +24,7 @@ class CpNotificationCenter extends LitElement { } .notification-trigger { - --c-icon-size: 1em; + --c-icon-size: var(--cp-chrome-icon-size, 1em); position: relative; } diff --git a/resources/js/modules/ui/UiNodeList.vue b/resources/js/modules/ui/UiNodeList.vue index b2483411b53..fa11a65a9eb 100644 --- a/resources/js/modules/ui/UiNodeList.vue +++ b/resources/js/modules/ui/UiNodeList.vue @@ -293,6 +293,7 @@ v-if="tab.props.icon" :name="tab.props.icon" slot="prefix" + class="ui-node-list__tab-icon" > {{ tab.props.label }} From 8e14a3e3b9bc4660dd5b802f3ef4dc1f03e0f40d Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 16:39:06 -0800 Subject: [PATCH 15/23] Changelog for #20612 Also moves #20599's entry up with the other control panel changes. [ci skip] Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45302a9174e..4ae1ed6fc88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +- The control panel’s grays and accent blue now match Craft 5’s, and the page background, the details pane, and dashboard widgets each have their own surface. ([#20612](https://github.com/craftcms/cms/pull/20612)) +- The global header now blends into the page and lines up with the global sidebar and details rail. Long system names are truncated, and the system name stays visible when the sidebar is collapsed. ([#20612](https://github.com/craftcms/cms/pull/20612)) +- The main content’s side padding now grows with its width, from 16px up to 48px, and the content, the details pane, and secondary navigation get matching space at their tops as the page widens, keeping their headers level. Cards’ body padding grows with the card’s own width, from 16px up to 24px, the field layout’s tabs have a larger gap below them, and structure indexes’ toggles and drag handles keep their place beside the content. ([#20599](https://github.com/craftcms/cms/pull/20599)) +- Icons in the global sidebar, global header, breadcrumbs, details rail, and content tabs are now the same size and color, and rows of icon buttons in the global header and on element cards are tighter. ([#20612](https://github.com/craftcms/cms/pull/20612)) +- Global nav items now stay highlighted while their flyout menus are open, and menu buttons stay highlighted while their menus are open. ([#20612](https://github.com/craftcms/cms/pull/20612)) +- The details pane now has a subtle inset shadow along its leading edge, and Ask AI’s input is transparent, like the Activity panel’s comment box. ([#20612](https://github.com/craftcms/cms/pull/20612)) +- Breadcrumb separators are now lighter. ([#20612](https://github.com/craftcms/cms/pull/20612)) - Fixed a bug where fields could be saved with the same handle as another field. ([#20600](https://github.com/craftcms/cms/issues/20600), [#20607](https://github.com/craftcms/cms/pull/20607)) - Fixed an error that occurred when changing a password from the **My Account** → **Password** screen. ([#20585](https://github.com/craftcms/cms/pull/20585)) - Fixed an infinite recursion that occurred when clearing the current user’s identity from a console request via `craft\console\User::setIdentity()`. ([#20602](https://github.com/craftcms/cms/issues/20602), [#20606](https://github.com/craftcms/cms/pull/20606)) @@ -10,12 +17,13 @@ - Fixed a bug where named image transforms generated by the `craft` asset transformer were stored and indexed by their parameters rather than their handle. ([#20588](https://github.com/craftcms/cms/issues/20588), [#20594](https://github.com/craftcms/cms/pull/20594)) - Fixed an error that occurred when transforming an asset that its asset transformer can’t transform, such as a video, via GraphQL or the transform arguments of `asset.getWidth()`, `getHeight()`, `getFormat()`, and `getMimeType()`. The asset’s own URL, dimensions, format, and MIME type are returned instead. ([#20588](https://github.com/craftcms/cms/issues/20588), [#20594](https://github.com/craftcms/cms/pull/20594)) - Fixed a bug where `asset.getUrl()` and `asset.url` returned `null` for assets that their asset transformer can’t transform, when a transform was set, rather than the asset’s own URL. ([#20588](https://github.com/craftcms/cms/issues/20588), [#20594](https://github.com/craftcms/cms/pull/20594)) +- Fixed a bug where global nav icons shifted when the sidebar was expanded or collapsed. ([#20612](https://github.com/craftcms/cms/pull/20612)) +- Fixed a bug where folder breadcrumbs had extra space after each separator. ([#20612](https://github.com/craftcms/cms/pull/20612)) - GraphQL transform arguments now accept `immediately` again, which asks the asset transformer to generate the transform before returning its URL. ([#20588](https://github.com/craftcms/cms/issues/20588), [#20594](https://github.com/craftcms/cms/pull/20594)) - Asset transform drivers now receive whether the transform should be generated immediately in `$request->immediately`, including legacy image transformers. ([#20588](https://github.com/craftcms/cms/issues/20588), [#20594](https://github.com/craftcms/cms/pull/20594)) - Added `CraftCms\Cms\Asset\Data\AssetTransformRequest::$immediately` and `$namedTransform`. ([#20588](https://github.com/craftcms/cms/issues/20588), [#20594](https://github.com/craftcms/cms/pull/20594)) - Added `CraftCms\Cms\Asset\AssetTransformers::withImmediateTransforms()`, `immediateTransforms()`, and `IMMEDIATE_TRANSFORMS_CONTEXT`, which apply to every asset transform driver. ([#20588](https://github.com/craftcms/cms/issues/20588), [#20594](https://github.com/craftcms/cms/pull/20594)) - Deprecated `CraftCms\Cms\Image\CraftAssetTransformDriver::withImmediateTransforms()` and `IMMEDIATE_TRANSFORMS_CONTEXT`. `CraftCms\Cms\Asset\AssetTransformers::withImmediateTransforms()` and `IMMEDIATE_TRANSFORMS_CONTEXT` should be used instead. ([#20588](https://github.com/craftcms/cms/issues/20588), [#20594](https://github.com/craftcms/cms/pull/20594)) -- The main content’s side padding now grows with its width, from 16px up to 48px, and the content, the details pane, and secondary navigation get matching space at their tops as the page widens, keeping their headers level. Cards’ body padding grows with the card’s own width, from 16px up to 24px, the field layout’s tabs have a larger gap below them, and structure indexes’ toggles and drag handles keep their place beside the content. ([#20599](https://github.com/craftcms/cms/pull/20599)) ## 6.0.0-beta.1 - 2026-11-10 From 5d85d604063619734c415c823d20d1e682cc9915 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 16:46:59 -0800 Subject: [PATCH 16/23] Keep cover cards' action buttons spaced apart Over a cover image the buttons have a backing of their own, so they keep the gap that plain ones dropped. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- resources/js/common/components/SelectableCardList.vue | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/resources/js/common/components/SelectableCardList.vue b/resources/js/common/components/SelectableCardList.vue index 3741585a36f..28bbe12c6e3 100644 --- a/resources/js/common/components/SelectableCardList.vue +++ b/resources/js/common/components/SelectableCardList.vue @@ -299,3 +299,11 @@ + + From 6fdb6f4f47ba32d5141eb54b6f80f4b19d3037fe Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 17:30:20 -0800 Subject: [PATCH 17/23] Match the mobile header's avatar spacing to its icons' Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- resources/js/common/components/CpHeaderBar.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/common/components/CpHeaderBar.vue b/resources/js/common/components/CpHeaderBar.vue index fec7e9e67c5..d01642216f7 100644 --- a/resources/js/common/components/CpHeaderBar.vue +++ b/resources/js/common/components/CpHeaderBar.vue @@ -134,7 +134,7 @@
-
+
From 7cf4ec3e742cd5c049a35d302cd34ad67039f05c Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 19:44:11 -0800 Subject: [PATCH 18/23] Theme tooltip contents dark, like a black card's A tooltip's fill is always black, but what it holds took the page's theme, so in the light theme the key caps in the header's Search and Ask AI tooltips were pale on pale. The dark header used to cover for this. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- .../scripts/generate-color-palette.js | 5 ++-- .../craftcms-ui/scripts/generate-colors.js | 4 ++-- packages/craftcms-ui/scripts/utils.js | 5 ++-- .../tooltip/tooltip.browser.test.ts | 23 +++++++++++++++++++ .../src/styles/shared/color-palette.css | 8 ++++--- .../src/styles/shared/colorable.css | 10 ++++---- .../craftcms-ui/src/styles/shared/tokens.css | 8 ++++--- .../src/styles/shared/variables.css | 3 ++- 8 files changed, 49 insertions(+), 17 deletions(-) diff --git a/packages/craftcms-ui/scripts/generate-color-palette.js b/packages/craftcms-ui/scripts/generate-color-palette.js index 2dd5967c8ad..805fb70b526 100644 --- a/packages/craftcms-ui/scripts/generate-color-palette.js +++ b/packages/craftcms-ui/scripts/generate-color-palette.js @@ -55,8 +55,9 @@ function printStyles() { lines.push(staticColors); lines.push('}\n'); lines.push( - "/* What's in a black card, (filled) chip, or badge is themed dark, so it holds\n" + - ' up against the dark fill. (The element itself keeps its own colors.) */' + "/* What's in a black card, (filled) chip, badge, or tooltip is themed dark, so\n" + + ' it holds up against the dark fill. (The element itself keeps its own\n' + + ' colors.) */' ); lines.push(`[data-theme='dark'],\n${BLACK_CONTENTS_SELECTOR} {`); lines.push(darkColors); diff --git a/packages/craftcms-ui/scripts/generate-colors.js b/packages/craftcms-ui/scripts/generate-colors.js index 932acb5d4bc..89ef07d066f 100644 --- a/packages/craftcms-ui/scripts/generate-colors.js +++ b/packages/craftcms-ui/scripts/generate-colors.js @@ -197,8 +197,8 @@ function generateStyles(paletteColors, semanticColors) { /* Also on [data-theme]: these tokens are built from the palette with var(), which is substituted where it's declared, so a themed subtree re-resolves them. - Likewise what's in a black card, chip, or badge, and a neutral card, so it - doesn't take a tinted card's colors (see utils.js). + Likewise what's in a black card, chip, badge, or tooltip, and a neutral + card, so it doesn't take a tinted card's colors (see utils.js). */ :root, [data-theme], diff --git a/packages/craftcms-ui/scripts/utils.js b/packages/craftcms-ui/scripts/utils.js index 72488991594..c54110ba0b5 100644 --- a/packages/craftcms-ui/scripts/utils.js +++ b/packages/craftcms-ui/scripts/utils.js @@ -36,14 +36,15 @@ export async function resolveFrom(pattern) { } /** - * What's in a black card, badge, or (filled) chip, which is themed dark so it + * What's in a black card, badge, (filled) chip, or tooltip, which is themed dark so it * holds up against the dark fill. Zero specificity, so the contents' own colors * still win. Matches the copies in tokens.css and variables.css. */ export const BLACK_CONTENTS_SELECTOR = `:where( craft-card[data-color='black'] > *, craft-chip[data-color='black']:not([appearance~='plain']) > *, - craft-badge[fill='black'] > * + craft-badge[fill='black'] > *, + craft-tooltip > [slot='content'] > * )`; /** diff --git a/packages/craftcms-ui/src/components/tooltip/tooltip.browser.test.ts b/packages/craftcms-ui/src/components/tooltip/tooltip.browser.test.ts index b55b19b13c6..2382820020f 100644 --- a/packages/craftcms-ui/src/components/tooltip/tooltip.browser.test.ts +++ b/packages/craftcms-ui/src/components/tooltip/tooltip.browser.test.ts @@ -1,6 +1,9 @@ import {beforeEach, expect, it} from 'vite-plus/test'; import type CraftTooltip from './tooltip.js'; +import '../../styles/cp.css'; +import '../shortcut/shortcut.js'; import './tooltip.js'; +import {contrast} from '../../../test/contrast'; beforeEach(() => { document.body.innerHTML = ''; @@ -572,3 +575,23 @@ it('refers its invoker to content replaced while it was out of the document', as expect(invoker.getAttribute('aria-describedby')).toBe('replacement'); }); + +it('themes what it holds dark, so key caps read against its fill in the light theme', async () => { + document.documentElement.dataset.theme = 'light'; + document.body.innerHTML = ` + + +
Search K
+
`; + const tooltip = document.querySelector('craft-tooltip')!; + await tooltip.updateComplete; + const shortcut = tooltip.querySelector('craft-shortcut')!; + await shortcut.updateComplete; + + const key = getComputedStyle(shortcut.shadowRoot!.querySelector('kbd')!); + const fill = getComputedStyle(tooltip.querySelector('[slot="content"]')!); + + expect( + contrast(key.color, key.backgroundColor, fill.backgroundColor) + ).toBeGreaterThanOrEqual(4.5); +}); diff --git a/packages/craftcms-ui/src/styles/shared/color-palette.css b/packages/craftcms-ui/src/styles/shared/color-palette.css index 7a9fbec313d..28a66929f45 100644 --- a/packages/craftcms-ui/src/styles/shared/color-palette.css +++ b/packages/craftcms-ui/src/styles/shared/color-palette.css @@ -513,13 +513,15 @@ --color-static-base-950: #161c23; } -/* What's in a black card, (filled) chip, or badge is themed dark, so it holds - up against the dark fill. (The element itself keeps its own colors.) */ +/* What's in a black card, (filled) chip, badge, or tooltip is themed dark, so + it holds up against the dark fill. (The element itself keeps its own + colors.) */ [data-theme='dark'], :where( craft-card[data-color='black'] > *, craft-chip[data-color='black']:not([appearance~='plain']) > *, - craft-badge[fill='black'] > * + craft-badge[fill='black'] > *, + craft-tooltip > [slot='content'] > * ) { --color-gray-50: #3b4252; --color-gray-100: #535a69; diff --git a/packages/craftcms-ui/src/styles/shared/colorable.css b/packages/craftcms-ui/src/styles/shared/colorable.css index 7eef1e3a439..694d8b84b69 100644 --- a/packages/craftcms-ui/src/styles/shared/colorable.css +++ b/packages/craftcms-ui/src/styles/shared/colorable.css @@ -3,15 +3,16 @@ /* Also on [data-theme]: these tokens are built from the palette with var(), which is substituted where it's declared, so a themed subtree re-resolves them. - Likewise what's in a black card, chip, or badge, and a neutral card, so it - doesn't take a tinted card's colors (see utils.js). + Likewise what's in a black card, chip, badge, or tooltip, and a neutral + card, so it doesn't take a tinted card's colors (see utils.js). */ :root, [data-theme], :where( craft-card[data-color='black'] > *, craft-chip[data-color='black']:not([appearance~='plain']) > *, - craft-badge[fill='black'] > * + craft-badge[fill='black'] > *, + craft-tooltip > [slot='content'] > * ), :where( craft-card:is([data-color='white'], [data-color='gray'], [data-color='slate']) @@ -429,7 +430,8 @@ :where( craft-card[data-color='black'] > *, craft-chip[data-color='black']:not([appearance~='plain']) > *, - craft-badge[fill='black'] > * + craft-badge[fill='black'] > *, + craft-tooltip > [slot='content'] > * ) { /* white */ --c-color-white-fill-quiet: var(--c-surface-white); diff --git a/packages/craftcms-ui/src/styles/shared/tokens.css b/packages/craftcms-ui/src/styles/shared/tokens.css index 62beffdf06b..01c222887d1 100644 --- a/packages/craftcms-ui/src/styles/shared/tokens.css +++ b/packages/craftcms-ui/src/styles/shared/tokens.css @@ -1,7 +1,7 @@ /* [data-theme] re-resolves these var()-based tokens against a themed subtree's palette; a var() is substituted where it's declared. What's in a black card, - chip, or badge is themed dark too (see scripts/utils.js). + chip, badge, or tooltip is themed dark too (see scripts/utils.js). */ :root, :host, @@ -9,7 +9,8 @@ :where( craft-card[data-color='black'] > *, craft-chip[data-color='black']:not([appearance~='plain']) > *, - craft-badge[fill='black'] > * + craft-badge[fill='black'] > *, + craft-tooltip > [slot='content'] > * ) { --c-text-xl: calc(20rem / 16); --c-text-lg: calc(18rem / 16); @@ -374,7 +375,8 @@ :where( craft-card[data-color='black'] > *, craft-chip[data-color='black']:not([appearance~='plain']) > *, - craft-badge[fill='black'] > * + craft-badge[fill='black'] > *, + craft-tooltip > [slot='content'] > * ) { color-scheme: dark; /* Icons in neutral badges stand out from their pale label text. */ diff --git a/packages/craftcms-ui/src/styles/shared/variables.css b/packages/craftcms-ui/src/styles/shared/variables.css index 37f34bbad78..5b8c09caba7 100644 --- a/packages/craftcms-ui/src/styles/shared/variables.css +++ b/packages/craftcms-ui/src/styles/shared/variables.css @@ -140,7 +140,8 @@ :where( craft-card[data-color='black'] > *, craft-chip[data-color='black']:not([appearance~='plain']) > *, - craft-badge[fill='black'] > * + craft-badge[fill='black'] > *, + craft-tooltip > [slot='content'] > * ) { --c-color-fill-quiet: var(--c-color-neutral-fill-quiet); --c-color-fill-normal: var(--c-color-neutral-fill-normal); From a2c5af7a02bb96d957d6312c542e72249701b422 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 11 Nov 2026 20:11:08 -0800 Subject: [PATCH 19/23] Put field action menus beside their labels, evenly spaced Beside a visible label, a field's actions now follow the label and its required and translatable indicators rather than sitting at the far end of the heading. The translation button and the field's action menu are flush, so the label text, indicators and buttons all sit the same distance apart. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- CHANGELOG.md | 1 + .../field-group/field-group.browser.test.ts | 34 ++++++++++ .../src/components/field-group/field-group.ts | 16 ++++- .../src/components/field/Field.mdx | 2 +- .../src/components/field/field.a11y.md | 3 +- .../src/components/field/field.styles.ts | 14 +++- .../src/components/field/field.test.ts | 27 ++++++-- .../craftcms-ui/src/components/field/field.ts | 65 ++++++++++++++----- src/Cp/Components/ActionMenu.php | 14 ++++ src/Cp/FormFields.php | 1 + src/Ui/Nodes/ActionMenu.php | 1 + tests/Unit/Cp/Components/ActionMenuTest.php | 11 ++++ 12 files changed, 161 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ae1ed6fc88..2f8ac1ba2a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Icons in the global sidebar, global header, breadcrumbs, details rail, and content tabs are now the same size and color, and rows of icon buttons in the global header and on element cards are tighter. ([#20612](https://github.com/craftcms/cms/pull/20612)) - Global nav items now stay highlighted while their flyout menus are open, and menu buttons stay highlighted while their menus are open. ([#20612](https://github.com/craftcms/cms/pull/20612)) - The details pane now has a subtle inset shadow along its leading edge, and Ask AI’s input is transparent, like the Activity panel’s comment box. ([#20612](https://github.com/craftcms/cms/pull/20612)) +- Fields’ action menus now sit right after their labels and required/translatable indicators, evenly spaced, rather than at the far end of the heading. ([#20612](https://github.com/craftcms/cms/pull/20612)) - Breadcrumb separators are now lighter. ([#20612](https://github.com/craftcms/cms/pull/20612)) - Fixed a bug where fields could be saved with the same handle as another field. ([#20600](https://github.com/craftcms/cms/issues/20600), [#20607](https://github.com/craftcms/cms/pull/20607)) - Fixed an error that occurred when changing a password from the **My Account** → **Password** screen. ([#20585](https://github.com/craftcms/cms/pull/20585)) diff --git a/packages/craftcms-ui/src/components/field-group/field-group.browser.test.ts b/packages/craftcms-ui/src/components/field-group/field-group.browser.test.ts index 26f50f18fd7..40d6cc1c693 100644 --- a/packages/craftcms-ui/src/components/field-group/field-group.browser.test.ts +++ b/packages/craftcms-ui/src/components/field-group/field-group.browser.test.ts @@ -203,3 +203,37 @@ it('keeps separated fields within a group too narrow for its full column gap', a const field = fieldGroup.querySelector('div')!; expect(field.getBoundingClientRect().width).toBe(300); }); + +it('drops a required sub-field’s asterisk in a required fieldset, leaving no gap for it', async () => { + await import('../../styles/cp.css'); + await import('../field/field.js'); + document.body.innerHTML = ` + + + + + + + `; + const [outer, required, optional] = [ + ...document.querySelectorAll('craft-field'), + ]; + for (const field of [outer!, required!, optional!]) { + await field.updateComplete; + } + + const actionsOffset = (field: Element) => { + const label = field.querySelector(':scope > [slot="label"]')!; + const range = document.createRange(); + range.selectNodeContents(label.firstChild!); + + return ( + field.querySelector('button')!.getBoundingClientRect().left - + range.getBoundingClientRect().right + ); + }; + + const asterisk = required!.querySelector('craft-icon[name="asterisk"]')!; + expect(getComputedStyle(asterisk).display).toBe('none'); + expect(actionsOffset(required!)).toBeCloseTo(actionsOffset(optional!), 0); +}); diff --git a/packages/craftcms-ui/src/components/field-group/field-group.ts b/packages/craftcms-ui/src/components/field-group/field-group.ts index 6631442c5ab..bef4a298e20 100644 --- a/packages/craftcms-ui/src/components/field-group/field-group.ts +++ b/packages/craftcms-ui/src/components/field-group/field-group.ts @@ -207,10 +207,24 @@ export default class CraftFieldGroup extends LitElement { > craft-field-group craft-field[required] > [slot='label'] - > craft-icon[name='asterisk'][data-craft-field-decoration] { + craft-icon[name='asterisk'][data-craft-field-decoration] { display: none; } + craft-field[fieldset][required] + > craft-field-group + craft-field[required] + > [slot='label'] + > [data-craft-field-decoration]:not(:has(craft-tooltip)) { + display: contents !important; + } + + craft-field[fieldset][required] + > craft-field-group + craft-field[required]:not([translatable])::part(actions) { + margin-inline-start: var(--_label-text-bearing); + } + @container (min-width: 30rem) { craft-field-group.auto-widths > :not([class*='width-']) { grid-column: span 2; diff --git a/packages/craftcms-ui/src/components/field/Field.mdx b/packages/craftcms-ui/src/components/field/Field.mdx index 61dd5d83106..a7b3aa7c48a 100644 --- a/packages/craftcms-ui/src/components/field/Field.mdx +++ b/packages/craftcms-ui/src/components/field/Field.mdx @@ -69,7 +69,7 @@ Use it when the field holds several controls that share one question — a group -The `actions` slot holds field-level controls: a hide-label toggle, a copy-value button, a settings menu. +The `actions` slot holds field-level controls: a hide-label toggle, a copy-value button, a settings menu. They follow a visible label and its indicators, or sit at the end of the heading when the label is visually hidden. `label-extra` does something similar and is **deprecated** — use `actions`. diff --git a/packages/craftcms-ui/src/components/field/field.a11y.md b/packages/craftcms-ui/src/components/field/field.a11y.md index 5ceae8a946e..485d87af920 100644 --- a/packages/craftcms-ui/src/components/field/field.a11y.md +++ b/packages/craftcms-ui/src/components/field/field.a11y.md @@ -39,6 +39,7 @@ The field supplies the label, instructions, notices, and errors around a slotted - [x] The translation indicator is a button named by `translation-description`. It's reachable with Tab, opens its tooltip on focus, closes it with Escape while keeping focus, and Tab then moves on to the control. Per WCAG 2.1.1 (Keyboard), 2.4.3 (Focus Order), 4.1.2 (Name, Role, Value) and 1.4.13 (Content on Hover or Focus). Verified by `exposes translation help on keyboard focus and dismisses it with Escape` in `field.browser.test.ts`. - [x] The translation indicator is at least 24×24 CSS px. Per WCAG 2.5.8 (Target Size (Minimum)). Verified by `exposes translation help on keyboard focus and dismisses it with Escape` in `field.browser.test.ts`. +- [ ] The translation indicator's target doesn't overlap the target of an action beside it. Per WCAG 2.5.8 (Target Size (Minimum)). Known gap: beside a visible label, the heading's items sit 4px apart, so the translation indicator and a flush action button that follows it overlap by about 5px. ### Visual @@ -48,7 +49,7 @@ The field supplies the label, instructions, notices, and errors around a slotted ### Reflow and zoom -- [ ] At a 320px viewport and at 200% zoom, the heading wraps and the control shrinks to fit, with nothing clipped or overlapping. Per WCAG 1.4.10 (Reflow) and 1.4.4 (Resize Text). Not yet verified. The actions group is absolutely positioned at the end of the heading, so a long label may run under it. +- [ ] At a 320px viewport and at 200% zoom, the heading wraps and the control shrinks to fit, with nothing clipped or overlapping. Per WCAG 1.4.10 (Reflow) and 1.4.4 (Resize Text). Not yet verified. With the label visually hidden, the actions group is absolutely positioned at the end of the heading, so other heading content may run under it. ## Depends on usage diff --git a/packages/craftcms-ui/src/components/field/field.styles.ts b/packages/craftcms-ui/src/components/field/field.styles.ts index fd9ee00f5bc..ef9e597dee9 100644 --- a/packages/craftcms-ui/src/components/field/field.styles.ts +++ b/packages/craftcms-ui/src/components/field/field.styles.ts @@ -63,10 +63,12 @@ export default css` /* Heading row (the CP's .field > .heading) */ .form-field__label { + --_heading-gap: var(--c-spacing); + --_label-text-bearing: calc(var(--c-spacing) * 0.375); position: relative; display: flex; flex-wrap: wrap; - gap: 5px; + gap: var(--_heading-gap); align-items: center; font-size: var(--c-field-label-font-size, inherit); font-weight: var(--c-field-label-font-weight, bold); @@ -89,10 +91,18 @@ export default css` inset-inline-end: 0; } + .field-actions--beside-label { + position: static; + } + + .field-actions--after-text { + margin-inline-start: var(--_label-text-bearing); + } + ::slotted([slot='label']) { display: flex; flex-wrap: wrap; - gap: var(--c-spacing-xs); + gap: var(--_heading-gap); align-items: center; line-height: var(--c-field-label-line-height, inherit); } diff --git a/packages/craftcms-ui/src/components/field/field.test.ts b/packages/craftcms-ui/src/components/field/field.test.ts index 13b1c1bf303..c7f6e396baa 100644 --- a/packages/craftcms-ui/src/components/field/field.test.ts +++ b/packages/craftcms-ui/src/components/field/field.test.ts @@ -391,19 +391,34 @@ describe('craft-field label extras', () => { }); describe('craft-field actions', () => { - it('renders a flex-grow spacer before slotted actions', async () => { + it('renders slotted actions right after a visible label, with no spacer', async () => { const element = await createField( {label: 'My field'}, '' ); + const heading = element.shadowRoot!.querySelector('.form-field__label')!; + const label = heading.querySelector('slot[name="label"]')!; + const actions = heading.querySelector('slot[name="actions"]')!; + expect( + label.compareDocumentPosition(actions) & Node.DOCUMENT_POSITION_FOLLOWING + ).toBeTruthy(); + expect(heading.querySelector('.flex-grow')).toBeNull(); + }); + + it('renders slotted actions at the end, after a spacer, when the label is visually hidden', async () => { + const element = await createField( + {label: 'My field', 'label-sr-only': ''}, + '' + ); + const heading = element.shadowRoot!.querySelector('.form-field__label')!; const spacer = heading.querySelector('.flex-grow'); - const slot = heading.querySelector('slot[name="actions"]'); + const actions = heading.querySelector('slot[name="actions"]')!; expect(spacer).not.toBeNull(); - expect(slot).not.toBeNull(); expect( - spacer!.compareDocumentPosition(slot!) & Node.DOCUMENT_POSITION_FOLLOWING + spacer!.compareDocumentPosition(actions) & + Node.DOCUMENT_POSITION_FOLLOWING ).toBeTruthy(); }); @@ -419,7 +434,7 @@ describe('craft-field actions', () => { expect(group.querySelector('slot[name="actions"]')).not.toBeNull(); }); - it('renders actions after label extras', async () => { + it('renders actions before label extras beside a visible label', async () => { const element = await createField( {label: 'My field'}, 'handle' @@ -430,7 +445,7 @@ describe('craft-field actions', () => { const actions = heading.querySelector('slot[name="actions"]')!; expect( labelExtra.compareDocumentPosition(actions) & - Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_PRECEDING ).toBeTruthy(); }); diff --git a/packages/craftcms-ui/src/components/field/field.ts b/packages/craftcms-ui/src/components/field/field.ts index 79ba9024b93..f443dd288b7 100644 --- a/packages/craftcms-ui/src/components/field/field.ts +++ b/packages/craftcms-ui/src/components/field/field.ts @@ -49,7 +49,8 @@ type FormControlTarget = HTMLElement & { * @slot label-extra - Extra heading content, rendered after a flex-grow spacer. * @deprecated Use `actions` instead. * @slot actions - Field-level actions (hide-label toggles, copy-value buttons, - * field settings menus), rendered as a group at the end of the heading. + * field settings menus), rendered as a group right after a visible label, + * or at the end of the heading when the label is visually hidden. */ export default class CraftField extends FormControl(LitElement) { static override styles = [visuallyHiddenStyles, baseFieldStyles, styles]; @@ -362,6 +363,9 @@ export default class CraftField extends FormControl(LitElement) { * and actions, mirroring `.field > .heading` in the Blade wrapper. Skipped * entirely when there's nothing to put in it. * + * Beside a visible label, the actions follow it and its indicators, so a + * field's menu sits with the name it acts on. Otherwise they go at the end. + * * With `label-sr-only`, the label stays available to screen readers but is * visually hidden, along with the read-only badge that belongs to it. When * they're all the heading has, the whole heading is hidden rather than just @@ -384,6 +388,25 @@ export default class CraftField extends FormControl(LitElement) { return html``; } + const actionsBesideLabel = hasActions && hasLabel && !this.labelSrOnly; + const actions = hasActions + ? html` +
+ +
+ ` + : html``; + let label: TemplateResult | typeof nothing = nothing; if (hasLabel) { label = @@ -413,22 +436,12 @@ export default class CraftField extends FormControl(LitElement) { >` : nothing} - ${hasLabelExtra || hasActions + ${actionsBesideLabel ? actions : nothing} + ${hasLabelExtra || (hasActions && !actionsBesideLabel) ? html`
` : nothing} - ${hasActions - ? html` -
- -
- ` - : html``} + ${actionsBesideLabel ? nothing : actions}
`; @@ -656,6 +669,11 @@ export default class CraftField extends FormControl(LitElement) { * the light DOM (like the server-rendered markup), so they pick up the CP's * global `.visually-hidden`, `.required` and `.t9n-indicator` styles. */ + /** Whether the label gets a required indicator or translation icon. */ + private get __hasLabelDecorations(): boolean { + return Boolean(this.label) && (this.required || this.translatable); + } + private __syncLabelDecorations(): void { const labelNode = this._labelNode; if (!labelNode) { @@ -668,10 +686,22 @@ export default class CraftField extends FormControl(LitElement) { el.remove(); } - if (!this.label) { + if (!this.label || !this.__hasLabelDecorations) { return; } + // One flex item, so only the gap after the label's text widens to make + // up for the space beside each icon's glyph. + const decorations = document.createElement('span'); + decorations.setAttribute('data-craft-field-decoration', ''); + Object.assign(decorations.style, { + display: 'inline-flex', + alignItems: 'center', + gap: 'var(--_heading-gap)', + marginInlineStart: 'var(--_label-text-bearing)', + }); + labelNode.append(decorations); + if (this.required) { const srLabel = document.createElement('craft-visually-hidden'); srLabel.textContent = t('Required'); @@ -688,7 +718,7 @@ export default class CraftField extends FormControl(LitElement) { indicator.setAttribute('data-color', 'danger'); indicator.setAttribute('aria-hidden', 'true'); - labelNode.append(srLabel, indicator); + decorations.append(srLabel, indicator); } if (this.translatable) { @@ -703,13 +733,14 @@ export default class CraftField extends FormControl(LitElement) { button.variant = 'plain'; button.size = 'small'; button.icon = 'language'; + button.setAttribute('flush', ''); button.className = 't9n-indicator prevent-autofocus'; button.setAttribute('aria-label', this.translationDescription); // Keeps the label it sits in from moving focus to the control. button.addEventListener('click', (event) => event.preventDefault()); tooltip.append(button); - labelNode.append(tooltip); + decorations.append(tooltip); } } } diff --git a/src/Cp/Components/ActionMenu.php b/src/Cp/Components/ActionMenu.php index 6d5166a871d..da8880acb36 100644 --- a/src/Cp/Components/ActionMenu.php +++ b/src/Cp/Components/ActionMenu.php @@ -68,6 +68,8 @@ class ActionMenu extends ViewComponent protected bool $disabled = false; + protected bool|string $flush = false; + protected bool $searchable = false; protected ?string $for = null; @@ -149,6 +151,17 @@ public function disabled(bool $disabled = true): static return $this; } + /** + * Pulls the default invoker's padding back from its edges, so its icon + * lines up with neighboring content. See `craft-button`'s `flush`. + */ + public function flush(bool|string $flush = true): static + { + $this->flush = $flush; + + return $this; + } + /** Adds a search input that filters the items as the user types. */ public function searchable(bool $searchable = true): static { @@ -236,6 +249,7 @@ protected function invokerHtml(): string 'variant' => 'plain', 'inherit' => true, 'size' => 'small', + 'flush' => $this->flush ?: null, // Bare `icon` attribute triggers the icon-only button styling // (the icon itself is slotted content, not the `icon` prop — it // needs its own `aria-label`-free presentation since the button diff --git a/src/Cp/FormFields.php b/src/Cp/FormFields.php index ab176d3756e..fcc5db497e4 100644 --- a/src/Cp/FormFields.php +++ b/src/Cp/FormFields.php @@ -154,6 +154,7 @@ private static function fieldFromConfig(string|Stringable|callable $input, array ? ActionMenu::make() ->menuItems($config['actionMenuItems']) ->label(t('Actions')) + ->flush() ->toHtml() : null, $showAttribute diff --git a/src/Ui/Nodes/ActionMenu.php b/src/Ui/Nodes/ActionMenu.php index a0555184396..f67a785a0f1 100644 --- a/src/Ui/Nodes/ActionMenu.php +++ b/src/Ui/Nodes/ActionMenu.php @@ -67,6 +67,7 @@ public static function renderHtml(NodePayload $node, UiPayload $payload, UiHtmlR ->items($node->props['items']) ->icon((string) $node->props['icon']) ->label($node->props['label']) + ->flush() ->attributes(['data-ui-node' => $node->uid]) ->toHtml(); } diff --git a/tests/Unit/Cp/Components/ActionMenuTest.php b/tests/Unit/Cp/Components/ActionMenuTest.php index 0d4d895d9b0..44349ea1b0d 100644 --- a/tests/Unit/Cp/Components/ActionMenuTest.php +++ b/tests/Unit/Cp/Components/ActionMenuTest.php @@ -34,3 +34,14 @@ expect($items[0]['action'])->toMatchArray(['type' => 'http', 'requireElevatedSession' => true]) ->and($items[1]['action'])->not->toHaveKey('requireElevatedSession'); }); + +it('makes its invoker flush only when asked', function () { + expect(ActionMenu::make()->items([['label' => 'News']])->toHtml()) + ->not->toContain('flush'); + + expect(ActionMenu::make()->items([['label' => 'News']])->flush()->toHtml()) + ->toMatch('/]*\bflush\b[^>]*slot="invoker"|]*slot="invoker"[^>]*\bflush\b/'); + + expect(ActionMenu::make()->items([['label' => 'News']])->flush('inline')->toHtml()) + ->toContain('flush="inline"'); +}); From b79d32e36dc4957448a28e14269a036851437563 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Thu, 12 Nov 2026 09:54:30 -0800 Subject: [PATCH 20/23] =?UTF-8?q?Show=20fields=E2=80=99=20translation=20in?= =?UTF-8?q?dicator=20as=20an=20info=20icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So its description opens on a press, including on touch screens, rather than on hover. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V33uARgrvVtiFqtAthPJbg --- .../src/components/field-group/field-group.ts | 2 +- .../src/components/field/Field.mdx | 2 +- .../src/components/field/field.a11y.md | 7 ++-- .../components/field/field.browser.test.ts | 40 +++++++++++-------- .../craftcms-ui/src/components/field/field.ts | 27 +++++-------- 5 files changed, 38 insertions(+), 40 deletions(-) diff --git a/packages/craftcms-ui/src/components/field-group/field-group.ts b/packages/craftcms-ui/src/components/field-group/field-group.ts index bef4a298e20..a009d8fe782 100644 --- a/packages/craftcms-ui/src/components/field-group/field-group.ts +++ b/packages/craftcms-ui/src/components/field-group/field-group.ts @@ -215,7 +215,7 @@ export default class CraftFieldGroup extends LitElement { > craft-field-group craft-field[required] > [slot='label'] - > [data-craft-field-decoration]:not(:has(craft-tooltip)) { + > [data-craft-field-decoration]:not(:has(craft-info-icon)) { display: contents !important; } diff --git a/packages/craftcms-ui/src/components/field/Field.mdx b/packages/craftcms-ui/src/components/field/Field.mdx index a7b3aa7c48a..fed79a6cf63 100644 --- a/packages/craftcms-ui/src/components/field/Field.mdx +++ b/packages/craftcms-ui/src/components/field/Field.mdx @@ -55,7 +55,7 @@ Errors render in the `feedback` slot and are wired to the control with `aria-des -The status badge marks a field whose value is outdated or overridden. The translation indicator marks a field whose value is per-site, with `translation-description` explaining the scope. +The status badge marks a field whose value is outdated or overridden. The translation indicator, an info icon, marks a field whose value is per-site; pressing it shows `translation-description`, explaining the scope. ## Fieldsets diff --git a/packages/craftcms-ui/src/components/field/field.a11y.md b/packages/craftcms-ui/src/components/field/field.a11y.md index 485d87af920..016a984b222 100644 --- a/packages/craftcms-ui/src/components/field/field.a11y.md +++ b/packages/craftcms-ui/src/components/field/field.a11y.md @@ -16,7 +16,7 @@ The field supplies the label, instructions, notices, and errors around a slotted - [x] `label-sr-only` hides the label visually but keeps it as the control's accessible name. Per WCAG 4.1.2 (Name, Role, Value). Verified by `leaves no space above the input for a visually hidden label` in `field.browser.test.ts` and `visually hides the whole heading when the label is all it has` in `field.test.ts`. - [x] `instructions-sr-only` hides the instructions visually but keeps them in the input's accessible description, for a field whose placeholder repeats them, as a placeholder alone isn't reliably announced and disappears once there's text. Per WCAG 1.3.1 (Info and Relationships). Verified by `visually hides the instructions but keeps them in the description` in `field.test.ts`. - [ ] Field actions are grouped with `role="group"` and the name "Field actions". Per WCAG 1.3.1 (Info and Relationships). Not yet verified. `groups slotted actions` in `field.test.ts` checks the role but not the name. -- [ ] The translation indicator sits inside the label node, so its name may be folded into the control's accessible name, and it puts a button inside a `