@uistate/css 1.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 ADDED
@@ -0,0 +1,63 @@
1
+ # @uistate/css
2
+
3
+ Zero-build CSS-native state management using CSS custom properties and data attributes.
4
+
5
+ ## What is this?
6
+
7
+ A state management library that uses **CSS custom properties** (`--var`) and **`data-*` attributes** as the state transport layer. No bundler, no framework — just drop a `<script>` tag and go.
8
+
9
+ State is inspectable in DevTools (Elements → Computed → CSS variables), and CSS selectors can react to state changes (`[data-theme="dark"]`).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @uistate/css
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```html
20
+ <script type="module">
21
+ import { createCssState } from '@uistate/css';
22
+
23
+ const ui = createCssState({ theme: 'light', count: 0 });
24
+
25
+ // State lives in CSS variables and data attributes
26
+ ui.setState('theme', 'dark');
27
+ // → document.documentElement has --theme: dark and data-theme="dark"
28
+
29
+ // Observe changes
30
+ ui.observe('theme', (value) => {
31
+ console.log('Theme changed to:', value);
32
+ });
33
+ </script>
34
+
35
+ <!-- Declarative observers -->
36
+ <span data-observe="count">0</span>
37
+
38
+ <!-- Declarative actions -->
39
+ <button data-state-action="theme" data-state-value="dark">Dark Mode</button>
40
+ ```
41
+
42
+ ## Modules
43
+
44
+ ### `createCssState(initialState)`
45
+ Core CSS state management. Sets/gets state via CSS custom properties, supports observers, declarative DOM binding via `data-observe` and `data-state-action` attributes.
46
+
47
+ ### `createSerializer(config)`
48
+ Configurable serialization between JavaScript values and CSS-compatible strings. Supports `'escape'`, `'json'`, and `'hybrid'` modes.
49
+
50
+ ### `createTemplateManager(stateManager)`
51
+ Component mounting via `<template>` elements, event delegation via `data-action` attributes, and CSS-variable-based templating.
52
+
53
+ ## Ideal for
54
+
55
+ - **WordPress** — reactive PHP templates without a build step
56
+ - **Jekyll / Hugo / 11ty** — interactivity for static sites
57
+ - **Shopify Liquid / Twig / Blade** — server-rendered templates with client-side state
58
+ - **Web Components** — CSS custom properties pierce shadow DOM
59
+ - **Progressive enhancement** — layer interactivity onto existing HTML
60
+
61
+ ## License
62
+
63
+ MIT
package/cssState.js ADDED
@@ -0,0 +1,212 @@
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/index.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @uistate/css - Zero-build CSS-native state management
3
+ *
4
+ * Uses CSS custom properties and data attributes as the state transport layer.
5
+ * Works in any HTML page — no bundler, no framework required.
6
+ */
7
+
8
+ // Primary: CSS State
9
+ export { createCssState } from './cssState.js';
10
+
11
+ // Serialization: CSS ↔ JS value transformation
12
+ export { default as stateSerializer, createSerializer, escapeCssValue, unescapeCssValue } from './stateSerializer.js';
13
+
14
+ // Templating: Component mounting and event delegation
15
+ export { createTemplateManager, TemplateManager } from './templateManager.js';
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@uistate/css",
3
+ "version": "1.0.0",
4
+ "description": "Zero-build CSS-native state management using CSS custom properties and data attributes",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./cssState": "./cssState.js",
10
+ "./stateSerializer": "./stateSerializer.js",
11
+ "./templateManager": "./templateManager.js"
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "cssState.js",
16
+ "stateSerializer.js",
17
+ "templateManager.js"
18
+ ],
19
+ "keywords": [
20
+ "css-state",
21
+ "css-custom-properties",
22
+ "css-variables",
23
+ "state-management",
24
+ "zero-build",
25
+ "no-framework",
26
+ "declarative",
27
+ "data-attributes",
28
+ "theming",
29
+ "wordpress",
30
+ "static-sites",
31
+ "progressive-enhancement"
32
+ ],
33
+ "author": "Ajdin Imsirovic",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/ImsirovicAjdin/uistate-css"
38
+ }
39
+ }
@@ -0,0 +1,267 @@
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 };
@@ -0,0 +1,216 @@
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 };