@softwareone/spi-sv5-library 1.3.1 → 1.4.0

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 (35) hide show
  1. package/dist/Form/Label.svelte +1 -3
  2. package/dist/Form/Select/Select.svelte +110 -95
  3. package/dist/Form/Toggle/Toggle.svelte +56 -21
  4. package/dist/Form/Toggle/Toggle.svelte.d.ts +3 -0
  5. package/dist/Header/Header.svelte +81 -24
  6. package/dist/Header/Header.svelte.d.ts +2 -1
  7. package/dist/Home/Home.svelte +6 -12
  8. package/dist/Menu/Menu.svelte +9 -50
  9. package/dist/Menu/Menu.svelte.d.ts +1 -1
  10. package/dist/Menu/MenuItem.svelte +2 -14
  11. package/dist/Menu/MenuItem.svelte.d.ts +0 -1
  12. package/dist/Modal/Modal.svelte +32 -23
  13. package/dist/Modal/ModalContent.svelte +7 -4
  14. package/dist/Modal/ModalContent.svelte.d.ts +1 -0
  15. package/dist/Modal/ModalHeader.svelte +14 -10
  16. package/dist/Modal/modalState.svelte.d.ts +2 -0
  17. package/dist/Processing/Processing.svelte +89 -0
  18. package/dist/Processing/Processing.svelte.d.ts +4 -0
  19. package/dist/Processing/processingState.svelte.d.ts +6 -0
  20. package/dist/Processing/processingState.svelte.js +1 -0
  21. package/dist/ProgressWizard/ProgressWizard.svelte +0 -1
  22. package/dist/Spinner/Spinner.svelte +1 -0
  23. package/dist/Switcher/Switcher.svelte +78 -0
  24. package/dist/Switcher/Switcher.svelte.d.ts +8 -0
  25. package/dist/Switcher/switcherState.svelte.d.ts +4 -0
  26. package/dist/Switcher/switcherState.svelte.js +1 -0
  27. package/dist/Waffle/Waffle.svelte +95 -0
  28. package/dist/Waffle/Waffle.svelte.d.ts +8 -0
  29. package/dist/Waffle/WaffleItems.svelte +82 -0
  30. package/dist/Waffle/WaffleItems.svelte.d.ts +9 -0
  31. package/dist/Waffle/waffleState.svelte.d.ts +6 -0
  32. package/dist/Waffle/waffleState.svelte.js +1 -0
  33. package/dist/index.d.ts +6 -1
  34. package/dist/index.js +4 -1
  35. package/package.json +1 -1
@@ -22,9 +22,7 @@
22
22
  {/if}
