diff --git a/.eslintrc.yaml b/.eslintrc.yaml
deleted file mode 100644
index b55284d8db0a1aff18183c1b968aab30e6fa604c..0000000000000000000000000000000000000000

--- a/.eslintrc.yaml
+++ /dev/null
@@ -1,30 +0,0 @@
-env:
-  '@beanbag/backbone': true
-  '@beanbag/django': true
-  browser: true
-  jquery: true
-
-
-extends:
-  - 'plugin:@beanbag/recommended'
-
-
-plugins:
-  - '@beanbag'
-
-
-parserOptions:
-  ecmaVersion: 2015
-
-  # All of our JavaScript will be compiled as modules, and then later turned
-  # into browser-safe JavaScript. We want to validate as modules when writing
-  # code.
-  sourceType: module
-
-
-globals:
-  Ink: writable
-
-
-rules:
-  'typescript-eslint/no-explicit-any': 'off'
diff --git a/babel.config.json b/babel.config.json
index c43543131f3ee57cdd6e2545f4b2fb8d42cb0019..02557124120d5b28d5f9eee31dbb37ad8b04db05 100644
--- a/babel.config.json
+++ b/babel.config.json
@@ -1,53 +1,64 @@
 {
     "env": {
         "development": {
-            "presets": [
-                "@babel/preset-env",
-                "@babel/preset-typescript"
-            ]
+            "plugins": [
+                "@babel/plugin-external-helpers",
+                [
+                    "htm",
+                    {
+                        "import": {
+                            "export": "craftComponent",
+                            "module": "@beanbag/ink"
+                        },
+                        "pragma": "craftComponent",
+                        "tag": "craft",
+                        "useBuiltins": true,
+                        "useNativeSpread": true
+                    },
+                    "htm-craft"
+                ],
+                [
+                    "htm", {
+                        "import": {
+                            "export": "paintComponent",
+                            "module": "@beanbag/ink"
+                        },
+                        "pragma": "paintComponent",
+                        "tag": "paint",
+                        "useBuiltins": true,
+                        "useNativeSpread": true
+                    },
+                    "htm-paint"
+                ]
+            ],
         }
     },
     "plugins": [
-        "@babel/plugin-external-helpers",
-        ["@babel/plugin-proposal-decorators", {
-            "version": "legacy"
-        }],
+        [
+            "@babel/plugin-proposal-decorators",
+            {
+                "version": "legacy"
+            }
+        ],
         "babel-plugin-dedent",
         "babel-plugin-django-gettext",
         [
-            "babel-plugin-transform-remove-imports", {
-                "test": [
-                    "babel-plugin-dedent",
-                    "babel-plugin-django-gettext"
-                ]
-            }
-        ],
+            "babel-plugin-transform-remove-imports",
+                {
+                    "test": [
+                        "babel-plugin-dedent",
+                        "babel-plugin-django-gettext"
+                    ]
+                }
+        ]
+    ],
+    "presets": [
+        "@babel/preset-env",
         [
-            "htm",
+            "@babel/preset-typescript",
             {
-                "import": {
-                    "export": "craftComponent",
-                    "module": "@beanbag/ink"
-                },
-                "pragma": "craftComponent",
-                "tag": "craft",
-                "useBuiltins": true,
-                "useNativeSpread": true
-            },
-            "htm-craft"
-        ],
-        [
-            "htm", {
-                "import": {
-                    "export": "paintComponent",
-                    "module": "@beanbag/ink"
-                },
-                "pragma": "paintComponent",
-                "tag": "paint",
-                "useBuiltins": true,
-                "useNativeSpread": true
-            },
-            "htm-paint"
+                "allowDeclareFields": true
+            }
         ]
     ],
     "sourceMaps": true,
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000000000000000000000000000000000000..8d6cbc78aae6844747c3cd829952716bbf4b40aa

--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,44 @@
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import { FlatCompat } from '@eslint/eslintrc';
+import js from '@eslint/js';
+import beanbag from '@beanbag/eslint-plugin';
+import { defineConfig, globalIgnores } from 'eslint/config';
+import globals from 'globals';
+
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const compat = new FlatCompat({
+    baseDirectory: __dirname,
+    recommendedConfig: js.configs.recommended,
+    allConfig: js.configs.all
+});
+
+export default defineConfig([
+    globalIgnores([
+        'src/@types/**/*',
+    ]),
+    {
+        extends: compat.extends('plugin:@beanbag/recommended'),
+        languageOptions: {
+            globals: {
+                ...beanbag.environments.backbone.globals,
+                ...beanbag.environments.django.globals,
+                ...globals.browser,
+                ...globals.jquery,
+                Ink: 'writable',
+            },
+
+            ecmaVersion: 2015,
+            sourceType: 'module',
+        },
+        plugins: {
+            '@beanbag': beanbag,
+        },
+        rules: {
+            '@typescript-eslint/no-explicit-any': 'off',
+        },
+    },
+]);
diff --git a/package.json b/package.json
index 20d1a803357540d85c3eec88065d2e8908f55eb5..286ed48c021249f6e53bc9d63dd986834169a8f2 100644
--- a/package.json
+++ b/package.json
@@ -23,15 +23,21 @@
         "build-storybook": "storybook build",
         "clean": "rm -rf lib _build",
         "full-build": "npm run clean && npm run build",
-        "prepare": "npm run build",
+        "lint": "eslint src",
         "storybook": "storybook dev -p 6006 --no-open",
