@ape-egg/vibe 1.1.2 → 1.3.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.
@@ -1,19 +1,25 @@
1
1
  import { resolvePath, deepEqual } from './iteration-utils.js';
2
2
  import { extractDependencies } from './conditionals.js';
3
3
  import { BINDING_REGEX } from './constants.js';
4
- import { evalInScope } from './utils.js';
4
+ import { evalInScope, resolveThisPath } from './utils.js';
5
5
 
6
6
  // Evaluate conditional expression
7
- const evaluateCondition = (expression, state) => !!evalInScope(expression, state);
7
+ const evaluateCondition = (expression, state, element = null) => !!evalInScope(expression, state, element);
8
8
 
9
9
  // Helper function to check if a match references a specific key
10
- const matchesKey = (matchStr, key) => matchStr === key || matchStr.startsWith(key + '.');
10
+ const matchesKey = (matchStr, key) =>
11
+ matchStr === key ||
12
+ matchStr.startsWith(key + '.') ||
13
+ matchStr.startsWith(key + '[');
11
14
 
12
- const recursive = (tree, state, newState, affected, scopedStateForHydration = null) => {
15
+ const recursive = (tree, state, newState, affected, scopedStateForHydration = null, depth = 0) => {
13
16
  // Handle iteration nodes specially
14
17
  if (tree.type === 'iteration') {
15
- const oldArray = resolvePath(state, tree.meta.arrayPath);
16
- const newArray = resolvePath(newState, tree.meta.arrayPath);
18
+ // Handle this.property for component-scoped arrays
19
+ const arrayPath = resolveThisPath(tree.meta.arrayPath, tree.meta.startComment?.parentElement);
20
+
21
+ const oldArray = resolvePath(state, arrayPath);
22
+ const newArray = resolvePath(newState, arrayPath);
17
23
 
18
24
  // Fast path: reference comparison (arrays are typically replaced, not mutated)
19
25
  // This avoids expensive O(n) deepEqual for large arrays
@@ -34,7 +40,8 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
34
40
  // Use the instance's scoped state (includes item, index, etc.)
35
41
  // Merge newState into scopedState to get updated global values
36
42
  const mergedNewState = { ...instance.scopedState, ...newState };
37
- recursive(instance.tree, instance.scopedState, mergedNewState, affected, mergedNewState);
43
+ // Pass the Proxy as scopedState so evalInScope can access iteration variables
44
+ recursive(instance.tree, instance.scopedState, mergedNewState, affected, instance.scopedState, depth + 1);
38
45
  }
39
46
  }
40
47
  }
@@ -44,8 +51,8 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
44
51
 
45
52
  // Handle conditional nodes specially
