Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c0d3a96
feat(wip): add theming
jona159 Jun 8, 2026
21db219
fix: color
jona159 Jun 10, 2026
f2fdea5
feat: add theme preference to user schema
jona159 Jun 10, 2026
1a00ca8
feat: apply theme preference in root loader
jona159 Jun 10, 2026
e409583
fix: colors
jona159 Jun 10, 2026
d48a332
fix: use theme tokens
jona159 Jun 10, 2026
d631f10
feat: theme preference migration
jona159 Jun 10, 2026
2f7250d
feat: new settings tab for preferences
jona159 Jun 11, 2026
4b3e416
fix: rm dark classes
jona159 Jun 11, 2026
b4f8776
fix: add red 900 color
jona159 Jun 11, 2026
66de738
fix: rm language from account settings
jona159 Jun 11, 2026
702c6c8
refactor: use hex colors
jona159 Jun 11, 2026
86d2aff
fix: styles for dark mode
jona159 Jun 11, 2026
6d8408a
feat: adjust styles for dark mode
jona159 Jun 11, 2026
fe6ce25
fix: selected exposure button darkmode
jona159 Jun 11, 2026
86e7f9e
fix: location step background
jona159 Jun 11, 2026
e2f54a7
feat: use style tokens in checkbox component for darkmode compat
jona159 Jun 11, 2026
1df2469
feat: improve switch visibility on darkmode
jona159 Jun 11, 2026
9480c7d
fix: use tooltip in topbar toggles
jona159 Jun 11, 2026
6de7dc4
feat: outsource device id cell for copy logic
jona159 Jun 11, 2026
1a8c3e9
fix: adjust destructive btn variant for weaker red in dark mode
jona159 Jun 11, 2026
55a882c
feat: adjust card component for dark mode
jona159 Jun 11, 2026
97d0762
feat: use color tokens in input component for dark mode
jona159 Jun 11, 2026
02965c7
feat: use color tokens in tabs component for dark mode
jona159 Jun 11, 2026
0f866ca
feat: translations
jona159 Jun 11, 2026
b01f070
feat: adjust tabs styling in settings
jona159 Jun 11, 2026
de42fbf
fix: rm dark specific styles on card
jona159 Jun 11, 2026
a33a422
fix: use button component
jona159 Jun 11, 2026
0c2609c
fix: destructive btn to delete device
jona159 Jun 11, 2026
b96d87a
fix: styles
jona159 Jun 11, 2026
8019954
fix: styling
jona159 Jun 11, 2026
79d7a9f
refactor: outsource prevent theme flash component
jona159 Jun 11, 2026
f756315
fix: language format
jona159 Jun 12, 2026
b33874f
feat: popup styling
jona159 Jun 15, 2026
f1be3b8
fix: decrease font weight
jona159 Jun 15, 2026
0e3770e
fix: json formatting
jona159 Jun 15, 2026
5d1a4bf
feat: adjust popup offset to point at the marker center
jona159 Jun 15, 2026
012247b
fix: typo
jona159 Jun 15, 2026
8707114
Merge branch 'dev' into feat/theme
zven Jun 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 42 additions & 18 deletions app/components/device/new/device-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,36 +121,53 @@ export function DeviceSelectionStep() {
return (
<Card
key={device.name}
role="button"
tabIndex={0}
className={cn(
'relative transform cursor-pointer overflow-hidden transition-all duration-300 ease-in-out hover:scale-105',
selectedDevice === device.name
? 'bg-primary/10 ring-primary ring-2'
: 'hover:bg-gray-50',
'border-border bg-card text-card-foreground relative transform cursor-pointer overflow-hidden rounded-xl border transition-all duration-300 ease-in-out',
'hover:border-primary/40 hover:bg-muted/40 hover:-translate-y-0.5 hover:shadow-md',
'focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none',
selectedDevice === device.name &&
'border-primary bg-primary/10 ring-primary/40 shadow-sm ring-2',
)}
onClick={() => {
if (selectedDevice === 'senseBox:Home') {
return
}
handleDeviceChange(device.name)
}}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()

if (selectedDevice === 'senseBox:Home') {
return
}

handleDeviceChange(device.name)
}
}}
>
<CardContent className="flex flex-row p-0">
<img
src={device.image}
alt={device.name}
className={cn(
'w-24 self-stretch',
device.imageHasPadding
? 'object-cover'
: 'object-contain p-4',
)}
/>
<div className="border-border flex w-24 shrink-0 items-center justify-center border-r bg-white">
<img
src={device.image}
alt={device.name}
className={cn(
'h-full w-full',
device.imageHasPadding
? 'object-cover'
: 'object-contain p-4',
)}
/>
</div>

