@flamingo-stack/openframe-frontend-core 0.0.348 → 0.0.349

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 (112) hide show
  1. package/dist/{chunk-3JIUN64F.cjs → chunk-26DV5GSP.cjs} +25 -25
  2. package/dist/{chunk-3JIUN64F.cjs.map → chunk-26DV5GSP.cjs.map} +1 -1
  3. package/dist/{chunk-YWEWRAMA.js → chunk-2CKA3CCD.js} +4 -4
  4. package/dist/{chunk-4UUFNLXQ.cjs → chunk-3DMOLXLC.cjs} +7 -7
  5. package/dist/{chunk-4UUFNLXQ.cjs.map → chunk-3DMOLXLC.cjs.map} +1 -1
  6. package/dist/{chunk-RDJNK5AV.js → chunk-63EJ7OVZ.js} +2 -2
  7. package/dist/{chunk-O5D3YWMW.cjs → chunk-6GQWIOOD.cjs} +936 -861
  8. package/dist/chunk-6GQWIOOD.cjs.map +1 -0
  9. package/dist/{chunk-ZS7YHQXO.js → chunk-CVFEPMED.js} +2 -2
  10. package/dist/{chunk-7CBXPBSB.js → chunk-CWNVR32U.js} +3536 -3461
  11. package/dist/chunk-CWNVR32U.js.map +1 -0
  12. package/dist/{chunk-RT6GEODM.js → chunk-ETCT2IBA.js} +2 -2
  13. package/dist/{chunk-6BW37KWJ.js → chunk-G2PUXREK.js} +4 -4
  14. package/dist/{chunk-4RMUTJTZ.js → chunk-GPLQR3AR.js} +2 -2
  15. package/dist/{chunk-WADLOXCY.js → chunk-H4AQ3DSB.js} +4 -4
  16. package/dist/{chunk-CH2F32CW.cjs → chunk-JENZN2RS.cjs} +56 -56
  17. package/dist/{chunk-CH2F32CW.cjs.map → chunk-JENZN2RS.cjs.map} +1 -1
  18. package/dist/{chunk-YFOLFU7F.cjs → chunk-NBZSABAO.cjs} +9 -9
  19. package/dist/{chunk-YFOLFU7F.cjs.map → chunk-NBZSABAO.cjs.map} +1 -1
  20. package/dist/{chunk-K5G3JKIU.cjs → chunk-NFUMSUOV.cjs} +5 -5
  21. package/dist/{chunk-K5G3JKIU.cjs.map → chunk-NFUMSUOV.cjs.map} +1 -1
  22. package/dist/{chunk-DU5LLHRI.js → chunk-O36624LY.js} +4 -4
  23. package/dist/{chunk-PQG3L4MK.cjs → chunk-O36BKXM2.cjs} +37 -37
  24. package/dist/{chunk-PQG3L4MK.cjs.map → chunk-O36BKXM2.cjs.map} +1 -1
  25. package/dist/{chunk-YTSX3CIK.cjs → chunk-PGBKYPM7.cjs} +12 -12
  26. package/dist/{chunk-YTSX3CIK.cjs.map → chunk-PGBKYPM7.cjs.map} +1 -1
  27. package/dist/{chunk-IROBEIGB.cjs → chunk-T4GH47TR.cjs} +31 -31
  28. package/dist/{chunk-IROBEIGB.cjs.map → chunk-T4GH47TR.cjs.map} +1 -1
  29. package/dist/{chunk-LBBRR56M.cjs → chunk-TAMJE3SX.cjs} +26 -26
  30. package/dist/{chunk-LBBRR56M.cjs.map → chunk-TAMJE3SX.cjs.map} +1 -1
  31. package/dist/{chunk-QZVKSUPR.cjs → chunk-UE4B7KNL.cjs} +62 -62
  32. package/dist/{chunk-QZVKSUPR.cjs.map → chunk-UE4B7KNL.cjs.map} +1 -1
  33. package/dist/{chunk-NUO4JXTS.js → chunk-UKEJUXQY.js} +2 -2
  34. package/dist/{chunk-XBJJIMIG.js → chunk-WY35YHCG.js} +2 -2
  35. package/dist/{chunk-YOVM7KGM.js → chunk-XKXJB4D4.js} +2 -2
  36. package/dist/{chunk-MZGJBQ72.cjs → chunk-Y2VXGKNP.cjs} +15 -15
  37. package/dist/{chunk-MZGJBQ72.cjs.map → chunk-Y2VXGKNP.cjs.map} +1 -1
  38. package/dist/components/announcement-bar.d.ts.map +1 -1
  39. package/dist/components/case-studies/index.cjs +8 -8
  40. package/dist/components/case-studies/index.js +2 -2
  41. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  42. package/dist/components/chat/guide-welcome.d.ts +6 -0
  43. package/dist/components/chat/guide-welcome.d.ts.map +1 -1
  44. package/dist/components/chat/index.cjs +2 -2
  45. package/dist/components/chat/index.cjs.map +1 -1
  46. package/dist/components/chat/index.js +7 -7
  47. package/dist/components/chat/slash-command-suggestions.d.ts +3 -3
  48. package/dist/components/chat/utils/{onboarding-icons.d.ts → icon-library.d.ts} +25 -15
  49. package/dist/components/chat/utils/icon-library.d.ts.map +1 -0
  50. package/dist/components/chat/utils/index.d.ts +1 -1
  51. package/dist/components/chat/utils/index.d.ts.map +1 -1
  52. package/dist/components/contact/index.cjs +3 -3
  53. package/dist/components/contact/index.js +2 -2
  54. package/dist/components/docs/index.cjs +5 -5
  55. package/dist/components/docs/index.js +4 -4
  56. package/dist/components/embeds/index.cjs +3 -3
  57. package/dist/components/embeds/index.js +2 -2
  58. package/dist/components/faq/index.cjs +3 -3
  59. package/dist/components/faq/index.js +2 -2
  60. package/dist/components/features/index.cjs +2 -2
  61. package/dist/components/features/index.js +1 -1
  62. package/dist/components/help-center-pages/index.cjs +20 -20
  63. package/dist/components/help-center-pages/index.js +11 -11
  64. package/dist/components/icon-display.d.ts +27 -0
  65. package/dist/components/icon-display.d.ts.map +1 -0
  66. package/dist/components/index.cjs +201 -241
  67. package/dist/components/index.cjs.map +1 -1
  68. package/dist/components/index.d.ts +1 -0
  69. package/dist/components/index.d.ts.map +1 -1
  70. package/dist/components/index.js +588 -628
  71. package/dist/components/index.js.map +1 -1
  72. package/dist/components/navigation/index.cjs +2 -2
  73. package/dist/components/navigation/index.js +1 -1
  74. package/dist/components/onboarding-guides/index.cjs +5 -5
  75. package/dist/components/onboarding-guides/index.js +4 -4
  76. package/dist/components/related-content/index.cjs +3 -3
  77. package/dist/components/related-content/index.js +2 -2
  78. package/dist/components/tickets/index.cjs +5 -5
  79. package/dist/components/tickets/index.js +4 -4
  80. package/dist/components/ui/index.cjs +2 -2
  81. package/dist/components/ui/index.cjs.map +1 -1
  82. package/dist/components/ui/index.js +7 -7
  83. package/dist/index.cjs +2 -2
  84. package/dist/index.cjs.map +1 -1
  85. package/dist/index.js +7 -7
  86. package/package.json +1 -1
  87. package/src/components/announcement-bar.tsx +19 -40
  88. package/src/components/chat/embeddable-chat.tsx +4 -5
  89. package/src/components/chat/guide-welcome.tsx +15 -0
  90. package/src/components/chat/slash-command-suggestions.tsx +5 -5
  91. package/src/components/chat/utils/__tests__/icon-resolver.test.ts +51 -0
  92. package/src/components/chat/utils/{onboarding-icons.tsx → icon-library.tsx} +68 -22
  93. package/src/components/chat/utils/index.ts +10 -10
  94. package/src/components/icon-display.tsx +67 -0
  95. package/src/components/index.ts +2 -0
  96. package/src/stories/MingoOnboardingCard.stories.tsx +3 -3
  97. package/src/stories/SlashCommandSuggestions.stories.tsx +1 -1
  98. package/dist/chunk-7CBXPBSB.js.map +0 -1
  99. package/dist/chunk-O5D3YWMW.cjs.map +0 -1
  100. package/dist/components/chat/utils/onboarding-icons.d.ts.map +0 -1
  101. /package/dist/{chunk-YWEWRAMA.js.map → chunk-2CKA3CCD.js.map} +0 -0
  102. /package/dist/{chunk-RDJNK5AV.js.map → chunk-63EJ7OVZ.js.map} +0 -0
  103. /package/dist/{chunk-ZS7YHQXO.js.map → chunk-CVFEPMED.js.map} +0 -0
  104. /package/dist/{chunk-RT6GEODM.js.map → chunk-ETCT2IBA.js.map} +0 -0
  105. /package/dist/{chunk-6BW37KWJ.js.map → chunk-G2PUXREK.js.map} +0 -0
  106. /package/dist/{chunk-4RMUTJTZ.js.map → chunk-GPLQR3AR.js.map} +0 -0
  107. /package/dist/{chunk-WADLOXCY.js.map → chunk-H4AQ3DSB.js.map} +0 -0
  108. /package/dist/{chunk-DU5LLHRI.js.map → chunk-O36624LY.js.map} +0 -0
  109. /package/dist/{chunk-NUO4JXTS.js.map → chunk-UKEJUXQY.js.map} +0 -0
  110. /package/dist/{chunk-XBJJIMIG.js.map → chunk-WY35YHCG.js.map} +0 -0
  111. /package/dist/{chunk-YOVM7KGM.js.map → chunk-XKXJB4D4.js.map} +0 -0
  112. /package/src/components/chat/utils/{.icon-registry.md → .icon-library.md} +0 -0
