This commit is contained in:
nub31
2026-03-16 23:41:41 +01:00
parent 8ad6fda412
commit f686c4a7d2
11 changed files with 43 additions and 752 deletions

8
package-lock.json generated
View File

@@ -7,17 +7,15 @@
"": { "": {
"name": "minibusservice.no", "name": "minibusservice.no",
"version": "0.0.0", "version": "0.0.0",
"dependencies": {
"leaflet": "^1.9.4",
"tailwind-merge": "^3.5.0"
},
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^7.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tailwindcss/vite": "^0.0.0-insiders.aaaefe8", "@tailwindcss/vite": "^0.0.0-insiders.aaaefe8",
"@tsconfig/svelte": "^5.0.8", "@tsconfig/svelte": "^5.0.8",
"@types/node": "^24.12.0", "@types/node": "^24.12.0",
"leaflet": "^1.9.4",
"svelte": "^5.53.7", "svelte": "^5.53.7",
"svelte-check": "^4.4.5", "svelte-check": "^4.4.5",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^0.0.0-insiders.aaaefe8", "tailwindcss": "^0.0.0-insiders.aaaefe8",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^8.0.0" "vite": "^8.0.0"
@@ -1207,6 +1205,7 @@
"version": "1.9.4", "version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"dev": true,
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/lightningcss": { "node_modules/lightningcss": {
@@ -1703,6 +1702,7 @@
"version": "3.5.0", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
"integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",

View File

@@ -14,14 +14,12 @@
"@tailwindcss/vite": "^0.0.0-insiders.aaaefe8", "@tailwindcss/vite": "^0.0.0-insiders.aaaefe8",
"@tsconfig/svelte": "^5.0.8", "@tsconfig/svelte": "^5.0.8",
"@types/node": "^24.12.0", "@types/node": "^24.12.0",
"leaflet": "^1.9.4",
"svelte": "^5.53.7", "svelte": "^5.53.7",
"svelte-check": "^4.4.5", "svelte-check": "^4.4.5",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^0.0.0-insiders.aaaefe8", "tailwindcss": "^0.0.0-insiders.aaaefe8",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^8.0.0" "vite": "^8.0.0"
},
"dependencies": {
"leaflet": "^1.9.4",
"tailwind-merge": "^3.5.0"
} }
} }

View File

