@flamingo-stack/openframe-frontend-core 0.0.701 → 0.0.702-snapshot.20261005191338

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 (106) hide show
  1. package/dist/{chunk-DJLZXJYB.js → chunk-2NE3DNXY.js} +1 -1
  2. package/dist/{chunk-V5QNDOEL.js → chunk-53AC23ZN.js} +4 -4
  3. package/dist/{chunk-BQ34PIFC.cjs → chunk-5VPIC5MD.cjs} +3 -3
  4. package/dist/{chunk-BRQY2HKD.cjs → chunk-67CXGPTW.cjs} +24 -24
  5. package/dist/{chunk-BZYMGI4L.js → chunk-7632TNE7.js} +1 -1
  6. package/dist/{chunk-QPNR4N7N.cjs → chunk-BMJUKS6S.cjs} +20 -20
  7. package/dist/{chunk-GD4JALTR.js → chunk-BOUPNYUV.js} +2 -2
  8. package/dist/{chunk-BPS7JQCI.cjs → chunk-BSEIEWM5.cjs} +2 -2
  9. package/dist/{chunk-6JG75WVO.js → chunk-DAW6TDZB.js} +4 -4
  10. package/dist/{chunk-B7M6QEEM.js → chunk-ENGBNKUJ.js} +673 -432
  11. package/dist/{chunk-N7HMUQRV.js → chunk-G55W76S6.js} +1 -1
  12. package/dist/{chunk-65YVUYWJ.cjs → chunk-GX3OUTFO.cjs} +32 -32
  13. package/dist/{chunk-S4ADN25W.js → chunk-GZYLZ4MB.js} +2 -2
  14. package/dist/{chunk-A5ZGKBN2.js → chunk-HHFPQNP3.js} +1 -1
  15. package/dist/{chunk-RTGPIHVO.cjs → chunk-HY5YL3U5.cjs} +247 -173
  16. package/dist/{chunk-DG2I57UM.cjs → chunk-HYLIQMZH.cjs} +21 -21
  17. package/dist/{chunk-N7DPIH57.js → chunk-J4K5JOLI.js} +1 -1
  18. package/dist/{chunk-4H5Q6MQG.cjs → chunk-KFDP3DUL.cjs} +44 -44
  19. package/dist/{chunk-QDIX2ZRL.cjs → chunk-MXHJROXJ.cjs} +754 -513
  20. package/dist/{chunk-CUPJCGLB.js → chunk-NCTYD7HL.js} +2 -2
  21. package/dist/{chunk-MFORWVGD.js → chunk-NMGX2ZTU.js} +5 -5
  22. package/dist/{chunk-QZGFJBG5.cjs → chunk-NU724ZVN.cjs} +70 -70
  23. package/dist/{chunk-ZSPHOHOW.cjs → chunk-NYSYSKSH.cjs} +9 -9
  24. package/dist/{chunk-PNWIVND4.js → chunk-PJP5SMOU.js} +3 -3
  25. package/dist/{chunk-6JPQDYYY.js → chunk-QGTDWFT6.js} +419 -345
  26. package/dist/{chunk-GFXZMFDX.cjs → chunk-RLDAYA2A.cjs} +9 -9
  27. package/dist/{chunk-2CSYUMNM.cjs → chunk-RTXTGLGB.cjs} +2 -2
  28. package/dist/{chunk-YBC346FY.cjs → chunk-S6V7A5FS.cjs} +12 -12
  29. package/dist/{chunk-7ZDTAABP.cjs → chunk-SFGXIFYO.cjs} +7 -7
  30. package/dist/{chunk-JPVDJB27.js → chunk-SL2FOFT2.js} +4 -4
  31. package/dist/{chunk-EONNPRAW.js → chunk-UTW4SQDI.js} +2 -2
  32. package/dist/{chunk-NIT257DV.cjs → chunk-YBGEPKC2.cjs} +4 -4
  33. package/dist/components/case-studies/index.cjs +14 -14
  34. package/dist/components/case-studies/index.js +3 -3
  35. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  36. package/dist/components/chat/index.cjs +2 -2
  37. package/dist/components/chat/index.js +1 -1
  38. package/dist/components/contact/index.cjs +4 -4
  39. package/dist/components/contact/index.js +3 -3
  40. package/dist/components/docs/index.cjs +7 -7
  41. package/dist/components/docs/index.js +6 -6
  42. package/dist/components/embeds/index.cjs +4 -4
  43. package/dist/components/embeds/index.js +3 -3
  44. package/dist/components/faq/index.cjs +4 -4
  45. package/dist/components/faq/index.js +3 -3
  46. package/dist/components/features/index.cjs +23 -3
  47. package/dist/components/features/index.js +22 -2
  48. package/dist/components/features/remote-session/index.d.ts +9 -1
  49. package/dist/components/features/remote-session/index.d.ts.map +1 -1
  50. package/dist/components/features/remote-session/remote-session-block.d.ts +4 -2
  51. package/dist/components/features/remote-session/remote-session-block.d.ts.map +1 -1
  52. package/dist/components/features/remote-session/remote-session-chat-panel.d.ts +4 -2
  53. package/dist/components/features/remote-session/remote-session-chat-panel.d.ts.map +1 -1
  54. package/dist/components/features/remote-session/remote-session-events.d.ts +24 -0
  55. package/dist/components/features/remote-session/remote-session-events.d.ts.map +1 -0
  56. package/dist/components/features/remote-session/remote-session-keep-modals.d.ts +41 -0
  57. package/dist/components/features/remote-session/remote-session-keep-modals.d.ts.map +1 -0
  58. package/dist/components/features/remote-session/remote-session-parts.d.ts +13 -2
  59. package/dist/components/features/remote-session/remote-session-parts.d.ts.map +1 -1
  60. package/dist/components/features/remote-session/remote-session-status.d.ts +34 -0
  61. package/dist/components/features/remote-session/remote-session-status.d.ts.map +1 -0
  62. package/dist/components/features/remote-session/remote-session-viewers.d.ts +15 -0
  63. package/dist/components/features/remote-session/remote-session-viewers.d.ts.map +1 -0
  64. package/dist/components/features/remote-session/types.d.ts +22 -0
  65. package/dist/components/features/remote-session/types.d.ts.map +1 -1
  66. package/dist/components/help-center-pages/index.cjs +71 -71
  67. package/dist/components/help-center-pages/index.js +14 -14
  68. package/dist/components/index.cjs +61 -41
  69. package/dist/components/index.js +33 -13
  70. package/dist/components/meeting-scheduler/index.cjs +25 -25
  71. package/dist/components/meeting-scheduler/index.js +2 -2
  72. package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
  73. package/dist/components/navigation/index.cjs +6 -6
  74. package/dist/components/navigation/index.js +5 -5
  75. package/dist/components/onboarding-guides/index.cjs +6 -6
  76. package/dist/components/onboarding-guides/index.js +5 -5
  77. package/dist/components/tickets/index.cjs +7 -7
  78. package/dist/components/tickets/index.js +6 -6
  79. package/dist/components/ui/drawer.d.ts +7 -3
  80. package/dist/components/ui/drawer.d.ts.map +1 -1
  81. package/dist/components/ui/index.cjs +3 -3
  82. package/dist/components/ui/index.js +2 -2
  83. package/dist/components/ui/panel-resize-handle.d.ts +25 -0
  84. package/dist/components/ui/panel-resize-handle.d.ts.map +1 -0
  85. package/dist/hooks/ui/use-resizable-panel-size.d.ts +46 -0
  86. package/dist/hooks/ui/use-resizable-panel-size.d.ts.map +1 -0
  87. package/dist/index.cjs +27 -7
  88. package/dist/index.js +26 -6
  89. package/dist/tailwind.config.cjs +7 -1
  90. package/dist/tailwind.config.js +7 -1
  91. package/package.json +1 -1
  92. package/src/components/chat/embeddable-chat.tsx +11 -9
  93. package/src/components/features/remote-session/index.ts +28 -0
  94. package/src/components/features/remote-session/remote-session-block.tsx +6 -3
  95. package/src/components/features/remote-session/remote-session-chat-panel.tsx +11 -7
  96. package/src/components/features/remote-session/remote-session-events.tsx +93 -0
  97. package/src/components/features/remote-session/remote-session-keep-modals.tsx +208 -0
  98. package/src/components/features/remote-session/remote-session-parts.tsx +32 -3
  99. package/src/components/features/remote-session/remote-session-status.tsx +102 -0
  100. package/src/components/features/remote-session/remote-session-viewers.tsx +103 -0
  101. package/src/components/features/remote-session/types.ts +26 -0
  102. package/src/components/navigation/app-layout-drawer.tsx +14 -237
  103. package/src/components/ui/drawer.tsx +41 -188
  104. package/src/components/ui/panel-resize-handle.tsx +184 -0
  105. package/src/hooks/ui/use-resizable-panel-size.ts +114 -0
  106. package/tailwind.config.ts +6 -0
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  SoftwareInfo,
10
10
  SoftwareSourceBadge,
