@gospore/theme 1.0.162 → 1.0.164

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,14 +27,52 @@ import { Theme } from "@gospore/theme";
27
27
  </Theme>
28
28
  ```
29
29
 
30
- `<Theme>` listens for system changes, stores the resolved mode, exposes a
30
+ `<Theme>` listens for system changes, reads the persisted mode, exposes a
31
31
  `theme` provide/inject value, and toggles the global `dark-mode` attribute so
32
32
  all Design System components react automatically.
33
33
 
34
+ When more than one independent provider is mounted, the most recently mounted
35
+ `<Theme>` owns the global attribute. A nested provider without an explicit
36
+ `mode` inherits its parent and does not take global ownership, which keeps
37
+ accent-only theme layers synchronized with the application shell. A nested
38
+ provider can opt into ownership with an explicit `mode`; removing that prop or
39
+ unmounting it restores the previous provider. Unmounting the last global owner
40
+ restores the document attribute that existed before the first provider mounted.
41
+
34
42
  Rendered brand tones can differ from the raw `accentColor` input. Theme-generated
35
- semantic `surface` / `on-surface` text and icon pairs maintain at least WCAG AA
36
- 4.5:1 contrast in light and dark modes; the default blue aliases follow the same
37
- contract.
43
+ semantic `surface` / `on-surface` pairs and canvas-facing brand text, icons, and
44
+ borders maintain at least WCAG AA 4.5:1 contrast in light and dark modes; the
45
+ default blue aliases follow the same contract. Accent roles are also explicit:
46
+ `--color-text-accent-<color>-<strength>-*` is same-hue text for the normal
47
+ application surface, while `--color-on-accent-<color>-<strength>-*` is the
48
+ contrast-safe foreground for the matching `--color-background-accent-*`
49
+ surface. The available ramps do not provide a same-hue AA foreground for most
50
+ accent surfaces, so the paired `on-accent` tokens preserve the accent in the
51
+ surface and choose a mode-appropriate neutral foreground. `--color-text-subtle-*`
52
+ and `--color-text-subtler-*` use distinct accessible neutral steps so their
53
+ hierarchy does not depend on low-contrast text.
54
+
55
+ The neutral ramps use eased OKLCH lightness rather than equal numeric jumps.
56
+ This keeps adjacent steps perceptually ordered while giving the middle roles
57
+ stable contrast targets: neutral `600` is suitable for meaningful UI graphics,
58
+ `700` is low-emphasis AA text on the normal application surface, and `800` is
59
+ the stronger secondary foreground. Component placeholders use their package
60
+ theme tokens mapped to `800`, because they also need to remain readable on
61
+ interactive input surfaces; base-surface details such as adjacent-month calendar
62
+ dates can use `700`.
63
+
64
+ Status colors follow the same paired-token rule. Use `--color-text-<status>-*`
65
+ for status-colored copy on the normal application surface. When text is rendered
66
+ on a matching status background, use its `--color-<status>-on-soft-*` or
67
+ `--color-<status>-on-strong-*` foreground instead. Interactive warning and danger
68
+ soft surfaces expose hovered and pressed foregrounds matching their background
69
+ states. Component packages map these semantic pairs through their own component
70
+ tokens instead of choosing palette colors in component styles.
71
+ Foreground-only UI such as required markers follows the same rule: component
72
+ tokens map to `--color-text-danger-*`, never to a background token used as text.
73
+ Component implementations consume those package-level tokens; direct global
74
+ color references belong in each component's `theme.css`, so themes can adapt a
75
+ component without overriding its implementation selectors.
38
76
 
39
77
  Brand semantics are now role-based (`surface`, `on-surface`, `border`). Do not
40
78
  rely on `--color-brand-100..1000` as stable UI meaning tokens.
@@ -67,9 +105,17 @@ const mode = ref("auto");
67
105
  </Theme>
68
106
  ```
69
107
 
70
- The switcher is a ready-made dropdown that persists the selected mode to local
71
- storage via `modeStorage`. It emits `update:mode` and updates `<Theme>` when you
72
- use it with `v-model`.
108
+ The switcher is a ready-made dropdown that persists the selected mode in a
109
+ host-only, one-year `SameSite=Lax` cookie via the exported `readStoredMode`,
110
+ `writeStoredMode`, and `applyStoredModeToDocument` helpers. HTTPS writes also
111
+ set `Secure`. Host-only scope intentionally avoids leaking the preference to
112
+ sibling subdomains and works on multi-level public suffixes without guessing a
113
+ registrable domain. On the first write, the helper also expires the shared-domain
114
+ cookie created by older releases before storing the host-only replacement. The
115
+ switcher emits `update:mode` and updates `<Theme>` when you use it with `v-model`.
116
+ Inside a `<Theme>`, the provider remains the sole owner of the global mode attribute;
117
+ the switcher only persists and emits the selection. A standalone switcher continues
118
+ to apply the selected mode directly for backwards compatibility.
73
119
 
74
120
  `ThemeSwitcher` composes `@gospore/dropdown-menu` + `DropdownItemRadio`, so
75
121
  import `@gospore/dropdown-menu/dropdown-menu.css`,
@@ -87,5 +133,8 @@ tokens when you mount it.
87
133
  ## Accessibility & motion
88
134
 
89
135
  `Theme` respects the `prefers-reduced-motion` media query when animating dark /
90
- light transitions. When users prefer reduced motion, color changes happen
91
- without fades.
136
+ light transitions. When users prefer reduced motion, the provider's transition
137
+ happens without a fade. Components may use short, property-scoped color
138
+ transitions for interaction feedback, so those properties can briefly
139
+ interpolate during a theme change. Accessibility and contrast contracts apply
140
+ to the settled light and dark modes.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export { default as Theme } from "./Theme.vue";
2
2
  export { default as ThemeSwitcher } from "./ThemeSwitcher.vue";
3
3
  export { themePortalHostKey, useThemePortalTarget } from "./portalHost";
