@syntrologie/adapt-faq 2.8.0-canary.31 → 2.8.0-canary.310
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/FAQWidgetLit.d.ts +85 -0
- package/dist/FAQWidgetLit.d.ts.map +1 -0
- package/dist/FAQWidgetLit.editable.d.ts +154 -0
- package/dist/FAQWidgetLit.editable.d.ts.map +1 -0
- package/dist/answerRendering.d.ts +4 -0
- package/dist/answerRendering.d.ts.map +1 -0
- package/dist/chunk-5WRI5ZAA.js +31 -0
- package/dist/chunk-5WRI5ZAA.js.map +7 -0
- package/dist/chunk-IGCYULL7.js +223 -0
- package/dist/chunk-IGCYULL7.js.map +7 -0
- package/dist/chunk-KRKRB4OL.js +598 -0
- package/dist/chunk-KRKRB4OL.js.map +7 -0
- package/dist/editor.d.ts +60 -33
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +5054 -313
- package/dist/editor.js.map +7 -0
- package/dist/faq-item-editor.d.ts +33 -0
- package/dist/faq-item-editor.d.ts.map +1 -0
- package/dist/faq-styles.d.ts +3 -1
- package/dist/faq-styles.d.ts.map +1 -1
- package/dist/faq-types.d.ts +4 -0
- package/dist/faq-types.d.ts.map +1 -1
- package/dist/runtime.d.ts +17 -5
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +841 -64
- package/dist/runtime.js.map +7 -0
- package/dist/schema.d.ts +1174 -555
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +290 -207
- package/dist/schema.js.map +7 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/node_modules/marked/LICENSE.md +44 -0
- package/node_modules/marked/README.md +107 -0
- package/node_modules/marked/bin/main.js +283 -0
- package/node_modules/marked/bin/marked.js +16 -0
- package/node_modules/marked/lib/marked.d.ts +759 -0
- package/node_modules/marked/lib/marked.esm.js +72 -0
- package/node_modules/marked/lib/marked.esm.js.map +7 -0
- package/node_modules/marked/lib/marked.umd.js +74 -0
- package/node_modules/marked/lib/marked.umd.js.map +7 -0
- package/node_modules/marked/man/marked.1 +113 -0
- package/node_modules/marked/man/marked.1.md +93 -0
- package/node_modules/marked/package.json +103 -0
- package/package.json +13 -18
- package/dist/FAQWidget.d.ts +0 -33
- package/dist/FAQWidget.d.ts.map +0 -1
- package/dist/FAQWidget.js +0 -388
- package/dist/cdn.d.ts +0 -70
- package/dist/cdn.d.ts.map +0 -1
- package/dist/cdn.js +0 -46
- package/dist/executors.js +0 -150
- package/dist/faq-styles.js +0 -204
- package/dist/faq-types.js +0 -7
- package/dist/state.js +0 -132
- package/dist/summarize.js +0 -62
- package/dist/types.js +0 -17
- package/node_modules/@syntrologie/sdk-contracts/dist/index.d.ts +0 -129
- package/node_modules/@syntrologie/sdk-contracts/dist/index.js +0 -15
- package/node_modules/@syntrologie/sdk-contracts/dist/schemas.d.ts +0 -2225
- package/node_modules/@syntrologie/sdk-contracts/dist/schemas.js +0 -162
- package/node_modules/@syntrologie/sdk-contracts/package.json +0 -33
package/dist/FAQWidget.js
DELETED
|
@@ -1,388 +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 { slateGrey } 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
|
-
return _jsx("p", { style: { margin: 0 }, children: answer });
|
|
32
|
-
}
|
|
33
|
-
if (answer.type === 'rich') {
|
|
34
|
-
// biome-ignore lint/security/noDangerouslySetInnerHtml: content is pre-sanitized by backend — FAQAnswer.html is CMS/config content, not user-controlled input
|
|
35
|
-
return _jsx("div", { style: { margin: 0 }, dangerouslySetInnerHTML: { __html: answer.html } });
|
|
36
|
-
}
|
|
37
|
-
// markdown — parse to HTML and render
|
|
38
|
-
const html = marked.parse(answer.content);
|
|
39
|
-
return (
|
|
40
|
-
// biome-ignore lint/security/noDangerouslySetInnerHtml: markdown content is CMS/config content, not user-controlled input
|
|
41
|
-
_jsx("div", { style: { margin: 0 }, "data-faq-markdown": "", dangerouslySetInnerHTML: { __html: html } }));
|
|
42
|
-
}
|
|
43
|
-
/** Resolve feedback config into a normalized FeedbackConfig or null */
|
|
44
|
-
function resolveFeedbackConfig(feedback) {
|
|
45
|
-
if (!feedback)
|
|
46
|
-
return null;
|
|
47
|
-
if (feedback === true) {
|
|
48
|
-
return { style: 'thumbs' };
|
|
49
|
-
}
|
|
50
|
-
return feedback;
|
|
51
|
-
}
|
|
52
|
-
/** Get the feedback prompt text */
|
|
53
|
-
function getFeedbackPrompt(feedbackConfig) {
|
|
54
|
-
return feedbackConfig.prompt || 'Was this helpful?';
|
|
55
|
-
}
|
|
56
|
-
function FAQItem({ item, isExpanded, isHighlighted, isLast, onToggle, theme, feedbackConfig, feedbackValue, onFeedback, }) {
|
|
57
|
-
const [isHovered, setIsHovered] = useState(false);
|
|
58
|
-
const colors = themeStyles[theme];
|
|
59
|
-
const { question, answer } = item.config;
|
|
60
|
-
const itemStyle = {
|
|
61
|
-
...baseStyles.item,
|
|
62
|
-
...colors.item,
|
|
63
|
-
...(isExpanded ? colors.itemExpanded : {}),
|
|
64
|
-
...(isHighlighted
|
|
65
|
-
? {
|
|
66
|
-
boxShadow: '0 0 0 2px #6366f1, 0 0 12px rgba(99, 102, 241, 0.4)',
|
|
67
|
-
transition: 'box-shadow 0.3s ease',
|
|
68
|
-
}
|
|
69
|
-
: {}),
|
|
70
|
-
...(!isLast ? { borderBottom: 'var(--sc-content-item-divider, none)' } : {}),
|
|
71
|
-
};
|
|
72
|
-
const questionStyle = {
|
|
73
|
-
...baseStyles.question,
|
|
74
|
-
...colors.question,
|
|
75
|
-
...(isHovered ? colors.questionHover : {}),
|
|
76
|
-
};
|
|
77
|
-
const chevronStyle = {
|
|
78
|
-
...baseStyles.chevron,
|
|
79
|
-
transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
|
80
|
-
};
|
|
81
|
-
const answerStyle = {
|
|
82
|
-
...baseStyles.answer,
|
|
83
|
-
...colors.answer,
|
|
84
|
-
maxHeight: isExpanded ? '500px' : '0',
|
|
85
|
-
paddingBottom: isExpanded ? '16px' : '0',
|
|
86
|
-
};
|
|
87
|
-
const feedbackStyle = {
|
|
88
|
-
...baseStyles.feedback,
|
|
89
|
-
...colors.feedbackPrompt,
|
|
90
|
-
};
|
|
91
|
-
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: {
|
|
92
|
-
...baseStyles.feedbackButton,
|
|
93
|
-
...(feedbackValue === 'up' ? baseStyles.feedbackButtonSelected : {}),
|
|
94
|
-
}, "aria-label": "Thumbs up", onClick: () => onFeedback(item.config.id, question, 'up'), children: '\uD83D\uDC4D' }), _jsx("button", { type: "button", style: {
|
|
95
|
-
...baseStyles.feedbackButton,
|
|
96
|
-
...(feedbackValue === 'down' ? baseStyles.feedbackButtonSelected : {}),
|
|
97
|
-
}, "aria-label": "Thumbs down", onClick: () => onFeedback(item.config.id, question, 'down'), children: '\uD83D\uDC4E' })] }))] })] }));
|
|
98
|
-
}
|
|
99
|
-
// ============================================================================
|
|
100
|
-
// FAQWidget Component
|
|
101
|
-
// ============================================================================
|
|
102
|
-
/**
|
|
103
|
-
* FAQWidget - Renders a collapsible Q&A accordion with per-item activation.
|
|
104
|
-
*
|
|
105
|
-
* This component demonstrates the compositional action pattern:
|
|
106
|
-
* - Parent (FAQWidget) receives `config.actions` array
|
|
107
|
-
* - Each action has optional `triggerWhen` for per-item visibility
|
|
108
|
-
* - Parent evaluates triggerWhen and filters visible questions
|
|
109
|
-
* - Parent manages expand state and re-rendering on context changes
|
|
110
|
-
*/
|
|
111
|
-
export function FAQWidget({ config, runtime, instanceId }) {
|
|
112
|
-
// Force re-render when context/accumulator changes.
|
|
113
|
-
// renderTick is used as a useMemo dependency to invalidate cached triggerWhen evaluations.
|
|
114
|
-
const [renderTick, forceUpdate] = useReducer((x) => x + 1, 0);
|
|
115
|
-
// Track expanded question IDs
|
|
116
|
-
const [expandedIds, setExpandedIds] = useState(new Set());
|
|
117
|
-
// Track which item is flash-highlighted from a deep-link
|
|
118
|
-
const [highlightId, setHighlightId] = useState(null);
|
|
119
|
-
// Search query state
|
|
120
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
121
|
-
// Track feedback state per item
|
|
122
|
-
const [feedbackState, setFeedbackState] = useState(new Map());
|
|
123
|
-
// Resolve feedback config
|
|
124
|
-
const feedbackConfig = useMemo(() => resolveFeedbackConfig(config.feedback), [config.feedback]);
|
|
125
|
-
// Subscribe to context changes for reactive updates
|
|
126
|
-
useEffect(() => {
|
|
127
|
-
const unsubscribe = runtime.context.subscribe(() => {
|
|
128
|
-
forceUpdate();
|
|
129
|
-
});
|
|
130
|
-
return unsubscribe;
|
|
131
|
-
}, [runtime.context]);
|
|
132
|
-
// Subscribe to accumulator changes for event_count-based triggerWhen
|
|
133
|
-
useEffect(() => {
|
|
134
|
-
if (!runtime.accumulator?.subscribe)
|
|
135
|
-
return;
|
|
136
|
-
return runtime.accumulator.subscribe(() => {
|
|
137
|
-
forceUpdate();
|
|
138
|
-
});
|
|
139
|
-
}, [runtime.accumulator]);
|
|
140
|
-
// Subscribe to faq:open:* events from overlay CTA clicks
|
|
141
|
-
useEffect(() => {
|
|
142
|
-
if (!runtime.events.subscribe)
|
|
143
|
-
return;
|
|
144
|
-
// Check EventBus history for pending faq:open events
|
|
145
|
-
// (may have fired before this widget mounted, e.g. when canvas was closed)
|
|
146
|
-
if (runtime.events.getRecent) {
|
|
147
|
-
const recentEvents = runtime.events.getRecent({ patterns: ['^action\\.tooltip_cta_clicked$', '^action\\.modal_cta_clicked$'] }, 10);
|
|
148
|
-
const pendingEvent = recentEvents
|
|
149
|
-
.filter((e) => {
|
|
150
|
-
const actionId = e.props?.actionId;
|
|
151
|
-
return typeof actionId === 'string' && actionId.startsWith('faq:open:');
|
|
152
|
-
})
|
|
153
|
-
.pop(); // Most recent
|
|
154
|
-
if (pendingEvent && Date.now() - pendingEvent.ts < 10000) {
|
|
155
|
-
const questionId = pendingEvent.props.actionId.replace('faq:open:', '');
|
|
156
|
-
setExpandedIds(new Set([questionId]));
|
|
157
|
-
// Scroll into view after render
|
|
158
|
-
requestAnimationFrame(() => {
|
|
159
|
-
const el = document.querySelector(`[data-faq-item-id="${questionId}"]`);
|
|
160
|
-
if (el)
|
|
161
|
-
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
// Subscribe to future CTA events
|
|
166
|
-
const unsubscribe = runtime.events.subscribe({ patterns: ['^action\\.tooltip_cta_clicked$', '^action\\.modal_cta_clicked$'] }, (event) => {
|
|
167
|
-
const actionId = event.props?.actionId;
|
|
168
|
-
if (typeof actionId !== 'string' || !actionId.startsWith('faq:open:'))
|
|
169
|
-
return;
|
|
170
|
-
const questionId = actionId.replace('faq:open:', '');
|
|
171
|
-
setExpandedIds(new Set([questionId]));
|
|
172
|
-
// Scroll the question into view
|
|
173
|
-
requestAnimationFrame(() => {
|
|
174
|
-
const el = document.querySelector(`[data-faq-item-id="${questionId}"]`);
|
|
175
|
-
if (el)
|
|
176
|
-
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
177
|
-
});
|
|
178
|
-
// Request canvas open (for future tile-based FAQ rendering)
|
|
179
|
-
runtime.events.publish('canvas.requestOpen');
|
|
180
|
-
});
|
|
181
|
-
return unsubscribe;
|
|
182
|
-
}, [runtime]);
|
|
183
|
-
// Subscribe to notification.deep_link events (from insertHtml deepLink clicks + notification toasts)
|
|
184
|
-
useEffect(() => {
|
|
185
|
-
if (!runtime.events.subscribe)
|
|
186
|
-
return;
|
|
187
|
-
const handleDeepLink = (event) => {
|
|
188
|
-
const tileId = event.props?.tileId;
|
|
189
|
-
const itemId = event.props?.itemId;
|
|
190
|
-
// Only handle if this deep link targets our tile
|
|
191
|
-
if (tileId !== instanceId)
|
|
192
|
-
return;
|
|
193
|
-
if (!itemId)
|
|
194
|
-
return;
|
|
195
|
-
// Expand the target item
|
|
196
|
-
setExpandedIds(new Set([itemId]));
|
|
197
|
-
// Flash-highlight the item
|
|
198
|
-
setHighlightId(itemId);
|
|
199
|
-
setTimeout(() => setHighlightId(null), 1500);
|
|
200
|
-
// Scroll into view after render
|
|
201
|
-
requestAnimationFrame(() => {
|
|
202
|
-
const el = document.querySelector(`[data-faq-item-id="${itemId}"]`);
|
|
203
|
-
if (el)
|
|
204
|
-
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
205
|
-
});
|
|
206
|
-
};
|
|
207
|
-
// Check recent events (may have fired before widget mounted, e.g. canvas was closed)
|
|
208
|
-
if (runtime.events.getRecent) {
|
|
209
|
-
const recent = runtime.events.getRecent({ names: ['notification.deep_link'] }, 5);
|
|
210
|
-
const pending = recent
|
|
211
|
-
.filter((e) => e.props?.tileId === instanceId && e.props?.itemId)
|
|
212
|
-
.pop();
|
|
213
|
-
if (pending && Date.now() - pending.ts < 10000) {
|
|
214
|
-
handleDeepLink(pending);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
// Subscribe to future events
|
|
218
|
-
const unsubscribe = runtime.events.subscribe({ names: ['notification.deep_link'] }, handleDeepLink);
|
|
219
|
-
return unsubscribe;
|
|
220
|
-
}, [runtime, instanceId]);
|
|
221
|
-
// Filter visible questions based on per-item triggerWhen
|
|
222
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: renderTick is intentionally included to force re-evaluation when the runtime's mutable context changes (subscribed above via forceUpdate)
|
|
223
|
-
const visibleQuestions = useMemo(() => config.actions.filter((q) => {
|
|
224
|
-
// No triggerWhen = always visible
|
|
225
|
-
if (!q.triggerWhen)
|
|
226
|
-
return true;
|
|
227
|
-
// Evaluate the decision strategy
|
|
228
|
-
const result = runtime.evaluateSync(q.triggerWhen);
|
|
229
|
-
return result.value;
|
|
230
|
-
}), [config.actions, runtime, renderTick]);
|
|
231
|
-
// NOTE: faq:question_revealed is now published by useNotifyWatcher in
|
|
232
|
-
// ShadowCanvasOverlay (always mounted), so it fires even with drawer closed.
|
|
233
|
-
// Apply priority ordering
|
|
234
|
-
const orderedQuestions = useMemo(() => {
|
|
235
|
-
if (config.ordering === 'priority') {
|
|
236
|
-
return [...visibleQuestions].sort((a, b) => (b.config.priority ?? 0) - (a.config.priority ?? 0));
|
|
237
|
-
}
|
|
238
|
-
// 'static' or undefined — preserve config order
|
|
239
|
-
return visibleQuestions;
|
|
240
|
-
}, [visibleQuestions, config.ordering]);
|
|
241
|
-
// Apply search filter
|
|
242
|
-
const filteredQuestions = useMemo(() => {
|
|
243
|
-
if (!config.searchable || !searchQuery.trim()) {
|
|
244
|
-
return orderedQuestions;
|
|
245
|
-
}
|
|
246
|
-
const query = searchQuery.toLowerCase();
|
|
247
|
-
return orderedQuestions.filter((q) => q.config.question.toLowerCase().includes(query) ||
|
|
248
|
-
getAnswerText(q.config.answer).toLowerCase().includes(query) ||
|
|
249
|
-
q.config.category?.toLowerCase().includes(query));
|
|
250
|
-
}, [orderedQuestions, searchQuery, config.searchable]);
|
|
251
|
-
// Group by category
|
|
252
|
-
const categoryGroups = useMemo(() => {
|
|
253
|
-
const groups = new Map();
|
|
254
|
-
for (const q of filteredQuestions) {
|
|
255
|
-
const cat = q.config.category;
|
|
256
|
-
if (!groups.has(cat)) {
|
|
257
|
-
groups.set(cat, []);
|
|
258
|
-
}
|
|
259
|
-
groups.get(cat).push(q);
|
|
260
|
-
}
|
|
261
|
-
return groups;
|
|
262
|
-
}, [filteredQuestions]);
|
|
263
|
-
// Check if any items have categories
|
|
264
|
-
const hasCategories = useMemo(() => filteredQuestions.some((q) => q.config.category), [filteredQuestions]);
|
|
265
|
-
// Resolve theme (auto -> detect system preference)
|
|
266
|
-
const resolvedTheme = useMemo(() => {
|
|
267
|
-
if (config.theme && config.theme !== 'auto')
|
|
268
|
-
return config.theme;
|
|
269
|
-
// Check system preference (SSR-safe)
|
|
270
|
-
if (typeof window !== 'undefined') {
|
|
271
|
-
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
272
|
-
}
|
|
273
|
-
return 'light';
|
|
274
|
-
}, [config.theme]);
|
|
275
|
-
// Handle question toggle
|
|
276
|
-
const handleToggle = useCallback((id) => {
|
|
277
|
-
setExpandedIds((prev) => {
|
|
278
|
-
const next = new Set(prev);
|
|
279
|
-
if (config.expandBehavior === 'single') {
|
|
280
|
-
// Single mode: collapse all others
|
|
281
|
-
if (prev.has(id)) {
|
|
282
|
-
return new Set();
|
|
283
|
-
}
|
|
284
|
-
return new Set([id]);
|
|
285
|
-
}
|
|
286
|
-
// Multiple mode: toggle this one
|
|
287
|
-
if (prev.has(id)) {
|
|
288
|
-
next.delete(id);
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
next.add(id);
|
|
292
|
-
}
|
|
293
|
-
return next;
|
|
294
|
-
});
|
|
295
|
-
// Publish toggle event for analytics
|
|
296
|
-
runtime.events.publish('faq:toggled', {
|
|
297
|
-
instanceId,
|
|
298
|
-
questionId: id,
|
|
299
|
-
expanded: !expandedIds.has(id),
|
|
300
|
-
timestamp: Date.now(),
|
|
301
|
-
});
|
|
302
|
-
}, [config.expandBehavior, runtime.events, instanceId, expandedIds]);
|
|
303
|
-
// Handle feedback
|
|
304
|
-
const handleFeedback = useCallback((itemId, question, value) => {
|
|
305
|
-
setFeedbackState((prev) => {
|
|
306
|
-
const next = new Map(prev);
|
|
307
|
-
next.set(itemId, value);
|
|
308
|
-
return next;
|
|
309
|
-
});
|
|
310
|
-
runtime.events.publish('faq:feedback', {
|
|
311
|
-
itemId,
|
|
312
|
-
question,
|
|
313
|
-
value,
|
|
314
|
-
});
|
|
315
|
-
}, [runtime.events]);
|
|
316
|
-
// Compute styles
|
|
317
|
-
const containerStyle = {
|
|
318
|
-
...baseStyles.container,
|
|
319
|
-
...themeStyles[resolvedTheme].container,
|
|
320
|
-
};
|
|
321
|
-
const searchInputStyle = {
|
|
322
|
-
...baseStyles.searchInput,
|
|
323
|
-
...themeStyles[resolvedTheme].searchInput,
|
|
324
|
-
};
|
|
325
|
-
const emptyStateStyle = {
|
|
326
|
-
...baseStyles.emptyState,
|
|
327
|
-
...themeStyles[resolvedTheme].emptyState,
|
|
328
|
-
};
|
|
329
|
-
const categoryHeaderStyle = {
|
|
330
|
-
...baseStyles.categoryHeader,
|
|
331
|
-
...themeStyles[resolvedTheme].categoryHeader,
|
|
332
|
-
};
|
|
333
|
-
// Render a list of FAQ items
|
|
334
|
-
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)));
|
|
335
|
-
// Empty state (no visible questions at all)
|
|
336
|
-
if (visibleQuestions.length === 0) {
|
|
337
|
-
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." }) }));
|
|
338
|
-
}
|
|
339
|
-
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
|
|
340
|
-
? 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')))
|
|
341
|
-
: renderItems(filteredQuestions) }), config.searchable && filteredQuestions.length === 0 && searchQuery && (_jsxs("div", { style: { ...baseStyles.noResults, ...themeStyles[resolvedTheme].emptyState }, children: ["No questions found matching \"", searchQuery, "\""] }))] }));
|
|
342
|
-
}
|
|
343
|
-
// ============================================================================
|
|
344
|
-
// Mountable Widget Interface
|
|
345
|
-
// ============================================================================
|
|
346
|
-
/**
|
|
347
|
-
* Mountable widget interface for the runtime's WidgetRegistry.
|
|
348
|
-
*/
|
|
349
|
-
export const FAQMountableWidget = {
|
|
350
|
-
mount(container, config) {
|
|
351
|
-
const { runtime, instanceId = 'faq-widget', ...faqConfig } = config || {
|
|
352
|
-
expandBehavior: 'single',
|
|
353
|
-
searchable: false,
|
|
354
|
-
theme: 'auto',
|
|
355
|
-
actions: [],
|
|
356
|
-
};
|
|
357
|
-
// React rendering when runtime + ReactDOM are available
|
|
358
|
-
if (runtime && typeof createRoot === 'function') {
|
|
359
|
-
const root = createRoot(container);
|
|
360
|
-
root.render(React.createElement(FAQWidget, {
|
|
361
|
-
config: faqConfig,
|
|
362
|
-
runtime: runtime,
|
|
363
|
-
instanceId,
|
|
364
|
-
}));
|
|
365
|
-
return () => {
|
|
366
|
-
root.unmount();
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
// HTML fallback when React is not available
|
|
370
|
-
const questions = faqConfig.actions || [];
|
|
371
|
-
container.innerHTML = `
|
|
372
|
-
<div style="font-family: system-ui; max-width: 800px;">
|
|
373
|
-
${questions
|
|
374
|
-
.map((q) => `
|
|
375
|
-
<div style="margin-bottom: 8px; padding: 16px; background: ${slateGrey[12]}; border-radius: 8px;">
|
|
376
|
-
<strong>${q.config.question}</strong>
|
|
377
|
-
<p style="margin-top: 8px; color: ${slateGrey[6]};">${getAnswerText(q.config.answer)}</p>
|
|
378
|
-
</div>
|
|
379
|
-
`)
|
|
380
|
-
.join('')}
|
|
381
|
-
</div>
|
|
382
|
-
`;
|
|
383
|
-
return () => {
|
|
384
|
-
container.innerHTML = '';
|
|
385
|
-
};
|
|
386
|
-
},
|
|
387
|
-
};
|
|
388
|
-
export default FAQWidget;
|
package/dist/cdn.d.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CDN Entry Point for Adaptive FAQ
|
|
3
|
-
*
|
|
4
|
-
* This module is bundled for CDN delivery and self-registers with the global
|
|
5
|
-
* SynOS app registry when loaded dynamically via the AppLoader.
|
|
6
|
-
*/
|
|
7
|
-
import FAQEditor from './editor';
|
|
8
|
-
/**
|
|
9
|
-
* App manifest for registry registration.
|
|
10
|
-
* Follows the AppManifest interface expected by AppLoader/AppRegistry.
|
|
11
|
-
*/
|
|
12
|
-
export declare const manifest: {
|
|
13
|
-
id: string;
|
|
14
|
-
version: string;
|
|
15
|
-
name: string;
|
|
16
|
-
description: string;
|
|
17
|
-
runtime: {
|
|
18
|
-
actions: readonly [{
|
|
19
|
-
readonly kind: "faq:scroll_to";
|
|
20
|
-
readonly executor: typeof import("./executors").executeScrollToFaq;
|
|
21
|
-
}, {
|
|
22
|
-
readonly kind: "faq:toggle_item";
|
|
23
|
-
readonly executor: typeof import("./executors").executeToggleFaqItem;
|
|
24
|
-
}, {
|
|
25
|
-
readonly kind: "faq:update";
|
|
26
|
-
readonly executor: typeof import("./executors").executeUpdateFaq;
|
|
27
|
-
}];
|
|
28
|
-
widgets: {
|
|
29
|
-
id: string;
|
|
30
|
-
component: {
|
|
31
|
-
mount(container: HTMLElement, config?: import("./types").FAQConfig & {
|
|
32
|
-
runtime?: import("./faq-types").FAQWidgetRuntime;
|
|
33
|
-
instanceId?: string;
|
|
34
|
-
}): () => void;
|
|
35
|
-
};
|
|
36
|
-
metadata: {
|
|
37
|
-
name: string;
|
|
38
|
-
description: string;
|
|
39
|
-
icon: string;
|
|
40
|
-
subtitle: string;
|
|
41
|
-
};
|
|
42
|
-
}[];
|
|
43
|
-
notifyWatchers: (props: Record<string, unknown>) => {
|
|
44
|
-
id: string;
|
|
45
|
-
strategy: import("./types").DecisionStrategy<boolean>;
|
|
46
|
-
eventName: string;
|
|
47
|
-
eventProps: {
|
|
48
|
-
questionId: string;
|
|
49
|
-
question: string;
|
|
50
|
-
title: string | undefined;
|
|
51
|
-
body: string | undefined;
|
|
52
|
-
icon: string | undefined;
|
|
53
|
-
};
|
|
54
|
-
}[];
|
|
55
|
-
};
|
|
56
|
-
editor: {
|
|
57
|
-
component: typeof FAQEditor;
|
|
58
|
-
panel: {
|
|
59
|
-
title: string;
|
|
60
|
-
icon: string;
|
|
61
|
-
description: string;
|
|
62
|
-
};
|
|
63
|
-
getActionLabel(action: Record<string, unknown>): string;
|
|
64
|
-
};
|
|
65
|
-
metadata: {
|
|
66
|
-
isBuiltIn: boolean;
|
|
67
|
-
};
|
|
68
|
-
};
|
|
69
|
-
export default manifest;
|
|
70
|
-
//# sourceMappingURL=cdn.d.ts.map
|
package/dist/cdn.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cdn.d.ts","sourceRoot":"","sources":["../src/cdn.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,SAA0B,MAAM,UAAU,CAAC;AAGlD;;;GAGG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;2BAoC4jiB,CAAC;8BAA8B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAtBtliB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;CAQjD,CAAC;AAaF,eAAe,QAAQ,CAAC"}
|
package/dist/cdn.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CDN Entry Point for Adaptive FAQ
|
|
3
|
-
*
|
|
4
|
-
* This module is bundled for CDN delivery and self-registers with the global
|
|
5
|
-
* SynOS app registry when loaded dynamically via the AppLoader.
|
|
6
|
-
*/
|
|
7
|
-
import FAQEditor, { editorPanel } from './editor';
|
|
8
|
-
import { runtime } from './runtime';
|
|
9
|
-
/**
|
|
10
|
-
* App manifest for registry registration.
|
|
11
|
-
* Follows the AppManifest interface expected by AppLoader/AppRegistry.
|
|
12
|
-
*/
|
|
13
|
-
export const manifest = {
|
|
14
|
-
id: 'adaptive-faq',
|
|
15
|
-
version: runtime.version,
|
|
16
|
-
name: runtime.name,
|
|
17
|
-
description: runtime.description,
|
|
18
|
-
// biome-ignore lint: manifest shape maps runtime fields to specific interface properties
|
|
19
|
-
runtime: {
|
|
20
|
-
actions: runtime.executors,
|
|
21
|
-
widgets: runtime.widgets,
|
|
22
|
-
notifyWatchers: runtime.notifyWatchers,
|
|
23
|
-
},
|
|
24
|
-
editor: {
|
|
25
|
-
component: FAQEditor,
|
|
26
|
-
panel: editorPanel,
|
|
27
|
-
getActionLabel(action) {
|
|
28
|
-
const config = action.config || {};
|
|
29
|
-
return config.question || action.kind || 'faq:update';
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
metadata: {
|
|
33
|
-
isBuiltIn: false,
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* Self-register with global registry if available.
|
|
38
|
-
* This happens when loaded via script tag (UMD).
|
|
39
|
-
*/
|
|
40
|
-
if (typeof window !== 'undefined') {
|
|
41
|
-
const registry = window.SynOS?.appRegistry;
|
|
42
|
-
if (registry && typeof registry.register === 'function') {
|
|
43
|
-
registry.register(manifest);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
export default manifest;
|
package/dist/executors.js
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adaptive FAQ - Action Executors
|
|
3
|
-
*
|
|
4
|
-
* Three executors that operate on the FAQStore:
|
|
5
|
-
* - executeScrollToFaq: scroll to a FAQ item and optionally expand it
|
|
6
|
-
* - executeToggleFaqItem: open / close / toggle a FAQ item
|
|
7
|
-
* - executeUpdateFaq: add, remove, reorder, or replace FAQ items
|
|
8
|
-
*/
|
|
9
|
-
// ============================================================================
|
|
10
|
-
// Helpers
|
|
11
|
-
// ============================================================================
|
|
12
|
-
/**
|
|
13
|
-
* Resolve a FAQ item by direct ID or by fuzzy question text match.
|
|
14
|
-
* Throws if neither yields a result.
|
|
15
|
-
*/
|
|
16
|
-
function resolveItem(store, itemId, itemQuestion) {
|
|
17
|
-
if (itemId) {
|
|
18
|
-
const found = store.getState().items.find((i) => i.config.id === itemId);
|
|
19
|
-
if (found)
|
|
20
|
-
return found;
|
|
21
|
-
}
|
|
22
|
-
if (itemQuestion) {
|
|
23
|
-
const found = store.findByQuestion(itemQuestion);
|
|
24
|
-
if (found)
|
|
25
|
-
return found;
|
|
26
|
-
}
|
|
27
|
-
throw new Error('FAQ item not found');
|
|
28
|
-
}
|
|
29
|
-
// ============================================================================
|
|
30
|
-
// executeScrollToFaq
|
|
31
|
-
// ============================================================================
|
|
32
|
-
/**
|
|
33
|
-
* Scroll to a FAQ item in the DOM and optionally expand it.
|
|
34
|
-
*
|
|
35
|
-
* Looks up the item by `itemId` or `itemQuestion`, scrolls the matching
|
|
36
|
-
* `[data-faq-item-id]` element into view, and expands it unless
|
|
37
|
-
* `expand` is explicitly set to `false`.
|
|
38
|
-
*/
|
|
39
|
-
export async function executeScrollToFaq(action, context, store) {
|
|
40
|
-
const item = resolveItem(store, action.itemId, action.itemQuestion);
|
|
41
|
-
const { id } = item.config;
|
|
42
|
-
// Expand the item unless explicitly told not to
|
|
43
|
-
if (action.expand !== false) {
|
|
44
|
-
store.expand(id);
|
|
45
|
-
}
|
|
46
|
-
// Scroll the DOM element into view (may be absent in test environments)
|
|
47
|
-
const el = document.querySelector(`[data-faq-item-id="${id}"]`);
|
|
48
|
-
if (el) {
|
|
49
|
-
el.scrollIntoView({
|
|
50
|
-
behavior: action.behavior ?? 'smooth',
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
// Publish analytics event
|
|
54
|
-
context.publishEvent('faq:scroll_to', { itemId: id });
|
|
55
|
-
return {
|
|
56
|
-
cleanup: () => {
|
|
57
|
-
// Optionally collapse on revert
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
// ============================================================================
|
|
62
|
-
// executeToggleFaqItem
|
|
63
|
-
// ============================================================================
|
|
64
|
-
/**
|
|
65
|
-
* Open, close, or toggle a FAQ item's expanded state.
|
|
66
|
-
*/
|
|
67
|
-
export async function executeToggleFaqItem(action, context, store) {
|
|
68
|
-
const item = resolveItem(store, action.itemId, action.itemQuestion);
|
|
69
|
-
const { id } = item.config;
|
|
70
|
-
const desiredState = action.state ?? 'toggle';
|
|
71
|
-
let newState;
|
|
72
|
-
switch (desiredState) {
|
|
73
|
-
case 'open':
|
|
74
|
-
store.expand(id);
|
|
75
|
-
newState = 'open';
|
|
76
|
-
break;
|
|
77
|
-
case 'closed':
|
|
78
|
-
store.collapse(id);
|
|
79
|
-
newState = 'closed';
|
|
80
|
-
break;
|
|
81
|
-
default: {
|
|
82
|
-
const wasExpanded = store.getState().expandedItems.has(id);
|
|
83
|
-
store.toggle(id);
|
|
84
|
-
newState = wasExpanded ? 'closed' : 'open';
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
context.publishEvent('faq:toggle', { itemId: id, newState });
|
|
89
|
-
return {
|
|
90
|
-
cleanup: () => {
|
|
91
|
-
// Revert toggle on cleanup
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
// ============================================================================
|
|
96
|
-
// executeUpdateFaq
|
|
97
|
-
// ============================================================================
|
|
98
|
-
/**
|
|
99
|
-
* Add, remove, reorder, or replace FAQ items in the store.
|
|
100
|
-
*/
|
|
101
|
-
export async function executeUpdateFaq(action, context, store) {
|
|
102
|
-
switch (action.operation) {
|
|
103
|
-
case 'add': {
|
|
104
|
-
const items = action.items ?? [];
|
|
105
|
-
const position = action.position === 'prepend' ? 'prepend' : 'append';
|
|
106
|
-
store.addItems(items, position);
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
case 'remove': {
|
|
110
|
-
if (!action.itemId) {
|
|
111
|
-
throw new Error('FAQ item not found');
|
|
112
|
-
}
|
|
113
|
-
// Verify the item exists before removing
|
|
114
|
-
const exists = store.getState().items.some((i) => i.config.id === action.itemId);
|
|
115
|
-
if (!exists) {
|
|
116
|
-
throw new Error('FAQ item not found');
|
|
117
|
-
}
|
|
118
|
-
store.removeItem(action.itemId);
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
case 'reorder': {
|
|
122
|
-
const order = action.order ?? [];
|
|
123
|
-
store.reorderItems(order);
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
case 'replace': {
|
|
127
|
-
const items = action.items ?? [];
|
|
128
|
-
store.replaceItems(items);
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
context.publishEvent('faq:update', { operation: action.operation });
|
|
133
|
-
return {
|
|
134
|
-
cleanup: () => {
|
|
135
|
-
// Could snapshot previous state for undo
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
// ============================================================================
|
|
140
|
-
// Executor Definitions for Registration
|
|
141
|
-
// ============================================================================
|
|
142
|
-
/**
|
|
143
|
-
* All executors provided by adaptive-faq.
|
|
144
|
-
* These are registered with the runtime's ExecutorRegistry.
|
|
145
|
-
*/
|
|
146
|
-
export const executorDefinitions = [
|
|
147
|
-
{ kind: 'faq:scroll_to', executor: executeScrollToFaq },
|
|
148
|
-
{ kind: 'faq:toggle_item', executor: executeToggleFaqItem },
|
|
149
|
-
{ kind: 'faq:update', executor: executeUpdateFaq },
|
|
150
|
-
];
|