@spicetify/kit 0.3.0 → 0.4.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 (37) hide show
  1. package/README.md +82 -0
  2. package/dist/args.js +15 -0
  3. package/dist/cli.js +4 -1
  4. package/dist/create.js +9 -5
  5. package/dist/dev.js +86 -22
  6. package/dist/install.js +4 -6
  7. package/dist/launch.js +24 -24
  8. package/dist/push.js +187 -44
  9. package/dist/remove.js +37 -0
  10. package/dist/stdlib-boundary.js +2 -2
  11. package/dist/version.js +1 -1
  12. package/package.json +6 -4
  13. package/vendor/classmaps/1030000/classmap-19f856aefd5.json +109 -0
  14. package/vendor/shims/platform.d.ts +1640 -3235
  15. package/vendor/stdlib/metadata.json +1 -1
  16. package/vendor/stdlib/query.ts +31 -0
  17. package/vendor/stdlib/src/client.ts +90 -1
  18. package/vendor/stdlib/src/events.mix.ts +9 -0
  19. package/vendor/stdlib/src/events.ts +1 -1
  20. package/vendor/stdlib/src/registers/menu-order.ts +17 -0
  21. package/vendor/stdlib/src/registers/menu.ts +8 -4
  22. package/vendor/stdlib/src/registers/mount.ts +35 -9
  23. package/vendor/stdlib/src/registers/navlink.tsx +3 -50
  24. package/vendor/stdlib/src/registers/playbarButton.tsx +0 -19
  25. package/vendor/stdlib/src/registers/playbarWidget.tsx +0 -19
  26. package/vendor/stdlib/src/registers/root.ts +1 -1
  27. package/vendor/stdlib/src/registers/route.ts +1 -23
  28. package/vendor/stdlib/src/registers/topbarLeftButton.tsx +0 -24
  29. package/vendor/stdlib/src/registers/topbarRightButton.tsx +0 -24
  30. package/vendor/stdlib/src/storage.ts +1 -1
  31. package/vendor/stdlib/src/webpack/ReactComponents.xpui.ts +3 -9
  32. package/vendor/stdlib/src/webpack/URI.xpui.ts +1 -1
  33. package/vendor/stdlib/src/webpack/capture-readiness.ts +5 -3
  34. package/vendor/stdlib/src/webpack/index.ts +10 -4
  35. package/vendor/stdlib/src/webpack/playlist-menu.ts +54 -0
  36. package/vendor/stdlib/src/wpunpk.mix.ts +1 -1
  37. package/vendor/classmaps/1020096/classmap-19f856aefd5.json +0 -109
