diff --git a/.vscode/settings.json b/.vscode/settings.json index 82ce10e888010..b3f595bde2d94 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -120,6 +120,7 @@ "stretchr", "STTY", "stuntest", + "subpage", "tailbroker", "tailcfg", "tailexchange", diff --git a/site/src/components/FeatureStageBadge/FeatureStageBadge.stories.tsx b/site/src/components/FeatureStageBadge/FeatureStageBadge.stories.tsx new file mode 100644 index 0000000000000..330b3c9a41105 --- /dev/null +++ b/site/src/components/FeatureStageBadge/FeatureStageBadge.stories.tsx @@ -0,0 +1,38 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { FeatureStageBadge } from "./FeatureStageBadge"; + +const meta: Meta = { + title: "components/FeatureStageBadge", + component: FeatureStageBadge, + args: { + contentType: "beta", + }, +}; + +export default meta; +type Story = StoryObj; + +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", + }, +}; diff --git a/site/src/components/FeatureStageBadge/FeatureStageBadge.tsx b/site/src/components/FeatureStageBadge/FeatureStageBadge.tsx new file mode 100644 index 0000000000000..92c230a4a23f7 --- /dev/null +++ b/site/src/components/FeatureStageBadge/FeatureStageBadge.tsx @@ -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; + +type FeatureStageBadgeProps = Readonly< + Omit, "children"> & { + contentType: keyof typeof featureStageBadgeTypes; + size?: "sm" | "md" | "lg"; + } +>; + +export const FeatureStageBadge: FC = ({ + contentType, + size = "md", + ...delegatedProps +}) => { + return ( + + + {({ isOpen }) => ( + + (This is a + {featureStageBadgeTypes[contentType]} + feature) + + )} + + + +

+ This feature has not yet reached general availability (GA). +

+ + + Learn about feature stages + (link opens in new tab) + +
+
+ ); +}; + +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>; diff --git a/site/src/components/Form/Form.tsx b/site/src/components/Form/Form.tsx index 286c307b6231b..7286e0df1e700 100644 --- a/site/src/components/Form/Form.tsx +++ b/site/src/components/Form/Form.tsx @@ -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", diff --git a/site/src/components/Popover/Popover.tsx b/site/src/components/Popover/Popover.tsx index 7db3c4eda1799..654be6d7931ae 100644 --- a/site/src/components/Popover/Popover.tsx +++ b/site/src/components/Popover/Popover.tsx @@ -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; -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 = (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 & { - children: TriggerElement; - }, -) => { +type PopoverTriggerRenderProps = Readonly<{ + isOpen: boolean; +}>; + +type PopoverTriggerProps = Readonly< + Omit, "children"> & { + children: + | TriggerElement + | ((props: PopoverTriggerRenderProps) => TriggerElement); + } +>; + +export const PopoverTrigger: FC = (props) => { const popover = usePopover(); - const { children, ...elementProps } = props; + const { children, onClick, onPointerEnter, onPointerLeave, ...elementProps } = + props; const clickProps = { - onClick: () => { + onClick: (event: PointerEvent) => { popover.setOpen(true); + onClick?.(event); }, }; const hoverProps = { - onPointerEnter: () => { + onPointerEnter: (event: PointerEvent) => { popover.setOpen(true); + onPointerEnter?.(event); }, - onPointerLeave: () => { + onPointerLeave: (event: PointerEvent) => { 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 = ({ horizontal = "left", + onPointerEnter, + onPointerLeave, ...popoverProps }) => { const popover = usePopover(); @@ -152,7 +186,7 @@ export const PopoverContent: FC = ({ }, }} {...horizontalProps(horizontal)} - {...modeProps(popover)} + {...modeProps(popover, onPointerEnter, onPointerLeave)} {...popoverProps} id={popover.id} open={popover.open} @@ -162,14 +196,20 @@ export const PopoverContent: FC = ({ ); }; -const modeProps = (popover: PopoverContextValue) => { +const modeProps = ( + popover: PopoverContextValue, + externalOnPointerEnter: PointerEventHandler | undefined, + externalOnPointerLeave: PointerEventHandler | undefined, +) => { if (popover.mode === "hover") { return { - onPointerEnter: () => { + onPointerEnter: (event: PointerEvent) => { popover.setOpen(true); + externalOnPointerEnter?.(event); }, - onPointerLeave: () => { + onPointerLeave: (event: PointerEvent) => { popover.setOpen(false); + externalOnPointerLeave?.(event); }, }; } diff --git a/site/src/components/SettingsHeader/SettingsHeader.tsx b/site/src/components/SettingsHeader/SettingsHeader.tsx index ea68415cc1e5b..30ed5b0c527a0 100644 --- a/site/src/components/SettingsHeader/SettingsHeader.tsx +++ b/site/src/components/SettingsHeader/SettingsHeader.tsx @@ -10,6 +10,7 @@ interface HeaderProps { secondary?: boolean; docsHref?: string; tooltip?: ReactNode; + badges?: ReactNode; } export const SettingsHeader: FC = ({ @@ -18,35 +19,40 @@ export const SettingsHeader: FC = ({ docsHref, secondary, tooltip, + badges, }) => { const theme = useTheme(); return (
- -

