Humuhumu33 commited on
Commit
1f363bf
·
verified ·
1 Parent(s): b91ba1b

κ-store: 12 shell blobs (aggregate 5061bc7afbef)

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ b/f6caa9eeead2c99deeb2cdb70b7e25bbaa2cf5c60555f5b4030ecf6b6d9efa51 filter=lfs diff=lfs merge=lfs -text
b/040b54338f22e6abbc19071ec1b97487717982c8ddc7fa3500d7c46269e93344 ADDED
The diff for this file is too large to render. See raw diff
 
b/076005621416e21a26486060b3f618d08f594fb0fea18a84375e68ba4256bebc ADDED
The diff for this file is too large to render. See raw diff
 
b/11d9ba0d372a50f7402c01cf9b2934fdb0b34a33425d308ec130e8e277de5260 ADDED
The diff for this file is too large to render. See raw diff
 
b/1f7746684dce60648c3d5ffbd4d289f862222ab4eb82c14c5f07239d1b47f5c6 ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // messenger-skin.mjs — bakes the SLACK re-tone as the messenger default (aubergine chrome + a floating,
2
+ // inset content pane; see the data-skin="slack" block in messenger-skins.css) and applies the hi-DPI /
3
+ // high-FPS perf layer. No picker / no toggle. Additive + guarded. Escape hatch: set localStorage
4
+ // "holo.skin" to graphite | warm | contrast | light (or "" to remove) to override the default.
5
+
6
+ (function () {
7
+ if (!document.querySelector("link[data-holo-skins]")) {
8
+ const l = document.createElement("link"); l.rel = "stylesheet"; l.href = "/apps/holo-messenger/messenger-skins.css"; l.setAttribute("data-holo-skins", "1"); document.head.appendChild(l);
9
+ }
10
+ let skin = "slack";
11
+ try { const s = localStorage.getItem("holo.skin"); if (s) skin = s; } catch {}
12
+ if (skin) document.documentElement.setAttribute("data-skin", skin); else document.documentElement.removeAttribute("data-skin");
13
+ // remove any lingering picker + its persisted flag from earlier ?skins=1 sessions
14
+ try { localStorage.removeItem("holo.skins"); } catch {}
15
+ try { const p = document.querySelector(".holo-skin-pick"); if (p) p.remove(); } catch {}
16
+ })();
b/38689d2013d1f64086bf523e77d557616cd702511c322e60ef15a5ebb325b6ad ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // holo-m1-boot.mjs — M1: INSTANT SERVERLESS BOOT. One self-verifying link boots the WHOLE messenger.
2
+ //
3
+ // A single web link that, opened in a COLD browser, brings up the entire Holo Messenger — shell + your
4
+ // identity + Q — with ZERO origin servers in the delivery path (every shell byte is a content-addressed κ
5
+ // object, served from cache/OPFS on repeat opens). The descriptor travels INSIDE the link (b64url JSON in
6
+ // the URL fragment), exactly like holo-chat-link.mjs / holo-together.mjs: a bare browser verifies it LOCALLY
7
+ // (Law L5: re-derive the κ), then boots. The κ is SHA-256 of the canonical PUBLIC fields — the shareable,
8
+ // tamper-evident id. Any private pairing bearer is EXCLUDED from the κ (whoever holds the link may adopt it).
9
+ //
10
+ // This commits to the SHELL MANIFEST (the exact asset set the service worker precaches), so a tampered link
11
+ // that points the boot at a different/injected asset set breaks integrity and is refused. Laws: L2 canonical,
12
+ // L5 verify-by-re-derivation. Additive + fail-soft: if anything here throws, app.html's normal boot proceeds.
13
+
14
+ export const BOOT_LINK_VERSION = 1;
15
+ const ENTRY_PATH = "/apps/holo-messenger/app.html";
16
+
17
+ // THE SHELL — the minimal asset set required to paint the messenger and reach interactive, and the single
18
+ // source of truth the service worker precaches (messenger-sw.js mirrors these basenames; the M1 witness
19
+ // asserts the two never drift). Q's brain, the bridges, and all media are DELIBERATELY absent: they are
20
+ // deferred (never on the first-paint critical path), so the shell stays tiny and cache-resident.
21
+ export const SHELL_MANIFEST = [
22
+ "/apps/holo-messenger/app.html",
23
+ "/apps/holo-messenger/_vendor/ui/chat-ui.bundle.js",
24
+ "/apps/holo-messenger/_vendor/ui/chat-ui.bundle.css",
25
+ "/apps/holo-messenger/holo-messenger-app.mjs",
26
+ "/apps/holo-messenger/messenger-shadcn-ui.mjs",
27
+ "/apps/holo-messenger/holo-messenger-weave.mjs",
28
+ "/apps/holo-messenger/messenger-skin.mjs",
29
+ "/apps/holo-messenger/messenger-skins.css",
30
+ "/apps/holo-messenger/messenger-sw-register.mjs",
31
+ "/apps/holo-messenger/holo-m1-boot.mjs",
32
+ "/apps/holo-messenger/mail/holo-mail-attach.mjs",
33
+ "/apps/holo-messenger/_vendor/wallpaper-default.jpg",
34
+ ];
35
+
36
+ const _te = new TextEncoder();
37
+ async function _sha256hex(s) { const h = await (globalThis.crypto || crypto).subtle.digest("SHA-256", _te.encode(s)); return [...new Uint8Array(h)].map((x) => x.toString(16).padStart(2, "0")).join(""); }
38
+ function _b64urlEncode(obj) { const b = btoa(unescape(encodeURIComponent(JSON.stringify(obj)))); return b.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); }
39
+ function _b64urlDecode(str) { const b = String(str).replace(/-/g, "+").replace(/_/g, "/"); return JSON.parse(decodeURIComponent(escape(atob(b)))); }
40
+
41
+ // The shell manifest (shell-manifest.json, minted by _build/gen-shell-manifest.mjs) is the content hash of
42
+ // every shell BYTE: [{ path, kappa=SHA-256(bytes) }]. In a browser we fetch it; in Node the caller passes it.
43
+ export async function loadShellManifest() {
44
+ try {
45
+ if (typeof fetch === "function" && typeof location !== "undefined") {
46
+ const r = await fetch("/apps/holo-messenger/shell-manifest.json", { cache: "no-store" });
47
+ if (r && r.ok) return await r.json();
48
+ }
49
+ } catch {}
50
+ return null;
51
+ }
52
+
53
+ // The ONE hash the boot link commits: SHA-256 over the SORTED per-asset content κs. Any browser re-derives it
54
+ // from shell-manifest.json → thereby verifies EVERY shell byte (Law L5). Recomputed from `assets`, never trusting
55
+ // the manifest's self-declared `aggregate`, so a forged manifest fails. Empty (unverifiable) → no commitment.
56
+ export async function shellAggregate(manifest) {
57
+ const m = manifest || (await loadShellManifest());
58
+ if (!m || !Array.isArray(m.assets) || !m.assets.length) return "";
59
+ return _sha256hex(m.assets.map((a) => String(a.kappa)).sort().join(","));
60
+ }
61
+
62
+ // canonical PUBLIC form — any private `pair` bearer is EXCLUDED so the κ is the shareable id. Commits to the
63
+ // entry, the shell set, the display identity (so a tampered "this is <who>'s messenger" card breaks — anti-phish).
64
+ function _canon(d) {
65
+ return [
66
+ "v" + (d.v | 0), d.kind || "boot", d.app || "holo-messenger", d.entry || ENTRY_PATH,
67
+ d.shell || "", d.name || "", d.truename || "", d.op || "",
68
+ String(d.created || 0), String(d.expires || 0),
69
+ ].join("␟");
70
+ }
71
+
72
+ // Build the one-link door for the whole messenger. `op`/`name`/`truename` are PUBLIC display identity (the
73
+ // landing shows "resume <name>'s messenger"); `pair` is an optional bearer pairing payload (device-roam) and
74
+ // is NOT part of the κ. Opening the link still auto-enrolls a sovereign identity unless a pair bearer is honored.
75
+ export async function makeBootLink({ op = "", name = "", truename = "", pair = null, ttlSeconds = 30 * 24 * 3600, origin = null, nowMs = null, aggregate = null, manifest = null } = {}) {
76
+ const now = nowMs || Date.now();
77
+ const shell = aggregate || (await shellAggregate(manifest)); // commit the shell BYTES (aggregate content κ)
78
+ const d = {
79
+ v: BOOT_LINK_VERSION, kind: "boot", app: "holo-messenger", entry: ENTRY_PATH,
80
+ shell,
81
+ op: String(op || "").slice(0, 96), name: String(name || "").slice(0, 60), truename: String(truename || "").slice(0, 60),
82
+ created: now, expires: now + ttlSeconds * 1000,
83
+ };
84
+ if (pair) d.pair = pair; // bearer pairing payload — NOT part of the κ
85
+ d.kappa = await _sha256hex(_canon(d));
86
+ const payload = _b64urlEncode(d);
87
+ const org = origin || (typeof location !== "undefined" ? location.origin : "");
88
+ return { kappa: d.kappa, payload, descriptor: d,
89
+ https: `${org}${ENTRY_PATH}#m1=${payload}`, // opens in ANY cold browser
90
+ holo: `holo://os${ENTRY_PATH}#m1=${payload}` }; // in-shell full surface
91
+ }
92
+
93
+ // parse a boot link (URL, holo:// form, `#m1=<payload>`, or bare payload) → { ok, descriptor, integrity, expired }.
94
+ export async function parseBootLink(input, { nowMs = null, aggregate = null, manifest = null } = {}) {
95
+ let payload = String(input || "");
96
+ if (payload.includes("#")) payload = payload.split("#").pop();
97
+ payload = payload.replace(/^m1=/, "");
98
+ let d; try { d = _b64urlDecode(payload); } catch { return { ok: false, error: "unreadable boot link" }; }
99
+ if (!d || d.v !== BOOT_LINK_VERSION || d.kind !== "boot" || d.app !== "holo-messenger" || !d.kappa) return { ok: false, error: "not a valid messenger boot link" };
100
+ const integrity = (await _sha256hex(_canon(d))) === d.kappa; // Law L5: re-derive; tamper → false
101
+ const current = aggregate != null ? aggregate : (await shellAggregate(manifest));// THIS build's shell-byte aggregate
102
+ const shellMatches = !!current && d.shell === current; // link's shell BYTES must match (unverifiable → false)
103
+ const expired = !!(d.expires && (nowMs || Date.now()) > d.expires);
104
+ return { ok: true, descriptor: d, integrity, shellMatches, expired, aggregate: current };
105
+ }
106
+
107
+ // convenience: verify + return the boot descriptor only if it is safe to act on (integrity + shell + fresh).
108
+ export async function resolveBootLink(input, opts = {}) {
109
+ const p = await parseBootLink(input, opts);
110
+ if (!p.ok) return { ok: false, reason: p.error };
111
+ if (!p.integrity) return { ok: false, reason: "tampered" };
112
+ if (!p.shellMatches) return { ok: false, reason: "shell-mismatch" };
113
+ if (p.expired) return { ok: false, reason: "expired" };
114
+ return { ok: true, descriptor: p.descriptor, pair: p.descriptor.pair || null };
115
+ }
116
+
117
+ export function describe(d) {
118
+ const who = (d && (d.name || d.truename)) || "someone";
119
+ return { headline: `Resume ${who}'s messenger`, cta: "Open" };
120
+ }
121
+
122
+ export function installM1Boot() {
123
+ if (typeof window === "undefined") return false;
124
+ window.HoloM1Boot = Object.assign(window.HoloM1Boot || {}, { version: BOOT_LINK_VERSION, SHELL_MANIFEST, makeBootLink, parseBootLink, resolveBootLink, describe, loadShellManifest, shellAggregate });
125
+ return true;
126
+ }
127
+ try { installM1Boot(); } catch {}
b/3946cf9e0a3bfb709b722ab949131b69f5670cbb75221f1c28701877ecdd4ec6 ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en"><head>
3
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
4
+ <title>Holo Messenger</title>
5
+ <link rel="stylesheet" href="/apps/holo-messenger/_vendor/ui/chat-ui.bundle.css?v=ec9aea0c39ca">
6
+ <style>html,body{height:100%;margin:0;background:#0b141a;color-scheme:dark}#root{height:100vh}</style>
7
+ <link rel="preload" as="image" href="/apps/holo-messenger/_vendor/wallpaper-default.jpg" fetchpriority="high">
8
+ </head><body>
9
+ <div id="root"></div>
10
+ <script type="module">
11
+ // ?v=N - bump on each deploy so the browser can never serve a stale cached bundle (the cause of a frozen/old build).
12
+ import "/apps/holo-messenger/_vendor/ui/chat-ui.bundle.js?v=c741295f6742"; // → window.HoloMessengerUI
13
+ // Opt-in: render the WhatsApp UX from STREAMING shadcn κ-components instead of the chatscope bundle.
14
+ // Additive — only takes over window.HoloMessengerUI.mount when ?ui=shadcn (or localStorage["holo.ui.shadcn"]="1");
15
+ // the default boot stays the untouched chatscope UI. Runs before boot() so it is fed the same live model.
16
+ import "/apps/holo-messenger/messenger-shadcn-ui.mjs";
17
+ import { boot } from "/apps/holo-messenger/holo-messenger-app.mjs?v=040b54338f22";
18
+ boot(document.getElementById("root")).catch(e => {
19
+ document.body.innerHTML = "<pre style='color:#f88;padding:16px;white-space:pre-wrap'>boot error: " + (e && e.stack || e) + "</pre>";
20
+ });
21
+ // Intelligent Inbox (holo-mail) - additive, feature-flagged OFF by default. Dynamic import + catch so it
22
+ // can NEVER block or break the messenger boot. Enable per-operator: localStorage["holo.mail.enabled"]="1".
23
+ import("/apps/holo-messenger/mail/holo-mail-attach.mjs").catch(() => {});
24
+ // HoloChat, woven into THIS messenger — one canonical surface, no separate overlay. Cards render natively
25
+ // in the bubbles; the composer "+" opens the unified tray (apps · Spaces · money · identity · prove).
26
+ // Additive + catch-guarded so it can never block boot. Enable: localStorage["holo.chat.enabled"]="1" or ?chat=1.
27
+ import("/apps/holo-messenger/holo-messenger-weave.mjs").catch(() => {});
28
+ // Colour re-tone picker (Default · Graphite · Warm · Contrast · Light) — colours only, all features
29
+ // intact. Gated: shows only with ?skins=1 or localStorage["holo.skins"]="1". Additive + catch-guarded.
30
+ import("/apps/holo-messenger/messenger-skin.mjs").catch(() => {});
31
+ // Asset cache (service worker) — repeat opens serve wallpaper/logos/bundle/css/js NETWORK-FREE. Caches
32
+ // only own-origin static assets (stale-while-revalidate); all dynamic/API/bridge/media traffic passes
33
+ // through untouched. Additive + catch-guarded. Escape hatch: open with ?nosw=1 to unregister.
34
+ import("/apps/holo-messenger/messenger-sw-register.mjs").catch(() => {});
35
+ </script>
36
+ </body></html>
b/4e8a770de63abc7d655780dad449eda292e54e62f96b7252ae978bbca40f1586 ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* messenger-skins.css — OPTIONAL colour re-tones, applied ONLY when <html> carries data-skin="…".
2
+ * Default (no attribute) is untouched, so this can never disturb the shipping look. Colours only — no
3
+ * layout — safe and fully reversible.
4
+ *
5
+ * The theme drives ~half its colour from --wa-* vars on .holo-wa-root and hardcodes the other half to the
6
+ * same values. This shared block re-points the major hardcoded surfaces back onto the vars; each skin then
7
+ * only remaps the vars and the whole surface re-tones consistently. */
8
+
9
+ html[data-skin] .holo-wa-root { background: var(--wa-bg) !important; color: var(--wa-ink); }
10
+ html[data-skin] .holo-wa-root .cs-main-container,
11
+ html[data-skin] .holo-wa-root .cs-chat-container,
12
+ html[data-skin] .holo-wa-root .cs-message-list { background: transparent; }
13
+ html[data-skin] .holo-wa-root .cs-sidebar,
14
+ html[data-skin] .holo-wa-root .holo-rail,
15
+ html[data-skin] .holo-wa-root .cs-conversation-list { background: var(--wa-side) !important; }
16
+ html[data-skin] .holo-wa-root .cs-conversation-header,
17
+ html[data-skin] .holo-wa-root .holo-list-title,
18
+ html[data-skin] .holo-wa-root .cs-message-input,
19
+ html[data-skin] .holo-wa-root .holo-composer { background: var(--wa-head) !important; }
20
+ html[data-skin] .holo-wa-root .cs-search,
21
+ html[data-skin] .holo-wa-root .holo-wp-search,
22
+ html[data-skin] .holo-wa-root .cs-message-input__content-editor-wrapper,
23
+ html[data-skin] .holo-wa-root .cs-message-input__content-editor { background: color-mix(in srgb, var(--wa-head) 78%, var(--wa-bg)) !important; color: var(--wa-ink) !important; }
24
+ html[data-skin] .holo-wa-root .cs-conversation:hover,
25
+ html[data-skin] .holo-wa-root .cs-conversation.cs-conversation--active { background: var(--wa-head) !important; }
26
+ html[data-skin] .holo-wa-root .cs-conversation__name,
27
+ html[data-skin] .holo-wa-root .cs-conversation-header__user-name { color: var(--wa-ink) !important; }
28
+ html[data-skin] .holo-wa-root .cs-conversation__info,
29
+ html[data-skin] .holo-wa-root .cs-conversation__last-sender,
30
+ html[data-skin] .holo-wa-root .cs-conversation-header__info { color: var(--wa-dim) !important; }
31
+ html[data-skin] .holo-wa-root .cs-message--incoming .cs-message__content { background: var(--wa-in) !important; color: var(--wa-ink) !important; }
32
+ html[data-skin] .holo-wa-root .cs-message--outgoing .cs-message__content { background: var(--wa-out) !important; }
33
+ html[data-skin] .holo-wa-root .cs-message__content { color: var(--wa-ink); }
34
+ html[data-skin] .holo-wa-root .holo-rail-btn.on,
35
+ html[data-skin] .holo-wa-root .holo-rail-net.on,
36
+ html[data-skin] .holo-wa-root .holo-catchup-pill { color: var(--wa-accent) !important; }
37
+ html[data-skin] .holo-wa-root .holo-c-send,
38
+ html[data-skin] .holo-wa-root .cs-button--send { background: var(--wa-accent) !important; }
39
+
40
+ /* ── cooler / graphite ── */
41
+ html[data-skin="graphite"] .holo-wa-root { --wa-bg:#0d1117; --wa-side:#12171e; --wa-head:#1c232c; --wa-in:#1c232c; --wa-out:#1f4a55; --wa-ink:#e6eaf0; --wa-dim:#8b96a3; --wa-accent:#4ab3d4; --wa-line:#222a34; }
42
+ /* ── warmer ── */
43
+ html[data-skin="warm"] .holo-wa-root { --wa-bg:#14100c; --wa-side:#1b1611; --wa-head:#271f18; --wa-in:#271f18; --wa-out:#4d3f1f; --wa-ink:#efe8df; --wa-dim:#a89a88; --wa-accent:#e0a53c; --wa-line:#2c241b; }
44
+ /* ── higher contrast ── */
45
+ html[data-skin="contrast"] .holo-wa-root { --wa-bg:#000000; --wa-side:#0a0a0a; --wa-head:#181818; --wa-in:#1d1d1d; --wa-out:#00805e; --wa-ink:#ffffff; --wa-dim:#a6adb4; --wa-accent:#00e08a; --wa-line:#2a2a2a; }
46
+ /* ── light mode (experimental: some deep-hardcoded text may stay dark) ── */
47
+ html[data-skin="light"] .holo-wa-root { --wa-bg:#d7dce1; --wa-side:#ffffff; --wa-head:#f0f2f5; --wa-in:#ffffff; --wa-out:#d9fdd3; --wa-ink:#111b21; --wa-dim:#54656f; --wa-accent:#008069; --wa-line:#e4e8ec; }
48
+ html[data-skin="light"] .holo-wa-root .cs-message__content { color: #111b21 !important; }
49
+ html[data-skin="light"] .holo-wa-root .cs-message-list { background: var(--wa-bg) !important; background-image: none !important; }
50
+ html[data-skin="light"] .holo-wa-root .holo-wp-tile { background-image: none !important; }
51
+
52
+ /* ────────────────────────────────────────────────────────────────────────────────────────────────
53
+ * HI-DPI + HIGH-FPS layer (always on, colour-independent). The honest "8K/retina" story: a DOM/CSS UI
54
+ * is ALREADY resolution-independent — text and the SVG icons are vectors, so on a retina/4K/8K panel it
55
+ * renders at the display's native device pixels with zero extra work (no bitmaps to upscale). What we
56
+ * actually tune here is smoothness + latency:
57
+ * • content-visibility skips rendering/layout of off-screen chat rows → a huge list scrolls at the
58
+ * display's full refresh (60/120Hz) and first paint is near-instant (browser does O(visible) work).
59
+ * • the scrollers are promoted to their own GPU layer so scrolling never touches the main thread.
60
+ * • crisp text rasterisation + high-quality image scaling for hi-DPI. */
61
+ .holo-wa-root { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; }
62
+ .holo-wa-root .cs-conversation { content-visibility: auto; contain-intrinsic-size: auto 72px; }
63
+ .holo-wa-root .cs-message { content-visibility: auto; contain-intrinsic-size: auto 44px; }
64
+ .holo-wa-root .cs-conversation-list,
65
+ .holo-wa-root .cs-message-list .scrollbar-container,
66
+ .holo-wa-root .cs-message-list__scroll-wrapper { will-change: scroll-position; transform: translateZ(0); backface-visibility: hidden; overflow-anchor: auto; }
67
+ .holo-wa-root .cs-avatar img, .holo-wa-root .holo-av-real, .holo-wa-root .holo-media-el img { image-rendering: auto; }
68
+ .holo-wa-root .holo-media-el img, .holo-wa-root .cs-message img { image-rendering: -webkit-optimize-contrast; }
69
+ @media (min-resolution: 1.5dppx) { .holo-wa-root { text-rendering: geometricPrecision; } }
70
+
71
+ /* floating skin picker */
72
+ .holo-skin-pick { position: fixed; right: 16px; bottom: 86px; z-index: 90; display: flex; gap: 4px; align-items: center; background: #0b141aee; border: 1px solid #2a3942; border-radius: 999px; padding: 5px; backdrop-filter: blur(10px); font: 12.5px ui-sans-serif, system-ui, sans-serif; box-shadow: 0 8px 30px #0007; }
73
+ .holo-skin-pick button { background: transparent; border: 0; color: #aebac1; border-radius: 999px; padding: 5px 11px; cursor: pointer; }
74
+ .holo-skin-pick button:hover { background: #ffffff14; color: #e9edef; }
75
+ .holo-skin-pick button.on { background: #00a884; color: #0b141a; font-weight: 600; }
76
+
77
+ /* instant wallpaper */
78
+ /* An inline low-res earth paints INSTANTLY (zero network — it ships in this CSS) as the chat-pane
79
+ * backdrop; the full 2560px wallpaper is preloaded (see app.html) and covers it the moment it decodes.
80
+ * So the pane is never empty and there is no visible load. */
81
+ html[data-skin] .holo-wa-root .cs-chat-container { background: var(--wa-bg) url("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDABQODxIPDRQSERIXFhQYHzMhHxwcHz8tLyUzSkFOTUlBSEZSXHZkUldvWEZIZoxob3p9hIWET2ORm4+AmnaBhH//2wBDARYXFx8bHzwhITx/VEhUf39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f3//wAARCAAgADADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwCAR+1SLHVaPUYm2Zyp/jyOB9KE1AbzgjPXaRgAf41pdisjQSL2/SphDx0/Sq8epW64zknuBV6O+s3XKzJ9CcGk5tFciZGIT6fpT1h+YZx+VTC5tuomT880yTUdNgI8ycF89EXJqPavoP2aW5ya28f8bMT9cVIsVqDyCfq1VPtPIOwgfWpDcKFyvzH8q1ujKzL8bWynAiT8RmpGmTsiAfSs0XR/5aKOem3mnrdkcBGz+FF0Go6aW6ZiEwF/6ZjH61XNvdH/AJZnn3qc3rFdrQMwqP7bcKP3cSj2C0tB3Z//2Q==") center center / cover no-repeat !important; }
82
+ html[data-skin] .holo-wa-root .cs-message-list { transition: background-image .25s ease; }
83
+
84
+ /* ────────────────────────────────────────────────────────────────────────────────────────────────
85
+ * SLACK skin — data-skin="slack". Unlike the colour-only re-tones above, this one also does LAYOUT: the
86
+ * Slack "window envelope". An aubergine chrome (far-left rail + channel sidebar) wraps a content pane that
87
+ * floats as a rounded island inset by an EVEN margin on ALL FOUR sides — the darker chrome shows through
88
+ * every gap (top, right, bottom, AND a left gutter between sidebar and pane), with rounded corners + a soft
89
+ * shadow for depth. Placed at end-of-file so source order beats the base skin block AND the wallpaper rule
90
+ * above (equal specificity → later wins); every rule carries `.holo-wa-root` to match the base depth.
91
+ *
92
+ * Two coupling facts make the float robust: (1) the composer is a position:absolute overlay on .holo-main
93
+ * whose `left` is CSS `34%` — the SAME 34% as the sidebar's flex-basis — so re-anchoring it to
94
+ * `calc(34% + gap)` keeps it glued to the gapped pane at every window width, no JS. (2) the bundle gives the
95
+ * pane height:100%, which ignores margin and overflows; `height:auto` + flex:1 1 auto lets the even margin
96
+ * actually shrink it. ALL geometry lives in the ≥900px media query so the narrow/mobile single-column layout
97
+ * (sidebar collapses, composer full-bleed) is left exactly as the bundle intends. Colours + margins only,
98
+ * fully reversible, every feature intact. */
99
+ html[data-skin="slack"] .holo-wa-root {
100
+ --wa-bg:#150E1A; --wa-side:#2A1530; --wa-head:#1B1B1F; --wa-in:#26262B; --wa-out:#3A2140;
101
+ --wa-ink:#E9E5ED; --wa-dim:#A99DB4; --wa-accent:#C6A6E6; --wa-line:#ffffff12;
102
+ --slk-pane:#1B1B1F; --slk-hi:#5C2D66; --slk-hover:#3A2140; --slk-gap:10px;
103
+ background: var(--wa-bg) !important;
104
+ }
105
+ /* chrome behind the floating pane (the colour seen in the inset margin/gutter) */
106
+ html[data-skin="slack"] .holo-wa-root.holo-wa-root,
107
+ html[data-skin="slack"] .holo-wa-root .cs-main-container { background: var(--wa-bg) !important; }
108
+ /* the WebGPU wallpaper backdrop is off in Slack — the pane is a flat surface */
109
+ html[data-skin="slack"] .holo-wa-root .holo-backdrop { display: none !important; }
110
+ /* far-left workspace rail: darkest aubergine */
111
+ html[data-skin="slack"] .holo-wa-root .holo-rail { background:#1C0E22 !important; border-right:1px solid #ffffff10; }
112
+ html[data-skin="slack"] .holo-wa-root .holo-rail-btn.on,
113
+ html[data-skin="slack"] .holo-wa-root .holo-rail-net.on { color: var(--wa-accent) !important; }
114
+ /* cohesive aubergine sidebar (title/chips/catch-up all share the sidebar surface) */
115
+ html[data-skin="slack"] .holo-wa-root .holo-list-head,
116
+ html[data-skin="slack"] .holo-wa-root .holo-list-title,
117
+ html[data-skin="slack"] .holo-wa-root .holo-chips,
118
+ html[data-skin="slack"] .holo-wa-root .holo-catchup { background: var(--wa-side) !important; }
119
+ html[data-skin="slack"] .holo-wa-root .cs-search { background:#1F0F24 !important; border:1px solid #ffffff12 !important; color:var(--wa-ink) !important; }
120
+ /* conversation rows = inset rounded pills (Slack). Inset applied to EVERY row so hover/active only swap the
121
+ * background — no layout jump. Active carries the highlight + white text. */
122
+ html[data-skin="slack"] .holo-wa-root .cs-conversation { margin:1px 8px !important; border-radius:8px !important; }
123
+ html[data-skin="slack"] .holo-wa-root .cs-conversation:hover { background: var(--slk-hover) !important; }
124
+ html[data-skin="slack"] .holo-wa-root .cs-conversation.cs-conversation--active { background: var(--slk-hi) !important; color:#fff !important; }
125
+ html[data-skin="slack"] .holo-wa-root .cs-conversation.cs-conversation--active * { color:#fff !important; }
126
+ /* pane + its inner surfaces (colours are width-independent; the float geometry is gated below) */
127
+ html[data-skin="slack"] .holo-wa-root .cs-chat-container { background: var(--slk-pane) !important; }
128
+ html[data-skin="slack"] .holo-wa-root .cs-message-list { background: var(--slk-pane) !important; background-image: none !important; }
129
+ html[data-skin="slack"] .holo-wa-root .cs-conversation-header { background: var(--slk-pane) !important; border-bottom: 1px solid #ffffff12; }
130
+ html[data-skin="slack"] .holo-wa-root .holo-composer { background: var(--slk-pane) !important; border-top: 1px solid #ffffff12; }
131
+ html[data-skin="slack"] .holo-wa-root .holo-c-input { background:#26262B !important; border:1px solid #ffffff14 !important; border-radius:8px !important; color:var(--wa-ink) !important; }
132
+
133
+ /* ── the floating envelope (desktop only; narrow layout keeps the bundle's single-column defaults) ── */
134
+ @media (min-width: 900px) {
135
+ html[data-skin="slack"] .holo-wa-root .cs-chat-container {
136
+ flex: 1 1 auto !important; min-width: 0; height: auto !important; align-self: stretch;
137
+ margin: var(--slk-gap) !important; /* even inset on all four sides */
138
+ border-radius: 12px; overflow: hidden;
139
+ box-shadow: 0 0 0 1px #00000055, 0 14px 38px #00000085;
140
+ }
141
+ /* glue the composer to the gapped pane: left tracks the sidebar's 34%, plus the gutter */
142
+ html[data-skin="slack"] .holo-wa-root .holo-composer {
143
+ left: calc(34% + var(--slk-gap)) !important; right: var(--slk-gap) !important;
144
+ bottom: var(--slk-gap) !important; width: auto !important; border-radius: 0 0 12px 12px;
145
+ }
146
+ }
147
+
148
+ /* ── THEATRE MODE — full-screen wallpaper + glass, under the Slack skin ────────────────────────────
149
+ * Theatre (`.theater`, toggle ⌘/Ctrl+\) collapses the rail + sidebar and floats ONE conversation over a
150
+ * FULL-BLEED wallpaper as centred glass capsules (see whatsapp-theme.css "Stage 6"). Three things make it
151
+ * immersive + fast here:
152
+ * 1. WALLPAPER edge-to-edge — a static image on the root (the whole viewport), NOT the WebGPU `.holo-backdrop`
153
+ * canvas (which we turn OFF: it was rendering at opacity:0, all cost no pixels). The image is the one
154
+ * app.html already preloads, so it paints with zero extra latency; every layer above it is transparent.
155
+ * 2. LIGHT + CLEAR — the heavy radial vignette + hue-tint is replaced by a soft top/bottom legibility scrim
156
+ * only, so the middle of the wallpaper stays bright and open.
157
+ * 3. STUNNING GLASS, CHEAP — real backdrop-blur is limited to the TWO capsules (header + composer); message
158
+ * bubbles are plain translucent fills (no per-bubble blur) so a long thread never janks input.
159
+ * Selector depth `…"slack"] .holo-wa-root.theater .X` = (0,4,1) beats the base slack rules (0,3,1) and
160
+ * theatre's own (0,3,0); transforms are left untouched so theatre's centring + auto-quiet fade keep animating. */
161
+ html[data-skin="slack"] .holo-wa-root.theater {
162
+ /* FOLLOW the operator's chosen wallpaper (--wp-img, set from localStorage holo-messenger/wallpaper-src),
163
+ falling back to the preloaded default when unset. `none` (the "plain" mode) is honoured → dark. */
164
+ background: #0b0f14 var(--wp-img, url("/apps/holo-messenger/_vendor/wallpaper-default.jpg")) center center / cover no-repeat !important;
165
+ }
166
+ html[data-skin="slack"] .holo-wa-root.theater .holo-backdrop { display: none !important; } /* WebGPU canvas off → lower latency */
167
+ html[data-skin="slack"] .holo-wa-root.theater .cs-chat-container {
168
+ background: transparent !important; margin: 0 !important; border-radius: 0 !important;
169
+ overflow: visible !important; box-shadow: none !important; /* full-bleed, no slack box */
170
+ }
171
+ /* transparent reading column, aligned to the SAME 812px as the chrome so bubbles line up with the composer */
172
+ html[data-skin="slack"] .holo-wa-root.theater .cs-message-list {
173
+ background: transparent !important;
174
+ padding-left: max(20px, calc((100% - 812px) / 2)) !important;
175
+ padding-right: max(20px, calc((100% - 812px) / 2)) !important;
176
+ }
177
+ /* clear + light: only a soft top/bottom scrim for legibility; the wallpaper stays bright in the middle */
178
+ html[data-skin="slack"] .holo-wa-root.theater .holo-main::before {
179
+ background: linear-gradient(to bottom, rgba(0,0,0,.30) 0%, transparent 16%, transparent 80%, rgba(0,0,0,.42) 100%) !important;
180
+ }
181
+ /* stunning glass capsules — vivid backdrop-blur, crisp edge, top highlight (only these two get real blur) */
182
+ html[data-skin="slack"] .holo-wa-root.theater .cs-conversation-header,
183
+ html[data-skin="slack"] .holo-wa-root.theater .holo-composer {
184
+ background: rgba(22,24,32,.44) !important;
185
+ -webkit-backdrop-filter: blur(30px) saturate(1.4) brightness(1.06) !important;
186
+ backdrop-filter: blur(30px) saturate(1.4) brightness(1.06) !important;
187
+ border: 1px solid rgba(255,255,255,.16) !important;
188
+ box-shadow: 0 14px 40px rgba(0,0,0,.42), inset 0 1px 0 rgba(255,255,255,.16) !important;
189
+ }
190
+ html[data-skin="slack"] .holo-wa-root.theater .holo-composer {
191
+ left: 50% !important; right: auto !important; width: min(calc(100% - 40px), 812px) !important;
192
+ bottom: 18px !important; border-radius: 18px !important;
193
+ }
194
+ /* bubbles: light translucent fills (no blur = no jank on long threads) */
195
+ html[data-skin="slack"] .holo-wa-root.theater .cs-message--incoming .cs-message__content { background: rgba(28,28,36,.62) !important; }
196
+ html[data-skin="slack"] .holo-wa-root.theater .cs-message--outgoing .cs-message__content { background: rgba(58,33,64,.60) !important; }
197
+
198
+ /* ── FRAME-COLLAPSED — the immersive focus view (chats folded into the rail) ───────────────────────
199
+ * "Collapse chats into the rail" (`.holo-main.frame-collapsed`) folds the conversation list away and lets
200
+ * ONE conversation take the whole frame. The slack skin otherwise left it broken: the composer stayed pinned
201
+ * to `left:34%` (the vanished sidebar edge) so it floated stranded in the middle. Here we make it the SAME
202
+ * immersive treatment as theatre — full-screen wallpaper + glass capsules — but KEEP the rail as a clean
203
+ * glass nav (that's the whole point of this mode), and centre the header/composer/reading-column in the frame
204
+ * to the RIGHT of the rail so everything lines up.
205
+ * · `:has(.holo-main.frame-collapsed)` puts the wallpaper on the ROOT (so it sits behind the rail too).
206
+ * · `:not(.theater)` so theatre (which hides the rail) keeps full ownership when both happen to be on.
207
+ * · Only 3 blur layers total (rail + header + composer); bubbles are plain translucent — stays responsive. */
208
+ html[data-skin="slack"] .holo-wa-root:has(.holo-main.frame-collapsed):not(.theater) {
209
+ /* same as theatre: follow the operator's --wp-img, fall back to the preloaded default */
210
+ background: #0b0f14 var(--wp-img, url("/apps/holo-messenger/_vendor/wallpaper-default.jpg")) center center / cover no-repeat !important;
211
+ }
212
+ html[data-skin="slack"] .holo-wa-root:has(.holo-main.frame-collapsed):not(.theater) .holo-backdrop { display: none !important; }
213
+ /* the left nav bar stays — a clean SOLID strip; the wallpaper covers the entire screen EXCEPT this rail */
214
+ html[data-skin="slack"] .holo-wa-root:has(.holo-main.frame-collapsed):not(.theater) .holo-rail {
215
+ background: #17111D !important;
216
+ border-right: 1px solid rgba(255,255,255,.08) !important;
217
+ }
218
+ /* transparent full-bleed frame + reading column centred in the space right of the rail */
219
+ html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed .cs-chat-container {
220
+ background: transparent !important; margin: 0 !important; border-radius: 0 !important; overflow: visible !important; box-shadow: none !important;
221
+ }
222
+ html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed .cs-message-list {
223
+ background: transparent !important;
224
+ padding-left: max(20px, calc((100% - 812px) / 2)) !important;
225
+ padding-right: max(20px, calc((100% - 812px) / 2)) !important;
226
+ }
227
+ html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed::before {
228
+ content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 0;
229
+ background: linear-gradient(to bottom, rgba(0,0,0,.30) 0%, transparent 16%, transparent 80%, rgba(0,0,0,.42) 100%);
230
+ }
231
+ /* glass header + composer, centred in the frame (the rail is the nav, the frame is everything right of it) */
232
+ html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed .cs-conversation-header,
233
+ html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed .holo-composer {
234
+ background: rgba(22,24,32,.44) !important;
235
+ -webkit-backdrop-filter: blur(30px) saturate(1.4) brightness(1.06) !important;
236
+ backdrop-filter: blur(30px) saturate(1.4) brightness(1.06) !important;
237
+ border: 1px solid rgba(255,255,255,.16) !important;
238
+ box-shadow: 0 14px 40px rgba(0,0,0,.42), inset 0 1px 0 rgba(255,255,255,.16) !important;
239
+ }
240
+ html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed .cs-conversation-header {
241
+ margin: 14px auto 0 !important; max-width: 812px !important; width: calc(100% - 40px) !important; border-radius: 16px !important;
242
+ }
243
+ html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed .holo-composer {
244
+ left: 50% !important; right: auto !important; transform: translateX(-50%) !important;
245
+ width: min(calc(100% - 40px), 812px) !important; bottom: 18px !important; border-radius: 18px !important;
246
+ }
247
+ html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed .cs-message--incoming .cs-message__content { background: rgba(28,28,36,.62) !important; }
248
+ html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed .cs-message--outgoing .cs-message__content { background: rgba(58,33,64,.60) !important; }
249
+
250
+ /* ── BASE-LAYER fix: composer alignment in frame-collapsed, for ANY skin ───────────────────────────
251
+ * Skin-agnostic (this file is loaded regardless of the active skin). The bundle pins the composer to
252
+ * `left:34%` — the sidebar's flex-basis — but frame-collapsed folds the sidebar to 0, so on every NON-slack
253
+ * skin (graphite/warm/contrast/light/none) the composer floated stranded in the middle. Align it to the
254
+ * full-bleed chat (left:0 = the rail's right edge; right:0 is already the base). `:not(.theater)` yields to
255
+ * theatre's own centred composer; slack's higher-specificity immersive rule above wins for the slack skin. */
256
+ .holo-wa-root:not(.theater) .holo-main.frame-collapsed .holo-composer { left: 0 !important; }
b/6bbf54f41980838ea8cf1c08123b7aa6e9820eeb58da964cd50e875b2893faef ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en"><head>
3
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
4
+ <title>Holo Messenger</title>
5
+ <link rel="stylesheet" href="/apps/holo-messenger/_vendor/ui/chat-ui.bundle.css?v=a0c782c6ebee">
6
+ <style>html,body{height:100%;margin:0;background:#0b141a;color-scheme:dark}#root{height:100vh}</style>
7
+ <link rel="preload" as="image" href="/apps/holo-messenger/_vendor/wallpaper-default.jpg" fetchpriority="high">
8
+ </head><body>
9
+ <div id="root"></div>
10
+ <script type="module">
11
+ // ?v=N - bump on each deploy so the browser can never serve a stale cached bundle (the cause of a frozen/old build).
12
+ import "/apps/holo-messenger/_vendor/ui/chat-ui.bundle.js?v=11d9ba0d372a"; // → window.HoloMessengerUI
13
+ // Opt-in: render the WhatsApp UX from STREAMING shadcn κ-components instead of the chatscope bundle.
14
+ // Additive — only takes over window.HoloMessengerUI.mount when ?ui=shadcn (or localStorage["holo.ui.shadcn"]="1");
15
+ // the default boot stays the untouched chatscope UI. Runs before boot() so it is fed the same live model.
16
+ import "/apps/holo-messenger/messenger-shadcn-ui.mjs";
17
+ import { boot } from "/apps/holo-messenger/holo-messenger-app.mjs?v=076005621416";
18
+ boot(document.getElementById("root")).catch(e => {
19
+ document.body.innerHTML = "<pre style='color:#f88;padding:16px;white-space:pre-wrap'>boot error: " + (e && e.stack || e) + "</pre>";
20
+ });
21
+ // Intelligent Inbox (holo-mail) - additive, feature-flagged OFF by default. Dynamic import + catch so it
22
+ // can NEVER block or break the messenger boot. Enable per-operator: localStorage["holo.mail.enabled"]="1".
23
+ import("/apps/holo-messenger/mail/holo-mail-attach.mjs").catch(() => {});
24
+ // HoloChat, woven into THIS messenger — one canonical surface, no separate overlay. Cards render natively
25
+ // in the bubbles; the composer "+" opens the unified tray (apps · Spaces · money · identity · prove).
26
+ // Additive + catch-guarded so it can never block boot. Enable: localStorage["holo.chat.enabled"]="1" or ?chat=1.
27
+ import("/apps/holo-messenger/holo-messenger-weave.mjs").catch(() => {});
28
+ // Colour re-tone picker (Default · Graphite · Warm · Contrast · Light) — colours only, all features
29
+ // intact. Gated: shows only with ?skins=1 or localStorage["holo.skins"]="1". Additive + catch-guarded.
30
+ import("/apps/holo-messenger/messenger-skin.mjs").catch(() => {});
31
+ // Asset cache (service worker) — repeat opens serve wallpaper/logos/bundle/css/js NETWORK-FREE. Caches
32
+ // only own-origin static assets (stale-while-revalidate); all dynamic/API/bridge/media traffic passes
33
+ // through untouched. Additive + catch-guarded. Escape hatch: open with ?nosw=1 to unregister.
34
+ import("/apps/holo-messenger/messenger-sw-register.mjs").catch(() => {});
35
+ </script>
36
+ </body></html>
b/8bd23f0acbaf00093b9daedeac08c9513ccbe0a8647b91eb104bfd27175682a8 ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // messenger-sw-register.mjs — eagerly register the ONE messenger service worker (holo-sw.js: Reach push +
2
+ // M1 shell cache) on boot, so repeat opens are network-free even before push permission is granted. Additive
3
+ // + catch-guarded so it can never block boot. Escape hatch: open with ?nosw=1 to unregister it.
4
+ //
5
+ // holo-sw.js is ALSO registered by the Reach push flow (holo-push.mjs) — same script, same {type:module}, same
6
+ // dir scope, so the two calls collapse to one registration (idempotent). We register here too because caching
7
+ // must not wait on push. We also retire any stale competing worker (the old messenger-sw.js) at this scope.
8
+
9
+ (async () => {
10
+ if (!("serviceWorker" in navigator)) return;
11
+ try {
12
+ const u = new URL(location.href);
13
+ if (u.searchParams.get("nosw") === "1") {
14
+ for (const r of await navigator.serviceWorker.getRegistrations()) {
15
+ if ((r.scope || "").includes("/apps/holo-messenger/")) await r.unregister();
16
+ }
17
+ console.log("[msgr-sw] unregistered (nosw=1)");
18
+ return;
19
+ }
20
+ // retire the old competing cache-worker if a previous build left it controlling this scope.
21
+ try {
22
+ for (const r of await navigator.serviceWorker.getRegistrations()) {
23
+ const s = (r.active && r.active.scriptURL) || (r.installing && r.installing.scriptURL) || (r.waiting && r.waiting.scriptURL) || "";
24
+ if (s.includes("/messenger-sw.js")) { await r.unregister(); console.log("[msgr-sw] retired stale messenger-sw.js"); }
25
+ }
26
+ } catch {}
27
+ await navigator.serviceWorker.register("/apps/holo-messenger/holo-sw.js", { type: "module", scope: "/apps/holo-messenger/" });
28
+ console.log("[msgr-sw] one worker active (push + shell cache) — repeat opens serve the shell network-free");
29
+ } catch (e) { /* never block boot */ }
30
+ })();
b/9e602f8270c163a794c8e1b9d455569a2069c15e2f7db955ded05659b326567b ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // holo-mail-attach.mjs - the in-app entry. Loaded additively by app.html; mounts the Intelligent Inbox
2
+ // (holo-mail-ui) over the REAL engine (email-bridge provider + on-device Q + holo-strand) as a full-screen
3
+ // overlay. When no mailbox is linked yet, it shows the one-tap ONBOARDING first. FEATURE-FLAGGED OFF by
4
+ // default: with the flag off nothing is added to the page, so it can't affect the messenger's paint.
5
+ // flag on: localStorage.setItem("holo.mail.enabled","1") - or open with ?mail=1
6
+ // manual: window.HoloMailOpen() (always available)
7
+
8
+ import { makeMailProvider } from "/apps/holo-messenger/mail/holo-mail-provider.mjs";
9
+ import { makeMultiProvider } from "/apps/holo-messenger/mail/holo-mail-multi.mjs";
10
+ import { attachMailEngine } from "/apps/holo-messenger/mail/holo-mail-engine.mjs";
11
+ import { mountMailUI } from "/apps/holo-messenger/mail/holo-mail-ui.mjs";
12
+ import { makeOnboarding } from "/apps/holo-messenger/mail/holo-mail-onboard.mjs";
13
+ import { mountOnboard } from "/apps/holo-messenger/mail/holo-mail-onboard-ui.mjs";
14
+ import { makeMailHealth } from "/apps/holo-messenger/mail/holo-mail-health.mjs";
15
+
16
+ const BASE = "http://127.0.0.1:8793"; // the primary bridge (account #1). More = more ports.
17
+ const SUPERVISOR = "http://127.0.0.1:8795"; // email-supervisor: spawns/owns one bridge per mailbox
18
+ const ACCOUNTS_KEY = "holo.mail.accounts";
19
+ // Each account is its own email-bridge instance on its own port (isolated state dir), spawned by the
20
+ // supervisor. The list is cached in localStorage so buildProvider() stays synchronous; the supervisor is the
21
+ // source of truth and we refresh from it whenever the inbox opens. The provider is ALWAYS the unified
22
+ // multi-provider, so the engine/UI work over one or many mailboxes unchanged.
23
+ const DEFAULT_ACCOUNTS = [{ id: "default", label: "", base: BASE }];
24
+ function loadAccounts() {
25
+ try { const a = JSON.parse(localStorage.getItem(ACCOUNTS_KEY) || "null"); if (Array.isArray(a) && a.length) return a; } catch {}
26
+ return DEFAULT_ACCOUNTS.slice();
27
+ }
28
+ function saveAccounts(list) { try { localStorage.setItem(ACCOUNTS_KEY, JSON.stringify(list)); } catch {} }
29
+ const primaryBase = () => (loadAccounts()[0] || { base: BASE }).base;
30
+ const flagOn = () => { try { return localStorage.getItem("holo.mail.enabled") === "1" || /(?:^|[?&])mail=1(?:&|$)/.test(location.search); } catch { return false; } };
31
+
32
+ function buildProvider() {
33
+ return makeMultiProvider({ accounts: loadAccounts().map((a) => ({ id: a.id, label: a.label, provider: makeMailProvider({ base: a.base }) })) });
34
+ }
35
+
36
+ // the supervisor's control API (loopback). Absent in the web preview / no-host dev - every call fails soft.
37
+ function supervisor(base = SUPERVISOR) {
38
+ const j = async (p, init) => { const r = await fetch(base + p, init); if (!r.ok) throw new Error(`${p} → ${r.status}`); return r.json(); };
39
+ return {
40
+ reachable: async () => { try { await j("/health"); return true; } catch { return false; } },
41
+ accounts: () => j("/accounts"),
42
+ add: (label) => j("/accounts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ label }) }),
43
+ remove: (id) => j("/accounts/" + encodeURIComponent(id), { method: "DELETE" }),
44
+ };
45
+ }
46
+ // pull the authoritative account list from the supervisor into the local cache (so buildProvider sees it).
47
+ async function syncAccountsFromSupervisor() {
48
+ try {
49
+ const rows = await supervisor().accounts();
50
+ if (Array.isArray(rows) && rows.length) saveAccounts(rows.map((r) => ({ id: r.id, label: r.label || "", base: r.base })));
51
+ return rows;
52
+ } catch { return null; } // supervisor down → keep whatever's cached (single default account still works)
53
+ }
54
+
55
+ // Account management surface. add() asks the supervisor to spawn a fresh bridge, then walks the user through
56
+ // linking THAT mailbox; it then joins the unified inbox. Fallback (no supervisor): register a manual port.
57
+ if (typeof window !== "undefined") window.HoloMailAccounts = {
58
+ list: () => loadAccounts(),
59
+ refresh: () => syncAccountsFromSupervisor(),
60
+ async add({ label = "", port, base } = {}) {
61
+ if (await supervisor().reachable()) {
62
+ const acct = await supervisor().add(label); // spawns the bridge on a fresh port
63
+ await syncAccountsFromSupervisor();
64
+ await addAccountFlow(acct.base); // link the new mailbox → unified inbox
65
+ return loadAccounts();
66
+ }
67
+ // no supervisor: accept a manually-started bridge (EMAIL_BRIDGE_PORT=<port> node email-bridge.mjs)
68
+ const b = base || (port ? `http://127.0.0.1:${port}` : null);
69
+ if (!b) throw new Error("no supervisor - pass a port of a manually-started bridge");
70
+ const list = loadAccounts();
71
+ if (!list.some((a) => a.base === b)) { list.push({ id: "acct" + (list.length + 1), label, base: b }); saveAccounts(list); }
72
+ await addAccountFlow(b);
73
+ return loadAccounts();
74
+ },
75
+ async remove(id) {
76
+ try { if (await supervisor().reachable()) await supervisor().remove(id); } catch {}
77
+ const list = loadAccounts().filter((a) => a.id !== id); saveAccounts(list.length ? list : DEFAULT_ACCOUNTS.slice());
78
+ if (overlay && overlay.style.display !== "none") openMail(true);
79
+ return loadAccounts();
80
+ },
81
+ reset() { try { localStorage.removeItem(ACCOUNTS_KEY); } catch {} return loadAccounts(); },
82
+ };
83
+
84
+ // drive onboarding for a specific (freshly-spawned, unlinked) mailbox, then return to the unified inbox.
85
+ async function addAccountFlow(targetBase) {
86
+ if (!overlay) await openMail();
87
+ showOnboarding(buildProvider(), targetBase);
88
+ }
89
+
90
+ // a thin client over the loopback bridge (creds pass straight through, never stored here).
91
+ function bridgeClient(base) {
92
+ const j = async (p, init) => { const r = await fetch(base + p, init); if (!r.ok) throw new Error(`${p} → ${r.status}`); return r.json(); };
93
+ return {
94
+ status: () => j("/status"),
95
+ login: (body) => j("/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }),
96
+ };
97
+ }
98
+
99
+ // build the inbox UI's `source` over the live provider corpus (filled by the bridge SSE stream).
100
+ function buildSource(provider) {
101
+ return {
102
+ async ids() {
103
+ let list = []; try { list = await provider.listThreads(); } catch {}
104
+ const ids = (list || []).map((s) => s.jid).filter(Boolean);
105
+ if (ids.length) return ids;
106
+ const seen = new Set(); provider.allMessages().forEach((m) => seen.add(m.threadId)); return [...seen];
107
+ },
108
+ meta(jid) {
109
+ const msgs = provider.threadMessages(jid);
110
+ const inbound = msgs.find((m) => !m.fromMe) || msgs[0] || {};
111
+ const last = msgs[msgs.length - 1] || {};
112
+ return { who: inbound.fromName || inbound.from || jid, subj: last.subject || inbound.subject || "(no subject)",
113
+ account: provider.accountLabel ? provider.accountLabel(jid) : null,
114
+ msgs: msgs.map((m) => ({ fromMe: m.fromMe, fromName: m.fromName || m.from, from: m.from, date: m.date, text: m.text })) };
115
+ },
116
+ };
117
+ }
118
+
119
+ let overlay = null, host = null, banner = null, ui = null, health = null;
120
+
121
+ const clearHost = () => { host.innerHTML = ""; host.className = ""; };
122
+
123
+ function showBanner(state) {
124
+ if (!banner) return;
125
+ if (state && state.needsReconnect) {
126
+ banner.innerHTML = `<span>${state.message || "Reconnect needed."}</span><button class="hm-reconnect">Reconnect</button>`;
127
+ banner.style.display = "flex";
128
+ banner.querySelector(".hm-reconnect").onclick = () => { banner.style.display = "none"; showOnboarding(buildProvider()); };
129
+ } else { banner.style.display = "none"; }
130
+ }
131
+
132
+ function showOnboarding(provider, targetBase = primaryBase()) {
133
+ if (health) { health.stop(); health = null; }
134
+ clearHost();
135
+ const onboarding = makeOnboarding({ bridge: bridgeClient(targetBase) });
136
+ mountOnboard(host, { onboarding, onLinked: () => showInbox(buildProvider()) }); // rebuild → include newly-linked account
137
+ }
138
+
139
+ async function showInbox(provider) {
140
+ clearHost();
141
+ const engine = await attachMailEngine({ base: primaryBase(), provider }); // real Q + window.HoloStrand
142
+ try { await provider.connect(); } catch {} // live SSE stream (fans out to all accounts)
143
+ await new Promise((r) => setTimeout(r, 600)); // let first messages land
144
+ ui = mountMailUI(host, { engine, source: buildSource(provider), foot: "On-device Q · sovereign κ",
145
+ onSend: (jid, text) => { try { provider.sendReply({ chat: jid, text }); } catch {} } });
146
+ // keep the mailbox healthy: a dropped/expired sign-in surfaces a reconnect banner instead of a dead inbox.
147
+ health = makeMailHealth({ bridge: bridgeClient(primaryBase()) });
148
+ health.start(showBanner);
149
+ }
150
+
151
+ export async function openMail(rebuild = false) {
152
+ if (overlay && rebuild) { closeMail(); overlay.remove(); overlay = null; } // re-render over a changed account set
153
+ if (overlay) { overlay.style.display = "block"; return; }
154
+ overlay = document.createElement("div");
155
+ overlay.id = "holo-mail-overlay";
156
+ overlay.style.cssText = "position:fixed;inset:0;z-index:2147482000;background:#0b0d10";
157
+ banner = document.createElement("div");
158
+ banner.style.cssText = "display:none;position:absolute;top:0;left:0;right:0;z-index:2147483001;align-items:center;justify-content:center;gap:12px;padding:9px 14px;background:#e6462e;color:#fff;font:600 13px system-ui";
159
+ host = document.createElement("div"); host.style.cssText = "position:absolute;inset:0";
160
+ const closer = document.createElement("button"); closer.textContent = "✕"; closer.title = "Close (Esc)";
161
+ closer.style.cssText = "position:fixed;top:10px;right:14px;z-index:2147483002;border:0;background:#0006;color:#fff;width:30px;height:30px;border-radius:50%;cursor:pointer;font-size:15px";
162
+ closer.onclick = closeMail;
163
+ const reBtnCss = "border:0;background:#fff;color:#e6462e;border-radius:7px;padding:4px 12px;font:inherit;font-weight:700;cursor:pointer";
164
+ const style = document.createElement("style"); style.textContent = `.hm-reconnect{${reBtnCss}}`; overlay.appendChild(style);
165
+ overlay.append(host, banner, closer); document.body.appendChild(overlay);
166
+ document.addEventListener("keydown", escClose);
167
+
168
+ // refresh the account list from the supervisor (source of truth), then build the unified multi-provider.
169
+ await syncAccountsFromSupervisor();
170
+ const provider = buildProvider();
171
+ let st = null; try { st = await bridgeClient(primaryBase()).status(); } catch {}
172
+ if (st && st.linked) return showInbox(provider);
173
+ showOnboarding(provider);
174
+ }
175
+
176
+ export function closeMail() { if (health) { health.stop(); health = null; } if (overlay) overlay.style.display = "none"; }
177
+ function escClose(e) { if (e.key === "Escape" && overlay && overlay.style.display !== "none" && !document.querySelector(".hm-scrim.on")) closeMail(); }
178
+
179
+ function addLauncher() {
180
+ if (document.getElementById("holo-mail-launch")) return;
181
+ const b = document.createElement("button");
182
+ b.id = "holo-mail-launch"; b.title = "Holo Mail"; b.textContent = "✦";
183
+ b.style.cssText = "position:fixed;bottom:18px;right:18px;z-index:2147481000;width:44px;height:44px;border-radius:50%;border:0;cursor:pointer;color:#fff;font-size:18px;background:linear-gradient(135deg,#3b6ef5,#8a4fd6);box-shadow:0 6px 20px rgba(0,0,0,.35)";
184
+ b.onclick = openMail; document.body.appendChild(b);
185
+ }
186
+
187
+ if (typeof window !== "undefined") {
188
+ window.HoloMailOpen = openMail;
189
+ window.HoloMailClose = closeMail;
190
+ if (flagOn()) {
191
+ if (document.readyState !== "loading") addLauncher();
192
+ else document.addEventListener("DOMContentLoaded", addLauncher);
193
+ }
194
+ }
195
+
196
+ export default { openMail, closeMail };
b/9f62d9e0480be19a114542104a51a4e69da89a49c2960d75910ed0b57ea41c55 ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // holo-messenger-weave.mjs — weave HoloChat INTO the real messenger (additive, no monolith edits).
2
+ //
3
+ // Why: there were two messengers — the beautiful one, and a plainer HoloChat surface behind "#". This
4
+ // dissolves the second into the first, so apps/money/identity/proofs live in the messenger you know.
5
+ // How: the real UI is a closed React/chatscope app exposing only low-level globals, so we weave at the
6
+ // DOM seam (the pattern the app itself uses to turn pay/watch links into cards): a body-level observer
7
+ // upgrades card-text bubbles into native cards, and the composer attach "+" opens the unified App Tray.
8
+ // What: in ANY conversation, a message whose text is a HoloChat card renders as that card; tapping "+"
9
+ // opens apps · Spaces · money · identity · prove, and a pick sends into the current conversation.
10
+
11
+ import "/apps/holo-messenger/holo-app-tray.mjs"; // installs window.HoloApps (+ HoloOpen)
12
+
13
+ const CONTENT = ".cs-message__content";
14
+ const EDITOR = ".cs-message-input__content-editor";
15
+ const SEND = ".cs-button--send";
16
+ const ATTACH = ".cs-button--attachment";
17
+
18
+ // drop a card into whatever conversation is open by POPULATING the real composer (reliable), then the user
19
+ // presses Send — exactly like attaching in WhatsApp. Set the contenteditable's text + dispatch a native
20
+ // `input` event so chatscope's React onInput registers it and enables Send; place the cursor at the end.
21
+ function composerChannel() {
22
+ return {
23
+ send(text) {
24
+ const ed = document.querySelector(EDITOR); if (!ed) return;
25
+ ed.focus();
26
+ ed.textContent = String(text);
27
+ ed.dispatchEvent(new InputEvent("input", { bubbles: true, cancelable: true, inputType: "insertText", data: String(text) }));
28
+ try { const r = document.createRange(); r.selectNodeContents(ed); r.collapse(false); const sel = getSelection(); sel.removeAllRanges(); sel.addRange(r); } catch {}
29
+ },
30
+ };
31
+ }
32
+
33
+ // upgrade one message-content node: if its text is a HoloChat card, replace it with the rendered card.
34
+ function upgrade(node) {
35
+ try {
36
+ if (!node || node.dataset.holoCard) return;
37
+ const A = window.HoloApps; if (!A || !A.isCard) return;
38
+ const text = (node.textContent || "").trim();
39
+ if (!A.isCard(text)) return;
40
+ node.dataset.holoCard = "1";
41
+ node.textContent = "";
42
+ node.appendChild(A.renderCard(text, composerChannel()));
43
+ } catch {}
44
+ }
45
+
46
+ // the composer attach "+" opens the unified tray (the modern "attach anything": apps, money, id, files).
47
+ function hookAttach(btn) {
48
+ if (!btn || btn.dataset.holoAttach) return;
49
+ btn.dataset.holoAttach = "1";
50
+ btn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); if (window.HoloApps) window.HoloApps.pick(composerChannel()); }, true);
51
+ }
52
+
53
+ // a native "Spaces" rail at the top of the chat list — the familiar status/Communities strip. Placed in
54
+ // the app's own .holo-convlist-wrap (a sibling of React's <ul>, so it survives re-renders); tap → open.
55
+ const WRAP = ".holo-convlist-wrap";
56
+ // one clean word per Space — no truncation, no ellipsis.
57
+ const SPACE_SHORT = { "org.hologram.holospace.Web3": "Web3", "org.hologram.holospace.CreatorStudio": "Studio", "org.hologram.holospace.DevCockpit": "Dev", "org.hologram.holospace.AILab": "AI", "org.hologram.holospace.TrustCenter": "Trust", "org.hologram.holospace.EmulationArcade": "Arcade" };
58
+ async function mountSpaces() {
59
+ try {
60
+ if (document.getElementById("holo-spaces")) return;
61
+ const list = document.querySelector(".cs-conversation-list"); if (!list) return;
62
+ const wrap = document.querySelector(WRAP) || list.parentElement; if (!wrap) return;
63
+ const A = window.HoloApps; if (!A || !A.catalog) return;
64
+ const spaces = (await A.catalog()).filter((x) => x.kind === "space");
65
+ if (!spaces.length || document.getElementById("holo-spaces")) return;
66
+ const open0 = (() => { try { return localStorage.getItem("holo.spaces.open") === "1"; } catch { return false; } })();
67
+ const sec = document.createElement("div"); sec.id = "holo-spaces";
68
+ sec.style.cssText = "flex:0 0 auto;border-bottom:1px solid #ffffff0d;background:transparent;";
69
+ const head = document.createElement("button"); head.type = "button"; // one quiet line by default; tap to reveal
70
+ head.style.cssText = "display:flex;align-items:center;gap:8px;width:100%;border:0;background:transparent;color:#e9edef;cursor:pointer;padding:11px 16px;font:600 13px system-ui;opacity:.6;transition:opacity .15s ease;";
71
+ head.innerHTML = `<span style="font-size:15px;">✨</span><span style="flex:1;text-align:left;">Spaces</span><span class="hsp-chev" style="font-size:17px;line-height:1;opacity:.7;transition:transform .18s ease;">›</span>`;
72
+ head.onmouseenter = () => { head.style.opacity = "1"; }; head.onmouseleave = () => { head.style.opacity = ".6"; };
73
+ const rail = document.createElement("div"); rail.style.cssText = "display:" + (open0 ? "flex" : "none") + ";gap:18px;overflow-x:auto;padding:2px 16px 12px;scrollbar-width:none;";
74
+ for (const s of spaces) {
75
+ const nm = SPACE_SHORT[s.id] || s.name;
76
+ const b = document.createElement("button"); b.type = "button"; b.title = s.tagline || s.name;
77
+ b.style.cssText = "flex:0 0 auto;display:flex;flex-direction:column;align-items:center;gap:6px;border:0;background:transparent;color:#e9edef;cursor:pointer;";
78
+ b.innerHTML = `<span class="hsp-av" style="width:40px;height:40px;border-radius:50%;display:grid;place-items:center;font-size:19px;background:${s.accent}12;box-shadow:inset 0 0 0 1px ${s.accent}44;transition:transform .15s ease,box-shadow .15s ease;">${s.glyph}</span>` +
79
+ `<span style="font-size:13px;font-weight:500;opacity:.62;">${nm}</span>`;
80
+ const av = b.querySelector(".hsp-av");
81
+ b.onmouseenter = () => { av.style.transform = "scale(1.08)"; av.style.boxShadow = "inset 0 0 0 1.5px " + s.accent + "aa"; b.lastChild.style.opacity = "1"; };
82
+ b.onmouseleave = () => { av.style.transform = "scale(1)"; av.style.boxShadow = "inset 0 0 0 1px " + s.accent + "44"; b.lastChild.style.opacity = ".62"; };
83
+ b.onclick = () => (window.HoloOpen ? window.HoloOpen(s) : A.openSpace(s));
84
+ rail.appendChild(b);
85
+ }
86
+ const chev = head.querySelector(".hsp-chev"); chev.style.transform = open0 ? "rotate(90deg)" : "rotate(0deg)";
87
+ head.onclick = () => {
88
+ const opening = rail.style.display === "none";
89
+ rail.style.display = opening ? "flex" : "none";
90
+ chev.style.transform = opening ? "rotate(90deg)" : "rotate(0deg)";
91
+ try { localStorage.setItem("holo.spaces.open", opening ? "1" : "0"); } catch {}
92
+ };
93
+ sec.append(head, rail); wrap.insertBefore(sec, list);
94
+ } catch {}
95
+ }
96
+
97
+ function scan(root) {
98
+ (root || document).querySelectorAll?.(CONTENT).forEach(upgrade);
99
+ (root || document).querySelectorAll?.(ATTACH).forEach(hookAttach);
100
+ mountSpaces();
101
+ }
102
+
103
+ // a light, reversible calm: soften the loudest native strip (the "need you" banner) and hide the rail's
104
+ // scrollbar. Conservative — no functional element is hidden; remove #holo-calm to revert entirely.
105
+ function injectCalm() {
106
+ if (document.getElementById("holo-calm")) return;
107
+ const st = document.createElement("style"); st.id = "holo-calm";
108
+ st.textContent = [
109
+ ".holo-catchup{opacity:.8;}", // soften the loud "need you" banner
110
+ ".holo-chip:not(.on){opacity:.62;font-weight:500;}", // quiet the inactive filters (all still work)
111
+ ".holo-chip{transition:opacity .15s ease;} .holo-chip:hover{opacity:1;}",
112
+ "#holo-spaces::-webkit-scrollbar{display:none;height:0;}",
113
+ ].join("");
114
+ (document.head || document.documentElement).appendChild(st);
115
+ }
116
+
117
+ let _obs = null;
118
+ function start() {
119
+ if (_obs || typeof MutationObserver === "undefined") return;
120
+ injectCalm();
121
+ scan(document);
122
+ _obs = new MutationObserver((muts) => {
123
+ for (const m of muts) for (const n of m.addedNodes) {
124
+ if (n.nodeType !== 1) continue;
125
+ if (n.matches?.(CONTENT)) upgrade(n);
126
+ if (n.matches?.(ATTACH)) hookAttach(n);
127
+ n.querySelectorAll?.(CONTENT).forEach(upgrade);
128
+ n.querySelectorAll?.(ATTACH).forEach(hookAttach);
129
+ if (n.matches?.(".cs-conversation-list") || n.querySelector?.(".cs-conversation-list")) mountSpaces();
130
+ }
131
+ });
132
+ _obs.observe(document.body, { childList: true, subtree: true });
133
+ }
134
+
135
+ export function installWeave() { if (typeof document !== "undefined") { if (document.body) start(); else document.addEventListener("DOMContentLoaded", start); } return true; }
136
+
137
+ if (typeof window !== "undefined") {
138
+ const flag = () => { try { return localStorage.getItem("holo.chat.enabled") === "1" || /(?:^|[?&])chat=1(?:&|$)/.test(location.search); } catch { return false; } };
139
+ if (flag()) installWeave();
140
+ }
141
+ export default { installWeave };
b/a0c782c6ebee1f7d91088089ea3a39942725459947f8882be8534cdb9f3d4029 ADDED
The diff for this file is too large to render. See raw diff
 
b/be803875bc9cdbbc411c84e5f8a407189d775d7d9fc3e72f0a28093f37414933 ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // messenger-shadcn-ui.mjs — DROP-IN mount(el, model) rendering the Holo Messenger UX from STREAMING
2
+ // shadcn κ-components, faithful to the real app (network app-rail · Q sidebar · triage · rich rows ·
3
+ // Q chat pane · earth wallpaper · real SVG line icons), same contract as the chatscope bundle.
4
+ //
5
+ // ADDITIVE + SAFE: only takes over window.HoloMessengerUI.mount on opt-in (?ui=shadcn or
6
+ // localStorage["holo.ui.shadcn"]="1"). Default boot stays the untouched chatscope UI.
7
+ //
8
+ // PERF: memoized rows · composer owns its draft · windowed thread (≤WINDOW rows in DOM; 1M chat stays tiny).
9
+ // PASS 1 = faithful shell. Rich message content + action surface are Pass 2/3.
10
+
11
+ const WANT = (() => {
12
+ try { const u = new URL(location.href);
13
+ if (u.searchParams.get("ui") === "shadcn") return true;
14
+ if (u.searchParams.get("ui") === "chatscope") return false;
15
+ return localStorage.getItem("holo.ui.shadcn") === "1";
16
+ } catch { return false; }
17
+ })();
18
+
19
+ const WINDOW = 80;
20
+ const BRANDS = "/usr/share/brands/";
21
+ const WP = "/apps/holo-messenger/_vendor/wallpaper-default.jpg"; // the earth photo (default)
22
+ const NET_SLUG = { whatsapp: "whatsapp", telegram: "telegram", signal: "signal", imessage: "imessage", instagram: "instagram", messenger: "messenger", discord: "discord", slack: "slack", gmessages: "googlemessages", googlemessages: "googlemessages", x: "x", twitter: "x", linkedin: "linkedin", gmail: "gmail", email: "gmail", internal: null };
23
+
24
+ const CSS = `
25
+ .wa{height:100dvh;display:grid;grid-template-columns:64px minmax(300px,.46fr) minmax(0,1fr);background:#0b141a;color:#e9edef;
26
+ font:14.5px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
27
+ .wa *{box-sizing:border-box}
28
+ .wa svg{display:block}
29
+ .rail{background:#111b21;border-right:1px solid #0a1015;display:flex;flex-direction:column;align-items:center;gap:7px;padding:12px 0}
30
+ .rb{position:relative;width:46px;height:46px;border-radius:15px;display:grid;place-items:center;cursor:pointer;color:#aebac1;transition:background .12s,color .12s;background:transparent;border:0}
31
+ .rb:hover{background:#202c33;color:#e9edef}.rb.on{background:#2a3942;color:#00a884}
32
+ .rb img{width:28px;height:28px;border-radius:8px;object-fit:cover}
33
+ .rb .dot{position:absolute;right:5px;bottom:5px;width:10px;height:10px;border-radius:50%;background:#00d95f;border:2px solid #111b21}
34
+ .rb .bdg{position:absolute;top:-3px;right:-3px;min-width:19px;height:19px;border-radius:10px;background:#00a884;color:#0b141a;font-size:11px;font-weight:700;display:grid;place-items:center;padding:0 4px;border:2px solid #111b21}
35
+ .rsep{width:28px;height:1px;background:#22303a;margin:3px 0}
36
+ .rgrow{flex:1 1 auto}
37
+ .rav{width:40px;height:40px;border-radius:50%;overflow:hidden;cursor:pointer;background:radial-gradient(circle at 35% 30%,#7cf0c8,#2b9e7a 60%,#0b3b2e);margin-top:2px}
38
+ .side{background:#111b21;border-right:1px solid #222d34;display:flex;flex-direction:column;min-width:0}
39
+ .stitle{display:flex;align-items:center;gap:6px;padding:15px 14px 10px}
40
+ .stitle .t{font-size:23px;font-weight:600;letter-spacing:-.4px;flex:1 1 auto}
41
+ .search{padding:4px 12px 8px;position:relative}
42
+ .search .si{position:absolute;left:26px;top:50%;transform:translateY(-50%);color:#8696a0;pointer-events:none;z-index:1;display:grid}
43
+ .filt{display:flex;align-items:center;gap:8px;padding:2px 14px 8px}
44
+ .chip{display:inline-flex;align-items:center;gap:6px;background:#202c33;color:#8696a0;border:0;border-radius:999px;padding:5px 14px;font-size:13px;cursor:pointer}
45
+ .chip.on{background:#0b3b2e;color:#00d95f}
46
+ .caret{background:#202c33;color:#8696a0;border:0;border-radius:999px;width:30px;height:28px;display:grid;place-items:center;cursor:pointer;font-size:11px}
47
+ .triage{margin:2px 12px 8px;display:flex;align-items:center;gap:9px;background:linear-gradient(90deg,#0e2a24,#122a1e);color:#7cf0c8;border:1px solid #123c30;border-radius:14px;padding:9px 14px;font-size:13.5px;cursor:pointer}
48
+ .triage .sk{color:#00d95f;display:grid;flex:0 0 auto}
49
+ .chats{flex:1 1 auto;overflow-y:auto;overflow-x:hidden}
50
+ .chats::-webkit-scrollbar{width:6px}.chats::-webkit-scrollbar-thumb{background:#374248;border-radius:6px}
51
+ .row{display:flex;gap:13px;align-items:center;padding:9px 14px;cursor:pointer;position:relative}
52
+ .row:after{content:"";position:absolute;left:78px;right:0;bottom:0;height:1px;background:#1c262d}
53
+ .row:hover{background:#202c33}.row.on{background:#2a3942}.row.on:after{background:transparent}
54
+ .av{position:relative;flex:0 0 auto;border-radius:50%;overflow:hidden}
55
+ .av img{width:100%;height:100%;object-fit:cover;display:block}
56
+ .row .b{min-width:0;flex:1 1 auto}
57
+ .row .n{color:#e9edef;font-size:16px;font-weight:400;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center;gap:6px}
58
+ .row .p{color:#8696a0;font-size:13.5px;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center;gap:3px}
59
+ .row .rc{color:#53bdeb}
60
+ .row .m{margin-left:auto;text-align:right;display:flex;flex-direction:column;align-items:flex-end;gap:6px;flex:0 0 auto}
61
+ .row .t{color:#8696a0;font-size:12px}.row.unread .t{color:#00d95f;font-weight:500}
62
+ .row .pin{color:#f15c6d;font-size:13px;transform:rotate(45deg)}
63
+ .netdot{width:16px;height:16px;border-radius:4px;object-fit:cover;flex:0 0 auto}
64
+ .main{position:relative;display:flex;flex-direction:column;min-width:0;background:#0b141a}
65
+ .wp{position:absolute;inset:0;background:#0b141a center/cover no-repeat;z-index:0}
66
+ .wp:after{content:"";position:absolute;inset:0;background:linear-gradient(rgba(11,20,26,.35),rgba(11,20,26,.5))}
67
+ .mh,.thread,.composer,.e2ewrap{position:relative;z-index:1}
68
+ .mh{height:60px;flex:0 0 auto;background:#202c33;display:flex;align-items:center;gap:14px;padding:0 16px}
69
+ .mh .who .n{font-size:16px;color:#e9edef;font-weight:500}
70
+ .mh .who .s{font-size:13px;color:#00a884;margin-top:1px}
71
+ .mh .sp{margin-left:auto}.mh .icons{display:flex;gap:2px}
72
+ .ico{width:40px;height:40px;border-radius:50%;display:grid;place-items:center;color:#aebac1;cursor:pointer;transition:background .12s;background:transparent;border:0}
73
+ .ico:hover{background:#ffffff14;color:#e9edef}
74
+ .ico.sk{color:#00d95f}
75
+ .e2ewrap{display:flex;justify-content:center;padding:14px 0 2px}
76
+ .e2e{background:#182229;color:#ffd279;font-size:12.5px;padding:6px 14px;border-radius:8px;box-shadow:0 1px .5px #0003}
77
+ .thread{flex:1 1 auto;overflow-y:auto;overflow-x:hidden;padding:10px 8% 14px;display:flex;flex-direction:column;gap:3px}
78
+ .thread::-webkit-scrollbar{width:6px}.thread::-webkit-scrollbar-thumb{background:#374248cc;border-radius:6px}
79
+ .more{align-self:center;color:#8696a0;font-size:12px;padding:8px;opacity:.85}
80
+ .day{align-self:center;background:#182229;color:#8696a0;font-size:12.5px;padding:5px 12px;border-radius:8px;margin:8px 0 6px;box-shadow:0 1px .5px #0003}
81
+ .bub{max-width:65%;padding:6px 9px 8px 10px;border-radius:8px;font-size:14.2px;line-height:1.42;box-shadow:0 1px .5px #0003;word-wrap:break-word;white-space:pre-wrap}
82
+ .in{align-self:flex-start;background:#202c33;border-top-left-radius:0}
83
+ .out{align-self:flex-end;background:#005c4b;border-top-right-radius:0}
84
+ .grp{margin-top:8px}
85
+ .bub .tt{font-size:11px;color:#ffffff8a;float:right;margin:6px 0 -3px 12px;position:relative;top:4px}
86
+ .bub .tt .rc{color:#53bdeb}
87
+ .empty{margin:auto;color:#8696a0;text-align:center;background:#182229;padding:8px 16px;border-radius:8px}
88
+ .composer{flex:0 0 auto;background:#202c33;display:flex;align-items:center;gap:6px;padding:8px 14px}
89
+ .composer .grow{flex:1 1 auto}
90
+ .send{width:44px;height:44px;border-radius:50%;background:#00a884;color:#0b141a;border:0;display:grid;place-items:center;cursor:pointer;flex:0 0 auto}
91
+ /* media */
92
+ .m-img{display:block;max-width:320px;max-height:340px;border-radius:6px;cursor:pointer;object-fit:cover}
93
+ .m-vid{position:relative;max-width:320px;border-radius:6px;overflow:hidden;cursor:pointer}
94
+ .m-vid img{display:block;width:100%;border-radius:6px}
95
+ .m-play{position:absolute;inset:0;display:grid;place-items:center}
96
+ .m-play span{width:48px;height:48px;border-radius:50%;background:#0009;color:#fff;display:grid;place-items:center;font-size:20px}
97
+ .m-file{display:flex;align-items:center;gap:10px;background:#ffffff0f;border-radius:8px;padding:9px 11px;min-width:200px;cursor:pointer}
98
+ .m-file .fi{width:38px;height:38px;border-radius:8px;background:#00a88433;color:#7cf0c8;display:grid;place-items:center;font-size:16px;flex:0 0 auto}
99
+ .m-file .fn{font-size:13.5px;color:#e9edef;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
100
+ .m-file .fs{font-size:12px;color:#8696a0;margin-top:2px}
101
+ .m-aud{display:flex;align-items:center;gap:10px;min-width:220px;padding:2px 0}
102
+ .m-aud audio{height:34px}
103
+ .lb{position:fixed;inset:0;background:#000c;z-index:60;display:grid;place-items:center;cursor:zoom-out}
104
+ .lb img,.lb video{max-width:90vw;max-height:90vh;border-radius:8px}
105
+ .lb .x{position:absolute;top:18px;right:22px;color:#fff;font-size:28px;cursor:pointer;background:0;border:0}
106
+ /* attach tray + pay sheet */
107
+ .tray{position:absolute;bottom:66px;left:14px;background:#233138;border-radius:12px;box-shadow:0 10px 34px #0009;padding:6px;z-index:6;min-width:236px}
108
+ .tray button{display:flex;align-items:center;gap:11px;width:100%;background:0;border:0;color:#e9edef;font-size:14.5px;padding:10px 12px;border-radius:8px;cursor:pointer;text-align:left}
109
+ .tray button:hover{background:#2a3942}.tray button:disabled{opacity:.5}
110
+ .tray .sep{height:1px;background:#2a3942;margin:4px 6px}
111
+ .paywrap{position:fixed;inset:0;background:#000a;z-index:60;display:grid;place-items:center}
112
+ .pay{background:#111b21;border-radius:14px;padding:20px;width:346px;max-width:90vw;box-shadow:0 20px 60px #000a}
113
+ .pay h3{margin:0 0 4px;font-size:17px}.pay .psub{color:#8696a0;font-size:13px;margin-bottom:14px}
114
+ .pay .seg{display:flex;background:#202c33;border-radius:10px;padding:3px;margin-bottom:14px}
115
+ .pay .seg button{flex:1;background:0;border:0;color:#8696a0;padding:8px;border-radius:8px;cursor:pointer;font-size:14px}
116
+ .pay .seg button.on{background:#00a884;color:#0b141a;font-weight:600}
117
+ .pay .amt{width:100%;background:#202c33;border:0;border-radius:10px;color:#e9edef;font-size:26px;text-align:center;padding:12px;margin-bottom:10px;outline:0}
118
+ .pay .chips{display:flex;gap:8px;margin-bottom:12px}
119
+ .pay .chips button{flex:1;background:#202c33;border:0;color:#e9edef;border-radius:8px;padding:9px;cursor:pointer}
120
+ .pay .chips button.on{background:#0b3b2e;color:#00d95f}
121
+ .pay .memo{width:100%;background:#202c33;border:0;border-radius:10px;color:#e9edef;padding:10px 12px;margin-bottom:12px;font-size:14px;outline:0}
122
+ .pay .err{color:#f15c6d;font-size:13px;margin-bottom:8px;text-align:center}
123
+ .pay .go{width:100%;background:#00a884;color:#0b141a;border:0;border-radius:10px;padding:12px;font-weight:600;cursor:pointer;font-size:15px}
124
+ .pay .go:disabled{opacity:.5;cursor:default}
125
+ .pay .cancel{width:100%;background:0;color:#8696a0;border:0;padding:10px;margin-top:6px;cursor:pointer}
126
+ `;
127
+
128
+ async function ensureImportmap() {
129
+ if (document.querySelector('script[type="importmap"][data-holo-ui]')) return;
130
+ const map = await (await fetch("/apps/ui/vendor/importmap.json")).json();
131
+ const imports = {}; for (const [k, v] of Object.entries(map.imports || map)) imports[k] = typeof v === "string" ? v.replace(/^\.\//, "/apps/ui/") : v;
132
+ const s = document.createElement("script"); s.type = "importmap"; s.setAttribute("data-holo-ui", "1"); s.textContent = JSON.stringify({ imports }); document.head.appendChild(s);
133
+ }
134
+ let _deps = null;
135
+ async function deps() {
136
+ if (_deps) return _deps;
137
+ await ensureImportmap();
138
+ const React = await import("react");
139
+ const { createRoot } = await import("react-dom/client");
140
+ const reg = await (await fetch("/apps/ui/registry/index.json")).json();
141
+ const K = Object.fromEntries(reg.components.map((c) => [c.name, c.holo]));
142
+ const [button, badge, avatar, input] = await Promise.all(["button", "badge", "avatar", "input"].map((n) => import(K[n])));
143
+ return (_deps = { React, createRoot, C: { button, badge, avatar, input } });
144
+ }
145
+ if (!document.getElementById("wa-shadcn-css")) { const st = document.createElement("style"); st.id = "wa-shadcn-css"; st.textContent = CSS; document.head.appendChild(st); }
146
+
147
+ const initials = (s) => (String(s || "?").trim().split(/\s+/).map((w) => w[0]).join("").slice(0, 2) || "?").toUpperCase();
148
+ const hue = (s) => { let h = 0; s = String(s || ""); for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; return `hsl(${h % 360} 40% 46%)`; };
149
+ const isOut = (m) => !!(m && (m.mine || m.out || m.fromMe || m.dir === "out" || m.from === "me" || m.self));
150
+ const threadOf = (model, id) => { try { if (id && model.thread) return model.thread(id) || []; } catch {} return (model.threads && model.threads[id]) || []; };
151
+ const rcMark = (m) => m.status === "read" || m.status === "delivered" ? "✓✓" : m.status === "sent" ? "✓" : "";
152
+ const slugOf = (net) => { const k = String(net || "").toLowerCase(); return NET_SLUG[k] === undefined ? k : NET_SLUG[k]; };
153
+ const mimeKind = (mime, kind) => { const k = String(kind || "").toLowerCase(); if (["image", "video", "audio", "voice"].includes(k)) return k === "voice" ? "audio" : k; const t = String(mime || "").split("/")[0]; if (t === "image" || t === "video" || t === "audio") return t; return "file"; };
154
+ const humanSize = (n) => { n = +n || 0; if (!n) return ""; const u = ["B", "KB", "MB", "GB"]; let i = 0; while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; } return (n < 10 && i ? n.toFixed(1) : Math.round(n)) + " " + u[i]; };
155
+ const fileGlyph = (mime, name) => { const m = String(mime || ""), e = String(name || "").split(".").pop().toLowerCase(); if (m.includes("pdf") || e === "pdf") return "📕"; if (["zip", "rar", "7z"].includes(e)) return "🗜"; if (["doc", "docx"].includes(e)) return "📘"; if (["xls", "xlsx", "csv"].includes(e)) return "📗"; return "📄"; };
156
+
157
+ function buildApp({ React, C }) {
158
+ const { createElement: h, memo, useState, useRef, useEffect, useCallback, useLayoutEffect } = React;
159
+ const Avatar = C.avatar.Avatar, AvatarImage = C.avatar.AvatarImage, AvatarFallback = C.avatar.AvatarFallback;
160
+ const Button = C.button.Button, Input = C.input.Input, Badge = C.badge.Badge;
161
+
162
+ // exact SVG line icons (from the real app's IC set)
163
+ const svg = (kids, s = 22, w = 1.8) => h("svg", { viewBox: "0 0 24 24", width: s, height: s, fill: "none", stroke: "currentColor", strokeWidth: w, strokeLinecap: "round", strokeLinejoin: "round" }, ...kids);
164
+ const P = (d, e) => h("path", { d, ...e });
165
+ const CIR = (cx, cy, r, e) => h("circle", { cx, cy, r, ...e });
166
+ const RC = (x, y, width, height, rx, e) => h("rect", { x, y, width, height, rx, ...e });
167
+ const fillC = { fill: "currentColor", stroke: "none" };
168
+ const IC = {
169
+ chats: () => svg([P("M20 11.4A7.6 7.6 0 0 1 8.9 18.2L4.5 19.4l1.2-4.2A7.6 7.6 0 1 1 20 11.4Z")]),
170
+ search: (s) => svg([CIR(11, 11, 7), P("m20 20-3.6-3.6")], s || 19),
171
+ phone: () => svg([P("M6.6 4H4.5A1.5 1.5 0 0 0 3 5.6 16 16 0 0 0 18.4 21a1.5 1.5 0 0 0 1.6-1.5v-2.1a1.5 1.5 0 0 0-1.2-1.45l-2.2-.45a1.5 1.5 0 0 0-1.5.6l-.5.7a12 12 0 0 1-5.1-5.1l.7-.5a1.5 1.5 0 0 0 .6-1.5l-.45-2.2A1.5 1.5 0 0 0 6.6 4Z")]),
172
+ video: () => svg([RC(3, 6.5, 12, 11, 2.5), P("m15 10.5 5.5-3v9l-5.5-3Z")]),
173
+ menu: () => svg([CIR(12, 5, 1.35, fillC), CIR(12, 12, 1.35, fillC), CIR(12, 19, 1.35, fillC)]),
174
+ newchat: () => svg([P("M13.5 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.5"), P("M18.4 3.6a2 2 0 0 1 2.8 2.8L13 14.6l-3.4.9.9-3.4 7.9-8.5Z")]),
175
+ emoji: () => svg([CIR(12, 12, 8.5), P("M8.8 14.2a4.2 4.2 0 0 0 6.4 0"), CIR(9, 10, 1, fillC), CIR(15, 10, 1, fillC)]),
176
+ plus: () => svg([P("M12 5.5v13M5.5 12h13")]),
177
+ mic: () => svg([RC(9, 3, 6, 11, 3), P("M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21M8.5 21h7")]),
178
+ send: () => svg([P("M4.5 12 20.5 4.5 13 20.5l-2.8-6.4L4.5 12Z")]),
179
+ spark: (s) => svg([P("M12 3.2l1.7 4.6 4.6 1.7-4.6 1.7L12 15.8l-1.7-4.6L5.7 9.5l4.6-1.7L12 3.2Z"), P("M5.5 15.5l.8 2 2 .8-2 .8-.8 2-.8-2-2-.8 2-.8.8-2Z")], s || 20),
180
+ sun: () => svg([CIR(12, 12, 4), P("M12 2v2M12 20v2M4 12H2M22 12h-2M5.6 5.6 4.2 4.2M19.8 19.8l-1.4-1.4M18.4 5.6l1.4-1.4M4.2 19.8l1.4-1.4")]),
181
+ plusThin: () => svg([P("M12 5.5v13M5.5 12h13")], 26, 1.6),
182
+ };
183
+
184
+ const Av = (name, size, bg, src) => h("div", { className: "av", style: { height: size + "px", width: size + "px" } },
185
+ h(Avatar, { style: { height: "100%", width: "100%", background: bg || hue(name), display: "grid", placeItems: "center" } },
186
+ src ? h(AvatarImage, { src, alt: "", style: { height: "100%", width: "100%", objectFit: "cover" } }) : null,
187
+ h(AvatarFallback, { style: { background: bg || hue(name), color: "#fff", fontSize: (size * 0.36) + "px", fontWeight: 500, height: "100%", width: "100%", display: "grid", placeItems: "center" } }, initials(name))));
188
+ const Ico = (icon, onClick, title, cls) => h("button", { className: "ico" + (cls ? " " + cls : ""), onClick, title }, icon);
189
+ const netLogo = (net) => { const s = slugOf(net); return s ? h("img", { className: "netdot", src: BRANDS + s + ".svg", alt: "", onError: (e) => { e.currentTarget.style.display = "none"; } }) : null; };
190
+
191
+ // rich media in a bubble — image/video/audio/file, with lazy bridge-media resolution
192
+ function MediaView({ media, model, onMedia }) {
193
+ const [url, setUrl] = useState(media.url || media.thumb || "");
194
+ const bucket = mimeKind(media.mime, media.kind);
195
+ useEffect(() => { let alive = true;
196
+ if (!media.url && (media.lazy || media.pending || media.ref) && model.resolveBridgeMedia) {
197
+ Promise.resolve(model.resolveBridgeMedia(media)).then((u) => { if (alive && u) setUrl(typeof u === "string" ? u : (u.url || u.src || "")); }).catch(() => {});
198
+ } return () => { alive = false; }; }, []);
199
+ if (bucket === "image") return url ? h("img", { className: "m-img", src: url, alt: "", loading: "lazy", onClick: () => onMedia({ kind: "image", url }) }) : h("div", { className: "m-file" }, "🖼 Photo");
200
+ if (bucket === "video") return h("div", { className: "m-vid", onClick: () => onMedia({ kind: "video", url }) }, h("img", { src: media.thumb || url, alt: "" }), h("div", { className: "m-play" }, h("span", {}, "▶")));
201
+ if (bucket === "audio") return h("div", { className: "m-aud" }, url ? h("audio", { controls: true, src: url }) : h("span", {}, "🎤 Voice message"));
202
+ return h("div", { className: "m-file", onClick: () => url && window.open(url, "_blank") }, h("div", { className: "fi" }, fileGlyph(media.mime, media.filename)), h("div", { style: { minWidth: 0 } }, h("div", { className: "fn" }, media.filename || "File"), media.size ? h("div", { className: "fs" }, humanSize(media.size)) : null));
203
+ }
204
+
205
+ const Rail = memo(function Rail({ nets, active, totalUnread, onPick }) {
206
+ return h("nav", { className: "rail" },
207
+ h("button", { className: "rb" + (active === "All" ? " on" : ""), title: "All chats", onClick: () => onPick("All") }, IC.chats(), totalUnread ? h("span", { className: "bdg" }, totalUnread > 99 ? "99+" : totalUnread) : null),
208
+ nets.length ? h("div", { className: "rsep" }) : null,
209
+ nets.map((n) => h("button", { key: n.id, className: "rb" + (active === n.id ? " on" : ""), title: n.label, onClick: () => onPick(n.id) },
210
+ (() => { const s = slugOf(n.id); return s ? h("img", { src: BRANDS + s + ".svg", alt: n.label, onError: (e) => { const t = document.createTextNode((n.label || "?")[0].toUpperCase()); try { e.currentTarget.replaceWith(t); } catch {} } }) : (n.label || "?")[0].toUpperCase(); })(),
211
+ h("span", { className: "dot" }), n.unread ? h("span", { className: "bdg" }, n.unread > 99 ? "99+" : n.unread) : null)),
212
+ h("button", { className: "rb", title: "Add network", onClick: () => onPick("+") }, IC.plusThin()),
213
+ h("div", { className: "rgrow" }),
214
+ h("button", { className: "rb", title: "Theme" }, IC.sun()),
215
+ h("div", { className: "rav", title: "Your profile" }));
216
+ });
217
+
218
+ const Row = memo(function Row({ c, on, preview, receipt, onSelect }) {
219
+ return h("div", { className: "row" + (on ? " on" : "") + (c.unread ? " unread" : ""), onClick: () => onSelect(c.id) },
220
+ Av(c.name, 49, c.isQ ? "#12b886" : hue(c.name), c.avatar),
221
+ h("div", { className: "b" },
222
+ h("div", { className: "n" }, c.name || c.id || "Chat", c.network ? netLogo(c.network) : null),
223
+ h("div", { className: "p" }, receipt ? h("span", { className: "rc" }, receipt + " ") : null, preview)),
224
+ h("div", { className: "m" },
225
+ h("div", { className: "t" }, c.time || c.ts || ""),
226
+ c.pinned ? h("span", { className: "pin" }, "📌") : null,
227
+ c.unread ? h(Badge, { style: { background: "#00a884", color: "#0b141a", borderRadius: "999px", height: "20px", minWidth: "20px", justifyContent: "center", padding: "0 6px", fontSize: "12px", fontWeight: 600 } }, String(c.unread)) : null));
228
+ });
229
+
230
+ function Thread({ model, activeId, onMedia }) {
231
+ const all = threadOf(model, activeId);
232
+ const [limit, setLimit] = useState(WINDOW);
233
+ const ref = useRef(null), prevId = useRef(activeId), anchorH = useRef(0), pin = useRef(true);
234
+ const toBottom = (el) => { el.scrollTop = el.scrollHeight; };
235
+ useEffect(() => { setLimit(WINDOW); }, [activeId]);
236
+ // Runs after every render (no deps): a fresh chat, or new messages arriving, both flow through here.
237
+ // While "pinned" we stay glued to the newest message — exactly like WhatsApp.
238
+ useLayoutEffect(() => { const el = ref.current; if (!el) return;
239
+ if (prevId.current !== activeId) { prevId.current = activeId; pin.current = true; anchorH.current = 0; toBottom(el); return; }
240
+ if (anchorH.current) { el.scrollTop = el.scrollHeight - anchorH.current; anchorH.current = 0; return; }
241
+ if (pin.current) toBottom(el); });
242
+ // Media resolves async (placeholder first, <img> swapped in later) and reflows the thread AFTER layout.
243
+ // A ResizeObserver on the scroll container won't catch it (its own box size never changes), and per-image
244
+ // listeners miss images added later — so listen for `load` in the CAPTURE phase on the container, which
245
+ // fires for ANY descendant image whenever it appears/loads. Keeps us glued to the newest message.
246
+ useEffect(() => { const el = ref.current; if (!el) return; pin.current = true; toBottom(el);
247
+ requestAnimationFrame(() => { if (pin.current && ref.current) toBottom(ref.current); });
248
+ const stick = () => { if (pin.current && ref.current) toBottom(ref.current); };
249
+ el.addEventListener("load", stick, true); el.addEventListener("error", stick, true);
250
+ return () => { el.removeEventListener("load", stick, true); el.removeEventListener("error", stick, true); };
251
+ }, [activeId]);
252
+ // Detach when the user scrolls up; re-attach ("jump to latest") once they're back near the bottom.
253
+ const onScroll = useCallback((e) => { const el = e.currentTarget;
254
+ pin.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
255
+ if (el.scrollTop < 120 && limit < all.length) { anchorH.current = el.scrollHeight; setLimit((l) => Math.min(all.length, l + WINDOW)); } }, [limit, all.length]);
256
+ const start = Math.max(0, all.length - limit), shown = all.slice(start);
257
+ if (!all.length) return h("div", { className: "thread", ref }, h("div", { className: "empty" }, "No messages yet. Say hello 👋"));
258
+ const rows = [h("div", { className: "e2ewrap", key: "e2e" }, h("div", { className: "e2e" }, "🔒 Messages are end-to-end encrypted")), h("div", { className: "day", key: "day" }, "Today")];
259
+ if (start > 0) rows.push(h("div", { className: "more", key: "more" }, `↑ ${start.toLocaleString()} earlier — scroll up`));
260
+ let lastOut = null;
261
+ shown.forEach((m, i) => { const out = isOut(m); const hasMedia = m.media && (m.media.url || m.media.thumb || m.media.ref || m.media.lazy || m.media.kind);
262
+ rows.push(h("div", { key: start + i, className: "bub " + (out ? "out" : "in") + (out !== lastOut ? " grp" : ""), style: hasMedia ? { maxWidth: "min(360px,72%)" } : null },
263
+ hasMedia ? h(MediaView, { media: m.media, model, onMedia }) : null,
264
+ m.text ? h("div", { style: hasMedia ? { marginTop: "5px" } : null }, m.text) : null,
265
+ h("span", { className: "tt" }, (m.time || m.ts || ""), out ? h("span", { className: "rc" }, " " + rcMark(m)) : null)));
266
+ lastOut = out; });
267
+ return h("div", { className: "thread", ref, onScroll }, rows);
268
+ }
269
+
270
+ const Composer = memo(function Composer({ activeId, onSend, onPlus }) {
271
+ const [draft, setDraft] = useState("");
272
+ const send = () => { const t = draft.trim(); if (!t) return; onSend(activeId, t); setDraft(""); };
273
+ return h("div", { className: "composer" },
274
+ Ico(IC.plus(), onPlus, "Attach"), Ico(IC.emoji(), null, "Emoji"),
275
+ h("div", { className: "grow" }, h(Input, { placeholder: "Type a message", value: draft, autoFocus: true,
276
+ onChange: (e) => setDraft(e.target.value), onKeyDown: (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } },
277
+ style: { background: "#2a3942", border: "0", borderRadius: "10px", height: "44px", color: "#e9edef", paddingLeft: "16px" } })),
278
+ h("button", { className: "send", onClick: send, "aria-label": "Send" }, draft.trim() ? IC.send() : IC.mic()));
279
+ });
280
+
281
+ const TrayBtn = (glyph, label, onClick, disabled) => h("button", { onClick, disabled }, h("span", { style: { fontSize: "17px" } }, glyph), label);
282
+
283
+ function PaySheet({ model, activeId, conv, sheet, setSheet }) {
284
+ const set = (patch) => setSheet((s) => ({ ...s, ...patch, err: null }));
285
+ const amt = parseFloat(sheet.amount);
286
+ const go = async () => { set({ busy: true }); let r;
287
+ try { r = await model.holoPay(sheet.genesis || activeId, { kind: sheet.kind, amount: amt, memo: (sheet.memo || "").trim() }); } catch (e) { r = { ok: false, error: String(e && e.message || e) }; }
288
+ if (r && r.ok) setSheet(null); else setSheet((s) => ({ ...s, busy: false, err: (r && r.error) || "Couldn't create the payment" })); };
289
+ return h("div", { className: "paywrap", onClick: () => setSheet(null) },
290
+ h("div", { className: "pay", onClick: (e) => e.stopPropagation() },
291
+ h("h3", {}, sheet.kind === "send" ? "Send money" : "Request money"), h("div", { className: "psub" }, (sheet.kind === "send" ? "To " : "From ") + (conv.name || "this chat")),
292
+ h("div", { className: "seg" }, h("button", { className: sheet.kind === "send" ? "on" : "", onClick: () => set({ kind: "send" }) }, "Send"), h("button", { className: sheet.kind === "request" ? "on" : "", onClick: () => set({ kind: "request" }) }, "Request")),
293
+ h("input", { className: "amt", inputMode: "decimal", placeholder: "$0", value: sheet.amount, onChange: (e) => set({ amount: e.target.value.replace(/[^0-9.]/g, "") }), autoFocus: true }),
294
+ h("div", { className: "chips" }, [10, 20, 50, 100].map((a) => h("button", { key: a, className: amt === a ? "on" : "", onClick: () => set({ amount: String(a) }) }, "$" + a))),
295
+ h("input", { className: "memo", placeholder: "What's it for? (optional)", maxLength: 120, value: sheet.memo || "", onChange: (e) => set({ memo: e.target.value }) }),
296
+ sheet.err ? h("div", { className: "err" }, sheet.err) : null,
297
+ h("button", { className: "go", disabled: sheet.busy || !(amt > 0), onClick: go }, sheet.busy ? (sheet.kind === "send" ? "Confirm in your wallet…" : "Creating…") : ((sheet.kind === "send" ? "Send" : "Request") + (amt > 0 ? " $" + sheet.amount : " money"))),
298
+ h("button", { className: "cancel", onClick: () => setSheet(null) }, "Cancel")));
299
+ }
300
+
301
+ const previewOf = (model, c) => { if (c.preview) return c.preview; const t = threadOf(model, c.id); const last = t.length ? t[t.length - 1] : null; return (last && (last.text || (last.media ? "📎 media" : ""))) || ""; };
302
+
303
+ return function App({ model }) {
304
+ const convs = Array.isArray(model.conversations) ? model.conversations : [];
305
+ const [activeId, setActiveId] = useState(null);
306
+ const [filter, setFilter] = useState("All");
307
+ const [q, setQ] = useState("");
308
+ const [lb, setLb] = useState(null);
309
+ const [attachOpen, setAttachOpen] = useState(false);
310
+ const [paySheet, setPaySheet] = useState(null);
311
+ const fileRef = useRef(null);
312
+ const onSelect = useCallback((id) => { setActiveId(id); setAttachOpen(false); }, []);
313
+ const active = (activeId && convs.some((c) => c.id === activeId)) ? activeId : ((convs.find((c) => c.active) || convs[0] || {}).id || null);
314
+ const conv = convs.find((c) => c.id === active) || {};
315
+ const onSend = useCallback((id, text) => { try { model.onSend && model.onSend(id, text); } catch (e) { console.error("[shadcn-ui] onSend", e); } }, [model]);
316
+ const call = useCallback((video) => { try { model.startCall && model.startCall(active, { video }); } catch (e) { console.error("[shadcn-ui] startCall", e); } }, [model, active]);
317
+ const pickFile = (accept) => { if (fileRef.current) { fileRef.current.accept = accept; fileRef.current.click(); } setAttachOpen(false); };
318
+ const onFile = (e) => { const f = e.target.files && e.target.files[0]; if (f) { try { model.onAttach && model.onAttach(active, f); } catch (er) { console.error("[shadcn-ui] onAttach", er); } } e.target.value = ""; };
319
+ const tryM = (fn, ...a) => { setAttachOpen(false); try { const r = model[fn] && model[fn](...a); if (r && r.catch) r.catch(() => {}); } catch (e) { console.error("[shadcn-ui] " + fn, e); } };
320
+ const nets = Array.isArray(model.networks) && model.networks.length ? model.networks
321
+ : Object.values(convs.reduce((a, c) => { const id = (c.network || c.platform); if (id && !a[id]) a[id] = { id, label: id[0].toUpperCase() + id.slice(1), unread: 0 }; if (id) a[id].unread += (c.unread || 0); return a; }, {}));
322
+ const totalUnread = convs.reduce((n, c) => n + (c.unread || 0), 0);
323
+ const signalUnread = model.signalUnread != null ? model.signalUnread : convs.filter((c) => c.unread).length;
324
+ const qs = q.trim().toLowerCase();
325
+ const shown = convs.filter((c) => (filter === "All" || (c.network || c.platform) === filter) && (!qs || (c.name || "").toLowerCase().includes(qs) || (previewOf(model, c) || "").toLowerCase().includes(qs)));
326
+ const sub = conv.presence || (conv.isQ ? "online · on your device" : (conv.status || conv.network || conv.platform || ""));
327
+ const runSearch = () => { const t = q.trim(); if (!t) return; try { if (model.qCommand) model.qCommand(t); else if (model.onNewChat) model.onNewChat(t); } catch {} };
328
+
329
+ return h("div", { className: "wa" },
330
+ h(Rail, { nets, active: filter, totalUnread, onPick: (id) => { if (id === "+") { try { model.openNetworks && model.openNetworks(); } catch {} } else setFilter(id); } }),
331
+ h("div", { className: "side" },
332
+ h("div", { className: "stitle" }, h("span", { className: "t" }, "Messenger"),
333
+ Ico(IC.spark(), () => { try { (model.askQ || model.qCommand) && (model.askQ ? model.askQ() : null); } catch {} }, "Ask Q", "sk"), Ico(IC.newchat(), () => { try { model.onNewChat && model.onNewChat(""); } catch {} }, "New chat"), Ico(IC.menu(), () => {}, "Menu")),
334
+ h("div", { className: "search" }, h("span", { className: "si" }, IC.search()),
335
+ h(Input, { placeholder: "Search, ask Q, or start a chat…", value: q, onChange: (e) => setQ(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") runSearch(); },
336
+ style: { background: "#202c33", border: "0", borderRadius: "10px", height: "42px", color: "#e9edef", paddingLeft: "40px", fontSize: "14.5px" } })),
337
+ h("div", { className: "filt" }, h("button", { className: "chip on" }, "All"), h("button", { className: "caret" }, "▾")),
338
+ signalUnread ? h("div", { className: "triage" }, h("span", { className: "sk" }, IC.spark(18)), `${signalUnread} need you · ~${Math.max(1, Math.round(signalUnread * 0.4))} min`) : null,
339
+ h("div", { className: "chats" }, shown.map((c) => h(Row, { key: c.id, c, on: c.id === active, preview: previewOf(model, c), receipt: c.receipt || "", onSelect })))),
340
+ h("div", { className: "main" },
341
+ h("div", { className: "wp", style: { backgroundImage: `url(${WP})` } }),
342
+ h("div", { className: "mh" }, Av(conv.name, 40, conv.isQ ? "#12b886" : hue(conv.name), conv.avatar),
343
+ h("div", { className: "who" }, h("div", { className: "n" }, conv.name || ""), sub ? h("div", { className: "s" }, sub) : null),
344
+ h("div", { className: "sp" }), h("div", { className: "icons" }, Ico(IC.video(), () => call(true), "Video call"), Ico(IC.phone(), () => call(false), "Voice call"), Ico(IC.menu(), () => { try { model.onFavourite && model.onFavourite(active); } catch {} }, "Menu"))),
345
+ h(Thread, { model, activeId: active, onMedia: setLb }),
346
+ h("input", { ref: fileRef, type: "file", style: { display: "none" }, onChange: onFile }),
347
+ attachOpen ? h("div", { className: "tray" },
348
+ TrayBtn("🖼", "Photos & videos", () => pickFile("image/*,video/*")),
349
+ TrayBtn("📄", "Document", () => pickFile("*/*")),
350
+ (conv.network && !conv.isQ && model.holoPay) ? TrayBtn("💸", "Send money", () => { setAttachOpen(false); setPaySheet({ kind: "send", amount: "", memo: "", busy: false, err: null }); }) : null,
351
+ h("div", { className: "sep" }),
352
+ model.startMeet ? TrayBtn("👥", "Start a meeting", () => tryM("startMeet", active, { video: true })) : null,
353
+ model.startTogether ? TrayBtn("🖥", "Share my screen", () => tryM("startTogether", active, { kind: "tab", title: "" })) : null,
354
+ model.startTogether ? TrayBtn("📝", "Co-edit a doc", () => tryM("startTogether", active, { kind: "doc", title: "" })) : null,
355
+ (conv.network && !conv.isQ && model.qDraft) ? [h("div", { className: "sep", key: "s" }), TrayBtn("✨", "Draft a reply with Q", () => tryM("qDraft", active))] : null) : null,
356
+ h(Composer, { key: active, activeId: active, onSend, onPlus: () => setAttachOpen((v) => !v) })),
357
+ lb ? h("div", { className: "lb", onClick: () => setLb(null) }, h("button", { className: "x", "aria-label": "Close" }, "✕"),
358
+ lb.kind === "video" ? h("video", { src: lb.url, controls: true, autoPlay: true, onClick: (e) => e.stopPropagation() }) : h("img", { src: lb.url, alt: "", onClick: (e) => e.stopPropagation() })) : null,
359
+ paySheet ? h(PaySheet, { model, activeId: active, conv, sheet: paySheet, setSheet: setPaySheet }) : null);
360
+ };
361
+ }
362
+
363
+ async function mount(el, model) {
364
+ const d = await deps();
365
+ const App = buildApp(d);
366
+ let cur = model;
367
+ const root = d.createRoot(el);
368
+ const render = (mdl) => { cur = mdl || cur; root.render(d.React.createElement(App, { model: cur })); };
369
+ render(model);
370
+ console.log("[shadcn-ui] Holo Messenger (Pass 1) mounted from κ-components ·", (model.conversations || []).length, "chats");
371
+ return { update: render, unmount: () => { try { root.unmount(); } catch {} } };
372
+ }
373
+
374
+ if (WANT && typeof window !== "undefined") {
375
+ const prev = window.HoloMessengerUI;
376
+ window.HoloMessengerUI = { mount, version: ((prev && prev.version) || "m1") + "+shadcn", _chatscope: prev };
377
+ console.log("[shadcn-ui] streaming-κ shadcn UI ACTIVE (opt out: ?ui=chatscope)");
378
+ }
379
+
380
+ export { mount };
b/c741295f6742e247afb4eabc8b1f7423a4171c56f8bc933aa7a696e8ef97756d ADDED
The diff for this file is too large to render. See raw diff
 
b/d80d218db7c66a0414c0c02204d0e0398249b2ec619f903d02d97781fef92102 ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en"><head>
3
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
4
+ <title>Holo Messenger</title>
5
+ <link rel="stylesheet" href="/apps/holo-messenger/_vendor/ui/chat-ui.bundle.css?v=f1ff491ce68b">
6
+ <style>html,body{height:100%;margin:0;background:#0b141a;color-scheme:dark}#root{height:100vh}</style>
7
+ <link rel="preload" as="image" href="/apps/holo-messenger/_vendor/wallpaper-default.jpg" fetchpriority="high">
8
+ </head><body>
9
+ <div id="root"></div>
10
+ <script type="module">
11
+ // ?v=N - bump on each deploy so the browser can never serve a stale cached bundle (the cause of a frozen/old build).
12
+ import "/apps/holo-messenger/_vendor/ui/chat-ui.bundle.js?v=e811350a91c0"; // → window.HoloMessengerUI
13
+ // Opt-in: render the WhatsApp UX from STREAMING shadcn κ-components instead of the chatscope bundle.
14
+ // Additive — only takes over window.HoloMessengerUI.mount when ?ui=shadcn (or localStorage["holo.ui.shadcn"]="1");
15
+ // the default boot stays the untouched chatscope UI. Runs before boot() so it is fed the same live model.
16
+ import "/apps/holo-messenger/messenger-shadcn-ui.mjs";
17
+ import { boot } from "/apps/holo-messenger/holo-messenger-app.mjs?v=076005621416";
18
+ boot(document.getElementById("root")).catch(e => {
19
+ document.body.innerHTML = "<pre style='color:#f88;padding:16px;white-space:pre-wrap'>boot error: " + (e && e.stack || e) + "</pre>";
20
+ });
21
+ // Intelligent Inbox (holo-mail) - additive, feature-flagged OFF by default. Dynamic import + catch so it
22
+ // can NEVER block or break the messenger boot. Enable per-operator: localStorage["holo.mail.enabled"]="1".
23
+ import("/apps/holo-messenger/mail/holo-mail-attach.mjs").catch(() => {});
24
+ // HoloChat, woven into THIS messenger — one canonical surface, no separate overlay. Cards render natively
25
+ // in the bubbles; the composer "+" opens the unified tray (apps · Spaces · money · identity · prove).
26
+ // Additive + catch-guarded so it can never block boot. Enable: localStorage["holo.chat.enabled"]="1" or ?chat=1.
27
+ import("/apps/holo-messenger/holo-messenger-weave.mjs").catch(() => {});
28
+ // Colour re-tone picker (Default · Graphite · Warm · Contrast · Light) — colours only, all features
29
+ // intact. Gated: shows only with ?skins=1 or localStorage["holo.skins"]="1". Additive + catch-guarded.
30
+ import("/apps/holo-messenger/messenger-skin.mjs").catch(() => {});
31
+ // Asset cache (service worker) — repeat opens serve wallpaper/logos/bundle/css/js NETWORK-FREE. Caches
32
+ // only own-origin static assets (stale-while-revalidate); all dynamic/API/bridge/media traffic passes
33
+ // through untouched. Additive + catch-guarded. Escape hatch: open with ?nosw=1 to unregister.
34
+ import("/apps/holo-messenger/messenger-sw-register.mjs").catch(() => {});
35
+ </script>
36
+ </body></html>
b/e811350a91c04a5999d552279aca78f833ec7946ae929e0af42575fb62c061c0 ADDED
The diff for this file is too large to render. See raw diff
 
b/ec9aea0c39ca06aa2001b22aef511d449b8e64c2c2b095ffedf46e2492f03b19 ADDED
The diff for this file is too large to render. See raw diff
 
b/f1ff491ce68b55a5a5d68a825e6a78dcb0ff53edd426f99c2b095573e93acd01 ADDED
The diff for this file is too large to render. See raw diff
 
b/f6caa9eeead2c99deeb2cdb70b7e25bbaa2cf5c60555f5b4030ecf6b6d9efa51 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f6caa9eeead2c99deeb2cdb70b7e25bbaa2cf5c60555f5b4030ecf6b6d9efa51
3
+ size 546613