@syntrologie/adapt-overlays 2.4.1 → 2.5.1

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.
Files changed (118) hide show
  1. package/dist/WorkflowTracker.d.ts +10 -0
  2. package/dist/WorkflowTracker.d.ts.map +1 -0
  3. package/dist/WorkflowTracker.js +19 -0
  4. package/dist/WorkflowWidget.d.ts +70 -0
  5. package/dist/WorkflowWidget.d.ts.map +1 -0
  6. package/dist/WorkflowWidget.js +329 -0
  7. package/dist/cdn.d.ts +2 -2
  8. package/dist/celebrations/__tests__/engine.test.d.ts +2 -0
  9. package/dist/celebrations/__tests__/engine.test.d.ts.map +1 -0
  10. package/dist/celebrations/__tests__/engine.test.js +130 -0
  11. package/dist/celebrations/__tests__/executor.test.d.ts +2 -0
  12. package/dist/celebrations/__tests__/executor.test.d.ts.map +1 -0
  13. package/dist/celebrations/__tests__/executor.test.js +102 -0
  14. package/dist/celebrations/effects/__tests__/confetti.test.d.ts +2 -0
  15. package/dist/celebrations/effects/__tests__/confetti.test.d.ts.map +1 -0
  16. package/dist/celebrations/effects/__tests__/confetti.test.js +89 -0
  17. package/dist/celebrations/effects/__tests__/emoji-rain.test.d.ts +2 -0
  18. package/dist/celebrations/effects/__tests__/emoji-rain.test.d.ts.map +1 -0
  19. package/dist/celebrations/effects/__tests__/emoji-rain.test.js +88 -0
  20. package/dist/celebrations/effects/__tests__/fireworks.test.d.ts +2 -0
  21. package/dist/celebrations/effects/__tests__/fireworks.test.d.ts.map +1 -0
  22. package/dist/celebrations/effects/__tests__/fireworks.test.js +87 -0
  23. package/dist/celebrations/effects/__tests__/sparkles.test.d.ts +2 -0
  24. package/dist/celebrations/effects/__tests__/sparkles.test.d.ts.map +1 -0
  25. package/dist/celebrations/effects/__tests__/sparkles.test.js +79 -0
  26. package/dist/celebrations/effects/confetti.d.ts +3 -0
  27. package/dist/celebrations/effects/confetti.d.ts.map +1 -0
  28. package/dist/celebrations/effects/confetti.js +80 -0
  29. package/dist/celebrations/effects/emoji-rain.d.ts +3 -0
  30. package/dist/celebrations/effects/emoji-rain.d.ts.map +1 -0
  31. package/dist/celebrations/effects/emoji-rain.js +73 -0
  32. package/dist/celebrations/effects/fireworks.d.ts +3 -0
  33. package/dist/celebrations/effects/fireworks.d.ts.map +1 -0
  34. package/dist/celebrations/effects/fireworks.js +69 -0
  35. package/dist/celebrations/effects/sparkles.d.ts +3 -0
  36. package/dist/celebrations/effects/sparkles.d.ts.map +1 -0
  37. package/dist/celebrations/effects/sparkles.js +83 -0
  38. package/dist/celebrations/engine.d.ts +16 -0
  39. package/dist/celebrations/engine.d.ts.map +1 -0
  40. package/dist/celebrations/engine.js +89 -0
  41. package/dist/celebrations/index.d.ts +3 -0
  42. package/dist/celebrations/index.d.ts.map +1 -0
  43. package/dist/celebrations/index.js +73 -0
  44. package/dist/celebrations/types.d.ts +34 -0
  45. package/dist/celebrations/types.d.ts.map +1 -0
  46. package/dist/celebrations/types.js +1 -0
  47. package/dist/editor.d.ts.map +1 -1
  48. package/dist/editor.js +59 -5
  49. package/dist/executors/tour.d.ts +20 -0
  50. package/dist/executors/tour.d.ts.map +1 -0
  51. package/dist/executors/tour.js +335 -0
  52. package/dist/modal.d.ts +2 -0
  53. package/dist/modal.d.ts.map +1 -1
  54. package/dist/modal.js +18 -8
  55. package/dist/runtime.d.ts +25 -2
  56. package/dist/runtime.d.ts.map +1 -1
  57. package/dist/runtime.js +141 -24
  58. package/dist/schema.d.ts +684 -4
  59. package/dist/schema.d.ts.map +1 -1
  60. package/dist/schema.js +36 -0
  61. package/dist/summarize.d.ts.map +1 -1
  62. package/dist/summarize.js +15 -4
  63. package/dist/tooltip.d.ts.map +1 -1
  64. package/dist/tooltip.js +26 -12
  65. package/dist/tour-types.d.ts +34 -0
  66. package/dist/tour-types.d.ts.map +1 -0
  67. package/dist/tour-types.js +7 -0
  68. package/dist/types.d.ts +20 -85
  69. package/dist/types.d.ts.map +1 -1
  70. package/dist/workflow-types.d.ts +15 -0
  71. package/dist/workflow-types.d.ts.map +1 -0
  72. package/dist/workflow-types.js +1 -0
  73. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.d.ts +2 -0
  74. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.d.ts.map +1 -0
  75. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.js +224 -0
  76. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.js +102 -0
  77. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DetectionBadge.test.js +58 -6
  78. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DismissedSection.test.js +18 -0
  79. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorCard.test.js +61 -2
  80. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorPanelShell.test.js +478 -7
  81. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ElementHighlight.test.js +54 -0
  82. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.d.ts +2 -0
  83. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.d.ts.map +1 -0
  84. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.js +257 -0
  85. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.d.ts +2 -0
  86. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.d.ts.map +1 -0
  87. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.js +1015 -0
  88. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.js +1 -1
  89. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts +4 -4
  90. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts.map +1 -1
  91. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.js +2 -2
  92. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts +2 -1
  93. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts.map +1 -1
  94. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.js +20 -3
  95. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts +10 -8
  96. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts.map +1 -1
  97. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.js +350 -87
  98. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.js +1 -1
  99. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts +3 -3
  100. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts.map +1 -1
  101. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.js +1 -1
  102. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts +1 -1
  103. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts.map +1 -1
  104. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.js +5 -2
  105. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts +24 -0
  106. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts.map +1 -0
  107. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/{useShowWhenStatus.js → useTriggerWhenStatus.js} +18 -15
  108. package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts +3 -3
  109. package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts.map +1 -1
  110. package/node_modules/@syntrologie/shared-editor-ui/dist/index.js +1 -1
  111. package/package.json +3 -2
  112. package/node_modules/@syntrologie/sdk-contracts/dist/index.d.ts +0 -26
  113. package/node_modules/@syntrologie/sdk-contracts/dist/index.js +0 -13
  114. package/node_modules/@syntrologie/sdk-contracts/dist/schemas.d.ts +0 -1428
  115. package/node_modules/@syntrologie/sdk-contracts/dist/schemas.js +0 -142
  116. package/node_modules/@syntrologie/sdk-contracts/package.json +0 -33
  117. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useShowWhenStatus.d.ts +0 -24
  118. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useShowWhenStatus.d.ts.map +0 -1
