import type { ReactNode } from 'react'
// SHARED FORM PRIMITIVES.
//
// These existed as copied class strings on every screen that had a form, which is how a product ends up with
// four slightly different inputs. They are here so the surfaces built on top of them cannot drift, and so a
// change to the focus ring is one edit rather than a search.
//
// Tokens only, no literal colours. The palette is defined once in index.css and a hard coded hex here would
// survive a rebrand and look wrong.
export const fieldCls =
'w-full rounded-md border border-line/70 bg-navy-950/60 px-3 py-1.5 text-sm text-ink outline-none placeholder:text-faint focus:border-blue/60 disabled:opacity-50'
export const btnPrimary =
'inline-flex items-center gap-1.5 rounded-md bg-blue/20 px-3 py-1.5 text-sm font-semibold text-blue-soft hover:bg-blue/30 disabled:opacity-50'
export const btnGhost =
'inline-flex items-center gap-1.5 rounded-md border border-line/70 px-3 py-1.5 text-xs font-semibold text-muted hover:text-ink disabled:opacity-40'
export const btnDanger =
'inline-flex items-center gap-1.5 rounded-md border border-line/70 px-3 py-1.5 text-xs font-semibold text-muted hover:border-risk/60 hover:text-risk disabled:opacity-40'
// Labeled wraps a control in the uppercase caption every existing form uses.
export function Labeled({
label,
hint,
children,
className = '',
}: {
label: string
hint?: string
children: ReactNode
className?: string
}) {
return (
)
}
// SettingRow is the settings layout: what the setting is, what it does in plain words, and the control.
//
// The consequence line is not decoration. "Managers and above" means nothing on its own; what an owner is
// actually choosing is who can infer the margin from a bill rate, and the row is where that gets said.
export function SettingRow({
label,
hint,
children,
}: {
label: string
hint: string
children: ReactNode
}) {
return (
{label}
{hint}
{children}
)
}
export function Select({
value,
onChange,
choices,
disabled,
className = '',
}: {
value: T
onChange: (v: T) => void
choices: { value: T; label: string }[]
disabled?: boolean
className?: string
}) {
return (
)
}
// Toggle is a switch, not a checkbox, because these are settings that take effect on click rather than on a
// later save. role and aria-checked are set so it is a switch to a screen reader too.
export function Toggle({
value,
onChange,
disabled,
label,
}: {
value: boolean
onChange: (v: boolean) => void
disabled?: boolean
label: string
}) {
return (
)
}
// Note is the one place a screen says something went wrong, went right, or needs care. One component so the
// three tones cannot be spelled three ways, and so nothing invents a fourth.
export function Note({ tone, children }: { tone: 'warn' | 'risk' | 'ok' | 'muted'; children: ReactNode }) {
const cls = {
warn: 'border-warn/40 bg-warn/10 text-warn',
risk: 'border-risk/40 bg-risk/10 text-risk',
ok: 'border-ok/40 bg-ok/10 text-ok',
muted: 'border-line/60 bg-navy-950/40 text-muted',
}[tone]
return (
{children}
)
}
// Empty is the state a list is in most of the time on a new account. It takes a reason rather than only a
// title, because "no candidates" and "no candidates YOU can see" are different facts and the second one is
// the one somebody would otherwise file a support ticket about.
export function Empty({ title, reason, action }: { title: string; reason?: string; action?: ReactNode }) {
return (