@flamingo-stack/openframe-frontend-core 0.0.284 → 0.0.285-snapshot.20260618135145

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 (229) hide show
  1. package/dist/chunk-3MCHAFHB.js +89 -0
  2. package/dist/chunk-3MCHAFHB.js.map +1 -0
  3. package/dist/{chunk-IMNGGLKL.js → chunk-3ZXUQQL4.js} +2 -2
  4. package/dist/{chunk-BRLGG3EZ.cjs → chunk-4WACBTZU.cjs} +30 -30
  5. package/dist/{chunk-BRLGG3EZ.cjs.map → chunk-4WACBTZU.cjs.map} +1 -1
  6. package/dist/{chunk-P4ARMFW3.js → chunk-5BTZOVDQ.js} +4 -4
  7. package/dist/chunk-5KD3S25X.cjs +312 -0
  8. package/dist/chunk-5KD3S25X.cjs.map +1 -0
  9. package/dist/chunk-6N26CURS.cjs +2417 -0
  10. package/dist/chunk-6N26CURS.cjs.map +1 -0
  11. package/dist/{chunk-XX7P4IL2.js → chunk-7NM7DEUK.js} +4 -143
  12. package/dist/chunk-7NM7DEUK.js.map +1 -0
  13. package/dist/chunk-BBZ7AX5H.cjs +290 -0
  14. package/dist/chunk-BBZ7AX5H.cjs.map +1 -0
  15. package/dist/chunk-C6SCWXDP.js +290 -0
  16. package/dist/chunk-C6SCWXDP.js.map +1 -0
  17. package/dist/{chunk-GVSTRN36.cjs → chunk-D3LEFMOA.cjs} +3 -3
  18. package/dist/{chunk-GVSTRN36.cjs.map → chunk-D3LEFMOA.cjs.map} +1 -1
  19. package/dist/{chunk-3SM6DA24.cjs → chunk-D5YY5U6J.cjs} +19 -15
  20. package/dist/chunk-D5YY5U6J.cjs.map +1 -0
  21. package/dist/chunk-DDAT4RKX.js +2417 -0
  22. package/dist/chunk-DDAT4RKX.js.map +1 -0
  23. package/dist/{chunk-PVXCVPJD.cjs → chunk-EI4WALN2.cjs} +67 -1626
  24. package/dist/chunk-EI4WALN2.cjs.map +1 -0
  25. package/dist/{chunk-ZIODAAFY.cjs → chunk-EL5GCMPU.cjs} +20 -159
  26. package/dist/chunk-EL5GCMPU.cjs.map +1 -0
  27. package/dist/{chunk-RGXDDOQR.js → chunk-FSBDVT6R.js} +655 -2419
  28. package/dist/chunk-FSBDVT6R.js.map +1 -0
  29. package/dist/{chunk-24L4FLRC.cjs → chunk-GDF2R2ER.cjs} +1079 -2843
  30. package/dist/chunk-GDF2R2ER.cjs.map +1 -0
  31. package/dist/chunk-HLQW7MWJ.cjs +1594 -0
  32. package/dist/chunk-HLQW7MWJ.cjs.map +1 -0
  33. package/dist/chunk-I6ZPGKZ2.cjs +151 -0
  34. package/dist/chunk-I6ZPGKZ2.cjs.map +1 -0
  35. package/dist/{chunk-J66U3LPZ.cjs → chunk-KBKZYJRI.cjs} +33 -332
  36. package/dist/chunk-KBKZYJRI.cjs.map +1 -0
  37. package/dist/{chunk-USPATRSC.js → chunk-N6ZM5PYZ.js} +45 -344
  38. package/dist/chunk-N6ZM5PYZ.js.map +1 -0
  39. package/dist/{chunk-RGALV4UX.js → chunk-POKKCWKF.js} +3 -82
  40. package/dist/chunk-POKKCWKF.js.map +1 -0
  41. package/dist/{chunk-XAS7JVHX.cjs → chunk-QCKN37OP.cjs} +45 -41
  42. package/dist/chunk-QCKN37OP.cjs.map +1 -0
  43. package/dist/{chunk-VKDJXB7L.js → chunk-QF2X6PTD.js} +10 -6
  44. package/dist/{chunk-VKDJXB7L.js.map → chunk-QF2X6PTD.js.map} +1 -1
  45. package/dist/{chunk-Z3V2SGQZ.js → chunk-R2KT5GDD.js} +324 -1883
  46. package/dist/chunk-R2KT5GDD.js.map +1 -0
  47. package/dist/{chunk-HX7ZGKQ4.cjs → chunk-RQ6RTBKF.cjs} +16 -12
  48. package/dist/chunk-RQ6RTBKF.cjs.map +1 -0
  49. package/dist/chunk-TFSYSWPS.cjs +89 -0
  50. package/dist/chunk-TFSYSWPS.cjs.map +1 -0
  51. package/dist/chunk-TYZEMPPH.js +312 -0
  52. package/dist/chunk-TYZEMPPH.js.map +1 -0
  53. package/dist/{chunk-33RLTUN2.js → chunk-VCQ3CTYK.js} +12 -8
  54. package/dist/{chunk-33RLTUN2.js.map → chunk-VCQ3CTYK.js.map} +1 -1
  55. package/dist/chunk-VRSXJ5QJ.js +1599 -0
  56. package/dist/chunk-VRSXJ5QJ.js.map +1 -0
  57. package/dist/{chunk-OUI2MBU7.js → chunk-XQL4WDML.js} +11 -7
  58. package/dist/{chunk-OUI2MBU7.js.map → chunk-XQL4WDML.js.map} +1 -1
  59. package/dist/chunk-Y4JNA4W6.cjs +1599 -0
  60. package/dist/chunk-Y4JNA4W6.cjs.map +1 -0
  61. package/dist/{chunk-ZSUNQJRO.cjs → chunk-YETA25JW.cjs} +2 -81
  62. package/dist/chunk-YETA25JW.cjs.map +1 -0
  63. package/dist/chunk-YVB3VDIQ.js +1594 -0
  64. package/dist/chunk-YVB3VDIQ.js.map +1 -0
  65. package/dist/chunk-ZLN6SM2U.js +151 -0
  66. package/dist/chunk-ZLN6SM2U.js.map +1 -0
  67. package/dist/components/chat/chat-attachment-bar.d.ts.map +1 -1
  68. package/dist/components/chat/index.cjs +12 -6
  69. package/dist/components/chat/index.cjs.map +1 -1
  70. package/dist/components/chat/index.js +45 -39
  71. package/dist/components/contact/index.cjs +11 -7
  72. package/dist/components/contact/index.cjs.map +1 -1
  73. package/dist/components/contact/index.js +10 -6
  74. package/dist/components/docs/doc-navigation-context.d.ts +39 -0
  75. package/dist/components/docs/doc-navigation-context.d.ts.map +1 -0
  76. package/dist/components/docs/doc-viewer.d.ts +66 -0
  77. package/dist/components/docs/doc-viewer.d.ts.map +1 -0
  78. package/dist/components/docs/index.cjs +34 -0
  79. package/dist/components/docs/index.cjs.map +1 -0
  80. package/dist/components/docs/index.d.ts +8 -0
  81. package/dist/components/docs/index.d.ts.map +1 -0
  82. package/dist/components/docs/index.js +34 -0
  83. package/dist/components/docs/index.js.map +1 -0
  84. package/dist/components/docs/use-document-tree.d.ts +41 -0
  85. package/dist/components/docs/use-document-tree.d.ts.map +1 -0
  86. package/dist/components/docs/use-scroll-spy.d.ts +16 -0
  87. package/dist/components/docs/use-scroll-spy.d.ts.map +1 -0
  88. package/dist/components/embeds/embed-iframe.d.ts +29 -0
  89. package/dist/components/embeds/embed-iframe.d.ts.map +1 -0
  90. package/dist/components/embeds/figma-embed.d.ts +27 -0
  91. package/dist/components/embeds/figma-embed.d.ts.map +1 -0
  92. package/dist/components/embeds/google-sheets-viewer.d.ts +8 -0
  93. package/dist/components/embeds/google-sheets-viewer.d.ts.map +1 -0
  94. package/dist/components/embeds/index.cjs +37 -0
  95. package/dist/components/embeds/index.cjs.map +1 -0
  96. package/dist/components/embeds/index.d.ts +9 -0
  97. package/dist/components/embeds/index.d.ts.map +1 -0
  98. package/dist/components/embeds/index.js +37 -0
  99. package/dist/components/embeds/index.js.map +1 -0
  100. package/dist/components/embeds/pdf-viewer.d.ts +10 -0
  101. package/dist/components/embeds/pdf-viewer.d.ts.map +1 -0
  102. package/dist/components/faq/index.cjs +12 -8
  103. package/dist/components/faq/index.cjs.map +1 -1
  104. package/dist/components/faq/index.js +11 -7
  105. package/dist/components/features/board/board-column.d.ts +3 -1
  106. package/dist/components/features/board/board-column.d.ts.map +1 -1
  107. package/dist/components/features/board/board-ticket-approval.d.ts +16 -0
  108. package/dist/components/features/board/board-ticket-approval.d.ts.map +1 -0
  109. package/dist/components/features/board/board.d.ts +3 -1
  110. package/dist/components/features/board/board.d.ts.map +1 -1
  111. package/dist/components/features/board/index.d.ts +3 -1
  112. package/dist/components/features/board/index.d.ts.map +1 -1
  113. package/dist/components/features/board/ticket-card.d.ts +3 -1
  114. package/dist/components/features/board/ticket-card.d.ts.map +1 -1
  115. package/dist/components/features/board/types.d.ts +11 -0
  116. package/dist/components/features/board/types.d.ts.map +1 -1
  117. package/dist/components/features/index.cjs +12 -6
  118. package/dist/components/features/index.cjs.map +1 -1
  119. package/dist/components/features/index.js +11 -5
  120. package/dist/components/index.cjs +249 -1120
  121. package/dist/components/index.cjs.map +1 -1
  122. package/dist/components/index.d.ts +2 -0
  123. package/dist/components/index.d.ts.map +1 -1
  124. package/dist/components/index.js +647 -1518
  125. package/dist/components/index.js.map +1 -1
  126. package/dist/components/navigation/index.cjs +15 -6
  127. package/dist/components/navigation/index.cjs.map +1 -1
  128. package/dist/components/navigation/index.d.ts +2 -0
  129. package/dist/components/navigation/index.d.ts.map +1 -1
  130. package/dist/components/navigation/index.js +15 -6
  131. package/dist/components/navigation/multi-level-navigation.d.ts +28 -0
  132. package/dist/components/navigation/multi-level-navigation.d.ts.map +1 -0
  133. package/dist/components/onboarding-guides/index.cjs +34 -29
  134. package/dist/components/onboarding-guides/index.cjs.map +1 -1
  135. package/dist/components/onboarding-guides/index.js +13 -8
  136. package/dist/components/onboarding-guides/index.js.map +1 -1
  137. package/dist/components/related-content/index.cjs +12 -8
  138. package/dist/components/related-content/index.cjs.map +1 -1
  139. package/dist/components/related-content/index.js +11 -7
  140. package/dist/components/tickets/index.cjs +75 -70
  141. package/dist/components/tickets/index.cjs.map +1 -1
  142. package/dist/components/tickets/index.js +16 -11
  143. package/dist/components/tickets/index.js.map +1 -1
  144. package/dist/components/ui/index.cjs +12 -6
  145. package/dist/components/ui/index.cjs.map +1 -1
  146. package/dist/components/ui/index.js +65 -59
  147. package/dist/embed-shims/index.cjs +3 -3
  148. package/dist/embed-shims/index.cjs.map +1 -1
  149. package/dist/embed-shims/index.js +8 -8
  150. package/dist/hooks/index.cjs +6 -3
  151. package/dist/hooks/index.cjs.map +1 -1
  152. package/dist/hooks/index.js +7 -4
  153. package/dist/index.cjs +64 -6
  154. package/dist/index.cjs.map +1 -1
  155. package/dist/index.js +259 -201
  156. package/dist/types/doc-source.cjs +4 -0
  157. package/dist/types/doc-source.cjs.map +1 -0
  158. package/dist/types/doc-source.d.ts +86 -0
  159. package/dist/types/doc-source.d.ts.map +1 -0
  160. package/dist/types/doc-source.js +3 -0
  161. package/dist/types/doc-source.js.map +1 -0
  162. package/dist/types/index.d.ts +1 -0
  163. package/dist/types/index.d.ts.map +1 -1
  164. package/dist/utils/doc-path-utils.d.ts +24 -0
  165. package/dist/utils/doc-path-utils.d.ts.map +1 -0
  166. package/dist/utils/doc-tree-nav.d.ts +35 -0
  167. package/dist/utils/doc-tree-nav.d.ts.map +1 -0
  168. package/dist/utils/embed-url-converters.d.ts +16 -0
  169. package/dist/utils/embed-url-converters.d.ts.map +1 -0
  170. package/dist/utils/format.d.ts +9 -0
  171. package/dist/utils/format.d.ts.map +1 -1
  172. package/dist/utils/index.cjs +297 -0
  173. package/dist/utils/index.cjs.map +1 -1
  174. package/dist/utils/index.d.ts +7 -1
  175. package/dist/utils/index.d.ts.map +1 -1
  176. package/dist/utils/index.js +276 -1
  177. package/dist/utils/index.js.map +1 -1
  178. package/dist/utils/markdown-section-extractor.d.ts +23 -0
  179. package/dist/utils/markdown-section-extractor.d.ts.map +1 -0
  180. package/dist/utils/markdown-to-plain.d.ts +32 -0
  181. package/dist/utils/markdown-to-plain.d.ts.map +1 -0
  182. package/dist/utils/tree-builder.d.ts +30 -0
  183. package/dist/utils/tree-builder.d.ts.map +1 -0
  184. package/package.json +19 -1
  185. package/src/components/chat/chat-attachment-bar.tsx +6 -11
  186. package/src/components/docs/doc-navigation-context.tsx +123 -0
  187. package/src/components/docs/doc-viewer.tsx +345 -0
  188. package/src/components/docs/index.ts +12 -0
  189. package/src/components/docs/use-document-tree.ts +427 -0
  190. package/src/components/docs/use-scroll-spy.ts +93 -0
  191. package/src/components/embeds/embed-iframe.tsx +103 -0
  192. package/src/components/embeds/figma-embed.tsx +147 -0
  193. package/src/components/embeds/google-sheets-viewer.tsx +54 -0
  194. package/src/components/embeds/index.ts +11 -0
  195. package/src/components/embeds/pdf-viewer.tsx +64 -0
  196. package/src/components/features/board/board-column.tsx +11 -1
  197. package/src/components/features/board/board-ticket-approval.tsx +76 -0
  198. package/src/components/features/board/board.tsx +6 -0
  199. package/src/components/features/board/index.ts +3 -0
  200. package/src/components/features/board/ticket-card.tsx +12 -0
  201. package/src/components/features/board/types.ts +12 -0
  202. package/src/components/index.ts +2 -0
  203. package/src/components/navigation/index.ts +5 -0
  204. package/src/components/navigation/multi-level-navigation.tsx +358 -0
  205. package/src/types/doc-source.ts +88 -0
  206. package/src/types/index.ts +4 -0
  207. package/src/utils/doc-path-utils.ts +47 -0
  208. package/src/utils/doc-tree-nav.ts +111 -0
  209. package/src/utils/embed-url-converters.ts +77 -0
  210. package/src/utils/format.ts +16 -0
  211. package/src/utils/index.ts +11 -0
  212. package/src/utils/markdown-section-extractor.ts +106 -0
  213. package/src/utils/markdown-to-plain.ts +69 -0
  214. package/src/utils/tree-builder.ts +114 -0
  215. package/dist/chunk-24L4FLRC.cjs.map +0 -1
  216. package/dist/chunk-3SM6DA24.cjs.map +0 -1
  217. package/dist/chunk-HX7ZGKQ4.cjs.map +0 -1
  218. package/dist/chunk-J66U3LPZ.cjs.map +0 -1
  219. package/dist/chunk-PVXCVPJD.cjs.map +0 -1
  220. package/dist/chunk-RGALV4UX.js.map +0 -1
  221. package/dist/chunk-RGXDDOQR.js.map +0 -1
  222. package/dist/chunk-USPATRSC.js.map +0 -1
  223. package/dist/chunk-XAS7JVHX.cjs.map +0 -1
  224. package/dist/chunk-XX7P4IL2.js.map +0 -1
  225. package/dist/chunk-Z3V2SGQZ.js.map +0 -1
  226. package/dist/chunk-ZIODAAFY.cjs.map +0 -1
  227. package/dist/chunk-ZSUNQJRO.cjs.map +0 -1
  228. /package/dist/{chunk-IMNGGLKL.js.map → chunk-3ZXUQQL4.js.map} +0 -0
  229. /package/dist/{chunk-P4ARMFW3.js.map → chunk-5BTZOVDQ.js.map} +0 -0
