@communitiesuk/svelte-component-library 0.1.19-beta.23 → 0.1.19-beta.26

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.
@@ -141,10 +141,24 @@
141
141
  }
142
142
  }
143
143
 
144
- let colorScale = $derived(
145
- customColorScale ??
146
- interpolateColors(startColor, endColor, nBins, midColor, skew),
147
- );
144
+ let colorScale = $derived(() => {
145
+ if (customColorScale) return customColorScale;
146
+
147
+ if (!startColor || !endColor || !nBins) return [];
148
+
149
+ if (skew) {
150
+ if (
151
+ !midColor ||
152
+ averageValue == null ||
153
+ xTickFirst == null ||
154
+ xTickLast == null
155
+ ) {
156
+ return [];
157
+ }
158
+ }
159
+
160
+ return interpolateColors(startColor, endColor, nBins, midColor, skew);
161
+ });
148
162
 
149
163
  $inspect({ colorScale });
150
164
 
@@ -56,6 +56,7 @@
56
56
  showGridlines = false,
57
57
  showTickMarks = false,
58
58
  strokeWidth = 2,
59
+ niceTicks = true,
59
60
  }: {
60
61
  chartHeight?: number;
61
62
  chartWidth?: number;
@@ -83,6 +84,7 @@
83
84
  strokeWidth?: Number;
84
85
  showGridlines?: Boolean;
85
86
  showTickMarks?: Boolean;
87
+ niceTicks?: Boolean;
86
88
  } = $props();
87
89
 
88
90
  // --- Helpers to compute default domain/range when not supplied ---
@@ -156,6 +158,7 @@
156
158
  {showGridlines}
157
159
  {showTickMarks}
158
160
  {strokeWidth}
161
+ {niceTicks}
159
162
  />
160
163
  {/key}
161
164
  {/if}
@@ -31,6 +31,7 @@ type $$ComponentProps = {
31
31
  strokeWidth?: Number;
32
32
  showGridlines?: Boolean;
33
33
  showTickMarks?: Boolean;
34
+ niceTicks?: Boolean;
34
35
  };
35
36
  declare const Axis: import("svelte").Component<$$ComponentProps, {}, "ticksArray" | "chartWidth">;
36
37
  type Axis = ReturnType<typeof Axis>;
@@ -36,6 +36,7 @@
36
36
 
37
37
  strokeWidth = 2,
38
38
  labelFormatter = undefined as LabelFormatter | undefined,
39
+ niceTicks = true,
39
40
  }: {
40
41
  ticksArray?: number[]; // bindable
41
42
  chartWidth: number;
@@ -54,6 +55,7 @@
54
55
 
55
56
  strokeWidth?: number;
56
57
  labelFormatter?: LabelFormatter;
58
+ niceTicks?: Boolean;
57
59
  } = $props();
58
60
  function axisValue(fn: any, tick: number): number {
59
61
  // Try single-call first: axisFunction(tick)
@@ -142,7 +144,9 @@
142
144
  );
143
145
 
144
146
  let rawTicks = $derived(
145
- generateTicks(min, max, computedTickCount, floor, ceiling),
147
+ niceTicks
148
+ ? generateTicks(min, max, computedTickCount, floor, ceiling)
149
+ : [min, max],
146
150
  );
147
151
 
148
152
  let ticksOrdered = $derived(
@@ -23,6 +23,7 @@ type $$ComponentProps = {
23
23
  showTickMarks?: Boolean;
24
24
  strokeWidth?: number;
25
25
  labelFormatter?: LabelFormatter;
26
+ niceTicks?: Boolean;
26
27
  };
27
28
  declare const Ticks: import("svelte").Component<$$ComponentProps, {}, "ticksArray">;
28
29
  type Ticks = ReturnType<typeof Ticks>;
@@ -11,6 +11,7 @@
11
11
  markerRect = undefined,
12
12
  tooltipSnippet,
13
13
  labelText = undefined,
14
+ yOffset = 20,
14
15
  } = $props();
15
16
 
16
17
  let textDimensions = $state();
@@ -31,7 +32,7 @@ left: {markerRect?.x +
31
32
  style="position:absolute; left: {markerRect?.x - textDimensions?.width / 2}px;
32
33
  top: {markerRect?.y -
33
34
  textDimensions?.height -
34
- 20}px; pointer-events: none"
35
+ yOffset}px; pointer-events: none"
35
36
  bind:contentRect={textDimensions}
36
37
  >
