@flamingo-stack/openframe-frontend-core 0.0.219 → 0.0.220-snapshot.20260602171504

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 (146) hide show
  1. package/dist/{chunk-EDW2NVRV.js → chunk-4WZOFD46.js} +37 -37
  2. package/dist/{chunk-EDW2NVRV.js.map → chunk-4WZOFD46.js.map} +1 -1
  3. package/dist/{chunk-ZGBXHK26.cjs → chunk-5GN7TXHY.cjs} +12 -12
  4. package/dist/{chunk-ZGBXHK26.cjs.map → chunk-5GN7TXHY.cjs.map} +1 -1
  5. package/dist/{chunk-F3FO2ZZZ.cjs → chunk-BAKZF4GU.cjs} +7 -7
  6. package/dist/{chunk-F3FO2ZZZ.cjs.map → chunk-BAKZF4GU.cjs.map} +1 -1
  7. package/dist/{chunk-MPHDM2VZ.cjs → chunk-BCL24DFU.cjs} +30 -30
  8. package/dist/{chunk-MPHDM2VZ.cjs.map → chunk-BCL24DFU.cjs.map} +1 -1
  9. package/dist/{chunk-65CPJ4SX.cjs → chunk-C6ASEPZL.cjs} +30 -30
  10. package/dist/{chunk-65CPJ4SX.cjs.map → chunk-C6ASEPZL.cjs.map} +1 -1
  11. package/dist/{chunk-SZXKKEUH.cjs → chunk-E6B4B7GM.cjs} +46 -30
  12. package/dist/chunk-E6B4B7GM.cjs.map +1 -0
  13. package/dist/{chunk-SRA2QYK6.js → chunk-HUA4XG4S.js} +4 -4
  14. package/dist/{chunk-A3PL6ZCF.js → chunk-KXF3WCPH.js} +6397 -5128
  15. package/dist/chunk-KXF3WCPH.js.map +1 -0
  16. package/dist/{chunk-SL3RGBPX.cjs → chunk-QNYH3WUU.cjs} +9 -9
  17. package/dist/{chunk-SL3RGBPX.cjs.map → chunk-QNYH3WUU.cjs.map} +1 -1
  18. package/dist/{chunk-24Q2WLIU.js → chunk-QYRV6MKX.js} +2 -2
  19. package/dist/{chunk-XG7DFRJL.js → chunk-RCECOGMI.js} +3 -3
  20. package/dist/{chunk-7UZLRI7W.cjs → chunk-SEECETJY.cjs} +3301 -2032
  21. package/dist/chunk-SEECETJY.cjs.map +1 -0
  22. package/dist/{chunk-ZII7TNVA.js → chunk-T5MEXJD5.js} +3 -3
  23. package/dist/{chunk-YX3YQNC4.cjs → chunk-W23DRJAA.cjs} +13 -13
  24. package/dist/{chunk-YX3YQNC4.cjs.map → chunk-W23DRJAA.cjs.map} +1 -1
  25. package/dist/{chunk-DRPECAXO.js → chunk-WR32ZE63.js} +2 -2
  26. package/dist/{chunk-Y3MXGCOW.js → chunk-YZDUOUMB.js} +46 -30
  27. package/dist/chunk-YZDUOUMB.js.map +1 -0
  28. package/dist/components/chat/chat-archive-page.d.ts +25 -0
  29. package/dist/components/chat/chat-archive-page.d.ts.map +1 -0
  30. package/dist/components/chat/chat-composer.d.ts +29 -0
  31. package/dist/components/chat/chat-composer.d.ts.map +1 -0
  32. package/dist/components/chat/chat-header-icon-button.d.ts +14 -0
  33. package/dist/components/chat/chat-header-icon-button.d.ts.map +1 -0
  34. package/dist/components/chat/chat-panel-header.d.ts +32 -0
  35. package/dist/components/chat/chat-panel-header.d.ts.map +1 -0
  36. package/dist/components/chat/embeddable-chat.d.ts +18 -0
  37. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  38. package/dist/components/chat/guide-mode-banner.d.ts +16 -0
  39. package/dist/components/chat/guide-mode-banner.d.ts.map +1 -0
  40. package/dist/components/chat/guide-welcome.d.ts +40 -0
  41. package/dist/components/chat/guide-welcome.d.ts.map +1 -0
  42. package/dist/components/chat/hooks/use-chat-dialog-manager.d.ts +58 -0
  43. package/dist/components/chat/hooks/use-chat-dialog-manager.d.ts.map +1 -0
  44. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +26 -1
  45. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  46. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  47. package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -1
  48. package/dist/components/chat/index.cjs +29 -5
  49. package/dist/components/chat/index.cjs.map +1 -1
  50. package/dist/components/chat/index.d.ts +9 -0
  51. package/dist/components/chat/index.d.ts.map +1 -1
  52. package/dist/components/chat/index.js +28 -4
  53. package/dist/components/chat/mingo-chat-history.d.ts +37 -0
  54. package/dist/components/chat/mingo-chat-history.d.ts.map +1 -0
  55. package/dist/components/chat/mingo-chat-modals.d.ts +50 -0
  56. package/dist/components/chat/mingo-chat-modals.d.ts.map +1 -0
  57. package/dist/components/chat/mingo-onboarding-card.d.ts.map +1 -1
  58. package/dist/components/chat/mingo-welcome.d.ts +78 -0
  59. package/dist/components/chat/mingo-welcome.d.ts.map +1 -0
  60. package/dist/components/chat/types/unified-chat-state.types.d.ts +6 -0
  61. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  62. package/dist/components/contact/index.cjs +6 -6
  63. package/dist/components/contact/index.js +5 -5
  64. package/dist/components/features/index.cjs +5 -5
  65. package/dist/components/features/index.js +4 -4
  66. package/dist/components/icons-v2-generated/brand-logos/fleet-mdm-logo-grey-icon.d.ts.map +1 -1
  67. package/dist/components/icons-v2-generated/brand-logos/fleet-mdm-logo-icon.d.ts.map +1 -1
  68. package/dist/components/icons-v2-generated/index.cjs +2 -2
  69. package/dist/components/icons-v2-generated/index.js +1 -1
  70. package/dist/components/index.cjs +128 -104
  71. package/dist/components/index.cjs.map +1 -1
  72. package/dist/components/index.js +31 -7
  73. package/dist/components/index.js.map +1 -1
  74. package/dist/components/layout/page-heading.d.ts +7 -6
  75. package/dist/components/layout/page-heading.d.ts.map +1 -1
  76. package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
  77. package/dist/components/navigation/header-mingo-button.d.ts +4 -2
  78. package/dist/components/navigation/header-mingo-button.d.ts.map +1 -1
  79. package/dist/components/navigation/index.cjs +5 -5
  80. package/dist/components/navigation/index.js +4 -4
  81. package/dist/components/onboarding-guides/index.cjs +28 -28
  82. package/dist/components/onboarding-guides/index.js +6 -6
  83. package/dist/components/tickets/index.cjs +88 -88
  84. package/dist/components/tickets/index.js +7 -7
  85. package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
  86. package/dist/components/ui/file-manager/index.cjs +50 -50
  87. package/dist/components/ui/file-manager/index.js +1 -1
  88. package/dist/components/ui/index.cjs +29 -5
  89. package/dist/components/ui/index.cjs.map +1 -1
  90. package/dist/components/ui/index.js +28 -4
  91. package/dist/components/ui/modal-v2.d.ts.map +1 -1
  92. package/dist/components/ui/more-actions-menu.d.ts +8 -1
  93. package/dist/components/ui/more-actions-menu.d.ts.map +1 -1
  94. package/dist/components/ui/portal-container.d.ts +21 -0
  95. package/dist/components/ui/portal-container.d.ts.map +1 -0
  96. package/dist/components/ui/tooltip.d.ts.map +1 -1
  97. package/dist/hooks/index.cjs +3 -3
  98. package/dist/hooks/index.js +2 -2
  99. package/dist/index.cjs +29 -5
  100. package/dist/index.cjs.map +1 -1
  101. package/dist/index.js +28 -4
  102. package/package.json +1 -1
  103. package/src/components/chat/chat-archive-page.tsx +93 -0
  104. package/src/components/chat/chat-composer.tsx +99 -0
  105. package/src/components/chat/chat-header-icon-button.tsx +36 -0
  106. package/src/components/chat/chat-panel-header.tsx +114 -0
  107. package/src/components/chat/embeddable-chat.tsx +386 -311
  108. package/src/components/chat/guide-mode-banner.tsx +75 -0
  109. package/src/components/chat/guide-welcome.tsx +207 -0
  110. package/src/components/chat/hooks/use-chat-dialog-manager.ts +227 -0
  111. package/src/components/chat/hooks/use-nats-chat-adapter.ts +85 -0
  112. package/src/components/chat/hooks/use-sse-chat-adapter.ts +8 -0
  113. package/src/components/chat/hooks/use-unified-chat.ts +12 -0
  114. package/src/components/chat/index.ts +9 -0
  115. package/src/components/chat/mingo-chat-history.tsx +308 -0
  116. package/src/components/chat/mingo-chat-modals.tsx +223 -0
  117. package/src/components/chat/mingo-onboarding-card.tsx +5 -8
  118. package/src/components/chat/mingo-welcome.tsx +396 -0
  119. package/src/components/chat/types/unified-chat-state.types.ts +8 -0
  120. package/src/components/icons-v2/brand-logos/fleet-mdm-logo-grey.svg +6 -6
  121. package/src/components/icons-v2/brand-logos/fleet-mdm-logo.svg +6 -6
  122. package/src/components/icons-v2-generated/brand-logos/fleet-mdm-logo-grey-icon.tsx +2 -22
  123. package/src/components/icons-v2-generated/brand-logos/fleet-mdm-logo-icon.tsx +22 -2
  124. package/src/components/layout/page-heading.tsx +13 -7
  125. package/src/components/navigation/app-header.tsx +12 -12
  126. package/src/components/navigation/app-layout-drawer.tsx +25 -15
  127. package/src/components/navigation/header-mingo-button.tsx +22 -7
  128. package/src/components/ui/dropdown-menu.tsx +9 -3
  129. package/src/components/ui/modal-v2.tsx +33 -3
  130. package/src/components/ui/more-actions-menu.tsx +15 -2
  131. package/src/components/ui/portal-container.tsx +28 -0
  132. package/src/components/ui/tooltip.tsx +9 -3
  133. package/src/stories/AppLayoutSidebar.stories.tsx +184 -0
  134. package/src/stories/EmbeddableChat.stories.tsx +114 -0
  135. package/src/stories/GuideWelcome.stories.tsx +102 -0
  136. package/src/stories/MingoChatModals.stories.tsx +82 -0
  137. package/src/stories/MingoWelcome.stories.tsx +119 -0
  138. package/dist/chunk-7UZLRI7W.cjs.map +0 -1
  139. package/dist/chunk-A3PL6ZCF.js.map +0 -1
  140. package/dist/chunk-SZXKKEUH.cjs.map +0 -1
  141. package/dist/chunk-Y3MXGCOW.js.map +0 -1
  142. /package/dist/{chunk-SRA2QYK6.js.map → chunk-HUA4XG4S.js.map} +0 -0
  143. /package/dist/{chunk-24Q2WLIU.js.map → chunk-QYRV6MKX.js.map} +0 -0
  144. /package/dist/{chunk-XG7DFRJL.js.map → chunk-RCECOGMI.js.map} +0 -0
  145. /package/dist/{chunk-ZII7TNVA.js.map → chunk-T5MEXJD5.js.map} +0 -0
  146. /package/dist/{chunk-DRPECAXO.js.map → chunk-WR32ZE63.js.map} +0 -0
