@eventop/sdk 1.2.14 → 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.
package/dist/core.cjs CHANGED
@@ -221,6 +221,71 @@ function injectStyles(theme, pos) {
221
221
  style.textContent = `
222
222
  #sai-trigger, #sai-panel { ${buildCSSVars(theme)} }
223
223
 
224
+ /* ── Ring Light Animation ── */
225
+ @keyframes sai-ring-pulse {
226
+ 0% {
227
+ box-shadow:
228
+ 0 0 0 0 rgba(var(--sai-accent-rgb), 0.7),
229
+ 0 0 0 0 rgba(var(--sai-accent-rgb), 0.5),
230
+ 0 0 0 0 rgba(var(--sai-accent-rgb), 0.3);
231
+ }
232
+ 50% {
233
+ box-shadow:
234
+ 0 0 0 8px rgba(var(--sai-accent-rgb), 0),
235
+ 0 0 0 16px rgba(var(--sai-accent-rgb), 0.3),
236
+ 0 0 0 24px rgba(var(--sai-accent-rgb), 0);
237
+ }
238
+ 100% {
239
+ box-shadow:
240
+ 0 0 0 16px rgba(var(--sai-accent-rgb), 0),
241
+ 0 0 0 32px rgba(var(--sai-accent-rgb), 0),
242
+ 0 0 0 48px rgba(var(--sai-accent-rgb), 0);
243
+ }
244
+ }
245
+
246
+ @keyframes sai-ring-pulse-hard {
247
+ 0% {
248
+ box-shadow:
249
+ inset 0 0 0 2px var(--sai-accent),
250
+ 0 0 0 3px var(--sai-accent),
251
+ 0 0 20px var(--sai-accent);
252
+ }
253
+ 50% {
254
+ box-shadow:
255
+ inset 0 0 0 2px var(--sai-accent),
256
+ 0 0 0 8px var(--sai-accent),
257
+ 0 0 40px var(--sai-accent);
258
+ }
259
+ 100% {
260
+ box-shadow:
261
+ inset 0 0 0 2px var(--sai-accent),
262
+ 0 0 0 3px var(--sai-accent),
263
+ 0 0 20px var(--sai-accent);
264
+ }
265
+ }
266
+
267
+ @keyframes sai-element-highlight {
268
+ 0% { filter: brightness(1); }
269
+ 50% { filter: brightness(1.15); }
270
+ 100% { filter: brightness(1); }
271
+ }
272
+
273
+ /* Highlighted element styles */
274
+ .sai-highlighted {
275
+ position: relative !important;
276
+ animation: sai-ring-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
277
+ z-index: 99997 !important;
278
+ }
279
+
280
+ .sai-highlighted.sai-highlight-hard {
281
+ animation: sai-ring-pulse-hard 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
282
+ }
283
+
284
+ .sai-highlighted.sai-highlight-subtle {
285
+ animation: sai-element-highlight 2s ease-in-out infinite !important;
286
+ filter: drop-shadow(0 0 8px var(--sai-accent)) !important;
287
+ }
288
+
224
289
  /* ── Trigger ── */
225
290
  #sai-trigger {
226
291
  position: fixed; ${triggerCSS};
@@ -486,13 +551,7 @@ function buildSystemPrompt(config) {
486
551
  var _f$screen;
487
552
  return (_f$screen = f.screen) === null || _f$screen === void 0 ? void 0 : _f$screen.id;
488
553
  }).map(f => f.screen.id))];
489
-
490
- // Split features into live (current page) and ghost (other pages).
491
- // This lets the AI understand what's immediately available vs reachable
492
- // via navigation, and craft the right message when something isn't found.
493
- const liveFeatures = (config.features || []).filter(f => !f._ghost);
494
- const ghostFeatures = (config.features || []).filter(f => f._ghost);
495
- const summarise = f => {
554
+ const featureSummary = (config.features || []).map(f => {
496
555
  var _f$screen2, _f$flow;
497
556
  const entry = {
498
557
  id: f.id,
@@ -507,18 +566,15 @@ function buildSystemPrompt(config) {
507
566
  entry.note = `This feature has ${f.flow.length} sequential sub-steps. Include ONE step per flow entry.`;
508
567
  }
509
568
  return entry;
510
- };
511
- const liveSection = liveFeatures.length ? `CURRENT PAGE FEATURES (user is here now):\n${JSON.stringify(liveFeatures.map(summarise), null, 2)}` : `CURRENT PAGE FEATURES: none registered yet.`;
512
- const ghostSection = ghostFeatures.length ? `OTHER PAGE FEATURES (reachable via navigation — SDK handles it automatically):\n${JSON.stringify(ghostFeatures.map(summarise), null, 2)}` : '';
569
+ });
513
570
  return `
514
571
  You are an in-app assistant called "${config.assistantName || 'AI Guide'}" for "${config.appName}".
515
572
  Your ONLY job: guide users step-by-step through tasks using the feature map below.
516
573
 
517
- ${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. The SDK navigates automatically — just pick the right feature id.` : ''}
518
-
519
- ${liveSection}
574
+ ${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. Features are screen-specific. The SDK handles navigation — just pick the right features.` : ''}
520
575
 
521
- ${ghostSection}
576
+ FEATURE MAP (only reference IDs from this list):
577
+ ${JSON.stringify(featureSummary, null, 2)}
522
578
 
523
579
  RESPOND ONLY with this exact JSON — no markdown, no extra text:
524
580
  {
@@ -535,26 +591,16 @@ RESPOND ONLY with this exact JSON — no markdown, no extra text:
535
591
  }
536
592
 
537
593
  RULES:
538
- 1. The step "id" MUST match a feature id from the current page or other page features above.
594
+ 1. The step "id" MUST match a feature id from the feature map.
539
595
  2. Only use selectors and IDs from the feature map. Never invent them.
540
- 3. position values: top | bottom | left | right | auto only.
541
- 4. Order steps logically. For multi-step flows, order as the user encounters them.
542
- 5. For forms: ALWAYS include a step for the form section or first input BEFORE the
596
+ 3. No matching feature steps: [], explain kindly in message.
597
+ 4. position values: top | bottom | left | right | auto only.
598
+ 5. Order steps logically. For multi-step flows, order as the user encounters them.
599
+ 6. For forms: ALWAYS include a step for the form section or first input BEFORE the
543
600
  continue/submit button. The button step must always be LAST in its section.
544
- 6. If a feature has a flow, include one step per flow entry using the same feature id —
601
+ 7. If a feature has a flow, include one step per flow entry using the same feature id —
545
602
  the SDK expands them automatically.
546
- 7. Never skip features in a required sequence. Include every step end-to-end.
547
-
548
- WHEN THE USER'S REQUEST DOESN'T MATCH ANY FEATURE:
549
- - If it partially matches something (e.g. they said "create template" and there's a
550
- "template-gallery" feature): guide them to the closest matching feature and explain
551
- what it does. Don't say you can't help.
552
- - If it matches a feature on another page (ghost): include that feature's steps normally.
553
- The SDK will navigate there automatically. Do NOT tell the user to navigate manually.
554
- - If there is genuinely no match anywhere in the feature map: set steps to [] and say
555
- something like "That doesn't seem to be a feature in ${config.appName} yet. Here's
556
- what I can help you with:" then list 2-3 relevant features from the map by name.
557
- Never say you "can only guide through available features" — always offer alternatives.
603
+ 8. Never skip features in a required sequence. Include every step end-to-end.
558
604
  `.trim();
559
605
  }
560
606
 
@@ -1025,6 +1071,7 @@ function expandFlowSteps(aiStep, feature) {
1025
1071
 
1026
1072
  // Loads Shepherd.js, builds + runs tours, wires up advanceOn listeners,
1027
1073
  // progress indicators, pause/resume, and step-level error display.
1074
+ // ENHANCED: Adds animated ring light effect to highlighted elements.
1028
1075
 
1029
1076
  const SHEPHERD_JS = 'https://cdn.jsdelivr.net/npm/shepherd.js@11.2.0/dist/js/shepherd.min.js';
1030
1077
 
@@ -1045,6 +1092,61 @@ async function ensureShepherd() {
1045
1092
  await loadScript(SHEPHERD_JS);
1046
1093
  }
1047
1094
 
1095
+ // ─── Highlight management ────────────────────────────────────────────────────
1096
+
1097
+ /**
1098
+ * Applies the ring light animation to an element.
1099
+ *
1100
+ * @param {HTMLElement} el
1101
+ * @param {string} [style='default'] - 'default' | 'hard' | 'subtle'
1102
+ */
1103
+ function applyHighlight(el, style = 'default') {
1104
+ if (!el) return;
1105
+
1106
+ // Set CSS variables for accent color parsing in RGB format
1107
+ const accentColor = getComputedStyle(el).getPropertyValue('--sai-accent') || '#e94560';
1108
+ const rgb = hexToRgb(accentColor.trim());
1109
+ if (rgb) {
1110
+ el.style.setProperty('--sai-accent-rgb', `${rgb.r}, ${rgb.g}, ${rgb.b}`);
1111
+ }
1112
+ el.classList.add('sai-highlighted');
1113
+ if (style === 'hard') {
1114
+ el.classList.add('sai-highlight-hard');
1115
+ } else if (style === 'subtle') {
1116
+ el.classList.add('sai-highlight-subtle');
1117
+ }
1118
+
1119
+ // Scroll into view with extra padding
1120
+ if (el.scrollIntoView) {
1121
+ el.scrollIntoView({
1122
+ behavior: 'smooth',
1123
+ block: 'center'
1124
+ });
1125
+ }
1126
+ }
1127
+
1128
+ /**
1129
+ * Removes the ring light animation from an element.
1130
+ */
1131
+ function removeHighlight(el) {
1132
+ if (!el) return;
1133
+ el.classList.remove('sai-highlighted', 'sai-highlight-hard', 'sai-highlight-subtle');
1134
+ }
1135
+
1136
+ /**
1137
+ * Converts hex color to RGB object.
1138
+ * @param {string} hex
1139
+ * @returns {object|null}
1140
+ */
1141
+ function hexToRgb(hex) {
1142
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
1143
+ return result ? {
1144
+ r: parseInt(result[1], 16),
1145
+ g: parseInt(result[2], 16),
1146
+ b: parseInt(result[3], 16)
1147
+ } : null;
1148
+ }
1149
+
1048
1150
  // ─── Feature-map merge ────────────────────────────────────────────────────────
1049
1151
 
1050
1152
  /**
@@ -1078,6 +1180,7 @@ function mergeWithFeature(step) {
1078
1180
  * 1. Cross-page navigation (route prop) with automatic waiting
1079
1181
  * 2. Legacy screen navigation (screen.navigate)
1080
1182
  * 3. Element waiting (waitFor prop)
1183
+ * 4. Ring light animation application
1081
1184
  *
1082
1185
  * @param {object} step
1083
1186
  * @param {number} waitTimeout
@@ -1101,6 +1204,10 @@ function makeBeforeShowPromise(step, waitTimeout) {
1101
1204
  });
1102
1205
  } catch (_) {/* non-fatal */}
1103
1206
  await waitForElement(postNavMerge.selector, waitTimeout);
1207
+
1208
+ // Apply ring light animation to the element
1209
+ const el = document.querySelector(postNavMerge.selector);
1210
+ if (el) applyHighlight(el, step._highlightStyle || 'default');
1104
1211
  return;
1105
1212
  }
1106
1213
  }
@@ -1110,6 +1217,12 @@ function makeBeforeShowPromise(step, waitTimeout) {
1110
1217
  if (freshMerged.waitFor) {
1111
1218
  await waitForElement(freshMerged.waitFor, waitTimeout);
1112
1219
  }
1220
+
1221
+ // Apply ring light animation to the main selector
1222
+ if (freshMerged.selector) {
1223
+ const el = document.querySelector(freshMerged.selector);
1224
+ if (el) applyHighlight(el, step._highlightStyle || 'default');
1225
+ }
1113
1226
  })();
1114
1227
  }
1115
1228
 
@@ -1158,11 +1271,13 @@ function addProgressIndicator(shepherdStep, index, total) {
1158
1271
 
1159
1272
  /**
1160
1273
  * Starts a Shepherd tour from the given steps array.
1274
+ * ENHANCED: Applies ring light animations to highlighted elements.
1161
1275
  *
1162
1276
  * @param {Array} steps
1163
1277
  * @param {object} [options]
1164
1278
  * @param {boolean} [options.showProgress=true]
1165
1279
  * @param {number} [options.waitTimeout=8000]
1280
+ * @param {string} [options.highlightStyle='default'] - 'default' | 'hard' | 'subtle'
1166
1281
  */
1167
1282
  async function runTour(steps, options = {}) {
1168
1283
  var _state$config2;
@@ -1175,9 +1290,13 @@ async function runTour(steps, options = {}) {
1175
1290
  if (!(steps !== null && steps !== void 0 && steps.length)) return;
1176
1291
  const {
1177
1292
  showProgress = true,
1178
- waitTimeout = 8000
1293
+ waitTimeout = 8000,
1294
+ highlightStyle = 'default'
1179
1295
  } = options;
1180
- const mergedSteps = steps.map(mergeWithFeature);
1296
+ const mergedSteps = steps.map(step => ({
1297
+ ...mergeWithFeature(step),
1298
+ _highlightStyle: highlightStyle
1299
+ }));
1181
1300
 
1182
1301
  // Legacy: navigate to the correct screen for the first step
1183
1302
  const firstFeature = (_state$config2 = config) === null || _state$config2 === void 0 || (_state$config2 = _state$config2.features) === null || _state$config2 === void 0 ? void 0 : _state$config2.find(f => {
@@ -1243,12 +1362,24 @@ async function runTour(steps, options = {}) {
1243
1362
  });
1244
1363
  step._shepherdRef = shepherdStep;
1245
1364
  shepherdStep._isLast = isLast;
1365
+
1366
+ // Clean up highlight on hide
1367
+ shepherdStep.on('hide', () => {
1368
+ if (step.selector) {
1369
+ const el = document.querySelector(step.selector);
1370
+ if (el) removeHighlight(el);
1371
+ }
1372
+ });
1246
1373
  if (hasAuto) wireAdvanceOn(shepherdStep, step.advanceOn, tour$1);
1247
1374
  if (showProgress && expandedSteps.length > 1) {
1248
1375
  addProgressIndicator(shepherdStep, i, expandedSteps.length);
1249
1376
  }
1250
1377
  });
1251
1378
  tour$1.on('complete', () => {
1379
+ // Clean up all highlights
1380
+ document.querySelectorAll('.sai-highlighted').forEach(el => {
1381
+ removeHighlight(el);
1382
+ });
1252
1383
  runAndClearCleanups();
1253
1384
  setPausedSteps(null);
1254
1385
  setPausedIndex(0);
@@ -1259,6 +1390,11 @@ async function runTour(steps, options = {}) {
1259
1390
  tour$1.on('cancel', () => {
1260
1391
  const currentStepEl = tour$1.getCurrentStep();
1261
1392
  const currentIdx = currentStepEl ? expandedSteps.findIndex(s => s.id === currentStepEl.id) : 0;
1393
+
1394
+ // Clean up all highlights
1395
+ document.querySelectorAll('.sai-highlighted').forEach(el => {
1396
+ removeHighlight(el);
1397
+ });
1262
1398
  runAndClearCleanups();
1263
1399
  setPausedSteps(expandedSteps);
1264
1400
  setPausedIndex(Math.max(0, currentIdx));
package/dist/core.js CHANGED
@@ -219,6 +219,71 @@ function injectStyles(theme, pos) {
219
219
  style.textContent = `
220
220
  #sai-trigger, #sai-panel { ${buildCSSVars(theme)} }
221
221
 
222
+ /* ── Ring Light Animation ── */
223
+ @keyframes sai-ring-pulse {
224
+ 0% {
225
+ box-shadow:
226
+ 0 0 0 0 rgba(var(--sai-accent-rgb), 0.7),
227
+ 0 0 0 0 rgba(var(--sai-accent-rgb), 0.5),
228
+ 0 0 0 0 rgba(var(--sai-accent-rgb), 0.3);
229
+ }
230
+ 50% {
231
+ box-shadow:
232
+ 0 0 0 8px rgba(var(--sai-accent-rgb), 0),
233
+ 0 0 0 16px rgba(var(--sai-accent-rgb), 0.3),
234
+ 0 0 0 24px rgba(var(--sai-accent-rgb), 0);
235
+ }
236
+ 100% {
237
+ box-shadow:
238
+ 0 0 0 16px rgba(var(--sai-accent-rgb), 0),
239
+ 0 0 0 32px rgba(var(--sai-accent-rgb), 0),
240
+ 0 0 0 48px rgba(var(--sai-accent-rgb), 0);
241
+ }
242
+ }
243
+
244
+ @keyframes sai-ring-pulse-hard {
245
+ 0% {
246
+ box-shadow:
247
+ inset 0 0 0 2px var(--sai-accent),
248
+ 0 0 0 3px var(--sai-accent),
249
+ 0 0 20px var(--sai-accent);
250
+ }
251
+ 50% {
252
+ box-shadow:
253
+ inset 0 0 0 2px var(--sai-accent),
254
+ 0 0 0 8px var(--sai-accent),
255
+ 0 0 40px var(--sai-accent);
256
+ }
257
+ 100% {
258
+ box-shadow:
259
+ inset 0 0 0 2px var(--sai-accent),
260
+ 0 0 0 3px var(--sai-accent),
261
+ 0 0 20px var(--sai-accent);
262
+ }
263
+ }
264
+
265
+ @keyframes sai-element-highlight {
266
+ 0% { filter: brightness(1); }
267
+ 50% { filter: brightness(1.15); }
268
+ 100% { filter: brightness(1); }
269
+ }
270
+
271
+ /* Highlighted element styles */
272
+ .sai-highlighted {
273
+ position: relative !important;
274
+ animation: sai-ring-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
275
+ z-index: 99997 !important;
276
+ }
277
+
278
+ .sai-highlighted.sai-highlight-hard {
279
+ animation: sai-ring-pulse-hard 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
280
+ }
281
+
282
+ .sai-highlighted.sai-highlight-subtle {
283
+ animation: sai-element-highlight 2s ease-in-out infinite !important;
284
+ filter: drop-shadow(0 0 8px var(--sai-accent)) !important;
285
+ }
286
+
222
287
  /* ── Trigger ── */
223
288
  #sai-trigger {
224
289
  position: fixed; ${triggerCSS};
@@ -484,13 +549,7 @@ function buildSystemPrompt(config) {
484
549
  var _f$screen;
485
550
  return (_f$screen = f.screen) === null || _f$screen === void 0 ? void 0 : _f$screen.id;
486
551
  }).map(f => f.screen.id))];
