@syntrologie/adapt-faq 0.0.0-semantically-released

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.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Adaptive FAQ - FAQWidget Component
3
+ *
4
+ * React component that renders a collapsible Q&A accordion with per-item
5
+ * conditional visibility based on showWhen decision strategies.
6
+ *
7
+ * Demonstrates the compositional action pattern where child actions
8
+ * (faq:question) serve as configuration data for the parent widget.
9
+ */
10
+ import type { FAQWidgetProps, FAQConfig, FAQWidgetRuntime } from './types';
11
+ /**
12
+ * FAQWidget - Renders a collapsible Q&A accordion with per-item activation.
13
+ *
14
+ * This component demonstrates the compositional action pattern:
15
+ * - Parent (FAQWidget) receives `config.actions` array
16
+ * - Each action has optional `showWhen` for per-item visibility
17
+ * - Parent evaluates showWhen and filters visible questions
18
+ * - Parent manages expand state and re-rendering on context changes
19
+ */
20
+ export declare function FAQWidget({ config, runtime, instanceId }: FAQWidgetProps): import("react/jsx-runtime").JSX.Element;
21
+ /**
22
+ * Mountable widget interface for the runtime's WidgetRegistry.
23
+ */
24
+ export declare const FAQMountableWidget: {
25
+ mount(container: HTMLElement, config?: FAQConfig & {
26
+ runtime?: FAQWidgetRuntime;
27
+ instanceId?: string;
28
+ }): () => void;
29
+ };
30
+ export default FAQWidget;
31
+ //# sourceMappingURL=FAQWidget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FAQWidget.d.ts","sourceRoot":"","sources":["../src/FAQWidget.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAqB,SAAS,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AA6N9F;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,cAAc,2CAuJxE;AAMD;;GAEG;AACH,eAAO,MAAM,kBAAkB;qBAEhB,WAAW,WACb,SAAS,GAAG;QAAE,OAAO,CAAC,EAAE,gBAAgB,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;CAuC3E,CAAC;AAEF,eAAe,SAAS,CAAC"}
@@ -0,0 +1,332 @@
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 showWhen 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 { useEffect, useReducer, useMemo, useCallback, useState } from 'react';
12
+ // ============================================================================
13
+ // Styles
14
+ // ============================================================================
15
+ const baseStyles = {
16
+ container: {
17
+ fontFamily: 'system-ui, -apple-system, sans-serif',
18
+ maxWidth: '800px',
19
+ margin: '0 auto',
20
+ },
21
+ searchWrapper: {
22
+ marginBottom: '16px',
23
+ },
24
+ searchInput: {
25
+ width: '100%',
26
+ padding: '12px 16px',
27
+ borderRadius: '8px',
28
+ fontSize: '14px',
29
+ outline: 'none',
30
+ transition: 'border-color 0.15s ease',
31
+ },
32
+ accordion: {
33
+ display: 'flex',
34
+ flexDirection: 'column',
35
+ gap: '8px',
36
+ },
37
+ item: {
38
+ borderRadius: '8px',
39
+ overflow: 'hidden',
40
+ transition: 'box-shadow 0.15s ease',
41
+ },
42
+ question: {
43
+ width: '100%',
44
+ padding: '16px 20px',
45
+ display: 'flex',
46
+ alignItems: 'center',
47
+ justifyContent: 'space-between',
48
+ border: 'none',
49
+ cursor: 'pointer',
50
+ fontSize: '15px',
51
+ fontWeight: 500,
52
+ textAlign: 'left',
53
+ transition: 'background-color 0.15s ease',
54
+ },
55
+ chevron: {
56
+ fontSize: '18px',
57
+ transition: 'transform 0.2s ease',
58
+ },
59
+ answer: {
60
+ padding: '0 20px 16px 20px',
61
+ fontSize: '14px',
62
+ lineHeight: 1.6,
63
+ overflow: 'hidden',
64
+ transition: 'max-height 0.2s ease, padding 0.2s ease',
65
+ },
66
+ category: {
67
+ display: 'inline-block',
68
+ fontSize: '11px',
69
+ fontWeight: 600,
70
+ textTransform: 'uppercase',
71
+ letterSpacing: '0.05em',
72
+ padding: '4px 8px',
73
+ borderRadius: '4px',
74
+ marginBottom: '8px',
75
+ },
76
+ emptyState: {
77
+ textAlign: 'center',
78
+ padding: '48px 24px',
79
+ fontSize: '14px',
80
+ },
81
+ noResults: {
82
+ textAlign: 'center',
83
+ padding: '32px 16px',
84
+ fontSize: '14px',
85
+ },
86
+ };
87
+ const themeStyles = {
88
+ light: {
89
+ container: {
90
+ backgroundColor: '#ffffff',
91
+ color: '#111827',
92
+ },
93
+ searchInput: {
94
+ backgroundColor: '#f9fafb',
95
+ border: '1px solid #e5e7eb',
96
+ color: '#111827',
97
+ },
98
+ item: {
99
+ backgroundColor: '#f9fafb',
100
+ border: '1px solid #e5e7eb',
101
+ },
102
+ itemExpanded: {
103
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
104
+ },
105
+ question: {
106
+ backgroundColor: 'transparent',
107
+ color: '#111827',
108
+ },
109
+ questionHover: {
110
+ backgroundColor: '#f3f4f6',
111
+ },
112
+ answer: {
113
+ color: '#4b5563',
114
+ },
115
+ category: {
116
+ backgroundColor: '#e0e7ff',
117
+ color: '#4338ca',
118
+ },
119
+ emptyState: {
120
+ color: '#9ca3af',
121
+ },
122
+ },
123
+ dark: {
124
+ container: {
125
+ backgroundColor: '#111827',
126
+ color: '#f9fafb',
127
+ },
128
+ searchInput: {
129
+ backgroundColor: '#1f2937',
130
+ border: '1px solid #374151',
131
+ color: '#f9fafb',
132
+ },
133
+ item: {
134
+ backgroundColor: '#1f2937',
135
+ border: '1px solid #374151',
136
+ },
137
+ itemExpanded: {
138
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
139
+ },
140
+ question: {
141
+ backgroundColor: 'transparent',
142
+ color: '#f9fafb',
143
+ },
144
+ questionHover: {
145
+ backgroundColor: '#374151',
146
+ },
147
+ answer: {
148
+ color: '#9ca3af',
149
+ },
150
+ category: {
151
+ backgroundColor: '#312e81',
152
+ color: '#a5b4fc',
153
+ },
154
+ emptyState: {
155
+ color: '#6b7280',
156
+ },
157
+ },
158
+ };
159
+ function FAQItem({ item, isExpanded, onToggle, theme }) {
160
+ const [isHovered, setIsHovered] = useState(false);
161
+ const colors = themeStyles[theme];
162
+ const { question, answer, category } = item.config;
163
+ const itemStyle = {
164
+ ...baseStyles.item,
165
+ ...colors.item,
166
+ ...(isExpanded ? colors.itemExpanded : {}),
167
+ };
168
+ const questionStyle = {
169
+ ...baseStyles.question,
170
+ ...colors.question,
171
+ ...(isHovered ? colors.questionHover : {}),
172
+ };
173
+ const chevronStyle = {
174
+ ...baseStyles.chevron,
175
+ transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
176
+ };
177
+ const answerStyle = {
178
+ ...baseStyles.answer,
179
+ ...colors.answer,
180
+ maxHeight: isExpanded ? '500px' : '0',
181
+ paddingBottom: isExpanded ? '16px' : '0',
182
+ };
183
+ const categoryStyle = {
184
+ ...baseStyles.category,
185
+ ...colors.category,
186
+ };
187
+ return (_jsxs("div", { style: itemStyle, children: [_jsxs("button", { style: questionStyle, onClick: onToggle, onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), "aria-expanded": isExpanded, children: [_jsx("span", { children: question }), _jsx("span", { style: chevronStyle, children: "\u25BC" })] }), _jsxs("div", { style: answerStyle, "aria-hidden": !isExpanded, children: [category && _jsx("span", { style: categoryStyle, children: category }), _jsx("p", { style: { margin: 0 }, children: answer })] })] }));
188
+ }
189
+ // ============================================================================
190
+ // FAQWidget Component
191
+ // ============================================================================
192
+ /**
193
+ * FAQWidget - Renders a collapsible Q&A accordion with per-item activation.
194
+ *
195
+ * This component demonstrates the compositional action pattern:
196
+ * - Parent (FAQWidget) receives `config.actions` array
197
+ * - Each action has optional `showWhen` for per-item visibility
198
+ * - Parent evaluates showWhen and filters visible questions
199
+ * - Parent manages expand state and re-rendering on context changes
200
+ */
201
+ export function FAQWidget({ config, runtime, instanceId }) {
202
+ // Force re-render when context changes
203
+ const [, forceUpdate] = useReducer((x) => x + 1, 0);
204
+ // Track expanded question IDs
205
+ const [expandedIds, setExpandedIds] = useState(new Set());
206
+ // Search query state
207
+ const [searchQuery, setSearchQuery] = useState('');
208
+ // Subscribe to context changes for reactive updates
209
+ useEffect(() => {
210
+ const unsubscribe = runtime.context.subscribe(() => {
211
+ forceUpdate();
212
+ });
213
+ return unsubscribe;
214
+ }, [runtime.context]);
215
+ // Filter visible questions based on per-item showWhen
216
+ const visibleQuestions = useMemo(() => {
217
+ return config.actions.filter((q) => {
218
+ // No showWhen = always visible
219
+ if (!q.showWhen)
220
+ return true;
221
+ // Evaluate the decision strategy
222
+ const result = runtime.evaluateSync(q.showWhen);
223
+ return result.value;
224
+ });
225
+ }, [config.actions, runtime]);
226
+ // Apply search filter
227
+ const filteredQuestions = useMemo(() => {
228
+ if (!config.searchable || !searchQuery.trim()) {
229
+ return visibleQuestions;
230
+ }
231
+ const query = searchQuery.toLowerCase();
232
+ return visibleQuestions.filter((q) => q.config.question.toLowerCase().includes(query) ||
233
+ q.config.answer.toLowerCase().includes(query) ||
234
+ q.config.category?.toLowerCase().includes(query));
235
+ }, [visibleQuestions, searchQuery, config.searchable]);
236
+ // Resolve theme (auto → detect system preference)
237
+ const resolvedTheme = useMemo(() => {
238
+ if (config.theme !== 'auto')
239
+ return config.theme;
240
+ // Check system preference (SSR-safe)
241
+ if (typeof window !== 'undefined') {
242
+ return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
243
+ }
244
+ return 'light';
245
+ }, [config.theme]);
246
+ // Handle question toggle
247
+ const handleToggle = useCallback((id) => {
248
+ setExpandedIds((prev) => {
249
+ const next = new Set(prev);
250
+ if (config.expandBehavior === 'single') {
251
+ // Single mode: collapse all others
252
+ if (prev.has(id)) {
253
+ return new Set();
254
+ }
255
+ return new Set([id]);
256
+ }
257
+ else {
258
+ // Multiple mode: toggle this one
259
+ if (prev.has(id)) {
260
+ next.delete(id);
261
+ }
262
+ else {
263
+ next.add(id);
264
+ }
265
+ return next;
266
+ }
267
+ });
268
+ // Publish toggle event for analytics
269
+ runtime.events.publish('faq:toggled', {
270
+ instanceId,
271
+ questionId: id,
272
+ expanded: !expandedIds.has(id),
273
+ timestamp: Date.now(),
274
+ });
275
+ }, [config.expandBehavior, runtime.events, instanceId, expandedIds]);
276
+ // Compute styles
277
+ const containerStyle = {
278
+ ...baseStyles.container,
279
+ ...themeStyles[resolvedTheme].container,
280
+ };
281
+ const searchInputStyle = {
282
+ ...baseStyles.searchInput,
283
+ ...themeStyles[resolvedTheme].searchInput,
284
+ };
285
+ const emptyStateStyle = {
286
+ ...baseStyles.emptyState,
287
+ ...themeStyles[resolvedTheme].emptyState,
288
+ };
289
+ // Empty state (no visible questions at all)
290
+ if (visibleQuestions.length === 0) {
291
+ return (_jsx("div", { style: containerStyle, "data-adaptive-id": instanceId, "data-adaptive-type": "adaptive-faq", children: _jsx("div", { style: emptyStateStyle, children: "No FAQ questions available." }) }));
292
+ }
293
+ return (_jsxs("div", { style: containerStyle, "data-adaptive-id": instanceId, "data-adaptive-type": "adaptive-faq", children: [config.searchable && (_jsx("div", { style: baseStyles.searchWrapper, children: _jsx("input", { type: "text", placeholder: "Search questions...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), style: searchInputStyle }) })), _jsx("div", { style: baseStyles.accordion, children: filteredQuestions.map((q) => (_jsx(FAQItem, { item: q, isExpanded: expandedIds.has(q.config.id), onToggle: () => handleToggle(q.config.id), theme: resolvedTheme }, q.config.id))) }), config.searchable && filteredQuestions.length === 0 && searchQuery && (_jsxs("div", { style: { ...baseStyles.noResults, ...themeStyles[resolvedTheme].emptyState }, children: ["No questions found matching \"", searchQuery, "\""] }))] }));
294
+ }
295
+ // ============================================================================
296
+ // Mountable Widget Interface
297
+ // ============================================================================
298
+ /**
299
+ * Mountable widget interface for the runtime's WidgetRegistry.
300
+ */
301
+ export const FAQMountableWidget = {
302
+ mount(container, config) {
303
+ // This is a simplified mount for non-React environments
304
+ // In practice, the runtime handles React rendering
305
+ const { runtime, instanceId: _instanceId = 'faq-widget', ...faqConfig } = config || {
306
+ expandBehavior: 'single',
307
+ searchable: false,
308
+ theme: 'auto',
309
+ actions: [],
310
+ };
311
+ // Create simple HTML fallback if no runtime
312
+ if (!runtime) {
313
+ const questions = faqConfig.actions || [];
314
+ container.innerHTML = `
315
+ <div style="font-family: system-ui; max-width: 800px;">
316
+ ${questions
317
+ .map((q) => `
318
+ <div style="margin-bottom: 8px; padding: 16px; background: #f9fafb; border-radius: 8px;">
319
+ <strong>${q.config.question}</strong>
320
+ <p style="margin-top: 8px; color: #4b5563;">${q.config.answer}</p>
321
+ </div>
322
+ `)
323
+ .join('')}
324
+ </div>
325
+ `;
326
+ }
327
+ return () => {
328
+ container.innerHTML = '';
329
+ };
330
+ },
331
+ };
332
+ export default FAQWidget;
package/dist/cdn.d.ts ADDED
@@ -0,0 +1,38 @@
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
+ /**
8
+ * App manifest for registry registration.
9
+ * Follows the AppManifest interface expected by AppLoader/AppRegistry.
10
+ */
11
+ export declare const manifest: {
12
+ id: string;
13
+ version: string;
14
+ name: string;
15
+ description: string;
16
+ runtime: {
17
+ actions: never[];
18
+ widgets: {
19
+ id: string;
20
+ component: {
21
+ mount(container: HTMLElement, config?: import("./types").FAQConfig & {
22
+ runtime?: import("./types").FAQWidgetRuntime;
23
+ instanceId?: string;
24
+ }): () => void;
25
+ };
26
+ metadata: {
27
+ name: string;
28
+ description: string;
29
+ icon: string;
30
+ };
31
+ }[];
32
+ };
33
+ metadata: {
34
+ isBuiltIn: boolean;
35
+ };
36
+ };
37
+ export default manifest;
38
+ //# sourceMappingURL=cdn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdn.d.ts","sourceRoot":"","sources":["../src/cdn.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;GAGG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;2BA2BixT,CAAC;8BAA8B,CAAC;;;;;;;;;;;;;CAdr0T,CAAC;AAaF,eAAe,QAAQ,CAAC"}
package/dist/cdn.js ADDED
@@ -0,0 +1,36 @@
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 { runtime } from './runtime';
8
+ /**
9
+ * App manifest for registry registration.
10
+ * Follows the AppManifest interface expected by AppLoader/AppRegistry.
11
+ */
12
+ export const manifest = {
13
+ id: 'faq',
14
+ version: runtime.version,
15
+ name: runtime.name,
16
+ description: runtime.description,
17
+ runtime: {
18
+ // FAQ is widget-based, no action executors
19
+ actions: [],
20
+ widgets: runtime.widgets,
21
+ },
22
+ metadata: {
23
+ isBuiltIn: false,
24
+ },
25
+ };
26
+ /**
27
+ * Self-register with global registry if available.
28
+ * This happens when loaded via script tag (UMD).
29
+ */
30
+ if (typeof window !== 'undefined') {
31
+ const globalRegistry = window.__SYNOS_APP_REGISTRY__;
32
+ if (globalRegistry && typeof globalRegistry.register === 'function') {
33
+ globalRegistry.register(manifest);
34
+ }
35
+ }
36
+ export default manifest;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Adaptive FAQ - Editor Component
3
+ *
4
+ * Visual editor panel for configuring FAQ questions.
5
+ */
6
+ import type { EditorPanelProps } from './types';
7
+ export declare function FAQEditor({ config, onChange, editor }: EditorPanelProps): import("react/jsx-runtime").JSX.Element;
8
+ /**
9
+ * Editor panel configuration for the app registry.
10
+ */
11
+ export declare const editorPanel: {
12
+ title: string;
13
+ icon: string;
14
+ description: string;
15
+ };
16
+ export default FAQEditor;
17
+ //# sourceMappingURL=editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../src/editor.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAqMhD,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,gBAAgB,2CAgNvE;AAED;;GAEG;AACH,eAAO,MAAM,WAAW;;;;CAIvB,CAAC;AAEF,eAAe,SAAS,CAAC"}