@flamingo-stack/openframe-frontend-core 0.0.581 → 0.0.582-1958.5391.1

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 (115) hide show
  1. package/dist/chunk-2EIGO6ZS.js +1055 -0
  2. package/dist/chunk-2EIGO6ZS.js.map +1 -0
  3. package/dist/{chunk-HVSZYUGP.js → chunk-53ZVUW2M.js} +5 -5
  4. package/dist/{chunk-R5JVRFSW.js → chunk-5U2FU5LM.js} +3 -3
  5. package/dist/{chunk-AIALG25H.cjs → chunk-7WE6FGGK.cjs} +69 -69
  6. package/dist/{chunk-AIALG25H.cjs.map → chunk-7WE6FGGK.cjs.map} +1 -1
  7. package/dist/{chunk-EZVABFEO.js → chunk-CJW6KZD3.js} +2 -2
  8. package/dist/{chunk-GMQEHDN2.cjs → chunk-E4TK2BBD.cjs} +5 -5
  9. package/dist/{chunk-GMQEHDN2.cjs.map → chunk-E4TK2BBD.cjs.map} +1 -1
  10. package/dist/{chunk-KAEVE7W2.js → chunk-EULY3BXB.js} +2 -2
  11. package/dist/{chunk-HJVKPG55.js → chunk-F227VMPL.js} +125 -15
  12. package/dist/{chunk-HJVKPG55.js.map → chunk-F227VMPL.js.map} +1 -1
  13. package/dist/{chunk-GVM37TRU.js → chunk-GAL6EXUY.js} +4 -4
  14. package/dist/{chunk-ZEZMGXYV.js → chunk-H7K76TPX.js} +5 -5
  15. package/dist/{chunk-U3W2ZVN4.cjs → chunk-HMEEW73F.cjs} +2058 -300
  16. package/dist/chunk-HMEEW73F.cjs.map +1 -0
  17. package/dist/{chunk-LWDIDSCF.js → chunk-IB23CRL2.js} +3 -3
  18. package/dist/{chunk-6U45VX5J.cjs → chunk-IVJ6TBVW.cjs} +43 -43
  19. package/dist/{chunk-6U45VX5J.cjs.map → chunk-IVJ6TBVW.cjs.map} +1 -1
  20. package/dist/{chunk-JW53WPB5.js → chunk-IZGXIZWF.js} +3 -3
  21. package/dist/chunk-JMWLF2GL.cjs +1055 -0
  22. package/dist/chunk-JMWLF2GL.cjs.map +1 -0
  23. package/dist/{chunk-OKA3DJJ3.js → chunk-JUXCEHK2.js} +2 -2
  24. package/dist/{chunk-GUC4JK6O.cjs → chunk-KS7NKONC.cjs} +9 -9
  25. package/dist/{chunk-GUC4JK6O.cjs.map → chunk-KS7NKONC.cjs.map} +1 -1
  26. package/dist/{chunk-WHFEG6UU.cjs → chunk-LHQ6RVFE.cjs} +22 -22
  27. package/dist/{chunk-WHFEG6UU.cjs.map → chunk-LHQ6RVFE.cjs.map} +1 -1
  28. package/dist/{chunk-YU2UJPAT.cjs → chunk-LOJ5KQ6B.cjs} +466 -356
  29. package/dist/chunk-LOJ5KQ6B.cjs.map +1 -0
  30. package/dist/{chunk-BTKMTVN5.cjs → chunk-M3ERSHPM.cjs} +4 -4
  31. package/dist/{chunk-BTKMTVN5.cjs.map → chunk-M3ERSHPM.cjs.map} +1 -1
  32. package/dist/{chunk-KNBXZ22O.cjs → chunk-RBMVTCRM.cjs} +3 -3
  33. package/dist/{chunk-KNBXZ22O.cjs.map → chunk-RBMVTCRM.cjs.map} +1 -1
  34. package/dist/{chunk-PBRXT7TD.cjs → chunk-RILGQ2OY.cjs} +10 -10
  35. package/dist/{chunk-PBRXT7TD.cjs.map → chunk-RILGQ2OY.cjs.map} +1 -1
  36. package/dist/{chunk-DSPQVZFE.cjs → chunk-RQMS2CF5.cjs} +24 -24
  37. package/dist/{chunk-DSPQVZFE.cjs.map → chunk-RQMS2CF5.cjs.map} +1 -1
  38. package/dist/{chunk-W2TXSZTM.cjs → chunk-S2ZXUQLR.cjs} +21 -21
  39. package/dist/{chunk-W2TXSZTM.cjs.map → chunk-S2ZXUQLR.cjs.map} +1 -1
  40. package/dist/{chunk-7CP5KY5W.js → chunk-SM6JAZ4C.js} +3 -3
  41. package/dist/{chunk-3GXNCJZI.js → chunk-T4DIAKIK.js} +3 -3
  42. package/dist/{chunk-UKHM2S6I.cjs → chunk-TMNFC24G.cjs} +15 -15
  43. package/dist/{chunk-UKHM2S6I.cjs.map → chunk-TMNFC24G.cjs.map} +1 -1
  44. package/dist/{chunk-ESWXXWCL.js → chunk-U6GFESVF.js} +2 -2
  45. package/dist/{chunk-VXUUSLD5.js → chunk-XA5UPYWE.js} +2669 -911
  46. package/dist/chunk-XA5UPYWE.js.map +1 -0
  47. package/dist/{chunk-SEZ25E22.cjs → chunk-XNSEZVV4.cjs} +10 -10
  48. package/dist/{chunk-SEZ25E22.cjs.map → chunk-XNSEZVV4.cjs.map} +1 -1
  49. package/dist/{chunk-VDRCKHML.cjs → chunk-YU4QZ6I6.cjs} +3 -3
  50. package/dist/{chunk-VDRCKHML.cjs.map → chunk-YU4QZ6I6.cjs.map} +1 -1
  51. package/dist/{chunk-GAHKWML7.js → chunk-ZSENCE55.js} +6 -6
  52. package/dist/components/case-studies/index.cjs +9 -9
  53. package/dist/components/case-studies/index.js +3 -3
  54. package/dist/components/chat/index.cjs +2 -2
  55. package/dist/components/chat/index.js +1 -1
  56. package/dist/components/contact/index.cjs +4 -4
  57. package/dist/components/contact/index.js +3 -3
  58. package/dist/components/docs/index.cjs +7 -7
  59. package/dist/components/docs/index.js +6 -6
  60. package/dist/components/embeds/index.cjs +4 -4
  61. package/dist/components/embeds/index.js +3 -3
  62. package/dist/components/faq/index.cjs +4 -4
  63. package/dist/components/faq/index.js +3 -3
  64. package/dist/components/features/index.cjs +5 -3
  65. package/dist/components/features/index.cjs.map +1 -1
  66. package/dist/components/features/index.js +4 -2
  67. package/dist/components/features/push-button-selector.d.ts +29 -1
  68. package/dist/components/features/push-button-selector.d.ts.map +1 -1
  69. package/dist/components/features/video.d.ts +12 -0
  70. package/dist/components/features/video.d.ts.map +1 -1
  71. package/dist/components/help-center-pages/index.cjs +23 -23
  72. package/dist/components/help-center-pages/index.js +14 -14
  73. package/dist/components/index.cjs +70 -899
  74. package/dist/components/index.cjs.map +1 -1
  75. package/dist/components/index.js +247 -1076
  76. package/dist/components/index.js.map +1 -1
  77. package/dist/components/meeting-scheduler/index.cjs +21 -21
  78. package/dist/components/meeting-scheduler/index.js +2 -2
  79. package/dist/components/navigation/index.cjs +6 -6
  80. package/dist/components/navigation/index.js +5 -5
  81. package/dist/components/onboarding-guides/index.cjs +6 -6
  82. package/dist/components/onboarding-guides/index.js +5 -5
  83. package/dist/components/tickets/index.cjs +7 -7
  84. package/dist/components/tickets/index.js +6 -6
  85. package/dist/components/ui/index.cjs +3 -3
  86. package/dist/components/ui/index.js +2 -2
  87. package/dist/index.cjs +9 -7
  88. package/dist/index.cjs.map +1 -1
  89. package/dist/index.js +8 -6
  90. package/dist/index.js.map +1 -1
  91. package/package.json +1 -1
  92. package/src/components/features/.push-button-selector.md +1 -1
  93. package/src/components/features/__tests__/video-playback-failure.test.tsx +250 -0
  94. package/src/components/features/push-button-selector.tsx +142 -17
  95. package/src/components/features/video.tsx +198 -16
  96. package/dist/chunk-IHFQ5VVR.js +0 -1867
  97. package/dist/chunk-IHFQ5VVR.js.map +0 -1
  98. package/dist/chunk-M4KQWGEL.cjs +0 -1867
  99. package/dist/chunk-M4KQWGEL.cjs.map +0 -1
  100. package/dist/chunk-U3W2ZVN4.cjs.map +0 -1
  101. package/dist/chunk-VXUUSLD5.js.map +0 -1
  102. package/dist/chunk-YU2UJPAT.cjs.map +0 -1
  103. /package/dist/{chunk-HVSZYUGP.js.map → chunk-53ZVUW2M.js.map} +0 -0
  104. /package/dist/{chunk-R5JVRFSW.js.map → chunk-5U2FU5LM.js.map} +0 -0
  105. /package/dist/{chunk-EZVABFEO.js.map → chunk-CJW6KZD3.js.map} +0 -0
  106. /package/dist/{chunk-KAEVE7W2.js.map → chunk-EULY3BXB.js.map} +0 -0
  107. /package/dist/{chunk-GVM37TRU.js.map → chunk-GAL6EXUY.js.map} +0 -0
  108. /package/dist/{chunk-ZEZMGXYV.js.map → chunk-H7K76TPX.js.map} +0 -0
  109. /package/dist/{chunk-LWDIDSCF.js.map → chunk-IB23CRL2.js.map} +0 -0
  110. /package/dist/{chunk-JW53WPB5.js.map → chunk-IZGXIZWF.js.map} +0 -0
  111. /package/dist/{chunk-OKA3DJJ3.js.map → chunk-JUXCEHK2.js.map} +0 -0
  112. /package/dist/{chunk-7CP5KY5W.js.map → chunk-SM6JAZ4C.js.map} +0 -0
  113. /package/dist/{chunk-3GXNCJZI.js.map → chunk-T4DIAKIK.js.map} +0 -0
  114. /package/dist/{chunk-ESWXXWCL.js.map → chunk-U6GFESVF.js.map} +0 -0
  115. /package/dist/{chunk-GAHKWML7.js.map → chunk-ZSENCE55.js.map} +0 -0
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  SoftwareInfo,
10
10
  SoftwareSourceBadge,
