tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

DocsContainer.mjs (4520B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
      4 
      5 // eslint-disable-next-line no-unused-vars
      6 import React, { useEffect, useLayoutEffect, useState } from "react";
      7 // eslint-disable-next-line no-unused-vars
      8 import { DocsContainer as BaseContainer } from "@storybook/addon-docs";
      9 import { addons } from "@storybook/preview-api";
     10 import {
     11  GLOBALS_UPDATED,
     12  UPDATE_GLOBALS,
     13  SET_GLOBALS,
     14 } from "@storybook/core-events";
     15 import darkTheme from "./theme-dark.mjs";
     16 import lightTheme from "./theme-light.mjs";
     17 
     18 /* -------- helpers -------- */
     19 function getSelectedFromUrl() {
     20  const g = new URLSearchParams(location.search).get("globals");
     21  return g?.match(/(?:^|,)theme:([^,]+)/)?.[1] ?? "system"; // 'light' | 'dark' | 'system'
     22 }
     23 function getOsDark() {
     24  return window.matchMedia("(prefers-color-scheme: dark)").matches;
     25 }
     26 function mergeGlobalsParam(existing, key, value) {
     27  const parts = (existing || "").split(",").filter(Boolean);
     28  const map = new Map(parts.map(s => s.split(":")));
     29  map.set(key, value);
     30  return [...map.entries()].map(([k, v]) => `${k}:${v}`).join(",");
     31 }
     32 function syncGlobalsUrl(key, value) {
     33  // update this iframe URL
     34  try {
     35    const u = new URL(window.location.href);
     36    const prev = u.searchParams.get("globals") || "";
     37    const merged = mergeGlobalsParam(prev, key, value);
     38    if (merged !== prev) {
     39      u.searchParams.set("globals", merged);
     40      window.history.replaceState({}, "", u.toString());
     41    }
     42  } catch {}
     43  // update top/manager URL (same-origin)
     44  try {
     45    const topWin = window.parent || window.top;
     46    const u2 = new URL(topWin.location.href);
     47    const prev2 = u2.searchParams.get("globals") || "";
     48    const merged2 = mergeGlobalsParam(prev2, key, value);
     49    if (merged2 !== prev2) {
     50      u2.searchParams.set("globals", merged2);
     51      topWin.history.replaceState({}, "", u2.toString());
     52    }
     53  } catch {}
     54 }
     55 function applyDocsDomTheme(dark) {
     56  const mode = dark ? "dark" : "light";
     57  const html = document.documentElement;
     58  html.style.colorScheme = mode; // UA widgets
     59  html.classList.toggle("dark", dark); // your CSS hook
     60  const root = document.getElementById("storybook-docs");
     61  if (root) {
     62    root.classList.toggle("dark", dark);
     63  }
     64 }
     65 
     66 /* -------- container -------- */
     67 export default function DocsContainer(props) {
     68  // initial selection from URL; "system" follows OS via mql below
     69  const [selected, setSelected] = useState(getSelectedFromUrl()); // 'light' | 'dark' | 'system'
     70  const [osDark, setOsDark] = useState(getOsDark());
     71 
     72  // concrete resolution
     73  const isDark = selected === "dark" || (selected !== "light" && osDark);
     74 
     75  // apply before paint on initial load + any theme change
     76  useLayoutEffect(() => {
     77    applyDocsDomTheme(isDark);
     78  }, [isDark]);
     79 
     80  // follow OS flips when toolbar = "system"
     81  useEffect(() => {
     82    const mql = window.matchMedia("(prefers-color-scheme: dark)");
     83    const onChange = e => setOsDark(e.matches);
     84    mql.addEventListener?.("change", onChange) || mql.addListener?.(onChange);
     85    return () => {
     86      mql.removeEventListener?.("change", onChange) ||
     87        mql.removeListener?.(onChange);
     88    };
     89  }, []);
     90 
     91  // toolbar changes → sync URL, flip DOM immediately, update state
     92  useEffect(() => {
     93    const channel = addons.getChannel();
     94    const onUpdate = payload => {
     95      const next = payload?.globals?.theme; // 'light' | 'dark' | 'system'
     96      if (!next) {
     97        return;
     98      }
     99      syncGlobalsUrl("theme", next); // make refresh consistent
    100      applyDocsDomTheme(next === "dark" || (next === "system" && getOsDark()));
    101      setSelected(prev => (prev === next ? prev : next));
    102    };
    103    channel.on(SET_GLOBALS, onUpdate);
    104    channel.on(GLOBALS_UPDATED, onUpdate);
    105    channel.on(UPDATE_GLOBALS, onUpdate);
    106    return () => {
    107      channel.off(SET_GLOBALS, onUpdate);
    108      channel.off(GLOBALS_UPDATED, onUpdate);
    109      channel.off(UPDATE_GLOBALS, onUpdate);
    110    };
    111  }, []);
    112 
    113  // Storybook theme object (NOT strings)
    114  const themeObj = isDark ? darkTheme : lightTheme;
    115  const themeKey = isDark ? "dark" : "light"; // remounts docs subtree when theme flips
    116 
    117  return <BaseContainer key={themeKey} {...props} theme={themeObj} />;
    118 }
    119 
    120 /*
    121 * If your Storybook doesn't export DocsContainer from '@storybook/addon-docs',
    122 * replace the import with:
    123 *   import { DocsContainer as BaseContainer } from "@storybook/blocks";
    124 */