{"record":{"id":"43976293ce8bcccd","repo":"moeru-ai/airi","slug":"widgets-element-not-found","errorCode":null,"errorMessage":"Widgets element not found","messagePattern":"Widgets element not found","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/stage-layouts/src/composables/theme-color.ts","lineNumber":23,"sourceCode":"\nimport Color from 'colorjs.io'\n\nimport { withRetry } from '@moeru/std'\nimport { colorFromElement, patchThemeSamplingHtml2CanvasClone } from '@proj-airi/stage-ui/libs'\nimport { useSettings } from '@proj-airi/stage-ui/stores/settings'\nimport { useTheme } from '@proj-airi/ui'\nimport { useDocumentVisibility, useIntervalFn } from '@vueuse/core'\nimport { storeToRefs } from 'pinia'\nimport { nextTick, watch } from 'vue'\n\nimport { BackgroundKind } from '../stores/background'\n\nexport function themeColorFromPropertyOf(colorFromClass: string, property: string): () => Promise<string> {\n  return async () => {\n    const fetchUntilWidgetMounted = withRetry(() => {\n      const widgets = document.querySelector(colorFromClass) as HTMLDivElement | undefined\n      if (!widgets)\n        throw new Error('Widgets element not found')\n\n      return widgets\n    }, { retry: 10, retryDelay: 1000 })\n\n    const widgets = await fetchUntilWidgetMounted()\n    return window.getComputedStyle(widgets).getPropertyValue(property)\n  }\n}\n\nexport function themeColorFromValue(value: string | { light: string, dark: string }): () => Promise<string> {\n  return async () => {\n    if (typeof value === 'string') {\n      return value\n    }\n    else {\n      const { isDark: dark } = useTheme()\n      return dark.value ? value.dark : value.light\n    }","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-layouts/src/composables/theme-color.ts#L5-L41","documentation":"themeColorFromPropertyOf (packages/stage-layouts/src/composables/theme-color.ts) resolves a theme color by querying the DOM for a selector (e.g. the tinted settings card) and reading a computed style property. It wraps document.querySelector in withRetry with 10 attempts spaced 1 s apart; if the selector still matches nothing after ~10 s, 'Widgets element not found' is thrown, meaning the element that carries the color never mounted.","triggerScenarios":"Passing a selector/class that does not exist in the current layout or route, calling the composable before the target component mounts (10 s is not enough on very slow loads), or running in an environment where document/DOM is absent or the component is conditionally v-if'd away.","commonSituations":"Refactoring renames the widget class and the hardcoded selector goes stale, headless/screenshot captures that query before hydration, routes where the tinted widget simply is never rendered.","solutions":["Verify the selector matches a real element in the current DOM (inspect the rendered widget container)","If the element may legitimately be absent, use themeColorFromValue with literal light/dark colors instead","Ensure the call happens after the owning component mounts (onMounted / after nextTick)","Update the selector when the widget's class changes in the layout package"],"exampleFix":"// before\nconst color = themeColorFromPropertyOf('.settings-appearance-card', '--tailwind-border-spacing')\n// after\nimport { themeColorFromValue } from '../composables/theme-color'\nconst color = themeColorFromValue({ light: '#e4e4e7', dark: '#27272a' })","handlingStrategy":"retry","validationCode":"async function readThemeColor(selector: string, property: string, fallback: string): Promise<string> {\n  for (let attempt = 0; attempt < 10; attempt++) {\n    const el = document.querySelector<HTMLElement>(selector)\n    if (el) {\n      const value = window.getComputedStyle(el).getPropertyValue(property).trim()\n      if (value) return value\n    }\n    await new Promise(r => setTimeout(r, 1000))\n  }\n  return fallback // literal color instead of throwing\n}","typeGuard":"function isHTMLElement(value: Element | null): value is HTMLElement {\n  return value instanceof HTMLElement\n}","tryCatchPattern":"try {\n  color = await themeColorFromPropertyOf(selector, property)()\n}\ncatch (error) {\n  if (error instanceof Error && error.message === 'Widgets element not found') {\n    color = '#7c7c7c' // known default for this widget\n  }\n  else throw error\n}","preventionTips":["Use themeColorFromValue with literal colors when the widget may not render","Keep selector strings in one constant next to the component's class definition","Search the codebase for the selector when renaming widget classes"],"tags":["dom","vue","composable","theming","stage-layouts"],"backgroundTag":"dom-element-not-found","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}