mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Phase 2, part 2. Baseline 72 -> 63.
Migrated: Tables, SortableTable, DonatePage, Share, Headline, ErrorBoundary,
and the QR code, VIN decoder, and temperature calculators.
New in the library, all in the gallery and tested:
SearchBox Semantic's `<Search category>`, keeping its grouped `results`
shape because four call sites already build it. A real
combobox: arrows move, Enter takes the highlighted suggestion
or submits the raw text, Escape closes without clearing.
Pagination Semantic's prop names, but onPageChange gets the page number
rather than an (event, data) pair.
TabBar the strip and its house style only; routing stays at the call
site, which is what decides what is active.
IconStack two glyphs composed into one symbol, named once for assistive
technology (Semantic's IconGroup).
Table.HeaderCell gains `sorted` and `onSort`.
That last one is a fix, not just a port. Semantic's `sorted` prop set
`aria-sort`; migrating a table by hand dropped it, leaving a clickable <th>
that no keyboard could reach and no screen reader could describe. Putting it
in the library means the next table gets it right without anyone remembering.
Night mode holes closed along the way: the QR codes on the donate, share, and
calculator pages are white by necessity — a scanner needs the contrast — and
they render as SVG, which the media filter does not target. In night mode that
was a lit panel. They now carry `.media`, so the filter remaps the block as a
unit and the contrast survives the mapping.
Also: `paypal` joins the icon map rather than being imported ad hoc in a page,
and FileBrowser stops passing Semantic-only table props into a DOM element.
204 lines
6.8 KiB
TypeScript
204 lines
6.8 KiB
TypeScript
import React from 'react';
|
|
import * as Tabler from '@tabler/icons-react';
|
|
|
|
/*
|
|
* Icons.
|
|
*
|
|
* Semantic UI shipped an icon font; we use Tabler's bundled SVG components, which
|
|
* are tree-shaken at build time and need nothing from the network.
|
|
*
|
|
* Icons never name a color. They stroke with `currentColor`, inheriting from the
|
|
* button or text that wraps them, so status color and every theme come for free.
|
|
*/
|
|
|
|
/** 16px inline (default), 20px emphasis, 24px page-level. */
|
|
export type IconSize = 'small' | 'medium' | 'large';
|
|
|
|
const sizes: Record<IconSize, number> = {small: 16, medium: 20, large: 24};
|
|
|
|
/**
|
|
* Semantic icon name -> Tabler component name.
|
|
*
|
|
* Covers every name the app renders today so call sites can migrate without also
|
|
* choosing new icons. New code should import the Tabler component directly and
|
|
* pass it as `component`; this map exists for the migration, not forever.
|
|
*/
|
|
const semanticNames: Record<string, keyof typeof Tabler> = {
|
|
'add': 'IconPlus',
|
|
'archive': 'IconArchive',
|
|
'arrow alternate circle up': 'IconCircleArrowUp',
|
|
'arrow down': 'IconArrowDown',
|
|
'arrow right': 'IconArrowRight',
|
|
'arrow up': 'IconArrowUp',
|
|
'balance scale': 'IconScale',
|
|
'book': 'IconBook',
|
|
'bug': 'IconBug',
|
|
'calculator': 'IconCalculator',
|
|
'calendar': 'IconCalendar',
|
|
'charset': 'IconLanguage',
|
|
'check': 'IconCheck',
|
|
'check circle': 'IconCircleCheck',
|
|
'checkmark': 'IconCheck',
|
|
'chevron left': 'IconChevronLeft',
|
|
'chevron right': 'IconChevronRight',
|
|
'chrome': 'IconBrandChrome',
|
|
'circle': 'IconCircle',
|
|
'circle notch': 'IconLoader2',
|
|
'circle notched': 'IconLoader2',
|
|
'close': 'IconX',
|
|
'columns': 'IconColumns',
|
|
'copy': 'IconCopy',
|
|
'cube': 'IconCube',
|
|
'directory': 'IconFolder',
|
|
'disk': 'IconDatabase',
|
|
'download': 'IconDownload',
|
|
'dropdown': 'IconChevronDown',
|
|
'edit': 'IconEdit',
|
|
'exclamation': 'IconExclamationMark',
|
|
'exclamation triangle': 'IconAlertTriangle',
|
|
'expand arrows alternate': 'IconArrowsMaximize',
|
|
'external': 'IconExternalLink',
|
|
'eye': 'IconEye',
|
|
'eye slash': 'IconEyeOff',
|
|
'file': 'IconFile',
|
|
'file alternate': 'IconFileDescription',
|
|
'file alternate outline': 'IconFileDescription',
|
|
'file code': 'IconFileCode',
|
|
'file pdf outline': 'IconFileTypePdf',
|
|
'file text': 'IconFileText',
|
|
'file video': 'IconFileSmile',
|
|
'film': 'IconMovie',
|
|
'filter': 'IconFilter',
|
|
'firefox': 'IconBrandFirefox',
|
|
'folder': 'IconFolder',
|
|
'folder open': 'IconFolderOpen',
|
|
'folder open outline': 'IconFolderOpen',
|
|
'folder outline': 'IconFolder',
|
|
'globe': 'IconWorld',
|
|
'hand point right': 'IconHandFinger',
|
|
'hdd': 'IconDeviceSdCard',
|
|
'heart': 'IconHeart',
|
|
'heartbeat': 'IconHeartRateMonitor',
|
|
'history': 'IconHistory',
|
|
'image': 'IconPhoto',
|
|
'lightbulb': 'IconBulb',
|
|
'lightbulb outline': 'IconBulb',
|
|
'lightning': 'IconBolt',
|
|
'moon': 'IconMoon',
|
|
'moon outline': 'IconMoon',
|
|
'linkify': 'IconLink',
|
|
'list': 'IconList',
|
|
'lock': 'IconLock',
|
|
'map outline': 'IconMap',
|
|
'microchip': 'IconCpu',
|
|
'minus': 'IconMinus',
|
|
'paypal': 'IconBrandPaypal',
|
|
'play': 'IconPlayerPlay',
|
|
'plug': 'IconPlug',
|
|
'plus': 'IconPlus',
|
|
'print': 'IconPrinter',
|
|
'puzzle piece': 'IconPuzzle',
|
|
'qrcode': 'IconQrcode',
|
|
'question': 'IconHelp',
|
|
'redo': 'IconArrowForwardUp',
|
|
'refresh': 'IconRefresh',
|
|
'rss': 'IconRss',
|
|
'save': 'IconDeviceFloppy',
|
|
'search': 'IconSearch',
|
|
'server': 'IconServer',
|
|
'settings': 'IconSettings',
|
|
'share': 'IconShare',
|
|
'shield': 'IconShield',
|
|
'spinner': 'IconLoader2',
|
|
'star': 'IconStarFilled',
|
|
'star outline': 'IconStar',
|
|
'stop': 'IconPlayerStop',
|
|
'sun': 'IconSun',
|
|
'sun outline': 'IconSun',
|
|
'sync': 'IconRefresh',
|
|
'tachometer alternate': 'IconGauge',
|
|
'tag': 'IconTag',
|
|
'terminal': 'IconTerminal2',
|
|
'th': 'IconLayoutGrid',
|
|
'thumbs up': 'IconThumbUp',
|
|
'trash': 'IconTrash',
|
|
'undo': 'IconArrowBackUp',
|
|
'unlock': 'IconLockOpen',
|
|
'upload': 'IconUpload',
|
|
'usb': 'IconUsb',
|
|
'volume up': 'IconVolume',
|
|
'warning': 'IconAlertTriangle',
|
|
'warning circle': 'IconAlertCircle',
|
|
'warning sign': 'IconAlertTriangle',
|
|
'wifi': 'IconWifi',
|
|
'wrench': 'IconTool',
|
|
'x': 'IconX',
|
|
};
|
|
|
|
export interface IconStackProps {
|
|
/** The main glyph. */
|
|
children: React.ReactNode;
|
|
/** A smaller glyph pinned to the bottom-right, qualifying the first. */
|
|
corner: React.ReactNode;
|
|
/** Accessible name for the pair; the two glyphs mean one thing together. */
|
|
label: string;
|
|
}
|
|
|
|
/**
|
|
* Two icons composed into one symbol — a wifi glyph with a question mark on it,
|
|
* say. Replaces Semantic's IconGroup.
|
|
*
|
|
* The corner glyph gets a background matching the surface so it stays legible
|
|
* over the icon beneath it.
|
|
*/
|
|
export function IconStack({children, corner, label}: IconStackProps) {
|
|
return <span className='wrolpi-icon-stack' role='img' aria-label={label}>
|
|
{children}
|
|
<span className='wrolpi-icon-stack-corner' aria-hidden='true'>{corner}</span>
|
|
</span>
|
|
}
|
|
|
|
export interface IconProps extends Omit<React.SVGProps<SVGSVGElement>, 'name' | 'ref'> {
|
|
/** A Semantic UI icon name, for migrated call sites. */
|
|
name?: string;
|
|
/** A Tabler component, for new code. */
|
|
component?: React.ComponentType<any>;
|
|
size?: IconSize | number;
|
|
/** Rotate continuously; for spinners and in-progress states. */
|
|
loading?: boolean;
|
|
/** Accessible name. Omit for decorative icons, which are hidden instead. */
|
|
label?: string;
|
|
}
|
|
|
|
/** Resolve a Semantic icon name to its Tabler component, or undefined if unmapped. */
|
|
export const resolveIconName = (name: string): React.ComponentType<any> | undefined => {
|
|
const taberName = semanticNames[name.trim().toLowerCase()];
|
|
return taberName ? (Tabler[taberName] as React.ComponentType<any>) : undefined;
|
|
}
|
|
|
|
export function Icon({name, component, size = 'small', loading, label, ...props}: IconProps) {
|
|
let Component = component;
|
|
if (!Component && name) {
|
|
Component = resolveIconName(name);
|
|
if (!Component) {
|
|
// Loud on purpose: a silently missing icon leaves a hole in the interface.
|
|
console.error(`No icon for Semantic name "${name}"; pass a Tabler component instead.`);
|
|
Component = Tabler.IconHelpCircle;
|
|
}
|
|
}
|
|
if (!Component) {
|
|
console.error('Icon requires `name` or `component`.');
|
|
return null;
|
|
}
|
|
|
|
const pixels = typeof size === 'number' ? size : sizes[size];
|
|
return <Component
|
|
size={pixels}
|
|
stroke={2}
|
|
className={loading ? 'wrolpi-icon-spin' : undefined}
|
|
aria-hidden={label ? undefined : 'true'}
|
|
aria-label={label}
|
|
role={label ? 'img' : undefined}
|
|
{...props}
|
|
/>
|
|
}
|