From c5065ec025a183843eca86d49229f2a712f0dea4 Mon Sep 17 00:00:00 2001 From: "Cristian D. Moreno (Kyonax)" Date: Mon, 27 Apr 2026 02:41:51 -0500 Subject: [PATCH 1/2] feat(context): news-style lower-third + sidebar HUD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build context-screen as the second @kyonax_on_tech web source — news-style lower-third with marquee row and toggleable right sidebar that renders any .org-authored context with full landing-page control via BroadcastChannel. - pull Geomanist from kyo-web-online + declare --font-display, --surface-bg gradient, and 4 motion tokens on :root - add uniorg-parse runtime dep (~25 KB gzipped, localhost-only) - shared/utils/org.js: AST parser + metadata / marquee / body partitioner (Rule J topic library, OrgSchemaError) - @ui/org-content.vue: recursive Vue template AST renderer; zero v-html (D11 — ESLint no-restricted-syntax bans innerHTML); D10 per-node styling for headlines / lists / checklists / tables / src+results / quotes / links / hr - brand-loader.js: +CONTEXTS glob /@*/data/contexts/*.org + getContexts() helper - @composables/use-context-channel.js: singleton BroadcastChannel composable + debounced localStorage persist + html-class toggle - @kyonax_on_tech/sources/hud/context-screen.vue: full HUD source — strip (Geomanist + SpaceMono), marquee (translateX loop), sidebar (translateX slide + translateY auto-scroll on inner wrapper + custom lateral indicator), peek arrow (layered static halo + animated opacity); contain: layout paint on every sub-tree - @ui/chip.vue: +shape prop (pill | square) for D15 sharp-corner lock - @modals/context-control.vue: clickable slug list + sidebar toggle + live iframe preview; extends BaseModal - @elements/card.vue: conditional CONTROLS button + modal mount - 2 fixture .org files at @kyonax_on_tech/data/contexts/ Modified-by: Cristian D. Moreno (Kyonax) --- .../data/contexts/obs-browser-sources.org | 75 ++ @kyonax_on_tech/data/contexts/quick-note.org | 2 + @kyonax_on_tech/sources.js | 31 + .../sources/hud/context-screen.vue | 599 +++++++++++++++ NOTICE | 13 + package-lock.json | 666 +++++++++++++++++ package.json | 2 + src/app/fonts/Geomanist/GeomanistBold.ttf | Bin 0 -> 11224 bytes src/app/fonts/Geomanist/GeomanistItalic.ttf | Bin 0 -> 26576 bytes src/app/fonts/Geomanist/GeomanistRegular.ttf | Bin 0 -> 23296 bytes src/app/scss/abstracts/_mixins.scss | 130 ++++ src/app/scss/abstracts/_theme.scss | 20 +- src/app/scss/base/_typography.scss | 4 + src/shared/brand-loader.js | 57 ++ src/shared/brand-loader.test.js | 62 +- src/shared/components/ui/chip.vue | 25 +- src/shared/components/ui/org-content.test.js | 151 ++++ src/shared/components/ui/org-content.vue | 698 ++++++++++++++++++ src/shared/composables/composables.test.js | 73 +- src/shared/composables/use-context-channel.js | 228 ++++++ src/shared/utils/highlight.js | 68 ++ src/shared/utils/org.js | 158 ++++ src/shared/utils/org.test.js | 184 +++++ src/views/components/elements/card.vue | 68 +- .../components/modals/context-control.vue | 533 +++++++++++++ src/views/components/modals/preview.vue | 4 +- src/views/components/sections/setup.vue | 2 +- src/views/components/sections/sources.vue | 8 +- vite.config.js | 67 +- 29 files changed, 3904 insertions(+), 24 deletions(-) create mode 100644 @kyonax_on_tech/data/contexts/obs-browser-sources.org create mode 100644 @kyonax_on_tech/data/contexts/quick-note.org create mode 100644 @kyonax_on_tech/sources/hud/context-screen.vue create mode 100644 src/app/fonts/Geomanist/GeomanistBold.ttf create mode 100644 src/app/fonts/Geomanist/GeomanistItalic.ttf create mode 100644 src/app/fonts/Geomanist/GeomanistRegular.ttf create mode 100644 src/shared/components/ui/org-content.test.js create mode 100644 src/shared/components/ui/org-content.vue create mode 100644 src/shared/composables/use-context-channel.js create mode 100644 src/shared/utils/highlight.js create mode 100644 src/shared/utils/org.js create mode 100644 src/shared/utils/org.test.js create mode 100644 src/views/components/modals/context-control.vue diff --git a/@kyonax_on_tech/data/contexts/obs-browser-sources.org b/@kyonax_on_tech/data/contexts/obs-browser-sources.org new file mode 100644 index 0000000..38ebe23 --- /dev/null +++ b/@kyonax_on_tech/data/contexts/obs-browser-sources.org @@ -0,0 +1,75 @@ +#+TITLE: How OBS Browser Sources Work +#+SUBTITLE: From WebSocket to Live HUD +#+DESCRIPTION: A walkthrough of how RECKIT mounts Vue overlays into OBS scenes via the browser-source plugin, with focus on the audio-meter pipeline. +#+TAGS: :obs:vue:websocket:audio:performance: + +#+begin_marquee +WS over REST +OBS scene tree +Audio meter perf +Direct DOM writes +Float32Array hot path +#+end_marquee + +* Mount Path + +OBS Studio loads each browser source as an isolated Chromium tab. The source URL points at a Vite dev server running locally; once mounted, the source can call into OBS WebSocket on port =4455= to read live state. + +The composable is a *module-level singleton* — N consumers share one subscription. + +#+begin_src javascript +import { useObsWebsocket } from '@composables/use-obs-websocket.js'; + +const { obs } = useObsWebsocket(); +obs.on('InputVolumeMeters', (event) => { + // 50 Hz audio levels — drive the HUD off this event +}); +#+end_src + +* Audio-Meter Hot Path + +Bar transforms are written *directly to the DOM* via template refs — Vue reactivity is bypassed in the hot path. + +#+begin_src scss +.audio-meter .bar { + transform: scaleY(var(--level)); + transform-origin: bottom; + contain: layout paint; +} +#+end_src + +#+RESULTS: +: 0 broad CSS filters +: 0 per-frame allocations +: 60 fps held on the test rig + +** Constraints (= must hold per session-file §1.14) + +- =transform= / =opacity= animations only +- preallocated typed arrays (=Float32Array=) +- precomputed =JITTER_TABLE= for jitter +- write-threshold skip: =Math.abs(next - last) < 0.01= → no DOM write + +** Checklist before merging + +- [X] no =cyberpunk-glow= mixin reintroduction +- [X] singleton composables +- [ ] =composables.test.js= asserts singleton identity +- [-] OBS smoke test (partially complete) + +* Why It Works + +| Technique | Cost saved | Reference | +|--------------------------+---------------------------+----------------------| +| Singleton composables | N→1 event subscriptions | session §1.14.5 | +| Direct DOM writes | Vue reactivity bypass | == | +| =transform: scaleY()= | layout / re-rasterization | session §1.14.3 | +| =contain: layout paint= | paint isolation | session §1.14.4 | + +#+begin_quote +fps wins. Every other rule below ranks under it. +#+end_quote + +----- + +See [[https://obsproject.com/docs/sources][OBS source documentation]] for the broader context. diff --git a/@kyonax_on_tech/data/contexts/quick-note.org b/@kyonax_on_tech/data/contexts/quick-note.org new file mode 100644 index 0000000..27644fe --- /dev/null +++ b/@kyonax_on_tech/data/contexts/quick-note.org @@ -0,0 +1,2 @@ +#+TITLE: Quick Note +#+DESCRIPTION: Minimal context — title + description, no marquee block, no body. Marquee row collapses to zero height. diff --git a/@kyonax_on_tech/sources.js b/@kyonax_on_tech/sources.js index 141bda6..0dfce01 100644 --- a/@kyonax_on_tech/sources.js +++ b/@kyonax_on_tech/sources.js @@ -43,6 +43,37 @@ export default [ triggers: [], status: 'ready', }, + { + id: 'context-screen', + type: 'hud', + brand: '@kyonax_on_tech', + name: 'CONTEXT-SCREEN', + description: + 'News-style **lower-third HUD** for the **CONTEXT scene**. ' + + 'Surfaces the **video title**, a short **description**, ' + + 'a **talking-point marquee**, and a **toggleable right ' + + 'sidebar** that renders the **full parsed .org context** ' + + '(headlines, lists, **checklists**, **tables**, **code ' + + 'blocks**, **#+RESULTS evaluations**, links). Authored ' + + 'per video as a small **.org file** in the brand folder.', + use_cases: [ + 'video context overlay', + 'news-style lower third', + 'topic outline', + 'code walkthrough sidebar', + 'talking-points reel', + ], + path: '/@kyonax_on_tech/context-screen', + width: CANVAS_WIDTH, + height: CANVAS_HEIGHT, + fps: TARGET_FPS, + requires: [ + '.org files at @kyonax_on_tech/data/contexts/', + 'BroadcastChannel-capable browser (OBS Chromium 32+)', + ], + triggers: [], + status: 'ready', + }, { id: 'item-explain', type: 'animation', diff --git a/@kyonax_on_tech/sources/hud/context-screen.vue b/@kyonax_on_tech/sources/hud/context-screen.vue new file mode 100644 index 0000000..18a2dd6 --- /dev/null +++ b/@kyonax_on_tech/sources/hud/context-screen.vue @@ -0,0 +1,599 @@ + + + + + + + diff --git a/NOTICE b/NOTICE index 3595c0a..4ab95c6 100644 --- a/NOTICE +++ b/NOTICE @@ -5,3 +5,16 @@ ORCID: https://orcid.org/0009-0006-4459-5538 This product includes software developed by Cristian D. Moreno (@Kyonax). Attribution to the original author must be preserved in all copies, modifications, and derivative works. + +Third-Party Fonts +================= + +SpaceMono Nerd Font — derived from Space Mono by Colophon Foundry +(SIL Open Font License 1.1) and the Nerd Fonts patcher project +(MIT License). See https://www.nerdfonts.com/ for full attribution. + +Geomanist — by Atipo Foundry (atipofoundry.com). Used in RECKIT under +the same licensee as the sibling project Kyonax/kyo-web-online, which +ships the same font under the same authorship. License-of-record: +verify directly with Atipo Foundry for any redistribution beyond the +Kyonax-owned project graph. diff --git a/package-lock.json b/package-lock.json index 0f07bb1..5201e6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "dependencies": { "dayjs": "^1.11.20", "obs-websocket-js": "^5.0.6", + "shiki": "^4.0.2", + "uniorg-parse": "^3.2.1", "vue": "^3.5.13", "vue-router": "^4.5.0" }, @@ -1543,6 +1545,106 @@ "dev": true, "license": "MIT" }, + "node_modules/@shikijs/core": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz", + "integrity": "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.0.2", + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz", + "integrity": "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz", + "integrity": "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz", + "integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.2.tgz", + "integrity": "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.2.tgz", + "integrity": "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", + "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -1575,6 +1677,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1589,6 +1700,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/node": { "version": "25.6.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", @@ -1606,6 +1726,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/whatwg-mimetype": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", @@ -1918,6 +2044,12 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-vue": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", @@ -2426,6 +2558,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2592,6 +2734,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -2619,6 +2771,26 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2694,6 +2866,16 @@ "dev": true, "license": "MIT" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", @@ -2913,6 +3095,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2924,6 +3115,19 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -3716,6 +3920,12 @@ "node": ">=12.0.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4178,6 +4388,42 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -4185,6 +4431,16 @@ "dev": true, "license": "ISC" }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4531,6 +4787,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4911,6 +5179,116 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -5235,6 +5613,23 @@ ], "license": "MIT" }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5511,6 +5906,16 @@ "node": ">= 0.8.0" } }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -5675,6 +6080,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -5985,6 +6414,25 @@ "node": ">=8" } }, + "node_modules/shiki": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz", + "integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.0.2", + "@shikijs/engine-javascript": "4.0.2", + "@shikijs/engine-oniguruma": "4.0.2", + "@shikijs/langs": "4.0.2", + "@shikijs/themes": "4.0.2", + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -6090,6 +6538,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -6288,6 +6746,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", @@ -6434,6 +6906,26 @@ "node": ">=14.0.0" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -6604,6 +7096,128 @@ "dev": true, "license": "MIT" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/uniorg": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/uniorg/-/uniorg-1.4.0.tgz", + "integrity": "sha512-1QYkebPJCmSNyA3bZDAqxgEJb435XYz8fQE4Mc5gxQ86tyG7EV/pyUcIrSizCkmph/ioteXUErByIjLGho/zBA==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@types/unist": "^3.0.0" + } + }, + "node_modules/uniorg-parse": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uniorg-parse/-/uniorg-parse-3.2.1.tgz", + "integrity": "sha512-4HKJj+bv3JfWxqGDqc3xJ3WLwGNg34kQS58DRBDT5er/lWKqyf17gvxuSrrw1deXqtmevFwsSYCdHTv8iL/cxQ==", + "license": "GPL-3.0-or-later", + "dependencies": { + "unified": "^11.0.4", + "uniorg": "^1.3.0", + "unist-builder": "^4.0.0", + "vfile": "^6.0.1", + "vfile-location": "^5.0.2" + } + }, + "node_modules/unist-builder": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-4.0.0.tgz", + "integrity": "sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -6674,6 +7288,48 @@ "spdx-license-ids": "^3.0.0" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", @@ -7199,6 +7855,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 84acb83..1b0c129 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,8 @@ "dependencies": { "dayjs": "^1.11.20", "obs-websocket-js": "^5.0.6", + "shiki": "^4.0.2", + "uniorg-parse": "^3.2.1", "vue": "^3.5.13", "vue-router": "^4.5.0" }, diff --git a/src/app/fonts/Geomanist/GeomanistBold.ttf b/src/app/fonts/Geomanist/GeomanistBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7e85856f224465165ec79141859624349d5e5186 GIT binary patch literal 11224 zcmai4349Y({(tXHlk}!(l7_alO)^Q-^hn#JNoY$;DU@>ALb(c*O6dWVLQhTsQBf4+ zST6(tcg6o!DYC8yDt6gjmsMF24;1i#1r(3{u>MsPSN?SJ|Gt^Dl!ER|XXeeD-|zQ+ z?{~kK5=sb(Bx{L4=9G>q8BOjcpAsUzjNX*eiRF{WJXSRg?`H|oJW)D%YVk*pf0{!` z+B$r%D4(3^y62-ieHOpJbeliyCgYj-!T(hYD#PHDX2uX?}MEGM%b9+bV=i&DgVmXZO z7m12s9Qq&D4W85_goKkpgt)b4t)MgOj9RM0QwX9zT-ht_dvmYUvUhJHowAp8_l%+& ziC_A_PbG;FKk@f`h&v-b|0N=UnudfC5@Siu@w#I4VN?|UR*{!ikslVZEg}NX{0g>3 zGSaVs0Y2Fsz=$VF;6&rri1H=+L=7&VTfW>Hw@<*uCR(S)PK&`MZV;<+RmRSU!DZcG zt+wursg7aY`&KPl)rA9(ef#!Frxro@lxY2X=?_dI8H5-;PQmU=q;8id#~I|w@%lV& zLn1Y5oXny&=o1BlUac{l&YX}Luh}wasHHHiXhKn1?bO<_leL>ev^H1buxZnV&6zT7 zyvpuz#*~^fvJJL)M^=Sr@}ltEyg^m25&GD{@%lK2Evqtra)-7YQjYOo5^U@Uq#Q|N ziIrp$5<*2P=wsY2uO}zjqSoYi-H?@9v?SA;_Oi=8-~N_G#U&YI6O*zMSJh7_YiPKo ztWJ7=U=LRhuFnUXOoNR{Cga$;x(90K(l@Vv2QM-HFW5$K6-O*2oe-6skIp5+Z{WtD z*BHGXH|{o<&ko_l2zqqnB#Ra`8!~F#qSc#obFy(}^rfdalAzgH% zjop{vD_>mhEi2~_UpdR1oi=M$+U&H{>h!9r^x0{gXORqNK`e{gISdH+Zi5J9v1?;j z*T&214jfqb(n}mJ$v>TZMGkVmG8R&wllF>Mt+5-^gKVjJjg`Kg)WzyAU%V^NCq?t)cGnrA^{M>W2?YXOGs;bUZfqo>I79kfko?1~0 zVNmhuQ7QMyMrAN;b^^uDza2jIiY8nbJ!fmL$hrcRc*i3ez)< zcS9Dj;Is%7LrDZ7Hm)GEhq|?HyT;9m$~vWEH0ns%kL%X$+_`fF?UY(R`iSd{^AV-+ zK`k1)&TKGi6n{`#lKZ%#n)JcgbXm5Wm z+gRb7w}PgpCYe&~=A={@SbU<{oER_OAf4e9BNZ}FAR-0qO0?))z8oh*`~fQ?|BG;v zZj=qZ_nK@Nd=tLJA{0;Q%~mXswG^+)@8?@IR!&`3ihgOffbU7`^^%MNX6Y~0Gn6v-wQL_dbM;QKnbg9?{O z&q+!29G$jq-H~JnH=uKIfktFWiyy2;59&D)eHf(<5ho4?P^w0S-rk`|}O{uun6fK@UwIwO`bMKg< zb?cwGXXBGO)5e^A@#U}H+S7W^*JHAr$>wY>3&@=9hKFbnB@iPtqRy=o1-pj+lR0n$xefBx8|9l!+3aOmF$fHPjtJC zJX#=#%fg|XI^TNti6zss#@Sk*D;?V zfYU}&rO4oOa-tmwDn5_sVXu$cvS|BzZ~yMqxlPeC3uZO8OwBEeTtV-XPI+>VJh}bd zl`VsFTBqOIo)B5MMH!!MRygIkil2z>TDH0CN>|qvS!2=CHz*~(r6_;pnUSvnkDRIl z?AKj#QtQSSo&O3g#TR4=)DEz?K$O?g(xufdr#?PD$pqjlwKKxDsl*}li@@kBfCH=1 zX?1SYa{?!N^v}{28g}?7h)dtoNa>H#gEUV10)PsREL)W^#N6>nR-%>}ckOuc%>F-|c`}ziAbCKzguaRr{4E8CJpdf&z|4ig zeoCIA&q$L2UPhnm8O3d#Th`CeL_)>&xYLAP|9QpCGKU&?v}_(ORlq%&(UtkTKdsK``usQd1BD-#pAj&05zl8)1^GtOo1YcVu ztZVhn^d<#gTScs4M=V1#WBV@j*|4K)MBDJLo#ETp(mzO(a*K=e=zghU%}!v# z`q(SLMC3Dz0yi?+eXyr{_wH`kN+kUrgOXs_;>WTCiJ33pI28T_Hn+9)wF56T-nQem z#+MGzm!&bl(b0eNi`y9GD@t}RFcCj=MxR^IiFU+P;oYT6k9>0G(7JVp&V0hUPn?kc zM|%9+Ia)(!0f)!nD&SyLv#1r^9FN|!_rzZoF8d%`s5~jE z83Q%5!k%-~E4|P3(i?QxBKq>SCaIKjVx@&F4$z>E(8sx;wJ+_csoC)|c)s)OS-J+~ zh#oq=$l|aPH6#1-g^w{%&!QP)(2DZ1$g334qD9|4duWEkUNow3ZpZC)4~L}QQqkI( zQfN;}O59U^t4Yk4#qzil5 zCswyjH`P}D*s`UMZ(DEJmzf0hP*kXdmaz}{Q^yk%LY|2Am#j7ea^&|F6`ej`M^!{} z_@EJNaM*fjCB0|;pqff{KjWMk{6(w?oW&CxI2$-ieI#m2Iri)1RW4UL-QK5)sGZKq zb%l+U*(DQJsS0!41&~R6L_j6c4#_pTAvZ2#t%>G)$Cf~4;5Q3{MnJwXuxzbN6BZRv zI`$g0v>`dklG523Ju)M+#AZ;_T&q3JB)!3oUyV;OWtQeAOQ$*C;Fg?a=aIXR9eDQg z$~8Kw^OKz`Cwe>+y)2^V5{odo?J4e@GQ#{P{6#WGD32yOtcdL%L{(2e#7}JzVa0vO z-yIS*v7t=UOF`k?3Eq+6C?Y5p#|Sb$W_tRi!w+_LZe|hRN}v7sBRek5JFd)>oCGwo zE6>7No$NS_L8c;DA@M{|0pHxj6cKPw3%Hmf8QRln9%kv>lUy|`!+UGx%uY*Uf=F}W z)wyDd-3p;Zct@6$G)$NF@E#tNW%-7I`Y+124)RUz+@l?RO}?pSnbdXig*UMZ&5s3J;J?l%d@Gj4;IR!2rOb?$MnxV>z{S1e-mWWYuu((DH;9MBk8 z00;KY0)=T{+w4?HV|;Z^QKr>X=&`m{t}JeHIFntarLL6`Uh6DpLZo3(yme&8xXPJR z9dXe{gQ?V0UJd$T{{PTowyMvk`Fg--P@_&Ppxn7H%$PAk8y1sVQ|orKRn6=FnoGB+ z>snt~-^?w5?{{dh7J~)A%3$jCxIn()Hw@*=%Beth?X2TIF#mX{uQkUFH3 zz*H137C+$SFV;xQ0$YB%7J1m$!$RmTz!Y#@m$|lc+=f)mOqj|Aj7qfQS80&%<{(Lvp zsIOco-ykQRt#=D8;3|~w7xh~+>9Bv>f8CV*Ox?z7YbRFv8%RN3UjRFd?@#q#VF(<# zZ`Fad>lT1&WxqMQfaU({#PU9y#^MIoyyEi!$ z&#+IfKu4VBVmQ z$o8$ou@5m1UL)Qtl5v9{5Y0}@vbb49gKPx8!zDz^yTyaJ&Gznb4SXs)RiDV4Im%tx zP3FJZC_7;J6^I&y)RWOX7U4iNLl5sO!lfK`>?ZNEh_sXEiA0NOIjyIw=uY}9JxWi} zud!H=7g*Z=?r-<Sl|r~jz*TF{Ha&v*}( zrGWj(JVMo!n5(Azw;5&v+Lthi#bJ4FMHmdQT0A1eB5+;%? z=EF5lhR@!QC&7=MB=UL5a{>Q-{Bqtn4}4q= zN$H~vIzl#9jL;+d#*ds8^p^jqe@)PfvUa(edXIX-=xbOf@^x`H+ko!-l_xz-T;vKj zb``1vJh*mwtM6!8(bkR^6S<;{A^c2{r<4&;_hZus-#8Zc7mlgN9US2qM4rMkS^lm@ zZRI30Ni|tX){wuFBjj!J9yv}vCg;f4rQ`B(dw`rG|2{$~FIg-1~Q??)nC+y|WvaLzx!M-u)E13ws| z!tg*FnG#V#=??--O`xs9?pkx8t?t*aA$GDV&>svP>qCZFRqX z4Jo6q1p0$XDANSmA^qB+{l*QGV}L?Gyk9$tL^DSKR}0_DAK46xS}<>Zhbg7jVRB_< zyG?Ufm})u}v@|>Q;n&kt){MiQBB)IQ*%Rr4Vn0| zWy_ojSM}|Ec$^{`2_?6X6{L+UAoIw4(gBlAA+^{-Gr^g%ph!1ba{*z3X>~?vCV)QiOYX!c|BTLY(0me1@d7$)b zc>H@WcQ?XikY4)JWKP;KZZk$VVKz?8iJv!OnfkCyGM{|DCUDF7sskN9gEq1dea)l+ z_iNlb`?Z%5e)jKi0=NApaag@|6*FWh`ovc>UE&>GUkXM)JcWY5Q{Og?y|=DUh~y{5sp zU>G2U(CY$lp_oTHp-nX%i}8)eX95}&54y0vjrS?hhVN!7kvm-I2zn9_%VlUE`4`(D z6wxTQfySYl*+5PFF2qqUEuv*~xlkk&p?3|vm$&F{R6wuMH-$15%F?leIbWCy=t{N~ z&%MH2oR!Epud%c28+KJt3sFKG#}mr<7hx{O^+79I5`BYKxE^f3paXZG{JHcCu!0%^ zlN<%v_e#DDQs*+~60ek{JsY{@R>(VqMEft|cfyPKpPq{(*ME%U`OlMlTvPBq4Ob=F z43>f&6TDFwRs}pgiSi#r-(lEIBB=u=pa4-J=e{WaDU7B^|9N0?$SiytfomMD3AoDq zmqGh5srG+KYVckM7)rwB(KzgQTqLh=SevzRp2=V*Hm1}j1QAqwCg}KSf*u7sXL66V(aA3aYfpcD-Z#kX{-gltR`922fDQNN8x^RsE z1c$CdyBgBrJW$X&4+{^M<$Ve;mjQDbv*R+CXO1ttrvg2^mt$OnH*h>I@BZJWfwwwv zDD*+2g2Nk^QZGK&;xU<$(&D4AzS-k{$NW3-e*~wl~JY1Y}t|3nGBCJ?1+cBrZ z=YA1%!g1%{xTY`To-X^D7Nb`pSu~M5$wOo}*+&kcu0BJ)C6~!nY?bI~DqywH=U~|j zGUV9f3&fj>KrE?51fhsedW;hZZU#X^ymcOyuZJx~!on=jOd7rw!0N_=k03xDCLaIq zi5G7}@kPP#080S$F?fz%#z&0<$U%VOy#=66@pcTlQN}ti_j{m$0$9nU0PXYmZ;*@V zxr`pJ&55#xI2HVl7otMW_$t{lM!FUQ+r+{fe52G(|dC^Z<5#j{)WKjQI z^dH7`3XyEU0qkErK<}o9Oba^|pne$VdGK}9V<3M2_ksLi{(+;98up2=9UPwH_kQht zUW2%_?IXsp!00l|EKF-JEo gm{$YnYdM-{aQ>Sm|9wb#27wl$4NksGf!|2~A1KE2Z~y=R literal 0 HcmV?d00001 diff --git a/src/app/fonts/Geomanist/GeomanistItalic.ttf b/src/app/fonts/Geomanist/GeomanistItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1e75211be151c377a8cc3ce2fc73dd5f9bbb66bf GIT binary patch literal 26576 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 WgJXoe3jJHbli;`Dg9J2=?Ee8&OTusf literal 0 HcmV?d00001 diff --git a/src/app/fonts/Geomanist/GeomanistRegular.ttf b/src/app/fonts/Geomanist/GeomanistRegular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..63d0aa6062e07d60106cda65de1cfd3cd6bcdf53 GIT binary patch literal 23296 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 literal 0 HcmV?d00001 diff --git a/src/app/scss/abstracts/_mixins.scss b/src/app/scss/abstracts/_mixins.scss index cf8d41b..565b762 100644 --- a/src/app/scss/abstracts/_mixins.scss +++ b/src/app/scss/abstracts/_mixins.scss @@ -37,3 +37,133 @@ position: absolute; } +// Ad-hoc color helpers for cases where a dedicated --clr-X-XX token does +// not exist. Always prefer a named token (e.g. var(--clr-primary-100-06)) +// when available — these helpers are for one-off variants only. Each +// produces a runtime color-mix() expression compatible with CSS custom +// properties. +// +// Usage: +// background: alpha-tint(--clr-primary-100, 6%); +// color: darken(--clr-primary-100, 30%); +// border: 1px solid lighten(--clr-neutral-100, 20%); +@function alpha-tint($color-var, $percent) { + @return color-mix(in srgb, var(#{$color-var}) #{$percent}, transparent); +} + +@function darken($color-var, $by) { + @return color-mix(in srgb, var(#{$color-var}) #{100% - $by}, black); +} + +@function lighten($color-var, $by) { + @return color-mix(in srgb, var(#{$color-var}) #{100% - $by}, white); +} + +// Adds hard-edge corner squares to a container — a HUD/cyberpunk +// treatment that pairs with a 1px solid border. Expects the consumer +// to set its own background-color + border; this mixin only paints +// the corner markers via background-image. +// +// Usage: +// .container { +// background-color: var(--clr-neutral-500); +// border: 1px solid var(--clr-border-100); +// @include corner-dots(var(--clr-border-100)); // all 4 +// @include corner-dots(var(--clr-border-100), 4.5px, tl bl); // left only +// } +// +// Parameters: +// $color — solid CSS color (e.g. var(--clr-border-100)) +// $size — square edge length, default 4.5px +// $corners — space-separated list of corner keys to draw: +// tl top-left +// tr top-right +// bl bottom-left +// br bottom-right +// Default: tl tr bl br (all four). +// +// Implementation: stacked linear-gradient background-image layers, one +// per requested corner. background-origin: border-box places position +// 0 0 / 100% 100% at the OUTER border edge, so each square sits flush +// with the corner intersection rather than inset by the border width. +@mixin corner-dots($color, $size: 4.5px, $corners: tl tr bl br) { + $position-map: ( + tl: 0 0, + tr: 100% 0, + bl: 0 100%, + br: 100% 100%, + ); + $images: (); + $positions: (); + + @each $corner in $corners { + $pos: map.get($position-map, $corner); + @if $pos { + $images: append( + $images, + linear-gradient(#{$color}, #{$color}), + $separator: comma + ); + $positions: append($positions, $pos, $separator: comma); + } + } + + @if length($images) > 0 { + background-image: $images; + background-position: $positions; + background-size: #{$size} #{$size}; + background-repeat: no-repeat; + background-origin: border-box; + background-clip: border-box; + } +} + +// Pseudo-element corner squares — half outside, half inside the +// container so the square sits CENTERED on the corner intersection. +// +// IMPORTANT: the consumer must be a positioned element (any of +// `position: relative | absolute | fixed | sticky`). The mixin does +// NOT set `position: relative` itself — that would clobber existing +// `position: absolute` consumers (e.g. a sidebar pinned to the right +// edge). The consumer must also NOT use `contain: paint`, which would +// clip pseudo-elements rendered outside the box. +// +// Each mixin handles ONE corner via either ::before or ::after, so a +// surface needing two corners (e.g. strip = TL + TR) calls both +// `corner-square-tl` and `corner-square-tr`. +// +// Usage: +// .container { +// position: relative; // or absolute / fixed +// border: 1px solid var(--clr-border-100); +// @include corner-square-tl(var(--clr-neutral-50)); +// @include corner-square-tr(var(--clr-neutral-50)); +// } +@mixin corner-square-tl($color, $size: 4.5px) { + &::before { + content: ''; + position: absolute; + top: -#{$size * 0.5}; + left: -#{$size * 0.5}; + width: $size; + height: $size; + background: $color; + pointer-events: none; + z-index: 1; + } +} + +@mixin corner-square-tr($color, $size: 4.5px) { + &::after { + content: ''; + position: absolute; + top: -#{$size * 0.5}; + right: -#{$size * 0.5}; + width: $size; + height: $size; + background: $color; + pointer-events: none; + z-index: 1; + } +} + diff --git a/src/app/scss/abstracts/_theme.scss b/src/app/scss/abstracts/_theme.scss index 6ff67aa..0c36400 100644 --- a/src/app/scss/abstracts/_theme.scss +++ b/src/app/scss/abstracts/_theme.scss @@ -35,14 +35,32 @@ } --font-mono: "SpaceMono", "Roboto Mono", "Courier New", Courier, monospace; + --font-display: "Geomanist", "Inter", "Helvetica Neue", Arial, sans-serif; --canvas-width: 1920px; --canvas-height: 1080px; --hud-bar-offset: 24px; --hud-bar-gap: 24px; - --clr-primary-100-80: color-mix(in srgb, var(--clr-primary-100) 80%, transparent); + --surface-bg: linear-gradient(135deg, hsl(0 0% 2%) 0%, hsl(0 0% 7%) 50%, hsl(0 0% 14%) 100%); + + --motion-sidebar-ms: 280ms; + --motion-strip-ease: cubic-bezier(0.22, 1, 0.36, 1); + --motion-peek-pulse-s: 2.5s; + --motion-marquee-s: 90s; + + --clr-primary-100-02: color-mix(in srgb, var(--clr-primary-100) 2%, transparent); + --clr-primary-100-03: color-mix(in srgb, var(--clr-primary-100) 3%, transparent); + --clr-primary-100-04: color-mix(in srgb, var(--clr-primary-100) 4%, transparent); + --clr-primary-100-06: color-mix(in srgb, var(--clr-primary-100) 6%, transparent); + --clr-primary-100-10: color-mix(in srgb, var(--clr-primary-100) 10%, transparent); + --clr-primary-100-14: color-mix(in srgb, var(--clr-primary-100) 14%, transparent); --clr-primary-100-40: color-mix(in srgb, var(--clr-primary-100) 40%, transparent); + --clr-primary-100-80: color-mix(in srgb, var(--clr-primary-100) 80%, transparent); + + --clr-neutral-50-02: color-mix(in srgb, var(--clr-neutral-50) 2%, transparent); + --clr-neutral-50-04: color-mix(in srgb, var(--clr-neutral-50) 4%, transparent); + --clr-neutral-50-12: color-mix(in srgb, var(--clr-neutral-50) 12%, transparent); --hud-halo: drop-shadow(0 0 1px rgba(0, 0, 0, 0.9)) diff --git a/src/app/scss/base/_typography.scss b/src/app/scss/base/_typography.scss index d385e9f..226162b 100644 --- a/src/app/scss/base/_typography.scss +++ b/src/app/scss/base/_typography.scss @@ -10,3 +10,7 @@ @include mixins.font-face("SpaceMono", "SpaceMono/SpaceMonoNerdFont-Bold", 700, normal); @include mixins.font-face("SpaceMono", "SpaceMono/SpaceMonoNerdFont-Italic", 400, italic); @include mixins.font-face("SpaceMono", "SpaceMono/SpaceMonoNerdFont-BoldItalic", 700, italic); + +@include mixins.font-face("Geomanist", "Geomanist/GeomanistRegular", 400, normal); +@include mixins.font-face("Geomanist", "Geomanist/GeomanistBold", 700, normal); +@include mixins.font-face("Geomanist", "Geomanist/GeomanistItalic", 400, italic); diff --git a/src/shared/brand-loader.js b/src/shared/brand-loader.js index e93cf2e..44cfc3d 100644 --- a/src/shared/brand-loader.js +++ b/src/shared/brand-loader.js @@ -7,8 +7,19 @@ * root and merges their metadata + source registries into a * single runtime object. Uses import.meta.glob for static * analysis by Vite's bundler. + * + * Also discovers per-brand .org context files at /@/data/ + * contexts/.org, parses each via uniorg-parse at glob-time, + * and exposes them as a CONTEXTS map: { brand → slug → { raw, + * parsed } }. Cached AST means the sidebar renderer has zero + * parse cost per frame (Plan #context-screen, decision D3). */ +import { OrgSchemaError, parseOrg } from '@shared/utils/org.js'; + +const BRAND_HANDLE_PATTERN = /^\/(@[^/]+)\/data\/contexts\//; +const ORG_FILENAME_PATTERN = /([^/]+)\.org$/; + const brand_modules = import.meta.glob( '/@*/brand.js', { eager: true, import: 'default' }, @@ -25,8 +36,14 @@ const animation_components = import.meta.glob( ); const scene_components = import.meta.glob('/@*/sources/scene/*.vue'); +const context_modules = import.meta.glob( + '/@*/data/contexts/*.org', + { eager: true, query: '?raw', import: 'default' }, +); + export const BRANDS = Object.values(brand_modules); export const SOURCES = Object.values(source_modules).flat(); +export const CONTEXTS = buildContextsMap(context_modules); export function getBrand(handle) { return BRANDS.find((b) => b.handle === handle) || null; @@ -49,3 +66,43 @@ export function resolveComponent(source) { return components[key] || null; } + +export function getContexts(handle) { + return CONTEXTS[handle] || {}; +} + +function buildContextsMap(modules) { + const map = {}; + for (const path of Object.keys(modules)) { + const brand_match = path.match(BRAND_HANDLE_PATTERN); + const slug_match = path.match(ORG_FILENAME_PATTERN); + if (!brand_match || !slug_match) { + continue; + } + const brand_handle = brand_match[1]; + const slug = slug_match[1]; + const raw = modules[path]; + + let parsed = null; + let parse_error = null; + try { + parsed = parseOrg(raw); + } catch (error) { + if (error instanceof OrgSchemaError) { + parse_error = { + name: error.name, + message: error.message, + missing_keys: error.missing_keys, + }; + } else { + parse_error = { name: 'Error', message: String(error) }; + } + } + + if (!map[brand_handle]) { + map[brand_handle] = {}; + } + map[brand_handle][slug] = { raw, parsed, parse_error }; + } + return map; +} diff --git a/src/shared/brand-loader.test.js b/src/shared/brand-loader.test.js index 137df52..00920d7 100644 --- a/src/shared/brand-loader.test.js +++ b/src/shared/brand-loader.test.js @@ -7,7 +7,7 @@ * Validates brand metadata and web source schema. */ -import { BRANDS, SOURCES } from '@shared/brand-loader.js'; +import { BRANDS, CONTEXTS, getContexts, SOURCES } from '@shared/brand-loader.js'; import { describe, expect, it } from 'vitest'; const REQUIRED_SOURCE_FIELDS = [ @@ -155,3 +155,63 @@ describe('Brand loader — SOURCES', () => { }, ); }); + +describe('Brand loader — CONTEXTS', () => { + const BRAND_KOT = '@kyonax_on_tech'; + + it('exposes CONTEXTS as a brand-keyed object', () => { + expect(typeof CONTEXTS).toBe('object'); + expect(CONTEXTS).not.toBe(null); + }); + + it(`discovers at least one context under ${BRAND_KOT}`, () => { + const kot = CONTEXTS[BRAND_KOT] || {}; + expect(Object.keys(kot).length).toBeGreaterThan(0); + }); + + it('every context has { raw: string, parsed: object|null }', () => { + for (const brand of Object.keys(CONTEXTS)) { + for (const slug of Object.keys(CONTEXTS[brand])) { + const entry = CONTEXTS[brand][slug]; + expect(typeof entry.raw).toBe('string'); + expect(entry.raw.length).toBeGreaterThan(0); + expect( + entry.parsed === null || typeof entry.parsed === 'object', + ).toBe(true); + } + } + }); + + it('every parsed context (no parse_error) has the locked Q5 schema', () => { + for (const brand of Object.keys(CONTEXTS)) { + for (const slug of Object.keys(CONTEXTS[brand])) { + const entry = CONTEXTS[brand][slug]; + if (entry.parse_error) { + continue; + } + expect(typeof entry.parsed.title).toBe('string'); + expect(typeof entry.parsed.description).toBe('string'); + expect(Array.isArray(entry.parsed.tags)).toBe(true); + expect(Array.isArray(entry.parsed.marquee_items)).toBe(true); + expect(Array.isArray(entry.parsed.body_ast)).toBe(true); + } + } + }); + + it('slugs are unique within each brand', () => { + for (const brand of Object.keys(CONTEXTS)) { + const slugs = Object.keys(CONTEXTS[brand]); + expect(new Set(slugs).size).toBe(slugs.length); + } + }); + + it('getContexts(handle) returns the per-brand map', () => { + const kot = getContexts(BRAND_KOT); + expect(typeof kot).toBe('object'); + expect(Object.keys(kot).length).toBeGreaterThan(0); + }); + + it('getContexts(missing-handle) returns empty object', () => { + expect(getContexts('@no-such-brand')).toEqual({}); + }); +}); diff --git a/src/shared/components/ui/chip.vue b/src/shared/components/ui/chip.vue index a46b061..7a82fc7 100644 --- a/src/shared/components/ui/chip.vue +++ b/src/shared/components/ui/chip.vue @@ -16,10 +16,20 @@ variant — "solid" (default) | "overflow" (dashed border + dimmed — used for "+N more" indicators at the end of a list). + shape — "pill" (default) | "square". Controls border-radius. + Both currently map to 0 (the chip ships sharp by + default per Plan #context-screen D15); "pill" is + reserved for a future redesign that may introduce + a radius. Consumers under a sharp-corner mandate + (context-screen surfaces) should pass shape="square" + to lock 0-radius regardless of future chip CSS. --> @@ -33,9 +43,15 @@ const props = defineProps({ default: 'solid', validator: (v) => ['solid', 'overflow'].includes(v), }, + shape: { + type: String, + default: 'pill', + validator: (v) => ['pill', 'square'].includes(v), + }, }); const variant_class = computed(() => `ui-chip--${props.variant}`); +const shape_class = computed(() => `ui-chip--${props.shape}`); diff --git a/src/shared/components/ui/org-content.test.js b/src/shared/components/ui/org-content.test.js new file mode 100644 index 0000000..9a68904 --- /dev/null +++ b/src/shared/components/ui/org-content.test.js @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2026 Cristian D. Moreno — @Kyonax + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. See LICENSE or https://mozilla.org/MPL/2.0/ + * + * Tests for — recursive AST renderer (Plan #context- + * screen D10 + D11). Mounts the parsed body AST from a fixture + * .org string and asserts each major node type produces the + * expected DOM. No v-html, no innerHTML — verified by template + * walk only. + */ + +import { collectBodyNodes } from '@shared/utils/org.js'; +import UiOrgContent from '@ui/org-content.vue'; +import { mount } from '@vue/test-utils'; +import { unified } from 'unified'; +import uniorgParse from 'uniorg-parse'; +import { describe, expect, it } from 'vitest'; + +const FIXTURE = `#+TITLE: Fixture +#+DESCRIPTION: Body fixture + +* Headline One + +A *bold* and /italic/ paragraph with =verbatim=. + +** Headline Two + +- plain item +- [ ] todo +- [X] done + +A separating paragraph between lists. + +1. ordered one +2. ordered two + +| col | val | +|-----+-----| +| a | 1 | + +#+begin_src js +const x = 1; +#+end_src + +#+RESULTS: +: 1 + +#+begin_quote +A quote +#+end_quote + +[[https://example.com][a link]] + +----- +`; + +const EXPECTED_HEADLINE_COUNT = 2; +const EXPECTED_LIST_ITEMS = 4; +const EXPECTED_TABLE_CELLS = 2; + +function buildBodyAst(raw_string) { + const ast = unified().use(uniorgParse).parse(raw_string); + return collectBodyNodes(ast); +} + +function mountFixture() { + return mount(UiOrgContent, { + props: { ast: buildBodyAst(FIXTURE) }, + }); +} + +describe('', () => { + it('renders without throwing on the empty default', () => { + const wrapper = mount(UiOrgContent); + expect(wrapper.find('.org-content').exists()).toBe(true); + }); + + it('renders headlines as h2/h3 with org-headline classes', () => { + const wrapper = mountFixture(); + const headlines = wrapper.findAll('.org-headline'); + expect(headlines.length).toBe(EXPECTED_HEADLINE_COUNT); + expect(headlines[0].element.tagName).toBe('H2'); + expect(headlines[0].classes()).toContain('org-headline--h1'); + expect(headlines[1].element.tagName).toBe('H3'); + expect(headlines[1].classes()).toContain('org-headline--h2'); + }); + + it('renders bold + italic + verbatim as semantic elements', () => { + const wrapper = mountFixture(); + expect(wrapper.find('strong.org-bold').exists()).toBe(true); + expect(wrapper.find('em.org-italic').exists()).toBe(true); + expect(wrapper.find('code.org-verbatim').exists()).toBe(true); + }); + + it('renders unordered + ordered lists with checkbox items', () => { + const wrapper = mountFixture(); + expect(wrapper.find('ul.org-list--unordered').exists()).toBe(true); + expect(wrapper.find('ol.org-list--ordered').exists()).toBe(true); + const items = wrapper.findAll('.org-list-item'); + expect(items.length).toBeGreaterThanOrEqual(EXPECTED_LIST_ITEMS); + expect(wrapper.find('.org-list-item--todo').exists()).toBe(true); + expect(wrapper.find('.org-list-item--done').exists()).toBe(true); + }); + + it('renders tables with cells', () => { + const wrapper = mountFixture(); + expect(wrapper.find('table.org-table').exists()).toBe(true); + const cells = wrapper.findAll('td.org-table__cell'); + expect(cells.length).toBeGreaterThanOrEqual(EXPECTED_TABLE_CELLS); + }); + + it('renders src-block as
 with lang label', () => {
