@omega.js/client 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/LICENSE +98 -0
- package/README.md +874 -0
- package/dist/index.js +999 -0
- package/dist/modules/analytics.js +584 -0
- package/dist/modules/auth.js +469 -0
- package/dist/modules/bindings.js +319 -0
- package/dist/modules/device.js +282 -0
- package/dist/modules/dom.js +96 -0
- package/dist/modules/features.js +30 -0
- package/dist/modules/firestore.js +313 -0
- package/dist/modules/form-manager.js +1577 -0
- package/dist/modules/icon-core.js +226 -0
- package/dist/modules/icon-renderer.js +149 -0
- package/dist/modules/live-page.js +235 -0
- package/dist/modules/logger.js +36 -0
- package/dist/modules/motion.js +853 -0
- package/dist/modules/notifications.js +433 -0
- package/dist/modules/path-prefix.js +22 -0
- package/dist/modules/request.js +223 -0
- package/dist/modules/sentry.js +108 -0
- package/dist/modules/service-worker.js +237 -0
- package/dist/modules/storage.js +133 -0
- package/dist/modules/triggers.js +117 -0
- package/dist/modules/utilities.js +479 -0
- package/dist/modules/vert-document.js +354 -0
- package/dist/modules/verts.js +1133 -0
- package/dist/vendor/account/engine.js +182 -0
- package/dist/vendor/account/features.js +220 -0
- package/dist/vendor/account/index.js +53 -0
- package/dist/vendor/account/schema.js +272 -0
- package/dist/vendor/account/subscription.js +38 -0
- package/dist/vendor/analytics/adapters/ga4.js +26 -0
- package/dist/vendor/analytics/adapters/meta.js +26 -0
- package/dist/vendor/analytics/adapters/resolve.js +130 -0
- package/dist/vendor/analytics/adapters/tiktok.js +27 -0
- package/dist/vendor/analytics/catalog.js +908 -0
- package/dist/vendor/analytics/consent.js +49 -0
- package/dist/vendor/analytics/core.js +141 -0
- package/dist/vendor/analytics/identity.js +136 -0
- package/dist/vendor/analytics/index.js +170 -0
- package/dist/vendor/analytics/logger.js +40 -0
- package/dist/vendor/analytics/transports/browser.js +110 -0
- package/dist/vendor/monitoring/browser.js +207 -0
- package/dist/vendor/monitoring/core.js +180 -0
- package/dist/vendor/monitoring/logger.js +39 -0
- package/docs/architecture.md +59 -0
- package/docs/bindings.md +235 -0
- package/docs/build-system.md +32 -0
- package/docs/cdp-debugging.md +29 -0
- package/docs/code-patterns.md +96 -0
- package/docs/common-tasks.md +36 -0
- package/docs/dependencies.md +19 -0
- package/docs/index.md +159 -0
- package/docs/modules.md +180 -0
- package/docs/shared/agent-docs.md +89 -0
- package/docs/shared/analytics.md +612 -0
- package/docs/shared/brands.md +51 -0
- package/docs/shared/breaking-changes.md +497 -0
- package/docs/shared/config.md +1387 -0
- package/docs/shared/deploys.md +215 -0
- package/docs/shared/icons.md +201 -0
- package/docs/shared/local-dev.md +147 -0
- package/docs/shared/logging.md +202 -0
- package/docs/shared/monitoring.md +153 -0
- package/docs/shared/publishing.md +183 -0
- package/docs/shared/rulings.md +34 -0
- package/docs/shared/testing.md +147 -0
- package/docs/shared/theming.md +604 -0
- package/docs/shared/translation.md +291 -0
- package/docs/shared/updates.md +61 -0
- package/docs/testing.md +9 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://itwcreativeworks.com">
|
|
3
|
+
<img src="https://cdn.itwcreativeworks.com/assets/itw-creative-works/images/logo/itw-creative-works-brandmark-black-x.svg" width="100px">
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>OMEGA Client</strong> — the shared frontend runtime (auth, data binding, storage, push notifications, error tracking) embedded by
|
|
9
|
+
<a href="../web/">@omega.js/web</a>,
|
|
10
|
+
<a href="../desktop/">@omega.js/desktop</a>, and
|
|
11
|
+
<a href="../extension/">@omega.js/extension</a>.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
- [Installation](#-installation)
|
|
16
|
+
- [Requirements](#-requirements)
|
|
17
|
+
- [Quick Start](#-quick-start)
|
|
18
|
+
- [Supported Environments](#-supported-environments)
|
|
19
|
+
- [Features](#-features)
|
|
20
|
+
- [Configuration](#-configuration)
|
|
21
|
+
- [API Reference](#-api-reference)
|
|
22
|
+
- [Manager Instance](#manager-instance)
|
|
23
|
+
- [Storage API](#storage-api)
|
|
24
|
+
- [Authentication](#authentication)
|
|
25
|
+
- [Data Binding System](#data-binding-system)
|
|
26
|
+
- [Firestore](#firestore)
|
|
27
|
+
- [Push Notifications](#push-notifications)
|
|
28
|
+
- [Service Worker](#service-worker)
|
|
29
|
+
- [Sentry Error Tracking](#sentry-error-tracking)
|
|
30
|
+
- [DOM Utilities](#dom-utilities)
|
|
31
|
+
- [Utility Functions](#utility-functions)
|
|
32
|
+
- [HTML Data Attributes](#-html-data-attributes)
|
|
33
|
+
- [Direct Module Imports](#-direct-module-imports)
|
|
34
|
+
- [Browser Support](#-browser-support)
|
|
35
|
+
- [Projects Using This Library](#-projects-using-this-library)
|
|
36
|
+
- [Support](#-support)
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
```shell
|
|
40
|
+
npm install @omega.js/client
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Requirements
|
|
44
|
+
- **Node.js**: >= 12
|
|
45
|
+
- **Browser**: Modern browsers (ES6+ support, transpiled to ES5 for older browsers)
|
|
46
|
+
|
|
47
|
+
**Note**: This library does not include TypeScript definitions.
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
import Manager from '@omega.js/client';
|
|
53
|
+
|
|
54
|
+
// Initialize with your configuration
|
|
55
|
+
await Manager.initialize({
|
|
56
|
+
environment: 'production',
|
|
57
|
+
buildTime: Date.now(),
|
|
58
|
+
brand: {
|
|
59
|
+
id: 'my-app',
|
|
60
|
+
name: 'My Application'
|
|
61
|
+
},
|
|
62
|
+
firebase: {
|
|
63
|
+
app: {
|
|
64
|
+
enabled: true,
|
|
65
|
+
config: {
|
|
66
|
+
apiKey: 'your-api-key',
|
|
67
|
+
authDomain: 'your-app.com', // brand host — the site self-hosts /__/auth/*
|
|
68
|
+
projectId: 'your-project-id',
|
|
69
|
+
storageBucket: 'your-app.appspot.com',
|
|
70
|
+
messagingSenderId: '123456789',
|
|
71
|
+
appId: '1:123456789:web:abcdef'
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
console.log('OMEGA Client initialized!');
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Supported Environments
|
|
81
|
+
|
|
82
|
+
Web Manager is designed to work in multiple environments:
|
|
83
|
+
|
|
84
|
+
| Environment | Support | Notes |
|
|
85
|
+
|-------------|---------|-------|
|
|
86
|
+
| **Web** | Full | Primary target, works with webpack bundlers |
|
|
87
|
+
| **Electron** | Full | Works in renderer process |
|
|
88
|
+
| **Chrome Extension** | Full | Content scripts and popup pages |
|
|
89
|
+
| **Firefox Extension** | Full | Content scripts and popup pages |
|
|
90
|
+
| **Safari Extension** | Partial | Basic functionality supported |
|
|
91
|
+
|
|
92
|
+
## Features
|
|
93
|
+
- **Firebase v12 Integration**: Modern Firebase Auth, Firestore, and Cloud Messaging
|
|
94
|
+
- **Data Binding System**: Reactive DOM updates with `data-omega-bind` attributes
|
|
95
|
+
- **Storage API**: Enhanced localStorage/sessionStorage with path-based access and JSON serialization
|
|
96
|
+
- **Utilities**: `clipboardCopy()`, `escapeHTML()`, `sanitizeURL()`, `renderMarkdown()`, `getContext()`, `showNotification()`, `getPlatform()`, `getBrowser()`, `getRuntime()`, `isMobile()`, `getDevice()`
|
|
97
|
+
- **DOM Utilities**: Dynamic script loading with retry/timeout support
|
|
98
|
+
- **Service Worker Management**: Registration, messaging, and state tracking
|
|
99
|
+
- **Push Notifications**: Firebase Cloud Messaging with auto-subscription
|
|
100
|
+
- **Error Tracking**: Sentry integration with session replay
|
|
101
|
+
- **App Check**: Optional reCAPTCHA Enterprise protection
|
|
102
|
+
- **Version Checking**: Auto-reload when new version is deployed
|
|
103
|
+
- **HTML Data Attributes**: Automatic `data-platform`, `data-browser`, `data-runtime`, `data-device` on `<html>`
|
|
104
|
+
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
> **Dev mode = local Firebase, zero flags.** When `environment` is `'development'`, the client auto-connects the REAL Auth + Firestore SDKs to the local emulator suite — never live Firebase. `omega dev` injects that environment automatically; production builds never connect. There is deliberately no live-Firebase opt-out for dev — build production locally if you truly need live.
|
|
108
|
+
>
|
|
109
|
+
> **Dev ports (N7):** the client resolves the port map with precedence `window.__OMEGA_DEV_PORTS__` (runtime channel — set by drivers like the devkit e2e harness after the page was built) → `config.dev.ports` (baked into the chrome by `omega dev`) → classic defaults (auth `:9099`, firestore `:8080`, functions `:5001`, hosting `:5002`). The emulator connects, `getFunctionsUrl()`, and `getApiUrl()` all read it, so bumped ports (a second brand's concurrent stack) reach the browser. Dev `getApiUrl()`: mapped `hosting` → plain `http://127.0.0.1:<port>` (the hosting emulator speaks http), mapped `https` → `mgr serve`'s mkcert proxy, no map → the classic `https://localhost:5002` serve assumption.
|
|
110
|
+
|
|
111
|
+
### Full Configuration Reference
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
await Manager.initialize({
|
|
115
|
+
// Environment: 'development' or 'production'
|
|
116
|
+
environment: 'production',
|
|
117
|
+
|
|
118
|
+
// Build timestamp for version checking
|
|
119
|
+
buildTime: Date.now(),
|
|
120
|
+
|
|
121
|
+
// Brand information
|
|
122
|
+
brand: {
|
|
123
|
+
id: 'my-app', // Used for custom protocol URLs
|
|
124
|
+
name: 'My Application',
|
|
125
|
+
description: 'App description',
|
|
126
|
+
type: 'Organization',
|
|
127
|
+
images: {
|
|
128
|
+
brandmark: 'https://example.com/logo.png',
|
|
129
|
+
wordmark: 'https://example.com/wordmark.png',
|
|
130
|
+
combomark: 'https://example.com/combomark.png'
|
|
131
|
+
},
|
|
132
|
+
contact: {
|
|
133
|
+
email: 'support@example.com',
|
|
134
|
+
phone: '+1-555-0123'
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
// Firebase configuration
|
|
139
|
+
firebase: {
|
|
140
|
+
app: {
|
|
141
|
+
enabled: true,
|
|
142
|
+
config: {
|
|
143
|
+
apiKey: 'your-api-key',
|
|
144
|
+
authDomain: 'your-app.com', // brand host — the site self-hosts /__/auth/*
|
|
145
|
+
projectId: 'your-project-id',
|
|
146
|
+
storageBucket: 'your-app.appspot.com',
|
|
147
|
+
messagingSenderId: '123456789',
|
|
148
|
+
appId: '1:123456789:web:abcdef'
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
appCheck: {
|
|
152
|
+
enabled: false,
|
|
153
|
+
config: {
|
|
154
|
+
siteKey: 'your-recaptcha-enterprise-site-key'
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// Authentication settings
|
|
160
|
+
auth: {
|
|
161
|
+
enabled: true,
|
|
162
|
+
config: {
|
|
163
|
+
redirects: {
|
|
164
|
+
authenticated: '/account', // Redirect after login
|
|
165
|
+
unauthenticated: '/signup' // Redirect when not logged in
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
// Sentry error tracking
|
|
171
|
+
sentry: {
|
|
172
|
+
enabled: true,
|
|
173
|
+
config: {
|
|
174
|
+
dsn: 'https://your-sentry-dsn',
|
|
175
|
+
release: '1.0.0',
|
|
176
|
+
replaysSessionSampleRate: 0.01, // 1% of sessions
|
|
177
|
+
replaysOnErrorSampleRate: 0.01 // 1% of error sessions
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// Push notifications
|
|
182
|
+
pushNotifications: {
|
|
183
|
+
enabled: true,
|
|
184
|
+
config: {
|
|
185
|
+
autoRequest: 60000, // Auto-request after 60s of first click
|
|
186
|
+
vapidKey: 'your-vapid-key' // Optional VAPID key
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// Service worker
|
|
191
|
+
serviceWorker: {
|
|
192
|
+
enabled: true,
|
|
193
|
+
config: {
|
|
194
|
+
path: '/service-worker.js'
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// Version checking (auto-reload on new version)
|
|
199
|
+
refreshNewVersion: {
|
|
200
|
+
enabled: true,
|
|
201
|
+
config: {
|
|
202
|
+
interval: 3600000 // Check every hour (1000 * 60 * 60)
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
// Valid hosts for auth redirects (security)
|
|
207
|
+
validRedirectHosts: ['example.com', 'app.example.com']
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Configuration Notes
|
|
212
|
+
|
|
213
|
+
- **Timeout values** can be specified as strings with math expressions: `'1000 * 60 * 60'` (evaluated safely)
|
|
214
|
+
- **Deep merge**: Your config is deep-merged with defaults, so you only need to specify what you want to change
|
|
215
|
+
- **Firebase required**: Most features require Firebase to be configured and enabled
|
|
216
|
+
|
|
217
|
+
## API Reference
|
|
218
|
+
|
|
219
|
+
### Manager Instance
|
|
220
|
+
|
|
221
|
+
The Manager is a singleton that provides access to all modules:
|
|
222
|
+
|
|
223
|
+
```javascript
|
|
224
|
+
import Manager from '@omega.js/client';
|
|
225
|
+
|
|
226
|
+
// Module getters
|
|
227
|
+
Manager.storage(); // Storage API
|
|
228
|
+
Manager.auth(); // Firebase Auth wrapper
|
|
229
|
+
Manager.bindings(); // Data binding system
|
|
230
|
+
Manager.firestore(); // Firestore wrapper
|
|
231
|
+
Manager.notifications(); // Push notifications
|
|
232
|
+
Manager.serviceWorker(); // Service worker management
|
|
233
|
+
Manager.sentry(); // Error tracking
|
|
234
|
+
Manager.dom(); // DOM utilities
|
|
235
|
+
Manager.utilities(); // Utility functions
|
|
236
|
+
Manager.verts(); // Verts (provider ladder + in-house fallback units)
|
|
237
|
+
|
|
238
|
+
// Helper methods
|
|
239
|
+
Manager.isDevelopment(); // Check if in development mode
|
|
240
|
+
Manager.getFunctionsUrl(); // Get Firebase Functions URL
|
|
241
|
+
Manager.getFunctionsUrl('development'); // Force development URL
|
|
242
|
+
Manager.getApiUrl(); // Get API URL (api.<brand.url host>)
|
|
243
|
+
Manager.isValidRedirectUrl('https://...'); // Validate redirect URL
|
|
244
|
+
|
|
245
|
+
// Firebase instances (after initialization)
|
|
246
|
+
Manager.firebaseApp; // Firebase App instance
|
|
247
|
+
Manager.firebaseAuth; // Firebase Auth instance
|
|
248
|
+
Manager.firebaseFirestore; // Firestore instance
|
|
249
|
+
Manager.firebaseMessaging; // FCM instance
|
|
250
|
+
|
|
251
|
+
// Configuration
|
|
252
|
+
Manager.config; // Access full configuration
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Storage API
|
|
256
|
+
|
|
257
|
+
Enhanced localStorage and sessionStorage with path-based access:
|
|
258
|
+
|
|
259
|
+
```javascript
|
|
260
|
+
const storage = Manager.storage();
|
|
261
|
+
|
|
262
|
+
// LocalStorage (persists across browser sessions)
|
|
263
|
+
storage.set('user.name', 'John');
|
|
264
|
+
storage.set('user.preferences', { theme: 'dark', lang: 'en' });
|
|
265
|
+
|
|
266
|
+
const name = storage.get('user.name'); // 'John'
|
|
267
|
+
const theme = storage.get('user.preferences.theme'); // 'dark'
|
|
268
|
+
const all = storage.get(); // Entire storage object
|
|
269
|
+
const fallback = storage.get('missing.path', 'default'); // 'default'
|
|
270
|
+
|
|
271
|
+
storage.remove('user.name');
|
|
272
|
+
storage.clear();
|
|
273
|
+
|
|
274
|
+
// SessionStorage (cleared when browser closes)
|
|
275
|
+
storage.session.set('temp.token', 'abc123');
|
|
276
|
+
storage.session.get('temp.token');
|
|
277
|
+
storage.session.remove('temp.token');
|
|
278
|
+
storage.session.clear();
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Features**:
|
|
282
|
+
- Automatic JSON serialization/deserialization
|
|
283
|
+
- Nested path access using dot notation
|
|
284
|
+
- Fallback to in-memory storage if localStorage unavailable
|
|
285
|
+
- Uses lodash `get`/`set` for reliable path access
|
|
286
|
+
|
|
287
|
+
### Authentication
|
|
288
|
+
|
|
289
|
+
Firebase Authentication wrapper with a promise-based auth settler:
|
|
290
|
+
|
|
291
|
+
```javascript
|
|
292
|
+
const auth = Manager.auth();
|
|
293
|
+
|
|
294
|
+
// Listen once — waits for auth to settle, fires exactly once
|
|
295
|
+
auth.listen({ once: true }, (state) => {
|
|
296
|
+
if (state.user) {
|
|
297
|
+
console.log('Logged in:', state.user.email);
|
|
298
|
+
console.log('Account:', state.account);
|
|
299
|
+
} else {
|
|
300
|
+
console.log('Not logged in');
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Persistent listener — fires on initial settle + every future auth change
|
|
305
|
+
const unsubscribe = auth.listen({}, (state) => {
|
|
306
|
+
console.log('Auth changed:', state.user?.email || 'signed out');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Check authentication status
|
|
310
|
+
if (auth.isAuthenticated()) {
|
|
311
|
+
const user = auth.getUser();
|
|
312
|
+
console.log('Logged in as:', user.email);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Sign in
|
|
316
|
+
await auth.signInWithEmailAndPassword('user@example.com', 'password');
|
|
317
|
+
|
|
318
|
+
// Sign in with custom token (from backend)
|
|
319
|
+
await auth.signInWithCustomToken('custom-jwt-token');
|
|
320
|
+
|
|
321
|
+
// Get ID token for API calls
|
|
322
|
+
const idToken = await auth.getIdToken();
|
|
323
|
+
const freshToken = await auth.getIdToken(true); // Force refresh
|
|
324
|
+
|
|
325
|
+
// Sign out
|
|
326
|
+
await auth.signOut();
|
|
327
|
+
|
|
328
|
+
// Stop persistent listener
|
|
329
|
+
unsubscribe();
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Auth Settler Design**:
|
|
333
|
+
|
|
334
|
+
On page load, Firebase Auth takes time to restore the user session. The auth settler (`Manager._authReady`) is a promise that resolves once Firebase determines the auth state (user or null). All `listen()` callbacks wait for this settler before firing — consumers never see an intermediate/unknown state.
|
|
335
|
+
|
|
336
|
+
- `{ once: true }` — Waits for the settler promise, calls the callback once, done. No cleanup needed.
|
|
337
|
+
- `{}` (persistent) — Gets the initial settled state via `_handleAuthStateChange`, then fires again on every future sign-in/sign-out.
|
|
338
|
+
|
|
339
|
+
**getUser() returns enhanced user object**:
|
|
340
|
+
```javascript
|
|
341
|
+
{
|
|
342
|
+
uid: 'abc123',
|
|
343
|
+
email: 'user@example.com',
|
|
344
|
+
displayName: 'John Doe', // Falls back to email or 'User'
|
|
345
|
+
photoURL: 'https://...', // Falls back to ui-avatars.com
|
|
346
|
+
emailVerified: true
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**HTML Auth Classes**:
|
|
351
|
+
- `.omega-signout` - Sign out button (shows confirmation dialog)
|
|
352
|
+
|
|
353
|
+
**Resolve Subscription State**:
|
|
354
|
+
|
|
355
|
+
Derives calculated subscription fields from raw account data. Returns only fields that require logic — raw data is on `account.subscription` directly.
|
|
356
|
+
|
|
357
|
+
```javascript
|
|
358
|
+
const resolved = auth.resolveSubscription(account);
|
|
359
|
+
// Or without an argument (falls back to stored auth state):
|
|
360
|
+
const resolved = auth.resolveSubscription();
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
When called without an argument, reads the account from `localStorage` (the last auth state saved by `listen()`). Pass an explicit account when you have one to avoid stale data.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
```javascript
|
|
367
|
+
{
|
|
368
|
+
plan: 'basic', // Effective plan ID right now ('basic' if cancelled/suspended)
|
|
369
|
+
active: true, // Has active access (active, trialing, or cancelling)
|
|
370
|
+
trialing: false, // In an active trial (status 'active' + unexpired trial)
|
|
371
|
+
cancelling: false, // Cancellation pending (status 'active' + cancellation.pending)
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Usage:
|
|
376
|
+
```javascript
|
|
377
|
+
auth.listen({ once: true }, (state) => {
|
|
378
|
+
const resolved = auth.resolveSubscription(state.account);
|
|
379
|
+
|
|
380
|
+
if (!resolved.active) {
|
|
381
|
+
// User is on free plan or subscription ended
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (resolved.trialing) {
|
|
385
|
+
// Show trial banner
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (resolved.cancelling) {
|
|
389
|
+
// Show "your plan will cancel at end of period" notice
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Use resolved.plan for effective plan ID
|
|
393
|
+
const product = products.find(p => p.id === resolved.plan);
|
|
394
|
+
});
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**⚠️ Auth State Timing**:
|
|
398
|
+
|
|
399
|
+
Methods like `auth.isAuthenticated()`, `auth.getUser()`, and `auth.getIdToken()` read the current state directly — they may return `null` before auth settles.
|
|
400
|
+
|
|
401
|
+
```javascript
|
|
402
|
+
// ❌ May fail on page load - auth state not yet determined
|
|
403
|
+
const token = await auth.getIdToken();
|
|
404
|
+
|
|
405
|
+
// ✅ Wait for auth to settle first
|
|
406
|
+
auth.listen({ once: true }, async (state) => {
|
|
407
|
+
if (state.user) {
|
|
408
|
+
const token = await auth.getIdToken(); // Safe
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Data Binding System
|
|
414
|
+
|
|
415
|
+
Reactive DOM updates with `data-omega-bind` attributes:
|
|
416
|
+
|
|
417
|
+
#### Basic Text Binding
|
|
418
|
+
```html
|
|
419
|
+
<!-- Display text content (default action) -->
|
|
420
|
+
<span data-omega-bind="auth.user.email"></span>
|
|
421
|
+
<span data-omega-bind="@text auth.user.displayName"></span>
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
#### Input/Textarea Value Binding
|
|
425
|
+
```html
|
|
426
|
+
<input data-omega-bind="@value settings.email" />
|
|
427
|
+
<textarea data-omega-bind="@value user.bio"></textarea>
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
#### Conditional Visibility
|
|
431
|
+
```html
|
|
432
|
+
<!-- Show when truthy -->
|
|
433
|
+
<div data-omega-bind="@show auth.user">Welcome back!</div>
|
|
434
|
+
|
|
435
|
+
<!-- Hide when truthy -->
|
|
436
|
+
<div data-omega-bind="@hide auth.user">Please log in</div>
|
|
437
|
+
|
|
438
|
+
<!-- Negation -->
|
|
439
|
+
<div data-omega-bind="@show !auth.user">Not logged in</div>
|
|
440
|
+
|
|
441
|
+
<!-- Comparisons -->
|
|
442
|
+
<div data-omega-bind="@show auth.account.plan === 'premium'">Premium content</div>
|
|
443
|
+
<div data-omega-bind="@hide settings.notifications === false">Notifications on</div>
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
#### Attribute Binding
|
|
447
|
+
```html
|
|
448
|
+
<img data-omega-bind="@attr src auth.user.photoURL" />
|
|
449
|
+
<a data-omega-bind="@attr href settings.profileUrl">Profile</a>
|
|
450
|
+
<input data-omega-bind="@attr disabled auth.loading" />
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
#### Style Binding
|
|
454
|
+
```html
|
|
455
|
+
<!-- CSS custom properties -->
|
|
456
|
+
<div data-omega-bind="@style --rating-width ratings.percent"></div>
|
|
457
|
+
|
|
458
|
+
<!-- Inline styles -->
|
|
459
|
+
<div data-omega-bind="@style background-color theme.primary"></div>
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
#### Multiple Actions
|
|
463
|
+
Combine actions with commas:
|
|
464
|
+
```html
|
|
465
|
+
<img data-omega-bind="@show auth.user, @attr src auth.user.photoURL, @attr alt auth.user.displayName" />
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
#### JavaScript API
|
|
469
|
+
```javascript
|
|
470
|
+
const bindings = Manager.bindings();
|
|
471
|
+
|
|
472
|
+
// Update context data
|
|
473
|
+
bindings.update({
|
|
474
|
+
settings: { theme: 'dark', email: 'user@example.com' },
|
|
475
|
+
custom: { value: 123 }
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// Get current context
|
|
479
|
+
const context = bindings.getContext();
|
|
480
|
+
|
|
481
|
+
// Clear all bindings
|
|
482
|
+
bindings.clear();
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
#### Skeleton Loaders
|
|
486
|
+
```html
|
|
487
|
+
<!-- Shows shimmer animation until bound -->
|
|
488
|
+
<span data-omega-bind="auth.user.name" class="omega-binding-skeleton"></span>
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
The skeleton automatically:
|
|
492
|
+
- Displays shimmer animation while loading
|
|
493
|
+
- Fades in smoothly when data arrives
|
|
494
|
+
- Adds `omega-bound` class when complete
|
|
495
|
+
- Respects `prefers-reduced-motion`
|
|
496
|
+
|
|
497
|
+
#### Supported Actions
|
|
498
|
+
|
|
499
|
+
| Action | Syntax | Description |
|
|
500
|
+
|--------|--------|-------------|
|
|
501
|
+
| `@text` | `@text path` | Set text content (default) |
|
|
502
|
+
| `@value` | `@value path` | Set input/textarea value |
|
|
503
|
+
| `@show` | `@show condition` | Show element if truthy |
|
|
504
|
+
| `@hide` | `@hide condition` | Hide element if truthy |
|
|
505
|
+
| `@attr` | `@attr name path` | Set attribute value |
|
|
506
|
+
| `@style` | `@style prop path` | Set CSS property or variable |
|
|
507
|
+
|
|
508
|
+
### Firestore
|
|
509
|
+
|
|
510
|
+
Simplified Firestore wrapper with chainable queries:
|
|
511
|
+
|
|
512
|
+
```javascript
|
|
513
|
+
const db = Manager.firestore();
|
|
514
|
+
|
|
515
|
+
// Document operations - two syntax options
|
|
516
|
+
await db.doc('users/user123').set({ name: 'John', age: 30 });
|
|
517
|
+
await db.doc('users', 'user123').update({ age: 31 });
|
|
518
|
+
|
|
519
|
+
const docSnap = await db.doc('users/user123').get();
|
|
520
|
+
if (docSnap.exists()) {
|
|
521
|
+
console.log('Data:', docSnap.data());
|
|
522
|
+
console.log('ID:', docSnap.id);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
await db.doc('users/user123').delete();
|
|
526
|
+
|
|
527
|
+
// Collection queries
|
|
528
|
+
const snapshot = await db.collection('users').get();
|
|
529
|
+
console.log('Count:', snapshot.size);
|
|
530
|
+
console.log('Empty:', snapshot.empty);
|
|
531
|
+
snapshot.docs.forEach(doc => {
|
|
532
|
+
console.log(doc.id, doc.data());
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Query with filters (chainable)
|
|
536
|
+
const results = await db.collection('users')
|
|
537
|
+
.where('age', '>=', 18)
|
|
538
|
+
.where('active', '==', true)
|
|
539
|
+
.orderBy('createdAt', 'desc')
|
|
540
|
+
.limit(20)
|
|
541
|
+
.get();
|
|
542
|
+
|
|
543
|
+
// Pagination
|
|
544
|
+
const page2 = await db.collection('users')
|
|
545
|
+
.orderBy('name')
|
|
546
|
+
.startAt('M')
|
|
547
|
+
.endAt('N')
|
|
548
|
+
.get();
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
**Where Operators**: `<`, `<=`, `==`, `!=`, `>=`, `>`, `array-contains`, `in`, `array-contains-any`, `not-in`
|
|
552
|
+
|
|
553
|
+
### Push Notifications
|
|
554
|
+
|
|
555
|
+
Firebase Cloud Messaging integration:
|
|
556
|
+
|
|
557
|
+
```javascript
|
|
558
|
+
const notifications = Manager.notifications();
|
|
559
|
+
|
|
560
|
+
// Check support
|
|
561
|
+
if (notifications.isSupported()) {
|
|
562
|
+
console.log('Push notifications available');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Check subscription status
|
|
566
|
+
const isSubscribed = await notifications.isSubscribed();
|
|
567
|
+
|
|
568
|
+
// Subscribe
|
|
569
|
+
try {
|
|
570
|
+
const result = await notifications.subscribe({
|
|
571
|
+
vapidKey: 'your-vapid-key' // Optional
|
|
572
|
+
});
|
|
573
|
+
console.log('Token:', result.token);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (error.message.includes('permission')) {
|
|
576
|
+
console.log('User denied permission');
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Unsubscribe
|
|
581
|
+
await notifications.unsubscribe();
|
|
582
|
+
|
|
583
|
+
// Get current token
|
|
584
|
+
const token = await notifications.getToken();
|
|
585
|
+
|
|
586
|
+
// Listen for foreground messages
|
|
587
|
+
const unsubscribe = await notifications.onMessage((payload) => {
|
|
588
|
+
console.log('Received:', payload);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// Sync subscription with auth state
|
|
592
|
+
await notifications.syncSubscription();
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
**Features**:
|
|
596
|
+
- Stores subscription in localStorage and Firestore
|
|
597
|
+
- Tracks device context (platform, runtime, device)
|
|
598
|
+
- Auto-requests after configurable delay post-click
|
|
599
|
+
- Syncs with user authentication state
|
|
600
|
+
|
|
601
|
+
### Service Worker
|
|
602
|
+
|
|
603
|
+
Service worker registration and messaging:
|
|
604
|
+
|
|
605
|
+
```javascript
|
|
606
|
+
const sw = Manager.serviceWorker();
|
|
607
|
+
|
|
608
|
+
// Check support
|
|
609
|
+
if (sw.isSupported()) {
|
|
610
|
+
console.log('Service workers available');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Register (done automatically during init if enabled)
|
|
614
|
+
const registration = await sw.register({
|
|
615
|
+
path: '/service-worker.js',
|
|
616
|
+
scope: '/'
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Wait for ready state
|
|
620
|
+
await sw.ready();
|
|
621
|
+
|
|
622
|
+
// Get registration
|
|
623
|
+
const reg = sw.getRegistration();
|
|
624
|
+
|
|
625
|
+
// Post message with response
|
|
626
|
+
try {
|
|
627
|
+
const response = await sw.postMessage({
|
|
628
|
+
command: 'cache-clear',
|
|
629
|
+
payload: { pattern: '*.js' }
|
|
630
|
+
}, { timeout: 5000 });
|
|
631
|
+
console.log('Response:', response);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
console.error('Timeout or error:', error);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Listen for messages from service worker
|
|
637
|
+
const unsubscribe = sw.onMessage('notification-click', (data, event) => {
|
|
638
|
+
console.log('Clicked:', data);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// Get current state
|
|
642
|
+
const state = sw.getState(); // 'none', 'installing', 'waiting', 'active', 'unknown'
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Sentry Error Tracking
|
|
646
|
+
|
|
647
|
+
Automatic error tracking with Sentry:
|
|
648
|
+
|
|
649
|
+
```javascript
|
|
650
|
+
const sentry = Manager.sentry();
|
|
651
|
+
|
|
652
|
+
// Capture an exception
|
|
653
|
+
try {
|
|
654
|
+
throw new Error('Something went wrong');
|
|
655
|
+
} catch (error) {
|
|
656
|
+
sentry.captureException(error, {
|
|
657
|
+
tags: { feature: 'checkout' },
|
|
658
|
+
extra: { orderId: '12345' }
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
**Automatic Features**:
|
|
664
|
+
- Environment and release tracking from config
|
|
665
|
+
- User context from auth state (uid, email)
|
|
666
|
+
- Session duration tracking
|
|
667
|
+
- Filters out Lighthouse and automated browsers (Selenium, Puppeteer)
|
|
668
|
+
- Blocks sending in development mode
|
|
669
|
+
- Dynamic import to reduce bundle size
|
|
670
|
+
|
|
671
|
+
### DOM Utilities
|
|
672
|
+
|
|
673
|
+
```javascript
|
|
674
|
+
import { loadScript, ready } from '@omega.js/client/modules/dom';
|
|
675
|
+
// Or: const { loadScript, ready } = Manager.dom();
|
|
676
|
+
|
|
677
|
+
// Wait for DOM ready
|
|
678
|
+
await ready();
|
|
679
|
+
|
|
680
|
+
// Load external script
|
|
681
|
+
await loadScript({
|
|
682
|
+
src: 'https://example.com/script.js',
|
|
683
|
+
async: true,
|
|
684
|
+
defer: false,
|
|
685
|
+
crossorigin: 'anonymous',
|
|
686
|
+
integrity: 'sha384-...',
|
|
687
|
+
timeout: 30000,
|
|
688
|
+
retries: 2,
|
|
689
|
+
parent: document.head,
|
|
690
|
+
attributes: { 'data-custom': 'value' }
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Simple string syntax
|
|
694
|
+
await loadScript('https://example.com/script.js');
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
**loadScript Options**:
|
|
698
|
+
|
|
699
|
+
| Option | Type | Default | Description |
|
|
700
|
+
|--------|------|---------|-------------|
|
|
701
|
+
| `src` | string | required | Script URL |
|
|
702
|
+
| `async` | boolean | `true` | Load asynchronously |
|
|
703
|
+
| `defer` | boolean | `false` | Defer execution |
|
|
704
|
+
| `crossorigin` | boolean/string | `false` | CORS setting |
|
|
705
|
+
| `integrity` | string | `null` | SRI hash |
|
|
706
|
+
| `timeout` | number | `60000` | Timeout in ms |
|
|
707
|
+
| `retries` | number | `0` | Retry attempts |
|
|
708
|
+
| `parent` | Element | `document.head` | Parent element |
|
|
709
|
+
| `attributes` | object | `{}` | Custom attributes |
|
|
710
|
+
|
|
711
|
+
### Utility Functions
|
|
712
|
+
|
|
713
|
+
```javascript
|
|
714
|
+
import {
|
|
715
|
+
clipboardCopy,
|
|
716
|
+
escapeHTML,
|
|
717
|
+
sanitizeURL,
|
|
718
|
+
renderMarkdown,
|
|
719
|
+
showNotification,
|
|
720
|
+
getPlatform,
|
|
721
|
+
getBrowser,
|
|
722
|
+
getRuntime,
|
|
723
|
+
isMobile,
|
|
724
|
+
getDevice,
|
|
725
|
+
getContext
|
|
726
|
+
} from '@omega.js/client/modules/utilities';
|
|
727
|
+
// Or: const utils = Manager.utilities();
|
|
728
|
+
|
|
729
|
+
// Copy to clipboard (rejects when the clipboard refuses — catch it)
|
|
730
|
+
await clipboardCopy('Text to copy');
|
|
731
|
+
await clipboardCopy(document.querySelector('#input')); // From element
|
|
732
|
+
|
|
733
|
+
// Escape HTML (XSS prevention)
|
|
734
|
+
const safe = escapeHTML('<script>alert("xss")</script>');
|
|
735
|
+
// '<script>alert("xss")</script>'
|
|
736
|
+
|
|
737
|
+
// Sanitize a URL (returns '' for javascript:, data:, and every non-http(s) scheme)
|
|
738
|
+
sanitizeURL('https://example.com/a'); // 'https://example.com/a'
|
|
739
|
+
sanitizeURL('javascript:alert(1)'); // ''
|
|
740
|
+
|
|
741
|
+
// Render untrusted text as safe markup — escape-first mini-markdown
|
|
742
|
+
// (headings, fenced + inline code, lists, bold/italic, http(s) links only)
|
|
743
|
+
renderMarkdown('## Spec\n\nOne **bold** and `code`.');
|
|
744
|
+
// '<h5 class="h6 mt-3 mb-2">Spec</h5><p>One <strong>bold</strong> and <code>code</code>.</p>'
|
|
745
|
+
|
|
746
|
+
// Show notification (Bootstrap-styled)
|
|
747
|
+
showNotification('Success!', { type: 'success', timeout: 5000 });
|
|
748
|
+
showNotification('Error!', 'danger');
|
|
749
|
+
showNotification(new Error('Failed'), { timeout: 0 }); // No auto-dismiss
|
|
750
|
+
|
|
751
|
+
// Platform detection
|
|
752
|
+
getPlatform(); // 'windows', 'mac', 'linux', 'ios', 'android', 'chromeos', 'unknown'
|
|
753
|
+
|
|
754
|
+
// Browser detection
|
|
755
|
+
getBrowser(); // 'chrome', 'firefox', 'safari', 'edge', 'opera', 'brave', null
|
|
756
|
+
|
|
757
|
+
// Runtime detection
|
|
758
|
+
getRuntime(); // 'web', 'browser-extension'
|
|
759
|
+
|
|
760
|
+
// Device detection
|
|
761
|
+
isMobile(); // true/false
|
|
762
|
+
getDevice(); // 'mobile' (<768px), 'tablet' (768-1199px), 'desktop' (>=1200px)
|
|
763
|
+
|
|
764
|
+
// Full context
|
|
765
|
+
getContext();
|
|
766
|
+
// {
|
|
767
|
+
// client: { language, mobile, device, platform, browser, vendor, runtime, userAgent, url },
|
|
768
|
+
// geolocation: { ip, country, region, city, latitude, longitude }
|
|
769
|
+
// }
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
**showNotification Options**:
|
|
773
|
+
|
|
774
|
+
| Option | Type | Default | Description |
|
|
775
|
+
|--------|------|---------|-------------|
|
|
776
|
+
| `type` | string | `'info'` | `'info'`, `'success'`, `'warning'`, `'danger'` |
|
|
777
|
+
| `timeout` | number | `5000` | Auto-dismiss after ms (0 = never) |
|
|
778
|
+
|
|
779
|
+
## HTML Data Attributes
|
|
780
|
+
|
|
781
|
+
Web Manager automatically sets these attributes on the `<html>` element during initialization:
|
|
782
|
+
|
|
783
|
+
```html
|
|
784
|
+
<html data-platform="mac" data-browser="chrome" data-runtime="web" data-device="desktop">
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
| Attribute | Values | Description |
|
|
788
|
+
|-----------|--------|-------------|
|
|
789
|
+
| `data-platform` | `windows`, `mac`, `linux`, `ios`, `android`, `chromeos`, `unknown` | Operating system |
|
|
790
|
+
| `data-browser` | `chrome`, `firefox`, `safari`, `edge`, `opera`, `brave` | Browser name |
|
|
791
|
+
| `data-runtime` | `web`, `browser-extension` | Runtime environment |
|
|
792
|
+
| `data-device` | `mobile`, `tablet`, `desktop` | Device type by screen width |
|
|
793
|
+
|
|
794
|
+
**CSS Usage**:
|
|
795
|
+
```css
|
|
796
|
+
/* Platform-specific styles */
|
|
797
|
+
[data-platform="ios"] .download-btn { display: none; }
|
|
798
|
+
[data-platform="windows"] .app-icon { content: url('windows-icon.png'); }
|
|
799
|
+
|
|
800
|
+
/* Browser-specific styles */
|
|
801
|
+
[data-browser="safari"] .webkit-fix { -webkit-transform: translateZ(0); }
|
|
802
|
+
[data-browser="firefox"] .gecko-fix { overflow: hidden; }
|
|
803
|
+
|
|
804
|
+
/* Device-responsive styles */
|
|
805
|
+
[data-device="mobile"] .sidebar { display: none; }
|
|
806
|
+
[data-device="desktop"] .mobile-menu { display: none; }
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
## Direct Module Imports
|
|
810
|
+
|
|
811
|
+
Import individual modules to reduce bundle size:
|
|
812
|
+
|
|
813
|
+
```javascript
|
|
814
|
+
// Storage only
|
|
815
|
+
import Storage from '@omega.js/client/modules/storage';
|
|
816
|
+
const storage = new Storage();
|
|
817
|
+
|
|
818
|
+
// Utilities only
|
|
819
|
+
import { clipboardCopy, escapeHTML } from '@omega.js/client/modules/utilities';
|
|
820
|
+
|
|
821
|
+
// DOM utilities only
|
|
822
|
+
import { loadScript, ready } from '@omega.js/client/modules/dom';
|
|
823
|
+
|
|
824
|
+
// Full manager (default)
|
|
825
|
+
import Manager from '@omega.js/client';
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
**Available Modules**:
|
|
829
|
+
- `@omega.js/client/modules/storage` - Storage class
|
|
830
|
+
- `@omega.js/client/modules/utilities` - Utility functions
|
|
831
|
+
- `@omega.js/client/modules/dom` - DOM utilities
|
|
832
|
+
- `@omega.js/client/modules/auth` - Auth class (requires Manager)
|
|
833
|
+
- `@omega.js/client/modules/bindings` - Bindings class (requires Manager)
|
|
834
|
+
- `@omega.js/client/modules/firestore` - Firestore class (requires Manager)
|
|
835
|
+
- `@omega.js/client/modules/notifications` - Notifications class (requires Manager)
|
|
836
|
+
- `@omega.js/client/modules/service-worker` - ServiceWorker class (requires Manager)
|
|
837
|
+
- `@omega.js/client/modules/sentry` - Sentry class (requires Manager)
|
|
838
|
+
|
|
839
|
+
## Browser Support
|
|
840
|
+
|
|
841
|
+
Web Manager is transpiled to ES5 for broad browser support:
|
|
842
|
+
|
|
843
|
+
| Browser | Version | Support |
|
|
844
|
+
|---------|---------|---------|
|
|
845
|
+
| Chrome | 60+ | Full |
|
|
846
|
+
| Firefox | 55+ | Full |
|
|
847
|
+
| Safari | 11+ | Full |
|
|
848
|
+
| Edge | 79+ | Full |
|
|
849
|
+
| IE | 11 | Not supported |
|
|
850
|
+
|
|
851
|
+
**Notes**:
|
|
852
|
+
- Service Workers require HTTPS (except localhost)
|
|
853
|
+
- Push Notifications require Service Worker support
|
|
854
|
+
- Some features use modern APIs with fallbacks
|
|
855
|
+
|
|
856
|
+
## Projects Using This Library
|
|
857
|
+
|
|
858
|
+
- [Somiibo](https://somiibo.com/): A Social Media Bot with an open-source module library
|
|
859
|
+
- [JekyllUp](https://jekyllup.com/): A website devoted to sharing the best Jekyll themes
|
|
860
|
+
- [Slapform](https://slapform.com/): A backend provider for HTML forms on static sites
|
|
861
|
+
- [SoundGrail Music App](https://app.soundgrail.com/): A resource for producers, musicians, and DJs
|
|
862
|
+
- [Hammock Report](https://hammockreport.com/): An API for exploring and listing backyard products
|
|
863
|
+
|
|
864
|
+
*Want your project listed? [Open an issue](https://github.com/itw-creative-works/web-manager/issues)!*
|
|
865
|
+
|
|
866
|
+
## Support
|
|
867
|
+
|
|
868
|
+
If you're having issues or have questions:
|
|
869
|
+
- [Open an issue](https://github.com/itw-creative-works/web-manager/issues) on GitHub
|
|
870
|
+
- Include code samples and relevant files to help us help you faster
|
|
871
|
+
|
|
872
|
+
## License
|
|
873
|
+
|
|
874
|
+
[Elastic License 2.0](LICENSE). The source is free to use and modify; a license key unlocks payments in production deploys and removes the attribution (local dev and test payments are always free); and you may not offer `@omega.js/client` to third parties as a hosted or managed service.
|