@spicetify/kit 0.3.1 → 0.4.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.
@@ -30,18 +30,106 @@ export interface DaemonCapabilities {
30
30
  apply(): Promise<unknown>;
31
31
  blockUpdates(): Promise<unknown>;
32
32
  unblockUpdates(): Promise<unknown>;
33
+ updateAndApplySupported?(): Promise<boolean | null>;
34
+ updateAndApply?: UpdateAndApplyCapability;
35
+ managedSpotify?: ManagedSpotifyCapability;
36
+ }
37
+
38
+ export type ManagedSpotifyJob =
39
+ | { kind: "idle" }
40
+ | { kind: "running"; jobId: string; phase: "checking" | "downloading" | "preparing" | "activating" }
41
+ | { kind: "complete"; jobId: string }
42
+ | { kind: "failed"; jobId: string; message: string };
43
+
44
+ export type ManagedSpotifyInstallation =
45
+ | { kind: "external" }
46
+ | { kind: "unavailable"; message: string }
47
+ | { kind: "managed"; version: string; channel: "stable" | "testing"; nativeBlocked: boolean | null };
48
+
49
+ export type ManagedSpotifyAvailability =
50
+ | { kind: "current"; version: string }
51
+ | { kind: "ready"; version: string }
52
+ | { kind: "unavailable"; version: string; message: string };
53
+
54
+ export interface ManagedSpotifySnapshot {
55
+ installation: ManagedSpotifyInstallation;
56
+ job: ManagedSpotifyJob;
57
+ }
58
+
59
+ export interface ManagedSpotifyCapability {
60
+ status(): Promise<ManagedSpotifySnapshot | null>;
61
+ check(): Promise<ManagedSpotifyAvailability>;
62
+ update(): Promise<UpdateAdmission>;
63
+ }
64
+
65
+ export type UpdateFailureCode =
66
+ | "unsupported-platform"
67
+ | "unsupported-target"
68
+ | "update-unavailable"
69
+ | "renderer-timeout"
70
+ | "spotify-update-failed"
71
+ | "apply-failed";
72
+
73
+ export type UpdateAndApplyStatus =
74
+ | { kind: "idle" }
75
+ | { kind: "accepted"; jobId: string; fromVersion: string }
76
+ | { kind: "waiting-for-update"; jobId: string; fromVersion: string }
77
+ | { kind: "downloading"; jobId: string; targetVersion: string }
78
+ | { kind: "installing-spotify"; jobId: string; targetVersion: string }
79
+ | { kind: "applying-spicetify"; jobId: string; targetVersion: string }
80
+ | { kind: "securing"; jobId: string; targetVersion?: string; message?: string; manualRecovery?: boolean }
81
+ | { kind: "complete"; jobId: string; fromVersion: string; toVersion: string }
82
+ | { kind: "failed-safe"; jobId: string; code: UpdateFailureCode; message: string };
83
+
84
+ export interface UpdateAdmission {
85
+ jobId: string;
86
+ disposition: "accepted" | "joined";
87
+ }
88
+
89
+ export interface UpdateAndApplyCapability {
90
+ (): Promise<UpdateAdmission>;
91
+ observe(listener: (status: UpdateAndApplyStatus) => void): () => void;
33
92
  }
34
93
 
35
94
  export interface SnackbarCapabilities {
36
95
  enqueueSnackbar(message: string, options?: { variant?: string }): unknown;
37
96
  }
38
97
 
