Merge pull request #1 from BradNut/headless-ui

Headless UI
This commit is contained in:
Bradley Shellnut 2022-07-07 19:38:01 +00:00 committed by GitHub
commit eda90dbeb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 2637 additions and 8728 deletions

13
.eslintignore Normal file
View file

@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View file

@ -1,20 +1,20 @@
module.exports = { module.exports = {
root: true, root: true,
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['svelte3', '@typescript-eslint'], plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'], ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: { settings: {
'svelte3/typescript': () => require('typescript') 'svelte3/typescript': () => require('typescript')
}, },
parserOptions: { parserOptions: {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 2020 ecmaVersion: 2020
}, },
env: { env: {
browser: true, browser: true,
es2017: true, es2017: true,
node: true node: true
} }
}; };

13
.prettierignore Normal file
View file

@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View file

@ -1,6 +1,6 @@
{ {
"useTabs": false, "useTabs": false,
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "trailingComma": "none",
"printWidth": 100 "printWidth": 100
} }

View file

@ -1,6 +1,3 @@
{ {
"cSpell.words": [ "cSpell.words": ["kickstarter", "msrp"]
"kickstarter", }
"msrp"
]
}

View file

@ -1,24 +1,14 @@
# create-svelte # Bored Game
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte); Feeling bored?
## Creating a project How about a board game?
If you're seeing this, you've probably already done this step. Congrats! Use this app to search for a board game to play because you are bored apparently.
```bash
# create a new project in the current directory
npm init svelte@next
# create a new project in my-app
npm init svelte@next my-app
```
> Note: the `@next` is temporary
## Developing ## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: Once you've checked out the project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash ```bash
npm run dev npm run dev
@ -29,10 +19,12 @@ npm run dev -- --open
## Building ## Building
Before creating a production version of your app, install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment. Then: Building for production?
Great! Run:
```bash ```bash
npm run build npm run build
``` ```
> You can preview the built app with `npm run preview`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production. > You can preview the built app with `npm run preview`. This should _not_ be used to serve your app in production.

6184
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,43 +2,46 @@
"name": "boredgame", "name": "boredgame",
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"dev": "svelte-kit dev --host", "dev": "vite dev --host",
"build": "svelte-kit build", "build": "vite build",
"package": "svelte-kit package", "package": "svelte-kit package",
"preview": "svelte-kit preview", "preview": "vite preview",
"prepare": "svelte-kit sync",
"test": "playwright test",
"check": "svelte-check --tsconfig ./tsconfig.json", "check": "svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .", "lint": "prettier --check --plugin-search-dir=. . && eslint .",
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ." "format": "prettier --write --plugin-search-dir=. ."
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.21.1", "@playwright/test": "^1.23.2",
"@rgossiaux/svelte-headlessui": "1.0.0-beta.12", "@rgossiaux/svelte-headlessui": "1.0.2",
"@sveltejs/adapter-auto": "next", "@sveltejs/adapter-auto": "next",
"@sveltejs/kit": "1.0.0-next.326", "@sveltejs/kit": "next",
"@types/cookie": "^0.5.1", "@types/cookie": "^0.5.1",
"@types/node": "^17.0.31", "@types/node": "^17.0.45",
"@typescript-eslint/eslint-plugin": "^5.22.0", "@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.22.0", "@typescript-eslint/parser": "^5.30.5",
"carbon-components-svelte": "^0.63.8", "carbon-components-svelte": "^0.63.8",
"carbon-icons-svelte": "^11.0.1", "carbon-icons-svelte": "^11.1.0",
"eslint": "^8.15.0", "eslint": "^8.19.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^3.4.1", "eslint-plugin-svelte3": "^3.4.1",
"prettier": "^2.6.2", "prettier": "^2.7.1",
"prettier-plugin-svelte": "^2.7.0", "prettier-plugin-svelte": "^2.7.0",
"svelte": "^3.48.0", "sass": "^1.53.0",
"svelte-check": "^2.7.0", "svelte": "^3.49.0",
"svelte-preprocess": "^4.10.6", "svelte-check": "^2.8.0",
"svelte-preprocess": "^4.10.7",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "^4.6.4" "typescript": "^4.7.4",
"vite": "^2.9.13"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@fontsource/fira-mono": "^4.5.8", "@fontsource/fira-mono": "^4.5.8",
"@lukeed/uuid": "^2.0.0", "@lukeed/uuid": "^2.0.0",
"cookie": "^0.5.0", "cookie": "^0.5.0",
"node-sass": "^7.0.1", "zod": "^3.17.3"
"zod": "^3.15.1"
} }
} }

View file

@ -1,10 +1,10 @@
import type { PlaywrightTestConfig } from '@playwright/test'; import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
webServer: { webServer: {
command: 'npm run build && npm run preview', command: 'npm run build && npm run preview',
port: 3000 port: 3000
} }
}; };
export default config; export default config;

File diff suppressed because it is too large Load diff

View file

@ -1,103 +1,107 @@
@import '@fontsource/fira-mono'; @import '@fontsource/fira-mono';
:root { :root {
font-family: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, font-family: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--font-mono: 'Fira Mono', monospace; --font-mono: 'Fira Mono', monospace;
--pure-white: #ffffff; --pure-white: #ffffff;
--primary-color: #b9c6d2; --primary-color: #b9c6d2;
--secondary-color: #d0dde9; --secondary-color: #d0dde9;
--tertiary-color: #edf0f8; --tertiary-color: #edf0f8;
--accent-color: #ff3e00; --accent-color: #ff3e00;
--heading-color: rgba(0, 0, 0, 0.7); --heading-color: rgba(0, 0, 0, 0.7);
--text-color: #444444; --text-color: #444444;
--background-without-opacity: rgba(255, 255, 255, 0.7); --background-without-opacity: rgba(255, 255, 255, 0.7);
--column-width: 42rem; --column-width: 42rem;
--column-margin-top: 4rem; --column-margin-top: 4rem;
} }
body { body {
min-height: 100vh; min-height: 100vh;
margin: 0; margin: 0;
background-color: var(--primary-color); background-color: var(--primary-color);
background: linear-gradient(180deg, background: linear-gradient(
var(--primary-color) 0%, 180deg,
var(--secondary-color) 10.45%, var(--primary-color) 0%,
var(--tertiary-color) 41.35%); var(--secondary-color) 10.45%,
var(--tertiary-color) 41.35%
);
} }
body::before { body::before {
content: ''; content: '';
width: 80vw; width: 80vw;
height: 100vh; height: 100vh;
position: absolute; position: absolute;
top: 0; top: 0;
left: 10vw; left: 10vw;
z-index: -1; z-index: -1;
background: radial-gradient(50% 50% at 50% 50%, background: radial-gradient(
var(--pure-white) 0%, 50% 50% at 50% 50%,
rgba(255, 255, 255, 0) 100%); var(--pure-white) 0%,
opacity: 0.05; rgba(255, 255, 255, 0) 100%
);
opacity: 0.05;
} }
#svelte { #svelte {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
h1, h1,
h2, h2,
p { p {
font-weight: 400; font-weight: 400;
color: var(--heading-color); color: var(--heading-color);
} }
p { p {
line-height: 1.5; line-height: 1.5;
} }
a { a {
color: var(--accent-color); color: var(--accent-color);
text-decoration: none; text-decoration: none;
} }
a:hover { a:hover {
text-decoration: underline; text-decoration: underline;
} }
h1 { h1 {
font-size: 2rem; font-size: 2rem;
text-align: center; text-align: center;
} }
h2 { h2 {
font-size: 1rem; font-size: 1rem;
} }
pre { pre {
font-size: 16px; font-size: 16px;
font-family: var(--font-mono); font-family: var(--font-mono);
background-color: rgba(255, 255, 255, 0.45); background-color: rgba(255, 255, 255, 0.45);
border-radius: 3px; border-radius: 3px;
box-shadow: 2px 2px 6px rgb(255 255 255 / 25%); box-shadow: 2px 2px 6px rgb(255 255 255 / 25%);
padding: 0.5em; padding: 0.5em;
overflow-x: auto; overflow-x: auto;
color: var(--text-color); color: var(--text-color);
} }
input, input,
button { button {
font-size: inherit; font-size: inherit;
font-family: inherit; font-family: inherit;
} }
button:focus:not(:focus-visible) { button:focus:not(:focus-visible) {
outline: none; outline: none;
} }
@media (min-width: 720px) { @media (min-width: 720px) {
h1 { h1 {
font-size: 2.4rem; font-size: 2.4rem;
} }
} }

14
src/app.d.ts vendored
View file

@ -2,14 +2,10 @@
// See https://kit.svelte.dev/docs/types#app // See https://kit.svelte.dev/docs/types#app
// for information about these interfaces // for information about these interfaces
// and what to do when importing types
declare namespace App { declare namespace App {
interface Locals { // interface Locals {}
userid: string; // interface Platform {}
} // interface Session {}
// interface Stuff {}
// interface Platform {}
// interface Session {}
// interface Stuff {}
} }

View file

@ -1,13 +1,41 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="description" content="Svelte demo app" /> <meta name="description" content="Bored? Find a game! Bored Game!" />
<link rel="icon" href="%svelte.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
%svelte.head% <script>
</head> const htmlElement = document.documentElement;
<body> const userTheme = localStorage.theme;
<div id="svelte">%svelte.body%</div> const userFont = localStorage.font;
</body>
// check if the user set a theme
if (userTheme) {
htmlElement.dataset.theme = userTheme;
}
// otherwise check for user preference
if (!userTheme && prefersDarkMode) {
htmlElement.dataset.theme = '🌛 Night';
localStorage.theme = '🌛 Night';
}
if (!userTheme && prefersLightMode) {
htmlElement.dataset.theme = '☀️ Daylight';
localStorage.theme = '☀️ Daylight';
}
// if nothing is set default to dark mode
if (!userTheme && !prefersDarkMode && !prefersLightMode) {
htmlElement.dataset.theme = '🌛 Night';
localStorage.theme = '🌛 Night';
}
</script>
%sveltekit.head%
</head>
<body>
<div id="svelte">%sveltekit.body%</div>
</body>
</html> </html>

View file

