Run prettier

This commit is contained in:
venashial
2022-06-04 00:46:23 -07:00
parent 597c071c3d
commit c9ec9f14de
49 changed files with 1515 additions and 1469 deletions

View File

@@ -1,102 +1,100 @@
<script lang="ts">
// TODO: Make square icon `md` more rounded
// TODO: Make square icon `md` more rounded
import { onMount } from 'svelte';
import { classCombine } from '../utils/classCombine';
import { onMount } from 'svelte'
import { classCombine } from '../utils/classCombine'
/** Optional, as a default icon will be substituted if no image was specified */
export let src: string | undefined;
export let size: 'xs' | 'sm' | 'md' | 'lg';
export let circle = false;
export let floatUp = false;
/** Optional, as a default icon will be substituted if no image was specified */
export let src: string | undefined
export let size: 'xs' | 'sm' | 'md' | 'lg'
export let circle = false
export let floatUp = false
let className: string;
$: className = classCombine([
'avatar',
circle && 'avatar--circle',
`avatar--size-${size}`,
floatUp && 'avatar--float-up',
]);
let className: string
$: className = classCombine([
'avatar',
circle && 'avatar--circle',
`avatar--size-${size}`,
floatUp && 'avatar--float-up',
])
let img;
let img
onMount(() => {
if (img && img.naturalWidth) {
const isPixelated = () => {
if (img.naturalWidth < 96 && img.naturalWidth > 0) {
img.style = 'image-rendering: pixelated;';
}
};
onMount(() => {
if (img && img.naturalWidth) {
const isPixelated = () => {
if (img.naturalWidth < 96 && img.naturalWidth > 0) {
img.style = 'image-rendering: pixelated;'
}
}
if (img.naturalWidth) {
isPixelated();
} else {
img.onload = isPixelated;
}
}
});
if (img.naturalWidth) {
isPixelated()
} else {
img.onload = isPixelated
}
}
})
</script>
{#if src}
<img {src} bind:this={img} class={className} alt="" />
<img {src} bind:this={img} class={className} alt="" />
{:else}
<svg
class={className}
xml:space="preserve"
fill-rule="evenodd"
stroke-linecap="round"
stroke-linejoin="round"
stroke-miterlimit="1.5"
clip-rule="evenodd"
viewBox="0 0 104 104"
>
<path fill="none" d="M0 0h103.4v103.4H0z" />
<path
fill="none"
stroke="#9a9a9a"
stroke-width="5"
d="M51.7 92.5V51.7L16.4 31.3l35.3 20.4L87 31.3 51.7 11 16.4 31.3v40.8l35.3 20.4L87 72V31.3L51.7 11"
/>
</svg>
<svg
class={className}
xml:space="preserve"
fill-rule="evenodd"
stroke-linecap="round"
stroke-linejoin="round"
stroke-miterlimit="1.5"
clip-rule="evenodd"
viewBox="0 0 104 104">
<path fill="none" d="M0 0h103.4v103.4H0z" />
<path
fill="none"
stroke="#9a9a9a"
stroke-width="5"
d="M51.7 92.5V51.7L16.4 31.3l35.3 20.4L87 31.3 51.7 11 16.4 31.3v40.8l35.3 20.4L87 72V31.3L51.7 11" />
</svg>
{/if}
<style lang="postcss">
.avatar {
border-radius: var(--rounded);
box-shadow: var(--shadow-inset-lg), var(--shadow-raised-lg);
height: var(--size);
width: var(--size);
background-color: var(--color-button-bg);
.avatar {
border-radius: var(--rounded);
box-shadow: var(--shadow-inset-lg), var(--shadow-raised-lg);
height: var(--size);
width: var(--size);
background-color: var(--color-button-bg);
&--size {
&-xs {
--size: 2.25rem;
box-shadow: var(--shadow-inset), var(--shadow-raised);
}
&--size {
&-xs {
--size: 2.25rem;
box-shadow: var(--shadow-inset), var(--shadow-raised);
}
&-sm {
--size: 3rem;
box-shadow: var(--shadow-inset), var(--shadow-raised);
}
&-sm {
--size: 3rem;
box-shadow: var(--shadow-inset), var(--shadow-raised);
}
&-md {
--size: 6rem;
border-radius: var(--rounded-lg);
}
&-md {
--size: 6rem;
border-radius: var(--rounded-lg);
}
&-lg {
--size: 9rem;
border-radius: var(--rounded-lg);
}
}
&-lg {
--size: 9rem;
border-radius: var(--rounded-lg);
}
}
&--float-up {
margin-top: calc(var(--size) * (-2 / 3));
z-index: 1;
}
&--float-up {
margin-top: calc(var(--size) * (-2 / 3));
z-index: 1;
}
&--circle {
border-radius: 50%;
}
}
&--circle {
border-radius: 50%;
}
}
</style>

View File

@@ -1,51 +1,51 @@
<script lang="ts">
export let label = ''
/** Supports `green`, `yellow`, `red`, & `gray` */
export let color = 'gray'
export let label = ''
/** Supports `green`, `yellow`, `red`, & `gray` */
export let color = 'gray'
</script>
<div class="badge badge--color-{color}">
{label}
{label}
</div>
<style lang="postcss">
.badge {
font-weight: var(--font-weight-bold);
display: inline;
position: relative;
padding-left: 0.9rem;
line-height: 1rem;
.badge {
font-weight: var(--font-weight-bold);
display: inline;
position: relative;
padding-left: 0.9rem;
line-height: 1rem;
&--color-green {
color: var(--color-badge-green-text);
--color-dot: var(--color-badge-green-dot);
}
&--color-green {
color: var(--color-badge-green-text);
--color-dot: var(--color-badge-green-dot);
}
&--color-yellow {
color: var(--color-badge-yellow-text);
--color-dot: var(--color-badge-yellow-dot);
}
&--color-yellow {
color: var(--color-badge-yellow-text);
--color-dot: var(--color-badge-yellow-dot);
}
&--color-red {
color: var(--color-badge-red-text);
--color-dot: var(--color-badge-red-dot);
}
&--color-red {
color: var(--color-badge-red-text);
--color-dot: var(--color-badge-red-dot);
}
&--color-gray {
color: var(--color-badge-gray-text);
--color-dot: var(--color-badge-gray-dot);
}
&::before {
content: '';
display: inline-block;
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background-color: var(--color-dot);
position: absolute;
left: 0;
bottom: 25%;
}
}
&::before {
content: '';
display: inline-block;
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background-color: var(--color-dot);
position: absolute;
left: 0;
bottom: 25%;
}
}
</style>

View File

@@ -1,156 +1,155 @@
<script lang="ts">
// TODO: sizes
// TODO: icon only buttons should have uniform padding
// TODO: Could be a class
// TODO: sizes
// TODO: icon only buttons should have uniform padding
import { classCombine } from '../utils/classCombine';
import { classCombine } from '../utils/classCombine'
/** The element to be styled as a button */
export let as: 'button' | 'a' | 'summary' | 'input' = 'button';
export let href = '';
if (href) as = 'a';
/** The element to be styled as a button */
export let as: 'button' | 'a' | 'summary' | 'input' = 'button'
export let href = ''
if (href) as = 'a'
/** Use `value` if the button is an `<input`> */
export let value = '';
/** Use `value` if the button is an `<input`> */
export let value = ''
export let size: 'sm' | 'md' | 'lg' = 'md';
export let color:
| ''
| 'raised'
| 'primary'
| 'primary-light'
| 'secondary'
| 'tertiary'
| 'danger'
| 'danger-light'
| 'transparent' = '';
export let size: 'sm' | 'md' | 'lg' = 'md'
export let color:
| ''
| 'raised'
| 'primary'
| 'primary-light'
| 'secondary'
| 'tertiary'
| 'danger'
| 'danger-light'
| 'transparent' = ''
/** Show notification badge in the upper right of button */
export let badge = false;
/** Show notification badge in the upper right of button */
export let badge = false
export let disabled = false;
export let disabled = false
/** Hover title for accessibility */
export let title = '';
/** Hover title for accessibility */
export let title = ''
/** Link target */
export let target = '';
/** Link target */
export let target = ''
let className: string;
$: className = classCombine([
'button',
`button--size-${size}`,
`button--color-${color}`,
badge && 'has-badge',
]);
let className: string
$: className = classCombine([
'button',
`button--size-${size}`,
`button--color-${color}`,
badge && 'has-badge',
])
</script>
{#if as === 'a'}
<a class={className} {href} {disabled} {title} {target} on:click>
<slot />
</a>
<a class={className} {href} {disabled} {title} {target} on:click>
<slot />
</a>
{:else if as === 'input'}
<input class={className} {value} {disabled} {title} on:click />
<input class={className} {value} {disabled} {title} on:click />
{:else}
<svelte:element this={as} class={className} {disabled} {title} on:click>
<slot />
</svelte:element>
<svelte:element this={as} class={className} {disabled} {title} on:click>
<slot />
</svelte:element>
{/if}
<style lang="postcss">
.button {
display: flex;
justify-content: flex-start;
align-items: center;
padding: 0.25rem 1rem;
grid-gap: 14px;
cursor: pointer;
position: relative;
white-space: nowrap;
.button {
display: flex;
justify-content: flex-start;
align-items: center;
padding: 0.25rem 1rem;
grid-gap: 14px;
cursor: pointer;
position: relative;
white-space: nowrap;
color: var(--color-bg-contrast);
color: var(--color-bg-contrast);
box-shadow: var(--shadow-inset-sm);
background-color: var(--color-button-bg);
box-shadow: var(--shadow-inset-sm);
background-color: var(--color-button-bg);
border-radius: var(--rounded);
transition: opacity 0.5s ease-in-out, filter 0.05s ease-in-out;
border-radius: var(--rounded);
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out;
&:hover {
filter: brightness(0.9);
}
&:hover {
filter: brightness(0.85);
}
&--color {
&-raised {
background-color: var(--color-raised-bg);
}
&--color {
&-raised {
background-color: var(--color-raised-bg);
}
&-primary {
background-color: var(--color-brand);
color: var(--color-brand-contrast);
}
&-primary {
background-color: var(--color-brand);
color: var(--color-brand-contrast);
}
&-primary-light {
background-color: var(--color-brand-light);
}
&-primary-light {
background-color: var(--color-brand-light);
}
&-secondary {
background-color: var(--color-secondary);
color: var(--color-brand-contrast);
}
&-secondary {
background-color: var(--color-secondary);
color: var(--color-brand-contrast);
}
&-tertiary {
background-color: var(--color-tertiary);
}
&-tertiary {
background-color: var(--color-tertiary);
}
&-transparent {
background-color: transparent;
box-shadow: none;
}
&-transparent {
background-color: transparent;
box-shadow: none;
}
&-danger {
background-color: var(--color-badge-red-dot);
color: var(--color-brand-contrast);
}
&-danger {
background-color: var(--color-badge-red-dot);
color: var(--color-brand-contrast);
}
&-danger-light {
background-color: var(--color-popup-danger-bg);
color: var(--color-popup-danger-text);
}
}
&-danger-light {
background-color: var(--color-popup-danger-bg);
color: var(--color-popup-danger-text);
}
}
&:disabled {
opacity: 50%;
cursor: not-allowed;
filter: grayscale(50%);
&:disabled {
opacity: 50%;
cursor: not-allowed;
filter: grayscale(50%);
/* Not ideal, but preventing events being fired needs to be implemented */
pointer-events: none;
}
/* Not ideal, but preventing events being fired needs to be implemented */
pointer-events: none;
}
&--pad-even {
padding: 0.5rem;
font-size: 1rem;
line-height: 0;
min-width: 2rem;
min-height: 2rem;
justify-content: center;
}
&--pad-even {
padding: 0.5rem;
font-size: 1rem;
line-height: 0;
min-width: 2rem;
min-height: 2rem;
justify-content: center;
}
&.has-badge::after {
content: '';
width: 0.5rem;
height: 0.5rem;
border-radius: var(--rounded-max);
background-color: var(--color-brand);
position: absolute;
top: 0.5rem;
right: 0.5rem;
}
&.has-badge::after {
content: '';
width: 0.5rem;
height: 0.5rem;
border-radius: var(--rounded-max);
background-color: var(--color-brand);
position: absolute;
top: 0.5rem;
right: 0.5rem;
}
/* Only child doesn't work as intended because text is passed through as `innerText` */
:global(.icon:only-child) {
margin: 4px -7px;
}
}
/* Only child doesn't work as intended because text is passed through as `innerText` */
:global(.icon:only-child) {
margin: 4px -7px;
}
}
</style>

View File

@@ -1,66 +1,66 @@
<script lang="ts">
import { uniqueId } from '../utils/uniqueId'
import IconCheck from 'virtual:icons/heroicons-outline/check'
import { uniqueId } from '../utils/uniqueId'
import IconCheck from 'virtual:icons/heroicons-outline/check'
export let checked = false;
export let checked = false
const id = `checkbox-${uniqueId()}`
const id = `checkbox-${uniqueId()}`
</script>
<div class="checkbox">
<input type="checkbox" bind:checked={checked} on:change id={id} name={id}>
<label for={id} class="checkbox__label">
<span class="checkbox__label__box">
<IconCheck />
</span>
<slot/>
</label>
<input type="checkbox" bind:checked on:change {id} name={id} />
<label for={id} class="checkbox__label">
<span class="checkbox__label__box">
<IconCheck />
</span>
<slot />
</label>
</div>
<style lang="postcss">
.checkbox {
input {
display: none;
}
.checkbox {
input {
display: none;
}
display: inline-flex;
align-items: center;
grid-gap: 0.5rem;
display: inline-flex;
align-items: center;
grid-gap: 0.5rem;
&__label {
display: flex;
align-items: center;
gap: 8px;
&__label {
display: flex;
align-items: center;
gap: 8px;
> :global(.icon) {
width: 1.1rem;
margin-right: -0.1rem;
}
> :global(.icon) {
width: 1.1rem;
margin-right: -0.1rem;
}
&__box {
height: 1rem;
width: 1rem;
display: flex;
border-radius: 0.25rem;
background-color: var(--color-button-bg);
justify-content: center;
align-items: center;
&__box {
height: 1rem;
width: 1rem;
display: flex;
border-radius: 0.25rem;
background-color: var(--color-button-bg);
justify-content: center;
align-items: center;
> :global(.icon) {
display: none;
width: 1rem;
height: 1rem;
color: var(--color-raised-bg)
}
}
}
> :global(.icon) {
display: none;
width: 1rem;
height: 1rem;
color: var(--color-raised-bg);
}
}
}
[type=checkbox]:checked + label span {
background-color: var(--color-brand);
[type='checkbox']:checked + label span {
background-color: var(--color-brand);
:global(.icon) {
display: unset;
}
}
}
</style>
:global(.icon) {
display: unset;
}
}
}
</style>

View File

@@ -1,46 +1,46 @@
<script lang="ts">
import Checkbox from './Checkbox.svelte';
import type { Option } from './types';
import Checkbox from './Checkbox.svelte'
import type { Option } from './types'
export let value = [];
export let options: Option[] = [];
export let value = []
export let options: Option[] = []
/** Wrap the options horizontally */
export let wrap = false;
/** Wrap the options horizontally */
export let wrap = false
const handleChange = (e, key) => {
if (e.target.checked) {
if (!value) value = [];
value = [key, ...value];
} else {
value = value.filter((it) => key !== it);
}
};
const handleChange = (e, key) => {
if (e.target.checked) {
if (!value) value = []
value = [key, ...value]
} else {
value = value.filter((it) => key !== it)
}
}
</script>
<div class="checkbox-list" class:wrap>
{#each options as option}
<Checkbox on:change={(e) => handleChange(e, option.value)}>
{#if option.icon && typeof option.icon === 'string'}
{@html option.icon}
{:else if option.icon}
<svelte:component this={option.icon} />
{/if}
{option.label}
</Checkbox>
{/each}
{#each options as option}
<Checkbox on:change={(e) => handleChange(e, option.value)}>
{#if option.icon && typeof option.icon === 'string'}
{@html option.icon}
{:else if option.icon}
<svelte:component this={option.icon} />
{/if}
{option.label}
</Checkbox>
{/each}
</div>
<style lang="postcss">
.checkbox-list {
display: flex;
flex-direction: column;
gap: 2px;
.checkbox-list {
display: flex;
flex-direction: column;
gap: 2px;
&.wrap {
flex-direction: row;
flex-wrap: wrap;
grid-gap: 2rem;
}
}
&.wrap {
flex-direction: row;
flex-wrap: wrap;
grid-gap: 2rem;
}
}
</style>

View File

@@ -1,45 +1,43 @@
<script lang="ts">
// TODO: Add fade out styling on top and bottom
// TODO: Add fade out styling on top and bottom
import Checkbox from './Checkbox.svelte';
import VirtualList from 'svelte-tiny-virtual-list';
import type { Option } from './types';
import Checkbox from './Checkbox.svelte'
import VirtualList from 'svelte-tiny-virtual-list'
import type { Option } from './types'
/** Height in pixels of list */
export let height = 200;
/** Height in pixels of list */
export let height = 200
export let value = [];
export let options: Option[] = [];
export let value = []
export let options: Option[] = []
const handleChange = (e, key) => {
if (e.target.checked) {
if (!value) value = [];
value = [key, ...value];
} else {
value = value.filter((it) => key !== it);
}
};
const handleChange = (e, key) => {
if (e.target.checked) {
if (!value) value = []
value = [key, ...value]
} else {
value = value.filter((it) => key !== it)
}
}
</script>
<VirtualList width="100%" {height} itemCount={options.length} itemSize={26}>
<div
slot="item"
let:index
let:style
{style}
style:padding-bottom={options.length - 1 === index ? '2.5rem' : ''}
>
{@const option = options[index]}
<Checkbox
checked={value && value.includes(option.value)}
on:change={(e) => handleChange(e, option.value)}
>
{#if option.icon && typeof option.icon === 'string'}
{@html option.icon}
{:else if option.icon}
<svelte:component this={option.icon} />
{/if}
{option.label}
</Checkbox>
</div>
<div
slot="item"
let:index
let:style
{style}
style:padding-bottom={options.length - 1 === index ? '2.5rem' : ''}>
{@const option = options[index]}
<Checkbox
checked={value && value.includes(option.value)}
on:change={(e) => handleChange(e, option.value)}>
{#if option.icon && typeof option.icon === 'string'}
{@html option.icon}
{:else if option.icon}
<svelte:component this={option.icon} />
{/if}
{option.label}
</Checkbox>
</div>
</VirtualList>

View File

@@ -1,48 +1,45 @@
<script lang="ts">
import Button from "./Button.svelte";
import IconCheck from 'virtual:icons/heroicons-outline/check'
import Button from './Button.svelte'
import IconCheck from 'virtual:icons/heroicons-outline/check'
interface Option {
label: string;
value: string | number;
}
interface Option {
label: string
value: string | number
}
export let options: Option[] = [];
export let value: string | number;
// If set to true, one chip is always selected
export let neverEmpty = false;
export let options: Option[] = []
export let value: string | number
// If set to true, one chip is always selected
export let neverEmpty = false
let selected: Option | null = options.find((option) => option.value === (value || ''));
let selected: Option | null = options.find((option) => option.value === (value || ''))
$: if (selected) {
value = selected.value;
} else {
value = ''
}
$: if (selected) {
value = selected.value
} else {
value = ''
}
</script>
<div class="chips">
{#each options as option}
{@const isSelected = selected?.value === option.value}
<Button
color={isSelected ? 'primary-light' : undefined}
on:click={() => {
isSelected && !neverEmpty
? selected = null
: selected = option
}}
>
{#if isSelected}
<IconCheck />
{/if}
{option.label}
</Button>
{/each}
{#each options as option}
{@const isSelected = selected?.value === option.value}
<Button
color={isSelected ? 'primary-light' : undefined}
on:click={() => {
isSelected && !neverEmpty ? (selected = null) : (selected = option)
}}>
{#if isSelected}
<IconCheck />
{/if}
{option.label}
</Button>
{/each}
</div>
<style lang="postcss">
.chips {
display: flex;
grid-gap: 0.5rem;
}
.chips {
display: flex;
grid-gap: 0.5rem;
}
</style>

View File

@@ -1,24 +1,23 @@
<script lang="ts">
import { uniqueId } from '../utils/uniqueId'
import { uniqueId } from '../utils/uniqueId'
export let required = false;
export let label: string;
export let required = false
export let label: string
const id = `form-field-${uniqueId()}`
const id = `form-field-${uniqueId()}`
</script>
<div class="form-field">
<label for={id} class="text-input__label" class:required>
{label}
</label>
<slot {id} />
<label for={id} class="text-input__label" class:required>
{label}
</label>
<slot {id} />
</div>
<style lang="postcss">
.form-field {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-field {
display: flex;
flex-direction: column;
gap: 8px;
}
</style>

View File

@@ -1,80 +1,79 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/stores'
interface Link {
href: string;
label: string;
}
interface Link {
href: string
label: string
}
export let links: Link[];
export let query = '';
export let links: Link[]
export let query = ''
export let resetScroll = false;
export let resetScroll = false
/** Path level in URL, zero-indexed */
export let level = 0;
/** Path level in URL, zero-indexed */
export let level = 0
let path: string[];
$: path = [
...$page.url.pathname
.substring(1)
.split('/')
.map((route) => '/' + route),
'/',
];
let path: string[]
$: path = [
...$page.url.pathname
.substring(1)
.split('/')
.map((route) => '/' + route),
'/',
]
$: basePath = path.slice(0, level).join('');
$: basePath = path.slice(0, level).join('')
</script>
<nav class="navigation">
{#each links as link}
<a
href={query
? link.href
? `?${query}=${link.href}`
: '?'
: level === 0
? link.href
: basePath + link.href}
on:click={() => {
if (resetScroll) document.body.scrollTo(0, 0);
}}
class="navigation__link"
class:is-active={query
? ($page.url.searchParams.get(query) || '') === link.href
: path[level] === link.href || path[level] === link.href.slice(0, -1)}
sveltekit:noscroll={!resetScroll}>{link.label}</a
>
{/each}
{#each links as link}
<a
href={query
? link.href
? `?${query}=${link.href}`
: '?'
: level === 0
? link.href
: basePath + link.href}
on:click={() => {
if (resetScroll) document.body.scrollTo(0, 0)
}}
class="navigation__link"
class:is-active={query
? ($page.url.searchParams.get(query) || '') === link.href
: path[level] === link.href || path[level] === link.href.slice(0, -1)}
sveltekit:noscroll={!resetScroll}>{link.label}</a>
{/each}
</nav>
<style lang="postcss">
.navigation {
display: flex;
flex-direction: row;
align-items: center;
grid-gap: 1rem;
flex-wrap: wrap;
.navigation {
display: flex;
flex-direction: row;
align-items: center;
grid-gap: 1rem;
flex-wrap: wrap;
&__link {
font-weight: var(--font-weight-bold);
color: var(--color-text-light);
&__link {
font-weight: var(--font-weight-bold);
color: var(--color-text-light);
&.is-active {
color: var(--color-text);
position: relative;
&.is-active {
color: var(--color-text);
position: relative;
&::after {
content: '';
display: block;
position: absolute;
bottom: -0.1rem;
width: 100%;
border-radius: var(--rounded-max);
height: 0.2rem;
background-color: var(--color-brand);
}
}
}
}
&::after {
content: '';
display: block;
position: absolute;
bottom: -0.1rem;
width: 100%;
border-radius: var(--rounded-max);
height: 0.2rem;
background-color: var(--color-brand);
}
}
}
}
</style>

View File

@@ -1,77 +1,75 @@
<script lang="ts">
// TODO: Fix mobile support, currently just cuts off buttons
// TODO: Fix mobile support, currently just cuts off buttons
import IconArrowRight from 'virtual:icons/heroicons-outline/arrow-right';
import IconArrowLeft from 'virtual:icons/heroicons-outline/arrow-left';
import IconMinus from 'virtual:icons/heroicons-outline/minus';
import Button from './Button.svelte';
import { createEventDispatcher } from 'svelte';
import IconArrowRight from 'virtual:icons/heroicons-outline/arrow-right'
import IconArrowLeft from 'virtual:icons/heroicons-outline/arrow-left'
import IconMinus from 'virtual:icons/heroicons-outline/minus'
import Button from './Button.svelte'
import { createEventDispatcher } from 'svelte'
export let page: number;
export let count: number;
export let page: number
export let count: number
$: options =
count > 4
? page + 3 >= count
? [1, '-', count - 4, count - 3, count - 2, count - 1, count]
: page > 4
? [1, '-', page - 1, page, page + 1, '-', count]
: [1, 2, 3, 4, 5, '-', count]
: Array.from({ length: count }, (_, i) => i + 1);
$: options =
count > 4
? page + 3 >= count
? [1, '-', count - 4, count - 3, count - 2, count - 1, count]
: page > 4
? [1, '-', page - 1, page, page + 1, '-', count]
: [1, 2, 3, 4, 5, '-', count]
: Array.from({ length: count }, (_, i) => i + 1)
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher()
</script>
{#if count > 1}
<div class="pagination">
<Button
color="raised"
on:click={() => dispatch('change', page - 1)}
disabled={page <= 1}
title="Last page"
><IconArrowLeft height="20px" />
</Button>
{#each options as option}
{#if option === '-'}
<IconMinus class="pagination__dash" />
{:else}
<Button
color={option === page ? 'primary' : 'raised'}
on:click={() => dispatch('change', option)}>{option}</Button
>
{/if}
{/each}
<Button
color="raised"
on:click={() => dispatch('change', page + 1)}
disabled={page >= count}
title="Next page"
>
<IconArrowRight height="20px" />
</Button>
</div>
<div class="pagination">
<Button
color="raised"
on:click={() => dispatch('change', page - 1)}
disabled={page <= 1}
title="Last page"
><IconArrowLeft height="20px" />
</Button>
{#each options as option}
{#if option === '-'}
<IconMinus class="pagination__dash" />
{:else}
<Button
color={option === page ? 'primary' : 'raised'}
on:click={() => dispatch('change', option)}>{option}</Button>
{/if}
{/each}
<Button
color="raised"
on:click={() => dispatch('change', page + 1)}
disabled={page >= count}
title="Next page">
<IconArrowRight height="20px" />
</Button>
</div>
{/if}
<style lang="postcss">
.pagination {
align-self: center;
display: flex;
grid-gap: 0.5rem;
align-items: center;
.pagination {
align-self: center;
display: flex;
grid-gap: 0.5rem;
align-items: center;
:global(&__dash) {
margin: 0 0.5rem;
width: 1rem;
height: 1rem;
}
:global(&__dash) {
margin: 0 0.5rem;
width: 1rem;
height: 1rem;
}
@media (width <= 500px) {
grid-gap: 0.25rem;
@media (width <= 500px) {
grid-gap: 0.25rem;
:global(> *:nth-child(4)),
:global(> *:nth-child(6)) {
display: none;
}
}
}
:global(> *:nth-child(4)),
:global(> *:nth-child(6)) {
display: none;
}
}
}
</style>

View File

@@ -1,204 +1,220 @@
<script lang="ts">
import IconChevronDown from 'virtual:icons/lucide/chevron-down'
import { debounce } from 'throttle-debounce'
import { clickOutside } from 'svelte-use-click-outside'
import { onMount } from 'svelte'
import IconChevronDown from 'virtual:icons/lucide/chevron-down'
import { debounce } from 'throttle-debounce'
import { clickOutside } from 'svelte-use-click-outside'
import { onMount } from 'svelte'
interface Option {
label: string;
value: string | number;
}
interface Option {
label: string
value: string | number
}
export let options: Option[] = []
export let value: string | number
export let selected: Option = options.find((option) => option.value === (value || ''))
export let color = ''
export let label = ''
export let icon = null
export let options: Option[] = []
export let value: string | number
export let selected: Option = options.find((option) => option.value === (value || ''))
export let color = ''
export let label = ''
export let icon = null
let open = false
let open = false
$: if (selected) {
value = selected.value
}
$: if (selected) {
value = selected.value
}
const minWidth = options
.map(it => it.label || it.value)
.reduce((it, acc) => String(it).length > acc ? it : acc, '')
.length * 9
// Returns the width of a string based on the font size and family
const getTextWidth = Object.assign(
(text: string, font: string) => {
if (typeof document !== 'undefined') {
const canvas =
getTextWidth.canvas || (getTextWidth.canvas = document.createElement('canvas'))
const context = canvas.getContext('2d')
context.font = font
const metrics = context.measureText(text)
return metrics.width
} else {
// Return estimate if SSR
return text.length * 7.75
}
},
// Reuses the same canvas object
{ canvas: null }
)
let shouldOpenUp = false
let element: HTMLElement
const minWidth = Math.max(
...options.map((it) => getTextWidth(String(it.label || it.value), '16px Inter'))
)
const checkShouldOpenUp = debounce(100, false, () => {
if (element) {
const bounding = element.getBoundingClientRect()
let shouldOpenUp = false
let element: HTMLElement
shouldOpenUp =
bounding.bottom + 32 * options.length + 16 >
(window.innerHeight || document.documentElement.clientHeight)
}
})
const checkShouldOpenUp = debounce(100, false, () => {
if (element) {
const bounding = element.getBoundingClientRect()
onMount(() => {
checkShouldOpenUp()
window.addEventListener('resize', checkShouldOpenUp)
})
shouldOpenUp =
bounding.bottom + 32 * options.length + 16 >
(window.innerHeight || document.documentElement.clientHeight)
}
})
function keydown(event: KeyboardEvent) {
if ((event.key === ' ' || event.key === 'Enter') && !open) {
open = true
} else if (event.key === 'ArrowUp') {
if (selected) {
const index = options.findIndex((option) => option.value === selected.value)
if (index > 0) {
selected = options[index - 1]
}
}
} else if (event.key === 'ArrowDown') {
if (selected) {
const index = options.findIndex((option) => option.value === selected.value)
if (index < options.length - 1) {
selected = options[index + 1]
}
}
} else if ((event.key === 'Escape' || event.key === 'Enter') && open) {
open = false
}
}
onMount(() => {
checkShouldOpenUp()
window.addEventListener('resize', checkShouldOpenUp)
})
function keydown(event: KeyboardEvent) {
if ((event.key === ' ' || event.key === 'Enter') && !open) {
open = true
} else if (event.key === 'ArrowUp') {
if (selected) {
const index = options.findIndex((option) => option.value === selected.value)
if (index > 0) {
selected = options[index - 1]
}
}
} else if (event.key === 'ArrowDown') {
if (selected) {
const index = options.findIndex((option) => option.value === selected.value)
if (index < options.length - 1) {
selected = options[index + 1]
}
}
} else if ((event.key === 'Escape' || event.key === 'Enter') && open) {
open = false
}
}
</script>
<div
class="select select--color-{color}"
class:is-open={open}
class:select--opens-up={shouldOpenUp}
use:clickOutside={() => {
open = false;
class="select select--color-{color}"
class:is-open={open}
class:select--opens-up={shouldOpenUp}
use:clickOutside={() => {
open = false
}}
bind:this={element}
tabindex="0"
on:keydown={keydown}
>
<div
class="select__input"
on:click={() => {
open = !open;
}}
>
{#if icon}
<svelte:component this={icon}/>
{/if}
<span class="select__input__value" style:min-width="{minWidth}px">{label || selected?.label || value || 'Choose...'}</span>
{#if !icon}
<div class="select__input__arrow">
<slot name="expandIcon">
<IconChevronDown/>
</slot>
</div>
{/if}
</div>
{#if open}
<ul class="select__options">
{#each options as option (option.value)}
<li
on:click={() => {
selected = option;
open = false;
bind:this={element}
tabindex="0"
on:keydown={keydown}>
<div
class="select__input"
on:click={() => {
open = !open
}}>
{#if icon}
<svelte:component this={icon} />
{/if}
<span class="select__input__value" style:min-width="{minWidth}px"
>{label || selected?.label || value || 'Choose...'}</span>
{#if !icon}
<div class="select__input__arrow">
<slot name="expandIcon">
<IconChevronDown />
</slot>
</div>
{/if}
</div>
{#if open}
<ul class="select__options">
{#each options as option (option.value)}
<li
on:click={() => {
selected = option
open = false
}}
class:is-selected={selected?.value === option.value}
>
{option.label || option.value}
</li>
{/each}
</ul>
{/if}
class:is-selected={selected?.value === option.value}>
{option.label || option.value}
</li>
{/each}
</ul>
{/if}
</div>
<style lang="postcss">
.select {
position: relative;
display: flex;
flex-direction: column;
color: var(--color-button-text);
cursor: pointer;
border-radius: var(--rounded);
align-self: flex-start;
.select {
position: relative;
display: flex;
flex-direction: column;
color: var(--color-button-text);
cursor: pointer;
border-radius: var(--rounded);
align-self: flex-start;
&__input {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
padding: 0.25rem 0.9rem;
grid-gap: 0.4rem;
background-color: var(--color-button-bg);
box-shadow: var(--shadow-inset-sm);
border-radius: var(--rounded);
}
&__input {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
padding: 0.25rem 0.9rem;
grid-gap: 0.4rem;
background-color: var(--color-button-bg);
box-shadow: var(--shadow-inset-sm);
border-radius: var(--rounded);
}
&__options {
list-style-type: none;
display: flex;
flex-direction: column;
width: 100%;
padding: 0;
margin: 0;
position: absolute;
top: 100%;
background-color: var(--color-button-bg);
border-radius: var(--rounded-bottom);
box-shadow: var(--shadow-inset-sm), var(--shadow-floating);
overflow: hidden;
border-top: 0.1rem solid var(--color-divider);
z-index: 5;
&__options {
list-style-type: none;
display: flex;
flex-direction: column;
width: 100%;
padding: 0;
margin: 0;
position: absolute;
top: 100%;
background-color: var(--color-button-bg);
border-radius: var(--rounded-bottom);
box-shadow: var(--shadow-inset-sm), var(--shadow-floating);
overflow: hidden;
border-top: 0.1rem solid var(--color-divider);
z-index: 5;
li {
padding: 0.25rem 1rem;
li {
padding: 0.25rem 1rem;
&:hover {
background-color: var(--color-button-bg-hover);
}
&:hover {
background-color: var(--color-button-bg-hover);
}
&.is-selected {
background-color: var(--color-brand-dark);
color: var(--color-brand-contrast);
cursor: default;
}
}
}
&.is-selected {
background-color: var(--color-brand-dark);
color: var(--color-brand-contrast);
cursor: default;
}
}
}
&.is-open {
z-index: 10;
&.is-open {
z-index: 10;
.select__input {
border-radius: var(--rounded-top);
box-shadow: none;
.select__input {
border-radius: var(--rounded-top);
box-shadow: none;
&__arrow {
transform: rotate(180deg);
}
}
}
&__arrow {
transform: rotate(180deg);
}
}
}
&--color-raised {
> * {
background-color: var(--color-raised-bg);
}
}
&--color-raised {
> * {
background-color: var(--color-raised-bg);
}
}
&--opens-up {
.select__options {
bottom: 100%;
top: auto;
border-radius: var(--rounded-top);
box-shadow: none;
border-top: none;
border-bottom: 0.1rem solid var(--color-divider);
}
&--opens-up {
.select__options {
bottom: 100%;
top: auto;
border-radius: var(--rounded-top);
box-shadow: none;
border-top: none;
border-bottom: 0.1rem solid var(--color-divider);
}
&.is-open .select__input {
border-radius: var(--rounded-bottom);
}
}
}
&.is-open .select__input {
border-radius: var(--rounded-bottom);
}
}
}
</style>

View File

@@ -1,48 +1,48 @@
<script lang="ts">
export let value: number;
export let min: number;
export let max: number;
export let id: string = undefined;
export let value: number
export let min: number
export let max: number
export let id: string = undefined
</script>
<div class="slider">
<input class="slider-input" type="range" name={id} {min} {max} bind:value />
<span>{value}</span>
<input class="slider-input" type="range" name={id} {min} {max} bind:value />
<span>{value}</span>
</div>
<style lang="postcss">
.slider {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
.slider {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
.slider-input {
-webkit-appearance: none;
appearance: none;
border-radius: var(--rounded-sm);
box-shadow: var(--shadow-inset-sm);
background-color: var(--color-button-bg);
border: none;
padding: 0.25rem 0;
width: 20rem;
height: 0.5rem;
max-width: 100%;
cursor: pointer;
.slider-input {
-webkit-appearance: none;
appearance: none;
border-radius: var(--rounded-sm);
box-shadow: var(--shadow-inset-sm);
background-color: var(--color-button-bg);
border: none;
padding: 0.25rem 0;
width: 20rem;
height: 0.5rem;
max-width: 100%;
cursor: pointer;
&::-webkit-slider-thumb {
cursor: ew-resize;
-webkit-appearance: none;
appearance: none;
width: 0.5rem;
height: 1.25rem;
border-radius: var(--rounded-sm);
background-color: var(--color-brand);
&::-webkit-slider-thumb {
cursor: ew-resize;
-webkit-appearance: none;
appearance: none;
width: 0.5rem;
height: 1.25rem;
border-radius: var(--rounded-sm);
background-color: var(--color-brand);
&:hover {
background-color: var(--color-brand-dark);
}
}
}
}
&:hover {
background-color: var(--color-brand-dark);
}
}
}
}
</style>

View File

@@ -1,56 +1,56 @@
<script lang="ts">
import type { SvelteComponent } from 'svelte';
import type { SvelteComponent } from 'svelte'
export let placeholder = '';
export let icon: SvelteComponent = undefined;
export let value = '';
export let multiline = false;
export let id: string = undefined;
export let placeholder = ''
export let icon: SvelteComponent = undefined
export let value = ''
export let multiline = false
export let id: string = undefined
</script>
<div class="text-input">
{#if multiline}
<textarea name={id} {placeholder} bind:value />
{:else}
<input type="text" name={id} {placeholder} bind:value class:has-icon={icon} />
{#if icon}
<svelte:component this={icon} />
{/if}
{/if}
{#if multiline}
<textarea name={id} {placeholder} bind:value />
{:else}
<input type="text" name={id} {placeholder} bind:value class:has-icon={icon} />
{#if icon}
<svelte:component this={icon} />
{/if}
{/if}
</div>
<style lang="postcss">
.text-input {
display: flex;
flex-direction: column;
gap: 8px;
position: relative;
.text-input {
display: flex;
flex-direction: column;
gap: 8px;
position: relative;
input,
textarea {
border-radius: var(--rounded-sm);
box-shadow: var(--shadow-inset-sm);
background-color: var(--color-button-bg);
border: none;
padding: 6px 14px;
width: 20rem;
max-width: 100%;
}
input,
textarea {
border-radius: var(--rounded-sm);
box-shadow: var(--shadow-inset-sm);
background-color: var(--color-button-bg);
border: none;
padding: 6px 14px;
width: 20rem;
max-width: 100%;
}
textarea {
min-height: 2.5rem;
}
textarea {
min-height: 2.5rem;
}
:global(.icon) {
position: absolute;
display: flex;
height: 100%;
left: 14px;
opacity: 0.75;
}
:global(.icon) {
position: absolute;
display: flex;
height: 100%;
left: 14px;
opacity: 0.75;
}
input.has-icon {
padding-left: 40px;
}
}
input.has-icon {
padding-left: 40px;
}
}
</style>

View File

@@ -43,7 +43,15 @@ export async function tags(API_URL) {
// Write JSON file
await fs.writeFile(
'./generated/tags.json',
JSON.stringify({ categories, loaders, projectTypes, licenses, donationPlatforms, reportTypes, tagIcons })
JSON.stringify({
categories,
loaders,
projectTypes,
licenses,
donationPlatforms,
reportTypes,
tagIcons,
})
)
progressBar.increment()