@qualified-salesforce/web 0.0.1-oidc-bootstrap → 0.1.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 (98) hide show
  1. package/CAPABILITY-REFERENCE.md +41 -0
  2. package/CHANGELOG.md +84 -0
  3. package/README.md +65 -0
  4. package/constants.generated.js +30 -2
  5. package/css/widget/multimodal_v2/index.split-css.panda.css +265 -47
  6. package/package.json +18 -4
  7. package/packs/widget/api/docking/viewport_lock.js +93 -2
  8. package/packs/widget/esm/index.d.ts +134 -0
  9. package/packs/widget/esm/index.js +18 -2
  10. package/packs/widget/esm/qualified.css +265 -47
  11. package/src/graphql-types.js +74 -1
  12. package/src/url.js +4 -1
  13. package/vendor/actioncable/connection.js +40 -3
  14. package/vendor/actioncable/connection_monitor.js +8 -0
  15. package/vendor/actioncable/consumer.js +7 -1
  16. package/vendor/actioncable/index.js +5 -2
  17. package/vendor/actioncable/sse_socket.js +262 -0
  18. package/widget/analytics.js +8 -0
  19. package/widget/api_handler/base_api_handler.js +24 -0
  20. package/widget/booker_outcome_tracker.js +58 -20
  21. package/widget/capabilities.js +190 -0
  22. package/widget/channel.js +38 -6
  23. package/widget/constants.generated.js +2 -1
  24. package/widget/multimodal_v2/components/AppLayout.js +1 -1
  25. package/widget/multimodal_v2/components/app/Foreground.js +10 -9
  26. package/widget/multimodal_v2/components/app/Input.js +8 -3
  27. package/widget/multimodal_v2/components/app/LightboxExperience.js +17 -3
  28. package/widget/multimodal_v2/components/app/Main.js +87 -31
  29. package/widget/multimodal_v2/components/app/MessengerButton.js +18 -1
  30. package/widget/multimodal_v2/components/chat/ChatInputForm.js +8 -6
  31. package/widget/multimodal_v2/components/chat/timeline/CalendarEventPickRenderer.js +2 -2
  32. package/widget/multimodal_v2/components/chat/timeline/PresentDeckRenderer.js +346 -170
  33. package/widget/multimodal_v2/components/chat/timeline/SystemRenderer.js +4 -7
  34. package/widget/multimodal_v2/components/decks/index.js +506 -52
  35. package/widget/multimodal_v2/components/helpers/content_click_payloads.js +67 -1
  36. package/widget/multimodal_v2/components/lightbox_gather/index.js +4 -4
  37. package/widget/multimodal_v2/components/meetings/MeetingBookerEntry.js +8 -1
  38. package/widget/multimodal_v2/components/meetings/MeetingOfferEmailStep.js +1 -1
  39. package/widget/multimodal_v2/components/meetings/MeetingSummaryRail.js +1 -1
  40. package/widget/multimodal_v2/components/meetings/MeetingTimeSelector.js +1 -1
  41. package/widget/multimodal_v2/components/multimodal/Controls.js +11 -3
  42. package/widget/multimodal_v2/components/multimodal/useMultimodal.js +19 -7
  43. package/widget/multimodal_v2/components/presentations/index.js +61 -0
  44. package/widget/multimodal_v2/constants.js +1 -1
  45. package/widget/multimodal_v2/images/icons.js +17 -1
  46. package/widget/multimodal_v2/index.panda.js +2 -2
  47. package/widget/multimodal_v2/locales/cs.js +8 -1
  48. package/widget/multimodal_v2/locales/da.js +8 -1
  49. package/widget/multimodal_v2/locales/de.js +8 -1
  50. package/widget/multimodal_v2/locales/en.js +8 -1
  51. package/widget/multimodal_v2/locales/en_AU.js +8 -1
  52. package/widget/multimodal_v2/locales/en_GB.js +8 -1
  53. package/widget/multimodal_v2/locales/es.js +8 -1
  54. package/widget/multimodal_v2/locales/es_LA.js +8 -1
  55. package/widget/multimodal_v2/locales/fi.js +8 -1
  56. package/widget/multimodal_v2/locales/fr.js +8 -1
  57. package/widget/multimodal_v2/locales/fr_CA.js +8 -1
  58. package/widget/multimodal_v2/locales/id.js +8 -1
  59. package/widget/multimodal_v2/locales/it.js +8 -1
  60. package/widget/multimodal_v2/locales/ja.js +8 -1
  61. package/widget/multimodal_v2/locales/ko.js +8 -1
  62. package/widget/multimodal_v2/locales/nb.js +8 -1
  63. package/widget/multimodal_v2/locales/nl.js +8 -1
  64. package/widget/multimodal_v2/locales/pl.js +8 -1
  65. package/widget/multimodal_v2/locales/pt_BR.js +8 -1
  66. package/widget/multimodal_v2/locales/ru.js +8 -1
  67. package/widget/multimodal_v2/locales/sv.js +8 -1
  68. package/widget/multimodal_v2/locales/th.js +8 -1
  69. package/widget/multimodal_v2/locales/tr.js +8 -1
  70. package/widget/multimodal_v2/locales/vi.js +8 -1
  71. package/widget/multimodal_v2/locales/zh_CN.js +8 -1
  72. package/widget/multimodal_v2/locales/zh_TW.js +8 -1
  73. package/widget/multimodal_v2/multimodal.js +5 -1
  74. package/widget/multimodal_v2/multimodal_channel.js +13 -8
  75. package/widget/multimodal_v2/signals/messages_signal.js +4 -0
  76. package/widget/multimodal_v2/stores/ai_conversation_store.js +6 -2
  77. package/widget/multimodal_v2/stores/artifact_store.js +352 -27
  78. package/widget/multimodal_v2/stores/call_store.js +40 -1
  79. package/widget/multimodal_v2/stores/call_ui_adapter.js +1 -1
  80. package/widget/multimodal_v2/stores/decks_store.js +405 -39
  81. package/widget/multimodal_v2/stores/meeting_store.js +15 -10
  82. package/widget/multimodal_v2/stores/presentation_ga_adapter.js +64 -0
  83. package/widget/multimodal_v2/stores/presentation_telemetry_store.js +556 -0
  84. package/widget/multimodal_v2/stores/presentations_store.js +1 -0
  85. package/widget/multimodal_v2/stores/root_store.js +157 -268
  86. package/widget/multimodal_v2/stores/screen_share_store.js +41 -21
  87. package/widget/multimodal_v2/stores/translation_store.js +9 -0
  88. package/widget/multimodal_v2/stores/ui_store.js +7 -7
  89. package/widget/multimodal_v2/stores/website_navigation_store.js +3 -1
  90. package/widget/multimodal_v2/text_turn_response_tracker.js +419 -0
  91. package/widget/multimodal_v2/types.js +21 -1
  92. package/widget/multimodal_v2/utils/locale.js +2 -1
  93. package/widget/multimodal_v2/utils/signals.js +35 -4
  94. package/widget/pardot_visitor.js +7 -1
  95. package/widget/server-logger.js +97 -2
  96. package/widget/tracker.js +69 -21
  97. package/widget/widget/page_mirror_factory.js +9 -1
  98. package/widget/widget/widget.js +29 -4
