/**
* The three miniatures a theme selector is made of.
*
* Drawn from fixed neutrals rather than from theme tokens, which is the one
* thing about this file that is not obvious. A preview built out of the active
* theme's colours shows the reader the theme they already have, three times —
* the light option has to look light while the app around it is dark, or it is
* not a preview of anything.
*
* The greys are the ones a screenshot of an app would actually have: a ground,
* a surface sitting on it, a mark, and the lines of text. Two sets, and the
* system option draws both.
*/
import { useId, type ReactNode } from 'react';
import Svg, { Circle, ClipPath, Defs, G, Path, Rect } from 'react-native-svg';
/** Which of the three a preview is drawing. */
export type ThemePreviewMode = 'system' | 'light' | 'dark';
/** How the miniature is drawn. */
export type ThemePreviewVariant = 'window' | 'card';
interface Palette {
/** Behind everything — the desktop, the page, the space around the app. */
ground: string;
/** The app's own surface. */
surface: string;
/** A shape on it: an avatar, an image, a control. */
mark: string;
/** Lines of text. */
line: string;
}
const PALETTE: Record<'light' | 'dark', Palette> = {
light: { ground: '#e5e5e5', surface: '#ffffff', mark: '#d4d4d4', line: '#e5e5e5' },
dark: { ground: '#171717', surface: '#262626', mark: '#525252', line: '#404040' },
};
/**
* The one colour that is not a grey.
*
* Fixed for the same reason the greys are, and warm rather than blue: it has to
* read as "a colour in the app" on both grounds without looking like the ring
* that marks the chosen option.
*/
const ACCENT = '#c1704f';
/** Everything is drawn in this box and scaled by the container. */
const VIEW_BOX = { width: 88, height: 70 };
/**
* The lower-right half of the box, cut from the top-right corner to the
* bottom-left one. Both text lines sit above it and the accent below it, which
* is what makes one card show a light screen and a dark one at the same time.
*/
const DIAGONAL = `M${VIEW_BOX.width} 0V${VIEW_BOX.height}H0Z`;
/* -------------------------------------------------------------------------- *
* window — an app screen, with the panel inset from the leading edge
* -------------------------------------------------------------------------- */
function WindowHalf({ palette, side }: { palette: Palette; side: 'left' | 'right' | 'full' }) {
if (side === 'full') {
return (
<>
>
);
}
/*
* Half a screen each, and the halves are not mirror images: the panel starts
* at the same inset on the left and runs to the edge on the right, so the
* seam falls where the two app surfaces meet rather than down the middle of
* one of them.
*/
const x = side === 'left' ? 0 : 44;
return (
<>
{side === 'left' ? (
<>
>
) : (
<>
>
)}
>
);
}
/* -------------------------------------------------------------------------- *
* card — a framed card, with an accent on it
* -------------------------------------------------------------------------- */
/*
* The card fills its box rather than floating inside it.
*
* It used to carry a margin of its own, which then sat on top of the padding
* between the artwork and the ring around it — two gaps doing one job, and the
* bigger the miniature the further apart they held the two edges.
*/
const CARD_FRAME = { x: 0, y: 0, width: 88, height: 70, rx: 16 } as const;
const CARD_SURFACE = { x: 8, y: 8, width: 72, height: 54, rx: 10 } as const;
function CardBody({ palette }: { palette: Palette }) {
return (
<>
>
);
}
export interface ThemePreviewProps {
mode: ThemePreviewMode;
variant?: ThemePreviewVariant;
width: number;
}
/**
* One miniature.
*
* `width` rather than a scale factor: the option's box is what decides the
* size, and everything inside is drawn against a fixed box and scaled by the
* viewBox — so the geometry above never has to know how big it ended up.
*/
export function ThemePreview({ mode, variant = 'window', width }: ThemePreviewProps): ReactNode {
const height = Math.round((width / VIEW_BOX.width) * VIEW_BOX.height);
/*
* `useId` returns something with colons in it, which is not a name an SVG
* fragment reference accepts — a clip that silently fails to resolve draws
* nothing at all, and on a shape covering half the artwork that is the whole
* option missing.
*/
const clip = `panelui-theme-${useId().replace(/[^a-zA-Z0-9-]/g, '')}`;
if (variant === 'card') {
return (
);
}
return (
);
}