@syntrologie/adapt-faq 2.13.0 → 2.15.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/FAQWidgetLit.d.ts +85 -0
- package/dist/FAQWidgetLit.d.ts.map +1 -0
- package/dist/FAQWidgetLit.js +534 -0
- package/dist/editor-lit.d.ts +37 -0
- package/dist/editor-lit.d.ts.map +1 -0
- package/dist/editor-lit.js +195 -0
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +3 -3
- package/dist/faq-types.d.ts +4 -0
- package/dist/faq-types.d.ts.map +1 -1
- package/dist/runtime-lit.d.ts +85 -0
- package/dist/runtime-lit.d.ts.map +1 -0
- package/dist/runtime-lit.js +94 -0
- package/dist/runtime.d.ts +17 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +28 -0
- package/dist/schema.d.ts +384 -384
- package/dist/schema.d.ts.map +1 -1
- package/node_modules/@syntrologie/sdk-contracts/dist/index.d.ts +1 -1
- package/node_modules/@syntrologie/sdk-contracts/dist/index.js +5 -3
- package/node_modules/@syntrologie/sdk-contracts/dist/schemas.d.ts +150 -79
- package/node_modules/@syntrologie/sdk-contracts/dist/schemas.js +266 -67
- package/package.json +12 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive FAQ - FAQWidgetLit
|
|
3
|
+
*
|
|
4
|
+
* Lit web component equivalent of FAQWidget.tsx.
|
|
5
|
+
* Renders a collapsible Q&A accordion with search, category grouping,
|
|
6
|
+
* feedback, and markdown rendering — all as a custom element with no
|
|
7
|
+
* Shadow DOM (light DOM via createRenderRoot).
|
|
8
|
+
*
|
|
9
|
+
* Tag name: <syntro-faq-accordion>
|
|
10
|
+
*
|
|
11
|
+
* Decorator-free: uses `static override properties` (tsconfig has no
|
|
12
|
+
* experimentalDecorators).
|
|
13
|
+
*/
|
|
14
|
+
import { LitElement } from 'lit';
|
|
15
|
+
import type { FAQWidgetRuntime } from './faq-types';
|
|
16
|
+
import type { FAQConfig, FeedbackValue } from './types';
|
|
17
|
+
/**
|
|
18
|
+
* <syntro-faq-accordion> — light-DOM Lit web component.
|
|
19
|
+
*
|
|
20
|
+
* Set properties imperatively (no attribute serialisation for objects):
|
|
21
|
+
* el.faqConfig = { expandBehavior: 'single', ... };
|
|
22
|
+
* el.runtime = runtimeInstance;
|
|
23
|
+
* el.instanceId = 'my-faq';
|
|
24
|
+
*/
|
|
25
|
+
export declare class FAQAccordionElement extends LitElement {
|
|
26
|
+
static properties: {
|
|
27
|
+
faqConfig: {
|
|
28
|
+
attribute: boolean;
|
|
29
|
+
};
|
|
30
|
+
runtime: {
|
|
31
|
+
attribute: boolean;
|
|
32
|
+
};
|
|
33
|
+
instanceId: {
|
|
34
|
+
type: StringConstructor;
|
|
35
|
+
};
|
|
36
|
+
_expandedIds: {
|
|
37
|
+
state: boolean;
|
|
38
|
+
};
|
|
39
|
+
_highlightId: {
|
|
40
|
+
state: boolean;
|
|
41
|
+
};
|
|
42
|
+
_searchQuery: {
|
|
43
|
+
state: boolean;
|
|
44
|
+
};
|
|
45
|
+
_feedbackState: {
|
|
46
|
+
state: boolean;
|
|
47
|
+
};
|
|
48
|
+
_hoveredId: {
|
|
49
|
+
state: boolean;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
faqConfig: FAQConfig;
|
|
53
|
+
runtime: FAQWidgetRuntime | null;
|
|
54
|
+
instanceId: string;
|
|
55
|
+
_expandedIds: Set<string>;
|
|
56
|
+
_highlightId: string | null;
|
|
57
|
+
_searchQuery: string;
|
|
58
|
+
_feedbackState: Map<string, FeedbackValue>;
|
|
59
|
+
_hoveredId: string | null;
|
|
60
|
+
private _unsubContext;
|
|
61
|
+
private _unsubAccumulator;
|
|
62
|
+
private _unsubCta;
|
|
63
|
+
private _unsubDeepLink;
|
|
64
|
+
private _unsubSessionMetrics;
|
|
65
|
+
private _highlightTimer;
|
|
66
|
+
createRenderRoot(): this;
|
|
67
|
+
connectedCallback(): void;
|
|
68
|
+
disconnectedCallback(): void;
|
|
69
|
+
updated(changedProps: Map<string, unknown>): void;
|
|
70
|
+
private _subscribeAll;
|
|
71
|
+
private _unsubscribeAll;
|
|
72
|
+
private _handleToggle;
|
|
73
|
+
private _handleFeedback;
|
|
74
|
+
private _visibleQuestions;
|
|
75
|
+
private _orderedQuestions;
|
|
76
|
+
private _filteredQuestions;
|
|
77
|
+
private _categoryGroups;
|
|
78
|
+
private _renderAnswer;
|
|
79
|
+
private _renderFeedback;
|
|
80
|
+
private _renderItem;
|
|
81
|
+
private _renderItems;
|
|
82
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
83
|
+
}
|
|
84
|
+
export default FAQAccordionElement;
|
|
85
|
+
//# sourceMappingURL=FAQWidgetLit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FAQWidgetLit.d.ts","sourceRoot":"","sources":["../src/FAQWidgetLit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAQ,UAAU,EAAW,MAAM,KAAK,CAAC;AAKhD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAEV,SAAS,EAGT,aAAa,EACd,MAAM,SAAS,CAAC;AA2DjB;;;;;;;GAOG;AACH,qBAAa,mBAAoB,SAAQ,UAAU;IAKjD,OAAgB,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;MAYxB;IAMF,SAAS,EAAE,SAAS,CAKlB;IAEF,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAAQ;IAExC,UAAU,EAAE,MAAM,CAAgB;IAGlC,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAa;IACtC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IACnC,YAAY,EAAE,MAAM,CAAM;IAC1B,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAa;IACvD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAQ;IAGjC,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,SAAS,CAA6B;IAC9C,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,oBAAoB,CAA6B;IACzD,OAAO,CAAC,eAAe,CAA8C;IAM5D,gBAAgB;IAQhB,iBAAiB;IAKjB,oBAAoB;IAUpB,OAAO,CAAC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC;IAWnD,OAAO,CAAC,aAAa;IAwGrB,OAAO,CAAC,eAAe;IAiBvB,OAAO,CAAC,aAAa;IA0BrB,OAAO,CAAC,eAAe;IAYvB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,eAAe;IAgBvB,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,eAAe;IAsCvB,OAAO,CAAC,WAAW;IAmFnB,OAAO,CAAC,YAAY;IAcX,MAAM;CAmHhB;AAUD,eAAe,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive FAQ - FAQWidgetLit
|
|
3
|
+
*
|
|
4
|
+
* Lit web component equivalent of FAQWidget.tsx.
|
|
5
|
+
* Renders a collapsible Q&A accordion with search, category grouping,
|
|
6
|
+
* feedback, and markdown rendering — all as a custom element with no
|
|
7
|
+
* Shadow DOM (light DOM via createRenderRoot).
|
|
8
|
+
*
|
|
9
|
+
* Tag name: <syntro-faq-accordion>
|
|
10
|
+
*
|
|
11
|
+
* Decorator-free: uses `static override properties` (tsconfig has no
|
|
12
|
+
* experimentalDecorators).
|
|
13
|
+
*/
|
|
14
|
+
import { purple } from '@syntro/design-system/tokens';
|
|
15
|
+
import { html, LitElement, nothing } from 'lit';
|
|
16
|
+
import { styleMap } from 'lit/directives/style-map.js';
|
|
17
|
+
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
|
18
|
+
import { Marked } from 'marked';
|
|
19
|
+
import { baseStyles, themeStyles } from './faq-styles';
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Utility — styleMap accepts Record<string, string | number | undefined>
|
|
22
|
+
// but its TypeScript signature is overly narrow in some Lit versions.
|
|
23
|
+
// Cast through unknown to avoid false positives when style values include
|
|
24
|
+
// numeric CSS properties (fontWeight, lineHeight, etc.).
|
|
25
|
+
// ============================================================================
|
|
26
|
+
function sm(styles) {
|
|
27
|
+
return styles;
|
|
28
|
+
}
|
|
29
|
+
const marked = new Marked({ async: false, gfm: true, breaks: true });
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Helpers (mirrored from FAQWidget.tsx)
|
|
32
|
+
// ============================================================================
|
|
33
|
+
function getAnswerText(answer) {
|
|
34
|
+
if (typeof answer === 'string')
|
|
35
|
+
return answer;
|
|
36
|
+
if (answer.type === 'rich')
|
|
37
|
+
return answer.html;
|
|
38
|
+
return answer.content;
|
|
39
|
+
}
|
|
40
|
+
function renderAnswerHtml(answer) {
|
|
41
|
+
if (typeof answer === 'string') {
|
|
42
|
+
return marked.parse(answer);
|
|
43
|
+
}
|
|
44
|
+
if (answer.type === 'rich') {
|
|
45
|
+
return answer.html;
|
|
46
|
+
}
|
|
47
|
+
return marked.parse(answer.content);
|
|
48
|
+
}
|
|
49
|
+
function resolveFeedbackConfig(feedback) {
|
|
50
|
+
if (!feedback)
|
|
51
|
+
return null;
|
|
52
|
+
if (feedback === true)
|
|
53
|
+
return { style: 'thumbs' };
|
|
54
|
+
return feedback;
|
|
55
|
+
}
|
|
56
|
+
function getFeedbackPrompt(feedbackConfig) {
|
|
57
|
+
return feedbackConfig.prompt ?? 'Was this helpful?';
|
|
58
|
+
}
|
|
59
|
+
function resolveTheme(theme) {
|
|
60
|
+
if (theme && theme !== 'auto')
|
|
61
|
+
return theme;
|
|
62
|
+
if (typeof window !== 'undefined') {
|
|
63
|
+
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
64
|
+
}
|
|
65
|
+
return 'light';
|
|
66
|
+
}
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// FAQAccordionElement — Lit element
|
|
69
|
+
// ============================================================================
|
|
70
|
+
/**
|
|
71
|
+
* <syntro-faq-accordion> — light-DOM Lit web component.
|
|
72
|
+
*
|
|
73
|
+
* Set properties imperatively (no attribute serialisation for objects):
|
|
74
|
+
* el.faqConfig = { expandBehavior: 'single', ... };
|
|
75
|
+
* el.runtime = runtimeInstance;
|
|
76
|
+
* el.instanceId = 'my-faq';
|
|
77
|
+
*/
|
|
78
|
+
export class FAQAccordionElement extends LitElement {
|
|
79
|
+
constructor() {
|
|
80
|
+
// -----------------------------------------------------------------------
|
|
81
|
+
// Reactive properties (no decorators — tsconfig forbids experimentalDecorators)
|
|
82
|
+
// -----------------------------------------------------------------------
|
|
83
|
+
super(...arguments);
|
|
84
|
+
// -----------------------------------------------------------------------
|
|
85
|
+
// Property declarations
|
|
86
|
+
// -----------------------------------------------------------------------
|
|
87
|
+
this.faqConfig = {
|
|
88
|
+
expandBehavior: 'single',
|
|
89
|
+
searchable: false,
|
|
90
|
+
theme: 'auto',
|
|
91
|
+
actions: [],
|
|
92
|
+
};
|
|
93
|
+
this.runtime = null;
|
|
94
|
+
this.instanceId = 'faq-widget';
|
|
95
|
+
// Internal state
|
|
96
|
+
this._expandedIds = new Set();
|
|
97
|
+
this._highlightId = null;
|
|
98
|
+
this._searchQuery = '';
|
|
99
|
+
this._feedbackState = new Map();
|
|
100
|
+
this._hoveredId = null;
|
|
101
|
+
// Subscription cleanup handles
|
|
102
|
+
this._unsubContext = null;
|
|
103
|
+
this._unsubAccumulator = null;
|
|
104
|
+
this._unsubCta = null;
|
|
105
|
+
this._unsubDeepLink = null;
|
|
106
|
+
this._unsubSessionMetrics = null;
|
|
107
|
+
this._highlightTimer = null;
|
|
108
|
+
}
|
|
109
|
+
// -----------------------------------------------------------------------
|
|
110
|
+
// Light DOM — no Shadow DOM so CSS variables from the host page apply
|
|
111
|
+
// -----------------------------------------------------------------------
|
|
112
|
+
createRenderRoot() {
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
// -----------------------------------------------------------------------
|
|
116
|
+
// Lifecycle
|
|
117
|
+
// -----------------------------------------------------------------------
|
|
118
|
+
connectedCallback() {
|
|
119
|
+
super.connectedCallback();
|
|
120
|
+
this._subscribeAll();
|
|
121
|
+
}
|
|
122
|
+
disconnectedCallback() {
|
|
123
|
+
super.disconnectedCallback();
|
|
124
|
+
this._unsubscribeAll();
|
|
125
|
+
if (this._highlightTimer !== null) {
|
|
126
|
+
clearTimeout(this._highlightTimer);
|
|
127
|
+
this._highlightTimer = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Re-subscribe when runtime changes (property may be set after connectedCallback)
|
|
131
|
+
updated(changedProps) {
|
|
132
|
+
if (changedProps.has('runtime')) {
|
|
133
|
+
this._unsubscribeAll();
|
|
134
|
+
this._subscribeAll();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// -----------------------------------------------------------------------
|
|
138
|
+
// Subscription management
|
|
139
|
+
// -----------------------------------------------------------------------
|
|
140
|
+
_subscribeAll() {
|
|
141
|
+
if (!this.runtime)
|
|
142
|
+
return;
|
|
143
|
+
// Context changes → force re-render
|
|
144
|
+
this._unsubContext = this.runtime.context.subscribe(() => {
|
|
145
|
+
this.requestUpdate();
|
|
146
|
+
});
|
|
147
|
+
// Accumulator changes → force re-render (for event_count triggerWhen)
|
|
148
|
+
if (this.runtime.accumulator?.subscribe) {
|
|
149
|
+
this._unsubAccumulator = this.runtime.accumulator.subscribe(() => {
|
|
150
|
+
this.requestUpdate();
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// Session metric changes → force re-render (for session_metric triggerWhen)
|
|
154
|
+
if (this.runtime.sessionMetrics?.subscribe) {
|
|
155
|
+
this._unsubSessionMetrics = this.runtime.sessionMetrics.subscribe(() => {
|
|
156
|
+
this.requestUpdate();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// faq:open:* events from overlay CTA clicks
|
|
160
|
+
if (this.runtime.events.subscribe) {
|
|
161
|
+
// Check EventBus history for pending faq:open events
|
|
162
|
+
if (this.runtime.events.getRecent) {
|
|
163
|
+
const recentEvents = this.runtime.events.getRecent({ patterns: ['^action\\.tooltip_cta_clicked$', '^action\\.modal_cta_clicked$'] }, 10);
|
|
164
|
+
const pendingEvent = recentEvents
|
|
165
|
+
.filter((e) => {
|
|
166
|
+
const actionId = e.props?.actionId;
|
|
167
|
+
return typeof actionId === 'string' && actionId.startsWith('faq:open:');
|
|
168
|
+
})
|
|
169
|
+
.pop();
|
|
170
|
+
if (pendingEvent && Date.now() - pendingEvent.ts < 10000) {
|
|
171
|
+
const questionId = pendingEvent.props.actionId.replace('faq:open:', '');
|
|
172
|
+
this._expandedIds = new Set([questionId]);
|
|
173
|
+
requestAnimationFrame(() => {
|
|
174
|
+
const el = document.querySelector(`[data-faq-item-id="${questionId}"]`);
|
|
175
|
+
if (el)
|
|
176
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
this._unsubCta = this.runtime.events.subscribe({ patterns: ['^action\\.tooltip_cta_clicked$', '^action\\.modal_cta_clicked$'] }, (event) => {
|
|
181
|
+
const actionId = event.props?.actionId;
|
|
182
|
+
if (typeof actionId !== 'string' || !actionId.startsWith('faq:open:'))
|
|
183
|
+
return;
|
|
184
|
+
const questionId = actionId.replace('faq:open:', '');
|
|
185
|
+
this._expandedIds = new Set([questionId]);
|
|
186
|
+
requestAnimationFrame(() => {
|
|
187
|
+
const el = document.querySelector(`[data-faq-item-id="${questionId}"]`);
|
|
188
|
+
if (el)
|
|
189
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
190
|
+
});
|
|
191
|
+
this.runtime?.events.publish('canvas.requestOpen');
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// notification.deep_link events
|
|
195
|
+
if (this.runtime.events.subscribe) {
|
|
196
|
+
const handleDeepLink = (event) => {
|
|
197
|
+
const tileId = event.props?.tileId;
|
|
198
|
+
const itemId = event.props?.itemId;
|
|
199
|
+
if (tileId !== this.instanceId)
|
|
200
|
+
return;
|
|
201
|
+
if (!itemId)
|
|
202
|
+
return;
|
|
203
|
+
this._expandedIds = new Set([itemId]);
|
|
204
|
+
this._highlightId = itemId;
|
|
205
|
+
if (this._highlightTimer !== null)
|
|
206
|
+
clearTimeout(this._highlightTimer);
|
|
207
|
+
this._highlightTimer = setTimeout(() => {
|
|
208
|
+
this._highlightId = null;
|
|
209
|
+
this._highlightTimer = null;
|
|
210
|
+
}, 1500);
|
|
211
|
+
requestAnimationFrame(() => {
|
|
212
|
+
const el = document.querySelector(`[data-faq-item-id="${itemId}"]`);
|
|
213
|
+
if (el)
|
|
214
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
// Check recent events (may have fired before widget mounted)
|
|
218
|
+
if (this.runtime.events.getRecent) {
|
|
219
|
+
const recent = this.runtime.events.getRecent({ names: ['notification.deep_link'] }, 5);
|
|
220
|
+
const pending = recent
|
|
221
|
+
.filter((e) => e.props?.tileId === this.instanceId && e.props?.itemId)
|
|
222
|
+
.pop();
|
|
223
|
+
if (pending && Date.now() - pending.ts < 10000) {
|
|
224
|
+
handleDeepLink(pending);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
this._unsubDeepLink = this.runtime.events.subscribe({ names: ['notification.deep_link'] }, handleDeepLink);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
_unsubscribeAll() {
|
|
231
|
+
this._unsubContext?.();
|
|
232
|
+
this._unsubAccumulator?.();
|
|
233
|
+
this._unsubSessionMetrics?.();
|
|
234
|
+
this._unsubCta?.();
|
|
235
|
+
this._unsubDeepLink?.();
|
|
236
|
+
this._unsubContext = null;
|
|
237
|
+
this._unsubAccumulator = null;
|
|
238
|
+
this._unsubSessionMetrics = null;
|
|
239
|
+
this._unsubCta = null;
|
|
240
|
+
this._unsubDeepLink = null;
|
|
241
|
+
}
|
|
242
|
+
// -----------------------------------------------------------------------
|
|
243
|
+
// Handlers
|
|
244
|
+
// -----------------------------------------------------------------------
|
|
245
|
+
_handleToggle(id) {
|
|
246
|
+
const prev = this._expandedIds;
|
|
247
|
+
let next;
|
|
248
|
+
if (this.faqConfig.expandBehavior === 'single') {
|
|
249
|
+
next = prev.has(id) ? new Set() : new Set([id]);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
next = new Set(prev);
|
|
253
|
+
if (prev.has(id)) {
|
|
254
|
+
next.delete(id);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
next.add(id);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const willBeExpanded = !prev.has(id);
|
|
261
|
+
this._expandedIds = next;
|
|
262
|
+
this.runtime?.events.publish('faq:toggled', {
|
|
263
|
+
instanceId: this.instanceId,
|
|
264
|
+
questionId: id,
|
|
265
|
+
expanded: willBeExpanded,
|
|
266
|
+
timestamp: Date.now(),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
_handleFeedback(itemId, question, value) {
|
|
270
|
+
const next = new Map(this._feedbackState);
|
|
271
|
+
next.set(itemId, value);
|
|
272
|
+
this._feedbackState = next;
|
|
273
|
+
this.runtime?.events.publish('faq:feedback', { itemId, question, value });
|
|
274
|
+
}
|
|
275
|
+
// -----------------------------------------------------------------------
|
|
276
|
+
// Computed helpers
|
|
277
|
+
// -----------------------------------------------------------------------
|
|
278
|
+
_visibleQuestions() {
|
|
279
|
+
return (this.faqConfig.actions ?? []).filter((q) => {
|
|
280
|
+
if (!q.triggerWhen)
|
|
281
|
+
return true;
|
|
282
|
+
if (!this.runtime)
|
|
283
|
+
return true;
|
|
284
|
+
const result = this.runtime.evaluateSync(q.triggerWhen);
|
|
285
|
+
return result.value;
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
_orderedQuestions(visible) {
|
|
289
|
+
if (this.faqConfig.ordering === 'priority') {
|
|
290
|
+
return [...visible].sort((a, b) => (b.config.priority ?? 0) - (a.config.priority ?? 0));
|
|
291
|
+
}
|
|
292
|
+
return visible;
|
|
293
|
+
}
|
|
294
|
+
_filteredQuestions(ordered) {
|
|
295
|
+
const q = this._searchQuery.trim().toLowerCase();
|
|
296
|
+
if (!this.faqConfig.searchable || !q)
|
|
297
|
+
return ordered;
|
|
298
|
+
return ordered.filter((item) => item.config.question.toLowerCase().includes(q) ||
|
|
299
|
+
getAnswerText(item.config.answer).toLowerCase().includes(q) ||
|
|
300
|
+
item.config.category?.toLowerCase().includes(q));
|
|
301
|
+
}
|
|
302
|
+
_categoryGroups(filtered) {
|
|
303
|
+
const groups = new Map();
|
|
304
|
+
for (const item of filtered) {
|
|
305
|
+
const cat = item.config.category;
|
|
306
|
+
if (!groups.has(cat))
|
|
307
|
+
groups.set(cat, []);
|
|
308
|
+
groups.get(cat).push(item);
|
|
309
|
+
}
|
|
310
|
+
return groups;
|
|
311
|
+
}
|
|
312
|
+
// -----------------------------------------------------------------------
|
|
313
|
+
// Render helpers
|
|
314
|
+
// -----------------------------------------------------------------------
|
|
315
|
+
_renderAnswer(answer) {
|
|
316
|
+
const html_str = renderAnswerHtml(answer);
|
|
317
|
+
return html `<div style="margin:0" data-faq-markdown="">${unsafeHTML(html_str)}</div>`;
|
|
318
|
+
}
|
|
319
|
+
_renderFeedback(item, feedbackConfig, feedbackValue, theme) {
|
|
320
|
+
const colors = themeStyles[theme];
|
|
321
|
+
const feedbackStyle = { ...baseStyles.feedback, ...colors.feedbackPrompt };
|
|
322
|
+
return html `
|
|
323
|
+
<div style=${styleMap(sm(feedbackStyle))}>
|
|
324
|
+
<span>${getFeedbackPrompt(feedbackConfig)}</span>
|
|
325
|
+
<button
|
|
326
|
+
type="button"
|
|
327
|
+
style=${styleMap(sm({
|
|
328
|
+
...baseStyles.feedbackButton,
|
|
329
|
+
...(feedbackValue === 'up' ? baseStyles.feedbackButtonSelected : {}),
|
|
330
|
+
}))}
|
|
331
|
+
aria-label="Thumbs up"
|
|
332
|
+
@click=${() => this._handleFeedback(item.config.id, item.config.question, 'up')}
|
|
333
|
+
>\uD83D\uDC4D</button>
|
|
334
|
+
<button
|
|
335
|
+
type="button"
|
|
336
|
+
style=${styleMap(sm({
|
|
337
|
+
...baseStyles.feedbackButton,
|
|
338
|
+
...(feedbackValue === 'down' ? baseStyles.feedbackButtonSelected : {}),
|
|
339
|
+
}))}
|
|
340
|
+
aria-label="Thumbs down"
|
|
341
|
+
@click=${() => this._handleFeedback(item.config.id, item.config.question, 'down')}
|
|
342
|
+
>\uD83D\uDC4E</button>
|
|
343
|
+
</div>
|
|
344
|
+
`;
|
|
345
|
+
}
|
|
346
|
+
_renderItem(item, isLast, theme, feedbackConfig) {
|
|
347
|
+
const colors = themeStyles[theme];
|
|
348
|
+
const isExpanded = this._expandedIds.has(item.config.id);
|
|
349
|
+
const isHighlighted = this._highlightId === item.config.id;
|
|
350
|
+
const isHovered = this._hoveredId === item.config.id;
|
|
351
|
+
const itemStyle = {
|
|
352
|
+
...baseStyles.item,
|
|
353
|
+
...colors.item,
|
|
354
|
+
...(isExpanded ? colors.itemExpanded : {}),
|
|
355
|
+
...(isHighlighted
|
|
356
|
+
? {
|
|
357
|
+
boxShadow: `0 0 0 2px ${purple[4]}, 0 0 12px rgba(106, 89, 206, 0.4)`,
|
|
358
|
+
transition: 'box-shadow 0.3s ease',
|
|
359
|
+
}
|
|
360
|
+
: {}),
|
|
361
|
+
...(!isLast ? { borderBottom: 'var(--sc-content-item-divider, none)' } : {}),
|
|
362
|
+
};
|
|
363
|
+
const questionStyle = {
|
|
364
|
+
...baseStyles.question,
|
|
365
|
+
...colors.question,
|
|
366
|
+
...(isHovered ? colors.questionHover : {}),
|
|
367
|
+
};
|
|
368
|
+
const chevronStyle = {
|
|
369
|
+
...baseStyles.chevron,
|
|
370
|
+
transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
|
371
|
+
};
|
|
372
|
+
const answerStyle = {
|
|
373
|
+
...baseStyles.answer,
|
|
374
|
+
...colors.answer,
|
|
375
|
+
maxHeight: isExpanded ? '500px' : '0',
|
|
376
|
+
paddingBottom: isExpanded ? '16px' : '0',
|
|
377
|
+
};
|
|
378
|
+
return html `
|
|
379
|
+
<div
|
|
380
|
+
style=${styleMap(sm(itemStyle))}
|
|
381
|
+
data-faq-item-id=${item.config.id}
|
|
382
|
+
>
|
|
383
|
+
<button
|
|
384
|
+
type="button"
|
|
385
|
+
style=${styleMap(sm(questionStyle))}
|
|
386
|
+
aria-expanded=${isExpanded}
|
|
387
|
+
@click=${() => this._handleToggle(item.config.id)}
|
|
388
|
+
@mouseenter=${() => {
|
|
389
|
+
this._hoveredId = item.config.id;
|
|
390
|
+
}}
|
|
391
|
+
@mouseleave=${() => {
|
|
392
|
+
this._hoveredId = null;
|
|
393
|
+
}}
|
|
394
|
+
>
|
|
395
|
+
<span>${item.config.question}</span>
|
|
396
|
+
<span style=${styleMap(sm(chevronStyle))}>\u203A</span>
|
|
397
|
+
</button>
|
|
398
|
+
|
|
399
|
+
<div
|
|
400
|
+
style=${styleMap(sm(answerStyle))}
|
|
401
|
+
aria-hidden=${!isExpanded}
|
|
402
|
+
>
|
|
403
|
+
${this._renderAnswer(item.config.answer)}
|
|
404
|
+
${isExpanded && feedbackConfig
|
|
405
|
+
? this._renderFeedback(item, feedbackConfig, this._feedbackState.get(item.config.id), theme)
|
|
406
|
+
: nothing}
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
`;
|
|
410
|
+
}
|
|
411
|
+
_renderItems(items, theme, feedbackConfig) {
|
|
412
|
+
return items.map((item, index) => this._renderItem(item, index === items.length - 1, theme, feedbackConfig));
|
|
413
|
+
}
|
|
414
|
+
// -----------------------------------------------------------------------
|
|
415
|
+
// Render
|
|
416
|
+
// -----------------------------------------------------------------------
|
|
417
|
+
render() {
|
|
418
|
+
const theme = resolveTheme(this.faqConfig.theme);
|
|
419
|
+
const colors = themeStyles[theme];
|
|
420
|
+
const feedbackConfig = resolveFeedbackConfig(this.faqConfig.feedback);
|
|
421
|
+
const visible = this._visibleQuestions();
|
|
422
|
+
const ordered = this._orderedQuestions(visible);
|
|
423
|
+
const filtered = this._filteredQuestions(ordered);
|
|
424
|
+
const hasCategories = filtered.some((q) => q.config.category);
|
|
425
|
+
const groups = hasCategories ? this._categoryGroups(filtered) : null;
|
|
426
|
+
const containerStyle = {
|
|
427
|
+
...baseStyles.container,
|
|
428
|
+
...colors.container,
|
|
429
|
+
};
|
|
430
|
+
const emptyStateStyle = {
|
|
431
|
+
...baseStyles.emptyState,
|
|
432
|
+
...colors.emptyState,
|
|
433
|
+
};
|
|
434
|
+
const categoryHeaderStyle = {
|
|
435
|
+
...baseStyles.categoryHeader,
|
|
436
|
+
...colors.categoryHeader,
|
|
437
|
+
};
|
|
438
|
+
const searchInputStyle = {
|
|
439
|
+
...baseStyles.searchInput,
|
|
440
|
+
...colors.searchInput,
|
|
441
|
+
};
|
|
442
|
+
// Empty state — no visible questions at all
|
|
443
|
+
if (visible.length === 0) {
|
|
444
|
+
return html `
|
|
445
|
+
<div
|
|
446
|
+
style=${styleMap(sm(containerStyle))}
|
|
447
|
+
data-adaptive-id=${this.instanceId}
|
|
448
|
+
data-adaptive-type="adaptive-faq"
|
|
449
|
+
>
|
|
450
|
+
<div style=${styleMap(sm(emptyStateStyle))}>
|
|
451
|
+
You're all set for now! We'll surface answers here when they're relevant to what
|
|
452
|
+
you're doing.
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
`;
|
|
456
|
+
}
|
|
457
|
+
return html `
|
|
458
|
+
<div
|
|
459
|
+
style=${styleMap(sm(containerStyle))}
|
|
460
|
+
data-adaptive-id=${this.instanceId}
|
|
461
|
+
data-adaptive-type="adaptive-faq"
|
|
462
|
+
>
|
|
463
|
+
${this.faqConfig.searchable
|
|
464
|
+
? html `
|
|
465
|
+
<div style=${styleMap(sm(baseStyles.searchWrapper))}>
|
|
466
|
+
<style>
|
|
467
|
+
[data-adaptive-id="${this.instanceId}"] input::placeholder {
|
|
468
|
+
color: var(--sc-content-search-color, inherit);
|
|
469
|
+
opacity: 0.7;
|
|
470
|
+
}
|
|
471
|
+
</style>
|
|
472
|
+
<input
|
|
473
|
+
type="text"
|
|
474
|
+
placeholder="Search questions..."
|
|
475
|
+
.value=${this._searchQuery}
|
|
476
|
+
style=${styleMap(sm(searchInputStyle))}
|
|
477
|
+
@input=${(e) => {
|
|
478
|
+
this._searchQuery = e.target.value;
|
|
479
|
+
}}
|
|
480
|
+
/>
|
|
481
|
+
</div>
|
|
482
|
+
`
|
|
483
|
+
: nothing}
|
|
484
|
+
|
|
485
|
+
<div style=${styleMap(sm(baseStyles.accordion))}>
|
|
486
|
+
${groups
|
|
487
|
+
? Array.from(groups.entries()).map(([category, items]) => html `
|
|
488
|
+
${category
|
|
489
|
+
? html `
|
|
490
|
+
<div
|
|
491
|
+
style=${styleMap(sm(categoryHeaderStyle))}
|
|
492
|
+
data-category-header=${category}
|
|
493
|
+
>
|
|
494
|
+
${category}
|
|
495
|
+
</div>
|
|
496
|
+
`
|
|
497
|
+
: nothing}
|
|
498
|
+
${this._renderItems(items, theme, feedbackConfig)}
|
|
499
|
+
`)
|
|
500
|
+
: this._renderItems(filtered, theme, feedbackConfig)}
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
${this.faqConfig.searchable && filtered.length === 0 && this._searchQuery
|
|
504
|
+
? html `
|
|
505
|
+
<div
|
|
506
|
+
style=${styleMap(sm({ ...baseStyles.noResults, ...colors.emptyState }))}
|
|
507
|
+
>
|
|
508
|
+
No questions found matching "${this._searchQuery}"
|
|
509
|
+
</div>
|
|
510
|
+
`
|
|
511
|
+
: nothing}
|
|
512
|
+
</div>
|
|
513
|
+
`;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
FAQAccordionElement.properties = {
|
|
517
|
+
// Public API — set from the outside
|
|
518
|
+
faqConfig: { attribute: false },
|
|
519
|
+
runtime: { attribute: false },
|
|
520
|
+
instanceId: { type: String },
|
|
521
|
+
// Internal reactive state (prefixed with _ to signal "private")
|
|
522
|
+
_expandedIds: { state: true },
|
|
523
|
+
_highlightId: { state: true },
|
|
524
|
+
_searchQuery: { state: true },
|
|
525
|
+
_feedbackState: { state: true },
|
|
526
|
+
_hoveredId: { state: true },
|
|
527
|
+
};
|
|
528
|
+
// ============================================================================
|
|
529
|
+
// Custom element registration
|
|
530
|
+
// ============================================================================
|
|
531
|
+
if (!customElements.get('syntro-faq-accordion')) {
|
|
532
|
+
customElements.define('syntro-faq-accordion', FAQAccordionElement);
|
|
533
|
+
}
|
|
534
|
+
export default FAQAccordionElement;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive FAQ - Lit Editor Component
|
|
3
|
+
*
|
|
4
|
+
* Lit web component port of the React FAQ editor (editor.tsx).
|
|
5
|
+
* Displays FAQ question cards with inline editing, detection,
|
|
6
|
+
* dismiss/restore, and rationale display.
|
|
7
|
+
*
|
|
8
|
+
* Custom events:
|
|
9
|
+
* navigate-home — user clicked back
|
|
10
|
+
* dirty-change — { dirty: boolean }
|
|
11
|
+
*/
|
|
12
|
+
import { LitElement } from 'lit';
|
|
13
|
+
import { type FAQConfig } from './types';
|
|
14
|
+
export declare class FAQEditorLit extends LitElement {
|
|
15
|
+
static properties: {
|
|
16
|
+
config: {
|
|
17
|
+
attribute: boolean;
|
|
18
|
+
};
|
|
19
|
+
onChange: {
|
|
20
|
+
attribute: boolean;
|
|
21
|
+
};
|
|
22
|
+
_editingKey: {
|
|
23
|
+
state: boolean;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
config: FAQConfig | null;
|
|
27
|
+
onChange: ((updated: Record<string, unknown>) => void) | null;
|
|
28
|
+
private _editingKey;
|
|
29
|
+
createRenderRoot(): this;
|
|
30
|
+
private _handleBack;
|
|
31
|
+
private _handleItemClick;
|
|
32
|
+
private _handleFieldChange;
|
|
33
|
+
private _renderEditMode;
|
|
34
|
+
private _renderListMode;
|
|
35
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=editor-lit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editor-lit.d.ts","sourceRoot":"","sources":["../src/editor-lit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAQ,UAAU,EAAW,MAAM,KAAK,CAAC;AAGhD,OAAO,EAAkB,KAAK,SAAS,EAAuC,MAAM,SAAS,CAAC;AAqC9F,qBAAa,YAAa,SAAQ,UAAU;IAC1C,OAAgB,UAAU;;;;;;;;;;MAIxB;IAEF,MAAM,EAAE,SAAS,GAAG,IAAI,CAAQ;IAChC,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,GAAG,IAAI,CAAQ;IAErE,OAAO,CAAC,WAAW,CAAuB;IAEjC,gBAAgB;IAQzB,OAAO,CAAC,WAAW,CAMjB;IAEF,OAAO,CAAC,gBAAgB,CAEtB;IAEF,OAAO,CAAC,kBAAkB,CAaxB;IAIF,OAAO,CAAC,eAAe,CAsDrB;IAEF,OAAO,CAAC,eAAe,CAoCrB;IAEO,MAAM;CAoChB"}
|