@uistate/core 5.3.0 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,17 +1,10 @@
1
1
  /**
2
2
  * UIstate v5 - Core barrel exports
3
3
  *
4
- * EventState is now the primary export for application state management.
5
- * cssState remains available for CSS variable/theme management use cases.
4
+ * EventState is the primary export for application state management.
5
+ * CSS state management has moved to @uistate/css.
6
6
  */
7
7
 
8
8
  // Primary: EventState (recommended for application state)
9
9
  export { createEventState } from './eventStateNew.js';
10
10
  export { createEventState as default } from './eventStateNew.js';
11
-
12
- // Specialized: CSS State (for CSS variables and theme management)
13
- export { createCssState } from './cssState.js';
14
-
15
- // Utilities
16
- export { default as stateSerializer } from './stateSerializer.js';
17
- export { createTemplateManager } from './templateManager.js';
package/package.json CHANGED
@@ -1,29 +1,21 @@
1
1
  {
2
2
  "name": "@uistate/core",
3
- "version": "5.3.0",
4
- "description": "Lightweight event-driven state management with slot orchestration and experimental event-sequence testing (eventTest.js available under dual license)",
3
+ "version": "5.5.0",
4
+ "description": "Lightweight event-driven state management with path-based subscriptions, wildcards, and async support",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {
8
8
  ".": "./index.js",
9
9
  "./eventState": "./eventState.js",
10
10
  "./eventStateNew": "./eventStateNew.js",
11
- "./cssState": "./cssState.js",
12
- "./stateSerializer": "./stateSerializer.js",
13
- "./templateManager": "./templateManager.js",
14
11
  "./query": "./queryClient.js"
15
12
  },
16
13
  "files": [
17
14
  "index.js",
18
15
  "eventState.js",
19
16
  "eventStateNew.js",
20
- "cssState.js",
21
- "stateSerializer.js",
22
- "templateManager.js",
23
17
  "queryClient.js",
24
- "eventTest.js",
25
- "LICENSE",
26
- "LICENSE-eventTest.md"
18
+ "LICENSE"
27
19
  ],
28
20
  "keywords": [
29
21
  "state-management",
@@ -34,8 +26,8 @@
34
26
  "zero-dependency",
35
27
  "framework-free",
36
28
  "micro-framework",
37
- "testing",
38
- "event-testing"
29
+ "async-state",
30
+ "query-client"
39
31
  ],
40
32
  "author": "Ajdin Imsirovic",
41
33
  "license": "MIT",
@@ -1,26 +0,0 @@
1
- # Proprietary License for eventTest.js
2
-
3
- Copyright (c) 2025 Ajdin Imsirovic
4
-
5
- ## Permitted Uses
6
-
7
- You may use `eventTest.js` for:
8
- - Personal projects
9
- - Open-source projects
10
- - Educational purposes
11
-
12
- ## Restrictions
13
-
14
- You may NOT:
15
- - Use this file in commercial products without a license
16
- - Modify or create derivative works
17
- - Redistribute this file separately from @uistate/core
18
- - Remove or alter this license notice
19
-
20
- ## Commercial Licensing
21
-
22
- For commercial use, please contact: your@email.com
23
-
24
- ---
25
-
26
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED.
package/cssState.js DELETED
@@ -1,212 +0,0 @@
1
- /**
2
- * UIstate - CSS-based state management module with integrated serialization
3
- * Part of the UIstate declarative state management system
4
- * Uses CSS custom properties and data attributes for state representation
5
- * Features modular extension capabilities for DOM binding and events
6
- */
7
- import StateSerializer from './stateSerializer.js';
8
-
9
- const createCssState = (initialState = {}, serializer = StateSerializer) => {
10
- const state = {
11
- _sheet: null,
12
- _observers: new Map(),
13
- _serializer: serializer,
14
- _specialHandlers: {},
15
- _eventHandlers: new Map(), // Store custom event binding handlers
16
-
17
- init(serializerConfig) {
18
- if (!this._sheet) {
19
- const style = document.createElement('style');
20
- document.head.appendChild(style);
21
- this._sheet = style.sheet;
22
- this._addRule(':root {}');
23
- }
24
-
25
- // Configure serializer if options provided
26
- if (serializerConfig && typeof serializerConfig === 'object') {
27
- this._serializer.configure(serializerConfig);
28
- }
29
-
30
- // Initialize with any provided state
31
- if (initialState && typeof initialState === 'object') {
32
- Object.entries(initialState).forEach(([key, value]) => {
33
- this.setState(key, value);
34
- });
35
- }
36
-
37
- return this;
38
- },
39
-
40
- setState(key, value) {
41
- // Use serializer for CSS variables
42
- const cssValue = this._serializer.serialize(key, value);
43
- document.documentElement.style.setProperty(`--${key}`, cssValue);
44
-
45
- // Use serializer to handle all attribute application consistently
46
- this._serializer.applyToAttributes(key, value);
47
-
48
- // Notify any registered observers of the state change
49
- this._notifyObservers(key, value);
50
- return value;
51
- },
52
-
53
- setStates(stateObject) {
54
- Object.entries(stateObject).forEach(([key, value]) => {
55
- this.setState(key, value);
56
- });
57
- return this;
58
- },
59
-
60
- getState(key) {
61
- const value = getComputedStyle(document.documentElement).getPropertyValue(`--${key}`).trim();
62
- if (!value) return '';
63
-
64
- // Use serializer for deserialization
65
- return this._serializer.deserialize(key, value);
66
- },
67
-
68
- observe(key, callback) {
69
- if (!this._observers.has(key)) {
70
- this._observers.set(key, new Set());
71
- }
72
- this._observers.get(key).add(callback);
73
- return () => {
74
- const observers = this._observers.get(key);
75
- if (observers) {
76
- observers.delete(callback);
77
- }
78
- };
79
- },
80
-
81
- _notifyObservers(key, value) {
82
- const observers = this._observers.get(key);
83
- if (observers) {
84
- observers.forEach(cb => cb(value));
85
- }
86
- },
87
-
88
- registerSpecialHandler(stateKey, handlerFn) {
89
- this._specialHandlers[stateKey] = handlerFn;
90
- return this;
91
- },
92
-
93
- // New method for registering event bindings
94
- registerEventBinding(eventType, handler) {
95
- this._eventHandlers.set(eventType, handler);
96
- return this;
97
- },
98
-
99
- setupObservers(container = document) {
100
- container.querySelectorAll('[data-observe]:not([data-observing])').forEach(el => {
101
- const stateKey = el.dataset.observe;
102
-
103
- this.observe(stateKey, (value) => {
104
- // Special handlers should run first to set data-state
105
- if (this._specialHandlers[stateKey]?.observe) {
106
- this._specialHandlers[stateKey].observe(value, el);
107
- } else if (stateKey.endsWith('-state') && el.hasAttribute('data-state')) {
108
- // Only update data-state for elements that already have this attribute
109
- el.dataset.state = value;
110
- } else {
111
- // For normal state observers like theme, counter, etc.
112
- el.textContent = value;
113
- }
114
- });
115
-
116
- // Trigger initial state
117
- const initialValue = this.getState(stateKey);
118
- if (this._specialHandlers[stateKey]?.observe) {
119
- this._specialHandlers[stateKey].observe(initialValue, el);
120
- } else if (stateKey.endsWith('-state') && el.hasAttribute('data-state')) {
121
- // Only set data-state for elements that should have this attribute
122
- el.dataset.state = initialValue;
123
- } else {
124
- // For normal elements
125
- el.textContent = initialValue;
126
- }
127
-
128
- el.dataset.observing = 'true';
129
- });
130
-
131
- return this;
132
- },
133
-
134
- // Default event handlers available for implementations to use
135
- defaultClickHandler(e) {
136
- const target = e.target.closest('[data-state-action]');
137
- if (!target) return;
138
-
139
- const stateAction = target.dataset.stateAction;
140
- if (!stateAction) return;
141
-
142
- // Special handlers get first priority
143
- if (this._specialHandlers[stateAction]?.action) {
144
- this._specialHandlers[stateAction].action(target);
145
- return;
146
- }
147
-
148
- // Handle direct value setting via data-state-value
149
- if (target.dataset.stateValue !== undefined) {
150
- const valueToSet = target.dataset.stateValue;
151
- this.setState(stateAction, valueToSet);
152
- }
153
- },
154
-
155
- defaultInputHandler(e) {
156
- const target = e.target;
157
- const stateAction = target.dataset.stateAction;
158
-
159
- if (!stateAction) return;
160
-
161
- // Special handlers should access any needed data directly from the target
162
- if (this._specialHandlers[stateAction]?.action) {
163
- this._specialHandlers[stateAction].action(target);
164
- }
165
- },
166
-
167
- // Updated setupStateActions to use registered event handlers
168
- setupStateActions(container = document) {
169
- // Only bind the registered event types
170
- this._eventHandlers.forEach((handler, eventType) => {
171
- container.addEventListener(eventType, handler);
172
- });
173
-
174
- // If no event handlers registered, register the default ones
175
- if (this._eventHandlers.size === 0) {
176
- container.addEventListener('click', (e) => this.defaultClickHandler(e));
177
- container.addEventListener('input', (e) => this.defaultInputHandler(e));
178
- }
179
-
180
- return this;
181
- },
182
-
183
- _addRule(rule) {
184
- if (this._sheet) {
185
- this._sheet.insertRule(rule, this._sheet.cssRules.length);
186
- }
187
- },
188
-
189
- // Add serializer configuration method
190
- configureSerializer(config) {
191
- if (this._serializer.configure) {
192
- this._serializer.configure(config);
193
- }
194
- return this;
195
- },
196
-
197
- // Clean up resources
198
- destroy() {
199
- this._observers.clear();
200
- // The style element will remain in the DOM
201
- // as removing it would affect the UI state
202
- }
203
- };
204
-
205
- return state.init();
206
- };
207
-
208
- // Create a singleton instance for easy usage
209
- const UIstate = createCssState();
210
-
211
- export { createCssState };
212
- export default UIstate;
package/eventTest.js DELETED
@@ -1,196 +0,0 @@
1
- /**
2
- * eventTest.js - Event-Sequence Testing for UIstate
3
- *
4
- * Copyright (c) 2025 Ajdin Imsirovic
5
- *
6
- * This file is licensed under a PROPRIETARY LICENSE.
7
- *
8
- * Permission is hereby granted to USE this software for:
9
- * - Personal projects
10
- * - Open-source projects
11
- * - Educational purposes
12
- *
13
- * RESTRICTIONS:
14
- * - Commercial use requires a separate license (contact: your@email.com)
15
- * - Modification and redistribution of this file are NOT permitted
16
- * - This file may not be included in derivative works without permission
17
- *
18
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
19
- *
20
- * For commercial licensing inquiries: your@email.com
21
- *
22
- * eventTest.js - Event-Sequence Testing for EventState
23
- *
24
- * Provides TDD-style testing with type extraction capabilities
25
- */
26
-
27
- import { createEventState } from './eventStateNew.js';
28
-
29
- export function createEventTest(initialState = {}) {
30
- const store = createEventState(initialState);
31
- const eventLog = [];
32
- const typeAssertions = [];
33
-
34
- // Spy on all events
35
- store.subscribe('*', (detail) => {
36
- const { path, value } = detail;
37
- eventLog.push({ timestamp: Date.now(), path, value });
38
- });
39
-
40
- const api = {
41
- store,
42
-
43
- // Trigger a state change
44
- trigger(path, value) {
45
- store.set(path, value);
46
- return this;
47
- },
48
-
49
- // Assert exact value
50
- assertPath(path, expected) {
51
- const actual = store.get(path);
52
- if (JSON.stringify(actual) !== JSON.stringify(expected)) {
53
- throw new Error(`Expected ${path} to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
54
- }
55
- return this;
56
- },
57
-
58
- // Assert type (for type generation)
59
- assertType(path, expectedType) {
60
- const actual = store.get(path);
61
- const actualType = typeof actual;
62
-
63
- if (actualType !== expectedType) {
64
- throw new Error(`Expected ${path} to be type ${expectedType}, got ${actualType}`);
65
- }
66
-
67
- // Store for type generation
68
- typeAssertions.push({ path, type: expectedType });
69
- return this;
70
- },
71
-
72
- // Assert array with element shape (for type generation)
73
- assertArrayOf(path, elementShape) {
74
- const actual = store.get(path);
75
-
76
- if (!Array.isArray(actual)) {
77
- throw new Error(`Expected ${path} to be an array, got ${typeof actual}`);
78
- }
79
-
80
- // Validate first element matches shape (if array not empty)
81
- if (actual.length > 0) {
82
- validateShape(actual[0], elementShape, path);
83
- }
84
-
85
- // Store for type generation
86
- typeAssertions.push({ path, type: 'array', elementShape });
87
- return this;
88
- },
89
-
90
- // Assert object shape (for type generation)
91
- assertShape(path, objectShape) {
92
- const actual = store.get(path);
93
-
94
- if (typeof actual !== 'object' || actual === null || Array.isArray(actual)) {
95
- throw new Error(`Expected ${path} to be an object, got ${typeof actual}`);
96
- }
97
-
98
- validateShape(actual, objectShape, path);
99
-
100
- // Store for type generation
101
- typeAssertions.push({ path, type: 'object', shape: objectShape });
102
- return this;
103
- },
104
-
105
- // Assert array length
106
- assertArrayLength(path, expectedLength) {
107
- const actual = store.get(path);
108
-
109
- if (!Array.isArray(actual)) {
110
- throw new Error(`Expected ${path} to be an array`);
111
- }
112
-
113
- if (actual.length !== expectedLength) {
114
- throw new Error(`Expected ${path} to have length ${expectedLength}, got ${actual.length}`);
115
- }
116
-
117
- return this;
118
- },
119
-
120
- // Assert event fired N times
121
- assertEventFired(path, times) {
122
- const count = eventLog.filter(e => e.path === path).length;
123
- if (times !== undefined && count !== times) {
124
- throw new Error(`Expected ${path} to fire ${times} times, fired ${count}`);
125
- }
126
- return this;
127
- },
128
-
129
- // Get event log
130
- getEventLog() {
131
- return [...eventLog];
132
- },
133
-
134
- // Get type assertions (for type generation)
135
- getTypeAssertions() {
136
- return [...typeAssertions];
137
- }
138
- };
139
-
140
- return api;
141
- }
142
-
143
- // Helper to validate object shape
144
- function validateShape(actual, shape, path) {
145
- for (const [key, expectedType] of Object.entries(shape)) {
146
- if (!(key in actual)) {
147
- throw new Error(`Expected ${path} to have property ${key}`);
148
- }
149
-
150
- const actualValue = actual[key];
151
-
152
- // Handle nested objects
153
- if (typeof expectedType === 'object' && !Array.isArray(expectedType)) {
154
- validateShape(actualValue, expectedType, `${path}.${key}`);
155
- } else {
156
- // Primitive type check
157
- const actualType = typeof actualValue;
158
- if (actualType !== expectedType) {
159
- throw new Error(`Expected ${path}.${key} to be type ${expectedType}, got ${actualType}`);
160
- }
161
- }
162
- }
163
- }
164
-
165
- // Simple test runner
166
- export function test(name, fn) {
167
- try {
168
- fn();
169
- console.log(`✓ ${name}`);
170
- return true;
171
- } catch (error) {
172
- console.error(`✗ ${name}`);
173
- console.error(` ${error.message}`);
174
- return false;
175
- }
176
- }
177
-
178
- // Run multiple tests
179
- export function runTests(tests) {
180
- console.log('\n🧪 Running tests...\n');
181
-
182
- let passed = 0;
183
- let failed = 0;
184
-
185
- for (const [name, fn] of Object.entries(tests)) {
186
- if (test(name, fn)) {
187
- passed++;
188
- } else {
189
- failed++;
190
- }
191
- }
192
-
193
- console.log(`\n📊 Results: ${passed} passed, ${failed} failed\n`);
194
-
195
- return { passed, failed };
196
- }
@@ -1,267 +0,0 @@
1
- /**
2
- * StateSerializer - Configurable serialization module for UIstate
3
- * Handles transformation between JavaScript values and CSS-compatible string values
4
- *
5
- * Supports multiple serialization strategies:
6
- * - 'escape': Uses custom escaping for all values (original UIstate approach)
7
- * - 'json': Uses JSON.stringify for complex objects, direct values for primitives
8
- * - 'hybrid': Automatically selects the best strategy based on value type
9
- *
10
- * Also handles serialization of values for data attributes and CSS variables
11
- * with consistent rules and unified serialization behavior
12
- */
13
-
14
- // Utility functions for CSS value escaping/unescaping
15
- function escapeCssValue(value) {
16
- if (typeof value !== 'string') return value;
17
- return value.replace(/[^\x20-\x7E]|[!;{}:()[\]/@,'"]/g, function(char) {
18
- const hex = char.charCodeAt(0).toString(16);
19
- return '\\' + hex + ' ';
20
- });
21
- }
22
-
23
- function unescapeCssValue(value) {
24
- if (typeof value !== 'string') return value;
25
- // Only perform unescaping if there are escape sequences
26
- if (!value.includes('\\')) return value;
27
-
28
- return value.replace(/\\([0-9a-f]{1,6})\s?/gi, function(match, hex) {
29
- return String.fromCharCode(parseInt(hex, 16));
30
- });
31
- }
32
-
33
- /**
34
- * Create a configured serializer instance
35
- * @param {Object} config - Configuration options
36
- * @returns {Object} - Serializer instance
37
- */
38
- function createSerializer(config = {}) {
39
- // Default configuration
40
- const defaultConfig = {
41
- mode: 'hybrid', // 'escape', 'json', or 'hybrid'
42
- debug: false, // Enable debug logging
43
- complexThreshold: 3, // Object properties threshold for hybrid mode
44
- preserveTypes: true // Preserve type information in serialization
45
- };
46
-
47
- // Merge provided config with defaults
48
- const options = { ...defaultConfig, ...config };
49
-
50
- // Serializer instance
51
- const serializer = {
52
- // Current configuration
53
- config: options,
54
-
55
- /**
56
- * Update configuration
57
- * @param {Object} newConfig - New configuration options
58
- */
59
- configure(newConfig) {
60
- Object.assign(this.config, newConfig);
61
- if (this.config.debug) {
62
- console.log('StateSerializer config updated:', this.config);
63
- }
64
- },
65
-
66
- /**
67
- * Serialize a value for storage in CSS variables
68
- * @param {string} key - The state key (for context-aware serialization)
69
- * @param {any} value - The value to serialize
70
- * @returns {string} - Serialized value
71
- */
72
- serialize(key, value) {
73
- // Handle null/undefined
74
- if (value === null || value === undefined) {
75
- return '';
76
- }
77
-
78
- const valueType = typeof value;
79
- const isComplex = valueType === 'object' &&
80
- (Array.isArray(value) ||
81
- (Object.keys(value).length >= this.config.complexThreshold));
82
-
83
- // Select serialization strategy based on configuration and value type
84
- if (this.config.mode === 'escape' ||
85
- (this.config.mode === 'hybrid' && !isComplex)) {
86
- // Use escape strategy for primitives or when escape mode is forced
87
- if (valueType === 'string') {
88
- return escapeCssValue(value);
89
- } else if (valueType === 'object') {
90
- // For simple objects in escape mode, still use JSON but with escaping
91
- const jsonStr = JSON.stringify(value);
92
- return escapeCssValue(jsonStr);
93
- } else {
94
- // For other primitives, convert to string
95
- return String(value);
96
- }
97
- } else {
98
- // Use JSON strategy for complex objects or when JSON mode is forced
99
- return JSON.stringify(value);
100
- }
101
- },
102
-
103
- /**
104
- * Deserialize a value from CSS variable storage
105
- * @param {string} key - The state key (for context-aware deserialization)
106
- * @param {string} value - The serialized value
107
- * @returns {any} - Deserialized value
108
- */
109
- deserialize(key, value) {
110
- // Handle empty values
111
- if (!value) return '';
112
-
113
- // Try JSON parse first for values that look like JSON
114
- if (this.config.mode !== 'escape' &&
115
- ((value.startsWith('{') && value.endsWith('}')) ||
116
- (value.startsWith('[') && value.endsWith(']')))) {
117
- try {
118
- return JSON.parse(value);
119
- } catch (e) {
120
- if (this.config.debug) {
121
- console.warn(`Failed to parse JSON for key "${key}":`, value);
122
- }
123
- // Fall through to unescaping if JSON parse fails
124
- }
125
- }
126
-
127
- // For non-JSON or escape mode, try unescaping
128
- const unescaped = unescapeCssValue(value);
129
-
130
- // If unescaped looks like JSON (might have been double-escaped), try parsing it
131
- if (this.config.mode !== 'escape' &&
132
- ((unescaped.startsWith('{') && unescaped.endsWith('}')) ||
133
- (unescaped.startsWith('[') && unescaped.endsWith(']')))) {
134
- try {
135
- return JSON.parse(unescaped);
136
- } catch (e) {
137
- // Not valid JSON, return unescaped string
138
- }
139
- }
140
-
141
- return unescaped;
142
- },
143
-
144
- /**
145
- * Serialize a value for data-* attribute
146
- * @param {string} key - The state key
147
- * @param {any} value - The value to serialize for attribute
148
- * @returns {string} - Serialized attribute value
149
- */
150
- serializeForAttribute(key, value) {
151
- if (value === null || value === undefined) return null;
152
-
153
- // For objects, use the standard serializer
154
- if (typeof value === 'object') {
155
- return this.serialize(key, value);
156
- }
157
-
158
- // For primitive values, use direct string conversion
159
- return String(value);
160
- },
161
-
162
- /**
163
- * Apply serialized state to HTML element attributes and properties
164
- * @param {string} key - State key
165
- * @param {any} value - Value to apply
166
- * @param {HTMLElement} element - Target element (defaults to documentElement)
167
- */
168
- applyToAttributes(key, value, element = document.documentElement) {
169
- // Skip null/undefined values
170
- if (value === null || value === undefined) {
171
- element.removeAttribute(`data-${key}`);
172
- return;
173
- }
174
-
175
- // Handle objects specially
176
- if (typeof value === 'object') {
177
- // Set the main attribute with serialized value
178
- element.setAttribute(`data-${key}`, this.serialize(key, value));
179
-
180
- // For non-array objects, set individual property attributes
181
- if (!Array.isArray(value)) {
182
- Object.entries(value).forEach(([propKey, propValue]) => {
183
- const attributeKey = `data-${key}-${propKey.toLowerCase()}`;
184
- if (propValue !== null && propValue !== undefined) {
185
- if (typeof propValue === 'object') {
186
- element.setAttribute(
187
- attributeKey,
188
- this.serialize(`${key}.${propKey}`, propValue)
189
- );
190
- } else {
191
- element.setAttribute(attributeKey, propValue);
192
- }
193
- } else {
194
- element.removeAttribute(attributeKey);
195
- }
196
- });
197
- }
198
- } else {
199
- // For primitives, set directly
200
- element.setAttribute(`data-${key}`, value);
201
- }
202
- },
203
-
204
- /**
205
- * Utility method to determine if a value needs complex serialization
206
- * @param {any} value - Value to check
207
- * @returns {boolean} - True if complex serialization is needed
208
- */
209
- needsComplexSerialization(value) {
210
- return typeof value === 'object' && value !== null;
211
- },
212
-
213
- /**
214
- * Set state with proper serialization for CSS variables
215
- * @param {Object} uistate - UIstate instance
216
- * @param {string} path - State path
217
- * @param {any} value - Value to set
218
- * @returns {any} - The set value
219
- */
220
- setStateWithCss(uistate, path, value) {
221
- // Update UIstate
222
- uistate.setState(path, value);
223
-
224
- // Update CSS variable with properly serialized value
225
- const cssPath = path.replace(/\./g, '-');
226
- const serialized = this.serialize(path, value);
227
- document.documentElement.style.setProperty(`--${cssPath}`, serialized);
228
-
229
- // Update data attribute for root level state
230
- const segments = path.split('.');
231
- if (segments.length === 1) {
232
- document.documentElement.dataset[path] = typeof value === 'object'
233
- ? JSON.stringify(value)
234
- : value;
235
- }
236
-
237
- return value;
238
- },
239
-
240
- /**
241
- * Get state with fallback to CSS variables
242
- * @param {Object} uistate - UIstate instance
243
- * @param {string} path - State path
244
- * @returns {any} - Retrieved value
245
- */
246
- getStateFromCss(uistate, path) {
247
- // First try UIstate
248
- const value = uistate.getState(path);
249
- if (value !== undefined) return value;
250
-
251
- // If not found, try CSS variable
252
- const cssPath = path.replace(/\./g, '-');
253
- const cssValue = getComputedStyle(document.documentElement)
254
- .getPropertyValue(`--${cssPath}`).trim();
255
-
256
- return cssValue ? this.deserialize(path, cssValue) : undefined;
257
- }
258
- };
259
-
260
- return serializer;
261
- }
262
-
263
- // Create a default instance with hybrid mode
264
- const StateSerializer = createSerializer();
265
-
266
- export default StateSerializer;
267
- export { createSerializer, escapeCssValue, unescapeCssValue };
@@ -1,216 +0,0 @@
1
- /**
2
- * TemplateManager - Component mounting and event delegation
3
- * Handles HTML templating, component mounting, and event delegation
4
- */
5
-
6
- const createTemplateManager = (stateManager) => {
7
- const manager = {
8
- handlers: {},
9
-
10
- onAction(action, handler) {
11
- this.handlers[action] = handler;
12
- return this;
13
- },
14
-
15
- /**
16
- * Register multiple actions with their handlers in a declarative way
17
- * @param {Object} actionsMap - Map of action names to handlers or handler configs
18
- * @returns {Object} - The manager instance for chaining
19
- *
20
- * Example usage:
21
- * templateManager.registerActions({
22
- * 'add-item': addItem,
23
- * 'delete-item': { fn: deleteItem, extractId: true },
24
- * 'toggle-state': toggleState
25
- * });
26
- */
27
- registerActions(actionsMap) {
28
- Object.entries(actionsMap).forEach(([action, handler]) => {
29
- if (typeof handler === 'function') {
30
- // Simple function handler
31
- this.onAction(action, handler);
32
- } else if (typeof handler === 'object' && handler !== null) {
33
- // Handler with configuration
34
- const { fn, extractId = true, idAttribute = 'id' } = handler;
35
-
36
- if (typeof fn !== 'function') {
37
- throw new Error(`Handler for action '${action}' must be a function`);
38
- }
39
-
40
- this.onAction(action, (e) => {
41
- if (extractId) {
42
- const target = e.target.closest('[data-action]');
43
- // Look for common ID attributes in order of preference
44
- const id = target.dataset[idAttribute] ||
45
- target.dataset.actionId ||
46
- target.dataset.cardId ||
47
- target.dataset.itemId;
48
-
49
- fn(id, e, target);
50
- } else {
51
- fn(e);
52
- }
53
- });
54
- } else {
55
- throw new Error(`Invalid handler for action '${action}'`);
56
- }
57
- });
58
- return this;
59
- },
60
-
61
- attachDelegation(root = document.body) {
62
- root.addEventListener('click', e => {
63
- const target = e.target.closest('[data-action]');
64
- if (!target) return;
65
-
66
- const action = target.dataset.action;
67
- if (!action) return;
68
-
69
- const handler = this.handlers[action];
70
- if (typeof handler === 'function') {
71
- handler(e);
72
- } else if (target.dataset.value !== undefined && stateManager) {
73
- // If we have a state manager, use it to update state
74
- stateManager.setState(action, target.dataset.value);
75
- }
76
- });
77
- return this;
78
- },
79
-
80
- /**
81
- * Render a template from a CSS variable
82
- * @param {string} templateName - Name of the template (will be prefixed with --template-)
83
- * @param {Object} data - Data to inject into the template
84
- * @returns {HTMLElement} - The rendered element
85
- */
86
- renderTemplateFromCss(templateName, data = {}) {
87
- const cssTemplate = getComputedStyle(document.documentElement)
88
- .getPropertyValue(`--template-${templateName}`)
89
- .trim()
90
- .replace(/^['"]|['"]$/g, ''); // Remove surrounding quotes
91
-
92
- if (!cssTemplate) throw new Error(`Template not found in CSS: --template-${templateName}`);
93
-
94
- let html = cssTemplate;
95
-
96
- // Replace all placeholders with actual data
97
- Object.entries(data).forEach(([key, value]) => {
98
- const regex = new RegExp(`{{${key}}}`, 'g');
99
- html = html.replace(regex, value);
100
- });
101
-
102
- // Create a temporary container
103
- const temp = document.createElement('div');
104
- temp.innerHTML = html;
105
-
106
- // Return the first child (the rendered template)
107
- return temp.firstElementChild;
108
- },
109
-
110
- mount(componentName, container) {
111
- const tpl = document.getElementById(`${componentName}-template`);
112
- if (!tpl) throw new Error(`Template not found: ${componentName}-template`);
113
- const clone = tpl.content.cloneNode(true);
114
-
115
- function resolvePlaceholders(fragment) {
116
- Array.from(fragment.querySelectorAll('*')).forEach(el => {
117
- const tag = el.tagName.toLowerCase();
118
- if (tag.endsWith('-placeholder')) {
119
- const name = tag.replace('-placeholder','');
120
- const childTpl = document.getElementById(`${name}-template`);
121
- if (!childTpl) throw new Error(`Template not found: ${name}-template`);
122
- const childClone = childTpl.content.cloneNode(true);
123
- resolvePlaceholders(childClone);
124
- el.replaceWith(childClone);
125
- }
126
- });
127
- }
128
-
129
- resolvePlaceholders(clone);
130
- container.appendChild(clone);
131
- return clone.firstElementChild;
132
- },
133
-
134
- // Helper to create a reactive component with automatic updates
135
- createComponent(name, renderFn, stateKeys = []) {
136
- if (!stateManager) {
137
- throw new Error('State manager is required for reactive components');
138
- }
139
-
140
- // Create template element if it doesn't exist
141
- let tpl = document.getElementById(`${name}-template`);
142
- if (!tpl) {
143
- tpl = document.createElement('template');
144
- tpl.id = `${name}-template`;
145
- document.body.appendChild(tpl);
146
- }
147
-
148
- // Initial render
149
- tpl.innerHTML = renderFn(stateManager);
150
-
151
- // Set up observers for reactive updates
152
- if (stateKeys.length > 0) {
153
- stateKeys.forEach(key => {
154
- stateManager.observe(key, () => {
155
- tpl.innerHTML = renderFn(stateManager);
156
- });
157
- });
158
- }
159
-
160
- return {
161
- mount: (container) => this.mount(name, container)
162
- };
163
- },
164
-
165
- /**
166
- * Apply CSS classes to an element based on a state key stored in CSS variables
167
- * @param {HTMLElement} element - Element to apply classes to
168
- * @param {string} stateKey - State key to look up in CSS variables
169
- * @param {Object} options - Options for class application
170
- * @returns {HTMLElement} - The element for chaining
171
- *
172
- * Example usage:
173
- * // CSS: :root { --card-primary-classes: "bg-primary text-white"; }
174
- * templateManager.applyClassesFromState(cardElement, 'card-primary');
175
- */
176
- applyClassesFromState(element, stateKey, options = {}) {
177
- if (!element) return element;
178
-
179
- const {
180
- prefix = '',
181
- clearExisting = false,
182
- namespace = ''
183
- } = typeof options === 'string' ? { prefix: options } : options;
184
-
185
- const prefixPath = prefix ? `${prefix}-` : '';
186
- const namespacePath = namespace ? `${namespace}-` : '';
187
-
188
- const classString = getComputedStyle(document.documentElement)
189
- .getPropertyValue(`--${namespacePath}${stateKey}-classes`)
190
- .trim()
191
- .replace(/^['"]|['"]$/g, '');
192
-
193
- if (classString) {
194
- // Clear existing classes if specified
195
- if (clearExisting) {
196
- element.className = '';
197
- }
198
-
199
- // Add new classes
200
- classString.split(' ').forEach(cls => {
201
- if (cls) element.classList.add(cls);
202
- });
203
- }
204
-
205
- return element; // For chaining
206
- }
207
- };
208
-
209
- return manager;
210
- };
211
-
212
- // Create a standalone instance that doesn't depend on any state manager
213
- const TemplateManager = createTemplateManager();
214
-
215
- export default createTemplateManager;
216
- export { createTemplateManager, TemplateManager };