11
11
  ToolBadge
12
- } from "./chunk-3GXNCJZI.js";
12
+ } from "./chunk-T4DIAKIK.js";
13
13
  import {
14
14
  AppHeader,
15
15
  AppLayout,
@@ -28,14 +28,14 @@ import {
28
28
  SlidingSidebar,
29
29
  TicketAlertsButton,
30
30
  TopNavigation
31
- } from "./chunk-GVM37TRU.js";
31
+ } from "./chunk-GAL6EXUY.js";
32
32
  import {
33
33
  MobileNavigationDropdown,
34
34
  MultiLevelNavigation,
35
35
  StickySectionNav,
36
36
  useSectionNavigation
37
- } from "./chunk-KAEVE7W2.js";
38
- import "./chunk-R5JVRFSW.js";
37
+ } from "./chunk-EULY3BXB.js";
38
+ import "./chunk-5U2FU5LM.js";
39
39
  import {
40
40
  ADMIN_APPROVAL_REQUEST_CONTEXT_TYPE,
41
41
  AIEnrichButton,
@@ -577,7 +577,7 @@ import {
577
577
  withFirstTouchAttribution,
578
578
  withWalkthroughOpenParam,
579
579
  writeDismissCookie
580
- } from "./chunk-VXUUSLD5.js";
580
+ } from "./chunk-XA5UPYWE.js";
581
581
  import "./chunk-L2SLFCXP.js";
582
582
  import "./chunk-BJXDHKUX.js";
