feat(appearance): expand preference controls
This commit is contained in:
@@ -9,7 +9,9 @@ import { BrightnessField } from "./brightness-field"
|
||||
import { ColorVisionModeField } from "./color-vision-mode-field"
|
||||
import { MotionModeField } from "./motion-mode-field"
|
||||
import { TextScaleField } from "./text-scale-field"
|
||||
import { TextScaleOptionList } from "./text-scale-option-list"
|
||||
import { accentColorSwatches, ThemeColorPicker } from "./theme-color-picker"
|
||||
import { ThemeModeOptionList } from "./theme-mode-option-list"
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -68,6 +70,39 @@ describe("appearance atoms", () => {
|
||||
expect(setBrightness).toHaveBeenCalledWith("dim")
|
||||
})
|
||||
|
||||
it("provides a radio-based text scale option list", () => {
|
||||
const onValueChange = vi.fn()
|
||||
|
||||
renderLocalized(
|
||||
<TextScaleOptionList value="125" onValueChange={onValueChange} />
|
||||
)
|
||||
|
||||
expect(screen.getByRole("group", { name: "Text size" })).not.toBeNull()
|
||||
expect(
|
||||
(screen.getByRole("radio", { name: "125%" }) as HTMLInputElement).checked
|
||||
).toBe(true)
|
||||
|
||||
fireEvent.click(screen.getByRole("radio", { name: "200%" }))
|
||||
expect(onValueChange).toHaveBeenCalledWith("200")
|
||||
})
|
||||
|
||||
it("provides a radio-based theme mode option list", () => {
|
||||
const onValueChange = vi.fn()
|
||||
|
||||
renderLocalized(
|
||||
<ThemeModeOptionList value="system" onValueChange={onValueChange} />
|
||||
)
|
||||
|
||||
expect(screen.getByRole("group", { name: "Theme mode" })).not.toBeNull()
|
||||
expect(
|
||||
(screen.getByRole("radio", { name: "System" }) as HTMLInputElement)
|
||||
.checked
|
||||
).toBe(true)
|
||||
|
||||
fireEvent.click(screen.getByRole("radio", { name: "Dark" }))
|
||||
expect(onValueChange).toHaveBeenCalledWith("dark")
|
||||
})
|
||||
|
||||
it("reports the selected theme color", () => {
|
||||
const onValueChange = vi.fn()
|
||||
|
||||
@@ -83,4 +118,27 @@ describe("appearance atoms", () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "Rose" }))
|
||||
expect(onValueChange).toHaveBeenCalledWith("rose")
|
||||
})
|
||||
|
||||
it("renders compact color swatches without visible labels", () => {
|
||||
const onValueChange = vi.fn()
|
||||
|
||||
renderLocalized(
|
||||
<ThemeColorPicker
|
||||
onValueChange={onValueChange}
|
||||
swatches={accentColorSwatches}
|
||||
title="Accent color"
|
||||
value="blue"
|
||||
variant="swatch"
|
||||
/>
|
||||
)
|
||||
|
||||
const blue = screen.getByRole("button", { name: "Blue" })
|
||||
const rose = screen.getByRole("button", { name: "Rose" })
|
||||
expect(blue.getAttribute("data-variant")).toBe("swatch")
|
||||
expect(blue.getAttribute("aria-pressed")).toBe("true")
|
||||
expect(rose.textContent).toBe("")
|
||||
|
||||
fireEvent.click(rose)
|
||||
expect(onValueChange).toHaveBeenCalledWith("rose")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useId, type ComponentProps, type ReactNode } from "react"
|
||||
import { CheckIcon } from "@workspace/ui/components/icon"
|
||||
import { cn } from "@workspace/ui/lib/utils"
|
||||
|
||||
export interface AppearanceOption<TValue extends string> {
|
||||
description?: ReactNode
|
||||
disabled?: boolean
|
||||
icon?: ReactNode
|
||||
label: ReactNode
|
||||
value: TValue
|
||||
}
|
||||
|
||||
export interface AppearanceOptionListProps<TValue extends string> extends Omit<
|
||||
ComponentProps<"fieldset">,
|
||||
"onChange"
|
||||
> {
|
||||
items: readonly AppearanceOption<TValue>[]
|
||||
label: ReactNode
|
||||
name?: string
|
||||
onValueChange: (value: TValue) => void
|
||||
value: TValue
|
||||
}
|
||||
|
||||
/**
|
||||
* A controlled, accessible option list shared by appearance preferences.
|
||||
* Native radio inputs provide keyboard navigation and form semantics while the
|
||||
* labels supply the larger visual target.
|
||||
*/
|
||||
export function AppearanceOptionList<TValue extends string>({
|
||||
className,
|
||||
disabled,
|
||||
items,
|
||||
label,
|
||||
name,
|
||||
onValueChange,
|
||||
value,
|
||||
...props
|
||||
}: AppearanceOptionListProps<TValue>) {
|
||||
const generatedName = useId()
|
||||
|
||||
return (
|
||||
<fieldset
|
||||
data-slot="appearance-option-list"
|
||||
className={cn(
|
||||
"min-w-0 space-y-1 rounded-xl border p-1 disabled:pointer-events-none disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
<legend className="sr-only">{label}</legend>
|
||||
{items.map((item) => {
|
||||
const checked = item.value === value
|
||||
|
||||
return (
|
||||
<label
|
||||
key={item.value}
|
||||
data-checked={checked}
|
||||
data-disabled={item.disabled || undefined}
|
||||
className={cn(
|
||||
"relative grid min-h-12 cursor-pointer grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 rounded-lg px-3 py-2 transition-colors outline-none",
|
||||
"has-[input:focus-visible]:ring-2 has-[input:focus-visible]:ring-ring has-[input:focus-visible]:ring-offset-2 has-[input:focus-visible]:ring-offset-background",
|
||||
checked
|
||||
? "bg-accent/12 text-accent dark:bg-accent/20"
|
||||
: "text-foreground hover:bg-highlight hover:text-highlight-foreground",
|
||||
item.disabled && "pointer-events-none opacity-50"
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
className="peer sr-only"
|
||||
checked={checked}
|
||||
disabled={item.disabled}
|
||||
name={name ?? generatedName}
|
||||
value={item.value}
|
||||
onChange={() => onValueChange(item.value)}
|
||||
/>
|
||||
<span
|
||||
className="inline-flex size-6 shrink-0 items-center justify-center [&_svg]:size-5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{item.icon}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-sm font-medium">
|
||||
{item.label}
|
||||
</span>
|
||||
{item.description == null ? null : (
|
||||
<span
|
||||
className={cn(
|
||||
"mt-0.5 block text-xs",
|
||||
checked ? "text-accent/80" : "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{item.description}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"size-4 shrink-0 transition-opacity",
|
||||
checked ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useMessage } from "@workspace/i18n"
|
||||
import { cn } from "@workspace/ui/lib/utils"
|
||||
|
||||
import type { TextScale } from "../display"
|
||||
import { appearanceMessages } from "../messages"
|
||||
import {
|
||||
AppearanceOptionList,
|
||||
type AppearanceOption,
|
||||
} from "./appearance-option-list"
|
||||
|
||||
const textScaleItems = [
|
||||
{
|
||||
icon: <span className="text-xs font-semibold">A</span>,
|
||||
label: "100%",
|
||||
value: "100",
|
||||
},
|
||||
{
|
||||
icon: <span className="text-sm font-semibold">A</span>,
|
||||
label: "125%",
|
||||
value: "125",
|
||||
},
|
||||
{
|
||||
icon: <span className="text-base font-semibold">A</span>,
|
||||
label: "150%",
|
||||
value: "150",
|
||||
},
|
||||
{
|
||||
icon: <span className="text-lg font-semibold">A</span>,
|
||||
label: "200%",
|
||||
value: "200",
|
||||
},
|
||||
] as const satisfies readonly AppearanceOption<TextScale>[]
|
||||
|
||||
export interface TextScaleOptionListProps {
|
||||
className?: string
|
||||
onValueChange: (value: TextScale) => void
|
||||
value: TextScale
|
||||
}
|
||||
|
||||
export function TextScaleOptionList({
|
||||
className,
|
||||
onValueChange,
|
||||
value,
|
||||
}: TextScaleOptionListProps) {
|
||||
const title = useMessage(appearanceMessages.textScale)
|
||||
const description = useMessage(appearanceMessages.textScaleDescription)
|
||||
|
||||
return (
|
||||
<div className={cn("min-w-0 lg:flex-1", className)}>
|
||||
<h2 className="font-semibold">{title}</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
||||
<AppearanceOptionList
|
||||
className="mt-2 w-full max-w-64"
|
||||
items={textScaleItems}
|
||||
label={title}
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CSSProperties, ReactNode } from "react"
|
||||
import { useMessage } from "@workspace/i18n"
|
||||
import { CheckIcon } from "@workspace/ui/components/icon"
|
||||
import { cn } from "@workspace/ui/lib/utils"
|
||||
|
||||
import { themeColorMessages } from "../messages"
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
} from "../theme"
|
||||
|
||||
export type ThemeColor = BaseColor | AccentColor
|
||||
export type ThemeColorPickerVariant = "pill" | "swatch"
|
||||
|
||||
export interface ThemeColorSwatch<T extends ThemeColor = ThemeColor> {
|
||||
dark: ThemeColorSwatchScheme
|
||||
@@ -53,6 +55,7 @@ export interface ThemeColorPickerProps<T extends ThemeColor> {
|
||||
swatches: readonly ThemeColorSwatch<T>[]
|
||||
title: ReactNode
|
||||
value: T
|
||||
variant?: ThemeColorPickerVariant
|
||||
}
|
||||
|
||||
export function ThemeColorPicker<T extends ThemeColor>({
|
||||
@@ -61,18 +64,38 @@ export function ThemeColorPicker<T extends ThemeColor>({
|
||||
swatches,
|
||||
title,
|
||||
value,
|
||||
variant = "pill",
|
||||
}: ThemeColorPickerProps<T>) {
|
||||
return (
|
||||
<fieldset className={cn("relative rounded-lg border p-4", className)}>
|
||||
<legend className="absolute top-0 left-4 z-2 block -translate-y-1/2 bg-popover px-2 text-xs text-muted-foreground">
|
||||
<fieldset
|
||||
data-slot="theme-color-picker"
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
variant === "pill" ? "relative rounded-lg border p-4" : "border-0 p-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<legend
|
||||
className={cn(
|
||||
variant === "pill"
|
||||
? "absolute top-0 left-4 z-2 block -translate-y-1/2 bg-popover px-2 text-xs text-muted-foreground"
|
||||
: "mb-3 px-0 text-sm font-medium text-foreground"
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</legend>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-wrap items-center",
|
||||
variant === "pill" ? "gap-2" : "gap-3"
|
||||
)}
|
||||
>
|
||||
{swatches.map((swatch) => (
|
||||
<ThemeColorSwatchButton
|
||||
key={swatch.name}
|
||||
swatch={swatch}
|
||||
pressed={value === swatch.name}
|
||||
variant={variant}
|
||||
onClick={() => onValueChange(swatch.name)}
|
||||
/>
|
||||
))}
|
||||
@@ -85,12 +108,14 @@ export interface ThemeColorSwatchButtonProps {
|
||||
onClick: VoidFunction
|
||||
pressed: boolean
|
||||
swatch: ThemeColorSwatch
|
||||
variant?: ThemeColorPickerVariant
|
||||
}
|
||||
|
||||
export function ThemeColorSwatchButton({
|
||||
onClick,
|
||||
pressed,
|
||||
swatch,
|
||||
variant = "pill",
|
||||
}: ThemeColorSwatchButtonProps) {
|
||||
const swatchTitle = useMessage(themeColorMessages[swatch.name])
|
||||
const style = {
|
||||
@@ -103,19 +128,35 @@ export function ThemeColorSwatchButton({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-slot="theme-color-swatch"
|
||||
data-variant={variant}
|
||||
aria-label={swatchTitle}
|
||||
aria-pressed={pressed}
|
||||
title={variant === "swatch" ? swatchTitle : undefined}
|
||||
className={cn(
|
||||
"h-8 min-w-8 appearance-none truncate overflow-hidden rounded-4xl border-2 px-2 text-xs whitespace-nowrap ring-popover ring-inset",
|
||||
"border-(--light-bg) dark:border-(--dark-bg)",
|
||||
"bg-(--light-bg) dark:bg-(--dark-bg)",
|
||||
"text-(--light-fg) dark:text-(--dark-fg)",
|
||||
"ring-0 aria-pressed:ring-2"
|
||||
"appearance-none bg-(--light-bg) dark:bg-(--dark-bg)",
|
||||
variant === "pill"
|
||||
? [
|
||||
"h-8 min-w-8 truncate overflow-hidden rounded-4xl border-2 px-2 text-xs whitespace-nowrap ring-popover ring-inset",
|
||||
"border-(--light-bg) text-(--light-fg) dark:border-(--dark-bg) dark:text-(--dark-fg)",
|
||||
"ring-0 aria-pressed:ring-2",
|
||||
]
|
||||
: [
|
||||
"inline-flex size-8 shrink-0 items-center justify-center rounded-full border border-black/10 p-0 ring-offset-2 ring-offset-background transition-[box-shadow,transform]",
|
||||
"hover:scale-105 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none",
|
||||
"aria-pressed:ring-2 aria-pressed:ring-(--light-bg) dark:border-white/15 dark:aria-pressed:ring-(--dark-bg)",
|
||||
]
|
||||
)}
|
||||
style={style}
|
||||
onClick={onClick}
|
||||
>
|
||||
{swatchTitle}
|
||||
{variant === "pill" ? (
|
||||
swatchTitle
|
||||
) : pressed ? (
|
||||
<span className="inline-flex size-5 items-center justify-center rounded-full bg-(--light-fg) text-(--light-bg) shadow-sm dark:bg-(--dark-fg) dark:text-(--dark-bg)">
|
||||
<CheckIcon className="size-3.5" />
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
baseColorSwatches,
|
||||
ThemeColorPicker,
|
||||
} from "./theme-color-picker"
|
||||
import { ThemePreview } from "./theme-preview"
|
||||
|
||||
export interface ThemeConfigCardProps {
|
||||
className?: string
|
||||
@@ -40,7 +39,6 @@ export function ThemeConfigCard({
|
||||
<p>
|
||||
<LocalizedText message={appearanceMessages.themeColorsDescription} />
|
||||
</p>
|
||||
<ThemePreview config={config} scheme={scheme} />
|
||||
<ThemeColorPicker
|
||||
swatches={baseColorSwatches}
|
||||
title={<LocalizedText message={appearanceMessages.baseColor} />}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useMessage } from "@workspace/i18n"
|
||||
import { MonitorIcon, MoonIcon, SunIcon } from "@workspace/ui/components/icon"
|
||||
import { cn } from "@workspace/ui/lib/utils"
|
||||
|
||||
import { appearanceMessages } from "../messages"
|
||||
import type { ThemeMode } from "../preferences"
|
||||
import {
|
||||
AppearanceOptionList,
|
||||
type AppearanceOption,
|
||||
} from "./appearance-option-list"
|
||||
|
||||
export interface ThemeModeOptionListProps {
|
||||
className?: string
|
||||
onValueChange: (value: ThemeMode) => void
|
||||
value: ThemeMode
|
||||
}
|
||||
|
||||
export function ThemeModeOptionList({
|
||||
className,
|
||||
onValueChange,
|
||||
value,
|
||||
}: ThemeModeOptionListProps) {
|
||||
const title = useMessage(appearanceMessages.themeMode)
|
||||
const lightLabel = useMessage(appearanceMessages.lightMode)
|
||||
const darkLabel = useMessage(appearanceMessages.darkMode)
|
||||
const systemLabel = useMessage(appearanceMessages.followSystem)
|
||||
const descriptions: Record<ThemeMode, string> = {
|
||||
light: useMessage(appearanceMessages.lightModeDescription),
|
||||
dark: useMessage(appearanceMessages.darkModeDescription),
|
||||
system: useMessage(appearanceMessages.followSystemDescription),
|
||||
}
|
||||
const items: readonly AppearanceOption<ThemeMode>[] = [
|
||||
{ icon: <SunIcon />, label: lightLabel, value: "light" },
|
||||
{ icon: <MoonIcon />, label: darkLabel, value: "dark" },
|
||||
{ icon: <MonitorIcon />, label: systemLabel, value: "system" },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className={cn("min-w-0 lg:flex-1", className)}>
|
||||
<h2 className="font-semibold">{title}</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{descriptions[value]}
|
||||
</p>
|
||||
<AppearanceOptionList
|
||||
className="mt-2 w-full max-w-64"
|
||||
items={items}
|
||||
label={title}
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import type { CSSProperties } from "react"
|
||||
import {
|
||||
getTheme,
|
||||
getThemeCssVariables,
|
||||
type ThemeColorConfig,
|
||||
type ThemeScheme,
|
||||
} from "../theme"
|
||||
import { cn } from "@workspace/ui/lib/utils"
|
||||
|
||||
export interface ThemePreviewProps {
|
||||
className?: string
|
||||
config: ThemeColorConfig
|
||||
scheme: ThemeScheme
|
||||
}
|
||||
|
||||
export function ThemePreview({ scheme, config, className }: ThemePreviewProps) {
|
||||
const themeVariables = getThemeCssVariables(scheme, config)
|
||||
const baseTheme = getTheme(config.baseColor) ?? getTheme("neutral")!
|
||||
const style = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(themeVariables).map(([key, value]) => [`--${key}`, value])
|
||||
),
|
||||
"--preview-base": baseTheme.cssVars[scheme].accent,
|
||||
} as CSSProperties
|
||||
|
||||
return (
|
||||
<div className={cn(className, `ui:${scheme}`)} style={style}>
|
||||
<div className="isolate h-52 max-w-103.5 rounded-lg bg-sidebar p-3 ring ring-border">
|
||||
<div className="flex gap-2.5 [&>div]:bg-foreground/10">
|
||||
<div className="size-5 rounded-sm" />
|
||||
<div className="ml-auto size-5 rounded-sm" />
|
||||
<div className="size-5 rounded-sm" />
|
||||
<div className="size-5 rounded-sm" />
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex w-16 flex-col gap-1 py-5 [&>div]:bg-foreground/10">
|
||||
<div className="h-2.5 w-full rounded-r-full" />
|
||||
<div className="h-2.5 w-full rounded-r-full bg-accent!" />
|
||||
<div className="h-2.5 w-full rounded-r-full" />
|
||||
<div className="h-2.5 w-full rounded-r-full" />
|
||||
<div className="mt-auto h-2.5 w-full rounded-r-full" />
|
||||
</div>
|
||||
<div className="flex-1 py-5">
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex h-2.5 items-center gap-2 [&>div]:bg-foreground/10">
|
||||
<div className="h-2.5 w-3 rounded-sm" />
|
||||
<div className="ml-auto size-2.5 rounded-sm" />
|
||||
<div className="h-2.5 w-3.75 rounded-sm" />
|
||||
<div className="h-2.5 w-5 rounded-sm bg-accent!" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 [&>div]:rounded-sm [&>div]:bg-foreground/10">
|
||||
<div className="flex h-18 items-end gap-2 p-2">
|
||||
<div className="h-6 w-2 rounded-t-sm bg-(--preview-base)/55" />
|
||||
<div className="h-4 w-2 rounded-t-sm bg-(--preview-base)/55" />
|
||||
<div className="h-11 w-2 rounded-t-sm bg-(--preview-base)/55" />
|
||||
<div className="h-8 w-2 rounded-t-sm bg-(--preview-base)/55" />
|
||||
<div className="h-9 w-2 rounded-t-sm bg-(--preview-base)/55" />
|
||||
</div>
|
||||
<div className="flex h-18 flex-col items-stretch justify-between gap-1 p-2">
|
||||
<div className="flex items-center gap-1 opacity-55">
|
||||
<div className="h-1 w-1 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="h-1 w-1 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="h-1 w-1 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="ml-auto h-1 w-6 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="h-1 w-3 rounded-xs bg-(--preview-base)/55" />
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<div className="size-1 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="h-1 w-6 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="h-1 w-3 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="h-1 w-5 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="h-1 w-5 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="size-1 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="h-1 w-5 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="h-1 w-2 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="size-1 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="h-1 w-3 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="size-1 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="h-1 w-3 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="size-1 rounded-xs bg-(--preview-base)/55" />
|
||||
<div className="h-1 w-6 rounded-xs bg-(--preview-base)/55" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="h-2 w-6 rounded-sm bg-accent" />
|
||||
<div className="h-2 w-6 rounded-sm bg-accent/75" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-18" />
|
||||
<div className="h-10 flex-1" />
|
||||
<div className="h-10 flex-1" />
|
||||
<div className="h-10 flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -23,6 +23,11 @@ export {
|
||||
AppearanceSettings,
|
||||
type AppearanceSettingsProps,
|
||||
} from "./components/appearance-settings"
|
||||
export {
|
||||
AppearanceOptionList,
|
||||
type AppearanceOption,
|
||||
type AppearanceOptionListProps,
|
||||
} from "./components/appearance-option-list"
|
||||
export {
|
||||
BrightnessField,
|
||||
type BrightnessFieldProps,
|
||||
@@ -39,6 +44,10 @@ export {
|
||||
TextScaleField,
|
||||
type TextScaleFieldProps,
|
||||
} from "./components/text-scale-field"
|
||||
export {
|
||||
TextScaleOptionList,
|
||||
type TextScaleOptionListProps,
|
||||
} from "./components/text-scale-option-list"
|
||||
export {
|
||||
ThemeColorPicker,
|
||||
ThemeColorSwatchButton,
|
||||
@@ -47,6 +56,7 @@ export {
|
||||
createThemeColorSwatches,
|
||||
type ThemeColor,
|
||||
type ThemeColorPickerProps,
|
||||
type ThemeColorPickerVariant,
|
||||
type ThemeColorSwatch,
|
||||
type ThemeColorSwatchButtonProps,
|
||||
type ThemeColorSwatchScheme,
|
||||
@@ -61,9 +71,9 @@ export {
|
||||
type ThemeModeFieldProps,
|
||||
} from "./components/theme-mode-field"
|
||||
export {
|
||||
ThemePreview,
|
||||
type ThemePreviewProps,
|
||||
} from "./components/theme-preview"
|
||||
ThemeModeOptionList,
|
||||
type ThemeModeOptionListProps,
|
||||
} from "./components/theme-mode-option-list"
|
||||
export {
|
||||
ThemeToggle,
|
||||
ThemeToggleButton,
|
||||
|
||||
Reference in New Issue
Block a user