8 | * ```
9 | */
10 | export const clickOutside: Action<
11 | Element,
12 | | {
13 | /**
14 | * Array of classnames. If the click target element has one of these classes, it will not fire the `onOutClick` event.
15 | */
16 | whitelist?: string[]
17 | }
18 | | undefined,
19 | {
20 | onOutClick?: (
21 | event: CustomEvent<{
22 | target: HTMLElement
23 | }>,
24 | ) => void
25 | }
26 | > = (node, options) => {
27 | const handleClick = (event: MouseEvent) => {
28 | let disable = false
29 |
30 | for (const className of options?.whitelist || []) {
31 | if (event.target instanceof Element && event.target.classList.contains(className)) {
32 | disable = true
33 | }
34 | }
35 |
36 | if (!disable && node && !node.contains(event.target as Node) && !event.defaultPrevented) {
37 | node.dispatchEvent(
38 | new CustomEvent('outclick', {
39 | detail: {
40 | target: event.target as HTMLElement,
41 | },
42 | }),
43 | )
44 | }
45 | }
46 |
47 | document.addEventListener('click', handleClick, true)
48 |
49 | return {
50 | update: (newOptions) => (options = { ...options, ...newOptions }),
51 | destroy() {
52 | document.removeEventListener('click', handleClick, true)
53 | },
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/lib/actions/focus.ts:
--------------------------------------------------------------------------------
1 | import { tick } from 'svelte';
2 |
3 | /** Sometimes the autofocus attribute is insufficient, we need to do this */
4 | export function forcefocus(node: HTMLInputElement) {
5 | tick().then(() => node.focus());
6 | }
7 |
8 | export function focusable_children(node: HTMLElement) {
9 | const nodes: HTMLElement[] = Array.from(
10 | node.querySelectorAll(
11 | 'a[href], button, input, textarea, select, summary, [tabindex]:not([tabindex="-1"])'
12 | )
13 | );
14 |
15 | const index = nodes.indexOf(document.activeElement as HTMLElement);
16 |
17 | const update = (d: number) => {
18 | let i = index + d;
19 | i += nodes.length;
20 | i %= nodes.length;
21 |
22 | nodes[i].focus();
23 | };
24 |
25 | function traverse(d: number, selector?: string) {
26 | const reordered = [...nodes.slice(index), ...nodes.slice(0, index)];
27 |
28 | let i = (reordered.length + d) % reordered.length;
29 | let node;
30 |
31 | while ((node = reordered[i])) {
32 | i += d;
33 |
34 | if (node.matches('details:not([open]) *')) {
35 | continue;
36 | }
37 |
38 | if (!selector || node.matches(selector)) {
39 | node.focus();
40 | return;
41 | }
42 | }
43 | }
44 |
45 | return {
46 | next: (selector?: string) => traverse(1, selector),
47 | prev: (selector?: string) => traverse(-1, selector),
48 | update
49 | };
50 | }
51 |
52 | export function trap(node: HTMLElement, { reset_focus = true }: { reset_focus?: boolean } = {}) {
53 | const previous = document.activeElement as HTMLElement;
54 |
55 | const handle_keydown = (e: KeyboardEvent) => {
56 | if (e.key === 'Tab') {
57 | e.preventDefault();
58 |
59 | const group = focusable_children(node);
60 | if (e.shiftKey) {
61 | group.prev();
62 | } else {
63 | group.next();
64 | }
65 | }
66 | };
67 |
68 | node.addEventListener('keydown', handle_keydown);
69 |
70 | return {
71 | destroy: () => {
72 | node.removeEventListener('keydown', handle_keydown);
73 | if (reset_focus) {
74 | previous?.focus({ preventScroll: true });
75 | }
76 | }
77 | };
78 | }
79 |
--------------------------------------------------------------------------------
/src/lib/actions/hover.ts:
--------------------------------------------------------------------------------
1 | import type { Action } from 'svelte/action'
2 |
3 | /**
4 | * Calls `onHover` when the user hovers over the element. When the user hovers out, it waits until
5 | * `delay` milliseconds have passed before calling `onHover` again with `hovering: false`.
6 | */
7 | export const hover: Action<
8 | Element,
9 | | {
10 | /**
11 | * Delay in milliseconds before releasing the hover state.
12 | * @default 0
13 | */
14 | delay?: number
15 | }
16 | | undefined,
17 | {
18 | onhover?: (event: CustomEvent<{ hovering: boolean }>) => void
19 | }
20 | > = (node, options) => {
21 | function enter(_e: Event) {
22 | clearTimeout(leaveTimer)
23 | node.dispatchEvent(new CustomEvent('hover', { detail: { hovering: true } }))
24 | }
25 |
26 | let leaveTimer: ReturnType
27 | function leave(_e: Event) {
28 | clearTimeout(leaveTimer)
29 | leaveTimer = setTimeout(() => {
30 | node.dispatchEvent(new CustomEvent('hover', { detail: { hovering: false } }))
31 | }, options?.delay ?? 0)
32 | }
33 |
34 | node.addEventListener('pointerleave', leave, true)
35 | node.addEventListener('pointerenter', enter, true)
36 |
37 | return {
38 | destroy() {
39 | clearTimeout(leaveTimer)
40 | node.removeEventListener('pointerleave', leave, true)
41 | node.removeEventListener('pointerenter', enter, true)
42 | },
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/lib/actions/index.ts:
--------------------------------------------------------------------------------
1 | export { focusable_children, forcefocus, trap } from './focus';
2 |
--------------------------------------------------------------------------------
/src/lib/actions/trap.ts:
--------------------------------------------------------------------------------
1 | // original from svelte.dev: https://github.com/sveltejs/svelte.dev/blob/a865f37f3f060a698b79a4b35cbce97835c5c413/packages/site-kit/src/lib/actions/focus.ts#L1-L78
2 |
3 | import { tick } from 'svelte'
4 |
5 | /** Sometimes the autofocus attribute is insufficient, we need to do this */
6 | export function forcefocus(node: HTMLInputElement) {
7 | tick().then(() => node.focus())
8 | }
9 |
10 | export function focusable_children(node: HTMLElement) {
11 | const nodes: HTMLElement[] = Array.from(
12 | node.querySelectorAll(
13 | 'a[href], button, input, textarea, select, summary, [tabindex]:not([tabindex="-1"])',
14 | ),
15 | )
16 |
17 | const index = nodes.indexOf(document.activeElement as HTMLElement)
18 |
19 | const update = (d: number) => {
20 | let i = index + d
21 | i += nodes.length
22 | i %= nodes.length
23 |
24 | nodes[i].focus()
25 | }
26 |
27 | function traverse(d: number, selector?: string) {
28 | const reordered = [...nodes.slice(index), ...nodes.slice(0, index)]
29 |
30 | let i = (reordered.length + d) % reordered.length
31 | let node
32 |
33 | while ((node = reordered[i])) {
34 | i += d
35 |
36 | if (node.matches('details:not([open]) *')) {
37 | continue
38 | }
39 |
40 | if (!selector || node.matches(selector)) {
41 | node.focus()
42 | return
43 | }
44 | }
45 | }
46 |
47 | return {
48 | next: (selector?: string) => traverse(1, selector),
49 | prev: (selector?: string) => traverse(-1, selector),
50 | update,
51 | }
52 | }
53 |
54 | export function trap(node: HTMLElement, { reset_focus = true }: { reset_focus?: boolean } = {}) {
55 | const previous = document.activeElement as HTMLElement
56 |
57 | const handle_keydown = (e: KeyboardEvent) => {
58 | if (e.key === 'Tab') {
59 | e.preventDefault()
60 |
61 | const group = focusable_children(node)
62 | if (e.shiftKey) {
63 | group.prev()
64 | } else {
65 | group.next()
66 | }
67 | }
68 | }
69 |
70 | node.addEventListener('keydown', handle_keydown)
71 |
72 | return {
73 | destroy: () => {
74 | node.removeEventListener('keydown', handle_keydown)
75 | if (reset_focus) {
76 | previous?.focus({ preventScroll: true })
77 | }
78 | },
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/lib/actions/utils.ts:
--------------------------------------------------------------------------------
1 | export function fix_position(element: HTMLElement, fn: Function) {
2 | let scroll_parent: HTMLElement | null = element;
3 |
4 | while ((scroll_parent = scroll_parent.parentElement)) {
5 | if (/^(scroll|auto)$/.test(getComputedStyle(scroll_parent).overflowY)) {
6 | break;
7 | }
8 | }
9 |
10 | const top = element.getBoundingClientRect().top;
11 | fn();
12 | const delta = element.getBoundingClientRect().top - top;
13 |
14 | if (delta !== 0) {
15 | // whichever element the user interacted with should stay in the same position
16 | (scroll_parent ?? window).scrollBy(0, delta);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/lib/components/Dropdown.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 | {
24 | onHover(detail.hovering)
25 | }}
26 | >
27 | {@render children()}
28 |
29 |
30 | {@render dropdown()}
31 |
32 |
33 |
34 |
70 |
--------------------------------------------------------------------------------
/src/lib/components/FontToggle.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
26 |
27 |
52 |
--------------------------------------------------------------------------------
/src/lib/components/Gooey.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
37 |
38 | {
40 | if (e.key === '1') {
41 | gooey!.toggleHidden()
42 | }
43 | }}
44 | />
45 |
--------------------------------------------------------------------------------
/src/lib/components/Hello.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | {#each 'Hello' as letter}
8 | {letter}
9 | {/each}
10 |
11 |
12 |
13 |
82 |
--------------------------------------------------------------------------------
/src/lib/components/Hero.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
33 | {@render word('svelte')}
34 |
35 |
43 |
44 | {@render word('starter', 6)}
45 |
46 |
47 | {#snippet word(letters: string, start = 0)}
48 |
49 | {#each letters as letter, i}
50 | {@const pct = (100 / letters.length) * i}
51 |
52 |
57 | {letter}
58 |
59 | {/each}
60 |
61 | {/snippet}
62 |
63 |
116 |
--------------------------------------------------------------------------------
/src/lib/components/HoverMenu.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
10 |
11 |
42 |
--------------------------------------------------------------------------------
/src/lib/components/Icon.svelte:
--------------------------------------------------------------------------------
1 |
6 |
17 |
18 |
19 |
20 |
21 |
22 |
42 |
--------------------------------------------------------------------------------
/src/lib/components/ModalOverlay.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 | {#if nav_state.open}
14 |
15 | {/if}
16 |
17 |
34 |
--------------------------------------------------------------------------------
/src/lib/components/Page.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 | {@render before?.()}
23 |
24 |
25 | {#if title}
26 |
{title}
27 | {/if}
28 |
29 |
30 | {@render children?.()}
31 |
32 |
33 |
34 | {@render after?.()}
35 |
36 |
37 |
101 |
--------------------------------------------------------------------------------
/src/lib/components/PageTitle.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 | svelte-starter · {pageTitle()}
19 |
20 |
--------------------------------------------------------------------------------
/src/lib/components/Shell.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
24 |
25 |
26 |
27 | {#if navigating.from}
28 |
29 | {/if}
30 |
31 | {#if nav_visible}
32 |
33 |
34 | {@render top_nav?.()}
35 | {/if}
36 |
37 | {@render children?.()}
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/lib/components/header/Header.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 | 0}>
11 |
16 |
17 | {#if !device.mobile}
18 |
19 | {/if}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
54 |
--------------------------------------------------------------------------------
/src/lib/components/header/Logo.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
13 |
14 |
15 |
16 |
21 |
26 |
27 |
28 |
29 |
34 |
39 |
40 |
41 |
42 |
43 |
123 |
--------------------------------------------------------------------------------
/src/lib/components/header/navs/Mobile/Burger.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 | {#if browser}
26 | {#key showMenu}
27 |
28 |
37 |
48 |
57 |
58 | {/key}
59 | {/if}
60 |
61 |
62 |
192 |
--------------------------------------------------------------------------------
/src/lib/components/header/navs/Mobile/PageFill.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | (showMenu = false)}
11 | onkeydown={(e) => {
12 | if (e.key === 'Escape') showMenu = false
13 | }}
14 | >
15 | {@render children?.()}
16 |
17 |
18 |
50 |
--------------------------------------------------------------------------------
/src/lib/components/header/navs/NavDesktop.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {#each routes as { title, path }, i}
10 |
15 | {title}
16 |
17 | {/each}
18 |
19 |
20 |
21 |
97 |
--------------------------------------------------------------------------------
/src/lib/components/header/navs/NavMobile.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 | (showMenu = false)}
18 | >
19 |
20 |
21 |
22 |
23 | {#if showMenu}
24 |
25 |
39 |
40 | {/if}
41 |
42 |
43 |
101 |
--------------------------------------------------------------------------------
/src/lib/components/nav/MobileSubMenu.svelte:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 | {title}
36 |
37 |
38 |
39 |
40 | {#each contents as child}
41 | {#if !child.children?.length}
42 |
49 | {:else}
50 |
51 | {#each child.children ?? [] as { title }}
52 |
53 | {#if title}
54 |
55 | {title}
56 |
57 | {/if}
58 |
59 |
60 | {#each child.children ?? [] as { path, title }}
61 |
62 |
63 | {title}
64 |
65 |
66 | {/each}
67 |
68 |
69 | {/each}
70 |
71 | {/if}
72 | {/each}
73 |
74 |
75 |
76 |
111 |
--------------------------------------------------------------------------------
/src/lib/components/nav/Nav.Menu.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
9 |
10 |
21 |
22 |
127 |
--------------------------------------------------------------------------------
/src/lib/components/nav/Nav.Mobile.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
57 |
58 | {
60 | // We only manage focus when Esc is hit, otherwise the navigation will reset focus.
61 | if (nav_state.open && e.key === 'Escape') {
62 | nav_state.open = false
63 | tick().then(() => menu_button?.focus())
64 | }
65 | }}
66 | />
67 |
68 | {#if title}
69 |
70 | {title}
71 |
72 | {/if}
73 |
74 |
95 |
96 | {#if nav_state.open}
97 |
98 | (nav_state.open = false)} />
99 |
100 |
101 | {/if}
102 |
103 |
138 |
--------------------------------------------------------------------------------
/src/lib/components/nav/Nav.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
174 |
--------------------------------------------------------------------------------
/src/lib/components/nav/PreloadingIndicator.svelte:
--------------------------------------------------------------------------------
1 |
34 |
35 | {#if visible}
36 |
39 | {/if}
40 |
41 | {#if p.current >= 0.4}
42 |
43 | {/if}
44 |
45 |
90 |
--------------------------------------------------------------------------------
/src/lib/components/nav/SkipLink.svelte:
--------------------------------------------------------------------------------
1 |
4 |
9 |
10 |
11 | {#if children}{@render children()}{:else}Skip to main content{/if}
12 |
13 |
14 |
39 |
--------------------------------------------------------------------------------
/src/lib/components/nav/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Nav } from './Nav.svelte';
2 | export { default as PreloadingIndicator } from './PreloadingIndicator.svelte';
3 | export { default as SkipLink } from './SkipLink.svelte';
4 |
--------------------------------------------------------------------------------
/src/lib/components/nav/nav_state.svelte.ts:
--------------------------------------------------------------------------------
1 | export const nav_state = $state({
2 | autohide: false,
3 | open: false,
4 | on_this_page: false,
5 | })
6 |
--------------------------------------------------------------------------------
/src/lib/icons/arrow-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/lib/icons/bluesky-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/src/lib/icons/bluesky-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/src/lib/icons/bluesky.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/chevron.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/lib/icons/contents.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/lib/icons/delete.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/lib/icons/discord-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/discord-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/download-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/download-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/external.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/lib/icons/file-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/lib/icons/file-edit-inline-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/lib/icons/file-edit.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/lib/icons/file-new.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/lib/icons/file.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/lib/icons/folder-new.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/lib/icons/folder-open.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/lib/icons/folder.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/lib/icons/github-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/github-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/github.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/lightbulb.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/refresh.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/lib/icons/rename.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/lib/icons/terminal.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/lib/icons/theme-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/theme-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/user-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/user-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/router/index.ts:
--------------------------------------------------------------------------------
1 | export type { Route } from './router.types'
2 | export { router } from './router.svelte'
3 |
--------------------------------------------------------------------------------
/src/lib/router/router.types.ts:
--------------------------------------------------------------------------------
1 | export type Route = {
2 | path: string
3 | title: string
4 | children?: Route[]
5 | /**
6 | * Whether the route is only reachable during development.
7 | * @default false
8 | */
9 | dev?: boolean
10 | }
11 |
12 | export type ExtractPaths = T extends readonly (infer R)[]
13 | ? R extends { path: string; children?: Route[] }
14 | ? R['path'] | (R['children'] extends Route[] ? ExtractPaths : never)
15 | : never
16 | : never
17 |
18 | export type GetRouteByPath<
19 | T extends Route[],
20 | P extends ExtractPaths,
21 | > = T extends readonly (infer R)[]
22 | ? R extends { path: P; title: string }
23 | ? R
24 | : R extends { children: Route[] }
25 | ? GetRouteByPath
26 | : never
27 | : never
28 |
--------------------------------------------------------------------------------
/src/lib/router/routes.test.ts:
--------------------------------------------------------------------------------
1 | import type { ExtractPaths, Route } from './router.types'
2 | import type { page as Page } from '$app/state'
3 |
4 | import { describe, expect, test } from 'vitest'
5 | import { Router } from './router.svelte'
6 |
7 | const routes = [
8 | { path: '/', title: 'Home' },
9 | {
10 | path: '/foo',
11 | title: 'Foo',
12 | children: [
13 | { path: '/foo/bar', title: 'Bar' },
14 | { path: '/foo/baz', title: 'Baz' },
15 | ],
16 | },
17 | ] as const satisfies Route[]
18 |
19 | const router = new Router(routes)
20 |
21 | const mock_page = (path: T) =>
22 | ({ url: { pathname: path } }) as typeof Page & {
23 | url: { pathname: T }
24 | }
25 |
26 | describe('router', () => {
27 | const page = mock_page('/foo')
28 |
29 | test('ExtractPaths inference', () => {
30 | type AllPaths = ExtractPaths
31 | const path: AllPaths = '/'
32 | expect(path).toBe('/')
33 | })
34 |
35 | test('router.get()', () => {
36 | const route = router.get('/foo/bar')
37 | expect(route?.path).toBe('/foo/bar')
38 | expect(route?.title).toBe('Bar')
39 | })
40 |
41 | test('router.isActive()', () => {
42 | const isActive = router.isActive('/foo', page)
43 | expect(isActive).toBe(true)
44 |
45 | const isntActive = router.isActive('/foo/bar', page)
46 | expect(isntActive, 'Child path is active, but parent is not.').toBe(false)
47 | })
48 |
49 | test('router.isParent()', () => {
50 | const hasActiveChild = router.isParent('/foo', mock_page('/foo/bar'))
51 | expect(hasActiveChild, `'/foo' is not a parent of ${page.url.pathname}`).toBe(true)
52 |
53 | const hasActiveChild2 = router.isChild('/foo/bar', mock_page('/'))
54 | expect(hasActiveChild2, `'/foo/bar' is not a child of '/'`).toBe(false)
55 | })
56 |
57 | test('router.isChild()', () => {
58 | const hasActiveParent = router.isChild('/foo/baz', page)
59 | expect(hasActiveParent, `'/foo/baz' is not a child of ${page.url.pathname}`).toBe(true)
60 | })
61 |
62 | test('router.children()', () => {
63 | const children = router.get('/foo')?.children
64 | expect(children).toHaveLength(2)
65 | expect(children?.[0].path).toBe('/foo/bar')
66 | expect(children?.[1].path).toBe('/foo/baz')
67 | })
68 |
69 | test('router.get()', () => {
70 | const homeRoute = router.get('/')
71 | expect(homeRoute?.path).toBe('/')
72 | expect(homeRoute?.title).toBe('Home')
73 | })
74 |
75 | test('router.gh()', () => {
76 | const githubUrl = router.gh('src/styles/inputs.scss')
77 | expect(githubUrl).toBe(
78 | 'https://github.com/braebo/svelte-starter/tree/main/src/styles/inputs.scss',
79 | )
80 | })
81 |
82 | test('router.get()', () => {
83 | const inputsRoute = router.get('/foo/bar')
84 | expect(inputsRoute?.path).toBe('/foo/bar')
85 | expect(inputsRoute?.title).toBe('Bar')
86 | })
87 | })
88 |
--------------------------------------------------------------------------------
/src/lib/routes.ts:
--------------------------------------------------------------------------------
1 | import type { Route } from './router/router.types'
2 |
3 | export type Routes = typeof routes
4 |
5 | export const routes = [
6 | {
7 | path: '/',
8 | title: 'Home',
9 | children: [],
10 | },
11 | {
12 | path: '/design',
13 | title: 'Design',
14 | children: [
15 | {
16 | path: '/design/elements',
17 | title: 'Elements',
18 | children: [],
19 | },
20 | {
21 | path: '/design/inputs',
22 | title: 'Inputs',
23 | children: [],
24 | },
25 | ],
26 | },
27 | {
28 | path: '/about',
29 | title: 'About',
30 | children: [],
31 | },
32 | {
33 | dev: true,
34 | path: '/playground',
35 | title: 'Playground',
36 | children: [
37 | {
38 | path: '/playground/misc',
39 | title: 'Miscellaneous',
40 | children: [
41 | {
42 | path: '/playground/misc/nested',
43 | title: 'Nested',
44 | children: [],
45 | },
46 | ],
47 | },
48 | ],
49 | },
50 | ] as const satisfies Route[]
51 |
--------------------------------------------------------------------------------
/src/lib/routes.types.ts:
--------------------------------------------------------------------------------
1 | export interface Route {
2 | path: string
3 | children?: Record
4 | }
5 |
6 | export type RouteTree = Record
7 |
8 | /**
9 | * Gets all possible paths from a route tree.
10 | */
11 | export type GetPaths = {
12 | [K in keyof T]: T[K] extends { path: string }
13 | ? T[K]['path'] | (T[K] extends { children: infer C } ? (C extends RouteTree ? GetPaths : never) : never)
14 | : never
15 | }[keyof T]
16 |
17 | /**
18 | * Gets a route by its path, including the key as the title.
19 | */
20 | export type GetRouteByPath> = {
21 | [K in keyof T]: T[K] extends { path: P }
22 | ? { path: T[K]['path']; title: K & string }
23 | : T[K] extends { children: infer C }
24 | ? C extends RouteTree
25 | ? GetRouteByPath
26 | : never
27 | : never
28 | }[keyof T]
29 |
--------------------------------------------------------------------------------
/src/lib/themer/resolveTheme.ts:
--------------------------------------------------------------------------------
1 | import type { BaseColors, Theme, ThemeColors, ThemeDefinition } from './themer.types'
2 | import { deep_merge } from '$lib/utils/deep-merge'
3 | import { DEFAULT_THEME, vanilla } from './themes/themes'
4 |
5 | /**
6 | * Merges a partial theme definition into the base theme.
7 | * @param def - A partial theme definition to resolve into a full {@link Theme} object.
8 | */
9 | export function resolveTheme(def: ThemeDefinition) {
10 | // Merge the new definition with vanilla as the base
11 | const merged = deep_merge({}, DEFAULT_THEME, def)
12 |
13 | const dark_a = getColor('dark-a')
14 | const dark_b = getColor('dark-b')
15 | const dark_c = getColor('dark-c')
16 | const dark_d = getColor('dark-d')
17 | const dark_e = getColor('dark-e')
18 | const light_a = getColor('light-a')
19 | const light_b = getColor('light-b')
20 | const light_c = getColor('light-c')
21 | const light_d = getColor('light-d')
22 | const light_e = getColor('light-e')
23 |
24 | const colors: ThemeColors = {
25 | '--theme-a': light_dark('theme-a'),
26 | '--theme-b': light_dark('theme-b'),
27 | '--theme-c': light_dark('theme-c'),
28 | '--dark-a': dark_a,
29 | '--dark-b': dark_b,
30 | '--dark-c': dark_c,
31 | '--dark-d': dark_d,
32 | '--dark-e': dark_e,
33 | '--light-a': light_a,
34 | '--light-b': light_b,
35 | '--light-c': light_c,
36 | '--light-d': light_d,
37 | '--light-e': light_e,
38 | '--bg-a': `light-dark(${light_a}, ${dark_a})`,
39 | '--bg-b': `light-dark(${light_b}, ${dark_b})`,
40 | '--bg-c': `light-dark(${light_c}, ${dark_c})`,
41 | '--bg-d': `light-dark(${light_d}, ${dark_d})`,
42 | '--bg-e': `light-dark(${light_e}, ${dark_e})`,
43 | '--fg-a': `light-dark(${dark_a}, ${light_a})`,
44 | '--fg-b': `light-dark(${dark_b}, ${light_b})`,
45 | '--fg-c': `light-dark(${dark_c}, ${light_c})`,
46 | '--fg-d': `light-dark(${dark_d}, ${light_d})`,
47 | '--fg-e': `light-dark(${dark_e}, ${light_e})`,
48 | }
49 |
50 | const theme = {
51 | title: def.title,
52 | colors,
53 | } satisfies Theme
54 |
55 | return theme
56 |
57 | function getColor(str: keyof BaseColors, mode: 'light' | 'dark' | 'base' = 'base'): string {
58 | const def = merged.colors.base[str] ?? DEFAULT_THEME.colors.base[str]
59 |
60 | if (!def) throw new Error(`Missing color base definition: ${str}`)
61 |
62 | if (mode === 'base') return def
63 |
64 | if (mode === 'dark') {
65 | return merged.colors.dark?.[str] ?? def
66 | }
67 |
68 | if (mode === 'light') {
69 | return merged.colors.light?.[str] ?? def
70 | }
71 |
72 | throw new Error(`Invalid mode: ${mode}`)
73 | }
74 |
75 | function light_dark(color: string) {
76 | return `light-dark(${getColor(color, 'light')}, ${getColor(color, 'dark')})`
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/lib/themer/themer.types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A fully resolved theme with all variables applied.
3 | */
4 | export type Theme = {
5 | title: string
6 | colors: ThemeColors
7 | }
8 |
9 | /**
10 | * A flat collection of all color variables in a resolved theme.
11 | */
12 | export type ThemeColors = BaseColors & ModeColors
13 |
14 | /**
15 | * Static colors that are used as a base for all themes and modes.
16 | */
17 | export interface BaseColors {
18 | [key: string]: string
19 | '--theme-a': string
20 | '--theme-b': string
21 | '--theme-c': string
22 |
23 | '--dark-a': string
24 | '--dark-b': string
25 | '--dark-c': string
26 | '--dark-d': string
27 | '--dark-e': string
28 |
29 | '--light-a': string
30 | '--light-b': string
31 | '--light-c': string
32 | '--light-d': string
33 | '--light-e': string
34 | }
35 |
36 | /**
37 | * Mode-specific colors that use CSS `light-dark()` to adapt to the current `color-scheme`.
38 | */
39 | export interface ModeColors {
40 | [key: string]: string
41 | '--bg-a': string
42 | '--bg-b': string
43 | '--bg-c': string
44 | '--bg-d': string
45 | '--bg-e': string
46 |
47 | '--fg-a': string
48 | '--fg-b': string
49 | '--fg-c': string
50 | '--fg-d': string
51 | '--fg-e': string
52 | }
53 |
54 | /**
55 | * The minimum required definition for a theme.
56 | */
57 | export type ThemeDefinition = {
58 | title: string
59 | /**
60 | * All themes come with a default color definition that can be overridden with partials.
61 | *
62 | * All shades have light and dark variants, allowing all other colors to adapt to the mode.
63 | *
64 | * The `light` and `dark` variables are automatically generated from the `base` colors
65 | * if not overridden manually.
66 | */
67 | colors: {
68 | /**
69 | * Base colors that are used as a base for all themes and modes.
70 | */
71 | base: Partial
72 | /**
73 | * Optional dark-mode overrides.
74 | */
75 | dark?: Partial & Record
76 | /**
77 | * Optional light-mode overrides.
78 | */
79 | light?: Partial & Record
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/lib/themer/themes/themes.ts:
--------------------------------------------------------------------------------
1 | import type { ThemeDefinition } from '../themer.types'
2 |
3 | import { resolveTheme } from '../resolveTheme'
4 |
5 | export const DEFAULT_THEME: ThemeDefinition = {
6 | title: 'vanilla',
7 | colors: {
8 | base: {
9 | 'theme-a': '#57b1ff',
10 | 'theme-b': '#ffcc8b',
11 | 'theme-c': '#ff8ba9',
12 | 'dark-a': '#0b0b11',
13 | 'dark-b': '#15161d',
14 | 'dark-c': '#1f202d',
15 | 'dark-d': '#353746',
16 | 'dark-e': '#474a5b',
17 | 'light-a': '#ffffff',
18 | 'light-b': '#dfe1e9',
19 | 'light-c': '#babeca',
20 | 'light-d': '#777d8f',
21 | 'light-e': '#5f6377',
22 | },
23 | dark: {},
24 | light: {},
25 | },
26 | }
27 | Object.freeze(DEFAULT_THEME)
28 |
29 | export const vanilla = resolveTheme(DEFAULT_THEME)
30 |
31 | export const autumn = resolveTheme({
32 | title: 'autumn',
33 | colors: {
34 | base: {
35 | 'theme-a': '#ff9a3d',
36 | 'theme-b': '#ff5e5e',
37 | 'theme-c': '#9b51e0',
38 | },
39 | },
40 | })
41 |
42 | export const neon = resolveTheme({
43 | title: 'neon',
44 | colors: {
45 | base: {
46 | 'theme-a': '#00ff95',
47 | 'theme-b': '#00e1ff',
48 | 'theme-c': '#ff007c',
49 | },
50 | },
51 | })
52 |
53 | export const mellow = resolveTheme({
54 | title: 'mellow',
55 | colors: {
56 | base: {
57 | 'theme-a': '#ff9a9e',
58 | 'theme-b': '#fad0c4',
59 | 'theme-c': '#f093fb',
60 | },
61 | },
62 | })
63 |
64 | export const cyberpunk = resolveTheme({
65 | title: 'cyberpunk',
66 | colors: {
67 | base: {
68 | 'theme-a': '#00ff99',
69 | 'theme-b': '#00c9a7',
70 | 'theme-c': '#c964ff',
71 | },
72 | },
73 | })
74 |
75 | export const themes = {
76 | vanilla,
77 | autumn,
78 | neon,
79 | mellow,
80 | cyberpunk,
81 | }
82 |
--------------------------------------------------------------------------------
/src/lib/utils/ansi/ansi-hex.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Creates an ANSI True Color _(24-bit RGB)_ formatter function from a hex color, falling back to
3 | * uncolored text in unsupported browsers _(Safari, Firefox)_.
4 | *
5 | * @example
6 | * ```ts
7 | * const red = ansiHex('#ff0000')
8 | * console.log(red('This text will be red'))
9 | * ```
10 | */
11 | export function ansiHex(hex_color: `#${string}`, background = false) {
12 | return (...args: any[]) => {
13 | const str = args.join('')
14 | if (globalThis.navigator?.userAgent.match(/firefox|safari/i)) {
15 | return str
16 | }
17 |
18 | const rgb = hexToRgb(hex_color)
19 | if (!rgb) return str
20 |
21 | return `\x1b[${background ? '48' : '38'};2;${rgb[0]};${rgb[1]};${rgb[2]}m${str}\x1b[0m`
22 | }
23 | }
24 |
25 | /**
26 | * @internal
27 | * @see https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
28 | */
29 | export const ANSI_STYLE_CODES = {
30 | reset: 0,
31 | bold: 1,
32 | dim: 2,
33 | italic: 3,
34 | underline: 4,
35 | inverse: 7,
36 | hidden: 8,
37 | strikethrough: 9,
38 | } as const
39 |
40 | export type AnsiStyle = keyof typeof ANSI_STYLE_CODES
41 |
42 | /**
43 | * Creates an {@link AnsiStyle|ANSI style} formatter function from a style name, falling back to
44 | * uncolored text in unsupported browsers (Safari, Firefox).
45 | *
46 | * @example
47 | * ```ts
48 | * const bold = ansiStyle('bold')
49 | * console.log(bold('This text will be bold'))
50 | * ```
51 | */
52 | export function ansiStyle(style: AnsiStyle): (...args: any[]) => string {
53 | if (globalThis.navigator?.userAgent.match(/firefox|safari/i)) {
54 | return (...args: any[]) => args.join('')
55 | }
56 |
57 | const code = ANSI_STYLE_CODES[style]
58 | const styleCode = `\x1b[${code}m`
59 | const resetCode =
60 | code === 0
61 | ? ''
62 | : code <= 2 // bold/dim
63 | ? '\x1b[22m'
64 | : `\x1b[2${code}m`
65 |
66 | return (...args: any[]) => `${styleCode}${args.join('')}${resetCode}`
67 | }
68 |
69 | /**
70 | * Converts a hex color string into an `[r, g, b]` tuple in range `[0,255]`.
71 | */
72 | export function hexToRgb(hex: string): [number, number, number] | null {
73 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
74 | return result ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)] : null
75 | }
76 |
--------------------------------------------------------------------------------
/src/lib/utils/ansi/ansi-logger.ts:
--------------------------------------------------------------------------------
1 | import { r, g, d, o, p, y } from './ansi-mini'
2 |
3 | export interface LogOptions {
4 | /**
5 | * Optional label to prepend to the log.
6 | */
7 | label?: string
8 | /**
9 | * Alternative logger function, i.e. `console.warn/error`.
10 | * @default console.log
11 | */
12 | logger?: (...args: any[]) => void
13 | /**
14 | * Prefix to prepend to the log.
15 | * @default d('︙ ')
16 | */
17 | prefix?: string
18 |
19 | /**
20 | * Delimiter to use between rest args.
21 | * @default undefined
22 | */
23 | delimiter?: string
24 |
25 | /**
26 | * Whether to print objects in a single line when passing multiple args.
27 | * @default true
28 | */
29 | inline?: boolean
30 | }
31 |
32 | /**
33 | * `console.log` wrapper that handles multi-line strings and includes an optional label and prefix.
34 | */
35 | export function log(args = [] as any[], opts: LogOptions = {}): void {
36 | // prettier-ignore
37 | const {
38 | label = '',
39 | logger = console.log,
40 | prefix = d('︙ '),
41 | delimiter = '',
42 | inline = true,
43 | } = opts
44 |
45 | if (args.length === 0) {
46 | label && logger(prefix + label)
47 | logger(prefix)
48 |
49 | return
50 | }
51 |
52 | if (typeof args[0] === 'string' && args.length === 1) {
53 | const lines = args[0].split('\n')
54 | for (let i = 0; i < lines.length; i++) {
55 | if (i === 0 && label) logger(prefix + label)
56 | logger(prefix + lines[i])
57 | }
58 |
59 | return
60 | }
61 |
62 | if (label) logger(prefix + label)
63 | try {
64 | const a = []
65 |
66 | for (let i = 0; i < args.length; i++) {
67 | switch (typeof args[i]) {
68 | case 'object': {
69 | if (!args[i]) {
70 | a.push(d(args[i]))
71 | break
72 | }
73 | const s = paint_object(args[i], { inline })
74 | if (inline) a.push(s)
75 | else a.push(s.replaceAll('\n', '\n' + prefix))
76 | break
77 | }
78 | case 'number': {
79 | a.push(p(args[i]))
80 | break
81 | }
82 | default: {
83 | a.push(args[i])
84 | break
85 | }
86 | }
87 | }
88 |
89 | logger(prefix + a.join(delimiter))
90 | } catch (e) {
91 | console.error(e)
92 | console.log(args)
93 | }
94 |
95 | return
96 | }
97 |
98 | interface ClrOptions {
99 | /**
100 | * Whether to print objects in a single line.
101 | * @default true
102 | */
103 | inline?: boolean
104 | /** @internal */
105 | indent?: number
106 | }
107 |
108 | /** Colors a primitive based on its type. */
109 | function paint_primitive(v: any, opts: ClrOptions = {}): string {
110 | if (v === null) return d('null')
111 | if (v === undefined) return d('undefined')
112 | if (v === true || v === false) return y(v)
113 |
114 | switch (typeof v) {
115 | case 'function':
116 | const s = d(o(v.toString().replaceAll(/\n/g, '')))
117 | if (s.length < 75) return s
118 | return d(o('[Function]'))
119 | case 'number':
120 | return p(v)
121 | case 'string':
122 | return d(g('"')) + g(v) + d(g('"'))
123 | case 'boolean':
124 | return v ? g('true') : r('false')
125 | case 'object':
126 | return paint_object(v, opts)
127 | default:
128 | return v
129 | }
130 | }
131 |
132 | /** Converts an object into a colorized string. */
133 | function paint_object(v: Record, opts: ClrOptions = {}): string {
134 | const { inline, indent = 1 } = opts
135 | const nl = inline ? '' : '\n'
136 | const indentStr = inline ? '' : ' '.repeat(indent)
137 | let s = '{ ' + nl
138 | const entries = Object.entries(v)
139 | for (let j = 0; j < entries.length; j++) {
140 | s += indentStr + d(entries[j][0])
141 | s += ': '
142 | s += paint_primitive(entries[j][1], { inline, indent: indent + 1 })
143 | if (j < entries.length - 1) {
144 | s += ', ' + nl
145 | }
146 | }
147 | s += nl
148 | if (inline) s += ' '
149 | s += '}'
150 | return s
151 | }
152 |
153 | log([1, 2, 3])
154 |
155 | log(['foo', ' ', { a: 1, b: true, c: '3' }, ' ', 5])
156 |
157 | log([{ str: 'two', num: 4 }, ' '])
158 |
159 | log([
160 | {
161 | bool: false,
162 | obj: {
163 | fn: (x: any) => {
164 | console.log(x)
165 | console.log(x)
166 | },
167 | },
168 | },
169 | ])
170 |
--------------------------------------------------------------------------------
/src/lib/utils/ansi/ansi-mini.ts:
--------------------------------------------------------------------------------
1 | import { ansiHex, ansiStyle } from './ansi-hex'
2 | import { log } from './ansi-logger'
3 |
4 | /**
5 | * @fileoverview ANSI Mini
6 | * Irresponsibly short ANSI code wrappers for the terminal / supported browsers (Chrome).
7 | * It's mostly a palette of ANSI True Color wrappers using `{@link ansiHex}` and `{@link ansiStyle}`.
8 | *
9 | * @example
10 | * ```ts
11 | * import { l, r, dim, bd, em } from '@braebo/ansi/mini'
12 | */
13 |
14 | /** Wraps args in ansi red. */
15 | export const r = ansiHex('#ff5347')
16 | /** Wraps args in ansi green. */
17 | export const g = ansiHex('#57ab57')
18 | /** Wraps args in ansi blue. */
19 | export const b = ansiHex('#4c4ce0')
20 | /** Wraps args in ansi yellow. */
21 | export const y = ansiHex('#e2e270')
22 | /** Wraps args in ansi magenta. */
23 | export const m = ansiHex('#d426d4')
24 | /** Wraps args in ansi cyan. */
25 | export const c = ansiHex('#2fdede')
26 | /** Wraps args in ansi orange. */
27 | export const o = ansiHex('#ff7f50')
28 | /** Wraps args in ansi purple. */
29 | export const p = ansiHex('#9542e7')
30 | /** Wraps args in ansi gray. */
31 | export const gr = ansiHex('#808080')
32 |
33 | /** Wraps args in ansi dim. */
34 | export const d = ansiStyle('dim')
35 | /** Wraps args in ansi bold. */
36 | export const bd = ansiStyle('bold')
37 | /** Wraps args in ansi italic. */
38 | export const em = ansiStyle('italic')
39 | /** Wraps args in ansi underline. */
40 | export const ul = ansiStyle('underline')
41 | /** Wraps args in ansi inverse. */
42 | export const inv = ansiStyle('inverse')
43 | /** Wraps args in ansi strikethrough. */
44 | export const s = ansiStyle('strikethrough')
45 |
46 | /** Logs a new line `count` times. */
47 | export function n(count = 1) {
48 | for (let i = 0; i < count; i++) {
49 | log()
50 | }
51 | }
52 |
53 | /** `console.log` shorthand. */
54 | export function l(...args: any[]) {
55 | log(args)
56 | }
57 |
58 | /** `console.error` with prefix and ERROR label */
59 | export function err(...args: any[]) {
60 | log(args, { label: r(bd('ERROR ')), logger: console.error })
61 | }
62 |
--------------------------------------------------------------------------------
/src/lib/utils/ansi/ansi.bench.ts:
--------------------------------------------------------------------------------
1 | import { ansiStyle } from './ansi-hex.js'
2 | import { bench } from 'vitest'
3 |
4 | const opts: Parameters[2] = {
5 | iterations: 10000,
6 | warmupIterations: 1000,
7 | }
8 |
9 | bench(
10 | 'ansiStyle',
11 | () => {
12 | const style = ansiStyle('bold')
13 | style('Hello, world!')
14 | },
15 | opts,
16 | )
17 |
--------------------------------------------------------------------------------
/src/lib/utils/ansi/index.ts:
--------------------------------------------------------------------------------
1 | export { log } from './ansi-logger'
2 | export { ansiHex, ansiStyle } from './ansi-hex'
3 | export { l, n, err, r, g, y, b, m, c, gr, o, p, d, bd, em, ul, inv, s } from './ansi-mini'
4 |
--------------------------------------------------------------------------------
/src/lib/utils/ansi/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ansi",
3 | "version": "0.0.1",
4 | "description": "ANSI code utilites for node.js and chrome",
5 | "main": "index.ts",
6 | "scripts": {
7 | "build": "bun build src/index.ts --compile --minify --outfile ./dist/ansi-mini.js"
8 | },
9 | "devDependencies": {
10 | "typescript": "^5.4.5"
11 | },
12 | "type": "module",
13 | "types": "./dist/ansi-mini.d.ts",
14 | "files": [
15 | "dist"
16 | ],
17 | "exports": {
18 | ".": {
19 | "default": "./dist/ansi-mini.js",
20 | "types": "./dist/ansi-mini.d.ts"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/lib/utils/deep-merge.ts:
--------------------------------------------------------------------------------
1 | export function deep_merge(...objects: Partial[]): T {
2 | return objects.reduce((acc, curr) => {
3 | if (!is_object(acc) || !is_object(curr)) {
4 | if (Array.isArray(acc) && Array.isArray(curr)) {
5 | return [...acc, ...curr]
6 | }
7 | return curr
8 | }
9 |
10 | for (const [key, value] of Object.entries(curr)) {
11 | if (is_object(value) && key in acc && is_object(acc[key])) {
12 | if (Array.isArray(acc[key]) && Array.isArray(value)) {
13 | acc[key] = [...acc[key], ...value]
14 | } else {
15 | acc[key] = deep_merge(acc[key], value)
16 | }
17 | } else {
18 | acc[key] = value
19 | }
20 | }
21 |
22 | return acc
23 | }, {}) as T
24 | }
25 |
26 | function is_object(value: unknown): value is Record {
27 | return typeof value === 'object' && value !== null
28 | }
29 |
--------------------------------------------------------------------------------
/src/lib/utils/defer.ts:
--------------------------------------------------------------------------------
1 | export const defer = (
2 | typeof globalThis.requestIdleCallback !== 'undefined'
3 | ? globalThis.requestIdleCallback
4 | : typeof globalThis.requestAnimationFrame !== 'undefined'
5 | ? globalThis.requestAnimationFrame
6 | : (fn: () => void) => setTimeout(fn, 0)
7 | ) as (fn: () => void) => number
8 |
9 | export const cancelDefer = (
10 | typeof globalThis?.cancelIdleCallback !== 'undefined'
11 | ? globalThis.cancelIdleCallback
12 | : typeof globalThis.cancelAnimationFrame !== 'undefined'
13 | ? globalThis.cancelAnimationFrame
14 | : globalThis.clearTimeout
15 | ) as (id: number) => void
16 |
--------------------------------------------------------------------------------
/src/lib/utils/device.svelte.ts:
--------------------------------------------------------------------------------
1 | import { Logger } from './logger/logger'
2 | import { DEV } from 'esm-env'
3 |
4 | class Device {
5 | /**
6 | * Mobile breakpoint in pixels.
7 | * @default 1000
8 | */
9 | public breakpoint = $state(1000)
10 | /** `window.innerWidth` */
11 | public width = $state(900)
12 | /** `window.innerHeight` */
13 | public height = $state(900)
14 | /** true if `window.innerWidth` < {@link breakpoint|`breakpoint`} */
15 | public mobile = $derived.by(() => this.width < this.breakpoint)
16 | /** `window.scrollY` */
17 | public scrollY = $state(0)
18 | /** Client coordinates of the mouse or touch point. */
19 | public mouse = $state({ x: 0, y: 0 })
20 |
21 | #log?: Logger
22 | #initialized = false
23 |
24 | constructor(
25 | /**
26 | * Mobile breakpoint in pixels.
27 | * @default 1000
28 | */
29 | breakpoint?: number,
30 | ) {
31 | if (!globalThis.window || this.#initialized) return
32 | this.#initialized = true
33 |
34 | if (breakpoint) this.breakpoint = breakpoint
35 |
36 | this.#onResize()
37 | this.#onScroll()
38 |
39 | addEventListener('resize', this.#onResize)
40 | addEventListener('scroll', this.#onScroll)
41 | addEventListener('pointermove', this.#onPointerMove)
42 |
43 | if (DEV) this.#log = new Logger('Device', { fg: 'plum' })
44 | }
45 |
46 | #onResize = (): void => {
47 | this.width = globalThis.window.innerWidth || 0
48 | this.height = globalThis.window.innerHeight || 0
49 | }
50 |
51 | #onScroll = (): void => {
52 | this.scrollY = globalThis.window.scrollY || 0
53 | }
54 |
55 | #frame = 0
56 | #onPointerMove = (e?: PointerEvent): void => {
57 | cancelAnimationFrame(this.#frame)
58 | this.#frame = requestAnimationFrame(() => {
59 | this.mouse.x = e?.clientX || 1
60 | this.mouse.y = e?.clientY || 1
61 | })
62 | }
63 | }
64 |
65 | /**
66 | * Reactive window / pointer wrapper with a dispose method.
67 | *
68 | * Available properties:
69 | * - `breakpoint` - _mobile breakpoint in pixels_
70 | * - `width` - _window width in pixels_
71 | * - `height` - _window height in pixels_
72 | * - `mobile` - _true if width < breakpoint_
73 | * - `scrollY` - _scroll position in pixels_
74 | * - `mouse` { x, y } - _client coordinates of the mouse or touch point_
75 | */
76 | export const device = new Device()
77 |
78 | type EventCallback = (e?: Event | PointerEvent) => void
79 |
--------------------------------------------------------------------------------
/src/lib/utils/hexToRgb.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Converts a hex color string to RGB values.
3 | * @param hex - The hex color string (e.g., '#ff0000' or 'ff0000')
4 | * @returns A tuple of [r, g, b] values (0-255) or null if invalid hex
5 | */
6 | export function hexToRgb(hex: string): [number, number, number] | null {
7 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
8 | return result ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)] : null
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/utils/logger/index.ts:
--------------------------------------------------------------------------------
1 | export { logger } from './logger'
--------------------------------------------------------------------------------
/src/lib/utils/logger/logger-colors.ts:
--------------------------------------------------------------------------------
1 | import { isFirefox, isSafari } from '../ua'
2 |
3 | export const ANSI_COLOR_CODES = {
4 | // Styles
5 | reset: '\x1b[0m',
6 | bold: '\x1b[1m',
7 | dim: '\x1b[2m',
8 | italic: '\x1b[3m',
9 | underline: '\x1b[4m',
10 | inverse: '\x1b[7m',
11 | hidden: '\x1b[8m',
12 | strikethrough: '\x1b[9m',
13 |
14 | // Foreground colors
15 | black: '\x1b[30m',
16 | red: '\x1b[31m',
17 | green: '\x1b[32m',
18 | yellow: '\x1b[33m',
19 | blue: '\x1b[34m',
20 | magenta: '\x1b[35m',
21 | cyan: '\x1b[36m',
22 | white: '\x1b[37m',
23 | gray: '\x1b[90m',
24 |
25 | // Bright foreground colors
26 | brightBlack: '\x1b[90m',
27 | brightRed: '\x1b[91m',
28 | brightGreen: '\x1b[92m',
29 | brightYellow: '\x1b[93m',
30 | brightBlue: '\x1b[94m',
31 | brightMagenta: '\x1b[95m',
32 | brightCyan: '\x1b[96m',
33 | brightWhite: '\x1b[97m',
34 |
35 | // Background colors
36 | bgBlack: '\x1b[40m',
37 | bgRed: '\x1b[41m',
38 | bgGreen: '\x1b[42m',
39 | bgYellow: '\x1b[43m',
40 | bgBlue: '\x1b[44m',
41 | bgMagenta: '\x1b[45m',
42 | bgCyan: '\x1b[46m',
43 | bgWhite: '\x1b[47m',
44 |
45 | // Bright background colors
46 | bgBrightBlack: '\x1b[100m',
47 | bgBrightRed: '\x1b[101m',
48 | bgBrightGreen: '\x1b[102m',
49 | bgBrightYellow: '\x1b[103m',
50 | bgBrightBlue: '\x1b[104m',
51 | bgBrightMagenta: '\x1b[105m',
52 | bgBrightCyan: '\x1b[106m',
53 | bgBrightWhite: '\x1b[107m',
54 | } as const
55 |
56 | type AnsiKeyword = keyof typeof ANSI_COLOR_CODES
57 |
58 | // Simple hex to RGB conversion
59 | function hexToRgb(hex: string): [number, number, number] | null {
60 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
61 | return result ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)] : null
62 | }
63 |
64 | // Function to create hex color
65 | export const hex =
66 | (hexColor: string) =>
67 | (...args: any[]) => {
68 | const str = args.join('')
69 | if (isSafari() || isFirefox()) return str
70 |
71 | const rgb = hexToRgb(hexColor)
72 | if (!rgb) return str
73 |
74 | return `\x1b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m${str}\x1b[0m`
75 | }
76 |
77 | /**
78 | * Wraps a string in an ANSI color code.
79 | * @param colorName The name of the color to wrap the string in.
80 | * @returns A function that takes arguments (like `console.log`) and returns the wrapped string.
81 | */
82 | export const color = (colorName: AnsiKeyword) => {
83 | if (isSafari() || isFirefox()) return (...args: any[]) => args.join('')
84 |
85 | // Use selective resets based on the type of style
86 | const resetCode = colorName.startsWith('bg')
87 | ? '\x1b[49m' // Reset background.
88 | : colorName === 'bold' || colorName === 'dim'
89 | ? '\x1b[22m' // Reset weight / intensity.
90 | : colorName === 'italic'
91 | ? '\x1b[23m' // Reset italic.
92 | : colorName === 'underline'
93 | ? '\x1b[24m' // Reset underline.
94 | : '\x1b[39m' // Reset foreground color.
95 |
96 | return (...args: any[]) => `${ANSI_COLOR_CODES[colorName]}${args.join('')}${resetCode}`
97 | }
98 |
--------------------------------------------------------------------------------
/src/lib/utils/prettify.ts:
--------------------------------------------------------------------------------
1 | import { r, g, y, d, p, o } from './ansi/ansi-mini'
2 |
3 | /**
4 | * Options for the `prettyPrint` function.
5 | */
6 | export interface PrettyPrintOptions {
7 | /**
8 | * Optional label to prepend to the log.
9 | */
10 | label?: string
11 |
12 | /**
13 | * Alternative logger function, i.e. `console.warn/error`.
14 | * @default console.log
15 | */
16 | logger?: (...args: any[]) => void
17 |
18 | /**
19 | * Prefix to prepend to the log.
20 | * @default d('︙ ')
21 | */
22 | prefix?: string
23 |
24 | /**
25 | * Delimiter to use between rest args.
26 | * @default undefined
27 | */
28 | delimiter?: string
29 |
30 | /**
31 | * Whether to print objects in a single line when passing multiple args.
32 | * @default true
33 | */
34 | inline?: boolean
35 | }
36 |
37 | /**
38 | * Colorful `console.log` wrapper that handles multi-line strings and includes
39 | * {@link PrettyPrintOptions|options} for customizing the label, logger, prefix,
40 | * delimiter, and inline printing of objects.
41 | */
42 | export function prettyPrint(args = [] as any[], opts: PrettyPrintOptions = {}): void {
43 | // prettier-ignore
44 | const {
45 | label = '',
46 | logger = console.log,
47 | prefix = d('︙ '),
48 | delimiter = '',
49 | inline = true,
50 | } = opts
51 |
52 | if (args.length === 0) {
53 | label && logger(prefix + label)
54 | logger(prefix)
55 |
56 | return
57 | }
58 |
59 | if (typeof args[0] === 'string' && args.length === 1) {
60 | const lines = args[0].split('\n')
61 | for (let i = 0; i < lines.length; i++) {
62 | if (i === 0 && label) logger(prefix + label)
63 | logger(prefix + lines[i])
64 | }
65 |
66 | return
67 | }
68 |
69 | if (label) logger(prefix + label)
70 | try {
71 | const a = []
72 |
73 | for (let i = 0; i < args.length; i++) {
74 | switch (typeof args[i]) {
75 | case 'object': {
76 | if (!args[i]) {
77 | a.push(d(args[i]))
78 | break
79 | }
80 | const s = clr_object(args[i], { inline })
81 | if (inline) a.push(s)
82 | else a.push(s.replaceAll('\n', '\n' + prefix))
83 | break
84 | }
85 | case 'number': {
86 | a.push(p(args[i]))
87 | break
88 | }
89 | default: {
90 | a.push(args[i])
91 | break
92 | }
93 | }
94 | }
95 |
96 | logger(prefix + a.join(delimiter))
97 | } catch (e) {
98 | console.error(e)
99 | console.log(args)
100 | }
101 |
102 | return
103 | }
104 |
105 | interface ClrOptions {
106 | /**
107 | * Whether to print objects in a single line.
108 | * @default true
109 | */
110 | inline?: boolean
111 | /** @internal */
112 | indent?: number
113 | }
114 |
115 | /** Colors a primitive based on its type. */
116 | function clr_primitive(v: any, opts: ClrOptions = {}): string {
117 | if (v === null) return d('null')
118 | if (v === undefined) return d('undefined')
119 | if (v === true || v === false) return y(v)
120 |
121 | switch (typeof v) {
122 | case 'function':
123 | const s = d(o(v.toString().replaceAll(/\n/g, '')))
124 | if (s.length < 75) return s
125 | return d(o('[Function]'))
126 | case 'number':
127 | return p(v)
128 | case 'string':
129 | return d(g('"')) + g(v) + d(g('"'))
130 | case 'boolean':
131 | return v ? g('true') : r('false')
132 | case 'object':
133 | return clr_object(v, opts)
134 | default:
135 | return v
136 | }
137 | }
138 |
139 | /** Converts an object into a colorized string. */
140 | function clr_object(v: Record, opts: ClrOptions = {}): string {
141 | const { inline, indent = 1 } = opts
142 | const nl = inline ? '' : '\n'
143 | const indentStr = inline ? '' : ' '.repeat(indent)
144 | let s = '{ ' + nl
145 | const entries = Object.entries(v)
146 | for (let j = 0; j < entries.length; j++) {
147 | s += indentStr + d(entries[j][0])
148 | s += ': '
149 | s += clr_primitive(entries[j][1], { inline, indent: indent + 1 })
150 | if (j < entries.length - 1) {
151 | s += ', ' + nl
152 | }
153 | }
154 | s += nl
155 | if (inline) s += ' '
156 | s += '}'
157 | return s
158 | }
159 |
160 | prettyPrint([1, 2, 3])
161 | prettyPrint(['foo', ' ', { a: 1, b: true, c: '3' }, ' ', 5])
162 | prettyPrint([{ str: 'two', num: 4 }, ' '])
163 | prettyPrint([
164 | {
165 | bool: false,
166 | obj: {
167 | fn: (x: any) => {
168 | console.log(x)
169 | console.log(x)
170 | },
171 | },
172 | },
173 | ])
174 |
--------------------------------------------------------------------------------
/src/lib/utils/stringify.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A stringify replacer that handles circular references, undefined values, and functions.
3 | * - Circular references are replaced with the string `[Circular ~]`
4 | * where `` is the path to the circular reference relative to the
5 | * root object, i.e. `[Circular ~.b.c]`.
6 | * - Functions are replaced with the string `"[Function]"`.
7 | * - `undefined` values are replaced with the string `"undefined"`.
8 | *
9 | * @param obj - The object to stringify.
10 | * @param indentation - Number of spaces for indentation. Optional.
11 | */
12 | export const stringify = (input: unknown, indentation = 0) => {
13 | const stack = [] as unknown[]
14 | return JSON.stringify(input, serialize(stack), indentation)
15 | }
16 |
17 | /**
18 | * A replacer function for `JSON.stringify` that handles circular references,
19 | * undefined values, and functions with strings.
20 | * @see {@link stringify}
21 | */
22 | export function serialize(stack: unknown[]) {
23 | const keys: string[] = []
24 |
25 | return function (this: unknown, key: string, value: unknown): unknown {
26 | if (typeof value === 'undefined') return
27 | if (typeof value === 'function') return '[Function]'
28 |
29 | let thisPos = stack.indexOf(this)
30 | if (thisPos !== -1) {
31 | stack.length = thisPos + 1
32 | keys.length = thisPos
33 | keys[thisPos] = key
34 | } else {
35 | stack.push(this)
36 | keys.push(key)
37 | }
38 |
39 | let valuePos = stack.indexOf(value)
40 | if (valuePos !== -1) {
41 | return '[Circular ~' + keys.slice(0, valuePos).join('.') + ']'
42 | }
43 |
44 | if (value instanceof Set) {
45 | return Array.from(value)
46 | }
47 |
48 | if (value instanceof Map) {
49 | return Object.fromEntries(
50 | Array.from(value.entries()).map(([k, v]) => {
51 | const newStack = [...stack]
52 | return [k, JSON.parse(JSON.stringify(v, serialize(newStack)))]
53 | }),
54 | )
55 | }
56 |
57 | if (value instanceof Element) {
58 | return `${value.tagName}.${Array.from(value.classList)
59 | .filter(s => !s.startsWith('s-'))
60 | .join('.')}#${value.id}`
61 | }
62 |
63 | if (stack.length > 0) {
64 | stack.push(value)
65 | }
66 |
67 | return value
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/lib/utils/tldr.ts:
--------------------------------------------------------------------------------
1 | import { b } from '@braebo/ansi'
2 |
3 | /**
4 | * Options for {@link tldr}.
5 | */
6 | export interface TldrOptions {
7 | /**
8 | * The max depth to traverse.
9 | * @default 2
10 | */
11 | maxDepth?: number
12 |
13 | /**
14 | * The max number of string characters before truncating th..
15 | * @default 30
16 | */
17 | maxLength?: number
18 |
19 | /**
20 | * The max number of object or array entries before truncating.
21 | * @default 4
22 | */
23 | maxSiblings?: number
24 |
25 | /**
26 | * Bypasses the {@link maxSiblings} limit for the top level if `true`.
27 | * @default false
28 | */
29 | preserveRootSiblings?: boolean
30 |
31 | /**
32 | * Whether to preserve numbers instead of truncating them according to {@link maxLength}.
33 | * @default false
34 | */
35 | preserveNumbers?: boolean
36 |
37 | /**
38 | * Preserve functions instead of serializing them to `[Function: name]`.
39 | * @default false
40 | */
41 | preserveFunctions?: boolean
42 | }
43 |
44 | /**
45 | * Truncate objects by depth, sibling count, and string/number length.
46 | */
47 | export function tldr(
48 | /**
49 | * The object to simplify.
50 | */
51 | object: unknown,
52 | /**
53 | * Optional {@link TldrOptions}.
54 | */
55 | {
56 | maxDepth = 2,
57 | maxLength = 30,
58 | maxSiblings = 4,
59 | preserveRootSiblings = false,
60 | preserveFunctions = false,
61 | preserveNumbers = false,
62 | }: TldrOptions = {},
63 | ) {
64 | return parse(object) as T
65 |
66 | function parse(obj: unknown, depth = 0): unknown {
67 | const seen = new WeakSet()
68 | if (obj === null) {
69 | return obj
70 | }
71 |
72 | if (typeof obj === 'object') {
73 | if (seen.has(obj)) return '[Circular]'
74 | seen.add(obj)
75 | }
76 |
77 | switch (typeof obj) {
78 | case 'boolean':
79 | case 'symbol':
80 | case 'undefined': {
81 | return obj
82 | }
83 |
84 | case 'function': {
85 | return preserveFunctions ? obj : `[Function: ${obj.name}]`
86 | }
87 |
88 | case 'string': {
89 | // Trim strings that are too long.
90 | if (obj.length < maxLength + 3) return obj
91 | return obj.slice(0, maxLength) + '..'
92 | }
93 |
94 | case 'number': {
95 | // Trim numbers that are too long.
96 | const s = !preserveNumbers ? obj.toFixed(maxLength) : obj.toString()
97 | if (s.length > maxLength + 3) {
98 | return +s.slice(0, maxLength) + '..'
99 | }
100 | return +s
101 | }
102 |
103 | case 'bigint': {
104 | // Bigints can't be serialized, so we have to trim them.
105 | return +obj.toString().slice(0, maxLength)
106 | }
107 |
108 | case 'object': {
109 | const depthReached = depth > maxDepth
110 |
111 | if (Array.isArray(obj)) {
112 | // if (depthReached) return `[..${o.length} ${o.length === 1 ? 'item' : 'items'}]`
113 | if (depthReached) return `[ ..${obj.length} ]`
114 | if (obj.length <= maxSiblings || depth === 0) return obj.map(s => parse(s, depth + 1))
115 |
116 | return [
117 | ...obj.slice(0, maxSiblings).map(s => parse(s, depth)),
118 | `..${obj.length - maxSiblings} more`,
119 | ]
120 | }
121 |
122 | const keyCount = Object.keys(obj).length
123 |
124 | if (depthReached) {
125 | return `{..${keyCount} ${keyCount === 1 ? 'entry' : 'entries'}}`
126 | }
127 |
128 | if (keyCount <= maxSiblings || (preserveRootSiblings && depth === 0)) {
129 | return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, parse(v, depth + 1)]))
130 | }
131 |
132 | return Object.fromEntries(
133 | Object.entries(obj)
134 | .slice(0, maxSiblings)
135 | .concat([['..', `${keyCount - maxSiblings} more`]])
136 | .map(([k, v]) => [k, b(parse(v, depth + 1))]),
137 | )
138 | }
139 | }
140 |
141 | return obj
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/lib/utils/transitions.ts:
--------------------------------------------------------------------------------
1 | import type { FlyParams, FadeParams } from 'svelte/transition'
2 |
3 | import { cubicOut, cubicIn, quintOut } from 'svelte/easing'
4 | import { fly, fade } from 'svelte/transition'
5 |
6 | export const IN = { duration: 150, delay: 100, easing: quintOut }
7 | export const OUT = { duration: 100, delay: 0, easing: quintOut }
8 |
9 | const duration = 75
10 | const delay = 75
11 |
12 | export const fade_out = (node: HTMLElement, params?: FadeParams) => {
13 | return fade(node, {
14 | duration,
15 | easing: cubicIn,
16 | ...params,
17 | })
18 | }
19 |
20 | export const fade_in = (node: HTMLElement, params?: FadeParams) => {
21 | return fade(node, {
22 | delay,
23 | easing: cubicOut,
24 | ...params,
25 | })
26 | }
27 |
28 | export const fly_in = (node: HTMLElement, params: FlyParams = { y: 10 }) => {
29 | return fly(node, {
30 | delay,
31 | easing: cubicIn,
32 | ...params,
33 | })
34 | }
35 |
36 | export const fly_out = (node: HTMLElement, params: FlyParams = { y: 10 }) => {
37 | return fly(node, {
38 | duration,
39 | easing: cubicOut,
40 | ...params,
41 | })
42 | }
43 |
--------------------------------------------------------------------------------
/src/lib/utils/ua.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Detects the current browser.
3 | */
4 | export function getBrowser(request?: Request): 'chrome' | 'firefox' | 'safari' | 'other' {
5 | if (isChrome(request) || isEdge(request)) return 'chrome'
6 | if (isFirefox(request)) return 'firefox'
7 | if (isSafari(request)) return 'safari'
8 | return 'other'
9 | }
10 |
11 | /**
12 | * Resolves the user agent string. On the server, the request object must be provided.
13 | * @param request - The request object. If not provided, the global `navigator` object is returned.
14 | */
15 | export function getUserAgent(request?: Request) {
16 | if (typeof globalThis.navigator === 'undefined' && !request) {
17 | console.error('Error getting user-agent: Request object is required on the server, but was not provided.')
18 | }
19 |
20 | return request?.headers.get('user-agent') || globalThis.navigator?.userAgent
21 | }
22 |
23 | /**
24 | * Detects if the current browser matches the provided platform.
25 | */
26 | export function isPlatform(platform: RegExp, request?: Request) {
27 | const ua = getUserAgent(request)
28 | return !!ua?.match(platform)
29 | }
30 |
31 | /**
32 | * Detects if the current browser is a webview. The request parameter is required on the server.
33 | * @param request - The request object. If not provided, the global `navigator` object is used.
34 | */
35 | export function isWebview(request?: Request) {
36 | return !!getUserAgent(request).match(/webview|wv|ip((?!.*Safari)|(?=.*like Safari))/i)
37 | }
38 |
39 | /**
40 | * `true` if the current browser is running on a desktop.
41 | */
42 | export function isDesktop(request?: Request) {
43 | return !isMobile(request)
44 | }
45 |
46 | /**
47 | * `true` if the current browser is running on MacOS.
48 | */
49 | export function isMac(request?: Request) {
50 | return isPlatform(/mac/i, request) && !isMobile(request)
51 | }
52 |
53 | /**
54 | * `true` if the current browser is running on MacOS.
55 | */
56 | export function isApple(request?: Request) {
57 | return isMac(request) || isIOS(request) || isIPad(request) || isIPadOS(request) || isIPad(request)
58 | }
59 |
60 | /**
61 | * `true` if the current browser is running on Windows.
62 | */
63 | export function isWindows(request?: Request) {
64 | return isPlatform(/win/i, request)
65 | }
66 |
67 | /**
68 | * `true` if the current browser is running on Linux.
69 | */
70 | export function isLinux(request?: Request) {
71 | return isPlatform(/linux/i, request)
72 | }
73 |
74 | /**
75 | * `true` when running in a browser.
76 | */
77 | export function isBrowser() {
78 | return typeof window !== 'undefined'
79 | }
80 |
81 | /**
82 | * `true` when running in a server environment.
83 | */
84 | export function isServer() {
85 | return typeof window === 'undefined'
86 | }
87 |
88 | /**
89 | * `true` if the current browser is running on a mobile device.
90 | */
91 | export function isMobile(request?: Request) {
92 | return isAndroid(request) || isIOS(request) || isIPad(request)
93 | }
94 |
95 | /**
96 | * `true` if the current browser is running on iOS.
97 | */
98 | export function isIOS(request?: Request) {
99 | return isPlatform(/iphone/i, request)
100 | }
101 |
102 | /**
103 | * `true` if the current browser is running on iPadOS.
104 | */
105 | export function isIPadOS(request?: Request) {
106 | return isIPad(request)
107 | }
108 |
109 | /**
110 | * `true` if the current browser is running on an iPad.
111 | */
112 | export function isIPad(request?: Request) {
113 | // return isSafari(request) && !isIOS(request)
114 | return isPlatform(/ipad/i, request)
115 | }
116 |
117 | /**
118 | * `true` if the current browser is running on Android.
119 | */
120 | export function isAndroid(request?: Request) {
121 | return isPlatform(/android/i, request)
122 | }
123 |
124 | /**
125 | * `true` if the current browser is running on Safari.
126 | */
127 | export function isSafari(request?: Request) {
128 | return isPlatform(/^((?!chrome|android).)*safari/i, request)
129 | }
130 |
131 | /**
132 | * `true` if the current browser is Chrome.
133 | */
134 | export function isChrome(request?: Request) {
135 | return isPlatform(/chrome/i, request)
136 | }
137 |
138 | /**
139 | * `true` if the current browser is Firefox.
140 | */
141 | export function isFirefox(request?: Request) {
142 | return isPlatform(/firefox/i, request)
143 | }
144 |
145 | /**
146 | * `true` if the current browser is Edge.
147 | */
148 | export function isEdge(request?: Request) {
149 | return isPlatform(/edg/i, request)
150 | }
151 |
--------------------------------------------------------------------------------
/src/routes/(dev)/+layout.server.ts:
--------------------------------------------------------------------------------
1 | export const prerender = false
2 |
--------------------------------------------------------------------------------
/src/routes/(dev)/playground/+page.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 | {#each router.current.children ?? [] as child}
11 |
14 | {/each}
15 |
16 |
17 |
18 |
31 |
--------------------------------------------------------------------------------
/src/routes/(dev)/playground/+page.ts:
--------------------------------------------------------------------------------
1 | // import type { SvelteComponent } from 'svelte'
2 |
3 | // import { l, r } from '@braebo/ansi'
4 |
5 | // export const prerender = true
6 | // export const ssr = false
7 |
8 | // // Glob of all directories in the src/routes/playground/ with vites import.meta.glob
9 | // const playground = import.meta.glob<{ default: () => typeof SvelteComponent }>('/src/routes/playground/**/*.svelte', {
10 | // eager: true,
11 | // })
12 |
13 | // const pages = Object.keys(playground).map(path => {
14 | // const route = path.replace('/src/routes/playground/', '').replace('.svelte', '')
15 |
16 | // const component = playground[path]
17 | // l(r('route'), route)
18 | // l(r('component'), component)
19 |
20 | // return {
21 | // path: route,
22 | // component,
23 | // }
24 | // })
25 |
26 | // export const load = () => {
27 | // return { pages }
28 | // }
29 |
--------------------------------------------------------------------------------
/src/routes/(dev)/playground/misc/+page.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
Hello
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/routes/(dev)/playground/misc/nested/+page.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
Hello. This is a nested page.
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/routes/+error.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 | {page.status}
9 |
10 |
11 | {#if page.error?.message}
12 | {page.error.message}
13 | {/if}
14 |
15 | {#if dev && page.error && 'stack' in page.error}
16 | {page.error.stack}
17 | {/if}
18 |
19 |
20 |
21 | Go home
22 |
23 |
24 |
42 |
--------------------------------------------------------------------------------
/src/routes/+layout.server.ts:
--------------------------------------------------------------------------------
1 | import type { Route } from '$lib/router/router.types'
2 |
3 | import { router } from '$lib/router/router.svelte.js'
4 | import { routes } from '$lib/routes'
5 | import { DEV } from 'esm-env'
6 |
7 | export const prerender = true
8 |
9 | export const load = async ({ url, locals }) => {
10 | const path = url.pathname
11 | const title = path === '/' ? 'Home' : router.get(path)?.title
12 |
13 | // Filter out dev routes if not in dev mode
14 | const all_routes = filterDevRoutes(routes)
15 |
16 | return {
17 | title,
18 | routes: all_routes,
19 | theme: locals.theme,
20 | }
21 | }
22 |
23 | /**
24 | * Recursively filters out dev routes from the route structure in production.
25 | */
26 | function filterDevRoutes(routes: Route[]): Route[] {
27 | if (DEV) return routes
28 |
29 | return routes
30 | .filter(route => DEV || route.dev !== true)
31 | .map(route => ({
32 | ...route,
33 | children: route.children ? filterDevRoutes(route.children) : [],
34 | }))
35 | }
36 |
--------------------------------------------------------------------------------
/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 | Svelte Starter · {data.title}
16 |
17 |
18 |
19 | {#snippet top_nav()}
20 |
21 |
22 | {/snippet}
23 |
24 | {#snippet children()}
25 |
26 | {@render layout_children()}
27 |
28 | {/snippet}
29 |
30 |
31 | {#if DEV && BROWSER}
32 |
33 | {/if}
34 |
--------------------------------------------------------------------------------
/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/routes/about/+page.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 | about
10 |
11 |
--------------------------------------------------------------------------------
/src/routes/design/+page.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 | Svelte Starter uses a minimal css reset, and somewhat opinionated default styles for vanilla
11 | HTML elements.
12 |
13 |
14 |
15 |
16 |
17 | Elements
18 | Stock HTML elements with a minimal CSS reset and a subtle style.
19 |
20 | Inputs
21 | Styled inputs with a minimal CSS reset.
22 |
23 |
24 |
25 |
26 |
27 | CSS
28 |
29 | CSS is a bit of a mess, but Svelte Starter tries to make it a bit more manageable.
30 |
31 |
32 |
33 | Tokens & Utilities
34 |
35 |
36 | A typical workflow usually involves the composition of utility classes from the various
37 | design tokens available, like the theme colors, font definitions, and spacing scale.
38 |
39 |
40 |
41 |
42 |
51 |
--------------------------------------------------------------------------------
/src/routes/design/elements/+page.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 | The most common HTML elements with basic styles.
10 |
11 |
12 | ℹ
14 |
15 | elements.scss
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | <h1>
24 | <h2>
25 | <h3>
26 | <h4>
27 | <h5>
28 | <h6>
29 |
30 | <strong>
31 |
32 | <em>
33 |
34 | <s>
35 |
36 | <small>
37 |
38 | <mark>
39 |
40 |
41 | super
42 | <script>
43 |
44 |
45 |
46 | sub
47 | <script>
48 |
49 |
50 |
51 |
52 | <li>
53 |
54 |
55 | <li>
56 |
59 |
60 |
61 | <li>
62 |
63 | <li>
64 | <li>
65 |
66 |
67 |
68 |
69 |
70 |
71 | Button
72 |
73 | <code />
74 |
75 |
76 |
77 |
78 |
87 |
88 |
89 |
90 |
91 |
92 | Paragraph canary danced beneath azure skies , while marble fountains sparkled with untold mysteries. Velvet shadows crept along ancient walls, whispering secrets to passing travelers who dared not linger.
93 |
94 |
95 |
96 |
97 | Blockquote golden leaves tumbled through forgotten courtyards, each one carrying fragments of memories long since scattered to the winds.
98 |
99 |
100 |
101 | https://some.really-long-link-that-would-normally-cause-horizontal-overflow-but-it-wraps-instead
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
116 |
--------------------------------------------------------------------------------
/src/routes/design/inputs/+page.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 | Stock HTML input elements with basic styles.
8 |
9 |
10 | ℹ
12 |
13 | inputs.scss
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | form
23 |
24 |
44 |
45 |
46 |
47 |
48 | select
49 |
50 | foo
51 | bar
52 |
53 |
54 |
55 |
69 |
70 |
71 |
72 |
73 |
114 |
--------------------------------------------------------------------------------
/src/routes/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 |
3 | describe('sum test', () => {
4 | it('adds 1 + 2 to equal 3', () => {
5 | expect(1 + 2).toBe(3)
6 | })
7 | })
8 |
--------------------------------------------------------------------------------
/src/styles/animations.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --quint-out: cubic-bezier(0.23, 1, 0.32, 1);
3 | --out: cubic-bezier(0.19, 1, 0.78, 1);
4 | }
5 |
6 | @keyframes fade-in {
7 | from {
8 | opacity: 0;
9 | }
10 | to {
11 | opacity: 1;
12 | }
13 | }
14 | @keyframes fade-out {
15 | from {
16 | opacity: 1;
17 | }
18 | to {
19 | opacity: 0;
20 | }
21 | }
22 |
23 | @keyframes rotate-in {
24 | from {
25 | transform: rotate3d(1, 0, 0, 90deg) translate3d(0, 0, -10px);
26 | }
27 | to {
28 | transform: rotate3d(1, 0, 0, 0deg) translate3d(0, 0, -4px);
29 | }
30 | }
31 | @keyframes rotate-out {
32 | from {
33 | transform: rotate3d(1, 0, 0, 0deg) translate3d(0, 0, -10px);
34 | }
35 | to {
36 | transform: rotate3d(1, 0, 0, -90deg) translate3d(0, 0, -10px);
37 | }
38 | }
39 |
40 | @keyframes swipe-in {
41 | from {
42 | clip-path: inset(0 100% 0 0);
43 | opacity: 0;
44 | }
45 | to {
46 | clip-path: inset(0);
47 | opacity: 1;
48 | }
49 | }
50 | @keyframes swipe-out {
51 | from {
52 | clip-path: inset(0);
53 | opacity: 1;
54 | }
55 | to {
56 | clip-path: inset(0 0 0 100%);
57 | opacity: 0;
58 | }
59 | }
60 |
61 | @keyframes fly-in {
62 | from {
63 | transform: translateY(0.5rem);
64 | }
65 | to {
66 | transform: translateY(0);
67 | }
68 | }
69 | @keyframes fly-out {
70 | from {
71 | transform: translateY(0);
72 | }
73 | to {
74 | transform: translateY(0.5rem);
75 | }
76 | }
77 |
78 | @keyframes land-in {
79 | from {
80 | transform: scale(1.2);
81 | }
82 | to {
83 | transform: scale(1);
84 | }
85 | }
86 |
87 | @keyframes expand {
88 | from {
89 | transform: scaleX(0);
90 | }
91 | to {
92 | transform: scaleX(1);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/styles/app.scss:
--------------------------------------------------------------------------------
1 | @use './reset.scss';
2 | @use './theme.scss';
3 |
4 | @use './view-transitions.scss';
5 | @use './animations.scss';
6 | @use './dimensions.scss';
7 | @use './elements.scss';
8 | @use './shadows.scss';
9 | @use './inputs.scss';
10 | @use './utils.scss';
11 | @use './font.scss';
12 | @use './code.scss';
13 |
14 | :root {
15 | --bg-ab: color-mix(in lab, var(--bg-a), var(--bg-b) 20%);
16 |
17 | &.light {
18 | --shadow-lightness: 0.33;
19 | }
20 | }
21 |
22 | html {
23 | font-size: 62.5%;
24 | color-scheme: light dark;
25 | -webkit-font-smoothing: antialiased;
26 | -moz-osx-font-smoothing: grayscale;
27 | }
28 |
29 | body {
30 | display: flex;
31 | flex-direction: column;
32 |
33 | min-height: 100vh;
34 | max-width: 100vw;
35 |
36 | transition:
37 | color 0.5s,
38 | background-color 0.1s;
39 |
40 | overflow-x: hidden;
41 | }
42 |
--------------------------------------------------------------------------------
/src/styles/code.scss:
--------------------------------------------------------------------------------
1 | pre {
2 | box-sizing: border-box;
3 | padding: var(--padding);
4 | outline: 1px solid var(--bg-a);
5 | }
6 |
7 | pre,
8 | :not(pre) > code {
9 | max-width: min(100%, 100vw);
10 | height: max-content;
11 | width: fit-content;
12 |
13 | background: var(--bg-b);
14 | color: var(--fg-a);
15 | border-radius: 3px;
16 | box-shadow: var(--shadow-sm);
17 |
18 | // font-size: 16px;
19 |
20 | overflow-x: auto;
21 | }
22 |
23 | // Inline elements.
24 | :not(pre) > code {
25 | padding: 0.15rem 0.5rem;
26 | }
27 |
28 | code {
29 | font-style: var(--h-dark-font-style);
30 |
31 | span {
32 | color: var(--h-dark);
33 | font-style: var(--h-dark-font-style);
34 | }
35 | }
36 |
37 | :root.light {
38 | pre,
39 | :not(pre) > code {
40 | background: var(--bg-ab);
41 | outline-color: var(--bg-b);
42 | color: var(--h-light);
43 | font-style: var(--h-light-font-style);
44 | }
45 |
46 | code span {
47 | color: var(--h-light);
48 | font-style: var(--h-light-font-style);
49 | }
50 | }
51 |
52 | pre[data-shiki-t-inline] {
53 | display: inline-flex;
54 | padding: 0.25rem 0.5rem;
55 | padding: 0 0.5rem;
56 | }
57 |
--------------------------------------------------------------------------------
/src/styles/dimensions.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --radius-sm: 0.3rem;
3 | --radius: 0.5rem;
4 | --radius-md: 0.7rem;
5 | --radius-lg: 1rem;
6 |
7 | --padding: 0.8rem;
8 | --gap: 2rem;
9 |
10 | --page-width: min(70rem, 100vw);
11 |
12 | --nav-height: 5rem;
13 | --nav-width: 100%;
14 | --secondary-nav-height: 5rem;
15 |
16 | --padding-top: 6rem;
17 | --padding-bottom: 8rem;
18 | --padding-inset: 2rem;
19 | --thick-border-width: 0.3rem;
20 | --border-radius: 0.4rem;
21 | --border-radius-inner: 0.2rem;
22 | --page-content-width: 76rem;
23 | --banner-height: 0px;
24 |
25 | @media screen and (min-width: 480px) {
26 | --padding-inset: 3.2rem;
27 | }
28 |
29 | @media screen and (min-width: 800px) {
30 | --padding-top: 8rem;
31 | --padding-inset: 4.8rem;
32 | --secondary-nav-height: 6rem;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/styles/elements.scss:
--------------------------------------------------------------------------------
1 | @use './theme.scss';
2 |
3 | h1,
4 | h2,
5 | h3,
6 | h4,
7 | h5,
8 | h6 {
9 | color: var(--fg-a);
10 | }
11 |
12 | p,
13 | li,
14 | a,
15 | blockquote {
16 | color: var(--fg-a);
17 |
18 | @media screen and (max-width: 560px) {
19 | word-wrap: break-word;
20 | }
21 | }
22 |
23 | li {
24 | margin-left: 1rem;
25 | list-style-position: inside;
26 | }
27 |
28 | a {
29 | color: color-mix(in lab, var(--fg-a), var(--theme-a) 33%);
30 |
31 | width: fit-content;
32 | text-decoration: none;
33 | box-shadow: 0 1px var(--bg-e);
34 | transition: 0.1s ease-out;
35 |
36 | &:hover {
37 | box-shadow: 0 1px var(--theme-a);
38 | }
39 | }
40 |
41 | a.external {
42 | position: relative;
43 | // padding-right: 2rem;
44 |
45 | &::after {
46 | content: '';
47 |
48 | display: flex;
49 | align-items: center;
50 | justify-content: center;
51 |
52 | position: absolute;
53 | top: 0;
54 | bottom: 0;
55 | right: -16px;
56 |
57 | width: 2rem;
58 | height: 100%;
59 |
60 | // background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor' stroke='currentColor' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m16 8.4l-8.875 8.9q-.3.3-.713.3t-.712-.3q-.3-.3-.3-.713t.3-.712L14.6 7H7q-.425 0-.713-.288T6 6q0-.425.288-.713T7 5h10q.425 0 .713.288T18 6v10q0 .425-.288.713T17 17q-.425 0-.713-.288T16 16V8.4Z'/%3E%3C/svg%3E");
61 | // background-image: url("$lib/icons/user-dark.svg");
62 | background-size: 75%;
63 | background-repeat: no-repeat;
64 | background-position: 100% 50%;
65 | background-color: var(--bg-e);
66 | -webkit-mask-image: url('$lib/icons/external.svg');
67 | -webkit-mask-size: 75%;
68 | -webkit-mask-repeat: no-repeat;
69 | -webkit-mask-position: 100% 50%;
70 | }
71 |
72 | &:hover::after {
73 | color: var(--theme-a);
74 | }
75 | }
76 |
77 | blockquote {
78 | position: relative;
79 |
80 | height: max-content;
81 | padding: 1rem 1rem 1rem 2rem;
82 | padding-left: 2rem;
83 |
84 | border-radius: var(--radius-lg);
85 |
86 | font-style: italic;
87 | outline: 1px solid var(--bg-a);
88 |
89 | color: var(--fg-a);
90 |
91 | &:before {
92 | content: '“';
93 |
94 | position: absolute;
95 | top: 0rem;
96 | left: 0.25rem;
97 |
98 | color: var(--fg-d);
99 |
100 | font-size: 4rem;
101 | }
102 | }
103 |
104 | button,
105 | .btn {
106 | width: fit-content;
107 | padding: var(--padding) calc(var(--padding) * 3);
108 |
109 | color: var(--fg-a);
110 | background: var(--bg-b);
111 | outline: 1px solid var(--bg-b);
112 | border: none;
113 | border-radius: var(--radius);
114 | box-shadow: var(--shadow-sm);
115 |
116 | transition: 0.1s ease-out;
117 |
118 | &:hover {
119 | background: color-mix(in hsl, var(--bg-b) 80%, var(--bg-a));
120 | outline-color: var(--bg-d);
121 | }
122 | &:active {
123 | background: color-mix(in hsl, var(--bg-b) 40%, var(--bg-a));
124 | box-shadow: 0 0 0 var(--shadow-sm);
125 | outline-color: var(--bg-e);
126 | scale: 0.98;
127 | }
128 |
129 | &.accent {
130 | color: var(--fg-a);
131 | outline: 2px solid var(--theme-a);
132 | outline-offset: -1px;
133 |
134 | font-weight: 600;
135 |
136 | &:hover {
137 | background: color-mix(in hsl, var(--theme-a) 80%, var(--bg-a));
138 | outline-color: var(--bg-d);
139 | }
140 | }
141 | }
142 |
143 | section {
144 | display: flex;
145 | flex-direction: column;
146 | gap: var(--padding);
147 |
148 | width: 100%;
149 | max-width: min(var(--page-width), calc(100vw - var(--padding-inset)));
150 | margin: 0 auto;
151 | }
152 |
153 | hr {
154 | width: 100%;
155 | }
156 |
157 | mark {
158 | background: var(--theme-a);
159 |
160 | width: fit-content;
161 | padding: 0 var(--padding);
162 |
163 | clip-path: polygon(10% 0, 100% 0, 90% 100%, 0 100%);
164 | }
165 |
166 | sup a,
167 | sub a {
168 | font: inherit;
169 | }
170 |
171 | em {
172 | font-style: italic !important;
173 | font-synthesis: style;
174 | opacity: 0.8;
175 |
176 | a {
177 | font-style: italic;
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/styles/font.scss:
--------------------------------------------------------------------------------
1 | /* RED HAT TEXT - Varitable Font (wght) */
2 | @font-face {
3 | font-family: 'red_hat_text';
4 | font-weight: 100 900;
5 | font-style: normal;
6 | src: url('/fonts/red_hat_text/red_hat_text.ttf') format('truetype');
7 | }
8 | @font-face {
9 | font-family: 'red_hat_text-italic';
10 | font-weight: 100 900;
11 | font-style: italic;
12 | src: url('/fonts/red_hat_text/red_hat_text-italic.ttf') format('truetype');
13 | }
14 |
15 | /* INCONSOLATA - Variable Font (wght) */
16 | @font-face {
17 | font-family: 'dosis';
18 | font-weight: 100 900;
19 | font-style: normal;
20 | src: url('/fonts/dosis/dosis.ttf') format('truetype');
21 | }
22 |
23 | /* INCONSOLATA - Variable Font (wght, wdth) */
24 | @font-face {
25 | font-family: 'inconsolata';
26 | font-weight: 100 900;
27 | font-style: monospace;
28 | src: url('/fonts/inconsolata/inconsolata.ttf') format('truetype');
29 | }
30 |
31 | :root {
32 | --font-a: 'red_hat_text', system-ui, Inter, Avenir, Helvetica, Arial, sans-serif;
33 | --font-b: 'dosis', system-ui, sans-serif;
34 | --font-m: 'inconsolata', system-ui, monospace;
35 |
36 | --font-xxs: clamp(0.8rem, 2vw, 1rem);
37 | --font-xs: clamp(1rem, 2vw, 1.2rem);
38 | --font-sm: clamp(1.2rem, 2vw, 1.3rem);
39 | --font: clamp(1.6rem, 2.7vw, 1.8rem); // todo: update the others like this one
40 | --font-md: clamp(1.6rem, 2vw, 2rem);
41 | --font-lg: clamp(2rem, 5vw, 2.8rem);
42 | --font-xl: clamp(2.4rem, 5vw, 3.2rem);
43 | --font-xxl: clamp(2.8rem, 7vw, 4.8rem);
44 | --font-xxxl: clamp(4.8rem, 10vw, 8rem);
45 |
46 | /* doing it this way (rather than just `1.5`) means it has a unit, and can be used elsewhere */
47 | --line-height-body: calc(1.5 * var(--font));
48 | --line-height-body-sm: calc(1.5 * var(--font-xs));
49 |
50 | --font-h1: 800 var(--font-xxxl) / 1.2 var(--font-a);
51 | --font-h2: 700 var(--font-xxl) / 1.2 var(--font-a);
52 | --font-h3: 500 var(--font-xl) / 1.2 var(--font-a);
53 | --font-h4: 400 var(--font-lg) / 1.2 var(--font-a);
54 | --font-h5: 400 var(--font-md) / 1.2 var(--font-a);
55 |
56 | --font-body: 400 var(--font) / var(--line-height-body) var(--font-a);
57 | --font-body-sm: 400 var(--font-xs) / var(--line-height-body-sm) var(--font-a);
58 |
59 | --font-ui-sm: 400 var(--font-sm) / 1.5 var(--font-a);
60 | --font-ui: 400 var(--font) / 1.5 var(--font-a);
61 | --font-ui-md: 400 var(--font-md) / 1.5 var(--font-a);
62 | --font-ui-lg: 400 var(--font-lg) / 1.5 var(--font-a);
63 | --font-ui-xl: 400 var(--font-xl) / 1.5 var(--font-a);
64 |
65 | --font-mono: 400 var(--font) / 1.1 var(--font-m);
66 |
67 | font-synthesis: none;
68 | text-rendering: optimizeLegibility;
69 | -webkit-font-smoothing: antialiased;
70 | -moz-osx-font-smoothing: grayscale;
71 | -webkit-text-size-adjust: 100%;
72 | }
73 |
74 | a,
75 | p,
76 | li,
77 | section,
78 | blockquote,
79 | small,
80 | mark,
81 | cite,
82 | figcaption,
83 | button,
84 | input,
85 | textarea,
86 | select,
87 | option,
88 | label,
89 | legend,
90 | fieldset {
91 | font: var(--font-body);
92 | }
93 |
94 | pre,
95 | code {
96 | font: var(--font-mono);
97 | }
98 |
99 | h1 {
100 | font: var(--font-h1);
101 | }
102 |
103 | h2 {
104 | font: var(--font-h2);
105 | }
106 |
107 | h3 {
108 | font: var(--font-h3);
109 | }
110 |
111 | h4 {
112 | font: var(--font-h4);
113 | }
114 |
115 | h5 {
116 | font: var(--font-h5);
117 | }
118 |
--------------------------------------------------------------------------------
/src/styles/inputs.scss:
--------------------------------------------------------------------------------
1 | input,
2 | select,
3 | textarea {
4 | --background: color-mix(in srgb, var(--bg-a), var(--bg-b) 42%);
5 | --outline: color-mix(in hsl, var(--bg-b), var(--bg-c) 33%);
6 | --outline-hover: var(--bg-c);
7 | --outline-focus: var(--bg-e);
8 |
9 | position: relative;
10 | margin-top: var(--padding);
11 | accent-color: var(--theme-a);
12 | outline-color: var(--outline);
13 | border: none;
14 |
15 | &:not([type='range']) {
16 | box-shadow: var(--shadow-sm);
17 | }
18 | }
19 |
20 | input,
21 | select,
22 | textarea,
23 | input::-webkit-outer-spin-button,
24 | input::-webkit-inner-spin-button,
25 | input::-webkit-slider-thumb,
26 | input::-webkit-slider-runnable-track {
27 | position: relative;
28 | border: none;
29 |
30 | font-size: var(--font-xs);
31 |
32 | transition-duration: 0.1s;
33 | transition-property: border-color, outline-color, background-color, color;
34 |
35 | &:hover {
36 | outline-color: var(--outline-hover);
37 | }
38 |
39 | &:focus-visible {
40 | outline-color: var(--outline-focus);
41 | }
42 |
43 | &:active,
44 | &:focus {
45 | &::placeholder {
46 | color: transparent;
47 | }
48 | }
49 | }
50 |
51 | input[type='text'],
52 | input[type='number'],
53 | select,
54 | textarea {
55 | width: 100%;
56 | padding: 0.2rem 1rem;
57 |
58 | color: var(--fg-a);
59 | background-color: var(--background);
60 | border-radius: var(--radius);
61 | outline-width: 1px;
62 | outline-style: solid;
63 | }
64 |
65 | textarea {
66 | padding: 0.75rem 1rem;
67 | }
68 |
69 | input::placeholder,
70 | textarea::placeholder {
71 | transition: 1s;
72 | transition: none;
73 | color: var(--bg-c);
74 |
75 | :root.dark & {
76 | color: var(--bg-e);
77 | }
78 | }
79 | input:focus::placeholder,
80 | textarea:focus::placeholder {
81 | color: transparent;
82 | }
83 |
84 | input::-webkit-outer-spin-button,
85 | input::-webkit-inner-spin-button {
86 | outline-style: 1px solid var(--outline);
87 | outline-width: 1px;
88 | border-radius: 2px;
89 | cursor: pointer;
90 | }
91 |
92 | select,
93 | input[type='checkbox'],
94 | input[type='radio'] {
95 | cursor: pointer;
96 | }
97 |
98 | input[type='radio'] {
99 | appearance: none;
100 | width: 1rem;
101 | height: 1rem;
102 | border-radius: 50%;
103 | background-color: var(--outline);
104 | outline: 1px solid var(--bg-d);
105 | cursor: pointer;
106 | pointer-events: all;
107 |
108 | &:hover {
109 | background-color: var(--bg-e);
110 | background-color: var(--bg-d);
111 | }
112 |
113 | &:checked {
114 | background-color: var(--theme-a);
115 | }
116 | }
117 |
118 | input[type='range'] {
119 | position: relative;
120 | appearance: none;
121 |
122 | width: 100%;
123 | margin: 0;
124 |
125 | background: none;
126 | outline: none;
127 | border-radius: var(--radius);
128 |
129 | &::before {
130 | content: '';
131 | $h: 6px;
132 | box-sizing: border-box;
133 | display: block;
134 | position: absolute;
135 | top: calc(50% - $h / 2);
136 | left: 0;
137 | right: 0;
138 |
139 | width: calc(100% - var(--padding) * 2);
140 | height: $h;
141 | margin: 0 auto;
142 |
143 | border-radius: 50px;
144 | background: var(--bg-b);
145 |
146 | transition: 0.15s;
147 |
148 | pointer-events: none;
149 | }
150 |
151 | &:hover {
152 | background-color: color-mix(in hsl, var(--bg-a), var(--bg-b) 25%);
153 | &::before {
154 | outline-color: var(--outline-hover);
155 | background-color: color-mix(in hsl, var(--bg-b), var(--bg-c));
156 | }
157 | }
158 |
159 | &::-webkit-slider-runnable-track {
160 | cursor: pointer;
161 | position: relative;
162 |
163 | width: 100%;
164 |
165 | transition-duration: 0.15s;
166 | transition-property: outline-color, background-color;
167 | }
168 |
169 | &::-webkit-slider-thumb {
170 | cursor: pointer;
171 | appearance: none;
172 | position: relative;
173 |
174 | width: 12px;
175 | height: 12px;
176 |
177 | border-radius: 20px;
178 | background-color: var(--fg-d);
179 | box-shadow: 0 0 1rem transparent;
180 |
181 | transition-duration: 0.3s;
182 | transition-property: background-color, box-shadow;
183 | }
184 | &:active::-webkit-slider-thumb {
185 | background-color: var(--theme-a);
186 | box-shadow: 0 0 1rem var(--theme-a);
187 | }
188 |
189 | &:focus-visible,
190 | &:active {
191 | &::-webkit-slider-thumb {
192 | outline-color: var(--theme-a);
193 | }
194 | &::before {
195 | outline-color: var(--outline-focus);
196 | background-color: var(--bg-c);
197 | }
198 | }
199 | }
200 |
201 | .label,
202 | label:has(input, select, textarea) {
203 | color: var(--fg-c);
204 | font-family: var(--font);
205 | font-size: var(--font-xs);
206 | }
207 |
--------------------------------------------------------------------------------
/src/styles/reset.scss:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | /**
6 | ** Hard normalise elements
7 | */
8 | html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,/* li ,*/fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video {
9 | margin: 0;
10 | padding: 0;
11 | }
12 |
13 | :root,
14 | html,
15 | body {
16 | min-height: 100%;
17 | height: auto;
18 |
19 | /**
20 | ** Prevent font size shifting on IOS.
21 | */
22 | -webkit-text-size-adjust: 100%;
23 |
24 | /**
25 | ** Better tab size.
26 | */
27 | -moz-tab-size: 4;
28 | tab-size: 4;
29 |
30 | /**
31 | ** Better fallback fonts.
32 | */
33 | // prettier-ignore
34 | font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';
35 | }
36 |
37 | hr {
38 | /**
39 | ** Fix firefox weirdness: https://bugzilla.mozilla.org/show_bug.cgi?id=190655
40 | */
41 | height: 0;
42 | color: inherit;
43 | }
44 |
45 | /**
46 | ** Normalise italics
47 | */
48 | em,
49 | i,
50 | cite,
51 | q,
52 | address,
53 | dfn,
54 | var {
55 | font-style: italic;
56 | }
57 |
58 | /**
59 | ** Stop line height being effected
60 | */
61 | sub,
62 | sup {
63 | font-size: 75%;
64 | line-height: 0;
65 | position: relative;
66 | vertical-align: baseline;
67 | }
68 |
69 | sub {
70 | bottom: -0.25em;
71 | }
72 |
73 | sup {
74 | top: -0.5em;
75 | }
76 |
77 | u {
78 | text-decoration: underline;
79 | }
80 |
81 | button,
82 | [type='button'],
83 | [type='reset'],
84 | [type='submit'] {
85 | /**
86 | ** Correct the inability to style clickable types in iOS and Safari
87 | */
88 | -webkit-appearance: button;
89 |
90 | /**
91 | ** Set pointer type
92 | */
93 | cursor: pointer;
94 | }
95 |
96 | /**
97 | ** Remove styling for invalid elements
98 | * (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
99 | */
100 | :-moz-ui-invalid {
101 | box-shadow: none;
102 | }
103 |
104 | /**
105 | ** Add the correct vertical alignment in Chrome and Firefox
106 | */
107 | progress {
108 | vertical-align: baseline;
109 | }
110 |
111 | /**
112 | ** Correct the cursor style of increment and decrement buttons in Safari
113 | */
114 | ::-webkit-inner-spin-button,
115 | ::-webkit-outer-spin-button {
116 | height: auto;
117 | }
118 |
119 | ::-webkit-file-upload-button {
120 | /**
121 | ** Correct the inability to style clickable types in iOS and Safari
122 | */
123 | -webkit-appearance: button;
124 |
125 | /**
126 | ** Change font properties to 'inherit' in Safari
127 | */
128 | font: inherit;
129 | }
130 |
131 | /**
132 | ** Add the correct display in Chrome and Safari
133 | */
134 | summary {
135 | display: list-item;
136 | }
137 |
138 | /**
139 | ** Contain overflow on specific elements (opinions)
140 | */
141 | figure,
142 | pre {
143 | overflow-x: auto;
144 | }
145 |
146 | /**
147 | ** Remove all animations for those that want them to be off
148 | */
149 | @media (prefers-reduced-motion: reduce) {
150 | html {
151 | scroll-behavior: auto;
152 | }
153 |
154 | *,
155 | *::before,
156 | *::after {
157 | animation-duration: 0.01ms !important;
158 | animation-iteration-count: 1 !important;
159 | transition-duration: 0.01ms !important;
160 | scroll-behavior: auto !important;
161 | }
162 | }
163 |
164 | /**
165 | ** Ensure certain elements are set to block by default
166 | */
167 | article,
168 | aside,
169 | details,
170 | figcaption,
171 | figure,
172 | footer,
173 | header,
174 | hgroup,
175 | menu,
176 | nav,
177 | section {
178 | display: block;
179 | }
180 |
--------------------------------------------------------------------------------
/src/styles/shadows.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --shadow-lightness: 0.5;
3 |
4 | --shadow-sm: 0rem 0.0313rem 0.0469rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.04)),
5 | 0rem 0.125rem 0.0938rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.04)),
6 | 0rem 0.15rem 0.125rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.05)),
7 | 0rem 0.1875rem 0.1875rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.1)),
8 | 0rem 0.3125rem 0.3125rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.1)),
9 | 0rem 0.4375rem 0.625rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.15));
10 |
11 | --shadow: 0rem 0.0469rem 0.0625rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.03)),
12 | 0rem 0.15rem 0.125rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.04)),
13 | 0rem 0.28rem 0.1875rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.05)),
14 | 0rem 0.3125rem 0.3125rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.065)),
15 | 0rem 0.625rem 0.625rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.09)),
16 | 0rem 0.625rem 1.25rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.1));
17 |
18 | --shadow-lg: 0rem 0.078rem 0.0625rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.06)),
19 | 0rem 0.15rem 0.15rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.07)),
20 | 0rem 0.28rem 0.3125rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.08)),
21 | 0rem 0.3125rem 0.5rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.1)),
22 | 0rem 0.625rem 0.9375rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.1)),
23 | 0rem 1.25rem 1.875rem rgba(0, 0, 0, calc(var(--shadow-lightness) * 0.01));
24 | }
25 |
--------------------------------------------------------------------------------
/src/styles/theme.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --theme-a: #57b1ff;
3 | --theme-b: #ffcc8b;
4 | --theme-c: #ff8ba9;
5 | --always-dark: #0b0e11;
6 | }
7 |
8 | :root {
9 | --dark-a: #0b0b11;
10 | --dark-b: #15161d;
11 | --dark-c: #1f202d;
12 | --dark-d: #353746;
13 | --dark-e: #474a5b;
14 | --light-a: #ffffff;
15 | --light-b: #dfe1e9;
16 | --light-c: #babeca;
17 | --light-d: #777d8f;
18 | --light-e: #5f6377;
19 |
20 | --bg-a: light-dark(var(--light-a), var(--dark-a));
21 | --bg-b: light-dark(var(--light-b), var(--dark-b));
22 | --bg-c: light-dark(var(--light-c), var(--dark-c));
23 | --bg-d: light-dark(var(--light-d), var(--dark-d));
24 | --bg-e: light-dark(var(--light-e), var(--dark-e));
25 | --fg-a: light-dark(var(--dark-a), var(--light-a));
26 | --fg-b: light-dark(var(--dark-b), var(--light-b));
27 | --fg-c: light-dark(var(--dark-c), var(--light-c));
28 | --fg-d: light-dark(var(--dark-d), var(--light-d));
29 | --fg-e: light-dark(var(--dark-e), var(--light-e));
30 | }
31 |
32 | :root.dark {
33 | color-scheme: dark;
34 | }
35 | :root.light {
36 | color-scheme: light;
37 | }
38 |
39 | :root,
40 | :root[data-theme='default'] {
41 | --theme-a: #57b1ff;
42 | --theme-b: #ffcc8b;
43 | --theme-c: #ff8ba9;
44 | }
45 |
46 | :root[data-theme='autumn'] {
47 | --theme-a: #ff9a3d;
48 | --theme-b: #ff5e5e;
49 | --theme-c: #9b51e0;
50 | }
51 |
52 | :root[data-theme='neon'] {
53 | --theme-a: #00ff95;
54 | --theme-b: #00e1ff;
55 | --theme-c: #ff007c;
56 | }
57 |
58 | :root[data-theme='mellow'] {
59 | --theme-a: #ff9a9e;
60 | --theme-b: #fad0c4;
61 | --theme-c: #f093fb;
62 | }
63 |
64 | :root[data-theme='cyberpunk'] {
65 | --theme-a: #00ff99;
66 | --theme-b: #00c9a7;
67 | --theme-c: #c964ff;
68 | }
69 |
--------------------------------------------------------------------------------
/src/styles/utils.scss:
--------------------------------------------------------------------------------
1 | br-xs,
2 | .br-xs {
3 | height: 0.5rem;
4 | }
5 |
6 | br-sm,
7 | .br-sm {
8 | height: 1rem;
9 | }
10 |
11 | br-md,
12 | .br-md {
13 | height: 4rem;
14 | @media screen and (max-width: 831px) {
15 | height: 3rem;
16 | }
17 | }
18 |
19 | br-lg,
20 | .br-lg {
21 | height: 6.5rem;
22 | @media screen and (max-width: 831px) {
23 | height: 5rem;
24 | }
25 | }
26 |
27 | br-xl,
28 | .br-xl {
29 | height: 10rem;
30 | @media screen and (max-width: 831px) {
31 | height: 6rem;
32 | }
33 | }
34 |
35 | .center {
36 | justify-content: center;
37 | text-align: center;
38 | margin: 0 auto;
39 | }
40 |
41 | .row {
42 | display: flex;
43 | flex-direction: row;
44 | align-items: center;
45 | }
46 |
47 | .col {
48 | display: flex;
49 | flex-direction: column;
50 | align-items: center;
51 | }
52 |
53 | .flex {
54 | display: flex;
55 | }
56 |
--------------------------------------------------------------------------------
/src/styles/view-transitions.scss:
--------------------------------------------------------------------------------
1 | ::view-transition-old(root),
2 | ::view-transition-new(root) {
3 | animation-duration: 0s;
4 | }
5 |
6 | // @view-transition {
7 | // navigation: auto;
8 | // }
9 | // @media (prefers-reduced-motion) {
10 | // @view-transition {
11 | // navigation: none;
12 | // }
13 | // }
14 |
--------------------------------------------------------------------------------
/static/Screenshot 2024-09-24 at 7.44.09 PM 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braebo/svelte-starter/e5c3a0fe404c6c3323c2bc93480c34dfbc7eb2c4/static/Screenshot 2024-09-24 at 7.44.09 PM 1.png
--------------------------------------------------------------------------------
/static/Screenshot 2024-09-24 at 7.44.09 PM 2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braebo/svelte-starter/e5c3a0fe404c6c3323c2bc93480c34dfbc7eb2c4/static/Screenshot 2024-09-24 at 7.44.09 PM 2.png
--------------------------------------------------------------------------------
/static/Screenshot 2024-09-24 at 7.48.41 PM 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braebo/svelte-starter/e5c3a0fe404c6c3323c2bc93480c34dfbc7eb2c4/static/Screenshot 2024-09-24 at 7.48.41 PM 1.png
--------------------------------------------------------------------------------
/static/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braebo/svelte-starter/e5c3a0fe404c6c3323c2bc93480c34dfbc7eb2c4/static/android-chrome-192x192.png
--------------------------------------------------------------------------------
/static/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braebo/svelte-starter/e5c3a0fe404c6c3323c2bc93480c34dfbc7eb2c4/static/android-chrome-512x512.png
--------------------------------------------------------------------------------
/static/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braebo/svelte-starter/e5c3a0fe404c6c3323c2bc93480c34dfbc7eb2c4/static/apple-touch-icon.png
--------------------------------------------------------------------------------
/static/assets/noise-361x370.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braebo/svelte-starter/e5c3a0fe404c6c3323c2bc93480c34dfbc7eb2c4/static/assets/noise-361x370.png
--------------------------------------------------------------------------------
/static/assets/svelte-starter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braebo/svelte-starter/e5c3a0fe404c6c3323c2bc93480c34dfbc7eb2c4/static/assets/svelte-starter.png
--------------------------------------------------------------------------------
/static/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #00d0ff
7 |
8 |
9 |
--------------------------------------------------------------------------------
/static/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braebo/svelte-starter/e5c3a0fe404c6c3323c2bc93480c34dfbc7eb2c4/static/favicon-16x16.png
--------------------------------------------------------------------------------
/static/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braebo/svelte-starter/e5c3a0fe404c6c3323c2bc93480c34dfbc7eb2c4/static/favicon-32x32.png
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braebo/svelte-starter/e5c3a0fe404c6c3323c2bc93480c34dfbc7eb2c4/static/favicon.ico
--------------------------------------------------------------------------------
/static/fonts/inconsolata/inconsolata.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braebo/svelte-starter/e5c3a0fe404c6c3323c2bc93480c34dfbc7eb2c4/static/fonts/inconsolata/inconsolata.ttf
--------------------------------------------------------------------------------
/static/fonts/readme.md:
--------------------------------------------------------------------------------
1 | TODO: Don't forget to delete unused fonts!
--------------------------------------------------------------------------------
/static/fonts/red_hat_text/red_hat_text-italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braebo/svelte-starter/e5c3a0fe404c6c3323c2bc93480c34dfbc7eb2c4/static/fonts/red_hat_text/red_hat_text-italic.ttf
--------------------------------------------------------------------------------
/static/fonts/red_hat_text/red_hat_text.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braebo/svelte-starter/e5c3a0fe404c6c3323c2bc93480c34dfbc7eb2c4/static/fonts/red_hat_text/red_hat_text.ttf
--------------------------------------------------------------------------------
/static/icons/check-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/icons/check-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/icons/chevron.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/static/icons/copy-to-clipboard-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/icons/copy-to-clipboard-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/icons/document-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/static/icons/document-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/static/icons/font-accessible-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/icons/font-accessible-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/icons/font-boring-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/icons/font-boring-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/icons/font-elegant-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/icons/font-elegant-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/icons/hash-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/static/icons/hash-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/static/icons/home.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/icons/link.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/static/icons/menu.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/static/icons/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/static/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braebo/svelte-starter/e5c3a0fe404c6c3323c2bc93480c34dfbc7eb2c4/static/mstile-150x150.png
--------------------------------------------------------------------------------
/static/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/static/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-starter",
3 | "short_name": "svelte-starter",
4 | "start_url": "/",
5 | "icons": [
6 | {
7 | "src": "/android-chrome-192x192.png",
8 | "sizes": "192x192",
9 | "type": "image/png"
10 | },
11 | {
12 | "src": "/android-chrome-512x512.png",
13 | "sizes": "512x512",
14 | "type": "image/png"
15 | }
16 | ],
17 | "theme_color": "#00d0ff",
18 | "background_color": "#3d3d3d",
19 | "display": "standalone",
20 | "description": "Braebo's personal sveltekit starter template"
21 | }
22 |
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import { createShikiLogger, processCodeblockSync, getOrLoadOpts } from '@samplekit/preprocess-shiki'
2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
3 | import mdsvexConfig from './mdsvex.config.mjs'
4 | import adapter from '@sveltejs/adapter-auto'
5 | import { mdsvex } from 'mdsvex'
6 |
7 | const opts = await getOrLoadOpts()
8 | const preprocessorRoot = `${import.meta.dirname}/src/routes/`
9 | const formatFilename = (/** @type {string} */ filename) => filename.replace(preprocessorRoot, '')
10 |
11 | const svelte_ignores = [
12 | 'element_invalid_self_closing_tag',
13 | 'no_static_element_interactions',
14 | 'a11y_click_events_have_key_events',
15 | ]
16 |
17 | /** @type {import('@sveltejs/kit').Config} */
18 | const config = {
19 | extensions: ['.svelte', ...mdsvexConfig.extensions],
20 | preprocess: [
21 | processCodeblockSync({
22 | include: filename => filename.startsWith(preprocessorRoot),
23 | logger: createShikiLogger(formatFilename),
24 | opts,
25 | }),
26 | vitePreprocess({ script: true }),
27 | mdsvex(mdsvexConfig),
28 | ],
29 | kit: {
30 | adapter: adapter(),
31 | // prerender: {
32 | // handleHttpError: ({ path, message }) => {
33 | // if (path === '/404') return
34 |
35 | // throw new Error(message)
36 | // },
37 | // },
38 | },
39 | vitePlugin: {
40 | inspector: {
41 | toggleButtonPos: 'bottom-left',
42 | toggleKeyCombo: 'meta-alt-control',
43 | },
44 | },
45 | onwarn: (warning, handler) => {
46 | if (svelte_ignores.includes(warning.code)) return
47 | handler(warning)
48 | },
49 | warningFilter: warning => {
50 | return !svelte_ignores.includes(warning.code)
51 | },
52 | }
53 |
54 | export default config
55 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "moduleResolution": "Bundler",
5 | "module": "ESNext",
6 | "lib": ["ESNext"],
7 | "target": "ESNext",
8 | "types": ["node", "vitest", "@sveltejs/kit"],
9 |
10 | "strict": true,
11 | "sourceMap": true,
12 | "skipLibCheck": true,
13 |
14 | "allowJs": true,
15 | "checkJs": true,
16 |
17 | "esModuleInterop": true,
18 | "isolatedModules": true,
19 | "resolveJsonModule": true,
20 | "verbatimModuleSyntax": true,
21 | "forceConsistentCasingInFileNames": true
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "github": {
3 | "silent": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite'
2 | import autoprefixer from 'autoprefixer'
3 | import { defineConfig } from 'vite'
4 | import Sonda from 'sonda/sveltekit'
5 |
6 | const DEV = process.env.NODE_ENV === 'development'
7 |
8 | export default defineConfig({
9 | plugins: [sveltekit(), DEV && Sonda({ server: true })],
10 | build: { sourcemap: DEV },
11 | server: { allowedHosts: [] },
12 | css: {
13 | postcss: { plugins: [autoprefixer()] },
14 | },
15 | test: {
16 | include: ['src/**/*.{test,spec}.{js,ts}', 'scripts/**/*.{test,spec}.{js,ts}'],
17 | },
18 | })
19 |
--------------------------------------------------------------------------------