@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.
@@ -25,7 +25,12 @@ import {
25
25
  SETTINGS_ROW_CLASS,
26
26
  SETTINGS_ROW_CONTROL_CLASS,
27
27
  SETTINGS_ROW_LABEL_CLASS,
28
+ SETTINGS_ROW_TEXT_CLASS,
29
+ SETTINGS_ACTION_GROUP_CLASS,
30
+ SETTINGS_HELP_TEXT_CLASS,
31
+ SETTINGS_LABEL_COPY_CLASS,
28
32
  SETTINGS_SECTION_CLASS,
33
+ SETTINGS_SECTION_HEADING_CLASS,
29
34
  badgeClass,
30
35
  buttonClass,
31
36
  type BadgeTone,
@@ -34,6 +39,7 @@ import {
34
39
  chipClass,
35
40
  DIALOG_BODY_CLASS,
36
41
  DIALOG_CLASS,
42
+ DIALOG_LARGE_CLASS,
37
43
  DIALOG_HEADER_CLASS,
38
44
  ICON_BUTTON_CLASS,
39
45
  MENU_ITEM_CLASS,
@@ -41,8 +47,11 @@ import {
41
47
  SEARCHBAR_CLASS,
42
48
  SELECT_CLASS,
43
49
  } from "./primitives-classes.ts";
50
+ import { activateDialog } from "./dialog-lifecycle.ts";
51
+ import { Tooltip } from "./floating.tsx";
44
52
 
45
53
  export type { BadgeTone, ButtonVariant } from "./primitives-classes.ts";
54
+ export { Popover, PopoverMenu, PopoverMenuItem, Tooltip } from "./floating.tsx";
46
55
 
47
56
  // ---------- buttons ----------
48
57
 
@@ -75,15 +84,40 @@ export const IconButton: React.FC<{
75
84
  </button>
76
85
  );
77
86
 
87
+ // One shared ordering affordance for provider lists and other short ordered
88
+ // settings. Keeping the arrows here prevents each module from inventing a
89
+ // different size, icon, disabled state, or accessible label.
90
+ export const ReorderButtons: React.FC<{
91
+ label: string;
92
+ disableUp: boolean;
93
+ disableDown: boolean;
94
+ onMove: (direction: -1 | 1) => void;
95
+ }> = (props) => (
96
+ <span className={SETTINGS_ACTION_GROUP_CLASS}>
97
+ <IconButton ariaLabel={`Move ${props.label} up`} disabled={props.disableUp} onClick={() => props.onMove(-1)}>
98
+ ↑
99
+ </IconButton>
100
+ <IconButton ariaLabel={`Move ${props.label} down`} disabled={props.disableDown} onClick={() => props.onMove(1)}>
101
+ ↓
102
+ </IconButton>
103
+ </span>
104
+ );
105
+
78
106
  // ---------- inputs ----------
79
107
 
80
108
  export function Select<T extends string>(props: {
81
109
  options: ReadonlyArray<{ value: T; label: string }>;
82
110
  value: T;
83
111
  onChange: (value: T) => void;
112
+ ariaLabel?: string;
84
113
  }): React.ReactElement {
85
114
  return (
86
- <select className={SELECT_CLASS} value={props.value} onChange={(e) => props.onChange(e.target.value as T)}>
115
+ <select
116
+ className={SELECT_CLASS}
117
+ value={props.value}
118
+ aria-label={props.ariaLabel}
119
+ onChange={(e) => props.onChange(e.target.value as T)}
120
+ >
87
121
  {props.options.map((option) => (
88
122
  <option key={option.value} value={option.value}>
89
123
  {option.label}
@@ -97,7 +131,11 @@ export const TextInput: React.FC<{
97
131
  placeholder?: string;
98
132
  value?: string;
99
133
  disabled?: boolean;
134
+ readOnly?: boolean;
100
135
  onInput?: (value: string) => void;
136
+ onFocus?: React.FocusEventHandler<HTMLInputElement>;
137
+ onBlur?: React.FocusEventHandler<HTMLInputElement>;
138
+ ariaLabel?: string;
101
139
  }> = (props) => (
102
140
  <input
103
141
  type="text"
@@ -105,6 +143,10 @@ export const TextInput: React.FC<{
105
143
  placeholder={props.placeholder}
106
144
  value={props.value}
107
145
  disabled={props.disabled}
146
+ readOnly={props.readOnly}
147
+ aria-label={props.ariaLabel}
148
+ onFocus={props.onFocus}
149
+ onBlur={props.onBlur}
108
150
  onChange={(e) => props.onInput?.(e.target.value)}
109
151
  />
110
152
  );
@@ -136,13 +178,12 @@ export const Card: React.FC<{ children: React.ReactNode }> = (props) => (
136
178
 
137
179
  // ---------- settings ----------
138
180
 
139
- // Native-looking settings chrome for the `settingsSection` register. These
140
- // render the client's own section/row structure rather than capturing its
141
- // components: the settings route is a lazily loaded chunk, and the loader
142
- // does not source-transform those, so a capture there never resolves.
181
+ // Native-looking settings chrome for the `settingsSection` register on the
182
+ // standalone Spicetify Settings page. These render the client's own section
183
+ // and row structure rather than capturing its route-local components.
143
184
  export const SettingsSection: React.FC<{ title?: React.ReactNode; children: React.ReactNode }> = (props) => (
144
185
  <div className={SETTINGS_SECTION_CLASS}>
145
- {props.title === undefined ? null : <h2>{props.title}</h2>}
186
+ {props.title === undefined ? null : <h2 className={SETTINGS_SECTION_HEADING_CLASS}>{props.title}</h2>}
146
187
  {props.children}
147
188
  </div>
148
189
  );
@@ -155,18 +196,79 @@ export const SettingsRow: React.FC<{ label: React.ReactNode; htmlFor?: string; c
155
196
  ) => (
156
197
  <div className={SETTINGS_ROW_CLASS}>
157
198
  <div className={SETTINGS_ROW_LABEL_CLASS}>
158
- {props.htmlFor === undefined ? props.label : <label htmlFor={props.htmlFor}>{props.label}</label>}
199
+ {props.htmlFor === undefined ? (
200
+ <span className={SETTINGS_ROW_TEXT_CLASS}>{props.label}</span>
201
+ ) : (
202
+ <label className={SETTINGS_ROW_TEXT_CLASS} htmlFor={props.htmlFor}>
203
+ {props.label}
204
+ </label>
205
+ )}
159
206
  </div>
160
207
  <div className={SETTINGS_ROW_CONTROL_CLASS}>{props.children}</div>
161
208
  </div>
162
209
  );
163
210
 
211
+ export const SettingsLabel: React.FC<{ label: React.ReactNode; description?: React.ReactNode }> = (props) => (
212
+ <span className={SETTINGS_LABEL_COPY_CLASS}>
213
+ <span>{props.label}</span>
214
+ {props.description === undefined ? null : <span className={SETTINGS_HELP_TEXT_CLASS}>{props.description}</span>}
215
+ </span>
216
+ );
217
+
218
+ export const SettingsActions: React.FC<{ children: React.ReactNode }> = (props) => (
219
+ <span className={SETTINGS_ACTION_GROUP_CLASS}>{props.children}</span>
220
+ );
221
+
222
+ export const SettingsButtonRow: React.FC<{
223
+ label: React.ReactNode;
224
+ description?: React.ReactNode;
225
+ buttonLabel: React.ReactNode;
226
+ disabled?: boolean;
227
+ variant?: ButtonVariant;
228
+ onClick: () => void;
229
+ }> = (props) => (
230
+ <SettingsRow label={<SettingsLabel label={props.label} description={props.description} />}>
231
+ <Button variant={props.variant ?? "secondary"} disabled={props.disabled} onClick={props.onClick}>
232
+ {props.buttonLabel}
233
+ </Button>
234
+ </SettingsRow>
235
+ );
236
+
237
+ export const SettingsTextInputRow: React.FC<{
238
+ label: React.ReactNode;
239
+ description?: React.ReactNode;
240
+ value: string;
241
+ placeholder?: string;
242
+ ariaLabel?: string;
243
+ onInput: (value: string) => void;
244
+ actionLabel?: React.ReactNode;
245
+ actionDisabled?: boolean;
246
+ onAction?: () => void;
247
+ }> = (props) => (
248
+ <SettingsRow label={<SettingsLabel label={props.label} description={props.description} />}>
249
+ <SettingsActions>
250
+ <TextInput
251
+ value={props.value}
252
+ placeholder={props.placeholder}
253
+ ariaLabel={props.ariaLabel}
254
+ onInput={props.onInput}
255
+ />
256
+ {props.actionLabel === undefined || props.onAction === undefined ? null : (
257
+ <Button variant="secondary" disabled={props.actionDisabled} onClick={props.onAction}>
258
+ {props.actionLabel}
259
+ </Button>
260
+ )}
261
+ </SettingsActions>
262
+ </SettingsRow>
263
+ );
264
+
164
265
  // The client's own switch structure around a native checkbox. Its semantic
165
266
  // classes are recreated by the CLI css-map, so Spotify and themes style this
166
267
  // exactly like the adjacent settings controls.
167
268
  export const Toggle: React.FC<{
168
269
  id?: string;
169
270
  ariaLabel?: string;
271
+ disabled?: boolean;
170
272
  value: boolean;
171
273
  onChange: (value: boolean) => void;
172
274
  }> = (props) => (
@@ -177,6 +279,7 @@ export const Toggle: React.FC<{
177
279
  id={props.id}
178
280
  aria-label={props.ariaLabel}
179
281
  checked={props.value}
282
+ disabled={props.disabled}
180
283
  onKeyDown={activateToggleOnKeyDown}
181
284
  onChange={(e) => props.onChange((e.target as HTMLInputElement).checked)}
182
285
  />
@@ -186,6 +289,46 @@ export const Toggle: React.FC<{
186
289
  </label>
187
290
  );
188
291
 
292
+ export const SettingsProviderRow: React.FC<{
293
+ label: string;
294
+ description?: React.ReactNode;
295
+ value: boolean;
296
+ disabled?: boolean;
297
+ disabledReason?: string;
298
+ index: number;
299
+ total: number;
300
+ onChange: (value: boolean) => void;
301
+ onMove: (direction: -1 | 1) => void;
302
+ }> = (props) => {
303
+ const id = React.useId();
304
+ const toggle = (
305
+ <Toggle
306
+ id={id}
307
+ ariaLabel={`${props.label} provider`}
308
+ value={props.value}
309
+ disabled={props.disabled}
310
+ onChange={props.onChange}
311
+ />
312
+ );
313
+ return (
314
+ <SettingsRow label={<SettingsLabel label={props.label} description={props.description} />} htmlFor={id}>
315
+ <SettingsActions>
316
+ <ReorderButtons
317
+ label={props.label}
318
+ disableUp={props.index === 0}
319
+ disableDown={props.index === props.total - 1}
320
+ onMove={props.onMove}
321
+ />
322
+ {props.disabled && props.disabledReason ? (
323
+ <Tooltip label={props.disabledReason}>{toggle}</Tooltip>
324
+ ) : (
325
+ toggle
326
+ )}
327
+ </SettingsActions>
328
+ </SettingsRow>
329
+ );
330
+ };
331
+
189
332
  // The "module with one boolean setting" pattern as a single row for the
190
333
  // `settingsRow` register. `getValue` is read lazily on every mount so
191
334
  // reopening settings always reflects the current state; the element itself
@@ -275,34 +418,41 @@ export const ConfirmButton: React.FC<{
275
418
  // transformed ancestor (the norm for Spotify's scroll containers) would
276
419
  // otherwise be clipped/offset to that ancestor. The backdrop and the ×
277
420
  // button both call onClose.
278
- export const Dialog: React.FC<{ title: string; onClose: () => void; children: React.ReactNode }> = (props) => {
279
- const { onClose } = props;
280
- // Escape closes the dialog, matching every native modal. A capturing
281
- // document listener catches the key wherever focus sits (the client's own
282
- // components steal focus), and the client's shortcut handlers do not see it.
421
+ export const Dialog: React.FC<{
422
+ title: string;
423
+ onClose: () => void;
424
+ size?: "normal" | "large";
425
+ children: React.ReactNode;
426
+ }> = (props) => {
427
+ const dialogRef = React.useRef<HTMLDivElement>(null);
428
+ const onCloseRef = React.useRef(props.onClose);
429
+ const titleId = React.useId();
430
+ onCloseRef.current = props.onClose;
283
431
  React.useEffect(() => {
284
- const onKey = (e: KeyboardEvent) => {
285
- if (e.key === "Escape") {
286
- e.preventDefault();
287
- e.stopPropagation();
288
- onClose();
289
- }
290
- };
291
- document.addEventListener("keydown", onKey, true);
292
- return () => document.removeEventListener("keydown", onKey, true);
293
- }, [onClose]);
432
+ const dialog = dialogRef.current;
433
+ if (!dialog) return;
434
+ return activateDialog(dialog, () => onCloseRef.current());
435
+ }, []);
436
+ const dialogClass = props.size === "large" ? `${DIALOG_CLASS} ${DIALOG_LARGE_CLASS}` : DIALOG_CLASS;
294
437
 
295
438
  return ReactDOM.createPortal(
296
439
  <div
297
440
  className={SCRIM_CLASS}
298
441
  onClick={(e) => {
299
- if (e.target === e.currentTarget) onClose();
442
+ if (e.target === e.currentTarget) props.onClose();
300
443
  }}
301
444
  >
302
- <div className={DIALOG_CLASS} role="dialog" aria-modal="true" aria-label={props.title}>
445
+ <div
446
+ ref={dialogRef}
447
+ className={dialogClass}
448
+ role="dialog"
449
+ aria-modal="true"
450
+ aria-labelledby={titleId}
451
+ tabIndex={-1}
452
+ >
303
453
  <div className={DIALOG_HEADER_CLASS}>
304
- <h2>{props.title}</h2>
305
- <IconButton ariaLabel="Close" onClick={onClose}>
454
+ <h2 id={titleId}>{props.title}</h2>
455
+ <IconButton ariaLabel="Close" onClick={props.onClose}>
306
456
  ×
307
457
  </IconButton>
308
458
  </div>
@@ -3,7 +3,7 @@
3
3
  "kind": "lib",
4
4
  "hidden": true,
5
5
  "preview": "./assets/PREVIEW.png",
6
- "version": "1.5.0",
6
+ "version": "1.10.0",
7
7
  "compat": ["0.3.0"],
8
8
  "authors": ["spicetify", "Delusoire"],
9
9
  "description": "The standard library",
@@ -18,9 +18,12 @@ export * from "./lib/primitives-vanilla.ts";
18
18
  export * from "./src/storage.ts";
19
19
  export * from "./src/wpunpk.ts";
20
20
  export * from "./src/logger.ts";
21
+ export { display as displayModal, hide as hideModal, type ModalOptions } from "./lib/modal.tsx";
22
+ export { React, ReactDOM, ReactDOMServer } from "./src/expose/React.ts";
23
+ export { Platform } from "./src/expose/Platform.ts";
24
+ export { createIconComponent } from "./src/createIconComponent.tsx";
25
+ export { startCase } from "./deps.ts";
21
26
 
22
- // Public barrel: everything dependent modules are allowed to deep-import.
23
- // Import from /modules/stdlib/mod.js at runtime; nothing else is public.
24
- export * from "./src/webpack/misc.xpui.ts";
25
- export * from "./src/webpack/ComponentLibrary.xpui.ts";
26
- export * from "./src/webpack/ReactComponents.xpui.ts";
27
+ // Palette Manager needs the client's Color constructor and its companion
28
+ // type. Export that semantic capability without exposing the webpack module.
29
+ export { Color } from "./src/webpack/misc.xpui.ts";
@@ -13,10 +13,28 @@
13
13
  * modules.
14
14
  */
15
15
 
16
+ import type { ModalOptions } from "../lib/modal.tsx";
17
+
16
18
  type SpicetifyRuntime = typeof Spicetify;
17
19
 
18
20
  export type PlayerState = Spicetify.PlayerState;
19
21
 
22
+ export interface PopupModalCompatibility {
23
+ display(options: ModalOptions): void;
24
+ hide(): void;
25
+ }
26
+
27
+ export interface DaemonCapabilities {
28
+ available(): Promise<boolean>;
29
+ apply(): Promise<unknown>;
30
+ blockUpdates(): Promise<unknown>;
31
+ unblockUpdates(): Promise<unknown>;
32
+ }
33
+
34
+ export interface SnackbarCapabilities {
35
+ enqueueSnackbar(message: string, options?: { variant?: string }): unknown;
36
+ }
37
+
20
38
  export interface ClientCapabilities {
21
39
  readonly player: typeof Spicetify.Player;
22
40
  readonly platform: typeof Spicetify.Platform;
@@ -30,9 +48,14 @@ export interface ClientCapabilities {
30
48
  readonly keyboard: typeof Spicetify.Keyboard;
31
49
  readonly mousetrap: typeof Spicetify.Mousetrap;
32
50
  readonly contextMenu: typeof Spicetify.ContextMenu;
33
- readonly popupModal: typeof Spicetify.PopupModal;
51
+ /** @deprecated Import displayModal and hideModal from /modules/stdlib/mod.ts. */
52
+ readonly popupModal: PopupModalCompatibility;
34
53
  readonly config: typeof Spicetify.Config;
35
54
  readonly modules: typeof Spicetify.Modules;
55
+ /** V3 manifest version, with the legacy wrapper config as a fallback. */
56
+ readonly spicetifyVersion: string | undefined;
57
+ readonly daemon: DaemonCapabilities | undefined;
58
+ readonly snackbar: SnackbarCapabilities | undefined;
36
59
  readonly react: typeof Spicetify.React;
37
60
  readonly reactDOM: typeof Spicetify.ReactDOM;
38
61
  readonly tippy: typeof Spicetify.Tippy;
@@ -47,6 +70,26 @@ const runtime = (): SpicetifyRuntime => {
47
70
  return value;
48
71
  };
49
72
 
73
+ const nonEmptyVersion = (value: unknown): string | undefined =>
74
+ typeof value === "string" && value.trim() ? value : undefined;
75
+
76
+ let modalRequestGeneration = 0;
77
+ const reportModalFailure = (error: unknown) => console.error("[stdlib] failed to load the owned modal:", error);
78
+ const popupModalCompatibility: PopupModalCompatibility = {
79
+ display(options) {
80
+ const generation = ++modalRequestGeneration;
81
+ void import("../lib/modal.tsx")
82
+ .then(({ display }) => {
83
+ if (generation === modalRequestGeneration) display(options);
84
+ })
85
+ .catch(reportModalFailure);
86
+ },
87
+ hide() {
88
+ ++modalRequestGeneration;
89
+ void import("../lib/modal.tsx").then(({ hide }) => hide()).catch(reportModalFailure);
90
+ },
91
+ };
92
+
50
93
  export const client: ClientCapabilities = {
51
94
  get player() {
52
95
  return runtime().Player;
@@ -85,7 +128,7 @@ export const client: ClientCapabilities = {
85
128
  return runtime().ContextMenu;
86
129
  },
87
130
  get popupModal() {
88
- return runtime().PopupModal;
131
+ return popupModalCompatibility;
89
132
  },
90
133
  get config() {
91
134
  return runtime().Config;
@@ -93,6 +136,26 @@ export const client: ClientCapabilities = {
93
136
  get modules() {
94
137
  return runtime().Modules;
95
138
  },
139
+ get spicetifyVersion() {
140
+ const current = runtime() as unknown as {
141
+ Modules?: {
142
+ manifest?: { cliVersion?: unknown };
143
+ registry?: { manifest?: { cliVersion?: unknown } };
144
+ };
145
+ Config?: { version?: unknown };
146
+ };
147
+ return (
148
+ nonEmptyVersion(current.Modules?.manifest?.cliVersion) ??
149
+ nonEmptyVersion(current.Modules?.registry?.manifest?.cliVersion) ??
150
+ nonEmptyVersion(current.Config?.version)
151
+ );
152
+ },
153
+ get daemon() {
154
+ return (runtime() as unknown as { Daemon?: DaemonCapabilities }).Daemon;
155
+ },
156
+ get snackbar() {
157
+ return (runtime() as unknown as { Snackbar?: SnackbarCapabilities }).Snackbar;
158
+ },
96
159
  get react() {
97
160
  return runtime().React;
98
161
  },
@@ -11,16 +11,22 @@ import menu from "./menu.ts";
11
11
  import { mountAdjacent } from "./mount.ts";
12
12
  import { isNativeAnchor, type NativeAnchor, resolveNativeAnchor } from "./nativeAnchors.ts";
13
13
  import navlink from "./navlink.tsx";
14
- import panel from "./panel.ts";
14
+ import panel, { type PanelController, type PanelRegistration } from "./panel.ts";
15
15
  import playbarButton, { PlaybarButton } from "./playbarButton.tsx";
16
16
  import playbarWidget from "./playbarWidget.tsx";
17
17
  import { Registry } from "./registry.ts";
18
18
  import root from "./root.ts";
19
19
  import route from "./route.ts";
20
20
  import settingsRow from "./settingsRow.tsx";
21
- import settingsSection from "./settingsSection.ts";
21
+ import settingsSection, { settingsAction } from "./settingsSection.ts";
22
22
  import topbarLeftButton, { TopbarLeftButton } from "./topbarLeftButton.tsx";
23
23
  import topbarRightButton, { TopbarRightButton } from "./topbarRightButton.tsx";
24
+ export type { PanelController, PanelRegistration, PanelWidth } from "./panel.ts";
25
+ export { closeMenu, openedFromProfileMenu, useMenuItem } from "./menu.ts";
26
+ export { NavLink, type NavLinkProps, useHistoryRefresh } from "./navlink.tsx";
27
+ export { PlaybarButton, type PlaybarButtonProps } from "./playbarButton.tsx";
28
+ export { TopbarLeftButton } from "./topbarLeftButton.tsx";
29
+ export { TopbarRightButton } from "./topbarRightButton.tsx";
24
30
  const [rootChild, rootProvider] = root;
25
31
  const registers = {
26
32
  menu,
@@ -32,12 +38,15 @@ const registers = {
32
38
  rootProvider,
33
39
  route,
34
40
  settingsRow,
41
+ settingsAction,
35
42
  settingsSection,
36
43
  topbarLeftButton,
37
44
  topbarRightButton,
38
45
  } satisfies Record<string, Registry<any>>;
39
46
  type Registers = typeof registers;
40
47
 
48
+ export { SPICETIFY_SETTINGS_ROUTE } from "./settingsSection.ts";
49
+
41
50
  // ---- placeButton: one ergonomic, position-controllable way to add a button ----
42
51
  // Instead of picking the right register key and hand-building the matching
43
52
  // component (easy to mismatch, no ordering), a module names a location and
@@ -52,7 +61,7 @@ export interface PlaceButtonOptions {
52
61
  label: string;
53
62
  /** Inner SVG markup drawn on the stdlib 16-grid. */
54
63
  icon?: string;
55
- onClick: () => void;
64
+ onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
56
65
  disabled?: boolean;
57
66
  /** Position within this slot's module buttons; lower renders earlier. Default 0. */
58
67
  order?: number;
@@ -73,6 +82,11 @@ export interface ButtonHandle {
73
82
  remove(): void;
74
83
  }
75
84
 
85
+ export interface RegisterPanelOptions extends Omit<PanelRegistration<React.ReactNode>, "id"> {
86
+ /** Stable within this module. Defaults to "default". */
87
+ id?: string;
88
+ }
89
+
76
90
  const BUTTON_SLOTS = {
77
91
  "topbar-left": ["topbarLeftButton", TopbarLeftButton],
78
92
  "topbar-right": ["topbarRightButton", TopbarRightButton],
@@ -139,6 +153,29 @@ export class Registrar {
139
153
  };
140
154
  }
141
155
 
156
+ /**
157
+ * Register a Spicetify-owned right-sidebar panel. The returned controller
158
+ * opens, closes and observes the panel; module unload disposes it.
159
+ */
160
+ registerPanel(options: RegisterPanelOptions): PanelController {
161
+ const registration: PanelRegistration<React.ReactNode> = {
162
+ ...options,
163
+ id: `${this.id}:${options.id ?? "default"}`,
164
+ };
165
+ panel.add(registration);
166
+ const controller = panel.controller(registration);
167
+ if (!controller) throw new Error(`[stdlib] failed to register panel: ${registration.id}`);
168
+ const dispose = () => panel.delete(registration);
169
+ this.disposers.add(dispose);
170
+ return {
171
+ ...controller,
172
+ dispose: () => {
173
+ this.disposers.delete(dispose);
174
+ dispose();
175
+ },
176
+ };
177
+ }
178
+
142
179
  register<R extends keyof Registers>(type: R, ...args: Parameters<Registers[R]["add"]>) {
143
180
  this.ledger.set(args[0], type);
144
181
  // @ts-ignore