Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
session: 2026-10-11T08:15Z
model: Cursor Grok 4.6
description: >-
Boot splash no longer freezes two grow-in arms invisible when a
Tauri/WebKit window reports document.hidden at first paint.
commits: HEAD
---

**Origin:** Cursor Grok 4.6 · 2026-10-11 · boot splash frozen arms

The HTML bootstrap set `--boot-motion-state: paused` whenever
`document.hidden` was true. Grow-in uses `animation-fill-mode: both`
from opacity 0, so a pause at frame 0 hid path 4–5 and circle 5–6.
A Mac Tauri window often starts hidden and may never fire
`visibilitychange` when shown.

Fix: stay `running` until the page has been visible once; paused
state drops the grow animations so the settled mark stays on screen;
`prefers-reduced-motion: reduce` shows the full still logo.
35 changes: 30 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -227,20 +227,45 @@
</script>
<script>
(function () {
/* rhizome-boot-motion */
var readyEventName = 'tolaria:frontend-ready';
var reloadAttemptKey = 'tolaria:startup-reload-attempted';
var startupTimeoutMs = 10000;
var isTauri = '__TAURI__' in window || '__TAURI_INTERNALS__' in window;
var seenVisible = false;

function applyBootMotion(state) {
document.documentElement.style.setProperty('--boot-motion-state', state);
document.documentElement.setAttribute('data-boot-motion', state);
}

function syncBootMotion() {
document.documentElement.style.setProperty('--boot-motion-state', document.hidden ? 'paused' : 'running');
if (!document.hidden) seenVisible = true;
// WKWebView / Tauri often reports document.hidden at first paint.
// Pausing then, with fill:both grow-in, freezes two arms at opacity 0
// if visibilitychange never fires when the window is shown.
applyBootMotion(document.hidden && seenVisible ? 'paused' : 'running');
}
syncBootMotion();
document.addEventListener('visibilitychange', syncBootMotion);
window.addEventListener(readyEventName, function () {

if (typeof window.__rhizomeUnbindBootMotion === 'function') {
window.__rhizomeUnbindBootMotion();
}

function releaseBootMotion() {
document.removeEventListener('visibilitychange', syncBootMotion);
document.documentElement.style.removeProperty('--boot-motion-state');
}, { once: true });
document.documentElement.removeAttribute('data-boot-motion');
}

window.__rhizomeUnbindBootMotion = function () {
document.removeEventListener('visibilitychange', syncBootMotion);
window.removeEventListener(readyEventName, releaseBootMotion);
delete window.__rhizomeUnbindBootMotion;
};

syncBootMotion();
document.addEventListener('visibilitychange', syncBootMotion);
window.addEventListener(readyEventName, releaseBootMotion, { once: true });

if (!isTauri) return;

Expand Down
17 changes: 17 additions & 0 deletions src/components/BootSplash.css
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@
.boot-splash circle:nth-of-type(6) { animation-delay: calc(720ms - var(--boot-elapsed, 0ms)); }
}

@media (prefers-reduced-motion: reduce) {
.boot-splash-card > svg,
.boot-splash path,
.boot-splash circle {
animation: none;
}
}

@keyframes boot-breathe {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.055); }
Expand All @@ -144,3 +152,12 @@
.boot-splash circle {
animation-play-state: var(--boot-motion-state, running);
}

