From d5139d07d2f78fe1924d1c30c8d503d7399d69ca Mon Sep 17 00:00:00 2001 From: Annisa Alexis Muljono Date: Fri, 16 Oct 2026 18:20:58 +0200 Subject: [PATCH] feat(#50): implement reusable responsive header and menu button --- frontend/src/components/header/Header.css | 219 +++++++++++++++--- frontend/src/components/header/Header.tsx | 113 +++++++++ frontend/src/components/logo/Logo.css | 10 + .../src/components/menuButton/MenuButton.css | 70 ++++++ .../src/components/menuButton/MenuButton.tsx | 41 ++++ 5 files changed, 423 insertions(+), 30 deletions(-) create mode 100644 frontend/src/components/header/Header.tsx create mode 100644 frontend/src/components/menuButton/MenuButton.css create mode 100644 frontend/src/components/menuButton/MenuButton.tsx diff --git a/frontend/src/components/header/Header.css b/frontend/src/components/header/Header.css index ebb1e61..dcfac5a 100644 --- a/frontend/src/components/header/Header.css +++ b/frontend/src/components/header/Header.css @@ -1,45 +1,204 @@ -/* Header – fra Figma (Components → Header) - Inneholder Logo, enten SearchField eller navigasjon, og en Button (variant dark, size s). */ + +/* ========================================= + HEADER – ISSUE #50 + Felles header for Handlelappen + ========================================= */ + +/* 1. Grunnstruktur */ + .header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 24px; - height: 64px; - padding: 0 40px; - background: var(--bg-page); - border-bottom: 1px solid var(--border-hairline); + display: grid; + grid-template-columns: + minmax(180px, 1fr) + minmax(0, 1.5fr) + minmax(180px, 1fr); + + align-items: center; + gap: 24px; + + width: 100%; + max-width: none; + min-height: 64px; + padding: 12px 40px; + box-sizing: border-box; + + background: var(--bg-page); + border-bottom: 1px solid var(--border-hairline); + color: var(--text-primary); } -.header .search-field { - flex: 1; +.header__logo { + display: flex; + align-items: center; + min-width: 0; } +/* 2. Navigasjon – hjemmeside */ + .header__nav { - display: flex; - gap: 32px; + display: flex; + justify-content: center; + align-items: center; + gap: 32px; + min-width: 0; } .header__link { - font: 500 11px var(--font-mono); - letter-spacing: 0.06em; - text-transform: uppercase; - color: var(--text-secondary); - text-decoration: none; + font: 500 11px var(--font-mono); + letter-spacing: 0.06em; + text-transform: uppercase; + + color: var(--text-secondary); + text-decoration: none; + white-space: nowrap; } .header__link:hover { - color: var(--text-accent); + color: var(--text-accent); +} + +.header__link:focus-visible { + outline: 2px solid var(--text-accent); + outline-offset: 5px; + border-radius: 2px; +} + +/* 3. Søkefelt – øvrige sider */ + +.header__search { + display: flex; + align-items: center; + gap: 12px; + + width: 100%; + max-width: 610px; + height: 40px; + padding: 0 14px; + box-sizing: border-box; + + justify-self: center; + + background: var(--bg-surface); + border: 1px solid transparent; + border-radius: 8px; +} + +.header__search:focus-within { + border-color: var(--text-accent); +} + +.header__search-icon { + display: flex; + width: 18px; + height: 18px; + flex-shrink: 0; + color: var(--text-muted); +} + +.header__search-icon svg { + width: 100%; + height: 100%; +} + +.header__search-input { + flex: 1; + min-width: 0; + width: 100%; + border: none; + outline: none; + background: transparent; + + color: var(--text-primary); + font: 400 13px var(--font-ui); } -/* Mobil: mindre luft på sidene */ -@media (max-width: 600px) { - .header { - gap: 12px; - padding: 0 16px; - } +.header__search-input::placeholder { + color: var(--text-muted); +} + +.header__search-input:disabled { + cursor: not-allowed; +} + +/* 4. Handlinger */ - .header__nav { - display: none; - } -} \ No newline at end of file +.header__actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 12px; + min-width: 0; +} + +.header__list-button { + white-space: nowrap; + justify-content: center; +} + +.header__list-button:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +/* 5. Nettbrett */ + +@media (max-width: 1000px) { + .header { + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 16px; + padding-left: 24px; + padding-right: 24px; + } + + .header__nav { + gap: 16px; + } +} + +/* 6. Mobil */ + +@media (max-width: 700px) { + .header { + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + padding: 10px 16px; + } + + .header__nav, + .header__search { + display: none; + } + + .header__actions { + gap: 8px; + } + + .header__list-button { + padding-left: 10px; + padding-right: 10px; + } +} + +/* 7. Ekstra små skjermer */ + +@media (max-width: 360px) { + .header__logo .logo { + gap: 6px; + font-size: 15px; + } + + .header__logo .logo__icon { + width: 24px; + height: 24px; + } + + .header__actions { + gap: 6px; + } + + .header__list-button { + font-size: 11px; + padding-left: 8px; + padding-right: 8px; + } +} diff --git a/frontend/src/components/header/Header.tsx b/frontend/src/components/header/Header.tsx new file mode 100644 index 0000000..94b7bd4 --- /dev/null +++ b/frontend/src/components/header/Header.tsx @@ -0,0 +1,113 @@ + +import { useState } from "react"; +import type { FormEvent } from "react"; + +import Logo from "../logo/Logo"; +import MenuButton from "../menuButton/MenuButton"; + +import "../button/Button.css"; +import "./Header.css"; + +type HeaderProps = { + variant?: "home" | "search"; + searchValue?: string; + onSearch?: (value: string) => void; + onOpenList?: () => void; + onOpenMenu?: () => void; + isMenuOpen?: boolean; + listCount?: number; +}; + +export default function Header({ + variant = "home", + searchValue = "", + onSearch, + onOpenList, + onOpenMenu, + isMenuOpen = false, + listCount = 0, +}: HeaderProps) { + const [searchInput, setSearchInput] = useState(searchValue); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + + const value = searchInput.trim(); + + if (value && onSearch) { + onSearch(value); + } + }; + + return ( +
+
+ +
+ + {variant === "home" ? ( + + ) : ( +
+ + + + setSearchInput(event.target.value) + } + disabled={!onSearch} + /> +
+ )} + +
+ + + +
+
+ ); +} diff --git a/frontend/src/components/logo/Logo.css b/frontend/src/components/logo/Logo.css index 593fbe6..ed2c624 100644 --- a/frontend/src/components/logo/Logo.css +++ b/frontend/src/components/logo/Logo.css @@ -84,3 +84,13 @@ transition: none; } } + +/* ========================================= + UNIVERSAL UTFORMING – FOKUSMARKERING + ========================================= */ + +.logo:focus-visible { + outline: 2px solid var(--text-accent); + outline-offset: 4px; + border-radius: 4px; +} \ No newline at end of file diff --git a/frontend/src/components/menuButton/MenuButton.css b/frontend/src/components/menuButton/MenuButton.css new file mode 100644 index 0000000..97222e2 --- /dev/null +++ b/frontend/src/components/menuButton/MenuButton.css @@ -0,0 +1,70 @@ + +/* ========================================= + MENU BUTTON – ISSUE #50 + Gjenbrukbar menyknapp + ========================================= */ + +.menu-button { + display: inline-flex; + align-items: center; + justify-content: center; + + width: 40px; + height: 40px; + padding: 0; + + flex-shrink: 0; + + background: var(--bg-surface); + border: 1px solid var(--border-hairline); + border-radius: 8px; + + color: var(--text-primary); + cursor: pointer; + + transition: + background-color 0.2s ease, + border-color 0.2s ease; +} + +.menu-button:hover:not(:disabled) { + background: var(--bg-tint); + border-color: var(--text-accent); +} + +.menu-button:focus-visible { + outline: 2px solid var(--text-accent); + outline-offset: 3px; +} + +.menu-button:disabled { + cursor: not-allowed; + opacity: 0.65; +} + +.menu-button__icon { + display: flex; + flex-direction: column; + justify-content: center; + gap: 4px; + + width: 18px; + height: 18px; +} + +.menu-button__icon span { + display: block; + width: 100%; + height: 2px; + + background: currentColor; + border-radius: 2px; +} + +/* Mobil */ +@media (max-width: 600px) { + .menu-button { + width: 38px; + height: 38px; + } +} diff --git a/frontend/src/components/menuButton/MenuButton.tsx b/frontend/src/components/menuButton/MenuButton.tsx new file mode 100644 index 0000000..ab07e55 --- /dev/null +++ b/frontend/src/components/menuButton/MenuButton.tsx @@ -0,0 +1,41 @@ + +import "./MenuButton.css"; + +type MenuButtonProps = { + onClick?: () => void; + isOpen?: boolean; + disabled?: boolean; +}; + +export default function MenuButton({ + onClick, + isOpen = false, + disabled = false, +}: MenuButtonProps) { + const isDisabled = disabled || !onClick; + + return ( + + ); +} +