487
-
488
- // Split features into live (current page) and ghost (other pages).
489
- // This lets the AI understand what's immediately available vs reachable
490
- // via navigation, and craft the right message when something isn't found.
491
- const liveFeatures = (config.features || []).filter(f => !f._ghost);
492
- const ghostFeatures = (config.features || []).filter(f => f._ghost);
493
- const summarise = f => {
552
+ const featureSummary = (config.features || []).map(f => {
494
553
  var _f$screen2, _f$flow;
495
554
  const entry = {
496
555
  id: f.id,
@@ -505,18 +564,15 @@ function buildSystemPrompt(config) {
505
564
  entry.note = `This feature has ${f.flow.length} sequential sub-steps. Include ONE step per flow entry.`;
506
565
  }
507
566
  return entry;
508
- };
509
- const liveSection = liveFeatures.length ? `CURRENT PAGE FEATURES (user is here now):\n${JSON.stringify(liveFeatures.map(summarise), null, 2)}` : `CURRENT PAGE FEATURES: none registered yet.`;
510
- const ghostSection = ghostFeatures.length ? `OTHER PAGE FEATURES (reachable via navigation — SDK handles it automatically):\n${JSON.stringify(ghostFeatures.map(summarise), null, 2)}` : '';
567
+ });
511
568
  return `
512
569
  You are an in-app assistant called "${config.assistantName || 'AI Guide'}" for "${config.appName}".
513
570
  Your ONLY job: guide users step-by-step through tasks using the feature map below.
514
571
 
515
- ${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. The SDK navigates automatically — just pick the right feature id.` : ''}
516
-
517
- ${liveSection}
572
+ ${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. Features are screen-specific. The SDK handles navigation — just pick the right features.` : ''}
518
573
 
519
- ${ghostSection}
574
+ FEATURE MAP (only reference IDs from this list):
575
+ ${JSON.stringify(featureSummary, null, 2)}
520
576
 
521
577
  RESPOND ONLY with this exact JSON — no markdown, no extra text:
522
578
  {
@@ -533,26 +589,16 @@ RESPOND ONLY with this exact JSON — no markdown, no extra text:
533
589
  }
534
590
 
535
591
  RULES:
536
- 1. The step "id" MUST match a feature id from the current page or other page features above.
592
+ 1. The step "id" MUST match a feature id from the feature map.
537
593
  2. Only use selectors and IDs from the feature map. Never invent them.
538
- 3. position values: top | bottom | left | right | auto only.
539
- 4. Order steps logically. For multi-step flows, order as the user encounters them.
540
- 5. For forms: ALWAYS include a step for the form section or first input BEFORE the
594
+ 3. No matching feature steps: [], explain kindly in message.
595
+ 4. position values: top | bottom | left | right | auto only.
596
+ 5. Order steps logically. For multi-step flows, order as the user encounters them.
597
+ 6. For forms: ALWAYS include a step for the form section or first input BEFORE the
541
598
  continue/submit button. The button step must always be LAST in its section.
542
- 6. If a feature has a flow, include one step per flow entry using the same feature id —
599
+ 7. If a feature has a flow, include one step per flow entry using the same feature id —
543
600
  the SDK expands them automatically.
544
- 7. Never skip features in a required sequence. Include every step end-to-end.
545
-
546
- WHEN THE USER'S REQUEST DOESN'T MATCH ANY FEATURE:
547
- - If it partially matches something (e.g. they said "create template" and there's a
548
- "template-gallery" feature): guide them to the closest matching feature and explain
549
- what it does. Don't say you can't help.
550
- - If it matches a feature on another page (ghost): include that feature's steps normally.
551
- The SDK will navigate there automatically. Do NOT tell the user to navigate manually.
552
- - If there is genuinely no match anywhere in the feature map: set steps to [] and say
553
- something like "That doesn't seem to be a feature in ${config.appName} yet. Here's
554
- what I can help you with:" then list 2-3 relevant features from the map by name.
555
- Never say you "can only guide through available features" — always offer alternatives.
601
+ 8. Never skip features in a required sequence. Include every step end-to-end.
556
602
  `.trim();
557
603
  }
