@lavalogic/scoria 0.40.26 → 0.40.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Components/Base/BaseModal.svelte +55 -41
- package/dist/Components/ChipsInput.svelte +194 -0
- package/dist/Components/ChipsInput.svelte.d.ts +4 -0
- package/dist/Components/ChipsInputProps.d.ts +39 -0
- package/dist/Components/ChipsInputProps.js +1 -0
- package/dist/Components/ConditionBuilder/ConditionBuilder.svelte +101 -101
- package/dist/Components/ResourceCalendar/ResourceCalendar.svelte +241 -241
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/scss/_colours.scss +6 -0
- package/dist/scss/colours.d.ts +3 -0
- package/dist/scss/colours.js +6 -0
- package/package.json +1 -1
|
@@ -1,29 +1,40 @@
|
|
|
1
|
-
<svelte:options runes />
|
|
2
|
-
|
|
3
|
-
<!--
|
|
4
|
-
BaseModal.svelte - internal primitive that owns the `<dialog>`
|
|
5
|
-
open/close + Escape/backdrop dismissal + autofocus plumbing
|
|
6
|
-
shared between `Modal`, `DesktopModal`, and `TouchModal`. See
|
|
7
|
-
`BaseModalProps.ts` for the contract.
|
|
8
|
-
|
|
9
|
-
Cosmetic-free for inner content by design: each consumer renders
|
|
10
|
-
its own header / main / footer / button-row markup as
|
|
11
|
-
`{@render children()}`. The consumer also keeps its own
|
|
12
|
-
footer-text rules, scanner shortcut wiring, colourSet button
|
|
13
|
-
styling, etc. - those concerns intentionally do not live in
|
|
14
|
-
BaseModal.
|
|
15
|
-
|
|
16
|
-
The shared dialog SHELL styling (border, backdrop, zoom-in
|
|
17
|
-
animation, the `--dialog-*` size variables and the static-height
|
|
18
|
-
/ static-width / noscroll modifier classes) is included here via
|
|
19
|
-
the `@mixin dialog` SCSS so it is shared across all three
|
|
20
|
-
consumer modals.
|
|
21
|
-
-->
|
|
22
|
-
|
|
23
|
-
<script lang="ts">import {
|
|
24
|
-
import {
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
BaseModal.svelte - internal primitive that owns the `<dialog>`
|
|
5
|
+
open/close + Escape/backdrop dismissal + autofocus plumbing
|
|
6
|
+
shared between `Modal`, `DesktopModal`, and `TouchModal`. See
|
|
7
|
+
`BaseModalProps.ts` for the contract.
|
|
8
|
+
|
|
9
|
+
Cosmetic-free for inner content by design: each consumer renders
|
|
10
|
+
its own header / main / footer / button-row markup as
|
|
11
|
+
`{@render children()}`. The consumer also keeps its own
|
|
12
|
+
footer-text rules, scanner shortcut wiring, colourSet button
|
|
13
|
+
styling, etc. - those concerns intentionally do not live in
|
|
14
|
+
BaseModal.
|
|
15
|
+
|
|
16
|
+
The shared dialog SHELL styling (border, backdrop, zoom-in
|
|
17
|
+
animation, the `--dialog-*` size variables and the static-height
|
|
18
|
+
/ static-width / noscroll modifier classes) is included here via
|
|
19
|
+
the `@mixin dialog` SCSS so it is shared across all three
|
|
20
|
+
consumer modals.
|
|
21
|
+
-->
|
|
22
|
+
|
|
23
|
+
<script lang="ts">import { TooltipContext } from "../../Contexts/TooltipContext.svelte.js";
|
|
24
|
+
import { devCatch } from "../../Helpers/Helpers.svelte.js";
|
|
25
|
+
import { getContext, onMount, tick } from "svelte";
|
|
26
|
+
import Tooltip from "../Contexts/Tooltip.svelte";
|
|
25
27
|
let { value = $bindable(true), children, onclose, mandatory = false, autofocusSelector, ariaLabelledby, ariaDescribedby, closeOnEscape = true, closeOnBackdropClick = true, dialogClass, dialogStyle } = $props();
|
|
26
28
|
let dialog = $state();
|
|
29
|
+
// A dialog opened with showModal() paints in the browser top layer,
|
|
30
|
+
// above every z-index on the page, including the app-root Tooltip host
|
|
31
|
+
// mounted by ContextWrapper. Hovering a Button / Action inside a modal
|
|
32
|
+
// therefore shows its tooltip BEHIND the modal. Rendering a second
|
|
33
|
+
// Tooltip host inside the dialog puts one in the top layer too; both
|
|
34
|
+
// hosts read the same app-level context, so whichever is visible paints
|
|
35
|
+
// the active tooltip. Skipped when no context is mounted (tests,
|
|
36
|
+
// context-free harnesses).
|
|
37
|
+
const tooltipContext = getContext(TooltipContext.identifier);
|
|
27
38
|
function tryFocusSelector() {
|
|
28
39
|
if (dialog && autofocusSelector != null && autofocusSelector.length > 0) {
|
|
29
40
|
const target = dialog.querySelector(autofocusSelector);
|
|
@@ -80,22 +91,25 @@ function handleBackdropClick(e) {
|
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
93
|
}
|
|
83
|
-
</script>
|
|
84
|
-
|
|
85
|
-
<dialog
|
|
86
|
-
bind:this={dialog}
|
|
87
|
-
class={dialogClass}
|
|
88
|
-
style={dialogStyle}
|
|
89
|
-
aria-modal="true"
|
|
90
|
-
aria-labelledby={ariaLabelledby}
|
|
91
|
-
aria-describedby={ariaDescribedby}
|
|
92
|
-
onclose={handleNativeClose}
|
|
93
|
-
onkeydown={handleKeydown}
|
|
94
|
-
onclick={handleBackdropClick}
|
|
95
|
-
>
|
|
96
|
-
{@render children()}
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<dialog
|
|
97
|
+
bind:this={dialog}
|
|
98
|
+
class={dialogClass}
|
|
99
|
+
style={dialogStyle}
|
|
100
|
+
aria-modal="true"
|
|
101
|
+
aria-labelledby={ariaLabelledby}
|
|
102
|
+
aria-describedby={ariaDescribedby}
|
|
103
|
+
onclose={handleNativeClose}
|
|
104
|
+
onkeydown={handleKeydown}
|
|
105
|
+
onclick={handleBackdropClick}
|
|
106
|
+
>
|
|
107
|
+
{@render children()}
|
|
108
|
+
{#if tooltipContext}
|
|
109
|
+
<Tooltip />
|
|
110
|
+
{/if}
|
|
111
|
+
</dialog>
|
|
112
|
+
|
|
99
113
|
<style>dialog {
|
|
100
114
|
overflow: clip;
|
|
101
115
|
background-color: #e9ecf1;
|
|
@@ -149,4 +163,4 @@ dialog .modal-main.noscroll {
|
|
|
149
163
|
}
|
|
150
164
|
dialog .footer {
|
|
151
165
|
flex-shrink: 0;
|
|
152
|
-
}</style>
|
|
166
|
+
}</style>
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">import { generateWeakId } from "../Helpers/Helpers.svelte.js";
|
|
4
|
+
let { values = $bindable([]), onchange, labelTop, optional = false, placeholder, disabled = false, invalid = false, id = generateWeakId() } = $props();
|
|
5
|
+
let draft = $state("");
|
|
6
|
+
function commit(next) {
|
|
7
|
+
values = next;
|
|
8
|
+
onchange?.(next);
|
|
9
|
+
}
|
|
10
|
+
function commitDraft() {
|
|
11
|
+
const value = draft.trim();
|
|
12
|
+
if (value === "") {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (!values.includes(value)) {
|
|
16
|
+
commit([...values, value]);
|
|
17
|
+
}
|
|
18
|
+
draft = "";
|
|
19
|
+
}
|
|
20
|
+
function remove(value) {
|
|
21
|
+
commit(values.filter((v) => v !== value));
|
|
22
|
+
}
|
|
23
|
+
function onkeydown(e) {
|
|
24
|
+
if (e.key === "Enter" || e.key === ",") {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
commitDraft();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (e.key === "Backspace" && draft === "" && values.length > 0) {
|
|
30
|
+
commit(values.slice(0, -1));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<label
|
|
36
|
+
class="chips-wrapper"
|
|
37
|
+
for={id}
|
|
38
|
+
>
|
|
39
|
+
{#if labelTop}
|
|
40
|
+
<span
|
|
41
|
+
class="top-label"
|
|
42
|
+
class:invalid
|
|
43
|
+
>{labelTop}{#if optional}<i> - optional</i>{/if}</span
|
|
44
|
+
>
|
|
45
|
+
<span
|
|
46
|
+
aria-hidden="true"
|
|
47
|
+
class="border"
|
|
48
|
+
class:invalid
|
|
49
|
+
>{labelTop}{#if optional}<i> - optional</i>{/if}</span
|
|
50
|
+
>
|
|
51
|
+
{/if}
|
|
52
|
+
<div
|
|
53
|
+
class="box"
|
|
54
|
+
class:invalid
|
|
55
|
+
class:disabled
|
|
56
|
+
>
|
|
57
|
+
{#each values as value (value)}
|
|
58
|
+
<span class="chip">
|
|
59
|
+
{value}
|
|
60
|
+
{#if !disabled}
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
class="remove"
|
|
64
|
+
aria-label="Remove {value}"
|
|
65
|
+
onclick={() => {
|
|
66
|
+
remove(value);
|
|
67
|
+
}}>×</button
|
|
68
|
+
>
|
|
69
|
+
{/if}
|
|
70
|
+
</span>
|
|
71
|
+
{/each}
|
|
72
|
+
<input
|
|
73
|
+
{id}
|
|
74
|
+
type="text"
|
|
75
|
+
{placeholder}
|
|
76
|
+
{disabled}
|
|
77
|
+
bind:value={draft}
|
|
78
|
+
{onkeydown}
|
|
79
|
+
onblur={commitDraft}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
</label>
|
|
83
|
+
|
|
84
|
+
<style>.chips-wrapper {
|
|
85
|
+
position: relative;
|
|
86
|
+
display: flex;
|
|
87
|
+
flex-flow: column nowrap;
|
|
88
|
+
min-width: 16rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
span.top-label {
|
|
92
|
+
position: absolute;
|
|
93
|
+
left: 0.5rem;
|
|
94
|
+
font-size: 0.875rem;
|
|
95
|
+
user-select: none;
|
|
96
|
+
padding-left: 0.5rem;
|
|
97
|
+
padding-right: 0.5rem;
|
|
98
|
+
line-height: 1rem;
|
|
99
|
+
padding: 0rem 0.5rem;
|
|
100
|
+
top: calc(-0.875rem / 2 - 2px);
|
|
101
|
+
z-index: 2;
|
|
102
|
+
color: #647d8e;
|
|
103
|
+
white-space: nowrap;
|
|
104
|
+
}
|
|
105
|
+
span.top-label.invalid {
|
|
106
|
+
color: #aa1414;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
span.border {
|
|
110
|
+
position: absolute;
|
|
111
|
+
left: 0.5rem;
|
|
112
|
+
font-size: 0.875rem;
|
|
113
|
+
user-select: none;
|
|
114
|
+
padding-left: 0.5rem;
|
|
115
|
+
padding-right: 0.5rem;
|
|
116
|
+
line-height: 1rem;
|
|
117
|
+
z-index: 1;
|
|
118
|
+
white-space: nowrap;
|
|
119
|
+
top: 0px;
|
|
120
|
+
line-height: 0px;
|
|
121
|
+
color: transparent;
|
|
122
|
+
border-bottom: solid 1px #ffffff;
|
|
123
|
+
}
|
|
124
|
+
span.border.invalid {
|
|
125
|
+
border-bottom-color: #f9e9e9;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.box {
|
|
129
|
+
display: flex;
|
|
130
|
+
flex-flow: row wrap;
|
|
131
|
+
align-items: center;
|
|
132
|
+
gap: 0.375rem;
|
|
133
|
+
border: solid 1px #cbd4da;
|
|
134
|
+
border-radius: 4px;
|
|
135
|
+
background: #ffffff;
|
|
136
|
+
padding: 0.45rem 0.75rem;
|
|
137
|
+
min-height: 2.65rem;
|
|
138
|
+
transition: border-color ease-out 0.15s 0s, background-color ease-out 0.15s 0s, color ease-out 0.15s 0s, fill ease-out 0.15s 0s;
|
|
139
|
+
}
|
|
140
|
+
.box:focus-within {
|
|
141
|
+
outline: solid 1px #259fc7;
|
|
142
|
+
outline-offset: -1px;
|
|
143
|
+
}
|
|
144
|
+
.box.invalid {
|
|
145
|
+
border-color: #aa1414;
|
|
146
|
+
background-color: #f9e9e9;
|
|
147
|
+
}
|
|
148
|
+
.box.disabled {
|
|
149
|
+
background-color: #f4f7f9;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.chip {
|
|
153
|
+
display: inline-flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
gap: 0.3rem;
|
|
156
|
+
border: solid 1px #cbd4da;
|
|
157
|
+
border-radius: 999px;
|
|
158
|
+
padding: 0.1rem 0.3rem 0.1rem 0.6rem;
|
|
159
|
+
font-size: 0.8125rem;
|
|
160
|
+
color: #3a4952;
|
|
161
|
+
background: #ffffff;
|
|
162
|
+
white-space: nowrap;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.remove {
|
|
166
|
+
border: 0;
|
|
167
|
+
background: transparent;
|
|
168
|
+
color: #647d8e;
|
|
169
|
+
font-size: 0.85rem;
|
|
170
|
+
line-height: 1;
|
|
171
|
+
padding: 0.1rem 0.25rem;
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
border-radius: 999px;
|
|
174
|
+
transition: border-color ease-out 0.15s 0s, background-color ease-out 0.15s 0s, color ease-out 0.15s 0s, fill ease-out 0.15s 0s;
|
|
175
|
+
}
|
|
176
|
+
.remove:hover, .remove:focus-visible {
|
|
177
|
+
color: #3a4952;
|
|
178
|
+
background: #e9ecf1;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
input {
|
|
182
|
+
flex: 1 1 6rem;
|
|
183
|
+
min-width: 6rem;
|
|
184
|
+
border: 0;
|
|
185
|
+
outline: none;
|
|
186
|
+
background: transparent;
|
|
187
|
+
font-size: 0.875rem;
|
|
188
|
+
font-weight: 400;
|
|
189
|
+
color: #3a4952;
|
|
190
|
+
padding: 0.2rem 0;
|
|
191
|
+
}
|
|
192
|
+
input::placeholder {
|
|
193
|
+
color: #647d8e;
|
|
194
|
+
}</style>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Props for the {@link ChipsInput} component: a free-text multi-value input.
|
|
3
|
+
* Committed values render as removable chips inside the input frame; typing
|
|
4
|
+
* adds a value on Enter or comma, and Backspace on an empty draft removes the
|
|
5
|
+
* last chip. Use it wherever a field holds an ordered list of short free-text
|
|
6
|
+
* values that have no lookup source (codes, tags, reference lists); reach for
|
|
7
|
+
* `MultiSelect` instead when the values come from a known option set.
|
|
8
|
+
*/
|
|
9
|
+
export interface ChipsInputProps {
|
|
10
|
+
/**
|
|
11
|
+
* The committed values, in order. Bindable; the component never mutates
|
|
12
|
+
* the array in place - every change (add, remove) assigns a fresh array
|
|
13
|
+
* and then fires {@link ChipsInputProps.onchange}.
|
|
14
|
+
*/
|
|
15
|
+
values: Array<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Called with the fresh values array after every add or remove. Optional
|
|
18
|
+
* when the consumer relies on `bind:values` alone.
|
|
19
|
+
*/
|
|
20
|
+
onchange?: (values: Array<string>) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Label rendered floating over the top border, matching the TextInput
|
|
23
|
+
* label treatment.
|
|
24
|
+
*/
|
|
25
|
+
labelTop?: string;
|
|
26
|
+
/** Render the "- optional" suffix next to `labelTop`. */
|
|
27
|
+
optional?: boolean;
|
|
28
|
+
/** Placeholder shown in the draft input while it is empty. */
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
/**
|
|
31
|
+
* When true the control is non-interactive: the draft input is disabled
|
|
32
|
+
* and chips lose their remove buttons.
|
|
33
|
+
*/
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
/** Force the invalid (error palette) visual state. */
|
|
36
|
+
invalid?: boolean;
|
|
37
|
+
/** DOM id placed on the inner draft `<input>`. */
|
|
38
|
+
id?: string;
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
<svelte:options runes />
|
|
2
|
-
|
|
3
|
-
<script
|
|
4
|
-
lang="ts"
|
|
5
|
-
generics="F extends string | number, O extends string | number, V"
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
<script
|
|
4
|
+
lang="ts"
|
|
5
|
+
generics="F extends string | number, O extends string | number, V"
|
|
6
6
|
>import SingleSelect from "../SingleSelect.svelte";
|
|
7
7
|
import Button from "../Button.svelte";
|
|
8
8
|
import { makeGroup, makeLeaf } from "./ConditionTree.js";
|
|
@@ -42,101 +42,101 @@ function setValue(leaf, value) {
|
|
|
42
42
|
leaf.value = value;
|
|
43
43
|
onChange(root);
|
|
44
44
|
}
|
|
45
|
-
</script>
|
|
46
|
-
|
|
47
|
-
<div
|
|
48
|
-
class="group"
|
|
49
|
-
class:nested={depth > 0}
|
|
50
|
-
>
|
|
51
|
-
<div class="combinator">
|
|
52
|
-
<Button
|
|
53
|
-
label={andLabel}
|
|
54
|
-
toggled={root.combinator === 'and'}
|
|
55
|
-
onclick={() => setCombinator('and')}>{andLabel}</Button
|
|
56
|
-
>
|
|
57
|
-
<Button
|
|
58
|
-
label={orLabel}
|
|
59
|
-
toggled={root.combinator === 'or'}
|
|
60
|
-
onclick={() => setCombinator('or')}>{orLabel}</Button
|
|
61
|
-
>
|
|
62
|
-
</div>
|
|
63
|
-
|
|
64
|
-
<div class="children">
|
|
65
|
-
{#each root.children as child (child.id)}
|
|
66
|
-
{#if child.kind === 'leaf'}
|
|
67
|
-
<div class="leaf">
|
|
68
|
-
<SingleSelect
|
|
69
|
-
label="Field"
|
|
70
|
-
options={[...fields]}
|
|
71
|
-
labelProp="label"
|
|
72
|
-
valueProp="value"
|
|
73
|
-
valueAsObject={false}
|
|
74
|
-
clearable
|
|
75
|
-
value={child.field}
|
|
76
|
-
onchange={(value) => setField(child, value)}
|
|
77
|
-
/>
|
|
78
|
-
<SingleSelect
|
|
79
|
-
label="Operator"
|
|
80
|
-
options={[...operatorsFor(child.field)]}
|
|
81
|
-
labelProp="label"
|
|
82
|
-
valueProp="value"
|
|
83
|
-
valueAsObject={false}
|
|
84
|
-
clearable
|
|
85
|
-
value={child.operator}
|
|
86
|
-
onchange={(value) => setOperator(child, value)}
|
|
87
|
-
/>
|
|
88
|
-
<div class="value-editor">
|
|
89
|
-
{@render valueEditor(child, (value) => setValue(child, value))}
|
|
90
|
-
</div>
|
|
91
|
-
<Button
|
|
92
|
-
label="Remove condition"
|
|
93
|
-
iconLeft={{ name: 'circle-cross' }}
|
|
94
|
-
onclick={() => removeChild(child)}
|
|
95
|
-
/>
|
|
96
|
-
</div>
|
|
97
|
-
{:else}
|
|
98
|
-
<div class="subgroup">
|
|
99
|
-
<Self
|
|
100
|
-
root={child}
|
|
101
|
-
{onChange}
|
|
102
|
-
{fields}
|
|
103
|
-
{operatorsFor}
|
|
104
|
-
{valueEditor}
|
|
105
|
-
{defaultValueFor}
|
|
106
|
-
{allowGroups}
|
|
107
|
-
{maxDepth}
|
|
108
|
-
{addConditionLabel}
|
|
109
|
-
{addGroupLabel}
|
|
110
|
-
{andLabel}
|
|
111
|
-
{orLabel}
|
|
112
|
-
depth={depth + 1}
|
|
113
|
-
/>
|
|
114
|
-
<Button
|
|
115
|
-
label="Remove group"
|
|
116
|
-
iconLeft={{ name: 'circle-cross' }}
|
|
117
|
-
onclick={() => removeChild(child)}
|
|
118
|
-
/>
|
|
119
|
-
</div>
|
|
120
|
-
{/if}
|
|
121
|
-
{/each}
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
<div class="actions">
|
|
125
|
-
<Button
|
|
126
|
-
label={addConditionLabel}
|
|
127
|
-
iconLeft={{ name: 'circle-plus' }}
|
|
128
|
-
onclick={addLeaf}>{addConditionLabel}</Button
|
|
129
|
-
>
|
|
130
|
-
{#if allowGroups && depth < maxDepth}
|
|
131
|
-
<Button
|
|
132
|
-
label={addGroupLabel}
|
|
133
|
-
iconLeft={{ name: 'circle-plus' }}
|
|
134
|
-
onclick={addGroup}>{addGroupLabel}</Button
|
|
135
|
-
>
|
|
136
|
-
{/if}
|
|
137
|
-
</div>
|
|
138
|
-
</div>
|
|
139
|
-
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<div
|
|
48
|
+
class="group"
|
|
49
|
+
class:nested={depth > 0}
|
|
50
|
+
>
|
|
51
|
+
<div class="combinator">
|
|
52
|
+
<Button
|
|
53
|
+
label={andLabel}
|
|
54
|
+
toggled={root.combinator === 'and'}
|
|
55
|
+
onclick={() => setCombinator('and')}>{andLabel}</Button
|
|
56
|
+
>
|
|
57
|
+
<Button
|
|
58
|
+
label={orLabel}
|
|
59
|
+
toggled={root.combinator === 'or'}
|
|
60
|
+
onclick={() => setCombinator('or')}>{orLabel}</Button
|
|
61
|
+
>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="children">
|
|
65
|
+
{#each root.children as child (child.id)}
|
|
66
|
+
{#if child.kind === 'leaf'}
|
|
67
|
+
<div class="leaf">
|
|
68
|
+
<SingleSelect
|
|
69
|
+
label="Field"
|
|
70
|
+
options={[...fields]}
|
|
71
|
+
labelProp="label"
|
|
72
|
+
valueProp="value"
|
|
73
|
+
valueAsObject={false}
|
|
74
|
+
clearable
|
|
75
|
+
value={child.field}
|
|
76
|
+
onchange={(value) => setField(child, value)}
|
|
77
|
+
/>
|
|
78
|
+
<SingleSelect
|
|
79
|
+
label="Operator"
|
|
80
|
+
options={[...operatorsFor(child.field)]}
|
|
81
|
+
labelProp="label"
|
|
82
|
+
valueProp="value"
|
|
83
|
+
valueAsObject={false}
|
|
84
|
+
clearable
|
|
85
|
+
value={child.operator}
|
|
86
|
+
onchange={(value) => setOperator(child, value)}
|
|
87
|
+
/>
|
|
88
|
+
<div class="value-editor">
|
|
89
|
+
{@render valueEditor(child, (value) => setValue(child, value))}
|
|
90
|
+
</div>
|
|
91
|
+
<Button
|
|
92
|
+
label="Remove condition"
|
|
93
|
+
iconLeft={{ name: 'circle-cross' }}
|
|
94
|
+
onclick={() => removeChild(child)}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
{:else}
|
|
98
|
+
<div class="subgroup">
|
|
99
|
+
<Self
|
|
100
|
+
root={child}
|
|
101
|
+
{onChange}
|
|
102
|
+
{fields}
|
|
103
|
+
{operatorsFor}
|
|
104
|
+
{valueEditor}
|
|
105
|
+
{defaultValueFor}
|
|
106
|
+
{allowGroups}
|
|
107
|
+
{maxDepth}
|
|
108
|
+
{addConditionLabel}
|
|
109
|
+
{addGroupLabel}
|
|
110
|
+
{andLabel}
|
|
111
|
+
{orLabel}
|
|
112
|
+
depth={depth + 1}
|
|
113
|
+
/>
|
|
114
|
+
<Button
|
|
115
|
+
label="Remove group"
|
|
116
|
+
iconLeft={{ name: 'circle-cross' }}
|
|
117
|
+
onclick={() => removeChild(child)}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
{/if}
|
|
121
|
+
{/each}
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div class="actions">
|
|
125
|
+
<Button
|
|
126
|
+
label={addConditionLabel}
|
|
127
|
+
iconLeft={{ name: 'circle-plus' }}
|
|
128
|
+
onclick={addLeaf}>{addConditionLabel}</Button
|
|
129
|
+
>
|
|
130
|
+
{#if allowGroups && depth < maxDepth}
|
|
131
|
+
<Button
|
|
132
|
+
label={addGroupLabel}
|
|
133
|
+
iconLeft={{ name: 'circle-plus' }}
|
|
134
|
+
onclick={addGroup}>{addGroupLabel}</Button
|
|
135
|
+
>
|
|
136
|
+
{/if}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
140
|
<style>.group {
|
|
141
141
|
display: flex;
|
|
142
142
|
flex-flow: column nowrap;
|
|
@@ -184,4 +184,4 @@ function setValue(leaf, value) {
|
|
|
184
184
|
display: flex;
|
|
185
185
|
flex-flow: row nowrap;
|
|
186
186
|
gap: 0.5rem;
|
|
187
|
-
}</style>
|
|
187
|
+
}</style>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
<svelte:options runes />
|
|
2
|
-
|
|
3
|
-
<script
|
|
4
|
-
lang="ts"
|
|
5
|
-
generics="E extends CalendarEvent"
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
<script
|
|
4
|
+
lang="ts"
|
|
5
|
+
generics="E extends CalendarEvent"
|
|
6
6
|
>import { Colour } from "../../scss/colours.js";
|
|
7
7
|
import { Size } from "../../Types/Internal/Size.js";
|
|
8
8
|
import Button from "../Button.svelte";
|
|
@@ -518,241 +518,241 @@ $effect(() => {
|
|
|
518
518
|
calendarBody.scrollTop += delta;
|
|
519
519
|
hasCentred = true;
|
|
520
520
|
});
|
|
521
|
-
</script>
|
|
522
|
-
|
|
523
|
-
<!-- A drag can end anywhere on the page; listen on the window so the drag state
|
|
524
|
-
is always finalised and never left "stuck" for the next drag. -->
|
|
525
|
-
<svelte:window onmouseup={onSlotMouseUp} />
|
|
526
|
-
|
|
527
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
528
|
-
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
529
|
-
<div
|
|
530
|
-
class="resource-calendar"
|
|
531
|
-
class:is-moving-event={isDraggingEvent}
|
|
532
|
-
role="application"
|
|
533
|
-
>
|
|
534
|
-
<!-- Header: navigation + view toggle -->
|
|
535
|
-
<div class="calendar-header">
|
|
536
|
-
<div class="header-left">
|
|
537
|
-
<Button
|
|
538
|
-
label="Today"
|
|
539
|
-
size={Size.Small}
|
|
540
|
-
onclick={goToToday}
|
|
541
|
-
>
|
|
542
|
-
Today
|
|
543
|
-
</Button>
|
|
544
|
-
<div class="nav-arrows">
|
|
545
|
-
<button
|
|
546
|
-
class="nav-btn"
|
|
547
|
-
onclick={() => {
|
|
548
|
-
navigateDate(-1);
|
|
549
|
-
}}
|
|
550
|
-
aria-label="Previous"
|
|
551
|
-
>
|
|
552
|
-
<Icon
|
|
553
|
-
name="chevron-left"
|
|
554
|
-
size={Size.Small}
|
|
555
|
-
colour={Colour['$ui-blue-14']}
|
|
556
|
-
/>
|
|
557
|
-
</button>
|
|
558
|
-
<button
|
|
559
|
-
class="nav-btn"
|
|
560
|
-
onclick={() => {
|
|
561
|
-
navigateDate(1);
|
|
562
|
-
}}
|
|
563
|
-
aria-label="Next"
|
|
564
|
-
>
|
|
565
|
-
<Icon
|
|
566
|
-
name="chevron-right"
|
|
567
|
-
size={Size.Small}
|
|
568
|
-
colour={Colour['$ui-blue-14']}
|
|
569
|
-
/>
|
|
570
|
-
</button>
|
|
571
|
-
</div>
|
|
572
|
-
<h2 class="current-date-label">{formatDate(selected)}</h2>
|
|
573
|
-
</div>
|
|
574
|
-
<div class="header-right">
|
|
575
|
-
{#if headerExtra}
|
|
576
|
-
{@render headerExtra()}
|
|
577
|
-
{/if}
|
|
578
|
-
<div class="view-toggle">
|
|
579
|
-
{#each viewModes as { mode, label } (mode)}
|
|
580
|
-
<button
|
|
581
|
-
class="view-btn"
|
|
582
|
-
class:active={viewMode === mode}
|
|
583
|
-
onclick={() => {
|
|
584
|
-
onviewmodechange(mode);
|
|
585
|
-
}}
|
|
586
|
-
>
|
|
587
|
-
{label}
|
|
588
|
-
</button>
|
|
589
|
-
{/each}
|
|
590
|
-
</div>
|
|
591
|
-
</div>
|
|
592
|
-
</div>
|
|
593
|
-
|
|
594
|
-
<!-- Week day tabs rendered OUTSIDE the scrollable body so they stay frozen -->
|
|
595
|
-
{#if viewMode === CalendarViewMode.Week}
|
|
596
|
-
<div class="week-day-tabs">
|
|
597
|
-
{#each weekDays as day (day.getTime())}
|
|
598
|
-
<button
|
|
599
|
-
class="week-day-tab"
|
|
600
|
-
class:active={sameDay(day, selected)}
|
|
601
|
-
class:today={sameDay(day, today)}
|
|
602
|
-
onclick={() => {
|
|
603
|
-
ondatechange(day);
|
|
604
|
-
}}
|
|
605
|
-
>
|
|
606
|
-
<span class="day-name">{dayNames[day.getDay()].slice(0, 3)}</span>
|
|
607
|
-
<span class="day-num">{day.getDate()}</span>
|
|
608
|
-
</button>
|
|
609
|
-
{/each}
|
|
610
|
-
</div>
|
|
611
|
-
{/if}
|
|
612
|
-
|
|
613
|
-
<!-- Calendar body -->
|
|
614
|
-
<div
|
|
615
|
-
class="calendar-body"
|
|
616
|
-
bind:this={calendarBody}
|
|
617
|
-
>
|
|
618
|
-
{#if isLoading}
|
|
619
|
-
<LoadingOverlay />
|
|
620
|
-
{/if}
|
|
621
|
-
|
|
622
|
-
{#if viewMode === CalendarViewMode.Day}
|
|
623
|
-
<!-- DAY VIEW -->
|
|
624
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
625
|
-
<div
|
|
626
|
-
class="day-view"
|
|
627
|
-
onmouseleave={() => {
|
|
628
|
-
hoveredSlotIndex = null;
|
|
629
|
-
}}
|
|
630
|
-
>
|
|
631
|
-
<!-- eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -->
|
|
632
|
-
{@render resourceGrid(selected)}
|
|
633
|
-
</div>
|
|
634
|
-
{:else if viewMode === CalendarViewMode.Week}
|
|
635
|
-
<!-- WEEK VIEW: day grid only, tabs are above calendar-body -->
|
|
636
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
637
|
-
<div
|
|
638
|
-
class="day-view"
|
|
639
|
-
onmouseleave={() => {
|
|
640
|
-
hoveredSlotIndex = null;
|
|
641
|
-
}}
|
|
642
|
-
>
|
|
643
|
-
<!-- eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -->
|
|
644
|
-
{@render resourceGrid(selected)}
|
|
645
|
-
</div>
|
|
646
|
-
{:else}
|
|
647
|
-
<!-- MONTH VIEW -->
|
|
648
|
-
<div class="month-view">
|
|
649
|
-
<div class="month-header-row">
|
|
650
|
-
{#each ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as dayLabel (dayLabel)}
|
|
651
|
-
<div class="month-day-header">{dayLabel}</div>
|
|
652
|
-
{/each}
|
|
653
|
-
</div>
|
|
654
|
-
<div class="month-grid">
|
|
655
|
-
{#each monthDays as { date: dayDate, isCurrentMonth } (dayDate.getTime())}
|
|
656
|
-
<button
|
|
657
|
-
class="month-day-cell"
|
|
658
|
-
class:other-month={!isCurrentMonth}
|
|
659
|
-
class:today={sameDay(dayDate, today)}
|
|
660
|
-
class:selected={sameDay(dayDate, selected)}
|
|
661
|
-
onclick={() => {
|
|
662
|
-
ondatechange(dayDate);
|
|
663
|
-
onviewmodechange(CalendarViewMode.Day);
|
|
664
|
-
}}
|
|
665
|
-
>
|
|
666
|
-
<span class="day-number">{dayDate.getDate()}</span>
|
|
667
|
-
<div class="day-badges">
|
|
668
|
-
{#each countsAt(dayDate) as { resourceId, resourceLabel, count } (resourceId)}
|
|
669
|
-
<span class="resource-badge">
|
|
670
|
-
{resourceLabel}: {count}
|
|
671
|
-
</span>
|
|
672
|
-
{/each}
|
|
673
|
-
</div>
|
|
674
|
-
</button>
|
|
675
|
-
{/each}
|
|
676
|
-
</div>
|
|
677
|
-
</div>
|
|
678
|
-
{/if}
|
|
679
|
-
</div>
|
|
680
|
-
</div>
|
|
681
|
-
|
|
682
|
-
{#snippet resourceGrid(day: Date)}
|
|
683
|
-
<div class="grid-header">
|
|
684
|
-
<div class="time-header">Time</div>
|
|
685
|
-
{#each resources as resource (resource.id)}
|
|
686
|
-
<div class="resource-header">{resource.label}</div>
|
|
687
|
-
{/each}
|
|
688
|
-
</div>
|
|
689
|
-
<div class="grid-body">
|
|
690
|
-
<div class="time-column">
|
|
691
|
-
{#each timeSlots as slot, slotIdx (slot.hour * 60 + slot.minute)}
|
|
692
|
-
<div
|
|
693
|
-
class="time-label"
|
|
694
|
-
class:hour-start={slot.minute === 0}
|
|
695
|
-
class:row-hover={hoveredSlotIndex === slotIdx}
|
|
696
|
-
data-centre-row={slot.hour === centreRowHour && slot.minute === 0 ? '' : undefined}
|
|
697
|
-
>
|
|
698
|
-
{#if slot.minute === 0}
|
|
699
|
-
<span>{slot.label}</span>
|
|
700
|
-
{/if}
|
|
701
|
-
</div>
|
|
702
|
-
{/each}
|
|
703
|
-
</div>
|
|
704
|
-
{#each resources as resource (resource.id)}
|
|
705
|
-
<div class="resource-column">
|
|
706
|
-
<!-- Slot cells for drag interaction (rendered first, below events) -->
|
|
707
|
-
{#each timeSlots as slot, slotIdx (slot.hour * 60 + slot.minute)}
|
|
708
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
709
|
-
<div
|
|
710
|
-
class="slot-cell"
|
|
711
|
-
class:hour-start={slot.minute === 0}
|
|
712
|
-
class:drag-selected={isSlotInDragRange(resource.id, slotIdx)}
|
|
713
|
-
class:move-target={isSlotInMoveTarget(resource.id, slotIdx)}
|
|
714
|
-
class:row-hover={hoveredSlotIndex === slotIdx}
|
|
715
|
-
class:unavailable={isSlotUnavailable(resource.id, slotIdx)}
|
|
716
|
-
onmousedown={(e) => {
|
|
717
|
-
onSlotMouseDown(resource.id, slotIdx, e);
|
|
718
|
-
}}
|
|
719
|
-
onmouseenter={(e) => {
|
|
720
|
-
hoveredSlotIndex = slotIdx;
|
|
721
|
-
onSlotMouseEnter(resource.id, slotIdx, e);
|
|
722
|
-
}}
|
|
723
|
-
ondblclick={() => {
|
|
724
|
-
onSlotDoubleClick(resource.id, slotIdx);
|
|
725
|
-
}}
|
|
726
|
-
></div>
|
|
727
|
-
{/each}
|
|
728
|
-
<!-- Unavailable shading: one continuous block per contiguous run -->
|
|
729
|
-
{#each unavailableRunsAt(resource.id) as run (run.topPercent)}
|
|
730
|
-
<div
|
|
731
|
-
class="unavailable-overlay"
|
|
732
|
-
style="top: {run.topPercent}%; height: {run.heightPercent}%;"
|
|
733
|
-
></div>
|
|
734
|
-
{/each}
|
|
735
|
-
<!-- Event blocks positioned absolutely (rendered after, above slots).
|
|
736
|
-
The consumer renders the card body via the `event` snippet. -->
|
|
737
|
-
{#each eventsAt(resource.id, day) as ev (ev.id)}
|
|
738
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
739
|
-
<div
|
|
740
|
-
class="event-block"
|
|
741
|
-
class:dragging={isDraggingEvent && draggedEvent?.id === ev.id}
|
|
742
|
-
class:movable={!!oneventmove}
|
|
743
|
-
style="top: {ev.topPercent}%; height: {ev.heightPercent}%;"
|
|
744
|
-
onmousedown={(e) => {
|
|
745
|
-
onEventMouseDown(ev, e);
|
|
746
|
-
}}
|
|
747
|
-
>
|
|
748
|
-
{@render eventContent(ev)}
|
|
749
|
-
</div>
|
|
750
|
-
{/each}
|
|
751
|
-
</div>
|
|
752
|
-
{/each}
|
|
753
|
-
</div>
|
|
754
|
-
{/snippet}
|
|
755
|
-
|
|
521
|
+
</script>
|
|
522
|
+
|
|
523
|
+
<!-- A drag can end anywhere on the page; listen on the window so the drag state
|
|
524
|
+
is always finalised and never left "stuck" for the next drag. -->
|
|
525
|
+
<svelte:window onmouseup={onSlotMouseUp} />
|
|
526
|
+
|
|
527
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
528
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
529
|
+
<div
|
|
530
|
+
class="resource-calendar"
|
|
531
|
+
class:is-moving-event={isDraggingEvent}
|
|
532
|
+
role="application"
|
|
533
|
+
>
|
|
534
|
+
<!-- Header: navigation + view toggle -->
|
|
535
|
+
<div class="calendar-header">
|
|
536
|
+
<div class="header-left">
|
|
537
|
+
<Button
|
|
538
|
+
label="Today"
|
|
539
|
+
size={Size.Small}
|
|
540
|
+
onclick={goToToday}
|
|
541
|
+
>
|
|
542
|
+
Today
|
|
543
|
+
</Button>
|
|
544
|
+
<div class="nav-arrows">
|
|
545
|
+
<button
|
|
546
|
+
class="nav-btn"
|
|
547
|
+
onclick={() => {
|
|
548
|
+
navigateDate(-1);
|
|
549
|
+
}}
|
|
550
|
+
aria-label="Previous"
|
|
551
|
+
>
|
|
552
|
+
<Icon
|
|
553
|
+
name="chevron-left"
|
|
554
|
+
size={Size.Small}
|
|
555
|
+
colour={Colour['$ui-blue-14']}
|
|
556
|
+
/>
|
|
557
|
+
</button>
|
|
558
|
+
<button
|
|
559
|
+
class="nav-btn"
|
|
560
|
+
onclick={() => {
|
|
561
|
+
navigateDate(1);
|
|
562
|
+
}}
|
|
563
|
+
aria-label="Next"
|
|
564
|
+
>
|
|
565
|
+
<Icon
|
|
566
|
+
name="chevron-right"
|
|
567
|
+
size={Size.Small}
|
|
568
|
+
colour={Colour['$ui-blue-14']}
|
|
569
|
+
/>
|
|
570
|
+
</button>
|
|
571
|
+
</div>
|
|
572
|
+
<h2 class="current-date-label">{formatDate(selected)}</h2>
|
|
573
|
+
</div>
|
|
574
|
+
<div class="header-right">
|
|
575
|
+
{#if headerExtra}
|
|
576
|
+
{@render headerExtra()}
|
|
577
|
+
{/if}
|
|
578
|
+
<div class="view-toggle">
|
|
579
|
+
{#each viewModes as { mode, label } (mode)}
|
|
580
|
+
<button
|
|
581
|
+
class="view-btn"
|
|
582
|
+
class:active={viewMode === mode}
|
|
583
|
+
onclick={() => {
|
|
584
|
+
onviewmodechange(mode);
|
|
585
|
+
}}
|
|
586
|
+
>
|
|
587
|
+
{label}
|
|
588
|
+
</button>
|
|
589
|
+
{/each}
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
</div>
|
|
593
|
+
|
|
594
|
+
<!-- Week day tabs rendered OUTSIDE the scrollable body so they stay frozen -->
|
|
595
|
+
{#if viewMode === CalendarViewMode.Week}
|
|
596
|
+
<div class="week-day-tabs">
|
|
597
|
+
{#each weekDays as day (day.getTime())}
|
|
598
|
+
<button
|
|
599
|
+
class="week-day-tab"
|
|
600
|
+
class:active={sameDay(day, selected)}
|
|
601
|
+
class:today={sameDay(day, today)}
|
|
602
|
+
onclick={() => {
|
|
603
|
+
ondatechange(day);
|
|
604
|
+
}}
|
|
605
|
+
>
|
|
606
|
+
<span class="day-name">{dayNames[day.getDay()].slice(0, 3)}</span>
|
|
607
|
+
<span class="day-num">{day.getDate()}</span>
|
|
608
|
+
</button>
|
|
609
|
+
{/each}
|
|
610
|
+
</div>
|
|
611
|
+
{/if}
|
|
612
|
+
|
|
613
|
+
<!-- Calendar body -->
|
|
614
|
+
<div
|
|
615
|
+
class="calendar-body"
|
|
616
|
+
bind:this={calendarBody}
|
|
617
|
+
>
|
|
618
|
+
{#if isLoading}
|
|
619
|
+
<LoadingOverlay />
|
|
620
|
+
{/if}
|
|
621
|
+
|
|
622
|
+
{#if viewMode === CalendarViewMode.Day}
|
|
623
|
+
<!-- DAY VIEW -->
|
|
624
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
625
|
+
<div
|
|
626
|
+
class="day-view"
|
|
627
|
+
onmouseleave={() => {
|
|
628
|
+
hoveredSlotIndex = null;
|
|
629
|
+
}}
|
|
630
|
+
>
|
|
631
|
+
<!-- eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -->
|
|
632
|
+
{@render resourceGrid(selected)}
|
|
633
|
+
</div>
|
|
634
|
+
{:else if viewMode === CalendarViewMode.Week}
|
|
635
|
+
<!-- WEEK VIEW: day grid only, tabs are above calendar-body -->
|
|
636
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
637
|
+
<div
|
|
638
|
+
class="day-view"
|
|
639
|
+
onmouseleave={() => {
|
|
640
|
+
hoveredSlotIndex = null;
|
|
641
|
+
}}
|
|
642
|
+
>
|
|
643
|
+
<!-- eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -->
|
|
644
|
+
{@render resourceGrid(selected)}
|
|
645
|
+
</div>
|
|
646
|
+
{:else}
|
|
647
|
+
<!-- MONTH VIEW -->
|
|
648
|
+
<div class="month-view">
|
|
649
|
+
<div class="month-header-row">
|
|
650
|
+
{#each ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as dayLabel (dayLabel)}
|
|
651
|
+
<div class="month-day-header">{dayLabel}</div>
|
|
652
|
+
{/each}
|
|
653
|
+
</div>
|
|
654
|
+
<div class="month-grid">
|
|
655
|
+
{#each monthDays as { date: dayDate, isCurrentMonth } (dayDate.getTime())}
|
|
656
|
+
<button
|
|
657
|
+
class="month-day-cell"
|
|
658
|
+
class:other-month={!isCurrentMonth}
|
|
659
|
+
class:today={sameDay(dayDate, today)}
|
|
660
|
+
class:selected={sameDay(dayDate, selected)}
|
|
661
|
+
onclick={() => {
|
|
662
|
+
ondatechange(dayDate);
|
|
663
|
+
onviewmodechange(CalendarViewMode.Day);
|
|
664
|
+
}}
|
|
665
|
+
>
|
|
666
|
+
<span class="day-number">{dayDate.getDate()}</span>
|
|
667
|
+
<div class="day-badges">
|
|
668
|
+
{#each countsAt(dayDate) as { resourceId, resourceLabel, count } (resourceId)}
|
|
669
|
+
<span class="resource-badge">
|
|
670
|
+
{resourceLabel}: {count}
|
|
671
|
+
</span>
|
|
672
|
+
{/each}
|
|
673
|
+
</div>
|
|
674
|
+
</button>
|
|
675
|
+
{/each}
|
|
676
|
+
</div>
|
|
677
|
+
</div>
|
|
678
|
+
{/if}
|
|
679
|
+
</div>
|
|
680
|
+
</div>
|
|
681
|
+
|
|
682
|
+
{#snippet resourceGrid(day: Date)}
|
|
683
|
+
<div class="grid-header">
|
|
684
|
+
<div class="time-header">Time</div>
|
|
685
|
+
{#each resources as resource (resource.id)}
|
|
686
|
+
<div class="resource-header">{resource.label}</div>
|
|
687
|
+
{/each}
|
|
688
|
+
</div>
|
|
689
|
+
<div class="grid-body">
|
|
690
|
+
<div class="time-column">
|
|
691
|
+
{#each timeSlots as slot, slotIdx (slot.hour * 60 + slot.minute)}
|
|
692
|
+
<div
|
|
693
|
+
class="time-label"
|
|
694
|
+
class:hour-start={slot.minute === 0}
|
|
695
|
+
class:row-hover={hoveredSlotIndex === slotIdx}
|
|
696
|
+
data-centre-row={slot.hour === centreRowHour && slot.minute === 0 ? '' : undefined}
|
|
697
|
+
>
|
|
698
|
+
{#if slot.minute === 0}
|
|
699
|
+
<span>{slot.label}</span>
|
|
700
|
+
{/if}
|
|
701
|
+
</div>
|
|
702
|
+
{/each}
|
|
703
|
+
</div>
|
|
704
|
+
{#each resources as resource (resource.id)}
|
|
705
|
+
<div class="resource-column">
|
|
706
|
+
<!-- Slot cells for drag interaction (rendered first, below events) -->
|
|
707
|
+
{#each timeSlots as slot, slotIdx (slot.hour * 60 + slot.minute)}
|
|
708
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
709
|
+
<div
|
|
710
|
+
class="slot-cell"
|
|
711
|
+
class:hour-start={slot.minute === 0}
|
|
712
|
+
class:drag-selected={isSlotInDragRange(resource.id, slotIdx)}
|
|
713
|
+
class:move-target={isSlotInMoveTarget(resource.id, slotIdx)}
|
|
714
|
+
class:row-hover={hoveredSlotIndex === slotIdx}
|
|
715
|
+
class:unavailable={isSlotUnavailable(resource.id, slotIdx)}
|
|
716
|
+
onmousedown={(e) => {
|
|
717
|
+
onSlotMouseDown(resource.id, slotIdx, e);
|
|
718
|
+
}}
|
|
719
|
+
onmouseenter={(e) => {
|
|
720
|
+
hoveredSlotIndex = slotIdx;
|
|
721
|
+
onSlotMouseEnter(resource.id, slotIdx, e);
|
|
722
|
+
}}
|
|
723
|
+
ondblclick={() => {
|
|
724
|
+
onSlotDoubleClick(resource.id, slotIdx);
|
|
725
|
+
}}
|
|
726
|
+
></div>
|
|
727
|
+
{/each}
|
|
728
|
+
<!-- Unavailable shading: one continuous block per contiguous run -->
|
|
729
|
+
{#each unavailableRunsAt(resource.id) as run (run.topPercent)}
|
|
730
|
+
<div
|
|
731
|
+
class="unavailable-overlay"
|
|
732
|
+
style="top: {run.topPercent}%; height: {run.heightPercent}%;"
|
|
733
|
+
></div>
|
|
734
|
+
{/each}
|
|
735
|
+
<!-- Event blocks positioned absolutely (rendered after, above slots).
|
|
736
|
+
The consumer renders the card body via the `event` snippet. -->
|
|
737
|
+
{#each eventsAt(resource.id, day) as ev (ev.id)}
|
|
738
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
739
|
+
<div
|
|
740
|
+
class="event-block"
|
|
741
|
+
class:dragging={isDraggingEvent && draggedEvent?.id === ev.id}
|
|
742
|
+
class:movable={!!oneventmove}
|
|
743
|
+
style="top: {ev.topPercent}%; height: {ev.heightPercent}%;"
|
|
744
|
+
onmousedown={(e) => {
|
|
745
|
+
onEventMouseDown(ev, e);
|
|
746
|
+
}}
|
|
747
|
+
>
|
|
748
|
+
{@render eventContent(ev)}
|
|
749
|
+
</div>
|
|
750
|
+
{/each}
|
|
751
|
+
</div>
|
|
752
|
+
{/each}
|
|
753
|
+
</div>
|
|
754
|
+
{/snippet}
|
|
755
|
+
|
|
756
756
|
<style>.resource-calendar {
|
|
757
757
|
display: flex;
|
|
758
758
|
flex-flow: column nowrap;
|
|
@@ -1149,4 +1149,4 @@ $effect(() => {
|
|
|
1149
1149
|
white-space: nowrap;
|
|
1150
1150
|
overflow: hidden;
|
|
1151
1151
|
text-overflow: ellipsis;
|
|
1152
|
-
}</style>
|
|
1152
|
+
}</style>
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export { default as Button } from './Components/Button.svelte';
|
|
|
20
20
|
export type { ButtonProps } from './Components/ButtonProps.js';
|
|
21
21
|
export { default as Checkbox } from './Components/Checkbox.svelte';
|
|
22
22
|
export type { CheckboxProps } from './Components/CheckboxProps.js';
|
|
23
|
+
export { default as ChipsInput } from './Components/ChipsInput.svelte';
|
|
24
|
+
export type { ChipsInputProps } from './Components/ChipsInputProps.js';
|
|
23
25
|
export { default as CollapsibleCard } from './Components/CollapsibleCard.svelte';
|
|
24
26
|
export type { CollapsibleCardProps } from './Components/CollapsibleCardProps.js';
|
|
25
27
|
export { default as CollapsibleMenuGroup } from './Components/CollapsibleMenuGroup.svelte';
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export {} from './Components/BubbleColour.js';
|
|
|
13
13
|
export {} from './Components/BubbleProps.js';
|
|
14
14
|
export { default as Button } from './Components/Button.svelte';
|
|
15
15
|
export { default as Checkbox } from './Components/Checkbox.svelte';
|
|
16
|
+
export { default as ChipsInput } from './Components/ChipsInput.svelte';
|
|
16
17
|
export { default as CollapsibleCard } from './Components/CollapsibleCard.svelte';
|
|
17
18
|
export { default as CollapsibleMenuGroup } from './Components/CollapsibleMenuGroup.svelte';
|
|
18
19
|
export { default as PopoverButton } from './Components/PopoverButton.svelte';
|
package/dist/scss/_colours.scss
CHANGED
|
@@ -64,6 +64,12 @@ $error-border: #aa1414;
|
|
|
64
64
|
$error-background: #f9e9e9;
|
|
65
65
|
$warning: #f8eab8;
|
|
66
66
|
$warning-border: #e7b500;
|
|
67
|
+
// Success / positive-confirmation state, mirroring the $error triplet.
|
|
68
|
+
// Hue matches $despatched-background (#2f921d) so it sits in the existing
|
|
69
|
+
// palette rather than introducing a new green.
|
|
70
|
+
$success: #2f921d;
|
|
71
|
+
$success-border: #277a18;
|
|
72
|
+
$success-background: #eaf5e7;
|
|
67
73
|
|
|
68
74
|
$allocated-background: #e7b100;
|
|
69
75
|
$in-picking-background: #ec58c3;
|
package/dist/scss/colours.d.ts
CHANGED
|
@@ -59,6 +59,9 @@ export declare const Colour: {
|
|
|
59
59
|
readonly '$error-background': "#F9E9E9";
|
|
60
60
|
readonly $warning: "rgba(231, 181, 0, 0.228)";
|
|
61
61
|
readonly '$warning-border': "rgb(231, 181, 0)";
|
|
62
|
+
readonly $success: "#2F921D";
|
|
63
|
+
readonly '$success-border': "#277A18";
|
|
64
|
+
readonly '$success-background': "#EAF5E7";
|
|
62
65
|
readonly '$allocated-background': "#E7B100";
|
|
63
66
|
readonly '$in-picking-background': "#EC58C3";
|
|
64
67
|
readonly '$in-picking-colour': "#720653";
|
package/dist/scss/colours.js
CHANGED
|
@@ -61,6 +61,12 @@ export const Colour = {
|
|
|
61
61
|
'$error-background': '#F9E9E9',
|
|
62
62
|
$warning: 'rgba(231, 181, 0, 0.228)',
|
|
63
63
|
'$warning-border': 'rgb(231, 181, 0)',
|
|
64
|
+
// Success / positive-confirmation state, mirroring the $error triplet.
|
|
65
|
+
// Hue matches $despatched-background (#2F921D) so it sits in the existing
|
|
66
|
+
// palette rather than introducing a new green.
|
|
67
|
+
$success: '#2F921D',
|
|
68
|
+
'$success-border': '#277A18',
|
|
69
|
+
'$success-background': '#EAF5E7',
|
|
64
70
|
'$allocated-background': '#E7B100',
|
|
65
71
|
'$in-picking-background': '#EC58C3',
|
|
66
72
|
'$in-picking-colour': '#720653',
|