@spicetify/kit 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 (115) hide show
  1. package/bin/spicetify-kit.js +4 -0
  2. package/dist/build.js +335 -0
  3. package/dist/check.js +284 -0
  4. package/dist/classmap.js +187 -0
  5. package/dist/cli.js +53 -0
  6. package/dist/create.js +429 -0
  7. package/dist/dev.js +100 -0
  8. package/dist/from-theme.js +96 -0
  9. package/dist/install.js +60 -0
  10. package/dist/launch.js +122 -0
  11. package/dist/pack.js +31 -0
  12. package/dist/push.js +211 -0
  13. package/dist/vault.js +108 -0
  14. package/package.json +49 -0
  15. package/vendor/classmaps/1020096/classmap-19f856aefd5.json +109 -0
  16. package/vendor/shims/chunks.d.ts +10 -0
  17. package/vendor/shims/remote-modules.d.ts +6 -0
  18. package/vendor/shims/spicetify.d.ts +2437 -0
  19. package/vendor/stdlib/deps.ts +24 -0
  20. package/vendor/stdlib/index.ts +24 -0
  21. package/vendor/stdlib/lib/modal.tsx +128 -0
  22. package/vendor/stdlib/lib/popover.ts +84 -0
  23. package/vendor/stdlib/lib/primitives-classes.ts +66 -0
  24. package/vendor/stdlib/lib/primitives-vanilla.ts +277 -0
  25. package/vendor/stdlib/lib/primitives.tsx +314 -0
  26. package/vendor/stdlib/load.ts +17 -0
  27. package/vendor/stdlib/metadata.json +18 -0
  28. package/vendor/stdlib/mixin.ts +32 -0
  29. package/vendor/stdlib/mod.ts +26 -0
  30. package/vendor/stdlib/src/chunks.d.ts +10 -0
  31. package/vendor/stdlib/src/client.ts +114 -0
  32. package/vendor/stdlib/src/createIconComponent.tsx +29 -0
  33. package/vendor/stdlib/src/events.mix.ts +55 -0
  34. package/vendor/stdlib/src/events.ts +58 -0
  35. package/vendor/stdlib/src/expose/GraphQL.ts +40 -0
  36. package/vendor/stdlib/src/expose/Platform.ts +48 -0
  37. package/vendor/stdlib/src/expose/React.ts +95 -0
  38. package/vendor/stdlib/src/expose/ReactFlipToolkitSpring.ts +27 -0
  39. package/vendor/stdlib/src/expose/ReduxStore.ts +39 -0
  40. package/vendor/stdlib/src/expose/SettingsSection.ts +60 -0
  41. package/vendor/stdlib/src/expose/Snackbar.ts +34 -0
  42. package/vendor/stdlib/src/expose/Tippy.ts +26 -0
  43. package/vendor/stdlib/src/expose/enqueueImageSnackbar.ts +25 -0
  44. package/vendor/stdlib/src/expose/index.ts +14 -0
  45. package/vendor/stdlib/src/expose/jsx-runtime.ts +46 -0
  46. package/vendor/stdlib/src/expose/react-dom-shim.ts +50 -0
  47. package/vendor/stdlib/src/expose/react-shim.ts +116 -0
  48. package/vendor/stdlib/src/logger.ts +35 -0
  49. package/vendor/stdlib/src/playbar-compat.tsx +136 -0
  50. package/vendor/stdlib/src/registers/index.ts +176 -0
  51. package/vendor/stdlib/src/registers/menu.ts +160 -0
  52. package/vendor/stdlib/src/registers/mount.ts +307 -0
  53. package/vendor/stdlib/src/registers/nativeAnchors.ts +49 -0
  54. package/vendor/stdlib/src/registers/navlink.tsx +165 -0
  55. package/vendor/stdlib/src/registers/order.ts +22 -0
  56. package/vendor/stdlib/src/registers/panel.ts +168 -0
  57. package/vendor/stdlib/src/registers/playbarButton.tsx +93 -0
  58. package/vendor/stdlib/src/registers/playbarWidget.tsx +71 -0
  59. package/vendor/stdlib/src/registers/registry.ts +12 -0
  60. package/vendor/stdlib/src/registers/root.ts +101 -0
  61. package/vendor/stdlib/src/registers/route.ts +123 -0
  62. package/vendor/stdlib/src/registers/settingsRow.tsx +56 -0
  63. package/vendor/stdlib/src/registers/settingsSection.ts +59 -0
  64. package/vendor/stdlib/src/registers/topbarLeftButton.tsx +86 -0
  65. package/vendor/stdlib/src/registers/topbarRightButton.tsx +87 -0
  66. package/vendor/stdlib/src/storage.ts +163 -0
  67. package/vendor/stdlib/src/util.ts +62 -0
  68. package/vendor/stdlib/src/utils/index.ts +16 -0
  69. package/vendor/stdlib/src/webpack/ClassNames.gen.ts +10 -0
  70. package/vendor/stdlib/src/webpack/ClassNames.ts +6 -0
  71. package/vendor/stdlib/src/webpack/ClassNames.xpui.ts +16 -0
  72. package/vendor/stdlib/src/webpack/ComponentLibrary.gen.ts +10 -0
  73. package/vendor/stdlib/src/webpack/ComponentLibrary.ts +6 -0
  74. package/vendor/stdlib/src/webpack/ComponentLibrary.xpui.ts +18 -0
  75. package/vendor/stdlib/src/webpack/FilterContext.gen.ts +10 -0
  76. package/vendor/stdlib/src/webpack/FilterContext.ts +6 -0
  77. package/vendor/stdlib/src/webpack/FilterContext.xpui.ts +10 -0
  78. package/vendor/stdlib/src/webpack/Mousetrap.gen.ts +10 -0
  79. package/vendor/stdlib/src/webpack/Mousetrap.ts +6 -0
  80. package/vendor/stdlib/src/webpack/Mousetrap.xpui.ts +12 -0
  81. package/vendor/stdlib/src/webpack/React.gen.ts +14 -0
  82. package/vendor/stdlib/src/webpack/React.ts +6 -0
  83. package/vendor/stdlib/src/webpack/React.xpui.ts +15 -0
  84. package/vendor/stdlib/src/webpack/ReactComponents.desktop.ts +17 -0
  85. package/vendor/stdlib/src/webpack/ReactComponents.gen.ts +74 -0
  86. package/vendor/stdlib/src/webpack/ReactComponents.panel.ts +19 -0
  87. package/vendor/stdlib/src/webpack/ReactComponents.ts +50 -0
  88. package/vendor/stdlib/src/webpack/ReactComponents.xpui.ts +164 -0
  89. package/vendor/stdlib/src/webpack/ReactFlipToolkit.gen.ts +12 -0
  90. package/vendor/stdlib/src/webpack/ReactFlipToolkit.ts +6 -0
  91. package/vendor/stdlib/src/webpack/ReactFlipToolkit.xpui.ts +14 -0
  92. package/vendor/stdlib/src/webpack/ReactHooks.fullscreen.ts +15 -0
  93. package/vendor/stdlib/src/webpack/ReactHooks.gen.ts +32 -0
  94. package/vendor/stdlib/src/webpack/ReactHooks.ts +6 -0
  95. package/vendor/stdlib/src/webpack/ReactHooks.xpui.ts +38 -0
  96. package/vendor/stdlib/src/webpack/ReactQuery.gen.ts +26 -0
  97. package/vendor/stdlib/src/webpack/ReactQuery.ts +6 -0
  98. package/vendor/stdlib/src/webpack/ReactQuery.xpui.ts +46 -0
  99. package/vendor/stdlib/src/webpack/ReactRouter.gen.ts +12 -0
  100. package/vendor/stdlib/src/webpack/ReactRouter.ts +6 -0
  101. package/vendor/stdlib/src/webpack/ReactRouter.xpui.ts +22 -0
  102. package/vendor/stdlib/src/webpack/Snackbar.gen.ts +12 -0
  103. package/vendor/stdlib/src/webpack/Snackbar.ts +6 -0
  104. package/vendor/stdlib/src/webpack/Snackbar.xpui.ts +27 -0
  105. package/vendor/stdlib/src/webpack/URI.gen.ts +26 -0
  106. package/vendor/stdlib/src/webpack/URI.ts +82 -0
  107. package/vendor/stdlib/src/webpack/URI.xpui.ts +111 -0
  108. package/vendor/stdlib/src/webpack/capture-readiness.ts +49 -0
  109. package/vendor/stdlib/src/webpack/index.ts +186 -0
  110. package/vendor/stdlib/src/webpack/misc.gen.ts +16 -0
  111. package/vendor/stdlib/src/webpack/misc.ts +6 -0
  112. package/vendor/stdlib/src/webpack/misc.xpui.ts +27 -0
  113. package/vendor/stdlib/src/wpunpk.mix.ts +104 -0
  114. package/vendor/stdlib/src/wpunpk.ts +36 -0
  115. package/vendor/stdlib/vendor/rxjs.d.ts +5 -0
