mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
refactor(desktop): add shared Field form-dialog primitive
Dialog forms each hand-rolled their own label+control+hint stack (or borrowed the settings-surface ListRow), so gaps and hint styling drifted between the profile, cron, and webhook dialogs. Add a single Field / FieldHint primitive for label-over-control dialog fields and adopt it in the create/rename profile dialogs as the first consumers.
This commit is contained in:
parent
d372fda6f0
commit
a8c7a5f70f
3 changed files with 55 additions and 30 deletions
|
|
@ -10,13 +10,13 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '@/components/ui/dialog'
|
||||
import { Field, FieldHint } from '@/components/ui/field'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { createProfile, updateProfileSoul } from '@/hermes'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { AlertTriangle } from '@/lib/icons'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { ProfileInfo } from '@/types/hermes'
|
||||
|
||||
const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/
|
||||
|
|
@ -100,10 +100,7 @@ export function CreateProfileDialog({
|
|||
</DialogHeader>
|
||||
|
||||
<form className="grid gap-4" onSubmit={handleSubmit}>
|
||||
<div className="grid gap-1.5">
|
||||
<label className="text-xs font-medium" htmlFor="new-profile-name">
|
||||
{p.nameLabel}
|
||||
</label>
|
||||
<Field htmlFor="new-profile-name" label={p.nameLabel}>
|
||||
<Input
|
||||
aria-invalid={invalid}
|
||||
autoFocus
|
||||
|
|
@ -112,15 +109,10 @@ export function CreateProfileDialog({
|
|||
placeholder="my-profile"
|
||||
value={name}
|
||||
/>
|
||||
<p className={cn('text-[0.66rem] leading-4', invalid ? 'text-destructive' : 'text-muted-foreground')}>
|
||||
{p.nameHint}
|
||||
</p>
|
||||
</div>
|
||||
<FieldHint error={invalid}>{p.nameHint}</FieldHint>
|
||||
</Field>
|
||||
|
||||
<div className="grid gap-1.5">
|
||||
<label className="text-xs font-medium" htmlFor="new-profile-clone-from">
|
||||
{p.cloneFrom}
|
||||
</label>
|
||||
<Field htmlFor="new-profile-clone-from" label={p.cloneFrom}>
|
||||
<Select
|
||||
onValueChange={value => setCloneFrom(value === '__none__' ? null : value)}
|
||||
value={cloneFrom ?? '__none__'}
|
||||
|
|
@ -137,13 +129,10 @@ export function CreateProfileDialog({
|
|||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">{p.cloneFromDesc}</p>
|
||||
</div>
|
||||
<FieldHint>{p.cloneFromDesc}</FieldHint>
|
||||
</Field>
|
||||
|
||||
<div className="grid gap-1.5">
|
||||
<label className="text-xs font-medium" htmlFor="new-profile-soul">
|
||||
SOUL.md <span className="font-normal text-muted-foreground">- {p.soulOptional}</span>
|
||||
</label>
|
||||
<Field htmlFor="new-profile-soul" label="SOUL.md" optional optionalLabel={p.soulOptional}>
|
||||
<Textarea
|
||||
className="min-h-28 font-mono text-xs leading-5"
|
||||
id="new-profile-soul"
|
||||
|
|
@ -151,7 +140,7 @@ export function CreateProfileDialog({
|
|||
placeholder={p.soulPlaceholder(cloneFrom ? p.soulPlaceholderCloned : p.soulPlaceholderEmpty)}
|
||||
value={soul}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '@/components/ui/dialog'
|
||||
import { Field, FieldHint } from '@/components/ui/field'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { renameProfile } from '@/hermes'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { AlertTriangle } from '@/lib/icons'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import { isValidProfileName } from './create-profile-dialog'
|
||||
|
||||
|
|
@ -93,11 +93,8 @@ export function RenameProfileDialog({
|
|||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form className="grid gap-3" onSubmit={handleSubmit}>
|
||||
<div className="grid gap-1.5">
|
||||
<label className="text-xs font-medium" htmlFor="rename-profile-name">
|
||||
{p.newNameLabel}
|
||||
</label>
|
||||
<form className="grid gap-4" onSubmit={handleSubmit}>
|
||||
<Field htmlFor="rename-profile-name" label={p.newNameLabel}>
|
||||
<Input
|
||||
aria-invalid={invalid}
|
||||
autoFocus
|
||||
|
|
@ -105,10 +102,8 @@ export function RenameProfileDialog({
|
|||
onChange={event => setName(event.target.value)}
|
||||
value={name}
|
||||
/>
|
||||
<p className={cn('text-[0.66rem] leading-4', invalid ? 'text-destructive' : 'text-muted-foreground')}>
|
||||
{p.nameHint}
|
||||
</p>
|
||||
</div>
|
||||
<FieldHint error={invalid}>{p.nameHint}</FieldHint>
|
||||
</Field>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||
|
|
|
|||
41
apps/desktop/src/components/ui/field.tsx
Normal file
41
apps/desktop/src/components/ui/field.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import type { ReactNode } from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
// Shared form-field primitive for dialog forms: a label stacked above its
|
||||
// control, with an optional inline "(optional)" tag. Pair with FieldHint for
|
||||
// help text below the control. This is the single field language for every form
|
||||
// dialog (cron, webhooks, profiles, …) — don't hand-roll label+control stacks
|
||||
// or reach for the settings-surface ListRow inside a dialog. Stack Fields in a
|
||||
// `grid gap-4` form; pair two across with `grid items-start gap-4 sm:grid-cols-2`.
|
||||
export function Field({
|
||||
children,
|
||||
htmlFor,
|
||||
label,
|
||||
optional,
|
||||
optionalLabel
|
||||
}: {
|
||||
children: ReactNode
|
||||
htmlFor?: string
|
||||
label: ReactNode
|
||||
optional?: boolean
|
||||
optionalLabel?: string
|
||||
}) {
|
||||
return (
|
||||
<div className="grid gap-1.5">
|
||||
<label className="flex items-baseline gap-2 text-xs font-medium text-foreground" htmlFor={htmlFor}>
|
||||
{label}
|
||||
{optional && optionalLabel && (
|
||||
<span className="text-[0.65rem] font-normal text-muted-foreground">{optionalLabel}</span>
|
||||
)}
|
||||
</label>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function FieldHint({ children, error }: { children: ReactNode; error?: boolean }) {
|
||||
return (
|
||||
<p className={cn('text-[0.66rem] leading-4', error ? 'text-destructive' : 'text-muted-foreground')}>{children}</p>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue