Add components and improve error page
This commit is contained in:
1
src/lib/assets/img/stop-error.svg
Normal file
1
src/lib/assets/img/stop-error.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 2496 3495"><path stroke="#000" stroke-miterlimit="10" d="M-29-28h2552v4439H-29z"/><path fill="#fff" stroke="#000" stroke-miterlimit="10" stroke-width=".9" d="M0 0h2495v3494H0z"/><path d="m814 2164-137 2q-2-28-11-46-28-54-120-54-52 0-83 21a65 65 0 0 0-32 56q0 27 18 40 21 17 97 26 158 19 213 65 61 51 61 137 0 97-71 154-71 56-193 56-124 0-199-62-77-62-80-167h140q3 52 41 82 37 29 102 29 56 0 90-23 33-22 33-61 0-17-9-31-20-31-93-40-122-16-167-33a175 175 0 0 1-117-164q0-92 68-146 69-54 181-54 122 0 192 57 72 57 76 156m104-216h126v154h108v101h-108v251q0 34 11 42 11 9 88 9h8v107l-36 1h-89q-61-4-88-37-12-17-16-41t-4-115v-217h-72v-101h72zm508 140q111 0 180 72t69 190q0 122-66 194-67 72-180 72-112 0-180-71-67-70-67-190 0-121 67-194 68-73 177-73m2 106q-57 0-91 44t-34 119q0 70 34 112 35 42 91 42 57 0 90-42 32-42 32-115t-33-117q-33-43-89-43m311-92h122v33q49-47 129-47 102 0 166 74 65 73 65 190 0 119-63 191-63 73-167 73-78 0-124-46v184h-128zm233 92q-66 0-91 51-16 31-16 121 0 61 16 94 25 50 95 50 56 0 88-41 32-42 32-115 0-74-34-117-33-43-90-43"/><circle cx="1247.7" cy="1049.7" r="708.7" fill="#e31837"/><path fill="#1a1a1a" d="M-17 2908h2530v42H-17z"/><path fill="#1a1a1a" d="M978 3051v46H857v77h94v44h-94v79h121v46H799v-292zm39 292v-219h33q9 0 12 3 4 4 5 11l3 22q10-18 23-29t30-11q15 0 24 7l-4 42-3 6-6 1-9-1-12-1-14 2-10 7q-5 4-9 10l-7 14v136zm161 0v-219h33q8 0 12 3 3 4 4 11l3 22q10-18 23-29t31-11q14 0 23 7l-4 42q0 4-3 6l-5 1-10-1-11-1q-8 0-14 2l-11 7q-5 4-8 10l-7 14v136zm252-222q24 0 44 8 20 7 34 22t21 36 8 46-8 47-21 36-34 22-44 8-44-8q-20-7-34-22-15-15-22-36-8-21-8-47t8-46 22-36 34-22 44-8m0 183q25 0 37-18t12-52-12-53-37-17-38 17-12 53 12 52 38 18m145 39v-219h33q8 0 12 3 3 4 4 11l3 22q10-18 23-29 14-11 31-11 14 0 24 7l-5 42q0 4-2 6l-6 1-10-1-11-1q-8 0-14 2l-11 7-8 10-7 14v136z" aria-label="Error" font-family="Carlito" font-size="451" font-weight="700" style="line-height:.9;-inkscape-font-specification:"Carlito Bold";text-align:center;white-space:pre" text-anchor="middle"/></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
82
src/lib/components/ui/Button.svelte
Normal file
82
src/lib/components/ui/Button.svelte
Normal file
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLButtonAttributes, HTMLAnchorAttributes } from 'svelte/elements';
|
||||
type ButtonColor = 'brand' | 'accent' | 'bg';
|
||||
|
||||
interface Props {
|
||||
children: import('svelte').Snippet;
|
||||
href?: string;
|
||||
color?: ButtonColor;
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
let { children, href, color = 'bg', onclick, ...rest }: Props = $props();
|
||||
|
||||
const isLink = $derived(!!href);
|
||||
const isExternal = $derived(href?.startsWith('http'));
|
||||
</script>
|
||||
|
||||
{#if isLink}
|
||||
<a
|
||||
{href}
|
||||
class="btn {color}"
|
||||
target={isExternal ? '_blank' : undefined}
|
||||
rel={isExternal ? 'noopener noreferrer' : undefined}
|
||||
{...rest}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button class="btn {color}" {onclick} {...rest}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.4rem 1.2rem;
|
||||
width: fit-content;
|
||||
min-width: 90px;
|
||||
min-height: 48px;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-family: 'URW Gothic', sans-serif;
|
||||
letter-spacing: 0.05ch;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
box-shadow: var(--shadow-std);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.accent {
|
||||
background-color: var(--color-accent);
|
||||
color: var(--color-title);
|
||||
}
|
||||
|
||||
.brand {
|
||||
background-color: var(--color-brand);
|
||||
color: rgb(30, 30, 30);
|
||||
}
|
||||
|
||||
.bg {
|
||||
background-color: var(--color-bg-light);
|
||||
color: var(--color-title);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
filter: brightness(1.5);
|
||||
}
|
||||
|
||||
.btn.active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
</style>
|
||||
94
src/lib/components/ui/Textbox.svelte
Normal file
94
src/lib/components/ui/Textbox.svelte
Normal file
@@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import type { HTLMInputAttributes } from 'svelte/elements';
|
||||
|
||||
interface Props extends HTMLInputAttributes {
|
||||
value?: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
type?: 'text' | 'password' | 'email' | 'number' | 'search' | 'tel' | 'url';
|
||||
error?: string;
|
||||
uppercase?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
let {
|
||||
value = $bindable(''),
|
||||
label,
|
||||
placeholder = '',
|
||||
type = 'text',
|
||||
error = '',
|
||||
uppercase = false,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
|
||||
let isFocussed = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="input-wrapper" class:focussed={isFocussed} class:has-error={!!error}>
|
||||
{#if label}
|
||||
<label for="adaptive-input">{label}</label>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
id="adaptive-input"
|
||||
class:all-caps={uppercase}
|
||||
{type}
|
||||
{placeholder}
|
||||
bind:value={value}
|
||||
onfocus={() => isFocussed = true}
|
||||
onblur={() => isFocussed = false}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
{#if error}
|
||||
<span class="error-message" transition:fade>{error}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
font-family: 'URW Gothic', sans-serif;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 400;
|
||||
color: var(--color-title)
|
||||
}
|
||||
|
||||
input {
|
||||
min-height: 48px;
|
||||
padding: 0 16px;
|
||||
background-color: var(--color-title);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 20px;
|
||||
color: var(--color-bg-dark);
|
||||
font-size: 1.5rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.all-caps {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.focussed input {
|
||||
border-color: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.has-error input {
|
||||
border-color: #ff4d4d;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4d4d;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
94
src/lib/components/ui/cards/BaseCard.svelte
Normal file
94
src/lib/components/ui/cards/BaseCard.svelte
Normal file
@@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { IconHelpCircle } from '@tabler/icons-svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
header?: string;
|
||||
helpText?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
children,
|
||||
header = "",
|
||||
helpText,
|
||||
}: Props = $props();
|
||||
|
||||
let showHelp = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
{#if header || helpText}
|
||||
<header class="card-header">
|
||||
<div class="header-content">
|
||||
{header}
|
||||
</div>
|
||||
{#if helpText}
|
||||
<button
|
||||
type="button"
|
||||
class="help-toggle"
|
||||
onclick={() => showHelp = !showHelp}
|
||||
aria-label="Show Help"
|
||||
>
|
||||
<IconHelpCircle size={26} stroke={2.25} color={showHelp ? 'var(--color-brand)' : 'var(--color-title)'} />
|
||||
</button>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{#if showHelp && helpText}
|
||||
<div class="help-drawer" transition:slide={{ duration: 400 }}>
|
||||
<p>{helpText}</p>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="card-body">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
background: var(--color-accent);
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
width: 95%;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
font-family: 'URW Gothic', sans-serif;
|
||||
color: var(--color-title);
|
||||
}
|
||||
|
||||
.header-content { flex: 1;
|
||||
font-size: 1.5rem; font-weight: 600; }
|
||||
|
||||
.help-toggle {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
cursor: help;
|
||||
opacity: 0.6;
|
||||
z-index: 2;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.help-toggle:hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.help-drawer {
|
||||
background-color: var(--color-accent);
|
||||
padding: 4px 16px;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.2;
|
||||
margin: auto;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
color: var(--color-title);
|
||||
}
|
||||
</style>
|
||||
@@ -4,78 +4,79 @@
|
||||
* Copyright (c) 2014,2015 by (URW)++ Design & Development
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'URW Gothic';
|
||||
src: url('/type/urwgothic/urwgothic-book-webfont.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'URW Gothic';
|
||||
src: url('/type/urwgothic/urwgothic-book-webfont.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* * URW Gothic is licensed under the SIL Open Font License, Version 1.1.
|
||||
* Copyright (c) 2014,2015 by (URW)++ Design & Development
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'URW Gothic';
|
||||
src: url('/type/urwgothic/urwgothic-bookoblique-webfont.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
font-family: 'URW Gothic';
|
||||
src: url('/type/urwgothic/urwgothic-bookoblique-webfont.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* * URW Gothic is licensed under the SIL Open Font License, Version 1.1.
|
||||
* Copyright (c) 2014,2015 by (URW)++ Design & Development
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'URW Gothic';
|
||||
src: url('/type/urwgothic/urwgothic-demi-webfont.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'URW Gothic';
|
||||
src: url('/type/urwgothic/urwgothic-demi-webfont.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* * URW Gothic is licensed under the SIL Open Font License, Version 1.1.
|
||||
* Copyright (c) 2014,2015 by (URW)++ Design & Development
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'URW Gothic';
|
||||
src: url('/type/urwgothic/urwgothic-demioblique-webfont.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
font-family: 'URW Gothic';
|
||||
src: url('/type/urwgothic/urwgothic-demioblique-webfont.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Brand Colours */
|
||||
--color-brand: #4fd1d1;
|
||||
--color-accent: #3c6f79;
|
||||
--color-title: #ebebeb;
|
||||
--color-bg-light: #404c55;
|
||||
--color-bg-dark: #2b343c;
|
||||
|
||||
/* Shadows */
|
||||
--color-shadow: hsla(210, 20%, 5%, 0.35);
|
||||
--shadow-std: 0 4px 12px var(--color-shadow);
|
||||
--shadow-up: 0 -4px 12px var(--color-shadow);
|
||||
}
|
||||
:root {
|
||||
/* Brand Colours */
|
||||
--color-brand: #4fd1d1;
|
||||
--color-accent: #3c6f79;
|
||||
--color-title: #ebebeb;
|
||||
--color-bg-light: #404c55;
|
||||
--color-bg-dark: #2b343c;
|
||||
|
||||
/* Shadows */
|
||||
--color-shadow: hsla(210, 20%, 5%, 0.35);
|
||||
--shadow-std: 0 4px 12px var(--color-shadow);
|
||||
--shadow-up: 0 -4px 12px var(--color-shadow);
|
||||
--shadow-right: 4px 0 12px var(--color-shadow);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--color-accent);
|
||||
color: white;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-mos-osx-font-smoothing: grayscale;
|
||||
overflow-x: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--color-accent);
|
||||
color: white;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-mos-osx-font-smoothing: grayscale;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-brand);
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: var(--color-brand);
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user