23
23
  {#if infoTooltip}
24
24
  <Tooltip content={infoTooltip} width="sm">
25
- {#snippet children()}
26
- <span class="material-icons-outlined">info</span>
27
- {/snippet}
25
+ <span class="material-icons-outlined">info</span>
28
26
  </Tooltip>
29
27
  {/if}
30
28
  </div>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { onMount, type Snippet } from 'svelte';
2
+ import { type Snippet } from 'svelte';
3
3
  import type { Action } from 'svelte/action';
4
4
 
5
5
  import { Search, type SelectOption } from '../../index.js';
@@ -50,39 +50,75 @@
50
50
  let searchText = $state('');
51
51
  let showInTopPosition = $state(false);
52
52
  let showListOptions = $state(false);
53
- let selectedOptions = $state<SelectOption[]>([]);
54
- let selectedOption = $state<SelectOption | undefined>();
55
53
 
56
- let dropdownElement: HTMLElement;
54
+ let dropdown: HTMLElement;
55
+ let dropdownContainer: HTMLElement;
57
56
 
58
57
  const isStringArray = (items: string[] | SelectOption[]): items is string[] =>
59
58
  typeof items[0] === 'string';
60
59
 
61
- const filterOptions = (): SelectOption[] => {
62
- const text = searchText.toLowerCase();
60
+ const generateSelectOption = (text: string, options: SelectOption[]): SelectOption =>
61
+ options.find((option) => option.value === text) ?? {
62
+ label: text,
63
+ value: text
64
+ };
63
65
 
64
- return text
65
- ? originalOptions.filter((option) => option.label.toLowerCase().includes(text))
66
- : originalOptions;
67
- };
66
+ const valueSet = $derived<Set<string>>(new Set(Array.isArray(value) ? value : []));
68
67
 
69
- const checkNoOptionsAvailable = (): boolean => {
70
- if (!options.length) return true;
71
- return multiple && originalOptions.every((option) => valueSet.has(option.value));
72
- };
68
+ const isInvalid = $derived<boolean>(!!error && !disableValidationColor);
69
+
70
+ const isValid = $derived<boolean>(
71
+ !isInvalid &&
72
+ !disableValidationColor &&
73
+ (Array.isArray(value) ? value.length > 0 : !!value || optional)
74
+ );
73
75
 
74
76
  const originalOptions = $derived<SelectOption[]>(
75
- isStringArray(options) ? options.map((value) => ({ label: value, value })) : options
77
+ isStringArray(options) ? options.map((option) => ({ label: option, value: option })) : options
76
78
  );
77
- const valueSet = $derived<Set<string>>(new Set(value));
78
- const isInvalid = $derived<boolean>(!!error && !disableValidationColor);
79
- const isValid = $derived<boolean>(!isInvalid && (!!value || optional) && !disableValidationColor);
80
- const filteredOptions = $derived<SelectOption[]>(filterOptions());
81
- const noOptionsAvailable = $derived<boolean>(checkNoOptionsAvailable());
82
79
 
83
- const generateSelectOption = (value: string): SelectOption => ({ label: value, value });
80
+ const selectedOptions = $derived<SelectOption[]>(
81
+ multiple && Array.isArray(value) && value.length
82
+ ? value.map((text) => generateSelectOption(text, originalOptions))
83
+ : []
84
+ );
84
85
 
85
- const generateMultiselectValue = (option: SelectOption): string => option.value;
86
+ const selectedOption = $derived<SelectOption | undefined>(
87
+ !multiple && typeof value === 'string' && value
88
+ ? generateSelectOption(value, originalOptions)
89
+ : undefined
90
+ );
91
+
92
+ const noOptionsAvailable = $derived<boolean>(
93
+ !options.length
94
+ ? true
95
+ : multiple && originalOptions.every((option) => valueSet.has(option.value))
96
+ );
97
+
98
+ const filteredOptions = $derived.by<SelectOption[]>(() => {
99
+ const text = searchText.toLowerCase();
100
+ return !text
101
+ ? originalOptions
102
+ : originalOptions.filter((option) => option.label.toLowerCase().includes(text));
103
+ });
104
+
105
+ const activeOptionScroll: Action<HTMLElement, boolean> = (node, isActive) => {
106
+ $effect(() => {
107
+ if (isActive) {
108
+ node.scrollIntoView({ behavior: 'instant', block: 'nearest' });
109
+ }
110
+ });
111
+ };
112
+
113
+ const autoDirection: Action = () => {
114
+ $effect(() => {
115
+ const rect = dropdown.getBoundingClientRect();
116
+ const viewportHeight = window.innerHeight;
117
+ const footer = 300;
118
+
119
+ showInTopPosition = rect.bottom + footer > viewportHeight;
120
+ });
121
+ };
86
122
 
87
123
  const canBeVisible = (option: SelectOption): boolean =>
88
124
  !selectedOptions.some((selectedOption) => selectedOption.value === option.value);
@@ -94,93 +130,61 @@
94
130
  };
95
131
 
96
132
  const onSelectOption = (option: SelectOption) => {
97
- if (multiple) {
98
- selectedOptions = [...selectedOptions, option];
99
- value = selectedOptions.map(generateMultiselectValue);
100
- } else {
101
- selectedOption = option;
102
- value = option.value;
103
- }
104
-
133
+ value = multiple ? [...(value ?? []), option.value] : option.value;
105
134
  closeAfterSelect();
106
135
  onchange?.(option);
107
136
  };
108
137
 
109
- const onClearAll = () => {
110
- if (multiple) {
111
- selectedOptions = [];
112
- value = [];
113
- } else {
114
- selectedOption = undefined;
115
- value = '';
116
- }
117
-
118
- onclear?.();
119
- };
120
-
121
138
  const onRemoveSelectedOption = (index: number) => {
122
139
  const newSelectedOptions = [...selectedOptions];
123
140
  newSelectedOptions.splice(index, 1);
124
- selectedOptions = newSelectedOptions;
125
- value = selectedOptions.map(generateMultiselectValue);
126
- };
127
-
128
- const onClearSearch = () => {
129
- searchText = '';
141
+ value = newSelectedOptions.map((option) => option.value);
130
142
  };
131
143
 
132
- const onHandleClickOutside = (event: MouseEvent) => {
133
- if (showListOptions && dropdownElement && !dropdownElement.contains(event.target as Node)) {
134
- showListOptions = false;
135
- }
136
- };
137
-
138
- const activeOptionScroll: Action<HTMLElement, boolean> = (node, isActive) => {
139
- $effect(() => {
140
- if (isActive) {
141
- node.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
142
- }
143
- });
144
+ const onClearAll = () => {
145
+ value = multiple ? [] : '';
146
+ onclear?.();
144
147
  };
145
148
 
146
- const autoDirection: Action = () => {
147
- $effect(() => {
148
- const rect = dropdownElement.getBoundingClientRect();
149
- const viewportHeight = window.innerHeight;
150
- const footer = 300;
149
+ const onClearSearch = () => (searchText = '');
151
150
 
152
- showInTopPosition = rect.bottom + footer > viewportHeight;
153
- });
154
- };
151
+ const onFocusDropdownContainer = () => dropdownContainer && dropdownContainer.focus();
155
152
 
156
- onMount(() => {
157
- if (!value) return;
153
+ const onToggleListOptions = () => (showListOptions = !showListOptions);
158
154
 
159
- if (multiple && Array.isArray(value)) {
160
- selectedOptions = value.map(generateSelectOption);
161
- } else if (!multiple && typeof value === 'string') {
162
- selectedOption = generateSelectOption(value);
155
+ const onHandleClickOutside = (event: MouseEvent) => {
156
+ if (showListOptions && dropdown && !dropdown.contains(event.target as Node)) {
157
+ showListOptions = false;
163
158
  }
164
- });
159
+ };
165
160
  </script>
166
161
 
167
- <svelte:window on:click={onHandleClickOutside} />
162
+ <svelte:window onclick={onHandleClickOutside} />
168
163
 
169
- <div class="form-container">
164
+ <div class="form-control">
170
165
  {#if label}
171
- <Label {label} {required} {optional} {infoTooltip} />
166
+ <div
167
+ role="button"
168
+ aria-disabled="true"
169
+ class="label-wrapper"
170
+ onclick={onFocusDropdownContainer}
171
+ onkeypress={onFocusDropdownContainer}
172
+ >
173
+ <Label {label} {required} {optional} {infoTooltip} />
174
+ </div>
172
175
  {/if}
173
176
 
174
177
  <div
175
178
  class={['dropdown', disabled ? 'disabled' : [isInvalid && 'invalid', isValid && 'valid']]}
176
- bind:this={dropdownElement}
179
+ bind:this={dropdown}
177
180
  >
178
181
  <section
179
- class="dropdown-container"
180
182
  role="button"
181
183
  tabindex="0"
182
- onclick={() => (showListOptions = !showListOptions)}
183
- onkeypress={() => {}}
184
+ class="dropdown-container"
185
+ onclick={onToggleListOptions}
186
+ onkeypress={onToggleListOptions}
187
+ bind:this={dropdownContainer}
184
188
  >
185
189
  <div class="dropdown-container-selected-options">
186
190
  {#if selectedOption || selectedOptions.length}
@@ -290,7 +294,7 @@
290
294
  {/snippet}
291
295
 
292
296
  <style>
293
- .form-container {
297
+ .form-control {
294
298
  --primary-color: #472aff;
295
299
  --white: #fff;
296
300
  --black: #000;
@@ -308,11 +312,17 @@
308
312
  width: 100%;
309
313
  gap: 8px;
310
314
  font-size: 14px;
311
- }
312
315
 
313
- .form-container > .form-message-error {
314
- font-size: 12px;
315
- color: var(--error);
316
+ > .label-wrapper {
317
+ width: fit-content;
318
+ word-break: break-word;
319
+ cursor: default;
320
+ }
321
+
322
+ > .form-message-error {
323
+ font-size: 12px;
324
+ color: var(--error);
325
+ }
316
326
  }
317
327
 
318
328
  .dropdown {
@@ -352,7 +362,7 @@
352
362
  }
353
363
 
354
364
  .dropdown.invalid {
355
- border-color: var(--error);
365
+ border: 1px solid var(--error);
356
366
  }
357
367
 
358
368
  .dropdown.invalid:focus-within {
@@ -362,11 +372,12 @@
362
372
  .dropdown > .dropdown-container {
363
373
  display: grid;
364
374
  grid-template-columns: 1fr auto;
365
- min-height: 42px;
366
- padding: 8px;
375
+ min-height: 36px;
376
+ padding: 8px 16px;
367
377
  gap: 8px;
368
378
  align-items: center;
369
379
  cursor: pointer;
380
+ outline: none;
370
381
  border: none;
371
382
  border-radius: 8px;
372
383
  background: var(--white);
@@ -386,13 +397,13 @@
386
397
  }
387
398
 
388
399
  > .dropdown-container-selected-option {
389
- display: grid;
390
- grid-template-columns: auto auto;
391
- padding: 8px;
392
- gap: 8px;
400
+ display: flex;
401
+ padding: 0px 5px;
402
+ gap: 4px;
393
403
  align-items: center;
394
404
  cursor: default;
395
- border-radius: 8px;
405
+ border-radius: 4px;
406
+ border: 1px solid var(--gray-2);
396
407
  background: var(--gray-1);
397
408
  }
398
409
  }
@@ -442,11 +453,13 @@
442
453
  min-height: 40px;
443
454
  padding: 0px 8px;
444
455
  cursor: pointer;
456
+ outline: none;
445
457
  border: none;
446
458
  background: transparent;
447
459
  }
448
460
  }
449
461
 
462
+ .dropdown-list-option:not(.active) > button:focus-visible,
450
463
  .dropdown-list-option:not(.active) > button:hover {
451
464
  background: var(--gray-1);
452
465
  }
@@ -457,12 +470,14 @@
457
470
  }
458
471
 
459
472
  .clear-button {
473
+ outline: none;
460
474
  border: none;
461
475
  color: var(--gray-4);
462
476
  background: transparent;
463
477
  transition: color 0.2s ease-in-out;
464
478
  }
465
479
 
480
+ .clear-button:focus-visible,
466
481
  .clear-button:hover {
467
482
  cursor: pointer;
468
483
  color: var(--black);
@@ -1,36 +1,71 @@
1
1
  <script lang="ts">
2
+ import Label from '../Label.svelte';
3
+
2
4
  interface ToggleProps {
3
5
  id?: string;
4
6
  checked?: boolean;
5
7
  disabled?: boolean;
8
+ label?: string;
9
+ infoTooltip?: string;
10
+ vertical?: boolean;
6
11
  onchange?: (event: Event) => void;
7
12
  }
8
13
 
9
- let { id = '', checked = $bindable(false), disabled = false, onchange }: ToggleProps = $props();
14
+ let {
15
+ id = '',
16
+ checked = $bindable(false),
17
+ disabled = false,
18
+ label,
19
+ infoTooltip,
20
+ vertical,
21
+ onchange
22
+ }: ToggleProps = $props();
10
23
  </script>
11
24
 
12
- <label class="toggle-container">
13
- <input
14
- type="checkbox"
15
- class="toggle-input"
16
- bind:checked
17
- {disabled}
18
- {id}
19
- {onchange}
20
- role="switch"
21
- />
22
- <div class={['toggle-slider', checked && 'checked', disabled && 'disabled']}>
23
- <span class="material-icons-outlined toggle-icon">
24
- {#if checked}
25
- done
26
- {:else}
27
- close
28
- {/if}
29
- </span>
30
- </div>
31
- </label>
25
+ <div class="container" class:vertical>
26
+ <label class="toggle-container">
27
+ <input
28
+ type="checkbox"
29
+ class="toggle-input"
30
+ bind:checked
31
+ {disabled}
32
+ {id}
33
+ {onchange}
34
+ role="switch"
35
+ />
36
+ <div class={['toggle-slider', checked && 'checked', disabled && 'disabled']}>
37
+ <span class="material-icons-outlined toggle-icon">
38
+ {#if checked}
39
+ done
40
+ {:else}
41
+ close
42
+ {/if}
43
+ </span>
44
+ </div>
45
+ </label>
46
+ <Label {label} {infoTooltip} />
47
+ </div>
32
48
 
33
49
  <style>
50
+ .container {
51
+ display: flex;
52
+ flex-direction: row;
53
+ align-items: center;
54
+ gap: 8px;
55
+ }
56
+
57
+ .container.vertical {
58
+ flex-direction: column;
59
+ align-items: flex-start;
60
+ }
61
+
62
+ .container.vertical .toggle-container {
63
+ order: 2;
64
+ }
65
+
66
+ .container.vertical label {
67
+ order: 1;
68
+ }
34
69
  .toggle-container {
35
70
  position: relative;
36
71
  }
@@ -2,6 +2,9 @@ interface ToggleProps {
2
2
  id?: string;
3
3
  checked?: boolean;
4
4
  disabled?: boolean;
5
+ label?: string;
6
+ infoTooltip?: string;
7
+ vertical?: boolean;
5
8
  onchange?: (event: Event) => void;
6
9
  }
7
10
  declare const Toggle: import("svelte").Component<ToggleProps, {}, "checked">;
@@ -15,29 +15,68 @@
15
15
  userName?: string;
16
16
  profileUrl?: string;
17
17
  hideLoader?: boolean;
18
- menu?: Snippet;
18
+ menu?: Snippet<[showMenu: boolean]>;
19
+ waffle?: Snippet<[showWaffle: boolean]>;
19
20
  }
20
21
 
21
22
  let {
22
- title = 'Default Title',
23
+ title = '',
23
24
  homeUrl = '/',
24
25
  hideAccount,
25
- hideHelp,
26
- hideNotification,
27
- accountName = 'Company Name',
28
- userName = 'User Name',
26
+ accountName = '',
27
+ userName = '',
29
28
  profileUrl = '/profile',
30
29
  hideLoader,
31
- menu
30
+ menu,
31
+ waffle
32
32
  }: HeaderProps = $props();
33
+
34
+ let showWaffle = $state(false);
35
+ let showMenu = $state(false);
36
+
37
+ const toggleWaffle = () => {
38
+ showWaffle = !showWaffle;
39
+ showMenu = false;
40
+ };
41
+
42
+ const toggleMenu = () => {
43
+ showMenu = !showMenu;
44
+ showWaffle = false;
45
+ };
46
+
47
+ const handleKeydown = (event: KeyboardEvent) => {
48
+ if (event.key === 'Escape' && (showWaffle || showMenu)) {
49
+ showWaffle = false;
50
+ showMenu = false;
51
+ }
52
+ };
33
53
  </script>
34
54
 
55
+ <svelte:window onkeydown={handleKeydown} />
56
+
35
57
  <div class="header-container">
36
58
  <nav class="header-section">
37
59
  {#if !hideLoader}
38
60
  <HeaderLoader />
39
61
  {/if}
40
- {@render menu?.()}
62
+ {#if waffle}
63
+ <button
64
+ type="button"
65
+ class={[showWaffle && 'active', 'header-button']}
66
+ onclick={toggleWaffle}
67
+ aria-label="Waffle Component"
68
+ aria-expanded={showWaffle}
69
+ >
70
+ <span class="material-icons icon-span">apps</span>
71
+ </button>
72
+ {@render waffle(showWaffle)}
73
+ {/if}
74
+ {#if menu}
75
+ <button type="button" class="header-button" onclick={toggleMenu} aria-label="menu button">
76
+ <span class="material-icons icon-span menu-icon">menu</span>
77
+ </button>
78
+ {@render menu(showMenu)}
79
+ {/if}
41
80
  <a href={homeUrl} title="Home">
42
81
  <HeaderLogo />
43
82
  </a>
@@ -47,16 +86,6 @@
47
86
  </nav>
48
87
 
49
88
  <nav class="header-section">
50
- {#if !hideHelp}
51
- <button class="header-btn material-icons-outlined" aria-labelledby="help-button">
52
- help_outline
53
- </button>
54
- {/if}
55
- {#if !hideNotification}
56
- <button class="header-btn material-icons-outlined" aria-labelledby="notifications-button">
57
- notifications
58
- </button>
59
- {/if}
60
89
  {#if !hideAccount}
61
90
  <a href={profileUrl} title="Profile">
62
91
  <HeaderAccount {accountName} {userName} />
@@ -66,6 +95,38 @@
66
95
  </div>
67
96
 
68
97
  <style>
98
+ .header-button {
99
+ display: flex;
100
+ justify-content: center;
101
+ align-items: center;
102
+ border-radius: 50%;
103
+ background: transparent;
104
+ z-index: 40;
105
+ cursor: pointer;
106
+ border: none;
107
+ width: 40px;
108
+ height: 40px;
109
+ transition: background-color 0.2s ease-in-out;
110
+ }
111
+
112
+ .header-button:hover {
113
+ background: #e0e5e8;
114
+ }
115
+
116
+ .header-button.active {
117
+ background: #fff;
118
+ color: #374151;
119
+ }
120
+
121
+ .header-button .menu-icon {
122
+ transform: scale(1.26);
123
+ }
124
+
125
+ .icon-span {
126
+ font-size: 24px;
127
+ color: #6b7180;
128
+ }
129
+
69
130
  .header-container {
70
131
  display: flex;
71
132
  gap: 24px;
@@ -75,6 +136,8 @@
75
136
  background: #fff;
76
137
  height: 80px;
77
138
  border-bottom: 3px solid #aeb1b9;
139
+ position: relative;
140
+ z-index: 50;
78
141
  }
79
142
 
80
143
  .header-section {
@@ -88,10 +151,4 @@
88
151
  font-size: 24px;
89
152
  font-weight: 600;
90
153
  }
91
-
92
- .header-btn {
93
- border: none;
94
- background: transparent;
95
- color: #6b7180;
96
- }
97
154
  </style>
@@ -9,7 +9,8 @@ interface HeaderProps {
9
9
  userName?: string;
10
10
  profileUrl?: string;
11
11
  hideLoader?: boolean;
12
- menu?: Snippet;
12
+ menu?: Snippet<[showMenu: boolean]>;
13
+ waffle?: Snippet<[showWaffle: boolean]>;
13
14
  }
14
15
  declare const Header: import("svelte").Component<HeaderProps, {}, "">;
15
16
  type Header = ReturnType<typeof Header>;
@@ -19,9 +19,7 @@
19
19
  <section class={['home-container', hasSingleItem && 'centered', hasManyItems && 'grid']}>
20
20
  {#each homeItems as homeItem}
21
21
  <a href={homeItem.url} class="home-item">
22
- <span>
23
- <img src={homeItem.icon} alt={homeItem.text} />
24
- </span>
22
+ <img src={homeItem.icon} alt={homeItem.text} />
25
23
  <div>
26
24
  <h2>{homeItem.text}</h2>
27
25
  <p>{homeItem.detail}</p>
@@ -82,15 +80,11 @@
82
80
  background: var(--black-1);
83
81
  transition: background 0.2s ease-in-out;
84
82
 
85
- > span {
86
- display: flex;
87
- align-items: center;
88
-
89
- > img {
90
- width: 40px;
91
- height: 40px;
92
- filter: invert(100%);
93
- }
83
+ > img {
84
+ width: 36px;
85
+ height: 36px;
86
+ margin: auto 0;
87
+ filter: invert(100%);
94
88
  }
95
89
 
96
90
  > div {