Setting control
The rows of a settings page, a switch, a field, a select or any kit control, each with the save state beside it.
On this page
Anchor watch
Reaching the crew
Code
import { SectionHeading } from "@bosunkit/components/section-heading";
import {
SettingField,
SettingSelect,
SettingToggle,
type SaveState,
type SettingSelectOption,
} from "@bosunkit/components/setting-control";
import { Anchor, Radio } from "lucide-react";
import {
useEffect,
useId,
useRef,
useState,
type Dispatch,
type ReactElement,
type ReactNode,
type RefObject,
type SetStateAction,
} from "react";
export const description: string =
"An anchor watch settings page: switches that save, fail, refuse input and wait on a save, a number field sized " +
"to its digits, text, secret and read-only fields, and a select, each save chip on its row's edge.";
interface Saved<Value> {
readonly value: Value;
readonly state: SaveState;
}
/** How long the example's save takes to come back. */
const SAVE_MS: number = 900;
/**
* A setting whose save comes back after SAVE_MS: it lands, or it fails and the control returns to the value that is
* saved on the vessel.
*/
function useSetting<Value>(initial: Value, fails: boolean = false): [Saved<Value>, (next: Value) => void] {
const [setting, setSetting]: [Saved<Value>, Dispatch<SetStateAction<Saved<Value>>>] = useState<Saved<Value>>({
value: initial,
state: "idle",
});
const saved: RefObject<Value> = useRef<Value>(initial);
const timer: RefObject<ReturnType<typeof setTimeout> | undefined> = useRef<ReturnType<typeof setTimeout> | undefined>(
undefined,
);
useEffect((): (() => void) => {
return (): void => {
clearTimeout(timer.current);
};
}, []);
function save(next: Value): void {
setSetting({ value: next, state: "saving" });
clearTimeout(timer.current);
// Stands in for the PUT to the vessel's settings.
timer.current = setTimeout((): void => {
if (fails) {
setSetting({ value: saved.current, state: "error" });
return;
}
saved.current = next;
setSetting({ value: next, state: "saved" });
}, SAVE_MS);
}
return [setting, save];
}
const LANGUAGES: readonly SettingSelectOption[] = [
{ value: "en", label: "English" },
{ value: "nl", label: "Nederlands" },
{ value: "fr", label: "Français" },
{ value: "de", label: "Deutsch" },
{ value: "es", label: "Español" },
{ value: "it", label: "Italiano" },
];
function ignore(): void {
// A disabled or read-only control saves nothing.
}
function Group({ icon, title, children }: { icon: ReactNode; title: string; children: ReactNode }): ReactElement {
const headingId: string = useId();
return (
<section aria-labelledby={headingId} style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
<SectionHeading level={3} id={headingId} icon={icon}>
{title}
</SectionHeading>
{children}
</section>
);
}
export default function Showcase(): ReactElement {
const [alarm, saveAlarm]: [Saved<boolean>, (next: boolean) => void] = useSetting<boolean>(true);
const [phone, savePhone]: [Saved<boolean>, (next: boolean) => void] = useSetting<boolean>(false, true);
const [radius, saveRadius]: [Saved<string>, (next: string) => void] = useSetting<string>("45");
const [message, saveMessage]: [Saved<string>, (next: string) => void] = useSetting<string>("");
const [language, saveLanguage]: [Saved<string>, (next: string) => void] = useSetting<string>("en");
const [token, saveToken]: [Saved<string>, (next: string) => void] = useSetting<string>("7311029847:AAF-anchor");
return (
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-xl)", width: "100%" }}>
<Group icon={<Anchor />} title="Anchor watch">
<SettingToggle
label="Anchor alarm"
description="Sound the alarm on deck and below when the vessel leaves the swing circle."
checked={alarm.value}
state={alarm.state}
onCheckedChange={saveAlarm}
/>
<SettingField
label="Swing radius"
description="How far the vessel may move from the anchor position before the alarm sounds."
type="number"
value={radius.value}
unit="m"
maxDigits={3}
placeholder="45"
state={radius.state}
onSave={saveRadius}
/>
<SettingField
label="Alarm announcement"
description="Spoken after the alarm tone; leave it empty for the standard announcement."
value={message.value}
placeholder="Anchor drag. All hands on deck."
state={message.state}
onSave={saveMessage}
/>
<SettingSelect
label="Announcement language"
description="The language the alarm is spoken in on deck."
value={language.value}
options={LANGUAGES}
state={language.state}
onSave={saveLanguage}
/>
</Group>
<Group icon={<Radio />} title="Reaching the crew">
<SettingToggle
label="Call the skipper's phone"
description="Ring Maya's phone if nobody acknowledges within two minutes. In this example the save fails."
checked={phone.value}
state={phone.state}
onCheckedChange={savePhone}
/>
<SettingToggle
label="Send the position to the crew"
description="Message the anchor position to the crew's phones every hour. In this example the save is on its way."
checked
state="saving"
onCheckedChange={ignore}
/>
<SettingToggle
label="Shore power alarm"
description="Available once the shore power monitor reports on the network."
checked={false}
state="idle"
onCheckedChange={ignore}
disabled
/>
<SettingField
label="Telegram bot token"
description="The token Bosun sends watch alerts with."
type="password"
value={token.value}
state={token.state}
onSave={saveToken}
/>
<SettingField
label="MMSI"
description="From the vessel's registration; change it on the vessel profile."
value="224123456"
state="idle"
onSave={ignore}
readOnly
/>
</Group>
</div>
);
}
A setting control is one row of a settings page: the setting's name and what it does on the left, the control on the
right, ending at the row's right edge, and the save state under it. The switch, the field and the select save as the
crew change them, with no save button, and each shows the same save chip so the crew see the write land or fail
without leaving the row. A secret, a key in a password field or a password in a setting row, is the exception: it
sits in a form of its own and saves when the crew press Save. The chip sits on the row's bottom edge, in the space between two rows, so nothing in the row
moves when a save starts, lands or fails. The module also exports the chip, and SettingRow, the same row for any
other kit control.
Examples
Toggle
The control changes at once and the chip follows the write: saving while it is in flight, then saved, which fades.
Code
import { SettingToggle, type SaveState } from "@bosunkit/components/setting-control";
import { useEffect, useRef, useState, type Dispatch, type ReactElement, type RefObject, type SetStateAction } from "react";
export const description: string =
"A switch that saves as it is pressed: it turns at once, shows saving, then saved.";
export default function Toggle(): ReactElement {
const [checked, setChecked]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
const [state, setState]: [SaveState, Dispatch<SetStateAction<SaveState>>] = useState<SaveState>("idle");
const timer: RefObject<ReturnType<typeof setTimeout> | undefined> = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect((): (() => void) => (): void => {
clearTimeout(timer.current);
}, []);
function toggle(next: boolean): void {
setChecked(next);
setState("saving");
clearTimeout(timer.current);
// Stands in for the PUT to the vessel's settings.
timer.current = setTimeout((): void => {
setState("saved");
}, 700);
}
return (
<SettingToggle
label="Anchor watch"
description="Sound the alarm when the vessel moves outside her swing circle."
checked={checked}
state={state}
onCheckedChange={toggle}
/>
);
}
Toggle saving
Code
import { SettingToggle } from "@bosunkit/components/setting-control";
import type { ReactElement } from "react";
export const description: string = "The switch while its change is being saved.";
function ignore(): void {
// The example saves nothing.
}
export default function ToggleSaving(): ReactElement {
return <SettingToggle label="Enabled" checked state="saving" onCheckedChange={ignore} />;
}
Field
Code
import { SettingField, type SaveState } from "@bosunkit/components/setting-control";
import { useEffect, useRef, useState, type Dispatch, type ReactElement, type RefObject, type SetStateAction } from "react";
export const description: string = "A text field that saves on blur or Enter, with no save button.";
export default function Field(): ReactElement {
const [value, setValue]: [string, Dispatch<SetStateAction<string>>] = useState<string>("Althea");
const [state, setState]: [SaveState, Dispatch<SetStateAction<SaveState>>] = useState<SaveState>("idle");
const timer: RefObject<ReturnType<typeof setTimeout> | undefined> = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect((): (() => void) => (): void => {
clearTimeout(timer.current);
}, []);
function save(next: string): void {
setValue(next);
setState("saving");
clearTimeout(timer.current);
// Stands in for the PUT to the vessel's settings.
timer.current = setTimeout((): void => {
setState("saved");
}, 700);
}
return (
<SettingField
label="Vessel name"
description="How bosun names the vessel on the radio and in messages."
value={value}
state={state}
onSave={save}
/>
);
}
Number field
A field for a number takes maxDigits, the most characters its value can have. It is then just wide enough for them,
its digits right-aligned in equal widths, with the unit inside its right edge.
Code
import { SettingField, type SaveState } from "@bosunkit/components/setting-control";
import {
useEffect,
useRef,
useState,
type Dispatch,
type ReactElement,
type RefObject,
type SetStateAction,
} from "react";
export const description: string =
"A number field sized to its three digits, right-aligned, with its unit inside its right edge.";
export default function NumberField(): ReactElement {
const [value, setValue]: [string, Dispatch<SetStateAction<string>>] = useState<string>("45");
const [state, setState]: [SaveState, Dispatch<SetStateAction<SaveState>>] = useState<SaveState>("idle");
const timer: RefObject<ReturnType<typeof setTimeout> | undefined> = useRef<ReturnType<typeof setTimeout> | undefined>(
undefined,
);
useEffect(
(): (() => void) => (): void => {
clearTimeout(timer.current);
},
[],
);
function save(next: string): void {
setValue(next);
setState("saving");
clearTimeout(timer.current);
// Stands in for the PUT to the vessel's settings.
timer.current = setTimeout((): void => {
setState("saved");
}, 700);
}
return (
<SettingField
label="Swing radius"
description="How far the vessel may move from the anchor position before the alarm sounds."
type="number"
value={value}
unit="m"
maxDigits={3}
state={state}
onSave={save}
/>
);
}
Select
Code
import { SettingSelect, type SaveState } from "@bosunkit/components/setting-control";
import { useEffect, useRef, useState, type Dispatch, type ReactElement, type RefObject, type SetStateAction } from "react";
export const description: string = "A list of choices for a setting that takes one of a few values, saved when chosen.";
export default function Select(): ReactElement {
const [value, setValue]: [string, Dispatch<SetStateAction<string>>] = useState<string>("en");
const [state, setState]: [SaveState, Dispatch<SetStateAction<SaveState>>] = useState<SaveState>("idle");
const timer: RefObject<ReturnType<typeof setTimeout> | undefined> = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect((): (() => void) => (): void => {
clearTimeout(timer.current);
}, []);
function save(next: string): void {
setValue(next);
setState("saving");
clearTimeout(timer.current);
// Stands in for the PUT to the vessel's settings.
timer.current = setTimeout((): void => {
setState("saved");
}, 700);
}
return (
<SettingSelect
label="Vessel default language"
description="Used when a crew member has not chosen a language of their own."
value={value}
options={[
{ value: "en", label: "English" },
{ value: "nl", label: "Nederlands" },
{ value: "fr", label: "Français" },
{ value: "de", label: "Deutsch" },
{ value: "es", label: "Español" },
{ value: "it", label: "Italiano" },
]}
state={state}
onSave={save}
/>
);
}
Saved
Code
import { SaveChip } from "@bosunkit/components/setting-control";
import type { ReactElement } from "react";
export const description: string = "The green chip a control shows once its change is saved, which fades.";
export default function Saved(): ReactElement {
return <SaveChip state="saved" />;
}
Save failed
When a save fails, the control goes back to the saved value, so it never shows a setting the vessel does not have, and the amber chip stays until the next attempt.
Code
import { SettingToggle, type SaveState } from "@bosunkit/components/setting-control";
import { useEffect, useRef, useState, type Dispatch, type ReactElement, type RefObject, type SetStateAction } from "react";
export const description: string =
"A switch whose save fails: it turns back to the saved value and the amber chip stays until the next attempt.";
export default function SaveFailed(): ReactElement {
const [checked, setChecked]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(true);
const [state, setState]: [SaveState, Dispatch<SetStateAction<SaveState>>] = useState<SaveState>("idle");
const timer: RefObject<ReturnType<typeof setTimeout> | undefined> = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect((): (() => void) => (): void => {
clearTimeout(timer.current);
}, []);
function toggle(next: boolean): void {
const previous: boolean = checked;
setChecked(next);
setState("saving");
clearTimeout(timer.current);
// Stands in for a PUT the vessel rejects: the switch returns to the value that is still saved.
timer.current = setTimeout((): void => {
setChecked(previous);
setState("error");
}, 900);
}
return (
<SettingToggle
label="Shore power alarm"
description="Press the switch: this example's save always fails."
checked={checked}
state={state}
onCheckedChange={toggle}
/>
);
}
Nesting kit controls
SettingRow hosts a kit control in the same row: the vessel's MMSI, its home port, a password. The row names the
control with its label, saves when the control commits a value (focus leaves it, or Enter is pressed in an input),
and never saves one that fails its rules. It keeps what the crew typed and says under the control why nothing was
saved, so a Wi-Fi password that misses a rule is not sent to the vessel and is not lost either. Each control keeps
its own help: the MMSI says which station the number names, the password lists its rules.
The label aligns with the first field, with help and actions beneath that field. Hosted controls and saved-secret summaries share a column width. When the row is too narrow for two columns, the control fills the width below its label, keeping the label, field and guidance on the same left edge.
A secret is a form of its own with a Save button, and is saved when the crew press Save or Enter, never when focus leaves the field: a new guest Wi-Fi password changes the network every guest is on, so the crew decide when, and a password manager offers to save what a form submits. A password the vessel cannot hand back is not shown again. The row says that a password is saved and offers a change button; the field appears empty when the crew choose to change it, and goes away, emptied, once the save lands. The crew network's password takes WPA2's lengths, 8 to 63 characters, leaves out the vessel's and the networks' names, and tells the password managers the same limits, so a generated password is accepted at the first try; it is filed under the network's name. The guest network's password is checked against the same rules. A refusal names the rule and what to do: "Not saved. The password has 6 characters. Use at least 8." The marina's password has no rules of ours to check.
The guest network's password is meant to be handed on, so the vessel hands it back and the row shows it: the field
is a password input with purpose="shared", plain text with no show button, so
the password stays in view before, during and after Save. It keeps the checklist, the refusals and the Save button,
and the password managers' opt-outs, since it is not a login of the crew's. Beside it are a Copy password button and
a QR code a guest's phone joins from (WIFI:T:WPA;S:Althea Guest;P:…;;, drawn by
QR code). The code holds the saved password, not a draft being typed, and is drawn dark
on a light panel in every theme, so a phone's camera reads it in Night as well. The crew
network's password, which the instruments and the crew's devices use, stays hidden behind the show button while it is
typed and is never shown again once saved.
Vessel identity
Wi-Fi
Code
import { Button } from "@bosunkit/components/button";
import { isValidMmsi, MmsiInput } from "@bosunkit/components/mmsi-input";
import { checkNewPassword, PasswordInput } from "@bosunkit/components/password-input";
import { PositionInput, type Position } from "@bosunkit/components/position-input";
import { QrCode, wifiNetworkText } from "@bosunkit/components/qr-code";
import { SectionHeading } from "@bosunkit/components/section-heading";
import { SettingRow, type SaveState, type SettingRowControl } from "@bosunkit/components/setting-control";
import type { MessageKey, Translator } from "@bosunkit/i18n";
import { useTranslator } from "@bosunkit/provider";
import { Anchor, Copy, Wifi } from "lucide-react";
import {
useEffect,
useId,
useRef,
useState,
type Dispatch,
type ReactElement,
type ReactNode,
type RefObject,
type SetStateAction,
} from "react";
export const description: string =
"Kit controls inside setting rows: the vessel's MMSI and home port, and its Wi-Fi passwords. A value that fails " +
"its rules is not saved and the row says why. The guest password is shown, with a copy button and a code a " +
"phone's camera joins from; the crew network's password is never shown again once saved.";
/** How long the example's save takes to come back. */
const SAVE_MS: number = 900;
/**
* A WPA2 passphrase: 8 to 63 printable ASCII characters, without the vessel's or the network's name. Lengths and a
* character set, as NIST SP 800-63B-4 asks; no character classes.
*/
const GUEST_RULES: {
readonly minLength: number;
readonly maxLength: number;
readonly contextWords: readonly string[];
} = { minLength: 8, maxLength: 63, contextWords: ["Althea", "Althea Guest"] };
const WPA2_RULES: string = "minlength: 8; maxlength: 63; allowed: ascii-printable;";
/** The crew network's password: the same lengths, without the vessel's or either network's name. */
const CREW_RULES: {
readonly minLength: number;
readonly maxLength: number;
readonly contextWords: readonly string[];
} = { minLength: 8, maxLength: 63, contextWords: ["Althea", "Althea Guest"] };
/** The guest password, shown so the crew can read it out, copy it, or let a guest's phone scan it. */
function GuestJoin({ password }: { password: string }): ReactElement {
const [copied, setCopied]: [string, Dispatch<SetStateAction<string>>] = useState<string>("");
return (
<div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "var(--space-md)" }}>
<QrCode
value={wifiNetworkText({ ssid: "Althea Guest", password })}
label="Code to join Althea Guest with a phone's camera"
size="8em"
/>
<div style={{ display: "grid", gap: "var(--space-xs)", justifyItems: "start" }}>
<Button
type="button"
size="sm"
onClick={(): void => {
navigator.clipboard.writeText(password).then(
(): void => {
setCopied("Copied the guest password.");
},
(): void => {
setCopied("Couldn't copy. Select the password in the field and copy it.");
},
);
}}
>
<Copy aria-hidden="true" size={14} /> Copy password
</Button>
<span role="status" style={{ fontSize: "0.75em" }}>
{copied}
</span>
</div>
</div>
);
}
/** Whether two positions are the same place; a position the fields do not hold whole is `null`. */
function samePosition(first: Position | null, second: Position | null): boolean {
return (
first === second ||
(first !== null && second !== null && first.latitude === second.latitude && first.longitude === second.longitude)
);
}
/** A setting's save lifecycle: a save comes back as saved after SAVE_MS, standing in for the PUT to the vessel. */
function useSave<Value>(onLanded: (saved: Value) => void): [SaveState, (next: Value) => void] {
const [state, setState]: [SaveState, Dispatch<SetStateAction<SaveState>>] = useState<SaveState>("idle");
const timer: RefObject<ReturnType<typeof setTimeout> | undefined> = useRef<ReturnType<typeof setTimeout> | undefined>(
undefined,
);
useEffect((): (() => void) => {
return (): void => {
clearTimeout(timer.current);
};
}, []);
function save(next: Value): void {
setState("saving");
clearTimeout(timer.current);
timer.current = setTimeout((): void => {
onLanded(next);
setState("saved");
}, SAVE_MS);
}
return [state, save];
}
function Group({ icon, title, children }: { icon: ReactNode; title: string; children: ReactNode }): ReactElement {
const headingId: string = useId();
return (
<section aria-labelledby={headingId} style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
<SectionHeading level={3} id={headingId} icon={icon}>
{title}
</SectionHeading>
{children}
</section>
);
}
export default function NestingControls(): ReactElement {
const translator: Translator<MessageKey> = useTranslator();
const [mmsi, setMmsi]: [string, Dispatch<SetStateAction<string>>] = useState<string>("235012345");
const [mmsiState, saveMmsi]: [SaveState, (next: string) => void] = useSave<string>(setMmsi);
const [homePort, setHomePort]: [Position | null, Dispatch<SetStateAction<Position | null>>] =
useState<Position | null>({ latitude: 50.8472, longitude: -1.3 });
const [homePortState, saveHomePort]: [SaveState, (next: Position | null) => void] = useSave<Position | null>(
setHomePort,
);
// The guest password is handed to guests, so the vessel hands it back and the row shows it.
const [guestPassword, setGuestPassword]: [string, Dispatch<SetStateAction<string>>] =
useState<string>("harbor-lantern-41");
const [guestState, saveGuest]: [SaveState, (next: string) => void] = useSave<string>(setGuestPassword);
// The crew network's password is never handed back, so the page only knows whether one is saved.
const [crewSaved, setCrewSaved]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(true);
const [crewState, saveCrew]: [SaveState, (next: string) => void] = useSave<string>((): void => {
setCrewSaved(true);
});
const [marinaSaved, setMarinaSaved]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(true);
const [marinaState, saveMarina]: [SaveState, (next: string) => void] = useSave<string>((): void => {
setMarinaSaved(true);
});
return (
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-xl)", width: "100%" }}>
<Group icon={<Anchor />} title="Vessel identity">
<SettingRow<string>
label="MMSI"
description="The vessel's own number, from its radio licence."
value={mmsi}
state={mmsiState}
validate={(draft: string): string | undefined =>
isValidMmsi(draft) ? undefined : "Type all nine digits of the vessel's MMSI."
}
onSave={saveMmsi}
>
{(control: SettingRowControl<string>): ReactElement => (
<MmsiInput
id={control.id}
value={control.draft}
onValueChange={control.onDraftChange}
disabled={control.disabled}
aria-describedby={control.describedBy}
/>
)}
</SettingRow>
<SettingRow<Position | null>
label="Home port"
description="Where the vessel is kept; the anchor alarm and the tide table start from here."
value={homePort}
isEqual={samePosition}
state={homePortState}
validate={(draft: Position | null): string | undefined =>
draft === null ? "Give the latitude and the longitude, both whole." : undefined
}
onSave={saveHomePort}
>
{(control: SettingRowControl<Position | null>): ReactElement => (
<PositionInput
id={control.id}
value={control.draft}
onValueChange={control.onDraftChange}
disabled={control.disabled}
/>
)}
</SettingRow>
</Group>
<Group icon={<Wifi />} title="Wi-Fi">
<SettingRow<string>
label="Guest Wi-Fi password"
description="The password visitors use on the Althea Guest network; shown, so the crew can hand it on."
value={guestPassword}
saved={false}
state={guestState}
validate={(draft: string): string | undefined => checkNewPassword(draft, GUEST_RULES, translator).refusal}
onSave={saveGuest}
>
{(control: SettingRowControl<string>): ReactElement => (
<>
<PasswordInput
id={control.id}
purpose="shared"
minLength={GUEST_RULES.minLength}
maxLength={GUEST_RULES.maxLength}
contextWords={GUEST_RULES.contextWords}
value={control.draft}
onValueChange={control.onDraftChange}
disabled={control.disabled}
aria-describedby={control.describedBy}
/>
<GuestJoin password={guestPassword} />
</>
)}
</SettingRow>
<SettingRow<string>
label="Crew Wi-Fi password"
description="The password of the Althea network the crew's devices and the instruments use."
value=""
saved={crewSaved}
savedSummary="A password is saved"
changeLabel="Change password"
state={crewState}
validate={(draft: string): string | undefined => checkNewPassword(draft, CREW_RULES, translator).refusal}
onSave={saveCrew}
>
{(control: SettingRowControl<string>): ReactElement => (
<PasswordInput
id={control.id}
purpose="new"
username="Althea"
passwordRules={WPA2_RULES}
minLength={CREW_RULES.minLength}
maxLength={CREW_RULES.maxLength}
contextWords={CREW_RULES.contextWords}
value={control.draft}
onValueChange={control.onDraftChange}
disabled={control.disabled}
aria-describedby={control.describedBy}
/>
)}
</SettingRow>
<SettingRow<string>
label="Upstream Wi-Fi password"
description="The password of Hamble Point Marina's network, which bosun joins for its internet."
value=""
saved={marinaSaved}
savedSummary="A password is saved"
changeLabel="Change password"
state={marinaState}
onSave={saveMarina}
>
{(control: SettingRowControl<string>): ReactElement => (
<PasswordInput
id={control.id}
purpose="current"
username="Hamble Point Marina"
value={control.draft}
onValueChange={control.onDraftChange}
disabled={control.disabled}
aria-describedby={control.describedBy}
/>
)}
</SettingRow>
</Group>
</div>
);
}
Props
SaveChip
The chip that says what just happened to a change. It shows nothing while the control is idle.
| Prop | Type | Default | Description |
|---|---|---|---|
| staterequired | SaveState | – | Where the save is: nothing is shown while idle. |
SettingToggle
A switch for a setting that is on or off.
| Prop | Type | Default | Description |
|---|---|---|---|
| labelrequired | string | – | The setting's name; it also names the switch. |
| description | string | undefined | – | What the setting does, under the name. |
| checkedrequired | boolean | – | Whether the setting is on. |
| staterequired | SaveState | – | Where the save of the last change is. |
| onCheckedChange | SwitchProps["onCheckedChange"] | – | Called with the value the crew chose. |
| disabled | boolean | undefined | – | Whether the switch refuses input. |
SettingField
An input that saves on blur or Enter. With maxDigits it is sized to a number of that many characters. With
type="password" it holds a key or token another service issued, such as a Telegram bot token: a
password input with purpose="secret", which the password managers leave alone,
in a form of its own that saves when the crew press Save.
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | undefined | – | The input's id, which the label points at; setting- and the label's words, hyphenated, by default. |
| labelrequired | string | – | The setting's name; it also names the field. |
| description | string | undefined | – | What the setting does, under the name. |
| valuerequired | string | – | The saved value. |
| staterequired | SaveState | – | Where the save of the last change is. |
| onSaverequired | (next: string) => void | – | Called on blur or Enter (on Save for a password), only when the value actually changed. |
| type | "text" | "number" | "password" | undefined | "text" | The kind of input; text by default. password is a secret another service issued, such as an API key or a bot
token: a PasswordInput with purpose="secret", which the password managers leave alone, in a form of its own
saved by its Save button rather than on blur. |
| placeholder | string | undefined | – | The hint shown in the empty field. |
| unit | string | undefined | – | The unit: after the field, or inside its right edge when maxDigits sizes the field. |
| maxDigits | number | undefined | – | The most characters the value can have, a sign or decimal point counted as one: the field is then just wide enough for them, its digits right-aligned in equal widths, as a number in a column of figures is. Full width otherwise. |
| readOnly | boolean | undefined | – | Whether the field shows the value without letting the crew change it. |
SettingSelect
A native select for a setting that takes one of a few values. It calls onSave only when the chosen value differs
from the saved one.
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | undefined | – | The select's id, which the label points at; setting- and the label's words, hyphenated, by default. |
| labelrequired | string | – | The setting's name; it also names the list. |
| description | string | undefined | – | What the setting does, under the name. |
| valuerequired | string | – | The saved value. |
| optionsrequired | readonly SettingSelectOption[] | – | The choices. |
| staterequired | SaveState | – | Where the save of the last change is. |
| onSaverequired | (next: string) => void | – | Called with the chosen value, only when it differs from the saved one. |
SettingRow
A row that hosts any kit control. Its child is a function that receives the id for the control's input, the
draft and onDraftChange, commit for a control whose own change is the commit, disabled and the id of the
refusal for aria-describedby. A password the crew choose, with its rules and its checklist, goes in a
SettingRow, so every control with its own help, rules and shape fits a row the same way.
| Prop | Type | Default | Description |
|---|---|---|---|
| labelrequired | string | – | The setting's name; it also names the hosted control. |
| description | string | undefined | – | What the setting does, under the name. |
| valuerequired | Value | – | The saved value; for a secret the vessel does not hand back, the empty value. |
| staterequired | SaveState | – | Where the save of the last change is. |
| onSaverequired | (next: Value) => void | – | Called with a value that changed and that validate accepts; never with an invalid one. |
| validate | ((draft: Value) => string | undefined) | undefined | – | Why a value cannot be saved, in the crew's words and ending with what to do, or undefined when it can. The row shows it under the control as "Not saved. ..." and keeps the draft. |
| isEqual | ((first: Value, second: Value) => boolean) | undefined | Object.is | Whether two values are the same setting; Object.is by default. |
| saved | boolean | undefined | – | Whether the vessel holds a value it never hands back, such as a password; set it, true or false, for any secret.
The control then sits in a form of its own with a Save button, saved only when the form is submitted, so a
password manager sees the password submitted and the crew decide when the network changes. Once a value is saved
the control is hidden behind savedSummary and a changeLabel button until the crew choose to change it, and it
is empty again, and hidden, once a save lands. |
| savedSummary | string | undefined | – | What the row says in place of the control while a secret is saved: "A password is saved". |
| changeLabel | string | undefined | – | The button that brings the control back to replace a saved secret: "Change password". |
| disabled | boolean | undefined | – | Whether the hosted control refuses input. |
| childrenrequired | (control: SettingRowControl<Value>) => ReactNode | – | The kit control to host, drawn with the row's id, draft and callbacks. |
Related
- Setting chips: the list setting that shares the save chip.
- Select: the list of choices that opens from a button.
- Password input, MMSI input and Position input: the controls the example nests.