@syntrologie/adapt-faq 2.15.0 → 2.17.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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../sdk-contracts/dist/schemas.js", "../src/schema.ts"],
4
+ "sourcesContent": ["/**\n * Shared Zod schemas for decision strategies, conditions, and event scoping.\n *\n * These are the canonical definitions \u2014 runtime-sdk and all adaptive packages\n * should import from here instead of duplicating.\n */\nimport { z } from 'zod';\n// =============================================================================\n// ANCHOR ID SCHEMA\n// =============================================================================\nexport const AnchorIdZ = z\n .object({\n selector: z.string(),\n route: z.union([z.string(), z.array(z.string())]),\n})\n .strict();\n// =============================================================================\n// AUTHORING FIELDS \u2014 title / description / validation\n//\n// These live on every action *during authoring* (LLM produces them; dashboard\n// displays them; reviewers QA-verify with them) but are stripped before the\n// runtime SDK receives the config. Stripping happens server-side in\n// `to_runtime_config` (platform/backend/app/domains/experiments/helpers.py).\n// They appear in the JSON Schema (and therefore in the tactician's prompt)\n// because the LLM needs to know they are valid action properties \u2014 otherwise\n// schema validation would reject what the prompt commands.\n//\n// Each action variant should `.extend(AuthoringFieldsZ)` alongside any\n// triggerWhen/condition extensions. Validation is an ordered list of\n// human-readable step strings \u2014 reviewers scan it as a checklist.\n// =============================================================================\nexport const AuthoringFieldsZ = {\n title: z\n .string()\n .max(200)\n .optional()\n .describe('Authoring-only: short label shown on the action plan dashboard. Stripped before serving to the runtime SDK.'),\n description: z\n .string()\n .max(1000)\n .optional()\n .describe('Authoring-only: one-sentence explanation of what this action does and why. Stripped before serving to the runtime SDK.'),\n validation: z\n .array(z.string().max(500))\n .max(10)\n .optional()\n .describe('Authoring-only: ordered steps a reviewer can follow to trigger this action and visually confirm it works. Each entry is one step. Stripped before serving to the runtime SDK.'),\n};\n// =============================================================================\n// TRIGGER VOCABULARY \u2014 canonical lists of valid event names, metric keys, etc.\n// These flow through to the JSON schema as enums and are used by the LLM prompt.\n// =============================================================================\n/** Events that can be counted in event_count conditions. */\nexport const COUNTABLE_EVENTS = [\n // User interactions (from PostHog autocapture normalization)\n 'ui.click',\n 'ui.scroll',\n 'ui.input',\n 'ui.change',\n 'ui.submit',\n // Behavioral detectors (from event-processor)\n 'ui.hover',\n 'ui.idle',\n 'ui.scroll_thrash',\n 'ui.focus_bounce',\n // Navigation\n 'nav.page_view',\n 'nav.page_leave',\n // Derived behavioral signals\n 'behavior.rage_click',\n 'behavior.hesitation',\n 'behavior.confusion',\n];\nexport const CountableEventZ = z\n .enum(COUNTABLE_EVENTS)\n .describe('Event name to count. ui.* = user interactions and behavioral detectors, nav.* = page navigation, behavior.* = derived behavioral signals.');\n/** Valid session metric keys. */\nexport const SESSION_METRIC_KEYS = ['time_on_page', 'page_views', 'scroll_depth'];\nexport const SessionMetricKeyZ = z\n .enum(SESSION_METRIC_KEYS)\n .describe('Session metric key. time_on_page = seconds on current page, page_views = pages visited this session, scroll_depth = 0-100 percentage.');\n/** Element chain match field prefixes for counter filters. */\nexport const ELEMENT_MATCH_FIELDS = ['tag_name', '$el_text'];\n// Note: attr__* is a dynamic prefix (attr__data-id, attr__class, attr__href, etc.)\n// and cannot be enumerated. The match key is either one of ELEMENT_MATCH_FIELDS\n// or starts with \"attr__\".\n// =============================================================================\n// CONDITION SCHEMAS\n// =============================================================================\nexport const PageUrlConditionZ = z\n .object({\n type: z.literal('page_url'),\n url: z.string().describe('URL path to match (e.g. \"/pricing\", \"/dashboard\")'),\n})\n .describe('Fires when the current page URL matches. Use for page-specific actions. ' +\n 'Example: {\"type\": \"page_url\", \"url\": \"/pricing\"}');\nexport const RouteConditionZ = z\n .object({\n type: z.literal('route'),\n routeId: z.string().describe('Named route ID from the route filter'),\n})\n .describe('Fires when the current route matches a named route ID.');\nexport const AnchorVisibleConditionZ = z\n .object({\n type: z.literal('anchor_visible'),\n anchorId: z.string().describe('CSS selector of the anchor element'),\n state: z\n .enum(['visible', 'present', 'absent'])\n .describe('\"visible\" = in viewport, \"present\" = in DOM, \"absent\" = not in DOM'),\n})\n .describe(\"Fires based on a DOM element's visibility state. \" +\n 'Example: {\"type\": \"anchor_visible\", \"anchorId\": \"#cta-button\", \"state\": \"visible\"}');\nexport const EventOccurredConditionZ = z\n .object({\n type: z.literal('event_occurred'),\n eventName: z.string().describe('Event name (e.g. \"ui.click\", \"$pageview\")'),\n withinMs: z.number().optional().describe('Time window in ms. Omit = any time this session.'),\n})\n .describe('Fires when a specific event has occurred during this session. ' +\n 'Example: {\"type\": \"event_occurred\", \"eventName\": \"ui.click\", \"withinMs\": 5000}');\nexport const StateEqualsConditionZ = z\n .object({\n type: z.literal('state_equals'),\n key: z\n .string()\n .describe('Key in the SDK persistent state store (localStorage). Only valid for keys the host app explicitly sets via syntro.state.set().'),\n value: z.unknown().describe('Expected value to match against'),\n})\n .describe('Checks the SDK persistent state store (localStorage). ONLY for host-app state set via syntro.state.set() \u2014 ' +\n 'NOT for user attributes like region, device, or UTM params (those are handled by segment targeting). ' +\n 'Do NOT use this for targeting. If you do not know the valid state keys, do not use this condition type.');\nexport const ViewportConditionZ = z\n .object({\n type: z.literal('viewport'),\n minWidth: z.number().optional().describe('Minimum viewport width in pixels'),\n maxWidth: z.number().optional().describe('Maximum viewport width in pixels'),\n minHeight: z.number().optional().describe('Minimum viewport height in pixels'),\n maxHeight: z.number().optional().describe('Maximum viewport height in pixels'),\n})\n .describe('Fires based on viewport (screen) size. Use for responsive behavior. ' +\n 'Example: {\"type\": \"viewport\", \"minWidth\": 768} \u2014 fires on tablet and larger.');\nexport const SessionMetricConditionZ = z\n .object({\n type: z.literal('session_metric'),\n key: SessionMetricKeyZ,\n operator: z.enum(['gte', 'lte', 'eq', 'gt', 'lt']),\n threshold: z.number().describe('Numeric threshold to compare against'),\n})\n .describe('Fires when a session metric crosses a threshold. Valid keys: \"time_on_page\" (seconds), ' +\n '\"page_views\" (count), \"scroll_depth\" (0-100). ' +\n 'Example: {\"type\": \"session_metric\", \"key\": \"time_on_page\", \"operator\": \"gte\", \"threshold\": 30}');\nexport const DismissedConditionZ = z\n .object({\n type: z.literal('dismissed'),\n key: z.string().describe('Dismissal key (usually a tile or action ID)'),\n inverted: z\n .boolean()\n .optional()\n .describe('When true, fires if NOT dismissed (default behavior)'),\n})\n .describe('Checks if an item has been dismissed by the user. Use with inverted: true to show only if not dismissed.');\nexport const CooldownActiveConditionZ = z\n .object({\n type: z.literal('cooldown_active'),\n key: z.string().describe('Cooldown key'),\n inverted: z.boolean().optional().describe('When true, fires if cooldown is NOT active'),\n})\n .describe('Checks if a cooldown timer is currently active. Use to prevent showing the same intervention too frequently.');\nexport const FrequencyLimitConditionZ = z\n .object({\n type: z.literal('frequency_limit'),\n key: z.string().describe('Frequency counter key'),\n limit: z.number().describe('Maximum allowed count'),\n inverted: z.boolean().optional().describe('When true, fires if limit NOT reached'),\n})\n .describe('Checks if a frequency limit has been reached. Use to cap how many times an action fires per session.');\nexport const MatchOpZ = z\n .object({\n equals: z.union([z.string(), z.number(), z.boolean()]).optional(),\n contains: z.string().optional(),\n})\n .describe('Match operator for counter filters. Exactly one of equals or contains must be specified.');\nexport const CounterDefZ = z\n .object({\n events: z\n .array(CountableEventZ)\n .min(1)\n .describe('Event names to count. Use values from the countable events enum.'),\n match: z\n .record(z.string(), MatchOpZ)\n .optional()\n .describe('Property filters. Keys are event prop names or element-chain fields ' +\n '(tag_name, $el_text, attr__*). All entries AND together.'),\n})\n .describe('Defines what events to count. Registered as an accumulator predicate at config-load time.');\nexport const EventCountConditionZ = z\n .object({\n type: z.literal('event_count'),\n key: z.string().describe('Unique key for this counter (used for accumulator registration)'),\n operator: z.enum(['gte', 'lte', 'eq', 'gt', 'lt']),\n count: z.number().int().min(0).describe('Target count threshold'),\n withinMs: z\n .number()\n .positive()\n .optional()\n .describe('Time window in ms. Omit = count across entire session.'),\n counter: CounterDefZ.optional().describe('Inline counter definition. Defines what events to count.'),\n})\n .describe('Fires when accumulated event count crosses a threshold. Most powerful trigger type. ' +\n 'Example: {\"type\": \"event_count\", \"key\": \"pricing-clicks\", \"operator\": \"gte\", \"count\": 3, ' +\n '\"counter\": {\"events\": [\"ui.click\"], \"match\": {\"attr__data-cta\": {\"contains\": \"pricing\"}}}}');\nexport const ConditionZ = z.discriminatedUnion('type', [\n PageUrlConditionZ,\n RouteConditionZ,\n AnchorVisibleConditionZ,\n EventOccurredConditionZ,\n StateEqualsConditionZ,\n ViewportConditionZ,\n SessionMetricConditionZ,\n DismissedConditionZ,\n CooldownActiveConditionZ,\n FrequencyLimitConditionZ,\n EventCountConditionZ,\n]);\n// =============================================================================\n// STRATEGY SCHEMAS\n// =============================================================================\nexport const RuleZ = z\n .object({\n conditions: z\n .array(ConditionZ)\n .describe('Array of conditions \u2014 ALL must match (AND logic) for this rule to fire.'),\n value: z\n .unknown()\n .describe('Value returned when all conditions match. For triggerWhen: true = fire the action.'),\n})\n .describe('A single rule. ALL conditions must match (AND logic). Rules in a strategy are evaluated ' +\n 'top-to-bottom \u2014 first rule where all conditions match wins and returns its value.');\nexport const RuleStrategyZ = z\n .object({\n type: z.literal('rules'),\n rules: z\n .array(RuleZ)\n .describe('Ordered list of rules. Evaluated top-to-bottom \u2014 first match wins.'),\n default: z\n .unknown()\n .describe('Fallback value when no rule matches. For triggerWhen: false = do not fire by default.'),\n})\n .describe('Rule-based strategy. Evaluates rules top-to-bottom. First rule where ALL conditions match ' +\n 'returns its value. If no rule matches, returns default. ' +\n 'For triggerWhen: set value=true on matching rules, default=false.');\nexport const ScoreStrategyZ = z\n .object({\n type: z.literal('score'),\n field: z.string(),\n threshold: z.number(),\n above: z.unknown(),\n below: z.unknown(),\n})\n .describe('Score-based strategy. Compares a field value against a threshold.');\nexport const ModelStrategyZ = z\n .object({\n type: z.literal('model'),\n modelId: z.string(),\n inputs: z.array(z.string()),\n outputMapping: z.record(z.string(), z.unknown()),\n default: z.unknown(),\n})\n .describe('ML model strategy. Sends inputs to a model and maps outputs.');\nexport const ExternalStrategyZ = z\n .object({\n type: z.literal('external'),\n endpoint: z.string(),\n method: z.enum(['GET', 'POST']).optional(),\n default: z.unknown(),\n timeoutMs: z.number().optional(),\n})\n .describe('External API strategy. Calls an endpoint to determine the value.');\nexport const DecisionStrategyZ = z.discriminatedUnion('type', [\n RuleStrategyZ,\n ScoreStrategyZ,\n ModelStrategyZ,\n ExternalStrategyZ,\n]);\n/** Canonical Zod schema for the optional triggerWhen field on actions and adaptive items. */\nexport const TriggerWhenZ = DecisionStrategyZ.nullable().optional();\n// =============================================================================\n// TRIGGER DOCUMENTATION \u2014 examples and match field docs\n// Exported as constants so the schema generator can inject them into the\n// JSON schema. The Python prompt builder reads them from the schema.\n// =============================================================================\n/** Complete triggerWhen examples showing the full rules wrapper structure. */\nexport const TRIGGER_EXAMPLES = [\n {\n name: 'Click count on a specific element',\n description: 'Fire when user clicks an element with data-id=\"hero-cta\" 2+ times',\n triggerWhen: {\n type: 'rules',\n rules: [\n {\n conditions: [\n {\n type: 'event_count',\n key: 'cta-clicks',\n operator: 'gte',\n count: 2,\n counter: {\n events: ['ui.click'],\n match: { 'attr__data-id': { equals: 'hero-cta' } },\n },\n },\n ],\n value: true,\n },\n ],\n default: false,\n },\n },\n {\n name: 'Time on page threshold',\n description: 'Fire after user spends 30+ seconds on the page',\n triggerWhen: {\n type: 'rules',\n rules: [\n {\n conditions: [\n {\n type: 'session_metric',\n key: 'time_on_page',\n operator: 'gte',\n threshold: 30,\n },\n ],\n value: true,\n },\n ],\n default: false,\n },\n },\n {\n name: 'Element visible in viewport',\n description: 'Fire when a DOM element becomes visible',\n triggerWhen: {\n type: 'rules',\n rules: [\n {\n conditions: [\n {\n type: 'anchor_visible',\n anchorId: '#pricing-section',\n state: 'visible',\n },\n ],\n value: true,\n },\n ],\n default: false,\n },\n },\n {\n name: 'No trigger (fire immediately)',\n description: 'Action fires as soon as the segment matches \u2014 no in-session condition needed',\n triggerWhen: null,\n },\n];\n/** Documentation for counter.match field keys. */\nexport const MATCH_FIELD_DOCS = {\n tag_name: 'HTML tag name (e.g. \"button\", \"a\", \"input\")',\n $el_text: 'Visible text content of the element',\n 'attr__*': 'HTML attribute prefixed with attr__. Example: attr__data-id matches the data-id attribute, ' +\n 'attr__class matches the class attribute, attr__href matches the href attribute.',\n};\n// =============================================================================\n// EVENT SCOPE SCHEMA\n// =============================================================================\n/** Scopes a widget to specific events/URLs. */\nexport const EventScopeZ = z.object({\n events: z.array(z.string()),\n urlContains: z.string().optional(),\n props: z.record(z.union([z.string(), z.number(), z.boolean()])).optional(),\n});\n// =============================================================================\n// NOTIFY SCHEMA\n// =============================================================================\n/** Toast notification config for triggerWhen transitions. */\nexport const NotifyZ = z\n .object({\n title: z.string().optional(),\n body: z.string().optional(),\n icon: z.string().optional(),\n})\n .nullable()\n .optional();\n", "/**\n * Adaptive FAQ - Config Schema\n *\n * Zod schema for validating FAQ accordion configuration.\n * Demonstrates compositional action pattern with per-item triggerWhen.\n */\n\nimport {\n AnchorIdZ,\n AuthoringFieldsZ,\n DecisionStrategyZ,\n NotifyZ,\n TriggerWhenZ,\n} from '@syntrologie/sdk-contracts';\nimport { z } from 'zod';\n\n// ============================================================================\n// Rich Answer Content Schemas\n// ============================================================================\n\n/** Schema for media assets embedded in enhanced markdown answers. */\nconst AssetZ = z\n .object({\n id: z\n .string()\n .describe('Unique asset ID referenced inside the markdown content via `asset:<id>`.'),\n type: z\n .enum(['image', 'video'])\n .describe('\"image\" for static images, \"video\" for embeddable video clips.'),\n src: z.string().describe('Absolute URL of the media asset.'),\n alt: z.string().optional().describe('Optional alt text for images (accessibility).'),\n width: z.number().optional().describe('Optional display width in pixels.'),\n height: z.number().optional().describe('Optional display height in pixels.'),\n })\n .describe(\n 'A single embedded media asset (image or video) referenced by ID in the markdown content.'\n );\n\n/** Schema for rich HTML answer content. */\nconst RichHTMLAnswerZ = z\n .object({\n type: z.literal('rich').describe('Use this variant when the answer is pre-rendered HTML.'),\n html: z.string().describe('Sanitized HTML string rendered directly as the answer body.'),\n })\n .describe('Rich HTML answer: use when the answer content is pre-rendered HTML markup.');\n\n/** Schema for enhanced markdown answer content with optional assets. */\nconst EnhancedMarkdownAnswerZ = z\n .object({\n type: z\n .literal('markdown')\n .describe(\n 'Use this variant for markdown with embedded images or video via the assets array.'\n ),\n content: z\n .string()\n .describe(\n 'Markdown string. Reference assets using the `asset:<id>` URI scheme, e.g. `![alt](asset:my-img)`.'\n ),\n assets: z\n .array(AssetZ)\n .optional()\n .describe('Optional media assets referenced in the markdown via `asset:<id>` URIs.'),\n })\n .describe(\n 'Enhanced markdown answer: use when the answer mixes formatted text with images or video assets.'\n );\n\n/** Union of all FAQ answer types: plain string, rich HTML, or enhanced markdown. */\nconst FAQAnswerZ = z\n .union([z.string(), RichHTMLAnswerZ, EnhancedMarkdownAnswerZ])\n .describe(\n 'Answer content. Use a plain string for simple text, \"rich\" for pre-rendered HTML, or \"markdown\" for formatted text with embedded assets.'\n );\n\n// ============================================================================\n// Feedback Schemas\n// ============================================================================\n\n/** Schema for feedback widget configuration. */\nconst FeedbackConfigZ = z\n .object({\n style: z\n .enum(['thumbs', 'rating'])\n .describe('\"thumbs\" shows a thumbs-up/down pair; \"rating\" shows a numeric scale.'),\n prompt: z\n .string()\n .optional()\n .describe(\n 'Optional prompt text shown above the feedback controls (e.g. \"Was this helpful?\").'\n ),\n })\n .describe('Detailed feedback widget configuration specifying style and optional prompt text.');\n\n/** Feedback can be a boolean (enable/disable) or a detailed config object. */\nconst FeedbackZ = z\n .union([z.boolean(), FeedbackConfigZ])\n .describe(\n 'Per-item feedback widget. Pass true for default thumbs feedback, false to disable, or a FeedbackConfig object for custom style and prompt.'\n );\n\n// ============================================================================\n// Ordering Schemas\n// ============================================================================\n\n/** Schema for segment-based ordering strategy. */\nconst SegmentOrderingZ = z\n .object({\n type: z\n .literal('segment')\n .describe('Segment-based ordering: item order varies per user segment.'),\n segmentWeights: z\n .record(z.array(z.string()))\n .describe(\n 'Map of segment ID \u2192 ordered list of question IDs. Each segment gets its own item order.'\n ),\n })\n .describe(\n 'Segment-based ordering strategy. Each key is a segment ID and the value is the ordered list of item IDs shown to users in that segment.'\n );\n\n/** Ordering strategy: static order, priority-based, or segment-based. */\nconst OrderingStrategyZ = z\n .union([z.enum(['static', 'priority']), SegmentOrderingZ])\n .describe(\n '\"static\" = config order, \"priority\" = sort by item priority field descending, or a segment object for per-segment ordering.'\n );\n\n// ============================================================================\n// AI Answer Strategy Schema\n// ============================================================================\n\n/** Schema for AI-generated answer configuration. */\nconst AnswerStrategyZ = z\n .object({\n endpoint: z\n .string()\n .min(1)\n .describe('Backend endpoint URL that generates the AI answer for this question.'),\n context: z\n .array(z.string())\n .optional()\n .describe('Optional list of context keys passed to the endpoint to ground the answer.'),\n cache: z\n .enum(['session', 'none'])\n .optional()\n .describe(\n '\"session\" caches the generated answer for the user\\'s session; \"none\" regenerates on each expand.'\n ),\n fallback: z\n .string()\n .optional()\n .describe('Static fallback answer text shown if the AI endpoint fails or times out.'),\n })\n .describe(\n 'AI answer generation strategy. When present, the answer is generated dynamically by calling the specified endpoint.'\n );\n\n// ============================================================================\n// Injection Rule Schema\n// ============================================================================\n\n/** Schema for dynamic FAQ injection rules. */\nconst InjectionRuleZ = z\n .object({\n trigger: DecisionStrategyZ.describe(\n 'Decision strategy that evaluates to true when the items should be injected (e.g. page URL matches checkout).'\n ),\n items: z\n .array(z.lazy(() => FAQQuestionSchema))\n .describe('FAQ question items to inject when the trigger fires.'),\n position: z\n .enum(['prepend', 'append'])\n .optional()\n .describe('\"prepend\" inserts injected items at the top; \"append\" adds them at the bottom.'),\n once: z\n .boolean()\n .optional()\n .describe('When true, items are injected only on the first trigger match and not again.'),\n })\n .describe(\n 'Dynamic injection rule: adds contextual FAQ items when a trigger condition is met (e.g. user is on the checkout page).'\n );\n\n// ============================================================================\n// FAQ Question Schema\n// ============================================================================\n\n/**\n * Schema for a single FAQ question (compositional action).\n */\nexport const FAQQuestionSchema = z\n .object({\n ...AuthoringFieldsZ,\n kind: z\n .literal('faq:question')\n .describe(\n 'Compositional action type for a single FAQ accordion item. Rendered by the adaptive-faq:accordion widget.'\n ),\n config: z\n .object({\n /** Unique identifier for this question */\n id: z.string().min(1, 'ID is required'),\n /** The question text */\n question: z\n .string()\n .min(1, 'Question is required')\n .describe('The question text shown in the accordion header.'),\n /** The answer content (plain string, rich HTML, or enhanced markdown) */\n answer: FAQAnswerZ,\n /** Optional category for grouping */\n category: z\n .string()\n .optional()\n .describe(\n 'Optional grouping label. Questions with the same category are collapsible under a shared section header.'\n ),\n /** Optional priority for ordering */\n priority: z\n .number()\n .optional()\n .describe(\n 'Numeric priority used when ordering=\"priority\". Higher values appear first. Omit for equal priority.'\n ),\n /** Optional AI answer generation strategy */\n answerStrategy: AnswerStrategyZ.optional().describe(\n 'Optional AI answer generation config. When set, the answer field is used as a fallback only.'\n ),\n })\n .describe(\n 'Per-question configuration including the question text, answer content, and optional metadata.'\n ),\n /** Per-item activation strategy (null = always show) */\n triggerWhen: TriggerWhenZ.describe(\n 'Conditional visibility strategy. When null or omitted, the question is always shown. Use a rules strategy to show/hide based on page URL, event counts, or session metrics.'\n ),\n /** Toast config when triggerWhen transitions false \u2192 true. Required when triggerWhen is set (use null to opt out). */\n notify: NotifyZ.describe(\n 'Toast notification shown when triggerWhen transitions false \u2192 true. Required when triggerWhen is set \u2014 pass null to opt out of the toast.'\n ),\n })\n .refine((data) => !data.triggerWhen || data.notify !== undefined, {\n message:\n 'notify is required when triggerWhen is present (use null to opt out of notifications)',\n });\n\nexport type FAQQuestionSchemaType = z.infer<typeof FAQQuestionSchema>;\n\n// ============================================================================\n// FAQ Config Schema\n// ============================================================================\n\n/**\n * Full configuration schema for adaptive-faq.\n */\nexport const configSchema = z\n .object({\n /** Display title for the FAQ widget */\n title: z.string().optional().describe('Optional header text shown above the accordion list.'),\n /** Whether only one or multiple questions can be expanded at once */\n expandBehavior: z\n .enum(['single', 'multiple'])\n .default('single')\n .describe(\n '\"single\" collapses any open item before expanding another; \"multiple\" allows any number open at once.'\n ),\n /** Whether to show a search/filter input */\n searchable: z\n .boolean()\n .default(false)\n .describe('When true, renders a text input that filters questions by keyword.'),\n /** Color theme */\n theme: z\n .enum(['light', 'dark', 'auto'])\n .default('auto')\n .describe(\n '\"auto\" follows the user\\'s OS preference; use \"light\" or \"dark\" to pin the theme.'\n ),\n /** FAQ questions (compositional actions) */\n actions: z\n .array(FAQQuestionSchema)\n .default([])\n .describe(\n 'The FAQ items rendered by the accordion. Each item is a faq:question compositional action \u2014 these are configuration data rendered by the widget, not actions executed by the runtime. This compositional pattern enables per-item conditional visibility via triggerWhen, category grouping, and dynamic injection.'\n ),\n /** Feedback widget configuration */\n feedback: FeedbackZ.optional().describe(\n 'Optional per-item feedback widget. Enable with true (default thumbs style) or a FeedbackConfig for custom style.'\n ),\n /** Question ordering strategy */\n ordering: OrderingStrategyZ.optional().describe(\n 'Controls the display order of items. Omit for static (config) order, \"priority\" to sort by priority field, or a segment object for per-segment ordering.'\n ),\n /** Dynamic FAQ injection rules */\n injections: z\n .array(InjectionRuleZ)\n .optional()\n .describe(\n 'Optional injection rules that dynamically add FAQ items when trigger conditions are met (e.g. inject checkout FAQs on the checkout page).'\n ),\n })\n .describe(\n 'Props for the adaptive-faq:accordion tile widget. Configures the FAQ accordion including items, expand behavior, search, feedback, and ordering.'\n );\n\nexport type FAQConfig = z.infer<typeof configSchema>;\n\n// ============================================================================\n// Executor Action Schemas\n// ============================================================================\n\n/**\n * Schema for scrolling to a specific FAQ item.\n * Requires either itemId or itemQuestion to identify the target.\n */\nexport const ScrollToFaqSchema = z\n .object({\n ...AuthoringFieldsZ,\n kind: z\n .literal('faq:scroll_to')\n .describe('Scrolls the viewport to a specific FAQ item and optionally expands it.'),\n itemId: z\n .string()\n .optional()\n .describe('Target question ID. Use this or itemQuestion (at least one required).'),\n itemQuestion: z\n .string()\n .optional()\n .describe(\n 'Target question text for fuzzy matching. Use this or itemId (at least one required).'\n ),\n expand: z\n .boolean()\n .optional()\n .describe('When true (default), expands the item after scrolling into view.'),\n behavior: z\n .enum(['smooth', 'instant', 'auto'])\n .optional()\n .describe('\"smooth\" animates the scroll; \"instant\" jumps; \"auto\" defers to browser default.'),\n })\n .refine((data) => data.itemId || data.itemQuestion, {\n message: 'Either itemId or itemQuestion is required',\n });\n\n/**\n * Schema for toggling a FAQ item open/closed.\n * Requires either itemId or itemQuestion to identify the target.\n */\nexport const ToggleFaqItemSchema = z\n .object({\n ...AuthoringFieldsZ,\n kind: z\n .literal('faq:toggle_item')\n .describe('Opens, closes, or toggles the expanded state of a specific FAQ item.'),\n itemId: z\n .string()\n .optional()\n .describe('Target question ID. Use this or itemQuestion (at least one required).'),\n itemQuestion: z\n .string()\n .optional()\n .describe(\n 'Target question text for fuzzy matching. Use this or itemId (at least one required).'\n ),\n state: z\n .enum(['open', 'closed', 'toggle'])\n .default('toggle')\n .describe('\"open\" expands, \"closed\" collapses, \"toggle\" flips the current state.'),\n })\n .refine((data) => data.itemId || data.itemQuestion, {\n message: 'Either itemId or itemQuestion is required',\n });\n\n/**\n * Schema for updating FAQ items (add, remove, reorder, replace).\n *\n * Per-operation field requirements are enforced via .superRefine() so the\n * validator rejects shapes that would silently no-op or destructively clear\n * the list at the executor level (e.g. `operation: 'reorder'` with no\n * `order` array would have wiped all items).\n */\nexport const UpdateFaqSchema = z\n .object({\n ...AuthoringFieldsZ,\n kind: z\n .literal('faq:update')\n .describe('Dynamically adds, removes, reorders, or replaces FAQ items at runtime.'),\n operation: z\n .enum(['add', 'remove', 'reorder', 'replace'])\n .describe(\n '\"add\" inserts items, \"remove\" deletes one by ID, \"reorder\" applies a new ID order, \"replace\" swaps all items.'\n ),\n items: z\n .array(FAQQuestionSchema)\n .optional()\n .describe(\n 'Items to act on. Required for \"add\" (the items to insert) and \"replace\" (the full replacement set; pass [] to intentionally clear all items).'\n ),\n itemId: z.string().optional().describe('ID of the item to remove. Required for \"remove\".'),\n order: z\n .array(z.string())\n .optional()\n .describe(\n 'New display order as an ordered list of item IDs. Required for \"reorder\". To intentionally clear the list, use operation \"replace\" with items: [].'\n ),\n position: z\n .enum(['prepend', 'append', 'before', 'after'])\n .optional()\n .describe('Where to insert new items relative to existing ones or relative to anchorId.'),\n anchorId: AnchorIdZ.optional().describe(\n 'Reference item for \"before\" or \"after\" positioning. Identifies an existing FAQ item as the insertion anchor.'\n ),\n })\n .describe(\n 'Runtime mutation action for the FAQ item list. Use to add contextual questions, remove outdated ones, or reorder by priority.'\n )\n .superRefine((data, ctx) => {\n switch (data.operation) {\n case 'add':\n if (data.items === undefined) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['items'],\n message: 'items is required when operation is \"add\"',\n });\n }\n break;\n case 'remove':\n if (data.itemId === undefined) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['itemId'],\n message: 'itemId is required when operation is \"remove\"',\n });\n }\n break;\n case 'reorder':\n if (data.order === undefined) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['order'],\n message:\n 'order is required when operation is \"reorder\" (use operation \"replace\" with items: [] to intentionally clear the list)',\n });\n }\n break;\n case 'replace':\n if (data.items === undefined) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['items'],\n message: 'items is required when operation is \"replace\" (pass [] to clear the list)',\n });\n }\n break;\n }\n });\n\n// ============================================================================\n// Validation Helpers\n// ============================================================================\n\n/**\n * Validate a FAQ question action.\n */\nexport function validateFAQQuestion(data: unknown) {\n return FAQQuestionSchema.safeParse(data);\n}\n\n/**\n * Validate the full FAQ config.\n */\nexport function validateFAQConfig(data: unknown) {\n return configSchema.safeParse(data);\n}\n\n// ============================================================================\n// Unified Schema Export\n// ============================================================================\n\n/**\n * Action step schemas for unified JSON Schema generation.\n * The build script reads this array to merge adaptive actions into the\n * unified canvas-config.schema.json.\n *\n * Note: `.innerType()` strips the `.refine()` wrapper since JSON Schema\n * cannot express cross-field refinements; runtime Zod handles that.\n */\nexport const actionStepSchemas = [\n { defName: 'faqQuestion', schema: FAQQuestionSchema.innerType() },\n { defName: 'faqScrollTo', schema: ScrollToFaqSchema.innerType() },\n { defName: 'faqToggleItem', schema: ToggleFaqItemSchema.innerType() },\n { defName: 'faqUpdate', schema: UpdateFaqSchema.innerType() },\n];\n\n// ============================================================================\n// Tile Widget Props Export\n// ============================================================================\n\n/**\n * Tile widget definitions for unified JSON Schema generation.\n * Maps widget IDs to their props validation schema so the build script\n * can inject if/then constraints on tile.props.\n */\nexport const tileWidgets = [\n { widget: 'adaptive-faq:accordion', defName: 'faqAccordionProps', propsSchema: configSchema },\n];\n\n// ============================================================================\n// Capabilities Documentation (injected into JSON Schema for LLM prompts)\n// ============================================================================\n\nexport const CAPABILITIES_DOCUMENTATION = {\n packageId: 'adaptive-faq',\n description:\n 'Collapsible Q&A accordion with actions, rich content, feedback, and personalization.',\n whenToUse: [\n {\n goal: 'Add an FAQ accordion widget',\n action: 'Add a tile with widget: \"adaptive-faq:accordion\"',\n },\n { goal: 'Scroll to and expand a specific FAQ item', action: 'faq:scroll_to' },\n { goal: 'Open, close, or toggle a FAQ item', action: 'faq:toggle_item' },\n { goal: 'Add, remove, reorder, or replace FAQ items', action: 'faq:update' },\n ],\n conventions: [\n {\n name: 'Compositional action pattern',\n description:\n 'faq:question actions are configuration data rendered by the accordion widget, not actions executed by the runtime. This allows per-item conditional visibility via triggerWhen, category grouping under collapsible section headers, and dynamic injection of contextual items.',\n },\n {\n name: 'Rich answer content formats',\n description:\n 'FAQ answers support three formats: plain string (simple text with basic markdown), rich HTML ({ \"type\": \"rich\", \"html\": \"<p>...</p>\" } for pre-rendered content), or enhanced markdown ({ \"type\": \"markdown\", \"content\": \"...\", \"assets\": [...] } with embedded media via asset:<id> URIs).',\n },\n {\n name: 'Companion overlay tooltips',\n description:\n 'FAQ questions can be surfaced proactively using overlays:tooltip with ctaButtons. Set the CTA actionId to \"faq:open:<questionId>\" \u2014 the FAQ widget listens for action.tooltip_cta_clicked events with this pattern and expands the matching question. Late-mount support: if the widget mounts after the click, it checks EventBus history (within 10 seconds) and auto-expands.',\n },\n {\n name: 'Feedback events',\n description:\n 'When feedback is enabled, user feedback events are published via context.publishEvent for analytics integration.',\n },\n ],\n events: [\n {\n name: 'faq:feedback_submitted',\n when: 'User submits feedback on a FAQ answer',\n props: '{ questionId, style, value }',\n },\n ],\n};\n"],
5
+ "mappings": "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",
6
+ "names": ["z"]
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syntrologie/adapt-faq",
3
- "version": "2.15.0",
3
+ "version": "2.17.0",
4
4
  "description": "Adaptive FAQ - Collapsible Q&A accordion with per-item conditional visibility",