- {title} -

- {tooltip} + + +

+ {title} +

+ {tooltip} +
+ {badges}
+ {description && ( css` text-decoration: none; color: inherit; - gap: 20px; + gap: 8px; padding: 8px 20px; font-size: 14px; diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx index beff71098fbac..5e845e7bb78a5 100644 --- a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx +++ b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx @@ -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 = () => { } /> {permissions.assignOrgRole && isCustomRolesEnabled && ( - } + - Provisioners - + } + /> + + {isEmpty ? ( - + } + /> {Boolean(error) && !isApiValidationError(error) && (
diff --git a/site/src/pages/ManagementSettingsPage/SidebarView.tsx b/site/src/pages/ManagementSettingsPage/SidebarView.tsx index 795458794bc53..01610596f7ffa 100644 --- a/site/src/pages/ManagementSettingsPage/SidebarView.tsx +++ b/site/src/pages/ManagementSettingsPage/SidebarView.tsx @@ -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 = ({ // TODO: Do something nice to scroll to the active org. return ( -
Deployment
+
+

Deployment

+
+ -
Organizations
+
+

Organizations

+ +
+ {permissions.createOrganization && ( = ({ 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; diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx index a4e520c2b7137..6559e31723156 100644 --- a/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx +++ b/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx @@ -175,23 +175,23 @@ const ThemePreview: FC = ({
-
-
-
+
+
+
-
-
+
+
-
+
-
-
-
-
-
+
+
+
+
+
diff --git a/site/src/theme/branding.ts b/site/src/theme/branding.ts index c1e42e5111abd..ae593e59a3e03 100644 --- a/site/src/theme/branding.ts +++ b/site/src/theme/branding.ts @@ -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; + }>; + }>; +}>; diff --git a/site/src/theme/dark/branding.ts b/site/src/theme/dark/branding.ts index cc0b603b62f0c..614bf630b51bf 100644 --- a/site/src/theme/dark/branding.ts +++ b/site/src/theme/dark/branding.ts @@ -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; diff --git a/site/src/theme/dark/roles.ts b/site/src/theme/dark/roles.ts index 32a9ea4f12992..ec59dfd75e4ba 100644 --- a/site/src/theme/dark/roles.ts +++ b/site/src/theme/dark/roles.ts @@ -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; diff --git a/site/src/theme/darkBlue/branding.ts b/site/src/theme/darkBlue/branding.ts index cc0b603b62f0c..c59079b58a3e6 100644 --- a/site/src/theme/darkBlue/branding.ts +++ b/site/src/theme/darkBlue/branding.ts @@ -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; diff --git a/site/src/theme/darkBlue/roles.ts b/site/src/theme/darkBlue/roles.ts index 744b7329249b9..413398ca6db45 100644 --- a/site/src/theme/darkBlue/roles.ts +++ b/site/src/theme/darkBlue/roles.ts @@ -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; diff --git a/site/src/theme/light/branding.ts b/site/src/theme/light/branding.ts index 97b6df71def0e..a23e43239355f 100644 --- a/site/src/theme/light/branding.ts +++ b/site/src/theme/light/branding.ts @@ -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; diff --git a/site/src/theme/light/roles.ts b/site/src/theme/light/roles.ts index fe3d1d9687bfa..ce04ab554798a 100644 --- a/site/src/theme/light/roles.ts +++ b/site/src/theme/light/roles.ts @@ -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; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy