wrolpi/app/src/components/HelpModal.js
Roland e8dd804010
ui: thirty-five files off Semantic, leaving fifteen
Phase 2, part 4.  Baseline 50 -> 15.

Migrated: all eight calculators, the rest of inventory and collections, the
Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage,
DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings
and Status, Download, and App.

Also the three shared modules everything imports: api.js and customHooks.js
swap the Semantic toast library for the library's shim (same call shape), and
useForm.js renders the library's Message.  useForm accepts both the old
`{content, header, positive|negative}` shape and the new `{kind, title}`,
because the pages building those objects migrate separately.

Two library gaps closed, both found by call sites needing them:

  ActionInput is a forwardRef now.  Its ref reaches the input rather than the
  wrapper, because callers use it to focus or select the field -- a ref to the
  surrounding div would do neither, which is why one call site had resorted to
  hand-copying the component's markup.

  Header takes a `color` token name.  Three separate call sites had reached for
  an inline style to colour a heading that carries meaning ("3 items to add"
  against "2 to remove"), which is how a hex ends up in markup.

Two files needed repair after being interrupted mid-edit: MapViewer had a
Segment left in its error branch, and VideoPlayer had lost the useContext
import its own code still used.  Neither jest nor tsc noticed; only
`npm run build` did, which is the whole reason that check exists.

Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser,
FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim,
ControllerPage, admin/Downloads.  Theme.tsx is deliberately last -- the
unmigrated files still import its Semantic-backed wrappers, so it can only go
once they have.
2026-08-08 18:04:01 -06:00

149 lines
5.5 KiB
JavaScript

import React from "react";
import {Button, Header, Modal, Table} from "./ui";
import {SHORTCUTS} from "./KeyboardShortcutsProvider";
import {usePlatformModifier} from "../hooks/customHooks";
// Format keyboard keys for display
function KeyboardKey({children}) {
return (
<kbd style={{
backgroundColor: 'var(--head)',
border: '1px solid var(--border)',
color: 'var(--text)',
display: 'inline-block',
fontFamily: 'var(--font-mono)',
fontSize: '0.85em',
lineHeight: '1.4',
margin: '0 2px',
padding: '2px 6px',
whiteSpace: 'nowrap',
}}>
{children}
</kbd>
);
}
// Parse shortcut keys string and render as keyboard keys
function ShortcutKeys({keys, isMac}) {
// Handle multiple key combinations (e.g., "meta+k, ctrl+k")
let combinations = keys.split(', ');
// Filter to show only platform-appropriate shortcuts when there are multiple combinations
if (combinations.length > 1) {
const hasMeta = combinations.some(c => c.toLowerCase().includes('meta'));
const hasCtrl = combinations.some(c => c.toLowerCase().includes('ctrl'));
// If we have both meta and ctrl variants, show only the platform-appropriate one
if (hasMeta && hasCtrl) {
combinations = combinations.filter(c => {
const lowerCombo = c.toLowerCase();
if (isMac) {
return lowerCombo.includes('meta');
} else {
return lowerCombo.includes('ctrl');
}
});
}
}
return (
<span>
{combinations.map((combo, idx) => {
const parts = combo.split('+').map(part => {
// Convert key names to display format
switch (part.toLowerCase()) {
case 'meta':
return 'Cmd';
case 'ctrl':
return 'Ctrl';
case 'shift':
return 'Shift';
case 'escape':
return 'Esc';
default:
return part.toLowerCase();
}
});
// Handle sequence shortcuts like "g h"
const isSequence = combo.includes(' ') && !combo.includes('+');
if (isSequence) {
const seqParts = combo.split(' ');
return (
<span key={idx}>
{idx > 0 && ' / '}
{seqParts.map((p, i) => (
<React.Fragment key={i}>
<KeyboardKey>{p.toLowerCase()}</KeyboardKey>
{i < seqParts.length - 1 && ' then '}
</React.Fragment>
))}
</span>
);
}
return (
<span key={idx}>
{idx > 0 && ' / '}
{parts.map((p, i) => (
<React.Fragment key={i}>
<KeyboardKey>{p}</KeyboardKey>
{i < parts.length - 1 && ' + '}
</React.Fragment>
))}
</span>
);
})}
</span>
);
}
// Group shortcuts by category
function groupShortcutsByCategory(shortcuts) {
const groups = {};
shortcuts.forEach(shortcut => {
const category = shortcut.category || 'Other';
if (!groups[category]) {
groups[category] = [];
}
groups[category].push(shortcut);
});
return groups;
}
export default function HelpModal({open, onClose}) {
const {isMac} = usePlatformModifier();
const groupedShortcuts = groupShortcutsByCategory(SHORTCUTS);
// Order categories
const categoryOrder = ['Search', 'General', 'Help', 'Navigation', 'Page Search', 'Video Player'];
const orderedCategories = categoryOrder.filter(cat => groupedShortcuts[cat]);
return (
<Modal open={open} onClose={onClose} size='small'>
<Modal.Header>Keyboard Shortcuts</Modal.Header>
<Modal.Content>
{orderedCategories.map(category => (
<div key={category} style={{marginBottom: '1.5em'}}>
<Header as='h4'>{category}</Header>
<Table>
<Table.Body>
{groupedShortcuts[category].map((shortcut, idx) => (
<Table.Row key={idx}>
<Table.Cell width={6}>
<ShortcutKeys keys={shortcut.keys} isMac={isMac}/>
</Table.Cell>
<Table.Cell>{shortcut.description}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
</div>
))}
</Modal.Content>
<Modal.Actions>
<Button role='cancel' onClick={onClose}>Close</Button>
</Modal.Actions>
</Modal>
);
}