@lavalogic/scoria 0.40.25 → 0.40.27

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.
@@ -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>&nbsp;- optional</i>{/if}</span
44
+ >
45
+ <span
46
+ aria-hidden="true"
47
+ class="border"
48
+ class:invalid
49
+ >{labelTop}{#if optional}<i>&nbsp;- 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
+ }}>&times;</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,4 @@
1
+ import type { ChipsInputProps } from './ChipsInputProps.js';
2
+ declare const ChipsInput: import("svelte").Component<ChipsInputProps, {}, "values">;
3
+ type ChipsInput = ReturnType<typeof ChipsInput>;
4
+ export default ChipsInput;
@@ -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>
@@ -83,14 +83,14 @@ $effect(() => {
83
83
  margin: 0;
84
84
  padding: 0;
85
85
  border: none;
86
- background: transparent;
86
+ background-color: rgba(0, 0, 0, 0.2);
87
87
  z-index: 98;
88
88
  overflow: visible;
89
89
  color: inherit;
90
90
  }
91
91
 
92
92
  dialog.overlay::backdrop {
93
- background-color: rgba(0, 0, 0, 0.2);
93
+ background-color: transparent;
94
94
  }
95
95
 
96
96
  div.services {
@@ -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';
@@ -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;
@@ -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";
@@ -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',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lavalogic/scoria",
3
3
  "description": "Svelte components used for the FloWMS Web Frontend",
4
- "version": "0.40.25",
4
+ "version": "0.40.27",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
7
7
  },