46
53
  if (tree.type === 'conditional') {
47
- const oldValue = evaluateCondition(tree.meta.expression, state);
48
- const newValue = evaluateCondition(tree.meta.expression, newState);
54
+ const oldValue = evaluateCondition(tree.meta.expression, state, tree.meta.startComment?.parentElement);
55
+ const newValue = evaluateCondition(tree.meta.expression, newState, tree.meta.startComment?.parentElement);
49
56
 
50
57
  // Check if condition result changed
51
58
  if (oldValue !== newValue) {
@@ -59,7 +66,7 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
59
66
 
60
67
  // Condition didn't change, check for affected elements inside active branch
61
68
  if (tree.runtime.activeInstance && tree.runtime.activeInstance.parsedTree) {
62
- return recursive(tree.runtime.activeInstance.parsedTree, state, newState, affected, scopedStateForHydration);
69
+ return recursive(tree.runtime.activeInstance.parsedTree, state, newState, affected, scopedStateForHydration, depth + 1);
63
70
  }
64
71
 
65
72
  return affected;
@@ -83,21 +90,24 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
83
90
  const checkedMatches = [];
84
91
 
85
92
  for (const m of matches) {
86
- const noMatch = !shallowState.some((key) => matchesKey(m.inner, key));
93
+ // Resolve this.property to componentId.property
94
+ const resolvedInner = resolveThisPath(m.inner, tree.element);
95
+
96
+ const noMatch = !shallowState.some((key) => matchesKey(resolvedInner, key));
87
97
 
88
98
  let shouldAffect = false;
89
99
  let relevantKeys = [];
90
100
 
91
101
  if (isInitialHydration) {
92
102
  // Initial hydration: affect all matched keys
93
- relevantKeys = shallowNewState.filter((key) => matchesKey(m.inner, key));
103
+ relevantKeys = shallowNewState.filter((key) => matchesKey(resolvedInner, key));
94
104
  shouldAffect = noMatch || relevantKeys.length > 0;
95
105
  } else {
96
106
  // Update: only affect if value changed
97
107
  const changedKeys = shallowNewState.filter((key) =>
98
- matchesKey(m.inner, key) && state[key] !== newState[key]
108
+ matchesKey(resolvedInner, key) && state[key] !== newState[key]
99
109
  );
100
- relevantKeys = changedKeys.length > 0 ? changedKeys : shallowState.filter((key) => matchesKey(m.inner, key));
110
+ relevantKeys = changedKeys.length > 0 ? changedKeys : shallowState.filter((key) => matchesKey(resolvedInner, key));
101
111
  shouldAffect = noMatch || changedKeys.length > 0;
102
112
  }
103
113
 
@@ -119,6 +129,7 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
119
129
  input: m.input,
120
130
  matches: m.matches,
121
131
  element: tree.element,
132
+ textNode: tree.textNode, // Reference to specific text node (prevents wiping children)
122
133
  scopedState: scopedStateForHydration, // Pass scoped state from iteration context
123
134
  });
124
135
  }
@@ -140,18 +151,21 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
140
151
  }
141
152
 
142
153
  for (const m of attrMatches) {
143
- const noMatch = !shallowState.some((key) => matchesKey(m.inner, key));
154
+ // Resolve this.property to componentId.property
155
+ const resolvedInner = resolveThisPath(m.inner, tree.element);
156
+
157
+ const noMatch = !shallowState.some((key) => matchesKey(resolvedInner, key));
144
158
 
145
159
  let shouldAffect = false;
146
160
 
147
161
  if (isInitialHydration) {
148
162
  // Initial hydration: affect all matched keys
149
- const newMatches = shallowNewState.filter((key) => matchesKey(m.inner, key));
163
+ const newMatches = shallowNewState.filter((key) => matchesKey(resolvedInner, key));
150
164
  shouldAffect = noMatch || newMatches.length > 0;
151
165
  } else {
152
166
  // Update: only affect if value changed
153
167
  const changedKeys = shallowNewState.filter((key) =>
154
- matchesKey(m.inner, key) && state[key] !== newState[key]
168
+ matchesKey(resolvedInner, key) && state[key] !== newState[key]
155
169
  );
156
170
  shouldAffect = noMatch || changedKeys.length > 0;
157
171
  }
@@ -162,7 +176,7 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
162
176
  attrName,
163
177
  attrValue,
164
178
  matchOuter: m.outer,
165
- matchInner: m.inner,
179
+ matchInner: m.inner, // Keep original, evalInScope will resolve this.
166
180
  element: tree.element,
167
181
  scopedState: scopedStateForHydration, // Pass scoped state from iteration context
168
182
  });
@@ -186,16 +200,19 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
186
200
  }
187
201
 
188
202
  for (const m of nameMatches) {
189
- const noMatch = !shallowState.some((key) => matchesKey(m.inner, key));
203
+ // Resolve this.property to componentId.property
204
+ const resolvedInner = resolveThisPath(m.inner, tree.element);
205
+
206
+ const noMatch = !shallowState.some((key) => matchesKey(resolvedInner, key));
190
207
 
191
208
  let shouldAffect = false;
192
209
 
193
210
  if (isInitialHydration) {
194
- const newMatches = shallowNewState.filter((key) => matchesKey(m.inner, key));
211
+ const newMatches = shallowNewState.filter((key) => matchesKey(resolvedInner, key));
195
212
  shouldAffect = noMatch || newMatches.length > 0;
196
213
  } else {
197
214
  const changedKeys = shallowNewState.filter((key) =>
198
- matchesKey(m.inner, key) && state[key] !== newState[key]
215
+ matchesKey(resolvedInner, key) && state[key] !== newState[key]
199
216
  );
200
217
  shouldAffect = noMatch || changedKeys.length > 0;
201
218
  }
@@ -205,7 +222,7 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
205
222
  type: 'nameBinding',
206
223
  nameBinding,
207
224
  matchOuter: m.outer,
208
- matchInner: m.inner,
225
+ matchInner: m.inner, // Keep original, evalInScope will resolve this.
209
226
  element: tree.element,
210
227
  scopedState: scopedStateForHydration,
211
228
  });
