pulse-js-framework 1.4.3 → 1.4.5

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/runtime/store.js CHANGED
@@ -1,13 +1,71 @@
1
1
  /**
2
2
  * Pulse Store - Global state management
3
+ * @module pulse-js-framework/runtime/store
3
4
  *
4
- * A simple but powerful store that integrates with Pulse reactivity
5
+ * A simple but powerful store that integrates with Pulse reactivity.
6
+ *
7
+ * @example
8
+ * import { createStore, createActions, createGetters } from './store.js';
9
+ *
10
+ * const store = createStore({ count: 0, name: 'App' });
11
+ * const actions = createActions(store, {
12
+ * increment: (store) => store.count.update(n => n + 1)
13
+ * });
14
+ *
15
+ * actions.increment();
5
16
  */
6
17
 
7
18
  import { pulse, computed, effect, batch } from './pulse.js';
19
+ import { loggers, createLogger } from './logger.js';
20
+
21
+ const log = loggers.store;
22
+
23
+ /**
24
+ * @typedef {Object} StoreOptions
25
+ * @property {boolean} [persist=false] - Persist state to localStorage
26
+ * @property {string} [storageKey='pulse-store'] - Key for localStorage persistence
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} Store
31
+ * @property {function(): Object} $getState - Get current state snapshot
32
+ * @property {function(Object): void} $setState - Update multiple values at once
33
+ * @property {function(): void} $reset - Reset to initial state
34
+ * @property {function(function): function} $subscribe - Subscribe to state changes
35
+ * @property {Object.<string, Pulse>} $pulses - Access underlying pulse objects
36
+ */
37
+
38
+ /**
39
+ * @typedef {Object} ModuleDef
40
+ * @property {Object} [state] - Initial module state
41
+ * @property {Object.<string, function>} [actions] - Module actions
42
+ * @property {Object.<string, function>} [getters] - Module getters
43
+ */
8
44
 
9
45
  /**
10
- * Create a global store
46
+ * @typedef {function(Store): Store} StorePlugin
47
+ */
48
+
49
+ /**
50
+ * Create a global store with reactive state properties.
51
+ * @template T
52
+ * @param {T} [initialState={}] - Initial state object
53
+ * @param {StoreOptions} [options={}] - Store configuration
54
+ * @returns {T & Store} Store with reactive properties and helper methods
55
+ * @example
56
+ * // Basic store
57
+ * const store = createStore({ count: 0, user: null });
58
+ * store.count.get(); // 0
59
+ * store.count.set(5);
60
+ *
61
+ * // With persistence
62
+ * const store = createStore(
63
+ * { theme: 'dark', lang: 'en' },
64
+ * { persist: true, storageKey: 'app-settings' }
65
+ * );
66
+ *
67
+ * // Batch updates
68
+ * store.$setState({ theme: 'light', lang: 'fr' });
11
69
  */
12
70
  export function createStore(initialState = {}, options = {}) {
13
71
  const { persist = false, storageKey = 'pulse-store' } = options;
@@ -21,14 +79,22 @@ export function createStore(initialState = {}, options = {}) {
21
79
  state = { ...initialState, ...JSON.parse(saved) };
22
80
  }
23
81
  } catch (e) {
24
- console.warn('Failed to load persisted state:', e);
82
+ log.warn('Failed to load persisted state:', e);
25
83
  }
26
84
  }
27
85
 
28
86
  // Create pulses for each state property
87
+ /** @type {Object.<string, Pulse>} */
29
88
  const pulses = {};
30
89
  const store = {};
31
90
 
91
+ /**
92
+ * Create a pulse for a state value, handling nested objects
93
+ * @private
94
+ * @param {string} key - State key
95
+ * @param {*} value - Initial value
96
+ * @returns {Pulse|Object} Pulse or nested object of pulses
97
+ */
32
98
  function createPulse(key, value) {
33
99
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
34
100
  // Nested object - create nested store
@@ -59,13 +125,14 @@ export function createStore(initialState = {}, options = {}) {
59
125
  try {
60
126
  localStorage.setItem(storageKey, JSON.stringify(snapshot));
61
127
  } catch (e) {
62
- console.warn('Failed to persist state:', e);
128
+ log.warn('Failed to persist state:', e);
63
129
  }
64
130
  });
