From 07a684424a5895793899c20366bd9b4a0426be6b Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Thu, 30 Jul 2026 05:33:41 -0700 Subject: [PATCH 1/2] chore(ui): add light mode to code elements (#9031) * chore(ui): add light mode to code elements * fixup! * fixup! * shiki Signed-off-by: Aviv Keller --------- Signed-off-by: Aviv Keller --- .changeset/young-breads-fall.md | 6 ++++ .../src/__tests__/highlighter.test.mjs | 16 ++++++++++ packages/rehype-shiki/src/highlighter.mjs | 27 ++++++++++++---- packages/rehype-shiki/src/index.css | 13 ++++++++ .../src/transformers/twoslash/index.css | 8 ++++- .../src/Common/BaseButton/index.module.css | 16 ++++++---- .../src/Common/BaseCodeBox/index.module.css | 24 ++++++++------ .../src/Common/CodeTabs/index.module.css | 31 ++++++++++++------- 8 files changed, 108 insertions(+), 33 deletions(-) create mode 100644 .changeset/young-breads-fall.md diff --git a/.changeset/young-breads-fall.md b/.changeset/young-breads-fall.md new file mode 100644 index 0000000000000..de376107555d4 --- /dev/null +++ b/.changeset/young-breads-fall.md @@ -0,0 +1,6 @@ +--- +'@node-core/ui-components': patch +'@node-core/rehype-shiki': patch +--- + +Add light mode theming to code elements diff --git a/packages/rehype-shiki/src/__tests__/highlighter.test.mjs b/packages/rehype-shiki/src/__tests__/highlighter.test.mjs index 05b1f208e0cf2..49479bedc367f 100644 --- a/packages/rehype-shiki/src/__tests__/highlighter.test.mjs +++ b/packages/rehype-shiki/src/__tests__/highlighter.test.mjs @@ -25,6 +25,13 @@ mock.module('shiki/themes/nord.mjs', { defaultExport: { name: 'nord', colors: { 'editor.background': '#2e3440' } }, }); +mock.module('shiki/themes/github-light-default.mjs', { + defaultExport: { + name: 'github-light-default', + colors: { 'editor.background': '#fff' }, + }, +}); + describe('createHighlighter', async () => { const { default: createHighlighter } = await import('../highlighter.mjs'); @@ -70,6 +77,15 @@ describe('createHighlighter', async () => { const [, options] = mockShiki.codeToHtml.mock.calls.at(-1).arguments; assert.strictEqual(options.lang, 'text'); }); + + it('emits light and dark syntax colors with light as the default', () => { + const highlighter = createHighlighter({}); + highlighter.highlightToHtml('const x = 1;', 'javascript'); + + const [, options] = mockShiki.codeToHtml.mock.calls.at(-1).arguments; + assert.deepStrictEqual(Object.keys(options.themes), ['light', 'dark']); + assert.strictEqual(options.defaultColor, 'light'); + }); }); describe('highlightToHast', () => { diff --git a/packages/rehype-shiki/src/highlighter.mjs b/packages/rehype-shiki/src/highlighter.mjs index 3b2c971563119..52920e33cbba0 100644 --- a/packages/rehype-shiki/src/highlighter.mjs +++ b/packages/rehype-shiki/src/highlighter.mjs @@ -1,12 +1,21 @@ import { createHighlighterCoreSync, isSpecialLang } from '@shikijs/core'; +import shikiGitHubLightTheme from 'shiki/themes/github-light-default.mjs'; import shikiNordTheme from 'shiki/themes/nord.mjs'; -const DEFAULT_THEME = { +const DEFAULT_DARK_THEME = { // We are updating this color because the background color and comment text color // in the Codebox component do not comply with accessibility standards. // See: https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html - colorReplacements: { '#616e88': '#707e99' }, ...shikiNordTheme, + colorReplacements: { + ...shikiNordTheme.colorReplacements, + '#616e88': '#707e99', + }, +}; + +const DEFAULT_THEMES = { + light: shikiGitHubLightTheme, + dark: DEFAULT_DARK_THEME, }; const FALLBACK_LANGUAGE = 'text'; @@ -42,12 +51,18 @@ export const getLanguageByName = (language, langs) => { * @returns {SyntaxHighlighter} */ const createHighlighter = ({ coreOptions = {}, highlighterOptions = {} }) => { + const usesCustomTheme = + 'theme' in highlighterOptions || 'themes' in highlighterOptions; const options = { - themes: [DEFAULT_THEME], + themes: Object.values(DEFAULT_THEMES), ...coreOptions, }; const shiki = createHighlighterCoreSync(options); - const theme = options.themes[0]; + const themeOptions = usesCustomTheme + ? {} + : coreOptions.themes + ? { theme: options.themes[0] } + : { themes: DEFAULT_THEMES, defaultColor: 'light' }; const loadedLanguages = new Set( shiki.getLoadedLanguages().map(lang => lang.toLowerCase()) @@ -86,7 +101,7 @@ const createHighlighter = ({ coreOptions = {}, highlighterOptions = {} }) => { shiki .codeToHtml(code, { lang: resolveLanguage(lang), - theme, + ...themeOptions, meta, ...highlighterOptions, }) @@ -105,7 +120,7 @@ const createHighlighter = ({ coreOptions = {}, highlighterOptions = {} }) => { const highlightToHast = (code, lang, meta = {}) => shiki.codeToHast(code, { lang: resolveLanguage(lang), - theme, + ...themeOptions, meta, ...highlighterOptions, }); diff --git a/packages/rehype-shiki/src/index.css b/packages/rehype-shiki/src/index.css index 61a75fb5ed25f..92ddfcfcce8ba 100644 --- a/packages/rehype-shiki/src/index.css +++ b/packages/rehype-shiki/src/index.css @@ -1 +1,14 @@ @import './transformers/twoslash/index.css'; + +/* + * Shiki renders the light palette as the inline default and stores the dark + * palette in custom properties. This selector also covers snippets whose + * highlighted HTML is inserted without the outer `.shiki` element. + */ +[data-theme='dark'] span[style*='--shiki-dark'] { + color: var(--shiki-dark) !important; + background-color: var(--shiki-dark-bg) !important; + font-style: var(--shiki-dark-font-style) !important; + font-weight: var(--shiki-dark-font-weight) !important; + text-decoration: var(--shiki-dark-text-decoration) !important; +} diff --git a/packages/rehype-shiki/src/transformers/twoslash/index.css b/packages/rehype-shiki/src/transformers/twoslash/index.css index 4ceaa24315153..37073ff585027 100644 --- a/packages/rehype-shiki/src/transformers/twoslash/index.css +++ b/packages/rehype-shiki/src/transformers/twoslash/index.css @@ -17,13 +17,19 @@ min-width: 100%; padding: 6px 12px; white-space: pre-wrap; - background-color: var(--color-neutral-950); + color: var(--color-neutral-900); + background-color: var(--color-neutral-100); span { display: inline-block; } } +[data-theme='dark'] .twoslash-popup-code:not(:has(div)) { + color: var(--color-white); + background-color: var(--color-neutral-950); +} + .twoslash-completion-list { display: block; width: auto; diff --git a/packages/ui-components/src/Common/BaseButton/index.module.css b/packages/ui-components/src/Common/BaseButton/index.module.css index bb4c5ce10619e..6d839ac2bce7f 100644 --- a/packages/ui-components/src/Common/BaseButton/index.module.css +++ b/packages/ui-components/src/Common/BaseButton/index.module.css @@ -27,21 +27,25 @@ &.neutral { @apply rounded-sm - bg-neutral-900 - text-white + bg-neutral-100 + text-neutral-900 + dark:bg-neutral-900 dark:text-neutral-200; &:hover:not([aria-disabled='true']) { - @apply bg-neutral-800; + @apply bg-neutral-200 + dark:bg-neutral-800; } &[aria-disabled='true'] { - @apply bg-neutral-900 - opacity-50; + @apply bg-neutral-100 + opacity-50 + dark:bg-neutral-900; } &:focus { - @apply bg-neutral-800; + @apply bg-neutral-200 + dark:bg-neutral-800; } } diff --git a/packages/ui-components/src/Common/BaseCodeBox/index.module.css b/packages/ui-components/src/Common/BaseCodeBox/index.module.css index b74075c89c921..a9a02a2a7af3d 100644 --- a/packages/ui-components/src/Common/BaseCodeBox/index.module.css +++ b/packages/ui-components/src/Common/BaseCodeBox/index.module.css @@ -6,8 +6,10 @@ overflow-x-hidden rounded-sm border - border-neutral-900 - bg-neutral-950; + border-neutral-200 + bg-white + dark:border-neutral-900 + dark:bg-neutral-950; .content { @apply m-0 @@ -26,8 +28,9 @@ p-0 text-sm leading-snug - text-neutral-400 - [counter-reset:line]; + text-neutral-800 + [counter-reset:line] + dark:text-neutral-400; & > [class='line'] { @apply relative @@ -48,9 +51,10 @@ mr-4 w-4.5 text-right - text-neutral-600 + text-neutral-700 [content:counter(line)] - [counter-increment:line]; + [counter-increment:line] + dark:text-neutral-600; } } @@ -74,14 +78,16 @@ items-center justify-between border-t - border-t-neutral-900 + border-t-neutral-200 px-3 py-2 text-sm - font-medium; + font-medium + dark:border-t-neutral-900; & > .language { - @apply text-neutral-400; + @apply text-neutral-800 + dark:text-neutral-400; } & > .action { diff --git a/packages/ui-components/src/Common/CodeTabs/index.module.css b/packages/ui-components/src/Common/CodeTabs/index.module.css index 5f738303ceff0..30a5f0dd155dd 100644 --- a/packages/ui-components/src/Common/CodeTabs/index.module.css +++ b/packages/ui-components/src/Common/CodeTabs/index.module.css @@ -10,21 +10,26 @@ rounded-t border-x border-t - border-neutral-900 - bg-neutral-950 + border-neutral-200 + bg-neutral-100 px-2 pt-2 - md:px-4; + md:px-4 + dark:border-neutral-900 + dark:bg-neutral-950; .trigger { @apply border-b border-b-transparent px-1 - text-neutral-200; + text-neutral-800 + dark:text-neutral-200; &[data-state='active'] { - @apply border-b-brand-400 - text-brand-400; + @apply border-b-brand-600 + text-brand-700 + dark:border-b-brand-400 + dark:text-brand-400; } } @@ -33,21 +38,25 @@ items-center gap-2 text-center - text-neutral-200 + text-neutral-800 motion-safe:transition-colors - lg:flex; + lg:flex + dark:text-neutral-200; & > .icon { @apply size-4 - text-neutral-300; + text-neutral-700 + dark:text-neutral-300; } &:is(:link, :visited) { &:hover { - @apply text-neutral-400; + @apply text-neutral-900 + dark:text-neutral-400; & > .icon { - @apply text-neutral-600; + @apply text-neutral-800 + dark:text-neutral-600; } } } From ca47d6fa908901335705782ee71216f711fa87c8 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Thu, 30 Jul 2026 05:37:15 -0700 Subject: [PATCH 2/2] chore(ui): account for nested `code` elements (#9028) * chore(ui): account for nested `code` elements * fixup! Signed-off-by: Aviv Keller --------- Signed-off-by: Aviv Keller --- .changeset/ui-hydration-fix.md | 5 +++++ .../ui-components/src/Common/BaseCodeBox/index.module.css | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/ui-hydration-fix.md diff --git a/.changeset/ui-hydration-fix.md b/.changeset/ui-hydration-fix.md new file mode 100644 index 0000000000000..6d9e08a4f5a58 --- /dev/null +++ b/.changeset/ui-hydration-fix.md @@ -0,0 +1,5 @@ +--- +'@node-core/ui-components': patch +--- + +Account for a possible wrapper between a code boxes `pre` and `code`. diff --git a/packages/ui-components/src/Common/BaseCodeBox/index.module.css b/packages/ui-components/src/Common/BaseCodeBox/index.module.css index a9a02a2a7af3d..da0f2e11de2a0 100644 --- a/packages/ui-components/src/Common/BaseCodeBox/index.module.css +++ b/packages/ui-components/src/Common/BaseCodeBox/index.module.css @@ -16,7 +16,7 @@ min-w-0 p-4; - & > code { + code { @apply font-ibm-plex-mono font-regular scrollbar-thin @@ -63,7 +63,7 @@ } } - &[class*='no-line-numbers'] > code > [class='line'] { + &[class*='no-line-numbers'] code > [class='line'] { @apply pl-0; &:not(:empty:last-child)::after {