storybook-addon-tag-badges
Version:
Display Storybook tags as badges in the sidebar and toolbar.
482 lines (475 loc) • 13.1 kB
JavaScript
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 };