add onboarding

This commit is contained in:
Eduard Gert
2026-06-03 17:33:13 +02:00
parent 1710868a09
commit 64199209cf
23 changed files with 731 additions and 92 deletions
@@ -7,21 +7,28 @@ import { ManagementMode } from "@/hooks/useManagementUrl.ts";
type Props = {
value: ManagementMode;
onChange: (mode: ManagementMode) => void;
// fullWidth stretches the segmented control to fill its container —
// the SettingsGeneral row uses the default (shrink-to-content) layout,
// the welcome dialog asks for the wide variant so the picker spans the
// narrow dialog width.
fullWidth?: boolean;
};
export const ManagementServerSwitch = ({ value, onChange }: Props) => {
export const ManagementServerSwitch = ({ value, onChange, fullWidth = false }: Props) => {
const { t, i18n } = useTranslation();
const itemClass = fullWidth ? "flex-1" : undefined;
return (
<SwitchItemGroup
key={i18n.language}
value={value}
onChange={(v) => onChange(v as ManagementMode)}
className={fullWidth ? "w-full" : undefined}
>
<SwitchItem value={ManagementMode.Cloud}>
<SwitchItem value={ManagementMode.Cloud} className={itemClass}>
<img src={netbirdLogo} alt={""} className={"h-[0.8rem] aspect-[31/23] shrink-0"} />
{t("settings.general.management.cloud")}
</SwitchItem>
<SwitchItem value={ManagementMode.SelfHosted}>
<SwitchItem value={ManagementMode.SelfHosted} className={itemClass}>
{t("settings.general.management.selfHosted")}
</SwitchItem>
</SwitchItemGroup>
@@ -15,7 +15,7 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, ButtonVar
const buttonVariants = cva(
[
"relative",
"text-sm focus:z-10 focus:ring-2 font-medium focus:outline-none whitespace-nowrap shadow-sm select-none",
"text-sm focus:z-10 focus:ring-2 font-medium focus:outline-none whitespace-nowrap shadow-sm select-none cursor-default",
"inline-flex gap-2 items-center justify-center transition-colors focus:ring-offset-1",
"disabled:opacity-40 disabled:cursor-not-allowed disabled:dark:text-nb-gray-300 dark:ring-offset-neutral-950/50",
],
@@ -24,7 +24,7 @@ export const ConfirmDialog = forwardRef<HTMLDivElement, ConfirmDialogProps>(func
<div
ref={ref}
className={cn(
"flex flex-col items-center gap-5 text-center px-8 py-6",
"flex flex-col items-center gap-5 text-center px-8 pt-6 pb-7",
isMacOS() && "pt-10",
)}
>
@@ -3,11 +3,32 @@ import { cn } from "@/lib/cn";
// DialogDescription is the supporting description text rendered under a
// DialogHeading inside ConfirmDialog (and similar dialog surfaces).
type DialogAlign = "left" | "center" | "right";
const alignClass: Record<DialogAlign, string> = {
left: "text-left",
center: "text-center",
right: "text-right",
};
type DialogDescriptionProps = {
children: ReactNode;
className?: string;
align?: DialogAlign;
};
export const DialogDescription = ({ children, className }: DialogDescriptionProps) => (
<p className={cn("text-sm text-nb-gray-300 select-none", className)}>{children}</p>
export const DialogDescription = ({ children, className, align = "center" }: DialogDescriptionProps) => (
// w-full for the same reason DialogHeading carries it — see the
// comment there. The default text-center remains visually identical
// to before; left/right alignment now anchors to the dialog content
// edge instead of collapsing to no-op on a content-width box.
<p
className={cn(
"w-full text-sm text-nb-gray-300 select-none",
alignClass[align],
className,
)}
>
{children}
</p>
);
@@ -4,13 +4,34 @@ import { cn } from "@/lib/cn";
// DialogHeading is the title text used inside ConfirmDialog (and any other
// dialog-style surface with the same shape). Pair with DialogDescription
// for the standard title/description stack.
type DialogAlign = "left" | "center" | "right";
const alignClass: Record<DialogAlign, string> = {
left: "text-left",
center: "text-center",
right: "text-right",
};
type DialogHeadingProps = {
children: ReactNode;
className?: string;
align?: DialogAlign;
};
export const DialogHeading = ({ children, className }: DialogHeadingProps) => (
<p className={cn("text-base font-semibold text-nb-gray-50 select-none", className)}>
export const DialogHeading = ({ children, className, align = "center" }: DialogHeadingProps) => (
// w-full so the alignClass actually has a box to anchor against.
// The wrapping <p> defaulted to content width inside a flex column,
// which made `text-left` a no-op (nothing to push the text away
// from). Stretching the element is invisible for the default
// text-center case (center of content == center of box) and lets
// text-left/right line up with the dialog's content edge.
<p
className={cn(
"w-full text-base font-semibold text-nb-gray-50 select-none",
alignClass[align],
className,
)}
>
{children}
</p>
);