Add components and improve error page
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user