4
+ export type { ThemePortalTarget } from "./portalHost";
5
+ export { applyStoredModeToDocument, readStoredMode, STORAGE_KEY, VALID_THEME_MODES, writeStoredMode, } from "./modeStorage";
6
+ export type { ThemeMode } from "./modeStorage";
package/dist/theme.cjs ADDED
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("vue");require("what-input");let t=require("@gospore/dropdown-menu");var n=[`auto`,`light`,`dark`],r=`gospore-theme-mode`,i=3600*24*365,a=`(prefers-color-scheme: dark)`;function o(){if(typeof document>`u`)return null;let e=u(r);return f(e)?e:null}function s(e){if(typeof document>`u`||!n.includes(e))return;let t=typeof window<`u`?window.location.hostname:``,a=(typeof window<`u`?window.location.protocol:``)===`https:`,o=[`path=/`,`max-age=${i}`,`SameSite=Lax`];a&&o.push(`Secure`);let s=c(t);s&&(document.cookie=[`${encodeURIComponent(r)}=`,`path=/`,`max-age=0`,`SameSite=Lax`,`domain=${s}`,...a?[`Secure`]:[]].join(`; `)),document.cookie=[`${encodeURIComponent(r)}=${encodeURIComponent(e)}`,...o].join(`; `)}function c(e){if(!e||e===`localhost`)return null;if(/^\d+(?:\.\d+){3}$/.test(e))return e;let t=e.split(`.`);return t.length<=1?null:t.slice(-2).join(`.`)}function l(e){if(typeof document>`u`)return;let t=d(f(e)?e:o()??`auto`);document.documentElement.setAttribute(`dark-mode`,String(t))}function u(e){if(typeof document>`u`||!document.cookie)return null;let t=`${encodeURIComponent(e)}=`,n=document.cookie.split(`; `);for(let e of n)if(e.startsWith(t))try{return decodeURIComponent(e.slice(t.length))}catch{return null}return null}function d(e){if(e===`dark`)return!0;if(e===`light`||typeof window>`u`)return!1;try{let e=typeof window.matchMedia==`function`?window.matchMedia(a):null;return e?e.matches:!1}catch{return!1}}function f(e){return typeof e==`string`&&n.includes(e)}var p=Symbol.for(`gospore.theme.portalHost`);function m(){let t=(0,e.inject)(p,null);return(0,e.computed)(()=>t?.value??`body`)}var h=[],g=new Set,_=new Map,v=[],y,b=`(prefers-color-scheme: dark)`,x=`(prefers-reduced-motion: reduce)`,S=200,C=4.5,w=1.8,T=1.15,E=1.08,D=4.5,ee=3,O=`#ffffff`,k=`#1f2228`,A=(0,e.defineComponent)({name:`Theme`,__name:`Theme`,props:{mode:{type:String,default:void 0,validator:e=>[`auto`,`light`,`dark`].includes(e)},accentColor:{type:String,required:!1,default:void 0},exposeGlobalAccent:{type:Boolean,default:!1}},setup(t){let n=t;function r(){if(typeof document>`u`)return;let e=document.documentElement;if(!e)return;g.forEach(t=>{let n=_.get(t);n?.value?e.style.setProperty(t,n.value,n.priority):e.style.removeProperty(t)}),g.clear();let t=[...h].reverse().find(e=>e.enabled&&e.vars&&Object.keys(e.vars).length>0);if(!t){_.clear();return}Object.entries(t.vars).forEach(([t,n])=>{_.has(t)||_.set(t,{priority:e.style.getPropertyPriority(t),value:e.style.getPropertyValue(t)}),e.style.setProperty(t,n),g.add(t)})}let i=R(b),a=R(x),s=(0,e.inject)(`theme`,null),c=L(),l=(0,e.ref)(c),u=null,d=(0,e.ref)({darkMode:N(c)}),f={darkMode:d.value.darkMode},m=!1;M();let A=(0,e.ref)(null);i.addEventListener(`change`,P),(0,e.watch)(()=>n.mode,()=>{j()},{immediate:!0}),(0,e.watch)(()=>s?.value.darkMode,()=>{n.mode===void 0&&j()}),(0,e.watch)(()=>d.value.darkMode,(e,t)=>{if(f.darkMode=e,!(!m||!I(f))){if(t===void 0){F(e);return}re(()=>{I(f)&&F(e)})}},{immediate:!0}),(0,e.onBeforeUnmount)(()=>{i.removeEventListener(`change`,P),u!==null&&typeof window<`u`&&(window.cancelAnimationFrame(u),u=null),m&&ne(f);let e=h.indexOf($);e!==-1&&h.splice(e,1),r()});function j(){let e=L(),t=N(e);l.value=e,f.darkMode=t,M(),d.value.darkMode=t}function M(){let e=s===null||n.mode!==void 0;e&&!m?m=te(f):!e&&m&&(ne(f),m=!1)}function N(e){return e===`dark`||e!==`light`&&i.matches}function P(e){!e||l.value!==`auto`||(d.value.darkMode=e.matches)}function F(e){typeof document>`u`||document.documentElement.setAttribute(`dark-mode`,String(e))}function te(e){return typeof document>`u`?!1:(v.length===0&&(y=document.documentElement.getAttribute(`dark-mode`)),v.push(e),F(e.darkMode),!0)}function I(e){return v.at(-1)===e}function ne(e){let t=v.indexOf(e);if(t===-1)return;let n=t===v.length-1;if(v.splice(t,1),!n||typeof document>`u`)return;let r=v.at(-1);if(r){F(r.darkMode);return}y===null?document.documentElement.removeAttribute(`dark-mode`):y!==void 0&&document.documentElement.setAttribute(`dark-mode`,y),y=void 0}function re(e){if(a.matches||typeof document>`u`||typeof window>`u`){e();return}let t=document.startViewTransition?.bind(document);if(t){t(()=>{e()});return}e();let n=document.documentElement;typeof n.animate==`function`&&(u!==null&&window.cancelAnimationFrame(u),u=window.requestAnimationFrame(()=>{u=null,n.animate([{opacity:.85},{opacity:1}],{duration:S,easing:`ease-in-out`})}))}function L(){return n.mode===void 0&&s?s.value.darkMode?`dark`:`light`:n.mode??o()??`auto`}function R(e){if(typeof window<`u`&&typeof window.matchMedia==`function`)try{return window.matchMedia(e)}catch{}return{addEventListener(){},addListener(){},dispatchEvent:()=>!1,matches:!1,media:e,onchange:null,removeEventListener(){},removeListener(){}}}(0,e.provide)(`theme`,d),(0,e.provide)(p,A);function z(e){if(!e)return null;let t=e.trim().toLowerCase();if(!/^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.exec(t))return null;if(t.length===4){let e=t[1],n=t[2],r=t[3];return`#${e}${e}${n}${n}${r}${r}`}return t}function B(e){let t=z(e);return t?{r:parseInt(t.slice(1,3),16),g:parseInt(t.slice(3,5),16),b:parseInt(t.slice(5,7),16)}:null}function V({r:e,g:t,b:n}){let r=e=>e.toString(16).padStart(2,`0`);return`#${r(e)}${r(t)}${r(n)}`}function H(e){return Math.max(0,Math.min(1,e))}function U(e){return e<=.04045?e/12.92:((e+.055)/1.055)**2.4}function W(e){let t=H(e);return t<=.0031308?12.92*t:1.055*t**(1/2.4)-.055}function ie({r:e,g:t,b:n}){let r=e/255,i=t/255,a=n/255,o=U(r),s=U(i),c=U(a),l=.4122214708*o+.5363325363*s+.0514459929*c,u=.2119034982*o+.6806995451*s+.1073969566*c,d=.0883024619*o+.2817188376*s+.6299787005*c,f=Math.cbrt(l),p=Math.cbrt(u),m=Math.cbrt(d),h=.2104542553*f+.793617785*p-.0040720468*m,g=1.9779984951*f-2.428592205*p+.4505937099*m,_=.0259040371*f+.7827717662*p-.808675766*m,v=Math.sqrt(g*g+_*_),y=180/Math.PI*Math.atan2(_,g);return y<0&&(y+=360),{l:h,c:v,h:y}}function G(e,t,n,r=!1){let i=n*Math.PI/180,a=t*Math.cos(i),o=t*Math.sin(i),s=e+.3963377774*a+.2158037573*o,c=e-.1055613458*a-.0638541728*o,l=e-.0894841775*a-1.291485548*o,u=r?H(s):s,d=r?H(c):c,f=r?H(l):l;if(!r&&(u<-1e-4||u>1.0001||d<-1e-4||d>1.0001||f<-1e-4||f>1.0001))return null;let p=u*u*u,m=d*d*d,h=f*f*f,g=4.0767416621*p-3.3077115913*m+.2309699292*h,_=-1.2684380046*p+2.6097574011*m-.3413193965*h,v=-.0041960863*p-.7034186147*m+1.707614701*h;if(r)g=H(g),_=H(_),v=H(v);else if(g<-1e-4||g>1.0001||_<-1e-4||_>1.0001||v<-1e-4||v>1.0001)return null;let y=H(W(g)),b=H(W(_)),x=H(W(v));return{r:Math.round(y*255),g:Math.round(b*255),b:Math.round(x*255)}}function K(e,t,n){let r=t;for(let t=0;t<8;t+=1){let t=G(e,r,n);if(t)return t;r*=.9}return G(e,H(r),n,!0)||{r:0,g:0,b:0}}function q(e){let t=B(e);if(!t)return 0;let n=U(t.r/255),r=U(t.g/255),i=U(t.b/255);return .2126*n+.7152*r+.0722*i}function J(e,t){let n=q(e),r=q(t),i=Math.max(n,r),a=Math.min(n,r);return(i+.05)/(a+.05)}function Y(e,t,n={}){let r=n.minContrast??C,i=n.preferDark!==!1,a=Math.max(.01,Math.min(.06,(e.c||.12)*(n.chromaScale??.25))),o=n.initialLightness??(i?.24:.94),s=i?-1:1,c=n.step??.03,l=H(o),u=V(K(l,a,e.h)),d=J(t,u);for(let n=0;n<48;n+=1){let n=V(K(l,a,e.h)),i=J(t,n);if(i>d&&(d=i,u=n),i>=r)return n;if(l=H(l+s*c),l===0||l===1)break}let f=`#000000`,p=`#ffffff`,m=J(t,f),h=J(t,p);return m>=r||h>=r?m>=h?f:p:u}function X(e,t,n={}){let r=n.minContrast??C,i=n.darkCandidate??`#101214`,a=n.lightCandidate??`#ffffff`,o=n.preferredCandidate,s=n.preferredMinContrast??r,c=n.preferredSwitchDelta??0,l=J(t,i),u=J(t,a),d=l>=r,f=u>=r,p=u>=s,m=l>=s;if(d||f)return o===`light`&&p?a:o===`dark`&&m?i:o===`light`&&f?!d||l-u<=c?a:i:o===`dark`&&d?!f||u-l<=c?i:a:d&&f?l>=u?i:a:d?i:a;let h=l>=u;return Y(e,t,{minContrast:r,preferDark:h,initialLightness:h?.2:.96,chromaScale:.1})}let ae=[100,200,300,400,500,600,700,800,900,1e3],Z=[.9,.84,.78,.72,.67,.62,.56,.5,.44,.38],oe=[.28,.4,.54,.7,.86,1,1.08,1.14,1.2,1.24];function Q(e,t,n,r){let i=t[t.length-1];for(let i of t){let t=e[i];if(t&&J(t,n)>=r)return i}return i}function se(e,t,n,r,i){for(let e of n){let n=t[e];if(n&&J(n,r)>=D)return n}return Y(e,r,{chromaScale:1,initialLightness:i?.45:.72,minContrast:D,preferDark:i})}function ce(e){let t=e.l-Z[5],n=e.c>.02?.02:0;return ae.reduce((r,i,a)=>(r[i]=V(K(H(Z[a]+t),Math.min(.4,Math.max(n,e.c*oe[a])),e.h)),r),{})}function le(e,t){let n=[100,200,300,400,500,600,700],r=Q(t,n,O,E),i=n.indexOf(r),a=n[Math.min(i+1,n.length-1)],o=n[Math.min(i+2,n.length-1)],s=[1e3,900,800,700,600,500],c=Q(t,s,k,E),l=s.indexOf(c),u=s[Math.min(l+1,s.length-1)],d=s[Math.min(l+2,s.length-1)],f=Q(t,[200,300,400,500,600,700,800],O,T),p=Q(t,[900,800,700,600,500,400],k,T),m=[600,700,800,900,1e3],h=Q(t,m,O,w),g=m.indexOf(h),_=m[Math.min(g+1,m.length-1)],v=m[Math.min(g+2,m.length-1)],y=[400,300,200,100],b=Q(t,y,k,w),x=y.indexOf(b),S=y[Math.min(x+1,y.length-1)],A=y[Math.min(x+2,y.length-1)],j={"--color-brand-surface-soft-light":t[r],"--color-brand-surface-soft-hovered-light":t[a],"--color-brand-surface-soft-pressed-light":t[o],"--color-brand-surface-soft-dark":t[c],"--color-brand-surface-soft-hovered-dark":t[u],"--color-brand-surface-soft-pressed-dark":t[d],"--color-brand-surface-subtle-light":t[f],"--color-brand-surface-subtle-dark":t[p],"--color-brand-surface-strong-light":t[h],"--color-brand-surface-strong-hovered-light":t[_],"--color-brand-surface-strong-pressed-light":t[v],"--color-brand-surface-strong-dark":t[b],"--color-brand-surface-strong-hovered-dark":t[S],"--color-brand-surface-strong-pressed-dark":t[A]},M=t[600];return j[`--color-brand-on-soft-light`]=J(j[`--color-brand-surface-soft-light`],M)>=C?M:X(e,j[`--color-brand-surface-soft-light`],{minContrast:C}),j[`--color-brand-on-soft-dark`]=X(e,j[`--color-brand-surface-soft-dark`],{minContrast:C}),j[`--color-brand-on-subtle-light`]=X(e,j[`--color-brand-surface-subtle-light`],{minContrast:D,preferredCandidate:`light`,preferredMinContrast:D,preferredSwitchDelta:ee}),j[`--color-brand-on-subtle-dark`]=X(e,j[`--color-brand-surface-subtle-dark`],{minContrast:C}),j[`--color-brand-on-strong-light`]=X(e,j[`--color-brand-surface-strong-light`],{minContrast:D,preferredCandidate:`light`,preferredMinContrast:D,preferredSwitchDelta:ee}),j[`--color-brand-on-strong-dark`]=X(e,j[`--color-brand-surface-strong-dark`],{minContrast:C}),j[`--color-brand-text-light`]=se(e,t,[600,700,800,900,1e3],O,!0),j[`--color-brand-text-dark`]=se(e,t,[400,300,200,100],k,!1),j[`--color-brand-icon-light`]=j[`--color-brand-text-light`],j[`--color-brand-icon-dark`]=j[`--color-brand-text-dark`],j[`--color-brand-border-light`]=j[`--color-brand-text-light`],j[`--color-brand-border-dark`]=j[`--color-brand-text-dark`],j}function ue(e){let t=z(e);if(!t)return null;let n=B(t);if(!n)return null;let r=ie(n),i=ce(r);return i[600]=t,{"--color-brand-seed":t,"--color-brand-100":i[100],"--color-brand-200":i[200],"--color-brand-300":i[300],"--color-brand-400":i[400],"--color-brand-500":i[500],"--color-brand-600":i[600],"--color-brand-700":i[700],"--color-brand-800":i[800],"--color-brand-900":i[900],"--color-brand-1000":i[1e3],...le(r,i)}}let de=(0,e.computed)(()=>ue(n.accentColor)??{}),$={vars:{},enabled:!!n.exposeGlobalAccent};return typeof document<`u`&&h.push($),(0,e.watch)(()=>n.exposeGlobalAccent,e=>{$.enabled=!!e,r()},{immediate:!0}),(0,e.watch)(de,e=>{e&&Object.keys(e).length>0?$.vars={...e}:$.vars={},r()},{immediate:!0}),(t,n)=>((0,e.openBlock)(),(0,e.createElementBlock)(`div`,{class:`theme`,style:(0,e.normalizeStyle)(de.value)},[(0,e.renderSlot)(t.$slots,`default`),(0,e.createElementVNode)(`div`,{ref_key:`portalHost`,ref:A,class:`theme__portal-host`},null,512)],4))}}),j=((e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n})((0,e.defineComponent)({__name:`ThemeSwitcher`,props:{mode:{type:String,default:`auto`,validator:e=>[`auto`,`light`,`dark`].includes(e)}},emits:[`update:mode`],setup(r,{emit:i}){let a=[{value:`auto`,label:`System`},{value:`light`,label:`Light`},{value:`dark`,label:`Dark`}].reduce((e,t)=>(e[t.value]=t.label,e),{}),c=n,u=r,d=i,f=(0,e.inject)(`theme`,null)!==null;f||l();let p=(0,e.ref)(!1),m=o()??u.mode,h=(0,e.ref)(c.includes(m)?m:`auto`);return(0,e.watch)(()=>u.mode,e=>{c.includes(e)&&e!==h.value&&(h.value=e)}),(0,e.watch)(h,e=>{if(!c.includes(e)){h.value=`auto`;return}p.value=!1,d(`update:mode`,e),s(e),f||l(e)},{immediate:!0}),(n,r)=>((0,e.openBlock)(),(0,e.createBlock)((0,e.unref)(t.DropdownMenu),{open:p.value,"onUpdate:open":r[3]||=e=>p.value=e,text:`Theme`,"aria-label":`Select theme mode`},{default:(0,e.withCtx)(()=>[(0,e.createVNode)((0,e.unref)(t.DropdownItemGroup),{title:`Theme`,"item-role":`menuitemradio`},{default:(0,e.withCtx)(()=>[(0,e.createVNode)((0,e.unref)(t.DropdownItemRadio),{checked:h.value,"onUpdate:checked":r[0]||=e=>h.value=e,value:`auto`,label:(0,e.unref)(a).auto,"aria-label":`${(0,e.unref)(a).auto} mode`},null,8,[`checked`,`label`,`aria-label`]),r[4]||=(0,e.createElementVNode)(`hr`,{class:`theme-switcher-divider`},null,-1),(0,e.createVNode)((0,e.unref)(t.DropdownItemRadio),{checked:h.value,"onUpdate:checked":r[1]||=e=>h.value=e,value:`light`,label:(0,e.unref)(a).light,"aria-label":`${(0,e.unref)(a).light} mode`},null,8,[`checked`,`label`,`aria-label`]),(0,e.createVNode)((0,e.unref)(t.DropdownItemRadio),{checked:h.value,"onUpdate:checked":r[2]||=e=>h.value=e,value:`dark`,label:(0,e.unref)(a).dark,"aria-label":`${(0,e.unref)(a).dark} mode`},null,8,[`checked`,`label`,`aria-label`])]),_:1})]),_:1},8,[`open`]))}}),[[`__scopeId`,`data-v-80f281b2`]]);exports.STORAGE_KEY=r,exports.Theme=A,exports.ThemeSwitcher=j,exports.VALID_THEME_MODES=n,exports.applyStoredModeToDocument=l,exports.readStoredMode=o,exports.themePortalHostKey=p,exports.useThemePortalTarget=m,exports.writeStoredMode=s;
2
+ //# sourceMappingURL=theme.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.cjs","names":[],"sources":["../src/modeStorage.ts","../src/portalHost.ts","../src/Theme.vue","../src/Theme.vue","../src/ThemeSwitcher.vue","../src/ThemeSwitcher.vue"],"sourcesContent":["const VALID_THEME_MODES = [\"auto\", \"light\", \"dark\"] as const;\nexport type ThemeMode = (typeof VALID_THEME_MODES)[number];\nconst STORAGE_KEY = \"gospore-theme-mode\";\nconst ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365;\nconst DARK_MODE_QUERY = \"(prefers-color-scheme: dark)\";\n\nexport function readStoredMode(): ThemeMode | null {\n if (typeof document === \"undefined\") {\n return null;\n }\n const stored = getCookie(STORAGE_KEY);\n return isThemeMode(stored) ? stored : null;\n}\n\nexport function writeStoredMode(mode: ThemeMode) {\n if (typeof document === \"undefined\" || !VALID_THEME_MODES.includes(mode)) {\n return;\n }\n const hostname = typeof window !== \"undefined\" ? window.location.hostname : \"\";\n const protocol = typeof window !== \"undefined\" ? window.location.protocol : \"\";\n const secure = protocol === \"https:\";\n const attributes = [\"path=/\", `max-age=${ONE_YEAR_IN_SECONDS}`, \"SameSite=Lax\"];\n if (secure) {\n attributes.push(\"Secure\");\n }\n\n const legacyDomain = getLegacyCookieDomain(hostname);\n if (legacyDomain) {\n document.cookie = [\n `${encodeURIComponent(STORAGE_KEY)}=`,\n \"path=/\",\n \"max-age=0\",\n \"SameSite=Lax\",\n `domain=${legacyDomain}`,\n ...(secure ? [\"Secure\"] : []),\n ].join(\"; \");\n }\n\n document.cookie = [\n `${encodeURIComponent(STORAGE_KEY)}=${encodeURIComponent(mode)}`,\n ...attributes,\n ].join(\"; \");\n}\n\n// Releases before host-only storage guessed a shared domain from the final two hostname labels.\n// Expire that exact legacy scope before writing the replacement so an older cookie cannot win when\n// a browser serializes duplicate names by creation time. Invalid public-suffix guesses are ignored\n// by the browser, just as they were when originally written.\nfunction getLegacyCookieDomain(hostname: string) {\n if (!hostname || hostname === \"localhost\") {\n return null;\n }\n const isIpAddress = /^\\d+(?:\\.\\d+){3}$/.test(hostname);\n if (isIpAddress) {\n return hostname;\n }\n const parts = hostname.split(\".\");\n if (parts.length <= 1) {\n return null;\n }\n return parts.slice(-2).join(\".\");\n}\n\nexport function applyStoredModeToDocument(mode?: string | null) {\n if (typeof document === \"undefined\") {\n return;\n }\n const resolvedMode = isThemeMode(mode) ? mode : (readStoredMode() ?? \"auto\");\n const darkMode = resolveDarkMode(resolvedMode);\n document.documentElement.setAttribute(\"dark-mode\", String(darkMode));\n}\n\nexport { VALID_THEME_MODES, STORAGE_KEY };\n\nfunction getCookie(name: string) {\n if (typeof document === \"undefined\" || !document.cookie) {\n return null;\n }\n const encodedName = `${encodeURIComponent(name)}=`;\n const cookies = document.cookie.split(\"; \");\n for (const cookie of cookies) {\n if (cookie.startsWith(encodedName)) {\n try {\n return decodeURIComponent(cookie.slice(encodedName.length));\n } catch {\n return null;\n }\n }\n }\n return null;\n}\n\nfunction resolveDarkMode(mode: string) {\n if (mode === \"dark\") {\n return true;\n }\n if (mode === \"light\") {\n return false;\n }\n if (typeof window === \"undefined\") {\n return false;\n }\n try {\n const mediaQueryList =\n typeof window.matchMedia === \"function\" ? window.matchMedia(DARK_MODE_QUERY) : null;\n return mediaQueryList ? mediaQueryList.matches : false;\n } catch {\n return false;\n }\n}\n\nfunction isThemeMode(value: string | null | undefined): value is ThemeMode {\n return typeof value === \"string\" && (VALID_THEME_MODES as readonly string[]).includes(value);\n}\n","import { computed, inject } from \"vue\";\nimport type { InjectionKey, Ref } from \"vue\";\n\nexport type ThemePortalTarget = HTMLElement | string;\n\nexport const themePortalHostKey = Symbol.for(\"gospore.theme.portalHost\") as InjectionKey<\n Ref<HTMLElement | null>\n>;\n\nexport function useThemePortalTarget() {\n const hostRef = inject(themePortalHostKey, null);\n const fallbackTarget: ThemePortalTarget = \"body\";\n\n return computed<ThemePortalTarget>(() => hostRef?.value ?? fallbackTarget);\n}\n","<script lang=\"ts\">\ninterface GlobalAccentEntry {\n vars: Record<string, string>;\n enabled: boolean;\n}\n\ninterface GlobalModeEntry {\n darkMode: boolean;\n}\n\nconst globalAccentStack: GlobalAccentEntry[] = [];\nconst appliedGlobalAccentKeys = new Set<string>();\nconst originalGlobalAccentValues = new Map<string, { priority: string; value: string }>();\nconst globalModeStack: GlobalModeEntry[] = [];\nlet originalGlobalDarkModeAttribute: string | null | undefined;\n\nconst DARK_MODE_QUERY = \"(prefers-color-scheme: dark)\"\nconst PREFERS_REDUCED_MOTION_QUERY = \"(prefers-reduced-motion: reduce)\"\nconst THEME_TRANSITION_DURATION = 200\nconst BRAND_MIN_CONTRAST = 4.5\nconst BRAND_MIN_SURFACE_CONTRAST = 1.8\nconst BRAND_MIN_SUBTLE_SURFACE_CONTRAST = 1.15\nconst BRAND_MIN_SOFT_SURFACE_CONTRAST = 1.08\nconst BRAND_UI_TEXT_MIN_CONTRAST = 4.5\nconst BRAND_LIGHT_TEXT_SWITCH_DELTA = 3\nconst LIGHT_CANVAS_HEX = \"#ffffff\"\nconst DARK_CANVAS_HEX = \"#1f2228\"</script>\n\n<template>\n <div class=\"theme\" :style=\"brandVars\">\n <slot />\n <div ref=\"portalHost\" class=\"theme__portal-host\"></div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { provide, ref, watch, onBeforeUnmount, computed, inject } from \"vue\";\nimport type { PropType, Ref } from \"vue\";\nimport { readStoredMode, type ThemeMode } from \"./modeStorage\";\nimport \"what-input\";\nimport { themePortalHostKey } from \"./portalHost\";\n\ninterface RgbColor {\n r: number;\n g: number;\n b: number;\n}\n\ninterface OklchColor {\n l: number;\n c: number;\n h: number;\n}\n\ntype BrandRamp = Record<number, string>;\ntype BrandVars = Record<string, string>;\n\ninterface ReadableTextOptions {\n minContrast?: number;\n preferDark?: boolean;\n chromaScale?: number;\n initialLightness?: number;\n step?: number;\n}\n\ninterface ForegroundOptions extends ReadableTextOptions {\n darkCandidate?: string;\n lightCandidate?: string;\n preferredCandidate?: \"dark\" | \"light\";\n preferredMinContrast?: number;\n preferredSwitchDelta?: number;\n}\n\ndefineOptions({\n name: \"Theme\",\n});\ndefineSlots<{\n /** Application content that receives theme tokens and portal context. */\n default?: () => unknown;\n}>();\nconst props = defineProps({\n /**\n * Controls which color mode is active. auto follows the OS preference (and persisted storage\n * when used with ThemeSwitcher).\n */\n mode: {\n type: String as PropType<ThemeMode | undefined>,\n default: undefined,\n validator: (value: string) => [\"auto\", \"light\", \"dark\"].includes(value),\n },\n /**\n * Optional hex accent color (\"#RRGGBB\" or \"#RGB\"). When provided, Theme derives contrast-safe\n * semantic brand tokens and exposes them on the root .theme element (and the global document\n * for portals).\n */\n accentColor: {\n type: String,\n required: false,\n default: undefined,\n },\n /**\n * When true the brand vars are also exposed on <html>, allowing portals attached outside the\n * Theme subtree to inherit the accent colors.\n */\n exposeGlobalAccent: {\n type: Boolean,\n default: false,\n },\n});\n\nfunction applyGlobalAccentFromStack() {\n if (typeof document === \"undefined\") {\n return;\n }\n const root = document.documentElement;\n if (!root) {\n return;\n }\n\n appliedGlobalAccentKeys.forEach((key) => {\n const original = originalGlobalAccentValues.get(key);\n if (original?.value) {\n root.style.setProperty(key, original.value, original.priority);\n } else {\n root.style.removeProperty(key);\n }\n });\n appliedGlobalAccentKeys.clear();\n\n const activeEntry = [...globalAccentStack]\n .reverse()\n .find((entry) => entry.enabled && entry.vars && Object.keys(entry.vars).length > 0);\n\n if (!activeEntry) {\n originalGlobalAccentValues.clear();\n return;\n }\n\n Object.entries(activeEntry.vars).forEach(([key, value]) => {\n if (!originalGlobalAccentValues.has(key)) {\n originalGlobalAccentValues.set(key, {\n priority: root.style.getPropertyPriority(key),\n value: root.style.getPropertyValue(key),\n });\n }\n root.style.setProperty(key, value);\n appliedGlobalAccentKeys.add(key);\n });\n}\n\n\n\n\nconst darkModeMedia = getMediaQueryList(DARK_MODE_QUERY);\nconst prefersReducedMotionMedia = getMediaQueryList(PREFERS_REDUCED_MOTION_QUERY);\nconst parentTheme = inject<Ref<{ darkMode: boolean }> | null>(\"theme\", null);\n\nconst initialMode = getEffectiveMode();\nconst currentMode = ref(initialMode);\nlet transitionAnimationFrameID: number | null = null;\n\nconst theme = ref({\n darkMode: resolveDarkMode(initialMode),\n});\nconst modeStackEntry: GlobalModeEntry = {\n darkMode: theme.value.darkMode,\n};\nlet modeStackEntryRegistered = false;\nsyncGlobalModeOwnership();\nconst portalHost = ref<HTMLElement | null>(null);\n\ndarkModeMedia.addEventListener(\"change\", handleSystemPreferenceChange);\n\nwatch(\n () => props.mode,\n () => {\n syncDarkMode();\n },\n { immediate: true },\n);\nwatch(\n () => parentTheme?.value.darkMode,\n () => {\n if (props.mode === undefined) {\n syncDarkMode();\n }\n },\n);\n\nwatch(\n () => theme.value.darkMode,\n (darkMode, previousValue) => {\n modeStackEntry.darkMode = darkMode;\n if (!modeStackEntryRegistered || !isActiveGlobalModeEntry(modeStackEntry)) {\n return;\n }\n if (previousValue === undefined) {\n setDocumentDarkModeAttribute(darkMode);\n return;\n }\n withThemeTransition(() => {\n if (isActiveGlobalModeEntry(modeStackEntry)) {\n setDocumentDarkModeAttribute(darkMode);\n }\n });\n },\n { immediate: true },\n);\n\nonBeforeUnmount(() => {\n darkModeMedia.removeEventListener(\"change\", handleSystemPreferenceChange);\n if (transitionAnimationFrameID !== null && typeof window !== \"undefined\") {\n window.cancelAnimationFrame(transitionAnimationFrameID);\n transitionAnimationFrameID = null;\n }\n if (modeStackEntryRegistered) {\n unregisterGlobalModeEntry(modeStackEntry);\n }\n const index = globalAccentStack.indexOf(accentStackEntry);\n if (index !== -1) {\n globalAccentStack.splice(index, 1);\n }\n applyGlobalAccentFromStack();\n});\n\nfunction syncDarkMode() {\n const mode = getEffectiveMode();\n const darkMode = resolveDarkMode(mode);\n currentMode.value = mode;\n modeStackEntry.darkMode = darkMode;\n syncGlobalModeOwnership();\n theme.value.darkMode = darkMode;\n}\n\nfunction syncGlobalModeOwnership() {\n const shouldOwnGlobalMode = parentTheme === null || props.mode !== undefined;\n if (shouldOwnGlobalMode && !modeStackEntryRegistered) {\n modeStackEntryRegistered = registerGlobalModeEntry(modeStackEntry);\n } else if (!shouldOwnGlobalMode && modeStackEntryRegistered) {\n unregisterGlobalModeEntry(modeStackEntry);\n modeStackEntryRegistered = false;\n }\n}\n\nfunction resolveDarkMode(mode: ThemeMode) {\n if (mode === \"dark\") {\n return true;\n }\n if (mode === \"light\") {\n return false;\n }\n return darkModeMedia.matches;\n}\n\nfunction handleSystemPreferenceChange(event: MediaQueryListEvent) {\n if (!event || currentMode.value !== \"auto\") {\n return;\n }\n theme.value.darkMode = event.matches;\n}\n\nfunction setDocumentDarkModeAttribute(darkMode: boolean) {\n if (typeof document === \"undefined\") {\n return;\n }\n document.documentElement.setAttribute(\"dark-mode\", String(darkMode));\n}\n\nfunction registerGlobalModeEntry(entry: GlobalModeEntry) {\n if (typeof document === \"undefined\") {\n return false;\n }\n if (globalModeStack.length === 0) {\n originalGlobalDarkModeAttribute = document.documentElement.getAttribute(\"dark-mode\");\n }\n globalModeStack.push(entry);\n setDocumentDarkModeAttribute(entry.darkMode);\n return true;\n}\n\nfunction isActiveGlobalModeEntry(entry: GlobalModeEntry) {\n return globalModeStack.at(-1) === entry;\n}\n\nfunction unregisterGlobalModeEntry(entry: GlobalModeEntry) {\n const index = globalModeStack.indexOf(entry);\n if (index === -1) {\n return;\n }\n\n const wasActive = index === globalModeStack.length - 1;\n globalModeStack.splice(index, 1);\n if (!wasActive || typeof document === \"undefined\") {\n return;\n }\n\n const previousEntry = globalModeStack.at(-1);\n if (previousEntry) {\n setDocumentDarkModeAttribute(previousEntry.darkMode);\n return;\n }\n\n if (originalGlobalDarkModeAttribute === null) {\n document.documentElement.removeAttribute(\"dark-mode\");\n } else if (originalGlobalDarkModeAttribute !== undefined) {\n document.documentElement.setAttribute(\"dark-mode\", originalGlobalDarkModeAttribute);\n }\n originalGlobalDarkModeAttribute = undefined;\n}\n\nfunction withThemeTransition(applyTheme: () => void) {\n if (\n prefersReducedMotionMedia.matches ||\n typeof document === \"undefined\" ||\n typeof window === \"undefined\"\n ) {\n applyTheme();\n return;\n }\n const startViewTransition = document.startViewTransition?.bind(document);\n if (startViewTransition) {\n startViewTransition(() => {\n applyTheme();\n });\n return;\n }\n applyTheme();\n const element = document.documentElement;\n if (typeof element.animate !== \"function\") {\n return;\n }\n if (transitionAnimationFrameID !== null) {\n window.cancelAnimationFrame(transitionAnimationFrameID);\n }\n transitionAnimationFrameID = window.requestAnimationFrame(() => {\n transitionAnimationFrameID = null;\n element.animate([{ opacity: 0.85 }, { opacity: 1 }], {\n duration: THEME_TRANSITION_DURATION,\n easing: \"ease-in-out\",\n });\n });\n}\n\nfunction getEffectiveMode(): ThemeMode {\n if (props.mode === undefined && parentTheme) {\n return parentTheme.value.darkMode ? \"dark\" : \"light\";\n }\n return props.mode ?? readStoredMode() ?? \"auto\";\n}\n\nfunction getMediaQueryList(query: string): MediaQueryList {\n if (typeof window !== \"undefined\" && typeof window.matchMedia === \"function\") {\n try {\n return window.matchMedia(query);\n } catch {\n // Fall through to a non-matching server-safe implementation.\n }\n }\n\n return {\n addEventListener() {},\n addListener() {},\n dispatchEvent: () => false,\n matches: false,\n media: query,\n onchange: null,\n removeEventListener() {},\n removeListener() {},\n };\n}\n\nprovide(\"theme\", theme);\nprovide(themePortalHostKey, portalHost);\n\n// --- Accent helpers -------------------------------------------------------\n\n\nfunction normalizeHex(hex: string | undefined) {\n if (!hex) return null;\n const h = hex.trim().toLowerCase();\n const m = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.exec(h);\n if (!m) return null;\n if (h.length === 4) {\n // expand #rgb -> #rrggbb\n const r = h[1],\n g = h[2],\n b = h[3];\n return `#${r}${r}${g}${g}${b}${b}`;\n }\n return h;\n}\n\nfunction hexToRgb(hex: string | undefined): RgbColor | null {\n const h = normalizeHex(hex);\n if (!h) return null;\n const r = parseInt(h.slice(1, 3), 16);\n const g = parseInt(h.slice(3, 5), 16);\n const b = parseInt(h.slice(5, 7), 16);\n return { r, g, b };\n}\n\nfunction rgbToHex({ r, g, b }: RgbColor) {\n const toHex = (value: number) => value.toString(16).padStart(2, \"0\");\n return `#${toHex(r)}${toHex(g)}${toHex(b)}`;\n}\n\nfunction clamp01(value: number) {\n return Math.max(0, Math.min(1, value));\n}\n\nfunction srgbChannelToLinear(channel: number) {\n if (channel <= 0.04045) {\n return channel / 12.92;\n }\n return Math.pow((channel + 0.055) / 1.055, 2.4);\n}\n\nfunction linearChannelToSrgb(channel: number) {\n const normalized = clamp01(channel);\n if (normalized <= 0.0031308) {\n return 12.92 * normalized;\n }\n return 1.055 * Math.pow(normalized, 1 / 2.4) - 0.055;\n}\n\nfunction rgbToOklch({ r, g, b }: RgbColor): OklchColor {\n const rNorm = r / 255;\n const gNorm = g / 255;\n const bNorm = b / 255;\n\n const rLin = srgbChannelToLinear(rNorm);\n const gLin = srgbChannelToLinear(gNorm);\n const bLin = srgbChannelToLinear(bNorm);\n\n const l = 0.4122214708 * rLin + 0.5363325363 * gLin + 0.0514459929 * bLin;\n const m = 0.2119034982 * rLin + 0.6806995451 * gLin + 0.1073969566 * bLin;\n const s = 0.0883024619 * rLin + 0.2817188376 * gLin + 0.6299787005 * bLin;\n\n const lRoot = Math.cbrt(l);\n const mRoot = Math.cbrt(m);\n const sRoot = Math.cbrt(s);\n\n const oklabL = 0.2104542553 * lRoot + 0.793617785 * mRoot - 0.0040720468 * sRoot;\n const oklabA = 1.9779984951 * lRoot - 2.428592205 * mRoot + 0.4505937099 * sRoot;\n const oklabB = 0.0259040371 * lRoot + 0.7827717662 * mRoot - 0.808675766 * sRoot;\n\n const chroma = Math.sqrt(oklabA * oklabA + oklabB * oklabB);\n let hue = Math.atan2(oklabB, oklabA) * (180 / Math.PI);\n if (hue < 0) {\n hue += 360;\n }\n\n return { l: oklabL, c: chroma, h: hue };\n}\n\nfunction tryOklchToSrgb(l: number, c: number, h: number, clampLinear = false): RgbColor | null {\n const angle = (h * Math.PI) / 180;\n const a = c * Math.cos(angle);\n const b = c * Math.sin(angle);\n\n const lComponent = l + 0.3963377774 * a + 0.2158037573 * b;\n const mComponent = l - 0.1055613458 * a - 0.0638541728 * b;\n const sComponent = l - 0.0894841775 * a - 1.291485548 * b;\n\n const lCube = clampLinear ? clamp01(lComponent) : lComponent;\n const mCube = clampLinear ? clamp01(mComponent) : mComponent;\n const sCube = clampLinear ? clamp01(sComponent) : sComponent;\n\n if (!clampLinear) {\n const outOfRange =\n lCube < -0.0001 ||\n lCube > 1.0001 ||\n mCube < -0.0001 ||\n mCube > 1.0001 ||\n sCube < -0.0001 ||\n sCube > 1.0001;\n if (outOfRange) {\n return null;\n }\n }\n\n const lLin = lCube * lCube * lCube;\n const mLin = mCube * mCube * mCube;\n const sLin = sCube * sCube * sCube;\n\n let rLin = 4.0767416621 * lLin - 3.3077115913 * mLin + 0.2309699292 * sLin;\n let gLin = -1.2684380046 * lLin + 2.6097574011 * mLin - 0.3413193965 * sLin;\n let bLin = -0.0041960863 * lLin - 0.7034186147 * mLin + 1.707614701 * sLin;\n\n if (clampLinear) {\n rLin = clamp01(rLin);\n gLin = clamp01(gLin);\n bLin = clamp01(bLin);\n } else {\n const outOfRange =\n rLin < -0.0001 ||\n rLin > 1.0001 ||\n gLin < -0.0001 ||\n gLin > 1.0001 ||\n bLin < -0.0001 ||\n bLin > 1.0001;\n if (outOfRange) {\n return null;\n }\n }\n\n const r = clamp01(linearChannelToSrgb(rLin));\n const g = clamp01(linearChannelToSrgb(gLin));\n const bValue = clamp01(linearChannelToSrgb(bLin));\n\n return {\n r: Math.round(r * 255),\n g: Math.round(g * 255),\n b: Math.round(bValue * 255),\n };\n}\n\nfunction oklchToSrgb(l: number, c: number, h: number): RgbColor {\n let chroma = c;\n for (let attempt = 0; attempt < 8; attempt += 1) {\n const rgb = tryOklchToSrgb(l, chroma, h);\n if (rgb) {\n return rgb;\n }\n chroma *= 0.9;\n }\n return tryOklchToSrgb(l, clamp01(chroma), h, true) || { r: 0, g: 0, b: 0 };\n}\n\nfunction relativeLuminance(hex: string) {\n const rgb = hexToRgb(hex);\n if (!rgb) {\n return 0;\n }\n const r = srgbChannelToLinear(rgb.r / 255);\n const g = srgbChannelToLinear(rgb.g / 255);\n const b = srgbChannelToLinear(rgb.b / 255);\n return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n}\n\nfunction contrastRatio(hexA: string, hexB: string) {\n const lumA = relativeLuminance(hexA);\n const lumB = relativeLuminance(hexB);\n const lighter = Math.max(lumA, lumB);\n const darker = Math.min(lumA, lumB);\n return (lighter + 0.05) / (darker + 0.05);\n}\n\nfunction makeReadableTextOnSurface(\n baseOklch: OklchColor,\n surfaceHex: string,\n options: ReadableTextOptions = {},\n) {\n const minContrast = options.minContrast ?? BRAND_MIN_CONTRAST;\n const preferDark = options.preferDark !== false;\n const textChroma = Math.max(\n 0.01,\n Math.min(0.06, (baseOklch.c || 0.12) * (options.chromaScale ?? 0.25)),\n );\n const initialLightness = options.initialLightness ?? (preferDark ? 0.24 : 0.94);\n const direction = preferDark ? -1 : 1;\n const step = options.step ?? 0.03;\n let lightness = clamp01(initialLightness);\n let bestHex = rgbToHex(oklchToSrgb(lightness, textChroma, baseOklch.h));\n let bestContrast = contrastRatio(surfaceHex, bestHex);\n\n for (let attempt = 0; attempt < 48; attempt += 1) {\n const textHex = rgbToHex(oklchToSrgb(lightness, textChroma, baseOklch.h));\n const ratio = contrastRatio(surfaceHex, textHex);\n\n if (ratio > bestContrast) {\n bestContrast = ratio;\n bestHex = textHex;\n }\n if (ratio >= minContrast) {\n return textHex;\n }\n\n lightness = clamp01(lightness + direction * step);\n if (lightness === 0 || lightness === 1) {\n break;\n }\n }\n\n const fallbackDark = \"#000000\";\n const fallbackLight = \"#ffffff\";\n const darkRatio = contrastRatio(surfaceHex, fallbackDark);\n const lightRatio = contrastRatio(surfaceHex, fallbackLight);\n\n if (darkRatio >= minContrast || lightRatio >= minContrast) {\n return darkRatio >= lightRatio ? fallbackDark : fallbackLight;\n }\n\n return bestHex;\n}\n\nfunction pickForegroundColor(\n baseOklch: OklchColor,\n surfaceHex: string,\n options: ForegroundOptions = {},\n) {\n const minContrast = options.minContrast ?? BRAND_MIN_CONTRAST;\n const darkCandidate = options.darkCandidate ?? \"#101214\";\n const lightCandidate = options.lightCandidate ?? \"#ffffff\";\n const preferredCandidate = options.preferredCandidate;\n const preferredMinContrast = options.preferredMinContrast ?? minContrast;\n const preferredSwitchDelta = options.preferredSwitchDelta ?? 0;\n const darkRatio = contrastRatio(surfaceHex, darkCandidate);\n const lightRatio = contrastRatio(surfaceHex, lightCandidate);\n const darkPasses = darkRatio >= minContrast;\n const lightPasses = lightRatio >= minContrast;\n const preferredLightPasses = lightRatio >= preferredMinContrast;\n const preferredDarkPasses = darkRatio >= preferredMinContrast;\n\n if (darkPasses || lightPasses) {\n if (preferredCandidate === \"light\" && preferredLightPasses) {\n return lightCandidate;\n }\n if (preferredCandidate === \"dark\" && preferredDarkPasses) {\n return darkCandidate;\n }\n\n if (preferredCandidate === \"light\" && lightPasses) {\n if (!darkPasses || darkRatio - lightRatio <= preferredSwitchDelta) {\n return lightCandidate;\n }\n return darkCandidate;\n }\n if (preferredCandidate === \"dark\" && darkPasses) {\n if (!lightPasses || lightRatio - darkRatio <= preferredSwitchDelta) {\n return darkCandidate;\n }\n return lightCandidate;\n }\n\n if (darkPasses && lightPasses) {\n return darkRatio >= lightRatio ? darkCandidate : lightCandidate;\n }\n return darkPasses ? darkCandidate : lightCandidate;\n }\n\n const preferDark = darkRatio >= lightRatio;\n return makeReadableTextOnSurface(baseOklch, surfaceHex, {\n minContrast,\n preferDark,\n initialLightness: preferDark ? 0.2 : 0.96,\n chromaScale: 0.1,\n });\n}\n\nconst BRAND_STOPS = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000];\nconst BRAND_LIGHTNESS_CURVE = [0.9, 0.84, 0.78, 0.72, 0.67, 0.62, 0.56, 0.5, 0.44, 0.38];\nconst BRAND_CHROMA_MULTIPLIERS = [0.28, 0.4, 0.54, 0.7, 0.86, 1, 1.08, 1.14, 1.2, 1.24];\n\n\n\n\n\n\n\n\nfunction pickRampStopForContrast(\n ramp: BrandRamp,\n stopCandidates: number[],\n referenceHex: string,\n minContrast: number,\n) {\n const fallbackStop = stopCandidates[stopCandidates.length - 1];\n for (const stop of stopCandidates) {\n const candidate = ramp[stop];\n if (!candidate) {\n continue;\n }\n if (contrastRatio(candidate, referenceHex) >= minContrast) {\n return stop;\n }\n }\n return fallbackStop;\n}\n\nfunction pickReadableRampText(\n baseOklch: OklchColor,\n ramp: BrandRamp,\n stopCandidates: number[],\n surfaceHex: string,\n preferDark: boolean,\n) {\n for (const stop of stopCandidates) {\n const candidate = ramp[stop];\n if (candidate && contrastRatio(candidate, surfaceHex) >= BRAND_UI_TEXT_MIN_CONTRAST) {\n return candidate;\n }\n }\n\n return makeReadableTextOnSurface(baseOklch, surfaceHex, {\n chromaScale: 1,\n initialLightness: preferDark ? 0.45 : 0.72,\n minContrast: BRAND_UI_TEXT_MIN_CONTRAST,\n preferDark,\n });\n}\n\nfunction makeBrandRampFromSeed(baseOklch: OklchColor): BrandRamp {\n const lightnessOffset = baseOklch.l - BRAND_LIGHTNESS_CURVE[5];\n const minSeedChroma = baseOklch.c > 0.02 ? 0.02 : 0;\n\n return BRAND_STOPS.reduce<BrandRamp>((acc, stop, index) => {\n const lightness = clamp01(BRAND_LIGHTNESS_CURVE[index] + lightnessOffset);\n const chroma = Math.min(\n 0.4,\n Math.max(minSeedChroma, baseOklch.c * BRAND_CHROMA_MULTIPLIERS[index]),\n );\n acc[stop] = rgbToHex(oklchToSrgb(lightness, chroma, baseOklch.h));\n return acc;\n }, {});\n}\n\nfunction makeSemanticBrandTokens(baseOklch: OklchColor, ramp: BrandRamp): BrandVars {\n const lightSoftStops = [100, 200, 300, 400, 500, 600, 700];\n const lightSoftStop = pickRampStopForContrast(\n ramp,\n lightSoftStops,\n LIGHT_CANVAS_HEX,\n BRAND_MIN_SOFT_SURFACE_CONTRAST,\n );\n const lightSoftIndex = lightSoftStops.indexOf(lightSoftStop);\n const lightSoftHoveredStop =\n lightSoftStops[Math.min(lightSoftIndex + 1, lightSoftStops.length - 1)];\n const lightSoftPressedStop =\n lightSoftStops[Math.min(lightSoftIndex + 2, lightSoftStops.length - 1)];\n\n const darkSoftStops = [1000, 900, 800, 700, 600, 500];\n const darkSoftStop = pickRampStopForContrast(\n ramp,\n darkSoftStops,\n DARK_CANVAS_HEX,\n BRAND_MIN_SOFT_SURFACE_CONTRAST,\n );\n const darkSoftIndex = darkSoftStops.indexOf(darkSoftStop);\n const darkSoftHoveredStop =\n darkSoftStops[Math.min(darkSoftIndex + 1, darkSoftStops.length - 1)];\n const darkSoftPressedStop =\n darkSoftStops[Math.min(darkSoftIndex + 2, darkSoftStops.length - 1)];\n\n const lightSubtleStops = [200, 300, 400, 500, 600, 700, 800];\n const lightSubtleStop = pickRampStopForContrast(\n ramp,\n lightSubtleStops,\n LIGHT_CANVAS_HEX,\n BRAND_MIN_SUBTLE_SURFACE_CONTRAST,\n );\n const darkSubtleStops = [900, 800, 700, 600, 500, 400];\n const darkSubtleStop = pickRampStopForContrast(\n ramp,\n darkSubtleStops,\n DARK_CANVAS_HEX,\n BRAND_MIN_SUBTLE_SURFACE_CONTRAST,\n );\n\n const lightStrongStops = [600, 700, 800, 900, 1000];\n const lightStrongStop = pickRampStopForContrast(\n ramp,\n lightStrongStops,\n LIGHT_CANVAS_HEX,\n BRAND_MIN_SURFACE_CONTRAST,\n );\n const lightStrongIndex = lightStrongStops.indexOf(lightStrongStop);\n const lightStrongHoveredStop =\n lightStrongStops[Math.min(lightStrongIndex + 1, lightStrongStops.length - 1)];\n const lightStrongPressedStop =\n lightStrongStops[Math.min(lightStrongIndex + 2, lightStrongStops.length - 1)];\n\n const darkStrongStops = [400, 300, 200, 100];\n const darkStrongStop = pickRampStopForContrast(\n ramp,\n darkStrongStops,\n DARK_CANVAS_HEX,\n BRAND_MIN_SURFACE_CONTRAST,\n );\n const darkStrongIndex = darkStrongStops.indexOf(darkStrongStop);\n const darkStrongHoveredStop =\n darkStrongStops[Math.min(darkStrongIndex + 1, darkStrongStops.length - 1)];\n const darkStrongPressedStop =\n darkStrongStops[Math.min(darkStrongIndex + 2, darkStrongStops.length - 1)];\n\n const tokens: BrandVars = {\n \"--color-brand-surface-soft-light\": ramp[lightSoftStop],\n \"--color-brand-surface-soft-hovered-light\": ramp[lightSoftHoveredStop],\n \"--color-brand-surface-soft-pressed-light\": ramp[lightSoftPressedStop],\n \"--color-brand-surface-soft-dark\": ramp[darkSoftStop],\n \"--color-brand-surface-soft-hovered-dark\": ramp[darkSoftHoveredStop],\n \"--color-brand-surface-soft-pressed-dark\": ramp[darkSoftPressedStop],\n \"--color-brand-surface-subtle-light\": ramp[lightSubtleStop],\n \"--color-brand-surface-subtle-dark\": ramp[darkSubtleStop],\n \"--color-brand-surface-strong-light\": ramp[lightStrongStop],\n \"--color-brand-surface-strong-hovered-light\": ramp[lightStrongHoveredStop],\n \"--color-brand-surface-strong-pressed-light\": ramp[lightStrongPressedStop],\n \"--color-brand-surface-strong-dark\": ramp[darkStrongStop],\n \"--color-brand-surface-strong-hovered-dark\": ramp[darkStrongHoveredStop],\n \"--color-brand-surface-strong-pressed-dark\": ramp[darkStrongPressedStop],\n };\n\n const preferredSoftLightForeground = ramp[600];\n const softLightContrast = contrastRatio(\n tokens[\"--color-brand-surface-soft-light\"],\n preferredSoftLightForeground,\n );\n tokens[\"--color-brand-on-soft-light\"] =\n softLightContrast >= BRAND_MIN_CONTRAST\n ? preferredSoftLightForeground\n : pickForegroundColor(baseOklch, tokens[\"--color-brand-surface-soft-light\"], {\n minContrast: BRAND_MIN_CONTRAST,\n });\n tokens[\"--color-brand-on-soft-dark\"] = pickForegroundColor(\n baseOklch,\n tokens[\"--color-brand-surface-soft-dark\"],\n {\n minContrast: BRAND_MIN_CONTRAST,\n },\n );\n tokens[\"--color-brand-on-subtle-light\"] = pickForegroundColor(\n baseOklch,\n tokens[\"--color-brand-surface-subtle-light\"],\n {\n minContrast: BRAND_UI_TEXT_MIN_CONTRAST,\n preferredCandidate: \"light\",\n preferredMinContrast: BRAND_UI_TEXT_MIN_CONTRAST,\n preferredSwitchDelta: BRAND_LIGHT_TEXT_SWITCH_DELTA,\n },\n );\n tokens[\"--color-brand-on-subtle-dark\"] = pickForegroundColor(\n baseOklch,\n tokens[\"--color-brand-surface-subtle-dark\"],\n {\n minContrast: BRAND_MIN_CONTRAST,\n },\n );\n tokens[\"--color-brand-on-strong-light\"] = pickForegroundColor(\n baseOklch,\n tokens[\"--color-brand-surface-strong-light\"],\n {\n minContrast: BRAND_UI_TEXT_MIN_CONTRAST,\n preferredCandidate: \"light\",\n preferredMinContrast: BRAND_UI_TEXT_MIN_CONTRAST,\n preferredSwitchDelta: BRAND_LIGHT_TEXT_SWITCH_DELTA,\n },\n );\n tokens[\"--color-brand-on-strong-dark\"] = pickForegroundColor(\n baseOklch,\n tokens[\"--color-brand-surface-strong-dark\"],\n {\n minContrast: BRAND_MIN_CONTRAST,\n },\n );\n\n tokens[\"--color-brand-text-light\"] = pickReadableRampText(\n baseOklch,\n ramp,\n [600, 700, 800, 900, 1000],\n LIGHT_CANVAS_HEX,\n true,\n );\n tokens[\"--color-brand-text-dark\"] = pickReadableRampText(\n baseOklch,\n ramp,\n [400, 300, 200, 100],\n DARK_CANVAS_HEX,\n false,\n );\n tokens[\"--color-brand-icon-light\"] = tokens[\"--color-brand-text-light\"];\n tokens[\"--color-brand-icon-dark\"] = tokens[\"--color-brand-text-dark\"];\n tokens[\"--color-brand-border-light\"] = tokens[\"--color-brand-text-light\"];\n tokens[\"--color-brand-border-dark\"] = tokens[\"--color-brand-text-dark\"];\n\n return tokens;\n}\n\nfunction makeBrandTokens(baseHex: string | undefined): BrandVars | null {\n const norm = normalizeHex(baseHex);\n if (!norm) {\n return null;\n }\n\n const baseRgb = hexToRgb(norm);\n if (!baseRgb) {\n return null;\n }\n\n const baseOklch = rgbToOklch(baseRgb);\n const ramp = makeBrandRampFromSeed(baseOklch);\n // Keep the incoming seed exact for debugging and transparency.\n ramp[600] = norm;\n\n return {\n \"--color-brand-seed\": norm,\n \"--color-brand-100\": ramp[100],\n \"--color-brand-200\": ramp[200],\n \"--color-brand-300\": ramp[300],\n \"--color-brand-400\": ramp[400],\n \"--color-brand-500\": ramp[500],\n \"--color-brand-600\": ramp[600],\n \"--color-brand-700\": ramp[700],\n \"--color-brand-800\": ramp[800],\n \"--color-brand-900\": ramp[900],\n \"--color-brand-1000\": ramp[1000],\n ...makeSemanticBrandTokens(baseOklch, ramp),\n };\n}\n\nconst brandVars = computed(() => makeBrandTokens(props.accentColor) ?? {});\n\nconst accentStackEntry = {\n vars: {},\n enabled: Boolean(props.exposeGlobalAccent),\n};\nif (typeof document !== \"undefined\") {\n globalAccentStack.push(accentStackEntry);\n}\n\nwatch(\n () => props.exposeGlobalAccent,\n (enabled) => {\n accentStackEntry.enabled = Boolean(enabled);\n applyGlobalAccentFromStack();\n },\n { immediate: true },\n);\n\nwatch(\n brandVars,\n (vars) => {\n if (vars && Object.keys(vars).length > 0) {\n accentStackEntry.vars = { ...vars };\n } else {\n accentStackEntry.vars = {};\n }\n applyGlobalAccentFromStack();\n },\n { immediate: true },\n);\n</script>\n\n<style lang=\"less\">\n@import \"index.less\";\n</style>\n","<script lang=\"ts\">\ninterface GlobalAccentEntry {\n vars: Record<string, string>;\n enabled: boolean;\n}\n\ninterface GlobalModeEntry {\n darkMode: boolean;\n}\n\nconst globalAccentStack: GlobalAccentEntry[] = [];\nconst appliedGlobalAccentKeys = new Set<string>();\nconst originalGlobalAccentValues = new Map<string, { priority: string; value: string }>();\nconst globalModeStack: GlobalModeEntry[] = [];\nlet originalGlobalDarkModeAttribute: string | null | undefined;\n</script>\n\n<template>\n <div class=\"theme\" :style=\"brandVars\">\n <slot />\n <div ref=\"portalHost\" class=\"theme__portal-host\"></div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { provide, ref, watch, onBeforeUnmount, computed, inject } from \"vue\";\nimport type { PropType, Ref } from \"vue\";\nimport { readStoredMode, type ThemeMode } from \"./modeStorage\";\nimport \"what-input\";\nimport { themePortalHostKey } from \"./portalHost\";\n\ninterface RgbColor {\n r: number;\n g: number;\n b: number;\n}\n\ninterface OklchColor {\n l: number;\n c: number;\n h: number;\n}\n\ntype BrandRamp = Record<number, string>;\ntype BrandVars = Record<string, string>;\n\ninterface ReadableTextOptions {\n minContrast?: number;\n preferDark?: boolean;\n chromaScale?: number;\n initialLightness?: number;\n step?: number;\n}\n\ninterface ForegroundOptions extends ReadableTextOptions {\n darkCandidate?: string;\n lightCandidate?: string;\n preferredCandidate?: \"dark\" | \"light\";\n preferredMinContrast?: number;\n preferredSwitchDelta?: number;\n}\n\ndefineOptions({\n name: \"Theme\",\n});\ndefineSlots<{\n /** Application content that receives theme tokens and portal context. */\n default?: () => unknown;\n}>();\nconst props = defineProps({\n /**\n * Controls which color mode is active. auto follows the OS preference (and persisted storage\n * when used with ThemeSwitcher).\n */\n mode: {\n type: String as PropType<ThemeMode | undefined>,\n default: undefined,\n validator: (value: string) => [\"auto\", \"light\", \"dark\"].includes(value),\n },\n /**\n * Optional hex accent color (\"#RRGGBB\" or \"#RGB\"). When provided, Theme derives contrast-safe\n * semantic brand tokens and exposes them on the root .theme element (and the global document\n * for portals).\n */\n accentColor: {\n type: String,\n required: false,\n default: undefined,\n },\n /**\n * When true the brand vars are also exposed on <html>, allowing portals attached outside the\n * Theme subtree to inherit the accent colors.\n */\n exposeGlobalAccent: {\n type: Boolean,\n default: false,\n },\n});\n\nfunction applyGlobalAccentFromStack() {\n if (typeof document === \"undefined\") {\n return;\n }\n const root = document.documentElement;\n if (!root) {\n return;\n }\n\n appliedGlobalAccentKeys.forEach((key) => {\n const original = originalGlobalAccentValues.get(key);\n if (original?.value) {\n root.style.setProperty(key, original.value, original.priority);\n } else {\n root.style.removeProperty(key);\n }\n });\n appliedGlobalAccentKeys.clear();\n\n const activeEntry = [...globalAccentStack]\n .reverse()\n .find((entry) => entry.enabled && entry.vars && Object.keys(entry.vars).length > 0);\n\n if (!activeEntry) {\n originalGlobalAccentValues.clear();\n return;\n }\n\n Object.entries(activeEntry.vars).forEach(([key, value]) => {\n if (!originalGlobalAccentValues.has(key)) {\n originalGlobalAccentValues.set(key, {\n priority: root.style.getPropertyPriority(key),\n value: root.style.getPropertyValue(key),\n });\n }\n root.style.setProperty(key, value);\n appliedGlobalAccentKeys.add(key);\n });\n}\n\nconst DARK_MODE_QUERY = \"(prefers-color-scheme: dark)\";\nconst PREFERS_REDUCED_MOTION_QUERY = \"(prefers-reduced-motion: reduce)\";\nconst THEME_TRANSITION_DURATION = 200;\nconst darkModeMedia = getMediaQueryList(DARK_MODE_QUERY);\nconst prefersReducedMotionMedia = getMediaQueryList(PREFERS_REDUCED_MOTION_QUERY);\nconst parentTheme = inject<Ref<{ darkMode: boolean }> | null>(\"theme\", null);\n\nconst initialMode = getEffectiveMode();\nconst currentMode = ref(initialMode);\nlet transitionAnimationFrameID: number | null = null;\n\nconst theme = ref({\n darkMode: resolveDarkMode(initialMode),\n});\nconst modeStackEntry: GlobalModeEntry = {\n darkMode: theme.value.darkMode,\n};\nlet modeStackEntryRegistered = false;\nsyncGlobalModeOwnership();\nconst portalHost = ref<HTMLElement | null>(null);\n\ndarkModeMedia.addEventListener(\"change\", handleSystemPreferenceChange);\n\nwatch(\n () => props.mode,\n () => {\n syncDarkMode();\n },\n { immediate: true },\n);\nwatch(\n () => parentTheme?.value.darkMode,\n () => {\n if (props.mode === undefined) {\n syncDarkMode();\n }\n },\n);\n\nwatch(\n () => theme.value.darkMode,\n (darkMode, previousValue) => {\n modeStackEntry.darkMode = darkMode;\n if (!modeStackEntryRegistered || !isActiveGlobalModeEntry(modeStackEntry)) {\n return;\n }\n if (previousValue === undefined) {\n setDocumentDarkModeAttribute(darkMode);\n return;\n }\n withThemeTransition(() => {\n if (isActiveGlobalModeEntry(modeStackEntry)) {\n setDocumentDarkModeAttribute(darkMode);\n }\n });\n },\n { immediate: true },\n);\n\nonBeforeUnmount(() => {\n darkModeMedia.removeEventListener(\"change\", handleSystemPreferenceChange);\n if (transitionAnimationFrameID !== null && typeof window !== \"undefined\") {\n window.cancelAnimationFrame(transitionAnimationFrameID);\n transitionAnimationFrameID = null;\n }\n if (modeStackEntryRegistered) {\n unregisterGlobalModeEntry(modeStackEntry);\n }\n const index = globalAccentStack.indexOf(accentStackEntry);\n if (index !== -1) {\n globalAccentStack.splice(index, 1);\n }\n applyGlobalAccentFromStack();\n});\n\nfunction syncDarkMode() {\n const mode = getEffectiveMode();\n const darkMode = resolveDarkMode(mode);\n currentMode.value = mode;\n modeStackEntry.darkMode = darkMode;\n syncGlobalModeOwnership();\n theme.value.darkMode = darkMode;\n}\n\nfunction syncGlobalModeOwnership() {\n const shouldOwnGlobalMode = parentTheme === null || props.mode !== undefined;\n if (shouldOwnGlobalMode && !modeStackEntryRegistered) {\n modeStackEntryRegistered = registerGlobalModeEntry(modeStackEntry);\n } else if (!shouldOwnGlobalMode && modeStackEntryRegistered) {\n unregisterGlobalModeEntry(modeStackEntry);\n modeStackEntryRegistered = false;\n }\n}\n\nfunction resolveDarkMode(mode: ThemeMode) {\n if (mode === \"dark\") {\n return true;\n }\n if (mode === \"light\") {\n return false;\n }\n return darkModeMedia.matches;\n}\n\nfunction handleSystemPreferenceChange(event: MediaQueryListEvent) {\n if (!event || currentMode.value !== \"auto\") {\n return;\n }\n theme.value.darkMode = event.matches;\n}\n\nfunction setDocumentDarkModeAttribute(darkMode: boolean) {\n if (typeof document === \"undefined\") {\n return;\n }\n document.documentElement.setAttribute(\"dark-mode\", String(darkMode));\n}\n\nfunction registerGlobalModeEntry(entry: GlobalModeEntry) {\n if (typeof document === \"undefined\") {\n return false;\n }\n if (globalModeStack.length === 0) {\n originalGlobalDarkModeAttribute = document.documentElement.getAttribute(\"dark-mode\");\n }\n globalModeStack.push(entry);\n setDocumentDarkModeAttribute(entry.darkMode);\n return true;\n}\n\nfunction isActiveGlobalModeEntry(entry: GlobalModeEntry) {\n return globalModeStack.at(-1) === entry;\n}\n\nfunction unregisterGlobalModeEntry(entry: GlobalModeEntry) {\n const index = globalModeStack.indexOf(entry);\n if (index === -1) {\n return;\n }\n\n const wasActive = index === globalModeStack.length - 1;\n globalModeStack.splice(index, 1);\n if (!wasActive || typeof document === \"undefined\") {\n return;\n }\n\n const previousEntry = globalModeStack.at(-1);\n if (previousEntry) {\n setDocumentDarkModeAttribute(previousEntry.darkMode);\n return;\n }\n\n if (originalGlobalDarkModeAttribute === null) {\n document.documentElement.removeAttribute(\"dark-mode\");\n } else if (originalGlobalDarkModeAttribute !== undefined) {\n document.documentElement.setAttribute(\"dark-mode\", originalGlobalDarkModeAttribute);\n }\n originalGlobalDarkModeAttribute = undefined;\n}\n\nfunction withThemeTransition(applyTheme: () => void) {\n if (\n prefersReducedMotionMedia.matches ||\n typeof document === \"undefined\" ||\n typeof window === \"undefined\"\n ) {\n applyTheme();\n return;\n }\n const startViewTransition = document.startViewTransition?.bind(document);\n if (startViewTransition) {\n startViewTransition(() => {\n applyTheme();\n });\n return;\n }\n applyTheme();\n const element = document.documentElement;\n if (typeof element.animate !== \"function\") {\n return;\n }\n if (transitionAnimationFrameID !== null) {\n window.cancelAnimationFrame(transitionAnimationFrameID);\n }\n transitionAnimationFrameID = window.requestAnimationFrame(() => {\n transitionAnimationFrameID = null;\n element.animate([{ opacity: 0.85 }, { opacity: 1 }], {\n duration: THEME_TRANSITION_DURATION,\n easing: \"ease-in-out\",\n });\n });\n}\n\nfunction getEffectiveMode(): ThemeMode {\n if (props.mode === undefined && parentTheme) {\n return parentTheme.value.darkMode ? \"dark\" : \"light\";\n }\n return props.mode ?? readStoredMode() ?? \"auto\";\n}\n\nfunction getMediaQueryList(query: string): MediaQueryList {\n if (typeof window !== \"undefined\" && typeof window.matchMedia === \"function\") {\n try {\n return window.matchMedia(query);\n } catch {\n // Fall through to a non-matching server-safe implementation.\n }\n }\n\n return {\n addEventListener() {},\n addListener() {},\n dispatchEvent: () => false,\n matches: false,\n media: query,\n onchange: null,\n removeEventListener() {},\n removeListener() {},\n };\n}\n\nprovide(\"theme\", theme);\nprovide(themePortalHostKey, portalHost);\n\n// --- Accent helpers -------------------------------------------------------\nconst BRAND_MIN_CONTRAST = 4.5;\n\nfunction normalizeHex(hex: string | undefined) {\n if (!hex) return null;\n const h = hex.trim().toLowerCase();\n const m = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.exec(h);\n if (!m) return null;\n if (h.length === 4) {\n // expand #rgb -> #rrggbb\n const r = h[1],\n g = h[2],\n b = h[3];\n return `#${r}${r}${g}${g}${b}${b}`;\n }\n return h;\n}\n\nfunction hexToRgb(hex: string | undefined): RgbColor | null {\n const h = normalizeHex(hex);\n if (!h) return null;\n const r = parseInt(h.slice(1, 3), 16);\n const g = parseInt(h.slice(3, 5), 16);\n const b = parseInt(h.slice(5, 7), 16);\n return { r, g, b };\n}\n\nfunction rgbToHex({ r, g, b }: RgbColor) {\n const toHex = (value: number) => value.toString(16).padStart(2, \"0\");\n return `#${toHex(r)}${toHex(g)}${toHex(b)}`;\n}\n\nfunction clamp01(value: number) {\n return Math.max(0, Math.min(1, value));\n}\n\nfunction srgbChannelToLinear(channel: number) {\n if (channel <= 0.04045) {\n return channel / 12.92;\n }\n return Math.pow((channel + 0.055) / 1.055, 2.4);\n}\n\nfunction linearChannelToSrgb(channel: number) {\n const normalized = clamp01(channel);\n if (normalized <= 0.0031308) {\n return 12.92 * normalized;\n }\n return 1.055 * Math.pow(normalized, 1 / 2.4) - 0.055;\n}\n\nfunction rgbToOklch({ r, g, b }: RgbColor): OklchColor {\n const rNorm = r / 255;\n const gNorm = g / 255;\n const bNorm = b / 255;\n\n const rLin = srgbChannelToLinear(rNorm);\n const gLin = srgbChannelToLinear(gNorm);\n const bLin = srgbChannelToLinear(bNorm);\n\n const l = 0.4122214708 * rLin + 0.5363325363 * gLin + 0.0514459929 * bLin;\n const m = 0.2119034982 * rLin + 0.6806995451 * gLin + 0.1073969566 * bLin;\n const s = 0.0883024619 * rLin + 0.2817188376 * gLin + 0.6299787005 * bLin;\n\n const lRoot = Math.cbrt(l);\n const mRoot = Math.cbrt(m);\n const sRoot = Math.cbrt(s);\n\n const oklabL = 0.2104542553 * lRoot + 0.793617785 * mRoot - 0.0040720468 * sRoot;\n const oklabA = 1.9779984951 * lRoot - 2.428592205 * mRoot + 0.4505937099 * sRoot;\n const oklabB = 0.0259040371 * lRoot + 0.7827717662 * mRoot - 0.808675766 * sRoot;\n\n const chroma = Math.sqrt(oklabA * oklabA + oklabB * oklabB);\n let hue = Math.atan2(oklabB, oklabA) * (180 / Math.PI);\n if (hue < 0) {\n hue += 360;\n }\n\n return { l: oklabL, c: chroma, h: hue };\n}\n\nfunction tryOklchToSrgb(l: number, c: number, h: number, clampLinear = false): RgbColor | null {\n const angle = (h * Math.PI) / 180;\n const a = c * Math.cos(angle);\n const b = c * Math.sin(angle);\n\n const lComponent = l + 0.3963377774 * a + 0.2158037573 * b;\n const mComponent = l - 0.1055613458 * a - 0.0638541728 * b;\n const sComponent = l - 0.0894841775 * a - 1.291485548 * b;\n\n const lCube = clampLinear ? clamp01(lComponent) : lComponent;\n const mCube = clampLinear ? clamp01(mComponent) : mComponent;\n const sCube = clampLinear ? clamp01(sComponent) : sComponent;\n\n if (!clampLinear) {\n const outOfRange =\n lCube < -0.0001 ||\n lCube > 1.0001 ||\n mCube < -0.0001 ||\n mCube > 1.0001 ||\n sCube < -0.0001 ||\n sCube > 1.0001;\n if (outOfRange) {\n return null;\n }\n }\n\n const lLin = lCube * lCube * lCube;\n const mLin = mCube * mCube * mCube;\n const sLin = sCube * sCube * sCube;\n\n let rLin = 4.0767416621 * lLin - 3.3077115913 * mLin + 0.2309699292 * sLin;\n let gLin = -1.2684380046 * lLin + 2.6097574011 * mLin - 0.3413193965 * sLin;\n let bLin = -0.0041960863 * lLin - 0.7034186147 * mLin + 1.707614701 * sLin;\n\n if (clampLinear) {\n rLin = clamp01(rLin);\n gLin = clamp01(gLin);\n bLin = clamp01(bLin);\n } else {\n const outOfRange =\n rLin < -0.0001 ||\n rLin > 1.0001 ||\n gLin < -0.0001 ||\n gLin > 1.0001 ||\n bLin < -0.0001 ||\n bLin > 1.0001;\n if (outOfRange) {\n return null;\n }\n }\n\n const r = clamp01(linearChannelToSrgb(rLin));\n const g = clamp01(linearChannelToSrgb(gLin));\n const bValue = clamp01(linearChannelToSrgb(bLin));\n\n return {\n r: Math.round(r * 255),\n g: Math.round(g * 255),\n b: Math.round(bValue * 255),\n };\n}\n\nfunction oklchToSrgb(l: number, c: number, h: number): RgbColor {\n let chroma = c;\n for (let attempt = 0; attempt < 8; attempt += 1) {\n const rgb = tryOklchToSrgb(l, chroma, h);\n if (rgb) {\n return rgb;\n }\n chroma *= 0.9;\n }\n return tryOklchToSrgb(l, clamp01(chroma), h, true) || { r: 0, g: 0, b: 0 };\n}\n\nfunction relativeLuminance(hex: string) {\n const rgb = hexToRgb(hex);\n if (!rgb) {\n return 0;\n }\n const r = srgbChannelToLinear(rgb.r / 255);\n const g = srgbChannelToLinear(rgb.g / 255);\n const b = srgbChannelToLinear(rgb.b / 255);\n return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n}\n\nfunction contrastRatio(hexA: string, hexB: string) {\n const lumA = relativeLuminance(hexA);\n const lumB = relativeLuminance(hexB);\n const lighter = Math.max(lumA, lumB);\n const darker = Math.min(lumA, lumB);\n return (lighter + 0.05) / (darker + 0.05);\n}\n\nfunction makeReadableTextOnSurface(\n baseOklch: OklchColor,\n surfaceHex: string,\n options: ReadableTextOptions = {},\n) {\n const minContrast = options.minContrast ?? BRAND_MIN_CONTRAST;\n const preferDark = options.preferDark !== false;\n const textChroma = Math.max(\n 0.01,\n Math.min(0.06, (baseOklch.c || 0.12) * (options.chromaScale ?? 0.25)),\n );\n const initialLightness = options.initialLightness ?? (preferDark ? 0.24 : 0.94);\n const direction = preferDark ? -1 : 1;\n const step = options.step ?? 0.03;\n let lightness = clamp01(initialLightness);\n let bestHex = rgbToHex(oklchToSrgb(lightness, textChroma, baseOklch.h));\n let bestContrast = contrastRatio(surfaceHex, bestHex);\n\n for (let attempt = 0; attempt < 48; attempt += 1) {\n const textHex = rgbToHex(oklchToSrgb(lightness, textChroma, baseOklch.h));\n const ratio = contrastRatio(surfaceHex, textHex);\n\n if (ratio > bestContrast) {\n bestContrast = ratio;\n bestHex = textHex;\n }\n if (ratio >= minContrast) {\n return textHex;\n }\n\n lightness = clamp01(lightness + direction * step);\n if (lightness === 0 || lightness === 1) {\n break;\n }\n }\n\n const fallbackDark = \"#000000\";\n const fallbackLight = \"#ffffff\";\n const darkRatio = contrastRatio(surfaceHex, fallbackDark);\n const lightRatio = contrastRatio(surfaceHex, fallbackLight);\n\n if (darkRatio >= minContrast || lightRatio >= minContrast) {\n return darkRatio >= lightRatio ? fallbackDark : fallbackLight;\n }\n\n return bestHex;\n}\n\nfunction pickForegroundColor(\n baseOklch: OklchColor,\n surfaceHex: string,\n options: ForegroundOptions = {},\n) {\n const minContrast = options.minContrast ?? BRAND_MIN_CONTRAST;\n const darkCandidate = options.darkCandidate ?? \"#101214\";\n const lightCandidate = options.lightCandidate ?? \"#ffffff\";\n const preferredCandidate = options.preferredCandidate;\n const preferredMinContrast = options.preferredMinContrast ?? minContrast;\n const preferredSwitchDelta = options.preferredSwitchDelta ?? 0;\n const darkRatio = contrastRatio(surfaceHex, darkCandidate);\n const lightRatio = contrastRatio(surfaceHex, lightCandidate);\n const darkPasses = darkRatio >= minContrast;\n const lightPasses = lightRatio >= minContrast;\n const preferredLightPasses = lightRatio >= preferredMinContrast;\n const preferredDarkPasses = darkRatio >= preferredMinContrast;\n\n if (darkPasses || lightPasses) {\n if (preferredCandidate === \"light\" && preferredLightPasses) {\n return lightCandidate;\n }\n if (preferredCandidate === \"dark\" && preferredDarkPasses) {\n return darkCandidate;\n }\n\n if (preferredCandidate === \"light\" && lightPasses) {\n if (!darkPasses || darkRatio - lightRatio <= preferredSwitchDelta) {\n return lightCandidate;\n }\n return darkCandidate;\n }\n if (preferredCandidate === \"dark\" && darkPasses) {\n if (!lightPasses || lightRatio - darkRatio <= preferredSwitchDelta) {\n return darkCandidate;\n }\n return lightCandidate;\n }\n\n if (darkPasses && lightPasses) {\n return darkRatio >= lightRatio ? darkCandidate : lightCandidate;\n }\n return darkPasses ? darkCandidate : lightCandidate;\n }\n\n const preferDark = darkRatio >= lightRatio;\n return makeReadableTextOnSurface(baseOklch, surfaceHex, {\n minContrast,\n preferDark,\n initialLightness: preferDark ? 0.2 : 0.96,\n chromaScale: 0.1,\n });\n}\n\nconst BRAND_STOPS = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000];\nconst BRAND_LIGHTNESS_CURVE = [0.9, 0.84, 0.78, 0.72, 0.67, 0.62, 0.56, 0.5, 0.44, 0.38];\nconst BRAND_CHROMA_MULTIPLIERS = [0.28, 0.4, 0.54, 0.7, 0.86, 1, 1.08, 1.14, 1.2, 1.24];\nconst BRAND_MIN_SURFACE_CONTRAST = 1.8;\nconst BRAND_MIN_SUBTLE_SURFACE_CONTRAST = 1.15;\nconst BRAND_MIN_SOFT_SURFACE_CONTRAST = 1.08;\nconst BRAND_UI_TEXT_MIN_CONTRAST = 4.5;\nconst BRAND_LIGHT_TEXT_SWITCH_DELTA = 3;\nconst LIGHT_CANVAS_HEX = \"#ffffff\";\nconst DARK_CANVAS_HEX = \"#1f2228\";\n\nfunction pickRampStopForContrast(\n ramp: BrandRamp,\n stopCandidates: number[],\n referenceHex: string,\n minContrast: number,\n) {\n const fallbackStop = stopCandidates[stopCandidates.length - 1];\n for (const stop of stopCandidates) {\n const candidate = ramp[stop];\n if (!candidate) {\n continue;\n }\n if (contrastRatio(candidate, referenceHex) >= minContrast) {\n return stop;\n }\n }\n return fallbackStop;\n}\n\nfunction pickReadableRampText(\n baseOklch: OklchColor,\n ramp: BrandRamp,\n stopCandidates: number[],\n surfaceHex: string,\n preferDark: boolean,\n) {\n for (const stop of stopCandidates) {\n const candidate = ramp[stop];\n if (candidate && contrastRatio(candidate, surfaceHex) >= BRAND_UI_TEXT_MIN_CONTRAST) {\n return candidate;\n }\n }\n\n return makeReadableTextOnSurface(baseOklch, surfaceHex, {\n chromaScale: 1,\n initialLightness: preferDark ? 0.45 : 0.72,\n minContrast: BRAND_UI_TEXT_MIN_CONTRAST,\n preferDark,\n });\n}\n\nfunction makeBrandRampFromSeed(baseOklch: OklchColor): BrandRamp {\n const lightnessOffset = baseOklch.l - BRAND_LIGHTNESS_CURVE[5];\n const minSeedChroma = baseOklch.c > 0.02 ? 0.02 : 0;\n\n return BRAND_STOPS.reduce<BrandRamp>((acc, stop, index) => {\n const lightness = clamp01(BRAND_LIGHTNESS_CURVE[index] + lightnessOffset);\n const chroma = Math.min(\n 0.4,\n Math.max(minSeedChroma, baseOklch.c * BRAND_CHROMA_MULTIPLIERS[index]),\n );\n acc[stop] = rgbToHex(oklchToSrgb(lightness, chroma, baseOklch.h));\n return acc;\n }, {});\n}\n\nfunction makeSemanticBrandTokens(baseOklch: OklchColor, ramp: BrandRamp): BrandVars {\n const lightSoftStops = [100, 200, 300, 400, 500, 600, 700];\n const lightSoftStop = pickRampStopForContrast(\n ramp,\n lightSoftStops,\n LIGHT_CANVAS_HEX,\n BRAND_MIN_SOFT_SURFACE_CONTRAST,\n );\n const lightSoftIndex = lightSoftStops.indexOf(lightSoftStop);\n const lightSoftHoveredStop =\n lightSoftStops[Math.min(lightSoftIndex + 1, lightSoftStops.length - 1)];\n const lightSoftPressedStop =\n lightSoftStops[Math.min(lightSoftIndex + 2, lightSoftStops.length - 1)];\n\n const darkSoftStops = [1000, 900, 800, 700, 600, 500];\n const darkSoftStop = pickRampStopForContrast(\n ramp,\n darkSoftStops,\n DARK_CANVAS_HEX,\n BRAND_MIN_SOFT_SURFACE_CONTRAST,\n );\n const darkSoftIndex = darkSoftStops.indexOf(darkSoftStop);\n const darkSoftHoveredStop =\n darkSoftStops[Math.min(darkSoftIndex + 1, darkSoftStops.length - 1)];\n const darkSoftPressedStop =\n darkSoftStops[Math.min(darkSoftIndex + 2, darkSoftStops.length - 1)];\n\n const lightSubtleStops = [200, 300, 400, 500, 600, 700, 800];\n const lightSubtleStop = pickRampStopForContrast(\n ramp,\n lightSubtleStops,\n LIGHT_CANVAS_HEX,\n BRAND_MIN_SUBTLE_SURFACE_CONTRAST,\n );\n const darkSubtleStops = [900, 800, 700, 600, 500, 400];\n const darkSubtleStop = pickRampStopForContrast(\n ramp,\n darkSubtleStops,\n DARK_CANVAS_HEX,\n BRAND_MIN_SUBTLE_SURFACE_CONTRAST,\n );\n\n const lightStrongStops = [600, 700, 800, 900, 1000];\n const lightStrongStop = pickRampStopForContrast(\n ramp,\n lightStrongStops,\n LIGHT_CANVAS_HEX,\n BRAND_MIN_SURFACE_CONTRAST,\n );\n const lightStrongIndex = lightStrongStops.indexOf(lightStrongStop);\n const lightStrongHoveredStop =\n lightStrongStops[Math.min(lightStrongIndex + 1, lightStrongStops.length - 1)];\n const lightStrongPressedStop =\n lightStrongStops[Math.min(lightStrongIndex + 2, lightStrongStops.length - 1)];\n\n const darkStrongStops = [400, 300, 200, 100];\n const darkStrongStop = pickRampStopForContrast(\n ramp,\n darkStrongStops,\n DARK_CANVAS_HEX,\n BRAND_MIN_SURFACE_CONTRAST,\n );\n const darkStrongIndex = darkStrongStops.indexOf(darkStrongStop);\n const darkStrongHoveredStop =\n darkStrongStops[Math.min(darkStrongIndex + 1, darkStrongStops.length - 1)];\n const darkStrongPressedStop =\n darkStrongStops[Math.min(darkStrongIndex + 2, darkStrongStops.length - 1)];\n\n const tokens: BrandVars = {\n \"--color-brand-surface-soft-light\": ramp[lightSoftStop],\n \"--color-brand-surface-soft-hovered-light\": ramp[lightSoftHoveredStop],\n \"--color-brand-surface-soft-pressed-light\": ramp[lightSoftPressedStop],\n \"--color-brand-surface-soft-dark\": ramp[darkSoftStop],\n \"--color-brand-surface-soft-hovered-dark\": ramp[darkSoftHoveredStop],\n \"--color-brand-surface-soft-pressed-dark\": ramp[darkSoftPressedStop],\n \"--color-brand-surface-subtle-light\": ramp[lightSubtleStop],\n \"--color-brand-surface-subtle-dark\": ramp[darkSubtleStop],\n \"--color-brand-surface-strong-light\": ramp[lightStrongStop],\n \"--color-brand-surface-strong-hovered-light\": ramp[lightStrongHoveredStop],\n \"--color-brand-surface-strong-pressed-light\": ramp[lightStrongPressedStop],\n \"--color-brand-surface-strong-dark\": ramp[darkStrongStop],\n \"--color-brand-surface-strong-hovered-dark\": ramp[darkStrongHoveredStop],\n \"--color-brand-surface-strong-pressed-dark\": ramp[darkStrongPressedStop],\n };\n\n const preferredSoftLightForeground = ramp[600];\n const softLightContrast = contrastRatio(\n tokens[\"--color-brand-surface-soft-light\"],\n preferredSoftLightForeground,\n );\n tokens[\"--color-brand-on-soft-light\"] =\n softLightContrast >= BRAND_MIN_CONTRAST\n ? preferredSoftLightForeground\n : pickForegroundColor(baseOklch, tokens[\"--color-brand-surface-soft-light\"], {\n minContrast: BRAND_MIN_CONTRAST,\n });\n tokens[\"--color-brand-on-soft-dark\"] = pickForegroundColor(\n baseOklch,\n tokens[\"--color-brand-surface-soft-dark\"],\n {\n minContrast: BRAND_MIN_CONTRAST,\n },\n );\n tokens[\"--color-brand-on-subtle-light\"] = pickForegroundColor(\n baseOklch,\n tokens[\"--color-brand-surface-subtle-light\"],\n {\n minContrast: BRAND_UI_TEXT_MIN_CONTRAST,\n preferredCandidate: \"light\",\n preferredMinContrast: BRAND_UI_TEXT_MIN_CONTRAST,\n preferredSwitchDelta: BRAND_LIGHT_TEXT_SWITCH_DELTA,\n },\n );\n tokens[\"--color-brand-on-subtle-dark\"] = pickForegroundColor(\n baseOklch,\n tokens[\"--color-brand-surface-subtle-dark\"],\n {\n minContrast: BRAND_MIN_CONTRAST,\n },\n );\n tokens[\"--color-brand-on-strong-light\"] = pickForegroundColor(\n baseOklch,\n tokens[\"--color-brand-surface-strong-light\"],\n {\n minContrast: BRAND_UI_TEXT_MIN_CONTRAST,\n preferredCandidate: \"light\",\n preferredMinContrast: BRAND_UI_TEXT_MIN_CONTRAST,\n preferredSwitchDelta: BRAND_LIGHT_TEXT_SWITCH_DELTA,\n },\n );\n tokens[\"--color-brand-on-strong-dark\"] = pickForegroundColor(\n baseOklch,\n tokens[\"--color-brand-surface-strong-dark\"],\n {\n minContrast: BRAND_MIN_CONTRAST,\n },\n );\n\n tokens[\"--color-brand-text-light\"] = pickReadableRampText(\n baseOklch,\n ramp,\n [600, 700, 800, 900, 1000],\n LIGHT_CANVAS_HEX,\n true,\n );\n tokens[\"--color-brand-text-dark\"] = pickReadableRampText(\n baseOklch,\n ramp,\n [400, 300, 200, 100],\n DARK_CANVAS_HEX,\n false,\n );\n tokens[\"--color-brand-icon-light\"] = tokens[\"--color-brand-text-light\"];\n tokens[\"--color-brand-icon-dark\"] = tokens[\"--color-brand-text-dark\"];\n tokens[\"--color-brand-border-light\"] = tokens[\"--color-brand-text-light\"];\n tokens[\"--color-brand-border-dark\"] = tokens[\"--color-brand-text-dark\"];\n\n return tokens;\n}\n\nfunction makeBrandTokens(baseHex: string | undefined): BrandVars | null {\n const norm = normalizeHex(baseHex);\n if (!norm) {\n return null;\n }\n\n const baseRgb = hexToRgb(norm);\n if (!baseRgb) {\n return null;\n }\n\n const baseOklch = rgbToOklch(baseRgb);\n const ramp = makeBrandRampFromSeed(baseOklch);\n // Keep the incoming seed exact for debugging and transparency.\n ramp[600] = norm;\n\n return {\n \"--color-brand-seed\": norm,\n \"--color-brand-100\": ramp[100],\n \"--color-brand-200\": ramp[200],\n \"--color-brand-300\": ramp[300],\n \"--color-brand-400\": ramp[400],\n \"--color-brand-500\": ramp[500],\n \"--color-brand-600\": ramp[600],\n \"--color-brand-700\": ramp[700],\n \"--color-brand-800\": ramp[800],\n \"--color-brand-900\": ramp[900],\n \"--color-brand-1000\": ramp[1000],\n ...makeSemanticBrandTokens(baseOklch, ramp),\n };\n}\n\nconst brandVars = computed(() => makeBrandTokens(props.accentColor) ?? {});\n\nconst accentStackEntry = {\n vars: {},\n enabled: Boolean(props.exposeGlobalAccent),\n};\nif (typeof document !== \"undefined\") {\n globalAccentStack.push(accentStackEntry);\n}\n\nwatch(\n () => props.exposeGlobalAccent,\n (enabled) => {\n accentStackEntry.enabled = Boolean(enabled);\n applyGlobalAccentFromStack();\n },\n { immediate: true },\n);\n\nwatch(\n brandVars,\n (vars) => {\n if (vars && Object.keys(vars).length > 0) {\n accentStackEntry.vars = { ...vars };\n } else {\n accentStackEntry.vars = {};\n }\n applyGlobalAccentFromStack();\n },\n { immediate: true },\n);\n</script>\n\n<style lang=\"less\">\n@import \"index.less\";\n</style>\n","<template>\n <DropdownMenu v-model:open=\"open\" text=\"Theme\" aria-label=\"Select theme mode\">\n <DropdownItemGroup title=\"Theme\" item-role=\"menuitemradio\">\n <DropdownItemRadio\n v-model:checked=\"internalMode\"\n value=\"auto\"\n :label=\"MODE_LABELS.auto\"\n :aria-label=\"`${MODE_LABELS.auto} mode`\"\n />\n <hr class=\"theme-switcher-divider\" />\n <DropdownItemRadio\n v-model:checked=\"internalMode\"\n value=\"light\"\n :label=\"MODE_LABELS.light\"\n :aria-label=\"`${MODE_LABELS.light} mode`\"\n />\n <DropdownItemRadio\n v-model:checked=\"internalMode\"\n value=\"dark\"\n :label=\"MODE_LABELS.dark\"\n :aria-label=\"`${MODE_LABELS.dark} mode`\"\n />\n </DropdownItemGroup>\n </DropdownMenu>\n</template>\n\n<script setup lang=\"ts\">\nimport { inject, ref, watch } from \"vue\";\nimport type { PropType } from \"vue\";\nimport { DropdownMenu, DropdownItemGroup, DropdownItemRadio } from \"@gospore/dropdown-menu\";\nimport {\n readStoredMode,\n writeStoredMode,\n VALID_THEME_MODES,\n applyStoredModeToDocument,\n type ThemeMode,\n} from \"./modeStorage\";\n\nconst MODE_OPTIONS: { value: ThemeMode; label: string }[] = [\n { value: \"auto\", label: \"System\" },\n { value: \"light\", label: \"Light\" },\n { value: \"dark\", label: \"Dark\" },\n];\nconst MODE_LABELS = MODE_OPTIONS.reduce<Record<ThemeMode, string>>(\n (map, option) => {\n map[option.value] = option.label;\n return map;\n },\n {} as Record<ThemeMode, string>,\n);\nconst VALID_MODES = VALID_THEME_MODES;\n\nconst props = defineProps({\n /** Selected theme mode. Supports v-model:mode and persists changes to local storage. */\n mode: {\n type: String as PropType<ThemeMode>,\n default: \"auto\",\n validator: (value: string) => [\"auto\", \"light\", \"dark\"].includes(value),\n },\n});\nconst emit = defineEmits([\"update:mode\"]);\n\nconst hasThemeProvider = inject(\"theme\", null) !== null;\nif (!hasThemeProvider) {\n applyStoredModeToDocument();\n}\n\nconst open = ref(false);\nconst initialMode = readStoredMode() ?? props.mode;\nconst internalMode = ref<ThemeMode>(VALID_MODES.includes(initialMode) ? initialMode : \"auto\");\nwatch(\n () => props.mode,\n (mode) => {\n if (VALID_MODES.includes(mode) && mode !== internalMode.value) {\n internalMode.value = mode;\n }\n },\n);\n\nwatch(\n internalMode,\n (mode) => {\n if (!VALID_MODES.includes(mode)) {\n internalMode.value = \"auto\";\n return;\n }\n open.value = false;\n emit(\"update:mode\", mode);\n writeStoredMode(mode);\n if (!hasThemeProvider) {\n applyStoredModeToDocument(mode);\n }\n },\n { immediate: true },\n);\n</script>\n\n<style scoped lang=\"less\">\n.theme-switcher-divider {\n margin: 4px 0;\n}\n</style>\n","<template>\n <DropdownMenu v-model:open=\"open\" text=\"Theme\" aria-label=\"Select theme mode\">\n <DropdownItemGroup title=\"Theme\" item-role=\"menuitemradio\">\n <DropdownItemRadio\n v-model:checked=\"internalMode\"\n value=\"auto\"\n :label=\"MODE_LABELS.auto\"\n :aria-label=\"`${MODE_LABELS.auto} mode`\"\n />\n <hr class=\"theme-switcher-divider\" />\n <DropdownItemRadio\n v-model:checked=\"internalMode\"\n value=\"light\"\n :label=\"MODE_LABELS.light\"\n :aria-label=\"`${MODE_LABELS.light} mode`\"\n />\n <DropdownItemRadio\n v-model:checked=\"internalMode\"\n value=\"dark\"\n :label=\"MODE_LABELS.dark\"\n :aria-label=\"`${MODE_LABELS.dark} mode`\"\n />\n </DropdownItemGroup>\n </DropdownMenu>\n</template>\n\n<script setup lang=\"ts\">\nimport { inject, ref, watch } from \"vue\";\nimport type { PropType } from \"vue\";\nimport { DropdownMenu, DropdownItemGroup, DropdownItemRadio } from \"@gospore/dropdown-menu\";\nimport {\n readStoredMode,\n writeStoredMode,\n VALID_THEME_MODES,\n applyStoredModeToDocument,\n type ThemeMode,\n} from \"./modeStorage\";\n\nconst MODE_OPTIONS: { value: ThemeMode; label: string }[] = [\n { value: \"auto\", label: \"System\" },\n { value: \"light\", label: \"Light\" },\n { value: \"dark\", label: \"Dark\" },\n];\nconst MODE_LABELS = MODE_OPTIONS.reduce<Record<ThemeMode, string>>(\n (map, option) => {\n map[option.value] = option.label;\n return map;\n },\n {} as Record<ThemeMode, string>,\n);\nconst VALID_MODES = VALID_THEME_MODES;\n\nconst props = defineProps({\n /** Selected theme mode. Supports v-model:mode and persists changes to local storage. */\n mode: {\n type: String as PropType<ThemeMode>,\n default: \"auto\",\n validator: (value: string) => [\"auto\", \"light\", \"dark\"].includes(value),\n },\n});\nconst emit = defineEmits<{\n \"update:mode\": [value: ThemeMode];\n}>();\n\nconst hasThemeProvider = inject(\"theme\", null) !== null;\nif (!hasThemeProvider) {\n applyStoredModeToDocument();\n}\n\nconst open = ref(false);\nconst initialMode = readStoredMode() ?? props.mode;\nconst internalMode = ref<ThemeMode>(VALID_MODES.includes(initialMode) ? initialMode : \"auto\");\nwatch(\n () => props.mode,\n (mode) => {\n if (VALID_MODES.includes(mode) && mode !== internalMode.value) {\n internalMode.value = mode;\n }\n },\n);\n\nwatch(\n internalMode,\n (mode) => {\n if (!VALID_MODES.includes(mode)) {\n internalMode.value = \"auto\";\n return;\n }\n open.value = false;\n emit(\"update:mode\", mode);\n writeStoredMode(mode);\n if (!hasThemeProvider) {\n applyStoredModeToDocument(mode);\n }\n },\n { immediate: true },\n);\n</script>\n\n<style scoped lang=\"less\">\n.theme-switcher-divider {\n margin: 4px 0;\n}\n</style>\n"],"mappings":"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"}