ngx-com 0.0.1 → 0.0.4

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 (82) hide show
  1. package/fesm2022/ngx-com-components-avatar.mjs +772 -0
  2. package/fesm2022/ngx-com-components-avatar.mjs.map +1 -0
  3. package/fesm2022/ngx-com-components-badge.mjs +138 -0
  4. package/fesm2022/ngx-com-components-badge.mjs.map +1 -0
  5. package/fesm2022/ngx-com-components-button.mjs +146 -0
  6. package/fesm2022/ngx-com-components-button.mjs.map +1 -0
  7. package/fesm2022/ngx-com-components-calendar.mjs +5046 -0
  8. package/fesm2022/ngx-com-components-calendar.mjs.map +1 -0
  9. package/fesm2022/ngx-com-components-card.mjs +590 -0
  10. package/fesm2022/ngx-com-components-card.mjs.map +1 -0
  11. package/fesm2022/ngx-com-components-checkbox.mjs +344 -0
  12. package/fesm2022/ngx-com-components-checkbox.mjs.map +1 -0
  13. package/fesm2022/ngx-com-components-collapsible.mjs +612 -0
  14. package/fesm2022/ngx-com-components-collapsible.mjs.map +1 -0
  15. package/fesm2022/ngx-com-components-confirm.mjs +562 -0
  16. package/fesm2022/ngx-com-components-confirm.mjs.map +1 -0
  17. package/fesm2022/ngx-com-components-dropdown-testing.mjs +255 -0
  18. package/fesm2022/ngx-com-components-dropdown-testing.mjs.map +1 -0
  19. package/fesm2022/ngx-com-components-dropdown.mjs +2692 -0
  20. package/fesm2022/ngx-com-components-dropdown.mjs.map +1 -0
  21. package/fesm2022/ngx-com-components-empty-state.mjs +382 -0
  22. package/fesm2022/ngx-com-components-empty-state.mjs.map +1 -0
  23. package/fesm2022/ngx-com-components-form-field.mjs +924 -0
  24. package/fesm2022/ngx-com-components-form-field.mjs.map +1 -0
  25. package/fesm2022/ngx-com-components-icon.mjs +183 -0
  26. package/fesm2022/ngx-com-components-icon.mjs.map +1 -0
  27. package/fesm2022/ngx-com-components-item.mjs +578 -0
  28. package/fesm2022/ngx-com-components-item.mjs.map +1 -0
  29. package/fesm2022/ngx-com-components-menu.mjs +1200 -0
  30. package/fesm2022/ngx-com-components-menu.mjs.map +1 -0
  31. package/fesm2022/ngx-com-components-paginator.mjs +823 -0
  32. package/fesm2022/ngx-com-components-paginator.mjs.map +1 -0
  33. package/fesm2022/ngx-com-components-popover.mjs +901 -0
  34. package/fesm2022/ngx-com-components-popover.mjs.map +1 -0
  35. package/fesm2022/ngx-com-components-radio.mjs +621 -0
  36. package/fesm2022/ngx-com-components-radio.mjs.map +1 -0
  37. package/fesm2022/ngx-com-components-segmented-control.mjs +538 -0
  38. package/fesm2022/ngx-com-components-segmented-control.mjs.map +1 -0
  39. package/fesm2022/ngx-com-components-sort.mjs +368 -0
  40. package/fesm2022/ngx-com-components-sort.mjs.map +1 -0
  41. package/fesm2022/ngx-com-components-spinner.mjs +189 -0
  42. package/fesm2022/ngx-com-components-spinner.mjs.map +1 -0
  43. package/fesm2022/ngx-com-components-tabs.mjs +1522 -0
  44. package/fesm2022/ngx-com-components-tabs.mjs.map +1 -0
  45. package/fesm2022/ngx-com-components-tooltip.mjs +625 -0
  46. package/fesm2022/ngx-com-components-tooltip.mjs.map +1 -0
  47. package/fesm2022/ngx-com-components.mjs +17 -0
  48. package/fesm2022/ngx-com-components.mjs.map +1 -0
  49. package/fesm2022/ngx-com-tokens.mjs +12 -0
  50. package/fesm2022/ngx-com-tokens.mjs.map +1 -0
  51. package/fesm2022/ngx-com-utils.mjs +601 -0
  52. package/fesm2022/ngx-com-utils.mjs.map +1 -0
  53. package/fesm2022/ngx-com.mjs +9 -23
  54. package/fesm2022/ngx-com.mjs.map +1 -1
  55. package/package.json +105 -1
  56. package/types/ngx-com-components-avatar.d.ts +409 -0
  57. package/types/ngx-com-components-badge.d.ts +97 -0
  58. package/types/ngx-com-components-button.d.ts +69 -0
  59. package/types/ngx-com-components-calendar.d.ts +1665 -0
  60. package/types/ngx-com-components-card.d.ts +373 -0
  61. package/types/ngx-com-components-checkbox.d.ts +116 -0
  62. package/types/ngx-com-components-collapsible.d.ts +379 -0
  63. package/types/ngx-com-components-confirm.d.ts +160 -0
  64. package/types/ngx-com-components-dropdown-testing.d.ts +116 -0
  65. package/types/ngx-com-components-dropdown.d.ts +938 -0
  66. package/types/ngx-com-components-empty-state.d.ts +269 -0
  67. package/types/ngx-com-components-form-field.d.ts +531 -0
  68. package/types/ngx-com-components-icon.d.ts +94 -0
  69. package/types/ngx-com-components-item.d.ts +336 -0
  70. package/types/ngx-com-components-menu.d.ts +479 -0
  71. package/types/ngx-com-components-paginator.d.ts +265 -0
  72. package/types/ngx-com-components-popover.d.ts +309 -0
  73. package/types/ngx-com-components-radio.d.ts +258 -0
  74. package/types/ngx-com-components-segmented-control.d.ts +274 -0
  75. package/types/ngx-com-components-sort.d.ts +133 -0
  76. package/types/ngx-com-components-spinner.d.ts +120 -0
  77. package/types/ngx-com-components-tabs.d.ts +396 -0
  78. package/types/ngx-com-components-tooltip.d.ts +200 -0
  79. package/types/ngx-com-components.d.ts +12 -0
  80. package/types/ngx-com-tokens.d.ts +7 -0
  81. package/types/ngx-com-utils.d.ts +424 -0
  82. package/types/ngx-com.d.ts +10 -7
