@a-company/paradigm 1.5.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.
Files changed (114) hide show
  1. package/README.md +142 -0
  2. package/dist/accept-orchestration-CWZNCGZX.js +188 -0
  3. package/dist/agents-suggest-35LIQKDH.js +83 -0
  4. package/dist/aggregate-W7Q6VIM2.js +88 -0
  5. package/dist/auto-IU7VN55K.js +470 -0
  6. package/dist/beacon-B47XSTL7.js +251 -0
  7. package/dist/chunk-2M6OSOIG.js +1302 -0
  8. package/dist/chunk-4NCFWYGG.js +110 -0
  9. package/dist/chunk-5C4SGQKH.js +705 -0
  10. package/dist/chunk-5GOA7WYD.js +1095 -0
  11. package/dist/chunk-5JGJACDU.js +37 -0
  12. package/dist/chunk-6QC3YGB6.js +114 -0
  13. package/dist/chunk-753RICFF.js +325 -0
  14. package/dist/chunk-AD2LSCHB.js +1595 -0
  15. package/dist/chunk-CHSHON3O.js +669 -0
  16. package/dist/chunk-ELLR7WP6.js +3175 -0
  17. package/dist/chunk-ILOWBJRC.js +12 -0
  18. package/dist/chunk-IRKUEJVW.js +405 -0
  19. package/dist/chunk-MC7XC7XQ.js +533 -0
  20. package/dist/chunk-MO4EEYFW.js +38 -0
  21. package/dist/chunk-MQWH7PFI.js +13366 -0
  22. package/dist/chunk-N6PJAPDE.js +364 -0
  23. package/dist/chunk-PBHIFAL4.js +259 -0
  24. package/dist/chunk-PMXRGPRQ.js +305 -0
  25. package/dist/chunk-PW2EXJQT.js +689 -0
  26. package/dist/chunk-TAP5N3HH.js +245 -0
  27. package/dist/chunk-THFVK5AE.js +148 -0
  28. package/dist/chunk-UM54F7G5.js +1533 -0
  29. package/dist/chunk-UUZ2DMG5.js +185 -0
  30. package/dist/chunk-WS5KM7OL.js +780 -0
  31. package/dist/chunk-YDNKXH4Z.js +2316 -0
  32. package/dist/chunk-YO6DVTL7.js +99 -0
  33. package/dist/claude-SUYNN72C.js +362 -0
  34. package/dist/claude-cli-OF43XAO3.js +276 -0
  35. package/dist/claude-code-PW6SKD2M.js +126 -0
  36. package/dist/claude-code-teams-JLZ5IXB6.js +199 -0
  37. package/dist/constellation-K3CIQCHI.js +225 -0
  38. package/dist/cost-AEK6R7HK.js +174 -0
  39. package/dist/cost-KYXIQ62X.js +93 -0
  40. package/dist/cursor-cli-IHJMPRCW.js +269 -0
  41. package/dist/cursorrules-KI5QWHIX.js +84 -0
  42. package/dist/diff-AJJ5H6HV.js +125 -0
  43. package/dist/dist-7MPIRMTZ-IOQOREMZ.js +10866 -0
  44. package/dist/dist-NHJQVVUW.js +68 -0
  45. package/dist/dist-ZEMSQV74.js +20 -0
  46. package/dist/doctor-6Y6L6HEB.js +11 -0
  47. package/dist/echo-VYZW3OTT.js +248 -0
  48. package/dist/export-R4FJ5NOH.js +38 -0
  49. package/dist/history-EVO3L6SC.js +277 -0
  50. package/dist/hooks-MBWE4ILT.js +12 -0
  51. package/dist/index.d.ts +2 -0
  52. package/dist/index.js +568 -0
  53. package/dist/lint-HXKTWRNO.js +316 -0
  54. package/dist/manual-Y3QOXWYA.js +204 -0
  55. package/dist/mcp.js +14745 -0
  56. package/dist/orchestrate-4ZH5GUQH.js +323 -0
  57. package/dist/probe-OYCP4JYG.js +151 -0
  58. package/dist/promote-Z52ZJTJU.js +181 -0
  59. package/dist/providers-4PGPZEWP.js +104 -0
  60. package/dist/remember-6VZ74B7E.js +77 -0
  61. package/dist/ripple-SBQOSTZD.js +215 -0
  62. package/dist/sentinel-LCFD56OJ.js +43 -0
  63. package/dist/server-F5ITNK6T.js +9846 -0
  64. package/dist/server-T6WIFYRQ.js +16076 -0
  65. package/dist/setup-DF4F3ICN.js +25 -0
  66. package/dist/setup-JHBPZAG7.js +296 -0
  67. package/dist/shift-HKIAP4ZN.js +226 -0
  68. package/dist/snapshot-GTVPRYZG.js +62 -0
  69. package/dist/spawn-BJRQA2NR.js +196 -0
  70. package/dist/summary-H6J6N6PJ.js +140 -0
  71. package/dist/switch-6EANJ7O6.js +232 -0
  72. package/dist/sync-BEOCW7TZ.js +11 -0
  73. package/dist/team-NWP2KJAB.js +32 -0
  74. package/dist/test-MA5TWJQV.js +934 -0
  75. package/dist/thread-JCJVRUQR.js +258 -0
  76. package/dist/triage-ETVXXFMV.js +1880 -0
  77. package/dist/tutorial-L5Q3ZDHK.js +666 -0
  78. package/dist/university-R2WDQLSI.js +40 -0
  79. package/dist/upgrade-5B3YGGC6.js +550 -0
  80. package/dist/validate-F3YHBCRZ.js +39 -0
  81. package/dist/validate-QEEY6KFS.js +64 -0
  82. package/dist/watch-4LT4O6K7.js +123 -0
  83. package/dist/watch-6IIWPWDN.js +111 -0
  84. package/dist/wisdom-LRM4FFCH.js +319 -0
  85. package/package.json +68 -0
  86. package/templates/paradigm/config.yaml +175 -0
  87. package/templates/paradigm/docs/commands.md +727 -0
  88. package/templates/paradigm/docs/decisions/000-template.md +47 -0
  89. package/templates/paradigm/docs/decisions/README.md +26 -0
  90. package/templates/paradigm/docs/error-patterns.md +215 -0
  91. package/templates/paradigm/docs/patterns.md +358 -0
  92. package/templates/paradigm/docs/queries.md +200 -0
  93. package/templates/paradigm/docs/troubleshooting.md +477 -0
  94. package/templates/paradigm/echoes.yaml +25 -0
  95. package/templates/paradigm/prompts/add-feature.md +152 -0
  96. package/templates/paradigm/prompts/add-gate.md +117 -0
  97. package/templates/paradigm/prompts/debug-auth.md +174 -0
  98. package/templates/paradigm/prompts/implement-ftux.md +722 -0
  99. package/templates/paradigm/prompts/implement-sandbox.md +651 -0
  100. package/templates/paradigm/prompts/read-docs.md +84 -0
  101. package/templates/paradigm/prompts/refactor.md +106 -0
  102. package/templates/paradigm/prompts/run-e2e-tests.md +340 -0
  103. package/templates/paradigm/prompts/trace-flow.md +202 -0
  104. package/templates/paradigm/prompts/validate-portals.md +279 -0
  105. package/templates/paradigm/specs/context-tracking.md +200 -0
  106. package/templates/paradigm/specs/context.md +461 -0
  107. package/templates/paradigm/specs/disciplines.md +413 -0
  108. package/templates/paradigm/specs/history.md +339 -0
  109. package/templates/paradigm/specs/logger.md +303 -0
  110. package/templates/paradigm/specs/navigator.md +236 -0
  111. package/templates/paradigm/specs/purpose.md +265 -0
  112. package/templates/paradigm/specs/scan.md +177 -0
  113. package/templates/paradigm/specs/symbols.md +451 -0
  114. package/templates/paradigm/specs/wisdom.md +294 -0
