mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-11-13 12:13:42 +01:00
458 lines
11 KiB
TypeScript
458 lines
11 KiB
TypeScript
import classNames from "classnames";
|
|
import type { ReactNode } from "react";
|
|
import { Checkbox } from "../Checkbox";
|
|
import { CopyIconButton as BaseCopyIconButton, type CopyIconButtonProps } from "../CopyIconButton";
|
|
import { useEditorHost } from "../EditorHost";
|
|
import { Icon } from "../Icon";
|
|
import { IconButton, type IconButtonProps } from "../IconButton";
|
|
import { PlainInput } from "../PlainInput";
|
|
import type { RadioDropdownItem } from "../RadioDropdown";
|
|
import { Select } from "../Select";
|
|
|
|
/** Copies through the host, quietly, as the rows' values do */
|
|
function CopyIconButton(props: Omit<CopyIconButtonProps, "copy">) {
|
|
const host = useEditorHost();
|
|
return <BaseCopyIconButton {...props} copy={host.copy} />;
|
|
}
|
|
|
|
export type SettingRowBaseProps = {
|
|
className?: string;
|
|
controlClassName?: string;
|
|
description?: ReactNode;
|
|
disabled?: boolean;
|
|
title: ReactNode;
|
|
};
|
|
|
|
export function SettingsList({ children, className }: { children: ReactNode; className?: string }) {
|
|
return <div className={classNames("w-full", className)}>{children}</div>;
|
|
}
|
|
|
|
export function SettingsSection({
|
|
children,
|
|
className,
|
|
description,
|
|
title,
|
|
}: {
|
|
children: ReactNode;
|
|
className?: string;
|
|
description?: ReactNode;
|
|
title: ReactNode | null;
|
|
}) {
|
|
const showHeader = title != null || description != null;
|
|
|
|
return (
|
|
<section className={classNames(className, "w-full")}>
|
|
{showHeader && (
|
|
<div className="border-b border-border-subtle pb-2">
|
|
{title != null && <div className="text-text-subtle">{title}</div>}
|
|
{description != null && <p className="mt-1 text-sm text-text-subtlest">{description}</p>}
|
|
</div>
|
|
)}
|
|
<div className="[&>*:last-child]:border-b-0">{children}</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function SettingRow({
|
|
children,
|
|
className,
|
|
controlClassName,
|
|
description,
|
|
disabled,
|
|
title,
|
|
}: {
|
|
children: ReactNode;
|
|
} & SettingRowBaseProps) {
|
|
return (
|
|
<div
|
|
aria-disabled={disabled || undefined}
|
|
className={classNames(
|
|
className,
|
|
"@container border-b border-border-subtle py-4",
|
|
disabled && "opacity-disabled",
|
|
)}
|
|
>
|
|
<div
|
|
className={classNames(
|
|
"grid grid-cols-1 gap-2",
|
|
"@[30rem]:grid-cols-[minmax(0,1fr)_auto] items-center",
|
|
)}
|
|
>
|
|
<div className="min-w-0">
|
|
<div className="text text-text">{title}</div>
|
|
{description != null && (
|
|
<div className="mt-1 max-w-2xl text-sm text-text-subtle">{description}</div>
|
|
)}
|
|
</div>
|
|
<div
|
|
className={classNames(
|
|
"flex min-w-0 items-center justify-start @[40rem]:justify-end",
|
|
controlClassName,
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SettingValue({
|
|
actions,
|
|
className,
|
|
copyText,
|
|
enableCopy = true,
|
|
value,
|
|
}: {
|
|
actions?: SettingValueAction[];
|
|
className?: string;
|
|
copyText?: string;
|
|
enableCopy?: boolean;
|
|
value: ReactNode;
|
|
}) {
|
|
const textValue = typeof value === "string" || typeof value === "number" ? `${value}` : null;
|
|
const textToCopy = copyText ?? textValue;
|
|
|
|
return (
|
|
<>
|
|
<span
|
|
className={classNames(
|
|
className,
|
|
"cursor-text select-text truncate font-mono text-editor text-text-subtle pr-1.5",
|
|
)}
|
|
>
|
|
{value}
|
|
</span>
|
|
{actions?.map((action) => (
|
|
<IconButton
|
|
key={action.title}
|
|
icon={action.icon}
|
|
title={action.title}
|
|
size="2xs"
|
|
iconSize="sm"
|
|
onClick={action.onClick}
|
|
/>
|
|
))}
|
|
{enableCopy && textToCopy != null && (
|
|
<CopyIconButton size="2xs" text={textToCopy} title="Copy value" />
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A command or config to copy, set as a block: its title, an optional line
|
|
* saying which file it goes in, then the snippet at full width with the copy
|
|
* button on it. One-line shell commands and multi-line JSON get the same block.
|
|
*/
|
|
export function SettingCommand({
|
|
className,
|
|
description,
|
|
disabled,
|
|
done,
|
|
language,
|
|
snippet,
|
|
title,
|
|
}: {
|
|
description?: ReactNode;
|
|
disabled?: boolean;
|
|
/** A short word shown with a check beside the title once the step is detected as done */
|
|
done?: ReactNode;
|
|
language: SettingSnippetLanguage;
|
|
snippet: string;
|
|
title: ReactNode;
|
|
} & Pick<SettingRowBaseProps, "className">) {
|
|
return (
|
|
<div
|
|
aria-disabled={disabled || undefined}
|
|
className={classNames(
|
|
className,
|
|
"border-b border-border-subtle py-4",
|
|
disabled && "opacity-disabled",
|
|
)}
|
|
>
|
|
<div className="flex min-w-0 items-center gap-2 text text-text">
|
|
<span>{title}</span>
|
|
{done != null && done !== false && (
|
|
<span className="inline-flex items-center gap-1 text-sm text-success">
|
|
<Icon icon="check" size="sm" />
|
|
{done}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{description != null && (
|
|
<div className="mt-1 max-w-2xl text-sm text-text-subtle">{description}</div>
|
|
)}
|
|
<SettingSnippet className="mt-3" language={language} snippet={snippet} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export type SettingSnippetLanguage = "shell" | "json" | "text";
|
|
|
|
/** The snippet half of a `SettingCommand`, for places with their own title, like a banner. */
|
|
export function SettingSnippet({
|
|
className,
|
|
language,
|
|
snippet,
|
|
}: {
|
|
className?: string;
|
|
language: SettingSnippetLanguage;
|
|
snippet: string;
|
|
}) {
|
|
const prompt = language === "shell";
|
|
|
|
return (
|
|
<div
|
|
className={classNames(
|
|
className,
|
|
"flex items-start gap-2 rounded-md border border-border-subtle bg-surface-highlight/40 py-1.5 pr-1.5 pl-3",
|
|
)}
|
|
>
|
|
<div className="flex min-h-sm min-w-0 flex-1 items-center font-mono text-editor text-text">
|
|
<span className="cursor-text select-text whitespace-pre-wrap [overflow-wrap:anywhere]">
|
|
{prompt && (
|
|
<span aria-hidden className="mr-2 select-none text-text-subtlest">
|
|
$
|
|
</span>
|
|
)}
|
|
{snippet}
|
|
</span>
|
|
</div>
|
|
<CopyIconButton size="sm" variant="border" title="Copy" text={snippet} className="shrink-0" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type SettingValueAction = {
|
|
icon: IconButtonProps["icon"];
|
|
onClick: () => void;
|
|
title: string;
|
|
};
|
|
|
|
export function SettingRowBoolean({
|
|
checked,
|
|
checkboxSize = "md",
|
|
onChange,
|
|
title,
|
|
...props
|
|
}: {
|
|
checked: boolean;
|
|
checkboxSize?: "sm" | "md";
|
|
onChange: (checked: boolean) => void;
|
|
} & SettingRowBaseProps) {
|
|
return (
|
|
<SettingRow title={title} {...props}>
|
|
<Checkbox
|
|
hideLabel
|
|
size={checkboxSize}
|
|
checked={checked}
|
|
disabled={props.disabled}
|
|
title={title}
|
|
onChange={onChange}
|
|
/>
|
|
</SettingRow>
|
|
);
|
|
}
|
|
|
|
export function SettingRowNumber({
|
|
inputClassName,
|
|
inputWidthClassName = "w-48!",
|
|
name,
|
|
onChange,
|
|
placeholder,
|
|
required,
|
|
title,
|
|
type = "number",
|
|
validate,
|
|
value,
|
|
...props
|
|
}: {
|
|
inputClassName?: string;
|
|
inputWidthClassName?: string;
|
|
name: string;
|
|
onChange: (value: number) => void;
|
|
placeholder?: string;
|
|
required?: boolean;
|
|
type?: "number";
|
|
validate?: (value: string) => boolean;
|
|
value: number;
|
|
} & SettingRowBaseProps) {
|
|
return (
|
|
<SettingRow title={title} {...props}>
|
|
<PlainInput
|
|
required={required}
|
|
hideLabel
|
|
size="sm"
|
|
name={name}
|
|
label={typeof title === "string" ? title : name}
|
|
placeholder={placeholder}
|
|
defaultValue={`${value}`}
|
|
validate={validate}
|
|
onChange={(value) => onChange(Number.parseInt(value, 10) || 0)}
|
|
type={type}
|
|
className={inputClassName}
|
|
containerClassName={inputWidthClassName}
|
|
disabled={props.disabled}
|
|
/>
|
|
</SettingRow>
|
|
);
|
|
}
|
|
|
|
export function SettingRowText({
|
|
inputClassName,
|
|
inputWidthClassName = "w-80!",
|
|
name,
|
|
onChange,
|
|
placeholder,
|
|
required,
|
|
title,
|
|
type = "text",
|
|
value,
|
|
...props
|
|
}: {
|
|
inputClassName?: string;
|
|
inputWidthClassName?: string;
|
|
name: string;
|
|
onChange: (value: string) => void;
|
|
placeholder?: string;
|
|
required?: boolean;
|
|
type?: "text" | "password";
|
|
value: string;
|
|
} & SettingRowBaseProps) {
|
|
return (
|
|
<SettingRow title={title} {...props}>
|
|
<PlainInput
|
|
required={required}
|
|
hideLabel
|
|
size="sm"
|
|
name={name}
|
|
label={typeof title === "string" ? title : name}
|
|
placeholder={placeholder}
|
|
defaultValue={value}
|
|
onChange={onChange}
|
|
type={type}
|
|
className={inputClassName}
|
|
containerClassName={inputWidthClassName}
|
|
disabled={props.disabled}
|
|
/>
|
|
</SettingRow>
|
|
);
|
|
}
|
|
|
|
export function SettingRowSelect<T extends string>({
|
|
defaultValue,
|
|
name,
|
|
onChange,
|
|
options,
|
|
selectClassName = "w-48!",
|
|
title,
|
|
value,
|
|
...props
|
|
}: {
|
|
defaultValue?: T;
|
|
name: string;
|
|
onChange: (value: T) => void;
|
|
options: RadioDropdownItem<T>[];
|
|
selectClassName?: string;
|
|
value: T;
|
|
} & SettingRowBaseProps) {
|
|
return (
|
|
<SettingRow title={title} {...props}>
|
|
<SettingSelectControl
|
|
name={name}
|
|
label={typeof title === "string" ? title : name}
|
|
value={value}
|
|
defaultValue={defaultValue}
|
|
selectClassName={selectClassName}
|
|
disabled={props.disabled}
|
|
onChange={onChange}
|
|
options={options}
|
|
/>
|
|
</SettingRow>
|
|
);
|
|
}
|
|
|
|
export function SettingSelectControl<T extends string>({
|
|
defaultValue,
|
|
disabled,
|
|
label,
|
|
name,
|
|
onChange,
|
|
options,
|
|
selectClassName = "w-48!",
|
|
value,
|
|
}: {
|
|
defaultValue?: T;
|
|
disabled?: boolean;
|
|
label: string;
|
|
name: string;
|
|
onChange: (value: T) => void;
|
|
options: RadioDropdownItem<T>[];
|
|
selectClassName?: string;
|
|
value: T;
|
|
}) {
|
|
return (
|
|
<Select
|
|
hideLabel
|
|
name={name}
|
|
value={value}
|
|
defaultValue={defaultValue}
|
|
label={label}
|
|
size="sm"
|
|
className={selectClassName}
|
|
disabled={disabled}
|
|
onChange={onChange}
|
|
options={options}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function SettingOverrideRow({
|
|
children,
|
|
className,
|
|
controlClassName,
|
|
description,
|
|
disabled,
|
|
onResetOverride,
|
|
overridden,
|
|
resetTitle = "Reset override",
|
|
title,
|
|
}: {
|
|
children: ReactNode;
|
|
className?: string;
|
|
controlClassName?: string;
|
|
description?: ReactNode;
|
|
disabled?: boolean;
|
|
onResetOverride: () => void;
|
|
overridden: boolean;
|
|
resetTitle?: string;
|
|
title: ReactNode;
|
|
}) {
|
|
return (
|
|
<SettingRow
|
|
className={className}
|
|
controlClassName={controlClassName}
|
|
description={description}
|
|
disabled={disabled}
|
|
title={
|
|
<span className="inline-flex items-center gap-1.5">
|
|
{title}
|
|
{overridden && (
|
|
<IconButton
|
|
icon="undo_2"
|
|
size="2xs"
|
|
iconSize="sm"
|
|
title={resetTitle}
|
|
className="text-text-subtle"
|
|
onClick={onResetOverride}
|
|
/>
|
|
)}
|
|
</span>
|
|
}
|
|
>
|
|
{children}
|
|
</SettingRow>
|
|
);
|
|
}
|