@company-semantics/contracts 0.40.0 → 0.41.1
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 +1 -1
- package/src/identity/avatar.ts +119 -0
- package/src/identity/index.ts +7 -1
- package/src/identity/types.ts +22 -0
package/package.json
CHANGED
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Source of the resolved avatar.
|
|
18
|
+
* - 'slack': Using Slack profile photo
|
|
19
|
+
* - 'initials': Using generated initials (fallback)
|
|
20
|
+
*/
|
|
21
|
+
export type AvatarSource = 'slack' | 'initials';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolved avatar for UI rendering.
|
|
25
|
+
*
|
|
26
|
+
* INVARIANT: initials is ALWAYS populated, regardless of source.
|
|
27
|
+
* This prevents UI regressions if Slack avatar fails to load.
|
|
28
|
+
*/
|
|
29
|
+
export interface ResolvedAvatar {
|
|
30
|
+
/** Source of the avatar (slack or initials) */
|
|
31
|
+
source: AvatarSource;
|
|
32
|
+
|
|
33
|
+
/** Slack profile image URL (present only if source === 'slack') */
|
|
34
|
+
url?: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* User initials for fallback display.
|
|
38
|
+
* REQUIRED - always populated to ensure graceful degradation.
|
|
39
|
+
*/
|
|
40
|
+
initials: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Helper Functions
|
|
45
|
+
// =============================================================================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generate initials from a full name.
|
|
49
|
+
*
|
|
50
|
+
* Takes the first letter of the first word and optionally the last word.
|
|
51
|
+
* Always returns uppercase initials (1-2 characters).
|
|
52
|
+
*
|
|
53
|
+
* @param fullName - The user's full name
|
|
54
|
+
* @returns Uppercase initials (e.g., "IH" for "Ian Heidt", "M" for "Madonna")
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* generateInitials("Ian Heidt") // "IH"
|
|
58
|
+
* generateInitials("Madonna") // "M"
|
|
59
|
+
* generateInitials("Mary Jane Doe") // "MD"
|
|
60
|
+
* generateInitials("") // ""
|
|
61
|
+
*/
|
|
62
|
+
export function generateInitials(fullName: string): string {
|
|
63
|
+
const trimmed = fullName.trim();
|
|
64
|
+
if (!trimmed) return '';
|
|
65
|
+
|
|
66
|
+
const words = trimmed.split(/\s+/);
|
|
67
|
+
const firstInitial = words[0]?.[0]?.toUpperCase() ?? '';
|
|
68
|
+
|
|
69
|
+
if (words.length === 1) {
|
|
70
|
+
return firstInitial;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const lastInitial = words[words.length - 1]?.[0]?.toUpperCase() ?? '';
|
|
74
|
+
return `${firstInitial}${lastInitial}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// =============================================================================
|
|
78
|
+
// Avatar Resolution
|
|
79
|
+
// =============================================================================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve avatar from user identity.
|
|
83
|
+
*
|
|
84
|
+
* This is the ONLY approved way to determine which avatar to display.
|
|
85
|
+
* Returns a ResolvedAvatar with source, optional URL, and required initials.
|
|
86
|
+
*
|
|
87
|
+
* INVARIANT: initials is ALWAYS populated, regardless of source.
|
|
88
|
+
* This ensures the UI always has a fallback if the Slack image fails to load.
|
|
89
|
+
*
|
|
90
|
+
* @param identity - Object with slackAvatarUrl and fullName fields
|
|
91
|
+
* @returns ResolvedAvatar with source, url (if slack), and initials
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // Slack avatar available
|
|
95
|
+
* resolveAvatar({ slackAvatarUrl: "https://...", fullName: "Ian Heidt" })
|
|
96
|
+
* // { source: 'slack', url: 'https://...', initials: 'IH' }
|
|
97
|
+
*
|
|
98
|
+
* // No Slack avatar (initials fallback)
|
|
99
|
+
* resolveAvatar({ fullName: "Ian Heidt" })
|
|
100
|
+
* // { source: 'initials', initials: 'IH' }
|
|
101
|
+
*/
|
|
102
|
+
export function resolveAvatar(
|
|
103
|
+
identity: Pick<UserIdentity, 'slackAvatarUrl' | 'fullName'>
|
|
104
|
+
): ResolvedAvatar {
|
|
105
|
+
const initials = generateInitials(identity.fullName);
|
|
106
|
+
|
|
107
|
+
if (identity.slackAvatarUrl) {
|
|
108
|
+
return {
|
|
109
|
+
source: 'slack',
|
|
110
|
+
url: identity.slackAvatarUrl,
|
|
111
|
+
initials,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
source: 'initials',
|
|
117
|
+
initials,
|
|
118
|
+
};
|
|
119
|
+
}
|
package/src/identity/index.ts
CHANGED
|
@@ -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';
|
package/src/identity/types.ts
CHANGED
|
@@ -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
|
};
|