@origostudio/origo-design 0.0.49 → 0.0.50

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 (80) hide show
  1. package/CHANGELOG.md +4 -4
  2. package/dist/packages/angular-renderer/CHANGELOG.md +10 -0
  3. package/dist/packages/angular-renderer/package.json +3 -3
  4. package/dist/packages/angular-renderer/src/components/primitives/autocomplete/autocomplete.component.d.ts +99 -0
  5. package/dist/packages/angular-renderer/src/components/primitives/autocomplete/autocomplete.component.js +306 -0
  6. package/dist/packages/angular-renderer/src/components/primitives/autocomplete/autocomplete.component.js.map +1 -0
  7. package/dist/packages/angular-renderer/src/components/primitives/combobox/combobox.component.d.ts +69 -0
  8. package/dist/packages/angular-renderer/src/components/primitives/combobox/combobox.component.js +276 -0
  9. package/dist/packages/angular-renderer/src/components/primitives/combobox/combobox.component.js.map +1 -0
  10. package/dist/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.d.ts +110 -0
  11. package/dist/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.js +323 -0
  12. package/dist/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.js.map +1 -0
  13. package/dist/packages/angular-renderer/src/components/primitives/select/select.component.d.ts +80 -19
  14. package/dist/packages/angular-renderer/src/components/primitives/select/select.component.js +201 -24
  15. package/dist/packages/angular-renderer/src/components/primitives/select/select.component.js.map +1 -1
  16. package/dist/packages/angular-renderer/src/components/primitives/select/selection-utils.d.ts +9 -0
  17. package/dist/packages/angular-renderer/src/components/primitives/select/selection-utils.js +69 -0
  18. package/dist/packages/angular-renderer/src/components/primitives/select/selection-utils.js.map +1 -0
  19. package/dist/packages/angular-renderer/src/index.d.ts +3 -0
  20. package/dist/packages/angular-renderer/src/index.js +3 -0
  21. package/dist/packages/angular-renderer/src/index.js.map +1 -1
  22. package/dist/packages/angular-renderer/src/lib/primitives.provider.js +6 -0
  23. package/dist/packages/angular-renderer/src/lib/primitives.provider.js.map +1 -1
  24. package/dist/packages/cli/CHANGELOG.md +4 -0
  25. package/dist/packages/cli/package.json +2 -2
  26. package/dist/packages/cli/package.json.bak +2 -2
  27. package/dist/packages/core/CHANGELOG.md +4 -0
  28. package/dist/packages/core/package.json +1 -1
  29. package/dist/packages/core/package.json.bak +1 -1
  30. package/dist/packages/design-tokens/CHANGELOG.md +4 -0
  31. package/dist/packages/design-tokens/package.json +1 -1
  32. package/dist/packages/playground/browser/{chunk-DgLZ-dIi.js → chunk--xdAQ9AM.js} +1 -1
  33. package/dist/packages/playground/browser/{chunk-CpaI4VGh.js → chunk-BDvJAz2Z.js} +1 -1
  34. package/dist/packages/playground/browser/{chunk-Cb2ALrKC.js → chunk-BfTgubd-.js} +1 -1
  35. package/dist/packages/playground/browser/{chunk-BlCRDoSd.js → chunk-C9TW72xp.js} +1 -1
  36. package/dist/packages/playground/browser/{chunk-CCE_vAje.js → chunk-CCN2KoWz.js} +1 -1
  37. package/dist/packages/playground/browser/{chunk-r4-UbXQJ.js → chunk-COiPNsUk.js} +1 -1
  38. package/dist/packages/playground/browser/{chunk-DoYDPF2-.js → chunk-CR9pdAu4.js} +1 -1
  39. package/dist/packages/playground/browser/{chunk-B_swdRuc.js → chunk-CW1H0x80.js} +1 -1
  40. package/dist/packages/playground/browser/{chunk-XTbjIUbx.js → chunk-CcRAf-SI.js} +1 -1
  41. package/dist/packages/playground/browser/{chunk-BbeNQQ9Z.js → chunk-CeWsvybE.js} +1 -1
  42. package/dist/packages/playground/browser/{chunk-C8l8Pst_.js → chunk-DB3lTZW4.js} +1 -1
  43. package/dist/packages/playground/browser/{chunk-lFsGUZ3A.js → chunk-DBaAs95p.js} +1 -1
  44. package/dist/packages/playground/browser/{chunk-B8mwHWgW.js → chunk-UVpCaxmD.js} +1 -1
  45. package/dist/packages/playground/browser/{chunk-DkxgAarv.js → chunk-rUmdxUDA.js} +1 -1
  46. package/dist/packages/playground/browser/{chunk-G1sQXMvG.js → chunk-wSEtHeaq.js} +1 -1
  47. package/dist/packages/playground/browser/index.html +1 -1
  48. package/dist/packages/playground/browser/main-QKXZMSV7.js +737 -0
  49. package/package.json +1 -1
  50. package/packages/angular-renderer/CHANGELOG.md +10 -0
  51. package/packages/angular-renderer/package.json +3 -3
  52. package/packages/angular-renderer/src/components/primitives/autocomplete/autocomplete.component.html +83 -0
  53. package/packages/angular-renderer/src/components/primitives/autocomplete/autocomplete.component.scss +161 -0
  54. package/packages/angular-renderer/src/components/primitives/autocomplete/autocomplete.component.ts +338 -0
  55. package/packages/angular-renderer/src/components/primitives/combobox/combobox.component.html +90 -0
  56. package/packages/angular-renderer/src/components/primitives/combobox/combobox.component.scss +189 -0
  57. package/packages/angular-renderer/src/components/primitives/combobox/combobox.component.ts +281 -0
  58. package/packages/angular-renderer/src/components/primitives/icon-button/icon-button.component.html +1 -1
  59. package/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.html +150 -0
  60. package/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.scss +240 -0
  61. package/packages/angular-renderer/src/components/primitives/multi-select/multi-select.component.ts +362 -0
  62. package/packages/angular-renderer/src/components/primitives/select/select.component.html +92 -14
  63. package/packages/angular-renderer/src/components/primitives/select/select.component.scss +188 -13
  64. package/packages/angular-renderer/src/components/primitives/select/select.component.ts +242 -34
  65. package/packages/angular-renderer/src/components/primitives/select/selection-utils.ts +92 -0
  66. package/packages/angular-renderer/src/index.ts +3 -0
  67. package/packages/angular-renderer/src/lib/primitives.provider.ts +6 -0
  68. package/packages/cli/CHANGELOG.md +4 -0
  69. package/packages/cli/package.json +2 -2
  70. package/packages/core/CHANGELOG.md +4 -0
  71. package/packages/core/package.json +1 -1
  72. package/packages/design-tokens/CHANGELOG.md +4 -0
  73. package/packages/design-tokens/package.json +1 -1
  74. package/packages/devtools/CHANGELOG.md +10 -0
  75. package/packages/devtools/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  76. package/packages/devtools/package.json +3 -3
  77. package/packages/playground/CHANGELOG.md +4 -0
  78. package/packages/playground/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  79. package/packages/playground/package.json +3 -3
  80. package/dist/packages/playground/browser/main-D2PNGI4C.js +0 -734
