diff --git a/app/components/device/new/device-info.tsx b/app/components/device/new/device-info.tsx index 8186f4dc..cedaf921 100644 --- a/app/components/device/new/device-info.tsx +++ b/app/components/device/new/device-info.tsx @@ -121,11 +121,14 @@ export function DeviceSelectionStep() { return ( { if (selectedDevice === 'senseBox:Home') { @@ -133,24 +136,38 @@ export function DeviceSelectionStep() { } handleDeviceChange(device.name) }} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + + if (selectedDevice === 'senseBox:Home') { + return + } + + handleDeviceChange(device.name) + } + }} > - {device.name} +
+ {device.name} +
+
{selectedDevice === 'senseBox:Home' && ( )} -

+ +

{device.name}

+ {device.name === 'senseBox:Home' && selectedDevice === 'senseBox:Home' && ( <> +
-

+

{t('connection_type')}

+ @@ -183,7 +204,10 @@ export function DeviceSelectionStep() { className="flex items-center space-x-2" > -
diff --git a/app/components/device/new/general-info.tsx b/app/components/device/new/general-info.tsx index 3bc640f7..f58d3cd6 100644 --- a/app/components/device/new/general-info.tsx +++ b/app/components/device/new/general-info.tsx @@ -14,6 +14,7 @@ import { TooltipProvider, TooltipTrigger, } from '~/components/ui/tooltip' +import { cn } from '~/lib/utils' type ExposureOption = 'outdoor' | 'indoor' | 'mobile' | 'unknown' @@ -140,17 +141,23 @@ export function GeneralInfoStep() { {exposureOptions.map((option) => ( ))}
diff --git a/app/components/device/new/location-info.tsx b/app/components/device/new/location-info.tsx index f33331da..6587ccd9 100644 --- a/app/components/device/new/location-info.tsx +++ b/app/components/device/new/location-info.tsx @@ -136,7 +136,7 @@ export function LocationStep() { -
+
{/* Breadcrumb Navigation */} @@ -192,8 +192,8 @@ export default function NewDeviceStepper() { onClick={() => stepper.navigation.goTo(step.id)} className={` ${ stepper.state.current.index === step.index - ? 'font-bold text-black' - : 'cursor-pointer text-gray-500 hover:text-black' + ? 'text-foreground font-bold' + : 'hover:text-foreground text-muted-foreground cursor-pointer' } `} > {t(step.label)} diff --git a/app/components/device/new/summary-info.tsx b/app/components/device/new/summary-info.tsx index d3f5ab35..6213f14c 100644 --- a/app/components/device/new/summary-info.tsx +++ b/app/components/device/new/summary-info.tsx @@ -1,7 +1,6 @@ import { MapPin, Tag, Smartphone, Cpu, Cog } from 'lucide-react' import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' -import { Badge } from '@/components/ui/badge' import { Card, CardContent } from '@/components/ui/card' export function SummaryInfo() { diff --git a/app/components/landing/header/language-selector.tsx b/app/components/landing/header/language-selector.tsx index 879135dd..048d9999 100644 --- a/app/components/landing/header/language-selector.tsx +++ b/app/components/landing/header/language-selector.tsx @@ -1,6 +1,11 @@ import { Languages } from 'lucide-react' import { useFetcher } from 'react-router' import { Button } from '~/components/ui/button' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '~/components/ui/tooltip' import { useRootRouteLoaderData } from '~/root' export default function LanguageSelector() { @@ -16,21 +21,24 @@ export default function LanguageSelector() { ) } + const languageLabel = locale === 'de' ? 'Deutsch' : 'English' + const nextLanguageLabel = locale === 'de' ? 'English' : 'Deutsch' + return ( -
- + + + + -
- {locale.toUpperCase()} -
-
+ {languageLabel} + ) } diff --git a/app/components/landing/header/theme-toggle.tsx b/app/components/landing/header/theme-toggle.tsx new file mode 100644 index 00000000..cfda60a6 --- /dev/null +++ b/app/components/landing/header/theme-toggle.tsx @@ -0,0 +1,75 @@ +import { Moon, Sun } from 'lucide-react' +import { useFetcher } from 'react-router' +import { Button } from '~/components/ui/button' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '~/components/ui/tooltip' +import type { ThemePreference } from '~/lib/theme' +import { useRootRouteLoaderData } from '~/root' + +function applyThemePreference(preference: ThemePreference) { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches + + const resolvedTheme = + preference === 'dark' || (preference === 'system' && prefersDark) + ? 'dark' + : 'light' + + const root = document.documentElement + + root.classList.remove('light', 'dark') + root.classList.add(resolvedTheme) + root.style.colorScheme = resolvedTheme +} + +function getCurrentResolvedTheme(): 'light' | 'dark' { + return document.documentElement.classList.contains('dark') ? 'dark' : 'light' +} + +export default function ThemeToggle() { + const { themePreference } = useRootRouteLoaderData() + const fetcher = useFetcher() + + const toggleTheme = () => { + const currentTheme = getCurrentResolvedTheme() + + const nextThemePreference: ThemePreference = + currentTheme === 'dark' ? 'light' : 'dark' + + applyThemePreference(nextThemePreference) + + void fetcher.submit( + { 'set-theme': nextThemePreference }, + { method: 'post', action: '/' }, + ) + } + + const tooltipText = + themePreference === 'system' + ? 'System theme' + : `${themePreference[0].toUpperCase()}${themePreference.slice(1)} theme` + + return ( + + + + + + + {tooltipText} + + + ) +} diff --git a/app/components/language-select.tsx b/app/components/language-select.tsx new file mode 100644 index 00000000..43830d23 --- /dev/null +++ b/app/components/language-select.tsx @@ -0,0 +1,53 @@ +import { useFetcher } from 'react-router' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '~/components/ui/select' +import { useRootRouteLoaderData } from '~/root' + +const supportedLocales = ['en', 'de'] as const + +type SupportedLocale = (typeof supportedLocales)[number] + +function isSupportedLocale(value: unknown): value is SupportedLocale { + return ( + typeof value === 'string' && + (supportedLocales as readonly string[]).includes(value) + ) +} + +export function LanguageSelect() { + const { locale } = useRootRouteLoaderData() + const fetcher = useFetcher<{ ok: boolean }>() + + const optimisticLocale = fetcher.formData?.get('set-language') + const currentLocale = isSupportedLocale(optimisticLocale) + ? optimisticLocale + : locale + + return ( + + ) +} diff --git a/app/components/map/topbar.tsx b/app/components/map/topbar.tsx index eef4f49c..00b53de4 100644 --- a/app/components/map/topbar.tsx +++ b/app/components/map/topbar.tsx @@ -4,6 +4,7 @@ import NavBar from '../header/nav-bar' import Info from '../header/info' import LanguageSelector from '../landing/header/language-selector' import { TooltipProvider } from '../ui/tooltip' +import ThemeToggle from '../landing/header/theme-toggle' interface MapHeaderProps { devices: any @@ -34,6 +35,7 @@ export default function MapHeader({
+
diff --git a/app/components/mydevices/dt/columns.tsx b/app/components/mydevices/dt/columns.tsx index 22d4cc9a..609acee9 100644 --- a/app/components/mydevices/dt/columns.tsx +++ b/app/components/mydevices/dt/columns.tsx @@ -1,12 +1,7 @@ 'use client' import { type ColumnDef } from '@tanstack/react-table' -import { - ArrowUpDown, - ClipboardCopy, - Ellipsis, - LucideMapPin, -} from 'lucide-react' +import { ArrowUpDown, Ellipsis, LucideMapPin } from 'lucide-react' import { type UseTranslationResponse } from 'react-i18next' import { Link } from 'react-router' import { Button } from '@/components/ui/button' @@ -19,6 +14,7 @@ import { DropdownMenuTrigger, } from '~/components/ui/dropdown-menu' import { type Device } from '~/db/schema' +import { DeviceIdCell } from './device-id-cell' export type SenseBox = { id: string @@ -130,21 +126,17 @@ export function getColumns( { accessorKey: 'id', header: () => ( -
{t('device_id')}
+
{t('device_id')}
), cell: ({ row }) => { const device = row.original return ( -
- - {device?.id} - - navigator.clipboard.writeText(device?.id)} - className="mr-1 ml-[6px] inline-block h-4 w-4 cursor-pointer align-text-bottom text-[#818a91] dark:text-white" - /> -
+ ) }, }, diff --git a/app/components/mydevices/dt/data-table.tsx b/app/components/mydevices/dt/data-table.tsx index 925e0f99..c232dd15 100644 --- a/app/components/mydevices/dt/data-table.tsx +++ b/app/components/mydevices/dt/data-table.tsx @@ -87,11 +87,11 @@ export function DataTable({ onChange={(event) => table.getColumn('name')?.setFilterValue(event.target.value) } - className="max-w-sm dark:border-white dark:text-white" + className="border-input bg-background text-foreground placeholder:text-muted-foreground max-w-sm" />
-
+
{table.getHeaderGroups().map((headerGroup) => ( @@ -110,7 +110,7 @@ export function DataTable({ ))} - + {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( ({ {row.getVisibleCells().map((cell, index) => ( {flexRender( cell.column.columnDef.cell, @@ -135,7 +135,7 @@ export function DataTable({ {t('no_results')} @@ -145,7 +145,7 @@ export function DataTable({
-
+
{t('rows_per_page')} @@ -155,7 +155,7 @@ export function DataTable({ table.setPageSize(Number(value)) }} > - + diff --git a/app/components/mydevices/dt/device-id-cell.tsx b/app/components/mydevices/dt/device-id-cell.tsx new file mode 100644 index 00000000..cb569d40 --- /dev/null +++ b/app/components/mydevices/dt/device-id-cell.tsx @@ -0,0 +1,55 @@ +import { Button } from '@/components/ui/button' +import { CopyCheckIcon, CopyIcon } from 'lucide-react' +import { toast } from '~/components/ui/use-toast' +import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard' +import { useTranslation } from 'react-i18next' + +export function DeviceIdCell({ + deviceId, + copyLabel, + copiedLabel, +}: { + deviceId: string + copyLabel: string + copiedLabel: string +}) { + const { copiedToClipboard, copyToClipboard } = useCopyToClipboard() + const { t } = useTranslation('data-table') + const handleCopyId = async () => { + const copied = await copyToClipboard(deviceId) + + if (!copied) return + + toast({ + title: t('copied'), + variant: 'success', + }) + } + + return ( +
+ + {deviceId} + + + +
+ ) +} diff --git a/app/components/prevent-theme-flash.tsx b/app/components/prevent-theme-flash.tsx new file mode 100644 index 00000000..552ecf67 --- /dev/null +++ b/app/components/prevent-theme-flash.tsx @@ -0,0 +1,37 @@ +import { ThemePreference } from '~/lib/theme' + +export function PreventFlashOnWrongTheme({ + themePreference, +}: { + themePreference: ThemePreference +}) { + const script = ` + /* + The server can read the saved theme preference from the cookie/database, + but it cannot know the browser's current "prefers-color-scheme" value. + + This matters when the saved preference is "system": + - "light" and "dark" are explicit and can be rendered correctly by the server. + - "system" must be resolved in the browser because it depends on + window.matchMedia('(prefers-color-scheme: dark)'). + + Running this small script in the document head lets us update the + class before the CSS is painted, preventing a flash of the wrong + theme during initial page load. + */ +(() => { + const preference = ${JSON.stringify(themePreference)}; + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const theme = preference === 'dark' || (preference === 'system' && prefersDark) + ? 'dark' + : 'light'; + + const root = document.documentElement; + root.classList.remove('light', 'dark'); + root.classList.add(theme); + root.style.colorScheme = theme; +})(); +` + + return