@ape-egg/vibe 1.0.5 → 1.1.2

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 (35) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +228 -23
  3. package/compiler/bin/vibe-compile.js +109 -0
  4. package/compiler/native/.gitkeep +0 -0
  5. package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
  6. package/compiler/src/Cargo.lock +1885 -0
  7. package/compiler/src/Cargo.toml +29 -0
  8. package/compiler/src/compiler/compile.rs +1209 -0
  9. package/compiler/src/compiler/mod.rs +5 -0
  10. package/compiler/src/config.rs +184 -0
  11. package/compiler/src/main.rs +284 -0
  12. package/compiler/src/parser/element.rs +96 -0
  13. package/compiler/src/parser/html.rs +335 -0
  14. package/compiler/src/parser/mod.rs +8 -0
  15. package/index.js +2 -248
  16. package/package.json +26 -3
  17. package/{affected.js → runtime/affected.js} +64 -4
  18. package/runtime/cleanup.js +59 -0
  19. package/runtime/component.js +116 -0
  20. package/{conditionals.js → runtime/conditionals.js} +25 -11
  21. package/{constants.js → runtime/constants.js} +23 -3
  22. package/runtime/debug.js +91 -0
  23. package/{hydrate.js → runtime/hydrate.js} +57 -7
  24. package/runtime/index.js +614 -0
  25. package/{iterate.js → runtime/iterate.js} +53 -45
  26. package/{iteration-utils.js → runtime/iteration-utils.js} +11 -1
  27. package/{parse.js → runtime/parse.js} +37 -7
  28. package/runtime/state.js +52 -0
  29. package/ROADMAP.md +0 -289
  30. package/llms.txt +0 -279
  31. package/state.js +0 -26
  32. /package/{_vibe-compiled-iteration-batch.js → runtime/_vibe-compiled-iteration-batch.js} +0 -0
  33. /package/{link.js → runtime/manifest.js} +0 -0
  34. /package/{utils.js → runtime/utils.js} +0 -0
  35. /package/{vibe.css → runtime/vibe.css} +0 -0
@@ -9,7 +9,7 @@ const evaluateCondition = (expression, state) => !!evalInScope(expression, state
9
9
  // Helper function to check if a match references a specific key
10
10
  const matchesKey = (matchStr, key) => matchStr === key || matchStr.startsWith(key + '.');
11
11
 
12
- const recursive = (tree, state, newState, affected) => {
12
+ const recursive = (tree, state, newState, affected, scopedStateForHydration = null) => {
13
13
  // Handle iteration nodes specially
14
14
  if (tree.type === 'iteration') {
15
15
  const oldArray = resolvePath(state, tree.meta.arrayPath);
@@ -23,7 +23,22 @@ const recursive = (tree, state, newState, affected) => {
23
23
  node: tree,
24
24
  changeType: 'array',
25
25
  });
26
+ return affected;
27
+ }
28
+
29
+ // Array didn't change, but check for affected elements inside iteration instances
30
+ // (e.g., when tutorialProgress changes, need to update checkmarks in menu items)
31
+ if (tree.runtime.instances) {
32
+ for (const instance of tree.runtime.instances) {
33
+ if (instance.tree && instance.scopedState) {
34
+ // Use the instance's scoped state (includes item, index, etc.)
35
+ // Merge newState into scopedState to get updated global values
36
+ const mergedNewState = { ...instance.scopedState, ...newState };
37
+ recursive(instance.tree, instance.scopedState, mergedNewState, affected, mergedNewState);
38
+ }
39
+ }
26
40
  }
41
+
27
42
  return affected;
28
43
  }
29
44
 
@@ -44,7 +59,7 @@ const recursive = (tree, state, newState, affected) => {
44
59
 
45
60
  // Condition didn't change, check for affected elements inside active branch
46
61
  if (tree.runtime.activeInstance && tree.runtime.activeInstance.parsedTree) {
47
- return recursive(tree.runtime.activeInstance.parsedTree, state, newState, affected);
62
+ return recursive(tree.runtime.activeInstance.parsedTree, state, newState, affected, scopedStateForHydration);
48
63
  }
49
64
 
50
65
  return affected;
@@ -104,6 +119,7 @@ const recursive = (tree, state, newState, affected) => {
104
119
  input: m.input,
105
120
  matches: m.matches,
106
121
  element: tree.element,
122
+ scopedState: scopedStateForHydration, // Pass scoped state from iteration context
107
123
  });
108
124
  }
