@syntrologie/adapt-faq 2.16.0 → 2.18.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.
Files changed (43) hide show
  1. package/dist/chunk-5WRI5ZAA.js +31 -0
  2. package/dist/chunk-5WRI5ZAA.js.map +7 -0
  3. package/dist/chunk-S6WIENQP.js +578 -0
  4. package/dist/chunk-S6WIENQP.js.map +7 -0
  5. package/dist/editor.d.ts +35 -33
  6. package/dist/editor.d.ts.map +1 -1
  7. package/dist/editor.js +4821 -308
  8. package/dist/editor.js.map +7 -0
  9. package/dist/runtime.d.ts +3 -5
  10. package/dist/runtime.d.ts.map +1 -1
  11. package/dist/runtime.js +848 -91
  12. package/dist/runtime.js.map +7 -0
  13. package/dist/schema.d.ts +609 -77
  14. package/dist/schema.d.ts.map +1 -1
  15. package/dist/schema.js +444 -206
  16. package/dist/schema.js.map +7 -0
  17. package/dist/types.d.ts +19 -0
  18. package/dist/types.d.ts.map +1 -1
  19. package/package.json +4 -20
  20. package/dist/FAQWidget.d.ts +0 -33
  21. package/dist/FAQWidget.d.ts.map +0 -1
  22. package/dist/FAQWidget.js +0 -375
  23. package/dist/FAQWidgetLit.js +0 -534
  24. package/dist/cdn.d.ts +0 -70
  25. package/dist/cdn.d.ts.map +0 -1
  26. package/dist/cdn.js +0 -46
  27. package/dist/editor-lit.d.ts +0 -37
  28. package/dist/editor-lit.d.ts.map +0 -1
  29. package/dist/editor-lit.js +0 -195
  30. package/dist/executors.js +0 -150
  31. package/dist/faq-styles.js +0 -204
  32. package/dist/faq-types.js +0 -7
  33. package/dist/runtime-lit.d.ts +0 -85
  34. package/dist/runtime-lit.d.ts.map +0 -1
  35. package/dist/runtime-lit.js +0 -94
  36. package/dist/state.js +0 -132
  37. package/dist/summarize.js +0 -62
  38. package/dist/types.js +0 -17
  39. package/node_modules/@syntrologie/sdk-contracts/dist/index.d.ts +0 -129
  40. package/node_modules/@syntrologie/sdk-contracts/dist/index.js +0 -17
  41. package/node_modules/@syntrologie/sdk-contracts/dist/schemas.d.ts +0 -2296
  42. package/node_modules/@syntrologie/sdk-contracts/dist/schemas.js +0 -361
  43. package/node_modules/@syntrologie/sdk-contracts/package.json +0 -33
