Files
daily_console_web/src/features/store-user/ui.tsx
2026-09-17 17:57:21 +05:30

180 lines
4.6 KiB
TypeScript

import { useState, type ReactNode } from 'react';
import { ImageOff } from 'lucide-react';
/**
* The pieces every Store user page is built from.
*
* Copied — deliberately, and once — from the Store Admin pages rather than
* invented, so the two workspaces are the same product seen from two heights.
* The values are not arbitrary: 32px controls, radius 12, 13px labels, centred
* table cells and a soft chip in the row's own colour are the corrections the
* merchant pages already went through.
*/
/* The sixth and last copy of `TabButton` lived here, exported for the Store
user pages. Every one of them now imports `components/TabBar`. */
/** A header action drawn like the tabs, so the two rows read as one bar. */
export function BarAction({
label,
icon,
onClick,
isDisabled,
}: {
label: string;
icon: ReactNode;
onClick: () => void;
isDisabled?: boolean;
}) {
const [isHovered, setIsHovered] = useState(false);
return (
<button
type="button"
onClick={onClick}
disabled={isDisabled}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 6,
height: 32,
padding: '0 12px',
borderRadius: 12,
border: 0,
background: isHovered && !isDisabled ? 'var(--color-surface-sunken)' : 'transparent',
color: isDisabled ? 'var(--color-ink-4)' : 'var(--color-ink-3)',
fontSize: 13,
fontWeight: 500,
cursor: isDisabled ? 'not-allowed' : 'pointer',
transition: 'background .2s, color .2s',
}}
>
{icon}
{label}
</button>
);
}
/**
* A column heading.
*
* `align` defaults to centre because every store-user table was built that
* way and changing them all is not this prop's job. Pass `"end"` for a column
* of numbers: a figure is read against the ones above and below it, and that
* only works when the digits line up on the right.
*/
export function Th({
children,
align = 'center',
}: {
children?: ReactNode;
align?: 'start' | 'center' | 'end';
}) {
return (
<th
style={{
textAlign: align,
padding: '10px',
borderBottom: '1px solid var(--color-line)',
fontSize: 12,
fontWeight: 600,
color: 'var(--color-ink-3)',
whiteSpace: 'nowrap',
}}
>
{children}
</th>
);
}
export function Td({
children,
isMuted,
isStrong,
align = 'center',
}: {
children: ReactNode;
isMuted?: boolean;
isStrong?: boolean;
align?: 'start' | 'center' | 'end';
}) {
return (
<td
style={{
textAlign: align,
padding: '10px',
borderBottom: '1px solid color-mix(in oklab, var(--color-line) 55%, transparent)',
color: isMuted ? 'var(--color-ink-3)' : 'var(--color-ink-1)',
fontWeight: isStrong ? 600 : 400,
verticalAlign: 'middle',
}}
>
{children}
</td>
);
}
/** Soft fill, 26% border, in its own colour — the console's one status chip. */
export function Chip({ label, color }: { label: string; color: string }) {
return (
<span
style={{
display: 'inline-flex',
alignItems: 'center',
borderRadius: 999,
padding: '2px 9px',
fontSize: 11.5,
fontWeight: 600,
whiteSpace: 'nowrap',
color,
background: `color-mix(in oklab, ${color} 12%, transparent)`,
border: `1px solid color-mix(in oklab, ${color} 26%, transparent)`,
}}
>
{label}
</span>
);
}
/**
* A 40px pack shot.
*
* `mix-blend-multiply` because catalogue photography is shot on white — without
* it every thumbnail carries a grey square where the photo's ground meets the
* row.
*/
export function Thumb({ images, alt }: { images: readonly string[]; alt: string }) {
const [failed, setFailed] = useState(false);
const src = images[0];
return (
<span
style={{
width: 40,
height: 40,
flex: 'none',
display: 'grid',
placeItems: 'center',
borderRadius: 10,
border: '1px solid var(--color-line)',
background: 'var(--color-surface)',
overflow: 'hidden',
}}
>
{src && !failed ? (
<img
src={src}
alt={alt}
loading="lazy"
referrerPolicy="no-referrer"
onError={() => setFailed(true)}
style={{ width: '100%', height: '100%', objectFit: 'contain', mixBlendMode: 'multiply' }}
/>
) : (
<ImageOff size={15} style={{ color: 'var(--color-line)' }} />
)}
</span>
);
}