ui: bring back the card mimetype accent

Two agents independently dropped it, each because our Card had no `color` prop
and each citing the other's precedent -- which is how a deliberate feature
quietly becomes a convention.  Roland wanted it back.

Card takes a token colour name and draws it as a 3px edge along the top rather
than tinting the surface: surfaces stay flat per the design rules, and an edge
survives night mode without becoming a bright patch.  Because it is a token, the
accent remaps per theme -- four hues in light and dark, a single one in night and
amber, which is the point of those themes.

Restored on every card that shows a file: the video, image and generic file cards
in Files.js, and the archive card.  All of them take it from
`mimetypeColor(mimetype, path)`, the same helper that colours the file icons, so
a card and its icon agree.
This commit is contained in:
Roland 2026-07-30 13:51:05 -06:00
parent 96d84dfab8
commit a1fbda7363
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
5 changed files with 51 additions and 11 deletions

View file

@ -20,7 +20,8 @@ import {
SearchInput,
TabLinks,
textEllipsis,
useTitle
mimetypeColor,
useTitle,
} from "./Common";
import {
deleteDomain,
@ -425,7 +426,8 @@ export function ArchiveCard({file}) {
</div>
</>;
return <Card media={media} title={titleElm} meta={meta}>
return <Card media={media} title={titleElm} meta={meta}
color={mimetypeColor(file.mimetype, file.primary_path)}>
<div style={{display: 'flex', gap: '0.5em', marginTop: '0.5em'}}>
<Link to={`/archives/${file.id}`}>
<Button icon='file alternate'>Details</Button>

View file

@ -17,7 +17,8 @@ import {
TagIcon,
Toggle,
textEllipsis,
useTitle
mimetypeColor,
useTitle,
} from "./Common";
import {
usePages,
@ -116,6 +117,7 @@ function EbookCard({file, sortField}) {
return <Card media={<CardPoster file={file} to={detailUrl}/>}
title={<Tooltip label={title}>{header}</Tooltip>}
meta={meta}
color={mimetypeColor(file.mimetype, file.primary_path)}
/>
}
@ -134,6 +136,7 @@ function ImageCard({file}) {
return <Card media={<PreviewLink file={file}><CardPoster file={file}/></PreviewLink>}
title={<Tooltip label={title}>{header}</Tooltip>}
meta={meta}
color={mimetypeColor(file.mimetype, file.primary_path)}
/>
}
@ -175,7 +178,8 @@ function FileCard({file}) {
</>
}
</>;
return <Card media={<CardPoster file={file}/>} title={header} meta={meta}/>
return <Card media={<CardPoster file={file}/>} title={header} meta={meta}
color={mimetypeColor(file.mimetype, file.primary_path)}/>
}
export function FileCards({files}) {

View file

@ -382,20 +382,26 @@ export function ThemeSamplePage() {
<Section label='Cards'>
<CardGroup>
{[
['How To Sharpen An Axe The Right Way', 'Wranglerstar · 2024-11-03'],
['Pressure Canning Basics', 'RoseRed Homestead · 2025-02-17'],
['Solar Panel Wiring: Series vs Parallel', 'DIY Solar Power · 2025-08-21'],
['Ham Radio General License, Part 1', 'Ham Radio Crash Course · 2024-05-09'],
].map(([title, meta]) => <Card
['How To Sharpen An Axe The Right Way', 'Wranglerstar · 2024-11-03', 'blue', 'film'],
['Pressure Canning Basics', 'RoseRed Homestead · 2025-02-17', 'blue', 'film'],
['Water Storage Guide.pdf', 'docs/ · 2025-08-21', 'red', 'file pdf'],
['Ham Radio General License.epub', 'ebooks/ · 2024-05-09', 'yellow', 'book'],
].map(([title, meta, color, icon]) => <Card
key={title}
title={title}
meta={meta}
color={color}
media={<div style={{
aspectRatio: '16/9', background: 'var(--head)', display: 'flex',
alignItems: 'center', justifyContent: 'center', color: 'var(--muted)',
}}><Icon name='film' size={34}/></div>}
}}><Icon name={icon} size={34}/></div>}
/>)}
</CardGroup>
<p style={{fontSize: 12, color: 'var(--muted)', marginTop: 10}}>
The top edge carries the file type's colour, so a grid of results is
scannable by kind before any title is read. It comes from a token, so night
and amber render the accent in their own single hue rather than four.
</p>
</Section>
<Section label='Loading placeholders'>

View file

@ -124,10 +124,17 @@ export interface CardProps {
meta?: React.ReactNode;
children?: React.ReactNode;
onClick?: React.MouseEventHandler<HTMLDivElement>;
/**
* A token colour name, drawn as an accent along the card's top edge. File cards use
* it to carry the mimetype's colour, so a grid of results is scannable by kind before
* any text is read. It remaps per theme, so night gets a red accent rather than a
* dozen hues.
*/
color?: string;
className?: string;
}
export function Card({media, title, meta, children, onClick, className}: CardProps) {
export function Card({media, title, meta, children, onClick, color, className}: CardProps) {
return <MCard
withBorder
padding={0}
@ -139,6 +146,9 @@ export function Card({media, title, meta, children, onClick, className}: CardPro
cursor: onClick ? 'pointer' : undefined,
display: 'flex',
flexDirection: 'column',
// An accent edge rather than a tinted surface: the design rules keep surfaces
// flat, and a 3px edge survives night mode without becoming a bright patch.
borderTop: color ? `3px solid var(--${color})` : undefined,
}}
>
{media && <div style={{borderBottom: '1px solid var(--border)'}}>{media}</div>}

View file

@ -11,6 +11,7 @@ import {themeChoices} from '../../themes/names';
import {
ActionInput,
Button,
Card,
Confirm,
Header,
Icon,
@ -432,6 +433,23 @@ describe('Table', () => {
});
});
describe('Card', () => {
it('draws a mimetype accent from a token, not a hex', () => {
// File cards carry the mimetype's colour on their top edge so a grid of results is
// scannable by kind. Semantic did this with `<Card color='violet'>`; two migrated
// files had dropped the accent because our Card had no equivalent.
const {container} = renderUI(<Card title='Water Storage.pdf' color='red'/>);
expect(container.firstChild).toHaveStyle({borderTop: '3px solid var(--red)'});
});
it('has no accent when no colour is given', () => {
const {container} = renderUI(<Card title='Plain'/>);
expect(container.firstChild.style.borderTop).toBe('');
});
});
describe('Panel', () => {
it('marks danger zones', () => {
const {container} = renderUI(<Panel danger>Destructive actions</Panel>);