@@ -0,0 +1,772 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, TemplateRef, Directive, ElementRef, input, output, contentChild, linkedSignal, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, Renderer2, contentChildren, signal, effect } from '@angular/core';
3
+ import { NgTemplateOutlet } from '@angular/common';
4
+ import { ComIcon } from 'ngx-com/components/icon';
5
+ import { cva } from 'class-variance-authority';
6
+
7
+ /**
8
+ * Directive to provide a custom template for avatar content.
9
+ *
10
+ * When this directive is used, the avatar ignores the `src`, `name`, and
11
+ * default icon fallback — the template has full control over the content.
12
+ * Use this for company logos, emoji avatars, or custom graphics.
13
+ *
14
+ * @example Company logo
15
+ * ```html
16
+ * <com-avatar name="Acme Corp" size="lg" color="primary">
17
+ * <ng-template comAvatarCustom let-initials="initials">
18
+ * <img src="/logos/acme.svg" class="size-full object-contain p-1" alt="Acme Corp" />
19
+ * </ng-template>
20
+ * </com-avatar>
21
+ * ```
22
+ *
23
+ * @example Emoji avatar
24
+ * ```html
25
+ * <com-avatar name="Bot" color="accent" variant="filled">
26
+ * <ng-template comAvatarCustom>
27
+ * <span class="text-lg">🤖</span>
28
+ * </ng-template>
29
+ * </com-avatar>
30
+ * ```
31
+ *
32
+ * @example Adaptive content using size context
33
+ * ```html
34
+ * <com-avatar name="Jane" [size]="avatarSize">
35
+ * <ng-template comAvatarCustom let-size="size">
36
+ * @if (size === 'xs' || size === 'sm') {
37
+ * <span class="text-xs">👤</span>
38
+ * } @else {
39
+ * <img src="/custom-avatar.png" class="size-full object-cover" />
40
+ * }
41
+ * </ng-template>
42
+ * </com-avatar>
43
+ * ```
44
+ */
45
+ class ComAvatarCustom {
46
+ templateRef = inject(TemplateRef);
47
+ /**
48
+ * Static type guard for template type checking.
49
+ * Enables type-safe access to context properties in templates.
50
+ */
51
+ static ngTemplateContextGuard(_dir, ctx) {
52
+ return true;
53
+ }
54
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComAvatarCustom, deps: [], target: i0.ɵɵFactoryTarget.Directive });
55
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: ComAvatarCustom, isStandalone: true, selector: "ng-template[comAvatarCustom]", ngImport: i0 });
56
+ }
57
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComAvatarCustom, decorators: [{
58
+ type: Directive,
59
+ args: [{
60
+ selector: 'ng-template[comAvatarCustom]',
61
+ }]
62
+ }] });
63
+
64
+ /** Colors available for auto-color generation. */
65
+ const AUTO_COLORS = ['primary', 'accent', 'warn'];
66
+ /**
67
+ * Deterministically generates a color index from a name string.
68
+ * The same name always produces the same color.
69
+ */
70
+ function nameToColorIndex(name) {
71
+ let hash = 0;
72
+ for (let i = 0; i < name.length; i++) {
73
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
74
+ hash = hash & hash; // Convert to 32bit integer
75
+ }
76
+ return Math.abs(hash) % AUTO_COLORS.length;
77
+ }
78
+ /**
79
+ * Resolves an auto color to a concrete color based on the name.
80
+ * Falls back to 'muted' when name is empty or undefined.
81
+ */
82
+ function resolveAutoColor(color, name) {
83
+ if (color !== 'auto') {
84
+ return color;
85
+ }
86
+ if (!name?.trim()) {
87
+ return 'muted';
88
+ }
89
+ return AUTO_COLORS[nameToColorIndex(name)];
90
+ }
91
+ /**
92
+ * Generates initials from a display name.
93
+ *
94
+ * @param name - The display name to extract initials from
95
+ * @param maxLength - Maximum number of characters (default: 2)
96
+ * @returns Uppercase initials, or empty string if name is empty
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * getInitials('Jane Doe') // 'JD'
101
+ * getInitials('Jane') // 'JA'
102
+ * getInitials('Jane Marie Doe') // 'JD' (first + last)
103
+ * getInitials('j') // 'J'
104
+ * getInitials('') // ''
105
+ * ```
106
+ */
107
+ function getInitials(name, maxLength = 2) {
108
+ const trimmed = name.trim();
109
+ if (!trimmed) {
110
+ return '';
111
+ }
112
+ const words = trimmed.split(/\s+/);
113
+ if (words.length === 1) {
114
+ // Single word: take first N characters
115
+ return words[0].slice(0, maxLength).toUpperCase();
116
+ }
117
+ // Multiple words: first char of first word + first char of last word
118
+ const first = words[0].charAt(0);
119
+ const last = words[words.length - 1].charAt(0);
120
+ return (first + last).slice(0, maxLength).toUpperCase();
121
+ }
122
+ /**
123
+ * CVA variants for the avatar container.
124
+ *
125
+ * @tokens `--color-primary`, `--color-primary-foreground`, `--color-primary-subtle`, `--color-primary-subtle-foreground`,
126
+ * `--color-accent`, `--color-accent-foreground`, `--color-accent-subtle`, `--color-accent-subtle-foreground`,
127
+ * `--color-warn`, `--color-warn-foreground`, `--color-warn-subtle`, `--color-warn-subtle-foreground`,
128
+ * `--color-muted`, `--color-muted-foreground`,
129
+ * `--color-border`, `--color-background`, `--color-foreground`, `--color-ring`
130
+ */
131
+ const avatarVariants = cva([
132
+ 'com-avatar',
133
+ 'relative overflow-hidden inline-flex items-center justify-center',
134
+ 'shrink-0 select-none',
135
+ ], {
136
+ variants: {
137
+ size: {
138
+ xs: 'size-5',
139
+ sm: 'size-7',
140
+ md: 'size-9',
141
+ lg: 'size-12',
142
+ xl: 'size-16',
143
+ '2xl': 'size-24',
144
+ },
145
+ shape: {
146
+ circle: 'rounded-full',
147
+ rounded: 'rounded-lg',
148
+ },
149
+ interactive: {
150
+ true: [
151
+ 'cursor-pointer',
152
+ 'hover:ring-2 hover:ring-ring',
153
+ 'active:scale-95',
154
+ 'transition-all duration-150',
155
+ ],
156
+ false: 'cursor-default',
157
+ },
158
+ },
159
+ compoundVariants: [
160
+ // Smaller rounded corners for xs/sm sizes
161
+ { shape: 'rounded', size: 'xs', class: 'rounded-md' },
162
+ { shape: 'rounded', size: 'sm', class: 'rounded-md' },
163
+ ],
164
+ defaultVariants: {
165
+ size: 'md',
166
+ shape: 'circle',
167
+ interactive: false,
168
+ },
169
+ });
170
+ /**
171
+ * CVA variants for the avatar color/variant styling.
172
+ * These are applied based on the resolved color and variant.
173
+ */
174
+ const avatarColorVariants = cva('', {
175
+ variants: {
176
+ variant: {
177
+ soft: '',
178
+ filled: '',
179
+ outline: 'ring-2 ring-background bg-background text-foreground',
180
+ },
181
+ color: {
182
+ primary: '',
183
+ accent: '',
184
+ muted: '',
185
+ warn: '',
186
+ },
187
+ },
188
+ compoundVariants: [
189
+ // Soft variants
190
+ { variant: 'soft', color: 'primary', class: 'bg-primary-subtle text-primary-subtle-foreground' },
191
+ { variant: 'soft', color: 'accent', class: 'bg-accent-subtle text-accent-subtle-foreground' },
192
+ { variant: 'soft', color: 'warn', class: 'bg-warn-subtle text-warn-subtle-foreground' },
193
+ { variant: 'soft', color: 'muted', class: 'bg-muted text-muted-foreground' },
194
+ // Filled variants
195
+ { variant: 'filled', color: 'primary', class: 'bg-primary text-primary-foreground' },
196
+ { variant: 'filled', color: 'accent', class: 'bg-accent text-accent-foreground' },
197
+ { variant: 'filled', color: 'warn', class: 'bg-warn text-warn-foreground' },
198
+ { variant: 'filled', color: 'muted', class: 'bg-muted-foreground text-muted' },
199
+ ],
200
+ defaultVariants: {
201
+ variant: 'soft',
202
+ color: 'primary',
203
+ },
204
+ });
205
+ /** Font size classes for initials, keyed by avatar size. */
206
+ const AVATAR_INITIALS_SIZES = {
207
+ xs: 'text-[0.5rem]',
208
+ sm: 'text-xs',
209
+ md: 'text-sm',
210
+ lg: 'text-base',
211
+ xl: 'text-xl',
212
+ '2xl': 'text-3xl',
213
+ };
214
+ /** Ring width classes for outline variant, keyed by avatar size. */
215
+ const AVATAR_OUTLINE_RING_SIZES = {
216
+ xs: 'ring-1',
217
+ sm: 'ring-[1.5px]',
218
+ md: 'ring-2',
219
+ lg: 'ring-2',
220
+ xl: 'ring-2',
221
+ '2xl': 'ring-[3px]',
222
+ };
223
+
224
+ /**
225
+ * Avatar component — displays a user's profile image, initials, or a fallback icon.
226
+ *
227
+ * Handles the full lifecycle of image loading with a graceful fallback chain:
228
+ * 1. Custom template (via `comAvatarCustom` directive) — if provided, always wins
229
+ * 2. Image — if `src` is provided and loads successfully
230
+ * 3. Initials — if `name` is provided, auto-generated from the name
231
+ * 4. Default icon — generic user silhouette via `com-icon`
232
+ *
233
+ * **Note:** The default fallback icon requires the `User` icon from lucide-angular
234
+ * to be registered via `provideComIcons({ User })` in your application config.
235
+ *
236
+ * @tokens `--color-primary`, `--color-primary-foreground`, `--color-primary-subtle`, `--color-primary-subtle-foreground`,
237
+ * `--color-accent`, `--color-accent-foreground`, `--color-accent-subtle`, `--color-accent-subtle-foreground`,
238
+ * `--color-warn`, `--color-warn-foreground`, `--color-warn-subtle`, `--color-warn-subtle-foreground`,
239
+ * `--color-muted`, `--color-muted-foreground`,
240
+ * `--color-border`, `--color-background`, `--color-foreground`, `--color-ring`
241
+ *
242
+ * @example Simple image avatar
243
+ * ```html
244
+ * <com-avatar src="/photos/jane.jpg" name="Jane Doe" />
245
+ * ```
246
+ *
247
+ * @example Initials fallback (no image)
248
+ * ```html
249
+ * <com-avatar name="Jane Doe" />
250
+ * <!-- renders "JD" with auto-computed background color -->
251
+ * ```
252
+ *
253
+ * @example Explicit color and shape
254
+ * ```html
255
+ * <com-avatar name="John Smith" color="primary" variant="filled" />
256
+ * <com-avatar name="Alice" color="accent" shape="rounded" />
257
+ * ```
258
+ *
259
+ * @example Sizes — from badge to profile header
260
+ * ```html
261
+ * <!-- Tiny: inside a badge or inline with text -->
262
+ * <com-avatar name="JD" size="xs" />
263
+ *
264
+ * <!-- Small: list items, comments -->
265
+ * <com-avatar src="/photos/jane.jpg" name="Jane" size="sm" />
266
+ *
267
+ * <!-- Medium: default, cards -->
268
+ * <com-avatar src="/photos/jane.jpg" name="Jane" />
269
+ *
270
+ * <!-- Large: profile sidebar -->
271
+ * <com-avatar src="/photos/jane.jpg" name="Jane" size="lg" />
272
+ *
273
+ * <!-- Extra large: profile hero -->
274
+ * <com-avatar src="/photos/jane.jpg" name="Jane" size="xl" />
275
+ *
276
+ * <!-- 2XL: full profile page header -->
277
+ * <com-avatar src="/photos/jane.jpg" name="Jane" size="2xl" />
278
+ * ```
279
+ *
280
+ * @example Default icon fallback (no name, no image)
281
+ * ```html
282
+ * <!-- Shows generic user icon -->
283
+ * <com-avatar />
284
+ * ```
285
+ *
286
+ * @example Interactive (clickable, for menus)
287
+ * ```html
288
+ * <com-avatar
289
+ * src="/photos/me.jpg"
290
+ * name="My Profile"
291
+ * [interactive]="true"
292
+ * (click)="openProfileMenu()"
293
+ * />
294
+ * ```
295
+ *
296
+ * @example With status indicator (composed externally)
297
+ * ```html
298
+ * <!-- The avatar itself doesn't own the status dot — the consumer composes it -->
299
+ * <div class="relative inline-flex">
300
+ * <com-avatar src="/photos/jane.jpg" name="Jane" size="sm" />
301
+ * <span class="absolute bottom-0 right-0 size-2.5 rounded-full bg-success ring-2 ring-background"></span>
302
+ * </div>
303
+ * ```
304
+ *
305
+ * @example Custom template — company logo with fallback
306
+ * ```html
307
+ * <com-avatar name="Acme Corp" size="lg" color="primary">
308
+ * <ng-template comAvatarCustom let-initials="initials">
309
+ * <img src="/logos/acme.svg" class="size-full object-contain p-1" alt="Acme Corp" />
310
+ * </ng-template>
311
+ * </com-avatar>
312
+ * ```
313
+ *
314
+ * @example Custom template — emoji avatar
315
+ * ```html
316
+ * <com-avatar name="Bot" color="accent" variant="filled">
317
+ * <ng-template comAvatarCustom>
318
+ * <span class="text-lg">🤖</span>
319
+ * </ng-template>
320
+ * </com-avatar>
321
+ * ```
322
+ *
323
+ * @example Inline with text
324
+ * ```html
325
+ * <span class="inline-flex items-center gap-2">
326
+ * <com-avatar name="Jane Doe" size="xs" />
327
+ * <span class="text-sm">Jane Doe</span>
328
+ * </span>
329
+ * ```
330
+ *
331
+ * @example Avatar in a badge context
332
+ * ```html
333
+ * <!-- Works at xs/sm sizes without breaking layout -->
334
+ * <div class="flex items-center gap-1.5 rounded-pill bg-muted px-2 py-0.5">
335
+ * <com-avatar name="Jane" size="xs" />
336
+ * <span class="text-xs">Jane Doe</span>
337
+ * <button class="text-muted-foreground hover:text-foreground">
338
+ * <com-icon name="x" size="xs" />
339
+ * </button>
340
+ * </div>
341
+ * ```
342
+ *
343
+ * @example Outline variant (good for overlapping stacks)
344
+ * ```html
345
+ * <div class="flex -space-x-2">
346
+ * <com-avatar src="/photos/a.jpg" name="Alice" size="sm" variant="outline" />
347
+ * <com-avatar src="/photos/b.jpg" name="Bob" size="sm" variant="outline" />
348
+ * <com-avatar src="/photos/c.jpg" name="Carol" size="sm" variant="outline" />
349
+ * <com-avatar name="+3" size="sm" variant="outline" color="muted" />
350
+ * </div>
351
+ * ```
352
+ */
353
+ class ComAvatar {
354
+ /** Host element reference (used by ComAvatarGroup). */
355
+ elementRef = inject(ElementRef);
356
+ // ─── Content ───
357
+ /** Image URL for the avatar. */
358
+ src = input(...(ngDevMode ? [undefined, { debugName: "src" }] : []));
359
+ /** Alt text for the image. Falls back to `name` if not provided. */
360
+ alt = input(...(ngDevMode ? [undefined, { debugName: "alt" }] : []));
361
+ /** User's display name — used to generate initials and as aria fallback. */
362
+ name = input(...(ngDevMode ? [undefined, { debugName: "name" }] : []));
363
+ // ─── CVA Variants ───
364
+ /** Size variant. */
365
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
366
+ /** Color variant. 'auto' deterministically picks a color based on the name. */
367
+ color = input('auto', ...(ngDevMode ? [{ debugName: "color" }] : []));
368
+ /** Shape variant. */
369
+ shape = input('circle', ...(ngDevMode ? [{ debugName: "shape" }] : []));
370
+ /** Visual style variant. */
371
+ variant = input('soft', ...(ngDevMode ? [{ debugName: "variant" }] : []));
372
+ // ─── Behavior ───
373
+ /** When true, renders as a button with hover/active states. */
374
+ interactive = input(false, ...(ngDevMode ? [{ debugName: "interactive" }] : []));
375
+ // ─── Outputs ───
376
+ /** Emits when the image fails to load (after fallback kicks in). */
377
+ imageError = output();
378
+ /** Emits when the image loads successfully. */
379
+ imageLoaded = output();
380
+ // ─── Template Projection ───
381
+ /** Custom template for full control over avatar content. */
382
+ customTemplate = contentChild(ComAvatarCustom, ...(ngDevMode ? [{ debugName: "customTemplate" }] : []));
383
+ // ─── Internal State ───
384
+ /**
385
+ * Current image loading state.
386
+ * Resets to 'loading' or 'idle' when `src` changes.
387
+ */
388
+ imageState = linkedSignal({ ...(ngDevMode ? { debugName: "imageState" } : {}), source: this.src,
389
+ computation: (src) => src ? 'loading' : 'idle' });
390
+ // ─── Computed Values ───
391
+ /** Resolved color (handles 'auto' based on name). */
392
+ resolvedColor = computed(() => resolveAutoColor(this.color(), this.name()), ...(ngDevMode ? [{ debugName: "resolvedColor" }] : []));
393
+ /** Computed initials from the name. */
394
+ computedInitials = computed(() => {
395
+ const name = this.name();
396
+ return name ? getInitials(name) : '';
397
+ }, ...(ngDevMode ? [{ debugName: "computedInitials" }] : []));
398
+ /** Template context for custom templates. */
399
+ templateContext = computed(() => ({
400
+ $implicit: this.name(),
401
+ initials: this.computedInitials(),
402
+ size: this.size(),
403
+ }), ...(ngDevMode ? [{ debugName: "templateContext" }] : []));
404
+ /** CSS classes for the host element. */
405
+ hostClasses = computed(() => {
406
+ const baseClasses = avatarVariants({
407
+ size: this.size(),
408
+ shape: this.shape(),
409
+ interactive: this.interactive(),
410
+ });
411
+ const colorClasses = this.variant() === 'outline'
412
+ ? avatarColorVariants({ variant: 'outline' })
413
+ : avatarColorVariants({
414
+ variant: this.variant(),
415
+ color: this.resolvedColor(),
416
+ });
417
+ // Add outline ring size for outline variant
418
+ const ringClass = this.variant() === 'outline'
419
+ ? AVATAR_OUTLINE_RING_SIZES[this.size()]
420
+ : '';
421
+ return [baseClasses, colorClasses, ringClass].filter(Boolean).join(' ');
422
+ }, ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
423
+ /** Font size class for initials. */
424
+ initialsSizeClass = computed(() => AVATAR_INITIALS_SIZES[this.size()], ...(ngDevMode ? [{ debugName: "initialsSizeClass" }] : []));
425
+ /** Icon size for the fallback icon (same as avatar size). */
426
+ iconSize = this.size;
427
+ // ─── Event Handlers ───
428
+ /** @internal Handles successful image load. */
429
+ onImageLoad() {
430
+ this.imageState.set('loaded');
431
+ this.imageLoaded.emit();
432
+ }
433
+ /** @internal Handles image load error. */
434
+ onImageError() {
435
+ this.imageState.set('error');
436
+ this.imageError.emit();
437
+ }
438
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComAvatar, deps: [], target: i0.ɵɵFactoryTarget.Component });
439
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComAvatar, isStandalone: true, selector: "com-avatar", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, alt: { classPropertyName: "alt", publicName: "alt", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { imageError: "imageError", imageLoaded: "imageLoaded" }, host: { properties: { "class": "hostClasses()", "attr.role": "interactive() ? \"button\" : \"img\"", "attr.tabindex": "interactive() ? 0 : null", "attr.aria-label": "alt() || name() || \"User avatar\"" } }, queries: [{ propertyName: "customTemplate", first: true, predicate: ComAvatarCustom, descendants: true, isSignal: true }], exportAs: ["comAvatar"], ngImport: i0, template: `
440
+ <!-- Layer 1: Background content (initials, icon, or custom template) -->
441
+ @if (customTemplate(); as template) {
442
+ <!-- Custom template — full consumer control -->
443
+ <ng-container
444
+ [ngTemplateOutlet]="template.templateRef"
445
+ [ngTemplateOutletContext]="templateContext()"
446
+ />
447
+ } @else if (computedInitials()) {
448
+ <!-- Initials -->
449
+ <span
450
+ aria-hidden="true"
451
+ class="font-medium leading-none"
452
+ [class]="initialsSizeClass()"
453
+ >
454
+ {{ computedInitials() }}
455
+ </span>
456
+ } @else {
457
+ <!-- Default icon fallback -->
458
+ <com-icon name="user" [size]="iconSize()" aria-hidden="true" />
459
+ }
460
+
461
+ <!-- Layer 2: Image (overlays the fallback when loaded) -->
462
+ @if (src() && !customTemplate()) {
463
+ <img
464
+ [src]="src()"
465
+ [alt]="alt() || name() || 'Avatar'"
466
+ class="absolute inset-0 size-full object-cover transition-opacity duration-200"
467
+ [class.opacity-0]="imageState() !== 'loaded'"
468
+ [class.opacity-100]="imageState() === 'loaded'"
469
+ [style.border-radius]="'inherit'"
470
+ (load)="onImageLoad()"
471
+ (error)="onImageError()"
472
+ />
473
+ }
474
+
475
+ <!-- Screen reader text -->
476
+ <span class="sr-only">{{ alt() || name() || 'User avatar' }}</span>
477
+ `, isInline: true, styles: [".sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: ComIcon, selector: "com-icon", inputs: ["name", "img", "color", "size", "strokeWidth", "absoluteStrokeWidth", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
478
+ }
479
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComAvatar, decorators: [{
480
+ type: Component,
481
+ args: [{ selector: 'com-avatar', exportAs: 'comAvatar', template: `
482
+ <!-- Layer 1: Background content (initials, icon, or custom template) -->
483
+ @if (customTemplate(); as template) {
484
+ <!-- Custom template — full consumer control -->
485
+ <ng-container
486
+ [ngTemplateOutlet]="template.templateRef"
487
+ [ngTemplateOutletContext]="templateContext()"
488
+ />
489
+ } @else if (computedInitials()) {
490
+ <!-- Initials -->
491
+ <span
492
+ aria-hidden="true"
493
+ class="font-medium leading-none"
494
+ [class]="initialsSizeClass()"
495
+ >
496
+ {{ computedInitials() }}
497
+ </span>
498
+ } @else {
499
+ <!-- Default icon fallback -->
500
+ <com-icon name="user" [size]="iconSize()" aria-hidden="true" />
501
+ }
502
+
503
+ <!-- Layer 2: Image (overlays the fallback when loaded) -->
504
+ @if (src() && !customTemplate()) {
505
+ <img
506
+ [src]="src()"
507
+ [alt]="alt() || name() || 'Avatar'"
508
+ class="absolute inset-0 size-full object-cover transition-opacity duration-200"
509
+ [class.opacity-0]="imageState() !== 'loaded'"
510
+ [class.opacity-100]="imageState() === 'loaded'"
511
+ [style.border-radius]="'inherit'"
512
+ (load)="onImageLoad()"
513
+ (error)="onImageError()"
514
+ />
515
+ }
516
+
517
+ <!-- Screen reader text -->
518
+ <span class="sr-only">{{ alt() || name() || 'User avatar' }}</span>
519
+ `, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [NgTemplateOutlet, ComIcon], host: {
520
+ '[class]': 'hostClasses()',
521
+ '[attr.role]': 'interactive() ? "button" : "img"',
522
+ '[attr.tabindex]': 'interactive() ? 0 : null',
523
+ '[attr.aria-label]': 'alt() || name() || "User avatar"',
524
+ }, styles: [".sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
525
+ }], propDecorators: { src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], alt: [{ type: i0.Input, args: [{ isSignal: true, alias: "alt", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], imageError: [{ type: i0.Output, args: ["imageError"] }], imageLoaded: [{ type: i0.Output, args: ["imageLoaded"] }], customTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComAvatarCustom), { isSignal: true }] }] } });
526
+
527
+ /** Spacing between overlapping avatars, keyed by size. */
528
+ const AVATAR_GROUP_SPACING = {
529
+ xs: '-0.375rem', // -6px, ~30% of 20px
530
+ sm: '-0.5rem', // -8px, ~29% of 28px
531
+ md: '-0.625rem', // -10px, ~28% of 36px
532
+ lg: '-0.75rem', // -12px, ~25% of 48px
533
+ xl: '-1rem', // -16px, ~25% of 64px
534
+ '2xl': '-1.5rem', // -24px, ~25% of 96px
535
+ };
536
+ /** Ring width for visual separation, keyed by size. */
537
+ const AVATAR_GROUP_RING = {
538
+ xs: '1px',
539
+ sm: '1.5px',
540
+ md: '2px',
541
+ lg: '2px',
542
+ xl: '2px',
543
+ '2xl': '3px',
544
+ };
545
+ /**
546
+ * Avatar group directive — displays multiple avatars in an overlapping stack.
547
+ *
548
+ * Apply this directive to a container element with `com-avatar` children.
549
+ * The directive handles negative spacing, ring styling for visual separation,
550
+ * and optionally limits the visible avatars with an overflow indicator.
551
+ *
552
+ * **Note:** Child avatars should use `variant="outline"` for best visual results,
553
+ * as this provides the ring that separates overlapping avatars. The directive
554
+ * adds `ring-background` to ensure proper visual separation.
555
+ *
556
+ * @tokens `--color-background`, `--color-muted`, `--color-muted-foreground`
557
+ *
558
+ * @example Basic usage
559
+ * ```html
560
+ * <div comAvatarGroup>
561
+ * <com-avatar src="/photos/a.jpg" name="Alice" variant="outline" />
562
+ * <com-avatar src="/photos/b.jpg" name="Bob" variant="outline" />
563
+ * <com-avatar src="/photos/c.jpg" name="Carol" variant="outline" />
564
+ * </div>
565
+ * ```
566
+ *
567
+ * @example With max limit and overflow indicator
568
+ * ```html
569
+ * <div comAvatarGroup [max]="3">
570
+ * <com-avatar src="/photos/a.jpg" name="Alice" variant="outline" />
571
+ * <com-avatar src="/photos/b.jpg" name="Bob" variant="outline" />
572
+ * <com-avatar src="/photos/c.jpg" name="Carol" variant="outline" />
573
+ * <com-avatar src="/photos/d.jpg" name="Dave" variant="outline" />
574
+ * <com-avatar src="/photos/e.jpg" name="Eve" variant="outline" />
575
+ * </div>
576
+ * <!-- Shows 3 avatars + "+2" indicator -->
577
+ * ```
578
+ *
579
+ * @example Different sizes
580
+ * ```html
581
+ * <div comAvatarGroup size="sm">
582
+ * <com-avatar name="A" size="sm" variant="outline" />
583
+ * <com-avatar name="B" size="sm" variant="outline" />
584
+ * </div>
585
+ *
586
+ * <div comAvatarGroup size="lg">
587
+ * <com-avatar name="A" size="lg" variant="outline" />
588
+ * <com-avatar name="B" size="lg" variant="outline" />
589
+ * </div>
590
+ * ```
591
+ *
592
+ * @example Reversed stacking (last avatar on top)
593
+ * ```html
594
+ * <div comAvatarGroup [reverse]="true">
595
+ * <com-avatar name="First" variant="outline" />
596
+ * <com-avatar name="Second" variant="outline" />
597
+ * <com-avatar name="Third (on top)" variant="outline" />
598
+ * </div>
599
+ * ```
600
+ */
601
+ class ComAvatarGroup {
602
+ renderer = inject(Renderer2);
603
+ elementRef = inject(ElementRef);
604
+ /** Query all child ComAvatar components. */
605
+ avatars = contentChildren(ComAvatar, ...(ngDevMode ? [{ debugName: "avatars" }] : []));
606
+ /**
607
+ * Size variant — should match child avatar sizes for proper spacing.
608
+ * If not provided, defaults to 'md'.
609
+ */
610
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
611
+ /**
612
+ * Maximum number of avatars to display.
613
+ * When exceeded, remaining avatars are hidden and an overflow indicator shows "+N".
614
+ * Set to 0 or undefined for unlimited.
615
+ */
616
+ max = input(...(ngDevMode ? [undefined, { debugName: "max" }] : []));
617
+ /**
618
+ * When true, reverses the stacking order (last avatar on top instead of first).
619
+ * Also reverses the visual order via flex-row-reverse.
620
+ */
621
+ reverse = input(false, ...(ngDevMode ? [{ debugName: "reverse" }] : []));
622
+ /** Overflow element reference for cleanup. */
623
+ overflowElement = null;
624
+ /** Track the number of hidden avatars. */
625
+ overflowCount = signal(0, ...(ngDevMode ? [{ debugName: "overflowCount" }] : []));
626
+ /** Whether to show the overflow indicator. */
627
+ showOverflow = computed(() => this.overflowCount() > 0, ...(ngDevMode ? [{ debugName: "showOverflow" }] : []));
628
+ constructor() {
629
+ // Apply styling to avatars when they change
630
+ effect(() => {
631
+ const avatarList = this.avatars();
632
+ const maxCount = this.max();
633
+ const size = this.size();
634
+ const isReverse = this.reverse();
635
+ this.applyAvatarStyles(avatarList, maxCount, size, isReverse);
636
+ });
637
+ }
638
+ /**
639
+ * Applies overlapping styles to child avatars and manages visibility.
640
+ */
641
+ applyAvatarStyles(avatars, max, size, reverse) {
642
+ const spacing = AVATAR_GROUP_SPACING[size];
643
+ const ringWidth = AVATAR_GROUP_RING[size];
644
+ const total = avatars.length;
645
+ const visibleCount = max && max > 0 ? Math.min(max, total) : total;
646
+ const hiddenCount = total - visibleCount;
647
+ this.overflowCount.set(hiddenCount);
648
+ avatars.forEach((avatar, index) => {
649
+ const hostEl = avatar['elementRef']?.nativeElement
650
+ ?? this.getAvatarElement(index);
651
+ if (!hostEl)
652
+ return;
653
+ // Determine if this avatar should be visible
654
+ const isVisible = index < visibleCount;
655
+ // Apply visibility
656
+ this.renderer.setStyle(hostEl, 'display', isVisible ? 'inline-flex' : 'none');
657
+ if (!isVisible)
658
+ return;
659
+ // Apply negative margin for overlap (not on first visible item)
660
+ if (index > 0) {
661
+ const marginProp = reverse ? 'marginRight' : 'marginLeft';
662
+ this.renderer.setStyle(hostEl, marginProp, spacing);
663
+ }
664
+ // Apply z-index for stacking (first on top by default, last on top if reverse)
665
+ const zIndex = reverse ? index + 1 : total - index;
666
+ this.renderer.setStyle(hostEl, 'zIndex', zIndex.toString());
667
+ this.renderer.setStyle(hostEl, 'position', 'relative');
668
+ // Add ring for visual separation
669
+ this.renderer.setStyle(hostEl, 'boxShadow', `0 0 0 ${ringWidth} var(--color-background)`);
670
+ });
671
+ // Manage overflow indicator
672
+ this.updateOverflowIndicator(hiddenCount, size, spacing, reverse, visibleCount);
673
+ }
674
+ /**
675
+ * Gets the native element of an avatar by index.
676
+ */
677
+ getAvatarElement(index) {
678
+ const children = this.elementRef.nativeElement.querySelectorAll('com-avatar');
679
+ return children[index];
680
+ }
681
+ /**
682
+ * Creates or updates the overflow indicator element.
683
+ */
684
+ updateOverflowIndicator(count, size, spacing, reverse, _visibleCount) {
685
+ if (count <= 0) {
686
+ // Remove existing overflow element
687
+ if (this.overflowElement) {
688
+ this.renderer.removeChild(this.elementRef.nativeElement, this.overflowElement);
689
+ this.overflowElement = null;
690
+ }
691
+ return;
692
+ }
693
+ // Create overflow element if it doesn't exist
694
+ let el = this.overflowElement;
695
+ if (!el) {
696
+ el = this.renderer.createElement('span');
697
+ this.renderer.addClass(el, 'com-avatar-group__overflow');
698
+ this.overflowElement = el;
699
+ }
700
+ // Apply size-specific classes and styles
701
+ const sizeClasses = this.getOverflowSizeClasses(size);
702
+ const ringWidth = AVATAR_GROUP_RING[size];
703
+ // Reset classes and reapply
704
+ el.className = 'com-avatar-group__overflow';
705
+ sizeClasses.forEach(cls => this.renderer.addClass(el, cls));
706
+ // Apply base styles
707
+ this.renderer.setStyle(el, 'display', 'inline-flex');
708
+ this.renderer.setStyle(el, 'alignItems', 'center');
709
+ this.renderer.setStyle(el, 'justifyContent', 'center');
710
+ this.renderer.setStyle(el, 'borderRadius', '9999px');
711
+ this.renderer.setStyle(el, 'backgroundColor', 'var(--color-muted)');
712
+ this.renderer.setStyle(el, 'color', 'var(--color-muted-foreground)');
713
+ this.renderer.setStyle(el, 'fontWeight', '500');
714
+ this.renderer.setStyle(el, 'position', 'relative');
715
+ this.renderer.setStyle(el, 'zIndex', '0');
716
+ this.renderer.setStyle(el, 'boxShadow', `0 0 0 ${ringWidth} var(--color-background)`);
717
+ this.renderer.setStyle(el, 'userSelect', 'none');
718
+ // Apply negative margin
719
+ const marginProp = reverse ? 'marginRight' : 'marginLeft';
720
+ this.renderer.setStyle(el, marginProp, spacing);
721
+ // Update text content
722
+ el.textContent = `+${count}`;
723
+ // Ensure it's in the DOM at the correct position
724
+ if (!el.parentElement) {
725
+ if (reverse) {
726
+ // Insert at the beginning for reverse mode
727
+ this.renderer.insertBefore(this.elementRef.nativeElement, el, this.elementRef.nativeElement.firstChild);
728
+ }
729
+ else {
730
+ // Append at the end for normal mode
731
+ this.renderer.appendChild(this.elementRef.nativeElement, el);
732
+ }
733
+ }
734
+ }
735
+ /**
736
+ * Returns size-specific classes for the overflow indicator.
737
+ */
738
+ getOverflowSizeClasses(size) {
739
+ const sizeMap = {
740
+ xs: ['size-5', 'text-[0.5rem]'],
741
+ sm: ['size-7', 'text-xs'],
742
+ md: ['size-9', 'text-sm'],
743
+ lg: ['size-12', 'text-base'],
744
+ xl: ['size-16', 'text-xl'],
745
+ '2xl': ['size-24', 'text-3xl'],
746
+ };
747
+ return sizeMap[size];
748
+ }
749
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComAvatarGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
750
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "21.2.0", type: ComAvatarGroup, isStandalone: true, selector: "[comAvatarGroup]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, reverse: { classPropertyName: "reverse", publicName: "reverse", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.flex-row-reverse": "reverse()" }, classAttribute: "com-avatar-group inline-flex items-center" }, queries: [{ propertyName: "avatars", predicate: ComAvatar, isSignal: true }], exportAs: ["comAvatarGroup"], ngImport: i0 });
751
+ }
752
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComAvatarGroup, decorators: [{
753
+ type: Directive,
754
+ args: [{
755
+ selector: '[comAvatarGroup]',
756
+ exportAs: 'comAvatarGroup',
757
+ host: {
758
+ class: 'com-avatar-group inline-flex items-center',
759
+ '[class.flex-row-reverse]': 'reverse()',
760
+ },
761
+ }]
762
+ }], ctorParameters: () => [], propDecorators: { avatars: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => ComAvatar), { isSignal: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], reverse: [{ type: i0.Input, args: [{ isSignal: true, alias: "reverse", required: false }] }] } });
763
+
764
+ // Public API for the avatar component
765
+ // Main component
766
+
767
+ /**
768
+ * Generated bundle index. Do not edit.
769
+ */
770
+
771
+ export { ComAvatar, ComAvatarCustom, ComAvatarGroup, avatarColorVariants, avatarVariants, getInitials };
772
+ //# sourceMappingURL=ngx-com-components-avatar.mjs.map