[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
267
components/settings/settings-ui.tsx
Normal file
267
components/settings/settings-ui.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
import React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { settingsAnchorDomId } from "../../domain/settingsSearchCatalog";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { TabsContent } from "../ui/tabs";
|
||||
|
||||
interface ToggleProps {
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export const Toggle: React.FC<ToggleProps> = ({ checked, onChange, disabled, ariaLabel }) => (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-label={ariaLabel}
|
||||
disabled={disabled}
|
||||
onClick={() => onChange(!checked)}
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
checked ? "bg-primary" : "bg-input",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform",
|
||||
checked ? "translate-x-4" : "translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
|
||||
interface SelectProps {
|
||||
value: string;
|
||||
options: { value: string; label: string; icon?: React.ReactNode }[];
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const Select: React.FC<SelectProps> = ({
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
className,
|
||||
disabled,
|
||||
placeholder,
|
||||
}) => {
|
||||
const selectedOption = options.find((opt) => opt.value === value);
|
||||
const fitSelectedText = typeof className !== "string" || !className.includes("w-full");
|
||||
return (
|
||||
<SelectPrimitive.Root value={value} onValueChange={onChange} disabled={disabled}>
|
||||
<SelectPrimitive.Trigger
|
||||
className={cn(
|
||||
"flex h-9 max-w-full items-center justify-between rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:min-w-0 [&>span]:truncate [&>span]:whitespace-nowrap",
|
||||
fitSelectedText && "min-w-max",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<SelectPrimitive.Value placeholder={placeholder}>
|
||||
<span className="flex min-w-0 items-center gap-2 truncate whitespace-nowrap">
|
||||
{selectedOption?.icon}
|
||||
<span className="truncate whitespace-nowrap">{selectedOption?.label}</span>
|
||||
</span>
|
||||
</SelectPrimitive.Value>
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
className="z-[200000] max-h-80 w-max max-w-[min(24rem,var(--radix-select-content-available-width))] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1"
|
||||
position="popper"
|
||||
sideOffset={4}
|
||||
style={{ minWidth: "max(12rem, var(--radix-select-trigger-width))" }}
|
||||
>
|
||||
<SelectPrimitive.ScrollUpButton className="flex cursor-default items-center justify-center py-1">
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
<SelectPrimitive.Viewport className="p-1">
|
||||
{options.map((opt) => (
|
||||
<SelectPrimitive.Item
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
className="relative flex w-full min-w-0 cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>
|
||||
<span className="flex min-w-0 items-center gap-2 whitespace-normal break-words leading-snug">
|
||||
{opt.icon}
|
||||
{opt.label}
|
||||
</span>
|
||||
</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectPrimitive.ScrollDownButton className="flex cursor-default items-center justify-center py-1">
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
</SelectPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export const SectionHeader: React.FC<{
|
||||
title: string;
|
||||
className?: string;
|
||||
anchorId?: string;
|
||||
}> = ({ title, className, anchorId }) => (
|
||||
<h3
|
||||
id={anchorId ? settingsAnchorDomId(anchorId) : undefined}
|
||||
data-settings-anchor={anchorId}
|
||||
className={cn("text-sm font-semibold text-foreground mb-3 rounded-md", className)}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
);
|
||||
|
||||
/** Section title row → content gap (shared across settings pages). */
|
||||
export const settingsSectionGapClassName = "gap-2";
|
||||
|
||||
/** Groups a section title (optional icon/actions) with its content at a uniform gap. */
|
||||
export const SettingsSection: React.FC<{
|
||||
title?: string;
|
||||
leading?: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
anchorId?: string;
|
||||
}> = ({ title, leading, actions, children, className, anchorId }) => (
|
||||
<section
|
||||
id={anchorId ? settingsAnchorDomId(anchorId) : undefined}
|
||||
data-settings-anchor={anchorId}
|
||||
className={cn("flex flex-col rounded-md", settingsSectionGapClassName, className)}
|
||||
>
|
||||
{(title || leading || actions) && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-h-8 items-center gap-2",
|
||||
actions && "justify-between gap-4",
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
{leading}
|
||||
{title ? <h3 className="text-sm font-semibold text-foreground">{title}</h3> : null}
|
||||
</div>
|
||||
{actions ? <div className="flex shrink-0 items-center gap-2">{actions}</div> : null}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
|
||||
/** Footer note under a SettingCard. Keep a gap so it does not kiss the card. */
|
||||
export const settingHintClassName = "mt-3 text-xs text-muted-foreground";
|
||||
|
||||
export const SettingHint: React.FC<{
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}> = ({ children, className }) => (
|
||||
<p className={cn(settingHintClassName, className)}>{children}</p>
|
||||
);
|
||||
|
||||
export const settingCardClassName = "rounded-lg border bg-card";
|
||||
|
||||
interface SettingCardProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
/** Row list with dividers; vertical spacing comes from SettingRow. */
|
||||
divided?: boolean;
|
||||
/** Free-form content; apply even padding on all sides. */
|
||||
padded?: boolean;
|
||||
}
|
||||
|
||||
export const SettingCard: React.FC<SettingCardProps> = ({
|
||||
children,
|
||||
className,
|
||||
divided = false,
|
||||
padded = false,
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
settingCardClassName,
|
||||
padded ? "p-4" : "px-4",
|
||||
divided && "space-y-0 divide-y divide-border",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
interface SettingRowProps {
|
||||
label?: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
align?: "center" | "start";
|
||||
/** Stable catalog id for settings search jump targets. */
|
||||
anchorId?: string;
|
||||
}
|
||||
|
||||
export const SettingRow: React.FC<SettingRowProps> = ({
|
||||
label,
|
||||
description,
|
||||
children,
|
||||
align = "center",
|
||||
anchorId,
|
||||
}) => (
|
||||
<div
|
||||
id={anchorId ? settingsAnchorDomId(anchorId) : undefined}
|
||||
data-settings-anchor={anchorId}
|
||||
className={cn(
|
||||
// Keep square corners: rounded rows bend divide-y separators at both ends.
|
||||
"flex justify-between py-3 gap-4",
|
||||
align === "start" ? "items-start" : "items-center",
|
||||
)}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
{label && <div className="text-sm font-medium">{label}</div>}
|
||||
{description && (
|
||||
<div className={cn("text-xs text-muted-foreground", label && "mt-0.5")}>{description}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="shrink-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/** Lightweight wrapper for non-SettingRow search targets. */
|
||||
export const SettingsAnchor: React.FC<{
|
||||
anchorId: string;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}> = ({ anchorId, className, children }) => (
|
||||
<div
|
||||
id={settingsAnchorDomId(anchorId)}
|
||||
data-settings-anchor={anchorId}
|
||||
// No default rounded-md: inside divide-y cards, child border-radius bends separators.
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SettingsTabContent: React.FC<{
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
}> = ({ value, children }) => (
|
||||
<TabsContent value={value} className="flex-1 m-0 h-full overflow-hidden">
|
||||
{/* data-settings-scroll-pane: search jump scrolls only this pane, not the window */}
|
||||
<div
|
||||
data-settings-scroll-pane
|
||||
className="h-full overflow-y-auto overflow-x-hidden"
|
||||
>
|
||||
<div className="p-6 space-y-6">{children}</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
);
|
||||
Reference in New Issue
Block a user