@@ -5,6 +5,7 @@ import { cn } from '../../utils/cn'
5
5
  import { MingoIcon } from '../icons'
6
6
  import { Skeleton } from '../ui/skeleton'
7
7
  import { ChatQuickActionRow } from './chat-quick-action-row'
8
+ import { EntityIcon } from '../icon-display'
8
9
 
9
10
  // =============================================================================
10
11
  // Types
@@ -18,6 +19,19 @@ export interface GuideQuickAction {
18
19
  label: string
19
20
  /** Prompt text seeded into the composer on click. Defaults to `label`. */
20
21
  prompt?: string
22
+ /** Optional library-glyph name (resolved via the onboarding-icon registry). */
23
+ iconName?: string | null
24
+ /** Optional uploaded image URL (wins over `iconName`). */
25
+ iconUrl?: string | null
26
+ /** Optional props spread onto the resolved glyph. */
27
+ iconProps?: Record<string, unknown> | null
28
+ }
29
+
30
+ /** A chip's leading icon via the unified <EntityIcon> (shared with the
31
+ * announcement bar). Undefined when the chip has no icon. */
32
+ function renderQuickActionIcon(a: GuideQuickAction): React.ReactNode {
33
+ if (!a.iconName && !a.iconUrl) return undefined
34
+ return <EntityIcon icon={{ name: a.iconName, url: a.iconUrl, props: a.iconProps }} size={16} />
21
35
  }