+    const wrapper = mountFixture();
+    const src_block = wrapper.find('pre.org-src-block');
+    expect(src_block.exists()).toBe(true);
+    expect(src_block.find('code.org-src-block__code').exists()).toBe(true);
+    expect(src_block.find('.org-src-block__lang').text()).toBe('js');
+  });
+
+  it('renders #+RESULTS: as a fixed-width block with OUTPUT label', () => {
+    const wrapper = mountFixture();
+    const results = wrapper.find('pre.org-results');
+    expect(results.exists()).toBe(true);
+    expect(results.find('.org-results__label').text()).toBe('OUTPUT');
+  });
+
+  it('renders quote-block as ', () => {
+    const wrapper = mountFixture();
+    expect(wrapper.find('blockquote.org-quote').exists()).toBe(true);
+  });
+
+  it('renders external link as  with target=_blank + rel=noopener', () => {
+    const wrapper = mountFixture();
+    const link = wrapper.find('a.org-link');
+    expect(link.exists()).toBe(true);
+    expect(link.attributes('href')).toBe('https://example.com');
+    expect(link.attributes('target')).toBe('_blank');
+    expect(link.attributes('rel')).toContain('noopener');
+  });
+
+  it('renders horizontal rule as ', () => {
+    const wrapper = mountFixture();
+    expect(wrapper.find('hr.org-hr').exists()).toBe(true);
+  });
+
+  it('does not inject script tags through any node value', () => {
+    const wrapper = mountFixture();
+    expect(wrapper.html()).not.toContain('; code blocks via 
{{ value }}
+  
. + + Self-referential — uses defineOptions({ name }) so the recursive + template binding resolves. + + Props: + ast — Array of OrgNode. Default empty array. +--> + + + + + + diff --git a/src/shared/composables/composables.test.js b/src/shared/composables/composables.test.js index 7481fc7..d782813 100644 --- a/src/shared/composables/composables.test.js +++ b/src/shared/composables/composables.test.js @@ -10,9 +10,10 @@ */ import { useAudioAnalyzer } from '@composables/use-audio-analyzer.js'; +import { useContextChannel } from '@composables/use-context-channel.js'; import { useRecordingStatus } from '@composables/use-recording-status.js'; import { useSceneName } from '@composables/use-scene-name.js'; -import { describe, expect, it, vi } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; vi.mock('@composables/use-obs-websocket.js', async () => { const vue = await vi.importActual('vue'); @@ -79,3 +80,73 @@ describe('useAudioAnalyzer (singleton)', () => { expect(a.tick).toBe(b.tick); }); }); + +const post_message_spy = vi.fn(); + +class MockBroadcastChannel { + constructor(name) { + this.name = name; + } + addEventListener() {} + removeEventListener() {} + postMessage(data) { + post_message_spy(data); + } + close() {} +} + +describe('useContextChannel (singleton)', () => { + beforeAll(() => { + vi.stubGlobal('BroadcastChannel', MockBroadcastChannel); + }); + + afterAll(() => { + vi.unstubAllGlobals(); + }); + + it('returns the same instance on repeated calls', () => { + const a = useContextChannel(); + const b = useContextChannel(); + expect(a).toBe(b); + expect(a.active_slug).toBe(b.active_slug); + expect(a.sidebar_open).toBe(b.sidebar_open); + }); +}); + +describe('useContextChannel (initial state)', () => { + it('starts with active_slug=null and sidebar_open=false', () => { + const state = useContextChannel(); + expect(state.active_slug.value).toBe(null); + expect(state.sidebar_open.value).toBe(false); + }); + + it('exposes setActiveSlug, toggleSidebar, hideSidebar methods', () => { + const state = useContextChannel(); + expect(typeof state.setActiveSlug).toBe('function'); + expect(typeof state.toggleSidebar).toBe('function'); + expect(typeof state.hideSidebar).toBe('function'); + }); +}); + +describe('useContextChannel (BroadcastChannel)', () => { + it('postMessage fires when setActiveSlug runs', () => { + const state = useContextChannel(); + post_message_spy.mockClear(); + state.setActiveSlug('obs-browser-sources'); + expect(post_message_spy).toHaveBeenCalledTimes(1); + const payload = post_message_spy.mock.calls[0][0]; + expect(payload.active_slug).toBe('obs-browser-sources'); + expect(typeof payload.sidebar_open).toBe('boolean'); + }); + + it('toggleSidebar flips sidebar_open and broadcasts', () => { + const state = useContextChannel(); + const before = state.sidebar_open.value; + post_message_spy.mockClear(); + state.toggleSidebar(); + expect(state.sidebar_open.value).toBe(!before); + expect(post_message_spy).toHaveBeenCalledTimes(1); + state.toggleSidebar(); + expect(state.sidebar_open.value).toBe(before); + }); +}); diff --git a/src/shared/composables/use-context-channel.js b/src/shared/composables/use-context-channel.js new file mode 100644 index 0000000..40a8d95 --- /dev/null +++ b/src/shared/composables/use-context-channel.js @@ -0,0 +1,228 @@ +/** + * Copyright (c) 2026 Cristian D. Moreno — @Kyonax + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. See LICENSE or https://mozilla.org/MPL/2.0/ + * + * useContextChannel — singleton composable for the context-screen + * cross-page control plane (Plan #context-screen, decisions D1 + + * D4). Owns two reactive refs (active_slug, sidebar_open) and + * three actions (setActiveSlug, toggleSidebar, hideSidebar). + * Synchronises across same-origin browser tabs via the native + * `BroadcastChannel` API, and persists state to `localStorage` so + * a page reload restores the last selection. Watches sidebar_open + * and toggles the `.context-sidebar-open` class on the document + * root — the lower-third strip + sidebar slide animation hang off + * that class flip (Plan R2a + R2c + R2f). + * + * Module-level singleton per session-file §1.14.5: every consumer + * shares one channel + one state object. No onUnmounted cleanup — + * the channel lives for the page lifetime. + * + * Mirrors the singleton shape of `use-audio-analyzer.js`. + */ + +import { effectScope, ref, watch } from 'vue'; + +const CHANNEL_NAME = 'reckit:context-screen'; +const LOCALSTORAGE_KEY = 'reckit:context-channel:state'; +const LOCALSTORAGE_DEBOUNCE_MS = 100; +const SIDEBAR_OPEN_CLASS = 'context-sidebar-open'; +const RELAY_ENDPOINT = '/__context_state'; +const RELAY_POLL_INTERVAL_MS = 300; + +let shared_state = null; + +export function useContextChannel() { + if (shared_state) { + return shared_state; + } + + const active_slug = ref(null); + const sidebar_open = ref(false); + + const persisted = readPersistedState(); + if (persisted) { + if (typeof persisted.active_slug !== 'undefined') { + active_slug.value = persisted.active_slug; + } + if (typeof persisted.sidebar_open === 'boolean') { + sidebar_open.value = persisted.sidebar_open; + } + } + + const channel = createChannel(); + function applyRemote(remote) { + if (!remote || typeof remote !== 'object') { + return; + } + if (typeof remote.active_slug !== 'undefined') { + active_slug.value = remote.active_slug; + } + if (typeof remote.sidebar_open === 'boolean') { + sidebar_open.value = remote.sidebar_open; + } + } + + channel.addEventListener('message', (event) => { + applyRemote(event && event.data); + }); + + // Cross-process bridge via HTTP. OBS browser source runs in its own + // Chromium (CEF) process — BroadcastChannel can't reach across that + // boundary. Every consumer polls the dev-server endpoint at a fixed + // interval; every local action pushes via POST. Latency ~300 ms p95. + // Suppress echo: when we POST our own snapshot, set last_pushed_hash + // so the very next poll skips applying it. + let last_pushed_hash = ''; + let is_pushing = false; + + async function pollState() { + if (is_pushing) { + return; + } + if (typeof fetch === 'undefined') { + return; + } + try { + const res = await fetch(RELAY_ENDPOINT, { cache: 'no-store' }); + if (!res.ok) { + return; + } + const data = await res.json(); + const hash = JSON.stringify(data); + if (hash !== last_pushed_hash) { + last_pushed_hash = hash; + applyRemote(data); + } + } catch { + // Network error — silently retry next interval. + } + } + + async function pushState(snapshot) { + if (typeof fetch === 'undefined') { + return; + } + is_pushing = true; + try { + last_pushed_hash = JSON.stringify(snapshot); + await fetch(RELAY_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(snapshot), + }); + } catch { + // Network error — local state still applied; remote may diverge + // until next successful push. + } finally { + is_pushing = false; + } + } + + pollState(); + setInterval(pollState, RELAY_POLL_INTERVAL_MS); + + let persist_timer = null; + function schedulePersist() { + if (persist_timer) { + clearTimeout(persist_timer); + } + persist_timer = setTimeout(persistNow, LOCALSTORAGE_DEBOUNCE_MS); + } + + function persistNow() { + persist_timer = null; + if (typeof localStorage === 'undefined') { + return; + } + try { + localStorage.setItem( + LOCALSTORAGE_KEY, + JSON.stringify({ + active_slug: active_slug.value, + sidebar_open: sidebar_open.value, + }), + ); + } catch { + // Storage quota / disabled — silently degrade. + } + } + + function broadcastSnapshot() { + const snapshot = { + active_slug: active_slug.value, + sidebar_open: sidebar_open.value, + }; + channel.postMessage(snapshot); + pushState(snapshot); + } + + function setActiveSlug(slug) { + active_slug.value = slug; + broadcastSnapshot(); + } + + function toggleSidebar() { + sidebar_open.value = !sidebar_open.value; + broadcastSnapshot(); + } + + function hideSidebar() { + sidebar_open.value = false; + broadcastSnapshot(); + } + + const scope = effectScope(true); + scope.run(() => { + watch([active_slug, sidebar_open], schedulePersist); + watch(sidebar_open, (open) => applyDocumentClass(open)); + }); + + applyDocumentClass(sidebar_open.value); + + shared_state = { + active_slug, + sidebar_open, + setActiveSlug, + toggleSidebar, + hideSidebar, + }; + return shared_state; +} + +function createChannel() { + if (typeof BroadcastChannel === 'undefined') { + return { postMessage: () => {}, onmessage: null, close: () => {} }; + } + return new BroadcastChannel(CHANNEL_NAME); +} + +function readPersistedState() { + if (typeof localStorage === 'undefined') { + return null; + } + try { + const raw = localStorage.getItem(LOCALSTORAGE_KEY); + if (!raw) { + return null; + } + const parsed = JSON.parse(raw); + if (typeof parsed === 'object' && parsed !== null) { + return parsed; + } + return null; + } catch { + return null; + } +} + +function applyDocumentClass(open) { + if (typeof document === 'undefined') { + return; + } + const root = document.documentElement; + if (!root || !root.classList) { + return; + } + root.classList.toggle(SIDEBAR_OPEN_CLASS, open); +} diff --git a/src/shared/utils/highlight.js b/src/shared/utils/highlight.js new file mode 100644 index 0000000..3f857b0 --- /dev/null +++ b/src/shared/utils/highlight.js @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2026 Cristian D. Moreno — @Kyonax + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. See LICENSE or https://mozilla.org/MPL/2.0/ + * + * highlight — async wrapper around Shiki for syntax-tokenizing code + * blocks rendered inside . Returns a 2-D token array + * ([line][token]) with `content` + `color` per token. Consumers + * render tokens through Vue templates as entries — + * never via v-html (D11). Cache keyed by `lang::code` so the same + * block isn't re-tokenized. + * + * Falls back to `null` when the language is not in the supported + * set OR when Shiki throws — consumers then render plain text. + */ + +import { codeToTokens } from 'shiki'; + +const SUPPORTED_LANGUAGES = new Set([ + 'js', + 'javascript', + 'ts', + 'typescript', + 'vue', + 'html', + 'css', + 'scss', + 'json', + 'bash', + 'sh', + 'shell', + 'python', + 'py', + 'markdown', + 'md', + 'yaml', + 'yml', + 'diff', +]); + +const SHIKI_THEME = 'tokyo-night'; + +const cache = new Map(); + +export async function highlightCode(code, language) { + if (typeof code !== 'string' || code.length === 0) { + return null; + } + const lang = (language || '').toLowerCase().trim(); + if (!SUPPORTED_LANGUAGES.has(lang)) { + return null; + } + const key = `${lang}::${code}`; + if (cache.has(key)) { + return cache.get(key); + } + try { + const result = await codeToTokens(code, { + lang, + theme: SHIKI_THEME, + }); + cache.set(key, result.tokens); + return result.tokens; + } catch { + cache.set(key, null); + return null; + } +} diff --git a/src/shared/utils/org.js b/src/shared/utils/org.js new file mode 100644 index 0000000..99edbb8 --- /dev/null +++ b/src/shared/utils/org.js @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2026 Cristian D. Moreno — @Kyonax + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. See LICENSE or https://mozilla.org/MPL/2.0/ + * + * org — topic library (Rule J) for parsing RECKIT context .org files. + * Wraps uniorg-parse to produce a unified AST, then partitions the + * top-level nodes into metadata (title, subtitle, description, tags), + * a marquee items array (extracted from a `#+begin_marquee` special + * block), and a body AST (everything else, rendered in the sidebar + * via per D11 — never via v-html). + * + * Required keys: TITLE, DESCRIPTION. Missing either throws + * `OrgSchemaError` so a malformed .org surfaces a parser-error + * indicator on the corresponding card (D7). + * + * Schema lock: see plan node Plan #context-screen, decisions D7 + D11. + */ + +import { unified } from 'unified'; +import uniorgParse from 'uniorg-parse'; + +const MARQUEE_BLOCK_NAME = 'marquee'; +const REQUIRED_KEYS = ['TITLE', 'DESCRIPTION']; +const TAG_KEYS = ['FILETAGS', 'TAGS']; +const TAG_DELIMITER = ':'; + +const processor = unified().use(uniorgParse); + +export class OrgSchemaError extends Error { + constructor(message, missing_keys = []) { + super(message); + this.name = 'OrgSchemaError'; + this.missing_keys = missing_keys; + } +} + +export function parseOrg(raw_string) { + const ast = processor.parse(raw_string); + + const required_values = {}; + const missing = []; + for (const key of REQUIRED_KEYS) { + const value = extractMetaKey(ast, key); + if (value === null) { + missing.push(key); + } else { + required_values[key] = value; + } + } + if (missing.length > 0) { + throw new OrgSchemaError( + `Required org keyword(s) missing: ${missing.join(', ')}`, + missing, + ); + } + + return { + title: required_values.TITLE, + subtitle: extractMetaKey(ast, 'SUBTITLE'), + description: required_values.DESCRIPTION, + tags: extractFiletags(ast), + marquee_items: extractMarqueeBlock(ast), + body_ast: collectBodyNodes(ast), + }; +} + +export function extractMetaKey(ast, key) { + if (!ast || !Array.isArray(ast.children)) { + return null; + } + for (const node of ast.children) { + if (node.type === 'keyword' && node.key === key) { + return typeof node.value === 'string' ? node.value : null; + } + } + return null; +} + +export function extractFiletags(ast) { + if (!ast || !Array.isArray(ast.children)) { + return []; + } + for (const node of ast.children) { + if (node.type !== 'keyword') { + continue; + } + if (!TAG_KEYS.includes(node.key)) { + continue; + } + const raw_value = typeof node.value === 'string' ? node.value : ''; + return raw_value + .split(TAG_DELIMITER) + .map((part) => part.trim()) + .filter((part) => part.length > 0); + } + return []; +} + +export function extractMarqueeBlock(ast) { + if (!ast || !Array.isArray(ast.children)) { + return []; + } + for (const node of ast.children) { + if ( + node.type === 'special-block' + && node.blockType === MARQUEE_BLOCK_NAME + ) { + return collectTextLines(node); + } + } + return []; +} + +export function collectBodyNodes(ast) { + if (!ast || !Array.isArray(ast.children)) { + return []; + } + const body = []; + for (const node of ast.children) { + if (node.type === 'keyword') { + continue; + } + if ( + node.type === 'special-block' + && node.blockType === MARQUEE_BLOCK_NAME + ) { + continue; + } + body.push(node); + } + return body; +} + +function collectTextLines(node) { + const buffer = []; + walkText(node, buffer); + return buffer + .join('') + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.length > 0); +} + +function walkText(node, buffer) { + if (!node) { + return; + } + if (node.type === 'text' && typeof node.value === 'string') { + buffer.push(node.value); + return; + } + if (Array.isArray(node.children)) { + for (const child of node.children) { + walkText(child, buffer); + } + } +} diff --git a/src/shared/utils/org.test.js b/src/shared/utils/org.test.js new file mode 100644 index 0000000..0cac3d7 --- /dev/null +++ b/src/shared/utils/org.test.js @@ -0,0 +1,184 @@ +/** + * Copyright (c) 2026 Cristian D. Moreno — @Kyonax + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. See LICENSE or https://mozilla.org/MPL/2.0/ + * + * Tests for the org topic library: required-field enforcement, + * metadata extraction, filetag parsing, marquee block extraction, + * body partitioning. Covers Plan #context-screen Q5 schema lock. + */ + +import { + collectBodyNodes, + extractFiletags, + extractMarqueeBlock, + extractMetaKey, + OrgSchemaError, + parseOrg, +} from '@shared/utils/org.js'; +import { unified } from 'unified'; +import uniorgParse from 'uniorg-parse'; +import { describe, expect, it } from 'vitest'; + +const FULL_FIXTURE = `#+TITLE: Hello +#+SUBTITLE: A second line +#+DESCRIPTION: Short description for the lower-third strip. +#+TAGS: :alpha:beta:gamma: + +#+begin_marquee +First item +Second item +Third item +#+end_marquee + +* Section A + +A paragraph with content. + +#+begin_src js +const x = 1; +#+end_src +`; + +const MIN_FIXTURE = `#+TITLE: Quick Note +#+DESCRIPTION: Minimal context — no marquee, no body. +`; + +const MISSING_DESCRIPTION_FIXTURE = `#+TITLE: Only Title +`; + +const MISSING_BOTH_FIXTURE = `#+TAGS: :nope: +`; + +const NO_MARQUEE_FIXTURE = `#+TITLE: A +#+DESCRIPTION: B +* Body section +Content paragraph. +`; + +const FILETAGS_FIXTURE = `#+TITLE: A +#+DESCRIPTION: B +#+FILETAGS: :tag1:tag2: +`; + +const EXPECTED_TAG_COUNT = 3; +const EXPECTED_MARQUEE_COUNT = 3; + +function parseRaw(raw_string) { + return unified().use(uniorgParse).parse(raw_string); +} + +describe('parseOrg — happy path', () => { + it('extracts every locked Q5 schema field from the full fixture', () => { + const result = parseOrg(FULL_FIXTURE); + expect(result.title).toBe('Hello'); + expect(result.subtitle).toBe('A second line'); + expect(result.description).toBe( + 'Short description for the lower-third strip.', + ); + expect(result.tags).toHaveLength(EXPECTED_TAG_COUNT); + expect(result.tags).toEqual(['alpha', 'beta', 'gamma']); + expect(result.marquee_items).toHaveLength(EXPECTED_MARQUEE_COUNT); + expect(result.marquee_items[0]).toBe('First item'); + expect(Array.isArray(result.body_ast)).toBe(true); + expect(result.body_ast.length).toBeGreaterThan(0); + }); + + it('returns null subtitle + empty arrays for the minimal fixture', () => { + const result = parseOrg(MIN_FIXTURE); + expect(result.title).toBe('Quick Note'); + expect(result.subtitle).toBe(null); + expect(result.tags).toEqual([]); + expect(result.marquee_items).toEqual([]); + }); +}); + +describe('parseOrg — error paths', () => { + it('throws OrgSchemaError when DESCRIPTION is missing', () => { + expect(() => parseOrg(MISSING_DESCRIPTION_FIXTURE)).toThrow( + OrgSchemaError, + ); + }); + + it('throws OrgSchemaError when both required keys are missing', () => { + let thrown = null; + try { + parseOrg(MISSING_BOTH_FIXTURE); + } catch (error) { + thrown = error; + } + expect(thrown).toBeInstanceOf(OrgSchemaError); + expect(thrown.missing_keys).toEqual(['TITLE', 'DESCRIPTION']); + }); +}); + +describe('extractMetaKey', () => { + it('returns the keyword value when present', () => { + const ast = parseRaw('#+TITLE: Hello\n'); + expect(extractMetaKey(ast, 'TITLE')).toBe('Hello'); + }); + + it('returns null for an absent key', () => { + const ast = parseRaw('#+TITLE: Hello\n'); + expect(extractMetaKey(ast, 'SUBTITLE')).toBe(null); + }); + + it('handles a null AST gracefully', () => { + expect(extractMetaKey(null, 'TITLE')).toBe(null); + }); +}); + +describe('extractFiletags', () => { + it('parses the colon-delimited TAGS form', () => { + const ast = parseRaw('#+TAGS: :a:b:c:\n'); + expect(extractFiletags(ast)).toEqual(['a', 'b', 'c']); + }); + + it('also recognises FILETAGS', () => { + const ast = parseRaw(FILETAGS_FIXTURE); + expect(extractFiletags(ast)).toEqual(['tag1', 'tag2']); + }); + + it('returns an empty array when no tag keyword is present', () => { + const ast = parseRaw('#+TITLE: A\n'); + expect(extractFiletags(ast)).toEqual([]); + }); +}); + +describe('extractMarqueeBlock', () => { + it('returns the items split by newline, trimmed, no empties', () => { + const ast = parseRaw(FULL_FIXTURE); + const items = extractMarqueeBlock(ast); + expect(items).toEqual(['First item', 'Second item', 'Third item']); + }); + + it('returns an empty array when no marquee block is present', () => { + const ast = parseRaw(NO_MARQUEE_FIXTURE); + expect(extractMarqueeBlock(ast)).toEqual([]); + }); +}); + +describe('collectBodyNodes', () => { + it('filters out top-level keywords', () => { + const ast = parseRaw(FULL_FIXTURE); + const body = collectBodyNodes(ast); + expect(body.every((node) => node.type !== 'keyword')).toBe(true); + }); + + it('filters out the marquee special-block', () => { + const ast = parseRaw(FULL_FIXTURE); + const body = collectBodyNodes(ast); + expect( + body.every( + (node) => + !(node.type === 'special-block' && node.blockType === 'marquee'), + ), + ).toBe(true); + }); + + it('preserves headlines, sections, paragraphs, src-blocks', () => { + const ast = parseRaw(FULL_FIXTURE); + const body = collectBodyNodes(ast); + expect(body.length).toBeGreaterThan(0); + }); +}); diff --git a/src/views/components/elements/card.vue b/src/views/components/elements/card.vue index bbc36a7..b3c54b8 100644 --- a/src/views/components/elements/card.vue +++ b/src/views/components/elements/card.vue @@ -153,14 +153,26 @@ - +
+ + + +
+ + + + diff --git a/src/views/components/modals/preview.vue b/src/views/components/modals/preview.vue index 1a39856..09d994c 100644 --- a/src/views/components/modals/preview.vue +++ b/src/views/components/modals/preview.vue @@ -236,7 +236,7 @@ onUnmounted(() => { .modal-stage { padding: 1em; - background: rgba(255, 255, 255, 0.04); + background: var(--clr-neutral-50-04); } .stage-inner { @@ -297,7 +297,7 @@ onUnmounted(() => { .action-button:hover { color: var(--clr-primary-100); border-color: var(--clr-primary-100); - background: rgba(255, 215, 0, 0.04); + background: var(--clr-primary-100-04); } .action-icon { diff --git a/src/views/components/sections/setup.vue b/src/views/components/sections/setup.vue index c0da439..e2f8956 100644 --- a/src/views/components/sections/setup.vue +++ b/src/views/components/sections/setup.vue @@ -94,7 +94,7 @@ } .setup-flow li:hover { - background: rgba(255, 215, 0, 0.04); + background: var(--clr-primary-100-04); color: var(--clr-neutral-50); } diff --git a/src/views/components/sections/sources.vue b/src/views/components/sections/sources.vue index cc86ded..d5490d0 100644 --- a/src/views/components/sections/sources.vue +++ b/src/views/components/sections/sources.vue @@ -182,12 +182,12 @@ const filtered_overlays = computed(() => { .brand-tab:hover { color: var(--clr-neutral-50); - background: rgba(255, 215, 0, 0.03); + background: var(--clr-primary-100-03); } .brand-tab.active { color: var(--clr-primary-100); - background: rgba(255, 215, 0, 0.06); + background: var(--clr-primary-100-06); border-color: var(--clr-border-100); border-bottom: 1px solid var(--clr-neutral-500); } @@ -325,12 +325,12 @@ const filtered_overlays = computed(() => { .status-chip:hover { color: var(--clr-neutral-50); - background: rgba(255, 255, 255, 0.04); + background: var(--clr-neutral-50-04); } .status-chip.active { color: var(--clr-primary-100); - background: rgba(255, 215, 0, 0.06); + background: var(--clr-primary-100-06); } .chip-dot { diff --git a/vite.config.js b/vite.config.js index bb0a4e5..3f85694 100644 --- a/vite.config.js +++ b/vite.config.js @@ -51,6 +51,71 @@ const pkg = require('./package.json'); const ROOT = dirname(fileURLToPath(import.meta.url)); const DEV_SERVER_PORT = 5173; +// Cross-process bridge for the context-screen control plane. +// OBS browser source runs in its own embedded Chromium (CEF) process — +// BroadcastChannel can't reach across processes, and Vite HMR custom +// events proved unreliable in CEF (silent failure of the WebSocket +// connection or the import.meta.hot handoff). HTTP polling + push is +// universal: every browser process can fetch + POST the same endpoint. +// +// State is held in a closure on the dev server. GET returns current +// state; POST replaces it. Composable polls at POLL_INTERVAL_MS for +// freshness; pushes on every local action. ~300 ms p95 cross-process +// latency, debuggable with `curl http://localhost:5173/__context_state`. +const CONTEXT_STATE_PATH = '/__context_state'; +const context_relay_plugin = { + name: 'reckit-context-relay', + configureServer(server) { + let current_state = { active_slug: null, sidebar_open: false }; + + server.middlewares.use(CONTEXT_STATE_PATH, (req, res) => { + res.setHeader('Cache-Control', 'no-store'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.statusCode = 204; + res.end(); + return; + } + + if (req.method === 'GET') { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(current_state)); + return; + } + + if (req.method === 'POST') { + let body = ''; + req.on('data', (chunk) => { + body += chunk; + }); + req.on('end', () => { + try { + const parsed = JSON.parse(body); + if (parsed && typeof parsed === 'object') { + current_state = parsed; + res.statusCode = 204; + res.end(); + return; + } + res.statusCode = 400; + res.end('invalid payload'); + } catch { + res.statusCode = 400; + res.end('invalid json'); + } + }); + return; + } + + res.statusCode = 405; + res.end(); + }); + }, +}; + export default defineConfig({ resolve: { alias: { @@ -69,7 +134,7 @@ export default defineConfig({ '@composables': resolve(ROOT, 'src/shared/composables'), }, }, - plugins: [vue()], + plugins: [vue(), context_relay_plugin], define: { __APP_VERSION__: JSON.stringify(pkg.version), }, From 2ff0df9acc754e771a9d091d313b9a52ed461414 Mon Sep 17 00:00:00 2001 From: "Cristian D. Moreno (Kyonax)" Date: Tue, 28 Apr 2026 03:37:10 -0500 Subject: [PATCH 2/2] feat(context): GitHub alerts + 60-30-10 color rebalance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refine context-screen toward GitHub-flavored alert callouts, a disciplined 60-30-10 palette where gold is reserved for the 10% accent tier, per-page rem-base scaling for OBS readability, and typography parity between sidebar headlines and alert titles. Privatize the contexts/ folder so personal notes stay local. - alerts: NOTE / TIP / IMPORTANT / WARNING / CAUTION via =[!TYPE]= quote-block prefix detection — Octicon SVG icons (info / light-bulb / report / alert / stop, MIT-licensed primer/octicons paths bound via =:d=, never v-html), per-type saturated 100-shade left-bar border + 6% color-mix surface + italic font-display body - alert plumbing: WeakMap-cached =detectAlertType=, AST prefix- strip via shallow clone (no AST mutation), font-display forced on every descendant via =.org-alert :where(*)= + explicit =.org-content= override (the recursive UiOrgContent wrapper would otherwise cascade =font-mono= down) - tertiary palette: +purple family (50–500, hue 266) added to =$colors= in =_variables.scss=; =--clr-tertiary-*= tokens emit automatically through =_theme.scss='s existing =@each= - color rebalance (60-30-10): gold demoted out of headlines / tables / chips into the 10% accent tier (marquee, status dot, headline 3-dot stack, OUTPUT frame, alert borders); body neutrals shifted neutral-50 → neutral-100 globally for a less-light feel - headline accent: replace the 3px gold =|= bar with three stacked 3px gold squares (single 3px square + ±6px box- shadow clones); =padding-left: 0.5em= + =transform: translateY(60%)= so the stack centers on the first text line - code blocks: lang flag bg → primary-400, OUTPUT flag border + text → neutral-300 to mirror the src-block frame, ws markers 2px → 3px (darker neutral-400 / primary-400), =padding-top: 2.5em= so code clears the absolute label; src-block + example get =padding-bottom: 0.4em= - tables: switch to inline =v-for= row/cell render — drop the recursive =UiOrgContent= inside == / == because Chromium can mistreat =display: contents= on table-internal divs and collapse columns; =table-layout: fixed= + =word-break: break-word= on cells for narrow-sidebar fit - sidebar/strip chrome: scope the html font-size base to the page via =html:has(.context-screen-overlay) { font-size: 16px }= so every rem-based =--fs-*= renders ~33% larger in OBS without touching the global 12px base; sidebar ratio 0.30 → 0.34; chip override → quiet outline (=fs-225=, neutral-100 text, transparent surface, =--clr-border-100= edge); strip =border-bottom= restored, marquee =border-top= dropped to avoid a 2px stack at the divider - marquee: =v-if= on empty items so the gold strip doesn't render when a context has no =#+begin_marquee= block; separator squares 3px → 6px - verbatim / inline-code: dark-neutral-400 pill + neutral-100 text + =line-height: 1.5em= + =width: fit-content= safeguard - quote: alert-format without header — =border-left: 1px solid var(--clr-border-100)=, italic, no gold left bar - lists: chevron + checkbox markers via flex =align-self: center= + =transform: translateY(0.05em)= (the parent =align-items: baseline= ignores =vertical-align= on flex children); chevron rule generalized to non-ordered lists so descriptive lists also get markers; tighter marker right- margins - title parity: =.org-headline= and =.org-alert__header= share =font-display= / weight 700 / =letter-spacing 0.06em= / uppercase — sidebar section titles and alert callout titles read as the same typography system - privatize contexts: =@kyonax_on_tech/data/contexts/= added to =.gitignore= — personal notes stay local; only the rendering plumbing is tracked. Existing fixtures removed from the index via =git rm --cached= Modified-by: Cristian D. Moreno (Kyonax) --- .gitignore | 5 + .../data/contexts/obs-browser-sources.org | 75 --- @kyonax_on_tech/data/contexts/quick-note.org | 2 - .../sources/hud/context-screen.vue | 105 +++- src/app/scss/abstracts/_variables.scss | 8 + src/shared/components/ui/org-content.vue | 568 +++++++++++++++--- 6 files changed, 575 insertions(+), 188 deletions(-) delete mode 100644 @kyonax_on_tech/data/contexts/obs-browser-sources.org delete mode 100644 @kyonax_on_tech/data/contexts/quick-note.org diff --git a/.gitignore b/.gitignore index c066ee1..0d57d11 100644 --- a/.gitignore +++ b/.gitignore @@ -146,3 +146,8 @@ temp/ CLAUDE.md COMMIT.org PR.org + +# Personal context content — each contributor's own .org notes +# loaded by for live overlays. The rendering +# plumbing is tracked; the content is private. +@kyonax_on_tech/data/contexts/ diff --git a/@kyonax_on_tech/data/contexts/obs-browser-sources.org b/@kyonax_on_tech/data/contexts/obs-browser-sources.org deleted file mode 100644 index 38ebe23..0000000 --- a/@kyonax_on_tech/data/contexts/obs-browser-sources.org +++ /dev/null @@ -1,75 +0,0 @@ -#+TITLE: How OBS Browser Sources Work -#+SUBTITLE: From WebSocket to Live HUD -#+DESCRIPTION: A walkthrough of how RECKIT mounts Vue overlays into OBS scenes via the browser-source plugin, with focus on the audio-meter pipeline. -#+TAGS: :obs:vue:websocket:audio:performance: - -#+begin_marquee -WS over REST -OBS scene tree -Audio meter perf -Direct DOM writes -Float32Array hot path -#+end_marquee - -* Mount Path - -OBS Studio loads each browser source as an isolated Chromium tab. The source URL points at a Vite dev server running locally; once mounted, the source can call into OBS WebSocket on port =4455= to read live state. - -The composable is a *module-level singleton* — N consumers share one subscription. - -#+begin_src javascript -import { useObsWebsocket } from '@composables/use-obs-websocket.js'; - -const { obs } = useObsWebsocket(); -obs.on('InputVolumeMeters', (event) => { - // 50 Hz audio levels — drive the HUD off this event -}); -#+end_src - -* Audio-Meter Hot Path - -Bar transforms are written *directly to the DOM* via template refs — Vue reactivity is bypassed in the hot path. - -#+begin_src scss -.audio-meter .bar { - transform: scaleY(var(--level)); - transform-origin: bottom; - contain: layout paint; -} -#+end_src - -#+RESULTS: -: 0 broad CSS filters -: 0 per-frame allocations -: 60 fps held on the test rig - -** Constraints (= must hold per session-file §1.14) - -- =transform= / =opacity= animations only -- preallocated typed arrays (=Float32Array=) -- precomputed =JITTER_TABLE= for jitter -- write-threshold skip: =Math.abs(next - last) < 0.01= → no DOM write - -** Checklist before merging - -- [X] no =cyberpunk-glow= mixin reintroduction -- [X] singleton composables -- [ ] =composables.test.js= asserts singleton identity -- [-] OBS smoke test (partially complete) - -* Why It Works - -| Technique | Cost saved | Reference | -|--------------------------+---------------------------+----------------------| -| Singleton composables | N→1 event subscriptions | session §1.14.5 | -| Direct DOM writes | Vue reactivity bypass | == | -| =transform: scaleY()= | layout / re-rasterization | session §1.14.3 | -| =contain: layout paint= | paint isolation | session §1.14.4 | - -#+begin_quote -fps wins. Every other rule below ranks under it. -#+end_quote - ------ - -See [[https://obsproject.com/docs/sources][OBS source documentation]] for the broader context. diff --git a/@kyonax_on_tech/data/contexts/quick-note.org b/@kyonax_on_tech/data/contexts/quick-note.org deleted file mode 100644 index 27644fe..0000000 --- a/@kyonax_on_tech/data/contexts/quick-note.org +++ /dev/null @@ -1,2 +0,0 @@ -#+TITLE: Quick Note -#+DESCRIPTION: Minimal context — title + description, no marquee block, no body. Marquee row collapses to zero height. diff --git a/@kyonax_on_tech/sources/hud/context-screen.vue b/@kyonax_on_tech/sources/hud/context-screen.vue index 18a2dd6..1d3d232 100644 --- a/@kyonax_on_tech/sources/hud/context-screen.vue +++ b/@kyonax_on_tech/sources/hud/context-screen.vue @@ -67,11 +67,11 @@ -
-
+
+
{ }); + +