-        "test": "cross-env NODE_ENV=test jasmine"
+        "test": "jasmine"
     },
     "main": "lib/ink.js",
     "exports": {
         ".": {
-            "import": "./lib/esm/index-all.js",
-            "require": "./lib/cjs/index.js"
+            "import": {
+                "types": "./lib/index.d.mts",
+                "default": "./lib/esm/index-all.js"
+            },
+            "require": {
+                "types": "./lib/index.d.ts",
+                "default": "./lib/cjs/index.js"
+            }
         },
         "./babel-preset": "./lib/babel-preset/index.js",
         "./lib/*.css": "./lib/*.css",
@@ -49,51 +55,43 @@
     "engines": {
         "node": ">=18.0.0"
     },
+    "dependencies": {
+        "@beanbag/spina": "^3.1.1",
+        "@types/jquery": "^3.5.33",
+        "babel-plugin-htm": "^3.0.0"
+    },
     "devDependencies": {
-        "@babel/plugin-external-helpers": "^7.23.3",
-        "@beanbag/eslint-plugin": "^2.0.0",
-        "@beanbag/frontend-buildkit": "^1.1.0",
-        "@beanbag/jasmine-suites": "^2.0.0",
-        "@rollup/plugin-alias": "^5.1.0",
-        "@storybook/addon-a11y": "^9.1.10",
-        "@storybook/addon-docs": "^9.1.10",
-        "@storybook/addon-links": "^9.1.10",
+        "@babel/plugin-external-helpers": "^7.27.1",
+        "@babel/register": "^7.28.3",
+        "@beanbag/frontend-buildkit": "^1.2.0",
+        "@beanbag/jasmine-suites": "^3.0.0",
+        "@beanbag/js-buildkit": "^1.0.3",
+        "@rollup/plugin-alias": "^5.1.1",
+        "@storybook/addon-a11y": "^9.1.13",
+        "@storybook/addon-docs": "^9.1.13",
+        "@storybook/addon-links": "^9.1.13",
         "@storybook/addon-styling-webpack": "^2.0.0",
-        "@storybook/addon-themes": "^9.1.10",
-        "@storybook/html-vite": "^9.1.10",
-        "@tabler/icons": "^3.5.0",
-        "@types/jasmine": "^5.1.4",
-        "@vitejs/plugin-react": "^4.3.2",
-        "babel-plugin-transform-remove-imports": "^1.8.0",
+        "@storybook/addon-themes": "^9.1.13",
+        "@storybook/html-vite": "^9.1.13",
+        "@tabler/icons": "^3.35.0",
+        "@types/jasmine": "^5.1.12",
+        "@vitejs/plugin-react": "^5.0.4",
+        "babel-plugin-transform-remove-imports": "^1.8.1",
         "clean-css-cli": "^5.6.3",
-        "cross-env": "^7.0.3",
-        "css-loader": "^6.10.0",
-        "eslint": "^8.56.0",
-        "jasmine": "^5.1.0",
+        "jasmine": "^5.12.0",
         "jasmine-spec-reporter": "^7.0.0",
         "jquery": "^3.7.1",
-        "jsdom": "^24.0.0",
-        "less": "^4.2.0",
-        "less-loader": "^12.2.0",
+        "jsdom": "^27.0.1",
+        "less": "^4.4.2",
+        "less-loader": "^12.3.0",
         "prop-types": "^15.8.1",
+        "resolve": "^1.22.11",
         "rollup-plugin-copy": "^3.5.0",
         "rollup-plugin-shell": "^1.0.9",
-        "storybook": "^9.1.10",
-        "style-loader": "^3.3.4",
-        "ts-node": "^10.9.2",
-        "typescript": "^5.3.3"
+        "storybook": "^9.1.13"
     },
     "repository": {
         "type": "git",
         "url": "https://github.com/beanbaginc/ink"
-    },
-    "dependencies": {
-        "@beanbag/spina": "^3.1.1",
-        "babel-plugin-htm": "^3.0.0"
-    },
-    "overrides": {
-        "@types/node": "22.10.2",
-        "@types/react": "^18",
-        "globby": "11.0.4"
     }
 }
diff --git a/rollup.config.js b/rollup.config.js
index d586d03687e56356b861408b8054889710ea7128..f37cccf8de1dec23a200d6e7bf0cc3804860b4e8 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -1,11 +1,12 @@
 import path from 'path';
+import resolve from 'resolve/sync';
 
 import alias from '@rollup/plugin-alias';
 import babel from '@rollup/plugin-babel';
+import rollupResolve from '@rollup/plugin-node-resolve';
 import copy from 'rollup-plugin-copy';
 import dts from 'rollup-plugin-dts';
 import execute from 'rollup-plugin-shell';
-import resolve from '@rollup/plugin-node-resolve';
 
 
 const extensions = ['.ts'];
@@ -19,11 +20,18 @@
 
 
 const inkPath = path.resolve(__dirname, 'src', 'ink');
+const tablerPath =
+    path.dirname(
+        resolve('@tabler/icons/icons.json',
+                { basedir: __dirname })
+        .replace('@', '\\@')
+    );
 const lessArgs = [
     '--source-map',
     '--include-path=src:node_modules',
     '--quiet',
     `--modify-var="ink-path=${inkPath}"`,
+    `--modify-var="tabler-path=${tablerPath}"`,
 ].join(' ');
 const cleanCSSArgs = [
     '-O2',
@@ -47,7 +55,7 @@
             {
                 esModule: false,
                 exports: 'named',
-                file: `lib/ink.js`,
+                file: 'lib/ink.js',
                 format: 'umd',
                 globals: globalsMap,
                 name: 'Ink',
@@ -73,8 +81,7 @@
                 entries: [
                     {
                         find: '@beanbag/ink',
-                        replacement: path.resolve(
-                            __dirname, 'src', 'ink', 'js'),
+                        replacement: path.resolve(inkPath, 'js'),
                     },
                 ],
             }),
@@ -82,7 +89,7 @@
                 babelHelpers: 'external',
                 extensions: extensions,
             }),
