@uistate/core 4.1.2 → 5.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.
Files changed (150) hide show
  1. package/LICENSE-eventTest.md +26 -0
  2. package/README.md +409 -42
  3. package/cssState.js +32 -1
  4. package/eventState.js +58 -49
  5. package/eventTest.js +196 -0
  6. package/examples/001-counter/README.md +44 -0
  7. package/examples/001-counter/eventState.js +86 -0
  8. package/examples/001-counter/index.html +30 -0
  9. package/examples/002-counter-improved/README.md +44 -0
  10. package/examples/002-counter-improved/eventState.js +86 -0
  11. package/examples/002-counter-improved/index.html +44 -0
  12. package/examples/003-input-reactive/README.md +44 -0
  13. package/examples/003-input-reactive/eventState.js +86 -0
  14. package/examples/003-input-reactive/index.html +30 -0
  15. package/examples/004-computed-state/README.md +45 -0
  16. package/examples/004-computed-state/eventState.js +86 -0
  17. package/examples/004-computed-state/index.html +62 -0
  18. package/examples/005-conditional-rendering/README.md +42 -0
  19. package/examples/005-conditional-rendering/eventState.js +86 -0
  20. package/examples/005-conditional-rendering/index.html +36 -0
  21. package/examples/006-list-rendering/README.md +49 -0
  22. package/examples/006-list-rendering/eventState.js +86 -0
  23. package/examples/006-list-rendering/index.html +60 -0
  24. package/examples/007-form-validation/README.md +52 -0
  25. package/examples/007-form-validation/eventState.js +86 -0
  26. package/examples/007-form-validation/index.html +99 -0
  27. package/examples/008-undo-redo/README.md +70 -0
  28. package/examples/008-undo-redo/eventState.js +86 -0
  29. package/examples/008-undo-redo/index.html +105 -0
  30. package/examples/009-localStorage-side-effects/README.md +72 -0
  31. package/examples/009-localStorage-side-effects/eventState.js +86 -0
  32. package/examples/009-localStorage-side-effects/index.html +54 -0
  33. package/examples/010-decoupled-components/README.md +74 -0
  34. package/examples/010-decoupled-components/eventState.js +86 -0
  35. package/examples/010-decoupled-components/index.html +90 -0
  36. package/examples/011-async-patterns/README.md +98 -0
  37. package/examples/011-async-patterns/eventState.js +86 -0
  38. package/examples/011-async-patterns/index.html +129 -0
  39. package/examples/028-counter-improved-eventTest/LICENSE +55 -0
  40. package/examples/028-counter-improved-eventTest/README.md +131 -0
  41. package/examples/028-counter-improved-eventTest/app/store.js +9 -0
  42. package/examples/028-counter-improved-eventTest/index.html +49 -0
  43. package/examples/028-counter-improved-eventTest/runtime/core/behaviors.runtime.js +282 -0
  44. package/examples/028-counter-improved-eventTest/runtime/core/eventState.js +100 -0
  45. package/examples/028-counter-improved-eventTest/runtime/core/helpers.js +212 -0
  46. package/examples/028-counter-improved-eventTest/runtime/core/router.js +271 -0
  47. package/examples/028-counter-improved-eventTest/store.d.ts +8 -0
  48. package/examples/028-counter-improved-eventTest/style.css +170 -0
  49. package/examples/028-counter-improved-eventTest/tests/README.md +208 -0
  50. package/examples/028-counter-improved-eventTest/tests/counter.test.js +116 -0
  51. package/examples/028-counter-improved-eventTest/tests/eventTest.js +176 -0
  52. package/examples/028-counter-improved-eventTest/tests/generateTypes.js +168 -0
  53. package/examples/028-counter-improved-eventTest/tests/run.js +20 -0
  54. package/examples/030-todo-app-with-eventTest/LICENSE +55 -0
  55. package/examples/030-todo-app-with-eventTest/README.md +121 -0
  56. package/examples/030-todo-app-with-eventTest/app/router.js +25 -0
  57. package/examples/030-todo-app-with-eventTest/app/store.js +16 -0
  58. package/examples/030-todo-app-with-eventTest/app/views/home.js +11 -0
  59. package/examples/030-todo-app-with-eventTest/app/views/todoDemo.js +88 -0
  60. package/examples/030-todo-app-with-eventTest/index.html +65 -0
  61. package/examples/030-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
  62. package/examples/030-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
  63. package/examples/030-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
  64. package/examples/030-todo-app-with-eventTest/runtime/core/router.js +271 -0
  65. package/examples/030-todo-app-with-eventTest/store.d.ts +18 -0
  66. package/examples/030-todo-app-with-eventTest/style.css +170 -0
  67. package/examples/030-todo-app-with-eventTest/tests/README.md +208 -0
  68. package/examples/030-todo-app-with-eventTest/tests/eventTest.js +176 -0
  69. package/examples/030-todo-app-with-eventTest/tests/generateTypes.js +189 -0
  70. package/examples/030-todo-app-with-eventTest/tests/run.js +20 -0
  71. package/examples/030-todo-app-with-eventTest/tests/todos.test.js +167 -0
  72. package/examples/031-todo-app-with-eventTest/LICENSE +55 -0
  73. package/examples/031-todo-app-with-eventTest/README.md +54 -0
  74. package/examples/031-todo-app-with-eventTest/TUTORIAL.md +390 -0
  75. package/examples/031-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
  76. package/examples/031-todo-app-with-eventTest/app/bridges.js +113 -0
  77. package/examples/031-todo-app-with-eventTest/app/router.js +26 -0
  78. package/examples/031-todo-app-with-eventTest/app/store.js +15 -0
  79. package/examples/031-todo-app-with-eventTest/app/views/home.js +46 -0
  80. package/examples/031-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
  81. package/examples/031-todo-app-with-eventTest/devtools/dock.js +41 -0
  82. package/examples/031-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
  83. package/examples/031-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
  84. package/examples/031-todo-app-with-eventTest/devtools/telemetry.js +104 -0
  85. package/examples/031-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
  86. package/examples/031-todo-app-with-eventTest/index.html +103 -0
  87. package/examples/031-todo-app-with-eventTest/package-lock.json +2184 -0
  88. package/examples/031-todo-app-with-eventTest/package.json +24 -0
  89. package/examples/031-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
  90. package/examples/031-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
  91. package/examples/031-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
  92. package/examples/031-todo-app-with-eventTest/runtime/core/router.js +271 -0
  93. package/examples/031-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
  94. package/examples/031-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
  95. package/examples/031-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
  96. package/examples/031-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
  97. package/examples/031-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
  98. package/examples/031-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
  99. package/examples/031-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
  100. package/examples/031-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
  101. package/examples/031-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
  102. package/examples/031-todo-app-with-eventTest/store.d.ts +23 -0
  103. package/examples/031-todo-app-with-eventTest/style.css +170 -0
  104. package/examples/031-todo-app-with-eventTest/tests/README.md +208 -0
  105. package/examples/031-todo-app-with-eventTest/tests/eventTest.js +176 -0
  106. package/examples/031-todo-app-with-eventTest/tests/generateTypes.js +191 -0
  107. package/examples/031-todo-app-with-eventTest/tests/run.js +20 -0
  108. package/examples/031-todo-app-with-eventTest/tests/todos.test.js +192 -0
  109. package/examples/032-todo-app-with-eventTest/LICENSE +55 -0
  110. package/examples/032-todo-app-with-eventTest/README.md +54 -0
  111. package/examples/032-todo-app-with-eventTest/TUTORIAL.md +390 -0
  112. package/examples/032-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
  113. package/examples/032-todo-app-with-eventTest/app/actions/index.js +153 -0
  114. package/examples/032-todo-app-with-eventTest/app/bridges.js +113 -0
  115. package/examples/032-todo-app-with-eventTest/app/router.js +26 -0
  116. package/examples/032-todo-app-with-eventTest/app/store.js +15 -0
  117. package/examples/032-todo-app-with-eventTest/app/views/home.js +46 -0
  118. package/examples/032-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
  119. package/examples/032-todo-app-with-eventTest/devtools/dock.js +41 -0
  120. package/examples/032-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
  121. package/examples/032-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
  122. package/examples/032-todo-app-with-eventTest/devtools/telemetry.js +104 -0
  123. package/examples/032-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
  124. package/examples/032-todo-app-with-eventTest/index.html +87 -0
  125. package/examples/032-todo-app-with-eventTest/package-lock.json +2184 -0
  126. package/examples/032-todo-app-with-eventTest/package.json +24 -0
  127. package/examples/032-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
  128. package/examples/032-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
  129. package/examples/032-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
  130. package/examples/032-todo-app-with-eventTest/runtime/core/router.js +271 -0
  131. package/examples/032-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
  132. package/examples/032-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
  133. package/examples/032-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
  134. package/examples/032-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
  135. package/examples/032-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
  136. package/examples/032-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
  137. package/examples/032-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
  138. package/examples/032-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
  139. package/examples/032-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
  140. package/examples/032-todo-app-with-eventTest/store.d.ts +23 -0
  141. package/examples/032-todo-app-with-eventTest/style.css +170 -0
  142. package/examples/032-todo-app-with-eventTest/tests/README.md +208 -0
  143. package/examples/032-todo-app-with-eventTest/tests/eventTest.js +176 -0
  144. package/examples/032-todo-app-with-eventTest/tests/generateTypes.js +191 -0
  145. package/examples/032-todo-app-with-eventTest/tests/run.js +20 -0
  146. package/examples/032-todo-app-with-eventTest/tests/todos.test.js +192 -0
  147. package/index.js +14 -3
  148. package/package.json +16 -7
  149. package/stateSerializer.js +99 -4
  150. package/templateManager.js +50 -2
