feat: create UI badges for labeling beta features (#14661)

* chore: finish draft work for FeatureBadge component

* fix: add visually-hidden helper text for screen readers

* chore: add stories for highlighted state

* fix: update base styles

* chore: remove debug display option

* chore: update Popover to propagate events

* wip: commit progress on FeatureBadge update

* wip: commit more progress

* chore: update tag definitions to satify Biome

* chore: update all colors for preview role

* fix: make sure badge shows as hovered while inside tooltip

* wip: commit progress on adding story for controlled variant

* fix: sort imports

* refactor: change component API to be more obvious/ergonomic

* fix: add biome-ignore comments to more base files

* fix: update import order again

* chore: revert biome-ignore comment

* chore: update body text for tooltip

* chore: update dark preivew role to use sky palette

* chore: update color palettes for light/darkBlue themes

* chore: add beta badge to organizations subheader

* chore: add beta badge to organizations settings page

* chore: beef up font weight for form header

* fix: update text contrast for org menu list

* chore: add beta badge to deployment dropdown

* fix: run biome on imports

* chore: remove API for controlling FeatureBadge hover styling externally

* chore: add xs size for badge

* fix: update font weight for xs feature badges

* chore: add beta badges to all org headers

* fix: turn badges and tooltips into separate concerns

* fix: update hover styling

* docs: update wording on comment

* fix: apply formatting

* chore: rename FeatureBadge to FeatureStageBadge

* refactor: redefine FeatureStageBadge

* chore: update stories

* fix: add blur behavior to popover

* chore: revert theme colors

* chore: create featureStage branding namespace

* fix: make sure cleanup function is set up properly

* docs: update wording on comment for clarity

* refactor: move styles down
This commit is contained in:
Michael Smith
2024-09-20 16:13:39 -05:00
committed by GitHub
parent 3338f32489
commit 661d22621a
22 changed files with 417 additions and 95 deletions
+1
View File
@@ -120,6 +120,7 @@
"stretchr",
"STTY",
"stuntest",
"subpage",
"tailbroker",
"tailcfg",
"tailexchange",
@@ -0,0 +1,38 @@
import type { Meta, StoryObj } from "@storybook/react";
import { FeatureStageBadge } from "./FeatureStageBadge";
const meta: Meta<typeof FeatureStageBadge> = {
title: "components/FeatureStageBadge",
component: FeatureStageBadge,
args: {
contentType: "beta",
},
};
export default meta;
type Story = StoryObj<typeof FeatureStageBadge>;
export const MediumBeta: Story = {
args: {
size: "md",
},
};
export const SmallBeta: Story = {
args: {
size: "sm",
},
};
export const LargeBeta: Story = {
args: {
size: "lg",
},
};
export const MediumExperimental: Story = {
args: {
size: "md",
contentType: "experimental",
},
};
@@ -0,0 +1,133 @@
import type { Interpolation, Theme } from "@emotion/react";
import Link from "@mui/material/Link";
import { visuallyHidden } from "@mui/utils";
import { HelpTooltipContent } from "components/HelpTooltip/HelpTooltip";
import { Popover, PopoverTrigger } from "components/Popover/Popover";
import type { FC, HTMLAttributes, ReactNode } from "react";
import { docs } from "utils/docs";
/**
* All types of feature that we are currently supporting. Defined as record to
* ensure that we can't accidentally make typos when writing the badge text.
*/
const featureStageBadgeTypes = {
beta: "beta",
experimental: "experimental",
} as const satisfies Record<string, ReactNode>;
type FeatureStageBadgeProps = Readonly<
Omit<HTMLAttributes<HTMLSpanElement>, "children"> & {
contentType: keyof typeof featureStageBadgeTypes;
size?: "sm" | "md" | "lg";
}
>;
export const FeatureStageBadge: FC<FeatureStageBadgeProps> = ({
contentType,
size = "md",
...delegatedProps
}) => {
return (
<Popover mode="hover">
<PopoverTrigger>
{({ isOpen }) => (
<span
css={[
styles.badge,
size === "sm" && styles.badgeSmallText,
size === "lg" && styles.badgeLargeText,
isOpen && styles.badgeHover,
]}
{...delegatedProps}
>
<span style={visuallyHidden}> (This is a</span>
{featureStageBadgeTypes[contentType]}
<span style={visuallyHidden}> feature)</span>
</span>
)}
</PopoverTrigger>
<HelpTooltipContent
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
transformOrigin={{ vertical: "top", horizontal: "center" }}
>
<p css={styles.tooltipDescription}>
This feature has not yet reached general availability (GA).
</p>
<Link
href={docs("/contributing/feature-stages")}
target="_blank"
rel="noreferrer"
css={styles.tooltipLink}
>
Learn about feature stages
<span style={visuallyHidden}> (link opens in new tab)</span>
</Link>
</HelpTooltipContent>
</Popover>
);
};
const styles = {
badge: (theme) => ({
// Base type is based on a span so that the element can be placed inside
// more types of HTML elements without creating invalid markdown, but we
// still want the default display behavior to be div-like
display: "block",
maxWidth: "fit-content",
// Base style assumes that medium badges will be the default
fontSize: "0.75rem",
cursor: "default",
flexShrink: 0,
padding: "4px 8px",
lineHeight: 1,
whiteSpace: "nowrap",
border: `1px solid ${theme.branding.featureStage.border}`,
color: theme.branding.featureStage.text,
backgroundColor: theme.branding.featureStage.background,
borderRadius: "6px",
transition:
"color 0.2s ease-in-out, border-color 0.2s ease-in-out, background-color 0.2s ease-in-out",
}),
badgeHover: (theme) => ({
color: theme.branding.featureStage.hover.text,
borderColor: theme.branding.featureStage.hover.border,
backgroundColor: theme.branding.featureStage.hover.background,
}),
badgeLargeText: {
fontSize: "1rem",
},
badgeSmallText: {
// Have to beef up font weight so that the letters still maintain the
// same relative thickness as all our other main UI text
fontWeight: 500,
fontSize: "0.625rem",
},
tooltipTitle: (theme) => ({
color: theme.palette.text.primary,
fontWeight: 600,
fontFamily: "inherit",
fontSize: 18,
margin: 0,
lineHeight: 1,
paddingBottom: "8px",
}),
tooltipDescription: {
margin: 0,
lineHeight: 1.4,
paddingBottom: "8px",
},
tooltipLink: {
fontWeight: 600,
lineHeight: 1.2,
},
} as const satisfies Record<string, Interpolation<Theme>>;
+1 -1
View File
@@ -170,7 +170,7 @@ const styles = {
formSectionInfoTitle: (theme) => ({
fontSize: 20,
color: theme.palette.text.primary,
fontWeight: 400,
fontWeight: 500,
margin: 0,
marginBottom: 8,
display: "flex",
+59 -19
View File
@@ -1,16 +1,19 @@
import MuiPopover, {
type PopoverProps as MuiPopoverProps,
// biome-ignore lint/nursery/noRestrictedImports: Used as base component
// biome-ignore lint/nursery/noRestrictedImports: This is the base component that our custom popover is based on
} from "@mui/material/Popover";
import {
type FC,
type HTMLAttributes,
type PointerEvent,
type PointerEventHandler,
type ReactElement,
type ReactNode,
type RefObject,
cloneElement,
createContext,
useContext,
useEffect,
useId,
useRef,
useState,
@@ -20,10 +23,13 @@ type TriggerMode = "hover" | "click";
type TriggerRef = RefObject<HTMLElement>;
type TriggerElement = ReactElement<{
ref: TriggerRef;
onClick?: () => void;
}>;
// Have to append ReactNode type to satisfy React's cloneElement function. It
// has absolutely no bearing on what happens at runtime
type TriggerElement = ReactNode &
ReactElement<{
ref: TriggerRef;
onClick?: () => void;
}>;
type PopoverContextValue = {
id: string;
@@ -61,6 +67,15 @@ export const Popover: FC<PopoverProps> = (props) => {
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
const triggerRef: TriggerRef = useRef(null);
// Helps makes sure that popovers close properly when the user switches to
// a different tab. This won't help with controlled instances of the
// component, but this is basically the most we can do from here
useEffect(() => {
const closeOnTabSwitch = () => setUncontrolledOpen(false);
window.addEventListener("blur", closeOnTabSwitch);
return () => window.removeEventListener("blur", closeOnTabSwitch);
}, []);
const value: PopoverContextValue = {
triggerRef,
id: `${hookId}-popover`,
@@ -86,30 +101,47 @@ export const usePopover = () => {
return context;
};
export const PopoverTrigger = (
props: HTMLAttributes<HTMLElement> & {
children: TriggerElement;
},
) => {
type PopoverTriggerRenderProps = Readonly<{
isOpen: boolean;
}>;
type PopoverTriggerProps = Readonly<
Omit<HTMLAttributes<HTMLElement>, "children"> & {
children:
| TriggerElement
| ((props: PopoverTriggerRenderProps) => TriggerElement);
}
>;
export const PopoverTrigger: FC<PopoverTriggerProps> = (props) => {
const popover = usePopover();
const { children, ...elementProps } = props;
const { children, onClick, onPointerEnter, onPointerLeave, ...elementProps } =
props;
const clickProps = {
onClick: () => {
onClick: (event: PointerEvent<HTMLElement>) => {
popover.setOpen(true);
onClick?.(event);
},
};
const hoverProps = {
onPointerEnter: () => {
onPointerEnter: (event: PointerEvent<HTMLElement>) => {
popover.setOpen(true);
onPointerEnter?.(event);
},
onPointerLeave: () => {
onPointerLeave: (event: PointerEvent<HTMLElement>) => {
popover.setOpen(false);
onPointerLeave?.(event);
},
};
return cloneElement(props.children, {
const evaluatedChildren =
typeof children === "function"
? children({ isOpen: popover.open })
: children;
return cloneElement(evaluatedChildren, {
...elementProps,
...(popover.mode === "click" ? clickProps : hoverProps),
"aria-haspopup": true,
@@ -130,6 +162,8 @@ export type PopoverContentProps = Omit<
export const PopoverContent: FC<PopoverContentProps> = ({
horizontal = "left",
onPointerEnter,
onPointerLeave,
...popoverProps
}) => {
const popover = usePopover();
@@ -152,7 +186,7 @@ export const PopoverContent: FC<PopoverContentProps> = ({
},
}}
{...horizontalProps(horizontal)}
{...modeProps(popover)}
{...modeProps(popover, onPointerEnter, onPointerLeave)}
{...popoverProps}
id={popover.id}
open={popover.open}
@@ -162,14 +196,20 @@ export const PopoverContent: FC<PopoverContentProps> = ({
);
};
const modeProps = (popover: PopoverContextValue) => {
const modeProps = (
popover: PopoverContextValue,
externalOnPointerEnter: PointerEventHandler<HTMLDivElement> | undefined,
externalOnPointerLeave: PointerEventHandler<HTMLDivElement> | undefined,
) => {
if (popover.mode === "hover") {
return {
onPointerEnter: () => {
onPointerEnter: (event: PointerEvent<HTMLDivElement>) => {
popover.setOpen(true);
externalOnPointerEnter?.(event);
},
onPointerLeave: () => {
onPointerLeave: (event: PointerEvent<HTMLDivElement>) => {
popover.setOpen(false);
externalOnPointerLeave?.(event);
},
};
}
@@ -10,6 +10,7 @@ interface HeaderProps {
secondary?: boolean;
docsHref?: string;
tooltip?: ReactNode;
badges?: ReactNode;
}
export const SettingsHeader: FC<HeaderProps> = ({
@@ -18,35 +19,40 @@ export const SettingsHeader: FC<HeaderProps> = ({
docsHref,
secondary,
tooltip,
badges,
}) => {
const theme = useTheme();
return (
<Stack alignItems="baseline" direction="row" justifyContent="space-between">
<div css={{ maxWidth: 420, marginBottom: 24 }}>
<Stack direction="row" spacing={1} alignItems="center">
<h1
css={[
{
fontSize: 32,
fontWeight: 700,
display: "flex",
alignItems: "center",
lineHeight: "initial",
margin: 0,
marginBottom: 4,
gap: 8,
},
secondary && {
fontSize: 24,
fontWeight: 500,
},
]}
>
{title}
</h1>
{tooltip}
<Stack direction="row" spacing={2} alignItems="center">
<Stack direction="row" spacing={1} alignItems="center">
<h1
css={[
{
fontSize: 32,
fontWeight: 700,
display: "flex",
alignItems: "baseline",
lineHeight: "initial",
margin: 0,
marginBottom: 4,
gap: 8,
},
secondary && {
fontSize: 24,
fontWeight: 500,
},
]}
>
{title}
</h1>
{tooltip}
</Stack>
{badges}
</Stack>
{description && (
<span
css={{
@@ -149,7 +149,7 @@ const styles = {
menuItem: (theme) => css`
text-decoration: none;
color: inherit;
gap: 20px;
gap: 8px;
padding: 8px 20px;
font-size: 14px;
@@ -5,6 +5,7 @@ import { organizationPermissions } from "api/queries/organizations";
import { deleteOrganizationRole, organizationRoles } from "api/queries/roles";
import type { Role } from "api/typesGenerated";
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
@@ -66,6 +67,7 @@ export const CustomRolesPage: FC = () => {
<SettingsHeader
title="Custom Roles"
description="Manage custom roles for this organization."
badges={<FeatureStageBadge contentType="beta" size="lg" />}
/>
{permissions.assignOrgRole && isCustomRolesEnabled && (
<Button component={RouterLink} startIcon={<AddIcon />} to="create">
@@ -5,6 +5,7 @@ import { groupsByOrganization } from "api/queries/groups";
import { organizationPermissions } from "api/queries/organizations";
import type { Organization } from "api/typesGenerated";
import { EmptyState } from "components/EmptyState/EmptyState";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import { displayError } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
@@ -80,6 +81,7 @@ export const GroupsPage: FC = () => {
<SettingsHeader
title="Groups"
description="Manage groups for this organization."
badges={<FeatureStageBadge contentType="beta" size="lg" />}
/>
{permissions.createGroup && feats.template_rbac && (
<Button component={RouterLink} startIcon={<GroupAdd />} to="create">
@@ -1,6 +1,7 @@
import AddIcon from "@mui/icons-material/AddOutlined";
import LaunchOutlined from "@mui/icons-material/LaunchOutlined";
import Button from "@mui/material/Button";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import type { FC } from "react";
@@ -73,6 +74,7 @@ export const IdpSyncPage: FC = () => {
title="IdP Sync"
description="Group and role sync mappings (configured outside Coder)."
tooltip={<IdpSyncHelpTooltip />}
badges={<FeatureStageBadge contentType="beta" size="lg" />}
/>
<Stack direction="row" spacing={2}>
<Button
@@ -17,6 +17,7 @@ import type {
} from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { AvatarData } from "components/AvatarData/AvatarData";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import {
MoreMenu,
@@ -60,7 +61,10 @@ export const OrganizationMembersPageView: FC<
> = (props) => {
return (
<div>
<SettingsHeader title="Members" />
<SettingsHeader
title="Members"
badges={<FeatureStageBadge contentType="beta" size="lg" />}
/>
<Stack>
{Boolean(props.error) && <ErrorAlert error={props.error} />}
@@ -6,7 +6,8 @@ import type {
ProvisionerKeyDaemons,
} from "api/typesGenerated";
import { EmptyState } from "components/EmptyState/EmptyState";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { ProvisionerGroup } from "modules/provisioners/ProvisionerGroup";
import type { FC } from "react";
@@ -17,7 +18,7 @@ interface OrganizationProvisionersPageViewProps {
buildInfo?: BuildInfoResponse;
/** Groups of provisioners, along with their key information */
provisioners: ProvisionerKeyDaemons[];
provisioners: readonly ProvisionerKeyDaemons[];
}
export const OrganizationProvisionersPageView: FC<
@@ -33,21 +34,23 @@ export const OrganizationProvisionersPageView: FC<
return (
<div>
<PageHeader
// The deployment settings layout already has padding.
css={{ paddingTop: 0, paddingBottom: 12 }}
actions={
<Button
endIcon={<OpenInNewIcon />}
target="_blank"
href={docs("/admin/provisioners")}
>
Create a provisioner
</Button>
}
<Stack
alignItems="baseline"
direction="row"
justifyContent="space-between"
>
<PageHeaderTitle>Provisioners</PageHeaderTitle>
</PageHeader>
<SettingsHeader
title="Provisioners"
badges={<FeatureStageBadge contentType="beta" size="lg" />}
/>
<Button
endIcon={<OpenInNewIcon />}
target="_blank"
href={docs("/admin/provisioners")}
>
Create a provisioner
</Button>
</Stack>
{isEmpty ? (
<EmptyState
message="No provisioners"
@@ -8,6 +8,7 @@ import type {
} from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import {
FormFields,
FormFooter,
@@ -66,7 +67,10 @@ export const OrganizationSettingsPageView: FC<
return (
<div>
<SettingsHeader title="Settings" />
<SettingsHeader
title="Settings"
badges={<FeatureStageBadge contentType="beta" size="lg" />}
/>
{Boolean(error) && !isApiValidationError(error) && (
<div css={{ marginBottom: 32 }}>
@@ -7,6 +7,7 @@ import type {
Experiments,
Organization,
} from "api/typesGenerated";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import { Loader } from "components/Loader/Loader";
import { Sidebar as BaseSidebar } from "components/Sidebar/Sidebar";
import { Stack } from "components/Stack/Stack";
@@ -47,7 +48,10 @@ export const SidebarView: FC<SidebarProps> = ({
// TODO: Do something nice to scroll to the active org.
return (
<BaseSidebar>
<header css={styles.sidebarHeader}>Deployment</header>
<header>
<h2 css={styles.sidebarHeader}>Deployment</h2>
</header>
<DeploymentSettingsNavigation
active={!activeOrganizationName && activeSettings}
experiments={experiments}
@@ -190,7 +194,18 @@ const OrganizationsSettingsNavigation: FC<
return (
<>
<header css={styles.sidebarHeader}>Organizations</header>
<header
css={{
display: "flex",
flexFlow: "row wrap",
columnGap: "8px",
alignItems: "baseline",
}}
>
<h2 css={styles.sidebarHeader}>Organizations</h2>
<FeatureStageBadge contentType="beta" size="sm" />
</header>
{permissions.createOrganization && (
<SidebarNavItem
active="auto"
@@ -364,7 +379,8 @@ const SidebarNavSubItem: FC<SidebarNavSubItemProps> = ({
const styles = {
sidebarHeader: {
textTransform: "uppercase",
letterSpacing: "0.15em",
letterSpacing: "0.1em",
margin: 0,
fontSize: 11,
fontWeight: 500,
paddingBottom: 4,
@@ -396,7 +412,7 @@ const classNames = {
`,
subLink: (css, theme) => css`
color: inherit;
color: ${theme.palette.text.secondary};
text-decoration: none;
display: block;
@@ -409,11 +425,13 @@ const classNames = {
position: relative;
&:hover {
color: ${theme.palette.text.primary};
background-color: ${theme.palette.action.hover};
}
`,
activeSubLink: (css) => css`
activeSubLink: (css, theme) => css`
color: ${theme.palette.text.primary};
font-weight: 600;
`,
} satisfies Record<string, ClassName>;
@@ -175,23 +175,23 @@ const ThemePreview: FC<ThemePreviewProps> = ({
<div css={styles.page}>
<div css={styles.header}>
<div css={styles.headerLinks}>
<div css={[styles.headerLink, styles.activeHeaderLink]}></div>
<div css={styles.headerLink}></div>
<div css={styles.headerLink}></div>
<div css={[styles.headerLink, styles.activeHeaderLink]} />
<div css={styles.headerLink} />
<div css={styles.headerLink} />
</div>
<div css={styles.headerLinks}>
<div css={styles.proxy}></div>
<div css={styles.user}></div>
<div css={styles.proxy} />
<div css={styles.user} />
</div>
</div>
<div css={styles.body}>
<div css={styles.title}></div>
<div css={styles.title} />
<div css={styles.table}>
<div css={styles.tableHeader}></div>
<div css={styles.workspace}></div>
<div css={styles.workspace}></div>
<div css={styles.workspace}></div>
<div css={styles.workspace}></div>
<div css={styles.tableHeader} />
<div css={styles.workspace} />
<div css={styles.workspace} />
<div css={styles.workspace} />
<div css={styles.workspace} />
</div>
</div>
</div>
+21 -6
View File
@@ -1,14 +1,29 @@
export interface Branding {
enterprise: {
export type Branding = Readonly<{
enterprise: Readonly<{
background: string;
divider: string;
border: string;
text: string;
};
premium: {
}>;
premium: Readonly<{
background: string;
divider: string;
border: string;
text: string;
};
}
}>;
featureStage: Readonly<{
background: string;
divider: string;
border: string;
text: string;
hover: Readonly<{
background: string;
divider: string;
border: string;
text: string;
}>;
}>;
}>;
+18 -2
View File
@@ -1,7 +1,7 @@
import type { Branding } from "../branding";
import colors from "../tailwindColors";
export default {
export const branding: Branding = {
enterprise: {
background: colors.blue[950],
divider: colors.blue[900],
@@ -14,4 +14,20 @@ export default {
border: colors.violet[400],
text: colors.violet[50],
},
} satisfies Branding;
featureStage: {
background: colors.sky[950],
divider: colors.sky[900],
border: colors.sky[400],
text: colors.sky[400],
hover: {
background: colors.zinc[950],
divider: colors.zinc[900],
border: colors.sky[400],
text: colors.sky[400],
},
},
};
export default branding;
+4 -2
View File
@@ -1,7 +1,7 @@
import type { Roles } from "../roles";
import colors from "../tailwindColors";
export default {
const roles: Roles = {
danger: {
background: colors.orange[950],
outline: colors.orange[500],
@@ -152,4 +152,6 @@ export default {
text: colors.white,
},
},
} satisfies Roles;
};
export default roles;
+18 -2
View File
@@ -1,7 +1,7 @@
import type { Branding } from "../branding";
import colors from "../tailwindColors";
export default {
export const branding: Branding = {
enterprise: {
background: colors.blue[950],
divider: colors.blue[900],
@@ -14,4 +14,20 @@ export default {
border: colors.violet[400],
text: colors.violet[50],
},
} satisfies Branding;
featureStage: {
background: colors.sky[900],
divider: colors.sky[800],
border: colors.sky[400],
text: colors.sky[400],
hover: {
background: colors.gray[900],
divider: colors.gray[800],
border: colors.sky[400],
text: colors.sky[400],
},
},
};
export default branding;
+4 -2
View File
@@ -1,7 +1,7 @@
import type { Roles } from "../roles";
import colors from "../tailwindColors";
export default {
const roles: Roles = {
danger: {
background: colors.orange[950],
outline: colors.orange[500],
@@ -152,4 +152,6 @@ export default {
text: colors.white,
},
},
} satisfies Roles;
};
export default roles;
+18 -2
View File
@@ -1,7 +1,7 @@
import type { Branding } from "../branding";
import colors from "../tailwindColors";
export default {
export const branding: Branding = {
enterprise: {
background: colors.blue[100],
divider: colors.blue[300],
@@ -14,4 +14,20 @@ export default {
border: colors.violet[600],
text: colors.violet[950],
},
} satisfies Branding;
featureStage: {
background: colors.sky[50],
divider: colors.sky[100],
border: colors.sky[700],
text: colors.sky[700],
hover: {
background: colors.white,
divider: colors.zinc[100],
border: colors.sky[700],
text: colors.sky[700],
},
},
};
export default branding;
+4 -2
View File
@@ -1,7 +1,7 @@
import type { Roles } from "../roles";
import colors from "../tailwindColors";
export default {
const roles: Roles = {
danger: {
background: colors.orange[50],
outline: colors.orange[400],
@@ -152,4 +152,6 @@ export default {
text: colors.white,
},
},
} satisfies Roles;
};
export default roles;