Skip to content

Commit d831ccc

Browse files
authored
[DT-2854] move dark mode button from side nav to top nav (#2720)
* add useDarkModePreference store, make existing useDarkMode derived * add dark mode button component * add dark mode button to top nav * add DarkModeButton to bottom nav * remove dark mode button from side nav * enhance dark mode utilities with preference type and next preference function * separate icon button and nav button for bottom nav * add some tests for the dark-mode file * use original default value from .env * use svelte5, tidy up * add system-window icon * use system-window icon for dark mode buttons * tweak side nav slot "bottom" to be more like original * change let to const in dark-mode buttons
1 parent dbfb0c9 commit d831ccc

11 files changed

Lines changed: 226 additions & 21 deletions

File tree

src/lib/components/bottom-nav-settings.svelte

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<script lang="ts">
22
import { onDestroy } from 'svelte';
33
4+
import DarkModeNavigationButton from '$lib/components/dark-mode-navigation-button.svelte';
45
import DataEncoderSettings from '$lib/components/data-encoder-settings.svelte';
56
import TimezoneSelect from '$lib/components/timezone-select.svelte';
67
import NavigationButton from '$lib/holocene/navigation/navigation-button.svelte';
78
import { translate } from '$lib/i18n/translate';
89
import { dataEncoder } from '$lib/stores/data-encoder';
9-
import { useDarkMode } from '$lib/utilities/dark-mode';
1010
1111
import { viewDataEncoderSettings } from './data-encoder-settings.svelte';
1212
@@ -43,14 +43,7 @@
4343
/>
4444
<DataEncoderSettings />
4545
<div class="border-b border-subtle"></div>
46-
<NavigationButton
47-
onClick={() => ($useDarkMode = !$useDarkMode)}
48-
tooltip={$useDarkMode
49-
? translate('common.night')
50-
: translate('common.day')}
51-
label={$useDarkMode ? translate('common.night') : translate('common.day')}
52-
icon={$useDarkMode ? 'moon' : 'sun'}
53-
/>
46+
<DarkModeNavigationButton />
5447
<slot />
5548
</div>
5649
{/if}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<script lang="ts">
2+
import type { IconName } from '$lib/holocene/icon';
3+
import IconButton from '$lib/holocene/icon-button.svelte';
4+
import Tooltip from '$lib/holocene/tooltip.svelte';
5+
import { translate } from '$lib/i18n/translate';
6+
import {
7+
getNextDarkModePreference,
8+
useDarkModePreference,
9+
} from '$lib/utilities/dark-mode';
10+
11+
const buttonText = $derived(
12+
$useDarkModePreference == null
13+
? translate('common.system-default')
14+
: $useDarkModePreference
15+
? translate('common.night')
16+
: translate('common.day'),
17+
);
18+
19+
const buttonIcon: IconName = $derived(
20+
$useDarkModePreference == null
21+
? 'system-window'
22+
: $useDarkModePreference
23+
? 'moon'
24+
: 'sun',
25+
);
26+
27+
function cycleDarkModePreference() {
28+
$useDarkModePreference = getNextDarkModePreference($useDarkModePreference);
29+
}
30+
</script>
31+
32+
<Tooltip bottomRight text={buttonText}>
33+
<IconButton
34+
variant="ghost"
35+
label={buttonText}
36+
icon={buttonIcon}
37+
on:click={cycleDarkModePreference}
38+
/>
39+
</Tooltip>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<script lang="ts">
2+
import type { IconName } from '$lib/holocene/icon';
3+
import NavigationButton from '$lib/holocene/navigation/navigation-button.svelte';
4+
import { translate } from '$lib/i18n/translate';
5+
import {
6+
getNextDarkModePreference,
7+
useDarkModePreference,
8+
} from '$lib/utilities/dark-mode';
9+
10+
const buttonText = $derived(
11+
$useDarkModePreference == null
12+
? translate('common.system-default')
13+
: $useDarkModePreference
14+
? translate('common.night')
15+
: translate('common.day'),
16+
);
17+
18+
const buttonIcon: IconName = $derived(
19+
$useDarkModePreference == null
20+
? 'system-window'
21+
: $useDarkModePreference
22+
? 'moon'
23+
: 'sun',
24+
);
25+
26+
function cycleDarkModePreference() {
27+
$useDarkModePreference = getNextDarkModePreference($useDarkModePreference);
28+
}
29+
</script>
30+
31+
<NavigationButton
32+
onClick={cycleDarkModePreference}
33+
tooltip={buttonText}
34+
label={buttonText}
35+
icon={buttonIcon}
36+
/>

src/lib/components/side-nav.svelte

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
<script lang="ts">
2-
import NavigationButton from '$lib/holocene/navigation/navigation-button.svelte';
32
import Navigation from '$lib/holocene/navigation/navigation-container.svelte';
43
import NavigationItem from '$lib/holocene/navigation/navigation-item.svelte';
54
import { translate } from '$lib/i18n/translate';
65
import type { NavLinkListItem } from '$lib/types/global';
7-
import { useDarkMode } from '$lib/utilities/dark-mode';
86
97
export let isCloud = false;
108
export let linkList: NavLinkListItem[];
@@ -28,14 +26,6 @@
2826
{/if}
2927
{/each}
3028
<svelte:fragment slot="bottom">
31-
<NavigationButton
32-
onClick={() => ($useDarkMode = !$useDarkMode)}
33-
tooltip={$useDarkMode
34-
? translate('common.night')
35-
: translate('common.day')}
36-
label={$useDarkMode ? translate('common.night') : translate('common.day')}
37-
icon={$useDarkMode ? 'moon' : 'sun'}
38-
/>
3929
<slot name="bottom" />
4030
</svelte:fragment>
4131
</Navigation>

src/lib/components/top-nav.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script lang="ts">
2+
import DarkModeIconButton from '$lib/components/dark-mode-icon-button.svelte';
23
import DataEncoderStatus from '$lib/components/data-encoder-status.svelte';
34
import TimezoneSelect from '$lib/components/timezone-select.svelte';
45
import { translate } from '$lib/i18n/translate';
@@ -18,6 +19,7 @@
1819
<div class="flex items-center gap-2">
1920
<TimezoneSelect position={screenWidth < 768 ? 'left' : 'right'} />
2021
<DataEncoderStatus />
22+
<DarkModeIconButton />
2123
<slot />
2224
</div>
2325
</nav>

src/lib/holocene/icon/paths.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import starFilled from './svg/star-filled.svelte';
102102
import summary from './svg/summary.svelte';
103103
import sun from './svg/sun.svelte';
104104
import support from './svg/support.svelte';
105+
import systemWindow from './svg/system-window.svelte';
105106
import table from './svg/table.svelte';
106107
import target from './svg/target.svelte';
107108
import temporalLogo from './svg/temporal-logo.svelte';
@@ -246,6 +247,7 @@ export const icons = {
246247
usage,
247248
'vertical-ellipsis': verticalEllipsis,
248249
warning,
250+
'system-window': systemWindow,
249251
workflow,
250252
'xmark-filled': xmarkFilled,
251253
} as const;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<script lang="ts">
2+
import Svg from '../svg.svelte';
3+
</script>
4+
5+
<Svg {...$$props}>
6+
<g
7+
clip-path="url(#clip0_21712_726)"
8+
id="g1"
9+
transform="matrix(1.5,0,0,1.5,-6,-6)"
10+
>
11+
<path
12+
d="M 20,5 H 4 v 4 1.5 0.5 6.5 1.5 h 1.5 13 1.5 V 17.5 11 10.5 9 Z M 5.5,11 h 13 v 6.5 H 5.5 Z M 16,7 h 2 V 9 H 16 Z M 15,7 V 9 H 13 V 7 Z m -5,0 h 2 v 2 h -2 z"
13+
fill="currentColor"
14+
id="path1"
15+
/>
16+
</g>
17+
<defs id="defs1">
18+
<clipPath id="clip0_21712_726">
19+
<rect
20+
width="16"
21+
height="14"
22+
fill="currentColor"
23+
transform="translate(4,5)"
24+
id="rect1"
25+
x="0"
26+
y="0"
27+
/>
28+
</clipPath>
29+
</defs>
30+
</Svg>

src/lib/i18n/locales/en/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export const Strings = {
169169
'execution-details': 'Execution Details',
170170
day: 'Day',
171171
night: 'Night',
172+
'system-default': 'System Default',
172173
docs: 'Docs',
173174
'upload-error': 'Error uploading file',
174175
description: 'Description',
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { get } from 'svelte/store';
2+
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4+
5+
import {
6+
darkMode,
7+
getNextDarkModePreference,
8+
useDarkMode,
9+
useDarkModePreference,
10+
} from './dark-mode';
11+
12+
describe('dark-mode utilities', () => {
13+
let matchMediaMock;
14+
15+
beforeEach(() => {
16+
matchMediaMock = vi.fn();
17+
Object.defineProperty(window, 'matchMedia', {
18+
writable: true,
19+
value: matchMediaMock,
20+
});
21+
});
22+
23+
afterEach(() => {
24+
vi.restoreAllMocks();
25+
});
26+
27+
describe('useDarkMode', () => {
28+
it('should return true if prefers-color-scheme is dark and preference is null', () => {
29+
matchMediaMock.mockReturnValue({ matches: true }); // prefers dark
30+
useDarkModePreference.set(null);
31+
const value = get(useDarkMode);
32+
expect(value).toBe(true);
33+
});
34+
35+
it('should return false if prefers-color-scheme is not dark and preference is null', () => {
36+
matchMediaMock.mockReturnValue({ matches: false });
37+
useDarkModePreference.set(null);
38+
const value = get(useDarkMode);
39+
expect(value).toBe(false);
40+
});
41+
42+
it('should return the user preference if it is set', () => {
43+
useDarkModePreference.set(true);
44+
const value1 = get(useDarkMode);
45+
expect(value1).toBe(true);
46+
47+
useDarkModePreference.set(false);
48+
const value2 = get(useDarkMode);
49+
expect(value2).toBe(false);
50+
});
51+
});
52+
53+
describe('getNextDarkModePreference', () => {
54+
it('should return true if the current value is null', () => {
55+
expect(getNextDarkModePreference(null)).toBe(true);
56+
});
57+
58+
it('should return false if the current value is true', () => {
59+
expect(getNextDarkModePreference(true)).toBe(false);
60+
});
61+
62+
it('should return null if the current value is false', () => {
63+
expect(getNextDarkModePreference(false)).toBe(null);
64+
});
65+
});
66+
67+
describe('darkMode', () => {
68+
it('should set data-theme to "dark" when dark mode is enabled', async () => {
69+
const node = document.createElement('div');
70+
useDarkModePreference.set(true);
71+
72+
darkMode(node);
73+
await new Promise((resolve) => setTimeout(resolve, 0));
74+
75+
expect(node.dataset.theme).toBe('dark');
76+
});
77+
78+
it('should set data-theme to "light" when dark mode is disabled', async () => {
79+
const node = document.createElement('div');
80+
useDarkModePreference.set(false);
81+
82+
darkMode(node);
83+
await new Promise((resolve) => setTimeout(resolve, 0));
84+
85+
expect(node.dataset.theme).toBe('light');
86+
});
87+
});
88+
});

src/lib/utilities/dark-mode/dark-mode.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,31 @@
1+
import { derived } from 'svelte/store';
2+
13
import { persistStore } from '$lib/stores/persist-store';
24

3-
export const useDarkMode = persistStore(
5+
type DarkModePreference = boolean | null;
6+
7+
export const useDarkModePreference = persistStore<DarkModePreference>(
48
'dark mode',
59
!!import.meta.env.VITE_DARK_MODE,
610
true,
711
);
812

13+
export const useDarkMode = derived(
14+
useDarkModePreference,
15+
($useDarkModePreference) => {
16+
if ($useDarkModePreference == null) {
17+
return (
18+
window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ?? false
19+
);
20+
} else {
21+
return $useDarkModePreference;
22+
}
23+
},
24+
);
25+
26+
export const getNextDarkModePreference = (value: DarkModePreference) =>
27+
value == null ? true : value == true ? false : null;
28+
929
export const darkMode = (node: HTMLElement) => {
1030
useDarkMode.subscribe((value) => {
1131
if (value) {

0 commit comments

Comments
 (0)