558
604
 
@@ -1023,6 +1069,7 @@ function expandFlowSteps(aiStep, feature) {
1023
1069
 
1024
1070
  // Loads Shepherd.js, builds + runs tours, wires up advanceOn listeners,
1025
1071
  // progress indicators, pause/resume, and step-level error display.
1072
+ // ENHANCED: Adds animated ring light effect to highlighted elements.
1026
1073
 
1027
1074
  const SHEPHERD_JS = 'https://cdn.jsdelivr.net/npm/shepherd.js@11.2.0/dist/js/shepherd.min.js';
1028
1075
 
@@ -1043,6 +1090,61 @@ async function ensureShepherd() {
1043
1090
  await loadScript(SHEPHERD_JS);
1044
1091
  }
1045
1092
 
1093
+ // ─── Highlight management ────────────────────────────────────────────────────
1094
+
1095
+ /**
1096
+ * Applies the ring light animation to an element.
1097
+ *
1098
+ * @param {HTMLElement} el
1099
+ * @param {string} [style='default'] - 'default' | 'hard' | 'subtle'
1100
+ */
1101
+ function applyHighlight(el, style = 'default') {
1102
+ if (!el) return;
1103
+
1104
+ // Set CSS variables for accent color parsing in RGB format
1105
+ const accentColor = getComputedStyle(el).getPropertyValue('--sai-accent') || '#e94560';
1106
+ const rgb = hexToRgb(accentColor.trim());
1107
+ if (rgb) {
1108
+ el.style.setProperty('--sai-accent-rgb', `${rgb.r}, ${rgb.g}, ${rgb.b}`);
1109
+ }
1110
+ el.classList.add('sai-highlighted');
1111
+ if (style === 'hard') {
1112
+ el.classList.add('sai-highlight-hard');
1113
+ } else if (style === 'subtle') {
1114
+ el.classList.add('sai-highlight-subtle');
1115
+ }
1116
+
1117
+ // Scroll into view with extra padding
1118
+ if (el.scrollIntoView) {
1119
+ el.scrollIntoView({
1120
+ behavior: 'smooth',
1121
+ block: 'center'
1122
+ });
1123
+ }
1124
+ }
1125
+
1126
+ /**
1127
+ * Removes the ring light animation from an element.
1128
+ */
1129
+ function removeHighlight(el) {
1130
+ if (!el) return;
1131
+ el.classList.remove('sai-highlighted', 'sai-highlight-hard', 'sai-highlight-subtle');
1132
+ }
1133
+
1134
+ /**
1135
+ * Converts hex color to RGB object.
1136
+ * @param {string} hex
1137
+ * @returns {object|null}
1138
+ */
1139
+ function hexToRgb(hex) {
1140
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
1141
+ return result ? {
1142
+ r: parseInt(result[1], 16),
1143
+ g: parseInt(result[2], 16),
1144
+ b: parseInt(result[3], 16)
1145
+ } : null;
1146
+ }
1147
+
1046
1148
  // ─── Feature-map merge ────────────────────────────────────────────────────────
1047
1149
 
1048
1150
  /**
@@ -1076,6 +1178,7 @@ function mergeWithFeature(step) {
1076
1178
  * 1. Cross-page navigation (route prop) with automatic waiting
1077
1179
  * 2. Legacy screen navigation (screen.navigate)
1078
1180
  * 3. Element waiting (waitFor prop)
1181
+ * 4. Ring light animation application
1079
1182
  *
1080
1183
  * @param {object} step
1081
1184
  * @param {number} waitTimeout
@@ -1099,6 +1202,10 @@ function makeBeforeShowPromise(step, waitTimeout) {
1099
1202
  });
1100
1203
  } catch (_) {/* non-fatal */}