@@ -0,0 +1,192 @@
1
+ /**
2
+ * todos.test.js - Event-sequence tests for todo functionality
3
+ *
4
+ * These tests define the behavior AND types of the todo domain
5
+ */
6
+
7
+ import { createEventTest, test, runTests } from './eventTest.js';
8
+
9
+ // Helper to set up todo bridges on a test store
10
+ function setupTodoBridges(store) {
11
+ let nextId = 1;
12
+
13
+ // Add todo
14
+ store.subscribe('intent.todo.add', (detail) => {
15
+ const { text } = detail;
16
+ const items = store.get('domain.todos.items') || [];
17
+ const todo = { id: nextId++, text: String(text || '').trim(), done: false };
18
+ if (!todo.text) return;
19
+ store.set('domain.todos.items', [...items, todo]);
20
+ });
21
+
22
+ // Toggle todo
23
+ store.subscribe('intent.todo.toggle', (detail) => {
24
+ const { id } = detail;
25
+ const items = store.get('domain.todos.items') || [];
26
+ const out = items.map(t => (String(t?.id) === String(id)) ? { ...t, done: !t.done } : t);
27
+ store.set('domain.todos.items', out);
28
+ });
29
+
30
+ // Clear completed
31
+ store.subscribe('intent.todo.clearCompleted', () => {
32
+ const items = store.get('domain.todos.items') || [];
33
+ store.set('domain.todos.items', items.filter(t => !t.done));
34
+ });
35
+
36
+ // UI filter
37
+ store.subscribe('intent.ui.filter', (detail) => {
38
+ const { filter } = detail;
39
+ const f = (filter === 'active' || filter === 'completed') ? filter : 'all';
40
+ store.set('ui.todos.filter', f);
41
+ });
42
+ }
43
+
44
+ // Test suite
45
+ const tests = {
46
+ 'add todo creates correct structure': () => {
47
+ const t = createEventTest({
48
+ domain: { todos: { items: [] } }
49
+ });
50
+
51
+ // Set up bridges for this test
52
+ setupTodoBridges(t.store);
53
+
54
+ // Trigger intent
55
+ t.trigger('intent.todo.add', { text: 'Buy milk' });
56
+
57
+ // Assert array structure and element types
58
+ t.assertArrayOf('domain.todos.items', {
59
+ id: 'number',
60
+ text: 'string',
61
+ done: 'boolean'
62
+ });
63
+
64
+ // Assert array length
65
+ t.assertArrayLength('domain.todos.items', 1);
66
+
67
+ // Assert specific values
68
+ const items = t.store.get('domain.todos.items');
69
+ if (items[0].text !== 'Buy milk') {
70
+ throw new Error('Expected first todo text to be "Buy milk"');
71
+ }
72
+ if (items[0].done !== false) {
73
+ throw new Error('Expected first todo to not be done');
74
+ }
75
+ },
76
+
77
+ 'toggle todo changes done state': () => {
78
+ const t = createEventTest({
79
+ domain: { todos: { items: [{ id: 1, text: 'Buy milk', done: false }] } }
80
+ });
81
+
82
+ // Set up bridges for this test
83
+ setupTodoBridges(t.store);
84
+
85
+ // Trigger toggle
86
+ t.trigger('intent.todo.toggle', { id: 1 });
87
+
88
+ // Assert structure
89
+ t.assertArrayOf('domain.todos.items', {
90
+ id: 'number',
91
+ text: 'string',
92
+ done: 'boolean'
93
+ });
94
+
95
+ // Assert done is now true
96
+ const items = t.store.get('domain.todos.items');
97
+ if (items[0].done !== true) {
98
+ throw new Error('Expected todo to be done');
99
+ }
100
+ },
101
+
102
+ 'clear completed removes done todos': () => {
103
+ const t = createEventTest({
104
+ domain: {
105
+ todos: {
106
+ items: [
107
+ { id: 1, text: 'Buy milk', done: true },
108
+ { id: 2, text: 'Walk dog', done: false }
109
+ ]
110
+ }
111
+ }
112
+ });
113
+
114
+ // Set up bridges for this test
115
+ setupTodoBridges(t.store);
116
+
117
+ // Trigger clear
118
+ t.trigger('intent.todo.clearCompleted');
119
+
120
+ // Assert only active todo remains
121
+ t.assertArrayLength('domain.todos.items', 1);
122
+
123
+ const items = t.store.get('domain.todos.items');
124
+ if (items[0].text !== 'Walk dog') {
125
+ throw new Error('Expected only "Walk dog" to remain');
126
+ }
127
+ },
128
+
129
+ 'filter intent updates filter state': () => {
130
+ const t = createEventTest({
131
+ ui: { todos: { filter: 'all' } }
132
+ });
133
+
134
+ // Set up bridges for this test
135
+ setupTodoBridges(t.store);
136
+
137
+ // Trigger filter change
138
+ t.trigger('intent.ui.filter', { filter: 'active' });
139
+
140
+ // Assert type
141
+ t.assertType('ui.todos.filter', 'string');
142
+
143
+ // Assert value
144
+ t.assertPath('ui.todos.filter', 'active');
145
+ },
146
+
147
+ 'intent.todo.add has correct shape': () => {
148
+ const t = createEventTest({});
149
+
150
+ // Trigger intent
151
+ t.trigger('intent.todo.add', { text: 'Buy milk' });
152
+
153
+ // Assert intent shape (for type generation)
154
+ t.assertShape('intent.todo.add', {
155
+ text: 'string'
156
+ });
157
+ },
158
+
159
+ 'intent.todo.toggle has correct shape': () => {
160
+ const t = createEventTest({});
161
+
162
+ // Trigger intent
163
+ t.trigger('intent.todo.toggle', { id: 1 });
164
+
165
+ // Assert intent shape
166
+ t.assertShape('intent.todo.toggle', {
167
+ id: 'number'
168
+ });
169
+ },
170
+
171
+ 'empty todos array is valid': () => {
172
+ const t = createEventTest({
173
+ domain: { todos: { items: [] } }
174
+ });
175
+
176
+ // Assert empty array is still typed correctly
177
+ t.assertArrayOf('domain.todos.items', {
178
+ id: 'number',
179
+ text: 'string',
180
+ done: 'boolean'
181
+ });
182
+
183
+ t.assertArrayLength('domain.todos.items', 0);
184
+ }
185
+ };
186
+
187
+ // Run tests if executed directly
188
+ if (import.meta.url === `file://${process.argv[1]}`) {
189
+ runTests(tests);
190
+ }
191
+
192
+ export default tests;
package/index.js CHANGED
@@ -1,6 +1,17 @@
1
- export { createCssState } from './cssState.js';
1
+ /**
2
+ * UIstate v5 - Core barrel exports
3
+ *
4
+ * EventState is now the primary export for application state management.
5
+ * cssState remains available for CSS variable/theme management use cases.
6
+ */
7
+
8
+ // Primary: EventState (recommended for application state)
2
9
  export { createEventState } from './eventState.js';
