Follow-up to a85e8452, closing the three items that sweep flagged and left.
SIXTEEN BROWSER DIALOGS, GONE. Every destructive action in an otherwise fully
branded console handed off to `window.confirm` — a grey OS box with the page's
URL in it, no brand, no red on a delete, and untouchable by any story or
screenshot, which is part of why it survived this long.
They are replaced by one promise-based surface (components/dialogs.tsx) rather
than a dialog per call site. The native calls were EXPRESSIONS — `if
(!confirm(…)) return;` — threaded through mutation handlers; rewriting each into
"hold the pending action in state, render a dialog, run it from onConfirm" would
have put dialog machinery in every section file and turned each linear handler
inside out. Returning a promise keeps them the shape they already were, and it
is what let the navigation guard come along too: TanStack's `shouldBlockFn`
accepts `Promise<boolean>`. `beforeunload` necessarily stays native — a reload
is the browser's dialog to draw, and it will not wait on ours.
No warning copy was rewritten. Each message was SPLIT at its existing sentence
boundary: the question becomes the dialog's title, the consequence its body,
and "Continue?" is dropped where the affirmative button now carries the verb
("Delete", "Uninstall", "Unpair", "Stop every session"). 16 new keys, en and de
in parity at 629.
Verified by driving the real dialogs in a headless browser — all seven contract
checks pass, including the two that would be invisible until they bit: Escape
SETTLES the promise (an unsettled one would hang a mutation handler forever with
no error), and a cancelled prompt resolves null rather than "", so a caller can
still tell "backed out" from "cleared the field".
FOUR OF THE SEVEN NUMERIC FIELDS became InputNumber; three deliberately did not,
and now say why in place. The layout X/Y pair had a real defect: a screen left
of the origin has a negative coordinate, and `Number("-") || 0` rewrote the lone
minus sign to "0" before the digits could be typed. Measured on the built page:
the field can now be emptied to retype instead of snapping to its floor, and 900
in a 1..=16 field clamps to 16. The three left alone cannot take it — the grace
seconds field writes to the HOST on blur (InputNumber commits while typing, so
its clamp would race the apply), and the library's year/players are OPTIONAL,
where `value: number` has no way to say "unset" and would invent a year for
every entry without one.
The select's highlighted row moves off @unom/ui's neutral grey onto the brand
wash the nav and the preset cards already use.
The Displays story earned its keep immediately: adding `useDialogs` to that page
broke it in Storybook, because the provider was mounted in __root and nowhere
else. It belongs beside the other app-level providers in .storybook/preview.
73 lines
2.5 KiB
TypeScript
73 lines
2.5 KiB
TypeScript
// The console's Select IS @unom/ui's radix select, with the same two corrections the Tabs wrapper
|
|
// needs — @unom/ui's palette names don't all mean the same thing in this app's token set:
|
|
//
|
|
// • `text-secondary`, which the trigger uses for BOTH the placeholder and the chevron, is a
|
|
// *text* colour upstream. Here `--secondary` is a SURFACE (#241c3d dark / #ece6fb light), so the
|
|
// chevron rendered at near-zero contrast against the card it sits on — a select that looked like
|
|
// a plain box with no affordance at all. `text-muted-foreground` is this app's "quiet text".
|
|
// • `border-main` is the foreground colour — a near-white 1px border in dark, which would make a
|
|
// select shout next to the `border-input` used by every Input beside it.
|
|
//
|
|
// The trigger also defaults to `w-full` (upstream is `w-fit`) and to the Input's `rounded-md`: in
|
|
// this console a select is a form field in a stacked column, never an inline chip.
|
|
//
|
|
// Same shape as the other `components/ui/*` wrappers: adapt the shared primitive to this app's
|
|
// tokens once, rather than restyling it at every call site.
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectGroup,
|
|
SelectItem as SelectItemBase,
|
|
SelectLabel,
|
|
SelectScrollDownButton,
|
|
SelectScrollUpButton,
|
|
SelectSeparator,
|
|
SelectTrigger as SelectTriggerBase,
|
|
SelectValue,
|
|
} from "@unom/ui/form/select";
|
|
import type { ComponentProps } from "react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const SelectTrigger = ({
|
|
className,
|
|
...props
|
|
}: ComponentProps<typeof SelectTriggerBase>) => (
|
|
<SelectTriggerBase
|
|
className={cn(
|
|
"w-full rounded-md border-input data-placeholder:text-muted-foreground",
|
|
"[&_svg:not([class*='text-'])]:text-muted-foreground",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
SelectTrigger.displayName = "SelectTrigger";
|
|
|
|
// The highlighted row. Upstream paints it `bg-main/25` — a neutral grey wash, since `--main` is the
|
|
// foreground colour. Everywhere else in this console the "this is the one" wash is brand violet
|
|
// (`bg-primary/15` on the nav's hover and active states, `ring-primary` on a chosen preset card), so
|
|
// a grey row is the odd one out the moment a select sits next to any of them.
|
|
const SelectItem = ({
|
|
className,
|
|
...props
|
|
}: ComponentProps<typeof SelectItemBase>) => (
|
|
<SelectItemBase
|
|
className={cn("focus:bg-primary/15 focus:text-foreground", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
SelectItem.displayName = "SelectItem";
|
|
|
|
export {
|
|
Select,
|
|
SelectContent,
|
|
SelectGroup,
|
|
SelectItem,
|
|
SelectLabel,
|
|
SelectScrollDownButton,
|
|
SelectScrollUpButton,
|
|
SelectSeparator,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
};
|