Converge choice and popover surfaces
This commit is contained in:
parent
44989700a6
commit
d2d06e15b6
6 changed files with 130 additions and 124 deletions
|
|
@ -4,6 +4,7 @@ import { useRef, useState, useEffect, useCallback } from 'react';
|
|||
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react';
|
||||
import { isLightColor, type CustomThemeColors } from '@/contexts/ThemeContext';
|
||||
import { PaletteIcon } from '@/components/icons/Icons';
|
||||
import { IconButton, Input, cn, popoverPanelClass } from '@/components/ui';
|
||||
|
||||
/**
|
||||
* Curated swatch palettes per color role, sourced from existing themes
|
||||
|
|
@ -78,7 +79,7 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps)
|
|||
|
||||
return (
|
||||
<Popover className="relative flex items-center">
|
||||
<PopoverButton className="cursor-pointer group focus:outline-none">
|
||||
<PopoverButton as={IconButton} size="sm" className="group rounded-full p-0" aria-label={`Pick ${label} color`}>
|
||||
<div
|
||||
className="w-6 h-6 rounded-full border-2 transition duration-fast group-focus-visible:ring-2 group-focus-visible:ring-offset-1"
|
||||
style={{
|
||||
|
|
@ -91,8 +92,10 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps)
|
|||
<PopoverPanel
|
||||
anchor="bottom start"
|
||||
transition
|
||||
className="z-[60] mt-2 w-56 rounded-lg shadow-elev-3 border border-line bg-background p-3 space-y-3
|
||||
transition duration-fast ease-standard data-[closed]:opacity-0 data-[closed]:scale-95"
|
||||
className={cn(
|
||||
popoverPanelClass,
|
||||
'z-[60] mt-2 w-56 bg-background space-y-3 transition duration-fast ease-standard data-[closed]:opacity-0 data-[closed]:scale-95',
|
||||
)}
|
||||
>
|
||||
{/* Label */}
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -101,14 +104,14 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps)
|
|||
</span>
|
||||
{/* Eyedropper / native picker */}
|
||||
<div className="relative">
|
||||
<button
|
||||
<IconButton
|
||||
type="button"
|
||||
onClick={() => nativeRef.current?.click()}
|
||||
className="p-1"
|
||||
size="xs"
|
||||
aria-label="Open system color picker"
|
||||
>
|
||||
<PaletteIcon className="w-4 h-4 text-soft transform transition-transform duration-base ease-standard hover:text-accent" />
|
||||
</button>
|
||||
</IconButton>
|
||||
<input
|
||||
ref={nativeRef}
|
||||
type="color"
|
||||
|
|
@ -156,7 +159,7 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps)
|
|||
borderColor: isLightColor(value) ? '#00000018' : '#ffffff18',
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={hexInput}
|
||||
onChange={(e) => setHexInput(e.target.value)}
|
||||
|
|
@ -166,7 +169,8 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps)
|
|||
}}
|
||||
spellCheck={false}
|
||||
maxLength={7}
|
||||
className="flex-1 rounded-lg px-2 py-1 text-xs font-mono border border-line bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-accent-line"
|
||||
controlSize="sm"
|
||||
className="flex-1 font-mono"
|
||||
/>
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import {
|
|||
SidebarNavItem,
|
||||
SegmentedControl,
|
||||
Button,
|
||||
ChoiceTile,
|
||||
IconButton,
|
||||
Input,
|
||||
ModalFrame,
|
||||
|
|
@ -109,6 +110,54 @@ const CUSTOM_COLOR_FIELDS: { key: keyof CustomThemeColors; label: string }[] = [
|
|||
{ key: 'muted', label: 'Muted' },
|
||||
];
|
||||
|
||||
function ThemeSwatches({ colors }: { colors: ThemeColorSet }) {
|
||||
return (
|
||||
<div className="flex gap-1 ml-auto">
|
||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.background }} />
|
||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.offbase }} />
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} />
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} />
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ThemeChoice({
|
||||
label,
|
||||
colors,
|
||||
selected,
|
||||
onClick,
|
||||
className,
|
||||
}: {
|
||||
label: string;
|
||||
colors: ThemeColorSet;
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<ChoiceTile
|
||||
selected={selected}
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
style={{ backgroundColor: colors.base }}
|
||||
>
|
||||
{selected ? (
|
||||
<CheckIcon className="h-3.5 w-3.5 shrink-0" style={{ color: colors.accent }} />
|
||||
) : (
|
||||
<span className="w-3.5 shrink-0" />
|
||||
)}
|
||||
<span
|
||||
className="text-xs font-medium w-14 shrink-0"
|
||||
style={{ color: colors.foreground }}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<ThemeSwatches colors={colors} />
|
||||
</ChoiceTile>
|
||||
);
|
||||
}
|
||||
|
||||
type SectionId = 'api' | 'theme' | 'docs' | 'account' | 'admin';
|
||||
|
||||
type SidebarSection = {
|
||||
|
|
@ -880,34 +929,13 @@ export function SettingsModal({
|
|||
const colors = getThemeColors(systemTheme.id);
|
||||
const isActive = theme === systemTheme.id;
|
||||
return (
|
||||
<button
|
||||
<ThemeChoice
|
||||
label={systemTheme.name}
|
||||
colors={colors}
|
||||
selected={isActive}
|
||||
onClick={() => setTheme(systemTheme.id)}
|
||||
className={`flex items-center gap-2 rounded-lg px-2 py-1.5 w-full text-left transition duration-base ease-standard transform border
|
||||
${isActive
|
||||
? 'border-accent'
|
||||
: 'border-line hover:border-accent-line'
|
||||
}`}
|
||||
style={{ backgroundColor: colors.base }}
|
||||
>
|
||||
{isActive ? (
|
||||
<CheckIcon className="h-3.5 w-3.5 shrink-0 text-accent" />
|
||||
) : (
|
||||
<span className="w-3.5 shrink-0" />
|
||||
)}
|
||||
<span
|
||||
className="text-xs font-medium w-14 shrink-0"
|
||||
style={{ color: colors.foreground }}
|
||||
>
|
||||
{systemTheme.name}
|
||||
</span>
|
||||
<div className="flex gap-1 ml-auto">
|
||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.background }} />
|
||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.offbase }} />
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} />
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} />
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
|
||||
</div>
|
||||
</button>
|
||||
className="w-full"
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
|
@ -921,37 +949,16 @@ export function SettingsModal({
|
|||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
<ThemeChoice
|
||||
label="Custom"
|
||||
colors={colors}
|
||||
selected={isActive}
|
||||
onClick={() => {
|
||||
setTheme('custom');
|
||||
setIsCustomExpanded(true);
|
||||
}}
|
||||
className={`flex items-center gap-2 rounded-lg px-2 py-1.5 flex-1 text-left transition duration-base ease-standard transform border
|
||||
${isActive
|
||||
? 'border-accent'
|
||||
: 'border-line hover:border-accent-line'
|
||||
}`}
|
||||
style={{ backgroundColor: colors.base }}
|
||||
>
|
||||
{isActive ? (
|
||||
<CheckIcon className="h-3.5 w-3.5 shrink-0" style={{ color: colors.accent }} />
|
||||
) : (
|
||||
<span className="w-3.5 shrink-0" />
|
||||
)}
|
||||
<span
|
||||
className="text-xs font-medium w-14 shrink-0"
|
||||
style={{ color: colors.foreground }}
|
||||
>
|
||||
Custom
|
||||
</span>
|
||||
<div className="flex gap-1 ml-auto">
|
||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.background }} />
|
||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.offbase }} />
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} />
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} />
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
|
||||
</div>
|
||||
</button>
|
||||
className="flex-1"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setIsCustomExpanded(!isCustomExpanded)}
|
||||
className="shrink-0 p-1.5 rounded-lg border border-line hover:border-accent-line transition-colors"
|
||||
|
|
@ -1023,35 +1030,13 @@ export function SettingsModal({
|
|||
const colors = getThemeColors(t.id);
|
||||
const isActive = theme === t.id;
|
||||
return (
|
||||
<button
|
||||
<ThemeChoice
|
||||
key={t.id}
|
||||
label={t.name}
|
||||
colors={colors}
|
||||
selected={isActive}
|
||||
onClick={() => setTheme(t.id)}
|
||||
className={`flex items-center gap-2 rounded-lg px-2 py-1.5 text-left transition duration-base ease-standard transform border
|
||||
${isActive
|
||||
? 'border-accent'
|
||||
: 'border-line hover:border-accent-line'
|
||||
}`}
|
||||
style={{ backgroundColor: colors.base }}
|
||||
>
|
||||
{isActive ? (
|
||||
<CheckIcon className="h-3.5 w-3.5 shrink-0 text-accent" />
|
||||
) : (
|
||||
<span className="w-3.5 shrink-0" />
|
||||
)}
|
||||
<span
|
||||
className="text-xs font-medium w-14 shrink-0"
|
||||
style={{ color: colors.foreground }}
|
||||
>
|
||||
{t.name}
|
||||
</span>
|
||||
<div className="flex gap-1 ml-auto">
|
||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.background }} />
|
||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.offbase }} />
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} />
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} />
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
|
||||
</div>
|
||||
</button>
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -1065,35 +1050,13 @@ export function SettingsModal({
|
|||
const colors = getThemeColors(t.id);
|
||||
const isActive = theme === t.id;
|
||||
return (
|
||||
<button
|
||||
<ThemeChoice
|
||||
key={t.id}
|
||||
label={t.name}
|
||||
colors={colors}
|
||||
selected={isActive}
|
||||
onClick={() => setTheme(t.id)}
|
||||
className={`flex items-center gap-2 rounded-lg px-2 py-1.5 text-left transition duration-base ease-standard transform border
|
||||
${isActive
|
||||
? 'border-accent'
|
||||
: 'border-line hover:border-accent-line'
|
||||
}`}
|
||||
style={{ backgroundColor: colors.base }}
|
||||
>
|
||||
{isActive ? (
|
||||
<CheckIcon className="h-3.5 w-3.5 shrink-0 text-accent" />
|
||||
) : (
|
||||
<span className="w-3.5 shrink-0" />
|
||||
)}
|
||||
<span
|
||||
className="text-xs font-medium w-14 shrink-0"
|
||||
style={{ color: colors.foreground }}
|
||||
>
|
||||
{t.name}
|
||||
</span>
|
||||
<div className="flex gap-1 ml-auto">
|
||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.background }} />
|
||||
<div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.offbase }} />
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} />
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} />
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
|
||||
</div>
|
||||
</button>
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@ import {
|
|||
listboxOptionClass,
|
||||
listboxOptionsClass,
|
||||
Button,
|
||||
IconButton,
|
||||
Input,
|
||||
MenuItemClass,
|
||||
cn,
|
||||
menuPanelClass,
|
||||
} from '@/components/ui';
|
||||
|
||||
type ProviderType = TtsProviderId;
|
||||
|
|
@ -641,7 +645,9 @@ export function AdminProvidersPanel() {
|
|||
</div>
|
||||
<Menu as="div" className="relative shrink-0">
|
||||
<MenuButton
|
||||
className="inline-flex items-center justify-center h-7 w-7 rounded-md border border-offbase bg-base text-muted hover:text-accent hover:bg-offbase transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent disabled:opacity-50"
|
||||
as={IconButton}
|
||||
tone="surface"
|
||||
size="sm"
|
||||
title="Provider actions"
|
||||
aria-label="Provider actions"
|
||||
disabled={!!editingId || deleteMutation.isPending || toggleEnabledMutation.isPending || setDefaultMutation.isPending}
|
||||
|
|
@ -659,14 +665,14 @@ export function AdminProvidersPanel() {
|
|||
>
|
||||
<MenuItems
|
||||
anchor="bottom end"
|
||||
className="z-50 mt-2 min-w-[170px] rounded-md bg-base shadow-lg ring-1 ring-black/5 focus:outline-none p-1"
|
||||
className={cn(menuPanelClass, 'z-50 mt-2 min-w-[170px] bg-base focus:outline-none')}
|
||||
>
|
||||
<MenuItem>
|
||||
{({ active }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => startEdit(p)}
|
||||
className={`${active ? 'bg-offbase text-accent' : 'text-foreground'} flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
|
||||
className={MenuItemClass(active)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
|
|
@ -678,7 +684,7 @@ export function AdminProvidersPanel() {
|
|||
type="button"
|
||||
onClick={() => setDefault(p.slug)}
|
||||
disabled={disabled}
|
||||
className={`${disabled ? 'text-faint cursor-not-allowed' : active ? 'bg-offbase text-accent' : 'text-foreground'} flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
|
||||
className={cn(MenuItemClass(active), disabled ? 'cursor-not-allowed text-faint' : '')}
|
||||
>
|
||||
Set as default
|
||||
</button>
|
||||
|
|
@ -689,7 +695,7 @@ export function AdminProvidersPanel() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => toggleEnabled(p)}
|
||||
className={`${active ? 'bg-offbase text-accent' : 'text-foreground'} flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
|
||||
className={MenuItemClass(active)}
|
||||
>
|
||||
{p.enabled ? 'Disable' : 'Enable'}
|
||||
</button>
|
||||
|
|
@ -700,7 +706,7 @@ export function AdminProvidersPanel() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => remove(p.id)}
|
||||
className={`${active ? 'bg-offbase' : ''} text-danger flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
|
||||
className={MenuItemClass(active, 'danger')}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type { Folder, SidebarFilter } from '@/types/documents';
|
|||
import { PDFIcon, EPUBIcon, FileIcon, DotsHorizontalIcon } from '@/components/icons/Icons';
|
||||
import { FolderIcon, HomeIcon, ClockIcon, FolderPlusIcon } from './finderIcons';
|
||||
import { DND_DOCUMENT, type DocumentDragItem } from '../dnd/dndTypes';
|
||||
import { IconButton, MenuItemClass, Sidebar as SidebarShell, SidebarNav, SidebarNavGroup, SidebarNavItem } from '@/components/ui';
|
||||
import { IconButton, MenuItemClass, Sidebar as SidebarShell, SidebarNav, SidebarNavGroup, SidebarNavItem, menuPanelClass } from '@/components/ui';
|
||||
|
||||
interface FinderSidebarProps {
|
||||
filter: SidebarFilter;
|
||||
|
|
@ -222,7 +222,7 @@ export function FinderSidebar({
|
|||
>
|
||||
<MenuItems
|
||||
anchor="bottom start"
|
||||
className="z-50 mt-2 min-w-[180px] rounded-md bg-surface shadow-elev-2 ring-1 ring-line-soft focus:outline-none p-1 normal-case tracking-normal font-normal"
|
||||
className={`${menuPanelClass} z-50 mt-2 min-w-[180px] focus:outline-none normal-case tracking-normal font-normal`}
|
||||
>
|
||||
<MenuItem>
|
||||
{({ active }) => (
|
||||
|
|
|
|||
32
src/components/ui/choice-tile.tsx
Normal file
32
src/components/ui/choice-tile.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
import { cn } from './cn';
|
||||
import { focusRing, motionColors } from './tokens';
|
||||
|
||||
export function ChoiceTile({
|
||||
selected = false,
|
||||
children,
|
||||
className,
|
||||
type = 'button',
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
selected?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
aria-pressed={selected}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg border px-2 py-1.5 text-left',
|
||||
'transition duration-base ease-standard',
|
||||
focusRing,
|
||||
motionColors,
|
||||
selected ? 'border-accent-line' : 'border-line hover:border-accent-line',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
export * from './badge';
|
||||
export * from './button';
|
||||
export * from './choice-tile';
|
||||
export * from './cn';
|
||||
export * from './dialog';
|
||||
export * from './divider';
|
||||
|
|
|
|||
Loading…
Reference in a new issue