d14f86478a
New token system in global.css (near-white surfaces, one ink-blue accent, no sub-12px type), self-hosted Schibsted Grotesk, rewritten primitives (buttons, fields, modal shell, segmented, disclosure, skeleton), a borderless 56px header with an SV | EN language toggle, an Icon component replacing glyphs, page skeletons that keep the layout during loading, and leave-type colours moved out of TS into CSS variables. Feature blocks keep their structure for later phases. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
83 lines
2.2 KiB
TypeScript
83 lines
2.2 KiB
TypeScript
import { ReactNode } from 'react'
|
|
|
|
/**
|
|
* The app's icon set — a handful of 24-unit line glyphs, inlined so there is
|
|
* no icon font or sprite request. Every icon is a 1.5 stroke in
|
|
* `currentColor`, so it takes the colour of the text next to it, and is
|
|
* `aria-hidden`: the control that contains it carries the accessible name.
|
|
*/
|
|
export type IconName =
|
|
| 'chevron-left'
|
|
| 'chevron-right'
|
|
| 'close'
|
|
| 'pin'
|
|
| 'check'
|
|
| 'briefcase'
|
|
| 'ban'
|
|
| 'help'
|
|
| 'clock'
|
|
| 'plus'
|
|
| 'external'
|
|
| 'calendar'
|
|
|
|
const CIRCLE = 'M12 3a9 9 0 1 0 0 18 9 9 0 1 0 0-18z'
|
|
|
|
const PATHS: Record<IconName, readonly string[]> = {
|
|
'chevron-left': ['M15 18l-6-6 6-6'],
|
|
'chevron-right': ['M9 18l6-6-6-6'],
|
|
close: ['M18 6L6 18', 'M6 6l12 12'],
|
|
pin: ['M12 17v5', 'M9 3h6l-1 7 3 3H7l3-3-1-7z'],
|
|
check: ['M5 12l5 5L20 7'],
|
|
briefcase: [
|
|
'M5 7h14a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2z',
|
|
'M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2',
|
|
'M3 12h18',
|
|
],
|
|
ban: [CIRCLE, 'M5.6 5.6l12.8 12.8'],
|
|
help: [CIRCLE, 'M9.5 9.5a2.5 2.5 0 1 1 3.5 2.3c-.7.4-1 1-1 1.7', 'M12 17h.01'],
|
|
clock: [CIRCLE, 'M12 7v5l3 2'],
|
|
plus: ['M12 5v14', 'M5 12h14'],
|
|
external: ['M14 4h6v6', 'M20 4l-9 9', 'M18 13v5a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h5'],
|
|
calendar: [
|
|
'M5 5h14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z',
|
|
'M3 10h18',
|
|
'M8 3v4',
|
|
'M16 3v4',
|
|
],
|
|
}
|
|
|
|
/** Every icon name, in one place, so a test can render each of them. */
|
|
export const ICON_NAMES = Object.keys(PATHS) as IconName[]
|
|
|
|
export type IconSize = 16 | 20 | 24
|
|
|
|
export interface IconProps {
|
|
name: IconName
|
|
/** Rendered size in px — matches --icon-sm / --icon-md / --icon-lg. */
|
|
size?: IconSize | undefined
|
|
className?: string | undefined
|
|
}
|
|
|
|
export function Icon({ name, size = 16, className }: IconProps): ReactNode {
|
|
return (
|
|
<svg
|
|
className={className ? `icon ${className}` : 'icon'}
|
|
width={size}
|
|
height={size}
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth={1.5}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
aria-hidden="true"
|
|
focusable="false"
|
|
data-icon={name}
|
|
>
|
|
{PATHS[name].map((d) => (
|
|
<path key={d} d={d} />
|
|
))}
|
|
</svg>
|
|
)
|
|
}
|