You've already forked AstralRinth
forked from didirus/AstralRinth
Add classes: Actions + Divider + Illustration + InfoTable + Stat, Add utilities (needs docs)
This commit is contained in:
98
src/package/components/Avatar.svelte
Normal file
98
src/package/components/Avatar.svelte
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
// TODO: Make square icon `md` more rounded
|
||||
|
||||
import { onMount } from 'svelte'
|
||||
import { classCombine } from '$package/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
|
||||
|
||||
let className: string;
|
||||
$: className = classCombine(['avatar', circle && 'avatar--circle', `avatar--size-${size}`, floatUp && 'avatar--float-up'])
|
||||
|
||||
let img
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if src}
|
||||
<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>
|
||||
{/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);
|
||||
|
||||
&--size {
|
||||
&-xs {
|
||||
--size: 2.25rem;
|
||||
box-shadow: var(--shadow-inset), var(--shadow-raised);
|
||||
border-radius: var(--rounded-sm);
|
||||
}
|
||||
|
||||
&-sm {
|
||||
--size: 3rem;
|
||||
box-shadow: var(--shadow-inset), var(--shadow-raised);
|
||||
border-radius: var(--rounded-sm);
|
||||
}
|
||||
|
||||
&-md {
|
||||
--size: 6rem;
|
||||
}
|
||||
|
||||
&-lg {
|
||||
--size: 9rem;
|
||||
background-color: var(--color-brand-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
&--float-up {
|
||||
margin-top: calc(var(--size) * (-2 / 3));
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&--circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
51
src/package/components/Badge.svelte
Normal file
51
src/package/components/Badge.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
export let label = ''
|
||||
/** Supports `green`, `yellow`, `red`, & `gray` */
|
||||
export let color = 'gray'
|
||||
</script>
|
||||
|
||||
<div class="badge badge--color-{color}">
|
||||
{label}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.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-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-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%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
151
src/package/components/Button.svelte
Normal file
151
src/package/components/Button.svelte
Normal file
@@ -0,0 +1,151 @@
|
||||
<script lang="ts">
|
||||
// TODO: sizes
|
||||
// TODO: icon only buttons should have uniform padding
|
||||
// TODO: Could be a class
|
||||
|
||||
import { classCombine } from '$package/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'
|
||||
|
||||
/** 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' | 'danger'| 'danger-light' | 'transparent'
|
||||
|
||||
/** Show notification badge in the upper right of button */
|
||||
export let badge = false
|
||||
|
||||
export let disabled = false
|
||||
|
||||
let className: string
|
||||
$: className = classCombine(['button', `button--size-${size}`, `button--color-${color}`, badge && 'has-badge'])
|
||||
</script>
|
||||
|
||||
{#if as === 'button'}
|
||||
<button class={className} {disabled} on:click>
|
||||
<slot/>
|
||||
</button>
|
||||
{:else if as === 'a'}
|
||||
<a class={className} {href} {disabled} on:click>
|
||||
<slot/>
|
||||
</a>
|
||||
{:else if as === 'summary'}
|
||||
<summary class={className} {disabled} on:click>
|
||||
<slot/>
|
||||
</summary>
|
||||
{:else if as === 'input'}
|
||||
<input class={className} {value} {disabled} on:click/>
|
||||
{/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;
|
||||
|
||||
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.5s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-button-bg-hover);
|
||||
}
|
||||
|
||||
&--color {
|
||||
&-raised {
|
||||
background-color: var(--color-raised-bg);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-raised-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&-primary {
|
||||
background-color: var(--color-brand);
|
||||
color: var(--color-brand-contrast);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-brand-dark);
|
||||
}
|
||||
}
|
||||
|
||||
&-primary-light {
|
||||
background-color: var(--color-brand-light);
|
||||
transition: filter 0s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-brand-light);
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
&-transparent {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&-danger {
|
||||
background-color: var(--color-badge-red-dot);
|
||||
color: var(--color-brand-contrast);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-badge-red-text);
|
||||
}
|
||||
}
|
||||
|
||||
&-danger-light {
|
||||
background-color: var(--color-popup-danger-bg);
|
||||
color: var(--color-popup-danger-text);
|
||||
transition: filter 0s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(50%);
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
&.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 -6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
66
src/package/components/Checkbox.svelte
Normal file
66
src/package/components/Checkbox.svelte
Normal file
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import uniqueId from 'lodash.uniqueid'
|
||||
import IconCheck from 'virtual:icons/heroicons-outline/check'
|
||||
|
||||
export let checked = false;
|
||||
|
||||
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>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.checkbox {
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
grid-gap: 0.5rem;
|
||||
|
||||
&__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
> :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;
|
||||
|
||||
> :global(.icon) {
|
||||
display: none;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: var(--color-raised-bg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[type=checkbox]:checked + label span {
|
||||
background-color: var(--color-brand);
|
||||
|
||||
:global(.icon) {
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
48
src/package/components/CheckboxList.svelte
Normal file
48
src/package/components/CheckboxList.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import Checkbox from './Checkbox.svelte'
|
||||
import type { SvelteComponent } from 'svelte'
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
/** The element that will be in the `value` array while the option is checked */
|
||||
value: string | number;
|
||||
icon: SvelteComponent;
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="checkbox-list">
|
||||
{#each options as option}
|
||||
<Checkbox on:change={(e) => handleChange(e, option.value)}>
|
||||
{#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;
|
||||
|
||||
&.wrap {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
grid-gap: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
45
src/package/components/CheckboxVirtualList.svelte
Normal file
45
src/package/components/CheckboxVirtualList.svelte
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
// TODO: Add fade out styling on top and bottom
|
||||
|
||||
import Checkbox from './Checkbox.svelte'
|
||||
import type { SvelteComponent } from 'svelte'
|
||||
import VirtualList from 'svelte-tiny-virtual-list'
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
/** The element that will be in the `value` array while the option is checked */
|
||||
value: string | number;
|
||||
icon: SvelteComponent;
|
||||
}
|
||||
|
||||
/** Height in pixels of list */
|
||||
export let height = 200
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
</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.includes(option.value)} on:change={(e) => handleChange(e, option.value)}>
|
||||
{#if option.icon}
|
||||
<svelte:component this={option.icon}/>
|
||||
{/if}
|
||||
{option.label}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</VirtualList>
|
||||
48
src/package/components/Chips.svelte
Normal file
48
src/package/components/Chips.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import Button from "./Button.svelte";
|
||||
import IconCheck from 'virtual:icons/heroicons-outline/check'
|
||||
|
||||
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;
|
||||
|
||||
let selected: Option | null = options.find((option) => option.value === (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}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.chips {
|
||||
display: flex;
|
||||
grid-gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
24
src/package/components/FormField.svelte
Normal file
24
src/package/components/FormField.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import uniqueId from 'lodash.uniqueid'
|
||||
|
||||
export let required = false;
|
||||
export let label: string;
|
||||
|
||||
const id = `form-field-${uniqueId()}`
|
||||
</script>
|
||||
|
||||
<div class="form-field">
|
||||
<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;
|
||||
}
|
||||
</style>
|
||||
81
src/package/components/NavRow.svelte
Normal file
81
src/package/components/NavRow.svelte
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
|
||||
interface Link {
|
||||
href: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export let links: Link[];
|
||||
export let query = '';
|
||||
|
||||
export let resetScroll = false
|
||||
|
||||
/** Path level in URL, zero-indexed */
|
||||
export let level = 0;
|
||||
|
||||
let path: string[];
|
||||
$: path = [
|
||||
...$page.url.pathname
|
||||
.substring(1)
|
||||
.split('/')
|
||||
.map((route) => '/' + route),
|
||||
'/',
|
||||
];
|
||||
|
||||
$: 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}
|
||||
</nav>
|
||||
|
||||
<style lang="postcss">
|
||||
.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);
|
||||
|
||||
&.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
62
src/package/components/Pagination.svelte
Normal file
62
src/package/components/Pagination.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
// 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'
|
||||
|
||||
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)
|
||||
|
||||
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)} evenPadding={true}>{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;
|
||||
|
||||
:global(&__dash) {
|
||||
margin: 0 0.5rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
@media (width <= 500px) {
|
||||
grid-gap: 0.25rem;
|
||||
|
||||
:global(> *:nth-child(4)), :global(> *:nth-child(6)) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
204
src/package/components/Select.svelte
Normal file
204
src/package/components/Select.svelte
Normal file
@@ -0,0 +1,204 @@
|
||||
<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'
|
||||
|
||||
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
|
||||
|
||||
let open = false
|
||||
|
||||
$: 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
|
||||
|
||||
let shouldOpenUp = false
|
||||
let element: HTMLElement
|
||||
|
||||
const checkShouldOpenUp = debounce(100, false, () => {
|
||||
if (element) {
|
||||
const bounding = element.getBoundingClientRect()
|
||||
|
||||
shouldOpenUp =
|
||||
bounding.bottom + 32 * options.length + 16 >
|
||||
(window.innerHeight || document.documentElement.clientHeight)
|
||||
}
|
||||
})
|
||||
|
||||
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;
|
||||
}}
|
||||
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}
|
||||
</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;
|
||||
|
||||
&__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;
|
||||
|
||||
li {
|
||||
padding: 0.25rem 1rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-button-bg-hover);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background-color: var(--color-brand-dark);
|
||||
color: var(--color-brand-contrast);
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
z-index: 10;
|
||||
|
||||
.select__input {
|
||||
border-radius: var(--rounded-top);
|
||||
box-shadow: none;
|
||||
|
||||
&__arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--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);
|
||||
}
|
||||
|
||||
&.is-open .select__input {
|
||||
border-radius: var(--rounded-bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
46
src/package/components/TextInput.svelte
Normal file
46
src/package/components/TextInput.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import type { SvelteComponent } from 'svelte'
|
||||
|
||||
export let placeholder = ''
|
||||
export let icon: SvelteComponent = ''
|
||||
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={value}/>
|
||||
{:else}
|
||||
<input type="text" name={id} {placeholder} bind:value={value}/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="postcss">
|
||||
.text-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
input, textarea {
|
||||
border-radius: var(--rounded-sm);
|
||||
box-shadow: var(--shadow-inset-sm);
|
||||
background-color: var(--color-button-bg);
|
||||
border: none;
|
||||
padding: 0.25rem 0.75rem;
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&--fill {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
textarea {
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
26
src/package/index.ts
Normal file
26
src/package/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export { default as Avatar } from './components/Avatar.svelte';
|
||||
|
||||
export { default as Badge } from './components/Badge.svelte';
|
||||
|
||||
export { default as Button } from './components/Button.svelte';
|
||||
|
||||
export { default as Checkbox } from './components/Checkbox.svelte';
|
||||
export { default as CheckboxList } from './components/CheckboxList.svelte';
|
||||
export { default as CheckboxVirtualList } from './components/CheckboxVirtualList.svelte';
|
||||
|
||||
export { default as Chips } from './components/Chips.svelte';
|
||||
|
||||
export { default as FormField } from './components/FormField.svelte';
|
||||
|
||||
export { default as NavRow } from './components/NavRow.svelte';
|
||||
|
||||
export { default as Pagination } from './components/Pagination.svelte';
|
||||
|
||||
export { default as Select } from './components/Select.svelte';
|
||||
|
||||
export { default as TextInput } from './components/TextInput.svelte';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
4
src/package/styles.postcss
Normal file
4
src/package/styles.postcss
Normal file
@@ -0,0 +1,4 @@
|
||||
@import-glob "./styles/normalize.postcss"; /* Doesn't need to glob but fixes warning */
|
||||
@import-glob "./styles/themes/**.postcss";
|
||||
@import-glob "./styles/variables/**.postcss";
|
||||
@import-glob "./styles/classes/**.postcss";
|
||||
20
src/package/styles/classes/actions.postcss
Normal file
20
src/package/styles/classes/actions.postcss
Normal file
@@ -0,0 +1,20 @@
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
grid-gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-left: auto;
|
||||
min-width: fit-content;
|
||||
|
||||
> *:last-child {
|
||||
margin-top: auto;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
@media (width <= 1000px) {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
margin-left: unset;
|
||||
}
|
||||
}
|
||||
26
src/package/styles/classes/base.postcss
Normal file
26
src/package/styles/classes/base.postcss
Normal file
@@ -0,0 +1,26 @@
|
||||
.base {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-standard);
|
||||
font-size: var(--font-size-nm);
|
||||
font-weight: var(--font-weight-regular);
|
||||
padding: 1rem;
|
||||
|
||||
scrollbar-color: var(--color-scrollbar) var(--color-bg);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-scrollbar);
|
||||
border-radius: 999px;
|
||||
border: 3px solid var(--color-bg);
|
||||
}
|
||||
}
|
||||
5
src/package/styles/classes/button-group.postcss
Normal file
5
src/package/styles/classes/button-group.postcss
Normal file
@@ -0,0 +1,5 @@
|
||||
.button-group {
|
||||
display: flex;
|
||||
grid-gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
101
src/package/styles/classes/card.postcss
Normal file
101
src/package/styles/classes/card.postcss
Normal file
@@ -0,0 +1,101 @@
|
||||
.card {
|
||||
--padding: 1rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
background-color: var(--color-raised-bg);
|
||||
border-radius: var(--rounded);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-raised), var(--shadow-inset);
|
||||
|
||||
padding: var(--padding);
|
||||
grid-gap: 1rem;
|
||||
max-width: 100%;
|
||||
|
||||
.profile-picture {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&--gap {
|
||||
&-compressed {
|
||||
grid-gap: 0.6rem;
|
||||
}
|
||||
|
||||
&-none {
|
||||
grid-gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&--pad {
|
||||
&-top {
|
||||
padding-top: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&--overflow-visible {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&__banner {
|
||||
--inverse-padding: calc(var(--padding) * -1);
|
||||
margin: var(--inverse-padding) var(--inverse-padding) 0 var(--inverse-padding);
|
||||
z-index: 0;
|
||||
background-color: var(--color-divider);
|
||||
|
||||
&:-moz-loading {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&--short {
|
||||
height: 6.5rem;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
&--dark {
|
||||
filter: brightness(0.7);
|
||||
}
|
||||
}
|
||||
|
||||
&__overlay {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
grid-gap: 0.5rem;
|
||||
|
||||
z-index: 1;
|
||||
|
||||
&--row {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
&--row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&--strip {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&--pad-x {
|
||||
--padding: 1rem 1.3rem;
|
||||
}
|
||||
|
||||
&.text {
|
||||
--padding: 1.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 130%;
|
||||
}
|
||||
}
|
||||
5
src/package/styles/classes/divider.postcss
Normal file
5
src/package/styles/classes/divider.postcss
Normal file
@@ -0,0 +1,5 @@
|
||||
.divider {
|
||||
margin: 0.25rem 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-divider);
|
||||
}
|
||||
16
src/package/styles/classes/illustration.postcss
Normal file
16
src/package/styles/classes/illustration.postcss
Normal file
@@ -0,0 +1,16 @@
|
||||
.illustration {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
grid-gap: 2rem;
|
||||
|
||||
&__image {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-text-light)
|
||||
}
|
||||
}
|
||||
11
src/package/styles/classes/info-table.postcss
Normal file
11
src/package/styles/classes/info-table.postcss
Normal file
@@ -0,0 +1,11 @@
|
||||
.info-table {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-gap: 0.25rem 2rem;
|
||||
width: fit-content;
|
||||
|
||||
&__label {
|
||||
color: var(--color-text-lightest);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
26
src/package/styles/classes/link.postcss
Normal file
26
src/package/styles/classes/link.postcss
Normal file
@@ -0,0 +1,26 @@
|
||||
.link {
|
||||
color: var(--color-link);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
line-height: 100%;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.link-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, auto);
|
||||
grid-gap: 0.75rem;
|
||||
|
||||
.link {
|
||||
color: var(--color-text);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-link);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
137
src/package/styles/classes/markdown.postcss
Normal file
137
src/package/styles/classes/markdown.postcss
Normal file
@@ -0,0 +1,137 @@
|
||||
.markdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: 1rem;
|
||||
|
||||
blockquote,
|
||||
details,
|
||||
dl,
|
||||
ol,
|
||||
p,
|
||||
code,
|
||||
pre,
|
||||
table,
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
padding-bottom: 0.2em;
|
||||
border-bottom: 1px solid var(--color-divider);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 0 1rem;
|
||||
color: var(--color-text);
|
||||
border-left: 0.25rem solid var(--color-divider);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-link);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
img,
|
||||
iframe {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: var(--rounded-sm);
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 35rem;
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 80%;
|
||||
border-radius: var(--rounded-sm);
|
||||
background-color: var(--color-code-bg);
|
||||
color: var(--color-code-text);
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 1rem;
|
||||
border-radius: var(--rounded-sm);
|
||||
overflow-x: auto;
|
||||
|
||||
code {
|
||||
font-size: 80%;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 0;
|
||||
color: var(--color-divider);
|
||||
}
|
||||
|
||||
table {
|
||||
display: block;
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
border-collapse: collapse;
|
||||
line-height: 1.5;
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0.4rem 0.85rem;
|
||||
border: 0.1rem solid var(--color-table-border);
|
||||
}
|
||||
|
||||
tr:nth-child(2n) {
|
||||
background-color: var(--color-table-alternate-row);
|
||||
}
|
||||
}
|
||||
|
||||
details {
|
||||
border: 0.15rem solid var(--color-button-bg);
|
||||
border-radius: var(--rounded-sm);
|
||||
padding: 0.5rem 0.5rem 0;
|
||||
overflow: hidden;
|
||||
|
||||
summary {
|
||||
font-weight: bold;
|
||||
margin: -0.5rem -0.5rem 0;
|
||||
padding: 0.5rem 0.8rem;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-button-bg);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-button-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&[open] {
|
||||
padding: 0.5rem;
|
||||
|
||||
summary {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li:has(> input) {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
13
src/package/styles/classes/member.postcss
Normal file
13
src/package/styles/classes/member.postcss
Normal file
@@ -0,0 +1,13 @@
|
||||
.member {
|
||||
display: flex;
|
||||
grid-gap: 0.75rem;
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__link {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/package/styles/classes/stat.postcss
Normal file
23
src/package/styles/classes/stat.postcss
Normal file
@@ -0,0 +1,23 @@
|
||||
.stat {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
grid-gap: 0.4rem;
|
||||
|
||||
&--light {
|
||||
color: var(--color-text-lightest);
|
||||
}
|
||||
|
||||
.icon {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-group {
|
||||
display: flex;
|
||||
grid-gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
18
src/package/styles/classes/tag.postcss
Normal file
18
src/package/styles/classes/tag.postcss
Normal file
@@ -0,0 +1,18 @@
|
||||
.tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-gap: 0.25rem;
|
||||
color: var(--color-text-lightest);
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-group {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: auto;
|
||||
grid-gap: 0.25rem 0.6rem;
|
||||
}
|
||||
14
src/package/styles/classes/title.postcss
Normal file
14
src/package/styles/classes/title.postcss
Normal file
@@ -0,0 +1,14 @@
|
||||
.title-primary {
|
||||
font-size: 24px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.title-secondary {
|
||||
font-size: 20px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.title-tertiary {
|
||||
font-size: 16px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
55
src/package/styles/normalize.postcss
Normal file
55
src/package/styles/normalize.postcss
Normal file
@@ -0,0 +1,55 @@
|
||||
@import 'sanitize.css';
|
||||
@import 'sanitize.css/forms.css';
|
||||
@import 'sanitize.css/typography.css';
|
||||
|
||||
/* Overrides */
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button:focus-visible,
|
||||
a:focus-visible,
|
||||
[tabindex='0']:focus-visible {
|
||||
outline: 0.2rem solid var(--color-brand);
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#svelte {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p {
|
||||
line-height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0 0 0 1.5rem;
|
||||
}
|
||||
17
src/package/styles/themes/base.postcss
Normal file
17
src/package/styles/themes/base.postcss
Normal file
@@ -0,0 +1,17 @@
|
||||
:root {
|
||||
--rounded: 1rem;
|
||||
--rounded-top: 1rem 1rem 0 0;
|
||||
--rounded-bottom: 0 0 1rem 1rem;
|
||||
--rounded-sm: 0.6rem;
|
||||
--rounded-max: 999999999px;
|
||||
|
||||
--font-standard: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto,
|
||||
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
|
||||
--font-size-nm: 1rem; /* 16px */
|
||||
--font-size-xl: 1.5rem; /* 24px */
|
||||
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 600;
|
||||
--font-weight-bold: 700;
|
||||
}
|
||||
61
src/package/styles/themes/dark.postcss
Normal file
61
src/package/styles/themes/dark.postcss
Normal file
@@ -0,0 +1,61 @@
|
||||
.theme-dark {
|
||||
/* Brand colors */
|
||||
--color-brand: hsl(155, 58%, 40%);
|
||||
--color-brand-light: hsl(155, 54%, 30%);
|
||||
--color-brand-dark: hsl(155, 58%, 25%);
|
||||
--color-brand-contrast: hsl(0, 0%, 100%);
|
||||
|
||||
/* Shadows */
|
||||
--shadow-inset-lg: inset 0px -2px 2px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-inset: inset 0px -2px 2px hsla(221, 39%, 11%, 0.05);
|
||||
--shadow-inset-sm: inset 0px -1px 1px hsla(221, 39%, 11%, 0.25);
|
||||
|
||||
--shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2);
|
||||
--shadow-raised: 0px -2px 4px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
|
||||
hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
|
||||
--shadow-mobile-bar: hsla(0, 0%, 0%, 0.3) 0 0 20px 2px;
|
||||
|
||||
/* Text colors */
|
||||
--color-text: hsl(221, 39%, 90%);
|
||||
--color-text-light: hsl(215, 14%, 74%);
|
||||
--color-text-lightest: hsl(220, 9%, 70%);
|
||||
--color-heading: hsl(222, 16%, 80%);
|
||||
--color-link: hsl(215, 100%, 75%);
|
||||
|
||||
/* Container colors */
|
||||
--color-bg: hsl(220, 13%, 15%);
|
||||
--color-raised-bg: hsl(220, 13%, 25%);
|
||||
--color-raised-bg-hover: hsl(220, 13%, 20%);
|
||||
--color-divider: hsl(220, 13%, 50%);
|
||||
--color-button-bg: hsl(220, 13%, 35%);
|
||||
--color-button-bg-hover: hsl(220, 13%, 32%);
|
||||
|
||||
/* Label colors */
|
||||
--color-badge-gray-text: hsl(0, 2%, 69%);
|
||||
--color-badge-gray-dot: hsl(0, 6%, 77%);
|
||||
--color-badge-red-text: hsl(343, 63%, 67%);
|
||||
--color-badge-red-dot: hsl(342, 70%, 53%);
|
||||
--color-badge-green-text: hsl(156, 53%, 50%);
|
||||
--color-badge-green-dot: hsl(140, 64%, 40%);
|
||||
--color-badge-yellow-text: hsl(40, 57%, 60%);
|
||||
--color-badge-yellow-dot: hsl(40, 92%, 62%);
|
||||
|
||||
/* Markdown colors */
|
||||
--color-table-border: hsl(214, 12%, 35%);
|
||||
--color-table-alternate-row: hsl(216, 12%, 17%);
|
||||
--color-code-bg: hsl(217, 12%, 29%);
|
||||
|
||||
/* Ad colors */
|
||||
--color-ad-bg: hsl(200, 70%, 25%);
|
||||
--color-ad-link: hsl(200, 70%, 50%);
|
||||
|
||||
/* Popup colors */
|
||||
--color-popup-danger-bg: hsl(355, 70%, 20%);
|
||||
--color-popup-danger-text: hsl(342, 70%, 75%);
|
||||
|
||||
--color-input-light: hsl(220, 13%, 20%);
|
||||
|
||||
/* Scrollbar color */
|
||||
--color-scrollbar-thumb: hsl(220, 13%, 40%);
|
||||
}
|
||||
60
src/package/styles/themes/light.postcss
Normal file
60
src/package/styles/themes/light.postcss
Normal file
@@ -0,0 +1,60 @@
|
||||
.theme-light {
|
||||
/* Brand colors */
|
||||
--color-brand: hsl(155, 58%, 44%);
|
||||
--color-brand-light: hsl(135, 50%, 78%);
|
||||
--color-brand-dark: hsl(155, 58%, 38%);
|
||||
--color-brand-contrast: hsl(0, 0%, 100%);
|
||||
|
||||
/* Shadows */
|
||||
--shadow-inset-lg: inset 0px -2px 2px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-inset: inset 0px -2px 2px hsla(221, 39%, 11%, 0.05);
|
||||
--shadow-inset-sm: inset 0px -1px 2px hsla(221, 39%, 11%, 0.15);
|
||||
|
||||
--shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2);
|
||||
--shadow-raised: 0px 2px 4px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
|
||||
hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, hsla(0, 0%, 0%, 0.1) 0px 2px 4px -1px;
|
||||
--shadow-mobile-bar: hsla(0, 0%, 0%, 0.3) 0 0 20px 2px;
|
||||
|
||||
/* Text colors */
|
||||
--color-text: hsl(221, 39%, 11%);
|
||||
--color-text-light: hsl(215, 14%, 34%);
|
||||
--color-text-lightest: hsl(220, 9%, 46%);
|
||||
--color-heading: hsl(222, 16%, 20%);
|
||||
--color-link: hsl(221, 55%, 50%);
|
||||
|
||||
/* Container colors */
|
||||
--color-bg: hsl(220, 13%, 91%);
|
||||
--color-raised-bg: hsl(0, 0%, 100%);
|
||||
--color-raised-bg-hover: hsl(220, 13%, 93%);
|
||||
--color-divider: hsl(220, 13%, 91%);
|
||||
--color-button-bg: hsl(220, 13%, 91%);
|
||||
--color-button-bg-hover: hsl(0, 0%, 85%);
|
||||
--color-input-text-light: hsl(0, 0%, 94%);
|
||||
|
||||
/* Label colors */
|
||||
--color-badge-gray-text: hsl(0, 2%, 39%);
|
||||
--color-badge-gray-dot: hsl(0, 6%, 77%);
|
||||
--color-badge-red-text: hsl(343, 63%, 27%);
|
||||
--color-badge-red-dot: hsl(342, 70%, 53%);
|
||||
--color-badge-green-text: hsl(156, 53%, 20%);
|
||||
--color-badge-green-dot: hsl(140, 64%, 40%);
|
||||
--color-badge-yellow-text: hsl(40, 57%, 29%);
|
||||
--color-badge-yellow-dot: hsl(40, 92%, 62%);
|
||||
|
||||
/* Markdown colors */
|
||||
--color-table-border: hsl(210, 10%, 89%);
|
||||
--color-table-alternate-row: hsl(210, 29%, 97%);
|
||||
--color-code-bg: hsl(210, 29%, 96%);
|
||||
|
||||
/* Ad colors */
|
||||
--color-ad-bg: hsl(200, 70%, 82%);
|
||||
--color-ad-link: hsl(200, 80%, 40%);
|
||||
|
||||
/* Popup colors */
|
||||
--color-popup-danger-bg: hsl(355, 70%, 88%);
|
||||
--color-popup-danger-text: hsl(342, 70%, 35%);
|
||||
|
||||
/* Scrollbar color */
|
||||
--color-scrollbar-thumb: hsl(220, 13%, 70%);
|
||||
}
|
||||
15
src/package/styles/themes/oled.postcss
Normal file
15
src/package/styles/themes/oled.postcss
Normal file
@@ -0,0 +1,15 @@
|
||||
.oled-theme {
|
||||
@extend .dark-theme;
|
||||
|
||||
/* Container colors */
|
||||
--color-bg: hsl(220, 13%, 0%);
|
||||
--color-raised-bg: hsl(220, 13%, 10%);
|
||||
--color-raised-bg-hover: hsl(220, 13%, 20%);
|
||||
--color-divider: hsl(220, 13%, 35%);
|
||||
--color-button-bg: hsl(220, 13%, 20%);
|
||||
--color-button-bg-hover: hsl(220, 13%, 15%);
|
||||
|
||||
/* Ad colors */
|
||||
--color-ad-bg: hsl(200, 70%, 15%);
|
||||
--color-ad-link: hsl(200, 70%, 45%);
|
||||
}
|
||||
16
src/package/styles/variables/borders.postcss
Normal file
16
src/package/styles/variables/borders.postcss
Normal file
@@ -0,0 +1,16 @@
|
||||
:root {
|
||||
/* Borders */
|
||||
--border-width: 1px;
|
||||
--border-style: solid;
|
||||
--border: var(--border-width) var(--border-style);
|
||||
|
||||
/* Rounded radii */
|
||||
--rounded-sm: 8px;
|
||||
--rounded: 10px;
|
||||
--rounded-lg: 12px;
|
||||
--rounded-max: 100px;
|
||||
--rounded-top: var(--rounded) var(--rounded) 0 0;
|
||||
--rounded-bottom: 0 0 var(--rounded) var(--rounded);
|
||||
--rounded-sm-top: var(--rounded-sm) var(--rounded-sm) 0 0;
|
||||
--rounded-sm-bottom: 0 0 var(--rounded-sm) var(--rounded-sm);
|
||||
}
|
||||
213
src/package/styles/variables/spacing.postcss
Normal file
213
src/package/styles/variables/spacing.postcss
Normal file
@@ -0,0 +1,213 @@
|
||||
:root {
|
||||
|
||||
/* these are values for the display CSS property */
|
||||
/*
|
||||
--display-values: (
|
||||
block,
|
||||
flex,
|
||||
inline,
|
||||
inline-block,
|
||||
inline-flex,
|
||||
none,
|
||||
table,
|
||||
table-cell
|
||||
);
|
||||
|
||||
// maps edges to respective corners for border-radius
|
||||
--edges: (
|
||||
top: (top-left, top-right),
|
||||
right: (top-right, bottom-right),
|
||||
bottom: (bottom-right, bottom-left),
|
||||
left: (bottom-left, top-left)
|
||||
);
|
||||
*/
|
||||
|
||||
/*
|
||||
These are our margin and padding utility spacers. The default step size we
|
||||
use is 8px. This gives us a key of:
|
||||
0 => 0px
|
||||
1 => 4px
|
||||
2 => 8px
|
||||
3 => 16px
|
||||
4 => 24px
|
||||
5 => 32px
|
||||
6 => 40px */
|
||||
--spacer: 8px;
|
||||
|
||||
/* Our spacing scale */
|
||||
--spacer-0: 0; /* 0 */
|
||||
--spacer-1: calc(var(--spacer) * 0.5); /* 4px */
|
||||
--spacer-2: --spacer; /* 8px */
|
||||
--spacer-3: calc(var(--spacer) * 2); /* 16px */
|
||||
--spacer-4: calc(var(--spacer) * 3); /* 24px */
|
||||
--spacer-5: calc(var(--spacer) * 4); /* 32px */
|
||||
--spacer-6: calc(var(--spacer) * 5); /* 40px */
|
||||
|
||||
/*
|
||||
/* The list of spacer values
|
||||
--spacers: (
|
||||
--spacer-0,
|
||||
--spacer-1,
|
||||
--spacer-2,
|
||||
--spacer-3,
|
||||
--spacer-4,
|
||||
--spacer-5,
|
||||
--spacer-6,
|
||||
);
|
||||
|
||||
/* And the map of spacers, for easier looping:
|
||||
/* @each --scale, --length in --spacer-map { ... }
|
||||
--spacer-map: (
|
||||
0: --spacer-0,
|
||||
1: --spacer-1,
|
||||
2: --spacer-2,
|
||||
3: --spacer-3,
|
||||
4: --spacer-4,
|
||||
5: --spacer-5,
|
||||
6: --spacer-6,
|
||||
);
|
||||
|
||||
/* Increases the core spacing scale first by 8px for --spacer-7, then by 16px
|
||||
/* increments from --spacer-8 to --spacer-12, i.e. after 40px, we have 48, 64,
|
||||
/* 80, 96, etc.
|
||||
--spacer-7: --spacer * 6; /* 48px
|
||||
--spacer-8: --spacer * 8; /* 64px
|
||||
--spacer-9: --spacer * 10; /* 80px
|
||||
--spacer-10: --spacer * 12; /* 96px
|
||||
--spacer-11: --spacer * 14; /* 112px
|
||||
--spacer-12: --spacer * 16; /* 128px
|
||||
|
||||
--spacers-large: (
|
||||
7: --spacer-7,
|
||||
8: --spacer-8,
|
||||
9: --spacer-9,
|
||||
10: --spacer-10,
|
||||
11: --spacer-11,
|
||||
12: --spacer-12,
|
||||
);
|
||||
|
||||
--spacer-map-extended: map-merge(
|
||||
(
|
||||
0: 0,
|
||||
1: --spacer-1,
|
||||
2: --spacer-2,
|
||||
3: --spacer-3,
|
||||
4: --spacer-4,
|
||||
5: --spacer-5,
|
||||
6: --spacer-6,
|
||||
),
|
||||
--spacers-large,
|
||||
);
|
||||
|
||||
/* Em spacer variables
|
||||
--em-spacer-1: 0.0625em; /* 1/16
|
||||
--em-spacer-2: 0.125em; /* 1/8
|
||||
--em-spacer-3: 0.25em; /* 1/4
|
||||
--em-spacer-4: 0.375em; /* 3/8
|
||||
--em-spacer-5: 0.5em; /* 1/2
|
||||
--em-spacer-6: 0.75em; /* 3/4
|
||||
|
||||
/* Size scale
|
||||
/* Used for buttons, inputs, labels, avatars etc.
|
||||
--size: 16px;
|
||||
|
||||
--size-0: 0;
|
||||
--size-1: --size; /* 16px
|
||||
--size-2: --size-1 + 4px; /* 20px
|
||||
--size-3: --size-2 + 4px; /* 24px
|
||||
--size-4: --size-3 + 4px; /* 28px
|
||||
--size-5: --size-4 + 4px; /* 32px
|
||||
--size-6: --size-5 + 8px; /* 40px
|
||||
--size-7: --size-6 + 8px; /* 48px
|
||||
--size-8: --size-7 + 16px; /* 64px
|
||||
|
||||
/* Fixed-width container variables
|
||||
--container-width: 980px;
|
||||
--grid-gutter: 10px;
|
||||
|
||||
// Breakpoint widths
|
||||
--width-xs: 0;
|
||||
// Small screen / phone
|
||||
--width-sm: 544px;
|
||||
// Medium screen / tablet
|
||||
--width-md: 768px;
|
||||
// Large screen / desktop (980 + (16 * 2)) <= container + gutters
|
||||
--width-lg: 1012px;
|
||||
// Extra large screen / wide desktop
|
||||
--width-xl: 1280px;
|
||||
|
||||
// Responsive container widths
|
||||
--container-sm: --width-sm;
|
||||
--container-md: --width-md;
|
||||
--container-lg: --width-lg;
|
||||
--container-xl: --width-xl;
|
||||
|
||||
// Breakpoints in the form (name: length)
|
||||
--breakpoints: (
|
||||
sm: --width-sm,
|
||||
md: --width-md,
|
||||
lg: --width-lg,
|
||||
xl: --width-xl
|
||||
);
|
||||
|
||||
// This map in the form (breakpoint: variant) is used to iterate over
|
||||
// breakpoints and create both responsive and non-responsive classes in one
|
||||
// loop:
|
||||
--responsive-variants: (
|
||||
'': '',
|
||||
sm: '-sm',
|
||||
md: '-md',
|
||||
lg: '-lg',
|
||||
xl: '-xl',
|
||||
);
|
||||
|
||||
// responsive utility position values
|
||||
--responsive-positions: (
|
||||
static,
|
||||
relative,
|
||||
absolute,
|
||||
fixed,
|
||||
sticky
|
||||
);
|
||||
|
||||
--sidebar-width: (
|
||||
sm: 220px,
|
||||
md: 256px,
|
||||
lg: 296px
|
||||
);
|
||||
|
||||
--sidebar-narrow-width: (
|
||||
md: 240px,
|
||||
lg: 256px
|
||||
);
|
||||
|
||||
--sidebar-wide-width: (
|
||||
lg: 320px,
|
||||
xl: 336px
|
||||
);
|
||||
|
||||
--gutter: (
|
||||
md: --spacer-3,
|
||||
lg: --spacer-4,
|
||||
xl: --spacer-5
|
||||
);
|
||||
|
||||
--gutter-condensed: (
|
||||
md: --spacer-3,
|
||||
lg: --spacer-3,
|
||||
xl: --spacer-4
|
||||
);
|
||||
|
||||
--gutter-spacious: (
|
||||
md: --spacer-4,
|
||||
lg: --spacer-5,
|
||||
xl: --spacer-6
|
||||
);
|
||||
*/
|
||||
}
|
||||
|
||||
/* Breakpoints */
|
||||
@custom-media --sm (min-width: 544px);
|
||||
@custom-media --md (min-width: 768px);
|
||||
@custom-media --lg (min-width: 1012px);
|
||||
@custom-media --xl (min-width: 544px);
|
||||
38
src/package/styles/variables/typography.postcss
Normal file
38
src/package/styles/variables/typography.postcss
Normal file
@@ -0,0 +1,38 @@
|
||||
:root {
|
||||
/* Heading sizes - mobile */
|
||||
/* h4-h6 remain the same size on both mobile & desktop */
|
||||
--h00-size-mobile: 40px;
|
||||
--h0-size-mobile: 32px;
|
||||
--h1-size-mobile: 26px;
|
||||
--h2-size-mobile: 22px;
|
||||
--h3-size-mobile: 18px;
|
||||
|
||||
/* Heading sizes - desktop */
|
||||
--h00-size: 48px;
|
||||
--h0-size: 40px;
|
||||
--h1-size: 32px;
|
||||
--h2-size: 24px;
|
||||
--h3-size: 20px;
|
||||
--h4-size: 16px;
|
||||
--h5-size: 14px;
|
||||
--h6-size: 12px;
|
||||
|
||||
--font-size-lg: 17px;
|
||||
--font-size-sm: 12px;
|
||||
--font-size: 14px;
|
||||
|
||||
/* Line heights */
|
||||
--lh-condensed-ultra: 1;
|
||||
--lh-condensed: 1.25;
|
||||
--lh-default: 1.5;
|
||||
|
||||
/* Font weights */
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-semibold: 500;
|
||||
--font-weight-bold: 600;
|
||||
|
||||
/* Font stacks */
|
||||
--body-font: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
|
||||
--mono-font: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
}
|
||||
57
src/package/utils/ago.ts
Normal file
57
src/package/utils/ago.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Human readable elapsed or remaining time (example: 3 minutes ago)
|
||||
* @author github.com/victornpb
|
||||
* @see https://stackoverflow.com/a/67338038/938822
|
||||
*/
|
||||
export function ago(
|
||||
/** A Date object, timestamp or string parsable with Date.parse() */
|
||||
date: string | number | Date,
|
||||
/** A Date object, timestamp or string parsable with Date.parse() */
|
||||
nowDate: string | number | Date = Date.now(),
|
||||
/** A Intl formater */
|
||||
rft: Intl.RelativeTimeFormat = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })
|
||||
): string {
|
||||
const SECOND = 1000;
|
||||
const MINUTE = 60 * SECOND;
|
||||
const HOUR = 60 * MINUTE;
|
||||
const DAY = 24 * HOUR;
|
||||
const WEEK = 7 * DAY;
|
||||
const MONTH = 30 * DAY;
|
||||
const YEAR = 365 * DAY;
|
||||
const intervals = [
|
||||
{ ge: YEAR, divisor: YEAR, unit: 'year' },
|
||||
{ ge: MONTH, divisor: MONTH, unit: 'month' },
|
||||
{ ge: WEEK, divisor: WEEK, unit: 'week' },
|
||||
{ ge: DAY, divisor: DAY, unit: 'day' },
|
||||
{ ge: HOUR, divisor: HOUR, unit: 'hour' },
|
||||
{ ge: MINUTE, divisor: MINUTE, unit: 'minute' },
|
||||
{ ge: 30 * SECOND, divisor: SECOND, unit: 'seconds' },
|
||||
{ ge: 0, divisor: 1, text: 'just now' },
|
||||
];
|
||||
const now = typeof nowDate === 'object' ? nowDate.getTime() : new Date(nowDate).getTime();
|
||||
const diff = now - (typeof date === 'object' ? date : new Date(date)).getTime();
|
||||
const diffAbs = Math.abs(diff);
|
||||
for (const interval of intervals) {
|
||||
if (diffAbs >= interval.ge) {
|
||||
const x = Math.round(Math.abs(diff) / interval.divisor);
|
||||
const isFuture = diff < 0;
|
||||
return interval.unit ? rft.format(isFuture ? x : -x, interval.unit as Unit) : interval.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Unit =
|
||||
| 'second'
|
||||
| 'seconds'
|
||||
| 'minute'
|
||||
| 'minutes'
|
||||
| 'hour'
|
||||
| 'hours'
|
||||
| 'day'
|
||||
| 'days'
|
||||
| 'week'
|
||||
| 'weeks'
|
||||
| 'month'
|
||||
| 'months'
|
||||
| 'year'
|
||||
| 'years';
|
||||
3
src/package/utils/classCombine.ts
Normal file
3
src/package/utils/classCombine.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function classCombine(names) {
|
||||
return names.filter(name => name && !name.includes('undefined')).join(' ')
|
||||
}
|
||||
40
src/package/utils/number.ts
Normal file
40
src/package/utils/number.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Convert large numbers to human readable strings
|
||||
* @source https://github.com/rohmanhm/simplify-number
|
||||
*/
|
||||
export function simplify(num = 0): string {
|
||||
let numberVar = num;
|
||||
|
||||
// 2 decimal places => 100, 3 => 1000, etc
|
||||
const decPlaces = Math.pow(10, 1);
|
||||
|
||||
// Enumerate number abbreviations
|
||||
const abbrev = ['K', 'M', 'B', 'T'];
|
||||
|
||||
// Go through the array backwards, so we do the largest first
|
||||
for (let i = abbrev.length - 1; i >= 0; i--) {
|
||||
// Convert array index to "1000", "1000000", etc
|
||||
const size = Math.pow(10, (i + 1) * 3);
|
||||
|
||||
// If the number is bigger or equal do the abbreviation
|
||||
if (size <= numberVar) {
|
||||
// Here, we multiply by decPlaces, round, and then divide by decPlaces.
|
||||
// This gives us nice rounding to a particular decimal place.
|
||||
numberVar = Math.round((numberVar * decPlaces) / size) / decPlaces;
|
||||
|
||||
// Handle special case where we round up to the next abbreviation
|
||||
if (numberVar === 1000 && i < abbrev.length - 1) {
|
||||
numberVar = 1;
|
||||
i++;
|
||||
}
|
||||
|
||||
// Add the letter for the abbreviation
|
||||
(numberVar as any) += abbrev[i];
|
||||
|
||||
// We are done... stop
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return String(numberVar);
|
||||
}
|
||||
134
src/package/utils/parse.ts
Normal file
134
src/package/utils/parse.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { marked } from 'marked';
|
||||
import hljs from 'highlight.js';
|
||||
import insane from 'insane';
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
renderer.image = (href, text) => {
|
||||
if (/^https?:\/\/(www\.)?youtube\.com\/watch\?v=[a-zA-Z0-9_]{11}$/.test(href)) {
|
||||
const id = href.substring(32, 43);
|
||||
return `<iframe src="https://www.youtube-nocookie.com/embed/${id}?&modestbranding=1&autoplay=0&rel=0" frameborder="0" allowfullscreen></iframe>`;
|
||||
} else {
|
||||
return `<img src="${href}" alt="${text}" />`;
|
||||
}
|
||||
};
|
||||
|
||||
renderer.link = (href, title, text) => {
|
||||
if (href === null) {
|
||||
return text;
|
||||
}
|
||||
let out = '<a href="' + href + '" rel="external nofollow"';
|
||||
if (title) {
|
||||
out += ' title="' + title + '"';
|
||||
}
|
||||
out += '>' + text + '</a>';
|
||||
return out;
|
||||
};
|
||||
|
||||
marked.setOptions({
|
||||
renderer,
|
||||
highlight: function (code, lang) {
|
||||
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
||||
return hljs.highlight(code, { language }).value;
|
||||
},
|
||||
langPrefix: 'hljs language-',
|
||||
headerPrefix: '',
|
||||
gfm: true,
|
||||
smartLists: true,
|
||||
});
|
||||
|
||||
function sanitize(html: string): string {
|
||||
return insane(html, {
|
||||
allowedAttributes: {
|
||||
a: ['href', 'name', 'target', 'title', 'rel'],
|
||||
iframe: ['allowfullscreen', 'src', 'width', 'height'],
|
||||
img: ['src', 'width', 'height', 'alt'],
|
||||
h1: ['id'],
|
||||
h2: ['id'],
|
||||
h3: ['id'],
|
||||
h4: ['id'],
|
||||
h5: ['id'],
|
||||
h6: ['id'],
|
||||
code: ['class'],
|
||||
span: ['class'],
|
||||
input: ['type', 'checked', 'disabled'],
|
||||
font: ['color'],
|
||||
},
|
||||
allowedClasses: {},
|
||||
allowedSchemes: ['http', 'https', 'mailto'],
|
||||
allowedTags: [
|
||||
'a',
|
||||
'b',
|
||||
'blockquote',
|
||||
'br',
|
||||
'caption',
|
||||
'center',
|
||||
'code',
|
||||
'del',
|
||||
'details',
|
||||
'div',
|
||||
'em',
|
||||
'font',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'hr',
|
||||
'i',
|
||||
'iframe',
|
||||
'img',
|
||||
'input',
|
||||
'ins',
|
||||
'kbd',
|
||||
'li',
|
||||
'main',
|
||||
'ol',
|
||||
'p',
|
||||
'pre',
|
||||
'span',
|
||||
'strike',
|
||||
'strong',
|
||||
'sub',
|
||||
'summary',
|
||||
'sup',
|
||||
'table',
|
||||
'tbody',
|
||||
'td',
|
||||
'th',
|
||||
'thead',
|
||||
'tr',
|
||||
'u',
|
||||
'ul',
|
||||
],
|
||||
filter: ({ tag, attrs }): boolean => {
|
||||
if (tag === 'iframe') {
|
||||
return /^https?:\/\/(www\.)?(youtube|youtube-nocookie)\.com\/embed\/[a-zA-Z0-9_]{11}(\?)?(&modestbranding=1)?(&autoplay=0)?(&loop=1)?(&playlist=[a-zA-Z0-9_]{11})?(&rel=0)?$/.test(
|
||||
attrs.src || ''
|
||||
);
|
||||
} else if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)) {
|
||||
return attrs.id !== 'svelte';
|
||||
} else if (tag === 'input') {
|
||||
return attrs.type === 'checkbox' && attrs.disabled === '';
|
||||
} else if (tag === 'code' || tag === 'span') {
|
||||
return !attrs.class || attrs.class.replace(' ', '').startsWith('hljs');
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
transformText: null,
|
||||
});
|
||||
}
|
||||
|
||||
export function markdown(markdown: string): string {
|
||||
return marked.parse(markdown);
|
||||
}
|
||||
|
||||
export function markdownInline(markdown: string): string {
|
||||
return marked.parseInline(markdown);
|
||||
}
|
||||
|
||||
export function markdownXSS(markdown: string): string {
|
||||
return sanitize(marked.parse(markdown));
|
||||
}
|
||||
38
src/package/utils/permissions.ts
Normal file
38
src/package/utils/permissions.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export class Permissions {
|
||||
data = {
|
||||
uploadVersions: false,
|
||||
deleteVersion: false,
|
||||
editDetails: false,
|
||||
editBody: false,
|
||||
manageInvites: false,
|
||||
removeMember: false,
|
||||
editMember: false,
|
||||
deleteProject: false,
|
||||
};
|
||||
|
||||
get settingsPage(): boolean {
|
||||
return (
|
||||
this.data.manageInvites ||
|
||||
this.data.removeMember ||
|
||||
this.data.editMember ||
|
||||
this.data.deleteProject
|
||||
);
|
||||
}
|
||||
|
||||
constructor(from: number | 'ALL' | null) {
|
||||
if (from === 'ALL' || from === 0b11111111 || from === null) {
|
||||
Object.keys(this.data).forEach((v) => (this.data[v] = true));
|
||||
} else if (typeof from === 'number') {
|
||||
this.data = {
|
||||
uploadVersions: !!(from & (1 << 0)),
|
||||
deleteVersion: !!(from & (1 << 1)),
|
||||
editDetails: !!(from & (1 << 2)),
|
||||
editBody: !!(from & (1 << 3)),
|
||||
manageInvites: !!(from & (1 << 4)),
|
||||
removeMember: !!(from & (1 << 5)),
|
||||
editMember: !!(from & (1 << 6)),
|
||||
deleteProject: !!(from & (1 << 7)),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
84
src/package/utils/versions.ts
Normal file
84
src/package/utils/versions.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import gameVersions from '$generated/gameVersions.json';
|
||||
|
||||
export function formatVersions(versionArray: string[]): string {
|
||||
const allVersions = gameVersions.slice().reverse();
|
||||
const allReleases = allVersions.filter((x) => x.version_type === 'release');
|
||||
|
||||
const intervals = [];
|
||||
let currentInterval = 0;
|
||||
|
||||
for (let i = 0; i < versionArray.length; i++) {
|
||||
const index = allVersions.findIndex((x) => x.version === versionArray[i]);
|
||||
const releaseIndex = allReleases.findIndex((x) => x.version === versionArray[i]);
|
||||
|
||||
if (i === 0) {
|
||||
intervals.push([[versionArray[i], index, releaseIndex]]);
|
||||
} else {
|
||||
const intervalBase = intervals[currentInterval];
|
||||
|
||||
if (
|
||||
(index - intervalBase[intervalBase.length - 1][1] === 1 ||
|
||||
releaseIndex - intervalBase[intervalBase.length - 1][2] === 1) &&
|
||||
(allVersions[intervalBase[0][1]].version_type === 'release' ||
|
||||
allVersions[index].version_type !== 'release')
|
||||
) {
|
||||
intervalBase[1] = [versionArray[i], index, releaseIndex];
|
||||
} else {
|
||||
currentInterval += 1;
|
||||
intervals[currentInterval] = [[versionArray[i], index, releaseIndex]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newIntervals = [];
|
||||
for (let i = 0; i < intervals.length; i++) {
|
||||
const interval = intervals[i];
|
||||
|
||||
if (interval.length === 2 && interval[0][2] !== -1 && interval[1][2] === -1) {
|
||||
let lastSnapshot = null;
|
||||
for (let j = interval[1][1]; j > interval[0][1]; j--) {
|
||||
if (allVersions[j].version_type === 'release') {
|
||||
newIntervals.push([
|
||||
interval[0],
|
||||
[
|
||||
allVersions[j].version,
|
||||
j,
|
||||
allReleases.findIndex((x) => x.version === allVersions[j].version),
|
||||
],
|
||||
]);
|
||||
|
||||
if (lastSnapshot !== null && lastSnapshot !== j + 1) {
|
||||
newIntervals.push([[allVersions[lastSnapshot].version, lastSnapshot, -1], interval[1]]);
|
||||
} else {
|
||||
newIntervals.push([interval[1]]);
|
||||
}
|
||||
|
||||
break;
|
||||
} else {
|
||||
lastSnapshot = j;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newIntervals.push(interval);
|
||||
}
|
||||
}
|
||||
|
||||
const output = [];
|
||||
|
||||
for (const interval of newIntervals) {
|
||||
if (interval.length === 2) {
|
||||
output.push(`${interval[0][0]}—${interval[1][0]}`);
|
||||
} else {
|
||||
output.push(interval[0][0]);
|
||||
}
|
||||
}
|
||||
|
||||
return output.join(', ');
|
||||
}
|
||||
|
||||
export const getPrimary = (files: Version['files']) =>
|
||||
files.find((file) => file.primary) || files[0];
|
||||
|
||||
export function downloadUrl(file): string {
|
||||
return import.meta.env.VITE_API_URL + `version_file/${file?.hashes.sha1}/download`;
|
||||
}
|
||||
Reference in New Issue
Block a user