@ -3,22 +3,22 @@ import { v4 as uuid } from '@lukeed/uuid';
import type { Handle } from '@sveltejs/kit'; import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => { export const handle: Handle = async ({ event, resolve }) => {
const cookies = cookie.parse(event.request.headers.get('cookie') || ''); const cookies = cookie.parse(event.request.headers.get('cookie') || '');
event.locals.userid = cookies.userid || uuid(); event.locals.userid = cookies.userid || uuid();
const response = await resolve(event); const response = await resolve(event);
if (!cookies.userid) { if (!cookies.userid) {
// if this is the first time the user has visited this app, // if this is the first time the user has visited this app,
// set a cookie so that we recognize them when they return // set a cookie so that we recognize them when they return
response.headers.set( response.headers.set(
'set-cookie', 'set-cookie',
cookie.serialize('userid', event.locals.userid, { cookie.serialize('userid', event.locals.userid, {
path: '/', path: '/',
httpOnly: true httpOnly: true
}) })
); );
} }
return response; return response;
}; };

View file

@ -1,103 +1,103 @@
<script lang="ts"> <script lang="ts">
import { spring } from 'svelte/motion'; import { spring } from 'svelte/motion';
let count = 0; let count = 0;
const displayed_count = spring(); const displayed_count = spring();
$: displayed_count.set(count); $: displayed_count.set(count);
$: offset = modulo($displayed_count, 1); $: offset = modulo($displayed_count, 1);
function modulo(n: number, m: number) { function modulo(n: number, m: number) {
// handle negative numbers // handle negative numbers
return ((n % m) + m) % m; return ((n % m) + m) % m;
} }
</script> </script>
<div class="counter"> <div class="counter">
<button on:click={() => (count -= 1)} aria-label="Decrease the counter by one"> <button on:click={() => (count -= 1)} aria-label="Decrease the counter by one">
<svg aria-hidden="true" viewBox="0 0 1 1"> <svg aria-hidden="true" viewBox="0 0 1 1">
<path d="M0,0.5 L1,0.5" /> <path d="M0,0.5 L1,0.5" />
</svg> </svg>
</button> </button>
<div class="counter-viewport"> <div class="counter-viewport">
<div class="counter-digits" style="transform: translate(0, {100 * offset}%)"> <div class="counter-digits" style="transform: translate(0, {100 * offset}%)">
<strong class="hidden" aria-hidden="true">{Math.floor($displayed_count + 1)}</strong> <strong class="hidden" aria-hidden="true">{Math.floor($displayed_count + 1)}</strong>
<strong>{Math.floor($displayed_count)}</strong> <strong>{Math.floor($displayed_count)}</strong>
</div> </div>
</div> </div>
<button on:click={() => (count += 1)} aria-label="Increase the counter by one"> <button on:click={() => (count += 1)} aria-label="Increase the counter by one">
<svg aria-hidden="true" viewBox="0 0 1 1"> <svg aria-hidden="true" viewBox="0 0 1 1">
<path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" /> <path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" />
</svg> </svg>
</button> </button>
</div> </div>
<style> <style>
.counter { .counter {
display: flex; display: flex;
border-top: 1px solid rgba(0, 0, 0, 0.1); border-top: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.1); border-bottom: 1px solid rgba(0, 0, 0, 0.1);
margin: 1rem 0; margin: 1rem 0;
} }
.counter button { .counter button {
width: 2em; width: 2em;
padding: 0; padding: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 0; border: 0;
background-color: transparent; background-color: transparent;
touch-action: manipulation; touch-action: manipulation;
color: var(--text-color); color: var(--text-color);
font-size: 2rem; font-size: 2rem;
} }
.counter button:hover { .counter button:hover {
background-color: var(--secondary-color); background-color: var(--secondary-color);
} }
svg { svg {
width: 25%; width: 25%;
height: 25%; height: 25%;
} }
path { path {
vector-effect: non-scaling-stroke; vector-effect: non-scaling-stroke;
stroke-width: 2px; stroke-width: 2px;
stroke: var(--text-color); stroke: var(--text-color);
} }
.counter-viewport { .counter-viewport {
width: 8em; width: 8em;
height: 4em; height: 4em;
overflow: hidden; overflow: hidden;
text-align: center; text-align: center;
position: relative; position: relative;
} }
.counter-viewport strong { .counter-viewport strong {
position: absolute; position: absolute;
display: flex; display: flex;
width: 100%; width: 100%;
height: 100%; height: 100%;
font-weight: 400; font-weight: 400;
color: var(--accent-color); color: var(--accent-color);
font-size: 4rem; font-size: 4rem;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.counter-digits { .counter-digits {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.hidden { .hidden {
top: -100%; top: -100%;
user-select: none; user-select: none;
} }
</style> </style>

View file

@ -24,7 +24,7 @@
</div> </div>
</article> </article>
<style> <style lang="scss">
.thumbnail { .thumbnail {
align-self: start; align-self: start;
} }
@ -33,8 +33,11 @@
border-radius: 10px; border-radius: 10px;
} }
.game-container:hover { .game-container {
background-color: var(--primary); background-color: var(--primary);
&:hover {
background-color: hsla(222, 9%, 65%, 1);
}
} }
.game-container { .game-container {

View file

@ -0,0 +1,146 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { browser } from '$app/env';
import {
Listbox,
ListboxButton,
ListboxOption,
ListboxOptions
} from '@rgossiaux/svelte-headlessui';
const themes = {
'🌛 Night': { name: '🌛 Night' },
'☀️ Daylight': { name: '☀️ Daylight' },
'🐺 Night Howl': { name: '🐺 Night Howl' },
'🧠 Night Mind': { name: '🧠 Night Mind' }
};
let selectedTheme = getTheme() ?? themes['🌛 Night'];
function getTheme() {
if (!browser) return;
const htmlElement = document.documentElement;
const userTheme = localStorage.theme;
const prefersDarkMode = window.matchMedia('prefers-color-scheme: dark').matches;
const prefersLightMode = window.matchMedia('prefers-color-scheme: light').matches;
// check if the user set a theme
if (userTheme) {
htmlElement.dataset.theme = userTheme;
return themes[userTheme];
}
// otherwise check for user preference
if (!userTheme && prefersDarkMode) {
htmlElement.dataset.theme = '🌛 Night';
localStorage.theme = '🌛 Night';
}
if (!userTheme && prefersLightMode) {
htmlElement.dataset.theme = '☀️ Daylight';
localStorage.theme = '☀️ Daylight';
}
// if nothing is set default to dark mode
if (!userTheme && !prefersDarkMode && !prefersLightMode) {
htmlElement.dataset.theme = '🌛 Night';
localStorage.theme = '🌛 Night';
}
return themes[userTheme];
}
function handleChange(event: CustomEvent) {
selectedTheme = themes[event.detail.name];
const htmlElement = document.documentElement;
htmlElement.dataset.theme = selectedTheme.name;
localStorage.theme = selectedTheme.name;
}
</script>
<div class="theme">
<div class="listbox">
<Listbox value={selectedTheme} on:change={handleChange} let:open>
<ListboxButton class="button">
<span>{selectedTheme.name}</span>
<span>
<svg
width="20"
height="20"
class="arrows"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</span>
</ListboxButton>
{#if open}
<div transition:fade={{ duration: 100 }}>
<ListboxOptions class="options" static>
{#each Object.entries(themes) as [key, theme] (key)}
<ListboxOption value={theme} let:active let:selected>
<span class="option" class:active class:selected>
{theme.name}
</span>
</ListboxOption>
{/each}
</ListboxOptions>
</div>
{/if}
</Listbox>
</div>
</div>
<style>
.listbox {
--width: 184px;
}
.listbox :global(.button) {
width: var(--width);
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-16) var(--spacing-24);
font-weight: 700;
background-color: var(--clr-primary);
color: var(--clr-theme-txt);
border-radius: var(--rounded-20);
box-shadow: var(--shadow-sm);
}
.listbox :global(.arrows) {
width: 20px;
height: 20px;
display: block;
}
.listbox :global(.options) {
width: var(--width);
position: absolute;
margin-top: 0.4rem;
font-weight: 700;
color: var(--clr-theme-txt);
background-color: var(--clr-primary);
border-radius: var(--rounded-20);
box-shadow: var(--shadow-sm);
list-style: none;
}
.listbox :global(.option) {
display: block;
padding: var(--spacing-16) var(--spacing-24);
border-radius: var(--rounded-20);
cursor: pointer;
}
.listbox :global(.active) {
background-color: var(--clr-theme-active);
}
</style>

View file

@ -3,82 +3,82 @@ import { invalidate } from '$app/navigation';
// this action (https://svelte.dev/tutorial/actions) allows us to // this action (https://svelte.dev/tutorial/actions) allows us to
// progressively enhance a <form> that already works without JS // progressively enhance a <form> that already works without JS
export function enhance( export function enhance(
form: HTMLFormElement, form: HTMLFormElement,
{ {
pending, pending,
error, error,
result result
}: { }: {
pending?: ({ data, form }: { data: FormData; form: HTMLFormElement }) => void; pending?: ({ data, form }: { data: FormData; form: HTMLFormElement }) => void;
error?: ({ error?: ({
data, data,
form, form,
response, response,
error error
}: { }: {
data: FormData; data: FormData;
form: HTMLFormElement; form: HTMLFormElement;
response: Response | null; response: Response | null;
error: Error | null; error: Error | null;
}) => void; }) => void;
result?: ({ result?: ({
data, data,
form, form,
response response
}: { }: {
data: FormData; data: FormData;
response: Response; response: Response;
form: HTMLFormElement; form: HTMLFormElement;
}) => void; }) => void;
} = {} } = {}
) { ) {
let current_token: unknown; let current_token: unknown;
async function handle_submit(e: SubmitEvent) { async function handle_submit(e: SubmitEvent) {
const token = (current_token = {}); const token = (current_token = {});
e.preventDefault(); e.preventDefault();
const data = new FormData(form); const data = new FormData(form);
if (pending) pending({ data, form }); if (pending) pending({ data, form });
try { try {
const response = await fetch(form.action, { const response = await fetch(form.action, {
method: form.method, method: form.method,
headers: { headers: {
accept: 'application/json' accept: 'application/json'
}, },
body: data body: data
}); });
if (token !== current_token) return; if (token !== current_token) return;
if (response.ok) { if (response.ok) {
if (result) result({ data, form, response }); if (result) result({ data, form, response });
const url = new URL(form.action); const url = new URL(form.action);
url.search = url.hash = ''; url.search = url.hash = '';
invalidate(url.href); invalidate(url.href);
} else if (error) { } else if (error) {
error({ data, form, error: null, response }); error({ data, form, error: null, response });
} else { } else {
console.error(await response.text()); console.error(await response.text());
} }
} catch (e: unknown) { } catch (e: unknown) {
if (error && e instanceof Error) { if (error && e instanceof Error) {
error({ data, form, error: e, response: null }); error({ data, form, error: e, response: null });
} else { } else {
throw e; throw e;
} }
} }
} }
form.addEventListener('submit', handle_submit); form.addEventListener('submit', handle_submit);
return { return {
destroy() { destroy() {
form.removeEventListener('submit', handle_submit); form.removeEventListener('submit', handle_submit);
} }
}; };
} }

View file

@ -6,7 +6,8 @@
// } from "carbon-components-svelte"; // } from "carbon-components-svelte";
// import type { CarbonTheme } from "carbon-components-svelte/types/Theme/Theme.svelte"; // import type { CarbonTheme } from "carbon-components-svelte/types/Theme/Theme.svelte";
import { page } from '$app/stores'; import { page } from '$app/stores';
import Toggle from '$root/components/toggle.svelte'; import Themes from '$lib/components/preferences/themes.svelte';
import Toggle from '$lib/components/toggle.svelte';
import logo from './svelte-logo.svg'; import logo from './svelte-logo.svg';
// let theme: CarbonTheme = "white"; // let theme: CarbonTheme = "white";
@ -34,12 +35,13 @@
<div> <div>
<Toggle /> <Toggle />
</div> </div>
<ul> <div><Themes /></div>
<!-- <ul>
<li class:active={$page.url.pathname === '/'}><a sveltekit:prefetch href="/">Home</a></li> <li class:active={$page.url.pathname === '/'}><a sveltekit:prefetch href="/">Home</a></li>
<li class:active={$page.url.pathname === '/about'}> <li class:active={$page.url.pathname === '/about'}>
<a sveltekit:prefetch href="/about">About</a> <a sveltekit:prefetch href="/about">About</a>
</li> </li>
</ul> </ul> -->
</nav> </nav>
</header> </header>

View file

@ -3,27 +3,27 @@ import { writable } from 'svelte/store';
// Custom store // Custom store
const newToast = () => { const newToast = () => {
const { subscribe, update } = writable([]); const { subscribe, update } = writable([]);
function send(message: string, { duration = 2000, type = 'INFO' } = {}) { function send(message: string, { duration = 2000, type = 'INFO' } = {}) {
const id = Math.floor(Math.random() * 1000); const id = Math.floor(Math.random() * 1000);
const newMessage = { const newMessage = {
id, id,
duration, duration,
type, type,
message message
}; };
update((store) => [...store, newMessage]); update((store) => [...store, newMessage]);
} }
function remove(id: number) { function remove(id: number) {
update((store) => { update((store) => {
const newStore = store.filter((item: ToastData) => item.id !== id); const newStore = store.filter((item: ToastData) => item.id !== id);
return [...newStore]; return [...newStore];
}); });
} }
return { subscribe, send, remove }; return { subscribe, send, remove };
}; };
export const toast = newToast(); export const toast = newToast();

View file

@ -9,7 +9,7 @@ export type ToastData = {
duration: number; duration: number;
type: ToastType; type: ToastType;
message: string; message: string;
} };
export type GameType = { export type GameType = {
id: string; id: string;
@ -18,7 +18,7 @@ export type GameType = {
url: string; url: string;
edit_url: string; edit_url: string;
thumb_url: string; thumb_url: string;
image_url: string, image_url: string;
price: number; price: number;
price_ca: number; price_ca: number;
price_uk: number; price_uk: number;
@ -33,7 +33,7 @@ export type GameType = {
description: string; description: string;
players: string; players: string;
playtime: string; playtime: string;
} };
export type SearchQuery = { export type SearchQuery = {
client_id: string; client_id: string;
@ -84,4 +84,4 @@ export type SearchQuery = {
lt_reddit_week_count?: number; lt_reddit_week_count?: number;
lt_reddit_day_count?: number; lt_reddit_day_count?: number;
fields?: string; fields?: string;
} };

View file

@ -1,10 +1,10 @@
import { z } from "zod"; import { z } from 'zod';
export const BoardGameSearch = z.object({ export const BoardGameSearch = z.object({
minAge: z.number(), minAge: z.number(),
maxAge: z.number(), maxAge: z.number(),
minPlayers: z.number(), minPlayers: z.number(),
maxPlayers: z.number(), maxPlayers: z.number()
}); });
export const Game = z.object({ export const Game = z.object({
@ -26,5 +26,5 @@ export const Game = z.object({
min_age: z.number(), min_age: z.number(),
description: z.string(), description: z.string(),
players: z.string(), players: z.string(),
playtime: z.string(), playtime: z.string()
}); });

View file

@ -1,21 +1,32 @@
<script lang="ts"> <script lang="ts">
import Header from '$lib/header/Header.svelte'; import Header from '$lib/header/Header.svelte';
import '$root/styles/global.css'; // import 'carbon-components-svelte/css/all.css';
import 'carbon-components-svelte/css/all.css'; import '$root/styles/styles.scss';
import '../app.css';
</script> </script>
<Header /> <div class="fade" style:animation-duration="250ms" style:animation-delay="250ms">
<Header />
<main> <main>
<slot /> <slot />
</main> </main>
<footer>
<footer> <p>Built by <a target="__blank" href="https://bradleyshellnut.com">Bradley Shellnut</a></p>
<p>Built by <a target="__blank" href="https://bradleyshellnut.com">Bradley Shellnut</a></p> </footer>
</footer> </div>
<style> <style>
.fade {
animation-name: fadeIn;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
main { main {
flex: 1; flex: 1;
display: flex; display: flex;

View file

@ -9,14 +9,19 @@
guarantees are made. Don't use it to organize your life.) guarantees are made. Don't use it to organize your life.)
*/ */
import { URLSearchParams } from "url"; import { URLSearchParams } from 'url';
const base = 'https://api.boardgameatlas.com/api'; const base = 'https://api.boardgameatlas.com/api';
export function boardGameApi(method: string, resource: string, queryParams: Record<string, string>, data?: Record<string, unknown>) { export function boardGameApi(
method: string,
resource: string,
queryParams: Record<string, string>,
data?: Record<string, unknown>
) {
queryParams.client_id = import.meta.env.VITE_PUBLIC_CLIENT_ID; queryParams.client_id = import.meta.env.VITE_PUBLIC_CLIENT_ID;
const urlQueryParams = new URLSearchParams(queryParams) const urlQueryParams = new URLSearchParams(queryParams);
const url = `${base}/${resource}${urlQueryParams ? `?${urlQueryParams}` : ''}` const url = `${base}/${resource}${urlQueryParams ? `?${urlQueryParams}` : ''}`;
return fetch(url, { return fetch(url, {
method, method,
headers: { headers: {

View file

@ -1,50 +1,50 @@
<script context="module"> <script context="module">
import { browser, dev } from '$app/env'; import { browser, dev } from '$app/env';
// we don't need any JS on this page, though we'll load // we don't need any JS on this page, though we'll load
// it in dev so that we get hot module replacement... // it in dev so that we get hot module replacement...
export const hydrate = dev; export const hydrate = dev;
// ...but if the client-side router is already loaded // ...but if the client-side router is already loaded
// (i.e. we came here from elsewhere in the app), use it // (i.e. we came here from elsewhere in the app), use it
export const router = browser; export const router = browser;
// since there's no dynamic data here, we can prerender // since there's no dynamic data here, we can prerender
// it so that it gets served as a static asset in prod // it so that it gets served as a static asset in prod
export const prerender = true; export const prerender = true;
</script> </script>
<svelte:head> <svelte:head>
<title>About</title> <title>About</title>
</svelte:head> </svelte:head>
<div class="content"> <div class="content">
<h1>About this app</h1> <h1>About this app</h1>
<p> <p>
This is a <a href="https://kit.svelte.dev">SvelteKit</a> app. You can make your own by typing the This is a <a href="https://kit.svelte.dev">SvelteKit</a> app. You can make your own by typing the
following into your command line and following the prompts: following into your command line and following the prompts:
</p> </p>
<!-- TODO lose the @next! --> <!-- TODO lose the @next! -->
<pre>npm init svelte@next</pre> <pre>npm init svelte@next</pre>
<p> <p>
The page you're looking at is purely static HTML, with no client-side interactivity needed. The page you're looking at is purely static HTML, with no client-side interactivity needed.
Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening
the devtools network panel and reloading. the devtools network panel and reloading.
</p> </p>
<p> <p>
The <a href="/todos">TODOs</a> page illustrates SvelteKit's data loading and form handling. Try using The <a href="/todos">TODOs</a> page illustrates SvelteKit's data loading and form handling. Try using
it with JavaScript disabled! it with JavaScript disabled!
</p> </p>
</div> </div>
<style> <style>
.content { .content {
width: 100%; width: 100%;
max-width: var(--column-width); max-width: var(--column-width);
margin: var(--column-margin-top) auto 0 auto; margin: var(--column-margin-top) auto 0 auto;
} }
</style> </style>

View file

@ -1,5 +1,5 @@
import type { RequestHandler } from "@sveltejs/kit"; import type { RequestHandler } from '@sveltejs/kit';
import type { SearchQuery } from "$lib/types"; import type { SearchQuery } from '$lib/types';
export const post: RequestHandler = async ({ request }) => { export const post: RequestHandler = async ({ request }) => {
const form = await request.formData(); const form = await request.formData();
@ -8,8 +8,8 @@ export const post: RequestHandler = async ({ request }) => {
order_by: 'rank', order_by: 'rank',
ascending: false, ascending: false,
limit: 20, limit: 20,
client_id: import.meta.env.VITE_PUBLIC_CLIENT_ID, client_id: import.meta.env.VITE_PUBLIC_CLIENT_ID
} };
const minAge = form.get('minAge'); const minAge = form.get('minAge');
const minPlayers = form.get('minPlayers'); const minPlayers = form.get('minPlayers');
@ -47,7 +47,7 @@ export const post: RequestHandler = async ({ request }) => {
if (exactMinPlayers) { if (exactMinPlayers) {
queryParams.min_players = +minPlayers; queryParams.min_players = +minPlayers;
} else { } else {
queryParams.gt_min_players = (+minPlayers === 1 ? 0 : (+minPlayers - 1)); queryParams.gt_min_players = +minPlayers === 1 ? 0 : +minPlayers - 1;
} }
} }
@ -70,12 +70,14 @@ export const post: RequestHandler = async ({ request }) => {
const urlQueryParams = new URLSearchParams(newQueryParams); const urlQueryParams = new URLSearchParams(newQueryParams);
const url = `https://api.boardgameatlas.com/api/search${urlQueryParams ? `?${urlQueryParams}` : ''}` const url = `https://api.boardgameatlas.com/api/search${
urlQueryParams ? `?${urlQueryParams}` : ''
}`;
const response = await fetch(url, { const response = await fetch(url, {
method: 'get', method: 'get',
headers: { headers: {
'content-type': 'application/json' 'content-type': 'application/json'
}, }
}); });
console.log('response', response); console.log('response', response);
if (response.status === 404) { if (response.status === 404) {
@ -94,7 +96,7 @@ export const post: RequestHandler = async ({ request }) => {
console.log('games', games); console.log('games', games);
return { return {
body: { body: {
games: gameResponse?.games, games: gameResponse?.games
} }
}; };
} }
@ -102,4 +104,4 @@ export const post: RequestHandler = async ({ request }) => {
return { return {
status: response.status status: response.status
}; };
} };

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { Game } from "$lib/types"; import type { Game } from '$lib/types';
import { Checkbox, NumberInput } from "carbon-components-svelte"; import { Checkbox, NumberInput } from 'carbon-components-svelte';
// import { enhance } from "$lib/form"; // import { enhance } from "$lib/form";
let games: Game[] = []; let games: Game[] = [];
@ -21,16 +21,15 @@ import { Checkbox, NumberInput } from "carbon-components-svelte";
} }
let minAge = 0; let minAge = 0;
let minPlayers = 1; let minPlayers = 1;
let maxPlayers = 1; let maxPlayers = 1;
let exactMinAge = false; let exactMinAge = false;
let exactMinPlayers = false; let exactMinPlayers = false;
let exactMaxPlayers = false; let exactMaxPlayers = false;
</script> </script>
<svelte:head> <svelte:head>
<title>Games</title> <title>Games</title>
</svelte:head> </svelte:head>
<h1>Search Boardgames!</h1> <h1>Search Boardgames!</h1>
@ -50,7 +49,12 @@ import { Checkbox, NumberInput } from "carbon-components-svelte";
invalidText="Number must be between 0 and 120" invalidText="Number must be between 0 and 120"
label="Min Age" label="Min Age"
/> />
<Checkbox name="exactMinAge" bind:value={exactMinAge} labelText="Search exact?" bind:checked={exactMinAge} /> <Checkbox
name="exactMinAge"
bind:value={exactMinAge}
labelText="Search exact?"
bind:checked={exactMinAge}
/>
</div> </div>
<div> <div>
<NumberInput <NumberInput
@ -61,7 +65,12 @@ import { Checkbox, NumberInput } from "carbon-components-svelte";
invalidText="Number must be between 1 and 50" invalidText="Number must be between 1 and 50"
label="Min Players" label="Min Players"
/> />
<Checkbox name="exactMinPlayers" labelText="Search exact?" bind:value={exactMinPlayers} bind:checked={exactMinPlayers} /> <Checkbox
name="exactMinPlayers"
labelText="Search exact?"
bind:value={exactMinPlayers}
bind:checked={exactMinPlayers}
/>
</div> </div>
<div> <div>
<NumberInput <NumberInput
@ -72,7 +81,12 @@ import { Checkbox, NumberInput } from "carbon-components-svelte";
invalidText="Number must be between 1 and 50" invalidText="Number must be between 1 and 50"
label="Max Players" label="Max Players"
/> />
<Checkbox name="exactMaxPlayers" labelText="Search exact?" bind:value={exactMaxPlayers} bind:checked={exactMaxPlayers} /> <Checkbox
name="exactMaxPlayers"
labelText="Search exact?"
bind:value={exactMaxPlayers}
bind:checked={exactMaxPlayers}
/>
</div> </div>
</fieldset> </fieldset>
<button type="submit" disabled={submitting}>Submit</button> <button type="submit" disabled={submitting}>Submit</button>
@ -86,22 +100,22 @@ import { Checkbox, NumberInput } from "carbon-components-svelte";
</div> </div>
<div class="games"> <div class="games">
<h1>Games</h1> <h1>Games</h1>
{#each games as game} {#each games as game}
<section> <section>
<div> <div>
<h2>{game.name}</h2> <h2>{game.name}</h2>
<p>price : {game.price}</p> <p>price : {game.price}</p>
<p>year_published : {game.year_published}</p> <p>year_published : {game.year_published}</p>
<p>min_players : {game.min_players}</p> <p>min_players : {game.min_players}</p>
<p>max_players : {game.max_players}</p> <p>max_players : {game.max_players}</p>
<p>min_playtime : {game.min_playtime}</p> <p>min_playtime : {game.min_playtime}</p>
<p>max_playtime : {game.max_playtime}</p> <p>max_playtime : {game.max_playtime}</p>
<p>min_age : {game.min_age}</p> <p>min_age : {game.min_age}</p>
<p>players : {game.players}</p> <p>players : {game.players}</p>
<p>playtime : {game.playtime}</p> <p>playtime : {game.playtime}</p>
<div class="description">{@html game.description}</div> <div class="description">{@html game.description}</div>
</div> </div>
</section> </section>
{/each} {/each}
</div> </div>
@ -117,14 +131,14 @@ import { Checkbox, NumberInput } from "carbon-components-svelte";
border-radius: 4px; border-radius: 4px;
margin: 0; margin: 0;
padding: 0.2rem; padding: 0.2rem;
background-color: palegreen; background-color: var(--color-btn-primary-active);
} }
.games { .games {
display: grid; display: grid;
gap: 2rem; gap: 2rem;
} }
.description { .description {
margin: 1rem; margin: 1rem;
} }
@ -139,4 +153,4 @@ import { Checkbox, NumberInput } from "carbon-components-svelte";
gap: 1rem; gap: 1rem;
grid-template-columns: repeat(3, minmax(200px, 1fr)); grid-template-columns: repeat(3, minmax(200px, 1fr));
} }
</style> </style>

View file

@ -81,4 +81,4 @@ import { boardGameApi } from '../_api';
// return { // return {
// status: response.status // status: response.status
// }; // };
// } // }

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Listbox from '$root/components/listbox.svelte'; import Listbox from '$lib/components/listbox.svelte';
</script> </script>
<Listbox /> <Listbox />

View file

@ -1,12 +1,8 @@
<script context="module" lang="ts">
export const prerender = true;
</script>
<script lang="ts"> <script lang="ts">
import { Checkbox, NumberInput } from 'carbon-components-svelte'; // import { Checkbox, NumberInput } from 'carbon-components-svelte';
import Game from '$root/components/game.svelte'; import Game from '$lib/components/game.svelte';
import type { GameType } from '$lib/types'; import type { GameType } from '$lib/types';
import Listbox from '$root/components/listbox.svelte'; import Listbox from '$lib/components/listbox.svelte';
// import { enhance } from "$lib/form"; // import { enhance } from "$lib/form";
let games: GameType[] = []; let games: GameType[] = [];
@ -35,7 +31,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title>Home</title> <title>Bored Game</title>
</svelte:head> </svelte:head>
<h1>Search Boardgames!</h1> <h1>Search Boardgames!</h1>
@ -44,23 +40,38 @@
<form on:submit|preventDefault={handleSubmit} method="post"> <form on:submit|preventDefault={handleSubmit} method="post">
<fieldset aria-busy={submitting} disabled={submitting}> <fieldset aria-busy={submitting} disabled={submitting}>
<div> <div>
<NumberInput <label htmlfor="minAge">
<input id="minAge" name="minAge" bind:value={minAge} type="number" min={0} max={120} />
Min Age
</label>
<!-- <NumberInput
name="minAge" name="minAge"
min={0} min={0}
max={120} max={120}
bind:value={minAge} bind:value={minAge}
invalidText="Number must be between 0 and 120" invalidText="Number must be between 0 and 120"
label="Min Age" label="Min Age"
/> /> -->
<Checkbox <!-- <Checkbox
name="exactMinAge" name="exactMinAge"
bind:checked={exactMinAge} bind:checked={exactMinAge}
bind:value={exactMinAge} bind:value={exactMinAge}
labelText="Search exact?" labelText="Search exact?"
/> /> -->
</div> </div>
<div> <div>
<NumberInput <label htmlfor="maxPlayers">
<input
id="maxPlayers"
name="maxPlayers"
bind:value={maxPlayers}
type="number"
min={0}
max={50}
/>
Min Players
</label>
<!-- <NumberInput
name="minPlayers" name="minPlayers"
min={1} min={1}
max={50} max={50}
@ -68,10 +79,21 @@
invalidText="Number must be between 1 and 50" invalidText="Number must be between 1 and 50"
label="Min Players" label="Min Players"
/> />
<Checkbox name="exactMinPlayers" labelText="Search exact?" bind:checked={exactMinPlayers} /> <Checkbox name="exactMinPlayers" labelText="Search exact?" bind:checked={exactMinPlayers} /> -->
</div> </div>
<div> <div>
<NumberInput <label htmlfor="maxPlayers">
<input
id="maxPlayers"
name="maxPlayers"
bind:value={maxPlayers}
type="number"
min={0}
max={50}
/>
Max Players
</label>
<!-- <NumberInput
name="maxPlayers" name="maxPlayers"
min={1} min={1}
max={50} max={50}
@ -79,7 +101,7 @@
invalidText="Number must be between 1 and 50" invalidText="Number must be between 1 and 50"
label="Max Players" label="Max Players"
/> />
<Checkbox name="exactMaxPlayers" labelText="Search exact?" bind:checked={exactMaxPlayers} /> <Checkbox name="exactMaxPlayers" labelText="Search exact?" bind:checked={exactMaxPlayers} /> -->
</div> </div>
</fieldset> </fieldset>
<button type="submit" disabled={submitting}>Submit</button> <button type="submit" disabled={submitting}>Submit</button>
@ -111,10 +133,11 @@
} }
button { button {
border-radius: 4px; border-radius: 10px;
margin: 0; margin: 0.5rem;
padding: 0.2rem; padding: 1rem;
background-color: palegreen; color: var(--clr-input-txt);
background-color: var(--color-btn-primary-active);
} }
.games { .games {
@ -127,8 +150,8 @@
} }
.game-form { .game-form {
display: flex; /* display: flex; */
place-items: center; /* place-items: center; */
} }
.game-form fieldset { .game-form fieldset {

View file

@ -19,8 +19,8 @@ export const post: RequestHandler = async ({ request, locals }) => {
gt_min_players: String(+minPlayers === 1 ? 0 : +minPlayers - 1), gt_min_players: String(+minPlayers === 1 ? 0 : +minPlayers - 1),
lt_max_players: String(+maxPlayers + 1), lt_max_players: String(+maxPlayers + 1),
gt_min_age: String(+minAge === 1 ? 0 : +minAge - 1), gt_min_age: String(+minAge === 1 ? 0 : +minAge - 1),
lt_max_age: String(+maxAge + 1), lt_max_age: String(+maxAge + 1)
} };
const response = await boardGameApi('get', `search`, queryParams); const response = await boardGameApi('get', `search`, queryParams);
console.log('response', response); console.log('response', response);
if (response.status === 404) { if (response.status === 404) {
@ -39,7 +39,7 @@ export const post: RequestHandler = async ({ request, locals }) => {
console.log('games', games); console.log('games', games);
return { return {
body: { body: {
games: gameResponse?.games, games: gameResponse?.games
} }
}; };
} }
@ -47,4 +47,4 @@ export const post: RequestHandler = async ({ request, locals }) => {
return { return {
status: response.status status: response.status
}; };
}; };

View file

@ -12,11 +12,11 @@
const base = 'https://api.svelte.dev'; const base = 'https://api.svelte.dev';
export function api(method: string, resource: string, data?: Record<string, unknown>) { export function api(method: string, resource: string, data?: Record<string, unknown>) {
return fetch(`${base}/${resource}`, { return fetch(`${base}/${resource}`, {
method, method,
headers: { headers: {
'content-type': 'application/json' 'content-type': 'application/json'
}, },
body: data && JSON.stringify(data) body: data && JSON.stringify(data)
}); });
} }

View file

@ -1,186 +1,186 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$lib/form'; import { enhance } from '$lib/form';
import { scale } from 'svelte/transition'; import { scale } from 'svelte/transition';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
type Todo = { type Todo = {
uid: string; uid: string;
created_at: Date; created_at: Date;
text: string; text: string;
done: boolean; done: boolean;
pending_delete: boolean; pending_delete: boolean;
}; };
export let todos: Todo[]; export let todos: Todo[];
</script> </script>
<svelte:head> <svelte:head>
<title>Todos</title> <title>Todos</title>
</svelte:head> </svelte:head>
<div class="todos"> <div class="todos">
<h1>Todos</h1> <h1>Todos</h1>
<form <form
class="new" class="new"
action="/todos" action="/todos"
method="post" method="post"
use:enhance={{ use:enhance={{
result: async ({ form }) => { result: async ({ form }) => {
form.reset(); form.reset();
} }
}} }}
> >
<input name="text" aria-label="Add todo" placeholder="+ tap to add a todo" /> <input name="text" aria-label="Add todo" placeholder="+ tap to add a todo" />
</form> </form>
{#each todos as todo (todo.uid)} {#each todos as todo (todo.uid)}
<div <div
class="todo" class="todo"
class:done={todo.done} class:done={todo.done}
transition:scale|local={{ start: 0.7 }} transition:scale|local={{ start: 0.7 }}
animate:flip={{ duration: 200 }} animate:flip={{ duration: 200 }}
> >
<form <form
action="/todos?_method=PATCH" action="/todos?_method=PATCH"
method="post" method="post"
use:enhance={{ use:enhance={{
pending: ({ data }) => { pending: ({ data }) => {
todo.done = !!data.get('done'); todo.done = !!data.get('done');
} }
}} }}
> >
<input type="hidden" name="uid" value={todo.uid} /> <input type="hidden" name="uid" value={todo.uid} />
<input type="hidden" name="done" value={todo.done ? '' : 'true'} /> <input type="hidden" name="done" value={todo.done ? '' : 'true'} />
<button class="toggle" aria-label="Mark todo as {todo.done ? 'not done' : 'done'}" /> <button class="toggle" aria-label="Mark todo as {todo.done ? 'not done' : 'done'}" />
</form> </form>
<form class="text" action="/todos?_method=PATCH" method="post" use:enhance> <form class="text" action="/todos?_method=PATCH" method="post" use:enhance>
<input type="hidden" name="uid" value={todo.uid} /> <input type="hidden" name="uid" value={todo.uid} />
<input aria-label="Edit todo" type="text" name="text" value={todo.text} /> <input aria-label="Edit todo" type="text" name="text" value={todo.text} />
<button class="save" aria-label="Save todo" /> <button class="save" aria-label="Save todo" />
</form> </form>
<form <form
action="/todos?_method=DELETE" action="/todos?_method=DELETE"
method="post" method="post"
use:enhance={{ use:enhance={{
pending: () => (todo.pending_delete = true) pending: () => (todo.pending_delete = true)
}} }}
> >
<input type="hidden" name="uid" value={todo.uid} /> <input type="hidden" name="uid" value={todo.uid} />
<button class="delete" aria-label="Delete todo" disabled={todo.pending_delete} /> <button class="delete" aria-label="Delete todo" disabled={todo.pending_delete} />
</form> </form>
</div> </div>
{/each} {/each}
</div> </div>
<style> <style>
.todos { .todos {
width: 100%; width: 100%;
max-width: var(--column-width); max-width: var(--column-width);
margin: var(--column-margin-top) auto 0 auto; margin: var(--column-margin-top) auto 0 auto;
line-height: 1; line-height: 1;
} }
.new { .new {
margin: 0 0 0.5rem 0; margin: 0 0 0.5rem 0;
} }
input { input {
border: 1px solid transparent; border: 1px solid transparent;
} }
input:focus-visible { input:focus-visible {
box-shadow: inset 1px 1px 6px rgba(0, 0, 0, 0.1); box-shadow: inset 1px 1px 6px rgba(0, 0, 0, 0.1);
border: 1px solid #ff3e00 !important; border: 1px solid #ff3e00 !important;
outline: none; outline: none;
} }
.new input { .new input {
font-size: 28px; font-size: 28px;
width: 100%; width: 100%;
padding: 0.5em 1em 0.3em 1em; padding: 0.5em 1em 0.3em 1em;
box-sizing: border-box; box-sizing: border-box;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border-radius: 8px; border-radius: 8px;
text-align: center; text-align: center;
} }
.todo { .todo {
display: grid; display: grid;
grid-template-columns: 2rem 1fr 2rem; grid-template-columns: 2rem 1fr 2rem;
grid-gap: 0.5rem; grid-gap: 0.5rem;
align-items: center; align-items: center;
margin: 0 0 0.5rem 0; margin: 0 0 0.5rem 0;
padding: 0.5rem; padding: 0.5rem;
background-color: white; background-color: white;
border-radius: 8px; border-radius: 8px;
filter: drop-shadow(2px 4px 6px rgba(0, 0, 0, 0.1)); filter: drop-shadow(2px 4px 6px rgba(0, 0, 0, 0.1));
transform: translate(-1px, -1px); transform: translate(-1px, -1px);
transition: filter 0.2s, transform 0.2s; transition: filter 0.2s, transform 0.2s;
} }
.done { .done {
transform: none; transform: none;
opacity: 0.4; opacity: 0.4;
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.1)); filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.1));
} }
form.text { form.text {
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
flex: 1; flex: 1;
} }
.todo input { .todo input {
flex: 1; flex: 1;
padding: 0.5em 2em 0.5em 0.8em; padding: 0.5em 2em 0.5em 0.8em;
border-radius: 3px; border-radius: 3px;
} }
.todo button { .todo button {
width: 2em; width: 2em;
height: 2em; height: 2em;
border: none; border: none;
background-color: transparent; background-color: transparent;
background-position: 50% 50%; background-position: 50% 50%;
background-repeat: no-repeat; background-repeat: no-repeat;
} }
button.toggle { button.toggle {
border: 1px solid rgba(0, 0, 0, 0.2); border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 50%; border-radius: 50%;
box-sizing: border-box; box-sizing: border-box;
background-size: 1em auto; background-size: 1em auto;
} }
.done .toggle { .done .toggle {
background-image: url("data:image/svg+xml,%3Csvg width='22' height='16' viewBox='0 0 22 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.5 1.5L7.4375 14.5L1.5 8.5909' stroke='%23676778' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg width='22' height='16' viewBox='0 0 22 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.5 1.5L7.4375 14.5L1.5 8.5909' stroke='%23676778' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
} }
.delete { .delete {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.5 5V22H19.5V5H4.5Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M10 10V16.5' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M14 10V16.5' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M2 5H22' stroke='%23676778' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M8 5L9.6445 2H14.3885L16 5H8Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3C/svg%3E%0A"); background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.5 5V22H19.5V5H4.5Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M10 10V16.5' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M14 10V16.5' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M2 5H22' stroke='%23676778' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M8 5L9.6445 2H14.3885L16 5H8Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
opacity: 0.2; opacity: 0.2;
} }
.delete:hover, .delete:hover,
.delete:focus { .delete:focus {
transition: opacity 0.2s; transition: opacity 0.2s;
opacity: 1; opacity: 1;
} }
.save { .save {
position: absolute; position: absolute;
right: 0; right: 0;
opacity: 0; opacity: 0;
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.5 2H3.5C2.67158 2 2 2.67157 2 3.5V20.5C2 21.3284 2.67158 22 3.5 22H20.5C21.3284 22 22 21.3284 22 20.5V3.5C22 2.67157 21.3284 2 20.5 2Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M17 2V11H7.5V2H17Z' fill='white' stroke='white' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M13.5 5.5V7.5' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3Cpath d='M5.99844 2H18.4992' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E%0A"); background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.5 2H3.5C2.67158 2 2 2.67157 2 3.5V20.5C2 21.3284 2.67158 22 3.5 22H20.5C21.3284 22 22 21.3284 22 20.5V3.5C22 2.67157 21.3284 2 20.5 2Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M17 2V11H7.5V2H17Z' fill='white' stroke='white' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M13.5 5.5V7.5' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3Cpath d='M5.99844 2H18.4992' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E%0A");
} }
.todo input:focus + .save, .todo input:focus + .save,
.save:focus { .save:focus {
transition: opacity 0.2s; transition: opacity 0.2s;
opacity: 1; opacity: 1;
} }
</style> </style>

View file

@ -1,214 +0,0 @@
/* Setup */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: inherit;
}
:root {
--color-brand: hsl(204 88% 53%);
--color-text-primary: hsl(0 0% 98%);
--color-text-muted: hsl(210 34% 80%);
--color-bg-primary: hsl(210 34% 13%);
--color-bg-secondary: hsl(209, 35%, 15%);
--color-btn-primary-active: var(--color-brand);
--color-btn-primary-active-hover: hsl(204 88% 60%);
--color-btn-primary-inactive: hsl(205 70% 33%);
--color-btn-secondary: hsl(192 19% 95%);
--color-border-primary: hsl(0, 0%, 34%);
--color-link-hover: hsl(209 22% 19%);
--color-placeholder: hsl(210 34% 80%);
--red: #990000;
--redBrown: #633539;
--blue: #336699;
--black: #1f273a;
--white: #fff;
--grey: #efefef;
--greyBlue: #888e9c;
--lighterGreyBlue: #6a707e;
--yellow: #ffc600;
--light: #ffffff;
--dark: #000000;
--lightGrey: #C5C5C5;
--lightGray: var(--lightGrey);
--imGoingToFaint: #fbfbfb;
--redBrown: #633539;
--maxWidth: 1200px;
/* Define Colors intentions */
--primary: var(--greyBlue);
--secondary: var(--redBrown);
--background: var(--white);
--textColor: var(--lighterGreyBlue);
--buttonTextColor: var(--black);
--lineColor: var(--grey);
--cardBg: var(--darkGrey);
--headerBackground: var(--greyBlue);
--footerBackground: var(--darkGrey);
--linkHover: var(--white);
--lightHairLine: #C5C5C5;
--radius-base: 2.4rem;
/* Type */
--headingFont: 'Merriweather Sans', sans-serif;
--bodyFont: 'Work Sans', sans-serif;
--baseFontSize: 100%;
--h1: 3.052rem;
--h2: 2.441rem;
--h3: 1.953rem;
--h4: 1.563rem;
--h5: 1.25rem;
--h6: 1.8rem;
--bodyTextSize: 1.8rem;
--smallText: 1.44rem;
--lineHeight: 2.25rem;
--font-serif: 'Inter', sans-serif;
--font-16: 1.6rem;
--font-18: 1.8rem;
--font-24: 2.4rem;
--font-32: 3.2rem;
--font-80: 8rem;
--spacing-4: 0.4rem;
--spacing-8: 0.5rem;
--spacing-16: 1.6rem;
--spacing-20: 2rem;
--spacing-24: 2.4rem;
--spacing-32: 3.2rem;
/* Elevation */
--level-0: none;
--level-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--level-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
--level-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
--level-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
/* Z Indexes */
--zBase: 1;
/* Positioning */
--containerPadding: 2.5%;
--headerHeight: 8rem;
--borderRadius: 4px;
--borderRadiusLarge: 10px;
--maxWidth: 1200px;
/* Media Queryies - Not yet supported in CSS */
/*
--xsmall: 340px;
--small: 500px;
--large: 960px;
--wide: 1200px;
*/
}
html {
/* background-image: url(${floatingCogs}); */
/* background-color: var(--background); */
/* background-size: 450px; */
/* background-attachment: fixed; */
font-size: 62.5%;
box-sizing: border-box;
scrollbar-width: thin;
}
html,
body {
height: 100%;
}
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-thumb {
background-color: var(--color-brand);
border-radius: var(--radius-base);
}
::selection {
background: var(--primary);
color: var(--white);
}
body {
font-family: var(--font-serif);
font-size: var(--font-18);
color: var(--color-text-primary);
background-color: var(--color-bg-primary);
}
a {
text-decoration: none;
color: var(--color-text-primary);
}
label {
display: block;
margin: var(--spacing-8) 0;
font-size: var(--font-24);
color: var(--color-text-muted);
}
input {
padding: var(--spacing-16);
font-size: var(--font-24);
border-radius: var(--radius-base);
border: none;
}
.btn {
padding: var(--spacing-16) var(--spacing-32);
font-size: var(--font-18);
font-weight: bold;
color: var(--color-text-primary);
background-color: var(--color-btn-primary-active);
border-radius: var(--radius-base);
border: none;
cursor: pointer;
}
.btn:hover {
background-color: var(--color-btn-primary-active-hover);
}
.btn:disabled {
color: var(--color-text-muted);
background-color: var(--color-btn-primary-inactive);
cursor: not-allowed;
}
ul,
ol {
list-style: none;
}
/* Utils */
.responsive {
resize: both;
overflow: scroll;
border: 1px solid hsl(0 0% 0%);
}
.placeholder {
padding: var(--spacing-20) 0;
background-color: var(--color-placeholder);
border-radius: var(--radius-base);
}

237
src/styles/global.scss Normal file
View file

@ -0,0 +1,237 @@
/* Setup */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
:root {
--color-brand: hsl(204 88% 53%);
--color-text-primary: hsl(0 0% 98%);
--color-text-muted: hsl(210 34% 80%);
--color-bg-primary: hsl(210 34% 13%);
--color-bg-secondary: hsl(209, 35%, 15%);
--color-btn-primary-active: var(--color-brand);
--color-btn-primary-active-hover: hsl(204 88% 60%);
--color-btn-primary-inactive: hsl(205 70% 33%);
--color-btn-secondary: hsl(192 19% 95%);
--color-border-primary: hsl(0, 0%, 34%);
--color-link-hover: hsl(209 22% 19%);
--color-placeholder: hsl(210 34% 80%);
--clr-input-txt: hsl(177 100% 15%);
--red: #990000;
--redBrown: #633539;
--blue: #336699;
--black: #1f273a;
--white: #fff;
--grey: #efefef;
--greyBlue: hsla(222, 9%, 57%, 1);
--lighterGreyBlue: #6a707e;
--yellow: #ffc600;
--light: #ffffff;
--dark: #000000;
--lightGrey: #c5c5c5;
--lightGray: var(--lightGrey);
--imGoingToFaint: #fbfbfb;
--redBrown: #633539;
--maxWidth: 1200px;
/* Define Colors intentions */
--primary: var(--greyBlue);
--secondary: var(--redBrown);
--background: var(--white);
--textColor: var(--lighterGreyBlue);
--buttonTextColor: var(--white);
--lineColor: var(--grey);
--cardBg: var(--darkGrey);
--headerBackground: var(--greyBlue);
--footerBackground: var(--darkGrey);
--linkHover: var(--white);
--lightHairLine: #c5c5c5;
--radius-base: 1rem;
/* Type */
--headingFont: 'Merriweather Sans', sans-serif;
--bodyFont: 'Work Sans', sans-serif;
--baseFontSize: 100%;
--h1: 3.052rem;
--h2: 2.441rem;
--h3: 1.953rem;
--h4: 1.563rem;
--h5: 1.25rem;
--h6: 1.8rem;
--bodyTextSize: 1.8rem;
--smallText: 1.44rem;
--lineHeight: 2.25rem;
--font-serif: 'Inter', sans-serif;
--font-16: 1.6rem;
--font-18: 1.8rem;
--font-24: 2.4rem;
--font-32: 3.2rem;
--font-80: 8rem;
--spacing-4: 0.4rem;
--spacing-8: 0.5rem;
--spacing-16: 1.6rem;
--spacing-20: 2rem;
--spacing-24: 2.4rem;
--spacing-32: 3.2rem;
/* Elevation */
--level-0: none;
--level-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--level-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--level-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--level-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
/* Z Indexes */
--zBase: 1;
/* Positioning */
--containerPadding: 2.5%;
--headerHeight: 8rem;
--borderRadius: 4px;
--borderRadiusLarge: 10px;
--maxWidth: 1200px;
/* Font */
--font-sans: 'Poppins', sans-serif;
--font-serif: 'Arsenica', serif;
--font-mono: Source Code Pro, monospace;
--font-dyslexic: OpenDyslexic, sans-serif;
--font-dyslexic-mono: OpenDyslexic Mono, monospace;
--font-16: 1.6rem;
--font-24: 2.4rem;
--font-32: 3.2rem;
--font-48: 4.8rem;
--font-96: 9.6rem;
/* Spacing */
--spacing-4: 0.4rem;
--spacing-8: 0.8rem;
--spacing-16: 1.6rem;
--spacing-24: 2.4rem;
--spacing-32: 3.2rem;
--spacing-64: 6.4rem;
--spacing-128: 12.8rem;
/* Scrollbar */
--clr-scrollbar-thumb: hsl(173 10% 20%);
/* Shadows */
--shadow-sm: 0px 0px 4px 4px hsl(0 0% 0% / 4%);
--shadow-md: 0px 0px 10px 4px hsl(0 0% 0% / 10%);
--shadow-lg: 0px 0px 20px 8px hsl(0 0% 0% / 20%);
/* Border radius */
--rounded-4: 4px;
--rounded-20: 20px;
/* Media Queryies - Not yet supported in CSS */
/*
--xsmall: 340px;
--small: 500px;
--large: 960px;
--wide: 1200px;
*/
}
html {
font-size: 62.5%;
box-sizing: border-box;
scrollbar-width: thin;
}
html,
body {
height: 100%;
}
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-thumb {
background-color: var(--color-brand);
border-radius: var(--radius-base);
}
::selection {
background: var(--primary);
color: var(--white);
}
body {
font-family: var(--font-serif);
font-size: var(--font-18);
color: var(--color-text-primary);
// background-color: var(--color-bg-primary);
background-color: #2d3a3a;
// background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 100 60'%3E%3Cg %3E%3Crect fill='%23555555' width='11' height='11'/%3E%3Crect fill='%23565656' x='10' width='11' height='11'/%3E%3Crect fill='%23575757' y='10' width='11' height='11'/%3E%3Crect fill='%23575757' x='20' width='11' height='11'/%3E%3Crect fill='%23585858' x='10' y='10' width='11' height='11'/%3E%3Crect fill='%23595959' y='20' width='11' height='11'/%3E%3Crect fill='%235a5a5a' x='30' width='11' height='11'/%3E%3Crect fill='%235b5b5b' x='20' y='10' width='11' height='11'/%3E%3Crect fill='%235c5c5c' x='10' y='20' width='11' height='11'/%3E%3Crect fill='%235c5c5c' y='30' width='11' height='11'/%3E%3Crect fill='%235d5d5d' x='40' width='11' height='11'/%3E%3Crect fill='%235e5e5e' x='30' y='10' width='11' height='11'/%3E%3Crect fill='%235f5f5f' x='20' y='20' width='11' height='11'/%3E%3Crect fill='%23606060' x='10' y='30' width='11' height='11'/%3E%3Crect fill='%23616161' y='40' width='11' height='11'/%3E%3Crect fill='%23626262' x='50' width='11' height='11'/%3E%3Crect fill='%23626262' x='40' y='10' width='11' height='11'/%3E%3Crect fill='%23636363' x='30' y='20' width='11' height='11'/%3E%3Crect fill='%23646464' x='20' y='30' width='11' height='11'/%3E%3Crect fill='%23656565' x='10' y='40' width='11' height='11'/%3E%3Crect fill='%23666666' y='50' width='11' height='11'/%3E%3Crect fill='%23676767' x='60' width='11' height='11'/%3E%3Crect fill='%23686868' x='50' y='10' width='11' height='11'/%3E%3Crect fill='%23686868' x='40' y='20' width='11' height='11'/%3E%3Crect fill='%23696969' x='30' y='30' width='11' height='11'/%3E%3Crect fill='%236a6a6a' x='20' y='40' width='11' height='11'/%3E%3Crect fill='%236b6b6b' x='10' y='50' width='11' height='11'/%3E%3Crect fill='%236c6c6c' x='70' width='11' height='11'/%3E%3Crect fill='%236d6d6d' x='60' y='10' width='11' height='11'/%3E%3Crect fill='%236e6e6e' x='50' y='20' width='11' height='11'/%3E%3Crect fill='%236e6e6e' x='40' y='30' width='11' height='11'/%3E%3Crect fill='%236f6f6f' x='30' y='40' width='11' height='11'/%3E%3Crect fill='%23707070' x='20' y='50' width='11' height='11'/%3E%3Crect fill='%23717171' x='80' width='11' height='11'/%3E%3Crect fill='%23727272' x='70' y='10' width='11' height='11'/%3E%3Crect fill='%23737373' x='60' y='20' width='11' height='11'/%3E%3Crect fill='%23747474' x='50' y='30' width='11' height='11'/%3E%3Crect fill='%23747474' x='40' y='40' width='11' height='11'/%3E%3Crect fill='%23757575' x='30' y='50' width='11' height='11'/%3E%3Crect fill='%23767676' x='90' width='11' height='11'/%3E%3Crect fill='%23777777' x='80' y='10' width='11' height='11'/%3E%3Crect fill='%23787878' x='70' y='20' width='11' height='11'/%3E%3Crect fill='%23797979' x='60' y='30' width='11' height='11'/%3E%3Crect fill='%237a7a7a' x='50' y='40' width='11' height='11'/%3E%3Crect fill='%237b7b7b' x='40' y='50' width='11' height='11'/%3E%3Crect fill='%237c7c7c' x='90' y='10' width='11' height='11'/%3E%3Crect fill='%237c7c7c' x='80' y='20' width='11' height='11'/%3E%3Crect fill='%237d7d7d' x='70' y='30' width='11' height='11'/%3E%3Crect fill='%237e7e7e' x='60' y='40' width='11' height='11'/%3E%3Crect fill='%237f7f7f' x='50' y='50' width='11' height='11'/%3E%3Crect fill='%23808080' x='90' y='20' width='11' height='11'/%3E%3Crect fill='%23818181' x='80' y='30' width='11' height='11'/%3E%3Crect fill='%23828282' x='70' y='40' width='11' height='11'/%3E%3Crect fill='%23838383' x='60' y='50' width='11' height='11'/%3E%3Crect fill='%23848484' x='90' y='30' width='11' height='11'/%3E%3Crect fill='%23848484' x='80' y='40' width='11' height='11'/%3E%3Crect fill='%23858585' x='70' y='50' width='11' height='11'/%3E%3Crect fill='%23868686' x='90' y='40' width='11' height='11'/%3E%3Crect fill='%23878787' x='80' y='50' width='11' height='11'/%3E%3Crect fill='%23888888' x='90' y='50' width='11' height='11'/%3E%3C/g%3E%3C/svg%3E");
background-attachment: fixed;
background-size: cover;
}
a {
text-decoration: none;
color: var(--color-text-primary);
}
label {
display: block;
margin: var(--spacing-8) 0;
font-size: var(--font-24);
color: var(--color-text-muted);
}
input {
padding: var(--spacing-8);
font-size: var(--font-16);
border-radius: var(--radius-base);
border: none;
}
.btn {
padding: var(--spacing-16) var(--spacing-32);
font-size: var(--font-18);
font-weight: bold;
color: var(--color-text-primary);
background-color: var(--color-btn-primary-active);
border-radius: var(--radius-base);
border: none;
cursor: pointer;
}
.btn:hover {
background-color: var(--color-btn-primary-active-hover);
}
.btn:disabled {
color: var(--color-text-muted);
background-color: var(--color-btn-primary-inactive);
cursor: not-allowed;
}
ul,
ol {
list-style: none;
}
/* Utils */
.responsive {
resize: both;
overflow: scroll;
border: 1px solid hsl(0 0% 0%);
}
.placeholder {
padding: var(--spacing-20) 0;
background-color: var(--color-placeholder);
border-radius: var(--radius-base);
}

58
src/styles/reset.scss Normal file
View file

@ -0,0 +1,58 @@
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
iframe,
video {
width: 100%;
border: none;
aspect-ratio: 16 / 9;
}
input,
button,
textarea,
select {
font: inherit;
}
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
button,
input {
font: inherit;
color: inherit;
border: none;
}
button {
font: inherit;
background: none;
cursor: pointer;
}
ul,
ol {
list-style: none;
}

3
src/styles/styles.scss Normal file
View file

@ -0,0 +1,3 @@
@use 'reset.scss';
@use 'global.scss';
@use 'theme.scss';

247
src/styles/theme.scss Normal file
View file

@ -0,0 +1,247 @@
html[data-theme='🌛 Night'] {
/* Global */
--clr-primary: hsl(173 100% 66%);
--clr-txt: hsl(0 0% 98%);
--clr-bg: hsl(210 7% 11%);
--bg: radial-gradient(hsl(173 100% 4%), var(--clr-bg));
--bg-opacity: 0.7;
/* Menu */
--clr-menu-text: hsl(0 0% 78%);
--clr-menu-bg: linear-gradient(180deg, hsl(180 7% 14%) 0%, hsl(216 7% 14%) 100%);
--clr-menu-arrow-bg: hsl(180 7% 14%);
--clr-menu-border: hsl(0 0% 19%);
--clr-theme-txt: hsl(177 100% 15%);
--clr-theme-active: hsl(177 100% 80%);
--clr-switch-on-bg: hsl(0 0% 24%);
--clr-switch-off-bg: hsl(0 0% 10%);
/* Hero */
--clr-hero-txt: hsl(174 27% 73%);
--clr-hero-bg: linear-gradient(270deg, hsl(210 15% 13%) 43%, hsl(176 19% 15%) 66%);
--clr-hero-divider-bg: hsl(0 0% 21%);
--clr-input-txt: hsl(177 100% 15%);
--clr-input-bg: hsl(210 13% 24%);
--clr-input-placeholder-txt: hsl(210 13% 50%);
--clr-input-border: hsl(0 0% 21%);
/* Card */
--clr-card-bg: linear-gradient(180deg, hsl(180 7% 14%) 0%, hsl(216 7% 14%) 100%);
--clr-card-txt: hsl(0 0% 78%);
/* Link */
--clr-link-txt: hsl(0 100% 98%);
--clr-link-background: hsl(0 0% 4%);
/* Footer */
--clr-footer-txt: hsl(0 0% 78%);
--clr-footer-bg: hsl(210 5% 7%);
/* Post */
--post-overlay-bg: radial-gradient(hsl(173 100% 4% / 80%), var(--clr-bg));
--post-blockquote-txt: hsl(173 100% 94%);
--post-blockquote-bg: hsl(173 60% 4%);
--post-blockquote-border: hsl(173 10% 10%);
--clr-code-bg: hsl(173 60% 4%);
--clr-code-title: hsl(173 100% 94%);
--clr-code-border: hsl(173 10% 10%);
--clr-code-line-number: hsl(173 20% 20%);
--clr-code-line-highlight: hsl(173 40% 8%);
--clr-code-inline-txt: hsl(173 100% 94%);
--clr-code-inline-bg: hsl(173 60% 4%);
--clr-token-1: hsl(173 100% 66%);
--clr-token-2: hsl(180 60% 80%);
--clr-token-3: hsl(173 100% 66%);
--clr-token-4: hsl(0 0% 98%);
--clr-token-5: hsl(173 10% 60%);
}
html[data-theme='☀️ Daylight'] {
/* Global */
--clr-primary: hsl(220 100% 50%);
--clr-txt: hsl(220 10% 10%);
--clr-bg: hsl(0 0% 98%);
--bg: radial-gradient(hsl(0 0% 98%), var(--clr-bg));
--bg-opacity: 0.7;
/* Menu */
--clr-menu-text: hsl(220 10% 10%);
--clr-menu-bg: linear-gradient(180deg, hsl(0 0% 98%) 0%, hsl(0 0% 94%) 100%);
--clr-menu-arrow-bg: hsl(0 0% 98%);
--clr-menu-border: hsl(0 0% 80%);
--clr-theme-txt: hsl(0 0% 98%);
--clr-theme-active: hsl(220 100% 60%);
--clr-switch-on-bg: hsl(220 40% 90%);
--clr-switch-off-bg: hsl(220 40% 80%);
/* Hero */
--clr-hero-txt: hsl(220 10% 40%);
--clr-hero-bg: linear-gradient(270deg, hsl(0 0% 94%) 43%, hsl(0 0% 98%) 66%);
--clr-hero-divider-bg: hsl(0 0% 80%);
--clr-input-txt: hsl(220 10% 98%);
--clr-input-bg: hsl(0 0% 98%);
--clr-input-placeholder-txt: hsl(220 10% 60%);
--clr-input-border: hsl(0 0% 80%);
/* Card */
--clr-card-bg: linear-gradient(180deg, hsl(0 0% 98%) 0%, hsl(0 0% 94%) 100%);
--clr-card-txt: hsl(220 10% 40%);
/* Link */
--clr-link-txt: hsl(220 10% 10%);
--clr-link-background: hsl(0 0% 100%);
/* Footer */
--clr-footer-txt: hsl(220 10% 10%);
--clr-footer-bg: hsl(0 0% 98%);
/* Post */
--post-overlay-bg: radial-gradient(hsl(0 0% 100% / 60%), var(--clr-bg));
--post-blockquote-txt: hsl(0 0% 40%);
--post-blockquote-bg: hsl(0 0% 98%);
--post-blockquote-border: hsl(0 0% 84%);
--clr-code-bg: hsl(0 0% 98%);
--clr-code-title: hsl(0 0% 40%);
--clr-code-border: hsl(0 0% 84%);
--clr-code-line-number: hsl(0 0% 60%);
--clr-code-line-highlight: hsl(0 0% 94%);
--clr-code-inline-txt: hsl(0 0% 98%);
--clr-code-inline-bg: hsl(0 0% 20%);
--clr-token-1: hsl(220 100% 50%);
--clr-token-2: hsl(220 60% 50%);
--clr-token-3: hsl(220 100% 50%);
--clr-token-4: hsl(0 0% 20%);
--clr-token-5: hsl(0 0% 60%);
}
html[data-theme='🐺 Night Howl'] {
/* Global */
--clr-primary: hsl(207 100% 94%);
--clr-txt: hsl(210 40% 80%);
--clr-bg: hsl(210 47% 12%);
--bg: radial-gradient(hsl(206 50% 11%), var(--clr-bg));
--bg-opacity: 0.7;
/* Menu */
--clr-menu-text: hsl(210 40% 80%);
--clr-menu-bg: linear-gradient(180deg, hsl(210 47% 14%) 0%, hsl(210 47% 12%) 100%);
--clr-menu-arrow-bg: hsl(210 47% 14%);
--clr-menu-border: hsl(210 40% 20%);
--clr-theme-txt: hsl(210 40% 20%);
--clr-theme-active: hsl(210 40% 80%);
--clr-switch-on-bg: hsl(210 40% 60%);
--clr-switch-off-bg: hsl(210 40% 20%);
/* Hero */
--clr-hero-txt: hsl(210 40% 60%);
--clr-hero-bg: linear-gradient(270deg, hsl(210 47% 12%) 43%, hsl(210 47% 14%) 66%);
--clr-hero-divider-bg: hsl(210 40% 20%);
--clr-input-txt: hsl(210 40% 20%);
--clr-input-bg: hsl(210 40% 16%);
--clr-input-placeholder-txt: hsl(210 40% 60%);
--clr-input-border: hsl(210 40% 20%);
/* Card */
--clr-card-bg: linear-gradient(180deg, hsl(210 47% 14%) 0%, hsl(210 47% 12%) 100%);
--clr-card-txt: hsl(210 40% 60%);
/* Link */
--clr-link-txt: hsl(210 40% 80%);
--clr-link-background: hsl(210 40% 4%);
/* Footer */
--clr-footer-txt: hsl(210 40% 80%);
--clr-footer-bg: hsl(210 47% 12%);
/* Post */
--post-overlay-bg: radial-gradient(hsl(206 50% 11% / 60%), var(--clr-bg));
--post-blockquote-txt: hsl(210 47% 80%);
--post-blockquote-bg: hsl(210 47% 14%);
--post-blockquote-border: hsl(210 40% 20%);
--clr-code-bg: hsl(210 47% 14%);
--clr-code-title: hsl(210 40% 60%);
--clr-code-border: hsl(210 40% 20%);
--clr-code-line-number: hsl(210 40% 40%);
--clr-code-line-highlight: hsl(210 40% 16%);
--clr-code-inline-txt: hsl(210 47% 98%);
--clr-code-inline-bg: hsl(210 47% 10%);
--clr-token-1: hsl(180 100% 43%);
--clr-token-2: hsl(197 100% 49%);
--clr-token-3: hsl(180 100% 43%);
--clr-token-4: hsl(206 50% 98%);
--clr-token-5: hsl(217 10% 64%);
}
html[data-theme='🧠 Night Mind'] {
/* Global */
--clr-primary: hsl(9 100% 84%);
--clr-txt: hsl(265 28% 98%);
--clr-bg: hsl(265 28% 10%);
--bg: radial-gradient(hsl(265 28% 20%), var(--clr-bg));
--bg-opacity: 0.7;
/* Menu */
--clr-menu-text: hsl(265 28% 98%);
--clr-menu-bg: linear-gradient(180deg, hsl(265 28% 20%) 0%, hsl(265 28% 18%) 100%);
--clr-menu-arrow-bg: hsl(265 28% 20%);
--clr-menu-border: hsl(265 28% 24%);
--clr-theme-txt: hsl(265 28% 20%);
--clr-theme-active: hsl(9 100% 90%);
--clr-switch-on-bg: hsl(265 28% 30%);
--clr-switch-off-bg: hsl(265 28% 24%);
/* Hero */
--clr-hero-txt: hsl(265 28% 80%);
--clr-hero-bg: linear-gradient(270deg, hsl(265 28% 20%) 43%, hsl(265 28% 24%) 66%);
--clr-hero-divider-bg: hsl(265 28% 24%);
--clr-input-txt: hsl(265 28% 20%);
--clr-input-bg: hsl(265 28% 24%);
--clr-input-placeholder-txt: hsl(265 28% 80%);
--clr-input-border: hsl(265 28% 26%);
/* Card */
--clr-card-bg: linear-gradient(180deg, hsl(265 28% 22%) 0%, hsl(265 28% 20%) 100%);
--clr-card-txt: hsl(265 28% 80%);
/* Link */
--clr-link-txt: hsl(265 28% 98%);
--clr-link-background: hsl(210 40% 4%);
/* Footer */
--clr-footer-txt: hsl(265 28% 98%);
--clr-footer-bg: hsl(265 28% 10%);
/* Post */
--post-overlay-bg: radial-gradient(hsl(265 28% 20% / 60%), var(--clr-bg));
--post-blockquote-txt: hsl(265 28% 90%);
--post-blockquote-bg: hsl(265 28% 10%);
--post-blockquote-border: hsl(265 28% 18%);
--clr-code-bg: hsl(265 28% 10%);
--clr-code-title: hsl(265 28% 80%);
--clr-code-border: hsl(265 28% 18%);
--clr-code-line-number: hsl(265 20% 34%);
--clr-code-line-highlight: hsl(265 28% 14%);
--clr-code-inline-txt: hsl(265 28% 98%);
--clr-code-inline-bg: hsl(265 28% 10%);
--clr-token-1: hsl(238 80% 80%);
--clr-token-2: hsl(205 80% 80%);
--clr-token-3: hsl(358 80% 80%);
--clr-token-4: hsl(256 80% 98%);
--clr-token-5: hsl(265 10% 60%);
}

View file

@ -1,29 +1,22 @@
import adapter from '@sveltejs/adapter-auto'; import adapter from '@sveltejs/adapter-auto';
import preprocess from 'svelte-preprocess'; import preprocess from 'svelte-preprocess';
import path from 'path';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
// Consult https://github.com/sveltejs/svelte-preprocess // Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors // for more information about preprocessors
preprocess: preprocess({ preprocess: preprocess(),
scss: {}, kit: {
}), adapter: adapter(),
alias: {
kit: { $components: 'src/components',
adapter: adapter(), $root: './src'
vite: { },
resolve: { // Override http methods in the Todo forms
alias: { methodOverride: {
$root: path.resolve('./src') allowed: ['PATCH', 'DELETE']
} }
} }
},
// Override http methods in the Todo forms
methodOverride: {
allowed: ['PATCH', 'DELETE']
}
}
}; };
export default config; export default config;

View file

@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
test('about page has expected h1', async ({ page }) => { test('about page has expected h1', async ({ page }) => {
await page.goto('/about'); await page.goto('/about');
expect(await page.textContent('h1')).toBe('About this app'); expect(await page.textContent('h1')).toBe('About this app');
}); });

View file

@ -1,20 +1,13 @@
{ {
"extends": "./.svelte-kit/tsconfig.json", "extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"lib": [ "resolveJsonModule": true,
"es2020", "skipLibCheck": true,
"DOM" "sourceMap": true,
], "strict": true
"moduleResolution": "node", }
"module": "es2020", }
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "es2020",
}
}

8
vite.config.js Normal file
View file

@ -0,0 +1,8 @@
import { sveltekit } from '@sveltejs/kit/vite';
/** @type {import('vite').UserConfig} */
const config = {
plugins: [sveltekit()]
};
export default config;