@flamingo-stack/openframe-frontend-core 0.0.422 → 0.0.423-snapshot.20260714123224

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 (124) hide show
  1. package/dist/{chunk-WAQH35VG.cjs → chunk-445ZGRPE.cjs} +30 -30
  2. package/dist/{chunk-WAQH35VG.cjs.map → chunk-445ZGRPE.cjs.map} +1 -1
  3. package/dist/{chunk-HBTKCED7.js → chunk-4ACSUJ7Q.js} +13 -6
  4. package/dist/chunk-4ACSUJ7Q.js.map +1 -0
  5. package/dist/{chunk-DUHM44CQ.js → chunk-56NEIHTC.js} +3 -3
  6. package/dist/{chunk-XFYQV2K2.js → chunk-5TCOKIAS.js} +2 -2
  7. package/dist/{chunk-4TMMYZTY.js → chunk-6PB5HGFM.js} +2 -2
  8. package/dist/{chunk-WQEEGFDM.js → chunk-AD4A44US.js} +3 -3
  9. package/dist/{chunk-BF5BXKM7.cjs → chunk-AEUFBPB3.cjs} +82 -82
  10. package/dist/{chunk-BF5BXKM7.cjs.map → chunk-AEUFBPB3.cjs.map} +1 -1
  11. package/dist/{chunk-LHM2VUTO.js → chunk-APWAFTFL.js} +3 -3
  12. package/dist/{chunk-RGKGRS2T.cjs → chunk-BM5RER5N.cjs} +59 -59
  13. package/dist/{chunk-RGKGRS2T.cjs.map → chunk-BM5RER5N.cjs.map} +1 -1
  14. package/dist/{chunk-A2IRUIUJ.cjs → chunk-C7AAFHP5.cjs} +37 -37
  15. package/dist/{chunk-A2IRUIUJ.cjs.map → chunk-C7AAFHP5.cjs.map} +1 -1
  16. package/dist/{chunk-LWC2U5OC.cjs → chunk-FVL25VKT.cjs} +9 -9
  17. package/dist/{chunk-LWC2U5OC.cjs.map → chunk-FVL25VKT.cjs.map} +1 -1
  18. package/dist/{chunk-YJDVAT7J.js → chunk-H2ECFX4G.js} +43 -22
  19. package/dist/chunk-H2ECFX4G.js.map +1 -0
  20. package/dist/{chunk-E6WANQ6K.cjs → chunk-IMK6RUEC.cjs} +13 -6
  21. package/dist/chunk-IMK6RUEC.cjs.map +1 -0
  22. package/dist/{chunk-PO4PXWZN.cjs → chunk-J2WSWXJN.cjs} +5 -5
  23. package/dist/{chunk-PO4PXWZN.cjs.map → chunk-J2WSWXJN.cjs.map} +1 -1
  24. package/dist/{chunk-2JNFQEVX.cjs → chunk-JSO2EOOV.cjs} +26 -26
  25. package/dist/{chunk-2JNFQEVX.cjs.map → chunk-JSO2EOOV.cjs.map} +1 -1
  26. package/dist/{chunk-ULUH2X53.cjs → chunk-LYO2FUKU.cjs} +11 -11
  27. package/dist/{chunk-ULUH2X53.cjs.map → chunk-LYO2FUKU.cjs.map} +1 -1
  28. package/dist/{chunk-B62DI2SC.cjs → chunk-M3ROXRUZ.cjs} +19 -19
  29. package/dist/{chunk-B62DI2SC.cjs.map → chunk-M3ROXRUZ.cjs.map} +1 -1
  30. package/dist/{chunk-YZQNKMFQ.cjs → chunk-NMKIS3JX.cjs} +31 -31
  31. package/dist/{chunk-YZQNKMFQ.cjs.map → chunk-NMKIS3JX.cjs.map} +1 -1
  32. package/dist/{chunk-JSL3VXGJ.js → chunk-ORKNKKOH.js} +2 -2
  33. package/dist/{chunk-3BGWK5QI.js → chunk-OXE2PNKV.js} +4 -4
  34. package/dist/{chunk-HFK33T6A.js → chunk-RUQ4MVBN.js} +5 -5
  35. package/dist/{chunk-O3FSSWV4.cjs → chunk-TMITO43G.cjs} +12 -12
  36. package/dist/{chunk-O3FSSWV4.cjs.map → chunk-TMITO43G.cjs.map} +1 -1
  37. package/dist/{chunk-KUDLV25M.cjs → chunk-X3IHSMPD.cjs} +388 -367
  38. package/dist/chunk-X3IHSMPD.cjs.map +1 -0
  39. package/dist/{chunk-YUMFRC3D.js → chunk-X7UBB6F6.js} +4 -4
  40. package/dist/{chunk-6F5OKJTW.js → chunk-XMCURSQ7.js} +4 -4
  41. package/dist/{chunk-4BWQYGIY.js → chunk-Y4WY2AF6.js} +2 -2
  42. package/dist/components/case-studies/index.cjs +9 -9
  43. package/dist/components/case-studies/index.js +3 -3
  44. package/dist/components/chat/index.cjs +3 -3
  45. package/dist/components/chat/index.js +2 -2
  46. package/dist/components/contact/index.cjs +4 -4
  47. package/dist/components/contact/index.js +3 -3
  48. package/dist/components/docs/index.cjs +6 -6
  49. package/dist/components/docs/index.js +5 -5
  50. package/dist/components/embeds/index.cjs +4 -4
  51. package/dist/components/embeds/index.js +3 -3
  52. package/dist/components/faq/index.cjs +4 -4
  53. package/dist/components/faq/index.js +3 -3
  54. package/dist/components/features/index.cjs +3 -3
  55. package/dist/components/features/index.js +2 -2
  56. package/dist/components/features/seo-editor-preview.d.ts.map +1 -1
  57. package/dist/components/features/time-tracker/time-tracker-panel.d.ts.map +1 -1
  58. package/dist/components/help-center-pages/index.cjs +21 -21
  59. package/dist/components/help-center-pages/index.js +12 -12
  60. package/dist/components/index.cjs +153 -153
  61. package/dist/components/index.js +10 -10
  62. package/dist/components/navigation/index.cjs +3 -3
  63. package/dist/components/navigation/index.js +2 -2
  64. package/dist/components/onboarding-guides/index.cjs +6 -6
  65. package/dist/components/onboarding-guides/index.js +5 -5
  66. package/dist/components/related-content/index.cjs +4 -4
  67. package/dist/components/related-content/index.js +3 -3
  68. package/dist/components/tickets/index.cjs +6 -6
  69. package/dist/components/tickets/index.js +5 -5
  70. package/dist/components/ui/actions-menu.d.ts.map +1 -1
  71. package/dist/components/ui/button/split-button.d.ts +6 -0
  72. package/dist/components/ui/button/split-button.d.ts.map +1 -1
  73. package/dist/components/ui/file-manager/index.cjs +50 -50
  74. package/dist/components/ui/file-manager/index.js +1 -1
  75. package/dist/components/ui/index.cjs +3 -3
  76. package/dist/components/ui/index.js +2 -2
  77. package/dist/components/ui/page-actions.d.ts.map +1 -1
  78. package/dist/index.cjs +5 -3
  79. package/dist/index.cjs.map +1 -1
  80. package/dist/index.js +4 -2
  81. package/dist/utils/index.cjs +4 -0
  82. package/dist/utils/index.cjs.map +1 -1
  83. package/dist/utils/index.d.ts +1 -0
  84. package/dist/utils/index.d.ts.map +1 -1
  85. package/dist/utils/index.js +4 -1
  86. package/dist/utils/index.js.map +1 -1
  87. package/dist/utils/seo-description.d.ts +14 -0
  88. package/dist/utils/seo-description.d.ts.map +1 -0
  89. package/package.json +1 -1
  90. package/src/components/.tool-icon.md +3 -3
  91. package/src/components/features/seo-editor-preview.tsx +24 -1
  92. package/src/components/features/time-tracker/time-tracker-panel.tsx +1 -0
  93. package/src/components/icons-v2-generated/brand-logos/.index.md +1 -2
  94. package/src/components/platform/.ShellTypeBadge.md +1 -1
  95. package/src/components/platform/.ToolBadge.md +2 -2
  96. package/src/components/ui/actions-menu.tsx +1 -0
  97. package/src/components/ui/button/split-button.tsx +18 -3
  98. package/src/components/ui/page-actions.tsx +16 -14
  99. package/src/stories/.OSTypeBadge.stories.md +1 -1
  100. package/src/stories/.ShellTypeBadge.stories.md +1 -1
  101. package/src/stories/.Toaster.stories.md +1 -1
  102. package/src/stories/.ToolBadge.stories.md +2 -2
  103. package/src/stories/.ToolExecutionDisplay.stories.md +4 -4
  104. package/src/types/.os.types.md +1 -1
  105. package/src/types/.shell.types.md +1 -1
  106. package/src/types/.tool.types.md +3 -3
  107. package/src/utils/.tool-utils.md +1 -3
  108. package/src/utils/index.ts +1 -0
  109. package/src/utils/seo-description.ts +13 -0
  110. package/dist/chunk-E6WANQ6K.cjs.map +0 -1
  111. package/dist/chunk-HBTKCED7.js.map +0 -1
  112. package/dist/chunk-KUDLV25M.cjs.map +0 -1
  113. package/dist/chunk-YJDVAT7J.js.map +0 -1
  114. /package/dist/{chunk-DUHM44CQ.js.map → chunk-56NEIHTC.js.map} +0 -0
  115. /package/dist/{chunk-XFYQV2K2.js.map → chunk-5TCOKIAS.js.map} +0 -0
  116. /package/dist/{chunk-4TMMYZTY.js.map → chunk-6PB5HGFM.js.map} +0 -0
  117. /package/dist/{chunk-WQEEGFDM.js.map → chunk-AD4A44US.js.map} +0 -0
  118. /package/dist/{chunk-LHM2VUTO.js.map → chunk-APWAFTFL.js.map} +0 -0
  119. /package/dist/{chunk-JSL3VXGJ.js.map → chunk-ORKNKKOH.js.map} +0 -0
  120. /package/dist/{chunk-3BGWK5QI.js.map → chunk-OXE2PNKV.js.map} +0 -0
  121. /package/dist/{chunk-HFK33T6A.js.map → chunk-RUQ4MVBN.js.map} +0 -0
  122. /package/dist/{chunk-YUMFRC3D.js.map → chunk-X7UBB6F6.js.map} +0 -0
  123. /package/dist/{chunk-6F5OKJTW.js.map → chunk-XMCURSQ7.js.map} +0 -0
  124. /package/dist/{chunk-4BWQYGIY.js.map → chunk-Y4WY2AF6.js.map} +0 -0
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Single source of truth for the `seo_description` length budget.
3
+ *
4
+ * Unlike `seo_title` (soft search-engine guidance), this is a HARD cap: the
5
+ * `seo_description` DB columns are `varchar(160)` and Postgres rejects longer
6
+ * values outright ("value too long for type character varying(160)").
7
+ *
8
+ * This number lives in exactly ONE place. Consumers:
9
+ * - `SEOEditorPreview` — input `maxLength` + the too-long alert.
10
+ * - the hub — imports it (via `@flamingo-stack/openframe-frontend-core/utils`)
11
+ * for the AI generation prompt guidance and the rewrite-to-fit helper.
12
+ */
13
+ export declare const SEO_DESCRIPTION_MAX_LENGTH = 160;
14
+ //# sourceMappingURL=seo-description.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo-description.d.ts","sourceRoot":"","sources":["../../src/utils/seo-description.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,MAAM,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.422",
3
+ "version": "0.0.423-snapshot.20260714123224",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,5 +1,5 @@
1
1
  <!-- source-hash: 2db0b6673e5d8f02f2a3713fb6528d5d -->
