@spicetify/kit 0.1.0 → 0.3.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.
@@ -508,16 +508,28 @@ declare namespace Spicetify {
508
508
  const CosmosAPI: any;
509
509
  /**
510
510
  * Reaches hosts that refuse the client's origin, through the local daemon
511
- * when it is running and the hosted proxy otherwise. Setting
512
- * `spicetify:corsProxyTemplate` replaces the chain with that one template.
511
+ * when it is running and the hosted proxy otherwise. Its global
512
+ * configuration API can replace the chain with one custom template.
513
513
  */
514
514
  namespace CORSProxy {
515
+ type Mode = "automatic" | "custom";
516
+ interface Configuration {
517
+ mode: Mode;
518
+ template: string | null;
519
+ automaticTemplates: [string, string];
520
+ }
515
521
  /** The proxied URL for `target` using the first template in the chain. */
516
522
  function url(target: string): string;
517
523
  /** Fetches `target` proxied, falling back to the next template only when a request gets no answer. */
518
524
  function fetch(target: string, options?: RequestInit): Promise<Response>;
519
525
  /** The active chain, in order of preference. */
520
526
  function templates(): string[];
527
+ /** Describes the global proxy mode without exposing its storage details. */
528
+ function configuration(): Configuration;
529
+ /** Applies the automatic chain or a validated custom URL template immediately. */
530
+ function configure(options: { mode: "automatic" } | { mode: "custom"; template: string }): Configuration;
531
+ /** True when a custom absolute URL template contains the required `{url}` placeholder. */
532
+ function isValidTemplate(template: string): boolean;
521
533
  }
522
534
  /**
523
535
  * Async wrappers of CosmosAPI
@@ -819,12 +831,12 @@ declare namespace Spicetify {
819
831
  };
820
832
 
821
833
  /**
822
- * The client's internal platform APIs. Typed from the generated
823
- * PlatformAutoGen surface, exposing both the property form the client uses
824
- * (`Platform.LibraryAPI`) and the generated `getLibraryAPI()` methods:
825
- * every `getFooAPI(): R` yields a `FooAPI: R` property too.
834
+ * The client's internal platform APIs, typed from platform.d.ts, the
835
+ * committed snapshot scripts/platform-types.ts regenerates per Spotify
836
+ * build. The snapshot's root is the property form the client uses
837
+ * (`Platform.LibraryAPI`).
826
838
  */
827
- type PlatformAutoGen = import("/hooks/PlatformAutoGen").PlatformAutoGen;
839
+ type PlatformAutoGen = import("./platform").Platform;
828
840
  // One level of member names per API — enough to autocomplete which methods an
829
841
  // API exposes (Platform.LibraryAPI.getContents…), with every value left `any`.
830
842
  // The generated method signatures are unreliable (arg counts and nullability
@@ -836,22 +848,10 @@ declare namespace Spicetify {
836
848
  : T extends object
837
849
  ? { [M in keyof T]: any }
838
850
  : T;
839
- // Both forms the client offers: the getFooAPI() methods (permissive args,
840
- // shaped return) and each getFooAPI(): R also as a FooAPI: R property (the
841
- // form the client uses). The Record<string, any> tail keeps unknown keys open.
842
- type PlatformApis =
843
- & {
844
- [K in keyof PlatformAutoGen]: PlatformAutoGen[K] extends (...args: any[]) => infer R
845
- ? (...args: any[]) => Shape<R>
846
- : PlatformAutoGen[K];
847
- }
848
- & {
849
- [K in keyof PlatformAutoGen as K extends `get${infer Name}` ? Name : never]: PlatformAutoGen[K] extends (
850
- ...args: any[]
851
- ) => infer R
852
- ? Shape<R>
853
- : never;
854
- };
851
+ // The Record<string, any> tail keeps unknown keys open.
852
+ type PlatformApis = {
853
+ [K in keyof PlatformAutoGen]: Shape<PlatformAutoGen[K]>;
854
+ };
855
855
  const Platform: PlatformApis & Record<string, any>;
856
856
  /**
857
857
  * Queue object contains list of queuing tracks,
@@ -0,0 +1,53 @@
1
+ /*
2
+ * Copyright (C) 2026 Afonso Jorge Ramos
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ const FOCUSABLE_SELECTOR = [
7
+ "button:not([disabled])",
8
+ "[href]",
9
+ "input:not([disabled])",
10
+ "select:not([disabled])",
11
+ "textarea:not([disabled])",
12
+ '[tabindex]:not([tabindex="-1"])',
13
+ ].join(",");
14
+
15
+ const focusableElements = (dialog: HTMLElement): HTMLElement[] =>
16
+ [...dialog.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)].filter(
17
+ (element) => !element.hidden && element.getAttribute("aria-hidden") !== "true",
18
+ );
19
+
20
+ export function activateDialog(dialog: HTMLElement, close: () => void): () => void {
21
+ const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
22
+ const onKeyDown = (event: KeyboardEvent) => {
23
+ if (event.key === "Escape") {
24
+ event.preventDefault();
25
+ event.stopPropagation();
26
+ close();
27
+ return;
28
+ }
29
+ if (event.key !== "Tab") return;
30
+ const focusable = focusableElements(dialog);
31
+ if (focusable.length === 0) {
32
+ event.preventDefault();
33
+ dialog.focus();
34
+ return;
35
+ }
36
+ const first = focusable[0];
37
+ const last = focusable.at(-1)!;
38
+ if (event.shiftKey && document.activeElement === first) {
39
+ event.preventDefault();
40
+ last.focus();
41
+ } else if (!event.shiftKey && document.activeElement === last) {
42
+ event.preventDefault();
43
+ first.focus();
44
+ }
45
+ };
46
+
47
+ document.addEventListener("keydown", onKeyDown, true);
48
+ (focusableElements(dialog)[0] ?? dialog).focus();
49
+ return () => {
50
+ document.removeEventListener("keydown", onKeyDown, true);
51
+ if (previousFocus?.isConnected) previousFocus.focus();
52
+ };
53
+ }
@@ -0,0 +1,57 @@
1
+ /*
2
+ * Copyright (C) 2026 Afonso Jorge Ramos
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ export interface AnchorRect {
7
+ left: number;
8
+ top: number;
9
+ right: number;
10
+ bottom: number;
11
+ }
12
+
13
+ export interface SurfaceSize {
14
+ width: number;
15
+ height: number;
16
+ }
17
+
18
+ export interface ViewportSize {
19
+ width: number;
20
+ height: number;
21
+ }
22
+
23
+ export interface FloatingPositionOptions {
24
+ align: "center" | "start";
25
+ gap: number;
26
+ margin?: number;
27
+ }
28
+
29
+ export interface FloatingPosition {
30
+ left: number;
31
+ top: number;
32
+ placement: "above" | "below";
33
+ }
34
+
35
+ const clamp = (value: number, minimum: number, maximum: number): number =>
36
+ Math.min(Math.max(value, minimum), Math.max(minimum, maximum));
37
+
38
+ export function calculateFloatingPosition(
39
+ anchor: AnchorRect,
40
+ surface: SurfaceSize,
41
+ viewport: ViewportSize,
42
+ options: FloatingPositionOptions,
43
+ ): FloatingPosition {
44
+ const margin = options.margin ?? 8;
45
+ const spaceBelow = viewport.height - anchor.bottom - margin;
46
+ const spaceAbove = anchor.top - margin;
47
+ const placement = spaceBelow >= surface.height + options.gap || spaceBelow >= spaceAbove ? "below" : "above";
48
+ const desiredTop = placement === "below" ? anchor.bottom + options.gap : anchor.top - options.gap - surface.height;
49
+ const anchorCenter = anchor.left + (anchor.right - anchor.left) / 2;
50
+ const desiredLeft = options.align === "center" ? anchorCenter - surface.width / 2 : anchor.left;
51
+
52
+ return {
53
+ left: Math.round(clamp(desiredLeft, margin, viewport.width - surface.width - margin)),
54
+ top: Math.round(clamp(desiredTop, margin, viewport.height - surface.height - margin)),
55
+ placement,
56
+ };
57
+ }
@@ -0,0 +1,298 @@
1
+ /*
2
+ * Copyright (C) 2026 Afonso Jorge Ramos
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ import { React, ReactDOM } from "../src/expose/React.ts";
7
+ import { calculateFloatingPosition } from "./floating-position.ts";
8
+
9
+ const TOOLTIP_HIDE_DELAY_MS = 300;
10
+ const FOCUSABLE_SELECTOR =
11
+ 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
12
+
13
+ type FloatingRole = "dialog" | "menu";
14
+ type TriggerProps = React.AriaAttributes & {
15
+ disabled?: boolean;
16
+ onClick?: React.MouseEventHandler<HTMLElement>;
17
+ };
18
+
19
+ function useFloatingPlacement(
20
+ open: boolean,
21
+ anchorRef: React.RefObject<HTMLElement | null>,
22
+ surfaceRef: React.RefObject<HTMLElement | null>,
23
+ align: "center" | "start",
24
+ gap: number,
25
+ ): void {
26
+ React.useLayoutEffect(() => {
27
+ if (!open) return;
28
+ const anchor = anchorRef.current;
29
+ const surface = surfaceRef.current;
30
+ if (!anchor || !surface) return;
31
+
32
+ const place = () => {
33
+ if (!anchor.isConnected || !surface.isConnected) return;
34
+ const anchorRect = anchor.getBoundingClientRect();
35
+ const surfaceRect = surface.getBoundingClientRect();
36
+ const position = calculateFloatingPosition(
37
+ anchorRect,
38
+ surfaceRect,
39
+ { width: window.innerWidth, height: window.innerHeight },
40
+ { align, gap },
41
+ );
42
+ surface.style.left = `${position.left}px`;
43
+ surface.style.top = `${position.top}px`;
44
+ surface.style.visibility = "visible";
45
+ surface.dataset.placement = position.placement;
46
+ };
47
+
48
+ place();
49
+ window.addEventListener("resize", place);
50
+ document.addEventListener("scroll", place, true);
51
+ const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(place);
52
+ observer?.observe(anchor);
53
+ observer?.observe(surface);
54
+ return () => {
55
+ window.removeEventListener("resize", place);
56
+ document.removeEventListener("scroll", place, true);
57
+ observer?.disconnect();
58
+ };
59
+ }, [align, anchorRef, gap, open, surfaceRef]);
60
+ }
61
+
62
+ function focusTrigger(anchor: HTMLElement | null): void {
63
+ anchor?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR)?.focus();
64
+ }
65
+
66
+ function moveMenuFocus(event: React.KeyboardEvent<HTMLElement>): void {
67
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
68
+ const items = [...event.currentTarget.querySelectorAll<HTMLElement>('[role^="menuitem"]:not([disabled])')];
69
+ if (!items.length) return;
70
+ event.preventDefault();
71
+ const currentIndex = items.indexOf(document.activeElement as HTMLElement);
72
+ const nextIndex =
73
+ event.key === "Home"
74
+ ? 0
75
+ : event.key === "End"
76
+ ? items.length - 1
77
+ : (currentIndex + (event.key === "ArrowDown" ? 1 : -1) + items.length) % items.length;
78
+ items[nextIndex]?.focus();
79
+ }
80
+
81
+ function usePopoverDismissal(
82
+ open: boolean,
83
+ close: () => void,
84
+ anchorRef: React.RefObject<HTMLElement | null>,
85
+ surfaceRef: React.RefObject<HTMLElement | null>,
86
+ ): void {
87
+ React.useEffect(() => {
88
+ if (!open) return;
89
+ const onDocumentMouseDown = (event: MouseEvent) => {
90
+ const target = event.target as Node;
91
+ if (!anchorRef.current?.contains(target) && !surfaceRef.current?.contains(target)) close();
92
+ };
93
+ const onDocumentKeyDown = (event: KeyboardEvent) => {
94
+ if (event.key !== "Escape") return;
95
+ event.preventDefault();
96
+ event.stopPropagation();
97
+ close();
98
+ requestAnimationFrame(() => focusTrigger(anchorRef.current));
99
+ };
100
+ const onDocumentFocus = (event: FocusEvent) => {
101
+ const target = event.target as Node;
102
+ if (!anchorRef.current?.contains(target) && !surfaceRef.current?.contains(target)) close();
103
+ };
104
+
105
+ document.addEventListener("mousedown", onDocumentMouseDown, true);
106
+ document.addEventListener("keydown", onDocumentKeyDown, true);
107
+ document.addEventListener("focusin", onDocumentFocus, true);
108
+ return () => {
109
+ document.removeEventListener("mousedown", onDocumentMouseDown, true);
110
+ document.removeEventListener("keydown", onDocumentKeyDown, true);
111
+ document.removeEventListener("focusin", onDocumentFocus, true);
112
+ };
113
+ }, [anchorRef, close, open, surfaceRef]);
114
+ }
115
+
116
+ export const Tooltip: React.FC<{
117
+ label: React.ReactNode;
118
+ children: React.ReactElement<TriggerProps>;
119
+ }> = (props) => {
120
+ const id = React.useId();
121
+ const anchorRef = React.useRef<HTMLSpanElement>(null);
122
+ const tooltipRef = React.useRef<HTMLDivElement>(null);
123
+ const hideTimer = React.useRef<number>();
124
+ const [visible, setVisible] = React.useState(false);
125
+ const cancelHide = React.useCallback(() => window.clearTimeout(hideTimer.current), []);
126
+ const show = React.useCallback(() => {
127
+ cancelHide();
128
+ setVisible(true);
129
+ }, [cancelHide]);
130
+ const dismiss = React.useCallback(() => {
131
+ cancelHide();
132
+ setVisible(false);
133
+ }, [cancelHide]);
134
+ const scheduleHide = React.useCallback(() => {
135
+ cancelHide();
136
+ hideTimer.current = window.setTimeout(() => setVisible(false), TOOLTIP_HIDE_DELAY_MS);
137
+ }, [cancelHide]);
138
+ useFloatingPlacement(visible, anchorRef, tooltipRef, "center", 8);
139
+
140
+ React.useEffect(() => () => cancelHide(), [cancelHide]);
141
+ React.useEffect(() => {
142
+ if (!visible) return;
143
+ const onKeyDown = (event: KeyboardEvent) => {
144
+ if (event.key !== "Escape") return;
145
+ event.preventDefault();
146
+ event.stopPropagation();
147
+ cancelHide();
148
+ setVisible(false);
149
+ };
150
+ document.addEventListener("keydown", onKeyDown, true);
151
+ return () => document.removeEventListener("keydown", onKeyDown, true);
152
+ }, [cancelHide, visible]);
153
+
154
+ const existingDescription = props.children.props["aria-describedby"];
155
+ const describedBy = [existingDescription, id].filter(Boolean).join(" ");
156
+ const child = React.cloneElement(props.children, { "aria-describedby": describedBy });
157
+ const disabled = props.children.props.disabled === true;
158
+
159
+ return (
160
+ <span
161
+ ref={anchorRef}
162
+ className="spicetify-floating-anchor"
163
+ tabIndex={disabled ? 0 : undefined}
164
+ aria-describedby={disabled ? id : undefined}
165
+ onClickCapture={dismiss}
166
+ onPointerEnter={show}
167
+ onPointerLeave={scheduleHide}
168
+ onFocusCapture={show}
169
+ onBlurCapture={scheduleHide}
170
+ >
171
+ {child}
172
+ {visible
173
+ ? ReactDOM.createPortal(
174
+ <div
175
+ ref={tooltipRef}
176
+ id={id}
177
+ role="tooltip"
178
+ className="spicetify-tooltip"
179
+ style={{ visibility: "hidden" }}
180
+ onPointerEnter={cancelHide}
181
+ onPointerLeave={scheduleHide}
182
+ >
183
+ {props.label}
184
+ </div>,
185
+ document.body,
186
+ )
187
+ : null}
188
+ </span>
189
+ );
190
+ };
191
+
192
+ export const Popover: React.FC<{
193
+ ariaLabel: string;
194
+ children: React.ReactElement<TriggerProps>;
195
+ content: React.ReactNode | ((close: () => void) => React.ReactNode);
196
+ role?: FloatingRole;
197
+ align?: "center" | "start";
198
+ className?: string;
199
+ tooltip?: React.ReactNode;
200
+ onClose?: () => void;
201
+ }> = (props) => {
202
+ const id = React.useId();
203
+ const anchorRef = React.useRef<HTMLSpanElement>(null);
204
+ const surfaceRef = React.useRef<HTMLDivElement>(null);
205
+ const openRef = React.useRef(false);
206
+ const [open, setOpen] = React.useState(false);
207
+ const close = React.useCallback(() => {
208
+ if (!openRef.current) return;
209
+ openRef.current = false;
210
+ setOpen(false);
211
+ props.onClose?.();
212
+ }, [props.onClose]);
213
+ const toggle = () => {
214
+ if (openRef.current) {
215
+ close();
216
+ return;
217
+ }
218
+ openRef.current = true;
219
+ setOpen(true);
220
+ };
221
+ const role = props.role ?? "dialog";
222
+ useFloatingPlacement(open, anchorRef, surfaceRef, props.align ?? "start", 8);
223
+ usePopoverDismissal(open, close, anchorRef, surfaceRef);
224
+ React.useEffect(() => {
225
+ if (!open) return;
226
+ const frame = requestAnimationFrame(() => {
227
+ const surface = surfaceRef.current;
228
+ (surface?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR) ?? surface)?.focus();
229
+ });
230
+ return () => cancelAnimationFrame(frame);
231
+ }, [open]);
232
+ React.useEffect(
233
+ () => () => {
234
+ openRef.current = false;
235
+ },
236
+ [],
237
+ );
238
+
239
+ const originalClick = props.children.props.onClick;
240
+ const trigger = React.cloneElement(props.children, {
241
+ "aria-haspopup": role,
242
+ "aria-expanded": open,
243
+ "aria-controls": open ? id : undefined,
244
+ onClick: (event: React.MouseEvent<HTMLElement>) => {
245
+ originalClick?.(event);
246
+ if (!event.defaultPrevented) toggle();
247
+ },
248
+ });
249
+ const describedTrigger = props.tooltip ? <Tooltip label={props.tooltip}>{trigger}</Tooltip> : trigger;
250
+ const surfaceClass = props.className
251
+ ? `spicetify-popover spicetify-popover--owned ${props.className}`
252
+ : "spicetify-popover spicetify-popover--owned";
253
+
254
+ return (
255
+ <span ref={anchorRef} className="spicetify-floating-anchor">
256
+ {describedTrigger}
257
+ {open
258
+ ? ReactDOM.createPortal(
259
+ <div
260
+ ref={surfaceRef}
261
+ id={id}
262
+ role={role}
263
+ aria-label={props.ariaLabel}
264
+ className={surfaceClass}
265
+ tabIndex={-1}
266
+ style={{ visibility: "hidden" }}
267
+ onKeyDown={role === "menu" ? moveMenuFocus : undefined}
268
+ >
269
+ {typeof props.content === "function" ? props.content(close) : props.content}
270
+ </div>,
271
+ document.body,
272
+ )
273
+ : null}
274
+ </span>
275
+ );
276
+ };
277
+
278
+ export const PopoverMenu: React.FC<{ children: React.ReactNode }> = (props) => (
279
+ <div className="spicetify-popover-menu">{props.children}</div>
280
+ );
281
+
282
+ export const PopoverMenuItem: React.FC<{
283
+ selected?: boolean;
284
+ disabled?: boolean;
285
+ onSelect: () => void;
286
+ children: React.ReactNode;
287
+ }> = (props) => (
288
+ <button
289
+ type="button"
290
+ role={props.selected === undefined ? "menuitem" : "menuitemradio"}
291
+ aria-checked={props.selected}
292
+ className="spicetify-popover-menu-item"
293
+ disabled={props.disabled}
294
+ onClick={props.onSelect}
295
+ >
296
+ <span>{props.children}</span>
297
+ </button>
298
+ );
@@ -1,128 +1,35 @@
1
1
  /*
2
2
  * Copyright (C) 2024 Delusoire
3
+ * Copyright (C) 2026 Afonso Jorge Ramos
3
4
  * SPDX-License-Identifier: GPL-3.0-or-later
4
5
  */
5
6
 
6
7
  import { React } from "../src/expose/React.ts";
7
- import { Locale } from "../src/webpack/misc.ts";
8
-
9
8
  import RootRegistry from "../src/registers/root.ts";
9
+ import { Dialog } from "./primitives.tsx";
10
+
10
11
  const [RootChildrenRegistry] = RootRegistry;
11
- import { createIconComponent } from "../src/createIconComponent.tsx";
12
12
 
13
- let ref: React.ReactElement | undefined = undefined;
13
+ export interface ModalOptions {
14
+ title: string;
15
+ content: React.ReactNode;
16
+ isLarge?: boolean;
17
+ }
14
18
 
15
- export function display({ title, content, isLarge }: { title: string; content: React.ReactElement; isLarge: boolean }) {
16
- hide();
19
+ let mountedModal: React.ReactElement | undefined;
17
20
 
18
- RootChildrenRegistry.add(
19
- (ref = (
20
- <PopupModal contentLabel={title} children={content} isEmbedWidgetGeneratorOrTrackCreditsModal={isLarge} />
21
- )),
21
+ export function display({ title, content, isLarge = false }: ModalOptions): void {
22
+ hide();
23
+ mountedModal = (
24
+ <Dialog title={title} size={isLarge ? "large" : "normal"} onClose={hide}>
25
+ {content}
26
+ </Dialog>
22
27
  );
28
+ RootChildrenRegistry.add(mountedModal);
23
29
  }
24
30
 
25
- export function hide() {
26
- if (ref) {
27
- RootChildrenRegistry.delete(ref);
28
- ref = undefined;
29
- }
30
- }
31
-
32
- interface PopupModalProps {
33
- contentLabel: string;
34
- children: React.ReactNode;
35
- isEmbedWidgetGeneratorOrTrackCreditsModal: boolean;
31
+ export function hide(): void {
32
+ if (!mountedModal) return;
33
+ RootChildrenRegistry.delete(mountedModal);
34
+ mountedModal = undefined;
36
35
  }
37
-
38
- // Self-contained modal shell: client modal components (GenericModal, UI.*)
39
- // require the client tree's contexts, which anchors don't have. The shell
40
- // owns the backdrop and dismissal; the MAP-classed chrome keeps native
41
- // styling.
42
- const ModalShell = (props: { contentLabel: string; children: React.ReactNode }) => {
43
- React.useEffect(() => {
44
- const onKey = (e: KeyboardEvent) => {
45
- if (e.key === "Escape") hide();
46
- };
47
- document.addEventListener("keydown", onKey);
48
- return () => document.removeEventListener("keydown", onKey);
49
- }, []);
50
-
51
- return (
52
- <div
53
- role="dialog"
54
- aria-label={props.contentLabel}
55
- style={{
56
- position: "fixed",
57
- inset: 0,
58
- zIndex: 100,
59
- display: "flex",
60
- alignItems: "center",
61
- justifyContent: "center",
62
- background: "rgba(0, 0, 0, 0.7)",
63
- }}
64
- onClick={(e) => {
65
- if (e.target === e.currentTarget) hide();
66
- }}
67
- >
68
- <div
69
- style={{
70
- background: "var(--spice-card, var(--background-elevated-base, #282828))",
71
- color: "var(--spice-text, var(--text-base, #fff))",
72
- borderRadius: "8px",
73
- maxHeight: "80vh",
74
- maxWidth: "90vw",
75
- overflow: "auto",
76
- }}
77
- >
78
- {props.children}
79
- </div>
80
- </div>
81
- );
82
- };
83
-
84
- const closeIcon =
85
- "<path d='M2.47 2.47a.75.75 0 0 1 1.06 0L8 6.94l4.47-4.47a.75.75 0 1 1 1.06 1.06L9.06 8l4.47 4.47a.75.75 0 1 1-1.06 1.06L8 9.06l-4.47 4.47a.75.75 0 0 1-1.06-1.06L6.94 8 2.47 3.53a.75.75 0 0 1 0-1.06Z'/>";
86
-
87
- const PopupModal = (props: PopupModalProps) => {
88
- if (props.isEmbedWidgetGeneratorOrTrackCreditsModal) {
89
- return (
90
- <ModalShell contentLabel={props.contentLabel}>
91
- <div className={MAP.modal.widget_generator.container} style={{ overflow: "scroll", width: "60vw" }}>
92
- <div className={MAP.modal.widget_generator.header.container}>
93
- <h1 className="encore-text encore-text-title-small">{props.contentLabel}</h1>
94
- <button className={MAP.modal.widget_generator.header.close} onClick={hide}>
95
- {createIconComponent({
96
- icon: closeIcon,
97
- "aria-label": Locale.get("close"),
98
- })}
99
- </button>
100
- </div>
101
- <div className={MAP.modal.widget_generator.content.container}>{props.children}</div>
102
- </div>
103
- </ModalShell>
104
- );
105
- }
106
-
107
- return (
108
- <ModalShell contentLabel={props.contentLabel}>
109
- <div className={MAP.modal.track_credits.container}>
110
- <div className={MAP.modal.track_credits.header.container}>
111
- <h1 className="encore-text encore-text-title-medium">{props.contentLabel}</h1>
112
- <button
113
- className={MAP.modal.track_credits.header.close}
114
- aria-label={Locale.get("close")}
115
- onClick={hide}
116
- >
117
- {createIconComponent({
118
- icon: closeIcon,
119
- "aria-label": Locale.get("close"),
120
- iconSize: 18,
121
- })}
122
- </button>
123
- </div>
124
- <div className={MAP.modal.track_credits.content.container}>{props.children}</div>
125
- </div>
126
- </ModalShell>
127
- );
128
- };