5
5
  "license": "Proprietary",
6
6
  "private": false,
@@ -19,10 +19,6 @@
19
19
  "types": "./dist/runtime.d.ts",
20
20
  "import": "./dist/runtime.js"
21
21
  },
22
- "./runtime-lit": {
23
- "types": "./src/runtime-lit.ts",
24
- "import": "./src/runtime-lit.ts"
25
- },
26
22
  "./schema": {
27
23
  "types": "./dist/schema.d.ts",
28
24
  "import": "./dist/schema.js"
@@ -30,10 +26,6 @@
30
26
  "./editor": {
31
27
  "types": "./dist/editor.d.ts",
32
28
  "import": "./dist/editor.js"
33
- },
34
- "./editor-lit": {
35
- "types": "./src/editor-lit.ts",
36
- "import": "./src/editor-lit.ts"
37
29
  }
38
30
  },
39
31
  "files": [
@@ -47,35 +39,27 @@
47
39
  ],
48
40
  "scripts": {
49
41
  "prepack": "node ../../../scripts/prepare-bundled-deps.mjs",
50
- "build": "tsc",
42
+ "build": "tsc --emitDeclarationOnly && node ../scripts/build-lib.mjs",
51
43
  "typecheck": "tsc --noEmit",
52
44
  "clean": "rm -rf dist",
53
45
  "test": "vitest run",
54
46
  "test:watch": "vitest"
55
47
  },