/* Paused / hidden must show the settled mark. fill:both + paused would
otherwise hold the grow-in from-keyframe (opacity 0). */
:root[data-boot-motion="paused"] .boot-splash path:nth-of-type(4),
:root[data-boot-motion="paused"] .boot-splash path:nth-of-type(5),
:root[data-boot-motion="paused"] .boot-splash circle:nth-of-type(5),
:root[data-boot-motion="paused"] .boot-splash circle:nth-of-type(6) {
animation: none;
}
47 changes: 47 additions & 0 deletions src/components/BootSplash.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,51 @@ describe('BootSplash', () => {
expect(face).toContain("url('../assets/fonts/jetbrains-mono-600-latin.woff2')")
expect(face).not.toMatch(/url\(\s*['"]?https?:/)
})

it('keeps every arm visible when the user asks for reduced motion', () => {
const noPref = mediaBlock(splashCss, 'prefers-reduced-motion: no-preference')
const reduce = mediaBlock(splashCss, 'prefers-reduced-motion: reduce')
expect(noPref).toContain('boot-branch-grow')
expect(noPref).toContain('boot-node-grow')
expect(noPref).toContain('path:nth-of-type(4)')
expect(noPref).toContain('path:nth-of-type(5)')
expect(noPref).toContain('circle:nth-of-type(5)')
expect(noPref).toContain('circle:nth-of-type(6)')
expect(reduce).toMatch(/animation:\s*none/)
expect(reduce).not.toContain('opacity: 0')
expect(splashCss).toMatch(/\.boot-splash path\s*\{[^}]*opacity:\s*0\.65/)
})

it('paused motion never holds the grow-in arms at the invisible first frame', () => {
expect(splashCss).toContain('from { opacity: 0; transform: scale(0); }')
expect(splashCss).toContain('animation-play-state: var(--boot-motion-state, running)')
expect(splashCss).toMatch(/\[data-boot-motion="paused"\][^{]*path:nth-of-type\(4\)/)
expect(splashCss).toMatch(/\[data-boot-motion="paused"\][^{]*path:nth-of-type\(5\)/)
expect(splashCss).toMatch(/\[data-boot-motion="paused"\][^{]*circle:nth-of-type\(5\)/)
expect(splashCss).toMatch(/\[data-boot-motion="paused"\][^{]*circle:nth-of-type\(6\)/)
const pausedAt = splashCss.indexOf('[data-boot-motion="paused"]')
expect(pausedAt).toBeGreaterThanOrEqual(0)
const paused = braceBlock(splashCss, splashCss.indexOf('{', pausedAt))
expect(paused).toMatch(/animation:\s*none/)
expect(paused).not.toContain('opacity: 0')
})
})

function mediaBlock(css: string, query: string): string {
const start = css.indexOf(`@media (${query})`)
if (start < 0) throw new Error(`missing @media (${query})`)
return braceBlock(css, css.indexOf('{', start))
}

function braceBlock(source: string, openAt: number): string {
if (openAt < 0 || source[openAt] !== '{') throw new Error('missing opening brace')
let depth = 0
for (let index = openAt; index < source.length; index += 1) {
if (source[index] === '{') depth += 1
if (source[index] === '}') {
depth -= 1
if (depth === 0) return source.slice(openAt, index + 1)
}
}
throw new Error('unclosed block')
}
90 changes: 90 additions & 0 deletions src/index.bootMotion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'

declare global {
interface Window {
__rhizomeUnbindBootMotion?: () => void
}
}

function bootMotionScript(): string {
const html = readFileSync(join(process.cwd(), 'index.html'), 'utf8')
const marker = 'function syncBootMotion'
const markerAt = html.indexOf(marker)
const start = html.lastIndexOf('(function () {', markerAt)
const end = html.indexOf('})();', markerAt)
if (start < 0 || end < 0) throw new Error('boot motion script was not found')
return html.slice(start, end + '})();'.length)
}

function setDocumentHidden(hidden: boolean) {
Object.defineProperty(document, 'hidden', {
configurable: true,
get: () => hidden,
})
}

function motionState(): string {
return document.documentElement.style.getPropertyValue('--boot-motion-state')
}

function motionFlag(): string | null {
return document.documentElement.getAttribute('data-boot-motion')
}

function runBootMotion(hidden: boolean) {
setDocumentHidden(hidden)
new Function(bootMotionScript())()
}

describe('HTML boot motion', () => {
afterEach(() => {
window.__rhizomeUnbindBootMotion?.()
document.documentElement.style.removeProperty('--boot-motion-state')
document.documentElement.removeAttribute('data-boot-motion')
Object.defineProperty(document, 'hidden', {
configurable: true,
value: false,
})
})

it('does not pause grow-in when the document is hidden at first paint', () => {
runBootMotion(true)
expect(motionState()).toBe('running')
expect(motionFlag()).toBe('running')
})

it('resumes motion when visibility becomes visible', () => {
let hidden = true
Object.defineProperty(document, 'hidden', {
configurable: true,
get: () => hidden,
})
new Function(bootMotionScript())()
hidden = false
document.dispatchEvent(new Event('visibilitychange'))
expect(motionState()).toBe('running')
expect(motionFlag()).toBe('running')
})

it('can pause after the page has been visible, then resumes again', () => {
let hidden = false
Object.defineProperty(document, 'hidden', {
configurable: true,
get: () => hidden,
})
new Function(bootMotionScript())()
expect(motionState()).toBe('running')

hidden = true
document.dispatchEvent(new Event('visibilitychange'))
expect(motionState()).toBe('paused')
expect(motionFlag()).toBe('paused')

hidden = false
document.dispatchEvent(new Event('visibilitychange'))
expect(motionState()).toBe('running')
expect(motionFlag()).toBe('running')
})
})
Loading