Skip to content

feat(context): news-style lower-third + sidebar HUD#6

Open
Kyonax wants to merge 2 commits intofeat-brand_kotfrom
context-screen
Open

feat(context): news-style lower-third + sidebar HUD#6
Kyonax wants to merge 2 commits intofeat-brand_kotfrom
context-screen

Conversation

@Kyonax
Copy link
Copy Markdown
Owner

@Kyonax Kyonax commented Apr 27, 2026

Checklist (check if it applies)

  • Contains testing instructions
  • Requires environment / credential changes
  • Requires special deployment steps
  • Has unit / integration tests
  • Touches licensing, security, or CI
  • License headers on new files (LICENSING.org)
  • Attribution preserved on modified files
  • Lint passes (npm run lint)
  • Security rules followed (SECURITY.org)
  • No credentials committed
  • All GitHub Checks have passed (no Pre-Check Failed label applied by CI)
  • CHANGELOG.org updated (release PRs only)
  • Version bumped (release PRs only)

What does this PR do?

Adds the second @kyonax_on_tech web source — a news-style lower-third HUD targeting an OBS CONTEXT scene. Per-video content authors as a single .org file under @kyonax_on_tech/data/contexts/<slug>.org, parses to a unified AST at Vite glob-time via uniorg-parse, and renders through a recursive Vue template walker (<UiOrgContent>) covering 18 org node types — headlines, lists, checklists, tables, src-blocks with Shiki tokyo-night syntax highlighting, #+RESULTS: blocks, quotes, links. State syncs from a new <ContextControlModal> on the landing page to every mounted browser source through a singleton useContextChannel composable that combines same-process BroadcastChannel with an HTTP polling/push relay over a Vite middleware endpoint — needed because OBS browser sources run inside a separate Chromium (CEF) process where BroadcastChannel does not propagate. 11 new files, 16 modifications, 46 new tests bringing the suite from 33 to 79.

As a content creator authoring a video, I want a single per-video .org file driving a news-style lower-third + an on-demand sidebar that renders any org-mode markup with proper syntax highlighting, so that I can swap context per video without leaving Emacs and without post-production edits.

Design / Reference: Brand identity tracked via .github/assets/logo.txt. Marquee animation pattern adapted from the Kyonax sibling kyo-web-online/src/app/scss/components/_marquee.scss. Scope tracked in CHANGELOG.org TODO block.

Implementation

[NEW] new file · [MOD] modified file · [DEL] removed · [MOV] renamed or relocated

Brand assets (src/app/, NOTICE)

  • [NEW] src/app/fonts/Geomanist/GeomanistRegular.ttf — pulled from sibling kyo-web-online; HUD title face
  • [NEW] src/app/fonts/Geomanist/GeomanistBold.ttf — display weight
  • [NEW] src/app/fonts/Geomanist/GeomanistItalic.ttf — italic display
  • [MOD] src/app/scss/base/_typography.scss — three new @font-face declarations via the existing font-face mixin
  • [MOD] src/app/scss/abstracts/_theme.scss — declares --font-display, --surface-bg token, four motion tokens (--motion-sidebar-ms: 280ms, --motion-strip-ease, --motion-peek-pulse-s: 2.5s, --motion-marquee-s), plus shade-tokens --clr-primary-100-{02,03,04,06,10,14,40,80} and --clr-neutral-50-{02,04,12}
  • [MOD] src/app/scss/abstracts/_mixins.scss — adds alpha-tint($color-var, $percent) / darken / lighten SCSS functions wrapping color-mix(); adds corner-dots($color, $size, $corners) background-image mixin and pseudo-element corner-square-tl / corner-square-tr mixins for the HUD-chrome sharp-corner intersection markers
  • [MOD] NOTICE — Third-Party Fonts section crediting Atipo Foundry (Geomanist) and Colophon Foundry (SpaceMono)