583
583
  import {
@@ -892,6 +892,7 @@ import {
892
892
  TooltipProvider,
893
893
  TooltipTrigger,
894
894
  UnarchiveChatModal,
895
+ VIDEO_PLAYBACK_FAILED_EVENT,
895
896
  Video,
896
897
  VideoBiteCard,
897
898
  VideoBitesStrip,
@@ -1054,7 +1055,7 @@ import {
1054
1055
  useVideoOriginPreconnect,
1055
1056
  useVideoWarmup,
1056
1057
  usesCanonicalStatusStyle
1057
- } from "./chunk-HJVKPG55.js";
1058
+ } from "./chunk-F227VMPL.js";
1058
1059
  import {
1059
1060
  HEADING_EMOJI_RE,
1060
1061
  HUB_HEADER_OFFSET_PX,
@@ -2504,6 +2505,7 @@ export {
2504
2505
  UserIcon,
2505
2506
  UsersGroupIcon,
2506
2507
  UsersIcon,
2508
+ VIDEO_PLAYBACK_FAILED_EVENT,
2507
2509
  VendorDirectoryIcon,
2508
2510
  VendorShowcaseMainIcon,
2509
2511
  VendorsIcon,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/logs-list.tsx","../src/components/log-severity-dot.tsx","../src/types/logs.types.ts","../src/assets/index.ts"],"sourcesContent":["import { type FC, forwardRef, useRef } from 'react';\nimport type { LogsListProps, LogEntry } from '../types/logs.types';\nimport { cn } from '../utils/cn';\nimport { LogSeverityDot } from './log-severity-dot';\nimport { ToolIcon } from './tool-icon';\n\nconst formatTimestamp = (timestamp: string | Date): string => {\n const date = timestamp instanceof Date ? timestamp : new Date(timestamp);\n\n // UTC getters so the timestamp is identical on server (UTC) and client\n // (local) — otherwise React #418 hydration mismatch.\n const year = date.getUTCFullYear();\n const month = String(date.getUTCMonth() + 1).padStart(2, '0');\n const day = String(date.getUTCDate()).padStart(2, '0');\n const hours = String(date.getUTCHours()).padStart(2, '0');\n const minutes = String(date.getUTCMinutes()).padStart(2, '0');\n\n return `${year}/${month}/${day},${hours}:${minutes}`;\n};\n\nconst LogCard: FC<{\n log: LogEntry;\n isLast: boolean;\n showConnector: boolean;\n onClick?: () => void;\n}> = ({ log, isLast, showConnector, onClick }) => {\n return (\n <div className=\"relative\">\n <div\n className={cn(\n 'relative box-border flex w-full items-start gap-3 rounded px-1 py-2',\n 'cursor-pointer transition-colors hover:bg-ods-bg-hover/50',\n )}\n onClick={onClick}\n role=\"button\"\n tabIndex={0}\n onKeyDown={e => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }}\n >\n <LogSeverityDot severity={log.severity} size=\"md\" />\n\n <div className=\"min-w-0 flex-1 space-y-1\">\n <p className=\"text-ods-text-primary text-h6\">{log.title}</p>\n <div className=\"flex items-center gap-2\">\n <p className=\"text-ods-text-secondary text-h5\">{formatTimestamp(log.timestamp)}</p>\n {log.toolType && <ToolIcon toolType={log.toolType} size={16} />}\n </div>\n </div>\n </div>\n\n {showConnector && !isLast && (\n <div\n className=\"absolute left-[15px] w-[2px] bg-ods-border\"\n style={{\n top: '28px',\n bottom: '-10px',\n }}\n aria-hidden=\"true\"\n />\n )}\n </div>\n );\n};\n\nexport const LogsList = forwardRef<HTMLDivElement, LogsListProps>(\n (\n {\n logs,\n maxHeight = '400px',\n showConnector = true,\n onLogClick,\n loading = false,\n emptyMessage = 'No logs to display',\n className,\n },\n ref,\n ) => {\n const containerRef = useRef<HTMLDivElement>(null);\n\n const isFullHeight = maxHeight === '100%';\n\n const getContainerStyles = () => {\n if (isFullHeight) return undefined;\n return { maxHeight, minHeight: '200px' };\n };\n\n const getContainerClasses = () => {\n if (isFullHeight) return 'h-full';\n return '';\n };\n\n if (loading) {\n return (\n <div\n ref={ref}\n className={cn(\n 'rounded-lg border border-ods-border bg-ods-card p-4',\n 'flex items-center justify-center',\n getContainerClasses(),\n className,\n )}\n style={getContainerStyles()}\n >\n <div className=\"text-ods-text-muted text-h6\">Loading logs...</div>\n </div>\n );\n }\n\n if (logs.length === 0) {\n return (\n <div\n ref={ref}\n className={cn(\n 'rounded-lg border border-ods-border bg-ods-card p-4',\n 'flex items-center justify-center',\n getContainerClasses(),\n className,\n )}\n style={getContainerStyles()}\n >\n <div className=\"text-ods-text-muted text-h6\">{emptyMessage}</div>\n </div>\n );\n }\n\n return (\n <div\n ref={ref}\n className={cn(\n 'relative rounded-lg border border-ods-border bg-ods-card',\n 'flex flex-col overflow-hidden',\n getContainerClasses(),\n className,\n )}\n style={getContainerStyles()}\n >\n <div ref={containerRef} className=\"flex-1 overflow-y-auto px-4 py-3\">\n {logs.map((log, index) => (\n <LogCard\n key={log.id}\n log={log}\n isLast={index === logs.length - 1}\n showConnector={showConnector}\n onClick={() => onLogClick?.(log)}\n />\n ))}\n </div>\n </div>\n );\n },\n);\n\nLogsList.displayName = 'LogsList';\n","import { forwardRef } from 'react';\nimport { type LogSeverityDotProps, LOG_SEVERITY_COLORS, LOG_SEVERITY_LABELS } from '../types/logs.types';\nimport { cn } from '../utils/cn';\n\nexport const LogSeverityDot = forwardRef<HTMLDivElement, LogSeverityDotProps>(\n ({ severity, size = 'md', className }, ref) => {\n const sizeClasses = {\n sm: 'w-4 h-4',\n md: 'w-6 h-6',\n lg: 'w-8 h-8',\n };\n\n const dotSizeClasses = {\n sm: 'w-2 h-2',\n md: 'w-3 h-3',\n lg: 'w-4 h-4',\n };\n\n return (\n <div\n ref={ref}\n className={cn('relative flex shrink-0 items-center justify-center', sizeClasses[size], className)}\n aria-label={`${LOG_SEVERITY_LABELS[severity]} severity`}\n >\n <div\n className={cn('rounded-full', dotSizeClasses[size])}\n style={{ backgroundColor: LOG_SEVERITY_COLORS[severity] }}\n />\n </div>\n );\n },\n);\n\nLogSeverityDot.displayName = 'LogSeverityDot';\n","import type { ToolType } from './tool.types';\n\nexport type LogSeverity = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL';\n\nexport interface LogEntry {\n id: string;\n ingestDay: string;\n eventType: string;\n severity: LogSeverity;\n title: string;\n timestamp: string | Date;\n toolType?: ToolType;\n toolIcon?: string;\n message?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface LogsListProps {\n logs: LogEntry[];\n maxHeight?: string | number;\n showConnector?: boolean;\n onLogClick?: (log: LogEntry) => void;\n loading?: boolean;\n emptyMessage?: string;\n className?: string;\n}\n\nexport interface LogSeverityDotProps {\n severity: LogSeverity;\n size?: 'sm' | 'md' | 'lg';\n className?: string;\n}\n\nexport const LOG_SEVERITY_COLORS: Record<LogSeverity, string> = {\n DEBUG: '#888888',\n INFO: '#888888',\n WARNING: '#e1b32f',\n ERROR: '#f36666',\n CRITICAL: '#b43b3b',\n};\n\nexport const LOG_SEVERITY_LABELS: Record<LogSeverity, string> = {\n DEBUG: 'Debug',\n INFO: 'Info',\n WARNING: 'Warning',\n ERROR: 'Error',\n CRITICAL: 'Critical',\n};\n","// Assets exports\n// Font files and other static assets\n// Platform utilities will be available when needed\n\nexport const assets = {} as const;\nexport * from './fae-avatar';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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","names":["forwardRef","jsx","forwardRef"]}
1
+ {"version":3,"sources":["../src/components/logs-list.tsx","../src/components/log-severity-dot.tsx","../src/types/logs.types.ts","../src/assets/index.ts"],"sourcesContent":["import { type FC, forwardRef, useRef } from 'react';\nimport type { LogsListProps, LogEntry } from '../types/logs.types';\nimport { cn } from '../utils/cn';\nimport { LogSeverityDot } from './log-severity-dot';\nimport { ToolIcon } from './tool-icon';\n\nconst formatTimestamp = (timestamp: string | Date): string => {\n const date = timestamp instanceof Date ? timestamp : new Date(timestamp);\n\n // UTC getters so the timestamp is identical on server (UTC) and client\n // (local) — otherwise React #418 hydration mismatch.\n const year = date.getUTCFullYear();\n const month = String(date.getUTCMonth() + 1).padStart(2, '0');\n const day = String(date.getUTCDate()).padStart(2, '0');\n const hours = String(date.getUTCHours()).padStart(2, '0');\n const minutes = String(date.getUTCMinutes()).padStart(2, '0');\n\n return `${year}/${month}/${day},${hours}:${minutes}`;\n};\n\nconst LogCard: FC<{\n log: LogEntry;\n isLast: boolean;\n showConnector: boolean;\n onClick?: () => void;\n}> = ({ log, isLast, showConnector, onClick }) => {\n return (\n <div className=\"relative\">\n <div\n className={cn(\n 'relative box-border flex w-full items-start gap-3 rounded px-1 py-2',\n 'cursor-pointer transition-colors hover:bg-ods-bg-hover/50',\n )}\n onClick={onClick}\n role=\"button\"\n tabIndex={0}\n onKeyDown={e => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }}\n >\n <LogSeverityDot severity={log.severity} size=\"md\" />\n\n <div className=\"min-w-0 flex-1 space-y-1\">\n <p className=\"text-ods-text-primary text-h6\">{log.title}</p>\n <div className=\"flex items-center gap-2\">\n <p className=\"text-ods-text-secondary text-h5\">{formatTimestamp(log.timestamp)}</p>\n {log.toolType && <ToolIcon toolType={log.toolType} size={16} />}\n </div>\n </div>\n </div>\n\n {showConnector && !isLast && (\n <div\n className=\"absolute left-[15px] w-[2px] bg-ods-border\"\n style={{\n top: '28px',\n bottom: '-10px',\n }}\n aria-hidden=\"true\"\n />\n )}\n </div>\n );\n};\n\nexport const LogsList = forwardRef<HTMLDivElement, LogsListProps>(\n (\n {\n logs,\n maxHeight = '400px',\n showConnector = true,\n onLogClick,\n loading = false,\n emptyMessage = 'No logs to display',\n className,\n },\n ref,\n ) => {\n const containerRef = useRef<HTMLDivElement>(null);\n\n const isFullHeight = maxHeight === '100%';\n\n const getContainerStyles = () => {\n if (isFullHeight) return undefined;\n return { maxHeight, minHeight: '200px' };\n };\n\n const getContainerClasses = () => {\n if (isFullHeight) return 'h-full';\n return '';\n };\n\n if (loading) {\n return (\n <div\n ref={ref}\n className={cn(\n 'rounded-lg border border-ods-border bg-ods-card p-4',\n 'flex items-center justify-center',\n getContainerClasses(),\n className,\n )}\n style={getContainerStyles()}\n >\n <div className=\"text-ods-text-muted text-h6\">Loading logs...</div>\n </div>\n );\n }\n\n if (logs.length === 0) {\n return (\n <div\n ref={ref}\n className={cn(\n 'rounded-lg border border-ods-border bg-ods-card p-4',\n 'flex items-center justify-center',\n getContainerClasses(),\n className,\n )}\n style={getContainerStyles()}\n >\n <div className=\"text-ods-text-muted text-h6\">{emptyMessage}</div>\n </div>\n );\n }\n\n return (\n <div\n ref={ref}\n className={cn(\n 'relative rounded-lg border border-ods-border bg-ods-card',\n 'flex flex-col overflow-hidden',\n getContainerClasses(),\n className,\n )}\n style={getContainerStyles()}\n >\n <div ref={containerRef} className=\"flex-1 overflow-y-auto px-4 py-3\">\n {logs.map((log, index) => (\n <LogCard\n key={log.id}\n log={log}\n isLast={index === logs.length - 1}\n showConnector={showConnector}\n onClick={() => onLogClick?.(log)}\n />\n ))}\n </div>\n </div>\n );\n },\n);\n\nLogsList.displayName = 'LogsList';\n","import { forwardRef } from 'react';\nimport { type LogSeverityDotProps, LOG_SEVERITY_COLORS, LOG_SEVERITY_LABELS } from '../types/logs.types';\nimport { cn } from '../utils/cn';\n\nexport const LogSeverityDot = forwardRef<HTMLDivElement, LogSeverityDotProps>(\n ({ severity, size = 'md', className }, ref) => {\n const sizeClasses = {\n sm: 'w-4 h-4',\n md: 'w-6 h-6',\n lg: 'w-8 h-8',\n };\n\n const dotSizeClasses = {\n sm: 'w-2 h-2',\n md: 'w-3 h-3',\n lg: 'w-4 h-4',\n };\n\n return (\n <div\n ref={ref}\n className={cn('relative flex shrink-0 items-center justify-center', sizeClasses[size], className)}\n aria-label={`${LOG_SEVERITY_LABELS[severity]} severity`}\n >\n <div\n className={cn('rounded-full', dotSizeClasses[size])}\n style={{ backgroundColor: LOG_SEVERITY_COLORS[severity] }}\n />\n </div>\n );\n },\n);\n\nLogSeverityDot.displayName = 'LogSeverityDot';\n","import type { ToolType } from './tool.types';\n\nexport type LogSeverity = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL';\n\nexport interface LogEntry {\n id: string;\n ingestDay: string;\n eventType: string;\n severity: LogSeverity;\n title: string;\n timestamp: string | Date;\n toolType?: ToolType;\n toolIcon?: string;\n message?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface LogsListProps {\n logs: LogEntry[];\n maxHeight?: string | number;\n showConnector?: boolean;\n onLogClick?: (log: LogEntry) => void;\n loading?: boolean;\n emptyMessage?: string;\n className?: string;\n}\n\nexport interface LogSeverityDotProps {\n severity: LogSeverity;\n size?: 'sm' | 'md' | 'lg';\n className?: string;\n}\n\nexport const LOG_SEVERITY_COLORS: Record<LogSeverity, string> = {\n DEBUG: '#888888',\n INFO: '#888888',\n WARNING: '#e1b32f',\n ERROR: '#f36666',\n CRITICAL: '#b43b3b',\n};\n\nexport const LOG_SEVERITY_LABELS: Record<LogSeverity, string> = {\n DEBUG: 'Debug',\n INFO: 'Info',\n WARNING: 'Warning',\n ERROR: 'Error',\n CRITICAL: 'Critical',\n};\n","// Assets exports\n// Font files and other static assets\n// Platform utilities will be available when needed\n\nexport const assets = {} as const;\nexport * from './fae-avatar';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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","names":["forwardRef","jsx","forwardRef"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.581",
3
+ "version": "0.0.582-1958.5391.1",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,4 +1,4 @@
1
- <!-- source-hash: 1b470f4e084b6cb0c7cd286c460838ca -->
1
+ <!-- source-hash: 0ccfb311d1bb254bbfb9ffdb994a9af1 -->
2
2
  A client-side React component that renders a list of toggleable push-button options with support for single/multi-select, section grouping, disabled states with tooltips, loading skeletons, and error handling.
3
3
 
4
4
  ## Key Components
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Playback-failure handling for the shared `<Video>`.
3
+ *
4
+ * Two branches: the file player (MuxPlayer) via a fake element that fires the
5
+ * media events the watchdog listens to, and the YouTube facade via the
6
+ * postMessage channel it subscribes to. Both assert the same contract — a
7
+ * failure surfaces a reachable retry AND is reported once.
8
+ */
9
+ import { render, screen, act, fireEvent } from '@testing-library/react';
10
+ import { forwardRef, useImperativeHandle } from 'react';
11
+ import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
12
+
13
+ interface FakeMuxEl {
14
+ paused: boolean;
15
+ ended: boolean;
16
+ currentTime: number;
17
+ loadCalls: number;
18
+ playCalls: number;
19
+ listeners: Record<string, Array<() => void>>;
20
+ emit: (type: string) => void;
21
+ }
22
+
23
+ let muxProps: Record<string, (() => void) | undefined> = {};
24
+ let muxEl: FakeMuxEl | null = null;
25
+
26
+ vi.mock('@mux/mux-player-react', () => {
27
+ const Fake = forwardRef((props: Record<string, unknown>, ref: React.Ref<unknown>) => {
28
+ muxProps = props as Record<string, (() => void) | undefined>;
29
+ useImperativeHandle(ref, () => {
30
+ const listeners: Record<string, Array<() => void>> = {};
31
+ const el = {
32
+ paused: false,
33
+ ended: false,
34
+ loadCalls: 0,
35
+ playCalls: 0,
36
+ muted: false,
37
+ volume: 1,
38
+ currentTime: 0,
39
+ duration: 100,
40
+ listeners,
41
+ play: () => {
42
+ el.playCalls += 1;
43
+ el.paused = false;
44
+ return Promise.resolve();
45
+ },
46
+ pause: () => {
47
+ el.paused = true;
48
+ },
49
+ // Mirrors the real element: a reload rewinds to zero.
50
+ load: () => {
51
+ el.loadCalls += 1;
52
+ el.currentTime = 0;
53
+ },
54
+ addEventListener: (type: string, fn: () => void) => {
55
+ (listeners[type] ??= []).push(fn);
56
+ },
57
+ removeEventListener: (type: string, fn: () => void) => {
58
+ listeners[type] = (listeners[type] ?? []).filter(l => l !== fn);
59
+ },
60
+ emit: (type: string) => {
61
+ for (const fn of [...(listeners[type] ?? [])]) fn();
62
+ },
63
+ };
64
+ muxEl = el;
65
+ return el;
66
+ }, []);
67
+ return <div data-testid="mux" />;
68
+ });
69
+ Fake.displayName = 'FakeMuxPlayer';
70
+ return { default: Fake };
71
+ });
72
+
73
+ import { Video, VIDEO_PLAYBACK_FAILED_EVENT } from '../video';
74
+
75
+ const YT_ORIGIN = 'https://www.youtube-nocookie.com';
76
+ const ERROR_TEXT = 'This video failed to play.';
77
+
78
+ function ytMessage(body: unknown): void {
79
+ window.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(body), origin: YT_ORIGIN }));
80
+ }
81
+
82
+ function failureEvents(spy: { mock: { calls: unknown[][] } }): CustomEvent[] {
83
+ return spy.mock.calls
84
+ .map(call => call[0])
85
+ .filter((e): e is CustomEvent => e instanceof CustomEvent && e.type === VIDEO_PLAYBACK_FAILED_EVENT);
86
+ }
87
+
88
+ describe('Video — file playback failure handling', () => {
89
+ beforeEach(() => {
90
+ vi.useFakeTimers();
91
+ muxProps = {};
92
+ muxEl = null;
93
+ });
94
+ afterEach(() => {
95
+ vi.runOnlyPendingTimers();
96
+ vi.useRealTimers();
97
+ // Re-spying on a live spy returns it with its calls intact — restore, or
98
+ // the next test counts this one's events.
99
+ vi.restoreAllMocks();
100
+ });
101
+
102
+ it('a mid-playback stall surfaces a retry and reports the failure once', () => {
103
+ const dispatched = vi.spyOn(window, 'dispatchEvent');
104
+ render(<Video kind="file" url="https://example.com/a.m3u8" layout="centered" />);
105
+
106
+ // Buffering while playback is expected — the watchdog arms.
107
+ act(() => muxProps.onWaiting?.());
108
+ expect(screen.queryByText(ERROR_TEXT)).toBeNull();
109
+
110
+ // No progress within the window → failure surfaces.
111
+ act(() => {
112
+ vi.advanceTimersByTime(15_000);
113
+ });
114
+ expect(screen.getByText(ERROR_TEXT)).toBeInTheDocument();
115
+ expect(screen.getByRole('button', { name: 'Try again' })).toBeInTheDocument();
116
+
117
+ const events = failureEvents(dispatched);
118
+ expect(events).toHaveLength(1);
119
+ expect(events[0].detail).toMatchObject({ kind: 'file', reason: 'stall' });
120
+ });
121
+
122
+ it('Try again reloads and resumes the element, clearing the error', () => {
123
+ render(<Video kind="file" url="https://example.com/a.m3u8" layout="centered" />);
124
+ act(() => muxProps.onWaiting?.());
125
+ act(() => {
126
+ vi.advanceTimersByTime(15_000);
127
+ });
128
+
129
+ fireEvent.click(screen.getByRole('button', { name: 'Try again' }));
130
+ expect(muxEl?.loadCalls).toBe(1);
131
+ expect(muxEl?.playCalls).toBe(1);
132
+ expect(screen.queryByText(ERROR_TEXT)).toBeNull();
133
+ });
134
+
135
+ it('Try again resumes from where the stall happened, not from the top', () => {
136
+ render(<Video kind="file" url="https://example.com/a.m3u8" layout="centered" />);
137
+ if (muxEl) muxEl.currentTime = 480;
138
+ act(() => muxProps.onWaiting?.());
139
+ act(() => {
140
+ vi.advanceTimersByTime(15_000);
141
+ });
142
+
143
+ fireEvent.click(screen.getByRole('button', { name: 'Try again' }));
144
+ expect(muxEl?.currentTime).toBe(0); // reload rewound it
145
+ act(() => muxEl?.emit('loadedmetadata'));
146
+ expect(muxEl?.currentTime).toBe(480);
147
+ // One-shot: a later reload must not drag the old position back.
148
+ expect(muxEl?.listeners.loadedmetadata).toHaveLength(0);
149
+ });
150
+
151
+ it('a hard media error surfaces the retry immediately', () => {
152
+ render(<Video kind="file" url="https://example.com/a.m3u8" layout="centered" />);
153
+ act(() => muxProps.onError?.());
154
+ expect(screen.getByText(ERROR_TEXT)).toBeInTheDocument();
155
+ });
156
+
157
+ it('a stall that self-heals dismisses its own overlay', () => {
158
+ render(<Video kind="file" url="https://example.com/a.m3u8" layout="centered" />);
159
+ act(() => muxProps.onWaiting?.());
160
+ act(() => {
161
+ vi.advanceTimersByTime(15_000);
162
+ });
163
+ expect(screen.getByText(ERROR_TEXT)).toBeInTheDocument();
164
+
165
+ act(() => muxProps.onPlaying?.());
166
+ expect(screen.queryByText(ERROR_TEXT)).toBeNull();
167
+ });
168
+
169
+ it('a paused player is never treated as a stall', () => {
170
+ render(<Video kind="file" url="https://example.com/a.m3u8" layout="centered" />);
171
+ if (muxEl) muxEl.paused = true;
172
+ act(() => muxProps.onWaiting?.());
173
+ act(() => {
174
+ vi.advanceTimersByTime(15_000);
175
+ });
176
+ expect(screen.queryByText(ERROR_TEXT)).toBeNull();
177
+ });
178
+
179
+ it('decorative first-frame previews stay silent on error', () => {
180
+ render(<Video kind="file" url="https://example.com/a.m3u8" firstFrameOnly />);
181
+ act(() => muxProps.onError?.());
182
+ expect(screen.queryByText(ERROR_TEXT)).toBeNull();
183
+ });
184
+ });
185
+
186
+ describe('Video — YouTube facade failure handling', () => {
187
+ beforeEach(() => {
188
+ vi.useFakeTimers();
189
+ });
190
+ afterEach(() => {
191
+ vi.runOnlyPendingTimers();
192
+ vi.useRealTimers();
193
+ vi.restoreAllMocks();
194
+ });
195
+
196
+ function activate(): void {
197
+ render(<Video kind="youtube" url="dQw4w9WgXcQ" />);
198
+ fireEvent.click(screen.getByRole('button', { name: /Play:/ }));
199
+ }
200
+
201
+ it('an explicit embed error surfaces retry + a YouTube fallback link', () => {
202
+ const dispatched = vi.spyOn(window, 'dispatchEvent');
203
+ activate();
204
+ act(() => ytMessage({ event: 'onError', info: 150 }));
205
+
206
+ expect(screen.getByText(ERROR_TEXT)).toBeInTheDocument();
207
+ expect(screen.getByRole('link', { name: 'Watch on YouTube' })).toHaveAttribute(
208
+ 'href',
209
+ 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
210
+ );
211
+ expect(failureEvents(dispatched)[0].detail).toMatchObject({
212
+ kind: 'youtube',
213
+ reason: 'error',
214
+ errorCode: 150,
215
+ });
216
+ });
217
+
218
+ // Regression guard: a silent embed is NOT a failed one. Safari/iOS block the
219
+ // unmuted autoplay and wait for a click inside the iframe, and a broken jsapi
220
+ // channel reports nothing at all — a readiness timeout would tear both down.
221
+ it('a silent embed is left alone, however long it stays silent', () => {
222
+ const dispatched = vi.spyOn(window, 'dispatchEvent');
223
+ activate();
224
+ act(() => {
225
+ vi.advanceTimersByTime(120_000);
226
+ });
227
+
228
+ expect(screen.queryByText(ERROR_TEXT)).toBeNull();
229
+ expect(screen.getByTitle('YouTube Video')).toBeInTheDocument();
230
+ expect(failureEvents(dispatched)).toHaveLength(0);
231
+ });
232
+
233
+ it('reaching PLAYING keeps the embed and never shows the error', () => {
234
+ activate();
235
+ act(() => ytMessage({ event: 'infoDelivery', info: { playerState: 1 } }));
236
+ act(() => {
237
+ vi.advanceTimersByTime(12_000);
238
+ });
239
+ expect(screen.queryByText(ERROR_TEXT)).toBeNull();
240
+ });
241
+
242
+ it('Try again remounts the embed', () => {
243
+ activate();
244
+ act(() => ytMessage({ event: 'onError', info: 150 }));
245
+ fireEvent.click(screen.getByRole('button', { name: 'Try again' }));
246
+ expect(screen.queryByText(ERROR_TEXT)).toBeNull();
247
+ expect(screen.queryByRole('link', { name: 'Watch on YouTube' })).toBeNull();
248
+ expect(screen.getByTitle('YouTube Video')).toBeInTheDocument();
249
+ });
250
+ });
@@ -2,7 +2,9 @@
2
2
 
3
3
  import { Check } from 'lucide-react';
4
4
  import type React from 'react';
5
- import { Badge } from '../ui';
5
+ import { useEffect, useRef } from 'react';
6
+ import { UnifiedSkeleton } from '../loading';
7
+ import { Badge, Input } from '../ui';
6
8
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
7
9
 
8
10
  export interface SelectableOption {
@@ -27,6 +29,28 @@ export interface SectionDefinition {
27
29
  description?: string; // Optional description shown under section label
28
30
  }
29
31
 
32
+ /** SERVER-SIDE search: the selector renders the input; the PARENT owns the
33
+ * query state and re-fetches `options` (never a client-side .filter of a
34
+ * pre-fetched list). While `isLoading`, the input stays mounted (typing
35
+ * must not lose focus to a skeleton swap). */
36
+ export interface PushButtonSelectorSearch {
37
+ value: string;
38
+ onChange: (value: string) => void;
39
+ placeholder?: string;
40
+ ariaLabel?: string;
41
+ }
42
+
43
+ /** Lazy paging: the option list becomes a FIXED-HEIGHT scroll viewport with
44
+ * an IntersectionObserver sentinel near the bottom — the selector calls
45
+ * `onLoadMore` as the user scrolls; the parent appends to `options`. */
46
+ export interface PushButtonSelectorLazyLoad {
47
+ hasMore: boolean;
48
+ isFetchingMore: boolean;
49
+ onLoadMore: () => void;
50
+ /** CSS height of the scroll viewport (default '336px' ≈ 6 rows). */
51
+ maxHeight?: string;
52
+ }
53
+
30
54
  interface PushButtonSelectorProps {
31
55
  options: SelectableOption[];
32
56
  selectedIds: string[];
@@ -41,25 +65,37 @@ interface PushButtonSelectorProps {
41
65
  error?: string | null;
42
66
  skeletonCount?: number;
43
67
  sections?: SectionDefinition[]; // Optional sections for grouping options
68
+ /** Built-in server-side search input (see PushButtonSelectorSearch). */
69
+ search?: PushButtonSelectorSearch;
70
+ /** Built-in fixed-height scroll + infinite paging (see PushButtonSelectorLazyLoad). */
71
+ lazyLoad?: PushButtonSelectorLazyLoad;
72
+ /** Rendered inside the list when `options` is empty and not loading. */
73
+ emptyMessage?: React.ReactNode;
74
+ /** Rendered under the list (result counts, hints). */
75
+ footer?: React.ReactNode;
44
76
  }
45
77
 
46
- // Skeleton component matching external pattern from announcement-form.tsx
78
+ // Composed from the app-wide UnifiedSkeleton system (components/loading) —
79
+ // the same base every other loading surface uses (bg-ods-skeleton pulse,
80
+ // prefers-reduced-motion support, role="status" a11y). Each skeleton row
81
+ // mirrors a LOADED option row exactly (bg-ods-bg card, p-4, icon + two
82
+ // 20px-tall text lines + selection box) so the list never jumps on load.
47
83
  function PushButtonSelectorSkeleton({ count = 3, hasTitle }: { count?: number; hasTitle?: boolean }) {
48
84
  return (
49
- <div className="space-y-3">
50
- {hasTitle && <div className="h-5 w-20 animate-pulse rounded bg-ods-skeleton" />}
85
+ <div className="space-y-3" role="status" aria-label="Loading options">
86
+ {hasTitle && <UnifiedSkeleton className="h-5 w-20" aria-label="Loading title" />}
51
87
  <div className="space-y-3">
52
88
  {Array.from({ length: count }, (_, i) => (
53
- <div key={i} className="animate-pulse rounded-lg border border-ods-border bg-ods-skeleton p-4">
54
- <div className="flex items-center justify-between">
89
+ <div key={i} className="rounded-lg border border-ods-border bg-ods-bg p-4">
90
+ <div className="flex items-center justify-between gap-4">
55
91
  <div className="flex items-center gap-3">
56
- <div className="h-8 w-8 rounded bg-ods-skeleton" />
92
+ <UnifiedSkeleton className="h-8 w-8" aria-label="Loading icon" />
57
93
  <div>
58
- <div className="mb-1 h-4 w-20 rounded bg-ods-skeleton" />
59
- <div className="h-3 w-32 rounded bg-ods-skeleton" />
94
+ <UnifiedSkeleton variant="text" className="mb-2 h-4 w-24" aria-label="Loading name" />
95
+ <UnifiedSkeleton variant="text" className="h-4 w-36" aria-label="Loading description" />
60
96
  </div>
61
97
  </div>
62
- <div className="h-6 w-6 rounded border-2 border-ods-border bg-ods-skeleton" />
98
+ <UnifiedSkeleton className="h-6 w-6" aria-label="Loading selection state" />
63
99
  </div>
64
100
  </div>
65
101
  ))}
@@ -94,18 +130,68 @@ export function PushButtonSelector({
94
130
  error = null,
95
131
  skeletonCount = 3,
96
132
  sections,
133
+ search,
134
+ lazyLoad,
135
+ emptyMessage,
136
+ footer,
97
137
  }: PushButtonSelectorProps) {
98
- // LOADING STATE
99
- if (isLoading) {
138
+ // Lazy-load sentinel: observe within the scroll viewport so onLoadMore
139
+ // fires as the user approaches the bottom. Hooks run unconditionally
140
+ // (before the early returns) per the rules of hooks. Pass a STABLE
141
+ // onLoadMore (react-query's fetchNextPage is) — an inline arrow re-arms
142
+ // the observer each render (harmless, just wasteful).
143
+ const scrollRef = useRef<HTMLDivElement | null>(null);
144
+ const sentinelRef = useRef<HTMLDivElement | null>(null);
145
+ const lazyHasMore = lazyLoad?.hasMore ?? false;
146
+ const lazyFetching = lazyLoad?.isFetchingMore ?? false;
147
+ const onLoadMore = lazyLoad?.onLoadMore;
148
+ const optionCount = options.length;
149
+ useEffect(() => {
150
+ const sentinel = sentinelRef.current;
151
+ const root = scrollRef.current;
152
+ if (!sentinel || !root || !onLoadMore || !lazyHasMore) return undefined;
153
+ const observer = new IntersectionObserver(
154
+ entries => {
155
+ if (entries.some(e => e.isIntersecting) && !lazyFetching) {
156
+ onLoadMore();
157
+ }
158
+ },
159
+ { root, rootMargin: '80px' },
160
+ );
161
+ observer.observe(sentinel);
162
+ return () => {
163
+ observer.disconnect();
164
+ };
165
+ // optionCount: re-arm when the list grows (the sentinel moves).
166
+ }, [lazyHasMore, lazyFetching, onLoadMore, optionCount]);
167
+
168
+ const searchInput = search ? (
169
+ <Input
170
+ value={search.value}
171
+ onChange={e => search.onChange(e.target.value)}
172
+ placeholder={search.placeholder}
173
+ aria-label={search.ariaLabel ?? search.placeholder ?? 'Search'}
174
+ />
175
+ ) : null;
176
+
177
+ // Framed mode (search and/or lazyLoad): loading and error are NOT early
178
+ // returns — the frame (title, search input, fixed-height viewport, footer)
179
+ // must stay pixel-identical across loading/loaded/empty/error, with only
180
+ // the viewport CONTENT swapping. An early-return skeleton dropped the
181
+ // bordered viewport + footer and rendered shorter rows, so the whole
182
+ // section collapsed and jumped on every debounced search.
183
+ const framed = !!search || !!lazyLoad;
184
+
185
+ // LOADING / ERROR STATES — legacy (unframed) consumers keep the historical
186
+ // whole-component swap, byte-identical.
187
+ if (isLoading && !framed) {
100
188
  return (
101
189
  <div className={className}>
102
190
  <PushButtonSelectorSkeleton count={skeletonCount} hasTitle={!!title} />
103
191
  </div>
104
192
  );
105
193
  }
106
-
107
- // ERROR STATE
108
- if (error) {
194
+ if (error && !framed) {
109
195
  return (
110
196
  <div className={className}>
111
197
  <PushButtonSelectorError message={error} title={title} />
@@ -252,15 +338,54 @@ export function PushButtonSelector({
252
338
  );
253
339
  };
254
340
 
341
+ // ~82px per row: enough skeleton rows to fill the fixed viewport, so the
342
+ // loading state occupies the SAME height as the loaded list.
343
+ const framedSkeletonCount = lazyLoad ? Math.max(skeletonCount, 4) : skeletonCount;
344
+ const listContent = isLoading ? (
345
+ <PushButtonSelectorSkeleton count={framedSkeletonCount} />
346
+ ) : error ? (
347
+ <PushButtonSelectorError message={error} />
348
+ ) : (
349
+ <>
350
+ {options.length === 0 && emptyMessage !== undefined ? (
351
+ <div className="text-ods-text-secondary text-h6">{emptyMessage}</div>
352
+ ) : (
353
+ renderOptionsContent()
354
+ )}
355
+ {lazyLoad ? (
356
+ <>
357
+ <div ref={sentinelRef} />
358
+ {lazyLoad.isFetchingMore ? (
359
+ <div className="py-2 text-center text-ods-text-secondary text-h6">Loading more…</div>
360
+ ) : null}
361
+ </>
362
+ ) : null}
363
+ </>
364
+ );
365
+
255
366
  return (
256
367
  <TooltipProvider delayDuration={150}>
257
368
  <div className={`space-y-4 ${className}`}>
258
369
  {title && <h3 className="text-ods-text-primary text-h5">{title}</h3>}
259
370
 
260
- {renderOptionsContent()}
371
+ {searchInput}
372
+
373
+ {lazyLoad ? (
374
+ <div
375
+ ref={scrollRef}
376
+ className="space-y-4 overflow-y-auto rounded-lg border border-ods-border p-2"
377
+ style={{ height: lazyLoad.maxHeight ?? '336px' }}
378
+ >
379
+ {listContent}
380
+ </div>
381
+ ) : (
382
+ listContent
383
+ )}
384
+
385
+ {footer}
261
386
 
262
387
  {/* Selection Summary */}
263
- {selectionSummary && validSelectedIds.length > 0 && (
388
+ {!isLoading && !error && selectionSummary && validSelectedIds.length > 0 && (
264
389
  <div className="rounded-lg border border-ods-success bg-ods-success-secondary p-4">
265
390
  <div className="mb-2 flex items-center gap-2">
266
391
  <div className="h-2 w-2 rounded-full bg-ods-success"></div>