11
11
  ToolBadge
12
- } from "./chunk-N7HMUQRV.js";
12
+ } from "./chunk-G55W76S6.js";
13
13
  import {
14
14
  AppHeader,
15
15
  AppLayout,
@@ -29,14 +29,14 @@ import {
29
29
  SlidingSidebar,
30
30
  TicketAlertsButton,
31
31
  TopNavigation
32
- } from "./chunk-PNWIVND4.js";
32
+ } from "./chunk-PJP5SMOU.js";
33
33
  import {
34
34
  MobileNavigationDropdown,
35
35
  MultiLevelNavigation,
36
36
  StickySectionNav,
37
37
  useSectionNavigation
38
- } from "./chunk-DJLZXJYB.js";
39
- import "./chunk-S4ADN25W.js";
38
+ } from "./chunk-2NE3DNXY.js";
39
+ import "./chunk-GZYLZ4MB.js";
40
40
  import {
41
41
  AIEnrichButton,
42
42
  AIEnrichSection,
@@ -233,6 +233,7 @@ import {
233
233
  InputTrigger,
234
234
  InviteLinkInvalidModal,
235
235
  JsonDetailList,
236
+ KeepRecordingModal,
236
237
  KnowledgeBaseLinksManager,
237
238
  LabeledDivider,
238
239
  LazyMount,
@@ -334,6 +335,7 @@ import {
334
335
  QueryReportTableRow,
335
336
  QueryReportTableSkeleton,
336
337
  REMOTE_SESSION_DRAG_HANDLE_CLASS,
338
+ REMOTE_SESSION_KEEP_REASON_LABELS,
337
339
  ROADMAP_STATUS_OPTIONS,
338
340
  ROW_HEIGHT_DESKTOP,
339
341
  ROW_HEIGHT_MOBILE,
@@ -342,6 +344,7 @@ import {
342
344
  RadioGroupBlock,
343
345
  RadioGroupItem,
344
346
  ReleaseChangelogSection,
347
+ ReleaseKeepingModal,
345
348
  RemoteAccessRequestCard,
346
349
  RemoteSessionBlockFrame,
347
350
  RemoteSessionBorder,
@@ -349,12 +352,18 @@ import {
349
352
  RemoteSessionDialog,
350
353
  RemoteSessionDragger,
351
354
  RemoteSessionEndButton,
355
+ RemoteSessionEventList,
356
+ RemoteSessionExpiry,
352
357
  RemoteSessionMinimizedChip,
353
358
  RemoteSessionOrgLogo,
354
359
  RemoteSessionPill,
355
360
  RemoteSessionRecordingTag,
356
361
  RemoteSessionScope,
362
+ RemoteSessionStatusTag,
363
+ RemoteSessionStorageAlert,
357
364
  RemoteSessionSummary,
365
+ RemoteSessionTimelineMarkers,
366
+ RemoteSessionViewers,
358
367
  ReopenTicketModal,
359
368
  ReopenTicketRequestModal,
360
369
  RichMarkdownRenderer,
@@ -528,6 +537,7 @@ import {
528
537
  formatDocName,
529
538
  formatPhoneE164,
530
539
  formatRemoteSessionElapsed,
540
+ formatRemoteSessionOffset,
531
541
  formatTestRunDuration,
532
542
  generateBiteId,
533
543
  getApprovalMeta,
@@ -655,7 +665,7 @@ import {
655
665
  withFirstTouchAttribution,
656
666
  withWalkthroughOpenParam,
657
667
  writeDismissCookie
658
- } from "./chunk-B7M6QEEM.js";
668
+ } from "./chunk-ENGBNKUJ.js";
659
669
  import "./chunk-LQYXMEYF.js";
660
670
  import "./chunk-4JPLYOS6.js";
661
671
  import {
@@ -1190,7 +1200,7 @@ import {
1190
1200
  useVideoOriginPreconnect,
1191
1201
  useVideoWarmup,
1192
1202
  usesCanonicalStatusStyle
1193
- } from "./chunk-6JPQDYYY.js";
1203
+ } from "./chunk-QGTDWFT6.js";
1194
1204
  import {
1195
1205
  HEADING_EMOJI_RE,
1196
1206
  HUB_HEADER_OFFSET_PX,
@@ -2390,6 +2400,7 @@ export {
2390
2400
  InviteLinkInvalidModal,
2391
2401
  JsonDetailList,
2392
2402
  JumpInIcon,
2403
+ KeepRecordingModal,
2393
2404
  KnowledgeBaseLinksManager,
2394
2405
  LOG_SEVERITY_COLORS,
2395
2406
  LOG_SEVERITY_LABELS,
@@ -2620,6 +2631,7 @@ export {
2620
2631
  RATIO_GRID_CLASS,
2621
2632
  RATIO_TO_CSS_ASPECT,
2622
2633
  REMOTE_SESSION_DRAG_HANDLE_CLASS,
2634
+ REMOTE_SESSION_KEEP_REASON_LABELS,
2623
2635
  RESCUE_FORMS,
2624
2636
  RESYNC_AFTER_HIDDEN_MS,
2625
2637
  ROADMAP_STATUS_OPTIONS,
@@ -2634,6 +2646,7 @@ export {
2634
2646
  ReclaimProfitsIcon,
2635
2647
  RedditIcon,
2636
2648
  ReleaseChangelogSection,
2649
+ ReleaseKeepingModal,
2637
2650
  RemoteAccessRequestCard,
2638
2651
  RemoteControlIcon,
2639
2652
  RemoteSessionBlockFrame,
@@ -2642,12 +2655,18 @@ export {
2642
2655
  RemoteSessionDialog,
2643
2656
  RemoteSessionDragger,
2644
2657
  RemoteSessionEndButton,
2658
+ RemoteSessionEventList,
2659
+ RemoteSessionExpiry,
2645
2660
  RemoteSessionMinimizedChip,
2646
2661
  RemoteSessionOrgLogo,
2647
2662
  RemoteSessionPill,
2648
2663
  RemoteSessionRecordingTag,
2649
2664
  RemoteSessionScope,
2665
+ RemoteSessionStatusTag,
2666
+ RemoteSessionStorageAlert,
2650
2667
  RemoteSessionSummary,
2668
+ RemoteSessionTimelineMarkers,
2669
+ RemoteSessionViewers,
2651
2670
  RenameChatModal,
2652
2671
  ReopenTicketModal,
2653
2672
  ReopenTicketRequestModal,
@@ -3105,6 +3124,7 @@ export {
3105
3124
  formatRelativeTime,
3106
3125
  formatReleaseDate,
3107
3126
  formatRemoteSessionElapsed,
3127
+ formatRemoteSessionOffset,
3108
3128
  formatSingularLookupInvocation,
3109
3129
  formatTestRunDuration,
3110
3130
  formatTicketFullTimestamp,
@@ -486,6 +486,11 @@ var config = {
486
486
  "dot-pulse": {
487
487
  "0%, 80%, 100%": { transform: "scale(1)", opacity: "0.7" },
488
488
  "40%": { transform: "scale(1.5)", opacity: "1" }
489
+ },
490
+ // The dot on a LIVE tag: opacity only, so it reads as "on air" without moving.
491
+ "live-pulse": {
492
+ "0%, 100%": { opacity: "1" },
493
+ "50%": { opacity: "0.25" }
489
494
  }
490
495
  },
491
496
  animation: {
@@ -493,7 +498,8 @@ var config = {
493
498
  "accordion-up": "accordion-up 0.2s ease-out",
494
499
  "fade-in": "fade-in 0.3s ease-out",
495
500
  "cursor-blink": "cursor-blink 1s steps(1) infinite",
496
- "dot-pulse": "dot-pulse 1.4s ease-in-out infinite"
501
+ "dot-pulse": "dot-pulse 1.4s ease-in-out infinite",
502
+ "live-pulse": "live-pulse 1.6s ease-in-out infinite"
497
503
  }
498
504
  }
499
505
  },
@@ -479,6 +479,11 @@ var config = {
479
479
  "dot-pulse": {
480
480
  "0%, 80%, 100%": { transform: "scale(1)", opacity: "0.7" },
481
481
  "40%": { transform: "scale(1.5)", opacity: "1" }
482
+ },
483
+ // The dot on a LIVE tag: opacity only, so it reads as "on air" without moving.
484
+ "live-pulse": {
485
+ "0%, 100%": { opacity: "1" },
486
+ "50%": { opacity: "0.25" }
482
487
  }
483
488
  },
484
489
  animation: {
@@ -486,7 +491,8 @@ var config = {
486
491
  "accordion-up": "accordion-up 0.2s ease-out",
487
492
  "fade-in": "fade-in 0.3s ease-out",
488
493
  "cursor-blink": "cursor-blink 1s steps(1) infinite",
489
- "dot-pulse": "dot-pulse 1.4s ease-in-out infinite"
494
+ "dot-pulse": "dot-pulse 1.4s ease-in-out infinite",
495
+ "live-pulse": "live-pulse 1.6s ease-in-out infinite"
490
496
  }
491
497
  }
492
498
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.701",
3
+ "version": "0.0.702-snapshot.20261005191338",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -126,13 +126,13 @@ const HISTORY_RAIL_WIDTH = 320;
126
126
  const CHAT_BLOCK_MIN_WIDTH = 400;
127
127
  const SPLIT_MIN_WIDTH = HISTORY_RAIL_WIDTH + CHAT_BLOCK_MIN_WIDTH;
128
128
 
129
- // Desktop drawer opens at this fraction of the viewport width (clamped to the
130
- // Drawer's own min/max). The user can still resize (persisted per DRAWER_WIDTH_KEY).
129
+ // Desktop drawer is this fraction of the viewport width (clamped to the
130
+ // Drawer's own min/max) until the user resizes it. Only the size they choose is
131
+ // stored (see DRAWER_WIDTH_KEY); the default tracks the window, so changing
132
+ // this policy needs no new key.
131
133
  const DRAWER_DEFAULT_WIDTH_RATIO = 0.5;
132
- // Bump the suffix whenever the default policy changes so previously-persisted
133
- // widths (e.g. the old fixed 750px / earlier 30% default) reset on next open.
134
134
  const DRAWER_WIDTH_KEY = 'mingo-chat-width-v4';
135
- const DRAWER_DEFAULT_WIDTH_PX = 750; // SSR fallback before the viewport is known
135
+ const DRAWER_DEFAULT_WIDTH_PX = 750; // before the viewport is known (server render)
136
136
  /**
137
137
  * A panel that owns a dialog list opens WIDE ENOUGH TO SPLIT, so the "Current
138
138
  * Chats" rail sits beside the conversation instead of replacing it.
@@ -144,12 +144,14 @@ const DRAWER_DEFAULT_WIDTH_PX = 750; // SSR fallback before the viewport is know
144
144
  * viewport, so a genuinely small screen keeps the stacked view.
145
145
  */
146
146
  const SPLIT_DEFAULT_WIDTH_ALLOWANCE = 16;
147
- function drawerDefaultWidth(withHistoryRail: boolean): number {
148
- if (typeof window === 'undefined') return DRAWER_DEFAULT_WIDTH_PX;
149
- const ratioWidth = Math.round(window.innerWidth * DRAWER_DEFAULT_WIDTH_RATIO);
147
+ function drawerDefaultWidth(viewportWidth: number, withHistoryRail: boolean): number {
148
+ if (viewportWidth <= 0) return DRAWER_DEFAULT_WIDTH_PX;
149
+ const ratioWidth = Math.round(viewportWidth * DRAWER_DEFAULT_WIDTH_RATIO);
150
150
  if (!withHistoryRail) return ratioWidth;
151
151
  return Math.max(ratioWidth, SPLIT_MIN_WIDTH + SPLIT_DEFAULT_WIDTH_ALLOWANCE);
152
152
  }
153
+ const drawerDefaultWidthWithRail = (viewportWidth: number) => drawerDefaultWidth(viewportWidth, true);
154
+ const drawerDefaultWidthWithoutRail = (viewportWidth: number) => drawerDefaultWidth(viewportWidth, false);
153
155
 
154
156
  // =============================================================================
155
157
  // Types
@@ -2810,7 +2812,7 @@ function EmbeddableChatInner({
2810
2812
  resizable
2811
2813
  minSize={480}
2812
2814
  maxSize={1600}
2813
- defaultSize={drawerDefaultWidth(historyListMode)}
2815
+ defaultSize={historyListMode ? drawerDefaultWidthWithRail : drawerDefaultWidthWithoutRail}
2814
2816
  storageKey={DRAWER_WIDTH_KEY}
2815
2817
  resizeAriaLabel="Resize chat panel"
2816
2818
  overlayClassName="mingo-chat-overlay"
@@ -16,6 +16,30 @@ export type { RemoteSessionChatPanelProps } from './remote-session-chat-panel';
16
16
  export { RemoteSessionDialog } from './remote-session-dialog';
17
17
  export type { RemoteSessionDialogProps } from './remote-session-dialog';
18
18
  export { RemoteSessionBorder, RemoteSessionPill } from './remote-session-overlay';
19
+ export {
20
+ REMOTE_SESSION_KEEP_REASON_LABELS,
21
+ KeepRecordingModal,
22
+ ReleaseKeepingModal,
23
+ } from './remote-session-keep-modals';
24
+ export type {
25
+ KeepRecordingModalProps,
26
+ KeepRecordingSelection,
27
+ ReleaseKeepingModalProps,
28
+ } from './remote-session-keep-modals';
29
+ export {
30
+ RemoteSessionEventList,
31
+ RemoteSessionTimelineMarkers,
32
+ formatRemoteSessionOffset,
33
+ } from './remote-session-events';
34
+ export type { RemoteSessionEventListProps, RemoteSessionTimelineMarkersProps } from './remote-session-events';
35
+ export { RemoteSessionExpiry, RemoteSessionStatusTag, RemoteSessionStorageAlert } from './remote-session-status';
36
+ export type {
37
+ RemoteSessionExpiryProps,
38
+ RemoteSessionStatusTagProps,
39
+ RemoteSessionStorageAlertProps,
40
+ } from './remote-session-status';
41
+ export { RemoteSessionViewers } from './remote-session-viewers';
42
+ export type { RemoteSessionViewersProps } from './remote-session-viewers';
19
43
  export {
20
44
  REMOTE_SESSION_DRAG_HANDLE_CLASS,
21
45
  RemoteSessionDragger,
@@ -32,5 +56,9 @@ export type {
32
56
  RemoteSessionChatMessage,
33
57
  RemoteSessionDialogVariant,
34
58
  RemoteSessionDragHandlers,
59
+ RemoteSessionEvent,
60
+ RemoteSessionKeepReason,
35
61
  RemoteSessionParty,
62
+ RemoteSessionStatus,
63
+ RemoteSessionViewer,
36
64
  } from './types';
@@ -10,9 +10,9 @@ import {
10
10
  REMOTE_SESSION_DRAG_HANDLE_CLASS,
11
11
  RemoteSessionDragger,
12
12
  RemoteSessionOrgLogo,
13
- RemoteSessionRecordingTag,
13
+ RemoteSessionStatusRow,
14
14
  } from './remote-session-parts';
15
- import type { RemoteSessionDragHandlers, RemoteSessionParty } from './types';
15
+ import type { RemoteSessionDragHandlers, RemoteSessionParty, RemoteSessionViewer } from './types';
16
16
 
17
17
  export interface RemoteSessionBlockFrameProps {
18
18
  /** The summary or the chat panel. */
@@ -74,6 +74,8 @@ export interface RemoteSessionSummaryProps {
74
74
  /** Elapsed session time, e.g. from `useRemoteSessionTimer`. */
75
75
  elapsed: string;
76
76
  showRecordingTag?: boolean;
77
+ /** Everyone in the session while colleagues watch it, the host included; hidden when empty. */
78
+ viewers?: RemoteSessionViewer[];
77
79
  onOpenChat: () => void;
78
80
  onEndSession: () => void;
79
81
  /** Makes the header row and the dragger move the window. */
@@ -85,6 +87,7 @@ export function RemoteSessionSummary({
85
87
  party,
86
88
  elapsed,
87
89
  showRecordingTag = false,
90
+ viewers,
88
91
  onOpenChat,
89
92
  onEndSession,
90
93
  dragHandlers,
@@ -118,7 +121,7 @@ export function RemoteSessionSummary({
118
121
  <p className="w-full truncate text-ods-text-secondary text-h4">{party.organizationName}</p>
119
122
  </div>
120
123
  </div>
121
- {showRecordingTag && <RemoteSessionRecordingTag />}
124
+ <RemoteSessionStatusRow showRecordingTag={showRecordingTag} viewers={viewers} />
122
125
  <div className="flex w-full items-stretch gap-[var(--spacing-system-m)]">
123
126
  <Button
124
127
  variant="outline"
@@ -6,12 +6,13 @@ import { MessageOffIcon, Send03Icon } from '../../icons-v2-generated';
6
6
  import { Button } from '../../ui/button/button';
7
7
  import { SquareAvatar } from '../../ui/square-avatar';
8
8
  import { RemoteSessionEndButton } from './remote-session-block';
9
- import {
10
- REMOTE_SESSION_DRAG_HANDLE_CLASS,
11
- RemoteSessionDragger,
12
- RemoteSessionRecordingTag,
13
- } from './remote-session-parts';
14
- import type { RemoteSessionChatMessage, RemoteSessionDragHandlers, RemoteSessionParty } from './types';
9
+ import { REMOTE_SESSION_DRAG_HANDLE_CLASS, RemoteSessionDragger, RemoteSessionStatusRow } from './remote-session-parts';
10
+ import type {
11
+ RemoteSessionChatMessage,
12
+ RemoteSessionDragHandlers,
13
+ RemoteSessionParty,
14
+ RemoteSessionViewer,
15
+ } from './types';
15
16
 
16
17
  export interface RemoteSessionChatPanelProps {
17
18
  party: RemoteSessionParty;
@@ -19,6 +20,8 @@ export interface RemoteSessionChatPanelProps {
19
20
  elapsed: string;
20
21
  messages: RemoteSessionChatMessage[];
21
22
  showRecordingTag?: boolean;
23
+ /** Everyone in the session while colleagues watch it, the host included; hidden when empty. */
24
+ viewers?: RemoteSessionViewer[];
22
25
  /** The composer's text; owned by the host so it survives hiding the block. */
23
26
  draft: string;
24
27
  onDraftChange: (draft: string) => void;
@@ -43,6 +46,7 @@ export function RemoteSessionChatPanel({
43
46
  elapsed,
44
47
  messages,
45
48
  showRecordingTag = false,
49
+ viewers,
46
50
  draft,
47
51
  onDraftChange,
48
52
  onSend,
@@ -83,7 +87,7 @@ export function RemoteSessionChatPanel({
83
87
  </p>
84
88
  <p className="w-full truncate text-ods-text-secondary text-h4">You can end this session at any time</p>
85
89
  </div>
86
- {showRecordingTag && <RemoteSessionRecordingTag />}
90
+ <RemoteSessionStatusRow showRecordingTag={showRecordingTag} viewers={viewers} />
87
91
  <div className="flex w-full items-stretch gap-[var(--spacing-system-m)]">
88
92
  <Button
89
93
  variant="outline"
@@ -0,0 +1,93 @@
1
+ 'use client';
2
+
3
+ import { cn } from '../../../utils/cn';
4
+ import type { RemoteSessionEvent } from './types';
5
+
6
+ /** `mm:ss` from the start of a recording; `h:mm:ss` from the first hour on. */
7
+ export function formatRemoteSessionOffset(offsetMs: number): string {
8
+ const total = Math.max(0, Math.floor(offsetMs / 1000));
9
+ const hours = Math.floor(total / 3600);
10
+ const minutes = String(Math.floor((total % 3600) / 60)).padStart(2, '0');
11
+ const seconds = String(total % 60).padStart(2, '0');
12
+ return hours > 0 ? `${hours}:${minutes}:${seconds}` : `${minutes}:${seconds}`;
13
+ }
14
+
15
+ export interface RemoteSessionEventListProps {
16
+ events: RemoteSessionEvent[];
17
+ /** The event the playhead is on; highlighted. */
18
+ activeEventId?: string | null;
19
+ /** Seek the player to the event. */
20
+ onSelect?: (event: RemoteSessionEvent) => void;
21
+ className?: string;
22
+ }
23
+
24
+ /** SESSION EVENTS under the recording player: each event with its time, newest last. */
25
+ export function RemoteSessionEventList({ events, activeEventId, onSelect, className }: RemoteSessionEventListProps) {
26
+ return (
27
+ <section className={cn('flex w-full flex-col gap-[var(--spacing-system-xxs)]', className)}>
28
+ <h2 className="text-ods-text-secondary text-h5">Session Events</h2>
29
+ <ol className="flex w-full flex-col gap-[var(--spacing-system-xxs)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-m)]">
30
+ {events.map(event => (
31
+ <li key={event.id}>
32
+ <button
33
+ type="button"
34
+ aria-current={event.id === activeEventId || undefined}
35
+ onClick={() => onSelect?.(event)}
36
+ className={cn(
37
+ 'flex w-full items-start gap-[var(--spacing-system-xxs)] rounded-md p-[var(--spacing-system-xxs)] text-left outline-none transition-colors',
38
+ 'hover:bg-ods-bg-hover focus-visible:ring-2 focus-visible:ring-ods-focus',
39
+ event.id === activeEventId && 'bg-ods-border hover:bg-ods-border',
40
+ )}
41
+ >
42
+ <span className="w-[84px] shrink-0 text-ods-text-secondary text-h4">
43
+ {formatRemoteSessionOffset(event.offsetMs)}
44
+ </span>
45
+ <span className="flex min-w-0 flex-1 flex-col">
46
+ <span className="text-ods-text-primary text-h4">{event.title}</span>
47
+ {event.detail && <span className="text-ods-text-secondary text-h6">{event.detail}</span>}
48
+ </span>
49
+ </button>
50
+ </li>
51
+ ))}
52
+ </ol>
53
+ </section>
54
+ );
55
+ }
56
+
57
+ export interface RemoteSessionTimelineMarkersProps {
58
+ events: RemoteSessionEvent[];
59
+ /** Length of the recording; events past it are pinned to the end. */
60
+ durationMs: number;
61
+ /** Seek the player to the event. */
62
+ onSelect?: (event: RemoteSessionEvent) => void;
63
+ className?: string;
64
+ }
65
+
66
+ /** The event marks above the player's track, one per event, at its share of the recording. */
67
+ export function RemoteSessionTimelineMarkers({
68
+ events,
69
+ durationMs,
70
+ onSelect,
71
+ className,
72
+ }: RemoteSessionTimelineMarkersProps) {
73
+ if (durationMs <= 0) return null;
74
+ return (
75
+ <div className={cn('relative h-4 w-full', className)}>
76
+ {events.map(event => {
77
+ const share = Math.min(1, Math.max(0, event.offsetMs / durationMs));
78
+ const label = `${event.title} at ${formatRemoteSessionOffset(event.offsetMs)}`;
79
+ return (
80
+ <button
81
+ key={event.id}
82
+ type="button"
83
+ aria-label={label}
84
+ title={label}
85
+ onClick={() => onSelect?.(event)}
86
+ style={{ left: `${share * 100}%` }}
87
+ className="absolute top-0 h-4 w-1 -translate-x-1/2 rounded-md bg-ods-text-secondary outline-none transition-colors hover:bg-ods-text-primary focus-visible:ring-2 focus-visible:ring-ods-focus"
88
+ />
89
+ );
90
+ })}
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,208 @@
1
+ 'use client';
2
+
3
+ import { type ReactNode, useState } from 'react';
4
+ import { HourglassIcon, InfoCircleIcon } from '../../icons-v2-generated';
5
+ import { Button } from '../../ui/button/button';
6
+ import { Input } from '../../ui/input';
7
+ import { ModalV2, ModalV2Footer, ModalV2Header, ModalV2Title } from '../../ui/modal-v2';
8
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select';
9
+ import type { RemoteSessionKeepReason } from './types';
10
+
11
+ /** How each Keep reason reads in the dialog, the session list and the release notice. */
12
+ export const REMOTE_SESSION_KEEP_REASON_LABELS: Record<RemoteSessionKeepReason, string> = {
13
+ CLIENT_DISPUTE: 'Client dispute',
14
+ INTERNAL_REVIEW: 'Internal review',
15
+ LEGAL_OR_COMPLIANCE: 'Legal or compliance request',
16
+ OTHER: 'Other',
17
+ };
18
+
19
+ const KEEP_REASONS = Object.keys(REMOTE_SESSION_KEEP_REASON_LABELS) as RemoteSessionKeepReason[];
20
+
21
+ const DESCRIPTION_MAX_LENGTH = 500;
22
+
23
+ /** A notice inside the Keep dialogs: an icon, a bold line and the explanation under it. */
24
+ function KeepNotice({ icon, title, children }: { icon: ReactNode; title: string; children: ReactNode }) {
25
+ return (
26
+ <div className="flex w-full items-start gap-[var(--spacing-system-m)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-m)]">
27
+ <span className="flex shrink-0 text-ods-text-secondary">{icon}</span>
28
+ <div className="flex min-w-0 flex-1 flex-col">
29
+ <p className="text-ods-text-primary text-h3">{title}</p>
30
+ <p className="text-ods-text-secondary text-h4">{children}</p>
31
+ </div>
32
+ </div>
33
+ );
34
+ }
35
+
36
+ export interface KeepRecordingSelection {
37
+ reason: RemoteSessionKeepReason;
38
+ /** Trimmed; set for `OTHER` only. */
39
+ description: string | null;
40
+ }
41
+
42
+ export interface KeepRecordingModalProps {
43
+ isOpen: boolean;
44
+ onClose: () => void;
45
+ /** What kept recordings take of the tenant's allowance, e.g. "612 MB of 50 GB". */
46
+ keptUsage: string;
47
+ /** The Keep is in flight: the button spins and the dialog refuses to close. */
48
+ isPending?: boolean;
49
+ onConfirm: (selection: KeepRecordingSelection) => void;
50
+ }
51
+
52
+ /** Keep this recording: exempts it from automatic deletion until someone releases it. */
53
+ export function KeepRecordingModal({
54
+ isOpen,
55
+ onClose,
56
+ keptUsage,
57
+ isPending = false,
58
+ onConfirm,
59
+ }: KeepRecordingModalProps) {
60
+ const [reason, setReason] = useState<RemoteSessionKeepReason | ''>('');
61
+ const [description, setDescription] = useState('');
62
+
63
+ // Every opening starts blank. Reset while rendering, not from an effect: the
64
+ // modal stays mounted when it closes, so an effect would paint the opening
65
+ // frame with the previous answer still filled in.
66
+ const [seededOpen, setSeededOpen] = useState(isOpen);
67
+ if (seededOpen !== isOpen) {
68
+ setSeededOpen(isOpen);
69
+ if (isOpen) {
70
+ setReason('');
71
+ setDescription('');
72
+ }
73
+ }
74
+
75
+ const trimmed = description.trim();
76
+ const canKeep = reason !== '' && (reason !== 'OTHER' || trimmed !== '');
77
+
78
+ const handleKeep = () => {
79
+ if (!canKeep || isPending) return;
80
+ onConfirm({ reason, description: reason === 'OTHER' ? trimmed : null });
81
+ };
82
+
83
+ return (
84
+ <ModalV2 isOpen={isOpen} onClose={isPending ? () => {} : onClose} className="text-left md:max-w-[600px]">
85
+ <ModalV2Header>
86
+ <ModalV2Title>Keep this recording</ModalV2Title>
87
+ </ModalV2Header>
88
+
89
+ <KeepNotice
90
+ icon={<InfoCircleIcon size={24} color="currentColor" />}
91
+ title="This recording won't be deleted automatically"
92
+ >
93
+ It stays until you stop keeping it. Kept recordings use {keptUsage}.
94
+ </KeepNotice>
95
+
96
+ <div className="flex w-full flex-col gap-[var(--spacing-system-xs)]">
97
+ <Select
98
+ value={reason}
99
+ onValueChange={value => setReason(value as RemoteSessionKeepReason)}
100
+ disabled={isPending}
101
+ >
102
+ <SelectTrigger label="Reason" labelVariant="large">
103
+ <SelectValue placeholder="Select Reason" />
104
+ </SelectTrigger>
105
+ <SelectContent>
106
+ {KEEP_REASONS.map(value => (
107
+ <SelectItem key={value} value={value}>
108
+ {REMOTE_SESSION_KEEP_REASON_LABELS[value]}
109
+ </SelectItem>
110
+ ))}
111
+ </SelectContent>
112
+ </Select>
113
+ {reason === 'OTHER' && (
114
+ <Input
115
+ aria-label="Reason description"
116
+ value={description}
117
+ onChange={event => setDescription(event.target.value)}
118
+ placeholder="Briefly describe what's going on"
119
+ maxLength={DESCRIPTION_MAX_LENGTH}
120
+ disabled={isPending}
121
+ />
122
+ )}
123
+ </div>
124
+
125
+ <ModalV2Footer>
126
+ <Button type="button" variant="outline" onClick={onClose} disabled={isPending} className="flex-1 md:hidden">
127
+ Cancel
128
+ </Button>
129
+ <div className="hidden flex-1 md:block" />
130
+ <Button
131
+ type="button"
132
+ variant="accent"
133
+ onClick={handleKeep}
134
+ loading={isPending}
135
+ disabled={!canKeep}
136
+ className="flex-1"
137
+ >
138
+ Keep
139
+ </Button>
140
+ </ModalV2Footer>
141
+ </ModalV2>
142
+ );
143
+ }
144
+
145
+ export interface ReleaseKeepingModalProps {
146
+ isOpen: boolean;
147
+ onClose: () => void;
148
+ /** Who placed the Keep. */
149
+ keptBy: string;
150
+ /** When the Keep was placed, formatted, e.g. "27 Jul 2026". */
151
+ keptOn: string;
152
+ /** The reason as it reads in the sentence, e.g. "Client dispute". */
153
+ reason: string;
154
+ /** The ticket or reference the Keep was placed for. */
155
+ ticket?: string | null;
156
+ /** When the recording will be deleted once released, formatted. */
157
+ expiresOn: string;
158
+ /** The original expiry date when it has already passed, so the release adds the grace period. */
159
+ dueOn?: string | null;
160
+ /** The release is in flight: the button spins and the dialog refuses to close. */
161
+ isPending?: boolean;
162
+ onConfirm: () => void;
163
+ }
164
+
165
+ /** Release Keeping: hands the recording back to the normal expiry, nothing is deleted on the spot. */
166
+ export function ReleaseKeepingModal({
167
+ isOpen,
168
+ onClose,
169
+ keptBy,
170
+ keptOn,
171
+ reason,
172
+ ticket,
173
+ expiresOn,
174
+ dueOn,
175
+ isPending = false,
176
+ onConfirm,
177
+ }: ReleaseKeepingModalProps) {
178
+ return (
179
+ <ModalV2 isOpen={isOpen} onClose={isPending ? () => {} : onClose} className="text-left md:max-w-[600px]">
180
+ <ModalV2Header>
181
+ <ModalV2Title>Release Keeping</ModalV2Title>
182
+ </ModalV2Header>
183
+
184
+ <div className="flex w-full flex-col gap-[var(--spacing-system-xsf)] text-ods-text-primary text-h4">
185
+ <p>
186
+ Kept by {keptBy} on {keptOn} for {reason}.
187
+ </p>
188
+ {ticket && <p>Ticket: {ticket}</p>}
189
+ </div>
190
+
191
+ <KeepNotice icon={<HourglassIcon size={24} color="currentColor" />} title={`It will expire on ${expiresOn}`}>
192
+ {dueOn
193
+ ? `It was due to expire on ${dueOn}. Releasing gives it 3 days' grace, and it shows amber until then. Whoever placed the Keep is reminded the day before it goes. Nothing is deleted now.`
194
+ : 'Nothing is deleted now.'}
195
+ </KeepNotice>
196
+
197
+ <ModalV2Footer>
198
+ <Button type="button" variant="outline" onClick={onClose} disabled={isPending} className="flex-1 md:hidden">
199
+ Cancel
200
+ </Button>
201
+ <div className="hidden flex-1 md:block" />
202
+ <Button type="button" variant="accent" onClick={onConfirm} loading={isPending} className="flex-1">
203
+ Release Keeping
204
+ </Button>
205
+ </ModalV2Footer>
206
+ </ModalV2>
207
+ );
208
+ }