@@ -0,0 +1,136 @@
1
+ /*
2
+ * Copyright (C) 2026 Afonso Jorge Ramos
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ // The client still exposes the classic Spicetify.Playbar.Button/Widget, but
7
+ // they no longer mount in the restructured playbar (they construct a DOM node
8
+ // that is never inserted). Every other v2 helper (Menu, ContextMenu, Topbar,
9
+ // Keyboard, PopupModal, ...) still works, so this is the one gap. We re-back
10
+ // Button/Widget with the v3 playbarButton register so classic extensions and
11
+ // muscle memory keep working, and the typed API never hands you a dead helper.
12
+
13
+ import { React } from "./expose/React.ts";
14
+ import playbarButtons, { PlaybarButton } from "./registers/playbarButton.tsx";
15
+
16
+ // The register feeds `icon` to createIconComponent, which supplies its own
17
+ // <svg> wrapper. Classic icons are full <svg>...</svg> strings, so hand it the
18
+ // inner markup; already-inner markup passes through untouched.
19
+ const innerSvg = (icon: string): string => {
20
+ const match = /<svg[^>]*>([\s\S]*)<\/svg>/i.exec(icon);
21
+ return match ? match[1] : icon;
22
+ };
23
+
24
+ class PlaybarCompat {
25
+ onClick: (self: PlaybarCompat) => void;
26
+ private _label: string;
27
+ private _icon: string;
28
+ private _active: boolean;
29
+ private _disabled: boolean;
30
+ private node: React.ReactNode | null = null;
31
+ private readonly refreshers = new Set<() => void>();
32
+
33
+ constructor(
34
+ label: string,
35
+ icon: string,
36
+ onClick: (self: PlaybarCompat) => void = () => {},
37
+ disabled = false,
38
+ active = false,
39
+ registerOnCreate = true,
40
+ ) {
41
+ this._label = label;
42
+ this._icon = icon;
43
+ this.onClick = onClick;
44
+ this._disabled = disabled;
45
+ this._active = active;
46
+ if (registerOnCreate) this.register();
47
+ }
48
+
49
+ private refresh() {
50
+ for (const f of this.refreshers) f();
51
+ }
52
+
53
+ // Mutating any display field re-renders the mounted button, matching the
54
+ // classic API where `widget.active = true` / `widget.label = "..."` update live.
55
+ get label() {
56
+ return this._label;
57
+ }
58
+ set label(v) {
59
+ this._label = v;
60
+ this.refresh();
61
+ }
62
+ get icon() {
63
+ return this._icon;
64
+ }
65
+ set icon(v) {
66
+ this._icon = v;
67
+ this.refresh();
68
+ }
69
+ get active() {
70
+ return this._active;
71
+ }
72
+ set active(v) {
73
+ this._active = v;
74
+ this.refresh();
75
+ }
76
+ get disabled() {
77
+ return this._disabled;
78
+ }
79
+ set disabled(v) {
80
+ this._disabled = v;
81
+ this.refresh();
82
+ }
83
+ // No standalone DOM node in the register model; classic code rarely touches these.
84
+ get element(): HTMLButtonElement | null {
85
+ return null;
86
+ }
87
+ get tippy(): unknown {
88
+ return null;
89
+ }
90
+
91
+ register() {
92
+ if (this.node) return;
93
+ const self = this;
94
+ const Compat = () => {
95
+ const [, force] = React.useReducer((n: number) => n + 1, 0);
96
+ React.useEffect(() => {
97
+ self.refreshers.add(force);
98
+ return () => {
99
+ self.refreshers.delete(force);
100
+ };
101
+ }, []);
102
+ return (
103
+ <PlaybarButton
104
+ label={self._label}
105
+ icon={innerSvg(self._icon)}
106
+ isActive={self._active}
107
+ disabled={self._disabled}
108
+ onClick={() => self.onClick(self)}
109
+ />
110
+ );
111
+ };
112
+ this.node = <Compat />;
113
+ playbarButtons.add(this.node);
114
+ }
115
+
116
+ deregister() {
117
+ if (!this.node) return;
118
+ playbarButtons.delete(this.node);
119
+ this.node = null;
120
+ }
121
+ }
122
+
123
+ // Replace the client's dead Button/Widget with the register-backed shim.
124
+ // Returns a teardown that restores the originals.
125
+ export function installPlaybarCompat(): () => void {
126
+ const S = (globalThis as { Spicetify?: { Playbar?: Record<string, unknown> } }).Spicetify;
127
+ if (!S?.Playbar) return () => {};
128
+ const original = { Button: S.Playbar.Button, Widget: S.Playbar.Widget };
129
+ S.Playbar.Button = PlaybarCompat;
130
+ S.Playbar.Widget = PlaybarCompat;
131
+ return () => {
132
+ if (!S.Playbar) return;
133
+ S.Playbar.Button = original.Button;
134
+ S.Playbar.Widget = original.Widget;
135
+ };
136
+ }
@@ -0,0 +1,176 @@
1
+ /*
2
+ * Copyright (C) 2024 Delusoire
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ import type { ModuleRuntimeContext } from "../../mod.ts";
7
+
8
+ import { React } from "../expose/React.ts";
9
+ import { warn } from "../logger.ts";
10
+ import menu from "./menu.ts";
11
+ import { mountAdjacent } from "./mount.ts";
12
+ import { isNativeAnchor, type NativeAnchor, resolveNativeAnchor } from "./nativeAnchors.ts";
13
+ import navlink from "./navlink.tsx";
14
+ import panel from "./panel.ts";
15
+ import playbarButton, { PlaybarButton } from "./playbarButton.tsx";
16
+ import playbarWidget from "./playbarWidget.tsx";
17
+ import { Registry } from "./registry.ts";
18
+ import root from "./root.ts";
19
+ import route from "./route.ts";
20
+ import settingsRow from "./settingsRow.tsx";
21
+ import settingsSection from "./settingsSection.ts";
22
+ import topbarLeftButton, { TopbarLeftButton } from "./topbarLeftButton.tsx";
23
+ import topbarRightButton, { TopbarRightButton } from "./topbarRightButton.tsx";
24
+ const [rootChild, rootProvider] = root;
25
+ const registers = {
26
+ menu,
27
+ navlink,
28
+ panel,
29
+ playbarButton,
30
+ playbarWidget,
31
+ rootChild,
32
+ rootProvider,
33
+ route,
34
+ settingsRow,
35
+ settingsSection,
36
+ topbarLeftButton,
37
+ topbarRightButton,
38
+ } satisfies Record<string, Registry<any>>;
39
+ type Registers = typeof registers;
40
+
41
+ // ---- placeButton: one ergonomic, position-controllable way to add a button ----
42
+ // Instead of picking the right register key and hand-building the matching
43
+ // component (easy to mismatch, no ordering), a module names a location and
44
+ // passes the button's props in one call. `order` controls position within the
45
+ // group of module buttons in that slot (lower renders earlier).
46
+
47
+ /** Where a module button can live. */
48
+ export type ButtonLocation = "topbar-left" | "topbar-right" | "playbar";
49
+
50
+ export interface PlaceButtonOptions {
51
+ /** Tooltip + accessible label. */
52
+ label: string;
53
+ /** Inner SVG markup drawn on the stdlib 16-grid. */
54
+ icon?: string;
55
+ onClick: () => void;
56
+ disabled?: boolean;
57
+ /** Position within this slot's module buttons; lower renders earlier. Default 0. */
58
+ order?: number;
59
+ /** Playbar only: render the active indicator. Ignored in the top bar. */
60
+ isActive?: boolean;
61
+ /**
62
+ * Place the button next to one of the client's own buttons instead of in the
63
+ * module-button group. `anchor` is a stable stdlib-owned name (e.g.
64
+ * "playbar:queue"); if it can't be resolved on this client the button falls
65
+ * back to ordinary `order` placement, so it is never hidden. `side` defaults
66
+ * to "after".
67
+ */
68
+ near?: { anchor: NativeAnchor; side?: "before" | "after" };
69
+ }
70
+
71
+ export interface ButtonHandle {
72
+ /** Remove the button now. It is also removed automatically when the module unloads. */
73
+ remove(): void;
74
+ }
75
+
76
+ const BUTTON_SLOTS = {
77
+ "topbar-left": ["topbarLeftButton", TopbarLeftButton],
78
+ "topbar-right": ["topbarRightButton", TopbarRightButton],
79
+ playbar: ["playbarButton", PlaybarButton],
80
+ } as const satisfies Record<ButtonLocation, [keyof Registers, React.FC<any>]>;
81
+
82
+ export class Registrar {
83
+ constructor(public id: string) {}
84
+
85
+ private ledger = new Map<any, keyof Registers>();
86
+ // Cleanups for things not held in the registry ledger (e.g. near-anchored
87
+ // buttons, which mount their own host). Run on dispose so nothing leaks
88
+ // when the module unloads.
89
+ private disposers = new Set<() => void>();
90
+
91
+ /**
92
+ * Place a button in a known client location without touching register keys
93
+ * or building the component yourself. Returns a handle to remove it early;
94
+ * otherwise it is cleaned up when the module unloads.
95
+ */
96
+ placeButton(location: ButtonLocation, options: PlaceButtonOptions): ButtonHandle {
97
+ const slot = BUTTON_SLOTS[location];
98
+ if (!slot) throw new Error(`[stdlib] unknown button location: ${location}`);
99
+ const [key, Component] = slot;
100
+ const element = React.createElement(Component as React.FC<PlaceButtonOptions>, { order: 0, ...options });
101
+
102
+ const inGroup = (): ButtonHandle => {
103
+ this.register(key, element);
104
+ return { remove: () => this.unregister(key, element) };
105
+ };
106
+
107
+ if (!options.near) return inGroup();
108
+ const { anchor, side = "after" } = options.near;
109
+ if (!isNativeAnchor(anchor)) {
110
+ warn(`[stdlib] unknown native anchor "${anchor}"; placing "${options.label}" with order instead`);
111
+ return inGroup();
112
+ }
113
+
114
+ // Try to sit next to the named native button; if it never appears, fall
115
+ // back to the module-button group so the button is never lost.
116
+ let fellBackHandle: ButtonHandle | undefined;
117
+ const adjacent = mountAdjacent({
118
+ className: "spicetify-near-button",
119
+ element,
120
+ findTarget: () => resolveNativeAnchor(anchor),
121
+ side,
122
+ giveUpMs: 5000,
123
+ onGiveUp: () => {
124
+ fellBackHandle ??= inGroup();
125
+ },
126
+ });
127
+ // Tracked so module unload (dispose) tears the near button down too; the
128
+ // near path never touches the registry ledger, so it needs its own hook.
129
+ const cleanup = () => {
130
+ adjacent.remove();
131
+ fellBackHandle?.remove();
132
+ };
133
+ this.disposers.add(cleanup);
134
+ return {
135
+ remove: () => {
136
+ this.disposers.delete(cleanup);
137
+ cleanup();
138
+ },
139
+ };
140
+ }
141
+
142
+ register<R extends keyof Registers>(type: R, ...args: Parameters<Registers[R]["add"]>) {
143
+ this.ledger.set(args[0], type);
144
+ // @ts-ignore
145
+ registers[type].add(...args);
146
+ }
147
+
148
+ // Routes are registered as elements with a string "route" tag; this
149
+ // hides that incantation. Returns the item for manual unregister.
150
+ registerRoute(path: string, element: React.ReactNode): React.ReactNode {
151
+ const item = React.createElement("route", { path, element });
152
+ this.register("route", item);
153
+ return item;
154
+ }
155
+
156
+ unregister<R extends keyof Registers>(type: R, ...args: Parameters<Registers[R]["delete"]>) {
157
+ this.ledger.delete(args[0]);
158
+ // @ts-ignore
159
+ registers[type].delete(...args);
160
+ }
161
+
162
+ dispose() {
163
+ for (const [item, type] of this.ledger.entries()) this.unregister(type, item);
164
+ this.ledger.clear();
165
+ for (const dispose of this.disposers) dispose();
166
+ this.disposers.clear();
167
+ }
168
+ }
169
+
170
+ export const createRegistrar = (ctx: ModuleRuntimeContext) => {
171
+ const registrar = new Registrar(ctx.identifier);
172
+ ctx.defer(() => {
173
+ registrar.dispose();
174
+ });
175
+ return registrar;
176
+ };
@@ -0,0 +1,160 @@
1
+ /*
2
+ * Copyright (C) 2024 Delusoire
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ import { React } from "../expose/React.ts";
7
+
8
+ import { matchLast } from "../util.ts";
9
+
10
+ import { warn } from "../logger.ts";
11
+ import { transformer } from "../../mixin.ts";
12
+ import { Registry } from "./registry.ts";
13
+
14
+ type __MenuContext = React.Context<MenuContext>;
15
+
16
+ declare global {
17
+ var __MenuContext: __MenuContext;
18
+ }
19
+
20
+ type MenuContext = {
21
+ props: any;
22
+ trigger: string;
23
+ target: HTMLElement;
24
+ };
25
+
26
+ const items = new Registry<React.ReactNode>();
27
+ export default items;
28
+
29
+ export const useMenuItem = () => React.useContext(globalThis.__MenuContext);
30
+
31
+ declare global {
32
+ var __renderMenuItems: any;
33
+ }
34
+
35
+ globalThis.__renderMenuItems = () => items.all();
36
+ transformer(
37
+ (emit) => (str) => {
38
+ emit();
39
+
40
+ str = str.replace(/("Menu".+?children:)([a-zA-Z_\$][\w\$]*)/, "$1[__renderMenuItems(),$2].flat()");
41
+
42
+ const croppedInput = str.match(/.*value:"contextmenu"/)![0];
43
+ const react = matchLast(croppedInput, /([a-zA-Z_\$][\w\$]*)\.useRef/g)[1];
44
+
45
+ const [, menu, trigger, target] =
46
+ matchLast(
47
+ croppedInput,
48
+ /\(\{[^}]*menu:([a-zA-Z_\$][\w\$]*),[^}]*trigger:([a-zA-Z_\$][\w\$]*),[^}]*triggerRef:([a-zA-Z_\$][\w\$]*)/g,
49
+ ) ?? [];
50
+
51
+ let value: string;
52
+ if (menu && trigger && target) {
53
+ value = `{props:${menu}?.props,trigger:${trigger},target:${target}}`;
54
+ } else {
55
+ value = `{props:e.menu?.props,trigger:e.trigger,target:e.triggerRef}`;
56
+ }
57
+
58
+ str = str.replace(
59
+ /render:(.{0,100}?\(0,([a-zA-Z_\$][\w\$]*)\.jsx\)\([a-zA-Z_\$][\w\$]*\.[a-zA-Z_\$][\w\$]*,\{value:"contextmenu",[^\}]+\}[^,]+),/,
60
+ `render:(props)=>{const value=${value};return ($2.jsx)((globalThis.__MenuContext??=${react}.createContext(null)).Provider,{value,children:($1)(props)});},`,
61
+ );
62
+
63
+ return str;
64
+ },
65
+ {
66
+ glob: /^\/xpui\.js/,
67
+ },
68
+ );
69
+
70
+ // True when the open menu came from the profile (avatar) button. 1.2.94
71
+ // dropped the user-widget-link testid; the account image inside the
72
+ // trigger button is the durable discriminator, with the old testid kept
73
+ // as a fallback for older clients.
74
+ export const openedFromProfileMenu = (ctx: Pick<MenuContext, "trigger" | "target"> | null): boolean => {
75
+ if (!ctx || ctx.trigger !== "click") return false;
76
+ const button = ctx.target?.closest?.("button");
77
+ if (!button) return false;
78
+ return !!button.querySelector("img, figure") || button.getAttribute("data-testid") === "user-widget-link";
79
+ };
80
+
81
+ export const createProfileMenuShouldAdd = () => (ctx: MenuContext) => openedFromProfileMenu(ctx);
82
+
83
+ // Foreign menu items have no access to the client's dismissal context;
84
+ // the tippy portal only closes on an outside press. Call after handling
85
+ // a click. Deferred so the item's own handler finishes first.
86
+ export const closeMenu = (): void => {
87
+ setTimeout(() => {
88
+ document.body.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
89
+ }, 0);
90
+ };
91
+
92
+ // Transform-free path: every context menu renders inside the #context-menu
93
+ // tippy portal; registered items are appended to the menu list when one
94
+ // opens. useMenuItem() carries the trigger and target captured from the
95
+ // opening event — the menu component's own props are not reachable from
96
+ // outside the client render pipeline and stay null.
97
+ if (
98
+ !(globalThis as never as Record<string, unknown>).__SPICETIFY_APPLY_TRANSFORMS__ &&
99
+ typeof document !== "undefined"
100
+ ) {
101
+ void CHUNKS.xpui.promise.then(async () => {
102
+ const { ReactDOM } = await import("../expose/React.js");
103
+ const { createItemBoundary } = await import("./mount.js");
104
+ const R = React as any;
105
+ const createRoot = (ReactDOM as any).createRoot;
106
+ if (typeof createRoot !== "function") {
107
+ warn("[stdlib] cannot mount menu items (no createRoot)");
108
+ return;
109
+ }
110
+
111
+ let last: Omit<MenuContext, "props"> = { trigger: "", target: document.body } as never;
112
+ document.addEventListener(
113
+ "contextmenu",
114
+ (e) => {
115
+ last = { trigger: "right-click", target: e.target as HTMLElement };
116
+ },
117
+ true,
118
+ );
119
+ document.addEventListener(
120
+ "mousedown",
121
+ (e) => {
122
+ last = { trigger: "click", target: e.target as HTMLElement };
123
+ },
124
+ true,
125
+ );
126
+
127
+ const ItemBoundary = createItemBoundary(R, "menu");
128
+ const live: Array<{ menu: Element; root: { unmount: () => void } }> = [];
129
+ const processed = new WeakSet<Element>();
130
+
131
+ const sweep = () => {
132
+ for (let i = live.length - 1; i >= 0; i--) {
133
+ if (!live[i].menu.isConnected) {
134
+ live[i].root.unmount();
135
+ live.splice(i, 1);
136
+ }
137
+ }
138
+ if (items.size === 0) return;
139
+ for (const menu of document.querySelectorAll("#context-menu [role=menu]")) {
140
+ if (processed.has(menu)) continue;
141
+ processed.add(menu);
142
+ const host = document.createElement("li");
143
+ host.setAttribute("role", "presentation");
144
+ host.className = "spicetify-menu-items";
145
+ menu.appendChild(host);
146
+ const root = createRoot(host);
147
+ globalThis.__MenuContext ??= R.createContext(null);
148
+ root.render(
149
+ R.createElement(
150
+ globalThis.__MenuContext.Provider,
151
+ { value: { props: null, ...last } },
152
+ ...items.all().map((item, i) => R.createElement(ItemBoundary, { key: i }, item)),
153
+ ),
154
+ );
155
+ live.push({ menu, root });
156
+ }
157
+ };
158
+ new MutationObserver(sweep).observe(document.body, { childList: true, subtree: true });
159
+ });
160
+ }