From 37f89e4ed85da4b0e172ec31822aaad77e8559e4 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Tue, 28 Jul 2026 15:42:55 -0700 Subject: [PATCH 1/2] chore(ui): add light mode to code elements --- .changeset/young-breads-fall.md | 5 +++ .../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/BaseCodeBox/index.module.css | 24 ++++++++------ .../src/Common/CodeTabs/index.module.css | 31 ++++++++++++------- 7 files changed, 97 insertions(+), 27 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..12677142020d0 --- /dev/null +++ b/.changeset/young-breads-fall.md @@ -0,0 +1,5 @@ +--- +'@node-core/ui-components': 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..6dc2dfee9c1ee 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.mjs', { + defaultExport: { + name: 'github-light', + 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..11b4057ce0879 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.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/BaseCodeBox/index.module.css b/packages/ui-components/src/Common/BaseCodeBox/index.module.css index b74075c89c921..1c92080f152be 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-500 [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-700 + 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..3867623005f01 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-700 + 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-700 motion-safe:transition-colors - lg:flex; + lg:flex + dark:text-neutral-200; & > .icon { @apply size-4 - text-neutral-300; + text-neutral-600 + 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 31efd1197ba960531ef1fc4d1ebba9313885db35 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Tue, 28 Jul 2026 15:49:02 -0700 Subject: [PATCH 2/2] fixup! --- .../src/Common/BaseButton/index.module.css | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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; } }