package/dist/runtime.js CHANGED
@@ -1,94 +1,851 @@
1
- /**
2
- * Adaptive FAQ - Runtime Module
3
- *
4
- * Runtime manifest for the FAQ accordion adaptive.
5
- * Provides action executors and widget registration.
6
- */
7
- import { executorDefinitions } from './executors';
8
- import { FAQMountableWidget } from './FAQWidget';
9
- import './FAQWidgetLit'; // registers <syntro-faq-accordion> custom element
10
- // ============================================================================
11
- // Lit Mountable Widget
12
- // ============================================================================
13
- /**
14
- * Mountable widget interface for <syntro-faq-accordion> (Lit web component).
15
- *
16
- * Mirrors FAQMountableWidget but mounts the Lit element instead of a React
17
- * root no React dependency required at mount time.
18
- */
19
- export const FAQWidgetLitMountable = {
20
- mount(container, config) {
21
- const { runtime, instanceId = 'faq-widget', ...faqConfig } = config ?? {
22
- expandBehavior: 'single',
23
- searchable: false,
24
- theme: 'auto',
25
- actions: [],
26
- };
27
- const el = document.createElement('syntro-faq-accordion');
28
- Object.assign(el, {
29
- faqConfig: faqConfig,
30
- runtime: runtime ?? null,
31
- instanceId,
32
- });
33
- container.appendChild(el);
34
- return () => el.remove();
35
- },
1
+ import {
2
+ purple,
3
+ slateGrey
4
+ } from "./chunk-S6WIENQP.js";
5
+ import "./chunk-5WRI5ZAA.js";
6
+
7
+ // src/executors.ts
8
+ function resolveItem(store, itemId, itemQuestion) {
9
+ if (itemId) {
10
+ const found = store.getState().items.find((i) => i.config.id === itemId);
11
+ if (found) return found;
12
+ }
13
+ if (itemQuestion) {
14
+ const found = store.findByQuestion(itemQuestion);
15
+ if (found) return found;
16
+ }
17
+ throw new Error("FAQ item not found");
18
+ }
19
+ async function executeScrollToFaq(action, context, store) {
20
+ const item = resolveItem(store, action.itemId, action.itemQuestion);
21
+ const { id } = item.config;
22
+ if (action.expand !== false) {
23
+ store.expand(id);
24
+ }
25
+ const el = document.querySelector(`[data-faq-item-id="${id}"]`);
26
+ if (el) {
27
+ el.scrollIntoView({
28
+ behavior: action.behavior ?? "smooth"
29
+ });
30
+ }
31
+ context.publishEvent("faq:scroll_to", { itemId: id });
32
+ return {
33
+ cleanup: () => {
34
+ }
35
+ };
36
+ }
37
+ async function executeToggleFaqItem(action, context, store) {
38
+ const item = resolveItem(store, action.itemId, action.itemQuestion);
39
+ const { id } = item.config;
40
+ const desiredState = action.state ?? "toggle";
41
+ let newState;
42
+ switch (desiredState) {
43
+ case "open":
44
+ store.expand(id);
45
+ newState = "open";
46
+ break;
47
+ case "closed":
48
+ store.collapse(id);
49
+ newState = "closed";
50
+ break;
51
+ default: {
52
+ const wasExpanded = store.getState().expandedItems.has(id);
53
+ store.toggle(id);
54
+ newState = wasExpanded ? "closed" : "open";
55
+ break;
56
+ }
57
+ }
58
+ context.publishEvent("faq:toggle", { itemId: id, newState });
59
+ return {
60
+ cleanup: () => {
61
+ }
62
+ };
63
+ }
64
+ async function executeUpdateFaq(action, context, store) {
65
+ switch (action.operation) {
66
+ case "add": {
67
+ const items = action.items ?? [];
68
+ const position = action.position === "prepend" ? "prepend" : "append";
69
+ store.addItems(items, position);
70
+ break;
71
+ }
72
+ case "remove": {
73
+ if (!action.itemId) {
74
+ throw new Error("FAQ item not found");
75
+ }
76
+ const exists = store.getState().items.some((i) => i.config.id === action.itemId);
77
+ if (!exists) {
78
+ throw new Error("FAQ item not found");
79
+ }
80
+ store.removeItem(action.itemId);
81
+ break;
82
+ }
83
+ case "reorder": {
84
+ const order = action.order ?? [];
85
+ store.reorderItems(order);
86
+ break;
87
+ }
88
+ case "replace": {
89
+ const items = action.items ?? [];
90
+ store.replaceItems(items);
91
+ break;
92
+ }
93
+ }
94
+ context.publishEvent("faq:update", { operation: action.operation });
95
+ return {
96
+ cleanup: () => {
97
+ }
98
+ };
99
+ }
100
+ var executorDefinitions = [
101
+ { kind: "faq:scroll_to", executor: executeScrollToFaq },
102
+ { kind: "faq:toggle_item", executor: executeToggleFaqItem },
103
+ { kind: "faq:update", executor: executeUpdateFaq }
104
+ ];
105
+
106
+ // src/FAQWidgetLit.ts
107
+ import { html, LitElement, nothing } from "lit";
108
+ import { styleMap } from "lit/directives/style-map.js";
109
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
110
+ import { Marked } from "marked";
111
+
112
+ // src/faq-styles.ts
113
+ var baseStyles = {
114
+ container: {
115
+ fontFamily: "var(--sc-font-family, system-ui, -apple-system, sans-serif)",
116
+ maxWidth: "800px",
117
+ margin: "0 auto"
118
+ },
119
+ searchWrapper: {
120
+ marginBottom: "8px"
121
+ },
122
+ searchInput: {
123
+ width: "100%",
124
+ padding: "12px 16px",
125
+ borderRadius: "8px",
126
+ fontSize: "14px",
127
+ outline: "none",
128
+ transition: "border-color 0.15s ease",
129
+ backgroundColor: "var(--sc-content-search-background)",
130
+ color: "var(--sc-content-search-color)"
131
+ },
132
+ accordion: {
133
+ display: "flex",
134
+ flexDirection: "column",
135
+ gap: "var(--sc-content-item-gap, 6px)"
136
+ },
137
+ item: {
138
+ borderRadius: "var(--sc-content-border-radius, 8px)",
139
+ overflow: "hidden",
140
+ transition: "box-shadow 0.15s ease"
141
+ },
142
+ question: {
143
+ width: "100%",
144
+ padding: "var(--sc-content-item-padding, 12px 16px)",
145
+ display: "flex",
146
+ alignItems: "center",
147
+ justifyContent: "space-between",
148
+ border: "none",
149
+ cursor: "pointer",
150
+ fontSize: "var(--sc-content-item-font-size, 15px)",
151
+ fontWeight: 500,
152
+ textAlign: "left",
153
+ transition: "background-color 0.15s ease"
154
+ },
155
+ chevron: {
156
+ fontSize: "20px",
157
+ transition: "transform 0.2s ease",
158
+ color: "var(--sc-content-chevron-color, currentColor)"
159
+ },
160
+ answer: {
161
+ padding: "var(--sc-content-body-padding, 0 16px 12px 16px)",
162
+ fontSize: "var(--sc-content-body-font-size, 14px)",
163
+ lineHeight: 1.6,
164
+ overflow: "hidden",
165
+ transition: "max-height 0.2s ease, padding 0.2s ease"
166
+ },
167
+ category: {
168
+ display: "inline-block",
169
+ fontSize: "11px",
170
+ fontWeight: 600,
171
+ textTransform: "uppercase",
172
+ letterSpacing: "0.05em",
173
+ padding: "4px 8px",
174
+ borderRadius: "4px",
175
+ marginBottom: "8px"
176
+ },
177
+ categoryHeader: {
178
+ fontSize: "var(--sc-content-category-font-size, 12px)",
179
+ fontWeight: 700,
180
+ textTransform: "uppercase",
181
+ letterSpacing: "0.05em",
182
+ padding: "var(--sc-content-category-padding, 8px 4px 4px 4px)",
183
+ marginTop: "var(--sc-content-category-gap, 4px)"
184
+ },
185
+ feedback: {
186
+ display: "flex",
187
+ alignItems: "center",
188
+ gap: "8px",
189
+ marginTop: "12px",
190
+ paddingTop: "10px",
191
+ borderTop: "1px solid rgba(0, 0, 0, 0.08)",
192
+ fontSize: "13px"
193
+ },
194
+ feedbackButton: {
195
+ background: "none",
196
+ border: "1px solid transparent",
197
+ cursor: "pointer",
198
+ fontSize: "16px",
199
+ padding: "4px 8px",
200
+ borderRadius: "4px",
201
+ transition: "background-color 0.15s ease, border-color 0.15s ease"
202
+ },
203
+ feedbackButtonSelected: {
204
+ borderColor: "rgba(0, 0, 0, 0.2)",
205
+ backgroundColor: "rgba(0, 0, 0, 0.04)"
206
+ },
207
+ emptyState: {
208
+ textAlign: "center",
209
+ padding: "48px 24px",
210
+ fontSize: "14px"
211
+ },
212
+ noResults: {
213
+ textAlign: "center",
214
+ padding: "32px 16px",
215
+ fontSize: "14px"
216
+ }
36
217
  };
37
- // ============================================================================
38
- // App Runtime Manifest
39
- // ============================================================================
40
- /**
41
- * Runtime manifest for adaptive-faq.
42
- *
43
- * Provides:
44
- * - FAQ action executors (scroll_to, toggle_item, update)
45
- * - Widget-based accordion (compositional faq:question actions)
46
- */
47
- export const runtime = {
48
- id: 'adaptive-faq',
49
- version: '2.0.0',
50
- name: 'FAQ Accordion',
51
- description: 'Collapsible Q&A accordion with actions, rich content, feedback, and personalization',
52
- /**
53
- * Action executors for programmatic FAQ interaction.
54
- */
55
- executors: executorDefinitions,
56
- /**
57
- * Widget definitions for the runtime's WidgetRegistry.
58
- */
59
- widgets: [
60
- {
61
- id: 'adaptive-faq:accordion',
62
- component: FAQMountableWidget,
63
- metadata: {
64
- name: 'FAQ Accordion',
65
- description: 'Collapsible Q&A accordion with search, categories, and feedback',
66
- icon: '❓',
67
- subtitle: 'Curated just for you.',
68
- },
69
- },
70
- ],
71
- /**
72
- * Extract notify watcher entries from tile config props.
73
- * The runtime evaluates these continuously (even with drawer closed)
74
- * and publishes faq:question_revealed when triggerWhen transitions false → true.
75
- */
76
- notifyWatchers(props) {
77
- const actions = (props.actions ?? []);
78
- return actions
79
- .filter((a) => a.notify && a.triggerWhen)
80
- .map((a) => ({
81
- id: `faq:${a.config.id}`,
82
- strategy: a.triggerWhen,
83
- eventName: 'faq:question_revealed',
84
- eventProps: {
85
- questionId: a.config.id,
86
- question: a.config.question,
87
- title: a.notify.title,
88
- body: a.notify.body,
89
- icon: a.notify.icon,
90
- },
91
- }));
218
+ var themeStyles = {
219
+ light: {
220
+ container: {
221
+ backgroundColor: "transparent",
222
+ color: "inherit"
223
+ },
224
+ searchInput: {
225
+ border: `1px solid ${slateGrey[11]}`
226
+ },
227
+ item: {
228
+ backgroundColor: "var(--sc-content-background)",
229
+ borderTop: "var(--sc-content-border)",
230
+ borderRight: "var(--sc-content-border)",
231
+ borderBottom: "var(--sc-content-border)",
232
+ borderLeft: "var(--sc-content-border)"
233
+ },
234
+ itemExpanded: {
235
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.08)"
236
+ },
237
+ question: {
238
+ backgroundColor: "transparent",
239
+ color: "var(--sc-content-text-color)"
240
+ },
241
+ questionHover: {
242
+ backgroundColor: "var(--sc-content-background-hover)"
243
+ },
244
+ answer: {
245
+ color: "var(--sc-content-text-secondary-color)"
246
+ },
247
+ category: {
248
+ backgroundColor: purple[8],
249
+ color: purple[2]
250
+ },
251
+ categoryHeader: {
252
+ color: slateGrey[7]
253
+ },
254
+ emptyState: {
255
+ color: slateGrey[8]
256
+ },
257
+ feedbackPrompt: {
258
+ color: slateGrey[7]
259
+ }
260
+ },
261
+ dark: {
262
+ container: {
263
+ backgroundColor: "transparent",
264
+ color: "inherit"
265
+ },
266
+ searchInput: {
267
+ border: `1px solid ${slateGrey[5]}`
268
+ },
269
+ item: {
270
+ backgroundColor: "var(--sc-content-background)",
271
+ borderTop: "var(--sc-content-border)",
272
+ borderRight: "var(--sc-content-border)",
273
+ borderBottom: "var(--sc-content-border)",
274
+ borderLeft: "var(--sc-content-border)"
92
275
  },
276
+ itemExpanded: {
277
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)"
278
+ },
279
+ question: {
280
+ backgroundColor: "transparent",
281
+ color: "var(--sc-content-text-color)"
282
+ },
283
+ questionHover: {
284
+ backgroundColor: "var(--sc-content-background-hover)"
285
+ },
286
+ answer: {
287
+ color: "var(--sc-content-text-secondary-color)"
288
+ },
289
+ category: {
290
+ backgroundColor: purple[0],
291
+ color: purple[6]
292
+ },
293
+ categoryHeader: {
294
+ color: slateGrey[8]
295
+ },
296
+ emptyState: {
297
+ color: slateGrey[7]
298
+ },
299
+ feedbackPrompt: {
300
+ color: slateGrey[8]
301
+ }
302
+ }
303
+ };
304
+
305
+ // src/FAQWidgetLit.ts
306
+ function sm(styles) {
307
+ return styles;
308
+ }
309
+ var marked = new Marked({ async: false, gfm: true, breaks: true });
310
+ function getAnswerText(answer) {
311
+ if (typeof answer === "string") return answer;
312
+ if (answer.type === "rich") return answer.html;
313
+ return answer.content;
314
+ }
315
+ function renderAnswerHtml(answer) {
316
+ if (typeof answer === "string") {
317
+ return marked.parse(answer);
318
+ }
319
+ if (answer.type === "rich") {
320
+ return answer.html;
321
+ }
322
+ return marked.parse(answer.content);
323
+ }
324
+ function resolveFeedbackConfig(feedback) {
325
+ if (!feedback) return null;
326
+ if (feedback === true) return { style: "thumbs" };
327
+ return feedback;
328
+ }
329
+ function getFeedbackPrompt(feedbackConfig) {
330
+ return feedbackConfig.prompt ?? "Was this helpful?";
331
+ }
332
+ function resolveTheme(theme) {
333
+ if (theme && theme !== "auto") return theme;
334
+ if (typeof window !== "undefined") {
335
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
336
+ }
337
+ return "light";
338
+ }
339
+ var FAQAccordionElement = class extends LitElement {
340
+ constructor() {
341
+ super(...arguments);
342
+ // -----------------------------------------------------------------------
343
+ // Property declarations
344
+ // -----------------------------------------------------------------------
345
+ this.faqConfig = {
346
+ expandBehavior: "single",
347
+ searchable: false,
348
+ theme: "auto",
349
+ actions: []
350
+ };
351
+ this.runtime = null;
352
+ this.instanceId = "faq-widget";
353
+ // Internal state
354
+ this._expandedIds = /* @__PURE__ */ new Set();
355
+ this._highlightId = null;
356
+ this._searchQuery = "";
357
+ this._feedbackState = /* @__PURE__ */ new Map();
358
+ this._hoveredId = null;
359
+ // Subscription cleanup handles
360
+ this._unsubContext = null;
361
+ this._unsubAccumulator = null;
362
+ this._unsubCta = null;
363
+ this._unsubDeepLink = null;
364
+ this._unsubSessionMetrics = null;
365
+ this._highlightTimer = null;
366
+ }
367
+ // -----------------------------------------------------------------------
368
+ // Light DOM — no Shadow DOM so CSS variables from the host page apply
369
+ // -----------------------------------------------------------------------
370
+ createRenderRoot() {
371
+ return this;
372
+ }
373
+ // -----------------------------------------------------------------------
374
+ // Lifecycle
375
+ // -----------------------------------------------------------------------
376
+ connectedCallback() {
377
+ super.connectedCallback();
378
+ this._subscribeAll();
379
+ }
380
+ disconnectedCallback() {
381
+ super.disconnectedCallback();
382
+ this._unsubscribeAll();
383
+ if (this._highlightTimer !== null) {
384
+ clearTimeout(this._highlightTimer);
385
+ this._highlightTimer = null;
386
+ }
387
+ }
388
+ // Re-subscribe when runtime changes (property may be set after connectedCallback)
389
+ updated(changedProps) {
390
+ if (changedProps.has("runtime")) {
391
+ this._unsubscribeAll();
392
+ this._subscribeAll();
393
+ }
394
+ }
395
+ // -----------------------------------------------------------------------
396
+ // Subscription management
397
+ // -----------------------------------------------------------------------
398
+ _subscribeAll() {
399
+ if (!this.runtime) return;
400
+ this._unsubContext = this.runtime.context.subscribe(() => {
401
+ this.requestUpdate();
402
+ });
403
+ if (this.runtime.accumulator?.subscribe) {
404
+ this._unsubAccumulator = this.runtime.accumulator.subscribe(() => {
405
+ this.requestUpdate();
406
+ });
407
+ }
408
+ if (this.runtime.sessionMetrics?.subscribe) {
409
+ this._unsubSessionMetrics = this.runtime.sessionMetrics.subscribe(() => {
410
+ this.requestUpdate();
411
+ });
412
+ }
413
+ if (this.runtime.events.subscribe) {
414
+ if (this.runtime.events.getRecent) {
415
+ const recentEvents = this.runtime.events.getRecent(
416
+ { patterns: ["^action\\.tooltip_cta_clicked$", "^action\\.modal_cta_clicked$"] },
417
+ 10
418
+ );
419
+ const pendingEvent = recentEvents.filter((e) => {
420
+ const actionId = e.props?.actionId;
421
+ return typeof actionId === "string" && actionId.startsWith("faq:open:");
422
+ }).pop();
423
+ if (pendingEvent && Date.now() - pendingEvent.ts < 1e4) {
424
+ const questionId = pendingEvent.props.actionId.replace("faq:open:", "");
425
+ this._expandedIds = /* @__PURE__ */ new Set([questionId]);
426
+ requestAnimationFrame(() => {
427
+ const el = document.querySelector(`[data-faq-item-id="${questionId}"]`);
428
+ if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
429
+ });
430
+ }
431
+ }
432
+ this._unsubCta = this.runtime.events.subscribe(
433
+ { patterns: ["^action\\.tooltip_cta_clicked$", "^action\\.modal_cta_clicked$"] },
434
+ (event) => {
435
+ const actionId = event.props?.actionId;
436
+ if (typeof actionId !== "string" || !actionId.startsWith("faq:open:")) return;
437
+ const questionId = actionId.replace("faq:open:", "");
438
+ this._expandedIds = /* @__PURE__ */ new Set([questionId]);
439
+ requestAnimationFrame(() => {
440
+ const el = document.querySelector(`[data-faq-item-id="${questionId}"]`);
441
+ if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
442
+ });
443
+ this.runtime?.events.publish("canvas.requestOpen");
444
+ }
445
+ );
446
+ }
447
+ if (this.runtime.events.subscribe) {
448
+ const handleDeepLink = (event) => {
449
+ const tileId = event.props?.tileId;
450
+ const itemId = event.props?.itemId;
451
+ if (tileId !== this.instanceId) return;
452
+ if (!itemId) return;
453
+ this._expandedIds = /* @__PURE__ */ new Set([itemId]);
454
+ this._highlightId = itemId;
455
+ if (this._highlightTimer !== null) clearTimeout(this._highlightTimer);
456
+ this._highlightTimer = setTimeout(() => {
457
+ this._highlightId = null;
458
+ this._highlightTimer = null;
459
+ }, 1500);
460
+ requestAnimationFrame(() => {
461
+ const el = document.querySelector(`[data-faq-item-id="${itemId}"]`);
462
+ if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
463
+ });
464
+ };
465
+ if (this.runtime.events.getRecent) {
466
+ const recent = this.runtime.events.getRecent({ names: ["notification.deep_link"] }, 5);
467
+ const pending = recent.filter((e) => e.props?.tileId === this.instanceId && e.props?.itemId).pop();
468
+ if (pending && Date.now() - pending.ts < 1e4) {
469
+ handleDeepLink(pending);
470
+ }
471
+ }
472
+ this._unsubDeepLink = this.runtime.events.subscribe(
473
+ { names: ["notification.deep_link"] },
474
+ handleDeepLink
475
+ );
476
+ }
477
+ }
478
+ _unsubscribeAll() {
479
+ this._unsubContext?.();
480
+ this._unsubAccumulator?.();
481
+ this._unsubSessionMetrics?.();
482
+ this._unsubCta?.();
483
+ this._unsubDeepLink?.();
484
+ this._unsubContext = null;
485
+ this._unsubAccumulator = null;
486
+ this._unsubSessionMetrics = null;
487
+ this._unsubCta = null;
488
+ this._unsubDeepLink = null;
489
+ }
490
+ // -----------------------------------------------------------------------
491
+ // Handlers
492
+ // -----------------------------------------------------------------------
493
+ _handleToggle(id) {
494
+ const prev = this._expandedIds;
495
+ let next;
496
+ if (this.faqConfig.expandBehavior === "single") {
497
+ next = prev.has(id) ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([id]);
498
+ } else {
499
+ next = new Set(prev);
500
+ if (prev.has(id)) {
501
+ next.delete(id);
502
+ } else {
503
+ next.add(id);
504
+ }
505
+ }
506
+ const willBeExpanded = !prev.has(id);
507
+ this._expandedIds = next;
508
+ this.runtime?.events.publish("faq:toggled", {
509
+ instanceId: this.instanceId,
510
+ questionId: id,
511
+ expanded: willBeExpanded,
512
+ timestamp: Date.now()
513
+ });
514
+ }
515
+ _handleFeedback(itemId, question, value) {
516
+ const next = new Map(this._feedbackState);
517
+ next.set(itemId, value);
518
+ this._feedbackState = next;
519
+ this.runtime?.events.publish("faq:feedback", { itemId, question, value });
520
+ }
521
+ // -----------------------------------------------------------------------
522
+ // Computed helpers
523
+ // -----------------------------------------------------------------------
524
+ _visibleQuestions() {
525
+ return (this.faqConfig.actions ?? []).filter((q) => {
526
+ if (!q.triggerWhen) return true;
527
+ if (!this.runtime) return true;
528
+ const result = this.runtime.evaluateSync(q.triggerWhen);
529
+ return result.value;
530
+ });
531
+ }
532
+ _orderedQuestions(visible) {
533
+ if (this.faqConfig.ordering === "priority") {
534
+ return [...visible].sort((a, b) => (b.config.priority ?? 0) - (a.config.priority ?? 0));
535
+ }
536
+ return visible;
537
+ }
538
+ _filteredQuestions(ordered) {
539
+ const q = this._searchQuery.trim().toLowerCase();
540
+ if (!this.faqConfig.searchable || !q) return ordered;
541
+ return ordered.filter(
542
+ (item) => item.config.question.toLowerCase().includes(q) || getAnswerText(item.config.answer).toLowerCase().includes(q) || item.config.category?.toLowerCase().includes(q)
543
+ );
544
+ }
545
+ _categoryGroups(filtered) {
546
+ const groups = /* @__PURE__ */ new Map();
547
+ for (const item of filtered) {
548
+ const cat = item.config.category;
549
+ if (!groups.has(cat)) groups.set(cat, []);
550
+ groups.get(cat).push(item);
551
+ }
552
+ return groups;
553
+ }
554
+ // -----------------------------------------------------------------------
555
+ // Render helpers
556
+ // -----------------------------------------------------------------------
557
+ _renderAnswer(answer) {
558
+ const html_str = renderAnswerHtml(answer);
559
+ return html`<div style="margin:0" data-faq-markdown="">${unsafeHTML(html_str)}</div>`;
560
+ }
561
+ _renderFeedback(item, feedbackConfig, feedbackValue, theme) {
562
+ const colors = themeStyles[theme];
563
+ const feedbackStyle = { ...baseStyles.feedback, ...colors.feedbackPrompt };
564
+ return html`
565
+ <div style=${styleMap(sm(feedbackStyle))}>
566
+ <span>${getFeedbackPrompt(feedbackConfig)}</span>
567
+ <button
568
+ type="button"
569
+ style=${styleMap(
570
+ sm({
571
+ ...baseStyles.feedbackButton,
572
+ ...feedbackValue === "up" ? baseStyles.feedbackButtonSelected : {}
573
+ })
574
+ )}
575
+ aria-label="Thumbs up"
576
+ @click=${() => this._handleFeedback(item.config.id, item.config.question, "up")}
577
+ >\uD83D\uDC4D</button>
578
+ <button
579
+ type="button"
580
+ style=${styleMap(
581
+ sm({
582
+ ...baseStyles.feedbackButton,
583
+ ...feedbackValue === "down" ? baseStyles.feedbackButtonSelected : {}
584
+ })
585
+ )}
586
+ aria-label="Thumbs down"
587
+ @click=${() => this._handleFeedback(item.config.id, item.config.question, "down")}
588
+ >\uD83D\uDC4E</button>
589
+ </div>
590
+ `;
591
+ }
592
+ _renderItem(item, isLast, theme, feedbackConfig) {
593
+ const colors = themeStyles[theme];
594
+ const isExpanded = this._expandedIds.has(item.config.id);
595
+ const isHighlighted = this._highlightId === item.config.id;
596
+ const isHovered = this._hoveredId === item.config.id;
597
+ const itemStyle = {
598
+ ...baseStyles.item,
599
+ ...colors.item,
600
+ ...isExpanded ? colors.itemExpanded : {},
601
+ ...isHighlighted ? {
602
+ boxShadow: `0 0 0 2px ${purple[4]}, 0 0 12px rgba(106, 89, 206, 0.4)`,
603
+ transition: "box-shadow 0.3s ease"
604
+ } : {},
605
+ ...!isLast ? { borderBottom: "var(--sc-content-item-divider, none)" } : {}
606
+ };
607
+ const questionStyle = {
608
+ ...baseStyles.question,
609
+ ...colors.question,
610
+ ...isHovered ? colors.questionHover : {}
611
+ };
612
+ const chevronStyle = {
613
+ ...baseStyles.chevron,
614
+ transform: isExpanded ? "rotate(90deg)" : "rotate(0deg)"
615
+ };
616
+ const answerStyle = {
617
+ ...baseStyles.answer,
618
+ ...colors.answer,
619
+ maxHeight: isExpanded ? "500px" : "0",
620
+ paddingBottom: isExpanded ? "16px" : "0"
621
+ };
622
+ return html`
623
+ <div
624
+ style=${styleMap(sm(itemStyle))}
625
+ data-faq-item-id=${item.config.id}
626
+ >
627
+ <button
628
+ type="button"
629
+ style=${styleMap(sm(questionStyle))}
630
+ aria-expanded=${isExpanded}
631
+ @click=${() => this._handleToggle(item.config.id)}
632
+ @mouseenter=${() => {
633
+ this._hoveredId = item.config.id;
634
+ }}
635
+ @mouseleave=${() => {
636
+ this._hoveredId = null;
637
+ }}
638
+ >
639
+ <span>${item.config.question}</span>
640
+ <span style=${styleMap(sm(chevronStyle))}>\u203A</span>
641
+ </button>
642
+
643
+ <div
644
+ style=${styleMap(sm(answerStyle))}
645
+ aria-hidden=${!isExpanded}
646
+ >
647
+ ${this._renderAnswer(item.config.answer)}
648
+ ${isExpanded && feedbackConfig ? this._renderFeedback(
649
+ item,
650
+ feedbackConfig,
651
+ this._feedbackState.get(item.config.id),
652
+ theme
653
+ ) : nothing}
654
+ </div>
655
+ </div>
656
+ `;
657
+ }
658
+ _renderItems(items, theme, feedbackConfig) {
659
+ return items.map(
660
+ (item, index) => this._renderItem(item, index === items.length - 1, theme, feedbackConfig)
661
+ );
662
+ }
663
+ // -----------------------------------------------------------------------
664
+ // Render
665
+ // -----------------------------------------------------------------------
666
+ render() {
667
+ const theme = resolveTheme(this.faqConfig.theme);
668
+ const colors = themeStyles[theme];
669
+ const feedbackConfig = resolveFeedbackConfig(this.faqConfig.feedback);
670
+ const visible = this._visibleQuestions();
671
+ const ordered = this._orderedQuestions(visible);
672
+ const filtered = this._filteredQuestions(ordered);
673
+ const hasCategories = filtered.some((q) => q.config.category);
674
+ const groups = hasCategories ? this._categoryGroups(filtered) : null;
675
+ const containerStyle = {
676
+ ...baseStyles.container,
677
+ ...colors.container
678
+ };
679
+ const emptyStateStyle = {
680
+ ...baseStyles.emptyState,
681
+ ...colors.emptyState
682
+ };
683
+ const categoryHeaderStyle = {
684
+ ...baseStyles.categoryHeader,
685
+ ...colors.categoryHeader
686
+ };
687
+ const searchInputStyle = {
688
+ ...baseStyles.searchInput,
689
+ ...colors.searchInput
690
+ };
691
+ if (visible.length === 0) {
692
+ return html`
693
+ <div
694
+ style=${styleMap(sm(containerStyle))}
695
+ data-adaptive-id=${this.instanceId}
696
+ data-adaptive-type="adaptive-faq"
697
+ >
698
+ <div style=${styleMap(sm(emptyStateStyle))}>
699
+ You're all set for now! We'll surface answers here when they're relevant to what
700
+ you're doing.
701
+ </div>
702
+ </div>
703
+ `;
704
+ }
705
+ return html`
706
+ <div
707
+ style=${styleMap(sm(containerStyle))}
708
+ data-adaptive-id=${this.instanceId}
709
+ data-adaptive-type="adaptive-faq"
710
+ >
711
+ ${this.faqConfig.searchable ? html`
712
+ <div style=${styleMap(sm(baseStyles.searchWrapper))}>
713
+ <style>
714
+ [data-adaptive-id="${this.instanceId}"] input::placeholder {
715
+ color: var(--sc-content-search-color, inherit);
716
+ opacity: 0.7;
717
+ }
718
+ </style>
719
+ <input
720
+ type="text"
721
+ placeholder="Search questions..."
722
+ .value=${this._searchQuery}
723
+ style=${styleMap(sm(searchInputStyle))}
724
+ @input=${(e) => {
725
+ this._searchQuery = e.target.value;
726
+ }}
727
+ />
728
+ </div>
729
+ ` : nothing}
730
+
731
+ <div style=${styleMap(sm(baseStyles.accordion))}>
732
+ ${groups ? Array.from(groups.entries()).map(
733
+ ([category, items]) => html`
734
+ ${category ? html`
735
+ <div
736
+ style=${styleMap(sm(categoryHeaderStyle))}
737
+ data-category-header=${category}
738
+ >
739
+ ${category}
740
+ </div>
741
+ ` : nothing}
742
+ ${this._renderItems(items, theme, feedbackConfig)}
743
+ `
744
+ ) : this._renderItems(filtered, theme, feedbackConfig)}
745
+ </div>
746
+
747
+ ${this.faqConfig.searchable && filtered.length === 0 && this._searchQuery ? html`
748
+ <div
749
+ style=${styleMap(sm({ ...baseStyles.noResults, ...colors.emptyState }))}
750
+ >
751
+ No questions found matching &quot;${this._searchQuery}&quot;
752
+ </div>
753
+ ` : nothing}
754
+ </div>
755
+ `;
756
+ }
757
+ };
758
+ // -----------------------------------------------------------------------
759
+ // Reactive properties (no decorators — tsconfig forbids experimentalDecorators)
760
+ // -----------------------------------------------------------------------
761
+ FAQAccordionElement.properties = {
762
+ // Public API — set from the outside
763
+ faqConfig: { attribute: false },
764
+ runtime: { attribute: false },
765
+ instanceId: { type: String },
766
+ // Internal reactive state (prefixed with _ to signal "private")
767
+ _expandedIds: { state: true },
768
+ _highlightId: { state: true },
769
+ _searchQuery: { state: true },
770
+ _feedbackState: { state: true },
771
+ _hoveredId: { state: true }
772
+ };
773
+ if (!customElements.get("syntro-faq-accordion")) {
774
+ customElements.define("syntro-faq-accordion", FAQAccordionElement);
775
+ }
776
+
777
+ // src/runtime.ts
778
+ var FAQWidgetLitMountable = {
779
+ mount(container, config) {
780
+ const {
781
+ runtime: runtime2,
782
+ instanceId = "faq-widget",
783
+ ...faqConfig
784
+ } = config ?? {
785
+ expandBehavior: "single",
786
+ searchable: false,
787
+ theme: "auto",
788
+ actions: []
789
+ };
790
+ const el = document.createElement("syntro-faq-accordion");
791
+ Object.assign(el, {
792
+ faqConfig,
793
+ runtime: runtime2 ?? null,
794
+ instanceId
795
+ });
796
+ container.appendChild(el);
797
+ return () => el.remove();
798
+ }
799
+ };
800
+ var runtime = {
801
+ id: "adaptive-faq",
802
+ version: "2.0.0",
803
+ name: "FAQ Accordion",
804
+ description: "Collapsible Q&A accordion with actions, rich content, feedback, and personalization",
805
+ /**
806
+ * Action executors for programmatic FAQ interaction.
807
+ */
808
+ executors: executorDefinitions,
809
+ /**
810
+ * Widget definitions for the runtime's WidgetRegistry.
811
+ */
812
+ widgets: [
813
+ {
814
+ id: "adaptive-faq:accordion",
815
+ component: FAQWidgetLitMountable,
816
+ metadata: {
817
+ name: "FAQ Accordion",
818
+ description: "Collapsible Q&A accordion with search, categories, and feedback",
819
+ icon: "\u2753",
820
+ subtitle: "Curated just for you."
821
+ }
822
+ }
823
+ ],
824
+ /**
825
+ * Extract notify watcher entries from tile config props.
826
+ * The runtime evaluates these continuously (even with drawer closed)
827
+ * and publishes faq:question_revealed when triggerWhen transitions false → true.
828
+ */
829
+ notifyWatchers(props) {
830
+ const actions = props.actions ?? [];
831
+ return actions.filter((a) => a.notify && a.triggerWhen).map((a) => ({
832
+ id: `faq:${a.config.id}`,
833
+ strategy: a.triggerWhen,
834
+ eventName: "faq:question_revealed",
835
+ eventProps: {
836
+ questionId: a.config.id,
837
+ question: a.config.question,
838
+ title: a.notify.title,
839
+ body: a.notify.body,
840
+ icon: a.notify.icon
841
+ }
842
+ }));
843
+ }
844
+ };
845
+ var runtime_default = runtime;
846
+ export {
847
+ FAQWidgetLitMountable,
848
+ runtime_default as default,
849
+ runtime
93
850
  };
94
- export default runtime;
851
+ //# sourceMappingURL=runtime.js.map