@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/FAQWidget.js DELETED
@@ -1,375 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /**
3
- * Adaptive FAQ - FAQWidget Component
4
- *
5
- * React component that renders a collapsible Q&A accordion with per-item
6
- * conditional visibility based on triggerWhen decision strategies.
7
- *
8
- * Demonstrates the compositional action pattern where child actions
9
- * (faq:question) serve as configuration data for the parent widget.
10
- */
11
- import { purple } from '@syntro/design-system/tokens';
12
- import { Marked } from 'marked';
13
- import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
14
- import { createRoot } from 'react-dom/client';
15
- const marked = new Marked({ async: false, gfm: true, breaks: true });
16
- import { baseStyles, themeStyles } from './faq-styles';
17
- // ============================================================================
18
- // Helpers
19
- // ============================================================================
20
- /** Extract plain text from an FAQAnswer for search matching */
21
- function getAnswerText(answer) {
22
- if (typeof answer === 'string')
23
- return answer;
24
- if (answer.type === 'rich')
25
- return answer.html;
26
- return answer.content;
27
- }
28
- /** Render an FAQAnswer based on its type */
29
- function renderAnswer(answer) {
30
- if (typeof answer === 'string') {
31
- const html = marked.parse(answer);
32
- return (
33
- // biome-ignore lint/security/noDangerouslySetInnerHtml: content is CMS/config content, not user-controlled input
34
- _jsx("div", { style: { margin: 0 }, "data-faq-markdown": "", dangerouslySetInnerHTML: { __html: html } }));
35
- }
36
- if (answer.type === 'rich') {
37
- // biome-ignore lint/security/noDangerouslySetInnerHtml: content is pre-sanitized by backend — FAQAnswer.html is CMS/config content, not user-controlled input
38
- return _jsx("div", { style: { margin: 0 }, dangerouslySetInnerHTML: { __html: answer.html } });
39
- }
40
- // markdown — parse to HTML and render
41
- const html = marked.parse(answer.content);
42
- return (
43
- // biome-ignore lint/security/noDangerouslySetInnerHtml: markdown content is CMS/config content, not user-controlled input
44
- _jsx("div", { style: { margin: 0 }, "data-faq-markdown": "", dangerouslySetInnerHTML: { __html: html } }));
45
- }
46
- /** Resolve feedback config into a normalized FeedbackConfig or null */
47
- function resolveFeedbackConfig(feedback) {
48
- if (!feedback)
49
- return null;
50
- if (feedback === true) {
51
- return { style: 'thumbs' };
52
- }
53
- return feedback;
54
- }
55
- /** Get the feedback prompt text */
56
- function getFeedbackPrompt(feedbackConfig) {
57
- return feedbackConfig.prompt || 'Was this helpful?';
58
- }
59
- function FAQItem({ item, isExpanded, isHighlighted, isLast, onToggle, theme, feedbackConfig, feedbackValue, onFeedback, }) {
60
- const [isHovered, setIsHovered] = useState(false);
61
- const colors = themeStyles[theme];
62
- const { question, answer } = item.config;
63
- const itemStyle = {
64
- ...baseStyles.item,
65
- ...colors.item,
66
- ...(isExpanded ? colors.itemExpanded : {}),
67
- ...(isHighlighted
68
- ? {
69
- // purple[4] = #6a59ce — design system primary purple
70
- boxShadow: `0 0 0 2px ${purple[4]}, 0 0 12px rgba(106, 89, 206, 0.4)`,
71
- transition: 'box-shadow 0.3s ease',
72
- }
73
- : {}),
74
- ...(!isLast ? { borderBottom: 'var(--sc-content-item-divider, none)' } : {}),
75
- };
76
- const questionStyle = {
77
- ...baseStyles.question,
78
- ...colors.question,
79
- ...(isHovered ? colors.questionHover : {}),
80
- };
81
- const chevronStyle = {
82
- ...baseStyles.chevron,
83
- transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
84
- };
85
- const answerStyle = {
86
- ...baseStyles.answer,
87
- ...colors.answer,
88
- maxHeight: isExpanded ? '500px' : '0',
89
- paddingBottom: isExpanded ? '16px' : '0',
90
- };
91
- const feedbackStyle = {
92
- ...baseStyles.feedback,
93
- ...colors.feedbackPrompt,
94
- };
95
- return (_jsxs("div", { style: itemStyle, "data-faq-item-id": item.config.id, children: [_jsxs("button", { type: "button", style: questionStyle, onClick: onToggle, onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), "aria-expanded": isExpanded, children: [_jsx("span", { children: question }), _jsx("span", { style: chevronStyle, children: '\u203A' })] }), _jsxs("div", { style: answerStyle, "aria-hidden": !isExpanded, children: [renderAnswer(answer), isExpanded && feedbackConfig && (_jsxs("div", { style: feedbackStyle, children: [_jsx("span", { children: getFeedbackPrompt(feedbackConfig) }), _jsx("button", { type: "button", style: {
96
- ...baseStyles.feedbackButton,
97
- ...(feedbackValue === 'up' ? baseStyles.feedbackButtonSelected : {}),
98
- }, "aria-label": "Thumbs up", onClick: () => onFeedback(item.config.id, question, 'up'), children: '\uD83D\uDC4D' }), _jsx("button", { type: "button", style: {
99
- ...baseStyles.feedbackButton,
100
- ...(feedbackValue === 'down' ? baseStyles.feedbackButtonSelected : {}),
101
- }, "aria-label": "Thumbs down", onClick: () => onFeedback(item.config.id, question, 'down'), children: '\uD83D\uDC4E' })] }))] })] }));
102
- }
103
- // ============================================================================
104
- // FAQWidget Component
105
- // ============================================================================
106
- /**
107
- * FAQWidget - Renders a collapsible Q&A accordion with per-item activation.
108
- *
109
- * This component demonstrates the compositional action pattern:
110
- * - Parent (FAQWidget) receives `config.actions` array
111
- * - Each action has optional `triggerWhen` for per-item visibility
112
- * - Parent evaluates triggerWhen and filters visible questions
113
- * - Parent manages expand state and re-rendering on context changes
114
- */
115
- export function FAQWidget({ config, runtime, instanceId }) {
116
- // Force re-render when context/accumulator changes.
117
- // renderTick is used as a useMemo dependency to invalidate cached triggerWhen evaluations.
118
- const [renderTick, forceUpdate] = useReducer((x) => x + 1, 0);
119
- // Track expanded question IDs
120
- const [expandedIds, setExpandedIds] = useState(new Set());
121
- // Track which item is flash-highlighted from a deep-link
122
- const [highlightId, setHighlightId] = useState(null);
123
- // Search query state
124
- const [searchQuery, setSearchQuery] = useState('');
125
- // Track feedback state per item
126
- const [feedbackState, setFeedbackState] = useState(new Map());
127
- // Resolve feedback config
128
- const feedbackConfig = useMemo(() => resolveFeedbackConfig(config.feedback), [config.feedback]);
129
- // Subscribe to context changes for reactive updates
130
- useEffect(() => {
131
- const unsubscribe = runtime.context.subscribe(() => {
132
- forceUpdate();
133
- });
134
- return unsubscribe;
135
- }, [runtime.context]);
136
- // Subscribe to accumulator changes for event_count-based triggerWhen
137
- useEffect(() => {
138
- if (!runtime.accumulator?.subscribe)
139
- return;
140
- return runtime.accumulator.subscribe(() => {
141
- forceUpdate();
142
- });
143
- }, [runtime.accumulator]);
144
- // Subscribe to faq:open:* events from overlay CTA clicks
145
- useEffect(() => {
146
- if (!runtime.events.subscribe)
147
- return;
148
- // Check EventBus history for pending faq:open events
149
- // (may have fired before this widget mounted, e.g. when canvas was closed)
150
- if (runtime.events.getRecent) {
151
- const recentEvents = runtime.events.getRecent({ patterns: ['^action\\.tooltip_cta_clicked$', '^action\\.modal_cta_clicked$'] }, 10);
152
- const pendingEvent = recentEvents
153
- .filter((e) => {
154
- const actionId = e.props?.actionId;
155
- return typeof actionId === 'string' && actionId.startsWith('faq:open:');
156
- })
157
- .pop(); // Most recent
158
- if (pendingEvent && Date.now() - pendingEvent.ts < 10000) {
159
- const questionId = pendingEvent.props.actionId.replace('faq:open:', '');
160
- setExpandedIds(new Set([questionId]));
161
- // Scroll into view after render
162
- requestAnimationFrame(() => {
163
- const el = document.querySelector(`[data-faq-item-id="${questionId}"]`);
164
- if (el)
165
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
166
- });
167
- }
168
- }
169
- // Subscribe to future CTA events
170
- const unsubscribe = runtime.events.subscribe({ patterns: ['^action\\.tooltip_cta_clicked$', '^action\\.modal_cta_clicked$'] }, (event) => {
171
- const actionId = event.props?.actionId;
172
- if (typeof actionId !== 'string' || !actionId.startsWith('faq:open:'))
173
- return;
174
- const questionId = actionId.replace('faq:open:', '');
175
- setExpandedIds(new Set([questionId]));
176
- // Scroll the question into view
177
- requestAnimationFrame(() => {
178
- const el = document.querySelector(`[data-faq-item-id="${questionId}"]`);
179
- if (el)
180
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
181
- });
182
- // Request canvas open (for future tile-based FAQ rendering)
183
- runtime.events.publish('canvas.requestOpen');
184
- });
185
- return unsubscribe;
186
- }, [runtime]);
187
- // Subscribe to notification.deep_link events (from insertHtml deepLink clicks + notification toasts)
188
- useEffect(() => {
189
- if (!runtime.events.subscribe)
190
- return;
191
- const handleDeepLink = (event) => {
192
- const tileId = event.props?.tileId;
193
- const itemId = event.props?.itemId;
194
- // Only handle if this deep link targets our tile
195
- if (tileId !== instanceId)
196
- return;
197
- if (!itemId)
198
- return;
199
- // Expand the target item
200
- setExpandedIds(new Set([itemId]));
201
- // Flash-highlight the item
202
- setHighlightId(itemId);
203
- setTimeout(() => setHighlightId(null), 1500);
204
- // Scroll into view after render
205
- requestAnimationFrame(() => {
206
- const el = document.querySelector(`[data-faq-item-id="${itemId}"]`);
207
- if (el)
208
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
209
- });
210
- };
211
- // Check recent events (may have fired before widget mounted, e.g. canvas was closed)
212
- if (runtime.events.getRecent) {
213
- const recent = runtime.events.getRecent({ names: ['notification.deep_link'] }, 5);
214
- const pending = recent
215
- .filter((e) => e.props?.tileId === instanceId && e.props?.itemId)
216
- .pop();
217
- if (pending && Date.now() - pending.ts < 10000) {
218
- handleDeepLink(pending);
219
- }
220
- }
221
- // Subscribe to future events
222
- const unsubscribe = runtime.events.subscribe({ names: ['notification.deep_link'] }, handleDeepLink);
223
- return unsubscribe;
224
- }, [runtime, instanceId]);
225
- // Filter visible questions based on per-item triggerWhen
226
- // biome-ignore lint/correctness/useExhaustiveDependencies: renderTick is intentionally included to force re-evaluation when the runtime's mutable context changes (subscribed above via forceUpdate)
227
- const visibleQuestions = useMemo(() => (config.actions ?? []).filter((q) => {
228
- // No triggerWhen = always visible
229
- if (!q.triggerWhen)
230
- return true;
231
- // Evaluate the decision strategy
232
- const result = runtime.evaluateSync(q.triggerWhen);
233
- return result.value;
234
- }), [config.actions, runtime, renderTick]);
235
- // NOTE: faq:question_revealed is now published by useNotifyWatcher in
236
- // ShadowCanvasOverlay (always mounted), so it fires even with drawer closed.
237
- // Apply priority ordering
238
- const orderedQuestions = useMemo(() => {
239
- if (config.ordering === 'priority') {
240
- return [...visibleQuestions].sort((a, b) => (b.config.priority ?? 0) - (a.config.priority ?? 0));
241
- }
242
- // 'static' or undefined — preserve config order
243
- return visibleQuestions;
244
- }, [visibleQuestions, config.ordering]);
245
- // Apply search filter
246
- const filteredQuestions = useMemo(() => {
247
- if (!config.searchable || !searchQuery.trim()) {
248
- return orderedQuestions;
249
- }
250
- const query = searchQuery.toLowerCase();
251
- return orderedQuestions.filter((q) => q.config.question.toLowerCase().includes(query) ||
252
- getAnswerText(q.config.answer).toLowerCase().includes(query) ||
253
- q.config.category?.toLowerCase().includes(query));
254
- }, [orderedQuestions, searchQuery, config.searchable]);
255
- // Group by category
256
- const categoryGroups = useMemo(() => {
257
- const groups = new Map();
258
- for (const q of filteredQuestions) {
259
- const cat = q.config.category;
260
- if (!groups.has(cat)) {
261
- groups.set(cat, []);
262
- }
263
- groups.get(cat).push(q);
264
- }
265
- return groups;
266
- }, [filteredQuestions]);
267
- // Check if any items have categories
268
- const hasCategories = useMemo(() => filteredQuestions.some((q) => q.config.category), [filteredQuestions]);
269
- // Resolve theme (auto -> detect system preference)
270
- const resolvedTheme = useMemo(() => {
271
- if (config.theme && config.theme !== 'auto')
272
- return config.theme;
273
- // Check system preference (SSR-safe)
274
- if (typeof window !== 'undefined') {
275
- return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
276
- }
277
- return 'light';
278
- }, [config.theme]);
279
- // Handle question toggle
280
- const handleToggle = useCallback((id) => {
281
- setExpandedIds((prev) => {
282
- const next = new Set(prev);
283
- if (config.expandBehavior === 'single') {
284
- // Single mode: collapse all others
285
- if (prev.has(id)) {
286
- return new Set();
287
- }
288
- return new Set([id]);
289
- }
290
- // Multiple mode: toggle this one
291
- if (prev.has(id)) {
292
- next.delete(id);
293
- }
294
- else {
295
- next.add(id);
296
- }
297
- return next;
298
- });
299
- // Publish toggle event for analytics
300
- runtime.events.publish('faq:toggled', {
301
- instanceId,
302
- questionId: id,
303
- expanded: !expandedIds.has(id),
304
- timestamp: Date.now(),
305
- });
306
- }, [config.expandBehavior, runtime.events, instanceId, expandedIds]);
307
- // Handle feedback
308
- const handleFeedback = useCallback((itemId, question, value) => {
309
- setFeedbackState((prev) => {
310
- const next = new Map(prev);
311
- next.set(itemId, value);
312
- return next;
313
- });
314
- runtime.events.publish('faq:feedback', {
315
- itemId,
316
- question,
317
- value,
318
- });
319
- }, [runtime.events]);
320
- // Compute styles
321
- const containerStyle = {
322
- ...baseStyles.container,
323
- ...themeStyles[resolvedTheme].container,
324
- };
325
- const searchInputStyle = {
326
- ...baseStyles.searchInput,
327
- ...themeStyles[resolvedTheme].searchInput,
328
- };
329
- const emptyStateStyle = {
330
- ...baseStyles.emptyState,
331
- ...themeStyles[resolvedTheme].emptyState,
332
- };
333
- const categoryHeaderStyle = {
334
- ...baseStyles.categoryHeader,
335
- ...themeStyles[resolvedTheme].categoryHeader,
336
- };
337
- // Render a list of FAQ items
338
- const renderItems = (items) => items.map((q, index) => (_jsx(FAQItem, { item: q, isExpanded: expandedIds.has(q.config.id), isHighlighted: highlightId === q.config.id, isLast: index === items.length - 1, onToggle: () => handleToggle(q.config.id), theme: resolvedTheme, feedbackConfig: feedbackConfig, feedbackValue: feedbackState.get(q.config.id), onFeedback: handleFeedback }, q.config.id)));
339
- // Empty state (no visible questions at all)
340
- if (visibleQuestions.length === 0) {
341
- return (_jsx("div", { style: containerStyle, "data-adaptive-id": instanceId, "data-adaptive-type": "adaptive-faq", children: _jsx("div", { style: emptyStateStyle, children: "You're all set for now! We'll surface answers here when they're relevant to what you're doing." }) }));
342
- }
343
- return (_jsxs("div", { style: containerStyle, "data-adaptive-id": instanceId, "data-adaptive-type": "adaptive-faq", children: [config.searchable && (_jsxs("div", { style: baseStyles.searchWrapper, children: [_jsx("style", { children: `[data-adaptive-id="${instanceId}"] input::placeholder { color: var(--sc-content-search-color, inherit); opacity: 0.7; }` }), _jsx("input", { type: "text", placeholder: "Search questions...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), style: searchInputStyle })] })), _jsx("div", { style: baseStyles.accordion, children: hasCategories
344
- ? Array.from(categoryGroups.entries()).map(([category, items]) => (_jsxs(React.Fragment, { children: [category && (_jsx("div", { style: categoryHeaderStyle, "data-category-header": category, children: category })), renderItems(items)] }, category ?? '__ungrouped')))
345
- : renderItems(filteredQuestions) }), config.searchable && filteredQuestions.length === 0 && searchQuery && (_jsxs("div", { style: { ...baseStyles.noResults, ...themeStyles[resolvedTheme].emptyState }, children: ["No questions found matching \"", searchQuery, "\""] }))] }));
346
- }
347
- // ============================================================================
348
- // Mountable Widget Interface
349
- // ============================================================================
350
- /**
351
- * Mountable widget interface for the runtime's WidgetRegistry.
352
- */
353
- export const FAQMountableWidget = {
354
- mount(container, config) {
355
- const { runtime, instanceId = 'faq-widget', ...faqConfig } = config || {
356
- expandBehavior: 'single',
357
- searchable: false,
358
- theme: 'auto',
359
- actions: [],
360
- };
361
- // React rendering when runtime + ReactDOM are available
362
- if (runtime && typeof createRoot === 'function') {
363
- const root = createRoot(container);
364
- root.render(React.createElement(FAQWidget, {
365
- config: faqConfig,
366
- runtime: runtime,
367
- instanceId,
368
- }));
369
- return () => {
370
- root.unmount();
371
- };
372
- }
373
- },
374
- };
375
- export default FAQWidget;