@frame-kit/ui-ng 0.2.23 → 0.2.25

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.
@@ -70,6 +70,13 @@ class FkContentSplitComponent {
70
70
  }
71
71
  const hostWidth = entry.contentRect.width;
72
72
  const bp = this.breakpoint();
73
+ // A width of 0 means the split is hidden (an inactive tab, a
74
+ // collapsed section), not that the screen is narrow. Keep the
75
+ // last real layout, so showing it again does not fold the left
76
+ // panel away and animate it back.
77
+ if (hostWidth === 0) {
78
+ return;
79
+ }
73
80
  if (hostWidth < bp && this.layout() !== 'narrow') {
74
81
  this.ngZone.run(() => {
75
82
  this.layout.set('narrow');
@@ -1 +1 @@
1
- {"version":3,"file":"frame-kit-ui-ng-layouts-content-split.mjs","sources":["../../../../packages/ui-ng/layouts/content-split/content-split.component.ts","../../../../packages/ui-ng/layouts/content-split/content-split.component.html","../../../../packages/ui-ng/layouts/content-split/frame-kit-ui-ng-layouts-content-split.ts"],"sourcesContent":["import { isPlatformBrowser } from '@angular/common';\nimport {\n ChangeDetectionStrategy,\n Component,\n computed,\n DestroyRef,\n ElementRef,\n HostBinding,\n inject,\n input,\n model,\n NgZone,\n PLATFORM_ID,\n signal,\n} from '@angular/core';\n\nimport type { ContentSplitLayout } from './content-split.types';\n\n@Component({\n selector: 'fk-content-split',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n templateUrl: './content-split.component.html',\n styleUrl: './content-split.component.scss',\n})\nexport class FkContentSplitComponent {\n private readonly el = inject(ElementRef<HTMLElement>);\n private readonly ngZone = inject(NgZone);\n private readonly destroyRef = inject(DestroyRef);\n private readonly platformId = inject(PLATFORM_ID);\n\n // ===== BASE PROPS =====\n readonly className = input<string>('');\n\n // ===== INPUTS =====\n /** Width of the left panel when the layout is in wide mode. */\n readonly leftWidth = input<string>('280px');\n /** Width of the left panel when collapsed to a narrow icon rail. */\n readonly railWidth = input<string>('64px');\n /** Host element pixel width below which the layout switches to narrow mode. */\n readonly breakpoint = input<number>(700);\n /**\n * Optional CSS max-width for the layout. When set, the split is capped at\n * this width and centered in its container (via auto inline margins);\n * unset (default), it fills the full available width as before.\n */\n readonly maxWidth = input<string | null>(null);\n /** Two-way binding for the collapsed state of the left panel. */\n readonly collapsed = model<boolean>(false);\n\n // ===== INTERNAL STATE =====\n readonly layout = signal<ContentSplitLayout>('wide');\n readonly animate = signal(false);\n\n private observer: ResizeObserver | null = null;\n\n // ===== COMPUTED =====\n readonly isNarrow = computed(() => this.layout() === 'narrow');\n readonly isWide = computed(() => this.layout() === 'wide');\n readonly showOverlay = computed(() => this.isNarrow() && !this.collapsed());\n\n readonly classes = computed(() =>\n [\n 'fk-content-split',\n this.isNarrow() ? 'fk-content-split--narrow' : 'fk-content-split--wide',\n this.isNarrow() && !this.collapsed() ? 'fk-content-split--open' : '',\n this.isNarrow() && this.collapsed() ? 'fk-content-split--closed' : '',\n this.animate() ? 'fk-content-split--animate' : '',\n this.className(),\n ]\n .filter(Boolean)\n .join(' '),\n );\n\n // ===== HOST BINDINGS =====\n\n @HostBinding('class')\n get hostClass() {\n return this.classes();\n }\n\n @HostBinding('style.--fk-content-split-left-width')\n get hostLeftWidth() {\n return this.leftWidth();\n }\n\n @HostBinding('style.--fk-content-split-rail-width')\n get hostRailWidth() {\n return this.railWidth();\n }\n\n @HostBinding('style.--fk-content-split-max-width')\n get hostMaxWidth() {\n return this.maxWidth();\n }\n\n // ===== CONSTRUCTOR =====\n\n constructor() {\n // ResizeObserver is browser-only — the SSR pass has no live\n // layout to observe, so the initial layout signal stays at its\n // default (\"wide\") and the browser pass wires the observer once\n // hydration mounts the element.\n if (isPlatformBrowser(this.platformId)) {\n this.ngZone.runOutsideAngular(() => {\n this.observer = new ResizeObserver((entries) => {\n const entry = entries[0];\n\n if (!entry) {\n return;\n }\n\n const hostWidth = entry.contentRect.width;\n const bp = this.breakpoint();\n\n if (hostWidth < bp && this.layout() !== 'narrow') {\n this.ngZone.run(() => {\n this.layout.set('narrow');\n this.animate.set(false);\n this.collapsed.set(true);\n });\n } else if (hostWidth >= bp && this.layout() !== 'wide') {\n this.ngZone.run(() => {\n this.layout.set('wide');\n this.animate.set(false);\n this.collapsed.set(true);\n });\n }\n });\n\n this.observer.observe(this.el.nativeElement);\n });\n }\n\n this.destroyRef.onDestroy(() => {\n this.observer?.disconnect();\n });\n }\n\n // ===== PUBLIC METHODS =====\n\n /** Expand the left panel. */\n open(): void {\n this.animate.set(true);\n this.collapsed.set(false);\n }\n\n /** Collapse the left panel. */\n close(): void {\n this.animate.set(true);\n this.collapsed.set(true);\n }\n\n /** Toggle the left panel between expanded and collapsed. */\n toggle(): void {\n if (this.collapsed()) {\n this.open();\n } else {\n this.close();\n }\n }\n\n /** Dismiss the left panel overlay on narrow viewports (alias for `close`). */\n dismiss(): void {\n this.close();\n }\n}\n","@if (showOverlay()) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div class=\"fk-content-split__overlay\" (click)=\"dismiss()\"></div>\n}\n\n<div\n class=\"fk-content-split__left\"\n [class.fk-content-split__left--open]=\"!collapsed()\"\n>\n <div class=\"fk-content-split__left-content\">\n <ng-content select=\"[contentSplitLeft]\" />\n </div>\n</div>\n\n<div class=\"fk-content-split__rail\">\n <button\n class=\"fk-content-split__rail-toggle\"\n type=\"button\"\n [attr.aria-label]=\"collapsed() ? 'Open panel' : 'Close panel'\"\n (click)=\"toggle()\"\n >\n <span\n class=\"fk-content-split__rail-toggle-icon\"\n [class.fk-content-split__rail-toggle-icon--open]=\"!collapsed()\"\n aria-hidden=\"true\"\n >\n <span class=\"fk-content-split__rail-toggle-projected\">\n <ng-content select=\"[contentSplitToggleIcon]\" />\n </span>\n <svg\n class=\"fk-content-split__rail-toggle-fallback\"\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 18 18\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"M15.25 9H2.75\" />\n <path d=\"M11 4.75L15.25 9L11 13.25\" />\n </svg>\n </span>\n </button>\n</div>\n\n<div class=\"fk-content-split__right\">\n <ng-content select=\"[contentSplitRight]\" />\n</div>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"frame-kit-ui-ng-layouts-content-split.mjs","sources":["../../../../packages/ui-ng/layouts/content-split/content-split.component.ts","../../../../packages/ui-ng/layouts/content-split/content-split.component.html","../../../../packages/ui-ng/layouts/content-split/frame-kit-ui-ng-layouts-content-split.ts"],"sourcesContent":["import { isPlatformBrowser } from '@angular/common';\nimport {\n ChangeDetectionStrategy,\n Component,\n computed,\n DestroyRef,\n ElementRef,\n HostBinding,\n inject,\n input,\n model,\n NgZone,\n PLATFORM_ID,\n signal,\n} from '@angular/core';\n\nimport type { ContentSplitLayout } from './content-split.types';\n\n@Component({\n selector: 'fk-content-split',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n templateUrl: './content-split.component.html',\n styleUrl: './content-split.component.scss',\n})\nexport class FkContentSplitComponent {\n private readonly el = inject(ElementRef<HTMLElement>);\n private readonly ngZone = inject(NgZone);\n private readonly destroyRef = inject(DestroyRef);\n private readonly platformId = inject(PLATFORM_ID);\n\n // ===== BASE PROPS =====\n readonly className = input<string>('');\n\n // ===== INPUTS =====\n /** Width of the left panel when the layout is in wide mode. */\n readonly leftWidth = input<string>('280px');\n /** Width of the left panel when collapsed to a narrow icon rail. */\n readonly railWidth = input<string>('64px');\n /** Host element pixel width below which the layout switches to narrow mode. */\n readonly breakpoint = input<number>(700);\n /**\n * Optional CSS max-width for the layout. When set, the split is capped at\n * this width and centered in its container (via auto inline margins);\n * unset (default), it fills the full available width as before.\n */\n readonly maxWidth = input<string | null>(null);\n /** Two-way binding for the collapsed state of the left panel. */\n readonly collapsed = model<boolean>(false);\n\n // ===== INTERNAL STATE =====\n readonly layout = signal<ContentSplitLayout>('wide');\n readonly animate = signal(false);\n\n private observer: ResizeObserver | null = null;\n\n // ===== COMPUTED =====\n readonly isNarrow = computed(() => this.layout() === 'narrow');\n readonly isWide = computed(() => this.layout() === 'wide');\n readonly showOverlay = computed(() => this.isNarrow() && !this.collapsed());\n\n readonly classes = computed(() =>\n [\n 'fk-content-split',\n this.isNarrow() ? 'fk-content-split--narrow' : 'fk-content-split--wide',\n this.isNarrow() && !this.collapsed() ? 'fk-content-split--open' : '',\n this.isNarrow() && this.collapsed() ? 'fk-content-split--closed' : '',\n this.animate() ? 'fk-content-split--animate' : '',\n this.className(),\n ]\n .filter(Boolean)\n .join(' '),\n );\n\n // ===== HOST BINDINGS =====\n\n @HostBinding('class')\n get hostClass() {\n return this.classes();\n }\n\n @HostBinding('style.--fk-content-split-left-width')\n get hostLeftWidth() {\n return this.leftWidth();\n }\n\n @HostBinding('style.--fk-content-split-rail-width')\n get hostRailWidth() {\n return this.railWidth();\n }\n\n @HostBinding('style.--fk-content-split-max-width')\n get hostMaxWidth() {\n return this.maxWidth();\n }\n\n // ===== CONSTRUCTOR =====\n\n constructor() {\n // ResizeObserver is browser-only — the SSR pass has no live\n // layout to observe, so the initial layout signal stays at its\n // default (\"wide\") and the browser pass wires the observer once\n // hydration mounts the element.\n if (isPlatformBrowser(this.platformId)) {\n this.ngZone.runOutsideAngular(() => {\n this.observer = new ResizeObserver((entries) => {\n const entry = entries[0];\n\n if (!entry) {\n return;\n }\n\n const hostWidth = entry.contentRect.width;\n const bp = this.breakpoint();\n\n // A width of 0 means the split is hidden (an inactive tab, a\n // collapsed section), not that the screen is narrow. Keep the\n // last real layout, so showing it again does not fold the left\n // panel away and animate it back.\n if (hostWidth === 0) {\n return;\n }\n\n if (hostWidth < bp && this.layout() !== 'narrow') {\n this.ngZone.run(() => {\n this.layout.set('narrow');\n this.animate.set(false);\n this.collapsed.set(true);\n });\n } else if (hostWidth >= bp && this.layout() !== 'wide') {\n this.ngZone.run(() => {\n this.layout.set('wide');\n this.animate.set(false);\n this.collapsed.set(true);\n });\n }\n });\n\n this.observer.observe(this.el.nativeElement);\n });\n }\n\n this.destroyRef.onDestroy(() => {\n this.observer?.disconnect();\n });\n }\n\n // ===== PUBLIC METHODS =====\n\n /** Expand the left panel. */\n open(): void {\n this.animate.set(true);\n this.collapsed.set(false);\n }\n\n /** Collapse the left panel. */\n close(): void {\n this.animate.set(true);\n this.collapsed.set(true);\n }\n\n /** Toggle the left panel between expanded and collapsed. */\n toggle(): void {\n if (this.collapsed()) {\n this.open();\n } else {\n this.close();\n }\n }\n\n /** Dismiss the left panel overlay on narrow viewports (alias for `close`). */\n dismiss(): void {\n this.close();\n }\n}\n","@if (showOverlay()) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div class=\"fk-content-split__overlay\" (click)=\"dismiss()\"></div>\n}\n\n<div\n class=\"fk-content-split__left\"\n [class.fk-content-split__left--open]=\"!collapsed()\"\n>\n <div class=\"fk-content-split__left-content\">\n <ng-content select=\"[contentSplitLeft]\" />\n </div>\n</div>\n\n<div class=\"fk-content-split__rail\">\n <button\n class=\"fk-content-split__rail-toggle\"\n type=\"button\"\n [attr.aria-label]=\"collapsed() ? 'Open panel' : 'Close panel'\"\n (click)=\"toggle()\"\n >\n <span\n class=\"fk-content-split__rail-toggle-icon\"\n [class.fk-content-split__rail-toggle-icon--open]=\"!collapsed()\"\n aria-hidden=\"true\"\n >\n <span class=\"fk-content-split__rail-toggle-projected\">\n <ng-content select=\"[contentSplitToggleIcon]\" />\n </span>\n <svg\n class=\"fk-content-split__rail-toggle-fallback\"\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 18 18\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"M15.25 9H2.75\" />\n <path d=\"M11 4.75L15.25 9L11 13.25\" />\n </svg>\n </span>\n </button>\n</div>\n\n<div class=\"fk-content-split__right\">\n <ng-content select=\"[contentSplitRight]\" />\n</div>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -31,7 +31,15 @@ class AvatarComponent {
31
31
  if (!name) {
32
32
  return '?';
33
33
  }
34
- const parts = name.trim().split(/\s+/);
34
+ // Only words that start with a letter count, so a suffix such as
35
+ // "(she/her)" or "(dev)" never lends a bracket to the initials.
36
+ const parts = name
37
+ .trim()
38
+ .split(/\s+/)
39
+ .filter((part) => /^\p{L}/u.test(part));
40
+ if (parts.length === 0) {
41
+ return '?';
42
+ }
35
43
  if (parts.length >= 2) {
36
44
  return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
37
45
  }
@@ -1 +1 @@
1
- {"version":3,"file":"frame-kit-ui-ng-ui-avatar.mjs","sources":["../../../../packages/ui-ng/ui/avatar/avatar.component.ts","../../../../packages/ui-ng/ui/avatar/avatar.component.html","../../../../packages/ui-ng/ui/avatar/frame-kit-ui-ng-ui-avatar.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n computed,\n HostBinding,\n input,\n} from '@angular/core';\n\nimport type { AvatarSize, AvatarStatus } from './avatar.types';\n\n@Component({\n selector: 'fk-avatar',\n standalone: true,\n imports: [],\n changeDetection: ChangeDetectionStrategy.OnPush,\n templateUrl: './avatar.component.html',\n styleUrl: './avatar.component.scss',\n})\nexport class AvatarComponent {\n // ===== IDENTITY =====\n /** Full display name used to derive initials when no explicit initials or image are provided. */\n readonly name = input.required<string>();\n /** URL of the avatar image; falls back to initials when the image fails to load. */\n readonly avatarUrl = input<string | null>(null);\n /** Explicit two-character initials that override the name-derived fallback. */\n readonly initials = input<string | null>(null);\n\n // ===== STATUS =====\n /** Online/offline/away status to reflect on the indicator dot. */\n readonly status = input<AvatarStatus | null>(null);\n /** When true, renders the status indicator dot over the avatar. */\n readonly showStatus = input<boolean>(false);\n\n // ===== DISPLAY =====\n /** Controls the avatar size. */\n readonly size = input<AvatarSize>('md');\n\n // ===== BASE PROPS =====\n readonly className = input<string>('');\n readonly id = input<string | null>(null);\n readonly ariaLabel = input<string | null>(null);\n\n // ===== COMPUTED =====\n\n readonly derivedInitials = computed(() => {\n const explicit = this.initials();\n\n if (explicit) {\n return explicit;\n }\n\n const name = this.name();\n\n if (!name) {\n return '?';\n }\n\n const parts = name.trim().split(/\\s+/);\n\n if (parts.length >= 2) {\n return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();\n }\n\n return parts[0].substring(0, 2).toUpperCase();\n });\n\n readonly showIndicator = computed(\n () => this.showStatus() && this.status() !== null,\n );\n\n readonly classes = computed(() => {\n return [\n 'fk-avatar',\n `fk-avatar--${this.size()}`,\n this.showIndicator() ? `fk-avatar--status-${this.status()}` : '',\n this.className(),\n ]\n .filter(Boolean)\n .join(' ');\n });\n\n @HostBinding('class')\n get hostClass() {\n return this.classes();\n }\n\n @HostBinding('attr.id')\n get hostId(): string | null {\n return this.id();\n }\n\n // Tracks whether the image failed to load\n readonly imgError = { value: false };\n\n onImgError(): void {\n this.imgError.value = true;\n }\n}\n","<!-- Avatar circle -->\n<span\n class=\"fk-avatar__circle\"\n [attr.aria-label]=\"ariaLabel() ?? 'Avatar for ' + name()\"\n role=\"img\"\n>\n @if (avatarUrl() && !imgError.value) {\n <img\n class=\"fk-avatar__image\"\n [src]=\"avatarUrl()\"\n [alt]=\"name()\"\n (error)=\"onImgError()\"\n />\n } @else {\n <span class=\"fk-avatar__initials\" aria-hidden=\"true\">\n {{ derivedInitials() }}\n </span>\n }\n</span>\n\n<!-- Status indicator -->\n@if (showIndicator()) {\n <span\n class=\"fk-avatar__status\"\n [attr.aria-label]=\"status() + ' status'\"\n role=\"status\"\n ></span>\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"frame-kit-ui-ng-ui-avatar.mjs","sources":["../../../../packages/ui-ng/ui/avatar/avatar.component.ts","../../../../packages/ui-ng/ui/avatar/avatar.component.html","../../../../packages/ui-ng/ui/avatar/frame-kit-ui-ng-ui-avatar.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n computed,\n HostBinding,\n input,\n} from '@angular/core';\n\nimport type { AvatarSize, AvatarStatus } from './avatar.types';\n\n@Component({\n selector: 'fk-avatar',\n standalone: true,\n imports: [],\n changeDetection: ChangeDetectionStrategy.OnPush,\n templateUrl: './avatar.component.html',\n styleUrl: './avatar.component.scss',\n})\nexport class AvatarComponent {\n // ===== IDENTITY =====\n /** Full display name used to derive initials when no explicit initials or image are provided. */\n readonly name = input.required<string>();\n /** URL of the avatar image; falls back to initials when the image fails to load. */\n readonly avatarUrl = input<string | null>(null);\n /** Explicit two-character initials that override the name-derived fallback. */\n readonly initials = input<string | null>(null);\n\n // ===== STATUS =====\n /** Online/offline/away status to reflect on the indicator dot. */\n readonly status = input<AvatarStatus | null>(null);\n /** When true, renders the status indicator dot over the avatar. */\n readonly showStatus = input<boolean>(false);\n\n // ===== DISPLAY =====\n /** Controls the avatar size. */\n readonly size = input<AvatarSize>('md');\n\n // ===== BASE PROPS =====\n readonly className = input<string>('');\n readonly id = input<string | null>(null);\n readonly ariaLabel = input<string | null>(null);\n\n // ===== COMPUTED =====\n\n readonly derivedInitials = computed(() => {\n const explicit = this.initials();\n\n if (explicit) {\n return explicit;\n }\n\n const name = this.name();\n\n if (!name) {\n return '?';\n }\n\n // Only words that start with a letter count, so a suffix such as\n // \"(she/her)\" or \"(dev)\" never lends a bracket to the initials.\n const parts = name\n .trim()\n .split(/\\s+/)\n .filter((part) => /^\\p{L}/u.test(part));\n\n if (parts.length === 0) {\n return '?';\n }\n\n if (parts.length >= 2) {\n return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();\n }\n\n return parts[0].substring(0, 2).toUpperCase();\n });\n\n readonly showIndicator = computed(\n () => this.showStatus() && this.status() !== null,\n );\n\n readonly classes = computed(() => {\n return [\n 'fk-avatar',\n `fk-avatar--${this.size()}`,\n this.showIndicator() ? `fk-avatar--status-${this.status()}` : '',\n this.className(),\n ]\n .filter(Boolean)\n .join(' ');\n });\n\n @HostBinding('class')\n get hostClass() {\n return this.classes();\n }\n\n @HostBinding('attr.id')\n get hostId(): string | null {\n return this.id();\n }\n\n // Tracks whether the image failed to load\n readonly imgError = { value: false };\n\n onImgError(): void {\n this.imgError.value = true;\n }\n}\n","<!-- Avatar circle -->\n<span\n class=\"fk-avatar__circle\"\n [attr.aria-label]=\"ariaLabel() ?? 'Avatar for ' + name()\"\n role=\"img\"\n>\n @if (avatarUrl() && !imgError.value) {\n <img\n class=\"fk-avatar__image\"\n [src]=\"avatarUrl()\"\n [alt]=\"name()\"\n (error)=\"onImgError()\"\n />\n } @else {\n <span class=\"fk-avatar__initials\" aria-hidden=\"true\">\n {{ derivedInitials() }}\n </span>\n }\n</span>\n\n<!-- Status indicator -->\n@if (showIndicator()) {\n <span\n class=\"fk-avatar__status\"\n [attr.aria-label]=\"status() + ' status'\"\n role=\"status\"\n ></span>\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frame-kit/ui-ng",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "description": "Style-agnostic, token-driven Angular UI component library for FrameKit.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -17,7 +17,7 @@
17
17
  "@angular/forms": "^21.2.0",
18
18
  "@angular/platform-browser": "^21.2.0",
19
19
  "@angular/router": "^21.2.0",
20
- "@frame-kit/utils": "0.2.23",
20
+ "@frame-kit/utils": "0.2.25",
21
21
  "rxjs": "^7.0.0"
22
22
  },
23
23
  "sideEffects": false,
@@ -25,7 +25,7 @@ A token-driven avatar atom that displays a user image with an initials fallback
25
25
  ## Features
26
26
 
27
27
  - Displays an image when `avatarUrl` is provided, gracefully falling back to initials on load error
28
- - Auto-derives initials from `name` (first + last initial for multi-word names, first two characters for single names)
28
+ - Auto-derives initials from `name` (first + last initial for multi-word names, first two characters for single names); words that do not start with a letter, such as a "(she/her)" suffix, are ignored
29
29
  - Status indicator dot positioned at the bottom-right corner, color-coded by status
30
30
  - Three size presets (`sm`, `md`, `lg`) controlled via tokens
31
31
  - Fully token-driven — all colors, sizes, typography, and radii are overridable