@@ -0,0 +1,722 @@
1
+ # Implement FTUX System
2
+
3
+ > Paradigm Prompt - AI Agent Guide for Implementing FTUX
4
+
5
+ Use this prompt when implementing the FTUX (First Time User Experience) system in a Paradigm project.
6
+
7
+ ---
8
+
9
+ ## Context
10
+
11
+ You are implementing the FTUX Component System in a project that uses the Paradigm framework. This system enables product teams to create guided onboarding flows, feature discovery, and contextual help by targeting components with `data-ftux-id` attributes.
12
+
13
+ ## Prerequisites
14
+
15
+ Before starting, ensure:
16
+ - [ ] Project uses React with TypeScript
17
+ - [ ] Database is Supabase (or compatible PostgreSQL)
18
+ - [ ] Paradigm logger is already set up (`src/lib/paradigmLogger.ts`)
19
+ - [ ] Basic auth system is in place
20
+
21
+ ## Reference Documentation
22
+
23
+ Read these specs before implementing:
24
+ - `specs/ftux-component-system.md` - Full system specification
25
+ - `specs/sandbox-mode.md` - For window shopper support (optional)
26
+
27
+ ---
28
+
29
+ ## Implementation Steps
30
+
31
+ ### Step 1: Create Database Tables
32
+
33
+ Create a migration file to set up the FTUX tables:
34
+
35
+ ```sql
36
+ -- migrations/YYYYMMDDHHMMSS_ftux_tables.sql
37
+
38
+ -- Component Registry
39
+ CREATE TABLE ftux_component_registry (
40
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
41
+ component_id TEXT UNIQUE NOT NULL,
42
+ page_identifier TEXT,
43
+ description TEXT,
44
+ component_path TEXT,
45
+ is_active BOOLEAN DEFAULT true,
46
+ created_at TIMESTAMPTZ DEFAULT now(),
47
+ updated_at TIMESTAMPTZ DEFAULT now()
48
+ );
49
+
50
+ -- Events
51
+ CREATE TABLE ftux_events (
52
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
53
+ component_id TEXT NOT NULL,
54
+ effect_type TEXT NOT NULL CHECK (effect_type IN ('highlight', 'standout', 'tooltip', 'pulse')),
55
+ effect_params JSONB DEFAULT '{}',
56
+ tooltip_text TEXT,
57
+ tooltip_direction TEXT DEFAULT 'auto',
58
+ conditions JSONB DEFAULT '{}',
59
+ action_text TEXT,
60
+ action_url TEXT,
61
+ dismiss_trigger TEXT DEFAULT 'click' CHECK (dismiss_trigger IN ('click', 'cta', 'timer')),
62
+ priority INTEGER DEFAULT 0,
63
+ journey_id UUID REFERENCES ftux_journeys(id) ON DELETE SET NULL,
64
+ journey_order INTEGER DEFAULT 0,
65
+ is_active BOOLEAN DEFAULT true,
66
+ created_at TIMESTAMPTZ DEFAULT now(),
67
+ updated_at TIMESTAMPTZ DEFAULT now()
68
+ );
69
+
70
+ -- Journeys
71
+ CREATE TABLE ftux_journeys (
72
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
73
+ journey_id TEXT UNIQUE NOT NULL,
74
+ name TEXT NOT NULL,
75
+ description TEXT,
76
+ trigger_conditions JSONB DEFAULT '{}',
77
+ is_active BOOLEAN DEFAULT true,
78
+ priority INTEGER DEFAULT 0,
79
+ show_progress BOOLEAN DEFAULT true,
80
+ dismissible BOOLEAN DEFAULT true,
81
+ created_at TIMESTAMPTZ DEFAULT now(),
82
+ updated_at TIMESTAMPTZ DEFAULT now()
83
+ );
84
+
85
+ -- User Completions
86
+ CREATE TABLE ftux_user_completions (
87
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
88
+ user_id UUID NOT NULL,
89
+ ftux_event_id UUID REFERENCES ftux_events(id) ON DELETE CASCADE NOT NULL,
90
+ journey_id UUID REFERENCES ftux_journeys(id) ON DELETE SET NULL,
91
+ completed_at TIMESTAMPTZ DEFAULT now(),
92
+ dismissed BOOLEAN DEFAULT false,
93
+ UNIQUE(user_id, ftux_event_id)
94
+ );
95
+
96
+ -- Indexes
97
+ CREATE INDEX idx_ftux_events_component ON ftux_events(component_id);
98
+ CREATE INDEX idx_ftux_events_journey ON ftux_events(journey_id);
99
+ CREATE INDEX idx_ftux_completions_user ON ftux_user_completions(user_id);
100
+
101
+ -- RLS Policies
102
+ ALTER TABLE ftux_events ENABLE ROW LEVEL SECURITY;
103
+ ALTER TABLE ftux_journeys ENABLE ROW LEVEL SECURITY;
104
+ ALTER TABLE ftux_user_completions ENABLE ROW LEVEL SECURITY;
105
+
106
+ CREATE POLICY "Users can read active events" ON ftux_events
107
+ FOR SELECT USING (is_active = true);
108
+
109
+ CREATE POLICY "Users can read active journeys" ON ftux_journeys
110
+ FOR SELECT USING (is_active = true);
111
+
112
+ CREATE POLICY "Users manage own completions" ON ftux_user_completions
113
+ FOR ALL USING (auth.uid() = user_id);
114
+ ```
115
+
116
+ ### Step 2: Create Core Components
117
+
118
+ #### 2.1 FTUXId.tsx - Component Targeting
119
+
120
+ ```tsx
121
+ // src/components/FTUXId.tsx
122
+ import React, { useEffect, useRef, useCallback, useMemo } from 'react';
123
+ import { log } from '@/lib/paradigmLogger';
124
+
125
+ export interface FTUXProps {
126
+ 'data-ftux-id': string;
127
+ ref: React.Ref<any>;
128
+ }
129
+
130
+ // HOC for auto-registration
131
+ export function withFTUXId<P extends object>(
132
+ componentId: string,
133
+ pageIdentifier: string,
134
+ description?: string,
135
+ componentPath?: string
136
+ ) {
137
+ return (WrappedComponent: React.ComponentType<P & { ftuxProps?: FTUXProps }>) => {
138
+ const ComponentWithFTUXId = React.forwardRef<any, P>((props, ref) => {
139
+ const internalRef = useRef<any>(null);
140
+
141
+ const mergedRef = useCallback((node: any) => {
142
+ internalRef.current = node;
143
+ if (typeof ref === 'function') {
144
+ ref(node);
145
+ } else if (ref) {
146
+ (ref as React.MutableRefObject<any>).current = node;
147
+ }
148
+ }, [ref]);
149
+
150
+ useEffect(() => {
151
+ if (import.meta.env.DEV) {
152
+ log.component('#ftux-registry').debug('Component registered', {
153
+ componentId,
154
+ pageIdentifier,
155
+ description,
156
+ });
157
+ }
158
+ }, []);
159
+
160
+ const ftuxProps: FTUXProps = {
161
+ 'data-ftux-id': componentId,
162
+ ref: mergedRef,
163
+ };
164
+
165
+ return <WrappedComponent {...props as P} ftuxProps={ftuxProps} />;
166
+ });
167
+
168
+ ComponentWithFTUXId.displayName = `withFTUXId(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;
169
+ return ComponentWithFTUXId;
170
+ };
171
+ }
172
+
173
+ // Hook for flexible usage
174
+ export function useFTUXId(
175
+ componentId: string,
176
+ pageIdentifier: string,
177
+ description?: string
178
+ ) {
179
+ const ref = useRef<any>(null);
180
+
181
+ useEffect(() => {
182
+ if (import.meta.env.DEV) {
183
+ log.component('#ftux-registry').debug('Component registered via hook', {
184
+ componentId,
185
+ pageIdentifier,
186
+ description,
187
+ });
188
+ }
189
+ }, [componentId, pageIdentifier, description]);
190
+
191
+ const ftuxProps: FTUXProps = useMemo(() => ({
192
+ 'data-ftux-id': componentId,
193
+ ref: ref,
194
+ }), [componentId]);
195
+
196
+ return ftuxProps;
197
+ }
198
+
199
+ // Wrapper component for declarative usage
200
+ interface FTUXTargetProps {
201
+ id: string;
202
+ page: string;
203
+ description?: string;
204
+ children: React.ReactElement;
205
+ }
206
+
207
+ export function FTUXTarget({ id, page, description, children }: FTUXTargetProps) {
208
+ const ref = useRef<any>(null);
209
+
210
+ useEffect(() => {
211
+ if (import.meta.env.DEV) {
212
+ log.component('#ftux-registry').debug('Component registered via FTUXTarget', {
213
+ componentId: id,
214
+ pageIdentifier: page,
215
+ description,
216
+ });
217
+ }
218
+ }, [id, page, description]);
219
+
220
+ return React.cloneElement(children, {
221
+ 'data-ftux-id': id,
222
+ ref: ref,
223
+ });
224
+ }
225
+ ```
226
+
227
+ #### 2.2 FTUXWrapper.tsx - Effect Renderer
228
+
229
+ ```tsx
230
+ // src/components/FTUXWrapper.tsx
231
+ import React, { useEffect, useState, useRef } from 'react';
232
+ import { useFTUX } from '@/hooks/useFTUX';
233
+ import { FTUXTooltip } from './FTUXTooltip';
234
+ import { log } from '@/lib/paradigmLogger';
235
+
236
+ interface FTUXWrapperProps {
237
+ pageIdentifier: string;
238
+ children: React.ReactNode;
239
+ }
240
+
241
+ export function FTUXWrapper({ pageIdentifier, children }: FTUXWrapperProps) {
242
+ const { activeEvent, completeEvent, dismissEvent } = useFTUX(pageIdentifier);
243
+ const [targetElement, setTargetElement] = useState<HTMLElement | null>(null);
244
+
245
+ useEffect(() => {
246
+ if (!activeEvent) {
247
+ setTargetElement(null);
248
+ return;
249
+ }
250
+
251
+ const element = document.querySelector(
252
+ `[data-ftux-id="${activeEvent.component_id}"]`
253
+ ) as HTMLElement;
254
+
255
+ if (element) {
256
+ setTargetElement(element);
257
+ log.flow('$ftux').info('Event activated', {
258
+ eventId: activeEvent.id,
259
+ componentId: activeEvent.component_id,
260
+ });
261
+ }
262
+ }, [activeEvent]);
263
+
264
+ useEffect(() => {
265
+ if (!targetElement || !activeEvent) return;
266
+
267
+ // Apply effect based on type
268
+ const cleanup = applyEffect(targetElement, activeEvent.effect_type, activeEvent.effect_params);
269
+
270
+ return cleanup;
271
+ }, [targetElement, activeEvent]);
272
+
273
+ const handleComplete = () => {
274
+ if (activeEvent) {
275
+ completeEvent(activeEvent.id);
276
+ log.signal('!ftux-event-complete').info('Event completed', {
277
+ eventId: activeEvent.id,
278
+ });
279
+ }
280
+ };
281
+
282
+ const handleDismiss = () => {
283
+ if (activeEvent) {
284
+ dismissEvent(activeEvent.id);
285
+ log.signal('!ftux-event-dismissed').info('Event dismissed', {
286
+ eventId: activeEvent.id,
287
+ });
288
+ }
289
+ };
290
+
291
+ return (
292
+ <>
293
+ {children}
294
+ {activeEvent && targetElement && activeEvent.effect_type === 'tooltip' && (
295
+ <FTUXTooltip
296
+ targetElement={targetElement}
297
+ text={activeEvent.tooltip_text || ''}
298
+ direction={activeEvent.tooltip_direction || 'auto'}
299
+ actionText={activeEvent.action_text}
300
+ actionUrl={activeEvent.action_url}
301
+ onComplete={handleComplete}
302
+ onDismiss={handleDismiss}
303
+ />
304
+ )}
305
+ </>
306
+ );
307
+ }
308
+
309
+ function applyEffect(
310
+ element: HTMLElement,
311
+ effectType: string,
312
+ params: Record<string, any>
313
+ ): () => void {
314
+ const originalStyles: Record<string, string> = {};
315
+
316
+ switch (effectType) {
317
+ case 'highlight':
318
+ originalStyles.boxShadow = element.style.boxShadow;
319
+ originalStyles.outline = element.style.outline;
320
+ element.style.boxShadow = '0 0 0 4px rgba(var(--primary), 0.3)';
321
+ element.style.outline = '2px solid rgb(var(--primary))';
322
+ break;
323
+
324
+ case 'standout':
325
+ originalStyles.transform = element.style.transform;
326
+ originalStyles.zIndex = element.style.zIndex;
327
+ originalStyles.boxShadow = element.style.boxShadow;
328
+ element.style.transform = 'scale(1.05)';
329
+ element.style.zIndex = '50';
330
+ element.style.boxShadow = '0 25px 50px -12px rgba(0, 0, 0, 0.25)';
331
+ break;
332
+
333
+ case 'pulse':
334
+ element.classList.add('animate-pulse');
335
+ break;
336
+ }
337
+
338
+ return () => {
339
+ if (effectType === 'pulse') {
340
+ element.classList.remove('animate-pulse');
341
+ } else {
342
+ Object.entries(originalStyles).forEach(([key, value]) => {
343
+ (element.style as any)[key] = value;
344
+ });
345
+ }
346
+ };
347
+ }
348
+ ```
349
+
350
+ #### 2.3 FTUXTooltip.tsx
351
+
352
+ ```tsx
353
+ // src/components/FTUXTooltip.tsx
354
+ import React, { useState, useEffect, useRef } from 'react';
355
+ import { Button } from '@/components/ui/button';
356
+ import { X } from 'lucide-react';
357
+
358
+ interface FTUXTooltipProps {
359
+ targetElement: HTMLElement;
360
+ text: string;
361
+ direction: string;
362
+ actionText?: string | null;
363
+ actionUrl?: string | null;
364
+ onComplete: () => void;
365
+ onDismiss: () => void;
366
+ }
367
+
368
+ export function FTUXTooltip({
369
+ targetElement,
370
+ text,
371
+ direction,
372
+ actionText,
373
+ actionUrl,
374
+ onComplete,
375
+ onDismiss,
376
+ }: FTUXTooltipProps) {
377
+ const tooltipRef = useRef<HTMLDivElement>(null);
378
+ const [position, setPosition] = useState({ top: 0, left: 0 });
379
+
380
+ useEffect(() => {
381
+ const updatePosition = () => {
382
+ const rect = targetElement.getBoundingClientRect();
383
+ const tooltip = tooltipRef.current;
384
+ if (!tooltip) return;
385
+
386
+ const tooltipRect = tooltip.getBoundingClientRect();
387
+ let top = 0;
388
+ let left = 0;
389
+
390
+ const actualDirection = direction === 'auto'
391
+ ? calculateBestDirection(rect, tooltipRect)
392
+ : direction;
393
+
394
+ switch (actualDirection) {
395
+ case 'top':
396
+ top = rect.top - tooltipRect.height - 8;
397
+ left = rect.left + (rect.width - tooltipRect.width) / 2;
398
+ break;
399
+ case 'bottom':
400
+ top = rect.bottom + 8;
401
+ left = rect.left + (rect.width - tooltipRect.width) / 2;
402
+ break;
403
+ case 'left':
404
+ top = rect.top + (rect.height - tooltipRect.height) / 2;
405
+ left = rect.left - tooltipRect.width - 8;
406
+ break;
407
+ case 'right':
408
+ top = rect.top + (rect.height - tooltipRect.height) / 2;
409
+ left = rect.right + 8;
410
+ break;
411
+ }
412
+
413
+ setPosition({ top, left });
414
+ };
415
+
416
+ updatePosition();
417
+ window.addEventListener('resize', updatePosition);
418
+ window.addEventListener('scroll', updatePosition);
419
+
420
+ return () => {
421
+ window.removeEventListener('resize', updatePosition);
422
+ window.removeEventListener('scroll', updatePosition);
423
+ };
424
+ }, [targetElement, direction]);
425
+
426
+ const handleAction = () => {
427
+ if (actionUrl) {
428
+ window.location.href = actionUrl;
429
+ }
430
+ onComplete();
431
+ };
432
+
433
+ return (
434
+ <div
435
+ ref={tooltipRef}
436
+ className="fixed z-[100] max-w-xs bg-popover border rounded-lg shadow-lg p-4 animate-in fade-in-0 zoom-in-95"
437
+ style={{ top: position.top, left: position.left }}
438
+ >
439
+ <button
440
+ onClick={onDismiss}
441
+ className="absolute top-2 right-2 text-muted-foreground hover:text-foreground"
442
+ >
443
+ <X className="h-4 w-4" />
444
+ </button>
445
+
446
+ <p className="text-sm pr-6">{text}</p>
447
+
448
+ {actionText && (
449
+ <div className="mt-3">
450
+ <Button size="sm" onClick={handleAction}>
451
+ {actionText}
452
+ </Button>
453
+ </div>
454
+ )}
455
+ </div>
456
+ );
457
+ }
458
+
459
+ function calculateBestDirection(
460
+ targetRect: DOMRect,
461
+ tooltipRect: DOMRect
462
+ ): string {
463
+ const viewportHeight = window.innerHeight;
464
+ const viewportWidth = window.innerWidth;
465
+
466
+ const spaceAbove = targetRect.top;
467
+ const spaceBelow = viewportHeight - targetRect.bottom;
468
+ const spaceLeft = targetRect.left;
469
+ const spaceRight = viewportWidth - targetRect.right;
470
+
471
+ if (spaceBelow >= tooltipRect.height + 8) return 'bottom';
472
+ if (spaceAbove >= tooltipRect.height + 8) return 'top';
473
+ if (spaceRight >= tooltipRect.width + 8) return 'right';
474
+ if (spaceLeft >= tooltipRect.width + 8) return 'left';
475
+
476
+ return 'bottom';
477
+ }
478
+ ```
479
+
480
+ ### Step 3: Create Hooks
481
+
482
+ #### 3.1 useFTUX.ts
483
+
484
+ ```tsx
485
+ // src/hooks/useFTUX.ts
486
+ import { useState, useEffect, useCallback } from 'react';
487
+ import { supabase } from '@/integrations/supabase/client';
488
+ import { useAuth } from '@/contexts/AuthContext';
489
+ import { log } from '@/lib/paradigmLogger';
490
+
491
+ interface FTUXEvent {
492
+ id: string;
493
+ component_id: string;
494
+ effect_type: string;
495
+ effect_params: Record<string, any>;
496
+ tooltip_text: string | null;
497
+ tooltip_direction: string | null;
498
+ conditions: Record<string, any>;
499
+ action_text: string | null;
500
+ action_url: string | null;
501
+ dismiss_trigger: string;
502
+ priority: number;
503
+ journey_id: string | null;
504
+ journey_order: number;
505
+ }
506
+
507
+ export function useFTUX(pageIdentifier: string) {
508
+ const { user } = useAuth();
509
+ const [activeEvent, setActiveEvent] = useState<FTUXEvent | null>(null);
510
+ const [completedEvents, setCompletedEvents] = useState<Set<string>>(new Set());
511
+ const [loading, setLoading] = useState(true);
512
+
513
+ // Fetch completed events
514
+ useEffect(() => {
515
+ if (!user) return;
516
+
517
+ const fetchCompleted = async () => {
518
+ const { data } = await supabase
519
+ .from('ftux_user_completions')
520
+ .select('ftux_event_id')
521
+ .eq('user_id', user.id);
522
+
523
+ if (data) {
524
+ setCompletedEvents(new Set(data.map(c => c.ftux_event_id)));
525
+ }
526
+ };
527
+
528
+ fetchCompleted();
529
+ }, [user]);
530
+
531
+ // Fetch and evaluate active event for page
532
+ useEffect(() => {
533
+ const fetchEvents = async () => {
534
+ setLoading(true);
535
+
536
+ // Get all active events for this page
537
+ const { data: events } = await supabase
538
+ .from('ftux_events')
539
+ .select('*')
540
+ .eq('is_active', true)
541
+ .order('priority', { ascending: false })
542
+ .order('journey_order', { ascending: true });
543
+
544
+ if (!events) {
545
+ setActiveEvent(null);
546
+ setLoading(false);
547
+ return;
548
+ }
549
+
550
+ // Find first uncompleted event that matches conditions
551
+ const matchingEvent = events.find(event => {
552
+ if (completedEvents.has(event.id)) return false;
553
+ return evaluateConditions(event.conditions, { pageIdentifier });
554
+ });
555
+
556
+ setActiveEvent(matchingEvent || null);
557
+ setLoading(false);
558
+ };
559
+
560
+ fetchEvents();
561
+ }, [pageIdentifier, completedEvents]);
562
+
563
+ const completeEvent = useCallback(async (eventId: string) => {
564
+ if (!user) return;
565
+
566
+ await supabase.from('ftux_user_completions').insert({
567
+ user_id: user.id,
568
+ ftux_event_id: eventId,
569
+ });
570
+
571
+ setCompletedEvents(prev => new Set([...prev, eventId]));
572
+ setActiveEvent(null);
573
+
574
+ log.signal('!ftux-complete').info('FTUX event completed', { eventId });
575
+ }, [user]);
576
+
577
+ const dismissEvent = useCallback(async (eventId: string) => {
578
+ if (!user) return;
579
+
580
+ await supabase.from('ftux_user_completions').insert({
581
+ user_id: user.id,
582
+ ftux_event_id: eventId,
583
+ dismissed: true,
584
+ });
585
+
586
+ setCompletedEvents(prev => new Set([...prev, eventId]));
587
+ setActiveEvent(null);
588
+
589
+ log.signal('!ftux-dismiss').info('FTUX event dismissed', { eventId });
590
+ }, [user]);
591
+
592
+ return {
593
+ activeEvent,
594
+ loading,
595
+ completeEvent,
596
+ dismissEvent,
597
+ };
598
+ }
599
+
600
+ function evaluateConditions(
601
+ conditions: Record<string, any>,
602
+ context: { pageIdentifier: string }
603
+ ): boolean {
604
+ if (!conditions || Object.keys(conditions).length === 0) {
605
+ return true;
606
+ }
607
+
608
+ if (conditions.routes && !conditions.routes.includes(context.pageIdentifier)) {
609
+ return false;
610
+ }
611
+
612
+ // Add more condition evaluations as needed
613
+ return true;
614
+ }
615
+ ```
616
+
617
+ ### Step 4: Add Component IDs to Key Components
618
+
619
+ Identify and tag important components:
620
+
621
+ ```tsx
622
+ // Example: Add Lead Button
623
+ <Button
624
+ data-ftux-id="add-lead-button"
625
+ onClick={handleAddLead}
626
+ >
627
+ Add Lead
628
+ </Button>
629
+
630
+ // Example: Integration Connect Button
631
+ <Button
632
+ data-ftux-id="connect-facebook-integration"
633
+ onClick={handleConnect}
634
+ >
635
+ Connect Facebook
636
+ </Button>
637
+
638
+ // Example: Sidebar Navigation
639
+ <NavLink
640
+ data-ftux-id="nav-analytics"
641
+ to="/analytics"
642
+ >
643
+ Analytics
644
+ </NavLink>
645
+ ```
646
+
647
+ ### Step 5: Wrap Pages with FTUXWrapper
648
+
649
+ ```tsx
650
+ // src/pages/LeadsPage.tsx
651
+ import { FTUXWrapper } from '@/components/FTUXWrapper';
652
+
653
+ export function LeadsPage() {
654
+ return (
655
+ <FTUXWrapper pageIdentifier="leads-page">
656
+ <div className="container">
657
+ {/* Page content */}
658
+ </div>
659
+ </FTUXWrapper>
660
+ );
661
+ }
662
+ ```
663
+
664
+ ### Step 6: Create Admin UI (Optional)
665
+
666
+ If the project needs admin management of FTUX:
667
+
668
+ 1. Create `src/pages/AdminFTUX/index.tsx` - Main admin page with tabs
669
+ 2. Create `src/pages/AdminFTUX/ComponentRegistry.tsx` - Manage component IDs
670
+ 3. Create `src/pages/AdminFTUX/EventBuilder.tsx` - Visual event editor
671
+ 4. Create `src/pages/AdminFTUX/JourneyDesigner.tsx` - Journey management
672
+ 5. Create `src/pages/AdminFTUX/AnalyticsDashboard.tsx` - Completion metrics
673
+
674
+ Add route:
675
+ ```tsx
676
+ <Route
677
+ path="/admin/ftux/*"
678
+ element={
679
+ <SuperAdminRoute>
680
+ <AdminFTUX />
681
+ </SuperAdminRoute>
682
+ }
683
+ />
684
+ ```
685
+
686
+ ---
687
+
688
+ ## Verification Checklist
689
+
690
+ After implementation, verify:
691
+
692
+ - [ ] Database tables created with correct schema
693
+ - [ ] RLS policies working (users can only see active events)
694
+ - [ ] Component IDs appear on key elements (`data-ftux-id`)
695
+ - [ ] FTUXWrapper renders effects correctly
696
+ - [ ] Events can be created in database and appear
697
+ - [ ] Completing event records in ftux_user_completions
698
+ - [ ] Completed events don't show again
699
+ - [ ] Paradigm logger outputs FTUX events
700
+
701
+ ---
702
+
703
+ ## Common Issues
704
+
705
+ ### Events Not Showing
706
+
707
+ 1. Check `is_active = true` on event
708
+ 2. Verify component has `data-ftux-id` attribute
709
+ 3. Check conditions match current user context
710
+ 4. Ensure event not already completed
711
+
712
+ ### Tooltip Positioning Wrong
713
+
714
+ 1. Check target element is visible in viewport
715
+ 2. Verify no CSS transforms on ancestors
716
+ 3. Use `direction: 'auto'` for automatic positioning
717
+
718
+ ### Performance Issues
719
+
720
+ 1. Lazy-load FTUX components
721
+ 2. Debounce condition evaluation
722
+ 3. Cache completed events in state