@lavalogic/scoria 0.40.13 → 0.40.15

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.
Files changed (143) hide show
  1. package/README.md +58 -58
  2. package/dist/Components/AccordionGroup.svelte +35 -46
  3. package/dist/Components/AccordionGroupButton.svelte +68 -76
  4. package/dist/Components/Action.svelte +103 -135
  5. package/dist/Components/AlertModal.svelte +171 -190
  6. package/dist/Components/AppointmentCalendar/AppointmentCalendar.svelte +614 -697
  7. package/dist/Components/Base/BaseModal.svelte +90 -116
  8. package/dist/Components/Base/ContextWrapper.svelte +67 -72
  9. package/dist/Components/Base/Pagination.svelte +173 -196
  10. package/dist/Components/Base/Snippets.svelte +34 -36
  11. package/dist/Components/BigRadioSet.svelte +36 -38
  12. package/dist/Components/Bubble.svelte +78 -101
  13. package/dist/Components/Button.svelte +175 -208
  14. package/dist/Components/Checkbox.svelte +75 -86
  15. package/dist/Components/CollapsibleCard.svelte +72 -87
  16. package/dist/Components/CollapsibleMenuGroup.svelte +53 -57
  17. package/dist/Components/ConditionBuilder/ConditionBuilder.svelte +39 -64
  18. package/dist/Components/Contexts/ContextMenu.svelte +141 -156
  19. package/dist/Components/Contexts/ContextMenuDivider.svelte +15 -15
  20. package/dist/Components/Contexts/ContextMenuOption.svelte +53 -60
  21. package/dist/Components/Contexts/Toast.svelte +81 -86
  22. package/dist/Components/Contexts/ToastContainer.svelte +58 -62
  23. package/dist/Components/Contexts/Tooltip.svelte +36 -37
  24. package/dist/Components/DataMatrixIcon.svelte +95 -108
  25. package/dist/Components/DateInput.svelte +118 -146
  26. package/dist/Components/DatePicker.svelte +255 -294
  27. package/dist/Components/DesktopModal.svelte +70 -90
  28. package/dist/Components/Dial.svelte +167 -197
  29. package/dist/Components/DimensionInput.svelte +124 -169
  30. package/dist/Components/DragMenu/DragMenuHolder.svelte +98 -114
  31. package/dist/Components/DragMenu/DraggableCard.svelte +64 -76
  32. package/dist/Components/FileCard.svelte +21 -24
  33. package/dist/Components/FileUpload.svelte +190 -225
  34. package/dist/Components/GridInput.svelte +159 -196
  35. package/dist/Components/GridInputSet.svelte +24 -29
  36. package/dist/Components/HorizontalTabGroup.svelte +73 -89
  37. package/dist/Components/HorizontalTabGroupButton.svelte +40 -44
  38. package/dist/Components/Icon.svelte +99 -128
  39. package/dist/Components/Icons.js +357 -357
  40. package/dist/Components/InfoAlert.svelte +35 -38
  41. package/dist/Components/LoadingAnimation.svelte +691 -755
  42. package/dist/Components/LoadingModal.svelte +36 -44
  43. package/dist/Components/LoadingOverlay.svelte +22 -26
  44. package/dist/Components/Modal.svelte +204 -254
  45. package/dist/Components/MultiSelect.svelte +186 -216
  46. package/dist/Components/Nav/Nav.svelte +94 -110
  47. package/dist/Components/Nav/NavServiceMenuList.svelte +57 -61
  48. package/dist/Components/Nav/NavTab.svelte +58 -62
  49. package/dist/Components/Nav/ServiceMenuItem.svelte +103 -111
  50. package/dist/Components/Nav/ServicesNav.svelte +72 -79
  51. package/dist/Components/NumberInput.svelte +121 -155
  52. package/dist/Components/OptionCards.svelte +62 -73
  53. package/dist/Components/PageHeading.svelte +23 -25
  54. package/dist/Components/PasswordInput.svelte +69 -88
  55. package/dist/Components/PopoverButton.svelte +151 -0
  56. package/dist/Components/PopoverButton.svelte.d.ts +9 -0
  57. package/dist/Components/PopoverButtonProps.d.ts +21 -0
  58. package/dist/Components/PopoverButtonProps.js +1 -0
  59. package/dist/Components/Portal.svelte +29 -35
  60. package/dist/Components/ProgressBubble.svelte +91 -106
  61. package/dist/Components/QuerySelect.svelte +260 -346
  62. package/dist/Components/SidebarPanel.svelte +34 -36
  63. package/dist/Components/SingleSelect.svelte +196 -244
  64. package/dist/Components/StepButton.svelte +65 -70
  65. package/dist/Components/StepForm.svelte +115 -135
  66. package/dist/Components/Switch.svelte +57 -69
  67. package/dist/Components/Table/ARCHITECTURE.md +298 -298
  68. package/dist/Components/Table/Body/QuickSearchCell.svelte +578 -655
  69. package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +42 -51
  70. package/dist/Components/Table/Body/Rows/ColumnList.svelte +42 -58
  71. package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +403 -442
  72. package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +51 -78
  73. package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +165 -184
  74. package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +110 -123
  75. package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +134 -160
  76. package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +190 -231
  77. package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +82 -104
  78. package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +52 -87
  79. package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +122 -150
  80. package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +132 -159
  81. package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +190 -236
  82. package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +173 -211
  83. package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +204 -244
  84. package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +140 -177
  85. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +188 -217
  86. package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +173 -193
  87. package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +166 -187
  88. package/dist/Components/Table/Body/Rows/TableRow.svelte +245 -288
  89. package/dist/Components/Table/Body/TableBody.svelte +141 -154
  90. package/dist/Components/Table/Headers/HighlightAllHeader.svelte +35 -41
  91. package/dist/Components/Table/Headers/SelectAllHeader.svelte +24 -30
  92. package/dist/Components/Table/Headers/TableHead.svelte +348 -402
  93. package/dist/Components/Table/Misc/ColumnPanel.svelte +37 -39
  94. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +35 -39
  95. package/dist/Components/Table/Misc/FilterInput.svelte +556 -619
  96. package/dist/Components/Table/Misc/FilterPanel.svelte +170 -183
  97. package/dist/Components/Table/Misc/FilterPanelModal.svelte +25 -31
  98. package/dist/Components/Table/Misc/ShareViewModal.svelte +252 -281
  99. package/dist/Components/Table/Misc/SidePanel.svelte +32 -35
  100. package/dist/Components/Table/Misc/TableConfigurationModal.svelte +574 -629
  101. package/dist/Components/Table/Misc/TableFooter.svelte +326 -332
  102. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +134 -140
  103. package/dist/Components/Table/Misc/TableSidebar.svelte +183 -198
  104. package/dist/Components/Table/Misc/TableViewDropdown.svelte +83 -103
  105. package/dist/Components/Table/Misc/ToolboxPanel.svelte +83 -100
  106. package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +88 -107
  107. package/dist/Components/Table/Misc/UpdateViewOptionsModal.svelte +163 -190
  108. package/dist/Components/Table/NestedTable.svelte +102 -128
  109. package/dist/Components/Table/Table.svelte +531 -663
  110. package/dist/Components/Test/TestContextConsumer.svelte +11 -13
  111. package/dist/Components/TextArea.svelte +95 -130
  112. package/dist/Components/TextInput.svelte +125 -176
  113. package/dist/Components/ThreeStateRadio.svelte +111 -130
  114. package/dist/Components/Touch/BoolCard.svelte +33 -45
  115. package/dist/Components/Touch/DateModal.svelte +164 -204
  116. package/dist/Components/Touch/EditCard.svelte +48 -59
  117. package/dist/Components/Touch/InfoCard.svelte +28 -31
  118. package/dist/Components/Touch/InfoTextCard.svelte +26 -28
  119. package/dist/Components/Touch/ModalExplanation.svelte +15 -17
  120. package/dist/Components/Touch/NumberKeyboard.svelte +181 -218
  121. package/dist/Components/Touch/NumberModal.svelte +59 -79
  122. package/dist/Components/Touch/PlusMinusCard.svelte +43 -46
  123. package/dist/Components/Touch/SelectModal.svelte +111 -126
  124. package/dist/Components/Touch/SummaryCard.svelte +80 -93
  125. package/dist/Components/Touch/TextAreaModal.svelte +72 -94
  126. package/dist/Components/Touch/TextModal.svelte +77 -105
  127. package/dist/Components/Touch/TouchCard.svelte +31 -33
  128. package/dist/Components/Touch/TouchModal.svelte +214 -270
  129. package/dist/Components/Touch/UtilityButton.svelte +88 -109
  130. package/dist/Components/VerticalTabGroup.svelte +60 -78
  131. package/dist/Components/VerticalTabGroupButton.svelte +54 -58
  132. package/dist/Helpers/Datamatrix.d.ts +19 -19
  133. package/dist/Helpers/Datamatrix.js +291 -291
  134. package/dist/Types/Examples/ExampleModal.svelte +31 -35
  135. package/dist/index.d.ts +2 -0
  136. package/dist/index.js +1 -0
  137. package/dist/scss/_basics.scss +12 -12
  138. package/dist/scss/_colours.scss +134 -134
  139. package/dist/scss/_mixins.scss +3392 -3392
  140. package/dist/scss/_motion.scss +57 -57
  141. package/dist/scss/_sizing.scss +85 -85
  142. package/dist/scss/_transitions.scss +8 -8
  143. package/package.json +1 -1
