@flamingo-stack/openframe-frontend-core 0.0.568 → 0.0.569-snapshot.20260827034838

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 (176) hide show
  1. package/dist/{chunk-2LKVYI7H.js → chunk-3H6NH2AD.js} +6 -6
  2. package/dist/{chunk-KZ3PIUCH.cjs → chunk-3YO2ZIZL.cjs} +14 -14
  3. package/dist/{chunk-KZ3PIUCH.cjs.map → chunk-3YO2ZIZL.cjs.map} +1 -1
  4. package/dist/{chunk-L22CSZZU.cjs → chunk-4BJ4FQPG.cjs} +18 -18
  5. package/dist/{chunk-L22CSZZU.cjs.map → chunk-4BJ4FQPG.cjs.map} +1 -1
  6. package/dist/{chunk-EYXZF46V.js → chunk-5NVVVIHJ.js} +1038 -922
  7. package/dist/chunk-5NVVVIHJ.js.map +1 -0
  8. package/dist/{chunk-P7QGTNEW.cjs → chunk-6WXL6CIL.cjs} +101 -101
  9. package/dist/{chunk-P7QGTNEW.cjs.map → chunk-6WXL6CIL.cjs.map} +1 -1
  10. package/dist/{chunk-CEAAOVCV.cjs → chunk-AJPGQCZ2.cjs} +1 -1
  11. package/dist/chunk-AJPGQCZ2.cjs.map +1 -0
  12. package/dist/{chunk-NK3M6F5A.cjs → chunk-AQ6MIFNX.cjs} +148 -13
  13. package/dist/chunk-AQ6MIFNX.cjs.map +1 -0
  14. package/dist/{chunk-6PWUUWCO.cjs → chunk-CSCAOET4.cjs} +42 -42
  15. package/dist/{chunk-6PWUUWCO.cjs.map → chunk-CSCAOET4.cjs.map} +1 -1
  16. package/dist/{chunk-6I76UK2Z.cjs → chunk-CYY2PEYQ.cjs} +3 -3
  17. package/dist/{chunk-6I76UK2Z.cjs.map → chunk-CYY2PEYQ.cjs.map} +1 -1
  18. package/dist/{chunk-Y2BYHTEN.cjs → chunk-D463PBIY.cjs} +23 -23
  19. package/dist/{chunk-Y2BYHTEN.cjs.map → chunk-D463PBIY.cjs.map} +1 -1
  20. package/dist/{chunk-NKEOD2AD.cjs → chunk-DP2L7US5.cjs} +3 -3
  21. package/dist/{chunk-NKEOD2AD.cjs.map → chunk-DP2L7US5.cjs.map} +1 -1
  22. package/dist/{chunk-KZS2DFC2.cjs → chunk-GDPPFPLD.cjs} +42 -42
  23. package/dist/{chunk-KZS2DFC2.cjs.map → chunk-GDPPFPLD.cjs.map} +1 -1
  24. package/dist/{chunk-LNUXUOPN.cjs → chunk-HJCXSI7R.cjs} +4 -4
  25. package/dist/{chunk-LNUXUOPN.cjs.map → chunk-HJCXSI7R.cjs.map} +1 -1
  26. package/dist/{chunk-RBYMJF2V.cjs → chunk-I7CFA5DO.cjs} +35 -35
  27. package/dist/{chunk-RBYMJF2V.cjs.map → chunk-I7CFA5DO.cjs.map} +1 -1
  28. package/dist/{chunk-LXWQLUBR.cjs → chunk-IJA26YDA.cjs} +9 -7
  29. package/dist/chunk-IJA26YDA.cjs.map +1 -0
  30. package/dist/{chunk-O3JCKEVG.js → chunk-KYDVOX5J.js} +2 -2
  31. package/dist/{chunk-UWY722RH.js → chunk-MR6RRXSD.js} +1 -1
  32. package/dist/chunk-MR6RRXSD.js.map +1 -0
  33. package/dist/{chunk-HB3IG2MC.js → chunk-MT7IDGCM.js} +3 -3
  34. package/dist/{chunk-G2PAUMV6.js → chunk-MTORQ4LY.js} +4 -4
  35. package/dist/{chunk-HKG7BZON.cjs → chunk-PS2TV7WG.cjs} +14 -14
  36. package/dist/{chunk-HKG7BZON.cjs.map → chunk-PS2TV7WG.cjs.map} +1 -1
  37. package/dist/{chunk-QVQLNKFS.js → chunk-PTDRJCPW.js} +2 -2
  38. package/dist/{chunk-DZ2Z7HRF.js → chunk-QIU5B2ZD.js} +3 -3
  39. package/dist/{chunk-6RSKK4E7.cjs → chunk-RVCCJB6M.cjs} +1278 -1162
  40. package/dist/chunk-RVCCJB6M.cjs.map +1 -0
  41. package/dist/{chunk-T7AE7CKV.cjs → chunk-T4MB5TX6.cjs} +7 -7
  42. package/dist/{chunk-T7AE7CKV.cjs.map → chunk-T4MB5TX6.cjs.map} +1 -1
  43. package/dist/{chunk-ZARYIFCI.js → chunk-TQER4FVN.js} +5 -3
  44. package/dist/{chunk-ZARYIFCI.js.map → chunk-TQER4FVN.js.map} +1 -1
  45. package/dist/{chunk-BZ7B255K.js → chunk-U6NPZMNT.js} +9 -9
  46. package/dist/{chunk-MQQM4NCN.js → chunk-VZQKENQU.js} +3 -3
  47. package/dist/{chunk-VEXTUGUA.js → chunk-XWMEHLDK.js} +7 -7
  48. package/dist/{chunk-ZNEVGXMU.js → chunk-YFLKONLB.js} +139 -4
  49. package/dist/chunk-YFLKONLB.js.map +1 -0
  50. package/dist/{chunk-ZXASQL3E.js → chunk-YYESWAAQ.js} +4 -4
  51. package/dist/{chunk-6TDWCBQM.js → chunk-Z3MSD6ZE.js} +7 -7
  52. package/dist/{chunk-DHTT77AG.cjs → chunk-Z7DXVKVT.cjs} +78 -78
  53. package/dist/{chunk-DHTT77AG.cjs.map → chunk-Z7DXVKVT.cjs.map} +1 -1
  54. package/dist/{chunk-KL6ZDLDF.js → chunk-ZAYZ7JHF.js} +2 -2
  55. package/dist/{chunk-ZIZJDXUI.js → chunk-ZTGWMXJK.js} +2 -2
  56. package/dist/components/case-studies/index.cjs +10 -10
  57. package/dist/components/case-studies/index.js +4 -4
  58. package/dist/components/chat/index.cjs +4 -17
  59. package/dist/components/chat/index.cjs.map +1 -1
  60. package/dist/components/chat/index.js +3 -16
  61. package/dist/components/chat/utils/index.d.ts +0 -2
  62. package/dist/components/chat/utils/index.d.ts.map +1 -1
  63. package/dist/components/contact/index.cjs +5 -5
  64. package/dist/components/contact/index.js +4 -4
  65. package/dist/components/docs/index.cjs +7 -7
  66. package/dist/components/docs/index.js +6 -6
  67. package/dist/components/embeds/claude-embed.d.ts +15 -8
  68. package/dist/components/embeds/claude-embed.d.ts.map +1 -1
  69. package/dist/components/embeds/embed-viewer-frame.d.ts +17 -2
  70. package/dist/components/embeds/embed-viewer-frame.d.ts.map +1 -1
  71. package/dist/components/embeds/index.cjs +5 -5
  72. package/dist/components/embeds/index.js +4 -4
  73. package/dist/components/faq/index.cjs +6 -6
  74. package/dist/components/faq/index.js +5 -5
  75. package/dist/components/features/index.cjs +4 -4
  76. package/dist/components/features/index.js +3 -3
  77. package/dist/components/help-center-pages/index.cjs +25 -25
  78. package/dist/components/help-center-pages/index.js +16 -16
  79. package/dist/components/index.cjs +142 -154
  80. package/dist/components/index.cjs.map +1 -1
  81. package/dist/components/index.js +14 -26
  82. package/dist/components/index.js.map +1 -1
  83. package/dist/components/meeting-scheduler/index.cjs +36 -36
  84. package/dist/components/meeting-scheduler/index.js +5 -5
  85. package/dist/components/navigation/index.cjs +4 -4
  86. package/dist/components/navigation/index.js +3 -3
  87. package/dist/components/onboarding-guides/index.cjs +8 -8
  88. package/dist/components/onboarding-guides/index.js +7 -7
  89. package/dist/components/related-content/index.cjs +6 -6
  90. package/dist/components/related-content/index.js +5 -5
  91. package/dist/components/tickets/index.cjs +8 -8
  92. package/dist/components/tickets/index.js +7 -7
  93. package/dist/components/ui/index.cjs +4 -16
  94. package/dist/components/ui/index.cjs.map +1 -1
  95. package/dist/components/ui/index.js +3 -15
  96. package/dist/components/ui/markdown/sanitize.d.ts.map +1 -1
  97. package/dist/components/ui/table/table.d.ts +1 -1
  98. package/dist/components/ui/table/table.d.ts.map +1 -1
  99. package/dist/components/ui/table/types.d.ts +9 -0
  100. package/dist/components/ui/table/types.d.ts.map +1 -1
  101. package/dist/contexts/endpoints-runtime-context.d.ts +6 -0
  102. package/dist/contexts/endpoints-runtime-context.d.ts.map +1 -1
  103. package/dist/contexts/index.cjs +2 -2
  104. package/dist/contexts/index.js +1 -1
  105. package/dist/hooks/index.cjs +5 -3
  106. package/dist/hooks/index.cjs.map +1 -1
  107. package/dist/hooks/index.d.ts +1 -0
  108. package/dist/hooks/index.d.ts.map +1 -1
  109. package/dist/hooks/index.js +4 -2
  110. package/dist/hooks/use-claude-mirror-src.d.ts +38 -0
  111. package/dist/hooks/use-claude-mirror-src.d.ts.map +1 -0
  112. package/dist/index.cjs +10 -8
  113. package/dist/index.cjs.map +1 -1
  114. package/dist/index.js +15 -13
  115. package/dist/utils/embed-proxy-auth-storage.d.ts.map +1 -1
  116. package/dist/utils/embed-url-converters.d.ts +20 -0
  117. package/dist/utils/embed-url-converters.d.ts.map +1 -1
  118. package/dist/utils/fullscreen-switch.d.ts +85 -0
  119. package/dist/utils/fullscreen-switch.d.ts.map +1 -0
  120. package/dist/utils/index.cjs +114 -0
  121. package/dist/utils/index.cjs.map +1 -1
  122. package/dist/utils/index.d.ts +1 -0
  123. package/dist/utils/index.d.ts.map +1 -1
  124. package/dist/utils/index.js +113 -1
  125. package/dist/utils/index.js.map +1 -1
  126. package/package.json +1 -1
  127. package/src/components/chat/hooks/use-chat-attachments.ts +3 -3
  128. package/src/components/chat/hooks/use-chat-history-hydration.ts +2 -2
  129. package/src/components/chat/hooks/use-chat-identity.ts +5 -5
  130. package/src/components/chat/hooks/use-sse-chat-adapter.ts +3 -3
  131. package/src/components/chat/utils/.index.md +1 -2
  132. package/src/components/chat/utils/chat-conversation-storage.ts +2 -2
  133. package/src/components/chat/utils/index.ts +3 -10
  134. package/src/components/embeds/claude-embed.tsx +50 -14
  135. package/src/components/embeds/embed-viewer-frame.tsx +103 -5
  136. package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +30 -0
  137. package/src/components/ui/markdown/rich/__tests__/shortcodes.test.ts +8 -0
  138. package/src/components/ui/markdown/sanitize.ts +4 -0
  139. package/src/components/ui/table/table.tsx +7 -2
  140. package/src/components/ui/table/types.ts +9 -0
  141. package/src/contexts/endpoints-runtime-context.tsx +6 -0
  142. package/src/hooks/index.ts +4 -0
  143. package/src/hooks/use-claude-mirror-src.ts +103 -0
  144. package/src/utils/.embed-proxy-auth-storage.md +2 -2
  145. package/src/utils/embed-proxy-auth-storage.ts +19 -14
  146. package/src/utils/embed-url-converters.ts +45 -0
  147. package/src/utils/fullscreen-switch.ts +180 -0
  148. package/src/utils/index.ts +8 -6
  149. package/dist/chunk-6RSKK4E7.cjs.map +0 -1
  150. package/dist/chunk-CEAAOVCV.cjs.map +0 -1
  151. package/dist/chunk-EYXZF46V.js.map +0 -1
  152. package/dist/chunk-LXWQLUBR.cjs.map +0 -1
  153. package/dist/chunk-NK3M6F5A.cjs.map +0 -1
  154. package/dist/chunk-UWY722RH.js.map +0 -1
  155. package/dist/chunk-ZNEVGXMU.js.map +0 -1
  156. package/dist/components/chat/utils/chat-authed-fetch.d.ts +0 -15
  157. package/dist/components/chat/utils/chat-authed-fetch.d.ts.map +0 -1
  158. package/dist/components/chat/utils/chat-proxy-auth-storage.d.ts +0 -16
  159. package/dist/components/chat/utils/chat-proxy-auth-storage.d.ts.map +0 -1
  160. package/src/components/chat/utils/.chat-authed-fetch.md +0 -25
  161. package/src/components/chat/utils/.chat-proxy-auth-storage.md +0 -38
  162. package/src/components/chat/utils/chat-authed-fetch.ts +0 -17
  163. package/src/components/chat/utils/chat-proxy-auth-storage.ts +0 -25
  164. /package/dist/{chunk-2LKVYI7H.js.map → chunk-3H6NH2AD.js.map} +0 -0
  165. /package/dist/{chunk-O3JCKEVG.js.map → chunk-KYDVOX5J.js.map} +0 -0
  166. /package/dist/{chunk-HB3IG2MC.js.map → chunk-MT7IDGCM.js.map} +0 -0
  167. /package/dist/{chunk-G2PAUMV6.js.map → chunk-MTORQ4LY.js.map} +0 -0
  168. /package/dist/{chunk-QVQLNKFS.js.map → chunk-PTDRJCPW.js.map} +0 -0
  169. /package/dist/{chunk-DZ2Z7HRF.js.map → chunk-QIU5B2ZD.js.map} +0 -0
  170. /package/dist/{chunk-BZ7B255K.js.map → chunk-U6NPZMNT.js.map} +0 -0
  171. /package/dist/{chunk-MQQM4NCN.js.map → chunk-VZQKENQU.js.map} +0 -0
  172. /package/dist/{chunk-VEXTUGUA.js.map → chunk-XWMEHLDK.js.map} +0 -0
  173. /package/dist/{chunk-ZXASQL3E.js.map → chunk-YYESWAAQ.js.map} +0 -0
  174. /package/dist/{chunk-6TDWCBQM.js.map → chunk-Z3MSD6ZE.js.map} +0 -0
  175. /package/dist/{chunk-KL6ZDLDF.js.map → chunk-ZAYZ7JHF.js.map} +0 -0
  176. /package/dist/{chunk-ZIZJDXUI.js.map → chunk-ZTGWMXJK.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.568",
3
+ "version": "0.0.569-snapshot.20260827034838",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,7 +9,7 @@
9
9
  * `AbortController` for clean remove-mid-upload.
10
10
  *
11
11
  * Auth propagation:
12
- * - `chatAuthedFetch` carries bearer-act-as headers + Supabase
12
+ * - `embedAuthedFetch` carries bearer-act-as headers + Supabase
13
13
  * session cookies — used both for minting the upload URL and as
14
14
  * the auth path before falling through to the signed PUT.
15
15
  * - The byte-PUT to the signed URL uses an inline `uploadWithProgress`
@@ -26,7 +26,7 @@
26
26
  import { useCallback, useMemo, useRef, useState } from 'react'
27
27
  import { fileTypeFromBlob } from 'file-type'
28
28
  import { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'
29
- import { chatAuthedFetch } from '../utils/chat-authed-fetch'
29
+ import { embedAuthedFetch } from '../../../utils/embed-authed-fetch'
30
30
  import {
31
31
  CHAT_ATTACHMENT_MIME_TYPES,
32
32
  CHAT_ATTACHMENT_CONCURRENT_UPLOADS_PER_USER,
@@ -243,7 +243,7 @@ export function useChatAttachments(): UseChatAttachmentsApi {
243
243
  // bearer-act-as headers (if present).
244
244
  updateOne(att.id, { status: 'uploading', progress: 5 })
245
245
 
246
- const urlResp = await chatAuthedFetch(uploadUrlEndpoint, {
246
+ const urlResp = await embedAuthedFetch(uploadUrlEndpoint, {
247
247
  method: 'POST',
248
248
  headers: { 'Content-Type': 'application/json' },
249
249
  signal: ctrl?.signal,
@@ -19,7 +19,7 @@
19
19
 
20
20
  import { useEffect, useRef, useState, type MutableRefObject } from 'react'
21
21
  import type { Message } from './use-chat'
22
- import { chatAuthedFetch } from '../utils/chat-authed-fetch'
22
+ import { embedAuthedFetch } from '../../../utils/embed-authed-fetch'
23
23
  import { AUTO_CONTINUATION_DIRECTIVE_PREFIX } from '../utils/auto-continuation-directive'
24
24
 
25
25
  export interface UseChatHistoryHydrationArgs {
@@ -79,7 +79,7 @@ export function useChatHistoryHydration({
79
79
  setIsHydratingHistory(true)
80
80
  ;(async () => {
81
81
  try {
82
- const res = await chatAuthedFetch(
82
+ const res = await embedAuthedFetch(
83
83
  `${historyUrl}?conversationId=${encodeURIComponent(conversationId)}`,
84
84
  { method: 'GET' },
85
85
  )
@@ -43,8 +43,8 @@
43
43
 
44
44
  import { createContext, createElement, useContext, useEffect, useState, type ReactNode } from 'react'
45
45
  import { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'
46
- import { chatAuthedFetch } from '../utils/chat-authed-fetch'
47
- import { getChatProxyAuth } from '../utils/chat-proxy-auth-storage'
46
+ import { embedAuthedFetch } from '../../../utils/embed-authed-fetch'
47
+ import { getEmbedProxyAuth } from '../../../utils/embed-proxy-auth-storage'
48
48
 
49
49
  /**
50
50
  * Wire-shape for the identity route response. Mirrors the hub's
@@ -109,10 +109,10 @@ const ANON_DEFAULTS: ChatIdentityResponse = {
109
109
  function useResolveChatIdentity(enabled: boolean): ChatIdentitySurface {
110
110
  const runtime = useRequiredChatRuntime()
111
111
  const url = runtime.endpoints.identityUrl
112
- // `getChatProxyAuth()` reads localStorage every render. If the user
112
+ // `getEmbedProxyAuth()` reads localStorage every render. If the user
113
113
  // pastes bearer creds mid-session (via the `/debug` creds bar),
114
114
  // their email arrives here and the effect's dep changes → refetch.
115
- const proxyEmail = getChatProxyAuth()?.email ?? null
115
+ const proxyEmail = getEmbedProxyAuth()?.email ?? null
116
116
 
117
117
  const [data, setData] = useState<ChatIdentityResponse>(ANON_DEFAULTS)
118
118
  // Only "loading" if THIS instance will actually fetch. A disabled
@@ -126,7 +126,7 @@ function useResolveChatIdentity(enabled: boolean): ChatIdentitySurface {
126
126
  const ctrl = new AbortController()
127
127
 
128
128
  setIsLoading(true)
129
- chatAuthedFetch(url, { signal: ctrl.signal })
129
+ embedAuthedFetch(url, { signal: ctrl.signal })
130
130
  .then(async (resp) => {
131
131
  if (!resp.ok) return ANON_DEFAULTS
132
132
  return (await resp.json()) as ChatIdentityResponse
@@ -57,7 +57,7 @@ import type { ChatRef } from '../chat-ref.types'
57
57
  import type { Message } from '../types/message.types'
58
58
  import type { MessageSegment } from '../types/message.types'
59
59
  import { useSlashCommandRegistry, type SlashCommandSummary } from './use-slash-commands'
60
- import { chatAuthedFetch } from '../utils/chat-authed-fetch'
60
+ import { embedAuthedFetch } from '../../../utils/embed-authed-fetch'
61
61
  import type { ScrollAnchor } from '../utils/scroll-anchor'
62
62
  import { buildConfirmToolBody, readServerErrorMessage } from '../../../chat-protocol/confirm-tool'
63
63
  import { createSseFrameDecoder } from '../../../chat-protocol/decode'
@@ -458,10 +458,10 @@ export function useSseChatAdapter(
458
458
  abortControllerRef.current = ctrl
459
459
 
460
460
  try {
461
- // `chatAuthedFetch` carries the bearer-act-as headers (+ Supabase
461
+ // `embedAuthedFetch` carries the bearer-act-as headers (+ Supabase
462
462
  // session cookies) — same wrapper `use-chat-attachments` and
463
463
  // `use-chat-identity` use.
464
- const response = await chatAuthedFetch(targetPath, {
464
+ const response = await embedAuthedFetch(targetPath, {
465
465
  method: 'POST',
466
466
  body: JSON.stringify(requestBody),
467
467
  signal: ctrl.signal,
@@ -8,7 +8,7 @@ Barrel export module that consolidates and re-exports all message processing, ch
8
8
  | **Message Accumulation** | `MessageSegmentAccumulator`, `createMessageSegmentAccumulator`, `AccumulatorCallbacks` |
9
9
  | **Historical Messages** | `processHistoricalMessages`, `extractErrorMessages`, `processHistoricalMessagesWithErrors` |
10
10
  | **Chat Navigation** | `handleChatNavClick`, `resolveExternalNavigation`, `decideNewTab`, `computeIsNewTab`, `buildAnchorProps` |
11
- | **Proxy Auth** | `getChatProxyAuth`, `setChatProxyAuth`, `clearChatProxyAuth`, `applyProxyAuth`, `chatAuthedFetch` |
11
+ | **Proxy Auth** | moved to `utils/` — `getEmbedProxyAuth`, `setEmbedProxyAuth`, `clearEmbedProxyAuth`, `applyProxyAuth`, `embedAuthedFetch` |
12
12
  | **Attachments** | `formatChatAttachmentMarkdownForBubble`, `stripChatAttachmentMarkdown`, `buildChatAttachmentViewUrl` |
13
13
  | **UI / Styling** | `chatChipClass`, compact card class constants (`COMPACT_CARD_*`), `getStatusColorScheme` |
14
14
  | **Icon Registry** | `ICON_REGISTRY`, `getIconComponent`, `normalizeIconKey`, `getDynamicIcon` |
@@ -21,7 +21,6 @@ Barrel export module that consolidates and re-exports all message processing, ch
21
21
  import {
22
22
  createMessageSegmentAccumulator,
23
23
  handleChatNavClick,
24
- getChatProxyAuth,
25
24
  getIconComponent,
26
25
  formatChatAttachmentMarkdownForBubble,
27
26
  decideNewTab,
@@ -19,7 +19,7 @@ import {
19
19
  createLocalStorageAdapter,
20
20
  type LocalStorageAdapter,
21
21
  } from '../../../utils/local-storage-adapter'
22
- import { getChatProxyAuth } from './chat-proxy-auth-storage'
22
+ import { getEmbedProxyAuth } from '../../../utils/embed-proxy-auth-storage'
23
23
 
24
24
  export interface PersistedChatConversation {
25
25
  conversationId: string
@@ -29,7 +29,7 @@ const STORAGE_KEY = 'conversation'
29
29
 
30
30
  const namespaceFor = (source: string): string => {
31
31
  const base = `mingo-chat-${source}`
32
- const auth = getChatProxyAuth()
32
+ const auth = getEmbedProxyAuth()
33
33
  return auth?.email ? `${base}-u-${encodeURIComponent(auth.email.toLowerCase())}` : base
34
34
  }
35
35
 
@@ -89,16 +89,9 @@ export {
89
89
  type RuntimeMode,
90
90
  } from './decide-new-tab'
91
91
 
92
- // Chat-proxy auth (localStorage adapter + fetch wrapper)
93
- export {
94
- getChatProxyAuth,
95
- getPersistedProxyEmail,
96
- setChatProxyAuth,
97
- clearChatProxyAuth,
98
- applyProxyAuth,
99
- type ChatProxyAuth,
100
- } from './chat-proxy-auth-storage'
101
- export { chatAuthedFetch } from './chat-authed-fetch'
92
+ // Embed-surface auth moved to `utils/` (embed-proxy-auth-storage /
93
+ // embed-authed-fetch) — import the Embed* names from
94
+ // `@flamingo-stack/openframe-frontend-core/utils`.
102
95
 
103
96
  // Pure content helpers
104
97
  export { flattenAssistantContent } from './flatten-assistant-content'
@@ -4,6 +4,7 @@ import { ExternalLink } from 'lucide-react'
4
4
  import { Button } from '../ui/button/button'
5
5
  import { ClaudeIcon } from '../icons/claude-icon'
6
6
  import { EmbedViewerFrame } from './embed-viewer-frame'
7
+ import { useClaudeMirrorSrc } from '../../hooks/use-claude-mirror-src'
7
8
  import { toClaudeEmbedUrl } from '../../utils/embed-url-converters'
8
9
 
9
10
  export type ClaudeEmbedKind = 'artifact' | 'design'
@@ -33,24 +34,44 @@ const KIND_HEADING: Record<ClaudeEmbedKind, string> = {
33
34
  * `{{claude-artifact:URL}}` / `{{claude-design:URL}}` shortcodes, so it looks
34
35
  * identical wherever markdown renders — a spec body, a comment, a links rail.
35
36
  *
36
- * A PUBLISHED artifact frames through its `/embed` route (`toClaudeEmbedUrl`).
37
- * Everything else has no embeddable route at all, and then `src` is null and
38
- * the shared frame shows its own empty state — the action stays either way, so
39
- * the artifact is always one click away.
37
+ * FRAME SOURCE — fully under the hood, in priority order:
38
+ * 1. The host's SELF-HOSTED MIRROR: the url's artifact id derives the
39
+ * storage-view proxy path (`toClaudeMirrorPath`), probed with a
40
+ * 1-byte ranged fetch. When the host has ingested a copy, the frame
41
+ * shows it — claude.ai frame-locks CODE artifacts entirely, so this
42
+ * is the only way those ever render inline.
43
+ * 2. claude.ai's own `/embed` route for a PUBLISHED artifact
44
+ * (`toClaudeEmbedUrl`).
45
+ * 3. Neither → the shared frame's empty state.
46
+ * The consumer passes only the claude URL; "Open in Claude" always
47
+ * targets it, so commenting stays on claude.ai.
40
48
  *
41
- * What defeats the frame: a Claude CODE url (no Embed settings exist for it,
42
- * and its `/embed` path answers `frame-ancestors 'self'`), a Claude Design url
43
- * (no embed route), an artifact whose author has not allow-listed this host,
44
- * or an http host (mixed content — it will not paint on `http://localhost`).
49
+ * What defeats the claude.ai leg: a Claude CODE url (no Embed settings
50
+ * exist for it, and its `/embed` path answers `frame-ancestors 'self'`),
51
+ * a Claude Design url (no embed route), an artifact whose author has not
52
+ * allow-listed this host, or an http host (mixed content).
45
53
  */
46
54
  export function ClaudeEmbed({ url, kind = 'artifact', title, height, loading = 'lazy' }: ClaudeEmbedProps) {
47
- const embedUrl = toClaudeEmbedUrl(url)
55
+ // Mirror detection lives in `useClaudeMirrorSrc` (derive from
56
+ // EndpointsRuntime + 1-byte ranged probe + background revalidation) — this
57
+ // component stays presentational.
58
+ const { src: mirrorSrc, status } = useClaudeMirrorSrc(url)
59
+ // While the probe is in flight (on a cold view that IS the ~1-2s
60
+ // self-heal), show the shell's loading state rather than the claude.ai
61
+ // fallback — a code artifact's fallback is the empty "no embeddable view"
62
+ // box, and flashing it before the mirror resolves reads as broken.
63
+ const isProbing = status === 'probing'
64
+ // Found → the mirror; otherwise the claude.ai embed (or its empty state).
65
+ // Suppressed while probing so nothing renders under the spinner.
66
+ const embedUrl = isProbing ? null : (mirrorSrc ?? toClaudeEmbedUrl(url))
48
67
  return (
49
68
  <EmbedViewerFrame
50
69
  className="my-6 space-y-3"
51
70
  icon={<ClaudeIcon className="w-5 h-5 shrink-0" />}
52
71
  title={title?.trim() || KIND_HEADING[kind]}
53
72
  titleVariant="h6"
73
+ isLoading={isProbing}
74
+ loadingMessage="Loading artifact…"
54
75
  actions={
55
76
  <Button
56
77
  variant="outline"
@@ -65,6 +86,10 @@ export function ClaudeEmbed({ url, kind = 'artifact', title, height, loading = '
65
86
  </Button>
66
87
  }
67
88
  src={embedUrl}
89
+ // The framed artifact has no internal fullscreen button of its own
90
+ // (unlike Figma's player) — the shell provides the toggle for both
91
+ // the claude.ai `/embed` route and a mirror.
92
+ fullscreenControl
68
93
  loading={loading}
69
94
  height={height}
70
95
  // Same frame contract Figma gets — an artifact is a real app: it copies
@@ -72,11 +97,22 @@ export function ClaudeEmbed({ url, kind = 'artifact', title, height, loading = '
72
97
  allow="clipboard-write; clipboard-read; fullscreen"
73
98
  // …but UNLIKE Figma, an artifact is USER-AUTHORED HTML and JS, so it is
74
99
  // sandboxed. Omitting `allow-top-navigation` is the point: an artifact
75
- // cannot navigate the page it is embedded in. `allow-same-origin` beside
76
- // `allow-scripts` is what Anthropic's own embed snippet uses and is safe
77
- // on a CROSS-ORIGIN frame — it grants the frame claude.ai's origin, never
78
- // ours.
79
- sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
100
+ // cannot navigate the page it is embedded in. The `allow-same-origin`
101
+ // token is CONDITIONAL on where the frame points:
102
+ // - claude.ai `/embed` (no mirror): Anthropic's own embed snippet
103
+ // uses it, and on a CROSS-ORIGIN frame it grants the frame
104
+ // claude.ai's origin — never ours. The artifact runtime needs
105
+ // it (storage, postMessage handshake).
106
+ // - the derived MIRROR is served from the HOST's OWN origin —
107
+ // `allow-same-origin` + `allow-scripts` there would be a no-op
108
+ // sandbox handing the artifact first-party cookies and
109
+ // `window.parent`. A static mirror renders fine from an opaque
110
+ // origin, so the token is dropped.
111
+ sandbox={
112
+ mirrorSrc
113
+ ? 'allow-scripts allow-popups allow-forms'
114
+ : 'allow-scripts allow-same-origin allow-popups allow-forms'
115
+ }
80
116
  allowFullScreen
81
117
  emptyIcon={<ClaudeIcon className="w-16 h-16 mb-4" />}
82
118
  emptyMessage="Open this one in Claude · it has no embeddable view"
@@ -1,7 +1,10 @@
1
1
  'use client'
2
2
 
3
- import React from 'react'
3
+ import React, { useEffect, useRef, useState } from 'react'
4
+ import { Loader2, Maximize2, Minimize2 } from 'lucide-react'
4
5
  import { cn } from '../../utils/cn'
6
+ import { FullscreenSwitchController } from '../../utils/fullscreen-switch'
7
+ import { Button } from '../ui/button/button'
5
8
  import { EmbedIframe } from './embed-iframe'
6
9
 
7
10
  /**
@@ -34,11 +37,19 @@ export interface EmbedViewerFrameProps {
34
37
  titleVariant?: 'h3' | 'h6'
35
38
  /** Rendered verbatim in the header row — the caller owns its own container/state. */
36
39
  actions?: React.ReactNode
37
- /** Iframe source; falsy renders the inline empty state under the header. */
40
+ /** Iframe source; falsy renders the inline empty state (or the loading
41
+ * state when `isLoading`) under the header. */
38
42
  src: string | null | undefined
43
+ /** While true AND there is no `src` yet, the body shows a loading skeleton
44
+ * instead of the empty state — for a viewer that is still resolving where
45
+ * its frame points (e.g. ClaudeEmbed probing for a self-hosted mirror),
46
+ * so a cold first view reads as "loading", not "nothing here". */
47
+ isLoading?: boolean
39
48
  /** Large icon for the inline empty state (the viewers pass `w-16 h-16`). */
40
49
  emptyIcon?: React.ReactNode
41
50
  emptyMessage?: string
51
+ /** Copy under the spinner in the loading state. */
52
+ loadingMessage?: string
42
53
  height?: string
43
54
  allow?: string
44
55
  allowFullScreen?: boolean
@@ -47,6 +58,13 @@ export interface EmbedViewerFrameProps {
47
58
  sandbox?: string
48
59
  /** Wrapper classes; defaults to the sheets/pdf `space-y-4`. */
49
60
  className?: string
61
+ /** Opt-in fullscreen toggle in the header row (rendered after `actions`,
62
+ * only when there is a `src`). Fullscreens the WHOLE frame — header
63
+ * included — so the exit control stays reachable (Esc works too).
64
+ * Exists because some embedded contents ship their own fullscreen
65
+ * button (the Figma player) and some cannot (a self-hosted Claude
66
+ * artifact mirror) — the shell provides the affordance uniformly. */
67
+ fullscreenControl?: boolean
50
68
  }
51
69
 
52
70
  export function EmbedViewerFrame({
@@ -55,17 +73,55 @@ export function EmbedViewerFrame({
55
73
  titleVariant = 'h3',
56
74
  actions,
57
75
  src,
76
+ isLoading,
58
77
  emptyIcon,
59
78
  emptyMessage = 'Content not available',
79
+ loadingMessage = 'Loading…',
60
80
  height,
61
81
  allow,
62
82
  allowFullScreen,
63
83
  loading,
64
84
  sandbox,
65
85
  className,
86
+ fullscreenControl,
66
87
  }: EmbedViewerFrameProps) {
88
+ const frameRef = useRef<HTMLDivElement>(null)
89
+ const [isFullscreen, setIsFullscreen] = useState(false)
90
+
91
+ // ONE fullscreen implementation across the library and its hosts:
92
+ // `FullscreenSwitchController` (shared with the hub's deck) owns the
93
+ // Fullscreen API calls, webkit fallbacks, rejection safety, and the
94
+ // change-event-driven state — Esc, F11, buttons and browser chrome all
95
+ // converge on its listener, so the icon can never desync. Here it runs
96
+ // element-level (`target` = this frame; no mask classes — the frame
97
+ // styles off its own state).
98
+ const controllerRef = useRef<FullscreenSwitchController | null>(null)
99
+ useEffect(() => {
100
+ if (!fullscreenControl) return
101
+ const controller = new FullscreenSwitchController({
102
+ target: () => frameRef.current,
103
+ onFullscreenChange: setIsFullscreen,
104
+ })
105
+ controllerRef.current = controller
106
+ controller.attach()
107
+ return () => {
108
+ controllerRef.current = null
109
+ controller.detach()
110
+ }
111
+ }, [fullscreenControl])
112
+ const toggleFullscreen = () => controllerRef.current?.toggle()
113
+
67
114
  return (
68
- <div className={cn('space-y-4', className)}>
115
+ <div
116
+ ref={frameRef}
117
+ className={cn(
118
+ 'space-y-[var(--spacing-system-mf)]',
119
+ className,
120
+ // Fullscreen paints its own ground (the fullscreened element
121
+ // otherwise sits on the UA's black backdrop) and scrolls itself.
122
+ isFullscreen && 'overflow-auto bg-ods-bg p-[var(--spacing-system-mf)]',
123
+ )}
124
+ >
69
125
  <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
70
126
  <div className="flex items-center gap-2 min-w-0">
71
127
  {icon}
@@ -75,18 +131,60 @@ export function EmbedViewerFrame({
75
131
  <span className="text-h6 font-semibold text-ods-text-primary truncate">{title}</span>
76
132
  )}
77
133
  </div>
78
- {actions}
134
+ {/* Same responsive action-row idiom as the PDF viewer's button pair
135
+ and every viewer's own action: full-width stacked on mobile,
136
+ inline row from `sm:` up, SAME Button idiom (outline /
137
+ small-legacy / w-4 icon / w-full sm:w-auto). Without the
138
+ fullscreen toggle, `actions` renders EXACTLY as before — the
139
+ wrapper exists only when the toggle joins it, so pre-existing
140
+ viewers (figma/pdf/sheets) are byte-identical. */}
141
+ {fullscreenControl && src ? (
142
+ <div className="flex flex-col gap-[var(--spacing-system-xsf)] sm:flex-row sm:items-center">
143
+ {actions}
144
+ <Button
145
+ variant="outline"
146
+ size="small-legacy"
147
+ onClick={toggleFullscreen}
148
+ aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
149
+ leftIcon={isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
150
+ className="w-full sm:w-auto"
151
+ >
152
+ {isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
153
+ </Button>
154
+ </div>
155
+ ) : (
156
+ actions
157
+ )}
79
158
  </div>
80
159
  {src ? (
81
160
  <EmbedIframe
82
161
  src={src}
83
162
  title={title}
84
- height={height}
163
+ // 96px = the fullscreen wrapper's own chrome above the iframe:
164
+ // header row (~40px incl. its gap) + the wrapper's mf padding
165
+ // (16px × 2) + the mf space-y gap (16px), rounded up so the
166
+ // frame never forces a scrollbar. Recheck if that chrome changes.
167
+ height={isFullscreen ? 'calc(100vh - 96px)' : height}
85
168
  allow={allow}
86
169
  allowFullScreen={allowFullScreen}
87
170
  loading={loading}
88
171
  sandbox={sandbox}
89
172
  />
173
+ ) : isLoading ? (
174
+ // SAME box as the empty/iframe state (height + border), so a viewer
175
+ // resolving its frame doesn't collapse then jump — it reads as
176
+ // "loading" and settles into the iframe in place.
177
+ <div
178
+ className="flex flex-col items-center justify-center gap-3 rounded-lg border border-ods-border px-4 py-16 text-center"
179
+ style={height ? { height } : undefined}
180
+ >
181
+ {/* The library's canonical section spinner — `Loader2` +
182
+ `animate-spin` is the only spinner idiom in the codebase (17
183
+ lib call sites, 67 in the hub), and `h-8 w-8 … text-ods-accent`
184
+ is its most-used full-size variant. */}
185
+ <Loader2 className="h-8 w-8 animate-spin text-ods-accent" />
186
+ <p className="text-ods-text-secondary">{loadingMessage}</p>
187
+ </div>
90
188
  ) : (
91
189
  // The SAME box the iframe would have filled: same height, same rounded
92
190
  // border. A viewer whose content is missing must not collapse the page
@@ -16,6 +16,7 @@ import {
16
16
  buildEffectiveTagSet,
17
17
  buildSanitizeSchema,
18
18
  escapeUnknownHtmlTags,
19
+ rehypeStripUnsafe,
19
20
  SAFE_HTML_TAGS,
20
21
  SVG_TAGS,
21
22
  } from '../sanitize'
@@ -352,3 +353,32 @@ describe('link-definition shelters blank in every shape (round 21)', () => {
352
353
  expect(masked).toContain('## after heading')
353
354
  })
354
355
  })
356
+
357
+ describe('URL_ATTRS covers hast PROPERTY keys, not attribute names', () => {
358
+ it('strips a javascript: data-url via its camelized hast key', () => {
359
+ // property-information camelizes `data-url` to `dataUrl` BEFORE the
360
+ // strip pass runs - the URL_ATTRS entry must match the lowercased
361
+ // PROPERTY key or the scheme check silently never fires.
362
+ const tree = {
363
+ type: 'root',
364
+ children: [
365
+ {
366
+ type: 'element',
367
+ tagName: 'div',
368
+ properties: {
369
+ className: ['claude-embed'],
370
+ dataUrl: 'javascript:alert(1)',
371
+ dataKind: 'artifact',
372
+ },
373
+ children: [],
374
+ },
375
+ ],
376
+ }
377
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
378
+ ;(rehypeStripUnsafe() as (t: unknown) => void)(tree as any)
379
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
380
+ const props = (tree.children[0] as any).properties
381
+ expect(props.dataUrl).toBeUndefined()
382
+ expect(props.dataKind).toBe('artifact')
383
+ })
384
+ })
@@ -91,6 +91,14 @@ describe('processShortcodes design-doc embeds', () => {
91
91
  expect(design).toContain('data-kind="design"');
92
92
  });
93
93
 
94
+ it('threads the optional pipe title into data-title', () => {
95
+ const titled = processShortcodes('{{claude-artifact:https://claude.ai/a|Brief doc}}');
96
+ expect(titled).toContain('data-title="Brief doc"');
97
+
98
+ const untitled = processShortcodes('{{claude-artifact:https://claude.ai/a}}');
99
+ expect(untitled).not.toContain('data-title');
100
+ });
101
+
94
102
  it('escapes the url attribute so a crafted link cannot break out of the block', () => {
95
103
  const output = processShortcodes('{{claude-artifact:https://claude.ai/a"><script>alert(1)</script>}}');
96
104
  expect(output).not.toContain('<script>');
@@ -395,6 +395,10 @@ const URL_ATTRS = new Set([
395
395
  'data',
396
396
  'action',
397
397
  'background',
398
+ // The embed shortcodes' primary url (`data-url` → `dataUrl`) — it
399
+ // becomes an anchor href ("Open in Claude") and a link-preview fetch
400
+ // target, so a `javascript:`/`data:` scheme there is a live sink too.
401
+ 'dataurl',
398
402
  ])
399
403
 
400
404
  /**
@@ -21,6 +21,7 @@ function injectSyntheticColumns<T>(
21
21
  rowActions?: RowAction<T>[],
22
22
  renderRowActions?: (item: T) => ReactNode,
23
23
  rowHref?: (item: T) => string | null | undefined,
24
+ actionsColumnWidth?: string,
24
25
  ): TableColumn<T>[] {
25
26
  const hasActions = Boolean(rowActions?.length) || Boolean(renderRowActions)
26
27
  const result = [...columns]
@@ -29,7 +30,10 @@ function injectSyntheticColumns<T>(
29
30
  const actionsColumn: TableColumn<T> = {
30
31
  key: '__actions__',
31
32
  label: '',
32
- width: 'min-w-[100px] w-auto shrink-0 flex-none',
33
+ // A caller-fixed width keeps the header and every row on ONE grid
34
+ // (see `actionsColumnWidth` in types.ts); the default stays
35
+ // content-sized for back-compat.
36
+ width: actionsColumnWidth ?? 'min-w-[100px] w-auto shrink-0 flex-none',
33
37
  align: 'right',
34
38
  renderCell: (item: T) => (
35
39
  <div className="flex gap-2 items-center justify-end pointer-events-auto" data-no-row-click>
@@ -120,11 +124,12 @@ export function Table<T = any>({
120
124
  stickyHeader,
121
125
  stickyHeaderOffset,
122
126
  animateRowReorder,
127
+ actionsColumnWidth,
123
128
  }: TableProps<T>) {
124
129
  // Opt-in row-reorder animation: framer-motion is loaded lazily (its own chunk)
125
130
  // ONLY when `animateRowReorder` is set, so the default table stays motion-free.
126
131
  const tableMotion = useTableMotion(Boolean(animateRowReorder))
127
- const columnsWithActions = injectSyntheticColumns(columns, rowActions, renderRowActions, rowHref)
132
+ const columnsWithActions = injectSyntheticColumns(columns, rowActions, renderRowActions, rowHref, actionsColumnWidth)
128
133
  const getRowHref = (item: T): string | undefined => {
129
134
  if (onRowClick || !rowHref) return undefined
130
135
  return rowHref(item) ?? undefined
@@ -110,6 +110,15 @@ export interface TableProps<T = any> {
110
110
  rowActions?: RowAction<T>[]
111
111
  // Custom renderer for the actions area (e.g., kebab/dots menu)
112
112
  renderRowActions?: (item: T) => ReactNode
113
+ /**
114
+ * Width class(es) for the synthetic actions column. The default is
115
+ * content-sized (`w-auto`), which mis-aligns every OTHER column between
116
+ * the header and the rows whenever the row actions are wider than the
117
+ * header's "Showing N results" counter (each side sizes to its own
118
+ * content). Pass a FIXED width (e.g. `w-60`) when the actions are wide
119
+ * text buttons so the header and rows share one grid.
120
+ */
121
+ actionsColumnWidth?: string
113
122
 
114
123
  /**
115
124
  * When provided, renders a right-pointing chevron link at the end of every row
@@ -34,6 +34,12 @@ export interface EndpointsRuntime {
34
34
  }
35
35
  /** POST contact-form submission. */
36
36
  contactUrl: string
37
+ /** GET base of the host's private-storage view proxy
38
+ * (`<base>/<bucket>/<object>`). `ClaudeEmbed` derives artifact mirror
39
+ * urls under it (`<base>/design-briefs/<uuid>.html`). OPTIONAL: a host
40
+ * without the proxy simply omits it and claude embeds fall back to
41
+ * claude.ai — no probing happens. */
42
+ storageViewBaseUrl?: string
37
43
  }
38
44
 
39
45
  export const EndpointsRuntimeContext = createContext<EndpointsRuntime | null>(null)
@@ -29,6 +29,10 @@ export * from './use-access-code-integration'
29
29
  // THE og-placeholder URL hook (endpoints-driven; no injected builder)
30
30
  export * from './use-og-placeholder-url'
31
31
 
32
+ // Claude artifact mirror detection (endpoints-driven; ClaudeEmbed's
33
+ // transparent mirror leg)
34
+ export * from './use-claude-mirror-src'
35
+
32
36
  // Deep-link "scroll to URL hash" after data loads. Pairs with URL
33
37
  // composers that emit `?<filter>=<id>#<prefix>-<id>` — the filter
34
38
  // narrows the list, the hash scrolls the matching DOM id.