package/dist/push.js CHANGED
@@ -92,15 +92,140 @@ export function stampRecord(rec, id, nonce) {
92
92
  export function newNonce() {
93
93
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
94
94
  }
95
+ export const DEFAULT_PORT = "9229";
96
+ // resolvePort picks the CDP port: an explicit --port, then SPICETIFY_CDP_PORT
97
+ // (shared with the monorepo's maintainer scripts), then the default.
98
+ export function resolvePort(flag, env = process.env) {
99
+ return flag ?? env.SPICETIFY_CDP_PORT ?? DEFAULT_PORT;
100
+ }
101
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
102
+ // classifyDevtools decides who owns a DevTools endpoint from its /json/version
103
+ // and /json/list bodies. Spotify's CEF user agent carries "Spotify/<version>";
104
+ // anything else (the Node inspector, wrangler, a browser) is a foreign listener
105
+ // that would make Spotify fail to bind the port.
106
+ export function classifyDevtools(version, targets) {
107
+ if (!/Spotify\//.test(String(version["User-Agent"] ?? ""))) {
108
+ const first = targets[0];
109
+ const what = String(version.Browser || first?.title || first?.type || "an unknown DevTools endpoint");
110
+ return { state: "other", what };
111
+ }
112
+ for (const t of targets) {
113
+ if (typeof t.url === "string" && t.url.includes("xpui") && typeof t.webSocketDebuggerUrl === "string") {
114
+ return { state: "xpui", ws: t.webSocketDebuggerUrl };
115
+ }
116
+ }
117
+ return { state: "spotify" };
118
+ }
119
+ // probePort reports whether the port is free, held by Spotify (with or without
120
+ // its xpui page loaded yet), held by something else, or answering in a way it
121
+ // cannot identify (a non-HTTP listener, or a client still binding the port).
122
+ export async function probePort(port) {
123
+ let version;
124
+ try {
125
+ const res = await fetch(`http://localhost:${port}/json/version`, { signal: AbortSignal.timeout(2_000) });
126
+ if (!res.ok)
127
+ return { state: "unknown" };
128
+ const body = await res.json();
129
+ version = isRecord(body) ? body : {};
130
+ }
131
+ catch (e) {
132
+ const code = e.cause?.code;
133
+ return code === "ECONNREFUSED" ? { state: "free" } : { state: "unknown" };
134
+ }
135
+ let targets = [];
136
+ try {
137
+ const res = await fetch(`http://localhost:${port}/json/list`, { signal: AbortSignal.timeout(2_000) });
138
+ const body = res.ok ? await res.json() : [];
139
+ if (Array.isArray(body))
140
+ targets = body.filter(isRecord);
141
+ }
142
+ catch {
143
+ /* version answered; an unreadable list classifies from the version alone */
144
+ }
145
+ return classifyDevtools(version, targets);
146
+ }
147
+ export function portInUseMessage(port, what) {
148
+ return (`port ${port} is already in use by ${what}, not Spotify. ` +
149
+ "Node's inspector (node --inspect, wrangler dev) defaults to 9229 too. " +
150
+ "Stop that process, or pick another port with --port <n> or SPICETIFY_CDP_PORT=<n>.");
151
+ }
95
152
  export async function wsUrl(port) {
96
- const res = await fetch(`http://localhost:${port}/json/list`);
97
- const targets = (await res.json());
98
- const target = targets.find((t) => t.url?.includes("xpui"));
99
- if (!target?.webSocketDebuggerUrl) {
100
- throw new Error(`no xpui debug target on port ${port}. Start Spotify with --remote-debugging-port=${port} ` +
101
- "(or pass --launch), and ensure `spicetify apply` has staged the v3 loader.");
153
+ const probe = await probePort(port);
154
+ if (probe.state === "xpui")
155
+ return probe.ws;
156
+ if (probe.state === "other")
157
+ throw new Error(portInUseMessage(port, probe.what));
158
+ if (probe.state === "spotify") {
159
+ throw new Error(`Spotify is listening on port ${port} but has no xpui page yet. ` +
160
+ "Wait for it to finish starting, and ensure `spicetify apply` has staged the v3 loader.");
102
161
  }
103
- return target.webSocketDebuggerUrl;
162
+ if (probe.state === "unknown") {
163
+ throw new Error(`port ${port} answered, but not as a Spotify DevTools endpoint; retry once Spotify has started.`);
164
+ }
165
+ throw new Error(`no Spotify listening on port ${port}. Start it with --remote-debugging-port=${port}, ` +
166
+ "or run `spicetify-kit dev`, which starts it for you.");
167
+ }
168
+ // A freshly launched client exposes its xpui target before the loader has
169
+ // booted, so client-side code waits up to 10s for Spicetify.Modules.
170
+ const AWAIT_LOADER = `let M = globalThis.Spicetify?.Modules;
171
+ for (let i = 0; !M && i < 100; i++) {
172
+ await new Promise((r) => setTimeout(r, 100));
173
+ M = globalThis.Spicetify?.Modules;
174
+ }`;
175
+ // evaluate runs an expression in the xpui page over CDP and resolves with its
176
+ // string value.
177
+ export function evaluate(port, expr, timeoutMs = 15_000) {
178
+ return new Promise((resolve, reject) => {
179
+ void wsUrl(port).then((url) => {
180
+ let ws;
181
+ try {
182
+ ws = new WebSocket(url);
183
+ }
184
+ catch (e) {
185
+ reject(new Error(`could not open the client websocket: ${e.message}`));
186
+ return;
187
+ }
188
+ const timer = setTimeout(() => {
189
+ ws.close();
190
+ reject(new Error("client evaluation timed out"));
191
+ }, timeoutMs);
192
+ ws.addEventListener("error", (e) => {
193
+ clearTimeout(timer);
194
+ reject(new Error(`websocket error: ${String(e.message ?? e)}`));
195
+ });
196
+ // A settled promise ignores this; it only fires for a socket the client
197
+ // closed before answering (for instance Spotify quitting mid-push).
198
+ ws.addEventListener("close", () => {
199
+ clearTimeout(timer);
200
+ reject(new Error("the client closed the connection before answering"));
201
+ });
202
+ ws.addEventListener("open", () => {
203
+ ws.send(JSON.stringify({
204
+ id: 1,
205
+ method: "Runtime.evaluate",
206
+ params: { expression: expr, awaitPromise: true, returnByValue: true },
207
+ }));
208
+ });
209
+ ws.addEventListener("message", (ev) => {
210
+ let msg;
211
+ try {
212
+ msg = JSON.parse(String(ev.data));
213
+ }
214
+ catch {
215
+ return;
216
+ }
217
+ if (msg.id !== 1)
218
+ return;
219
+ clearTimeout(timer);
220
+ ws.close();
221
+ const outcome = interpretResult(msg);
222
+ if ("error" in outcome)
223
+ reject(new Error(outcome.error));
224
+ else
225
+ resolve(outcome.value);
226
+ });
227
+ }, reject);
228
+ });
104
229
  }
105
230
  export function push(rec, id, port) {
106
231
  // Stamp before the quota check so the stamp's own bytes are counted.
@@ -112,14 +237,18 @@ export function push(rec, id, port) {
112
237
  // The whole exchange runs in the client: disable the old instance, install
113
238
  // the fresh content, and re-enable anything the unload cascade took down.
114
239
  const expr = `(async () => {
115
- const M = globalThis.Spicetify?.Modules;
240
+ ${AWAIT_LOADER}
116
241
  if (!M) return JSON.stringify({ error: "loader not ready" });
117
242
  const rec = ${JSON.stringify(rec)};
118
243
  const id = ${JSON.stringify(id)};
119
244
  const before = M.list().filter((m) => m.loaded).map((m) => m.identifier);
120
245
  const hadPrevious = before.includes(id);
121
- await M.disable(id).catch(() => {});
122
- await M.installLocal(id, rec);
246
+ const installed = await M.installLocal(id, rec);
247
+ // installLocal keeps a module the client has marked disabled off. Pushing
248
+ // it is an explicit request to run it, so load it for this session;
249
+ // reload is transient, so the persisted choice still applies after removal.
250
+ const reenabled = installed?.disabled === true;
251
+ if (reenabled) await (M.reload ?? M.enable)(id);
123
252
  // Re-enabling a theme the loader just unloaded would fight the
124
253
  // single-active-theme invariant and knock the pushed theme back off.
125
254
  const pushedIsTheme = (rec.metadata.tags ?? []).includes("theme");
@@ -136,40 +265,54 @@ export function push(rec, id, port) {
136
265
  failed: M.report?.failed?.[id] ?? null,
137
266
  stamp: ${stamped ? '(stampLive ? "live" : "stale")' : '"unstamped"'},
138
267
  hadPrevious,
268
+ reenabled,
139
269
  });
140
270
  })()`;
141
- return new Promise((resolve, reject) => {
142
- void wsUrl(port).then((url) => {
143
- const ws = new WebSocket(url);
144
- const timer = setTimeout(() => {
145
- ws.close();
146
- reject(new Error("push timed out"));
147
- }, 15_000);
148
- ws.addEventListener("error", (e) => {
149
- clearTimeout(timer);
150
- reject(new Error(`websocket error: ${String(e.message ?? e)}`));
151
- });
152
- ws.addEventListener("open", () => {
153
- ws.send(JSON.stringify({
154
- id: 1,
155
- method: "Runtime.evaluate",
156
- params: { expression: expr, awaitPromise: true, returnByValue: true },
157
- }));
158
- });
159
- ws.addEventListener("message", (ev) => {
160
- const msg = JSON.parse(String(ev.data));
161
- if (msg.id !== 1)
162
- return;
163
- clearTimeout(timer);
164
- ws.close();
165
- const outcome = interpretResult(msg);
166
- if ("error" in outcome)
167
- reject(new Error(outcome.error));
168
- else
169
- resolve(outcome.value);
170
- });
171
- }, reject);
172
- });
271
+ return evaluate(port, expr);
272
+ }
273
+ // removeLocal drops a hot-pushed override so the client falls back to the
274
+ // staged or store-installed copy.
275
+ export async function removeLocal(id, port) {
276
+ const raw = await evaluate(port, `(async () => {
277
+ ${AWAIT_LOADER}
278
+ if (!M?.removeLocal) return JSON.stringify({ error: "loader not ready" });
279
+ const id = ${JSON.stringify(id)};
280
+ const had = M.list().some((m) => m.identifier === id && m.local) ||
281
+ (M.listLocal?.() ?? []).some((r) => r?.metadata?.identifier === id);
282
+ const out = await M.removeLocal(id);
283
+ return JSON.stringify({ had, out: out ?? null });
284
+ })()`);
285
+ let parsed;
286
+ try {
287
+ parsed = JSON.parse(raw);
288
+ }
289
+ catch {
290
+ parsed = null;
291
+ }
292
+ if (isRecord(parsed) && parsed.error) {
293
+ throw new Error("the v3 loader is not staged in this client (Spicetify.Modules is absent)");
294
+ }
295
+ if (!isRecord(parsed) || typeof parsed.had !== "boolean") {
296
+ throw new Error(`unexpected removeLocal result: ${raw.slice(0, 200)}`);
297
+ }
298
+ const out = isRecord(parsed.out) ? parsed.out : {};
299
+ if (out.requiresRestart === true)
300
+ return { kind: "requires-restart" };
301
+ if (typeof out.revertedTo === "string")
302
+ return { kind: "reverted", version: out.revertedTo };
303
+ return parsed.had ? { kind: "removed" } : { kind: "none" };
304
+ }
305
+ export function formatRemoveOutcome(id, outcome) {
306
+ switch (outcome.kind) {
307
+ case "none":
308
+ return `${id} has no local override to remove`;
309
+ case "removed":
310
+ return `removed the local override of ${id} (no staged copy, so it is no longer installed)`;
311
+ case "reverted":
312
+ return `removed the local override of ${id}; the client is back on the installed ${outcome.version}`;
313
+ case "requires-restart":
314
+ return `removed the local override of ${id}; restart Spotify to finish reverting it`;
315
+ }
173
316
  }
174
317
  // Turn the raw client-side push result into an honest, actionable line.
175
318
  export function formatPushResult(raw) {
@@ -199,9 +342,9 @@ export function formatPushResult(raw) {
199
342
  "Restart the client (or removeLocal, then push again) before trusting any verification.",
200
343
  };
201
344
  }
202
- const remount = parsed.hadPrevious
345
+ const remount = (parsed.hadPrevious
203
346
  ? " — UI mounted before the push may still be the old build; re-navigate to its surface to remount"
204
- : "";
347
+ : "") + (parsed.reenabled ? " (it was disabled in the client; the push turned it back on)" : "");
205
348
  if (parsed.stamp === "live")
206
349
  return { ok: true, message: `loaded, pushed build verified executing${remount}` };
207
350
  // css-only records carry no executable entry to stamp.
package/dist/remove.js ADDED
@@ -0,0 +1,37 @@
1
+ /*
2
+ * Copyright (C) 2026 Afonso Jorge Ramos
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+ /**
6
+ * remove - drop a hot-pushed local override from a running client, so it
7
+ * falls back to the staged or store-installed copy. Takes a module directory
8
+ * (its metadata.json names the id) or the id itself.
9
+ */
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import path from "node:path";
12
+ import { flagValue } from "./args.js";
13
+ import { formatRemoveOutcome, removeLocal, resolvePort } from "./push.js";
14
+ const USAGE = "spicetify-kit remove <module-dir|id> [--port 9229]";
15
+ // moduleId reads the id from <target>/metadata.json when target is a module
16
+ // directory, and otherwise treats target as the id.
17
+ export function moduleId(target, cwd) {
18
+ const meta = path.join(path.resolve(cwd, target), "metadata.json");
19
+ if (!existsSync(meta))
20
+ return target;
21
+ const name = JSON.parse(readFileSync(meta, "utf8")).name;
22
+ if (typeof name !== "string" || !name)
23
+ throw new Error(`${meta} has no "name" to identify the module by`);
24
+ return name;
25
+ }
26
+ export async function runRemove(argv, cwd = process.cwd()) {
27
+ const target = argv.find((a, i) => !a.startsWith("--") && argv[i - 1] !== "--port");
28
+ if (!target || argv.includes("--help") || argv.includes("-h")) {
29
+ console.log(USAGE);
30
+ if (!target)
31
+ process.exitCode = 1;
32
+ return;
33
+ }
34
+ const port = resolvePort(flagValue(argv, "port"));
35
+ const id = moduleId(target, cwd);
36
+ console.log(`[remove] ${formatRemoveOutcome(id, await removeLocal(id, port))}`);
37
+ }
@@ -7,7 +7,7 @@ import path from "node:path";
7
7
  export const STDLIB_BOUNDARY_WARNING_RULES = ["ambient-client", "client-dom", "direct-map"];
8
8
  const SOURCE_EXTENSIONS = new Set([".js", ".jsx", ".mjs", ".ts", ".tsx"]);
9
9
  const STYLE_EXTENSIONS = new Set([".css", ".scss"]);
10
- const PUBLIC_STDLIB_IMPORT = /^\/modules\/stdlib\/(?:mod\.(?:js|ts)|lib\/primitives(?:-classes|-vanilla)?\.(?:js|ts|tsx))$/;
10
+ const PUBLIC_STDLIB_IMPORT = /^\/modules\/stdlib\/(?:(?:mod|query)\.(?:js|ts)|lib\/primitives(?:-classes|-vanilla)?\.(?:js|ts|tsx))$/;
11
11
  const CLIENT_DOM_PATTERN = /(?:^|[^A-Za-z0-9_-])(?:Root__|main-[A-Za-z]|player-controls__|playback-progressbar|npv-)|\[data-testid(?:=|\])/;
12
12
  const posix = (value) => value.split(path.sep).join("/");
13
13
  const lineAt = (text, index) => text.slice(0, index).split("\n").length;
@@ -98,7 +98,7 @@ export function inspectStdlibBoundaryFile(file, text) {
98
98
  file: normalizedFile,
99
99
  line: lineAt(source, match.index),
100
100
  rule: "private-stdlib-import",
101
- detail: `${match[1]} is private; import through /modules/stdlib/mod.js or the primitives kit`,
101
+ detail: `${match[1]} is private; import through /modules/stdlib/mod.js, query.js, or the primitives kit`,
102
102
  });
103
103
  }
104
104
  const ambient = firstMatch(source, /\bSpicetify\s*(?:\?\.|\.)|\b(?:globalThis|window)\s*\.\s*Spicetify\b/);
package/dist/version.js CHANGED
@@ -4,5 +4,5 @@
4
4
  */
5
5
  // Kept in lockstep with package.json by release-please. Scaffolds must depend
6
6
  // on the kit version that generated them, especially across pre-1.0 minors.
7
- export const KIT_VERSION = "0.3.0"; // x-release-please-version
7
+ export const KIT_VERSION = "0.4.0"; // x-release-please-version
8
8
  export const KIT_DEPENDENCY_RANGE = `^${KIT_VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spicetify/kit",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Developer kit for spicetify v3 modules: scaffold, build, hot-push, and pack",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "homepage": "https://github.com/spicetify/modules#readme",
@@ -25,7 +25,8 @@
25
25
  "./from-theme": "./dist/from-theme.js",
26
26
  "./check": "./dist/check.js",
27
27
  "./vault": "./dist/vault.js",
28
- "./install": "./dist/install.js"
28
+ "./install": "./dist/install.js",
29
+ "./remove": "./dist/remove.js"
29
30
  },
30
31
  "files": [
31
32
  "bin",
@@ -39,7 +40,7 @@
39
40
  "access": "public"
40
41
  },
41
42
  "spicetify": {
42
- "stdlibVersion": "1.10.1"
43
+ "stdlibVersion": "1.13.0"
43
44
  },
44
45
  "scripts": {
45
46
  "build": "tsc -p tsconfig.build.json",
@@ -48,6 +49,7 @@
48
49
  },
49
50
  "dependencies": {
50
51
  "rolldown": "^1.2.0",
51
- "sass-embedded": "^1.80.0"
52
+ "sass-embedded": "^1.80.0",
53
+ "@tanstack/query-core": "5.103.0"
52
54
  }
53
55
  }
@@ -0,0 +1,109 @@
1
+ {
2
+ "main": {
3
+ "topbar": {
4
+ "wrapper": "bQetA_KiP9n0DQVcGa7m",
5
+ "left": {
6
+ "button": {
7
+ "wrapper": "e-10860-legacy-button e-10860-button-tertiary--icon-only-medium",
8
+ "icon": {
9
+ "wrapper": "e-10860-icon e-10860-icon--auto-mirror"
10
+ }
11
+ },
12
+ "button_t": {
13
+ "wrapper": "e-10860-legacy-button e-10860-button-tertiary--icon-only-medium"
14
+ }
15
+ },
16
+ "right": {
17
+ "button": {
18
+ "wrapper": "e-10860-legacy-button e-10860-button-tertiary--condensed-all"
19
+ },
20
+ "button_t": {
21
+ "wrapper": "nR3ekhH3c1IU7dX9smcR"
22
+ },
23
+ "upgrade_button": {
24
+ "wrapper": "e-10860-legacy-button e-10860-legacy-button-primary"
25
+ }
26
+ }
27
+ },
28
+ "playbar": {
29
+ "widget": {},
30
+ "controls": {},
31
+ "buttons": {
32
+ "button": {
33
+ "wrapper": "e-10860-legacy-button e-10860-button-tertiary--icon-only-small",
34
+ "wrapper__active": "e-10860-legacy-button e-10860-legacy-button-primary",
35
+ "wrapper__indicator": "e-10860-button-primary__inner"
36
+ }
37
+ }
38
+ },
39
+ "navbar": {
40
+ "link": {
41
+ "wrapper": "xlH6FJnBzFT4l3TgUBre",
42
+ "container": "xlH6FJnBzFT4l3TgUBre",
43
+ "container__active": "ajKYUZPcNmFVzcAzzlwe"
44
+ }
45
+ },
46
+ "global_navbar": {},
47
+ "sidebar": {},
48
+ "panel": {}
49
+ },
50
+ "tracklist": {
51
+ "column_header": "e-10860-legacy-list-row__header"
52
+ },
53
+ "search_chips": {
54
+ "wrapper_wrapper": "e-10860-chip-group",
55
+ "wrapper": "e-10860-chip-group__component",
56
+ "container": "e-10860-chip-group__component",
57
+ "chip": "e-10860-legacy-chip"
58
+ },
59
+ "modal": {
60
+ "widget_generator": {
61
+ "container": "_XeAt3IgtZQepOYqU2g4",
62
+ "header": {
63
+ "container": "mnU1hqlaF5JIcrTxLnZF",
64
+ "close": "AslPcD0TRZBBGZofe58g"
65
+ },
66
+ "content": {
67
+ "container": "Xu_Z4l6TIOF1f5kDB9dt"
68
+ }
69
+ },
70
+ "track_credits": {
71
+ "container": "gtXQasDkhtg93wGk6yNG",
72
+ "header": {
73
+ "container": "nQ_c7yoZ2XOD6fWtmv18",
74
+ "close": "CaHvWv36mAu4x5QTKlb1"
75
+ },
76
+ "content": {
77
+ "container": "wYQNBdH2rNMGe_B4oumf"
78
+ }
79
+ }
80
+ },
81
+ "settings": {
82
+ "header": {
83
+ "container": "Hwax_pfCzrntLhnYXiqL"
84
+ },
85
+ "section": {
86
+ "container": "HdwEUcX8xijMdU7doYPt"
87
+ },
88
+ "button": {
89
+ "wrapper": "e-10860-legacy-button e-10860-legacy-button--small e-10860-legacy-button-secondary"
90
+ },
91
+ "text_input": "e-10860-form-input e-10860-form-control encore-text-body-medium"
92
+ },
93
+ "scrollable_text": {
94
+ "container": "e-10860-legacy-box e-10860-legacy-box--naked",
95
+ "wrapper": "e-10860-legacy-box"
96
+ },
97
+ "sort_box": {
98
+ "list": {
99
+ "button": "MYWr4UY4pKAAscUXvqrB"
100
+ }
101
+ },
102
+ "search_box": {
103
+ "container": "mlx0CgXAnGz0dADVsCdS",
104
+ "expand_button": "e-10860-legacy-button e-10860-button-tertiary--icon-only-medium aslJk0YypwKvyKXaBJcG"
105
+ },
106
+ "context_menu": {
107
+ "menu_item": "fmmygDM45MTS7I35OyLh"
108
+ }
109
+ }