@servlyadmin/runtime-core 0.1.46 → 0.2.1

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 CHANGED
@@ -45,6 +45,34 @@ const app = await mount({
45
45
  });
46
46
  ```
47
47
 
48
+ ### With Loading & Error States
49
+
50
+ ```typescript
51
+ const app = await mount({
52
+ componentId: 'my-component-id',
53
+ target: '#app',
54
+ props: { title: 'Hello' },
55
+
56
+ // Show loading indicator
57
+ loadingComponent: '<div class="skeleton animate-pulse h-32 bg-gray-200 rounded"></div>',
58
+
59
+ // Show error message on failure
60
+ errorComponent: (err) => `<div class="text-red-500">Failed to load: ${err.message}</div>`,
61
+
62
+ // Optional: delay before showing loading (avoids flash for fast loads)
63
+ loadingDelay: 200,
64
+
65
+ // Optional: minimum time to show loading (avoids flash)
66
+ minLoadingTime: 500,
67
+
68
+ // Lifecycle callbacks
69
+ onLoadStart: () => console.log('Loading...'),
70
+ onLoadEnd: () => console.log('Done!'),
71
+ onReady: (result) => console.log('Mounted!'),
72
+ onError: (err) => console.error('Failed:', err)
73
+ });
74
+ ```
75
+
48
76
  ### With Version & Cache Control
49
77
 
50
78
  ```typescript
@@ -133,6 +161,36 @@ const { data } = await fetchComponent('my-component', {
133
161
  });
134
162
  ```
135
163
 
164
+ ## Prefetching
165
+
166
+ Preload components for faster subsequent rendering:
167
+
168
+ ```typescript
169
+ import { prefetch, prefetchAll, prefetchOnHover, prefetchOnVisible, prefetchOnIdle } from '@servlyadmin/runtime-core';
170
+
171
+ // Prefetch a single component
172
+ await prefetch('pricing-card');
173
+
174
+ // Prefetch multiple components
175
+ await prefetchAll(['pricing-card', 'contact-form', 'navbar']);
176
+
177
+ // Prefetch with versions
178
+ await prefetchAll([
179
+ { id: 'pricing-card', version: '^1.0.0' },
180
+ { id: 'contact-form', version: 'latest' }
181
+ ]);
182
+
183
+ // Prefetch on hover (great for modals/dialogs)
184
+ const cleanup = prefetchOnHover('#open-modal-btn', 'modal-component');
185
+ // Later: cleanup() to remove listener
186
+
187
+ // Prefetch when element becomes visible (Intersection Observer)
188
+ const cleanup = prefetchOnVisible('#pricing-section', ['pricing-card', 'feature-list']);
189
+
190
+ // Prefetch when browser is idle (non-critical components)
191
+ prefetchOnIdle(['footer', 'sidebar', 'help-modal']);
192
+ ```
193
+
136
194
  ## Core Concepts
137
195
 
138
196
  ### Layout Elements
@@ -204,6 +262,16 @@ const app = await mount({
204
262
  apiKey?: string,
205
263
  retryConfig?: RetryConfig,
206
264
  },
265
+
266
+ // Loading & Error States
267
+ loadingComponent?: string | HTMLElement, // HTML to show while loading
268
+ errorComponent?: (err: Error) => string | HTMLElement, // Error display
269
+ loadingDelay?: number, // Delay before showing loading (ms)
270
+ minLoadingTime?: number, // Minimum loading display time (ms)
271
+
272
+ // Lifecycle Callbacks
273
+ onLoadStart?: () => void, // Called when loading starts
274
+ onLoadEnd?: () => void, // Called when loading ends
207
275
  onReady?: (result: MountResult) => void,
208
276
  onError?: (error: Error) => void,
209
277
  });
@@ -383,6 +451,53 @@ hasTemplateSyntax('{{props.name}}'); // true
383
451
  hasTemplateSyntax('static text'); // false
384
452
  ```
385
453
 
454
+ ### Prefetch API
455
+
456
+ Preload components for faster subsequent rendering.
457
+
458
+ ```typescript
459
+ import {
460
+ prefetch,
461
+ prefetchAll,
462
+ prefetchOnHover,
463
+ prefetchOnVisible,
464
+ prefetchOnIdle
465
+ } from '@servlyadmin/runtime-core';
466
+
467
+ // Prefetch single component
468
+ await prefetch('pricing-card');
469
+ await prefetch('pricing-card', '^1.0.0'); // With version
470
+
471
+ // Prefetch multiple components
472
+ const { success, failed } = await prefetchAll([
473
+ 'pricing-card',
474
+ 'contact-form',
475
+ { id: 'navbar', version: '^2.0.0' }
476
+ ]);
477
+
478
+ // Prefetch on hover (returns cleanup function)
479
+ const cleanup = prefetchOnHover(
480
+ '#open-modal-btn', // Target element
481
+ 'modal-component', // Component ID
482
+ 'latest', // Version
483
+ { delay: 100 } // Options: delay before prefetch
484
+ );
485
+ // Later: cleanup()
486
+
487
+ // Prefetch when element becomes visible
488
+ const cleanup = prefetchOnVisible(
489
+ '#pricing-section', // Target element
490
+ ['pricing-card', 'feature-list'], // Components to prefetch
491
+ { rootMargin: '100px', threshold: 0 } // IntersectionObserver options
492
+ );
493
+
494
+ // Prefetch during browser idle time
495
+ prefetchOnIdle(
496
+ ['footer', 'sidebar', 'help-modal'],
497
+ { timeout: 5000 } // Max wait time before forcing prefetch
498
+ );
499
+ ```
500
+
386
501
  ## Slots
387
502
 
388
503
  Components can define slots for content injection:
