import type { EnvironmentId } from "@t3tools/contracts";import { ScaleIcon } from "lucide-react";import { memo, useMemo } from "react"; import type { EnvironmentOption } from "./BranchToolbar.logic";import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";import { composerFloatingLayerProps } from "./chat/composerEventScope";import { Select, SelectGroup, SelectGroupLabel, SelectItem, SelectPopup, SelectTrigger, SelectValue,} from "./ui/select"; interface BranchToolbarEnvironmentSelectorProps { autoEnvironmentLabel?: string | undefined; onAutoEnvironment?: (() => void) | undefined; envLocked: boolean; environmentId: EnvironmentId; availableEnvironments: readonly EnvironmentOption[]; // Absent when there is only one environment to show: the indicator still // renders (as a static label) so remote projects are always identifiable. onEnvironmentChange?: (environmentId: EnvironmentId) => void;} export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvironmentSelector({ autoEnvironmentLabel, onAutoEnvironment, envLocked, environmentId, availableEnvironments, onEnvironmentChange,}: BranchToolbarEnvironmentSelectorProps) { const activeEnvironment = useMemo(() => { return availableEnvironments.find((env) => env.environmentId === environmentId) ?? null; }, [availableEnvironments, environmentId]); const environmentItems = useMemo( () => [ ...(onAutoEnvironment ? [{ value: "auto", label: autoEnvironmentLabel ?? "Auto balance" }] : []), ...availableEnvironments.map((env) => ({ value: env.environmentId, label: env.label, })), ], [availableEnvironments, autoEnvironmentLabel, onAutoEnvironment], ); // The static label carries the xs control's height (h-7 sm:h-6) as well as // its padding: the composer context strip has no min-height of its own, and // the glass seam joining it to the composer assumes a fixed strip height, so // a shorter label would drag the seam out of line whenever this label is the // only thing in the strip. if (envLocked || onEnvironmentChange === undefined) { return ( <span className="inline-flex h-7 min-w-0 max-w-full items-center gap-1 border border-transparent px-[calc(--spacing(2)-1px)] font-normal text-muted-foreground/70 text-xs sm:h-6" data-composer-context-control > <EnvironmentMachineIcon kind={activeEnvironment?.machine ?? "server"} className="size-3 shrink-0" /> <span data-composer-label className="min-w-0 max-w-[240px] group-data-[compact]/composer-context:max-w-0" > <span data-composer-label-motion className="block w-full min-w-0 max-w-[240px] truncate transition-opacity duration-180 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-[compact]/composer-context:opacity-0 motion-reduce:transition-none" > {activeEnvironment?.label ?? "Run on"} </span> </span> </span> ); } return ( <Select modal={false} value={autoEnvironmentLabel ? "auto" : environmentId} onValueChange={(value) => value === "auto" ? onAutoEnvironment?.() : onEnvironmentChange(value as EnvironmentId) } items={environmentItems} > <SelectTrigger variant="ghost" size="xs" className="min-w-0 max-w-full font-normal text-xs!" aria-label="Run on" data-composer-context-control > {autoEnvironmentLabel ? ( <ScaleIcon className="size-3 shrink-0" aria-hidden="true" /> ) : ( <EnvironmentMachineIcon kind={activeEnvironment?.machine ?? "server"} className="size-3 shrink-0" /> )} <span data-composer-label className="min-w-0 max-w-[240px] group-data-[compact]/composer-context:max-w-0" > <span data-composer-label-motion className="block w-full min-w-0 max-w-[240px] truncate transition-opacity duration-180 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-[compact]/composer-context:opacity-0 motion-reduce:transition-none" > <SelectValue /> </span> </span> </SelectTrigger> <SelectPopup alignItemWithTrigger={false} {...composerFloatingLayerProps}> <SelectGroup> <SelectGroupLabel>Run on</SelectGroupLabel> {onAutoEnvironment && ( <SelectItem value="auto" onClick={() => { if (autoEnvironmentLabel) onAutoEnvironment?.(); }} > <span className="inline-flex items-center gap-1.5"> <ScaleIcon className="size-3" aria-hidden="true" /> {autoEnvironmentLabel ?? "Auto balance"} </span> </SelectItem> )} {availableEnvironments.map((env) => ( <SelectItem key={env.environmentId} value={env.environmentId}> <span className="inline-flex items-center gap-1.5"> <EnvironmentMachineIcon kind={env.machine} className="size-3" /> {env.label} </span> </SelectItem> ))} </SelectGroup> </SelectPopup> </Select> );});
import type { EnvironmentId } from "@t3tools/contracts";
import { ScaleIcon } from "lucide-react";
import { memo, useMemo } from "react";
import type { EnvironmentOption } from "./BranchToolbar.logic";
import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
import { composerFloatingLayerProps } from "./chat/composerEventScope";
import {
Select,
SelectGroup,
SelectGroupLabel,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "./ui/select";
interface BranchToolbarEnvironmentSelectorProps {
autoEnvironmentLabel?: string | undefined;
onAutoEnvironment?: (() => void) | undefined;
envLocked: boolean;
environmentId: EnvironmentId;
availableEnvironments: readonly EnvironmentOption[];
// Absent when there is only one environment to show: the indicator still
// renders (as a static label) so remote projects are always identifiable.
onEnvironmentChange?: (environmentId: EnvironmentId) => void;
}
export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvironmentSelector({
autoEnvironmentLabel,
onAutoEnvironment,
envLocked,
environmentId,
availableEnvironments,
onEnvironmentChange,
}: BranchToolbarEnvironmentSelectorProps) {
const activeEnvironment = useMemo(() => {
return availableEnvironments.find((env) => env.environmentId === environmentId) ?? null;
}, [availableEnvironments, environmentId]);
const environmentItems = useMemo(
() => [
...(onAutoEnvironment
? [{ value: "auto", label: autoEnvironmentLabel ?? "Auto balance" }]
: []),
...availableEnvironments.map((env) => ({
value: env.environmentId,
label: env.label,
})),
],
[availableEnvironments, autoEnvironmentLabel, onAutoEnvironment],
);
// The static label carries the xs control's height (h-7 sm:h-6) as well as
// its padding: the composer context strip has no min-height of its own, and
// the glass seam joining it to the composer assumes a fixed strip height, so
// a shorter label would drag the seam out of line whenever this label is the
// only thing in the strip.
if (envLocked || onEnvironmentChange === undefined) {
return (
<span
className="inline-flex h-7 min-w-0 max-w-full items-center gap-1 border border-transparent px-[calc(--spacing(2)-1px)] font-normal text-muted-foreground/70 text-xs sm:h-6"
data-composer-context-control
>
<EnvironmentMachineIcon
kind={activeEnvironment?.machine ?? "server"}
className="size-3 shrink-0"
/>
<span
data-composer-label
className="min-w-0 max-w-[240px] group-data-[compact]/composer-context:max-w-0"
>
<span
data-composer-label-motion
className="block w-full min-w-0 max-w-[240px] truncate transition-opacity duration-180 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-[compact]/composer-context:opacity-0 motion-reduce:transition-none"
>
{activeEnvironment?.label ?? "Run on"}
</span>
</span>
</span>
);
}
return (
<Select
modal={false}
value={autoEnvironmentLabel ? "auto" : environmentId}
onValueChange={(value) =>
value === "auto" ? onAutoEnvironment?.() : onEnvironmentChange(value as EnvironmentId)
}
items={environmentItems}
>
<SelectTrigger
variant="ghost"
size="xs"
className="min-w-0 max-w-full font-normal text-xs!"
aria-label="Run on"
data-composer-context-control
>
{autoEnvironmentLabel ? (
<ScaleIcon className="size-3 shrink-0" aria-hidden="true" />
) : (
<EnvironmentMachineIcon
kind={activeEnvironment?.machine ?? "server"}
className="size-3 shrink-0"
/>
)}
<span
data-composer-label
className="min-w-0 max-w-[240px] group-data-[compact]/composer-context:max-w-0"
>
<span
data-composer-label-motion
className="block w-full min-w-0 max-w-[240px] truncate transition-opacity duration-180 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-[compact]/composer-context:opacity-0 motion-reduce:transition-none"
>
<SelectValue />
</span>
</span>
</SelectTrigger>
<SelectPopup alignItemWithTrigger={false} {...composerFloatingLayerProps}>
<SelectGroup>
<SelectGroupLabel>Run on</SelectGroupLabel>
{onAutoEnvironment && (
<SelectItem
value="auto"
onClick={() => {
if (autoEnvironmentLabel) onAutoEnvironment?.();
}}
>
<span className="inline-flex items-center gap-1.5">
<ScaleIcon className="size-3" aria-hidden="true" />
{autoEnvironmentLabel ?? "Auto balance"}
</span>
</SelectItem>
)}
{availableEnvironments.map((env) => (
<SelectItem key={env.environmentId} value={env.environmentId}>
<span className="inline-flex items-center gap-1.5">
<EnvironmentMachineIcon kind={env.machine} className="size-3" />
{env.label}
</span>
</SelectItem>
))}
</SelectGroup>
</SelectPopup>
</Select>
);
});