56
48
  "peerDependencies": {
57
- "react": ">=18.0.0",
58
- "react-dom": ">=18.0.0",
49
+ "lit": "^3.0.0",
59
50
  "zod": "^3.0.0"
60
51
  },
61
52
  "dependencies": {
62
- "@syntro/design-system": "*",
63
- "@syntrologie/sdk-contracts": "*",
64
- "@syntrologie/shared-editor-ui": "*",
65
53
  "marked": "17.0.4"
66
54
  },
67
55
  "devDependencies": {
68
56
  "@open-wc/testing": "4.0.0",
69
57
  "@open-wc/testing-helpers": "3.0.1",
70
58
  "@syntro/design-system": "1.0.0",
59
+ "@syntrologie/sdk-contracts": "*",
71
60
  "@syntrologie/shared-editor-ui": "*",
72
- "@testing-library/react": "16.3.2",
73
- "@types/react": "19.2.14",
74
- "@types/react-dom": "19.2.3",
75
61
  "jsdom": "26.1.0",
76
62
  "lit": "3.3.2",
77
- "react": "19.2.1",
78
- "react-dom": "19.2.1",
79
63
  "typescript": "5.9.3",
80
64
  "vitest": "4.0.18",
81
65
  "zod": "3.25.76"
@@ -1,33 +0,0 @@
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 triggerWhen 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, FAQWidgetRuntime } from './faq-types';
11
- import type { FAQConfig } from './types';
12
- export type { FAQWidgetRuntime } from './faq-types';
13
- /**
14
- * FAQWidget - Renders a collapsible Q&A accordion with per-item activation.
15
- *
16
- * This component demonstrates the compositional action pattern:
17
- * - Parent (FAQWidget) receives `config.actions` array
18
- * - Each action has optional `triggerWhen` for per-item visibility
19
- * - Parent evaluates triggerWhen and filters visible questions
20
- * - Parent manages expand state and re-rendering on context changes
21
- */
22
- export declare function FAQWidget({ config, runtime, instanceId }: FAQWidgetProps): import("react/jsx-runtime").JSX.Element;
23
- /**
24
- * Mountable widget interface for the runtime's WidgetRegistry.
25
- */
26
- export declare const FAQMountableWidget: {
27
- mount(container: HTMLElement, config?: FAQConfig & {
28
- runtime?: FAQWidgetRuntime;
29
- instanceId?: string;
30
- }): (() => void) | undefined;
31
- };
32
- export default FAQWidget;
33
- //# sourceMappingURL=FAQWidget.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"FAQWidget.d.ts","sourceRoot":"","sources":["../src/FAQWidget.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAUH,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,KAAK,EAEV,SAAS,EAIV,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA2KpD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,cAAc,2CA8VxE;AAMD;;GAEG;AACH,eAAO,MAAM,kBAAkB;qBAEhB,WAAW,WACb,SAAS,GAAG;QAAE,OAAO,CAAC,EAAE,gBAAgB,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;CA4B3E,CAAC;AAEF,eAAe,SAAS,CAAC"}
package/dist/FAQWidget.js DELETED
@@ -1,375 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /**
3
- * Adaptive FAQ - FAQWidget Component
4
- *
5
- * React component that renders a collapsible Q&A accordion with per-item
6
- * conditional visibility based on triggerWhen decision strategies.
7
- *
8
- * Demonstrates the compositional action pattern where child actions
9
- * (faq:question) serve as configuration data for the parent widget.
10
- */
11
- import { purple } from '@syntro/design-system/tokens';
12
- import { Marked } from 'marked';
13
- import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
14
- import { createRoot } from 'react-dom/client';
15
- const marked = new Marked({ async: false, gfm: true, breaks: true });
16
- import { baseStyles, themeStyles } from './faq-styles';
17
- // ============================================================================
18
- // Helpers
19
- // ============================================================================
20
- /** Extract plain text from an FAQAnswer for search matching */
21
- function getAnswerText(answer) {
22
- if (typeof answer === 'string')
23
- return answer;
24
- if (answer.type === 'rich')
25
- return answer.html;
26
- return answer.content;
27
- }
28
- /** Render an FAQAnswer based on its type */
29
- function renderAnswer(answer) {
30
- if (typeof answer === 'string') {
31
- const html = marked.parse(answer);
32
- return (
33
- // biome-ignore lint/security/noDangerouslySetInnerHtml: content is CMS/config content, not user-controlled input
34
- _jsx("div", { style: { margin: 0 }, "data-faq-markdown": "", dangerouslySetInnerHTML: { __html: html } }));
35
- }
36
- if (answer.type === 'rich') {
37
- // biome-ignore lint/security/noDangerouslySetInnerHtml: content is pre-sanitized by backend — FAQAnswer.html is CMS/config content, not user-controlled input
38
- return _jsx("div", { style: { margin: 0 }, dangerouslySetInnerHTML: { __html: answer.html } });
39
- }
40
- // markdown — parse to HTML and render
41
- const html = marked.parse(answer.content);
42
- return (
43
- // biome-ignore lint/security/noDangerouslySetInnerHtml: markdown content is CMS/config content, not user-controlled input
44
- _jsx("div", { style: { margin: 0 }, "data-faq-markdown": "", dangerouslySetInnerHTML: { __html: html } }));
45
- }
46
- /** Resolve feedback config into a normalized FeedbackConfig or null */
47
- function resolveFeedbackConfig(feedback) {
48
- if (!feedback)
49
- return null;
50
- if (feedback === true) {
51
- return { style: 'thumbs' };
52
- }
53
- return feedback;
54
- }
55
- /** Get the feedback prompt text */
56
- function getFeedbackPrompt(feedbackConfig) {
57
- return feedbackConfig.prompt || 'Was this helpful?';
58
- }
59
- function FAQItem({ item, isExpanded, isHighlighted, isLast, onToggle, theme, feedbackConfig, feedbackValue, onFeedback, }) {
60
- const [isHovered, setIsHovered] = useState(false);
61
- const colors = themeStyles[theme];
62
- const { question, answer } = item.config;
63
- const itemStyle = {
64
- ...baseStyles.item,
65
- ...colors.item,
66
- ...(isExpanded ? colors.itemExpanded : {}),
67
- ...(isHighlighted
68
- ? {
69
- // purple[4] = #6a59ce — design system primary purple
70
- boxShadow: `0 0 0 2px ${purple[4]}, 0 0 12px rgba(106, 89, 206, 0.4)`,
71
- transition: 'box-shadow 0.3s ease',
72
- }
73
- : {}),
74
- ...(!isLast ? { borderBottom: 'var(--sc-content-item-divider, none)' } : {}),
75
- };
76
- const questionStyle = {
77
- ...baseStyles.question,
78
- ...colors.question,
79
- ...(isHovered ? colors.questionHover : {}),
80
- };
81
- const chevronStyle = {
82
- ...baseStyles.chevron,
83
- transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
84
- };
85
- const answerStyle = {
86
- ...baseStyles.answer,
87
- ...colors.answer,
88
- maxHeight: isExpanded ? '500px' : '0',
89
- paddingBottom: isExpanded ? '16px' : '0',
90
- };
91
- const feedbackStyle = {
92
- ...baseStyles.feedback,
93
- ...colors.feedbackPrompt,
94
- };
95
- return (_jsxs("div", { style: itemStyle, "data-faq-item-id": item.config.id, children: [_jsxs("button", { type: "button", style: questionStyle, onClick: onToggle, onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), "aria-expanded": isExpanded, children: [_jsx("span", { children: question }), _jsx("span", { style: chevronStyle, children: '\u203A' })] }), _jsxs("div", { style: answerStyle, "aria-hidden": !isExpanded, children: [renderAnswer(answer), isExpanded && feedbackConfig && (_jsxs("div", { style: feedbackStyle, children: [_jsx("span", { children: getFeedbackPrompt(feedbackConfig) }), _jsx("button", { type: "button", style: {
96
- ...baseStyles.feedbackButton,
97
- ...(feedbackValue === 'up' ? baseStyles.feedbackButtonSelected : {}),
98
- }, "aria-label": "Thumbs up", onClick: () => onFeedback(item.config.id, question, 'up'), children: '\uD83D\uDC4D' }), _jsx("button", { type: "button", style: {
99
- ...baseStyles.feedbackButton,
100
- ...(feedbackValue === 'down' ? baseStyles.feedbackButtonSelected : {}),
101
- }, "aria-label": "Thumbs down", onClick: () => onFeedback(item.config.id, question, 'down'), children: '\uD83D\uDC4E' })] }))] })] }));
102
- }
103
- // ============================================================================
104
- // FAQWidget Component
105
- // ============================================================================
106
- /**
107
- * FAQWidget - Renders a collapsible Q&A accordion with per-item activation.
108
- *
109
- * This component demonstrates the compositional action pattern:
110
- * - Parent (FAQWidget) receives `config.actions` array
111
- * - Each action has optional `triggerWhen` for per-item visibility
112
- * - Parent evaluates triggerWhen and filters visible questions
113
- * - Parent manages expand state and re-rendering on context changes
114
- */
115
- export function FAQWidget({ config, runtime, instanceId }) {
116
- // Force re-render when context/accumulator changes.
117
- // renderTick is used as a useMemo dependency to invalidate cached triggerWhen evaluations.
118
- const [renderTick, forceUpdate] = useReducer((x) => x + 1, 0);
119
- // Track expanded question IDs
120
- const [expandedIds, setExpandedIds] = useState(new Set());
121
- // Track which item is flash-highlighted from a deep-link
122
- const [highlightId, setHighlightId] = useState(null);
123
- // Search query state
124
- const [searchQuery, setSearchQuery] = useState('');
125
- // Track feedback state per item
126
- const [feedbackState, setFeedbackState] = useState(new Map());
127
- // Resolve feedback config
128
- const feedbackConfig = useMemo(() => resolveFeedbackConfig(config.feedback), [config.feedback]);
129
- // Subscribe to context changes for reactive updates
130
- useEffect(() => {
131
- const unsubscribe = runtime.context.subscribe(() => {
132
- forceUpdate();
133
- });
134
- return unsubscribe;
135
- }, [runtime.context]);
136
- // Subscribe to accumulator changes for event_count-based triggerWhen
137
- useEffect(() => {
138
- if (!runtime.accumulator?.subscribe)
139
- return;
140
- return runtime.accumulator.subscribe(() => {
141
- forceUpdate();
142
- });
143
- }, [runtime.accumulator]);
144
- // Subscribe to faq:open:* events from overlay CTA clicks
145
- useEffect(() => {
146
- if (!runtime.events.subscribe)
147
- return;
148
- // Check EventBus history for pending faq:open events
149
- // (may have fired before this widget mounted, e.g. when canvas was closed)
150
- if (runtime.events.getRecent) {
151
- const recentEvents = runtime.events.getRecent({ patterns: ['^action\\.tooltip_cta_clicked$', '^action\\.modal_cta_clicked$'] }, 10);
152
- const pendingEvent = recentEvents
153
- .filter((e) => {
154
- const actionId = e.props?.actionId;
155
- return typeof actionId === 'string' && actionId.startsWith('faq:open:');
156
- })
157
- .pop(); // Most recent
158
- if (pendingEvent && Date.now() - pendingEvent.ts < 10000) {
159
- const questionId = pendingEvent.props.actionId.replace('faq:open:', '');
160
- setExpandedIds(new Set([questionId]));
161
- // Scroll into view after render
162
- requestAnimationFrame(() => {
163
- const el = document.querySelector(`[data-faq-item-id="${questionId}"]`);
164
- if (el)
165
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
166
- });
167
- }
168
- }
169
- // Subscribe to future CTA events
170
- const unsubscribe = runtime.events.subscribe({ patterns: ['^action\\.tooltip_cta_clicked$', '^action\\.modal_cta_clicked$'] }, (event) => {
171
- const actionId = event.props?.actionId;
172
- if (typeof actionId !== 'string' || !actionId.startsWith('faq:open:'))
173
- return;
174
- const questionId = actionId.replace('faq:open:', '');
175
- setExpandedIds(new Set([questionId]));
176
- // Scroll the question into view
177
- requestAnimationFrame(() => {
178
- const el = document.querySelector(`[data-faq-item-id="${questionId}"]`);
179
- if (el)
180
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
181
- });
182
- // Request canvas open (for future tile-based FAQ rendering)
183
- runtime.events.publish('canvas.requestOpen');
184
- });
185
- return unsubscribe;
186
- }, [runtime]);
187
- // Subscribe to notification.deep_link events (from insertHtml deepLink clicks + notification toasts)
188
- useEffect(() => {
189
- if (!runtime.events.subscribe)
190
- return;
191
- const handleDeepLink = (event) => {
192
- const tileId = event.props?.tileId;
193
- const itemId = event.props?.itemId;
194
- // Only handle if this deep link targets our tile
195
- if (tileId !== instanceId)
196
- return;
197
- if (!itemId)
198
- return;
199
- // Expand the target item
200
- setExpandedIds(new Set([itemId]));
201
- // Flash-highlight the item
202
- setHighlightId(itemId);
203
- setTimeout(() => setHighlightId(null), 1500);
204
- // Scroll into view after render
205
- requestAnimationFrame(() => {
206
- const el = document.querySelector(`[data-faq-item-id="${itemId}"]`);
207
- if (el)
208
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
209
- });
210
- };
211
- // Check recent events (may have fired before widget mounted, e.g. canvas was closed)
212
- if (runtime.events.getRecent) {
213
- const recent = runtime.events.getRecent({ names: ['notification.deep_link'] }, 5);
214
- const pending = recent
215
- .filter((e) => e.props?.tileId === instanceId && e.props?.itemId)
216
- .pop();
217
- if (pending && Date.now() - pending.ts < 10000) {
218
- handleDeepLink(pending);
219
- }
220
- }
221
- // Subscribe to future events
222
- const unsubscribe = runtime.events.subscribe({ names: ['notification.deep_link'] }, handleDeepLink);
223
- return unsubscribe;
224
- }, [runtime, instanceId]);
225
- // Filter visible questions based on per-item triggerWhen
226
- // biome-ignore lint/correctness/useExhaustiveDependencies: renderTick is intentionally included to force re-evaluation when the runtime's mutable context changes (subscribed above via forceUpdate)
227
- const visibleQuestions = useMemo(() => (config.actions ?? []).filter((q) => {
228
- // No triggerWhen = always visible
229
- if (!q.triggerWhen)
230
- return true;
231
- // Evaluate the decision strategy
232
- const result = runtime.evaluateSync(q.triggerWhen);
233
- return result.value;
234
- }), [config.actions, runtime, renderTick]);
235
- // NOTE: faq:question_revealed is now published by useNotifyWatcher in
236
- // ShadowCanvasOverlay (always mounted), so it fires even with drawer closed.
237
- // Apply priority ordering
238
- const orderedQuestions = useMemo(() => {
239
- if (config.ordering === 'priority') {
240
- return [...visibleQuestions].sort((a, b) => (b.config.priority ?? 0) - (a.config.priority ?? 0));
241
- }
242
- // 'static' or undefined — preserve config order
243
- return visibleQuestions;
244
- }, [visibleQuestions, config.ordering]);
245
- // Apply search filter
246
- const filteredQuestions = useMemo(() => {
247
- if (!config.searchable || !searchQuery.trim()) {
248
- return orderedQuestions;
249
- }
250
- const query = searchQuery.toLowerCase();
251
- return orderedQuestions.filter((q) => q.config.question.toLowerCase().includes(query) ||
252
- getAnswerText(q.config.answer).toLowerCase().includes(query) ||
253
- q.config.category?.toLowerCase().includes(query));
254
- }, [orderedQuestions, searchQuery, config.searchable]);
255
- // Group by category
256
- const categoryGroups = useMemo(() => {
257
- const groups = new Map();
258
- for (const q of filteredQuestions) {
259
- const cat = q.config.category;
260
- if (!groups.has(cat)) {
261
- groups.set(cat, []);
262
- }
263
- groups.get(cat).push(q);
264
- }
265
- return groups;
266
- }, [filteredQuestions]);
267
- // Check if any items have categories
268
- const hasCategories = useMemo(() => filteredQuestions.some((q) => q.config.category), [filteredQuestions]);
269
- // Resolve theme (auto -> detect system preference)
270
- const resolvedTheme = useMemo(() => {
271
- if (config.theme && config.theme !== 'auto')
272
- return config.theme;
273
- // Check system preference (SSR-safe)
274
- if (typeof window !== 'undefined') {
275
- return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
276
- }
277
- return 'light';
278
- }, [config.theme]);
279
- // Handle question toggle
280
- const handleToggle = useCallback((id) => {
281
- setExpandedIds((prev) => {
282
- const next = new Set(prev);
283
- if (config.expandBehavior === 'single') {
284
- // Single mode: collapse all others
285
- if (prev.has(id)) {
286
- return new Set();
287
- }
288
- return new Set([id]);
289
- }
290
- // Multiple mode: toggle this one
291
- if (prev.has(id)) {
292
- next.delete(id);
293
- }
294
- else {
295
- next.add(id);
296
- }
297
- return next;
298
- });
299
- // Publish toggle event for analytics
300
- runtime.events.publish('faq:toggled', {
301
- instanceId,
302
- questionId: id,
303
- expanded: !expandedIds.has(id),
304
- timestamp: Date.now(),
305
- });
306
- }, [config.expandBehavior, runtime.events, instanceId, expandedIds]);
307
- // Handle feedback
308
- const handleFeedback = useCallback((itemId, question, value) => {
309
- setFeedbackState((prev) => {
310
- const next = new Map(prev);
311
- next.set(itemId, value);
312
- return next;
313
- });
314
- runtime.events.publish('faq:feedback', {
315
- itemId,
316
- question,
317
- value,
318
- });
319
- }, [runtime.events]);
320
- // Compute styles
321
- const containerStyle = {
322
- ...baseStyles.container,
323
- ...themeStyles[resolvedTheme].container,
324
- };
325
- const searchInputStyle = {
326
- ...baseStyles.searchInput,
327
- ...themeStyles[resolvedTheme].searchInput,
328
- };
329
- const emptyStateStyle = {
330
- ...baseStyles.emptyState,
331
- ...themeStyles[resolvedTheme].emptyState,
332
- };
333
- const categoryHeaderStyle = {
334
- ...baseStyles.categoryHeader,
335
- ...themeStyles[resolvedTheme].categoryHeader,
336
- };
337
- // Render a list of FAQ items
338
- const renderItems = (items) => items.map((q, index) => (_jsx(FAQItem, { item: q, isExpanded: expandedIds.has(q.config.id), isHighlighted: highlightId === q.config.id, isLast: index === items.length - 1, onToggle: () => handleToggle(q.config.id), theme: resolvedTheme, feedbackConfig: feedbackConfig, feedbackValue: feedbackState.get(q.config.id), onFeedback: handleFeedback }, q.config.id)));
339
- // Empty state (no visible questions at all)
340
- if (visibleQuestions.length === 0) {
341
- return (_jsx("div", { style: containerStyle, "data-adaptive-id": instanceId, "data-adaptive-type": "adaptive-faq", children: _jsx("div", { style: emptyStateStyle, children: "You're all set for now! We'll surface answers here when they're relevant to what you're doing." }) }));
342
- }
343
- return (_jsxs("div", { style: containerStyle, "data-adaptive-id": instanceId, "data-adaptive-type": "adaptive-faq", children: [config.searchable && (_jsxs("div", { style: baseStyles.searchWrapper, children: [_jsx("style", { children: `[data-adaptive-id="${instanceId}"] input::placeholder { color: var(--sc-content-search-color, inherit); opacity: 0.7; }` }), _jsx("input", { type: "text", placeholder: "Search questions...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), style: searchInputStyle })] })), _jsx("div", { style: baseStyles.accordion, children: hasCategories
344
- ? Array.from(categoryGroups.entries()).map(([category, items]) => (_jsxs(React.Fragment, { children: [category && (_jsx("div", { style: categoryHeaderStyle, "data-category-header": category, children: category })), renderItems(items)] }, category ?? '__ungrouped')))
345
- : renderItems(filteredQuestions) }), config.searchable && filteredQuestions.length === 0 && searchQuery && (_jsxs("div", { style: { ...baseStyles.noResults, ...themeStyles[resolvedTheme].emptyState }, children: ["No questions found matching \"", searchQuery, "\""] }))] }));
346
- }
347
- // ============================================================================
348
- // Mountable Widget Interface
349
- // ============================================================================
350
- /**
351
- * Mountable widget interface for the runtime's WidgetRegistry.
352
- */
353
- export const FAQMountableWidget = {
354
- mount(container, config) {
355
- const { runtime, instanceId = 'faq-widget', ...faqConfig } = config || {
356
- expandBehavior: 'single',
357
- searchable: false,
358
- theme: 'auto',
359
- actions: [],
360
- };
361
- // React rendering when runtime + ReactDOM are available
362
- if (runtime && typeof createRoot === 'function') {
363
- const root = createRoot(container);
364
- root.render(React.createElement(FAQWidget, {
365
- config: faqConfig,
366
- runtime: runtime,
367
- instanceId,
368
- }));
369
- return () => {
370
- root.unmount();
371
- };
372
- }
373
- },
374
- };
375
- export default FAQWidget;