package/dist/index.cjs CHANGED
@@ -720,6 +720,7 @@ __export(index_exports, {
720
720
  extractDependenciesFromCode: () => extractDependenciesFromCode,
721
721
  extractOverrideDependencies: () => extractOverrideDependencies,
722
722
  extractReferencedViewIds: () => extractReferencedViewIds,
723
+ extractSlotBindings: () => extractSlotBindings,
723
724
  fetchComponent: () => fetchComponent,
724
725
  fetchComponentWithDependencies: () => fetchComponentWithDependencies,
725
726
  formatStyleValue: () => formatStyleValue,
@@ -777,7 +778,12 @@ __export(index_exports, {
777
778
  mountData: () => mountData,
778
779
  navigateTo: () => navigateTo,
779
780
  parseVersion: () => parseVersion,
781
+ prefetch: () => prefetch,
782
+ prefetchAll: () => prefetchAll,
780
783
  prefetchComponents: () => prefetchComponents,
784
+ prefetchOnHover: () => prefetchOnHover,
785
+ prefetchOnIdle: () => prefetchOnIdle,
786
+ prefetchOnVisible: () => prefetchOnVisible,
781
787
  preloadIcons: () => preloadIcons,
782
788
  preloadTailwind: () => preloadTailwind,
783
789
  preventFOUC: () => preventFOUC,
@@ -1519,8 +1525,96 @@ function resolveBindingPath(path, context) {
1519
1525
  }
1520
1526
  return navigatePath(source, parts.slice(startIndex));
1521
1527
  }
1528
+ var FUNCTION_CALL_REGEX = /^(\w+)\s*\((.*)\)$/s;
1529
+ function executeGlobalFunction(funcName, argsStr, context) {
1530
+ if (context.functionMap?.[funcName]) {
1531
+ try {
1532
+ const args = parseFunctionArguments(argsStr, context);
1533
+ return context.functionMap[funcName](...args);
1534
+ } catch (error) {
1535
+ console.error(`[GlobalFunction] Error executing ${funcName}:`, error);
1536
+ return void 0;
1537
+ }
1538
+ }
1539
+ const func = context.globalFunctions?.find((f) => f.name === funcName);
1540
+ if (!func) {
1541
+ console.warn(`[GlobalFunction] Function not found: ${funcName}`);
1542
+ return void 0;
1543
+ }
1544
+ try {
1545
+ const args = parseFunctionArguments(argsStr, context);
1546
+ const paramNames = func.parameters.map((p) => p.name);
1547
+ const fnBody = `
1548
+ const state = __ctx__.state || {};
1549
+ const props = __ctx__.props || {};
1550
+ const config = __ctx__.configs || __ctx__.context || {};
1551
+ ${func.code}
1552
+ `;
1553
+ const fn = new Function("__ctx__", ...paramNames, fnBody);
1554
+ return fn(context, ...args);
1555
+ } catch (error) {
1556
+ console.error(`[GlobalFunction] Error executing ${funcName}:`, error);
1557
+ return void 0;
1558
+ }
1559
+ }
1560
+ function parseFunctionArguments(argsStr, context) {
1561
+ if (!argsStr.trim()) return [];
1562
+ const args = [];
1563
+ let current = "";
1564
+ let depth = 0;
1565
+ let inString = false;
1566
+ let stringChar = "";
1567
+ for (let i = 0; i < argsStr.length; i++) {
1568
+ const char = argsStr[i];
1569
+ const prevChar = argsStr[i - 1];
1570
+ if ((char === '"' || char === "'") && prevChar !== "\\") {
1571
+ if (!inString) {
1572
+ inString = true;
1573
+ stringChar = char;
1574
+ } else if (char === stringChar) {
1575
+ inString = false;
1576
+ }
1577
+ }
1578
+ if (!inString) {
1579
+ if (char === "(" || char === "[" || char === "{") depth++;
1580
+ if (char === ")" || char === "]" || char === "}") depth--;
1581
+ }
1582
+ if (char === "," && depth === 0 && !inString) {
1583
+ args.push(parseArgumentValue(current.trim(), context));
1584
+ current = "";
1585
+ } else {
1586
+ current += char;
1587
+ }
1588
+ }
1589
+ if (current.trim()) {
1590
+ args.push(parseArgumentValue(current.trim(), context));
1591
+ }
1592
+ return args;
1593
+ }
1594
+ function parseArgumentValue(value, context) {
1595
+ const trimmed = value.trim();
1596
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
1597
+ return trimmed.slice(1, -1);
1598
+ }
1599
+ if (!isNaN(Number(trimmed)) && trimmed !== "") {
1600
+ return Number(trimmed);
1601
+ }
1602
+ if (trimmed === "true") return true;
1603
+ if (trimmed === "false") return false;
1604
+ if (trimmed === "null") return null;
1605
+ if (trimmed === "undefined") return void 0;
1606
+ if (trimmed.startsWith("{{") && trimmed.endsWith("}}")) {
1607
+ return resolveTemplateValue(trimmed, context);
1608
+ }
1609
+ return resolveBindingPath(trimmed, context);
1610
+ }
1522
1611
  function resolveExpression(expression, context) {
1523
1612
  const trimmed = expression.trim();
1613
+ const funcMatch = trimmed.match(FUNCTION_CALL_REGEX);
1614
+ if (funcMatch) {
1615
+ const [, funcName, argsStr] = funcMatch;
1616
+ return executeGlobalFunction(funcName, argsStr, context);
1617
+ }
1524
1618
  const comparisonOperators = ["===", "!==", "==", "!=", ">=", "<=", ">", "<"];
1525
1619
  for (const op of comparisonOperators) {
1526
1620
  if (trimmed.includes(op)) {
@@ -2437,6 +2531,203 @@ function getUrlInfo() {
2437
2531
  }
2438
2532
 
2439
2533
  // src/eventSystem.ts
2534
+ function createShortcuts(ctx) {
2535
+ return {
2536
+ /**
2537
+ * Render a dynamic list of items using a blueprint view
2538
+ * This is a simplified version for runtime - full implementation requires DOM access
2539
+ */
2540
+ renderDynamicList: (options) => {
2541
+ const { blueprint, targetContainer, data = [], clearExisting = true, itemProps = {} } = options;
2542
+ const container = typeof document !== "undefined" ? document.querySelector(targetContainer) || document.querySelector(`[data-servly-id="${targetContainer.replace("#", "")}"]`) : null;
2543
+ if (!container) {
2544
+ console.warn(`[shortcuts.renderDynamicList] Container not found: ${targetContainer}`);
2545
+ return { success: false, error: "Container not found" };
2546
+ }
2547
+ if (clearExisting) {
2548
+ container.innerHTML = "";
2549
+ }
2550
+ const event = new CustomEvent("servly:renderDynamicList", {
2551
+ bubbles: true,
2552
+ detail: {
2553
+ blueprint,
2554
+ targetContainer,
2555
+ data,
2556
+ itemProps,
2557
+ container
2558
+ }
2559
+ });
2560
+ container.dispatchEvent(event);
2561
+ return { success: true, itemCount: data.length };
2562
+ },
2563
+ /**
2564
+ * Quick list rendering helper
2565
+ */
2566
+ renderList: (blueprint, targetContainer, data, options = {}) => {
2567
+ return createShortcuts(ctx).renderDynamicList({
2568
+ blueprint,
2569
+ targetContainer,
2570
+ data,
2571
+ ...options
2572
+ });
2573
+ },
2574
+ /**
2575
+ * Set state value
2576
+ */
2577
+ setState: (key, value) => {
2578
+ if (ctx.stateManager) {
2579
+ ctx.stateManager.set(key, value, ctx.elementId);
2580
+ return { success: true };
2581
+ }
2582
+ return { success: false, error: "No state manager" };
2583
+ },
2584
+ /**
2585
+ * Get state value
2586
+ */
2587
+ getState: (key) => {
2588
+ if (ctx.stateManager) {
2589
+ return ctx.stateManager.get(key);
2590
+ }
2591
+ return void 0;
2592
+ },
2593
+ /**
2594
+ * Toggle state value
2595
+ */
2596
+ toggleState: (key) => {
2597
+ if (ctx.stateManager) {
2598
+ ctx.stateManager.toggle(key, ctx.elementId);
2599
+ return { success: true };
2600
+ }
2601
+ return { success: false, error: "No state manager" };
2602
+ },
2603
+ /**
2604
+ * Navigate to URL
2605
+ */
2606
+ navigateTo: (url, options) => {
2607
+ navigateTo(url, options);
2608
+ return { success: true };
2609
+ },
2610
+ /**
2611
+ * Log to console (for debugging)
2612
+ */
2613
+ log: (...args) => {
2614
+ console.log("[Servly]", ...args);
2615
+ },
2616
+ /**
2617
+ * Show alert
2618
+ */
2619
+ alert: (message) => {
2620
+ if (typeof alert !== "undefined") {
2621
+ alert(message);
2622
+ }
2623
+ },
2624
+ /**
2625
+ * Get element by ID
2626
+ */
2627
+ getElement: (id) => {
2628
+ if (typeof document === "undefined") return null;
2629
+ return document.querySelector(`[data-servly-id="${id}"]`) || document.getElementById(id);
2630
+ },
2631
+ /**
2632
+ * Set element text content
2633
+ */
2634
+ setText: (selector, text) => {
2635
+ if (typeof document === "undefined") return { success: false };
2636
+ const el = document.querySelector(selector) || document.querySelector(`[data-servly-id="${selector.replace("#", "")}"]`);
2637
+ if (el) {
2638
+ el.textContent = text;
2639
+ return { success: true };
2640
+ }
2641
+ return { success: false, error: "Element not found" };
2642
+ },
2643
+ /**
2644
+ * Add class to element
2645
+ */
2646
+ addClass: (selector, className) => {
2647
+ if (typeof document === "undefined") return { success: false };
2648
+ const el = document.querySelector(selector);
2649
+ if (el) {
2650
+ el.classList.add(className);
2651
+ return { success: true };
2652
+ }
2653
+ return { success: false, error: "Element not found" };
2654
+ },
2655
+ /**
2656
+ * Remove class from element
2657
+ */
2658
+ removeClass: (selector, className) => {
2659
+ if (typeof document === "undefined") return { success: false };
2660
+ const el = document.querySelector(selector);
2661
+ if (el) {
2662
+ el.classList.remove(className);
2663
+ return { success: true };
2664
+ }
2665
+ return { success: false, error: "Element not found" };
2666
+ },
2667
+ /**
2668
+ * Toggle class on element
2669
+ */
2670
+ toggleClass: (selector, className) => {
2671
+ if (typeof document === "undefined") return { success: false };
2672
+ const el = document.querySelector(selector);
2673
+ if (el) {
2674
+ el.classList.toggle(className);
2675
+ return { success: true };
2676
+ }
2677
+ return { success: false, error: "Element not found" };
2678
+ },
2679
+ /**
2680
+ * Local storage helpers
2681
+ */
2682
+ localStorage: {
2683
+ get: (key, defaultValue) => getLocalStorage(key, defaultValue),
2684
+ set: (key, value) => setLocalStorage(key, value),
2685
+ remove: (key) => {
2686
+ if (typeof localStorage !== "undefined") {
2687
+ localStorage.removeItem(key);
2688
+ }
2689
+ }
2690
+ },
2691
+ /**
2692
+ * Session storage helpers
2693
+ */
2694
+ sessionStorage: {
2695
+ get: (key, defaultValue) => getSessionStorage(key, defaultValue),
2696
+ set: (key, value) => setSessionStorage(key, value),
2697
+ remove: (key) => {
2698
+ if (typeof sessionStorage !== "undefined") {
2699
+ sessionStorage.removeItem(key);
2700
+ }
2701
+ }
2702
+ },
2703
+ /**
2704
+ * Clipboard helpers
2705
+ */
2706
+ clipboard: {
2707
+ copy: async (text) => {
2708
+ if (typeof navigator !== "undefined" && navigator.clipboard) {
2709
+ try {
2710
+ await navigator.clipboard.writeText(text);
2711
+ return { success: true };
2712
+ } catch (error) {
2713
+ return { success: false, error };
2714
+ }
2715
+ }
2716
+ return { success: false, error: "Clipboard not available" };
2717
+ }
2718
+ },
2719
+ /**
2720
+ * Delay execution
2721
+ */
2722
+ delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
2723
+ /**
2724
+ * Current context accessors
2725
+ */
2726
+ currentItem: ctx.currentItem,
2727
+ currentIndex: ctx.currentIndex,
2728
+ elementId: ctx.elementId
2729
+ };
2730
+ }
2440
2731
  var builtInPlugins = {
2441
2732
  /**
2442
2733
  * Set state value
@@ -2563,6 +2854,7 @@ var builtInPlugins = {
2563
2854
  const code = action.code || action.config?.code;
2564
2855
  if (!code) return { success: false, error: "No code provided" };
2565
2856
  try {
2857
+ const shortcuts = createShortcuts(ctx);
2566
2858
  const fn = new Function(
2567
2859
  "event",
2568
2860
  "props",
@@ -2571,6 +2863,7 @@ var builtInPlugins = {
2571
2863
  "stateManager",
2572
2864
  "currentItem",
2573
2865
  "currentIndex",
2866
+ "shortcuts",
2574
2867
  code
2575
2868
  );
2576
2869
  const result = fn(
@@ -2580,7 +2873,8 @@ var builtInPlugins = {
2580
2873
  ctx.context.context || {},
2581
2874
  ctx.stateManager,
2582
2875
  ctx.currentItem,
2583
- ctx.currentIndex
2876
+ ctx.currentIndex,
2877
+ shortcuts
2584
2878
  );
2585
2879
  return { success: true, result };
2586
2880
  } catch (error) {
@@ -5296,18 +5590,78 @@ async function mount(options) {
5296
5590
  eventHandlers,
5297
5591
  fetchOptions = {},
5298
5592
  onReady,
5299
- onError
5593
+ onError,
5594
+ onLoadStart,
5595
+ onLoadEnd,
5596
+ loadingComponent,
5597
+ errorComponent,
5598
+ loadingDelay = 0,
5599
+ minLoadingTime = 0
5300
5600
  } = options;
5601
+ const container = typeof target === "string" ? document.querySelector(target) : target;
5602
+ if (!container) {
5603
+ const err = new Error(`Target container not found: ${target}`);
5604
+ onError?.(err);
5605
+ throw err;
5606
+ }
5607
+ let loadingElement = null;
5608
+ let loadingTimeout = null;
5609
+ let loadingStartTime = null;
5610
+ const showLoading = () => {
5611
+ if (loadingComponent) {
5612
+ loadingStartTime = Date.now();
5613
+ if (typeof loadingComponent === "string") {
5614
+ loadingElement = document.createElement("div");
5615
+ loadingElement.innerHTML = loadingComponent;
5616
+ loadingElement = loadingElement.firstElementChild || loadingElement;
5617
+ } else {
5618
+ loadingElement = loadingComponent.cloneNode(true);
5619
+ }
5620
+ container.appendChild(loadingElement);
5621
+ }
5622
+ };
5623
+ const removeLoading = async () => {
5624
+ if (loadingTimeout) {
5625
+ clearTimeout(loadingTimeout);
5626
+ loadingTimeout = null;
5627
+ }
5628
+ if (loadingStartTime && minLoadingTime > 0) {
5629
+ const elapsed = Date.now() - loadingStartTime;
5630
+ if (elapsed < minLoadingTime) {
5631
+ await new Promise((resolve) => setTimeout(resolve, minLoadingTime - elapsed));
5632
+ }
5633
+ }
5634
+ if (loadingElement && loadingElement.parentNode) {
5635
+ loadingElement.parentNode.removeChild(loadingElement);
5636
+ loadingElement = null;
5637
+ }
5638
+ };
5639
+ const showError = (err) => {
5640
+ if (errorComponent) {
5641
+ const errorContent = errorComponent(err);
5642
+ if (typeof errorContent === "string") {
5643
+ container.innerHTML = errorContent;
5644
+ } else {
5645
+ container.innerHTML = "";
5646
+ container.appendChild(errorContent);
5647
+ }
5648
+ }
5649
+ };
5301
5650
  try {
5302
- const container = typeof target === "string" ? document.querySelector(target) : target;
5303
- if (!container) {
5304
- throw new Error(`Target container not found: ${target}`);
5651
+ onLoadStart?.();
5652
+ if (loadingComponent) {
5653
+ if (loadingDelay > 0) {
5654
+ loadingTimeout = setTimeout(showLoading, loadingDelay);
5655
+ } else {
5656
+ showLoading();
5657
+ }
5305
5658
  }
5306
5659
  const fetchResult = await fetchComponentWithDependencies(componentId, {
5307
5660
  ...fetchOptions,
5308
5661
  version
5309
5662
  });
5310
5663
  const { data, fromCache, version: resolvedVersion, registry, views } = fetchResult;
5664
+ await removeLoading();
5311
5665
  const bindingContext = {
5312
5666
  props,
5313
5667
  state,
@@ -5338,10 +5692,14 @@ async function mount(options) {
5338
5692
  fromCache,
5339
5693
  version: resolvedVersion
5340
5694
  };
5695
+ onLoadEnd?.();
5341
5696
  onReady?.(result);
5342
5697
  return result;
5343
5698
  } catch (error) {
5344
5699
  const err = error instanceof Error ? error : new Error(String(error));
5700
+ await removeLoading();
5701
+ showError(err);
5702
+ onLoadEnd?.();
5345
5703
  onError?.(err);
5346
5704
  throw err;
5347
5705
  }
@@ -5387,6 +5745,108 @@ function mountData(options) {
5387
5745
  };
5388
5746
  }
5389
5747
 
5748
+ // src/prefetch.ts
5749
+ async function prefetch(componentId, version = "latest", options = {}) {
5750
+ const { includeDependencies = true, ...fetchOptions } = options;
5751
+ try {
5752
+ if (includeDependencies) {
5753
+ await fetchComponentWithDependencies(componentId, { ...fetchOptions, version });
5754
+ } else {
5755
+ await fetchComponent(componentId, { ...fetchOptions, version });
5756
+ }
5757
+ return true;
5758
+ } catch (error) {
5759
+ console.warn(`[Servly] Failed to prefetch ${componentId}:`, error);
5760
+ return false;
5761
+ }
5762
+ }
5763
+ async function prefetchAll(components, options = {}) {
5764
+ const results = await Promise.allSettled(
5765
+ components.map((comp) => {
5766
+ const id = typeof comp === "string" ? comp : comp.id;
5767
+ const version = typeof comp === "string" ? "latest" : comp.version || "latest";
5768
+ return prefetch(id, version, options).then((success2) => ({ id, success: success2 }));
5769
+ })
5770
+ );
5771
+ const success = [];
5772
+ const failed = [];
5773
+ for (const result of results) {
5774
+ if (result.status === "fulfilled" && result.value.success) {
5775
+ success.push(result.value.id);
5776
+ } else {
5777
+ const id = result.status === "fulfilled" ? result.value.id : "unknown";
5778
+ failed.push(id);
5779
+ }
5780
+ }
5781
+ return { success, failed };
5782
+ }
5783
+ function prefetchOnVisible(target, components, options = {}) {
5784
+ const { rootMargin = "100px", threshold = 0, ...prefetchOptions } = options;
5785
+ const element = typeof target === "string" ? document.querySelector(target) : target;
5786
+ if (!element) {
5787
+ console.warn(`[Servly] prefetchOnVisible: Target not found: ${target}`);
5788
+ return () => {
5789
+ };
5790
+ }
5791
+ let hasPrefetched = false;
5792
+ const observer = new IntersectionObserver(
5793
+ (entries) => {
5794
+ for (const entry of entries) {
5795
+ if (entry.isIntersecting && !hasPrefetched) {
5796
+ hasPrefetched = true;
5797
+ prefetchAll(components, prefetchOptions);
5798
+ observer.disconnect();
5799
+ }
5800
+ }
5801
+ },
5802
+ { rootMargin, threshold }
5803
+ );
5804
+ observer.observe(element);
5805
+ return () => observer.disconnect();
5806
+ }
5807
+ function prefetchOnIdle(components, options = {}) {
5808
+ const { timeout = 5e3, ...prefetchOptions } = options;
5809
+ const callback = () => {
5810
+ prefetchAll(components, prefetchOptions);
5811
+ };
5812
+ if ("requestIdleCallback" in window) {
5813
+ window.requestIdleCallback(callback, { timeout });
5814
+ } else {
5815
+ setTimeout(callback, 1);
5816
+ }
5817
+ }
5818
+ function prefetchOnHover(target, componentId, version = "latest", options = {}) {
5819
+ const { delay = 100, ...prefetchOptions } = options;
5820
+ const element = typeof target === "string" ? document.querySelector(target) : target;
5821
+ if (!element) {
5822
+ console.warn(`[Servly] prefetchOnHover: Target not found: ${target}`);
5823
+ return () => {
5824
+ };
5825
+ }
5826
+ let timeoutId = null;
5827
+ let hasPrefetched = false;
5828
+ const handleMouseEnter = () => {
5829
+ if (hasPrefetched) return;
5830
+ timeoutId = setTimeout(() => {
5831
+ hasPrefetched = true;
5832
+ prefetch(componentId, version, prefetchOptions);
5833
+ }, delay);
5834
+ };
5835
+ const handleMouseLeave = () => {
5836
+ if (timeoutId) {
5837
+ clearTimeout(timeoutId);
5838
+ timeoutId = null;
5839
+ }
5840
+ };
5841
+ element.addEventListener("mouseenter", handleMouseEnter);
5842
+ element.addEventListener("mouseleave", handleMouseLeave);
5843
+ return () => {
5844
+ element.removeEventListener("mouseenter", handleMouseEnter);
5845
+ element.removeEventListener("mouseleave", handleMouseLeave);
5846
+ if (timeoutId) clearTimeout(timeoutId);
5847
+ };
5848
+ }
5849
+
5390
5850
  // src/version.ts
5391
5851
  function parseVersion(version) {
5392
5852
  const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
@@ -5757,6 +6217,55 @@ function getSampleValue(def) {
5757
6217
  // src/index.ts
5758
6218
  init_registry();
5759
6219
  init_tailwind();
6220
+
6221
+ // src/slotBindings.ts
6222
+ function extractSlotBindings(layout) {
6223
+ const bindings = /* @__PURE__ */ new Map();
6224
+ const elementMap = /* @__PURE__ */ new Map();
6225
+ for (const element of layout) {
6226
+ elementMap.set(element.i, element);
6227
+ }
6228
+ const traceToProps = (element, propPath, visited = /* @__PURE__ */ new Set()) => {
6229
+ if (visited.has(element.i)) return null;
6230
+ visited.add(element.i);
6231
+ const inputs = element.configuration?.bindings?.inputs || {};
6232
+ const binding = inputs[propPath];
6233
+ if (!binding) return null;
6234
+ if (binding.source === "props" && binding.path) {
6235
+ return binding.path;
6236
+ }
6237
+ if (binding.source === "parent" && binding.path && element.parent) {
6238
+ const parentElement = elementMap.get(element.parent);
6239
+ if (parentElement) {
6240
+ return traceToProps(parentElement, binding.path, visited);
6241
+ }
6242
+ }
6243
+ return null;
6244
+ };
6245
+ for (const element of layout) {
6246
+ if (element.componentId !== "slot") continue;
6247
+ const inputs = element.configuration?.bindings?.inputs || {};
6248
+ const slotId = element.configuration?.slotName || element.i;
6249
+ for (const [targetProp, binding] of Object.entries(inputs)) {
6250
+ if (["child", "children", "content"].includes(targetProp)) {
6251
+ const b = binding;
6252
+ if (!b) continue;
6253
+ if (b.source === "props" && b.path) {
6254
+ bindings.set(b.path, slotId);
6255
+ } else if (b.source === "parent" && b.path && element.parent) {
6256
+ const parentElement = elementMap.get(element.parent);
6257
+ if (parentElement) {
6258
+ const originalProp = traceToProps(parentElement, b.path);
6259
+ if (originalProp) {
6260
+ bindings.set(originalProp, slotId);
6261
+ }
6262
+ }
6263
+ }
6264
+ }
6265
+ }
6266
+ }
6267
+ return bindings;
6268
+ }
5760
6269
  // Annotate the CommonJS export names for ESM import in node:
5761
6270
  0 && (module.exports = {
5762
6271
  AnalyticsCollector,
@@ -5806,6 +6315,7 @@ init_tailwind();
5806
6315
  extractDependenciesFromCode,
5807
6316
  extractOverrideDependencies,
5808
6317
  extractReferencedViewIds,
6318
+ extractSlotBindings,
5809
6319
  fetchComponent,
5810
6320
  fetchComponentWithDependencies,
5811
6321
  formatStyleValue,
@@ -5863,7 +6373,12 @@ init_tailwind();
5863
6373
  mountData,
5864
6374
  navigateTo,
5865
6375
  parseVersion,
6376
+ prefetch,
6377
+ prefetchAll,
5866
6378
  prefetchComponents,
6379
+ prefetchOnHover,
6380
+ prefetchOnIdle,
6381
+ prefetchOnVisible,
5867
6382
  preloadIcons,
5868
6383
  preloadTailwind,
5869
6384
  preventFOUC,
package/dist/index.js CHANGED
@@ -718,8 +718,96 @@ function resolveBindingPath(path, context) {
718
718
  }
719
719
  return navigatePath(source, parts.slice(startIndex));
720
720
  }
721
+ var FUNCTION_CALL_REGEX = /^(\w+)\s*\((.*)\)$/s;
722
+ function executeGlobalFunction(funcName, argsStr, context) {
723
+ if (context.functionMap?.[funcName]) {
724
+ try {
725
+ const args = parseFunctionArguments(argsStr, context);
726
+ return context.functionMap[funcName](...args);
727
+ } catch (error) {
728
+ console.error(`[GlobalFunction] Error executing ${funcName}:`, error);
729
+ return void 0;
730
+ }
731
+ }
732
+ const func = context.globalFunctions?.find((f) => f.name === funcName);
733
+ if (!func) {
734
+ console.warn(`[GlobalFunction] Function not found: ${funcName}`);
735
+ return void 0;
736
+ }
737
+ try {
738
+ const args = parseFunctionArguments(argsStr, context);
739
+ const paramNames = func.parameters.map((p) => p.name);
740
+ const fnBody = `
741
+ const state = __ctx__.state || {};
742
+ const props = __ctx__.props || {};
743
+ const config = __ctx__.configs || __ctx__.context || {};
744
+ ${func.code}
745
+ `;
746
+ const fn = new Function("__ctx__", ...paramNames, fnBody);
747
+ return fn(context, ...args);
748
+ } catch (error) {
749
+ console.error(`[GlobalFunction] Error executing ${funcName}:`, error);
750
+ return void 0;
751
+ }
752
+ }
753
+ function parseFunctionArguments(argsStr, context) {
754
+ if (!argsStr.trim()) return [];
755
+ const args = [];
756
+ let current = "";
757
+ let depth = 0;
758
+ let inString = false;
759
+ let stringChar = "";
760
+ for (let i = 0; i < argsStr.length; i++) {
761
+ const char = argsStr[i];
762
+ const prevChar = argsStr[i - 1];
763
+ if ((char === '"' || char === "'") && prevChar !== "\\") {
764
+ if (!inString) {
765
+ inString = true;
766
+ stringChar = char;
767
+ } else if (char === stringChar) {
768
+ inString = false;
769
+ }
770
+ }
771
+ if (!inString) {
772
+ if (char === "(" || char === "[" || char === "{") depth++;
773
+ if (char === ")" || char === "]" || char === "}") depth--;
774
+ }
775
+ if (char === "," && depth === 0 && !inString) {
776
+ args.push(parseArgumentValue(current.trim(), context));
777
+ current = "";
778
+ } else {
779
+ current += char;
780
+ }
781
+ }
782
+ if (current.trim()) {
783
+ args.push(parseArgumentValue(current.trim(), context));
784
+ }
785
+ return args;
786
+ }
787
+ function parseArgumentValue(value, context) {
788
+ const trimmed = value.trim();
789
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
790
+ return trimmed.slice(1, -1);
791
+ }
792
+ if (!isNaN(Number(trimmed)) && trimmed !== "") {
793
+ return Number(trimmed);
794
+ }
795
+ if (trimmed === "true") return true;
796
+ if (trimmed === "false") return false;
797
+ if (trimmed === "null") return null;
798
+ if (trimmed === "undefined") return void 0;
799
+ if (trimmed.startsWith("{{") && trimmed.endsWith("}}")) {
800
+ return resolveTemplateValue(trimmed, context);
801
+ }
802
+ return resolveBindingPath(trimmed, context);
803
+ }
721
804
  function resolveExpression(expression, context) {
722
805
  const trimmed = expression.trim();
806
+ const funcMatch = trimmed.match(FUNCTION_CALL_REGEX);
807
+ if (funcMatch) {
808
+ const [, funcName, argsStr] = funcMatch;
809
+ return executeGlobalFunction(funcName, argsStr, context);
810
+ }
723
811
  const comparisonOperators = ["===", "!==", "==", "!=", ">=", "<=", ">", "<"];
724
812
  for (const op of comparisonOperators) {
725
813
  if (trimmed.includes(op)) {
@@ -1636,6 +1724,203 @@ function getUrlInfo() {
1636
1724
  }
1637
1725
 
1638
1726
  // src/eventSystem.ts
1727
+ function createShortcuts(ctx) {
1728
+ return {
1729
+ /**
1730
+ * Render a dynamic list of items using a blueprint view
1731
+ * This is a simplified version for runtime - full implementation requires DOM access
1732
+ */
1733
+ renderDynamicList: (options) => {
1734
+ const { blueprint, targetContainer, data = [], clearExisting = true, itemProps = {} } = options;
1735
+ const container = typeof document !== "undefined" ? document.querySelector(targetContainer) || document.querySelector(`[data-servly-id="${targetContainer.replace("#", "")}"]`) : null;
1736
+ if (!container) {
1737
+ console.warn(`[shortcuts.renderDynamicList] Container not found: ${targetContainer}`);
1738
+ return { success: false, error: "Container not found" };
1739
+ }
1740
+ if (clearExisting) {
1741
+ container.innerHTML = "";
1742
+ }
1743
+ const event = new CustomEvent("servly:renderDynamicList", {
1744
+ bubbles: true,
1745
+ detail: {
1746
+ blueprint,
1747
+ targetContainer,
1748
+ data,
1749
+ itemProps,
1750
+ container
1751
+ }
1752
+ });
1753
+ container.dispatchEvent(event);
1754
+ return { success: true, itemCount: data.length };
1755
+ },
1756
+ /**
1757
+ * Quick list rendering helper
1758
+ */
1759
+ renderList: (blueprint, targetContainer, data, options = {}) => {
1760
+ return createShortcuts(ctx).renderDynamicList({
1761
+ blueprint,
1762
+ targetContainer,
1763
+ data,
1764
+ ...options
1765
+ });
1766
+ },
1767
+ /**
1768
+ * Set state value
1769
+ */
1770
+ setState: (key, value) => {
1771
+ if (ctx.stateManager) {
1772
+ ctx.stateManager.set(key, value, ctx.elementId);
1773
+ return { success: true };
1774
+ }
1775
+ return { success: false, error: "No state manager" };
1776
+ },
1777
+ /**
1778
+ * Get state value
1779
+ */
1780
+ getState: (key) => {
1781
+ if (ctx.stateManager) {
1782
+ return ctx.stateManager.get(key);
1783
+ }
1784
+ return void 0;
1785
+ },
1786
+ /**
1787
+ * Toggle state value
1788
+ */
1789
+ toggleState: (key) => {
1790
+ if (ctx.stateManager) {
1791
+ ctx.stateManager.toggle(key, ctx.elementId);
1792
+ return { success: true };
1793
+ }
1794
+ return { success: false, error: "No state manager" };
1795
+ },
1796
+ /**
1797
+ * Navigate to URL
1798
+ */
1799
+ navigateTo: (url, options) => {
1800
+ navigateTo(url, options);
1801
+ return { success: true };
1802
+ },
1803
+ /**
1804
+ * Log to console (for debugging)
1805
+ */
1806
+ log: (...args) => {
1807
+ console.log("[Servly]", ...args);
1808
+ },
1809
+ /**
1810
+ * Show alert
1811
+ */
1812
+ alert: (message) => {
1813
+ if (typeof alert !== "undefined") {
1814
+ alert(message);
1815
+ }
1816
+ },
1817
+ /**
1818
+ * Get element by ID
1819
+ */
1820
+ getElement: (id) => {
1821
+ if (typeof document === "undefined") return null;
1822
+ return document.querySelector(`[data-servly-id="${id}"]`) || document.getElementById(id);
1823
+ },
1824
+ /**
1825
+ * Set element text content
1826
+ */
1827
+ setText: (selector, text) => {
1828
+ if (typeof document === "undefined") return { success: false };
1829
+ const el = document.querySelector(selector) || document.querySelector(`[data-servly-id="${selector.replace("#", "")}"]`);
1830
+ if (el) {
1831
+ el.textContent = text;
1832
+ return { success: true };
1833
+ }
1834
+ return { success: false, error: "Element not found" };
1835
+ },
1836
+ /**
1837
+ * Add class to element
1838
+ */
1839
+ addClass: (selector, className) => {
1840
+ if (typeof document === "undefined") return { success: false };
1841
+ const el = document.querySelector(selector);
1842
+ if (el) {
1843
+ el.classList.add(className);
1844
+ return { success: true };
1845
+ }
1846
+ return { success: false, error: "Element not found" };
1847
+ },
1848
+ /**
1849
+ * Remove class from element
1850
+ */
1851
+ removeClass: (selector, className) => {
1852
+ if (typeof document === "undefined") return { success: false };
1853
+ const el = document.querySelector(selector);
1854
+ if (el) {
1855
+ el.classList.remove(className);
1856
+ return { success: true };
1857
+ }
1858
+ return { success: false, error: "Element not found" };
1859
+ },
1860
+ /**
1861
+ * Toggle class on element
1862
+ */
1863
+ toggleClass: (selector, className) => {
1864
+ if (typeof document === "undefined") return { success: false };
1865
+ const el = document.querySelector(selector);
1866
+ if (el) {
1867
+ el.classList.toggle(className);
1868
+ return { success: true };
1869
+ }
1870
+ return { success: false, error: "Element not found" };
1871
+ },
1872
+ /**
1873
+ * Local storage helpers
1874
+ */
1875
+ localStorage: {
1876
+ get: (key, defaultValue) => getLocalStorage(key, defaultValue),
1877
+ set: (key, value) => setLocalStorage(key, value),
1878
+ remove: (key) => {
1879
+ if (typeof localStorage !== "undefined") {
1880
+ localStorage.removeItem(key);
1881
+ }
1882
+ }
1883
+ },
1884
+ /**
1885
+ * Session storage helpers
1886
+ */
1887
+ sessionStorage: {
1888
+ get: (key, defaultValue) => getSessionStorage(key, defaultValue),
1889
+ set: (key, value) => setSessionStorage(key, value),
1890
+ remove: (key) => {
1891
+ if (typeof sessionStorage !== "undefined") {
1892
+ sessionStorage.removeItem(key);
1893
+ }
1894
+ }
1895
+ },
1896
+ /**
1897
+ * Clipboard helpers
1898
+ */
1899
+ clipboard: {
1900
+ copy: async (text) => {
1901
+ if (typeof navigator !== "undefined" && navigator.clipboard) {
1902
+ try {
1903
+ await navigator.clipboard.writeText(text);
1904
+ return { success: true };
1905
+ } catch (error) {
1906
+ return { success: false, error };
1907
+ }
1908
+ }
1909
+ return { success: false, error: "Clipboard not available" };
1910
+ }
1911
+ },
1912
+ /**
1913
+ * Delay execution
1914
+ */
1915
+ delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
1916
+ /**
1917
+ * Current context accessors
1918
+ */
1919
+ currentItem: ctx.currentItem,
1920
+ currentIndex: ctx.currentIndex,
1921
+ elementId: ctx.elementId
1922
+ };
1923
+ }
1639
1924
  var builtInPlugins = {
1640
1925
  /**
1641
1926
  * Set state value
@@ -1762,6 +2047,7 @@ var builtInPlugins = {
1762
2047
  const code = action.code || action.config?.code;
1763
2048
  if (!code) return { success: false, error: "No code provided" };
1764
2049
  try {
2050
+ const shortcuts = createShortcuts(ctx);
1765
2051
  const fn = new Function(
1766
2052
  "event",
1767
2053
  "props",
@@ -1770,6 +2056,7 @@ var builtInPlugins = {
1770
2056
  "stateManager",
1771
2057
  "currentItem",
1772
2058
  "currentIndex",
2059
+ "shortcuts",
1773
2060
  code
1774
2061
  );
1775
2062
  const result = fn(
@@ -1779,7 +2066,8 @@ var builtInPlugins = {
1779
2066
  ctx.context.context || {},
1780
2067
  ctx.stateManager,
1781
2068
  ctx.currentItem,
1782
- ctx.currentIndex
2069
+ ctx.currentIndex,
2070
+ shortcuts
1783
2071
  );
1784
2072
  return { success: true, result };
1785
2073
  } catch (error) {
@@ -4491,18 +4779,78 @@ async function mount(options) {
4491
4779
  eventHandlers,
4492
4780
  fetchOptions = {},
4493
4781
  onReady,
4494
- onError
4782
+ onError,
4783
+ onLoadStart,
4784
+ onLoadEnd,
4785
+ loadingComponent,
4786
+ errorComponent,
4787
+ loadingDelay = 0,
4788
+ minLoadingTime = 0
4495
4789
  } = options;
4790
+ const container = typeof target === "string" ? document.querySelector(target) : target;
4791
+ if (!container) {
4792
+ const err = new Error(`Target container not found: ${target}`);
4793
+ onError?.(err);
4794
+ throw err;
4795
+ }
4796
+ let loadingElement = null;
4797
+ let loadingTimeout = null;
4798
+ let loadingStartTime = null;
4799
+ const showLoading = () => {
4800
+ if (loadingComponent) {
4801
+ loadingStartTime = Date.now();
4802
+ if (typeof loadingComponent === "string") {
4803
+ loadingElement = document.createElement("div");
4804
+ loadingElement.innerHTML = loadingComponent;
4805
+ loadingElement = loadingElement.firstElementChild || loadingElement;
4806
+ } else {
4807
+ loadingElement = loadingComponent.cloneNode(true);
4808
+ }
4809
+ container.appendChild(loadingElement);
4810
+ }
4811
+ };
4812
+ const removeLoading = async () => {
4813
+ if (loadingTimeout) {
4814
+ clearTimeout(loadingTimeout);
4815
+ loadingTimeout = null;
4816
+ }
4817
+ if (loadingStartTime && minLoadingTime > 0) {
4818
+ const elapsed = Date.now() - loadingStartTime;
4819
+ if (elapsed < minLoadingTime) {
4820
+ await new Promise((resolve) => setTimeout(resolve, minLoadingTime - elapsed));
4821
+ }
4822
+ }
4823
+ if (loadingElement && loadingElement.parentNode) {
4824
+ loadingElement.parentNode.removeChild(loadingElement);
4825
+ loadingElement = null;
4826
+ }
4827
+ };
4828
+ const showError = (err) => {
4829
+ if (errorComponent) {
4830
+ const errorContent = errorComponent(err);
4831
+ if (typeof errorContent === "string") {
4832
+ container.innerHTML = errorContent;
4833
+ } else {
4834
+ container.innerHTML = "";
4835
+ container.appendChild(errorContent);
4836
+ }
4837
+ }
4838
+ };
4496
4839
  try {
4497
- const container = typeof target === "string" ? document.querySelector(target) : target;
4498
- if (!container) {
4499
- throw new Error(`Target container not found: ${target}`);
4840
+ onLoadStart?.();
4841
+ if (loadingComponent) {
4842
+ if (loadingDelay > 0) {
4843
+ loadingTimeout = setTimeout(showLoading, loadingDelay);
4844
+ } else {
4845
+ showLoading();
4846
+ }
4500
4847
  }
4501
4848
  const fetchResult = await fetchComponentWithDependencies(componentId, {
4502
4849
  ...fetchOptions,
4503
4850
  version
4504
4851
  });
4505
4852
  const { data, fromCache, version: resolvedVersion, registry, views } = fetchResult;
4853
+ await removeLoading();
4506
4854
  const bindingContext = {
4507
4855
  props,
4508
4856
  state,
@@ -4533,10 +4881,14 @@ async function mount(options) {
4533
4881
  fromCache,
4534
4882
  version: resolvedVersion
4535
4883
  };
4884
+ onLoadEnd?.();
4536
4885
  onReady?.(result);
4537
4886
  return result;
4538
4887
  } catch (error) {
4539
4888
  const err = error instanceof Error ? error : new Error(String(error));
4889
+ await removeLoading();
4890
+ showError(err);
4891
+ onLoadEnd?.();
4540
4892
  onError?.(err);
4541
4893
  throw err;
4542
4894
  }
@@ -4582,6 +4934,108 @@ function mountData(options) {
4582
4934
  };
4583
4935
  }
4584
4936
 
4937
+ // src/prefetch.ts
4938
+ async function prefetch(componentId, version = "latest", options = {}) {
4939
+ const { includeDependencies = true, ...fetchOptions } = options;
4940
+ try {
4941
+ if (includeDependencies) {
4942
+ await fetchComponentWithDependencies(componentId, { ...fetchOptions, version });
4943
+ } else {
4944
+ await fetchComponent(componentId, { ...fetchOptions, version });
4945
+ }
4946
+ return true;
4947
+ } catch (error) {
4948
+ console.warn(`[Servly] Failed to prefetch ${componentId}:`, error);
4949
+ return false;
4950
+ }
4951
+ }
4952
+ async function prefetchAll(components, options = {}) {
4953
+ const results = await Promise.allSettled(
4954
+ components.map((comp) => {
4955
+ const id = typeof comp === "string" ? comp : comp.id;
4956
+ const version = typeof comp === "string" ? "latest" : comp.version || "latest";
4957
+ return prefetch(id, version, options).then((success2) => ({ id, success: success2 }));
4958
+ })
4959
+ );
4960
+ const success = [];
4961
+ const failed = [];
4962
+ for (const result of results) {
4963
+ if (result.status === "fulfilled" && result.value.success) {
4964
+ success.push(result.value.id);
4965
+ } else {
4966
+ const id = result.status === "fulfilled" ? result.value.id : "unknown";
4967
+ failed.push(id);
4968
+ }
4969
+ }
4970
+ return { success, failed };
4971
+ }
4972
+ function prefetchOnVisible(target, components, options = {}) {
4973
+ const { rootMargin = "100px", threshold = 0, ...prefetchOptions } = options;
4974
+ const element = typeof target === "string" ? document.querySelector(target) : target;
4975
+ if (!element) {
4976
+ console.warn(`[Servly] prefetchOnVisible: Target not found: ${target}`);
4977
+ return () => {
4978
+ };
4979
+ }
4980
+ let hasPrefetched = false;
4981
+ const observer = new IntersectionObserver(
4982
+ (entries) => {
4983
+ for (const entry of entries) {
4984
+ if (entry.isIntersecting && !hasPrefetched) {
4985
+ hasPrefetched = true;
4986
+ prefetchAll(components, prefetchOptions);
4987
+ observer.disconnect();
4988
+ }
4989
+ }
4990
+ },
4991
+ { rootMargin, threshold }
4992
+ );
4993
+ observer.observe(element);
4994
+ return () => observer.disconnect();
4995
+ }
4996
+ function prefetchOnIdle(components, options = {}) {
4997
+ const { timeout = 5e3, ...prefetchOptions } = options;
4998
+ const callback = () => {
4999
+ prefetchAll(components, prefetchOptions);
5000
+ };
5001
+ if ("requestIdleCallback" in window) {
5002
+ window.requestIdleCallback(callback, { timeout });
5003
+ } else {
5004
+ setTimeout(callback, 1);
5005
+ }
5006
+ }
5007
+ function prefetchOnHover(target, componentId, version = "latest", options = {}) {
5008
+ const { delay = 100, ...prefetchOptions } = options;
5009
+ const element = typeof target === "string" ? document.querySelector(target) : target;
5010
+ if (!element) {
5011
+ console.warn(`[Servly] prefetchOnHover: Target not found: ${target}`);
5012
+ return () => {
5013
+ };
5014
+ }
5015
+ let timeoutId = null;
5016
+ let hasPrefetched = false;
5017
+ const handleMouseEnter = () => {
5018
+ if (hasPrefetched) return;
5019
+ timeoutId = setTimeout(() => {
5020
+ hasPrefetched = true;
5021
+ prefetch(componentId, version, prefetchOptions);
5022
+ }, delay);
5023
+ };
5024
+ const handleMouseLeave = () => {
5025
+ if (timeoutId) {
5026
+ clearTimeout(timeoutId);
5027
+ timeoutId = null;
5028
+ }
5029
+ };
5030
+ element.addEventListener("mouseenter", handleMouseEnter);
5031
+ element.addEventListener("mouseleave", handleMouseLeave);
5032
+ return () => {
5033
+ element.removeEventListener("mouseenter", handleMouseEnter);
5034
+ element.removeEventListener("mouseleave", handleMouseLeave);
5035
+ if (timeoutId) clearTimeout(timeoutId);
5036
+ };
5037
+ }
5038
+
4585
5039
  // src/version.ts
4586
5040
  function parseVersion(version) {
4587
5041
  const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
@@ -4948,6 +5402,55 @@ function getSampleValue(def) {
4948
5402
  return def.defaultValue;
4949
5403
  }
4950
5404
  }
5405
+
5406
+ // src/slotBindings.ts
5407
+ function extractSlotBindings(layout) {
5408
+ const bindings = /* @__PURE__ */ new Map();
5409
+ const elementMap = /* @__PURE__ */ new Map();
5410
+ for (const element of layout) {
5411
+ elementMap.set(element.i, element);
5412
+ }
5413
+ const traceToProps = (element, propPath, visited = /* @__PURE__ */ new Set()) => {
5414
+ if (visited.has(element.i)) return null;
5415
+ visited.add(element.i);
5416
+ const inputs = element.configuration?.bindings?.inputs || {};
5417
+ const binding = inputs[propPath];
5418
+ if (!binding) return null;
5419
+ if (binding.source === "props" && binding.path) {
5420
+ return binding.path;
5421
+ }
5422
+ if (binding.source === "parent" && binding.path && element.parent) {
5423
+ const parentElement = elementMap.get(element.parent);
5424
+ if (parentElement) {
5425
+ return traceToProps(parentElement, binding.path, visited);
5426
+ }
5427
+ }
5428
+ return null;
5429
+ };
5430
+ for (const element of layout) {
5431
+ if (element.componentId !== "slot") continue;
5432
+ const inputs = element.configuration?.bindings?.inputs || {};
5433
+ const slotId = element.configuration?.slotName || element.i;
5434
+ for (const [targetProp, binding] of Object.entries(inputs)) {
5435
+ if (["child", "children", "content"].includes(targetProp)) {
5436
+ const b = binding;
5437
+ if (!b) continue;
5438
+ if (b.source === "props" && b.path) {
5439
+ bindings.set(b.path, slotId);
5440
+ } else if (b.source === "parent" && b.path && element.parent) {
5441
+ const parentElement = elementMap.get(element.parent);
5442
+ if (parentElement) {
5443
+ const originalProp = traceToProps(parentElement, b.path);
5444
+ if (originalProp) {
5445
+ bindings.set(originalProp, slotId);
5446
+ }
5447
+ }
5448
+ }
5449
+ }
5450
+ }
5451
+ }
5452
+ return bindings;
5453
+ }
4951
5454
  export {
4952
5455
  AnalyticsCollector,
4953
5456
  DEFAULT_CACHE_CONFIG,
@@ -4996,6 +5499,7 @@ export {
4996
5499
  extractDependenciesFromCode,
4997
5500
  extractOverrideDependencies,
4998
5501
  extractReferencedViewIds,
5502
+ extractSlotBindings,
4999
5503
  fetchComponent,
5000
5504
  fetchComponentWithDependencies,
5001
5505
  formatStyleValue,
@@ -5053,7 +5557,12 @@ export {
5053
5557
  mountData,
5054
5558
  navigateTo,
5055
5559
  parseVersion,
5560
+ prefetch,
5561
+ prefetchAll,
5056
5562
  prefetchComponents,
5563
+ prefetchOnHover,
5564
+ prefetchOnIdle,
5565
+ prefetchOnVisible,
5057
5566
  preloadIcons,
5058
5567
  preloadTailwind,
5059
5568
  preventFOUC,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@servlyadmin/runtime-core",
3
- "version": "0.1.46",
4
- "description": "Framework-agnostic core renderer for Servly components",
3
+ "version": "0.2.1",
4
+ "description": "Framework-agnostic core renderer for Servly components with prefetching and loading states",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",