@@ -0,0 +1,362 @@
1
+ import {
2
+ Component,
3
+ input,
4
+ model,
5
+ ChangeDetectionStrategy,
6
+ computed,
7
+ ViewEncapsulation,
8
+ inject,
9
+ effect,
10
+ untracked,
11
+ signal,
12
+ PLATFORM_ID,
13
+ HostListener,
14
+ ElementRef,
15
+ } from '@angular/core';
16
+ import { isPlatformBrowser } from '@angular/common';
17
+ import { InteractionContract } from '@origostudio/core';
18
+ import { OrigoAdapter } from '../../../adapters/web/adapter';
19
+ import { WebExperienceAdapterService } from '../../../adapters/web/experience-adapter.service';
20
+ import { NormalizedOption, normalizeOptions, filterOptions } from '../select/selection-utils';
21
+
22
+ export interface MultiSelectProps {
23
+ id?: string;
24
+ value?: Array<unknown>;
25
+ options: Array<unknown>;
26
+ optionLabel?: string;
27
+ optionValue?: string;
28
+ placeholder?: string;
29
+ filter?: boolean;
30
+ filterBy?: string;
31
+ filterPlaceholder?: string;
32
+ showToggleAll?: boolean;
33
+ maxSelectedLabels?: number;
34
+ selectedItemsLabel?: string;
35
+ display?: 'comma' | 'chip';
36
+ clearable?: boolean;
37
+ virtualScroll?: boolean;
38
+ itemSize?: number;
39
+ loading?: boolean;
40
+ disabled?: boolean;
41
+ readonly?: boolean;
42
+ required?: boolean;
43
+ invalid?: boolean;
44
+ errorText?: string;
45
+ helpText?: string;
46
+ variant?: 'outlined' | 'filled';
47
+ fluid?: boolean;
48
+ 'aria-label'?: string;
49
+ 'aria-describedby'?: string;
50
+ permissions?: Record<string, string>;
51
+ rules?: Record<string, unknown>;
52
+ metadata?: Record<string, unknown>;
53
+ }
54
+
55
+ @Component({
56
+ selector: 'origo-multi-select',
57
+ standalone: true,
58
+ templateUrl: './multi-select.component.html',
59
+ styleUrls: ['./multi-select.component.scss'],
60
+ changeDetection: ChangeDetectionStrategy.OnPush,
61
+ encapsulation: ViewEncapsulation.ShadowDom,
62
+ host: {
63
+ '[class.origo-multi-select]': 'true',
64
+ '[attr.data-testid]': 'contract().id ?? ""',
65
+ '[class.origo-multi-select--fluid]': 'computedFluid()',
66
+ '[class.origo-multi-select--outlined]': 'computedVariant() === "outlined"',
67
+ '[class.origo-multi-select--filled]': 'computedVariant() === "filled"',
68
+ '[class.origo-multi-select--invalid]': 'computedInvalid()',
69
+ '[class.origo-multi-select--disabled]': 'computedDisabled()',
70
+ '[class.origo-multi-select--readonly]': 'computedReadonly()',
71
+ },
72
+ })
73
+ export class MultiSelectComponent implements OrigoAdapter<MultiSelectProps> {
74
+ static readonly contractSchema = {
75
+ permissions: 'object',
76
+ rules: 'object',
77
+ metadata: 'object',
78
+ options: 'array',
79
+ value: 'array',
80
+ optionLabel: 'string',
81
+ optionValue: 'string',
82
+ placeholder: 'string',
83
+ filter: 'boolean',
84
+ filterBy: 'string',
85
+ filterPlaceholder: 'string',
86
+ showToggleAll: 'boolean',
87
+ maxSelectedLabels: 'number',
88
+ selectedItemsLabel: 'string',
89
+ display: 'string',
90
+ clearable: 'boolean',
91
+ virtualScroll: 'boolean',
92
+ itemSize: 'number',
93
+ loading: 'boolean',
94
+ disabled: 'boolean',
95
+ readonly: 'boolean',
96
+ required: 'boolean',
97
+ invalid: 'boolean',
98
+ errorText: 'string',
99
+ helpText: 'string',
100
+ variant: 'string',
101
+ fluid: 'boolean',
102
+ 'aria-label': 'string',
103
+ 'aria-describedby': 'string',
104
+ };
105
+ static readonly strictContract = false;
106
+
107
+ contract = input.required<InteractionContract<MultiSelectProps>>();
108
+ value = model<Array<unknown>>([]);
109
+
110
+ isOpen = signal<boolean>(false);
111
+ filterQuery = signal<string>('');
112
+ activeIndex = signal<number>(-1);
113
+
114
+ computedNormalizedOptions = computed(() => {
115
+ const props = this.contract().props;
116
+ return normalizeOptions(props?.options, props?.optionLabel, props?.optionValue);
117
+ });
118
+
119
+ filteredOptions = computed(() => {
120
+ const opts = this.computedNormalizedOptions();
121
+ if (!this.computedFilter() && !this.filterQuery()) {
122
+ return opts;
123
+ }
124
+ return filterOptions(opts, this.filterQuery(), 'contains');
125
+ });
126
+
127
+ computedSelectedOptions = computed(() => {
128
+ const current = new Set(this.value());
129
+ return this.computedNormalizedOptions().filter(opt => current.has(opt.value));
130
+ });
131
+
132
+ computedPlaceholder = computed(() => this.contract().props?.placeholder ?? 'Select options');
133
+ computedDisabled = computed(() => !!this.contract().props?.disabled);
134
+ computedReadonly = computed(() => !!this.contract().props?.readonly);
135
+ computedRequired = computed(() => !!this.contract().props?.required);
136
+ computedInvalid = computed(() => !!this.contract().props?.invalid);
137
+ computedFilter = computed(() => !!this.contract().props?.filter);
138
+ computedFilterPlaceholder = computed(
139
+ () => this.contract().props?.filterPlaceholder ?? 'Search...'
140
+ );
141
+ computedShowToggleAll = computed(() => this.contract().props?.showToggleAll ?? true);
142
+ computedClearable = computed(() => !!this.contract().props?.clearable);
143
+ computedDisplay = computed(() => this.contract().props?.display ?? 'comma');
144
+ computedMaxSelectedLabels = computed(() => this.contract().props?.maxSelectedLabels ?? 3);
145
+ computedSelectedItemsLabel = computed(
146
+ () => this.contract().props?.selectedItemsLabel ?? '{0} items selected'
147
+ );
148
+ computedVariant = computed(() => this.contract().props?.variant ?? 'outlined');
149
+ computedFluid = computed(() => !!this.contract().props?.fluid);
150
+ computedErrorText = computed(() => this.contract().props?.errorText ?? '');
151
+ computedHelpText = computed(() => this.contract().props?.helpText ?? '');
152
+
153
+ computedAriaLabel = computed(() => {
154
+ const label = this.contract().props?.['aria-label'];
155
+ if (label !== undefined && label !== null && String(label).trim() !== '') {
156
+ return String(label);
157
+ }
158
+ return this.computedPlaceholder();
159
+ });
160
+
161
+ computedAriaDescribedBy = computed(() => {
162
+ const parts: string[] = [];
163
+ const desc = this.contract().props?.['aria-describedby'];
164
+ if (desc) parts.push(String(desc));
165
+ if (this.computedErrorText()) parts.push(`${this.contract().id}-error`);
166
+ if (this.computedHelpText()) parts.push(`${this.contract().id}-help`);
167
+ return parts.length > 0 ? parts.join(' ') : undefined;
168
+ });
169
+
170
+ displaySummary = computed(() => {
171
+ const selected = this.computedSelectedOptions();
172
+ const count = selected.length;
173
+ if (count === 0) return '';
174
+ const max = this.computedMaxSelectedLabels();
175
+ if (count <= max) {
176
+ return selected.map(s => s.label).join(', ');
177
+ }
178
+ const template = this.computedSelectedItemsLabel();
179
+ return template.replace('{0}', String(count));
180
+ });
181
+
182
+ isAllSelected = computed(() => {
183
+ const filtered = this.filteredOptions().filter(o => !o.disabled);
184
+ if (filtered.length === 0) return false;
185
+ const current = new Set(this.value());
186
+ return filtered.every(o => current.has(o.value));
187
+ });
188
+
189
+ private experienceAdapter = inject(WebExperienceAdapterService);
190
+ private platformId = inject(PLATFORM_ID);
191
+ private elementRef = inject(ElementRef);
192
+
193
+ constructor() {
194
+ effect(() => {
195
+ const contractVal = this.contract().props?.value;
196
+ untracked(() => {
197
+ if (Array.isArray(contractVal)) {
198
+ const current = this.value();
199
+ if (
200
+ current.length !== contractVal.length ||
201
+ !current.every((v, i) => v === contractVal[i])
202
+ ) {
203
+ this.value.set([...contractVal]);
204
+ }
205
+ }
206
+ });
207
+ });
208
+ }
209
+
210
+ toggleOpen() {
211
+ if (this.computedDisabled() || this.computedReadonly()) return;
212
+ const next = !this.isOpen();
213
+ this.isOpen.set(next);
214
+ if (next) {
215
+ this.filterQuery.set('');
216
+ this.activeIndex.set(0);
217
+ }
218
+ }
219
+
220
+ isSelected(opt: NormalizedOption): boolean {
221
+ return this.value().includes(opt.value);
222
+ }
223
+
224
+ toggleOption(optionValue: unknown) {
225
+ if (this.computedDisabled() || this.computedReadonly()) return;
226
+ const current = [...this.value()];
227
+ const idx = current.indexOf(optionValue);
228
+ if (idx >= 0) {
229
+ current.splice(idx, 1);
230
+ } else {
231
+ current.push(optionValue);
232
+ }
233
+ this.value.set(current);
234
+ this.experienceAdapter.updateState(this.contract().id, 'value', current);
235
+ }
236
+
237
+ toggleAll() {
238
+ if (this.computedDisabled() || this.computedReadonly()) return;
239
+ const filtered = this.filteredOptions().filter(o => !o.disabled);
240
+ let nextValue: Array<unknown>;
241
+
242
+ if (this.isAllSelected()) {
243
+ const filteredSet = new Set(filtered.map(o => o.value));
244
+ nextValue = this.value().filter(v => !filteredSet.has(v));
245
+ } else {
246
+ const set = new Set(this.value());
247
+ for (const opt of filtered) {
248
+ set.add(opt.value);
249
+ }
250
+ nextValue = Array.from(set);
251
+ }
252
+
253
+ this.value.set(nextValue);
254
+ this.experienceAdapter.updateState(this.contract().id, 'value', nextValue);
255
+ }
256
+
257
+ removeOption(optionValue: unknown, event?: Event) {
258
+ event?.stopPropagation();
259
+ if (this.computedDisabled() || this.computedReadonly()) return;
260
+ const next = this.value().filter(v => v !== optionValue);
261
+ this.value.set(next);
262
+ this.experienceAdapter.updateState(this.contract().id, 'value', next);
263
+ }
264
+
265
+ clear(event?: Event) {
266
+ if (this.computedDisabled() || this.computedReadonly()) return;
267
+ event?.stopPropagation();
268
+ this.value.set([]);
269
+ this.experienceAdapter.updateState(this.contract().id, 'value', []);
270
+ }
271
+
272
+ onFilterInput(event: Event) {
273
+ const target = event.target as HTMLInputElement;
274
+ this.filterQuery.set(target.value);
275
+ this.activeIndex.set(0);
276
+ }
277
+
278
+ onKeydown(event: KeyboardEvent) {
279
+ if (this.computedDisabled() || this.computedReadonly()) return;
280
+
281
+ const opts = this.filteredOptions();
282
+ const count = opts.length;
283
+
284
+ switch (event.key) {
285
+ case 'ArrowDown': {
286
+ event.preventDefault();
287
+ if (!this.isOpen()) {
288
+ this.isOpen.set(true);
289
+ this.activeIndex.set(0);
290
+ } else if (count > 0) {
291
+ const next = (this.activeIndex() + 1) % count;
292
+ this.activeIndex.set(next);
293
+ }
294
+ break;
295
+ }
296
+ case 'ArrowUp': {
297
+ event.preventDefault();
298
+ if (!this.isOpen()) {
299
+ this.isOpen.set(true);
300
+ this.activeIndex.set(count - 1);
301
+ } else if (count > 0) {
302
+ const prev = (this.activeIndex() - 1 + count) % count;
303
+ this.activeIndex.set(prev);
304
+ }
305
+ break;
306
+ }
307
+ case 'Enter':
308
+ case ' ': {
309
+ const target = event.target as HTMLElement;
310
+ if (event.key === ' ' && target && target.tagName === 'INPUT') {
311
+ return;
312
+ }
313
+ event.preventDefault();
314
+ if (this.isOpen()) {
315
+ const idx = this.activeIndex();
316
+ if (idx >= 0 && idx < count && !opts[idx].disabled) {
317
+ this.toggleOption(opts[idx].value);
318
+ }
319
+ } else {
320
+ this.isOpen.set(true);
321
+ }
322
+ break;
323
+ }
324
+ case 'Escape': {
325
+ if (this.isOpen()) {
326
+ event.preventDefault();
327
+ this.isOpen.set(false);
328
+ }
329
+ break;
330
+ }
331
+ case 'Tab': {
332
+ if (this.isOpen()) {
333
+ this.isOpen.set(false);
334
+ }
335
+ break;
336
+ }
337
+ case 'Home': {
338
+ if (this.isOpen() && count > 0) {
339
+ event.preventDefault();
340
+ this.activeIndex.set(0);
341
+ }
342
+ break;
343
+ }
344
+ case 'End': {
345
+ if (this.isOpen() && count > 0) {
346
+ event.preventDefault();
347
+ this.activeIndex.set(count - 1);
348
+ }
349
+ break;
350
+ }
351
+ }
352
+ }
353
+
354
+ @HostListener('document:click', ['$event'])
355
+ onDocumentClick(event: MouseEvent) {
356
+ if (!isPlatformBrowser(this.platformId)) return;
357
+ const clickedInside = this.elementRef.nativeElement.contains(event.target as Node);
358
+ if (!clickedInside && this.isOpen()) {
359
+ this.isOpen.set(false);
360
+ }
361
+ }
362
+ }
@@ -1,15 +1,93 @@
1
- <select
2
- [id]="contract().id"
3
- [disabled]="computedDisabled()"
4
- [required]="computedRequired()"
5
- [attr.aria-label]="computedAriaLabel()"
6
- [attr.aria-describedby]="computedAriaDescribedBy()"
7
- (change)="onChange($event)"
8
- >
9
- @if (computedPlaceholder()) {
10
- <option value="" disabled selected hidden>{{ computedPlaceholder() }}</option>
1
+ <div class="origo-select__wrapper">
2
+ <div class="origo-select__control">
3
+ <button
4
+ type="button"
5
+ class="origo-select__trigger"
6
+ [id]="contract().id + '-trigger'"
7
+ role="combobox"
8
+ [attr.aria-expanded]="isOpen()"
9
+ [attr.aria-haspopup]="'listbox'"
10
+ [attr.aria-controls]="isOpen() ? contract().id + '-listbox' : null"
11
+ [attr.aria-activedescendant]="
12
+ isOpen() && activeIndex() >= 0 ? contract().id + '-option-' + activeIndex() : null
13
+ "
14
+ [attr.aria-invalid]="computedInvalid() ? 'true' : null"
15
+ [attr.aria-required]="computedRequired() ? 'true' : null"
16
+ [attr.aria-label]="computedAriaLabel()"
17
+ [attr.aria-describedby]="computedAriaDescribedBy()"
18
+ [disabled]="computedDisabled()"
19
+ (click)="toggleOpen()"
20
+ (keydown)="onKeydown($event)"
21
+ >
22
+ <span class="origo-select__label" [class.origo-select__label--placeholder]="!selectedLabel()">
23
+ {{ selectedLabel() || computedPlaceholder() }}
24
+ </span>
25
+ <span class="origo-select__icons">
26
+ @if (computedClearable() && value() && !computedDisabled() && !computedReadonly()) {
27
+ <span
28
+ class="origo-select__clear"
29
+ role="button"
30
+ tabindex="0"
31
+ aria-label="Clear selection"
32
+ (click)="clear($event)"
33
+ (keydown.enter)="clear($event)"
34
+ >✕</span
35
+ >
36
+ }
37
+ <span class="origo-select__arrow" aria-hidden="true">▼</span>
38
+ </span>
39
+ </button>
40
+ </div>
41
+
42
+ @if (isOpen()) {
43
+ <div
44
+ class="origo-select__dropdown"
45
+ [id]="contract().id + '-listbox'"
46
+ role="listbox"
47
+ [attr.aria-label]="computedAriaLabel()"
48
+ >
49
+ @if (computedFilter()) {
50
+ <div class="origo-select__filter-container">
51
+ <input
52
+ type="text"
53
+ class="origo-select__filter-input"
54
+ [placeholder]="computedFilterPlaceholder()"
55
+ [value]="filterQuery()"
56
+ (input)="onFilterInput($event)"
57
+ (keydown)="onKeydown($event)"
58
+ aria-label="Filter options"
59
+ />
60
+ </div>
61
+ }
62
+ <ul class="origo-select__options">
63
+ @for (opt of filteredOptions(); track opt.value; let idx = $index) {
64
+ @if (opt.group && (idx === 0 || filteredOptions()[idx - 1].group !== opt.group)) {
65
+ <li class="origo-select__group-label" role="presentation">{{ opt.group }}</li>
66
+ }
67
+ <li
68
+ class="origo-select__option"
69
+ [class.origo-select__option--selected]="opt.value === value()"
70
+ [class.origo-select__option--active]="activeIndex() === idx"
71
+ [class.origo-select__option--disabled]="opt.disabled"
72
+ [id]="contract().id + '-option-' + idx"
73
+ role="option"
74
+ [attr.aria-selected]="opt.value === value()"
75
+ [attr.aria-disabled]="opt.disabled ? 'true' : null"
76
+ (click)="!opt.disabled && selectOption(opt.value)"
77
+ >
78
+ {{ opt.label }}
79
+ </li>
80
+ } @empty {
81
+ <li class="origo-select__no-options" role="presentation">No options found</li>
82
+ }
83
+ </ul>
84
+ </div>
11
85
  }
12
- @for (option of computedOptions(); track option.value) {
13
- <option [value]="option.value" [selected]="value() === option.value">{{ option.label }}</option>
14
- }
15
- </select>
86
+ </div>
87
+
88
+ @if (computedErrorText()) {
89
+ <div class="origo-select__error" [id]="contract().id + '-error'">{{ computedErrorText() }}</div>
90
+ }
91
+ @if (computedHelpText()) {
92
+ <div class="origo-select__help" [id]="contract().id + '-help'">{{ computedHelpText() }}</div>
93
+ }
@@ -1,26 +1,201 @@
1
1
  :host {
2
- display: block;
2
+ display: inline-block;
3
+ position: relative;
4
+ box-sizing: border-box;
5
+ font-family: var(--origo-typography-input-font-family);
6
+ font-size: var(--origo-typography-input-font-size);
7
+ font-weight: var(--origo-typography-input-font-weight);
8
+ line-height: var(--origo-typography-input-line-height);
3
9
  }
4
10
 
5
- select {
11
+ :host(.origo-select--fluid) {
6
12
  display: block;
7
13
  width: 100%;
8
- font-family: var(--origo-typography-input-font-family, inherit);
9
- font-size: var(--origo-typography-input-font-size, 1rem);
10
- color: var(--origo-color-text-primary, #333);
11
- background-color: var(--origo-color-surface-background, #fff);
12
- border: 1px solid var(--origo-color-border-default, #ccc);
13
- border-radius: var(--origo-radius-sm, 4px);
14
- padding: var(--origo-spacing-container-padding, 8px);
14
+ }
15
+
16
+ .origo-select__wrapper {
17
+ display: flex;
18
+ flex-direction: column;
19
+ position: relative;
20
+ width: 100%;
21
+ }
22
+
23
+ .origo-select__control {
24
+ display: flex;
25
+ width: 100%;
26
+ }
27
+
28
+ .origo-select__trigger {
15
29
  box-sizing: border-box;
30
+ width: 100%;
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: space-between;
34
+ background-color: var(--origo-color-surface-background);
35
+ color: var(--origo-color-text-primary);
36
+ padding: var(--origo-spacing-container-padding);
37
+ border: var(--origo-border-width-default) solid var(--origo-color-border-default);
38
+ border-radius: var(--origo-radius-sm);
39
+ box-shadow: var(--origo-shadow-sm);
40
+ font-family: inherit;
41
+ font-size: inherit;
42
+ text-align: start;
43
+ cursor: pointer;
44
+ transition: all var(--origo-duration-fast) ease-in-out;
16
45
 
17
- &:focus {
18
- outline: 2px solid var(--origo-color-focus, #005fcc);
19
- outline-offset: 2px;
46
+ &:hover:not(:disabled) {
47
+ border-color: var(--origo-color-border-hover);
48
+ }
49
+
50
+ &:active:not(:disabled) {
51
+ border-color: var(--origo-color-border-active);
52
+ }
53
+
54
+ &:focus,
55
+ &:focus-visible {
56
+ outline: var(--origo-border-width-thick) solid var(--origo-color-focus);
57
+ outline-offset: var(--origo-border-offset-negative);
58
+ border-color: var(--origo-color-focus);
20
59
  }
21
60
 
22
61
  &:disabled {
23
- opacity: var(--origo-opacity-disabled, 0.5);
24
62
  cursor: not-allowed;
63
+ opacity: var(--origo-opacity-disabled);
64
+ background-color: var(--origo-color-surface-disabled);
65
+ color: var(--origo-color-text-disabled);
25
66
  }
26
67
  }
68
+
69
+ :host(.origo-select--filled) .origo-select__trigger {
70
+ background-color: var(--origo-color-surface-subtle);
71
+ border: none;
72
+ }
73
+
74
+ :host(.origo-select--invalid) .origo-select__trigger {
75
+ border-color: var(--origo-color-text-danger);
76
+ }
77
+
78
+ .origo-select__label {
79
+ flex: 1;
80
+ overflow: hidden;
81
+ text-overflow: ellipsis;
82
+ white-space: nowrap;
83
+
84
+ &--placeholder {
85
+ color: var(--origo-color-text-muted);
86
+ }
87
+ }
88
+
89
+ .origo-select__icons {
90
+ display: inline-flex;
91
+ align-items: center;
92
+ gap: var(--origo-spacing-xs);
93
+ margin-inline-start: var(--origo-spacing-sm);
94
+ }
95
+
96
+ .origo-select__clear {
97
+ cursor: pointer;
98
+ color: var(--origo-color-text-muted);
99
+ font-size: var(--origo-typography-caption-font-size);
100
+
101
+ &:hover {
102
+ color: var(--origo-color-text-primary);
103
+ }
104
+ }
105
+
106
+ .origo-select__arrow {
107
+ color: var(--origo-color-text-muted);
108
+ font-size: var(--origo-typography-caption-font-size);
109
+ }
110
+
111
+ .origo-select__dropdown {
112
+ position: absolute;
113
+ top: 100%;
114
+ left: 0;
115
+ right: 0;
116
+ z-index: 1000;
117
+ margin-top: var(--origo-spacing-xs);
118
+ box-sizing: border-box;
119
+ background-color: var(--origo-color-surface-background);
120
+ border: var(--origo-border-width-default) solid var(--origo-color-border-default);
121
+ border-radius: var(--origo-radius-sm);
122
+ box-shadow: var(--origo-shadow-md);
123
+ max-height: 16rem;
124
+ overflow-y: auto;
125
+ }
126
+
127
+ .origo-select__filter-container {
128
+ padding: var(--origo-spacing-xs);
129
+ border-bottom: var(--origo-border-width-default) solid var(--origo-color-border-default);
130
+ }
131
+
132
+ .origo-select__filter-input {
133
+ box-sizing: border-box;
134
+ width: 100%;
135
+ padding: var(--origo-spacing-xs);
136
+ background-color: var(--origo-color-surface-background);
137
+ color: var(--origo-color-text-primary);
138
+ border: var(--origo-border-width-default) solid var(--origo-color-border-default);
139
+ border-radius: var(--origo-radius-sm);
140
+ font-family: inherit;
141
+ font-size: inherit;
142
+
143
+ &:focus {
144
+ outline: var(--origo-border-width-thick) solid var(--origo-color-focus);
145
+ outline-offset: var(--origo-border-offset-negative);
146
+ }
147
+ }
148
+
149
+ .origo-select__options {
150
+ list-style: none;
151
+ margin: 0;
152
+ padding: 0;
153
+ }
154
+
155
+ .origo-select__group-label {
156
+ padding: var(--origo-spacing-xs) var(--origo-spacing-container-padding);
157
+ font-weight: bold;
158
+ font-size: var(--origo-typography-caption-font-size);
159
+ color: var(--origo-color-text-muted);
160
+ background-color: var(--origo-color-surface-subtle);
161
+ }
162
+
163
+ .origo-select__option {
164
+ padding: var(--origo-spacing-container-padding);
165
+ color: var(--origo-color-text-primary);
166
+ cursor: pointer;
167
+ transition: background-color var(--origo-duration-fast) ease-in-out;
168
+
169
+ &:hover,
170
+ &--active {
171
+ background-color: var(--origo-color-surface-subtle);
172
+ }
173
+
174
+ &--selected {
175
+ background-color: var(--origo-color-surface-subtle);
176
+ font-weight: bold;
177
+ }
178
+
179
+ &--disabled {
180
+ opacity: var(--origo-opacity-disabled);
181
+ cursor: not-allowed;
182
+ }
183
+ }
184
+
185
+ .origo-select__no-options {
186
+ padding: var(--origo-spacing-container-padding);
187
+ color: var(--origo-color-text-muted);
188
+ text-align: center;
189
+ }
190
+
191
+ .origo-select__error {
192
+ color: var(--origo-color-text-danger);
193
+ font-size: var(--origo-typography-caption-font-size);
194
+ margin-top: var(--origo-spacing-xs);
195
+ }
196
+
197
+ .origo-select__help {
198
+ color: var(--origo-color-text-muted);
199
+ font-size: var(--origo-typography-caption-font-size);
200
+ margin-top: var(--origo-spacing-xs);
201
+ }