freezmo/components/icons.tsx
Martin Dvorak dfa7b7502a Add full-screen API-key gate
In user-key mode a browser with no stored Ollama key now gets a polished
gate instead of the app shell: one masked input with a show/hide toggle,
a single Continue button, inline validation errors, and a "?" help sheet
with the steps to a free key.

- Validates the draft key through /api/health (real 1-token probe) before
  saving; invalid-key and network failures read differently.
- Key still lives only in this browser's localStorage; nothing server-side.
- Gate wraps /, /create and deck study; server-key and local-daemon modes
  skip it; removing the key in Settings brings it back.
- Removes the now-redundant dashboard/create API-key banner.
- Monochrome corner glows (dark near-black / light near-white), 24px card,
  48px field and button, gentle fade/slide-in, reduced-motion aware.
- Help modal: centred dialog on desktop, bottom sheet on mobile, closes via
  X, Escape or backdrop.
- README and browser suites updated (34 gate checks, 15 key-flow checks,
  12 shipped-default checks; ui/folders/docs/pwa/security/contrast green).
2026-10-06 21:20:22 +00:00

277 lines
7.5 KiB
TypeScript

"use client";
/**
* Small inline SVG icons used for the app chrome so that critical controls
* don't depend on a system emoji font being installed.
*/
type IconProps = { className?: string; strokeWidth?: number };
const base = "size-5";
function Svg({
children,
className,
filled,
}: {
children: React.ReactNode;
className?: string;
filled?: boolean;
}) {
return (
<svg
viewBox="0 0 24 24"
className={className ?? base}
fill={filled ? "currentColor" : "none"}
stroke={filled ? "none" : "currentColor"}
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
{children}
</svg>
);
}
export function IceCubeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 3.2 19 7v10l-7 3.8L5 17V7l7-3.8Z" />
<path d="M5 7l7 3.8L19 7M12 10.8V21" />
</Svg>
);
}
export function SnowflakeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 2v20M4.5 6.5l15 11M19.5 6.5l-15 11" />
<path d="M12 6l-2.2-2.2M12 6l2.2-2.2M12 18l-2.2 2.2M12 18l2.2 2.2M7 9.2 4.8 8.6M7 14.8l-2.2.6M17 9.2l2.2-.6M17 14.8l2.2.6" />
</Svg>
);
}
export function GearIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="3.2" />
<path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1.03 1.56V21a2 2 0 1 1-4 0v-.11A1.7 1.7 0 0 0 8.9 19.3a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.7 15a1.7 1.7 0 0 0-1.56-1.03H3a2 2 0 1 1 0-4h.11A1.7 1.7 0 0 0 4.7 8.9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.7a1.7 1.7 0 0 0 1.03-1.56V3a2 2 0 1 1 4 0v.11A1.7 1.7 0 0 0 15.1 4.7a1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9v.03A1.7 1.7 0 0 0 21 10.06H21a2 2 0 1 1 0 4h-.11A1.7 1.7 0 0 0 19.4 15Z" />
</Svg>
);
}
export function SunIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
</Svg>
);
}
export function MoonIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M21 12.8A8.5 8.5 0 1 1 11.2 3a6.6 6.6 0 0 0 9.8 9.8Z" />
</Svg>
);
}
export function BoltIcon({ className }: IconProps) {
return (
<Svg className={className} filled>
<path d="M13.5 2 4 13.2h5.4L9.5 22 19 10.8h-5.4L13.5 2Z" />
</Svg>
);
}
export function FlameIcon({ className }: IconProps) {
return (
<Svg className={className} filled>
<path d="M12 2s4.5 4.2 4.5 8.2c0 1.2-.5 2.2-1.2 2.9.1-1.6-.5-3.3-1.8-4.6.3 2.6-1 4-2.2 5.1-1 .9-1.8 1.8-1.8 3.2 0 1 .4 1.9 1.1 2.6-2.9-.3-5.1-2.6-5.1-5.5C5.5 9.7 12 8 12 2Z" />
<path d="M12 22c2.2 0 4-1.6 4-3.7 0-1.6-.8-2.4-1.7-3.3-.6-.6-1.3-1.3-1.6-2.3-.8.8-1.4 2-1.4 3.2 0 .9-.4 1.4-1 1.8-.5.4-1.1.9-1.1 1.9C9.2 21.1 10.4 22 12 22Z" />
</Svg>
);
}
export function SparklesIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6L12 3Z" />
<path d="M18.5 15.5l.8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8.8-2.2Z" />
</Svg>
);
}
export function LayersIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 3.5 20 8l-8 4.5L4 8l8-4.5Z" />
<path d="m4.6 12.4 7.4 4.2 7.4-4.2M4.6 16.4l7.4 4.2 7.4-4.2" />
</Svg>
);
}
export function FolderIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M3 7.5A1.5 1.5 0 0 1 4.5 6h4l2 2.5h9A1.5 1.5 0 0 1 21 10v8a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 18V7.5Z" />
</Svg>
);
}
export function CameraIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M4 8.5A1.5 1.5 0 0 1 5.5 7h1.9l1.1-1.8A1 1 0 0 1 9.4 5h5.2a1 1 0 0 1 .9.5L16.6 7h1.9A1.5 1.5 0 0 1 20 8.5v9A1.5 1.5 0 0 1 18.5 19h-13A1.5 1.5 0 0 1 4 17.5v-9Z" />
<circle cx="12" cy="12.8" r="3.2" />
</Svg>
);
}
export function PlayIcon({ className }: IconProps) {
return (
<Svg className={className} filled>
<path d="M8 5.5v13l11-6.5-11-6.5Z" />
</Svg>
);
}
export function DownloadIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 3v12M7.5 10.5 12 15l4.5-4.5M4 20h16" />
</Svg>
);
}
export function UploadIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 16V4M7.5 8.5 12 4l4.5 4.5M4 20h16" />
</Svg>
);
}
export function HomeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M4 10.5 12 4l8 6.5V19a1 1 0 0 1-1 1h-4v-5H9v5H5a1 1 0 0 1-1-1v-8.5Z" />
</Svg>
);
}
export function CheckCircleIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="9" />
<path d="m8.2 12.4 2.6 2.6 5-5.4" />
</Svg>
);
}
export function AlertTriangleIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 4.5 21 19.5H3L12 4.5Z" />
<path d="M12 10v4.2M12 17.2v.1" />
</Svg>
);
}
export function XCircleIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="9" />
<path d="m9 9 6 6M15 9l-6 6" />
</Svg>
);
}
export function PlusIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 5v14M5 12h14" />
</Svg>
);
}
export function ShareIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 3v12M8.2 6.8 12 3l3.8 3.8" />
<path d="M6 12H5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1h-1" />
</Svg>
);
}
export function ChevronLeftIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M14.5 5.5 8 12l6.5 6.5" />
</Svg>
);
}
export function MenuIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M4 7h16M4 12h16M4 17h16" />
</Svg>
);
}
export function ChevronRightIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="m9.5 5.5 6.5 6.5-6.5 6.5" />
</Svg>
);
}
export function RefreshIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M20 12a8 8 0 1 1-2.4-5.7M20 4v4h-4" />
</Svg>
);
}
export function EyeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z" />
<circle cx="12" cy="12" r="3" />
</Svg>
);
}
export function EyeOffIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="m3 3 18 18" />
<path d="M10.6 5.7c.5-.1.9-.2 1.4-.2 6 0 9.5 6.5 9.5 6.5a17.5 17.5 0 0 1-2.6 3.5" />
<path d="M6.6 6.8A16.6 16.6 0 0 0 2.5 12S6 18.5 12 18.5c1.4 0 2.7-.4 3.8-1" />
<path d="M9.9 9.9a3 3 0 0 0 4.2 4.2" />
</Svg>
);
}
export function QuestionIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="9" />
<path d="M9.7 9.4a2.5 2.5 0 0 1 4.9.7c0 1.7-2.5 2-2.5 3.5" />
<path d="M12 17h.01" />
</Svg>
);
}
export function XIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="m6 6 12 12M18 6 6 18" />
</Svg>
);
}