@@ -220,7 +237,7 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
220
237
  for (const key in children) {
221
238
  const child = children[key];
222
239
  if (child && typeof child === 'object') {
223
- recursive(child, state, newState, affected, scopedStateForHydration);
240
+ recursive(child, state, newState, affected, scopedStateForHydration, depth + 1);
224
241
  }
225
242
  }
226
243
  }
@@ -228,4 +245,7 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
228
245
  return affected;
229
246
  };
230
247
 
231
- export default (tree, state, newState) => recursive(tree, state, newState, [], null);
248
+ export default (tree, state, newState) => {
249
+ const affected = recursive(tree, state, newState, [], null, 0);
250
+ return affected;
251
+ };
@@ -1,5 +1,5 @@
1
1
  import { debugLog } from './debug.js';
2
- import { PHASE_COMPLETE } from './constants.js';
2
+ import { PHASE_READY, FOUC_CLASS_OR_ATTR } from './constants.js';
3
3
 
4
4
  /**
5
5
  * Check if all Vibe processing is complete and cleanup can run
@@ -7,30 +7,27 @@ import { PHASE_COMPLETE } from './constants.js';
7
7
  * @returns {Boolean} - true if cleanup should run
8
8
  */
9
9
  export const shouldCleanup = (rootElement) => {
10
- // 1. Check for pending <component> elements
11
- const componentElements = rootElement.querySelectorAll('component');
10
+ // 1. Check for pending <component> elements (only those with src - fetched components)
11
+ // Inline component wrappers (<component> without src) are fine to remain
12
+ const componentElements = rootElement.querySelectorAll('component[src], div.component[src]');
12
13
  if (componentElements.length > 0) {
13
14
  return false;
14
15
  }
15
16
 
16
17
  // 2. Check for unhydrated bindings (literal @[...] in DOM, excluding dehydrated elements)
17
- const walker = document.createTreeWalker(
18
- rootElement,
19
- NodeFilter.SHOW_TEXT,
20
- {
21
- acceptNode(node) {
22
- // Check if this text node is inside a dehydrated element
23
- let parent = node.parentElement;
24
- while (parent && parent !== rootElement) {
25
- if (parent.hasAttribute('dehydrate')) {
26
- return NodeFilter.FILTER_REJECT; // Skip dehydrated content
27
- }
28
- parent = parent.parentElement;
18
+ const walker = document.createTreeWalker(rootElement, NodeFilter.SHOW_TEXT, {
19
+ acceptNode(node) {
20
+ // Check if this text node is inside a dehydrated element
21
+ let parent = node.parentElement;
22
+ while (parent && parent !== rootElement) {
23
+ if (parent.hasAttribute('dehydrate')) {
24
+ return NodeFilter.FILTER_REJECT; // Skip dehydrated content
29
25
  }
30
- return NodeFilter.FILTER_ACCEPT;
26
+ parent = parent.parentElement;
31
27
  }
32
- }
33
- );
28
+ return NodeFilter.FILTER_ACCEPT;
29
+ },
30
+ });
34
31
 
35
32
  let node;
36
33
  while ((node = walker.nextNode())) {
@@ -44,16 +41,40 @@ export const shouldCleanup = (rootElement) => {
44
41
  };
45
42
 
46
43
  /**
47
- * Perform cleanup - remove vibe attribute to reveal content
44
+ * Perform cleanup - remove attribute or class from all matching elements
48
45
  * @param {Element} rootElement - Root element
49
- * @param {String} attrName - Attribute name to remove (default: 'vibe')
50
46
  * @param {Boolean} debug - Debug mode
51
47
  */
52
- export const cleanup = (rootElement, attrName = 'vibe', debug = false) => {
48
+ export const cleanup = (rootElement, debug = false) => {
53
49
  // Force reflow
54
50
  rootElement.offsetHeight;
55
51
 
56
- // Remove vibe attribute
57
- debugLog(PHASE_COMPLETE, `removing [${attrName}] attribute`, debug);
58
- rootElement.removeAttribute(attrName);
52
+ // Determine if selector is a class (starts with .) or attribute (default)
53
+ const isClass = FOUC_CLASS_OR_ATTR.startsWith('.');
54
+ const cleanName = FOUC_CLASS_OR_ATTR.replace(/^\./, '').replace(/^\[/, '').replace(/\]$/, '');
55
+
56
+ if (isClass) {
57
+ // Remove class from all elements in document that have it
58
+ const elements = document.querySelectorAll(`.${cleanName}`);
59
+ elements.forEach((el) => el.classList.remove(cleanName));
60
+ debugLog(PHASE_READY, `removing .${cleanName} class from ${elements.length} elements`, debug);
61
+ } else {
62
+ // Remove attribute from all elements in document that have it
63
+ const elements = document.querySelectorAll(`[${cleanName}]`);
64
+ elements.forEach((el) => el.removeAttribute(cleanName));
65
+ debugLog(
66
+ PHASE_READY,
67
+ `removing [${cleanName}] attribute from ${elements.length} elements`,
68
+ debug,
69
+ );
70
+ }
71
+
72
+ // Dispatch ready event to signal that Vibe has completed all initial processing
73
+ if (typeof document !== 'undefined') {
74
+ document.dispatchEvent(
75
+ new CustomEvent('vibe:ready', {
76
+ detail: { rootElement, cleanName, isClass },
77
+ }),
78
+ );
79
+ }
59
80
  };
@@ -0,0 +1,63 @@
1
+ // Shared utility for processing component state
2
+
3
+ /**
4
+ * Generate unique component ID
5
+ */
6
+ export const generateComponentId = () => {
7
+ return `_c${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
8
+ };
9
+
10
+ /**
11
+ * Transform variable declarations to property assignments
12
+ * let count = 0; → this.count = 0;
13
+ */
14
+ const transformDeclarations = (code) => {
15
+ return code
16
+ // Transform: let varName = value;
17
+ .replace(/\b(let|const|var)\s+(\w+)\s*=\s*([^;]+);/g, 'this.$2 = $3;')
18
+ // Transform: let varName;
19
+ .replace(/\b(let|const|var)\s+(\w+)\s*;/g, 'this.$2 = undefined;');
20
+ };
21
+
22
+ /**
23
+ * Execute component script and return state object
24
+ * @param {string} scriptContent - JavaScript code to execute
25
+ * @returns {object} - Component state object
26
+ */
27
+ export const executeComponentScript = (scriptContent) => {
28
+ const componentState = {};
29
+
30
+ if (!scriptContent || !scriptContent.trim()) {
31
+ return componentState;
32
+ }
33
+
34
+ try {
35
+ // Transform declarations to property assignments
36
+ const transformed = transformDeclarations(scriptContent);
37
+
38
+ // Execute script with 'this' = componentState
39
+ const fn = new Function(transformed);
40
+ fn.call(componentState);
41
+
42
+ } catch (error) {
43
+ console.error('[vibe] Error executing component script:', error);
44
+ console.error('[vibe] Script content:', scriptContent);
45
+ }
46
+
47
+ return componentState;
48
+ };
49
+
50
+ /**
51
+ * Register component state in global store
52
+ * @param {string} componentId - Unique component identifier
53
+ * @param {object} state - Component state object
54
+ * @param {object} globalState - Global $ object
55
+ */
56
+ export const registerComponentState = (componentId, state, globalState) => {
57
+ if (!globalState[componentId]) {
58
+ globalState[componentId] = state;
59
+ } else {
60
+ // Component already registered, merge new state
61
+ Object.assign(globalState[componentId], state);
62
+ }
63
+ };
@@ -1,6 +1,7 @@
1
1
  import { debugLog } from './debug.js';
2
2
  import { PHASE_FETCH } from './constants.js';
3
3
  import { evalInScope } from './utils.js';
4
+ import { generateComponentId, executeComponentScript } from './component-state.js';
4
5
 
5
6
  // Helper to escape regex special characters
6
7
  const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -19,7 +20,10 @@ export const abortComponentFetch = (element) => {
19
20
 
20
21
  export const processComponent = (rootElement, onComplete, config = {}) => {
21
22
  const debug = !!config?.debug;
22
- const componentElements = rootElement.querySelectorAll('component');
23
+ // Only process component elements with src attribute (fetched components)
24
+ // Supports: <component src="..."> and <div class="component" src="...">
25
+ // Ignores: <component> and <div class="component"> (inline component wrappers)
26
+ const componentElements = rootElement.querySelectorAll('component[src], div.component[src]');
23
27
 
24
28
  if (componentElements.length === 0) {
25
29
  if (onComplete) onComplete();
@@ -30,12 +34,6 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
30
34
  const el = componentElements[0];
31
35
  const src = el.getAttribute('src');
32
36
 
33
- if (!src) {
34
- el.remove();
35
- // Don't recursively call - let MutationObserver handle it
36
- return;
37
- }
38
-
39
37
  // Capture children and props before fetching
40
38
  const children = el.innerHTML.trim();
41
39
  const props = {};
@@ -52,18 +50,54 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
52
50
  fetch(src, { signal: controller.signal })
53
51
  .then((r) => r.text())
54
52
  .then((html) => {
55
- // Transform the fetched HTML
56
- let transformedHtml = html;
53
+ // Parse HTML in temporary container to process component scripts
54
+ const temp = document.createElement('div');
55
+ temp.innerHTML = html;
56
+
57
+ // Process any <script type="component"> elements
58
+ const componentScripts = temp.querySelectorAll('script[type="component"]');
59
+ componentScripts.forEach((script) => {
60
+ const scriptContent = script.textContent?.trim() || '';
61
+ if (!scriptContent) return;
62
+
63
+ // Generate component ID
64
+ const componentId = generateComponentId();
65
+
66
+ // Tag script element
67
+ script.setAttribute('data-vibe-component-id', componentId);
68
+
69
+ // Tag following siblings with this component ID
70
+ let sibling = script.nextElementSibling;
71
+ while (sibling) {
72
+ // Stop if we hit another component script
73
+ if (sibling.tagName === 'SCRIPT' && sibling.getAttribute('type') === 'component') {
74
+ break;
75
+ }
76
+ sibling.setAttribute('data-vibe-component-id', componentId);
77
+ sibling = sibling.nextElementSibling;
78
+ }
79
+
80
+ // Execute component script to get state
81
+ const componentState = executeComponentScript(scriptContent);
82
+
83
+ // Register in global state
84
+ if (window.$) {
85
+ window.$[componentId] = componentState;
86
+ }
87
+ });
88
+
89
+ // Get transformed HTML from temp container
90
+ let transformedHtml = temp.innerHTML;
57
91
 
58
92
  // Replace props
59
93
  Object.entries(props).forEach(([propName, propValue]) => {
60
94
  const bindingMatch = propValue.match(/^@\[(.+)\]$/);
61
95
 
62
96
  if (bindingMatch) {
63
- // Reactive prop: replace propName as word boundary
97
+ // Reactive prop: replace @[propName] with @[path]
64
98
  const path = bindingMatch[1];
65
- const propPattern = new RegExp(`\\b${escapeRegex(propName)}\\b`, 'g');
66
- transformedHtml = transformedHtml.replace(propPattern, path);
99
+ const propPattern = new RegExp(`@\\[${escapeRegex(propName)}\\]`, 'g');
100
+ transformedHtml = transformedHtml.replace(propPattern, `@[${path}]`);
67
101
  } else {
68
102
  // Static prop: replace @[propName] with literal value
69
103
  const propPattern = new RegExp(`@\\[${escapeRegex(propName)}\\]`, 'g');
@@ -80,14 +114,24 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
80
114
  // Clean up pending fetch tracker
81
115
  pendingFetches.delete(el);
82
116
 
83
- // Set outerHTML - this triggers MutationObserver
117
+ // Replace with clean component wrapper (no src, no props)
84
118
  // Check if element still has a parent (might have been removed during fetch)
85
119
  if (el.parentNode) {
86
- el.outerHTML = transformedHtml;
120
+ // Create clean wrapper element (preserve tag type: component or div.component)
121
+ const newWrapper = el.tagName === 'DIV'
122
+ ? document.createElement('div')
123
+ : document.createElement('component');
124
+
125
+ if (el.tagName === 'DIV') {
126
+ newWrapper.className = 'component';
127
+ }
128
+
129
+ newWrapper.innerHTML = transformedHtml;
130
+ el.replaceWith(newWrapper);
87
131
  debugLog(PHASE_FETCH, src, debug);
88
132
 
89
133
  // Force immediate processing of the mutation (MutationObserver is async, but we need sync)
90
- // Use microtask to process right after outerHTML completes
134
+ // Use microtask to process right after replaceWith completes
91
135
  if (config._forceSync && config._processMutations && config._observer) {
92
136
  Promise.resolve().then(() => {
93
137
  const pending = config._observer.takeRecords();
@@ -5,7 +5,7 @@ import { createScopedState, renderAllIterations, initializeBlock } from './itera
5
5
  import { evalInScope } from './utils.js';
6
6
 
7
7
  // Evaluate conditional expression in state context
8
- const evaluateCondition = (expression, state) => !!evalInScope(expression, state);
8
+ const evaluateCondition = (expression, state, element = null) => !!evalInScope(expression, state, element);
9
9
 
10
10
  // Extract state dependencies from an expression
11
11
  // e.g., "count > 5" → ["count"]
@@ -78,7 +78,7 @@ export const renderConditional = (node, state, manifest, parentScope = {}) => {
78
78
  startComment.__vibeRendered = true;
79
79
 
80
80
  // Evaluate condition with current state
81
- const conditionResult = evaluateCondition(expression, state);
81
+ const conditionResult = evaluateCondition(expression, state, startComment.parentElement);
82
82
 
83
83
  // Determine which branch to mount
84
84
  const branchToMount = conditionResult ? branches.if : branches.else;
@@ -154,7 +154,7 @@ const unmountBranch = (node) => {
154
154
 
155
155
  // Update conditional when dependencies change
156
156
  export const updateConditional = (node, newState, oldState, manifest, parentScope = {}) => {
157
- const { expression, branches } = node.meta;
157
+ const { expression, branches, startComment } = node.meta;
158
158
 
159
159
  // If not yet rendered, skip (renderConditional handles initial render)
160
160
  if (!node.runtime.templateRemoved) {
@@ -162,7 +162,7 @@ export const updateConditional = (node, newState, oldState, manifest, parentScop
162
162
  }
163
163
 
164
164
  // Evaluate expression with new state
165
- const newConditionResult = evaluateCondition(expression, newState);
165
+ const newConditionResult = evaluateCondition(expression, newState, startComment?.parentElement);
166
166
  const newBranchData = newConditionResult ? branches.if : branches.else;
167
167
 
168
168
  // Check if branch changed (compare references)
@@ -1,12 +1,13 @@
1
1
  // Debug logger name
2
2
  export const DEBUGGER_NAME = '[vibe-debug]:';
3
+ export const FOUC_CLASS_OR_ATTR = 'vibe-fouc'; // Class or attribute used to prevent FOUC (default: [vibe])
3
4
 
4
5
  // Lifecycle phase names for debug logging
5
6
  // ONE-OFF operations (run once during initialization)
6
7
  export const PHASE_ATTACH = 'Attached'; // Latches onto DOM element (index.js)
7
8
  export const PHASE_MANIFEST = 'Manifested'; // Creates DOM manifest (manifest.js)
8
9
  export const PHASE_OBSERVE = 'Observer'; // Starts MutationObserver (index.js)
9
- export const PHASE_COMPLETE = 'Cleanup'; // Removes [vibe] attribute (index.js)
10
+ export const PHASE_READY = 'Ready'; // Removes [vibe-fouc] attribute (or .vibe-fouc class) and dispatches vibe:ready event (cleanup.js)
10
11
 
11
12
  // REPEATED operations (run during init + can repeat during runtime)
12
13
  export const PHASE_PARSE = 'Parsed'; // Reads DOM structure (parse.js)
@@ -16,9 +17,12 @@ export const PHASE_CONDITION = 'Evaluated'; // Renders <!-- if --> blocks (condi
16
17
  export const PHASE_FETCH = 'Fetched'; // Loads <component> content (component.js)
17
18
  export const PHASE_UPDATE = 'Proxy'; // State changes trigger re-hydration (index.js)
18
19
  export const PHASE_MUTATE = 'Mutation'; // DOM mutations detected by observer (index.js)
20
+ export const PHASE_HYPERSPEED = 'Hyperspeed'; // Restores @[...] markers from pre-compiled manifest (hyperspeed.js)
19
21
 
20
22
  // Elements that should not have reactive bindings
21
- export const NON_REACTIVE_ELEMENTS = ['SCRIPT', 'HEAD', 'PRE', 'COMPONENT'];
23
+ // Note: COMPONENT is NOT in this list - inline component wrappers need to be parsed
24
+ // Fetched components (<component src="">) are handled separately by processComponent()
25
+ export const NON_REACTIVE_ELEMENTS = ['SCRIPT', 'HEAD', 'PRE'];
22
26
 
23
27
  // Attributes where the string value is meaningful (should NOT be removed when falsy)
24
28
  // All other attributes are treated as boolean-like (removed when falsy, present when truthy)
@@ -156,6 +160,46 @@ export const VALUE_ATTRS = [
156
160
  // Properties that should be set directly on the DOM element (not as attributes)
157
161
  export const DOM_PROPERTIES = ['value', 'checked', 'selected'];
158
162
 
163
+ // DOM element properties that should NOT be rewritten in event handlers
164
+ // When parsing onclick="this.value = this.inputValue", preserve this.value (DOM) but rewrite this.inputValue (component state)
165
+ export const DOM_ELEMENT_PROPERTIES = new Set([
166
+ 'value',
167
+ 'checked',
168
+ 'selected',
169
+ 'disabled',
170
+ 'readOnly',
171
+ 'files',
172
+ 'tagName',
173
+ 'nodeName',
174
+ 'nodeType',
175
+ 'classList',
176
+ 'className',
177
+ 'id',
178
+ 'innerHTML',
179
+ 'outerHTML',
180
+ 'textContent',
181
+ 'innerText',
182
+ 'parentElement',
183
+ 'parentNode',
184
+ 'nextSibling',
185
+ 'previousSibling',
186
+ 'firstChild',
187
+ 'lastChild',
188
+ 'children',
189
+ 'childNodes',
190
+ 'offsetWidth',
191
+ 'offsetHeight',
192
+ 'clientWidth',
193
+ 'clientHeight',
194
+ 'scrollTop',
195
+ 'scrollLeft',
196
+ 'scrollWidth',
197
+ 'scrollHeight',
198
+ 'style',
199
+ 'dataset',
200
+ 'attributes',
201
+ ]);
202
+
159
203
  // Regex for matching reactive bindings (@[expression])
160
204
  // Supports one level of nested brackets: @[items[0]] or @[obj[key]]
161
205
  export const BINDING_REGEX = /\@\[((?:[^\[\]]|\[[^\]]*\])+)\]/g;
package/runtime/debug.js CHANGED
@@ -14,6 +14,7 @@ const PHASE_COLORS = {
14
14
  Fetched: 'oklch(0.59 0.12 307)', // muted purple
15
15
  Mutation: 'oklch(0.60 0.11 240)', // muted blue
16
16
  Proxy: 'oklch(0.60 0.11 240)', // muted blue
17
+ Hyperspeed: 'oklch(0.60 0.11 180)', // muted cyan
17
18
  };
18
19
 
19
20
  // Special colors for styled segments (Tailwind 500)