@flamingo-stack/openframe-frontend-core 0.0.691 → 0.0.692

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 (128) hide show
  1. package/dist/{chunk-LCENRN2N.cjs → chunk-5XPK2QGU.cjs} +3 -3
  2. package/dist/{chunk-LCENRN2N.cjs.map → chunk-5XPK2QGU.cjs.map} +1 -1
  3. package/dist/{chunk-SA4KQCNB.cjs → chunk-7BNWMOPM.cjs} +36 -36
  4. package/dist/{chunk-SA4KQCNB.cjs.map → chunk-7BNWMOPM.cjs.map} +1 -1
  5. package/dist/{chunk-6Z26ZMR4.js → chunk-A5NHAHV6.js} +2 -2
  6. package/dist/{chunk-Y3Q76RAA.cjs → chunk-ATREVNIN.cjs} +33 -33
  7. package/dist/{chunk-Y3Q76RAA.cjs.map → chunk-ATREVNIN.cjs.map} +1 -1
  8. package/dist/{chunk-FNMQ2J44.js → chunk-AVAZ6BD6.js} +2 -2
  9. package/dist/{chunk-DDWLF2JV.cjs → chunk-CG7OWW7X.cjs} +4 -4
  10. package/dist/{chunk-DDWLF2JV.cjs.map → chunk-CG7OWW7X.cjs.map} +1 -1
  11. package/dist/{chunk-KMW2PXBU.js → chunk-CKO6NKNE.js} +750 -212
  12. package/dist/chunk-CKO6NKNE.js.map +1 -0
  13. package/dist/{chunk-ZLYLHP64.cjs → chunk-CKUPANVU.cjs} +4 -4
  14. package/dist/{chunk-ZLYLHP64.cjs.map → chunk-CKUPANVU.cjs.map} +1 -1
  15. package/dist/{chunk-H54RDLVC.cjs → chunk-DKJDO7YM.cjs} +18 -18
  16. package/dist/{chunk-H54RDLVC.cjs.map → chunk-DKJDO7YM.cjs.map} +1 -1
  17. package/dist/{chunk-N4S7RBUM.cjs → chunk-DUURFYGS.cjs} +3 -3
  18. package/dist/{chunk-N4S7RBUM.cjs.map → chunk-DUURFYGS.cjs.map} +1 -1
  19. package/dist/{chunk-CWXRZVDI.cjs → chunk-EXWS2QSD.cjs} +588 -50
  20. package/dist/chunk-EXWS2QSD.cjs.map +1 -0
  21. package/dist/{chunk-WZYIAKIH.cjs → chunk-F6LXGP6S.cjs} +3 -3
  22. package/dist/{chunk-WZYIAKIH.cjs.map → chunk-F6LXGP6S.cjs.map} +1 -1
  23. package/dist/{chunk-4N2JOPAQ.cjs → chunk-FTK3S57R.cjs} +13 -13
  24. package/dist/{chunk-4N2JOPAQ.cjs.map → chunk-FTK3S57R.cjs.map} +1 -1
  25. package/dist/{chunk-MJI5GVT7.js → chunk-IQTQIBZ3.js} +4 -4
  26. package/dist/{chunk-BXE2RZEV.cjs → chunk-KDCIYRZV.cjs} +17 -17
  27. package/dist/{chunk-BXE2RZEV.cjs.map → chunk-KDCIYRZV.cjs.map} +1 -1
  28. package/dist/{chunk-ESFPY5FF.cjs → chunk-KQZMM3QJ.cjs} +51 -51
  29. package/dist/{chunk-ESFPY5FF.cjs.map → chunk-KQZMM3QJ.cjs.map} +1 -1
  30. package/dist/{chunk-Y7ZQZOE6.js → chunk-LYJJH4D6.js} +2 -2
  31. package/dist/{chunk-TSNGVWQO.js → chunk-LYQULSK4.js} +5 -5
  32. package/dist/{chunk-KVDLL4LU.js → chunk-MCNQKS6I.js} +2 -2
  33. package/dist/{chunk-DC3G4XGP.js → chunk-PKZGXRU2.js} +3 -3
  34. package/dist/{chunk-Y3BC7TOJ.cjs → chunk-Q25VYAEL.cjs} +17 -17
  35. package/dist/{chunk-Y3BC7TOJ.cjs.map → chunk-Q25VYAEL.cjs.map} +1 -1
  36. package/dist/{chunk-JSG7WZKV.js → chunk-RK47E3PP.js} +2 -2
  37. package/dist/{chunk-4HZXOQRM.js → chunk-RLNNV46K.js} +2 -2
  38. package/dist/{chunk-BXAGR3PO.cjs → chunk-RTKF5AYA.cjs} +3 -3
  39. package/dist/{chunk-BXAGR3PO.cjs.map → chunk-RTKF5AYA.cjs.map} +1 -1
  40. package/dist/{chunk-Z5FHXCXF.js → chunk-TEIHEFXP.js} +4 -4
  41. package/dist/{chunk-XBYF4SGD.js → chunk-TXFGG47A.js} +2 -2
  42. package/dist/{chunk-5N2ZSB42.cjs → chunk-WTYQCT4A.cjs} +10 -10
  43. package/dist/{chunk-5N2ZSB42.cjs.map → chunk-WTYQCT4A.cjs.map} +1 -1
  44. package/dist/{chunk-CMYDGVTZ.js → chunk-X5XFKI6N.js} +2 -2
  45. package/dist/{chunk-7WYWPELF.js → chunk-Z7QCNCBN.js} +5 -5
  46. package/dist/{chunk-DZMDG7PC.js → chunk-ZAV2F4VX.js} +2 -2
  47. package/dist/components/case-studies/index.cjs +11 -11
  48. package/dist/components/case-studies/index.js +2 -2
  49. package/dist/components/contact/index.cjs +3 -3
  50. package/dist/components/contact/index.js +2 -2
  51. package/dist/components/docs/index.cjs +6 -6
  52. package/dist/components/docs/index.js +5 -5
  53. package/dist/components/embeds/index.cjs +3 -3
  54. package/dist/components/embeds/index.js +2 -2
  55. package/dist/components/faq/index.cjs +3 -3
  56. package/dist/components/faq/index.js +2 -2
  57. package/dist/components/features/index.cjs +34 -2
  58. package/dist/components/features/index.cjs.map +1 -1
  59. package/dist/components/features/index.d.ts +1 -0
  60. package/dist/components/features/index.d.ts.map +1 -1
  61. package/dist/components/features/index.js +33 -1
  62. package/dist/components/features/remote-session/index.d.ts +13 -0
  63. package/dist/components/features/remote-session/index.d.ts.map +1 -0
  64. package/dist/components/features/remote-session/remote-access-request-card.d.ts +19 -0
  65. package/dist/components/features/remote-session/remote-access-request-card.d.ts.map +1 -0
  66. package/dist/components/features/remote-session/remote-session-block.d.ts +41 -0
  67. package/dist/components/features/remote-session/remote-session-block.d.ts.map +1 -0
  68. package/dist/components/features/remote-session/remote-session-chat-panel.d.ts +23 -0
  69. package/dist/components/features/remote-session/remote-session-chat-panel.d.ts.map +1 -0
  70. package/dist/components/features/remote-session/remote-session-dialog.d.ts +15 -0
  71. package/dist/components/features/remote-session/remote-session-dialog.d.ts.map +1 -0
  72. package/dist/components/features/remote-session/remote-session-overlay.d.ts +12 -0
  73. package/dist/components/features/remote-session/remote-session-overlay.d.ts.map +1 -0
  74. package/dist/components/features/remote-session/remote-session-parts.d.ts +34 -0
  75. package/dist/components/features/remote-session/remote-session-parts.d.ts.map +1 -0
  76. package/dist/components/features/remote-session/types.d.ts +33 -0
  77. package/dist/components/features/remote-session/types.d.ts.map +1 -0
  78. package/dist/components/help-center-pages/index.cjs +49 -49
  79. package/dist/components/help-center-pages/index.js +13 -13
  80. package/dist/components/index.cjs +64 -32
  81. package/dist/components/index.cjs.map +1 -1
  82. package/dist/components/index.js +44 -12
  83. package/dist/components/index.js.map +1 -1
  84. package/dist/components/meeting-scheduler/index.cjs +22 -22
  85. package/dist/components/meeting-scheduler/index.js +1 -1
  86. package/dist/components/navigation/index.cjs +5 -5
  87. package/dist/components/navigation/index.js +4 -4
  88. package/dist/components/onboarding-guides/index.cjs +5 -5
  89. package/dist/components/onboarding-guides/index.js +4 -4
  90. package/dist/components/tickets/index.cjs +6 -6
  91. package/dist/components/tickets/index.js +5 -5
  92. package/dist/components/ui/index.cjs +2 -2
  93. package/dist/components/ui/index.js +1 -1
  94. package/dist/index.cjs +38 -6
  95. package/dist/index.cjs.map +1 -1
  96. package/dist/index.js +37 -5
  97. package/dist/index.js.map +1 -1
  98. package/package.json +1 -1
  99. package/src/components/features/__tests__/remote-session.test.tsx +97 -0
  100. package/src/components/features/index.ts +1 -0
  101. package/src/components/features/remote-session/index.ts +36 -0
  102. package/src/components/features/remote-session/remote-access-request-card.tsx +102 -0
  103. package/src/components/features/remote-session/remote-session-block.tsx +182 -0
  104. package/src/components/features/remote-session/remote-session-chat-panel.tsx +178 -0
  105. package/src/components/features/remote-session/remote-session-dialog.tsx +77 -0
  106. package/src/components/features/remote-session/remote-session-overlay.tsx +33 -0
  107. package/src/components/features/remote-session/remote-session-parts.tsx +138 -0
  108. package/src/components/features/remote-session/types.ts +41 -0
  109. package/src/stories/RemoteSessionAccessRequest.stories.tsx +71 -0
  110. package/src/stories/RemoteSessionBlock.stories.tsx +164 -0
  111. package/src/stories/RemoteSessionDialog.stories.tsx +49 -0
  112. package/src/stories/RemoteSessionIndicator.stories.tsx +44 -0
  113. package/dist/chunk-CWXRZVDI.cjs.map +0 -1
  114. package/dist/chunk-KMW2PXBU.js.map +0 -1
  115. /package/dist/{chunk-6Z26ZMR4.js.map → chunk-A5NHAHV6.js.map} +0 -0
  116. /package/dist/{chunk-FNMQ2J44.js.map → chunk-AVAZ6BD6.js.map} +0 -0
  117. /package/dist/{chunk-MJI5GVT7.js.map → chunk-IQTQIBZ3.js.map} +0 -0
  118. /package/dist/{chunk-Y7ZQZOE6.js.map → chunk-LYJJH4D6.js.map} +0 -0
  119. /package/dist/{chunk-TSNGVWQO.js.map → chunk-LYQULSK4.js.map} +0 -0
  120. /package/dist/{chunk-KVDLL4LU.js.map → chunk-MCNQKS6I.js.map} +0 -0
  121. /package/dist/{chunk-DC3G4XGP.js.map → chunk-PKZGXRU2.js.map} +0 -0
  122. /package/dist/{chunk-JSG7WZKV.js.map → chunk-RK47E3PP.js.map} +0 -0
  123. /package/dist/{chunk-4HZXOQRM.js.map → chunk-RLNNV46K.js.map} +0 -0
  124. /package/dist/{chunk-Z5FHXCXF.js.map → chunk-TEIHEFXP.js.map} +0 -0
  125. /package/dist/{chunk-XBYF4SGD.js.map → chunk-TXFGG47A.js.map} +0 -0
  126. /package/dist/{chunk-CMYDGVTZ.js.map → chunk-X5XFKI6N.js.map} +0 -0
  127. /package/dist/{chunk-7WYWPELF.js.map → chunk-Z7QCNCBN.js.map} +0 -0
  128. /package/dist/{chunk-DZMDG7PC.js.map → chunk-ZAV2F4VX.js.map} +0 -0
