@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@origostudio/origo-design",
3
- "version": "0.0.49",
3
+ "version": "0.0.50",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,3 +1,13 @@
1
+ ## 0.0.50 (2026-10-08)
2
+
3
+ ### 🚀 Features
4
+
5
+ - add core selection control primitives including select, multi-select, combobox, and autocomplete components ([3866703](https://github.com/OrigoStudio/origo-design/commit/3866703))
6
+
7
+ ### ❤️ Thank You
8
+
9
+ - Hardik Patel @hardikpatelse
10
+
1
11
  ## 0.0.49 (2026-10-08)
2
12
 
3
13
  ### 🚀 Features
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@origostudio/angular-renderer",
3
- "version": "0.0.49",
3
+ "version": "0.0.50",
4
4
  "private": false,
5
5
  "type": "commonjs",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "dependencies": {
9
- "@origostudio/core": "0.0.49",
10
- "@origostudio/design-tokens": "0.0.49",
9
+ "@origostudio/core": "0.0.50",
10
+ "@origostudio/design-tokens": "0.0.50",
11
11
  "tslib": "^2.3.0",
12
12
  "@analogjs/vite-plugin-angular": "^2.7.1",
13
13
  "zone.js": "^0.16.3",
@@ -0,0 +1,83 @@
1
+ <div class="origo-autocomplete__wrapper">
2
+ <div class="origo-autocomplete__control">
3
+ <input
4
+ type="text"
5
+ class="origo-autocomplete__input"
6
+ [id]="contract().id + '-input'"
7
+ role="combobox"
8
+ aria-autocomplete="list"
9
+ [attr.aria-expanded]="isOpen()"
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
+ [placeholder]="computedPlaceholder()"
19
+ [disabled]="computedDisabled()"
20
+ [readonly]="computedReadonly()"
21
+ [value]="inputValue()"
22
+ (input)="onInput($event)"
23
+ (paste)="onPaste($event)"
24
+ (focus)="onFocus()"
25
+ (blur)="onBlur()"
26
+ (keydown)="onKeydown($event)"
27
+ />
28
+
29
+ <span class="origo-autocomplete__icons">
30
+ @if (computedClearable() && inputValue() && !computedDisabled() && !computedReadonly()) {
31
+ <span
32
+ class="origo-autocomplete__clear"
33
+ role="button"
34
+ tabindex="0"
35
+ aria-label="Clear value"
36
+ (click)="clear($event)"
37
+ (keydown.enter)="clear($event)"
38
+ >✕</span
39
+ >
40
+ }
41
+ </span>
42
+ </div>
43
+
44
+ @if (isOpen()) {
45
+ <div
46
+ class="origo-autocomplete__dropdown"
47
+ [id]="contract().id + '-listbox'"
48
+ role="listbox"
49
+ [attr.aria-label]="computedAriaLabel()"
50
+ >
51
+ <ul class="origo-autocomplete__options">
52
+ @for (opt of filteredSuggestions(); track opt.value; let idx = $index) {
53
+ <li
54
+ class="origo-autocomplete__option"
55
+ [class.origo-autocomplete__option--selected]="opt.value === value()"
56
+ [class.origo-autocomplete__option--active]="activeIndex() === idx"
57
+ [class.origo-autocomplete__option--disabled]="opt.disabled"
58
+ [id]="contract().id + '-option-' + idx"
59
+ role="option"
60
+ [attr.aria-selected]="opt.value === value()"
61
+ [attr.aria-disabled]="opt.disabled ? 'true' : null"
62
+ (click)="!opt.disabled && selectSuggestion(opt)"
63
+ >
64
+ {{ opt.label }}
65
+ </li>
66
+ } @empty {
67
+ <li class="origo-autocomplete__no-options" role="presentation">No suggestions found</li>
68
+ }
69
+ </ul>
70
+ </div>
71
+ }
72
+ </div>
73
+
74
+ @if (computedErrorText()) {
75
+ <div class="origo-autocomplete__error" [id]="contract().id + '-error'">
76
+ {{ computedErrorText() }}
77
+ </div>
78
+ }
79
+ @if (computedHelpText()) {
80
+ <div class="origo-autocomplete__help" [id]="contract().id + '-help'">
81
+ {{ computedHelpText() }}
82
+ </div>
83
+ }
@@ -0,0 +1,161 @@
1
+ :host {
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);
9
+ }
10
+
11
+ :host(.origo-autocomplete--fluid) {
12
+ display: block;
13
+ width: 100%;
14
+ }
15
+
16
+ .origo-autocomplete__wrapper {
17
+ display: flex;
18
+ flex-direction: column;
19
+ position: relative;
20
+ width: 100%;
21
+ }
22
+
23
+ .origo-autocomplete__control {
24
+ position: relative;
25
+ display: flex;
26
+ align-items: center;
27
+ width: 100%;
28
+ }
29
+
30
+ .origo-autocomplete__input {
31
+ box-sizing: border-box;
32
+ width: 100%;
33
+ background-color: var(--origo-color-surface-background);
34
+ color: var(--origo-color-text-primary);
35
+ padding: var(--origo-spacing-container-padding);
36
+ border: var(--origo-border-width-default) solid var(--origo-color-border-default);
37
+ border-radius: var(--origo-radius-sm);
38
+ box-shadow: var(--origo-shadow-sm);
39
+ font-family: inherit;
40
+ font-size: inherit;
41
+ transition: all var(--origo-duration-fast) ease-in-out;
42
+
43
+ &:hover:not(:disabled):not([readonly]) {
44
+ border-color: var(--origo-color-border-hover);
45
+ }
46
+
47
+ &:active:not(:disabled):not([readonly]) {
48
+ border-color: var(--origo-color-border-active);
49
+ }
50
+
51
+ &:focus,
52
+ &:focus-visible {
53
+ outline: var(--origo-border-width-thick) solid var(--origo-color-focus);
54
+ outline-offset: var(--origo-border-offset-negative);
55
+ border-color: var(--origo-color-focus);
56
+ }
57
+
58
+ &:disabled {
59
+ cursor: not-allowed;
60
+ opacity: var(--origo-opacity-disabled);
61
+ background-color: var(--origo-color-surface-disabled);
62
+ color: var(--origo-color-text-disabled);
63
+ }
64
+
65
+ &[readonly] {
66
+ background-color: var(--origo-color-surface-readonly);
67
+ }
68
+
69
+ &::placeholder {
70
+ color: var(--origo-color-text-muted);
71
+ }
72
+ }
73
+
74
+ :host(.origo-autocomplete--filled) .origo-autocomplete__input {
75
+ background-color: var(--origo-color-surface-subtle);
76
+ border: none;
77
+ }
78
+
79
+ :host(.origo-autocomplete--invalid) .origo-autocomplete__input {
80
+ border-color: var(--origo-color-text-danger);
81
+ }
82
+
83
+ .origo-autocomplete__icons {
84
+ position: absolute;
85
+ right: var(--origo-spacing-sm);
86
+ display: inline-flex;
87
+ align-items: center;
88
+ gap: var(--origo-spacing-xs);
89
+ }
90
+
91
+ .origo-autocomplete__clear {
92
+ cursor: pointer;
93
+ color: var(--origo-color-text-muted);
94
+ font-size: var(--origo-typography-caption-font-size);
95
+
96
+ &:hover {
97
+ color: var(--origo-color-text-primary);
98
+ }
99
+ }
100
+
101
+ .origo-autocomplete__dropdown {
102
+ position: absolute;
103
+ top: 100%;
104
+ left: 0;
105
+ right: 0;
106
+ z-index: 1000;
107
+ margin-top: var(--origo-spacing-xs);
108
+ box-sizing: border-box;
109
+ background-color: var(--origo-color-surface-background);
110
+ border: var(--origo-border-width-default) solid var(--origo-color-border-default);
111
+ border-radius: var(--origo-radius-sm);
112
+ box-shadow: var(--origo-shadow-md);
113
+ max-height: 16rem;
114
+ overflow-y: auto;
115
+ }
116
+
117
+ .origo-autocomplete__options {
118
+ list-style: none;
119
+ margin: 0;
120
+ padding: 0;
121
+ }
122
+
123
+ .origo-autocomplete__option {
124
+ padding: var(--origo-spacing-container-padding);
125
+ color: var(--origo-color-text-primary);
126
+ cursor: pointer;
127
+ transition: background-color var(--origo-duration-fast) ease-in-out;
128
+
129
+ &:hover,
130
+ &--active {
131
+ background-color: var(--origo-color-surface-subtle);
132
+ }
133
+
134
+ &--selected {
135
+ background-color: var(--origo-color-surface-subtle);
136
+ font-weight: bold;
137
+ }
138
+
139
+ &--disabled {
140
+ opacity: var(--origo-opacity-disabled);
141
+ cursor: not-allowed;
142
+ }
143
+ }
144
+
145
+ .origo-autocomplete__no-options {
146
+ padding: var(--origo-spacing-container-padding);
147
+ color: var(--origo-color-text-muted);
148
+ text-align: center;
149
+ }
150
+
151
+ .origo-autocomplete__error {
152
+ color: var(--origo-color-text-danger);
153
+ font-size: var(--origo-typography-caption-font-size);
154
+ margin-top: var(--origo-spacing-xs);
155
+ }
156
+
157
+ .origo-autocomplete__help {
158
+ color: var(--origo-color-text-muted);
159
+ font-size: var(--origo-typography-caption-font-size);
160
+ margin-top: var(--origo-spacing-xs);
161
+ }
@@ -0,0 +1,338 @@
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 AutocompleteProps {
23
+ id?: string;
24
+ value?: unknown;
25
+ suggestions?: Array<unknown>;
26
+ minQueryLength?: number;
27
+ delay?: number;
28
+ multiple?: boolean;
29
+ forceSelection?: boolean;
30
+ optionLabel?: string;
31
+ optionValue?: string;
32
+ completeOnFocus?: boolean;
33
+ virtualScroll?: boolean;
34
+ loading?: boolean;
35
+ placeholder?: string;
36
+ clearable?: boolean;
37
+ disabled?: boolean;
38
+ readonly?: boolean;
39
+ required?: boolean;
40
+ invalid?: boolean;
41
+ errorText?: string;
42
+ helpText?: string;
43
+ variant?: 'outlined' | 'filled';
44
+ fluid?: boolean;
45
+ 'aria-label'?: string;
46
+ 'aria-describedby'?: string;
47
+ permissions?: Record<string, string>;
48
+ rules?: Record<string, unknown>;
49
+ metadata?: Record<string, unknown>;
50
+ }
51
+
52
+ @Component({
53
+ selector: 'origo-autocomplete',
54
+ standalone: true,
55
+ templateUrl: './autocomplete.component.html',
56
+ styleUrls: ['./autocomplete.component.scss'],
57
+ changeDetection: ChangeDetectionStrategy.OnPush,
58
+ encapsulation: ViewEncapsulation.ShadowDom,
59
+ host: {
60
+ '[class.origo-autocomplete]': 'true',
61
+ '[attr.data-testid]': 'contract().id ?? ""',
62
+ '[class.origo-autocomplete--fluid]': 'computedFluid()',
63
+ '[class.origo-autocomplete--outlined]': 'computedVariant() === "outlined"',
64
+ '[class.origo-autocomplete--filled]': 'computedVariant() === "filled"',
65
+ '[class.origo-autocomplete--invalid]': 'computedInvalid()',
66
+ '[class.origo-autocomplete--disabled]': 'computedDisabled()',
67
+ '[class.origo-autocomplete--readonly]': 'computedReadonly()',
68
+ },
69
+ })
70
+ export class AutocompleteComponent implements OrigoAdapter<AutocompleteProps> {
71
+ static readonly contractSchema = {
72
+ permissions: 'object',
73
+ rules: 'object',
74
+ metadata: 'object',
75
+ suggestions: 'array',
76
+ value: 'string',
77
+ minQueryLength: 'number',
78
+ delay: 'number',
79
+ multiple: 'boolean',
80
+ forceSelection: 'boolean',
81
+ optionLabel: 'string',
82
+ optionValue: 'string',
83
+ completeOnFocus: 'boolean',
84
+ virtualScroll: 'boolean',
85
+ loading: 'boolean',
86
+ placeholder: 'string',
87
+ clearable: 'boolean',
88
+ disabled: 'boolean',
89
+ readonly: 'boolean',
90
+ required: 'boolean',
91
+ invalid: 'boolean',
92
+ errorText: 'string',
93
+ helpText: 'string',
94
+ variant: 'string',
95
+ fluid: 'boolean',
96
+ 'aria-label': 'string',
97
+ 'aria-describedby': 'string',
98
+ };
99
+ static readonly strictContract = false;
100
+
101
+ contract = input.required<InteractionContract<AutocompleteProps>>();
102
+ value = model<unknown>('');
103
+
104
+ inputValue = signal<string>('');
105
+ query = signal<string>('');
106
+ isOpen = signal<boolean>(false);
107
+ activeIndex = signal<number>(-1);
108
+
109
+ private debounceTimer: ReturnType<typeof setTimeout> | null = null;
110
+
111
+ computedNormalizedSuggestions = computed(() => {
112
+ const props = this.contract().props;
113
+ return normalizeOptions(props?.suggestions, props?.optionLabel, props?.optionValue);
114
+ });
115
+
116
+ filteredSuggestions = computed(() => {
117
+ const suggestions = this.computedNormalizedSuggestions();
118
+ const q = this.query();
119
+ if (!q) return suggestions;
120
+ return filterOptions(suggestions, q, 'contains');
121
+ });
122
+
123
+ computedPlaceholder = computed(() => this.contract().props?.placeholder ?? '');
124
+ computedMinQueryLength = computed(() => this.contract().props?.minQueryLength ?? 1);
125
+ computedDelay = computed(() => this.contract().props?.delay ?? 300);
126
+ computedForceSelection = computed(() => !!this.contract().props?.forceSelection);
127
+ computedClearable = computed(() => !!this.contract().props?.clearable);
128
+ computedDisabled = computed(() => !!this.contract().props?.disabled);
129
+ computedReadonly = computed(() => !!this.contract().props?.readonly);
130
+ computedRequired = computed(() => !!this.contract().props?.required);
131
+ computedInvalid = computed(() => !!this.contract().props?.invalid);
132
+ computedVariant = computed(() => this.contract().props?.variant ?? 'outlined');
133
+ computedFluid = computed(() => !!this.contract().props?.fluid);
134
+ computedErrorText = computed(() => this.contract().props?.errorText ?? '');
135
+ computedHelpText = computed(() => this.contract().props?.helpText ?? '');
136
+
137
+ computedAriaLabel = computed(() => {
138
+ const label = this.contract().props?.['aria-label'];
139
+ if (label !== undefined && label !== null && String(label).trim() !== '') {
140
+ return String(label);
141
+ }
142
+ return this.computedPlaceholder() || 'Autocomplete';
143
+ });
144
+
145
+ computedAriaDescribedBy = computed(() => {
146
+ const parts: string[] = [];
147
+ const desc = this.contract().props?.['aria-describedby'];
148
+ if (desc) parts.push(String(desc));
149
+ if (this.computedErrorText()) parts.push(`${this.contract().id}-error`);
150
+ if (this.computedHelpText()) parts.push(`${this.contract().id}-help`);
151
+ return parts.length > 0 ? parts.join(' ') : undefined;
152
+ });
153
+
154
+ private experienceAdapter = inject(WebExperienceAdapterService);
155
+ private platformId = inject(PLATFORM_ID);
156
+ private elementRef = inject(ElementRef);
157
+
158
+ constructor() {
159
+ effect(() => {
160
+ const contractVal = this.contract().props?.value;
161
+ untracked(() => {
162
+ if (contractVal !== undefined && contractVal !== this.value()) {
163
+ this.value.set(contractVal);
164
+ // Sync input display
165
+ const match = this.computedNormalizedSuggestions().find(s => s.value === contractVal);
166
+ this.inputValue.set(match ? match.label : String(contractVal ?? ''));
167
+ }
168
+ });
169
+ });
170
+ }
171
+
172
+ onInput(event: Event) {
173
+ if (this.computedDisabled() || this.computedReadonly()) return;
174
+ const target = event.target as HTMLInputElement;
175
+ const text = target.value;
176
+ this.inputValue.set(text);
177
+
178
+ if (this.debounceTimer) {
179
+ clearTimeout(this.debounceTimer);
180
+ }
181
+
182
+ this.debounceTimer = setTimeout(() => {
183
+ this.query.set(text);
184
+ if (text.length >= this.computedMinQueryLength()) {
185
+ this.isOpen.set(true);
186
+ this.activeIndex.set(0);
187
+ } else {
188
+ this.isOpen.set(false);
189
+ }
190
+ }, this.computedDelay());
191
+ }
192
+
193
+ onPaste(event: ClipboardEvent) {
194
+ if (this.computedDisabled() || this.computedReadonly()) return;
195
+ event.preventDefault();
196
+ const plain = event.clipboardData?.getData('text/plain') ?? '';
197
+ const target = event.target as HTMLInputElement;
198
+ if (!target) return;
199
+ const start = target.selectionStart ?? target.value.length;
200
+ const end = target.selectionEnd ?? target.value.length;
201
+ const newValue = target.value.slice(0, start) + plain + target.value.slice(end);
202
+ target.value = newValue;
203
+ this.inputValue.set(newValue);
204
+ this.query.set(newValue);
205
+ if (newValue.length >= this.computedMinQueryLength()) {
206
+ this.isOpen.set(true);
207
+ this.activeIndex.set(0);
208
+ }
209
+ }
210
+
211
+ onFocus() {
212
+ if (
213
+ this.contract().props?.completeOnFocus &&
214
+ !this.computedDisabled() &&
215
+ !this.computedReadonly()
216
+ ) {
217
+ this.isOpen.set(true);
218
+ this.activeIndex.set(0);
219
+ }
220
+ }
221
+
222
+ onBlur() {
223
+ if (this.computedForceSelection()) {
224
+ const currentText = this.inputValue().trim().toLowerCase();
225
+ const match = this.computedNormalizedSuggestions().find(
226
+ s => s.label.toLowerCase() === currentText
227
+ );
228
+ if (match) {
229
+ this.selectSuggestion(match);
230
+ } else {
231
+ // Revert to existing value or clear
232
+ const existing = this.computedNormalizedSuggestions().find(s => s.value === this.value());
233
+ if (existing) {
234
+ this.inputValue.set(existing.label);
235
+ } else {
236
+ this.value.set('');
237
+ this.inputValue.set('');
238
+ }
239
+ }
240
+ }
241
+ }
242
+
243
+ selectSuggestion(suggestion: NormalizedOption) {
244
+ if (this.computedDisabled() || this.computedReadonly() || suggestion.disabled) return;
245
+ this.value.set(suggestion.value);
246
+ this.inputValue.set(suggestion.label);
247
+ this.isOpen.set(false);
248
+ this.experienceAdapter.updateState(this.contract().id, 'value', suggestion.value);
249
+ }
250
+
251
+ clear(event?: Event) {
252
+ if (this.computedDisabled() || this.computedReadonly()) return;
253
+ event?.stopPropagation();
254
+ this.value.set('');
255
+ this.inputValue.set('');
256
+ this.query.set('');
257
+ this.isOpen.set(false);
258
+ this.experienceAdapter.updateState(this.contract().id, 'value', '');
259
+ }
260
+
261
+ onKeydown(event: KeyboardEvent) {
262
+ if (this.computedDisabled() || this.computedReadonly()) return;
263
+
264
+ const list = this.filteredSuggestions();
265
+ const count = list.length;
266
+
267
+ switch (event.key) {
268
+ case 'ArrowDown': {
269
+ event.preventDefault();
270
+ if (!this.isOpen() && count > 0) {
271
+ this.isOpen.set(true);
272
+ this.activeIndex.set(0);
273
+ } else if (count > 0) {
274
+ const next = (this.activeIndex() + 1) % count;
275
+ this.activeIndex.set(next);
276
+ }
277
+ break;
278
+ }
279
+ case 'ArrowUp': {
280
+ event.preventDefault();
281
+ if (!this.isOpen() && count > 0) {
282
+ this.isOpen.set(true);
283
+ this.activeIndex.set(count - 1);
284
+ } else if (count > 0) {
285
+ const prev = (this.activeIndex() - 1 + count) % count;
286
+ this.activeIndex.set(prev);
287
+ }
288
+ break;
289
+ }
290
+ case 'Enter': {
291
+ if (this.isOpen()) {
292
+ event.preventDefault();
293
+ const idx = this.activeIndex();
294
+ if (idx >= 0 && idx < count && !list[idx].disabled) {
295
+ this.selectSuggestion(list[idx]);
296
+ }
297
+ }
298
+ break;
299
+ }
300
+ case 'Escape': {
301
+ if (this.isOpen()) {
302
+ event.preventDefault();
303
+ this.isOpen.set(false);
304
+ }
305
+ break;
306
+ }
307
+ case 'Tab': {
308
+ if (this.isOpen()) {
309
+ this.isOpen.set(false);
310
+ }
311
+ break;
312
+ }
313
+ case 'Home': {
314
+ if (this.isOpen() && count > 0) {
315
+ event.preventDefault();
316
+ this.activeIndex.set(0);
317
+ }
318
+ break;
319
+ }
320
+ case 'End': {
321
+ if (this.isOpen() && count > 0) {
322
+ event.preventDefault();
323
+ this.activeIndex.set(count - 1);
324
+ }
325
+ break;
326
+ }
327
+ }
328
+ }
329
+
330
+ @HostListener('document:click', ['$event'])
331
+ onDocumentClick(event: MouseEvent) {
332
+ if (!isPlatformBrowser(this.platformId)) return;
333
+ const clickedInside = this.elementRef.nativeElement.contains(event.target as Node);
334
+ if (!clickedInside && this.isOpen()) {
335
+ this.isOpen.set(false);
336
+ }
337
+ }
338
+ }