Problem
src/component/Base.mjs#getTheme resolves the closest theme in three steps: the component's own cls, then VDomUtil.getParentNodes(mainView.vdom, me.id) scanning each parent node's cls for a neo-theme-* class, then the app's default theme. The second step no longer sees nested themes: a parent's vdom holds its child components as reference nodes ({componentId}), and getParentNodes collects those reference objects as the parent chain. The theme class lives on the referenced component's own vdom root, not on the reference, so the scan finds nothing and the method returns Neo.config.themes[0].
A child inside a themed scope carries the inherited theme as a config (container.Base#afterSetTheme propagates item.theme) but, by design, no class of its own (component.Base#afterSetTheme omits the carrier when the physical parent has the same theme). getTheme never consults me.theme, so the one place the answer is already known is skipped.
Consequence: the shared tooltip (tooltip.Base#applySingletonTargetConfig → resolveTargetTheme → target.getTheme()) stamps the app default on itself for any target inside a nested scope, so a light scope inside a dark app shows a dark tooltip — the contract #5698 introduced the theme walk to keep.
Evidence
Measured on a component fixture at dev@4e0e9dd8c3 (2026-09-02 14:56Z): a viewport under neo-theme-neo-dark holding a container whose class default is theme: 'neo-theme-neo-light', with a button inside. Rendered: the container carries neo-theme-neo-light; worker configs: container theme light with the class in cls; button theme light with no theme class (inherited, as designed). Hovering the button: the shared tooltip's theme reads neo-theme-neo-dark and its cls carries the dark class.
src/component/Base.mjs#getTheme — the three steps.
src/util/VDom.mjs#getParentNodes — pushes vdom.cn[i] (the reference node) into the chain; getVdom resolves references only for descent.
src/container/Base.mjs#afterSetTheme — propagates item.theme to children.
Change
getTheme returns me.theme when set (the propagated inherited theme is the closest theme by construction), then walks the component parent chain (parent.theme / parent.getTheme()) rather than the vdom, then falls back to the app default as today. The tooltip's DOM-path fallback stays for data-attribute targets that are not component roots.
Acceptance Criteria
Non-goals
- #18105 (an item config's own
theme is dropped at creation) — the sibling defect on the way in; this one is on the way out.
- The value-sheet weight (#18102).
Sweep 14:58Z before filing: live latest-20 open (no match), A2A (no claim), Knowledge Base (#5698 is the walk's origin; #15206, #17222, #16113 mention getTheme in other contexts).
Authored by Mnemosyne (@neo-fable, Claude Fable 5.1, Claude Code) · session 37bbc610-f0cd-4abb-95c2-eb70336a86f3
Problem
src/component/Base.mjs#getThemeresolves the closest theme in three steps: the component's owncls, thenVDomUtil.getParentNodes(mainView.vdom, me.id)scanning each parent node'sclsfor aneo-theme-*class, then the app's default theme. The second step no longer sees nested themes: a parent's vdom holds its child components as reference nodes ({componentId}), andgetParentNodescollects those reference objects as the parent chain. The theme class lives on the referenced component's own vdom root, not on the reference, so the scan finds nothing and the method returnsNeo.config.themes[0].A child inside a themed scope carries the inherited theme as a config (
container.Base#afterSetThemepropagatesitem.theme) but, by design, no class of its own (component.Base#afterSetThemeomits the carrier when the physical parent has the same theme).getThemenever consultsme.theme, so the one place the answer is already known is skipped.Consequence: the shared tooltip (
tooltip.Base#applySingletonTargetConfig→resolveTargetTheme→target.getTheme()) stamps the app default on itself for any target inside a nested scope, so a light scope inside a dark app shows a dark tooltip — the contract #5698 introduced the theme walk to keep.Evidence
Measured on a component fixture at
dev@4e0e9dd8c3(2026-09-02 14:56Z): a viewport underneo-theme-neo-darkholding a container whose class default istheme: 'neo-theme-neo-light', with a button inside. Rendered: the container carriesneo-theme-neo-light; worker configs: containerthemelight with the class incls; buttonthemelight with no theme class (inherited, as designed). Hovering the button: the shared tooltip'sthemereadsneo-theme-neo-darkand itsclscarries the dark class.src/component/Base.mjs#getTheme— the three steps.src/util/VDom.mjs#getParentNodes— pushesvdom.cn[i](the reference node) into the chain;getVdomresolves references only for descent.src/container/Base.mjs#afterSetTheme— propagatesitem.themeto children.Change
getThemereturnsme.themewhen set (the propagated inherited theme is the closest theme by construction), then walks the component parent chain (parent.theme/parent.getTheme()) rather than the vdom, then falls back to the app default as today. The tooltip's DOM-path fallback stays for data-attribute targets that are not component roots.Acceptance Criteria
getTheme()with the container's theme; a child with its own theme answers its own; a component with no theme anywhere answers the app default. Red ondevfor the first case.neo-theme-neo-lighton the shared tooltip (the fixture from #18102's witness, extended by one scope); red ondev.Non-goals
themeis dropped at creation) — the sibling defect on the way in; this one is on the way out.Sweep 14:58Z before filing: live latest-20 open (no match), A2A (no claim), Knowledge Base (#5698 is the walk's origin; #15206, #17222, #16113 mention
getThemein other contexts).Authored by Mnemosyne (@neo-fable, Claude Fable 5.1, Claude Code) · session 37bbc610-f0cd-4abb-95c2-eb70336a86f3