@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.
- package/README.md +241 -0
- package/bin/build.js +81 -0
- package/bin/compile.js +109 -0
- package/bin/init.js +251 -0
- package/dist/SidekickAPIClient.d.ts +231 -0
- package/dist/SidekickAPIClient.d.ts.map +1 -0
- package/dist/SidekickAPIClient.js +274 -0
- package/dist/SidekickBaseApp.d.ts +177 -0
- package/dist/SidekickBaseApp.d.ts.map +1 -0
- package/dist/SidekickBaseApp.js +228 -0
- package/dist/SidekickDOMAPI.d.ts +433 -0
- package/dist/SidekickDOMAPI.d.ts.map +1 -0
- package/dist/SidekickDOMAPI.js +620 -0
- package/dist/WeaveAPIClient.d.ts +231 -0
- package/dist/WeaveAPIClient.d.ts.map +1 -0
- package/dist/WeaveAPIClient.js +274 -0
- package/dist/WeaveBaseApp.d.ts +177 -0
- package/dist/WeaveBaseApp.d.ts.map +1 -0
- package/dist/WeaveBaseApp.js +229 -0
- package/dist/WeaveDOMAPI.d.ts +433 -0
- package/dist/WeaveDOMAPI.d.ts.map +1 -0
- package/dist/WeaveDOMAPI.js +620 -0
- package/dist/global.d.ts +19 -0
- package/dist/global.d.ts.map +1 -0
- package/dist/global.js +17 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/package.json +45 -0
- package/scripts/copy-sdk-files.js +57 -0
- package/scripts/extract-settings-schema.js +235 -0
- package/templates/README.md +202 -0
- package/templates/WEAVE_SPEC.md +3703 -0
- package/tsconfig.app.json +17 -0
|
@@ -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`)
|