@@ -1,25 +1,76 @@
1
- import { jsxRuntime_module_u, utils_module_o, useSignalEffect } from "../../../../js/multimodal_v2.js";
1
+ import { preact_module_k, useComputed, jsxRuntime_module_u, utils_module_o, useSignalEffect } from "../../../../js/multimodal_v2.js";
2
+ import { SpeakNowMicIcon, ArrowRightIcon } from "../../images/icons.js";
2
3
  import "../../styled-system/jsx/index.js";
3
4
  import { slideZoomStyle } from "../../utils/slide_zoom.js";
5
+ import { RichText } from "../helpers/RichText.js";
6
+ import { Status } from "../multimodal/Status.js";
7
+ import { Button } from "../shared/Button.js";
8
+ import { DeckMode as types_DeckMode, StartCallTrigger as types_StartCallTrigger } from "../../types.js";
4
9
  import { styled } from "../../styled-system/jsx/factory.js";
5
10
 
11
+ function _extends() {
12
+ _extends = Object.assign || function(target) {
13
+ for(var i = 1; i < arguments.length; i++){
14
+ var source = arguments[i];
15
+ for(var key in source){
16
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
17
+ target[key] = source[key];
18
+ }
19
+ }
20
+ }
21
+ return target;
22
+ };
23
+ return _extends.apply(this, arguments);
24
+ }
6
25
 
7
26
  /** @jsxImportSource preact */
8
27
 
9
28
 
10
29
 