@@ -105,16 +105,14 @@ const appLayoutDrawerVariants = cva(
105
105
  },
106
106
  },
107
107
  compoundVariants: [
108
- { side: "right", flush: false, class: "pr-4 py-4" },
109
- { side: "left", flush: false, class: "pl-4 py-4" },
110
- { side: "top", flush: false, class: "pt-4 px-4" },
111
- { side: "bottom", flush: false, class: "pb-4 px-4" },
112
- // flush=true: keep wrapper padding on desktop (uniform 16px gap so the
113
- // panel floats), drop on mobile for full-bleed — matches base Drawer.
114
- { side: "right", flush: true, class: "md:pr-4 md:py-4" },
115
- { side: "left", flush: true, class: "md:pl-4 md:py-4" },
116
- { side: "top", flush: true, class: "md:pt-4 md:px-4" },
117
- { side: "bottom", flush: true, class: "md:pb-4 md:px-4" },
108
+ { side: "right", flush: false, class: "pr-[var(--spacing-system-m)] py-[var(--spacing-system-m)]" },
109
+ { side: "left", flush: false, class: "pl-[var(--spacing-system-m)] py-[var(--spacing-system-m)]" },
110
+ { side: "top", flush: false, class: "pt-[var(--spacing-system-m)] px-[var(--spacing-system-m)]" },
111
+ { side: "bottom", flush: false, class: "pb-[var(--spacing-system-m)] px-[var(--spacing-system-m)]" },
112
+ { side: "right", flush: true, class: "md:pr-[var(--spacing-system-m)] md:py-[var(--spacing-system-m)]" },
113
+ { side: "left", flush: true, class: "md:pl-[var(--spacing-system-m)] md:py-[var(--spacing-system-m)]" },
114
+ { side: "top", flush: true, class: "md:pt-[var(--spacing-system-m)] md:px-[var(--spacing-system-m)]" },
115
+ { side: "bottom", flush: true, class: "md:pb-[var(--spacing-system-m)] md:px-[var(--spacing-system-m)]" },
118
116
  ],
