@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.
- package/README.md +142 -0
- package/dist/accept-orchestration-CWZNCGZX.js +188 -0
- package/dist/agents-suggest-35LIQKDH.js +83 -0
- package/dist/aggregate-W7Q6VIM2.js +88 -0
- package/dist/auto-IU7VN55K.js +470 -0
- package/dist/beacon-B47XSTL7.js +251 -0
- package/dist/chunk-2M6OSOIG.js +1302 -0
- package/dist/chunk-4NCFWYGG.js +110 -0
- package/dist/chunk-5C4SGQKH.js +705 -0
- package/dist/chunk-5GOA7WYD.js +1095 -0
- package/dist/chunk-5JGJACDU.js +37 -0
- package/dist/chunk-6QC3YGB6.js +114 -0
- package/dist/chunk-753RICFF.js +325 -0
- package/dist/chunk-AD2LSCHB.js +1595 -0
- package/dist/chunk-CHSHON3O.js +669 -0
- package/dist/chunk-ELLR7WP6.js +3175 -0
- package/dist/chunk-ILOWBJRC.js +12 -0
- package/dist/chunk-IRKUEJVW.js +405 -0
- package/dist/chunk-MC7XC7XQ.js +533 -0
- package/dist/chunk-MO4EEYFW.js +38 -0
- package/dist/chunk-MQWH7PFI.js +13366 -0
- package/dist/chunk-N6PJAPDE.js +364 -0
- package/dist/chunk-PBHIFAL4.js +259 -0
- package/dist/chunk-PMXRGPRQ.js +305 -0
- package/dist/chunk-PW2EXJQT.js +689 -0
- package/dist/chunk-TAP5N3HH.js +245 -0
- package/dist/chunk-THFVK5AE.js +148 -0
- package/dist/chunk-UM54F7G5.js +1533 -0
- package/dist/chunk-UUZ2DMG5.js +185 -0
- package/dist/chunk-WS5KM7OL.js +780 -0
- package/dist/chunk-YDNKXH4Z.js +2316 -0
- package/dist/chunk-YO6DVTL7.js +99 -0
- package/dist/claude-SUYNN72C.js +362 -0
- package/dist/claude-cli-OF43XAO3.js +276 -0
- package/dist/claude-code-PW6SKD2M.js +126 -0
- package/dist/claude-code-teams-JLZ5IXB6.js +199 -0
- package/dist/constellation-K3CIQCHI.js +225 -0
- package/dist/cost-AEK6R7HK.js +174 -0
- package/dist/cost-KYXIQ62X.js +93 -0
- package/dist/cursor-cli-IHJMPRCW.js +269 -0
- package/dist/cursorrules-KI5QWHIX.js +84 -0
- package/dist/diff-AJJ5H6HV.js +125 -0
- package/dist/dist-7MPIRMTZ-IOQOREMZ.js +10866 -0
- package/dist/dist-NHJQVVUW.js +68 -0
- package/dist/dist-ZEMSQV74.js +20 -0
- package/dist/doctor-6Y6L6HEB.js +11 -0
- package/dist/echo-VYZW3OTT.js +248 -0
- package/dist/export-R4FJ5NOH.js +38 -0
- package/dist/history-EVO3L6SC.js +277 -0
- package/dist/hooks-MBWE4ILT.js +12 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +568 -0
- package/dist/lint-HXKTWRNO.js +316 -0
- package/dist/manual-Y3QOXWYA.js +204 -0
- package/dist/mcp.js +14745 -0
- package/dist/orchestrate-4ZH5GUQH.js +323 -0
- package/dist/probe-OYCP4JYG.js +151 -0
- package/dist/promote-Z52ZJTJU.js +181 -0
- package/dist/providers-4PGPZEWP.js +104 -0
- package/dist/remember-6VZ74B7E.js +77 -0
- package/dist/ripple-SBQOSTZD.js +215 -0
- package/dist/sentinel-LCFD56OJ.js +43 -0
- package/dist/server-F5ITNK6T.js +9846 -0
- package/dist/server-T6WIFYRQ.js +16076 -0
- package/dist/setup-DF4F3ICN.js +25 -0
- package/dist/setup-JHBPZAG7.js +296 -0
- package/dist/shift-HKIAP4ZN.js +226 -0
- package/dist/snapshot-GTVPRYZG.js +62 -0
- package/dist/spawn-BJRQA2NR.js +196 -0
- package/dist/summary-H6J6N6PJ.js +140 -0
- package/dist/switch-6EANJ7O6.js +232 -0
- package/dist/sync-BEOCW7TZ.js +11 -0
- package/dist/team-NWP2KJAB.js +32 -0
- package/dist/test-MA5TWJQV.js +934 -0
- package/dist/thread-JCJVRUQR.js +258 -0
- package/dist/triage-ETVXXFMV.js +1880 -0
- package/dist/tutorial-L5Q3ZDHK.js +666 -0
- package/dist/university-R2WDQLSI.js +40 -0
- package/dist/upgrade-5B3YGGC6.js +550 -0
- package/dist/validate-F3YHBCRZ.js +39 -0
- package/dist/validate-QEEY6KFS.js +64 -0
- package/dist/watch-4LT4O6K7.js +123 -0
- package/dist/watch-6IIWPWDN.js +111 -0
- package/dist/wisdom-LRM4FFCH.js +319 -0
- package/package.json +68 -0
- package/templates/paradigm/config.yaml +175 -0
- package/templates/paradigm/docs/commands.md +727 -0
- package/templates/paradigm/docs/decisions/000-template.md +47 -0
- package/templates/paradigm/docs/decisions/README.md +26 -0
- package/templates/paradigm/docs/error-patterns.md +215 -0
- package/templates/paradigm/docs/patterns.md +358 -0
- package/templates/paradigm/docs/queries.md +200 -0
- package/templates/paradigm/docs/troubleshooting.md +477 -0
- package/templates/paradigm/echoes.yaml +25 -0
- package/templates/paradigm/prompts/add-feature.md +152 -0
- package/templates/paradigm/prompts/add-gate.md +117 -0
- package/templates/paradigm/prompts/debug-auth.md +174 -0
- package/templates/paradigm/prompts/implement-ftux.md +722 -0
- package/templates/paradigm/prompts/implement-sandbox.md +651 -0
- package/templates/paradigm/prompts/read-docs.md +84 -0
- package/templates/paradigm/prompts/refactor.md +106 -0
- package/templates/paradigm/prompts/run-e2e-tests.md +340 -0
- package/templates/paradigm/prompts/trace-flow.md +202 -0
- package/templates/paradigm/prompts/validate-portals.md +279 -0
- package/templates/paradigm/specs/context-tracking.md +200 -0
- package/templates/paradigm/specs/context.md +461 -0
- package/templates/paradigm/specs/disciplines.md +413 -0
- package/templates/paradigm/specs/history.md +339 -0
- package/templates/paradigm/specs/logger.md +303 -0
- package/templates/paradigm/specs/navigator.md +236 -0
- package/templates/paradigm/specs/purpose.md +265 -0
- package/templates/paradigm/specs/scan.md +177 -0
- package/templates/paradigm/specs/symbols.md +451 -0
- 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
|