@@ -1,42 +0,0 @@
<script lang="ts">
import Button from "./button/Button.svelte";
import { twMerge } from "tailwind-merge";
export let maxItems: number;
let pageParam = $page.url.searchParams.get("page");
let currentPage = 1;
if (pageParam) {
let pageParamAsNumber = Number(pageParam);
if (pageParamAsNumber) {
currentPage = pageParamAsNumber;
}
}
</script>
<section
class={twMerge("mt-8 flex justify-center gap-5", $$restProps["class"])}
>
{#if currentPage > 1}
<a
on:click={() =>
setTimeout(() => {
currentPage = currentPage - 1;
}, 10)}
href={`?page=${currentPage - 1}`}
>
<Button>Forrige</Button>
</a>
{/if}
{#if (currentPage + 1) * 10 - 10 < maxItems}
<a
on:click={() =>
setTimeout(() => {
currentPage = currentPage + 1;
}, 10)}
href={`?page=${currentPage + 1}`}
>
<Button>Neste</Button>
</a>
{/if}
</section>

View File

@@ -1,322 +0,0 @@
<script lang="ts">
import Link from './link/Link.svelte';
</script>
<h2 class="mb-1 text-xl font-semibold">Innledning</h2>
<p class="mb-2">
Dette kjøpet er regulert av de nedenstående standard salgsbetingelser for forbrukerkjøp av varer
over Internett. Forbrukerkjøp over internett reguleres hovedsakelig av avtaleloven,
forbrukerkjøpsloven, markedsføringsloven, angrerettloven og ehandelsloven, og disse lovene gir
forbrukeren ufravikelige rettigheter. Lovene er tilgjengelig på
<Link href="www.lovdata.no">www.lovdata.no</Link>. Vilkårene i denne avtalen skal ikke forstås som
noen begrensning i de lovbestemte rettighetene, men oppstiller partenes viktigste rettigheter og
plikter for handelen.
</p>
<p class="mb-2">
Salgsbetingelsene er utarbeidet og anbefalt av Forbrukertilsynet. For en bedre forståelse av disse
salgsbetingelsene, se Forbrukertilsynets veileder her.
</p>
<h2 class="mb-1 text-xl font-semibold">1. Avtalen</h2>
<p class="mb-2">
Avtalen består av disse salgsbetingelsene, opplysninger gitt i bestillingsløsningen og eventuelt
særskilt avtalte vilkår. Ved eventuell motstrid mellom opplysningene, går det som særskilt er
avtalt mellom partene foran, så fremt det ikke strider mot ufravikelig lovgivning.
</p>
<p class="mb-2">
Avtalen vil i tillegg bli utfylt av relevante lovbestemmelser som regulerer kjøp av varer mellom
næringsdrivende og forbrukere.
</p>
<h2 class="mb-1 text-xl font-semibold">2. Partene</h2>
<p class="mb-2">
Selger er Tor Stian Stene, adresse: Solnørdalsvegen 40, 6240 Ørskog, e-post:
minibusservice@hotmail.com, tlf: 45266161, org.nr: 816 230 942, og betegnes i det følgende som
selgeren.
</p>
<p class="mb-2">
Kjøper er den forbrukeren som foretar bestillingen, og betegnes i det følgende som kjøperen.
</p>
<h2 class="mb-1 text-xl font-semibold">3. Pris</h2>
<p class="mb-2">
Den oppgitte prisen for varen og tjenester er den totale prisen kjøper skal betale. Denne prisen
inkluderer alle avgifter og tilleggskostnader. Ytterligere kostnader som selger før kjøpet ikke
har informert om, skal kjøper ikke bære.
</p>
<h2 class="mb-1 text-xl font-semibold">4. Avtaleinngåelse</h2>
<p class="mb-2">
Avtalen er bindende for begge parter når kjøperen har sendt sin bestilling til selgeren.
</p>
<p class="mb-2">
Avtalen er likevel ikke bindende hvis det har forekommet skrive- eller tastefeil i tilbudet fra
selgeren i bestillingsløsningen i nettbutikken eller i kjøperens bestilling, og den annen part
innså eller burde ha innsett at det forelå en slik feil.
</p>
<h2 class="mb-1 text-xl font-semibold">5. Betalingen</h2>
<p class="mb-2">
Selgeren kan kreve betaling for varen fra det tidspunkt den blir sendt fra selgeren til kjøperen.
</p>
<p class="mb-2">
Dersom kjøperen bruker kredittkort eller debetkort ved betaling, kan selgeren reservere
kjøpesummen på kortet ved bestilling. Kortet blir belastet samme dag som varen sendes.
</p>
<p class="mb-2">
Ved betaling med faktura, blir fakturaen til kjøperen utstedt ved forsendelse av varen.
Betalingsfristen fremgår av fakturaen og er på minimum 14 dager fra mottak.
</p>
<p class="mb-2">Kjøpere under 18 år kan ikke betale med etterfølgende faktura.</p>
<h2 class="mb-1 text-xl font-semibold">6. Levering</h2>
<p class="mb-2">Levering er skjedd når kjøperen, eller hans representant, har overtatt tingen.</p>
<p class="mb-2">
Hvis ikke leveringstidspunkt fremgår av bestillingsløsningen, skal selgeren levere varen til
kjøper uten unødig opphold og senest 30 dager etter bestillingen fra kunden. Varen skal leveres
hos kjøperen med mindre annet er særskilt avtalt mellom partene.
</p>
<h2 class="mb-1 text-xl font-semibold">7. Risikoen for varen</h2>
<p class="mb-2">
Risikoen for varen går over på kjøper når han, eller kjøpers representant, har fått varene levert
i tråd med punkt 6.
</p>
<h2 class="mb-1 text-xl font-semibold">8. Angrerett</h2>
<p class="mb-2">
Med mindre avtalen er unntatt fra angrerett, kan kjøperen angre kjøpet av varen i henhold til
angrerettloven.
</p>
<p class="mb-2">
Kjøperen må gi selger melding om bruk av angreretten innen 14 dager fra fristen begynner å løpe. I
fristen inkluderes alle kalenderdager. Dersom fristen ender på en lørdag, helligdag eller
høytidsdag forlenges fristen til nærmeste virkedag.
</p>
<p class="mb-2">
Angrefristen anses overholdt dersom melding er sendt før utløpet av fristen. Kjøper har
bevisbyrden for at angreretten er blitt gjort gjeldende, og meldingen bør derfor skje skriftlig
(angrerettskjema, e-post eller brev).
</p>
<p class="mb-2">Angrefristen begynner å løpe:</p>
<ul>
<li>
Ved kjøp av enkeltstående varer vil angrefristen løpe fra dagen etter varen(e) er mottatt.
</li>
<li>
Selges et abonnement, eller innebærer avtalen regelmessig levering av identiske varer, løper
fristen fra dagen etter første forsendelse er mottatt.
</li>
<li>
Består kjøpet av flere leveranser, vil angrefristen løpe fra dagen etter siste leveranse er
mottatt.
</li>
</ul>
<p class="mb-2">
Angrefristen utvides til 12 måneder etter utløpet av den opprinnelige fristen dersom selger ikke
før avtaleinngåelsen opplyser om at det foreligger angrerett og standardisert angreskjema.
Tilsvarende gjelder ved manglende opplysning om vilkår, tidsfrister og fremgangsmåte for å benytte
angreretten. Sørger den næringsdrivende for å gi opplysningene i løpet av disse 12 månedene,
utløper angrefristen likevel 14 dager etter den dagen kjøperen mottok opplysningene.
</p>
<p class="mb-2">
Ved bruk av angreretten må varen leveres tilbake til selgeren uten unødig opphold og senest 14
dager fra melding om bruk av angreretten er gitt. Kjøper dekker de direkte kostnadene ved å
returnere varen, med mindre annet er avtalt eller selger har unnlatt å opplyse om at kjøper skal
dekke returkostnadene. Selgeren kan ikke fastsette gebyr for kjøperens bruk av angreretten.
</p>
<p class="mb-2">
Kjøper kan prøve eller teste varen på en forsvarlig måte for å fastslå varens art, egenskaper og
funksjon, uten at angreretten faller bort. Dersom prøving eller test av varen går utover hva som
er forsvarlig og nødvendig, kan kjøperen bli ansvarlig for eventuell redusert verdi på varen.
</p>
<p class="mb-2">
Selgeren er forpliktet til å tilbakebetale kjøpesummen til kjøperen uten unødig opphold, og senest
14 dager fra selgeren fikk melding om kjøperens beslutning om å benytte angreretten. Selger har
rett til å holde tilbake betalingen til han/hun har mottatt varene fra kjøperen, eller til kjøper
har lagt frem dokumentasjon for at varene er sendt tilbake.
</p>
<h2 class="mb-1 text-xl font-semibold">
9. Forsinkelse og manglende levering - kjøpernes rettigheter og frist for å melde krav
</h2>
<p class="mb-2">
Dersom selgeren ikke leverer varen eller leverer den for sent i henhold til avtalen mellom
partene, og dette ikke skyldes kjøperen eller forhold på kjøperens side, kan kjøperen i henhold
til reglene i forbrukerkjøpslovens kapittel 5 etter omstendighetene holde kjøpesummen tilbake,
kreve oppfyllelse, heve avtalen og/eller kreve erstatning fra selgeren.
</p>
<p class="mb-2">
Ved krav om misligholdsbeføyelser bør meldingen av bevishensyn være skriftlig (for eksempel
e-post).
</p>
<h3 class="mt-1 text-lg font-semibold">Oppfyllelse</h3>
<p class="mb-2">
Kjøper kan fastholde kjøpet og kreve oppfyllelse fra selger. Kjøper kan imidlertid ikke kreve
oppfyllelse dersom det foreligger en hindring som selgeren ikke kan overvinne, eller dersom
oppfyllelse vil medføre en så stor ulempe eller kostnad for selger at det står i vesentlig
misforhold til kjøperens interesse i at selgeren oppfyller. Skulle vanskene falle bort innen
rimelig tid, kan kjøper likevel kreve oppfyllelse.
</p>
<p class="mb-2">
Kjøperen taper sin rett til å kreve oppfyllelse om han eller hun venter urimelig lenge med å
fremme kravet.
</p>
<h3 class="mt-1 text-lg font-semibold">Heving</h3>
<p class="mb-2">
Dersom selgeren ikke leverer varen på leveringstidspunktet, skal kjøperen oppfordre selger til å
levere innen en rimelig tilleggsfrist for oppfyllelse. Dersom selger ikke leverer varen innen
tilleggsfristen, kan kjøperen heve kjøpet.
</p>
<p class="mb-2">
Kjøper kan imidlertid heve kjøpet umiddelbart hvis selger nekter å levere varen. Tilsvarende
gjelder dersom levering til avtalt tid var avgjørende for inngåelsen av avtalen, eller dersom
kjøperen har underrettet selger om at leveringstidspunktet er avgjørende.
</p>
<p class="mb-2">
Leveres tingen etter tilleggsfristen forbrukeren har satt eller etter leveringstidspunktet som var
avgjørende for inngåelsen av avtalen, må krav om heving gjøres gjeldende innen rimelig tid etter
at kjøperen fikk vite om leveringen.
</p>
<h3 class="mt-1 text-lg font-semibold">Erstatning</h3>
<p class="mb-2">
Kjøperen kan kreve erstatning for lidt tap som følge av forsinkelsen. Dette gjelder imidlertid
ikke dersom selgeren godtgjør at forsinkelsen skyldes hindring utenfor selgers kontroll som ikke
med rimelighet kunne blitt tatt i betraktning på avtaletiden, unngått, eller overvunnet følgene
av.
</p>
<h2 class="mb-1 text-xl font-semibold">
10. Mangel ved varen - kjøperens rettigheter og reklamasjonsfrist
</h2>
<p class="mb-2">
Hvis det foreligger en mangel ved varen må kjøper innen rimelig tid etter at den ble oppdaget
eller burde ha blitt oppdaget, gi selger melding om at han eller hun vil påberope seg mangelen.
Kjøper har alltid reklamert tidsnok dersom det skjer innen 2 mnd. fra mangelen ble oppdaget eller
burde blitt oppdaget. Reklamasjon kan skje senest to år etter at kjøper overtok varen. Dersom
varen eller deler av den er ment å vare vesentlig lenger enn to år, er reklamasjonsfristen fem år.
</p>
<p class="mb-2">
Dersom varen har en mangel og dette ikke skyldes kjøperen eller forhold på kjøperens side, kan
kjøperen i henhold til reglene i forbrukerkjøpsloven kapittel 6 etter omstendighetene holde
kjøpesummen tilbake, velge mellom retting og omlevering, kreve prisavslag, kreve avtalen hevet
og/eller kreve erstatning fra selgeren.
</p>
<p class="mb-2">Reklamasjon til selgeren bør skje skriftlig.</p>
<h3 class="mt-1 text-lg font-semibold">Retting eller omlevering</h3>
<p class="mb-2">
Kjøperen kan velge mellom å kreve mangelen rettet eller levering av tilsvarende ting. Selger kan
likevel motsette seg kjøperens krav dersom gjennomføringen av kravet er umulig eller volder
selgeren urimelige kostnader. Retting eller omlevering skal foretas innen rimelig tid. Selger har
i utgangspunktet ikke rett til å foreta mer enn to avhjelpsforsøk for samme mangel.
</p>
<h3 class="mt-1 text-lg font-semibold">Prisavslag</h3>
<p class="mb-2">
Kjøper kan kreve et passende prisavslag dersom varen ikke blir rettet eller omlevert. Dette
innebærer at forholdet mellom nedsatt og avtalt pris svarer til forholdet mellom tingens verdi i
mangelfull og kontraktsmessig stand. Dersom særlige grunner taler for det, kan prisavslaget i
stedet settes lik mangelens betydning for kjøperen.
</p>
<h3 class="mt-1 text-lg font-semibold">Heving</h3>
<p class="mb-2">
Dersom varen ikke er rettet eller omlevert, kan kjøperen også heve kjøpet når mangelen ikke er
uvesentlig.
</p>
<h3 class="mt-1 text-lg font-semibold">11. Selgerens rettigheter ved kjøperens mislighold</h3>
<p class="mb-2">
Dersom kjøperen ikke betaler eller oppfyller de øvrige pliktene etter avtalen eller loven, og
dette ikke skyldes selgeren eller forhold på selgerens side, kan selgeren i henhold til reglene i
forbrukerkjøpsloven kapittel 9 etter omstendighetene holde varen tilbake, kreve oppfyllelse av
avtalen, kreve avtalen hevet samt kreve erstatning fra kjøperen. Selgeren vil også etter
omstendighetene kunne kreve renter ved forsinket betaling, inkassogebyr og et rimelig gebyr ved
uavhentede varer.
</p>
<h3 class="mt-1 text-lg font-semibold">Oppfyllelse</h3>
<p class="mb-2">
Selger kan fastholde kjøpet og kreve at kjøperen betaler kjøpesummen. Er varen ikke levert, taper
selgeren sin rett dersom han venter urimelig lenge med å fremme kravet.
</p>
<h3 class="mt-1 text-lg font-semibold">Heving</h3>
<p class="mb-2">
Selger kan heve avtalen dersom det foreligger vesentlig betalingsmislighold eller annet vesentlig
mislighold fra kjøperens side. Selger kan likevel ikke heve dersom hele kjøpesummen er betalt.
Fastsetter selger en rimelig tilleggsfrist for oppfyllelse og kjøperen ikke betaler innen denne
fristen, kan selger heve kjøpet.
</p>
<h3 class="mt-1 text-lg font-semibold">Renter ved forsinket betaling/inkassogebyr</h3>
<p class="mb-2">
Dersom kjøperen ikke betaler kjøpesummen i henhold til avtalen, kan selger kreve renter av
kjøpesummen etter forsinkelsesrenteloven. Ved manglende betaling kan kravet, etter forutgående
varsel, bli sendt til inkasso. Kjøper kan da bli holdt ansvarlig for gebyr etter inkassoloven.
</p>
<h3 class="mt-1 text-lg font-semibold">Gebyr ved uavhentede ikke-forskuddsbetalte varer</h3>
<p class="mb-2">
Dersom kjøperen unnlater å hente ubetalte varer, kan selger belaste kjøper med et gebyr. Gebyret
skal maksimalt dekke selgerens faktiske utlegg for å levere varen til kjøperen. Et slikt gebyr kan
ikke belastes kjøpere under 18 år.
</p>
<h2 class="mb-1 text-xl font-semibold">12. Garanti</h2>
<p class="mb-2">
Garanti som gis av selgeren eller produsenten, gir kjøperen rettigheter i tillegg til de kjøperen
allerede har etter ufravikelig lovgivning. En garanti innebærer dermed ingen begrensninger i
kjøperens rett til reklamasjon og krav ved forsinkelse eller mangler etter punkt 9 og 10.
</p>
<h2 class="mb-1 text-xl font-semibold">13. Personopplysninger</h2>
<p class="mb-2">
Behandlingsansvarlig for innsamlede personopplysninger er selger. Med mindre kjøperen samtykker
til noe annet, kan selgeren, i tråd med personopplysningsloven, kun innhente og lagre de
personopplysninger som er nødvendig for at selgeren skal kunne gjennomføre forpliktelsene etter
avtalen. Kjøperens personopplysninger vil kun bli utlevert til andre hvis det er nødvendig for at
selger skal få gjennomført avtalen med kjøperen, eller i lovbestemte tilfelle.
</p>
<h2 class="mb-1 text-xl font-semibold">14. Konfliktløsning</h2>
<p class="mb-2">
Klager rettes til selger innen rimelig tid, jf. punkt 9 og 10. Partene skal forsøke å løse
eventuelle tvister i minnelighet. Dersom dette ikke lykkes, kan kjøperen ta kontakt med
Forbrukertilsynet for mekling. Forbrukertilsynet er tilgjengelig på telefon 23 400 600 eller
<Link href="www.forbrukertilsynet.no">www.forbrukertilsynet.no</Link>.
</p>
<p class="mb-2">
Europa-Kommisjonens klageportal kan også brukes hvis du ønsker å inngi en klage. Det er særlig
relevant, hvis du er forbruker bosatt i et annet EU-land. Klagen inngis her:
<Link href="http://ec.europa.eu/odr">http://ec.europa.eu/odr</Link>.
</p>

View File

@@ -1,23 +1,37 @@
<script lang="ts"> <script lang="ts">
export let checked: boolean; export let checked: boolean;
export let leftIcon: string | null = null; export let leftIcon: string | null = null;
export let rightIcon: string | null = null; export let rightIcon: string | null = null;
export let invertColor = false; export let invertColor = false;
</script> </script>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
{#if leftIcon} {#if leftIcon}
<img src={leftIcon} class={`h-6 ${invertColor && 'dark:invert'}`} alt="" /> <img
{/if} src={leftIcon}
<label class="relative inline-flex cursor-pointer items-center"> class={`h-6 ${invertColor && "dark:invert"}`}
<input on:change {checked} type="checkbox" value="" class="peer sr-only" /> alt=""
<div />
class="h-6 w-11 rounded-full border-2 border-contrast-100 outline-2 transition-colors after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border-2 after:border-contrast-100 after:bg-contrast-900 after:transition-all after:content-[''] peer-checked:bg-accent peer-checked:after:translate-x-full dark:peer-focus:ring-accent" {/if}
/> <label class="relative inline-flex cursor-pointer items-center">
</label> <input
{#if rightIcon} on:change
<img src={rightIcon} class={`h-6 ${invertColor && 'dark:invert'}`} alt="" /> {checked}
{/if} type="checkbox"
</div> value=""
class="peer sr-only"
/>
<div
class="h-6 w-11 rounded-full border-2 border-contrast-100 outline-2 transition-colors after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border-2 after:border-contrast-100 after:bg-contrast-900 after:transition-all after:content-[''] peer-checked:bg-accent peer-checked:after:translate-x-full dark:peer-focus:ring-accent"
></div>
</label>
{#if rightIcon}
<img
src={rightIcon}
class={`h-6 ${invertColor && "dark:invert"}`}
alt=""
/>
{/if}
</div>

View File

@@ -1,123 +0,0 @@
<script lang="ts" generics="T">
import ListboxItem from './ListboxItem.svelte';
import { writable, type Writable } from 'svelte/store';
import { setListboxContext, type ListboxItemAbstract } from './context';
import { twMerge } from 'tailwind-merge';
/** @description Determine if multiple items can be selected */
export let allowMultiple = true;
/** @description Determine if multiple items can be selected */
export let items: T[];
/** @description Determine if multiple items can be selected */
export let defaultSelectedItems: T[];
/** @description Function called when the selected items changes */
export let onChange: (items: T[]) => void;
let selectedItems: Writable<ListboxItemAbstract<T>[]> = writable([]);
let registeredItems: Writable<ListboxItemAbstract<T>[]> = writable([]);
let focusedItem: Writable<ListboxItemAbstract<T> | null> = writable(null);
function toggleItemSelected(item: ListboxItemAbstract<T>): void {
if (item) {
setFocus(item);
if ($selectedItems.includes(item)) {
if (allowMultiple) {
$selectedItems = $selectedItems.filter((x) => x !== item);
} else {
$selectedItems = [];
}
} else {
if (allowMultiple) {
$selectedItems.push(item);
$selectedItems = $selectedItems;
} else {
$selectedItems = [item];
}
}
}
onChange($selectedItems.map((x) => x.item));
}
function setFocus(item: ListboxItemAbstract<T>) {
const itemEl = document.getElementById(item.id);
$focusedItem = item;
itemEl?.focus();
}
function onKeyDown(e: KeyboardEvent, item: ListboxItemAbstract<T>) {
const index = $registeredItems.findIndex((x) => x === item);
if (index === -1) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if ($registeredItems.length - 1 === index) {
setFocus($registeredItems[0]);
} else {
setFocus($registeredItems[index + 1]);
}
break;
case 'ArrowUp':
e.preventDefault();
if (index === 0) {
setFocus($registeredItems[$registeredItems.length - 1]);
} else {
setFocus($registeredItems[index - 1]);
}
break;
case 'Enter':
case ' ':
e.preventDefault();
toggleItemSelected(item);
break;
case 'Home':
setFocus($registeredItems[0]);
break;
case 'End':
setFocus($registeredItems[$registeredItems.length - 1]);
break;
}
}
function registerItem(item: ListboxItemAbstract<T>): void {
$registeredItems.push(item);
$registeredItems = $registeredItems;
if (defaultSelectedItems.includes(item.item)) {
$selectedItems.push(item);
$selectedItems = $selectedItems;
}
if ($registeredItems.length === 1) {
$focusedItem = $registeredItems[0];
}
}
function unregisterItem(item: ListboxItemAbstract<T>): void {
$registeredItems = $registeredItems.filter((x) => x.id !== item.id);
$selectedItems = $selectedItems.filter((x) => x.id !== item.id);
}
setListboxContext<T>({
registerItem: registerItem,
unregisterItem: unregisterItem,
selectedItems: selectedItems,
focusedItem: focusedItem
});
</script>
<ul
class={twMerge('flex flex-col gap-2 p-4', $$restProps['class'])}
role="listbox"
aria-activedescendant={$focusedItem?.id}
aria-multiselectable={allowMultiple}
tabindex={0}
>
{#each items as item}
<ListboxItem {onKeyDown} onClick={(e, x) => toggleItemSelected(x)} {item}>
<slot {item} />
</ListboxItem>
{/each}
</ul>

View File

@@ -1,51 +0,0 @@
<script lang="ts" generics="T">
import { onMount } from 'svelte';
import { getListboxContext, type ListboxItemAbstract } from './context';
import { twMerge } from 'tailwind-merge';
export let item: T;
export let onClick: (e: MouseEvent, item: ListboxItemAbstract<T>) => void;
export let onKeyDown: (e: KeyboardEvent, item: ListboxItemAbstract<T>) => void;
const id: string = `listbox-item-${
crypto.randomUUID
? crypto.randomUUID()
: Math.floor(Date.now() * Math.random() * 100).toString()
}`;
const itemAbstract: ListboxItemAbstract<T> = { id: id, item: item };
let {
selectedItems: selectedItems,
registerItem: register,
unregisterItem: unregister,
focusedItem: focusedItem
} = getListboxContext<T>();
onMount(() => {
register(itemAbstract);
return () => {
unregister(itemAbstract);
};
});
</script>
<li
{id}
role="option"
aria-selected={$selectedItems.includes(itemAbstract)}
on:click={(e) => onClick(e, itemAbstract)}
on:keydown={(e) => onKeyDown(e, itemAbstract)}
tabindex={$focusedItem && $focusedItem === itemAbstract ? 0 : -1}
class={twMerge(
`cursor-pointer overflow-hidden rounded-md border-2 border-contrast-200 opacity-80 ${
$selectedItems.includes(itemAbstract)
? 'border-contrast-900 opacity-100 dark:border-accent'
: ''
}`,
$$restProps['class']
)}
>
<slot />
</li>

View File

@@ -1,22 +0,0 @@
import { getContext, setContext } from 'svelte';
import type { Writable } from 'svelte/store';
type ListboxContext<T> = {
registerItem: (item: ListboxItemAbstract<T>) => void;
unregisterItem: (item: ListboxItemAbstract<T>) => void;
selectedItems: Writable<ListboxItemAbstract<T>[]>;
focusedItem: Writable<ListboxItemAbstract<T> | null>;
};
export type ListboxItemAbstract<T> = {
id: string;
item: T;
};
export function setListboxContext<T>(context: ListboxContext<T>): void {
setContext('listbox', context);
}
export function getListboxContext<T>(): ListboxContext<T> {
return getContext('listbox') as ListboxContext<T>;
}

View File

@@ -1,38 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { getTabsContext } from './context';
import { twMerge } from 'tailwind-merge';
export let label: string;
const id: string = `tab-${
crypto.randomUUID
? crypto.randomUUID()
: Math.floor(Date.now() * Math.random() * 100).toString()
}`;
const context = getTabsContext();
const { selectedTab } = context;
onMount(() => {
context.register({
id: id,
label: label
});
return () => {
context.unregister(id);
};
});
</script>
{#if $selectedTab === id}
<div
{...$$restProps}
class={twMerge('rounded-md bg-primary-300 p-4 shadow-inner', $$restProps['class'])}
role="tabpanel"
aria-labelledby={id}
>
<slot />
</div>
{/if}

View File

@@ -1,102 +0,0 @@
<script lang="ts">
import { twMerge } from 'tailwind-merge';
import { setTabsContext, type TabAbstract } from './context';
import { writable, type Writable } from 'svelte/store';
export let buttonClass = '';
let tabs: TabAbstract[] = [];
let selectedTab: Writable<string | null> = writable(null);
function setTab(id: string): void {
const tab = tabs.find((x) => x.id == id);
if (tab) {
$selectedTab = tab.id;
}
}
function registerTab(tab: TabAbstract): void {
tabs.push(tab);
tabs = tabs;
if (tabs.length === 1) {
$selectedTab = tabs[0].id;
}
}
function unregisterTab(id: string): void {
tabs = tabs.filter((x) => x.id !== id);
}
function setFocus(tab: TabAbstract) {
const tabEl = document.getElementById(tab.id);
tabEl?.focus();
}
function onKeyDown(e: KeyboardEvent, tab: TabAbstract) {
const index = tabs.findIndex((x) => x.id === tab.id);
if (index === -1) return;
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
if (tabs.length - 1 === index) {
setFocus(tabs[0]);
} else {
setFocus(tabs[index + 1]);
}
break;
case 'ArrowLeft':
e.preventDefault();
if (index === 0) {
setFocus(tabs[tabs.length - 1]);
} else {
setFocus(tabs[index - 1]);
}
break;
case 'Home':
e.preventDefault();
setFocus(tabs[0]);
break;
case 'End':
e.preventDefault();
setFocus(tabs[tabs.length - 1]);
break;
}
}
setTabsContext({
register: registerTab,
unregister: unregisterTab,
selectedTab: selectedTab
});
</script>
<div {...$$restProps} class={twMerge('flex flex-col gap-4', $$restProps['class'])}>
<div
role="tablist"
aria-labelledby="tablist"
class="flex gap-4 overflow-auto rounded-md bg-primary-300 p-4 shadow-inner"
>
{#each tabs as tab, i}
<button
role="tab"
type="button"
id={tab.id}
tabindex={$selectedTab === tab.id || ($selectedTab === null && i === 0) ? 0 : -1}
aria-selected={$selectedTab === tab.id}
aria-controls={tab.id}
class={twMerge(
`rounded-md border-2 border-contrast-200 bg-primary-100 px-4 py-2 hover:border-contrast-900 dark:hover:border-accent ${$selectedTab === tab.id ? 'border-contrast-900 dark:border-accent' : ''}`,
buttonClass
)}
on:click={() => setTab(tab.id)}
on:keydown={(e) => onKeyDown(e, tab)}
>
{tab.label}
</button>
{/each}
</div>
<slot />
</div>

View File

@@ -1,21 +0,0 @@
import { getContext, setContext } from 'svelte';
import type { Writable } from 'svelte/store';
type TabsContext = {
register: (tab: TabAbstract) => void;
unregister: (id: string) => void;
selectedTab: Writable<string | null>;
};
export type TabAbstract = {
id: string;
label: string;
};
export function setTabsContext(context: TabsContext): void {
setContext('tabs', context);
}
export function getTabsContext(): TabsContext {
return getContext('tabs') as TabsContext;
}