@@ -0,0 +1,77 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import { Button } from '../../ui/button/button';
5
+ import type { RemoteSessionDialogVariant } from './types';
6
+
7
+ export interface RemoteSessionDialogProps {
8
+ variant: RemoteSessionDialogVariant;
9
+ /** Named in the "ended" and "connection lost" notices. */
10
+ organizationName: string;
11
+ /** End confirm: the end call is in flight. */
12
+ ending?: boolean;
13
+ /** End confirm: the user confirmed. */
14
+ onEndSession?: () => void;
15
+ /** Cancel (end confirm) or Close (the notices). */
16
+ onClose: () => void;
17
+ }
18
+
19
+ const NOTICE_COPY: Record<Exclude<RemoteSessionDialogVariant, 'end-confirm'>, { title: string; suffix: string }> = {
20
+ 'session-ended': { title: 'Remote session ended', suffix: 'has disconnected from your device.' },
21
+ 'connection-lost': { title: 'Connection lost', suffix: 'lost the connection to your device.' },
22
+ };
23
+
24
+ /** The session's terminal dialogs: the end confirm and the "it is over" notices. */
25
+ export function RemoteSessionDialog({
26
+ variant,
27
+ organizationName,
28
+ ending = false,
29
+ onEndSession,
30
+ onClose,
31
+ }: RemoteSessionDialogProps) {
32
+ if (variant === 'end-confirm') {
33
+ return (
34
+ <DialogCard>
35
+ <div className="flex w-full flex-col gap-[var(--spacing-system-xs)]">
36
+ <h1 className="w-full text-ods-text-primary text-h2">End remote session?</h1>
37
+ <p className="w-full text-ods-text-primary text-h4">
38
+ The technician will lose access to your device immediately.
39
+ </p>
40
+ </div>
41
+ <div className="flex w-full items-stretch gap-[var(--spacing-system-l)]">
42
+ <Button variant="outline" fullWidth disabled={ending} onClick={onClose}>
43
+ Cancel
44
+ </Button>
45
+ <Button variant="destructive" fullWidth loading={ending} onClick={onEndSession}>
46
+ End Session
47
+ </Button>
48
+ </div>
49
+ </DialogCard>
50
+ );
51
+ }
52
+
53
+ const copy = NOTICE_COPY[variant];
54
+ return (
55
+ <DialogCard>
56
+ <div className="flex w-full flex-col gap-[var(--spacing-system-xs)]">
57
+ <h1 className="w-full text-ods-text-primary text-h2">{copy.title}</h1>
58
+ <p className="w-full text-ods-text-primary text-h4">
59
+ <span className="text-ods-accent">{organizationName}</span> {copy.suffix}
60
+ </p>
61
+ </div>
62
+ <div className="flex w-full justify-end">
63
+ <Button variant="outline" className="w-1/2" onClick={onClose}>
64
+ Close
65
+ </Button>
66
+ </div>
67
+ </DialogCard>
68
+ );
69
+ }
70
+
71
+ function DialogCard({ children }: { children: ReactNode }) {
72
+ return (
73
+ <div className="flex w-full flex-col gap-[var(--spacing-system-l)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-xl)]">
74
+ {children}
75
+ </div>
76
+ );
77
+ }
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+
3
+ import { cn } from '../../../utils/cn';
4
+ import { MonitorOffIcon } from '../../icons-v2-generated';
5
+
6
+ /*
7
+ * The always-visible "your screen is being controlled" indicator: a border
8
+ * around the screen and an End Session pill hanging from its top edge.
9
+ * The colour is the ODS warning token, not the accent: the accent follows
10
+ * tenant branding, and this indicator must stay a recognisable fixed yellow.
11
+ */
12
+
13
+ /** The warning-yellow frame drawn around a whole screen; it never takes pointer events. */
14
+ export function RemoteSessionBorder({ className }: { className?: string }) {
15
+ return <div className={cn('pointer-events-none size-full border-4 border-ods-warning', className)} />;
16
+ }
17
+
18
+ /**
19
+ * The End Session pill. It sits flush against the top edge of the screen, so
20
+ * only its bottom corners are rounded and it reads as one piece with the border.
21
+ */
22
+ export function RemoteSessionPill({ onEndSession }: { onEndSession: () => void }) {
23
+ return (
24
+ <button
25
+ type="button"
26
+ className="flex items-center gap-[var(--spacing-system-xsf)] rounded-b-md bg-ods-warning px-[var(--spacing-system-m)] py-[var(--spacing-system-xs)] text-ods-text-on-accent"
27
+ onClick={onEndSession}
28
+ >
29
+ <MonitorOffIcon size={24} color="currentColor" />
30
+ <span className="whitespace-nowrap text-h3">End Session</span>
31
+ </button>
32
+ );
33
+ }
@@ -0,0 +1,138 @@
1
+ 'use client';
2
+
3
+ import { type CSSProperties, type ReactNode, useEffect, useState } from 'react';
4
+ import { cn } from '../../../utils/cn';
5
+ import { DraggerIcon, RecordingIcon } from '../../icons-v2-generated';
6
+ import { OpenFrameLogo } from '../../icons/openframe-logo';
7
+ import { SquareAvatar } from '../../ui/square-avatar';
8
+ import type { RemoteSessionDragHandlers } from './types';
9
+
10
+ /**
11
+ * The remote session surfaces are fixed-size desktop windows (440 / 600 px
12
+ * wide), so the responsive ODS scale would step their typography and spacing
13
+ * down to the mobile values below the 800 px breakpoint. The scope pins the
14
+ * scale to its desktop values; everything inside keeps the regular utilities.
15
+ */
16
+ const DESKTOP_SCALE = {
17
+ '--spacing-system-xs': '0.5rem',
18
+ '--spacing-system-s': '0.75rem',
19
+ '--spacing-system-m': '1rem',
20
+ '--spacing-system-l': '1.5rem',
21
+ '--spacing-system-xl': '2.5rem',
22
+ '--font-size-h1-title': '3.5rem',
23
+ '--font-size-h2-sub-title': '2rem',
24
+ '--font-size-h3-body': '1.125rem',
25
+ '--font-size-h4-body': '1.125rem',
26
+ '--font-size-h5-caption': '0.875rem',
27
+ '--font-size-h6-caption': '0.875rem',
28
+ '--font-line-space-h1-main-title': '4rem',
29
+ '--font-line-space-h2-sub-title': '2.5rem',
30
+ '--font-line-space-h3-body': '1.5rem',
31
+ '--font-line-space-h4-body': '1.5rem',
32
+ '--font-line-space-h5-caption': '1.25rem',
33
+ '--font-line-space-h6-caption': '1.25rem',
34
+ } as CSSProperties;
35
+
36
+ export interface RemoteSessionScopeProps {
37
+ children: ReactNode;
38
+ className?: string;
39
+ }
40
+
41
+ /**
42
+ * Root of every remote session surface: the desktop type and spacing scale,
43
+ * and the OpenFrame brand whatever the host app's tenant branding is - the
44
+ * "you are being helped" surfaces are OpenFrame-branded by design.
45
+ */
46
+ export function RemoteSessionScope({ children, className }: RemoteSessionScopeProps) {
47
+ return (
48
+ <div data-app-type="openframe" style={DESKTOP_SCALE} className={className}>
49
+ {children}
50
+ </div>
51
+ );
52
+ }
53
+
54
+ export interface RemoteSessionOrgLogoProps {
55
+ name: string;
56
+ logoUrl?: string | null;
57
+ /** Width and height in px. */
58
+ sizePx: number;
59
+ className?: string;
60
+ }
61
+
62
+ /** Organization logo tile; the OpenFrame mark when there is no logo. */
63
+ export function RemoteSessionOrgLogo({ name, logoUrl, sizePx, className }: RemoteSessionOrgLogoProps) {
64
+ if (logoUrl) {
65
+ return (
66
+ <SquareAvatar
67
+ variant="square"
68
+ fit="contain"
69
+ sizePx={sizePx}
70
+ src={logoUrl}
71
+ alt={name}
72
+ fallback={name}
73
+ className={cn('shrink-0 rounded-md border border-ods-border bg-ods-card', className)}
74
+ />
75
+ );
76
+ }
77
+ return (
78
+ <div
79
+ role="img"
80
+ aria-label={name}
81
+ style={{ width: sizePx, height: sizePx }}
82
+ className={cn(
83
+ 'flex shrink-0 items-center justify-center rounded-md border border-ods-border bg-ods-card',
84
+ className,
85
+ )}
86
+ >
87
+ <OpenFrameLogo className="size-1/2" />
88
+ </div>
89
+ );
90
+ }
91
+
92
+ /** The green SESSION RECORDING tag. */
93
+ export function RemoteSessionRecordingTag() {
94
+ return (
95
+ <div className="flex h-8 items-center gap-[var(--spacing-system-xs)] self-start rounded-md bg-ods-success-secondary p-[var(--spacing-system-xsf)]">
96
+ <RecordingIcon size={16} color="currentColor" className="text-ods-success" />
97
+ <span className="text-ods-success text-h5">Session Recording</span>
98
+ </div>
99
+ );
100
+ }
101
+
102
+ /** Grab cursor on the surfaces the window is moved by. */
103
+ export const REMOTE_SESSION_DRAG_HANDLE_CLASS = 'cursor-grab active:cursor-grabbing';
104
+
105
+ /** The dragger glyph in the top-right corner of a block header: the visible handle for moving it. */
106
+ export function RemoteSessionDragger({ dragHandlers }: { dragHandlers?: RemoteSessionDragHandlers }) {
107
+ return (
108
+ <span
109
+ aria-hidden
110
+ className={cn(
111
+ 'absolute right-[var(--spacing-system-xsf)] top-[var(--spacing-system-xsf)] text-ods-text-secondary',
112
+ dragHandlers && REMOTE_SESSION_DRAG_HANDLE_CLASS,
113
+ )}
114
+ {...dragHandlers}
115
+ >
116
+ <DraggerIcon size={24} color="currentColor" />
117
+ </span>
118
+ );
119
+ }
120
+
121
+ /** `m:ss` for an elapsed duration; `h:mm:ss` from the first hour on. */
122
+ export function formatRemoteSessionElapsed(elapsedMs: number): string {
123
+ const total = Math.max(0, Math.floor(elapsedMs / 1000));
124
+ const hours = Math.floor(total / 3600);
125
+ const minutes = Math.floor((total % 3600) / 60);
126
+ const seconds = String(total % 60).padStart(2, '0');
127
+ return hours > 0 ? `${hours}:${String(minutes).padStart(2, '0')}:${seconds}` : `${minutes}:${seconds}`;
128
+ }
129
+
130
+ /** The session's elapsed time since `startedAt` (ISO), ticking once a second. */
131
+ export function useRemoteSessionTimer(startedAt: string): string {
132
+ const [nowMs, setNowMs] = useState(() => Date.now());
133
+ useEffect(() => {
134
+ const timer = setInterval(() => setNowMs(Date.now()), 1_000);
135
+ return () => clearInterval(timer);
136
+ }, []);
137
+ return formatRemoteSessionElapsed(nowMs - Date.parse(startedAt));
138
+ }
@@ -0,0 +1,41 @@
1
+ import type { HTMLAttributes } from 'react';
2
+
3
+ /**
4
+ * Who is on the other end of a remote session, as the end user's surfaces show
5
+ * it: the organization always, the technician when the backend names one.
6
+ */
7
+ export interface RemoteSessionParty {
8
+ organizationName: string;
9
+ organizationLogoUrl?: string | null;
10
+ /** Shown under the organization name; offered as a link only when it is https. */
11
+ organizationSiteUrl?: string | null;
12
+ /** Present for the technician variant of the session block; the organization variant otherwise. */
13
+ technicianName?: string | null;
14
+ technicianAvatarUrl?: string | null;
15
+ }
16
+
17
+ /** The end user's answer to a remote access request. */
18
+ export type RemoteAccessDecision = 'APPROVED' | 'DENIED';
19
+
20
+ export type RemoteSessionChatAuthor = 'technician' | 'user' | 'system';
21
+
22
+ export interface RemoteSessionChatMessage {
23
+ id: string;
24
+ author: RemoteSessionChatAuthor;
25
+ /** The technician's display name on their rows; the party's technician otherwise. */
26
+ name?: string | null;
27
+ text: string;
28
+ at: Date;
29
+ }
30
+
31
+ /** The terminal dialogs of a session: the end confirm and the two "it is over" notices. */
32
+ export type RemoteSessionDialogVariant = 'end-confirm' | 'session-ended' | 'connection-lost';
33
+
34
+ /**
35
+ * Pointer handlers a host attaches to the surfaces the user moves the window
36
+ * by (the desktop chat starts a native window drag from them).
37
+ */
38
+ export type RemoteSessionDragHandlers = Pick<
39
+ HTMLAttributes<HTMLElement>,
40
+ 'onPointerDown' | 'onPointerMove' | 'onPointerUp'
41
+ >;
@@ -0,0 +1,71 @@
1
+ import type { Meta, StoryObj } from '@storybook/nextjs-vite';
2
+ import type React from 'react';
3
+ import { fn } from 'storybook/test';
4
+
5
+ import {
6
+ RemoteAccessRequestCard,
7
+ RemoteSessionScope,
8
+ type RemoteSessionParty,
9
+ } from '../components/features/remote-session';
10
+
11
+ /**
12
+ * The end user's consent dialog for a remote access request, shown by the
13
+ * desktop chat in a 600 px always-on-top window. The host owns the request,
14
+ * the delivery ack and the decision call; the card only renders and reports
15
+ * the click.
16
+ */
17
+
18
+ const party: RemoteSessionParty = {
19
+ organizationName: 'TechFlow Solutions',
20
+ organizationSiteUrl: 'https://www.techflow.com',
21
+ technicianName: 'Mike Rodriguez',
22
+ };
23
+
24
+ function WindowDecorator(Story: React.ComponentType) {
25
+ return (
26
+ <RemoteSessionScope className="w-[600px]">
27
+ <Story />
28
+ </RemoteSessionScope>
29
+ );
30
+ }
31
+
32
+ const meta = {
33
+ title: 'Remote Session/Client/Access Request',
34
+ component: RemoteAccessRequestCard,
35
+ tags: ['autodocs'],
36
+ decorators: [WindowDecorator],
37
+ args: { party, onDecide: fn(), onOpenSite: fn() },
38
+ } satisfies Meta<typeof RemoteAccessRequestCard>;
39
+
40
+ export default meta;
41
+ type Story = StoryObj<typeof meta>;
42
+
43
+ /** A recorded session: the recording notice above the consent line. */
44
+ export const Recorded: Story = {
45
+ args: { showRecordingNotice: true },
46
+ };
47
+
48
+ /** Not recorded: no notice. */
49
+ export const NotRecorded: Story = {
50
+ args: { showRecordingNotice: false },
51
+ };
52
+
53
+ /** No site and no logo: the OpenFrame mark, no website button. */
54
+ export const WithoutSite: Story = {
55
+ args: { party: { organizationName: 'TechFlow Solutions' } },
56
+ };
57
+
58
+ /** A non-https site is shown but never offered as a link. */
59
+ export const PlainHttpSite: Story = {
60
+ args: { party: { ...party, organizationSiteUrl: 'http://techflow.local' } },
61
+ };
62
+
63
+ /** Allow Access sent: both buttons lock, the chosen one spins. */
64
+ export const Allowing: Story = {
65
+ args: { showRecordingNotice: true, deciding: 'APPROVED' },
66
+ };
67
+
68
+ /** The decision could not be sent: the error replaces the closing line. */
69
+ export const DecisionFailed: Story = {
70
+ args: { showRecordingNotice: true, error: 'Could not reach the server. Try again.' },
71
+ };
@@ -0,0 +1,164 @@
1
+ import type { Meta, StoryObj } from '@storybook/nextjs-vite';
2
+ import type React from 'react';
3
+ import { useState } from 'react';
4
+ import { fn } from 'storybook/test';
5
+
6
+ import {
7
+ RemoteSessionBlockFrame,
8
+ RemoteSessionChatPanel,
9
+ RemoteSessionMinimizedChip,
10
+ RemoteSessionScope,
11
+ RemoteSessionSummary,
12
+ type RemoteSessionChatMessage,
13
+ type RemoteSessionParty,
14
+ } from '../components/features/remote-session';
15
+
16
+ /**
17
+ * The session block the end user sees while a technician is connected: the
18
+ * summary, the chat with the technician, and the minimized chip. In the
19
+ * desktop chat it lives in its own 440 px window the user can drag around.
20
+ */
21
+
22
+ const organization: RemoteSessionParty = { organizationName: 'TechFlow Solutions' };
23
+ const technician: RemoteSessionParty = { ...organization, technicianName: 'Mike Rodriguez' };
24
+
25
+ const minutesAgo = (minutes: number) => new Date(Date.now() - minutes * 60_000);
26
+
27
+ const messages: RemoteSessionChatMessage[] = [
28
+ { id: 'm1', author: 'system', text: 'Mike Rodriguez joined the session', at: minutesAgo(6) },
29
+ { id: 'm2', author: 'technician', text: 'Hi! I can see your screen now.', at: minutesAgo(5) },
30
+ { id: 'm3', author: 'user', text: 'Great, the printer is the one on the left.', at: minutesAgo(4) },
31
+ { id: 'm4', author: 'technician', text: 'Got it - reinstalling the driver now.', at: minutesAgo(2) },
32
+ ];
33
+
34
+ function WindowDecorator(Story: React.ComponentType) {
35
+ return (
36
+ <RemoteSessionScope>
37
+ <Story />
38
+ </RemoteSessionScope>
39
+ );
40
+ }
41
+
42
+ const meta = {
43
+ title: 'Remote Session/Client/Session Block',
44
+ component: RemoteSessionSummary,
45
+ tags: ['autodocs'],
46
+ decorators: [WindowDecorator],
47
+ args: {
48
+ party: technician,
49
+ elapsed: '5:23',
50
+ showRecordingTag: true,
51
+ onOpenChat: fn(),
52
+ onEndSession: fn(),
53
+ },
54
+ render: args => (
55
+ <RemoteSessionBlockFrame onHide={fn()}>
56
+ <RemoteSessionSummary {...args} dragHandlers={{ onPointerDown: fn() }} />
57
+ </RemoteSessionBlockFrame>
58
+ ),
59
+ } satisfies Meta<typeof RemoteSessionSummary>;
60
+
61
+ export default meta;
62
+ type Story = StoryObj<typeof meta>;
63
+
64
+ /** A named technician: avatar and "<name> connected". */
65
+ export const Technician: Story = {};
66
+
67
+ /** No technician name on the session: the organization's logo and "IT support team is connected". */
68
+ export const Organization: Story = {
69
+ args: { party: organization },
70
+ };
71
+
72
+ /** Not recorded: no SESSION RECORDING tag. */
73
+ export const NotRecorded: Story = {
74
+ args: { showRecordingTag: false },
75
+ };
76
+
77
+ /** A long name truncates before the dragger in the corner. */
78
+ export const LongName: Story = {
79
+ args: { party: { ...technician, technicianName: 'Maximilian Alexander Rodriguez-Fernandez' } },
80
+ };
81
+
82
+ /** The chat with the technician, opened from the block. */
83
+ export const Chat: Story = {
84
+ render: args => {
85
+ const [draft, setDraft] = useState('');
86
+ return (
87
+ <RemoteSessionBlockFrame expanded onHide={fn()}>
88
+ <RemoteSessionChatPanel
89
+ party={args.party}
90
+ elapsed={args.elapsed}
91
+ showRecordingTag={args.showRecordingTag}
92
+ messages={messages}
93
+ draft={draft}
94
+ onDraftChange={setDraft}
95
+ onSend={fn()}
96
+ onCloseChat={fn()}
97
+ onEndSession={args.onEndSession}
98
+ dragHandlers={{ onPointerDown: fn() }}
99
+ />
100
+ </RemoteSessionBlockFrame>
101
+ );
102
+ },
103
+ };
104
+
105
+ /** The chat while it is still connecting: the composer is disabled. */
106
+ export const ChatConnecting: Story = {
107
+ render: args => (
108
+ <RemoteSessionBlockFrame expanded onHide={fn()}>
109
+ <RemoteSessionChatPanel
110
+ party={args.party}
111
+ elapsed={args.elapsed}
112
+ messages={[]}
113
+ ready={false}
114
+ draft=""
115
+ onDraftChange={fn()}
116
+ onSend={fn()}
117
+ onCloseChat={fn()}
118
+ onEndSession={args.onEndSession}
119
+ />
120
+ </RemoteSessionBlockFrame>
121
+ ),
122
+ };
123
+
124
+ /** "Hide this Block": the technician's avatar and a chevron to bring the block back. */
125
+ export const MinimizedTechnician: Story = {
126
+ render: args => <RemoteSessionMinimizedChip party={args.party} onExpand={fn()} />,
127
+ };
128
+
129
+ /** The minimized chip of the organization variant. */
130
+ export const MinimizedOrganization: Story = {
131
+ render: () => <RemoteSessionMinimizedChip party={organization} onExpand={fn()} />,
132
+ };
133
+
134
+ /** The whole block, switching between summary, chat and the chip like the desktop window does. */
135
+ export const Interactive: Story = {
136
+ render: args => {
137
+ const [panel, setPanel] = useState<'summary' | 'chat'>('summary');
138
+ const [minimized, setMinimized] = useState(false);
139
+ const [draft, setDraft] = useState('');
140
+ const [thread, setThread] = useState(messages);
141
+ if (minimized) return <RemoteSessionMinimizedChip party={args.party} onExpand={() => setMinimized(false)} />;
142
+ return (
143
+ <RemoteSessionBlockFrame expanded={panel === 'chat'} onHide={() => setMinimized(true)}>
144
+ {panel === 'summary' ? (
145
+ <RemoteSessionSummary {...args} onOpenChat={() => setPanel('chat')} />
146
+ ) : (
147
+ <RemoteSessionChatPanel
148
+ party={args.party}
149
+ elapsed={args.elapsed}
150
+ showRecordingTag={args.showRecordingTag}
151
+ messages={thread}
152
+ draft={draft}
153
+ onDraftChange={setDraft}
154
+ onSend={text =>
155
+ setThread(current => [...current, { id: `m${current.length + 1}`, author: 'user', text, at: new Date() }])
156
+ }
157
+ onCloseChat={() => setPanel('summary')}
158
+ onEndSession={args.onEndSession}
159
+ />
160
+ )}
161
+ </RemoteSessionBlockFrame>
162
+ );
163
+ },
164
+ };
@@ -0,0 +1,49 @@
1
+ import type { Meta, StoryObj } from '@storybook/nextjs-vite';
2
+ import type React from 'react';
3
+ import { fn } from 'storybook/test';
4
+
5
+ import { RemoteSessionDialog, RemoteSessionScope } from '../components/features/remote-session';
6
+
7
+ /** The session's terminal dialogs, each shown by the desktop chat in its own 600 px window. */
8
+
9
+ function WindowDecorator(Story: React.ComponentType) {
10
+ return (
11
+ <RemoteSessionScope className="w-[600px]">
12
+ <Story />
13
+ </RemoteSessionScope>
14
+ );
15
+ }
16
+
17
+ const meta = {
18
+ title: 'Remote Session/Client/Dialogs',
19
+ component: RemoteSessionDialog,
20
+ tags: ['autodocs'],
21
+ decorators: [WindowDecorator],
22
+ args: {
23
+ variant: 'end-confirm',
24
+ organizationName: 'TechFlow Solutions',
25
+ onEndSession: fn(),
26
+ onClose: fn(),
27
+ },
28
+ } satisfies Meta<typeof RemoteSessionDialog>;
29
+
30
+ export default meta;
31
+ type Story = StoryObj<typeof meta>;
32
+
33
+ /** End Session from the block or the pill asks first. */
34
+ export const EndConfirm: Story = {};
35
+
36
+ /** The end call is in flight. */
37
+ export const Ending: Story = {
38
+ args: { ending: true },
39
+ };
40
+
41
+ /** The technician (or the user) ended the session. */
42
+ export const SessionEnded: Story = {
43
+ args: { variant: 'session-ended' },
44
+ };
45
+
46
+ /** The relay dropped: the session is over, there is no rejoin. */
47
+ export const ConnectionLost: Story = {
48
+ args: { variant: 'connection-lost' },
49
+ };
@@ -0,0 +1,44 @@
1
+ import type { Meta, StoryObj } from '@storybook/nextjs-vite';
2
+ import { fn } from 'storybook/test';
3
+
4
+ import { RemoteSessionBorder, RemoteSessionPill, RemoteSessionScope } from '../components/features/remote-session';
5
+
6
+ /**
7
+ * The always-visible "your screen is being controlled" indicator: the yellow
8
+ * border around every screen and the End Session pill hanging from its top
9
+ * edge. The desktop chat draws each in its own click-through window.
10
+ */
11
+
12
+ const meta = {
13
+ title: 'Remote Session/Client/Screen Indicator',
14
+ component: RemoteSessionPill,
15
+ tags: ['autodocs'],
16
+ args: { onEndSession: fn() },
17
+ decorators: [
18
+ Story => (
19
+ <RemoteSessionScope>
20
+ <Story />
21
+ </RemoteSessionScope>
22
+ ),
23
+ ],
24
+ } satisfies Meta<typeof RemoteSessionPill>;
25
+
26
+ export default meta;
27
+ type Story = StoryObj<typeof meta>;
28
+
29
+ /** The pill on its own. */
30
+ export const Pill: Story = {};
31
+
32
+ /** Border and pill together, on a screen-sized stand-in. */
33
+ export const Screen: Story = {
34
+ render: args => (
35
+ <div className="relative h-[360px] w-[640px] bg-ods-bg">
36
+ <div className="absolute inset-0">
37
+ <RemoteSessionBorder />
38
+ </div>
39
+ <div className="absolute inset-x-0 top-0 flex justify-center">
40
+ <RemoteSessionPill {...args} />
41
+ </div>
42
+ </div>
43
+ ),
44
+ };