@dataengineeringformachinelearning/viking-ui 3.0.0-alpha.2 → 4.0.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 (198) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +55 -12
  3. package/dist/LICENSE +201 -0
  4. package/dist/README.md +251 -0
  5. package/dist/fesm2022/dataengineeringformachinelearning-viking-ui.mjs +13751 -0
  6. package/dist/fesm2022/dataengineeringformachinelearning-viking-ui.mjs.map +1 -0
  7. package/dist/index.js +1 -4051
  8. package/dist/package.json +87 -0
  9. package/dist/scripts/build-web-components.mjs +37 -0
  10. package/dist/src/tokens/series-presets.ts +15 -0
  11. package/dist/src/tokens/tailwind.preset.js +209 -0
  12. package/dist/src/tokens/viking-tokens.json +318 -0
  13. package/dist/src/web/badge/viking-badge-wc.ts +116 -0
  14. package/dist/src/web/button/viking-button-wc.ts +165 -0
  15. package/dist/src/web/callout/viking-callout-wc.ts +105 -0
  16. package/dist/src/web/card/viking-card-wc.ts +66 -0
  17. package/dist/src/web/core/base.ts +56 -0
  18. package/dist/src/web/core/dom.ts +53 -0
  19. package/dist/src/web/core/icons-inline.ts +44 -0
  20. package/dist/src/web/core/styles.ts +1138 -0
  21. package/dist/src/web/core/types.ts +19 -0
  22. package/dist/src/web/field/viking-field-wc.ts +154 -0
  23. package/dist/src/web/index.ts +73 -0
  24. package/dist/src/web/input/viking-input-wc.ts +211 -0
  25. package/dist/src/web/modal/viking-modal-wc.ts +187 -0
  26. package/dist/src/web/search-palette/viking-search-palette-wc.ts +494 -0
  27. package/dist/src/web/select/viking-select-wc.ts +218 -0
  28. package/dist/src/web/suite-header/viking-suite-header-wc.ts +714 -0
  29. package/dist/src/web/suite-search-palette/viking-suite-search-palette-wc.ts +234 -0
  30. package/dist/src/web/theme-toggle/viking-theme-toggle-wc.ts +139 -0
  31. package/dist/src/web/types.ts +37 -0
  32. package/dist/src/web/web-components.spec.ts +297 -0
  33. package/dist/types/dataengineeringformachinelearning-viking-ui.d.ts +2658 -0
  34. package/dist/viking.manifest.json +271 -0
  35. package/dist/web-components.js +2427 -126
  36. package/package.json +25 -11
  37. package/src/assets/drakkar-favicon.svg +17 -0
  38. package/src/core/brand/deml-lockup.svg +5 -0
  39. package/src/core/brand/deml-mark-compact.svg +4 -0
  40. package/src/core/brand/deml-mark-filled.svg +3 -0
  41. package/src/core/brand/deml-mark-outline.svg +4 -0
  42. package/src/core/brand-icons.ts +62 -0
  43. package/src/core/icons.spec.ts +52 -0
  44. package/src/core/icons.ts +206 -0
  45. package/src/core/integration-brand-icons.ts +77 -0
  46. package/src/core/lucide-paths.generated.ts +130 -0
  47. package/src/core/types.ts +85 -0
  48. package/src/core/uid.ts +7 -0
  49. package/src/lib/accordion/accordion.ts +126 -0
  50. package/src/lib/app-header/app-header.scss +274 -0
  51. package/src/lib/app-header/app-header.ts +43 -0
  52. package/src/lib/app-sidebar/app-sidebar.scss +415 -0
  53. package/src/lib/app-sidebar/app-sidebar.ts +29 -0
  54. package/src/lib/auth-panel/auth-panel.spec.ts +132 -0
  55. package/src/lib/auth-panel/auth-panel.ts +228 -0
  56. package/src/lib/autocomplete/autocomplete.ts +223 -0
  57. package/src/lib/avatar/avatar.ts +126 -0
  58. package/src/lib/badge/badge.ts +182 -0
  59. package/src/lib/bar/bar.ts +31 -0
  60. package/src/lib/brand/brand.ts +60 -0
  61. package/src/lib/breadcrumbs/breadcrumbs.ts +81 -0
  62. package/src/lib/button/button-group.ts +40 -0
  63. package/src/lib/button/button.scss +96 -0
  64. package/src/lib/button/button.ts +98 -0
  65. package/src/lib/calendar/calendar.ts +253 -0
  66. package/src/lib/callout/callout.ts +211 -0
  67. package/src/lib/card/card-title.spec.ts +27 -0
  68. package/src/lib/card/card-title.ts +50 -0
  69. package/src/lib/card/card.ts +170 -0
  70. package/src/lib/carousel/carousel.ts +206 -0
  71. package/src/lib/chart/chart-zoom.spec.ts +65 -0
  72. package/src/lib/chart/chart-zoom.ts +71 -0
  73. package/src/lib/chart/chart.ts +1257 -0
  74. package/src/lib/chart-card-header/chart-card-header.ts +80 -0
  75. package/src/lib/chart-empty-state/chart-empty-state.ts +146 -0
  76. package/src/lib/chart-panel/chart-panel.ts +133 -0
  77. package/src/lib/checkbox/checkbox.ts +139 -0
  78. package/src/lib/color-picker/color-picker.ts +148 -0
  79. package/src/lib/command/command.ts +260 -0
  80. package/src/lib/composer/composer.ts +180 -0
  81. package/src/lib/confirm-dialog/confirm-dialog.ts +163 -0
  82. package/src/lib/confirm-dialog/dialog.service.ts +79 -0
  83. package/src/lib/context/context.ts +75 -0
  84. package/src/lib/core/cva.ts +33 -0
  85. package/src/lib/date-picker/date-picker.ts +139 -0
  86. package/src/lib/dropdown/dropdown.ts +188 -0
  87. package/src/lib/editor/editor.ts +170 -0
  88. package/src/lib/empty-state/empty-state.ts +100 -0
  89. package/src/lib/fab/fab.ts +76 -0
  90. package/src/lib/field/field.ts +111 -0
  91. package/src/lib/file-upload/file-upload.ts +208 -0
  92. package/src/lib/footer/footer.scss +155 -0
  93. package/src/lib/footer/footer.ts +28 -0
  94. package/src/lib/form-section/form-section.ts +200 -0
  95. package/src/lib/forms.spec.ts +92 -0
  96. package/src/lib/gauge-arc/gauge-arc.ts +85 -0
  97. package/src/lib/hud-panel/hud-panel.ts +27 -0
  98. package/src/lib/icon/icon.ts +284 -0
  99. package/src/lib/icon-badge/icon-badge.ts +87 -0
  100. package/src/lib/icon-heading/icon-heading.spec.ts +32 -0
  101. package/src/lib/icon-heading/icon-heading.ts +38 -0
  102. package/src/lib/icon-text/icon-text.ts +120 -0
  103. package/src/lib/input/input.ts +119 -0
  104. package/src/lib/kanban/kanban.ts +211 -0
  105. package/src/lib/kbd/kbd.ts +32 -0
  106. package/src/lib/label/label.ts +39 -0
  107. package/src/lib/loading-overlay/loading-overlay.ts +80 -0
  108. package/src/lib/menubar/menubar.ts +77 -0
  109. package/src/lib/metric-card/metric-card.ts +33 -0
  110. package/src/lib/modal/modal.ts +207 -0
  111. package/src/lib/native-select/native-select.ts +124 -0
  112. package/src/lib/navbar/navbar.ts +124 -0
  113. package/src/lib/navigation-menu/navigation-menu.ts +116 -0
  114. package/src/lib/otp-input/otp-input.ts +164 -0
  115. package/src/lib/page-back-link/page-back-link.ts +74 -0
  116. package/src/lib/page-header/page-header.ts +70 -0
  117. package/src/lib/pagination/pagination.ts +157 -0
  118. package/src/lib/pillbox/pillbox.ts +157 -0
  119. package/src/lib/popover/popover.ts +92 -0
  120. package/src/lib/profile/profile.ts +73 -0
  121. package/src/lib/progress/progress.ts +123 -0
  122. package/src/lib/radio/radio.ts +132 -0
  123. package/src/lib/ring-gauge/ring-gauge.ts +75 -0
  124. package/src/lib/scroll-area/scroll-area.ts +57 -0
  125. package/src/lib/search-palette/search-palette.scss +288 -0
  126. package/src/lib/search-palette/search-palette.ts +114 -0
  127. package/src/lib/select/select.ts +292 -0
  128. package/src/lib/separator/separator.ts +59 -0
  129. package/src/lib/sheet/sheet.ts +192 -0
  130. package/src/lib/sidebar-nav/sidebar-nav.scss +218 -0
  131. package/src/lib/sidebar-nav/sidebar-nav.ts +158 -0
  132. package/src/lib/site-drakkar/site-footer.ts +129 -0
  133. package/src/lib/site-drakkar/site-navbar.scss +2 -0
  134. package/src/lib/site-drakkar/site-navbar.ts +267 -0
  135. package/src/lib/site-drakkar/suite-search-items.spec.ts +61 -0
  136. package/src/lib/skeleton/skeleton.ts +51 -0
  137. package/src/lib/slider/slider.ts +133 -0
  138. package/src/lib/spinner/spinner.ts +75 -0
  139. package/src/lib/status-metric-row/status-metric-row.ts +74 -0
  140. package/src/lib/suite-search-palette/suite-search-palette.ts +95 -0
  141. package/src/lib/switch/switch.ts +131 -0
  142. package/src/lib/table/table.ts +137 -0
  143. package/src/lib/tabs/tab-panel.ts +61 -0
  144. package/src/lib/tabs/tab.ts +107 -0
  145. package/src/lib/tabs/tabs.ts +109 -0
  146. package/src/lib/textarea/textarea.ts +103 -0
  147. package/src/lib/theme-toggle/theme-toggle.spec.ts +40 -0
  148. package/src/lib/theme-toggle/theme-toggle.ts +39 -0
  149. package/src/lib/time-picker/time-picker.ts +197 -0
  150. package/src/lib/timeline/timeline.ts +152 -0
  151. package/src/lib/toast/toast.ts +196 -0
  152. package/src/lib/toggle/toggle-group.ts +60 -0
  153. package/src/lib/toggle/toggle.ts +101 -0
  154. package/src/lib/tooltip/tooltip.ts +119 -0
  155. package/src/lib/typography/heading.ts +52 -0
  156. package/src/lib/typography/text.ts +51 -0
  157. package/src/lib/uptime-bar/uptime-bar.ts +55 -0
  158. package/src/lib/verification-code-field/verification-code-field.ts +149 -0
  159. package/src/lib/whitepaper-cta/whitepaper-cta.scss +130 -0
  160. package/src/lib/whitepaper-cta/whitepaper-cta.ts +78 -0
  161. package/src/lib/wizard/wizard.ts +80 -0
  162. package/src/public-api.ts +172 -0
  163. package/src/tokens/series-presets.ts +15 -0
  164. package/src/tokens/tailwind.preset.js +209 -0
  165. package/src/viking-ui.spec.ts +416 -0
  166. package/src/web/web-components.spec.ts +297 -0
  167. package/dist/elements/button/viking-button.d.ts +0 -14
  168. package/dist/elements/card/viking-card.d.ts +0 -8
  169. package/dist/elements/core/dom.d.ts +0 -4
  170. package/dist/elements/core/styles.d.ts +0 -2
  171. package/dist/elements/modal/viking-modal.d.ts +0 -15
  172. package/dist/index.d.ts +0 -1
  173. package/dist/lib/core/brand-icons.d.ts +0 -29
  174. package/dist/lib/core/icons.d.ts +0 -132
  175. package/dist/lib/core/integration-brand-icons.d.ts +0 -18
  176. package/dist/lib/core/lucide-paths.generated.d.ts +0 -90
  177. package/dist/lib/site-drakkar/site-drakkar.config.d.ts +0 -50
  178. package/dist/lib/site-drakkar/suite-search-items.d.ts +0 -17
  179. package/dist/web/badge/viking-badge-wc.d.ts +0 -28
  180. package/dist/web/button/viking-button-wc.d.ts +0 -28
  181. package/dist/web/callout/viking-callout-wc.d.ts +0 -27
  182. package/dist/web/card/viking-card-wc.d.ts +0 -29
  183. package/dist/web/core/base.d.ts +0 -12
  184. package/dist/web/core/dom.d.ts +0 -11
  185. package/dist/web/core/icons-inline.d.ts +0 -7
  186. package/dist/web/core/styles.d.ts +0 -12
  187. package/dist/web/core/types.d.ts +0 -10
  188. package/dist/web/field/viking-field-wc.d.ts +0 -32
  189. package/dist/web/index.d.ts +0 -15
  190. package/dist/web/input/viking-input-wc.d.ts +0 -32
  191. package/dist/web/modal/viking-modal-wc.d.ts +0 -50
  192. package/dist/web/search-palette/viking-search-palette-wc.d.ts +0 -64
  193. package/dist/web/select/viking-select-wc.d.ts +0 -48
  194. package/dist/web/suite-header/viking-suite-header-wc.d.ts +0 -36
  195. package/dist/web/suite-search-palette/viking-suite-search-palette-wc.d.ts +0 -39
  196. package/dist/web/theme-toggle/viking-theme-toggle-wc.d.ts +0 -19
  197. package/dist/web/types.d.ts +0 -40
  198. package/dist/web-components/index.d.ts +0 -1