-            resolve({
+            rollupResolve({
                 extensions: extensions,
                 modulePaths: [],
             }),
@@ -109,12 +116,16 @@
         ],
     },
     {
-        input: './_build/_ts/index.d.ts',
+        input: './_build/dts/index.d.ts',
         output: [
             {
                 file: 'lib/index.d.ts',
                 format: 'es',
             },
+            {
+                file: 'lib/index.d.mts',
+                format: 'es',
+            },
         ],
         plugins: [
             dts.default(),
diff --git a/spec/helpers/babel.js b/spec/helpers/babel.js
new file mode 100644
index 0000000000000000000000000000000000000000..be73d82fcb46b08b60514037b90493c0e5a80cee

--- /dev/null
+++ b/spec/helpers/babel.js
@@ -0,0 +1,4 @@
+require('@babel/register')({
+    envName: 'test',
+    extensions: ['.js', '.ts'],
+});
diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json
index c8dcec6ca05f48d78139c9d33f74d734764b6836..9670f379c4ede31991af2d99a51a337d07c50678 100644
--- a/spec/support/jasmine.json
+++ b/spec/support/jasmine.json
@@ -1,12 +1,13 @@
 {
     "env": {
         "stopSpecOnExpectationFailure": false,
-        "random": true
+        "random": false
     },
     "helpers": [
-        "../node_modules/ts-node/register/type-check.js",
+        "../spec/helpers/babel.js",
         "../spec/helpers/*.ts"
     ],
+    "jsLoader": "require",
     "reporters": [
         {
             "name": "jasmine-spec-reporter#SpecReporter",
diff --git a/src/@types/backbone/index.d.ts b/src/@types/backbone/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8d95dc134df6a6e9729ac3f5cc658135574ddf45

--- /dev/null
+++ b/src/@types/backbone/index.d.ts
@@ -0,0 +1,688 @@
+/// <reference types="jquery" />
+/// <reference types="underscore" />
+
+export = Backbone;
+export as namespace Backbone;
+
+declare namespace Backbone {
+    type _Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
+    type _Result<T> = T | (() => T);
+    type _StringKey<T> = keyof T & string;
+    type _NoInfer<T> = T extends infer S ? S : never;
+
+    interface AddOptions extends Silenceable {
+        at?: number | undefined;
+        merge?: boolean | undefined;
+        sort?: boolean | undefined;
+    }
+
+    interface CollectionSetOptions extends Parseable, Silenceable {
+        add?: boolean | undefined;
+        remove?: boolean | undefined;
+        merge?: boolean | undefined;
+        at?: number | undefined;
+        sort?: boolean | undefined;
+    }
+
+    type CollectionComparator<TModel extends (Model | undefined)> =
+        | string
+        | { bivarianceHack(element: TModel): number | string }['bivarianceHack']
+        | { bivarianceHack(compare: TModel, to?: TModel): number }['bivarianceHack'];
+
+    interface CollectionOptions<
+        TModel extends (Model | undefined) = Model
+    > extends CollectionSetOptions {
+        comparator?: CollectionComparator<TModel>;
+        model?: new (...args: any[]) => TModel;
+    }
+
+    type CombinedCollectionConstructorOptions<
+        TExtraCollectionOptions,
+        TModel extends (Model | undefined) = Model,
+    > = CollectionOptions<TModel> & TExtraCollectionOptions;
+
+    type CombinedRouterConstructorOptions<
+        TExtraRouterOptions,
+        TOptions extends RouterOptions = RouterOptions,
+    > = TOptions & TExtraRouterOptions;
+
+    interface HistoryOptions extends Silenceable {
+        pushState?: boolean | undefined;
+        root?: string | undefined;
+        hashChange?: boolean | undefined;
+    }
+
+    interface NavigateOptions {
+        trigger?: boolean | undefined;
+        replace?: boolean | undefined;
+    }
+
+    interface RouterOptions {
+        routes?: _Result<RoutesHash>;
+    }
+
+    interface Silenceable {
+        silent?: boolean | undefined;
+    }
+
+    interface Validable {
+        validate?: boolean | undefined;
+    }
+
+    interface Waitable {
+        wait?: boolean | undefined;
+    }
+
+    interface Parseable {
+        parse?: boolean | undefined;
+    }
+
+    interface PersistenceOptions extends Partial<_Omit<JQueryAjaxSettings, "success" | "error">> {
+        // TODO: Generalize modelOrCollection
+        success?: ((modelOrCollection: any, response: any, options: any) => void) | undefined;
+        error?: ((modelOrCollection: any, response: any, options: any) => void) | undefined;
+        emulateJSON?: boolean | undefined;
+        emulateHTTP?: boolean | undefined;
+    }
+
+    interface ModelConstructorOptions<TModel extends Model = Model> extends ModelSetOptions, Parseable {
+        collection?: Collection<TModel> | undefined;
+    }
+
+    type CombinedModelConstructorOptions<E, M extends Model<any, any, E> = Model> = ModelConstructorOptions<M> & E;
+
+    interface ModelSetOptions extends Silenceable, Validable {}
+
+    interface ModelFetchOptions extends PersistenceOptions, ModelSetOptions, Parseable {}
+
+    interface ModelSaveOptions extends Silenceable, Waitable, Validable, Parseable, PersistenceOptions {
+        patch?: boolean | undefined;
+    }
+
+    interface ModelDestroyOptions extends Waitable, PersistenceOptions {}
+
+    interface CollectionFetchOptions extends PersistenceOptions, Parseable, CollectionSetOptions {
+        reset?: boolean | undefined;
+    }
+
+    type ObjectHash = Record<string, any>;
+
+    interface RoutesHash {
+        [routePattern: string]: string | { (...urlParts: string[]): void };
+    }
+
+    /**
+     * DOM events (used in the events property of a View)
+     */
+    interface EventsHash {
+        [selector: string]: string | { (eventObject: JQuery.TriggeredEvent): void };
+    }
+
+    /**
+     * JavaScript events (used in the methods of the Events interface)
+     */
+    interface EventHandler {
+        (...args: any[]): void;
+    }
+    interface EventMap {
+        [event: string]: EventHandler;
+    }
+
+    const Events: Events;
+    interface Events extends EventsMixin {}
+
+    /**
+     * Helper shorthands for classes that implement the Events interface.
+     * Define your class like this:
+     *
+     * import {
+     *     Events,
+     *     Events_On,
+     *     Events_Off,
+     *     Events_Trigger,
+     *     Events_Listen,
+     *     Events_Stop,
+     * } from 'backbone';
+     *
+     * class YourClass implements Events {
+     *     on: Events_On<YourClass>;
+     *     off: Events_Off<YourClass>;
+     *     trigger: Events_Trigger<YourClass>;
+     *     bind: Events_On<YourClass>;
+     *     unbind: Events_Off<YourClass>;
+     *
+     *     once: Events_On<YourClass>;
+     *     listenTo: Events_Listen<YourClass>;
+     *     listenToOnce: Events_Listen<YourClass>;
+     *     stopListening: Events_Stop<YourClass>;
+     *
+     *     // ... (other methods)
+     * }
+     *
+     * Object.assign(YourClass.prototype, Events);  // can also use _.extend
+     *
+     * If you are just writing a class type declaration that doesn't already
+     * extend some other base class, you can use the EventsMixin instead;
+     * see below.
+     */
+    interface Events_On<BaseT> {
+        <T extends BaseT>(this: T, eventName: string, callback: EventHandler, context?: any): T;
+        <T extends BaseT>(this: T, eventMap: EventMap, context?: any): T;
+    }
+    interface Events_Off<BaseT> {
+        <T extends BaseT>(this: T, eventName?: string | null, callback?: EventHandler | null, context?: any): T;
+        <T extends BaseT>(this: T, eventMap: EventMap, context?: any): T;
+    }
+    interface Events_Trigger<BaseT> {
+        <T extends BaseT>(this: T, eventName: string, ...args: any[]): T;
+    }
+    interface Events_Listen<BaseT> {
+        <T extends BaseT>(this: T, object: any, events: string, callback: EventHandler): T;
+        <T extends BaseT>(this: T, object: any, eventMap: EventMap): T;
+    }
+    interface Events_Stop<BaseT> {
+        <T extends BaseT>(this: T, object?: any, events?: string, callback?: EventHandler): T;
+        <T extends BaseT>(this: T, object: any, eventMap: EventMap): T;
+    }
+
+    /**
+     * Helper to avoid code repetition in type declarations.
+     * Backbone.Events cannot be extended, hence a separate abstract
+     * class with a different name. Both classes and interfaces can
+     * extend from this helper class to reuse the signatures.
+     *
+     * For class type declarations that already extend another base
+     * class, and for actual class definitions, please see the
+     * Events_* interfaces above.
+     */
+    abstract class EventsMixin implements Events {
+        on(eventName: string, callback: EventHandler, context?: any): this;
+        on(eventMap: EventMap, context?: any): this;
+        off(eventName?: string | null, callback?: EventHandler | null, context?: any): this;
+        off(eventMap: EventMap, context?: any): this;
+        trigger(eventName: string, ...args: any[]): this;
+        bind(eventName: string, callback: EventHandler, context?: any): this;
+        bind(eventMap: EventMap, context?: any): this;
+        unbind(eventName?: string, callback?: EventHandler, context?: any): this;
+
+        once(events: string, callback: EventHandler, context?: any): this;
+        once(eventMap: EventMap, context?: any): this;
+        listenTo(object: any, events: string, callback: EventHandler): this;
+        listenTo(object: any, eventMap: EventMap): this;
+        listenToOnce(object: any, events: string, callback: EventHandler): this;
+        listenToOnce(object: any, eventMap: EventMap): this;
+        stopListening(object?: any, events?: string, callback?: EventHandler): this;
+        stopListening(object: any, eventMap: EventMap): this;
+    }
+
+    class ModelBase extends EventsMixin {
+        parse(response: any, options?: any): any;
+        toJSON(options?: any): any;
+        sync(...arg: any[]): JQueryXHR;
+    }
+
+    /**
+     * E - Extensions to the model constructor options. You can accept additional constructor options
+     * by listing them in the E parameter.
+     */
+    class Model<T extends ObjectHash = any, S = ModelSetOptions, E = any> extends ModelBase implements Events {
+        /**
+         * Do not use, prefer TypeScript's extend functionality.
+         */
+        static extend(properties: any, classProperties?: any): any;
+
+        attributes: Partial<T>;
+        changed: Partial<T>;
+        cidPrefix: string;
+        cid: string;
+        collection: Collection<this>;
+
+        private _changing: boolean;
+        private _previousAttributes: Partial<T>;
+        private _pending: boolean;
+
+        /**
+         * Default attributes for the model. It can be an object hash or a method returning an object hash.
+         * For assigning an object hash, do it like this: this.defaults = <any>{ attribute: value, ... };
+         * That works only if you set it in the constructor or the initialize method.
+         */
+        defaults: _Result<Partial<T>>;
+        id: string | number;
+        idAttribute: string;
+        validationError: any;
+
+        /**
+         * Returns the relative URL where the model's resource would be located on the server.
+         */
+        url: _Result<string>;
+
+        urlRoot: _Result<string>;
+
+        /**
+         * For use with models as ES classes. If you define a preinitialize
+         * method, it will be invoked when the Model is first created, before
+         * any instantiation logic is run for the Model.
+         * @see https://backbonejs.org/#Model-preinitialize
+         */
+        preinitialize(
+            attributes?: Partial<T>,
+            options?: CombinedModelConstructorOptions<E, this>,
+        ): void;
+
+        constructor(
+            attributes?: _NoInfer<Partial<T>>,
+            options?: CombinedModelConstructorOptions<_NoInfer<E>>,
+        );
+
+        initialize(
+            attributes?: Partial<T>,
+            options?: CombinedModelConstructorOptions<E, this>,
+        ): void;
+
+        fetch(options?: ModelFetchOptions): JQueryXHR;
+
+        /**
+         * For strongly-typed access to attributes, use the `get` method only privately in public getter properties.
+         * @example
+         * get name(): string {
+         *    return super.get("name");
+         * }
+         */
+        get<A extends _StringKey<T>>(attributeName: A): T[A] | undefined;
+
+        /**
+         * For strongly-typed assignment of attributes, use the `set` method only privately in public setter properties.
+         * @example
+         * set name(value: string) {
+         *    super.set("name", value);
+         * }
+         */
+        set<A extends _StringKey<T>>(attributeName: A, value?: T[A], options?: S): this;
+        set(attributeName: Partial<T>, options?: S): this;
+        set<A extends _StringKey<T>>(attributeName: A | Partial<T>, value?: T[A] | S, options?: S): this;
+
+        /**
+         * Return an object containing all the attributes that have changed, or
+         * false if there are no changed attributes. Useful for determining what
+         * parts of a view need to be updated and/or what attributes need to be
+         * persisted to the server. Unset attributes will be set to undefined.
+         * You can also pass an attributes object to diff against the model,
+         * determining if there *would be* a change.
+         */
+        changedAttributes(attributes?: Partial<T>): Partial<T> | false;
+        clear(options?: Silenceable): this;
+        clone(): Model;
+        destroy(options?: ModelDestroyOptions): JQueryXHR | false;
+        escape(attribute: _StringKey<T>): string;
+        has(attribute: _StringKey<T>): boolean;
+        hasChanged(attribute?: _StringKey<T>): boolean;
+        isNew(): boolean;
+        isValid(options?: any): boolean;
+        previous<A extends _StringKey<T>>(attribute: A): T[A] | null | undefined;
+        previousAttributes(): Partial<T>;
+        save(attributes?: Partial<T> | null, options?: ModelSaveOptions): JQueryXHR;
+        unset(attribute: _StringKey<T>, options?: Silenceable): this;
+        validate(attributes: Partial<T>, options?: any): any;
+        private _validate(attributes: Partial<T>, options: any): boolean;
+
+        // mixins from underscore
+
+        keys(): string[];
+        values(): any[];
+        pairs(): any[];
+        invert(): any;
+        pick<A extends _StringKey<T>>(keys: A[]): Partial<Pick<T, A>>;
+        pick<A extends _StringKey<T>>(...keys: A[]): Partial<Pick<T, A>>;
+        pick(fn: (value: any, key: any, object: any) => any): Partial<T>;
+        omit<A extends _StringKey<T>>(keys: A[]): Partial<_Omit<T, A>>;
+        omit<A extends _StringKey<T>>(...keys: A[]): Partial<_Omit<T, A>>;
+        omit(fn: (value: any, key: any, object: any) => any): Partial<T>;
+        chain(): any;
+        isEmpty(): boolean;
+        matches(attrs: any): boolean;
+    }
+
+    class Collection<
+        TModel extends Model = Model,
+        TExtraCollectionOptions = unknown,
+        TCollectionOptions = CollectionOptions<TModel>
+    > extends ModelBase implements Events {
+        /**
+         * Do not use, prefer TypeScript's extend functionality.
+         */
+        static extend(properties: any, classProperties?: any): any;
+
+        model: (new(...args: any[]) => TModel) | ((...args: any[]) => TModel);
+        models: TModel[];
+        length: number;
+
+        /**
+         * For use with collections as ES classes. If you define a preinitialize
+         * method, it will be invoked when the Collection is first created and
+         * before any instantiation logic is run for the Collection.
+         * @see https://backbonejs.org/#Collection-preinitialize
+         */
+        preinitialize(
+            models?: TModel[] | Array<Record<string, any>>,
+            options?: CombinedCollectionConstructorOptions<
+                TExtraCollectionOptions,
+                TModel
+            >,
+        ): void;
+
+        constructor(
+            models?: TModel[] | Array<Record<string, any>>,
+            options?: CombinedCollectionConstructorOptions<
+                _NoInfer<TExtraCollectionOptions>,
+                TModel
+            >,
+        );
+
+        initialize(
+            models?: TModel[] | Array<Record<string, any>>,
+            options?: CombinedCollectionConstructorOptions<
+                TExtraCollectionOptions,
+                TModel
+            >,
+        ): void;
+
+        fetch(options?: CollectionFetchOptions): JQueryXHR;
+
+        /**
+         * Specify a model attribute name (string) or function that will be used to sort the collection.
+         */
+        comparator: CollectionComparator<TModel>;
+
+        add(models: Array<{} | TModel>, options?: AddOptions): TModel[];
+        add(model: {} | TModel, options?: AddOptions): TModel;
+        at(index: number): TModel;
+        /**
+         * Get a model from a collection, specified by an id, a cid, or by passing in a model.
+         */
+        get(id: number | string | Model): TModel;
+        has(key: number | string | Model): boolean;
+        clone(): this;
+        create(attributes: any, options?: ModelSaveOptions): TModel;
+        pluck(attribute: string): any[];
+        push(model: TModel, options?: AddOptions): TModel;
+        pop(options?: Silenceable): TModel;
+        remove(model: {} | TModel, options?: Silenceable): TModel;
+        remove(models: Array<{} | TModel>, options?: Silenceable): TModel[];
+        reset(models?: Array<{} | TModel>, options?: Silenceable): TModel[];
+
+        /**
+         * The set method performs a "smart" update of the collection with the passed list of models.
+         * If a model in the list isn't yet in the collection it will be added; if the model is already in the
+         * collection its attributes will be merged; and if the collection contains any models that aren't present
+         * in the list, they'll be removed. All of the appropriate "add", "remove", and "change" events are fired as
+         * this happens. Returns the touched models in the collection. If you'd like to customize the behavior, you can
+         * disable it with options: {add: false}, {remove: false}, or {merge: false}.
+         * @param models
+         * @param options
+         */
+        set(models?: Array<{} | TModel>, options?: CollectionSetOptions): TModel[];
+        shift(options?: Silenceable): TModel;
+        sort(options?: Silenceable): this;
+        unshift(model: TModel, options?: AddOptions): TModel;
+        where(properties: any): TModel[];
+        findWhere(properties: any): TModel;
+        modelId(attrs: any): any;
+
+        values(): Iterator<TModel>;
+        keys(): Iterator<any>;
+        entries(): Iterator<[any, TModel]>;
+        [Symbol.iterator](): Iterator<TModel>;
+
+        private _prepareModel(attributes?: any, options?: any): any;
+        private _removeReference(model: TModel): void;
+        private _onModelEvent(event: string, model: TModel, collection: Collection<TModel>, options: any): void;
+        private _isModel(obj: any): obj is Model;
+
+        /**
+         * Return a shallow copy of this collection's models, using the same options as native Array#slice.
+         */
+        slice(min?: number, max?: number): TModel[];
+
+        // mixins from underscore
+
+        all(iterator?: _.ListIterator<TModel, boolean>, context?: any): boolean;
+        any(iterator?: _.ListIterator<TModel, boolean>, context?: any): boolean;
+        chain(): any;
+        collect<TResult>(iterator: _.ListIterator<TModel, TResult>, context?: any): TResult[];
+        contains(value: TModel): boolean;
+        countBy(iterator?: _.ListIterator<TModel, any>): _.Dictionary<number>;
+        countBy(iterator: string): _.Dictionary<number>;
+        detect(iterator: _.ListIterator<TModel, boolean>, context?: any): TModel;
+        difference(others: TModel[]): TModel[];
+        drop(n?: number): TModel[];
+        each(iterator: _.ListIterator<TModel, void>, context?: any): TModel[];
+        every(iterator: _.ListIterator<TModel, boolean>, context?: any): boolean;
+        filter(iterator: _.ListIterator<TModel, boolean>, context?: any): TModel[];
+        find(iterator: _.ListIterator<TModel, boolean>, context?: any): TModel;
+        findIndex(predicate: _.ListIterator<TModel, boolean>, context?: any): number;
+        findLastIndex(predicate: _.ListIterator<TModel, boolean>, context?: any): number;
+        first(): TModel;
+        first(n: number): TModel[];
+        foldl<TResult>(iterator: _.MemoIterator<TModel, TResult>, memo?: TResult, context?: any): TResult;
+        foldr<TResult>(iterator: _.MemoIterator<TModel, TResult>, memo?: TResult, context?: any): TResult;
+        forEach(iterator: _.ListIterator<TModel, void>, context?: any): TModel[];
+        groupBy(iterator: _.ListIterator<TModel, any> | string, context?: any): _.Dictionary<TModel[]>;
+        head(): TModel;
+        head(n: number): TModel[];
+        include(value: TModel): boolean;
+        includes(value: TModel): boolean;
+        indexBy(iterator: _.ListIterator<TModel, any>, context?: any): _.Dictionary<TModel>;
+        indexBy(iterator: string, context?: any): _.Dictionary<TModel>;
+        indexOf(value: TModel, isSorted?: boolean): number;
+        initial(): TModel;
+        initial(n: number): TModel[];
+        inject<TResult>(iterator: _.MemoIterator<TModel, TResult>, memo?: TResult, context?: any): TResult;
+        invoke(methodName: string, ...args: any[]): any;
+        isEmpty(): boolean;
+        last(): TModel;
+        last(n: number): TModel[];
+        lastIndexOf(value: TModel, from?: number): number;
+        map<TResult>(iterator: _.ListIterator<TModel, TResult>, context?: any): TResult[];
+        max(iterator?: _.ListIterator<TModel, any>, context?: any): TModel;
+        min(iterator?: _.ListIterator<TModel, any>, context?: any): TModel;
+        partition(iterator: _.ListIterator<TModel, boolean>): TModel[][];
+        reduce<TResult>(iterator: _.MemoIterator<TModel, TResult>, memo?: TResult, context?: any): TResult;
+        reduceRight<TResult>(iterator: _.MemoIterator<TModel, TResult>, memo?: TResult, context?: any): TResult;
+        reject(iterator: _.ListIterator<TModel, boolean>, context?: any): TModel[];
+        rest(n?: number): TModel[];
+        sample(): TModel;
+        sample(n: number): TModel[];
+        select(iterator: _.ListIterator<TModel, boolean>, context?: any): TModel[];
+        shuffle(): TModel[];
+        size(): number;
+        some(iterator?: _.ListIterator<TModel, boolean>, context?: any): boolean;
+        sortBy(iterator?: _.ListIterator<TModel, any>, context?: any): TModel[];
+        sortBy(iterator: string, context?: any): TModel[];
+        tail(n?: number): TModel[];
+        take(): TModel;
+        take(n: number): TModel[];
+        toArray(): TModel[];
+
+        /**
+         * Sets the url property (or function) on a collection to reference its location on the server.
+         */
+        url: _Result<string>;
+
+        without(...values: TModel[]): TModel[];
+    }
+
+    type RouterCallback = (...args: string[]) => void;
+
+    class Router<
+        TExtraRouterOptions = unknown,
+        TOptions extends RouterOptions = RouterOptions
+    > extends EventsMixin implements Events {
+        /**
+         * Do not use, prefer TypeScript's extend functionality.
+         */
+        static extend(properties: any, classProperties?: any): any;
+
+        /**
+         * Routes hash or a method returning the routes hash that maps URLs with parameters to methods on your Router.
+         * For assigning routes as object hash, do it like this: this.routes = <any>{ "route": callback, ... };
+         * That works only if you set it in the constructor or the initialize method.
+         */
+        routes: _Result<RoutesHash>;
+
+        /**
+         * For use with Router as ES classes. If you define a preinitialize method,
+         * it will be invoked when the Router is first created, before any
+         * instantiation logic is run for the Router.
+         * @see https://backbonejs.org/#Router-preinitialize
+         */
+        preinitialize(
+            options?: CombinedRouterConstructorOptions<TExtraRouterOptions, TOptions>,
+        ): void;
+
+        constructor(
+            options?: CombinedRouterConstructorOptions<
+                _NoInfer<TExtraRouterOptions>,
+                _NoInfer<TOptions>
+            >,
+        );
+
+        initialize(
+            options?: CombinedRouterConstructorOptions<TExtraRouterOptions, TOptions>,
+        ): void;
+
+        route(route: string | RegExp, name: string, callback?: RouterCallback): this;
+        route(route: string | RegExp, callback: RouterCallback): this;
+        navigate(fragment: string, options?: NavigateOptions | boolean): this;
+
+        execute(callback: RouterCallback, args: string[], name: string): void;
+
+        protected _bindRoutes(): void;
+        protected _routeToRegExp(route: string): RegExp;
+        protected _extractParameters(route: RegExp, fragment: string): string[];
+    }
+
+    const history: History;
+
+    class History extends EventsMixin implements Events {
+        handlers: any[];
+        interval: number;
+
+        start(options?: HistoryOptions): boolean;
+
+        getHash(window?: Window): string;
+        getFragment(fragment?: string): string;
+        decodeFragment(fragment: string): string;
+        getSearch(): string;
+        stop(): void;
+        route(route: string | RegExp, callback: (fragment: string) => void): number;
+        checkUrl(e?: any): void;
+        getPath(): string;
+        matchRoot(): boolean;
+        atRoot(): boolean;
+        loadUrl(fragmentOverride?: string): boolean;
+        navigate(fragment: string, options?: any): boolean;
+        static started: boolean;
+        options: any;
+
+        private _updateHash(location: Location, fragment: string, replace: boolean): void;
+    }
+
+    interface ViewOptions<TModel extends (Model | undefined) = Model, TElement extends Element = HTMLElement> {
+        model?: TModel | undefined;
+        // TODO: quickfix, this can't be fixed easy. The collection does not need to have the same model as the parent view.
+        collection?: Collection<any> | undefined; // was: Collection<TModel>;
+        el?: TElement | JQuery | string | undefined;
+        id?: string | undefined;
+        attributes?: Record<string, any> | undefined;
+        className?: string | undefined;
+        tagName?: string | undefined;
+        events?: _Result<EventsHash> | undefined;
+    }
+
+    type CombinedViewConstructorOptions<
+        TExtraViewOptions,
+        TModel extends (Model | undefined) = Model,
+        TElement extends Element = HTMLElement,
+    > = ViewOptions<TModel, TElement> & TExtraViewOptions;
+
+    type ViewEventListener = (event: JQuery.Event) => void;
+
+    class View<
+        TModel extends (Model | undefined) = Model,
+        TElement extends Element = HTMLElement,
+        TExtraViewOptions = unknown,
+        TViewOptions = ViewOptions
+    > extends EventsMixin implements Events {
+        /**
+         * Do not use, prefer TypeScript's extend functionality.
+         */
+        static extend(properties: any, classProperties?: any): any;
+
+        /**
+         * For use with views as ES classes. If you define a preinitialize
+         * method, it will be invoked when the view is first created, before any
+         * instantiation logic is run.
+         * @see https://backbonejs.org/#View-preinitialize
+         */
+        preinitialize(
+            options?: CombinedViewConstructorOptions<
+                TExtraViewOptions, TModel, TElement
+            >,
+        ): void;
+
+        constructor(
+            options?: CombinedViewConstructorOptions<
+                _NoInfer<TExtraViewOptions>, TModel, TElement
+            >,
+        );
+
+        initialize(
+            options?: CombinedViewConstructorOptions<
+                TExtraViewOptions, TModel, TElement
+            >,
+        ): void;
+
+        /**
+         * Events hash or a method returning the events hash that maps events/selectors to methods on your View.
+         * For assigning events as object hash, do it like this: this.events = <any>{ "event:selector": callback, ... };
+         * That works only if you set it in the constructor or the initialize method.
+         */
+        events: _Result<EventsHash>;
+
+        // A conditional type used here to prevent `TS2532: Object is possibly 'undefined'`
+        model: TModel extends Model ? TModel : undefined;
+        collection: Collection<any>;
+        setElement(element: TElement | JQuery): this;
+        id?: _Result<string | undefined>;
+        cid: string;
+        className?: _Result<string | undefined>;
+        tagName: _Result<string>;
+
+        el: TElement;
+        $el: JQuery;
+        attributes: Record<string, any>;
+        $(selector: string): JQuery;
+        render(): this;
+        remove(): this;
+        delegateEvents(events?: _Result<EventsHash>): this;
+        delegate(eventName: string, selector: string, listener: ViewEventListener): this;
+        undelegateEvents(): this;
+        undelegate(eventName: string, selector?: string, listener?: ViewEventListener): this;
+
+        protected _removeElement(): void;
+        protected _setElement(el: TElement | JQuery): void;
+        protected _createElement(tagName: string): void;
+        protected _ensureElement(): void;
+        protected _setAttributes(attributes: Record<string, any>): void;
+    }
+
+    // SYNC
+    function sync(method: string, model: Model | Collection, options?: JQueryAjaxSettings): any;
+    function ajax(options?: JQueryAjaxSettings): JQueryXHR;
+    let emulateHTTP: boolean;
+    let emulateJSON: boolean;
+
+    // Utility
+    function noConflict(): typeof Backbone;
+    let $: JQueryStatic;
+}
diff --git a/src/ink/js/components/models/menuItemModel.ts b/src/ink/js/components/models/menuItemModel.ts
index 77e35d258991c11e835bf518e2ecbdfc564100e2..ecd3d91c44873639258bd767c8f19bd8058de61c 100644
--- a/src/ink/js/components/models/menuItemModel.ts
+++ b/src/ink/js/components/models/menuItemModel.ts
@@ -11,7 +11,7 @@
 } from '@beanbag/spina';
 
 import { KeyboardShortcutRegistry } from '../../foundation';
-import { MenuItemsRadioGroup } from '../collections/menuItemsRadioGroup';
+import { type MenuItemsRadioGroup } from '../collections/menuItemsRadioGroup';
 
 
 /**
diff --git a/src/ink/js/components/views/menuView.ts b/src/ink/js/components/views/menuView.ts
index e70dbf4b27fc725345c35e1352215929a0aca918..8538a5a69ba0840803c40f06517c7f6171232a3b 100644
--- a/src/ink/js/components/views/menuView.ts
+++ b/src/ink/js/components/views/menuView.ts
@@ -186,6 +186,8 @@
     HTMLMenuElement,
     TOptions
 > {
+    declare ['constructor']: typeof MenuView;
+
     static tagName = 'menu';
     static className = 'ink-c-menu';
 
@@ -371,8 +373,10 @@
         const currentItemIndex = options.currentItemIndex ?? null;
 
         /* Close any existing menu. */
-        if (MenuView.openedMenu !== null) {
-            MenuView.openedMenu.close({
+        const cls = this.constructor;
+
+        if (cls.openedMenu !== null) {
+            cls.openedMenu.close({
                 animate: false,
             });
         }
@@ -395,7 +399,7 @@
             this.setCurrentItem(currentItemIndex);
         }
 
-        MenuView.openedMenu = this;
+        cls.openedMenu = this;
 
         /*
          * Set up a handler so any click on the document will close the menu.
@@ -497,7 +501,9 @@
         el.tabIndex = -1;
 
         document.removeEventListener('click', this.#onDocClick);
-        MenuView.openedMenu = null;
+
+        this.constructor.openedMenu = null;
+
         this.isStickyOpen = null;
 
         if (triggerEvents) {
@@ -1509,6 +1515,8 @@
  */
 @spina
 class BaseCheckableMenuItemView extends MenuItemView {
+    declare ['constructor']: typeof BaseCheckableMenuItemView;
+
     static modelEvents: EventsHash = {
         'change:checked': '_onCheckedChanged',
     };
@@ -1568,7 +1576,7 @@
         const model = this.model;
         const checked = model.get('checked');
         const checkIconName =
-            BaseCheckableMenuItemView._CHECKED_ICONS[model.get('type')];
+            this.constructor._CHECKED_ICONS[model.get('type')];
 
         el.setAttribute('aria-checked', `${checked}`);
         el.querySelector('.ink-c-menu__item-icon')
diff --git a/tsconfig.build.json b/tsconfig.build.json
index 6cdd53eddc773902b1b6abf2765e4133b3844c4f..63be0cc45126dc925cf0dc8a03e984cf3c9ae26a 100644
--- a/tsconfig.build.json
+++ b/tsconfig.build.json
@@ -1,9 +1,5 @@
 {
     "extends": "./tsconfig.json",
-    "compilerOptions": {
-        "declaration": true,
-        "declarationDir": "_build/_ts/",
-    },
     "include": [
         "src/ink/js/*",
         "src/ink/js/**/*",
diff --git a/tsconfig.json b/tsconfig.json
index 0918c727003a0f472b42f5d65269d9e11fdf5228..b3fce950f60f5f17b55b8821e2b6e20226675e35 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,19 +1,14 @@
 {
+    "extends": "@beanbag/js-buildkit/tsconfig.json",
     "compilerOptions": {
         "baseUrl": "./",
         "emitDeclarationOnly": true,
-        "esModuleInterop": true,
-        "experimentalDecorators": true,
-        "isolatedModules": true,
         "moduleResolution": "node",
         "paths": {
-            "backbone": ["node_modules/@beanbag/spina/lib/@types/backbone"],
+            "backbone": ["./src/@types/backbone"]
         },
-        "target": "ESNext",
-        "typeRoots": [
-            "node_modules/@beanbag/spina/lib/@types",
-            "node_modules/@types"
-        ]
+        "rewriteRelativeImportExtensions": true,
+        "strict": false
     },
     "include": [
         "src/ink/js/*",
diff --git a/vite.config.js b/vite.config.js
index c81160106b98d215e5df22372e530caddb84c4d3..7b82027740732713d33ab50b3bc6b9861176fdfe 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,37 +1,42 @@
 /* Vite configuration used to compile assets for Storyboard. */
 import path from 'path';
+import resolve from 'resolve/sync';
 
 import { defineConfig } from 'vite';
 import react from '@vitejs/plugin-react';
 
 
+const srcPath = path.resolve(__dirname, 'src');
+const tablerPath = path.dirname(
+    resolve('@tabler/icons/icons.json', { basedir: __dirname }));
+
+
 export default defineConfig({
+    build: {
+        target: 'esnext',
+    },
     css: {
         preprocessorOptions: {
             less: {
                 javascriptEnabled: true,
-                paths: [
-                    path.resolve(__dirname, 'src'),
-                ],
                 modifyVars: {
-                    'ink-path': path.resolve(__dirname, 'src', 'ink'),
-                    'tabler-path': path.resolve(__dirname, 'node_modules',
-                                                '\\@tabler', 'icons'),
+                    'ink-path': path.resolve(srcPath, 'ink'),
+                    'tabler-path': tablerPath.replace('@', '\\@'),
                 },
+                paths: [
+                    srcPath,
+                ],
             },
         },
     },
+    esbuild: {
+        target: 'esnext',
+    },
     optimizeDeps: {
         esbuildOptions: {
             target: 'esnext',
         },
     },
-    esbuild: {
-        target: "esnext",
-    },
-    build: {
-        target: "esnext",
-    },
     plugins: [
         react({
             babel: {
@@ -44,12 +49,15 @@
                  */
                 plugins: [
                     '@babel/plugin-external-helpers',
-                    ['@babel/plugin-proposal-decorators', {
-                        'version': 'legacy'
-                    }],
+                    [
+                        '@babel/plugin-proposal-decorators',
+                        {
+                            'version': 'legacy',
+                        },
+                    ],
                     'babel-plugin-dedent',
                     'babel-plugin-django-gettext',
-                ]
+                ],
             },
 
             /*
@@ -63,5 +71,5 @@
         noExternal: [
             '@beanbag/spina',
         ],
-    }
+    },
 });
