@flamingo-stack/openframe-frontend-core 0.0.347 → 0.0.349-snapshot.20260701004402

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 (127) hide show
  1. package/dist/{chunk-AQWXNUY5.js → chunk-22YDMHQ6.js} +4 -4
  2. package/dist/{chunk-SN55DABG.cjs → chunk-2UFPHLQW.cjs} +62 -62
  3. package/dist/{chunk-SN55DABG.cjs.map → chunk-2UFPHLQW.cjs.map} +1 -1
  4. package/dist/{chunk-RVQCUROB.cjs → chunk-2VJOIQWC.cjs} +15 -15
  5. package/dist/{chunk-RVQCUROB.cjs.map → chunk-2VJOIQWC.cjs.map} +1 -1
  6. package/dist/{chunk-TTVJV7AX.js → chunk-4LIII4VU.js} +4 -4
  7. package/dist/{chunk-BB75OCWM.cjs → chunk-4TWVKDZ3.cjs} +25 -25
  8. package/dist/{chunk-BB75OCWM.cjs.map → chunk-4TWVKDZ3.cjs.map} +1 -1
  9. package/dist/{chunk-SNGWHQOT.cjs → chunk-5DLWZXWX.cjs} +12 -12
  10. package/dist/{chunk-SNGWHQOT.cjs.map → chunk-5DLWZXWX.cjs.map} +1 -1
  11. package/dist/{chunk-DNIXKKAO.js → chunk-5EVW3XJK.js} +4 -4
  12. package/dist/{chunk-FCQTT3EN.js → chunk-5O4OTDH3.js} +3538 -3463
  13. package/dist/chunk-5O4OTDH3.js.map +1 -0
  14. package/dist/{chunk-2WTA2TGZ.cjs → chunk-6EBR75I2.cjs} +26 -26
  15. package/dist/{chunk-2WTA2TGZ.cjs.map → chunk-6EBR75I2.cjs.map} +1 -1
  16. package/dist/{chunk-T3NJFBR4.cjs → chunk-B6MWUSP2.cjs} +9 -9
  17. package/dist/{chunk-T3NJFBR4.cjs.map → chunk-B6MWUSP2.cjs.map} +1 -1
  18. package/dist/{chunk-JDWOPC5W.js → chunk-DGUTSVZ2.js} +2 -2
  19. package/dist/{chunk-WV2O2KJQ.js → chunk-DIWT3MLF.js} +2 -2
  20. package/dist/{chunk-DBP5XYHP.js → chunk-EET6FJB4.js} +2 -2
  21. package/dist/{chunk-BAQUBE3J.cjs → chunk-HEBUZLJ2.cjs} +5 -5
  22. package/dist/{chunk-BAQUBE3J.cjs.map → chunk-HEBUZLJ2.cjs.map} +1 -1
  23. package/dist/{chunk-LURIO2V6.cjs → chunk-ICKM2U5N.cjs} +7 -7
  24. package/dist/{chunk-LURIO2V6.cjs.map → chunk-ICKM2U5N.cjs.map} +1 -1
  25. package/dist/{chunk-EQQS4C66.js → chunk-K5R7QYZJ.js} +2 -2
  26. package/dist/{chunk-USF72I36.js → chunk-MIYA5JM5.js} +2 -2
  27. package/dist/{chunk-LLKZVG42.cjs → chunk-NLOK2CZ2.cjs} +31 -31
  28. package/dist/{chunk-LLKZVG42.cjs.map → chunk-NLOK2CZ2.cjs.map} +1 -1
  29. package/dist/{chunk-E5GGKGLG.js → chunk-OSILW6QN.js} +4 -4
  30. package/dist/{chunk-IK76VFPB.cjs → chunk-QM4W5LAZ.cjs} +56 -56
  31. package/dist/{chunk-IK76VFPB.cjs.map → chunk-QM4W5LAZ.cjs.map} +1 -1
  32. package/dist/{chunk-Q2MTKMST.js → chunk-SHCBCWUN.js} +2 -2
  33. package/dist/{chunk-LUZMRZIT.cjs → chunk-T265FMHX.cjs} +37 -37
  34. package/dist/{chunk-LUZMRZIT.cjs.map → chunk-T265FMHX.cjs.map} +1 -1
  35. package/dist/{chunk-Q2ACKKFN.cjs → chunk-VLBYQVU6.cjs} +938 -863
  36. package/dist/chunk-VLBYQVU6.cjs.map +1 -0
  37. package/dist/{chunk-LYKMMMJM.js → chunk-Y4JKCD7B.js} +2 -2
  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 +202 -242
  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/navigation/sliding-sidebar.d.ts.map +1 -1
  75. package/dist/components/onboarding-guides/index.cjs +5 -5
  76. package/dist/components/onboarding-guides/index.js +4 -4
  77. package/dist/components/related-content/index.cjs +3 -3
  78. package/dist/components/related-content/index.js +2 -2
  79. package/dist/components/tickets/index.cjs +5 -5
  80. package/dist/components/tickets/index.js +4 -4
  81. package/dist/components/ui/index.cjs +2 -2
  82. package/dist/components/ui/index.cjs.map +1 -1
  83. package/dist/components/ui/index.js +7 -7
  84. package/dist/index.cjs +2 -2
  85. package/dist/index.cjs.map +1 -1
  86. package/dist/index.js +7 -7
  87. package/dist/types/announcement.cjs.map +1 -1
  88. package/dist/types/announcement.d.ts +17 -22
  89. package/dist/types/announcement.d.ts.map +1 -1
  90. package/dist/types/announcement.js.map +1 -1
  91. package/dist/types/index.cjs.map +1 -1
  92. package/dist/types/index.js.map +1 -1
  93. package/dist/types/supabase.d.ts +6 -9
  94. package/dist/types/supabase.d.ts.map +1 -1
  95. package/package.json +1 -1
  96. package/src/ODS_TOKEN_RULES.md +8 -2
  97. package/src/components/announcement-bar.tsx +20 -38
  98. package/src/components/chat/embeddable-chat.tsx +4 -5
  99. package/src/components/chat/guide-welcome.tsx +15 -0
  100. package/src/components/chat/slash-command-suggestions.tsx +5 -5
  101. package/src/components/chat/utils/__tests__/icon-resolver.test.ts +51 -0
  102. package/src/components/chat/utils/{onboarding-icons.tsx → icon-library.tsx} +68 -22
  103. package/src/components/chat/utils/index.ts +10 -10
  104. package/src/components/footer.tsx +1 -1
  105. package/src/components/icon-display.tsx +67 -0
  106. package/src/components/index.ts +2 -0
  107. package/src/components/navigation/sliding-sidebar.tsx +7 -3
  108. package/src/stories/AnnouncementBar.stories.tsx +9 -11
  109. package/src/stories/MingoOnboardingCard.stories.tsx +3 -3
  110. package/src/stories/SlashCommandSuggestions.stories.tsx +1 -1
  111. package/src/types/announcement.ts +17 -23
  112. package/src/types/supabase.ts +6 -9
  113. package/dist/chunk-FCQTT3EN.js.map +0 -1
  114. package/dist/chunk-Q2ACKKFN.cjs.map +0 -1
  115. package/dist/components/chat/utils/onboarding-icons.d.ts.map +0 -1
  116. /package/dist/{chunk-AQWXNUY5.js.map → chunk-22YDMHQ6.js.map} +0 -0
  117. /package/dist/{chunk-TTVJV7AX.js.map → chunk-4LIII4VU.js.map} +0 -0
  118. /package/dist/{chunk-DNIXKKAO.js.map → chunk-5EVW3XJK.js.map} +0 -0
  119. /package/dist/{chunk-JDWOPC5W.js.map → chunk-DGUTSVZ2.js.map} +0 -0
  120. /package/dist/{chunk-WV2O2KJQ.js.map → chunk-DIWT3MLF.js.map} +0 -0
  121. /package/dist/{chunk-DBP5XYHP.js.map → chunk-EET6FJB4.js.map} +0 -0
  122. /package/dist/{chunk-EQQS4C66.js.map → chunk-K5R7QYZJ.js.map} +0 -0
  123. /package/dist/{chunk-USF72I36.js.map → chunk-MIYA5JM5.js.map} +0 -0
  124. /package/dist/{chunk-E5GGKGLG.js.map → chunk-OSILW6QN.js.map} +0 -0
  125. /package/dist/{chunk-Q2MTKMST.js.map → chunk-SHCBCWUN.js.map} +0 -0
  126. /package/dist/{chunk-LYKMMMJM.js.map → chunk-Y4JKCD7B.js.map} +0 -0
  127. /package/src/components/chat/utils/{.icon-registry.md → .icon-library.md} +0 -0
