@lavalogic/scoria 0.40.17 → 0.40.19

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.
@@ -100,6 +100,15 @@ const timeSlots = $derived.by(() => {
100
100
  * all) is treated as fully available, so nothing is shaded until the
101
101
  * caller supplies windows for that door.
102
102
  */
103
+ function slotUnavailableInWindows(windows, slot) {
104
+ const minutes = slot.hour * 60 + slot.minute;
105
+ for (const window of windows) {
106
+ if (minutes >= window.startMinutes && minutes < window.endMinutes) {
107
+ return false;
108
+ }
109
+ }
110
+ return true;
111
+ }
103
112
  function isSlotUnavailable(doorId, slotIndex) {
104
113
  if (!availability) {
105
114
  return false;
@@ -112,13 +121,53 @@ function isSlotUnavailable(doorId, slotIndex) {
112
121
  if (!slot) {
113
122
  return false;
114
123
  }
115
- const minutes = slot.hour * 60 + slot.minute;
116
- for (const window of windows) {
117
- if (minutes >= window.startMinutes && minutes < window.endMinutes) {
118
- return false;
124
+ return slotUnavailableInWindows(windows, slot);
125
+ }
126
+ /**
127
+ * Contiguous unavailable runs per door, expressed as top/height
128
+ * percentages of the day column. Rendered as a single overlay block per
129
+ * run so the shading pattern is continuous across the whole region
130
+ * rather than restarting (and visibly seaming) at every slot cell.
131
+ */
132
+ const unavailableRunsByDoor = $derived.by(() => {
133
+ // Local index built inside a $derived; outer reactivity covers reads.
134
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
135
+ const byDoor = new Map();
136
+ const slotCount = timeSlots.length;
137
+ if (!availability || slotCount === 0) {
138
+ return byDoor;
139
+ }
140
+ for (const door of doors) {
141
+ const windows = availability.get(door.id);
142
+ if (!windows) {
143
+ continue;
144
+ }
145
+ const runs = [];
146
+ let runStart = null;
147
+ // One extra iteration (i === slotCount) flushes a run that reaches
148
+ // the end of the day.
149
+ for (let i = 0; i <= slotCount; i++) {
150
+ const unavailable = i < slotCount && slotUnavailableInWindows(windows, timeSlots[i]);
151
+ if (unavailable) {
152
+ if (runStart === null) {
153
+ runStart = i;
154
+ }
155
+ } else if (runStart !== null) {
156
+ runs.push({
157
+ topPercent: runStart / slotCount * 100,
158
+ heightPercent: (i - runStart) / slotCount * 100
159
+ });
160
+ runStart = null;
161
+ }
162
+ }
163
+ if (runs.length > 0) {
164
+ byDoor.set(door.id, runs);
119
165
  }
120
166
  }
121
- return true;
167
+ return byDoor;
168
+ });
169
+ function unavailableRunsAt(doorId) {
170
+ return unavailableRunsByDoor.get(doorId) ?? [];
122
171
  }
123
172
  /**
124
173
  * Build a `YYYY-MM-DD` key for date bucketing. Avoids `toISOString`
@@ -696,6 +745,13 @@ $effect(() => {
696
745
  }}
697
746
  ></div>
698
747
  {/each}
748
+ <!-- Unavailable shading: one continuous block per contiguous run -->
749
+ {#each unavailableRunsAt(door.id) as run (run.topPercent)}
750
+ <div
751
+ class="unavailable-overlay"
752
+ style="top: {run.topPercent}%; height: {run.heightPercent}%;"
753
+ ></div>
754
+ {/each}
699
755
  <!-- Appointment blocks positioned absolutely (rendered after, sit above slots) -->
700
756
  {#each appointmentsAt(door.id, day) as apt (apt.id)}
701
757
  <!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -715,6 +771,9 @@ $effect(() => {
715
771
  {#if apt.sublabel}
716
772
  <span class="apt-sublabel">{apt.sublabel}</span>
717
773
  {/if}
774
+ {#if apt.reference}
775
+ <span class="apt-reference">Ref: {apt.reference}</span>
776
+ {/if}
718
777
  </div>
719
778
  {/each}
720
779
  </div>
@@ -939,8 +998,16 @@ $effect(() => {
939
998
  }
940
999
  .slot-cell.unavailable {
941
1000
  cursor: not-allowed;
942
- background-color: #e9ecf1;
943
- background-image: repeating-linear-gradient(45deg, transparent, transparent 5px, rgba(128, 150, 165, 0.18) 5px, rgba(128, 150, 165, 0.18) 10px);
1001
+ }
1002
+
1003
+ .unavailable-overlay {
1004
+ position: absolute;
1005
+ left: 0;
1006
+ right: 0;
1007
+ z-index: 3;
1008
+ pointer-events: none;
1009
+ background-color: rgba(128, 150, 165, 0.06);
1010
+ background-image: repeating-linear-gradient(45deg, transparent, transparent 6px, rgba(128, 150, 165, 0.14) 6px, rgba(128, 150, 165, 0.14) 12px);
944
1011
  }
945
1012
 
946
1013
  .appointment-block {
@@ -980,11 +1047,15 @@ $effect(() => {
980
1047
  color: #259fc7;
981
1048
  }
982
1049
  .appointment-block.outbound {
983
- border-color: #e7b100;
984
- background-color: #f0faf0;
1050
+ border-color: #66bb6a;
1051
+ background-color: #eaf7ec;
985
1052
  }
986
1053
  .appointment-block.outbound .apt-type {
987
- color: #e7b100;
1054
+ color: #2e7d32;
1055
+ }
1056
+ .appointment-block.outbound:hover {
1057
+ background-color: #dcefdd;
1058
+ border-color: #43a047;
988
1059
  }
989
1060
 
990
1061
  .apt-type {
@@ -1003,6 +1074,12 @@ $effect(() => {
1003
1074
  font-size: 0.75rem;
1004
1075
  }
1005
1076
 
1077
+ .apt-reference {
1078
+ color: #647d8e;
1079
+ font-size: 0.75rem;
1080
+ font-weight: 600;
1081
+ }
1082
+
1006
1083
  .week-day-tabs {
1007
1084
  display: flex;
1008
1085
  flex-flow: row nowrap;
@@ -29,6 +29,8 @@ export interface CalendarAppointment {
29
29
  readonly typeCode: string;
30
30
  readonly label: string;
31
31
  readonly sublabel?: string;
32
+ /** Optional booking reference shown on the appointment card. */
33
+ readonly reference?: string;
32
34
  }
33
35
  export interface CalendarDoor {
34
36
  readonly id: number;
@@ -82,7 +82,7 @@ function handleChange(e) {
82
82
  />
83
83
  {/if}
84
84
  <input
85
- step={type === 'datetime-local' ? 1 : 'any'}
85
+ step={type === 'datetime-local' ? 1 : type === 'time' ? 60 : 'any'}
86
86
  class:invalid={isInvalid}
87
87
  class:left-pad={leftIcon != undefined}
88
88
  class:right-pad={rightIcon != undefined}
@@ -1,3 +1,4 @@
1
1
  import type { DateOnlyInputProps } from './DateOnlyInputProps.js';
2
2
  import type { DateTimeInputProps } from './DateTimeInputProps.js';
3
- export type DateInputProps = DateOnlyInputProps | DateTimeInputProps;
3
+ import type { TimeInputProps } from './TimeInputProps.js';
4
+ export type DateInputProps = DateOnlyInputProps | DateTimeInputProps | TimeInputProps;
@@ -0,0 +1,51 @@
1
+ import type { InputVariant } from '../Types/Internal/InputVariant.js';
2
+ import type { InputTime } from '../Types/Internal/Misc.js';
3
+ import type { PartialIconProps } from './PartialIconProps.js';
4
+ /**
5
+ * Props for the `time` variant of {@link DateInput}.
6
+ *
7
+ * Renders a native `<input type="time">` with the same overlapping-label
8
+ * border chrome as the date/datetime variants. `value` is branded with
9
+ * {@link InputTime} (`HH:MM`); the empty string is a known sentinel that
10
+ * the consumer is expected to handle when binding.
11
+ */
12
+ export interface TimeInputProps {
13
+ /** Discriminator. Must be the literal `'time'`. */
14
+ type: 'time';
15
+ /**
16
+ * Bindable time value (`HH:MM`). `''` or `null` represent "no time
17
+ * selected" — both are accepted because consumers commonly back the
18
+ * binding with a nullable field; the component treats them alike.
19
+ */
20
+ value?: InputTime | '' | null;
21
+ /** Label rendered above the input. */
22
+ label?: string;
23
+ /** Native `placeholder` attribute. */
24
+ placeholder?: string;
25
+ /** Native `required` attribute. */
26
+ required?: boolean;
27
+ /** Render the "- optional" suffix next to `label`. */
28
+ optional?: boolean;
29
+ /** Render variant. See {@link InputVariant}. */
30
+ variant?: InputVariant;
31
+ /** Native `min` constraint. */
32
+ min?: InputTime;
33
+ /** Native `max` constraint. */
34
+ max?: InputTime;
35
+ /** Native `disabled` attribute. */
36
+ disabled?: boolean;
37
+ /** Icon rendered on the left of the input. */
38
+ leftIcon?: PartialIconProps;
39
+ /** Icon rendered on the right of the input. */
40
+ rightIcon?: PartialIconProps;
41
+ /** Bindable reference to the underlying `<input>` element. */
42
+ inputRef?: HTMLInputElement;
43
+ /** Force the invalid visual state. */
44
+ invalid?: boolean;
45
+ /** Blur handler. */
46
+ onblur?: (e: FocusEvent) => void;
47
+ /** Focus handler. */
48
+ onfocus?: (e: FocusEvent) => void;
49
+ /** Change handler. May return `void` or `Promise<void>`. */
50
+ onchange?: (value: InputTime | null) => void | Promise<void>;
51
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -69,6 +69,8 @@ export type ScoriaStorageKey = BrandedString<'ScoriaStorageKey'>;
69
69
  export type InputDate = BrandedString<'InputDate'>;
70
70
  /** ISO date-time string of the form `YYYY-MM-DDTHH:MM:SS`. */
71
71
  export type InputDateWithTime = BrandedString<'InputDateWithTime'>;
72
+ /** ISO time-only string of the form `HH:MM`. */
73
+ export type InputTime = BrandedString<'InputTime'>;
72
74
  /** Date string formatted for the FloWMS SQL backend. */
73
75
  export type SQLDate = BrandedString<'SQLDate'>;
74
76
  /** Integer centimetre count. Used by dimension inputs. */
package/dist/index.d.ts CHANGED
@@ -41,6 +41,7 @@ export type { DateOnlyInputProps } from './Components/DateOnlyInputProps.js';
41
41
  export { default as DatePicker } from './Components/DatePicker.svelte';
42
42
  export { type DatePickerProps } from './Components/DatePickerProps.js';
43
43
  export type { DateTimeInputProps } from './Components/DateTimeInputProps.js';
44
+ export type { TimeInputProps } from './Components/TimeInputProps.js';
44
45
  export { default as DesktopModal } from './Components/DesktopModal.svelte';
45
46
  export type { DesktopModalProps } from './Components/DesktopModalProps.js';
46
47
  export { default as Dial } from './Components/Dial.svelte';
@@ -210,7 +211,7 @@ export { type GridStringItem } from './Types/Internal/GridStringItem.js';
210
211
  export { type GridTextItem } from './Types/Internal/GridTextItem.js';
211
212
  export { type IdentifiableToastMessage } from './Types/Internal/IdentifiableToastMessage.js';
212
213
  export { InputVariant } from './Types/Internal/InputVariant.js';
213
- export { type BrandedInteger, type BrandedNumber, type BrandedString, type Centimetres, type CssLength, type InputDate, type InputDateWithTime, type NonNullableKey, type Pixels, type ScoriaStorageKey, type SQLDate, type Unsubscriber, type Uuid, type ValueProp, } from './Types/Internal/Misc.js';
214
+ export { type BrandedInteger, type BrandedNumber, type BrandedString, type Centimetres, type CssLength, type InputDate, type InputDateWithTime, type InputTime, type NonNullableKey, type Pixels, type ScoriaStorageKey, type SQLDate, type Unsubscriber, type Uuid, type ValueProp, } from './Types/Internal/Misc.js';
214
215
  export { type RefWrapper } from './Types/Internal/RefWrapper.js';
215
216
  export { type SelectOption } from './Types/Internal/SelectOption.js';
216
217
  export { Shape } from './Types/Internal/Shape.js';
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.17",
4
+ "version": "0.40.19",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
7
7
  },