@proveanything/smartlinks 1.3.1 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,510 @@
1
+ # SmartLinks Widget System
2
+
3
+ This document covers the widget system that allows SmartLinks apps to expose embeddable React components for use in parent applications (like a SmartLinks portal or homepage).
4
+
5
+ ---
6
+
7
+ ## Overview
8
+
9
+ Widgets are self-contained React components that:
10
+ - Run inside the parent React application (not iframes)
11
+ - Receive standardized context props
12
+ - Inherit styling from the parent via CSS variables
13
+ - Can trigger navigation to the full app
14
+
15
+ ```text
16
+ ┌─────────────────────────────────────────────────────────────────┐
17
+ │ Parent SmartLinks Portal (React 18) │
18
+ │ │
19
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
20
+ │ │ Competition │ │ Music App │ │ Warranty │ │
21
+ │ │ Widget │ │ Widget │ │ Widget │ │
22
+ │ │ (ESM import) │ │ (ESM import) │ │ (ESM import) │ │
23
+ │ └──────────────┘ └──────────────┘ └──────────────┘ │
24
+ │ ↑ ↑ ↑ │
25
+ │ │ │ │ │
26
+ │ Props + SL SDK Props + SL SDK Props + SL SDK │
27
+ │ │
28
+ └─────────────────────────────────────────────────────────────────┘
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Widget Props Interface
34
+
35
+ All widgets receive the `SmartLinksWidgetProps` interface:
36
+
37
+ ```typescript
38
+ interface SmartLinksWidgetProps {
39
+ // Required context
40
+ collectionId: string;
41
+ appId: string;
42
+
43
+ // Optional context
44
+ productId?: string;
45
+ proofId?: string;
46
+
47
+ // User info (if logged in)
48
+ user?: {
49
+ id?: string;
50
+ email?: string;
51
+ name?: string;
52
+ admin?: boolean;
53
+ };
54
+
55
+ // SmartLinks SDK (pre-initialized by parent)
56
+ SL: typeof import('@proveanything/smartlinks');
57
+
58
+ // Callback to navigate within the parent application
59
+ onNavigate?: (path: string) => void;
60
+
61
+ // Base URL to the full public portal for deep linking
62
+ publicPortalUrl?: string;
63
+
64
+ // Size hint for responsive rendering
65
+ size?: 'compact' | 'standard' | 'large';
66
+
67
+ // Internationalization
68
+ lang?: string;
69
+ translations?: Record<string, string>;
70
+ }
71
+ ```
72
+
73
+ ### Props Explained
74
+
75
+ | Prop | Type | Description |
76
+ |------|------|-------------|
77
+ | `collectionId` | `string` | The collection context (required) |
78
+ | `appId` | `string` | This app's identifier (required) |
79
+ | `productId` | `string?` | Optional product context |
80
+ | `proofId` | `string?` | Optional proof context |
81
+ | `user` | `object?` | Current user info if authenticated |
82
+ | `SL` | `typeof SL` | Pre-initialized SmartLinks SDK |
83
+ | `onNavigate` | `function?` | Callback to navigate within parent app |
84
+ | `publicPortalUrl` | `string?` | Base URL to full portal for deep links |
85
+ | `size` | `string?` | Size hint: 'compact', 'standard', or 'large' |
86
+ | `lang` | `string?` | Language code (e.g., 'en', 'de', 'fr') |
87
+ | `translations` | `object?` | Translation overrides |
88
+
89
+ ### Navigation: onNavigate vs publicPortalUrl
90
+
91
+ Widgets support two navigation patterns:
92
+
93
+ **`onNavigate` (parent-controlled)**
94
+ - Parent provides a callback to handle navigation
95
+ - Widget passes a relative path (e.g., `/#/?collectionId=x&tab=details`)
96
+ - Parent decides what to do (router push, open modal, etc.)
97
+
98
+ **`publicPortalUrl` (direct redirect)**
99
+ - Widget knows the full URL to the public portal
100
+ - Uses `SL.iframe.redirectParent()` for navigation
101
+ - Automatically handles iframe escape via postMessage
102
+ - Useful when widget needs to break out of nested iframes
103
+
104
+ **Priority:** If both are provided, `onNavigate` takes precedence.
105
+
106
+ ```typescript
107
+ // Parent provides callback
108
+ <MyWidget
109
+ onNavigate={(path) => router.push(path)}
110
+ // ...
111
+ />
112
+
113
+ // Widget uses direct redirect
114
+ <MyWidget
115
+ publicPortalUrl="https://my-app.smartlinks.io"
116
+ // ...
117
+ />
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Building a Widget
123
+
124
+ ### 1. Create the Widget Component
125
+
126
+ ```typescript
127
+ // src/widgets/MyWidget/MyWidget.tsx
128
+ import { SmartLinksWidgetProps } from '../types';
129
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
130
+ import { Button } from '@/components/ui/button';
131
+
132
+ export const MyWidget: React.FC<SmartLinksWidgetProps> = ({
133
+ collectionId,
134
+ appId,
135
+ productId,
136
+ user,
137
+ SL,
138
+ onNavigate,
139
+ publicPortalUrl,
140
+ size = 'standard'
141
+ }) => {
142
+ // Use the SL SDK for API calls
143
+ const handleAction = async () => {
144
+ const data = await SL.appConfiguration.getConfig({
145
+ collectionId,
146
+ appId
147
+ });
148
+ console.log('Config:', data);
149
+ };
150
+
151
+ // Navigate to full app (supports both patterns)
152
+ const handleOpenApp = () => {
153
+ const params = new URLSearchParams({ collectionId, appId });
154
+ if (productId) params.set('productId', productId);
155
+ const relativePath = `/#/?${params.toString()}`;
156
+
157
+ if (onNavigate) {
158
+ // Parent-controlled navigation
159
+ onNavigate(relativePath);
160
+ } else if (publicPortalUrl) {
161
+ // Direct redirect (handles iframe escape automatically)
162
+ SL.iframe.redirectParent(`${publicPortalUrl}${relativePath}`);
163
+ }
164
+ };
165
+
166
+ return (
167
+ <Card>
168
+ <CardHeader>
169
+ <CardTitle>My Widget</CardTitle>
170
+ </CardHeader>
171
+ <CardContent>
172
+ <p className="text-muted-foreground mb-4">
173
+ Widget content goes here
174
+ </p>
175
+ <Button onClick={handleOpenApp}>Open App</Button>
176
+ </CardContent>
177
+ </Card>
178
+ );
179
+ };
180
+ ```
181
+
182
+ ### 2. Create the Index File
183
+
184
+ ```typescript
185
+ // src/widgets/MyWidget/index.tsx
186
+ export { MyWidget } from './MyWidget';
187
+ ```
188
+
189
+ ### 3. Export from Widget Barrel
190
+
191
+ ```typescript
192
+ // src/widgets/index.ts
193
+ export { MyWidget } from './MyWidget';
194
+
195
+ // Update the manifest
196
+ export const WIDGET_MANIFEST = {
197
+ version: '1.0.0',
198
+ reactVersion: '18.x',
199
+ widgets: [
200
+ // ... existing widgets
201
+ {
202
+ name: 'MyWidget',
203
+ description: 'Description of my widget',
204
+ sizes: ['compact', 'standard', 'large']
205
+ }
206
+ ]
207
+ };
208
+ ```
209
+
210
+ ---
211
+
212
+ ## Size Modes
213
+
214
+ Widgets should support three size modes:
215
+
216
+ | Size | Use Case | Typical Height |
217
+ |------|----------|----------------|
218
+ | `compact` | Sidebar, small spaces | 80-120px |
219
+ | `standard` | Default widget display | 150-250px |
220
+ | `large` | Featured/expanded view | 300px+ |
221
+
222
+ ```typescript
223
+ const MyWidget: React.FC<SmartLinksWidgetProps> = ({ size = 'standard' }) => {
224
+ const isCompact = size === 'compact';
225
+ const isLarge = size === 'large';
226
+
227
+ return (
228
+ <Card className={isCompact ? 'p-2' : ''}>
229
+ <CardHeader className={isCompact ? 'pb-2' : ''}>
230
+ <CardTitle className={isCompact ? 'text-sm' : 'text-lg'}>
231
+ Widget Title
232
+ </CardTitle>
233
+ </CardHeader>
234
+ <CardContent>
235
+ {isLarge && (
236
+ <p>Additional content shown only in large mode</p>
237
+ )}
238
+ <Button size={isCompact ? 'sm' : 'default'}>
239
+ Action
240
+ </Button>
241
+ </CardContent>
242
+ </Card>
243
+ );
244
+ };
245
+ ```
246
+
247
+ ---
248
+
249
+ ## Building Widget Bundle
250
+
251
+ The project includes a separate Vite config for building widgets:
252
+
253
+ ```bash
254
+ # Build widgets only
255
+ vite build --config vite.config.widget.ts
256
+
257
+ # Output: dist/widgets.es.js
258
+ ```
259
+
260
+ ### Build Configuration
261
+
262
+ The widget build:
263
+ - Outputs ESM format for modern browsers
264
+ - Externalizes dependencies the parent already provides (see below)
265
+ - Generates source maps for debugging
266
+ - Minifies with esbuild for production
267
+ - Outputs to `/dist` alongside the main app (not a separate folder)
268
+
269
+ ### Externalized Dependencies (Peer Dependencies)
270
+
271
+ The widget bundle does **not** include these libraries—the parent app must provide them:
272
+
273
+ | Package | Why Externalized |
274
+ |---------|------------------|
275
+ | `react`, `react-dom` | Parent's React context |
276
+ | `@proveanything/smartlinks` | Passed via props as `SL` |
277
+ | `tailwind-merge` | Utility for merging Tailwind classes |
278
+ | `clsx` | Utility for conditional class names |
279
+ | `class-variance-authority` | Utility for component variants |
280
+
281
+ These are standard packages that any modern React + Tailwind app will have. Externalizing them:
282
+ 1. Reduces bundle size significantly
283
+ 2. Removes JSDoc comments that inflate the bundle
284
+ 3. Ensures consistent behavior with parent's versions
285
+
286
+ ### Enabling Widget Builds
287
+
288
+ Widget builds are disabled by default to keep builds fast. To enable:
289
+
290
+ 1. Add to `.env`:
291
+ ```
292
+ VITE_ENABLE_WIDGETS=true
293
+ ```
294
+
295
+ 2. The build script should be: `vite build && vite build --config vite.config.widget.ts`
296
+
297
+ If `VITE_ENABLE_WIDGETS` is not set to `true`, the build produces a minimal stub file and skips quickly.
298
+
299
+ ---
300
+
301
+ ## Parent Integration
302
+
303
+ ### Dynamic Import
304
+
305
+ ```typescript
306
+ // In parent SmartLinks portal
307
+ import { lazy, Suspense } from 'react';
308
+ import * as SL from '@proveanything/smartlinks';
309
+
310
+ // Dynamic import from app's CDN
311
+ const CompetitionWidget = lazy(() =>
312
+ import('https://competition-app.example.com/widgets.es.js')
313
+ .then(m => ({ default: m.CompetitionWidget }))
314
+ );
315
+
316
+ function Portal() {
317
+ return (
318
+ <Suspense fallback={<WidgetSkeleton />}>
319
+ {/* Option 1: Parent controls navigation */}
320
+ <CompetitionWidget
321
+ collectionId="abc123"
322
+ appId="competition"
323
+ user={currentUser}
324
+ SL={SL}
325
+ onNavigate={(path) => window.open(`https://competition-app.example.com${path}`)}
326
+ size="standard"
327
+ />
328
+
329
+ {/* Option 2: Widget handles its own navigation */}
330
+ <CompetitionWidget
331
+ collectionId="abc123"
332
+ appId="competition"
333
+ user={currentUser}
334
+ SL={SL}
335
+ publicPortalUrl="https://competition-app.example.com"
336
+ size="standard"
337
+ />
338
+ </Suspense>
339
+ );
340
+ }
341
+ ```
342
+
343
+ ### Using WidgetWrapper
344
+
345
+ The `WidgetWrapper` component provides error boundary and loading handling:
346
+
347
+ ```typescript
348
+ import { WidgetWrapper, CompetitionWidget } from 'competition-app/widgets';
349
+
350
+ <WidgetWrapper
351
+ loading={<CustomLoader />}
352
+ error={<CustomError />}
353
+ >
354
+ <CompetitionWidget {...props} />
355
+ </WidgetWrapper>
356
+ ```
357
+
358
+ ### Checking Compatibility
359
+
360
+ ```typescript
361
+ import { WIDGET_MANIFEST } from 'competition-app/widgets';
362
+
363
+ // Verify React version compatibility
364
+ if (!WIDGET_MANIFEST.reactVersion.startsWith('18')) {
365
+ console.warn('Widget React version mismatch');
366
+ }
367
+
368
+ // List available widgets
369
+ console.log('Available widgets:', WIDGET_MANIFEST.widgets);
370
+ ```
371
+
372
+ ---
373
+
374
+ ## Styling
375
+
376
+ Widgets inherit styling from the parent application via CSS variables:
377
+
378
+ ```css
379
+ /* Parent defines these in their index.css */
380
+ :root {
381
+ --background: 0 0% 100%;
382
+ --foreground: 222.2 84% 4.9%;
383
+ --primary: 222.2 47.4% 11.2%;
384
+ --muted: 210 40% 96.1%;
385
+ /* ... etc */
386
+ }
387
+ ```
388
+
389
+ Widgets use semantic class names that reference these variables:
390
+
391
+ ```tsx
392
+ // ✅ DO: Use semantic classes
393
+ <div className="bg-background text-foreground">
394
+ <p className="text-muted-foreground">
395
+ <Button className="bg-primary text-primary-foreground">
396
+
397
+ // ❌ DON'T: Use hardcoded colors
398
+ <div className="bg-white text-black">
399
+ <p className="text-gray-500">
400
+ <Button className="bg-blue-500 text-white">
401
+ ```
402
+
403
+ ---
404
+
405
+ ## Best Practices
406
+
407
+ ### Do's
408
+
409
+ - ✅ Keep widgets focused and single-purpose
410
+ - ✅ Support all three size modes
411
+ - ✅ Use semantic color classes for theming
412
+ - ✅ Handle loading and error states gracefully
413
+ - ✅ Use the provided `SL` SDK for API calls
414
+ - ✅ Provide meaningful navigation via `onNavigate`
415
+
416
+ ### Don'ts
417
+
418
+ - ❌ Don't bundle React or SmartLinks SDK
419
+ - ❌ Don't use hardcoded colors
420
+ - ❌ Don't assume specific viewport sizes
421
+ - ❌ Don't make widgets too complex (use full app for that)
422
+ - ❌ Don't store state that should persist (use parent or SDK)
423
+
424
+ ---
425
+
426
+ ## Widget Manifest
427
+
428
+ Each app exports a `WIDGET_MANIFEST` for discovery:
429
+
430
+ ```typescript
431
+ export const WIDGET_MANIFEST = {
432
+ version: '1.0.0', // Widget bundle version
433
+ reactVersion: '18.x', // Required React version
434
+ widgets: [
435
+ {
436
+ name: 'ExampleWidget',
437
+ description: 'Demo widget showing SmartLinks integration',
438
+ sizes: ['compact', 'standard', 'large']
439
+ }
440
+ ]
441
+ };
442
+ ```
443
+
444
+ Parent applications can use this manifest to:
445
+ - Verify version compatibility
446
+ - Discover available widgets
447
+ - Show widget descriptions in UI
448
+
449
+ ---
450
+
451
+ ## Testing Widgets Locally
452
+
453
+ During development, you can test widgets within the app itself:
454
+
455
+ ```typescript
456
+ // In a development page
457
+ import { ExampleWidget } from '@/widgets';
458
+ import * as SL from '@proveanything/smartlinks';
459
+
460
+ function WidgetTestPage() {
461
+ return (
462
+ <div className="p-8 space-y-8">
463
+ <h2>Compact</h2>
464
+ <ExampleWidget
465
+ collectionId="test"
466
+ appId="example"
467
+ SL={SL}
468
+ size="compact"
469
+ />
470
+
471
+ <h2>Standard</h2>
472
+ <ExampleWidget
473
+ collectionId="test"
474
+ appId="example"
475
+ SL={SL}
476
+ size="standard"
477
+ />
478
+
479
+ <h2>Large</h2>
480
+ <ExampleWidget
481
+ collectionId="test"
482
+ appId="example"
483
+ SL={SL}
484
+ size="large"
485
+ />
486
+ </div>
487
+ );
488
+ }
489
+ ```
490
+
491
+ ---
492
+
493
+ ## File Structure
494
+
495
+ ```
496
+ src/widgets/
497
+ ├── index.ts # Main exports barrel
498
+ ├── types.ts # SmartLinksWidgetProps and related types
499
+ ├── WidgetWrapper.tsx # Error boundary + Suspense wrapper
500
+ └── ExampleWidget/
501
+ ├── index.tsx # Re-export
502
+ └── ExampleWidget.tsx # Widget implementation
503
+ ```
504
+
505
+ When creating new widgets, follow this structure:
506
+ 1. Create a folder: `src/widgets/MyWidget/`
507
+ 2. Add component: `MyWidget.tsx`
508
+ 3. Add re-export: `index.tsx`
509
+ 4. Export from: `src/widgets/index.ts`
510
+ 5. Add to: `WIDGET_MANIFEST`