11 |
12 | export interface ClickOutsideOptions {
13 | /**
14 | * Array of classnames. If the click target element has one of these classes, it will not be considered an outclick.
15 | */
16 | whitelist?: string[]
17 | }
18 |
19 | // Attributes applied to the element that does use:clickOutside
20 | export interface ClickOutsideAttr {
21 | 'on:outclick'?: (event: ClickOutsideEvent) => void
22 | }
23 |
24 | /**
25 | * Calls a function when the user clicks outside the element.
26 | * @example
27 | * ```svelte
28 | *
29 | * ```
30 | */
31 | export const clickOutside: Action = (
32 | node,
33 | options?: ClickOutsideOptions,
34 | ) => {
35 | const handleClick = (event: MouseEvent) => {
36 | let disable = false
37 |
38 | for (const className of options?.whitelist || []) {
39 | if (event.target instanceof Element && event.target.classList.contains(className)) {
40 | disable = true
41 | }
42 | }
43 |
44 | if (!disable && node && !node.contains(event.target as Node) && !event.defaultPrevented) {
45 | node.dispatchEvent(
46 | new CustomEvent('outclick', {
47 | detail: {
48 | target: event.target as HTMLElement,
49 | },
50 | }),
51 | )
52 | }
53 | }
54 |
55 | document.addEventListener('click', handleClick, true)
56 |
57 | return {
58 | update: (newOptions) => (options = { ...options, ...newOptions }),
59 | destroy() {
60 | document.removeEventListener('click', handleClick, true)
61 | },
62 | }
63 | }
--------------------------------------------------------------------------------
/www/src/utils/dedent.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Trims a multiline string by an amount equal to the least indented line.
3 | * Leading and trailing newlines are removed.
4 | *
5 | * If the first line is _not_ empty, it will be _indented_ by the
6 | * same amount as the rest is _un-indented_.
7 | *
8 | * @returns The dedented string.
9 | */
10 | export function dedent(
11 | /**
12 | * The string to dedent.
13 | */
14 | string: string,
15 | options = {
16 | /**
17 | * Whether to trim the last line if its empty.
18 | * @defaultValue `true`
19 | */
20 | trimEndingNewline: true,
21 | },
22 | ): string {
23 | const lines = string.split('\n')
24 | const leadingNewline = string[0] !== '\n'
25 | const indent = lines
26 | .filter((line, i) => {
27 | if (leadingNewline && i === 0) {
28 | return false
29 | }
30 | return line.trim()
31 | })
32 | .map(line => line.match(/^\s*/)?.[0].length)
33 | .filter(indent => indent !== undefined)
34 | // @ts-ignore - Astro is hallucinating errors here and throwing on build
35 | .reduce((a, b) => Math.min(a, b), 9999)
36 |
37 | if (leadingNewline) {
38 | // @ts-ignore - Astro is hallucinating errors here and throwing on build
39 | lines[0] = ' '.repeat(indent) + lines[0]
40 | } else {
41 | lines.shift()
42 | }
43 |
44 | if (options.trimEndingNewline) {
45 | if (lines.at(-1) === '') {
46 | lines.pop()
47 | }
48 | }
49 |
50 | return lines.map(line => line.slice(indent)).join('\n')
51 | }
52 |
--------------------------------------------------------------------------------
/www/src/utils/gridColor.ts:
--------------------------------------------------------------------------------
1 | import { quintOut } from 'svelte/easing'
2 | import { tweened } from 'svelte/motion'
3 |
4 | export const gridColors = {
5 | greyscale: [0.1, 0.1, 0.1],
6 | purple: [0.57, 0.23, 1],
7 | cyan: [0.2, 0.77, 0.96],
8 | red: [1, 0.23137254901960785, 0.5254901960784314],
9 | blue: [0.27450980392156865, 0.5215686274509804, 0.9254901960784314],
10 | }
11 |
12 | export const gridColor = tweened(gridColors.greyscale, {
13 | duration: 1500,
14 | easing: quintOut,
15 | })
16 |
17 | export const gridYeet = tweened(1.0, {
18 | duration: 1500,
19 | easing: quintOut,
20 | })
21 |
--------------------------------------------------------------------------------
/www/src/utils/nanoid.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Generate a random ID.
3 | * @param length The length of the ID to generate. Default: `21`
4 | */
5 | export function nanoid(length = 21) {
6 | return crypto
7 | .getRandomValues(new Uint8Array(length))
8 | .reduce(
9 | (t, e) =>
10 | (t +=
11 | (e &= 63) < 36
12 | ? e.toString(36)
13 | : e < 62
14 | ? (e - 26).toString(36).toUpperCase()
15 | : e > 62
16 | ? '-'
17 | : '_'),
18 | '',
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/www/src/utils/teleportIntoView.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Like `scrollIntoView({ behavior: 'instant' })` with a quick fade/transform animation.
3 | */
4 | export function teleportIntoView(
5 | target: Element,
6 | {
7 | yAmount = 0.5,
8 | durationOut = 200,
9 | durationIn = 300,
10 | easingOut = 'ease',
11 | easeIn = 'ease-out',
12 | } = {},
13 | ) {
14 | // return if the target is already in view
15 |
16 | setTimeout(async () => {
17 | const rect = target.getBoundingClientRect()
18 |
19 | if (rect.top >= -100 && rect.top <= window.innerHeight / 3) {
20 | // The element is in the viewport
21 | return
22 | }
23 |
24 | const direction = rect.top > 0 ? 1 : -1
25 |
26 | await document.querySelector('.page')?.animate(
27 | [
28 | { opacity: 1, transform: 'translateY(0)' },
29 | {
30 | opacity: 0,
31 | transform: `translateY(${-direction * yAmount}rem)`,
32 | },
33 | ],
34 | {
35 | duration: durationOut,
36 | easing: easingOut,
37 | },
38 | ).finished
39 | target.scrollIntoView({
40 | behavior: 'instant',
41 | block: 'start',
42 | })
43 | document.querySelector('.page')?.animate(
44 | [
45 | {
46 | opacity: 0,
47 | transform: `translateY(${direction * yAmount}rem)`,
48 | },
49 | { opacity: 1, transform: 'translateY(0)' },
50 | ],
51 | {
52 | duration: durationIn,
53 | easing: easeIn,
54 | },
55 | )
56 | })
57 | }
58 |
--------------------------------------------------------------------------------
/www/svelte.config.js:
--------------------------------------------------------------------------------
1 | import { vitePreprocess } from '@astrojs/svelte';
2 |
3 | export default {
4 | preprocess: vitePreprocess(),
5 | }
6 |
--------------------------------------------------------------------------------
/www/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "strict": true,
6 | "lib": ["DOM", "ESNext"],
7 | "skipLibCheck": true,
8 | "allowJs": true,
9 | "checkJs": true,
10 | "noEmit": true,
11 | "experimentalDecorators": true,
12 | "emitDecoratorMetadata": true
13 | },
14 | "exclude": ["node_modules", "dist"]
15 | }
16 |
--------------------------------------------------------------------------------