30
+
31
+
32
+
33
+
34
+
11
35
  /**
12
- * Deck is the entrypoint for the `present_deck` artifact.
13
- * Renders the current deck slide image when a deck is the active artifact.
36
+ * Prefix for every `data-component-name` this component emits, per mode.
37
+ *
38
+ * This is load-bearing, not cosmetic. `pointAtSlide` finds the element to zoom
39
+ * by querying `[data-component-name="Deck.SlideImage"]`
40
+ * (website_navigation_store), and the self-guided viewer must not be findable by
41
+ * it: the deck card stays available during a call once something has been
42
+ * presented, so the visitor can be reading slide 5 while Piper points at slide
43
+ * 2. Were the viewer's image named `Deck.SlideImage`, her focal point would be
44
+ * applied to whatever slide they happen to be browsing, mid-interaction.
45
+ *
46
+ * Every name goes through `componentName` below so the guarantee holds for the
47
+ * whole subtree rather than one attribute, and index.test.tsx asserts no
48
+ * `Deck.*` element exists in self-guided mode.
49
+ *
50
+ * The prefixes are a DOM contract rather than a mirror of `DeckMode`'s names:
51
+ * guided must stay exactly `Deck`, and the other prefix only has to be
52
+ * something that does not begin with `Deck.` — `SelfGuidedDeck.SlideImage`
53
+ * qualifies, since the query and the guard both match on that leading `Deck.`.
54
+ */ const COMPONENT_NAME_PREFIX = {
55
+ [types_DeckMode.Guided]: "Deck",
56
+ [types_DeckMode.SelfGuided]: "SelfGuidedDeck"
57
+ };
58
+ /**
59
+ * Deck is the entrypoint for the `present_deck` artifact, in either mode. Which
60
+ * mode is a property of the store's playback state.
14
61
  */ const Deck = (param)=>{
15
62
  let { rootStore } = param;
16
63
  var _ref, _currentDeck_pageTitle;
17
- var _rootStore_decksStore_value;
64
+ const { t } = rootStore.translationStore;
18
65
  const zoomRef = utils_module_o(null);
19
66
  // Zoom the slide in place toward wherever Piper is pointing (see pointAtSlide + utils/slide_zoom),
20
67
  // mirroring the demo <video> (presentations/index.tsx). Applied to an inner layer so the <img>'s
21
68
  // static white-strip transform and SlideClip's shadow/rounding are left untouched by the zoom.
22
69
  // Only touches the style during a call (when the nav store exists).
70
+ //
71
+ // Safe to run in both modes only because of the early return: self-guided mode never renders
72
+ // SlideZoomLayer, so `layer` is null and this bails before reading `slideZoom` — never
73
+ // subscribing to it. Rendering the zoom layer in self-guided mode would break that.
23
74
  useSignalEffect(()=>{
24
75
  const layer = zoomRef.value;
25
76
  const navStore = rootStore.websiteNavigationStore.value;
@@ -30,37 +81,211 @@ import { styled } from "../../styled-system/jsx/factory.js";
30
81
  layer.style.clipPath = style.clipPath;
31
82
  layer.style.transition = style.transition;
32
83
  });
33
- const currentDeck = (_rootStore_decksStore_value = rootStore.decksStore.value) == null ? void 0 : _rootStore_decksStore_value.currentDeck.value;
34
- if (!currentDeck) return null;
35
- const isSequence = currentDeck.sequenceLength > 1;
84
+ // Whether handing the deck back to Piper is on the table. Three things have to
85
+ // hold, and each closes off a different dead end:
86
+ //
87
+ // - A call has to be possible at all. Mode and capability agree in production
88
+ // (UIStore#validateConversationMode forces the mode down to TEXT when voice and
89
+ // video are both disallowed), so this is the belt to the mode's braces: without
90
+ // it a mid-call voice failover would leave a button offering a call that cannot
91
+ // start. The same pair PresentDeckRenderer weighs before offering its own call.
92
+ // - Text-only has no call to switch to.
93
+ // - Neither does a call already running, and that is a live state rather than a
94
+ // theoretical one: the viewer stays open during a call, so the visitor can be
95
+ // reading slide 5 while Piper points at slide 2.
96
+ //
97
+ // Above the early return because it is a hook, and computed rather than read
98
+ // straight so a failover mid-read takes the button away with it.
99
+ const voiceSwitchAvailable = useComputed(()=>(rootStore.uiStore.allowedConversationModes.voice.value || rootStore.uiStore.allowedConversationModes.video.value) && !rootStore.conversationMode.text.value && !rootStore.callInProgress.value);
100
+ // The message over the dimmed slide while the call is starting, in two phases:
101
+ // the call being established, then the presentation itself.
102
+ //
103
+ // The first phase is over when the call UI's own connecting state is over, on
104
+ // that state's signal rather than one of this overlay's choosing
105
+ // (ArtifactStore#deckPresentationConnecting wraps it). It borrows the call UI's
106
+ // words as well, so what the two surfaces say about one event is the same
107
+ // sentence, and neither the sentence nor the moment it stops being true can
108
+ // drift from the panel's.
109
+ //
110
+ // Live, with nothing latched on the way past: a label that held its phase could
111
+ // only hold the wrong one, which is what it did — Piper would start talking over
112
+ // an overlay still promising to connect the visitor to her. Following the signal
113
+ // means a reconnect takes the text back to connecting, which is the same thing
114
+ // the call UI does at that moment and so reads as one interface rather than two.
115
+ //
116
+ // A signal rather than a value read during render: passed straight to `Status`,
117
+ // the text advances without this component re-rendering, so the slide behind it
118
+ // is not even diffed, let alone remounted.
119
+ const startingMessage = useComputed(()=>{
120
+ var _rootStore_activeConversation_aiSdr_value;
121
+ if (!rootStore.artifactStore.deckPresentationConnecting.value) {
122
+ return t("presentation.starting");
123
+ }
124
+ const name = ((_rootStore_activeConversation_aiSdr_value = rootStore.activeConversation.aiSdr.value) == null ? void 0 : _rootStore_activeConversation_aiSdr_value.name) || t("video.yourAssistant");
125
+ return t("video.connectingTo", {
126
+ name
127
+ });
128
+ });
129
+ const decksStore = rootStore.decksStore.value;
130
+ const currentDeck = decksStore == null ? void 0 : decksStore.currentDeck.value;
131
+ if (!decksStore || !currentDeck) return null;
132
+ const { sequencePosition, sequenceLength, mode, starting } = currentDeck;
133
+ const componentName = (part)=>COMPONENT_NAME_PREFIX[mode] + "." + part;
134
+ // Guided mode is Piper already presenting, so there is nothing to hand back.
135
+ // Neither is there while the call that takes it off them is starting: the offer
136
+ // has been accepted, and the panel it lives on is gone for the duration.
137
+ const showVoiceSwitch = mode === types_DeckMode.SelfGuided && !starting && voiceSwitchAvailable.value;
138
+ // Hand the deck back to Piper from wherever the visitor has read to, not from
139
+ // the slide the share opened on — resuming where they are is the whole point of
140
+ // the switch. The brief and the target duration come off the share driving the
141
+ // viewer, which is the same message `currentDeck` is computed from (DecksStore),
142
+ // so the call opens with the intent the off-call share recorded, exactly as the
143
+ // timeline card's own call does (PresentDeckRenderer#activate).
144
+ const switchToVoice = ()=>{
145
+ if (rootStore.callInProgress.peek()) return;
146
+ const artifactMessage = rootStore.artifact.message.peek();
147
+ const share = (artifactMessage == null ? void 0 : artifactMessage.type) === "present_deck" ? artifactMessage : undefined;
148
+ rootStore.getCallUiAdapter().actions.startCall({
149
+ trigger: _extends({
150
+ type: types_StartCallTrigger.StartDeck,
151
+ deckId: currentDeck.deckId,
152
+ slideIndex: currentDeck.currentIndex
153
+ }, (share == null ? void 0 : share.presentationBrief) && {
154
+ presentationBrief: share.presentationBrief
155
+ }, (share == null ? void 0 : share.targetDurationSeconds) && {
156
+ targetDurationSeconds: share.targetDurationSeconds
157
+ })
158
+ });
159
+ };
160
+ const slideImage = /* Crossorigin like every other slide load: the assets origin only sends
161
+ `Access-Control-Allow-Origin` when the request carries an `Origin`
162
+ header, so a plain load here would cache an ACAO-less response and
163
+ fail the CORS check for the same url elsewhere. */ /*#__PURE__*/ jsxRuntime_module_u(SlideImage, {
164
+ mode: mode,
165
+ "data-component-name": componentName("SlideImage"),
166
+ crossOrigin: "anonymous",
167
+ src: currentDeck.slideImageUrls[currentDeck.currentIndex],
168
+ alt: (_ref = (_currentDeck_pageTitle = currentDeck.pageTitle) != null ? _currentDeck_pageTitle : currentDeck.deckTitle) != null ? _ref : "",
169
+ onLoad: ()=>decksStore.notifyCurrentSlideReady()
170
+ });
171
+ const progressDots = /*#__PURE__*/ jsxRuntime_module_u(ProgressDots, {
172
+ mode: mode,
173
+ "data-component-name": componentName("ProgressDots"),
174
+ // A bare div may not carry aria-label (axe: aria-prohibited-attr), and
175
+ // without the label the dots are announced as nothing at all.
176
+ role: "group",
177
+ "aria-label": t("presentation.slidePosition", {
178
+ current: sequencePosition + 1,
179
+ total: sequenceLength
180
+ }),
181
+ children: Array.from({
182
+ length: sequenceLength
183
+ }).map((_, index)=>/*#__PURE__*/ jsxRuntime_module_u(Dot, {
184
+ mode: mode,
185
+ "data-active": index === sequencePosition ? "true" : undefined
186
+ }, index))
187
+ });
36
188
  return /*#__PURE__*/ jsxRuntime_module_u(Layout, {
37
- "data-component-name": "Deck.Layout",
189
+ mode: mode,
190
+ "data-component-name": componentName("Layout"),
38
191
  children: [
39
192
  /*#__PURE__*/ jsxRuntime_module_u(SlideFrame, {
40
- "data-component-name": "Deck.SlideFrame",
41
- children: /*#__PURE__*/ jsxRuntime_module_u(SlideClip, {
42
- "data-component-name": "Deck.SlideClip",
43
- children: /*#__PURE__*/ jsxRuntime_module_u(SlideZoomLayer, {
44
- ref: zoomRef,
45
- "data-component-name": "Deck.SlideZoomLayer",
46
- children: /*#__PURE__*/ jsxRuntime_module_u(SlideImage, {
47
- "data-component-name": "Deck.SlideImage",
48
- crossOrigin: "anonymous",
49
- src: currentDeck.slideImageUrls[currentDeck.currentIndex],
50
- alt: (_ref = (_currentDeck_pageTitle = currentDeck.pageTitle) != null ? _currentDeck_pageTitle : currentDeck.deckTitle) != null ? _ref : ""
193
+ mode: mode,
194
+ withVoiceSwitch: showVoiceSwitch,
195
+ starting: starting,
196
+ "data-component-name": componentName("SlideFrame"),
197
+ children: [
198
+ mode === types_DeckMode.Guided ? // SlideClip owns the rounding and shadow, and SlideZoomLayer is the
199
+ // element the zoom is applied to, kept apart so scaling the slide
200
+ // leaves both of those and the image's own transform alone.
201
+ /*#__PURE__*/ jsxRuntime_module_u(SlideClip, {
202
+ "data-component-name": componentName("SlideClip"),
203
+ children: /*#__PURE__*/ jsxRuntime_module_u(SlideZoomLayer, {
204
+ ref: zoomRef,
205
+ "data-component-name": componentName("SlideZoomLayer"),
206
+ children: slideImage
51
207
  })
208
+ }) : slideImage,
209
+ starting && /*#__PURE__*/ jsxRuntime_module_u(preact_module_k, {
210
+ children: [
211
+ /*#__PURE__*/ jsxRuntime_module_u(Scrim, {
212
+ "data-component-name": componentName("Scrim"),
213
+ "aria-hidden": "true"
214
+ }),
215
+ /*#__PURE__*/ jsxRuntime_module_u(Progress, {
216
+ "data-component-name": componentName("Progress"),
217
+ role: "status",
218
+ children: /*#__PURE__*/ jsxRuntime_module_u(Status, {
219
+ variant: "pill",
220
+ colorScheme: "overlayText",
221
+ mainText: startingMessage,
222
+ subText: undefined,
223
+ // The widget's own call-loading indicator, in the form it takes
224
+ // over a visual: the animated ellipsis it uses over blurred video
225
+ // (useMultimodal), rather than a second spinner of this
226
+ // component's.
227
+ loadingIndicator: "dots"
228
+ })
229
+ })
230
+ ]
52
231
  })
53
- })
232
+ ]
54
233
  }),
55
- isSequence && /*#__PURE__*/ jsxRuntime_module_u(ProgressDots, {
56
- "data-component-name": "Deck.ProgressDots",
57
- "aria-label": "Slide " + (currentDeck.sequencePosition + 1) + " of " + currentDeck.sequenceLength,
58
- children: Array.from({
59
- length: currentDeck.sequenceLength
60
- }).map((_, index)=>/*#__PURE__*/ jsxRuntime_module_u(Dot, {
61
- "data-active": index === currentDeck.sequencePosition ? "true" : undefined
62
- }, index))
63
- })
234
+ mode === types_DeckMode.SelfGuided && !starting && /*#__PURE__*/ jsxRuntime_module_u(ControlPanel, {
235
+ withVoiceSwitch: showVoiceSwitch,
236
+ "data-component-name": componentName("ControlPanel"),
237
+ children: [
238
+ /*#__PURE__*/ jsxRuntime_module_u(Caption, {
239
+ "data-component-name": componentName("Caption"),
240
+ children: currentDeck.caption && /*#__PURE__*/ jsxRuntime_module_u(RichText, {
241
+ text: currentDeck.caption,
242
+ animated: false,
243
+ paragraphSpacing: false
244
+ })
245
+ }),
246
+ showVoiceSwitch && /*#__PURE__*/ jsxRuntime_module_u(Button, {
247
+ variant: "primary",
248
+ size: "md",
249
+ "data-component-name": componentName("VoiceSwitch"),
250
+ onClick: switchToVoice,
251
+ css: {
252
+ alignSelf: "flex-start",
253
+ maxWidth: "[100%]"
254
+ },
255
+ children: [
256
+ /*#__PURE__*/ jsxRuntime_module_u(SpeakNowMicIcon, {}),
257
+ t("presentation.switchToVoice")
258
+ ]
259
+ }),
260
+ /*#__PURE__*/ jsxRuntime_module_u(Controls, {
261
+ children: [
262
+ /*#__PURE__*/ jsxRuntime_module_u(Button, {
263
+ variant: "primary",
264
+ size: "md",
265
+ iconOnly: true,
266
+ "aria-label": t("presentation.previousSlide"),
267
+ disabled: sequencePosition === 0,
268
+ onClick: ()=>decksStore.goToPreviousSlide(),
269
+ css: {
270
+ transform: "rotate(180deg)"
271
+ },
272
+ children: /*#__PURE__*/ jsxRuntime_module_u(ArrowRightIcon, {})
273
+ }),
274
+ progressDots,
275
+ /*#__PURE__*/ jsxRuntime_module_u(Button, {
276
+ variant: "primary",
277
+ size: "md",
278
+ iconOnly: true,
279
+ "aria-label": t("presentation.nextSlide"),
280
+ disabled: sequencePosition >= sequenceLength - 1,
281
+ onClick: ()=>decksStore.goToNextSlide(),
282
+ children: /*#__PURE__*/ jsxRuntime_module_u(ArrowRightIcon, {})
283
+ })
284
+ ]
285
+ })
286
+ ]
287
+ }),
288
+ mode === types_DeckMode.Guided && sequenceLength > 1 && progressDots
64
289
  ]
65
290
  });
66
291
  };
@@ -69,32 +294,148 @@ const Layout = styled("div", {
69
294
  base: {
70
295
  display: "flex",
71
296
  flexDirection: "column",
72
- alignItems: "center",
73
- justifyContent: "center",
74
- gap: "[12px]",
75
297
  width: "[100%]",
76
- height: "[100%]",
77
298
  minHeight: "[0]"
299
+ },
300
+ variants: {
301
+ mode: {
302
+ // Centred in whatever the overlay gives it, over the dark backdrop.
303
+ guided: {
304
+ alignItems: "center",
305
+ justifyContent: "center",
306
+ gap: "[12px]",
307
+ height: "[100%]"
308
+ },
309
+ // A reading column: slide above, panel below, both full width, centred in
310
+ // the overlay when the two do not fill it. Takes the overlay's full height
311
+ // rather than shrink-wrapping so that height is a definite number the two
312
+ // can be divided against — SlideFrame's cap below is a percentage of it.
313
+ //
314
+ // Ungapped on purpose: the slide and the panel are stitched into one card,
315
+ // each rounding only its outer corners (see SlideFrame and ControlPanel),
316
+ // and a gap here would put a stripe of scrim through the seam.
317
+ self_guided: {
318
+ alignItems: "stretch",
319
+ justifyContent: "center",
320
+ height: "[100%]"
321
+ }
322
+ }
78
323
  }
79
324
  });
80
325
  Layout.displayName = "Layout";
81
326
  const SlideFrame = styled("div", {
82
327
  base: {
83
328
  display: "flex",
84
- flexDirection: "column",
85
- alignItems: "center",
86
- gap: "[10px]",
87
- width: "[auto]",
88
- height: "[auto]",
89
- maxWidth: "[100%]",
90
- maxHeight: "[100%]",
91
329
  minHeight: "[0]"
330
+ },
331
+ variants: {
332
+ mode: {
333
+ // Shrink-wraps the slide at its intrinsic size; SlideClip inside does the
334
+ // white-strip clipping and carries the rounding.
335
+ guided: {
336
+ flexDirection: "column",
337
+ alignItems: "center",
338
+ gap: "[10px]",
339
+ width: "[auto]",
340
+ height: "[auto]",
341
+ maxWidth: "[100%]",
342
+ maxHeight: "[100%]"
343
+ },
344
+ // No SlideClip in this mode, so the frame itself clips the white strip and
345
+ // carries the rounding.
346
+ //
347
+ // Unshrinkable under a cap, which is what orders the squeeze: flex-shrink
348
+ // would take from both children at once, so the slide would start
349
+ // letterboxing while the panel still had room to give. Instead the frame is
350
+ // simply the smaller of the slide's natural height and everything the panel
351
+ // does not need, so the panel yields first and the slide only gives ground
352
+ // once the panel is at its floor. The subtrahend is that floor, the column
353
+ // being ungapped — change either and this has to follow. The floor itself
354
+ // moves with what the panel holds, which is what the variant below is for.
355
+ //
356
+ // Square along the bottom, where ControlPanel butts against it: the slide
357
+ // and its narration are one card, so only the pair's outer corners round.
358
+ // The slide's own bottom edge is what lands on that seam — the image is
359
+ // stretched to the frame's full height, so `contain` letterboxes it down
360
+ // the sides rather than under it, and it always reaches this edge.
361
+ //
362
+ // Squared for as long as there is a panel to meet, which is not the whole of
363
+ // this mode: the starting variant below has none and puts the rounding back.
364
+ //
365
+ // Deliberately unfilled: what the cap letterboxes shows the overlay's
366
+ // scrim, the same surround the slide has in guided mode.
367
+ self_guided: {
368
+ justifyContent: "center",
369
+ overflow: "hidden",
370
+ borderRadius: "lg",
371
+ borderBottomLeftRadius: "[0]",
372
+ borderBottomRightRadius: "[0]",
373
+ flexShrink: "0",
374
+ maxHeight: "[calc(100% - 124px)]"
375
+ }
376
+ },
377
+ // The panel's taller floor, subtracted the same way. Declared after `mode` so
378
+ // it wins the merge, and only ever true in self-guided, the one mode with a
379
+ // panel to subtract at all.
380
+ withVoiceSwitch: {
381
+ true: {
382
+ maxHeight: "[calc(100% - 172px)]"
383
+ }
384
+ },
385
+ // The panel is gone while the call starts, so the two things the self-guided
386
+ // frame owes it are gone with it. There is nothing to subtract — the caps
387
+ // above are arithmetic against a panel's contents, and holding 124px of empty
388
+ // scrim open under the slide is what inheriting one would do — and nothing
389
+ // butting against the bottom edge, so the slide is the whole card and every
390
+ // corner is an outer corner again. Both stated here rather than by touching
391
+ // the variants above, whose numbers belong to the state that has a panel.
392
+ //
393
+ // The bottom corners are named individually because that is how they were
394
+ // squared: a `borderRadius` shorthand cannot be relied on to beat the
395
+ // longhands above it, whichever order the variants are declared in. Through
396
+ // `token()` because only the shorthand takes the radii scale by name
397
+ // (panda.config's utilities), so this is `lg` said the long way round.
398
+ //
399
+ // `relative` for the scrim and the message, which are positioned against the
400
+ // slide. The slide's own width is unchanged either way, and for any slide
401
+ // wider than it is tall the width is what binds, so what the visitor sees
402
+ // across the hand-off is the panel going away rather than the slide resizing.
403
+ starting: {
404
+ true: {
405
+ position: "relative",
406
+ maxHeight: "[100%]",
407
+ borderRadius: "lg",
408
+ borderBottomLeftRadius: "[token(radii.lg)]",
409
+ borderBottomRightRadius: "[token(radii.lg)]"
410
+ }
411
+ }
92
412
  }
93
413
  });
94
414
  SlideFrame.displayName = "SlideFrame";
415
+ // Dims the slide while the call starts, covering the frame it is positioned in.
416
+ const Scrim = styled("div", {
417
+ base: {
418
+ position: "absolute",
419
+ inset: "[0]",
420
+ backgroundColor: "[rgba(0, 0, 0, 0.5)]"
421
+ }
422
+ });
423
+ Scrim.displayName = "Scrim";
424
+ // Holds the starting message over the dimmed slide rather than under it. Under it
425
+ // would need a strip of chrome to sit on, and that strip is the panel this state
426
+ // exists without. Boxless past the positioning: Status' `pill` variant fills and
427
+ // centres itself.
428
+ const Progress = styled("div", {
429
+ base: {
430
+ position: "absolute",
431
+ inset: "[0]",
432
+ display: "flex"
433
+ }
434
+ });
435
+ Progress.displayName = "Progress";
95
436
  // Clips the ~2px white strip baked into the top of each rendered PDF page (a
96
437
  // Ghostscript rasterization artifact). Holds the rounded corners + shadow so
97
- // the scaled image inside is clipped cleanly.
438
+ // the scaled image inside is clipped cleanly. Guided mode only.
98
439
  const SlideClip = styled("div", {
99
440
  base: {
100
441
  display: "flex",
@@ -109,7 +450,7 @@ const SlideClip = styled("div", {
109
450
  SlideClip.displayName = "SlideClip";
110
451
  // Shrink-wraps the slide image so the point-at-screen zoom (slideZoomStyle, applied imperatively in
111
452
  // Deck's effect) scales/clips the image in place without touching SlideClip's shadow + rounding or
112
- // the image's own white-strip transform.
453
+ // the image's own white-strip transform. Guided mode only — see the effect's comment.
113
454
  const SlideZoomLayer = styled("div", {
114
455
  base: {
115
456
  display: "flex",
@@ -121,37 +462,150 @@ const SlideZoomLayer = styled("div", {
121
462
  SlideZoomLayer.displayName = "SlideZoomLayer";
122
463
  const SlideImage = styled("img", {
123
464
  base: {
124
- width: "[auto]",
125
465
  height: "[auto]",
126
466
  maxWidth: "[100%]",
127
467
  maxHeight: "[100%]",
128
468
  minHeight: "[0]",
129
469
  objectFit: "contain",
130
470
  display: "block",
131
- // Scale up a hair from the bottom so the top edge is pushed past SlideClip's
132
- // overflow, hiding the baked-in white strip. Scales with any display size.
471
+ // Scale up a hair from the bottom so the top edge is pushed past the
472
+ // clipping ancestor's overflow, hiding the baked-in white strip. Scales with
473
+ // any display size.
133
474
  transform: "[scale(1.004)]",
134
475
  transformOrigin: "[bottom]"
476
+ },
477
+ variants: {
478
+ mode: {
479
+ // Intrinsic size, so the slide is never upscaled past its own resolution.
480
+ guided: {
481
+ width: "[auto]"
482
+ },
483
+ // Fills the reading column, whatever the slide's own size, leaving the base's
484
+ // `contain` to handle the one case where the frame is capped below the
485
+ // slide's natural height: it scales the whole slide down to fit rather than
486
+ // filling the frame with a piece of it.
487
+ self_guided: {
488
+ width: "[100%]"
489
+ }
490
+ }
135
491
  }
136
492
  });
137
493
  SlideImage.displayName = "SlideImage";
138
- const ProgressDots = styled("div", {
494
+ // The white panel under the slide, holding the narration and the controls.
495
+ // Self-guided mode only.
496
+ //
497
+ // Gives up its height to the slide before the slide gives up any, the caption
498
+ // scrolling inside whatever is left. The floor is what the panel is still worth
499
+ // reading at: $lg of padding twice, $lg of gap, the 32px control row, and two
500
+ // 22px lines of caption under it — 124px. The voice-switch row adds a second $lg
501
+ // gap and a second 32px row, taking the floor to 172px. The arrows and the switch
502
+ // are outside the caption's scroll box, so they have to be counted here rather
503
+ // than left to scroll. Stating a floor at all also replaces the automatic
504
+ // minimum, which would otherwise pin this to its content and let nothing shrink.
505
+ //
506
+ // Square along the top, the other half of the stitch SlideFrame makes: the two
507
+ // meet flush, so the rounding belongs on the pair's outer corners only.
508
+ const ControlPanel = styled("div", {
139
509
  base: {
140
510
  display: "flex",
141
- gap: "[8px]",
511
+ flexDirection: "column",
512
+ gap: "$lg",
513
+ padding: "$lg",
514
+ borderRadius: "lg",
515
+ borderTopLeftRadius: "[0]",
516
+ borderTopRightRadius: "[0]",
517
+ backgroundColor: "$white.solid",
518
+ minHeight: "[124px]"
519
+ },
520
+ variants: {
521
+ // Conditional, so the floor is too: fixed at the taller number it would hold
522
+ // 48px of empty panel open in text-only mode, where the switch never appears
523
+ // and the slide could have had the room.
524
+ withVoiceSwitch: {
525
+ true: {
526
+ minHeight: "[172px]"
527
+ }
528
+ }
529
+ }
530
+ });
531
+ ControlPanel.displayName = "ControlPanel";
532
+ const Caption = styled("div", {
533
+ base: {
534
+ fontFamily: "$inter",
535
+ fontSize: "[14px]",
536
+ lineHeight: "[22px]",
537
+ color: "$text.dark",
538
+ flexGrow: "1",
539
+ overflowY: "auto",
540
+ // Spelt out rather than left to `overflow` to zero the automatic minimum:
541
+ // this is the only thing standing between a long talk track and a panel that
542
+ // refuses to shrink, taking the scrolling with it.
543
+ minHeight: "[0]",
544
+ "&:empty": {
545
+ display: "none"
546
+ }
547
+ }
548
+ });
549
+ Caption.displayName = "Caption";
550
+ // Outside the caption's scroll box, so squeezing the panel scrolls the narration
551
+ // rather than carrying the arrows off with it.
552
+ const Controls = styled("div", {
553
+ base: {
554
+ display: "flex",
555
+ alignItems: "center",
556
+ justifyContent: "space-between",
557
+ gap: "$md",
142
558
  flexShrink: "0"
143
559
  }
144
560
  });
561
+ Controls.displayName = "Controls";
562
+ const ProgressDots = styled("div", {
563
+ base: {
564
+ display: "flex"
565
+ },
566
+ variants: {
567
+ mode: {
568
+ // Sits alone under the slide.
569
+ guided: {
570
+ gap: "[8px]",
571
+ flexShrink: "0"
572
+ },
573
+ // Sits between the two nav buttons, and wraps rather than pushing them
574
+ // apart when the deck is long.
575
+ self_guided: {
576
+ alignItems: "center",
577
+ justifyContent: "center",
578
+ gap: "$sm",
579
+ flexWrap: "wrap"
580
+ }
581
+ }
582
+ }
583
+ });
145
584
  ProgressDots.displayName = "ProgressDots";
146
585
  const Dot = styled("span", {
147
586
  base: {
148
587
  width: "[8px]",
149
588
  height: "[8px]",
150
- borderRadius: "[50%]",
151
- backgroundColor: "[rgba(255, 255, 255, 0.4)]",
152
- transition: "[background-color 0.2s ease]",
153
- "&[data-active='true']": {
154
- backgroundColor: "[rgba(255, 255, 255, 0.95)]"
589
+ transition: "[background-color 0.2s ease]"
590
+ },
591
+ variants: {
592
+ mode: {
593
+ // On the dark backdrop, so white at two opacities.
594
+ guided: {
595
+ borderRadius: "[50%]",
596
+ backgroundColor: "[rgba(255, 255, 255, 0.4)]",
597
+ "&[data-active='true']": {
598
+ backgroundColor: "[rgba(255, 255, 255, 0.95)]"
599
+ }
600
+ },
601
+ // On the white panel, so the theme's grey and primary.
602
+ self_guided: {
603
+ borderRadius: "full",
604
+ backgroundColor: "$gray.border",
605
+ "&[data-active='true']": {
606
+ backgroundColor: "$primary"
607
+ }
608
+ }
155
609
  }
156
610
  }
157
611
  });