@@ -0,0 +1,151 @@
1
+ <svelte:options runes />
2
+
3
+ <!--
4
+ @component
5
+ An icon button that toggles a floating popover anchored beneath it, with a
6
+ caret pointing back at the button. The popover holds arbitrary content (the
7
+ default snippet) and closes on click-outside or Escape.
8
+ -->
9
+ <script lang="ts">import { Colour } from "../scss/colours.js";
10
+ import { Size } from "../Types/Internal/Size.js";
11
+ import Icon from "./Icon.svelte";
12
+ let { icon, label, children, align = "center", borderless = false, open = $bindable(false) } = $props();
13
+ let wrapper = $state();
14
+ function onWindowPointerDown(e) {
15
+ if (!open) {
16
+ return;
17
+ }
18
+ if (wrapper && e.target instanceof Node && !wrapper.contains(e.target)) {
19
+ open = false;
20
+ }
21
+ }
22
+ function onWindowKeydown(e) {
23
+ if (open && e.key === "Escape") {
24
+ open = false;
25
+ }
26
+ }
27
+ </script>
28
+
29
+ <svelte:window
30
+ onpointerdown={onWindowPointerDown}
31
+ onkeydown={onWindowKeydown}
32
+ />
33
+
34
+ <div
35
+ class="popover-button"
36
+ bind:this={wrapper}
37
+ >
38
+ <button
39
+ type="button"
40
+ class="trigger"
41
+ class:open
42
+ class:borderless
43
+ aria-label={label}
44
+ aria-expanded={open}
45
+ aria-haspopup="dialog"
46
+ onclick={() => {
47
+ open = !open;
48
+ }}
49
+ >
50
+ <Icon
51
+ name={icon}
52
+ size={Size.Large}
53
+ colour={open ? Colour['$brand-primary'] : Colour['$font-secondary']}
54
+ />
55
+ </button>
56
+
57
+ {#if open}
58
+ <div
59
+ class="popover align-{align}"
60
+ role="dialog"
61
+ aria-label={label}
62
+ >
63
+ <span class="caret"></span>
64
+ <div class="popover-inner">
65
+ {@render children()}
66
+ </div>
67
+ </div>
68
+ {/if}
69
+ </div>
70
+
71
+ <style>.popover-button {
72
+ position: relative;
73
+ display: inline-flex;
74
+ }
75
+
76
+ .trigger {
77
+ display: inline-flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ padding: 0.75rem;
81
+ cursor: pointer;
82
+ background-color: #ffffff;
83
+ border: solid 1px #cbd4da;
84
+ border-radius: 4px;
85
+ }
86
+ .trigger:hover {
87
+ background-color: #f4f7f9;
88
+ }
89
+ .trigger.open:not(.borderless) {
90
+ border-color: #259fc7;
91
+ }
92
+ .trigger.borderless {
93
+ border: none;
94
+ border-radius: 0;
95
+ background-color: transparent;
96
+ width: 100%;
97
+ height: 100%;
98
+ }
99
+ .trigger.borderless:hover {
100
+ background-color: #f4f7f9;
101
+ }
102
+ .trigger.borderless.open {
103
+ background-color: #f4fbfd;
104
+ }
105
+
106
+ .popover {
107
+ position: absolute;
108
+ top: calc(100% + 0.6rem);
109
+ z-index: 100;
110
+ background-color: #ffffff;
111
+ border: solid 1px #cbd4da;
112
+ border-radius: 4px;
113
+ box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.1333333333);
114
+ padding: 1.25rem;
115
+ width: max-content;
116
+ max-width: 80vw;
117
+ }
118
+ .popover.align-center {
119
+ left: 50%;
120
+ transform: translateX(-50%);
121
+ }
122
+ .popover.align-start {
123
+ left: 0;
124
+ }
125
+ .popover.align-end {
126
+ right: 0;
127
+ }
128
+
129
+ .caret {
130
+ position: absolute;
131
+ top: -6px;
132
+ width: 12px;
133
+ height: 12px;
134
+ background-color: #ffffff;
135
+ border-left: solid 1px #cbd4da;
136
+ border-top: solid 1px #cbd4da;
137
+ transform: rotate(45deg);
138
+ }
139
+
140
+ .align-center .caret {
141
+ left: 50%;
142
+ margin-left: -6px;
143
+ }
144
+
145
+ .align-start .caret {
146
+ left: 1.5rem;
147
+ }
148
+
149
+ .align-end .caret {
150
+ right: 1.5rem;
151
+ }</style>
@@ -0,0 +1,9 @@
1
+ import type { PopoverButtonProps } from './PopoverButtonProps.js';
2
+ /**
3
+ * An icon button that toggles a floating popover anchored beneath it, with a
4
+ * caret pointing back at the button. The popover holds arbitrary content (the
5
+ * default snippet) and closes on click-outside or Escape.
6
+ */
7
+ declare const PopoverButton: import("svelte").Component<PopoverButtonProps, {}, "open">;
8
+ type PopoverButton = ReturnType<typeof PopoverButton>;
9
+ export default PopoverButton;
@@ -0,0 +1,21 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { icons } from './Icons.js';
3
+ /**
4
+ * Props for {@link PopoverButton} — an icon button that toggles a floating
5
+ * popover (with a caret pointing back at the button) holding arbitrary
6
+ * content. Closes on click-outside or Escape.
7
+ */
8
+ export interface PopoverButtonProps {
9
+ /** Icon shown in the trigger button. */
10
+ icon: keyof typeof icons;
11
+ /** Accessible label for the trigger button. */
12
+ label: string;
13
+ /** Popover contents. */
14
+ children: Snippet;
15
+ /** Horizontal alignment of the popover relative to the button. Defaults to `'center'`. */
16
+ align?: 'center' | 'start' | 'end';
17
+ /** Drop the trigger's border so it can sit flush inside a grouped bar. */
18
+ borderless?: boolean;
19
+ /** Two-way bindable open state. */
20
+ open?: boolean;
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,36 +1,30 @@
1
- <svelte:options runes />
2
-
3
- <script lang="ts">
4
- import { browser } from '$app/environment';
5
- import type { PortalProps } from './PortalProps.js';
6
-
7
- let { target = browser ? globalThis.document.body : undefined, children }: PortalProps = $props();
8
-
9
- let ref: HTMLElement | undefined = $state();
10
- let lastTarget: HTMLElement | undefined;
11
-
12
- $effect(() => {
13
- // Identity guard: only re-parent when `target` actually changes.
14
- // `appendChild` already moves the node when called, so this avoids
15
- // unnecessary detach/attach when an upstream `$state` target
16
- // re-fires the effect without changing identity.
17
- if (target && ref && target !== lastTarget) {
18
- if (target.ownerDocument === globalThis.document) {
19
- target.appendChild(ref);
20
- lastTarget = target;
21
- }
1
+ <svelte:options runes />
2
+
3
+ <script lang="ts">import { browser } from "$app/environment";
4
+ let { target = browser ? globalThis.document.body : undefined, children } = $props();
5
+ let ref = $state();
6
+ let lastTarget;
7
+ $effect(() => {
8
+ // Identity guard: only re-parent when `target` actually changes.
9
+ // `appendChild` already moves the node when called, so this avoids
10
+ // unnecessary detach/attach when an upstream `$state` target
11
+ // re-fires the effect without changing identity.
12
+ if (target && ref && target !== lastTarget) {
13
+ if (target.ownerDocument === globalThis.document) {
14
+ target.appendChild(ref);
15
+ lastTarget = target;
22
16
  }
23
-
24
- // Cleanup is necessary, not redundant: without it, the appended
25
- // node would remain inside `target` after the Portal unmounts,
26
- // because `target` outlives the component.
27
- return () => {
28
- ref?.parentNode?.removeChild(ref);
29
- lastTarget = undefined;
30
- };
31
- });
32
- </script>
33
-
34
- <div bind:this={ref}>
35
- {@render children()}
36
- </div>
17
+ }
18
+ // Cleanup is necessary, not redundant: without it, the appended
19
+ // node would remain inside `target` after the Portal unmounts,
20
+ // because `target` outlives the component.
21
+ return () => {
22
+ ref?.parentNode?.removeChild(ref);
23
+ lastTarget = undefined;
24
+ };
25
+ });
26
+ </script>
27
+
28
+ <div bind:this={ref}>
29
+ {@render children()}
30
+ </div>
@@ -1,109 +1,94 @@
1
- <svelte:options runes />
2
-
3
- <script lang="ts">
4
- import { useAsyncFlag } from '../Helpers/AsyncFlag.svelte.js';
5
- import { getCanvasContext } from '../Helpers/Helpers.svelte.js';
6
- import type { BubbleColour } from './BubbleColour.js';
7
- import type { ProgressBubbleProps } from './ProgressBubbleProps.js';
8
-
9
- const { value, colours }: ProgressBubbleProps = $props();
10
-
11
- const current = $derived(value[0]);
12
- const total = $derived(value[1]);
13
- const progress = $derived(`${current}/${total}`);
14
-
15
- // Clamp the percentage so callers passing `current > total` do not
16
- // blow past the bar's edge.
17
- const percent = $derived(Math.min((current / total) * 100, 100));
18
-
19
- let progressBarRef: HTMLDivElement | undefined = $state();
20
-
21
- // Observed inline-size of the filled bar. A `ResizeObserver` is used
22
- // instead of `getBoundingClientRect()` inside an `$effect`: the latter
23
- // forces a synchronous layout flush on every `progress` change, which
24
- // is expensive inside a table with many `ProgressBubble` cells. The
25
- // observer notifies us only when the bar's actual rendered size
26
- // changes, and does so via a microtask scheduled by the browser so we
27
- // never force layout ourselves.
28
- let barWidth = $state(0);
29
- $effect(() => {
30
- if (!progressBarRef) {
31
- return;
32
- }
33
- const ro = new ResizeObserver((entries) => {
34
- const entry = entries[0];
35
- barWidth = entry.borderBoxSize[0]?.inlineSize ?? entry.contentRect.width;
36
- });
37
- ro.observe(progressBarRef);
38
- return () => {
39
- ro.disconnect();
40
- };
1
+ <svelte:options runes />
2
+
3
+ <script lang="ts">import { useAsyncFlag } from "../Helpers/AsyncFlag.svelte.js";
4
+ import { getCanvasContext } from "../Helpers/Helpers.svelte.js";
5
+ const { value, colours } = $props();
6
+ const current = $derived(value[0]);
7
+ const total = $derived(value[1]);
8
+ const progress = $derived(`${current}/${total}`);
9
+ // Clamp the percentage so callers passing `current > total` do not
10
+ // blow past the bar's edge.
11
+ const percent = $derived(Math.min(current / total * 100, 100));
12
+ let progressBarRef = $state();
13
+ // Observed inline-size of the filled bar. A `ResizeObserver` is used
14
+ // instead of `getBoundingClientRect()` inside an `$effect`: the latter
15
+ // forces a synchronous layout flush on every `progress` change, which
16
+ // is expensive inside a table with many `ProgressBubble` cells. The
17
+ // observer notifies us only when the bar's actual rendered size
18
+ // changes, and does so via a microtask scheduled by the browser so we
19
+ // never force layout ourselves.
20
+ let barWidth = $state(0);
21
+ $effect(() => {
22
+ if (!progressBarRef) {
23
+ return;
24
+ }
25
+ const ro = new ResizeObserver((entries) => {
26
+ const entry = entries[0];
27
+ barWidth = entry.borderBoxSize[0]?.inlineSize ?? entry.contentRect.width;
41
28
  });
42
-
43
- // Whether the `current/total` label fits inside the filled bar.
44
- // Measured via a memoised offscreen canvas so we do not trigger
45
- // layout.
46
- const fitsInProgressBar = $derived.by(() => {
47
- const textWidth = getCanvasContext()?.measureText(`__${progress}__`).width ?? 0;
48
- return barWidth > textWidth;
49
- });
50
-
51
- // `colours` may be a bare `BubbleColour`, a Promise resolving to one,
52
- // or undefined. Routed through `useAsyncFlag` which (a) cancels stale
53
- // Promises via a sequence ticket so the freshest value always wins,
54
- // (b) reports errors through `ToastContext` when one is mounted, and
55
- // (c) falls back to `undefined` while no resolution is available.
56
- const awaitedColoursFlag = useAsyncFlag<BubbleColour | undefined>(() => colours, undefined);
57
- const awaitedColours = $derived(awaitedColoursFlag.value);
58
-
59
- // Precompute the wrapper and bar style strings. Each colour comes
60
- // from the trusted `Colour` registry (see `Bubble.svelte` security
61
- // note) so interpolation into `style="..."` is safe.
62
- const wrapperStyle = $derived(
63
- awaitedColours
64
- ? `--bar-background-colour: ${awaitedColours.backgroundColour}; --text-colour: ${awaitedColours.colour};`
65
- : ''
66
- );
67
- const barStyle = $derived(`width: ${percent}%;`);
68
- </script>
69
-
70
- <div
71
- class="progress-wrapper"
72
- style={wrapperStyle}
73
- >
74
- <!--
75
- Native `<progress>` is kept as a fallback for assistive tech that
76
- ignores ARIA progressbar roles. Visually hidden rather than
77
- `display: none` so the element stays in the accessibility tree.
78
- -->
79
- <progress
80
- class="visually-hidden"
81
- value={current}
82
- max={total}>{progress}</progress
83
- >
84
-
85
- <div
86
- class="progress-background"
87
- role="progressbar"
88
- aria-valuenow={current}
89
- aria-valuemin={0}
90
- aria-valuemax={total}
91
- aria-valuetext={progress}
92
- >
93
- <div
94
- class:fits={fitsInProgressBar}
95
- class="progress-value"
96
- bind:this={progressBarRef}
97
- style={barStyle}
98
- >
99
- <span
100
- class:fits={fitsInProgressBar}
101
- class="progress-text">{progress}</span
102
- >
103
- </div>
104
- </div>
105
- </div>
106
-
29
+ ro.observe(progressBarRef);
30
+ return () => {
31
+ ro.disconnect();
32
+ };
33
+ });
34
+ // Whether the `current/total` label fits inside the filled bar.
35
+ // Measured via a memoised offscreen canvas so we do not trigger
36
+ // layout.
37
+ const fitsInProgressBar = $derived.by(() => {
38
+ const textWidth = getCanvasContext()?.measureText(`__${progress}__`).width ?? 0;
39
+ return barWidth > textWidth;
40
+ });
41
+ // `colours` may be a bare `BubbleColour`, a Promise resolving to one,
42
+ // or undefined. Routed through `useAsyncFlag` which (a) cancels stale
43
+ // Promises via a sequence ticket so the freshest value always wins,
44
+ // (b) reports errors through `ToastContext` when one is mounted, and
45
+ // (c) falls back to `undefined` while no resolution is available.
46
+ const awaitedColoursFlag = useAsyncFlag(() => colours, undefined);
47
+ const awaitedColours = $derived(awaitedColoursFlag.value);
48
+ // Precompute the wrapper and bar style strings. Each colour comes
49
+ // from the trusted `Colour` registry (see `Bubble.svelte` security
50
+ // note) so interpolation into `style="..."` is safe.
51
+ const wrapperStyle = $derived(awaitedColours ? `--bar-background-colour: ${awaitedColours.backgroundColour}; --text-colour: ${awaitedColours.colour};` : "");
52
+ const barStyle = $derived(`width: ${percent}%;`);
53
+ </script>
54
+
55
+ <div
56
+ class="progress-wrapper"
57
+ style={wrapperStyle}
58
+ >
59
+ <!--
60
+ Native `<progress>` is kept as a fallback for assistive tech that
61
+ ignores ARIA progressbar roles. Visually hidden rather than
62
+ `display: none` so the element stays in the accessibility tree.
63
+ -->
64
+ <progress
65
+ class="visually-hidden"
66
+ value={current}
67
+ max={total}>{progress}</progress
68
+ >
69
+
70
+ <div
71
+ class="progress-background"
72
+ role="progressbar"
73
+ aria-valuenow={current}
74
+ aria-valuemin={0}
75
+ aria-valuemax={total}
76
+ aria-valuetext={progress}
77
+ >
78
+ <div
79
+ class:fits={fitsInProgressBar}
80
+ class="progress-value"
81
+ bind:this={progressBarRef}
82
+ style={barStyle}
83
+ >
84
+ <span
85
+ class:fits={fitsInProgressBar}
86
+ class="progress-text">{progress}</span
87
+ >
88
+ </div>
89
+ </div>
90
+ </div>
91
+
107
92
  <style>.progress-wrapper {
108
93
  width: 100%;
109
94
  height: 100%;
@@ -172,4 +157,4 @@
172
157
  }
173
158
  .progress-text.fits {
174
159
  color: var(--text-colour, #3a4952);
175
- }</style>
160
+ }</style>