109
125
  }
@@ -148,6 +164,50 @@ const recursive = (tree, state, newState, affected) => {
148
164
  matchOuter: m.outer,
149
165
  matchInner: m.inner,
150
166
  element: tree.element,
167
+ scopedState: scopedStateForHydration, // Pass scoped state from iteration context
168
+ });
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ // Check name bindings (bindings in attribute names)
175
+ if (tree.nameBindings) {
176
+ const shallowState = Object.keys(state);
177
+ const shallowNewState = Object.keys(newState);
178
+ const isInitialHydration = state === newState;
179
+
180
+ for (const nameBinding of tree.nameBindings) {
181
+ BINDING_REGEX.lastIndex = 0;
182
+ const nameMatches = [];
183
+ let nameMatch;
184
+ while ((nameMatch = BINDING_REGEX.exec(nameBinding))) {
185
+ nameMatches.push({ outer: nameMatch[0], inner: nameMatch[1] });
186
+ }
187
+
188
+ for (const m of nameMatches) {
189
+ const noMatch = !shallowState.some((key) => matchesKey(m.inner, key));
190
+
191
+ let shouldAffect = false;
192
+
193
+ if (isInitialHydration) {
194
+ const newMatches = shallowNewState.filter((key) => matchesKey(m.inner, key));
195
+ shouldAffect = noMatch || newMatches.length > 0;
196
+ } else {
197
+ const changedKeys = shallowNewState.filter((key) =>
198
+ matchesKey(m.inner, key) && state[key] !== newState[key]
199
+ );
200
+ shouldAffect = noMatch || changedKeys.length > 0;
201
+ }
202
+
203
+ if (shouldAffect) {
204
+ affected.push({
205
+ type: 'nameBinding',
206
+ nameBinding,
207
+ matchOuter: m.outer,
208
+ matchInner: m.inner,
209
+ element: tree.element,
210
+ scopedState: scopedStateForHydration,
151
211
  });
152
212
  }
153
213
  }
@@ -160,7 +220,7 @@ const recursive = (tree, state, newState, affected) => {
160
220
  for (const key in children) {
161
221
  const child = children[key];
162
222
  if (child && typeof child === 'object') {
163
- recursive(child, state, newState, affected);
223
+ recursive(child, state, newState, affected, scopedStateForHydration);
164
224
  }
165
225
  }
166
226
  }
@@ -168,4 +228,4 @@ const recursive = (tree, state, newState, affected) => {
168
228
  return affected;
169
229
  };
170
230
 
171
- export default (tree, state, newState) => recursive(tree, state, newState, []);
231
+ export default (tree, state, newState) => recursive(tree, state, newState, [], null);
@@ -0,0 +1,59 @@
1
+ import { debugLog } from './debug.js';
2
+ import { PHASE_COMPLETE } from './constants.js';
3
+
4
+ /**
5
+ * Check if all Vibe processing is complete and cleanup can run
6
+ * @param {Element} rootElement - Root element to check
7
+ * @returns {Boolean} - true if cleanup should run
8
+ */
9
+ export const shouldCleanup = (rootElement) => {
10
+ // 1. Check for pending <component> elements
11
+ const componentElements = rootElement.querySelectorAll('component');
12
+ if (componentElements.length > 0) {
13
+ return false;
14
+ }
15
+
16
+ // 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;
29
+ }
30
+ return NodeFilter.FILTER_ACCEPT;
31
+ }
32
+ }
33
+ );
34
+
35
+ let node;
36
+ while ((node = walker.nextNode())) {
37
+ if (/@\[.+?\]/.test(node.textContent)) {
38
+ return false; // Found literal binding (not in dehydrated element)
39
+ }
40
+ }
41
+
42
+ // 3. All processing appears complete
43
+ return true;
44
+ };
45
+
46
+ /**
47
+ * Perform cleanup - remove vibe attribute to reveal content
48
+ * @param {Element} rootElement - Root element
49
+ * @param {String} attrName - Attribute name to remove (default: 'vibe')
50
+ * @param {Boolean} debug - Debug mode
51
+ */
52
+ export const cleanup = (rootElement, attrName = 'vibe', debug = false) => {
53
+ // Force reflow
54
+ rootElement.offsetHeight;
55
+
56
+ // Remove vibe attribute
57
+ debugLog(PHASE_COMPLETE, `removing [${attrName}] attribute`, debug);
58
+ rootElement.removeAttribute(attrName);
59
+ };
@@ -0,0 +1,116 @@
1
+ import { debugLog } from './debug.js';
2
+ import { PHASE_FETCH } from './constants.js';
3
+ import { evalInScope } from './utils.js';
4
+
5
+ // Helper to escape regex special characters
6
+ const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
+
8
+ // Track pending fetches to cancel them if element is removed
9
+ const pendingFetches = new WeakMap(); // element → AbortController
10
+
11
+ // Cancel a pending fetch for a component element
12
+ export const abortComponentFetch = (element) => {
13
+ const controller = pendingFetches.get(element);
14
+ if (controller) {
15
+ controller.abort();
16
+ pendingFetches.delete(element);
17
+ }
18
+ };
19
+
20
+ export const processComponent = (rootElement, onComplete, config = {}) => {
21
+ const debug = !!config?.debug;
22
+ const componentElements = rootElement.querySelectorAll('component');
23
+
24
+ if (componentElements.length === 0) {
25
+ if (onComplete) onComplete();
26
+ return;
27
+ }
28
+
29
+ // Process just the first element - MutationObserver will trigger next call
30
+ const el = componentElements[0];
31
+ const src = el.getAttribute('src');
32
+
33
+ if (!src) {
34
+ el.remove();
35
+ // Don't recursively call - let MutationObserver handle it
36
+ return;
37
+ }
38
+
39
+ // Capture children and props before fetching
40
+ const children = el.innerHTML.trim();
41
+ const props = {};
42
+ Array.from(el.attributes).forEach((attr) => {
43
+ if (attr.name !== 'src') {
44
+ props[attr.name] = attr.value;
45
+ }
46
+ });
47
+
48
+ // Create AbortController to cancel fetch if element is removed
49
+ const controller = new AbortController();
50
+ pendingFetches.set(el, controller);
51
+
52
+ fetch(src, { signal: controller.signal })
53
+ .then((r) => r.text())
54
+ .then((html) => {
55
+ // Transform the fetched HTML
56
+ let transformedHtml = html;
57
+
58
+ // Replace props
59
+ Object.entries(props).forEach(([propName, propValue]) => {
60
+ const bindingMatch = propValue.match(/^@\[(.+)\]$/);
61
+
62
+ if (bindingMatch) {
63
+ // Reactive prop: replace propName as word boundary
64
+ const path = bindingMatch[1];
65
+ const propPattern = new RegExp(`\\b${escapeRegex(propName)}\\b`, 'g');
66
+ transformedHtml = transformedHtml.replace(propPattern, path);
67
+ } else {
68
+ // Static prop: replace @[propName] with literal value
69
+ const propPattern = new RegExp(`@\\[${escapeRegex(propName)}\\]`, 'g');
70
+ transformedHtml = transformedHtml.replace(propPattern, propValue);
71
+ }
72
+ });
73
+
74
+ // Replace <slot></slot> with children
75
+ if (children) {
76
+ transformedHtml = transformedHtml.replace(/<slot><\/slot>/g, children);
77
+ transformedHtml = transformedHtml.replace(/<slot\s*\/>/g, children);
78
+ }
79
+
80
+ // Clean up pending fetch tracker
81
+ pendingFetches.delete(el);
82
+
83
+ // Set outerHTML - this triggers MutationObserver
84
+ // Check if element still has a parent (might have been removed during fetch)
85
+ if (el.parentNode) {
86
+ el.outerHTML = transformedHtml;
87
+ debugLog(PHASE_FETCH, src, debug);
88
+
89
+ // Force immediate processing of the mutation (MutationObserver is async, but we need sync)
90
+ // Use microtask to process right after outerHTML completes
91
+ if (config._forceSync && config._processMutations && config._observer) {
92
+ Promise.resolve().then(() => {
93
+ const pending = config._observer.takeRecords();
94
+ if (pending.length > 0) {
95
+ config._processMutations(pending);
96
+ }
97
+ });
98
+ }
99
+ }
100
+ })
101
+ .catch((error) => {
102
+ // Clean up pending fetch tracker
103
+ pendingFetches.delete(el);
104
+
105
+ // If fetch was aborted (element removed), silently skip
106
+ if (error.name === 'AbortError') {
107
+ return;
108
+ }
109
+
110
+ console.error('[vibe] Failed to load:', src, error);
111
+ if (el.parentNode) {
112
+ el.remove();
113
+ }
114
+ // Don't recursively call - let MutationObserver handle it
115
+ });
116
+ };
@@ -25,11 +25,13 @@ export const extractDependencies = (expression) => {
25
25
  };