119
117
  defaultVariants: {
120
118
  side: "right",
@@ -192,11 +190,11 @@ function useContainedResizableSize({
192
190
 
193
191
  const clampToContainer = React.useCallback(
194
192
  (value: number) => {
195
- // Reserve 32px (16px outside-edge padding from the wrapper + 16px
196
- // matching gap on the inside edge) so the panel sits symmetrically
193
+ // Reserve 40px (the `system-m` outside-edge padding from the wrapper
194
+ // plus a matching gap on the inside edge) so the panel sits symmetrically
197
195
  // inside the container. This also keeps the resize grip on-screen at
198
196
  // maximum extent.
199
- const effectiveMax = available > 0 ? Math.min(maxSize, available - 32) : maxSize
197
+ const effectiveMax = available > 0 ? Math.min(maxSize, available - 40) : maxSize
200
198
  return clamp(value, minSize, Math.max(minSize, effectiveMax))
201
199
  },
202
200
  [available, minSize, maxSize],
@@ -547,13 +545,19 @@ const AppLayoutDrawerContent = React.forwardRef<
547
545
  aria-hidden
548
546
  data-state={open ? "open" : "closed"}
549
547
  className={cn(
550
- "absolute inset-0 z-[40] bg-black/50 outline-none data-[state=open]:pointer-events-auto data-[state=closed]:pointer-events-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
548
+ "absolute inset-0 z-[40] bg-[color-mix(in_srgb,var(--ods-system-greys-background)_50%,transparent)] outline-none data-[state=open]:pointer-events-auto data-[state=closed]:pointer-events-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
551
549
  overlayClassName,
552
550
  )}
553
551
  />
554
552
  <DialogPrimitive.Content
555
553
  ref={ref}
556
- className={cn(appLayoutDrawerVariants({ side, flush }))}
554
+ className={cn(
555
+ appLayoutDrawerVariants({ side, flush }),
556
+ // Mobile: span the whole container and drop the outer gap so the
557
+ // panel renders edge-to-edge full-screen. (`p-0` overrides the
558
+ // variant's wrapper padding via tailwind-merge.)
559
+ isMobile && "inset-0 p-0",
560
+ )}
557
561
  style={style}
558
562
  onInteractOutside={(event) => {
559
563
  onInteractOutside?.(event)
@@ -585,6 +589,12 @@ const AppLayoutDrawerContent = React.forwardRef<
585
589
  <div
586
590
  className={cn(
587
591
  appLayoutDrawerPanelVariants({ side, flush }),
592
+ // Mobile: fill the container (the side already pins one axis) and
593
+ // drop the card chrome so the panel is edge-to-edge full-screen.
594
+ // Inner padding/gap is preserved so content isn't glued to the
595
+ // edges. tailwind-merge lets these override the variant classes.
596
+ isMobile && (isHorizontal ? "w-full" : "h-full"),
597
+ isMobile && "rounded-none border-0",
588
598
  className,
589
599
  panelClassName,
590
600
  )}
@@ -3,6 +3,7 @@
3
3
  import React from 'react'
4
4
  import { cn } from '../../utils/cn'
5
5
  import { MingoIcon } from '../icons'
6
+ import { XmarkIcon } from '../icons-v2-generated'
6
7
 
7
8
  export interface HeaderMingoButtonProps
8
9
  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
@@ -18,8 +19,10 @@ export interface HeaderMingoButtonProps
18
19
  /**
19
20
  * "Mingo AI" launcher button for `AppHeader`. Mirrors the `HeaderButton`
20
21
  * visual contract (sticky header height, `ods-card` rest / `ods-bg-hover`
21
- * hover / `ods-bg-active` active, divider via `AppHeader`'s `divide-x`), but
22
- * carries both the Mingo logo and the bold "Mingo AI" wordmark.
22
+ * hover, divider via `AppHeader`'s `divide-x`), but carries both the Mingo
23
+ * logo and the bold "Mingo AI" wordmark. The open/active state is exposed via
24
+ * `aria-pressed` (and, in `iconOnly` mode, by swapping the logo for a close
25
+ * "X"); there is no distinct active background.
23
26
  *
24
27
  * Figma: 7532:222103 — `button-full`.
25
28
  */
@@ -32,20 +35,32 @@ export function HeaderMingoButton({
32
35
  return (
33
36
  <button
34
37
  type="button"
35
- aria-label="Mingo AI"
38
+ aria-label={iconOnly && isActive ? 'Close Mingo AI' : 'Mingo AI'}
36
39
  aria-pressed={isActive}
37
40
  className={cn(
38
41
  'flex items-center shrink-0 h-full gap-2 px-4',
39
42
  'transition-colors duration-200',
40
43
  'focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent',
41
- isActive
42
- ? 'text-ods-text-primary bg-ods-bg-active'
43
- : 'text-ods-text-primary bg-ods-card hover:bg-ods-bg-hover',
44
+ 'text-ods-text-primary bg-ods-card hover:bg-ods-bg-hover',
44
45
  className,
45
46
  )}
46
47
  {...props}
47
48
  >
48
- <MingoIcon className="w-6 h-6 shrink-0" />
49
+ {iconOnly && isActive ? (
50
+ // Mobile, drawer open: the icon-only button doubles as the close
51
+ // affordance, so swap the Mingo logo for an X. Match the other header
52
+ // icons (e.g. notifications): secondary color, w-4 h-4 → md:w-6 h-6.
53
+ <XmarkIcon className="w-4 h-4 md:w-6 md:h-6 shrink-0 text-ods-text-secondary" />
54
+ ) : (
55
+ // Outer frame follows the button's text color (currentColor); the eyes
56
+ // and corner block are ODS cyan.
57
+ <MingoIcon
58
+ color="currentColor"
59
+ eyesColor="var(--ods-flamingo-cyan-base)"
60
+ cornerColor="var(--ods-flamingo-cyan-base)"
61
+ className="w-4 h-4 md:w-6 md:h-6 shrink-0"
62
+ />
63
+ )}
49
64
  {!iconOnly && (
50
65
  <span className="text-h3 font-bold tracking-[-0.36px] whitespace-nowrap">
51
66
  Mingo AI
@@ -5,6 +5,7 @@ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5
5
  import { Check, ChevronRight, Circle } from "lucide-react"
6
6
 
7
7
  import { cn } from "../../utils/cn"
8
+ import { usePortalContainer } from "./portal-container"
8
9
 
9
10
  const DropdownMenu = DropdownMenuPrimitive.Root
10
11
 
@@ -59,8 +60,12 @@ DropdownMenuSubContent.displayName =
59
60
  const DropdownMenuContent = React.forwardRef<
60
61
  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
61
62
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
62
- >(({ className, sideOffset = 4, ...props }, ref) => (
63
- <DropdownMenuPrimitive.Portal>
63
+ >(({ className, sideOffset = 4, ...props }, ref) => {
64
+ // Portal into the active container (e.g. a drawer) so the menu inherits its
65
+ // stacking context; falls back to `document.body` when none is provided.
66
+ const container = usePortalContainer()
67
+ return (
68
+ <DropdownMenuPrimitive.Portal container={container ?? undefined}>
64
69
  <DropdownMenuPrimitive.Content
65
70
  ref={ref}
66
71
  sideOffset={sideOffset}
@@ -71,7 +76,8 @@ const DropdownMenuContent = React.forwardRef<
71
76
  {...props}
72
77
  />
73
78
  </DropdownMenuPrimitive.Portal>
74
- ))
79
+ )
80
+ })
75
81
  DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76
82
 
77
83
  const DropdownMenuItem = React.forwardRef<
@@ -1,10 +1,14 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from "react"
4
- import { useEffect } from "react"
4
+ import { useEffect, useState } from "react"
5
5
  import { XmarkIcon } from "../icons-v2-generated"
6
6
  import { cn } from "../../utils/cn"
7
7
 
8
+ // Duration of the open/close animation in ms — keep in sync with the
9
+ // `duration-200` utilities applied to the backdrop and panel below.
10
+ const ANIMATION_DURATION = 200
11
+
8
12
  const ModalContext = React.createContext<{ onClose?: () => void }>({})
9
13
 
10
14
  interface ModalProps {
@@ -36,6 +40,18 @@ interface ModalFooterProps {
36
40
 
37
41
  const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
38
42
  ({ isOpen, onClose, children, className }, ref) => {
43
+ // Keep the modal mounted while the exit animation plays.
44
+ const [isMounted, setIsMounted] = useState(isOpen)
45
+
46
+ useEffect(() => {
47
+ if (isOpen) {
48
+ setIsMounted(true)
49
+ return
50
+ }
51
+ const timeout = setTimeout(() => setIsMounted(false), ANIMATION_DURATION)
52
+ return () => clearTimeout(timeout)
53
+ }, [isOpen])
54
+
39
55
  useEffect(() => {
40
56
  const handleKeyDown = (event: KeyboardEvent) => {
41
57
  if (event.key === 'Escape') {
@@ -54,17 +70,26 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
54
70
  }
55
71
  }, [isOpen, onClose])
56
72
 
57
- if (!isOpen) return null
73
+ if (!isMounted) return null
74
+
75
+ const state = isOpen ? "open" : "closed"
58
76
 
59
77
  return (
60
78
  <div className="fixed inset-0 z-[1300] flex items-end md:items-center justify-center">
61
79
  <div
62
- className="absolute inset-0 bg-black/50 backdrop-blur-[2px] md:backdrop-blur-none"
80
+ data-state={state}
81
+ className={cn(
82
+ "absolute inset-0 bg-black/50 backdrop-blur-[2px] md:backdrop-blur-none",
83
+ "duration-200",
84
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0",
85
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0"
86
+ )}
63
87
  onClick={onClose}
64
88
  aria-hidden="true"
65
89
  />
66
90
  <div
67
91
  ref={ref}
92
+ data-state={state}
68
93
  className={cn(
69
94
  "relative z-10 w-full max-w-md flex flex-col",
70
95
  "mx-4 mb-4 md:mb-0",
@@ -72,6 +97,11 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
72
97
  "bg-ods-bg md:bg-ods-card",
73
98
  "border border-ods-border rounded-md shadow-xl",
74
99
  "p-[var(--spacing-system-xl)] gap-[var(--spacing-system-l)]",
100
+ "duration-200",
101
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0",
102
+ "data-[state=open]:slide-in-from-bottom-4 md:data-[state=open]:slide-in-from-bottom-0 md:data-[state=open]:zoom-in-95",
103
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
104
+ "data-[state=closed]:slide-out-to-bottom-4 md:data-[state=closed]:slide-out-to-bottom-0 md:data-[state=closed]:zoom-out-95",
75
105
  className
76
106
  )}
77
107
  role="dialog"
@@ -31,6 +31,10 @@ export interface MoreActionsMenuProps {
31
31
  side?: 'top' | 'right' | 'bottom' | 'left'
32
32
  sideOffset?: number
33
33
  className?: string
34
+ /** Appended to the dropdown content. To render the menu above a high-z
35
+ * surface (drawer, modal), prefer wrapping that surface in a
36
+ * `PortalContainerContext` provider rather than escalating z-index here. */
37
+ contentClassName?: string
34
38
  ariaLabel?: string
35
39
  /** Custom trigger element. When provided, replaces the default ellipsis icon button. */
36
40
  trigger?: React.ReactNode
@@ -38,6 +42,9 @@ export interface MoreActionsMenuProps {
38
42
  open?: boolean
39
43
  /** Called when the open state changes — use together with `open`. */
40
44
  onOpenChange?: (open: boolean) => void
45
+ /** Forwarded to the dropdown content. Call `e.preventDefault()` to stop
46
+ * Radix returning focus (and its focus ring) to the trigger on close. */
47
+ onCloseAutoFocus?: (event: Event) => void
41
48
  }
42
49
 
43
50
  /**
@@ -50,10 +57,12 @@ export function MoreActionsMenu({
50
57
  side = 'bottom',
51
58
  sideOffset = 6,
52
59
  className,
60
+ contentClassName,
53
61
  ariaLabel = 'More actions',
54
62
  trigger,
55
63
  open,
56
- onOpenChange
64
+ onOpenChange,
65
+ onCloseAutoFocus
57
66
  }: MoreActionsMenuProps) {
58
67
  return (
59
68
  <DropdownMenu open={open} onOpenChange={onOpenChange}>
@@ -73,7 +82,11 @@ export function MoreActionsMenu({
73
82
  align={align}
74
83
  side={side}
75
84
  sideOffset={sideOffset}
76
- className="bg-ods-card border border-ods-border p-0 rounded-[4px] min-w-[200px]"
85
+ onCloseAutoFocus={onCloseAutoFocus}
86
+ className={cn(
87
+ 'bg-ods-card border border-ods-border p-0 rounded-[4px] min-w-[200px]',
88
+ contentClassName,
89
+ )}
77
90
  >
78
91
  {items.map((item, idx) => {
79
92
  const itemClassName =
@@ -0,0 +1,28 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ /**
6
+ * The DOM node that Radix overlays (dropdown menus, tooltips) should portal
7
+ * into, instead of the default `document.body`.
8
+ *
9
+ * Portaling to `document.body` lifts content out of every local stacking
10
+ * context, which forces overlays opened *inside* a high-z surface (e.g. the
11
+ * chat drawer) to escalate their own z-index to compete at the document
12
+ * root. Pointing the portal at a node *inside* that surface instead
13
+ * lets the content inherit the surface's stacking context — small, local
14
+ * z-indices then "just work" and no escalation is needed. Radix positions
15
+ * content with `strategy: "fixed"`, so it still escapes ancestor
16
+ * `overflow: hidden` clipping regardless of where it is portaled.
17
+ *
18
+ * Default `null` → Radix falls back to `document.body` (unchanged behaviour
19
+ * everywhere a provider isn't present).
20
+ */
21
+ export const PortalContainerContext = React.createContext<HTMLElement | null>(
22
+ null,
23
+ )
24
+
25
+ /** Read the active portal container (or `null` to use `document.body`). */
26
+ export function usePortalContainer(): HTMLElement | null {
27
+ return React.useContext(PortalContainerContext)
28
+ }
@@ -3,6 +3,7 @@
3
3
  import * as React from "react"
4
4
  import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5
5
  import { cn } from "../../utils/cn"
6
+ import { usePortalContainer } from "./portal-container"
6
7
 
7
8
  const TooltipProvider = TooltipPrimitive.Provider
8
9
 
@@ -13,8 +14,12 @@ const TooltipTrigger = TooltipPrimitive.Trigger
13
14
  const TooltipContent = React.forwardRef<
14
15
  React.ElementRef<typeof TooltipPrimitive.Content>,
15
16
  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
16
- >(({ className, sideOffset = 4, ...props }, ref) => (
17
- <TooltipPrimitive.Portal>
17
+ >(({ className, sideOffset = 4, ...props }, ref) => {
18
+ // Portal into the active container (e.g. a drawer) so the tooltip inherits
19
+ // its stacking context; falls back to `document.body` when none is provided.
20
+ const container = usePortalContainer()
21
+ return (
22
+ <TooltipPrimitive.Portal container={container ?? undefined}>
18
23
  <TooltipPrimitive.Content
19
24
  ref={ref}
20
25
  sideOffset={sideOffset}
@@ -25,7 +30,8 @@ const TooltipContent = React.forwardRef<
25
30
  {...props}
26
31
  />
27
32
  </TooltipPrimitive.Portal>
28
- ))
33
+ )
34
+ })
29
35
  TooltipContent.displayName = TooltipPrimitive.Content.displayName
30
36
 
31
37
  export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,184 @@
1
+ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
2
+ import { useState } from 'react'
3
+ import { fn } from 'storybook/test'
4
+
5
+ import {
6
+ BracketCurlyIcon,
7
+ ChartDonutIcon,
8
+ IdCardIcon,
9
+ MonitorIcon,
10
+ Settings02Icon,
11
+ } from '../components/icons-v2-generated'
12
+ import {
13
+ AppLayout,
14
+ AppLayoutDrawer,
15
+ AppLayoutDrawerBody,
16
+ AppLayoutDrawerContent,
17
+ AppLayoutDrawerDescription,
18
+ AppLayoutDrawerHeader,
19
+ AppLayoutDrawerTitle,
20
+ } from '../components/navigation'
21
+ import { NavigationSidebarConfig } from '../types/navigation'
22
+
23
+ const navigationItems: NavigationSidebarConfig['items'] = [
24
+ { id: 'dashboard', label: 'Dashboard', icon: <ChartDonutIcon size={24} />, path: '/dashboard', isActive: true },
25
+ { id: 'customers', label: 'Customers', icon: <IdCardIcon size={24} />, path: '/customers' },
26
+ { id: 'devices', label: 'Devices', icon: <MonitorIcon size={24} />, path: '/devices' },
27
+ { id: 'scripts', label: 'Scripts', icon: <BracketCurlyIcon size={24} />, path: '/scripts' },
28
+ { id: 'settings', label: 'Settings', icon: <Settings02Icon size={24} />, path: '/settings', section: 'secondary' },
29
+ ]
30
+
31
+ const meta = {
32
+ title: 'Navigation/AppLayoutSidebar',
33
+ component: AppLayout,
34
+ parameters: {
35
+ layout: 'fullscreen',
36
+ docs: {
37
+ description: {
38
+ component: `
39
+ \`AppLayout\` wired so the **Mingo AI** header button toggles a right-side,
40
+ in-layout sidebar (an \`AppLayoutDrawer\`). The sidebar renders inside the
41
+ main content area — the navigation sidebar and header stay visible and
42
+ interactive while it is open.
43
+
44
+ The header button reflects the open state via \`isMingoAIActive\`, and the
45
+ sidebar is persistent: clicks on the header/sidebar/overlay do not close it
46
+ (use the X button or Escape).
47
+ `,
48
+ },
49
+ },
50
+ },
51
+ tags: ['autodocs'],
52
+ } satisfies Meta<typeof AppLayout>
53
+
54
+ export default meta
55
+ type Story = StoryObj<typeof meta>
56
+
57
+ function DashboardChildren() {
58
+ return (
59
+ <div className="space-y-6 p-6">
60
+ <div>
61
+ <h1 className="text-2xl font-semibold text-ods-text-primary">Devices Overview</h1>
62
+ <p className="text-ods-text-secondary mt-1">8,250 Devices in Total</p>
63
+ </div>
64
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
65
+ {[
66
+ { label: 'Active Devices', value: '6,930' },
67
+ { label: 'Active Tickets', value: '136' },
68
+ { label: 'Resolved Tickets', value: '825' },
69
+ ].map((stat) => (
70
+ <div key={stat.label} className="p-4 bg-ods-card rounded-lg border border-ods-border">
71
+ <p className="text-sm text-ods-text-secondary">{stat.label}</p>
72
+ <p className="text-2xl font-semibold text-ods-text-primary mt-1">{stat.value}</p>
73
+ </div>
74
+ ))}
75
+ </div>
76
+ <p className="text-ods-text-secondary">
77
+ Click <span className="font-medium text-ods-text-primary">Mingo AI</span> in the header to open the chat sidebar.
78
+ </p>
79
+ </div>
80
+ )
81
+ }
82
+
83
+ /**
84
+ * Mingo AI header button toggles the right-side chat sidebar.
85
+ */
86
+ export const Default: Story = {
87
+ render: function Render() {
88
+ const [open, setOpen] = useState(false)
89
+ return (
90
+ <AppLayout
91
+ sidebarConfig={{ items: navigationItems, onNavigate: fn(), onToggleMinimized: fn() }}
92
+ headerProps={{
93
+ showNotifications: true,
94
+ showMingoAI: true,
95
+ onMingoAI: () => setOpen((prev) => !prev),
96
+ isMingoAIActive: open,
97
+ showUser: true,
98
+ userName: 'Alex Developer',
99
+ userEmail: 'alex@openframe.dev',
100
+ onProfile: fn(),
101
+ onLogout: fn(),
102
+ }}
103
+ mobileBurgerMenuProps={{
104
+ user: {
105
+ userName: 'Alex Developer',
106
+ userEmail: 'alex@openframe.dev',
107
+ userRole: 'Admin',
108
+ },
109
+ }}
110
+ drawer={
111
+ <AppLayoutDrawer open={open} onOpenChange={setOpen}>
112
+ <AppLayoutDrawerContent side="right">
113
+ <AppLayoutDrawerHeader>
114
+ <AppLayoutDrawerTitle>Mingo AI</AppLayoutDrawerTitle>
115
+ <AppLayoutDrawerDescription>
116
+ Ask Mingo about your devices, tickets, and scripts.
117
+ </AppLayoutDrawerDescription>
118
+ </AppLayoutDrawerHeader>
119
+ <AppLayoutDrawerBody>
120
+ <p className="text-sm text-ods-text-secondary">Chat content goes here.</p>
121
+ </AppLayoutDrawerBody>
122
+ </AppLayoutDrawerContent>
123
+ </AppLayoutDrawer>
124
+ }
125
+ >
126
+ <DashboardChildren />
127
+ </AppLayout>
128
+ )
129
+ },
130
+ }
131
+
132
+ /**
133
+ * Resizable chat sidebar — drag the inside edge to resize. Starts open.
134
+ */
135
+ export const Resizable: Story = {
136
+ render: function Render() {
137
+ const [open, setOpen] = useState(true)
138
+ return (
139
+ <AppLayout
140
+ sidebarConfig={{ items: navigationItems, onNavigate: fn(), onToggleMinimized: fn() }}
141
+ headerProps={{
142
+ showNotifications: true,
143
+ showMingoAI: true,
144
+ onMingoAI: () => setOpen((prev) => !prev),
145
+ isMingoAIActive: open,
146
+ showUser: true,
147
+ userName: 'Alex Developer',
148
+ userEmail: 'alex@openframe.dev',
149
+ onProfile: fn(),
150
+ onLogout: fn(),
151
+ }}
152
+ mobileBurgerMenuProps={{
153
+ user: {
154
+ userName: 'Alex Developer',
155
+ userEmail: 'alex@openframe.dev',
156
+ userRole: 'Admin',
157
+ },
158
+ }}
159
+ drawer={
160
+ <AppLayoutDrawer open={open} onOpenChange={setOpen}>
161
+ <AppLayoutDrawerContent
162
+ side="right"
163
+ resizable
164
+ minSize={360}
165
+ defaultSize={560}
166
+ storageKey="storybook:app-layout-sidebar:mingo"
167
+ >
168
+ <AppLayoutDrawerHeader>
169
+ <AppLayoutDrawerTitle>Mingo AI</AppLayoutDrawerTitle>
170
+ </AppLayoutDrawerHeader>
171
+ <AppLayoutDrawerBody>
172
+ <p className="text-sm text-ods-text-secondary">
173
+ Drag the grip on the left edge to resize the chat sidebar.
174
+ </p>
175
+ </AppLayoutDrawerBody>
176
+ </AppLayoutDrawerContent>
177
+ </AppLayoutDrawer>
178
+ }
179
+ >
180
+ <DashboardChildren />
181
+ </AppLayout>
182
+ )
183
+ },
184
+ }
@@ -8,6 +8,7 @@ import {
8
8
  import { EmbeddableChat } from '../components/chat/embeddable-chat'
9
9
  import type { UseNatsChatAdapterConfig } from '../components/chat/hooks/use-nats-chat-adapter'
10
10
  import type { SlashCommandSummary } from '../components/chat/hooks/use-slash-commands'
11
+ import type { DialogItem } from '../components/chat/types/component.types'
11
12
 
12
13
  // =============================================================================
13
14
  // Stub commands endpoint — module-level constant referenced both by the
@@ -59,6 +60,92 @@ function createMockMingoConfig(): UseNatsChatAdapterConfig {
59
60
  // eslint-disable-next-line no-console
60
61
  console.log('[story] mingo publish', { text, options })
61
62
  },
63
+ // Selecting a dialog loads its history via this callback — without it
64
+ // `hasMessages` stays false and the panel never switches to the
65
+ // conversation view. Return a tiny canned thread so clicks "open" a chat.
66
+ fetchDialogMessages: ({ dialogId }) =>
67
+ Promise.resolve({
68
+ messages: [
69
+ {
70
+ id: `${dialogId}-u1`,
71
+ createdAt: new Date().toISOString(),
72
+ owner: { type: 'CLIENT' },
73
+ messageData: {
74
+ type: 'TEXT',
75
+ text: 'Which devices have not sent any logs in the last 24 hours?',
76
+ },
77
+ },
78
+ {
79
+ id: `${dialogId}-a1`,
80
+ createdAt: new Date().toISOString(),
81
+ owner: { type: 'ASSISTANT' },
82
+ messageData: {
83
+ type: 'TEXT',
84
+ text: 'Found 3 devices that have not reported in over 24 hours. Want me to run a full diagnostic?',
85
+ },
86
+ },
87
+ ],
88
+ nextCursor: null,
89
+ }),
90
+ }
91
+ }
92
+
93
+ // Sample dialog history (Figma node 7532:223950) — supplied via `fetchDialogs`
94
+ // so EmbeddableChat reports `dialogs.length > 0` and renders MingoWelcome's
95
+ // returning-user variation (outline "Start Guide Chat" chip, no promo).
96
+ const SAMPLE_DIALOGS: ReadonlyArray<DialogItem> = [
97
+ { id: 'd1', title: 'PowerShell script for bulk user creation', unreadMessagesCount: 1 },
98
+ { id: 'd2', title: 'Setting up automated backup verification', unreadMessagesCount: 1 },
99
+ { id: 'd3', title: 'Network segmentation best practices for client' },
100
+ { id: 'd4', title: 'Creating GPO for software deployment' },
101
+ { id: 'd5', title: 'WSUS patching strategy optimization' },
102
+ { id: 'd6', title: 'Office 365 license assignment automation' },
103
+ { id: 'd7', title: 'Firewall rule configuration for new application' },
104
+ { id: 'd8', title: 'SQL Server maintenance plan troubleshooting' },
105
+ ]
106
+
107
+ /** Mingo config whose `fetchDialogs` resolves a single page of history (with
108
+ * timestamps so the list splits into Today / Yesterday) plus working
109
+ * rename/archive callbacks — used by the returning-user story. */
110
+ function createMockMingoConfigWithDialogs(): UseNatsChatAdapterConfig {
111
+ return {
112
+ ...createMockMingoConfig(),
113
+ fetchDialogs: () => {
114
+ const now = Date.now()
115
+ const day = 24 * 60 * 60 * 1000
116
+ // First six → Today, the rest → Yesterday.
117
+ const dialogs = SAMPLE_DIALOGS.map((d, i) => ({
118
+ ...d,
119
+ timestamp: new Date(now - (i < 6 ? 0 : day)),
120
+ }))
121
+ return Promise.resolve({ dialogs, nextCursor: null })
122
+ },
123
+ renameDialog: (id, title) => {
124
+ // eslint-disable-next-line no-console
125
+ console.log('[story] mingo rename', { id, title })
126
+ return Promise.resolve()
127
+ },
128
+ archiveDialog: (id) => {
129
+ // eslint-disable-next-line no-console
130
+ console.log('[story] mingo archive', { id })
131
+ return Promise.resolve()
132
+ },
133
+ fetchArchivedDialogs: () => {
134
+ const now = Date.now()
135
+ const day = 24 * 60 * 60 * 1000
136
+ const dialogs: DialogItem[] = [
137
+ { id: 'a1', title: 'Exchange hybrid migration planning', timestamp: new Date(now) },
138
+ { id: 'a2', title: 'VPN split-tunnel configuration review', timestamp: new Date(now) },
139
+ { id: 'a3', title: 'Decommissioning legacy file server', timestamp: new Date(now - day) },
140
+ { id: 'a4', title: 'Intune compliance policy rollout', timestamp: new Date(now - day) },
141
+ ]
142
+ return Promise.resolve({ dialogs, nextCursor: null })
143
+ },
144
+ unarchiveDialog: (id) => {
145
+ // eslint-disable-next-line no-console
146
+ console.log('[story] mingo unarchive', { id })
147
+ return Promise.resolve()
148
+ },
62
149
  }
63
150
  }
64
151
 
@@ -354,3 +441,30 @@ export const BothModes: Story = {
354
441
  ),
355
442
  args: {},
356
443
  }
444
+
445
+ // =============================================================================
446
+ // 4. Returning user — Mingo with existing chats (Figma node 7532:223950)
447
+ // =============================================================================
448
+
449
+ /**
450
+ * Returning-user variation. The mock `fetchDialogs` resolves a page of dialog
451
+ * history, so `dialogs.length > 0` and the empty state switches to its
452
+ * returning-user form: the "New to OpenFrame?" notification is hidden and the
453
+ * "Start Guide Chat" chip drops from the accent yellow to the muted outline
454
+ * style. The dialog list itself renders inline via MingoChatHistory.
455
+ */
456
+ export const ReturningUser: Story = {
457
+ render: (args) => (
458
+ <EmbeddableChat
459
+ {...args}
460
+ modes={{
461
+ guide: {},
462
+ mingo: createMockMingoConfigWithDialogs(),
463
+ }}
464
+ defaultActiveMode="mingo"
465
+ defaultOpen
466
+ showInternalTrigger={false}
467
+ />
468
+ ),
469
+ args: {},
470
+ }