@@ -1,10 +1,9 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useEffect } from 'react';
4
- import Image from '../embed-shims/next-image';
5
4
  import { X } from 'lucide-react';
6
5
  import { Button } from './ui/button';
7
- import { renderSvgIcon } from './icon-utils';
6
+ import { EntityIcon } from './icon-display';
8
7
  import {
9
8
  setStoredAnnouncement,
10
9
  getStoredAnnouncement,
@@ -14,19 +13,6 @@ import { Announcement } from '../types/announcement';
14
13
  import { getAppType } from '../utils/app-config';
15
14
  import { useEndpointsRuntime } from '../contexts/endpoints-runtime-context';
16
15
 
17
- // Helper that defers to renderSvgIcon so we don't need local icon imports
18
- const getSvgIcon = (
19
- name: string,
20
- size: 'main' | 'cta' = 'main',
21
- extra: Record<string, any> = {}
22
- ) => {
23
- const cls =
24
- size === 'cta'
25
- ? 'relative shrink-0 w-3 h-3 md:w-4 md:h-4'
26
- : 'relative shrink-0 w-6 h-6 md:w-8 md:h-8';
27
- return renderSvgIcon(name, { className: cls, ...extra });
28
- };
29
-
30
16
  export function AnnouncementBar() {
31
17
  const [announcement, setAnnouncement] = useState<Announcement | null>(null);
32
18
  const [isVisible, setIsVisible] = useState<boolean>(false);
@@ -138,24 +124,18 @@ export function AnnouncementBar() {
138
124
 
139
125
  const renderIcon = () => {
140
126
  if (!announcement) return null;
141
-
142
- if (announcement.icon_type === 'png' && announcement.icon_png_url) {
143
- return (
144
- <Image
145
- src={announcement.icon_png_url}
146
- alt="Announcement icon"
147
- width={32}
148
- height={32}
149
- className="relative shrink-0"
150
- aria-hidden
151
- />
152
- );
153
- }
154
-
155
- return getSvgIcon(
156
- announcement.icon_svg_name || 'openframe-logo',
157
- 'main',
158
- announcement.icon_svg_props ?? {}
127
+ // ONE unified display path (shared with the chat): uploaded image URL wins,
128
+ // else a library glyph by name (+ props), via <EntityIcon>.
129
+ return (
130
+ <EntityIcon
131
+ icon={{
132
+ name: announcement.icon_name || 'openframe-logo',
133
+ url: announcement.icon_url,
134
+ props: announcement.icon_props,
135
+ }}
136
+ size={32}
137
+ className="relative shrink-0 w-6 h-6 md:w-8 md:h-8"
138
+ />
159
139
  );
160
140
  };
161
141
 
@@ -201,11 +181,13 @@ export function AnnouncementBar() {
201
181
  variant="outline"
202
182
  size="small-legacy"
203
183
  leftIcon={
204
- announcement.cta_show_icon && announcement.cta_icon
205
- ? getSvgIcon(
206
- announcement.cta_icon,
207
- 'cta',
208
- announcement.cta_icon_props ?? {}
184
+ announcement.cta_show_icon && announcement.cta_icon_name
185
+ ? (
186
+ <EntityIcon
187
+ icon={{ name: announcement.cta_icon_name, props: announcement.cta_icon_props }}
188
+ size={16}
189
+ className="w-3 h-3 md:w-4 md:h-4"
190
+ />
209
191
  )
210
192
  : undefined
211
193
  }
@@ -99,8 +99,7 @@ import { resolveHrefForRuntime } from './utils/chat-nav-resolution'
99
99
  import { ChatPanelContext, type ChatPanelHandle } from './chat-panel-context'
100
100
  import { resolveSourceRowCTA, sourceRowCtxFromRuntime } from './utils/source-row-cta'
101
101
  import { chatChipClass } from './utils/chip-styles'
102
- import { getIconComponent } from './utils/icon-registry'
103
- import { resolveOnboardingIcon } from './utils/onboarding-icons'
102
+ import { resolveIcon } from './utils/icon-library'
104
103
  import { getSourceIconName } from './utils/source-icons'
105
104
  import { formatSingularLookupInvocation } from './utils/slash-dispatch-utils'
106
105
 
@@ -442,7 +441,7 @@ function dispatchSlashCommandAction(
442
441
  */
443
442
  function resolveChipIcon(src: ChatSource): React.ReactNode {
444
443
  const iconName = getSourceIconName(src.sourceRepo)
445
- const Icon = iconName ? getIconComponent(iconName) : FileText
444
+ const Icon = iconName ? resolveIcon(iconName, { variant: 'brand' }) : FileText
446
445
  return <Icon className="h-3.5 w-3.5" />
447
446
  }
448
447
 
@@ -874,7 +873,7 @@ function EmbeddableChatInner({
874
873
  resolveSourceIcon: (sourceId: string) => {
875
874
  const iconName = getSourceIconName(sourceId)
876
875
  if (!iconName) return undefined
877
- return { Icon: getIconComponent(iconName), label: sourceId }
876
+ return { Icon: resolveIcon(iconName, { variant: 'brand' }), label: sourceId }
878
877
  },
879
878
  onAction: (cmd: SlashCommandSummary, actionId: SlashCommandActionId) => {
880
879
  dispatchSlashCommandAction(actionId, cmd.id, chatInputRef)
@@ -1837,7 +1836,7 @@ function EmbeddableChatInner({
1837
1836
  />
1838
1837
  ))}
1839
1838
  {chipCommands.map((cmd) => {
1840
- const Icon = resolveOnboardingIcon(cmd.iconName)
1839
+ const Icon = resolveIcon(cmd.iconName)
1841
1840
  const cmdId = cmd.id
1842
1841
  const label = cmd.label ?? `/${cmdId}`
1843
1842
  const cardActions = cmd.actions.map((action) => ({
@@ -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,
@@ -79,7 +79,7 @@ function UniversalFooter({ config, renderLink }: { config: FooterConfig; renderL
79
79
  const linkRenderer = renderLink || defaultRenderLink
80
80
 
81
81
  return (
82
- <footer className={`w-full flex flex-col justify-center items-center ${config.backgroundColor || 'bg-ods-bg-card'} px-6 py-10 relative gap-6 md:gap-6 min-h-[auto] md:min-h-[248px] z-50 border-t border-ods-border`}>
82
+ <footer className={`w-full flex flex-col justify-center items-center ${config.backgroundColor || 'bg-ods-bg-card'} px-6 py-10 relative gap-6 md:gap-6 min-h-[auto] md:min-h-[248px] z-[44] border-t border-ods-border`}>
83
83
  <div className="w-full grid grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8 items-start">
84
84
 
85
85
  {/* Column 1: Logo and optionally description */}
@@ -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'
@@ -155,7 +155,11 @@ export function SlidingSidebar({ config }: SlidingSidebarProps) {
155
155
 
156
156
  return (
157
157
  <>
158
- {/* Overlay */}
158
+ {/* Overlay — admin-drawer tier (overlay z-[45] / panel z-[46]): ABOVE the
159
+ footer (lowered to z-[44]) so the open drawer covers it, but BEHIND the
160
+ header (z-[50]) so the header stays visible on top (the panel has a
161
+ header spacer below). Kept under z-50 so it also stays beneath modals/
162
+ dialogs (z-50+). See the z-index hierarchy in ODS_TOKEN_RULES.md. */}
159
163
  <AnimatePresence>
160
164
  {config.isOpen && (
161
165
  <motion.div
@@ -166,7 +170,7 @@ export function SlidingSidebar({ config }: SlidingSidebarProps) {
166
170
  duration: 0.3,
167
171
  ease: [0.4, 0, 0.2, 1]
168
172
  }}
169
- className="fixed inset-0 bg-black/50 z-[40]"
173
+ className="fixed inset-0 bg-black/50 z-[45]"
170
174
  onClick={() => config.onClose()}
171
175
  />
172
176
  )}
@@ -187,7 +191,7 @@ export function SlidingSidebar({ config }: SlidingSidebarProps) {
187
191
  velocity: config.isOpen ? 5 : -5
188
192
  }}
189
193
  className={cn(
190
- "fixed top-0 bottom-0 z-[45] w-72 bg-ods-card border-ods-border flex flex-col shadow-xl",
194
+ "fixed top-0 bottom-0 z-[46] w-72 bg-ods-card border-ods-border flex flex-col shadow-xl",
191
195
  config.position === 'right' ? "right-0 border-l" : "left-0 border-r",
192
196
  config.className
193
197
  )}
@@ -40,8 +40,7 @@ const baseAnnouncement: Announcement = {
40
40
  title: 'New Feature Released',
41
41
  description: 'Check out our latest updates and improvements to the platform.',
42
42
  background_color: '#FFD951',
43
- icon_type: 'svg',
44
- icon_svg_name: 'rocket',
43
+ icon_name: 'rocket',
45
44
  platform_id: 'openframe',
46
45
  is_active: true,
47
46
  created_at: now,
@@ -95,12 +94,12 @@ export const WithCTA: Story = {
95
94
  title: 'OpenFrame v2.0 is here!',
96
95
  description: 'Explore the new dashboard, improved device management, and more.',
97
96
  background_color: '#FFD951',
98
- icon_svg_name: 'star',
97
+ icon_name: 'star',
99
98
  cta_enabled: true,
100
99
  cta_text: 'Learn More',
101
100
  cta_url: 'https://example.com',
102
101
  cta_target: '_blank',
103
- cta_icon: 'rocket',
102
+ cta_icon_name: 'rocket',
104
103
  cta_show_icon: true,
105
104
  cta_button_background_color: '#1A1A1A',
106
105
  cta_button_text_color: '#FFFFFF',
@@ -122,7 +121,7 @@ export const WithCTASameTab: Story = {
122
121
  title: 'Scheduled Maintenance',
123
122
  description: 'We will perform maintenance on Saturday from 2:00 AM to 4:00 AM UTC.',
124
123
  background_color: '#FFE082',
125
- icon_svg_name: 'info',
124
+ icon_name: 'info',
126
125
  cta_enabled: true,
127
126
  cta_text: 'View Status',
128
127
  cta_url: '/status',
@@ -145,7 +144,7 @@ export const BlueBackground: Story = {
145
144
  title: 'Join our Community',
146
145
  description: 'Connect with other MSPs and share best practices.',
147
146
  background_color: '#60A5FA',
148
- icon_svg_name: 'megaphone',
147
+ icon_name: 'megaphone',
149
148
  })
150
149
  return <Story />
151
150
  },
@@ -164,7 +163,7 @@ export const GreenBackground: Story = {
164
163
  title: 'All Systems Operational',
165
164
  description: 'Everything is running smoothly. No issues detected.',
166
165
  background_color: '#86EFAC',
167
- icon_svg_name: 'bell',
166
+ icon_name: 'bell',
168
167
  })
169
168
  return <Story />
170
169
  },
@@ -183,7 +182,7 @@ export const WithOpenFrameLogo: Story = {
183
182
  title: 'Welcome to OpenFrame',
184
183
  description: 'Your open-source IT infrastructure management platform.',
185
184
  background_color: '#FFD951',
186
- icon_svg_name: 'openframe-logo',
185
+ icon_name: 'openframe-logo',
187
186
  cta_enabled: true,
188
187
  cta_text: 'Get Started',
189
188
  cta_url: '/getting-started',
@@ -208,8 +207,7 @@ export const WithPNGIcon: Story = {
208
207
  title: 'Partner Program Launch',
209
208
  description: 'Become an OpenFrame certified partner and grow your business.',
210
209
  background_color: '#C4B5FD',
211
- icon_type: 'png',
212
- icon_png_url: 'https://placehold.co/32x32/1a1a1a/white?text=OF',
210
+ icon_url: 'https://placehold.co/32x32/1a1a1a/white?text=OF',
213
211
  })
214
212
  return <Story />
215
213
  },
@@ -229,7 +227,7 @@ export const LongContent: Story = {
229
227
  description:
230
228
  'This is an extremely long description that goes into great detail about the announcement. It should be truncated on the desktop view and hidden on mobile view to keep the bar compact and readable.',
231
229
  background_color: '#FCA5A5',
232
- icon_svg_name: 'info',
230
+ icon_name: 'info',
233
231
  cta_enabled: true,
234
232
  cta_text: 'Read Full Announcement',
235
233
  cta_url: '/announcements/long',