adding all button variants as dark mode (#30989)

* adding all button variants as dark mode

* feat: removing unecessary mixin after move to dm buttons

* fix: prefix button css vars, and scope them out of modals

* fix: update link button on project list dash

* Use themed styles in the search form (#30489)

* [web] Introduce dark mode for codemirror search form

* [web] Tweak search form dark mode

---------

Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>

---------

Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>
Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
GitOrigin-RevId: a67ea76d5e03e96c1df8a17063aa332e7cb2d1a3
This commit is contained in:
Jimmy Domagala-Tang
2026-01-29 10:04:16 -05:00
committed by Copybot
parent e80ce42896
commit 0e51f08e58
10 changed files with 218 additions and 108 deletions

View File

@@ -38,6 +38,7 @@ import { EditorSelection, EditorState } from '@codemirror/state'
import { sendSearchEvent } from '@/features/event-tracking/search-events'
import { FullProjectSearchButton } from './full-project-search-button'
import { isInvalidRegExp } from '../utils/regexp'
import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme'
const MATCH_COUNT_DEBOUNCE_WAIT = 100 // the amount of ms to wait before counting matches
const MAX_MATCH_COUNT = 999 // the maximum number of matches to count
@@ -82,6 +83,8 @@ const CodeMirrorSearchForm: FC<React.PropsWithChildren> = () => {
const inputRef = useRef<HTMLInputElement | null>(null)
const replaceRef = useRef<HTMLInputElement | null>(null)
const overallTheme = useActiveOverallTheme()
const handleInputRef = useCallback((node: HTMLInputElement) => {
inputRef.current = node
@@ -418,7 +421,7 @@ const CodeMirrorSearchForm: FC<React.PropsWithChildren> = () => {
<div className="ol-cm-search-form-group ol-cm-search-next-previous">
<OLButtonGroup className="ol-cm-search-form-button-group">
<OLButton
variant="secondary"
variant="ghost"
size="sm"
onClick={() => findPrevious(view)}
>
@@ -428,11 +431,7 @@ const CodeMirrorSearchForm: FC<React.PropsWithChildren> = () => {
/>
</OLButton>
<OLButton
variant="secondary"
size="sm"
onClick={() => findNext(view)}
>
<OLButton variant="ghost" size="sm" onClick={() => findNext(view)}>
<MaterialIcon
type="keyboard_arrow_down"
accessibilityLabel={t('search_next')}
@@ -456,7 +455,7 @@ const CodeMirrorSearchForm: FC<React.PropsWithChildren> = () => {
{showReplace && (
<div className="ol-cm-search-form-group ol-cm-search-replace-buttons">
<OLButton
variant="secondary"
variant="ghost"
size="sm"
onClick={() => {
sendSearchEvent('search-replace-click', {
@@ -471,7 +470,7 @@ const CodeMirrorSearchForm: FC<React.PropsWithChildren> = () => {
</OLButton>
<OLButton
variant="secondary"
variant="ghost"
size="sm"
onClick={() => {
sendSearchEvent('search-replace-click', {
@@ -490,7 +489,10 @@ const CodeMirrorSearchForm: FC<React.PropsWithChildren> = () => {
<div className="ol-cm-search-form-close">
<OLTooltip id="search-close" description={<>{t('close')} (Esc)</>}>
<OLCloseButton onClick={() => closeSearchPanel(view)} />
<OLCloseButton
variant={overallTheme === 'dark' ? 'white' : undefined}
onClick={() => closeSearchPanel(view)}
/>
</OLTooltip>
</div>
</form>

View File

@@ -47,7 +47,7 @@ export const FullProjectSearchButton = ({ query }: { query: SearchQuery }) => {
overlayProps={{ placement: 'top' }}
description={t('search_all_project_files')}
>
<OLButton variant="secondary" size="sm" onClick={onClick}>
<OLButton variant="ghost" size="sm" onClick={onClick}>
<MaterialIcon
type="manage_search"
accessibilityLabel={t('search_all_project_files')}

View File

@@ -301,12 +301,13 @@ export const search = (initialSearchQuery: SearchQuery | null) => {
const searchFormTheme = EditorView.theme({
'.ol-cm-search-form': {
'--ol-cm-search-form-gap': '10px',
'--ol-cm-search-form-button-margin': '3px',
'--ol-cm-search-form-gap': 'var(--spacing-05)',
'--ol-cm-search-form-button-margin': 'var(--spacing-02)',
'--input-border': 'var(--border-primary)',
'--input-border-focus': 'var(--border-active)',
padding: 'var(--ol-cm-search-form-gap)',
display: 'flex',
gap: 'var(--ol-cm-search-form-gap)',
background: 'var(--neutral-20)',
'--ol-cm-search-form-focus-shadow':
'inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px rgb(102 175 233 / 60%)',
'--ol-cm-search-form-error-shadow':
@@ -316,12 +317,6 @@ const searchFormTheme = EditorView.theme({
padding: 'var(--spacing-03) var(--spacing-05)',
},
},
'&.ol-cm-search-form': {
'--ol-cm-search-form-gap': 'var(--spacing-05)',
'--ol-cm-search-form-button-margin': 'var(--spacing-02)',
'--input-border': 'var(--border-primary)',
'--input-border-focus': 'var(--border-active)',
},
'.ol-cm-search-controls': {
display: 'grid',
gridTemplateColumns: 'auto auto',
@@ -345,24 +340,30 @@ const searchFormTheme = EditorView.theme({
alignItems: 'center',
},
'.ol-cm-search-input-group': {
border: '1px solid var(--input-border)',
backgroundColor: 'var(--bg-primary-themed)',
border: '1px solid var(--border-primary-themed)',
borderRadius: '20px',
background: 'white',
width: '100%',
maxWidth: '50em',
display: 'inline-flex',
alignItems: 'center',
'--input-field-color': 'var(--content-primary-themed)',
'--input-field-bg': 'var(--bg-primary-themed)',
'--input-placeholder-content': 'var(--content-placeholder-themed)',
'--input-field-content-disabled': 'var(--content-disabled-themed)',
'& input[type="text"]': {
background: 'none',
boxShadow: 'none',
borderRadius: '20px',
},
'& input[type="text"]:focus': {
outline: 'none',
background: 'none',
boxShadow: 'none',
},
'& .btn.btn': {
background: 'var(--neutral-10)',
color: 'var(--neutral-60)',
background: 'var(--bg-secondary-themed)',
color: 'var(--content-secondary-themed)',
borderRadius: '50%',
height: '2em',
display: 'inline-flex',
@@ -379,9 +380,11 @@ const searchFormTheme = EditorView.theme({
},
},
'&:focus-within': {
borderColor: 'var(--input-border-focus)',
boxShadow: 'var(--ol-cm-search-form-focus-shadow)',
},
'& .form-control': {
color: 'var(--content-primary-themed)',
},
},
'.ol-cm-search-input-group.ol-cm-search-input-error': {
'&:focus-within': {
@@ -406,7 +409,7 @@ const searchFormTheme = EditorView.theme({
},
'.ol-cm-search-form-position': {
flexShrink: 0,
color: 'var(--content-secondary)',
color: 'var(--content-secondary-themed)',
minWidth: '5em',
},
'.ol-cm-search-hidden-inputs': {

View File

@@ -188,6 +188,12 @@ const staticTheme = EditorView.theme({
'&.cm-editor.cm-focused:not(:focus-visible)': {
outline: 'none',
},
'.cm-panels': {
backgroundColor: 'var(--bg-secondary-themed)',
},
'.cm-panel-bottom': {
borderTop: '1px solid var(--border-primary-themed)',
},
// override default styles for the search panel
'.cm-panel.cm-search label': {
display: 'inline-flex',

View File

@@ -320,7 +320,7 @@ const toolbarTheme = EditorView.theme({
})
const toolbarBorderTheme = EditorView.baseTheme({
'&light.overall-theme-dark .cm-panels-top': {
'&.overall-theme-dark .cm-panels-top': {
borderBottom: '1px solid var(--border-divider-dark)',
},
})

View File

@@ -23,22 +23,46 @@ const variants: ButtonProps['variant'][] = [
export const Variants: Story = {
render() {
return (
<div className="d-flex flex-column gap-2">
{variants.map(variant => (
<div key={variant} className="d-flex gap-2">
<OLButton variant={variant}>Button</OLButton>
<OLButton variant={variant} isLoading>
Button
</OLButton>
<OLButton variant={variant} disabled>
Button
</OLButton>
<OLButton variant={variant} size="sm">
Button
</OLButton>
</div>
))}
</div>
<>
<div className="d-flex flex-column gap-2 p-4">
{variants.map(variant => (
<div key={variant} className="d-flex gap-2">
<OLButton variant={variant}>Button</OLButton>
<OLButton variant={variant} isLoading>
Button
</OLButton>
<OLButton variant={variant} disabled>
Button
</OLButton>
<OLButton variant={variant} size="sm">
Button
</OLButton>
</div>
))}
</div>
<h1> Dark Mode</h1>
<br />
<div
className="d-flex flex-column gap-2 p-4 ide-redesign-main"
data-theme="default"
style={{ backgroundColor: '#2f3a4c' }}
>
{variants.map(variant => (
<div key={variant} className="d-flex gap-2">
<OLButton variant={variant}>Button</OLButton>
<OLButton variant={variant} isLoading>
Button
</OLButton>
<OLButton variant={variant} disabled>
Button
</OLButton>
<OLButton variant={variant} size="sm">
Button
</OLButton>
</div>
))}
</div>
</>
)
},
}

View File

@@ -75,39 +75,6 @@
);
}
// util for creating the base themes for each button, only usable when scoped to -themed available classes
// currently only primary and secondary buttons implemented
// todo: once all button types have matching light/dark designs, remove mixin and implement in buttons.scss
@mixin ol-button-themed($variant) {
// primary button remains the same in both dark and light
@if $variant == 'primary' {
@include ol-button-variant(
$color: var(--content-primary-dark),
$background: var(--bg-accent-01),
$hover-background: var(--bg-accent-02),
$hover-border: var(--bg-accent-02)
);
} @else if $variant == 'secondary' {
@include ol-button-variant(
$color: var(--content-primary-themed),
$background: var(--bg-primary-themed),
$border: var(--border-primary-themed),
$hover-background: var(--bg-secondary-themed),
$hover-border: var(--border-hover-themed),
$borderless: false
);
} @else {
@include ol-button-variant(
$color: var(--content-primary-themed),
$background: var(--bg-primary-themed),
$border: var(--border-primary-themed),
$hover-background: var(--bg-secondary-themed),
$hover-border: var(--border-hover-themed),
$borderless: false
);
}
}
@mixin reset-button() {
padding: 0;
cursor: pointer;

View File

@@ -1,3 +1,108 @@
@mixin light-buttons {
// primary
--btn-primary-color: var(--white);
--btn-primary-background: var(--bg-accent-01);
--btn-primary-hover-background: var(--bg-accent-02);
--btn-primary-hover-border: var(--bg-accent-02);
// secondary
--btn-secondary-color: var(--content-primary);
--btn-secondary-background: var(--bg-light-primary);
--btn-secondary-border: var(--border-primary);
--btn-secondary-hover-background: var(--bg-light-tertiary);
--btn-secondary-hover-border: var(--border-primary);
// ghost
--btn-ghost-color: var(--content-primary);
--btn-ghost-background: var(--bg-light-primary);
--btn-ghost-hover-background: var(--bg-light-tertiary);
// danger
--btn-danger-color: var(--white);
--btn-danger-background: var(--bg-danger-01);
--btn-danger-border: var(--bg-danger-01);
--btn-danger-hover-background: var(--bg-danger-02);
// danger-ghost
--btn-danger-ghost-color: var(--content-danger);
--btn-danger-ghost-background: var(--bg-light-primary);
--btn-danger-ghost-hover-background: var(--bg-danger-03);
--btn-danger-ghost-border: var(--bg-light-primary);
--btn-danger-ghost-hover-border: var(--bg-light-primary);
// premium
--btn-premium-color: var(--white);
--btn-premium-background: var(--blue-70);
--btn-premium-hover-background: var(--blue-70);
// premium-secondary
--btn-premium-secondary-color: var(--blue-60);
--btn-premium-secondary-background: var(--bg-light-primary);
--btn-premium-secondary-border: var(--blue-60);
--btn-premium-secondary-hover-background: var(--bg-info-03);
--btn-premium-secondary-hover-border: var(--blue-60);
}
@mixin dark-buttons {
// primary
--btn-primary-color: var(--white);
--btn-primary-background: var(--bg-accent-01);
--btn-primary-hover-background: var(--bg-accent-02);
--btn-primary-hover-border: var(--bg-accent-02);
// secondary
--btn-secondary-color: var(--white);
--btn-secondary-background: var(--bg-dark-primary);
--btn-secondary-border: var(--bg-light-primary);
--btn-secondary-hover-background: var(--bg-dark-tertiary);
--btn-secondary-hover-border: var(--bg-light-primary);
// ghost
--btn-ghost-color: var(--white);
--btn-ghost-background: var(--bg-dark-secondary);
--btn-ghost-hover-background: var(--bg-dark-tertiary);
// danger
--btn-danger-color: var(--white);
--btn-danger-background: var(--bg-danger-01);
--btn-danger-border: var(--bg-danger-01);
--btn-danger-hover-background: var(--bg-danger-02);
// danger-ghost
--btn-danger-ghost-color: var(--bg-danger-03);
--btn-danger-ghost-background: var(--bg-dark-secondary);
--btn-danger-ghost-hover-background: var(--bg-dark-tertiary);
--btn-danger-ghost-border: var(--bg-danger-01);
--btn-danger-ghost-hover-border: var(--bg-danger-01);
// premium
--btn-premium-color: var(--white);
--btn-premium-background: var(--blue-70);
--btn-premium-hover-background: var(--blue-70);
// premium-secondary, todo once WIP designs for dm are finished
--btn-premium-secondary-color: var(--content-primary);
--btn-premium-secondary-background: var(--blue-30);
--btn-premium-secondary-border: var(--blue-60);
--btn-premium-secondary-hover-background: var(--blue-30);
--btn-premium-secondary-hover-border: var(--blue-60);
}
:root {
.btn {
@include light-buttons;
}
@include theme('default') {
.ide-redesign-main,
.project-ds-nav-page {
.btn {
@include dark-buttons;
}
}
}
}
.btn {
// Focus style for all buttons
--bs-btn-focus-box-shadow: 0 0 0 2px var(--border-active-dark);
@@ -35,70 +140,73 @@
// Variants
&.btn-primary {
@include ol-button-variant(
$color: var(--white),
$background: var(--bg-accent-01),
$hover-background: var(--bg-accent-02),
$hover-border: var(--bg-accent-02)
$color: var(--btn-primary-color),
$background: var(--btn-primary-background),
$hover-background: var(--btn-primary-hover-background),
$hover-border: var(--btn-primary-hover-border)
);
}
&.btn-secondary {
@include ol-button-variant(
$color: var(--content-primary),
$background: var(--bg-light-primary),
$border: var(--border-primary),
$hover-background: var(--bg-light-tertiary),
$hover-border: var(--border-primary),
$color: var(--btn-secondary-color),
$background: var(--btn-secondary-background),
$border: var(--btn-secondary-border),
$hover-background: var(--btn-secondary-hover-background),
$hover-border: var(--btn-secondary-hover-border),
$borderless: false
);
}
&.btn-ghost {
@include ol-button-variant(
$color: var(--content-primary),
$background: var(--bg-light-primary),
$hover-background: var(--bg-light-tertiary)
$color: var(--btn-ghost-color),
$background: var(--btn-ghost-background),
$hover-background: var(--btn-ghost-hover-background)
);
}
&.btn-danger {
@include ol-button-variant(
$color: var(--white),
$background: var(--bg-danger-01),
$border: var(--bg-danger-01),
$hover-background: var(--bg-danger-02)
$color: var(--btn-danger-color),
$background: var(--btn-danger-background),
$border: var(--btn-danger-border),
$hover-background: var(--btn-danger-hover-background)
);
}
&.btn-danger-ghost {
@include ol-button-variant(
$color: var(--content-danger),
$background: var(--bg-light-primary),
$hover-background: var(--bg-danger-03)
$color: var(--btn-danger-ghost-color),
$background: var(--btn-danger-ghost-background),
$border: var(--btn-danger-ghost-border),
$hover-background: var(--btn-danger-ghost-hover-background),
$hover-border: var(--btn-danger-ghost-hover-border),
$borderless: false
);
}
&.btn-premium {
@include ol-button-variant(
$color: var(--white),
$background: var(--blue-70)
$color: var(--btn-premium-color),
$background: var(--btn-premium-background)
);
background: var(--premium-gradient);
transition: none;
&:hover {
background: var(--blue-70);
background: var(--btn-premium-hover-background);
}
}
&.btn-premium-secondary {
@include ol-button-variant(
$color: var(--blue-60),
$background: var(--bg-light-primary),
$border: var(--blue-60),
$hover-background: var(--bg-info-03),
$hover-border: var(--blue-60),
$color: var(--btn-premium-secondary-color),
$background: var(--btn-premium-secondary-background),
$border: var(--btn-premium-secondary-border),
$hover-background: var(--btn-premium-secondary-hover-background),
$hover-border: var(--btn-premium-secondary-hover-border),
$borderless: false
);
}

View File

@@ -8,6 +8,7 @@
@include form-check-light;
@include light-notification;
@include light-input;
@include light-buttons;
}
@include media-breakpoint-up(sm) {

View File

@@ -173,12 +173,6 @@ body {
color: inherit;
}
.notification-body {
.btn {
@include ol-button-themed('secondary');
}
}
p {
margin-bottom: var(--spacing-03);
}
@@ -317,6 +311,11 @@ body {
z-index: auto;
}
}
.btn-link {
--link-color: var(--link-web-themed);
--link-hover-color: var(--link-web-hover-themed);
}
}
.ds-nav-icon-dropdown {