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
-
3frontend/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"> |
|||
import ModalContext from "$lib/contexts/ModalContext.svelte"; |
|||
import SelectContext from "$lib/contexts/SelectContext.svelte"; |
|||
import StateContext from "$lib/contexts/StateContext.svelte"; |
|||
</script> |
|||
|
|||
<StateContext> |
|||
<SelectContext> |
|||
<ModalContext> |
|||
<slot></slot> |
|||
</ModalContext> |
|||
</SelectContext> |
|||
</StateContext> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue