Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/sso-directory-sync-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: improvement
---

Refreshed the SSO & Directory Sync settings page layout and copy
4 changes: 3 additions & 1 deletion apps/webapp/app/components/LoginPageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ export function LoginPageLayout({ children }: { children: React.ReactNode }) {
<LogoType className="w-36" />
</a>
</div>
<div className="flex h-full max-w-sm items-center justify-center">{children}</div>
<div className="flex h-full w-full max-w-xs items-center justify-center">
<div className="w-full">{children}</div>
</div>
<Paragraph variant="small" className="text-center">
Having login issues? <TextLink href="https://trigger.dev/contact">Email us</TextLink> or{" "}
<TextLink href="https://trigger.dev/discord">ask us in Discord</TextLink>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export function OrganizationSettingsSideMenu({
)}
{isSsoUsingPlugin && (
<SideMenuItem
name="Identity & Access"
name="SSO & Directory Sync"
icon={PadlockIcon}
activeIconColor="text-text-bright"
inactiveIconColor="text-text-dimmed"
Expand Down
20 changes: 16 additions & 4 deletions apps/webapp/app/components/primitives/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ export interface SelectProps<TValue extends string | string[], TItem> extends Om
allowItemShortcuts?: boolean;
clearSearchOnSelection?: boolean;
dropdownIcon?: boolean | React.ReactNode;
popoverClassName?: string;
placement?: Ariakit.SelectProviderProps<TValue>["placement"];
}

export function Select<TValue extends string | string[], TItem>({
Expand All @@ -135,6 +137,8 @@ export function Select<TValue extends string | string[], TItem>({
disabled,
clearSearchOnSelection = true,
dropdownIcon,
popoverClassName,
placement,
...props
}: SelectProps<TValue, TItem>) {
const [searchValue, setSearchValue] = useState("");
Expand Down Expand Up @@ -191,6 +195,7 @@ export function Select<TValue extends string | string[], TItem>({
open={open}
setOpen={setOpen}
virtualFocus={searchable}
placement={placement}
value={value}
setValue={(v) => {
if (clearSearchOnSelection) {
Expand All @@ -215,7 +220,7 @@ export function Select<TValue extends string | string[], TItem>({
dropdownIcon={dropdownIcon}
{...props}
/>
<SelectPopover>
<SelectPopover className={popoverClassName}>
{!searchable && showHeading && heading && <SelectHeading render={<>{heading}</>} />}
{searchable && <ComboBox placeholder={heading} shortcut={shortcut} value={searchValue} />}

Expand Down Expand Up @@ -437,7 +442,7 @@ export function SelectList(props: SelectListProps) {
<Component
{...props}
className={cn(
"overflow-y-auto overscroll-contain scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control focus-custom",
"overflow-y-auto overflow-x-hidden overscroll-contain scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control focus-custom",
props.className
)}
/>
Expand All @@ -449,6 +454,9 @@ export interface SelectItemProps extends Ariakit.SelectItemProps {
checkIcon?: React.ReactNode;
checkPosition?: "left" | "right";
shortcut?: ShortcutDefinition;
// Allow the item to grow to multiple lines and wrap its content instead of
// being locked to a single truncated line. Use for options with a subtitle.
wrap?: boolean;
}

const selectItemClasses =
Expand All @@ -461,6 +469,7 @@ export function SelectItem({
checkIcon = <Ariakit.SelectItemCheck className="size-8 flex-none text-text-bright" />,
checkPosition = "right",
shortcut,
wrap = false,
...props
}: SelectItemProps) {
const combobox = Ariakit.useComboboxContext();
Expand Down Expand Up @@ -508,13 +517,16 @@ export function SelectItem({
>
<div
className={cn(
"flex h-8 w-full items-center rounded-sm px-2 group-data-[active-item=true]:bg-tertiary hover:bg-tertiary",
"flex w-full items-center rounded-sm px-2 group-data-[active-item=true]:bg-tertiary hover:bg-tertiary",
wrap ? "min-h-8" : "h-8",
checkPosition === "left" ? "gap-2" : "gap-1"
)}
>
{checkPosition === "left" && <CheckboxIndicator checked={isChecked} />}
{icon}
<div className="grow truncate">{props.children || props.value}</div>
<div className={cn("grow", wrap ? "min-w-0 break-words py-1.5" : "truncate")}>
{props.children || props.value}
</div>
{checkPosition === "right" && checkIcon}
{shortcut && (
<ShortcutKey
Expand Down
216 changes: 216 additions & 0 deletions apps/webapp/app/components/primitives/SettingsLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { type ReactNode } from "react";
import { MainHorizontallyCenteredContainer } from "~/components/layout/AppLayout";
import { cn } from "~/utils/cn";
import { Header2, Header3 } from "./Headers";
import { Paragraph } from "./Paragraph";

// A composable layout system for settings pages: a centered container holds
// sections; each section has a header (title/description/action over a divide)
// followed by rows. A row lays out a title + description on the left and an
// action (button/switch/select/status) on the right, separated by divides and
// spacing rather than bordered boxes.
//
// Everything that renders text accepts `ReactNode`, and every piece takes a
// `className` so callers can restyle without forking. For layouts the built-in
// props don't cover, pass `children` to a row/block for full control.

const rowSize = {
sm: "py-3",
md: "py-4",
} as const;

type RowSize = keyof typeof rowSize;

/** Page-level wrapper that centers content and sets the settings column width. */
export function SettingsContainer({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<MainHorizontallyCenteredContainer
className={cn("max-w-[37.5rem] overflow-visible", className)}
>
{children}
</MainHorizontallyCenteredContainer>
);
}

/** A group of related rows. Adds vertical spacing between sibling sections. */
export function SettingsSection({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<section className={cn("w-full [&:not(:first-child)]:mt-12", className)}>{children}</section>
);
}

/**
* Section (or sub-section) heading with an optional description and a
* right-aligned action, sitting above a bottom divide. Use `as="h3"` for a
* heading nested inside a section.
*/
export function SettingsHeader({
title,
description,
action,
as = "h2",
className,
}: {
title: ReactNode;
description?: ReactNode;
action?: ReactNode;
as?: "h2" | "h3";
className?: string;
}) {
const Heading = as === "h3" ? Header3 : Header2;
return (
<div
className={cn(
"flex w-full items-end justify-between gap-8 border-b border-grid-dimmed",
// An h2 section header gets its top spacing from SettingsSection's margin,
// so it only needs bottom padding. An h3 sits mid-section among rows, so it
// takes the full row rhythm (py-4) to separate from the divide above it.
as === "h3" ? "py-4" : "pb-3",
className
)}
>
<div className="space-y-1">
<Heading>{title}</Heading>
{description ? <Paragraph variant="small">{description}</Paragraph> : null}
</div>
{action ? <div className="flex flex-none items-center">{action}</div> : null}
</div>
);
}

/** Title typography for a row. Renders a `<label>` when `htmlFor` is set. */
export function SettingsRowTitle({
children,
htmlFor,
className,
}: {
children: ReactNode;
htmlFor?: string;
className?: string;
}) {
const classes = cn("font-sans text-sm font-semibold leading-tight text-text-bright", className);
return htmlFor ? (
<label htmlFor={htmlFor} className={classes}>
{children}
</label>
) : (
<span className={classes}>{children}</span>
);
}

/** Description/subtitle typography for a row. */
export function SettingsRowDescription({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<Paragraph variant="small" className={className}>
{children}
</Paragraph>
);
}

/**
* A single settings row: title + description on the left, action on the right.
*
* Pass `title`/`description` for the common case, or `children` to supply
* custom left-hand content (the built-in title group is skipped when `children`
* is provided). `action` renders on the right in both cases.
*/
export function SettingsRow({
title,
description,
action,
htmlFor,
children,
className,
titleClassName,
size = "md",
align = "center",
bordered = true,
}: {
title?: ReactNode;
description?: ReactNode;
action?: ReactNode;
htmlFor?: string;
children?: ReactNode;
className?: string;
titleClassName?: string;
size?: RowSize;
align?: "center" | "start";
bordered?: boolean;
}) {
return (
<div
className={cn(
"flex w-full justify-between gap-8",
align === "center" ? "items-center" : "items-start",
rowSize[size],
bordered && "border-b border-grid-dimmed",
className
)}
>
{children ?? (
<div className="flex-1 space-y-1">
{title ? (
<SettingsRowTitle htmlFor={htmlFor} className={titleClassName}>
{title}
</SettingsRowTitle>
) : null}
{description ? <SettingsRowDescription>{description}</SettingsRowDescription> : null}
</div>
)}
{action ? <div className="flex flex-none items-center">{action}</div> : null}
</div>
);
}

/**
* Full-width row for arbitrary content (callouts, empty states, custom blocks)
* that shouldn't be split into a title/action layout.
*/
export function SettingsBlock({
children,
className,
size = "md",
bordered = true,
}: {
children: ReactNode;
className?: string;
size?: RowSize;
bordered?: boolean;
}) {
return (
<div
className={cn("w-full", rowSize[size], bordered && "border-b border-grid-dimmed", className)}
>
{children}
</div>
);
}

/** Right-aligned action bar, typically for a section's Save button. */
export function SettingsActions({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return <div className={cn("flex justify-end gap-2 py-4", className)}>{children}</div>;
}
Loading