65
131
  }
66
132
 
67
133
  /**
68
- * Get a snapshot of the current state
134
+ * Get a snapshot of the current state (without tracking)
135
+ * @returns {Object} Plain object with current values
69
136
  */
70
137
  function getState() {
71
138
  const snapshot = {};
@@ -76,7 +143,9 @@ export function createStore(initialState = {}, options = {}) {
76
143
  }
77
144
 
78
145
  /**
79
- * Set multiple values at once
146
+ * Set multiple values at once (batched)
147
+ * @param {Object} updates - Key-value pairs to update
148
+ * @returns {void}
80
149
  */
81
150
  function setState(updates) {
82
151
  batch(() => {
@@ -90,6 +159,7 @@ export function createStore(initialState = {}, options = {}) {
90
159
 
91
160
  /**
92
161
  * Reset state to initial values
162
+ * @returns {void}
93
163
  */
94
164
  function reset() {
95
165
  batch(() => {
@@ -103,6 +173,8 @@ export function createStore(initialState = {}, options = {}) {
103
173
 
104
174
  /**
105
175
  * Subscribe to all state changes
176
+ * @param {function(Object): void} callback - Called with current state on each change
177
+ * @returns {function(): void} Unsubscribe function
106
178
  */
107
179
  function subscribe(callback) {
108
180
  return effect(() => {
@@ -122,7 +194,23 @@ export function createStore(initialState = {}, options = {}) {
122
194
  }
123
195
 
124
196
  /**
125
- * Create actions that can modify the store
197
+ * Create bound actions that can modify the store.
198
+ * Actions receive the store as their first argument.
199
+ * @template T
200
+ * @param {Store} store - The store to bind actions to
201
+ * @param {Object.<string, function(Store, ...any): any>} actions - Action definitions
202
+ * @returns {Object.<string, function(...any): any>} Bound action functions
203
+ * @example
204
+ * const store = createStore({ count: 0 });
205
+ *
206
+ * const actions = createActions(store, {
207
+ * increment: (store) => store.count.update(n => n + 1),
208
+ * decrement: (store) => store.count.update(n => n - 1),
209
+ * add: (store, amount) => store.count.update(n => n + amount)
210
+ * });
211
+ *
212
+ * actions.increment();
213
+ * actions.add(10);
126
214
  */
127
215
  export function createActions(store, actions) {
128
216
  const boundActions = {};
@@ -137,7 +225,24 @@ export function createActions(store, actions) {
137
225
  }
138
226
 
139
227
  /**
140
- * Create getters (computed values) for the store
228
+ * Create computed getters for the store.
229
+ * Getters are memoized and update automatically when dependencies change.
230
+ * @param {Store} store - The store to create getters for
231
+ * @param {Object.<string, function(Store): any>} getters - Getter definitions
232
+ * @returns {Object.<string, Pulse>} Object of computed pulses
233
+ * @example
234
+ * const store = createStore({ items: [], filter: '' });
235
+ *
236
+ * const getters = createGetters(store, {
237
+ * filteredItems: (store) => {
238
+ * const items = store.items.get();
239
+ * const filter = store.filter.get();
240
+ * return items.filter(i => i.includes(filter));
241
+ * },
242
+ * itemCount: (store) => store.items.get().length
243
+ * });
244
+ *
245
+ * getters.filteredItems.get(); // Computed value
141
246
  */
142
247
  export function createGetters(store, getters) {
143
248
  const boundGetters = {};
@@ -150,7 +255,21 @@ export function createGetters(store, getters) {
150
255
  }
151
256
 
152
257
  /**
153
- * Combine multiple stores
258
+ * Combine multiple stores into a single object.
259
+ * Each store is namespaced under its key.
260
+ * @param {Object.<string, Store>} stores - Stores to combine
261
+ * @returns {Object.<string, Store>} Combined stores object
262
+ * @example
263
+ * const userStore = createStore({ name: '', email: '' });
264
+ * const settingsStore = createStore({ theme: 'dark' });
265
+ *
266
+ * const rootStore = combineStores({
267
+ * user: userStore,
268
+ * settings: settingsStore
269
+ * });
270
+ *
271
+ * rootStore.user.name.get();
272
+ * rootStore.settings.theme.set('light');
154
273
  */
155
274
  export function combineStores(stores) {
156
275
  const combined = {};
@@ -163,7 +282,35 @@ export function combineStores(stores) {
163
282
  }
164
283
 
165
284
  /**
166
- * Create a module-based store (like Vuex modules)
285
+ * Create a module-based store similar to Vuex modules.
286
+ * Each module has its own state, actions, and getters.
287
+ * @param {Object.<string, ModuleDef>} modules - Module definitions
288
+ * @returns {Object} Root store with namespaced modules
289
+ * @example
290
+ * const store = createModuleStore({
291
+ * user: {
292
+ * state: { name: '', loggedIn: false },
293
+ * actions: {
294
+ * login: (store, name) => {
295
+ * store.name.set(name);
296
+ * store.loggedIn.set(true);
297
+ * }
298
+ * },
299
+ * getters: {
300
+ * displayName: (store) => store.loggedIn.get() ? store.name.get() : 'Guest'
301
+ * }
302
+ * },
303
+ * cart: {
304
+ * state: { items: [] },
305
+ * actions: {
306
+ * addItem: (store, item) => store.items.update(arr => [...arr, item])
307
+ * }
308
+ * }
309
+ * });
310
+ *
311
+ * store.user.login('John');
312
+ * store.cart.addItem({ id: 1, name: 'Product' });
313
+ * store.$getState(); // { user: {...}, cart: {...} }
167
314
  */
168
315
  export function createModuleStore(modules) {
169
316
  const stores = {};
@@ -186,7 +333,10 @@ export function createModuleStore(modules) {
186
333
  rootStore[name] = stores[name];
187
334
  }
188
335
 
189
- // Root methods
336
+ /**
337
+ * Get combined state from all modules
338
+ * @returns {Object} State from all modules
339
+ */
190
340
  rootStore.$getState = () => {
191
341
  const state = {};
192
342
  for (const [name, store] of Object.entries(stores)) {
@@ -195,6 +345,10 @@ export function createModuleStore(modules) {
195
345
  return state;
196
346
  };
197
347
 
348
+ /**
349
+ * Reset all modules to initial state
350
+ * @returns {void}
351
+ */
198
352
  rootStore.$reset = () => {
199
353
  for (const store of Object.values(stores)) {
200
354
  store.$reset();
@@ -205,35 +359,72 @@ export function createModuleStore(modules) {
205
359
  }
206
360
 
207
361
  /**
208
- * Plugin system for store
362
+ * Apply a plugin to a store.
363
+ * Plugins can extend store functionality.
364
+ * @param {Store} store - The store to enhance
365
+ * @param {StorePlugin} plugin - Plugin function
366
+ * @returns {Store} Enhanced store
367
+ * @example
368
+ * const store = createStore({ count: 0 });
369
+ * usePlugin(store, loggerPlugin);
370
+ * usePlugin(store, historyPlugin);
209
371
  */
210
372
  export function usePlugin(store, plugin) {
211
373
  return plugin(store);
212
374
  }
213
375
 
214
376
  /**
215
- * Logger plugin - logs all state changes
377
+ * Logger plugin - logs all state changes to console.
378
+ * Useful for debugging store updates.
379
+ * @param {Store} store - The store to add logging to
380
+ * @returns {Store} Store with logging enabled
381
+ * @example
382
+ * const store = createStore({ count: 0 });
383
+ * usePlugin(store, loggerPlugin);
384
+ *
385
+ * store.$setState({ count: 5 });
386
+ * // Console: [Store:Update] State Change
387
+ * // Previous: { count: 0 }
388
+ * // Updates: { count: 5 }
389
+ * // Next: { count: 5 }
216
390
  */
217
391
  export function loggerPlugin(store) {
218
392
  const originalSetState = store.$setState;
393
+ const pluginLog = createLogger('Store:Update');
219
394
 
220
395
  store.$setState = (updates) => {
221
- console.group('Store Update');
222
- console.log('Previous:', store.$getState());
223
- console.log('Updates:', updates);
396
+ pluginLog.group('State Change');
397
+ pluginLog.debug('Previous:', store.$getState());
398
+ pluginLog.debug('Updates:', updates);
224
399
  originalSetState(updates);
225
- console.log('Next:', store.$getState());
226
- console.groupEnd();
400
+ pluginLog.debug('Next:', store.$getState());
401
+ pluginLog.groupEnd();
227
402
  };
228
403
 
229
404
  return store;
230
405
  }
231
406
 
232
407
  /**
233
- * History plugin - enables undo/redo
408
+ * History plugin - enables undo/redo functionality.
409
+ * Tracks state changes and allows reverting to previous states.
410
+ * @param {Store} store - The store to add history to
411
+ * @param {number} [maxHistory=50] - Maximum number of history states to keep
412
+ * @returns {Store & {$undo: function, $redo: function, $canUndo: function, $canRedo: function}} Store with history methods
413
+ * @example
414
+ * const store = createStore({ count: 0 });
415
+ * usePlugin(store, (s) => historyPlugin(s, 100));
416
+ *
417
+ * store.$setState({ count: 1 });
418
+ * store.$setState({ count: 2 });
419
+ *
420
+ * store.$canUndo(); // true
421
+ * store.$undo(); // count = 1
422
+ * store.$redo(); // count = 2
234
423
  */
235
424
  export function historyPlugin(store, maxHistory = 50) {
425
+ /** @type {Array<Object>} */
236
426
  const history = [store.$getState()];
427
+ /** @type {number} */
237
428
  let currentIndex = 0;
238
429
 
239
430
  const originalSetState = store.$setState;
@@ -255,6 +446,10 @@ export function historyPlugin(store, maxHistory = 50) {
255
446
  }
256
447
  };
257
448
 
449
+ /**
450
+ * Undo the last state change
451
+ * @returns {void}
452
+ */
258
453
  store.$undo = () => {
259
454
  if (currentIndex > 0) {
260
455
  currentIndex--;
@@ -269,6 +464,10 @@ export function historyPlugin(store, maxHistory = 50) {
269
464
  }
270
465
  };
271
466
 
467
+ /**
468
+ * Redo a previously undone state change
469
+ * @returns {void}
470
+ */
272
471
  store.$redo = () => {
273
472
  if (currentIndex < history.length - 1) {
274
473
  currentIndex++;
@@ -283,7 +482,16 @@ export function historyPlugin(store, maxHistory = 50) {
283
482
  }
284
483
  };
285
484
 
485
+ /**
486
+ * Check if undo is available
487
+ * @returns {boolean} True if there are states to undo
488
+ */
286
489
  store.$canUndo = () => currentIndex > 0;
490
+
491
+ /**
492
+ * Check if redo is available
493
+ * @returns {boolean} True if there are states to redo
494
+ */
287
495
  store.$canRedo = () => currentIndex < history.length - 1;
288
496
 
289
497
  return store;
package/types/dom.d.ts ADDED
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Pulse Framework - DOM Helpers Type Definitions
3
+ * @module pulse-js-framework/runtime
4
+ */
5
+
6
+ import { Pulse } from './pulse';
7
+
8
+ /** Value or getter function */
9
+ export type MaybeGetter<T> = T | (() => T);
10
+
11
+ /** Value, getter, or Pulse */
12
+ export type Reactive<T> = T | (() => T) | Pulse<T>;
13
+
14
+ /** Child content type for elements */
15
+ export type Child =
16
+ | string
17
+ | number
18
+ | Node
19
+ | null
20
+ | undefined
21
+ | (() => Child | Child[])
22
+ | Child[];
23
+
24
+ /** Parsed selector result */
25
+ export interface ParsedSelector {
26
+ tag: string;
27
+ id: string | null;
28
+ classes: string[];
29
+ attrs: Record<string, string>;
30
+ }
31
+
32
+ /**
33
+ * Parse CSS selector into configuration
34
+ */
35
+ export declare function parseSelector(selector: string): ParsedSelector;
36
+
37
+ /**
38
+ * Create element from CSS selector syntax
39
+ * @example el('div.container#main', 'Hello')
40
+ * @example el('input[type=text][placeholder=Name]')
41
+ */
42
+ export declare function el(selector: string, ...children: Child[]): HTMLElement;
43
+
44
+ /**
45
+ * Create reactive text node
46
+ */
47
+ export declare function text(getValue: MaybeGetter<string | number>): Text;
48
+
49
+ /**
50
+ * Bind attribute reactively
51
+ * @returns Element (chainable)
52
+ */
53
+ export declare function bind<E extends HTMLElement>(
54
+ element: E,
55
+ attr: string,
56
+ getValue: MaybeGetter<string | boolean | number | null>
57
+ ): E;
58
+
59
+ /**
60
+ * Bind property reactively
61
+ * @returns Element (chainable)
62
+ */
63
+ export declare function prop<E extends HTMLElement>(
64
+ element: E,
65
+ propName: string,
66
+ getValue: MaybeGetter<unknown>
67
+ ): E;
68
+
69
+ /**
70
+ * Bind CSS class reactively
71
+ * @returns Element (chainable)
72
+ */
73
+ export declare function cls<E extends HTMLElement>(
74
+ element: E,
75
+ className: string,
76
+ condition: MaybeGetter<boolean>
77
+ ): E;
78
+
79
+ /**
80
+ * Bind style property reactively
81
+ * @returns Element (chainable)
82
+ */
83
+ export declare function style<E extends HTMLElement>(
84
+ element: E,
85
+ prop: string,
86
+ getValue: MaybeGetter<string>
87
+ ): E;
88
+
89
+ /** Event handler options */
90
+ export interface EventOptions {
91
+ capture?: boolean;
92
+ once?: boolean;
93
+ passive?: boolean;
94
+ }
95
+
96
+ /**
97
+ * Attach event listener
98
+ * @returns Element (chainable)
99
+ */
100
+ export declare function on<E extends HTMLElement, K extends keyof HTMLElementEventMap>(
101
+ element: E,
102
+ event: K,
103
+ handler: (e: HTMLElementEventMap[K]) => void,
104
+ options?: EventOptions
105
+ ): E;
106
+ export declare function on<E extends HTMLElement>(
107
+ element: E,
108
+ event: string,
109
+ handler: (e: Event) => void,
110
+ options?: EventOptions
111
+ ): E;
112
+
113
+ /** Key function for list rendering */
114
+ export type KeyFn<T> = (item: T, index: number) => unknown;
115
+
116
+ /** Template function for list items */
117
+ export type ListTemplate<T> = (item: T, index: number) => Node | Node[];
118
+
119
+ /**
120
+ * Render reactive list with efficient keyed diffing
121
+ */
122
+ export declare function list<T>(
123
+ getItems: Reactive<T[]>,
124
+ template: ListTemplate<T>,
125
+ keyFn?: KeyFn<T>
126
+ ): DocumentFragment;
127
+
128
+ /** Template function for conditional rendering */
129
+ export type ConditionTemplate = () => Node | Node[] | null;
130
+
131
+ /**
132
+ * Conditional rendering (removes unmounted elements)
133
+ */
134
+ export declare function when(
135
+ condition: Reactive<boolean>,
136
+ thenTemplate: ConditionTemplate,
137
+ elseTemplate?: ConditionTemplate
138
+ ): DocumentFragment;
139
+
140
+ /** Match cases object */
141
+ export interface MatchCases<T extends string | number | symbol = string> {
142
+ [key: string]: ConditionTemplate;
143
+ default?: ConditionTemplate;
144
+ }
145
+
146
+ /**
147
+ * Switch/case rendering
148
+ */
149
+ export declare function match<T extends string | number>(
150
+ getValue: Reactive<T>,
151
+ cases: MatchCases
152
+ ): Comment;
153
+
154
+ /**
155
+ * Two-way binding for form inputs
156
+ * @returns Element (chainable)
157
+ */
158
+ export declare function model<E extends HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>(
159
+ element: E,
160
+ pulseValue: Pulse<string | number | boolean>
161
+ ): E;
162
+
163
+ /**
164
+ * Mount element to DOM target
165
+ * @returns Unmount function
166
+ */
167
+ export declare function mount(
168
+ target: string | HTMLElement,
169
+ element: Node
170
+ ): () => void;
171
+
172
+ /** Component context object */
173
+ export interface ComponentContext {
174
+ /** Create reactive state */
175
+ state: <T extends Record<string, unknown>>(initial: T) => T;
176
+ /** Create methods bound to component */
177
+ methods: <T extends Record<string, (...args: unknown[]) => unknown>>(fns: T) => T;
178
+ /** Component props */
179
+ props: Record<string, unknown>;
180
+ /** Create pulse */
181
+ pulse: <T>(value: T) => Pulse<T>;
182
+ /** Create element */
183
+ el: typeof el;
184
+ /** Create text node */
185
+ text: typeof text;
186
+ /** Render list */
187
+ list: typeof list;
188
+ /** Conditional render */
189
+ when: typeof when;
190
+ /** Attach event */
191
+ on: typeof on;
192
+ /** Bind attribute */
193
+ bind: typeof bind;
194
+ /** Two-way binding */
195
+ model: typeof model;
196
+ /** Register mount callback */
197
+ onMount: (fn: () => void) => void;
198
+ /** Register unmount callback */
199
+ onUnmount: (fn: () => void) => void;
200
+ }
201
+
202
+ /** Component setup function */
203
+ export type ComponentSetup = (ctx: ComponentContext) => Node;
204
+
205
+ /** Component factory function */
206
+ export type ComponentFactory = (props?: Record<string, unknown>) => Node;
207
+
208
+ /**
209
+ * Create component with lifecycle support
210
+ */
211
+ export declare function component(setup: ComponentSetup): ComponentFactory;
212
+
213
+ /**
214
+ * Register mount callback
215
+ */
216
+ export declare function onMount(fn: () => void): void;
217
+
218
+ /**
219
+ * Register unmount callback
220
+ */
221
+ export declare function onUnmount(fn: () => void): void;
222
+
223
+ /**
224
+ * Toggle visibility without removing from DOM
225
+ * @returns Element (chainable)
226
+ */
227
+ export declare function show<E extends HTMLElement>(
228
+ condition: Reactive<boolean>,
229
+ element: E
230
+ ): E;
231
+
232
+ /**
233
+ * Render content into different DOM location
234
+ */
235
+ export declare function portal(
236
+ children: Node | Node[] | (() => Node | Node[]),
237
+ target: string | HTMLElement
238
+ ): Comment;
239
+
240
+ /** Error fallback function */
241
+ export type ErrorFallback = (error: Error) => Node | Node[];
242
+
243
+ /**
244
+ * Error boundary for child components
245
+ */
246
+ export declare function errorBoundary(
247
+ children: Node | Node[] | (() => Node | Node[]),
248
+ fallback?: ErrorFallback
249
+ ): DocumentFragment;
250
+
251
+ /** Transition options */
252
+ export interface TransitionOptions {
253
+ enter?: string;
254
+ exit?: string;
255
+ duration?: number;
256
+ onEnter?: (el: HTMLElement) => void;
257
+ onExit?: (el: HTMLElement) => void;
258
+ }
259
+
260
+ /** Element with transition methods */
261
+ export interface TransitionElement extends HTMLElement {
262
+ _pulseTransitionExit?: () => void;
263
+ }
264
+
265
+ /**
266
+ * Animate element enter/exit
267
+ */
268
+ export declare function transition<E extends HTMLElement>(
269
+ element: E,
270
+ options?: TransitionOptions
271
+ ): E & TransitionElement;
272
+
273
+ /** WhenTransition options */
274
+ export interface WhenTransitionOptions {
275
+ duration?: number;
276
+ enterClass?: string;
277
+ exitClass?: string;
278
+ }
279
+
280
+ /**
281
+ * Conditional rendering with transitions
282
+ */
283
+ export declare function whenTransition(
284
+ condition: Reactive<boolean>,
285
+ thenTemplate: ConditionTemplate,
286
+ elseTemplate?: ConditionTemplate,
287
+ options?: WhenTransitionOptions
288
+ ): DocumentFragment;