feat(web): settings form polish — dialog layout, homepage button, no default rows, unified dropdown text (#39)
Co-authored-by: Alice <alice@prismshadow.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,11 @@ export interface OptionMenuChoice<T extends string> {
|
||||
|
||||
const PANEL_WIDTH = 288; // w-72
|
||||
|
||||
/** Row-title text follows the control's size tier, so the dropdown reads exactly like an Input of the same tier (review: dropdown text = input text); descriptions stay text-xs secondary. */
|
||||
const rowTextClass: Record<ControlSize, string> = { base: "text-base", sm: "text-xs" };
|
||||
/** Descriptions keep one step below the row title at every tier, so the title/description hierarchy survives the sm tier's text-xs titles. */
|
||||
const rowDescClass: Record<ControlSize, string> = { base: "text-xs", sm: "text-[11px]" };
|
||||
|
||||
export function OptionMenu<T extends string>({
|
||||
options,
|
||||
value,
|
||||
@@ -143,7 +148,7 @@ export function OptionMenu<T extends string>({
|
||||
>
|
||||
<span className="flex items-center justify-between gap-2">
|
||||
<span
|
||||
className={`text-sm ${
|
||||
className={`${rowTextClass[size]} ${
|
||||
opt.value === value
|
||||
? "font-medium text-gray-900 dark:text-gray-100"
|
||||
: "text-gray-700 dark:text-gray-300"
|
||||
@@ -170,7 +175,9 @@ export function OptionMenu<T extends string>({
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-xs text-gray-500 dark:text-gray-400">
|
||||
<span
|
||||
className={`mt-0.5 block ${rowDescClass[size]} text-gray-500 dark:text-gray-400`}
|
||||
>
|
||||
{opt.description}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -48,6 +48,9 @@ const CONTROL_CLASS =
|
||||
"dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:border-gray-600 " +
|
||||
"dark:focus:border-gray-400 dark:focus:ring-gray-500/30";
|
||||
|
||||
/** Menu-row text follows the control's size tier, so the dropdown reads exactly like an Input of the same tier (review: dropdown text = input text). */
|
||||
const menuTextClass: Record<ControlSize, string> = { base: "text-base", sm: "text-xs" };
|
||||
|
||||
export function Select({
|
||||
label,
|
||||
hint,
|
||||
@@ -169,7 +172,7 @@ export function Select({
|
||||
aria-selected={o.value === current}
|
||||
disabled={o.disabled}
|
||||
onClick={() => pick(o.value)}
|
||||
className={`flex w-full items-center px-3 py-1.5 text-left text-sm transition-colors duration-150 disabled:opacity-50 ${
|
||||
className={`flex w-full items-center px-3 py-1.5 text-left ${menuTextClass[size]} transition-colors duration-150 disabled:opacity-50 ${
|
||||
o.value === current
|
||||
? "bg-gray-100 font-medium text-gray-900 dark:bg-gray-800 dark:text-gray-100"
|
||||
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* iOS-style toggle switch: a `button` with `role="switch"` + `aria-checked` (a native
|
||||
* button is keyboard-operable out of the box — Space/Enter activate it — and is labelable
|
||||
* content, so clicking an enclosing `<label>`'s text toggles it too). Sliding knob with a
|
||||
* color/transform transition; the on-state follows the theme accent variable (same source
|
||||
* as Button primary), the off-state is gray; dark-mode aware; disabled dims and blocks.
|
||||
* Sized for compact (sm) form rows, matching the dialogs' controls.
|
||||
*/
|
||||
import type { ButtonHTMLAttributes } from "react";
|
||||
|
||||
export interface SwitchProps extends Omit<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
"onChange" | "type" | "role"
|
||||
> {
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
export function Switch({ checked, onChange, disabled, className, ...rest }: SwitchProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
disabled={disabled}
|
||||
onClick={() => onChange(!checked)}
|
||||
className={
|
||||
"relative inline-flex h-5 w-9 shrink-0 items-center rounded-full " +
|
||||
"transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-gray-400/30 " +
|
||||
"disabled:cursor-not-allowed disabled:opacity-60 " +
|
||||
(checked ? "bg-[var(--accent-bg)]" : "bg-gray-300 dark:bg-gray-600") +
|
||||
` ${className ?? ""}`
|
||||
}
|
||||
{...rest}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={`inline-block size-4 transform rounded-full bg-white shadow transition-transform duration-200 ${
|
||||
checked ? "translate-x-[18px]" : "translate-x-0.5"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user