2
- Renders the appropriate logo icon for a given MSP tool type (Fleet MDM, MeshCentral, TacticalRMM, Authentik, Osquery, or OpenFrame variants).
2
+ Renders the appropriate logo icon for a given MSP tool type (Fleet MDM, MeshCentral, Authentik, Osquery, or OpenFrame variants).
3
3
 
4
4
  ## Key Components
5
5
 
@@ -25,9 +25,9 @@ import { ToolTypeValues } from "../types/tool.types";
25
25
  // Render a Fleet MDM icon at default size (16px)
26
26
  <ToolIcon toolType={ToolTypeValues.FLEET_MDM} />
27
27
 
28
- // Render a TacticalRMM icon at 24px with a custom class
28
+ // Render a MeshCentral icon at 24px with a custom class
29
29
  <ToolIcon
30
- toolType={ToolTypeValues.TACTICAL_RMM}
30
+ toolType={ToolTypeValues.MESHCENTRAL}
31
31
  size={24}
32
32
  className="h-6 w-6 text-accent"
33
33
  />
@@ -10,6 +10,7 @@ import Image from '../../embed-shims/next-image';
10
10
  // SSOT for the field cap (server-safe constant). The seo_title renders as the
11
11
  // page <title> verbatim (no brand suffix), so this is the full ~60-char budget.
