GDGT-2454 Badge (#74714)

This commit is contained in:
Kamil Mielnik
2026-07-09 21:47:43 +07:00
committed by GitHub
parent 6912a50e85
commit a9e1b0edf7
49 changed files with 466 additions and 190 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -198,12 +198,7 @@ function FeedbackCard({
/>
<Text fw={700}>{feedback.positive ? t`Positive` : t`Negative`}</Text>
{!feedback.positive && feedback.issue_type && (
<Badge
variant="light"
bg="background_surface-error"
c="feedback-negative"
ml="xs"
>
<Badge color="negative" ml="xs" size="sm">
{getIssueTypeLabel(feedback.issue_type)}
</Badge>
)}

View File

@@ -127,7 +127,7 @@ export function ConversationsTable({
<td>{convo.user ? getUserName(convo.user) : t`Unknown`}</td>
<td>
{convo.profile_id && (
<Badge size="sm" variant="light">
<Badge color="brand" variant="filled">
{renderMetabotProfileLabel(convo.profile_id)}
</Badge>
)}

View File

@@ -66,7 +66,9 @@ export function initializePlugin() {
component={ForwardRefLink}
to={`${Urls.collection(collection)}/cleanup`}
rightSection={
hasStaleItems ? <Badge>{t`Recommended`}</Badge> : null
hasStaleItems ? (
<Badge color="brand">{t`Recommended`}</Badge>
) : null
}
>
{t`Clear out unused items`}

View File

@@ -21,12 +21,15 @@ export const DatabaseReplicationStatusInfo = memo(
t`Replicating to Metabase Cloud Storage`
: // eslint-disable-next-line metabase/no-literal-metabase-strings -- This string only shows for admins.
t`Not replicating to Metabase Cloud Storage`;
const color = hasConnection ? "success" : "text-disabled";
return (
<Flex align="center" gap="sm" {...flexProps}>
<Tooltip label={message}>
<Badge size="12" circle bg={color} style={{ flexShrink: 0 }} />
<Badge
color={hasConnection ? "positive" : "neutral"}
flex="0 0 auto"
indicator
/>
</Tooltip>
<Text lh="1.25rem">{message}</Text>
</Flex>

View File

@@ -71,7 +71,7 @@ export function BrokenDependentsSection({
<Stack role="region" aria-label={getDependentErrorNodesLabel()}>
<Group justify="space-between" wrap="nowrap">
<Group gap="sm" wrap="nowrap">
<Badge variant="filled" bg="feedback-negative">
<Badge color="negative" size="sm">
{count}
</Badge>
<Title order={5}>

View File

@@ -72,7 +72,7 @@ function ErrorList({ title, items, "aria-label": ariaLabel }: ErrorListProps) {
return (
<Stack role="region" aria-label={ariaLabel}>
<Group gap="sm" wrap="nowrap">
<Badge variant="filled" bg="feedback-negative">
<Badge color="negative" size="sm">
{items.length}
</Badge>
<Title order={5}>{title}</Title>

View File

@@ -34,7 +34,7 @@ export function FieldsSection({ node }: FieldsSectionProps) {
return (
<Stack role="region" aria-label={t`Fields`}>
<Group gap="sm" wrap="nowrap">
<Badge variant="filled" bg="core-brand">
<Badge color="brand" size="sm">
{fields.length}
</Badge>
<Title order={5}>{getNodeFieldsLabel(fields.length)}</Title>

View File

@@ -85,14 +85,9 @@ export const GitSyncOptionsDropdown = ({
{isSwitchBranchDisabled ? (
<Badge
color="text-primary"
bg="background_page-secondary"
size="md"
fz="12px"
py="md"
my="xs"
style={{ textTransform: "none" }}
bdrs="sm"
size="sm"
variant="light"
>{t`Branch set by an environment variable`}</Badge>
) : (
<Combobox.Option

View File

@@ -54,7 +54,7 @@ export function DependentsSection({ cardId }: DependentsSectionProps) {
<Stack role="region" aria-label={label}>
<Group justify="space-between" wrap="nowrap">
<Group gap="sm" wrap="nowrap">
<Badge variant="filled" bg="core-brand">
<Badge color="brand" size="sm">
{allDependents.length}
</Badge>
<Title order={5}>{label}</Title>

View File

@@ -1,24 +0,0 @@
.severityCritical {
background-color: var(--mb-color-feedback-negative);
color: var(--mb-color-core-white);
}
.severityHigh {
background-color: var(--mb-color-feedback-warning);
color: var(--mb-color-core-white);
}
.severityMedium {
background-color: var(--mb-color-feedback-warning);
color: var(--mb-color-text-primary);
}
.severityLow {
background-color: var(--mb-color-feedback-positive);
color: var(--mb-color-core-white);
}
.severityNeutral {
background-color: var(--mb-color-background_surface-disabled);
color: var(--mb-color-text-disabled);
}

View File

@@ -1,8 +1,10 @@
import { match } from "ts-pattern";
import { t } from "ttag";
import {
Anchor,
Badge,
type BadgeColor,
Box,
Button,
Card,
@@ -22,15 +24,6 @@ import type {
import { isAcknowledged } from "../../utils";
import S from "./AdvisoryCard.module.css";
const SEVERITY_CLASS: Record<AdvisorySeverity, string> = {
critical: S.severityCritical,
high: S.severityHigh,
medium: S.severityMedium,
low: S.severityLow,
};
interface AdvisoryCardProps {
advisory: Advisory;
isAffecting: boolean;
@@ -49,13 +42,18 @@ export function AdvisoryCard({
<Stack gap="md">
<Group gap="sm" justify="space-between" align="flex-start">
<Badge
className={
isAffecting
? SEVERITY_CLASS[advisory.severity]
: S.severityNeutral
color={getSeverityColor({
isAffecting,
severity: advisory.severity,
})}
tt={
isAffecting && advisory.severity === "critical"
? "uppercase"
: undefined
}
size="sm"
>
{advisory.severity}
{getSeverityLabel(advisory.severity)}
</Badge>
{!isAffecting && (
<Group gap="xs" align="center">
@@ -130,3 +128,31 @@ export function AdvisoryCard({
</Card>
);
}
function getSeverityColor({
isAffecting,
severity,
}: {
isAffecting: boolean;
severity: AdvisorySeverity;
}): BadgeColor {
if (!isAffecting) {
return "neutral";
}
return match(severity)
.with("critical", () => "negative" as const)
.with("high", () => "negative" as const)
.with("medium", () => "warning" as const)
.with("low", () => "positive" as const)
.exhaustive();
}
function getSeverityLabel(severity: AdvisorySeverity): string {
return match(severity)
.with("critical", () => t`Critical`)
.with("high", () => t`High`)
.with("medium", () => t`Medium`)
.with("low", () => t`Low`)
.exhaustive();
}

View File

@@ -16,8 +16,3 @@
min-height: 50vh;
gap: 24px;
}
.countBadge {
background-color: var(--mb-color-text-primary);
color: var(--mb-color-core-white);
}

View File

@@ -70,9 +70,7 @@ export function AdvisoryList({
{affecting.length > 0 && (
<Box>
<Group gap="sm" mb="md" align="center">
<Badge size="sm" className={S.countBadge}>
{affecting.length}
</Badge>
<Badge size="sm">{affecting.length}</Badge>
<Title order={4}>{t`Issues impacting your instance`}</Title>
</Group>
<AdvisorySection

View File

@@ -74,8 +74,9 @@ export const AccessGrantList = (props: AccessGrantListProps) => {
) : (
<Group gap="sm">
<Badge
m={0}
color="brand"
variant="outline"
size="sm"
title={effectiveEndDate.format("lll")}
>
{c("{0} is the time until expiration")

View File

@@ -20,9 +20,9 @@ const _AddBadgeListItem = (
}}
bg="background_page-secondary"
tt="capitalize"
size="lg"
size="sm"
variant="transparent"
c="text-brand"
color="brand"
pr="sm"
pl="xs"
leftSection={

View File

@@ -18,7 +18,7 @@ const _BadgeListItem = (
) => (
<Badge
ref={ref}
size="lg"
size="sm"
tt="capitalize"
variant="light"
classNames={{
@@ -27,6 +27,7 @@ const _BadgeListItem = (
}}
pr={0}
pl="sm"
color="brand"
rightSection={
<ActionIcon
radius="xl"

View File

@@ -39,13 +39,8 @@ export function AIProviderSettingsSection({ id }: { id?: string }) {
<Group gap="xs" wrap="nowrap">
{connectedProvider && (
<Badge
circle
size="12"
bg={
hasCredentialsError
? "feedback-negative"
: "feedback-positive"
}
color={hasCredentialsError ? "negative" : "positive"}
indicator
mr="sm"
/>
)}

View File

@@ -18,6 +18,7 @@ import CS from "metabase/css/core/index.css";
import type { WithRouterProps } from "metabase/router";
import {
Badge,
type BadgeColor,
Box,
Card,
Ellipsified,
@@ -27,7 +28,6 @@ import {
type TreeTableColumnDef,
useTreeTableInstance,
} from "metabase/ui";
import type { MetabaseColorKey } from "metabase/ui/colors/types";
import type {
OAuthAuthorization,
OAuthClientEventType,
@@ -45,10 +45,10 @@ const ALL_EVENT_TYPES = "all";
type EventTypeFilter = OAuthClientEventType | typeof ALL_EVENT_TYPES;
const EVENT_COLORS: Record<OAuthClientEventType, MetabaseColorKey> = {
registered: "text-secondary",
approved: "success",
denied: "error",
const EVENT_COLORS: Record<OAuthClientEventType, BadgeColor> = {
registered: "neutral",
approved: "positive",
denied: "negative",
};
type UrlState = {

View File

@@ -3,7 +3,7 @@ import { match } from "ts-pattern";
import { t } from "ttag";
import { useGetDatabaseHealthQuery } from "metabase/api";
import { Badge, Flex, type FlexProps, Text, Tooltip } from "metabase/ui";
import { Badge, Flex, type FlexProps, Text, Tooltip, rem } from "metabase/ui";
import type { DatabaseConnectionType, DatabaseId } from "metabase-types/api";
interface DatabaseConnectionHealthInfoProps extends FlexProps {
@@ -26,7 +26,7 @@ export const DatabaseConnectionHealthInfo = ({
return match(healthQuery)
.with(
{ currentData: { status: "ok" } },
() => ({ message: t`Connected`, color: "feedback-positive" }) as const,
() => ({ message: t`Connected`, color: "positive" }) as const,
)
.with(
{ isUninitialized: true },
@@ -35,7 +35,7 @@ export const DatabaseConnectionHealthInfo = ({
() =>
({
message: t`Loading...`,
color: "tooltip-text",
color: "neutral",
}) as const,
)
.with(
@@ -43,7 +43,7 @@ export const DatabaseConnectionHealthInfo = ({
(q) =>
({
message: q.currentData.message,
color: "feedback-negative",
color: "negative",
}) as const,
)
.with(
@@ -51,22 +51,19 @@ export const DatabaseConnectionHealthInfo = ({
() =>
({
message: t`Failed to retrieve database health status.`,
color: "tooltip-text",
color: "neutral",
}) as const,
)
.exhaustive();
}, [healthQuery]);
return (
<Flex
align="center"
gap="sm"
{...props}
data-testid="database-connection-health-info"
>
<Tooltip label={health.message}>
<Badge size="12" circle bg={health.color} style={{ flexShrink: 0 }} />
</Tooltip>
<Flex gap="sm" {...props} data-testid="database-connection-health-info">
<Flex py={rem(6)}>
<Tooltip label={health.message}>
<Badge flex="0 0 auto" indicator color={health.color} />
</Tooltip>
</Flex>
{displayText === "inline" && <Text lh="1.4">{health.message}</Text>}
</Flex>
);

View File

@@ -52,10 +52,9 @@ const SlackConnectionStatus = ({
<Flex justify="space-between" align="center">
<Flex align="center" gap="sm">
<Badge
circle
size="12"
bg={isValid ? "feedback-positive" : "feedback-negative"}
style={{ flexShrink: 0 }}
color={isValid ? "positive" : "negative"}
flex="0 0 auto"
indicator
/>
<Text>
{isValid ? t`Slack app is working` : t`Slack app is not working.`}

View File

@@ -116,9 +116,8 @@ export function TablePicker({
{filtersCount > 0 && (
<Badge
bg="core-brand"
circle
size="8"
color="brand"
indicator
pos="absolute"
top={-6}
right={-8}

View File

@@ -212,8 +212,8 @@ const CodeEditDataPart = ({
<Icon name="document" c="text-secondary" />
<Text fw="bold">{formatPartType(type)}</Text>
<Text c="text-secondary">{t`Buffer ID: ${value.buffer_id}`}</Text>
<Badge variant="light" size="sm">
{value.mode}
<Badge color="brand" size="sm" variant="light">
{getModeLabel(value.mode)}
</Badge>
</Flex>
<ActionIcon
@@ -237,3 +237,9 @@ const CodeEditDataPart = ({
</Box>
);
};
function getModeLabel(mode: MetabotCodeEdit["mode"]): string {
return match(mode)
.with("rewrite", () => t`Rewrite`)
.exhaustive();
}

View File

@@ -57,9 +57,9 @@ const ToolCallDetailsModal = ({
onClose={onClose}
size="lg"
title={
<Flex align="center">
<Flex align="center" gap="sm">
{t`Tool Call: ${message.name}`}
<Badge ml="sm" color="text-primary">
<Badge color="brand" size="sm" variant="light">
{message.id}
</Badge>
</Flex>
@@ -84,14 +84,10 @@ const ToolCallDetailsModal = ({
{message.result && (
<Stack gap="xs">
<Flex gap="xs">
<Flex align="center">
<Flex align="center" gap="sm">
<Text fw="bold">{t`Response`}</Text>
{message.is_error && (
<Badge
ml="sm"
bg="feedback-negative"
c="text-primary-inverse"
>
<Badge color="negative" size="sm">
{t`Errored`}
</Badge>
)}

View File

@@ -1,6 +1,6 @@
.badge {
height: auto;
min-height: 2rem;
min-height: var(--badge-height);
}
/* override Badge's single-line ellipsis truncation */

View File

@@ -20,16 +20,9 @@ export const LabeledValue = ({ children, label }: Props) => {
<Badge
aria-labelledby={id}
bg="accent-gray-light"
c="text-primary"
classNames={{ root: S.badge, label: S.badgeLabel }}
fw="normal"
p="sm"
radius="md"
size="lg"
tt="none"
variant="default"
lh="sm"
size="sm"
variant="light"
>
{children}
</Badge>

View File

@@ -1,6 +1,5 @@
.metricAccordionItem {
overflow: hidden;
border-radius: 0.5rem;
}
.metricAccordionControl {

View File

@@ -42,9 +42,11 @@ export function MetricAccordionItem({
</Text>
{showOccurrenceCount && (
<Badge
className={S.occurrenceCountBadge}
circle
c="text-brand-hover"
className={S.occurrenceCountBadge}
color="brand"
size="sm"
variant="outline"
>
{group.occurrenceCount}
</Badge>

View File

@@ -24,7 +24,7 @@ export function MetricDimensionSelect({
{row.metricName}
</Text>
{showOccurrenceCount && (
<Badge circle c="text-brand-hover">
<Badge circle color="brand" variant="light">
{row.occurrenceCount}
</Badge>
)}

View File

@@ -222,7 +222,7 @@ function MetricGroupList({
<Flex align="center" gap="xs" fw={700}>
<span>{group.metricName}</span>
{(group.metricCount ?? 0) > 1 && (
<Badge circle c="text-brand-hover">
<Badge circle color="brand" variant="light">
{group.metricCount}
</Badge>
)}

View File

@@ -145,9 +145,10 @@ export function MetricExpressionPill({
if (typeof segment === "number") {
return (
<Badge
key={i}
circle
c="text-brand-hover"
color="brand"
key={i}
variant="light"
style={{ marginInlineStart: "0.2em" }}
>
{segment}

View File

@@ -79,6 +79,7 @@ export function MetricsFilterPillPopover({
// override background from Badge.config.tsx
styles={{ root: { background: "var(--badge-bg)" } }}
ml="xs"
variant="light"
>
{metricCount}
</Badge>

View File

@@ -95,6 +95,7 @@ export function MetricsSegmentFilterPillPopover({
// override background from Badge.config.tsx
styles={{ root: { background: "var(--badge-bg)" } }}
ml="xs"
variant="light"
>
{metricCount}
</Badge>

View File

@@ -8,7 +8,11 @@ type TaskRunStatusBadgeProps = {
};
export const TaskRunStatusBadge = ({ taskRun }: TaskRunStatusBadgeProps) => (
<Badge color={getTaskRunStatusColor(taskRun.status)}>
<Badge
color={getTaskRunStatusColor(taskRun.status)}
size="sm"
variant="light"
>
{formatTaskRunStatus(taskRun.status)}
</Badge>
);

View File

@@ -8,7 +8,7 @@ type TaskStatusProps = {
};
export const TaskStatusBadge = ({ task }: TaskStatusProps) => (
<Badge color={getTaskStatusColor(task.status)}>
<Badge color={getTaskStatusColor(task.status)} size="sm" variant="light">
{formatTaskStatus(task.status)}
</Badge>
);

View File

@@ -63,20 +63,7 @@ export const NotificationRunSummaryLog = ({
</Text>
</Tooltip>
<Tooltip label={run.error} disabled={!hasError}>
<Badge
color={isFailing ? "feedback-negative" : undefined}
variant={isFailing ? "light" : "outline"}
radius="lg"
tt="none"
fw="normal"
c={isFailing ? undefined : "text-secondary"}
bd={
isFailing
? undefined
: "1px solid var(--mb-color-border-neutral)"
}
style={hasError ? { cursor: "pointer" } : undefined}
>
<Badge color={isFailing ? "negative" : "positive"} variant="light">
{isFailing ? t`Failed` : t`Successful`}
</Badge>
</Tooltip>

View File

@@ -81,20 +81,7 @@ export const NotificationsTable = ({
accessorFn: (notification) => notification.id,
cell: ({ row }) => (
<Flex justify="center">
<Badge
variant="default"
bg="background_page-secondary"
c="text-primary"
tt="none"
bd="1px solid var(--mb-color-border-neutral)"
h="auto"
px="sm"
py="xs"
fz="md"
lh="xs"
fw={400}
miw={29}
>
<Badge variant="outline" size="sm" miw={29}>
{row.original.id}
</Badge>
</Flex>

View File

@@ -1,6 +1,7 @@
import { match } from "ts-pattern";
import { t } from "ttag";
import type { BadgeColor } from "metabase/ui";
import * as Urls from "metabase/urls";
import type {
Task,
@@ -55,18 +56,18 @@ export const getEntityUrl = (
.with("dashboard", () => Urls.dashboard({ id: entityId }))
.exhaustive();
export const getTaskRunStatusColor = (status: TaskRunStatus) =>
export const getTaskRunStatusColor = (status: TaskRunStatus): BadgeColor =>
match(status)
.with("success", () => "success" as const)
.with("failed", () => "error" as const)
.with("success", () => "positive" as const)
.with("failed", () => "negative" as const)
.with("abandoned", () => "warning" as const)
.otherwise(() => "text-primary" as const);
.otherwise(() => "neutral" as const);
export const getTaskStatusColor = (status: TaskStatus) =>
export const getTaskStatusColor = (status: TaskStatus): BadgeColor =>
match(status)
.with("success", () => "success" as const)
.with("failed", () => "error" as const)
.otherwise(() => "text-primary" as const);
.with("success", () => "positive" as const)
.with("failed", () => "negative" as const)
.otherwise(() => "neutral" as const);
export const getFilename = (task: Task | undefined) =>
task ? `task-${task.id}.json` : "task.json";

View File

@@ -114,9 +114,11 @@ export const ChartTypeOption = ({
label={t`This is a development version of the visualization`}
>
<Badge
color="brand"
size="sm"
variant="outline"
aria-label={t`This is a development version of the visualization`}
>{t`dev`}</Badge>
>{t`Dev`}</Badge>
</Tooltip>
)}
</Stack>

View File

@@ -87,7 +87,7 @@ export const JobRunSidebar = memo(function JobRunSidebar({
<JobRunInfoSection run={run} />
<Stack gap="md">
<Group gap="sm" wrap="nowrap">
<Badge variant="filled" bg="core-brand">
<Badge color="brand" size="sm" variant="filled">
{transformRuns.length}
</Badge>
<Title order={5}>{t`Transforms`}</Title>

View File

@@ -49,7 +49,8 @@ export const DERIVED_COLOR_KEYS = [
"core-filter",
"core-summarize",
] as const;
type DerivedColorKey = (typeof DERIVED_COLOR_KEYS)[number];
export type DerivedColorKey = (typeof DERIVED_COLOR_KEYS)[number];
/**
* Metabase color keys without colors that are automatically derived.

View File

@@ -1,15 +1,116 @@
import { Badge } from "@mantine/core";
export type { BadgeProps } from "@mantine/core";
import type { ColorName } from "metabase/ui/colors/types";
import { color } from "metabase/ui/utils/colors";
import BadgeStyles from "./Badge.module.css";
const BADGE_COLORS = [
"neutral",
"brand",
"negative",
"warning",
"positive",
] as const;
type BadgeColor = (typeof BADGE_COLORS)[number];
const FILLED_BG: Record<BadgeColor, ColorName> = {
neutral: "feedback-neutral-strong",
brand: "background_surface-brand-strong",
negative: "feedback-negative-strong",
warning: "feedback-warning-strong",
positive: "feedback-positive-strong",
};
const FILLED_TEXT: Record<BadgeColor, ColorName> = {
neutral: "core-white_constant",
brand: "text-primary-inverse",
negative: "text-primary-inverse",
warning: "text-primary-inverse",
positive: "text-primary-inverse",
};
const ACCENT_TEXT: Record<BadgeColor, ColorName> = {
neutral: "text-primary",
brand: "text-brand-strong",
negative: "feedback-negative-strong",
warning: "feedback-warning-strong",
positive: "feedback-positive-strong",
};
const OUTLINE_BORDER: Record<BadgeColor, ColorName> = {
neutral: "feedback-neutral-strong",
brand: "core-brand",
negative: "feedback-negative-strong",
warning: "feedback-warning-strong",
positive: "feedback-positive-strong",
};
const LIGHT_BG: ColorName = "background_surface-secondary";
const INDICATOR_BG: Record<BadgeColor, ColorName> = {
neutral: "icon-disabled",
brand: "core-brand",
negative: "feedback-negative",
warning: "feedback-warning",
positive: "feedback-positive",
};
const SIZES = {
xs: { height: "1rem", paddingX: "0.375rem" }, // 16px / 6px
sm: { height: "1.5rem", paddingX: "0.5rem" }, // 24px / 8px
} as const;
const isBadgeColor = (value: unknown): value is BadgeColor =>
BADGE_COLORS.includes(value as BadgeColor);
type BadgeRootVars = Partial<
Record<
| "--badge-height"
| "--badge-padding-x"
| "--badge-fz"
| "--badge-bg"
| "--badge-color"
| "--badge-bd",
string
>
>;
export const badgeOverrides = {
Badge: Badge.extend({
defaultProps: {
variant: "light",
},
classNames: {
root: BadgeStyles.root,
},
vars: (_theme, props) => {
const { size, variant, color: badgeColor } = props;
const isIndicator = "data-indicator" in props;
const { height, paddingX } = SIZES[size === "sm" ? "sm" : "xs"];
const root: BadgeRootVars = {
"--badge-height": height,
"--badge-padding-x": paddingX,
"--badge-fz": "0.6875rem", // 11px
};
// Non-BadgeColor values fall through to Mantine's default resolver so
// colors like "core-filter" can still work.
if (isBadgeColor(badgeColor)) {
if (isIndicator) {
root["--badge-bg"] = color(INDICATOR_BG[badgeColor]);
} else if (variant === "filled") {
root["--badge-bg"] = color(FILLED_BG[badgeColor]);
root["--badge-color"] = color(FILLED_TEXT[badgeColor]);
} else if (variant === "light") {
root["--badge-bg"] = color(LIGHT_BG);
root["--badge-color"] = color(ACCENT_TEXT[badgeColor]);
} else if (variant === "outline") {
root["--badge-bg"] = "transparent";
root["--badge-color"] = color(ACCENT_TEXT[badgeColor]);
root["--badge-bd"] = `1px solid ${color(OUTLINE_BORDER[badgeColor])}`;
}
}
return { root };
},
}),
};

View File

@@ -1,3 +1,16 @@
.root {
background-color: var(--mb-color-background_surface-brand-subtle);
--badge-radius: 1rem; /* 16px */
text-transform: none;
font-weight: 600;
line-height: 0.9375rem; /* 15px */
letter-spacing: 0;
}
.root[data-indicator] {
width: 0.5rem; /* 8px */
height: 0.5rem;
min-width: 0.5rem;
padding: 0;
border-radius: 50%;
}

View File

@@ -0,0 +1,159 @@
import { Fragment, type ReactNode } from "react";
import {
Badge,
type BadgeColor,
type BadgeProps,
Box,
Group,
Text,
} from "metabase/ui";
import {
StoryJsx,
StorySection,
StoryShowcase,
} from "metabase/ui/stories/showcase";
const VARIANTS = ["filled", "light", "outline"] as const;
const COLORS: BadgeColor[] = [
"neutral",
"brand",
"negative",
"warning",
"positive",
];
const SIZES = ["xs", "sm"] as const;
const args = {
variant: "light",
color: "neutral",
size: "xs",
children: "Badge",
};
const argTypes = {
variant: {
options: VARIANTS,
control: { type: "inline-radio" },
},
color: {
options: COLORS,
control: { type: "inline-radio" },
},
size: {
options: SIZES,
control: { type: "inline-radio" },
},
circle: {
control: { type: "boolean" },
},
children: {
control: { type: "text" },
},
};
export default {
title: "Components/Data display/Badge",
component: Badge,
args,
argTypes,
};
export const Default = {
render: (args: BadgeProps) => <Badge {...args} />,
};
const gridStyle = {
display: "grid",
gridTemplateColumns: "24rem 4rem 4rem",
columnGap: "1rem",
rowGap: "0.75rem",
alignItems: "center",
} as const;
const SizeHeader = () => (
<>
<div />
{SIZES.map((size) => (
<Text key={size} size="sm" fw="bold" c="text-secondary">
{size}
</Text>
))}
</>
);
const VariantSection = ({
variant,
children,
}: {
variant: (typeof VARIANTS)[number];
children: ReactNode;
}) => (
<StorySection title={variant[0].toUpperCase() + variant.slice(1)}>
<Box style={gridStyle}>
{COLORS.map((color) => (
<Fragment key={color}>
<StoryJsx>{`<Badge variant="${variant}" color="${color}" />`}</StoryJsx>
{SIZES.map((size) => (
<Badge key={size} variant={variant} color={color} size={size}>
{children}
</Badge>
))}
</Fragment>
))}
</Box>
</StorySection>
);
const Overview = ({ children }: BadgeProps) => (
<StoryShowcase title="Badge">
<StorySection title="Sizes" description="Note: the default size is “xs”.">
<Box style={gridStyle}>
<SizeHeader />
<StoryJsx>{`<Badge size="…" />`}</StoryJsx>
{SIZES.map((size) => (
<Badge key={size} size={size}>
{children}
</Badge>
))}
</Box>
</StorySection>
{VARIANTS.map((variant) => (
<VariantSection key={variant} variant={variant}>
{children}
</VariantSection>
))}
<StorySection title="Circle">
<Box style={gridStyle}>
<StoryJsx>{`<Badge variant="filled" color="neutral" circle />`}</StoryJsx>
{SIZES.map((size) => (
<Badge key={size} variant="filled" color="neutral" circle size={size}>
1
</Badge>
))}
</Box>
</StorySection>
<StorySection title="Indicator">
<Box style={{ ...gridStyle, gridTemplateColumns: "24rem 8rem" }}>
<StoryJsx>{`<Badge indicator color="…" />`}</StoryJsx>
<Group gap="sm">
{COLORS.map((color) => (
<Badge key={color} indicator color={color} />
))}
</Group>
</Box>
</StorySection>
</StoryShowcase>
);
export const SizesAndVariants = {
render: Overview,
name: "Sizes and variants",
args: { theme: "light" },
parameters: {
controls: { include: ["theme", "children"] },
},
};

View File

@@ -0,0 +1,48 @@
import {
Badge as MantineBadge,
type BadgeProps as MantineBadgeProps,
} from "@mantine/core";
import { type ComponentPropsWithoutRef, forwardRef } from "react";
import type { DerivedColorKey } from "metabase/ui/colors/types/theme";
export type BadgeColor =
| "neutral"
| "brand"
| "negative"
| "warning"
| "positive";
export interface BadgeProps
extends
Omit<MantineBadgeProps, "color">,
Omit<ComponentPropsWithoutRef<"div">, keyof MantineBadgeProps | "color"> {
color?: BadgeColor | DerivedColorKey;
/** Render as a small solid dot. Used as a status indicator. */
indicator?: boolean;
}
export const Badge = forwardRef<HTMLDivElement, BadgeProps>(function Badge(
{
color = "neutral",
size = "xs",
variant = "filled",
indicator,
children,
...props
},
ref,
) {
return (
<MantineBadge
color={color as MantineBadgeProps["color"]}
ref={ref}
size={size}
variant={variant}
{...(indicator ? { "data-indicator": true } : {})}
{...props}
>
{indicator ? null : children}
</MantineBadge>
);
});

View File

@@ -1,2 +1,2 @@
export { Badge } from "@mantine/core";
export * from "./Badge.config";
export { Badge, type BadgeColor, type BadgeProps } from "./Badge";
export { badgeOverrides } from "./Badge.config";

View File

@@ -35,16 +35,7 @@ const ChartSettingsWidgetList = ({
return (
<div key={`group-${groupIndex}`}>
{group && (
<Badge
mb="lg"
ml="lg"
fz="0.75rem"
px="0.5rem"
tt="none"
radius="xs"
size="lg"
c="text-primary"
>
<Badge color="brand" mb="lg" ml="lg" size="sm" variant="light">
{group}
</Badge>
)}

View File

@@ -132,7 +132,7 @@ export function PreviousValueComparison({
</VariationDetails>
{showsOtherValuesInTooltip && (
<Badge px="xs" size="xs" variant="light" w={ELLIPSIS_BADGE_WIDTH}>
<Badge size="xs" variant="light" w={ELLIPSIS_BADGE_WIDTH}>
<Group align="center" h="100%">
<Icon name="ellipsis" size={12} />
</Group>

View File

@@ -16,6 +16,7 @@ module.exports = {
"^Components/Documents",
"^Components/Feedback/Alert",
"^Components/Ask Before Using/Chip Overview",
"^Components/Data display/Badge Sizes and variants",
"^Components/Data display/KeyboardShortcut Overview",
"^Components/Table",
"^App/Palette",