storybook-addon-data-theme-switcher
Version:
A Storybook addon to switch the data-theme attribute on the html element
138 lines (137 loc) • 5.26 kB
JavaScript
import { addons, types, useGlobals } from "storybook/manager-api";
import React, { useCallback, useMemo, useState } from "react";
import { ActionList, PopoverProvider, ToggleButton } from "storybook/internal/components";
import { styled } from "storybook/theming";
import * as Icon from "@storybook/icons";
//#region src/constants.ts
const ADDON_ID = "storybook/addon-data-theme-switcher";
const TOOL_ID = `${ADDON_ID}/tool`;
const TAB_ID = `${ADDON_ID}/tab`;
const DATA_THEME_KEY = "dataTheme";
//#endregion
//#region src/utils/index.ts
const defaultOptions = {
clearable: true,
list: []
};
function getConfig(parameters) {
const options = Array.isArray(parameters) ? { list: parameters } : parameters;
return {
...defaultOptions,
...options
};
}
function getSelectedThemeName(list, defaultTheme, currentSelectedValue) {
if (!list.length) return "none";
if (currentSelectedValue === "none") return currentSelectedValue;
if (currentSelectedValue && list.some((i) => i.name === currentSelectedValue)) return currentSelectedValue;
if (defaultTheme && list.some((i) => i.dataTheme === defaultTheme)) return list.find((i) => i.dataTheme === defaultTheme)?.name ?? "none";
if (defaultTheme) return defaultTheme;
if (list.some((i) => i.default)) return list.find((i) => i.default)?.name ?? "none";
return "none";
}
function getSelectedTheme(list, themeName) {
return list.find(({ name }) => name === themeName);
}
//#endregion
//#region src/components/ColorIcon.tsx
const ColorIcon = styled.span(({ background }) => ({
borderRadius: "50%",
display: "block",
height: 14,
width: 14,
flexShrink: 0,
background
}), ({ theme }) => ({ boxShadow: `${theme.appBorderColor} 0 0 0 1px inset` }));
//#endregion
//#region src/Tool.tsx
const MenuButton = styled(ActionList.Button)(({ theme }) => ({
width: "100%",
fontWeight: theme.typography.weight.regular,
color: theme.color.defaultText,
justifyContent: "flex-start"
}));
const MenuText = styled(ActionList.Text)({ flexGrow: 0 });
const ThemeMenu = ({ items, onHide }) => /* @__PURE__ */ React.createElement(ActionList, { style: { minWidth: 180 } }, items.map((item) => /* @__PURE__ */ React.createElement(ActionList.Item, {
key: item.id,
active: item.active
}, /* @__PURE__ */ React.createElement(MenuButton, {
ariaLabel: false,
onClick: () => {
item.onClick();
onHide();
}
}, item.left && /* @__PURE__ */ React.createElement(ActionList.Icon, null, item.left), /* @__PURE__ */ React.createElement(MenuText, null, item.title)))));
const DataThemeSelector = () => {
const [isOpen, setIsOpen] = useState(false);
const [{ dataTheme, dataThemes: themes }, updateThemeGlobals] = useGlobals();
const themeConfig = useMemo(() => ({
default: dataTheme,
...themes
}), [dataTheme, themes]);
const change = useCallback((newSelected) => {
const { list, onChange } = getConfig(themeConfig);
const selectedTheme = getSelectedTheme(list, newSelected);
updateThemeGlobals({ [DATA_THEME_KEY]: selectedTheme?.dataTheme ?? "none" });
if (typeof onChange === "function") onChange(selectedTheme);
}, [themeConfig, updateThemeGlobals]);
const { items, selectedTheme } = useMemo(() => {
const { clearable, list, default: defaultTheme } = getConfig(themeConfig);
const selectedThemeName = getSelectedThemeName(list, defaultTheme);
const selectorItems = [];
if (selectedThemeName !== "none" && clearable) selectorItems.push({
id: "none",
title: "Clear data-theme",
onClick: () => change("none"),
value: "none",
left: /* @__PURE__ */ React.createElement(Icon.UndoIcon, null),
active: false
});
if (list.length) for (const { dataTheme, color, name } of list) {
const id = name ?? dataTheme;
selectorItems.push({
id,
title: id,
onClick: () => change(id),
value: id,
left: /* @__PURE__ */ React.createElement(ColorIcon, { background: color ?? "transparent" }),
active: name === selectedThemeName
});
}
return {
items: selectorItems,
selectedTheme: getSelectedTheme(list, selectedThemeName)
};
}, [themeConfig, change]);
const iconKey = themeConfig.toolbar?.icon ?? "PaintBrushIcon";
const ThemeConfigIcon = useMemo(() => Icon[iconKey], [iconKey]);
const renderPopover = useCallback((props) => /* @__PURE__ */ React.createElement(ThemeMenu, {
items,
onHide: props.onHide
}), [items]);
return themeConfig.list && themeConfig.list.length > 0 ? /* @__PURE__ */ React.createElement(PopoverProvider, {
ariaLabel: "Theme selector",
placement: "top-start",
padding: 0,
onVisibleChange: setIsOpen,
popover: renderPopover
}, /* @__PURE__ */ React.createElement(ToggleButton, {
variant: "ghost",
pressed: selectedTheme !== void 0,
key: TOOL_ID,
ariaLabel: "Change data-theme attribute",
tooltip: themeConfig.toolbar?.title ?? "Change data-theme attribute",
disableAllTooltips: isOpen
}, /* @__PURE__ */ React.createElement(ThemeConfigIcon, null), selectedTheme ? ` ${selectedTheme.name}` : null)) : null;
};
//#endregion
//#region src/preset/manager.ts
addons.register(ADDON_ID, () => {
addons.add(TOOL_ID, {
title: "Data Theme Switcher",
type: types.TOOL,
match: ({ viewMode, tabId }) => !!(viewMode?.match(/^(story|docs)$/) || tabId === TAB_ID),
render: DataThemeSelector
});
});
//#endregion