@spicetify/kit 0.1.0 → 0.2.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.
@@ -0,0 +1,371 @@
1
+ /*
2
+ * Copyright (C) 2026 Afonso Jorge Ramos
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ export interface PanelWidth {
7
+ default?: number;
8
+ min?: number;
9
+ max?: number;
10
+ }
11
+
12
+ export interface PanelRegistration<Content = unknown> {
13
+ id: string;
14
+ label: string;
15
+ render: () => Content;
16
+ width?: PanelWidth;
17
+ onOpen?: () => void;
18
+ onClose?: () => void;
19
+ }
20
+
21
+ export interface PanelController {
22
+ open(): void;
23
+ close(): void;
24
+ toggle(): void;
25
+ isOpen(): boolean;
26
+ subscribe(listener: (open: boolean) => void): () => void;
27
+ dispose(): void;
28
+ }
29
+
30
+ export interface PanelCoordinatorOptions<Content = unknown> {
31
+ document: Document;
32
+ window: Window;
33
+ mount(host: HTMLElement, panel: PanelRegistration<Content>, close: () => void): () => void;
34
+ }
35
+
36
+ export interface PanelCoordinator<Content = unknown> {
37
+ register(panel: PanelRegistration<Content>): PanelController;
38
+ dispose(): void;
39
+ }
40
+
41
+ type RegistrationState<Content> = {
42
+ panel: PanelRegistration<Content>;
43
+ disposed: boolean;
44
+ listeners: Set<(open: boolean) => void>;
45
+ };
46
+
47
+ type NativeSnapshot = { hidden: string | null; inert: string | null };
48
+ type StyleSnapshot = { value: string; priority: string };
49
+
50
+ const WIDTH_DEFAULT = 360;
51
+ const WIDTH_MIN = 280;
52
+ const WIDTH_MAX = 520;
53
+
54
+ const panelWidth = ({ width }: PanelRegistration): number => {
55
+ const min = width?.min ?? WIDTH_MIN;
56
+ const max = Math.max(min, width?.max ?? WIDTH_MAX);
57
+ return Math.min(max, Math.max(min, width?.default ?? WIDTH_DEFAULT));
58
+ };
59
+
60
+ const validatePanelWidth = (width: PanelWidth | undefined) => {
61
+ if (!width) return;
62
+ for (const key of ["default", "min", "max"] as const) {
63
+ const value = width[key];
64
+ if (value !== undefined && (!Number.isFinite(value) || value < 0)) {
65
+ throw new Error(`[stdlib] panel width ${key} must be a finite non-negative number`);
66
+ }
67
+ }
68
+ };
69
+
70
+ const gridTracks = (template: string): string[] => {
71
+ const tracks: string[] = [];
72
+ let token = "";
73
+ let parentheses = 0;
74
+ let brackets = 0;
75
+ for (const character of template.trim()) {
76
+ if (/\s/.test(character) && parentheses === 0 && brackets === 0) {
77
+ if (token) tracks.push(token);
78
+ token = "";
79
+ continue;
80
+ }
81
+ token += character;
82
+ if (character === "(") parentheses += 1;
83
+ else if (character === ")") parentheses -= 1;
84
+ else if (character === "[") brackets += 1;
85
+ else if (character === "]") brackets -= 1;
86
+ }
87
+ if (token) tracks.push(token);
88
+ return tracks.filter((track) => !(track.startsWith("[") && track.endsWith("]")));
89
+ };
90
+
91
+ export function createPanelCoordinator<Content = unknown>(options: PanelCoordinatorOptions<Content>) {
92
+ const registrations = new Map<string, RegistrationState<Content>>();
93
+ const suspended = new Map<HTMLElement, NativeSnapshot>();
94
+ const ownedStyles = new Map<HTMLElement, Map<string, StyleSnapshot>>();
95
+ let active: RegistrationState<Content> | undefined;
96
+ let activeHost: HTMLElement | undefined;
97
+ let activeSidebar: HTMLElement | undefined;
98
+ let activeTopContainer: HTMLElement | undefined;
99
+ let unmount: (() => void) | undefined;
100
+ let shellObserver: MutationObserver | undefined;
101
+ let sidebarObserver: MutationObserver | undefined;
102
+ let returnFocus: HTMLElement | undefined;
103
+ let disposed = false;
104
+ let transitioning = false;
105
+ const transitions: Array<() => void> = [];
106
+ const Observer = (options.window as unknown as { MutationObserver: typeof MutationObserver }).MutationObserver;
107
+
108
+ const safely = (label: string, callback: (() => void) | undefined) => {
109
+ if (!callback) return;
110
+ try {
111
+ callback();
112
+ } catch (error) {
113
+ console.error(`[stdlib] panel ${label} failed:`, error);
114
+ }
115
+ };
116
+
117
+ const notify = (state: RegistrationState<Content>, open: boolean) => {
118
+ for (const listener of state.listeners) safely("subscriber", () => listener(open));
119
+ };
120
+
121
+ const transition = (action: () => void) => {
122
+ transitions.push(action);
123
+ if (transitioning) return;
124
+ transitioning = true;
125
+ let firstError: unknown;
126
+ try {
127
+ let next: (() => void) | undefined;
128
+ while ((next = transitions.shift())) {
129
+ try {
130
+ next();
131
+ } catch (error) {
132
+ firstError ??= error;
133
+ }
134
+ }
135
+ } finally {
136
+ transitioning = false;
137
+ }
138
+ if (firstError) throw firstError;
139
+ };
140
+
141
+ const restoreNative = () => {
142
+ for (const [element, snapshot] of suspended) {
143
+ for (const attribute of ["hidden", "inert"] as const) {
144
+ const value = snapshot[attribute];
145
+ if (value === null) element.removeAttribute(attribute);
146
+ else element.setAttribute(attribute, value);
147
+ }
148
+ }
149
+ suspended.clear();
150
+ };
151
+
152
+ const setOwnedStyle = (element: HTMLElement, property: string, value: string) => {
153
+ let snapshots = ownedStyles.get(element);
154
+ if (!snapshots) {
155
+ snapshots = new Map();
156
+ ownedStyles.set(element, snapshots);
157
+ }
158
+ if (!snapshots.has(property)) {
159
+ snapshots.set(property, {
160
+ value: element.style.getPropertyValue(property),
161
+ priority: element.style.getPropertyPriority(property),
162
+ });
163
+ }
164
+ element.style.setProperty(property, value, "important");
165
+ };
166
+
167
+ const restoreOwnedStyles = (element: HTMLElement | undefined) => {
168
+ if (!element) return;
169
+ const snapshots = ownedStyles.get(element);
170
+ if (!snapshots) return;
171
+ for (const [property, snapshot] of snapshots) {
172
+ if (snapshot.value) element.style.setProperty(property, snapshot.value, snapshot.priority);
173
+ else element.style.removeProperty(property);
174
+ }
175
+ ownedStyles.delete(element);
176
+ };
177
+
178
+ const suspendNative = (sidebar: HTMLElement) => {
179
+ for (const child of sidebar.children) {
180
+ if (!(child instanceof HTMLElement) || child === activeHost) continue;
181
+ if (!suspended.has(child)) {
182
+ suspended.set(child, { hidden: child.getAttribute("hidden"), inert: child.getAttribute("inert") });
183
+ }
184
+ child.setAttribute("hidden", "");
185
+ child.setAttribute("inert", "");
186
+ }
187
+ };
188
+
189
+ const observeSidebar = (sidebar: HTMLElement) => {
190
+ sidebarObserver?.disconnect();
191
+ sidebarObserver = new Observer(() => suspendNative(sidebar));
192
+ sidebarObserver.observe(sidebar, { childList: true });
193
+ };
194
+
195
+ const clearLayout = () => {
196
+ activeTopContainer?.removeAttribute("data-spicetify-panel-active");
197
+ restoreOwnedStyles(activeTopContainer);
198
+ restoreOwnedStyles(activeSidebar);
199
+ activeTopContainer = undefined;
200
+ activeSidebar = undefined;
201
+ };
202
+
203
+ const placeActiveHost = (): boolean => {
204
+ if (!active || !activeHost) return false;
205
+ const sidebar = options.document.querySelector<HTMLElement>(".Root__right-sidebar");
206
+ const topContainer = options.document.querySelector<HTMLElement>(".Root__top-container");
207
+ if (!sidebar || !topContainer) return false;
208
+
209
+ if (activeSidebar !== sidebar) {
210
+ restoreNative();
211
+ restoreOwnedStyles(activeSidebar);
212
+ activeSidebar = sidebar;
213
+ observeSidebar(sidebar);
214
+ }
215
+ if (activeHost.parentElement !== sidebar) sidebar.prepend(activeHost);
216
+ if (activeTopContainer !== topContainer) {
217
+ activeTopContainer?.removeAttribute("data-spicetify-panel-active");
218
+ restoreOwnedStyles(activeTopContainer);
219
+ activeTopContainer = topContainer;
220
+ }
221
+ topContainer.dataset.spicetifyPanelActive = "";
222
+ const trailingTracks = gridTracks(options.window.getComputedStyle(topContainer).gridTemplateColumns).slice(3);
223
+ const trailingWidth = trailingTracks.reduce(
224
+ (total, track) => total + (track.endsWith("px") ? Number.parseFloat(track) || 0 : 0),
225
+ 0,
226
+ );
227
+ setOwnedStyle(topContainer, "--spicetify-panel-requested-width", `${panelWidth(active.panel)}px`);
228
+ setOwnedStyle(topContainer, "--spicetify-panel-trailing-width", `${trailingWidth}px`);
229
+ setOwnedStyle(
230
+ topContainer,
231
+ "grid-template-columns",
232
+ ["auto", "minmax(0, 1fr)", "var(--spicetify-panel-width)", ...trailingTracks].join(" "),
233
+ );
234
+ setOwnedStyle(sidebar, "width", "var(--spicetify-panel-width)");
235
+ setOwnedStyle(sidebar, "min-width", "var(--spicetify-panel-width)");
236
+ suspendNative(sidebar);
237
+ return true;
238
+ };
239
+
240
+ const closeActive = (restoreFocus: boolean) => {
241
+ const closing = active;
242
+ if (!closing) return;
243
+ active = undefined;
244
+ shellObserver?.disconnect();
245
+ shellObserver = undefined;
246
+ sidebarObserver?.disconnect();
247
+ sidebarObserver = undefined;
248
+ safely("unmount", unmount);
249
+ unmount = undefined;
250
+ activeHost?.remove();
251
+ activeHost = undefined;
252
+ restoreNative();
253
+ clearLayout();
254
+ safely("onClose", closing.panel.onClose);
255
+ notify(closing, false);
256
+ if (restoreFocus) {
257
+ if (returnFocus?.isConnected) returnFocus.focus();
258
+ returnFocus = undefined;
259
+ }
260
+ };
261
+
262
+ const open = (state: RegistrationState<Content>) => {
263
+ if (disposed || state.disposed) return;
264
+ if (active === state) return;
265
+ const replacing = !!active;
266
+ if (replacing) closeActive(false);
267
+ const focused = options.document.activeElement;
268
+ if (!replacing) returnFocus = focused instanceof HTMLElement ? focused : undefined;
269
+ active = state;
270
+ activeHost = options.document.createElement("section");
271
+ activeHost.className = "spicetify-panel-host";
272
+ activeHost.dataset.panelId = state.panel.id;
273
+ activeHost.setAttribute("role", "complementary");
274
+ activeHost.setAttribute("aria-label", state.panel.label);
275
+ activeHost.tabIndex = -1;
276
+ try {
277
+ unmount = options.mount(activeHost, state.panel, () => transition(() => closeActive(true)));
278
+ } catch (error) {
279
+ active = undefined;
280
+ activeHost.remove();
281
+ activeHost = undefined;
282
+ if (returnFocus?.isConnected) returnFocus.focus();
283
+ returnFocus = undefined;
284
+ throw error;
285
+ }
286
+ placeActiveHost();
287
+ shellObserver = new Observer(() => {
288
+ if (activeHost?.isConnected && activeSidebar?.isConnected && activeTopContainer?.isConnected) return;
289
+ placeActiveHost();
290
+ });
291
+ if (options.document.body) shellObserver.observe(options.document.body, { childList: true, subtree: true });
292
+ safely("onOpen", state.panel.onOpen);
293
+ notify(state, true);
294
+ activeHost.focus();
295
+ };
296
+
297
+ const onKeyDown = (event: KeyboardEvent) => {
298
+ if (event.key !== "Escape" || event.defaultPrevented || !active) return;
299
+ if (
300
+ options.document.querySelector(
301
+ ".spicetify-popover, .spicetify-scrim, .spicetify-root-children [role='dialog']",
302
+ )
303
+ )
304
+ return;
305
+ event.preventDefault();
306
+ transition(() => closeActive(true));
307
+ };
308
+ options.document.addEventListener("keydown", onKeyDown);
309
+
310
+ const register = (panel: PanelRegistration<Content>): PanelController => {
311
+ if (disposed) throw new Error("[stdlib] panel coordinator is disposed");
312
+ if (!panel.id.trim()) throw new Error("[stdlib] panel id must not be empty");
313
+ if (!panel.label.trim()) throw new Error("[stdlib] panel label must not be empty");
314
+ validatePanelWidth(panel.width);
315
+ if (registrations.has(panel.id)) throw new Error(`[stdlib] duplicate panel id: ${panel.id}`);
316
+ const state: RegistrationState<Content> = { panel, disposed: false, listeners: new Set() };
317
+ registrations.set(panel.id, state);
318
+
319
+ return {
320
+ open: () => {
321
+ if (disposed || state.disposed) throw new Error(`[stdlib] panel "${state.panel.id}" is disposed`);
322
+ transition(() => open(state));
323
+ },
324
+ close: () => {
325
+ transition(() => {
326
+ if (active === state) closeActive(true);
327
+ });
328
+ },
329
+ toggle: () => {
330
+ if (disposed || state.disposed) throw new Error(`[stdlib] panel "${state.panel.id}" is disposed`);
331
+ transition(() => {
332
+ if (active === state) closeActive(true);
333
+ else open(state);
334
+ });
335
+ },
336
+ isOpen: () => active === state,
337
+ subscribe(listener) {
338
+ if (state.disposed) throw new Error(`[stdlib] panel "${state.panel.id}" is disposed`);
339
+ state.listeners.add(listener);
340
+ safely("subscriber", () => listener(active === state));
341
+ return () => state.listeners.delete(listener);
342
+ },
343
+ dispose() {
344
+ if (state.disposed) return;
345
+ state.disposed = true;
346
+ transition(() => {
347
+ if (active === state) closeActive(true);
348
+ state.listeners.clear();
349
+ registrations.delete(panel.id);
350
+ });
351
+ },
352
+ };
353
+ };
354
+
355
+ const dispose = () => {
356
+ if (disposed) return;
357
+ disposed = true;
358
+ transition(() => {
359
+ closeActive(false);
360
+ returnFocus = undefined;
361
+ for (const state of registrations.values()) {
362
+ state.disposed = true;
363
+ state.listeners.clear();
364
+ }
365
+ registrations.clear();
366
+ options.document.removeEventListener("keydown", onKeyDown);
367
+ });
368
+ };
369
+
370
+ return { register, dispose } satisfies PanelCoordinator<Content>;
371
+ }
@@ -1,168 +1,80 @@
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
- import { transformer } from "../../mixin.ts";
7
-
8
- import type { createMachine as createMachineT } from "npm:xstate";
7
+ import { React, ReactDOM } from "../expose/React.ts";
8
+ import { createItemBoundary } from "./mount.ts";
9
9
  import { Registry } from "./registry.ts";
10
-
11
- export type StateMachine = ReturnType<typeof createMachineT>;
12
- export let Machine: StateMachine;
13
-
14
- const registry = new (class extends Registry<React.ReactNode> {
15
- private static NodeNash = Symbol.for("NodeHash");
16
-
17
- getHash(value: React.ReactNode) {
18
- if (!this.has(value)) {
19
- return null;
20
- }
21
- // @ts-ignore
22
- const hash = value[this.constructor.NodeNash];
23
- const state = `bespoke_${hash}`;
24
- const event = `bespoke_${hash}_button_click`;
25
- return { state, event };
26
- }
27
-
28
- override add(value: React.ReactNode, onEntry?: any, onExit?: any): this {
29
- const hash = crypto.randomUUID();
30
- // @ts-ignore
31
- value[this.constructor.NodeNash] = hash;
32
- const state = `bespoke_${hash}`;
33
- const event = `bespoke_${hash}_button_click`;
34
-
35
- stateToNode.set(state, value);
36
-
37
- ON[event] = {
38
- target: state,
39
- };
40
-
41
- STATES[state] = {
42
- on: Object.setPrototypeOf(
43
- {
44
- [event]: {
45
- target: "disabled",
10
+ import {
11
+ createPanelCoordinator,
12
+ type PanelController,
13
+ type PanelCoordinator,
14
+ type PanelRegistration,
15
+ } from "./panel-logic.ts";
16
+
17
+ export type { PanelController, PanelRegistration, PanelWidth } from "./panel-logic.ts";
18
+
19
+ const mountReactPanel = (host: HTMLElement, panel: PanelRegistration<React.ReactNode>, close: () => void) => {
20
+ const root = ReactDOM.createRoot(host);
21
+ const PanelBoundary = createItemBoundary(React, "panel") as unknown as React.ComponentType<{
22
+ children?: React.ReactNode;
23
+ }>;
24
+ const PanelContent = () => React.createElement("div", { className: "spicetify-panel-content" }, panel.render());
25
+ const PanelSurface = () =>
26
+ React.createElement(
27
+ React.Fragment,
28
+ null,
29
+ React.createElement(
30
+ "header",
31
+ { className: "spicetify-panel-header" },
32
+ React.createElement("h2", { className: "spicetify-panel-title" }, panel.label),
33
+ React.createElement(
34
+ "button",
35
+ {
36
+ className: "spicetify-panel-close spicetify-button-circle",
37
+ "aria-label": `Close ${panel.label}`,
38
+ onClick: close,
46
39
  },
47
- },
48
- ON,
40
+ "×",
41
+ ),
49
42
  ),
50
- };
51
-
52
- if (onEntry) {
53
- const entry = `bespoke_${hash}_entry`;
54
- STATES[state].entry = [entry];
55
- ACTIONS[entry] = onEntry;
56
- }
57
- if (onExit) {
58
- const exit = `bespoke_${hash}_exit`;
59
- Machine.config.states![state].exit = [exit];
60
- ACTIONS[exit] = onExit;
61
- }
43
+ React.createElement(PanelBoundary, null, React.createElement(PanelContent)),
44
+ );
45
+ root.render(React.createElement(PanelSurface));
46
+ return () => root.unmount();
47
+ };
62
48
 
63
- return super.add(value);
64
- }
49
+ const panelCoordinatorKey = Symbol.for("spicetify.stdlib.panel-coordinator");
50
+ const shared = globalThis as typeof globalThis & Record<symbol, unknown>;
51
+ const previousCoordinator = shared[panelCoordinatorKey] as PanelCoordinator<React.ReactNode> | undefined;
52
+ previousCoordinator?.dispose();
65
53
 
66
- override delete(item: React.ReactNode): boolean {
67
- // @ts-ignore
68
- const hash = item[this.constructor.NodeNash];
69
- const state = `bespoke_${hash}`;
70
- const event = `bespoke_${hash}_button_click`;
54
+ export const panelCoordinator = createPanelCoordinator<React.ReactNode>({
55
+ document,
56
+ window,
57
+ mount: mountReactPanel,
58
+ });
59
+ shared[panelCoordinatorKey] = panelCoordinator;
71
60
 
72
- stateToNode.delete(state);
61
+ class PanelRegistry extends Registry<PanelRegistration<React.ReactNode>> {
62
+ private controllers = new Map<PanelRegistration<React.ReactNode>, PanelController>();
73
63
 
74
- delete ON[event];
75
- return super.delete(item);
64
+ override add(panel: PanelRegistration<React.ReactNode>): this {
65
+ this.controllers.set(panel, panelCoordinator.register(panel));
66
+ return super.add(panel);
76
67
  }
77
- })();
78
- export default registry;
79
-
80
- const stateToNode = new Map<string, React.ReactNode>();
81
-
82
- declare global {
83
- var __renderPanel: any;
84
- }
85
68
 
86
- globalThis.__renderPanel = (state: string) => {
87
- if (!state.startsWith("bespoke_")) {
88
- return null;
69
+ override delete(panel: PanelRegistration<React.ReactNode>): boolean {
70
+ this.controllers.get(panel)?.dispose();
71
+ this.controllers.delete(panel);
72
+ return super.delete(panel);
89
73
  }
90
74
 
91
- return stateToNode.get(state);
92
- };
93
-
94
- let ON: Record<string, any> = {};
95
- let STATES: Record<string, any> = {};
96
- let ACTIONS: Record<string, any> = {};
97
-
98
- transformer(
99
- (emit) => (str) => {
100
- emit();
101
-
102
- str = str.replace(/(=\(0,[a-zA-Z_\$][\w\$]*\.[a-zA-Z_\$][\w\$]*\)\(\{id:"RightPanelState)/, "=__Machine$1");
103
- Object.defineProperty(globalThis, "__Machine", {
104
- set: ($: StateMachine) => {
105
- Machine = $;
106
-
107
- queueMicrotask(() => {
108
- ON = {
109
- ...ON,
110
- ...Machine.config.states!.disabled.on,
111
- panel_close_click: [
112
- {
113
- target: "disabled",
114
- },
115
- ],
116
- };
117
- delete ON.playback_autoplay_context_changed;
118
-
119
- for (const [k, v] of Object.entries(Machine.config.states! as Record<string, any>)) {
120
- if (k === "puffin_activation") {
121
- continue;
122
- }
123
- v.on = new Proxy(v.on!, {
124
- get(target, p, _receiver) {
125
- // @ts-ignore
126
- if (p.startsWith("bespoke_")) {
127
- // @ts-ignore
128
- return ON[p];
129
- }
130
- // @ts-ignore
131
- return target[p];
132
- },
133
- });
134
- }
135
-
136
- Object.setPrototypeOf(Machine.config.states!, STATES);
137
-
138
- Machine._options.actions = ACTIONS;
139
- });
140
- },
141
- get: () => Machine,
142
- });
143
-
144
- // ! HACKY ALERT
145
- str = str.replace(
146
- /(case [a-zA-Z_\$][\w\$]*\.[a-zA-Z_\$][\w\$]*\.Disabled:return!0;default:)/,
147
- "$1return true;",
148
- );
149
-
150
- return str;
151
- },
152
- {
153
- glob: /^\/xpui\.js/,
154
- },
155
- );
156
-
157
- transformer(
158
- (emit) => (str) => {
159
- emit();
160
-
161
- str = str.replace(/(\(([a-zA-Z_\$][\w\$]*),"PanelSection".+?children:\[?)/, "$1__renderPanel($2),");
75
+ controller(panel: PanelRegistration<React.ReactNode>): PanelController | undefined {
76
+ return this.controllers.get(panel);
77
+ }
78
+ }
162
79
 
163
- return str;
164
- },
165
- {
166
- glob: /^\/dwp\-panel\-section\.js/,
167
- },
168
- );
80
+ export default new PanelRegistry();
@@ -63,7 +63,7 @@ export type PlaybarButtonProps = {
63
63
  isActiveNoIndicator?: boolean;
64
64
  disabled?: boolean;
65
65
  icon?: string;
66
- onClick: () => void;
66
+ onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
67
67
  /** Position within the module-button group; lower renders earlier. Read by the mount sorter. */
