diff --git a/app/sites/[id]/replays/page.tsx b/app/sites/[id]/replays/page.tsx
index 79fc82d..7fdfd10 100644
--- a/app/sites/[id]/replays/page.tsx
+++ b/app/sites/[id]/replays/page.tsx
@@ -7,6 +7,7 @@ import { listReplays, formatDuration, type ReplayListItem, type ReplayFilters }
import { toast } from 'sonner'
import { LockClosedIcon } from '@radix-ui/react-icons'
import LoadingOverlay from '@/components/LoadingOverlay'
+import Select from '@/components/ui/Select'
function formatDate(dateString: string) {
const date = new Date(dateString)
@@ -128,28 +129,28 @@ export default function ReplaysPage() {
{/* Filters */}
-
+
{/* Replays List */}
diff --git a/app/sites/[id]/settings/page.tsx b/app/sites/[id]/settings/page.tsx
index 607b612..8222a0e 100644
--- a/app/sites/[id]/settings/page.tsx
+++ b/app/sites/[id]/settings/page.tsx
@@ -8,6 +8,7 @@ import { toast } from 'sonner'
import LoadingOverlay from '@/components/LoadingOverlay'
import VerificationModal from '@/components/sites/VerificationModal'
import PasswordInput from '@/components/PasswordInput'
+import Select from '@/components/ui/Select'
import { APP_URL, API_URL } from '@/lib/api/client'
import { motion, AnimatePresence } from 'framer-motion'
import {
@@ -305,26 +306,15 @@ export default function SiteSettingsPage() {
-
-
setFormData({ ...formData, timezone: e.target.value })}
- className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-neutral-50/50 dark:bg-neutral-900/50 focus:bg-white dark:focus:bg-neutral-900
- focus:border-brand-orange focus:ring-4 focus:ring-brand-orange/10 outline-none transition-all duration-200 dark:text-white appearance-none"
- >
- {TIMEZONES.map((tz) => (
-
- ))}
-
-
-
+ setFormData({ ...formData, timezone: v })}
+ options={TIMEZONES.map((tz) => ({ value: tz, label: tz }))}
+ variant="input"
+ fullWidth
+ align="left"
+ />
@@ -649,22 +639,18 @@ export default function SiteSettingsPage() {
Control location tracking granularity
-
-
setFormData({ ...formData, collect_geo_data: e.target.value as GeoDataLevel })}
- className="px-4 py-2 border border-neutral-200 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white text-sm font-medium appearance-none pr-10 focus:outline-none focus:ring-2 focus:ring-brand-orange/20 focus:border-brand-orange"
- >
-
-
-
-
-
-
+ setFormData({ ...formData, collect_geo_data: v as GeoDataLevel })}
+ options={[
+ { value: 'full', label: 'Full (country, region, city)' },
+ { value: 'country', label: 'Country only' },
+ { value: 'none', label: 'None' },
+ ]}
+ variant="input"
+ align="right"
+ className="min-w-[200px]"
+ />
@@ -850,24 +836,20 @@ export default function SiteSettingsPage() {
How long to keep recordings
-
-
setFormData({ ...formData, replay_retention_days: parseInt(e.target.value) })}
- className="px-4 py-2 border border-neutral-200 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white text-sm font-medium appearance-none pr-10 focus:outline-none focus:ring-2 focus:ring-brand-orange/20 focus:border-brand-orange"
- >
-
-
-
-
-
-
-
-
+ setFormData({ ...formData, replay_retention_days: parseInt(v, 10) })}
+ options={[
+ { value: '7', label: '7 days' },
+ { value: '14', label: '14 days' },
+ { value: '30', label: '30 days' },
+ { value: '60', label: '60 days' },
+ { value: '90', label: '90 days' },
+ ]}
+ variant="input"
+ align="right"
+ className="min-w-[120px]"
+ />
diff --git a/components/ui/Select.tsx b/components/ui/Select.tsx
index 87113a2..18bb326 100644
--- a/components/ui/Select.tsx
+++ b/components/ui/Select.tsx
@@ -12,9 +12,29 @@ interface SelectProps {
onChange: (value: string) => void
options: Option[]
className?: string
+ /** Form-field style (input-like). Default: toolbar style (btn-secondary). */
+ variant?: 'default' | 'input'
+ /** Shown when value is empty or does not match any option. */
+ placeholder?: string
+ /** Full-width trigger and panel. Use in form layouts. */
+ fullWidth?: boolean
+ /** Id for the trigger (e.g. for label htmlFor). */
+ id?: string
+ /** Alignment of the dropdown panel. */
+ align?: 'left' | 'right'
}
-export default function Select({ value, onChange, options, className = '' }: SelectProps) {
+export default function Select({
+ value,
+ onChange,
+ options,
+ className = '',
+ variant = 'default',
+ placeholder,
+ fullWidth = false,
+ id,
+ align = 'right',
+}: SelectProps) {
const [isOpen, setIsOpen] = useState(false)
const ref = useRef(null)
@@ -28,18 +48,35 @@ export default function Select({ value, onChange, options, className = '' }: Sel
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
- const selectedOption = options.find(o => o.value === value) || options.find(o => o.value === 'custom')
+ const selectedOption = options.find((o) => o.value === value)
+ const displayLabel = selectedOption?.label ?? placeholder ?? value ?? ''
+
+ const triggerBase =
+ variant === 'input'
+ ? 'px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-neutral-50/50 dark:bg-neutral-900/50 text-neutral-900 dark:text-white text-left text-sm ' +
+ 'focus:outline-none focus:border-brand-orange focus:ring-4 focus:ring-brand-orange/10 transition-all duration-200 ' +
+ (isOpen ? 'ring-4 ring-brand-orange/10 border-brand-orange' : '')
+ : 'btn-secondary min-w-[140px]'
+
+ const triggerLayout = fullWidth ? 'w-full ' : ''
+ const alignClass = align === 'left' ? 'left-0' : 'right-0'
+ const panelMinW = fullWidth ? 'w-full' : 'min-w-[140px] w-full'
return (
-
+