98
+ type CosmosBody = Record<string, unknown>;
99
+ type CosmosHeaders = Spicetify.CosmosAsync.Headers;
100
+ type CosmosMethod = Spicetify.CosmosAsync.Method;
101
+ type CosmosResponse = Omit<Spicetify.CosmosAsync.Response, "body"> & { body: unknown };
102
+
103
+ export interface CosmosCapabilities {
104
+ head(url: string, headers?: CosmosHeaders): Promise<CosmosHeaders>;
105
+ get(url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<unknown>;
106
+ post(url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<unknown>;
107
+ put(url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<unknown>;
108
+ del(url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<unknown>;
109
+ patch(url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<unknown>;
110
+ sub(
111
+ url: string,
112
+ callback: (body: unknown) => void,
113
+ onError?: (error: unknown) => void,
114
+ body?: CosmosBody,
115
+ headers?: CosmosHeaders,
116
+ ): Promise<unknown>;
117
+ postSub(
118
+ url: string,
119
+ body: CosmosBody | null,
120
+ callback: (body: unknown) => void,
121
+ onError?: (error: unknown) => void,
122
+ ): Promise<unknown>;
123
+ request(method: CosmosMethod, url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<CosmosResponse>;
124
+ resolve(method: CosmosMethod, url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<CosmosResponse>;
125
+ }
126
+
39
127
  export interface ClientCapabilities {
40
128
  readonly player: typeof Spicetify.Player;
41
129
  readonly platform: typeof Spicetify.Platform;
42
130
  readonly storage: typeof Spicetify.LocalStorage;
43
131
  readonly uri: typeof Spicetify.URI;
44
- readonly cosmos: typeof Spicetify.CosmosAsync;
132
+ readonly cosmos: CosmosCapabilities;
45
133
  readonly corsProxy: typeof Spicetify.CORSProxy;
46
134
  readonly graphQL: typeof Spicetify.GraphQL;
47
135
  readonly locale: typeof Spicetify.Locale;
@@ -21,6 +21,15 @@ function createGlobalThisShadow() {
21
21
 
22
22
  type CosmosRequest = { uri: string; method: string; body: string };
23
23
 
24
+ declare global {
25
+ var sendCosmosRequest: (request: {
26
+ request: string;
27
+ persistent?: boolean;
28
+ onSuccess?: (response: string) => void;
29
+ onFailure?: (error: unknown) => void;
30
+ }) => unknown;
31
+ }
32
+
24
33
  const CosmosRequestSubject = new Subject<CosmosRequest>();
25
34
  export const UpdateTitlebarSubject = new BehaviorSubject<number>(-1);
26
35
 
@@ -41,7 +41,7 @@ const playerListener = ({ data: state }: any) => {
41
41
  cachedState = state;
42
42
  };
43
43
 
44
- const historyListener = (location) => EventBus.History.updated.next(location);
44
+ const historyListener = (location: unknown) => EventBus.History.updated.next(location);
45
45
 
46
46
  const updateTitlebarListener = (height: number) => EventBus.ControlMessage.titlebar_updated.next(height);
47
47
 
@@ -0,0 +1,17 @@
1
+ /*
2
+ * Copyright (C) 2026 Afonso Jorge Ramos
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ export const placeMenuItemsHost = (menu: Element, host: HTMLElement): void => {
7
+ const settingsRow = Array.from(menu.children).find(
8
+ (child) => child.matches('a[href="/preferences"]') || child.querySelector('a[href="/preferences"]'),
9
+ );
10
+
11
+ if (settingsRow) {
12
+ if (settingsRow.nextElementSibling !== host) settingsRow.after(host);
13
+ return;
14
+ }
15
+
16
+ if (host.parentElement !== menu) menu.append(host);
17
+ };
@@ -9,6 +9,7 @@ import { matchLast } from "../util.ts";
9
9
 
10
10
  import { warn } from "../logger.ts";
11
11
  import { transformer } from "../../mixin.ts";
12
+ import { placeMenuItemsHost } from "./menu-order.ts";
12
13
  import { Registry } from "./registry.ts";
13
14
 
14
15
  type __MenuContext = React.Context<MenuContext>;
@@ -125,14 +126,17 @@ if (
125
126
  );
126
127
 
127
128
  const ItemBoundary = createItemBoundary(R, "menu");
128
- const live: Array<{ menu: Element; root: { unmount: () => void } }> = [];
129
+ const live: Array<{ menu: Element; host: HTMLElement; root: { unmount: () => void } }> = [];
129
130
  const processed = new WeakSet<Element>();
130
131
 
131
- const sweep = () => {
132
+ const sweep = (records: MutationRecord[] = []) => {
133
+ const changedMenus = new Set(records.map((record) => record.target));
132
134
  for (let i = live.length - 1; i >= 0; i--) {
133
135
  if (!live[i].menu.isConnected) {
134
136
  live[i].root.unmount();
135
137
  live.splice(i, 1);
138
+ } else if (changedMenus.has(live[i].menu)) {
139
+ placeMenuItemsHost(live[i].menu, live[i].host);
136
140
  }
137
141
  }
138
142
  if (items.size === 0) return;
@@ -142,7 +146,7 @@ if (
142
146
  const host = document.createElement("li");
143
147
  host.setAttribute("role", "presentation");
144
148
  host.className = "spicetify-menu-items";
145
- menu.appendChild(host);
149
+ placeMenuItemsHost(menu, host);
146
150
  const root = createRoot(host);
147
151
  globalThis.__MenuContext ??= R.createContext(null);
148
152
  root.render(
@@ -152,7 +156,7 @@ if (
152
156
  ...items.all().map((item, i) => R.createElement(ItemBoundary, { key: i }, item)),
153
157
  ),
154
158
  );
155
- live.push({ menu, root });
159
+ live.push({ menu, host, root });
156
160
  }
157
161
  };
158
162
  new MutationObserver(sweep).observe(document.body, { childList: true, subtree: true });
@@ -143,10 +143,10 @@ const ensureAnchorStyle = () => {
143
143
  // constantly, and per-anchor subtree observers multiply that cost. Each
144
144
  // tick is one isConnected check per pending or placed anchor.
145
145
  type WatchedAnchor = { host: HTMLElement; place: () => boolean; started: boolean; start: () => void };
146
- const watched: WatchedAnchor[] = [];
146
+ const watched = new Set<WatchedAnchor>();
147
147
  let bodyObserver: MutationObserver | undefined;
148
148
  const watchAnchor = (anchor: WatchedAnchor) => {
149
- watched.push(anchor);
149
+ watched.add(anchor);
150
150
  bodyObserver ??= (() => {
151
151
  const observer = new MutationObserver(() => {
152
152
  for (const a of watched) {
@@ -160,6 +160,13 @@ const watchAnchor = (anchor: WatchedAnchor) => {
160
160
  observer.observe(document.body, { childList: true, subtree: true });
161
161
  return observer;
162
162
  })();
163
+ return () => {
164
+ watched.delete(anchor);
165
+ if (watched.size === 0) {
166
+ bodyObserver?.disconnect();
167
+ bodyObserver = undefined;
168
+ }
169
+ };
163
170
  };
164
171
 
165
172
  export function mountRegistryAnchor(spec: AnchorSpec): void {
@@ -192,6 +199,8 @@ export function mountRegistryAnchor(spec: AnchorSpec): void {
192
199
  const start = () => {
193
200
  const root = createRoot(host);
194
201
  const ItemBoundary = createItemBoundary(R, spec.className);
202
+ const itemKeys = new Map<unknown, number>();
203
+ let nextItemKey = 0;
195
204
  const Wrapper = () => {
196
205
  const [, refresh] = R.useReducer((n: number) => n + 1, 0);
197
206
  R.useEffect(() => {
@@ -201,12 +210,20 @@ export function mountRegistryAnchor(spec: AnchorSpec): void {
201
210
  if (spec.renderItems) {
202
211
  return R.createElement(ItemBoundary, null, spec.renderItems(spec.registry.all()));
203
212
  }
213
+ for (const item of itemKeys.keys()) {
214
+ if (!spec.registry.has(item)) itemKeys.delete(item);
215
+ }
204
216
  return R.createElement(
205
217
  R.Fragment,
206
218
  null,
207
- ...byOrder(spec.registry.all()).map((item: unknown, i: number) =>
208
- R.createElement(ItemBoundary, { key: i }, item),
209
- ),
219
+ ...byOrder(spec.registry.all()).map((item: unknown) => {
220
+ let key = itemKeys.get(item);
221
+ if (key === undefined) {
222
+ key = nextItemKey++;
223
+ itemKeys.set(item, key);
224
+ }
225
+ return R.createElement(ItemBoundary, { key }, item);
226
+ }),
210
227
  );
211
228
  };
212
229
  root.render(R.createElement(Wrapper));
@@ -240,6 +257,8 @@ export interface AdjacentSpec {
240
257
 
241
258
  export function mountAdjacent(spec: AdjacentSpec): { remove: () => void } {
242
259
  let removed = false;
260
+ let unwatch: (() => void) | undefined;
261
+ let retry: ReturnType<typeof setTimeout> | undefined;
243
262
  let root: { render?: (node: unknown) => void; unmount?: () => void } | undefined;
244
263
  const host = document.createElement("span");
245
264
  host.className = spec.className;
@@ -247,6 +266,7 @@ export function mountAdjacent(spec: AdjacentSpec): { remove: () => void } {
247
266
  host.dataset.spicetifyAnchor = "";
248
267
 
249
268
  const place = (): boolean => {
269
+ if (removed) return false;
250
270
  const target = spec.findTarget();
251
271
  if (!target?.parentElement) return false;
252
272
  const before = spec.side === "before" ? target : target.nextSibling;
@@ -255,9 +275,15 @@ export function mountAdjacent(spec: AdjacentSpec): { remove: () => void } {
255
275
  };
256
276
 
257
277
  const remove = () => {
278
+ if (removed) return;
258
279
  removed = true;
259
- root?.unmount?.();
260
- host.remove();
280
+ clearTimeout(retry);
281
+ unwatch?.();
282
+ try {
283
+ root?.unmount?.();
284
+ } finally {
285
+ host.remove();
286
+ }
261
287
  };
262
288
 
263
289
  // Transform experiments render through injected __renderX() calls, and there
@@ -291,14 +317,14 @@ export function mountAdjacent(spec: AdjacentSpec): { remove: () => void } {
291
317
  if (place()) {
292
318
  start();
293
319
  // Reuse the shared observer for re-placement only (already started).
294
- watchAnchor({ host, place, started: true, start: () => {} });
320
+ unwatch = watchAnchor({ host, place, started: true, start: () => {} });
295
321
  return;
296
322
  }
297
323
  if (Date.now() > deadline) {
298
324
  spec.onGiveUp();
299
325
  return;
300
326
  }
301
- setTimeout(attempt, 150);
327
+ retry = setTimeout(attempt, 150);
302
328
  };
303
329
  attempt();
304
330
  });
@@ -56,7 +56,7 @@ globalThis.__renderRootChildren = () =>
56
56
  return childrenRegistry.all();
57
57
  });
58
58
  globalThis.__renderRootProviders = (providers: React.ReactElement[]) => {
59
- const MultiProvider = ({ children }) => {
59
+ const MultiProvider = ({ children }: React.PropsWithChildren) => {
60
60
  const [, refresh] = React.useReducer((n) => n + 1, 0);
61
61
  if (!providersRegistry.refresh.value) {
62
62
  providersRegistry.refresh.resolve(refresh);
@@ -55,7 +55,7 @@ export function createSyncedStorage(playlistUri: string) {
55
55
  async function getUris(key: string) {
56
56
  assertSmallerSize(encodeURIComponent(key), CHUNK_SIZE, 1, "Can't fit key in a single chunk");
57
57
 
58
- const { items } = await PlaylistAPI.getContents(playlistUri, {
58
+ const { items }: { items: Array<{ uri: string }> } = await PlaylistAPI.getContents(playlistUri, {
59
59
  filter: key,
60
60
  limit: 1e9,
61
61
  });
@@ -11,6 +11,7 @@ import { Platform } from "../expose/Platform.ts";
11
11
  import { exportedFunctions, exportedMemos, modules, src } from "./index.ts";
12
12
  import { webpackRequire } from "../wpunpk.mix.ts";
13
13
  import { React } from "../expose/React.ts";
14
+ import { findPlaylistMenu } from "./playlist-menu.ts";
14
15
 
15
16
  type SnackbarProviderT = any;
16
17
 
@@ -43,16 +44,9 @@ const requireExports = (id: ReturnType<typeof findModuleId>): Record<string, any
43
44
  id === undefined ? {} : (webpackRequire(id) as Record<string, any>);
44
45
 
45
46
  const ContextMenuModuleID = findModuleId("toggleContextMenu", (s) => s.includes("toggleContextMenu"));
46
- // Dead since 1.2.9x: no module carries value:"playlist" + canView +
47
- // permissions anymore; Menus.Playlist stays undefined until re-needled.
48
- const playlistMenuModuleID = findModuleId(
49
- 'value:"playlist" + canView + permissions',
50
- (s) => s.includes('value:"playlist"') && s.includes("canView") && s.includes("permissions"),
51
- );
52
47
 
53
- Menus.Playlist = Object.values(requireExports(playlistMenuModuleID)).find(
54
- (m) => typeof m === "function" || typeof m === "object",
55
- );
48
+ Menus.Playlist = findPlaylistMenu(modules, requireExports);
49
+ if (Menus.Playlist === undefined) warn("[stdlib] webpack needle miss: playlist menu component");
56
50
 
57
51
  export const Cards: any = Object.assign(
58
52
  {
@@ -105,7 +105,7 @@ export const from: (uri: ParsableAsURI) => URIClass<any> = findAndExcludeBy("all
105
105
  export const fromString: (str: string) => URIClass<any> = findAndExcludeBy("Argument `uri` must be a string.") as any;
106
106
 
107
107
  is.PlaylistV1OrV2 = (
108
- is.Playlist && is.PlaylistV2
108
+ typeof is.Playlist === "function" && typeof is.PlaylistV2 === "function"
109
109
  ? findAndExcludeBy(`${(is.Playlist as Function).name}(e)||${(is.PlaylistV2 as Function).name}(e)`)
110
110
  : undefined
111
111
  ) as any;
@@ -3,18 +3,20 @@
3
3
  * SPDX-License-Identifier: GPL-3.0-or-later
4
4
  */
5
5
 
6
+ type TimeoutHandle = ReturnType<typeof setTimeout> | number;
7
+
6
8
  export interface CaptureReadinessOptions {
7
9
  timeoutMs: number;
8
10
  onTimeout?: () => void;
9
- scheduleTimeout?: typeof setTimeout;
10
- clearScheduledTimeout?: typeof clearTimeout;
11
+ scheduleTimeout?: (callback: () => void, delay: number) => TimeoutHandle;
12
+ clearScheduledTimeout?: (timer: TimeoutHandle) => void;
11
13
  }
12
14
 
13
15
  export function createCaptureReadiness(options: CaptureReadinessOptions) {
14
16
  const settled = Promise.withResolvers<void>();
15
17
  let released = false;
16
18
  let analyzed = false;
17
- let timer: ReturnType<typeof setTimeout> | undefined;
19
+ let timer: TimeoutHandle | undefined;
18
20
  const scheduleTimeout = options.scheduleTimeout ?? setTimeout;
19
21
  const clearScheduledTimeout = options.clearScheduledTimeout ?? clearTimeout;
20
22
  const release = () => {
@@ -64,6 +64,12 @@ export const analyzeWebpackRequire = (webpackRequire: WebpackRequire) => {
64
64
  };
65
65
  };
66
66
 
67
+ function resolveChunk(path: string): void {
68
+ const chunk = (CHUNKS[path] ??= Promise.withResolvers());
69
+ if (!chunk.resolve) throw new TypeError(`Chunk ${path} has no resolver`);
70
+ chunk.resolve(undefined);
71
+ }
72
+
67
73
  CHUNKS["/vendor~xpui.js"] ??= Promise.withResolvers();
68
74
  CHUNKS["/xpui.js"] ??= Promise.withResolvers();
69
75
  Object.assign(CHUNKS, {
@@ -84,7 +90,7 @@ postWebpackRequireHooks.push((wpr: any) => {
84
90
  load(
85
91
  url,
86
92
  (event: unknown) => {
87
- (CHUNKS[new URL(url, location.href).pathname] ??= Promise.withResolvers()).resolve(undefined);
93
+ resolveChunk(new URL(url, location.href).pathname);
88
94
  return done(event);
89
95
  },
90
96
  key,
@@ -98,7 +104,7 @@ postWebpackRequireHooks.push((wpr: any) => {
98
104
  const url = new URL(entry.name, location.href);
99
105
  if (url.pathname === "/vendor~xpui.js" || url.pathname === "/xpui.js") continue;
100
106
  if (url.origin === location.origin && url.pathname.endsWith(".js")) {
101
- (CHUNKS[url.pathname] ??= Promise.withResolvers()).resolve(undefined);
107
+ resolveChunk(url.pathname);
102
108
  }
103
109
  }
104
110
  // Capture can fire while boot is still registering modules (xpui-modules
@@ -115,8 +121,8 @@ postWebpackRequireHooks.push((wpr: any) => {
115
121
  last = count;
116
122
  if (stable >= 3 || ++ticks > 100) {
117
123
  clearInterval(settle);
118
- CHUNKS["/vendor~xpui.js"].resolve(undefined);
119
- CHUNKS["/xpui.js"].resolve(undefined);
124
+ resolveChunk("/vendor~xpui.js");
125
+ resolveChunk("/xpui.js");
120
126
  }
121
127
  }, 100);
122
128
  });
@@ -0,0 +1,54 @@
1
+ /*
2
+ * Copyright (C) 2026 Spicetify
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ function source(value: unknown): string {
7
+ return typeof value === "function" ? Function.prototype.toString.call(value) : "";
8
+ }
9
+
10
+ function hasPlaylistMenuActions(text: string): boolean {
11
+ return (
12
+ text.includes("contextmenu.share.copy-playlist-link") &&
13
+ text.includes("canView") &&
14
+ text.includes("permissions")
15
+ );
16
+ }
17
+
18
+ export function isPlaylistMenuFactory(value: unknown): boolean {
19
+ return hasPlaylistMenuActions(source(value));
20
+ }
21
+
22
+ function isMenuComponent(value: unknown): boolean {
23
+ let render = value;
24
+ if (typeof render === "object" && render !== null && "$$typeof" in render) {
25
+ if (render.$$typeof === Symbol.for("react.memo") && "type" in render) render = render.type;
26
+ else if (render.$$typeof === Symbol.for("react.forward_ref") && "render" in render) render = render.render;
27
+ }
28
+ const text = source(render);
29
+ if (/^async\b/.test(text)) return false;
30
+ if (hasPlaylistMenuActions(text)) return /\b(?:jsx|jsxs|createElement)\s*\)?\s*\(/.test(text);
31
+ // Current clients export a thin JSX wrapper around the unexported memo.
32
+ // Match the whole wrapper, not JSX nested inside a page root.
33
+ return /^\(?([$\w]+)\)?=>\(0,[$\w]+\.jsx\)\([$\w]+,\{\.\.\.\1\}\)$/.test(text.replace(/\s/g, ""));
34
+ }
35
+
36
+ /** Match the menu factory, then its renderable export; never a playlist data mapper. */
37
+ export function findPlaylistMenu(
38
+ modules: Iterable<readonly [PropertyKey, unknown]>,
39
+ requireModule: (id: PropertyKey) => unknown,
40
+ ): unknown {
41
+ const components = new Set<unknown>();
42
+ try {
43
+ for (const [id, factory] of modules) {
44
+ if (!isPlaylistMenuFactory(factory)) continue;
45
+ const exports = requireModule(id);
46
+ if (typeof exports !== "object" || exports === null) continue;
47
+ for (const value of Object.values(exports)) if (isMenuComponent(value)) components.add(value);
48
+ }
49
+ } catch {
50
+ // An unreadable client export leaves uniqueness unproven; degrade only this menu.
51
+ return undefined;
52
+ }
53
+ return components.size === 1 ? components.values().next().value : undefined;
54
+ }
@@ -42,7 +42,7 @@ export { Subject, BehaviorSubject };
42
42
 
43
43
  export const chunkLoadedSubjectPre = new Subject<unknown>();
44
44
  export const chunkLoadedSubjectPost = new Subject<unknown>();
45
- export const moduleLoadedSubject = new Subject<unknown>();
45
+ export const moduleLoadedSubject = new Subject<[string, WebpackModule]>();
46
46
 
47
47
  const pendingHooks: Array<(wpr: any) => void> = [];
48
48
  export const postWebpackRequireHooks = {
@@ -1,109 +0,0 @@
1
- {
2
- "main": {
3
- "topbar": {
4
- "wrapper": "oy96nRTKcIs7wm5K",
5
- "left": {
6
- "button": {
7
- "wrapper": "e-10810-legacy-button e-10810-button-tertiary--icon-only-medium",
8
- "icon": {
9
- "wrapper": "e-10810-icon e-10810-icon--auto-mirror"
10
- }
11
- },
12
- "button_t": {
13
- "wrapper": "e-10810-legacy-button e-10810-button-tertiary--icon-only-medium"
14
- }
15
- },
16
- "right": {
17
- "button": {
18
- "wrapper": "e-10810-legacy-button e-10810-button-tertiary--condensed-all"
19
- },
20
- "button_t": {
21
- "wrapper": "e-10810-legacy-button e-10810-button-tertiary--condensed-all"
22
- },
23
- "upgrade_button": {
24
- "wrapper": "e-10810-legacy-button e-10810-legacy-button-primary"
25
- }
26
- }
27
- },
28
- "playbar": {
29
- "widget": {},
30
- "controls": {},
31
- "buttons": {
32
- "button": {
33
- "wrapper": "e-10810-legacy-button e-10810-button-tertiary--icon-only-small",
34
- "wrapper__active": "e-10810-legacy-button e-10810-legacy-button-primary",
35
- "wrapper__indicator": "e-10810-button-primary__inner"
36
- }
37
- }
38
- },
39
- "navbar": {
40
- "link": {
41
- "wrapper": "main-globalNav-navLink",
42
- "container": "main-globalNav-navLink",
43
- "container__active": "main-globalNav-navLinkActive"
44
- }
45
- },
46
- "global_navbar": {},
47
- "sidebar": {},
48
- "panel": {}
49
- },
50
- "tracklist": {
51
- "column_header": "e-10810-legacy-list-row__header"
52
- },
53
- "search_chips": {
54
- "wrapper_wrapper": "e-10810-chip-group",
55
- "wrapper": "e-10810-chip-group__component",
56
- "container": "e-10810-chip-group__component",
57
- "chip": "e-10810-legacy-chip"
58
- },
59
- "modal": {
60
- "widget_generator": {
61
- "container": "main-embedWidgetGenerator-container",
62
- "header": {
63
- "container": "main-embedWidgetGenerator-header",
64
- "close": "main-embedWidgetGenerator-closeBtn"
65
- },
66
- "content": {
67
- "container": "main-embedWidgetGenerator-content"
68
- }
69
- },
70
- "track_credits": {
71
- "container": "main-trackCreditsModalV2-container",
72
- "header": {
73
- "container": "main-trackCreditsModalV2-header",
74
- "close": "main-trackCreditsModalV2-closeBtn"
75
- },
76
- "content": {
77
- "container": "main-trackCreditsModalV2-mainSection"
78
- }
79
- }
80
- },
81
- "settings": {
82
- "header": {
83
- "container": "fEEfljGMIEhhmWVi"
84
- },
85
- "section": {
86
- "container": "nWR7PbHjFXQJiYNL"
87
- },
88
- "button": {
89
- "wrapper": "e-10810-legacy-button e-10810-legacy-button--small e-10810-legacy-button-secondary"
90
- },
91
- "text_input": "e-10810-form-input e-10810-form-control encore-text-body-medium"
92
- },
93
- "scrollable_text": {
94
- "container": "e-10810-legacy-box e-10810-legacy-box--naked",
95
- "wrapper": "e-10810-legacy-box"
96
- },
97
- "sort_box": {
98
- "list": {
99
- "button": "x-sortBox-sortDropdown"
100
- }
101
- },
102
- "search_box": {
103
- "container": "main-globalNav-searchContainer",
104
- "expand_button": "e-10810-legacy-button e-10810-button-tertiary--icon-only-medium main-globalNav-link-icon"
105
- },
106
- "context_menu": {
107
- "menu_item": "main-contextMenu-menuItemButton"
108
- }
109
- }