10
+ export { createEventState as default } from './eventState.js';
11
+
12
+ // Specialized: CSS State (for CSS variables and theme management)
13
+ export { createCssState } from './cssState.js';
14
+
15
+ // Utilities
3
16
  export { default as stateSerializer } from './stateSerializer.js';
4
17
  export { createTemplateManager } from './templateManager.js';
5
-
6
- export { createCssState as default } from './cssState.js';
package/package.json CHANGED
@@ -1,29 +1,38 @@
1
1
  {
2
2
  "name": "@uistate/core",
3
- "version": "4.1.2",
4
- "description": "Revolutionary DOM-based state management using CSS custom properties - zero dependencies, potential O(1) updates",
3
+ "version": "5.0.0",
4
+ "description": "Lightweight event-driven state management with slot orchestration and experimental event-sequence testing (eventTest.js available under dual license)",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {
8
8
  ".": "./index.js",
9
- "./cssState": "./cssState.js",
10
9
  "./eventState": "./eventState.js",
10
+ "./cssState": "./cssState.js",
11
11
  "./stateSerializer": "./stateSerializer.js",
12
12
  "./templateManager": "./templateManager.js"
13
13
  },
14
14
  "files": [
15
15
  "index.js",
16
+ "eventState.js",
16
17
  "cssState.js",
17
- "eventState.js",
18
18
  "stateSerializer.js",
19
- "templateManager.js"
19
+ "templateManager.js",
20
+ "eventTest.js",
21
+ "LICENSE",
22
+ "LICENSE-eventTest.md",
23
+ "examples/"
20
24
  ],
21
25
  "keywords": [
22
26
  "state-management",
23
- "css-variables",
27
+ "event-driven",
28
+ "reactive",
24
29
  "dom-events",
30
+ "slot-orchestration",
25
31
  "zero-dependency",
26
- "modular"
32
+ "framework-free",
33
+ "micro-framework",
34
+ "testing",
35
+ "event-testing"
27
36
  ],
28
37
  "author": "Ajdin Imsirovic",
29
38
  "license": "MIT",
@@ -1,3 +1,17 @@
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
1
15
  function escapeCssValue(value) {
2
16
  if (typeof value !== 'string') return value;
3
17
  return value.replace(/[^\x20-\x7E]|[!;{}:()[\]/@,'"]/g, function(char) {
@@ -8,6 +22,7 @@ function escapeCssValue(value) {
8
22
 
9
23
  function unescapeCssValue(value) {
10
24
  if (typeof value !== 'string') return value;
25
+ // Only perform unescaping if there are escape sequences
11
26
  if (!value.includes('\\')) return value;
12
27
 
13
28
  return value.replace(/\\([0-9a-f]{1,6})\s?/gi, function(match, hex) {
@@ -15,19 +30,32 @@ function unescapeCssValue(value) {
15
30
  });
16
31
  }
17
32
 
33
+ /**
34
+ * Create a configured serializer instance
35
+ * @param {Object} config - Configuration options
36
+ * @returns {Object} - Serializer instance
37
+ */
18
38
  function createSerializer(config = {}) {
39
+ // Default configuration
19
40
  const defaultConfig = {
20
- mode: 'hybrid',
21
- debug: false,
22
- complexThreshold: 3,
23
- preserveTypes: true,
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
24
45
  };
25
46
 
47
+ // Merge provided config with defaults
26
48
  const options = { ...defaultConfig, ...config };
27
49
 
50
+ // Serializer instance
28
51
  const serializer = {
52
+ // Current configuration
29
53
  config: options,
30
54
 
55
+ /**
56
+ * Update configuration
57
+ * @param {Object} newConfig - New configuration options
58
+ */
31
59
  configure(newConfig) {
32
60
  Object.assign(this.config, newConfig);
33
61
  if (this.config.debug) {
@@ -35,7 +63,14 @@ function createSerializer(config = {}) {
35
63
  }
36
64
  },
37
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
+ */
38
72
  serialize(key, value) {
73
+ // Handle null/undefined
39
74
  if (value === null || value === undefined) {
40
75
  return '';
41
76
  }
@@ -45,24 +80,37 @@ function createSerializer(config = {}) {
45
80
  (Array.isArray(value) ||
46
81
  (Object.keys(value).length >= this.config.complexThreshold));
47
82
 
83
+ // Select serialization strategy based on configuration and value type
48
84
  if (this.config.mode === 'escape' ||
49
85
  (this.config.mode === 'hybrid' && !isComplex)) {
86
+ // Use escape strategy for primitives or when escape mode is forced
50
87
  if (valueType === 'string') {
51
88
  return escapeCssValue(value);
52
89
  } else if (valueType === 'object') {
90
+ // For simple objects in escape mode, still use JSON but with escaping
53
91
  const jsonStr = JSON.stringify(value);
54
92
  return escapeCssValue(jsonStr);
55
93
  } else {
94
+ // For other primitives, convert to string
56
95
  return String(value);
57
96
  }
58
97
  } else {
98
+ // Use JSON strategy for complex objects or when JSON mode is forced
59
99
  return JSON.stringify(value);
60
100
  }
61
101
  },
62
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
+ */
63
109
  deserialize(key, value) {
110
+ // Handle empty values
64
111
  if (!value) return '';
65
112
 
113
+ // Try JSON parse first for values that look like JSON
66
114
  if (this.config.mode !== 'escape' &&
67
115
  ((value.startsWith('{') && value.endsWith('}')) ||
68
116
  (value.startsWith('[') && value.endsWith(']')))) {
@@ -72,42 +120,64 @@ function createSerializer(config = {}) {
72
120
  if (this.config.debug) {
73
121
  console.warn(`Failed to parse JSON for key "${key}":`, value);
74
122
  }
123
+ // Fall through to unescaping if JSON parse fails
75
124
  }
76
125
  }
77
126
 
127
+ // For non-JSON or escape mode, try unescaping
78
128
  const unescaped = unescapeCssValue(value);
79
129
 
130
+ // If unescaped looks like JSON (might have been double-escaped), try parsing it
80
131
  if (this.config.mode !== 'escape' &&
81
132
  ((unescaped.startsWith('{') && unescaped.endsWith('}')) ||
82
133
  (unescaped.startsWith('[') && unescaped.endsWith(']')))) {
83
134
  try {
84
135
  return JSON.parse(unescaped);
85
136
  } catch (e) {
137
+ // Not valid JSON, return unescaped string
86
138
  }
87
139
  }
88
140
 
89
141
  return unescaped;
90
142
  },
91
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
+ */
92
150
  serializeForAttribute(key, value) {
93
151
  if (value === null || value === undefined) return null;
94
152
 
153
+ // For objects, use the standard serializer
95
154
  if (typeof value === 'object') {
96
155
  return this.serialize(key, value);
97
156
  }
98
157
 
158
+ // For primitive values, use direct string conversion
99
159
  return String(value);
100
160
  },
101
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
+ */
102
168
  applyToAttributes(key, value, element = document.documentElement) {
169
+ // Skip null/undefined values
103
170
  if (value === null || value === undefined) {
104
171
  element.removeAttribute(`data-${key}`);
105
172
  return;
106
173
  }
107
174
 
175
+ // Handle objects specially
108
176
  if (typeof value === 'object') {
177
+ // Set the main attribute with serialized value
109
178
  element.setAttribute(`data-${key}`, this.serialize(key, value));
110
179
 
180
+ // For non-array objects, set individual property attributes
111
181
  if (!Array.isArray(value)) {
112
182
  Object.entries(value).forEach(([propKey, propValue]) => {
113
183
  const attributeKey = `data-${key}-${propKey.toLowerCase()}`;
@@ -126,21 +196,37 @@ function createSerializer(config = {}) {
126
196
  });
127
197
  }
128
198
  } else {
199
+ // For primitives, set directly
129
200
  element.setAttribute(`data-${key}`, value);
130
201
  }
131
202
  },
132
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
+ */
133
209
  needsComplexSerialization(value) {
134
210
  return typeof value === 'object' && value !== null;
135
211
  },
136
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
+ */
137
220
  setStateWithCss(uistate, path, value) {
221
+ // Update UIstate
138
222
  uistate.setState(path, value);
139
223
 
224
+ // Update CSS variable with properly serialized value
140
225
  const cssPath = path.replace(/\./g, '-');
141
226
  const serialized = this.serialize(path, value);
142
227
  document.documentElement.style.setProperty(`--${cssPath}`, serialized);
143
228
 
229
+ // Update data attribute for root level state
144
230
  const segments = path.split('.');
145
231
  if (segments.length === 1) {
146
232
  document.documentElement.dataset[path] = typeof value === 'object'
@@ -151,10 +237,18 @@ function createSerializer(config = {}) {
151
237
  return value;
152
238
  },
153
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
+ */
154
246
  getStateFromCss(uistate, path) {
247
+ // First try UIstate
155
248
  const value = uistate.getState(path);
156
249
  if (value !== undefined) return value;
157
250
 
251
+ // If not found, try CSS variable
158
252
  const cssPath = path.replace(/\./g, '-');
159
253
  const cssValue = getComputedStyle(document.documentElement)
160
254
  .getPropertyValue(`--${cssPath}`).trim();
@@ -166,6 +260,7 @@ function createSerializer(config = {}) {
166
260
  return serializer;
167
261
  }
168
262
 
263
+ // Create a default instance with hybrid mode
169
264
  const StateSerializer = createSerializer();
170
265
 
171
266
  export default StateSerializer;
@@ -1,3 +1,8 @@
1
+ /**
2
+ * TemplateManager - Component mounting and event delegation
3
+ * Handles HTML templating, component mounting, and event delegation
4
+ */
5
+
1
6
  const createTemplateManager = (stateManager) => {
2
7
  const manager = {
3
8
  handlers: {},
@@ -7,11 +12,25 @@ const createTemplateManager = (stateManager) => {
7
12
  return this;
8
13
  },
9
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
+ */
10
27
  registerActions(actionsMap) {
11
28
  Object.entries(actionsMap).forEach(([action, handler]) => {
12
29
  if (typeof handler === 'function') {
30
+ // Simple function handler
13
31
  this.onAction(action, handler);
14
32
  } else if (typeof handler === 'object' && handler !== null) {
33
+ // Handler with configuration
15
34
  const { fn, extractId = true, idAttribute = 'id' } = handler;
16
35
 
17
36
  if (typeof fn !== 'function') {
@@ -21,6 +40,7 @@ const createTemplateManager = (stateManager) => {
21
40
  this.onAction(action, (e) => {
22
41
  if (extractId) {
23
42
  const target = e.target.closest('[data-action]');
43
+ // Look for common ID attributes in order of preference
24
44
  const id = target.dataset[idAttribute] ||
25
45
  target.dataset.actionId ||
26
46
  target.dataset.cardId ||
@@ -50,30 +70,40 @@ const createTemplateManager = (stateManager) => {
50
70
  if (typeof handler === 'function') {
51
71
  handler(e);
52
72
  } else if (target.dataset.value !== undefined && stateManager) {
73
+ // If we have a state manager, use it to update state
53
74
  stateManager.setState(action, target.dataset.value);
54
75
  }
55
76
  });
56
77
  return this;
57
78
  },
58
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
+ */
59
86
  renderTemplateFromCss(templateName, data = {}) {
60
87
  const cssTemplate = getComputedStyle(document.documentElement)
61
88
  .getPropertyValue(`--template-${templateName}`)
62
89
  .trim()
63
- .replace(/^['"]|['"]$/g, '');
90
+ .replace(/^['"]|['"]$/g, ''); // Remove surrounding quotes
64
91
 
65
92
  if (!cssTemplate) throw new Error(`Template not found in CSS: --template-${templateName}`);
66
93
 
67
94
  let html = cssTemplate;
68
95
 
96
+ // Replace all placeholders with actual data
69
97
  Object.entries(data).forEach(([key, value]) => {
70
98
  const regex = new RegExp(`{{${key}}}`, 'g');
71
99
  html = html.replace(regex, value);
72
100
  });
73
101
 
102
+ // Create a temporary container
74
103
  const temp = document.createElement('div');
75
104
  temp.innerHTML = html;
76
105
 
106
+ // Return the first child (the rendered template)
77
107
  return temp.firstElementChild;
78
108
  },
79
109
 
@@ -101,11 +131,13 @@ const createTemplateManager = (stateManager) => {
101
131
  return clone.firstElementChild;
102
132
  },
103
133
 
134
+ // Helper to create a reactive component with automatic updates
104
135
  createComponent(name, renderFn, stateKeys = []) {
105
136
  if (!stateManager) {
106
137
  throw new Error('State manager is required for reactive components');
107
138
  }
108
139
 
140
+ // Create template element if it doesn't exist
109
141
  let tpl = document.getElementById(`${name}-template`);
110
142
  if (!tpl) {
111
143
  tpl = document.createElement('template');
@@ -113,8 +145,10 @@ const createTemplateManager = (stateManager) => {
113
145
  document.body.appendChild(tpl);
114
146
  }
115
147
 
148
+ // Initial render
116
149
  tpl.innerHTML = renderFn(stateManager);
117
150
 
151
+ // Set up observers for reactive updates
118
152
  if (stateKeys.length > 0) {
119
153
  stateKeys.forEach(key => {
120
154
  stateManager.observe(key, () => {
@@ -128,6 +162,17 @@ const createTemplateManager = (stateManager) => {
128
162
  };
129
163
  },
130
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
+ */
131
176
  applyClassesFromState(element, stateKey, options = {}) {
132
177
  if (!element) return element;
133
178
 
@@ -146,22 +191,25 @@ const createTemplateManager = (stateManager) => {
146
191
  .replace(/^['"]|['"]$/g, '');
147
192
 
148
193
  if (classString) {
194
+ // Clear existing classes if specified
149
195
  if (clearExisting) {
150
196
  element.className = '';
151
197
  }
152
198
 
199
+ // Add new classes
153
200
  classString.split(' ').forEach(cls => {
154
201
  if (cls) element.classList.add(cls);
155
202
  });
156
203
  }
157
204
 
158
- return element;
205
+ return element; // For chaining
159
206
  }
160
207
  };
161
208
 
162
209
  return manager;
163
210
  };
164
211
 
212
+ // Create a standalone instance that doesn't depend on any state manager
165
213
  const TemplateManager = createTemplateManager();
166
214
 
167
215
  export default createTemplateManager;