@company-semantics/contracts 0.40.0 → 0.41.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Avatar Resolution Functions
3
+ *
4
+ * Canonical functions for resolving user avatars.
5
+ * Determines whether to use Slack profile photo or initials fallback.
6
+ *
7
+ * @see ADR-BE-063 for design rationale
8
+ */
9
+
10
+ import type { UserIdentity } from './types';
11
+ import { extractFirstWord } from './display-name';
12
+
13
+ // =============================================================================
14
+ // Types
15
+ // =============================================================================
16
+
17
+ /**
18
+ * Source of the resolved avatar.
19
+ * - 'slack': Using Slack profile photo
20
+ * - 'initials': Using generated initials (fallback)
21
+ */
22
+ export type AvatarSource = 'slack' | 'initials';
23
+
24
+ /**
25
+ * Resolved avatar for UI rendering.
26
+ *
27
+ * INVARIANT: initials is ALWAYS populated, regardless of source.
28
+ * This prevents UI regressions if Slack avatar fails to load.
29
+ */
30
+ export interface ResolvedAvatar {
31
+ /** Source of the avatar (slack or initials) */
32
+ source: AvatarSource;
33
+
34
+ /** Slack profile image URL (present only if source === 'slack') */
35
+ url?: string;
36
+
37
+ /**
38
+ * User initials for fallback display.
39
+ * REQUIRED - always populated to ensure graceful degradation.
40
+ */
41
+ initials: string;
42
+ }
43
+
44
+ // =============================================================================
45
+ // Helper Functions
46
+ // =============================================================================
47
+
48
+ /**
49
+ * Generate initials from a full name.
50
+ *
51
+ * Takes the first letter of the first word and optionally the last word.
52
+ * Always returns uppercase initials (1-2 characters).
53
+ *
54
+ * @param fullName - The user's full name
55
+ * @returns Uppercase initials (e.g., "IH" for "Ian Heidt", "M" for "Madonna")
56
+ *
57
+ * @example
58
+ * generateInitials("Ian Heidt") // "IH"
59
+ * generateInitials("Madonna") // "M"
60
+ * generateInitials("Mary Jane Doe") // "MD"
61
+ * generateInitials("") // ""
62
+ */
63
+ export function generateInitials(fullName: string): string {
64
+ const trimmed = fullName.trim();
65
+ if (!trimmed) return '';
66
+
67
+ const words = trimmed.split(/\s+/);
68
+ const firstInitial = words[0]?.[0]?.toUpperCase() ?? '';
69
+
70
+ if (words.length === 1) {
71
+ return firstInitial;
72
+ }
73
+
74
+ const lastInitial = words[words.length - 1]?.[0]?.toUpperCase() ?? '';
75
+ return `${firstInitial}${lastInitial}`;
76
+ }
77
+
78
+ // =============================================================================
79
+ // Avatar Resolution
80
+ // =============================================================================
81
+
82
+ /**
83
+ * Resolve avatar from user identity.
84
+ *
85
+ * This is the ONLY approved way to determine which avatar to display.
86
+ * Returns a ResolvedAvatar with source, optional URL, and required initials.
87
+ *
88
+ * INVARIANT: initials is ALWAYS populated, regardless of source.
89
+ * This ensures the UI always has a fallback if the Slack image fails to load.
90
+ *
91
+ * @param identity - Object with slackAvatarUrl and fullName fields
92
+ * @returns ResolvedAvatar with source, url (if slack), and initials
93
+ *
94
+ * @example
95
+ * // Slack avatar available
96
+ * resolveAvatar({ slackAvatarUrl: "https://...", fullName: "Ian Heidt" })
97
+ * // { source: 'slack', url: 'https://...', initials: 'IH' }
98
+ *
99
+ * // No Slack avatar (initials fallback)
100
+ * resolveAvatar({ fullName: "Ian Heidt" })
101
+ * // { source: 'initials', initials: 'IH' }
102
+ */
103
+ export function resolveAvatar(
104
+ identity: Pick<UserIdentity, 'slackAvatarUrl' | 'fullName'>
105
+ ): ResolvedAvatar {
106
+ const initials = generateInitials(identity.fullName);
107
+
108
+ if (identity.slackAvatarUrl) {
109
+ return {
110
+ source: 'slack',
111
+ url: identity.slackAvatarUrl,
112
+ initials,
113
+ };
114
+ }
115
+
116
+ return {
117
+ source: 'initials',
118
+ initials,
119
+ };
120
+ }
@@ -8,10 +8,16 @@
8
8
  // Types
9
9
  export type { ISODateString, NameSource, UserIdentity } from './types';
10
10
 
11
- // Functions
11
+ // Functions - Display Name
12
12
  export {
13
13
  extractFirstWord,
14
14
  resolveDisplayName,
15
15
  /** @deprecated Use fullName directly */
16
16
  deriveFullName,
17
17
  } from './display-name';
18
+
19
+ // Types - Avatar
20
+ export type { AvatarSource, ResolvedAvatar } from './avatar';
21
+
22
+ // Functions - Avatar
23
+ export { generateInitials, resolveAvatar } from './avatar';
@@ -71,4 +71,26 @@ export type UserIdentity = {
71
71
 
72
72
  /** When the identity was last updated */
73
73
  updatedAt: ISODateString;
74
+
75
+ // ==========================================================================
76
+ // Slack Avatar Fields (ADR-BE-063)
77
+ // ==========================================================================
78
+
79
+ /**
80
+ * Slack user ID (set when Slack is connected and email matches).
81
+ * Used for efficient lookups in user_change event handling.
82
+ */
83
+ slackUserId?: string;
84
+
85
+ /**
86
+ * Slack profile image URL (synced from Slack events).
87
+ * May be undefined if Slack not connected or email mismatch.
88
+ */
89
+ slackAvatarUrl?: string;
90
+
91
+ /**
92
+ * When the Slack avatar was last synced.
93
+ * Used for debugging and audit purposes.
94
+ */
95
+ slackAvatarLastSyncedAt?: ISODateString;
74
96
  };