Desktop 1: preferences window
The Settings window of a Tauri or Electron app: toolbar tabs on macOS, a side nav with setting cards on Windows. Launch at login, menu bar, appearance, a global shortcut recorder and update settings that apply as they change.
// Ballmac UI: Desktop 1. https://ui.ballmac.com/blocks/desktop-1
"use client"
import * as React from "react"
import { Tabs as TabsPrimitive } from "radix-ui"
import { Keyboard, Palette, RefreshCw, Settings2, SlidersHorizontal } from "lucide-react"
import { AppWindow, AppWindowContent, AppWindowTitleBar } from "@/components/ballmac/app-window"
import { Button } from "@/components/ballmac/button"
import { SegmentedControl, SegmentedControlItem } from "@/components/ballmac/segmented-control"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ballmac/select"
import { Switch } from "@/components/ballmac/switch"
import { useDirection } from "@/lib/ballmac/direction"
import { useFeel, type FeelId } from "@/lib/ballmac/feel"
import { formatShortcut, shortcutFromEvent, usesMacKeys } from "@/lib/ballmac/shortcut"
import { cn } from "@/lib/utils"
type Desktop1Values = {
launchAtLogin: boolean
showInMenuBar: boolean
appearance: "system" | "light" | "dark"
density: "comfortable" | "compact"
/** The global shortcut as key names joined with "+", for example "Mod+Shift+Space". Mod is ⌘ on a Mac and Ctrl elsewhere. */
shortcut: string
autoCheck: boolean
autoInstall: boolean
channel: "stable" | "beta"
crashReports: boolean
}
type Desktop1Pane = {
id: string
label: string
icon: React.ReactNode
content: React.ReactNode
}
type Desktop1Props = Omit<React.ComponentProps<"div">, "onChange" | "defaultValue"> & {
/** Which platform the window handles like. Defaults to the page's feel. */
feel?: FeelId
/** Draw the window frame. Turn off inside a real Tauri or Electron window that already has one. */
frame?: boolean
/** The app name, used in copy such as "Open Acme at login". */
appName?: string
/** The settings when the window opens. */
defaultValues?: Desktop1Values
/** Controlled settings. Pass with onValuesChange. */
values?: Desktop1Values
/** Called with every change. Settings apply immediately, as they do in desktop apps: there is no Save button. */
onValuesChange?: (values: Desktop1Values) => void
/** The pane shown first. */
defaultPane?: string
/** More panes after the built-in ones, for your own settings. */
extraPanes?: Desktop1Pane[]
/** The version shown in the Updates pane. */
version?: string
/** Called by "Check now" in the Updates pane. */
onCheckForUpdates?: () => void
/** Called by "Reset all settings" after the built-in values are restored. */
onReset?: () => void
/** Window controls, passed to App Window. */
onClose?: () => void
onMinimize?: () => void
}
const defaults: Desktop1Values = {
launchAtLogin: true,
showInMenuBar: true,
appearance: "system",
density: "comfortable",
shortcut: "Mod+Shift+Space",
autoCheck: true,
autoInstall: false,
channel: "stable",
crashReports: true,
}
/**
* A button that records the next key combination. Escape cancels, Backspace or Delete clears. A combination needs at least one
* modifier, so typing a letter cannot steal it from every other app.
*/
function ShortcutRecorder({ value, onChange, feel, labelledBy }: { value: string; onChange: (v: string) => void; feel: FeelId; labelledBy: string }) {
const [recording, setRecording] = React.useState(false)
const mac = usesMacKeys(feel)
return (
<div className="flex items-center gap-2">
<button
type="button"
data-slot="desktop-1-shortcut"
aria-labelledby={labelledBy}
aria-describedby={`${labelledBy}-hint`}
aria-pressed={recording}
onClick={() => setRecording((r) => !r)}
onBlur={() => setRecording(false)}
onKeyDown={(e) => {
if (!recording) return
if (e.key === "Tab") return setRecording(false)
e.preventDefault()
if (e.key === "Escape") return setRecording(false)
if (e.key === "Backspace" || e.key === "Delete") {
onChange("")
return setRecording(false)
}
const next = shortcutFromEvent(e, mac)
if (next) {
onChange(next)
setRecording(false)
}
}}
className={cn(
"border-input bg-background inline-flex h-[var(--bm-control-h-sm,calc(var(--spacing)*8))] min-w-32 items-center justify-center rounded-md border px-3 font-mono text-[13px] tabular-nums outline-none",
"focus-visible:ring-[3px] focus-visible:ring-ring/50",
recording && "border-ring ring-ring/30 text-muted-foreground ring-[3px]"
)}
>
<span dir="ltr">{recording ? "Type a shortcut…" : formatShortcut(value, feel) || "Record shortcut"}</span>
</button>
<span id={`${labelledBy}-hint`} className="sr-only">
{recording ? "Press the new key combination. Escape cancels, Delete clears." : "Press to record a new shortcut."}
</span>
{value && !recording ? (
<Button variant="ghost" size="sm" onClick={() => onChange("")}>
Clear
</Button>
) : null}
</div>
)
}
const FeelContext = React.createContext<FeelId>("web")
/** A group of setting rows. Windows draws each row as its own card; macOS, iOS and the web group them in one rounded box. */
function Group({ title, children }: { title?: string; children: React.ReactNode }) {
const feel = React.useContext(FeelContext)
const id = React.useId()
return (
<section aria-labelledby={title ? id : undefined} className="grid gap-2">
{title ? (
<h3 id={id} className={cn("text-sm font-semibold", feel === "ios" && "text-muted-foreground px-4 text-xs font-normal uppercase")}>
{title}
</h3>
) : null}
<div className={cn(feel === "windows" ? "grid gap-1" : "bg-card divide-border divide-y overflow-hidden rounded-lg border", feel === "macos" && "bg-muted/40")}>{children}</div>
</section>
)
}
/** One setting: its name and an optional line of help on the start side, the control on the end side. */
function Row({ label, description, children, id: idProp }: { label: string; description?: string; children: (id: string) => React.ReactNode; id?: string }) {
const feel = React.useContext(FeelContext)
const auto = React.useId()
const id = idProp ?? auto
return (
<div
data-slot="desktop-1-row"
className={cn("flex flex-wrap items-center justify-between gap-x-6 gap-y-2", feel === "windows" ? "bg-card rounded-md border px-4 py-3.5" : "px-4 py-3")}
>
<div className="min-w-0 flex-1 basis-48">
<p id={id} className="text-sm">
{label}
</p>
{description ? (
<p id={`${id}-desc`} className="text-muted-foreground mt-0.5 text-xs text-pretty">
{description}
</p>
) : null}
</div>
<div className="flex shrink-0 items-center">{children(id)}</div>
</div>
)
}
function Desktop1({
feel: feelProp,
frame = true,
appName = "Acme",
defaultValues = defaults,
values: valuesProp,
onValuesChange,
defaultPane = "general",
extraPanes = [],
version = "2.4.1",
onCheckForUpdates,
onReset,
onClose,
onMinimize,
className,
...props
}: Desktop1Props) {
const page = useFeel().feel
const feel = feelProp ?? page
const dir = useDirection()
const [internal, setInternal] = React.useState(defaultValues)
const v = valuesProp ?? internal
const [pane, setPane] = React.useState(defaultPane)
const set = <K extends keyof Desktop1Values>(k: K, value: Desktop1Values[K]) => {
const next = { ...v, [k]: value }
if (!valuesProp) setInternal(next)
onValuesChange?.(next)
}
const sideNav = feel === "windows"
const settingsApp = feel === "macos" ? "System Settings" : feel === "windows" ? "Settings" : "settings"
const panes: Desktop1Pane[] = [
{
id: "general",
label: "General",
icon: <Settings2 />,
content: (
<>
<Group title="Startup">
<Row label={`Open ${appName} at login`} description={`${appName} starts in the background when you sign in to this ${feel === "macos" ? "Mac" : "computer"}.`}>
{(id) => <Switch checked={v.launchAtLogin} onCheckedChange={(c) => set("launchAtLogin", c)} aria-labelledby={id} aria-describedby={`${id}-desc`} />}
</Row>
<Row label={feel === "windows" ? "Show in the system tray" : "Show in the menu bar"} description="Quick access without opening the main window.">
{(id) => <Switch checked={v.showInMenuBar} onCheckedChange={(c) => set("showInMenuBar", c)} aria-labelledby={id} aria-describedby={`${id}-desc`} />}
</Row>
</Group>
<Group title="Privacy">
<Row label="Send crash reports" description="Anonymous reports help fix problems. They never include your files.">
{(id) => <Switch checked={v.crashReports} onCheckedChange={(c) => set("crashReports", c)} aria-labelledby={id} aria-describedby={`${id}-desc`} />}
</Row>
</Group>
</>
),
},
{
id: "appearance",
label: "Appearance",
icon: <Palette />,
content: (
<Group title="Look">
<Row label="Appearance" description={`System follows the light or dark setting in ${settingsApp}.`}>
{(id) => (
<SegmentedControl value={v.appearance} onValueChange={(a) => set("appearance", a as Desktop1Values["appearance"])} aria-labelledby={id} size="sm">
<SegmentedControlItem value="system">System</SegmentedControlItem>
<SegmentedControlItem value="light">Light</SegmentedControlItem>
<SegmentedControlItem value="dark">Dark</SegmentedControlItem>
</SegmentedControl>
)}
</Row>
<Row label="Density">
{(id) => (
<Select value={v.density} onValueChange={(d) => set("density", d as Desktop1Values["density"])}>
<SelectTrigger size="sm" className="w-36" aria-labelledby={id}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="comfortable">Comfortable</SelectItem>
<SelectItem value="compact">Compact</SelectItem>
</SelectContent>
</Select>
)}
</Row>
</Group>
),
},
{
id: "shortcuts",
label: "Shortcuts",
icon: <Keyboard />,
content: (
<Group title="Global">
<Row label={`Show ${appName}`} description="Works from any app. Use at least one modifier key.">
{(id) => <ShortcutRecorder value={v.shortcut} onChange={(s) => set("shortcut", s)} feel={feel} labelledBy={id} />}
</Row>
</Group>
),
},
{
id: "updates",
label: "Updates",
icon: <RefreshCw />,
content: (
<>
<Group>
<Row label={`${appName} ${version}`} description={v.autoCheck ? "Checks for updates once a day." : "Automatic checks are off."}>
{() => (
<Button variant="outline" size="sm" onClick={onCheckForUpdates}>
Check now
</Button>
)}
</Row>
</Group>
<Group title="Automatic updates">
<Row label="Check for updates automatically">
{(id) => <Switch checked={v.autoCheck} onCheckedChange={(c) => set("autoCheck", c)} aria-labelledby={id} />}
</Row>
<Row label="Download and install in the background" description="The update is applied the next time you quit.">
{(id) => <Switch checked={v.autoInstall} disabled={!v.autoCheck} onCheckedChange={(c) => set("autoInstall", c)} aria-labelledby={id} aria-describedby={`${id}-desc`} />}
</Row>
<Row label="Update channel" description="Beta builds arrive earlier and may have rough edges.">
{(id) => (
<SegmentedControl value={v.channel} onValueChange={(c) => set("channel", c as Desktop1Values["channel"])} aria-labelledby={id} size="sm">
<SegmentedControlItem value="stable">Stable</SegmentedControlItem>
<SegmentedControlItem value="beta">Beta</SegmentedControlItem>
</SegmentedControl>
)}
</Row>
</Group>
</>
),
},
{
id: "advanced",
label: "Advanced",
icon: <SlidersHorizontal />,
content: (
<Group title="Reset">
<Row label="Reset all settings" description="Puts every setting back to how it was when you installed the app. Your data is kept.">
{() => (
<Button
variant="outline"
size="sm"
onClick={() => {
if (!valuesProp) setInternal(defaults)
onValuesChange?.(defaults)
onReset?.()
}}
>
Reset…
</Button>
)}
</Row>
</Group>
),
},
...extraPanes,
]
const current = panes.find((p) => p.id === pane) ?? panes[0]!
// macOS names the window after the pane, as System Settings and every Settings window do; Windows keeps "Settings"
const title = feel === "macos" ? current.label : "Settings"
const tabs = (
<TabsPrimitive.Root
value={current.id}
onValueChange={setPane}
orientation={sideNav ? "vertical" : "horizontal"}
dir={dir}
className={cn("flex min-h-0 flex-1", sideNav ? "flex-col sm:flex-row" : "flex-col")}
>
<TabsPrimitive.List
aria-label="Settings"
data-slot="desktop-1-tabs"
className={cn(
"flex shrink-0",
sideNav
? "gap-0.5 overflow-x-auto p-2 sm:w-52 sm:flex-col sm:overflow-visible sm:p-3"
: "justify-center gap-1 overflow-x-auto border-b px-2 py-1.5"
)}
>
{panes.map((p) => (
<TabsPrimitive.Trigger
key={p.id}
value={p.id}
className={cn(
"text-muted-foreground data-[state=active]:text-foreground relative flex shrink-0 items-center rounded-md outline-none [&_svg]:shrink-0",
"focus-visible:ring-[3px] focus-visible:ring-ring/50 transition-colors duration-150",
sideNav
? "hover:bg-foreground/[0.05] data-[state=active]:bg-foreground/[0.06] h-9 gap-3 px-3 text-sm [&_svg]:size-4 sm:w-full"
: "hover:bg-foreground/[0.05] data-[state=active]:bg-foreground/[0.07] min-w-16 flex-col gap-1 px-2.5 py-1.5 text-[11px] [&_svg]:size-5"
)}
>
{sideNav ? (
<span aria-hidden="true" className="bg-primary absolute inset-y-2 start-0 hidden w-[3px] rounded-full [[data-state=active]>&]:block" />
) : null}
<span aria-hidden="true" className={cn(!sideNav && "[[data-state=active]>&]:text-primary")}>
{p.icon}
</span>
{p.label}
</TabsPrimitive.Trigger>
))}
</TabsPrimitive.List>
{panes.map((p) => (
<TabsPrimitive.Content key={p.id} value={p.id} className="min-h-0 flex-1 overflow-auto outline-none focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50">
<div className={cn("mx-auto grid w-full max-w-2xl gap-6", sideNav ? "p-4 sm:p-6" : "p-5")}>
{sideNav ? <h2 className="text-xl font-semibold tracking-tight">{p.label}</h2> : null}
{p.content}
</div>
</TabsPrimitive.Content>
))}
</TabsPrimitive.Root>
)
const body = <FeelContext.Provider value={feel}>{tabs}</FeelContext.Provider>
if (!frame)
return (
<div data-slot="desktop-1" data-feel={feelProp} className={cn("flex min-h-0 flex-col", className)} {...props}>
{body}
</div>
)
return (
<AppWindow data-slot="desktop-1" feel={feel} onClose={onClose} onMinimize={onMinimize} className={cn("h-[30rem] w-full max-w-3xl", className)} {...props}>
<AppWindowTitleBar title={title} icon={<Settings2 />} className={cn(!sideNav && "border-b-0")} />
<AppWindowContent className="flex flex-col overflow-hidden">{body}</AppWindowContent>
</AppWindow>
)
}
export { Desktop1, formatShortcut, type Desktop1Props, type Desktop1Values, type Desktop1Pane }Installation
The default, and what a Radix-style project installs.
$ pnpm dlx shadcn@latest add @ballmac/desktop-1Install the dependencies.
$ pnpm add lucide-react radix-uiAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/app-window @ballmac/button @ballmac/segmented-control @ballmac/select @ballmac/switch @ballmac/direction @ballmac/feel-presets @ballmac/shortcutCopy the source into your project.
components/ballmac/blocks/desktop-1/desktop-1.tsx// Ballmac UI: Desktop 1. https://ui.ballmac.com/blocks/desktop-1 "use client" import * as React from "react" import { Tabs as TabsPrimitive } from "radix-ui" import { Keyboard, Palette, RefreshCw, Settings2, SlidersHorizontal } from "lucide-react" import { AppWindow, AppWindowContent, AppWindowTitleBar } from "@/components/ballmac/app-window" import { Button } from "@/components/ballmac/button" import { SegmentedControl, SegmentedControlItem } from "@/components/ballmac/segmented-control" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ballmac/select" import { Switch } from "@/components/ballmac/switch" import { useDirection } from "@/lib/ballmac/direction" import { useFeel, type FeelId } from "@/lib/ballmac/feel" import { formatShortcut, shortcutFromEvent, usesMacKeys } from "@/lib/ballmac/shortcut" import { cn } from "@/lib/utils" type Desktop1Values = { launchAtLogin: boolean showInMenuBar: boolean appearance: "system" | "light" | "dark" density: "comfortable" | "compact" /** The global shortcut as key names joined with "+", for example "Mod+Shift+Space". Mod is ⌘ on a Mac and Ctrl elsewhere. */ shortcut: string autoCheck: boolean autoInstall: boolean channel: "stable" | "beta" crashReports: boolean } type Desktop1Pane = { id: string label: string icon: React.ReactNode content: React.ReactNode } type Desktop1Props = Omit<React.ComponentProps<"div">, "onChange" | "defaultValue"> & { /** Which platform the window handles like. Defaults to the page's feel. */ feel?: FeelId /** Draw the window frame. Turn off inside a real Tauri or Electron window that already has one. */ frame?: boolean /** The app name, used in copy such as "Open Acme at login". */ appName?: string /** The settings when the window opens. */ defaultValues?: Desktop1Values /** Controlled settings. Pass with onValuesChange. */ values?: Desktop1Values /** Called with every change. Settings apply immediately, as they do in desktop apps: there is no Save button. */ onValuesChange?: (values: Desktop1Values) => void /** The pane shown first. */ defaultPane?: string /** More panes after the built-in ones, for your own settings. */ extraPanes?: Desktop1Pane[] /** The version shown in the Updates pane. */ version?: string /** Called by "Check now" in the Updates pane. */ onCheckForUpdates?: () => void /** Called by "Reset all settings" after the built-in values are restored. */ onReset?: () => void /** Window controls, passed to App Window. */ onClose?: () => void onMinimize?: () => void } const defaults: Desktop1Values = { launchAtLogin: true, showInMenuBar: true, appearance: "system", density: "comfortable", shortcut: "Mod+Shift+Space", autoCheck: true, autoInstall: false, channel: "stable", crashReports: true, } /** * A button that records the next key combination. Escape cancels, Backspace or Delete clears. A combination needs at least one * modifier, so typing a letter cannot steal it from every other app. */ function ShortcutRecorder({ value, onChange, feel, labelledBy }: { value: string; onChange: (v: string) => void; feel: FeelId; labelledBy: string }) { const [recording, setRecording] = React.useState(false) const mac = usesMacKeys(feel) return ( <div className="flex items-center gap-2"> <button type="button" data-slot="desktop-1-shortcut" aria-labelledby={labelledBy} aria-describedby={`${labelledBy}-hint`} aria-pressed={recording} onClick={() => setRecording((r) => !r)} onBlur={() => setRecording(false)} onKeyDown={(e) => { if (!recording) return if (e.key === "Tab") return setRecording(false) e.preventDefault() if (e.key === "Escape") return setRecording(false) if (e.key === "Backspace" || e.key === "Delete") { onChange("") return setRecording(false) } const next = shortcutFromEvent(e, mac) if (next) { onChange(next) setRecording(false) } }} className={cn( "border-input bg-background inline-flex h-[var(--bm-control-h-sm,calc(var(--spacing)*8))] min-w-32 items-center justify-center rounded-md border px-3 font-mono text-[13px] tabular-nums outline-none", "focus-visible:ring-[3px] focus-visible:ring-ring/50", recording && "border-ring ring-ring/30 text-muted-foreground ring-[3px]" )} > <span dir="ltr">{recording ? "Type a shortcut…" : formatShortcut(value, feel) || "Record shortcut"}</span> </button> <span id={`${labelledBy}-hint`} className="sr-only"> {recording ? "Press the new key combination. Escape cancels, Delete clears." : "Press to record a new shortcut."} </span> {value && !recording ? ( <Button variant="ghost" size="sm" onClick={() => onChange("")}> Clear </Button> ) : null} </div> ) } const FeelContext = React.createContext<FeelId>("web") /** A group of setting rows. Windows draws each row as its own card; macOS, iOS and the web group them in one rounded box. */ function Group({ title, children }: { title?: string; children: React.ReactNode }) { const feel = React.useContext(FeelContext) const id = React.useId() return ( <section aria-labelledby={title ? id : undefined} className="grid gap-2"> {title ? ( <h3 id={id} className={cn("text-sm font-semibold", feel === "ios" && "text-muted-foreground px-4 text-xs font-normal uppercase")}> {title} </h3> ) : null} <div className={cn(feel === "windows" ? "grid gap-1" : "bg-card divide-border divide-y overflow-hidden rounded-lg border", feel === "macos" && "bg-muted/40")}>{children}</div> </section> ) } /** One setting: its name and an optional line of help on the start side, the control on the end side. */ function Row({ label, description, children, id: idProp }: { label: string; description?: string; children: (id: string) => React.ReactNode; id?: string }) { const feel = React.useContext(FeelContext) const auto = React.useId() const id = idProp ?? auto return ( <div data-slot="desktop-1-row" className={cn("flex flex-wrap items-center justify-between gap-x-6 gap-y-2", feel === "windows" ? "bg-card rounded-md border px-4 py-3.5" : "px-4 py-3")} > <div className="min-w-0 flex-1 basis-48"> <p id={id} className="text-sm"> {label} </p> {description ? ( <p id={`${id}-desc`} className="text-muted-foreground mt-0.5 text-xs text-pretty"> {description} </p> ) : null} </div> <div className="flex shrink-0 items-center">{children(id)}</div> </div> ) } function Desktop1({ feel: feelProp, frame = true, appName = "Acme", defaultValues = defaults, values: valuesProp, onValuesChange, defaultPane = "general", extraPanes = [], version = "2.4.1", onCheckForUpdates, onReset, onClose, onMinimize, className, ...props }: Desktop1Props) { const page = useFeel().feel const feel = feelProp ?? page const dir = useDirection() const [internal, setInternal] = React.useState(defaultValues) const v = valuesProp ?? internal const [pane, setPane] = React.useState(defaultPane) const set = <K extends keyof Desktop1Values>(k: K, value: Desktop1Values[K]) => { const next = { ...v, [k]: value } if (!valuesProp) setInternal(next) onValuesChange?.(next) } const sideNav = feel === "windows" const settingsApp = feel === "macos" ? "System Settings" : feel === "windows" ? "Settings" : "settings" const panes: Desktop1Pane[] = [ { id: "general", label: "General", icon: <Settings2 />, content: ( <> <Group title="Startup"> <Row label={`Open ${appName} at login`} description={`${appName} starts in the background when you sign in to this ${feel === "macos" ? "Mac" : "computer"}.`}> {(id) => <Switch checked={v.launchAtLogin} onCheckedChange={(c) => set("launchAtLogin", c)} aria-labelledby={id} aria-describedby={`${id}-desc`} />} </Row> <Row label={feel === "windows" ? "Show in the system tray" : "Show in the menu bar"} description="Quick access without opening the main window."> {(id) => <Switch checked={v.showInMenuBar} onCheckedChange={(c) => set("showInMenuBar", c)} aria-labelledby={id} aria-describedby={`${id}-desc`} />} </Row> </Group> <Group title="Privacy"> <Row label="Send crash reports" description="Anonymous reports help fix problems. They never include your files."> {(id) => <Switch checked={v.crashReports} onCheckedChange={(c) => set("crashReports", c)} aria-labelledby={id} aria-describedby={`${id}-desc`} />} </Row> </Group> </> ), }, { id: "appearance", label: "Appearance", icon: <Palette />, content: ( <Group title="Look"> <Row label="Appearance" description={`System follows the light or dark setting in ${settingsApp}.`}> {(id) => ( <SegmentedControl value={v.appearance} onValueChange={(a) => set("appearance", a as Desktop1Values["appearance"])} aria-labelledby={id} size="sm"> <SegmentedControlItem value="system">System</SegmentedControlItem> <SegmentedControlItem value="light">Light</SegmentedControlItem> <SegmentedControlItem value="dark">Dark</SegmentedControlItem> </SegmentedControl> )} </Row> <Row label="Density"> {(id) => ( <Select value={v.density} onValueChange={(d) => set("density", d as Desktop1Values["density"])}> <SelectTrigger size="sm" className="w-36" aria-labelledby={id}> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="comfortable">Comfortable</SelectItem> <SelectItem value="compact">Compact</SelectItem> </SelectContent> </Select> )} </Row> </Group> ), }, { id: "shortcuts", label: "Shortcuts", icon: <Keyboard />, content: ( <Group title="Global"> <Row label={`Show ${appName}`} description="Works from any app. Use at least one modifier key."> {(id) => <ShortcutRecorder value={v.shortcut} onChange={(s) => set("shortcut", s)} feel={feel} labelledBy={id} />} </Row> </Group> ), }, { id: "updates", label: "Updates", icon: <RefreshCw />, content: ( <> <Group> <Row label={`${appName} ${version}`} description={v.autoCheck ? "Checks for updates once a day." : "Automatic checks are off."}> {() => ( <Button variant="outline" size="sm" onClick={onCheckForUpdates}> Check now </Button> )} </Row> </Group> <Group title="Automatic updates"> <Row label="Check for updates automatically"> {(id) => <Switch checked={v.autoCheck} onCheckedChange={(c) => set("autoCheck", c)} aria-labelledby={id} />} </Row> <Row label="Download and install in the background" description="The update is applied the next time you quit."> {(id) => <Switch checked={v.autoInstall} disabled={!v.autoCheck} onCheckedChange={(c) => set("autoInstall", c)} aria-labelledby={id} aria-describedby={`${id}-desc`} />} </Row> <Row label="Update channel" description="Beta builds arrive earlier and may have rough edges."> {(id) => ( <SegmentedControl value={v.channel} onValueChange={(c) => set("channel", c as Desktop1Values["channel"])} aria-labelledby={id} size="sm"> <SegmentedControlItem value="stable">Stable</SegmentedControlItem> <SegmentedControlItem value="beta">Beta</SegmentedControlItem> </SegmentedControl> )} </Row> </Group> </> ), }, { id: "advanced", label: "Advanced", icon: <SlidersHorizontal />, content: ( <Group title="Reset"> <Row label="Reset all settings" description="Puts every setting back to how it was when you installed the app. Your data is kept."> {() => ( <Button variant="outline" size="sm" onClick={() => { if (!valuesProp) setInternal(defaults) onValuesChange?.(defaults) onReset?.() }} > Reset… </Button> )} </Row> </Group> ), }, ...extraPanes, ] const current = panes.find((p) => p.id === pane) ?? panes[0]! // macOS names the window after the pane, as System Settings and every Settings window do; Windows keeps "Settings" const title = feel === "macos" ? current.label : "Settings" const tabs = ( <TabsPrimitive.Root value={current.id} onValueChange={setPane} orientation={sideNav ? "vertical" : "horizontal"} dir={dir} className={cn("flex min-h-0 flex-1", sideNav ? "flex-col sm:flex-row" : "flex-col")} > <TabsPrimitive.List aria-label="Settings" data-slot="desktop-1-tabs" className={cn( "flex shrink-0", sideNav ? "gap-0.5 overflow-x-auto p-2 sm:w-52 sm:flex-col sm:overflow-visible sm:p-3" : "justify-center gap-1 overflow-x-auto border-b px-2 py-1.5" )} > {panes.map((p) => ( <TabsPrimitive.Trigger key={p.id} value={p.id} className={cn( "text-muted-foreground data-[state=active]:text-foreground relative flex shrink-0 items-center rounded-md outline-none [&_svg]:shrink-0", "focus-visible:ring-[3px] focus-visible:ring-ring/50 transition-colors duration-150", sideNav ? "hover:bg-foreground/[0.05] data-[state=active]:bg-foreground/[0.06] h-9 gap-3 px-3 text-sm [&_svg]:size-4 sm:w-full" : "hover:bg-foreground/[0.05] data-[state=active]:bg-foreground/[0.07] min-w-16 flex-col gap-1 px-2.5 py-1.5 text-[11px] [&_svg]:size-5" )} > {sideNav ? ( <span aria-hidden="true" className="bg-primary absolute inset-y-2 start-0 hidden w-[3px] rounded-full [[data-state=active]>&]:block" /> ) : null} <span aria-hidden="true" className={cn(!sideNav && "[[data-state=active]>&]:text-primary")}> {p.icon} </span> {p.label} </TabsPrimitive.Trigger> ))} </TabsPrimitive.List> {panes.map((p) => ( <TabsPrimitive.Content key={p.id} value={p.id} className="min-h-0 flex-1 overflow-auto outline-none focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50"> <div className={cn("mx-auto grid w-full max-w-2xl gap-6", sideNav ? "p-4 sm:p-6" : "p-5")}> {sideNav ? <h2 className="text-xl font-semibold tracking-tight">{p.label}</h2> : null} {p.content} </div> </TabsPrimitive.Content> ))} </TabsPrimitive.Root> ) const body = <FeelContext.Provider value={feel}>{tabs}</FeelContext.Provider> if (!frame) return ( <div data-slot="desktop-1" data-feel={feelProp} className={cn("flex min-h-0 flex-col", className)} {...props}> {body} </div> ) return ( <AppWindow data-slot="desktop-1" feel={feel} onClose={onClose} onMinimize={onMinimize} className={cn("h-[30rem] w-full max-w-3xl", className)} {...props}> <AppWindowTitleBar title={title} icon={<Settings2 />} className={cn(!sideNav && "border-b-0")} /> <AppWindowContent className="flex flex-col overflow-hidden">{body}</AppWindowContent> </AppWindow> ) } export { Desktop1, formatShortcut, type Desktop1Props, type Desktop1Values, type Desktop1Pane }Update the import paths to match your project setup.
$ pnpm dlx shadcn@latest add https://ui.ballmac.com/r/base/desktop-1.jsonInstall the dependencies.
$ pnpm add lucide-react @base-ui/react@^1.9.0Add the Ballmac items it builds on (their Base UI versions where they have one).
$ pnpm dlx shadcn@latest add https://ui.ballmac.com/r/base/app-window.json https://ui.ballmac.com/r/base/button.json https://ui.ballmac.com/r/base/segmented-control.json https://ui.ballmac.com/r/base/select.json https://ui.ballmac.com/r/base/switch.json https://ui.ballmac.com/r/base/direction.json https://ui.ballmac.com/r/base/feel-presets.json https://ui.ballmac.com/r/base/shortcut.jsonCopy the source into your project.
components/ballmac/blocks/desktop-1/desktop-1.tsx// Ballmac UI: Desktop 1. https://ui.ballmac.com/blocks/desktop-1 "use client" import * as React from "react" import { Tabs as TabsPrimitive } from "@base-ui/react/tabs" import { Keyboard, Palette, RefreshCw, Settings2, SlidersHorizontal } from "lucide-react" import { AppWindow, AppWindowContent, AppWindowTitleBar } from "@/components/ballmac/app-window" import { Button } from "@/components/ballmac/button" import { SegmentedControl, SegmentedControlItem } from "@/components/ballmac/segmented-control" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ballmac/select" import { Switch } from "@/components/ballmac/switch" import { useDirection } from "@/lib/ballmac/direction" import { useFeel, type FeelId } from "@/lib/ballmac/feel" import { formatShortcut, shortcutFromEvent, usesMacKeys } from "@/lib/ballmac/shortcut" import { cn } from "@/lib/utils" type Desktop1Values = { launchAtLogin: boolean showInMenuBar: boolean appearance: "system" | "light" | "dark" density: "comfortable" | "compact" /** The global shortcut as key names joined with "+", for example "Mod+Shift+Space". Mod is ⌘ on a Mac and Ctrl elsewhere. */ shortcut: string autoCheck: boolean autoInstall: boolean channel: "stable" | "beta" crashReports: boolean } type Desktop1Pane = { id: string label: string icon: React.ReactNode content: React.ReactNode } type Desktop1Props = Omit<React.ComponentProps<"div">, "onChange" | "defaultValue"> & { /** Which platform the window handles like. Defaults to the page's feel. */ feel?: FeelId /** Draw the window frame. Turn off inside a real Tauri or Electron window that already has one. */ frame?: boolean /** The app name, used in copy such as "Open Acme at login". */ appName?: string /** The settings when the window opens. */ defaultValues?: Desktop1Values /** Controlled settings. Pass with onValuesChange. */ values?: Desktop1Values /** Called with every change. Settings apply immediately, as they do in desktop apps: there is no Save button. */ onValuesChange?: (values: Desktop1Values) => void /** The pane shown first. */ defaultPane?: string /** More panes after the built-in ones, for your own settings. */ extraPanes?: Desktop1Pane[] /** The version shown in the Updates pane. */ version?: string /** Called by "Check now" in the Updates pane. */ onCheckForUpdates?: () => void /** Called by "Reset all settings" after the built-in values are restored. */ onReset?: () => void /** Window controls, passed to App Window. */ onClose?: () => void onMinimize?: () => void } const defaults: Desktop1Values = { launchAtLogin: true, showInMenuBar: true, appearance: "system", density: "comfortable", shortcut: "Mod+Shift+Space", autoCheck: true, autoInstall: false, channel: "stable", crashReports: true, } /** * A button that records the next key combination. Escape cancels, Backspace or Delete clears. A combination needs at least one * modifier, so typing a letter cannot steal it from every other app. */ function ShortcutRecorder({ value, onChange, feel, labelledBy }: { value: string; onChange: (v: string) => void; feel: FeelId; labelledBy: string }) { const [recording, setRecording] = React.useState(false) const mac = usesMacKeys(feel) return ( <div className="flex items-center gap-2"> <button type="button" data-slot="desktop-1-shortcut" aria-labelledby={labelledBy} aria-describedby={`${labelledBy}-hint`} aria-pressed={recording} onClick={() => setRecording((r) => !r)} onBlur={() => setRecording(false)} onKeyDown={(e) => { if (!recording) return if (e.key === "Tab") return setRecording(false) e.preventDefault() if (e.key === "Escape") return setRecording(false) if (e.key === "Backspace" || e.key === "Delete") { onChange("") return setRecording(false) } const next = shortcutFromEvent(e, mac) if (next) { onChange(next) setRecording(false) } }} className={cn( "border-input bg-background inline-flex h-[var(--bm-control-h-sm,calc(var(--spacing)*8))] min-w-32 items-center justify-center rounded-md border px-3 font-mono text-[13px] tabular-nums outline-none", "focus-visible:ring-[3px] focus-visible:ring-ring/50", recording && "border-ring ring-ring/30 text-muted-foreground ring-[3px]" )} > <span dir="ltr">{recording ? "Type a shortcut…" : formatShortcut(value, feel) || "Record shortcut"}</span> </button> <span id={`${labelledBy}-hint`} className="sr-only"> {recording ? "Press the new key combination. Escape cancels, Delete clears." : "Press to record a new shortcut."} </span> {value && !recording ? ( <Button variant="ghost" size="sm" onClick={() => onChange("")}> Clear </Button> ) : null} </div> ) } const FeelContext = React.createContext<FeelId>("web") /** A group of setting rows. Windows draws each row as its own card; macOS, iOS and the web group them in one rounded box. */ function Group({ title, children }: { title?: string; children: React.ReactNode }) { const feel = React.useContext(FeelContext) const id = React.useId() return ( <section aria-labelledby={title ? id : undefined} className="grid gap-2"> {title ? ( <h3 id={id} className={cn("text-sm font-semibold", feel === "ios" && "text-muted-foreground px-4 text-xs font-normal uppercase")}> {title} </h3> ) : null} <div className={cn(feel === "windows" ? "grid gap-1" : "bg-card divide-border divide-y overflow-hidden rounded-lg border", feel === "macos" && "bg-muted/40")}>{children}</div> </section> ) } /** One setting: its name and an optional line of help on the start side, the control on the end side. */ function Row({ label, description, children, id: idProp }: { label: string; description?: string; children: (id: string) => React.ReactNode; id?: string }) { const feel = React.useContext(FeelContext) const auto = React.useId() const id = idProp ?? auto return ( <div data-slot="desktop-1-row" className={cn("flex flex-wrap items-center justify-between gap-x-6 gap-y-2", feel === "windows" ? "bg-card rounded-md border px-4 py-3.5" : "px-4 py-3")} > <div className="min-w-0 flex-1 basis-48"> <p id={id} className="text-sm"> {label} </p> {description ? ( <p id={`${id}-desc`} className="text-muted-foreground mt-0.5 text-xs text-pretty"> {description} </p> ) : null} </div> <div className="flex shrink-0 items-center">{children(id)}</div> </div> ) } function Desktop1({ feel: feelProp, frame = true, appName = "Acme", defaultValues = defaults, values: valuesProp, onValuesChange, defaultPane = "general", extraPanes = [], version = "2.4.1", onCheckForUpdates, onReset, onClose, onMinimize, className, ...props }: Desktop1Props) { const page = useFeel().feel const feel = feelProp ?? page const dir = useDirection() const [internal, setInternal] = React.useState(defaultValues) const v = valuesProp ?? internal const [pane, setPane] = React.useState(defaultPane) const set = <K extends keyof Desktop1Values>(k: K, value: Desktop1Values[K]) => { const next = { ...v, [k]: value } if (!valuesProp) setInternal(next) onValuesChange?.(next) } const sideNav = feel === "windows" const settingsApp = feel === "macos" ? "System Settings" : feel === "windows" ? "Settings" : "settings" const panes: Desktop1Pane[] = [ { id: "general", label: "General", icon: <Settings2 />, content: ( <> <Group title="Startup"> <Row label={`Open ${appName} at login`} description={`${appName} starts in the background when you sign in to this ${feel === "macos" ? "Mac" : "computer"}.`}> {(id) => <Switch checked={v.launchAtLogin} onCheckedChange={(c) => set("launchAtLogin", c)} aria-labelledby={id} aria-describedby={`${id}-desc`} />} </Row> <Row label={feel === "windows" ? "Show in the system tray" : "Show in the menu bar"} description="Quick access without opening the main window."> {(id) => <Switch checked={v.showInMenuBar} onCheckedChange={(c) => set("showInMenuBar", c)} aria-labelledby={id} aria-describedby={`${id}-desc`} />} </Row> </Group> <Group title="Privacy"> <Row label="Send crash reports" description="Anonymous reports help fix problems. They never include your files."> {(id) => <Switch checked={v.crashReports} onCheckedChange={(c) => set("crashReports", c)} aria-labelledby={id} aria-describedby={`${id}-desc`} />} </Row> </Group> </> ), }, { id: "appearance", label: "Appearance", icon: <Palette />, content: ( <Group title="Look"> <Row label="Appearance" description={`System follows the light or dark setting in ${settingsApp}.`}> {(id) => ( <SegmentedControl value={v.appearance} onValueChange={(a) => set("appearance", a as Desktop1Values["appearance"])} aria-labelledby={id} size="sm"> <SegmentedControlItem value="system">System</SegmentedControlItem> <SegmentedControlItem value="light">Light</SegmentedControlItem> <SegmentedControlItem value="dark">Dark</SegmentedControlItem> </SegmentedControl> )} </Row> <Row label="Density"> {(id) => ( <Select value={v.density} onValueChange={(d) => set("density", d as Desktop1Values["density"])}> <SelectTrigger size="sm" className="w-36" aria-labelledby={id}> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="comfortable">Comfortable</SelectItem> <SelectItem value="compact">Compact</SelectItem> </SelectContent> </Select> )} </Row> </Group> ), }, { id: "shortcuts", label: "Shortcuts", icon: <Keyboard />, content: ( <Group title="Global"> <Row label={`Show ${appName}`} description="Works from any app. Use at least one modifier key."> {(id) => <ShortcutRecorder value={v.shortcut} onChange={(s) => set("shortcut", s)} feel={feel} labelledBy={id} />} </Row> </Group> ), }, { id: "updates", label: "Updates", icon: <RefreshCw />, content: ( <> <Group> <Row label={`${appName} ${version}`} description={v.autoCheck ? "Checks for updates once a day." : "Automatic checks are off."}> {() => ( <Button variant="outline" size="sm" onClick={onCheckForUpdates}> Check now </Button> )} </Row> </Group> <Group title="Automatic updates"> <Row label="Check for updates automatically"> {(id) => <Switch checked={v.autoCheck} onCheckedChange={(c) => set("autoCheck", c)} aria-labelledby={id} />} </Row> <Row label="Download and install in the background" description="The update is applied the next time you quit."> {(id) => <Switch checked={v.autoInstall} disabled={!v.autoCheck} onCheckedChange={(c) => set("autoInstall", c)} aria-labelledby={id} aria-describedby={`${id}-desc`} />} </Row> <Row label="Update channel" description="Beta builds arrive earlier and may have rough edges."> {(id) => ( <SegmentedControl value={v.channel} onValueChange={(c) => set("channel", c as Desktop1Values["channel"])} aria-labelledby={id} size="sm"> <SegmentedControlItem value="stable">Stable</SegmentedControlItem> <SegmentedControlItem value="beta">Beta</SegmentedControlItem> </SegmentedControl> )} </Row> </Group> </> ), }, { id: "advanced", label: "Advanced", icon: <SlidersHorizontal />, content: ( <Group title="Reset"> <Row label="Reset all settings" description="Puts every setting back to how it was when you installed the app. Your data is kept."> {() => ( <Button variant="outline" size="sm" onClick={() => { if (!valuesProp) setInternal(defaults) onValuesChange?.(defaults) onReset?.() }} > Reset… </Button> )} </Row> </Group> ), }, ...extraPanes, ] const current = panes.find((p) => p.id === pane) ?? panes[0]! // macOS names the window after the pane, as System Settings and every Settings window do; Windows keeps "Settings" const title = feel === "macos" ? current.label : "Settings" const tabs = ( <TabsPrimitive.Root value={current.id} onValueChange={(value) => setPane(String(value))} orientation={sideNav ? "vertical" : "horizontal"} dir={dir} className={cn("flex min-h-0 flex-1", sideNav ? "flex-col sm:flex-row" : "flex-col")} > <TabsPrimitive.List aria-label="Settings" activateOnFocus data-slot="desktop-1-tabs" className={cn( "flex shrink-0", sideNav ? "gap-0.5 overflow-x-auto p-2 sm:w-52 sm:flex-col sm:overflow-visible sm:p-3" : "justify-center gap-1 overflow-x-auto border-b px-2 py-1.5" )} > {panes.map((p) => ( <TabsPrimitive.Tab key={p.id} value={p.id} className={cn( "text-muted-foreground data-active:text-foreground relative flex shrink-0 items-center rounded-md outline-none [&_svg]:shrink-0", "focus-visible:ring-[3px] focus-visible:ring-ring/50 transition-colors duration-150", sideNav ? "hover:bg-foreground/[0.05] data-active:bg-foreground/[0.06] h-9 gap-3 px-3 text-sm [&_svg]:size-4 sm:w-full" : "hover:bg-foreground/[0.05] data-active:bg-foreground/[0.07] min-w-16 flex-col gap-1 px-2.5 py-1.5 text-[11px] [&_svg]:size-5" )} > {sideNav ? ( <span aria-hidden="true" className="bg-primary absolute inset-y-2 start-0 hidden w-[3px] rounded-full [[data-active]>&]:block" /> ) : null} <span aria-hidden="true" className={cn(!sideNav && "[[data-active]>&]:text-primary")}> {p.icon} </span> {p.label} </TabsPrimitive.Tab> ))} </TabsPrimitive.List> {panes.map((p) => ( <TabsPrimitive.Panel key={p.id} value={p.id} className="min-h-0 flex-1 overflow-auto outline-none focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50"> <div className={cn("mx-auto grid w-full max-w-2xl gap-6", sideNav ? "p-4 sm:p-6" : "p-5")}> {sideNav ? <h2 className="text-xl font-semibold tracking-tight">{p.label}</h2> : null} {p.content} </div> </TabsPrimitive.Panel> ))} </TabsPrimitive.Root> ) const body = <FeelContext.Provider value={feel}>{tabs}</FeelContext.Provider> if (!frame) return ( <div data-slot="desktop-1" data-feel={feelProp} className={cn("flex min-h-0 flex-col", className)} {...props}> {body} </div> ) return ( <AppWindow data-slot="desktop-1" feel={feel} onClose={onClose} onMinimize={onMinimize} className={cn("h-[30rem] w-full max-w-3xl", className)} {...props}> <AppWindowTitleBar title={title} icon={<Settings2 />} className={cn(!sideNav && "border-b-0")} /> <AppWindowContent className="flex flex-col overflow-hidden">{body}</AppWindowContent> </AppWindow> ) } export { Desktop1, formatShortcut, type Desktop1Props, type Desktop1Values, type Desktop1Pane }Update the import paths to match your project setup.
Usage
import { Desktop1 } from "@/components/ballmac/blocks/desktop-1/desktop-1"
export default function Desktop1Demo() {
return <Desktop1 feel="macos" appName="Acme" />
}States
This block has no states. Settings apply the moment they change, as desktop apps do: nothing to load, submit or fail.
Quality scorecard
Every applicable check passes
Measured in Chromium 153.0.8010.12 on October 19, 2026, on version 1.0.1, across 3 examples.
Passed: Accessibility scan
No serious or critical issues in light and dark, on a phone and a desktop (axe-core 4.13.0, WCAG 2.1 AA and best-practice rules).
Passed: Small screens
No sideways scrolling at 360, 390 or 1280 px wide.
Passed: Reduced motion
No endless animation to begin with.
Passed: Right to left
Mirrors correctly in Arabic, Hebrew, Persian and Urdu layouts (4% of elements off the mirror, limit 15%).
Passed: Clean console
No console errors or React hydration errors while loading.
Passed: Keyboard
14 tab stops, each showing a visible change when it takes focus. 3 documented keys.
Not applicable: Loading, empty and error
Settings apply the moment they change, as desktop apps do: nothing to load, submit or fail.
Passed: Automated tests
Covered by desktop-1.test.tsx.
For information: Footprint
4.9 KB of source (gzip) in 1 file. Installs lucide-react, radix-ui.
Automated checks find a subset of problems and do not replace testing with a screen reader and a keyboard in your own app. What they miss.
Dependencies
Use with AI
A complete preferences window. <Desktop1 appName defaultValues onValuesChange={(v) => persist(v)} onCheckForUpdates />. Panes: General, Appearance, Shortcuts (a key-combination recorder that writes ⇧⌘Space on a Mac and Ctrl+Shift+Space on Windows), Updates, Advanced, plus extraPanes. Layout follows the feel: toolbar tabs and a pane-named title on macOS, side nav and setting cards on Windows. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Desktop 1: preferences window (@ballmac/desktop-1) to this project with the shadcn MCP, then use it where it fits.
Use it for
- The Settings or Preferences window of a Tauri or Electron app
- A desktop-style web app that needs app-level preferences
Not for
- Account settings saved with a button (use settings-1)
- A settings page inside a web app's layout (use settings-2 or settings-panel)
Registry JSON: https://ui.ballmac.com/r/desktop-1.json
Free to use in personal and commercial projects.
More desktop app blocks
Desktop 2: menu bar and tray panel
The panel of a menu bar app on macOS or a tray flyout on Windows: a clipboard history with search, pins, a pause switch, keyboard selection and Preferences and Quit, with loading, empty and error states.
Desktop 3: software update
The update window of a desktop app, the whole cycle: checking, a new version with release notes, downloading with progress, ready to relaunch, up to date and failed. Buttons follow each platform's order and wording.
Desktop 4: licence activation
Licence activation for a paid desktop app: a trial meter, a key field that cleans up pasted keys, activation that shows why a key failed, the licensed view with deactivate, and a trial-ended mode.
Desktop 5: permissions setup
The first-run permissions window: why each one is needed, Allow for ones not asked yet, the exact System Settings path for ones turned off, a re-check on return, and Continue once required ones are on.
Ballmac UI Pro
Need the whole screen, not one piece?
150 premium blocks (dashboards, settings, calendars, mail, shops, onboarding), a private shadcn registry and Figma tokens. Blocks that show data come with loading, empty and error states built in.
Not ready? Try three Pro blocks free
Leave your email and the blocks appear right here, ready to copy. Yours to use in any project.