refactor(site): migrate 39 files from emotion css to tailwind

Migrate all non-MUI emotion files (useTheme + css= prop patterns) to
Tailwind utility classes. This covers all 5 'easy' files (static css=,
no theme) and 34 'theme' files (useTheme for palette colors, converted
to semantic Tailwind tokens).

Theme property mappings used:
- theme.palette.text.primary → text-content-primary
- theme.palette.text.secondary → text-content-secondary
- theme.palette.text.disabled → text-content-disabled
- theme.palette.divider → border-border-default
- theme.palette.background.paper → bg-surface-secondary
- theme.palette.background.default → bg-surface-primary
- theme.palette.primary.main → text-content-link
- theme.roles.active.outline → border-border-pending
- theme.roles.danger.outline → border-border-warning

Net reduction: ~750 lines removed.
This commit is contained in:
Kayla はな
2026-04-10 22:08:15 +00:00
parent bd467ce443
commit 7f16ef73f5
39 changed files with 403 additions and 1155 deletions
+10 -1
View File
@@ -12,6 +12,7 @@
import { useTheme } from "@emotion/react";
import { cva, type VariantProps } from "class-variance-authority";
import { Avatar as AvatarPrimitive } from "radix-ui";
import type { CSSProperties } from "react";
import { getExternalImageStylesFromUrl } from "#/theme/externalImages";
import { cn } from "#/utils/cn";
@@ -68,6 +69,9 @@ export const Avatar: React.FC<AvatarProps> = ({
children,
...props
}) => {
// Kept for runtime access to theme.externalImages, which provides
// dynamic CSS filter styles based on the current theme mode and URL
// parameters. No Tailwind token equivalent exists for this value.
const theme = useTheme();
return (
@@ -78,7 +82,12 @@ export const Avatar: React.FC<AvatarProps> = ({
<AvatarPrimitive.Image
src={src}
className="aspect-square size-full object-contain"
css={getExternalImageStylesFromUrl(theme.externalImages, src)}
style={
getExternalImageStylesFromUrl(
theme.externalImages,
src,
) as CSSProperties
}
/>
{fallback && (
<AvatarPrimitive.Fallback className="flex h-full w-full items-center justify-center rounded-full">
+4 -28
View File
@@ -1,4 +1,3 @@
import { type CSSObject, useTheme } from "@emotion/react";
import type { FC, ReactNode } from "react";
import { Avatar } from "#/components/Avatar/Avatar";
@@ -15,20 +14,11 @@ export const AvatarCard: FC<AvatarCardProps> = ({
subtitle,
maxWidth = "none",
}) => {
const theme = useTheme();
return (
<div
css={{
className="flex flex-row flex-nowrap items-center border border-solid border-border gap-4 p-4 rounded-lg cursor-default"
style={{
maxWidth: maxWidth === "none" ? undefined : `${maxWidth}px`,
display: "flex",
flexFlow: "row nowrap",
alignItems: "center",
border: `1px solid ${theme.palette.divider}`,
gap: "16px",
padding: "16px",
borderRadius: "8px",
cursor: "default",
}}
>
{/**
@@ -41,27 +31,13 @@ export const AvatarCard: FC<AvatarCardProps> = ({
<h3
// Lets users hover over truncated text to see whole thing
title={header}
css={[
theme.typography.body1 as CSSObject,
{
lineHeight: 1.4,
margin: 0,
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
},
]}
className="text-[1rem] leading-[1.4] m-0 truncate"
>
{header}
</h3>
{subtitle && (
<div
css={[
theme.typography.body2 as CSSObject,
{ color: theme.palette.text.secondary },
]}
>
<div className="text-[14px] leading-[160%] text-content-secondary">
{subtitle}
</div>
)}
+13 -34
View File
@@ -1,4 +1,3 @@
import type { Interpolation, Theme } from "@emotion/react";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import { type FC, useState } from "react";
import { Button } from "#/components/Button/Button";
@@ -7,7 +6,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import { MONOSPACE_FONT_FAMILY } from "#/theme/constants";
import { cn } from "#/utils/cn";
import { CopyButton } from "../CopyButton/CopyButton";
interface CodeExampleProps {
@@ -52,8 +51,18 @@ export const CodeExample: FC<CodeExampleProps> = ({
);
return (
<div css={styles.container} className={className}>
<code css={[styles.code, secret && styles.secret]}>
<div
className={cn(
"cursor-pointer flex flex-row items-center text-content-primary font-mono text-[14px] rounded-lg p-2 leading-[150%] border border-solid border-border hover:bg-surface-tertiary",
className,
)}
>
<code
className={cn(
"px-2 grow break-all",
secret && "[-webkit-text-security:disc]",
)}
>
{secret ? (
<>
{/*
@@ -99,33 +108,3 @@ export const CodeExample: FC<CodeExampleProps> = ({
function obfuscateText(text: string): string {
return new Array(text.length).fill("*").join("");
}
const styles = {
container: (theme) => ({
cursor: "pointer",
display: "flex",
flexDirection: "row",
alignItems: "center",
color: theme.experimental.l1.text,
fontFamily: MONOSPACE_FONT_FAMILY,
fontSize: 14,
borderRadius: 8,
padding: 8,
lineHeight: "150%",
border: `1px solid ${theme.experimental.l1.outline}`,
"&:hover": {
backgroundColor: theme.experimental.l2.hover.background,
},
}),
code: {
padding: "0 8px",
flexGrow: 1,
wordBreak: "break-all",
},
secret: {
"-webkit-text-security": "disc", // also supported by firefox
},
} satisfies Record<string, Interpolation<Theme>>;
@@ -1,16 +1,26 @@
import { useTheme } from "@emotion/react";
import type { CSSProperties } from "react";
import { getExternalImageStylesFromUrl } from "#/theme/externalImages";
export const ExternalImage: React.FC<React.ComponentPropsWithRef<"img">> = ({
...props
}) => {
// Kept for runtime access to theme.externalImages, which provides
// dynamic CSS filter styles based on the current theme mode and URL
// parameters. No Tailwind token equivalent exists for this value.
const theme = useTheme();
return (
// biome-ignore lint/a11y/useAltText: alt should be passed in as a prop
<img
css={getExternalImageStylesFromUrl(theme.externalImages, props.src)}
{...props}
style={{
...props.style,
...(getExternalImageStylesFromUrl(
theme.externalImages,
props.src,
) as CSSProperties),
}}
/>
);
};
+28 -82
View File
@@ -1,4 +1,3 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import {
type ComponentProps,
createContext,
@@ -22,21 +21,17 @@ type FormProps = HTMLProps<HTMLFormElement> & {
};
export const Form: FC<FormProps> = ({ direction, ...formProps }) => {
const theme = useTheme();
return (
<FormContext.Provider value={{ direction }}>
<form
{...formProps}
css={{
display: "flex",
flexDirection: "column",
gap: direction === "horizontal" ? 80 : 40,
[theme.breakpoints.down("md")]: {
gap: 64,
},
}}
className={cn(
"flex flex-col gap-16",
direction === "horizontal"
? "min-[900px]:gap-20"
: "min-[900px]:gap-10",
formProps.className,
)}
/>
</FormContext.Provider>
);
@@ -92,27 +87,35 @@ export const FormSection: FC<FormSectionProps> = ({
return (
<section
ref={ref}
css={[
styles.formSection,
direction === "horizontal" && styles.formSectionHorizontal,
]}
className={classes.root}
className={cn(
"flex items-start flex-col gap-4 min-[1200px]:gap-6",
direction === "horizontal" &&
"min-[1200px]:flex-row min-[1200px]:gap-[120px]",
classes.root,
)}
>
<div
css={[
styles.formSectionInfo,
direction === "horizontal" && styles.formSectionInfoHorizontal,
]}
className={classes.sectionInfo}
className={cn(
"w-full shrink-0 top-6",
direction === "horizontal" && "max-w-[312px] min-[1200px]:sticky",
classes.sectionInfo,
)}
>
<header className="flex items-center gap-4">
<h2 css={styles.formSectionInfoTitle} className={classes.infoTitle}>
<h2
className={cn(
"text-[20px] text-content-primary font-medium m-0 mb-2 flex flex-row items-center gap-3",
classes.infoTitle,
)}
>
{title}
</h2>
{alpha && <AlphaBadge />}
{deprecated && <DeprecatedBadge />}
</header>
<div css={styles.formSectionInfoDescription}>{description}</div>
<div className="text-[14px] text-content-secondary leading-[160%] m-0">
{description}
</div>
</div>
{children}
@@ -126,68 +129,11 @@ export const FormFields: FC<ComponentProps<typeof Stack>> = (props) => {
direction="column"
spacing={3}
{...props}
css={styles.formSectionFields}
className={cn("w-full", props.className)}
/>
);
};
const styles = {
formSection: (theme) => ({
display: "flex",
alignItems: "flex-start",
flexDirection: "column",
gap: 24,
[theme.breakpoints.down("lg")]: {
flexDirection: "column",
gap: 16,
},
}),
formSectionHorizontal: {
flexDirection: "row",
gap: 120,
},
formSectionInfo: (theme) => ({
width: "100%",
flexShrink: 0,
top: 24,
[theme.breakpoints.down("md")]: {
width: "100%",
position: "initial" as const,
},
}),
formSectionInfoHorizontal: (theme) => ({
maxWidth: 312,
[theme.breakpoints.up("lg")]: {
position: "sticky",
},
}),
formSectionInfoTitle: (theme) => ({
fontSize: 20,
color: theme.palette.text.primary,
fontWeight: 500,
margin: 0,
marginBottom: 8,
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 12,
}),
formSectionInfoDescription: (theme) => ({
fontSize: 14,
color: theme.palette.text.secondary,
lineHeight: "160%",
margin: 0,
}),
formSectionFields: {
width: "100%",
},
} satisfies Record<string, Interpolation<Theme>>;
export const FormFooter: FC<HTMLProps<HTMLDivElement>> = ({
className,
...props
@@ -1,4 +1,3 @@
import { useTheme } from "@emotion/react";
import {
cloneElement,
type FC,
@@ -51,10 +50,9 @@ export const TopbarData: FC<HTMLAttributes<HTMLDivElement>> = (props) => {
export const TopbarDivider: FC<
Omit<HTMLAttributes<HTMLSpanElement>, "children">
> = (props) => {
const theme = useTheme();
> = ({ className, ...props }) => {
return (
<span {...props} css={{ color: theme.palette.divider }}>
<span {...props} className={cn("text-border", className)}>
/
</span>
);
+9 -9
View File
@@ -1,12 +1,12 @@
import type { CSSObject } from "@emotion/react";
import { cn } from "#/utils/cn";
type StackProps = React.ComponentPropsWithRef<"div"> & {
className?: string;
direction?: "column" | "row";
spacing?: number;
alignItems?: CSSObject["alignItems"];
justifyContent?: CSSObject["justifyContent"];
wrap?: CSSObject["flexWrap"];
alignItems?: React.CSSProperties["alignItems"];
justifyContent?: React.CSSProperties["justifyContent"];
wrap?: React.CSSProperties["flexWrap"];
};
/**
@@ -26,14 +26,14 @@ export const Stack: React.FC<StackProps> = (props) => {
return (
<div
{...divProps}
css={{
display: "flex",
className={cn("flex max-w-full", divProps.className)}
style={{
...divProps.style,
flexDirection: direction,
gap: spacing * 8,
alignItems: alignItems,
justifyContent: justifyContent,
alignItems,
justifyContent,
flexWrap: wrap,
maxWidth: "100%",
}}
>
{children}
+8 -23
View File
@@ -1,4 +1,3 @@
import { useTheme } from "@emotion/react";
import { Building2Icon, UserIcon } from "lucide-react";
import type { FC } from "react";
import type { HealthMessage, ProvisionerDaemon } from "#/api/typesGenerated";
@@ -8,6 +7,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import { cn } from "#/utils/cn";
import { createDayString } from "#/utils/createDayString";
import { ProvisionerTag } from "./ProvisionerTag";
@@ -20,7 +20,6 @@ export const Provisioner: FC<ProvisionerProps> = ({
provisioner,
warnings,
}) => {
const theme = useTheme();
const daemonScope = provisioner.tags.scope || "organization";
const iconScope =
daemonScope === "organization" ? (
@@ -36,20 +35,16 @@ export const Provisioner: FC<ProvisionerProps> = ({
return (
<div
key={provisioner.name}
css={[
{
borderRadius: 8,
border: `1px solid ${theme.palette.divider}`,
fontSize: 14,
},
isWarning && { borderColor: theme.palette.warning.light },
]}
className={cn(
"rounded-lg border border-solid border-border text-sm",
isWarning && "border-border-warning",
)}
>
<header className="p-6 flex items-center justify-between gap-6">
<div className="flex items-center gap-6 object-fill">
<div className="leading-[160%]">
<h4 className="font-medium m-0">{provisioner.name}</h4>
<span css={{ color: theme.palette.text.secondary }}>
<span className="text-content-secondary">
<code>{provisioner.version}</code>
</span>
</div>
@@ -71,17 +66,7 @@ export const Provisioner: FC<ProvisionerProps> = ({
</div>
</header>
<div
css={{
borderTop: `1px solid ${theme.palette.divider}`,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "8px 24px",
fontSize: 12,
color: theme.palette.text.secondary,
}}
>
<div className="border-solid border-0 border-t border-border flex items-center justify-between py-2 px-6 text-xs text-content-secondary">
{warnings && warnings.length > 0 ? (
<div className="flex flex-col">
{warnings.map((warning) => (
@@ -92,7 +77,7 @@ export const Provisioner: FC<ProvisionerProps> = ({
<span>No warnings</span>
)}
{provisioner.last_seen_at && (
<span css={{ color: theme.roles.info.text }} data-chromatic="ignore">
<span className="text-content-primary" data-chromatic="ignore">
Last seen {createDayString(provisioner.last_seen_at)}
</span>
)}
@@ -1,4 +1,3 @@
import type { Interpolation, Theme } from "@emotion/react";
import { CircleCheckIcon, CircleMinusIcon, TagIcon, XIcon } from "lucide-react";
import type { ComponentProps, FC } from "react";
import { Button } from "#/components/Button/Button";
@@ -85,9 +84,9 @@ const BooleanPill: FC<BooleanPillProps> = ({
size="lg"
icon={
value ? (
<CircleCheckIcon css={styles.truePill} className="size-icon-sm" />
<CircleCheckIcon className="text-border-pending size-icon-sm" />
) : (
<CircleMinusIcon css={styles.falsePill} className="size-icon-sm" />
<CircleMinusIcon className="text-border-warning size-icon-sm" />
)
}
{...divProps}
@@ -96,12 +95,3 @@ const BooleanPill: FC<BooleanPillProps> = ({
</Pill>
);
};
const styles = {
truePill: (theme) => ({
color: theme.roles.active.outline,
}),
falsePill: (theme) => ({
color: theme.roles.danger.outline,
}),
} satisfies Record<string, Interpolation<Theme>>;
+17 -103
View File
@@ -1,9 +1,9 @@
import type { Interpolation, Theme } from "@emotion/react";
import type { FC } from "react";
import type { WorkspaceAgent } from "#/api/typesGenerated";
import { TerminalIcon } from "#/components/Icons/TerminalIcon";
import { VSCodeIcon } from "#/components/Icons/VSCodeIcon";
import { Stack } from "#/components/Stack/Stack";
import { cn } from "#/utils/cn";
import { DisplayAppNameMap } from "./AppLink/AppLink";
import { AppPreview } from "./AppLink/AppPreview";
import { BaseIcon } from "./AppLink/BaseIcon";
@@ -27,52 +27,42 @@ export const AgentRowPreview: FC<AgentRowPreviewProps> = ({
direction="row"
alignItems="center"
justifyContent="space-between"
css={styles.agentRow}
className="py-4 px-8 bg-surface-secondary text-[16px] relative [&:not(:last-child)]:pb-0 after:content-[''] after:absolute after:top-0 after:left-[43px] after:h-full after:w-0.5 after:bg-border"
>
<Stack direction="row" alignItems="baseline">
<div css={styles.agentStatusWrapper}>
<div css={styles.agentStatusPreview} />
<div className="flex w-6 justify-center shrink-0">
<div className="w-2.5 h-2.5 border-2 border-solid border-content-secondary rounded-full relative z-[1] bg-surface-secondary" />
</div>
<Stack
alignItems="baseline"
direction="row"
spacing={4}
css={styles.agentData}
className="text-[14px] text-content-secondary max-[900px]:gap-4 max-[900px]:flex-wrap"
>
<Stack
direction="row"
alignItems="baseline"
spacing={1}
css={[
styles.noShrink,
styles.agentDataItem,
(theme) => ({
[theme.breakpoints.up("sm")]: {
minWidth: alignValues ? 240 : undefined,
},
}),
]}
className={cn(
"shrink-0 max-[900px]:flex-col max-[900px]:items-start max-[900px]:gap-2 max-[900px]:w-fit",
alignValues && "min-[600px]:min-w-[240px]",
)}
>
<span>Agent:</span>
<span css={styles.agentDataValue}>{agent.name}</span>
<span className="text-content-primary">{agent.name}</span>
</Stack>
<Stack
direction="row"
alignItems="baseline"
spacing={1}
css={[
styles.noShrink,
styles.agentDataItem,
(theme) => ({
[theme.breakpoints.up("sm")]: {
minWidth: alignValues ? 100 : undefined,
},
}),
]}
className={cn(
"shrink-0 max-[900px]:flex-col max-[900px]:items-start max-[900px]:gap-2 max-[900px]:w-fit",
alignValues && "min-[600px]:min-w-[100px]",
)}
>
<span>OS:</span>
<span css={[styles.agentDataValue, styles.agentOS]}>
<span className="text-content-primary capitalize text-[14px]">
{agent.operating_system}
</span>
</Stack>
@@ -81,7 +71,7 @@ export const AgentRowPreview: FC<AgentRowPreviewProps> = ({
direction="row"
alignItems="center"
spacing={1}
css={styles.agentDataItem}
className="max-[900px]:flex-col max-[900px]:items-start max-[900px]:gap-2 max-[900px]:w-fit"
>
<span>Apps:</span>
<Stack
@@ -128,7 +118,7 @@ export const AgentRowPreview: FC<AgentRowPreviewProps> = ({
)
)}
{agent.apps.length === 0 && agent.display_apps.length === 0 && (
<span css={styles.agentDataValue}>None</span>
<span className="text-content-primary">None</span>
)}
</Stack>
</Stack>
@@ -137,79 +127,3 @@ export const AgentRowPreview: FC<AgentRowPreviewProps> = ({
</Stack>
);
};
const styles = {
agentRow: (theme) => ({
padding: "16px 32px",
backgroundColor: theme.palette.background.paper,
fontSize: 16,
position: "relative",
"&:not(:last-child)": {
paddingBottom: 0,
},
"&:after": {
content: "''",
height: "100%",
width: 2,
backgroundColor: theme.palette.divider,
position: "absolute",
top: 0,
left: 43,
},
}),
agentStatusWrapper: {
width: 24,
display: "flex",
justifyContent: "center",
flexShrink: 0,
},
agentStatusPreview: (theme) => ({
width: 10,
height: 10,
border: `2px solid ${theme.palette.text.secondary}`,
borderRadius: "100%",
position: "relative",
zIndex: 1,
background: theme.palette.background.paper,
}),
agentName: {
fontWeight: 600,
},
agentOS: {
textTransform: "capitalize",
fontSize: 14,
},
agentData: (theme) => ({
fontSize: 14,
color: theme.palette.text.secondary,
[theme.breakpoints.down("md")]: {
gap: 16,
flexWrap: "wrap",
},
}),
agentDataValue: (theme) => ({
color: theme.palette.text.primary,
}),
noShrink: {
flexShrink: 0,
},
agentDataItem: (theme) => ({
[theme.breakpoints.down("md")]: {
flexDirection: "column",
alignItems: "flex-start",
gap: 8,
width: "fit-content",
},
}),
} satisfies Record<string, Interpolation<Theme>>;
+23 -73
View File
@@ -1,4 +1,3 @@
import type { Interpolation, Theme } from "@emotion/react";
import { Children, type FC, type JSX, useState } from "react";
import type { WorkspaceAgent, WorkspaceResource } from "#/api/typesGenerated";
import { ChevronDownIcon } from "#/components/AnimatedIcons/ChevronDown";
@@ -14,66 +13,6 @@ import {
import { ResourceAvatar } from "./ResourceAvatar";
import { SensitiveValue } from "./SensitiveValue";
const styles = {
resourceCard: (theme) => ({
border: `1px solid ${theme.palette.divider}`,
background: theme.palette.background.default,
"&:not(:last-child)": {
borderBottom: 0,
},
"&:first-of-type": {
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
},
"&:last-child": {
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
},
}),
resourceCardProfile: {
flexShrink: 0,
width: "fit-content",
minWidth: 220,
},
resourceCardHeader: (theme) => ({
padding: "24px 32px",
borderBottom: `1px solid ${theme.palette.divider}`,
"&:last-child": {
borderBottom: 0,
},
[theme.breakpoints.down("md")]: {
width: "100%",
overflow: "scroll",
},
}),
metadata: () => ({
lineHeight: "1.5",
fontSize: 14,
}),
metadataLabel: (theme) => ({
fontSize: 12,
color: theme.palette.text.secondary,
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
}),
metadataValue: () => ({
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
}),
} satisfies Record<string, Interpolation<Theme>>;
interface ResourceCardProps {
resource: WorkspaceResource;
agentRow: (agent: WorkspaceAgent) => JSX.Element;
@@ -96,20 +35,29 @@ export const ResourceCard: FC<ResourceCardProps> = ({ resource, agentRow }) => {
const gridWidth = mLength === 1 ? 1 : 4;
return (
<div key={resource.id} css={styles.resourceCard} className="resource-card">
<div
key={resource.id}
className="resource-card border border-solid border-border bg-surface-primary [&:not(:last-child)]:border-b-0 first-of-type:rounded-t-[8px] last:rounded-b-[8px]"
>
<Stack
direction="row"
alignItems="flex-start"
css={styles.resourceCardHeader}
className="py-6 px-8 border-0 border-b border-solid border-border last:border-b-0 max-[900px]:w-full max-[900px]:overflow-scroll"
spacing={10}
>
<Stack direction="row" spacing={1} css={styles.resourceCardProfile}>
<Stack
direction="row"
spacing={1}
className="shrink-0 w-fit min-w-[220px]"
>
<div>
<ResourceAvatar resource={resource} />
</div>
<div css={styles.metadata}>
<div css={styles.metadataLabel}>{resource.type}</div>
<div css={styles.metadataValue}>{resource.name}</div>
<div className="text-[14px] leading-normal">
<div className="text-[12px] text-content-secondary truncate">
{resource.type}
</div>
<div className="truncate">{resource.name}</div>
</div>
</Stack>
@@ -120,18 +68,20 @@ export const ResourceCard: FC<ResourceCardProps> = ({ resource, agentRow }) => {
}}
>
{resource.daily_cost > 0 && (
<div css={styles.metadata}>
<div css={styles.metadataLabel}>
<div className="text-[14px] leading-normal">
<div className="text-[12px] text-content-secondary truncate">
<b>Daily cost</b>
</div>
<div css={styles.metadataValue}>{resource.daily_cost}</div>
<div className="truncate">{resource.daily_cost}</div>
</div>
)}
{visibleMetadata.map((meta) => {
return (
<div css={styles.metadata} key={meta.key}>
<div css={styles.metadataLabel}>{meta.key}</div>
<div css={styles.metadataValue}>
<div className="text-[14px] leading-normal" key={meta.key}>
<div className="text-[12px] text-content-secondary truncate">
{meta.key}
</div>
<div className="truncate">
{meta.sensitive ? (
<SensitiveValue value={meta.value} />
) : (
@@ -1,4 +1,3 @@
import type { Interpolation, Theme } from "@emotion/react";
import type { FC } from "react";
import { MemoizedMarkdown } from "#/components/Markdown/Markdown";
@@ -10,35 +9,15 @@ export const TemplateUpdateMessage: FC<TemplateUpdateMessageProps> = ({
children,
}) => {
return (
<MemoizedMarkdown css={styles.versionMessage}>{children}</MemoizedMarkdown>
<MemoizedMarkdown
className={[
"text-[14px] leading-[1.2]",
"[&_:is(h1,h2,h3,h4,h5,h6)]:m-0 [&_:is(h1,h2,h3,h4,h5,h6)]:mb-[0.75em]",
"[&_h1]:text-[1.2em] [&_h2]:text-[1.15em] [&_h3]:text-[1.1em]",
"[&_h4]:text-[1.05em] [&_h5]:text-[1em] [&_h6]:text-[0.95em]",
].join(" ")}
>
{children}
</MemoizedMarkdown>
);
};
const styles = {
versionMessage: {
fontSize: 14,
lineHeight: 1.2,
"& h1, & h2, & h3, & h4, & h5, & h6": {
margin: "0 0 0.75em",
},
"& h1": {
fontSize: "1.2em",
},
"& h2": {
fontSize: "1.15em",
},
"& h3": {
fontSize: "1.1em",
},
"& h4": {
fontSize: "1.05em",
},
"& h5": {
fontSize: "1em",
},
"& h6": {
fontSize: "0.95em",
},
},
} satisfies Record<string, Interpolation<Theme>>;
@@ -1,4 +1,3 @@
import type { Interpolation, Theme } from "@emotion/react";
import type { FC, HTMLProps } from "react";
import { cn } from "#/utils/cn";
@@ -48,7 +47,16 @@ export const YAxisHeader: FC<HTMLProps<HTMLSpanElement>> = (props) => {
};
export const YAxisLabels: FC<HTMLProps<HTMLUListElement>> = (props) => {
return <ul css={styles.labels} {...props} />;
return (
<ul
{...props}
className={cn(
"m-0 list-none flex flex-col text-right",
"gap-[var(--x-axis-rows-gap)] p-[var(--section-padding)]",
props.className,
)}
/>
);
};
type YAxisLabelProps = Omit<HTMLProps<HTMLLIElement>, "id"> & {
@@ -57,32 +65,16 @@ type YAxisLabelProps = Omit<HTMLProps<HTMLLIElement>, "id"> & {
export const YAxisLabel: FC<YAxisLabelProps> = ({ id, ...props }) => {
return (
<li {...props} css={styles.label} id={encodeURIComponent(id)}>
<li
{...props}
className={cn(
"flex items-center",
"[&>*]:block [&>*]:w-full [&>*]:truncate",
props.className,
)}
id={encodeURIComponent(id)}
>
<span>{props.children}</span>
</li>
);
};
const styles = {
labels: {
margin: 0,
listStyle: "none",
display: "flex",
flexDirection: "column",
gap: "var(--x-axis-rows-gap)",
textAlign: "right",
padding: "var(--section-padding)",
},
label: {
display: "flex",
alignItems: "center",
"& > *": {
display: "block",
width: "100%",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
},
},
} satisfies Record<string, Interpolation<Theme>>;
@@ -1,8 +1,5 @@
import type { Interpolation, Theme } from "@emotion/react";
import type { FC } from "react";
import type { AuditDiff } from "#/api/typesGenerated";
import { MONOSPACE_FONT_FAMILY } from "#/theme/constants";
import colors from "#/theme/tailwindColors";
const getDiffValue = (value: unknown): string => {
if (typeof value === "string") {
@@ -50,29 +47,33 @@ export const AuditLogDiff: FC<AuditLogDiffProps> = ({ diff }) => {
const diffEntries = Object.entries(diff);
return (
<div css={styles.diff}>
<div css={[styles.diffColumn, styles.diffOld]}>
<div className="flex items-start text-sm border-t border-border font-mono relative z-[2]">
<div className="flex-1 pt-4 pb-5 pr-4 leading-[160%] self-stretch [overflow-wrap:anywhere] bg-red-950 text-red-50">
{diffEntries.map(([attrName, valueDiff], index) => (
<div key={attrName} css={styles.diffRow}>
<div css={styles.diffLine}>{index + 1}</div>
<div css={styles.diffIcon}>-</div>
<div key={attrName} className="flex items-baseline">
<div className="opacity-50 w-12 text-right shrink-0">
{index + 1}
</div>
<div className="w-8 text-center text-base shrink-0">-</div>
<div>
{attrName}:{" "}
<span css={[styles.diffValue, styles.diffValueOld]}>
<span className="p-px rounded bg-red-800">
{valueDiff.secret ? "••••••••" : getDiffValue(valueDiff.old)}
</span>
</div>
</div>
))}
</div>
<div css={[styles.diffColumn, styles.diffNew]}>
<div className="flex-1 pt-4 pb-5 pr-4 leading-[160%] self-stretch [overflow-wrap:anywhere] bg-green-950 text-green-50">
{diffEntries.map(([attrName, valueDiff], index) => (
<div key={attrName} css={styles.diffRow}>
<div css={styles.diffLine}>{index + 1}</div>
<div css={styles.diffIcon}>+</div>
<div key={attrName} className="flex items-baseline">
<div className="opacity-50 w-12 text-right shrink-0">
{index + 1}
</div>
<div className="w-8 text-center text-base shrink-0">+</div>
<div>
{attrName}:{" "}
<span css={[styles.diffValue, styles.diffValueNew]}>
<span className="p-px rounded bg-green-800">
{valueDiff.secret ? "••••••••" : getDiffValue(valueDiff.new)}
</span>
</div>
@@ -82,67 +83,3 @@ export const AuditLogDiff: FC<AuditLogDiffProps> = ({ diff }) => {
</div>
);
};
const styles = {
diff: (theme) => ({
display: "flex",
alignItems: "flex-start",
fontSize: theme.typography.body2.fontSize,
borderTop: `1px solid ${theme.palette.divider}`,
fontFamily: MONOSPACE_FONT_FAMILY,
position: "relative",
zIndex: 2,
}),
diffColumn: {
flex: 1,
paddingTop: 16,
paddingBottom: 20,
paddingRight: 16,
lineHeight: "160%",
alignSelf: "stretch",
overflowWrap: "anywhere",
},
diffOld: {
backgroundColor: colors.red[950],
color: colors.red[50],
},
diffRow: {
display: "flex",
alignItems: "baseline",
},
diffLine: {
opacity: 0.5,
width: 48,
textAlign: "right",
flexShrink: 0,
},
diffIcon: (theme) => ({
width: 32,
textAlign: "center",
fontSize: theme.typography.body1.fontSize,
flexShrink: 0,
}),
diffNew: {
backgroundColor: colors.green[950],
color: colors.green[50],
},
diffValue: {
padding: 1,
borderRadius: 4,
},
diffValueOld: {
backgroundColor: colors.red[800],
},
diffValueNew: {
backgroundColor: colors.green[800],
},
} satisfies Record<string, Interpolation<Theme>>;
@@ -1,4 +1,3 @@
import type { Interpolation, Theme } from "@emotion/react";
import type { FC } from "react";
import { Link as RouterLink } from "react-router";
import { CodeExample } from "#/components/CodeExample/CodeExample";
@@ -10,10 +9,10 @@ type CliInstallPageViewProps = {
export const CliInstallPageView: FC<CliInstallPageViewProps> = ({ origin }) => {
return (
<div css={styles.container}>
<div className="flex-1 h-screen flex flex-col justify-center items-center w-[480px] mx-auto">
<Welcome>Install the Coder CLI</Welcome>
<p css={styles.instructions}>
<p className="text-[16px] text-content-secondary pb-2 text-center leading-[1.4]">
Copy the command below and{" "}
<strong className="block">paste it in your terminal.</strong>
</p>
@@ -25,56 +24,16 @@ export const CliInstallPageView: FC<CliInstallPageViewProps> = ({ origin }) => {
/>
<div className="pt-4">
<RouterLink to="/workspaces" css={styles.backLink}>
<RouterLink
to="/workspaces"
className="block text-center text-content-primary underline underline-offset-[3px] decoration-[hsla(0,0%,100%,0.7)] pt-4 pb-4 hover:no-underline"
>
Go to workspaces
</RouterLink>
</div>
<div css={styles.copyright}>
<div className="text-[12px] text-content-secondary mt-6">
&copy; {new Date().getFullYear()} Coder Technologies, Inc.
</div>
</div>
);
};
const styles = {
container: {
flex: 1,
// Fallback to 100vh
height: ["100vh", "-webkit-fill-available"],
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
width: 480,
margin: "auto",
},
instructions: (theme) => ({
fontSize: 16,
color: theme.palette.text.secondary,
paddingBottom: 8,
textAlign: "center",
lineHeight: 1.4,
}),
backLink: (theme) => ({
display: "block",
textAlign: "center",
color: theme.palette.text.primary,
textDecoration: "underline",
textUnderlineOffset: 3,
textDecorationColor: "hsla(0deg, 0%, 100%, 0.7)",
paddingTop: 16,
paddingBottom: 16,
"&:hover": {
textDecoration: "none",
},
}),
copyright: (theme) => ({
fontSize: 12,
color: theme.palette.text.secondary,
marginTop: 24,
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -1,9 +1,9 @@
import type { Interpolation, Theme } from "@emotion/react";
import type { FC } from "react";
import { Link, useSearchParams } from "react-router";
import type { TemplateExample } from "#/api/typesGenerated";
import { Stack } from "#/components/Stack/Stack";
import { TemplateExampleCard } from "#/modules/templates/TemplateExampleCard/TemplateExampleCard";
import { cn } from "#/utils/cn";
import type { StarterTemplatesByTag } from "#/utils/starterTemplates";
const getTagLabel = (tag: string) => {
@@ -67,13 +67,20 @@ export const StarterTemplates: FC<StarterTemplatesProps> = ({
<Stack direction="row" spacing={4} alignItems="flex-start">
{starterTemplatesByTag && tags && (
<Stack className="w-[202px] shrink-0 sticky">
<h2 css={styles.sectionTitle}>Choose a starter template</h2>
<span css={styles.filterCaption}>Filter</span>
<h2 className="text-content-primary text-base font-normal m-0">
Choose a starter template
</h2>
<span className="uppercase font-semibold text-xs text-content-secondary tracking-[0.1em]">
Filter
</span>
{tags.map((tag) => (
<Link
key={tag}
to={`?tag=${tag}`}
css={[styles.tagLink, tag === activeTag && styles.tagLinkActive]}
className={cn(
"text-content-secondary no-underline text-sm capitalize hover:text-content-primary",
tag === activeTag && "text-content-primary font-semibold",
)}
>
{getTagLabel(tag)} ({starterTemplatesByTag[tag].length})
</Link>
@@ -84,9 +91,7 @@ export const StarterTemplates: FC<StarterTemplatesProps> = ({
<div className="flex flex-wrap gap-8 h-max">
{visibleTemplates?.map((example) => (
<TemplateExampleCard
css={(theme) => ({
backgroundColor: theme.palette.background.paper,
})}
className="bg-surface-secondary"
example={example}
key={example.id}
activeTag={activeTag}
@@ -96,36 +101,3 @@ export const StarterTemplates: FC<StarterTemplatesProps> = ({
</Stack>
);
};
const styles = {
filterCaption: (theme) => ({
textTransform: "uppercase",
fontWeight: 600,
fontSize: 12,
color: theme.palette.text.secondary,
letterSpacing: "0.1em",
}),
tagLink: (theme) => ({
color: theme.palette.text.secondary,
textDecoration: "none",
fontSize: 14,
textTransform: "capitalize",
"&:hover": {
color: theme.palette.text.primary,
},
}),
tagLinkActive: (theme) => ({
color: theme.palette.text.primary,
fontWeight: 600,
}),
sectionTitle: (theme) => ({
color: theme.palette.text.primary,
fontSize: 16,
fontWeight: 400,
margin: 0,
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -1,4 +1,3 @@
import { css } from "@emotion/react";
import type { FC } from "react";
import type {
DeploymentValues,
@@ -60,19 +59,7 @@ export const ExternalAuthSettingsPageView: FC<
</Alert>
</div>
<Table
css={css`
& td {
padding-top: 24px;
padding-bottom: 24px;
}
& td:last-child,
& th:last-child {
padding-left: 32px;
}
`}
>
<Table className="[&_td]:py-6 [&_td:last-child]:pl-8 [&_th:last-child]:pl-8">
<TableHeader>
<TableRow>
<TableHead className="w-1/3">ID</TableHead>
@@ -1,4 +1,3 @@
import { useTheme } from "@emotion/react";
import type { FC } from "react";
import { useMutation } from "react-query";
import { toast } from "sonner";
@@ -23,18 +22,10 @@ export const Troubleshooting: FC<TroubleshootingProps> = ({
}),
});
const theme = useTheme();
return (
<>
<div
css={{
fontSize: 14,
color: theme.palette.text.secondary,
lineHeight: "160%",
marginBottom: 16,
}}
>
Send a test notification to troubleshoot your notification settings.
<div className="text-[14px] text-content-secondary leading-[160%] mb-4">
Send a test notification to troubleshoot your notification settings.{" "}
</div>
<div>
<span>
@@ -1,8 +1,7 @@
import { css, type Interpolation, type Theme, useTheme } from "@emotion/react";
import { WrenchIcon } from "lucide-react";
import type { FC, HTMLAttributes, PropsWithChildren } from "react";
import { DisabledBadge, EnabledBadge } from "#/components/Badges/Badges";
import { MONOSPACE_FONT_FAMILY } from "#/theme/constants";
import { cn } from "#/utils/cn";
export const OptionName: FC<PropsWithChildren> = ({ children }) => {
return (
@@ -22,7 +21,6 @@ interface OptionValueProps {
export const OptionValue: FC<OptionValueProps> = (props) => {
const { children: value } = props;
const theme = useTheme();
if (typeof value === "boolean") {
return (
@@ -34,7 +32,7 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
if (typeof value === "number") {
return (
<span css={styles.option} className="option-value-number">
<span className="option-value-number font-mono text-[14px] [overflow-wrap:anywhere] select-all [&_ul]:p-4">
{value}
</span>
);
@@ -42,7 +40,7 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
if (!value || value.length === 0) {
return (
<span css={styles.option} className="option-value-empty">
<span className="option-value-empty font-mono text-[14px] [overflow-wrap:anywhere] select-all [&_ul]:p-4">
Not set
</span>
);
@@ -50,7 +48,7 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
if (typeof value === "string") {
return (
<span css={styles.option} className="option-value-string">
<span className="option-value-string font-mono text-[14px] [overflow-wrap:anywhere] select-all [&_ul]:p-4">
{value}
</span>
);
@@ -64,16 +62,12 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
.map(([option, isEnabled]) => (
<li
key={option}
css={[
styles.option,
!isEnabled && {
marginLeft: 32,
color: theme.palette.text.disabled,
},
]}
className={`option-array-item-${option} ${
isEnabled ? "option-enabled" : "option-disabled"
}`}
className={cn(
"font-mono text-[14px] [overflow-wrap:anywhere] select-all [&_ul]:p-4",
!isEnabled && "ml-8 text-content-disabled",
`option-array-item-${option}`,
isEnabled ? "option-enabled" : "option-disabled",
)}
>
<div className="inline-flex items-center">
{isEnabled && <WrenchIcon className="size-4 mx-2" />}
@@ -89,7 +83,10 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
return (
<ul className="option-array list-inside">
{value.map((item) => (
<li key={item} css={styles.option}>
<li
key={item}
className="font-mono text-[14px] [overflow-wrap:anywhere] select-all [&_ul]:p-4"
>
{item}
</li>
))}
@@ -98,7 +95,7 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
}
return (
<span css={styles.option} className="option-value-json">
<span className="option-value-json font-mono text-[14px] [overflow-wrap:anywhere] select-all [&_ul]:p-4">
{JSON.stringify(value)}
</span>
);
@@ -107,66 +104,35 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
type OptionConfigProps = HTMLAttributes<HTMLDivElement> & { isSource: boolean };
// OptionConfig takes a isSource bool to indicate if the Option is the source of the configured value.
export const OptionConfig: FC<OptionConfigProps> = ({ isSource, ...attrs }) => {
export const OptionConfig: FC<OptionConfigProps> = ({
isSource,
className,
...attrs
}) => {
return (
<div
{...attrs}
css={[styles.configOption, isSource && styles.sourceConfigOption]}
className={cn(
"font-mono text-[13px] font-semibold bg-surface-secondary inline-flex items-center rounded p-1.5 leading-none gap-1.5 border border-solid border-border",
isSource &&
"border-border-pending [&_.OptionConfigFlag]:bg-content-link",
className,
)}
/>
);
};
export const OptionConfigFlag: FC<HTMLAttributes<HTMLDivElement>> = (props) => {
const theme = useTheme();
export const OptionConfigFlag: FC<HTMLAttributes<HTMLDivElement>> = ({
className,
...props
}) => {
return (
<div
{...props}
className="OptionConfigFlag"
css={{
fontSize: 10,
fontWeight: 600,
display: "block",
backgroundColor: theme.palette.divider,
lineHeight: 1,
padding: "2px 4px",
borderRadius: 1,
}}
className={cn(
"OptionConfigFlag text-[10px] font-semibold block bg-border leading-none px-1 py-0.5 rounded-[1px]",
className,
)}
/>
);
};
const styles = {
configOption: (theme) => ({
fontSize: 13,
fontFamily: MONOSPACE_FONT_FAMILY,
fontWeight: 600,
backgroundColor: theme.palette.background.paper,
display: "inline-flex",
alignItems: "center",
borderRadius: 4,
padding: 6,
lineHeight: 1,
gap: 6,
border: `1px solid ${theme.palette.divider}`,
}),
sourceConfigOption: (theme) => ({
border: `1px solid ${theme.roles.active.fill.outline}`,
"& .OptionConfigFlag": {
background: theme.roles.active.fill.solid,
},
}),
option: css`
font-size: 14px;
font-family: ${MONOSPACE_FONT_FAMILY};
overflow-wrap: anywhere;
user-select: all;
& ul {
padding: 16px;
}
`,
} satisfies Record<string, Interpolation<Theme>>;
+6 -12
View File
@@ -1,4 +1,3 @@
import type { Interpolation, Theme } from "@emotion/react";
import { EllipsisVertical, UserPlusIcon } from "lucide-react";
import { type FC, useState } from "react";
import { useMutation, useQueryClient } from "react-query";
@@ -35,6 +34,7 @@ import {
TableRow,
} from "#/components/Table/Table";
import { isEveryoneGroup } from "#/modules/groups";
import { cn } from "#/utils/cn";
import type { GroupPageOutletContext } from "./GroupPage";
const GroupMembersPage: FC = () => {
@@ -213,9 +213,12 @@ const GroupMemberRow: FC<GroupMemberRowProps> = ({
</TableCell>
<TableCell
width="40%"
css={[styles.status, member.status === "suspended" && styles.suspended]}
className={cn(
"capitalize",
member.status === "suspended" && "text-content-secondary",
)}
>
<div>{member.status}</div>
<div>{member.status}</div>{" "}
<LastSeen at={member.last_seen_at} className="text-xs" />
</TableCell>
<TableCell width="1%">
@@ -243,13 +246,4 @@ const GroupMemberRow: FC<GroupMemberRowProps> = ({
);
};
const styles = {
status: {
textTransform: "capitalize",
},
suspended: (theme) => ({
color: theme.palette.text.secondary,
}),
} satisfies Record<string, Interpolation<Theme>>;
export default GroupMembersPage;
+24 -66
View File
@@ -1,4 +1,3 @@
import { useTheme } from "@emotion/react";
import {
CircleAlertIcon,
CircleCheckIcon,
@@ -14,8 +13,8 @@ import {
} from "react";
import type { HealthCode, HealthSeverity } from "#/api/typesGenerated";
import { Link } from "#/components/Link/Link";
import { cn } from "#/utils/cn";
import { docs } from "#/utils/docs";
import { healthyColor } from "./healthyColor";
const CONTENT_PADDING = 36;
@@ -44,11 +43,18 @@ interface HealthIconProps {
}
export const HealthIcon: FC<HealthIconProps> = ({ size, severity }) => {
const theme = useTheme();
const color = healthyColor(theme, severity);
const Icon = severity === "error" ? CircleAlertIcon : CircleCheckIcon;
return <Icon css={{ width: size, height: size, color }} />;
return (
<Icon
className={cn(
severity === "ok" && "text-content-success",
severity === "warning" && "text-content-warning",
severity === "error" && "text-content-destructive",
)}
style={{ width: size, height: size }}
/>
);
};
interface HealthyDotProps {
@@ -56,16 +62,14 @@ interface HealthyDotProps {
}
export const HealthyDot: FC<HealthyDotProps> = ({ severity }) => {
const theme = useTheme();
return (
<div
css={{
width: 8,
height: 8,
borderRadius: 9999,
backgroundColor: healthyColor(theme, severity),
}}
className={cn(
"size-2 rounded-full",
severity === "ok" && "bg-content-success",
severity === "warning" && "bg-content-warning",
severity === "error" && "bg-content-destructive",
)}
/>
);
};
@@ -93,30 +97,13 @@ export const GridData: FC<HTMLAttributes<HTMLDivElement>> = (props) => {
};
export const GridDataLabel: FC<HTMLAttributes<HTMLSpanElement>> = (props) => {
const theme = useTheme();
return (
<span
css={{
fontSize: 14,
fontWeight: 500,
color: theme.palette.text.secondary,
}}
{...props}
/>
<span className="text-sm font-medium text-content-secondary" {...props} />
);
};
export const GridDataValue: FC<HTMLAttributes<HTMLSpanElement>> = (props) => {
const theme = useTheme();
return (
<span
css={{
fontSize: 14,
color: theme.palette.text.primary,
}}
{...props}
/>
);
return <span className="text-sm text-content-primary" {...props} />;
};
export const SectionLabel: FC<HTMLAttributes<HTMLHeadingElement>> = (props) => {
@@ -130,22 +117,9 @@ type PillProps = React.ComponentPropsWithRef<"div"> & {
};
export const Pill: React.FC<PillProps> = ({ icon, children, ...divProps }) => {
const theme = useTheme();
return (
<div
css={{
display: "inline-flex",
alignItems: "center",
height: 32,
borderRadius: 9999,
border: `1px solid ${theme.palette.divider}`,
fontSize: 12,
fontWeight: 500,
padding: 8,
gap: 8,
cursor: "default",
}}
className="inline-flex items-center h-8 rounded-full border border-border text-xs font-medium p-2 gap-2 cursor-default"
{...divProps}
>
{cloneElement(icon, { className: "size-[14px]" })}
@@ -178,16 +152,13 @@ export const BooleanPill: FC<BooleanPillProps> = ({
children,
...divProps
}) => {
const theme = useTheme();
const color = value ? theme.roles.success.outline : theme.roles.error.outline;
return (
<Pill
icon={
value ? (
<CircleCheckIcon css={{ color }} className="size-icon-sm" />
<CircleCheckIcon className="size-icon-sm text-content-success" />
) : (
<CircleMinusIcon css={{ color }} className="size-icon-sm" />
<CircleMinusIcon className="size-icon-sm text-content-destructive" />
)
}
{...divProps}
@@ -200,20 +171,9 @@ export const BooleanPill: FC<BooleanPillProps> = ({
type LogsProps = HTMLAttributes<HTMLDivElement> & { lines: readonly string[] };
export const Logs: FC<LogsProps> = ({ lines, ...divProps }) => {
const theme = useTheme();
return (
<div
css={{
fontFamily: "monospace",
fontSize: 13,
lineHeight: "160%",
padding: 24,
backgroundColor: theme.palette.background.paper,
overflowX: "auto",
whiteSpace: "pre-wrap",
wordBreak: "break-all",
}}
className="font-mono text-[13px] leading-[160%] p-6 bg-surface-secondary overflow-x-auto whitespace-pre-wrap break-all"
{...divProps}
>
{lines.map((line, index) => (
@@ -222,9 +182,7 @@ export const Logs: FC<LogsProps> = ({ lines, ...divProps }) => {
</span>
))}
{lines.length === 0 && (
<span css={{ color: theme.palette.text.secondary }}>
No logs available
</span>
<span className="text-content-secondary">No logs available</span>
)}
</div>
);
+2 -16
View File
@@ -1,4 +1,3 @@
import { useTheme } from "@emotion/react";
import { CodeIcon } from "lucide-react";
import { useOutletContext } from "react-router";
import type { HealthcheckReport } from "#/api/typesGenerated";
@@ -8,7 +7,6 @@ import {
TooltipContent,
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import { MONOSPACE_FONT_FAMILY } from "#/theme/constants";
import { pageTitle } from "#/utils/page";
import {
Header,
@@ -23,7 +21,6 @@ import { DismissWarningButton } from "./DismissWarningButton";
const WebsocketPage = () => {
const healthStatus = useOutletContext<HealthcheckReport>();
const { websocket } = healthStatus;
const theme = useTheme();
return (
<>
@@ -65,22 +62,11 @@ const WebsocketPage = () => {
<section>
<SectionLabel>Body</SectionLabel>
<div
css={{
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8,
fontSize: 14,
padding: 24,
fontFamily: MONOSPACE_FONT_FAMILY,
}}
>
<div className="bg-surface-secondary border border-border rounded-lg text-sm p-6 font-mono">
{websocket.body !== "" ? (
websocket.body
) : (
<span css={{ color: theme.palette.text.secondary }}>
No body message
</span>
<span className="text-content-secondary">No body message</span>
)}
</div>
</section>
@@ -1,4 +1,3 @@
import { useTheme } from "@emotion/react";
import { GlobeIcon, HashIcon } from "lucide-react";
import type { FC } from "react";
import { useOutletContext } from "react-router";
@@ -9,6 +8,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import { cn } from "#/utils/cn";
import { createDayString } from "#/utils/createDayString";
import { pageTitle } from "#/utils/page";
import {
@@ -26,7 +26,6 @@ const WorkspaceProxyPage: FC = () => {
const healthStatus = useOutletContext<HealthcheckReport>();
const { workspace_proxy } = healthStatus;
const { regions } = workspace_proxy.workspace_proxies;
const theme = useTheme();
return (
<>
@@ -66,15 +65,10 @@ const WorkspaceProxyPage: FC = () => {
return (
<div
key={region.id}
css={{
borderRadius: 8,
border: `1px solid ${
region.healthy
? theme.palette.divider
: theme.palette.warning.light
}`,
fontSize: 14,
}}
className={cn(
"rounded-lg border text-sm",
region.healthy ? "border-border" : "border-border-warning",
)}
>
<header className="p-6 flex items-center justify-between gap-6">
<div className="flex items-center gap-6">
@@ -87,7 +81,7 @@ const WorkspaceProxyPage: FC = () => {
</div>
<div className="leading-[160%]">
<h4 className="font-medium m-0">{region.display_name}</h4>
<span css={{ color: theme.palette.text.secondary }}>
<span className="text-content-secondary">
{region.version}
</span>
</div>
@@ -132,17 +126,7 @@ const WorkspaceProxyPage: FC = () => {
</div>
</header>
<div
css={{
borderTop: `1px solid ${theme.palette.divider}`,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "8px 24px",
fontSize: 12,
color: theme.palette.text.secondary,
}}
>
<div className="border-t border-border flex items-center justify-between py-2 px-6 text-xs text-content-secondary">
{region.status?.status === "unregistered" ? (
<span>Has not connected yet</span>
) : warnings.length === 0 && errors.length === 0 ? (
@@ -1,4 +1,3 @@
import type { Interpolation, Theme } from "@emotion/react";
import type { FC } from "react";
import type { ApiErrorResponse } from "#/api/errors";
import type { ExternalAuthDevice } from "#/api/typesGenerated";
@@ -36,7 +35,7 @@ const LoginOAuthDevicePageView: FC<LoginOAuthDevicePageViewProps> = ({
<SignInLayout>
<Welcome>You&apos;ve authenticated with GitHub!</Welcome>
<p css={styles.text}>
<p className="text-base text-content-secondary text-center leading-[160%] m-0">
If you&apos;re not redirected automatically,{" "}
<a href={redirectUrl}>click here</a>.
</p>
@@ -45,13 +44,3 @@ const LoginOAuthDevicePageView: FC<LoginOAuthDevicePageViewProps> = ({
};
export default LoginOAuthDevicePageView;
const styles = {
text: (theme) => ({
fontSize: 16,
color: theme.palette.text.secondary,
textAlign: "center",
lineHeight: "160%",
margin: 0,
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -1,9 +1,16 @@
import type { Interpolation, Theme } from "@emotion/react";
import type { FC, HTMLAttributes, ReactNode } from "react";
import { cn } from "#/utils/cn";
export const HorizontalContainer: FC<HTMLAttributes<HTMLDivElement>> = ({
className,
...attrs
}) => {
return <div css={styles.horizontalContainer} {...attrs} />;
return (
<div
className={cn("flex flex-col gap-16 md:gap-20", className)}
{...attrs}
/>
);
};
interface HorizontalSectionProps
@@ -17,71 +24,27 @@ export const HorizontalSection: FC<HorizontalSectionProps> = ({
children,
title,
description,
className,
...attrs
}) => {
return (
<section css={styles.formSection} {...attrs}>
<div css={styles.formSectionInfo}>
<h2 css={styles.formSectionInfoTitle}>{title}</h2>
<div css={styles.formSectionInfoDescription}>{description}</div>
<section
className={cn(
"flex flex-col gap-4 lg:flex-row lg:gap-[120px]",
className,
)}
{...attrs}
>
<div className="w-full shrink-0 top-6 max-w-[312px] md:sticky">
<h2 className="text-[20px] text-content-primary font-normal m-0 mb-2 flex flex-row items-center gap-3">
{title}
</h2>
<div className="text-[14px] text-content-secondary leading-[160%] m-0">
{description}
</div>
</div>
{children}
</section>
);
};
const styles = {
horizontalContainer: (theme) => ({
display: "flex",
flexDirection: "column",
gap: 80,
[theme.breakpoints.down("md")]: {
gap: 64,
},
}),
formSection: (theme) => ({
display: "flex",
flexDirection: "row",
gap: 120,
[theme.breakpoints.down("lg")]: {
flexDirection: "column",
gap: 16,
},
}),
formSectionInfo: (theme) => ({
width: "100%",
flexShrink: 0,
top: 24,
maxWidth: 312,
position: "sticky",
[theme.breakpoints.down("md")]: {
width: "100%",
position: "initial",
},
}),
formSectionInfoTitle: (theme) => ({
fontSize: 20,
color: theme.palette.text.primary,
fontWeight: 400,
margin: 0,
marginBottom: 8,
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 12,
}),
formSectionInfoDescription: (theme) => ({
fontSize: 14,
color: theme.palette.text.secondary,
lineHeight: "160%",
margin: 0,
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -13,7 +13,6 @@
* went with a simpler design. If we decide we really do need to display the
* users like that, though, know that it will be painful
*/
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import type { FC } from "react";
import type { LoginType, SlimRole } from "#/api/typesGenerated";
import { Pill } from "#/components/Pill/Pill";
@@ -78,12 +77,12 @@ export const UserRoleCell: FC<UserRoleCellProps> = ({
)}
<Pill
css={
className={
hasOwnerRole
? styles.ownerRoleBadge
? "bg-surface-sky border-border-pending"
: mainDisplayRole.global
? styles.globalRoleBadge
: styles.roleBadge
? "bg-surface-sky border-border-pending"
: "bg-surface-tertiary border-border"
}
>
{mainDisplayRole.global ? (
@@ -111,18 +110,11 @@ type OverflowRolePillProps = {
};
const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
const theme = useTheme();
return (
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<Pill
css={{
backgroundColor: theme.palette.background.paper,
borderColor: theme.palette.divider,
}}
>
<Pill className="bg-surface-secondary border-border">
+{roles.length} more
</Pill>
</TooltipTrigger>
@@ -131,7 +123,11 @@ const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
{roles.map((role) => (
<Pill
key={role.name}
css={role.global ? styles.globalRoleBadge : styles.roleBadge}
className={
role.global
? "bg-surface-sky border-border-pending"
: "bg-surface-tertiary border-border"
}
>
{role.global ? (
<span title="This user has this role for all organizations.">
@@ -148,21 +144,6 @@ const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
);
};
const styles = {
globalRoleBadge: (theme) => ({
backgroundColor: theme.roles.active.background,
borderColor: theme.roles.active.outline,
}),
ownerRoleBadge: (theme) => ({
backgroundColor: theme.roles.notice.background,
borderColor: theme.roles.notice.outline,
}),
roleBadge: (theme) => ({
backgroundColor: theme.experimental.l2.background,
borderColor: theme.experimental.l2.outline,
}),
} satisfies Record<string, Interpolation<Theme>>;
const fallbackRole: TieredSlimRole = {
name: "member",
display_name: "Member",
@@ -1,4 +1,3 @@
import { useTheme } from "@emotion/react";
import { ExternalLinkIcon, PlusIcon } from "lucide-react";
import type { FC } from "react";
import { Link } from "react-router";
@@ -25,8 +24,6 @@ export const StarterTemplatePageView: FC<StarterTemplatePageViewProps> = ({
starterTemplate,
error,
}) => {
const theme = useTheme();
if (error) {
return (
<Margins>
@@ -73,14 +70,11 @@ export const StarterTemplatePageView: FC<StarterTemplatePageViewProps> = ({
</PageHeader>
<div
css={{
background: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8,
}}
className="bg-surface-secondary border border-solid border-border rounded-lg"
id="readme"
>
<div className="px-10 pt-10 pb-16 max-w-[800px] mx-auto">
{" "}
<MemoizedMarkdown>{starterTemplate.markdown}</MemoizedMarkdown>
</div>
</div>
@@ -1,4 +1,3 @@
import type { CSSObject, Interpolation, Theme } from "@emotion/react";
import type { FC } from "react";
import { useNavigate } from "react-router";
import type { TemplateVersion } from "#/api/typesGenerated";
@@ -40,11 +39,11 @@ export const VersionRow: FC<VersionRowProps> = ({
{...clickableProps}
className={clickableProps.className}
>
<TableCell css={styles.versionCell}>
<TableCell className="!p-0 relative border-b-0">
<Stack
direction="row"
alignItems="center"
css={styles.versionWrapper}
className="px-8 py-4"
justifyContent="space-between"
>
<Stack direction="row" alignItems="center">
@@ -53,7 +52,7 @@ export const VersionRow: FC<VersionRowProps> = ({
src={version.created_by.avatar_url}
/>
<Stack
css={styles.versionSummary}
className="text-base [font-family:inherit]"
direction="row"
alignItems="center"
spacing={1}
@@ -67,7 +66,7 @@ export const VersionRow: FC<VersionRowProps> = ({
<InfoTooltip title="Message" message={version.message} />
)}
<span css={styles.versionTime}>
<span className="text-content-secondary text-[12px]">
{new Date(version.created_at).toLocaleTimeString()}
</span>
</Stack>
@@ -138,25 +137,3 @@ export const VersionRow: FC<VersionRowProps> = ({
</TimelineEntry>
);
};
const styles = {
versionWrapper: {
padding: "16px 32px",
},
versionCell: {
padding: "0 !important",
position: "relative",
borderBottom: 0,
},
versionSummary: (theme) => ({
...(theme.typography.body1 as CSSObject),
fontFamily: "inherit",
}),
versionTime: (theme) => ({
color: theme.palette.text.secondary,
fontSize: 12,
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -1,4 +1,3 @@
import type { Interpolation, Theme } from "@emotion/react";
import { ArrowRightIcon, PlusIcon } from "lucide-react";
import type { FC } from "react";
import { Link as RouterLink, useNavigate } from "react-router";
@@ -42,6 +41,7 @@ import {
import { useClickableTableRow } from "#/hooks/useClickableTableRow";
import { linkToTemplate, useLinks } from "#/modules/navigation";
import type { WorkspacePermissions } from "#/modules/permissions/workspaces";
import { cn } from "#/utils/cn";
import { createDayString } from "#/utils/createDayString";
import { docs } from "#/utils/docs";
import {
@@ -143,7 +143,10 @@ const TemplateRow: FC<TemplateRowProps> = ({
key={template.id}
data-testid={`template-${template.id}`}
{...clickableRow}
css={styles.tableRow}
className={cn(
clickableRow.className,
"[&:hover_.actionButton]:text-content-primary [&:hover_.actionButton]:border-border-hover",
)}
>
<TableCell>
<AvatarData
@@ -160,7 +163,7 @@ const TemplateRow: FC<TemplateRowProps> = ({
/>
</TableCell>
<TableCell css={styles.secondary}>
<TableCell className="text-content-secondary">
{showOrganizations ? (
<AvatarData
title={template.organization_display_name}
@@ -172,15 +175,15 @@ const TemplateRow: FC<TemplateRowProps> = ({
)}
</TableCell>
<TableCell css={styles.secondary}>
<TableCell className="text-content-secondary">
{formatTemplateBuildTime(template.build_time_stats.start.P50)}
</TableCell>
<TableCell data-chromatic="ignore" css={styles.secondary}>
<TableCell data-chromatic="ignore" className="text-content-secondary">
{createDayString(template.updated_at)}
</TableCell>
<TableCell css={styles.actionCell}>
<TableCell className="whitespace-nowrap">
<TemplateActions
template={template}
workspacePermissions={workspacePermissions}
@@ -308,41 +311,3 @@ const TableLoader: FC = () => {
</TableLoaderSkeleton>
);
};
const styles = {
templateIconWrapper: {
// Same size then the avatar component
width: 36,
height: 36,
padding: 2,
"& img": {
width: "100%",
},
},
actionCell: {
whiteSpace: "nowrap",
},
cellPrimaryLine: (theme) => ({
color: theme.palette.text.primary,
fontWeight: 600,
}),
cellSecondaryLine: (theme) => ({
fontSize: 13,
color: theme.palette.text.secondary,
lineHeight: "150%",
}),
secondary: (theme) => ({
color: theme.palette.text.secondary,
}),
tableRow: (theme) => ({
"&:hover .actionButton": {
color: theme.experimental.l2.hover.text,
borderColor: theme.experimental.l2.hover.outline,
},
}),
actionButton: (theme) => ({
transition: "none",
color: theme.palette.text.primary,
}),
} satisfies Record<string, Interpolation<Theme>>;
+2 -10
View File
@@ -160,16 +160,8 @@ const TerminalPage: FC = () => {
</div>
{latency && isDebugging && (
<span
css={{
position: "absolute",
bottom: 24,
right: 24,
color: theme.palette.text.disabled,
fontSize: 14,
}}
>
Latency: {latency.latencyMS.toFixed(0)}ms
<span className="absolute bottom-6 right-6 text-content-disabled text-[14px]">
Latency: {latency.latencyMS.toFixed(0)}ms{" "}
</span>
)}
</ThemeOverride>
@@ -1,4 +1,3 @@
import { useTheme } from "@emotion/react";
import { EllipsisVertical, RefreshCcwIcon } from "lucide-react";
import { type FC, useCallback, useEffect, useState } from "react";
import { useQuery } from "react-query";
@@ -114,7 +113,6 @@ const ExternalAuthRow: FC<ExternalAuthRowProps> = ({
onUnlinkExternalAuth,
onValidateExternalAuth,
}) => {
const theme = useTheme();
const name = app.display_name || app.id || app.type;
const authURL = `/external-auth/${app.id}`;
@@ -152,11 +150,7 @@ const ExternalAuthRow: FC<ExternalAuthRowProps> = ({
{link?.validate_error && (
<span>
<span
css={{ paddingLeft: "1em", color: theme.palette.error.light }}
>
Error:{" "}
</span>
<span className="pl-[1em] text-content-destructive">Error: </span>
{link?.validate_error}
</span>
)}
+7 -20
View File
@@ -1,4 +1,3 @@
import type { Interpolation, Theme } from "@emotion/react";
import type { FC, ReactNode } from "react";
import {
FeatureStageBadge,
@@ -36,7 +35,7 @@ export const Section: FC<SectionProps> = ({
<section className={className} id={id} data-testid={id}>
<div className={layout === "fluid" ? "max-w-full" : "max-w-[500px]"}>
{(title || description) && (
<div css={styles.header}>
<div className="mb-6 flex flex-row justify-between">
<div>
{title && (
<Stack direction="row" alignItems="center">
@@ -51,10 +50,14 @@ export const Section: FC<SectionProps> = ({
</Stack>
)}
{description && typeof description === "string" && (
<p css={styles.description}>{description}</p>
<p className="text-content-secondary text-base m-0 mt-1 leading-[140%]">
{description}
</p>
)}
{description && typeof description !== "string" && (
<div css={styles.description}>{description}</div>
<div className="text-content-secondary text-base m-0 mt-1 leading-[140%]">
{description}
</div>
)}
</div>
{toolbar && <div>{toolbar}</div>}
@@ -66,19 +69,3 @@ export const Section: FC<SectionProps> = ({
</section>
);
};
const styles = {
header: {
marginBottom: 24,
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
},
description: (theme) => ({
color: theme.palette.text.secondary,
fontSize: 16,
margin: 0,
marginTop: 4,
lineHeight: "140%",
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -1,4 +1,3 @@
import { css, type Interpolation, type Theme } from "@emotion/react";
import { PlusIcon } from "lucide-react";
import { type FC, useState } from "react";
import { Link as RouterLink } from "react-router";
@@ -34,7 +33,7 @@ const TokensPage: FC = () => {
<>
<Section
title="Tokens"
css={styles.section}
className="[&_code]:bg-border [&_code]:text-[12px] [&_code]:px-1 [&_code]:py-0.5 [&_code]:text-content-primary [&_code]:rounded-[2px]"
description={
<>
Tokens are used to authenticate with the Coder API. You can create a
@@ -75,16 +74,4 @@ const TokenActions: FC = () => (
</Stack>
);
const styles = {
section: (theme) => css`
& code {
background: ${theme.palette.divider};
font-size: 12px;
padding: 2px 4px;
color: ${theme.palette.text.primary};
border-radius: 2px;
}
`,
} satisfies Record<string, Interpolation<Theme>>;
export default TokensPage;
+7 -27
View File
@@ -29,25 +29,12 @@ export const SidebarItem: FC<SidebarItemProps> = ({
}) => {
return (
<button
css={(theme) => ({
background: active ? theme.experimental.l2.background : "none",
border: "none",
fontSize: 14,
width: "100%",
textAlign: "left",
padding: "0 24px",
cursor: "pointer",
pointerEvents: active ? "none" : "auto",
color: active
? theme.palette.text.primary
: theme.palette.text.secondary,
"&:hover": {
background: theme.palette.action.hover,
color: theme.palette.text.primary,
},
paddingTop: 10,
paddingBottom: 10,
})}
className={cn(
"border-none text-sm w-full text-left px-6 py-2.5 cursor-pointer",
active
? "bg-surface-secondary text-content-primary pointer-events-none"
: "bg-transparent text-content-secondary hover:bg-surface-tertiary hover:text-content-primary",
)}
{...attrs}
>
{children}
@@ -61,14 +48,7 @@ export const SidebarCaption: FC<HTMLAttributes<HTMLDivElement>> = ({
}) => {
return (
<div
css={(theme) => ({
fontSize: 10,
textTransform: "uppercase",
fontWeight: 500,
color: theme.palette.text.secondary,
padding: "12px 24px",
letterSpacing: "0.5px",
})}
className="text-2xs uppercase font-medium text-content-secondary py-3 px-6 tracking-[0.5px]"
{...attrs}
>
{children}
@@ -1,4 +1,3 @@
import type { Interpolation, Theme } from "@emotion/react";
import { Children, type FC, type HTMLAttributes } from "react";
import type { WorkspaceResource } from "#/api/typesGenerated";
import { CopyableValue } from "#/components/CopyableValue/CopyableValue";
@@ -28,11 +27,14 @@ export const ResourceMetadata: FC<ResourceMetadataProps> = ({
}
return (
<header css={styles.root} {...headerProps}>
<header
className="p-6 flex flex-wrap gap-12 gap-y-6 mb-6 text-sm"
{...headerProps}
>
{metadata.map((meta) => {
return (
<div css={styles.item} key={meta.key}>
<div css={styles.value}>
<div className="leading-normal" key={meta.key}>
<div className="truncate">
{meta.sensitive ? (
<SensitiveValue value={meta.value} />
) : (
@@ -59,40 +61,12 @@ export const ResourceMetadata: FC<ResourceMetadataProps> = ({
</MemoizedInlineMarkdown>
)}
</div>
<div css={styles.label}>{meta.key}</div>
<div className="text-[13px] text-content-secondary truncate">
{meta.key}
</div>
</div>
);
})}
</header>
);
};
const styles = {
root: () => ({
padding: 24,
display: "flex",
flexWrap: "wrap",
gap: 48,
rowGap: 24,
marginBottom: 24,
fontSize: 14,
}),
item: {
lineHeight: "1.5",
},
label: (theme) => ({
fontSize: 13,
color: theme.palette.text.secondary,
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
}),
value: {
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
},
} satisfies Record<string, Interpolation<Theme>>;
@@ -1,4 +1,3 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import { type FC, type ReactNode, useState } from "react";
import type { AlertProps } from "#/components/Alert/Alert";
import { Button, type ButtonProps } from "#/components/Button/Button";
@@ -10,6 +9,7 @@ import {
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import type { ThemeRole } from "#/theme/roles";
import { cn } from "#/utils/cn";
export type NotificationItem = {
title: string;
@@ -24,22 +24,61 @@ type NotificationsProps = {
icon: ReactNode;
};
// Maps a ThemeRole severity to Tailwind classes for the role's outline
// color. These are the closest semantic matches available in the design
// token system.
const severityStyles: Record<ThemeRole, { svgColor: string; border: string }> =
{
error: {
svgColor: "[&_svg]:text-border-destructive",
border: "border-border-destructive",
},
warning: {
svgColor: "[&_svg]:text-border-warning",
border: "border-border-warning",
},
notice: {
svgColor: "[&_svg]:text-border-pending",
border: "border-border-pending",
},
info: {
svgColor: "[&_svg]:text-content-secondary",
border: "border-border",
},
success: {
svgColor: "[&_svg]:text-border-success",
border: "border-border-success",
},
active: {
svgColor: "[&_svg]:text-border-pending",
border: "border-border-pending",
},
inactive: {
svgColor: "[&_svg]:text-content-disabled",
border: "border-border",
},
danger: {
svgColor: "[&_svg]:text-border-warning",
border: "border-border-warning",
},
preview: {
svgColor: "[&_svg]:text-border-purple",
border: "border-border-purple",
},
};
export const Notifications: FC<NotificationsProps> = ({
items,
severity,
icon,
}) => {
const [isOpen, setIsOpen] = useState(false);
const theme = useTheme();
return (
<TooltipProvider>
<Tooltip open={isOpen} onOpenChange={setIsOpen} delayDuration={0}>
<TooltipTrigger asChild>
<div
css={styles.pillContainer}
data-testid={`${severity}-notifications`}
>
<div className="py-2" data-testid={`${severity}-notifications`}>
<NotificationPill
items={items}
severity={severity}
@@ -51,10 +90,10 @@ export const Notifications: FC<NotificationsProps> = ({
<TooltipContent
align="end"
collisionPadding={16}
className="max-w-[400px] p-0 bg-surface-secondary border-surface-quaternary text-sm text-content-primary"
style={{
borderColor: theme.roles[severity].outline,
}}
className={cn(
"max-w-[400px] p-0 bg-surface-secondary text-sm text-content-primary",
severityStyles[severity].border,
)}
>
{items.map((n) => (
<NotificationItem notification={n} key={n.title} />
@@ -78,10 +117,10 @@ const NotificationPill: FC<NotificationPillProps> = ({
return (
<Pill
icon={icon}
css={(theme) => ({
"& svg": { color: theme.roles[severity].outline },
borderColor: isTooltipOpen ? theme.roles[severity].outline : undefined,
})}
className={cn(
severityStyles[severity].svgColor,
isTooltipOpen && severityStyles[severity].border,
)}
>
{items.length}
</Pill>
@@ -94,10 +133,12 @@ interface NotificationItemProps {
const NotificationItem: FC<NotificationItemProps> = ({ notification }) => {
return (
<article css={styles.notificationItem}>
<article className="p-5 leading-normal border-solid border-0 border-t border-border first-of-type:border-t-0">
<h4 className="m-0 font-medium">{notification.title}</h4>
{notification.detail && (
<p css={styles.notificationDetail}>{notification.detail}</p>
<p className="m-0 text-content-secondary leading-[1.6] block mt-2">
{notification.detail}
</p>
)}
<div className="mt-2 flex items-center gap-1">{notification.actions}</div>
</article>
@@ -107,26 +148,3 @@ const NotificationItem: FC<NotificationItemProps> = ({ notification }) => {
export const NotificationActionButton: FC<ButtonProps> = (props) => {
return <Button variant="default" size="sm" {...props} />;
};
const styles = {
// Adds some spacing from the Tooltip content
pillContainer: {
padding: "8px 0",
},
notificationItem: (theme) => ({
padding: 20,
lineHeight: "1.5",
borderTop: `1px solid ${theme.palette.divider}`,
"&:first-of-type": {
borderTop: 0,
},
}),
notificationDetail: (theme) => ({
margin: 0,
color: theme.palette.text.secondary,
lineHeight: 1.6,
display: "block",
marginTop: 8,
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -1,4 +1,3 @@
import type { Interpolation, Theme } from "@emotion/react";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { InfoIcon, TriangleAlertIcon } from "lucide-react";
@@ -242,7 +241,7 @@ export const WorkspaceNotifications: FC<WorkspaceNotificationsProps> = ({
}
return (
<div css={styles.notificationsGroup}>
<div className="flex items-center gap-3">
{infoNotifications.length > 0 && (
<Notifications
items={infoNotifications}
@@ -262,14 +261,6 @@ export const WorkspaceNotifications: FC<WorkspaceNotificationsProps> = ({
);
};
const styles = {
notificationsGroup: {
display: "flex",
alignItems: "center",
gap: 12,
},
} satisfies Record<string, Interpolation<Theme>>;
export const findTroubleshootingURL = (
workspaceBuild: WorkspaceBuild,
): string | undefined => {
@@ -185,10 +185,7 @@ export const WorkspaceParametersPageView: FC<
</a>
</Button>
}
css={(theme) => ({
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8,
})}
className="border border-solid border-border rounded-lg"
/>
)
) : (
@@ -197,10 +197,7 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
{pageNumberIsInvalid ? (
<EmptyState
css={(theme) => ({
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadius,
})}
className="border border-solid border-border rounded-lg"
message="Page not found"
description="The page you are trying to access does not exist."
cta={