behavioural-dashboard 0.9.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ricardo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,261 @@
1
+ # behavioural-dashboard
2
+
3
+ A framework-agnostic TypeScript engine that tracks user interaction patterns and outputs serializable state for adaptive UIs. No DOM manipulation, no CSS, no framework lock-in — just behavioural primitives you wire into your own components.
4
+
5
+ > **ESM only.** This package ships ES modules exclusively. It requires a bundler or a runtime that supports `"type": "module"`.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install behavioural-dashboard
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```typescript
16
+ import { BehaviouralEngine } from 'behavioural-dashboard';
17
+
18
+ const engine = new BehaviouralEngine({
19
+ budget: 100,
20
+ increment: 5,
21
+ growthRate: 0.2,
22
+ variants: ['compact', 'standard', 'expanded'],
23
+ });
24
+
25
+ // Register widgets — optionally with initial scores for a pre-seeded layout
26
+ engine.register('orders', 40);
27
+ engine.register('fleet', 35);
28
+ engine.register('alerts', 25);
29
+
30
+ // Record an interaction
31
+ engine.record('orders');
32
+
33
+ // Read state (returned in registration order)
34
+ const states = engine.getState();
35
+ // [
36
+ // { id: 'orders', score: 43.2, weight: 0.432, clicks: 1, variant: 'standard' },
37
+ // { id: 'fleet', score: 32.1, weight: 0.321, clicks: 0, variant: 'compact' },
38
+ // { id: 'alerts', score: 24.7, weight: 0.247, clicks: 0, variant: 'compact' },
39
+ // ]
40
+
41
+ // Subscribe to changes
42
+ engine.on('change', (states) => {
43
+ for (const s of states) {
44
+ const el = document.getElementById(s.id)!;
45
+ el.className = `widget widget--${s.variant}`;
46
+ el.style.flexGrow = String(s.weight * states.length);
47
+ }
48
+ });
49
+ ```
50
+
51
+ ## How it works
52
+
53
+ ### Zero-sum budget scoring
54
+
55
+ A fixed **budget** (default: `100`) is shared across all registered widgets. Scores always sum to exactly the budget. When a widget is interacted with, it gains points drained proportionally from every other widget — widgets that hold more score lose more than those that hold less.
56
+
57
+ ### Proportional drain redistribution
58
+
59
+ On each `record(id)` call:
60
+
61
+ 1. The engine collects all widgets other than the target.
62
+ 2. It drains up to `increment` points from them, taking from each in proportion to its current score.
63
+ 3. Those points are added to the target widget's score.
64
+ 4. Scores are renormalized so the total stays at `budget`.
65
+
66
+ Unused widgets shrink only when other widgets are clicked. There is no time-based decay.
67
+
68
+ ### Weight-based variant resolution
69
+
70
+ Each widget's **weight** is its fraction of the total budget:
71
+
72
+ ```
73
+ weight = score / budget // range: 0–1
74
+ ```
75
+
76
+ The active variant is chosen by mapping weight through the `growthRate` step:
77
+
78
+ ```
79
+ variantIndex = min(floor(weight / growthRate), variants.length - 1)
80
+ ```
81
+
82
+ With the default `growthRate` of `0.2` and variants `['compact', 'standard', 'expanded']`:
83
+
84
+ | weight range | variantIndex | variant |
85
+ |---|---|---|
86
+ | 0.00 – 0.19 | 0 | `compact` |
87
+ | 0.20 – 0.39 | 1 | `standard` |
88
+ | 0.40 – 1.00 | 2 | `expanded` |
89
+
90
+ A smaller `growthRate` makes variants unlock at lower weights (faster progression). A larger value requires a widget to hold more of the budget before advancing.
91
+
92
+ ### Pre-seeded default layouts
93
+
94
+ Registering widgets with explicit initial scores sets a default layout:
95
+
96
+ ```typescript
97
+ engine.register('primary', 50); // starts prominent
98
+ engine.register('secondary', 30);
99
+ engine.register('tertiary', 20); // starts small
100
+ ```
101
+
102
+ If no initial scores are given, all widgets start at `0` and receive an equal share on first normalization. `reset()` always returns to those initial values.
103
+
104
+ ## Configuration
105
+
106
+ | Option | Default | Description |
107
+ |---|---|---|
108
+ | `budget` | `100` | Total score pool shared across all widgets |
109
+ | `increment` | `5` | Points transferred per interaction |
110
+ | `growthRate` | `0.2` | Weight step per variant level (lower = faster unlock) |
111
+ | `variants` | `['default']` | Ordered variant names, least to most prominent |
112
+
113
+ All options are optional. The constructor throws if `budget`, `increment`, or `growthRate` are non-positive, or if `variants` is empty.
114
+
115
+ ## API reference
116
+
117
+ ### `new BehaviouralEngine(config?)`
118
+
119
+ Creates a new engine. All config fields are optional and fall back to defaults.
120
+
121
+ ### `register(id: string, initialScore?: number): void`
122
+
123
+ Registers a widget. Throws if `id` is already registered. Does not emit a `'change'` event.
124
+
125
+ If `initialScore` is omitted the widget starts at score `0`. What happens next depends on the mix of registered widgets:
126
+
127
+ - **All widgets have score 0** (no explicit initial scores anywhere): on first normalization they receive an equal share of the budget.
128
+ - **Some widgets have explicit scores**: the scoreless widget stays at `0` until other widgets are interacted with; it then receives budget share proportionally through normalization, but it does not get an automatic equal slice.
129
+
130
+ ### `record(id: string): void`
131
+
132
+ Records one interaction on `id`. Redistributes the budget and fires `'change'` listeners. Throws if `id` is not registered.
133
+
134
+ ### `getState(): WidgetState[]`
135
+
136
+ Returns the current state of all widgets in **registration order**. The array is not sorted by score.
137
+
138
+ ### `getWidget(id: string): WidgetState`
139
+
140
+ Returns the current state of a single widget. Throws if `id` is not registered.
141
+
142
+ ### `export(): AdaptiveState`
143
+
144
+ Returns a plain, JSON-serializable snapshot of the engine state. See [Persistence](#persistence).
145
+
146
+ ### `import(state: AdaptiveState): void`
147
+
148
+ Restores from a snapshot. Widgets in the engine that are absent from the snapshot retain their current scores; the scores are then renormalized. Widgets in the snapshot that are not registered in the engine are ignored. Throws on unsupported `version` values.
149
+
150
+ ### `reset(): void`
151
+
152
+ Resets all widgets to their initial scores and zeroes click counts. Fires `'change'` listeners.
153
+
154
+ ### `on(event: 'change', cb: (states: WidgetState[]) => void): void`
155
+
156
+ Subscribes to state changes. `cb` is called after every `record`, `import`, and `reset`.
157
+
158
+ ### `off(event: 'change', cb: (states: WidgetState[]) => void): void`
159
+
160
+ Unsubscribes a previously registered listener.
161
+
162
+ ### `destroy(): void`
163
+
164
+ Removes all listeners. Call this when tearing down a component to prevent memory leaks.
165
+
166
+ ## WidgetState shape
167
+
168
+ ```typescript
169
+ interface WidgetState {
170
+ id: string; // widget identifier
171
+ score: number; // raw score (sums to budget across all widgets)
172
+ weight: number; // score / budget, range 0–1
173
+ clicks: number; // total interactions recorded since last reset
174
+ variant: string; // active variant name from your variants array
175
+ }
176
+ ```
177
+
178
+ There is no `order` field. If you need a sorted list, sort `getState()` yourself:
179
+
180
+ ```typescript
181
+ const ranked = engine.getState().sort((a, b) => b.score - a.score);
182
+ ```
183
+
184
+ ## Styling
185
+
186
+ This library produces **data, not DOM**. Wiring state to your UI is your responsibility.
187
+
188
+ | Engine output | Suggested UI mapping |
189
+ |---|---|
190
+ | `weight` (0–1) | Size the widget: `flex-grow`, `grid-column: span N`, width percentage |
191
+ | `variant` | Apply a CSS class: `.widget--compact`, `.widget--standard`, etc. |
192
+ | `clicks > 0` | Progressive disclosure: reveal detail layers after first interaction |
193
+
194
+ Example — flex layout that grows widgets proportionally:
195
+
196
+ ```typescript
197
+ engine.on('change', (states) => {
198
+ for (const s of states) {
199
+ const el = document.getElementById(s.id)!;
200
+ el.className = `widget widget--${s.variant}`;
201
+ el.style.flexGrow = String(s.weight * states.length);
202
+ }
203
+ });
204
+ ```
205
+
206
+ Recommended CSS transitions so size and style changes animate smoothly:
207
+
208
+ ```css
209
+ .widget {
210
+ transition: flex-grow 0.4s ease, min-height 0.4s ease;
211
+ }
212
+ ```
213
+
214
+ ## Persistence
215
+
216
+ `export()` returns an `AdaptiveState` object you can serialize and store anywhere. The type is exported and can be imported directly:
217
+
218
+ ```typescript
219
+ import type { AdaptiveState } from 'behavioural-dashboard';
220
+ ```
221
+
222
+ ```typescript
223
+ interface AdaptiveState {
224
+ version: 1;
225
+ widgets: Array<{ id: string; score: number; clicks: number }>;
226
+ lastInteraction: number; // Unix timestamp (ms) of the most recent record() call
227
+ }
228
+ ```
229
+
230
+ Example with `localStorage`:
231
+
232
+ ```typescript
233
+ // Save after every change
234
+ engine.on('change', () => {
235
+ localStorage.setItem('dashboard', JSON.stringify(engine.export()));
236
+ });
237
+
238
+ // Restore on page load
239
+ const raw = localStorage.getItem('dashboard');
240
+ if (raw) {
241
+ engine.import(JSON.parse(raw));
242
+ }
243
+ ```
244
+
245
+ The same pattern works with `IndexedDB`, a REST endpoint, or any other store — `AdaptiveState` is plain JSON with no class instances or circular references.
246
+
247
+ ## Demo
248
+
249
+ [ricardomonteirosimoes.github.io/behavioural-dashboard](https://ricardomonteirosimoes.github.io/behavioural-dashboard/)
250
+
251
+ ## Attribution
252
+
253
+ The concept of a behavioural, interaction-adaptive dashboard was designed and demonstrated by [Tanvi Palkar](https://www.linkedin.com/in/tanvi-palkar-ab9b159a/). Her original interactive demo is available at [tanvin-alt.github.io/behavioural-dashboard](https://tanvin-alt.github.io/behavioural-dashboard/) ([source](https://github.com/tanvin-alt/behavioural-dashboard)).
254
+
255
+ This library extracts and generalises the mechanics from that demo into a framework-agnostic TypeScript package.
256
+
257
+ Vibe-engineered with Claude (Anthropic).
258
+
259
+ ## License
260
+
261
+ MIT
@@ -0,0 +1,51 @@
1
+ import type { AdaptiveState } from './state';
2
+ export interface BehaviouralEngineConfig {
3
+ /** Total score budget across all widgets. Default: 100 */
4
+ budget: number;
5
+ /** Score transferred per interaction. Default: 5 */
6
+ increment: number;
7
+ /** Weight threshold step per variant level. A widget needs weight >= (level * growthRate) to reach that variant. With the default of 0.2, variants unlock at weights 0, 0.2, 0.4, 0.6. Default: 0.2 */
8
+ growthRate: number;
9
+ /** Ordered variant names, least to most prominent. */
10
+ variants: string[];
11
+ }
12
+ export interface WidgetState {
13
+ id: string;
14
+ score: number;
15
+ weight: number;
16
+ clicks: number;
17
+ variant: string;
18
+ }
19
+ type ChangeListener = (states: WidgetState[]) => void;
20
+ export declare class BehaviouralEngine {
21
+ private readonly config;
22
+ private readonly widgets;
23
+ private readonly listeners;
24
+ private lastInteraction;
25
+ private dirty;
26
+ constructor(config?: Partial<BehaviouralEngineConfig>);
27
+ register(id: string, initialScore?: number): void;
28
+ record(id: string): void;
29
+ getState(): WidgetState[];
30
+ getWidget(id: string): WidgetState;
31
+ export(): AdaptiveState;
32
+ /**
33
+ * Restores engine state from a previously exported snapshot.
34
+ *
35
+ * Partial state is fully supported: widgets registered in this engine that
36
+ * are absent from the imported snapshot retain their current scores. After
37
+ * all matching widgets are updated, scores are renormalized to the configured
38
+ * budget, so the absent widgets participate in that redistribution.
39
+ */
40
+ import(state: AdaptiveState): void;
41
+ reset(): void;
42
+ on(event: 'change', cb: ChangeListener): void;
43
+ off(event: 'change', cb: ChangeListener): void;
44
+ destroy(): void;
45
+ private toWidgetState;
46
+ private ensureNormalized;
47
+ private normalizeScores;
48
+ private emit;
49
+ }
50
+ export {};
51
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,MAAM,WAAW,uBAAuB;IACtC,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,uMAAuM;IACvM,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,KAAK,cAAc,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;AAiBtD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAC1D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,KAAK,CAAQ;gBAET,MAAM,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC;IAgBrD,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI;IAsBjD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAgCxB,QAAQ,IAAI,WAAW,EAAE;IAKzB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW;IASlC,MAAM,IAAI,aAAa;IAavB;;;;;;;OAOG;IACH,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IA0BlC,KAAK,IAAI,IAAI;IAWb,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI;IAO7C,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI;IAO9C,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,eAAe;IAwBvB,OAAO,CAAC,IAAI;CAMb"}
package/dist/engine.js ADDED
@@ -0,0 +1,206 @@
1
+ const DEFAULTS = {
2
+ budget: 100,
3
+ increment: 5,
4
+ growthRate: 0.2,
5
+ variants: ['default'],
6
+ };
7
+ export class BehaviouralEngine {
8
+ config;
9
+ widgets = new Map();
10
+ listeners = new Set();
11
+ lastInteraction = 0;
12
+ dirty = true;
13
+ constructor(config) {
14
+ this.config = { ...DEFAULTS, ...config };
15
+ if (this.config.variants.length === 0) {
16
+ throw new Error('variants must contain at least one entry');
17
+ }
18
+ if (this.config.budget <= 0) {
19
+ throw new Error('budget must be positive');
20
+ }
21
+ if (this.config.growthRate <= 0) {
22
+ throw new Error('growthRate must be positive');
23
+ }
24
+ if (this.config.increment <= 0) {
25
+ throw new Error('increment must be positive');
26
+ }
27
+ }
28
+ register(id, initialScore) {
29
+ if (this.widgets.has(id)) {
30
+ throw new Error(`Widget "${id}" is already registered`);
31
+ }
32
+ if (initialScore !== undefined) {
33
+ if (!Number.isFinite(initialScore) || initialScore < 0) {
34
+ throw new Error(`initialScore must be a finite non-negative number, got: ${initialScore}`);
35
+ }
36
+ }
37
+ const score = initialScore ?? 0;
38
+ this.widgets.set(id, {
39
+ id,
40
+ score,
41
+ clicks: 0,
42
+ initialScore: score,
43
+ hasExplicitInitial: initialScore !== undefined,
44
+ });
45
+ this.dirty = true;
46
+ }
47
+ record(id) {
48
+ const widget = this.widgets.get(id);
49
+ if (!widget) {
50
+ throw new Error(`Widget "${id}" is not registered`);
51
+ }
52
+ this.ensureNormalized();
53
+ widget.clicks++;
54
+ this.lastInteraction = Date.now();
55
+ let sumOthers = 0;
56
+ for (const w of this.widgets.values()) {
57
+ if (w.id !== id)
58
+ sumOthers += w.score;
59
+ }
60
+ if (sumOthers > 0) {
61
+ const drain = Math.min(this.config.increment, sumOthers);
62
+ let actualDrain = 0;
63
+ for (const other of this.widgets.values()) {
64
+ if (other.id === id)
65
+ continue;
66
+ const loss = drain * (other.score / sumOthers);
67
+ const clamped = Math.max(0, other.score - loss);
68
+ actualDrain += other.score - clamped;
69
+ other.score = clamped;
70
+ }
71
+ widget.score += actualDrain;
72
+ }
73
+ this.normalizeScores();
74
+ this.emit();
75
+ }
76
+ getState() {
77
+ this.ensureNormalized();
78
+ return [...this.widgets.values()].map((w) => this.toWidgetState(w));
79
+ }
80
+ getWidget(id) {
81
+ this.ensureNormalized();
82
+ const widget = this.widgets.get(id);
83
+ if (!widget) {
84
+ throw new Error(`Widget "${id}" is not registered`);
85
+ }
86
+ return this.toWidgetState(widget);
87
+ }
88
+ export() {
89
+ this.ensureNormalized();
90
+ return {
91
+ version: 1,
92
+ widgets: [...this.widgets.values()].map((w) => ({
93
+ id: w.id,
94
+ score: w.score,
95
+ clicks: w.clicks,
96
+ })),
97
+ lastInteraction: this.lastInteraction,
98
+ };
99
+ }
100
+ /**
101
+ * Restores engine state from a previously exported snapshot.
102
+ *
103
+ * Partial state is fully supported: widgets registered in this engine that
104
+ * are absent from the imported snapshot retain their current scores. After
105
+ * all matching widgets are updated, scores are renormalized to the configured
106
+ * budget, so the absent widgets participate in that redistribution.
107
+ */
108
+ import(state) {
109
+ if (state.version !== 1) {
110
+ throw new Error(`Unsupported state version: ${state.version}`);
111
+ }
112
+ for (const ws of state.widgets) {
113
+ const widget = this.widgets.get(ws.id);
114
+ if (widget) {
115
+ // Only update widgets that exist in this engine; unknown ids are ignored.
116
+ if (!Number.isFinite(ws.score) || ws.score < 0)
117
+ continue;
118
+ if (!Number.isFinite(ws.clicks) ||
119
+ !Number.isInteger(ws.clicks) ||
120
+ ws.clicks < 0)
121
+ continue;
122
+ widget.score = ws.score;
123
+ widget.clicks = ws.clicks;
124
+ }
125
+ }
126
+ this.lastInteraction = Number.isFinite(state.lastInteraction)
127
+ ? state.lastInteraction
128
+ : 0;
129
+ this.normalizeScores();
130
+ this.emit();
131
+ }
132
+ reset() {
133
+ for (const widget of this.widgets.values()) {
134
+ widget.score = widget.initialScore;
135
+ widget.clicks = 0;
136
+ }
137
+ this.lastInteraction = 0;
138
+ this.dirty = true;
139
+ this.ensureNormalized();
140
+ this.emit();
141
+ }
142
+ on(event, cb) {
143
+ if (event !== 'change') {
144
+ throw new Error(`Unknown event: "${event}"`);
145
+ }
146
+ this.listeners.add(cb);
147
+ }
148
+ off(event, cb) {
149
+ if (event !== 'change') {
150
+ throw new Error(`Unknown event: "${event}"`);
151
+ }
152
+ this.listeners.delete(cb);
153
+ }
154
+ destroy() {
155
+ this.listeners.clear();
156
+ }
157
+ toWidgetState(entry) {
158
+ const { budget, growthRate, variants } = this.config;
159
+ const weight = budget > 0 ? entry.score / budget : 0;
160
+ const variantIndex = Math.min(Math.floor(weight / growthRate), variants.length - 1);
161
+ return {
162
+ id: entry.id,
163
+ score: entry.score,
164
+ weight,
165
+ clicks: entry.clicks,
166
+ variant: variants[variantIndex],
167
+ };
168
+ }
169
+ ensureNormalized() {
170
+ if (!this.dirty)
171
+ return;
172
+ this.normalizeScores();
173
+ this.dirty = false;
174
+ }
175
+ normalizeScores() {
176
+ const entries = [...this.widgets.values()];
177
+ if (entries.length === 0)
178
+ return;
179
+ const sum = entries.reduce((s, w) => s + w.score, 0);
180
+ if (sum === 0) {
181
+ const equal = this.config.budget / entries.length;
182
+ for (const entry of entries) {
183
+ entry.score = equal;
184
+ // Stamp the equal-share as initialScore so that reset() restores equal
185
+ // distribution for widgets that were registered without explicit scores.
186
+ // Widgets registered with an explicit initialScore (even 0) are excluded
187
+ // because their initialScore was deliberately set at registration time.
188
+ if (!entry.hasExplicitInitial) {
189
+ entry.initialScore = equal;
190
+ }
191
+ }
192
+ return;
193
+ }
194
+ const factor = this.config.budget / sum;
195
+ for (const entry of entries) {
196
+ entry.score *= factor;
197
+ }
198
+ }
199
+ emit() {
200
+ const states = this.getState();
201
+ for (const listener of this.listeners) {
202
+ listener(states);
203
+ }
204
+ }
205
+ }
206
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AA+BA,MAAM,QAAQ,GAA4B;IACxC,MAAM,EAAE,GAAG;IACX,SAAS,EAAE,CAAC;IACZ,UAAU,EAAE,GAAG;IACf,QAAQ,EAAE,CAAC,SAAS,CAAC;CACtB,CAAC;AAEF,MAAM,OAAO,iBAAiB;IACX,MAAM,CAA0B;IAChC,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IACzC,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC/C,eAAe,GAAG,CAAC,CAAC;IACpB,KAAK,GAAG,IAAI,CAAC;IAErB,YAAY,MAAyC;QACnD,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,QAAQ,EAAE,GAAG,MAAM,EAAE,CAAC;QACzC,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtC,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;QAC9D,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC7C,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;QACjD,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;IAED,QAAQ,CAAC,EAAU,EAAE,YAAqB;QACxC,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,WAAW,EAAE,yBAAyB,CAAC,CAAC;QAC1D,CAAC;QACD,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;YAC/B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;gBACvD,MAAM,IAAI,KAAK,CACb,2DAA2D,YAAY,EAAE,CAC1E,CAAC;YACJ,CAAC;QACH,CAAC;QACD,MAAM,KAAK,GAAG,YAAY,IAAI,CAAC,CAAC;QAChC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE;YACnB,EAAE;YACF,KAAK;YACL,MAAM,EAAE,CAAC;YACT,YAAY,EAAE,KAAK;YACnB,kBAAkB,EAAE,YAAY,KAAK,SAAS;SAC/C,CAAC,CAAC;QACH,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;IAED,MAAM,CAAC,EAAU;QACf,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACpC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,WAAW,EAAE,qBAAqB,CAAC,CAAC;QACtD,CAAC;QAED,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACxB,MAAM,CAAC,MAAM,EAAE,CAAC;QAChB,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAElC,IAAI,SAAS,GAAG,CAAC,CAAC;QAClB,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YACtC,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE;gBAAE,SAAS,IAAI,CAAC,CAAC,KAAK,CAAC;QACxC,CAAC;QAED,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;YAClB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YACzD,IAAI,WAAW,GAAG,CAAC,CAAC;YACpB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;gBAC1C,IAAI,KAAK,CAAC,EAAE,KAAK,EAAE;oBAAE,SAAS;gBAC9B,MAAM,IAAI,GAAG,KAAK,GAAG,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;gBAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;gBAChD,WAAW,IAAI,KAAK,CAAC,KAAK,GAAG,OAAO,CAAC;gBACrC,KAAK,CAAC,KAAK,GAAG,OAAO,CAAC;YACxB,CAAC;YACD,MAAM,CAAC,KAAK,IAAI,WAAW,CAAC;QAC9B,CAAC;QAED,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAED,QAAQ;QACN,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACtE,CAAC;IAED,SAAS,CAAC,EAAU;QAClB,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACxB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACpC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,WAAW,EAAE,qBAAqB,CAAC,CAAC;QACtD,CAAC;QACD,OAAO,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,CAAC;IAED,MAAM;QACJ,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACxB,OAAO;YACL,OAAO,EAAE,CAAC;YACV,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC9C,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,MAAM,EAAE,CAAC,CAAC,MAAM;aACjB,CAAC,CAAC;YACH,eAAe,EAAE,IAAI,CAAC,eAAe;SACtC,CAAC;IACJ,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,KAAoB;QACzB,IAAI,KAAK,CAAC,OAAO,KAAK,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,8BAA8B,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACjE,CAAC;QACD,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;YACvC,IAAI,MAAM,EAAE,CAAC;gBACX,0EAA0E;gBAC1E,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,GAAG,CAAC;oBAAE,SAAS;gBACzD,IACE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,CAAC;oBAC3B,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,MAAM,CAAC;oBAC5B,EAAE,CAAC,MAAM,GAAG,CAAC;oBAEb,SAAS;gBACX,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC;gBACxB,MAAM,CAAC,MAAM,GAAG,EAAE,CAAC,MAAM,CAAC;YAC5B,CAAC;QACH,CAAC;QACD,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,eAAe,CAAC;YAC3D,CAAC,CAAC,KAAK,CAAC,eAAe;YACvB,CAAC,CAAC,CAAC,CAAC;QACN,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAED,KAAK;QACH,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YAC3C,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC;YACnC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;QACpB,CAAC;QACD,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACxB,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAED,EAAE,CAAC,KAAe,EAAE,EAAkB;QACpC,IAAK,KAAgB,KAAK,QAAQ,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,mBAAmB,KAAK,GAAG,CAAC,CAAC;QAC/C,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACzB,CAAC;IAED,GAAG,CAAC,KAAe,EAAE,EAAkB;QACrC,IAAK,KAAgB,KAAK,QAAQ,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,mBAAmB,KAAK,GAAG,CAAC,CAAC;QAC/C,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC5B,CAAC;IAED,OAAO;QACL,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;IAEO,aAAa,CAAC,KAAkB;QACtC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QACrD,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QACrD,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAC3B,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,UAAU,CAAC,EAC/B,QAAQ,CAAC,MAAM,GAAG,CAAC,CACpB,CAAC;QACF,OAAO;YACL,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,MAAM;YACN,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC;SAChC,CAAC;IACJ,CAAC;IAEO,gBAAgB;QACtB,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO;QACxB,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IAEO,eAAe;QACrB,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAC3C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACjC,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QACrD,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC;YACd,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAClD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;gBAC5B,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;gBACpB,uEAAuE;gBACvE,yEAAyE;gBACzE,yEAAyE;gBACzE,wEAAwE;gBACxE,IAAI,CAAC,KAAK,CAAC,kBAAkB,EAAE,CAAC;oBAC9B,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;gBAC7B,CAAC;YACH,CAAC;YACD,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC;QACxC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,KAAK,CAAC,KAAK,IAAI,MAAM,CAAC;QACxB,CAAC;IACH,CAAC;IAEO,IAAI;QACV,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC/B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACtC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,4 @@
1
+ export { BehaviouralEngine } from './engine';
2
+ export type { BehaviouralEngineConfig, WidgetState } from './engine';
3
+ export type { AdaptiveState } from './state';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,YAAY,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACrE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { BehaviouralEngine } from './engine';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Serializable snapshot of the full engine state.
3
+ * Designed to be JSON.stringify'd and stored anywhere:
4
+ * localStorage, IndexedDB, REST API, etc.
5
+ */
6
+ export interface AdaptiveState {
7
+ version: 1;
8
+ widgets: {
9
+ id: string;
10
+ score: number;
11
+ clicks: number;
12
+ }[];
13
+ lastInteraction: number;
14
+ }
15
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACzD,eAAe,EAAE,MAAM,CAAC;CACzB"}
package/dist/state.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "behavioural-dashboard",
3
+ "version": "0.9.1",
4
+ "description": "Framework-agnostic TypeScript engine for self-adapting UI layouts based on user behaviour",
5
+ "type": "module",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.build.json",
21
+ "lint": "eslint src/ demo/",
22
+ "lint:fix": "eslint src/ demo/ --fix",
23
+ "format": "prettier --write src/ demo/",
24
+ "format:check": "prettier --check src/ demo/",
25
+ "knip": "knip",
26
+ "duplication": "jscpd src/",
27
+ "test": "vitest run",
28
+ "check": "tsc --noEmit && eslint src/ demo/ && prettier --check src/ demo/ && knip && jscpd src/ && vitest run",
29
+ "demo": "vite",
30
+ "demo:build": "vite build",
31
+ "demo:preview": "vite preview",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "keywords": [
35
+ "behavioural-ui",
36
+ "adaptive-dashboard",
37
+ "interaction-tracking",
38
+ "progressive-disclosure",
39
+ "zero-sum-budget",
40
+ "typescript",
41
+ "framework-agnostic"
42
+ ],
43
+ "author": "Ricardo Monteiro Simoes",
44
+ "license": "MIT",
45
+ "homepage": "https://ricardomonteirosimoes.github.io/behavioural-dashboard/",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/RicardoMonteiroSimoes/behavioural-dashboard.git"
49
+ },
50
+ "devDependencies": {
51
+ "@eslint/js": "^9.27.0",
52
+ "eslint": "^9.27.0",
53
+ "eslint-config-prettier": "^10.1.5",
54
+ "jscpd": "^4.0.5",
55
+ "knip": "^5.57.2",
56
+ "prettier": "^3.5.3",
57
+ "typescript": "^5.8.3",
58
+ "typescript-eslint": "^8.32.1",
59
+ "vite": "^6.3.5",
60
+ "vitest": "^3.1.4",
61
+ "happy-dom": "^18.0.1"
62
+ }
63
+ }