@@ -0,0 +1,10 @@
1
+ import type { WorkflowEntry } from './workflow-types';
2
+ interface WorkflowTrackerProps {
3
+ workflows: WorkflowEntry[];
4
+ expanded?: boolean;
5
+ onStepClick: (tourId: string, stepId: string) => void;
6
+ onDismiss: (tourId: string) => void;
7
+ }
8
+ export declare function WorkflowTracker({ workflows, expanded, onStepClick, onDismiss, }: WorkflowTrackerProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=WorkflowTracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkflowTracker.d.ts","sourceRoot":"","sources":["../src/WorkflowTracker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,UAAU,oBAAoB;IAC5B,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAkID,wBAAgB,eAAe,CAAC,EAC9B,SAAS,EACT,QAAQ,EACR,WAAW,EACX,SAAS,GACV,EAAE,oBAAoB,2CAsBtB"}
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ function ProgressBar({ completed, total }) {
3
+ const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
4
+ return (_jsx("div", { className: "se-w-full se-h-1.5 se-rounded-full se-bg-white/[0.08] se-overflow-hidden", children: _jsx("div", { role: "progressbar", "aria-valuenow": percent, "aria-valuemin": 0, "aria-valuemax": 100, className: "se-h-full se-rounded-full se-bg-blue-5 se-transition-all se-duration-300", style: { width: `${percent}%` } }) }));
5
+ }
6
+ function StepItem({ step, isCompleted, isCurrent, tourId, onStepClick, }) {
7
+ return (_jsxs("button", { type: "button", "data-testid": `step-${step.id}`, "data-current": isCurrent ? 'true' : undefined, "data-completed": isCompleted ? 'true' : undefined, "aria-current": isCurrent ? 'step' : undefined, className: `se-flex se-items-center se-gap-2 se-w-full se-py-1.5 se-px-2 se-rounded se-border-none se-bg-transparent se-cursor-pointer se-text-left se-text-xs ${isCurrent ? 'se-font-semibold se-text-slate-grey-10' : 'se-text-slate-grey-8'}`, onClick: () => onStepClick(tourId, step.id), children: [_jsx("span", { className: "se-shrink-0 se-w-4 se-text-center", children: isCompleted ? (_jsx("span", { role: "img", "aria-label": "completed", className: "se-text-green-5", children: "\u2713" })) : isCurrent ? (_jsx("span", { className: "se-inline-block se-w-1.5 se-h-1.5 se-rounded-full se-bg-blue-5" })) : (_jsx("span", { className: "se-inline-block se-w-1.5 se-h-1.5 se-rounded-full se-bg-white/[0.12]" })) }), _jsx("span", { className: "se-flex-1 se-overflow-hidden se-text-ellipsis se-whitespace-nowrap", children: step.title })] }));
8
+ }
9
+ function WorkflowCard({ workflow, expanded, onStepClick, onDismiss, }) {
10
+ const completedCount = workflow.completedSteps.length;
11
+ const totalSteps = workflow.steps.length;
12
+ return (_jsxs("div", { className: "se-p-3 se-rounded-lg se-border se-border-white/[0.08] se-bg-white/[0.02]", children: [_jsxs("div", { className: "se-flex se-items-center se-gap-2 se-mb-2", children: [workflow.meta.icon && (_jsx("span", { "data-testid": "workflow-icon", className: "se-shrink-0 se-text-sm", children: workflow.meta.icon })), _jsx("span", { className: "se-flex-1 se-text-[13px] se-font-semibold se-text-slate-grey-10 se-overflow-hidden se-text-ellipsis se-whitespace-nowrap", children: workflow.meta.title }), _jsx("button", { type: "button", "data-testid": `dismiss-${workflow.tourId}`, className: "se-shrink-0 se-py-0.5 se-px-1.5 se-rounded se-border-none se-bg-transparent se-text-slate-grey-7 se-text-xs se-cursor-pointer se-leading-none", onClick: () => onDismiss(workflow.tourId), "aria-label": `Dismiss ${workflow.meta.title}`, children: "\u2715" })] }), _jsxs("div", { className: "se-mb-2", children: [_jsx(ProgressBar, { completed: completedCount, total: totalSteps }), _jsxs("div", { className: "se-text-[10px] se-text-slate-grey-7 se-mt-1", children: [completedCount, " of ", totalSteps, " steps"] })] }), _jsx("div", { className: "se-flex se-flex-col", children: workflow.steps.map((step) => (_jsx(StepItem, { step: step, isCompleted: workflow.completedSteps.includes(step.id), isCurrent: workflow.currentStepId === step.id, tourId: workflow.tourId, onStepClick: onStepClick }, step.id))) }), expanded && workflow.meta.description && (_jsx("div", { className: "se-mt-2 se-text-[11px] se-text-slate-grey-7", children: workflow.meta.description }))] }));
13
+ }
14
+ export function WorkflowTracker({ workflows, expanded, onStepClick, onDismiss, }) {
15
+ if (workflows.length === 0) {
16
+ return (_jsx("div", { className: "se-flex se-items-center se-justify-center se-py-6 se-text-xs se-text-slate-grey-7", children: "No active workflows" }));
17
+ }
18
+ return (_jsx("div", { className: "se-flex se-flex-col se-gap-2", children: workflows.map((workflow) => (_jsx(WorkflowCard, { workflow: workflow, expanded: expanded, onStepClick: onStepClick, onDismiss: onDismiss }, workflow.tourId))) }));
19
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Workflow Widget - MountableWidget for the WorkflowTracker
3
+ *
4
+ * Bridges runtime.actions.getActive() (tour actions with workflow metadata),
5
+ * runtime events (tour.started, tour.step_started, etc.), and the
6
+ * WorkflowTracker presentational component.
7
+ *
8
+ * Responsibilities:
9
+ * 1. Scan runtime.actions.getActive() for tours with `workflow` field
10
+ * 2. Re-scan on tour.started events (tiles render before actions are applied)
11
+ * 3. Load persisted state from runtime.state.user namespace
12
+ * 4. Subscribe to tour.* events and update workflow entries
13
+ * 5. Render WorkflowTracker with derived workflow entries
14
+ * 6. Handle step clicks (publish workflow:jump_to_step)
15
+ * 7. Handle dismiss (persist and re-render)
16
+ */
17
+ interface ActiveAction {
18
+ id: string;
19
+ action: Record<string, unknown>;
20
+ adaptiveId?: string;
21
+ appliedTs: number;
22
+ state: string;
23
+ }
24
+ interface WorkflowRuntime {
25
+ events: {
26
+ subscribe: (filter: {
27
+ patterns?: string[];
28
+ names?: string[];
29
+ }, callback: (event: {
30
+ name: string;
31
+ props?: Record<string, unknown>;
32
+ ts: number;
33
+ }) => void) => () => void;
34
+ publish: (name: string, props?: Record<string, unknown>) => void;
35
+ };
36
+ actions: {
37
+ getActive: () => ActiveAction[];
38
+ };
39
+ state?: {
40
+ user?: {
41
+ ns?: (namespace: string) => {
42
+ get: (key: string) => unknown;
43
+ set: (key: string, value: unknown) => void;
44
+ };
45
+ };
46
+ };
47
+ }
48
+ /**
49
+ * MountableWidget contract — matches the interface from @syntrologie/runtime-sdk
50
+ * Defined locally to avoid adding runtime-sdk as a dependency (same pattern as other adaptives).
51
+ */
52
+ export interface MountableWidget {
53
+ mount(container: HTMLElement, config?: Record<string, unknown>): (() => void) | void;
54
+ update?(container: HTMLElement, config?: Record<string, unknown>): void;
55
+ }
56
+ interface WorkflowWidgetInnerProps {
57
+ runtime: WorkflowRuntime | null;
58
+ }
59
+ export declare function WorkflowWidgetInner({ runtime }: WorkflowWidgetInnerProps): import("react/jsx-runtime").JSX.Element;
60
+ /**
61
+ * Mountable widget interface for the runtime's WidgetRegistry.
62
+ *
63
+ * Follows the MountableWidget contract:
64
+ * mount(container, config?) -> cleanup | void
65
+ *
66
+ * Config is enriched with `runtime` by WidgetRegistry.bindRuntime().
67
+ */
68
+ export declare const WorkflowMountableWidget: MountableWidget;
69
+ export {};
70
+ //# sourceMappingURL=WorkflowWidget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkflowWidget.d.ts","sourceRoot":"","sources":["../src/WorkflowWidget.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAYH,UAAU,YAAY;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,eAAe;IACvB,MAAM,EAAE;QACN,SAAS,EAAE,CACT,MAAM,EAAE;YAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;SAAE,EACjD,QAAQ,EAAE,CAAC,KAAK,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAAC,EAAE,EAAE,MAAM,CAAA;SAAE,KAAK,IAAI,KACrF,MAAM,IAAI,CAAC;QAChB,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;KAClE,CAAC;IACF,OAAO,EAAE;QACP,SAAS,EAAE,MAAM,YAAY,EAAE,CAAC;KACjC,CAAC;IACF,KAAK,CAAC,EAAE;QACN,IAAI,CAAC,EAAE;YACL,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK;gBAC1B,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;gBAC9B,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;aAC5C,CAAC;SACH,CAAC;KACH,CAAC;CACH;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,SAAS,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;IACrF,MAAM,CAAC,CAAC,SAAS,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACzE;AAsGD,UAAU,wBAAwB;IAChC,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;CACjC;AAED,wBAAgB,mBAAmB,CAAC,EAAE,OAAO,EAAE,EAAE,wBAAwB,2CA0QxE;AAMD;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,EAAE,eAiBrC,CAAC"}
@@ -0,0 +1,329 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Workflow Widget - MountableWidget for the WorkflowTracker
4
+ *
5
+ * Bridges runtime.actions.getActive() (tour actions with workflow metadata),
6
+ * runtime events (tour.started, tour.step_started, etc.), and the
7
+ * WorkflowTracker presentational component.
8
+ *
9
+ * Responsibilities:
10
+ * 1. Scan runtime.actions.getActive() for tours with `workflow` field
11
+ * 2. Re-scan on tour.started events (tiles render before actions are applied)
12
+ * 3. Load persisted state from runtime.state.user namespace
13
+ * 4. Subscribe to tour.* events and update workflow entries
14
+ * 5. Render WorkflowTracker with derived workflow entries
15
+ * 6. Handle step clicks (publish workflow:jump_to_step)
16
+ * 7. Handle dismiss (persist and re-render)
17
+ */
18
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
19
+ import { createRoot } from 'react-dom/client';
20
+ import { WorkflowTracker } from './WorkflowTracker';
21
+ // ============================================================================
22
+ // Helpers
23
+ // ============================================================================
24
+ /**
25
+ * Show a toast notification for a workflow tour.
26
+ * Creates a DOM element, appends it to the container, and auto-removes after 4 seconds.
27
+ */
28
+ function showWorkflowToast(container, notification) {
29
+ const toast = document.createElement('div');
30
+ toast.setAttribute('data-testid', 'workflow-toast');
31
+ toast.className = 'se-fixed se-bottom-4 se-right-4 se-z-50';
32
+ Object.assign(toast.style, {
33
+ position: 'fixed',
34
+ bottom: '16px',
35
+ right: '16px',
36
+ zIndex: '2147483646',
37
+ padding: '12px 16px',
38
+ borderRadius: '8px',
39
+ backgroundColor: 'var(--se-color-bg-surface, #fff)',
40
+ color: 'var(--se-color-text-primary, #1a1a1a)',
41
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
42
+ maxWidth: '320px',
43
+ fontFamily: 'var(--se-font-family, system-ui, sans-serif)',
44
+ fontSize: '14px',
45
+ lineHeight: '1.4',
46
+ transition: 'opacity 0.3s ease',
47
+ });
48
+ const titleEl = document.createElement('div');
49
+ titleEl.className = 'se-font-semibold';
50
+ titleEl.style.fontWeight = '600';
51
+ titleEl.textContent = notification.title;
52
+ toast.appendChild(titleEl);
53
+ if (notification.body) {
54
+ const bodyEl = document.createElement('div');
55
+ bodyEl.className = 'se-mt-1 se-text-sm';
56
+ bodyEl.style.marginTop = '4px';
57
+ bodyEl.style.fontSize = '13px';
58
+ bodyEl.style.color = 'var(--se-color-text-secondary, #666)';
59
+ bodyEl.textContent = notification.body;
60
+ toast.appendChild(bodyEl);
61
+ }
62
+ container.appendChild(toast);
63
+ // Auto-remove after 4 seconds
64
+ let removeTimer;
65
+ const fadeTimer = setTimeout(() => {
66
+ toast.style.opacity = '0';
67
+ removeTimer = setTimeout(() => {
68
+ toast.remove();
69
+ }, 300);
70
+ }, 4000);
71
+ // Return cleanup function for unmount safety
72
+ return () => {
73
+ clearTimeout(fadeTimer);
74
+ clearTimeout(removeTimer);
75
+ toast.remove();
76
+ };
77
+ }
78
+ /**
79
+ * Extract workflow-enabled tours from active actions (runtime.actions.getActive()).
80
+ * Only actions with kind 'core:tour', a tourId, and a workflow field are included.
81
+ */
82
+ function extractWorkflowsFromActive(activeActions) {
83
+ const workflows = new Map();
84
+ for (const entry of activeActions) {
85
+ const action = entry.action;
86
+ if (action.kind === 'core:tour' && action.workflow && action.tourId) {
87
+ const meta = action.workflow;
88
+ const rawSteps = action.steps || [];
89
+ const steps = rawSteps.map((s) => ({
90
+ id: s.id,
91
+ title: meta.stepTitles?.[s.id] || s.id,
92
+ }));
93
+ workflows.set(action.tourId, { meta, steps });
94
+ }
95
+ }
96
+ return workflows;
97
+ }
98
+ export function WorkflowWidgetInner({ runtime }) {
99
+ // Scan active actions for workflow-enabled tours.
100
+ // Re-scans when actionVersion bumps (triggered by tour.started events).
101
+ const [actionVersion, setActionVersion] = useState(0);
102
+ const tourWorkflows = useMemo(() => {
103
+ const active = runtime?.actions?.getActive?.() || [];
104
+ return extractWorkflowsFromActive(active);
105
+ // eslint-disable-next-line react-hooks/exhaustive-deps
106
+ }, [runtime, actionVersion]);
107
+ // Keep a ref so event handlers always see the latest tourWorkflows
108
+ const tourWorkflowsRef = useRef(tourWorkflows);
109
+ tourWorkflowsRef.current = tourWorkflows;
110
+ // Re-scan active actions when a tour starts (handles tile-renders-before-actions timing)
111
+ useEffect(() => {
112
+ if (!runtime?.events?.subscribe)
113
+ return;
114
+ return runtime.events.subscribe({ names: ['tour.started', 'tour.resumed'] }, () => setActionVersion((v) => v + 1));
115
+ }, [runtime]);
116
+ // Load persisted state
117
+ const stateNs = useMemo(() => runtime?.state?.user?.ns?.('workflows'), [runtime]);
118
+ // Ref to track which tours have had a toast notification
119
+ const notifiedRef = useRef(new Set());
120
+ // Ref to track toast cleanup functions for unmount safety
121
+ const toastCleanupsRef = useRef([]);
122
+ // Ref to track the container element for toast rendering
123
+ const containerRef = useRef(null);
124
+ // Ref to track completed tour timestamps
125
+ const completedMapRef = useRef({});
126
+ // Build workflow entries from discovered tours + persisted state
127
+ const [workflowEntries, setWorkflowEntries] = useState([]);
128
+ // Initialize persisted state refs once
129
+ const persistInitialized = useRef(false);
130
+ if (!persistInitialized.current && stateNs) {
131
+ const notified = stateNs.get?.('notified') || [];
132
+ for (const id of notified) {
133
+ notifiedRef.current.add(id);
134
+ }
135
+ const completed = stateNs.get?.('completed') || {};
136
+ completedMapRef.current = { ...completed };
137
+ persistInitialized.current = true;
138
+ }
139
+ // Sync workflowEntries when tourWorkflows changes (new tours discovered via getActive)
140
+ useEffect(() => {
141
+ if (tourWorkflows.size === 0)
142
+ return;
143
+ const dismissed = stateNs?.get?.('dismissed') || [];
144
+ const completed = stateNs?.get?.('completed') || {};
145
+ setWorkflowEntries((prev) => {
146
+ const existingIds = new Set(prev.map((e) => e.tourId));
147
+ const newEntries = [];
148
+ for (const [tourId, { meta, steps }] of tourWorkflows) {
149
+ if (existingIds.has(tourId))
150
+ continue; // Already tracked
151
+ let status = 'active';
152
+ if (dismissed.includes(tourId)) {
153
+ status = 'dismissed';
154
+ }
155
+ else if (completed[tourId]) {
156
+ status = 'completed';
157
+ }
158
+ newEntries.push({
159
+ tourId,
160
+ meta,
161
+ steps,
162
+ currentStepId: null,
163
+ completedSteps: [],
164
+ status,
165
+ completedAt: completed[tourId] || undefined,
166
+ });
167
+ }
168
+ return newEntries.length > 0 ? [...prev, ...newEntries] : prev;
169
+ });
170
+ // Fire toast for newly discovered active tours.
171
+ // This handles the race condition where tour.started fires before the
172
+ // widget discovers the tour via getActive() re-scan.
173
+ for (const [tourId, { meta }] of tourWorkflows) {
174
+ if (!notifiedRef.current.has(tourId) &&
175
+ meta.notification &&
176
+ containerRef.current &&
177
+ !dismissed.includes(tourId) &&
178
+ !completed[tourId]) {
179
+ notifiedRef.current.add(tourId);
180
+ stateNs?.set?.('notified', [...notifiedRef.current]);
181
+ const cleanup = showWorkflowToast(containerRef.current, meta.notification);
182
+ toastCleanupsRef.current.push(cleanup);
183
+ }
184
+ }
185
+ }, [tourWorkflows, stateNs]);
186
+ // Subscribe to tour events
187
+ useEffect(() => {
188
+ if (!runtime?.events?.subscribe)
189
+ return;
190
+ const unsubscribe = runtime.events.subscribe({ patterns: ['^tour\\.'] }, (event) => {
191
+ const tourId = event.props?.tourId;
192
+ if (!tourId)
193
+ return;
194
+ // Re-scan active actions on tour.started (handles tile-renders-before-actions timing)
195
+ const currentWorkflows = tourWorkflowsRef.current;
196
+ if (!currentWorkflows.has(tourId) && event.name === 'tour.started') {
197
+ // Tour just became active — re-scan will pick it up on next render
198
+ setActionVersion((v) => v + 1);
199
+ return;
200
+ }
201
+ // Only process events for tours that have workflow metadata
202
+ if (!currentWorkflows.has(tourId))
203
+ return;
204
+ setWorkflowEntries((prev) => {
205
+ const updated = prev.map((entry) => {
206
+ if (entry.tourId !== tourId)
207
+ return entry;
208
+ switch (event.name) {
209
+ case 'tour.started': {
210
+ const startStepId = event.props?.startStepId || entry.steps[0]?.id || null;
211
+ // Fire toast notification if not already notified
212
+ if (!notifiedRef.current.has(tourId)) {
213
+ notifiedRef.current.add(tourId);
214
+ stateNs?.set?.('notified', [...notifiedRef.current]);
215
+ const workflow = currentWorkflows.get(tourId);
216
+ if (workflow?.meta.notification && containerRef.current) {
217
+ const cleanup = showWorkflowToast(containerRef.current, workflow.meta.notification);
218
+ toastCleanupsRef.current.push(cleanup);
219
+ }
220
+ }
221
+ // Persist active state
222
+ const activeIds = prev
223
+ .filter((e) => e.status === 'active' || e.tourId === tourId)
224
+ .map((e) => e.tourId);
225
+ if (!activeIds.includes(tourId)) {
226
+ activeIds.push(tourId);
227
+ }
228
+ stateNs?.set?.('active', [...new Set(activeIds)]);
229
+ return {
230
+ ...entry,
231
+ status: 'active',
232
+ currentStepId: startStepId,
233
+ completedSteps: entry.status === 'active' ? entry.completedSteps : [],
234
+ };
235
+ }
236
+ case 'tour.step_started': {
237
+ const stepId = event.props?.stepId;
238
+ return {
239
+ ...entry,
240
+ currentStepId: stepId || entry.currentStepId,
241
+ };
242
+ }
243
+ case 'tour.step_changed': {
244
+ const previousStepId = event.props?.previousStepId;
245
+ const nextStepId = event.props?.nextStepId;
246
+ const completedSteps = previousStepId && !entry.completedSteps.includes(previousStepId)
247
+ ? [...entry.completedSteps, previousStepId]
248
+ : entry.completedSteps;
249
+ return {
250
+ ...entry,
251
+ currentStepId: nextStepId || entry.currentStepId,
252
+ completedSteps,
253
+ };
254
+ }
255
+ case 'tour.completed': {
256
+ const completedAt = Date.now();
257
+ // Persist completed state with timestamp
258
+ completedMapRef.current[tourId] = completedAt;
259
+ stateNs?.set?.('completed', { ...completedMapRef.current });
260
+ return {
261
+ ...entry,
262
+ status: 'completed',
263
+ currentStepId: null,
264
+ completedSteps: entry.steps.map((s) => s.id),
265
+ completedAt,
266
+ };
267
+ }
268
+ case 'tour.paused': {
269
+ // Keep current state, just note it was paused
270
+ return entry;
271
+ }
272
+ default:
273
+ return entry;
274
+ }
275
+ });
276
+ return updated;
277
+ });
278
+ });
279
+ return () => {
280
+ unsubscribe();
281
+ // Clean up any pending toast timers
282
+ for (const cleanup of toastCleanupsRef.current) {
283
+ cleanup();
284
+ }
285
+ toastCleanupsRef.current = [];
286
+ };
287
+ }, [runtime, stateNs]);
288
+ // Handle step click — publish jump event
289
+ const handleStepClick = useCallback((tourId, stepId) => {
290
+ runtime?.events?.publish?.('workflow:jump_to_step', { tourId, stepId });
291
+ }, [runtime]);
292
+ // Handle dismiss
293
+ const handleDismiss = useCallback((tourId) => {
294
+ setWorkflowEntries((prev) => {
295
+ const updated = prev.map((entry) => entry.tourId === tourId ? { ...entry, status: 'dismissed' } : entry);
296
+ // Persist dismissed state
297
+ const dismissedIds = updated.filter((e) => e.status === 'dismissed').map((e) => e.tourId);
298
+ stateNs?.set?.('dismissed', dismissedIds);
299
+ return updated;
300
+ });
301
+ }, [stateNs]);
302
+ // Filter to active workflows only for rendering
303
+ const activeWorkflows = useMemo(() => workflowEntries.filter((w) => w.status === 'active'), [workflowEntries]);
304
+ return (_jsx("div", { ref: containerRef, children: _jsx(WorkflowTracker, { workflows: activeWorkflows, onStepClick: handleStepClick, onDismiss: handleDismiss }) }));
305
+ }
306
+ // ============================================================================
307
+ // MountableWidget Interface
308
+ // ============================================================================
309
+ /**
310
+ * Mountable widget interface for the runtime's WidgetRegistry.
311
+ *
312
+ * Follows the MountableWidget contract:
313
+ * mount(container, config?) -> cleanup | void
314
+ *
315
+ * Config is enriched with `runtime` by WidgetRegistry.bindRuntime().
316
+ */
317
+ export const WorkflowMountableWidget = {
318
+ mount(container, config) {
319
+ const runtime = config?.runtime || null;
320
+ const root = createRoot(container);
321
+ root.render(React.createElement(WorkflowWidgetInner, {
322
+ runtime,
323
+ }));
324
+ // Return cleanup function
325
+ return () => {
326
+ root.unmount();
327
+ };
328
+ },
329
+ };
package/dist/cdn.d.ts CHANGED
@@ -15,8 +15,8 @@ export declare const manifest: {
15
15
  description: string;
16
16
  runtime: {
17
17
  actions: {
18
- kind: "overlays:highlight" | "overlays:pulse" | "overlays:badge" | "overlays:tooltip" | "overlays:modal";
19
- executor: import("packages/sdk-contracts/dist").ActionExecutor<import("./types").ModalAction> | import("packages/sdk-contracts/dist").ActionExecutor<import("./types").HighlightAction> | import("packages/sdk-contracts/dist").ActionExecutor<import("./types").PulseAction> | import("packages/sdk-contracts/dist").ActionExecutor<import("./types").BadgeAction> | import("packages/sdk-contracts/dist").ActionExecutor<import("./types").TooltipAction>;
18
+ kind: "core:tour" | "overlays:celebrate" | "overlays:highlight" | "overlays:pulse" | "overlays:badge" | "overlays:tooltip" | "overlays:modal";
19
+ executor: import("packages/sdk-contracts/dist").ActionExecutor<import("./types").CelebrateAction> | import("packages/sdk-contracts/dist").ActionExecutor<import("./tour-types").TourAction> | import("packages/sdk-contracts/dist").ActionExecutor<import("./types").ModalAction> | import("packages/sdk-contracts/dist").ActionExecutor<import("./types").HighlightAction> | import("packages/sdk-contracts/dist").ActionExecutor<import("./types").PulseAction> | import("packages/sdk-contracts/dist").ActionExecutor<import("./types").BadgeAction> | import("packages/sdk-contracts/dist").ActionExecutor<import("./types").TooltipAction>;
20
20
  }[];
21
21
  };
22
22
  editor: {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=engine.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.test.d.ts","sourceRoot":"","sources":["../../../src/celebrations/__tests__/engine.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,130 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ /** Stub CanvasRenderingContext2D for jsdom (which lacks canvas support) */
3
+ function stubCanvasContext() {
4
+ const ctx = {
5
+ scale: vi.fn(),
6
+ clearRect: vi.fn(),
7
+ save: vi.fn(),
8
+ restore: vi.fn(),
9
+ translate: vi.fn(),
10
+ rotate: vi.fn(),
11
+ fillRect: vi.fn(),
12
+ beginPath: vi.fn(),
13
+ arc: vi.fn(),
14
+ fill: vi.fn(),
15
+ fillText: vi.fn(),
16
+ globalAlpha: 1,
17
+ fillStyle: '',
18
+ font: '',
19
+ shadowBlur: 0,
20
+ shadowColor: '',
21
+ };
22
+ vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(ctx);
23
+ return ctx;
24
+ }
25
+ function mockEffect() {
26
+ return {
27
+ init: vi.fn(() => [
28
+ {
29
+ x: 100,
30
+ y: 0,
31
+ vx: 0,
32
+ vy: 1,
33
+ rotation: 0,
34
+ rotationSpeed: 0,
35
+ size: 5,
36
+ color: '#ff0000',
37
+ opacity: 1,
38
+ },
39
+ ]),
40
+ update: vi.fn(() => true),
41
+ render: vi.fn(),
42
+ };
43
+ }
44
+ const defaultConfig = {
45
+ duration: 3000,
46
+ intensity: 'medium',
47
+ colors: ['#ff0000', '#00ff00'],
48
+ };
49
+ describe('CelebrationEngine', () => {
50
+ let container;
51
+ beforeEach(() => {
52
+ container = document.createElement('div');
53
+ document.body.appendChild(container);
54
+ stubCanvasContext();
55
+ vi.useFakeTimers();
56
+ });
57
+ afterEach(() => {
58
+ vi.useRealTimers();
59
+ vi.restoreAllMocks();
60
+ container.remove();
61
+ });
62
+ it('creates a canvas element when started', async () => {
63
+ const { CelebrationEngine } = await import('../engine');
64
+ const engine = new CelebrationEngine();
65
+ const effect = mockEffect();
66
+ engine.start(container, effect, defaultConfig);
67
+ const canvas = container.querySelector('canvas[data-syntro-celebrate]');
68
+ expect(canvas).not.toBeNull();
69
+ expect(canvas?.style.position).toBe('fixed');
70
+ expect(canvas?.style.pointerEvents).toBe('none');
71
+ engine.stop();
72
+ });
73
+ it('removes canvas when stopped', async () => {
74
+ const { CelebrationEngine } = await import('../engine');
75
+ const engine = new CelebrationEngine();
76
+ const effect = mockEffect();
77
+ engine.start(container, effect, defaultConfig);
78
+ expect(container.querySelector('canvas[data-syntro-celebrate]')).not.toBeNull();
79
+ engine.stop();
80
+ expect(container.querySelector('canvas[data-syntro-celebrate]')).toBeNull();
81
+ });
82
+ it('calls effect.init with canvas dimensions', async () => {
83
+ const { CelebrationEngine } = await import('../engine');
84
+ const engine = new CelebrationEngine();
85
+ const effect = mockEffect();
86
+ engine.start(container, effect, defaultConfig);
87
+ expect(effect.init).toHaveBeenCalledWith(expect.any(Number), expect.any(Number), defaultConfig);
88
+ engine.stop();
89
+ });
90
+ it('calls effect.update and effect.render on animation frame', async () => {
91
+ const { CelebrationEngine } = await import('../engine');
92
+ const engine = new CelebrationEngine();
93
+ const effect = mockEffect();
94
+ engine.start(container, effect, defaultConfig);
95
+ // Trigger an animation frame
96
+ vi.advanceTimersByTime(16);
97
+ expect(effect.update).toHaveBeenCalled();
98
+ expect(effect.render).toHaveBeenCalled();
99
+ engine.stop();
100
+ });
101
+ it('auto-stops after duration expires', async () => {
102
+ const { CelebrationEngine } = await import('../engine');
103
+ const engine = new CelebrationEngine();
104
+ const effect = mockEffect();
105
+ engine.start(container, effect, { ...defaultConfig, duration: 1000 });
106
+ // Advance past duration
107
+ vi.advanceTimersByTime(1100);
108
+ expect(container.querySelector('canvas[data-syntro-celebrate]')).toBeNull();
109
+ });
110
+ it('auto-stops when effect.update returns false', async () => {
111
+ const { CelebrationEngine } = await import('../engine');
112
+ const engine = new CelebrationEngine();
113
+ const effect = mockEffect();
114
+ // After first frame, update returns false (all particles done)
115
+ effect.update.mockReturnValue(false);
116
+ engine.start(container, effect, defaultConfig);
117
+ // Trigger animation frame
118
+ vi.advanceTimersByTime(16);
119
+ expect(container.querySelector('canvas[data-syntro-celebrate]')).toBeNull();
120
+ });
121
+ it('stop() is safe to call multiple times', async () => {
122
+ const { CelebrationEngine } = await import('../engine');
123
+ const engine = new CelebrationEngine();
124
+ const effect = mockEffect();
125
+ engine.start(container, effect, defaultConfig);
126
+ engine.stop();
127
+ engine.stop(); // should not throw
128
+ expect(container.querySelector('canvas[data-syntro-celebrate]')).toBeNull();
129
+ });
130
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=executor.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.test.d.ts","sourceRoot":"","sources":["../../../src/celebrations/__tests__/executor.test.ts"],"names":[],"mappings":""}