@uistate/core 2.0.0 → 3.0.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/README.md +53 -141
- package/package.json +1 -1
- package/src/cssState.js +53 -9
- package/src/index.js +282 -40
- package/src/stateInspector.js +140 -0
- package/src/stateSerializer.js +204 -0
- package/src/telemetryPlugin.js +638 -0
- package/src/templateManager.js +119 -0
package/src/index.js
CHANGED
|
@@ -3,58 +3,211 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A unified system that combines CSS and Event-based state management with templating
|
|
5
5
|
* Integrates CSS variables, event-based state, and template management for optimal performance
|
|
6
|
+
* Provides a simple, declarative API for state management
|
|
7
|
+
* Uses the DOM as the source of truth for state
|
|
8
|
+
* Supports plugins through a clean composition system
|
|
6
9
|
*/
|
|
7
|
-
import
|
|
10
|
+
import createCssState from './cssState.js';
|
|
8
11
|
import { createEventState } from './eventState.js';
|
|
9
12
|
import { createTemplateManager } from './templateManager.js';
|
|
10
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Utility function to convert hierarchical path notation to CSS variable notation
|
|
16
|
+
* Converts dot notation (e.g., 'user.profile.name') to dash notation (e.g., 'user-profile-name')
|
|
17
|
+
* @param {string} path - Hierarchical path with dot notation
|
|
18
|
+
* @returns {string} - CSS-compatible path with dash notation
|
|
19
|
+
*/
|
|
20
|
+
function convertPathToCssPath(path) {
|
|
21
|
+
return path.replace(/\./g, "-");
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
const createUnifiedState = (initialState = {}) => {
|
|
12
|
-
//
|
|
13
|
-
const store = JSON.parse(JSON.stringify(initialState));
|
|
14
|
-
|
|
15
|
-
// Create the CSS state manager
|
|
25
|
+
// Create the CSS state manager with integrated serialization
|
|
16
26
|
const cssState = createCssState(initialState);
|
|
17
27
|
|
|
18
28
|
// Create the event state manager with the same initial state
|
|
19
29
|
const eventState = createEventState(initialState);
|
|
20
30
|
|
|
31
|
+
// Plugin system
|
|
32
|
+
const plugins = new Map();
|
|
33
|
+
const middlewares = [];
|
|
34
|
+
const lifecycleHooks = {
|
|
35
|
+
beforeStateChange: [],
|
|
36
|
+
afterStateChange: [],
|
|
37
|
+
onInit: [],
|
|
38
|
+
onDestroy: []
|
|
39
|
+
};
|
|
40
|
+
|
|
21
41
|
// Create a unified API
|
|
22
42
|
const unifiedState = {
|
|
23
|
-
_isNotifying: false,
|
|
43
|
+
_isNotifying: false,
|
|
44
|
+
_cssState: cssState, // Reference to cssState for direct access to advanced features
|
|
24
45
|
|
|
25
46
|
// Get state with hierarchical path support
|
|
26
47
|
getState(path) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// Try to get from event state first (faster)
|
|
48
|
+
// Always use eventState as the source of truth
|
|
49
|
+
// This ensures consistency with the DOM state
|
|
30
50
|
const value = eventState.get(path);
|
|
31
51
|
|
|
32
|
-
|
|
52
|
+
// If path is undefined or value not found in eventState
|
|
53
|
+
if (path && value === undefined) {
|
|
54
|
+
// Fall back to CSS variable (for values set outside this API)
|
|
55
|
+
const cssPath = convertPathToCssPath(path);
|
|
56
|
+
return cssState.getState(cssPath);
|
|
57
|
+
}
|
|
33
58
|
|
|
34
|
-
|
|
35
|
-
const cssPath = path.replace(/\./g, "-");
|
|
36
|
-
return cssState.getState(cssPath);
|
|
59
|
+
return value;
|
|
37
60
|
},
|
|
38
61
|
|
|
39
62
|
// Set state with hierarchical path support
|
|
40
63
|
setState(path, value) {
|
|
41
|
-
|
|
42
|
-
|
|
64
|
+
if (!path) return this;
|
|
65
|
+
|
|
66
|
+
// Run before state change middlewares
|
|
67
|
+
let finalValue = value;
|
|
68
|
+
let shouldContinue = true;
|
|
69
|
+
|
|
70
|
+
// Apply middlewares
|
|
71
|
+
for (const middleware of middlewares) {
|
|
72
|
+
const result = middleware(path, finalValue, this.getState.bind(this));
|
|
73
|
+
if (result === false) {
|
|
74
|
+
shouldContinue = false;
|
|
75
|
+
break;
|
|
76
|
+
} else if (result !== undefined) {
|
|
77
|
+
finalValue = result;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Run lifecycle hooks
|
|
82
|
+
for (const hook of lifecycleHooks.beforeStateChange) {
|
|
83
|
+
hook(path, finalValue, this.getState.bind(this));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!shouldContinue) return this;
|
|
87
|
+
|
|
88
|
+
// Update event state (for JS access and pub/sub)
|
|
89
|
+
eventState.set(path, finalValue);
|
|
90
|
+
|
|
91
|
+
// Update CSS state (for styling and DOM representation)
|
|
92
|
+
const cssPath = convertPathToCssPath(path);
|
|
93
|
+
cssState.setState(cssPath, finalValue);
|
|
94
|
+
|
|
95
|
+
// Run after state change hooks
|
|
96
|
+
for (const hook of lifecycleHooks.afterStateChange) {
|
|
97
|
+
hook(path, finalValue, this.getState.bind(this));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return this;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// Plugin system methods
|
|
104
|
+
use(pluginName, plugin) {
|
|
105
|
+
if (plugins.has(pluginName)) {
|
|
106
|
+
console.warn(`Plugin '${pluginName}' is already registered. It will be replaced.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Register the plugin
|
|
110
|
+
plugins.set(pluginName, plugin);
|
|
111
|
+
|
|
112
|
+
// Initialize the plugin
|
|
113
|
+
if (typeof plugin.init === 'function') {
|
|
114
|
+
// Pass the API to the plugin
|
|
115
|
+
plugin.init(this);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Register middlewares
|
|
119
|
+
if (Array.isArray(plugin.middlewares)) {
|
|
120
|
+
middlewares.push(...plugin.middlewares);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Register lifecycle hooks
|
|
124
|
+
if (plugin.hooks) {
|
|
125
|
+
Object.entries(plugin.hooks).forEach(([hookName, hookFn]) => {
|
|
126
|
+
if (Array.isArray(lifecycleHooks[hookName])) {
|
|
127
|
+
lifecycleHooks[hookName].push(hookFn);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Add plugin methods to the API
|
|
133
|
+
if (plugin.methods) {
|
|
134
|
+
Object.entries(plugin.methods).forEach(([methodName, method]) => {
|
|
135
|
+
if (typeof method === 'function') {
|
|
136
|
+
this[methodName] = method.bind(plugin);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return this;
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
getPlugin(pluginName) {
|
|
145
|
+
return plugins.get(pluginName);
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
hasPlugin(pluginName) {
|
|
149
|
+
return plugins.has(pluginName);
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
removePlugin(pluginName) {
|
|
153
|
+
const plugin = plugins.get(pluginName);
|
|
154
|
+
if (!plugin) return false;
|
|
155
|
+
|
|
156
|
+
// Run cleanup if available
|
|
157
|
+
if (typeof plugin.destroy === 'function') {
|
|
158
|
+
plugin.destroy();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Remove plugin methods
|
|
162
|
+
if (plugin.methods) {
|
|
163
|
+
Object.keys(plugin.methods).forEach(methodName => {
|
|
164
|
+
delete this[methodName];
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Remove middlewares
|
|
169
|
+
if (Array.isArray(plugin.middlewares)) {
|
|
170
|
+
plugin.middlewares.forEach(middleware => {
|
|
171
|
+
const index = middlewares.indexOf(middleware);
|
|
172
|
+
if (index !== -1) {
|
|
173
|
+
middlewares.splice(index, 1);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
43
177
|
|
|
44
|
-
|
|
178
|
+
// Remove lifecycle hooks
|
|
179
|
+
if (plugin.hooks) {
|
|
180
|
+
Object.entries(plugin.hooks).forEach(([hookName, hookFn]) => {
|
|
181
|
+
if (Array.isArray(lifecycleHooks[hookName])) {
|
|
182
|
+
const index = lifecycleHooks[hookName].indexOf(hookFn);
|
|
183
|
+
if (index !== -1) {
|
|
184
|
+
lifecycleHooks[hookName].splice(index, 1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
45
189
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this._isNotifying = false;
|
|
190
|
+
// Remove the plugin
|
|
191
|
+
plugins.delete(pluginName);
|
|
192
|
+
return true;
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
// Add middleware
|
|
196
|
+
addMiddleware(middleware) {
|
|
197
|
+
if (typeof middleware === 'function') {
|
|
198
|
+
middlewares.push(middleware);
|
|
199
|
+
return true;
|
|
57
200
|
}
|
|
201
|
+
return false;
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
// Add lifecycle hook
|
|
205
|
+
addHook(hookName, hookFn) {
|
|
206
|
+
if (typeof hookFn === 'function' && Array.isArray(lifecycleHooks[hookName])) {
|
|
207
|
+
lifecycleHooks[hookName].push(hookFn);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
58
211
|
},
|
|
59
212
|
|
|
60
213
|
// Subscribe to state changes with support for wildcards
|
|
@@ -67,8 +220,44 @@ const createUnifiedState = (initialState = {}) => {
|
|
|
67
220
|
return cssState.observe(key, callback);
|
|
68
221
|
},
|
|
69
222
|
|
|
223
|
+
// Configure serialization options
|
|
224
|
+
configureSerializer(config) {
|
|
225
|
+
cssState.configureSerializer(config);
|
|
226
|
+
return this;
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
// Get current serializer configuration
|
|
230
|
+
getSerializerConfig() {
|
|
231
|
+
return cssState._serializer?.config || null;
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
// Set serialization mode ('hybrid', 'json', or 'escape')
|
|
235
|
+
setSerializationMode(mode) {
|
|
236
|
+
return this.configureSerializer({ mode });
|
|
237
|
+
},
|
|
238
|
+
|
|
70
239
|
// Clean up resources
|
|
71
240
|
destroy() {
|
|
241
|
+
// Run onDestroy hooks
|
|
242
|
+
for (const hook of lifecycleHooks.onDestroy) {
|
|
243
|
+
hook();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Destroy all plugins
|
|
247
|
+
for (const [pluginName, plugin] of plugins.entries()) {
|
|
248
|
+
if (typeof plugin.destroy === 'function') {
|
|
249
|
+
plugin.destroy();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Clear plugins and middlewares
|
|
254
|
+
plugins.clear();
|
|
255
|
+
middlewares.length = 0;
|
|
256
|
+
Object.keys(lifecycleHooks).forEach(key => {
|
|
257
|
+
lifecycleHooks[key].length = 0;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Destroy core state management
|
|
72
261
|
cssState.destroy();
|
|
73
262
|
eventState.destroy();
|
|
74
263
|
}
|
|
@@ -80,23 +269,75 @@ const createUnifiedState = (initialState = {}) => {
|
|
|
80
269
|
// Create a singleton instance of the unified state
|
|
81
270
|
const UnifiedState = createUnifiedState();
|
|
82
271
|
|
|
83
|
-
// Create a template manager
|
|
84
|
-
const
|
|
272
|
+
// Create a template manager plugin
|
|
273
|
+
const templateManagerPlugin = {
|
|
274
|
+
name: 'templateManager',
|
|
275
|
+
instance: null,
|
|
276
|
+
|
|
277
|
+
init(api) {
|
|
278
|
+
this.instance = createTemplateManager(api);
|
|
279
|
+
return this;
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
destroy() {
|
|
283
|
+
// Any cleanup needed for the template manager
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
// Plugin methods that will be added to the UIstate API
|
|
287
|
+
methods: {
|
|
288
|
+
onAction(action, handler) {
|
|
289
|
+
return this.instance.onAction(action, handler);
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
registerActions(actionsMap) {
|
|
293
|
+
return this.instance.registerActions(actionsMap);
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
attachDelegation(root = document.body) {
|
|
297
|
+
return this.instance.attachDelegation(root);
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
mount(componentName, container) {
|
|
301
|
+
return this.instance.mount(componentName, container);
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
renderTemplateFromCss(templateName, data) {
|
|
305
|
+
return this.instance.renderTemplateFromCss(templateName, data);
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
createComponent(name, renderFn, stateKeys) {
|
|
309
|
+
return this.instance.createComponent(name, renderFn, stateKeys);
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
applyClassesFromState(element, stateKey, options) {
|
|
313
|
+
return this.instance.applyClassesFromState(element, stateKey, options);
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
// Expose handlers property
|
|
318
|
+
get handlers() {
|
|
319
|
+
return this.instance.handlers;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
85
322
|
|
|
86
|
-
// Create a combined API
|
|
323
|
+
// Create a combined API with plugin support
|
|
87
324
|
const UIstate = {
|
|
88
325
|
...UnifiedState,
|
|
89
326
|
|
|
90
|
-
//
|
|
91
|
-
handlers: TemplateManager.handlers,
|
|
92
|
-
onAction: TemplateManager.onAction.bind(TemplateManager),
|
|
93
|
-
attachDelegation: TemplateManager.attachDelegation.bind(TemplateManager),
|
|
94
|
-
mount: TemplateManager.mount.bind(TemplateManager),
|
|
95
|
-
|
|
96
|
-
// Initialize both systems
|
|
327
|
+
// Initialize the system with plugins
|
|
97
328
|
init() {
|
|
329
|
+
// Register the template manager plugin
|
|
330
|
+
this.use('templateManager', templateManagerPlugin);
|
|
331
|
+
|
|
332
|
+
// Run onInit hooks
|
|
333
|
+
for (const hook of this.getPlugin('templateManager').instance.lifecycleHooks?.onInit || []) {
|
|
334
|
+
// for (const hook of lifecycleHooks.onInit) {
|
|
335
|
+
hook();
|
|
336
|
+
}
|
|
337
|
+
|
|
98
338
|
// Attach event delegation to document body
|
|
99
339
|
this.attachDelegation(document.body);
|
|
340
|
+
|
|
100
341
|
return this;
|
|
101
342
|
}
|
|
102
343
|
};
|
|
@@ -104,8 +345,9 @@ const UIstate = {
|
|
|
104
345
|
export default UIstate;
|
|
105
346
|
export {
|
|
106
347
|
createUnifiedState,
|
|
107
|
-
|
|
348
|
+
createCssState,
|
|
349
|
+
createEventState,
|
|
108
350
|
createTemplateManager,
|
|
109
|
-
|
|
110
|
-
|
|
351
|
+
convertPathToCssPath,
|
|
352
|
+
templateManagerPlugin
|
|
111
353
|
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StateInspector - Real-time state inspection panel for UIstate
|
|
3
|
+
*
|
|
4
|
+
* This module provides a configurable UI panel that displays and allows manipulation
|
|
5
|
+
* of CSS variables and state values in UIstate applications.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Toggle visibility with a dedicated button
|
|
9
|
+
* - Real-time updates of CSS variable values
|
|
10
|
+
* - Organized display of state by categories
|
|
11
|
+
* - Ability to filter state variables
|
|
12
|
+
* - Direct manipulation of state values
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a configured state inspector instance
|
|
17
|
+
* @param {Object} config - Configuration options
|
|
18
|
+
* @returns {Object} - StateInspector instance
|
|
19
|
+
*/
|
|
20
|
+
function createStateInspector(config = {}) {
|
|
21
|
+
// Default configuration
|
|
22
|
+
const defaultConfig = {
|
|
23
|
+
target: document.body,
|
|
24
|
+
initiallyVisible: false,
|
|
25
|
+
categories: ['app', 'ui', 'data'],
|
|
26
|
+
position: 'bottom-right',
|
|
27
|
+
maxHeight: '300px',
|
|
28
|
+
stateManager: null, // Optional reference to UIstate or CssState instance
|
|
29
|
+
theme: 'light'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Merge provided config with defaults
|
|
33
|
+
const options = { ...defaultConfig, ...config };
|
|
34
|
+
|
|
35
|
+
// Private state
|
|
36
|
+
let isVisible = options.initiallyVisible;
|
|
37
|
+
let panel = null;
|
|
38
|
+
let toggleButton = null;
|
|
39
|
+
let filterInput = null;
|
|
40
|
+
let stateContainer = null;
|
|
41
|
+
let currentFilter = '';
|
|
42
|
+
|
|
43
|
+
// StateInspector instance
|
|
44
|
+
const inspector = {
|
|
45
|
+
// Current configuration
|
|
46
|
+
config: options,
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Update configuration
|
|
50
|
+
* @param {Object} newConfig - New configuration options
|
|
51
|
+
*/
|
|
52
|
+
configure(newConfig) {
|
|
53
|
+
Object.assign(this.config, newConfig);
|
|
54
|
+
this.refresh();
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Attach the inspector panel to the target element
|
|
59
|
+
* @param {Element} element - Target element to attach to (defaults to config.target)
|
|
60
|
+
* @returns {Object} - The inspector instance for chaining
|
|
61
|
+
*/
|
|
62
|
+
attach(element = this.config.target) {
|
|
63
|
+
// Implementation will create and attach the panel
|
|
64
|
+
// For now, this is a placeholder
|
|
65
|
+
console.log('StateInspector: Panel would be attached to', element);
|
|
66
|
+
return this;
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Show the inspector panel
|
|
71
|
+
* @returns {Object} - The inspector instance for chaining
|
|
72
|
+
*/
|
|
73
|
+
show() {
|
|
74
|
+
isVisible = true;
|
|
75
|
+
if (panel) panel.style.display = 'block';
|
|
76
|
+
return this;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Hide the inspector panel
|
|
81
|
+
* @returns {Object} - The inspector instance for chaining
|
|
82
|
+
*/
|
|
83
|
+
hide() {
|
|
84
|
+
isVisible = false;
|
|
85
|
+
if (panel) panel.style.display = 'none';
|
|
86
|
+
return this;
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Toggle the visibility of the inspector panel
|
|
91
|
+
* @returns {Object} - The inspector instance for chaining
|
|
92
|
+
*/
|
|
93
|
+
toggle() {
|
|
94
|
+
return isVisible ? this.hide() : this.show();
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Refresh the state display
|
|
99
|
+
* @returns {Object} - The inspector instance for chaining
|
|
100
|
+
*/
|
|
101
|
+
refresh() {
|
|
102
|
+
// Implementation will update the displayed state values
|
|
103
|
+
// For now, this is a placeholder
|
|
104
|
+
console.log('StateInspector: State display would be refreshed');
|
|
105
|
+
return this;
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Filter the displayed state variables
|
|
110
|
+
* @param {string} filterText - Text to filter by
|
|
111
|
+
* @returns {Object} - The inspector instance for chaining
|
|
112
|
+
*/
|
|
113
|
+
filter(filterText) {
|
|
114
|
+
currentFilter = filterText;
|
|
115
|
+
// Implementation will filter the displayed variables
|
|
116
|
+
return this.refresh();
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Clean up resources used by the inspector
|
|
121
|
+
*/
|
|
122
|
+
destroy() {
|
|
123
|
+
if (panel && panel.parentNode) {
|
|
124
|
+
panel.parentNode.removeChild(panel);
|
|
125
|
+
}
|
|
126
|
+
panel = null;
|
|
127
|
+
toggleButton = null;
|
|
128
|
+
filterInput = null;
|
|
129
|
+
stateContainer = null;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return inspector;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Create a default instance
|
|
137
|
+
const StateInspector = createStateInspector();
|
|
138
|
+
|
|
139
|
+
export default StateInspector;
|
|
140
|
+
export { createStateInspector };
|