wallet frame kappa-objects: theme/mobile css + teleport/theme js (fix half-shipped kappa-links)
Browse files- sha256/31365b0b2f83cc3ae69f1ab91477a7cbed4fd2ab00366a84d5aea9212db0d4dc +886 -0
- sha256/a0ebbc696bda5154727b76b42c11921231cbc652de3e705dd6b632456e6f34aa +143 -0
- sha256/b2bfa104b9ee8e3bb631dcd0ec0e6f7017b8898c4304666f5b3dfeeebc926eb9 +127 -0
- sha256/c4414e9c61a6ae6b8c19145dddbbe24c68bea83784666434a94cf033b06cc483 +384 -0
sha256/31365b0b2f83cc3ae69f1ab91477a7cbed4fd2ab00366a84d5aea9212db0d4dc
ADDED
|
@@ -0,0 +1,886 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Holo Theme (holo-theme.js) β the Hologram OS theme runtime. Zero dependencies,
|
| 2 |
+
// content-addressable. A self-contained module: call it once and it brings its own
|
| 3 |
+
// stylesheet β <script src="β¦/holo-theme.js"></script> is the whole adoption.
|
| 4 |
+
// The lean OS service the brief calls a "unified runtime theme engine": it resolves the
|
| 5 |
+
// initial theme, applies it, persists the user's choice, and propagates live changes
|
| 6 |
+
// across the whole nested-holospace tree β instantly, no rebuild, no restart.
|
| 7 |
+
//
|
| 8 |
+
// W3C-native end to end: state is expressed only as CSS custom properties + the
|
| 9 |
+
// color-scheme/light-dark() palette attribute (holo-theme.css); transport is the HTML
|
| 10 |
+
// Standard's postMessage; the theme's identity is a ΞΊ artifact (ADR-022).
|
| 11 |
+
//
|
| 12 |
+
// Precedence model (see THEMING-W3C-ADR.md):
|
| 13 |
+
// β’ USER accessibility (font scale, density, motion) β always wins. Applied here as
|
| 14 |
+
// INLINE custom properties on :root, so it beats app stylesheets. enforce-os adds
|
| 15 |
+
// !important so it beats app !important too.
|
| 16 |
+
// β’ App vs OS aesthetics β negotiated. An app declares <meta name="holo-theme-policy"
|
| 17 |
+
// content="own"> to keep its colors; default "adopt" lets it follow the OS palette
|
| 18 |
+
// (apps that use the --holo-* tokens follow automatically; apps with private vars
|
| 19 |
+
// resist until migrated β an honest limit, like canvas/VM surfaces).
|
| 20 |
+
//
|
| 21 |
+
// API (window.HoloTheme):
|
| 22 |
+
// .get() β current state
|
| 23 |
+
// .setPalette("auto"|"light"|"dark")
|
| 24 |
+
// .setPresentation("standard"|"immersive")
|
| 25 |
+
// .setFontScale(n) β 0.85 β¦ 1.4 (Settings βΊ Display βΊ Text size)
|
| 26 |
+
// .setFontFamily(css-font-list) β UI typeface (Settings βΊ Display βΊ Typeface)
|
| 27 |
+
// .setDensity(n) / .setAccent(c) / .reset()
|
| 28 |
+
// .openSettings() β the built-in Display settings panel (any surface)
|
| 29 |
+
|
| 30 |
+
(function () {
|
| 31 |
+
"use strict";
|
| 32 |
+
if (window.HoloTheme) return;
|
| 33 |
+
|
| 34 |
+
// Bundled: bring our own stylesheet so adopting Holo Theme is a single <script> call.
|
| 35 |
+
// A page may still pre-link holo-theme.css in <head> to avoid any flash β we skip
|
| 36 |
+
// injection if it is already present.
|
| 37 |
+
(function injectCss() {
|
| 38 |
+
try {
|
| 39 |
+
if (document.querySelector('link[href*="holo-theme.css"]')) return;
|
| 40 |
+
var src = (document.currentScript && document.currentScript.src) || "";
|
| 41 |
+
var href = /\/\.holo\/sha256\//.test(src) ? "/_shared/holo-theme.css" : src ? src.replace(/holo-theme\.js(\?.*)?$/, "holo-theme.css") : "_shared/holo-theme.css";
|
| 42 |
+
var l = document.createElement("link"); l.rel = "stylesheet"; l.href = href;
|
| 43 |
+
(document.head || document.documentElement).appendChild(l);
|
| 44 |
+
} catch (e) {}
|
| 45 |
+
})();
|
| 46 |
+
|
| 47 |
+
// Resolve sibling shared files (to lazy-load the DTCG format module on demand).
|
| 48 |
+
var SELF = (document.currentScript && document.currentScript.src) || "";
|
| 49 |
+
// ΞΊ-routed (src is /.holo/sha256/<hex>.js): siblings can't be path-resolved β use the canonical _shared mount.
|
| 50 |
+
var SHARED = /\/\.holo\/sha256\//.test(SELF) ? "/_shared/" : SELF ? SELF.replace(/holo-theme\.js(\?.*)?$/, "") : "_shared/";
|
| 51 |
+
|
| 52 |
+
// Bootstrap the Holo UX experience layer (ADR-028): device-tier resolution, golden-ratio
|
| 53 |
+
// proportion (holo-ux.js self-injects holo-phi.css), and propagation down the holospace tree.
|
| 54 |
+
// Loading it from the theme runtime β which every Holo Theme / Holo UI citizen already loads
|
| 55 |
+
// (the kernel injects it too) β makes every citizen a Holo UX citizen with NO per-app script
|
| 56 |
+
// tag: one canonical wire (Law L2). Idempotent + additive (holo-ux.js guards its own effects).
|
| 57 |
+
(function bootHoloUx() {
|
| 58 |
+
try {
|
| 59 |
+
if (window.HoloUX || document.querySelector('script[src*="holo-ux.js"]')) return;
|
| 60 |
+
var s = document.createElement("script"); s.type = "module"; s.src = SHARED + "holo-ux.js";
|
| 61 |
+
(document.head || document.documentElement).appendChild(s);
|
| 62 |
+
} catch (e) {}
|
| 63 |
+
})();
|
| 64 |
+
|
| 65 |
+
// Bootstrap the Holo FX motion engine (the OS's faithful adoption of unicode-animations):
|
| 66 |
+
// window.HoloFX β one sharp, content-addressed loading vocabulary (braille Β· scan Β· dna Β·
|
| 67 |
+
// cascade) for every surface. Same canonical wire as Holo UX (Law L2): every Holo Theme
|
| 68 |
+
// citizen gets it with NO per-app script tag. Classic script (sets window.HoloFX), idempotent.
|
| 69 |
+
(function bootHoloFx() {
|
| 70 |
+
try {
|
| 71 |
+
if (window.HoloFX || document.querySelector('script[src*="holo-fx.js"]')) return;
|
| 72 |
+
var s = document.createElement("script"); s.src = SHARED + "holo-fx.js";
|
| 73 |
+
(document.head || document.documentElement).appendChild(s);
|
| 74 |
+
} catch (e) {}
|
| 75 |
+
})();
|
| 76 |
+
|
| 77 |
+
// Bootstrap the canonical ΞΊβrender path (window.HoloRender): the ONE lean, low-latency way every app
|
| 78 |
+
// displays a content-addressed UOR object β resolve by ΞΊ (arena-cached), verify by re-derivation, mount
|
| 79 |
+
// its ORIGINAL bytes (no compiler on the hot path). Same canonical wire as Holo UX / FX / Telemetry (Law
|
| 80 |
+
// L2): every Holo Theme citizen gets it with NO per-app script tag. The renderer rides the substrate's
|
| 81 |
+
// SINGLE resolver (resolveByKappa) + ΞΊ-route when present, else its own re-derive fallback (same L5 law).
|
| 82 |
+
(function bootHoloRender() {
|
| 83 |
+
try {
|
| 84 |
+
if (window.HoloRender || window.__holoRenderBoot) return;
|
| 85 |
+
window.__holoRenderBoot = 1;
|
| 86 |
+
var code =
|
| 87 |
+
'import HoloRender from "' + SHARED + 'holo-render.js";\n' +
|
| 88 |
+
'const route = (h) => "/.holo/sha256/" + h;\n' +
|
| 89 |
+
'let resolver;\n' +
|
| 90 |
+
'try { const m = await import("/holo-resolver.mjs"); const store = new Map();\n' +
|
| 91 |
+
' const src = async (k) => { try { const r = await fetch(route(String(k).split(":").pop())); return r.ok ? new Uint8Array(await r.arrayBuffer()) : null; } catch (e) { return null; } };\n' +
|
| 92 |
+
' resolver = (k) => m.resolveByKappa(k, [src], store); } catch (e) {}\n' +
|
| 93 |
+
'try { await HoloRender.configure({ base: "/", route, resolver }); window.HoloRender = HoloRender;\n' +
|
| 94 |
+
' (document.documentElement || document).dispatchEvent(new Event("holo-render-ready")); } catch (e) { console.warn("holo-render boot:", e); }\n';
|
| 95 |
+
var s = document.createElement("script"); s.type = "module"; s.textContent = code;
|
| 96 |
+
(document.head || document.documentElement).appendChild(s);
|
| 97 |
+
} catch (e) {}
|
| 98 |
+
})();
|
| 99 |
+
|
| 100 |
+
// Clean chrome (Law L2, one universal wire): the OS injects a row of per-app floating "chrome" tiles
|
| 101 |
+
// (Open Holo UI Β· capture-a-thought Β· go-live Β· capture-screen Β· manage). They clutter every app's
|
| 102 |
+
// surface; remove them OS-wide for the cleanest UX. One CSS rule applies in every app that loads the
|
| 103 |
+
// engine β no per-app edit. These actions remain reachable from the shell/dock, not over each app.
|
| 104 |
+
(function bootCleanChrome() {
|
| 105 |
+
try {
|
| 106 |
+
if (document.getElementById("holo-clean-chrome")) return;
|
| 107 |
+
var s = document.createElement("style"); s.id = "holo-clean-chrome";
|
| 108 |
+
s.textContent = "#holo-themedash-btn,#holo-notepad-btn,#holo-stream-btn,#holo-capture-btn,#holo-manage-btn{display:none!important}";
|
| 109 |
+
(document.head || document.documentElement).appendChild(s);
|
| 110 |
+
} catch (e) {}
|
| 111 |
+
})();
|
| 112 |
+
|
| 113 |
+
// Bootstrap the Remix layer (window.HoloEdit): right-click any ΞΊ-object β Inspect Β· Edit Β· Share Β· Move Β·
|
| 114 |
+
// Hide Β· Delete; an edit forks a NEW ΞΊ (auto-persisted, re-derivable, shareable). Same canonical wire as
|
| 115 |
+
// UX / FX / Render (Law L2): the shell β the container for every app β and every app inherit it with NO
|
| 116 |
+
// per-app tag, and agents get the identical powers via window.HoloEdit.api. Idempotent (it self-guards).
|
| 117 |
+
(function bootHoloEdit() {
|
| 118 |
+
try {
|
| 119 |
+
if (window.HoloEdit || document.querySelector('script[src*="holo-edit.js"]')) return;
|
| 120 |
+
var s = document.createElement("script"); s.defer = true; s.src = SHARED + "holo-edit.js";
|
| 121 |
+
(document.head || document.documentElement).appendChild(s);
|
| 122 |
+
} catch (e) {}
|
| 123 |
+
})();
|
| 124 |
+
|
| 125 |
+
// Bootstrap Holo Telemetry (ADR-0073): system-wide observability native to the substrate β the
|
| 126 |
+
// OpenTelemetry data model + W3C Trace Context as content-addressed UOR objects (window.HoloTelemetry).
|
| 127 |
+
// Same canonical wire as Holo UX / FX (Law L2): every Holo Theme citizen gets it with NO per-app script
|
| 128 |
+
// tag. The module self-registers on holo-app-ready; once ready, a tiny PerformanceObserver BRIDGE turns
|
| 129 |
+
// the existing perf marks/measures every app already emits into local spans β instrumentation with no
|
| 130 |
+
// per-app code. Telemetry stays LOCAL by default (Law L1); nothing is exported without an explicit,
|
| 131 |
+
// conscience-gated consent. Idempotent + additive (the module guards its own effects).
|
| 132 |
+
(function bootHoloTelemetry() {
|
| 133 |
+
try {
|
| 134 |
+
if (window.HoloTelemetry || document.querySelector('script[src*="holo-telemetry.mjs"]')) return;
|
| 135 |
+
var s = document.createElement("script"); s.type = "module"; s.src = SHARED + "holo-telemetry.mjs";
|
| 136 |
+
(document.head || document.documentElement).appendChild(s);
|
| 137 |
+
// the one canonical bridge: existing PerformanceObserver "measure" entries β local spans. Best-effort,
|
| 138 |
+
// gated by reduced-data-cost intent and never exporting β pure local self-observation.
|
| 139 |
+
var bridge = function () {
|
| 140 |
+
try {
|
| 141 |
+
if (!window.HoloTelemetry || window.__holoTelemetryBridge) return;
|
| 142 |
+
window.__holoTelemetryBridge = true;
|
| 143 |
+
if (typeof PerformanceObserver === "undefined") return;
|
| 144 |
+
var tr = window.HoloTelemetry.tracer("perf", "1.0");
|
| 145 |
+
var po = new PerformanceObserver(function (list) {
|
| 146 |
+
for (var i = 0; i < list.getEntries().length; i++) {
|
| 147 |
+
var e = list.getEntries()[i];
|
| 148 |
+
try { tr.startSpan(e.name, { kind: "internal", attributes: { entryType: e.entryType }, start: e.startTime })
|
| 149 |
+
.end({ end: e.startTime + (e.duration || 0) }); } catch (x) {}
|
| 150 |
+
}
|
| 151 |
+
});
|
| 152 |
+
po.observe({ entryTypes: ["measure"] });
|
| 153 |
+
} catch (x) {}
|
| 154 |
+
};
|
| 155 |
+
if (window.HoloTelemetry) bridge();
|
| 156 |
+
else if (document.documentElement) document.documentElement.addEventListener("holo-telemetry-ready", bridge, { once: true });
|
| 157 |
+
} catch (e) {}
|
| 158 |
+
})();
|
| 159 |
+
|
| 160 |
+
// Bootstrap the AUTONOMY SPINE (S0βS4): the OS observes itself (tap), reflects (coherence), proposes
|
| 161 |
+
// (observer), speaks under discipline (courier), and may act bounded (trust). Same canonical wire as
|
| 162 |
+
// Telemetry (Law L2) β no per-app script tag. Each module guards its own effects (binds its window.Holo*
|
| 163 |
+
// only when its deps exist, chained by holo-*-ready events); loading them is idempotent + additive. The
|
| 164 |
+
// live cadence is driven by the heal-boot idle loop calling window.HoloSpine.runOnce(tick) β proposals
|
| 165 |
+
// only; acting stays behind HoloTrust. Local-first throughout (Law L1).
|
| 166 |
+
(function bootHoloSpine() {
|
| 167 |
+
try {
|
| 168 |
+
if (window.__holoSpineBoot) return; window.__holoSpineBoot = true;
|
| 169 |
+
var mods = ["holo-control-dsp.js", "holo-telemetry-tap.mjs", "holo-coherence.mjs",
|
| 170 |
+
"holo-observer.mjs", "holo-courier.mjs", "holo-trust.mjs", "holo-spine.mjs"];
|
| 171 |
+
mods.forEach(function (m) {
|
| 172 |
+
if (document.querySelector('script[src*="' + m + '"]')) return;
|
| 173 |
+
var s = document.createElement("script");
|
| 174 |
+
s.type = "module"; // every entry in this spine-boot list is an ES module. holo-control-dsp.js is .js-named but `export`s β loading it as a classic script threw "Unexpected token 'export'" and silently dropped the DSP core. Module scripts defer by default.
|
| 175 |
+
s.src = SHARED + m;
|
| 176 |
+
(document.head || document.documentElement).appendChild(s);
|
| 177 |
+
});
|
| 178 |
+
} catch (e) {}
|
| 179 |
+
})();
|
| 180 |
+
|
| 181 |
+
var KEY = "holo.theme.v1"; // one OS-wide choice, shared same-origin
|
| 182 |
+
var TKEY = "holo.theme.tokens.v1"; // imported DTCG theme (JSON)
|
| 183 |
+
var CKEY = "holo.theme.tokens.css.v1";// its compiled CSS (fast init, no format module)
|
| 184 |
+
var FKEY = "holo.theme.fonts.v1"; // the live font registry (catalog + imported), JSON
|
| 185 |
+
var FCKEY = "holo.theme.fonts.css.v1";// its compiled @font-face CSS (fast init)
|
| 186 |
+
var LKEY = "holo.theme.lookandfeel.v1";// the active Global Theme's look-and-feel block (JSON)
|
| 187 |
+
var themeJson = null; // the active custom theme (DTCG), if any
|
| 188 |
+
var lookAndFeel = null; // the active Global Theme's look-and-feel (icons/decoration/layoutβ¦)
|
| 189 |
+
var userFonts = []; // registered fonts: [{family, category, stack, faces, origin}]
|
| 190 |
+
var _catalog = null; // the ΞΊ-pinned font library (fonts/index.json), lazy
|
| 191 |
+
|
| 192 |
+
// ββ Portable themes (DTCG β CSS via holo-theme-format.js) ββββββββββββββββββββββββ
|
| 193 |
+
// A theme is injected as a <style id="holo-theme-tokens"> block (unlayered β it beats
|
| 194 |
+
// holo-theme.css's layered defaults; the engine's inline user overrides still win).
|
| 195 |
+
function injectTokens(cssText) {
|
| 196 |
+
var s = document.getElementById("holo-theme-tokens");
|
| 197 |
+
if (cssText == null || cssText === "") { if (s) s.remove(); return; }
|
| 198 |
+
if (!s) { s = document.createElement("style"); s.id = "holo-theme-tokens"; (document.head || document.documentElement).appendChild(s); }
|
| 199 |
+
s.textContent = cssText;
|
| 200 |
+
}
|
| 201 |
+
function loadFormat() {
|
| 202 |
+
return new Promise(function (res, rej) {
|
| 203 |
+
if (globalThis.HoloThemeFormat) return res(globalThis.HoloThemeFormat);
|
| 204 |
+
var s = document.createElement("script");
|
| 205 |
+
s.src = SHARED + "holo-theme-format.js";
|
| 206 |
+
s.onload = function () { res(globalThis.HoloThemeFormat); };
|
| 207 |
+
s.onerror = function () { rej(new Error("could not load holo-theme-format.js")); };
|
| 208 |
+
(document.head || document.documentElement).appendChild(s);
|
| 209 |
+
});
|
| 210 |
+
}
|
| 211 |
+
function broadcastTokens(css, json) {
|
| 212 |
+
var msg = { type: "holo-theme-tokens", css: css, json: json };
|
| 213 |
+
childFrames().forEach(function (w) { try { w.postMessage(msg, "*"); } catch (e) {} });
|
| 214 |
+
}
|
| 215 |
+
// Import a DTCG theme (object or JSON string): validate β compile β apply β persist β broadcast.
|
| 216 |
+
function importTheme(json, opts) {
|
| 217 |
+
opts = opts || {};
|
| 218 |
+
return loadFormat().then(function (F) {
|
| 219 |
+
if (!F) return { ok: false, errors: ["format module unavailable"] };
|
| 220 |
+
if (typeof json === "string") { try { json = JSON.parse(json); } catch (e) { return { ok: false, errors: ["invalid JSON: " + e.message] }; } }
|
| 221 |
+
var v = F.validate(json); if (!v.ok) return v;
|
| 222 |
+
var css; try { css = F.toCss(json); } catch (e) { return { ok: false, errors: [String(e.message || e)] }; }
|
| 223 |
+
themeJson = json; injectTokens(css);
|
| 224 |
+
if (opts.persist !== false) { try { localStorage.setItem(TKEY, JSON.stringify(json)); localStorage.setItem(CKEY, css); } catch (e) {} }
|
| 225 |
+
apply();
|
| 226 |
+
if (opts.broadcast !== false) broadcastTokens(css, json);
|
| 227 |
+
root.dispatchEvent(new CustomEvent("holo-theme-change", { detail: state, bubbles: false }));
|
| 228 |
+
return { ok: true };
|
| 229 |
+
});
|
| 230 |
+
}
|
| 231 |
+
// Apply a Holo GLOBAL THEME (a KDE Look-and-Feel package adopted as one ΞΊ-object): it IS a
|
| 232 |
+
// DTCG theme (color + font + radius), so its visual layer flows through importTheme unchanged β
|
| 233 |
+
// Holo Theme stays the single source of truth (ADR-023). The package's $extensions
|
| 234 |
+
// "org.hologram.lookandfeel" block (icons Β· window decoration Β· layout Β· splash Β· cursor) is
|
| 235 |
+
// surfaced for the shell as data-attributes + a holo-lookandfeel-change event, and persisted.
|
| 236 |
+
function applyGlobalTheme(gt, opts) {
|
| 237 |
+
opts = opts || {};
|
| 238 |
+
if (typeof gt === "string") { try { gt = JSON.parse(gt); } catch (e) { return Promise.resolve({ ok: false, errors: ["invalid JSON: " + e.message] }); } }
|
| 239 |
+
var laf = (gt && gt.$extensions && gt.$extensions["org.hologram.lookandfeel"]) || null;
|
| 240 |
+
return importTheme(gt, opts).then(function (res) {
|
| 241 |
+
if (!res.ok) return res;
|
| 242 |
+
applyLookAndFeel(laf, opts);
|
| 243 |
+
return { ok: true, lookAndFeel: laf };
|
| 244 |
+
});
|
| 245 |
+
}
|
| 246 |
+
function applyLookAndFeel(laf, opts) {
|
| 247 |
+
opts = opts || {};
|
| 248 |
+
lookAndFeel = laf;
|
| 249 |
+
if (laf && laf.icons) root.setAttribute("data-holo-icons", laf.icons); else root.removeAttribute("data-holo-icons");
|
| 250 |
+
var deco = laf && laf.decoration && (laf.decoration.theme || laf.decoration);
|
| 251 |
+
if (deco) root.setAttribute("data-holo-decoration", deco); else root.removeAttribute("data-holo-decoration");
|
| 252 |
+
if (laf && laf.layout) root.setAttribute("data-holo-layout", laf.layout); else root.removeAttribute("data-holo-layout");
|
| 253 |
+
if (opts.persist !== false) { try { if (laf) localStorage.setItem(LKEY, JSON.stringify(laf)); else localStorage.removeItem(LKEY); } catch (e) {} }
|
| 254 |
+
root.dispatchEvent(new CustomEvent("holo-lookandfeel-change", { detail: laf, bubbles: false }));
|
| 255 |
+
}
|
| 256 |
+
function readVars() {
|
| 257 |
+
var cs = getComputedStyle(root), out = {};
|
| 258 |
+
["--holo-bg", "--holo-surface", "--holo-surface-2", "--holo-border", "--holo-ink", "--holo-ink-dim",
|
| 259 |
+
"--holo-accent", "--holo-accent-ink", "--holo-ok", "--holo-warn", "--holo-danger",
|
| 260 |
+
"--holo-font-sans", "--holo-font-serif", "--holo-font-mono", "--holo-font-scale",
|
| 261 |
+
"--holo-radius-sm", "--holo-radius", "--holo-radius-lg", "--holo-density"
|
| 262 |
+
].forEach(function (n) { out[n] = cs.getPropertyValue(n).trim(); });
|
| 263 |
+
return out;
|
| 264 |
+
}
|
| 265 |
+
// Export the current theme as a DTCG token object (the shareable artifact). WYSIWYG:
|
| 266 |
+
// reads the LIVE custom properties (base theme + any user tweaks), and carries over the
|
| 267 |
+
// imported theme's metadata + web fonts so nothing is lost.
|
| 268 |
+
function exportTheme(meta) {
|
| 269 |
+
return loadFormat().then(function (F) {
|
| 270 |
+
if (!F) return null;
|
| 271 |
+
var ext = themeJson && themeJson.$extensions && themeJson.$extensions["org.hologram.theme"];
|
| 272 |
+
var t = F.fromVars(readVars(), meta || (ext ? { name: ext.name, author: ext.author } : { name: "My Hologram theme" }));
|
| 273 |
+
if (themeJson) {
|
| 274 |
+
if (themeJson.$description) t.$description = themeJson.$description;
|
| 275 |
+
if (themeJson.font && themeJson.font.face) t.font.face = themeJson.font.face;
|
| 276 |
+
if (themeJson.font && themeJson.font.variation) t.font.variation = themeJson.font.variation;
|
| 277 |
+
}
|
| 278 |
+
return t;
|
| 279 |
+
});
|
| 280 |
+
}
|
| 281 |
+
function clearTheme() { themeJson = null; injectTokens(""); try { localStorage.removeItem(TKEY); localStorage.removeItem(CKEY); localStorage.removeItem(KKEY); } catch (e) {} applyLookAndFeel(null, {}); broadcastTokens("", null); apply(); }
|
| 282 |
+
|
| 283 |
+
// ββ ΞΊ-addressed theme catalog β themes as content-addressed, self-verifying UOR objects βββββββββ
|
| 284 |
+
// A theme is a DTCG object served at /_shared/themes/<file>; its identity is ΞΊ = sha256 of its bytes.
|
| 285 |
+
// setThemeByKappa fetches it, RE-DERIVES sha256, refuses on mismatch (Law L5), then applies it live.
|
| 286 |
+
var _themes = null; // cached themes/index.json entries
|
| 287 |
+
var KKEY = "holo.theme.kappa.v1"; // the active ΞΊ-theme (so the studio can highlight it)
|
| 288 |
+
function listThemes() {
|
| 289 |
+
if (_themes) return Promise.resolve(_themes);
|
| 290 |
+
return fetch(SHARED + "themes/index.json").then(function (r) { return r.ok ? r.json() : { themes: [] }; })
|
| 291 |
+
.then(function (j) { _themes = (j && j.themes) || []; return _themes; })
|
| 292 |
+
.catch(function () { _themes = []; return _themes; });
|
| 293 |
+
}
|
| 294 |
+
function sha256Hex(buf) {
|
| 295 |
+
if (!(globalThis.crypto && crypto.subtle)) return Promise.resolve(null);
|
| 296 |
+
return crypto.subtle.digest("SHA-256", buf).then(function (d) {
|
| 297 |
+
var b = new Uint8Array(d), s = ""; for (var i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0"); return s;
|
| 298 |
+
});
|
| 299 |
+
}
|
| 300 |
+
function setThemeByKappa(kappa) {
|
| 301 |
+
var want = String(kappa).replace(/^sha256:/, "");
|
| 302 |
+
return listThemes().then(function (themes) {
|
| 303 |
+
var t = themes.filter(function (x) { return x.kappa === "sha256:" + want || x.kappa === kappa || x.name === kappa; })[0];
|
| 304 |
+
if (!t) return { ok: false, errors: ["unknown theme " + kappa] };
|
| 305 |
+
return fetch(SHARED + "themes/" + t.file).then(function (r) { return r.ok ? r.arrayBuffer() : Promise.reject(new Error("theme fetch failed")); })
|
| 306 |
+
.then(function (buf) {
|
| 307 |
+
return sha256Hex(buf).then(function (hex) {
|
| 308 |
+
// self-verify: the bytes must re-derive the claimed ΞΊ (Law L5). null = no SubtleCrypto (file://) β skip.
|
| 309 |
+
var hexWant = t.kappa.replace(/^sha256:/, "");
|
| 310 |
+
if (hex && hex !== hexWant) return { ok: false, errors: ["ΞΊ mismatch β theme bytes do not re-derive (Law L5)"] };
|
| 311 |
+
var json; try { json = JSON.parse(new TextDecoder().decode(buf)); } catch (e) { return { ok: false, errors: ["invalid theme JSON"] }; }
|
| 312 |
+
// Applying a theme hands AESTHETIC governance to it: clear any ad-hoc accent/typeface
|
| 313 |
+
// override so the theme's own tokens win (accessibility β font scale/min/density β is kept).
|
| 314 |
+
if (state.accent || state.fontFamily) set({ accent: "", fontFamily: "" });
|
| 315 |
+
return importTheme(json).then(function (res) { if (res.ok) { try { localStorage.setItem(KKEY, t.kappa); } catch (e) {} } return res; });
|
| 316 |
+
});
|
| 317 |
+
});
|
| 318 |
+
}).catch(function (e) { return { ok: false, errors: [String(e.message || e)] }; });
|
| 319 |
+
}
|
| 320 |
+
function setTheme(nameOrKappa) {
|
| 321 |
+
return listThemes().then(function (themes) {
|
| 322 |
+
var t = themes.filter(function (x) { return x.kappa === nameOrKappa || x.name === nameOrKappa || x.file === nameOrKappa; })[0];
|
| 323 |
+
return t ? setThemeByKappa(t.kappa) : { ok: false, errors: ["unknown theme " + nameOrKappa] };
|
| 324 |
+
});
|
| 325 |
+
}
|
| 326 |
+
function activeThemeKappa() { try { return localStorage.getItem(KKEY) || null; } catch (e) { return null; } }
|
| 327 |
+
|
| 328 |
+
var root = document.documentElement;
|
| 329 |
+
var isTop = (window.parent === window);
|
| 330 |
+
var policy = (document.querySelector('meta[name="holo-theme-policy"]') || {}).content || "adopt";
|
| 331 |
+
|
| 332 |
+
// Built-in typeface presets the Settings panel offers (all standards-only font lists).
|
| 333 |
+
var FONTS = {
|
| 334 |
+
"Helvetica Neue": '"Helvetica Neue", Helvetica, Arial, "Segoe UI", Roboto, system-ui, sans-serif',
|
| 335 |
+
System: 'system-ui, -apple-system, "Segoe UI", Roboto, Inter, "Helvetica Neue", Arial, sans-serif',
|
| 336 |
+
Humanist: 'Inter, "Segoe UI", Roboto, system-ui, sans-serif',
|
| 337 |
+
Serif: 'ui-serif, Georgia, "Times New Roman", serif',
|
| 338 |
+
Mono: 'ui-monospace, "Cascadia Code", "JetBrains Mono", Menlo, Consolas, monospace',
|
| 339 |
+
Readable: 'Atkinson Hyperlegible, "Comic Neue", Verdana, system-ui, sans-serif' // dyslexia-friendly fallbacks
|
| 340 |
+
};
|
| 341 |
+
|
| 342 |
+
var DEFAULTS = {
|
| 343 |
+
palette: "auto", // auto = follow prefers-color-scheme (Media Queries L5)
|
| 344 |
+
presentation: "standard",
|
| 345 |
+
immersive: false, // Immersive backdrop ON β a ΞΊ-sealed wallpaper behind translucent surfaces
|
| 346 |
+
wallpaper: "", // the immersive backdrop: a ΞΊ (sha256:/blake3:) or a URL; "" = none
|
| 347 |
+
fontScale: 1,
|
| 348 |
+
fontMin: 16, // readability FLOOR in px (0 = off). The whole rem ramp clamps up to it.
|
| 349 |
+
fontFamily: "", // "" = OS default (--holo-font-sans from holo-theme.css)
|
| 350 |
+
density: 1,
|
| 351 |
+
accent: "", // "" = OS default
|
| 352 |
+
enforce: false // user "enforce OS theme on every app" switch
|
| 353 |
+
};
|
| 354 |
+
|
| 355 |
+
function read() {
|
| 356 |
+
try { return Object.assign({}, DEFAULTS, JSON.parse(localStorage.getItem(KEY) || "{}")); }
|
| 357 |
+
catch (e) { return Object.assign({}, DEFAULTS); }
|
| 358 |
+
}
|
| 359 |
+
function write(s) { try { localStorage.setItem(KEY, JSON.stringify(s)); } catch (e) {} }
|
| 360 |
+
|
| 361 |
+
var state = read();
|
| 362 |
+
|
| 363 |
+
function clamp(n, lo, hi) { n = parseFloat(n); return isNaN(n) ? lo : Math.min(hi, Math.max(lo, n)); }
|
| 364 |
+
|
| 365 |
+
// Apply the current state to THIS document using only W3C primitives.
|
| 366 |
+
function apply() {
|
| 367 |
+
var prio = state.enforce ? "important" : ""; // enforce-os = user beats app !important
|
| 368 |
+
var pushAesthetic = (policy !== "own") || state.enforce;
|
| 369 |
+
var pinned = state.palette && state.palette !== "auto";
|
| 370 |
+
|
| 371 |
+
// Palette: data attribute (for CSS hooks / app reaction) + INLINE color-scheme.
|
| 372 |
+
// Inline is required β a layered @layer rule would lose to an app's UNLAYERED
|
| 373 |
+
// `color-scheme` (e.g. the shell's `:root{color-scheme:dark}`). For "own" apps we
|
| 374 |
+
// leave their own scheme untouched (unless the user chose enforce-os).
|
| 375 |
+
if (pinned) root.setAttribute("data-holo-palette", state.palette);
|
| 376 |
+
else root.removeAttribute("data-holo-palette");
|
| 377 |
+
root.setAttribute("data-holo-presentation", state.presentation || "standard");
|
| 378 |
+
if (pushAesthetic) root.style.setProperty("color-scheme", pinned ? state.palette : "light dark", prio);
|
| 379 |
+
else root.style.removeProperty("color-scheme");
|
| 380 |
+
|
| 381 |
+
// Immersive backdrop β an orthogonal axis to the light/dark palette: a ΞΊ-sealed wallpaper
|
| 382 |
+
// published behind translucent surfaces (data-holo-immersive="on" + --holo-wallpaper). The
|
| 383 |
+
// pre-paint bootstrap (holo-appearance-boot.js) sets the SAME attribute/var from the SAME
|
| 384 |
+
// state, so the async engine and the first frame never disagree.
|
| 385 |
+
root.setAttribute("data-holo-immersive", state.immersive ? "on" : "off");
|
| 386 |
+
if (state.wallpaper) setVar("--holo-wallpaper", 'url("' + wallUrl(state.wallpaper) + '")', prio);
|
| 387 |
+
else root.style.removeProperty("--holo-wallpaper");
|
| 388 |
+
|
| 389 |
+
// Shoelace (Web Awesome) activates dark tokens via a class β follow the resolved palette.
|
| 390 |
+
if (pushAesthetic) {
|
| 391 |
+
var dark = pinned ? (state.palette === "dark")
|
| 392 |
+
: !!(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
| 393 |
+
root.classList.toggle("sl-theme-dark", dark);
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
// Accessibility (always applied, even to "own" apps) β inline custom props win.
|
| 397 |
+
setVar("--holo-font-scale", clamp(state.fontScale, 0.7, 2), prio);
|
| 398 |
+
setVar("--holo-density", clamp(state.density, 0.7, 1.4), prio);
|
| 399 |
+
// Readability floor: the type ramp + root font-size clamp UP to this (holo-theme.css /
|
| 400 |
+
// holo-mobile.css use max(--holo-font-min, β¦)). 0 = off β no floor. px units required.
|
| 401 |
+
setVar("--holo-font-min", clamp(state.fontMin, 0, 28) + "px", prio);
|
| 402 |
+
|
| 403 |
+
// Aesthetics β applied unless this surface owns its theme (then only enforce-os pushes).
|
| 404 |
+
if (pushAesthetic && state.fontFamily) setVar("--holo-font-sans", state.fontFamily, prio);
|
| 405 |
+
else if (!state.fontFamily) root.style.removeProperty("--holo-font-sans");
|
| 406 |
+
if (pushAesthetic && state.accent) setVar("--holo-accent", state.accent, prio);
|
| 407 |
+
else if (!state.accent) root.style.removeProperty("--holo-accent");
|
| 408 |
+
}
|
| 409 |
+
function setVar(name, val, prio) { root.style.setProperty(name, val, prio || ""); }
|
| 410 |
+
|
| 411 |
+
// ββ Propagation across the nested-holospace tree (postMessage) βββββββββββββββββββ
|
| 412 |
+
function childFrames() {
|
| 413 |
+
var out = [];
|
| 414 |
+
var ifr = document.getElementsByTagName("iframe");
|
| 415 |
+
for (var i = 0; i < ifr.length; i++) if (ifr[i].contentWindow) out.push(ifr[i].contentWindow);
|
| 416 |
+
return out;
|
| 417 |
+
}
|
| 418 |
+
function broadcastDown() {
|
| 419 |
+
var msg = { type: "holo-theme", state: state };
|
| 420 |
+
childFrames().forEach(function (w) { try { w.postMessage(msg, "*"); } catch (e) {} });
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
// ββ Font registry β fonts as first-class, content-addressed citizens βββββββββββββ
|
| 424 |
+
// Imported and catalog fonts are injected as a <style id="holo-user-fonts"> block of
|
| 425 |
+
// @font-face rules (separate from a DTCG theme, so fonts and palettes are independent).
|
| 426 |
+
// An IMPORTED font's bytes ride inline as a data: URL, so the SAME CSS string propagates
|
| 427 |
+
// to every isolated holospace and each document loads it locally β no server route, no
|
| 428 |
+
// cross-origin fetch. A CATALOG font uses its ΞΊ path URL (smaller). Identical transport
|
| 429 |
+
// to themes: postMessage down the nested tree (HTML Standard).
|
| 430 |
+
function injectFonts(cssText) {
|
| 431 |
+
var s = document.getElementById("holo-user-fonts");
|
| 432 |
+
if (cssText == null || cssText === "") { if (s) s.remove(); return; }
|
| 433 |
+
if (!s) { s = document.createElement("style"); s.id = "holo-user-fonts"; (document.head || document.documentElement).appendChild(s); }
|
| 434 |
+
s.textContent = cssText;
|
| 435 |
+
}
|
| 436 |
+
function buildFontsCss(list) {
|
| 437 |
+
var F = globalThis.HoloThemeFormat; if (!F) return "";
|
| 438 |
+
return (list || []).map(function (d) { return F.faceCss(d.faces); }).filter(Boolean).join("\n");
|
| 439 |
+
}
|
| 440 |
+
function broadcastFonts(css, list) {
|
| 441 |
+
var msg = { type: "holo-fonts", css: css, list: list };
|
| 442 |
+
childFrames().forEach(function (w) { try { w.postMessage(msg, "*"); } catch (e) {} });
|
| 443 |
+
}
|
| 444 |
+
function quoteFamily(f) { return (/\s/.test(f) && !/^["']/.test(f)) ? '"' + f + '"' : f; }
|
| 445 |
+
function fontStack(desc) {
|
| 446 |
+
if (desc.stack && desc.stack.length) return desc.stack.map(quoteFamily).join(", ");
|
| 447 |
+
var fb = desc.category === "mono" ? 'ui-monospace, "Cascadia Code", Menlo, monospace'
|
| 448 |
+
: desc.category === "serif" ? 'ui-serif, Georgia, "Times New Roman", serif'
|
| 449 |
+
: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
|
| 450 |
+
return quoteFamily(desc.family) + ", " + fb;
|
| 451 |
+
}
|
| 452 |
+
// Preload the ACTIVE registered font's primary path src (a data: URL is already inline,
|
| 453 |
+
// so it needs none). For local ΞΊ fonts this removes the font-display:swap flash β the
|
| 454 |
+
// "magical, instant" feel comes from preload, not swap.
|
| 455 |
+
function preloadActive() {
|
| 456 |
+
try {
|
| 457 |
+
var ex = document.getElementById("holo-font-preload"); if (ex) ex.remove();
|
| 458 |
+
var fam = (state.fontFamily || "").split(",")[0].replace(/^\s*["']?|["']?\s*$/g, "");
|
| 459 |
+
if (!fam) return;
|
| 460 |
+
var d = userFonts.filter(function (x) { return x.family === fam; })[0]; if (!d) return;
|
| 461 |
+
var face = (d.faces || [])[0]; if (!face || !face.src) return;
|
| 462 |
+
var m = /url\(\s*["']?([^"')]+)["']?\s*\)/.exec(face.src); var url = m && m[1];
|
| 463 |
+
if (!url || /^data:/.test(url)) return;
|
| 464 |
+
var l = document.createElement("link");
|
| 465 |
+
l.id = "holo-font-preload"; l.rel = "preload"; l.as = "font"; l.type = "font/woff2";
|
| 466 |
+
l.crossOrigin = "anonymous"; l.href = url;
|
| 467 |
+
(document.head || document.documentElement).appendChild(l);
|
| 468 |
+
} catch (e) {}
|
| 469 |
+
}
|
| 470 |
+
// Register a font {family, faces:[β¦], category?, stack?, origin?}. Dedupes by family.
|
| 471 |
+
function addFont(desc, opts) {
|
| 472 |
+
opts = opts || {};
|
| 473 |
+
return loadFormat().then(function (F) {
|
| 474 |
+
if (!F) return { ok: false, errors: ["format module unavailable"] };
|
| 475 |
+
if (!desc || !desc.family || !Array.isArray(desc.faces) || !desc.faces.length) return { ok: false, errors: ["a font needs a family and at least one face"] };
|
| 476 |
+
userFonts = userFonts.filter(function (x) { return x.family !== desc.family; }).concat([desc]);
|
| 477 |
+
var css = buildFontsCss(userFonts); injectFonts(css);
|
| 478 |
+
if (opts.persist !== false) { try { localStorage.setItem(FKEY, JSON.stringify(userFonts)); localStorage.setItem(FCKEY, css); } catch (e) {} }
|
| 479 |
+
if (opts.broadcast !== false) broadcastFonts(css, userFonts);
|
| 480 |
+
if (opts.activate !== false) setFontFamily(fontStack(desc)); // setFontFamily also broadcasts state
|
| 481 |
+
preloadActive();
|
| 482 |
+
root.dispatchEvent(new CustomEvent("holo-theme-change", { detail: state, bubbles: false }));
|
| 483 |
+
return { ok: true, family: desc.family };
|
| 484 |
+
});
|
| 485 |
+
}
|
| 486 |
+
function removeFont(family) {
|
| 487 |
+
userFonts = userFonts.filter(function (x) { return x.family !== family; });
|
| 488 |
+
var css = buildFontsCss(userFonts); injectFonts(css);
|
| 489 |
+
try { localStorage.setItem(FKEY, JSON.stringify(userFonts)); localStorage.setItem(FCKEY, css); } catch (e) {}
|
| 490 |
+
broadcastFonts(css, userFonts);
|
| 491 |
+
preloadActive();
|
| 492 |
+
}
|
| 493 |
+
// Import a raw font File β registry descriptor (bytes inline as data: URL) β register + activate.
|
| 494 |
+
function importFontFile(file) {
|
| 495 |
+
return new Promise(function (resolve) {
|
| 496 |
+
if (!file) return resolve({ ok: false, errors: ["no file"] });
|
| 497 |
+
file.arrayBuffer().then(function (buf) {
|
| 498 |
+
var bytes = new Uint8Array(buf), bin = "", CH = 0x8000;
|
| 499 |
+
for (var i = 0; i < bytes.length; i += CH) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + CH));
|
| 500 |
+
var base64 = btoa(bin);
|
| 501 |
+
loadFormat().then(function (F) {
|
| 502 |
+
if (!F) return resolve({ ok: false, errors: ["format module unavailable"] });
|
| 503 |
+
var desc; try { desc = F.importDescriptor({ filename: file.name, base64: base64, mime: file.type }); }
|
| 504 |
+
catch (e) { return resolve({ ok: false, errors: [String(e.message || e)] }); }
|
| 505 |
+
addFont(desc, { activate: true }).then(resolve);
|
| 506 |
+
});
|
| 507 |
+
}, function (e) { resolve({ ok: false, errors: [String(e)] }); });
|
| 508 |
+
});
|
| 509 |
+
}
|
| 510 |
+
// The ΞΊ-pinned font library (fonts/index.json) β one-click add. Lazy + cached.
|
| 511 |
+
function loadCatalog() {
|
| 512 |
+
if (_catalog) return Promise.resolve(_catalog);
|
| 513 |
+
return fetch(SHARED + "fonts/index.json").then(function (r) { return r.ok ? r.json() : { fonts: [] }; })
|
| 514 |
+
.then(function (j) { _catalog = j || { fonts: [] }; return _catalog; })
|
| 515 |
+
.catch(function () { _catalog = { fonts: [] }; return _catalog; });
|
| 516 |
+
}
|
| 517 |
+
function catalogToDesc(e) {
|
| 518 |
+
return {
|
| 519 |
+
family: e.family, category: e.category || "sans", stack: e.stack, origin: "catalog",
|
| 520 |
+
faces: (e.faces || []).map(function (f) {
|
| 521 |
+
return { family: e.family, src: f.src, weight: f.weight || "100 900", style: f.style || "normal", display: f.display || "swap", unicodeRange: f.unicodeRange };
|
| 522 |
+
})
|
| 523 |
+
};
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
function set(patch, opts) {
|
| 527 |
+
opts = opts || {};
|
| 528 |
+
Object.assign(state, patch);
|
| 529 |
+
apply();
|
| 530 |
+
if (opts.persist !== false) write(state);
|
| 531 |
+
if (opts.broadcast !== false) broadcastDown();
|
| 532 |
+
root.dispatchEvent(new CustomEvent("holo-theme-change", { detail: state, bubbles: false }));
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
// Receive from parent; re-broadcast to our own children (transitive nesting).
|
| 536 |
+
window.addEventListener("message", function (e) {
|
| 537 |
+
var d = e.data;
|
| 538 |
+
if (!d || typeof d !== "object") return;
|
| 539 |
+
if (d.type === "holo-theme" && d.state) {
|
| 540 |
+
state = Object.assign({}, DEFAULTS, d.state);
|
| 541 |
+
apply(); write(state); broadcastDown();
|
| 542 |
+
} else if (d.type === "holo-theme-tokens") {
|
| 543 |
+
// A custom DTCG theme arrived from the parent β inject + persist + propagate down.
|
| 544 |
+
themeJson = d.json || null; injectTokens(d.css);
|
| 545 |
+
try { if (d.json) { localStorage.setItem(TKEY, JSON.stringify(d.json)); localStorage.setItem(CKEY, d.css || ""); } else { localStorage.removeItem(TKEY); localStorage.removeItem(CKEY); } } catch (err) {}
|
| 546 |
+
apply(); broadcastTokens(d.css, d.json);
|
| 547 |
+
} else if (d.type === "holo-fonts") {
|
| 548 |
+
// Registered fonts arrived from the parent β inject the @font-face block (bytes ride
|
| 549 |
+
// inline for imports) + persist + propagate down. No format module needed: css is compiled.
|
| 550 |
+
userFonts = Array.isArray(d.list) ? d.list : [];
|
| 551 |
+
injectFonts(d.css || "");
|
| 552 |
+
try { if (userFonts.length) { localStorage.setItem(FKEY, JSON.stringify(userFonts)); localStorage.setItem(FCKEY, d.css || ""); } else { localStorage.removeItem(FKEY); localStorage.removeItem(FCKEY); } } catch (err) {}
|
| 553 |
+
preloadActive(); broadcastFonts(d.css, userFonts);
|
| 554 |
+
} else if (d.type === "holo-theme-hello" && e.source) {
|
| 555 |
+
// A freshly-mounted child asks for the current theme; answer state + custom tokens + fonts.
|
| 556 |
+
try { e.source.postMessage({ type: "holo-theme", state: state }, "*"); } catch (err) {}
|
| 557 |
+
var sEl = document.getElementById("holo-theme-tokens");
|
| 558 |
+
if (sEl && themeJson) { try { e.source.postMessage({ type: "holo-theme-tokens", css: sEl.textContent, json: themeJson }, "*"); } catch (err) {} }
|
| 559 |
+
var fEl = document.getElementById("holo-user-fonts");
|
| 560 |
+
if (fEl && userFonts.length) { try { e.source.postMessage({ type: "holo-fonts", css: fEl.textContent, list: userFonts }, "*"); } catch (err) {} }
|
| 561 |
+
}
|
| 562 |
+
});
|
| 563 |
+
|
| 564 |
+
// ββ Initial theme: saved choice β (else) system preference β (else) OS default βββ
|
| 565 |
+
// Restore a previously imported custom theme first (fast path: compiled CSS, no format module).
|
| 566 |
+
try {
|
| 567 |
+
var savedCss = localStorage.getItem(CKEY);
|
| 568 |
+
if (savedCss) { injectTokens(savedCss); themeJson = JSON.parse(localStorage.getItem(TKEY) || "null"); }
|
| 569 |
+
var savedLaf = localStorage.getItem(LKEY);
|
| 570 |
+
if (savedLaf) applyLookAndFeel(JSON.parse(savedLaf), { persist: false });
|
| 571 |
+
} catch (e) {}
|
| 572 |
+
// Restore registered fonts (fast path: compiled @font-face CSS, no format module).
|
| 573 |
+
try {
|
| 574 |
+
var savedFonts = localStorage.getItem(FCKEY);
|
| 575 |
+
if (savedFonts) { injectFonts(savedFonts); userFonts = JSON.parse(localStorage.getItem(FKEY) || "[]"); }
|
| 576 |
+
} catch (e) {}
|
| 577 |
+
apply();
|
| 578 |
+
preloadActive();
|
| 579 |
+
// Re-resolve when the system appearance changes while we're in "auto".
|
| 580 |
+
if (window.matchMedia) {
|
| 581 |
+
try {
|
| 582 |
+
var mq = window.matchMedia("(prefers-color-scheme: dark)");
|
| 583 |
+
var onSys = function () { if (state.palette === "auto") apply(); };
|
| 584 |
+
if (mq.addEventListener) mq.addEventListener("change", onSys);
|
| 585 |
+
else if (mq.addListener) mq.addListener(onSys);
|
| 586 |
+
} catch (e) {}
|
| 587 |
+
}
|
| 588 |
+
// Ask the parent holospace for the OS-wide theme (covers mount-timing for nested apps).
|
| 589 |
+
if (!isTop) { try { window.parent.postMessage({ type: "holo-theme-hello" }, "*"); } catch (e) {} }
|
| 590 |
+
// When new child frames load, push the current theme + custom tokens + fonts to them.
|
| 591 |
+
document.addEventListener("load", function (e) {
|
| 592 |
+
if (e.target && e.target.tagName === "IFRAME" && e.target.contentWindow) {
|
| 593 |
+
var w = e.target.contentWindow;
|
| 594 |
+
try { w.postMessage({ type: "holo-theme", state: state }, "*"); } catch (err) {}
|
| 595 |
+
var sEl = document.getElementById("holo-theme-tokens");
|
| 596 |
+
if (sEl && themeJson) { try { w.postMessage({ type: "holo-theme-tokens", css: sEl.textContent, json: themeJson }, "*"); } catch (err) {} }
|
| 597 |
+
var fEl = document.getElementById("holo-user-fonts");
|
| 598 |
+
if (fEl && userFonts.length) { try { w.postMessage({ type: "holo-fonts", css: fEl.textContent, list: userFonts }, "*"); } catch (err) {} }
|
| 599 |
+
}
|
| 600 |
+
}, true);
|
| 601 |
+
|
| 602 |
+
// ββ Built-in Display settings panel β usable from any surface (the OS shell wires a
|
| 603 |
+
// button to it). Pure DOM, themed by the very tokens it edits. ββββββββββββββββ
|
| 604 |
+
function openSettings() {
|
| 605 |
+
if (document.getElementById("holo-theme-panel")) { closeSettings(); return; }
|
| 606 |
+
var css = document.createElement("style");
|
| 607 |
+
css.id = "holo-theme-panel-css";
|
| 608 |
+
css.textContent =
|
| 609 |
+
'#holo-theme-scrim{position:fixed;inset:0;z-index:2147482500;background:#0008;backdrop-filter:blur(2px)}' +
|
| 610 |
+
'#holo-theme-panel{position:fixed;z-index:2147482600;top:0;right:0;height:100%;width:min(360px,92vw);' +
|
| 611 |
+
'background:var(--holo-surface,#14161b);color:var(--holo-ink,#e6edf3);border-left:1px solid var(--holo-border,#23272f);' +
|
| 612 |
+
'box-shadow:-12px 0 40px #0006;display:flex;flex-direction:column;font-family:var(--holo-font-sans);overflow:auto;container-type:inline-size;container-name:holopanel}' +
|
| 613 |
+
'@container holopanel (max-width:300px){#holo-theme-panel .seg button{flex:1 1 100%}}' +
|
| 614 |
+
'#holo-theme-panel h2{font-size:var(--holo-text-lg,1.15rem);margin:0;padding:18px 20px;border-bottom:1px solid var(--holo-border,#23272f);display:flex;justify-content:space-between;align-items:center}' +
|
| 615 |
+
'#holo-theme-panel .grp{padding:16px 20px;border-bottom:1px solid var(--holo-border,#23272f);display:flex;flex-direction:column;gap:10px}' +
|
| 616 |
+
'#holo-theme-panel label{font-size:var(--holo-text-sm,.85rem);color:var(--holo-ink-dim,#8b949e)}' +
|
| 617 |
+
'#holo-theme-panel .seg{display:flex;gap:6px;flex-wrap:wrap}' +
|
| 618 |
+
'#holo-theme-panel .seg button{flex:1 1 auto;min-height:40px;border:1px solid var(--holo-border,#23272f);background:transparent;color:inherit;border-radius:10px;cursor:pointer;font:inherit;font-size:var(--holo-text-sm,.85rem)}' +
|
| 619 |
+
'#holo-theme-panel .seg button[aria-pressed="true"]{background:var(--holo-accent,#5b8cff);color:var(--holo-accent-ink,#fff);border-color:transparent}' +
|
| 620 |
+
'#holo-theme-panel input[type=range]{width:100%}' +
|
| 621 |
+
'#holo-theme-panel select,#holo-theme-panel input[type=color]{width:100%;min-height:40px;background:var(--holo-surface-2,#181b21);color:inherit;border:1px solid var(--holo-border,#23272f);border-radius:10px;padding:0 10px;font:inherit}' +
|
| 622 |
+
'#holo-theme-panel .x{min-width:40px;min-height:40px;border:0;background:transparent;color:inherit;font-size:1.3rem;cursor:pointer}' +
|
| 623 |
+
'#holo-theme-panel .sample{font-size:var(--holo-text,1rem);line-height:1.5}' +
|
| 624 |
+
'#holo-theme-panel .reset{margin:16px 20px;min-height:44px;border:1px solid var(--holo-border,#23272f);background:transparent;color:inherit;border-radius:10px;cursor:pointer;font:inherit}';
|
| 625 |
+
document.head.appendChild(css);
|
| 626 |
+
|
| 627 |
+
var scrim = el("div", { id: "holo-theme-scrim" });
|
| 628 |
+
scrim.onclick = closeSettings;
|
| 629 |
+
var lastFocus = document.activeElement; // restored on close (WCAG 2.4.3)
|
| 630 |
+
var p = el("aside", { id: "holo-theme-panel", role: "dialog", "aria-modal": "true", "aria-labelledby": "holo-theme-title", tabindex: "-1" });
|
| 631 |
+
|
| 632 |
+
var seg = function (label, opts, cur, on) {
|
| 633 |
+
var g = el("div", { class: "grp" }); g.appendChild(el("label", {}, label));
|
| 634 |
+
var s = el("div", { class: "seg" });
|
| 635 |
+
opts.forEach(function (o) {
|
| 636 |
+
var b = el("button", { "aria-pressed": String(o[1] === cur) }, o[0]);
|
| 637 |
+
b.onclick = function () { on(o[1]); refresh(); };
|
| 638 |
+
s.appendChild(b);
|
| 639 |
+
});
|
| 640 |
+
g.appendChild(s); return g;
|
| 641 |
+
};
|
| 642 |
+
|
| 643 |
+
function refresh() {
|
| 644 |
+
// re-render by closing + reopening keeps it tiny; state already applied live.
|
| 645 |
+
var sc = p.scrollTop; p.innerHTML = ""; build(); p.scrollTop = sc; p.focus();
|
| 646 |
+
}
|
| 647 |
+
function build() {
|
| 648 |
+
var head = el("h2", { id: "holo-theme-title" }, "Holo UI");
|
| 649 |
+
var x = el("button", { class: "x", "aria-label": "Close" }, "Γ"); x.onclick = closeSettings;
|
| 650 |
+
head.appendChild(x); p.appendChild(head);
|
| 651 |
+
|
| 652 |
+
p.appendChild(seg("Appearance", [["Auto", "auto"], ["Light", "light"], ["Dark", "dark"]], state.palette, function (v) { setPalette(v); }));
|
| 653 |
+
p.appendChild(seg("Mode", [["Standard", "standard"], ["Immersive", "immersive"]], state.presentation, function (v) { setPresentation(v); }));
|
| 654 |
+
// Immersive backdrop β the ΞΊ-sealed wallpaper layer (the curated picker lands in the shell
|
| 655 |
+
// Settings Β· Appearance section; this is the engine-level on/off the boot bootstrap reads).
|
| 656 |
+
p.appendChild(seg("Immersive backdrop", [["Off", "off"], ["On", "on"]], state.immersive ? "on" : "off", function (v) { setImmersive(v === "on"); }));
|
| 657 |
+
|
| 658 |
+
// Text size
|
| 659 |
+
var gT = el("div", { class: "grp" });
|
| 660 |
+
gT.appendChild(el("label", {}, "Text size β " + Math.round(state.fontScale * 100) + "%"));
|
| 661 |
+
var rng = el("input", { type: "range", min: "0.85", max: "1.4", step: "0.05", value: String(state.fontScale) });
|
| 662 |
+
rng.oninput = function () { setFontScale(this.value); gT.firstChild.textContent = "Text size β " + Math.round(state.fontScale * 100) + "%"; };
|
| 663 |
+
gT.appendChild(rng);
|
| 664 |
+
gT.appendChild(el("div", { class: "sample" }, "The quick brown fox jumps over the lazy dog."));
|
| 665 |
+
p.appendChild(gT);
|
| 666 |
+
|
| 667 |
+
// Minimum text size β the readability FLOOR. No text rendered through the OS tokens
|
| 668 |
+
// (the type ramp, form controls, base text) can fall below this. "Off" restores the raw ramp.
|
| 669 |
+
p.appendChild(seg("Minimum text size", [["Off", 0], ["14", 14], ["16", 16], ["18", 18], ["20", 20]],
|
| 670 |
+
state.fontMin, function (v) { setFontMin(v); }));
|
| 671 |
+
|
| 672 |
+
// Typeface β built-in presets + every registered (imported / library) font.
|
| 673 |
+
var gF = el("div", { class: "grp" });
|
| 674 |
+
gF.appendChild(el("label", {}, "Typeface"));
|
| 675 |
+
var sel = el("select", {});
|
| 676 |
+
sel.appendChild(el("option", { value: "" }, "OS default"));
|
| 677 |
+
Object.keys(FONTS).forEach(function (name) {
|
| 678 |
+
var o = el("option", { value: FONTS[name] }, name);
|
| 679 |
+
if (FONTS[name] === state.fontFamily) o.selected = true;
|
| 680 |
+
sel.appendChild(o);
|
| 681 |
+
});
|
| 682 |
+
userFonts.forEach(function (d) {
|
| 683 |
+
var val = fontStack(d);
|
| 684 |
+
var o = el("option", { value: val }, d.family + (d.origin === "import" ? " Β· imported" : ""));
|
| 685 |
+
if (val === state.fontFamily) o.selected = true;
|
| 686 |
+
sel.appendChild(o);
|
| 687 |
+
});
|
| 688 |
+
sel.onchange = function () { setFontFamily(this.value); };
|
| 689 |
+
gF.appendChild(sel);
|
| 690 |
+
gF.appendChild(el("div", { class: "sample" }, "The quick brown fox jumps over the lazy dog. 0123456789"));
|
| 691 |
+
|
| 692 |
+
// Add a font β import ANY WOFF2/WOFF/TTF/OTF; itβs content-addressed and applied
|
| 693 |
+
// across every holospace instantly (bytes ride inline, so isolation is no barrier).
|
| 694 |
+
var addRow = el("div", { class: "seg" });
|
| 695 |
+
var addBtn = el("button", {}, "οΌ Add fontβ¦");
|
| 696 |
+
var ffile = el("input", { type: "file", accept: ".woff2,.woff,.ttf,.otf,font/woff2,font/woff,font/ttf,font/otf" }); ffile.style.display = "none";
|
| 697 |
+
ffile.onchange = function () {
|
| 698 |
+
var f = ffile.files && ffile.files[0]; if (!f) { return; }
|
| 699 |
+
addBtn.disabled = true; addBtn.textContent = "Addingβ¦";
|
| 700 |
+
importFontFile(f).then(function (r) {
|
| 701 |
+
if (!r.ok) { var b = el("div", {}, "Font import failed: " + ((r.errors || []).join("; ") || "invalid")); b.style.cssText = "color:var(--holo-danger);font-size:var(--holo-text-sm);margin-top:6px"; gF.appendChild(b); addBtn.disabled = false; addBtn.textContent = "οΌ Add fontβ¦"; }
|
| 702 |
+
else refresh();
|
| 703 |
+
});
|
| 704 |
+
ffile.value = "";
|
| 705 |
+
};
|
| 706 |
+
addBtn.onclick = function () { ffile.click(); };
|
| 707 |
+
addRow.appendChild(addBtn); gF.appendChild(addRow); gF.appendChild(ffile);
|
| 708 |
+
|
| 709 |
+
// Remove imported fonts (catalog fonts are ΞΊ-pinned; leave them).
|
| 710 |
+
var imported = userFonts.filter(function (d) { return d.origin === "import"; });
|
| 711 |
+
if (imported.length) {
|
| 712 |
+
var rm = el("div", { class: "seg" });
|
| 713 |
+
imported.forEach(function (d) {
|
| 714 |
+
var b = el("button", { title: "Remove " + d.family }, "β " + d.family);
|
| 715 |
+
b.onclick = function () { removeFont(d.family); if (state.fontFamily === fontStack(d)) setFontFamily(""); refresh(); };
|
| 716 |
+
rm.appendChild(b);
|
| 717 |
+
});
|
| 718 |
+
gF.appendChild(rm);
|
| 719 |
+
}
|
| 720 |
+
var fhint = el("div", {}, "Drop in any WOFF2/TTF β itβs applied uniformly across the whole OS, instantly.");
|
| 721 |
+
fhint.style.cssText = "font-size:var(--holo-text-sm,.8rem);color:var(--holo-ink-dim);margin-top:2px"; gF.appendChild(fhint);
|
| 722 |
+
p.appendChild(gF);
|
| 723 |
+
|
| 724 |
+
// Font library β the ΞΊ-pinned catalog (fonts/index.json). One click = add + activate.
|
| 725 |
+
var gL = el("div", { class: "grp" });
|
| 726 |
+
gL.appendChild(el("label", {}, "Font library Β· ΞΊ-pinned"));
|
| 727 |
+
var lib = el("div", { class: "seg" }); gL.appendChild(lib); p.appendChild(gL);
|
| 728 |
+
lib.appendChild(el("div", {}, "Loadingβ¦"));
|
| 729 |
+
loadCatalog().then(function (cat) {
|
| 730 |
+
lib.innerHTML = "";
|
| 731 |
+
var fonts = (cat && cat.fonts) || [];
|
| 732 |
+
if (!fonts.length) { lib.appendChild(el("div", {}, "β")); return; }
|
| 733 |
+
fonts.forEach(function (entry) {
|
| 734 |
+
var have = userFonts.some(function (x) { return x.family === entry.family; });
|
| 735 |
+
var b = el("button", { "aria-pressed": String(have) }, (have ? "β " : "οΌ ") + entry.family);
|
| 736 |
+
b.onclick = function () { addFont(catalogToDesc(entry), { activate: true }).then(function () { refresh(); }); };
|
| 737 |
+
lib.appendChild(b);
|
| 738 |
+
});
|
| 739 |
+
});
|
| 740 |
+
|
| 741 |
+
// Accent
|
| 742 |
+
var gA = el("div", { class: "grp" });
|
| 743 |
+
gA.appendChild(el("label", {}, "Accent colour"));
|
| 744 |
+
var col = el("input", { type: "color", value: toHex(state.accent) || "#5b8cff" });
|
| 745 |
+
col.oninput = function () { setAccent(this.value); };
|
| 746 |
+
gA.appendChild(col);
|
| 747 |
+
p.appendChild(gA);
|
| 748 |
+
|
| 749 |
+
// Enforce on apps
|
| 750 |
+
p.appendChild(seg("Apply to apps", [["Respect apps", "respect"], ["Enforce OS theme", "enforce"]],
|
| 751 |
+
state.enforce ? "enforce" : "respect", function (v) { set({ enforce: v === "enforce" }); }));
|
| 752 |
+
|
| 753 |
+
// Theme β create / save / load as a W3C Design Tokens (DTCG) file (shareable).
|
| 754 |
+
var gM = el("div", { class: "grp" });
|
| 755 |
+
gM.appendChild(el("label", {}, "Theme Β· W3C Design Tokens (.json)"));
|
| 756 |
+
var row = el("div", { class: "seg" });
|
| 757 |
+
var expBtn = el("button", {}, "β€ Export");
|
| 758 |
+
expBtn.onclick = function () {
|
| 759 |
+
exportTheme().then(function (t) {
|
| 760 |
+
if (!t) return;
|
| 761 |
+
var ext = t.$extensions && t.$extensions["org.hologram.theme"];
|
| 762 |
+
var name = (ext && ext.name) || "hologram";
|
| 763 |
+
var a = el("a", {});
|
| 764 |
+
a.href = URL.createObjectURL(new Blob([JSON.stringify(t, null, 2)], { type: "application/json" }));
|
| 765 |
+
a.download = String(name).toLowerCase().replace(/\s+/g, "-") + ".tokens.json";
|
| 766 |
+
document.body.appendChild(a); a.click(); a.remove();
|
| 767 |
+
setTimeout(function () { URL.revokeObjectURL(a.href); }, 2000);
|
| 768 |
+
});
|
| 769 |
+
};
|
| 770 |
+
var impBtn = el("button", {}, "β€ Import");
|
| 771 |
+
var file = el("input", { type: "file", accept: ".json,application/json" }); file.style.display = "none";
|
| 772 |
+
file.onchange = function () {
|
| 773 |
+
var f = file.files && file.files[0]; if (!f) return;
|
| 774 |
+
f.text().then(function (txt) {
|
| 775 |
+
importTheme(txt).then(function (r) {
|
| 776 |
+
if (!r.ok) { var b = el("div", { class: "themeerr" }, "Import failed: " + ((r.errors || []).join("; ") || "invalid")); b.style.cssText = "color:var(--holo-danger);font-size:var(--holo-text-sm);margin-top:6px"; gM.appendChild(b); }
|
| 777 |
+
else refresh();
|
| 778 |
+
});
|
| 779 |
+
});
|
| 780 |
+
file.value = "";
|
| 781 |
+
};
|
| 782 |
+
impBtn.onclick = function () { file.click(); };
|
| 783 |
+
var defBtn = el("button", {}, "OS default");
|
| 784 |
+
defBtn.onclick = function () { clearTheme(); refresh(); };
|
| 785 |
+
row.appendChild(expBtn); row.appendChild(impBtn); row.appendChild(defBtn);
|
| 786 |
+
gM.appendChild(row); gM.appendChild(file);
|
| 787 |
+
var hint = el("div", {}, "Adjust the controls above, then Export to save & share your theme β or Import one.");
|
| 788 |
+
hint.style.cssText = "font-size:var(--holo-text-sm,.8rem);color:var(--holo-ink-dim);margin-top:2px"; gM.appendChild(hint);
|
| 789 |
+
// The canonical, full appearance surface is Holo Control (the Holo UI app's Appearance panel):
|
| 790 |
+
// themes + accent + typography + accessibility + import/fork, all live. This quick panel stays for
|
| 791 |
+
// fast in-place tweaks; the button below opens the complete control panel (ADR-0079).
|
| 792 |
+
var browse = el("button", {}, "π Open Holo Control β full appearance settings");
|
| 793 |
+
browse.style.cssText = "margin-top:8px;min-height:40px;border:1px solid var(--holo-border);background:transparent;color:inherit;border-radius:10px;cursor:pointer;font:inherit;font-size:var(--holo-text-sm)";
|
| 794 |
+
browse.onclick = function () { try { window.open(SHARED + "../apps/ui/index.html#appearance", "_blank"); } catch (e) {} };
|
| 795 |
+
gM.appendChild(browse);
|
| 796 |
+
p.appendChild(gM);
|
| 797 |
+
|
| 798 |
+
var reset = el("button", { class: "reset" }, "Reset to defaults"); reset.onclick = function () { resetAll(); clearTheme(); refresh(); };
|
| 799 |
+
p.appendChild(reset);
|
| 800 |
+
}
|
| 801 |
+
build();
|
| 802 |
+
document.body.appendChild(scrim);
|
| 803 |
+
document.body.appendChild(p);
|
| 804 |
+
|
| 805 |
+
// ARIA modal behaviour (WCAG 2.1.2 / 2.4.3 / 4.1.2): focus in, trap Tab, Escape closes.
|
| 806 |
+
var focusables = function () { return p.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); };
|
| 807 |
+
(focusables()[0] || p).focus();
|
| 808 |
+
p.__lastFocus = lastFocus;
|
| 809 |
+
p.__onkey = function (e) {
|
| 810 |
+
if (e.key === "Escape") { e.preventDefault(); closeSettings(); return; }
|
| 811 |
+
if (e.key !== "Tab") return;
|
| 812 |
+
var f = focusables(); if (!f.length) return;
|
| 813 |
+
var first = f[0], last = f[f.length - 1];
|
| 814 |
+
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
| 815 |
+
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
| 816 |
+
};
|
| 817 |
+
document.addEventListener("keydown", p.__onkey, true);
|
| 818 |
+
}
|
| 819 |
+
function closeSettings() {
|
| 820 |
+
var p = document.getElementById("holo-theme-panel");
|
| 821 |
+
if (p && p.__onkey) document.removeEventListener("keydown", p.__onkey, true);
|
| 822 |
+
["holo-theme-panel", "holo-theme-scrim", "holo-theme-panel-css"].forEach(function (id) {
|
| 823 |
+
var n = document.getElementById(id); if (n) n.remove();
|
| 824 |
+
});
|
| 825 |
+
if (p && p.__lastFocus && p.__lastFocus.focus) { try { p.__lastFocus.focus(); } catch (e) {} }
|
| 826 |
+
}
|
| 827 |
+
function el(tag, attrs, text) {
|
| 828 |
+
var n = document.createElement(tag);
|
| 829 |
+
if (attrs) for (var k in attrs) n.setAttribute(k, attrs[k]);
|
| 830 |
+
if (text != null) n.textContent = text;
|
| 831 |
+
return n;
|
| 832 |
+
}
|
| 833 |
+
function toHex(c) { return /^#[0-9a-f]{6}$/i.test(c || "") ? c : ""; }
|
| 834 |
+
// Resolve an immersive wallpaper reference to a URL: a ΞΊ (sha256:/blake3:/sha512:<hex>)
|
| 835 |
+
// becomes the serverless ΞΊ-store path /.holo/<algo>/<hex> (re-derived on fetch, Law L5);
|
| 836 |
+
// anything else (a path, data: URL) is used verbatim.
|
| 837 |
+
function wallUrl(w) {
|
| 838 |
+
if (!w) return "";
|
| 839 |
+
var m = String(w).match(/^(sha256|blake3|sha512):([0-9a-f]+)$/i);
|
| 840 |
+
return m ? "/.holo/" + m[1].toLowerCase() + "/" + m[2] : String(w);
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
// ββ Public API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 844 |
+
function setPalette(v) { set({ palette: v }); }
|
| 845 |
+
function setPresentation(v) { set({ presentation: v }); }
|
| 846 |
+
function setImmersive(on) { set({ immersive: !!on }); }
|
| 847 |
+
function setWallpaper(k) { set({ wallpaper: k || "" }); }
|
| 848 |
+
// The one user-facing appearance choice (Settings Β· login picker): Dark Β· Light Β· Immersive.
|
| 849 |
+
// Dark/Light pin the palette and drop the backdrop; Immersive turns the backdrop on and keeps
|
| 850 |
+
// the current palette (a light OR dark immersive backdrop both read well behind the scrim).
|
| 851 |
+
function setMode(m) {
|
| 852 |
+
if (m === "light") set({ palette: "light", immersive: false });
|
| 853 |
+
else if (m === "dark") set({ palette: "dark", immersive: false });
|
| 854 |
+
else if (m === "immersive") set({ immersive: true });
|
| 855 |
+
}
|
| 856 |
+
function setFontScale(n) { set({ fontScale: clamp(n, 0.7, 2) }); }
|
| 857 |
+
function setFontMin(n) { set({ fontMin: clamp(n, 0, 28) }); } // readability floor in px (0 = off)
|
| 858 |
+
function setFontFamily(f) { set({ fontFamily: f || "" }); }
|
| 859 |
+
function setDensity(n) { set({ density: clamp(n, 0.7, 1.4) }); }
|
| 860 |
+
function setAccent(c) { set({ accent: c || "" }); }
|
| 861 |
+
function resetAll() { state = Object.assign({}, DEFAULTS); apply(); write(state); broadcastDown(); }
|
| 862 |
+
|
| 863 |
+
window.HoloTheme = {
|
| 864 |
+
get: function () { return Object.assign({}, state); },
|
| 865 |
+
set: set, setPalette: setPalette, setPresentation: setPresentation,
|
| 866 |
+
setImmersive: setImmersive, setWallpaper: setWallpaper, setMode: setMode,
|
| 867 |
+
setFontScale: setFontScale, setFontMin: setFontMin, setFontFamily: setFontFamily, setDensity: setDensity,
|
| 868 |
+
setAccent: setAccent, reset: resetAll, openSettings: openSettings, closeSettings: closeSettings,
|
| 869 |
+
// Portable themes (DTCG): import a token file/object, export the current theme, clear back to OS default.
|
| 870 |
+
importTheme: importTheme, exportTheme: exportTheme, clearTheme: clearTheme,
|
| 871 |
+
getThemeJson: function () { return themeJson ? JSON.parse(JSON.stringify(themeJson)) : null; },
|
| 872 |
+
// ΞΊ-addressed theme catalog: list the content-addressed DTCG themes, and swap to one by ΞΊ (or name).
|
| 873 |
+
// setThemeByKappa re-derives sha256 and refuses on mismatch (Law L5) before applying β live, OS-wide.
|
| 874 |
+
listThemes: listThemes, setTheme: setTheme, setThemeByKappa: setThemeByKappa, activeThemeKappa: activeThemeKappa,
|
| 875 |
+
setVar: function (name, val) { setVar(name, val); root.dispatchEvent(new CustomEvent("holo-theme-change", { detail: state, bubbles: false })); },
|
| 876 |
+
// Global Theme (KDE Look-and-Feel package adopted as one ΞΊ-object): import flows through
|
| 877 |
+
// importTheme, then surfaces the look-and-feel block (icons/decoration/layout/splash) to the shell.
|
| 878 |
+
applyGlobalTheme: applyGlobalTheme,
|
| 879 |
+
getLookAndFeel: function () { return lookAndFeel ? JSON.parse(JSON.stringify(lookAndFeel)) : null; },
|
| 880 |
+
// Fonts as first-class citizens: import a file (data: URL), add a descriptor, or pull from
|
| 881 |
+
// the ΞΊ-pinned library β each propagates @font-face across every isolated holospace.
|
| 882 |
+
importFontFile: importFontFile, addFont: addFont, removeFont: removeFont,
|
| 883 |
+
loadFontCatalog: loadCatalog, getFonts: function () { return JSON.parse(JSON.stringify(userFonts)); },
|
| 884 |
+
FONTS: FONTS, policy: policy
|
| 885 |
+
};
|
| 886 |
+
})();
|
sha256/a0ebbc696bda5154727b76b42c11921231cbc652de3e705dd6b632456e6f34aa
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// teleport.js β ONE reusable Share affordance for every hologram page.
|
| 2 |
+
//
|
| 3 |
+
// Drop-in, zero config: <script src="teleport.js" defer></script>
|
| 4 |
+
//
|
| 5 |
+
// It gives the page a floating "Share" button that builds a TELEPORT LINK β a
|
| 6 |
+
// single URL that, opened on ANY device, boots the hologram environment (the
|
| 7 |
+
// browser everyone already has) and lands on this exact content. No install, no
|
| 8 |
+
// server. The link is handed to the OS share sheet via the W3C Web Share API
|
| 9 |
+
// (navigator.share), with a clipboard fallback on desktop browsers that lack it.
|
| 10 |
+
//
|
| 11 |
+
// Content-addressed (#k=): if the page knows its ΞΊ β via
|
| 12 |
+
// <meta name="holo-kappa" content="blake3:β¦"> or HoloTeleport.stamp("blake3:β¦")
|
| 13 |
+
// β the link carries #k=<ΞΊ> so the content is identified by its hash, not just
|
| 14 |
+
// its location. Opening such a link shows a provenance chip; a page that can hand
|
| 15 |
+
// us the exact bytes (HoloTeleport.verifyAgainst) gets a TRUE re-derivation check
|
| 16 |
+
// (Law L5): a β only when the re-derived ΞΊ equals the pinned one β never faked.
|
| 17 |
+
//
|
| 18 |
+
// ΞΊ is computed by the app's own wasm (blake3, identical to the native ΞΊ-store)
|
| 19 |
+
// when present, else SHA-256 via Web Crypto β the prefix (blake3:/sha256:) always
|
| 20 |
+
// names the algorithm, so nothing is ever mislabelled.
|
| 21 |
+
|
| 22 |
+
(function () {
|
| 23 |
+
"use strict";
|
| 24 |
+
const W = window;
|
| 25 |
+
if (W.HoloTeleport) return;
|
| 26 |
+
|
| 27 |
+
const metaOf = (n) => {
|
| 28 |
+
const e = document.querySelector(`meta[name="${n}"]`) || document.querySelector(`meta[property="${n}"]`);
|
| 29 |
+
return e ? (e.getAttribute("content") || "") : "";
|
| 30 |
+
};
|
| 31 |
+
let pageKappa = metaOf("holo-kappa") || ""; // optional content hash for this page
|
| 32 |
+
|
| 33 |
+
// ββ teleport-link builder ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 34 |
+
// Host-agnostic: uses location.origin, so it works on localhost today and on a
|
| 35 |
+
// real host (e.g. GitHub Pages) the moment you publish β same code.
|
| 36 |
+
function link(opts) {
|
| 37 |
+
opts = opts || {};
|
| 38 |
+
const base = location.origin + location.pathname + (opts.search != null ? opts.search : location.search);
|
| 39 |
+
const k = opts.kappa || pageKappa;
|
| 40 |
+
return base + (k ? "#k=" + encodeURIComponent(k) : (opts.keepHash === false ? "" : location.hash || ""));
|
| 41 |
+
}
|
| 42 |
+
const titleOf = () => (document.title || "hologram").replace(/\s+β\s+holospaces$/i, "") + " Β· on hologram";
|
| 43 |
+
const textOf = () =>
|
| 44 |
+
metaOf("description") || metaOf("og:description") ||
|
| 45 |
+
"Opens on any device β boots hologram and this content, verified by its content hash. No install.";
|
| 46 |
+
|
| 47 |
+
// ββ share: OS sheet via Web Share, clipboard fallback ββββββββββββββββββββ
|
| 48 |
+
async function share(opts) {
|
| 49 |
+
const url = link(opts);
|
| 50 |
+
const data = { title: (opts && opts.title) || titleOf(), text: (opts && opts.text) || textOf(), url };
|
| 51 |
+
try {
|
| 52 |
+
if (navigator.share && (!navigator.canShare || navigator.canShare(data))) {
|
| 53 |
+
await navigator.share(data); toast("Shared."); return url;
|
| 54 |
+
}
|
| 55 |
+
} catch (e) { if (e && e.name === "AbortError") return url; }
|
| 56 |
+
try { await navigator.clipboard.writeText(url); toast("Teleport link copied β paste it anywhere."); }
|
| 57 |
+
catch { W.prompt && W.prompt("Teleport link:", url); }
|
| 58 |
+
return url;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// ββ ΞΊ helpers β blake3 via the app wasm, else SHA-256 ββββββββββββββββββββ
|
| 62 |
+
let _wasm; // memoised import
|
| 63 |
+
async function kappaOf(bytes) {
|
| 64 |
+
const u8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
| 65 |
+
try {
|
| 66 |
+
if (_wasm === undefined) { _wasm = null; const m = await import("./pkg/holospaces_web.js").catch(() => null); if (m) { if (m.default) await m.default(); _wasm = m; } }
|
| 67 |
+
if (_wasm && _wasm.kappa) { const k = _wasm.kappa(u8); return k.includes(":") ? k : "blake3:" + k; }
|
| 68 |
+
} catch {}
|
| 69 |
+
try {
|
| 70 |
+
const d = await crypto.subtle.digest("SHA-256", u8);
|
| 71 |
+
return "sha256:" + Array.from(new Uint8Array(d), (b) => b.toString(16).padStart(2, "0")).join("");
|
| 72 |
+
} catch { return ""; }
|
| 73 |
+
}
|
| 74 |
+
// TRUE verification: re-derive the ΞΊ of the given bytes and compare to want.
|
| 75 |
+
async function verifyAgainst(want, bytes) {
|
| 76 |
+
if (!want) return false;
|
| 77 |
+
const got = await kappaOf(bytes);
|
| 78 |
+
const ok = !!got && got === want;
|
| 79 |
+
chip(ok ? "β verified genuine Β· " + shortK(want) : "ΞΊ mismatch β refused (content β shared hash)", ok ? "ok" : "bad");
|
| 80 |
+
return ok;
|
| 81 |
+
}
|
| 82 |
+
const shortK = (k) => { const [a, h] = String(k).split(":"); return h ? a + ":" + h.slice(0, 12) + "β¦" : String(k).slice(0, 16) + "β¦"; };
|
| 83 |
+
|
| 84 |
+
// ββ on open: surface #k= provenance, then TRY a real re-derivation check ββ
|
| 85 |
+
// Convention: on teleport.js pages, #k= is THIS page's content ΞΊ. So we can
|
| 86 |
+
// refetch the page bytes and re-derive β a β only on a true match (Law L5),
|
| 87 |
+
// a refusal banner on mismatch. If the bytes aren't fetchable we leave the
|
| 88 |
+
// neutral provenance chip rather than claim anything.
|
| 89 |
+
async function onOpen() {
|
| 90 |
+
const h = new URLSearchParams((location.hash || "").replace(/^#/, ""));
|
| 91 |
+
const k = h.get("k");
|
| 92 |
+
if (!k) return;
|
| 93 |
+
const want = decodeURIComponent(k);
|
| 94 |
+
chip("content-addressed Β· ΞΊ " + shortK(want), "info");
|
| 95 |
+
try {
|
| 96 |
+
const buf = await (await fetch(location.pathname, { cache: "no-store" })).arrayBuffer();
|
| 97 |
+
const got = await kappaOf(new Uint8Array(buf));
|
| 98 |
+
if (got && got === want) chip("β verified genuine Β· ΞΊ " + shortK(want), "ok");
|
| 99 |
+
else if (got && want.split(":")[0] === got.split(":")[0]) chip("ΞΊ mismatch β content differs from the shared hash", "bad");
|
| 100 |
+
} catch { /* opaque/cross-origin β keep the neutral provenance chip */ }
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// ββ UI: floating Share button + toast + provenance chip (self-contained) ββ
|
| 104 |
+
function injectStyle() {
|
| 105 |
+
if (document.getElementById("holo-teleport-css")) return;
|
| 106 |
+
const s = document.createElement("style"); s.id = "holo-teleport-css";
|
| 107 |
+
s.textContent = `
|
| 108 |
+
#holo-toast{position:fixed;left:50%;bottom:84px;transform:translateX(-50%);z-index:2147483001;max-width:88vw;
|
| 109 |
+
background:#11181c;color:#e6edf3;border:1px solid #243038;border-radius:10px;padding:10px 14px;font:var(--holo-text-sm, 1rem) system-ui;
|
| 110 |
+
box-shadow:0 12px 36px rgba(0,0,0,.5);opacity:0;transition:opacity .18s;pointer-events:none}
|
| 111 |
+
#holo-toast.in{opacity:1}
|
| 112 |
+
#holo-chip{position:fixed;left:50%;top:12px;transform:translateX(-50%);z-index:2147483001;
|
| 113 |
+
background:#0d1117ee;border:1px solid #243038;border-radius:999px;padding:6px 13px;font:var(--holo-text-sm, 1rem)/1.2 ui-monospace,monospace;
|
| 114 |
+
color:#9fb0bd;backdrop-filter:blur(6px)}
|
| 115 |
+
#holo-chip.ok{border-color:#2dd4bf;color:#7defc9} #holo-chip.bad{border-color:#f87171;color:#fca5a5}`;
|
| 116 |
+
document.head.appendChild(s);
|
| 117 |
+
}
|
| 118 |
+
// (No auto-injected floating "Share" button β removed by request. Sharing remains
|
| 119 |
+
// available programmatically via HoloTeleport.share(); the #k= provenance chip and
|
| 120 |
+
// Law-L5 re-derivation are unaffected.)
|
| 121 |
+
let toastT;
|
| 122 |
+
function toast(m) {
|
| 123 |
+
injectStyle(); let t = document.getElementById("holo-toast");
|
| 124 |
+
if (!t) { t = document.createElement("div"); t.id = "holo-toast"; document.body.appendChild(t); }
|
| 125 |
+
t.textContent = m; requestAnimationFrame(() => t.classList.add("in"));
|
| 126 |
+
clearTimeout(toastT); toastT = setTimeout(() => t.classList.remove("in"), 3600);
|
| 127 |
+
}
|
| 128 |
+
function chip(m, kind) {
|
| 129 |
+
injectStyle(); let c = document.getElementById("holo-chip");
|
| 130 |
+
if (!c) { c = document.createElement("div"); c.id = "holo-chip"; document.body.appendChild(c); }
|
| 131 |
+
c.className = kind || "info"; c.textContent = m;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
function boot() { injectStyle(); onOpen(); }
|
| 135 |
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
|
| 136 |
+
else boot();
|
| 137 |
+
|
| 138 |
+
W.HoloTeleport = {
|
| 139 |
+
link, share, kappaOf, verifyAgainst,
|
| 140 |
+
stamp: (k) => { pageKappa = k || ""; }, // page declares its content hash
|
| 141 |
+
toast, chip,
|
| 142 |
+
};
|
| 143 |
+
})();
|
sha256/b2bfa104b9ee8e3bb631dcd0ec0e6f7017b8898c4304666f5b3dfeeebc926eb9
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* holo-mobile.css β the Hologram mobile conformance layer.
|
| 2 |
+
*
|
| 3 |
+
* One stylesheet every holospace links. It does NOT theme apps β they keep their
|
| 4 |
+
* own colors and feel. It enforces the mobile *mechanics* that must be standard on
|
| 5 |
+
* every screen: no horizontal overflow, safe-area insets, β₯48px tap targets, and
|
| 6 |
+
* 16px form controls (no iOS auto-zoom). The spec is WCAG 2.2 (Β§1.4.10 Reflow,
|
| 7 |
+
* Β§2.5.8 Target Size) + Material Design 3 (48dp). Conformance is enforced by
|
| 8 |
+
* mobile-conformance-witness.mjs (the gate), not by this file alone.
|
| 9 |
+
*
|
| 10 |
+
* Specificity discipline: structural defaults use :where() (specificity 0) so any
|
| 11 |
+
* app rule overrides them freely. This layer is loaded AFTER each page's own styles.
|
| 12 |
+
* Deliberately NO `overflow-x:clip` backstop β clipping would hide overflow from the
|
| 13 |
+
* witness and let real violations pass. Overflow is fixed at the cause, not masked.
|
| 14 |
+
*/
|
| 15 |
+
|
| 16 |
+
:root {
|
| 17 |
+
/* Safe-area insets (notch / home indicator). 0 on devices without them. */
|
| 18 |
+
--safe-t: env(safe-area-inset-top, 0px);
|
| 19 |
+
--safe-r: env(safe-area-inset-right, 0px);
|
| 20 |
+
--safe-b: env(safe-area-inset-bottom, 0px);
|
| 21 |
+
--safe-l: env(safe-area-inset-left, 0px);
|
| 22 |
+
|
| 23 |
+
/* Shared tokens β consistency without dictating color. Apps map these to brand. */
|
| 24 |
+
--holo-tap: 48px; /* Material Design 3 minimum touch target */
|
| 25 |
+
--holo-space-1: 4px; --holo-space-2: 8px; --holo-space-3: 12px;
|
| 26 |
+
--holo-space-4: 16px; --holo-space-5: 24px; --holo-space-6: 32px; /* 8px grid */
|
| 27 |
+
--holo-radius-sm: 8px; --holo-radius: 12px; --holo-radius-lg: 16px;
|
| 28 |
+
/* Fluid type scale (clamps to the viewport; readable floor, sane ceiling). Authored
|
| 29 |
+
* in rem β NOT px β so the whole ramp scales from the OS text-size lever
|
| 30 |
+
* (--holo-font-scale β root font-size; see holo-theme.css). 1rem = 16px at base.
|
| 31 |
+
* The sub-1rem steps clamp UP to the readability floor (--holo-font-min, default 16px,
|
| 32 |
+
* set by the engine) with max(), so caption/label text can never render too small to read;
|
| 33 |
+
* -lg/-xl already exceed the floor so the hierarchy above it is preserved. */
|
| 34 |
+
--holo-text-sm: max(var(--holo-font-min, 16px), clamp(0.8125rem, 0.78rem + 0.2vw, 0.875rem));
|
| 35 |
+
--holo-text: max(var(--holo-font-min, 16px), clamp(0.9375rem, 0.9rem + 0.3vw, 1rem));
|
| 36 |
+
--holo-text-lg: clamp(1.125rem, 1.0rem + 0.6vw, 1.3125rem);
|
| 37 |
+
--holo-text-xl: clamp(1.5rem, 1.2rem + 1.6vw, 2.125rem);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* ββ Global guards (safe on every page) βββββββββββββββββββββββββββββββββββββββ */
|
| 41 |
+
*, *::before, *::after { box-sizing: border-box; }
|
| 42 |
+
:where(html) { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
|
| 43 |
+
/* Replaced/wide elements must never force the page wider than the viewport. */
|
| 44 |
+
:where(img, svg, video, canvas, iframe, table) { max-width: 100%; }
|
| 45 |
+
:where(pre, code) { overflow-wrap: anywhere; }
|
| 46 |
+
|
| 47 |
+
/* ββ Tap targets β WCAG 2.5.8 + MD3 48px floor (low specificity = app-overridable). */
|
| 48 |
+
:where(button, [role="button"], a.btn, .btn, input:not([type="checkbox"]):not([type="radio"]):not([type="range"]), select, summary) {
|
| 49 |
+
min-height: var(--holo-tap);
|
| 50 |
+
}
|
| 51 |
+
/* Buttons (incl. icon-only) also meet the 48px width floor by default. */
|
| 52 |
+
:where(button, [role="button"], a.btn, .btn, summary) { min-width: var(--holo-tap); }
|
| 53 |
+
/* Escape hatch for genuinely-dense, essential chrome where 48px is impractical
|
| 54 |
+
* (status bars, emulator window controls) β WCAG 2.5.8 allows such exceptions.
|
| 55 |
+
* The conformance witness skips tap-size checks inside `.holo-dense`. Use sparingly. */
|
| 56 |
+
:where(.holo-dense) :where(button, [role="button"], a.btn) { min-width: 0; min-height: 0; }
|
| 57 |
+
|
| 58 |
+
/* ββ Form controls β readability floor (β₯16px) also stops iOS auto-zoom on focus. ββ */
|
| 59 |
+
:where(input, select, textarea) { font-size: max(var(--holo-font-min, 16px), 1rem); }
|
| 60 |
+
|
| 61 |
+
/* ββ Safe-area variables exposed for adoption. Applied generically to the body so
|
| 62 |
+
* content clears the notch/home-indicator; bespoke fixed bars opt in via the
|
| 63 |
+
* utility classes below (env() on a bar's own padding is overridden by the app's
|
| 64 |
+
* padding rule, so we keep this best-effort + opt-in rather than fighting apps). */
|
| 65 |
+
:where(body) { padding-left: var(--safe-l); padding-right: var(--safe-r); }
|
| 66 |
+
.holo-safe-top { padding-top: max(var(--holo-space-2), var(--safe-t)); }
|
| 67 |
+
.holo-safe-bottom { padding-bottom: max(var(--holo-space-2), var(--safe-b)); }
|
| 68 |
+
|
| 69 |
+
/* ββ Adopt-in primitives β the standard way to present these elements βββββββββ */
|
| 70 |
+
|
| 71 |
+
/* A top app bar that never overflows: brand stays put, the rest can shrink/scroll. */
|
| 72 |
+
.holo-appbar {
|
| 73 |
+
display: flex; align-items: center; gap: var(--holo-space-3);
|
| 74 |
+
padding: max(var(--holo-space-2), var(--safe-t)) var(--holo-space-4) var(--holo-space-2);
|
| 75 |
+
min-height: var(--holo-tap);
|
| 76 |
+
}
|
| 77 |
+
.holo-appbar > .brand, .holo-appbar > :first-child { flex: 0 0 auto; min-width: 0; }
|
| 78 |
+
.holo-appbar > .grow { flex: 1 1 auto; min-width: 0; }
|
| 79 |
+
|
| 80 |
+
/* A horizontally scrollable chip/meta row β scrolls instead of clipping (the fix
|
| 81 |
+
* for the Linux/QVAC header chips). Momentum scroll, no visible scrollbar. */
|
| 82 |
+
.holo-scroll-x {
|
| 83 |
+
display: flex; flex-wrap: nowrap; gap: var(--holo-space-2);
|
| 84 |
+
overflow-x: auto; -webkit-overflow-scrolling: touch;
|
| 85 |
+
scrollbar-width: none; min-width: 0; max-width: 100%;
|
| 86 |
+
}
|
| 87 |
+
.holo-scroll-x::-webkit-scrollbar { display: none; }
|
| 88 |
+
.holo-scroll-x > * { flex: 0 0 auto; }
|
| 89 |
+
|
| 90 |
+
/* Long unbroken strings (hashes, ΞΊ ids, URLs) wrap instead of widening the page. */
|
| 91 |
+
.holo-break { overflow-wrap: anywhere; word-break: break-word; }
|
| 92 |
+
|
| 93 |
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 94 |
+
* CONTAINER-QUERY WINDOW-SIZE CLASSES β Material Design 3 adaptive layout.
|
| 95 |
+
*
|
| 96 |
+
* A Holo app renders INSIDE a window/iframe, not at the device viewport β so its
|
| 97 |
+
* responsiveness must key on the app's OWN width, not the screen. We establish the app
|
| 98 |
+
* surface as a named container (`holoapp`) and expose MD3 window-size classes
|
| 99 |
+
* (Compact <600 Β· Medium 600β839 Β· Expanded β₯840) as @container queries + a --holo-size
|
| 100 |
+
* token. The SAME app is therefore Compact whether maximized on a phone OR floated in a
|
| 101 |
+
* small desktop window β one rule, every context, no per-app viewport breakpoints. The
|
| 102 |
+
* host marks the surface with [data-holo-surface] (the shell run-injection Β· the scaffold
|
| 103 |
+
* template Β· holo-embed Β· the Service Worker for standalone-served apps); OS chrome never
|
| 104 |
+
* sets it, so this whole block is inert there.
|
| 105 |
+
*
|
| 106 |
+
* PWA: combined with viewport-fit=cover + the env() safe-areas above, this is exactly
|
| 107 |
+
* what an installed display:standalone PWA needs β the layer makes an app correct in a
|
| 108 |
+
* browser tab AND installed to the home screen, with no app-specific work.
|
| 109 |
+
* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 110 |
+
:where(html[data-holo-surface]) { container: holoapp / inline-size; --holo-size: expanded; }
|
| 111 |
+
/* body INHERITS the default expanded; the @container rules set --holo-size DIRECTLY on body β a declared
|
| 112 |
+
* value beats an inherited one even at :where() specificity 0, so the narrower class wins when the
|
| 113 |
+
* container matches, without out-specifying the app. Read the class as var(--holo-size). */
|
| 114 |
+
@container holoapp (max-width: 839.98px) { :where(body) { --holo-size: medium; } }
|
| 115 |
+
@container holoapp (max-width: 599.98px) { :where(body) { --holo-size: compact; } }
|
| 116 |
+
|
| 117 |
+
/* Opt-in adaptive utilities β declarative Compact behavior keyed to the CONTAINER, so an
|
| 118 |
+
* app reflows correctly in a small window or on a phone without authoring breakpoints. */
|
| 119 |
+
.holo-cq-cols { display: grid; gap: var(--holo-space-3);
|
| 120 |
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); } /* multi-col β single col when narrow */
|
| 121 |
+
@container holoapp (max-width: 599.98px) {
|
| 122 |
+
.holo-cq-row { flex-direction: column; align-items: stretch; } /* a horizontal row stacks when Compact */
|
| 123 |
+
.holo-hide-compact { display: none !important; } /* drop secondary chrome on a phone-class surface */
|
| 124 |
+
}
|
| 125 |
+
@container holoapp (min-width: 600px) {
|
| 126 |
+
.holo-show-compact { display: none !important; } /* Compact-only affordances hide when roomy */
|
| 127 |
+
}
|
sha256/c4414e9c61a6ae6b8c19145dddbbe24c68bea83784666434a94cf033b06cc483
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Holo Theme (holo-theme.css) β the Hologram OS theming kernel. Companion to holo-mobile.css:
|
| 2 |
+
* that file enforces mobile *mechanics*; this one defines the *visual language*
|
| 3 |
+
* (palette, typography, density) as a runtime contract every holospace can adopt.
|
| 4 |
+
*
|
| 5 |
+
* 100% W3C standards-track β NO framework, NO build tool is the source of truth:
|
| 6 |
+
* β’ CSS Custom Properties for Cascading Variables L1 β the tokens
|
| 7 |
+
* β’ CSS Properties and Values API (@property) β the typed token schema
|
| 8 |
+
* β’ CSS Cascading & Inheritance L5 (@layer) β OS / app / user precedence
|
| 9 |
+
* β’ CSS Color 4 + Color Adjustment L1 (light-dark()) β light & dark in one value
|
| 10 |
+
* β’ Media Queries L5 (prefers-*) β honor system + a11y defaults
|
| 11 |
+
*
|
| 12 |
+
* The theme PAYLOAD is this file; the theme IDENTITY is its ΞΊ (sha256 + JSON-LD + SRI
|
| 13 |
+
* + did:holo:, emitted by build-holo-site.mjs per ADR-022). Switched live by
|
| 14 |
+
* holo-theme.js over the postMessage iframe tree. Gated by theme-conformance-witness.mjs.
|
| 15 |
+
*
|
| 16 |
+
* Precedence (last @layer wins): an app's UNLAYERED rules beat any layer here, so an
|
| 17 |
+
* app keeps its own look by simply declaring its own values ("own"). An app that does
|
| 18 |
+
* NOT declare a token falls through to the OS value ("adopt"). The user always wins for
|
| 19 |
+
* accessibility because holo-theme.js applies font-scale/density as INLINE custom
|
| 20 |
+
* properties on :root (author inline beats stylesheet rules; enforce-os adds !important).
|
| 21 |
+
*/
|
| 22 |
+
|
| 23 |
+
@layer hos-base, hos-os, app, hos-user;
|
| 24 |
+
|
| 25 |
+
/* ββ Typed token schema (W3C @property) β gives tokens a type, an initial value, and
|
| 26 |
+
* inheritance without any JSON format or compiler. Invalid values are ignored. βββ */
|
| 27 |
+
@property --holo-font-scale { syntax: "<number>"; inherits: true; initial-value: 1; }
|
| 28 |
+
@property --holo-density { syntax: "<number>"; inherits: true; initial-value: 1; }
|
| 29 |
+
@property --holo-accent { syntax: "<color>"; inherits: true; initial-value: #5b8cff; }
|
| 30 |
+
/* Minimum text size β the readability FLOOR (default 16px). Set live by the engine on :root;
|
| 31 |
+
* the type ramp (holo-mobile.css) and the root font-size below clamp UP to it with max(), so no
|
| 32 |
+
* OS/app text rendered through the tokens can fall below it. 0 = off (restore the raw ramp). */
|
| 33 |
+
@property --holo-font-min { syntax: "<length>"; inherits: true; initial-value: 16px; }
|
| 34 |
+
|
| 35 |
+
@layer hos-os {
|
| 36 |
+
:root {
|
| 37 |
+
/* Enable light-dark(); palette attribute (set by the engine) pins one mode. */
|
| 38 |
+
color-scheme: light dark;
|
| 39 |
+
|
| 40 |
+
/* ββ Typography β the OS-settable type system (Hologram OS βΊ Settings βΊ Display).
|
| 41 |
+
* --holo-font-scale drives the ROOT font-size, so every rem-based size and the
|
| 42 |
+
* --holo-text* ramp (holo-mobile.css, authored in rem) scale from ONE lever. */
|
| 43 |
+
--holo-font-scale: 1; /* 0.85 β¦ 1.4, set live by the engine */
|
| 44 |
+
/* Default OS typeface: Helvetica Neue (the iPhone's face) β a system font on Apple
|
| 45 |
+
* devices; falls back to Helvetica/Arial elsewhere. (Not vendored; it's proprietary.) */
|
| 46 |
+
--holo-font-sans: "Helvetica Neue", Helvetica, Arial, "Segoe UI", Roboto, system-ui, sans-serif;
|
| 47 |
+
--holo-font-serif: ui-serif, Georgia, "Times New Roman", serif;
|
| 48 |
+
--holo-font-mono: ui-monospace, "Cascadia Code", "JetBrains Mono", Menlo, Consolas, monospace;
|
| 49 |
+
|
| 50 |
+
/* ββ Density β multiplies UI spacing; immersive mode tightens it. */
|
| 51 |
+
--holo-density: 1;
|
| 52 |
+
|
| 53 |
+
/* Focus ring width (WCAG 2.4.7/2.4.13); bumped under prefers-contrast. */
|
| 54 |
+
--holo-focus-width: 3px;
|
| 55 |
+
|
| 56 |
+
/* ββ Palette (Chakra-derived, ADR) β core neutrals rebased onto Chakra's zinc ramp so every
|
| 57 |
+
* vendored shadcn/Magic UI component renders on the palette it was designed for, while the
|
| 58 |
+
* Holo brand accent is preserved. light & dark in one value (CSS Color Adjustment L1). */
|
| 59 |
+
--holo-bg: light-dark(#fafafa, #09090B);
|
| 60 |
+
--holo-surface: light-dark(#FFFFFF, #111111);
|
| 61 |
+
--holo-surface-2: light-dark(#f4f4f5, #18181b);
|
| 62 |
+
--holo-surface-emphasized: light-dark(#e4e4e7, #27272a);
|
| 63 |
+
--holo-border: light-dark(#e4e4e7, #27272a);
|
| 64 |
+
--holo-border-subtle: light-dark(#f4f4f5, #18181b);
|
| 65 |
+
--holo-border-emphasized: light-dark(#d4d4d8, #3f3f46);
|
| 66 |
+
--holo-ink: light-dark(#18181b, #fafafa);
|
| 67 |
+
--holo-ink-dim: light-dark(#71717a, #a1a1aa);
|
| 68 |
+
--holo-ink-subtle: light-dark(#a1a1aa, #71717a);
|
| 69 |
+
--holo-accent: light-dark(#3b5bdb, #5b8cff); /* Holo brand β preserved */
|
| 70 |
+
--holo-accent-ink: #ffffff;
|
| 71 |
+
--holo-ok: light-dark(#16a34a, #22c55e);
|
| 72 |
+
--holo-warn: light-dark(#ea580c, #f97316);
|
| 73 |
+
--holo-danger: light-dark(#dc2626, #ef4444);
|
| 74 |
+
/* Derived surface tints β Color 4 color-mix(), no extra tokens to maintain. */
|
| 75 |
+
--holo-accent-soft: color-mix(in srgb, var(--holo-accent) 16%, transparent);
|
| 76 |
+
--holo-hover: color-mix(in srgb, var(--holo-ink) 8%, transparent);
|
| 77 |
+
|
| 78 |
+
/* ββ Chakra-derived foundation (ramps + scales) β additive, content-addressed theme system. */
|
| 79 |
+
/* color ramps β Chakra's tuned hues, available to every app (charts, status, colorPalette). */
|
| 80 |
+
--holo-white: #FFFFFF; --holo-black: #09090B;
|
| 81 |
+
|
| 82 |
+
/* gray */
|
| 83 |
+
--holo-gray-50: #fafafa;
|
| 84 |
+
--holo-gray-100: #f4f4f5;
|
| 85 |
+
--holo-gray-200: #e4e4e7;
|
| 86 |
+
--holo-gray-300: #d4d4d8;
|
| 87 |
+
--holo-gray-400: #a1a1aa;
|
| 88 |
+
--holo-gray-500: #71717a;
|
| 89 |
+
--holo-gray-600: #52525b;
|
| 90 |
+
--holo-gray-700: #3f3f46;
|
| 91 |
+
--holo-gray-800: #27272a;
|
| 92 |
+
--holo-gray-900: #18181b;
|
| 93 |
+
--holo-gray-950: #111111;
|
| 94 |
+
|
| 95 |
+
/* red */
|
| 96 |
+
--holo-red-50: #fef2f2;
|
| 97 |
+
--holo-red-100: #fee2e2;
|
| 98 |
+
--holo-red-200: #fecaca;
|
| 99 |
+
--holo-red-300: #fca5a5;
|
| 100 |
+
--holo-red-400: #f87171;
|
| 101 |
+
--holo-red-500: #ef4444;
|
| 102 |
+
--holo-red-600: #dc2626;
|
| 103 |
+
--holo-red-700: #991919;
|
| 104 |
+
--holo-red-800: #511111;
|
| 105 |
+
--holo-red-900: #300c0c;
|
| 106 |
+
--holo-red-950: #1f0808;
|
| 107 |
+
|
| 108 |
+
/* orange */
|
| 109 |
+
--holo-orange-50: #fff7ed;
|
| 110 |
+
--holo-orange-100: #ffedd5;
|
| 111 |
+
--holo-orange-200: #fed7aa;
|
| 112 |
+
--holo-orange-300: #fdba74;
|
| 113 |
+
--holo-orange-400: #fb923c;
|
| 114 |
+
--holo-orange-500: #f97316;
|
| 115 |
+
--holo-orange-600: #ea580c;
|
| 116 |
+
--holo-orange-700: #92310a;
|
| 117 |
+
--holo-orange-800: #6c2710;
|
| 118 |
+
--holo-orange-900: #3b1106;
|
| 119 |
+
--holo-orange-950: #220a04;
|
| 120 |
+
|
| 121 |
+
/* yellow */
|
| 122 |
+
--holo-yellow-50: #fefce8;
|
| 123 |
+
--holo-yellow-100: #fef9c3;
|
| 124 |
+
--holo-yellow-200: #fef08a;
|
| 125 |
+
--holo-yellow-300: #fde047;
|
| 126 |
+
--holo-yellow-400: #facc15;
|
| 127 |
+
--holo-yellow-500: #eab308;
|
| 128 |
+
--holo-yellow-600: #ca8a04;
|
| 129 |
+
--holo-yellow-700: #845209;
|
| 130 |
+
--holo-yellow-800: #713f12;
|
| 131 |
+
--holo-yellow-900: #422006;
|
| 132 |
+
--holo-yellow-950: #281304;
|
| 133 |
+
|
| 134 |
+
/* green */
|
| 135 |
+
--holo-green-50: #f0fdf4;
|
| 136 |
+
--holo-green-100: #dcfce7;
|
| 137 |
+
--holo-green-200: #bbf7d0;
|
| 138 |
+
--holo-green-300: #86efac;
|
| 139 |
+
--holo-green-400: #4ade80;
|
| 140 |
+
--holo-green-500: #22c55e;
|
| 141 |
+
--holo-green-600: #16a34a;
|
| 142 |
+
--holo-green-700: #116932;
|
| 143 |
+
--holo-green-800: #124a28;
|
| 144 |
+
--holo-green-900: #042713;
|
| 145 |
+
--holo-green-950: #03190c;
|
| 146 |
+
|
| 147 |
+
/* teal */
|
| 148 |
+
--holo-teal-50: #f0fdfa;
|
| 149 |
+
--holo-teal-100: #ccfbf1;
|
| 150 |
+
--holo-teal-200: #99f6e4;
|
| 151 |
+
--holo-teal-300: #5eead4;
|
| 152 |
+
--holo-teal-400: #2dd4bf;
|
| 153 |
+
--holo-teal-500: #14b8a6;
|
| 154 |
+
--holo-teal-600: #0d9488;
|
| 155 |
+
--holo-teal-700: #0c5d56;
|
| 156 |
+
--holo-teal-800: #114240;
|
| 157 |
+
--holo-teal-900: #032726;
|
| 158 |
+
--holo-teal-950: #021716;
|
| 159 |
+
|
| 160 |
+
/* blue */
|
| 161 |
+
--holo-blue-50: #eff6ff;
|
| 162 |
+
--holo-blue-100: #dbeafe;
|
| 163 |
+
--holo-blue-200: #bfdbfe;
|
| 164 |
+
--holo-blue-300: #a3cfff;
|
| 165 |
+
--holo-blue-400: #60a5fa;
|
| 166 |
+
--holo-blue-500: #3b82f6;
|
| 167 |
+
--holo-blue-600: #2563eb;
|
| 168 |
+
--holo-blue-700: #173da6;
|
| 169 |
+
--holo-blue-800: #1a3478;
|
| 170 |
+
--holo-blue-900: #14204a;
|
| 171 |
+
--holo-blue-950: #0c142e;
|
| 172 |
+
|
| 173 |
+
/* cyan */
|
| 174 |
+
--holo-cyan-50: #ecfeff;
|
| 175 |
+
--holo-cyan-100: #cffafe;
|
| 176 |
+
--holo-cyan-200: #a5f3fc;
|
| 177 |
+
--holo-cyan-300: #67e8f9;
|
| 178 |
+
--holo-cyan-400: #22d3ee;
|
| 179 |
+
--holo-cyan-500: #06b6d4;
|
| 180 |
+
--holo-cyan-600: #0891b2;
|
| 181 |
+
--holo-cyan-700: #0c5c72;
|
| 182 |
+
--holo-cyan-800: #134152;
|
| 183 |
+
--holo-cyan-900: #072a38;
|
| 184 |
+
--holo-cyan-950: #051b24;
|
| 185 |
+
|
| 186 |
+
/* purple */
|
| 187 |
+
--holo-purple-50: #faf5ff;
|
| 188 |
+
--holo-purple-100: #f3e8ff;
|
| 189 |
+
--holo-purple-200: #e9d5ff;
|
| 190 |
+
--holo-purple-300: #d8b4fe;
|
| 191 |
+
--holo-purple-400: #c084fc;
|
| 192 |
+
--holo-purple-500: #a855f7;
|
| 193 |
+
--holo-purple-600: #9333ea;
|
| 194 |
+
--holo-purple-700: #641ba3;
|
| 195 |
+
--holo-purple-800: #4a1772;
|
| 196 |
+
--holo-purple-900: #2f0553;
|
| 197 |
+
--holo-purple-950: #1a032e;
|
| 198 |
+
|
| 199 |
+
/* pink */
|
| 200 |
+
--holo-pink-50: #fdf2f8;
|
| 201 |
+
--holo-pink-100: #fce7f3;
|
| 202 |
+
--holo-pink-200: #fbcfe8;
|
| 203 |
+
--holo-pink-300: #f9a8d4;
|
| 204 |
+
--holo-pink-400: #f472b6;
|
| 205 |
+
--holo-pink-500: #ec4899;
|
| 206 |
+
--holo-pink-600: #db2777;
|
| 207 |
+
--holo-pink-700: #a41752;
|
| 208 |
+
--holo-pink-800: #6d0e34;
|
| 209 |
+
--holo-pink-900: #45061f;
|
| 210 |
+
--holo-pink-950: #2c0514;
|
| 211 |
+
|
| 212 |
+
/* alpha ramps */
|
| 213 |
+
--holo-whiteAlpha-50: rgba(255, 255, 255, 0.04);
|
| 214 |
+
--holo-whiteAlpha-100: rgba(255, 255, 255, 0.06);
|
| 215 |
+
--holo-whiteAlpha-200: rgba(255, 255, 255, 0.08);
|
| 216 |
+
--holo-whiteAlpha-300: rgba(255, 255, 255, 0.16);
|
| 217 |
+
--holo-whiteAlpha-400: rgba(255, 255, 255, 0.24);
|
| 218 |
+
--holo-whiteAlpha-500: rgba(255, 255, 255, 0.36);
|
| 219 |
+
--holo-whiteAlpha-600: rgba(255, 255, 255, 0.48);
|
| 220 |
+
--holo-whiteAlpha-700: rgba(255, 255, 255, 0.64);
|
| 221 |
+
--holo-whiteAlpha-800: rgba(255, 255, 255, 0.80);
|
| 222 |
+
--holo-whiteAlpha-900: rgba(255, 255, 255, 0.92);
|
| 223 |
+
--holo-blackAlpha-50: rgba(0, 0, 0, 0.04);
|
| 224 |
+
--holo-blackAlpha-100: rgba(0, 0, 0, 0.06);
|
| 225 |
+
--holo-blackAlpha-200: rgba(0, 0, 0, 0.08);
|
| 226 |
+
--holo-blackAlpha-300: rgba(0, 0, 0, 0.16);
|
| 227 |
+
--holo-blackAlpha-400: rgba(0, 0, 0, 0.24);
|
| 228 |
+
--holo-blackAlpha-500: rgba(0, 0, 0, 0.36);
|
| 229 |
+
--holo-blackAlpha-600: rgba(0, 0, 0, 0.48);
|
| 230 |
+
--holo-blackAlpha-700: rgba(0, 0, 0, 0.64);
|
| 231 |
+
--holo-blackAlpha-800: rgba(0, 0, 0, 0.80);
|
| 232 |
+
--holo-blackAlpha-900: rgba(0, 0, 0, 0.92);
|
| 233 |
+
|
| 234 |
+
/* spacing scale (rem) β fills --holo-space-3/4 used by holo-theme.css and drives the primitives. */
|
| 235 |
+
--holo-space-1: 0.25rem;
|
| 236 |
+
--holo-space-2: 0.5rem;
|
| 237 |
+
--holo-space-3: 0.75rem;
|
| 238 |
+
--holo-space-4: 1rem;
|
| 239 |
+
--holo-space-5: 1.25rem;
|
| 240 |
+
--holo-space-6: 1.5rem;
|
| 241 |
+
--holo-space-7: 1.75rem;
|
| 242 |
+
--holo-space-8: 2rem;
|
| 243 |
+
--holo-space-9: 2.25rem;
|
| 244 |
+
--holo-space-10: 2.5rem;
|
| 245 |
+
--holo-space-11: 2.75rem;
|
| 246 |
+
--holo-space-12: 3rem;
|
| 247 |
+
--holo-space-14: 3.5rem;
|
| 248 |
+
--holo-space-16: 4rem;
|
| 249 |
+
--holo-space-20: 5rem;
|
| 250 |
+
--holo-space-24: 6rem;
|
| 251 |
+
--holo-space-28: 7rem;
|
| 252 |
+
--holo-space-32: 8rem;
|
| 253 |
+
--holo-space-36: 9rem;
|
| 254 |
+
--holo-space-40: 10rem;
|
| 255 |
+
--holo-space-44: 11rem;
|
| 256 |
+
--holo-space-48: 12rem;
|
| 257 |
+
--holo-space-52: 13rem;
|
| 258 |
+
--holo-space-56: 14rem;
|
| 259 |
+
--holo-space-60: 15rem;
|
| 260 |
+
--holo-space-64: 16rem;
|
| 261 |
+
--holo-space-72: 18rem;
|
| 262 |
+
--holo-space-80: 20rem;
|
| 263 |
+
--holo-space-96: 24rem;
|
| 264 |
+
--holo-space-0p5: 0.125rem;
|
| 265 |
+
--holo-space-1p5: 0.375rem;
|
| 266 |
+
--holo-space-2p5: 0.625rem;
|
| 267 |
+
--holo-space-3p5: 0.875rem;
|
| 268 |
+
--holo-space-4p5: 1.125rem;
|
| 269 |
+
|
| 270 |
+
/* radius scale β --holo-radius keeps the OS default (0.75rem = the 12px components assumed). */
|
| 271 |
+
--holo-radius-none: 0;
|
| 272 |
+
--holo-radius-2xs: 0.0625rem;
|
| 273 |
+
--holo-radius-xs: 0.125rem;
|
| 274 |
+
--holo-radius-sm: 0.25rem;
|
| 275 |
+
--holo-radius-md: 0.375rem;
|
| 276 |
+
--holo-radius-lg: 0.5rem;
|
| 277 |
+
--holo-radius-xl: 0.75rem;
|
| 278 |
+
--holo-radius-2xl: 1rem;
|
| 279 |
+
--holo-radius-3xl: 1.5rem;
|
| 280 |
+
--holo-radius-4xl: 2rem;
|
| 281 |
+
--holo-radius-full: 9999px;
|
| 282 |
+
--holo-radius: var(--holo-radius-xl);
|
| 283 |
+
|
| 284 |
+
/* weight Β· line-height Β· tracking Β· elevation scales */
|
| 285 |
+
--holo-weight-thin: 100;
|
| 286 |
+
--holo-weight-extralight: 200;
|
| 287 |
+
--holo-weight-light: 300;
|
| 288 |
+
--holo-weight-normal: 400;
|
| 289 |
+
--holo-weight-medium: 500;
|
| 290 |
+
--holo-weight-semibold: 600;
|
| 291 |
+
--holo-weight-bold: 700;
|
| 292 |
+
--holo-weight-extrabold: 800;
|
| 293 |
+
--holo-weight-black: 900;
|
| 294 |
+
--holo-leading-shorter: 1.25;
|
| 295 |
+
--holo-leading-short: 1.375;
|
| 296 |
+
--holo-leading-moderate: 1.5;
|
| 297 |
+
--holo-leading-tall: 1.625;
|
| 298 |
+
--holo-leading-taller: 2;
|
| 299 |
+
--holo-tracking-tighter: -0.05em;
|
| 300 |
+
--holo-tracking-tight: -0.025em;
|
| 301 |
+
--holo-tracking-wide: 0.025em;
|
| 302 |
+
--holo-tracking-wider: 0.05em;
|
| 303 |
+
--holo-tracking-widest: 0.1em;
|
| 304 |
+
--holo-shadow-xs: 0 1px 2px light-dark(rgba(0,0,0,0.06), rgba(0,0,0,0.40));
|
| 305 |
+
--holo-shadow-sm: 0 1px 3px light-dark(rgba(0,0,0,0.08), rgba(0,0,0,0.46)), 0 1px 2px light-dark(rgba(0,0,0,0.06), rgba(0,0,0,0.40));
|
| 306 |
+
--holo-shadow-md: 0 4px 12px -2px light-dark(rgba(0,0,0,0.10), rgba(0,0,0,0.52));
|
| 307 |
+
--holo-shadow-lg: 0 12px 28px -8px light-dark(rgba(0,0,0,0.14), rgba(0,0,0,0.60));
|
| 308 |
+
--holo-shadow-xl: 0 24px 56px -16px light-dark(rgba(0,0,0,0.18), rgba(0,0,0,0.66));
|
| 309 |
+
|
| 310 |
+
/* ββ Glass / translucency (ADR-0059) β TranslucentTB's accent-state model
|
| 311 |
+
* (holo-translucenttb-accents.json) expressed as CSS. A surface picks ONE state
|
| 312 |
+
* via its own [data-glass] attribute; CSS blur px = TranslucentTB blur_radius/3.
|
| 313 |
+
* Acrylic = blur + saturate + a faint accent tint (Fluent material). Used by the
|
| 314 |
+
* dock (#holo-dock) and adoptable by any chrome that wants the same glass. */
|
| 315 |
+
--holo-glass-tint: light-dark(rgba(246,248,252,0.72), rgba(22,26,34,0.70));
|
| 316 |
+
--holo-glass-blur: 22px; /* β TranslucentTB blur_radius 66 / 3 β crisp Fluent frost */
|
| 317 |
+
--holo-glass-sat: 1.8;
|
| 318 |
+
--holo-glass-border: light-dark(rgba(255,255,255,0.90), rgba(255,255,255,0.22));
|
| 319 |
+
--holo-glass-edge: light-dark(rgba(255,255,255,0.85), rgba(255,255,255,0.28)); /* inset top bevel β reads on any bg */
|
| 320 |
+
--holo-glass-shadow: 0 0.5rem 2.5rem light-dark(rgba(20,30,50,0.28), rgba(0,0,0,0.60));
|
| 321 |
+
--holo-glass-opaque-bg: light-dark(#ffffff, #14161b); --holo-glass-opaque-fx: none;
|
| 322 |
+
--holo-glass-clear-bg: transparent; --holo-glass-clear-fx: none;
|
| 323 |
+
--holo-glass-blur-bg: var(--holo-glass-tint); --holo-glass-blur-fx: blur(var(--holo-glass-blur)) saturate(1.4) brightness(1.04);
|
| 324 |
+
--holo-glass-acrylic-bg: color-mix(in srgb, var(--holo-glass-tint) 86%, var(--holo-accent) 14%);
|
| 325 |
+
--holo-glass-acrylic-fx: blur(var(--holo-glass-blur)) saturate(var(--holo-glass-sat)) brightness(1.08);
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
/* Explicit palette beats the system default (Media Queries L5 still applies on auto). */
|
| 329 |
+
:root[data-holo-palette="light"] { color-scheme: light; }
|
| 330 |
+
:root[data-holo-palette="dark"] { color-scheme: dark; }
|
| 331 |
+
|
| 332 |
+
/* Presentation axis β orthogonal to palette. Immersive = denser chrome, calm surfaces. */
|
| 333 |
+
:root[data-holo-presentation="immersive"] { --holo-density: 0.82; }
|
| 334 |
+
|
| 335 |
+
/* ββ Global application at specificity 0 (:where) so ANY app rule overrides freely.
|
| 336 |
+
* This is the only place the OS reaches every surface by default; it intentionally
|
| 337 |
+
* sets typography (the user's readability) but NOT colors (apps keep their identity
|
| 338 |
+
* unless they adopt the tokens), matching holo-mobile.css's restraint. */
|
| 339 |
+
:where(html) { font-size: max(var(--holo-font-min, 16px), calc(100% * var(--holo-font-scale, 1))); }
|
| 340 |
+
/* font-optical-sizing + synthesis are CSS Fonts Module controls (real type quality). */
|
| 341 |
+
:where(body) { font-family: var(--holo-font-sans); font-optical-sizing: auto; font-variation-settings: var(--holo-font-variation, normal); }
|
| 342 |
+
:where(code, pre, kbd, samp, .holo-mono) { font-family: var(--holo-font-mono); }
|
| 343 |
+
|
| 344 |
+
/* Visible keyboard focus (WCAG 2.4.7 Focus Visible / 2.4.13 Focus Appearance). Mouse
|
| 345 |
+
* focus stays quiet via :focus-visible; specificity 0 so apps can restyle, not remove. */
|
| 346 |
+
:where(:focus-visible) { outline: var(--holo-focus-width, 3px) solid var(--holo-accent); outline-offset: 2px; }
|
| 347 |
+
|
| 348 |
+
/* Honor the user's motion preference OS-wide (WCAG 2.3.3 / Media Queries L5). */
|
| 349 |
+
@media (prefers-reduced-motion: reduce) {
|
| 350 |
+
:where(*, *::before, *::after) {
|
| 351 |
+
animation-duration: 0.001ms !important; animation-iteration-count: 1 !important;
|
| 352 |
+
transition-duration: 0.001ms !important; scroll-behavior: auto !important;
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
/* Honor the user's contrast preference (Media Queries L5 / WCAG 1.4.6 + 1.4.11):
|
| 357 |
+
* push borders/ink to the extremes and widen the focus ring. */
|
| 358 |
+
@media (prefers-contrast: more) {
|
| 359 |
+
:root {
|
| 360 |
+
--holo-border: light-dark(#000000, #ffffff);
|
| 361 |
+
--holo-ink: light-dark(#000000, #ffffff);
|
| 362 |
+
--holo-ink-dim: light-dark(#26303a, #d7dee6);
|
| 363 |
+
--holo-focus-width: 4px;
|
| 364 |
+
}
|
| 365 |
+
}
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
/* ββ Adopt-in primitives β the standard way to render OS-themed surface chrome. Apps
|
| 369 |
+
* opt in by using these tokens/classes; they then follow palette + font changes. */
|
| 370 |
+
.holo-surface { background: var(--holo-surface); color: var(--holo-ink); border: 1px solid var(--holo-border); border-radius: var(--holo-radius, 12px); }
|
| 371 |
+
.holo-accent { background: var(--holo-accent); color: var(--holo-accent-ink); }
|
| 372 |
+
.holo-ink-dim { color: var(--holo-ink-dim); }
|
| 373 |
+
|
| 374 |
+
/* ββ Container-query responsiveness (W3C CSS Containment / Container Queries) ββββββββ
|
| 375 |
+
* A holospace nested in a small iframe must lay out by its OWN width, not the device
|
| 376 |
+
* viewport β that's exactly what container queries give us (media queries can't see
|
| 377 |
+
* the iframe's box). A region opts in with .holo-cq, and its children adapt to the
|
| 378 |
+
* CONTAINER size. This is the standards-correct answer for an OS of nested frames. */
|
| 379 |
+
.holo-cq { container-type: inline-size; container-name: holo; }
|
| 380 |
+
.holo-cq-grid { display: grid; gap: var(--holo-space-4, 16px); grid-template-columns: 1fr; }
|
| 381 |
+
@container holo (min-width: 30rem) { .holo-cq-grid { grid-template-columns: repeat(2, 1fr); } }
|
| 382 |
+
@container holo (min-width: 52rem) { .holo-cq-grid { grid-template-columns: repeat(3, 1fr); } }
|
| 383 |
+
.holo-cq-row { display: flex; flex-direction: column; gap: var(--holo-space-3, 12px); }
|
| 384 |
+
@container holo (min-width: 34rem) { .holo-cq-row { flex-direction: row; align-items: center; } }
|