<div className="flex min-w-0 flex-1 flex-col justify-center p-3">
{selectedDevice === 'senseBox:Home' && (
<Button
variant="ghost"
size="icon"
className="absolute top-2 right-2"
className="text-muted-foreground hover:bg-muted hover:text-foreground absolute top-2 right-2"
onClick={(e) => {
e.stopPropagation()
handleClose()
Expand All @@ -159,17 +176,21 @@ export function DeviceSelectionStep() {
<X className="h-4 w-4" />
</Button>
)}
<h3 className="text-lg font-semibold wrap-break-word">

<h3 className="text-foreground text-lg font-semibold wrap-break-word">
{device.name}
</h3>

{device.name === 'senseBox:Home' &&
selectedDevice === 'senseBox:Home' && (
<>
<Separator className="my-2" />

<div className="w-full max-w-xs">
<h4 className="mb-2 text-sm font-medium">
<h4 className="text-muted-foreground mb-2 text-sm font-medium">
{t('connection_type')}
</h4>

<RadioGroup
value={selectedConnectionType}
onValueChange={(value) =>
Expand All @@ -183,7 +204,10 @@ export function DeviceSelectionStep() {
className="flex items-center space-x-2"
>
<RadioGroupItem value={type} id={type} />
<Label htmlFor={type} className="text-sm">
<Label
htmlFor={type}
className="text-foreground text-sm"
>
{type}
</Label>
</div>
Expand Down
21 changes: 14 additions & 7 deletions app/components/device/new/general-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from '~/components/ui/tooltip'
import { cn } from '~/lib/utils'

type ExposureOption = 'outdoor' | 'indoor' | 'mobile' | 'unknown'

Expand Down Expand Up @@ -140,17 +141,23 @@ export function GeneralInfoStep() {
{exposureOptions.map((option) => (
<Button
key={option.value}
type="button" // Prevent form submission
type="button"
onClick={() => setValue('exposure', option.value)}
variant={'outline'}
className={`flex items-center gap-2 transition-all duration-200 ease-in-out ${
variant="outline"
aria-pressed={currentExposure === option.value}
className={cn(
'flex items-center gap-2 transition-all duration-200 ease-in-out',
currentExposure === option.value
? 'bg-green-100 shadow-md hover:bg-green-100'
: 'hover:bg-gray-100'
}`}
? [
'border-primary bg-primary/10 text-primary ring-primary/40 shadow-md ring-2',
'hover:bg-primary/15',
'dark:border-primary dark:bg-primary/20 dark:text-primary dark:hover:bg-primary/25',
]
: 'hover:bg-muted',
)}
>
{option.icon}
<span className="text-sm">{t(option.label)}</span>
<span className="text-sm">{option.label}</span>
</Button>
))}
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/components/device/new/location-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export function LocationStep() {
</BaseMap>
</div>

<div className="flex w-full items-center justify-around bg-gray-50 p-4 dark:bg-gray-800">
<div className="bg-background flex w-full items-center justify-around p-4">
<div>
<Label htmlFor="latitude">{t('latitude')}</Label>
<Input
Expand Down
6 changes: 3 additions & 3 deletions app/components/device/new/new-device-stepper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export default function NewDeviceStepper() {
<FormProvider {...form}>
<Form
onSubmit={form.handleSubmit(onSubmit, onError)}
className="flex h-full w-1/2 flex-col justify-between space-y-6 rounded-lg border bg-white p-6"
className="bg-card flex h-full w-1/2 flex-col justify-between space-y-6 rounded-lg border p-6"
>
<div className="space-y-4">
{/* Breadcrumb Navigation */}
Expand All @@ -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)}
Expand Down
1 change: 0 additions & 1 deletion app/components/device/new/summary-info.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
36 changes: 22 additions & 14 deletions app/components/landing/header/language-selector.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -16,21 +21,24 @@ export default function LanguageSelector() {
)
}

const languageLabel = locale === 'de' ? 'Deutsch' : 'English'
const nextLanguageLabel = locale === 'de' ? 'English' : 'Deutsch'

return (
<div className="group relative">
<Button
variant="topbar"
size="topbarPill"
onClick={toggleLanguage}
disabled={fetcher.state !== 'idle'}
aria-label={`Current language: ${locale.toUpperCase()}`}
>
<Languages />
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="topbar"
size="topbarPill"
onClick={toggleLanguage}
disabled={fetcher.state !== 'idle'}
aria-label={`Current language: ${languageLabel}. Switch to ${nextLanguageLabel}.`}
>
<Languages />
</Button>
</TooltipTrigger>

<div className="bg-popover text-popover-foreground pointer-events-none absolute top-full left-1/2 mt-2 -translate-x-1/2 rounded-md px-2 py-1 text-xs opacity-0 shadow-md transition-opacity group-hover:opacity-100">
{locale.toUpperCase()}
</div>
</div>
<TooltipContent side="bottom">{languageLabel}</TooltipContent>
</Tooltip>
)
}
75 changes: 75 additions & 0 deletions app/components/landing/header/theme-toggle.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="topbar"
size="topbarPill"
onClick={toggleTheme}
disabled={fetcher.state !== 'idle'}
aria-label={`Toggle theme. Current preference: ${themePreference}`}
>
<Sun className="block dark:hidden" />
<Moon className="hidden dark:block" />
</Button>
</TooltipTrigger>

<TooltipContent side="bottom">{tooltipText}</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
53 changes: 53 additions & 0 deletions app/components/language-select.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Select
value={currentLocale}
disabled={fetcher.state !== 'idle'}
onValueChange={(nextLocale) => {
if (!isSupportedLocale(nextLocale)) return

void fetcher.submit(
{ 'set-language': nextLocale },
{ method: 'post', action: '/' },
)
}}
>
<SelectTrigger className="border-input bg-background text-foreground w-36">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="en">English</SelectItem>
<SelectItem value="de">Deutsch</SelectItem>
</SelectContent>
</Select>
)
}
2 changes: 2 additions & 0 deletions app/components/map/topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -34,6 +35,7 @@ export default function MapHeader({

<div className="flex shrink-0 items-center gap-4">
<LanguageSelector />
<ThemeToggle />

<div className="h-7 w-px bg-black/10 dark:bg-white/15" />

Expand Down
Loading
Loading