mvc-kit 2.11.1 → 2.12.1
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.
- package/README.md +4 -0
- package/agent-config/bin/postinstall.mjs +5 -3
- package/agent-config/bin/setup.mjs +3 -4
- package/agent-config/claude-code/agents/mvc-kit-architect.md +14 -0
- package/agent-config/claude-code/skills/guide/anti-patterns.md +41 -0
- package/agent-config/claude-code/skills/guide/api-reference.md +66 -2
- package/agent-config/claude-code/skills/guide/patterns.md +52 -0
- package/agent-config/copilot/copilot-instructions.md +9 -5
- package/agent-config/cursor/cursorrules +9 -5
- package/agent-config/lib/install-claude.mjs +10 -33
- package/dist/Feed.cjs +10 -22
- package/dist/Feed.cjs.map +1 -1
- package/dist/Feed.d.ts +2 -5
- package/dist/Feed.d.ts.map +1 -1
- package/dist/Feed.js +10 -22
- package/dist/Feed.js.map +1 -1
- package/dist/Model.cjs +9 -1
- package/dist/Model.cjs.map +1 -1
- package/dist/Model.d.ts +1 -1
- package/dist/Model.d.ts.map +1 -1
- package/dist/Model.js +9 -1
- package/dist/Model.js.map +1 -1
- package/dist/Pagination.cjs +8 -20
- package/dist/Pagination.cjs.map +1 -1
- package/dist/Pagination.d.ts +2 -5
- package/dist/Pagination.d.ts.map +1 -1
- package/dist/Pagination.js +8 -20
- package/dist/Pagination.js.map +1 -1
- package/dist/Pending.cjs +26 -39
- package/dist/Pending.cjs.map +1 -1
- package/dist/Pending.d.ts +5 -9
- package/dist/Pending.d.ts.map +1 -1
- package/dist/Pending.js +26 -39
- package/dist/Pending.js.map +1 -1
- package/dist/Selection.cjs +5 -13
- package/dist/Selection.cjs.map +1 -1
- package/dist/Selection.d.ts +2 -4
- package/dist/Selection.d.ts.map +1 -1
- package/dist/Selection.js +5 -13
- package/dist/Selection.js.map +1 -1
- package/dist/Sorting.cjs +7 -19
- package/dist/Sorting.cjs.map +1 -1
- package/dist/Sorting.d.ts +2 -5
- package/dist/Sorting.d.ts.map +1 -1
- package/dist/Sorting.js +7 -19
- package/dist/Sorting.js.map +1 -1
- package/dist/Trackable.cjs +81 -0
- package/dist/Trackable.cjs.map +1 -0
- package/dist/Trackable.d.ts +82 -0
- package/dist/Trackable.d.ts.map +1 -0
- package/dist/Trackable.js +81 -0
- package/dist/Trackable.js.map +1 -0
- package/dist/ViewModel.cjs +9 -1
- package/dist/ViewModel.cjs.map +1 -1
- package/dist/ViewModel.d.ts +1 -1
- package/dist/ViewModel.d.ts.map +1 -1
- package/dist/ViewModel.js +9 -1
- package/dist/ViewModel.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/mvc-kit.cjs +7 -0
- package/dist/mvc-kit.cjs.map +1 -1
- package/dist/mvc-kit.js +7 -0
- package/dist/mvc-kit.js.map +1 -1
- package/dist/produceDraft.cjs +105 -0
- package/dist/produceDraft.cjs.map +1 -0
- package/dist/produceDraft.d.ts +19 -0
- package/dist/produceDraft.d.ts.map +1 -0
- package/dist/produceDraft.js +105 -0
- package/dist/produceDraft.js.map +1 -0
- package/dist/react/guards.cjs +2 -0
- package/dist/react/guards.cjs.map +1 -1
- package/dist/react/guards.d.ts +4 -0
- package/dist/react/guards.d.ts.map +1 -1
- package/dist/react/guards.js +3 -1
- package/dist/react/guards.js.map +1 -1
- package/dist/react/use-local.cjs +5 -0
- package/dist/react/use-local.cjs.map +1 -1
- package/dist/react/use-local.d.ts.map +1 -1
- package/dist/react/use-local.js +6 -1
- package/dist/react/use-local.js.map +1 -1
- package/dist/react/use-singleton.cjs +5 -0
- package/dist/react/use-singleton.cjs.map +1 -1
- package/dist/react/use-singleton.d.ts.map +1 -1
- package/dist/react/use-singleton.js +6 -1
- package/dist/react/use-singleton.js.map +1 -1
- package/dist/react/use-subscribe-only.cjs +25 -0
- package/dist/react/use-subscribe-only.cjs.map +1 -0
- package/dist/react/use-subscribe-only.d.ts +9 -0
- package/dist/react/use-subscribe-only.d.ts.map +1 -0
- package/dist/react/use-subscribe-only.js +25 -0
- package/dist/react/use-subscribe-only.js.map +1 -0
- package/package.json +4 -2
- package/src/Channel.md +408 -0
- package/src/Channel.test.ts +957 -0
- package/src/Channel.ts +429 -0
- package/src/Collection.md +533 -0
- package/src/Collection.test.ts +1559 -0
- package/src/Collection.ts +653 -0
- package/src/Controller.md +306 -0
- package/src/Controller.test.ts +380 -0
- package/src/Controller.ts +90 -0
- package/src/EventBus.md +308 -0
- package/src/EventBus.test.ts +295 -0
- package/src/EventBus.ts +110 -0
- package/src/Feed.md +218 -0
- package/src/Feed.test.ts +442 -0
- package/src/Feed.ts +101 -0
- package/src/Model.md +524 -0
- package/src/Model.test.ts +642 -0
- package/src/Model.ts +260 -0
- package/src/Pagination.md +168 -0
- package/src/Pagination.test.ts +244 -0
- package/src/Pagination.ts +92 -0
- package/src/Pending.md +380 -0
- package/src/Pending.test.ts +1719 -0
- package/src/Pending.ts +390 -0
- package/src/PersistentCollection.md +183 -0
- package/src/PersistentCollection.test.ts +649 -0
- package/src/PersistentCollection.ts +375 -0
- package/src/Resource.ViewModel.test.ts +503 -0
- package/src/Resource.md +239 -0
- package/src/Resource.test.ts +786 -0
- package/src/Resource.ts +231 -0
- package/src/Selection.md +155 -0
- package/src/Selection.test.ts +326 -0
- package/src/Selection.ts +117 -0
- package/src/Service.md +440 -0
- package/src/Service.test.ts +241 -0
- package/src/Service.ts +72 -0
- package/src/Sorting.md +170 -0
- package/src/Sorting.test.ts +334 -0
- package/src/Sorting.ts +135 -0
- package/src/Trackable.md +166 -0
- package/src/Trackable.test.ts +236 -0
- package/src/Trackable.ts +129 -0
- package/src/ViewModel.async.test.ts +813 -0
- package/src/ViewModel.derived.test.ts +1583 -0
- package/src/ViewModel.md +1111 -0
- package/src/ViewModel.test.ts +1236 -0
- package/src/ViewModel.ts +800 -0
- package/src/bindPublicMethods.test.ts +126 -0
- package/src/bindPublicMethods.ts +48 -0
- package/src/env.d.ts +5 -0
- package/src/errors.test.ts +155 -0
- package/src/errors.ts +133 -0
- package/src/index.ts +49 -0
- package/src/produceDraft.md +90 -0
- package/src/produceDraft.test.ts +394 -0
- package/src/produceDraft.ts +168 -0
- package/src/react/components/CardList.md +97 -0
- package/src/react/components/CardList.test.tsx +142 -0
- package/src/react/components/CardList.tsx +68 -0
- package/src/react/components/DataTable.md +179 -0
- package/src/react/components/DataTable.test.tsx +599 -0
- package/src/react/components/DataTable.tsx +267 -0
- package/src/react/components/InfiniteScroll.md +116 -0
- package/src/react/components/InfiniteScroll.test.tsx +218 -0
- package/src/react/components/InfiniteScroll.tsx +70 -0
- package/src/react/components/types.ts +90 -0
- package/src/react/derived.test.tsx +261 -0
- package/src/react/guards.ts +24 -0
- package/src/react/index.ts +40 -0
- package/src/react/provider.test.tsx +143 -0
- package/src/react/provider.tsx +55 -0
- package/src/react/strict-mode.test.tsx +266 -0
- package/src/react/types.ts +25 -0
- package/src/react/use-event-bus.md +214 -0
- package/src/react/use-event-bus.test.tsx +168 -0
- package/src/react/use-event-bus.ts +40 -0
- package/src/react/use-instance.md +204 -0
- package/src/react/use-instance.test.tsx +350 -0
- package/src/react/use-instance.ts +60 -0
- package/src/react/use-local.md +457 -0
- package/src/react/use-local.rapid-remount.test.tsx +503 -0
- package/src/react/use-local.test.tsx +692 -0
- package/src/react/use-local.ts +165 -0
- package/src/react/use-model.md +364 -0
- package/src/react/use-model.test.tsx +394 -0
- package/src/react/use-model.ts +161 -0
- package/src/react/use-singleton.md +415 -0
- package/src/react/use-singleton.test.tsx +296 -0
- package/src/react/use-singleton.ts +69 -0
- package/src/react/use-subscribe-only.ts +39 -0
- package/src/react/use-teardown.md +169 -0
- package/src/react/use-teardown.test.tsx +86 -0
- package/src/react/use-teardown.ts +27 -0
- package/src/react-native/NativeCollection.test.ts +250 -0
- package/src/react-native/NativeCollection.ts +138 -0
- package/src/react-native/index.ts +1 -0
- package/src/singleton.md +310 -0
- package/src/singleton.test.ts +204 -0
- package/src/singleton.ts +70 -0
- package/src/types.ts +70 -0
- package/src/walkPrototypeChain.ts +22 -0
- package/src/web/IndexedDBCollection.test.ts +235 -0
- package/src/web/IndexedDBCollection.ts +66 -0
- package/src/web/WebStorageCollection.test.ts +214 -0
- package/src/web/WebStorageCollection.ts +116 -0
- package/src/web/idb.ts +184 -0
- package/src/web/index.ts +2 -0
- package/src/wrapAsyncMethods.ts +249 -0
package/dist/Selection.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Selection.cjs","sources":["../src/Selection.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"Selection.cjs","sources":["../src/Selection.ts"],"sourcesContent":["import { Trackable } from './Trackable';\n\n/**\n * Key-based selection set with toggle and select-all support.\n * Tracks which items are selected by their key (id).\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Selection<K extends string | number = string | number> extends Trackable {\n private _selected: Set<K> = new Set();\n private _readonlyView: ReadonlySet<K> = this._selected;\n\n constructor() {\n super();\n }\n\n // ── Readable state ──\n\n /** Read-only view of currently selected keys. */\n get selected(): ReadonlySet<K> {\n return this._readonlyView;\n }\n\n /** Number of currently selected items. */\n get count(): number {\n return this._selected.size;\n }\n\n /** Whether any items are currently selected. */\n get hasSelection(): boolean {\n return this._selected.size > 0;\n }\n\n // ── Query ──\n\n /** Check whether a specific key is selected. */\n isSelected(key: K): boolean {\n return this._selected.has(key);\n }\n\n // ── Actions ──\n\n /** Toggle a key's selection state (select if unselected, deselect if selected). */\n toggle(key: K): void {\n if (this._selected.has(key)) {\n this._selected.delete(key);\n } else {\n this._selected.add(key);\n }\n this._publish();\n }\n\n /** Add one or more keys to the selection. */\n select(...keys: K[]): void {\n let changed = false;\n for (const key of keys) {\n if (!this._selected.has(key)) {\n this._selected.add(key);\n changed = true;\n }\n }\n if (changed) this._publish();\n }\n\n /** Remove one or more keys from the selection. */\n deselect(...keys: K[]): void {\n let changed = false;\n for (const key of keys) {\n if (this._selected.has(key)) {\n this._selected.delete(key);\n changed = true;\n }\n }\n if (changed) this._publish();\n }\n\n /** If all selected → deselect all, else select all. */\n toggleAll(allKeys: K[]): void {\n const allSelected = allKeys.length > 0 && allKeys.every(k => this._selected.has(k));\n if (allSelected) {\n this._selected.clear();\n } else {\n for (const key of allKeys) this._selected.add(key);\n }\n this._publish();\n }\n\n /** Replace the entire selection atomically. Single notification. */\n set(...keys: K[]): void {\n // Check if anything actually changed\n if (keys.length === this._selected.size && keys.every(k => this._selected.has(k))) return;\n this._selected.clear();\n for (const key of keys) this._selected.add(key);\n this._publish();\n }\n\n /** Remove all items from the selection. */\n clear(): void {\n if (this._selected.size === 0) return;\n this._selected.clear();\n this._publish();\n }\n\n // ── Utility ──\n\n /** Filter an array to only items whose key is in the selection. */\n selectedFrom<T>(items: T[], keyOf: (item: T) => K): T[] {\n return items.filter(item => this._selected.has(keyOf(item)));\n }\n\n // ── Internal ──\n\n private _publish(): void {\n // Replace readonlyView so reference equality changes (needed for React)\n this._readonlyView = new Set(this._selected);\n this.notify();\n }\n}\n"],"names":["Trackable"],"mappings":";;;AAOO,MAAM,kBAA+DA,UAAAA,UAAU;AAAA,EAC5E,gCAAwB,IAAA;AAAA,EACxB,gBAAgC,KAAK;AAAA,EAE7C,cAAc;AACZ,UAAA;AAAA,EACF;AAAA;AAAA;AAAA,EAKA,IAAI,WAA2B;AAC7B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,QAAgB;AAClB,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA;AAAA,EAGA,IAAI,eAAwB;AAC1B,WAAO,KAAK,UAAU,OAAO;AAAA,EAC/B;AAAA;AAAA;AAAA,EAKA,WAAW,KAAiB;AAC1B,WAAO,KAAK,UAAU,IAAI,GAAG;AAAA,EAC/B;AAAA;AAAA;AAAA,EAKA,OAAO,KAAc;AACnB,QAAI,KAAK,UAAU,IAAI,GAAG,GAAG;AAC3B,WAAK,UAAU,OAAO,GAAG;AAAA,IAC3B,OAAO;AACL,WAAK,UAAU,IAAI,GAAG;AAAA,IACxB;AACA,SAAK,SAAA;AAAA,EACP;AAAA;AAAA,EAGA,UAAU,MAAiB;AACzB,QAAI,UAAU;AACd,eAAW,OAAO,MAAM;AACtB,UAAI,CAAC,KAAK,UAAU,IAAI,GAAG,GAAG;AAC5B,aAAK,UAAU,IAAI,GAAG;AACtB,kBAAU;AAAA,MACZ;AAAA,IACF;AACA,QAAI,cAAc,SAAA;AAAA,EACpB;AAAA;AAAA,EAGA,YAAY,MAAiB;AAC3B,QAAI,UAAU;AACd,eAAW,OAAO,MAAM;AACtB,UAAI,KAAK,UAAU,IAAI,GAAG,GAAG;AAC3B,aAAK,UAAU,OAAO,GAAG;AACzB,kBAAU;AAAA,MACZ;AAAA,IACF;AACA,QAAI,cAAc,SAAA;AAAA,EACpB;AAAA;AAAA,EAGA,UAAU,SAAoB;AAC5B,UAAM,cAAc,QAAQ,SAAS,KAAK,QAAQ,MAAM,CAAA,MAAK,KAAK,UAAU,IAAI,CAAC,CAAC;AAClF,QAAI,aAAa;AACf,WAAK,UAAU,MAAA;AAAA,IACjB,OAAO;AACL,iBAAW,OAAO,QAAS,MAAK,UAAU,IAAI,GAAG;AAAA,IACnD;AACA,SAAK,SAAA;AAAA,EACP;AAAA;AAAA,EAGA,OAAO,MAAiB;AAEtB,QAAI,KAAK,WAAW,KAAK,UAAU,QAAQ,KAAK,MAAM,CAAA,MAAK,KAAK,UAAU,IAAI,CAAC,CAAC,EAAG;AACnF,SAAK,UAAU,MAAA;AACf,eAAW,OAAO,KAAM,MAAK,UAAU,IAAI,GAAG;AAC9C,SAAK,SAAA;AAAA,EACP;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,KAAK,UAAU,SAAS,EAAG;AAC/B,SAAK,UAAU,MAAA;AACf,SAAK,SAAA;AAAA,EACP;AAAA;AAAA;AAAA,EAKA,aAAgB,OAAY,OAA4B;AACtD,WAAO,MAAM,OAAO,CAAA,SAAQ,KAAK,UAAU,IAAI,MAAM,IAAI,CAAC,CAAC;AAAA,EAC7D;AAAA;AAAA,EAIQ,WAAiB;AAEvB,SAAK,gBAAgB,IAAI,IAAI,KAAK,SAAS;AAC3C,SAAK,OAAA;AAAA,EACP;AACF;;"}
|
package/dist/Selection.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { Trackable } from './Trackable';
|
|
1
2
|
/**
|
|
2
3
|
* Key-based selection set with toggle and select-all support.
|
|
3
4
|
* Tracks which items are selected by their key (id).
|
|
4
5
|
* Subscribable — auto-tracked when used as a ViewModel property.
|
|
5
6
|
*/
|
|
6
|
-
export declare class Selection<K extends string | number = string | number> {
|
|
7
|
+
export declare class Selection<K extends string | number = string | number> extends Trackable {
|
|
7
8
|
private _selected;
|
|
8
|
-
private _listeners;
|
|
9
9
|
private _readonlyView;
|
|
10
10
|
constructor();
|
|
11
11
|
/** Read-only view of currently selected keys. */
|
|
@@ -30,8 +30,6 @@ export declare class Selection<K extends string | number = string | number> {
|
|
|
30
30
|
clear(): void;
|
|
31
31
|
/** Filter an array to only items whose key is in the selection. */
|
|
32
32
|
selectedFrom<T>(items: T[], keyOf: (item: T) => K): T[];
|
|
33
|
-
/** Subscribe to selection changes. Returns an unsubscribe function. */
|
|
34
|
-
subscribe(cb: () => void): () => void;
|
|
35
33
|
private _publish;
|
|
36
34
|
}
|
|
37
35
|
//# sourceMappingURL=Selection.d.ts.map
|
package/dist/Selection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Selection.d.ts","sourceRoot":"","sources":["../src/Selection.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Selection.d.ts","sourceRoot":"","sources":["../src/Selection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC;;;;GAIG;AACH,qBAAa,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAE,SAAQ,SAAS;IACnF,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,aAAa,CAAkC;;IAQvD,iDAAiD;IACjD,IAAI,QAAQ,IAAI,WAAW,CAAC,CAAC,CAAC,CAE7B;IAED,0CAA0C;IAC1C,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,gDAAgD;IAChD,IAAI,YAAY,IAAI,OAAO,CAE1B;IAID,gDAAgD;IAChD,UAAU,CAAC,GAAG,EAAE,CAAC,GAAG,OAAO;IAM3B,mFAAmF;IACnF,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI;IASpB,6CAA6C;IAC7C,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI;IAW1B,kDAAkD;IAClD,QAAQ,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI;IAW5B,uDAAuD;IACvD,SAAS,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,IAAI;IAU7B,oEAAoE;IACpE,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI;IAQvB,2CAA2C;IAC3C,KAAK,IAAI,IAAI;IAQb,mEAAmE;IACnE,YAAY,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE;IAMvD,OAAO,CAAC,QAAQ;CAKjB"}
|
package/dist/Selection.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
class Selection {
|
|
1
|
+
import { Trackable } from "./Trackable.js";
|
|
2
|
+
class Selection extends Trackable {
|
|
3
3
|
_selected = /* @__PURE__ */ new Set();
|
|
4
|
-
_listeners = /* @__PURE__ */ new Set();
|
|
5
4
|
_readonlyView = this._selected;
|
|
6
5
|
constructor() {
|
|
7
|
-
|
|
6
|
+
super();
|
|
8
7
|
}
|
|
9
8
|
// ── Readable state ──
|
|
10
9
|
/** Read-only view of currently selected keys. */
|
|
@@ -84,17 +83,10 @@ class Selection {
|
|
|
84
83
|
selectedFrom(items, keyOf) {
|
|
85
84
|
return items.filter((item) => this._selected.has(keyOf(item)));
|
|
86
85
|
}
|
|
87
|
-
// ──
|
|
88
|
-
/** Subscribe to selection changes. Returns an unsubscribe function. */
|
|
89
|
-
subscribe(cb) {
|
|
90
|
-
this._listeners.add(cb);
|
|
91
|
-
return () => {
|
|
92
|
-
this._listeners.delete(cb);
|
|
93
|
-
};
|
|
94
|
-
}
|
|
86
|
+
// ── Internal ──
|
|
95
87
|
_publish() {
|
|
96
88
|
this._readonlyView = new Set(this._selected);
|
|
97
|
-
|
|
89
|
+
this.notify();
|
|
98
90
|
}
|
|
99
91
|
}
|
|
100
92
|
export {
|
package/dist/Selection.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Selection.js","sources":["../src/Selection.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"Selection.js","sources":["../src/Selection.ts"],"sourcesContent":["import { Trackable } from './Trackable';\n\n/**\n * Key-based selection set with toggle and select-all support.\n * Tracks which items are selected by their key (id).\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Selection<K extends string | number = string | number> extends Trackable {\n private _selected: Set<K> = new Set();\n private _readonlyView: ReadonlySet<K> = this._selected;\n\n constructor() {\n super();\n }\n\n // ── Readable state ──\n\n /** Read-only view of currently selected keys. */\n get selected(): ReadonlySet<K> {\n return this._readonlyView;\n }\n\n /** Number of currently selected items. */\n get count(): number {\n return this._selected.size;\n }\n\n /** Whether any items are currently selected. */\n get hasSelection(): boolean {\n return this._selected.size > 0;\n }\n\n // ── Query ──\n\n /** Check whether a specific key is selected. */\n isSelected(key: K): boolean {\n return this._selected.has(key);\n }\n\n // ── Actions ──\n\n /** Toggle a key's selection state (select if unselected, deselect if selected). */\n toggle(key: K): void {\n if (this._selected.has(key)) {\n this._selected.delete(key);\n } else {\n this._selected.add(key);\n }\n this._publish();\n }\n\n /** Add one or more keys to the selection. */\n select(...keys: K[]): void {\n let changed = false;\n for (const key of keys) {\n if (!this._selected.has(key)) {\n this._selected.add(key);\n changed = true;\n }\n }\n if (changed) this._publish();\n }\n\n /** Remove one or more keys from the selection. */\n deselect(...keys: K[]): void {\n let changed = false;\n for (const key of keys) {\n if (this._selected.has(key)) {\n this._selected.delete(key);\n changed = true;\n }\n }\n if (changed) this._publish();\n }\n\n /** If all selected → deselect all, else select all. */\n toggleAll(allKeys: K[]): void {\n const allSelected = allKeys.length > 0 && allKeys.every(k => this._selected.has(k));\n if (allSelected) {\n this._selected.clear();\n } else {\n for (const key of allKeys) this._selected.add(key);\n }\n this._publish();\n }\n\n /** Replace the entire selection atomically. Single notification. */\n set(...keys: K[]): void {\n // Check if anything actually changed\n if (keys.length === this._selected.size && keys.every(k => this._selected.has(k))) return;\n this._selected.clear();\n for (const key of keys) this._selected.add(key);\n this._publish();\n }\n\n /** Remove all items from the selection. */\n clear(): void {\n if (this._selected.size === 0) return;\n this._selected.clear();\n this._publish();\n }\n\n // ── Utility ──\n\n /** Filter an array to only items whose key is in the selection. */\n selectedFrom<T>(items: T[], keyOf: (item: T) => K): T[] {\n return items.filter(item => this._selected.has(keyOf(item)));\n }\n\n // ── Internal ──\n\n private _publish(): void {\n // Replace readonlyView so reference equality changes (needed for React)\n this._readonlyView = new Set(this._selected);\n this.notify();\n }\n}\n"],"names":[],"mappings":";AAOO,MAAM,kBAA+D,UAAU;AAAA,EAC5E,gCAAwB,IAAA;AAAA,EACxB,gBAAgC,KAAK;AAAA,EAE7C,cAAc;AACZ,UAAA;AAAA,EACF;AAAA;AAAA;AAAA,EAKA,IAAI,WAA2B;AAC7B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,QAAgB;AAClB,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA;AAAA,EAGA,IAAI,eAAwB;AAC1B,WAAO,KAAK,UAAU,OAAO;AAAA,EAC/B;AAAA;AAAA;AAAA,EAKA,WAAW,KAAiB;AAC1B,WAAO,KAAK,UAAU,IAAI,GAAG;AAAA,EAC/B;AAAA;AAAA;AAAA,EAKA,OAAO,KAAc;AACnB,QAAI,KAAK,UAAU,IAAI,GAAG,GAAG;AAC3B,WAAK,UAAU,OAAO,GAAG;AAAA,IAC3B,OAAO;AACL,WAAK,UAAU,IAAI,GAAG;AAAA,IACxB;AACA,SAAK,SAAA;AAAA,EACP;AAAA;AAAA,EAGA,UAAU,MAAiB;AACzB,QAAI,UAAU;AACd,eAAW,OAAO,MAAM;AACtB,UAAI,CAAC,KAAK,UAAU,IAAI,GAAG,GAAG;AAC5B,aAAK,UAAU,IAAI,GAAG;AACtB,kBAAU;AAAA,MACZ;AAAA,IACF;AACA,QAAI,cAAc,SAAA;AAAA,EACpB;AAAA;AAAA,EAGA,YAAY,MAAiB;AAC3B,QAAI,UAAU;AACd,eAAW,OAAO,MAAM;AACtB,UAAI,KAAK,UAAU,IAAI,GAAG,GAAG;AAC3B,aAAK,UAAU,OAAO,GAAG;AACzB,kBAAU;AAAA,MACZ;AAAA,IACF;AACA,QAAI,cAAc,SAAA;AAAA,EACpB;AAAA;AAAA,EAGA,UAAU,SAAoB;AAC5B,UAAM,cAAc,QAAQ,SAAS,KAAK,QAAQ,MAAM,CAAA,MAAK,KAAK,UAAU,IAAI,CAAC,CAAC;AAClF,QAAI,aAAa;AACf,WAAK,UAAU,MAAA;AAAA,IACjB,OAAO;AACL,iBAAW,OAAO,QAAS,MAAK,UAAU,IAAI,GAAG;AAAA,IACnD;AACA,SAAK,SAAA;AAAA,EACP;AAAA;AAAA,EAGA,OAAO,MAAiB;AAEtB,QAAI,KAAK,WAAW,KAAK,UAAU,QAAQ,KAAK,MAAM,CAAA,MAAK,KAAK,UAAU,IAAI,CAAC,CAAC,EAAG;AACnF,SAAK,UAAU,MAAA;AACf,eAAW,OAAO,KAAM,MAAK,UAAU,IAAI,GAAG;AAC9C,SAAK,SAAA;AAAA,EACP;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,KAAK,UAAU,SAAS,EAAG;AAC/B,SAAK,UAAU,MAAA;AACf,SAAK,SAAA;AAAA,EACP;AAAA;AAAA;AAAA,EAKA,aAAgB,OAAY,OAA4B;AACtD,WAAO,MAAM,OAAO,CAAA,SAAQ,KAAK,UAAU,IAAI,MAAM,IAAI,CAAC,CAAC;AAAA,EAC7D;AAAA;AAAA,EAIQ,WAAiB;AAEvB,SAAK,gBAAgB,IAAI,IAAI,KAAK,SAAS;AAC3C,SAAK,OAAA;AAAA,EACP;AACF;"}
|
package/dist/Sorting.cjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const
|
|
4
|
-
class Sorting {
|
|
3
|
+
const Trackable = require("./Trackable.cjs");
|
|
4
|
+
class Sorting extends Trackable.Trackable {
|
|
5
5
|
_sorts;
|
|
6
|
-
_listeners = /* @__PURE__ */ new Set();
|
|
7
6
|
constructor(options) {
|
|
7
|
+
super();
|
|
8
8
|
this._sorts = Object.freeze(options?.sorts?.map((s) => ({ ...s })) ?? []);
|
|
9
|
-
bindPublicMethods.bindPublicMethods(this);
|
|
10
9
|
}
|
|
11
10
|
// ── Readable state ──
|
|
12
11
|
/** Current list of active sort descriptors, in priority order. */
|
|
@@ -49,22 +48,22 @@ class Sorting {
|
|
|
49
48
|
} else {
|
|
50
49
|
this._sorts = Object.freeze(this._sorts.filter((_, i) => i !== idx));
|
|
51
50
|
}
|
|
52
|
-
this.
|
|
51
|
+
this.notify();
|
|
53
52
|
}
|
|
54
53
|
/** Replace all with a single sort. */
|
|
55
54
|
setSort(key, direction) {
|
|
56
55
|
this._sorts = Object.freeze([{ key, direction }]);
|
|
57
|
-
this.
|
|
56
|
+
this.notify();
|
|
58
57
|
}
|
|
59
58
|
/** Replace all sorts. */
|
|
60
59
|
setSorts(sorts) {
|
|
61
60
|
this._sorts = Object.freeze(sorts.map((s) => ({ ...s })));
|
|
62
|
-
this.
|
|
61
|
+
this.notify();
|
|
63
62
|
}
|
|
64
63
|
/** Clear all sort descriptors. */
|
|
65
64
|
reset() {
|
|
66
65
|
this._sorts = Object.freeze([]);
|
|
67
|
-
this.
|
|
66
|
+
this.notify();
|
|
68
67
|
}
|
|
69
68
|
// ── Pipeline ──
|
|
70
69
|
/** Sort an array using the current descriptors. Returns a new sorted array. */
|
|
@@ -87,17 +86,6 @@ class Sorting {
|
|
|
87
86
|
});
|
|
88
87
|
return sorted;
|
|
89
88
|
}
|
|
90
|
-
// ── Subscribable interface ──
|
|
91
|
-
/** Subscribe to sort state changes. Returns an unsubscribe function. */
|
|
92
|
-
subscribe(cb) {
|
|
93
|
-
this._listeners.add(cb);
|
|
94
|
-
return () => {
|
|
95
|
-
this._listeners.delete(cb);
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
_notify() {
|
|
99
|
-
for (const cb of this._listeners) cb();
|
|
100
|
-
}
|
|
101
89
|
}
|
|
102
90
|
function defaultCompare(a, b, key) {
|
|
103
91
|
const aVal = a[key];
|
package/dist/Sorting.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sorting.cjs","sources":["../src/Sorting.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"Sorting.cjs","sources":["../src/Sorting.ts"],"sourcesContent":["import { Trackable } from './Trackable';\n\n/** Describes a single sort column with key and direction. */\nexport interface SortDescriptor {\n key: string;\n direction: 'asc' | 'desc';\n}\n\n/**\n * Multi-column sort state manager with a comparator pipeline.\n * Maintains an ordered list of sort descriptors and applies them to arrays.\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Sorting<T = any> extends Trackable {\n private _sorts: readonly SortDescriptor[];\n\n constructor(options?: { sorts?: SortDescriptor[] }) {\n super();\n this._sorts = Object.freeze(options?.sorts?.map(s => ({ ...s })) ?? []);\n }\n\n // ── Readable state ──\n\n /** Current list of active sort descriptors, in priority order. */\n get sorts(): readonly SortDescriptor[] {\n return this._sorts;\n }\n\n /** Primary sort key (first descriptor), or null when empty. */\n get key(): string | null {\n return this._sorts.length > 0 ? this._sorts[0].key : null;\n }\n\n /** Primary sort direction. Defaults to 'asc' when empty. */\n get direction(): 'asc' | 'desc' {\n return this._sorts.length > 0 ? this._sorts[0].direction : 'asc';\n }\n\n // ── Query ──\n\n /** Whether the given key is currently sorted. */\n isSorted(key: string): boolean {\n return this._sorts.some(s => s.key === key);\n }\n\n /** Returns the sort direction for a key, or null if not sorted. */\n directionOf(key: string): 'asc' | 'desc' | null {\n const found = this._sorts.find(s => s.key === key);\n return found ? found.direction : null;\n }\n\n /** Returns the priority index of a sorted key, or -1 if not sorted. */\n indexOf(key: string): number {\n return this._sorts.findIndex(s => s.key === key);\n }\n\n // ── Actions ──\n\n /** 3-click cycle: not sorted → asc → desc → removed. */\n toggle(key: string): void {\n const idx = this.indexOf(key);\n if (idx === -1) {\n // Add as asc\n this._sorts = Object.freeze([...this._sorts, { key, direction: 'asc' as const }]);\n } else if (this._sorts[idx].direction === 'asc') {\n // Flip to desc\n const next = this._sorts.map((s, i) =>\n i === idx ? { key: s.key, direction: 'desc' as const } : s\n );\n this._sorts = Object.freeze(next);\n } else {\n // Remove\n this._sorts = Object.freeze(this._sorts.filter((_, i) => i !== idx));\n }\n this.notify();\n }\n\n /** Replace all with a single sort. */\n setSort(key: string, direction: 'asc' | 'desc'): void {\n this._sorts = Object.freeze([{ key, direction }]);\n this.notify();\n }\n\n /** Replace all sorts. */\n setSorts(sorts: SortDescriptor[]): void {\n this._sorts = Object.freeze(sorts.map(s => ({ ...s })));\n this.notify();\n }\n\n /** Clear all sort descriptors. */\n reset(): void {\n this._sorts = Object.freeze([]);\n this.notify();\n }\n\n // ── Pipeline ──\n\n /** Sort an array using the current descriptors. Returns a new sorted array. */\n apply(\n items: T[],\n compareFn?: (a: T, b: T, key: string, dir: 'asc' | 'desc') => number,\n ): T[] {\n if (this._sorts.length === 0) return items;\n const sorted = items.slice();\n const sorts = this._sorts;\n sorted.sort((a, b) => {\n for (const { key, direction } of sorts) {\n let cmp: number;\n if (compareFn) {\n cmp = compareFn(a, b, key, direction);\n } else {\n cmp = defaultCompare(a, b, key);\n }\n if (direction === 'desc') cmp = -cmp;\n if (cmp !== 0) return cmp;\n }\n return 0;\n });\n return sorted;\n }\n}\n\nfunction defaultCompare(a: any, b: any, key: string): number {\n const aVal = a[key];\n const bVal = b[key];\n if (aVal == null && bVal == null) return 0;\n if (aVal == null) return -1;\n if (bVal == null) return 1;\n if (typeof aVal === 'string' && typeof bVal === 'string') {\n return aVal.localeCompare(bVal);\n }\n if (aVal < bVal) return -1;\n if (aVal > bVal) return 1;\n return 0;\n}\n"],"names":["Trackable"],"mappings":"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"}
|
package/dist/Sorting.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Trackable } from './Trackable';
|
|
1
2
|
/** Describes a single sort column with key and direction. */
|
|
2
3
|
export interface SortDescriptor {
|
|
3
4
|
key: string;
|
|
@@ -8,9 +9,8 @@ export interface SortDescriptor {
|
|
|
8
9
|
* Maintains an ordered list of sort descriptors and applies them to arrays.
|
|
9
10
|
* Subscribable — auto-tracked when used as a ViewModel property.
|
|
10
11
|
*/
|
|
11
|
-
export declare class Sorting<T = any> {
|
|
12
|
+
export declare class Sorting<T = any> extends Trackable {
|
|
12
13
|
private _sorts;
|
|
13
|
-
private _listeners;
|
|
14
14
|
constructor(options?: {
|
|
15
15
|
sorts?: SortDescriptor[];
|
|
16
16
|
});
|
|
@@ -36,8 +36,5 @@ export declare class Sorting<T = any> {
|
|
|
36
36
|
reset(): void;
|
|
37
37
|
/** Sort an array using the current descriptors. Returns a new sorted array. */
|
|
38
38
|
apply(items: T[], compareFn?: (a: T, b: T, key: string, dir: 'asc' | 'desc') => number): T[];
|
|
39
|
-
/** Subscribe to sort state changes. Returns an unsubscribe function. */
|
|
40
|
-
subscribe(cb: () => void): () => void;
|
|
41
|
-
private _notify;
|
|
42
39
|
}
|
|
43
40
|
//# sourceMappingURL=Sorting.d.ts.map
|
package/dist/Sorting.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sorting.d.ts","sourceRoot":"","sources":["../src/Sorting.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Sorting.d.ts","sourceRoot":"","sources":["../src/Sorting.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,6DAA6D;AAC7D,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,qBAAa,OAAO,CAAC,CAAC,GAAG,GAAG,CAAE,SAAQ,SAAS;IAC7C,OAAO,CAAC,MAAM,CAA4B;gBAE9B,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,cAAc,EAAE,CAAA;KAAE;IAOlD,kEAAkE;IAClE,IAAI,KAAK,IAAI,SAAS,cAAc,EAAE,CAErC;IAED,+DAA+D;IAC/D,IAAI,GAAG,IAAI,MAAM,GAAG,IAAI,CAEvB;IAED,4DAA4D;IAC5D,IAAI,SAAS,IAAI,KAAK,GAAG,MAAM,CAE9B;IAID,iDAAiD;IACjD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAI9B,mEAAmE;IACnE,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI;IAK/C,uEAAuE;IACvE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAM5B,wDAAwD;IACxD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAkBzB,sCAAsC;IACtC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,GAAG,MAAM,GAAG,IAAI;IAKrD,yBAAyB;IACzB,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,IAAI;IAKvC,kCAAkC;IAClC,KAAK,IAAI,IAAI;IAOb,+EAA+E;IAC/E,KAAK,CACH,KAAK,EAAE,CAAC,EAAE,EACV,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,GAAG,MAAM,KAAK,MAAM,GACnE,CAAC,EAAE;CAmBP"}
|
package/dist/Sorting.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
class Sorting {
|
|
1
|
+
import { Trackable } from "./Trackable.js";
|
|
2
|
+
class Sorting extends Trackable {
|
|
3
3
|
_sorts;
|
|
4
|
-
_listeners = /* @__PURE__ */ new Set();
|
|
5
4
|
constructor(options) {
|
|
5
|
+
super();
|
|
6
6
|
this._sorts = Object.freeze(options?.sorts?.map((s) => ({ ...s })) ?? []);
|
|
7
|
-
bindPublicMethods(this);
|
|
8
7
|
}
|
|
9
8
|
// ── Readable state ──
|
|
10
9
|
/** Current list of active sort descriptors, in priority order. */
|
|
@@ -47,22 +46,22 @@ class Sorting {
|
|
|
47
46
|
} else {
|
|
48
47
|
this._sorts = Object.freeze(this._sorts.filter((_, i) => i !== idx));
|
|
49
48
|
}
|
|
50
|
-
this.
|
|
49
|
+
this.notify();
|
|
51
50
|
}
|
|
52
51
|
/** Replace all with a single sort. */
|
|
53
52
|
setSort(key, direction) {
|
|
54
53
|
this._sorts = Object.freeze([{ key, direction }]);
|
|
55
|
-
this.
|
|
54
|
+
this.notify();
|
|
56
55
|
}
|
|
57
56
|
/** Replace all sorts. */
|
|
58
57
|
setSorts(sorts) {
|
|
59
58
|
this._sorts = Object.freeze(sorts.map((s) => ({ ...s })));
|
|
60
|
-
this.
|
|
59
|
+
this.notify();
|
|
61
60
|
}
|
|
62
61
|
/** Clear all sort descriptors. */
|
|
63
62
|
reset() {
|
|
64
63
|
this._sorts = Object.freeze([]);
|
|
65
|
-
this.
|
|
64
|
+
this.notify();
|
|
66
65
|
}
|
|
67
66
|
// ── Pipeline ──
|
|
68
67
|
/** Sort an array using the current descriptors. Returns a new sorted array. */
|
|
@@ -85,17 +84,6 @@ class Sorting {
|
|
|
85
84
|
});
|
|
86
85
|
return sorted;
|
|
87
86
|
}
|
|
88
|
-
// ── Subscribable interface ──
|
|
89
|
-
/** Subscribe to sort state changes. Returns an unsubscribe function. */
|
|
90
|
-
subscribe(cb) {
|
|
91
|
-
this._listeners.add(cb);
|
|
92
|
-
return () => {
|
|
93
|
-
this._listeners.delete(cb);
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
_notify() {
|
|
97
|
-
for (const cb of this._listeners) cb();
|
|
98
|
-
}
|
|
99
87
|
}
|
|
100
88
|
function defaultCompare(a, b, key) {
|
|
101
89
|
const aVal = a[key];
|
package/dist/Sorting.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sorting.js","sources":["../src/Sorting.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"Sorting.js","sources":["../src/Sorting.ts"],"sourcesContent":["import { Trackable } from './Trackable';\n\n/** Describes a single sort column with key and direction. */\nexport interface SortDescriptor {\n key: string;\n direction: 'asc' | 'desc';\n}\n\n/**\n * Multi-column sort state manager with a comparator pipeline.\n * Maintains an ordered list of sort descriptors and applies them to arrays.\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Sorting<T = any> extends Trackable {\n private _sorts: readonly SortDescriptor[];\n\n constructor(options?: { sorts?: SortDescriptor[] }) {\n super();\n this._sorts = Object.freeze(options?.sorts?.map(s => ({ ...s })) ?? []);\n }\n\n // ── Readable state ──\n\n /** Current list of active sort descriptors, in priority order. */\n get sorts(): readonly SortDescriptor[] {\n return this._sorts;\n }\n\n /** Primary sort key (first descriptor), or null when empty. */\n get key(): string | null {\n return this._sorts.length > 0 ? this._sorts[0].key : null;\n }\n\n /** Primary sort direction. Defaults to 'asc' when empty. */\n get direction(): 'asc' | 'desc' {\n return this._sorts.length > 0 ? this._sorts[0].direction : 'asc';\n }\n\n // ── Query ──\n\n /** Whether the given key is currently sorted. */\n isSorted(key: string): boolean {\n return this._sorts.some(s => s.key === key);\n }\n\n /** Returns the sort direction for a key, or null if not sorted. */\n directionOf(key: string): 'asc' | 'desc' | null {\n const found = this._sorts.find(s => s.key === key);\n return found ? found.direction : null;\n }\n\n /** Returns the priority index of a sorted key, or -1 if not sorted. */\n indexOf(key: string): number {\n return this._sorts.findIndex(s => s.key === key);\n }\n\n // ── Actions ──\n\n /** 3-click cycle: not sorted → asc → desc → removed. */\n toggle(key: string): void {\n const idx = this.indexOf(key);\n if (idx === -1) {\n // Add as asc\n this._sorts = Object.freeze([...this._sorts, { key, direction: 'asc' as const }]);\n } else if (this._sorts[idx].direction === 'asc') {\n // Flip to desc\n const next = this._sorts.map((s, i) =>\n i === idx ? { key: s.key, direction: 'desc' as const } : s\n );\n this._sorts = Object.freeze(next);\n } else {\n // Remove\n this._sorts = Object.freeze(this._sorts.filter((_, i) => i !== idx));\n }\n this.notify();\n }\n\n /** Replace all with a single sort. */\n setSort(key: string, direction: 'asc' | 'desc'): void {\n this._sorts = Object.freeze([{ key, direction }]);\n this.notify();\n }\n\n /** Replace all sorts. */\n setSorts(sorts: SortDescriptor[]): void {\n this._sorts = Object.freeze(sorts.map(s => ({ ...s })));\n this.notify();\n }\n\n /** Clear all sort descriptors. */\n reset(): void {\n this._sorts = Object.freeze([]);\n this.notify();\n }\n\n // ── Pipeline ──\n\n /** Sort an array using the current descriptors. Returns a new sorted array. */\n apply(\n items: T[],\n compareFn?: (a: T, b: T, key: string, dir: 'asc' | 'desc') => number,\n ): T[] {\n if (this._sorts.length === 0) return items;\n const sorted = items.slice();\n const sorts = this._sorts;\n sorted.sort((a, b) => {\n for (const { key, direction } of sorts) {\n let cmp: number;\n if (compareFn) {\n cmp = compareFn(a, b, key, direction);\n } else {\n cmp = defaultCompare(a, b, key);\n }\n if (direction === 'desc') cmp = -cmp;\n if (cmp !== 0) return cmp;\n }\n return 0;\n });\n return sorted;\n }\n}\n\nfunction defaultCompare(a: any, b: any, key: string): number {\n const aVal = a[key];\n const bVal = b[key];\n if (aVal == null && bVal == null) return 0;\n if (aVal == null) return -1;\n if (bVal == null) return 1;\n if (typeof aVal === 'string' && typeof bVal === 'string') {\n return aVal.localeCompare(bVal);\n }\n if (aVal < bVal) return -1;\n if (aVal > bVal) return 1;\n return 0;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const bindPublicMethods = require("./bindPublicMethods.cjs");
|
|
4
|
+
const PROTECTED_KEYS = /* @__PURE__ */ new Set(["addCleanup", "notify"]);
|
|
5
|
+
class Trackable {
|
|
6
|
+
_listeners = null;
|
|
7
|
+
_disposed = false;
|
|
8
|
+
_abortController = null;
|
|
9
|
+
_cleanups = null;
|
|
10
|
+
constructor() {
|
|
11
|
+
bindPublicMethods.bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);
|
|
12
|
+
}
|
|
13
|
+
// ── Disposable interface ──
|
|
14
|
+
/** Whether this instance has been disposed. */
|
|
15
|
+
get disposed() {
|
|
16
|
+
return this._disposed;
|
|
17
|
+
}
|
|
18
|
+
/** AbortSignal that fires when this instance is disposed. Lazily created. */
|
|
19
|
+
get disposeSignal() {
|
|
20
|
+
if (!this._abortController) {
|
|
21
|
+
this._abortController = new AbortController();
|
|
22
|
+
}
|
|
23
|
+
return this._abortController.signal;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Tear down the instance: abort the dispose signal, run all registered
|
|
27
|
+
* cleanups, clear subscribers, and call onDispose. Idempotent.
|
|
28
|
+
*/
|
|
29
|
+
dispose() {
|
|
30
|
+
if (this._disposed) return;
|
|
31
|
+
this._disposed = true;
|
|
32
|
+
this._abortController?.abort();
|
|
33
|
+
if (this._cleanups) {
|
|
34
|
+
for (const fn of this._cleanups) fn();
|
|
35
|
+
this._cleanups = null;
|
|
36
|
+
}
|
|
37
|
+
this._listeners?.clear();
|
|
38
|
+
this.onDispose?.();
|
|
39
|
+
}
|
|
40
|
+
// ── Subscribable (notification-only, no state) ──
|
|
41
|
+
/**
|
|
42
|
+
* Subscribe to change notifications. The callback is invoked (with no
|
|
43
|
+
* arguments) whenever the subclass calls {@link notify}.
|
|
44
|
+
*
|
|
45
|
+
* This is the duck-typed contract that ViewModel's auto-tracking system
|
|
46
|
+
* recognizes — any object with a `subscribe` method is automatically
|
|
47
|
+
* tracked when accessed inside a ViewModel getter.
|
|
48
|
+
*
|
|
49
|
+
* @returns An unsubscribe function.
|
|
50
|
+
*/
|
|
51
|
+
subscribe(cb) {
|
|
52
|
+
if (!this._listeners) this._listeners = /* @__PURE__ */ new Set();
|
|
53
|
+
this._listeners.add(cb);
|
|
54
|
+
return () => {
|
|
55
|
+
this._listeners?.delete(cb);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Notify all subscribers that state has changed. Call this after
|
|
60
|
+
* mutating internal state to trigger ViewModel getter invalidation
|
|
61
|
+
* and React re-renders.
|
|
62
|
+
* @protected
|
|
63
|
+
*/
|
|
64
|
+
notify() {
|
|
65
|
+
if (this._listeners) {
|
|
66
|
+
for (const cb of this._listeners) cb();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// ── Protected utilities ──
|
|
70
|
+
/**
|
|
71
|
+
* Register a cleanup function to be called on {@link dispose}.
|
|
72
|
+
* Cleanups run in registration order.
|
|
73
|
+
* @protected
|
|
74
|
+
*/
|
|
75
|
+
addCleanup(fn) {
|
|
76
|
+
if (!this._cleanups) this._cleanups = [];
|
|
77
|
+
this._cleanups.push(fn);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.Trackable = Trackable;
|
|
81
|
+
//# sourceMappingURL=Trackable.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Trackable.cjs","sources":["../src/Trackable.ts"],"sourcesContent":["import type { Disposable } from './types';\nimport { bindPublicMethods } from './bindPublicMethods';\n\nconst PROTECTED_KEYS = new Set(['addCleanup', 'notify']);\n\n/**\n * Base class for custom reactive objects that integrate with ViewModel's\n * auto-tracking system. Provides subscribable notifications, disposal\n * lifecycle, and automatic method binding.\n *\n * Any object with a `subscribe()` method is auto-tracked by ViewModel\n * getters. Trackable gives you that plus cleanup infrastructure and\n * point-free methods — the same building blocks used by Sorting,\n * Selection, Feed, Pagination, and Pending.\n *\n * Use Trackable when integrating third-party SDKs, custom query objects,\n * or any reactive state that doesn't fit ViewModel's state/getter model.\n *\n * @example\n * ```ts\n * class RPCQuery<Data> extends Trackable {\n * private _data: Data | undefined;\n * private _loading = false;\n *\n * get data() { return this._data; }\n * get loading() { return this._loading; }\n *\n * async call(): Promise<void> {\n * this._loading = true;\n * this.notify();\n * this._data = await fetchData();\n * this._loading = false;\n * this.notify();\n * }\n * }\n *\n * // Used as a ViewModel property — auto-tracked:\n * class UsersVM extends ViewModel {\n * readonly users = new RPCQuery<User[]>();\n * get userList() { return this.users.data ?? []; }\n * }\n * ```\n */\nexport class Trackable implements Disposable {\n private _listeners: Set<() => void> | null = null;\n private _disposed = false;\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n constructor() {\n bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);\n }\n\n // ── Disposable interface ──\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /**\n * Tear down the instance: abort the dispose signal, run all registered\n * cleanups, clear subscribers, and call onDispose. Idempotent.\n */\n dispose(): void {\n if (this._disposed) return;\n this._disposed = true;\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this._listeners?.clear();\n this.onDispose?.();\n }\n\n // ── Subscribable (notification-only, no state) ──\n\n /**\n * Subscribe to change notifications. The callback is invoked (with no\n * arguments) whenever the subclass calls {@link notify}.\n *\n * This is the duck-typed contract that ViewModel's auto-tracking system\n * recognizes — any object with a `subscribe` method is automatically\n * tracked when accessed inside a ViewModel getter.\n *\n * @returns An unsubscribe function.\n */\n subscribe(cb: () => void): () => void {\n if (!this._listeners) this._listeners = new Set();\n this._listeners.add(cb);\n return () => { this._listeners?.delete(cb); };\n }\n\n /**\n * Notify all subscribers that state has changed. Call this after\n * mutating internal state to trigger ViewModel getter invalidation\n * and React re-renders.\n * @protected\n */\n protected notify(): void {\n if (this._listeners) {\n for (const cb of this._listeners) cb();\n }\n }\n\n // ── Protected utilities ──\n\n /**\n * Register a cleanup function to be called on {@link dispose}.\n * Cleanups run in registration order.\n * @protected\n */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) this._cleanups = [];\n this._cleanups.push(fn);\n }\n\n /** Lifecycle hook called at the end of dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n}\n"],"names":["bindPublicMethods"],"mappings":";;;AAGA,MAAM,iBAAiB,oBAAI,IAAI,CAAC,cAAc,QAAQ,CAAC;AAwChD,MAAM,UAAgC;AAAA,EACnC,aAAqC;AAAA,EACrC,YAAY;AAAA,EACZ,mBAA2C;AAAA,EAC3C,YAAmC;AAAA,EAE3C,cAAc;AACZA,sBAAAA,kBAAkB,MAAM,OAAO,WAAW,cAAc;AAAA,EAC1D;AAAA;AAAA;AAAA,EAKA,IAAI,WAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,gBAA6B;AAC/B,QAAI,CAAC,KAAK,kBAAkB;AAC1B,WAAK,mBAAmB,IAAI,gBAAA;AAAA,IAC9B;AACA,WAAO,KAAK,iBAAiB;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAgB;AACd,QAAI,KAAK,UAAW;AACpB,SAAK,YAAY;AACjB,SAAK,kBAAkB,MAAA;AACvB,QAAI,KAAK,WAAW;AAClB,iBAAW,MAAM,KAAK,UAAW,IAAA;AACjC,WAAK,YAAY;AAAA,IACnB;AACA,SAAK,YAAY,MAAA;AACjB,SAAK,YAAA;AAAA,EACP;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,UAAU,IAA4B;AACpC,QAAI,CAAC,KAAK,WAAY,MAAK,iCAAiB,IAAA;AAC5C,SAAK,WAAW,IAAI,EAAE;AACtB,WAAO,MAAM;AAAE,WAAK,YAAY,OAAO,EAAE;AAAA,IAAG;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQU,SAAe;AACvB,QAAI,KAAK,YAAY;AACnB,iBAAW,MAAM,KAAK,WAAY,IAAA;AAAA,IACpC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASU,WAAW,IAAsB;AACzC,QAAI,CAAC,KAAK,UAAW,MAAK,YAAY,CAAA;AACtC,SAAK,UAAU,KAAK,EAAE;AAAA,EACxB;AAIF;;"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Disposable } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Base class for custom reactive objects that integrate with ViewModel's
|
|
4
|
+
* auto-tracking system. Provides subscribable notifications, disposal
|
|
5
|
+
* lifecycle, and automatic method binding.
|
|
6
|
+
*
|
|
7
|
+
* Any object with a `subscribe()` method is auto-tracked by ViewModel
|
|
8
|
+
* getters. Trackable gives you that plus cleanup infrastructure and
|
|
9
|
+
* point-free methods — the same building blocks used by Sorting,
|
|
10
|
+
* Selection, Feed, Pagination, and Pending.
|
|
11
|
+
*
|
|
12
|
+
* Use Trackable when integrating third-party SDKs, custom query objects,
|
|
13
|
+
* or any reactive state that doesn't fit ViewModel's state/getter model.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* class RPCQuery<Data> extends Trackable {
|
|
18
|
+
* private _data: Data | undefined;
|
|
19
|
+
* private _loading = false;
|
|
20
|
+
*
|
|
21
|
+
* get data() { return this._data; }
|
|
22
|
+
* get loading() { return this._loading; }
|
|
23
|
+
*
|
|
24
|
+
* async call(): Promise<void> {
|
|
25
|
+
* this._loading = true;
|
|
26
|
+
* this.notify();
|
|
27
|
+
* this._data = await fetchData();
|
|
28
|
+
* this._loading = false;
|
|
29
|
+
* this.notify();
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* // Used as a ViewModel property — auto-tracked:
|
|
34
|
+
* class UsersVM extends ViewModel {
|
|
35
|
+
* readonly users = new RPCQuery<User[]>();
|
|
36
|
+
* get userList() { return this.users.data ?? []; }
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare class Trackable implements Disposable {
|
|
41
|
+
private _listeners;
|
|
42
|
+
private _disposed;
|
|
43
|
+
private _abortController;
|
|
44
|
+
private _cleanups;
|
|
45
|
+
constructor();
|
|
46
|
+
/** Whether this instance has been disposed. */
|
|
47
|
+
get disposed(): boolean;
|
|
48
|
+
/** AbortSignal that fires when this instance is disposed. Lazily created. */
|
|
49
|
+
get disposeSignal(): AbortSignal;
|
|
50
|
+
/**
|
|
51
|
+
* Tear down the instance: abort the dispose signal, run all registered
|
|
52
|
+
* cleanups, clear subscribers, and call onDispose. Idempotent.
|
|
53
|
+
*/
|
|
54
|
+
dispose(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Subscribe to change notifications. The callback is invoked (with no
|
|
57
|
+
* arguments) whenever the subclass calls {@link notify}.
|
|
58
|
+
*
|
|
59
|
+
* This is the duck-typed contract that ViewModel's auto-tracking system
|
|
60
|
+
* recognizes — any object with a `subscribe` method is automatically
|
|
61
|
+
* tracked when accessed inside a ViewModel getter.
|
|
62
|
+
*
|
|
63
|
+
* @returns An unsubscribe function.
|
|
64
|
+
*/
|
|
65
|
+
subscribe(cb: () => void): () => void;
|
|
66
|
+
/**
|
|
67
|
+
* Notify all subscribers that state has changed. Call this after
|
|
68
|
+
* mutating internal state to trigger ViewModel getter invalidation
|
|
69
|
+
* and React re-renders.
|
|
70
|
+
* @protected
|
|
71
|
+
*/
|
|
72
|
+
protected notify(): void;
|
|
73
|
+
/**
|
|
74
|
+
* Register a cleanup function to be called on {@link dispose}.
|
|
75
|
+
* Cleanups run in registration order.
|
|
76
|
+
* @protected
|
|
77
|
+
*/
|
|
78
|
+
protected addCleanup(fn: () => void): void;
|
|
79
|
+
/** Lifecycle hook called at the end of dispose(). Override for custom teardown. @protected */
|
|
80
|
+
protected onDispose?(): void;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=Trackable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Trackable.d.ts","sourceRoot":"","sources":["../src/Trackable.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAK1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,qBAAa,SAAU,YAAW,UAAU;IAC1C,OAAO,CAAC,UAAU,CAAgC;IAClD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,SAAS,CAA+B;;IAQhD,+CAA+C;IAC/C,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,6EAA6E;IAC7E,IAAI,aAAa,IAAI,WAAW,CAK/B;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI;IAcf;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAMrC;;;;;OAKG;IACH,SAAS,CAAC,MAAM,IAAI,IAAI;IAQxB;;;;OAIG;IACH,SAAS,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAK1C,8FAA8F;IAC9F,SAAS,CAAC,SAAS,CAAC,IAAI,IAAI;CAC7B"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { bindPublicMethods } from "./bindPublicMethods.js";
|
|
2
|
+
const PROTECTED_KEYS = /* @__PURE__ */ new Set(["addCleanup", "notify"]);
|
|
3
|
+
class Trackable {
|
|
4
|
+
_listeners = null;
|
|
5
|
+
_disposed = false;
|
|
6
|
+
_abortController = null;
|
|
7
|
+
_cleanups = null;
|
|
8
|
+
constructor() {
|
|
9
|
+
bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);
|
|
10
|
+
}
|
|
11
|
+
// ── Disposable interface ──
|
|
12
|
+
/** Whether this instance has been disposed. */
|
|
13
|
+
get disposed() {
|
|
14
|
+
return this._disposed;
|
|
15
|
+
}
|
|
16
|
+
/** AbortSignal that fires when this instance is disposed. Lazily created. */
|
|
17
|
+
get disposeSignal() {
|
|
18
|
+
if (!this._abortController) {
|
|
19
|
+
this._abortController = new AbortController();
|
|
20
|
+
}
|
|
21
|
+
return this._abortController.signal;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Tear down the instance: abort the dispose signal, run all registered
|
|
25
|
+
* cleanups, clear subscribers, and call onDispose. Idempotent.
|
|
26
|
+
*/
|
|
27
|
+
dispose() {
|
|
28
|
+
if (this._disposed) return;
|
|
29
|
+
this._disposed = true;
|
|
30
|
+
this._abortController?.abort();
|
|
31
|
+
if (this._cleanups) {
|
|
32
|
+
for (const fn of this._cleanups) fn();
|
|
33
|
+
this._cleanups = null;
|
|
34
|
+
}
|
|
35
|
+
this._listeners?.clear();
|
|
36
|
+
this.onDispose?.();
|
|
37
|
+
}
|
|
38
|
+
// ── Subscribable (notification-only, no state) ──
|
|
39
|
+
/**
|
|
40
|
+
* Subscribe to change notifications. The callback is invoked (with no
|
|
41
|
+
* arguments) whenever the subclass calls {@link notify}.
|
|
42
|
+
*
|
|
43
|
+
* This is the duck-typed contract that ViewModel's auto-tracking system
|
|
44
|
+
* recognizes — any object with a `subscribe` method is automatically
|
|
45
|
+
* tracked when accessed inside a ViewModel getter.
|
|
46
|
+
*
|
|
47
|
+
* @returns An unsubscribe function.
|
|
48
|
+
*/
|
|
49
|
+
subscribe(cb) {
|
|
50
|
+
if (!this._listeners) this._listeners = /* @__PURE__ */ new Set();
|
|
51
|
+
this._listeners.add(cb);
|
|
52
|
+
return () => {
|
|
53
|
+
this._listeners?.delete(cb);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Notify all subscribers that state has changed. Call this after
|
|
58
|
+
* mutating internal state to trigger ViewModel getter invalidation
|
|
59
|
+
* and React re-renders.
|
|
60
|
+
* @protected
|
|
61
|
+
*/
|
|
62
|
+
notify() {
|
|
63
|
+
if (this._listeners) {
|
|
64
|
+
for (const cb of this._listeners) cb();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// ── Protected utilities ──
|
|
68
|
+
/**
|
|
69
|
+
* Register a cleanup function to be called on {@link dispose}.
|
|
70
|
+
* Cleanups run in registration order.
|
|
71
|
+
* @protected
|
|
72
|
+
*/
|
|
73
|
+
addCleanup(fn) {
|
|
74
|
+
if (!this._cleanups) this._cleanups = [];
|
|
75
|
+
this._cleanups.push(fn);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
Trackable
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=Trackable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Trackable.js","sources":["../src/Trackable.ts"],"sourcesContent":["import type { Disposable } from './types';\nimport { bindPublicMethods } from './bindPublicMethods';\n\nconst PROTECTED_KEYS = new Set(['addCleanup', 'notify']);\n\n/**\n * Base class for custom reactive objects that integrate with ViewModel's\n * auto-tracking system. Provides subscribable notifications, disposal\n * lifecycle, and automatic method binding.\n *\n * Any object with a `subscribe()` method is auto-tracked by ViewModel\n * getters. Trackable gives you that plus cleanup infrastructure and\n * point-free methods — the same building blocks used by Sorting,\n * Selection, Feed, Pagination, and Pending.\n *\n * Use Trackable when integrating third-party SDKs, custom query objects,\n * or any reactive state that doesn't fit ViewModel's state/getter model.\n *\n * @example\n * ```ts\n * class RPCQuery<Data> extends Trackable {\n * private _data: Data | undefined;\n * private _loading = false;\n *\n * get data() { return this._data; }\n * get loading() { return this._loading; }\n *\n * async call(): Promise<void> {\n * this._loading = true;\n * this.notify();\n * this._data = await fetchData();\n * this._loading = false;\n * this.notify();\n * }\n * }\n *\n * // Used as a ViewModel property — auto-tracked:\n * class UsersVM extends ViewModel {\n * readonly users = new RPCQuery<User[]>();\n * get userList() { return this.users.data ?? []; }\n * }\n * ```\n */\nexport class Trackable implements Disposable {\n private _listeners: Set<() => void> | null = null;\n private _disposed = false;\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n constructor() {\n bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);\n }\n\n // ── Disposable interface ──\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /**\n * Tear down the instance: abort the dispose signal, run all registered\n * cleanups, clear subscribers, and call onDispose. Idempotent.\n */\n dispose(): void {\n if (this._disposed) return;\n this._disposed = true;\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this._listeners?.clear();\n this.onDispose?.();\n }\n\n // ── Subscribable (notification-only, no state) ──\n\n /**\n * Subscribe to change notifications. The callback is invoked (with no\n * arguments) whenever the subclass calls {@link notify}.\n *\n * This is the duck-typed contract that ViewModel's auto-tracking system\n * recognizes — any object with a `subscribe` method is automatically\n * tracked when accessed inside a ViewModel getter.\n *\n * @returns An unsubscribe function.\n */\n subscribe(cb: () => void): () => void {\n if (!this._listeners) this._listeners = new Set();\n this._listeners.add(cb);\n return () => { this._listeners?.delete(cb); };\n }\n\n /**\n * Notify all subscribers that state has changed. Call this after\n * mutating internal state to trigger ViewModel getter invalidation\n * and React re-renders.\n * @protected\n */\n protected notify(): void {\n if (this._listeners) {\n for (const cb of this._listeners) cb();\n }\n }\n\n // ── Protected utilities ──\n\n /**\n * Register a cleanup function to be called on {@link dispose}.\n * Cleanups run in registration order.\n * @protected\n */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) this._cleanups = [];\n this._cleanups.push(fn);\n }\n\n /** Lifecycle hook called at the end of dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n}\n"],"names":[],"mappings":";AAGA,MAAM,iBAAiB,oBAAI,IAAI,CAAC,cAAc,QAAQ,CAAC;AAwChD,MAAM,UAAgC;AAAA,EACnC,aAAqC;AAAA,EACrC,YAAY;AAAA,EACZ,mBAA2C;AAAA,EAC3C,YAAmC;AAAA,EAE3C,cAAc;AACZ,sBAAkB,MAAM,OAAO,WAAW,cAAc;AAAA,EAC1D;AAAA;AAAA;AAAA,EAKA,IAAI,WAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,gBAA6B;AAC/B,QAAI,CAAC,KAAK,kBAAkB;AAC1B,WAAK,mBAAmB,IAAI,gBAAA;AAAA,IAC9B;AACA,WAAO,KAAK,iBAAiB;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAgB;AACd,QAAI,KAAK,UAAW;AACpB,SAAK,YAAY;AACjB,SAAK,kBAAkB,MAAA;AACvB,QAAI,KAAK,WAAW;AAClB,iBAAW,MAAM,KAAK,UAAW,IAAA;AACjC,WAAK,YAAY;AAAA,IACnB;AACA,SAAK,YAAY,MAAA;AACjB,SAAK,YAAA;AAAA,EACP;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,UAAU,IAA4B;AACpC,QAAI,CAAC,KAAK,WAAY,MAAK,iCAAiB,IAAA;AAC5C,SAAK,WAAW,IAAI,EAAE;AACtB,WAAO,MAAM;AAAE,WAAK,YAAY,OAAO,EAAE;AAAA,IAAG;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQU,SAAe;AACvB,QAAI,KAAK,YAAY;AACnB,iBAAW,MAAM,KAAK,WAAY,IAAA;AAAA,IACpC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASU,WAAW,IAAsB;AACzC,QAAI,CAAC,KAAK,UAAW,MAAK,YAAY,CAAA;AACtC,SAAK,UAAU,KAAK,EAAE;AAAA,EACxB;AAIF;"}
|