@unityevolv/ofiskit-ui-map 0.1.0

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 (103) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +34 -0
  3. package/dist/Announcer.d.ts +21 -0
  4. package/dist/Announcer.d.ts.map +1 -0
  5. package/dist/Announcer.js +32 -0
  6. package/dist/Announcer.js.map +1 -0
  7. package/dist/CallAudio.d.ts +32 -0
  8. package/dist/CallAudio.d.ts.map +1 -0
  9. package/dist/CallAudio.js +72 -0
  10. package/dist/CallAudio.js.map +1 -0
  11. package/dist/CallControls.d.ts +60 -0
  12. package/dist/CallControls.d.ts.map +1 -0
  13. package/dist/CallControls.js +62 -0
  14. package/dist/CallControls.js.map +1 -0
  15. package/dist/CallTiles.d.ts +48 -0
  16. package/dist/CallTiles.d.ts.map +1 -0
  17. package/dist/CallTiles.js +134 -0
  18. package/dist/CallTiles.js.map +1 -0
  19. package/dist/DevicePanel.d.ts +32 -0
  20. package/dist/DevicePanel.d.ts.map +1 -0
  21. package/dist/DevicePanel.js +133 -0
  22. package/dist/DevicePanel.js.map +1 -0
  23. package/dist/Knocks.d.ts +46 -0
  24. package/dist/Knocks.d.ts.map +1 -0
  25. package/dist/Knocks.js +46 -0
  26. package/dist/Knocks.js.map +1 -0
  27. package/dist/OfficeMap.d.ts +69 -0
  28. package/dist/OfficeMap.d.ts.map +1 -0
  29. package/dist/OfficeMap.js +222 -0
  30. package/dist/OfficeMap.js.map +1 -0
  31. package/dist/Overflow.d.ts +41 -0
  32. package/dist/Overflow.d.ts.map +1 -0
  33. package/dist/Overflow.js +82 -0
  34. package/dist/Overflow.js.map +1 -0
  35. package/dist/PersonAvatar.d.ts +32 -0
  36. package/dist/PersonAvatar.d.ts.map +1 -0
  37. package/dist/PersonAvatar.js +80 -0
  38. package/dist/PersonAvatar.js.map +1 -0
  39. package/dist/Reactions.d.ts +47 -0
  40. package/dist/Reactions.d.ts.map +1 -0
  41. package/dist/Reactions.js +61 -0
  42. package/dist/Reactions.js.map +1 -0
  43. package/dist/RoomBar.d.ts +43 -0
  44. package/dist/RoomBar.d.ts.map +1 -0
  45. package/dist/RoomBar.js +120 -0
  46. package/dist/RoomBar.js.map +1 -0
  47. package/dist/Share.d.ts +74 -0
  48. package/dist/Share.d.ts.map +1 -0
  49. package/dist/Share.js +115 -0
  50. package/dist/Share.js.map +1 -0
  51. package/dist/StatusControl.d.ts +25 -0
  52. package/dist/StatusControl.d.ts.map +1 -0
  53. package/dist/StatusControl.js +77 -0
  54. package/dist/StatusControl.js.map +1 -0
  55. package/dist/controls.d.ts +32 -0
  56. package/dist/controls.d.ts.map +1 -0
  57. package/dist/controls.js +24 -0
  58. package/dist/controls.js.map +1 -0
  59. package/dist/hooks.d.ts +60 -0
  60. package/dist/hooks.d.ts.map +1 -0
  61. package/dist/hooks.js +182 -0
  62. package/dist/hooks.js.map +1 -0
  63. package/dist/index.d.ts +45 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +45 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/layout.d.ts +65 -0
  68. package/dist/layout.d.ts.map +1 -0
  69. package/dist/layout.js +93 -0
  70. package/dist/layout.js.map +1 -0
  71. package/dist/placement.d.ts +70 -0
  72. package/dist/placement.d.ts.map +1 -0
  73. package/dist/placement.js +83 -0
  74. package/dist/placement.js.map +1 -0
  75. package/dist/sounds.d.ts +30 -0
  76. package/dist/sounds.d.ts.map +1 -0
  77. package/dist/sounds.js +125 -0
  78. package/dist/sounds.js.map +1 -0
  79. package/dist/status.d.ts +47 -0
  80. package/dist/status.d.ts.map +1 -0
  81. package/dist/status.js +65 -0
  82. package/dist/status.js.map +1 -0
  83. package/dist/theme.d.ts +27 -0
  84. package/dist/theme.d.ts.map +1 -0
  85. package/dist/theme.js +57 -0
  86. package/dist/theme.js.map +1 -0
  87. package/dist/tileLayout.d.ts +27 -0
  88. package/dist/tileLayout.d.ts.map +1 -0
  89. package/dist/tileLayout.js +41 -0
  90. package/dist/tileLayout.js.map +1 -0
  91. package/dist/useCall.d.ts +107 -0
  92. package/dist/useCall.d.ts.map +1 -0
  93. package/dist/useCall.js +238 -0
  94. package/dist/useCall.js.map +1 -0
  95. package/dist/useShare.d.ts +54 -0
  96. package/dist/useShare.d.ts.map +1 -0
  97. package/dist/useShare.js +114 -0
  98. package/dist/useShare.js.map +1 -0
  99. package/dist/useSounds.d.ts +31 -0
  100. package/dist/useSounds.d.ts.map +1 -0
  101. package/dist/useSounds.js +56 -0
  102. package/dist/useSounds.js.map +1 -0
  103. package/package.json +25 -0
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @unityevolv/ofiskit-ui-map
2
+
3
+ The office you can see: the map, the room bars, the avatars, the video tiles, the
4
+ call controls and the status control.
5
+
6
+ React DOM, built entirely from [unitykit](https://github.com/UnityEvolv/unity-kit):
7
+ every colour is a token, so both themes follow from the token layer, and every
8
+ icon comes from the kit at one weight.
9
+
10
+ ```tsx
11
+ import { OfficeMap } from '@unityevolv/ofiskit-ui-map'
12
+
13
+ ;<OfficeMap
14
+ template={template}
15
+ state={state}
16
+ imageUrl={officeImageUrl}
17
+ onJoin={client.joinRoom}
18
+ />
19
+ ```
20
+
21
+ It takes the office state and calls back; it never opens a socket itself. The
22
+ same components draw a template somebody downloaded from the free builder and one
23
+ a product stores in a database.
24
+
25
+ Peer dependencies: `react`, `react-dom` and `@unityevolv/unitykit`.
26
+
27
+ ## Licence
28
+
29
+ AGPL-3.0-only. The interface packages (`ofiskit-template`,
30
+ `ofiskit-presence-store` and `ofiskit-adapters`) are Apache-2.0, so building
31
+ against the interfaces is not a licensing decision even though extending this is.
32
+
33
+ Part of [ofiskit](https://github.com/UnityEvolv/ofis-kit): a virtual office you
34
+ can run yourself. [Try the demo](https://unityevolv.com/ofis-kit/).
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Things that happen *to* you, said out loud.
3
+ *
4
+ * A knock, somebody admitting you, a call starting, a move being refused: all of
5
+ * these are drawn somewhere on the screen, and somebody using a screen reader
6
+ * will not know any of them happened. Drawing is not telling.
7
+ *
8
+ * Two regions rather than one, because the distinction matters to how a screen
9
+ * reader behaves: `polite` waits for a pause, `assertive` interrupts. Almost
10
+ * everything is polite. A knock is assertive, because somebody is standing
11
+ * outside the door waiting for an answer.
12
+ */
13
+ interface AnnouncerValue {
14
+ announce(message: string, urgency?: 'polite' | 'assertive'): void;
15
+ }
16
+ export declare function AnnouncerProvider({ children }: {
17
+ children: React.ReactNode;
18
+ }): import("react").JSX.Element;
19
+ export declare function useAnnounce(): AnnouncerValue['announce'];
20
+ export {};
21
+ //# sourceMappingURL=Announcer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Announcer.d.ts","sourceRoot":"","sources":["../src/Announcer.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAEH,UAAU,cAAc;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,IAAI,CAAA;CAClE;AAaD,wBAAgB,iBAAiB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,+BA8B5E;AAED,wBAAgB,WAAW,IAAI,cAAc,CAAC,UAAU,CAAC,CAIxD"}
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
3
+ /**
4
+ * Appended to repeat an announcement.
5
+ *
6
+ * Most screen readers ignore a live region whose text has not changed, so two
7
+ * people knocking one after the other would be announced once. A zero-width
8
+ * space makes it a different string and is not read out or drawn.
9
+ */
10
+ const REPEAT_MARK = String.fromCharCode(0x200b);
11
+ const AnnouncerContext = createContext(null);
12
+ export function AnnouncerProvider({ children }) {
13
+ const [polite, setPolite] = useState('');
14
+ const [assertive, setAssertive] = useState('');
15
+ const previous = useRef('');
16
+ const announce = useCallback((message, urgency = 'polite') => {
17
+ const text = message === previous.current ? `${message}${REPEAT_MARK}` : message;
18
+ previous.current = text;
19
+ if (urgency === 'assertive')
20
+ setAssertive(text);
21
+ else
22
+ setPolite(text);
23
+ }, []);
24
+ const value = useMemo(() => ({ announce }), [announce]);
25
+ return (_jsxs(AnnouncerContext.Provider, { value: value, children: [children, _jsx("div", { className: "sr-only", role: "status", "aria-live": "polite", "aria-atomic": "true", children: polite }), _jsx("div", { className: "sr-only", role: "alert", "aria-live": "assertive", "aria-atomic": "true", children: assertive })] }));
26
+ }
27
+ export function useAnnounce() {
28
+ // A component outside the provider should still work, silently, rather than
29
+ // taking the page down over an announcement.
30
+ return useContext(AnnouncerContext)?.announce ?? (() => { });
31
+ }
32
+ //# sourceMappingURL=Announcer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Announcer.js","sourceRoot":"","sources":["../src/Announcer.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAmBzF;;;;;;GAMG;AACH,MAAM,WAAW,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAA;AAE/C,MAAM,gBAAgB,GAAG,aAAa,CAAwB,IAAI,CAAC,CAAA;AAEnE,MAAM,UAAU,iBAAiB,CAAC,EAAE,QAAQ,EAAiC;IAC3E,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IACxC,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAC9C,MAAM,QAAQ,GAAG,MAAM,CAAC,EAAE,CAAC,CAAA;IAE3B,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,OAAe,EAAE,UAAkC,QAAQ,EAAE,EAAE;QAC3F,MAAM,IAAI,GAAG,OAAO,KAAK,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,GAAG,WAAW,EAAE,CAAC,CAAC,CAAC,OAAO,CAAA;QAChF,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAA;QACvB,IAAI,OAAO,KAAK,WAAW;YAAE,YAAY,CAAC,IAAI,CAAC,CAAA;;YAC1C,SAAS,CAAC,IAAI,CAAC,CAAA;IACtB,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;IAEvD,OAAO,CACL,MAAC,gBAAgB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,aACpC,QAAQ,EAMT,cAAK,SAAS,EAAC,SAAS,EAAC,IAAI,EAAC,QAAQ,eAAW,QAAQ,iBAAa,MAAM,YACzE,MAAM,GACH,EACN,cAAK,SAAS,EAAC,SAAS,EAAC,IAAI,EAAC,OAAO,eAAW,WAAW,iBAAa,MAAM,YAC3E,SAAS,GACN,IACoB,CAC7B,CAAA;AACH,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,4EAA4E;IAC5E,6CAA6C;IAC7C,OAAO,UAAU,CAAC,gBAAgB,CAAC,EAAE,QAAQ,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;AAC7D,CAAC"}
@@ -0,0 +1,32 @@
1
+ import type { OfisClient } from '@unityevolv/ofiskit-realtime-client';
2
+ /**
3
+ * Where the other people's voices actually come out.
4
+ *
5
+ * Easy to forget, and invisible when it is missing: the peer connections succeed,
6
+ * the tiles appear, the indicators move, and nobody can hear anybody. A stream
7
+ * that is never attached to an element is a stream the browser does not play.
8
+ *
9
+ * Deliberately separate from the video tiles. Audio must keep playing when a tile
10
+ * is paged out of the visible set, when somebody has no camera, and before any
11
+ * tile exists at all — the conversation is the point, and the picture is not.
12
+ *
13
+ * One element per peer rather than one mixed element, because output routing is
14
+ * per element: that is what lets a call go to headphones while system sounds stay
15
+ * on the speakers.
16
+ */
17
+ export interface CallAudioProps {
18
+ client: OfisClient;
19
+ /** The chosen speaker. Empty follows the operating system. */
20
+ speakerDeviceId?: string;
21
+ /**
22
+ * Peers this person has muted for themselves only.
23
+ *
24
+ * Applied here rather than on a tile, because it has to hold when that tile is
25
+ * paged out of the visible five: somebody silenced for being noisy must not
26
+ * come back the moment they stop being on screen. Nobody else is affected and
27
+ * the person is not told.
28
+ */
29
+ mutedForMe?: ReadonlySet<string>;
30
+ }
31
+ export declare function CallAudio({ client, speakerDeviceId, mutedForMe }: CallAudioProps): import("react").JSX.Element;
32
+ //# sourceMappingURL=CallAudio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CallAudio.d.ts","sourceRoot":"","sources":["../src/CallAudio.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qCAAqC,CAAA;AAKrE;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAA;IAClB,8DAA8D;IAC9D,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CACjC;AAgBD,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,eAAe,EAAE,UAAU,EAAE,EAAE,cAAc,+BAmEhF"}
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { useClientEvents } from './hooks.js';
4
+ /**
5
+ * Route one element to a speaker, where the browser can.
6
+ *
7
+ * The DOM types declare `setSinkId` as always present; the browsers do not. Safari
8
+ * has never had it, and a mobile browser lets the operating system decide — which
9
+ * is the right answer on a phone anyway. So a missing method means the system
10
+ * default rather than a failure, and that is exactly what somebody who has not
11
+ * chosen a speaker wants.
12
+ */
13
+ function routeTo(element, speakerDeviceId) {
14
+ if (typeof element.setSinkId !== 'function')
15
+ return;
16
+ void element.setSinkId(speakerDeviceId).catch(() => { });
17
+ }
18
+ export function CallAudio({ client, speakerDeviceId, mutedForMe }) {
19
+ const [streams, setStreams] = useState(new Map());
20
+ const elements = useRef(new Map());
21
+ useClientEvents(client, (event) => {
22
+ if (event.type !== 'rtc')
23
+ return;
24
+ if (event.event.type === 'track' && event.event.source === 'audio') {
25
+ const { deviceId, stream } = event.event;
26
+ setStreams((current) => new Map(current).set(deviceId, stream));
27
+ }
28
+ // A leg that has gone takes its element with it, or a call that ends leaves
29
+ // elements holding dead streams.
30
+ if (event.event.type === 'participant.left') {
31
+ const { deviceId } = event.event;
32
+ setStreams((current) => {
33
+ if (!current.has(deviceId))
34
+ return current;
35
+ const next = new Map(current);
36
+ next.delete(deviceId);
37
+ return next;
38
+ });
39
+ }
40
+ });
41
+ /*
42
+ * Route every element to the chosen speaker.
43
+ *
44
+ * In an effect rather than as an attribute, because `setSinkId` is a method and
45
+ * a promise, and it is unavailable on Safari and on mobile — where the
46
+ * operating system decides, which is the right answer on a phone anyway. A
47
+ * failure here is silently the system default, which is exactly what somebody
48
+ * who has not chosen a speaker wants.
49
+ */
50
+ useEffect(() => {
51
+ if (!speakerDeviceId)
52
+ return;
53
+ for (const element of elements.current.values())
54
+ routeTo(element, speakerDeviceId);
55
+ }, [speakerDeviceId, streams]);
56
+ return (_jsx(_Fragment, { children: [...streams].map(([deviceId, stream]) => (_jsx("audio", { autoPlay: true,
57
+ // Silencing somebody for yourself is exactly this and nothing else: their
58
+ // audio muted on your side. A prop rather than a mutation, so React owns
59
+ // it and it cannot drift from what was chosen.
60
+ muted: mutedForMe?.has(deviceId) ?? false, "data-testid": `call-audio-${deviceId}`, ref: (element) => {
61
+ if (!element) {
62
+ elements.current.delete(deviceId);
63
+ return;
64
+ }
65
+ elements.current.set(deviceId, element);
66
+ if (element.srcObject !== stream)
67
+ element.srcObject = stream;
68
+ if (speakerDeviceId)
69
+ routeTo(element, speakerDeviceId);
70
+ } }, deviceId))) }));
71
+ }
72
+ //# sourceMappingURL=CallAudio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CallAudio.js","sourceRoot":"","sources":["../src/CallAudio.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import type { Reaction, RoomCall } from '@unityevolv/ofiskit-realtime-client';
2
+ /**
3
+ * The bar at the bottom of the office, where a call starts, is controlled and ends.
4
+ *
5
+ * It speaks to the provider through the shared realtime client, so it looks and
6
+ * behaves identically whichever provider an org uses and never knows whether the
7
+ * call is a peer-to-peer mesh or somebody's SFU.
8
+ *
9
+ * The rule that catches people out: **entering a room never joins its call.**
10
+ * Pressing the microphone or the camera is what joins, and the other one stays off
11
+ * until it is pressed too.
12
+ *
13
+ * The bar itself is always there, because in this app it is the only chrome — the
14
+ * map fills the window and there is no header. The **call** controls are what comes
15
+ * and goes, since reception and the break room never have calls, and the `leading`
16
+ * and `trailing` slots carry whatever else the host needs within reach: the status
17
+ * control, a view toggle, a way out.
18
+ */
19
+ export interface CallControlsProps {
20
+ /** False in reception and the break room, which never have calls. */
21
+ available: boolean;
22
+ /** Sits at the left of the bar. The status control goes here. */
23
+ leading?: React.ReactNode;
24
+ /** Sits at the right: view toggles, and leaving the office. */
25
+ trailing?: React.ReactNode;
26
+ inCall: boolean;
27
+ muted: boolean;
28
+ cameraOn: boolean;
29
+ sharing: boolean;
30
+ /**
31
+ * Somebody else's name, when they are the one sharing.
32
+ *
33
+ * Only so the control can say what pressing it would do. There is one share per
34
+ * call, so pressing it here means taking the slot, and a button labelled "Share
35
+ * your screen" that silently ends somebody else's is the kind of surprise this
36
+ * bar exists to avoid.
37
+ */
38
+ sharedByOther?: string | null;
39
+ callView: boolean;
40
+ /**
41
+ * The call is always full size here — on a phone — so the toggle between it
42
+ * and the office is not offered.
43
+ */
44
+ callViewFixed?: boolean;
45
+ call: RoomCall | null;
46
+ /** Your own hand, so the control can say whether pressing it puts it up or down. */
47
+ handRaised: boolean;
48
+ /** Set when a control cannot be used, and shown rather than hiding it. */
49
+ disabledReason?: string | null;
50
+ onToggleMic(): void;
51
+ onToggleCamera(): void;
52
+ onToggleShare(): void;
53
+ onToggleHand(): void;
54
+ onReact(reaction: Reaction): void;
55
+ onToggleCallView(): void;
56
+ onLeaveCall(): void;
57
+ onOpenDevices(): void;
58
+ }
59
+ export declare function CallControls(props: CallControlsProps): import("react").JSX.Element;
60
+ //# sourceMappingURL=CallControls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CallControls.d.ts","sourceRoot":"","sources":["../src/CallControls.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,qCAAqC,CAAA;AAM7E;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,SAAS,EAAE,OAAO,CAAA;IAClB,iEAAiE;IACjE,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,MAAM,EAAE,OAAO,CAAA;IACf,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,QAAQ,EAAE,OAAO,CAAA;IACjB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAA;IACrB,oFAAoF;IACpF,UAAU,EAAE,OAAO,CAAA;IACnB,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAE9B,WAAW,IAAI,IAAI,CAAA;IACnB,cAAc,IAAI,IAAI,CAAA;IACtB,aAAa,IAAI,IAAI,CAAA;IACrB,YAAY,IAAI,IAAI,CAAA;IACpB,OAAO,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAA;IACjC,gBAAgB,IAAI,IAAI,CAAA;IACxB,WAAW,IAAI,IAAI,CAAA;IACnB,aAAa,IAAI,IAAI,CAAA;CACtB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,+BAuNpD"}
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Icon, Popover } from '@unityevolv/unitykit';
3
+ import { useEffect } from 'react';
4
+ import { Control, controlClasses } from './controls.js';
5
+ import { ReactionPicker } from './Reactions.js';
6
+ export function CallControls(props) {
7
+ const { available, inCall, muted, cameraOn, sharing, sharedByOther, callView, call, handRaised } = props;
8
+ // Full is about the call rather than the room, and about other people rather
9
+ // than you: somebody already in it is never told it is full.
10
+ const full = call !== null && call.participants.length >= call.limit && !inCall;
11
+ const reason = props.disabledReason ?? (full ? `This call is full (${call?.limit} people).` : null);
12
+ const blocked = Boolean(reason);
13
+ /**
14
+ * Shortcuts for the two controls people press constantly.
15
+ *
16
+ * Ignored while typing, so pressing M in a status box does not mute somebody
17
+ * mid-sentence, and ignored with a modifier held, so browser shortcuts still
18
+ * work.
19
+ */
20
+ const { onToggleMic, onToggleCamera } = props;
21
+ useEffect(() => {
22
+ if (!available || blocked)
23
+ return;
24
+ const onKey = (event) => {
25
+ if (event.metaKey || event.ctrlKey || event.altKey)
26
+ return;
27
+ const target = event.target;
28
+ if (target?.isContentEditable)
29
+ return;
30
+ if (target && ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName))
31
+ return;
32
+ if (event.key === 'm' || event.key === 'M') {
33
+ event.preventDefault();
34
+ onToggleMic();
35
+ }
36
+ if (event.key === 'v' || event.key === 'V') {
37
+ event.preventDefault();
38
+ onToggleCamera();
39
+ }
40
+ };
41
+ globalThis.addEventListener('keydown', onKey);
42
+ return () => globalThis.removeEventListener('keydown', onKey);
43
+ }, [available, blocked, onToggleMic, onToggleCamera]);
44
+ return (_jsxs("div", { role: "toolbar", "aria-label": "Office controls",
45
+ // Never squeezed by the office above it, and clear of an iPhone's home
46
+ // indicator: the page opts into the whole screen with `viewport-fit=cover`,
47
+ // so the bottom few points belong to the system unless something says so.
48
+ className: "flex w-full shrink-0 flex-wrap items-center gap-1 border-t border-base-300 bg-base-100 px-2 pt-1.5 pb-[max(0.375rem,env(safe-area-inset-bottom))]", children: [props.leading, _jsx("div", { className: [
49
+ 'flex min-w-0 flex-wrap items-center justify-center gap-1 sm:flex-1',
50
+ available
51
+ ? 'order-first basis-full sm:order-none sm:basis-auto'
52
+ : // Empty in a room with no calls: on a wide bar it is the space that
53
+ // pushes the office controls to the right, and on a phone it would
54
+ // take half of the second row and squeeze the room's name for nothing.
55
+ 'flex-1 max-sm:hidden',
56
+ ].join(' '), children: available && (_jsxs(_Fragment, { children: [_jsx(Control, { label: inCall && !muted ? 'Mute microphone' : 'Turn on microphone', hint: "M", icon: inCall && !muted ? 'mic' : 'mic-off', active: inCall && !muted, danger: inCall && muted, disabled: blocked, reason: reason, onClick: props.onToggleMic }), _jsx(Control, { label: cameraOn ? 'Turn off camera' : 'Turn on camera', hint: "V", icon: cameraOn ? 'video' : 'video-off', active: cameraOn, disabled: blocked, reason: reason, onClick: props.onToggleCamera }), _jsx(Control, { label: sharing
57
+ ? 'Stop sharing your screen'
58
+ : sharedByOther
59
+ ? 'Share your screen instead'
60
+ : 'Share your screen', icon: "share", active: sharing, disabled: blocked, reason: reason, onClick: props.onToggleShare }), inCall && (_jsxs(_Fragment, { children: [_jsx(Control, { label: handRaised ? 'Lower your hand' : 'Raise your hand', icon: "raise-hand", active: handRaised, onClick: props.onToggleHand }), _jsx(Popover, { width: "auto", trigger: _jsx("button", { type: "button", "aria-label": "React", className: controlClasses({}), children: _jsx(Icon, { name: "reactions", size: "sm" }) }), children: _jsx(ReactionPicker, { onReact: props.onReact }) })] })), _jsx(Control, { label: "Microphone, camera and speaker", icon: "settings", onClick: props.onOpenDevices }), !props.callViewFixed && (_jsxs(_Fragment, { children: [_jsx("span", { className: "mx-1 h-5 w-px bg-base-300 max-sm:hidden", "aria-hidden": "true" }), _jsx(Control, { label: callView ? 'Show the office map' : 'Show the call full size', icon: "call-view", active: callView, onClick: props.onToggleCallView })] })), inCall && (_jsxs(_Fragment, { children: [_jsx("span", { className: "mx-1 h-5 w-px bg-base-300 max-sm:hidden", "aria-hidden": "true" }), _jsxs("button", { type: "button", onClick: props.onLeaveCall, className: "inline-flex items-center gap-1 rounded-lg bg-error px-2.5 py-1.5 text-sm font-medium text-error-content hover:bg-error/90 focus-visible:outline-2 focus-visible:outline-primary max-[340px]:px-2", children: [_jsx(Icon, { name: "leave-call", size: "sm" }), _jsx("span", { className: "max-sm:sr-only", children: "Leave call" })] })] })), reason && (_jsx("p", { className: "w-full text-center text-[11px] text-base-content/70", role: "note", children: reason }))] })) }), props.trailing] }));
61
+ }
62
+ //# sourceMappingURL=CallControls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CallControls.js","sourceRoot":"","sources":["../src/CallControls.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import type { PublicPresence, RoomCall } from '@unityevolv/ofiskit-realtime-client';
2
+ import type { CallMedia, LiveReaction } from './useCall.js';
3
+ /**
4
+ * The tiles for the people in your call.
5
+ *
6
+ * Five at a time, whatever the size of the call, ordered by raised hand and then
7
+ * by most recent speaker. Both halves matter: the five bounds what each person
8
+ * downloads, and the ordering makes sure the five are the ones worth seeing — a
9
+ * person who has asked to speak is the last one who should be paged off screen.
10
+ * Only visible tiles receive video; everybody else is audio-only until they are
11
+ * paged to.
12
+ *
13
+ * **No audio here.** The voices come out of the one audio sink, which keeps
14
+ * playing when a tile is paged out of the visible five, when somebody has no
15
+ * camera, and in the office view where there are no tiles at all. A second
16
+ * element on the same stream would be the same person, twice.
17
+ *
18
+ * Provider-independent: these take streams and speaking state from the shared
19
+ * realtime client and never know whether a mesh or an SFU is behind them.
20
+ */
21
+ /** Five across, or five down. The rule is the same in both orientations. */
22
+ export declare const TILES_VISIBLE = 5;
23
+ export interface CallTilesProps {
24
+ call: RoomCall;
25
+ /** Everybody in the office, by user, so a tile can find its person. */
26
+ people: ReadonlyMap<string, PublicPresence>;
27
+ /** Ordered by most recent speaker. From `useSpeakerOrder`. */
28
+ order: string[];
29
+ media: CallMedia;
30
+ /**
31
+ * Reactions in the air, by device. From `useReactions`.
32
+ *
33
+ * Passed in rather than subscribed to here, because the same reaction is drawn
34
+ * over a tile and over an avatar on the map and both have to be the same one.
35
+ */
36
+ reactions?: ReadonlyMap<string, LiveReaction[]>;
37
+ you: {
38
+ userId: string;
39
+ deviceId: string;
40
+ };
41
+ /** A strip across the top, or a column down the right. The canvas shape decides. */
42
+ placement: 'top' | 'right' | 'grid';
43
+ onMuteForMe(deviceId: string, muted: boolean): void;
44
+ /** Tells the adapter which peers should be sending video. */
45
+ onVisibleChange(deviceIds: string[]): void;
46
+ }
47
+ export declare function CallTiles(props: CallTilesProps): import("react").JSX.Element;
48
+ //# sourceMappingURL=CallTiles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CallTiles.d.ts","sourceRoot":"","sources":["../src/CallTiles.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,qCAAqC,CAAA;AAOnF,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAE3D;;;;;;;;;;;;;;;;;GAiBG;AAEH,4EAA4E;AAC5E,eAAO,MAAM,aAAa,IAAI,CAAA;AAE9B,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,QAAQ,CAAA;IACd,uEAAuE;IACvE,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;IAC3C,8DAA8D;IAC9D,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,KAAK,EAAE,SAAS,CAAA;IAChB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,CAAA;IAC/C,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;IACzC,oFAAoF;IACpF,SAAS,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,CAAA;IACnC,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAA;IACnD,6DAA6D;IAC7D,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;CAC3C;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,+BAmJ9C"}
@@ -0,0 +1,134 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Icon } from '@unityevolv/unitykit';
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { useMeasured, useReducedMotion } from './hooks.js';
5
+ import { StatusDot } from './status.js';
6
+ import { ReactionFloat } from './Reactions.js';
7
+ import { tileLayout } from './tileLayout.js';
8
+ /**
9
+ * The tiles for the people in your call.
10
+ *
11
+ * Five at a time, whatever the size of the call, ordered by raised hand and then
12
+ * by most recent speaker. Both halves matter: the five bounds what each person
13
+ * downloads, and the ordering makes sure the five are the ones worth seeing — a
14
+ * person who has asked to speak is the last one who should be paged off screen.
15
+ * Only visible tiles receive video; everybody else is audio-only until they are
16
+ * paged to.
17
+ *
18
+ * **No audio here.** The voices come out of the one audio sink, which keeps
19
+ * playing when a tile is paged out of the visible five, when somebody has no
20
+ * camera, and in the office view where there are no tiles at all. A second
21
+ * element on the same stream would be the same person, twice.
22
+ *
23
+ * Provider-independent: these take streams and speaking state from the shared
24
+ * realtime client and never know whether a mesh or an SFU is behind them.
25
+ */
26
+ /** Five across, or five down. The rule is the same in both orientations. */
27
+ export const TILES_VISIBLE = 5;
28
+ export function CallTiles(props) {
29
+ const { call, people, order, media, reactions, you, placement } = props;
30
+ const [page, setPage] = useState(0);
31
+ // Your own tile is pinned at the front and is not one of the five, so seeing
32
+ // yourself never costs somebody else their place.
33
+ const others = order.filter((deviceId) => deviceId !== you.deviceId);
34
+ const pages = Math.max(1, Math.ceil(others.length / TILES_VISIBLE));
35
+ const current = Math.min(page, pages - 1);
36
+ const visible = others.slice(current * TILES_VISIBLE, current * TILES_VISIBLE + TILES_VISIBLE);
37
+ const offscreen = others.length - visible.length;
38
+ /*
39
+ * Paging changes which video streams are received.
40
+ *
41
+ * Without this the five-tile rule would bound what is *drawn* and not what is
42
+ * downloaded, which is the half that actually costs anything.
43
+ */
44
+ const key = visible.join(',');
45
+ const notify = props.onVisibleChange;
46
+ useEffect(() => {
47
+ notify(key ? key.split(',') : []);
48
+ }, [key, notify]);
49
+ const column = placement === 'right';
50
+ const grid = placement === 'grid';
51
+ /*
52
+ * The grid fills the space it has, the way every video product does.
53
+ *
54
+ * It measures itself and picks the number of columns that makes tiles of the
55
+ * video's own shape as large as they can be: a two-by-two filling a desktop
56
+ * window, a column on a phone held upright. The tile being the video's shape is
57
+ * what lets it fill the space without cropping anybody — dividing the space into
58
+ * equal cells and cropping each picture to its cell fills every pixel and cuts off
59
+ * whoever is at the edge of the frame. The layout is computed to fit, so it cannot
60
+ * spill over the controls; the area is clipped all the same, as a guarantee.
61
+ */
62
+ const [area, size] = useMeasured();
63
+ const layout = grid ? tileLayout(1 + visible.length, size.width, size.height) : null;
64
+ const tileSize = layout && layout.tileWidth > 0
65
+ ? { width: layout.tileWidth, height: layout.tileHeight }
66
+ : undefined;
67
+ const tiles = (_jsxs(_Fragment, { children: [_jsx(Tile, { deviceId: you.deviceId, person: people.get(you.userId), stream: media.local.camera, you: true, placement: placement, ...(tileSize ? { size: tileSize } : {}), quality: null, mutedForMe: false, reactions: reactions?.get(you.deviceId) ?? [], onMuteForMe: props.onMuteForMe }), visible.map((deviceId) => {
68
+ const participant = call.participants.find((one) => one.deviceId === deviceId);
69
+ return (_jsx(Tile, { deviceId: deviceId, person: participant ? people.get(participant.userId) : undefined, stream: media.peers.get(deviceId)?.camera, placement: placement, ...(tileSize ? { size: tileSize } : {}), quality: media.quality.get(deviceId) ?? null, mutedForMe: media.mutedForMe.has(deviceId), reactions: reactions?.get(deviceId) ?? [], onMuteForMe: props.onMuteForMe }, deviceId));
70
+ })] }));
71
+ return (_jsxs("section", { "aria-label": `Call with ${call.participants.length} ${call.participants.length === 1 ? 'person' : 'people'}`, "data-testid": "call-tiles", "data-placement": placement, className: [
72
+ 'flex gap-2 bg-base-200 p-2',
73
+ grid
74
+ ? 'h-full w-full flex-col'
75
+ : column
76
+ ? 'h-full w-56 flex-col overflow-y-auto'
77
+ : 'w-full flex-row overflow-x-auto',
78
+ ].join(' '), children: [grid ? (_jsx("div", { ref: area, "data-testid": "call-grid", "data-columns": layout?.columns, className: "flex min-h-0 flex-1 flex-wrap content-center justify-center gap-2 overflow-hidden", children: tiles })) : (tiles), pages > 1 && (_jsxs("div", { className: [
79
+ 'flex shrink-0 items-center gap-1',
80
+ // A row of its own under the grid, sized to its buttons rather than
81
+ // taking a tile's place.
82
+ column || grid ? 'flex-row justify-center' : 'flex-col',
83
+ ].join(' '), "data-testid": "call-pager", children: [_jsx("button", { type: "button", onClick: () => setPage(Math.max(0, current - 1)), disabled: current === 0, "aria-label": "Previous tiles", className: "rounded p-1 hover:bg-base-300 focus-visible:outline-2 focus-visible:outline-primary disabled:opacity-40", children: _jsx(Icon, { name: column ? 'chevron-up' : 'chevron-left', size: "sm" }) }), _jsx("span", { className: "text-[10px] tabular-nums text-base-content/70", children: offscreen > 0 ? `+${offscreen}` : `${current + 1}/${pages}` }), _jsx("button", { type: "button", onClick: () => setPage(Math.min(pages - 1, current + 1)), disabled: current >= pages - 1, "aria-label": "More tiles", className: "rounded p-1 hover:bg-base-300 focus-visible:outline-2 focus-visible:outline-primary disabled:opacity-40", children: _jsx(Icon, { name: column ? 'chevron-down' : 'chevron-right', size: "sm" }) })] }))] }));
84
+ }
85
+ function Tile({ deviceId, person, stream, you, placement, quality, mutedForMe, reactions, size, onMuteForMe, }) {
86
+ const video = useRef(null);
87
+ // Assigned imperatively: a MediaStream is not a URL and React has no prop for it.
88
+ useEffect(() => {
89
+ if (video.current)
90
+ video.current.srcObject = stream ?? null;
91
+ }, [stream]);
92
+ const device = person?.devices.find((one) => one.deviceId === deviceId);
93
+ const poor = quality !== null && (quality.packetLoss > 0.08 || quality.roundTripMs > 400);
94
+ /*
95
+ * The same ring as on the map, so "who is talking" looks like one thing
96
+ * wherever it appears — and the same rule about motion, which is that the ring
97
+ * carries the information and the pulse is what goes when somebody has asked
98
+ * for less of it.
99
+ */
100
+ const reducedMotion = useReducedMotion();
101
+ const speaking = device?.speaking ?? false;
102
+ const handRaised = device?.handRaisedAt != null;
103
+ return (_jsxs("div", { className: [
104
+ 'relative shrink-0 overflow-hidden rounded-lg bg-base-300 ring-1 ring-base-300',
105
+ placement === 'grid'
106
+ ? // Sized by the grid once it has measured itself; a sensible size until then.
107
+ size
108
+ ? ''
109
+ : 'aspect-video w-72 max-w-full'
110
+ : placement === 'right'
111
+ ? 'aspect-video w-full'
112
+ : 'aspect-video h-24',
113
+ speaking ? 'ring-2 ring-primary' : '',
114
+ ].join(' '), style: size ? { width: size.width, height: size.height } : undefined, "data-testid": `tile-${deviceId}`, children: [speaking && !reducedMotion && (_jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0 animate-pulse rounded-lg ring-2 ring-primary ring-inset" })), stream ? (_jsx("video", { ref: video, autoPlay: true, playsInline: true,
115
+ // Always muted: this element is a picture. The sound is the audio sink's
116
+ // job, and an unmuted tile would play everybody twice.
117
+ muted: true,
118
+ // Your own camera is mirrored, because that is how people expect to see
119
+ // themselves. Everybody else is not.
120
+ //
121
+ // Contained, never cropped: the tile is already the shape of a camera, so
122
+ // an ordinary picture fills it exactly, and one of another shape — a
123
+ // phone held upright — gets a border rather than losing somebody's face.
124
+ className: ['h-full w-full object-contain', you ? 'scale-x-[-1]' : ''].join(' ') })) : (
125
+ // Camera off is not a broken tile: it is somebody who has not turned it
126
+ // on, and their name is what you actually need.
127
+ _jsx("div", { className: "grid h-full w-full place-items-center px-2 text-center text-xs text-base-content/70", children: person?.displayName ?? 'Connecting…' })), _jsx(ReactionFloat, { reactions: reactions ?? [], size: placement === 'top' ? 20 : 30 }), _jsxs("div", { className: "absolute inset-x-0 bottom-0 flex items-center gap-1 bg-base-100/85 px-1 py-0.5 text-[10px] backdrop-blur-sm", children: [person && _jsx(StatusDot, { status: person.status, size: 8, labelled: false }), _jsxs("span", { className: "min-w-0 flex-1 truncate", children: [you ? 'You' : (person?.displayName ?? 'Someone'), speaking && _jsx("span", { className: "sr-only", children: ", speaking" })] }), handRaised && (_jsx("span", { className: "text-primary", children: _jsx(Icon, { name: "raise-hand", size: "xs", title: "Hand raised" }) })), device?.muted && (_jsx("span", { className: "text-error", children: _jsx(Icon, { name: "mic-off", size: "xs", title: "Microphone off" }) })), poor && (_jsx("span", { className: "text-warning", children: _jsx(Icon, { name: "alert", size: "xs", title: quality?.relayed ? 'Relayed, weak connection' : 'Weak connection' }) })), !you && (_jsx("button", { type: "button", onClick: () => onMuteForMe(deviceId, !mutedForMe), "aria-pressed": mutedForMe, "aria-label": mutedForMe
128
+ ? `Unmute ${person?.displayName ?? 'this person'} for yourself`
129
+ : `Mute ${person?.displayName ?? 'this person'} for yourself only`, className: [
130
+ 'rounded px-1 focus-visible:outline-2 focus-visible:outline-primary',
131
+ mutedForMe ? 'bg-error text-error-content' : 'hover:bg-base-300',
132
+ ].join(' '), children: _jsx(Icon, { name: mutedForMe ? 'mic-off' : 'mic', size: "xs" }) }))] }), mutedForMe && (_jsx("span", { className: "absolute left-1 top-1 rounded bg-error px-1 text-[9px] text-error-content", children: "Muted for you" }))] }));
133
+ }
134
+ //# sourceMappingURL=CallTiles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CallTiles.js","sourceRoot":"","sources":["../src/CallTiles.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import type { DeviceChoice } from '@unityevolv/ofiskit-realtime-client';
2
+ /**
3
+ * Choosing a microphone, a camera and a speaker, and seeing that they work
4
+ * before anybody else sees you.
5
+ *
6
+ * Most of the value here is in the failure cases. Picking a device from a list
7
+ * is easy; the reason this screen exists is that "you denied permission" and
8
+ * "another app is holding your camera" are different problems with different
9
+ * fixes, and the browser reports them almost identically and explains neither.
10
+ */
11
+ export interface DevicePanelProps {
12
+ open: boolean;
13
+ onClose(): void;
14
+ choice: DeviceChoice;
15
+ onChoose(choice: DeviceChoice): void;
16
+ /** Applies the choice to a call that is already running. */
17
+ onApply?(choice: DeviceChoice): void;
18
+ /** Shows the camera preview and the level meter before joining. */
19
+ preview?: boolean;
20
+ }
21
+ export declare function DevicePanel(props: DevicePanelProps): import("react").JSX.Element;
22
+ /**
23
+ * Said before the browser's own prompt, not after.
24
+ *
25
+ * The browser asks a blunt question with no context, and somebody who says no
26
+ * to it has a much harder time saying yes later. Explaining first is the
27
+ * difference between a prompt that gets accepted and a support ticket.
28
+ */
29
+ export declare function PermissionPrimer({ onContinue }: {
30
+ onContinue(): void;
31
+ }): import("react").JSX.Element;
32
+ //# sourceMappingURL=DevicePanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DevicePanel.d.ts","sourceRoot":"","sources":["../src/DevicePanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAW,MAAM,qCAAqC,CAAA;AAUhF;;;;;;;;GAQG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,IAAI,IAAI,CAAA;IACf,MAAM,EAAE,YAAY,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAA;IACpC,4DAA4D;IAC5D,OAAO,CAAC,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAA;IACpC,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,+BA2OlD;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,IAAI,IAAI,CAAA;CAAE,+BActE"}