37
38
  {#if tooltipSnippet === undefined}
@@ -15,6 +15,7 @@ declare const ValueLabel: import("svelte").Component<{
15
15
  markerRect?: any;
16
16
  tooltipSnippet: any;
17
17
  labelText?: any;
18
+ yOffset?: number;
18
19
  }, {}, "">;
19
20
  type $$ComponentProps = {
20
21
  activeMarkerId: any;
@@ -28,4 +29,5 @@ type $$ComponentProps = {
28
29
  markerRect?: any;
29
30
  tooltipSnippet: any;
30
31
  labelText?: any;
32
+ yOffset?: number;
31
33
  };
@@ -96,6 +96,7 @@
96
96
  labelFormatter = (tick, index, ticksArrayLength) => {
97
97
  return tick;
98
98
  },
99
+ niceTicks = true,
99
100
  } = $props();
100
101
 
101
102
  let xTicks = $state([]);
@@ -478,6 +479,7 @@
478
479
  {showTickMarks}
479
480
  {showGridlines}
480
481
  {labelFormatter}
482
+ {niceTicks}
481
483
  ></Axis>
482
484
  {/if}
483
485
  {#if showAverage}
@@ -576,6 +578,7 @@
576
578
  {showTickMarks}
577
579
  {showGridlines}
578
580
  {labelFormatter}
581
+ {niceTicks}
579
582
  ></Axis>
580
583
  {/if}
581
584
  </div>
@@ -54,6 +54,7 @@ declare const PositionChart: import("svelte").Component<{
54
54
  showTickMarks?: boolean;
55
55
  showGridlines?: boolean;
56
56
  labelFormatter?: Function;
57
+ niceTicks?: boolean;
57
58
  }, {}, "chartWidth">;
58
59
  type $$ComponentProps = {
59
60
  value?: any;
@@ -106,4 +107,5 @@ type $$ComponentProps = {
106
107
  showTickMarks?: boolean;
107
108
  showGridlines?: boolean;
108
109
  labelFormatter?: Function;
110
+ niceTicks?: boolean;
109
111
  };
@@ -1,72 +1,174 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from "svelte";
3
3
  import { browser } from "$app/environment";
4
-
4
+ import IconSearch from "../../icons/IconSearch.svelte";
5
+ import crossIconUrl from "./../../assets/govuk_publishing_components/images/cross-icon.svg";
5
6
  type Option = { id: string | number; label: string };
6
7
  type SelectedWithColor = Option & { color: string };
8
+ type SelectedItem = Option | SelectedWithColor;
7
9
 
8
- // ---------- Props ----------
9
10
  let {
10
11
  options = [],
11
- selectedWithColors = $bindable<SelectedWithColor[]>([]),
12
+ selected = $bindable<SelectedItem[]>([]),
12
13
  colorArray = ["#808080"],
13
- placeholder = "Select...",
14
+ placeholder = "Search and select…",
15
+ label = "Choose options",
16
+ hint,
17
+ enableColors = true,
18
+ startsWithSearch = true,
19
+ resetButton = false,
20
+ }: {
21
+ options?: Option[];
22
+ selected?: SelectedItem[];
23
+ colorArray?: string[];
24
+ placeholder?: string;
25
+ label?: string;
26
+ hint?: string;
27
+ enableColors?: boolean;
28
+ startsWithSearch?: boolean;
29
+ resetButton?: boolean;
14
30
  } = $props();
15
31
 
16
32
  let showDropdown = $state(false);
17
33
  let search = $state("");
18
34
 
19
- // ---------- Assign colours to incoming items ----------
35
+ let initialSelected = [];
36
+
37
+ const isAtDefault = $derived(() => {
38
+ if (!hasInitialSelection) return true;
39
+
40
+ const current = selected
41
+ .map((s) => s.id)
42
+ .sort()
43
+ .join(",");
44
+ const initial = initialSelected
45
+ .map((s) => s.id)
46
+ .sort()
47
+ .join(",");
48
+
49
+ return current === initial;
50
+ });
51
+
52
+ const hasInitialSelection = $derived(() => initialSelected.length > 0);
53
+
54
+ function resetToInitial() {
55
+ selected = initialSelected.map((item) => ({ ...item }));
56
+ search = "";
57
+ queueMicrotask(() => inputEl?.focus());
58
+ }
59
+
60
+ // Cursor used ONLY when palette is exhausted (Option 2)
61
+ let colorCursor = $state(0);
62
+
63
+ function nextColorPreferUnused(used?: Set<string>) {
64
+ if (!Array.isArray(colorArray) || colorArray.length === 0) return "#808080";
65
+
66
+ const usedColors =
67
+ used ??
68
+ new Set(
69
+ selected
70
+ .filter(
71
+ (s): s is SelectedWithColor => "color" in s && !!(s as any).color,
72
+ )
73
+ .map((s) => (s as SelectedWithColor).color),
74
+ );
75
+
76
+ // 1) Prefer unused
77
+ const unused = colorArray.find((c) => !usedColors.has(c));
78
+ if (unused) return unused;
79
+
80
+ // 2) Otherwise cycle
81
+ const color = colorArray[colorCursor % colorArray.length];
82
+ colorCursor = (colorCursor + 1) % colorArray.length;
83
+ return color;
84
+ }
85
+
20
86
  $effect(() => {
21
- if (!Array.isArray(selectedWithColors)) return;
22
- if (!colorArray.length) return;
87
+ if (!enableColors) return;
88
+ if (!Array.isArray(selected)) return;
89
+ if (!Array.isArray(colorArray) || colorArray.length === 0) return;
90
+
91
+ if (colorCursor >= colorArray.length) colorCursor = 0;
23
92
 
24
93
  const used = new Set(
25
- selectedWithColors.map((s) => s.color).filter(Boolean),
94
+ selected
95
+ .filter(
96
+ (s): s is SelectedWithColor => "color" in s && !!(s as any).color,
97
+ )
98
+ .map((s) => (s as SelectedWithColor).color),
26
99
  );
100
+
27
101
  let changed = false;
28
102
 
29
- const updated = selectedWithColors.map((item) => {
30
- if (item.color) return item;
31
- const color = colorArray.find((c) => !used.has(c)) ?? colorArray[0];
103
+ const updated = selected.map((item) => {
104
+ if ("color" in item && (item as any).color) return item;
105
+
106
+ const color = nextColorPreferUnused(used);
32
107
  used.add(color);
33
108
  changed = true;
34
- return { ...item, color };
109
+
110
+ return { ...(item as Option), color };
35
111
  });
36
112
 
37
- if (changed) selectedWithColors = updated;
113
+ if (changed) selected = updated;
38
114
  });
39
115
 
40
- // ---------- Compute filtered options ----------
41
- const filteredOptions = $derived(
42
- options.filter(
43
- (o) =>
44
- !selectedWithColors.some((s) => s.id === o.id) &&
45
- o.label.toLowerCase().includes(search.toLowerCase()),
46
- ),
47
- );
116
+ const filteredOptions = $derived.by(() => {
117
+ const q = search.trim().toLowerCase();
118
+ const selectedIds = new Set(selected.map((s) => s.id));
48
119
 
49
- // ---------- Helpers ----------
50
- function getFirstAvailableColor() {
51
- const used = new Set(selectedWithColors.map((s) => s.color));
52
- return colorArray.find((c) => !used.has(c)) ?? colorArray[0];
53
- }
120
+ // remove already-selected
121
+ const available = options
122
+ .filter((o) => !selectedIds.has(o.id))
123
+ // alphabetical sort
124
+ .slice() // avoid mutating original
125
+ .sort((a, b) =>
126
+ a.label.localeCompare(b.label, undefined, {
127
+ sensitivity: "base", // case-insensitive
128
+ numeric: true, // "Item 2" < "Item 10"
129
+ }),
130
+ );
131
+
132
+ // apply search match after sort (or before—either is fine)
133
+ const matched = q
134
+ ? available.filter((o) => {
135
+ const label = o.label.toLowerCase();
136
+ return startsWithSearch ? label.startsWith(q) : label.includes(q);
137
+ })
138
+ : available;
139
+
140
+ return matched;
141
+ });
142
+
143
+ function selectOption(option: Option) {
144
+ if (enableColors) {
145
+ const color = nextColorPreferUnused();
146
+ selected = [...selected, { ...option, color }];
147
+ } else {
148
+ selected = [...selected, option];
149
+ }
54
150
 
55
- function select(option: Option) {
56
- const color = getFirstAvailableColor();
57
- selectedWithColors = [...selectedWithColors, { ...option, color }];
58
151
  search = "";
59
152
  }
60
153
 
61
- function remove(item: SelectedWithColor) {
62
- selectedWithColors = selectedWithColors.filter((s) => s.id !== item.id);
154
+ function remove(id: Option["id"]) {
155
+ selected = selected.filter((s) => s.id !== id);
63
156
  }
64
157
 
65
158
  // Close dropdown on outside click — browser only
66
159
  let container: HTMLDivElement | null = null;
160
+ let inputEl: HTMLInputElement | null = null;
161
+
162
+ function open() {
163
+ showDropdown = true;
164
+ queueMicrotask(() => inputEl?.focus());
165
+ }
67
166
 
68
167
  onMount(() => {
69
168
  if (!browser) return;
169
+ if (resetButton === true) {
170
+ initialSelected = selected.map((item) => ({ ...item }));
171
+ }
70
172
 
71
173
  function handleOutside(e: MouseEvent) {
72
174
  if (container && !container.contains(e.target as Node)) {
@@ -77,109 +179,566 @@
77
179
  document.addEventListener("mousedown", handleOutside);
78
180
  return () => document.removeEventListener("mousedown", handleOutside);
79
181
  });
182
+
183
+ function clearAll() {
184
+ selected = [];
185
+ search = "";
186
+ queueMicrotask(() => inputEl?.focus());
187
+ }
80
188
  </script>
81
189
 
82
- <!-- ---------- TEMPLATE ---------- -->
83
- <div class="container" bind:this={container}>
84
- <div class="box" on:click={() => (showDropdown = true)}>
85
- <input
86
- {placeholder}
87
- bind:value={search}
88
- on:input={() => (showDropdown = true)}
89
- />
90
-
91
- <div class="tags">
92
- {#each selectedWithColors as item (item.id)}
93
- <span class="tag">
94
- <span class="dot" style={`background:${item.color}`}></span>
95
- {item.label}
96
- <button on:click|stopPropagation={() => remove(item)}>×</button>
97
- </span>
98
- {/each}
190
+ <div class="gem-c-select-with-search" bind:this={container}>
191
+ <label class="govuk-label" for="ms-input">{label}</label>
192
+ {#if hint}
193
+ <div class="govuk-hint">{hint}</div>
194
+ {/if}
195
+
196
+ <div
197
+ class:choices={true}
198
+ class:is-open={showDropdown}
199
+ class:is-focused={showDropdown}
200
+ data-type="select-multiple"
201
+ on:click={open}
202
+ >
203
+ <div class="choices__search-row">
204
+ <div class="choices__box">
205
+ <div class="choices__inner">
206
+ <input
207
+ id="ms-input"
208
+ class="choices__input"
209
+ bind:this={inputEl}
210
+ bind:value={search}
211
+ {placeholder}
212
+ autocomplete="off"
213
+ on:focus={() => (showDropdown = true)}
214
+ on:input={() => (showDropdown = true)}
215
+ />
216
+ </div>
217
+
218
+ {#if selected.length}
219
+ <div
220
+ class="choices__list choices__list--multiple"
221
+ aria-label="Selected items"
222
+ >
223
+ {#each selected as item (item.id)}
224
+ <span class="choices__item">
225
+ {#if enableColors && "color" in item}
226
+ <span
227
+ class="choices__item-circle"
228
+ style={`background:${(item as any).color}`}
229
+ ></span>
230
+ {/if}
231
+
232
+ <span class="choices__item-label">{item.label}</span>
233
+
234
+ <button
235
+ type="button"
236
+ class="choices__button"
237
+ on:click|stopPropagation={() => remove(item.id)}
238
+ aria-label={`Remove ${item.label}`}
239
+ title={`Remove ${item.label}`}
240
+ >
241
+ <img
242
+ src={crossIconUrl}
243
+ alt=""
244
+ aria-hidden="true"
245
+ width="18"
246
+ height="18"
247
+ />
248
+ </button>
249
+ </span>
250
+ {/each}
251
+ </div>
252
+ {/if}
253
+ {#if selected.length || (resetButton && hasInitialSelection && !isAtDefault)}
254
+ <div class="choices__actions">
255
+ {#if selected.length}
256
+ <button
257
+ type="button"
258
+ class="choices__clear-all"
259
+ on:click|stopPropagation={clearAll}
260
+ >
261
+ Remove all selected
262
+ </button>
263
+ {/if}
264
+
265
+ {#if resetButton && hasInitialSelection && !isAtDefault}
266
+ <button
267
+ type="button"
268
+ class="choices__reset"
269
+ on:click|stopPropagation={resetToInitial}
270
+ >
271
+ Reset to default
272
+ </button>
273
+ {/if}
274
+ </div>
275
+ {/if}
276
+ </div>
277
+
278
+ <button
279
+ type="button"
280
+ class="search-addon-btn"
281
+ aria-label="Search"
282
+ on:click|stopPropagation={() => open()}
283
+ >
284
+ <span class="search-addon-icon"><IconSearch /></span>
285
+ </button>
99
286
  </div>
100
287
  </div>
101
-
102
288
  {#if showDropdown}
103
- <ul class="dropdown">
289
+ <div
290
+ class="choices__list choices__list--dropdown"
291
+ class:is-open={showDropdown}
292
+ role="listbox"
293
+ aria-label="Options"
294
+ >
104
295
  {#each filteredOptions as o (o.id)}
105
- <li on:click={() => select(o)}>{o.label}</li>
296
+ <div
297
+ class="choices__item choices__item--selectable"
298
+ role="option"
299
+ on:click={() => selectOption(o)}
300
+ >
301
+ {o.label}
302
+ </div>
106
303
  {/each}
107
304
 
108
305
  {#if filteredOptions.length === 0}
109
- <li class="no">No results</li>
306
+ <div class="choices__item" aria-disabled="true" style="opacity:0.75">
307
+ No results found
308
+ </div>
110
309
  {/if}
111
- </ul>
310
+ </div>
112
311
  {/if}
113
312
  </div>
114
313
 
115
314
  <style>
116
- .container {
117
- position: relative;
118
- width: 100%;
315
+ :root {
316
+ --govuk-black: #0b0c0c;
317
+ --govuk-blue: #1d70b8;
318
+ --govuk-grey: #b1b4b6;
319
+ --govuk-light-grey: #f3f2f1;
320
+ --govuk-focus: #fd0;
321
+
322
+ --select-height: 46px;
323
+ --item-height: 44px;
324
+ --addon-width: 46px; /* ✅ used for dropdown width alignment */
119
325
  }
120
326
 
121
- .box {
122
- border: 1px solid #ccc;
123
- padding: 8px;
124
- border-radius: 6px;
125
- cursor: pointer;
327
+ :global(.gem-c-select-with-search) {
328
+ display: block;
329
+ width: 100%; /* ✅ inherit parent width */
330
+ max-width: 100%; /* ✅ never exceed parent */
331
+ box-sizing: border-box;
126
332
  }
127
333
 
128
- input {
334
+ :global(.govuk-label) {
335
+ font-family: "GDS Transport", arial, sans-serif;
336
+ font-size: 1.1875rem;
337
+ line-height: 1.31;
338
+ color: var(--govuk-black);
339
+ display: block;
340
+ margin-bottom: 5px;
341
+ }
342
+
343
+ /* ========================================
344
+ CHOICES BASE
345
+ ======================================== */
346
+
347
+ :global(.gem-c-select-with-search) .choices {
129
348
  width: 100%;
130
- font-size: 16px;
349
+ max-width: 100%;
350
+ box-sizing: border-box;
351
+
352
+ display: flex;
353
+ flex-direction: column;
354
+ align-items: stretch;
355
+ }
356
+
357
+ .choices__search-row {
358
+ display: flex;
359
+ flex-direction: row; /* side by side */
360
+ align-items: flex-start; /* ✅ align items to bottom of row */
361
+ gap: 0;
362
+ }
363
+
364
+ /* ✅ ONE box around input + divider + chips */
365
+ :global(.gem-c-select-with-search) .choices__box {
366
+ border: 2px solid var(--govuk-black);
367
+ border-radius: 0;
368
+ background: white;
369
+
370
+ flex: 1 1 auto;
371
+ min-width: 0;
372
+
373
+ /* ✅ stacks input, divider, chips in one box */
374
+ display: flex;
375
+ flex-direction: column;
376
+
377
+ padding-right: 0; /* reserve space for button */
378
+ box-sizing: border-box;
379
+ }
380
+
381
+ /* Yellow focus ring around WHOLE box */
382
+ :global(.gem-c-select-with-search) .choices.is-focused .choices__box,
383
+ :global(.gem-c-select-with-search) .choices.is-open .choices__box {
384
+ outline: 3px solid var(--govuk-focus);
385
+ outline-offset: 0;
386
+ box-shadow: inset 0 0 0 2px var(--govuk-black);
387
+ }
388
+
389
+ :global(.gem-c-select-with-search) .choices__inner {
390
+ border: 0;
391
+ background: white;
392
+ min-height: var(--select-height);
393
+ display: flex;
394
+ align-items: center;
395
+ padding: 5px;
396
+ gap: 8px;
397
+ cursor: text;
398
+ flex: 0 0 var(--select-height);
399
+ }
400
+
401
+ :global(.gem-c-select-with-search) .choices__input {
131
402
  border: none;
403
+ font-size: 19px;
404
+ margin: 0;
405
+ width: 100%;
406
+ min-width: 0;
407
+ }
408
+
409
+ :global(.gem-c-select-with-search) .choices__input:focus {
132
410
  outline: none;
133
411
  }
134
412
 
135
- .tags {
136
- margin-top: 6px;
413
+ /* ✅ Divider is now an internal border line */
414
+ .ms-divider {
415
+ border-top: 1px solid var(--govuk-grey);
416
+ margin: 0 5px;
417
+ }
418
+
419
+ /* ========================================
420
+ MULTI SELECT CHIPS (now INSIDE the same box)
421
+ ======================================== */
422
+
423
+ :global(.gem-c-select-with-search) .choices__list--multiple {
137
424
  display: flex;
138
425
  flex-wrap: wrap;
139
- gap: 4px;
426
+ align-items: flex-start;
427
+ gap: 4px 10px;
428
+ width: 100%;
429
+
430
+ border-top: 1px solid var(--govuk-grey); /* ✅ the ONE divider */
431
+ margin: 0 5px; /* aligns with input padding */
432
+ padding: 8px 8px 10px; /* slightly nicer spacing */
433
+ width: calc(100% - 10px);
434
+
435
+ box-sizing: border-box;
140
436
  }
141
437
 
142
- .tag {
143
- background: #f3f2f1;
144
- padding: 2px 6px;
145
- border-radius: 4px;
438
+ :global(.gem-c-select-with-search) .choices__list--multiple .choices__item {
146
439
  display: inline-flex;
147
440
  align-items: center;
148
- gap: 6px;
441
+ background: var(--govuk-light-grey);
442
+ color: var(--govuk-black);
443
+ box-shadow: 0 2px 0 var(--govuk-grey);
444
+ border-radius: 0;
445
+
446
+ min-height: 32px;
447
+ padding: 2px 0 2px 10px;
448
+ margin: 4px 10px 0 0;
449
+ line-height: 1.2;
450
+
451
+ max-width: 100%;
452
+ box-sizing: border-box;
149
453
  }
150
454
 
151
- .dot {
152
- width: 10px;
153
- height: 10px;
455
+ :global(.gem-c-select-with-search) .choices__item-circle {
456
+ width: 16px;
457
+ height: 16px;
154
458
  border-radius: 50%;
155
- border: 1px solid #999;
459
+ margin-right: 8px;
460
+ border: 1px solid var(--govuk-black);
156
461
  }
157
462
 
158
- .dropdown {
159
- position: absolute;
160
- background: white;
463
+ :global(.gem-c-select-with-search) .choices__item-label {
464
+ flex: 1 1 auto;
465
+ min-width: 0;
466
+ overflow: visible;
467
+ text-overflow: unset;
468
+ white-space: normal;
469
+ word-break: break-word;
470
+ max-width: 26ch;
471
+ line-height: 1.2;
472
+ }
473
+
474
+ :global(.gem-c-select-with-search)
475
+ .choices[data-type*="select-multiple"]
476
+ .choices__button {
477
+ width: 32px;
478
+ padding: 0;
479
+
480
+ border: 0;
481
+ margin-left: 8px;
482
+
483
+ background: transparent;
484
+ color: var(--govuk-black);
485
+ font-size: 20px;
486
+ line-height: 1;
487
+
488
+ opacity: 0.85;
489
+ cursor: pointer;
490
+ transition:
491
+ background-color 120ms ease,
492
+ opacity 120ms ease;
493
+
494
+ align-self: stretch;
495
+ height: auto;
496
+ min-height: 100%;
497
+ padding: 0 10px;
498
+ border-left: 1px solid var(--govuk-grey);
499
+ display: inline-flex;
500
+ align-items: center;
501
+ justify-content: center;
502
+ }
503
+
504
+ :global(.gem-c-select-with-search)
505
+ .choices[data-type*="select-multiple"]
506
+ .choices__button:hover {
507
+ background-color: #e0e0e0;
508
+ opacity: 1;
509
+ }
510
+
511
+ :global(.gem-c-select-with-search)
512
+ .choices[data-type*="select-multiple"]
513
+ .choices__button:focus {
514
+ outline: 3px solid var(--govuk-focus);
515
+ outline-offset: 0;
516
+ opacity: 1;
517
+ }
518
+
519
+ /* ========================================
520
+ SEARCH BUTTON
521
+ ======================================== */
522
+
523
+ .search-addon-btn {
524
+ width: var(--addon-width); /* keeps the width fixed */
525
+ height: var(--select-height); /* match the input box height */
526
+ display: inline-flex;
527
+ align-items: center; /* center the icon vertically */
528
+ justify-content: center; /* center the icon horizontally */
529
+ background-color: var(--govuk-blue); /* keep the blue */
530
+ color: #fff;
531
+ border: 0;
532
+ padding: 0;
533
+ font-size: 19px;
534
+ font-family: "GDS Transport", arial, sans-serif;
535
+ cursor: pointer;
536
+ }
537
+
538
+ .search-addon-btn:focus-visible {
539
+ outline: 3px solid var(--govuk-focus);
540
+ box-shadow: inset 0 0 0 4px var(--govuk-black);
541
+ }
542
+
543
+ .search-addon-icon {
544
+ position: relative;
545
+ width: var(--addon-width);
546
+ height: var(--addon-width);
547
+ display: flex;
548
+ align-items: center;
549
+ justify-content: center;
550
+ }
551
+
552
+ :global(.gem-c-select-with-search) .choices__list--dropdown {
553
+ /* IMPORTANT: ensure it participates in normal layout */
554
+ position: static !important;
555
+ inset: auto !important; /* cancels top/left/right/bottom if set somewhere */
556
+ transform: none !important;
557
+
558
+ /* layout */
559
+ display: block !important;
161
560
  width: 100%;
162
- border: 1px solid #ccc;
163
- border-radius: 6px;
164
- margin-top: 4px;
165
- z-index: 10;
166
- max-height: 200px;
561
+ box-sizing: border-box;
562
+
563
+ margin-top: 0;
564
+ border: 2px solid var(--govuk-black);
565
+ border-top: none;
566
+ background: white;
567
+
568
+ max-height: 300px;
167
569
  overflow-y: auto;
570
+ z-index: auto; /* z-index not needed when in normal flow */
571
+ width: calc(100% - var(--addon-width));
572
+ margin-left: 0;
573
+ }
574
+
575
+ :global(.gem-c-select-with-search) .choices__list--dropdown .choices__item {
576
+ display: flex;
577
+ align-items: center;
578
+ min-height: var(--item-height);
579
+ padding: 12px 10px;
580
+ position: relative;
581
+ width: 100%;
582
+
583
+ box-sizing: border-box;
584
+ flex: 0 0 auto; /* don't shrink into columns */
585
+ white-space: normal; /* allow wrappintext */
586
+ }
587
+
588
+ :global(.gem-c-select-with-search)
589
+ .choices__list--dropdown
590
+ .choices__item::after {
591
+ content: "";
592
+ position: absolute;
593
+ left: 15px;
594
+ right: 15px;
595
+ bottom: 0;
596
+ height: 1px;
597
+ background: var(--govuk-grey);
598
+ }
599
+
600
+ :global(.gem-c-select-with-search) .choices__item--selectable:hover {
601
+ background: var(--govuk-blue);
602
+ color: white;
603
+ cursor: pointer;
604
+ }
605
+ :global(.gem-c-select-with-search) .choices__list--dropdown {
606
+ display: block !important;
607
+ }
608
+
609
+ :global(.gem-c-select-with-search)
610
+ .choices.is-open
611
+ .choices__box
612
+ .choices__inner,
613
+ :global(.gem-c-select-with-search)
614
+ .choices.is-focused
615
+ .choices__box
616
+ .choices__inner {
617
+ border-bottom: 0;
618
+ box-shadow: none;
619
+ margin-bottom: 0 !important;
620
+ }
621
+
622
+ /* Only when chips are present: make the inner section visually merge into chips */
623
+ :global(.gem-c-select-with-search)
624
+ .choices.is-open
625
+ .choices__box:has(.choices__list--multiple)
626
+ .choices__inner,
627
+ :global(.gem-c-select-with-search)
628
+ .choices.is-focused
629
+ .choices__box:has(.choices__list--multiple)
630
+ .choices__inner {
631
+ border-bottom: 0;
632
+ padding-bottom: 0; /* optional: removes gap above the divider */
633
+ }
634
+
635
+ :global(.gem-c-select-with-search) .choices.is-focused .choices__box,
636
+ :global(.gem-c-select-with-search) .choices.is-open .choices__box {
637
+ outline: none;
638
+ /* keep a subtle black inset so focus isn't invisible */
639
+ box-shadow: inset 0 0 0 2px var(--govuk-black);
640
+ }
641
+
642
+ /* Remove yellow outline on the blue search button */
643
+ .search-addon-btn:focus-visible {
644
+ outline: none;
645
+ box-shadow: inset 0 0 0 3px var(--govuk-black);
646
+ }
647
+
648
+ /* Remove yellow outline on the chip remove buttons */
649
+ :global(.gem-c-select-with-search)
650
+ .choices[data-type*="select-multiple"]
651
+ .choices__button:focus {
652
+ outline: none;
653
+ box-shadow: inset 0 0 0 3px var(--govuk-black);
654
+ }
655
+
656
+ .choices__actions {
657
+ /* stack inside the box under chips */
658
+ display: block;
659
+ width: 100%;
660
+ box-sizing: border-box;
661
+
662
+ /* spacing aligned with your chips area */
663
+ margin: 0 5px;
664
+ width: calc(100% - 10px);
665
+ padding: 8px 8px 10px;
666
+
667
+ /* optional divider line (remove if you don't want another line) */
668
+ .choices__actions {
669
+ border-top: 0;
670
+ }
671
+
672
+ text-align: left; /* keeps it looking like it belongs "under" */
673
+ }
674
+
675
+ .choices__clear-all {
676
+ background: transparent;
677
+ border: 0;
168
678
  padding: 0;
169
- list-style: none;
679
+ color: var(--govuk-blue);
680
+ font-family: "GDS Transport", arial, sans-serif;
681
+ font-size: 19px;
682
+ cursor: pointer;
683
+ text-decoration: underline;
170
684
  }
171
685
 
172
- .dropdown li {
173
- padding: 8px;
686
+ .choices__clear-all:hover {
687
+ color: #003078; /* slightly darker GOV.UK blue */
688
+ }
689
+
690
+ .choices__clear-all:focus-visible {
691
+ outline: 3px solid var(--govuk-focus);
692
+ outline-offset: 0;
693
+ box-shadow: inset 0 0 0 3px var(--govuk-black);
694
+ text-decoration: none;
695
+ }
696
+
697
+ :global(.gem-c-select-with-search) .choices__list--dropdown {
698
+ margin: 0 !important; /* ✅ kills the 16px */
699
+ padding: 0;
700
+ }
701
+
702
+ .choices__reset {
703
+ background: transparent;
704
+ border: 0;
705
+ padding: 0;
706
+ margin-left: 16px;
707
+
708
+ color: var(--govuk-blue);
709
+ font-family: "GDS Transport", arial, sans-serif;
710
+ font-size: 19px;
174
711
  cursor: pointer;
712
+ text-decoration: underline;
713
+ }
714
+
715
+ .choices__reset:hover {
716
+ color: #003078;
717
+ }
718
+
719
+ .choices__reset:focus-visible {
720
+ outline: 3px solid var(--govuk-focus);
721
+ outline-offset: 0;
722
+ box-shadow: inset 0 0 0 3px var(--govuk-black);
723
+ text-decoration: none;
724
+ }
725
+
726
+ :global(.gem-c-select-with-search) .choices__list--dropdown {
727
+ scrollbar-width: thin;
728
+ }
729
+
730
+ :global(.gem-c-select-with-search)
731
+ .choices__list--dropdown::-webkit-scrollbar {
732
+ width: 10px;
175
733
  }
176
734
 
177
- .dropdown li:hover {
178
- background: #eef4ff;
735
+ :global(.gem-c-select-with-search)
736
+ .choices__list--dropdown::-webkit-scrollbar-thumb {
737
+ background-color: var(--govuk-grey);
738
+ border-radius: 0;
179
739
  }
180
740
 
181
- .no {
182
- color: #777;
183
- padding: 8px;
741
+ .choices__actions:empty {
742
+ display: none;
184
743
  }
185
744
  </style>
@@ -1,8 +1,22 @@
1
- declare const BasicMultiSelect: import("svelte").Component<{
2
- options?: any[];
3
- selectedWithColors?: any[];
4
- colorArray?: any[];
1
+ type Option = {
2
+ id: string | number;
3
+ label: string;
4
+ };
5
+ type SelectedWithColor = Option & {
6
+ color: string;
7
+ };
8
+ type SelectedItem = Option | SelectedWithColor;
9
+ type $$ComponentProps = {
10
+ options?: Option[];
11
+ selected?: SelectedItem[];
12
+ colorArray?: string[];
5
13
  placeholder?: string;
6
- }, {}, "selectedWithColors">;
14
+ label?: string;
15
+ hint?: string;
16
+ enableColors?: boolean;
17
+ startsWithSearch?: boolean;
18
+ resetButton?: boolean;
19
+ };
20
+ declare const BasicMultiSelect: import("svelte").Component<$$ComponentProps, {}, "selected">;
7
21
  type BasicMultiSelect = ReturnType<typeof BasicMultiSelect>;
8
22
  export default BasicMultiSelect;
@@ -58,6 +58,7 @@
58
58
  // Modify toggleCheckbox to handle non-JS scenarios
59
59
  function toggleCheckbox(option: CheckboxOption) {
60
60
  // If JS/modern features aren't supported, let the native checkbox behavior work
61
+
61
62
  if (!isSupported) return;
62
63
 
63
64
  if (option.exclusive) {
@@ -72,6 +72,8 @@
72
72
  }
73
73
  }
74
74
 
75
+ //$inspect(isSupported, "isSupported");
76
+
75
77
  // Handle keyboard navigation
76
78
  function handleKeydown(event: KeyboardEvent, currentIndex: number): void {
77
79
  // Skip navigation on mobile or if component isn't ready
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@communitiesuk/svelte-component-library",
3
- "version": "0.1.19-beta.23",
3
+ "version": "0.1.19-beta.26",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/communitiesuk/mhclg_svelte_component_library.git"