1101
1204
  await waitForElement(postNavMerge.selector, waitTimeout);
1205
+
1206
+ // Apply ring light animation to the element
1207
+ const el = document.querySelector(postNavMerge.selector);
1208
+ if (el) applyHighlight(el, step._highlightStyle || 'default');
1102
1209
  return;
1103
1210
  }
1104
1211
  }
@@ -1108,6 +1215,12 @@ function makeBeforeShowPromise(step, waitTimeout) {
1108
1215
  if (freshMerged.waitFor) {
1109
1216
  await waitForElement(freshMerged.waitFor, waitTimeout);
1110
1217
  }
1218
+
1219
+ // Apply ring light animation to the main selector
1220
+ if (freshMerged.selector) {
1221
+ const el = document.querySelector(freshMerged.selector);
1222
+ if (el) applyHighlight(el, step._highlightStyle || 'default');
1223
+ }
1111
1224
  })();
1112
1225
  }
1113
1226
 
@@ -1156,11 +1269,13 @@ function addProgressIndicator(shepherdStep, index, total) {
1156
1269
 
1157
1270
  /**
1158
1271
  * Starts a Shepherd tour from the given steps array.
1272
+ * ENHANCED: Applies ring light animations to highlighted elements.
1159
1273
  *
1160
1274
  * @param {Array} steps
1161
1275
  * @param {object} [options]
1162
1276
  * @param {boolean} [options.showProgress=true]
1163
1277
  * @param {number} [options.waitTimeout=8000]
1278
+ * @param {string} [options.highlightStyle='default'] - 'default' | 'hard' | 'subtle'
1164
1279
  */
1165
1280
  async function runTour(steps, options = {}) {
1166
1281
  var _state$config2;
@@ -1173,9 +1288,13 @@ async function runTour(steps, options = {}) {
1173
1288
  if (!(steps !== null && steps !== void 0 && steps.length)) return;
1174
1289
  const {
1175
1290
  showProgress = true,
1176
- waitTimeout = 8000
1291
+ waitTimeout = 8000,
1292
+ highlightStyle = 'default'
1177
1293
  } = options;
1178
- const mergedSteps = steps.map(mergeWithFeature);
1294
+ const mergedSteps = steps.map(step => ({
1295
+ ...mergeWithFeature(step),
1296
+ _highlightStyle: highlightStyle
1297
+ }));
1179
1298
 
1180
1299
  // Legacy: navigate to the correct screen for the first step
1181
1300
  const firstFeature = (_state$config2 = config) === null || _state$config2 === void 0 || (_state$config2 = _state$config2.features) === null || _state$config2 === void 0 ? void 0 : _state$config2.find(f => {
@@ -1241,12 +1360,24 @@ async function runTour(steps, options = {}) {
1241
1360
  });
1242
1361
  step._shepherdRef = shepherdStep;
1243
1362
  shepherdStep._isLast = isLast;
1363
+
1364
+ // Clean up highlight on hide
1365
+ shepherdStep.on('hide', () => {
1366
+ if (step.selector) {
1367
+ const el = document.querySelector(step.selector);
1368
+ if (el) removeHighlight(el);
1369
+ }
1370
+ });
1244
1371
  if (hasAuto) wireAdvanceOn(shepherdStep, step.advanceOn, tour$1);
1245
1372
  if (showProgress && expandedSteps.length > 1) {
1246
1373
  addProgressIndicator(shepherdStep, i, expandedSteps.length);
1247
1374
  }
1248
1375
  });
1249
1376
  tour$1.on('complete', () => {
1377
+ // Clean up all highlights
1378
+ document.querySelectorAll('.sai-highlighted').forEach(el => {
1379
+ removeHighlight(el);
1380
+ });
1250
1381
  runAndClearCleanups();
1251
1382
  setPausedSteps(null);
1252
1383
  setPausedIndex(0);
@@ -1257,6 +1388,11 @@ async function runTour(steps, options = {}) {
1257
1388
  tour$1.on('cancel', () => {
1258
1389
  const currentStepEl = tour$1.getCurrentStep();
1259
1390
  const currentIdx = currentStepEl ? expandedSteps.findIndex(s => s.id === currentStepEl.id) : 0;
1391
+
1392
+ // Clean up all highlights
1393
+ document.querySelectorAll('.sai-highlighted').forEach(el => {
1394
+ removeHighlight(el);
1395
+ });
1260
1396
  runAndClearCleanups();
1261
1397
  setPausedSteps(expandedSteps);
1262
1398
  setPausedIndex(Math.max(0, currentIdx));
@@ -221,6 +221,71 @@ function injectStyles(theme, pos) {
221
221
  style.textContent = `
222
222
  #sai-trigger, #sai-panel { ${buildCSSVars(theme)} }
223
223
 
224
+ /* ── Ring Light Animation ── */
225
+ @keyframes sai-ring-pulse {
226
+ 0% {
227
+ box-shadow:
228
+ 0 0 0 0 rgba(var(--sai-accent-rgb), 0.7),
229
+ 0 0 0 0 rgba(var(--sai-accent-rgb), 0.5),
230
+ 0 0 0 0 rgba(var(--sai-accent-rgb), 0.3);
231
+ }
232
+ 50% {
233
+ box-shadow:
234
+ 0 0 0 8px rgba(var(--sai-accent-rgb), 0),
235
+ 0 0 0 16px rgba(var(--sai-accent-rgb), 0.3),
236
+ 0 0 0 24px rgba(var(--sai-accent-rgb), 0);
237
+ }
238
+ 100% {
239
+ box-shadow:
240
+ 0 0 0 16px rgba(var(--sai-accent-rgb), 0),
241
+ 0 0 0 32px rgba(var(--sai-accent-rgb), 0),
242
+ 0 0 0 48px rgba(var(--sai-accent-rgb), 0);
243
+ }
244
+ }
245
+
246
+ @keyframes sai-ring-pulse-hard {
247
+ 0% {
248
+ box-shadow:
249
+ inset 0 0 0 2px var(--sai-accent),
250
+ 0 0 0 3px var(--sai-accent),
251
+ 0 0 20px var(--sai-accent);
252
+ }
253
+ 50% {
254
+ box-shadow:
255
+ inset 0 0 0 2px var(--sai-accent),
256
+ 0 0 0 8px var(--sai-accent),
257
+ 0 0 40px var(--sai-accent);
258
+ }
259
+ 100% {
260
+ box-shadow:
261
+ inset 0 0 0 2px var(--sai-accent),
262
+ 0 0 0 3px var(--sai-accent),
263
+ 0 0 20px var(--sai-accent);
264
+ }
265
+ }
266
+
267
+ @keyframes sai-element-highlight {
268
+ 0% { filter: brightness(1); }
269
+ 50% { filter: brightness(1.15); }
270
+ 100% { filter: brightness(1); }
271
+ }
272
+
273
+ /* Highlighted element styles */
274
+ .sai-highlighted {
275
+ position: relative !important;
276
+ animation: sai-ring-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
277
+ z-index: 99997 !important;
278
+ }
279
+
280
+ .sai-highlighted.sai-highlight-hard {
281
+ animation: sai-ring-pulse-hard 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
282
+ }
283
+
284
+ .sai-highlighted.sai-highlight-subtle {
285
+ animation: sai-element-highlight 2s ease-in-out infinite !important;
286
+ filter: drop-shadow(0 0 8px var(--sai-accent)) !important;
287
+ }
288
+
224
289
  /* ── Trigger ── */
225
290
  #sai-trigger {
226
291
  position: fixed; ${triggerCSS};
@@ -486,13 +551,7 @@ function buildSystemPrompt(config) {
486
551
  var _f$screen;
487
552
  return (_f$screen = f.screen) === null || _f$screen === void 0 ? void 0 : _f$screen.id;
488
553
  }).map(f => f.screen.id))];
489
-
490
- // Split features into live (current page) and ghost (other pages).
491
- // This lets the AI understand what's immediately available vs reachable
492
- // via navigation, and craft the right message when something isn't found.
493
- const liveFeatures = (config.features || []).filter(f => !f._ghost);
494
- const ghostFeatures = (config.features || []).filter(f => f._ghost);
495
- const summarise = f => {
554
+ const featureSummary = (config.features || []).map(f => {
496
555
  var _f$screen2, _f$flow;
497
556
  const entry = {
498
557
  id: f.id,
@@ -507,18 +566,15 @@ function buildSystemPrompt(config) {
507
566
  entry.note = `This feature has ${f.flow.length} sequential sub-steps. Include ONE step per flow entry.`;
508
567
  }
509
568
  return entry;
510
- };
511
- const liveSection = liveFeatures.length ? `CURRENT PAGE FEATURES (user is here now):\n${JSON.stringify(liveFeatures.map(summarise), null, 2)}` : `CURRENT PAGE FEATURES: none registered yet.`;
512
- const ghostSection = ghostFeatures.length ? `OTHER PAGE FEATURES (reachable via navigation — SDK handles it automatically):\n${JSON.stringify(ghostFeatures.map(summarise), null, 2)}` : '';
569
+ });
513
570
  return `
514
571
  You are an in-app assistant called "${config.assistantName || 'AI Guide'}" for "${config.appName}".
515
572
  Your ONLY job: guide users step-by-step through tasks using the feature map below.
516
573
 
517
- ${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. The SDK navigates automatically — just pick the right feature id.` : ''}
518
-
519
- ${liveSection}
574
+ ${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. Features are screen-specific. The SDK handles navigation — just pick the right features.` : ''}
520
575
 
521
- ${ghostSection}
576
+ FEATURE MAP (only reference IDs from this list):
577
+ ${JSON.stringify(featureSummary, null, 2)}
522
578
 
523
579
  RESPOND ONLY with this exact JSON — no markdown, no extra text:
524
580
  {
@@ -535,26 +591,16 @@ RESPOND ONLY with this exact JSON — no markdown, no extra text:
535
591
  }
536
592
 
537
593
  RULES:
538
- 1. The step "id" MUST match a feature id from the current page or other page features above.
594
+ 1. The step "id" MUST match a feature id from the feature map.
539
595
  2. Only use selectors and IDs from the feature map. Never invent them.
540
- 3. position values: top | bottom | left | right | auto only.
541
- 4. Order steps logically. For multi-step flows, order as the user encounters them.
542
- 5. For forms: ALWAYS include a step for the form section or first input BEFORE the
596
+ 3. No matching feature steps: [], explain kindly in message.
597
+ 4. position values: top | bottom | left | right | auto only.
598
+ 5. Order steps logically. For multi-step flows, order as the user encounters them.
599
+ 6. For forms: ALWAYS include a step for the form section or first input BEFORE the
543
600
  continue/submit button. The button step must always be LAST in its section.
544
- 6. If a feature has a flow, include one step per flow entry using the same feature id —
601
+ 7. If a feature has a flow, include one step per flow entry using the same feature id —
545
602
  the SDK expands them automatically.
546
- 7. Never skip features in a required sequence. Include every step end-to-end.
547
-
548
- WHEN THE USER'S REQUEST DOESN'T MATCH ANY FEATURE:
549
- - If it partially matches something (e.g. they said "create template" and there's a
550
- "template-gallery" feature): guide them to the closest matching feature and explain
551
- what it does. Don't say you can't help.
552
- - If it matches a feature on another page (ghost): include that feature's steps normally.
553
- The SDK will navigate there automatically. Do NOT tell the user to navigate manually.
554
- - If there is genuinely no match anywhere in the feature map: set steps to [] and say
555
- something like "That doesn't seem to be a feature in ${config.appName} yet. Here's
556
- what I can help you with:" then list 2-3 relevant features from the map by name.
557
- Never say you "can only guide through available features" — always offer alternatives.
603
+ 8. Never skip features in a required sequence. Include every step end-to-end.
558
604
  `.trim();
559
605
  }
560
606
 
@@ -1025,6 +1071,7 @@ function expandFlowSteps(aiStep, feature) {
1025
1071
 
1026
1072
  // Loads Shepherd.js, builds + runs tours, wires up advanceOn listeners,
1027
1073
  // progress indicators, pause/resume, and step-level error display.
1074
+ // ENHANCED: Adds animated ring light effect to highlighted elements.
1028
1075
 
1029
1076
  const SHEPHERD_JS = 'https://cdn.jsdelivr.net/npm/shepherd.js@11.2.0/dist/js/shepherd.min.js';
1030
1077
 
@@ -1045,6 +1092,61 @@ async function ensureShepherd() {
1045
1092
  await loadScript(SHEPHERD_JS);
1046
1093
  }
1047
1094
 
1095
+ // ─── Highlight management ────────────────────────────────────────────────────
1096
+
1097
+ /**
1098
+ * Applies the ring light animation to an element.
1099
+ *
1100
+ * @param {HTMLElement} el
1101
+ * @param {string} [style='default'] - 'default' | 'hard' | 'subtle'
1102
+ */
1103
+ function applyHighlight(el, style = 'default') {
1104
+ if (!el) return;
1105
+
1106
+ // Set CSS variables for accent color parsing in RGB format
1107
+ const accentColor = getComputedStyle(el).getPropertyValue('--sai-accent') || '#e94560';
1108
+ const rgb = hexToRgb(accentColor.trim());
1109
+ if (rgb) {
1110
+ el.style.setProperty('--sai-accent-rgb', `${rgb.r}, ${rgb.g}, ${rgb.b}`);
1111
+ }
1112
+ el.classList.add('sai-highlighted');
1113
+ if (style === 'hard') {
1114
+ el.classList.add('sai-highlight-hard');
1115
+ } else if (style === 'subtle') {
1116
+ el.classList.add('sai-highlight-subtle');
1117
+ }
1118
+
1119
+ // Scroll into view with extra padding
1120
+ if (el.scrollIntoView) {
1121
+ el.scrollIntoView({
1122
+ behavior: 'smooth',
1123
+ block: 'center'
1124
+ });
1125
+ }
1126
+ }
1127
+
1128
+ /**
1129
+ * Removes the ring light animation from an element.
1130
+ */
1131
+ function removeHighlight(el) {
1132
+ if (!el) return;
1133
+ el.classList.remove('sai-highlighted', 'sai-highlight-hard', 'sai-highlight-subtle');
1134
+ }
1135
+
1136
+ /**
1137
+ * Converts hex color to RGB object.
1138
+ * @param {string} hex
1139
+ * @returns {object|null}
1140
+ */
1141
+ function hexToRgb(hex) {
1142
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
1143
+ return result ? {
1144
+ r: parseInt(result[1], 16),
1145
+ g: parseInt(result[2], 16),
1146
+ b: parseInt(result[3], 16)
1147
+ } : null;
1148
+ }
1149
+
1048
1150
  // ─── Feature-map merge ────────────────────────────────────────────────────────
1049
1151
 
1050
1152
  /**
@@ -1078,6 +1180,7 @@ function mergeWithFeature(step) {
1078
1180
  * 1. Cross-page navigation (route prop) with automatic waiting
1079
1181
  * 2. Legacy screen navigation (screen.navigate)
1080
1182
  * 3. Element waiting (waitFor prop)
1183
+ * 4. Ring light animation application
1081
1184
  *
1082
1185
  * @param {object} step
1083
1186
  * @param {number} waitTimeout
@@ -1101,6 +1204,10 @@ function makeBeforeShowPromise(step, waitTimeout) {
1101
1204
  });
1102
1205
  } catch (_) {/* non-fatal */}
1103
1206
  await waitForElement(postNavMerge.selector, waitTimeout);
1207
+
1208
+ // Apply ring light animation to the element
1209
+ const el = document.querySelector(postNavMerge.selector);
1210
+ if (el) applyHighlight(el, step._highlightStyle || 'default');
1104
1211
  return;
1105
1212
  }
1106
1213
  }
@@ -1110,6 +1217,12 @@ function makeBeforeShowPromise(step, waitTimeout) {
1110
1217
  if (freshMerged.waitFor) {
1111
1218
  await waitForElement(freshMerged.waitFor, waitTimeout);
1112
1219
  }
1220
+
1221
+ // Apply ring light animation to the main selector
1222
+ if (freshMerged.selector) {
1223
+ const el = document.querySelector(freshMerged.selector);
1224
+ if (el) applyHighlight(el, step._highlightStyle || 'default');
1225
+ }
1113
1226
  })();
1114
1227
  }
1115
1228
 
@@ -1158,11 +1271,13 @@ function addProgressIndicator(shepherdStep, index, total) {
1158
1271
 
1159
1272
  /**
1160
1273
  * Starts a Shepherd tour from the given steps array.
1274
+ * ENHANCED: Applies ring light animations to highlighted elements.
1161
1275
  *
1162
1276
  * @param {Array} steps
1163
1277
  * @param {object} [options]
1164
1278
  * @param {boolean} [options.showProgress=true]
1165
1279
  * @param {number} [options.waitTimeout=8000]
1280
+ * @param {string} [options.highlightStyle='default'] - 'default' | 'hard' | 'subtle'
1166
1281
  */
1167
1282
  async function runTour(steps, options = {}) {
1168
1283
  var _state$config2;
@@ -1175,9 +1290,13 @@ async function runTour(steps, options = {}) {
1175
1290
  if (!(steps !== null && steps !== void 0 && steps.length)) return;
1176
1291
  const {
1177
1292
  showProgress = true,
1178
- waitTimeout = 8000
1293
+ waitTimeout = 8000,
1294
+ highlightStyle = 'default'
1179
1295
  } = options;
1180
- const mergedSteps = steps.map(mergeWithFeature);
1296
+ const mergedSteps = steps.map(step => ({
1297
+ ...mergeWithFeature(step),
1298
+ _highlightStyle: highlightStyle
1299
+ }));
1181
1300
 
1182
1301
  // Legacy: navigate to the correct screen for the first step
1183
1302
  const firstFeature = (_state$config2 = config) === null || _state$config2 === void 0 || (_state$config2 = _state$config2.features) === null || _state$config2 === void 0 ? void 0 : _state$config2.find(f => {
@@ -1243,12 +1362,24 @@ async function runTour(steps, options = {}) {
1243
1362
  });
1244
1363
  step._shepherdRef = shepherdStep;
1245
1364
  shepherdStep._isLast = isLast;
1365
+
1366
+ // Clean up highlight on hide
1367
+ shepherdStep.on('hide', () => {
1368
+ if (step.selector) {
1369
+ const el = document.querySelector(step.selector);
1370
+ if (el) removeHighlight(el);
1371
+ }
1372
+ });
1246
1373
  if (hasAuto) wireAdvanceOn(shepherdStep, step.advanceOn, tour$1);
1247
1374
  if (showProgress && expandedSteps.length > 1) {
1248
1375
  addProgressIndicator(shepherdStep, i, expandedSteps.length);
1249
1376
  }
1250
1377
  });
1251
1378
  tour$1.on('complete', () => {
1379
+ // Clean up all highlights
1380
+ document.querySelectorAll('.sai-highlighted').forEach(el => {
1381
+ removeHighlight(el);
1382
+ });
1252
1383
  runAndClearCleanups();
1253
1384
  setPausedSteps(null);
1254
1385
  setPausedIndex(0);
@@ -1259,6 +1390,11 @@ async function runTour(steps, options = {}) {
1259
1390
  tour$1.on('cancel', () => {
1260
1391
  const currentStepEl = tour$1.getCurrentStep();
1261
1392
  const currentIdx = currentStepEl ? expandedSteps.findIndex(s => s.id === currentStepEl.id) : 0;
1393
+
1394
+ // Clean up all highlights
1395
+ document.querySelectorAll('.sai-highlighted').forEach(el => {
1396
+ removeHighlight(el);
1397
+ });
1262
1398
  runAndClearCleanups();
1263
1399
  setPausedSteps(expandedSteps);
1264
1400
  setPausedIndex(Math.max(0, currentIdx));
@@ -219,6 +219,71 @@ function injectStyles(theme, pos) {
219
219
  style.textContent = `
220
220
  #sai-trigger, #sai-panel { ${buildCSSVars(theme)} }
221
221
 
222
+ /* ── Ring Light Animation ── */
223
+ @keyframes sai-ring-pulse {
224
+ 0% {
225
+ box-shadow:
226
+ 0 0 0 0 rgba(var(--sai-accent-rgb), 0.7),
227
+ 0 0 0 0 rgba(var(--sai-accent-rgb), 0.5),
228
+ 0 0 0 0 rgba(var(--sai-accent-rgb), 0.3);
229
+ }
230
+ 50% {
231
+ box-shadow:
232
+ 0 0 0 8px rgba(var(--sai-accent-rgb), 0),
233
+ 0 0 0 16px rgba(var(--sai-accent-rgb), 0.3),
234
+ 0 0 0 24px rgba(var(--sai-accent-rgb), 0);
235
+ }
236
+ 100% {
237
+ box-shadow:
238
+ 0 0 0 16px rgba(var(--sai-accent-rgb), 0),
239
+ 0 0 0 32px rgba(var(--sai-accent-rgb), 0),
240
+ 0 0 0 48px rgba(var(--sai-accent-rgb), 0);
241
+ }
242
+ }
243
+
244
+ @keyframes sai-ring-pulse-hard {
245
+ 0% {
246
+ box-shadow:
247
+ inset 0 0 0 2px var(--sai-accent),
248
+ 0 0 0 3px var(--sai-accent),
249
+ 0 0 20px var(--sai-accent);
250
+ }
251
+ 50% {
252
+ box-shadow:
253
+ inset 0 0 0 2px var(--sai-accent),
254
+ 0 0 0 8px var(--sai-accent),
255
+ 0 0 40px var(--sai-accent);
256
+ }
257
+ 100% {
258
+ box-shadow:
259
+ inset 0 0 0 2px var(--sai-accent),
260
+ 0 0 0 3px var(--sai-accent),
261
+ 0 0 20px var(--sai-accent);
262
+ }
263
+ }
264
+
265
+ @keyframes sai-element-highlight {
266
+ 0% { filter: brightness(1); }
267
+ 50% { filter: brightness(1.15); }
268
+ 100% { filter: brightness(1); }
269
+ }
270
+
271
+ /* Highlighted element styles */
272
+ .sai-highlighted {
273
+ position: relative !important;
274
+ animation: sai-ring-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
275
+ z-index: 99997 !important;
276
+ }
277
+
278
+ .sai-highlighted.sai-highlight-hard {
279
+ animation: sai-ring-pulse-hard 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
280
+ }
281
+
282
+ .sai-highlighted.sai-highlight-subtle {
283
+ animation: sai-element-highlight 2s ease-in-out infinite !important;
284
+ filter: drop-shadow(0 0 8px var(--sai-accent)) !important;
285
+ }
286
+
222
287
  /* ── Trigger ── */
223
288
  #sai-trigger {
224
289
  position: fixed; ${triggerCSS};
@@ -484,13 +549,7 @@ function buildSystemPrompt(config) {
484
549
  var _f$screen;
485
550
  return (_f$screen = f.screen) === null || _f$screen === void 0 ? void 0 : _f$screen.id;
486
551
  }).map(f => f.screen.id))];
487
-
488
- // Split features into live (current page) and ghost (other pages).
489
- // This lets the AI understand what's immediately available vs reachable
490
- // via navigation, and craft the right message when something isn't found.
491
- const liveFeatures = (config.features || []).filter(f => !f._ghost);
492
- const ghostFeatures = (config.features || []).filter(f => f._ghost);
493
- const summarise = f => {
552
+ const featureSummary = (config.features || []).map(f => {
494
553
  var _f$screen2, _f$flow;
495
554
  const entry = {
496
555
  id: f.id,
@@ -505,18 +564,15 @@ function buildSystemPrompt(config) {
505
564
  entry.note = `This feature has ${f.flow.length} sequential sub-steps. Include ONE step per flow entry.`;
506
565
  }
507
566
  return entry;
508
- };
509
- const liveSection = liveFeatures.length ? `CURRENT PAGE FEATURES (user is here now):\n${JSON.stringify(liveFeatures.map(summarise), null, 2)}` : `CURRENT PAGE FEATURES: none registered yet.`;
510
- const ghostSection = ghostFeatures.length ? `OTHER PAGE FEATURES (reachable via navigation — SDK handles it automatically):\n${JSON.stringify(ghostFeatures.map(summarise), null, 2)}` : '';
567
+ });
511
568
  return `
512
569
  You are an in-app assistant called "${config.assistantName || 'AI Guide'}" for "${config.appName}".
513
570
  Your ONLY job: guide users step-by-step through tasks using the feature map below.
514
571
 
515
- ${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. The SDK navigates automatically — just pick the right feature id.` : ''}
516
-
517
- ${liveSection}
572
+ ${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. Features are screen-specific. The SDK handles navigation — just pick the right features.` : ''}
518
573
 
519
- ${ghostSection}
574
+ FEATURE MAP (only reference IDs from this list):
575
+ ${JSON.stringify(featureSummary, null, 2)}
520
576
 
521
577
  RESPOND ONLY with this exact JSON — no markdown, no extra text:
522
578
  {
@@ -533,26 +589,16 @@ RESPOND ONLY with this exact JSON — no markdown, no extra text:
533
589
  }
534
590
 
535
591
  RULES:
536
- 1. The step "id" MUST match a feature id from the current page or other page features above.
592
+ 1. The step "id" MUST match a feature id from the feature map.
537
593
  2. Only use selectors and IDs from the feature map. Never invent them.
538
- 3. position values: top | bottom | left | right | auto only.
539
- 4. Order steps logically. For multi-step flows, order as the user encounters them.
540
- 5. For forms: ALWAYS include a step for the form section or first input BEFORE the
594
+ 3. No matching feature steps: [], explain kindly in message.
595
+ 4. position values: top | bottom | left | right | auto only.
596
+ 5. Order steps logically. For multi-step flows, order as the user encounters them.
597
+ 6. For forms: ALWAYS include a step for the form section or first input BEFORE the
541
598
  continue/submit button. The button step must always be LAST in its section.
542
- 6. If a feature has a flow, include one step per flow entry using the same feature id —
599
+ 7. If a feature has a flow, include one step per flow entry using the same feature id —
543
600
  the SDK expands them automatically.
544
- 7. Never skip features in a required sequence. Include every step end-to-end.
545
-
546
- WHEN THE USER'S REQUEST DOESN'T MATCH ANY FEATURE:
547
- - If it partially matches something (e.g. they said "create template" and there's a
548
- "template-gallery" feature): guide them to the closest matching feature and explain
549
- what it does. Don't say you can't help.
550
- - If it matches a feature on another page (ghost): include that feature's steps normally.
551
- The SDK will navigate there automatically. Do NOT tell the user to navigate manually.
552
- - If there is genuinely no match anywhere in the feature map: set steps to [] and say
553
- something like "That doesn't seem to be a feature in ${config.appName} yet. Here's
554
- what I can help you with:" then list 2-3 relevant features from the map by name.
555
- Never say you "can only guide through available features" — always offer alternatives.
601
+ 8. Never skip features in a required sequence. Include every step end-to-end.
556
602
  `.trim();
557
603
  }
558
604
 
@@ -1023,6 +1069,7 @@ function expandFlowSteps(aiStep, feature) {
1023
1069
 
1024
1070
  // Loads Shepherd.js, builds + runs tours, wires up advanceOn listeners,
1025
1071
  // progress indicators, pause/resume, and step-level error display.
1072
+ // ENHANCED: Adds animated ring light effect to highlighted elements.
1026
1073
 
1027
1074
  const SHEPHERD_JS = 'https://cdn.jsdelivr.net/npm/shepherd.js@11.2.0/dist/js/shepherd.min.js';
1028
1075
 
@@ -1043,6 +1090,61 @@ async function ensureShepherd() {
1043
1090
  await loadScript(SHEPHERD_JS);
1044
1091
  }
1045
1092
 
1093
+ // ─── Highlight management ────────────────────────────────────────────────────
1094
+
1095
+ /**
1096
+ * Applies the ring light animation to an element.
1097
+ *
1098
+ * @param {HTMLElement} el
1099
+ * @param {string} [style='default'] - 'default' | 'hard' | 'subtle'
1100
+ */
1101
+ function applyHighlight(el, style = 'default') {
1102
+ if (!el) return;
1103
+
1104
+ // Set CSS variables for accent color parsing in RGB format
1105
+ const accentColor = getComputedStyle(el).getPropertyValue('--sai-accent') || '#e94560';
1106
+ const rgb = hexToRgb(accentColor.trim());
1107
+ if (rgb) {
1108
+ el.style.setProperty('--sai-accent-rgb', `${rgb.r}, ${rgb.g}, ${rgb.b}`);
1109
+ }
1110
+ el.classList.add('sai-highlighted');
1111
+ if (style === 'hard') {
1112
+ el.classList.add('sai-highlight-hard');
1113
+ } else if (style === 'subtle') {
1114
+ el.classList.add('sai-highlight-subtle');
1115
+ }
1116
+
1117
+ // Scroll into view with extra padding
1118
+ if (el.scrollIntoView) {
1119
+ el.scrollIntoView({
1120
+ behavior: 'smooth',
1121
+ block: 'center'
1122
+ });
1123
+ }
1124
+ }
1125
+
1126
+ /**
1127
+ * Removes the ring light animation from an element.
1128
+ */
1129
+ function removeHighlight(el) {
1130
+ if (!el) return;
1131
+ el.classList.remove('sai-highlighted', 'sai-highlight-hard', 'sai-highlight-subtle');
1132
+ }
1133
+
1134
+ /**
1135
+ * Converts hex color to RGB object.
1136
+ * @param {string} hex
1137
+ * @returns {object|null}
1138
+ */
1139
+ function hexToRgb(hex) {
1140
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
1141
+ return result ? {
1142
+ r: parseInt(result[1], 16),
1143
+ g: parseInt(result[2], 16),
1144
+ b: parseInt(result[3], 16)
1145
+ } : null;
1146
+ }
1147
+
1046
1148
  // ─── Feature-map merge ────────────────────────────────────────────────────────
1047
1149
 
1048
1150
  /**
@@ -1076,6 +1178,7 @@ function mergeWithFeature(step) {
1076
1178
  * 1. Cross-page navigation (route prop) with automatic waiting
1077
1179
  * 2. Legacy screen navigation (screen.navigate)
1078
1180
  * 3. Element waiting (waitFor prop)
1181
+ * 4. Ring light animation application
1079
1182
  *
1080
1183
  * @param {object} step
1081
1184
  * @param {number} waitTimeout
@@ -1099,6 +1202,10 @@ function makeBeforeShowPromise(step, waitTimeout) {
1099
1202
  });
1100
1203
  } catch (_) {/* non-fatal */}
1101
1204
  await waitForElement(postNavMerge.selector, waitTimeout);
1205
+
1206
+ // Apply ring light animation to the element
1207
+ const el = document.querySelector(postNavMerge.selector);
1208
+ if (el) applyHighlight(el, step._highlightStyle || 'default');
1102
1209
  return;
1103
1210
  }
1104
1211
  }
@@ -1108,6 +1215,12 @@ function makeBeforeShowPromise(step, waitTimeout) {
1108
1215
  if (freshMerged.waitFor) {
1109
1216
  await waitForElement(freshMerged.waitFor, waitTimeout);
1110
1217
  }
1218
+
1219
+ // Apply ring light animation to the main selector
1220
+ if (freshMerged.selector) {
1221
+ const el = document.querySelector(freshMerged.selector);
1222
+ if (el) applyHighlight(el, step._highlightStyle || 'default');
1223
+ }
1111
1224
  })();
1112
1225
  }
1113
1226
 
@@ -1156,11 +1269,13 @@ function addProgressIndicator(shepherdStep, index, total) {
1156
1269
 
1157
1270
  /**
1158
1271
  * Starts a Shepherd tour from the given steps array.
1272
+ * ENHANCED: Applies ring light animations to highlighted elements.
1159
1273
  *
1160
1274
  * @param {Array} steps
1161
1275
  * @param {object} [options]
1162
1276
  * @param {boolean} [options.showProgress=true]
1163
1277
  * @param {number} [options.waitTimeout=8000]
1278
+ * @param {string} [options.highlightStyle='default'] - 'default' | 'hard' | 'subtle'
1164
1279
  */
1165
1280
  async function runTour(steps, options = {}) {
1166
1281
  var _state$config2;
@@ -1173,9 +1288,13 @@ async function runTour(steps, options = {}) {
1173
1288
  if (!(steps !== null && steps !== void 0 && steps.length)) return;
1174
1289
  const {
1175
1290
  showProgress = true,
1176
- waitTimeout = 8000
1291
+ waitTimeout = 8000,
1292
+ highlightStyle = 'default'
1177
1293
  } = options;
1178
- const mergedSteps = steps.map(mergeWithFeature);
1294
+ const mergedSteps = steps.map(step => ({
1295
+ ...mergeWithFeature(step),
1296
+ _highlightStyle: highlightStyle
1297
+ }));
1179
1298
 
1180
1299
  // Legacy: navigate to the correct screen for the first step
1181
1300
  const firstFeature = (_state$config2 = config) === null || _state$config2 === void 0 || (_state$config2 = _state$config2.features) === null || _state$config2 === void 0 ? void 0 : _state$config2.find(f => {
@@ -1241,12 +1360,24 @@ async function runTour(steps, options = {}) {
1241
1360
  });
1242
1361
  step._shepherdRef = shepherdStep;
1243
1362
  shepherdStep._isLast = isLast;
1363
+
1364
+ // Clean up highlight on hide
1365
+ shepherdStep.on('hide', () => {
1366
+ if (step.selector) {
1367
+ const el = document.querySelector(step.selector);
1368
+ if (el) removeHighlight(el);
1369
+ }
1370
+ });
1244
1371
  if (hasAuto) wireAdvanceOn(shepherdStep, step.advanceOn, tour$1);
1245
1372
  if (showProgress && expandedSteps.length > 1) {
1246
1373
  addProgressIndicator(shepherdStep, i, expandedSteps.length);
1247
1374
  }
1248
1375
  });
1249
1376
  tour$1.on('complete', () => {
1377
+ // Clean up all highlights
1378
+ document.querySelectorAll('.sai-highlighted').forEach(el => {
1379
+ removeHighlight(el);
1380
+ });
1250
1381
  runAndClearCleanups();
1251
1382
  setPausedSteps(null);
1252
1383
  setPausedIndex(0);
@@ -1257,6 +1388,11 @@ async function runTour(steps, options = {}) {
1257
1388
  tour$1.on('cancel', () => {
1258
1389
  const currentStepEl = tour$1.getCurrentStep();
1259
1390
  const currentIdx = currentStepEl ? expandedSteps.findIndex(s => s.id === currentStepEl.id) : 0;
1391
+
1392
+ // Clean up all highlights
1393
+ document.querySelectorAll('.sai-highlighted').forEach(el => {
1394
+ removeHighlight(el);
1395
+ });
1260
1396
  runAndClearCleanups();
1261
1397
  setPausedSteps(expandedSteps);
1262
1398
  setPausedIndex(Math.max(0, currentIdx));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventop/sdk",
3
- "version": "1.2.14",
3
+ "version": "1.3.0",
4
4
  "description": "AI-powered guided tours for any web app. Drop-in, themeable, provider-agnostic.",
5
5
  "keywords": [
6
6
  "onboarding",