22
36
 
23
37
  export interface GuideWelcomeProps {
@@ -113,6 +127,7 @@ export function GuideWelcome({
113
127
  quickActions.map((action) => ({
114
128
  id: action.id,
115
129
  label: action.label,
130
+ icon: renderQuickActionIcon(action),
116
131
  onSelect: () => onQuickAction?.(action),
117
132
  onHoverStart: () => onQuickActionHover?.(action),
118
133
  onHoverEnd: () => onQuickActionHoverEnd?.(),
@@ -3,7 +3,7 @@
3
3
  import type { KeyboardEvent } from 'react'
4
4
  import { cn } from '../../utils/cn'
5
5
  import { MingoOnboardingCard } from './mingo-onboarding-card'
6
- import { resolveOnboardingIcon } from './utils/onboarding-icons'
6
+ import { resolveIcon } from './utils/icon-library'
7
7
  import type {
8
8
  SlashCommandActionId,
9
9
  SlashCommandSourceMeta,
@@ -24,7 +24,7 @@ interface SlashCommandSuggestionsProps {
24
24
  onSelect: (cmd: SlashCommandSummary) => void
25
25
  /** Legacy `primarySourceId` → icon resolver. Used as a FALLBACK only
26
26
  * when `cmd.iconName` is missing. Newer command summaries carry
27
- * `iconName` directly and route through `resolveOnboardingIcon` so
27
+ * `iconName` directly and route through `resolveIcon` so
28
28
  * the dropdown row uses the same icon registry as the empty-state
29
29
  * onboarding cards. */
30
30
  resolveSourceIcon?: (sourceId: string) => SlashCommandSourceMeta | undefined
@@ -47,11 +47,11 @@ interface SlashCommandSuggestionsProps {
47
47
  * `border-b` divider into a clean 1-px frame.
48
48
  *
49
49
  * Icon resolution priority:
50
- * 1. `cmd.iconName` → `resolveOnboardingIcon` (production path —
50
+ * 1. `cmd.iconName` → `resolveIcon` (production path —
51
51
  * matches the keys returned by `/api/docs/commands`).
52
52
  * 2. `cmd.primarySourceId` → consumer-provided `resolveSourceIcon`
53
53
  * (legacy fallback for command feeds without `iconName`).
54
- * 3. `FileIcon` (default inside `resolveOnboardingIcon`).
54
+ * 3. `FileIcon` (default inside `resolveIcon`).
55
55
  *
56
56
  * Clicking the row body fires `onSelect` (the host typically maps this
57
57
  * to Search-mode pre-fill so the user can refine). Clicking an action
@@ -85,7 +85,7 @@ export function SlashCommandSuggestions({
85
85
  )}
86
86
  >
87
87
  {commands.map((cmd, idx) => {
88
- const Icon = resolveOnboardingIcon(cmd.iconName)
88
+ const Icon = resolveIcon(cmd.iconName)
89
89
  // Legacy resolver runs ONLY when `iconName` is missing — otherwise
90
90
  // the v2 registry above is authoritative.
91
91
  const legacyMeta =
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { resolveIcon, ICON_OPTIONS } from '../icon-library'
3
+ import { getIconComponent } from '../icon-registry'
4
+
5
+ /**
6
+ * Proof that unifying the two resolvers into one variant-aware `resolveIcon`
7
+ * does NOT change any glyph:
8
+ * - brand variant must be REFERENCE-IDENTICAL to the legacy `getIconComponent`
9
+ * (so every social / source-row consumer renders exactly the same icon).
10
+ * - design variant must resolve every curated picker name to a real component
11
+ * (no new FileIcon regressions), and never throw on null/unknown.
12
+ */
13
+
14
+ // Names spanning BOTH consumer families, incl. the overlap set flagged in review
15
+ // (github/slack/clickup/hubspot) and the brand-color social names, plus PascalCase
16
+ // DB values that `getIconComponent` normalises.
17
+ const BRAND_NAMES = [
18
+ 'github', 'slack', 'clickup', 'hubspot', 'openframe',
19
+ 'linkedin', 'facebook', 'youtube', 'twitter', 'instagram', 'discord', 'rocket',
20
+ // PascalCase social_platforms.icon_name values:
21
+ 'LinkedInIcon', 'XLogo', 'YouTubeIcon', 'Github',
22
+ // unknown + nullish:
23
+ 'definitely-not-an-icon',
24
+ ]
25
+
26
+ describe('unified resolveIcon — brand variant preserves getIconComponent exactly', () => {
27
+ for (const name of BRAND_NAMES) {
28
+ it(`brand('${name}') === getIconComponent('${name}')`, () => {
29
+ expect(resolveIcon(name, { variant: 'brand' })).toBe(getIconComponent(name))
30
+ })
31
+ }
32
+
33
+ it('null/undefined brand → same fallback as getIconComponent, no throw', () => {
34
+ expect(resolveIcon(null, { variant: 'brand' })).toBe(getIconComponent(null))
35
+ expect(resolveIcon(undefined, { variant: 'brand' })).toBe(getIconComponent(undefined))
36
+ })
37
+ })
38
+
39
+ describe('unified resolveIcon — design variant (default) resolves every picker name', () => {
40
+ it('every ICON_OPTIONS key resolves to a real component', () => {
41
+ for (const opt of ICON_OPTIONS) {
42
+ const Comp = resolveIcon(opt.key)
43
+ expect(Comp, `design('${opt.key}') should resolve`).toBeDefined()
44
+ }
45
+ })
46
+
47
+ it('null/unknown design → defined fallback, never throws', () => {
48
+ expect(resolveIcon(null)).toBeDefined()
49
+ expect(resolveIcon('definitely-not-an-icon')).toBeDefined()
50
+ })
51
+ })
@@ -28,8 +28,9 @@
28
28
  * misconfigured row still renders.
29
29
  */
30
30
 
31
- import type { ComponentType } from 'react'
31
+ import type { ComponentType, CSSProperties } from 'react'
32
32
 
33
+ import { getIconComponent } from './icon-registry'
33
34
  import * as IconsV2 from '../../icons-v2-generated'
34
35
  import { ClickupLogoGreyIcon } from '../../icons-v2-generated/brand-logos/clickup-logo-grey-icon'
35
36
  import { GithubIcon } from '../../icons-v2-generated/brand-logos/github-icon'
@@ -52,14 +53,30 @@ import { VideoRecorderIcon } from '../../icons-v2-generated/audio-and-visual/vid
52
53
  import { SearchIcon } from '../../icons-v2-generated/interface/search-icon'
53
54
  import { TicketIcon } from '../../icons-v2-generated/shopping/ticket-icon'
54
55
  import { OpenFrameLogo } from '../../icons/openframe-logo'
56
+ import { OpenmspLogo } from '../../icons/openmsp-logo'
57
+ import { FlamingoLogo } from '../../icons/flamingo-logo'
58
+ import { Megaphone, Bell, Info, Star, Package as PackageGlyph } from 'lucide-react'
55
59
 
56
60
  /** Shape used by every icon registered here (matches `icons-v2-generated`). */
57
- export type OnboardingIconComponent = ComponentType<{
61
+ export type IconComponent = ComponentType<{
58
62
  size?: number
59
63
  className?: string
60
64
  color?: string
61
65
  }>
62
66
 
67
+ /** Wrap a brand logo (className/SVG-sized) so it satisfies the `size`-prop
68
+ * `IconComponent` contract — drives size via `style` (CSS wins over
69
+ * the logo's hardcoded width/height), keeping the logo's own brand fill. */
70
+ function sizedLogo(
71
+ Logo: ComponentType<{ className?: string; style?: CSSProperties }>,
72
+ ): IconComponent {
73
+ return function SizedLogo({ size = 16, className }) {
74
+ return <Logo className={className} style={{ width: size, height: size }} />
75
+ }
76
+ }
77
+ const OpenmspLogoIcon = sizedLogo(OpenmspLogo)
78
+ const FlamingoLogoIcon = sizedLogo(FlamingoLogo)
79
+
63
80
  /**
64
81
  * `OpenFrameLogo` from `components/icons/` predates the v2 set and:
65
82
  * 1. hardcodes `width="32" height="32"` AFTER spreading `{...props}`,
@@ -74,16 +91,20 @@ export type OnboardingIconComponent = ComponentType<{
74
91
  function LogoOpenframeIcon({
75
92
  size = 16,
76
93
  className,
94
+ color,
77
95
  }: {
78
96
  size?: number
79
97
  className?: string
80
98
  color?: string
81
99
  }) {
100
+ // Forward `color` (from `icon_props.color`) like every other resolver-backed
101
+ // glyph; default to `currentColor` so the logo tints to its slot.
102
+ const tint = color ?? 'currentColor'
82
103
  return (
83
104
  <OpenFrameLogo
84
105
  className={className}
85
- lowerPathColor="currentColor"
86
- upperPathColor="currentColor"
106
+ lowerPathColor={tint}
107
+ upperPathColor={tint}
87
108
  style={{ width: size, height: size }}
88
109
  />
89
110
  )
@@ -95,7 +116,7 @@ function LogoOpenframeIcon({
95
116
  * monochrome v2 glyphs that inherit `currentColor` from the card slot
96
117
  * (`text-ods-text-secondary`).
97
118
  */
98
- export const ONBOARDING_ICONS: Record<string, OnboardingIconComponent> = {
119
+ export const ICON_ALIASES: Record<string, IconComponent> = {
99
120
  // Production `/api/docs/commands` keys (multi-platform-hub contract)
100
121
  clickup: ClickupLogoGreyIcon,
101
122
  slack: SlackLogoGreyIcon,
@@ -124,6 +145,18 @@ export const ONBOARDING_ICONS: Record<string, OnboardingIconComponent> = {
124
145
  'rocket-01': Rocket02Icon,
125
146
  'rocket-02': Rocket02Icon,
126
147
 
148
+ // Announcement-bar legacy names — folded in so resolveIcon covers them too
149
+ // (was the separate `renderSvgIcon` map; now ONE resolver everywhere).
150
+ 'openframe-logo': LogoOpenframeIcon,
151
+ 'openmsp-logo': OpenmspLogoIcon,
152
+ flamingo: FlamingoLogoIcon,
153
+ 'flamingo-logo': FlamingoLogoIcon,
154
+ megaphone: Megaphone,
155
+ bell: Bell,
156
+ info: Info,
157
+ star: Star,
158
+ package: PackageGlyph,
159
+
127
160
  // Generic v2-native keys — useful fall-throughs for slash commands that
128
161
  // don't have a brand-specific glyph in the design.
129
162
  search: SearchIcon,
@@ -145,7 +178,7 @@ export const ONBOARDING_ICONS: Record<string, OnboardingIconComponent> = {
145
178
  *
146
179
  * Every generated icon follows one naming convention: a file named
147
180
  * `<kebab>-icon.tsx` exports a component `<PascalCase>Icon` with the
148
- * `{ size, className, color }` signature `OnboardingIconComponent`
181
+ * `{ size, className, color }` signature `IconComponent`
149
182
  * expects. So any backend `iconName` that names a real icon (e.g.
150
183
  * `chart-pie`, `bank`, `money-bill-dollar`, `piggy-bank`) resolves
151
184
  * here without a hand-maintained entry — the whole library is
@@ -161,7 +194,7 @@ export const ONBOARDING_ICONS: Record<string, OnboardingIconComponent> = {
161
194
  */
162
195
  function resolveFromLibrary(
163
196
  iconName: string,
164
- ): OnboardingIconComponent | undefined {
197
+ ): IconComponent | undefined {
165
198
  const tokens = iconName.trim().toLowerCase().replace(/_/g, '-').split('-')
166
199
  if (tokens.some((t) => t.length === 0)) return undefined
167
200
  const cap = (t: string) => t.charAt(0).toUpperCase() + t.slice(1)
@@ -169,7 +202,7 @@ function resolveFromLibrary(
169
202
  `${tokens.map(cap).join('')}Icon`,
170
203
  `${tokens.map((t) => (t === 'ai' ? 'AI' : cap(t))).join('')}Icon`,
171
204
  ]
172
- const registry = IconsV2 as unknown as Record<string, OnboardingIconComponent | undefined>
205
+ const registry = IconsV2 as unknown as Record<string, IconComponent | undefined>
173
206
  for (const name of candidates) {
174
207
  const Icon = registry[name]
175
208
  if (Icon) return Icon
@@ -178,25 +211,38 @@ function resolveFromLibrary(
178
211
  }
179
212
 
180
213
  /**
181
- * Look up the icon component for a backend `iconName`. Resolution order:
182
- * 1. Curated `ONBOARDING_ICONS` alias wins so brand-grey logos, the
183
- * tinted OpenFrame logo, and plural/semantic aliases keep their
184
- * intended glyph instead of the literal-name match.
185
- * 2. Generic `icons-v2-generated` lookup by naming convention — covers
186
- * every other icon in the library.
187
- * 3. `FileIcon` for unknown / missing keys so the card always renders.
214
+ * THE single icon-name component resolver for the whole app. Two variants —
215
+ * one entry point, no duplicated resolver machinery:
216
+ *
217
+ * `'design'` (DEFAULT): the icons-v2 design set — chat chips, slash
218
+ * autocomplete, announcement bar, AI-agent icons, the admin picker.
219
+ * Order: curated `ICON_ALIASES` (brand-grey logos, tinted OpenFrame,
220
+ * semantic aliases) generic `icons-v2-generated` by naming convention
221
+ * `FileIcon`.
222
+ * • `'brand'`: the brand/social set (lucide + brand-colored logos) via the
223
+ * registry resolver `getIconComponent` (PascalCase-normalised) — social
224
+ * links, source-row CTAs.
225
+ *
226
+ * The two glyph SETS differ on purpose (a grey design 'slack' vs a brand-color
227
+ * social 'slack'); this resolver is the ONE place that owns both. `getIconComponent`
228
+ * / `getDynamicIcon` remain as the brand-variant + sized-brand helpers.
188
229
  */
189
- export function resolveOnboardingIcon(
230
+ export function resolveIcon(
190
231
  iconName: string | null | undefined,
191
- ): OnboardingIconComponent {
232
+ opts?: { variant?: 'design' | 'brand' },
233
+ ): IconComponent {
234
+ if (opts?.variant === 'brand') {
235
+ // Brand variant — delegate to the registry resolver (same glyph as before).
236
+ return getIconComponent(iconName) as IconComponent
237
+ }
192
238
  if (!iconName) return FileIcon
193
- return ONBOARDING_ICONS[iconName] ?? resolveFromLibrary(iconName) ?? FileIcon
239
+ return ICON_ALIASES[iconName] ?? resolveFromLibrary(iconName) ?? FileIcon
194
240
  }
195
241
 
196
242
  /** One selectable icon in the admin slash-command picker. */
197
- export interface OnboardingIconOption {
243
+ export interface IconOption {
198
244
  /** Stored `icon_name` value — a kebab-case `icons-v2-generated` key (or a
199
- * curated alias) that `resolveOnboardingIcon` displays in the chat. */
245
+ * curated alias) that `resolveIcon` displays in the chat. */
200
246
  key: string
201
247
  /** Human-friendly label shown in the picker dropdown. */
202
248
  label: string
@@ -207,14 +253,14 @@ export interface OnboardingIconOption {
207
253
  * (`/admin/chat-config` → Edit slash command).
208
254
  *
209
255
  * THE PICKER AND THE CHAT SHARE ONE ICON SOURCE: every `key` here is an
210
- * `icons-v2-generated` name (or curated alias) that `resolveOnboardingIcon`
256
+ * `icons-v2-generated` name (or curated alias) that `resolveIcon`
211
257
  * renders — so whatever the admin selects always displays in the chip /
212
258
  * autocomplete, with no fallback-glyph drift. (The full library is far larger
213
259
  * and resolvable generically; this is the curated, slash-command-relevant
214
260
  * subset for a usable dropdown.) Keep keys kebab-case to match the DB
215
261
  * `chat_admin_slash_commands.icon_name` convention.
216
262
  */
217
- export const ONBOARDING_ICON_OPTIONS: ReadonlyArray<OnboardingIconOption> = [
263
+ export const ICON_OPTIONS: ReadonlyArray<IconOption> = [
218
264
  // Brands
219
265
  { key: 'openframe', label: 'OpenFrame' },
220
266
  { key: 'slack', label: 'Slack' },
@@ -173,16 +173,16 @@ export {
173
173
  getDynamicIcon,
174
174
  type DynamicIconSize,
175
175
  } from './icon-registry'
176
- // Onboarding/slash-command icons — `icons-v2-generated`-backed resolver shared
177
- // by the chat empty-state chips, the slash-command autocomplete, AND the admin
178
- // icon picker (`ONBOARDING_ICON_OPTIONS`), so picked icons always display.
179
- export {
180
- resolveOnboardingIcon,
181
- ONBOARDING_ICONS,
182
- ONBOARDING_ICON_OPTIONS,
183
- type OnboardingIconComponent,
184
- type OnboardingIconOption,
185
- } from './onboarding-icons'
176
+ // Library icon resolver — `icons-v2-generated`-backed, shared by the chat
177
+ // empty-state chips, the slash-command autocomplete, AND the admin icon picker
178
+ // (`ICON_OPTIONS`), so picked icons always display.
179
+ export {
180
+ resolveIcon,
181
+ ICON_ALIASES,
182
+ ICON_OPTIONS,
183
+ type IconComponent,
184
+ type IconOption,
185
+ } from './icon-library'
186
186
  export {
187
187
  SOURCE_ICON_NAMES,
188
188
  getSourceIconName,
@@ -0,0 +1,67 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { AgentMark, type AgentName } from './agent-mark'
5
+ import { resolveIcon } from './chat/utils/icon-library'
6
+
7
+ /** Unified icon value used across the app (announcement bar, chat quick actions,
8
+ * AI-agent identity). Exactly one of `name`/`url` is typically set. */
9
+ export interface EntityIconValue {
10
+ /** Library glyph name resolved via {@link resolveIcon} (icons-v2 + curated aliases). */
11
+ name?: string | null
12
+ /** Uploaded image URL — wins over `name`. */
13
+ url?: string | null
14
+ /** Props spread onto the resolved glyph (e.g. `{ color }`). */
15
+ props?: Record<string, unknown> | null
16
+ }
17
+
18
+ const BRAND_MARK_NAMES = new Set<string>(['fae', 'mingo'])
19
+
20
+ /**
21
+ * THE single icon-display path for the whole app. Resolution order:
22
+ * 1. `url` → uploaded image
23
+ * 2. `name` ∈ {fae,mingo} → packaged `AgentMark`
24
+ * 3. `name` → library glyph via `resolveIcon` (+ `props`)
25
+ *
26
+ * Replaces the old per-surface logic (the announcement bar's `renderSvgIcon`
27
+ * map and the chat's ad-hoc `resolveIcon` calls) so every surface renders an
28
+ * icon identically.
29
+ */
30
+ export function EntityIcon({
31
+ icon,
32
+ size = 20,
33
+ className,
34
+ }: {
35
+ icon?: EntityIconValue | null
36
+ size?: number
37
+ className?: string
38
+ }): React.ReactElement {
39
+ if (icon?.url) {
40
+ // eslint-disable-next-line @next/next/no-img-element
41
+ return (
42
+ <img
43
+ src={icon.url}
44
+ alt=""
45
+ width={size}
46
+ height={size}
47
+ className={className}
48
+ style={{ objectFit: 'contain' }}
49
+ />
50
+ )
51
+ }
52
+ if (icon?.name && BRAND_MARK_NAMES.has(icon.name)) {
53
+ // Size via `className` when provided (so responsive Tailwind sizing like
54
+ // `w-6 md:w-8` works); fall back to a fixed `size` px box ONLY when no
55
+ // className is given — an inline `style` would otherwise override the class.
56
+ return (
57
+ <span
58
+ className={`inline-flex${className ? ` ${className}` : ''}`}
59
+ style={className ? undefined : { width: size, height: size }}
60
+ >
61
+ <AgentMark agent={icon.name as AgentName} className="w-full h-full" />
62
+ </span>
63
+ )
64
+ }
65
+ const Glyph = resolveIcon(icon?.name ?? undefined)
66
+ return <Glyph size={size} className={className} {...(icon?.props ?? {})} />
67
+ }
@@ -110,3 +110,5 @@ export { AuthorDetailView, type AuthorDetailViewProps } from './authors/author-d
110
110
 
111
111
  // Priority UI components that exist in main components directory
112
112
  // Note: These are re-exported from ./ui already, no need to duplicate
113
+
114
+ export { EntityIcon, type EntityIconValue } from './icon-display'
@@ -1,6 +1,6 @@
1
1
  import type { Meta, StoryObj } from '@storybook/nextjs-vite'
2
2
  import { MingoOnboardingCard } from '../components/chat/mingo-onboarding-card'
3
- import { resolveOnboardingIcon } from '../components/chat/utils/onboarding-icons'
3
+ import { resolveIcon } from '../components/chat/utils/icon-library'
4
4
  import { ClickupLogoGreyIcon } from '../components/icons-v2-generated/brand-logos/clickup-logo-grey-icon'
5
5
  import { CompassIcon } from '../components/icons-v2-generated/map-and-travel/compass-icon'
6
6
  import { Rocket02Icon } from '../components/icons-v2-generated/vehicles-and-delivery/rocket-02-icon'
@@ -126,7 +126,7 @@ export const WithActions: Story = {
126
126
 
127
127
  // =============================================================================
128
128
  // Stack of `chipCommands` — mirrors Figma node 7363:205938 (full empty-state).
129
- // Icons are resolved via the shared `resolveOnboardingIcon` registry so the
129
+ // Icons are resolved via the shared `resolveIcon` registry so the
130
130
  // story stays in sync with the chat's runtime mapping.
131
131
  // =============================================================================
132
132
 
@@ -247,7 +247,7 @@ export const ChipCommands: Story = {
247
247
  <>
248
248
  {SAMPLE_CHIP_COMMANDS.map(
249
249
  ({ iconName, label, slashCommand, description, actions }) => {
250
- const Icon = resolveOnboardingIcon(iconName)
250
+ const Icon = resolveIcon(iconName)
251
251
  return (
252
252
  <MingoOnboardingCard
253
253
  key={slashCommand + label}
@@ -14,7 +14,7 @@ const meta: Meta<typeof SlashCommandSuggestions> = {
14
14
  docs: {
15
15
  description: {
16
16
  component:
17
- 'Slash-command autocomplete dropdown — opens above the chat input when the user types `/`. Each row is a `MingoOnboardingCard` so the dropdown and the empty-state list share one card design. Icons resolve through the shared `resolveOnboardingIcon` registry; the production `/api/docs/commands` short keys (`clickup`, `github`, `slack`, `openframe`, `rocket`, `briefcase`, `graduation-cap`, `video`, `headphones`, `calendar`, …) are wired up alongside the legacy Figma-style aliases.',
17
+ 'Slash-command autocomplete dropdown — opens above the chat input when the user types `/`. Each row is a `MingoOnboardingCard` so the dropdown and the empty-state list share one card design. Icons resolve through the shared `resolveIcon` registry; the production `/api/docs/commands` short keys (`clickup`, `github`, `slack`, `openframe`, `rocket`, `briefcase`, `graduation-cap`, `video`, `headphones`, `calendar`, …) are wired up alongside the legacy Figma-style aliases.',
18
18
  },
19
19
  },
20
20
  },