Org parser + AST renderer (src/shared/utils/, src/shared/components/ui/)

  • [NEW] src/shared/utils/org.js (158 LoC) — Rule J topic library wrapping uniorg-parse. Named exports: parseOrg, extractMetaKey, extractFiletags, extractMarqueeBlock, collectBodyNodes. OrgSchemaError class for required-field violations; REQUIRED_KEYS = ['TITLE', 'DESCRIPTION'] drives the validation loop.
  • [NEW] src/shared/utils/org.test.js — 14 tests covering happy-path, OrgSchemaError, every named export, both #+TAGS: and :filetag: forms, marquee-absent collapse path
  • [NEW] src/shared/utils/highlight.js (68 LoC) — async wrapper around Shiki's codeToTokens with tokyo-night theme. In-memory Map cache keyed by <lang>:<code>; SUPPORTED_LANGUAGES set covering js / ts / vue / html / css / scss / json / bash / python / markdown / yaml / diff; falls back to a single grey-token line for unsupported langs.
  • [NEW] src/shared/components/ui/org-content.vue (698 LoC) — <UiOrgContent> recursive Vue template AST renderer. Zero v-html (project ESLint no-restricted-syntax bans innerHTML). Per-node styling covers 18 node types — headlines (with | rule glyph instead of corner-bracket SVG), paragraphs, bold/italic/verbatim/code/strike, ul/ol/dl, list items with checkbox glyphs, tables, src-blocks with lang label + Shiki tokenized output + 2px whitespace markers via background-image gradient, #+RESULTS: blocks with OUTPUT label, quotes, examples, external links, horizontal rules. Lists use display: flex; align-items: baseline to keep marker + paragraph child on the same line.
  • [NEW] src/shared/components/ui/org-content.test.js — 11 mount tests via @vue/test-utils mount asserting expected DOM per node type plus a final no-<script>-injection regression check
  • [MOD] src/shared/brand-loader.js — adds CONTEXTS glob /@*/data/contexts/*.org (eager, ?raw); getContexts(handle) per-brand helper; buildContextsMap() partition that parses every .org at glob-time and caches { raw, parsed, parse_error }
  • [MOD] src/shared/brand-loader.test.js — 7 new assertions on CONTEXTS shape, per-brand discovery, parsed schema, slug uniqueness, getContexts happy + missing-handle paths
  • [MOD] src/shared/components/ui/chip.vue+shape prop (pill | square) with inline validator; both shapes lock border-radius: 0; square is the explicit sharp-corner-mandate consumer used by the context-screen sidebar tags

Cross-page state plane (src/shared/composables/, vite.config.js)

  • [NEW] src/shared/composables/use-context-channel.js (228 LoC) — singleton wrapping BroadcastChannel('reckit:context-screen'). Reactive refs active_slug + sidebar_open; methods setActiveSlug / toggleSidebar / hideSidebar. Debounced localStorage persist (100 ms). Toggles .context-sidebar-open class on document.documentElement via effectScope(true) + watch. Adds an HTTP polling/push bridge to a Vite middleware endpoint (RELAY_ENDPOINT = '/__context_state', RELAY_POLL_INTERVAL_MS = 300) — every action POSTs the snapshot, every consumer GETs every 300 ms; echo-suppression via last_pushed_hash.
  • [MOD] src/shared/composables/composables.test.js — 5 new tests across three describe blocks: singleton-identity, initial-state shape + method exposure, BroadcastChannel mock with postMessage spy on setActiveSlug + toggleSidebar. Mock implements addEventListener / removeEventListener per unicorn/prefer-add-event-listener.
  • [MOD] vite.config.js — adds context_relay_plugin Vite plugin. configureServer(server) mounts a middleware on /__context_state: GET returns the current { active_slug, sidebar_open } JSON snapshot; POST replaces it. State lives in plugin closure for the lifetime of the dev server. Cross-process bridge — OBS browser source's CEF process polls the same endpoint as the landing page, so toggling the sidebar in the landing-page modal updates OBS within ~300 ms p95 even though BroadcastChannel cannot cross the process boundary.

HUD source (@kyonax_on_tech/)

  • [NEW] @kyonax_on_tech/sources/hud/context-screen.vue (599 LoC) — full HUD source. Four surfaces: .context-strip lower-third (Geomanist Bold title + SpaceMono description; width transitions 100% to 62% on toggle — the one acknowledged non-transform animation per D13), .context-marquee row (gold background, black text, transform: translateX(0% to -50%) infinite linear, items duplicated for seamless loop), .context-sidebar slide-in panel (z-index 100, transform: translateX() + opacity, mounts <UiOrgContent>, native overflow-y: auto for user scroll), .context-peek closed-state ambient indicator (z-index 99, layered span + animated opacity pulse mirroring <UiStatusDot>'s static-halo + animated-opacity pattern). Strip + sidebar share a single 1px border (border-right: none on the strip); strip's TR corner-square overflows above the sidebar via z-index: 110 on .context-lower. Auto-scroll constants: OPEN_DELAY_MS = 3000, BOTTOM_HOLD_MS = 3000, SCROLL_SPEED_PX_PER_SEC = 16, MS_PER_SEC = 1000, USER_INTERACTION_PAUSE_MS = 5000 (auto-scroll yields for 5 s after any user wheel/touch interaction).
  • [NEW] @kyonax_on_tech/data/contexts/obs-browser-sources.org — fixture-rich example covering every supported D10 node type (headlines, lists, checklists, table, src-block, #+RESULTS:, quote, link, horizontal rule, marquee block)
  • [NEW] @kyonax_on_tech/data/contexts/quick-note.org — minimal no-marquee fixture exercising the collapse-to-zero validation path
  • [MOD] @kyonax_on_tech/sources.js+context-screen registry entry, status: 'ready'

Landing-page control surface (src/views/)

  • [NEW] src/views/components/modals/context-control.vue (533 LoC) — <ContextControlModal> extending <BaseModal>. Two-section grid: clickable slug list on the left (active slug highlighted via <UiBadge variant="active">, parser errors via <UiBadge variant="dim"> + inline message); live preview iframe on the right with getBoundingClientRect() scaling per existing <PreviewModal> pattern + 100 ms-debounced resize handler. Header brand chip + RELOAD IFRAME footer button.
  • [MOD] src/views/components/elements/card.vue+CONTEXT_SCREEN_ID constant; +is_context_screen computed; +is_control_open ref; +openControl / +closeControl methods; new .card-secondary-actions row holding DETAILS + conditional CONTROLS button; <ContextControlModal> mount conditional on source.id === 'context-screen'
  • [MOD] src/views/components/sections/setup.vue — landing-page hover state migrated from rgba(255, 215, 0, 0.04) literal to the new var(--clr-primary-100-04) shade token (token-migration polish surfaced during the context-screen styling pass)
  • [MOD] src/views/components/sections/sources.vue — same shade-token migration for hover/border states
  • [MOD] src/views/components/modals/preview.vue — same shade-token migration for the preview overlay backdrop

Dependencies

  • Runtime added: uniorg-parse@^3.2.1, shiki@^4.0.2uniorg-parse pulls unified plus a small unist transitive set; shiki ships pre-bundled grammars and the tokyo-night theme. Combined ~70 KB gzipped contribution to the index chunk; acceptable on localhost-only browser-source runtime per §1.14 budget reasoning (no network roundtrip, OBS Chromium loads once at scene start).
  • Dev added:
  • Upgraded:
  • Removed:
  • Lockfile: package-lock.json updated

Technical Details

  • BroadcastChannel + HTTP relay over OBS WebSocket bus

    • Chose: singleton useContextChannel combining same-process BroadcastChannel with an HTTP polling/push bridge over a custom Vite middleware (/__context_state).
    • Over: OBS WebSocket custom event; URL-hash polling; SharedWorker; pure BroadcastChannel; import.meta.hot.send HMR custom events.
    • Why: OBS browser sources run inside a separate Chromium (CEF) process — BroadcastChannel only crosses tabs inside the same Chromium process, so a landing-page tab and an OBS source never see each other's broadcasts. The Vite-middleware HTTP bridge gives a universal cross-process channel that works during dev (the only environment that matters for a localhost-only app), is debuggable with curl http://localhost:5173/__context_state, and frees the OBS WebSocket budget (per CONTRIBUTING.org) for video state. import.meta.hot.send was tried first and silently failed in CEF.
    • Trade-off: ~300 ms p95 propagation latency (vs the ~5 ms a same-process BroadcastChannel delivers); the bridge only works while the Vite dev server is running, so this design is dev-mode-only — production builds would need a different transport. Acceptable: this app is a localhost streaming rig, not a deployed service.
  • uniorg-parse AST + custom Vue renderer

    • Chose: add uniorg-parse as a runtime dep; parse .org to a unified AST at Vite glob-time; walk the AST inside a new <UiOrgContent> primitive.
    • Over: custom 400+ LoC regex parser; uniorg-rehype to generic HTML; abandoning .org for .json / .md.
    • Why: rendering "any org thing" — checklists, tables, #+RESULTS:, multi-section bodies, quotes, links — overshoots the break-even point of a custom parser. uniorg-parse is battle-tested; bundle cost is irrelevant on localhost.
    • Trade-off: ~25 KB gzipped runtime weight on the index chunk. Acceptable — no network roundtrip, OBS Chromium loads once at scene start.
  • Recursive Vue template walk, never v-html

    • Chose: <UiOrgContent> walks the AST through a recursive <template> v-for over node.children with one branch per node type. Inline text via mustache; code via <pre><code>{{ value }}</code></pre> (textContent-bound).
    • Over: v-html with uniorg-rehype to HTML; manual document.createElement inside onMounted; string templates with manual escaping.
    • Why: the project's eslint.config.mjs bans innerHTML assignment via no-restricted-syntax (XSS hardening). v-html compiles to innerHTML, so it's effectively forbidden. The recursive-template approach is also faster (Vue's reactivity tracks individual nodes), more secure, and gives per-node styling control for free since each node is a real DOM element with its own classes.
    • Trade-off: ~200 LoC of explicit Vue template branches per node type vs a one-liner v-html. Trade accepted for security + styling control.
  • Shiki tokyo-night syntax highlighting in src-blocks

    • Chose: async tokenize each #+BEGIN_SRC block via Shiki's codeToTokens API on first render; cache the tokenization result per <lang>:<code> key in a module-level Map; render tokens as styled <span> elements inside <pre><code>.
    • Over: Prism (eager-bundled, larger surface); shipping plain monospace (the original v1 plan); server-side highlighting; highlight.js.
    • Why: Shiki produces VS Code-grade highlighting with bundled grammars and themes; tokyo-night matches the cyberpunk aesthetic without per-element CSS overrides. Async tokenization yields the main thread on initial render so the lower-third strip + marquee paint immediately while code colors arrive on the next microtask. Cached per-block so re-renders hit zero cost. Whitespace inside src-blocks is also visualized via a per-segment splitWhitespaceSegments helper that emits <span class="org-src-block__ws"> nodes painted with a 2px-tall background-image gradient at 1ch tile size — the marker color is per-block (--clr-neutral-300 for src, --clr-primary-300 for #+RESULTS:).
    • Trade-off: Shiki pulls ~45 KB gzipped + the bundled grammars. Async-first render means an unstyled-token flash for the duration of one microtask on cold cache.
  • Hybrid 3-surface layout: lower-third + marquee + right sidebar

    • Chose: lower-third strip (always visible) + marquee row directly below + toggleable right sidebar (Option C dynamic split — strip width transitions 100% to 62% when the sidebar is open).
    • Over: pure lower-third only; static multi-zone with all surfaces always visible; sidebar overlays the strip without reflow.
    • Why: matches the news-broadcast convention while supporting deep-context surfacing on demand. Dynamic split keeps the speaker visible on the left even when the sidebar is open — RHS overlay never blocks the camera.
    • Trade-off: the strip's width transition is the SINGLE non-transform / non-opacity animation in the source — a one-time-per-toggle ~280 ms layout reflow. Amortised cost across a recording session (typically 2 toggles).
  • Sidebar auto-scroll via body.scrollTop write with user-interaction pause

    • Chose: rAF loop after OPEN_DELAY_MS (3 s) writing body.scrollTop += SCROLL_SPEED_PX_PER_SEC * delta_seconds (~16 px/s) on the sidebar body; bottom-hold 3 s; reset and loop. Wheel/touch handlers update last_user_interaction_at; the tick pauses for USER_INTERACTION_PAUSE_MS (5 s) after any interaction, then resumes from the user's manual scroll position.
    • Over: programmatic transform: translateY() on an inner wrapper inside overflow: hidden (the original D13 plan); CSS scroll-behavior: smooth; one-shot Element.scrollTo; ignoring the user.
    • Why: native scroll preserves the browser's wheel/touch handling for free, so the user can scroll back to a section they want to read; the auto-scroll yields gracefully via the interaction pause. The original transform-based approach disabled native scroll entirely. transform-on-wrapper saves layout cost in theory but the sidebar's content surface is the only updating sub-tree and contain: layout paint is set on the consumer.
    • Trade-off: scrollTop writes do trigger layout per frame inside the contained sub-tree — measured fine on the test rig. The hot path is otherwise zero-allocation per §1.14.6 (delta computed via performance.now(), single DOM write, no new objects).
  • Closed-sidebar peek indicator with layered halo + animated opacity

    • Chose: outer span owns static halo via box-shadow: var(--hud-halo-text) (never animated); inner <span class="context-peek__pulse"> owns the breathing opacity via @keyframes; root fades to opacity 0 on the .context-sidebar-open toggle.
    • Over: single span animating both halo + opacity; CSS animation-composition; JS-driven opacity tick.
    • Why: mirrors the perf-proven <UiStatusDot> idiom — only opacity changes per frame; the dark shadow stays cached. Single composited property.
    • Trade-off: two DOM nodes per indicator instead of one. Negligible cost.
  • Reuse existing @ui/ primitives, expand only via prop

    • Chose: sidebar tag chips render via <UiChip variant="solid" shape="square">; active-slug indicator via <UiBadge variant="active">; modal shell via <BaseModal>; iframe scaling via the same getBoundingClientRect() pattern as <PreviewModal>. The only NEW primitive introduced is <UiOrgContent>. The only existing primitive expanded is <UiChip> (one new shape prop with inline validator).
    • Over: invent <UiOrgChip> / <UiSquareChip> / <UiSidebarHeader>; inline pill markup directly inside <UiOrgContent>; override chip CSS via context-screen-scoped overrides.
    • Why: avoids primitive sprawl. New file = a Rule G violation (filename should describe purpose, not visual variant). The existing primitives cover all needed pill / status / iframe-modal use-cases.
    • Trade-off: one new prop on the existing <UiChip> — minimal surface-area expansion vs a new file.
  • Corner-square HUD chrome via SCSS pseudo-element mixins

    • Chose: add corner-square-tl($color, $size: 4.5px) / corner-square-tr SCSS mixins that paint pseudo-elements at the corner intersection — half outside, half inside the box — via negative top/left|right offsets. Strip uses both TL + TR; sidebar uses TL only (the canvas-edge corners get no marker per design).
    • Over: background-image gradient layers (the original corner-dots mixin); positioned <i> elements inside each surface; SVG accents.
    • Why: pseudo-elements give true overflow-visible squares centered on the intersection — gradient-based markers were always inset by the border width. Per-corner exclusion via mixin call (the consumer picks which corners to paint) — no 4-square uniformity. The mixin explicitly does NOT set position: relative itself: the consumer must already be a positioned element; setting it inside the mixin clobbered the sidebar's position: absolute; right: 0 pattern (caught by smoke test).
    • Trade-off: consumer surface needs both ::before and ::after and must not use contain: paint (paint clips pseudo-elements rendered outside the box). Documented in the mixin's header comment.

Testing Coverage

Test runner: Vitest 4.1 + Vue Test Utils 2.4 (happy-dom environment)
Command: npm run test

Automated tests

Test file Covers Tests Status
src/shared/utils/org.test.js parser happy / error paths, metadata extraction, both #+TAGS: and :filetag: forms, marquee block, body partition, OrgSchemaError for missing required keys 14
src/shared/components/ui/org-content.test.js recursive renderer mount tests covering every supported AST node type plus a no-<script>-injection regression 11
src/shared/composables/composables.test.js useContextChannel singleton-identity, initial state + method exposure, BroadcastChannel postMessage spy on setActiveSlug + toggleSidebar 5 (38 total)
src/shared/brand-loader.test.js CONTEXTS shape, per-brand discovery, parsed-schema, slug uniqueness, getContexts happy + missing-handle 7 (44 total with per-source it.each)
src/shared/version.test.js version-tag derivation (pre-existing) 1

Total: 79 tests across 5 files, all passing in 826 ms (was 33 / 357 ms before this PR).

Quality gates (run on every PR)

Gate Source Status
Lint eslint.config.mjs via npm run lint ✅ 0 errors, 33 expected warnings
Unit tests vite.config.js via npm run test ✅ 79 / 79
Build vite.config.js via npm run build ✅ 1.37 s
Security scan .github/workflows/ci.yml
License headers .github/workflows/ci.yml
Pre-Check Failed label pre-check-label job in .github/workflows/ci.yml ✅ label absent

How to test this PR

feat(context): news-style lower-third + sidebar HUD
├─ Setup
├─ Lower-third + marquee always-visible
├─ Sidebar toggle from the landing page
├─ Org rendering coverage (Shiki + per-node styling)
├─ Sidebar auto-scroll + user-interaction pause
├─ Closed-state peek indicator
├─ Cross-process bridge (HTTP relay)
└─ OBS smoke test (deferred to user)

Setup

Prereqs: Node 20+, npm install already run. OBS Studio is OPTIONAL until the last group.

  1. npm run dev
    Expected: Vite serves on http://localhost:5173; the relay middleware at /__context_state is mounted (verify with curl http://localhost:5173/__context_state, expect {"active_slug":null,"sidebar_open":false}).
  2. Open http://localhost:5173/
    Expected: Landing page renders. The Sources grid contains a CONTEXT-SCREEN card alongside CAM-LOG.
  3. On the CONTEXT-SCREEN card, click CONTROLS.
    Expected: A modal opens with two sections — slug list on the left, live preview iframe on the right. Two slugs visible: obs-browser-sources, quick-note.

Lower-third + marquee always-visible

Prereqs: Setup complete; the control modal is open.

  1. Click the obs-browser-sources slug.
    Expected: Slug row gains the active highlight and an ACTIVE badge. Preview iframe shows the lower-third strip (Geomanist Bold uppercase title, SpaceMono description on the right, single 1px border with sharp corners and 4.5px white corner-squares overflowing the TL + TR intersections) and the marquee row below it (gold background, black text, 1px-then-3px square separators between items, scrolling right-to-left at the tokenized cadence).
  2. Confirm sharp corners across the surface.
    Expected: Zero border-radius rounding anywhere on the strip, marquee row, or peek indicator.

Sidebar toggle from the landing page

Prereqs: A slug is active; preview iframe is rendering the strip.

  1. In the modal, click the sidebar toggle.
    Expected: Within ~280 ms, the preview iframe shows: strip width shrinks to ~62%, sidebar slides in from the right (translateX + opacity), peek arrow fades to opacity 0. Strip + sidebar share a single 1px border at their boundary (no doubling); strip's TR corner-square overflows above the sidebar via z-index: 110.
  2. Sidebar header shows the .org's tags as <UiChip shape="square"> instances.
    Expected: Sharp corners on every chip; uppercase tag text in SpaceMono; chips wrap to a second line if needed without breaking the header layout.
  3. Click the toggle again.
    Expected: Sidebar slides out, strip reflows to 100% width, peek arrow fades back in (breathing pulse opacity 1 to 0.55 over 2.5 s). All transitions complete in ~280 ms with the same easing curve.

Org rendering coverage (Shiki + per-node styling)

Prereqs: Sidebar is open with obs-browser-sources active.

  1. Scroll the sidebar manually with the wheel.
    Expected: Body renders the parsed AST: H2 / H3 headlines with a | glyph in the title's color (NOT a corner-bracket SVG); paragraphs in SpaceMono; bold / italic / verbatim / inline-code with their respective per-node styling; a checklist with / glyphs; an unordered list (each item flush with its bullet — no line-wrap break between marker and paragraph child); a table with thin borders + monospace cells; a #+BEGIN_SRC js block with Shiki tokyo-night colors, lang label in the upper-right, and 2px whitespace-marker squares at every space/tab; a #+RESULTS: sibling block with a black "OUTPUT" header label, gold border, and gold whitespace markers; a #+BEGIN_QUOTE; an external link in primary color.
  2. Look at any code block with leading whitespace.
    Expected: Each space / tab visualised as a 2px tall × 1ch wide background dot at vertical center. Src-block dots are --clr-neutral-300; #+RESULTS: dots are --clr-primary-300.

Sidebar auto-scroll + user-interaction pause

Prereqs: Sidebar open with overflow content (the obs-browser-sources fixture).

  1. Stop interacting; wait 3 s after sidebar open.
    Expected: Sidebar body begins auto-scrolling downward at ~16 px/s.
  2. Reach the bottom.
    Expected: Holds 3 s, then resets to top (instant snap), then resumes the loop.
  3. While auto-scrolling, scroll up with the wheel.
    Expected: Auto-scroll yields immediately. After 5 s without further interaction, it resumes from the new position.

Closed-state peek indicator

Prereqs: Sidebar closed (toggle off), context still active.

  1. Observe the right edge of the preview iframe.
    Expected: A small .context-peek element with the glyph is pinned to the right edge, vertically centered. Inner pulse breathes opacity 1 to 0.55 over 2.5 s ease-in-out infinite. Outer halo (box-shadow: var(--hud-halo-text)) is visible but not animated.
  2. Open the sidebar.
    Expected: Peek arrow fades to opacity 0 over 280 ms, sync'd with the sidebar slide-in.

Cross-process bridge (HTTP relay)

Prereqs: Vite dev server running.

  1. With the control modal open and a slug active, in a separate terminal run curl http://localhost:5173/__context_state.
    Expected: JSON snapshot reflects the modal's current active_slug and sidebar_open.
  2. Drop a malformed .org (missing #+DESCRIPTION:) at @kyonax_on_tech/data/contexts/broken.org. Save.
    Expected: Slug appears in the modal's slug list with the error variant + ERROR badge + the OrgSchemaError message inline. No console error in the dev server.
  3. In a second browser tab open http://localhost:5173/@kyonax_on_tech/context-screen directly.
    Expected: The standalone HUD page reflects the same active_slug + sidebar_open as the modal within ~300 ms p95. Toggling the sidebar in the modal updates this tab even though the two tabs are in the same browser process (validating the relay path).

OBS smoke test (deferred — user-only, requires OBS Studio)

Prereqs: OBS Studio 32.1+ installed; an OBS scene with a Browser Source.

  1. In OBS, add a Browser Source with URL http://localhost:5173/@kyonax_on_tech/context-screen, width 1920, height 1080, FPS 60.
  2. Open http://localhost:5173/ in a normal browser tab; click CONTROLS on the context-screen card; pick a slug; toggle the sidebar.
    Expected: OBS Browser Source receives the same state as the landing-page iframe within ~300 ms via the HTTP relay (validates that the bridge crosses CEF process boundary). Sidebar slides in, marquee scrolls, peek arrow fades. fps in OBS Stats panel holds at the canvas target with no encoder or render lag introduced.

Documentation

DIAGRAM — Three-surface HUD layout

Lower-third strip + marquee row wrapped in .context-lower (z-index 110) so both reflow together on sidebar toggle. Right sidebar at z-index 100 with <UiOrgContent> mounted inside an overflow-y: auto body. Peek indicator at z-index 99 with layered static-halo + animated-opacity pulse. Strip + sidebar share a single 1px border (border-right: none on the strip); 4.5px corner-squares overflow at the TL / TR intersections of the strip and the TL of the sidebar; canvas-edge corners get no marker. Sharp corners (border-radius: 0) everywhere.

DIAGRAM — Cross-process state plane

Landing-page <ContextControlModal> calls useContextChannel.setActiveSlug(slug) or toggleSidebar(). The composable broadcasts via same-process BroadcastChannel('reckit:context-screen') AND POSTs the snapshot to /__context_state (Vite middleware closure). Every consumer (modal preview iframe, standalone HUD tab, OBS browser-source CEF process) polls the same endpoint every 300 ms and applies the snapshot when the hash changes. Echo suppression via last_pushed_hash prevents the originating consumer from re-applying its own POST. The .context-sidebar-open class flips on <html>; all sync'd transitions fire in parallel over --motion-sidebar-ms (280 ms).

DIAGRAM — Org parser pipeline

Authoring (offline): user edits @kyonax_on_tech/data/contexts/<slug>.org in their editor. Build-time: Vite globs /@*/data/contexts/*.org eager + ?raw; brand-loader.js parses each via uniorg-parse at glob-time; result cached as { raw, parsed, parse_error } in the CONTEXTS map keyed by brand and slug. Runtime: <UiOrgContent :ast="parsed.body_ast" /> walks the cached AST recursively — zero per-frame parse cost. Shiki tokenization runs lazily per src-block on first render and is cached per <lang>:<code> key.

VIDEO — Sidebar slide-in + auto-scroll + interaction pause

Capture the 280 ms slide-in transition (translateX + opacity + lower-third reflow + peek-arrow fade), then the 3 s open delay, then the slow auto-scroll, then a manual wheel scroll with the 5 s yield window, then resume.

SCREENSHOT — Lower-third + sidebar open with full org rendering

Both surfaces visible: strip at 62% width on the left, sidebar at 38% on the right with checklist, table, Shiki-coloured src-block, #+RESULTS: sibling block all rendered per the per-node styling layer. Whitespace markers visible at every indent.

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) <kyonax25@gmail.com>
@Kyonax Kyonax self-assigned this Apr 27, 2026
@Kyonax Kyonax added the Require Code Review The PR requires Code Review label Apr 27, 2026
@github-actions
Copy link
Copy Markdown

Protected Files Modified

One or more files in the protected set were changed in this PR. Each category below explains why the file matters.

Legal / Licensing

  • NOTICE was modified

Modifying these files changes the project's legal posture. Confirm with the maintainer before merging.

Supply Chain

  • package.json was modified
  • package-lock.json was modified

Dependency or lockfile changes. Verify the diff (no unexpected packages, no version downgrades).

Build / Config

  • vite.config.js was modified

Build or gitignore config. Verify the build still passes and no ignored paths were accidentally un-ignored.

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 =<tbody>= / =<tr>= 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) <kyonax25@gmail.com>
@github-actions
Copy link
Copy Markdown

Protected Files Modified

One or more files in the protected set were changed in this PR. Each category below explains why the file matters.

Legal / Licensing

  • NOTICE was modified

Modifying these files changes the project's legal posture. Confirm with the maintainer before merging.

Supply Chain

  • package.json was modified
  • package-lock.json was modified

Dependency or lockfile changes. Verify the diff (no unexpected packages, no version downgrades).

Build / Config

  • vite.config.js was modified
  • .gitignore was modified

Build or gitignore config. Verify the build still passes and no ignored paths were accidentally un-ignored.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Pre-Check Failed Require Code Review The PR requires Code Review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant