@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
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* icon-core — the ONE place Font Awesome icon semantics live (C4 cp108).
|
|
3
|
+
*
|
|
4
|
+
* Pure functions, zero runtime assumptions: no DOM, no fs, no transport.
|
|
5
|
+
* Web's build-time inlining pass (@omega.js/web src/inline-icons.js) and
|
|
6
|
+
* @omega.js/desktop's main-process icon server both consume THIS module, so
|
|
7
|
+
* lookup rules and rendered SVG markup can never drift between surfaces
|
|
8
|
+
* again. File reading stays with each consumer (build tags read at build
|
|
9
|
+
* time, desktop main reads at runtime) — this module owns every decision
|
|
10
|
+
* ABOUT the files: valid names/styles, candidate lookup order, the inline
|
|
11
|
+
* root attributes, the package preference order (Pro when the brand
|
|
12
|
+
* supplies it, free floor), and alias mapping from the icon set's own
|
|
13
|
+
* metadata ('search' → 'magnifying-glass').
|
|
14
|
+
*
|
|
15
|
+
* CJS on purpose: template-kit and Electron main require() it directly (via
|
|
16
|
+
* the package's dist exports); browser modules import it with standard
|
|
17
|
+
* interop.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// Icon asset packages, best-first (cp111): a brand that supplies Font
|
|
21
|
+
// Awesome Pro gets it automatically; the free set is the always-present
|
|
22
|
+
// floor (a declared dependency of web + desktop). Pro is NEVER a dependency
|
|
23
|
+
// of any omega package — redistribution is a license violation — each brand
|
|
24
|
+
// brings its own licensed copy (npm token install, or a fontawesome.com
|
|
25
|
+
// download dir via OMEGA_FONTAWESOME_ROOT).
|
|
26
|
+
const PACKAGES = ['@fortawesome/fontawesome-pro', '@fortawesome/fontawesome-free'];
|
|
27
|
+
|
|
28
|
+
// The ONE emitted directory name, every surface: a site serves it at
|
|
29
|
+
// /assets/icons/<style>/<name>.svg, an extension packs it at
|
|
30
|
+
// assets/icons/… ([#619](https://github.com/Omega-JS-Stack/omega/issues/619)).
|
|
31
|
+
// `icons` and not `fa` because flags ride the same tree as a second
|
|
32
|
+
// namespace, and nothing about the channel is Font Awesome specific. It lives
|
|
33
|
+
// HERE with the rest of the lookup rules — the build side re-exports it
|
|
34
|
+
// (@omega.js/devkit/icons) and every browser transport derives its base from
|
|
35
|
+
// it, so the name can never drift between what a build emits and what a page
|
|
36
|
+
// fetches. (The extension manifest's web_accessible_resources stays a literal:
|
|
37
|
+
// manifest.json is static JSON, read by Chrome, not by us.)
|
|
38
|
+
const ICONS_DIR = 'icons';
|
|
39
|
+
|
|
40
|
+
// The free set's svgs/ directories. Pro supplies more (light, thin,
|
|
41
|
+
// duotone, sharp-*, …) — validation is by shape, not this list, so new
|
|
42
|
+
// Pro families work without this module tracking Font Awesome's catalog.
|
|
43
|
+
const STYLES = ['solid', 'regular', 'brands'];
|
|
44
|
+
|
|
45
|
+
// Style dirs are path segments too — same traversal rule as names, so an
|
|
46
|
+
// unknown style can only ever be a file-not-found, never an escape.
|
|
47
|
+
const STYLE_REGEX = /^[a-z][a-z-]*$/;
|
|
48
|
+
|
|
49
|
+
// Lowercase slug names only — lookups build file paths, so this whitelist
|
|
50
|
+
// is also what keeps callers (like desktop's IPC channel) from ever reading
|
|
51
|
+
// outside the icon directories.
|
|
52
|
+
const NAME_REGEX = /^[a-z0-9-]+$/;
|
|
53
|
+
|
|
54
|
+
// Attributes injected on the <svg> root at serve time: icons size to the
|
|
55
|
+
// surrounding font and inherit its color. overflow="visible" mirrors FA's
|
|
56
|
+
// own kit CSS (.svg-inline--fa { overflow: visible }) — FA 7 glyphs may
|
|
57
|
+
// draw OUTSIDE their viewBox (fa-lock's shackle peaks at y=-32 in a
|
|
58
|
+
// 0 0 384 512 box) and the SVG-root default of overflow:hidden clips them.
|
|
59
|
+
const SVG_ATTRIBUTES = [
|
|
60
|
+
['width', 'width="1em"'],
|
|
61
|
+
['height', 'height="1em"'],
|
|
62
|
+
['fill', 'fill="currentColor"'],
|
|
63
|
+
['aria-hidden', 'aria-hidden="true"'],
|
|
64
|
+
['focusable', 'focusable="false"'],
|
|
65
|
+
['overflow', 'overflow="visible"'],
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether a value is a valid icon slug ('magnifying-glass').
|
|
70
|
+
*
|
|
71
|
+
* @param {*} name - Candidate icon name.
|
|
72
|
+
* @returns {boolean} True for lowercase slug strings.
|
|
73
|
+
*/
|
|
74
|
+
function isValidIconName(name) {
|
|
75
|
+
return typeof name === 'string' && NAME_REGEX.test(name);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether a value is a plausible icon style ('solid', 'duotone',
|
|
80
|
+
* 'sharp-light'). Shape-validated (path-safe slug), not whitelist-validated:
|
|
81
|
+
* whether the style actually exists in the supplied icon set is decided by
|
|
82
|
+
* the file lookup — a bogus style is a missing icon, never a crash or an
|
|
83
|
+
* escape from the icon directories.
|
|
84
|
+
*
|
|
85
|
+
* @param {*} style - Candidate style.
|
|
86
|
+
* @returns {boolean} True for lowercase path-safe style slugs.
|
|
87
|
+
*/
|
|
88
|
+
function isValidStyle(style) {
|
|
89
|
+
return typeof style === 'string' && STYLE_REGEX.test(style);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Inject the shared root attributes into a raw icon SVG string — each one
|
|
94
|
+
* only when the root doesn't already carry it, so hand-authored SVGs keep
|
|
95
|
+
* their own sizing/fill.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} svg - Raw SVG source.
|
|
98
|
+
* @returns {string} SVG with the inline-icon root attributes.
|
|
99
|
+
*/
|
|
100
|
+
function injectSvgAttributes(svg) {
|
|
101
|
+
if (typeof svg !== 'string' || !svg.includes('<svg')) return svg;
|
|
102
|
+
|
|
103
|
+
// No comment survives into the injected markup: Font Awesome ships its
|
|
104
|
+
// license comment inside every glyph, and that comment's `-->` TERMINATES
|
|
105
|
+
// any HTML comment the icon happens to sit inside — a commented-out block
|
|
106
|
+
// carrying an icon (body.html's parked flash-sale banner) rendered on every
|
|
107
|
+
// page. Attribution stays with the set: the emitted /assets/icons/ files
|
|
108
|
+
// are copies of the originals, comments intact.
|
|
109
|
+
return svg.replace(/<!--[\s\S]*?-->/g, '').replace(/<svg([^>]*)>/, (match, existingAttrs) => {
|
|
110
|
+
const toAdd = SVG_ATTRIBUTES
|
|
111
|
+
.filter(([attr]) => !existingAttrs.includes(`${attr}=`))
|
|
112
|
+
.map(([, pair]) => pair);
|
|
113
|
+
|
|
114
|
+
return toAdd.length ? `<svg${existingAttrs} ${toAdd.join(' ')}>` : match;
|
|
115
|
+
}).trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Relative candidate paths for an icon, in lookup order: the requested
|
|
120
|
+
* style first, then the brands fallback — so logos like 'apple' resolve
|
|
121
|
+
* without callers knowing which side of the set they live on.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} name - Icon slug.
|
|
124
|
+
* @param {string} style - Requested style.
|
|
125
|
+
* @returns {string[]} Relative paths to try against each icon root.
|
|
126
|
+
*/
|
|
127
|
+
function candidateRelPaths(name, style) {
|
|
128
|
+
const candidates = [`${style}/${name}.svg`];
|
|
129
|
+
if (style !== 'brands') {
|
|
130
|
+
candidates.push(`brands/${name}.svg`);
|
|
131
|
+
}
|
|
132
|
+
return candidates;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Font Awesome's class model: a base WEIGHT class, an optional FAMILY
|
|
136
|
+
// prefix class, and everything else is a modifier or the icon name.
|
|
137
|
+
const BASE_STYLE_CLASSES = {
|
|
138
|
+
'fa-solid': 'solid', fas: 'solid',
|
|
139
|
+
'fa-regular': 'regular', far: 'regular',
|
|
140
|
+
'fa-light': 'light', fal: 'light',
|
|
141
|
+
'fa-thin': 'thin', fat: 'thin',
|
|
142
|
+
'fa-brands': 'brands', fab: 'brands',
|
|
143
|
+
};
|
|
144
|
+
const FAMILY_CLASSES = { 'fa-sharp': 'sharp', 'fa-duotone': 'duotone', fad: 'duotone', 'fa-sharp-duotone': 'sharp-duotone' };
|
|
145
|
+
|
|
146
|
+
// fa-* classes that are modifiers (style/size/animation/layout), not icon names.
|
|
147
|
+
// The size roster is the sheet's $fa-sizes map, all 12 of it (@omega.js/web
|
|
148
|
+
// core/css/core/_custom-font-awesome.scss). A shipped size class this list
|
|
149
|
+
// misses reads as an icon NAME and the renderer hunts for a glyph called '4xl'.
|
|
150
|
+
const MODIFIER_REGEX = /^fa-(?:solid|brands|regular|light|thin|duotone|sharp-duotone|sharp|fw|2xs|xs|sm|base|md|lg|xl|2xl|3xl|4xl|5xl|6xl|[0-9]+x|spin|spin-pulse|spin-reverse|pulse|beat|fade|beat-fade|bounce|shake|flip(?:-horizontal|-vertical|-both)?|rotate-(?:90|180|270|by)|inverse|border|pull-left|pull-right|stack(?:-1x|-2x)?|li|ul|sr-only)$/;
|
|
151
|
+
|
|
152
|
+
// Country flags are the SECOND namespace on this pipeline (#619) — not Font
|
|
153
|
+
// Awesome, but the same markup shape, the same lookup, the same emitted tree:
|
|
154
|
+
// `<i class="omega-flag omega-flag-us">` is `flags/us.svg`.
|
|
155
|
+
const FLAG_CLASS_REGEX = /^omega-flag-([a-z0-9-]+)$/;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse an element's class list the way Font Awesome does:
|
|
159
|
+
* `fa-sharp fa-light fa-play me-2` → { name: 'play', style: 'sharp-light' }.
|
|
160
|
+
* Weight defaults to solid; a family class prefixes it (duotone-solid lives
|
|
161
|
+
* in the bare `duotone` dir). A flag class (`omega-flag-us`) is the one
|
|
162
|
+
* non-FA namespace and resolves to the `flags` style. Null when no icon name
|
|
163
|
+
* is present.
|
|
164
|
+
*
|
|
165
|
+
* @param {Iterable<string>} classList - Element class names.
|
|
166
|
+
* @returns {{ name: string, style: string }|null} Parsed lookup, or null.
|
|
167
|
+
*/
|
|
168
|
+
function parseIconClasses(classList) {
|
|
169
|
+
let base = 'solid';
|
|
170
|
+
let family = '';
|
|
171
|
+
let name = null;
|
|
172
|
+
|
|
173
|
+
for (const cls of classList) {
|
|
174
|
+
const flag = FLAG_CLASS_REGEX.exec(cls);
|
|
175
|
+
if (flag) {
|
|
176
|
+
return { name: flag[1], style: 'flags' };
|
|
177
|
+
}
|
|
178
|
+
if (BASE_STYLE_CLASSES[cls]) {
|
|
179
|
+
base = BASE_STYLE_CLASSES[cls];
|
|
180
|
+
} else if (FAMILY_CLASSES[cls]) {
|
|
181
|
+
family = FAMILY_CLASSES[cls];
|
|
182
|
+
} else if (!name && cls.startsWith('fa-') && !MODIFIER_REGEX.test(cls)) {
|
|
183
|
+
name = cls.slice(3);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!name) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const style = (base === 'brands' || !family) ? base
|
|
191
|
+
: (family === 'duotone' && base === 'solid') ? 'duotone'
|
|
192
|
+
: `${family}-${base}`;
|
|
193
|
+
return { name, style };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Build the alias → canonical name map from fontawesome-free's
|
|
198
|
+
* metadata/icon-families.json ('search' → 'magnifying-glass').
|
|
199
|
+
*
|
|
200
|
+
* @param {object} iconFamilies - Parsed icon-families.json content.
|
|
201
|
+
* @returns {Map<string, string>} Alias slug → canonical slug.
|
|
202
|
+
*/
|
|
203
|
+
function buildAliasMap(iconFamilies) {
|
|
204
|
+
const map = new Map();
|
|
205
|
+
for (const [canonical, entry] of Object.entries(iconFamilies || {})) {
|
|
206
|
+
for (const alias of entry?.aliases?.names || []) {
|
|
207
|
+
map.set(alias, canonical);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return map;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
PACKAGES,
|
|
215
|
+
ICONS_DIR,
|
|
216
|
+
STYLES,
|
|
217
|
+
STYLE_REGEX,
|
|
218
|
+
NAME_REGEX,
|
|
219
|
+
SVG_ATTRIBUTES,
|
|
220
|
+
isValidIconName,
|
|
221
|
+
isValidStyle,
|
|
222
|
+
injectSvgAttributes,
|
|
223
|
+
candidateRelPaths,
|
|
224
|
+
parseIconClasses,
|
|
225
|
+
buildAliasMap,
|
|
226
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* icon-renderer — the ONE Font Awesome DOM auto-render (C4 cp112), shared
|
|
3
|
+
* by every browser surface: desktop renderers, web pages, and extension
|
|
4
|
+
* pages when one appears. Authoring is plain Font Awesome markup:
|
|
5
|
+
*
|
|
6
|
+
* <i class="fa-solid fa-rocket"></i> — static HTML
|
|
7
|
+
* <i class="omega-flag omega-flag-us"></i> — the flags namespace (#619)
|
|
8
|
+
* el.className = 'fa-sharp fa-light fa-play' — set or CHANGED via JS,
|
|
9
|
+
* any time; both render.
|
|
10
|
+
*
|
|
11
|
+
* The module owns everything except transport: scanning, the
|
|
12
|
+
* MutationObserver (insertions AND class changes), class parsing
|
|
13
|
+
* (icon-core's parseIconClasses), SVG root attributes, caching, and
|
|
14
|
+
* re-render/clear semantics. The caller injects ONE function — where SVG
|
|
15
|
+
* text comes from:
|
|
16
|
+
*
|
|
17
|
+
* desktop → (name, style) => ipc.invoke('desktop:fontawesome:get', …)
|
|
18
|
+
* web → (name, style) => fetch(`/assets/icons/${style}/${name}.svg`)
|
|
19
|
+
* extension→ (name, style) => fetch(chrome.runtime.getURL(…))
|
|
20
|
+
*
|
|
21
|
+
* Rendered elements carry data-omega-fa="<style>/<name>". Unknown icons
|
|
22
|
+
* leave the element empty (marked) — a missing icon is a content problem,
|
|
23
|
+
* never a crash, and never a wrong-style fallback.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const { parseIconClasses, isValidIconName, isValidStyle, injectSvgAttributes } = require('./icon-core.js');
|
|
27
|
+
|
|
28
|
+
// Both namespaces the class parser understands (#619): Font Awesome's fa-*
|
|
29
|
+
// and the country flags' omega-flag-*.
|
|
30
|
+
const ICON_SELECTOR = 'i[class*="fa-"], i[class*="omega-flag-"]';
|
|
31
|
+
const RENDERED_SELECTOR = `${ICON_SELECTOR}, i[data-omega-fa]`;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create an icon renderer bound to a transport.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} options
|
|
37
|
+
* @param {function(string, string): Promise<string|null>} options.resolve -
|
|
38
|
+
* Icon transport: (name, style) → raw SVG text or null.
|
|
39
|
+
* @returns {{ start: function, stop: function, scan: function }}
|
|
40
|
+
*/
|
|
41
|
+
function createIconRenderer(options) {
|
|
42
|
+
const cache = new Map(); // 'style/name' → Promise<string|null>
|
|
43
|
+
let observer = null;
|
|
44
|
+
let started = false;
|
|
45
|
+
|
|
46
|
+
const resolve = (name, style) => {
|
|
47
|
+
const key = `${style}/${name}`;
|
|
48
|
+
if (!cache.has(key)) {
|
|
49
|
+
cache.set(key, Promise.resolve()
|
|
50
|
+
.then(() => options.resolve(name, style))
|
|
51
|
+
.then((svg) => (svg ? injectSvgAttributes(svg) : null))
|
|
52
|
+
.catch(() => null));
|
|
53
|
+
}
|
|
54
|
+
return cache.get(key);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const render = (el) => {
|
|
58
|
+
const parsed = parseIconClasses(el.classList);
|
|
59
|
+
if (!parsed || !isValidIconName(parsed.name) || !isValidStyle(parsed.style)) {
|
|
60
|
+
// No (valid) icon classes left — clear a previously rendered icon.
|
|
61
|
+
if (el.dataset.omegaFa) {
|
|
62
|
+
delete el.dataset.omegaFa;
|
|
63
|
+
el.innerHTML = '';
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const key = `${parsed.style}/${parsed.name}`;
|
|
69
|
+
if (el.dataset.omegaFa === key) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
el.dataset.omegaFa = key;
|
|
74
|
+
el.innerHTML = '';
|
|
75
|
+
resolve(parsed.name, parsed.style).then((svg) => {
|
|
76
|
+
// Stale guard: classes may have changed again while resolving.
|
|
77
|
+
if (svg && el.isConnected && el.dataset.omegaFa === key && !el.querySelector('svg')) {
|
|
78
|
+
el.innerHTML = svg;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const scan = (root) => {
|
|
84
|
+
if (root.matches?.(ICON_SELECTOR)) {
|
|
85
|
+
render(root);
|
|
86
|
+
}
|
|
87
|
+
root.querySelectorAll?.(ICON_SELECTOR).forEach(render);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Scan the document and observe it for inserted icons AND class changes
|
|
92
|
+
* on existing ones. Idempotent.
|
|
93
|
+
*
|
|
94
|
+
* @param {Document} [doc] - Defaults to the global document.
|
|
95
|
+
*/
|
|
96
|
+
const start = (doc = typeof document !== 'undefined' ? document : null) => {
|
|
97
|
+
if (!doc || started) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
started = true;
|
|
101
|
+
|
|
102
|
+
const begin = () => {
|
|
103
|
+
observer = new MutationObserver((mutations) => {
|
|
104
|
+
for (const mutation of mutations) {
|
|
105
|
+
if (mutation.type === 'attributes') {
|
|
106
|
+
if (mutation.target.matches?.(RENDERED_SELECTOR)) {
|
|
107
|
+
render(mutation.target);
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
mutation.addedNodes.forEach((node) => {
|
|
112
|
+
if (node.nodeType === 1) {
|
|
113
|
+
scan(node);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
observer.observe(doc.documentElement, {
|
|
119
|
+
childList: true,
|
|
120
|
+
subtree: true,
|
|
121
|
+
attributes: true,
|
|
122
|
+
attributeFilter: ['class'],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
scan(doc.documentElement);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Deferred until the document exists — preload-time callers (the
|
|
129
|
+
// desktop test harness) run before documentElement is built.
|
|
130
|
+
if (doc.documentElement && doc.readyState !== 'loading') {
|
|
131
|
+
begin();
|
|
132
|
+
} else {
|
|
133
|
+
doc.addEventListener('DOMContentLoaded', begin, { once: true });
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const stop = () => {
|
|
138
|
+
if (observer) {
|
|
139
|
+
observer.disconnect();
|
|
140
|
+
observer = null;
|
|
141
|
+
}
|
|
142
|
+
started = false;
|
|
143
|
+
cache.clear();
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return { start, stop, scan };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = { createIconRenderer };
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* live-page — the primitives a page that REFRESHES ITSELF needs, ported from
|
|
3
|
+
* the workkit tower's page runtime.
|
|
4
|
+
*
|
|
5
|
+
* A live page is polled: it re-reads its feeds on a timer and re-renders with
|
|
6
|
+
* the answers. The naive shape of that — write the whole body on every tick —
|
|
7
|
+
* repaints identical markup ten times a minute, which blinks the cards, drops
|
|
8
|
+
* the scroll position inside a scrolling strip and closes whatever `details`
|
|
9
|
+
* was open. And before the first answer arrives the same render draws an empty
|
|
10
|
+
* region, so a slow feed looks like an empty one.
|
|
11
|
+
*
|
|
12
|
+
* Three primitives, one rule each:
|
|
13
|
+
* - `swap(host, markup)` writes ONLY when the markup differs from what was
|
|
14
|
+
* written last, so an unchanged section is left alone — DOM, focus, scroll
|
|
15
|
+
* and all.
|
|
16
|
+
* - `loading(message)` is what a section shows while its feed has never
|
|
17
|
+
* answered: a first paint says which read it is waiting on, never a blank.
|
|
18
|
+
* - `createFeedPoller(options)` owns the declared feed table, the in-flight
|
|
19
|
+
* count and the keep-last-good rule — a refresh that fails leaves the last
|
|
20
|
+
* good answer on screen, marked stale with the reason.
|
|
21
|
+
*
|
|
22
|
+
* Transport-free, like `icon-renderer` and `motion`: nothing here fetches. The
|
|
23
|
+
* poller takes its fetcher as an ARGUMENT — an `omega.request`-shaped function
|
|
24
|
+
* (resolves with the body, throws an Error carrying `.code`) — so the embedding
|
|
25
|
+
* page passes `omega.request` and a non-singleton context (desktop main, the
|
|
26
|
+
* extension service worker) passes its own `createRequest(...)` instance, the
|
|
27
|
+
* same seam `request.js` already offers.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import Utilities from './utilities.js';
|
|
31
|
+
|
|
32
|
+
// Escaping is utilities' job, never this module's. The helpers are pure — they
|
|
33
|
+
// touch no manager state — so a manager-free instance is the whole dependency.
|
|
34
|
+
const { escapeHTML } = new Utilities();
|
|
35
|
+
|
|
36
|
+
/** What swap last wrote into each host, keyed by the element itself. */
|
|
37
|
+
const written = new WeakMap();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The markup a section shows while its feed has not answered yet.
|
|
41
|
+
*
|
|
42
|
+
* The theme's spinner, in the muted voice a "nothing here" line uses.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} message - what is being read, in the page's own words
|
|
45
|
+
* @returns {string} markup
|
|
46
|
+
*/
|
|
47
|
+
export function loading(message) {
|
|
48
|
+
return `<div class="d-flex align-items-center gap-2 text-body-secondary">
|
|
49
|
+
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
50
|
+
<span class="classy-micro" aria-live="polite">${escapeHTML(message)}</span>
|
|
51
|
+
</div>`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Write markup into a host, but only when it is not already there.
|
|
56
|
+
*
|
|
57
|
+
* The comparison is against what swap itself last wrote, held in a WeakMap, and
|
|
58
|
+
* NOT against `host.innerHTML` — the browser re-serializes what it parses
|
|
59
|
+
* (attribute order, entities, void tags), so reading it back never matches the
|
|
60
|
+
* string that produced it and every tick would count as a change.
|
|
61
|
+
*
|
|
62
|
+
* @param {{innerHTML: string}} host - the element to draw into
|
|
63
|
+
* @param {string} markup - what the render produced this time
|
|
64
|
+
* @returns {boolean} true when the DOM was written, false when it was left
|
|
65
|
+
* alone — the caller's post-draw work (charts, listeners) hangs off this
|
|
66
|
+
*/
|
|
67
|
+
export function swap(host, markup) {
|
|
68
|
+
if (written.get(host) === markup) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
written.set(host, markup);
|
|
73
|
+
host.innerHTML = markup;
|
|
74
|
+
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Boot a feed poller over a declared feed table.
|
|
80
|
+
*
|
|
81
|
+
* The cadence is the poller's own: it stops while the page is hidden (a covered
|
|
82
|
+
* tab) and resumes on the way back with one immediate read. There is no option
|
|
83
|
+
* for it.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} options
|
|
86
|
+
* @param {Object<string, {path: string, every: number, fresh?: string}>} options.feeds -
|
|
87
|
+
* every feed this page reads: where it lives, how often it is re-read, and
|
|
88
|
+
* the optional path a user-triggered refresh uses instead (a cache bypass)
|
|
89
|
+
* @param {(path: string) => Promise<any>} options.fetcher - an
|
|
90
|
+
* `omega.request`-shaped function: resolves with the body, throws on failure
|
|
91
|
+
* @param {() => void} [options.onChange] - called at every state transition (a
|
|
92
|
+
* read starting, a read landing), which is when the page repaints
|
|
93
|
+
* @returns {{state: object, read: Function, readAll: Function, staleFeeds: Function, start: Function, stop: Function}}
|
|
94
|
+
*/
|
|
95
|
+
export function createFeedPoller(options) {
|
|
96
|
+
if (typeof options.fetcher !== 'function') {
|
|
97
|
+
throw new Error('createFeedPoller requires a fetcher');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const feeds = options.feeds || {};
|
|
101
|
+
const names = Object.keys(feeds);
|
|
102
|
+
const onChange = options.onChange || (() => {});
|
|
103
|
+
|
|
104
|
+
// `pending` is how many reads are in flight, which a page's chrome draws its
|
|
105
|
+
// spinner from: a refresh is visible while it happens, and the page under it
|
|
106
|
+
// keeps showing the data it already has.
|
|
107
|
+
const state = { feeds: {}, pending: 0, stamp: '' };
|
|
108
|
+
|
|
109
|
+
let timers = [];
|
|
110
|
+
let started = false;
|
|
111
|
+
|
|
112
|
+
// The hidden-tab rule needs a document to read: a poller running where there
|
|
113
|
+
// is none (the extension service worker the fetcher seam exists for) has no
|
|
114
|
+
// tab that can be covered, and just keeps its cadence.
|
|
115
|
+
const page = typeof document === 'undefined' ? null : document;
|
|
116
|
+
|
|
117
|
+
// The fetcher throws; a feed result never does. Every way a read can let a
|
|
118
|
+
// page down lands in the same four-key shape, told apart by status and reason.
|
|
119
|
+
const ask = async (path) => {
|
|
120
|
+
try {
|
|
121
|
+
return { ok: true, data: await options.fetcher(path), status: null, reason: null };
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return { ok: false, data: null, status: error?.code ?? null, reason: error?.message || 'the read failed' };
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const read = async (name, fresh) => {
|
|
128
|
+
const spec = feeds[name];
|
|
129
|
+
if (!spec) {
|
|
130
|
+
throw new Error(`createFeedPoller: no feed named "${name}"`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
state.pending += 1;
|
|
134
|
+
onChange();
|
|
135
|
+
|
|
136
|
+
const answer = await ask(fresh && spec.fresh ? spec.fresh : spec.path);
|
|
137
|
+
const previous = state.feeds[name];
|
|
138
|
+
state.pending -= 1;
|
|
139
|
+
|
|
140
|
+
// A refresh that fails does not take the page down with it. The last good
|
|
141
|
+
// answer stays on screen, marked stale so the chrome can say a feed is
|
|
142
|
+
// unavailable — replacing a full board with an error line because one poll
|
|
143
|
+
// missed is the "clearing to empty" this module exists to prevent.
|
|
144
|
+
state.feeds[name] = !answer.ok && previous && previous.ok
|
|
145
|
+
? { ...previous, stale: answer.reason }
|
|
146
|
+
: answer;
|
|
147
|
+
state.stamp = `updated ${new Date().toLocaleTimeString()}`;
|
|
148
|
+
onChange();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const readAll = (fresh) => Promise.all(names.map((name) => read(name, fresh)));
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Every feed that is not currently telling the truth — one that failed
|
|
155
|
+
* outright, and one showing a last-good answer behind a failed refresh.
|
|
156
|
+
*
|
|
157
|
+
* @returns {Array<{name: string, reason: string}>}
|
|
158
|
+
*/
|
|
159
|
+
const staleFeeds = () => Object.entries(state.feeds)
|
|
160
|
+
.filter(([, result]) => result && (!result.ok || result.stale))
|
|
161
|
+
.map(([name, result]) => ({ name, reason: result.stale || result.reason }));
|
|
162
|
+
|
|
163
|
+
const arm = () => {
|
|
164
|
+
timers = names.map((name) => setInterval(() => { read(name, false); }, feeds[name].every));
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const disarm = () => {
|
|
168
|
+
timers.forEach((timer) => clearInterval(timer));
|
|
169
|
+
timers = [];
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// A covered TAB reads nothing. Polling a page nobody is looking at spends the
|
|
173
|
+
// device's battery and the backend's quota on answers that are thrown away,
|
|
174
|
+
// so the intervals are cleared while the page is hidden and re-armed when it
|
|
175
|
+
// comes back — with ONE immediate read, because whatever is on screen is as
|
|
176
|
+
// old as the time the tab spent covered.
|
|
177
|
+
//
|
|
178
|
+
// `document.hidden` on purpose, and never focus: a visible-but-unfocused
|
|
179
|
+
// window is still being WATCHED — a board on a second monitor is the case
|
|
180
|
+
// this module exists for — and keeps its cadence.
|
|
181
|
+
const onVisibilityChange = () => {
|
|
182
|
+
if (!started) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (page.hidden) {
|
|
187
|
+
disarm();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Armed already means this was not a return from hidden (a browser can say
|
|
192
|
+
// "visible" to a page that never left), and there is nothing to catch up on.
|
|
193
|
+
if (timers.length) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
readAll(false);
|
|
198
|
+
arm();
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const start = async () => {
|
|
202
|
+
if (started) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
started = true;
|
|
206
|
+
|
|
207
|
+
await readAll(false);
|
|
208
|
+
|
|
209
|
+
// Armed AFTER the first pass so a slow first read is never overlapped by
|
|
210
|
+
// its own timer. A stop() during that first pass flips `started` back off
|
|
211
|
+
// before this line runs — arming anyway would leave intervals nothing can
|
|
212
|
+
// ever clear, so the re-check is what makes stop() final.
|
|
213
|
+
if (!started) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
page?.addEventListener('visibilitychange', onVisibilityChange);
|
|
218
|
+
|
|
219
|
+
// A start in a tab that is ALREADY covered arms nothing: the listener above
|
|
220
|
+
// is what gives the page its cadence when it comes into view.
|
|
221
|
+
if (page?.hidden) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
arm();
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const stop = () => {
|
|
229
|
+
page?.removeEventListener('visibilitychange', onVisibilityChange);
|
|
230
|
+
disarm();
|
|
231
|
+
started = false;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return { state, read, readAll, staleFeeds, start, stop };
|
|
235
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client's runtime log tag — the ONE identity tag every OMEGA surface
|
|
3
|
+
* prints: `[@omega.js/client:<module>]` ([#12](https://github.com/Omega-JS-Stack/omega/issues/12)).
|
|
4
|
+
*
|
|
5
|
+
* No timestamp on purpose: devtools already stamps runtime lines. The
|
|
6
|
+
* build-time twin (`@omega.js/devkit/logger`) is the one that prefixes
|
|
7
|
+
* `[HH:MM:SS]` before the same tag.
|
|
8
|
+
*
|
|
9
|
+
* Every client module logs through this factory — no module hand-writes a tag,
|
|
10
|
+
* so the shape can never drift again.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// The package segment — this file IS @omega.js/client, so it is a literal.
|
|
14
|
+
const PACKAGE = '@omega.js/client';
|
|
15
|
+
|
|
16
|
+
// Create a tagged console for one module, e.g. createLogger('push').
|
|
17
|
+
export function createLogger(module) {
|
|
18
|
+
const tag = `[${PACKAGE}:${module}]`;
|
|
19
|
+
|
|
20
|
+
// GETTERS returning a BOUND console method, not wrapper arrows: devtools
|
|
21
|
+
// attributes a line to the frame that called console, so a wrapper would make
|
|
22
|
+
// every line in the app read as coming from this file. Binding hands the real
|
|
23
|
+
// call site back. Resolution stays at ACCESS time, so a test (or a consumer)
|
|
24
|
+
// that swaps console[method] still sees its own stub.
|
|
25
|
+
const logger = { tag };
|
|
26
|
+
for (const method of ['log', 'info', 'warn', 'error', 'debug']) {
|
|
27
|
+
Object.defineProperty(logger, method, {
|
|
28
|
+
get: () => console[method].bind(console, tag),
|
|
29
|
+
enumerable: true,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return logger;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default createLogger;
|