Gisle Aune
1 year ago
7 changed files with 416 additions and 9 deletions
-
274frontend/src/lib/components/Modal.svelte
-
32frontend/src/lib/contexts/ModalContext.svelte
-
73frontend/src/lib/contexts/SelectContext.svelte
-
27frontend/src/lib/css/colors.sass
-
2frontend/src/lib/models/device.ts
-
7frontend/src/routes/+layout.svelte
-
10frontend/src/routes/+page.svelte
@ -0,0 +1,274 @@ |
|||||
|
<script lang="ts"> |
||||
|
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 cancelLabel: string = "Cancel"; |
||||
|
export let wide: boolean = false; |
||||
|
export let error: string | null = null; |
||||
|
export let closable: boolean = false; |
||||
|
export let disabled: boolean = false; |
||||
|
export let nobody: boolean = false; |
||||
|
export let closeInstruction: ModalSelection = { kind: "closed" }; |
||||
|
|
||||
|
const { modal } = getModalContext(); |
||||
|
|
||||
|
function onClose() { |
||||
|
modal.set(closeInstruction); |
||||
|
} |
||||
|
|
||||
|
function onKeyPress(e: KeyboardEvent) { |
||||
|
if (e.key.toLocaleLowerCase() === "escape") { |
||||
|
onClose(); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
{#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="header" class:nobody> |
||||
|
<div class="title" class:noclose={!closable}>{verb} {noun}</div> |
||||
|
{#if (closable)} |
||||
|
<div class="x"> |
||||
|
<!-- svelte-ignore a11y-click-events-have-key-events --> |
||||
|
<!-- svelte-ignore a11y-no-static-element-interactions --> |
||||
|
<div class="button" on:click={onClose} class:nobody>×</div> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
{#if (error != null)} |
||||
|
<div class="error">{error}</div> |
||||
|
{/if} |
||||
|
<div class="body" class:nobody> |
||||
|
<slot></slot> |
||||
|
</div> |
||||
|
<div class="button-row" class:nobody> |
||||
|
<button disabled={disabled} type="submit">{verb} {noun}</button> |
||||
|
<slot name="secondary-button-1"></slot> |
||||
|
<slot name="secondary-button-2"></slot> |
||||
|
<button disabled={!closable} on:click|preventDefault={onClose}>{cancelLabel}</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
{/if} |
||||
|
|
||||
|
<style lang="scss"> |
||||
|
@import "$lib/css/colors.sass"; |
||||
|
|
||||
|
div.modal-background { |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
background: rgba(0,0,0,0.5); |
||||
|
} |
||||
|
|
||||
|
div.modal { |
||||
|
position: absolute; |
||||
|
left: 50%; |
||||
|
top: 50%; |
||||
|
width: calc(100vw - 4em); |
||||
|
max-width: 40ch; |
||||
|
max-height: calc(100vh - 4em); |
||||
|
overflow: auto; |
||||
|
transform: translate(-50%,-50%); |
||||
|
border-radius: 0.5em; |
||||
|
box-shadow: 1px 1px 1px $color-main0; |
||||
|
|
||||
|
background: $color-main1; |
||||
|
|
||||
|
&.nobody { |
||||
|
background: none; |
||||
|
} |
||||
|
} |
||||
|
div.modal.wide { |
||||
|
max-width: 80ch; |
||||
|
} |
||||
|
|
||||
|
div.header { |
||||
|
padding: 1em; |
||||
|
padding-bottom: 0; |
||||
|
} |
||||
|
|
||||
|
div.error { |
||||
|
margin: 0.5em; |
||||
|
padding: 0.5em; |
||||
|
|
||||
|
border: 1px solid rgb(204, 65, 65); |
||||
|
border-radius: 0.2em; |
||||
|
background-color: rgb(133, 39, 39); |
||||
|
color: rgb(211, 141, 141); |
||||
|
|
||||
|
animation: fadein 0.5s; |
||||
|
} |
||||
|
|
||||
|
div.body { |
||||
|
padding: 1em 0; |
||||
|
margin-top: 0; |
||||
|
padding-top: 0; |
||||
|
|
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
|
||||
|
@media screen and (max-width: 749px) { |
||||
|
display: block; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
div.title { |
||||
|
color: $color-main9; |
||||
|
line-height: 1em; |
||||
|
} |
||||
|
|
||||
|
div.x { |
||||
|
position: relative; |
||||
|
line-height: 1em; |
||||
|
top: -1em; |
||||
|
text-align: right; |
||||
|
} |
||||
|
div.x div.button { |
||||
|
color: $color-main9; |
||||
|
display: inline-block; |
||||
|
padding: 0em 0.5ch 0.1em 0.5ch; |
||||
|
line-height: 1em; |
||||
|
user-select: none; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
div.x div.button:hover { |
||||
|
color: $color-main9; |
||||
|
} |
||||
|
|
||||
|
div.button-row { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
border-top: 0.5px solid #000; |
||||
|
|
||||
|
:global(button) { |
||||
|
display: inline-block; |
||||
|
padding: 0.25em 1ch; |
||||
|
font-size: 1.25em; |
||||
|
|
||||
|
background: none; |
||||
|
border: none; |
||||
|
color: $color-main9; |
||||
|
border-left : 0.5px solid #111; |
||||
|
|
||||
|
cursor: pointer; |
||||
|
} |
||||
|
:global(button:first-of-type) { |
||||
|
margin-left: auto; |
||||
|
} |
||||
|
:global(button:hover) { |
||||
|
background-color: #111; |
||||
|
} |
||||
|
:global(button:disabled) { |
||||
|
color: #444; |
||||
|
} |
||||
|
|
||||
|
&.nobody { |
||||
|
display: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
div.modal :global(label) { |
||||
|
padding: 1em 0 0.125em 0.25ch; |
||||
|
font-size: 0.9em; |
||||
|
user-select: none; |
||||
|
-webkit-user-select: none; |
||||
|
-moz-user-select: none; |
||||
|
} |
||||
|
|
||||
|
div.modal :global(input), div.modal :global(select), div.modal :global(textarea) { |
||||
|
width: calc(100% - 2ch); |
||||
|
margin-bottom: 1em; |
||||
|
margin-top: 0.25em; |
||||
|
|
||||
|
background: #121418; |
||||
|
color: #789; |
||||
|
border: none; |
||||
|
outline: none; |
||||
|
resize: vertical; |
||||
|
padding: 0.5em 1ch; |
||||
|
} |
||||
|
div.modal :global(select) { |
||||
|
padding-left: 0.5ch; |
||||
|
padding: 0.45em 1ch; |
||||
|
width: 100%; |
||||
|
} |
||||
|
div.modal :global(select option), div.modal :global(select optgroup) { |
||||
|
-webkit-appearance: none; |
||||
|
-moz-appearance: none; |
||||
|
appearance: none; |
||||
|
padding: 0.5em 0; |
||||
|
} |
||||
|
|
||||
|
div.modal :global(input)::placeholder { |
||||
|
opacity: 0.5; |
||||
|
} |
||||
|
div.modal :global(select:disabled) { |
||||
|
background: #1a1c1f; |
||||
|
opacity: 1; |
||||
|
color: #789; |
||||
|
} |
||||
|
div.modal :global(input:disabled) { |
||||
|
background: #1a1c1f; |
||||
|
color: #789; |
||||
|
} |
||||
|
div.modal :global(textarea) { |
||||
|
min-height: 6em; |
||||
|
height: 6em; |
||||
|
font-family: inherit; |
||||
|
resize: none0; |
||||
|
} |
||||
|
div.modal :global(textarea:disabled) { |
||||
|
background: #444; |
||||
|
color: #aaa; |
||||
|
} |
||||
|
|
||||
|
div.modal :global(input.nolast) { |
||||
|
margin-bottom: 0.5em; |
||||
|
} |
||||
|
|
||||
|
div.modal :global(input[type="checkbox"]) { |
||||
|
width: initial; |
||||
|
display: inline-block; |
||||
|
} |
||||
|
div.modal :global(input[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) { |
||||
|
background: #121418; |
||||
|
color: $color-main9; |
||||
|
border: none; |
||||
|
outline: none; |
||||
|
} |
||||
|
|
||||
|
div.modal :global(p) { |
||||
|
margin: 0.25em 1ch 1em 1ch; |
||||
|
font-size: 0.9em; |
||||
|
} |
||||
|
|
||||
|
div.modal :global(input::-webkit-outer-spin-button), |
||||
|
div.modal :global(input::-webkit-inner-spin-button) { |
||||
|
-webkit-appearance: none; |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
div.modal :global(input[type=number]) { |
||||
|
appearance: textfield; |
||||
|
-moz-appearance: textfield; |
||||
|
} |
||||
|
|
||||
|
@keyframes fadein { |
||||
|
from { opacity: 0; } |
||||
|
to { opacity: 1; } |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,32 @@ |
|||||
|
<script lang="ts" context="module"> |
||||
|
import type { DeviceEditOp } from "$lib/models/device"; |
||||
|
import type Device from "$lib/models/device"; |
||||
|
import type Script from "$lib/models/script"; |
||||
|
import type { ScriptLine } from "$lib/models/script"; |
||||
|
import { getContext, setContext } from "svelte"; |
||||
|
import { writable, type Writable } from "svelte/store"; |
||||
|
|
||||
|
const ctxKey = {ctx: "ModalContext"}; |
||||
|
|
||||
|
export type ModalSelection = |
||||
|
| { kind: "closed" } |
||||
|
| { kind: "device.edit", images: Device[], op: DeviceEditOp } |
||||
|
| { kind: "script.edit", id: string | null, lines: ScriptLine[] } |
||||
|
| { kind: "script.execute", script: Script } |
||||
|
|
||||
|
export interface ModalContextState { |
||||
|
modal: Writable<ModalSelection> |
||||
|
} |
||||
|
|
||||
|
export function getModalContext(): ModalContextState { |
||||
|
return getContext(ctxKey) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
setContext<ModalContextState>(ctxKey, { |
||||
|
modal: writable({kind: "closed"}), |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<slot></slot> |
@ -0,0 +1,27 @@ |
|||||
|
$color-maindark: hsl(240, 8%, 3.5%) |
||||
|
$color-main0: hsl(240, 8%, 7%) |
||||
|
$color-main0-transparent: hsla(240, 8%, 10%, 0.7) |
||||
|
$color-mainhalf: hsl(240, 8%, 10.5%) |
||||
|
$color-main1: hsl(240, 8%, 14%) |
||||
|
$color-main1-transparent: hsla(240, 8%, 17%, 0.7) |
||||
|
$color-main2: hsl(240, 8%, 21%) |
||||
|
$color-main2-transparent: hsla(240, 8%, 24%, 0.7) |
||||
|
$color-main3: hsl(240, 8%, 28%) |
||||
|
$color-main4: hsl(240, 8%, 35%) |
||||
|
$color-main5: hsl(240, 8%, 42%) |
||||
|
$color-main6: hsl(240, 8%, 49%) |
||||
|
$color-main7: hsl(240, 8%, 56%) // Default |
||||
|
$color-main8: hsl(240, 8%, 63%) |
||||
|
$color-main9: hsl(240, 8%, 70%) |
||||
|
$color-main10: hsl(240, 8%, 77%) |
||||
|
$color-main11: hsl(240, 8%, 84%) |
||||
|
$color-main12: hsl(240, 8%, 91%) |
||||
|
$color-main13: hsl(240, 8%, 98%) |
||||
|
|
||||
|
$opacity-entry3: 0.20 |
||||
|
$opacity-entry4: 0.40 |
||||
|
$opacity-entry5: 0.60 |
||||
|
$opacity-entry6: 0.80 |
||||
|
|
||||
|
$color-green: hsl(120, 50%, 74%) |
||||
|
$color-green-dark: hsl(120, 50%, 35%) |
@ -1,10 +1,13 @@ |
|||||
<script lang="ts"> |
<script lang="ts"> |
||||
import SelectContext from "$lib/contexts/SelectContext.svelte"; |
|
||||
|
import ModalContext from "$lib/contexts/ModalContext.svelte"; |
||||
|
import SelectContext from "$lib/contexts/SelectContext.svelte"; |
||||
import StateContext from "$lib/contexts/StateContext.svelte"; |
import StateContext from "$lib/contexts/StateContext.svelte"; |
||||
</script> |
</script> |
||||
|
|
||||
<StateContext> |
<StateContext> |
||||
<SelectContext> |
<SelectContext> |
||||
<slot></slot> |
|
||||
|
<ModalContext> |
||||
|
<slot></slot> |
||||
|
</ModalContext> |
||||
</SelectContext> |
</SelectContext> |
||||
</StateContext> |
</StateContext> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue