@syntrologie/runtime-sdk 2.1.0-canary.8 → 2.1.0-canary.9
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/CAPABILITIES.md +273 -22
- package/dist/adaptives/faq/index.js +5 -5
- package/dist/adaptives/faq/index.js.map +4 -4
- package/dist/blocks/data/ComparisonBlock.js +9 -9
- package/dist/blocks/data/ComparisonBlock.js.map +1 -1
- package/dist/blocks/data/StatsBlock.js +10 -10
- package/dist/blocks/data/StatsBlock.js.map +1 -1
- package/dist/blocks/interactive/ChecklistBlock.js +12 -12
- package/dist/blocks/interactive/ChecklistBlock.js.map +1 -1
- package/dist/blocks/interactive/RatingBlock.js +12 -14
- package/dist/blocks/interactive/RatingBlock.js.map +1 -1
- package/dist/blocks/notification/NotificationBlock.js +23 -23
- package/dist/blocks/notification/NotificationBlock.js.map +1 -1
- package/dist/bootstrap.js +13 -21
- package/dist/bootstrap.js.map +1 -1
- package/dist/components/ShadowCanvasOverlay.js +23 -22
- package/dist/components/ShadowCanvasOverlay.js.map +1 -1
- package/dist/components/TileCard.js +23 -22
- package/dist/components/TileCard.js.map +1 -1
- package/dist/components/TileWheel.js +23 -1
- package/dist/components/TileWheel.js.map +1 -1
- package/dist/editorLoader.d.ts +19 -9
- package/dist/editorLoader.js +91 -94
- package/dist/editorLoader.js.map +1 -1
- package/dist/smart-canvas.esm.js +61 -49
- package/dist/smart-canvas.esm.js.map +4 -4
- package/dist/smart-canvas.js +21757 -24175
- package/dist/smart-canvas.js.map +4 -4
- package/dist/smart-canvas.min.js +61 -49
- package/dist/smart-canvas.min.js.map +4 -4
- package/dist/theme/defaultTheme.d.ts +2 -3
- package/dist/theme/defaultTheme.js +60 -51
- package/dist/theme/defaultTheme.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +17 -12
package/CAPABILITIES.md
CHANGED
|
@@ -291,7 +291,7 @@ Removes a CSS class from an element.
|
|
|
291
291
|
|
|
292
292
|
# @syntrologie/adapt-faq
|
|
293
293
|
|
|
294
|
-
|
|
294
|
+
Collapsible Q&A accordion with actions, rich content, feedback, and personalization. Supports mounting a full FAQ widget, scrolling to specific items, toggling item state, and dynamically updating the item list at runtime.
|
|
295
295
|
|
|
296
296
|
## Actions
|
|
297
297
|
|
|
@@ -299,22 +299,33 @@ FAQ accordion widget with conditional item visibility.
|
|
|
299
299
|
|
|
300
300
|
Mounts an FAQ accordion widget to a surface slot.
|
|
301
301
|
|
|
302
|
-
| Property
|
|
303
|
-
|
|
|
304
|
-
| `kind`
|
|
305
|
-
| `slot`
|
|
306
|
-
| `config.title`
|
|
307
|
-
| `config.
|
|
302
|
+
| Property | Type | Required | Description |
|
|
303
|
+
| ----------------------- | --------------------------------- | -------- | ------------------------------------------------------------------- |
|
|
304
|
+
| `kind` | `"mount_faq"` | Yes | Action type |
|
|
305
|
+
| `slot` | string | Yes | Target slot (e.g., `"drawer_right"`, `"overlay_center"`) |
|
|
306
|
+
| `config.title` | string | No | Widget title |
|
|
307
|
+
| `config.expandBehavior` | `"single"` \| `"multiple"` | No | Whether one or many items can be open at once (default: `"single"`) |
|
|
308
|
+
| `config.searchable` | boolean | No | Show a search/filter input (default: `false`) |
|
|
309
|
+
| `config.theme` | `"light"` \| `"dark"` \| `"auto"` | No | Color theme (default: `"auto"`) |
|
|
310
|
+
| `config.items` | array | Yes | FAQ items (see below) |
|
|
311
|
+
| `config.feedback` | boolean \| FeedbackConfig | No | Enable per-item feedback widget |
|
|
312
|
+
| `config.ordering` | OrderingStrategy | No | Item ordering strategy (default: `"static"`) |
|
|
313
|
+
| `config.injections` | InjectionRule[] | No | Dynamic item injection rules |
|
|
308
314
|
|
|
309
315
|
### FAQ Item Schema
|
|
310
316
|
|
|
311
317
|
Each item in the `items` array:
|
|
312
318
|
|
|
313
|
-
| Property
|
|
314
|
-
|
|
|
315
|
-
| `
|
|
316
|
-
| `
|
|
317
|
-
| `
|
|
319
|
+
| Property | Type | Required | Description |
|
|
320
|
+
| ----------------------- | ------------------------ | -------- | ----------------------------------------------- |
|
|
321
|
+
| `kind` | `"faq:question"` | Yes | Compositional action type |
|
|
322
|
+
| `config.id` | string | Yes | Unique identifier for this question |
|
|
323
|
+
| `config.question` | string | Yes | The question text |
|
|
324
|
+
| `config.answer` | FAQAnswer | Yes | Answer content (string, rich HTML, or markdown) |
|
|
325
|
+
| `config.category` | string | No | Category for grouping items |
|
|
326
|
+
| `config.priority` | number | No | Priority weight for ordering |
|
|
327
|
+
| `config.answerStrategy` | AnswerStrategy | No | AI-generated answer configuration |
|
|
328
|
+
| `showWhen` | DecisionStrategy \| null | No | Conditional visibility strategy |
|
|
318
329
|
|
|
319
330
|
```json
|
|
320
331
|
{
|
|
@@ -322,14 +333,34 @@ Each item in the `items` array:
|
|
|
322
333
|
"slot": "drawer_right",
|
|
323
334
|
"config": {
|
|
324
335
|
"title": "Frequently Asked Questions",
|
|
336
|
+
"expandBehavior": "single",
|
|
337
|
+
"searchable": true,
|
|
338
|
+
"theme": "auto",
|
|
339
|
+
"feedback": {
|
|
340
|
+
"style": "thumbs",
|
|
341
|
+
"prompt": "Was this helpful?"
|
|
342
|
+
},
|
|
343
|
+
"ordering": "priority",
|
|
325
344
|
"items": [
|
|
326
345
|
{
|
|
327
|
-
"
|
|
328
|
-
"
|
|
346
|
+
"kind": "faq:question",
|
|
347
|
+
"config": {
|
|
348
|
+
"id": "getting-started",
|
|
349
|
+
"question": "How do I get started?",
|
|
350
|
+
"answer": "Sign up for a free account and follow our quickstart guide.",
|
|
351
|
+
"category": "General",
|
|
352
|
+
"priority": 10
|
|
353
|
+
}
|
|
329
354
|
},
|
|
330
355
|
{
|
|
331
|
-
"
|
|
332
|
-
"
|
|
356
|
+
"kind": "faq:question",
|
|
357
|
+
"config": {
|
|
358
|
+
"id": "payment-methods",
|
|
359
|
+
"question": "What payment methods do you accept?",
|
|
360
|
+
"answer": "We accept all major credit cards and PayPal.",
|
|
361
|
+
"category": "Billing",
|
|
362
|
+
"priority": 5
|
|
363
|
+
},
|
|
333
364
|
"showWhen": {
|
|
334
365
|
"type": "rules",
|
|
335
366
|
"rules": [
|
|
@@ -346,16 +377,236 @@ Each item in the `items` array:
|
|
|
346
377
|
}
|
|
347
378
|
```
|
|
348
379
|
|
|
380
|
+
### scroll_to_faq
|
|
381
|
+
|
|
382
|
+
Scrolls the viewport to a specific FAQ item and optionally expands it.
|
|
383
|
+
|
|
384
|
+
| Property | Type | Required | Default | Description |
|
|
385
|
+
| -------------- | ----------------- | -------- | ---------- | ------------------------------------------ |
|
|
386
|
+
| `kind` | `"scroll_to_faq"` | Yes | | Action type |
|
|
387
|
+
| `itemId` | string | No\* | | Target item ID |
|
|
388
|
+
| `itemQuestion` | string | No\* | | Target item question text (fuzzy match) |
|
|
389
|
+
| `expand` | boolean | No | `true` | Whether to expand the item after scrolling |
|
|
390
|
+
| `behavior` | string | No | `"smooth"` | `"smooth"`, `"instant"`, `"auto"` |
|
|
391
|
+
|
|
392
|
+
\* Either `itemId` or `itemQuestion` is required.
|
|
393
|
+
|
|
394
|
+
```json
|
|
395
|
+
{
|
|
396
|
+
"kind": "scroll_to_faq",
|
|
397
|
+
"itemId": "payment-methods",
|
|
398
|
+
"expand": true,
|
|
399
|
+
"behavior": "smooth"
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### toggle_faq_item
|
|
404
|
+
|
|
405
|
+
Opens, closes, or toggles a FAQ item's expanded state.
|
|
406
|
+
|
|
407
|
+
| Property | Type | Required | Default | Description |
|
|
408
|
+
| -------------- | ------------------- | -------- | ---------- | --------------------------------------- |
|
|
409
|
+
| `kind` | `"toggle_faq_item"` | Yes | | Action type |
|
|
410
|
+
| `itemId` | string | No\* | | Target item ID |
|
|
411
|
+
| `itemQuestion` | string | No\* | | Target item question text (fuzzy match) |
|
|
412
|
+
| `state` | string | No | `"toggle"` | `"open"`, `"closed"`, `"toggle"` |
|
|
413
|
+
|
|
414
|
+
\* Either `itemId` or `itemQuestion` is required.
|
|
415
|
+
|
|
416
|
+
```json
|
|
417
|
+
{
|
|
418
|
+
"kind": "toggle_faq_item",
|
|
419
|
+
"itemId": "getting-started",
|
|
420
|
+
"state": "open"
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### update_faq
|
|
425
|
+
|
|
426
|
+
Dynamically adds, removes, reorders, or replaces FAQ items at runtime.
|
|
427
|
+
|
|
428
|
+
| Property | Type | Required | Description |
|
|
429
|
+
| ----------- | ------------------- | -------- | ------------------------------------------------------- |
|
|
430
|
+
| `kind` | `"update_faq"` | Yes | Action type |
|
|
431
|
+
| `operation` | string | Yes | `"add"`, `"remove"`, `"reorder"`, `"replace"` |
|
|
432
|
+
| `items` | FAQQuestionAction[] | No | Items to add or replace with (required for add/replace) |
|
|
433
|
+
| `itemId` | string | No | Item to remove (required for remove) |
|
|
434
|
+
| `order` | string[] | No | Ordered list of item IDs (required for reorder) |
|
|
435
|
+
| `position` | string | No | `"prepend"`, `"append"`, `"before"`, `"after"` |
|
|
436
|
+
| `anchorId` | string | No | Reference item for before/after positioning |
|
|
437
|
+
|
|
438
|
+
**Add items:**
|
|
439
|
+
|
|
440
|
+
```json
|
|
441
|
+
{
|
|
442
|
+
"kind": "update_faq",
|
|
443
|
+
"operation": "add",
|
|
444
|
+
"position": "append",
|
|
445
|
+
"items": [
|
|
446
|
+
{
|
|
447
|
+
"kind": "faq:question",
|
|
448
|
+
"config": {
|
|
449
|
+
"id": "new-feature",
|
|
450
|
+
"question": "What is the new feature?",
|
|
451
|
+
"answer": "Our latest release includes AI-powered recommendations."
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
]
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Remove an item:**
|
|
459
|
+
|
|
460
|
+
```json
|
|
461
|
+
{
|
|
462
|
+
"kind": "update_faq",
|
|
463
|
+
"operation": "remove",
|
|
464
|
+
"itemId": "outdated-question"
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**Reorder items:**
|
|
469
|
+
|
|
470
|
+
```json
|
|
471
|
+
{
|
|
472
|
+
"kind": "update_faq",
|
|
473
|
+
"operation": "reorder",
|
|
474
|
+
"order": ["getting-started", "new-feature", "payment-methods"]
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
**Replace all items:**
|
|
479
|
+
|
|
480
|
+
```json
|
|
481
|
+
{
|
|
482
|
+
"kind": "update_faq",
|
|
483
|
+
"operation": "replace",
|
|
484
|
+
"items": [
|
|
485
|
+
{
|
|
486
|
+
"kind": "faq:question",
|
|
487
|
+
"config": {
|
|
488
|
+
"id": "only-question",
|
|
489
|
+
"question": "Is this the only question?",
|
|
490
|
+
"answer": "Yes, after replacing all items."
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
349
497
|
## Compositional Pattern
|
|
350
498
|
|
|
351
|
-
The FAQ widget
|
|
499
|
+
The FAQ widget uses a **compositional action pattern** where `faq:question` actions serve as configuration data rendered by the widget, rather than being executed by the runtime. This allows:
|
|
352
500
|
|
|
353
|
-
-
|
|
354
|
-
-
|
|
355
|
-
-
|
|
356
|
-
-
|
|
501
|
+
- **Per-item conditional visibility** via `showWhen` strategies -- items can appear or hide based on page URL, user segment, viewport, or any DecisionStrategy condition
|
|
502
|
+
- **Category grouping** -- items with a `category` field are grouped under collapsible section headers
|
|
503
|
+
- **Dynamic injection** -- `injections` rules can add items when trigger conditions are met, supporting contextual FAQ content
|
|
504
|
+
- **Ordering control** -- the `ordering` strategy determines how items are sorted within categories
|
|
357
505
|
|
|
358
|
-
Items without `showWhen` are always visible.
|
|
506
|
+
Items without `showWhen` are always visible. Items without `category` appear in an ungrouped section.
|
|
507
|
+
|
|
508
|
+
## Rich Answer Content
|
|
509
|
+
|
|
510
|
+
FAQ answers support three content formats via the `FAQAnswer` union type:
|
|
511
|
+
|
|
512
|
+
- **Plain string** -- simple text, supports basic markdown
|
|
513
|
+
- **Rich HTML** (`{ "type": "rich", "html": "<p>...</p>" }`) -- pre-rendered HTML content
|
|
514
|
+
- **Enhanced markdown** (`{ "type": "markdown", "content": "...", "assets": [...] }`) -- markdown with embedded media assets (images, videos)
|
|
515
|
+
|
|
516
|
+
```json
|
|
517
|
+
{
|
|
518
|
+
"config": {
|
|
519
|
+
"id": "rich-example",
|
|
520
|
+
"question": "How does the visual editor work?",
|
|
521
|
+
"answer": {
|
|
522
|
+
"type": "markdown",
|
|
523
|
+
"content": "The visual editor lets you create experiments with a point-and-click interface.\n\n",
|
|
524
|
+
"assets": [
|
|
525
|
+
{
|
|
526
|
+
"id": "editor-screenshot",
|
|
527
|
+
"type": "image",
|
|
528
|
+
"src": "https://cdn.example.com/editor.png",
|
|
529
|
+
"alt": "Visual editor interface",
|
|
530
|
+
"width": 800,
|
|
531
|
+
"height": 450
|
|
532
|
+
}
|
|
533
|
+
]
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
## Feedback
|
|
540
|
+
|
|
541
|
+
Per-item feedback allows users to rate answer helpfulness. Enable with a boolean or detailed config:
|
|
542
|
+
|
|
543
|
+
- `"feedback": true` -- enables thumbs up/down with default prompt
|
|
544
|
+
- `"feedback": { "style": "thumbs", "prompt": "Was this helpful?" }` -- thumbs with custom prompt
|
|
545
|
+
- `"feedback": { "style": "rating" }` -- numeric rating scale
|
|
546
|
+
|
|
547
|
+
Feedback events are published via `context.publishEvent` for analytics integration.
|
|
548
|
+
|
|
549
|
+
## Personalization
|
|
550
|
+
|
|
551
|
+
### Ordering Strategies
|
|
552
|
+
|
|
553
|
+
The `ordering` field controls how FAQ items are sorted:
|
|
554
|
+
|
|
555
|
+
- **`"static"`** (default) -- items appear in the order defined in the config
|
|
556
|
+
- **`"priority"`** -- items are sorted by their `priority` field (higher values first)
|
|
557
|
+
- **Segment-based** -- items are ordered differently per user segment:
|
|
558
|
+
|
|
559
|
+
```json
|
|
560
|
+
{
|
|
561
|
+
"ordering": {
|
|
562
|
+
"type": "segment",
|
|
563
|
+
"segmentWeights": {
|
|
564
|
+
"new_user": ["getting-started", "pricing", "support"],
|
|
565
|
+
"power_user": ["api-docs", "advanced-config", "integrations"]
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Dynamic Injection
|
|
572
|
+
|
|
573
|
+
Injection rules add contextual FAQ items when conditions are met:
|
|
574
|
+
|
|
575
|
+
```json
|
|
576
|
+
{
|
|
577
|
+
"injections": [
|
|
578
|
+
{
|
|
579
|
+
"trigger": {
|
|
580
|
+
"type": "rules",
|
|
581
|
+
"rules": [
|
|
582
|
+
{
|
|
583
|
+
"conditions": [{ "type": "page_url", "pattern": "/checkout*" }],
|
|
584
|
+
"value": true
|
|
585
|
+
}
|
|
586
|
+
],
|
|
587
|
+
"default": false
|
|
588
|
+
},
|
|
589
|
+
"items": [
|
|
590
|
+
{
|
|
591
|
+
"kind": "faq:question",
|
|
592
|
+
"config": {
|
|
593
|
+
"id": "checkout-help",
|
|
594
|
+
"question": "Having trouble with checkout?",
|
|
595
|
+
"answer": "Contact support at help@example.com for immediate assistance."
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
],
|
|
599
|
+
"position": "prepend",
|
|
600
|
+
"once": true
|
|
601
|
+
}
|
|
602
|
+
]
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
- `trigger` -- a DecisionStrategy that evaluates to `true` when injection should occur
|
|
607
|
+
- `items` -- FAQ items to inject
|
|
608
|
+
- `position` -- `"prepend"` or `"append"` relative to existing items
|
|
609
|
+
- `once` -- if `true`, items are injected only on the first trigger match
|
|
359
610
|
|
|
360
611
|
|
|
361
612
|
---
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import{useEffect as
|
|
1
|
+
import h,{useEffect as y,useReducer as S,useMemo as k,useCallback as v,useState as F}from"react";import{jsx as A,jsxs as C}from"react/jsx-runtime";function f(e){return typeof e=="string"?e:e.type==="rich"?e.html:e.content}var c={mount(e,r){let{runtime:t,instanceId:n="faq-widget",...o}=r||{expandBehavior:"single",searchable:!1,theme:"auto",actions:[]};if(!t){let i=o.actions||[];e.innerHTML=`
|
|
2
2
|
<div style="font-family: system-ui; max-width: 800px;">
|
|
3
|
-
${
|
|
3
|
+
${i.map(a=>`
|
|
4
4
|
<div style="margin-bottom: 8px; padding: 16px; background: #f9fafb; border-radius: 8px;">
|
|
5
|
-
<strong>${
|
|
6
|
-
<p style="margin-top: 8px; color: #4b5563;">${
|
|
5
|
+
<strong>${a.config.question}</strong>
|
|
6
|
+
<p style="margin-top: 8px; color: #4b5563;">${f(a.config.answer)}</p>
|
|
7
7
|
</div>
|
|
8
8
|
`).join("")}
|
|
9
9
|
</div>
|
|
10
|
-
`}return()=>{e.innerHTML=""}}};
|
|
10
|
+
`}return()=>{e.innerHTML=""}}};function d(e,r,t){if(r){let n=e.getState().items.find(o=>o.config.id===r);if(n)return n}if(t){let n=e.findByQuestion(t);if(n)return n}throw new Error("FAQ item not found")}async function g(e,r,t){let o=d(t,e.itemId,e.itemQuestion).config.id;e.expand!==!1&&t.expand(o);let i=document.querySelector(`[data-faq-item-id="${o}"]`);return i&&i.scrollIntoView({behavior:e.behavior??"smooth"}),r.publishEvent("faq:scroll_to",{itemId:o}),{cleanup:()=>{}}}async function m(e,r,t){let o=d(t,e.itemId,e.itemQuestion).config.id,i=e.state??"toggle",a;switch(i){case"open":t.expand(o),a="open";break;case"closed":t.collapse(o),a="closed";break;default:{let u=t.getState().expandedItems.has(o);t.toggle(o),a=u?"closed":"open";break}}return r.publishEvent("faq:toggle",{itemId:o,newState:a}),{cleanup:()=>{}}}async function b(e,r,t){switch(e.operation){case"add":{let n=e.items??[],o=e.position==="prepend"?"prepend":"append";t.addItems(n,o);break}case"remove":{if(!e.itemId)throw new Error("FAQ item not found");if(!t.getState().items.some(o=>o.config.id===e.itemId))throw new Error("FAQ item not found");t.removeItem(e.itemId);break}case"reorder":{let n=e.order??[];t.reorderItems(n);break}case"replace":{let n=e.items??[];t.replaceItems(n);break}}return r.publishEvent("faq:update",{operation:e.operation}),{cleanup:()=>{}}}var p=[{kind:"faq:scroll_to",executor:g},{kind:"faq:toggle_item",executor:m},{kind:"faq:update",executor:b}];var s={id:"adaptive-faq",version:"2.0.0",name:"FAQ Accordion",description:"Collapsible Q&A accordion with actions, rich content, feedback, and personalization",executors:p,widgets:[{id:"adaptive-faq:accordion",component:c,metadata:{name:"FAQ Accordion",description:"Collapsible Q&A accordion with search, categories, and feedback",icon:"\u2753"}}]};var l={id:"faq",version:s.version,name:s.name,description:s.description,runtime:{actions:s.executors,widgets:s.widgets},metadata:{isBuiltIn:!1}};if(typeof window<"u"){let e=window.__SYNOS_APP_REGISTRY__;e&&typeof e.register=="function"&&e.register(l)}var E=l;export{E as default,l as manifest};
|
|
11
11
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../../../adaptives/adaptive-faq/src/FAQWidget.tsx", "../../../../adaptives/adaptive-faq/src/runtime.ts", "../../../../adaptives/adaptive-faq/src/cdn.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Adaptive FAQ - FAQWidget Component\n *\n * React component that renders a collapsible Q&A accordion with per-item\n * conditional visibility based on showWhen decision strategies.\n *\n * Demonstrates the compositional action pattern where child actions\n * (faq:question) serve as configuration data for the parent widget.\n */\n\nimport React, { useEffect, useReducer, useMemo, useCallback, useState } from 'react';\n\nimport type { FAQWidgetProps, FAQQuestionAction, FAQConfig, FAQWidgetRuntime } from './types';\n\n// ============================================================================\n// Styles\n// ============================================================================\n\nconst baseStyles = {\n container: {\n fontFamily: 'system-ui, -apple-system, sans-serif',\n maxWidth: '800px',\n margin: '0 auto',\n },\n searchWrapper: {\n marginBottom: '16px',\n },\n searchInput: {\n width: '100%',\n padding: '12px 16px',\n borderRadius: '8px',\n fontSize: '14px',\n outline: 'none',\n transition: 'border-color 0.15s ease',\n },\n accordion: {\n display: 'flex',\n flexDirection: 'column' as const,\n gap: '8px',\n },\n item: {\n borderRadius: '8px',\n overflow: 'hidden',\n transition: 'box-shadow 0.15s ease',\n },\n question: {\n width: '100%',\n padding: '16px 20px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n border: 'none',\n cursor: 'pointer',\n fontSize: '15px',\n fontWeight: 500,\n textAlign: 'left' as const,\n transition: 'background-color 0.15s ease',\n },\n chevron: {\n fontSize: '18px',\n transition: 'transform 0.2s ease',\n },\n answer: {\n padding: '0 20px 16px 20px',\n fontSize: '14px',\n lineHeight: 1.6,\n overflow: 'hidden',\n transition: 'max-height 0.2s ease, padding 0.2s ease',\n },\n category: {\n display: 'inline-block',\n fontSize: '11px',\n fontWeight: 600,\n textTransform: 'uppercase' as const,\n letterSpacing: '0.05em',\n padding: '4px 8px',\n borderRadius: '4px',\n marginBottom: '8px',\n },\n emptyState: {\n textAlign: 'center' as const,\n padding: '48px 24px',\n fontSize: '14px',\n },\n noResults: {\n textAlign: 'center' as const,\n padding: '32px 16px',\n fontSize: '14px',\n },\n} as const;\n\nconst themeStyles = {\n light: {\n container: {\n backgroundColor: '#ffffff',\n color: '#111827',\n },\n searchInput: {\n backgroundColor: '#f9fafb',\n border: '1px solid #e5e7eb',\n color: '#111827',\n },\n item: {\n backgroundColor: '#f9fafb',\n border: '1px solid #e5e7eb',\n },\n itemExpanded: {\n boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',\n },\n question: {\n backgroundColor: 'transparent',\n color: '#111827',\n },\n questionHover: {\n backgroundColor: '#f3f4f6',\n },\n answer: {\n color: '#4b5563',\n },\n category: {\n backgroundColor: '#e0e7ff',\n color: '#4338ca',\n },\n emptyState: {\n color: '#9ca3af',\n },\n },\n dark: {\n container: {\n backgroundColor: '#111827',\n color: '#f9fafb',\n },\n searchInput: {\n backgroundColor: '#1f2937',\n border: '1px solid #374151',\n color: '#f9fafb',\n },\n item: {\n backgroundColor: '#1f2937',\n border: '1px solid #374151',\n },\n itemExpanded: {\n boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',\n },\n question: {\n backgroundColor: 'transparent',\n color: '#f9fafb',\n },\n questionHover: {\n backgroundColor: '#374151',\n },\n answer: {\n color: '#9ca3af',\n },\n category: {\n backgroundColor: '#312e81',\n color: '#a5b4fc',\n },\n emptyState: {\n color: '#6b7280',\n },\n },\n} as const;\n\n// ============================================================================\n// FAQItem Component\n// ============================================================================\n\ninterface FAQItemProps {\n item: FAQQuestionAction;\n isExpanded: boolean;\n onToggle: () => void;\n theme: 'light' | 'dark';\n}\n\nfunction FAQItem({ item, isExpanded, onToggle, theme }: FAQItemProps) {\n const [isHovered, setIsHovered] = useState(false);\n const colors = themeStyles[theme];\n const { question, answer, category } = item.config;\n\n const itemStyle: React.CSSProperties = {\n ...baseStyles.item,\n ...colors.item,\n ...(isExpanded ? colors.itemExpanded : {}),\n };\n\n const questionStyle: React.CSSProperties = {\n ...baseStyles.question,\n ...colors.question,\n ...(isHovered ? colors.questionHover : {}),\n };\n\n const chevronStyle: React.CSSProperties = {\n ...baseStyles.chevron,\n transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',\n };\n\n const answerStyle: React.CSSProperties = {\n ...baseStyles.answer,\n ...colors.answer,\n maxHeight: isExpanded ? '500px' : '0',\n paddingBottom: isExpanded ? '16px' : '0',\n };\n\n const categoryStyle: React.CSSProperties = {\n ...baseStyles.category,\n ...colors.category,\n };\n\n return (\n <div style={itemStyle}>\n <button\n style={questionStyle}\n onClick={onToggle}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n aria-expanded={isExpanded}\n >\n <span>{question}</span>\n <span style={chevronStyle}>\u25BC</span>\n </button>\n <div style={answerStyle} aria-hidden={!isExpanded}>\n {category && <span style={categoryStyle}>{category}</span>}\n <p style={{ margin: 0 }}>{answer}</p>\n </div>\n </div>\n );\n}\n\n// ============================================================================\n// FAQWidget Component\n// ============================================================================\n\n/**\n * FAQWidget - Renders a collapsible Q&A accordion with per-item activation.\n *\n * This component demonstrates the compositional action pattern:\n * - Parent (FAQWidget) receives `config.actions` array\n * - Each action has optional `showWhen` for per-item visibility\n * - Parent evaluates showWhen and filters visible questions\n * - Parent manages expand state and re-rendering on context changes\n */\nexport function FAQWidget({ config, runtime, instanceId }: FAQWidgetProps) {\n // Force re-render when context changes\n const [, forceUpdate] = useReducer((x) => x + 1, 0);\n\n // Track expanded question IDs\n const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());\n\n // Search query state\n const [searchQuery, setSearchQuery] = useState('');\n\n // Subscribe to context changes for reactive updates\n useEffect(() => {\n const unsubscribe = runtime.context.subscribe(() => {\n forceUpdate();\n });\n return unsubscribe;\n }, [runtime.context]);\n\n // Filter visible questions based on per-item showWhen\n const visibleQuestions = useMemo(\n () =>\n config.actions.filter((q) => {\n // No showWhen = always visible\n if (!q.showWhen) return true;\n\n // Evaluate the decision strategy\n const result = runtime.evaluateSync<boolean>(q.showWhen);\n return result.value;\n }),\n [config.actions, runtime]\n );\n\n // Apply search filter\n const filteredQuestions = useMemo(() => {\n if (!config.searchable || !searchQuery.trim()) {\n return visibleQuestions;\n }\n\n const query = searchQuery.toLowerCase();\n return visibleQuestions.filter(\n (q) =>\n q.config.question.toLowerCase().includes(query) ||\n q.config.answer.toLowerCase().includes(query) ||\n q.config.category?.toLowerCase().includes(query)\n );\n }, [visibleQuestions, searchQuery, config.searchable]);\n\n // Resolve theme (auto \u2192 detect system preference)\n const resolvedTheme = useMemo(() => {\n if (config.theme !== 'auto') return config.theme;\n\n // Check system preference (SSR-safe)\n if (typeof window !== 'undefined') {\n return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n }\n return 'light';\n }, [config.theme]);\n\n // Handle question toggle\n const handleToggle = useCallback(\n (id: string) => {\n setExpandedIds((prev) => {\n const next = new Set(prev);\n\n if (config.expandBehavior === 'single') {\n // Single mode: collapse all others\n if (prev.has(id)) {\n return new Set();\n }\n return new Set([id]);\n }\n // Multiple mode: toggle this one\n if (prev.has(id)) {\n next.delete(id);\n } else {\n next.add(id);\n }\n return next;\n });\n\n // Publish toggle event for analytics\n runtime.events.publish('faq:toggled', {\n instanceId,\n questionId: id,\n expanded: !expandedIds.has(id),\n timestamp: Date.now(),\n });\n },\n [config.expandBehavior, runtime.events, instanceId, expandedIds]\n );\n\n // Compute styles\n const containerStyle: React.CSSProperties = {\n ...baseStyles.container,\n ...themeStyles[resolvedTheme].container,\n };\n\n const searchInputStyle: React.CSSProperties = {\n ...baseStyles.searchInput,\n ...themeStyles[resolvedTheme].searchInput,\n };\n\n const emptyStateStyle: React.CSSProperties = {\n ...baseStyles.emptyState,\n ...themeStyles[resolvedTheme].emptyState,\n };\n\n // Empty state (no visible questions at all)\n if (visibleQuestions.length === 0) {\n return (\n <div style={containerStyle} data-adaptive-id={instanceId} data-adaptive-type=\"adaptive-faq\">\n <div style={emptyStateStyle}>No FAQ questions available.</div>\n </div>\n );\n }\n\n return (\n <div style={containerStyle} data-adaptive-id={instanceId} data-adaptive-type=\"adaptive-faq\">\n {/* Search input */}\n {config.searchable && (\n <div style={baseStyles.searchWrapper}>\n <input\n type=\"text\"\n placeholder=\"Search questions...\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n style={searchInputStyle}\n />\n </div>\n )}\n\n {/* Accordion */}\n <div style={baseStyles.accordion}>\n {filteredQuestions.map((q) => (\n <FAQItem\n key={q.config.id}\n item={q}\n isExpanded={expandedIds.has(q.config.id)}\n onToggle={() => handleToggle(q.config.id)}\n theme={resolvedTheme}\n />\n ))}\n </div>\n\n {/* No search results */}\n {config.searchable && filteredQuestions.length === 0 && searchQuery && (\n <div style={{ ...baseStyles.noResults, ...themeStyles[resolvedTheme].emptyState }}>\n No questions found matching \"{searchQuery}\"\n </div>\n )}\n </div>\n );\n}\n\n// ============================================================================\n// Mountable Widget Interface\n// ============================================================================\n\n/**\n * Mountable widget interface for the runtime's WidgetRegistry.\n */\nexport const FAQMountableWidget = {\n mount(\n container: HTMLElement,\n config?: FAQConfig & { runtime?: FAQWidgetRuntime; instanceId?: string }\n ) {\n // This is a simplified mount for non-React environments\n // In practice, the runtime handles React rendering\n\n const {\n runtime,\n instanceId: _instanceId = 'faq-widget',\n ...faqConfig\n } = config || {\n expandBehavior: 'single' as const,\n searchable: false,\n theme: 'auto' as const,\n actions: [],\n };\n\n // Create simple HTML fallback if no runtime\n if (!runtime) {\n const questions = faqConfig.actions || [];\n container.innerHTML = `\n <div style=\"font-family: system-ui; max-width: 800px;\">\n ${questions\n .map(\n (q) => `\n <div style=\"margin-bottom: 8px; padding: 16px; background: #f9fafb; border-radius: 8px;\">\n <strong>${q.config.question}</strong>\n <p style=\"margin-top: 8px; color: #4b5563;\">${q.config.answer}</p>\n </div>\n `\n )\n .join('')}\n </div>\n `;\n }\n\n return () => {\n container.innerHTML = '';\n };\n },\n};\n\nexport default FAQWidget;\n", "/**\n * Adaptive FAQ - Runtime Module\n *\n * Runtime manifest for the FAQ accordion adaptive.\n * This is a widget-based adaptive with no action executors.\n */\n\nimport { FAQMountableWidget } from './FAQWidget';\n\n// ============================================================================\n// App Runtime Manifest\n// ============================================================================\n\n/**\n * Runtime manifest for adaptive-faq.\n *\n * Note: This adaptive is widget-based, not action-based.\n * The `faq:question` actions are compositional - they're rendered by\n * the widget, not executed by the runtime.\n */\nexport const runtime = {\n id: 'adaptive-faq',\n version: '1.0.0',\n name: 'FAQ Accordion',\n description: 'Collapsible Q&A accordion with per-item conditional visibility',\n\n /**\n * No action executors - faq:question actions are compositional,\n * meaning they serve as configuration for the FAQWidget.\n */\n executors: [],\n\n /**\n * Widget definitions for the runtime's WidgetRegistry.\n */\n widgets: [\n {\n id: 'adaptive-faq:accordion',\n component: FAQMountableWidget,\n metadata: {\n name: 'FAQ Accordion',\n description: 'Collapsible Q&A accordion with search',\n icon: '\u2753',\n },\n },\n ],\n};\n\nexport default runtime;\n", "/**\n * CDN Entry Point for Adaptive FAQ\n *\n * This module is bundled for CDN delivery and self-registers with the global\n * SynOS app registry when loaded dynamically via the AppLoader.\n */\n\nimport { runtime } from './runtime';\n\n/**\n * App manifest for registry registration.\n * Follows the AppManifest interface expected by AppLoader/AppRegistry.\n */\nexport const manifest = {\n id: 'faq',\n version: runtime.version,\n name: runtime.name,\n description: runtime.description,\n runtime: {\n // FAQ is widget-based, no action executors\n actions: [],\n widgets: runtime.widgets,\n },\n metadata: {\n isBuiltIn: false,\n },\n};\n\n/**\n * Self-register with global registry if available.\n * This happens when loaded via script tag (UMD).\n */\nif (typeof window !== 'undefined') {\n const globalRegistry = (window as any).__SYNOS_APP_REGISTRY__;\n if (globalRegistry && typeof globalRegistry.register === 'function') {\n globalRegistry.register(manifest);\n }\n}\n\nexport default manifest;\n"],
|
|
5
|
-
"mappings": "AAUA,
|
|
6
|
-
"names": ["useEffect", "useReducer", "useMemo", "useCallback", "useState", "jsx", "jsxs", "FAQMountableWidget", "container", "config", "runtime", "_instanceId", "faqConfig", "questions", "q", "runtime", "FAQMountableWidget", "manifest", "runtime", "globalRegistry", "cdn_default"]
|
|
3
|
+
"sources": ["../../../../adaptives/adaptive-faq/src/FAQWidget.tsx", "../../../../adaptives/adaptive-faq/src/executors.ts", "../../../../adaptives/adaptive-faq/src/runtime.ts", "../../../../adaptives/adaptive-faq/src/cdn.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Adaptive FAQ - FAQWidget Component\n *\n * React component that renders a collapsible Q&A accordion with per-item\n * conditional visibility based on showWhen decision strategies.\n *\n * Demonstrates the compositional action pattern where child actions\n * (faq:question) serve as configuration data for the parent widget.\n */\n\nimport React, { useEffect, useReducer, useMemo, useCallback, useState } from 'react';\n\nimport type {\n FAQQuestionAction,\n FAQConfig,\n FAQAnswer,\n FeedbackConfig,\n FeedbackValue,\n DecisionStrategy,\n} from './types';\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/** Extract plain text from an FAQAnswer for search matching */\nfunction getAnswerText(answer: FAQAnswer): string {\n if (typeof answer === 'string') return answer;\n if (answer.type === 'rich') return answer.html;\n return answer.content;\n}\n\n/** Render an FAQAnswer based on its type */\nfunction renderAnswer(answer: FAQAnswer): React.ReactNode {\n if (typeof answer === 'string') {\n return <p style={{ margin: 0 }}>{answer}</p>;\n }\n if (answer.type === 'rich') {\n return <div style={{ margin: 0 }} dangerouslySetInnerHTML={{ __html: answer.html }} />;\n }\n // markdown \u2014 render content as text (full markdown rendering is a future enhancement)\n return <p style={{ margin: 0 }}>{answer.content}</p>;\n}\n\n/** Resolve feedback config into a normalized FeedbackConfig or null */\nfunction resolveFeedbackConfig(\n feedback: boolean | FeedbackConfig | undefined\n): FeedbackConfig | null {\n if (!feedback) return null;\n if (feedback === true) {\n return { style: 'thumbs' };\n }\n return feedback;\n}\n\n/** Get the feedback prompt text */\nfunction getFeedbackPrompt(feedbackConfig: FeedbackConfig): string {\n return feedbackConfig.prompt || 'Was this helpful?';\n}\n\n// Widget runtime and props types defined inline (not part of the types.ts spec)\nexport interface FAQWidgetRuntime {\n evaluateSync: <T>(strategy: DecisionStrategy<T>) => { value: T; isFallback: boolean };\n context: { subscribe: (callback: () => void) => () => void };\n events: { publish: (name: string, props?: Record<string, unknown>) => void };\n state?: { get: (key: string) => unknown; set: (key: string, value: unknown) => void };\n}\n\ninterface FAQWidgetProps {\n config: FAQConfig;\n runtime: FAQWidgetRuntime;\n instanceId: string;\n}\n\n// ============================================================================\n// Styles\n// ============================================================================\n\nconst baseStyles = {\n container: {\n fontFamily: 'system-ui, -apple-system, sans-serif',\n maxWidth: '800px',\n margin: '0 auto',\n },\n searchWrapper: {\n marginBottom: '16px',\n },\n searchInput: {\n width: '100%',\n padding: '12px 16px',\n borderRadius: '8px',\n fontSize: '14px',\n outline: 'none',\n transition: 'border-color 0.15s ease',\n },\n accordion: {\n display: 'flex',\n flexDirection: 'column' as const,\n gap: '8px',\n },\n item: {\n borderRadius: '8px',\n overflow: 'hidden',\n transition: 'box-shadow 0.15s ease',\n },\n question: {\n width: '100%',\n padding: '16px 20px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n border: 'none',\n cursor: 'pointer',\n fontSize: '15px',\n fontWeight: 500,\n textAlign: 'left' as const,\n transition: 'background-color 0.15s ease',\n },\n chevron: {\n fontSize: '18px',\n transition: 'transform 0.2s ease',\n },\n answer: {\n padding: '0 20px 16px 20px',\n fontSize: '14px',\n lineHeight: 1.6,\n overflow: 'hidden',\n transition: 'max-height 0.2s ease, padding 0.2s ease',\n },\n category: {\n display: 'inline-block',\n fontSize: '11px',\n fontWeight: 600,\n textTransform: 'uppercase' as const,\n letterSpacing: '0.05em',\n padding: '4px 8px',\n borderRadius: '4px',\n marginBottom: '8px',\n },\n categoryHeader: {\n fontSize: '13px',\n fontWeight: 700,\n textTransform: 'uppercase' as const,\n letterSpacing: '0.05em',\n padding: '12px 4px 6px 4px',\n marginTop: '8px',\n },\n feedback: {\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n marginTop: '12px',\n paddingTop: '10px',\n borderTop: '1px solid rgba(0, 0, 0, 0.08)',\n fontSize: '13px',\n },\n feedbackButton: {\n background: 'none',\n border: '1px solid transparent',\n cursor: 'pointer',\n fontSize: '16px',\n padding: '4px 8px',\n borderRadius: '4px',\n transition: 'background-color 0.15s ease, border-color 0.15s ease',\n },\n feedbackButtonSelected: {\n borderColor: 'rgba(0, 0, 0, 0.2)',\n backgroundColor: 'rgba(0, 0, 0, 0.04)',\n },\n emptyState: {\n textAlign: 'center' as const,\n padding: '48px 24px',\n fontSize: '14px',\n },\n noResults: {\n textAlign: 'center' as const,\n padding: '32px 16px',\n fontSize: '14px',\n },\n} as const;\n\nconst themeStyles = {\n light: {\n container: {\n backgroundColor: '#ffffff',\n color: '#111827',\n },\n searchInput: {\n backgroundColor: '#f9fafb',\n border: '1px solid #e5e7eb',\n color: '#111827',\n },\n item: {\n backgroundColor: '#f9fafb',\n border: '1px solid #e5e7eb',\n },\n itemExpanded: {\n boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',\n },\n question: {\n backgroundColor: 'transparent',\n color: '#111827',\n },\n questionHover: {\n backgroundColor: '#f3f4f6',\n },\n answer: {\n color: '#4b5563',\n },\n category: {\n backgroundColor: '#e0e7ff',\n color: '#4338ca',\n },\n categoryHeader: {\n color: '#6b7280',\n },\n emptyState: {\n color: '#9ca3af',\n },\n feedbackPrompt: {\n color: '#6b7280',\n },\n },\n dark: {\n container: {\n backgroundColor: '#111827',\n color: '#f9fafb',\n },\n searchInput: {\n backgroundColor: '#1f2937',\n border: '1px solid #374151',\n color: '#f9fafb',\n },\n item: {\n backgroundColor: '#1f2937',\n border: '1px solid #374151',\n },\n itemExpanded: {\n boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',\n },\n question: {\n backgroundColor: 'transparent',\n color: '#f9fafb',\n },\n questionHover: {\n backgroundColor: '#374151',\n },\n answer: {\n color: '#9ca3af',\n },\n category: {\n backgroundColor: '#312e81',\n color: '#a5b4fc',\n },\n categoryHeader: {\n color: '#9ca3af',\n },\n emptyState: {\n color: '#6b7280',\n },\n feedbackPrompt: {\n color: '#9ca3af',\n },\n },\n} as const;\n\n// ============================================================================\n// FAQItem Component\n// ============================================================================\n\ninterface FAQItemProps {\n item: FAQQuestionAction;\n isExpanded: boolean;\n onToggle: () => void;\n theme: 'light' | 'dark';\n feedbackConfig: FeedbackConfig | null;\n feedbackValue: FeedbackValue | undefined;\n onFeedback: (itemId: string, question: string, value: FeedbackValue) => void;\n}\n\nfunction FAQItem({\n item,\n isExpanded,\n onToggle,\n theme,\n feedbackConfig,\n feedbackValue,\n onFeedback,\n}: FAQItemProps) {\n const [isHovered, setIsHovered] = useState(false);\n const colors = themeStyles[theme];\n const { question, answer } = item.config;\n\n const itemStyle: React.CSSProperties = {\n ...baseStyles.item,\n ...colors.item,\n ...(isExpanded ? colors.itemExpanded : {}),\n };\n\n const questionStyle: React.CSSProperties = {\n ...baseStyles.question,\n ...colors.question,\n ...(isHovered ? colors.questionHover : {}),\n };\n\n const chevronStyle: React.CSSProperties = {\n ...baseStyles.chevron,\n transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',\n };\n\n const answerStyle: React.CSSProperties = {\n ...baseStyles.answer,\n ...colors.answer,\n maxHeight: isExpanded ? '500px' : '0',\n paddingBottom: isExpanded ? '16px' : '0',\n };\n\n const feedbackStyle: React.CSSProperties = {\n ...baseStyles.feedback,\n ...colors.feedbackPrompt,\n };\n\n return (\n <div style={itemStyle} data-faq-item-id={item.config.id}>\n <button\n style={questionStyle}\n onClick={onToggle}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n aria-expanded={isExpanded}\n >\n <span>{question}</span>\n <span style={chevronStyle}>{'\\u25BC'}</span>\n </button>\n <div style={answerStyle} aria-hidden={!isExpanded}>\n {renderAnswer(answer)}\n\n {/* Feedback UI \u2014 only when expanded and feedback is enabled */}\n {isExpanded && feedbackConfig && (\n <div style={feedbackStyle}>\n <span>{getFeedbackPrompt(feedbackConfig)}</span>\n <button\n style={{\n ...baseStyles.feedbackButton,\n ...(feedbackValue === 'up' ? baseStyles.feedbackButtonSelected : {}),\n }}\n aria-label=\"Thumbs up\"\n onClick={() => onFeedback(item.config.id, question, 'up')}\n >\n {'\\uD83D\\uDC4D'}\n </button>\n <button\n style={{\n ...baseStyles.feedbackButton,\n ...(feedbackValue === 'down' ? baseStyles.feedbackButtonSelected : {}),\n }}\n aria-label=\"Thumbs down\"\n onClick={() => onFeedback(item.config.id, question, 'down')}\n >\n {'\\uD83D\\uDC4E'}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n}\n\n// ============================================================================\n// FAQWidget Component\n// ============================================================================\n\n/**\n * FAQWidget - Renders a collapsible Q&A accordion with per-item activation.\n *\n * This component demonstrates the compositional action pattern:\n * - Parent (FAQWidget) receives `config.actions` array\n * - Each action has optional `showWhen` for per-item visibility\n * - Parent evaluates showWhen and filters visible questions\n * - Parent manages expand state and re-rendering on context changes\n */\nexport function FAQWidget({ config, runtime, instanceId }: FAQWidgetProps) {\n // Force re-render when context changes\n const [, forceUpdate] = useReducer((x) => x + 1, 0);\n\n // Track expanded question IDs\n const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());\n\n // Search query state\n const [searchQuery, setSearchQuery] = useState('');\n\n // Track feedback state per item\n const [feedbackState, setFeedbackState] = useState<Map<string, FeedbackValue>>(new Map());\n\n // Resolve feedback config\n const feedbackConfig = useMemo(() => resolveFeedbackConfig(config.feedback), [config.feedback]);\n\n // Subscribe to context changes for reactive updates\n useEffect(() => {\n const unsubscribe = runtime.context.subscribe(() => {\n forceUpdate();\n });\n return unsubscribe;\n }, [runtime.context]);\n\n // Filter visible questions based on per-item showWhen\n const visibleQuestions = useMemo(\n () =>\n config.actions.filter((q) => {\n // No showWhen = always visible\n if (!q.showWhen) return true;\n\n // Evaluate the decision strategy\n const result = runtime.evaluateSync<boolean>(q.showWhen);\n return result.value;\n }),\n [config.actions, runtime]\n );\n\n // Apply priority ordering\n const orderedQuestions = useMemo(() => {\n if (config.ordering === 'priority') {\n return [...visibleQuestions].sort(\n (a, b) => (b.config.priority ?? 0) - (a.config.priority ?? 0)\n );\n }\n // 'static' or undefined \u2014 preserve config order\n return visibleQuestions;\n }, [visibleQuestions, config.ordering]);\n\n // Apply search filter\n const filteredQuestions = useMemo(() => {\n if (!config.searchable || !searchQuery.trim()) {\n return orderedQuestions;\n }\n\n const query = searchQuery.toLowerCase();\n return orderedQuestions.filter(\n (q) =>\n q.config.question.toLowerCase().includes(query) ||\n getAnswerText(q.config.answer).toLowerCase().includes(query) ||\n q.config.category?.toLowerCase().includes(query)\n );\n }, [orderedQuestions, searchQuery, config.searchable]);\n\n // Group by category\n const categoryGroups = useMemo(() => {\n const groups = new Map<string | undefined, FAQQuestionAction[]>();\n for (const q of filteredQuestions) {\n const cat = q.config.category;\n if (!groups.has(cat)) {\n groups.set(cat, []);\n }\n groups.get(cat)!.push(q);\n }\n return groups;\n }, [filteredQuestions]);\n\n // Check if any items have categories\n const hasCategories = useMemo(() => {\n return filteredQuestions.some((q) => q.config.category);\n }, [filteredQuestions]);\n\n // Resolve theme (auto -> detect system preference)\n const resolvedTheme = useMemo(() => {\n if (config.theme !== 'auto') return config.theme;\n\n // Check system preference (SSR-safe)\n if (typeof window !== 'undefined') {\n return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n }\n return 'light';\n }, [config.theme]);\n\n // Handle question toggle\n const handleToggle = useCallback(\n (id: string) => {\n setExpandedIds((prev) => {\n const next = new Set(prev);\n\n if (config.expandBehavior === 'single') {\n // Single mode: collapse all others\n if (prev.has(id)) {\n return new Set();\n }\n return new Set([id]);\n }\n // Multiple mode: toggle this one\n if (prev.has(id)) {\n next.delete(id);\n } else {\n next.add(id);\n }\n return next;\n });\n\n // Publish toggle event for analytics\n runtime.events.publish('faq:toggled', {\n instanceId,\n questionId: id,\n expanded: !expandedIds.has(id),\n timestamp: Date.now(),\n });\n },\n [config.expandBehavior, runtime.events, instanceId, expandedIds]\n );\n\n // Handle feedback\n const handleFeedback = useCallback(\n (itemId: string, question: string, value: FeedbackValue) => {\n setFeedbackState((prev) => {\n const next = new Map(prev);\n next.set(itemId, value);\n return next;\n });\n\n runtime.events.publish('faq:feedback', {\n itemId,\n question,\n value,\n });\n },\n [runtime.events]\n );\n\n // Compute styles\n const containerStyle: React.CSSProperties = {\n ...baseStyles.container,\n ...themeStyles[resolvedTheme].container,\n };\n\n const searchInputStyle: React.CSSProperties = {\n ...baseStyles.searchInput,\n ...themeStyles[resolvedTheme].searchInput,\n };\n\n const emptyStateStyle: React.CSSProperties = {\n ...baseStyles.emptyState,\n ...themeStyles[resolvedTheme].emptyState,\n };\n\n const categoryHeaderStyle: React.CSSProperties = {\n ...baseStyles.categoryHeader,\n ...themeStyles[resolvedTheme].categoryHeader,\n };\n\n // Render a list of FAQ items\n const renderItems = (items: FAQQuestionAction[]) =>\n items.map((q) => (\n <FAQItem\n key={q.config.id}\n item={q}\n isExpanded={expandedIds.has(q.config.id)}\n onToggle={() => handleToggle(q.config.id)}\n theme={resolvedTheme}\n feedbackConfig={feedbackConfig}\n feedbackValue={feedbackState.get(q.config.id)}\n onFeedback={handleFeedback}\n />\n ));\n\n // Empty state (no visible questions at all)\n if (visibleQuestions.length === 0) {\n return (\n <div style={containerStyle} data-adaptive-id={instanceId} data-adaptive-type=\"adaptive-faq\">\n <div style={emptyStateStyle}>No FAQ questions available.</div>\n </div>\n );\n }\n\n return (\n <div style={containerStyle} data-adaptive-id={instanceId} data-adaptive-type=\"adaptive-faq\">\n {/* Search input */}\n {config.searchable && (\n <div style={baseStyles.searchWrapper}>\n <input\n type=\"text\"\n placeholder=\"Search questions...\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n style={searchInputStyle}\n />\n </div>\n )}\n\n {/* Accordion \u2014 grouped by category if categories are present */}\n <div style={baseStyles.accordion}>\n {hasCategories\n ? Array.from(categoryGroups.entries()).map(([category, items]) => (\n <React.Fragment key={category ?? '__ungrouped'}>\n {category && (\n <div style={categoryHeaderStyle} data-category-header={category}>\n {category}\n </div>\n )}\n {renderItems(items)}\n </React.Fragment>\n ))\n : renderItems(filteredQuestions)}\n </div>\n\n {/* No search results */}\n {config.searchable && filteredQuestions.length === 0 && searchQuery && (\n <div style={{ ...baseStyles.noResults, ...themeStyles[resolvedTheme].emptyState }}>\n No questions found matching "{searchQuery}"\n </div>\n )}\n </div>\n );\n}\n\n// ============================================================================\n// Mountable Widget Interface\n// ============================================================================\n\n/**\n * Mountable widget interface for the runtime's WidgetRegistry.\n */\nexport const FAQMountableWidget = {\n mount(\n container: HTMLElement,\n config?: FAQConfig & { runtime?: FAQWidgetRuntime; instanceId?: string }\n ) {\n // This is a simplified mount for non-React environments\n // In practice, the runtime handles React rendering\n\n const {\n runtime,\n instanceId: _instanceId = 'faq-widget',\n ...faqConfig\n } = config || {\n expandBehavior: 'single' as const,\n searchable: false,\n theme: 'auto' as const,\n actions: [],\n };\n\n // Create simple HTML fallback if no runtime\n if (!runtime) {\n const questions = faqConfig.actions || [];\n container.innerHTML = `\n <div style=\"font-family: system-ui; max-width: 800px;\">\n ${questions\n .map(\n (q) => `\n <div style=\"margin-bottom: 8px; padding: 16px; background: #f9fafb; border-radius: 8px;\">\n <strong>${q.config.question}</strong>\n <p style=\"margin-top: 8px; color: #4b5563;\">${getAnswerText(q.config.answer)}</p>\n </div>\n `\n )\n .join('')}\n </div>\n `;\n }\n\n return () => {\n container.innerHTML = '';\n };\n },\n};\n\nexport default FAQWidget;\n", "/**\n * Adaptive FAQ - Action Executors\n *\n * Three executors that operate on the FAQStore:\n * - executeScrollToFaq: scroll to a FAQ item and optionally expand it\n * - executeToggleFaqItem: open / close / toggle a FAQ item\n * - executeUpdateFaq: add, remove, reorder, or replace FAQ items\n */\n\nimport type {\n ScrollToFaqAction,\n ToggleFaqItemAction,\n UpdateFaqAction,\n ExecutorResult,\n ExecutorContext,\n FAQQuestionAction,\n} from './types';\nimport type { FAQStore } from './state';\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Resolve a FAQ item by direct ID or by fuzzy question text match.\n * Throws if neither yields a result.\n */\nfunction resolveItem(store: FAQStore, itemId?: string, itemQuestion?: string): FAQQuestionAction {\n if (itemId) {\n const found = store.getState().items.find((i) => i.config.id === itemId);\n if (found) return found;\n }\n\n if (itemQuestion) {\n const found = store.findByQuestion(itemQuestion);\n if (found) return found;\n }\n\n throw new Error('FAQ item not found');\n}\n\n// ============================================================================\n// executeScrollToFaq\n// ============================================================================\n\n/**\n * Scroll to a FAQ item in the DOM and optionally expand it.\n *\n * Looks up the item by `itemId` or `itemQuestion`, scrolls the matching\n * `[data-faq-item-id]` element into view, and expands it unless\n * `expand` is explicitly set to `false`.\n */\nexport async function executeScrollToFaq(\n action: ScrollToFaqAction,\n context: ExecutorContext,\n store: FAQStore\n): Promise<ExecutorResult> {\n const item = resolveItem(store, action.itemId, action.itemQuestion);\n const id = item.config.id;\n\n // Expand the item unless explicitly told not to\n if (action.expand !== false) {\n store.expand(id);\n }\n\n // Scroll the DOM element into view (may be absent in test environments)\n const el = document.querySelector(`[data-faq-item-id=\"${id}\"]`);\n if (el) {\n el.scrollIntoView({\n behavior: action.behavior ?? 'smooth',\n });\n }\n\n // Publish analytics event\n context.publishEvent('faq:scroll_to', { itemId: id });\n\n return {\n cleanup: () => {\n // Optionally collapse on revert\n },\n };\n}\n\n// ============================================================================\n// executeToggleFaqItem\n// ============================================================================\n\n/**\n * Open, close, or toggle a FAQ item's expanded state.\n */\nexport async function executeToggleFaqItem(\n action: ToggleFaqItemAction,\n context: ExecutorContext,\n store: FAQStore\n): Promise<ExecutorResult> {\n const item = resolveItem(store, action.itemId, action.itemQuestion);\n const id = item.config.id;\n const desiredState = action.state ?? 'toggle';\n\n let newState: 'open' | 'closed';\n\n switch (desiredState) {\n case 'open':\n store.expand(id);\n newState = 'open';\n break;\n case 'closed':\n store.collapse(id);\n newState = 'closed';\n break;\n case 'toggle':\n default: {\n const wasExpanded = store.getState().expandedItems.has(id);\n store.toggle(id);\n newState = wasExpanded ? 'closed' : 'open';\n break;\n }\n }\n\n context.publishEvent('faq:toggle', { itemId: id, newState });\n\n return {\n cleanup: () => {\n // Revert toggle on cleanup\n },\n };\n}\n\n// ============================================================================\n// executeUpdateFaq\n// ============================================================================\n\n/**\n * Add, remove, reorder, or replace FAQ items in the store.\n */\nexport async function executeUpdateFaq(\n action: UpdateFaqAction,\n context: ExecutorContext,\n store: FAQStore\n): Promise<ExecutorResult> {\n switch (action.operation) {\n case 'add': {\n const items = action.items ?? [];\n const position = action.position === 'prepend' ? 'prepend' : 'append';\n store.addItems(items, position);\n break;\n }\n\n case 'remove': {\n if (!action.itemId) {\n throw new Error('FAQ item not found');\n }\n // Verify the item exists before removing\n const exists = store.getState().items.some((i) => i.config.id === action.itemId);\n if (!exists) {\n throw new Error('FAQ item not found');\n }\n store.removeItem(action.itemId);\n break;\n }\n\n case 'reorder': {\n const order = action.order ?? [];\n store.reorderItems(order);\n break;\n }\n\n case 'replace': {\n const items = action.items ?? [];\n store.replaceItems(items);\n break;\n }\n }\n\n context.publishEvent('faq:update', { operation: action.operation });\n\n return {\n cleanup: () => {\n // Could snapshot previous state for undo\n },\n };\n}\n\n// ============================================================================\n// Executor Definitions for Registration\n// ============================================================================\n\n/**\n * All executors provided by adaptive-faq.\n * These are registered with the runtime's ExecutorRegistry.\n */\nexport const executorDefinitions = [\n { kind: 'faq:scroll_to', executor: executeScrollToFaq },\n { kind: 'faq:toggle_item', executor: executeToggleFaqItem },\n { kind: 'faq:update', executor: executeUpdateFaq },\n] as const;\n", "/**\n * Adaptive FAQ - Runtime Module\n *\n * Runtime manifest for the FAQ accordion adaptive.\n * Provides action executors and widget registration.\n */\n\nimport { FAQMountableWidget } from './FAQWidget';\nimport { executorDefinitions } from './executors';\n\n// ============================================================================\n// App Runtime Manifest\n// ============================================================================\n\n/**\n * Runtime manifest for adaptive-faq.\n *\n * Provides:\n * - FAQ action executors (scroll_to, toggle_item, update)\n * - Widget-based accordion (compositional faq:question actions)\n */\nexport const runtime = {\n id: 'adaptive-faq',\n version: '2.0.0',\n name: 'FAQ Accordion',\n description:\n 'Collapsible Q&A accordion with actions, rich content, feedback, and personalization',\n\n /**\n * Action executors for programmatic FAQ interaction.\n */\n executors: executorDefinitions,\n\n /**\n * Widget definitions for the runtime's WidgetRegistry.\n */\n widgets: [\n {\n id: 'adaptive-faq:accordion',\n component: FAQMountableWidget,\n metadata: {\n name: 'FAQ Accordion',\n description: 'Collapsible Q&A accordion with search, categories, and feedback',\n icon: '\u2753',\n },\n },\n ],\n};\n\nexport default runtime;\n", "/**\n * CDN Entry Point for Adaptive FAQ\n *\n * This module is bundled for CDN delivery and self-registers with the global\n * SynOS app registry when loaded dynamically via the AppLoader.\n */\n\nimport { runtime } from './runtime';\n\n/**\n * App manifest for registry registration.\n * Follows the AppManifest interface expected by AppLoader/AppRegistry.\n */\nexport const manifest = {\n id: 'faq',\n version: runtime.version,\n name: runtime.name,\n description: runtime.description,\n runtime: {\n actions: runtime.executors,\n widgets: runtime.widgets,\n },\n metadata: {\n isBuiltIn: false,\n },\n};\n\n/**\n * Self-register with global registry if available.\n * This happens when loaded via script tag (UMD).\n */\nif (typeof window !== 'undefined') {\n const globalRegistry = (window as any).__SYNOS_APP_REGISTRY__;\n if (globalRegistry && typeof globalRegistry.register === 'function') {\n globalRegistry.register(manifest);\n }\n}\n\nexport default manifest;\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["React", "useEffect", "useReducer", "useMemo", "useCallback", "useState", "jsx", "jsxs", "getAnswerText", "answer", "FAQMountableWidget", "container", "config", "runtime", "_instanceId", "faqConfig", "questions", "q", "getAnswerText", "resolveItem", "store", "itemId", "itemQuestion", "found", "i", "executeScrollToFaq", "action", "context", "id", "el", "executeToggleFaqItem", "desiredState", "newState", "wasExpanded", "executeUpdateFaq", "items", "position", "order", "executorDefinitions", "runtime", "executorDefinitions", "FAQMountableWidget", "manifest", "runtime", "globalRegistry", "cdn_default"]
|
|
7
7
|
}
|
|
@@ -12,11 +12,11 @@ function ComparisonItemCard({ item, accentColor, layout, }) {
|
|
|
12
12
|
padding: 'var(--sc-spacing-md, 0.75rem)',
|
|
13
13
|
borderRadius: 'var(--sc-border-radius-sm, 6px)',
|
|
14
14
|
background: isHighlighted
|
|
15
|
-
? 'var(--sc-color-primary-muted,
|
|
16
|
-
: 'var(--sc-color-background-subtle,
|
|
15
|
+
? 'var(--sc-color-primary-muted, #2c0b0a)'
|
|
16
|
+
: 'var(--sc-color-background-subtle, #07080a33)',
|
|
17
17
|
border: isHighlighted
|
|
18
18
|
? `1px solid ${accentColor}`
|
|
19
|
-
: '1px solid var(--sc-color-border-subtle,
|
|
19
|
+
: '1px solid var(--sc-color-border-subtle, #1c222a)',
|
|
20
20
|
transition: 'all 0.15s ease',
|
|
21
21
|
};
|
|
22
22
|
const iconStyle = {
|
|
@@ -25,13 +25,13 @@ function ComparisonItemCard({ item, accentColor, layout, }) {
|
|
|
25
25
|
};
|
|
26
26
|
const labelStyle = {
|
|
27
27
|
fontSize: 'var(--sc-font-size-sm, 0.8rem)',
|
|
28
|
-
color: 'var(--sc-color-text-secondary, #
|
|
28
|
+
color: 'var(--sc-color-text-secondary, #a8afba)',
|
|
29
29
|
flex: layout === 'list' ? 1 : undefined,
|
|
30
30
|
};
|
|
31
31
|
const valueStyle = {
|
|
32
32
|
fontSize: 'var(--sc-font-size-lg, 1rem)',
|
|
33
33
|
fontWeight: 'var(--sc-font-weight-semibold, 600)',
|
|
34
|
-
color: isHighlighted ? accentColor : 'var(--sc-color-text, #
|
|
34
|
+
color: isHighlighted ? accentColor : 'var(--sc-color-text, #cbd0d7)',
|
|
35
35
|
};
|
|
36
36
|
return (_jsxs("div", { style: cardStyle, children: [item.icon && _jsx("span", { style: iconStyle, children: item.icon }), _jsx("span", { style: labelStyle, children: item.label }), _jsx("span", { style: valueStyle, children: item.value })] }));
|
|
37
37
|
}
|
|
@@ -39,7 +39,7 @@ function ComparisonItemCard({ item, accentColor, layout, }) {
|
|
|
39
39
|
* Comparison block component - displays items in grid/list/table layout
|
|
40
40
|
*/
|
|
41
41
|
export function ComparisonBlock({ content, accentColor }) {
|
|
42
|
-
const primaryColor = accentColor || 'var(--sc-color-primary, #
|
|
42
|
+
const primaryColor = accentColor || 'var(--sc-color-primary, #b72e2a)';
|
|
43
43
|
const layout = content.layout || 'grid';
|
|
44
44
|
const columns = content.columns || 2;
|
|
45
45
|
// Find best item if highlightBest is true
|
|
@@ -71,11 +71,11 @@ export function ComparisonBlock({ content, accentColor }) {
|
|
|
71
71
|
fontSize: 'var(--sc-font-size-sm, 0.8rem)',
|
|
72
72
|
}, children: _jsx("tbody", { children: processedItems.map((item, idx) => (_jsxs("tr", { style: {
|
|
73
73
|
borderBottom: idx < processedItems.length - 1
|
|
74
|
-
? '1px solid var(--sc-color-border-subtle,
|
|
74
|
+
? '1px solid var(--sc-color-border-subtle, #1c222a)'
|
|
75
75
|
: undefined,
|
|
76
76
|
}, children: [_jsxs("td", { style: {
|
|
77
77
|
padding: 'var(--sc-spacing-sm, 0.5rem) 0',
|
|
78
|
-
color: 'var(--sc-color-text-secondary, #
|
|
78
|
+
color: 'var(--sc-color-text-secondary, #a8afba)',
|
|
79
79
|
display: 'flex',
|
|
80
80
|
alignItems: 'center',
|
|
81
81
|
gap: 'var(--sc-spacing-sm, 0.5rem)',
|
|
@@ -83,7 +83,7 @@ export function ComparisonBlock({ content, accentColor }) {
|
|
|
83
83
|
padding: 'var(--sc-spacing-sm, 0.5rem) 0',
|
|
84
84
|
textAlign: 'right',
|
|
85
85
|
fontWeight: 'var(--sc-font-weight-semibold, 600)',
|
|
86
|
-
color: item.highlight ? primaryColor : 'var(--sc-color-text, #
|
|
86
|
+
color: item.highlight ? primaryColor : 'var(--sc-color-text, #cbd0d7)',
|
|
87
87
|
}, children: item.value })] }, idx))) }) }));
|
|
88
88
|
}
|
|
89
89
|
// Grid or list layout
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ComparisonBlock.js","sourceRoot":"","sources":["../../../src/blocks/data/ComparisonBlock.tsx"],"names":[],"mappings":";AASA;;GAEG;AACH,SAAS,kBAAkB,CAAC,EAC1B,IAAI,EACJ,WAAW,EACX,MAAM,GAKP;IACC,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,CAAC;IAErC,MAAM,SAAS,GAAkB;QAC/B,OAAO,EAAE,MAAM;QACf,aAAa,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ;QACnD,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY;QACvD,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,+BAA+B,CAAC,CAAC,CAAC,+BAA+B;QAC1F,OAAO,EAAE,+BAA+B;QACxC,YAAY,EAAE,iCAAiC;QAC/C,UAAU,EAAE,aAAa;YACvB,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"ComparisonBlock.js","sourceRoot":"","sources":["../../../src/blocks/data/ComparisonBlock.tsx"],"names":[],"mappings":"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"}
|
|
@@ -7,15 +7,15 @@ function TrendBadge({ trend }) {
|
|
|
7
7
|
const isPositive = trend.direction === 'up';
|
|
8
8
|
const isNegative = trend.direction === 'down';
|
|
9
9
|
const color = isPositive
|
|
10
|
-
? 'var(--sc-color-success, #
|
|
10
|
+
? 'var(--sc-color-success, #24ad32)'
|
|
11
11
|
: isNegative
|
|
12
|
-
? 'var(--sc-color-error, #
|
|
13
|
-
: 'var(--sc-color-text-muted, #
|
|
12
|
+
? 'var(--sc-color-error, #ff2524)'
|
|
13
|
+
: 'var(--sc-color-text-muted, #87919f)';
|
|
14
14
|
const bgColor = isPositive
|
|
15
|
-
? 'var(--sc-color-success-muted,
|
|
15
|
+
? 'var(--sc-color-success-muted, #07230a)'
|
|
16
16
|
: isNegative
|
|
17
|
-
? 'var(--sc-color-error-muted,
|
|
18
|
-
: '
|
|
17
|
+
? 'var(--sc-color-error-muted, #330707)'
|
|
18
|
+
: '#87919f26';
|
|
19
19
|
const ArrowIcon = () => (_jsx("svg", { width: "10", height: "10", viewBox: "0 0 10 10", fill: "none", style: {
|
|
20
20
|
transform: isPositive ? 'rotate(0deg)' : isNegative ? 'rotate(180deg)' : 'rotate(90deg)',
|
|
21
21
|
}, children: _jsx("path", { d: "M5 1v8M5 1L2 4M5 1l3 3", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
@@ -29,7 +29,7 @@ function TrendBadge({ trend }) {
|
|
|
29
29
|
color,
|
|
30
30
|
fontSize: 'var(--sc-font-size-xs, 0.7rem)',
|
|
31
31
|
fontWeight: 'var(--sc-font-weight-medium, 500)',
|
|
32
|
-
}, children: [_jsx(ArrowIcon, {}), trend.value, trend.timeframe && (_jsx("span", { style: { color: 'var(--sc-color-text-muted, #
|
|
32
|
+
}, children: [_jsx(ArrowIcon, {}), trend.value, trend.timeframe && (_jsx("span", { style: { color: 'var(--sc-color-text-muted, #87919f)' }, children: trend.timeframe }))] }));
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
35
|
* Mini sparkline chart using SVG
|
|
@@ -61,7 +61,7 @@ function Sparkline({ data, color }) {
|
|
|
61
61
|
* Stats block component - displays a metric with trend and sparkline
|
|
62
62
|
*/
|
|
63
63
|
export function StatsBlock({ content, accentColor }) {
|
|
64
|
-
const primaryColor = accentColor || 'var(--sc-color-primary, #
|
|
64
|
+
const primaryColor = accentColor || 'var(--sc-color-primary, #b72e2a)';
|
|
65
65
|
const containerStyle = {
|
|
66
66
|
display: 'flex',
|
|
67
67
|
flexDirection: 'column',
|
|
@@ -83,11 +83,11 @@ export function StatsBlock({ content, accentColor }) {
|
|
|
83
83
|
const unitStyle = {
|
|
84
84
|
fontSize: 'var(--sc-font-size-md, 0.9rem)',
|
|
85
85
|
fontWeight: 'var(--sc-font-weight-normal, 400)',
|
|
86
|
-
color: 'var(--sc-color-text-muted, #
|
|
86
|
+
color: 'var(--sc-color-text-muted, #87919f)',
|
|
87
87
|
};
|
|
88
88
|
const labelStyle = {
|
|
89
89
|
fontSize: 'var(--sc-font-size-sm, 0.8rem)',
|
|
90
|
-
color: 'var(--sc-color-text-secondary, #
|
|
90
|
+
color: 'var(--sc-color-text-secondary, #a8afba)',
|
|
91
91
|
margin: 0,
|
|
92
92
|
};
|
|
93
93
|
const bottomRowStyle = {
|