@@ -0,0 +1,228 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ input,
5
+ output,
6
+ } from "@angular/core";
7
+ import { VikingButton } from "../button/button";
8
+ import { VikingSeparator } from "../separator/separator";
9
+
10
+ /**
11
+ * viking-auth-panel — access portal / sign-in layout template.
12
+ * Full-width stacked actions, Google + Apple OAuth row, WCAG-friendly structure.
13
+ */
14
+ @Component({
15
+ selector: "viking-auth-panel",
16
+ imports: [VikingButton, VikingSeparator],
17
+ changeDetection: ChangeDetectionStrategy.OnPush,
18
+ host: { class: "viking-auth-panel-host" },
19
+ template: `
20
+ <section class="viking-auth-panel" aria-labelledby="viking-auth-title">
21
+ <header class="viking-auth-header">
22
+ @if (tag()) {
23
+ <div class="viking-auth-tag-row">
24
+ <span class="viking-auth-tag">{{ tag() }}</span>
25
+ </div>
26
+ }
27
+ <h1 id="viking-auth-title" class="viking-auth-title">{{ title() }}</h1>
28
+ @if (subtitle()) {
29
+ <p class="viking-auth-subtitle">{{ subtitle() }}</p>
30
+ }
31
+ </header>
32
+
33
+ <div class="viking-auth-body">
34
+ <ng-content />
35
+ </div>
36
+
37
+ <ng-content select="[vikingAuthLinks]" />
38
+
39
+ @if (showSocial()) {
40
+ <viking-separator text="or" />
41
+ <div
42
+ class="viking-auth-social"
43
+ role="group"
44
+ aria-label="Social sign-in"
45
+ >
46
+ <viking-button
47
+ variant="outline"
48
+ type="button"
49
+ icon="google"
50
+ [fullWidth]="true"
51
+ [loading]="socialLoading()"
52
+ [disabled]="socialLoading()"
53
+ (pressed)="googleSignIn.emit()"
54
+ >
55
+ Sign In with Google
56
+ </viking-button>
57
+ <viking-button
58
+ variant="outline"
59
+ type="button"
60
+ icon="apple"
61
+ [fullWidth]="true"
62
+ [loading]="socialLoading()"
63
+ [disabled]="socialLoading()"
64
+ (pressed)="appleSignIn.emit()"
65
+ >
66
+ Sign In with Apple
67
+ </viking-button>
68
+ </div>
69
+ }
70
+
71
+ <footer class="viking-auth-footer">
72
+ <ng-content select="[vikingAuthFooter]" />
73
+ </footer>
74
+ </section>
75
+ `,
76
+ styles: [
77
+ `
78
+ :host {
79
+ display: block;
80
+ width: 100%;
81
+ max-width: 480px;
82
+ margin-inline: auto;
83
+ font-family: var(--viking-font-family);
84
+ }
85
+ .viking-auth-panel {
86
+ display: flex;
87
+ flex-direction: column;
88
+ gap: var(--viking-space-3);
89
+ width: 100%;
90
+ padding: var(--viking-space-3);
91
+ background: var(--viking-surface);
92
+ border: 1px solid var(--viking-border);
93
+ border-radius: var(--viking-radius-lg);
94
+ box-shadow: var(--viking-shadow-md);
95
+ position: relative;
96
+ overflow: hidden;
97
+ }
98
+ .viking-auth-panel::before {
99
+ content: "";
100
+ position: absolute;
101
+ inset: 0 0 auto;
102
+ height: 1px;
103
+ background: linear-gradient(
104
+ 90deg,
105
+ transparent,
106
+ color-mix(in srgb, var(--viking-metallic-200) 22%, transparent),
107
+ transparent
108
+ );
109
+ pointer-events: none;
110
+ }
111
+ .viking-auth-header {
112
+ display: flex;
113
+ flex-direction: column;
114
+ gap: var(--viking-space-1);
115
+ margin-bottom: var(--viking-space-1);
116
+ }
117
+ .viking-auth-tag-row {
118
+ display: flex;
119
+ align-items: center;
120
+ }
121
+ .viking-auth-tag {
122
+ display: inline-flex;
123
+ align-items: center;
124
+ gap: var(--viking-space-1);
125
+ font-size: var(--viking-font-size-ui);
126
+ font-weight: 700;
127
+ letter-spacing: 0.08em;
128
+ text-transform: uppercase;
129
+ color: var(--viking-text-muted);
130
+ padding: var(--viking-space-1) var(--viking-space-2);
131
+ border: 1px solid var(--viking-border);
132
+ border-radius: var(--viking-radius-pill);
133
+ background: var(--viking-surface);
134
+ box-shadow: var(--viking-shadow-sm);
135
+ }
136
+ .viking-auth-tag::before {
137
+ content: "";
138
+ width: var(--viking-space-1);
139
+ height: var(--viking-space-1);
140
+ border-radius: 50%;
141
+ background: var(--viking-accent);
142
+ flex-shrink: 0;
143
+ }
144
+ .viking-auth-title {
145
+ margin: 0;
146
+ font-size: var(--viking-font-size-xl);
147
+ font-weight: 700;
148
+ letter-spacing: var(--header-letter-spacing, -0.02em);
149
+ color: var(--viking-text);
150
+ line-height: var(--viking-line-height-tight);
151
+ }
152
+ .viking-auth-subtitle {
153
+ margin: 0;
154
+ font-size: var(--viking-font-size);
155
+ line-height: 1.6;
156
+ color: var(--viking-text-muted);
157
+ }
158
+ .viking-auth-body {
159
+ display: flex;
160
+ flex-direction: column;
161
+ gap: var(--viking-space-2);
162
+ width: 100%;
163
+ }
164
+ :host ::ng-deep .viking-auth-stack {
165
+ display: flex;
166
+ flex-direction: column;
167
+ gap: var(--viking-space-2);
168
+ width: 100%;
169
+ }
170
+ :host ::ng-deep .viking-auth-stack viking-callout {
171
+ width: 100%;
172
+ }
173
+ :host ::ng-deep .viking-auth-stack .login-form,
174
+ :host ::ng-deep .login-form {
175
+ display: flex;
176
+ flex-direction: column;
177
+ gap: var(--viking-space-2);
178
+ width: 100%;
179
+ }
180
+ .viking-auth-social {
181
+ display: flex;
182
+ flex-direction: column;
183
+ gap: var(--viking-space-2);
184
+ width: 100%;
185
+ }
186
+ .viking-auth-footer {
187
+ display: flex;
188
+ flex-direction: column;
189
+ gap: var(--viking-space-3);
190
+ width: 100%;
191
+ margin-top: var(--viking-space-4);
192
+ padding-top: var(--viking-space-4);
193
+ border-top: 1px solid var(--viking-border);
194
+ }
195
+ :host ::ng-deep [vikingAuthFooter] {
196
+ display: flex;
197
+ flex-direction: column;
198
+ gap: var(--viking-space-3);
199
+ width: 100%;
200
+ }
201
+ :host ::ng-deep [vikingAuthFooter] viking-button .viking-btn {
202
+ min-width: 0;
203
+ }
204
+ :host ::ng-deep [vikingAuthLinks].viking-auth-links,
205
+ :host ::ng-deep .viking-auth-links {
206
+ display: flex;
207
+ flex-direction: column;
208
+ gap: var(--viking-space-2);
209
+ width: 100%;
210
+ }
211
+ :host ::ng-deep viking-field {
212
+ width: 100%;
213
+ }
214
+ `,
215
+ ],
216
+ })
217
+ export class VikingAuthPanel {
218
+ readonly tag = input("Access Portal");
219
+ readonly title = input.required<string>();
220
+ readonly subtitle = input(
221
+ "Secure entry to the DEML (DATA ENGINEERING FOR MACHINE LEARNING).",
222
+ );
223
+ readonly showSocial = input(true);
224
+ readonly socialLoading = input(false);
225
+
226
+ readonly googleSignIn = output<void>();
227
+ readonly appleSignIn = output<void>();
228
+ }
@@ -0,0 +1,223 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ ElementRef,
5
+ computed,
6
+ input,
7
+ model,
8
+ output,
9
+ signal,
10
+ } from "@angular/core";
11
+ import { VikingControl, provideVikingCva } from "../core/cva";
12
+ import { VikingIcon } from "../icon/icon";
13
+ import { vikingUid } from "../../core/uid";
14
+
15
+ /**
16
+ * viking-autocomplete — text input with filtered suggestions
17
+ *. ControlValueAccessor-compatible.
18
+ */
19
+ @Component({
20
+ selector: "viking-autocomplete",
21
+ imports: [VikingIcon],
22
+ providers: [provideVikingCva(VikingAutocomplete)],
23
+ changeDetection: ChangeDetectionStrategy.OnPush,
24
+ host: { "(document:click)": "onDocumentClick($event)" },
25
+ template: `
26
+ <div class="viking-control viking-autocomplete-shell">
27
+ <viking-icon class="viking-autocomplete-icon" name="search" [size]="20" />
28
+ <input
29
+ type="text"
30
+ role="combobox"
31
+ [placeholder]="placeholder()"
32
+ [value]="value()"
33
+ [disabled]="disabled() || formDisabled()"
34
+ [attr.aria-expanded]="open()"
35
+ [attr.aria-controls]="panelId"
36
+ [attr.aria-label]="label() || placeholder() || 'Autocomplete'"
37
+ aria-autocomplete="list"
38
+ (input)="onInput($event)"
39
+ (focus)="open.set(true)"
40
+ (keydown)="onKeydown($event)"
41
+ (blur)="onTouched()"
42
+ />
43
+ </div>
44
+ @if (open() && filtered().length > 0) {
45
+ <div class="viking-autocomplete-panel" role="listbox" [id]="panelId">
46
+ @for (item of filtered(); track item; let index = $index) {
47
+ <button
48
+ type="button"
49
+ role="option"
50
+ class="viking-autocomplete-option"
51
+ [class.viking-active]="index === activeIndex()"
52
+ [attr.aria-selected]="index === activeIndex()"
53
+ (mousedown)="$event.preventDefault()"
54
+ (click)="pick(item)"
55
+ (mouseenter)="activeIndex.set(index)"
56
+ >
57
+ {{ item }}
58
+ </button>
59
+ }
60
+ </div>
61
+ }
62
+ `,
63
+ styles: [
64
+ `
65
+ :host {
66
+ position: relative;
67
+ display: block;
68
+ }
69
+ .viking-autocomplete-shell {
70
+ display: flex;
71
+ align-items: center;
72
+ gap: var(--viking-space-1);
73
+ min-height: var(--viking-control-height);
74
+ padding: 0 var(--viking-space-2);
75
+ background: var(--viking-surface);
76
+ border: 1px solid var(--viking-border-strong);
77
+ border-radius: var(--viking-radius);
78
+ box-shadow: var(--viking-shadow-sm);
79
+ transition: var(--viking-transition);
80
+ }
81
+ .viking-autocomplete-shell:focus-within {
82
+ outline: var(--viking-ring-width) solid var(--viking-ring);
83
+ outline-offset: var(--viking-ring-offset);
84
+ }
85
+ .viking-autocomplete-icon {
86
+ color: var(--viking-text-muted);
87
+ }
88
+ input {
89
+ flex: 1;
90
+ min-width: 0;
91
+ border: none;
92
+ outline: none;
93
+ background: transparent;
94
+ color: var(--viking-text);
95
+ font-family: var(--viking-font-family);
96
+ font-size: var(--viking-font-size);
97
+ }
98
+ input::placeholder {
99
+ color: var(--viking-text-muted);
100
+ }
101
+ .viking-autocomplete-panel {
102
+ position: absolute;
103
+ top: calc(100% + var(--viking-space-1));
104
+ left: 0;
105
+ right: 0;
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 2px;
109
+ margin: 0;
110
+ padding: var(--viking-space-1);
111
+ background: var(--viking-surface);
112
+ border: 1px solid var(--viking-border-strong);
113
+ border-radius: var(--viking-radius);
114
+ box-shadow: var(--viking-shadow-md);
115
+ z-index: var(--viking-z-overlay);
116
+ max-height: 288px;
117
+ overflow: auto;
118
+ }
119
+ .viking-autocomplete-option {
120
+ padding: var(--viking-space-1);
121
+ border: none;
122
+ background: transparent;
123
+ text-align: left;
124
+ width: 100%;
125
+ border-radius: calc(var(--viking-radius) / 1.5);
126
+ font-family: var(--viking-font-family);
127
+ font-size: var(--viking-font-size);
128
+ color: var(--viking-text);
129
+ cursor: pointer;
130
+ }
131
+ .viking-autocomplete-option:focus-visible {
132
+ outline: var(--viking-ring-width) solid var(--viking-ring);
133
+ outline-offset: -2px;
134
+ }
135
+ .viking-active {
136
+ background: var(--viking-accent-soft);
137
+ }
138
+ `,
139
+ ],
140
+ })
141
+ export class VikingAutocomplete extends VikingControl<string> {
142
+ readonly suggestions = input.required<string[]>();
143
+ readonly value = model<string>("");
144
+ readonly placeholder = input<string>("");
145
+ readonly label = input<string>("");
146
+ readonly disabled = input<boolean>(false);
147
+
148
+ readonly selected = output<string>();
149
+
150
+ constructor(private readonly host: ElementRef<HTMLElement>) {
151
+ super();
152
+ }
153
+
154
+ protected readonly open = signal(false);
155
+ protected readonly activeIndex = signal(0);
156
+ protected readonly panelId = vikingUid("viking-autocomplete-panel");
157
+
158
+ protected readonly filtered = computed(() => {
159
+ const query = this.value().toLowerCase().trim();
160
+ if (!query) {
161
+ return this.suggestions();
162
+ }
163
+ return this.suggestions().filter((item) =>
164
+ item.toLowerCase().includes(query),
165
+ );
166
+ });
167
+
168
+ writeValue(value: string): void {
169
+ this.value.set(value ?? "");
170
+ }
171
+
172
+ protected onInput = (event: Event): void => {
173
+ const next = (event.target as HTMLInputElement).value;
174
+ this.value.set(next);
175
+ this.onChange(next);
176
+ this.open.set(true);
177
+ this.activeIndex.set(0);
178
+ };
179
+
180
+ protected pick = (item: string): void => {
181
+ this.value.set(item);
182
+ this.onChange(item);
183
+ this.selected.emit(item);
184
+ this.open.set(false);
185
+ };
186
+
187
+ protected onKeydown = (event: KeyboardEvent): void => {
188
+ if (!this.open()) {
189
+ return;
190
+ }
191
+ const items = this.filtered();
192
+ switch (event.key) {
193
+ case "ArrowDown":
194
+ event.preventDefault();
195
+ this.activeIndex.update((index) =>
196
+ Math.min(items.length - 1, index + 1),
197
+ );
198
+ break;
199
+ case "ArrowUp":
200
+ event.preventDefault();
201
+ this.activeIndex.update((index) => Math.max(0, index - 1));
202
+ break;
203
+ case "Enter":
204
+ if (items[this.activeIndex()]) {
205
+ event.preventDefault();
206
+ this.pick(items[this.activeIndex()]);
207
+ }
208
+ break;
209
+ case "Escape":
210
+ this.open.set(false);
211
+ break;
212
+ }
213
+ };
214
+
215
+ protected onDocumentClick = (event: Event): void => {
216
+ if (
217
+ this.open() &&
218
+ !this.host.nativeElement.contains(event.target as Node)
219
+ ) {
220
+ this.open.set(false);
221
+ }
222
+ };
223
+ }
@@ -0,0 +1,126 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ input,
6
+ signal,
7
+ } from "@angular/core";
8
+ import { VikingTone } from "../../core/types";
9
+
10
+ /**
11
+ * viking-avatar — image or initials avatar with optional status indicator
12
+ *.
13
+ */
14
+ @Component({
15
+ selector: "viking-avatar",
16
+ changeDetection: ChangeDetectionStrategy.OnPush,
17
+ host: {
18
+ "[style.width.px]": "size()",
19
+ "[style.height.px]": "size()",
20
+ "[class.viking-avatar-square]": "square()",
21
+ },
22
+ template: `
23
+ @if (src() && !imageFailed()) {
24
+ <img [src]="src()" [alt]="name()" (error)="imageFailed.set(true)" />
25
+ } @else {
26
+ <span class="viking-avatar-initials">
27
+ {{ initials() }}
28
+ </span>
29
+ }
30
+ @if (status()) {
31
+ <span
32
+ class="viking-avatar-status"
33
+ [class]="'viking-status-' + status()"
34
+ role="status"
35
+ >
36
+ <span class="viking-visually-hidden">{{ status() }}</span>
37
+ </span>
38
+ }
39
+ `,
40
+ styles: [
41
+ `
42
+ :host {
43
+ position: relative;
44
+ display: inline-flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ border-radius: var(--viking-radius-pill);
48
+ background: var(--viking-surface-alt);
49
+ border: 1px solid var(--viking-border);
50
+ overflow: visible;
51
+ flex-shrink: 0;
52
+ }
53
+ :host(.viking-avatar-square) {
54
+ border-radius: var(--viking-radius);
55
+ }
56
+ img {
57
+ width: 100%;
58
+ height: 100%;
59
+ object-fit: cover;
60
+ border-radius: inherit;
61
+ }
62
+ .viking-avatar-initials {
63
+ font-family: var(--viking-font-family);
64
+ font-size: var(--viking-font-size-ui);
65
+ font-weight: 600;
66
+ color: var(--viking-text);
67
+ text-transform: uppercase;
68
+ letter-spacing: 0.02em;
69
+ user-select: none;
70
+ }
71
+ .viking-avatar-status {
72
+ position: absolute;
73
+ right: -1px;
74
+ bottom: -1px;
75
+ width: 27%;
76
+ height: 27%;
77
+ min-width: 9px;
78
+ min-height: 9px;
79
+ border-radius: var(--viking-radius-pill);
80
+ border: 2px solid var(--viking-surface);
81
+ }
82
+ .viking-status-success {
83
+ background: var(--viking-success);
84
+ }
85
+ .viking-status-warning {
86
+ background: var(--viking-warning);
87
+ }
88
+ .viking-status-danger {
89
+ background: var(--viking-danger);
90
+ }
91
+ .viking-status-accent {
92
+ background: var(--viking-accent);
93
+ }
94
+ .viking-status-muted {
95
+ background: var(--viking-text-muted);
96
+ }
97
+ .viking-visually-hidden {
98
+ position: absolute;
99
+ width: 1px;
100
+ height: 1px;
101
+ overflow: hidden;
102
+ clip: rect(0 0 0 0);
103
+ white-space: nowrap;
104
+ }
105
+ `,
106
+ ],
107
+ })
108
+ export class VikingAvatar {
109
+ readonly src = input<string | null>(null);
110
+ readonly name = input<string>("");
111
+ readonly size = input<number>(45);
112
+ readonly square = input<boolean>(false);
113
+ readonly status = input<VikingTone | null>(null);
114
+
115
+ protected readonly imageFailed = signal(false);
116
+
117
+ protected readonly initials = computed(() => {
118
+ const parts = this.name().trim().split(/\s+/).filter(Boolean);
119
+ if (parts.length === 0) {
120
+ return "?";
121
+ }
122
+ const first = parts[0].charAt(0);
123
+ const last = parts.length > 1 ? parts[parts.length - 1].charAt(0) : "";
124
+ return `${first}${last}`;
125
+ });
126
+ }