script editor

This commit is contained in:
2023-10-06 13:15:19 +02:00
parent 13da56009e
commit e06cf1ae34
35 changed files with 776 additions and 68 deletions
+7 -1
View File
@@ -4,12 +4,13 @@
import Icon, { type IconName } from "./Icon.svelte";
export let tabIndex: number | undefined | null = void(0);
export let checked = false;
export let checked: boolean | string | number;
export let centered = false;
export let disabled = false;
export let noLabel = false;
export let noBorder = false;
export let inline = false;
export let small = false;
export let icon: IconName = "check";
export let label = "(Missing label property)";
@@ -35,6 +36,7 @@
class:centered
class:noLabel
class:inline
class:small
role="checkbox"
aria-checked={checked}
aria-disabled={disabled}
@@ -72,6 +74,10 @@
margin-bottom: 0
font-size: 0.75em
&.small
padding-top: 0
font-size: 0.65em
div.checkbox.centered
margin: auto
+11
View File
@@ -55,7 +55,13 @@
import { faMasksTheater } from "@fortawesome/free-solid-svg-icons/faMasksTheater";
import { faTag } from "@fortawesome/free-solid-svg-icons/faTag";
import { faFilter } from "@fortawesome/free-solid-svg-icons/faFilter";
import { faSignature } from "@fortawesome/free-solid-svg-icons/faSignature";
import { faAsterisk } from "@fortawesome/free-solid-svg-icons/faAsterisk";
import { faPlay } from "@fortawesome/free-solid-svg-icons/faPlay";
import { faClockRotateLeft } from "@fortawesome/free-solid-svg-icons/faClockRotateLeft";
import { faNoteSticky } from "@fortawesome/free-solid-svg-icons/faNoteSticky";
const icons = {
"clock": faClock,
"thumbtack": faThumbtack,
@@ -90,6 +96,11 @@
"masks_theater": faMasksTheater,
"tag": faTag,
"filter": faFilter,
"signature": faSignature,
"asterisk": faAsterisk,
"play": faPlay,
"clock_rotate_left": faClockRotateLeft,
"note_sticky": faNoteSticky,
};
export type IconName = keyof typeof icons;
+13 -7
View File
@@ -6,6 +6,8 @@
export let submitText: string = "Submit";
export let cancelLabel: string = "Cancel";
export let wide: boolean = false;
export let ultrawide: boolean = false;
export let fullheight: boolean = false;
export let error: string | null = null;
export let closable: boolean = false;
export let disabled: boolean = false;
@@ -28,7 +30,7 @@
{#if show}
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div role="dialog" class="modal-background" on:keypress={onKeyPress}>
<div class="modal" class:wide class:nobody>
<div class="modal" class:wide class:ultrawide class:fullheight class:nobody>
<div class="header" class:nobody>
<div class="title" class:noclose={!closable}>{titleText}</div>
{#if (closable)}
@@ -88,9 +90,13 @@
&.fullheight {
min-height: 100%
}
}
div.modal.wide {
max-width: 80ch;
&.wide {
max-width: 80ch;
}
&.ultrawide {
max-width: 120ch;
}
}
div.header {
@@ -186,7 +192,7 @@
-moz-user-select: none;
}
div.modal :global(input:not(.custom)), div.modal :global(select), div.modal :global(textarea) {
div.modal :global(input:not(.custom)), div.modal :global(select:not(.custom)), div.modal :global(textarea) {
width: calc(100% - 2ch);
margin-bottom: 1em;
margin-top: 0.25em;
@@ -198,12 +204,12 @@
resize: vertical;
padding: 0.5em 1ch;
}
div.modal :global(select) {
div.modal :global(select:not(.custom)) {
padding-left: 0.5ch;
padding: 0.45em 1ch;
width: 100%;
}
div.modal :global(select option), div.modal :global(select optgroup) {
div.modal :global(select:not(.custom) option), div.modal :global(select:not(.custom) optgroup) {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
@@ -3,9 +3,12 @@
export let expanded: boolean;
export let title: string;
export let disableExpandToggle: boolean = false;
function toggleExpand() {
expanded = !expanded;
if (!disableExpandToggle) {
expanded = !expanded;
}
}
</script>
@@ -13,7 +16,9 @@
<!-- 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"}" />
{#if !disableExpandToggle}
<Icon block name="{expanded ? "chevron_down" : "chevron_right"}" />
{/if}
</div>
<div class="modal-section-body">
<slot></slot>
@@ -5,6 +5,7 @@
<script lang="ts">
import { rgbString, type Color, stringifyColor, parseColor } from "$lib/models/color";
import { hsToHsl, kToRgb, xyToRgb } from "$lib/utils/color";
import { onMount } from "svelte";
import ColorPicker, { selectedColorPicker } from "../ColorPicker.svelte";
import BFormOption from "./BFormOption.svelte";
import BFormParameter from "./BFormParameter.svelte";
@@ -46,6 +47,18 @@
value = stringifyColor(color);
}
onMount(() => {
if (value !== null && $selectedColorPicker !== null) {
$selectedColorPicker = colorPickerId;
}
return () => {
if ($selectedColorPicker = colorPickerId) {
$selectedColorPicker = null;
}
}
})
let colorStr = "";
$: {
if (color !== null) {
@@ -17,14 +17,15 @@
export let icon: IconName = "check";
export let unclickable: boolean = false;
export let red: boolean = false;
export let alwaysOpen: boolean = false;
const enabled = writable(state === true);
$: $enabled = state === true;
$: $enabled = state === true || alwaysOpen;
setContext(ctxKey, { subscribe: enabled.subscribe });
</script>
<div class="bform-option" class:unclickable class:on={state === true} class:off={state === false} style="--color: {color}">
<div class="bform-option" class:red class:unclickable class:on={state === true} class:off={state === false} style="--color: {color}">
<Icon on:click block name={icon} />
<div class="bform-option-body">
<slot></slot>
@@ -42,24 +43,24 @@
cursor: pointer
background: $color-main1
:global(.icon)
> :global(div > .icon)
padding: 0.1em 0.5ch
padding-top: 0.45em
color: $color-main2
&.off
background-color: $color-main2
:global(.icon)
> :global(div > .icon)
color: $color-main4
&.on
background-color: $color-main2
:global(.icon)
> :global(div > .icon)
color: var(--color)
&.red
background-color: $color-main2-red
:global(.icon)
> :global(div > .icon)
color: $color-main6-redder
&.unclickable
@@ -1,9 +1,10 @@
<script lang="ts">
import { getBFormOptionEnabled } from "./BFormOption.svelte";
import Checkbox from "../Checkbox.svelte";
import { getBFormOptionEnabled } from "./BFormOption.svelte";
export let label: string;
export let type: "number" | "text" | "select";
export let value: number | string;
export let type: "number" | "text" | "select" | "checkbox";
export let value: number | string | boolean;
export let wide: boolean = false;
export let narrow: boolean = false;
export let narrower: boolean = false;
@@ -23,6 +24,8 @@
<input class="custom" on:blur on:focus type="number" bind:value={value} {min} {max} {step} />
{:else if type === "text"}
<input class="custom" on:blur on:focus type="text" bind:value={value} />
{:else if type === "checkbox"}
<Checkbox small centered noLabel bind:checked={value} />
{:else if type === "select"}
<select class="custom" bind:value={value}>
{#each options as opt (opt.value)}
@@ -84,10 +87,7 @@
&.wide
> input, > label, > select
text-align: left
width: 20rem
> label
padding-left: 0.2rch
&.narrow
> input, > label, > select
@@ -1,6 +1,5 @@
<script lang="ts">
import type { State } from "$lib/models/device";
import type { ColorRGB } from '$lib/models/color';
import BFormLine from "../bforms/BFormLine.svelte";
import BFormTemperatureOption from "../bforms/BFormTemperatureOption.svelte";
import BFormColorOption from "../bforms/BFormColorOption.svelte";
@@ -10,8 +9,6 @@
export let value: State;
export let deletable: boolean = false;
$: console.log(value);
</script>
<BFormLine>
@@ -0,0 +1,58 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import Icon from "../Icon.svelte";
export let label: string;
export let add: boolean = false;
export let nopad: boolean = false;
const dispatch = createEventDispatcher();
function onAdd() {
dispatch("add");
}
</script>
<div class="block" class:nopad>
<div class="label">
<div class="text">{label}</div>
{#if add}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="add-button" on:click={onAdd}><Icon name="plus" /></div>
{/if}
</div>
<slot></slot>
</div>
<style lang="sass">
@import "$lib/css/colors.sass"
div.block
padding-left: 1ch
margin-left: 0.25ch
border-left: 2px solid $color-main2
margin-bottom: 0.5em
&.nopad
margin-left: 0
div.label
color: $color-main5
font-size: 0.666em
padding-top: 0.1em
padding-left: 0.3ch
display: flex
flex-direction: row
div.add-button
color: $color-green-dark
margin-top: 0.1em
margin-left: 0.5ch
font-size: 0.9em
line-height: 1em
cursor: pointer
&:hover
color: $color-green
</style>
@@ -0,0 +1,31 @@
<script lang="ts">
import type { ScriptCondition } from "$lib/models/script";
import BFormOption from "../bforms/BFormOption.svelte";
import BFormParameter from "../bforms/BFormParameter.svelte";
export let value: Required<ScriptCondition>
</script>
<BFormOption state unclickable icon="question">
<BFormParameter narrowest type="checkbox" bind:value={value.not} label="Not" />
<BFormParameter narrow type="select" bind:value={value.scope} label="Scope" options={[
{ label: "Match", value: "match" },
{ label: "Devices", value: "device" },
{ label: "Global", value: "global" },
]} />
<BFormParameter type="text" bind:value={value.key} label="Key" />
<BFormParameter narrow type="select" bind:value={value.op} label="Op" options={[
{ label: "==", value: "eq" },
{ label: "!==", value: "neq" },
{ label: ">", value: "gt" },
{ label: ">=", value: "gte" },
{ label: "<", value: "lt" },
{ label: "<=", value: "lte" },
{ label: "contains", value: "contains" },
{ label: "exists", value: "exists" },
{ label: "numerical", value: "numerical" },
]} />
{#if value.op !== "numerical" && value.op !== "exists"}
<BFormParameter type="text" bind:value={value.value} label="Value" />
{/if}
</BFormOption>
@@ -0,0 +1,74 @@
<script lang="ts">
import type { EffectRaw } from "$lib/models/assignment";
import BFormOption from "../bforms/BFormOption.svelte";
import BFormParameter from "../bforms/BFormParameter.svelte";
export let value: EffectRaw
</script>
<BFormOption unclickable state icon="play">
<BFormParameter
narrow label="Effect" type="select" bind:value={value.kind}
options={[
{ value:"solid", label:"Solid" },
{ value:"gradient", label:"Gradient" },
{ value:"pattern", label:"Pattern" },
{ value:"random", label:"Random" },
{ value:"vrange", label:"V. Range" },
]}
/>
</BFormOption>
{#if value.kind !== "manual" && value.kind !== "vrange"}
<BFormOption alwaysOpen unclickable state={value.animationMs > 0} icon="clock_rotate_left">
<BFormParameter
narrow label="Milliseconds" type="number"
min={0} max={10000000} step={100}
bind:value={value.animationMs}
/>
{#if value.kind === "solid"}
<BFormParameter
narrower label="Interpolate" type="number"
min={0} max={10} step={1}
bind:value={value.interleave}
/>
{/if}
{#if value.kind === "gradient"}
<BFormParameter
narrower label="Reverse" type="checkbox"
bind:value={value.reverse}
/>
{/if}
{#if value.kind === "gradient" || value.kind === "random"}
<BFormParameter
narrower label="Interpolate" type="checkbox"
bind:value={value.interpolate}
/>
{/if}
</BFormOption>
{/if}
{#if value.kind === "vrange"}
<BFormOption unclickable state icon="temperature_half">
<BFormParameter
label="Variable" type="select" bind:value={value.variable}
options={[
{ value:"motion.min", label:"Min Motion" },
{ value:"motion.max", label:"Max Motion" },
{ value:"motion.avg", label:"Avg Motion" },
{ value:"temperature.min", label:"Min Temperature" },
{ value:"temperature.max", label:"Max Temperature" },
{ value:"temperature.avg", label:"Avg Temperature" },
]}
/>
<BFormParameter
narrower label="Min" type="number"
min={0} max={1000} step={1}
bind:value={value.min}
/>
<BFormParameter
narrower label="Max" type="number"
min={0} max={1000} step={1}
bind:value={value.max}
/>
</BFormOption>
{/if}
@@ -0,0 +1,111 @@
<script lang="ts" context="module">
const KIND_ICONS: Record<ScriptLineEditable["kind"], IconName> = {
"if": "chevron_right",
"select": "filter",
"assign": "swatch_book",
"set": "pen",
}
</script>
<script lang="ts">
import { BLANK_STATE, copyState } from "$lib/models/device";
import { toEditableScriptLine, type ScriptLineEditable } from "$lib/models/script";
import type { IconName } from "../Icon.svelte";
import BFormDeleteOption from "../bforms/BFormDeleteOption.svelte";
import BFormLine from "../bforms/BFormLine.svelte";
import BFormOption from "../bforms/BFormOption.svelte";
import BFormParameter from "../bforms/BFormParameter.svelte";
import ScriptAssignmentState from "./ScriptAssignmentState.svelte";
import ScriptBlock from "./ScriptBlock.svelte";
import ScriptCondition from "./ScriptCondition.svelte";
import ScriptEffect from "./ScriptEffect.svelte";
import ScriptLineBlock from "./ScriptLineBlock.svelte";
import ScriptMatch from "./ScriptMatch.svelte";
import ScriptSet from "./ScriptSet.svelte";
export let value: ScriptLineEditable;
function deleteState(index: number) {
value.effect.states = [
...value.effect.states.slice(0, index),
...value.effect.states.slice(index + 1),
];
if (value.effect.states.length === 0) {
value.effect.states = [{...BLANK_STATE}];
}
}
function addState() {
value.effect.states = [
...value.effect.states,
copyState(value.effect.states[value.effect.states.length - 1]),
];
}
function addThen() {
value.then = [
...value.then,
toEditableScriptLine({if: {
condition: { scope: "global", key: "", op: "eq" },
then: [],
else: [],
}})
]
}
function addElse() {
value.else = [
...value.else,
toEditableScriptLine({if: {
condition: { scope: "global", key: "", op: "eq" },
then: [],
else: [],
}})
]
}
</script>
<BFormLine>
<BFormOption unclickable state icon={KIND_ICONS[value.kind]}>
<BFormParameter
narrow
label="Op"
type="select"
bind:value={value.kind}
options={[
{label: "If", value: "if"},
{label: "Assign", value: "assign"},
{label: "Set", value: "set"},
{label: "Select", value: "select"},
]}
/>
</BFormOption>
{#if value.kind === "assign" || value.kind === "select"}
<ScriptMatch bind:matchKind={value.matchKind} bind:matchValue={value.matchValue} />
{/if}
{#if value.kind === "assign"}
<ScriptEffect bind:value={value.effect} />
{/if}
{#if value.kind === "if"}
<ScriptCondition bind:value={value.condition} />
{/if}
{#if value.kind === "set"}
<ScriptSet bind:key={value.key} bind:scope={value.scope} bind:value={value.value} />
{/if}
<BFormDeleteOption on:delete />
</BFormLine>
{#if value.kind === "assign"}
<ScriptBlock on:add={addState} add label="States">
{#each value.effect.states as state, i}
<ScriptAssignmentState bind:value={state} deletable on:delete={() => deleteState(i)} />
{/each}
</ScriptBlock>
{/if}
{#if value.kind === "select" || value.kind === "if"}
<ScriptLineBlock on:add={addThen} add label="Then" bind:value={value.then} />
{/if}
{#if value.kind === "if"}
<ScriptLineBlock on:add={addElse} add label="Else" bind:value={value.else} />
{/if}
@@ -0,0 +1,19 @@
<script lang="ts">
import type { ScriptLineEditable } from "$lib/models/script";
import ScriptBlock from "./ScriptBlock.svelte";
import ScriptLine from "./ScriptLine.svelte";
export let label: string;
export let value: ScriptLineEditable[];
export let add: boolean = false;
function deleteLine(i: number) {
value = [...value.slice(0, i), ...value.slice(i + 1)];
}
</script>
<ScriptBlock on:add add={add} label={label}>
{#each value as line, i}
<ScriptLine bind:value={line} on:delete={() => deleteLine(i)} />
{/each}
</ScriptBlock>
@@ -0,0 +1,38 @@
<script lang="ts" context="module">
const MATCH_ICONS: Record<"all" | "raw" | "name" | "role", IconName> = {
name: "signature",
raw: "cog",
role: "masks_theater",
all: "asterisk",
}
</script>
<script lang="ts">
import type { IconName } from "../Icon.svelte";
import BFormOption from "../bforms/BFormOption.svelte";
import BFormParameter from "../bforms/BFormParameter.svelte";
export let matchKind: "all" | "name" | "role" | "raw";
export let matchValue: string;
function toggleMatchKind() {
switch (matchKind) {
case "all": matchKind = "name"; break;
case "name": matchKind = "role"; break;
case "role": matchKind = "raw"; break;
case "raw": matchKind = "all"; break;
}
}
</script>
<BFormOption on:click={toggleMatchKind} state icon={MATCH_ICONS[matchKind]}>
{#if matchKind !== "all"}
<BFormParameter
wide={matchKind === "raw"}
type="text"
label="Match ({matchKind})"
bind:value={matchValue}
/>
{/if}
</BFormOption>
@@ -0,0 +1,18 @@
<script lang="ts">
import BFormOption from "../bforms/BFormOption.svelte";
import BFormParameter from "../bforms/BFormParameter.svelte";
export let scope: string;
export let key: string;
export let value: string;
</script>
<BFormOption state unclickable icon="note_sticky">
<BFormParameter narrow type="select" bind:value={scope} label="Scope" options={[
{ label: "Match", value: "match" },
{ label: "Devices", value: "device" },
{ label: "Global", value: "global" },
]} />
<BFormParameter type="text" bind:value={key} label="Key" />
<BFormParameter type="text" bind:value={value} label="Value" />
</BFormOption>