@weave-apps/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3703 @@
1
+ # Weave App Development Specification
2
+
3
+ > **AI Assistant Guide**: This document provides the complete specification for building Weave applications. Use this as your reference when helping developers create apps.
4
+
5
+ ## Architecture Overview
6
+
7
+ Weave apps are **Web Components** that run in an **iframe** inside a browser extension sidebar. They interact with two secure APIs:
8
+
9
+ 1. **Weave Backend API** - AI services and data storage
10
+ 2. **DOM Bridge API** - Parent page DOM manipulation
11
+
12
+ ```
13
+ [Weave Backend] ←→ [Sidebar: API Bridge] ←→ [Iframe: Weave App]
14
+
15
+ [Parent Page] ←→ [Content Script: DOMBridge] ←→ [Iframe: Weave App]
16
+ ```
17
+
18
+ ### Key Constraints
19
+
20
+ - ✅ Apps run in an **isolated iframe** (different origin)
21
+ - ✅ Apps use **Shadow DOM** for style isolation
22
+ - ✅ **Tailwind CSS is available** for styling
23
+ - ✅ **No direct DOM access** to parent page
24
+ - ✅ **No arbitrary HTTP requests** to external APIs
25
+ - ✅ All backend calls go through **`weaveAPI`** (AI, data storage)
26
+ - ✅ All parent page interactions go through **`weaveDOM`** (DOM manipulation)
27
+ - ✅ Apps are uploaded as **plain JavaScript strings** (not modules)
28
+ - ✅ SDK is available on **window globals** (not imported)
29
+
30
+ ## App Structure
31
+
32
+ ### Important: App Header Already Displayed
33
+
34
+ **The Weave sidebar automatically displays your app's name as a header at the top of the drawer.** You do NOT need to include your app name as a header in your render method. Including it would create a duplicate header.
35
+
36
+ ✅ **CORRECT:**
37
+ ```typescript
38
+ protected render(): void {
39
+ this.renderHTML(`
40
+ <div class="p-4">
41
+ <!-- No need for app name header - it's already shown by the sidebar -->
42
+ <p>Your app content starts here...</p>
43
+ </div>
44
+ `);
45
+ }
46
+ ```
47
+
48
+ ❌ **WRONG - Creates duplicate header:**
49
+ ```typescript
50
+ protected render(): void {
51
+ this.renderHTML(`
52
+ <div class="p-4">
53
+ <h1>My App Name</h1> <!-- ❌ Duplicate! Sidebar already shows this -->
54
+ <p>Your app content...</p>
55
+ </div>
56
+ `);
57
+ }
58
+ ```
59
+
60
+ ### Base Class: `WeaveBaseApp`
61
+
62
+ All apps **must** extend `WeaveBaseApp` (available as `window.WeaveBaseApp`):
63
+
64
+ ```typescript
65
+ import { WeaveBaseApp } from '@weave/app-sdk';
66
+
67
+ class MyApp extends WeaveBaseApp {
68
+ constructor() {
69
+ super({
70
+ name: 'My App', // Display name
71
+ version: '1.0.0', // Semantic version
72
+ category: 'utility', // App category
73
+ description: 'Description', // What the app does
74
+ tags: ['tag1', 'tag2'] // Optional tags
75
+ });
76
+
77
+ // Initialize state
78
+ this.state = {
79
+ // Your app state here
80
+ };
81
+ }
82
+
83
+ protected render(): void {
84
+ // Render your UI
85
+ this.renderHTML(`
86
+ <style>
87
+ /* Scoped styles */
88
+ </style>
89
+ <div>Your HTML</div>
90
+ `);
91
+ }
92
+
93
+ protected setupEventListeners(): void {
94
+ // Setup event listeners
95
+ this.query('#myButton')?.addEventListener('click', () => {
96
+ this.handleClick();
97
+ });
98
+ }
99
+
100
+ protected cleanup(): void {
101
+ // Optional: cleanup when app unmounts
102
+ }
103
+ }
104
+
105
+ // Register the custom element
106
+ customElements.define('my-app', MyApp);
107
+ ```
108
+
109
+ ### Required Methods
110
+
111
+ #### `constructor(appInfo: WeaveAppInfo)`
112
+ - Initialize app metadata
113
+ - Set initial state
114
+ - **Do NOT** render or setup listeners here
115
+
116
+ #### `render(): void`
117
+ - Called automatically when app is added to DOM
118
+ - Use `this.renderHTML(html)` to inject content
119
+ - Include `<style>` tags for scoped CSS
120
+
121
+ #### `setupEventListeners(): void` (Optional)
122
+ - Called after `render()`
123
+ - Attach event listeners to shadow DOM elements
124
+ - Use `this.query()` to find elements
125
+
126
+ #### `cleanup(): void` (Optional)
127
+ - Called when app is removed from DOM
128
+ - Clear intervals, remove listeners, etc.
129
+
130
+ ## App Settings
131
+
132
+ ### Overview
133
+
134
+ App Settings provide a way for **Enterprise Admins** to configure app behavior without modifying code. Settings are simple **key-value pairs** (both key and value are strings) that are:
135
+
136
+ - 🔧 **Configured in Enterprise Admin Console** - Admins set values per app
137
+ - 🚀 **Automatically injected at runtime** - Available in `this.appSettings` before any app code runs
138
+ - 🔒 **Read-only in apps** - Apps cannot modify settings (admin-controlled)
139
+ - 🌍 **Company-scoped** - Each company can have different settings for the same app
140
+ - 💡 **Perfect for configuration** - URLs, API keys, feature flags, thresholds, etc.
141
+
142
+ ### How It Works
143
+
144
+ ```
145
+ 1. Admin uploads app to Enterprise Console
146
+ 2. Admin configures settings (key-value pairs)
147
+ 3. User loads sidebar → Apps are instantiated
148
+ 4. BackgroundAppManager automatically calls setAppSettings()
149
+ 5. Settings available in this.appSettings throughout app lifecycle
150
+ ```
151
+
152
+ ### Architecture
153
+
154
+ **Enterprise Admin Console** → Sets settings → **Weave API** → Stores in database
155
+
156
+ **Sidebar loads** → Fetches app data → **BackgroundAppManager** → Calls `setAppSettings()`
157
+
158
+ **Your App** → Access via `this.appSettings`
159
+
160
+ ### Accessing Settings in Your App
161
+
162
+ Settings are automatically available in the `this.appSettings` property:
163
+
164
+ ```typescript
165
+ class MyApp extends WeaveBaseApp {
166
+ constructor() {
167
+ super({
168
+ name: 'My App',
169
+ version: '1.0.0',
170
+ category: 'utility',
171
+ description: 'Configurable app',
172
+ tags: ['config']
173
+ });
174
+ }
175
+
176
+ async onBackgroundService() {
177
+ // ✅ Settings are already available here
178
+ const apiUrl = this.appSettings?.apiEndpoint || 'https://default-api.com';
179
+ const maxRetries = parseInt(this.appSettings?.maxRetries || '3');
180
+ const featureEnabled = this.appSettings?.enableFeature === 'true';
181
+
182
+ console.log('API URL:', apiUrl);
183
+ console.log('Max Retries:', maxRetries);
184
+ console.log('Feature Enabled:', featureEnabled);
185
+ }
186
+
187
+ render() {
188
+ // ✅ Settings available in render too
189
+ const theme = this.appSettings?.theme || 'light';
190
+
191
+ this.renderHTML(`
192
+ <div class="p-4">
193
+ <p>Current theme: ${theme}</p>
194
+ </div>
195
+ `);
196
+ }
197
+ }
198
+ ```
199
+
200
+ ### Setting Types and Parsing
201
+
202
+ **Important:** All setting values are **strings**. You must parse them for other types:
203
+
204
+ ```typescript
205
+ // String values (no parsing needed)
206
+ const apiUrl = this.appSettings?.apiUrl || 'https://default.com';
207
+ const userName = this.appSettings?.userName || 'Guest';
208
+
209
+ // Numeric values (parse to number)
210
+ const timeout = parseInt(this.appSettings?.timeout || '5000');
211
+ const maxItems = parseFloat(this.appSettings?.maxItems || '100');
212
+
213
+ // Boolean values (compare string)
214
+ const isEnabled = this.appSettings?.featureFlag === 'true';
215
+ const debugMode = this.appSettings?.debug === '1'; // or '1', 'yes', etc.
216
+
217
+ // JSON values (parse JSON string)
218
+ const config = JSON.parse(this.appSettings?.config || '{}');
219
+ const allowedRoles = JSON.parse(this.appSettings?.roles || '[]');
220
+ ```
221
+
222
+ ### Common Use Cases
223
+
224
+ #### 1. Configurable URLs
225
+
226
+ ```typescript
227
+ class DataExportApp extends WeaveBaseApp {
228
+ async injectButton() {
229
+ const buttonId = await window.weaveDOM.injectElement(
230
+ '.header',
231
+ 'beforeend',
232
+ '<button>Export Data</button>',
233
+ {
234
+ onClick: async () => {
235
+ // ✅ Use setting for target system domain (admin-configurable)
236
+ const targetDomain = this.appSettings?.targetSystemUrl || 'https://app.example.com';
237
+
238
+ // Open target system with configured domain
239
+ window.open(`${targetDomain}/import`, '_blank');
240
+ }
241
+ }
242
+ );
243
+ }
244
+ }
245
+ ```
246
+
247
+ **Why this is useful:**
248
+ - Different companies may use different system instances (staging, production, custom domains)
249
+ - Admin can configure without code changes
250
+ - Same app works for all companies with different URLs
251
+
252
+ #### 2. Feature Flags
253
+
254
+ ```typescript
255
+ class SmartFormApp extends WeaveBaseApp {
256
+ async onBackgroundService() {
257
+ // Check if AI suggestions feature is enabled
258
+ const aiEnabled = this.appSettings?.enableAiSuggestions === 'true';
259
+ const autoFillEnabled = this.appSettings?.enableAutoFill === 'true';
260
+
261
+ if (aiEnabled) {
262
+ await this.setupAiSuggestions();
263
+ }
264
+
265
+ if (autoFillEnabled) {
266
+ await this.setupAutoFill();
267
+ }
268
+ }
269
+ }
270
+ ```
271
+
272
+ #### 3. API Keys and Credentials
273
+
274
+ ```typescript
275
+ class IntegrationApp extends WeaveBaseApp {
276
+ async callExternalService() {
277
+ // ✅ Admin configures API key per company
278
+ const apiKey = this.appSettings?.apiKey;
279
+
280
+ if (!apiKey) {
281
+ console.error('API key not configured');
282
+ return;
283
+ }
284
+
285
+ // Use API key in requests
286
+ const response = await this.weaveAPI.ai.chat({
287
+ prompt: `Call external service with key: ${apiKey}`,
288
+ context: 'Integration request'
289
+ });
290
+ }
291
+ }
292
+ ```
293
+
294
+ #### 4. Thresholds and Limits
295
+
296
+ ```typescript
297
+ class MonitoringApp extends WeaveBaseApp {
298
+ async checkMetrics() {
299
+ // Admin-configurable thresholds
300
+ const warningThreshold = parseInt(this.appSettings?.warningThreshold || '80');
301
+ const criticalThreshold = parseInt(this.appSettings?.criticalThreshold || '95');
302
+ const checkInterval = parseInt(this.appSettings?.checkIntervalMs || '60000');
303
+
304
+ setInterval(() => {
305
+ const currentValue = this.getCurrentMetric();
306
+
307
+ if (currentValue > criticalThreshold) {
308
+ this.showAlert('critical');
309
+ } else if (currentValue > warningThreshold) {
310
+ this.showAlert('warning');
311
+ }
312
+ }, checkInterval);
313
+ }
314
+ }
315
+ ```
316
+
317
+ #### 5. UI Customization
318
+
319
+ ```typescript
320
+ class CustomDashboard extends WeaveBaseApp {
321
+ render() {
322
+ // Admin-configurable UI elements
323
+ const primaryColor = this.appSettings?.primaryColor || '#4f46e5';
324
+ const logoUrl = this.appSettings?.logoUrl || '';
325
+ const welcomeMessage = this.appSettings?.welcomeMessage || 'Welcome!';
326
+
327
+ this.renderHTML(`
328
+ <style>
329
+ .header { background: ${primaryColor}; }
330
+ </style>
331
+ <div class="p-4">
332
+ ${logoUrl ? `<img src="${logoUrl}" alt="Logo" />` : ''}
333
+ <h2>${welcomeMessage}</h2>
334
+ </div>
335
+ `);
336
+ }
337
+ }
338
+ ```
339
+
340
+ ### Setting Key Naming Conventions
341
+
342
+ **Best Practices:**
343
+ - Use **camelCase** for keys: `apiEndpoint`, `maxRetries`, `enableFeature`
344
+ - Be **descriptive**: `dcpDomain` not `url`, `enableAiSuggestions` not `ai`
345
+ - Use **prefixes** for grouped settings: `email_host`, `email_port`, `email_username`
346
+ - Keep keys **short but clear**: `timeout` not `t`, `maxItems` not `maximumNumberOfItems`
347
+
348
+ **Valid Key Format:**
349
+ - Must start with a letter (a-z, A-Z)
350
+ - Can contain letters, numbers, and underscores
351
+ - Examples: `apiKey`, `max_retries`, `feature1_enabled`
352
+
353
+ ### Default Values Pattern
354
+
355
+ **Always provide defaults** to handle missing settings:
356
+
357
+ ```typescript
358
+ // ✅ CORRECT - Provides fallback
359
+ const timeout = parseInt(this.appSettings?.timeout || '5000');
360
+ const apiUrl = this.appSettings?.apiUrl || 'https://default-api.com';
361
+ const enabled = this.appSettings?.enabled === 'true'; // false if not set
362
+
363
+ // ❌ WRONG - Will crash if setting not configured
364
+ const timeout = parseInt(this.appSettings.timeout); // Error if undefined
365
+ ```
366
+
367
+ ### Checking if Settings Exist
368
+
369
+ ```typescript
370
+ class MyApp extends WeaveBaseApp {
371
+ async onBackgroundService() {
372
+ // Check if settings object exists
373
+ if (!this.appSettings) {
374
+ console.log('No settings configured for this app');
375
+ return;
376
+ }
377
+
378
+ // Check if specific setting exists
379
+ if (!this.appSettings.apiKey) {
380
+ console.error('API key not configured - app cannot function');
381
+ this.showConfigurationError();
382
+ return;
383
+ }
384
+
385
+ // Proceed with configured settings
386
+ await this.initialize();
387
+ }
388
+ }
389
+ ```
390
+
391
+ ### Settings Lifecycle
392
+
393
+ ```
394
+ 1. Sidebar loads
395
+
396
+ 2. BackgroundAppManager fetches app data from API
397
+
398
+ 3. For each app with settings:
399
+ - BackgroundAppManager calls app.setAppSettings(settings)
400
+ - this.appSettings is populated
401
+
402
+ 4. App constructor runs
403
+ - this.appSettings already available
404
+
405
+ 5. onBackgroundService() called
406
+ - this.appSettings available
407
+
408
+ 6. App continues running
409
+ - this.appSettings available throughout lifecycle
410
+ ```
411
+
412
+ **Key Point:** Settings are injected **before** your constructor runs, so they're available everywhere in your app.
413
+
414
+ ### Real-World Example: Medical Notes Export Integration
415
+
416
+ This example shows how an app uses settings for configurable target system domain:
417
+
418
+ ```typescript
419
+ class MedicalNotesExportApp extends WeaveBaseApp {
420
+ constructor() {
421
+ super({
422
+ name: 'Medical Notes Export',
423
+ version: '1.0.0',
424
+ category: 'integration',
425
+ description: 'Export medical notes to external system',
426
+ tags: ['medical', 'export', 'integration']
427
+ });
428
+
429
+ this.state = {
430
+ buttonInjected: false,
431
+ buttonElementId: null
432
+ };
433
+ }
434
+
435
+ async onBackgroundService() {
436
+ const pageUrl = await window.weaveDOM.getPageUrl();
437
+
438
+ // Only inject on medical records pages
439
+ if (pageUrl.includes('medical-records.example.com')) {
440
+ await this.injectButton();
441
+ }
442
+ }
443
+
444
+ async injectButton() {
445
+ if (this.state.buttonInjected) return;
446
+
447
+ const self = this;
448
+
449
+ this.state.buttonElementId = await window.weaveDOM.injectElement(
450
+ '.toolbar',
451
+ 'beforeend',
452
+ '<button class="export-btn">📋 Export Notes</button>',
453
+ {
454
+ onClick: async () => {
455
+ try {
456
+ // Get content from page
457
+ const content = await window.weaveDOM.query('[data-testid="notes-editor"]');
458
+
459
+ if (content && content.textContent) {
460
+ // Save content via API
461
+ await self.saveContent(content.textContent);
462
+
463
+ // ✅ Use configurable target system domain from settings
464
+ // Admin can set different domains per company:
465
+ // - Production: https://app.target-system.com
466
+ // - Staging: https://staging.target-system.com
467
+ // - Custom: https://custom-instance.example.com
468
+ const targetDomain = self.appSettings?.targetSystemUrl || 'https://app.target-system.com';
469
+
470
+ // Open target system with configured domain
471
+ window.open(`${targetDomain}/import`, '_blank');
472
+ }
473
+ } catch (error) {
474
+ console.error('Error exporting notes:', error);
475
+ }
476
+ }
477
+ }
478
+ );
479
+
480
+ this.state.buttonInjected = true;
481
+ }
482
+
483
+ async saveContent(content: string) {
484
+ const apiClient = this.weaveAPI;
485
+
486
+ // Save to app data
487
+ await apiClient.appData.create({
488
+ dataKey: 'exported-note',
489
+ data: { content, timestamp: Date.now() }
490
+ });
491
+ }
492
+ }
493
+
494
+ customElements.define('medical-notes-export-app', MedicalNotesExportApp);
495
+ ```
496
+
497
+ **What the admin configures:**
498
+ ```
499
+ Key: targetSystemUrl
500
+ Value: https://staging.target-system.com
501
+ ```
502
+
503
+ **Result:** All users in that company will have notes exported to the staging instance instead of production.
504
+
505
+ ### Settings vs State
506
+
507
+ **App Settings (`this.appSettings`):**
508
+ - ✅ Configured by **Enterprise Admin**
509
+ - ✅ **Read-only** in app code
510
+ - ✅ **Company-scoped** (different per company)
511
+ - ✅ **Persistent** (stored in database)
512
+ - ✅ Use for: URLs, API keys, feature flags, thresholds
513
+ - ❌ Cannot be modified by app
514
+
515
+ **App State (`this.state`):**
516
+ - ✅ Managed by **app code**
517
+ - ✅ **Read-write** in app
518
+ - ✅ **User-scoped** (per user instance)
519
+ - ✅ **Temporary** (lost on sidebar reload)
520
+ - ✅ Use for: UI state, counters, flags, temporary data
521
+ - ❌ Not persisted across sessions
522
+
523
+ ### Best Practices
524
+
525
+ #### ✅ DO:
526
+ - Always provide **default values** using `||` operator
527
+ - **Parse** string values to appropriate types (number, boolean, JSON)
528
+ - **Validate** settings before use (check if exists, check format)
529
+ - Use settings for **configuration** (URLs, keys, flags, limits)
530
+ - Document **expected settings** in your app description
531
+ - Use **descriptive key names** (camelCase)
532
+
533
+ #### ❌ DON'T:
534
+ - Don't assume settings exist (always check or provide defaults)
535
+ - Don't try to modify `this.appSettings` (read-only)
536
+ - Don't use settings for **user data** (use `this.weaveAPI.appData` instead)
537
+ - Don't use settings for **temporary state** (use `this.state` instead)
538
+ - Don't forget to **parse** non-string values
539
+ - Don't use generic key names like `url`, `key`, `value`
540
+
541
+ ### Troubleshooting
542
+
543
+ **Settings are undefined:**
544
+ ```typescript
545
+ // Problem: this.appSettings is undefined
546
+ console.log(this.appSettings); // undefined
547
+
548
+ // Solution: Admin hasn't configured settings yet
549
+ // Always provide defaults:
550
+ const apiUrl = this.appSettings?.apiUrl || 'https://default.com';
551
+ ```
552
+
553
+ **Settings not updating:**
554
+ ```typescript
555
+ // Problem: Changed settings in admin console but app still uses old values
556
+ // Solution: Reload sidebar (settings are loaded on sidebar initialization)
557
+ // Users need to refresh the page or reload the extension
558
+ ```
559
+
560
+ **Type conversion issues:**
561
+ ```typescript
562
+ // Problem: Setting is string but need number
563
+ const timeout = this.appSettings?.timeout; // "5000" (string)
564
+ setTimeout(() => {}, timeout); // Wrong! Expects number
565
+
566
+ // Solution: Parse to number
567
+ const timeout = parseInt(this.appSettings?.timeout || '5000');
568
+ setTimeout(() => {}, timeout); // Correct!
569
+ ```
570
+
571
+ ### Summary
572
+
573
+ - **App Settings** = Admin-configured key-value pairs (strings)
574
+ - **Automatically injected** by BackgroundAppManager before app runs
575
+ - **Available everywhere** via `this.appSettings`
576
+ - **Read-only** in apps (admin controls values)
577
+ - **Perfect for configuration** (URLs, keys, flags, limits)
578
+ - **Always provide defaults** to handle missing settings
579
+ - **Parse values** for non-string types (numbers, booleans, JSON)
580
+
581
+ ## Background Services & URL Change Detection
582
+
583
+ ### Overview
584
+
585
+ Apps run as **single instances** that serve both background and foreground purposes. When the sidebar loads, all apps are instantiated immediately and can run background tasks **before** being attached to the DOM. This enables powerful use cases like:
586
+
587
+ - 🔧 **Auto-inject UI elements** based on URL patterns
588
+ - 🔄 **React to SPA navigation** without page reload
589
+ - 💾 **Maintain persistent state** across open/close cycles
590
+ - 🎯 **Monitor page changes** and trigger actions
591
+ - 🤖 **Run background logic** without user interaction
592
+
593
+ ### App Lifecycle
594
+
595
+ ```
596
+ Sidebar loads
597
+
598
+ Apps instantiated (in memory, not attached to DOM)
599
+
600
+ onBackgroundService() called ← Background tasks start
601
+
602
+ App runs in background...
603
+
604
+ User clicks app → Attach to DOM → render() called
605
+
606
+ User closes app → Detach from DOM
607
+
608
+ Background continues running with same state
609
+ ```
610
+
611
+ ### Background Service Method
612
+
613
+ #### `onBackgroundService(): void` (Optional)
614
+
615
+ Called **immediately** after app instantiation, before DOM attachment. Use this for:
616
+ - Injecting UI elements onto the page
617
+ - Setting up event listeners on the parent page
618
+ - Monitoring page state
619
+ - Running background logic
620
+
621
+ **Important:** This method runs even when the app is not open in the drawer.
622
+
623
+ ```typescript
624
+ class AutoButtonInjector extends WeaveBaseApp {
625
+ constructor() {
626
+ super({
627
+ id: 'auto-button-injector',
628
+ name: 'Auto Button Injector',
629
+ version: '1.0.0',
630
+ category: 'utility',
631
+ description: 'Automatically injects buttons based on URL',
632
+ tags: ['automation']
633
+ });
634
+
635
+ this.state = {
636
+ buttonInjected: false,
637
+ clickCount: 0
638
+ };
639
+ }
640
+
641
+ // 🔧 Background service - runs immediately
642
+ async onBackgroundService() {
643
+ console.log('🔧 Background service started');
644
+
645
+ // Get the actual page URL (not iframe URL)
646
+ const pageUrl = await weaveDOM.getPageUrl();
647
+ console.log('Current page:', pageUrl);
648
+
649
+ // Check current URL and inject if needed
650
+ this.checkAndInject(pageUrl);
651
+ }
652
+
653
+ async checkAndInject(url) {
654
+ // Define your URL pattern
655
+ const shouldInject = url.includes('/dashboard');
656
+
657
+ if (shouldInject && !this.state.buttonInjected) {
658
+ // Inject button with onClick listener using injectElement
659
+ const buttonId = await weaveDOM.injectElement(
660
+ 'body',
661
+ 'beforeend',
662
+ '<button style="position: fixed; top: 10px; right: 10px; z-index: 9999; padding: 10px 20px; background: #4f46e5; color: white; border: none; border-radius: 6px; cursor: pointer; font-family: system-ui;">🚀 Quick Action</button>',
663
+ {
664
+ onClick: (data) => {
665
+ // This callback fires when the button is clicked!
666
+ this.state.clickCount++;
667
+ console.log(`Button clicked! Total clicks: ${this.state.clickCount}`);
668
+ console.log('Click position:', data.event.clientX, data.event.clientY);
669
+
670
+ // Update UI if app is open
671
+ if (this.isConnected) {
672
+ this.render();
673
+ }
674
+
675
+ // You can do anything here:
676
+ // - Call an API
677
+ // - Show a notification
678
+ // - Trigger other actions
679
+ }
680
+ }
681
+ );
682
+
683
+ this.state.buttonInjected = buttonId;
684
+ console.log('✅ Button injected with click listener');
685
+
686
+ // Update UI if app is open
687
+ if (this.isConnected) {
688
+ this.render();
689
+ }
690
+ } else if (!shouldInject && this.state.buttonInjected) {
691
+ // Remove button when URL doesn't match
692
+ await weaveDOM.removeInjectedElement(this.state.buttonInjected);
693
+ this.state.buttonInjected = false;
694
+ console.log('❌ Button removed');
695
+
696
+ // Update UI if app is open
697
+ if (this.isConnected) {
698
+ this.render();
699
+ }
700
+ }
701
+ }
702
+
703
+ // 🎨 Foreground - renders when user opens app
704
+ render() {
705
+ this.renderHTML(`
706
+ <style>
707
+ .status { padding: 20px; font-family: system-ui; }
708
+ .status p { margin: 10px 0; }
709
+ .status strong { color: #4f46e5; }
710
+ </style>
711
+ <div class="status">
712
+ <h2>Background Service Status</h2>
713
+ <p>Button injected: <strong>${this.state.buttonInjected ? 'Yes' : 'No'}</strong></p>
714
+ <p>Click count: <strong>${this.state.clickCount}</strong></p>
715
+ <p><em>Navigate to /dashboard to see button injection</em></p>
716
+ </div>
717
+ `);
718
+ }
719
+ }
720
+
721
+ customElements.define('auto-button-injector', AutoButtonInjector);
722
+ ```
723
+
724
+ ### URL Change Detection
725
+
726
+ #### `onUrlChange(url: string): void` (Optional)
727
+
728
+ Called when the page URL changes (SPA navigation). This detects:
729
+ - `history.pushState()` - SPA route changes
730
+ - `history.replaceState()` - SPA route replacements
731
+ - `popstate` events - Back/forward button navigation
732
+ - `hashchange` events - Hash changes
733
+
734
+ **Use cases:**
735
+ - React to navigation in single-page applications
736
+ - Inject/remove UI elements based on current route
737
+ - Track user navigation patterns
738
+ - Trigger actions on specific pages
739
+
740
+ ```typescript
741
+ class SmartNavigationHelper extends WeaveBaseApp {
742
+ constructor() {
743
+ super({
744
+ id: 'smart-nav-helper',
745
+ name: 'Smart Navigation Helper',
746
+ version: '1.0.0',
747
+ category: 'productivity',
748
+ description: 'Provides contextual help based on current page',
749
+ tags: ['navigation', 'help']
750
+ });
751
+
752
+ this.state = {
753
+ currentPage: '',
754
+ helpBubbleId: null
755
+ };
756
+ }
757
+
758
+ async onBackgroundService() {
759
+ console.log('🔧 Navigation helper started');
760
+ const pageUrl = await weaveDOM.getPageUrl();
761
+ this.handlePageChange(pageUrl);
762
+ }
763
+
764
+ // 🔄 Called on every URL change (SPA navigation)
765
+ onUrlChange(newUrl) {
766
+ console.log('🔄 URL changed to:', newUrl);
767
+ this.handlePageChange(newUrl);
768
+ }
769
+
770
+ handlePageChange(url) {
771
+ // Remove old help bubble if exists
772
+ if (this.state.helpBubbleId) {
773
+ weaveDOM.removeElement(`#${this.state.helpBubbleId}`);
774
+ this.state.helpBubbleId = null;
775
+ }
776
+
777
+ // Determine page type
778
+ let pageType = 'unknown';
779
+ let helpText = '';
780
+
781
+ if (url.includes('/dashboard')) {
782
+ pageType = 'dashboard';
783
+ helpText = '📊 Dashboard: View your analytics and metrics';
784
+ } else if (url.includes('/settings')) {
785
+ pageType = 'settings';
786
+ helpText = '⚙️ Settings: Configure your preferences';
787
+ } else if (url.includes('/profile')) {
788
+ pageType = 'profile';
789
+ helpText = '👤 Profile: Manage your account information';
790
+ }
791
+
792
+ this.state.currentPage = pageType;
793
+
794
+ // Inject contextual help if we have help text
795
+ if (helpText) {
796
+ const helpHTML = `
797
+ <div id="weave-help-bubble" style="
798
+ position: fixed;
799
+ bottom: 20px;
800
+ left: 20px;
801
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
802
+ color: white;
803
+ padding: 15px 20px;
804
+ border-radius: 10px;
805
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
806
+ z-index: 9999;
807
+ max-width: 300px;
808
+ font-family: system-ui;
809
+ font-size: 14px;
810
+ ">${helpText}</div>
811
+ `;
812
+
813
+ weaveDOM.insertHTML('body', helpHTML, 'beforeend');
814
+ this.state.helpBubbleId = 'weave-help-bubble';
815
+ }
816
+
817
+ // Update UI if app is open
818
+ if (this.isConnected) {
819
+ this.render();
820
+ }
821
+ }
822
+
823
+ render() {
824
+ this.renderHTML(`
825
+ <style>
826
+ .nav-status { padding: 20px; font-family: system-ui; }
827
+ .page-badge {
828
+ display: inline-block;
829
+ padding: 5px 10px;
830
+ background: #4f46e5;
831
+ color: white;
832
+ border-radius: 4px;
833
+ font-size: 12px;
834
+ font-weight: 600;
835
+ }
836
+ </style>
837
+ <div class="nav-status">
838
+ <h2>Navigation Helper</h2>
839
+ <p>Current page: <span class="page-badge">${this.state.currentPage}</span></p>
840
+ <p>Help bubble: ${this.state.helpBubbleId ? '✅ Active' : '❌ Inactive'}</p>
841
+ <p><em>Navigate to different pages to see contextual help</em></p>
842
+ </div>
843
+ `);
844
+ }
845
+ }
846
+
847
+ customElements.define('smart-nav-helper', SmartNavigationHelper);
848
+ ```
849
+
850
+ ### Shared State Between Background and Foreground
851
+
852
+ The same app instance serves both background and foreground, so `this.state` is shared:
853
+
854
+ ```typescript
855
+ class SharedStateExample extends WeaveBaseApp {
856
+ constructor() {
857
+ super({
858
+ id: 'shared-state-example',
859
+ name: 'Shared State Example',
860
+ version: '1.0.0',
861
+ category: 'utility',
862
+ description: 'Demonstrates shared state',
863
+ tags: ['example']
864
+ });
865
+
866
+ this.state = {
867
+ backgroundClicks: 0,
868
+ foregroundClicks: 0
869
+ };
870
+ }
871
+
872
+ // Background: Inject button and track clicks
873
+ onBackgroundService() {
874
+ const buttonHTML = `
875
+ <button id="bg-button" style="position: fixed; top: 10px; right: 10px; z-index: 9999; padding: 10px; background: #10b981; color: white; border: none; border-radius: 6px; cursor: pointer;">
876
+ Background Button
877
+ </button>
878
+ `;
879
+
880
+ weaveDOM.insertHTML('body', buttonHTML, 'beforeend');
881
+
882
+ // Listen for clicks on injected button
883
+ // Note: You'd typically use injectElement with onClick callback
884
+ // This is simplified for demonstration
885
+ }
886
+
887
+ // Foreground: Show state from background
888
+ render() {
889
+ this.renderHTML(`
890
+ <style>
891
+ .container { padding: 20px; font-family: system-ui; }
892
+ button { padding: 10px 20px; background: #4f46e5; color: white; border: none; border-radius: 6px; cursor: pointer; margin: 10px 0; }
893
+ </style>
894
+ <div class="container">
895
+ <h2>Shared State Demo</h2>
896
+ <p>Background clicks: <strong>${this.state.backgroundClicks}</strong></p>
897
+ <p>Foreground clicks: <strong>${this.state.foregroundClicks}</strong></p>
898
+ <button id="fg-button">Click Me (Foreground)</button>
899
+ <p><em>State is shared between background and foreground!</em></p>
900
+ </div>
901
+ `);
902
+ }
903
+
904
+ setupEventListeners() {
905
+ this.query('#fg-button')?.addEventListener('click', () => {
906
+ this.state.foregroundClicks++;
907
+ this.render(); // Re-render to show updated count
908
+ });
909
+ }
910
+ }
911
+
912
+ customElements.define('shared-state-example', SharedStateExample);
913
+ ```
914
+
915
+ ### Checking if App is Attached to DOM
916
+
917
+ Use `this.isConnected` to check if the app is currently attached to the DOM (visible in drawer):
918
+
919
+ ```typescript
920
+ onBackgroundService() {
921
+ // Background logic runs
922
+ this.state.data = 'some value';
923
+
924
+ // Only update UI if app is currently open
925
+ if (this.isConnected) {
926
+ this.render();
927
+ }
928
+ }
929
+
930
+ onUrlChange(url) {
931
+ // Update state
932
+ this.state.currentUrl = url;
933
+
934
+ // Re-render if app is open
935
+ if (this.isConnected) {
936
+ this.render();
937
+ }
938
+ }
939
+ ```
940
+
941
+ ### Best Practices
942
+
943
+ #### ✅ DO:
944
+ - Use `onBackgroundService()` for auto-injection and monitoring
945
+ - Use `onUrlChange()` to react to SPA navigation
946
+ - Check `this.isConnected` before calling `render()`
947
+ - Clean up injected elements in `cleanup()`
948
+ - Use shared state for communication between background and foreground
949
+
950
+ #### ❌ DON'T:
951
+ - Don't call `render()` in `onBackgroundService()` (app not attached to DOM yet)
952
+ - Don't assume app is always open when background logic runs
953
+ - Don't forget to remove injected elements when no longer needed
954
+ - Don't inject elements without checking if they already exist
955
+
956
+ ### Complete Example: Smart Form Assistant
957
+
958
+ ```typescript
959
+ class SmartFormAssistant extends WeaveBaseApp {
960
+ constructor() {
961
+ super({
962
+ id: 'smart-form-assistant',
963
+ name: 'Smart Form Assistant',
964
+ version: '1.0.0',
965
+ category: 'productivity',
966
+ description: 'Automatically detects forms and offers AI-powered assistance',
967
+ tags: ['forms', 'ai', 'automation']
968
+ });
969
+
970
+ this.state = {
971
+ formsDetected: 0,
972
+ currentUrl: '',
973
+ assistButtonInjected: false
974
+ };
975
+ }
976
+
977
+ // 🔧 Background: Start monitoring
978
+ async onBackgroundService() {
979
+ console.log('🔧 Form assistant started');
980
+ const pageUrl = await weaveDOM.getPageUrl();
981
+ this.checkForForms(pageUrl);
982
+ }
983
+
984
+ // 🔄 URL changes: Re-check for forms
985
+ onUrlChange(newUrl) {
986
+ console.log('🔄 URL changed, checking for forms');
987
+ this.state.currentUrl = newUrl;
988
+ this.checkForForms(newUrl);
989
+ }
990
+
991
+ async checkForForms(url) {
992
+ // Remove old assist button if exists
993
+ if (this.state.assistButtonInjected) {
994
+ await weaveDOM.removeElement('#form-assist-btn');
995
+ this.state.assistButtonInjected = false;
996
+ }
997
+
998
+ // Query for forms on the page
999
+ try {
1000
+ const forms = await weaveDOM.queryAll('form');
1001
+ this.state.formsDetected = forms.length;
1002
+
1003
+ // If forms exist, inject assist button
1004
+ if (forms.length > 0) {
1005
+ const buttonHTML = `
1006
+ <button id="form-assist-btn" style="
1007
+ position: fixed;
1008
+ bottom: 20px;
1009
+ right: 20px;
1010
+ padding: 15px 25px;
1011
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1012
+ color: white;
1013
+ border: none;
1014
+ border-radius: 8px;
1015
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
1016
+ cursor: pointer;
1017
+ font-size: 16px;
1018
+ font-weight: 600;
1019
+ z-index: 9999;
1020
+ ">🤖 AI Form Help</button>
1021
+ `;
1022
+
1023
+ await weaveDOM.insertHTML('body', buttonHTML, 'beforeend');
1024
+ this.state.assistButtonInjected = true;
1025
+ console.log('✅ Form assist button injected');
1026
+ }
1027
+
1028
+ // Update UI if app is open
1029
+ if (this.isConnected) {
1030
+ this.render();
1031
+ }
1032
+ } catch (error) {
1033
+ console.error('Error checking for forms:', error);
1034
+ }
1035
+ }
1036
+
1037
+ // 🎨 Foreground: Show status
1038
+ render() {
1039
+ this.renderHTML(`
1040
+ <style>
1041
+ .container { padding: 20px; font-family: system-ui; }
1042
+ .stat { margin: 15px 0; }
1043
+ .stat strong { color: #4f46e5; }
1044
+ .badge {
1045
+ display: inline-block;
1046
+ padding: 5px 10px;
1047
+ background: #10b981;
1048
+ color: white;
1049
+ border-radius: 4px;
1050
+ font-size: 12px;
1051
+ font-weight: 600;
1052
+ }
1053
+ </style>
1054
+ <div class="container">
1055
+ <h2>Smart Form Assistant</h2>
1056
+ <div class="stat">
1057
+ <p>Forms detected: <strong>${this.state.formsDetected}</strong></p>
1058
+ </div>
1059
+ <div class="stat">
1060
+ <p>Assist button: ${this.state.assistButtonInjected ? '<span class="badge">Active</span>' : '<span style="color: #6b7280;">Inactive</span>'}</p>
1061
+ </div>
1062
+ <div class="stat">
1063
+ <p>Current URL: <code style="font-size: 12px; color: #6b7280;">${this.state.currentUrl}</code></p>
1064
+ </div>
1065
+ <p style="margin-top: 20px; color: #6b7280; font-size: 14px;">
1066
+ <em>Navigate to pages with forms to see the AI assist button</em>
1067
+ </p>
1068
+ </div>
1069
+ `);
1070
+ }
1071
+
1072
+ // 🧹 Cleanup: Remove injected elements
1073
+ async cleanup() {
1074
+ if (this.state.assistButtonInjected) {
1075
+ await weaveDOM.removeElement('#form-assist-btn');
1076
+ }
1077
+ }
1078
+ }
1079
+
1080
+ customElements.define('smart-form-assistant', SmartFormAssistant);
1081
+ ```
1082
+
1083
+ ### Key Takeaways
1084
+
1085
+ 1. **Single Instance Architecture**: One app instance serves both background and foreground
1086
+ 2. **Persistent State**: `this.state` persists across open/close cycles
1087
+ 3. **Background Service**: `onBackgroundService()` runs immediately, before DOM attachment
1088
+ 4. **URL Monitoring**: `onUrlChange()` detects SPA navigation automatically
1089
+ 5. **Shared Context**: Background and foreground share the same instance and state
1090
+ 6. **DOM Check**: Use `this.isConnected` to check if app is attached to DOM
1091
+ 7. **No User Interaction Required**: Apps can run and inject UI automatically
1092
+
1093
+ ### Helper Methods (Available in `this`)
1094
+
1095
+ ```typescript
1096
+ // Render HTML into shadow root
1097
+ this.renderHTML(html: string): void
1098
+
1099
+ // Query single element in shadow root
1100
+ this.query<T>(selector: string): T | null
1101
+
1102
+ // Query all elements in shadow root
1103
+ this.queryAll<T>(selector: string): NodeListOf<T>
1104
+
1105
+ // Update app state
1106
+ this.setState(updates: object): void
1107
+
1108
+ // Access current state
1109
+ this.state: Record<string, any>
1110
+
1111
+ // Access app metadata
1112
+ this.appInfo: WeaveAppInfo
1113
+
1114
+ // Access shadow root
1115
+ this.shadowRoot: ShadowRoot
1116
+ ```
1117
+
1118
+ ## Weave Backend API
1119
+
1120
+ ### ⚠️ CRITICAL: API Access Restrictions
1121
+
1122
+ Apps **CANNOT** make arbitrary HTTP requests to external APIs. Apps can **ONLY** interact with:
1123
+
1124
+ 1. **Weave Backend API** via `window.weaveAPI` (AI, data storage)
1125
+ 2. **Parent Page DOM** via `window.weaveDOM` (read/write page elements)
1126
+
1127
+ ❌ **BLOCKED:**
1128
+ ```typescript
1129
+ // ❌ WRONG - Cannot make arbitrary API calls
1130
+ await fetch('https://api.example.com/data');
1131
+ await axios.get('https://some-api.com');
1132
+ ```
1133
+
1134
+ ✅ **ALLOWED:**
1135
+ ```typescript
1136
+ // ✅ CORRECT - Use Weave API
1137
+ await weaveAPI.ai.chat({ appName: 'my-app', prompt: '...' });
1138
+ await weaveAPI.appData.create({ appId: 'my-app', dataKey: 'key', data: {} });
1139
+
1140
+ // ✅ CORRECT - Use DOM Bridge
1141
+ await weaveDOM.getText('h1');
1142
+ ```
1143
+
1144
+ ### Weave API Client
1145
+
1146
+ Access Weave backend services via `window.weaveAPI`:
1147
+
1148
+ ```typescript
1149
+ // Available globally at runtime
1150
+ const weaveAPI = window.weaveAPI;
1151
+ ```
1152
+
1153
+ ### ⚠️ CRITICAL: App-Specific API Client (`this.weaveAPI`)
1154
+
1155
+ **IMPORTANT:** Each app instance has its own dedicated API client accessible via `this.weaveAPI`. This ensures that API calls are made with the correct app ID, especially critical for background services.
1156
+
1157
+ #### Why Use `this.weaveAPI`?
1158
+
1159
+ When multiple apps run simultaneously (especially as background services), using the global `window.weaveAPI` can cause **app ID conflicts**. Each app needs its own API client instance to ensure data is saved/loaded with the correct app ID.
1160
+
1161
+ #### ✅ CORRECT: Use `this.weaveAPI`
1162
+
1163
+ ```typescript
1164
+ class MyApp extends WeaveBaseApp {
1165
+ async saveData() {
1166
+ // ✅ CORRECT - Uses app-specific API client
1167
+ const data = await this.weaveAPI.appData.create({
1168
+ dataKey: 'my-data',
1169
+ data: { value: 'hello' }
1170
+ });
1171
+ }
1172
+
1173
+ async loadData() {
1174
+ // ✅ CORRECT - Uses app-specific API client
1175
+ const response = await this.weaveAPI.appData.getAll();
1176
+ const allData = response.data;
1177
+ return allData.find(d => d.dataKey === 'my-data');
1178
+ }
1179
+
1180
+ async askAI() {
1181
+ // ✅ CORRECT - Uses app-specific API client
1182
+ const response = await this.weaveAPI.ai.chat({
1183
+ prompt: 'Hello AI',
1184
+ context: 'User greeting'
1185
+ });
1186
+ return response.response;
1187
+ }
1188
+ }
1189
+ ```
1190
+
1191
+ #### ❌ WRONG: Using Global `window.weaveAPI`
1192
+
1193
+ ```typescript
1194
+ class MyApp extends WeaveBaseApp {
1195
+ async saveData() {
1196
+ // ❌ WRONG - May use wrong app ID if multiple apps are running
1197
+ const data = await window.weaveAPI.appData.create({
1198
+ dataKey: 'my-data',
1199
+ data: { value: 'hello' }
1200
+ });
1201
+ }
1202
+ }
1203
+ ```
1204
+
1205
+ #### Context Preservation in Async Methods
1206
+
1207
+ When using `await` with `this.weaveAPI`, the context can be lost in certain scenarios (especially in callbacks). To prevent this, **capture the API client reference in a local variable**:
1208
+
1209
+ ```typescript
1210
+ class MyApp extends WeaveBaseApp {
1211
+ async saveContent(content: string) {
1212
+ try {
1213
+ // ✅ CORRECT - Capture reference to avoid context loss
1214
+ const apiClient = this.weaveAPI;
1215
+
1216
+ // Now use apiClient consistently
1217
+ const response = await apiClient.appData.getAll();
1218
+ const allData = response.data;
1219
+ const existing = allData.find(d => d.dataKey === 'content');
1220
+
1221
+ if (existing) {
1222
+ await apiClient.appData.update(existing._id, {
1223
+ data: { content }
1224
+ });
1225
+ } else {
1226
+ await apiClient.appData.create({
1227
+ dataKey: 'content',
1228
+ data: { content }
1229
+ });
1230
+ }
1231
+ } catch (error) {
1232
+ console.error('Save failed:', error);
1233
+ }
1234
+ }
1235
+ }
1236
+ ```
1237
+
1238
+ #### Context Preservation in Callbacks
1239
+
1240
+ When using callbacks (e.g., button click handlers via DOM Bridge), capture both `this` and the API client:
1241
+
1242
+ ```typescript
1243
+ class MyApp extends WeaveBaseApp {
1244
+ async injectButton() {
1245
+ // ✅ CORRECT - Capture 'this' reference for callback
1246
+ const self = this;
1247
+
1248
+ const buttonId = await window.weaveDOM.injectElement(
1249
+ 'body',
1250
+ 'beforeend',
1251
+ '<button>Save Data</button>',
1252
+ {
1253
+ onClick: async () => {
1254
+ // Use captured 'self' reference
1255
+ const apiClient = self.weaveAPI;
1256
+
1257
+ await apiClient.appData.create({
1258
+ dataKey: 'clicked',
1259
+ data: { timestamp: Date.now() }
1260
+ });
1261
+
1262
+ // Update UI if app is open
1263
+ if (self.isConnected) {
1264
+ self.render();
1265
+ }
1266
+ }
1267
+ }
1268
+ );
1269
+ }
1270
+ }
1271
+ ```
1272
+
1273
+ #### Key Rules for API Client Usage
1274
+
1275
+ 1. **Always use `this.weaveAPI`** instead of `window.weaveAPI`
1276
+ 2. **Capture in local variable** when using multiple `await` calls: `const apiClient = this.weaveAPI;`
1277
+ 3. **Capture `this` in callbacks** using `const self = this;` before async callbacks
1278
+ 4. **Use captured references** consistently throughout the method
1279
+ 5. **Never mix** `this.weaveAPI` and `window.weaveAPI` in the same app
1280
+
1281
+ #### How It Works
1282
+
1283
+ When your app is instantiated, `WeaveBaseApp` automatically:
1284
+ 1. Creates a dedicated `WeaveAPIClient` instance for your app
1285
+ 2. Sets the correct app UUID on this instance
1286
+ 3. Assigns it to `this.weaveAPI`
1287
+
1288
+ This ensures all API calls from your app include the correct app ID, preventing data from being saved to the wrong app.
1289
+
1290
+ ### AI Service
1291
+
1292
+ Call the Weave AI service to get intelligent responses:
1293
+
1294
+ **Note:** Your app's UUID is automatically included. Always use `this.weaveAPI` in your app methods.
1295
+
1296
+ ```typescript
1297
+ import { type AIChatRequest, type AIChatResponse } from '@weave/app-sdk';
1298
+
1299
+ class MyApp extends WeaveBaseApp {
1300
+ async summarizeText(text: string) {
1301
+ // ✅ Use this.weaveAPI (app-specific client)
1302
+ const request: AIChatRequest = {
1303
+ prompt: `Summarize this text: ${text}`,
1304
+ context: 'User is reading an article',
1305
+ disableJsonExtraction: false
1306
+ };
1307
+
1308
+ const response: AIChatResponse = await this.weaveAPI.ai.chat(request);
1309
+ return response.response; // AI's response text
1310
+ }
1311
+ }
1312
+ ```
1313
+
1314
+ **Example Use Cases:**
1315
+ - Summarize page content
1316
+ - Answer questions about data
1317
+ - Generate suggestions
1318
+ - Analyze text
1319
+ - Extract information
1320
+
1321
+ ### App Data Service
1322
+
1323
+ Store and retrieve app-specific data in the Weave backend:
1324
+
1325
+ ```typescript
1326
+ import {
1327
+ type AppData,
1328
+ type CreateAppDataRequest,
1329
+ type UpdateAppDataRequest,
1330
+ type PaginatedResponse,
1331
+ type PaginationMeta
1332
+ } from '@weave/app-sdk';
1333
+ ```
1334
+
1335
+ #### Create Data
1336
+
1337
+ **Note:** Your app's UUID is automatically included. Always use `this.weaveAPI`.
1338
+
1339
+ ```typescript
1340
+ class MyApp extends WeaveBaseApp {
1341
+ async savePreferences(theme: string, language: string) {
1342
+ // ✅ Use this.weaveAPI (app-specific client)
1343
+ const request: CreateAppDataRequest = {
1344
+ dataKey: 'user-preferences',
1345
+ data: {
1346
+ theme,
1347
+ language,
1348
+ notifications: true
1349
+ }
1350
+ };
1351
+
1352
+ const saved: AppData = await this.weaveAPI.appData.create(request);
1353
+ console.log('Created with ID:', saved._id);
1354
+ return saved;
1355
+ }
1356
+ }
1357
+ ```
1358
+
1359
+ #### Get All Data (Paginated)
1360
+
1361
+ **Important:** The API returns paginated responses to handle large datasets efficiently (500-5000+ rows).
1362
+
1363
+ ```typescript
1364
+ class MyApp extends WeaveBaseApp {
1365
+ async loadPreferences() {
1366
+ // ✅ Use this.weaveAPI (app-specific client)
1367
+ // Get all data for your app (scoped to current company/user)
1368
+ // Returns: { data: AppData[], meta: { offset, limit, totalResultCount } }
1369
+ const response: PaginatedResponse<AppData> = await this.weaveAPI.appData.getAll();
1370
+
1371
+ // Access the array of items from response.data
1372
+ const allData = response.data;
1373
+
1374
+ // Check pagination metadata
1375
+ console.log('Total items:', response.meta.totalResultCount);
1376
+ console.log('Current page limit:', response.meta.limit);
1377
+ console.log('Current offset:', response.meta.offset);
1378
+
1379
+ // Find specific data by key
1380
+ const prefs = allData.find(d => d.dataKey === 'user-preferences');
1381
+ return prefs?.data;
1382
+ }
1383
+ }
1384
+ ```
1385
+
1386
+ **Pagination Details:**
1387
+ - **Default limit:** 25 items per page
1388
+ - **Response structure:** `{ data: AppData[], meta: PaginationMeta }`
1389
+ - **Why paginated:** Apps can store 500-5000+ rows of data. Pagination prevents performance issues and reduces memory usage.
1390
+ - **Access data:** Always use `response.data` to get the array of items
1391
+
1392
+ #### Get Specific Data
1393
+
1394
+ ```typescript
1395
+ class MyApp extends WeaveBaseApp {
1396
+ async getDataById(id: string) {
1397
+ // ✅ Use this.weaveAPI (app-specific client)
1398
+ const data: AppData = await this.weaveAPI.appData.get(id);
1399
+ return data.data; // Your stored data object
1400
+ }
1401
+ }
1402
+ ```
1403
+
1404
+ #### Update Data
1405
+
1406
+ ```typescript
1407
+ class MyApp extends WeaveBaseApp {
1408
+ async updateTheme(id: string, theme: string) {
1409
+ // ✅ Use this.weaveAPI (app-specific client)
1410
+ const updates: UpdateAppDataRequest = {
1411
+ data: {
1412
+ theme // Partial or full update
1413
+ }
1414
+ };
1415
+
1416
+ const updated: AppData = await this.weaveAPI.appData.update(id, updates);
1417
+ return updated;
1418
+ }
1419
+ }
1420
+ ```
1421
+
1422
+ #### Delete Data
1423
+
1424
+ ```typescript
1425
+ class MyApp extends WeaveBaseApp {
1426
+ async deleteData(id: string) {
1427
+ // ✅ Use this.weaveAPI (app-specific client)
1428
+ await this.weaveAPI.appData.delete(id);
1429
+ }
1430
+ }
1431
+ ```
1432
+
1433
+ ### AppData Structure
1434
+
1435
+ ```typescript
1436
+ interface AppData {
1437
+ _id: string; // Unique ID
1438
+ appId: string; // Your app's ID
1439
+ companyId: string; // Auto-injected (user's company)
1440
+ userId: string; // Auto-injected (current user)
1441
+ dataKey: string; // Your organization key
1442
+ data: Record<string, any>; // Your data (any structure)
1443
+ createdAt: Date;
1444
+ updatedAt: Date;
1445
+ createdBy: string;
1446
+ }
1447
+ ```
1448
+
1449
+ ### API Error Handling
1450
+
1451
+ All API methods return promises. Always use try-catch:
1452
+
1453
+ ```typescript
1454
+ try {
1455
+ const response = await weaveAPI.ai.chat({
1456
+ appName: 'my-app',
1457
+ prompt: 'Hello'
1458
+ });
1459
+ console.log(response.response);
1460
+ } catch (error) {
1461
+ console.error('API call failed:', error.message);
1462
+ // Show user-friendly error message
1463
+ }
1464
+ ```
1465
+
1466
+ ### API Timeouts
1467
+
1468
+ - All API requests timeout after **30 seconds**
1469
+ - Requests will reject with timeout error if exceeded
1470
+
1471
+ ### Security & Scoping
1472
+
1473
+ - **Authentication**: Automatic (uses current user's session)
1474
+ - **Company Scoping**: Data is automatically scoped to user's company
1475
+ - **User Scoping**: Each user sees only their own data
1476
+ - **App Scoping**: Apps can only access their own data (via `appId`)
1477
+
1478
+ ## Parent Page DOM Interaction
1479
+
1480
+ ### DOMBridge API
1481
+
1482
+ Access parent page DOM via `window.weaveDOM`:
1483
+
1484
+ ```typescript
1485
+ // All methods return Promises
1486
+ const weaveDOM = window.weaveDOM;
1487
+ ```
1488
+
1489
+ ### Read Operations
1490
+
1491
+ ```typescript
1492
+ // Query element (returns serialized snapshot)
1493
+ const element = await weaveDOM.query('h1');
1494
+ // Returns: { tagName, id, className, textContent, attributes, exists }
1495
+
1496
+ // Query all elements
1497
+ const elements = await weaveDOM.queryAll('.item');
1498
+ // Returns: Array of element snapshots
1499
+
1500
+ // Get text content
1501
+ const text = await weaveDOM.getText('h1');
1502
+ // Returns: string
1503
+
1504
+ // Get attribute value
1505
+ const href = await weaveDOM.getAttribute('a', 'href');
1506
+ // Returns: string | null
1507
+
1508
+ // Get input/textarea value
1509
+ const value = await weaveDOM.getValue('input[name="email"]');
1510
+ // Returns: string
1511
+
1512
+ // Check if element has class
1513
+ const hasClass = await weaveDOM.hasClass('.button', 'active');
1514
+ // Returns: boolean
1515
+
1516
+ // Get current page URL
1517
+ const pageUrl = await weaveDOM.getPageUrl();
1518
+ // Returns: string (e.g., 'https://example.com/dashboard')
1519
+ ```
1520
+
1521
+ **Important:** Apps run in an iframe, so `window.location.href` returns the iframe URL, not the parent page URL. Use `weaveDOM.getPageUrl()` to get the actual page URL.
1522
+
1523
+ ### Write Operations
1524
+
1525
+ ```typescript
1526
+ // Set text content
1527
+ await weaveDOM.setText('h1', 'New Title');
1528
+
1529
+ // Set attribute
1530
+ await weaveDOM.setAttribute('input', 'placeholder', 'Enter text');
1531
+
1532
+ // Set input/textarea value
1533
+ await weaveDOM.setValue('input[name="email"]', 'user@example.com');
1534
+
1535
+ // Add CSS class
1536
+ await weaveDOM.addClass('.button', 'active');
1537
+
1538
+ // Remove CSS class
1539
+ await weaveDOM.removeClass('.button', 'active');
1540
+
1541
+ // Toggle CSS class
1542
+ await weaveDOM.toggleClass('.button', 'active');
1543
+
1544
+ // Set inline style
1545
+ await weaveDOM.setStyle('h1', 'color', 'red');
1546
+ await weaveDOM.setStyle('h1', 'font-size', '24px');
1547
+ ```
1548
+
1549
+ ### DOM Manipulation
1550
+
1551
+ ```typescript
1552
+ // Insert HTML
1553
+ await weaveDOM.insertHTML(
1554
+ '.container', // Target selector
1555
+ '<p>New content</p>', // HTML to insert
1556
+ 'beforeend' // Position: beforebegin | afterbegin | beforeend | afterend
1557
+ );
1558
+
1559
+ // Remove element
1560
+ await weaveDOM.removeElement('.old-element');
1561
+
1562
+ // Click an element
1563
+ await weaveDOM.clickElement('button#submit');
1564
+ await weaveDOM.clickElement('a.nav-item[href="/dashboard"]');
1565
+ await weaveDOM.clickElement('.custom-button');
1566
+ ```
1567
+
1568
+ **Click Element Use Cases:**
1569
+ - Trigger button clicks programmatically
1570
+ - Navigate by clicking links
1571
+ - Activate UI controls (tabs, toggles, etc.)
1572
+ - Automate user interactions
1573
+ - Trigger form submissions
1574
+
1575
+ **Example - Auto-click a button after form fill:**
1576
+ ```typescript
1577
+ // Fill form fields
1578
+ await weaveDOM.setFormFieldValue('input[name="email"]', 'user@example.com');
1579
+ await weaveDOM.setFormFieldValue('input[name="password"]', 'password123');
1580
+
1581
+ // Click the submit button
1582
+ await weaveDOM.clickElement('button[type="submit"]');
1583
+ ```
1584
+
1585
+ ### Form Detection & Auto-Fill
1586
+
1587
+ The DOM Bridge provides powerful form interaction capabilities for detecting form clicks and automatically filling form fields.
1588
+
1589
+ #### Form Click Detection
1590
+
1591
+ Listen for clicks on form elements and receive complete form data:
1592
+
1593
+ ```typescript
1594
+ // Start listening for form clicks
1595
+ await weaveDOM.startFormClickListener((data) => {
1596
+ console.log('Form clicked!', data);
1597
+
1598
+ // Access form metadata
1599
+ const formData = data.formData;
1600
+ console.log('Form ID:', formData.formId);
1601
+ console.log('Form Name:', formData.formName);
1602
+ console.log('Form Action:', formData.formAction);
1603
+ console.log('Form Method:', formData.formMethod);
1604
+
1605
+ // Access all form fields
1606
+ formData.fields.forEach(field => {
1607
+ console.log(`Field: ${field.name} (${field.type})`);
1608
+ console.log(` Label: ${field.label}`);
1609
+ console.log(` Value: ${field.value}`);
1610
+ console.log(` Required: ${field.required}`);
1611
+ });
1612
+
1613
+ // Access clicked element info
1614
+ console.log('Clicked:', data.clickedElement.tagName);
1615
+ console.log('Type:', data.clickedElement.type);
1616
+ });
1617
+
1618
+ // Stop listening when done
1619
+ await weaveDOM.stopFormClickListener();
1620
+ ```
1621
+
1622
+ **Callback Data Structure:**
1623
+ ```typescript
1624
+ {
1625
+ formData: {
1626
+ formId: string; // Form's ID attribute
1627
+ formName: string; // Form's name attribute
1628
+ formAction: string; // Form's action URL
1629
+ formMethod: string; // Form's method (get/post)
1630
+ fields: FormFieldData[]; // All form fields
1631
+ },
1632
+ clickedElement: {
1633
+ tagName: string; // Element tag (input, select, etc.)
1634
+ name: string; // Element name attribute
1635
+ id: string; // Element ID
1636
+ type: string; // Input type (text, email, etc.)
1637
+ }
1638
+ }
1639
+ ```
1640
+
1641
+ **Form Field Data:**
1642
+ ```typescript
1643
+ interface FormFieldData {
1644
+ name: string; // Field name attribute
1645
+ id: string; // Field ID attribute
1646
+ type: string; // Input type (text, email, checkbox, toggleButtonGroup, etc.)
1647
+ value: string | string[]; // Current value(s)
1648
+ label: string; // Associated label text
1649
+ placeholder: string; // Placeholder text
1650
+ required: boolean; // Is field required?
1651
+ disabled: boolean; // Is field disabled?
1652
+ readonly: boolean; // Is field readonly?
1653
+ pattern: string; // Validation pattern
1654
+ min: string; // Min value (numbers/dates)
1655
+ max: string; // Max value (numbers/dates)
1656
+ minLength: number; // Min length
1657
+ maxLength: number; // Max length
1658
+ checked?: boolean; // For checkboxes/radios
1659
+ options?: Array<{ // For select elements
1660
+ value: string;
1661
+ text: string;
1662
+ selected: boolean;
1663
+ }>;
1664
+ buttons?: Array<{ // For toggle button groups (MUI, etc.)
1665
+ value: string;
1666
+ text: string;
1667
+ selected: boolean;
1668
+ id: string;
1669
+ }>;
1670
+ }
1671
+ ```
1672
+
1673
+ #### Manual Form Data Retrieval
1674
+
1675
+ Get form data without waiting for a click:
1676
+
1677
+ ```typescript
1678
+ // Get data from a specific form
1679
+ const formData = await weaveDOM.getFormData('#login-form');
1680
+
1681
+ // Get data from a form containing a specific input
1682
+ const formData = await weaveDOM.getFormData('input[name="email"]');
1683
+ ```
1684
+
1685
+ #### Auto-Fill Form Fields
1686
+
1687
+ Set form field values programmatically. Automatically triggers validation events (`input`, `change`, `blur`):
1688
+
1689
+ ```typescript
1690
+ // Text inputs
1691
+ await weaveDOM.setFormFieldValue('input[name="email"]', 'user@example.com');
1692
+ await weaveDOM.setFormFieldValue('#username', 'johndoe');
1693
+
1694
+ // Number inputs
1695
+ await weaveDOM.setFormFieldValue('input[name="age"]', '25');
1696
+
1697
+ // Checkboxes (use boolean)
1698
+ await weaveDOM.setFormFieldValue('input[name="terms"]', true);
1699
+ await weaveDOM.setFormFieldValue('input[name="newsletter"]', false);
1700
+
1701
+ // Radio buttons (use the value of the radio to select)
1702
+ await weaveDOM.setFormFieldValue('input[name="gender"][value="female"]', 'female');
1703
+
1704
+ // Select dropdowns
1705
+ await weaveDOM.setFormFieldValue('select[name="country"]', 'US');
1706
+
1707
+ // Multi-select (use array)
1708
+ await weaveDOM.setFormFieldValue('select[name="interests"]', ['sports', 'music']);
1709
+
1710
+ // Textareas
1711
+ await weaveDOM.setFormFieldValue('textarea[name="bio"]', 'This is my bio...');
1712
+
1713
+ // Date inputs
1714
+ await weaveDOM.setFormFieldValue('input[name="birthdate"]', '1990-01-15');
1715
+
1716
+ // Toggle button groups (MUI, Ant Design, etc.)
1717
+ await weaveDOM.setFormFieldValue('#other_creators_involved', 'true');
1718
+
1719
+ // For IDs with dots, use attribute selector instead of escaping
1720
+ await weaveDOM.setFormFieldValue('[id="dependency_level.level"]', 'High');
1721
+ ```
1722
+
1723
+ **Supported Field Types:**
1724
+ - Text inputs: `text`, `email`, `password`, `tel`, `url`, `search`
1725
+ - Number inputs: `number`, `range`
1726
+ - Date/time inputs: `date`, `datetime-local`, `time`, `month`, `week`
1727
+ - Checkboxes: `checkbox` (use boolean values)
1728
+ - Radio buttons: `radio` (use string value to select)
1729
+ - Select elements: single and multi-select
1730
+ - Textareas
1731
+ - **Toggle button groups**: `toggleButtonGroup` (MUI ToggleButtonGroup, custom button groups)
1732
+
1733
+ **Event Triggering:**
1734
+ The `setFormFieldValue` method automatically triggers these events to ensure page validation runs:
1735
+ - `input` event
1736
+ - `change` event
1737
+ - `blur` event
1738
+ - `InputEvent` (for modern frameworks)
1739
+ - **Button clicks** (for toggle button groups)
1740
+
1741
+ **Visual Feedback with Scroll Into View:**
1742
+
1743
+ The `setFormFieldValue` method accepts an optional third parameter `scrollIntoView` that scrolls the element to the center of the viewport before setting its value. This provides visual feedback to users, showing them what's being filled in real-time.
1744
+
1745
+ ```typescript
1746
+ // Scroll element to center before filling (great for AI form filling)
1747
+ await weaveDOM.setFormFieldValue('input[name="email"]', 'user@example.com', true);
1748
+
1749
+ // Scroll checkbox into view before checking it
1750
+ await weaveDOM.setFormFieldValue('input[name="terms"]', true, true);
1751
+
1752
+ // Default behavior - no scroll (backward compatible)
1753
+ await weaveDOM.setFormFieldValue('input[name="username"]', 'johndoe');
1754
+ ```
1755
+
1756
+ **How it works:**
1757
+ 1. When `scrollIntoView: true` is passed, the element scrolls to the **center** of the viewport (not top)
1758
+ 2. Uses smooth animation for better UX
1759
+ 3. Waits 300ms for scroll animation to complete
1760
+ 4. Then sets the field value and triggers events
1761
+
1762
+ **Use cases:**
1763
+ - **AI form filling** - Show users what fields are being filled as the AI works
1764
+ - **Long forms** - Ensure each field is visible as it's being filled
1765
+ - **User guidance** - Draw attention to specific fields being auto-filled
1766
+ - **Debugging** - Visually verify which fields are being set
1767
+
1768
+ ```typescript
1769
+ // Example: Fill form with visual feedback
1770
+ async function fillFormWithVisualFeedback(formData, values) {
1771
+ for (const field of formData.fields) {
1772
+ const selector = field.id ? `#${field.id}` : `[name="${field.name}"]`;
1773
+ const value = values[field.name];
1774
+
1775
+ if (value !== undefined) {
1776
+ // Scroll to show user what's being filled
1777
+ await weaveDOM.setFormFieldValue(selector, value, true);
1778
+
1779
+ // Optional: small delay between fields for better UX
1780
+ await new Promise(resolve => setTimeout(resolve, 200));
1781
+ }
1782
+ }
1783
+ }
1784
+ ```
1785
+
1786
+ #### Complete Example: AI Form Filler
1787
+
1788
+ ```typescript
1789
+ class AIFormFiller extends WeaveBaseApp {
1790
+ constructor() {
1791
+ super({
1792
+ id: 'ai-form-filler',
1793
+ name: 'AI Form Filler',
1794
+ version: '1.0.0',
1795
+ category: 'productivity',
1796
+ description: 'Automatically fills forms using AI',
1797
+ tags: ['forms', 'ai', 'automation']
1798
+ });
1799
+
1800
+ this.state = {
1801
+ formData: null,
1802
+ filling: false
1803
+ };
1804
+ }
1805
+
1806
+ async connectedCallback() {
1807
+ super.connectedCallback();
1808
+
1809
+ // Listen for form clicks
1810
+ await window.weaveDOM.startFormClickListener(async (data) => {
1811
+ this.state.formData = data.formData;
1812
+ this.render();
1813
+
1814
+ // Automatically fill the form
1815
+ await this.fillFormWithAI(data.formData);
1816
+ });
1817
+ }
1818
+
1819
+ async disconnectedCallback() {
1820
+ super.disconnectedCallback();
1821
+ await window.weaveDOM.stopFormClickListener();
1822
+ }
1823
+
1824
+ async fillFormWithAI(formData) {
1825
+ this.state.filling = true;
1826
+ this.render();
1827
+
1828
+ try {
1829
+ // Get AI-generated values for each field
1830
+ const aiValues = await this.getAIValues(formData);
1831
+
1832
+ // Fill each field with visual feedback
1833
+ for (const field of formData.fields) {
1834
+ if (aiValues[field.name]) {
1835
+ // Construct selector from field ID or name
1836
+ const selector = field.id
1837
+ ? `#${field.id}`
1838
+ : `[name="${field.name}"]`;
1839
+
1840
+ // Set the value with scroll for visual feedback
1841
+ await window.weaveDOM.setFormFieldValue(
1842
+ selector,
1843
+ aiValues[field.name],
1844
+ true // Scroll into view so user can see what's being filled
1845
+ );
1846
+
1847
+ // Optional: small delay between fields for better UX
1848
+ await new Promise(resolve => setTimeout(resolve, 200));
1849
+ }
1850
+ }
1851
+
1852
+ this.showSuccess('Form filled successfully!');
1853
+ } catch (error) {
1854
+ this.showError(`Error: ${error.message}`);
1855
+ } finally {
1856
+ this.state.filling = false;
1857
+ this.render();
1858
+ }
1859
+ }
1860
+
1861
+ async getAIValues(formData) {
1862
+ // Use Weave AI to generate form values
1863
+ const prompt = `Generate appropriate values for this form:
1864
+ ${JSON.stringify(formData.fields.map(f => ({
1865
+ name: f.name,
1866
+ type: f.type,
1867
+ label: f.label,
1868
+ required: f.required
1869
+ })))}`;
1870
+
1871
+ const response = await window.weaveAPI.ai.chat({
1872
+ prompt,
1873
+ context: 'Filling form fields with appropriate test data'
1874
+ });
1875
+
1876
+ // Parse AI response to get field values
1877
+ return JSON.parse(response.response);
1878
+ }
1879
+ }
1880
+
1881
+ customElements.define('ai-form-filler', AIFormFiller);
1882
+ ```
1883
+
1884
+ #### Use Cases
1885
+
1886
+ **AI-Powered Autofill:**
1887
+ - User clicks a form field
1888
+ - App sends form structure to AI
1889
+ - AI generates appropriate values
1890
+ - App fills all fields automatically
1891
+
1892
+ **Form Validation Helper:**
1893
+ - Detect form interactions
1894
+ - Validate fields in real-time
1895
+ - Show helpful suggestions
1896
+ - Auto-correct common mistakes
1897
+
1898
+ **Data Extraction:**
1899
+ - Capture form structures
1900
+ - Analyze form patterns
1901
+ - Export form data
1902
+ - Generate form documentation
1903
+
1904
+ **Automated Testing:**
1905
+ - Fill forms with test data
1906
+ - Verify form behavior
1907
+ - Test validation rules
1908
+ - Simulate user interactions
1909
+
1910
+ #### Toggle Button Groups (MUI & Custom Controls)
1911
+
1912
+ Many modern frameworks (Material-UI, Ant Design, etc.) use **button-based form controls** instead of traditional radio buttons or checkboxes. These are automatically detected and extracted by the DOM Bridge.
1913
+
1914
+ **What are Toggle Button Groups?**
1915
+
1916
+ Toggle button groups are `<div role="group">` containers with multiple `<button type="button">` elements that act like radio buttons. Selection is managed through:
1917
+ - `aria-pressed="true"` attribute
1918
+ - CSS classes like `Mui-selected`
1919
+ - React/framework event handlers
1920
+
1921
+ **Example HTML Structure:**
1922
+ ```html
1923
+ <div role="group" id="dependency_level.level" aria-label="dependency_level.level">
1924
+ <button type="button" value="Low" aria-pressed="true" class="Mui-selected">Low</button>
1925
+ <button type="button" value="Medium" aria-pressed="false">Medium</button>
1926
+ <button type="button" value="High" aria-pressed="false">High</button>
1927
+ </div>
1928
+ ```
1929
+
1930
+ **Extracted Form Data:**
1931
+ ```typescript
1932
+ {
1933
+ name: "dependency_level.level",
1934
+ type: "toggleButtonGroup", // Special type for button groups
1935
+ value: "Low", // Currently selected value
1936
+ label: "Dependency Level",
1937
+ buttons: [ // All available options
1938
+ { value: "Low", text: "Low", selected: true, id: "dependency_level.level-Low" },
1939
+ { value: "Medium", text: "Medium", selected: false, id: "dependency_level.level-Medium" },
1940
+ { value: "High", text: "High", selected: false, id: "dependency_level.level-High" }
1941
+ ]
1942
+ }
1943
+ ```
1944
+
1945
+ **How to Use in Your App:**
1946
+
1947
+ 1. **Recognize as choice field** - Similar to radio buttons or select dropdowns
1948
+ 2. **Use `buttons` array** - To see all available options
1949
+ 3. **Set values by button value** - Use the `value` attribute, not the `text`
1950
+
1951
+ ```typescript
1952
+ // AI analyzes the field
1953
+ const field = formData.fields.find(f => f.type === 'toggleButtonGroup');
1954
+
1955
+ if (field) {
1956
+ console.log('Available options:', field.buttons.map(b => b.text));
1957
+ // Output: ['Low', 'Medium', 'High']
1958
+
1959
+ console.log('Current selection:', field.value);
1960
+ // Output: 'Low'
1961
+
1962
+ // AI decides to select 'High'
1963
+ await weaveDOM.setFormFieldValue(`#${field.id}`, 'High');
1964
+ // This clicks the "High" button, triggering React handlers
1965
+ }
1966
+ ```
1967
+
1968
+ **Setting Values:**
1969
+
1970
+ When you set a value on a toggle button group, the DOM Bridge:
1971
+ 1. Finds the group by selector
1972
+ 2. Searches for the button with matching `value` attribute
1973
+ 3. **Clicks the button** to trigger framework event handlers
1974
+ 4. Framework updates UI and state automatically
1975
+
1976
+ ```typescript
1977
+ // Yes/No toggle buttons
1978
+ await weaveDOM.setFormFieldValue('#other_creators_involved', 'true');
1979
+
1980
+ // Multi-option toggle buttons with dots in ID
1981
+ // Use attribute selector for IDs with special characters (dots, colons, etc.)
1982
+ await weaveDOM.setFormFieldValue('[id="dependency_level.level"]', 'High');
1983
+ ```
1984
+
1985
+ **Common Patterns:**
1986
+
1987
+ ```typescript
1988
+ // Yes/No questions (boolean-like)
1989
+ {
1990
+ type: "toggleButtonGroup",
1991
+ value: "true", // or "false"
1992
+ buttons: [
1993
+ { value: "true", text: "Yes", selected: false },
1994
+ { value: "false", text: "No", selected: false }
1995
+ ]
1996
+ }
1997
+
1998
+ // Multiple choice (Low/Medium/High)
1999
+ {
2000
+ type: "toggleButtonGroup",
2001
+ value: "Medium",
2002
+ buttons: [
2003
+ { value: "Low", text: "Low", selected: false },
2004
+ { value: "Medium", text: "Medium", selected: true },
2005
+ { value: "High", text: "High", selected: false }
2006
+ ]
2007
+ }
2008
+ ```
2009
+
2010
+ **AI Decision Making:**
2011
+
2012
+ ```typescript
2013
+ // AI analyzes user needs and selects appropriate option
2014
+ async function fillDependencyLevel(userNeeds, formData) {
2015
+ const field = formData.fields.find(f => f.name === 'dependency_level.level');
2016
+
2017
+ if (field.type === 'toggleButtonGroup') {
2018
+ // AI sees available options
2019
+ const options = field.buttons.map(b => b.value);
2020
+ // ['Low', 'Medium', 'High']
2021
+
2022
+ // AI makes decision based on criteria
2023
+ let selectedLevel;
2024
+ if (userNeeds.requiresMedication && userNeeds.noFamilySupport) {
2025
+ selectedLevel = 'High';
2026
+ } else if (userNeeds.requiresPersonalCare) {
2027
+ selectedLevel = 'Medium';
2028
+ } else {
2029
+ selectedLevel = 'Low';
2030
+ }
2031
+
2032
+ // Set the value (clicks the button)
2033
+ await weaveDOM.setFormFieldValue(`#${field.id}`, selectedLevel);
2034
+ }
2035
+ }
2036
+ ```
2037
+
2038
+ **Supported Frameworks:**
2039
+ - Material-UI (MUI) - ToggleButtonGroup component
2040
+ - Ant Design - Button groups with radio behavior
2041
+ - Bootstrap - Button groups with data-toggle
2042
+ - Custom implementations - Any `div[role="group"]` with buttons
2043
+
2044
+ **Key Points:**
2045
+ - ✅ Automatically detected - No special handling needed
2046
+ - ✅ Works with React/Vue/Angular - Clicks trigger framework handlers
2047
+ - ✅ AI-friendly format - Clear options and values
2048
+ - ✅ Same API as other fields - Use `setFormFieldValue()`
2049
+ - ⚠️ Use button **values**, not text - "true"/"false" for Yes/No, not "Yes"/"No"
2050
+ - ⚠️ **IDs with dots** - Use `[id="field.name"]` attribute selector, not `#field\\.name`
2051
+
2052
+ ### Error Handling
2053
+
2054
+ All DOM operations can fail. Always use try-catch:
2055
+
2056
+ ```typescript
2057
+ try {
2058
+ await weaveDOM.setText('h1', 'New Title');
2059
+ } catch (error) {
2060
+ console.error('Failed to update title:', error.message);
2061
+ // Handle error (show user message, etc.)
2062
+ }
2063
+ ```
2064
+
2065
+ ### Element Injection with Event Listeners
2066
+
2067
+ Apps can inject custom HTML elements onto the parent page with click event listeners. This is useful for adding floating buttons, overlays, badges, or any interactive UI elements.
2068
+
2069
+ #### `injectElement(targetSelector, position, html, options)`
2070
+
2071
+ Injects an element onto the parent page with optional click event listener.
2072
+
2073
+ **Parameters:**
2074
+ - `targetSelector` (string) - CSS selector for the element to inject relative to
2075
+ - `position` (InsertPosition) - Position relative to target: `'beforebegin'`, `'afterbegin'`, `'beforeend'`, `'afterend'`
2076
+ - `html` (string) - HTML string to inject (will be sanitized)
2077
+ - `options` (object, optional)
2078
+ - `onClick` (function) - Callback function invoked when element is clicked
2079
+ - `elementId` (string) - Custom element ID (auto-generated if not provided)
2080
+
2081
+ **Returns:** Promise<string> - The element ID
2082
+
2083
+ **Example: Floating Action Button**
2084
+
2085
+ ```typescript
2086
+ class FloatingButtonApp extends WeaveBaseApp {
2087
+ constructor() {
2088
+ super({
2089
+ id: 'floating-button-app',
2090
+ name: 'Floating Button',
2091
+ version: '1.0.0',
2092
+ category: 'utility',
2093
+ description: 'Adds a floating action button',
2094
+ tags: ['button']
2095
+ });
2096
+
2097
+ this.state = {
2098
+ buttonId: null,
2099
+ clickCount: 0
2100
+ };
2101
+ }
2102
+
2103
+ async connectedCallback() {
2104
+ super.connectedCallback();
2105
+ await this.injectButton();
2106
+ }
2107
+
2108
+ async injectButton() {
2109
+ const buttonHTML = `
2110
+ <button style="
2111
+ position: fixed;
2112
+ bottom: 20px;
2113
+ right: 20px;
2114
+ width: 60px;
2115
+ height: 60px;
2116
+ border-radius: 50%;
2117
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
2118
+ color: white;
2119
+ border: none;
2120
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
2121
+ cursor: pointer;
2122
+ font-size: 24px;
2123
+ z-index: 9999;
2124
+ ">✨</button>
2125
+ `;
2126
+
2127
+ const elementId = await weaveDOM.injectElement(
2128
+ 'body',
2129
+ 'beforeend',
2130
+ buttonHTML,
2131
+ {
2132
+ onClick: (data) => {
2133
+ this.state.clickCount++;
2134
+ console.log('Button clicked!', data);
2135
+ this.render();
2136
+ }
2137
+ }
2138
+ );
2139
+
2140
+ this.state.buttonId = elementId;
2141
+ this.render();
2142
+ }
2143
+
2144
+ async disconnectedCallback() {
2145
+ // Clean up: remove button when app is closed
2146
+ if (this.state.buttonId) {
2147
+ await weaveDOM.removeInjectedElement(this.state.buttonId);
2148
+ }
2149
+ super.disconnectedCallback();
2150
+ }
2151
+ }
2152
+
2153
+ customElements.define('floating-button-app', FloatingButtonApp);
2154
+ ```
2155
+
2156
+ #### `removeInjectedElement(elementId)`
2157
+
2158
+ Removes an injected element from the parent page.
2159
+
2160
+ **Parameters:**
2161
+ - `elementId` (string) - ID of the element to remove (returned from `injectElement`)
2162
+
2163
+ **Example:**
2164
+ ```typescript
2165
+ await weaveDOM.removeInjectedElement('weave-injected-1');
2166
+ ```
2167
+
2168
+ #### Click Event Data
2169
+
2170
+ When an injected element is clicked, the callback receives:
2171
+
2172
+ ```typescript
2173
+ {
2174
+ elementId: 'weave-injected-1', // ID of the clicked element
2175
+ event: {
2176
+ clientX: 450, // Mouse X position
2177
+ clientY: 300, // Mouse Y position
2178
+ target: {
2179
+ tagName: 'button', // Element tag name
2180
+ id: 'weave-injected-1', // Element ID
2181
+ className: 'my-button' // Element classes
2182
+ }
2183
+ }
2184
+ }
2185
+ ```
2186
+
2187
+ #### Common Use Cases
2188
+
2189
+ **1. Floating Action Button**
2190
+ ```typescript
2191
+ const buttonId = await weaveDOM.injectElement(
2192
+ 'body',
2193
+ 'beforeend',
2194
+ '<button class="fab">🚀</button>',
2195
+ {
2196
+ onClick: () => console.log('Action triggered!')
2197
+ }
2198
+ );
2199
+ ```
2200
+
2201
+ **2. Page Banner/Overlay**
2202
+ ```typescript
2203
+ const bannerId = await weaveDOM.injectElement(
2204
+ 'body',
2205
+ 'afterbegin',
2206
+ `<div style="position: fixed; top: 0; left: 0; right: 0;
2207
+ background: #3b82f6; color: white; padding: 15px;
2208
+ text-align: center; z-index: 9999;">
2209
+ 🎉 Special Offer! Click to learn more
2210
+ </div>`,
2211
+ {
2212
+ onClick: () => {
2213
+ // Handle banner click
2214
+ console.log('Banner clicked!');
2215
+ }
2216
+ }
2217
+ );
2218
+ ```
2219
+
2220
+ **3. Notification Badge**
2221
+ ```typescript
2222
+ const badgeId = await weaveDOM.injectElement(
2223
+ '#user-profile',
2224
+ 'afterbegin',
2225
+ `<span style="position: absolute; top: -5px; right: -5px;
2226
+ background: red; color: white; border-radius: 50%;
2227
+ width: 20px; height: 20px; display: flex;
2228
+ align-items: center; justify-content: center;
2229
+ font-size: 12px;">3</span>`,
2230
+ {
2231
+ onClick: () => console.log('Show notifications')
2232
+ }
2233
+ );
2234
+ ```
2235
+
2236
+ **4. Inline Action Buttons**
2237
+ ```typescript
2238
+ // Add quick action buttons next to specific elements
2239
+ const quickBuyId = await weaveDOM.injectElement(
2240
+ '.product-card',
2241
+ 'beforeend',
2242
+ '<button class="quick-buy">Quick Buy</button>',
2243
+ {
2244
+ onClick: (data) => {
2245
+ console.log('Quick buy clicked!');
2246
+ // Handle purchase
2247
+ }
2248
+ }
2249
+ );
2250
+ ```
2251
+
2252
+ #### Security & Best Practices
2253
+
2254
+ **Security Features:**
2255
+ - ✅ HTML is sanitized (scripts and inline event handlers removed)
2256
+ - ✅ Click listeners registered via `addEventListener` (not inline)
2257
+ - ✅ All injected elements tracked for cleanup
2258
+ - ✅ Elements marked with `data-weave-injected="true"`
2259
+ - ✅ Automatic cleanup when DOMBridge is destroyed
2260
+
2261
+ **Best Practices:**
2262
+ 1. **Always clean up** - Remove injected elements when app is closed
2263
+ 2. **Use high z-index** - Use 9999+ for overlays to ensure visibility
2264
+ 3. **Handle errors** - Wrap injection calls in try-catch blocks
2265
+ 4. **Unique IDs** - Provide custom element IDs if tracking multiple elements
2266
+ 5. **Responsive design** - Use fixed positioning and responsive units
2267
+ 6. **Accessibility** - Add ARIA labels and keyboard support
2268
+
2269
+ **Limitations:**
2270
+ - ⚠️ Only click events supported (more events can be added if needed)
2271
+ - ⚠️ Inline event handlers removed for security
2272
+ - ⚠️ HTML should contain one root element
2273
+ - ⚠️ No direct DOM access to injected elements
2274
+
2275
+ ### Element Watching (MutationObserver)
2276
+
2277
+ Apps can watch elements on the parent page for changes using the browser's native MutationObserver API. This allows you to react to attribute changes, element removal, and child node changes.
2278
+
2279
+ #### What Can Be Watched
2280
+
2281
+ 1. **Attribute Changes** - Detect when element attributes change (class, data-*, style, etc.)
2282
+ 2. **Element Removal** - Detect when an element is removed from the DOM
2283
+ 3. **Child Changes** - Detect when child nodes are added/removed (optional)
2284
+
2285
+ #### `watchElement(selector, callback, options)`
2286
+
2287
+ Watch an element for changes.
2288
+
2289
+ **Parameters:**
2290
+ - `selector` (string) - CSS selector for the element to watch
2291
+ - `callback` (function) - Function called when element changes
2292
+ - `options` (object, optional)
2293
+ - `watchAttributes` (boolean) - Watch for attribute changes (default: true)
2294
+ - `watchChildren` (boolean) - Watch for child node changes (default: false)
2295
+ - `attributeFilter` (string[]) - Optional array of specific attributes to watch
2296
+
2297
+ **Returns:** Promise<string> - The watcher ID (use to stop watching)
2298
+
2299
+ **Callback Data:**
2300
+ ```typescript
2301
+ {
2302
+ changeType: 'attribute' | 'removed' | 'childList',
2303
+ element: ElementSnapshot, // Current state of element
2304
+ attributeName?: string, // Only for 'attribute' changes
2305
+ attributeValue?: string // Only for 'attribute' changes
2306
+ }
2307
+ ```
2308
+
2309
+ #### `unwatchElement(watcherId)`
2310
+
2311
+ Stop watching an element.
2312
+
2313
+ **Parameters:**
2314
+ - `watcherId` (string) - ID of the watcher to stop (returned from `watchElement`)
2315
+
2316
+ #### Example: Watch for Attribute Changes
2317
+
2318
+ ```typescript
2319
+ class AttributeWatcherApp extends WeaveBaseApp {
2320
+ constructor() {
2321
+ super({
2322
+ id: 'attribute-watcher',
2323
+ name: 'Attribute Watcher',
2324
+ version: '1.0.0',
2325
+ category: 'utility',
2326
+ description: 'Watches element attributes',
2327
+ tags: ['monitoring']
2328
+ });
2329
+
2330
+ this.state = {
2331
+ watcherId: null
2332
+ };
2333
+ }
2334
+
2335
+ async onBackgroundService() {
2336
+ // Watch for class changes on a button
2337
+ this.state.watcherId = await window.weaveDOM.watchElement(
2338
+ '#submit-button',
2339
+ (data) => {
2340
+ if (data.changeType === 'attribute' && data.attributeName === 'class') {
2341
+ console.log(`Button class changed to: ${data.attributeValue}`);
2342
+
2343
+ // React to specific class changes
2344
+ if (data.element.className.includes('disabled')) {
2345
+ console.log('Button was disabled!');
2346
+ this.showWarning('Submit button is now disabled');
2347
+ }
2348
+ }
2349
+ },
2350
+ {
2351
+ watchAttributes: true,
2352
+ attributeFilter: ['class'] // Only watch class attribute
2353
+ }
2354
+ );
2355
+ }
2356
+
2357
+ cleanup() {
2358
+ if (this.state.watcherId) {
2359
+ window.weaveDOM.unwatchElement(this.state.watcherId);
2360
+ }
2361
+ }
2362
+ }
2363
+ ```
2364
+
2365
+ #### Example: Watch for Element Removal
2366
+
2367
+ ```typescript
2368
+ async onBackgroundService() {
2369
+ const watcherId = await window.weaveDOM.watchElement(
2370
+ '#notification-banner',
2371
+ (data) => {
2372
+ if (data.changeType === 'removed') {
2373
+ console.log('Notification banner was removed!');
2374
+ // Re-inject your custom UI or take other action
2375
+ this.reinjectCustomBanner();
2376
+ }
2377
+ }
2378
+ );
2379
+ }
2380
+ ```
2381
+
2382
+ #### Example: Watch Multiple Attributes
2383
+
2384
+ ```typescript
2385
+ await window.weaveDOM.watchElement(
2386
+ '#status-indicator',
2387
+ (data) => {
2388
+ if (data.changeType === 'attribute') {
2389
+ console.log(`${data.attributeName} changed to: ${data.attributeValue}`);
2390
+
2391
+ // React to specific attributes
2392
+ if (data.attributeName === 'data-status') {
2393
+ this.handleStatusChange(data.attributeValue);
2394
+ } else if (data.attributeName === 'aria-busy') {
2395
+ this.handleLoadingChange(data.attributeValue === 'true');
2396
+ }
2397
+ }
2398
+ },
2399
+ {
2400
+ watchAttributes: true,
2401
+ attributeFilter: ['data-status', 'aria-busy', 'class']
2402
+ }
2403
+ );
2404
+ ```
2405
+
2406
+ #### Example: Watch Child Node Changes
2407
+
2408
+ ```typescript
2409
+ await window.weaveDOM.watchElement(
2410
+ '#message-container',
2411
+ (data) => {
2412
+ if (data.changeType === 'childList') {
2413
+ console.log('Children changed in message container');
2414
+ // New messages might have been added
2415
+ this.checkForNewMessages();
2416
+ }
2417
+ },
2418
+ {
2419
+ watchChildren: true
2420
+ }
2421
+ );
2422
+ ```
2423
+
2424
+ #### Example: Auto-Reinject UI When Removed
2425
+
2426
+ ```typescript
2427
+ class AutoReinjectApp extends WeaveBaseApp {
2428
+ private buttonElementId: string | null = null;
2429
+ private watcherId: string | null = null;
2430
+
2431
+ async onBackgroundService() {
2432
+ await this.injectButton();
2433
+ await this.watchForRemoval();
2434
+ }
2435
+
2436
+ private async injectButton() {
2437
+ this.buttonElementId = await window.weaveDOM.injectElement(
2438
+ '#toolbar',
2439
+ 'beforeend',
2440
+ '<button id="my-button">My Action</button>',
2441
+ {
2442
+ onClick: () => this.handleClick()
2443
+ }
2444
+ );
2445
+ }
2446
+
2447
+ private async watchForRemoval() {
2448
+ // Watch the parent container for child changes
2449
+ this.watcherId = await window.weaveDOM.watchElement(
2450
+ '#toolbar',
2451
+ async (data) => {
2452
+ if (data.changeType === 'childList') {
2453
+ // Check if our button still exists
2454
+ const buttonExists = data.element.outerHTML.includes('my-button');
2455
+ if (!buttonExists && this.buttonElementId) {
2456
+ console.log('Button was removed, re-injecting...');
2457
+ await this.injectButton();
2458
+ }
2459
+ }
2460
+ },
2461
+ {
2462
+ watchChildren: true
2463
+ }
2464
+ );
2465
+ }
2466
+
2467
+ cleanup() {
2468
+ if (this.watcherId) {
2469
+ window.weaveDOM.unwatchElement(this.watcherId);
2470
+ }
2471
+ }
2472
+ }
2473
+ ```
2474
+
2475
+ #### Example: React to Loading States
2476
+
2477
+ ```typescript
2478
+ async onBackgroundService() {
2479
+ await window.weaveDOM.watchElement(
2480
+ '#main-content',
2481
+ (data) => {
2482
+ if (data.changeType === 'attribute' && data.attributeName === 'aria-busy') {
2483
+ const isLoading = data.attributeValue === 'true';
2484
+
2485
+ if (isLoading) {
2486
+ console.log('Page is loading...');
2487
+ this.showLoadingIndicator();
2488
+ } else {
2489
+ console.log('Page finished loading');
2490
+ this.hideLoadingIndicator();
2491
+ this.processPageContent();
2492
+ }
2493
+ }
2494
+ },
2495
+ {
2496
+ watchAttributes: true,
2497
+ attributeFilter: ['aria-busy']
2498
+ }
2499
+ );
2500
+ }
2501
+ ```
2502
+
2503
+ #### Common Use Cases
2504
+
2505
+ **1. Monitor Form Validation States**
2506
+ ```typescript
2507
+ // Watch for validation class changes
2508
+ await window.weaveDOM.watchElement(
2509
+ 'form#checkout-form',
2510
+ (data) => {
2511
+ if (data.changeType === 'attribute' && data.attributeName === 'class') {
2512
+ if (data.element.className.includes('has-errors')) {
2513
+ this.showFormHelp();
2514
+ } else if (data.element.className.includes('is-valid')) {
2515
+ this.hideFormHelp();
2516
+ }
2517
+ }
2518
+ },
2519
+ { watchAttributes: true, attributeFilter: ['class'] }
2520
+ );
2521
+ ```
2522
+
2523
+ **2. Detect Dynamic Content Loading**
2524
+ ```typescript
2525
+ // Watch for new items being added to a list
2526
+ await window.weaveDOM.watchElement(
2527
+ '#product-list',
2528
+ (data) => {
2529
+ if (data.changeType === 'childList') {
2530
+ console.log('New products loaded');
2531
+ this.processNewProducts();
2532
+ }
2533
+ },
2534
+ { watchChildren: true }
2535
+ );
2536
+ ```
2537
+
2538
+ **3. Monitor Status Changes**
2539
+ ```typescript
2540
+ // Watch for status attribute changes
2541
+ await window.weaveDOM.watchElement(
2542
+ '#order-status',
2543
+ (data) => {
2544
+ if (data.changeType === 'attribute' && data.attributeName === 'data-status') {
2545
+ const status = data.attributeValue;
2546
+ if (status === 'completed') {
2547
+ this.notifyOrderComplete();
2548
+ }
2549
+ }
2550
+ },
2551
+ { watchAttributes: true, attributeFilter: ['data-status'] }
2552
+ );
2553
+ ```
2554
+
2555
+ **4. Persistent UI Injection**
2556
+ ```typescript
2557
+ // Re-inject button if removed by page updates
2558
+ async onBackgroundService() {
2559
+ await this.injectCustomButton();
2560
+
2561
+ // Watch parent for changes
2562
+ await window.weaveDOM.watchElement(
2563
+ '#button-container',
2564
+ async (data) => {
2565
+ if (data.changeType === 'childList') {
2566
+ const hasButton = data.element.outerHTML.includes('my-custom-button');
2567
+ if (!hasButton) {
2568
+ await this.injectCustomButton();
2569
+ }
2570
+ }
2571
+ },
2572
+ { watchChildren: true }
2573
+ );
2574
+ }
2575
+ ```
2576
+
2577
+ #### Best Practices
2578
+
2579
+ **✅ Do:**
2580
+ - Always clean up watchers in the `cleanup()` method
2581
+ - Use `attributeFilter` when you only care about specific attributes (more efficient)
2582
+ - Check `changeType` before accessing attribute-specific fields
2583
+ - Store watcher IDs as class properties for cleanup
2584
+ - Watch parent elements to detect child removal
2585
+
2586
+ **❌ Don't:**
2587
+ - Don't watch too many elements - each watcher uses resources
2588
+ - Don't forget to unwatch - memory leaks can occur
2589
+ - Don't watch `document.body` with `watchChildren: true` - too many events
2590
+ - Don't assume element exists - check `data.element.exists`
2591
+
2592
+ #### Performance Considerations
2593
+
2594
+ 1. **Attribute Filters**: Always use `attributeFilter` when possible - reduces event volume
2595
+ 2. **Child Watching**: Use `watchChildren` sparingly - can fire many events
2596
+ 3. **Cleanup**: Always unwatch when done - observers consume memory
2597
+ 4. **Selector Specificity**: Use specific selectors to avoid watching wrong elements
2598
+
2599
+ #### Automatic Cleanup
2600
+
2601
+ Watchers are automatically cleaned up when:
2602
+ - Element is removed from DOM (parent observer detects and cleans up)
2603
+ - `unwatchElement()` is called
2604
+ - DOMBridge is destroyed (e.g., page unload)
2605
+
2606
+ #### Error Handling
2607
+
2608
+ ```typescript
2609
+ try {
2610
+ const watcherId = await window.weaveDOM.watchElement(
2611
+ '#my-element',
2612
+ (data) => console.log(data)
2613
+ );
2614
+ } catch (error) {
2615
+ // Element not found or watcher already exists
2616
+ console.error('Failed to watch element:', error.message);
2617
+ }
2618
+ ```
2619
+
2620
+ #### Comparison to Polling
2621
+
2622
+ **MutationObserver (Element Watching) ✅**
2623
+ - Native browser API
2624
+ - Event-driven (no polling)
2625
+ - Efficient (only fires when changes occur)
2626
+ - Detects exact attribute changes
2627
+ - Automatic cleanup on removal
2628
+
2629
+ **setInterval Polling ❌**
2630
+ - Custom implementation
2631
+ - Constant CPU usage
2632
+ - Inefficient (checks even when nothing changes)
2633
+ - Must manually compare values
2634
+ - Must manually check if element exists
2635
+
2636
+ ### Security Restrictions
2637
+
2638
+ The DOMBridge **blocks** dangerous operations:
2639
+
2640
+ ❌ **Blocked Selectors:**
2641
+ - `*` (universal selector)
2642
+ - `body`
2643
+ - `html`
2644
+ - Selectors targeting `<script>` tags
2645
+
2646
+ ❌ **Blocked HTML:**
2647
+ - `<script>` tags
2648
+ - Event handler attributes (`onclick`, `onload`, etc.)
2649
+ - `javascript:` URLs
2650
+
2651
+ ❌ **Blocked Attributes:**
2652
+ - `onclick`, `onload`, `onerror`, etc.
2653
+ - `href` with `javascript:`
2654
+ - `src` on certain elements
2655
+
2656
+ ✅ **Allowed:**
2657
+ - CSS selectors (class, id, attribute, etc.)
2658
+ - Safe HTML content
2659
+ - Style manipulation
2660
+ - Class/attribute manipulation
2661
+ - Text content changes
2662
+ - Element injection with event listeners (via `injectElement`)
2663
+
2664
+ ## Shadow DOM & Styling
2665
+
2666
+ ### Style Isolation
2667
+
2668
+ Apps use Shadow DOM for complete style isolation:
2669
+
2670
+ ```typescript
2671
+ this.renderHTML(`
2672
+ <style>
2673
+ /* These styles ONLY affect your app */
2674
+ .button {
2675
+ background: blue;
2676
+ color: white;
2677
+ }
2678
+
2679
+ /* Parent page styles do NOT leak in */
2680
+ /* Your styles do NOT leak out */
2681
+ </style>
2682
+
2683
+ <button class="button">Click Me</button>
2684
+ `);
2685
+ ```
2686
+
2687
+ ### Tailwind CSS Available
2688
+
2689
+ **Tailwind CSS is available to all apps!** The sidebar iframe loads Tailwind, so you can use Tailwind utility classes directly in your HTML:
2690
+
2691
+ ```typescript
2692
+ this.renderHTML(`
2693
+ <div class="p-4 bg-white rounded-lg shadow-md">
2694
+ <h1 class="text-2xl font-bold text-gray-800 mb-4">My App</h1>
2695
+ <button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
2696
+ Click Me
2697
+ </button>
2698
+ </div>
2699
+ `);
2700
+ ```
2701
+
2702
+ **You can mix Tailwind with custom CSS:**
2703
+
2704
+ ```typescript
2705
+ this.renderHTML(`
2706
+ <style>
2707
+ /* Custom styles for specific needs */
2708
+ .custom-gradient {
2709
+ background: linear-gradient(to right, #667eea, #764ba2);
2710
+ }
2711
+ </style>
2712
+
2713
+ <div class="p-6 space-y-4">
2714
+ <div class="custom-gradient text-white p-4 rounded-lg">
2715
+ <h2 class="text-xl font-semibold">Gradient Header</h2>
2716
+ </div>
2717
+
2718
+ <div class="flex gap-2">
2719
+ <button class="flex-1 px-4 py-2 bg-green-500 text-white rounded">
2720
+ Save
2721
+ </button>
2722
+ <button class="flex-1 px-4 py-2 bg-gray-300 text-gray-700 rounded">
2723
+ Cancel
2724
+ </button>
2725
+ </div>
2726
+ </div>
2727
+ `);
2728
+ ```
2729
+
2730
+ **Common Tailwind Patterns:**
2731
+
2732
+ ```typescript
2733
+ // Card layout
2734
+ <div class="bg-white rounded-lg shadow p-4">
2735
+ <h3 class="text-lg font-semibold mb-2">Card Title</h3>
2736
+ <p class="text-gray-600">Card content</p>
2737
+ </div>
2738
+
2739
+ // Form inputs
2740
+ <input
2741
+ type="text"
2742
+ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
2743
+ placeholder="Enter text"
2744
+ />
2745
+
2746
+ // Buttons
2747
+ <button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 active:bg-blue-700 transition">
2748
+ Primary Button
2749
+ </button>
2750
+
2751
+ // Flex layouts
2752
+ <div class="flex items-center justify-between">
2753
+ <span>Label</span>
2754
+ <button>Action</button>
2755
+ </div>
2756
+
2757
+ // Grid layouts
2758
+ <div class="grid grid-cols-2 gap-4">
2759
+ <div>Item 1</div>
2760
+ <div>Item 2</div>
2761
+ </div>
2762
+
2763
+ // Spacing
2764
+ <div class="space-y-4"> <!-- Vertical spacing between children -->
2765
+ <div>Item 1</div>
2766
+ <div>Item 2</div>
2767
+ </div>
2768
+ ```
2769
+
2770
+ ### Best Practices
2771
+
2772
+ 1. **Prefer Tailwind utilities for common styles:**
2773
+ ```typescript
2774
+ // ✅ Good - Use Tailwind
2775
+ <button class="px-4 py-2 bg-blue-500 text-white rounded">Click</button>
2776
+
2777
+ // ❌ Less ideal - Custom CSS for simple styles
2778
+ <style>
2779
+ .my-button { padding: 0.5rem 1rem; background: blue; }
2780
+ </style>
2781
+ <button class="my-button">Click</button>
2782
+ ```
2783
+
2784
+ 2. **Use custom CSS for complex/unique styles:**
2785
+ ```typescript
2786
+ this.renderHTML(`
2787
+ <style>
2788
+ .custom-animation {
2789
+ animation: slideIn 0.3s ease-out;
2790
+ }
2791
+ @keyframes slideIn {
2792
+ from { transform: translateX(-100%); }
2793
+ to { transform: translateX(0); }
2794
+ }
2795
+ </style>
2796
+ <div class="custom-animation p-4 bg-white rounded">Content</div>
2797
+ `);
2798
+ ```
2799
+
2800
+ 3. **Combine Tailwind with custom CSS variables:**
2801
+ ```typescript
2802
+ this.renderHTML(`
2803
+ <style>
2804
+ :host {
2805
+ --primary-color: #4f46e5;
2806
+ }
2807
+ .custom-primary {
2808
+ background-color: var(--primary-color);
2809
+ }
2810
+ </style>
2811
+ <button class="custom-primary px-4 py-2 text-white rounded hover:opacity-90">
2812
+ Custom Primary
2813
+ </button>
2814
+ `);
2815
+ ```
2816
+
2817
+ 4. **Use Tailwind's responsive classes:**
2818
+ ```typescript
2819
+ // Mobile-first responsive design
2820
+ <div class="p-2 md:p-4 lg:p-6">
2821
+ <h1 class="text-xl md:text-2xl lg:text-3xl">Responsive Title</h1>
2822
+ </div>
2823
+ ```
2824
+
2825
+ 5. **Leverage Tailwind's dark mode (if needed):**
2826
+ ```typescript
2827
+ <div class="bg-white dark:bg-gray-800 text-gray-900 dark:text-white">
2828
+ Content that adapts to dark mode
2829
+ </div>
2830
+ ```
2831
+
2832
+ ### CSS Variables & Tailwind Customization
2833
+
2834
+ **CSS variables can be overridden in shadow DOM!** This is one of the few things that crosses the shadow boundary because CSS variables are inherited properties.
2835
+
2836
+ #### How It Works
2837
+
2838
+ Tailwind uses CSS variables for theme values (colors, spacing, etc.). You can override these variables in your app's shadow DOM, and Tailwind classes will respect your custom values:
2839
+
2840
+ ```typescript
2841
+ this.renderHTML(`
2842
+ <style>
2843
+ /* Override Tailwind's CSS variables in your shadow DOM */
2844
+ :host {
2845
+ --color-primary: #ff6b6b;
2846
+ --color-secondary: #4ecdc4;
2847
+ --spacing-unit: 8px;
2848
+ }
2849
+
2850
+ /* Or override for specific elements */
2851
+ .custom-section {
2852
+ --color-primary: #95e1d3;
2853
+ }
2854
+
2855
+ /* Create your own variables */
2856
+ :host {
2857
+ --app-accent: #f38181;
2858
+ --app-border-radius: 12px;
2859
+ }
2860
+
2861
+ .custom-card {
2862
+ background: var(--color-primary);
2863
+ border-radius: var(--app-border-radius);
2864
+ padding: calc(var(--spacing-unit) * 2);
2865
+ }
2866
+ </style>
2867
+
2868
+ <div class="p-4">
2869
+ <!-- Tailwind classes work normally -->
2870
+ <div class="bg-blue-500 text-white p-4 rounded">
2871
+ Standard Tailwind
2872
+ </div>
2873
+
2874
+ <!-- Custom classes using your variables -->
2875
+ <div class="custom-card">
2876
+ Uses custom CSS variables
2877
+ </div>
2878
+
2879
+ <!-- Override variables for nested elements -->
2880
+ <div class="custom-section">
2881
+ <div class="custom-card">
2882
+ Uses overridden primary color
2883
+ </div>
2884
+ </div>
2885
+ </div>
2886
+ `);
2887
+ ```
2888
+
2889
+ #### Cascading Overrides
2890
+
2891
+ CSS variables follow the cascade, so you can override them at different levels:
2892
+
2893
+ ```typescript
2894
+ this.renderHTML(`
2895
+ <style>
2896
+ /* Root level for entire app */
2897
+ :host {
2898
+ --btn-color: blue;
2899
+ --btn-padding: 12px;
2900
+ }
2901
+
2902
+ /* Override for specific section */
2903
+ .danger-zone {
2904
+ --btn-color: red;
2905
+ }
2906
+
2907
+ /* Override for specific element */
2908
+ .special-button {
2909
+ --btn-color: purple;
2910
+ --btn-padding: 20px;
2911
+ }
2912
+
2913
+ /* Use the variables */
2914
+ .btn {
2915
+ background: var(--btn-color);
2916
+ padding: var(--btn-padding);
2917
+ color: white;
2918
+ border: none;
2919
+ border-radius: 4px;
2920
+ }
2921
+ </style>
2922
+
2923
+ <div class="p-4 space-y-4">
2924
+ <!-- Uses blue (from :host) -->
2925
+ <button class="btn">Normal Button</button>
2926
+
2927
+ <!-- Uses red (overridden in .danger-zone) -->
2928
+ <div class="danger-zone">
2929
+ <button class="btn">Danger Button</button>
2930
+ </div>
2931
+
2932
+ <!-- Uses purple (overridden in .special-button) -->
2933
+ <button class="btn special-button">Special Button</button>
2934
+ </div>
2935
+ `);
2936
+ ```
2937
+
2938
+ #### Theming Your App
2939
+
2940
+ Create a complete theme system using CSS variables:
2941
+
2942
+ ```typescript
2943
+ this.renderHTML(`
2944
+ <style>
2945
+ :host {
2946
+ /* Color palette */
2947
+ --theme-primary: #3b82f6;
2948
+ --theme-secondary: #8b5cf6;
2949
+ --theme-success: #10b981;
2950
+ --theme-danger: #ef4444;
2951
+ --theme-warning: #f59e0b;
2952
+
2953
+ /* Neutral colors */
2954
+ --theme-bg: #ffffff;
2955
+ --theme-surface: #f3f4f6;
2956
+ --theme-border: #e5e7eb;
2957
+ --theme-text: #111827;
2958
+ --theme-text-muted: #6b7280;
2959
+
2960
+ /* Spacing scale */
2961
+ --space-xs: 4px;
2962
+ --space-sm: 8px;
2963
+ --space-md: 16px;
2964
+ --space-lg: 24px;
2965
+ --space-xl: 32px;
2966
+
2967
+ /* Typography */
2968
+ --font-size-sm: 0.875rem;
2969
+ --font-size-base: 1rem;
2970
+ --font-size-lg: 1.125rem;
2971
+ --font-size-xl: 1.25rem;
2972
+
2973
+ /* Border radius */
2974
+ --radius-sm: 4px;
2975
+ --radius-md: 8px;
2976
+ --radius-lg: 12px;
2977
+
2978
+ /* Shadows */
2979
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
2980
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
2981
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
2982
+ }
2983
+
2984
+ /* Apply theme variables */
2985
+ .card {
2986
+ background: var(--theme-surface);
2987
+ border: 1px solid var(--theme-border);
2988
+ border-radius: var(--radius-md);
2989
+ padding: var(--space-lg);
2990
+ box-shadow: var(--shadow-sm);
2991
+ }
2992
+
2993
+ .btn-primary {
2994
+ background: var(--theme-primary);
2995
+ color: white;
2996
+ padding: var(--space-sm) var(--space-md);
2997
+ border-radius: var(--radius-sm);
2998
+ font-size: var(--font-size-base);
2999
+ border: none;
3000
+ cursor: pointer;
3001
+ }
3002
+
3003
+ .btn-primary:hover {
3004
+ filter: brightness(1.1);
3005
+ }
3006
+
3007
+ .text-muted {
3008
+ color: var(--theme-text-muted);
3009
+ font-size: var(--font-size-sm);
3010
+ }
3011
+ </style>
3012
+
3013
+ <div class="p-4 space-y-4">
3014
+ <div class="card">
3015
+ <h2 class="text-xl font-bold mb-2">Themed Card</h2>
3016
+ <p class="text-muted mb-4">Using custom CSS variables</p>
3017
+ <button class="btn-primary">Primary Action</button>
3018
+ </div>
3019
+ </div>
3020
+ `);
3021
+ ```
3022
+
3023
+ #### Dark Mode with CSS Variables
3024
+
3025
+ Implement dark mode by overriding variables based on a class or state:
3026
+
3027
+ ```typescript
3028
+ this.renderHTML(`
3029
+ <style>
3030
+ :host {
3031
+ /* Light mode (default) */
3032
+ --theme-bg: #ffffff;
3033
+ --theme-text: #111827;
3034
+ --theme-surface: #f3f4f6;
3035
+ --theme-border: #e5e7eb;
3036
+ }
3037
+
3038
+ /* Dark mode overrides */
3039
+ :host(.dark-mode) {
3040
+ --theme-bg: #1f2937;
3041
+ --theme-text: #f9fafb;
3042
+ --theme-surface: #374151;
3043
+ --theme-border: #4b5563;
3044
+ }
3045
+
3046
+ .container {
3047
+ background: var(--theme-bg);
3048
+ color: var(--theme-text);
3049
+ min-height: 100vh;
3050
+ padding: 20px;
3051
+ }
3052
+
3053
+ .card {
3054
+ background: var(--theme-surface);
3055
+ border: 1px solid var(--theme-border);
3056
+ padding: 16px;
3057
+ border-radius: 8px;
3058
+ }
3059
+ </style>
3060
+
3061
+ <div class="container">
3062
+ <div class="card">
3063
+ <h2>Theme-aware Card</h2>
3064
+ <p>Automatically adapts to light/dark mode</p>
3065
+ </div>
3066
+ </div>
3067
+ `);
3068
+
3069
+ // Toggle dark mode
3070
+ toggleDarkMode() {
3071
+ if (this.classList.contains('dark-mode')) {
3072
+ this.classList.remove('dark-mode');
3073
+ } else {
3074
+ this.classList.add('dark-mode');
3075
+ }
3076
+ }
3077
+ ```
3078
+
3079
+ #### Key Benefits
3080
+
3081
+ 1. **Inheritance Across Shadow Boundary**: CSS variables are one of the few things that cross shadow DOM boundaries
3082
+ 2. **Cascade Support**: Variables can be overridden at any DOM level
3083
+ 3. **Dynamic Theming**: Change variables programmatically for instant theme updates
3084
+ 4. **Tailwind Integration**: Override Tailwind's variables to customize its utility classes
3085
+ 5. **Scoped Customization**: Different parts of your app can have different themes
3086
+
3087
+ #### Important Notes
3088
+
3089
+ - **Tailwind classes themselves don't cross shadow boundaries** - they must be available in the shadow DOM
3090
+ - **CSS variables DO cross shadow boundaries** - they're inherited
3091
+ - **Override specificity follows CSS cascade rules** - more specific selectors win
3092
+ - **Use `:host` to target the shadow root** - this is your app's top-level element
3093
+
3094
+ ## State Management
3095
+
3096
+ ### Using `this.state`
3097
+
3098
+ ```typescript
3099
+ class MyApp extends WeaveBaseApp {
3100
+ constructor() {
3101
+ super({ /* ... */ });
3102
+
3103
+ // Initialize state
3104
+ this.state = {
3105
+ count: 0,
3106
+ items: [],
3107
+ isLoading: false
3108
+ };
3109
+ }
3110
+
3111
+ private increment(): void {
3112
+ // Update state
3113
+ this.setState({ count: this.state.count + 1 });
3114
+
3115
+ // Update UI
3116
+ this.updateDisplay();
3117
+ }
3118
+
3119
+ private updateDisplay(): void {
3120
+ const countEl = this.query('#count');
3121
+ if (countEl) {
3122
+ countEl.textContent = String(this.state.count);
3123
+ }
3124
+ }
3125
+ }
3126
+ ```
3127
+
3128
+ ### State Updates
3129
+
3130
+ - Use `this.setState()` to update state
3131
+ - State updates are **not reactive** - manually update UI
3132
+ - State is **not persisted** - use localStorage if needed
3133
+
3134
+ ## Event Handling
3135
+
3136
+ ### Internal Events (Shadow DOM)
3137
+
3138
+ ```typescript
3139
+ protected setupEventListeners(): void {
3140
+ // Button clicks
3141
+ this.query('#myButton')?.addEventListener('click', () => {
3142
+ this.handleClick();
3143
+ });
3144
+
3145
+ // Input changes
3146
+ this.query<HTMLInputElement>('#myInput')?.addEventListener('input', (e) => {
3147
+ const value = (e.target as HTMLInputElement).value;
3148
+ this.handleInput(value);
3149
+ });
3150
+
3151
+ // Form submission
3152
+ this.query('form')?.addEventListener('submit', (e) => {
3153
+ e.preventDefault();
3154
+ this.handleSubmit();
3155
+ });
3156
+ }
3157
+ ```
3158
+
3159
+ ### Cleanup
3160
+
3161
+ ```typescript
3162
+ protected cleanup(): void {
3163
+ // Clear intervals
3164
+ if (this.intervalId) {
3165
+ clearInterval(this.intervalId);
3166
+ }
3167
+
3168
+ // Remove external listeners (if any)
3169
+ // Note: Shadow DOM listeners are auto-cleaned
3170
+ }
3171
+ ```
3172
+
3173
+ ## TypeScript Types
3174
+
3175
+ ### Available Types
3176
+
3177
+ ```typescript
3178
+ import {
3179
+ WeaveBaseApp,
3180
+ WeaveAppInfo,
3181
+ ElementSnapshot,
3182
+ InsertPosition
3183
+ } from '@weave/app-sdk';
3184
+
3185
+ // App metadata
3186
+ interface WeaveAppInfo {
3187
+ id: string;
3188
+ name: string;
3189
+ version: string;
3190
+ category: string;
3191
+ description: string;
3192
+ author: string;
3193
+ tags?: string[];
3194
+ }
3195
+
3196
+ // Element from parent page
3197
+ interface ElementSnapshot {
3198
+ tagName: string;
3199
+ id: string;
3200
+ className: string;
3201
+ textContent: string;
3202
+ attributes: Record<string, string>;
3203
+ exists: boolean;
3204
+ }
3205
+
3206
+ // HTML insertion position
3207
+ type InsertPosition = 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend';
3208
+ ```
3209
+
3210
+ ## Build Process
3211
+
3212
+ ### Development Workflow
3213
+
3214
+ ```bash
3215
+ # Write TypeScript in src/app.ts
3216
+ npm run build
3217
+
3218
+ # Output: dist/{app-name}.js (plain JavaScript)
3219
+ ```
3220
+
3221
+ ### What Happens During Build
3222
+
3223
+ 1. **TypeScript Compilation** → JavaScript (ES2020)
3224
+ 2. **Import Removal** → SDK is on window globals
3225
+ 3. **Reference Replacement:**
3226
+ - `WeaveBaseApp` → `window.WeaveBaseApp`
3227
+ - `weaveDOM` → `window.weaveDOM`
3228
+ 4. **Clean Output** → Readable, unobfuscated JavaScript
3229
+
3230
+ ### Final Output Format
3231
+
3232
+ ```javascript
3233
+ /**
3234
+ * my-app
3235
+ *
3236
+ * Built with Weave App SDK
3237
+ * Generated: 2025-01-01T00:00:00.000Z
3238
+ */
3239
+
3240
+ // Access SDK from window globals
3241
+ const { WeaveBaseApp, weaveDOM } = window;
3242
+
3243
+ class MyApp extends window.WeaveBaseApp {
3244
+ // Your compiled code here
3245
+ }
3246
+
3247
+ customElements.define('my-app', MyApp);
3248
+ ```
3249
+
3250
+ ## Common Patterns
3251
+
3252
+ ### Loading State
3253
+
3254
+ ```typescript
3255
+ class MyApp extends WeaveBaseApp {
3256
+ constructor() {
3257
+ super({ /* ... */ });
3258
+ this.state = { isLoading: false, data: null };
3259
+ }
3260
+
3261
+ private async loadData(): Promise<void> {
3262
+ this.setState({ isLoading: true });
3263
+ this.updateUI();
3264
+
3265
+ try {
3266
+ const text = await window.weaveDOM.getText('h1');
3267
+ this.setState({ data: text, isLoading: false });
3268
+ } catch (error) {
3269
+ this.setState({ isLoading: false });
3270
+ console.error('Failed to load:', error);
3271
+ }
3272
+
3273
+ this.updateUI();
3274
+ }
3275
+ }
3276
+ ```
3277
+
3278
+ ### Form Handling
3279
+
3280
+ ```typescript
3281
+ protected setupEventListeners(): void {
3282
+ this.query('form')?.addEventListener('submit', async (e) => {
3283
+ e.preventDefault();
3284
+ await this.handleSubmit();
3285
+ });
3286
+ }
3287
+
3288
+ private async handleSubmit(): Promise<void> {
3289
+ const input = this.query<HTMLInputElement>('#textInput');
3290
+ if (!input) return;
3291
+
3292
+ const value = input.value.trim();
3293
+ if (!value) return;
3294
+
3295
+ try {
3296
+ await window.weaveDOM.setText('h1', value);
3297
+ input.value = '';
3298
+ this.showSuccess('Updated!');
3299
+ } catch (error) {
3300
+ this.showError('Failed to update');
3301
+ }
3302
+ }
3303
+ ```
3304
+
3305
+ ### List Rendering
3306
+
3307
+ ```typescript
3308
+ private renderList(): void {
3309
+ const items = this.state.items;
3310
+ const html = items.map(item => `
3311
+ <li data-id="${item.id}">
3312
+ ${item.name}
3313
+ <button class="delete-btn" data-id="${item.id}">Delete</button>
3314
+ </li>
3315
+ `).join('');
3316
+
3317
+ const list = this.query('#itemList');
3318
+ if (list) {
3319
+ list.innerHTML = html;
3320
+ }
3321
+ }
3322
+
3323
+ protected setupEventListeners(): void {
3324
+ // Event delegation
3325
+ this.query('#itemList')?.addEventListener('click', (e) => {
3326
+ const target = e.target as HTMLElement;
3327
+ if (target.classList.contains('delete-btn')) {
3328
+ const id = target.dataset.id;
3329
+ this.deleteItem(id);
3330
+ }
3331
+ });
3332
+ }
3333
+ ```
3334
+
3335
+ ## Limitations & Constraints
3336
+
3337
+ ### What You CANNOT Do
3338
+
3339
+ ❌ **Direct DOM Access**
3340
+ ```typescript
3341
+ // ❌ WRONG - No access to parent page DOM
3342
+ document.querySelector('h1');
3343
+ window.parent.document;
3344
+ ```
3345
+
3346
+ ❌ **Arbitrary HTTP Requests**
3347
+ ```typescript
3348
+ // ❌ WRONG - Cannot make external API calls
3349
+ await fetch('https://api.example.com/data');
3350
+ await axios.get('https://some-api.com');
3351
+
3352
+ // ✅ CORRECT - Use Weave API instead
3353
+ await weaveAPI.ai.chat({ appName: 'my-app', prompt: '...' });
3354
+ await weaveAPI.appData.getAll();
3355
+ ```
3356
+
3357
+ ❌ **ES6 Module Imports in Final Output**
3358
+ ```typescript
3359
+ // ❌ WRONG - Will be removed during build
3360
+ import { something } from 'some-library';
3361
+ ```
3362
+
3363
+ ❌ **External Dependencies**
3364
+ ```typescript
3365
+ // ❌ WRONG - No npm packages in final output
3366
+ import axios from 'axios';
3367
+ import lodash from 'lodash';
3368
+ ```
3369
+
3370
+ ❌ **Dangerous HTML**
3371
+ ```typescript
3372
+ // ❌ WRONG - Will be sanitized/blocked
3373
+ await weaveDOM.insertHTML('.container', '<script>alert("xss")</script>');
3374
+ ```
3375
+
3376
+ ### What You CAN Do
3377
+
3378
+ ✅ **Use Weave APIs**
3379
+ ```typescript
3380
+ // Backend API - AI and data storage
3381
+ await weaveAPI.ai.chat({ appName: 'my-app', prompt: 'Hello' });
3382
+ await weaveAPI.appData.create({ appId: 'my-app', dataKey: 'key', data: {} });
3383
+
3384
+ // DOM API - Parent page manipulation
3385
+ await weaveDOM.getText('h1');
3386
+ await weaveDOM.setText('.status', 'Updated!');
3387
+ ```
3388
+
3389
+ ✅ **Use Browser APIs**
3390
+ ```typescript
3391
+ localStorage.setItem('key', 'value');
3392
+ setTimeout(() => {}, 1000);
3393
+ new Date();
3394
+ console.log('Debug message');
3395
+ ```
3396
+
3397
+ ✅ **Use Modern JavaScript**
3398
+ ```typescript
3399
+ const items = [1, 2, 3].map(x => x * 2);
3400
+ const { name, age } = person;
3401
+ async/await, promises, etc.
3402
+ ```
3403
+
3404
+ ## Testing & Debugging
3405
+
3406
+ ### Console Logging
3407
+
3408
+ ```typescript
3409
+ console.log('✅ App initialized');
3410
+ console.error('❌ Error:', error);
3411
+ console.warn('⚠️ Warning:', message);
3412
+ ```
3413
+
3414
+ ### Debugging DOM Operations
3415
+
3416
+ ```typescript
3417
+ try {
3418
+ const element = await window.weaveDOM.query('h1');
3419
+ console.log('Element found:', element);
3420
+
3421
+ if (!element.exists) {
3422
+ console.warn('Element does not exist on page');
3423
+ }
3424
+ } catch (error) {
3425
+ console.error('DOM operation failed:', error);
3426
+ }
3427
+ ```
3428
+
3429
+ ### Check App Lifecycle
3430
+
3431
+ ```typescript
3432
+ connectedCallback(): void {
3433
+ console.log('✅ App connected to DOM');
3434
+ super.connectedCallback();
3435
+ }
3436
+
3437
+ disconnectedCallback(): void {
3438
+ console.log('🔌 App disconnected from DOM');
3439
+ super.disconnectedCallback();
3440
+ }
3441
+ ```
3442
+
3443
+ ## Best Practices
3444
+
3445
+ ### 1. No Duplicate Headers
3446
+ The sidebar automatically displays your app name as a header. Do NOT add your app name as an `<h1>` in your render method.
3447
+
3448
+ ### 2. Error Handling
3449
+ Always wrap DOM operations in try-catch blocks.
3450
+
3451
+ ### 3. User Feedback
3452
+ Show loading states and error messages to users.
3453
+
3454
+ ### 4. Performance
3455
+ - Debounce frequent operations
3456
+ - Avoid unnecessary DOM queries
3457
+ - Cache element references when possible
3458
+
3459
+ ### 5. Accessibility
3460
+ - Use semantic HTML
3461
+ - Include ARIA labels
3462
+ - Support keyboard navigation
3463
+
3464
+ ### 6. Responsive Design
3465
+ - Use relative units (rem, em, %)
3466
+ - Test different sidebar widths
3467
+ - Mobile-friendly touch targets
3468
+
3469
+ ### 7. Clean Code
3470
+ - Use TypeScript types
3471
+ - Comment complex logic
3472
+ - Follow consistent naming conventions
3473
+
3474
+ ## Example: Complete App
3475
+
3476
+ ```typescript
3477
+ import { WeaveBaseApp } from '@weave/app-sdk';
3478
+
3479
+ class PageTitleEditor extends WeaveBaseApp {
3480
+ constructor() {
3481
+ super({
3482
+ id: 'page-title-editor',
3483
+ name: 'Page Title Editor',
3484
+ version: '1.0.0',
3485
+ category: 'utility',
3486
+ description: 'Edit the page title',
3487
+ author: 'Developer Name',
3488
+ tags: ['editor', 'title']
3489
+ });
3490
+
3491
+ this.state = {
3492
+ currentTitle: '',
3493
+ isLoading: false,
3494
+ error: null
3495
+ };
3496
+ }
3497
+
3498
+ protected render(): void {
3499
+ this.renderHTML(`
3500
+ <div class="p-5 font-sans">
3501
+ <!-- Note: App name header is already shown by the sidebar, no need to duplicate it -->
3502
+
3503
+ <div class="mb-4">
3504
+ <label for="titleInput" class="block mb-2 font-medium text-gray-700">
3505
+ New Title:
3506
+ </label>
3507
+ <input
3508
+ type="text"
3509
+ id="titleInput"
3510
+ placeholder="Enter new title"
3511
+ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
3512
+ />
3513
+ </div>
3514
+
3515
+ <div class="flex gap-2 mb-4">
3516
+ <button
3517
+ id="updateBtn"
3518
+ class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 active:bg-blue-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
3519
+ >
3520
+ Update Title
3521
+ </button>
3522
+ <button
3523
+ id="loadBtn"
3524
+ class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 active:bg-gray-700 transition"
3525
+ >
3526
+ Load Current Title
3527
+ </button>
3528
+ </div>
3529
+
3530
+ <div id="message"></div>
3531
+ </div>
3532
+ `);
3533
+ }
3534
+
3535
+ protected setupEventListeners(): void {
3536
+ this.query('#updateBtn')?.addEventListener('click', () => this.updateTitle());
3537
+ this.query('#loadBtn')?.addEventListener('click', () => this.loadTitle());
3538
+
3539
+ this.query('#titleInput')?.addEventListener('keypress', (e) => {
3540
+ if (e.key === 'Enter') this.updateTitle();
3541
+ });
3542
+ }
3543
+
3544
+ private async loadTitle(): Promise<void> {
3545
+ this.setState({ isLoading: true, error: null });
3546
+ this.showMessage('Loading...', 'info');
3547
+
3548
+ try {
3549
+ const title = await window.weaveDOM.getText('title');
3550
+ this.setState({ currentTitle: title, isLoading: false });
3551
+
3552
+ const input = this.query<HTMLInputElement>('#titleInput');
3553
+ if (input) input.value = title;
3554
+
3555
+ this.showMessage('Title loaded!', 'success');
3556
+ } catch (error) {
3557
+ this.setState({ isLoading: false, error: error.message });
3558
+ this.showMessage('Failed to load title', 'error');
3559
+ }
3560
+ }
3561
+
3562
+ private async updateTitle(): Promise<void> {
3563
+ const input = this.query<HTMLInputElement>('#titleInput');
3564
+ if (!input) return;
3565
+
3566
+ const newTitle = input.value.trim();
3567
+ if (!newTitle) {
3568
+ this.showMessage('Please enter a title', 'error');
3569
+ return;
3570
+ }
3571
+
3572
+ this.setState({ isLoading: true, error: null });
3573
+ this.showMessage('Updating...', 'info');
3574
+
3575
+ try {
3576
+ await window.weaveDOM.setText('title', newTitle);
3577
+ this.setState({ currentTitle: newTitle, isLoading: false });
3578
+ this.showMessage('Title updated!', 'success');
3579
+ } catch (error) {
3580
+ this.setState({ isLoading: false, error: error.message });
3581
+ this.showMessage('Failed to update title', 'error');
3582
+ }
3583
+ }
3584
+
3585
+ private showMessage(text: string, type: 'success' | 'error' | 'info'): void {
3586
+ const messageEl = this.query('#message');
3587
+ if (messageEl) {
3588
+ messageEl.textContent = text;
3589
+
3590
+ // Use Tailwind classes for styling
3591
+ const baseClasses = 'p-3 rounded-md';
3592
+ const typeClasses = {
3593
+ success: 'bg-green-100 text-green-800 border border-green-200',
3594
+ error: 'bg-red-100 text-red-800 border border-red-200',
3595
+ info: 'bg-blue-100 text-blue-800 border border-blue-200'
3596
+ };
3597
+
3598
+ messageEl.className = `${baseClasses} ${typeClasses[type]}`;
3599
+ }
3600
+ }
3601
+ }
3602
+
3603
+ customElements.define('page-title-editor', PageTitleEditor);
3604
+ ```
3605
+
3606
+ ---
3607
+
3608
+ ## Quick Reference
3609
+
3610
+ ### SDK Globals
3611
+ - `window.WeaveBaseApp` - Base class
3612
+ - `window.weaveAPI` - Backend API (AI, data storage)
3613
+ - `window.weaveDOM` - DOM API (parent page manipulation)
3614
+
3615
+ ### Styling
3616
+ - **Tailwind CSS** - Available for all utility classes
3617
+ - **Custom CSS** - Use `<style>` tags in `renderHTML()` for custom styles
3618
+ - **Shadow DOM** - Complete style isolation from parent page
3619
+
3620
+ ### Base Class Methods
3621
+ - `this.renderHTML(html)` - Render UI
3622
+ - `this.query(selector)` - Find element
3623
+ - `this.queryAll(selector)` - Find all elements
3624
+ - `this.setState(updates)` - Update state
3625
+ - **`this.weaveAPI`** - ⚠️ **CRITICAL:** App-specific API client (use instead of `window.weaveAPI`)
3626
+
3627
+ ### Backend API Methods (`this.weaveAPI`)
3628
+ - **⚠️ ALWAYS use `this.weaveAPI` instead of `window.weaveAPI`**
3629
+ - **AI Service:**
3630
+ - `this.weaveAPI.ai.chat(request)` - Call Weave AI
3631
+ - **App Data Service:**
3632
+ - `this.weaveAPI.appData.getAll()` - Get all app data (returns `PaginatedResponse<AppData>`)
3633
+ - `this.weaveAPI.appData.create(request)` - Create new data
3634
+ - `this.weaveAPI.appData.get(id)` - Get specific data
3635
+ - `this.weaveAPI.appData.update(id, request)` - Update data
3636
+ - `this.weaveAPI.appData.delete(id)` - Delete data
3637
+
3638
+ ### DOM API Methods (`weaveDOM`)
3639
+ - **Read:** `query`, `queryAll`, `getText`, `getAttribute`, `getValue`, `hasClass`, `getPageUrl`
3640
+ - **Write:** `setText`, `setAttribute`, `setValue`, `addClass`, `removeClass`, `toggleClass`, `setStyle`
3641
+ - **Manipulate:** `insertHTML`, `removeElement`, `clickElement`
3642
+ - **Forms:** `getFormData`, `startFormClickListener`, `stopFormClickListener`, `setFormFieldValue`
3643
+ - **Element Injection:** `injectElement`, `removeInjectedElement`
3644
+ - **Element Listeners:** `startElementClickListener`, `stopElementClickListener`
3645
+
3646
+ ### Lifecycle
3647
+ 1. `constructor()` - Initialize
3648
+ 2. `connectedCallback()` - Added to DOM
3649
+ 3. `render()` - Render UI
3650
+ 4. `setupEventListeners()` - Attach listeners
3651
+ 5. `disconnectedCallback()` - Removed from DOM
3652
+ 6. `cleanup()` - Cleanup resources
3653
+
3654
+ ---
3655
+
3656
+ ## ⚠️ Critical Rules for AI Assistants
3657
+
3658
+ When helping developers build Weave apps, **ALWAYS enforce these rules:**
3659
+
3660
+ ### 1. **ALWAYS Use `this.weaveAPI`**
3661
+ - ❌ **NEVER** use `window.weaveAPI` in app methods
3662
+ - ✅ **ALWAYS** use `this.weaveAPI` for all API calls
3663
+ - This prevents app ID conflicts when multiple apps run simultaneously
3664
+
3665
+ ### 2. **Capture Context in Async Methods**
3666
+ - When using multiple `await` calls, capture the API client:
3667
+ ```typescript
3668
+ const apiClient = this.weaveAPI;
3669
+ const response = await apiClient.appData.getAll();
3670
+ const allData = response.data; // Access paginated data
3671
+ await apiClient.appData.create(...);
3672
+ ```
3673
+
3674
+ ### 3. **Capture Context in Callbacks**
3675
+ - When using callbacks (e.g., DOM Bridge click handlers):
3676
+ ```typescript
3677
+ const self = this;
3678
+ onClick: async () => {
3679
+ const apiClient = self.weaveAPI;
3680
+ await apiClient.appData.create(...);
3681
+ }
3682
+ ```
3683
+
3684
+ ### 4. **No Arbitrary API Calls**
3685
+ - Apps can **ONLY** use `this.weaveAPI` and `window.weaveDOM`
3686
+ - NO `fetch()`, NO `axios`, NO external HTTP requests
3687
+ - All backend calls go through `this.weaveAPI`
3688
+
3689
+ ### 5. **Check `this.isConnected` Before Rendering**
3690
+ - Background services run before DOM attachment
3691
+ - Always check if app is attached before calling `render()`:
3692
+ ```typescript
3693
+ if (this.isConnected) {
3694
+ this.render();
3695
+ }
3696
+ ```
3697
+
3698
+ ---
3699
+
3700
+ **Remember:**
3701
+ - Apps are isolated, secure, and communicate through secure message bridges
3702
+ - Always handle errors and provide user feedback
3703
+ - Use `this.weaveAPI` for ALL API calls (never `window.weaveAPI`)