moar stuff.

This commit is contained in:
2023-09-23 14:04:13 +02:00
parent c7d3a3200e
commit 2188d489ea
48 changed files with 1684 additions and 91 deletions
@@ -0,0 +1,294 @@
<script lang="ts" context="module">
const cache: Record<string, ColorRGB> = {};
function getColor(color: string) {
if (cache.hasOwnProperty(color)) {
return Promise.resolve(cache[color]);
}
return fetchColor(color).then(r => {
cache[color] = r.rgb || {red: 255, green: 255, blue: 255};
return cache[color];
}).catch(err => {
delete cache[color];
throw err;
});
}
</script>
<script lang="ts">
import type { State } from "$lib/models/device";
import Icon from "./Icon.svelte";
import type { ColorRGB } from '$lib/models/color';
import { fetchColor } from '$lib/client/lucifer';
import { createEventDispatcher } from "svelte";
export let value: State;
export let deletable: boolean = false;
const dispatch = createEventDispatcher()
function togglePower() {
switch (value.power) {
case null: value.power = true; break;
case true: value.power = false; break;
case false: value.power = null; break;
}
}
function toggleIntensity() {
if (value.intensity === null) {
value.intensity = 0.5;
} else {
value.intensity = null;
}
}
function toggleTemperature() {
if (value.temperature === null) {
value.temperature = 20;
} else {
value.temperature = null;
}
}
function toggleColor() {
switch (value.color?.split(":")[0]) {
case undefined: value.color = "k:2750"; break;
case "k": value.color = "hs:180,0.5"; break;
case "hs": value.color = "rgb:1.000,0.800,0.066"; break;
case "rgb": value.color = "xy:0.2000,0.2000"; break;
case "xy": value.color = null; break;
}
}
function computeColorInputs(color: string | null) {
const kind = color?.split(":")[0];
const values = color?.split(":")[1]?.split(",").map(v => parseFloat(v)) || [0,0];
switch (kind) {
case undefined: colorKind = "null"; break;
case "k": colorKind = "k"; colorX = values[0]; colorY = values[1]||0; break;
case "hs": colorKind = "hs"; colorX = values[0]; colorY = values[1]||0; break;
case "xy": colorKind = "xy"; colorX = values[0]; colorY = values[1]||0; break;
case "rgb": colorKind = "rgb"; colorX = values[0]; colorY = values[1]||0, colorZ = values[2]||0; break;
}
computedColor = color;
}
function updateColor(kind: string, x: number, y: number, z: number) {
x = x || 0;
y = y || 0;
z = z || 0;
switch (kind) {
case "k": value.color = `k:${x.toFixed(0)}`; break;
case "xy": value.color = `xy:${x.toFixed(4)},${y.toFixed(4)}`; break;
case "hs": value.color = `hs:${x.toFixed(0)},${y.toFixed(3)}`; break;
case "rgb": value.color = `rgb:${x.toFixed(3)},${y.toFixed(3)},${z.toFixed(3)}`; break;
}
computedColor = value.color;
}
let intensityColor = "none";
$: intensityColor = value.intensity !== null ? "off" : "none";
let temperatureColor = "none";
$: temperatureColor = value.temperature !== null ? "off" : "none";
let powerColor = "";
$: if (value.power != null) {
powerColor = value.power ? "on" : "off"
} else {
powerColor = "none"
}
let computedColor: string | null = "";
let colorButton = "none";
let colorKind = "null";
let colorX = 0;
let colorY = 0;
let colorZ = 0;
$: if (value.color !== computedColor) {
computeColorInputs(value.color);
}
$: updateColor(colorKind, colorX, colorY, colorZ);
let timeout: NodeJS.Timeout | null = null;
let rgb = "";
$: if (value.color !== null) {
let before = value.color;
if (timeout !== null) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
if (value.color !== null) {
getColor(value.color).then(v => {
if (value.color === before) {
rgb = `rgb(${v.red*255}, ${v.green*255}, ${v.blue*255})`;
}
});
}
}, 50)
} else {
rgb = "hsl(240, 8%, 21%)";
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="assignment-state">
<div class="option {powerColor}"><Icon on:click={togglePower} block name="power" /></div>
<div class="option {intensityColor}">
<Icon on:click={toggleIntensity} block name="cirlce_notch" />
{#if value.intensity != null}
<input class="custom" type="number" min={0} max={1} step={0.01} bind:value={value.intensity} />
{/if}
</div>
<div class="option {temperatureColor}">
<Icon on:click={toggleTemperature} block name="temperature_half" />
{#if value.temperature != null}
<input class="custom" type="number" min={10} max={40} step={0.5} bind:value={value.temperature} />
{/if}
</div>
<div class="option {colorButton}" style="--color: {rgb}">
<Icon on:click={toggleColor} block name="palette" />
{#if colorKind !== "null"}
{#if colorKind === "k"}
<div class="color-input">
<label for="color_x">Kelvin</label>
<input class="custom" name="color_x" type="number" min={1000} max={12000} step={10} bind:value={colorX} />
</div>
{/if}
{#if colorKind === "hs"}
<div class="color-input">
<label for="color_x">Hue</label>
<input class="custom" name="color_x" type="number" bind:value={colorX} />
</div>
<div class="color-input">
<label for="color_y">Sat</label>
<input class="custom" name="color_y" type="number" min={0} max={1} step={0.01} bind:value={colorY} />
</div>
{/if}
{#if colorKind === "xy"}
<div class="color-input">
<label for="color_x">X</label>
<input class="custom" name="color_x" type="number" min={0} max={1} step={0.0001} bind:value={colorX} />
</div>
<div class="color-input">
<label for="color_y">Y</label>
<input class="custom" name="color_y" type="number" min={0} max={1} step={0.0001} bind:value={colorY} />
</div>
{/if}
{#if colorKind === "rgb"}
<div class="color-input">
<label class="short" for="color_x">Red</label>
<input class="custom short" name="color_x" type="number" min={0} max={1} step={0.001} bind:value={colorX} />
</div>
<div class="color-input">
<label class="short" for="color_y">Green</label>
<input class="custom short" name="color_y" type="number" min={0} max={1} step={0.001} bind:value={colorY} />
</div>
<div class="color-input">
<label class="short" for="color_z">Blue</label>
<input class="custom short" name="color_z" type="number" min={0} max={1} step={0.001} bind:value={colorZ} />
</div>
{/if}
{/if}
</div>
{#if deletable}
<div class="option red">
<Icon on:click={() => dispatch("delete")} block name="trash" />
</div>
{/if}
</div>
<style lang="sass">
@import "$lib/css/colors.sass"
div.assignment-state
display: flex
user-select: none
font-size: 1rem
width: 100%
flex-wrap: wrap
@media screen and (max-width: 749px)
font-size: 0.66rem
> div.option
box-shadow: 1px 1px 1px #000
display: flex
margin: 0.25em 0.25ch
cursor: pointer
:global(.icon)
padding: 0.1em 0.5ch
padding-top: 0.35em
color: var(--color)
input, :global(div.rangeSlider)
width: 4rem
font-size: 0.9rem
padding-left: 0
background: $color-main1
outline: none
border: none
text-align: center
font-size: 1rem
color: $color-main9
&::-webkit-inner-spin-button
-webkit-appearance: none
margin: 0
moz-appearance: textfield
&:focus
color: $color-main13
> div.color-input
display: flex
flex-direction: column
> label
font-size: 0.5em
color: $color-main5
text-align: center
margin: 0
line-height: 1em
margin-top: 0.15rem !important
padding-top: 0
margin-bottom: -0.2em
width: 2.5rem
&.short
width: 2.2rem
> input
width: 2.5rem
font-size: 0.8em
background: none
&.short
width: 2.2rem
&.none
background: $color-main1
color: $color-main2
&.off
background-color: $color-main2
color: $color-main6
&.on
background-color: $color-main2
color: #00FF00
&.red
background-color: $color-main2-red
color: $color-main6-redder
</style>
+31
View File
@@ -0,0 +1,31 @@
<script lang="ts">
export let red: boolean = false;
export let icon: boolean = false;
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class:red class:icon class="button" on:click><slot></slot></div>
<style lang="sass">
@import "$lib/css/colors.sass"
div.button
box-shadow: 1px 1px 1px #000
margin: 0.25em 0.25ch
cursor: pointer
user-select: none
font-size: 1rem
background-color: $color-main2
color: $color-main6
> :global(.icon)
padding: 0.1em 0.5ch
padding-top: 0.35em
&.icon
width: 2.8ch
&.red
background-color: $color-main2-red
color: $color-main6-redder
</style>
+117
View File
@@ -0,0 +1,117 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import Icon, { type IconName } from "./Icon.svelte";
export let tabIndex: number | undefined | null = void(0);
export let checked = false;
export let centered = false;
export let disabled = false;
export let noLabel = false;
export let noBorder = false;
export let inline = false;
export let icon: IconName = "check";
export let label = "(Missing label property)";
const dispatch = createEventDispatcher();
function handleClick() {
if (!disabled) {
checked = !checked;
dispatch('check', { checked });
}
}
function handlePress(e: KeyboardEvent) {
if (!disabled && ["enter", " ", " ", "c"].includes(e.key?.toLowerCase())) {
checked = !checked;
dispatch('check', { checked });
}
}
</script>
<div
class="checkbox"
class:centered
class:noLabel
class:inline
role="checkbox"
aria-checked={checked}
aria-disabled={disabled}
tabindex={tabIndex}
on:keypress={handlePress}
on:click={handleClick}
>
<div class="box" class:noBorder class:disabled class:checked class:unchecked={!checked}>
<Icon name={icon} />
</div>
{#if !noLabel}
<div class="label">{label}</div>
{/if}
</div>
<style lang="sass">
@import "$lib/css/colors"
div.checkbox
display: flex
flex-direction: row
flex-shrink: 0
margin-top: 0.5em
margin-bottom: 1em
-webkit-user-select: none
-moz-user-select: none
user-select: none
transition: 250ms
padding: 0.1em
font-size: 0.9em
&.noLabel
display: block
margin-top: 0.35em
margin-bottom: 0
font-size: 0.75em
div.checkbox.centered
margin: auto
:global(div.checkbox + div.checkbox)
margin-top: -0.75em
div.checkbox.inline
margin-top: 0.25rem
margin-bottom: 0.25rem
margin-right: 1rch
div.box
cursor: pointer
border: 0.5px solid
padding: 0.05em 0.3ch
padding-top: 0.2em
line-height: 1
background-color: $color-mainhalf
color: $color-mainhalf
border-color: $color-main5
font-size: 0.9em
&.noBorder
color: $color-main2
&.checked
color: $color-main9
background-color: $color-main4
&.disabled
color: $color-main1
&.checked
color: $color-main5
background-color: $color-main1
&.noBorder
border: none
background: none
div.label
margin: auto 0
margin-left: 1ch
-webkit-user-select: none
-moz-user-select: none
user-select: none
</style>
+12 -10
View File
@@ -12,11 +12,11 @@
import hue_adore_tube from "./icons/hue_adore_tube.svg?raw";
import hue_playbar from "./icons/hue_playbar.svg?raw";
import hue_go from "./icons/hue_go.svg?raw";
import square from "./icons/square.svg?raw";
import hexagon from "./icons/hexagon.svg?raw";
import triangle from "./icons/triangle.svg?raw";
import shape_square from "./icons/shape_square.svg?raw";
import shape_hexagon from "./icons/shape_hexagon.svg?raw";
import shape_triangle from "./icons/shape_triangle.svg?raw";
export const iconMap = Object.seal({
export const deviceIconMap = Object.seal({
generic_lamp,
generic_boob,
generic_ball,
@@ -29,13 +29,15 @@
hue_lightbulb_gu10,
hue_adore_tube,
hue_playbar,
square,
hexagon,
triangle,
shape_square,
shape_hexagon,
shape_triangle,
hue_go,
});
export type IconName = keyof typeof iconMap;
export const deviceIconList = Object.seal(Object.keys(deviceIconMap).sort()) as DeviceIconName[];
export type DeviceIconName = keyof typeof deviceIconMap;
</script>
<script lang="ts">
@@ -44,7 +46,7 @@
export let brightColor: ColorRGB;
export let darkColor: ColorRGB;
export let name: IconName;
export let name: DeviceIconName;
let gandalf: string;
let voldemort: string;
@@ -52,7 +54,7 @@
$: voldemort = rgbToHex(darkColor);
let icon: string;
$: icon = iconMap[name] || iconMap.generic_lamp;
$: icon = deviceIconMap[name] || deviceIconMap.generic_lamp;
let dataUrl: string;
$: {
@@ -0,0 +1,43 @@
<script lang="ts">
import { rgb, type ColorRGB } from "$lib/models/color";
import DeviceIcon, { deviceIconList, type DeviceIconName } from "./DeviceIcon.svelte";
import { DARK_COLOR, DARK_COLOR_SELECTED } from "./Lamp.svelte";
export let value: DeviceIconName;
</script>
<div class="device-icon-selector">
{#each deviceIconList as deviceIconName (deviceIconName)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="item" class:selected={value === deviceIconName} on:click={() => { value = deviceIconName }}>
<DeviceIcon
darkColor={ value === deviceIconName ? DARK_COLOR_SELECTED : DARK_COLOR }
brightColor={ value === deviceIconName ? rgb(0.517, 0.537, 1.000) : DARK_COLOR }
name={deviceIconName}
/>
</div>
{/each}
</div>
<style lang="sass">
@import "$lib/css/colors.sass"
div.device-icon-selector
display: flex
flex-direction: row
flex-wrap: wrap
> div.item
font-size: 2em
padding: 0.3rem
padding-bottom: 0
padding-top: 0.5rem
box-sizing: border-box
border: 2px solid $color-mainhalf
&.selected
border: 2px solid $color-main1
&:hover
background-color: $color-mainquarter
</style>
+18
View File
@@ -0,0 +1,18 @@
<script lang="ts">
export let reverse: boolean = false
</script>
<div class="hsplit" class:reverse><slot></slot></div>
<style lang="sass">
div.hsplit
display: flex
flex-direction: row
flex-basis: 10
&.reverse
flex-direction: row-reverse
@media screen and (max-width: 500px)
display: block
</style>
@@ -0,0 +1,18 @@
<script lang="ts">
export let weight = 1;
export let left = false;
export let right = false;
</script>
<div class="hsplit-part" class:right class:left style="flex: {weight}"><slot></slot></div>
<style lang="sass">
div.hsplit-part
box-sizing: border-box
padding: 0 0.5ch
&.left
padding-left: 0
&.right
padding-right: 0
</style>
+97
View File
@@ -0,0 +1,97 @@
<script lang="ts">
import Icon from "fa-svelte";
export let name: IconName = "question";
export let block: boolean = false;
export let marginAutio: boolean = false;
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
{#if block}
<div on:click class:marginAutio>
<Icon class="icon" icon={icons[name] || icons.question} />
</div>
{:else}
<Icon on:click class="icon" icon={icons[name] || icons.question} />
{/if}
<style>
div.marginAutio {
margin: auto;
}
</style>
<script lang="ts" context="module">
import { faQuestion } from "@fortawesome/free-solid-svg-icons/faQuestion";
import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus";
import { faPen } from "@fortawesome/free-solid-svg-icons/faPen";
import { faArchive } from "@fortawesome/free-solid-svg-icons/faArchive";
import { faCheck } from "@fortawesome/free-solid-svg-icons/faCheck";
import { faCog } from "@fortawesome/free-solid-svg-icons/faCog";
import { faLink } from "@fortawesome/free-solid-svg-icons/faLink";
import { faStar } from "@fortawesome/free-solid-svg-icons/faStar";
import { faTimes } from "@fortawesome/free-solid-svg-icons/faTimes";
import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner";
import { faHourglass } from "@fortawesome/free-solid-svg-icons/faHourglass";
import { faCalendar } from "@fortawesome/free-solid-svg-icons/faCalendar";
import { faExpand } from "@fortawesome/free-solid-svg-icons/faExpand";
import { faSearch } from "@fortawesome/free-solid-svg-icons/faSearch";
import { faClock } from "@fortawesome/free-solid-svg-icons/faClock";
import { faThumbtack } from "@fortawesome/free-solid-svg-icons/faThumbtack";
import { faHistory } from "@fortawesome/free-solid-svg-icons/faHistory";
import { faLightbulb } from "@fortawesome/free-solid-svg-icons/faLightbulb";
import { faChevronRight } from "@fortawesome/free-solid-svg-icons/faChevronRight";
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
import { faCheckToSlot } from "@fortawesome/free-solid-svg-icons/faCheckToSlot";
import { faEye } from "@fortawesome/free-solid-svg-icons/faEye";
import { faList } from "@fortawesome/free-solid-svg-icons/faList";
import { faPowerOff } from "@fortawesome/free-solid-svg-icons/faPowerOff";
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
import { faTemperatureHalf } from "@fortawesome/free-solid-svg-icons/faTemperatureHalf";
import { faPalette } from "@fortawesome/free-solid-svg-icons/faPalette";
import { faSwatchbook } from "@fortawesome/free-solid-svg-icons/faSwatchbook";
import { faCircleDot } from "@fortawesome/free-solid-svg-icons/faCircleDot";
import { faMasksTheater } from "@fortawesome/free-solid-svg-icons/faMasksTheater";
import { faTag } from "@fortawesome/free-solid-svg-icons/faTag";
const icons = {
"clock": faClock,
"thumbtack": faThumbtack,
"history": faHistory,
"question": faQuestion,
"plus": faPlus,
"pen": faPen,
"archive": faArchive,
"check": faCheck,
"cog": faCog,
"link": faLink,
"star": faStar,
"times": faTimes,
"lightbulb": faLightbulb,
"spinner": faSpinner,
"hourglass": faHourglass,
"calendar": faCalendar,
"expand": faExpand,
"search": faSearch,
"chevron_right": faChevronRight,
"chevron_down": faChevronDown,
"trash": faTrash,
"check_slot": faCheckToSlot,
"eye": faEye,
"list": faList,
"power": faPowerOff,
"cirlce_notch": faCircleNotch,
"temperature_half": faTemperatureHalf,
"palette": faPalette,
"swatch_book": faSwatchbook,
"circle_dot": faCircleDot,
"masks_theater": faMasksTheater,
"tag": faTag,
};
export type IconName = keyof typeof icons;
export const iconNames = Object.keys(icons).sort() as IconName[];
export const DEFAULT_ICON: IconName = "question";
</script>
+45 -2
View File
@@ -9,6 +9,7 @@
import { SupportFlags } from "$lib/models/device";
import { rgb, type ColorRGB } from "../models/color";
import DeviceIcon from "./DeviceIcon.svelte";
import Icon from "./Icon.svelte";
export let device: Device;
export let compact: boolean = false;
@@ -25,6 +26,9 @@
let barColor: ColorRGB | null;
let barFraction: number | null;
let roundboiText: string | null;
let roles: string[];
let tags: string[];
let hasRoleOrTag: boolean;
$: {
// TODO: Fix device.name on the backend
const nameAlias = device.aliases?.find(a => a.startsWith("lucifer:name:"));
@@ -34,6 +38,10 @@
deviceTitle = "";
}
roles = device.aliases?.filter(a => a.startsWith("lucifer:role:")).map(a => a.slice("lucifer:role:".length));
tags = device.aliases?.filter(a => a.startsWith("lucifer:tag:")).map(a => a.slice("lucifer:tag:".length));
hasRoleOrTag = !!(roles.length || tags.length)
barFraction = null;
barColor = null;
iconColor = null;
@@ -114,7 +122,25 @@
</DeviceIcon>
{/if}
</div>
<div class="title">{displayTitle}</div>
<div class="title">
<div class="name" class:hasRoleOrTag>{displayTitle}</div>
{#if hasRoleOrTag}
<div class="tag-list">
{#each roles as role}
<div class="tag">
<Icon block name="masks_theater" />
<div class="tag-name">{role}</div>
</div>
{/each}
{#each tags as tag}
<div class="tag">
<Icon block name="tag" />
<div class="tag-name">{tag}</div>
</div>
{/each}
</div>
{/if}
</div>
</div>
<div class="flatboi">
{#if barColor != null && barFraction != null}
@@ -182,7 +208,24 @@
margin-left: 2em
margin-right: 1ch
height: 1.6em
> div.name.hasRoleOrTag
margin-top: -0.275em
> div.tag-list
display: flex
flex-direction: row
font-size: 0.5em
> div.tag
display: flex
flex-direction: row
padding: 0 0.5ch
padding-right: 1ch
div.tag-name
padding-left: 0.5ch
> div.flatboi
height: 0.2em
+18 -14
View File
@@ -2,8 +2,8 @@
import { getModalContext, type ModalSelection } from "$lib/contexts/ModalContext.svelte";
export let show: boolean = false;
export let verb: string = "Submit";
export let noun: string = "Form";
export let titleText: string = "Edit Form";
export let submitText: string = "Submit";
export let cancelLabel: string = "Cancel";
export let wide: boolean = false;
export let error: string | null = null;
@@ -30,7 +30,7 @@
<div role="dialog" class="modal-background" on:keypress={onKeyPress}>
<div class="modal" class:wide class:nobody>
<div class="header" class:nobody>
<div class="title" class:noclose={!closable}>{verb} {noun}</div>
<div class="title" class:noclose={!closable}>{titleText}</div>
{#if (closable)}
<div class="x">
<!-- svelte-ignore a11y-click-events-have-key-events -->
@@ -46,7 +46,7 @@
<slot></slot>
</div>
<div class="button-row" class:nobody>
<button disabled={disabled} type="submit">{verb} {noun}</button>
<button disabled={disabled} type="submit">{submitText}</button>
<slot name="secondary-button-1"></slot>
<slot name="secondary-button-2"></slot>
<button disabled={!closable} on:click|preventDefault={onClose}>{cancelLabel}</button>
@@ -84,6 +84,10 @@
&.nobody {
background: none;
}
&.fullheight {
min-height: 100%
}
}
div.modal.wide {
max-width: 80ch;
@@ -182,7 +186,7 @@
-moz-user-select: none;
}
div.modal :global(input), div.modal :global(select), div.modal :global(textarea) {
div.modal :global(input:not(.custom)), div.modal :global(select), div.modal :global(textarea) {
width: calc(100% - 2ch);
margin-bottom: 1em;
margin-top: 0.25em;
@@ -206,7 +210,7 @@
padding: 0.5em 0;
}
div.modal :global(input)::placeholder {
div.modal :global(input:not(.custom))::placeholder {
opacity: 0.5;
}
div.modal :global(select:disabled) {
@@ -214,7 +218,7 @@
opacity: 1;
color: #789;
}
div.modal :global(input:disabled) {
div.modal :global(input:not(.custom):disabled) {
background: #1a1c1f;
color: #789;
}
@@ -229,22 +233,22 @@
color: #aaa;
}
div.modal :global(input.nolast) {
div.modal :global(input:not(.custom).nolast) {
margin-bottom: 0.5em;
}
div.modal :global(input[type="checkbox"]) {
div.modal :global(input:not(.custom)[type="checkbox"]) {
width: initial;
display: inline-block;
}
div.modal :global(input[type="checkbox"] + label) {
div.modal :global(input:not(.custom)[type="checkbox"] + label) {
width: initial;
display: inline-block;
padding: 0;
margin: 0;
}
div.modal :global(input:focus), div.modal :global(select:focus), div.modal :global(textarea:focus) {
div.modal :global(input:not(.custom):focus), div.modal :global(select:focus), div.modal :global(textarea:focus) {
background: #121418;
color: $color-main9;
border: none;
@@ -256,13 +260,13 @@
font-size: 0.9em;
}
div.modal :global(input::-webkit-outer-spin-button),
div.modal :global(input::-webkit-inner-spin-button) {
div.modal :global(input:not(.custom)::-webkit-outer-spin-button),
div.modal :global(input:not(.custom)::-webkit-inner-spin-button) {
-webkit-appearance: none;
margin: 0;
}
div.modal :global(input[type=number]) {
div.modal :global(input:not(.custom)[type=number]) {
appearance: textfield;
-moz-appearance: textfield;
}
@@ -0,0 +1,15 @@
<div class="modal-body"><slot></slot></div>
<style lang="sass">
div.modal-body
padding: 1em
padding-left: 0
flex: 1
@media screen and (max-width: 749px)
padding-left: 1em
padding: 0 1em
&:first-of-type
padding-left: 1em
</style>
@@ -0,0 +1,59 @@
<script lang="ts">
import Icon from "./Icon.svelte";
export let expanded: boolean;
export let title: string;
function toggleExpand() {
expanded = !expanded;
}
</script>
<div class="modal-section" class:expanded>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="modal-section-header" on:click={toggleExpand}>
<div class="title">{title}</div>
<Icon block name="{expanded ? "chevron_down" : "chevron_right"}" />
</div>
<div class="modal-section-body">
<slot></slot>
</div>
</div>
<style lang="sass">
@import "$lib/css/colors.sass"
div.modal-section
margin-left: -1.8ch
margin-right: -1.8ch
margin-bottom: 0.5em
> div.modal-section-body
display: none
padding: 0.25em 2.3ch
> div.modal-section-header
display: flex
flex-direction: row
user-select: none
padding: 0.5ch 1.6ch
padding-right: 1.8ch
color: $color-main4
> div.title
margin-left: 0.5ch
font-size: 0.9em
color: $color-main5
margin-right: auto
&.expanded
background-color: $color-mainhalf
> div.modal-section-body
display: block
> div.modal-section-header
color: $color-main5
> div.title
color: $color-main9
</style>
@@ -0,0 +1,92 @@
<script lang="ts">
import Icon from "./Icon.svelte";
export let value: string[];
export let exclaimMode = false;
let nextTag = "";
function onKey(ev: KeyboardEvent) {
if ((ev.metaKey||ev.ctrlKey) && nextTag === "" && ev.key === "Backspace") {
value = value.slice(0, -1);
}
if ((ev.metaKey||ev.ctrlKey) && nextTag !== "" && ev.key === "Enter") {
value = [...value, nextTag];
nextTag = "";
ev.preventDefault();
}
}
$: while (nextTag.includes(",")) {
const newTags = nextTag.split(",").map(t => t.trim());
value = [...value, ...newTags.slice(0, -1)];
nextTag = newTags[newTags.length - 1];
}
$: value = (value||[]).filter((e, i) => !value.slice(0, i).includes(e));
$: if (exclaimMode) {
value = (value||[]).filter((e, i) => (
e.startsWith("!")
? !value.slice(i+1).includes(e.slice(1))
: !value.slice(i+1).includes("!"+e)
));
}
</script>
<div class="tag-input">
{#each value as tag (tag)}
<div class="tag">
<span>{tag}</span>
<span class="comma">,</span>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span class="x" on:click={() => value = value.filter(v => v !== tag)}>
<Icon name="times" />
</span>
</div>
{/each}
<input placeholder="(type , or press Ctrl+Enter to add)" on:keyup={onKey} bind:value={nextTag} />
</div>
<style lang="sass">
@import "$lib/css/colors.sass"
div.tag-input
width: calc(100% - 2ch)
margin-bottom: 1em
margin-top: 0.20em
min-height: 2em
background: $color-mainhalf
color: $color-main8
border: none
outline: none
resize: vertical
padding: 0.5em 1ch
display: flex
flex-direction: row
flex-wrap: wrap
> div.tag
margin: 0.25em 0.5ch
background-color: $color-main1
padding: 0.25em 1ch
border-radius: 0.25em
span.x
font-size: 0.75em
line-height: 1em
user-select: none
cursor: pointer
&:hover
color: $color-main12
span.comma
font-size: 0
> input
margin-bottom: 0.125em
</style>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB