Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dark mode toggle, modularization into Svelte components, filter "and/or" mode #295

Merged
merged 12 commits into from
Jul 25, 2023
Merged
13 changes: 4 additions & 9 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,10 @@ details:not([open]) > summary svg {
outline: auto;
}

/* user prefered / OS colour scheme for tag colors */
@media (prefers-color-scheme: light) {
.tag-color {
background-color: var(--tag-color) !important;
}
.tag-color {
background-color: var(--tag-color) !important;
}

@media (prefers-color-scheme: dark) {
.tag-color {
background-color: var(--tag-color-dark) !important;
}
.dark .tag-color {
background-color: var(--tag-color-dark) !important;
}
13 changes: 13 additions & 0 deletions src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%

<script>
// 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");
}
</script>
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
Expand Down
70 changes: 70 additions & 0 deletions src/lib/components/ButtonLinks.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<script lang="ts">
// DESIGN
export let version: "hollow" | "filled" = "hollow";
export let color: "green" | "red" = "green";
export let isCircle: boolean = false;
export let extraClasses: string = "";

// button el
export let disabled: boolean = false;
export let type: "button" | "reset" | "submit" = "button";

// anchor el
export let link: boolean = false;
export let url: string = "#";
export let newTab: boolean = false;
export let download: boolean = false;

let target: "_blank" | undefined;
let rel: string | undefined;

if (newTab) {
target = "_blank";
rel = "noreferrer";
}

let designClasses: string = isCircle ? "rounded-full" : "rounded-lg";

const designClassesMap = {
hollow: {
green: "border-green-500 dark:border-green-700 text-green-700 dark:text-green-500 hover:text-black hover:bg-green-500 dark:hover:text-white dark:hover:bg-green-900",
red: "border-red-500 dark:border-red-700 text-red-500 dark:text-red-400 hover:bg-red-500 hover:text-black dark:hover:text-white dark:hover:bg-red-900",
},
filled: {
green: "bg-green-700 text-white dark:bg-green-900/75 border-green-700 hover:border-green-500 dark:hover:border-green-700 dark:border-green-900/75",
red: "bg-red-700 text-white dark:bg-red-900/75 border-red-700 hover:border-red-500 dark:hover:border-red-700 dark:border-red-900/75",
},
};
designClasses = `${designClasses} ${designClassesMap[version][color]}`;
</script>

{#if link}
<a
class="btn-base {designClasses} {extraClasses}"
href={url}
{target}
{rel}
{download}
><slot name="icon" />
<slot name="label" />
{#if newTab}<span class="sr-only">in a new tab</span>{/if}
</a>
{:else}
<button
{type}
{disabled}
on:click
class="btn-base {designClasses} {extraClasses} disabled:text-zinc-400 disabled:border-current disabled:bg-zinc-200 dark:disabled:bg-zinc-700 disabled:hover:text-zinc-400 disabled:cursor-not-allowed"
>
<slot name="icon" />
<slot name="label" />
</button>
{/if}

<style>
.btn-base {
@apply inline-flex items-center gap-1;
@apply border-2 p-2;
@apply transition-colors;
}
</style>
28 changes: 28 additions & 0 deletions src/lib/components/Checkbox.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts">
const removeWhitespace = (str: string) => {
return str.replace(/\W+/g, "");
};

export let name: string;
export let id: string | undefined = undefined;
export let checked: boolean;

let checkboxId: string = id ? id : removeWhitespace(name);
</script>

<label
class="cursor-pointer py-2 px-3 rounded-full flex items-center gap-2 text-sm"
for={checkboxId}
>
<input
type="checkbox"
class="appearance-none cursor-pointer w-6 h-6 rounded-full bg-white dark:bg-black checked:bg-zinc-800 dark:checked:bg-green-600 transition duration-200"
bind:checked
id={checkboxId}
name={checkboxId}
/>
<span>
{name}
<slot />
</span>
</label>
45 changes: 45 additions & 0 deletions src/lib/components/DarkModeControl.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script lang="ts">
import { browser } from "$app/environment";
export let cssClass: string;
let darkMode: boolean;
$: darkMode = browser
? localStorage.theme === "dark" ||
(!("theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
: false;

const setColorMode = () => {
if (darkMode) {
document.documentElement.classList.remove("dark");
darkMode = false;
if (browser) {
localStorage.theme = "light";
}
} else {
document.documentElement.classList.add("dark");
darkMode = true;
if (browser) {
localStorage.theme = "dark";
}
}
return;
};
</script>

<button type="button" role="switch" aria-checked={darkMode} class={cssClass} on:click={setColorMode}>
{#if darkMode}
<!-- sun-fill icon -->
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-sun-fill" viewBox="0 0 16 16">
<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/>
</svg>
<span class="sr-only">Light</span>
{:else}
<!-- moon-stars-fill icon -->
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-moon-stars-fill" viewBox="0 0 16 16">
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/>
<path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"/>
</svg>
<span class="sr-only">Dark</span>
{/if}
<span class="sr-only">&nbsp;Colour Scheme Mode</span>
</button>
142 changes: 142 additions & 0 deletions src/lib/components/FilterForm.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { FilterOption, FilterLogic, CustomFilterEvent } from "$lib/interfaces";
import Collapsible from "$lib/components/Collapsible.svelte";
import TagWrapper from "$lib/components/TagWrapper.svelte";
import Checkbox from "$lib/components/Checkbox.svelte";
import ButtonLinks from "$lib/components/ButtonLinks.svelte";
const dispatch = createEventDispatcher<CustomFilterEvent>();
let form: HTMLFormElement;

export let filterOptions: FilterOption[];

export let showFilterLogic: boolean = true;
// Whether all the selected tags must match the resource (vs any of the selected tags)
export let filterLogicAnd: boolean = true;
let filterLogic: FilterLogic
$: filterLogic = filterLogicAnd ? "and" : "or";

let isFilterDirty: boolean;
$: isFilterDirty = filterOptions.some(
(option: FilterOption) => option.active === true
);

const resetFilters = () => {
filterOptions.forEach(option => option.active = false)
dispatch("filter", {filterOptions, filterLogic})
}

const onSubmit = () => {
dispatch("filter", {filterOptions, filterLogic})
}
</script>

{#if filterOptions}
VeckoTheGecko marked this conversation as resolved.
Show resolved Hide resolved
<Collapsible label="Filter">
<form
bind:this={form}
on:submit|preventDefault={onSubmit}
class="p-4 space-y-4"
>
<div class="flex flex-row flex-wrap gap-2">
{#each filterOptions as filterOption}
<!-- checkboxes -->
<TagWrapper
tagColor={filterOption.color}
extraClasses="input-wrapper-focus flex justify-between gap-2"
>
<Checkbox
name={filterOption.name}
bind:checked={filterOption.active}
>
<span
class="text-zinc-700 dark:text-zinc-300 italic"
>({filterOption.count})</span
>
</Checkbox>
</TagWrapper>
{/each}
</div>
<div class="flex gap-2 justify-end">
{#if showFilterLogic}
VeckoTheGecko marked this conversation as resolved.
Show resolved Hide resolved
<label aria-label="filter with and / or"
for="switch"
class="inline-flex items-center rounded-md cursor-pointer outline-2 outline-offset-1 focus-within:outline text-white border-2 border-green-700 dark:border-green-900/75"
>
<input
bind:checked={filterLogicAnd}
aria-checked={filterLogicAnd}
id="switch"
role="switch"
type="checkbox"
class="opacity-0 absolute peer"
/>
<span
class="px-4 py-3 rounded-l-sm
bg-white text-zinc-500 dark:text-zinc-400 dark:bg-zinc-800 peer-checked:bg-green-700 peer-checked:text-white dark:peer-checked:bg-green-900/75"
>
<!-- intersect icon -->
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-intersect" viewBox="0 0 16 16">
<path d="M0 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2H2a2 2 0 0 1-2-2V2zm5 10v2a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-2v5a2 2 0 0 1-2 2H5zm6-8V2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2V6a2 2 0 0 1 2-2h5z"/>
</svg><span class="sr-only">and</span></span
>
<span
class="px-4 py-3 rounded-r-sm
bg-green-700 dark:bg-green-900/75
peer-checked:text-zinc-500 peer-checked:bg-white dark:peer-checked:bg-zinc-800 dark:peer-checked:text-zinc-400"
>
<!-- union icon -->

<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-union" viewBox="0 0 16 16">
<path d="M0 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2H2a2 2 0 0 1-2-2V2z"/>
</svg><span class="sr-only">or</span></span
>
</label>
{/if}
<ButtonLinks
type="reset"
on:click={resetFilters}
disabled={!isFilterDirty}
version="filled"
color="green"
>
<svg
slot="icon"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6 inline"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
<span slot="label">Clear All</span>
</ButtonLinks>

<ButtonLinks type="submit" version="filled" color="green">
<svg
slot="icon"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6 inline"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z"
/>
</svg>
<span slot="label">Filter</span>
</ButtonLinks>
</div>
</form>
</Collapsible>
{/if}
Loading
Loading