26
26
 
27
27
  // Render all conditionals in the parsed tree
28
- export const renderAllConditionals = (tree, state, linkList, parentScope = {}) => {
28
+ export const renderAllConditionals = (tree, state, manifest, parentScope = {}) => {
29
+ let count = 0;
30
+
29
31
  // If this is a conditional node, render it
30
32
  if (tree.type === 'conditional') {
31
- renderConditional(tree, state, linkList, parentScope);
32
- return;
33
+ renderConditional(tree, state, manifest, parentScope);
34
+ return 1;
33
35
  }
34
36
 
35
37
  // Recursively render conditionals in child nodes
@@ -37,16 +39,24 @@ export const renderAllConditionals = (tree, state, linkList, parentScope = {}) =
37
39
  Object.keys(tree.children).forEach((key) => {
38
40
  const child = tree.children[key];
39
41
  if (typeof child === 'object' && child !== null) {
40
- renderAllConditionals(child, state, linkList, parentScope);
42
+ count += renderAllConditionals(child, state, manifest, parentScope);
41
43
  }
42
44
  });
43
45
  }
46
+
47
+ return count;
44
48
  };
45
49
 
46
50
  // Initial render of a conditional block
47
- export const renderConditional = (node, state, linkList, parentScope = {}) => {
51
+ export const renderConditional = (node, state, manifest, parentScope = {}) => {
48
52
  const { expression, startComment, endComment, branches } = node.meta;
49
53
 
54
+ // Check if already rendered (using marker on comment node)
55
+ // @ts-ignore - adding custom property to comment node
56
+ if (startComment.__vibeRendered) {
57
+ return;
58
+ }
59
+
50
60
  // Remove original template nodes from DOM (between start and end comments)
51
61
  // Only do this on first render (when activeBranch is undefined)
52
62
  if (node.runtime.activeBranch === undefined) {
@@ -63,6 +73,10 @@ export const renderConditional = (node, state, linkList, parentScope = {}) => {
63
73
  node.runtime.templateRemoved = true;
64
74
  }
65
75
 
76
+ // Mark comment as rendered (survives re-parsing)
77
+ // @ts-ignore - adding custom property to comment node
78
+ startComment.__vibeRendered = true;
79
+
66
80
  // Evaluate condition with current state
67
81
  const conditionResult = evaluateCondition(expression, state);
68
82
 
@@ -70,14 +84,14 @@ export const renderConditional = (node, state, linkList, parentScope = {}) => {
70
84
  const branchToMount = conditionResult ? branches.if : branches.else;
71
85
 
72
86
  // Mount the appropriate branch
73
- mountBranch(node, branchToMount, state, linkList, parentScope);
87
+ mountBranch(node, branchToMount, state, manifest, parentScope);
74
88
 
75
89
  // Store active branch reference
76
90
  node.runtime.activeBranch = branchToMount;
77
91
  };
78
92
 
79
93
  // Mount a specific branch
80
- const mountBranch = (node, branchData, state, linkList, parentScope) => {
94
+ const mountBranch = (node, branchData, state, manifest, parentScope) => {
81
95
  const { startComment, endComment } = node.meta;
82
96
 
83
97
  // If branch doesn't exist (no else clause), just unmount current
@@ -109,8 +123,8 @@ const mountBranch = (node, branchData, state, linkList, parentScope) => {
109
123
 
110
124
  // Recursively render any nested iterations and conditionals
111
125
  if (branchTree) {
112
- renderAllIterations(branchTree, scopedState, linkList);
113
- renderAllConditionals(branchTree, scopedState, linkList, parentScope);
126
+ renderAllIterations(branchTree, scopedState, manifest, parentScope);
127
+ renderAllConditionals(branchTree, scopedState, manifest, parentScope);
114
128
  }
115
129
 
116
130
  // Store active instance
@@ -139,7 +153,7 @@ const unmountBranch = (node) => {
139
153
  };
140
154
 
141
155
  // Update conditional when dependencies change
142
- export const updateConditional = (node, newState, oldState, linkList, parentScope = {}) => {
156
+ export const updateConditional = (node, newState, oldState, manifest, parentScope = {}) => {
143
157
  const { expression, branches } = node.meta;
144
158
 
145
159
  // If not yet rendered, skip (renderConditional handles initial render)
@@ -156,7 +170,7 @@ export const updateConditional = (node, newState, oldState, linkList, parentScop
156
170
 
157
171
  if (branchChanged) {
158
172
  // Switch branches
159
- mountBranch(node, newBranchData, newState, linkList, parentScope);
173
+ mountBranch(node, newBranchData, newState, manifest, parentScope);
160
174
  node.runtime.activeBranch = newBranchData;
161
175
  } else {
162
176
  // Same branch, but state might have changed - rehydrate
@@ -1,5 +1,24 @@
1
+ // Debug logger name
2
+ export const DEBUGGER_NAME = '[vibe-debug]:';
3
+
4
+ // Lifecycle phase names for debug logging
5
+ // ONE-OFF operations (run once during initialization)
6
+ export const PHASE_ATTACH = 'Attached'; // Latches onto DOM element (index.js)
7
+ export const PHASE_MANIFEST = 'Manifested'; // Creates DOM manifest (manifest.js)
8
+ export const PHASE_OBSERVE = 'Observer'; // Starts MutationObserver (index.js)
9
+ export const PHASE_COMPLETE = 'Cleanup'; // Removes [vibe] attribute (index.js)
10
+
11
+ // REPEATED operations (run during init + can repeat during runtime)
12
+ export const PHASE_PARSE = 'Parsed'; // Reads DOM structure (parse.js)
13
+ export const PHASE_HYDRATE = 'Hydrated'; // Replaces @[...] with values (hydrate.js)
14
+ export const PHASE_ITERATE = 'Iterated'; // Renders <!-- each --> blocks (iterate.js)
15
+ export const PHASE_CONDITION = 'Evaluated'; // Renders <!-- if --> blocks (conditionals.js)
16
+ export const PHASE_FETCH = 'Fetched'; // Loads <component> content (component.js)
17
+ export const PHASE_UPDATE = 'Proxy'; // State changes trigger re-hydration (index.js)
18
+ export const PHASE_MUTATE = 'Mutation'; // DOM mutations detected by observer (index.js)
19
+
1
20
  // Elements that should not have reactive bindings
2
- export const NON_REACTIVE_ELEMENTS = ['SCRIPT', 'HEAD', 'PRE'];
21
+ export const NON_REACTIVE_ELEMENTS = ['SCRIPT', 'HEAD', 'PRE', 'COMPONENT'];
3
22
 
4
23
  // Attributes where the string value is meaningful (should NOT be removed when falsy)
5
24
  // All other attributes are treated as boolean-like (removed when falsy, present when truthy)
@@ -138,10 +157,11 @@ export const VALUE_ATTRS = [
138
157
  export const DOM_PROPERTIES = ['value', 'checked', 'selected'];
139
158
 
140
159
  // Regex for matching reactive bindings (@[expression])
141
- export const BINDING_REGEX = /\@\[([^\]]+)\]/g;
160
+ // Supports one level of nested brackets: @[items[0]] or @[obj[key]]
161
+ export const BINDING_REGEX = /\@\[((?:[^\[\]]|\[[^\]]*\])+)\]/g;
142
162
 
143
163
  // Regex for detecting a pure binding (entire value is just @[expression])
144
- export const PURE_BINDING_REGEX = /^\@\[([^\]]+)\]$/;
164
+ export const PURE_BINDING_REGEX = /^\@\[((?:[^\[\]]|\[[^\]]*\])+)\]$/;
145
165
 
146
166
  // Regex for parsing iteration comment syntax (<!-- each items as item, index -->)
147
167
  // Supports nested paths like category.items
@@ -0,0 +1,91 @@
1
+ import { DEBUGGER_NAME } from './constants.js';
2
+
3
+ // Phase colors - synced with index.css debug phase colors
4
+ // Muted palette based on comment green (#6a9955)
5
+ const PHASE_COLORS = {
6
+ Attached: 'oklch(0.55 0.02 250)', // slate (minimal saturation)
7
+ Manifested: 'oklch(0.55 0.02 250)', // slate (minimal saturation)
8
+ Observer: 'oklch(0.60 0.11 240)', // muted blue
9
+ Cleanup: 'oklch(0.55 0.02 250)', // slate (minimal saturation)
10
+ Parsed: 'oklch(0.62 0.12 50)', // muted orange
11
+ Hydrated: 'oklch(0.60 0.12 340)', // muted pink
12
+ Iterated: 'oklch(0.58 0.11 142)', // comment green (baseline)
13
+ Evaluated: 'oklch(0.58 0.11 142)', // comment green (baseline)
14
+ Fetched: 'oklch(0.59 0.12 307)', // muted purple
15
+ Mutation: 'oklch(0.60 0.11 240)', // muted blue
16
+ Proxy: 'oklch(0.60 0.11 240)', // muted blue
17
+ };
18
+
19
+ // Special colors for styled segments (Tailwind 500)
20
+ const COLORS = {
21
+ green: 'oklch(0.769 0.227 141.41)', // green-500
22
+ red: 'oklch(0.637 0.237 27.33)', // red-500
23
+ yellow: 'oklch(0.809 0.177 94.36)', // yellow-500
24
+ pink: 'oklch(0.649 0.237 346.06)', // pink-500
25
+ slate: 'oklch(0.556 0.016 256.85)', // slate-500
26
+ commentGreen: 'oklch(0.58 0.11 142)', // muted green (same as Iterated/Evaluated)
27
+ };
28
+
29
+ /**
30
+ * Debug logging helper for Vibe
31
+ * Only logs when debug mode is enabled
32
+ * @param {string} phase - The lifecycle phase (Attach, Parse, Hydrate, etc.)
33
+ * @param {string|Array} message - The message to log (string or array of {text, colored: boolean, color: string})
34
+ * @param {boolean} debug - Whether debug mode is enabled
35
+ * @param {number} indent - Indentation level (0 = no indent, 1+ = nested operations)
36
+ * @param {HTMLElement} element - Optional DOM element to log (becomes clickable in console)
37
+ */
38
+ export const debugLog = (phase, message, debug = false, indent = 0, element = null) => {
39
+ if (!debug) return;
40
+
41
+ const phaseBracket = `[${phase}] `.padEnd(13, ' '); // Pad to 13 chars (longest is "[Manifested] ")
42
+ const indentStr = indent > 0 ? ' '.repeat(indent) + '├─ ' : '';
43
+ const phaseColor = PHASE_COLORS[phase] || 'oklch(0.55 0.02 250)';
44
+
45
+ // Handle array of styled segments
46
+ if (Array.isArray(message)) {
47
+ let formatStr = `%c${phaseBracket}%c${DEBUGGER_NAME}%c ${indentStr}`;
48
+ const styles = [
49
+ `color: ${phaseColor}; font-weight: bold`,
50
+ 'color: oklch(0.70 0.01 250)',
51
+ 'color: inherit',
52
+ ];
53
+
54
+ message.forEach((segment) => {
55
+ formatStr += '%c' + segment.text;
56
+ // Support both 'colored' (uses phase color) and 'color' (uses specific color)
57
+ if (segment.color) {
58
+ styles.push(`color: ${COLORS[segment.color] || segment.color}; font-weight: bold`);
59
+ } else if (segment.colored) {
60
+ styles.push(`color: ${phaseColor}; font-weight: bold`);
61
+ } else {
62
+ styles.push('color: inherit');
63
+ }
64
+ });
65
+
66
+ // Add element if provided (makes it clickable in console)
67
+ if (element) {
68
+ console.info(formatStr, ...styles, element);
69
+ } else {
70
+ console.info(formatStr, ...styles);
71
+ }
72
+ } else {
73
+ // Simple string message
74
+ if (element) {
75
+ console.info(
76
+ `%c${phaseBracket}%c${DEBUGGER_NAME}%c ${indentStr}${message}`,
77
+ `color: ${phaseColor}; font-weight: bold`,
78
+ 'color: oklch(0.70 0.01 250)', // light gray
79
+ 'color: inherit',
80
+ element,
81
+ );
82
+ } else {
83
+ console.info(
84
+ `%c${phaseBracket}%c${DEBUGGER_NAME}%c ${indentStr}${message}`,
85
+ `color: ${phaseColor}; font-weight: bold`,
86
+ 'color: oklch(0.70 0.01 250)', // light gray
87
+ 'color: inherit',
88
+ );
89
+ }
90
+ }
91
+ };
@@ -10,17 +10,67 @@ export const setPreviousState = (state) => {
10
10
  previousState = { ...state };
11
11
  };
12
12
 
13
- export default (affected, state, linkList = {}) => {
13
+ export default (affected, state, manifest = {}) => {
14
14
  affected.forEach((aff) => {
15
+ // Use scoped state if provided (from iteration instances)
16
+ const effectiveState = aff.scopedState || state;
17
+
15
18
  // Handle iteration updates
16
19
  if (aff.type === 'iteration') {
17
- updateIteration(aff.node, state, previousState, linkList);
20
+ updateIteration(aff.node, state, previousState, manifest);
18
21
  return;
19
22
  }
20
23
 
21
24
  // Handle conditional updates
22
25
  if (aff.type === 'conditional') {
23
- updateConditional(aff.node, state, previousState, linkList);
26
+ updateConditional(aff.node, state, previousState, manifest);
27
+ return;
28
+ }
29
+
30
+ // Handle name bindings (e.g., <icon @[section.icon]>)
31
+ if (aff.type === 'nameBinding') {
32
+ const { nameBinding, matchInner, element } = aff;
33
+ try {
34
+ // HTML lowercases attribute names, so we need case-insensitive lookup
35
+ // Try exact match first, then try finding a case-insensitive match
36
+ let attrName = evalInScope(matchInner, effectiveState);
37
+
38
+ // If exact match failed and expression is a simple property (no dots/brackets)
39
+ if (!attrName && !matchInner.includes('.') && !matchInner.includes('[')) {
40
+ // Find the property with case-insensitive match
41
+ const keys = Object.keys(effectiveState);
42
+ const matchingKey = keys.find(k => k.toLowerCase() === matchInner.toLowerCase());
43
+ if (matchingKey) {
44
+ attrName = effectiveState[matchingKey];
45
+ }
46
+ }
47
+
48
+ // Track multiple name bindings per element (need a map of binding -> evaluated attr)
49
+ if (!element._vibeNameBindings) {
50
+ element._vibeNameBindings = new Map();
51
+ }
52
+
53
+ // Remove the old evaluated attribute for this specific binding
54
+ const oldAttrName = element._vibeNameBindings.get(nameBinding);
55
+ if (oldAttrName) {
56
+ element.removeAttribute(oldAttrName);
57
+ }
58
+
59
+ // Remove the binding attribute itself
60
+ if (element.hasAttribute(nameBinding)) {
61
+ element.removeAttribute(nameBinding);
62
+ }
63
+
64
+ // Set the new attribute (empty value for boolean-like attributes)
65
+ if (attrName) {
66
+ element.setAttribute(attrName, '');
67
+ element._vibeNameBindings.set(nameBinding, attrName);
68
+ } else {
69
+ element._vibeNameBindings.delete(nameBinding);
70
+ }
71
+ } catch (e) {
72
+ console.error('Error hydrating name binding:', e);
73
+ }
24
74
  return;
25
75
  }
26
76
 
@@ -41,12 +91,12 @@ export default (affected, state, linkList = {}) => {
41
91
  if (isDomProperty && isPureBinding) {
42
92
  // For DOM properties like value, set the property directly
43
93
  const expr = isPureBinding[1];
44
- const value = evalInScope(expr, state);
94
+ const value = evalInScope(expr, effectiveState);
45
95
  element[attrName] = value;
46
96
  } else if (!isValueAttr && isPureBinding) {
47
97
  // Boolean-like attributes: add or remove based on truthiness
48
98
  const expr = isPureBinding[1];
49
- const value = evalInScope(expr, state);
99
+ const value = evalInScope(expr, effectiveState);
50
100
  if (value) {
51
101
  element.setAttribute(attrName, '');
52
102
  } else {
@@ -55,7 +105,7 @@ export default (affected, state, linkList = {}) => {
55
105
  } else {
56
106
  // Value attribute - replace bindings with values
57
107
  const newValue = attrValue.replace(BINDING_REGEX, (_, expr) => {
58
- return evalInScope(expr, state);
108
+ return evalInScope(expr, effectiveState);
59
109
  });
60
110
  element.setAttribute(attrName, newValue);
61
111
  }
@@ -69,7 +119,7 @@ export default (affected, state, linkList = {}) => {
69
119
  // This prevents undefined store properties to throw an error
70
120
  try {
71
121
  // Evaluate the expression with state as context
72
- const evaluated = evalInScope(matchInner, state);
122
+ const evaluated = evalInScope(matchInner, effectiveState);
73
123
 
74
124
  const toReplace = input.replaceAll(matchOuter, evaluated).trim();
75
125