12
12
  import { SEO_TITLE_MAX_LENGTH } from '../../utils/seo-title';
13
+ import { SEO_DESCRIPTION_MAX_LENGTH } from '../../utils/seo-description';
13
14
 
14
15
  export interface SEOEditorPreviewProps {
15
16
  // SEO fields - must be strings (not undefined)
@@ -76,6 +77,11 @@ export function SEOEditorPreview({
76
77
  const seoTitleLength = (seoTitle || '').length;
77
78
  const seoTitleTooLong = seoTitleLength > SEO_TITLE_MAX_LENGTH;
78
79
 
80
+ // SEO description length state — the column is varchar(160); flag over-length so
81
+ // the editor (or a programmatically-set value) can be brought within budget.
82
+ const seoDescriptionLength = (seoDescription || '').length;
83
+ const seoDescriptionTooLong = seoDescriptionLength > SEO_DESCRIPTION_MAX_LENGTH;
84
+
79
85
  // Use fallback values if OG fields are empty
80
86
  const displayTitle = seoTitle.trim() || title || 'Untitled';
81
87
  const displayDescription = seoDescription.trim() || summary || 'No description';
@@ -137,7 +143,7 @@ export function SEOEditorPreview({
137
143
  <div className="flex items-center justify-between gap-2">
138
144
  <span className="text-[11px] text-ods-error font-['DM_Sans']">
139
145
  {seoTitleTooLong
140
- ? `Too long search engines may truncate this title (keep it ${SEO_TITLE_MAX_LENGTH})`
146
+ ? `Too long: search engines may truncate this title (keep it under ${SEO_TITLE_MAX_LENGTH})`
141
147
  : ''}
142
148
  </span>
143
149
  <span
@@ -206,10 +212,27 @@ export function SEOEditorPreview({
206
212
  value={seoDescription || ''}
207
213
  onChange={(e) => onSeoDescriptionChange(e.target.value)}
208
214
  disabled={disabled}
215
+ maxLength={SEO_DESCRIPTION_MAX_LENGTH}
216
+ invalid={seoDescriptionTooLong}
209
217
  placeholder="Enter SEO meta description..."
210
218
  className="bg-ods-bg border-ods-border text-ods-text-primary flex-1 resize-none"
211
219
  rows={6}
212
220
  />
221
+ <div className="flex items-center justify-between gap-2">
222
+ <span className="text-[11px] text-ods-error font-['DM_Sans']">
223
+ {seoDescriptionTooLong
224
+ ? `Too long: search engines may truncate this description (keep it under ${SEO_DESCRIPTION_MAX_LENGTH})`
225
+ : ''}
226
+ </span>
227
+ <span
228
+ className={cn(
229
+ "text-[11px] font-['DM_Sans'] tabular-nums shrink-0",
230
+ seoDescriptionTooLong ? 'text-ods-error font-semibold' : 'text-ods-text-secondary'
231
+ )}
232
+ >
233
+ {seoDescriptionLength}/{SEO_DESCRIPTION_MAX_LENGTH}
234
+ </span>
235
+ </div>
213
236
  {!seoDescription && summary && (
214
237
  <p className="text-[11px] text-ods-accent font-['DM_Sans']">
215
238
  Auto-populated from summary
@@ -303,6 +303,7 @@ export function TimeTrackerPanel({
303
303
  'aria-label': 'Open my time in a new view',
304
304
  onClick: onOpenMyTimeMenu,
305
305
  disabled: !onOpenMyTimeMenu,
306
+ openInNewTab: true,
306
307
  }}
307
308
  >
308
309
  Open My Time
@@ -13,7 +13,7 @@ A flat collection of named icon exports organized by brand/platform category:
13
13
  | **Microsoft** | `MicrosoftLogoIcon`, `MicrosoftLogoGreyIcon`, `Office365LogoIcon`, `WindowsIcon`, `WindowsLogoIcon` |
14
14
  | **AI Platforms** | `AnthropicLogoIcon`, `GeminiLogoIcon`, `OpenaiLogoIcon` (+ grey variants) |
15
15
  | **Social** | `FacebookIcon`, `InstagramIcon`, `LinkedinIcon`, `TwitterIcon`, `TwitterXIcon`, `TiktokIcon`, `YoutubeIcon` |
16
- | **MSP/IT Tools** | `TacticalRmmLogoIcon`, `MeshcentralLogoIcon`, `FleetMdmLogoIcon`, `OsqueryLogoIcon`, `PowershellLogoIcon` |
16
+ | **MSP/IT Tools** | `MeshcentralLogoIcon`, `FleetMdmLogoIcon`, `OsqueryLogoIcon`, `PowershellLogoIcon` |
17
17
  | **Dev Tools** | `GithubIcon`, `FigmaIcon`, `FramerIcon`, `TrelloIcon`, `ClickupLogoIcon` |
18
18
 
19
19
  > Many brands include both a standard and a `Grey` variant (e.g., `SlackLogoIcon` / `SlackLogoGreyIcon`), useful for active/inactive states.
@@ -26,7 +26,6 @@ import {
26
26
  GithubIcon,
27
27
  SlackLogoIcon,
28
28
  SlackLogoGreyIcon,
29
- TacticalRmmLogoIcon,
30
29
  OpenaiLogoIcon,
31
30
  } from '@/components/icons/brands';
32
31
 
@@ -1,5 +1,5 @@
1
1
  <!-- source-hash: f614dd9faeadf368597d094822bcd3c7 -->
2
- Displays a badge combining an icon and label for a given shell/script type, supporting all Tactical RMM shell types (PowerShell, CMD, Bash, Python, Nushell, Deno, Shell).
2
+ Displays a badge combining an icon and label for a given shell/script type, supporting all OpenFrame RMM shell types (PowerShell, CMD, Bash, Python, Nushell, Deno, Shell).
3
3
 
4
4
  ## Key Components
5
5
 
@@ -1,5 +1,5 @@
1
1
  <!-- source-hash: 5668aa064d8c050beff751a96d6469be -->
2
- A React component that renders a visual badge combining a tool icon and label for OpenFrame-integrated MSP tools (e.g., Tactical RMM, Fleet MDM, MeshCentral).
2
+ A React component that renders a visual badge combining a tool icon and label for OpenFrame-integrated MSP tools (e.g., Fleet MDM, MeshCentral).
3
3
 
4
4
  ## Key Components
5
5
 
@@ -15,7 +15,7 @@ A React component that renders a visual badge combining a tool icon and label fo
15
15
  import { ToolBadge } from './components/ToolBadge'
16
16
 
17
17
  // Basic usage in a table cell
18
- <ToolBadge toolType="TACTICAL" />
18
+ <ToolBadge toolType="OPENFRAME_RMM" />
19
19
 
20
20
  // With custom styling
21
21
  <ToolBadge
@@ -90,6 +90,7 @@ const SecondaryAction: React.FC<{ action: ActionsMenuItemIconAction }> = ({ acti
90
90
 
91
91
  const classes = cn(
92
92
  SECONDARY_ACTION_CLASSES,
93
+ action.openInNewTab && "max-md:hidden",
93
94
  action.disabled && "cursor-not-allowed opacity-60 pointer-events-none",
94
95
  );
95
96
 
@@ -32,6 +32,7 @@ const splitHalfVariants = cva(splitHalfBase, {
32
32
  small: "h-6 md:h-8 px-[var(--spacing-system-xs)] text-h5 [&_svg]:h-3 [&_svg]:w-3 md:[&_svg]:h-4 md:[&_svg]:w-4",
33
33
  },
34
34
  side: { main: "", icon: "" },
35
+ soloOnMobile: { true: "", false: "" },
35
36
  },
36
37
  compoundVariants: [
37
38
  // Rounded corners + per-variant seam. The icon-side's left border is the divider.
@@ -55,8 +56,12 @@ const splitHalfVariants = cva(splitHalfBase, {
55
56
  // Icon half: per Figma, narrower than main height (default: 40×48; small: 32×32).
56
57
  { side: "icon", size: "default", class: "w-10 px-0" },
57
58
  { side: "icon", size: "small", class: "w-6 md:w-8 px-0" },
59
+
60
+ { side: "icon", soloOnMobile: true, class: "max-md:hidden" },
61
+ { side: "main", soloOnMobile: true, class: "max-md:rounded-r-md" },
62
+ { variant: "outline", side: "main", soloOnMobile: true, class: "max-md:border-r" },
58
63
  ],
59
- defaultVariants: { variant: "accent", size: "default", side: "main" },
64
+ defaultVariants: { variant: "accent", size: "default", side: "main", soloOnMobile: false },
60
65
  })
61
66
 
62
67
  type SplitButtonVariant = NonNullable<VariantProps<typeof splitHalfVariants>["variant"]>
@@ -68,6 +73,11 @@ interface SplitButtonIconAction {
68
73
  "aria-label": string
69
74
  onClick?: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>
70
75
  href?: string
76
+ /**
77
+ * Opens `href` with `target="_blank"`. Also marks the half as a new-tab
78
+ * affordance, which hides it below `md` (set it for `onClick` handlers that
79
+ * open a new tab/window themselves).
80
+ */
71
81
  openInNewTab?: boolean
72
82
  prefetch?: boolean
73
83
  disabled?: boolean
@@ -101,6 +111,7 @@ interface HalfOptions {
101
111
  variant: SplitButtonVariant
102
112
  size: SplitButtonSize
103
113
  side: "main" | "icon"
114
+ soloOnMobile?: boolean
104
115
  href?: string
105
116
  openInNewTab?: boolean
106
117
  prefetch?: boolean
@@ -113,8 +124,8 @@ interface HalfOptions {
113
124
  children: React.ReactNode
114
125
  }
115
126
 
116
- function Half({ variant, size, side, href, openInNewTab, prefetch, onClick, disabled, grow, type = "button", ariaLabel, children }: HalfOptions) {
117
- const classes = cn(splitHalfVariants({ variant, size, side }), grow && "flex-1")
127
+ function Half({ variant, size, side, soloOnMobile, href, openInNewTab, prefetch, onClick, disabled, grow, type = "button", ariaLabel, children }: HalfOptions) {
128
+ const classes = cn(splitHalfVariants({ variant, size, side, soloOnMobile }), grow && "flex-1")
118
129
 
119
130
  if (href) {
120
131
  return (
@@ -169,6 +180,8 @@ const SplitButton = React.forwardRef<HTMLDivElement, SplitButtonProps>(function
169
180
  },
170
181
  ref,
171
182
  ) {
183
+ const soloOnMobile = !!iconAction.openInNewTab
184
+
172
185
  return (
173
186
  <div
174
187
  ref={ref}
@@ -180,6 +193,7 @@ const SplitButton = React.forwardRef<HTMLDivElement, SplitButtonProps>(function
180
193
  variant={variant}
181
194
  size={size}
182
195
  side="main"
196
+ soloOnMobile={soloOnMobile}
183
197
  href={href}
184
198
  openInNewTab={openInNewTab}
185
199
  prefetch={prefetch}
@@ -197,6 +211,7 @@ const SplitButton = React.forwardRef<HTMLDivElement, SplitButtonProps>(function
197
211
  variant={variant}
198
212
  size={size}
199
213
  side="icon"
214
+ soloOnMobile={soloOnMobile}
200
215
  href={iconAction.href}
201
216
  openInNewTab={iconAction.openInNewTab}
202
217
  prefetch={iconAction.prefetch}
@@ -245,6 +245,12 @@ function IconButtonsVariant({
245
245
  const singleAction = isSingleAction ? actions[0] : null
246
246
  const useSingleActionMobile = isSingleAction && !hasMenuActions
247
247
 
248
+ const mobileMenuGroups = [
249
+ { items: actions.flatMap(actionToMenuItems) },
250
+ ...(menuActions ?? []),
251
+ ]
252
+ const hasMobileMenuItems = mobileMenuGroups.some(g => g.items.length > 0)
253
+
248
254
  return (
249
255
  <>
250
256
  {/* Desktop: every action as an icon button + optional overflow menu */}
@@ -262,14 +268,9 @@ function IconButtonsVariant({
262
268
  <div className={cn('flex md:hidden', className)}>
263
269
  {useSingleActionMobile && singleAction ? (
264
270
  renderActionButton(singleAction, { iconOnly: true })
265
- ) : (
266
- <ActionsMenuDropdown
267
- groups={[
268
- { items: actions.flatMap(actionToMenuItems) },
269
- ...(menuActions ?? []),
270
- ]}
271
- />
272
- )}
271
+ ) : hasMobileMenuItems ? (
272
+ <ActionsMenuDropdown groups={mobileMenuGroups} />
273
+ ) : null}
273
274
  </div>
274
275
  </>
275
276
  )
@@ -328,6 +329,12 @@ function MenuPrimaryVariant({
328
329
  const desktopActions = actions.filter(a => !a.showOnlyMobile)
329
330
  const hasMenuActions = menuActions.some(g => g.items.length > 0)
330
331
 
332
+ const mobileMenuGroups = [
333
+ { items: actions.flatMap(actionToMenuItems) },
334
+ ...menuActions,
335
+ ]
336
+ const hasMobileMenuItems = mobileMenuGroups.some(g => g.items.length > 0)
337
+
331
338
  return (
332
339
  <>
333
340
  <div className={cn('hidden md:flex items-center', ACTIONS_GAP, className)}>
@@ -341,12 +348,7 @@ function MenuPrimaryVariant({
341
348
  </div>
342
349
 
343
350
  <div className={cn('flex md:hidden', className)}>
344
- <ActionsMenuDropdown
345
- groups={[
346
- { items: actions.flatMap(actionToMenuItems) },
347
- ...menuActions,
348
- ]}
349
- />
351
+ {hasMobileMenuItems ? <ActionsMenuDropdown groups={mobileMenuGroups} /> : null}
350
352
  </div>
351
353
  </>
352
354
  )
@@ -31,4 +31,4 @@ export const AllOSTypes: Story = {
31
31
  };
32
32
  ```
33
33
 
34
- The stories demonstrate OS type normalization from various MDM sources (Fleet, Tactical RMM) with support for aliases like "Darwin" → "macOS" and "win32" → "Windows". Includes examples for icon-only, label-only, and combined display modes with customizable sizing options.
34
+ The stories demonstrate OS type normalization from various MDM sources (Fleet) with support for aliases like "Darwin" → "macOS" and "win32" → "Windows". Includes examples for icon-only, label-only, and combined display modes with customizable sizing options.
@@ -1,5 +1,5 @@
1
1
  <!-- source-hash: fe5ee5e74229fbc813833a5d6c81b725 -->
2
- Storybook configuration for the ShellTypeBadge component, defining visual documentation and interactive examples for all supported Tactical RMM shell types including PowerShell, CMD, Bash, Python, Nushell, Deno, and generic Shell.
2
+ Storybook configuration for the ShellTypeBadge component, defining visual documentation and interactive examples for all supported OpenFrame RMM shell types including PowerShell, CMD, Bash, Python, Nushell, Deno, and generic Shell.
3
3
 
4
4
  ## Key Components
5
5
 
@@ -9,7 +9,7 @@ Storybook stories for the `Toaster` / `ToastCard` component system, covering sta
9
9
  | `Playground` | Story | Arg-driven interactive card — use Storybook Controls to tweak `variant`, `title`, `description`, `duration`, and `dismissible` |
10
10
  | `CommandApprovalCollapsed` | Story | `CommandApprovalToast` in its default collapsed state |
11
11
  | `CommandApprovalExpanded` | Story | `CommandApprovalToast` with `defaultExpanded` showing the command preview and Approve/Reject actions |
12
- | `CommandApprovalPerTool` | Story | One expanded approval toast per supported tool type (`FLEET_MDM`, `TACTICAL_RMM`, `MESHCENTRAL`, `AUTHENTIK`, `OSQUERY`, `OPENFRAME`) to verify icon mapping |
12
+ | `CommandApprovalPerTool` | Story | One expanded approval toast per supported tool type (`FLEET_MDM`, `MESHCENTRAL`, `AUTHENTIK`, `OSQUERY`, `OPENFRAME`) to verify icon mapping |
13
13
  | `CommandApprovalLive` | Story | Fires real Sonner toasts via `showCommandApprovalToast()` with `onApprove`/`onReject` callbacks |
14
14
  | `LiveTriggers` | Story | Button panel that fires every variant as a live Sonner toast using both `toast()` and `showToast()` |
15
15
 
@@ -4,7 +4,7 @@ Storybook configuration file for the ToolBadge component, providing interactive
4
4
  ## Key Components
5
5
 
6
6
  - **meta**: Main Storybook configuration with component metadata, controls, and documentation
7
- - **Story exports**: Individual stories showcasing different tool types (Tactical RMM, Fleet MDM, MeshCentral, etc.)
7
+ - **Story exports**: Individual stories showcasing different tool types (Fleet MDM, MeshCentral, etc.)
8
8
  - **AllTools story**: Comparison view showing multiple tool badges together
9
9
  - **argTypes**: Interactive controls for `toolType` selection and `className` customization
10
10
 
@@ -14,7 +14,7 @@ Storybook configuration file for the ToolBadge component, providing interactive
14
14
  // Individual tool badge stories
15
15
  export const Default: Story = {
16
16
  args: {
17
- toolType: 'TACTICAL_RMM',
17
+ toolType: 'OPENFRAME_RMM',
18
18
  },
19
19
  };
20
20
 
@@ -5,7 +5,7 @@ Storybook stories for the `ToolExecutionDisplay` chat component, covering execut
5
5
 
6
6
  | Export | Description |
7
7
  |--------|-------------|
8
- | `CollapsedExecuting` | Spinner state while a tool (e.g. `run_script` via Tactical RMM) is in progress |
8
+ | `CollapsedExecuting` | Spinner state while a tool (e.g. `run_script` via OpenFrame RMM) is in progress |
9
9
  | `CollapsedSuccess` | Green check state after successful tool execution, with result output |
10
10
  | `CollapsedFailure` | Red error icon state after a failed tool execution (e.g. Fleet MDM profile deploy) |
11
11
  | `constrainedDecorator` | Helper decorator that wraps stories in a `max-width` container (default 400px) |
@@ -18,7 +18,7 @@ import type { ToolExecutionData } from "../components/chat/types";
18
18
 
19
19
  const message: ToolExecutionData = {
20
20
  type: "EXECUTED_TOOL",
21
- integratedToolType: "TACTICAL_RMM",
21
+ integratedToolType: "OPENFRAME_RMM",
22
22
  toolFunction: "run_script",
23
23
  parameters: { RunAsUser: "False", timeoutSeconds: 60 },
24
24
  result: "Script output here...",
@@ -34,8 +34,8 @@ Three fixture payloads cover the core display states:
34
34
 
35
35
  | Fixture | `type` | `integratedToolType` | `success` |
36
36
  |---------|--------|----------------------|-----------|
37
- | `executingMessage` | `EXECUTING_TOOL` | `TACTICAL_RMM` | — |
38
- | `executedSuccessMessage` | `EXECUTED_TOOL` | `TACTICAL_RMM` | `true` |
37
+ | `executingMessage` | `EXECUTING_TOOL` | `OPENFRAME_RMM` | — |
38
+ | `executedSuccessMessage` | `EXECUTED_TOOL` | `OPENFRAME_RMM` | `true` |
39
39
  | `executedFailureMessage` | `EXECUTED_TOOL` | `FLEET_MDM` | `false` |
40
40
 
41
41
  The component renders a collapsed card by default; clicking toggles full details including parameters and result output.
@@ -1,5 +1,5 @@
1
1
  <!-- source-hash: 6581d93b3edee981ded786a177e99959 -->
2
- Defines the centralized source of truth for operating system type metadata across the Flamingo platform, normalizing OS values from Fleet MDM, Tactical RMM, and GraphQL into a unified structure.
2
+ Defines the centralized source of truth for operating system type metadata across the Flamingo platform, normalizing OS values from Fleet MDM and GraphQL into a unified structure.
3
3
 
4
4
  ## Key Components
5
5
 
@@ -1,5 +1,5 @@
1
1
  <!-- source-hash: b8477e619839e162b7edcfaaf835462d -->
2
- Centralized type definitions for shell/script types supported across the Flamingo platform. Provides a single source of truth for shell metadata, icons, and labels based on Tactical RMM supported types.
2
+ Centralized type definitions for shell/script types supported across the Flamingo platform. Provides a single source of truth for shell metadata, icons, and labels based on OpenFrame RMM supported types.
3
3
 
4
4
  ## Key Components
5
5
 
@@ -3,7 +3,7 @@ Centralized type definitions for MSP tool types used throughout the Flamingo/Ope
3
3
 
4
4
  ## Key Components
5
5
 
6
- - **`ToolTypeValues`** - Const assertion object containing all available tool type constants (TACTICAL_RMM, FLEET_MDM, MESHCENTRAL, etc.)
6
+ - **`ToolTypeValues`** - Const assertion object containing all available tool type constants (FLEET_MDM, MESHCENTRAL, etc.)
7
7
  - **`ToolType`** - Union type derived from ToolTypeValues for type-safe tool type references
8
8
  - **`toolLabels`** - Record mapping tool types to human-readable display names
9
9
 
@@ -13,10 +13,10 @@ Centralized type definitions for MSP tool types used throughout the Flamingo/Ope
13
13
  import { ToolType, ToolTypeValues, toolLabels } from './tool.types'
14
14
 
15
15
  // Type-safe tool type usage
16
- const currentTool: ToolType = ToolTypeValues.TACTICAL_RMM
16
+ const currentTool: ToolType = ToolTypeValues.MESHCENTRAL
17
17
 
18
18
  // Get display label for UI
19
- const displayName = toolLabels[currentTool] // "Tactical"
19
+ const displayName = toolLabels[currentTool] // "MeshCentral"
20
20
 
21
21
  // Use in component props
22
22
  interface ToolBadgeProps {
@@ -22,7 +22,6 @@ import {
22
22
  } from './tool-utils'
23
23
 
24
24
  // Normalize various input formats
25
- normalizeToolType('tactical') // => 'TACTICAL_RMM'
26
25
  normalizeToolType('FLEET-MDM') // => 'FLEET_MDM'
27
26
  normalizeToolType('unknown') // => undefined
28
27
 
@@ -31,7 +30,6 @@ normalizeToolTypeWithFallback('mesh') // => 'MESHCENTRAL'
31
30
  normalizeToolTypeWithFallback('invalid') // => 'SYSTEM'
32
31
 
33
32
  // Convert to display labels
34
- toToolLabel('tactical') // => 'Tactical'
35
33
  toToolLabel('fleet_mdm') // => 'Fleet'
36
34
 
37
35
  // Validation
@@ -39,4 +37,4 @@ isValidToolType('authentik') // => true
39
37
  isValidToolType('invalid') // => false
40
38
  ```
41
39
 
42
- The utility supports all major MSP tools including Tactical RMM, Fleet MDM, MeshCentral, Authentik, and OpenFrame components with comprehensive alias mapping for different naming conventions.
40
+ The utility supports all major MSP tools including Fleet MDM, MeshCentral, Authentik, and OpenFrame components with comprehensive alias mapping for different naming conventions.
@@ -29,6 +29,7 @@ export { getBaseUrl } from '../utils/cn'
29
29
  // SEO title length budget — server-safe constant (SSOT). Consumed by the hub
30
30
  // (prompt guidance + DB check value) and by SEOEditorPreview (input maxLength).
31
31
  export { SEO_TITLE_MAX_LENGTH } from './seo-title'
32
+ export { SEO_DESCRIPTION_MAX_LENGTH } from './seo-description'
32
33
 
33
34
  // Brand silhouette SVG registry (CSS mask-image tinting) — centralized so
34
35
  // consumers (e.g. company-hub deck) don't ship their own copies of the marks.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Single source of truth for the `seo_description` length budget.
3
+ *
4
+ * Unlike `seo_title` (soft search-engine guidance), this is a HARD cap: the
5
+ * `seo_description` DB columns are `varchar(160)` and Postgres rejects longer
6
+ * values outright ("value too long for type character varying(160)").
7
+ *
8
+ * This number lives in exactly ONE place. Consumers:
9
+ * - `SEOEditorPreview` — input `maxLength` + the too-long alert.
10
+ * - the hub — imports it (via `@flamingo-stack/openframe-frontend-core/utils`)
11
+ * for the AI generation prompt guidance and the rewrite-to-fit helper.
12
+ */
13
+ export const SEO_DESCRIPTION_MAX_LENGTH = 160;