feat: add Appearance panel to Settings modal
This features two knobs: theming and indent. I was curious whether I could actually hand the theme switching to the parent, so I created an event dispatcher for it. It works! But I had to hack the Svelte component a little. I also need to sync it two the theme switcher in the popup and go over every component that is focused on light mode only at the moment. To limit the work I will constraint myself to the Profile page here. If the theme is meant to apply to other pages as well, we will need to persist the setting. That means a database in Anvil, I guess. I'm not willing to take that step right now. It must come at some point (e.g. when talking to Vervis to actually inform about changes made in Anvil). Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
This commit is contained in:
parent
b5fed52bc5
commit
5af877bcaa
11 changed files with 514 additions and 262 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "anvil",
|
"name": "anvil",
|
||||||
"version": "0.0.12",
|
"version": "0.0.13",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "anvil",
|
"name": "anvil",
|
||||||
"version": "0.0.12",
|
"version": "0.0.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/dom": "1.6.8",
|
"@floating-ui/dom": "1.6.8",
|
||||||
"@fontsource/spline-sans-mono": "5.0.20",
|
"@fontsource/spline-sans-mono": "5.0.20",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "anvil",
|
"name": "anvil",
|
||||||
"version": "0.0.12",
|
"version": "0.0.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|
|
@ -13,15 +13,34 @@ You should have received a copy of the GNU Affero General Public License along w
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import SettingsAccount from './SettingsAccount.svelte';
|
import SettingsAccount from './SettingsAccount.svelte';
|
||||||
|
import SettingsAppearance from './SettingsAppearance.svelte';
|
||||||
import SettingsKeys from './SettingsKeys.svelte';
|
import SettingsKeys from './SettingsKeys.svelte';
|
||||||
import SettingsSidebar from '../atoms/SettingsSidebar.svelte';
|
import SettingsSidebar from '../atoms/SettingsSidebar.svelte';
|
||||||
import SettingsProfile from './SettingsProfile.svelte';
|
import SettingsProfile from './SettingsProfile.svelte';
|
||||||
|
|
||||||
let activeSetting = 'profile';
|
let activeSetting = 'profile';
|
||||||
|
let activeTheme = 'auto';
|
||||||
|
|
||||||
function onTileChanged(ev) {
|
function onTileChanged(ev) {
|
||||||
activeSetting = ev.detail;
|
activeSetting = ev.detail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onThemeSwitched(ev) {
|
||||||
|
activeTheme = ev.detail;
|
||||||
|
// This is a hack.
|
||||||
|
// Skeleton relies on Cookies and a hook:
|
||||||
|
// https://github.com/skeletonlabs/skeleton/blob/%40skeletonlabs/skeleton%402.10.2/sites/skeleton.dev/src/hooks.server.ts#L16
|
||||||
|
// Svelte itself features a svelte:document special element that must
|
||||||
|
// only be used at the top level - except SvelteKit has app.html
|
||||||
|
// as a static file :[]
|
||||||
|
// Ideas welcome!
|
||||||
|
if (window?.document?.documentElement) {
|
||||||
|
window.document.documentElement.classList.remove('auto');
|
||||||
|
window.document.documentElement.classList.remove('dark');
|
||||||
|
window.document.documentElement.classList.remove('light');
|
||||||
|
window.document.documentElement.classList.add(activeTheme);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex bg-surface-50 min-h-[900px]">
|
<div class="flex bg-surface-50 min-h-[900px]">
|
||||||
|
@ -33,5 +52,7 @@ You should have received a copy of the GNU Affero General Public License along w
|
||||||
<SettingsAccount />
|
<SettingsAccount />
|
||||||
{:else if activeSetting === 'ssh_gpg_keys'}
|
{:else if activeSetting === 'ssh_gpg_keys'}
|
||||||
<SettingsKeys />
|
<SettingsKeys />
|
||||||
|
{:else if activeSetting === 'appearance'}
|
||||||
|
<SettingsAppearance on:switch-theme={onThemeSwitched} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
68
src/lib/components/molecules/SettingsAppearance.svelte
Normal file
68
src/lib/components/molecules/SettingsAppearance.svelte
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<!--
|
||||||
|
SettingsAppearance molecule.
|
||||||
|
Copyright (C) 2024 André Jaenisch
|
||||||
|
SPDX-FileCopyrightText: 2024 André Jaenisch
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
|
const dispatcher = createEventDispatcher();
|
||||||
|
|
||||||
|
function onThemeSwitched(ev) {
|
||||||
|
dispatcher('switch-theme', ev.target.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col flex-1 gap-6 pe-8 pb-8 ps-8 pt-20 bg-surface-100-800-token">
|
||||||
|
<div class="text-surface-500-300-token text-xl font-semibold">
|
||||||
|
{$_('settings.appearance.headline')}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="text-surface-500-300-token font-semibold py-1">
|
||||||
|
{$_('settings.appearance.theme.headline')}
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-8">
|
||||||
|
<label class="label flex items-center space-x-2">
|
||||||
|
<input type="radio" class="me-2" name="theme" value="light" on:change={onThemeSwitched} />
|
||||||
|
{$_('settings.appearance.theme.light')}
|
||||||
|
</label>
|
||||||
|
<label class="label flex items-center space-x-2">
|
||||||
|
<input type="radio" class="me-2" name="theme" value="dark" on:change={onThemeSwitched} />
|
||||||
|
{$_('settings.appearance.theme.dark')}
|
||||||
|
</label>
|
||||||
|
<label class="label flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="me-2"
|
||||||
|
name="theme"
|
||||||
|
value="auto"
|
||||||
|
checked
|
||||||
|
on:change={onThemeSwitched}
|
||||||
|
/>
|
||||||
|
{$_('settings.appearance.theme.auto')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="text-surface-500-300-token font-semibold py-1">
|
||||||
|
{$_('settings.appearance.tab_indent.headline')}
|
||||||
|
</div>
|
||||||
|
<p class="text-sm">{$_('settings.appearance.tab_indent.intro')}</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input bg-white text-center text-surface-500-300-token max-w-16"
|
||||||
|
name="tab_indent"
|
||||||
|
value="4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -192,7 +192,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"label": ""
|
"headline": "",
|
||||||
|
"label": "",
|
||||||
|
"tab_indent": {
|
||||||
|
"headline": "",
|
||||||
|
"intro": ""
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"auto": "",
|
||||||
|
"dark": "",
|
||||||
|
"headline": "",
|
||||||
|
"light": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"label": ""
|
"label": ""
|
||||||
|
|
|
@ -192,7 +192,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"label": ""
|
"headline": "",
|
||||||
|
"label": "",
|
||||||
|
"tab_indent": {
|
||||||
|
"headline": "",
|
||||||
|
"intro": ""
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"auto": "",
|
||||||
|
"dark": "",
|
||||||
|
"headline": "",
|
||||||
|
"light": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"label": ""
|
"label": ""
|
||||||
|
|
|
@ -192,7 +192,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"label": "Appearance"
|
"headline": "Appearance",
|
||||||
|
"label": "Appearance",
|
||||||
|
"tab_indent": {
|
||||||
|
"headline": "Tab indenting",
|
||||||
|
"intro": "Number of spaces per tab in code view."
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"auto": "Auto",
|
||||||
|
"dark": "Dark",
|
||||||
|
"headline": "Theme",
|
||||||
|
"light": "Light"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"label": "Notifications"
|
"label": "Notifications"
|
||||||
|
|
|
@ -192,7 +192,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"label": ""
|
"headline": "",
|
||||||
|
"label": "",
|
||||||
|
"tab_indent": {
|
||||||
|
"headline": "",
|
||||||
|
"intro": ""
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"auto": "",
|
||||||
|
"dark": "",
|
||||||
|
"headline": "",
|
||||||
|
"light": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"label": ""
|
"label": ""
|
||||||
|
|
|
@ -192,7 +192,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"label": ""
|
"headline": "",
|
||||||
|
"label": "",
|
||||||
|
"tab_indent": {
|
||||||
|
"headline": "",
|
||||||
|
"intro": ""
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"auto": "",
|
||||||
|
"dark": "",
|
||||||
|
"headline": "",
|
||||||
|
"light": ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"label": ""
|
"label": ""
|
||||||
|
|
26
stories/molecules/SettingsAppearance.stories.ts
Normal file
26
stories/molecules/SettingsAppearance.stories.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/* Stories for SettingsAppearance molecule.
|
||||||
|
* Copyright (C) 2024 André Jaenisch
|
||||||
|
* SPDX-FileCopyrightText: 2024 André Jaenisch
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Meta, StoryObj } from '@storybook/svelte';
|
||||||
|
|
||||||
|
import SettingsAppearance from '$lib/components/molecules/SettingsAppearance.svelte';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Molecules/SettingsAppearance',
|
||||||
|
component: SettingsAppearance,
|
||||||
|
tags: ['autodocs']
|
||||||
|
} satisfies Meta<SettingsAppearance>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Plain: Story = {};
|
82
tests/components/molecules/SettingsAppearance.test.ts
Normal file
82
tests/components/molecules/SettingsAppearance.test.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
/* Component test for SettingsAppearance molecule.
|
||||||
|
* Copyright (C) 2024 André Jaenisch
|
||||||
|
* SPDX-FileCopyrightText: 2024 André Jaenisch
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { render, screen } from '@testing-library/svelte';
|
||||||
|
import { init, locale, register } from 'svelte-i18n';
|
||||||
|
|
||||||
|
import SettingsAppearance from '../../../src/lib/components/molecules/SettingsAppearance.svelte';
|
||||||
|
import enMessages from '../../../src/lib/i18n/locales/en.json';
|
||||||
|
|
||||||
|
describe('SettingsAppearance.svelte', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
register('en', () => import('../../../src/lib/i18n/locales/en.json'));
|
||||||
|
init({ fallbackLocale: 'en', initialLocale: 'en' });
|
||||||
|
locale.set('en');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mount', () => {
|
||||||
|
// Arrange
|
||||||
|
// Nothing to prepare
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { container } = render(SettingsAppearance);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(container).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have a radio group for themes', () => {
|
||||||
|
// Arrange
|
||||||
|
// Nothing to prepare
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(SettingsAppearance);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByLabelText(enMessages.settings.appearance.theme.light)).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText(enMessages.settings.appearance.theme.dark)).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText(enMessages.settings.appearance.theme.auto)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when clicking a theme', () => {
|
||||||
|
it('should dispatch a switch-theme event', () => {
|
||||||
|
// Arrange
|
||||||
|
const listener = vi.fn();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { component } = render(SettingsAppearance);
|
||||||
|
component.$on('switch-theme', listener);
|
||||||
|
const darkTheme = screen.getByLabelText(enMessages.settings.appearance.theme.dark);
|
||||||
|
darkTheme.click();
|
||||||
|
const ev = new CustomEvent({ detail: 'dark' });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(listener).toHaveBeenCalledOnce();
|
||||||
|
expect(listener).toHaveBeenCalledWith(ev);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Mark up proper label and input
|
||||||
|
it.skip('should have a tab indent input', () => {
|
||||||
|
// Arrange
|
||||||
|
// Nothing to prepare
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(SettingsAppearance);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(
|
||||||
|
screen.getByPlaceholderText(enMessages.settings.appearance.tab_indent.label)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue