From d3638868a3950c510cf929115063e2f2270def68 Mon Sep 17 00:00:00 2001 From: Jere Vaara Date: Sat, 21 Feb 2026 15:58:17 +0200 Subject: [PATCH 01/18] wip changing theme --- src/components/AreaSelector.tsx | 88 ++++--- src/components/Button.tsx | 16 +- src/components/DaySelector.tsx | 115 ++++++---- src/components/Modal.tsx | 25 +- src/components/PageContainer.tsx | 7 +- src/components/Radio.tsx | 50 ++-- src/components/RoundedButton.tsx | 8 +- .../Settings/PriceCategorySelector.tsx | 29 +-- src/components/Settings/Settings.tsx | 216 ++++++++++-------- src/components/Toggle.tsx | 84 ++++--- src/components/TopBar/TopBar.tsx | 72 ++++-- src/globalStyles.ts | 20 ++ src/icons.ts | 12 +- 13 files changed, 427 insertions(+), 315 deletions(-) diff --git a/src/components/AreaSelector.tsx b/src/components/AreaSelector.tsx index 5d0249a..0ee4d40 100644 --- a/src/components/AreaSelector.tsx +++ b/src/components/AreaSelector.tsx @@ -3,7 +3,6 @@ import { css, styled } from 'solid-styled-components'; import { FilledStarIcon, WalkIcon } from '../icons'; import { computedState, resources, setState, state } from '../state'; import type allTranslations from '../translations'; -import Button from './Button'; const iconStyles = css` margin-right: 0.5ch; @@ -37,52 +36,71 @@ const specialAreas: SpecialArea[] = [ }, ]; -const AreaWrapper = styled.div` - width: 50%; - box-sizing: border-box; -`; - -const AreaButton = styled(Button)<{ selected: boolean }>` - background-color: ${props => - props.selected ? 'var(--gray6)' : 'transparent'}; - color: inherit; +const AreaButton = styled.button<{ selected: boolean }>` width: 100%; - padding: 1em 0.5em; - border-radius: 4px; - font-weight: inherit; - text-align: center; - outline: none; + border-radius: var(--radius-sm); + padding: 0.6em 0.75em; + display: flex; + align-items: center; + justify-content: space-between; + text-align: left; + font-size: 0.875rem; + font-family: inherit; + font-weight: ${props => (props.selected ? '500' : 'inherit')}; + background: ${props => (props.selected ? 'var(--gray5)' : 'transparent')}; color: ${props => (props.selected ? 'var(--accent_color)' : 'var(--gray1)')}; + border: none; cursor: pointer; + transition: background 0.1s, color 0.1s; + + &:hover { + background: var(--gray5); + } - &:hover, &:focus { - background: var(--gray6); + outline: 2px solid var(--accent_color); + outline-offset: -2px; } `; +const Checkmark = styled.span` + color: var(--accent_color); + font-size: 0.9rem; + font-weight: 600; +`; + +const Divider = styled.hr` + border: none; + border-top: 1px solid var(--gray5); + margin: 4px 8px; +`; + const Area = (props: { area: { icon?: JSX.Element; label: JSX.Element; id: number }; selectedAreaId: number; selectArea: (id: number) => void; -}) => ( - +}) => { + const selected = () => props.selectedAreaId === props.area.id; + return ( e.key === 'Enter' && props.selectArea(props.area.id) } onMouseUp={() => props.selectArea(props.area.id)} - selected={props.selectedAreaId === props.area.id} + selected={selected()} > - {props.area.icon && ( -
- {props.area.icon} -
- )} - {props.area.label} + + {props.area.icon && ( + + {props.area.icon} + + )} + {props.area.label} + + {selected() && }
-
-); + ); +}; interface Props { onAreaSelected?: () => void; @@ -90,11 +108,10 @@ interface Props { const Container = styled.menu` margin: 0; - padding: 0; + padding: 4px 0; display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: space-between; + flex-direction: column; + gap: 2px; user-select: none; `; @@ -123,7 +140,12 @@ export default function AreaSelector(props: Props) { /> )} - (a.name > b.name ? -1 : 1))}> + + (a.name > b.name ? 1 : -1))} + > {area => ( ` border: none; padding: 0.8em 1.2em; - border-radius: 0.4em; + border-radius: var(--radius-md); font-family: inherit; font-size: 0.8rem; display: inline-block; - text-transform: uppercase; min-width: 4rem; background: ${props => props.color === 'secondary' || props.secondary @@ -21,21 +20,26 @@ const Button = styled.button` : 'var(--accent_color)'}; text-align: center; color: var(--gray6); - outline: none; - font-weight: 500; - transition: transform 0.1s; + font-weight: 600; + letter-spacing: 0.01em; + transition: transform 0.1s, box-shadow 0.1s; opacity: 0.95; &:hover { opacity: 1; + transform: translateY(-1px); + box-shadow: var(--shadow-sm); } &:active { - transform: scale(0.98); + transform: translateY(0) scale(0.98); + box-shadow: none; } &:focus { color: var(--gray6); + outline: 2px solid var(--accent_color); + outline-offset: 2px; } &:disabled { diff --git a/src/components/DaySelector.tsx b/src/components/DaySelector.tsx index c7041b7..ee6c26d 100644 --- a/src/components/DaySelector.tsx +++ b/src/components/DaySelector.tsx @@ -1,12 +1,10 @@ -import { useLocation } from '@solidjs/router'; +import { A, useLocation } from '@solidjs/router'; import { format, isSameDay } from 'date-fns'; -import { For } from 'solid-js'; +import { createMemo, For, type JSX, splitProps } from 'solid-js'; import { styled } from 'solid-styled-components'; -import { breakLarge, breakSmall } from '../globalStyles'; import { state } from '../state'; import { formattedDay, isDateInRange } from '../utils'; -import Link from './Link'; interface DayLinkProps { day: Date; @@ -15,71 +13,90 @@ interface DayLinkProps { const Container = styled.nav` flex: 1; - white-space: nowrap; - overflow: auto; + display: flex; + align-items: center; + gap: 2px; + overflow-x: auto; + padding: 6px 0; &::-webkit-scrollbar { display: none; } `; -const StyledLink = styled(Link)<{ activeLink: boolean }>` - && { - border: none; - background: transparent; - display: inline-block; - text-transform: uppercase; - font-size: 0.75rem; - border-radius: 0.25em; - color: var(--gray3); - padding: 1.25em 2em 1.25em 0; - transition: color 0.2s; - font-weight: 500; - - &:first-child { - margin-left: 0; - } +// Wrapper so 'active' doesn't leak to the DOM element +function DayA(props: { + active: boolean; + href: string; + noScroll?: boolean; + class?: string; + children?: JSX.Element; +}) { + const [, rest] = splitProps(props, ['active']); + return ; +} - &:focus { - outline: none; - color: var(--accent_color); - } +const StyledLink = styled(DayA)<{ active: boolean }>` + && { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + padding: 0.35rem 0.6rem; + border-radius: var(--radius-md); + min-width: 2.5rem; + text-align: center; + background: ${props => (props.active ? 'var(--radio-track)' : 'transparent')}; + color: ${props => (props.active ? 'var(--gray1)' : 'var(--gray4)')}; + transition: background 0.15s, color 0.15s; &:hover { + background: var(--radio-track); color: var(--gray1); } - ${props => - props.activeLink - ? ` - color: var(--gray1); - font-weight: 600; - ` - : ''} - - @media (min-width: ${breakSmall}) { - font-size: 0.8rem; - } - - @media (max-width: ${breakLarge}) { - margin: 0; + &:focus { + outline: 2px solid var(--accent_color); + outline-offset: 2px; } } `; -const DayLink = (props: DayLinkProps) => { - const date = formattedDay(props.day, 'iiiiii d.M.'); - const search = () => - isSameDay(props.day, new Date()) - ? '' - : `?day=${format(props.day, 'y-MM-dd')}`; - const active = () => isSameDay(props.selectedDay, props.day); +const WeekDay = styled.span` + font-size: 0.6rem; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + line-height: 1; +`; +const DateNum = styled.span` + font-size: 1rem; + font-weight: 600; + line-height: 1.1; +`; + +const DayLink = (props: DayLinkProps) => { + const weekday = formattedDay(props.day, 'iiiiii'); + const dateNum = formattedDay(props.day, 'd'); const location = useLocation(); + const isActive = () => isSameDay(props.selectedDay, props.day); + + const href = createMemo(() => { + const params = new URLSearchParams(location.search); + if (isSameDay(props.day, new Date())) { + params.delete('day'); + } else { + params.set('day', format(props.day, 'y-MM-dd')); + } + const qs = params.toString(); + return location.pathname + (qs ? `?${qs}` : ''); + }); return ( - - {date()} + + {weekday()} + {dateNum()} ); }; diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 5a33ab5..c50a61f 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -39,7 +39,9 @@ const Container = styled.div<{ open: boolean }>` const Overlay = styled.div<{ open: boolean; darkMode: boolean }>` background: ${props => - props.darkMode ? 'rgba(50, 50, 50, 0.5)' : 'rgba(0, 0, 0, 0.55)'}; + props.darkMode ? 'rgba(50, 50, 50, 0.5)' : 'rgba(0, 0, 0, 0.4)'}; + backdrop-filter: blur(3px) saturate(1.1); + -webkit-backdrop-filter: blur(3px); position: absolute; width: 100%; height: 100%; @@ -59,20 +61,26 @@ const Content = styled.div<{ open: boolean }>` z-index: 6; position: relative; max-width: 40rem; - border-radius: 0.8rem; + border-radius: var(--radius-lg); overflow: auto; + background: var(--gray7); border: 1px var(--gray6) solid; - box-shadow: 0rem 0.1rem 0.6rem -0.2rem rgba(0, 0, 0, 0.3); + box-shadow: var(--shadow-md), 0 0 0 1px rgba(0,0,0,0.04); flex: 1; - transition: opacity 0.3s; + transition: opacity 0.2s ease-out, transform 0.2s ease-out; opacity: 0; + transform: translateY(12px); max-height: 90vh; @media (max-width: ${breakSmall}) { max-width: 100%; } - ${props => (props.open ? 'opacity: 1;' : '')} + ${props => + props.open + ? `opacity: 1; + transform: translateY(0);` + : ''} `; const CloseText = styled.div<{ open: boolean }>` @@ -80,10 +88,9 @@ const CloseText = styled.div<{ open: boolean }>` display: flex; align-items: center; justify-content: center; - text-transform: uppercase; - color: var(--gray4); - font-weight: 300; - font-size: 0.9em; + color: var(--gray3); + font-weight: 400; + font-size: 0.85rem; height: 5rem; transition: opacity 0.2s; opacity: 0; diff --git a/src/components/PageContainer.tsx b/src/components/PageContainer.tsx index 88eccfd..62cb814 100644 --- a/src/components/PageContainer.tsx +++ b/src/components/PageContainer.tsx @@ -11,7 +11,7 @@ interface Props { const Container = styled.div` background: var(--gray7); - padding: 0.5rem 1rem 1rem; + padding: 1.25rem 1.5rem 1.5rem; height: 100%; overflow: auto; box-sizing: border-box; @@ -26,10 +26,10 @@ const Container = styled.div` const Title = styled.h1<{ compact?: boolean }>` margin: 1em 0; - font-weight: 300; + font-weight: 500; letter-spacing: 0.02em; color: var(--gray1); - font-size: ${props => (props.compact ? '1.5em' : '1.75em')}; + font-size: ${props => (props.compact ? '1.4em' : '1.5em')}; &:first-child { margin-top: 0; @@ -42,7 +42,6 @@ const Title = styled.h1<{ compact?: boolean }>` &:first-child { margin-top: 0; margin-bottom: 0.5rem; - font-weight: normal; padding: 0.5rem 0 0.5rem 0; top: 0; z-index: 1000; diff --git a/src/components/Radio.tsx b/src/components/Radio.tsx index d66aad8..0a5412f 100644 --- a/src/components/Radio.tsx +++ b/src/components/Radio.tsx @@ -1,6 +1,5 @@ import { For, splitProps } from 'solid-js'; import { styled } from 'solid-styled-components'; -import { breakLarge, breakSmall } from '../globalStyles'; interface Props { options: { @@ -14,8 +13,13 @@ interface Props { } const Container = styled.div` - white-space: nowrap; + display: inline-flex; + background: var(--radio-track); + border-radius: var(--radius-full); + padding: 3px; + gap: 0; overflow-x: auto; + white-space: nowrap; &::-webkit-scrollbar { display: none; @@ -23,43 +27,23 @@ const Container = styled.div` `; export const Button = styled.button<{ selected: boolean }>` - color: var(--gray3); - background: transparent; + color: ${props => (props.selected ? 'var(--gray1)' : 'var(--gray3)')}; + background: ${props => (props.selected ? 'var(--radio-selected)' : 'transparent')}; + box-shadow: ${props => (props.selected ? 'var(--shadow-sm)' : 'none')}; font-family: inherit; font-weight: 500; - text-transform: uppercase; - transition: background 0.1s, color 0.1s; + font-size: 0.8rem; + padding: 0.45rem 1rem; + border-radius: var(--radius-full); + border: none; + transition: background 0.15s, color 0.15s, box-shadow 0.15s; margin: 0; - border-radius: 0.2em; - outline: none; - border: solid 2px transparent; - - @media (max-width: ${breakSmall}) { - font-size: 0.7rem; - padding: 0.5rem 0.4rem; - } - - @media (min-width: ${breakLarge}) { - font-size: 0.8rem; - padding: 0.5rem 1rem; - } + cursor: pointer; &:focus { - border-color: var(--gray4); + outline: 2px solid var(--accent_color); + outline-offset: -2px; } - - ${props => - props.selected - ? ` - background: var(--accent_color); - color: var(--gray6); - - &:focus { - filter: brightness(115%); - color: var(--gray6); - } - ` - : ''} `; export default function Radio(props: Props) { diff --git a/src/components/RoundedButton.tsx b/src/components/RoundedButton.tsx index aa9942e..4d79fe8 100644 --- a/src/components/RoundedButton.tsx +++ b/src/components/RoundedButton.tsx @@ -31,13 +31,13 @@ export const RoundedButton = styled.button<{ flex-grow: 1; background: transparent; border: 1px solid ${props => props.color}; - border-radius: 1rem; + border-radius: var(--radius-full); transition: background 0.1s, color 0.1s; - padding: 0.4rem 0.8rem 0.45rem; - outline: none; + padding: 0.35rem 0.75rem 0.4rem; &:focus { - background: var(--gray5); + outline: 2px solid currentColor; + outline-offset: 1px; } ${props => diff --git a/src/components/Settings/PriceCategorySelector.tsx b/src/components/Settings/PriceCategorySelector.tsx index 5fa86eb..8f1f32d 100644 --- a/src/components/Settings/PriceCategorySelector.tsx +++ b/src/components/Settings/PriceCategorySelector.tsx @@ -18,17 +18,19 @@ const categories = [ ]; const ButtonContainer = styled.div` - border: solid 2px var(--accent_color); - border-radius: 4px; - display: inline-block; - overflow: hidden; + background: var(--gray5); + border-radius: var(--radius-full); + padding: 3px; + display: inline-flex; `; const Item = styled(Button)` - border-radius: 0; - color: ${props => (props.selected ? 'var(--gray7)' : 'var(--accent_color)')}; + border-radius: var(--radius-full); + color: ${props => (props.selected ? 'var(--gray1)' : 'var(--gray3)')}; + background: ${props => (props.selected ? 'var(--gray7)' : 'transparent')}; + box-shadow: ${props => (props.selected ? 'var(--shadow-sm)' : 'none')}; min-width: 3rem; - padding-top: 0.6rem; + padding: 0.45rem 1rem; svg { font-size: 1rem; @@ -38,18 +40,9 @@ const Item = styled(Button)` } } - :first-child { - border-radius: 1em 0 0 1em; - } - - :last-child { - border-radius: 0 1em 1em 0; - border-right: none; - } - :focus { - background: var(--accent_color); - border-color: transparent; + outline: 2px solid var(--accent_color); + outline-offset: -2px; } `; diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx index 989ebfa..563a1d9 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -9,25 +9,32 @@ import Toggle from '../Toggle'; import PriceCategorySelector from './PriceCategorySelector'; import PropertySelector from './PropertySelector'; -interface ItemProps { - label: JSXElement; - children: JSXElement; -} +const SettingsCard = styled.div` + background: var(--gray6); + border-radius: var(--radius-lg); + padding: 0.25rem 0; + margin-bottom: 0.75rem; + overflow: hidden; +`; -const ItemTitle = styled.h2` - font-size: 0.8rem; - text-transform: uppercase; - font-weight: 500; - color: var(--gray3); - margin-top: 2em; +const SettingsRow = styled.div<{ column?: boolean }>` + display: flex; + align-items: ${props => (props.column ? 'flex-start' : 'center')}; + flex-direction: ${props => (props.column ? 'column' : 'row')}; + justify-content: space-between; + padding: 0.75rem 1rem; + gap: 1rem; + + & + & { + border-top: 1px solid var(--gray5); + } `; -const Item = (props: ItemProps) => ( - <> - {props.label} - {props.children} - -); +const RowLabel = styled.span` + font-size: 0.85rem; + color: var(--gray2); + font-weight: 500; +`; const orders = [Order.AUTOMATIC, Order.ALPHABET, Order.DISTANCE]; @@ -36,85 +43,110 @@ const languageOptions = [ { label: 'English', value: Lang.EN }, ]; +interface RowProps { + label: JSXElement; + children: JSXElement; + column?: boolean; +} + +const Row = (props: RowProps) => ( + + {props.label} + {props.children} + +); + const Settings = () => { return ( - - setState('preferences', 'lang', value)} - /> - - - setState('preferences', 'maxPriceCategory', value)} - /> - - - setState('preferences', 'darkMode', value)} - /> - - - ({ - label: computedState.translations()[order], - value: order, - }))} - selected={state.preferences.order} - onChange={value => setState('preferences', 'order', value)} - /> - - - setState('preferences', 'useLocation', value)} - /> - - - - - - - setState('preferences', 'highlightOperator', value) - } - /> - - - - - - - + + + setState('preferences', 'lang', value)} + /> + + + setState('preferences', 'darkMode', value)} + /> + + + + + ({ + label: computedState.translations()[order], + value: order, + }))} + selected={state.preferences.order} + onChange={value => setState('preferences', 'order', value)} + /> + + + setState('preferences', 'useLocation', value)} + /> + + + + + + setState('preferences', 'maxPriceCategory', value) + } + /> + + + + + + + + + setState('preferences', 'highlightOperator', value) + } + /> + + + + + + + + + + ); }; diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx index 74256af..ca0c0d8 100644 --- a/src/components/Toggle.tsx +++ b/src/components/Toggle.tsx @@ -5,58 +5,56 @@ interface Props { selected: boolean; } -const StyledToggle = styled.span<{ switchedOn: boolean }>` +const Track = styled.button` position: relative; - display: inline-block; - border: 2px solid var(--gray3); - background: var(--gray3); - border-radius: 20px; - padding: 10px 0; - min-width: 4em; - min-height: 1em; - transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); - outline: none; - - &:focus { - background: var(--gray1); + display: inline-flex; + align-items: center; + width: 44px; + height: 26px; + border-radius: 13px; + border: none; + padding: 0; + cursor: pointer; + flex-shrink: 0; + background: var(--gray4); + transition: background 0.2s ease; + + &[data-on] { + background: var(--accent_color); } - &:after { - cursor: pointer; - position: absolute; - background: var(--gray6); - box-sizing: border-box; - top: 50%; - border-radius: 30px; - margin: 0.1em; - margin-top: -25%; - width: 2em; - height: 2em; - transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); - content: ''; - ${props => (props.switchedOn ? 'margin-left: 1.9em;' : '')} + &:focus-visible { + outline: 2px solid var(--accent_color); + outline-offset: 2px; } +`; - ${props => - props.switchedOn - ? ` - background: var(--accent_color); - border-color: var(--accent_color); - - &:focus { - background: var(--accent_color); - filter: brightness(120%); - } - ` - : ''} +const Knob = styled.span` + position: absolute; + left: 3px; + width: 20px; + height: 20px; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + transition: transform 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); + pointer-events: none; `; const Toggle = (props: Props) => ( - props.onChange(!props.selected)} - /> + onKeyDown={(e: KeyboardEvent) => + e.key === 'Enter' && props.onChange(!props.selected) + } + > + + ); export default Toggle; diff --git a/src/components/TopBar/TopBar.tsx b/src/components/TopBar/TopBar.tsx index 01d584c..2658476 100644 --- a/src/components/TopBar/TopBar.tsx +++ b/src/components/TopBar/TopBar.tsx @@ -1,8 +1,8 @@ -import { createSignal, onCleanup, onMount } from 'solid-js'; +import { createMemo, createSignal, onCleanup, onMount } from 'solid-js'; import { styled } from 'solid-styled-components'; import { breakSmall } from '../../globalStyles'; -import { MapIcon, NewsIcon, SettingsIcon } from '../../icons'; -import { computedState, setState, state } from '../../state'; +import { CaretDownIcon, MapIcon, NewsIcon, SettingsIcon } from '../../icons'; +import { computedState, resources, setState, state } from '../../state'; import { Lang } from '../../types'; import AreaSelector from '../AreaSelector'; import ClickOutside from '../ClickOutside'; @@ -12,25 +12,24 @@ import Link from '../Link'; import EN from './en.svg'; import FI from './fi.svg'; -const Container = styled.header<{ darkMode: boolean }>` - background: linear-gradient(to bottom, var(--gray6) 0%, var(--gray7) 100%); +const Container = styled.header` + background: var(--topbar-bg); + backdrop-filter: blur(16px) saturate(1.8); + -webkit-backdrop-filter: blur(16px) saturate(1.8); + border-bottom: 1px solid var(--topbar-border); box-sizing: border-box; padding: 0 0.5em; position: fixed; width: 100%; z-index: 10; color: var(--gray3); - border-bottom: 1px solid var(--gray5); user-select: none; @media (max-width: ${breakSmall}) { - background-color: var(--gray7); justify-content: flex-start; padding: 0.2em; padding-left: 1rem; } - - ${props => (props.darkMode ? 'background: var(--gray7);' : '')} `; const Content = styled.div` @@ -56,20 +55,21 @@ const AreaSelectorButton = styled(ClickOutside)` const AreaSelectorContainer = styled.div<{ isOpen: boolean }>` position: absolute; right: 0; - top: 32px; - width: 20em; + top: 36px; + width: 22em; background: var(--gray7); padding: 0.4em; - box-shadow: 0rem 0.1rem 0.6rem -0.2rem rgba(0, 0, 0, 0.3); - border-radius: 4px; - border: solid 1px var(--gray5); + box-shadow: var(--shadow-popover); + border-radius: var(--radius-md); opacity: 0; - transition: opacity 75ms; + transform: translateY(-6px); + transition: opacity 150ms ease-out, transform 150ms ease-out; pointer-events: none; ${props => props.isOpen ? `opacity: 1; + transform: translateY(0); pointer-events: all; ` : ''} @@ -82,12 +82,20 @@ const AreaSelectorContainer = styled.div<{ isOpen: boolean }>` } `; -const iconLinkStyles = ` +const PopoverHeader = styled.div` text-transform: uppercase; + font-size: 0.65rem; + color: var(--gray4); + padding: 0.5rem 0.75rem 0.25rem; + letter-spacing: 0.06em; +`; + +const iconLinkStyles = ` font-weight: 500; font-size: 0.8rem; display: flex; align-items: center; + gap: 0.3em; padding: 0 1em; :last-child { @@ -121,6 +129,19 @@ const NativeIconLink = styled.a` ${iconLinkStyles} `; +const AreaTriggerLabel = styled.span` + @media (max-width: ${breakSmall}) { + display: none; + } +`; + +const CaretIcon = styled(CaretDownIcon)<{ open: boolean }>` + display: block !important; + font-size: 0.7rem; + transition: transform 0.15s ease-out; + ${props => (props.open ? 'transform: rotate(180deg);' : '')} +`; + const FlagImg = styled.img` cursor: pointer; height: 16px; @@ -196,8 +217,19 @@ export default function TopBar() { ); } + const [areas] = resources.areas; + + const currentAreaLabel = createMemo(() => { + const t = computedState.translations(); + const selected = state.preferences.selectedArea; + if (selected === -2) return t.nearby; + if (selected === -1) return t.starred; + const area = areas()?.find(a => a.id === selected); + return area?.name ?? t.selectArea; + }); + return ( - + {computedState.unseenUpdates().length > 0 && ( @@ -215,9 +247,13 @@ export default function TopBar() { onKeyDown={e => e.key === 'Enter' && toggleAreaSelector()} > - {computedState.translations().selectArea} + {currentAreaLabel()} + + + {computedState.translations().selectArea} + diff --git a/src/globalStyles.ts b/src/globalStyles.ts index 6bbad04..82aa1c3 100644 --- a/src/globalStyles.ts +++ b/src/globalStyles.ts @@ -34,12 +34,32 @@ export default createGlobalStyles` --priceCategory_studentPremium: #8b8f4f; --priceCategory_regular: #875555; + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 16px; + --radius-full: 9999px; + + --shadow-sm: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.05); + --shadow-md: 0 4px 16px rgba(0,0,0,0.12), 0 1px 4px rgba(0,0,0,0.06); + --shadow-popover: 0 8px 32px rgba(0,0,0,0.14), 0 0 0 1px rgba(0,0,0,0.06); + + --radio-track: var(--gray5); + --radio-selected: var(--gray7); + + --topbar-bg: rgba(254, 254, 254, 0.82); + --topbar-border: rgba(0, 0, 0, 0.07); + &.dark { + --topbar-bg: rgba(43, 49, 56, 0.85); + --topbar-border: rgba(255, 255, 255, 0.06); --accent_color: #0ba3cb; --gray7: #2B3138; --gray6: #202329; --gray5: #313131; + + --radio-track: #1c2128; + --radio-selected: #363d47; --gray4: #989898; --gray3: #adadad; --gray2: #b3b3b3; diff --git a/src/icons.ts b/src/icons.ts index a87177e..2f14236 100644 --- a/src/icons.ts +++ b/src/icons.ts @@ -19,13 +19,13 @@ export { FiCopy as CopyIcon, FiMoreVertical as MoreIcon, } from 'solid-icons/fi'; -export { - IoLocationSharp as LocationIcon, - IoNewspaperSharp as NewsIcon, - IoSettingsSharp as SettingsIcon, -} from 'solid-icons/io'; +export { IoLocationSharp as LocationIcon } from 'solid-icons/io'; export { RiFinanceMoneyEuroCircleFill as MoneyIcon } from 'solid-icons/ri'; -export { TbOutlineBike as BikeIcon } from 'solid-icons/tb'; +export { + TbOutlineBell as NewsIcon, + TbOutlineBike as BikeIcon, + TbOutlineSettings2 as SettingsIcon, +} from 'solid-icons/tb'; export { VsHeart as HeartIcon, VsHeartFilled as HeartFilledIcon, From 76f20e716fa338f823c1f8744e01e2ba9519af0e Mon Sep 17 00:00:00 2001 From: Jere Vaara Date: Sat, 21 Feb 2026 16:03:25 +0200 Subject: [PATCH 02/18] also fix price cat selector colors --- src/components/Footer.tsx | 1 + src/components/Settings/PriceCategorySelector.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index a6f4cb7..442a363 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -60,6 +60,7 @@ const StyledExternalLink = styled.a` const LogoImage = styled.img<{ darkMode: boolean }>` height: 48px; margin-right: 1rem; + border-radius: var(--radius-md); @media (max-width: ${breakSmall}) { display: none; diff --git a/src/components/Settings/PriceCategorySelector.tsx b/src/components/Settings/PriceCategorySelector.tsx index 8f1f32d..defdd0e 100644 --- a/src/components/Settings/PriceCategorySelector.tsx +++ b/src/components/Settings/PriceCategorySelector.tsx @@ -18,7 +18,7 @@ const categories = [ ]; const ButtonContainer = styled.div` - background: var(--gray5); + background: var(--radio-track); border-radius: var(--radius-full); padding: 3px; display: inline-flex; @@ -27,7 +27,7 @@ const ButtonContainer = styled.div` const Item = styled(Button)` border-radius: var(--radius-full); color: ${props => (props.selected ? 'var(--gray1)' : 'var(--gray3)')}; - background: ${props => (props.selected ? 'var(--gray7)' : 'transparent')}; + background: ${props => (props.selected ? 'var(--radio-selected)' : 'transparent')}; box-shadow: ${props => (props.selected ? 'var(--shadow-sm)' : 'none')}; min-width: 3rem; padding: 0.45rem 1rem; From 3961c04a2b09efc1463bd6a4932d9395058d1701 Mon Sep 17 00:00:00 2001 From: Jere Vaara Date: Sat, 21 Feb 2026 16:15:04 +0200 Subject: [PATCH 03/18] dark theme update --- src/components/CourseList/Course.tsx | 2 +- src/globalStyles.ts | 29 ++++++++++++++-------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/components/CourseList/Course.tsx b/src/components/CourseList/Course.tsx index 778d1e2..45749ec 100644 --- a/src/components/CourseList/Course.tsx +++ b/src/components/CourseList/Course.tsx @@ -46,7 +46,7 @@ const CourseWrapper = styled.li<{ font-size: 0.9rem; &:not(:last-child) { - border-bottom: 1px solid var(--gray6); + border-bottom: 1px solid var(--gray5); } ${props => (props.favorite ? 'color: var(--hearty);' : '')} diff --git a/src/globalStyles.ts b/src/globalStyles.ts index 82aa1c3..74a6c94 100644 --- a/src/globalStyles.ts +++ b/src/globalStyles.ts @@ -50,20 +50,21 @@ export default createGlobalStyles` --topbar-border: rgba(0, 0, 0, 0.07); &.dark { - --topbar-bg: rgba(43, 49, 56, 0.85); - --topbar-border: rgba(255, 255, 255, 0.06); - --accent_color: #0ba3cb; - - --gray7: #2B3138; - --gray6: #202329; - --gray5: #313131; - - --radio-track: #1c2128; - --radio-selected: #363d47; - --gray4: #989898; - --gray3: #adadad; - --gray2: #b3b3b3; - --gray1: #c3c3c3; + --topbar-bg: rgba(22, 28, 30, 0.88); + --topbar-border: rgba(255, 255, 255, 0.07); + --accent_color: #0898be; + + --gray7: #1e2629; + --gray6: #161c1e; + --gray5: #253032; + + --radio-track: #0c1416; + --radio-selected: #1c2b2e; + + --gray4: #505858; + --gray3: #707c7c; + --gray2: #9aa4a4; + --gray1: #c4c8c8; --hearty: #fe346e; --friendly: #06CBB0; From e75222a5103a1327ea80cdc23a9011fb2c0bf952 Mon Sep 17 00:00:00 2001 From: Jere Vaara Date: Sat, 21 Feb 2026 16:44:47 +0200 Subject: [PATCH 04/18] make report/contact two-step to highlight tos --- src/components/Contact.tsx | 14 +++++++-- src/components/ReportModal/ReportModal.tsx | 9 ++++++ src/components/Restaurant/Restaurant.tsx | 35 ++++++++++++++++++++-- src/translations.tsx | 16 ++++++++-- 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/src/components/Contact.tsx b/src/components/Contact.tsx index 2507ab9..17c866c 100644 --- a/src/components/Contact.tsx +++ b/src/components/Contact.tsx @@ -1,3 +1,4 @@ +import { createSignal } from 'solid-js'; import { computedState } from '../state'; import { useFeedback } from '../utils'; import Button from './Button'; @@ -6,6 +7,7 @@ import PageContainer from './PageContainer'; const Contact = () => { const [feedback, send] = useFeedback(); + const [acknowledged, setAcknowledged] = createSignal(false); const onSubmit = (e: any) => { e.preventDefault(); @@ -17,6 +19,15 @@ const Contact = () => { {feedback.sent ? ( computedState.translations().thanksForFeedback + ) : !acknowledged() ? ( + <> +

+ {computedState.translations().tosShort} +

+ + ) : (
{ label={computedState.translations().message} rows={10} /> -

- {computedState.translations().tosShort} -

+ {form => ( <> diff --git a/src/components/Restaurant/Restaurant.tsx b/src/components/Restaurant/Restaurant.tsx index 1c6319c..b8ee96a 100644 --- a/src/components/Restaurant/Restaurant.tsx +++ b/src/components/Restaurant/Restaurant.tsx @@ -203,6 +203,34 @@ const StyledActionLink = styled(Link)` } `; +const EditLink = styled(Link)` + && { + display: inline-flex; + align-items: center; + gap: 0.3em; + border: 1px solid var(--gray5); + border-radius: var(--radius-full); + padding: 0.22em 0.55em 0.22em 0.35em; + color: var(--gray3); + font-size: 0.72rem; + font-weight: 500; + text-transform: none; + + &:hover, + &:focus { + outline: none; + color: var(--accent_color); + border-color: var(--accent_color); + } + } +`; + +const EditLabel = styled.span` + @media (max-width: ${breakSmall}) { + display: none; + } +`; + const StyledNativeActionLink = styled.a<{ color: string }>` ${actionLinkStyles} color: ${props => props.color} !important; @@ -305,12 +333,13 @@ const Restaurant = (props: Props) => { - - - + + Propose edit + Date: Sat, 21 Feb 2026 16:44:58 +0200 Subject: [PATCH 05/18] translate fix info btn text --- src/components/Restaurant/Restaurant.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Restaurant/Restaurant.tsx b/src/components/Restaurant/Restaurant.tsx index b8ee96a..64170ae 100644 --- a/src/components/Restaurant/Restaurant.tsx +++ b/src/components/Restaurant/Restaurant.tsx @@ -338,7 +338,7 @@ const Restaurant = (props: Props) => { to={`/report/${props.restaurant.id}`} > - Propose edit + {computedState.translations().fixInfo} Date: Sat, 21 Feb 2026 16:45:12 +0200 Subject: [PATCH 06/18] update footer style and remove out-of-use stuff --- src/components/Footer.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 442a363..160204b 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -19,7 +19,6 @@ const Footer = styled.footer` const linkStyles = ` color: var(--gray2); - text-transform: uppercase; margin: 0 0.5rem; text-decoration: none; vertical-align: middle; @@ -100,13 +99,7 @@ export default () => { {computedState.translations().contact} - - {computedState.translations().otherClients} - - - {computedState.translations().updates} - - + {computedState.translations().termsOfService} {showInfo && ( From 0f8d55d1cafeca69c266d7b4426582d036773b2a Mon Sep 17 00:00:00 2001 From: Jere Vaara Date: Sat, 21 Feb 2026 18:17:48 +0200 Subject: [PATCH 07/18] mobile style changes --- src/components/Restaurant/Restaurant.tsx | 6 +----- src/icons.ts | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/Restaurant/Restaurant.tsx b/src/components/Restaurant/Restaurant.tsx index 64170ae..cb26012 100644 --- a/src/components/Restaurant/Restaurant.tsx +++ b/src/components/Restaurant/Restaurant.tsx @@ -225,11 +225,7 @@ const EditLink = styled(Link)` } `; -const EditLabel = styled.span` - @media (max-width: ${breakSmall}) { - display: none; - } -`; +const EditLabel = styled.span``; const StyledNativeActionLink = styled.a<{ color: string }>` ${actionLinkStyles} diff --git a/src/icons.ts b/src/icons.ts index 2f14236..b9fdf77 100644 --- a/src/icons.ts +++ b/src/icons.ts @@ -11,7 +11,6 @@ export { } from 'solid-icons/bi'; export { FaSolidAngleDown as CaretDownIcon, - FaSolidMap as MapIcon, FaSolidPersonWalking as WalkIcon, } from 'solid-icons/fa'; export { @@ -22,9 +21,10 @@ export { export { IoLocationSharp as LocationIcon } from 'solid-icons/io'; export { RiFinanceMoneyEuroCircleFill as MoneyIcon } from 'solid-icons/ri'; export { + TbOutlineAdjustmentsHorizontal as SettingsIcon, TbOutlineBell as NewsIcon, TbOutlineBike as BikeIcon, - TbOutlineSettings2 as SettingsIcon, + TbOutlineMapPin as MapIcon, } from 'solid-icons/tb'; export { VsHeart as HeartIcon, From 0c052cacaddf7129dd4e831b1abfb4a78940bfa6 Mon Sep 17 00:00:00 2001 From: Jere Vaara Date: Sat, 21 Feb 2026 18:25:06 +0200 Subject: [PATCH 08/18] responsiveness fixes --- src/components/PageContainer.tsx | 4 ++++ src/components/Settings/Settings.tsx | 20 ++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/components/PageContainer.tsx b/src/components/PageContainer.tsx index 62cb814..6e470d2 100644 --- a/src/components/PageContainer.tsx +++ b/src/components/PageContainer.tsx @@ -12,6 +12,10 @@ interface Props { const Container = styled.div` background: var(--gray7); padding: 1.25rem 1.5rem 1.5rem; + + @media (max-width: ${breakSmall}) { + padding: 1rem 0.75rem 1.25rem; + } height: 100%; overflow: auto; box-sizing: border-box; diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx index 563a1d9..684141c 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -1,5 +1,6 @@ import type { JSXElement } from 'solid-js'; import { styled } from 'solid-styled-components'; +import { breakSmall } from '../../globalStyles'; import { computedState, setState, state } from '../../state'; import { DarkModeChoice, HighlighOperator, Lang, Order } from '../../types'; import FavoriteSelector from '../FavoriteSelector'; @@ -17,7 +18,7 @@ const SettingsCard = styled.div` overflow: hidden; `; -const SettingsRow = styled.div<{ column?: boolean }>` +const SettingsRow = styled.div<{ column?: boolean; mobileColumn?: boolean }>` display: flex; align-items: ${props => (props.column ? 'flex-start' : 'center')}; flex-direction: ${props => (props.column ? 'column' : 'row')}; @@ -28,6 +29,13 @@ const SettingsRow = styled.div<{ column?: boolean }>` & + & { border-top: 1px solid var(--gray5); } + + @media (max-width: ${breakSmall}) { + ${props => + props.mobileColumn + ? 'flex-direction: column; align-items: flex-start;' + : ''} + } `; const RowLabel = styled.span` @@ -47,10 +55,11 @@ interface RowProps { label: JSXElement; children: JSXElement; column?: boolean; + mobileColumn?: boolean; } const Row = (props: RowProps) => ( - + {props.label} {props.children} @@ -89,7 +98,7 @@ const Settings = () => { - + ({ label: computedState.translations()[order], @@ -120,7 +129,10 @@ const Settings = () => { - + Date: Sat, 21 Feb 2026 18:34:30 +0200 Subject: [PATCH 09/18] improve area selector ux on mobile --- src/components/TopBar/TopBar.tsx | 128 +++++++++++++++++++++---------- 1 file changed, 86 insertions(+), 42 deletions(-) diff --git a/src/components/TopBar/TopBar.tsx b/src/components/TopBar/TopBar.tsx index 2658476..51addfc 100644 --- a/src/components/TopBar/TopBar.tsx +++ b/src/components/TopBar/TopBar.tsx @@ -76,12 +76,49 @@ const AreaSelectorContainer = styled.div<{ isOpen: boolean }>` @media (max-width: ${breakSmall}) { top: 52px; + left: 0.5rem; + right: 0.5rem; + width: auto; + border: 1px solid var(--gray6); + } +`; + +const MobileOverlay = styled.div<{ open: boolean }>` + display: none; + + @media (max-width: ${breakSmall}) { + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + padding-bottom: 2.5rem; + position: fixed; + top: 0; left: 0; - width: 100%; - box-sizing: border-box; + right: 0; + bottom: 0; + z-index: 9; + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + pointer-events: none; + opacity: 0; + transition: opacity 0.15s ease-out; + + ${props => (props.open ? 'opacity: 1; pointer-events: auto;' : '')} } `; +const MobileOverlayClose = styled.div` + color: var(--gray2); + font-size: 0.85rem; + font-weight: 500; + padding: 0.6rem 1.5rem; + border-radius: var(--radius-full); + background: var(--gray7); + box-shadow: var(--shadow-sm); + border: 1px solid var(--gray5); +`; + const PopoverHeader = styled.div` text-transform: uppercase; font-size: 0.65rem; @@ -229,49 +266,56 @@ export default function TopBar() { }); return ( - - - - {computedState.unseenUpdates().length > 0 && ( - - - - - - )} - + <> + + + {computedState.translations().closeModal} + + + + + + {computedState.unseenUpdates().length > 0 && ( + + + + + + )} + + e.key === 'Enter' && toggleAreaSelector()} + > + + {currentAreaLabel()} + + + + + {computedState.translations().selectArea} + + + + + + + {computedState.translations().settings} + e.key === 'Enter' && toggleAreaSelector()} + onClick={toggleLang} + onKeyDown={e => e.key === 'Enter' && toggleLang()} > - - {currentAreaLabel()} - + - - - {computedState.translations().selectArea} - - - - - - - {computedState.translations().settings} - - e.key === 'Enter' && toggleLang()} - > - - - - + + + ); } From c0ab67a2de5cc37ce25c60da97456190cbf9205d Mon Sep 17 00:00:00 2001 From: Jere Vaara Date: Sat, 21 Feb 2026 19:39:30 +0200 Subject: [PATCH 10/18] semantic color names --- src/components/AreaSelector.tsx | 8 +-- src/components/Button.tsx | 6 +-- src/components/ChangeLog.tsx | 6 +-- src/components/Contact.tsx | 2 +- src/components/CourseList/Course.tsx | 8 +-- src/components/CourseList/CourseList.tsx | 2 +- src/components/CourseList/Property.tsx | 2 +- src/components/DaySelector.tsx | 4 +- src/components/Footer.tsx | 8 +-- src/components/Input.tsx | 10 ++-- src/components/MenuViewer.tsx | 2 +- src/components/Modal.tsx | 8 +-- src/components/Notice.tsx | 2 +- src/components/PageContainer.tsx | 8 +-- src/components/PriceCategoryBadge.tsx | 2 +- src/components/Radio.tsx | 2 +- src/components/ReportModal/ReportModal.tsx | 6 +-- src/components/Restaurant/PlaceHolder.tsx | 8 +-- src/components/Restaurant/Restaurant.tsx | 24 ++++----- src/components/RestaurantList.tsx | 10 ++-- src/components/RestaurantModal/Map.tsx | 4 +- .../RestaurantModal/RestaurantModal.tsx | 4 +- src/components/RoundedButton.tsx | 2 +- .../Settings/PriceCategorySelector.tsx | 2 +- src/components/Settings/PropertySelector.tsx | 2 +- src/components/Settings/Settings.tsx | 6 +-- src/components/Toggle.tsx | 2 +- src/components/Tooltip.tsx | 4 +- src/components/TopBar/TopBar.tsx | 14 ++--- src/globalStyles.ts | 52 +++++++++++++++---- 30 files changed, 127 insertions(+), 93 deletions(-) diff --git a/src/components/AreaSelector.tsx b/src/components/AreaSelector.tsx index 0ee4d40..b19211e 100644 --- a/src/components/AreaSelector.tsx +++ b/src/components/AreaSelector.tsx @@ -47,14 +47,14 @@ const AreaButton = styled.button<{ selected: boolean }>` font-size: 0.875rem; font-family: inherit; font-weight: ${props => (props.selected ? '500' : 'inherit')}; - background: ${props => (props.selected ? 'var(--gray5)' : 'transparent')}; - color: ${props => (props.selected ? 'var(--accent_color)' : 'var(--gray1)')}; + background: ${props => (props.selected ? 'var(--bg-interactive)' : 'transparent')}; + color: ${props => (props.selected ? 'var(--accent_color)' : 'var(--text-primary)')}; border: none; cursor: pointer; transition: background 0.1s, color 0.1s; &:hover { - background: var(--gray5); + background: var(--bg-interactive); } &:focus { @@ -71,7 +71,7 @@ const Checkmark = styled.span` const Divider = styled.hr` border: none; - border-top: 1px solid var(--gray5); + border-top: 1px solid var(--border-subtle); margin: 4px 8px; `; diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 5735735..d6a7a80 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -16,10 +16,10 @@ const Button = styled.button` min-width: 4rem; background: ${props => props.color === 'secondary' || props.secondary - ? 'var(--gray3)' + ? 'var(--text-muted)' : 'var(--accent_color)'}; text-align: center; - color: var(--gray6); + color: white; font-weight: 600; letter-spacing: 0.01em; transition: transform 0.1s, box-shadow 0.1s; @@ -37,7 +37,7 @@ const Button = styled.button` } &:focus { - color: var(--gray6); + color: white; outline: 2px solid var(--accent_color); outline-offset: 2px; } diff --git a/src/components/ChangeLog.tsx b/src/components/ChangeLog.tsx index 833ed2a..fb7103a 100644 --- a/src/components/ChangeLog.tsx +++ b/src/components/ChangeLog.tsx @@ -14,13 +14,13 @@ import PageContainer from './PageContainer'; const UpdateWrapper = styled.div` margin-bottom: 0.5em; display: flex; - color: var(--gray2); + color: var(--text-secondary); padding: 0.5em; border-radius: 0.2em; cursor: pointer; &:hover { - background: var(--gray5); + background: var(--bg-interactive); } &:last-child { @@ -47,7 +47,7 @@ const PublishedAt = styled.p` text-transform: uppercase; font-weight: 500; margin: 0 0 0; - color: var(--gray2) !important; + color: var(--text-secondary) !important; `; const ArrowDownIcon = styled(CaretDownIcon)<{ isVisible: boolean }>` diff --git a/src/components/Contact.tsx b/src/components/Contact.tsx index 17c866c..d10ca57 100644 --- a/src/components/Contact.tsx +++ b/src/components/Contact.tsx @@ -21,7 +21,7 @@ const Contact = () => { computedState.translations().thanksForFeedback ) : !acknowledged() ? ( <> -

+

{computedState.translations().tosShort}