@@ -0,0 +1,147 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { Button, ToggleGroup, ToggleGroupItem } from '../ui'
5
+ import { FigmaIcon } from '../icons-v2-generated'
6
+ import { ExternalLink, Play, LayoutGrid } from 'lucide-react'
7
+ import { toFigmaEmbedUrl, toFigmaOriginalUrl, isFigmaSlidesUrl } from '../../utils/embed-url-converters'
8
+ import { EmbedIframe } from './embed-iframe'
9
+
10
+ export interface FigmaEmbedProps {
11
+ /** Any Figma URL (design/file/proto/board/slides/deck) or an already-resolved embed URL. */
12
+ url: string
13
+ /** Heading shown above the embed. Defaults to "Figma Design". */
14
+ title?: string
15
+ /**
16
+ * iframe height (CSS value). The data-room document viewer omits it (full
17
+ * height, `calc(100vh - 250px)`); inline markdown passes e.g. `"70vh"` so the
18
+ * embed sits naturally inside article content.
19
+ */
20
+ height?: string
21
+ /** iframe loading strategy. Defaults to `"lazy"`; the data-room viewer passes `"eager"`. */
22
+ loading?: 'eager' | 'lazy'
23
+ }
24
+
25
+ type SlidesView = 'present' | 'browse'
26
+
27
+ /**
28
+ * Two-state present/browse toggle for Figma Slides. `present` (default) uses
29
+ * Figma's deck viewer (full-bleed slide + `‹ n/N ›` nav bar + keyboard nav);
30
+ * `browse` uses the thumbnail-rail + zoom viewer.
31
+ */
32
+ function SlidesViewToggle({
33
+ view,
34
+ onChange,
35
+ }: {
36
+ view: SlidesView
37
+ onChange: (v: SlidesView) => void
38
+ }) {
39
+ const options: { key: SlidesView; label: string; Icon: typeof Play }[] = [
40
+ { key: 'present', label: 'Present', Icon: Play },
41
+ { key: 'browse', label: 'Browse', Icon: LayoutGrid },
42
+ ]
43
+ return (
44
+ <ToggleGroup
45
+ type="single"
46
+ value={view}
47
+ onValueChange={(v: string) => {
48
+ if (v && v !== view) onChange(v as SlidesView)
49
+ }}
50
+ aria-label="Figma slides view mode"
51
+ className="flex shrink-0 items-center gap-0.5 rounded-lg border border-ods-border bg-ods-card p-0.5"
52
+ >
53
+ {options.map(({ key, label, Icon }) => {
54
+ const active = view === key
55
+ return (
56
+ <ToggleGroupItem
57
+ key={key}
58
+ value={key}
59
+ aria-label={label}
60
+ className={`flex items-center gap-1.5 rounded-md px-2.5 py-1 text-sm font-medium transition-colors ${
61
+ active
62
+ ? 'bg-ods-accent text-ods-text-on-accent'
63
+ : 'text-ods-text-secondary hover:text-ods-text-primary hover:bg-ods-bg-hover'
64
+ }`}
65
+ >
66
+ <Icon className="h-4 w-4 shrink-0" />
67
+ {label}
68
+ </ToggleGroupItem>
69
+ )
70
+ })}
71
+ </ToggleGroup>
72
+ )
73
+ }
74
+
75
+ /**
76
+ * Single source of truth for every Figma surface — the data-room document viewer
77
+ * and in-article markdown both render this. A header (icon + title + "Open in
78
+ * Figma") over an interactive Figma iframe, built from the canonical
79
+ * `toFigmaEmbedUrl` / `toFigmaOriginalUrl` converters + the shared `<EmbedIframe>`.
80
+ * Only height/loading differ per surface.
81
+ *
82
+ * For Slides decks, a present/browse toggle (default = present) lets viewers flip
83
+ * slides with Figma's native nav bar + keyboard, or switch to the thumbnail-rail
84
+ * browse view.
85
+ */
86
+ export function FigmaEmbed({ url, title, height, loading = 'lazy' }: FigmaEmbedProps) {
87
+ const [view, setView] = useState<SlidesView>('present')
88
+ const isSlides = url ? isFigmaSlidesUrl(url) : false
89
+ const embedSrc = url ? toFigmaEmbedUrl(url, { slidesView: view }) : null
90
+ const originalUrl = (() => {
91
+ if (!url) return null
92
+ try {
93
+ const parsed = new URL(toFigmaOriginalUrl(url))
94
+ const host = parsed.hostname.toLowerCase()
95
+ const okHost = host === 'figma.com' || host.endsWith('.figma.com')
96
+ const okProtocol = parsed.protocol === 'https:' || parsed.protocol === 'http:'
97
+ return okHost && okProtocol ? parsed.toString() : null
98
+ } catch {
99
+ return null
100
+ }
101
+ })()
102
+ const heading = title || 'Figma Design'
103
+
104
+ return (
105
+ <div className="my-6 space-y-3">
106
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
107
+ <div className="flex items-center gap-2 min-w-0">
108
+ <FigmaIcon className="w-5 h-5 shrink-0" />
109
+ <span className="font-sans text-base font-semibold text-ods-text-primary truncate">
110
+ {heading}
111
+ </span>
112
+ </div>
113
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
114
+ {isSlides && embedSrc && <SlidesViewToggle view={view} onChange={setView} />}
115
+ {originalUrl && (
116
+ <Button
117
+ variant="outline"
118
+ size="small-legacy"
119
+ href={originalUrl}
120
+ openInNewTab
121
+ leftIcon={<FigmaIcon className="w-4 h-4" />}
122
+ rightIcon={<ExternalLink className="w-4 h-4" />}
123
+ className="w-full sm:w-auto"
124
+ >
125
+ Open in Figma
126
+ </Button>
127
+ )}
128
+ </div>
129
+ </div>
130
+ {embedSrc ? (
131
+ <EmbedIframe
132
+ src={embedSrc}
133
+ title={heading}
134
+ allow="clipboard-write; clipboard-read; fullscreen"
135
+ loading={loading}
136
+ height={height}
137
+ allowFullScreen
138
+ />
139
+ ) : (
140
+ <div className="flex flex-col items-center justify-center py-16 text-center">
141
+ <FigmaIcon className="w-16 h-16 text-ods-text-secondary mb-4" />
142
+ <p className="text-ods-text-secondary">Figma URL not configured</p>
143
+ </div>
144
+ )}
145
+ </div>
146
+ )
147
+ }
@@ -0,0 +1,54 @@
1
+ "use client"
2
+
3
+ import React from 'react'
4
+ import { Button } from '../ui'
5
+ import { ExternalLink } from 'lucide-react'
6
+ import { GoogleSheetsIcon } from '../icons-v2-generated'
7
+ import { EmbedIframe } from './embed-iframe'
8
+ import { toGoogleSheetsEmbedUrl, toGoogleSheetsOriginalUrl } from '../../utils/embed-url-converters'
9
+
10
+ export interface GoogleSheetsViewerProps {
11
+ externalUrl: string
12
+ fileName?: string
13
+ height?: string
14
+ }
15
+
16
+ export function GoogleSheetsViewer({ externalUrl, fileName, height }: GoogleSheetsViewerProps) {
17
+ const displayName = fileName || 'Google Sheet'
18
+
19
+ if (!externalUrl) {
20
+ return (
21
+ <div className="flex flex-col items-center justify-center py-16 text-center">
22
+ <GoogleSheetsIcon className="w-16 h-16 text-ods-text-secondary mb-4" />
23
+ <p className="text-ods-text-secondary">Google Sheet URL not configured</p>
24
+ </div>
25
+ )
26
+ }
27
+
28
+ return (
29
+ <div className="space-y-4">
30
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
31
+ <div className="flex items-center gap-2 min-w-0">
32
+ <GoogleSheetsIcon className="w-5 h-5 shrink-0" />
33
+ <h2 className="text-xl font-semibold text-ods-text-primary truncate">{displayName}</h2>
34
+ </div>
35
+ <Button
36
+ variant="outline"
37
+ size="small-legacy"
38
+ href={toGoogleSheetsOriginalUrl(externalUrl)}
39
+ openInNewTab
40
+ leftIcon={<GoogleSheetsIcon className="w-4 h-4" />}
41
+ rightIcon={<ExternalLink className="w-4 h-4" />}
42
+ className="w-full sm:w-auto"
43
+ >
44
+ Open in Google Sheets
45
+ </Button>
46
+ </div>
47
+ <EmbedIframe
48
+ src={toGoogleSheetsEmbedUrl(externalUrl)}
49
+ title={displayName}
50
+ height={height}
51
+ />
52
+ </div>
53
+ )
54
+ }
@@ -0,0 +1,11 @@
1
+ export { EmbedIframe } from './embed-iframe'
2
+ export type { EmbedIframeProps } from './embed-iframe'
3
+
4
+ export { PdfViewer } from './pdf-viewer'
5
+ export type { PdfViewerProps } from './pdf-viewer'
6
+
7
+ export { GoogleSheetsViewer } from './google-sheets-viewer'
8
+ export type { GoogleSheetsViewerProps } from './google-sheets-viewer'
9
+
10
+ export { FigmaEmbed } from './figma-embed'
11
+ export type { FigmaEmbedProps } from './figma-embed'
@@ -0,0 +1,64 @@
1
+ "use client"
2
+
3
+ import React from 'react'
4
+ import { Button } from '../ui'
5
+ import { Download, Eye } from 'lucide-react'
6
+ import { AdobePdfIcon } from '../icons-v2-generated'
7
+ import { EmbedIframe } from './embed-iframe'
8
+
9
+ export interface PdfViewerProps {
10
+ src: string
11
+ fileName?: string
12
+ onPreview?: () => void
13
+ onDownload?: () => void
14
+ height?: string
15
+ }
16
+
17
+ export function PdfViewer({ src, fileName, onPreview, onDownload, height }: PdfViewerProps) {
18
+ const displayName = fileName || 'PDF Document'
19
+
20
+ if (!src) {
21
+ return (
22
+ <div className="flex flex-col items-center justify-center py-16 text-center">
23
+ <AdobePdfIcon className="w-16 h-16 text-ods-text-secondary mb-4" />
24
+ <p className="text-ods-text-secondary">PDF file not available</p>
25
+ </div>
26
+ )
27
+ }
28
+
29
+ return (
30
+ <div className="space-y-4">
31
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
32
+ <div className="flex items-center gap-2 min-w-0">
33
+ <AdobePdfIcon className="w-5 h-5 shrink-0" />
34
+ <h2 className="text-xl font-semibold text-ods-text-primary truncate">{displayName}</h2>
35
+ </div>
36
+ <div className="flex items-center gap-2 w-full sm:w-auto">
37
+ <Button
38
+ variant="outline"
39
+ size="small-legacy"
40
+ href={onPreview ? undefined : src}
41
+ openInNewTab={!onPreview}
42
+ onClick={onPreview}
43
+ leftIcon={<Eye className="w-4 h-4" />}
44
+ className="flex-1 sm:flex-initial"
45
+ >
46
+ Preview
47
+ </Button>
48
+ <Button
49
+ variant="outline"
50
+ size="small-legacy"
51
+ href={onDownload ? undefined : src}
52
+ openInNewTab={!onDownload}
53
+ onClick={onDownload}
54
+ leftIcon={<Download className="w-4 h-4" />}
55
+ className="flex-1 sm:flex-initial"
56
+ >
57
+ Download
58
+ </Button>
59
+ </div>
60
+ </div>
61
+ <EmbedIframe src={src} title={displayName} height={height} />
62
+ </div>
63
+ )
64
+ }
@@ -19,6 +19,8 @@ export interface BoardColumnProps {
19
19
  onArchive?: (columnId: string) => void
20
20
  getTicketHref?: (ticketId: string) => string
21
21
  renderAssignSlot?: (ticket: BoardTicket) => React.ReactNode
22
+ onApprove?: (ticketId: string, requestId?: string) => void | Promise<void>
23
+ onReject?: (ticketId: string, requestId?: string) => void | Promise<void>
22
24
  onLoadMore?: (columnId: string) => void
23
25
  loadMoreRootMargin?: string
24
26
  joinLeft?: boolean
@@ -33,6 +35,8 @@ export function BoardColumn({
33
35
  onArchive,
34
36
  getTicketHref,
35
37
  renderAssignSlot,
38
+ onApprove,
39
+ onReject,
36
40
  onLoadMore,
37
41
  loadMoreRootMargin = '200px 0px',
38
42
  joinLeft = false,
@@ -64,6 +68,8 @@ export function BoardColumn({
64
68
  column={column}
65
69
  getTicketHref={getTicketHref}
66
70
  renderAssignSlot={renderAssignSlot}
71
+ onApprove={onApprove}
72
+ onReject={onReject}
67
73
  onLoadMore={onLoadMore}
68
74
  loadMoreRootMargin={loadMoreRootMargin}
69
75
  />
@@ -77,11 +83,13 @@ interface ColumnBodyProps {
77
83
  column: BoardColumnDef
78
84
  getTicketHref?: (ticketId: string) => string
79
85
  renderAssignSlot?: (ticket: BoardTicket) => React.ReactNode
86
+ onApprove?: (ticketId: string, requestId?: string) => void | Promise<void>
87
+ onReject?: (ticketId: string, requestId?: string) => void | Promise<void>
80
88
  onLoadMore?: (columnId: string) => void
81
89
  loadMoreRootMargin: string
82
90
  }
83
91
 
84
- function ColumnBody({ column, getTicketHref, renderAssignSlot, onLoadMore, loadMoreRootMargin }: ColumnBodyProps) {
92
+ function ColumnBody({ column, getTicketHref, renderAssignSlot, onApprove, onReject, onLoadMore, loadMoreRootMargin }: ColumnBodyProps) {
85
93
  const ticketIds = React.useMemo(() => column.tickets.map(t => t.id), [column.tickets])
86
94
 
87
95
  const droppableData = React.useMemo(
@@ -150,6 +158,8 @@ function ColumnBody({ column, getTicketHref, renderAssignSlot, onLoadMore, loadM
150
158
  columnColor={column.color}
151
159
  href={getTicketHref?.(t.id)}
152
160
  renderAssignSlot={renderAssignSlot}
161
+ onApprove={onApprove}
162
+ onReject={onReject}
153
163
  dragDisabled={column.dragDisabled}
154
164
  />
155
165
  ))
@@ -0,0 +1,76 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '../../../utils/cn'
5
+ import { ApprovalBatchMessage } from '../../chat/approval-batch-message'
6
+ import { useCollapsible } from '../../chat/hooks/use-collapsible'
7
+ import { Chevron02DownIcon, Chevron02UpIcon, DotsLoaderIcon, ShieldCheckIcon } from '../../icons-v2-generated'
8
+ import type { BoardTicketPendingApproval } from './types'
9
+
10
+ export interface BoardTicketApprovalProps {
11
+ pendingApproval: BoardTicketPendingApproval
12
+ onApprove?: (requestId?: string) => void | Promise<void>
13
+ onReject?: (requestId?: string) => void | Promise<void>
14
+ }
15
+
16
+ /**
17
+ * Inline, collapsed-by-default approval section for a board ticket card. Expands to
18
+ * the shared {@link ApprovalBatchMessage} so the request can be approved/rejected
19
+ * without leaving the board. The header switches between the client variant (grey,
20
+ * "Pending client approval") and the technician/admin variant (yellow shield,
21
+ * "Technician approval required") on `approvalType`.
22
+ */
23
+ export function BoardTicketApproval({ pendingApproval, onApprove, onReject }: BoardTicketApprovalProps) {
24
+ const [expanded, setExpanded] = React.useState(false)
25
+ const { innerRef, containerStyle } = useCollapsible({ expanded })
26
+ const isAdmin = pendingApproval.approvalType === 'ADMIN'
27
+
28
+ return (
29
+ <div className="pointer-events-auto flex flex-col">
30
+ <button
31
+ type="button"
32
+ onClick={() => setExpanded(v => !v)}
33
+ aria-expanded={expanded}
34
+ className="flex w-full cursor-pointer items-center gap-[var(--spacing-system-xxs)] text-h6"
35
+ >
36
+ <span
37
+ className={cn(
38
+ 'flex min-w-0 flex-1 items-center gap-[var(--spacing-system-xxs)]',
39
+ isAdmin ? 'text-ods-open-yellow' : 'text-ods-text-secondary',
40
+ )}
41
+ >
42
+ {isAdmin ? (
43
+ <ShieldCheckIcon className="size-4 shrink-0" />
44
+ ) : (
45
+ <DotsLoaderIcon size={16} className="shrink-0" />
46
+ )}
47
+ <span className="flex-1 truncate text-left">
48
+ {isAdmin ? 'Technician approval required' : 'Pending client approval'}
49
+ </span>
50
+ </span>
51
+ {expanded ? (
52
+ <Chevron02UpIcon className="size-4 shrink-0 text-ods-text-secondary" />
53
+ ) : (
54
+ <Chevron02DownIcon className="size-4 shrink-0 text-ods-text-secondary" />
55
+ )}
56
+ </button>
57
+
58
+ <div style={containerStyle}>
59
+ <div ref={innerRef} className="pt-[var(--spacing-system-xs)]">
60
+ <ApprovalBatchMessage
61
+ className="mb-0"
62
+ data={{
63
+ approvalRequestId: pendingApproval.id,
64
+ approvalType: pendingApproval.approvalType ?? 'CLIENT',
65
+ toolCalls: pendingApproval.toolCalls ?? [],
66
+ }}
67
+ status="pending"
68
+ showExecutionStatus={false}
69
+ onApprove={onApprove}
70
+ onReject={onReject}
71
+ />
72
+ </div>
73
+ </div>
74
+ </div>
75
+ )
76
+ }
@@ -32,6 +32,8 @@ export interface BoardProps {
32
32
  onArchiveColumn?: (columnId: string) => void
33
33
  getTicketHref?: (ticketId: string) => string
34
34
  renderAssignSlot?: (ticket: BoardTicket) => React.ReactNode
35
+ onApprove?: (ticketId: string, requestId?: string) => void | Promise<void>
36
+ onReject?: (ticketId: string, requestId?: string) => void | Promise<void>
35
37
  collapseStorageKey?: string
36
38
  loadMoreRootMargin?: string
37
39
  className?: string
@@ -45,6 +47,8 @@ export function Board({
45
47
  onArchiveColumn,
46
48
  getTicketHref,
47
49
  renderAssignSlot,
50
+ onApprove,
51
+ onReject,
48
52
  collapseStorageKey,
49
53
  loadMoreRootMargin,
50
54
  className,
@@ -271,6 +275,8 @@ export function Board({
271
275
  onArchive={onArchiveColumn}
272
276
  getTicketHref={getTicketHref}
273
277
  renderAssignSlot={renderAssignSlot}
278
+ onApprove={onApprove}
279
+ onReject={onReject}
274
280
  onLoadMore={onLoadMore}
275
281
  loadMoreRootMargin={loadMoreRootMargin}
276
282
  joinLeft={joinLeft}
@@ -8,6 +8,8 @@ export { BoardColumnHeader } from './board-column-header'
8
8
  export type { BoardColumnHeaderProps } from './board-column-header'
9
9
  export { TicketCard } from './ticket-card'
10
10
  export type { TicketCardProps } from './ticket-card'
11
+ export { BoardTicketApproval } from './board-ticket-approval'
12
+ export type { BoardTicketApprovalProps } from './board-ticket-approval'
11
13
  export { TicketCardSkeleton } from './ticket-card-skeleton'
12
14
  export type { TicketCardSkeletonProps } from './ticket-card-skeleton'
13
15
  export { useBoardCollapse } from './use-board-collapse'
@@ -23,4 +25,5 @@ export type {
23
25
  BoardPriority,
24
26
  BoardTicket,
25
27
  BoardTicketAssignee,
28
+ BoardTicketPendingApproval,
26
29
  } from './types'
@@ -11,6 +11,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../
11
11
  import { cn } from '../../../utils/cn'
12
12
  import { getReadableTextColor } from '../../../utils/ods-color-utils'
13
13
  import { formatTicketRelativeTime, formatTicketFullTimestamp } from '../../../utils/date-utils'
14
+ import { BoardTicketApproval } from './board-ticket-approval'
14
15
  import type { BoardPriority, BoardTicket } from './types'
15
16
 
16
17
  const PRIORITY_COLOR_CLASS: Record<BoardPriority, string> = {
@@ -31,6 +32,8 @@ export interface TicketCardProps {
31
32
  isOverlay?: boolean
32
33
  dragDisabled?: boolean
33
34
  renderAssignSlot?: (ticket: BoardTicket) => React.ReactNode
35
+ onApprove?: (ticketId: string, requestId?: string) => void | Promise<void>
36
+ onReject?: (ticketId: string, requestId?: string) => void | Promise<void>
34
37
  }
35
38
 
36
39
  export function TicketCard({
@@ -41,6 +44,8 @@ export function TicketCard({
41
44
  isOverlay = false,
42
45
  dragDisabled,
43
46
  renderAssignSlot,
47
+ onApprove,
48
+ onReject,
44
49
  }: TicketCardProps) {
45
50
  const sortableData = React.useMemo(
46
51
  () => ({ columnId, type: 'ticket' as const }),
@@ -143,6 +148,13 @@ export function TicketCard({
143
148
  style={{ backgroundColor: columnColor, color: newMessageTextColor }}
144
149
  />
145
150
  )}
151
+ {ticket.pendingApproval && (
152
+ <BoardTicketApproval
153
+ pendingApproval={ticket.pendingApproval}
154
+ onApprove={onApprove ? requestId => onApprove(ticket.id, requestId) : undefined}
155
+ onReject={onReject ? requestId => onReject(ticket.id, requestId) : undefined}
156
+ />
157
+ )}
146
158
  </>
147
159
  )
148
160
 
@@ -1,7 +1,18 @@
1
+ import type { PendingToolCallData } from '../../chat/types'
1
2
  import type { TicketStatus } from '../../ui/ticket-status-tag'
2
3
 
3
4
  export type BoardPriority = 'low' | 'medium' | 'high' | 'urgent'
4
5
 
6
+ /** Latest pending tool-approval request attached to a board ticket (from `Ticket.pendingApproval`). */
7
+ export interface BoardTicketPendingApproval {
8
+ id: string
9
+ approvalType?: string
10
+ command?: string
11
+ explanation?: string
12
+ createdAt?: string
13
+ toolCalls?: PendingToolCallData[]
14
+ }
15
+
5
16
  export interface BoardTicketAssignee {
6
17
  id: string
7
18
  initials?: string
@@ -22,6 +33,7 @@ export interface BoardTicket {
22
33
  tags?: string[]
23
34
  createdAt?: string
24
35
  hasNewMessage?: boolean
36
+ pendingApproval?: BoardTicketPendingApproval
25
37
  }
26
38
 
27
39
  export interface BoardColumnDef {
@@ -2,6 +2,8 @@
2
2
 
3
3
  // Components exports
4
4
  export * from './chat'
5
+ export * from './docs'
6
+ export * from './embeds'
5
7
  export * from './features'
6
8
  export * from './icons'
7
9
  export * from './navigation'
@@ -56,6 +56,11 @@ export type { HeaderGlobalSearchProps } from './header-global-search'
56
56
  export { HeaderOrganizationFilter } from './header-organization-filter'
57
57
  export type { HeaderOrganizationFilterOrganization, HeaderOrganizationFilterProps } from './header-organization-filter'
58
58
 
59
+ // Multi-level navigation — sidebar + mobile dropdown for the doc-viewer
60
+ // (DocViewer / DocSourceViewer) and any other tree-shaped navigation surface.
61
+ export { MultiLevelNavigation, MobileNavigationDropdown } from './multi-level-navigation'
62
+ export type { NavigationNode } from './multi-level-navigation'
63
+
59
64
  // Re-export types from navigation types
60
65
  export type {
61
66
  MobileNavConfig, NavigationItem, NavigationSidebarConfig,