freezmo/components/icons.tsx
Martin Dvorak a4c85ed77a Frutiger Aero: the theme now plays its soundtrack
Selecting Aero starts a random track from the Frutiger Aero playlist
(Bigblamboii's set on SoundCloud); every following track is drawn at random
too, so the set shuffles itself. The player is the credit bar from the mock:
a 44px glass circle with a note by default, expanding into pause + the
track's title + "SoundCloud - artist" (both reported by the widget).

- Only Aero ever touches SoundCloud: the embed and the widget API are created
  while the theme is on and the listener has not turned it off; light/dark
  request nothing (pinned by a suite check).
- The off choice persists (freezmo.aero-music.v1); re-selecting Aero in
  Settings is fresh consent (applyTheme clears it).
- Autoplay rides the theme-selection gesture; after a reload the circle comes
  up paused (browsers require a gesture) and one tap starts it.
- Volume 45; the iframe is 1px and inert, only the audio engine; the credit
  area collapses back to the circle without stopping the music.
- Geometry: clears the phone tab bar, lifts above the chat composer and the
  wizard bar, steps aside under immersive overlays and while the cookie
  notice shares the corner. 44px targets on phones.
- Cookie policy gained the third-party-embed paragraph it was owed.

tests/aero-music-test.mjs (16th suite, stubbed widget API, 21 checks). All
suites green, contrast scan clean. Version 0.7.0, SW v92.
2026-10-15 22:47:44 +00:00

451 lines
12 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>
);
}
/** A chat bubble with a plus: "new chat" — distinct from the plain plus the
* rail uses for "new deck" (two identical + buttons read as the same thing). */
export function ChatPlusIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5Z" />
<path d="M12 8.5v5M9.5 11h5" />
</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 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>
);
}
export function ExternalLinkIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M14 4h6v6" />
<path d="m20 4-8.5 8.5" />
<path d="M18 13.5V19a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5.5" />
</Svg>
);
}
/* --- Install-sheet chips ------------------------------------------------ */
export function CompassIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="9" />
<path d="m15.6 8.4-2.3 5.3-5.3 2.3 2.3-5.3 5.3-2.3Z" />
</Svg>
);
}
export function EllipsisIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="5.5" cy="12" r="1.4" fill="currentColor" stroke="none" />
<circle cx="12" cy="12" r="1.4" fill="currentColor" stroke="none" />
<circle cx="18.5" cy="12" r="1.4" fill="currentColor" stroke="none" />
</Svg>
);
}
export function ChevronDownIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="m6 9.5 6 6 6-6" />
</Svg>
);
}
export function AddToHomeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<rect x="4" y="4" width="16" height="16" rx="4.5" />
<path d="M12 8.8v6.4M8.8 12h6.4" />
</Svg>
);
}
/* --- Chat --------------------------------------------------------------- */
export function ChatIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5Z" />
</Svg>
);
}
export function SendIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 19V5" />
<path d="m6 11 6-6 6 6" />
</Svg>
);
}
export function StopIcon({ className }: IconProps) {
return (
<Svg className={className}>
<rect x="7" y="7" width="10" height="10" rx="2" fill="currentColor" stroke="none" />
</Svg>
);
}
export function CopyIcon({ className }: IconProps) {
return (
<Svg className={className}>
<rect x="9" y="9" width="11" height="11" rx="2.5" />
<path d="M5.5 15H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v.5" />
</Svg>
);
}
export function PaperclipIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="m20 11.5-7.8 7.8a5 5 0 0 1-7-7l8.3-8.3a3.3 3.3 0 0 1 4.7 4.7l-8.3 8.3a1.7 1.7 0 0 1-2.4-2.4l7.4-7.4" />
</Svg>
);
}
export function PencilIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M4 20h4.2L20 8.2a2.1 2.1 0 0 0-3-3L5.2 17 4 20Z" />
<path d="m14.5 6.7 2.8 2.8" />
</Svg>
);
}
export function HistoryIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M4.5 12a7.5 7.5 0 1 1 2.2 5.3" />
<path d="M4.5 12H2m2.5 0 2.3-2.3M4.5 12l2.3 2.3" />
<path d="M12 8.5V12l2.4 1.6" />
</Svg>
);
}
export function ChromeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="9" />
<circle cx="12" cy="12" r="3.4" />
<path d="M12 8.6V3M9.05 13.7 4.2 16.5M14.95 13.7l4.85 2.8" />
</Svg>
);
}
/** Read-aloud control (study cards); the waves light up while speaking. */
export function SpeakerIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M4.5 9.4h2.9l4-3.2v11.6l-4-3.2H4.5z" />
<path d="M15.2 9.2a3.9 3.9 0 0 1 0 5.6M17.7 6.7a7.3 7.3 0 0 1 0 10.6" />
</Svg>
);
}
/** Timed runs. */
export function ClockIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="8.2" />
<path d="M12 7.4V12l3.1 1.9" />
</Svg>
);
}
/** Frutiger Aero's soundtrack player. */
export function MusicIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M9.4 17.2V6.6l8-1.6v10.4" />
<circle cx="6.9" cy="17.6" r="2.5" />
<circle cx="14.9" cy="15.8" r="2.5" />
</Svg>
);
}
export function PauseIcon({ className }: IconProps) {
return (
<Svg className={className} filled>
<rect x="7.2" y="5.6" width="3.4" height="12.8" rx="1.4" />
<rect x="13.4" y="5.6" width="3.4" height="12.8" rx="1.4" />
</Svg>
);
}
/** "From your notes" — a page with a folded corner and text lines. */
export function NotesIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M6.4 3.6h7.4l4.8 4.8v12H6.4z" />
<path d="M13.6 3.9v4.7h4.7M9.4 12.4h5.2M9.4 15.8h5.2" />
</Svg>
);
}