mirror of
https://github.com/gosticks/partition-filter-visualization.git
synced 2026-08-12 12:50:22 +00:00
wip: filter handling
This commit is contained in:
@@ -34,8 +34,10 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-static": "^2.0.2",
|
||||
"@types/papaparse": "^5.3.7",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"feather-icons": "^4.29.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"postcss": "^8.4.24",
|
||||
"sass": "^1.63.6",
|
||||
"svelte-feather-icons": "^4.0.1",
|
||||
|
||||
Generated
+20
@@ -1,15 +1,25 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
'@sveltejs/adapter-static':
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2(@sveltejs/kit@1.20.5)
|
||||
'@types/papaparse':
|
||||
specifier: ^5.3.7
|
||||
version: 5.3.7
|
||||
autoprefixer:
|
||||
specifier: ^10.4.14
|
||||
version: 10.4.14(postcss@8.4.24)
|
||||
feather-icons:
|
||||
specifier: ^4.29.0
|
||||
version: 4.29.0
|
||||
papaparse:
|
||||
specifier: ^5.4.1
|
||||
version: 5.4.1
|
||||
postcss:
|
||||
specifier: ^8.4.24
|
||||
version: 8.4.24
|
||||
@@ -472,6 +482,12 @@ packages:
|
||||
/@types/node@20.3.1:
|
||||
resolution: {integrity: sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==}
|
||||
|
||||
/@types/papaparse@5.3.7:
|
||||
resolution: {integrity: sha512-f2HKmlnPdCvS0WI33WtCs5GD7X1cxzzS/aduaxSu3I7TbhWlENjSPs6z5TaB9K0J+BH1jbmqTaM+ja5puis4wg==}
|
||||
dependencies:
|
||||
'@types/node': 20.3.1
|
||||
dev: false
|
||||
|
||||
/@types/pug@2.0.6:
|
||||
resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
|
||||
dev: true
|
||||
@@ -1507,6 +1523,10 @@ packages:
|
||||
p-limit: 3.1.0
|
||||
dev: true
|
||||
|
||||
/papaparse@5.4.1:
|
||||
resolution: {integrity: sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==}
|
||||
dev: false
|
||||
|
||||
/parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Smooth scroll on href */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
min-height: 100vh;
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<body data-sveltekit-preload-data="hover" class="bg-secondary-100">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+14
-27
@@ -1,50 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { Theme, appSettingsStore, toggleThemeMode, updateTheme } from '../store/SettingsStore';
|
||||
|
||||
let className: string | undefined = '';
|
||||
export { className as class };
|
||||
|
||||
const theme = writable('light'); // Initial theme
|
||||
|
||||
// Switch theme function
|
||||
function toggleTheme() {
|
||||
theme.update((currentTheme) => (currentTheme === 'light' ? 'dark' : 'light'));
|
||||
}
|
||||
let theme: Theme;
|
||||
|
||||
onMount(() => {
|
||||
// Check for previously saved theme in local storage
|
||||
// const savedTheme = localStorage.getItem('theme');
|
||||
// if (savedTheme) {
|
||||
// theme.set(savedTheme);
|
||||
// }
|
||||
appSettingsStore.subscribe((value) => {
|
||||
theme = value.theme;
|
||||
});
|
||||
});
|
||||
|
||||
$: {
|
||||
// Save the current theme to local storage
|
||||
// localStorage.setItem('theme', $theme);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="sidebar {className} {theme}">
|
||||
<div class="p-4">
|
||||
<!-- Sidebar content goes here -->
|
||||
<slot />
|
||||
<div>
|
||||
<!-- Sidebar content goes here -->
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mt-4">
|
||||
<label for="themeToggle" class="flex items-center cursor-pointer">
|
||||
<span class="mr-2">Dark Mode</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="themeToggle"
|
||||
class="hidden"
|
||||
on:change={toggleTheme}
|
||||
checked={$theme === 'dark'}
|
||||
/>
|
||||
<input type="checkbox" id="themeToggle" class="hidden" on:change={toggleThemeMode} />
|
||||
<span class="relative inline-block w-10 h-6 transition bg-gray-300 rounded-full">
|
||||
<span
|
||||
class="absolute top-0 left-0 w-6 h-6 transition transform bg-white rounded-full shadow-md"
|
||||
style="transform: translateX({$theme === 'dark' ? 'calc(100% - 0.75rem)' : '0'})"
|
||||
style="transform: translateX({$appSettingsStore.theme === Theme.Dark
|
||||
? 'calc(100% - 0.75rem)'
|
||||
: '0'})"
|
||||
/>
|
||||
</span>
|
||||
<span class="ml-2">Light Mode</span>
|
||||
@@ -55,8 +42,8 @@
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
width: 200px;
|
||||
height: 100vh;
|
||||
overflow-y: scroll;
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import type { Action } from '@sveltejs/kit';
|
||||
|
||||
export let size: 'sm' | 'md' | 'lg' = 'md';
|
||||
export let disabled: boolean = false;
|
||||
export let color: 'primary' | 'secondary' = 'secondary';
|
||||
|
||||
let className: string | undefined = undefined;
|
||||
|
||||
export { className as class };
|
||||
|
||||
function classForSize() {
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
return 'px-2.5 py-1.5 text-xs';
|
||||
case 'md':
|
||||
return 'px-4 py-2 text-sm';
|
||||
case 'lg':
|
||||
return 'px-4 py-2 text-base';
|
||||
}
|
||||
}
|
||||
|
||||
function classForColors(): string {
|
||||
switch (color) {
|
||||
case 'primary':
|
||||
return 'text-white bg-primary-600 hover:bg-primary-700 ';
|
||||
case 'secondary':
|
||||
return 'text-secondary-600 bg-white hover:bg-secondary-50 hover:bg-gray-50';
|
||||
}
|
||||
}
|
||||
|
||||
function classForDisabledState(): string {
|
||||
if (disabled) {
|
||||
return 'cursor-not-allowed opacity-50 pointer-events-none';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
{disabled}
|
||||
on:click
|
||||
class="inline-flex justify-between items-center {classForDisabledState()} {classForColors()} {classForSize()} border rounded-xl shadow-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 {className ??
|
||||
''}"
|
||||
>
|
||||
<slot name="leading" />
|
||||
<slot />
|
||||
<slot name="trailing" />
|
||||
</button>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import Button from './Button.svelte';
|
||||
import { ChevronDownIcon, ChevronUpIcon } from 'svelte-feather-icons';
|
||||
|
||||
export let isOpen: boolean = false;
|
||||
export let disabled: boolean = false;
|
||||
export let buttonClass: string | undefined = undefined;
|
||||
|
||||
let className: string | undefined = undefined;
|
||||
export { className as class };
|
||||
|
||||
let popoverElement: HTMLDivElement;
|
||||
|
||||
function toggleDropdown() {
|
||||
isOpen = !isOpen;
|
||||
}
|
||||
|
||||
interface $$Slots {
|
||||
button: {};
|
||||
content: {};
|
||||
}
|
||||
|
||||
function fadeSlide(node: HTMLElement, options?: { duration?: number }) {
|
||||
return {
|
||||
duration: options?.duration || 100,
|
||||
css: (t: number) => `
|
||||
transform: translateY(${(1 - t) * -20}px) scale(${0.9 + t * 0.1});
|
||||
opacity: ${t};
|
||||
`
|
||||
};
|
||||
}
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (popoverElement && !popoverElement.contains(event.target as Node)) {
|
||||
isOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Close the dropdown when clicking outside
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
window.addEventListener('click', handleClickOutside);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (browser) {
|
||||
window.removeEventListener('click', handleClickOutside);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="relative {className}" bind:this={popoverElement}>
|
||||
<Button
|
||||
class="flex items-center justify-between gap-2 {buttonClass}"
|
||||
on:click={toggleDropdown}
|
||||
{disabled}
|
||||
>
|
||||
<slot name="button" />
|
||||
{#if isOpen}
|
||||
<ChevronDownIcon />
|
||||
{:else}
|
||||
<ChevronUpIcon />
|
||||
{/if}
|
||||
</Button>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
transition:fadeSlide={{ duration: 100 }}
|
||||
class="z-10 overflow-hidden origin-top-left absolute left-0 mt-2 w-56 rounded-xl shadow-xl bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||
>
|
||||
<div role="none">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,119 @@
|
||||
<script lang="ts">
|
||||
import { CheckCircleIcon, CheckIcon, MailIcon, PhoneIcon } from 'svelte-feather-icons';
|
||||
import Dropdown from './Dropdown.svelte';
|
||||
import Button from './Button.svelte';
|
||||
|
||||
export let isOpen = false;
|
||||
export let singular = false;
|
||||
export let disabled = false;
|
||||
export let label: string | undefined = undefined;
|
||||
|
||||
// Data type
|
||||
type T = $$Generic;
|
||||
|
||||
export let options: { label: string; value: T }[];
|
||||
|
||||
export let selected: T[] = [];
|
||||
|
||||
// add on select action
|
||||
export let onSelect: (selected: T[]) => void | undefined;
|
||||
|
||||
let buttonText = labelForSelection();
|
||||
|
||||
function internalOnSelect(value: T) {
|
||||
if (singular) {
|
||||
selected = [value];
|
||||
onSelect?.(selected);
|
||||
|
||||
buttonText = labelForSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected.includes(value)) {
|
||||
selected = selected.filter((v) => v !== value);
|
||||
} else {
|
||||
selected = [...selected, value];
|
||||
}
|
||||
onSelect?.(selected);
|
||||
buttonText = labelForSelection();
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
selected = [];
|
||||
onSelect?.(selected);
|
||||
buttonText = labelForSelection();
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
selected = options.map((o) => o.value);
|
||||
onSelect?.(selected);
|
||||
buttonText = labelForSelection();
|
||||
}
|
||||
|
||||
// Computes the button text based on current selection
|
||||
function labelForSelection() {
|
||||
if (selected.length === 0) {
|
||||
return 'Select';
|
||||
}
|
||||
|
||||
if (singular) {
|
||||
return options.find((o) => o.value === selected[0])?.label || 'Select';
|
||||
}
|
||||
|
||||
if (options.length === selected.length) {
|
||||
return 'All selected';
|
||||
}
|
||||
|
||||
return `(${selected.length}) selected`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col">
|
||||
{#if label !== undefined}<div class="font-bold text-sm px-4 pb-1 text-secondary-500">
|
||||
{label}
|
||||
</div>{/if}
|
||||
<Dropdown buttonClass="w-full" {isOpen} {disabled}>
|
||||
<span slot="button">
|
||||
{#if $$slots.default}
|
||||
<slot />
|
||||
{:else}
|
||||
<span class="text-sm">{buttonText}</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<div slot="content">
|
||||
<ul>
|
||||
{#each options as { label, value }, i}
|
||||
{@const isSelected = selected.includes(value)}
|
||||
<li class="border-spacing-1 border-b last:border-b-0">
|
||||
<button
|
||||
on:click={() => internalOnSelect(value)}
|
||||
class="p-4 hover:bg-secondary-200 w-full text-left flex gap-4"
|
||||
>
|
||||
<div class="w-6">
|
||||
{#if singular}
|
||||
<div
|
||||
class="rounded-full op w-6 h-6 border-2 flex items-center justify-center border-secondary-900 {isSelected
|
||||
? 'opacity-100'
|
||||
: 'opacity-20'}"
|
||||
>
|
||||
<div hidden={!isSelected} class="rounded-full w-4 h-4 bg-secondary-900" />
|
||||
</div>
|
||||
{:else}
|
||||
<i hidden={!isSelected}><CheckIcon /></i>
|
||||
{/if}
|
||||
</div>
|
||||
<span class={isSelected ? 'font-bold' : ''}>{label}</span></button
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if !singular}
|
||||
<div class="border-t p-4 flex justify-end gap-2">
|
||||
<Button size="sm" color="primary" on:click={selectAll}>Select all</Button>
|
||||
<Button size="sm" on:click={clearAll}>Clear</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
@@ -1,3 +1,25 @@
|
||||
<div class="min-h-screen relative isolate bg-slate-50 pb-96">
|
||||
<slot />
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import '../app.css';
|
||||
|
||||
onMount(() => {
|
||||
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
||||
if (
|
||||
localStorage.theme === 'dark' ||
|
||||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
// Whenever the user explicitly chooses light mode
|
||||
localStorage.theme = 'light';
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen relative isolate max-h-screen max-w-full">
|
||||
<main class="p-4">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const prerender = true;
|
||||
@@ -4,6 +4,8 @@ import type { Load } from '@sveltejs/kit';
|
||||
|
||||
export type DataEntry = {
|
||||
name: string;
|
||||
fixture: string;
|
||||
filterName: string;
|
||||
infoUrl: string;
|
||||
dataUrl: string;
|
||||
};
|
||||
@@ -17,32 +19,56 @@ export type EntryDefinition = {
|
||||
// FIXME: add missing fields maybe generate even
|
||||
};
|
||||
|
||||
export type FilterEntry = {
|
||||
name: string;
|
||||
entries: DataEntry[];
|
||||
};
|
||||
|
||||
export const load: Load = async ({ params }) => {
|
||||
// Use glob to index all available data entries
|
||||
const definitions = await import.meta.glob('/static/datasets/*/*.json');
|
||||
const data = await import.meta.glob('/static/datasets/*/*.csv');
|
||||
// const data = await import.meta.glob('/static/datasets/*/*.csv');
|
||||
|
||||
const dataEntries: DataEntry[] = [];
|
||||
const filters: { [key: string]: FilterEntry } = {};
|
||||
|
||||
// Iterate over found modules
|
||||
for (const path in definitions) {
|
||||
// Simplit path into [filter]/[name] and remove extension
|
||||
const [filter, name] = path
|
||||
let [filter, name] = path
|
||||
.split('/')
|
||||
.slice(-2)
|
||||
.map((p) => p.replace('.json', ''));
|
||||
|
||||
filter = filter.replace('_part', '');
|
||||
|
||||
// load defintiion info
|
||||
const info: EntryDefinition = (await definitions[path]()) as EntryDefinition;
|
||||
|
||||
dataEntries.push({
|
||||
const entry = {
|
||||
name: info.name,
|
||||
// FIXME: just a temporary hack to get the filter name -> add this value to test output
|
||||
filterName: info.name.split(' ')[0],
|
||||
fixture: info.fixutre,
|
||||
infoUrl: `/datasets/${filter}/${name}.json`,
|
||||
dataUrl: `/datasets/${filter}/${name}.csv`
|
||||
});
|
||||
};
|
||||
|
||||
dataEntries.push(entry);
|
||||
|
||||
// Construct build time filters
|
||||
if (!filters[filter]) {
|
||||
filters[filter] = {
|
||||
name: filter,
|
||||
entries: [entry]
|
||||
};
|
||||
} else {
|
||||
filters[filter].entries.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: dataEntries
|
||||
data: dataEntries,
|
||||
filters: filters
|
||||
};
|
||||
};
|
||||
|
||||
+64
-23
@@ -1,9 +1,18 @@
|
||||
<script lang="ts">
|
||||
import Papa from 'papaparse';
|
||||
import type { ParseResult } from 'papaparse';
|
||||
import Sidebar from '../lib/Sidebar.svelte';
|
||||
import type { PageServerData } from './$types';
|
||||
import type { DataEntry, EntryDefinition } from './proxy+page.server';
|
||||
import { contenteditable_truthy_values } from 'svelte/internal';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import Dropdown from '$lib/components/Dropdown.svelte';
|
||||
import DropdownSelect from '$lib/components/DropdownSelect.svelte';
|
||||
export let data: PageServerData;
|
||||
|
||||
// TODO: either generate or manually write types for csv data
|
||||
type CsvData = object;
|
||||
|
||||
let selectedItem: {
|
||||
name: string;
|
||||
info: Promise<EntryDefinition>;
|
||||
@@ -16,41 +25,73 @@
|
||||
let entry: typeof selectedItem = {
|
||||
name: item.name,
|
||||
info: fetch(item.infoUrl).then((r) => r.json()),
|
||||
content: fetch(item.dataUrl).then((r) => r.text())
|
||||
content: fetchAndParseCSV(item.dataUrl)
|
||||
};
|
||||
|
||||
selectedItem = entry;
|
||||
}
|
||||
|
||||
async function fetchAndParseCSV(csvUrl: string) {
|
||||
const url = new URL(csvUrl, location.href);
|
||||
|
||||
console.log(url);
|
||||
const promise = new Promise<Papa.ParseResult<CsvData>>((resolve, reject) => {
|
||||
Papa.parse<CsvData>(url.href, {
|
||||
download: true,
|
||||
header: true,
|
||||
worker: true,
|
||||
dynamicTyping: true,
|
||||
skipEmptyLines: true,
|
||||
complete: function (results) {
|
||||
console.log('Finished:', results);
|
||||
results.errors.length > 0 ? reject(results.errors) : resolve(results);
|
||||
}
|
||||
});
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid">
|
||||
<Sidebar class="grid-cols-1">
|
||||
<ul>
|
||||
{#each data.data as item}
|
||||
<li>
|
||||
<button
|
||||
class:selected={item.name === selectedItem?.name}
|
||||
on:click={() => selectItem(item)}>{item.name}</button
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</Sidebar>
|
||||
<div class="grid-cols-5">
|
||||
<div class="">
|
||||
<div class="flex gap-4">
|
||||
<DropdownSelect
|
||||
label="Filters"
|
||||
onSelect={(selected) => {
|
||||
console.log(selected);
|
||||
//selectItem(selected);
|
||||
}}
|
||||
options={Object.entries(data.filters).map(([key, value]) => ({
|
||||
label: key,
|
||||
value: value
|
||||
}))}
|
||||
/>
|
||||
<DropdownSelect
|
||||
label="Number of elements"
|
||||
singular
|
||||
disabled={true}
|
||||
onSelect={(selected) => {
|
||||
console.log(selected);
|
||||
//selectItem(selected);
|
||||
}}
|
||||
options={Object.entries(data.filters).map(([key, value]) => ({
|
||||
label: key,
|
||||
value: value
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div class="border-t-2 mt-6">
|
||||
{#if selectedItem}
|
||||
<h2>{selectedItem.name}</h2>
|
||||
{#await selectedItem.info}
|
||||
<div>loading...</div>
|
||||
{:then info}
|
||||
<pre>{JSON.stringify(info)}</pre>
|
||||
{/await}
|
||||
{#await selectedItem.content}
|
||||
<div>loading...</div>
|
||||
{:then content}
|
||||
<pre>{content}</pre>
|
||||
<pre>{JSON.stringify(content.data)}</pre>
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.selected {
|
||||
color: hotpink;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export enum Theme {
|
||||
Light = 'light',
|
||||
Dark = 'dark'
|
||||
}
|
||||
|
||||
/**
|
||||
* Store for all application settings
|
||||
*/
|
||||
interface AppSettings {
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const initialAppSettings: AppSettings = {
|
||||
theme: Theme.Light
|
||||
};
|
||||
|
||||
export const appSettingsStore: Writable<AppSettings> = writable(initialAppSettings);
|
||||
|
||||
export const updateTheme = (theme: Theme) => {
|
||||
appSettingsStore.update((settings) => {
|
||||
return { ...settings, theme };
|
||||
});
|
||||
};
|
||||
|
||||
export const toggleThemeMode = () => {
|
||||
console.log('toggleThemeMode', appSettingsStore);
|
||||
updateTheme(appSettingsStore.theme === Theme.Dark ? Theme.Light : Theme.Dark);
|
||||
};
|
||||
+11
-1
@@ -1,12 +1,22 @@
|
||||
const colors = require('tailwindcss/colors');
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
fontFamily: {
|
||||
display: ['Source Serif Pro', 'Georgia', 'serif'],
|
||||
body: ['Manrope', 'system-ui', 'sans-serif']
|
||||
},
|
||||
extend: {}
|
||||
|
||||
extend: {
|
||||
colors: {
|
||||
primary: colors.blue,
|
||||
secondary: colors.gray,
|
||||
accent: colors.orange
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user