68
68
  order?: number;
69
69
  };
@@ -97,11 +97,12 @@ mountRegistryAnchor({
97
97
  const route = items.find((item: any) => {
98
98
  const path = item?.props?.path;
99
99
  return typeof path === "string" && matchRoute(pathname, path);
100
- }) as { props: { element?: unknown; children?: unknown } } | undefined;
100
+ }) as { props: { path: string; element?: unknown; children?: unknown } } | undefined;
101
101
  if (!route) return null;
102
102
  return (React as any).createElement(
103
103
  "div",
104
104
  {
105
+ key: route.props.path,
105
106
  className: "spicetify-route-page",
106
107
  style: {
107
108
  position: "absolute",
@@ -8,9 +8,9 @@ import { SettingsSection } from "../../lib/primitives.tsx";
8
8
  import { Registry } from "./registry.ts";
9
9
  import settingsSection from "./settingsSection.ts";
10
10
 
11
- // Rows from every module, rendered together under one "Spicetify" section
12
- // in the client's settings page. A module with a single setting registers a
13
- // row here instead of claiming a whole section (or a profile-menu slot) of
11
+ // Rows from every module, rendered together under one "General" section on
12
+ // the standalone Spicetify Settings page. A module with a single setting
13
+ // registers a row here instead of claiming a whole section (or a menu slot) of
14
14
  // its own; SettingsToggleRow (lib/primitives.tsx) is the one-boolean case.
15
15
  const listeners = new Set<() => void>();
16
16
 
@@ -38,7 +38,7 @@ const SpicetifyGroup = () => {
38
38
  const rows = registry.all();
39
39
  if (rows.length === 0) return null;
40
40
  return (
41
- <SettingsSection title="Spicetify">
41
+ <SettingsSection title="General">
42
42
  {rows.map((row, i) => (
43
43
  <React.Fragment key={i}>{row}</React.Fragment>
44
44
  ))}