UNPKG

storybook-addon-tag-badges

Version:

Display Storybook tags as badges in the sidebar and toolbar.

482 lines (475 loc) 13.1 kB
import React, { Fragment, useMemo } from 'react'; import { WithTooltip, TooltipMessage } from 'storybook/internal/components'; import { styled, useTheme } from 'storybook/theming'; import { useStorybookApi, addons, experimental_useStatusStore } from 'storybook/manager-api'; // src/utils/tag.ts function getTagParts(tag) { const [prefix, ...rest] = tag.split(":"); return { prefix, suffix: rest.join(":") || null }; } function getTagPrefix(tag) { return getTagParts(tag).prefix; } function getTagSuffix(tag) { return getTagParts(tag).suffix; } function normalisePattern(pattern) { if (pattern === void 0) { return /.*/; } if (typeof pattern === "string") { let patternWithBoundaries = pattern; if (!patternWithBoundaries.startsWith("^")) { patternWithBoundaries = `^${patternWithBoundaries}`; } if (!patternWithBoundaries.endsWith("$")) { patternWithBoundaries += "$"; } return new RegExp(patternWithBoundaries); } return pattern; } function matchTag(tag, patterns) { const normalisedPatterns = [patterns].flat(); for (const pattern of normalisedPatterns) { if (pattern instanceof RegExp) { if (tag.match(pattern)) { return true; } } else if (typeof pattern === "string") { if (tag === pattern) { return true; } } else { const { prefix, suffix } = getTagParts(tag); const prefixPattern = normalisePattern(pattern.prefix); const suffixPattern = normalisePattern(pattern.suffix); const matchesPrefix = prefix.match(prefixPattern); const matchesSuffix = suffix && suffix.match(suffixPattern); if (matchesPrefix && matchesSuffix) { return true; } } } return false; } function matchTags(tags, config) { return tags.filter((tag) => matchTag(tag, config)); } // src/defaultConfig.ts var newBadge = { tags: "new", badge: { text: "New", style: "green" } }; var preReleaseBadge = { tags: ["alpha", "beta", "rc", "experimental"], badge: ({ tag }) => { const upperFirst = (str) => str[0].toUpperCase() + str.slice(1); return { text: tag === "rc" ? "Release candidate" : upperFirst(tag), style: "purple" }; } }; var deprecatedBadge = { tags: "deprecated", badge: { text: "Deprecated", style: "yellow" } }; var outdatedBadge = { tags: "outdated", badge: { text: "Outdated", style: "orange" } }; var dangerBadge = { tags: "danger", badge: { text: "Danger", style: "red" } }; var codeOnlyBadge = { tags: ["code-only"], badge: { text: "Code Only", style: "grey" } }; var versionBadge = { tags: [ { prefix: "v" }, { prefix: "version" } ], badge: ({ getTagSuffix: getTagSuffix2, tag }) => { const version = getTagSuffix2(tag); const isExperimental = version?.startsWith("0"); return { text: `${version}`, style: isExperimental ? "turquoise" : "blue" }; } }; var defaultConfig = [ newBadge, preReleaseBadge, deprecatedBadge, outdatedBadge, dangerBadge, codeOnlyBadge, versionBadge ]; // src/utils/display.ts var DISPLAY_DEFAULTS = { mdx: ["story", "component"], sidebar: [ { type: "story", skipInherited: true }, { type: "docs", skipInherited: true }, { type: "component", skipInherited: false }, { type: "group", skipInherited: false } ], toolbar: ["docs", "story"] }; function toArray(value) { return Array.isArray(value) ? value : [value]; } function normaliseDisplay(display) { return { mdx: toArray(display?.mdx ?? DISPLAY_DEFAULTS.mdx), sidebar: toArray(display?.sidebar ?? DISPLAY_DEFAULTS.sidebar), toolbar: toArray(display?.toolbar ?? DISPLAY_DEFAULTS.toolbar) }; } function shouldDisplay({ config, context, type }) { if (type === "root") { return "never" /* NEVER */; } for (const condition of normaliseDisplay(config.display)[context]) { if (condition === true) { return "always" /* ALWAYS */; } if (condition === false) { return "never" /* NEVER */; } if (condition === type) { return "always" /* ALWAYS */; } if (context === "sidebar" && typeof condition === "object") { if (condition.type === type) { return condition.skipInherited ? "skip-inherited" /* SKIP_INHERITED */ : "always" /* ALWAYS */; } } } return "never" /* NEVER */; } // src/constants.ts var ADDON_ID = "storybook/addon-tag-badges"; var TOOL_ID = `${ADDON_ID}/tool`; var KEY = `tagBadges`; var EVENTS = { REQUEST_MDX_BADGE_RENDER: `${ADDON_ID}/requestMdxBadgeRender`, MDX_BADGE_RENDER_RESPONSE: `${ADDON_ID}/mdxBadgeRenderResponse` }; var WithTooltipPatched = styled(WithTooltip)` line-height: 1px; `; var BadgeUI = styled.div(({ as, context, extraStyle, hasLongText, theme }) => ({ display: "inline-block", fontSize: 11, lineHeight: ".75rem", alignSelf: "center", padding: context === "sidebar" ? "3px 8px" : "4px 12px", border: "none", cursor: as === "button" ? "help" : context === "sidebar" ? "cursor" : "initial", borderRadius: "3em", fontWeight: theme.typography.weight.bold, boxShadow: theme.base === "light" ? `inset 0 0 0 1px ${extraStyle.borderColor ?? `color-mix(in oklab, ${extraStyle.color ?? theme.color.dark} 10%, transparent 90%)`}` : `inset 0 0 0 1px ${extraStyle.borderColor ?? "none"}`, backgroundColor: theme.color.mediumlight, color: theme.color.dark, wordBreak: "normal", width: hasLongText ? "min-content" : "fit-content", flexShrink: 0, textWrapStyle: "pretty", textAlign: "center", ...extraStyle, borderColor: void 0 })); var TooltipUI = styled.div(({ theme }) => ({ padding: "8px 12px", boxSizing: "border-box", color: theme.color.defaultText, lineHeight: "1.125rem" })); var Badge = ({ context, style, text, tooltip }) => { const theme = useTheme(); let extraStyle; if (style === "green") { extraStyle = { backgroundColor: "hsl(130, 100%, 74%)", borderColor: "hsl(130, 100%, 34%)", color: "hsl(130, 100%, 6%)" }; } else if (style === "purple") { extraStyle = { backgroundColor: "hsl(257, 100%, 84%)", borderColor: "hsl(257, 100%, 64%)", color: "hsl(257, 100%, 12%)" }; } else if (style === "blue") { extraStyle = { backgroundColor: "hsl(194, 100%, 74%)", borderColor: "hsl(194, 100%, 34%)", color: "hsl(194, 100%, 12%)" }; } else if (style === "grey") { extraStyle = { backgroundColor: "hsl(0, 0%, 84%)", borderColor: "hsl(0, 0%, 34%)", color: "hsl(0, 0%, 12%)" }; } else if (style === "orange") { extraStyle = { backgroundColor: "hsl(16, 100%, 74%)", borderColor: "hsl(16, 100%, 34%)", color: "hsl(16, 100%, 12%)" }; } else if (style === "red") { extraStyle = { backgroundColor: "hsl(0, 100%, 44%)", borderColor: "hsl(0, 100%, 64%)", color: "hsl(0, 100%, 94%)" }; } else if (style === "yellow") { extraStyle = { backgroundColor: "hsl(36, 100%, 74%)", borderColor: "hsl(36, 100%, 34%)", color: "hsl(36, 100%, 12%)" }; } else if (style === "pink") { extraStyle = { backgroundColor: "hsl(330, 100%, 74%)", borderColor: "hsl(330, 100%, 34%)", color: "hsl(330, 100%, 12%)" }; } else if (style === "turquoise") { extraStyle = { backgroundColor: "hsl(157, 100%, 74%)", borderColor: "hsl(157, 100%, 34%)", color: "hsl(157, 100%, 12%)" }; } else if (typeof style === "object") { extraStyle = { ...style }; } if (typeof text !== "string") { throw new Error( "Badge: the text prop must be defined and must be a string." ); } const hasLongText = text.length > 15; return /* @__PURE__ */ React.createElement(Fragment, null, !tooltip || context == "sidebar" ? /* @__PURE__ */ React.createElement( BadgeUI, { context, extraStyle: extraStyle ?? {}, hasLongText, theme }, text ) : /* @__PURE__ */ React.createElement( WithTooltipPatched, { closeOnOutsideClick: true, placement: "bottom", tooltip: typeof tooltip === "string" ? /* @__PURE__ */ React.createElement(TooltipUI, null, tooltip) : /* @__PURE__ */ React.createElement(TooltipMessage, { ...tooltip }) }, /* @__PURE__ */ React.createElement( BadgeUI, { as: "button", context, extraStyle: extraStyle ?? {}, hasLongText, theme }, text ) )); }; function getBadgeProps(config, entry, tag, context) { const props = typeof config === "function" ? config({ context, entry, getTagParts, getTagPrefix, getTagSuffix, tag }) : config; return props; } var WithBadge = ({ context, config, entry, tag, ...restProps }) => { const cfg = getBadgeProps(config, entry, tag, context); return /* @__PURE__ */ React.createElement(Badge, { ...cfg, context, ...restProps }); }; function _useBadgesToDisplay({ api, context, parameters, parent, tags, type }) { if (!tags || !type) { return []; } let parentTags; let resolvedParent; if (api && parent) { resolvedParent = api.resolveStory(parent); if (resolvedParent && resolvedParent.type !== "root") { parentTags = resolvedParent.tags; } } return (parameters || []).map((config) => ({ ...config, displayOutcome: shouldDisplay({ context, config, type }) })).filter(({ displayOutcome }) => displayOutcome !== "never" /* NEVER */).flatMap( (config) => matchTags(tags, config.tags).map((tag) => ({ badge: config.badge, displayOutcome: config.displayOutcome, tag })) ).reduce((acc, current) => { if (current.displayOutcome === "skip-inherited" /* SKIP_INHERITED */ && resolvedParent && resolvedParent.type !== "root" && parentTags?.includes(current.tag)) { const displayParent = _useBadgesToDisplay({ api, context, parameters, parent: resolvedParent.parent, tags: parentTags, type: resolvedParent.type }); if (displayParent.find(({ tag }) => tag === current.tag)) { return acc; } } if (acc.every(({ tag }) => tag !== current.tag)) { acc.push(current); } return acc; }, []); } function useBadgesToDisplay({ context, parameters, parent, tags, type }) { const api = useStorybookApi(); return useMemo( () => _useBadgesToDisplay({ api, context, parameters, parent, tags, type }), [context, parameters, parent, tags, type] ); } // src/components/Sidebar.tsx var Container = styled.div( ({ hasParentPadding, hasStatusWithUI: hasStatusWithUI2 }) => ` display: flex; flex: 1; align-items: flex-start; flex-wrap: wrap; text-wrap-style: balance; gap: 4px; margin-right: ${hasStatusWithUI2 ? "6px" : hasParentPadding ? "28px" : "34px"}; ` ); var FirstLineAlignedLabel = styled.div` display: flex; align-items: center; min-height: 19px; `; var Spacer = styled.div` flex: 1; `; var Sidebar = ({ children, item, hasStatusWithUI: hasStatusWithUI2 }) => { const { [KEY]: parameters } = addons.getConfig(); if (item.type !== "component" && item.type !== "group" && item.type !== "docs" && item.type !== "story") { return children; } const badgesToDisplay = useBadgesToDisplay({ context: "sidebar", parameters, parent: item.parent, tags: item.tags, type: item.type }); return /* @__PURE__ */ React.createElement( Container, { hasParentPadding: item.type === "component" || item.type === "group", hasStatusWithUI: hasStatusWithUI2 ?? false }, /* @__PURE__ */ React.createElement(FirstLineAlignedLabel, null, children), /* @__PURE__ */ React.createElement(Spacer, null), badgesToDisplay.length ? /* @__PURE__ */ React.createElement( WithBadge, { config: badgesToDisplay[0].badge, context: "sidebar", entry: item, tag: badgesToDisplay[0].tag } ) : "" ); }; function hasStatusWithUI(itemStatuses) { if (!itemStatuses) { return false; } if (itemStatuses["storybook/component-test"]) { return true; } return false; } function RenderLabelContent({ item }) { const itemStatuses = experimental_useStatusStore((all) => all[item.id]); return /* @__PURE__ */ React.createElement(Sidebar, { item, hasStatusWithUI: hasStatusWithUI(itemStatuses) }, item.name); } function renderLabel(item) { if (item.type !== "story" && item.type !== "group" && item.type !== "docs" && item.type !== "component") { return; } return /* @__PURE__ */ React.createElement(RenderLabelContent, { item }); } export { ADDON_ID, Badge, DISPLAY_DEFAULTS, EVENTS, KEY, Sidebar, TOOL_ID, WithBadge, codeOnlyBadge, dangerBadge, defaultConfig, deprecatedBadge, getBadgeProps, getTagParts, getTagPrefix, getTagSuffix, matchTag, matchTags, newBadge, outdatedBadge, preReleaseBadge, renderLabel, useBadgesToDisplay, versionBadge };