velxio/frontend/src/components/simulator/PinPickerDialog.tsx

345 lines
10 KiB
TypeScript
Raw Normal View History

/**
* PinPickerDialog
*
* Touch-friendly modal for picking a pin from a component or board. Shown
* primarily on mobile when the user can't reliably hit a 12px pin overlay.
*
* Used in two flows:
* 1. Wire in progress + user taps a component or board body → this dialog
* lists every pin so they can finish the wire by tapping a name.
* 2. (Future) Long-press on a board → start a wire from a chosen pin.
*
* Touch propagation: same trick as SelectionActionBar / WireModeBanner — bind
* native listeners to stop the canvas's own touch handler from running
* preventDefault and swallowing our synthetic clicks.
*/
import React, { useEffect, useMemo, useRef, useState } from 'react';
feat(i18n): translate small editor remates + admin internals (Blocks 11+12) Block 11 — small editor surfaces - PinPickerDialog: close, filter pins, no-match, rotate / delete action buttons. - RaspberryPiWorkspace: connection status (Connected / Starting… / Offline), Start Pi button + tooltip, Connect / Disconnect buttons + tooltips, Terminal tab label, file-tab close, the full offline overlay (title / subtitle / start CTA / two-line note about the staging area), terminal-loading + no-file- selected fallbacks. - GitHubStarBanner: aria-label, title, body copy, Star CTA, dismiss button. - ExampleLoaderPage: "Loading example…" status, "Example {{id}} not found." 404 line, "Browse all examples" recovery link. /editor and /examples links wrapped in localize(). Block 12 — admin-only internals (only admins ever see these) - AdminBoardsTab: section headings ("By board family" / "By exact FQBN"), full table column headers (Family / FQBN / Compiles / Errors / Success rate / Runs / Distinct users / Distinct projects), range selector label, no-data fallback, load-failed error. - AdminDashboardTab: 8 KPI cards (Total users / Total projects / Compiles / Runs / DAU / WAU / MAU / Public-Private), all chart titles + subtitles (Activity over time, Compiles by board family, Board diversity for pricing signal, Top FQBNs, Top countries with Cloudflare disclaimer, Top users / Top projects), table column headers, no-data fallbacks, load errors. Pluralised "{{count}} board(s)" via i18next plurals in the diversity pie chart. - UserActivityModal: title with username interpolation, subtitle, range selector label, table column headers (Date / Project / Compiles / Errors / Runs / Saves), pluralised "{{count}} project(s)" line, deleted-project / no-project placeholder strings, no-activity empty state, load-failed. All 8 non-English locales auto-translated via the `scripts/translate-i18n.mjs` DeepSeek pipeline (one --force run, ~7 min). sameShape() validation passed on every output.
2026-05-10 02:22:16 +07:00
import { useTranslation } from 'react-i18next';
interface PinInfoLike {
name: string;
x: number;
y: number;
description?: string;
}
interface PinPickerDialogProps {
/** Component or board ID — used by the parent to compute world-space pin coords. */
targetId: string;
/** Display label, e.g. "Arduino Uno" or "7 Segment". */
title: string;
/** Subtitle hint, e.g. "Tap a pin to connect". */
subtitle?: string;
/** Pin list to render. */
pins: PinInfoLike[];
onPinSelect: (targetId: string, pinName: string) => void;
onClose: () => void;
/** Optional rotate action — only meaningful for components. */
onRotate?: () => void;
/** Optional delete action — closes the dialog and removes the target. */
onDelete?: () => void;
}
export const PinPickerDialog: React.FC<PinPickerDialogProps> = ({
targetId,
title,
subtitle,
pins,
onPinSelect,
onClose,
onRotate,
onDelete,
}) => {
feat(i18n): translate small editor remates + admin internals (Blocks 11+12) Block 11 — small editor surfaces - PinPickerDialog: close, filter pins, no-match, rotate / delete action buttons. - RaspberryPiWorkspace: connection status (Connected / Starting… / Offline), Start Pi button + tooltip, Connect / Disconnect buttons + tooltips, Terminal tab label, file-tab close, the full offline overlay (title / subtitle / start CTA / two-line note about the staging area), terminal-loading + no-file- selected fallbacks. - GitHubStarBanner: aria-label, title, body copy, Star CTA, dismiss button. - ExampleLoaderPage: "Loading example…" status, "Example {{id}} not found." 404 line, "Browse all examples" recovery link. /editor and /examples links wrapped in localize(). Block 12 — admin-only internals (only admins ever see these) - AdminBoardsTab: section headings ("By board family" / "By exact FQBN"), full table column headers (Family / FQBN / Compiles / Errors / Success rate / Runs / Distinct users / Distinct projects), range selector label, no-data fallback, load-failed error. - AdminDashboardTab: 8 KPI cards (Total users / Total projects / Compiles / Runs / DAU / WAU / MAU / Public-Private), all chart titles + subtitles (Activity over time, Compiles by board family, Board diversity for pricing signal, Top FQBNs, Top countries with Cloudflare disclaimer, Top users / Top projects), table column headers, no-data fallbacks, load errors. Pluralised "{{count}} board(s)" via i18next plurals in the diversity pie chart. - UserActivityModal: title with username interpolation, subtitle, range selector label, table column headers (Date / Project / Compiles / Errors / Runs / Saves), pluralised "{{count}} project(s)" line, deleted-project / no-project placeholder strings, no-activity empty state, load-failed. All 8 non-English locales auto-translated via the `scripts/translate-i18n.mjs` DeepSeek pipeline (one --force run, ~7 min). sameShape() validation passed on every output.
2026-05-10 02:22:16 +07:00
const { t } = useTranslation();
const overlayRef = useRef<HTMLDivElement | null>(null);
const [filter, setFilter] = useState('');
// Stop native touch events from reaching the simulator canvas listener.
useEffect(() => {
const el = overlayRef.current;
if (!el) return;
const stop = (e: Event) => e.stopPropagation();
el.addEventListener('touchstart', stop, { passive: false });
el.addEventListener('touchmove', stop, { passive: false });
el.addEventListener('touchend', stop, { passive: false });
el.addEventListener('mousedown', stop);
return () => {
el.removeEventListener('touchstart', stop);
el.removeEventListener('touchmove', stop);
el.removeEventListener('touchend', stop);
el.removeEventListener('mousedown', stop);
};
}, []);
const filteredPins = useMemo(() => {
const q = filter.trim().toLowerCase();
if (!q) return pins;
return pins.filter(
(p) =>
p.name.toLowerCase().includes(q) ||
(p.description && p.description.toLowerCase().includes(q)),
);
}, [pins, filter]);
return (
<div
ref={overlayRef}
role="dialog"
aria-modal="true"
aria-label={`Pick a pin on ${title}`}
onClick={(e) => {
// Click on backdrop (not the panel itself) closes the dialog.
if (e.target === overlayRef.current) onClose();
}}
style={{
position: 'absolute',
inset: 0,
background: 'rgba(0, 0, 0, 0.5)',
zIndex: 1100,
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'center',
padding: 8,
touchAction: 'none',
}}
>
<div
style={{
background: '#2d2d2d',
color: '#fff',
border: '1px solid #555',
borderRadius: 12,
width: '100%',
maxWidth: 420,
maxHeight: '90%',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 -8px 24px rgba(0,0,0,0.6)',
overflow: 'hidden',
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
}}
>
{/* Header */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '12px 14px',
borderBottom: '1px solid #444',
flexShrink: 0,
}}
>
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minWidth: 0 }}>
<span style={{ fontSize: 14, fontWeight: 600 }}>{title}</span>
{subtitle && (
<span style={{ fontSize: 12, color: '#9aa', marginTop: 2 }}>{subtitle}</span>
)}
</div>
<button
type="button"
onClick={onClose}
feat(i18n): translate small editor remates + admin internals (Blocks 11+12) Block 11 — small editor surfaces - PinPickerDialog: close, filter pins, no-match, rotate / delete action buttons. - RaspberryPiWorkspace: connection status (Connected / Starting… / Offline), Start Pi button + tooltip, Connect / Disconnect buttons + tooltips, Terminal tab label, file-tab close, the full offline overlay (title / subtitle / start CTA / two-line note about the staging area), terminal-loading + no-file- selected fallbacks. - GitHubStarBanner: aria-label, title, body copy, Star CTA, dismiss button. - ExampleLoaderPage: "Loading example…" status, "Example {{id}} not found." 404 line, "Browse all examples" recovery link. /editor and /examples links wrapped in localize(). Block 12 — admin-only internals (only admins ever see these) - AdminBoardsTab: section headings ("By board family" / "By exact FQBN"), full table column headers (Family / FQBN / Compiles / Errors / Success rate / Runs / Distinct users / Distinct projects), range selector label, no-data fallback, load-failed error. - AdminDashboardTab: 8 KPI cards (Total users / Total projects / Compiles / Runs / DAU / WAU / MAU / Public-Private), all chart titles + subtitles (Activity over time, Compiles by board family, Board diversity for pricing signal, Top FQBNs, Top countries with Cloudflare disclaimer, Top users / Top projects), table column headers, no-data fallbacks, load errors. Pluralised "{{count}} board(s)" via i18next plurals in the diversity pie chart. - UserActivityModal: title with username interpolation, subtitle, range selector label, table column headers (Date / Project / Compiles / Errors / Runs / Saves), pluralised "{{count}} project(s)" line, deleted-project / no-project placeholder strings, no-activity empty state, load-failed. All 8 non-English locales auto-translated via the `scripts/translate-i18n.mjs` DeepSeek pipeline (one --force run, ~7 min). sameShape() validation passed on every output.
2026-05-10 02:22:16 +07:00
aria-label={t('editor.pinPicker.close')}
style={{
background: 'transparent',
border: 'none',
color: '#ccc',
cursor: 'pointer',
fontSize: 22,
width: 36,
height: 36,
borderRadius: 6,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
×
</button>
</div>
{/* Filter input — handy when boards have 30+ pins */}
{pins.length > 8 && (
<div style={{ padding: '8px 12px', flexShrink: 0 }}>
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
feat(i18n): translate small editor remates + admin internals (Blocks 11+12) Block 11 — small editor surfaces - PinPickerDialog: close, filter pins, no-match, rotate / delete action buttons. - RaspberryPiWorkspace: connection status (Connected / Starting… / Offline), Start Pi button + tooltip, Connect / Disconnect buttons + tooltips, Terminal tab label, file-tab close, the full offline overlay (title / subtitle / start CTA / two-line note about the staging area), terminal-loading + no-file- selected fallbacks. - GitHubStarBanner: aria-label, title, body copy, Star CTA, dismiss button. - ExampleLoaderPage: "Loading example…" status, "Example {{id}} not found." 404 line, "Browse all examples" recovery link. /editor and /examples links wrapped in localize(). Block 12 — admin-only internals (only admins ever see these) - AdminBoardsTab: section headings ("By board family" / "By exact FQBN"), full table column headers (Family / FQBN / Compiles / Errors / Success rate / Runs / Distinct users / Distinct projects), range selector label, no-data fallback, load-failed error. - AdminDashboardTab: 8 KPI cards (Total users / Total projects / Compiles / Runs / DAU / WAU / MAU / Public-Private), all chart titles + subtitles (Activity over time, Compiles by board family, Board diversity for pricing signal, Top FQBNs, Top countries with Cloudflare disclaimer, Top users / Top projects), table column headers, no-data fallbacks, load errors. Pluralised "{{count}} board(s)" via i18next plurals in the diversity pie chart. - UserActivityModal: title with username interpolation, subtitle, range selector label, table column headers (Date / Project / Compiles / Errors / Runs / Saves), pluralised "{{count}} project(s)" line, deleted-project / no-project placeholder strings, no-activity empty state, load-failed. All 8 non-English locales auto-translated via the `scripts/translate-i18n.mjs` DeepSeek pipeline (one --force run, ~7 min). sameShape() validation passed on every output.
2026-05-10 02:22:16 +07:00
placeholder={t('editor.pinPicker.filterPins')}
autoFocus={false}
style={{
width: '100%',
boxSizing: 'border-box',
padding: '8px 10px',
background: '#1f1f1f',
color: '#fff',
border: '1px solid #555',
borderRadius: 6,
fontSize: 14,
}}
/>
</div>
)}
{/* Pin list */}
<div
style={{
flex: 1,
overflowY: 'auto',
WebkitOverflowScrolling: 'touch',
overscrollBehavior: 'contain',
padding: '4px 8px 8px',
}}
>
{filteredPins.length === 0 ? (
feat(i18n): translate small editor remates + admin internals (Blocks 11+12) Block 11 — small editor surfaces - PinPickerDialog: close, filter pins, no-match, rotate / delete action buttons. - RaspberryPiWorkspace: connection status (Connected / Starting… / Offline), Start Pi button + tooltip, Connect / Disconnect buttons + tooltips, Terminal tab label, file-tab close, the full offline overlay (title / subtitle / start CTA / two-line note about the staging area), terminal-loading + no-file- selected fallbacks. - GitHubStarBanner: aria-label, title, body copy, Star CTA, dismiss button. - ExampleLoaderPage: "Loading example…" status, "Example {{id}} not found." 404 line, "Browse all examples" recovery link. /editor and /examples links wrapped in localize(). Block 12 — admin-only internals (only admins ever see these) - AdminBoardsTab: section headings ("By board family" / "By exact FQBN"), full table column headers (Family / FQBN / Compiles / Errors / Success rate / Runs / Distinct users / Distinct projects), range selector label, no-data fallback, load-failed error. - AdminDashboardTab: 8 KPI cards (Total users / Total projects / Compiles / Runs / DAU / WAU / MAU / Public-Private), all chart titles + subtitles (Activity over time, Compiles by board family, Board diversity for pricing signal, Top FQBNs, Top countries with Cloudflare disclaimer, Top users / Top projects), table column headers, no-data fallbacks, load errors. Pluralised "{{count}} board(s)" via i18next plurals in the diversity pie chart. - UserActivityModal: title with username interpolation, subtitle, range selector label, table column headers (Date / Project / Compiles / Errors / Runs / Saves), pluralised "{{count}} project(s)" line, deleted-project / no-project placeholder strings, no-activity empty state, load-failed. All 8 non-English locales auto-translated via the `scripts/translate-i18n.mjs` DeepSeek pipeline (one --force run, ~7 min). sameShape() validation passed on every output.
2026-05-10 02:22:16 +07:00
<div style={{ padding: 16, color: '#999', fontSize: 13 }}>{t('editor.pinPicker.noMatch')}</div>
) : (
filteredPins.map((pin) => (
<button
key={pin.name}
type="button"
onClick={() => onPinSelect(targetId, pin.name)}
style={{
display: 'flex',
alignItems: 'center',
width: '100%',
textAlign: 'left',
padding: '12px 12px',
margin: '2px 0',
background: '#3d3d3d',
color: '#fff',
border: '1px solid transparent',
borderRadius: 6,
cursor: 'pointer',
fontSize: 14,
minHeight: 44,
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#4a4a4a';
e.currentTarget.style.borderColor = '#007acc';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#3d3d3d';
e.currentTarget.style.borderColor = 'transparent';
}}
>
<span style={{ color: '#00d9ff', fontWeight: 600, marginRight: 10 }}>
{pin.name}
</span>
{pin.description && (
<span style={{ color: '#aaa', fontSize: 12, flex: 1, minWidth: 0 }}>
{pin.description}
</span>
)}
<span
aria-hidden="true"
style={{ marginLeft: 'auto', color: '#007acc', fontSize: 16, fontWeight: 700 }}
>
→
</span>
</button>
))
)}
</div>
{/* Action footer — Rotate / Delete pinned at the bottom of the
sheet so they're always reachable, mirroring the property
dialog. Only rendered when the parent passes the handlers. */}
{(onRotate || onDelete) && (
<div
style={{
display: 'flex',
gap: 8,
padding: '10px 12px',
borderTop: '1px solid #444',
background: '#252525',
flexShrink: 0,
}}
>
{onRotate && (
<button
type="button"
onClick={onRotate}
style={footerButtonStyle}
feat(i18n): translate small editor remates + admin internals (Blocks 11+12) Block 11 — small editor surfaces - PinPickerDialog: close, filter pins, no-match, rotate / delete action buttons. - RaspberryPiWorkspace: connection status (Connected / Starting… / Offline), Start Pi button + tooltip, Connect / Disconnect buttons + tooltips, Terminal tab label, file-tab close, the full offline overlay (title / subtitle / start CTA / two-line note about the staging area), terminal-loading + no-file- selected fallbacks. - GitHubStarBanner: aria-label, title, body copy, Star CTA, dismiss button. - ExampleLoaderPage: "Loading example…" status, "Example {{id}} not found." 404 line, "Browse all examples" recovery link. /editor and /examples links wrapped in localize(). Block 12 — admin-only internals (only admins ever see these) - AdminBoardsTab: section headings ("By board family" / "By exact FQBN"), full table column headers (Family / FQBN / Compiles / Errors / Success rate / Runs / Distinct users / Distinct projects), range selector label, no-data fallback, load-failed error. - AdminDashboardTab: 8 KPI cards (Total users / Total projects / Compiles / Runs / DAU / WAU / MAU / Public-Private), all chart titles + subtitles (Activity over time, Compiles by board family, Board diversity for pricing signal, Top FQBNs, Top countries with Cloudflare disclaimer, Top users / Top projects), table column headers, no-data fallbacks, load errors. Pluralised "{{count}} board(s)" via i18next plurals in the diversity pie chart. - UserActivityModal: title with username interpolation, subtitle, range selector label, table column headers (Date / Project / Compiles / Errors / Runs / Saves), pluralised "{{count}} project(s)" line, deleted-project / no-project placeholder strings, no-activity empty state, load-failed. All 8 non-English locales auto-translated via the `scripts/translate-i18n.mjs` DeepSeek pipeline (one --force run, ~7 min). sameShape() validation passed on every output.
2026-05-10 02:22:16 +07:00
aria-label={t('editor.pinPicker.rotate')}
>
feat(i18n): translate small editor remates + admin internals (Blocks 11+12) Block 11 — small editor surfaces - PinPickerDialog: close, filter pins, no-match, rotate / delete action buttons. - RaspberryPiWorkspace: connection status (Connected / Starting… / Offline), Start Pi button + tooltip, Connect / Disconnect buttons + tooltips, Terminal tab label, file-tab close, the full offline overlay (title / subtitle / start CTA / two-line note about the staging area), terminal-loading + no-file- selected fallbacks. - GitHubStarBanner: aria-label, title, body copy, Star CTA, dismiss button. - ExampleLoaderPage: "Loading example…" status, "Example {{id}} not found." 404 line, "Browse all examples" recovery link. /editor and /examples links wrapped in localize(). Block 12 — admin-only internals (only admins ever see these) - AdminBoardsTab: section headings ("By board family" / "By exact FQBN"), full table column headers (Family / FQBN / Compiles / Errors / Success rate / Runs / Distinct users / Distinct projects), range selector label, no-data fallback, load-failed error. - AdminDashboardTab: 8 KPI cards (Total users / Total projects / Compiles / Runs / DAU / WAU / MAU / Public-Private), all chart titles + subtitles (Activity over time, Compiles by board family, Board diversity for pricing signal, Top FQBNs, Top countries with Cloudflare disclaimer, Top users / Top projects), table column headers, no-data fallbacks, load errors. Pluralised "{{count}} board(s)" via i18next plurals in the diversity pie chart. - UserActivityModal: title with username interpolation, subtitle, range selector label, table column headers (Date / Project / Compiles / Errors / Runs / Saves), pluralised "{{count}} project(s)" line, deleted-project / no-project placeholder strings, no-activity empty state, load-failed. All 8 non-English locales auto-translated via the `scripts/translate-i18n.mjs` DeepSeek pipeline (one --force run, ~7 min). sameShape() validation passed on every output.
2026-05-10 02:22:16 +07:00
<RotateGlyph /> {t('editor.pinPicker.rotate')}
</button>
)}
{onDelete && (
<button
type="button"
onClick={onDelete}
style={{
...footerButtonStyle,
marginLeft: onRotate ? 0 : 'auto',
background: '#3a2424',
borderColor: '#5a2c2c',
color: '#e06c75',
}}
feat(i18n): translate small editor remates + admin internals (Blocks 11+12) Block 11 — small editor surfaces - PinPickerDialog: close, filter pins, no-match, rotate / delete action buttons. - RaspberryPiWorkspace: connection status (Connected / Starting… / Offline), Start Pi button + tooltip, Connect / Disconnect buttons + tooltips, Terminal tab label, file-tab close, the full offline overlay (title / subtitle / start CTA / two-line note about the staging area), terminal-loading + no-file- selected fallbacks. - GitHubStarBanner: aria-label, title, body copy, Star CTA, dismiss button. - ExampleLoaderPage: "Loading example…" status, "Example {{id}} not found." 404 line, "Browse all examples" recovery link. /editor and /examples links wrapped in localize(). Block 12 — admin-only internals (only admins ever see these) - AdminBoardsTab: section headings ("By board family" / "By exact FQBN"), full table column headers (Family / FQBN / Compiles / Errors / Success rate / Runs / Distinct users / Distinct projects), range selector label, no-data fallback, load-failed error. - AdminDashboardTab: 8 KPI cards (Total users / Total projects / Compiles / Runs / DAU / WAU / MAU / Public-Private), all chart titles + subtitles (Activity over time, Compiles by board family, Board diversity for pricing signal, Top FQBNs, Top countries with Cloudflare disclaimer, Top users / Top projects), table column headers, no-data fallbacks, load errors. Pluralised "{{count}} board(s)" via i18next plurals in the diversity pie chart. - UserActivityModal: title with username interpolation, subtitle, range selector label, table column headers (Date / Project / Compiles / Errors / Runs / Saves), pluralised "{{count}} project(s)" line, deleted-project / no-project placeholder strings, no-activity empty state, load-failed. All 8 non-English locales auto-translated via the `scripts/translate-i18n.mjs` DeepSeek pipeline (one --force run, ~7 min). sameShape() validation passed on every output.
2026-05-10 02:22:16 +07:00
aria-label={t('editor.pinPicker.delete')}
>
feat(i18n): translate small editor remates + admin internals (Blocks 11+12) Block 11 — small editor surfaces - PinPickerDialog: close, filter pins, no-match, rotate / delete action buttons. - RaspberryPiWorkspace: connection status (Connected / Starting… / Offline), Start Pi button + tooltip, Connect / Disconnect buttons + tooltips, Terminal tab label, file-tab close, the full offline overlay (title / subtitle / start CTA / two-line note about the staging area), terminal-loading + no-file- selected fallbacks. - GitHubStarBanner: aria-label, title, body copy, Star CTA, dismiss button. - ExampleLoaderPage: "Loading example…" status, "Example {{id}} not found." 404 line, "Browse all examples" recovery link. /editor and /examples links wrapped in localize(). Block 12 — admin-only internals (only admins ever see these) - AdminBoardsTab: section headings ("By board family" / "By exact FQBN"), full table column headers (Family / FQBN / Compiles / Errors / Success rate / Runs / Distinct users / Distinct projects), range selector label, no-data fallback, load-failed error. - AdminDashboardTab: 8 KPI cards (Total users / Total projects / Compiles / Runs / DAU / WAU / MAU / Public-Private), all chart titles + subtitles (Activity over time, Compiles by board family, Board diversity for pricing signal, Top FQBNs, Top countries with Cloudflare disclaimer, Top users / Top projects), table column headers, no-data fallbacks, load errors. Pluralised "{{count}} board(s)" via i18next plurals in the diversity pie chart. - UserActivityModal: title with username interpolation, subtitle, range selector label, table column headers (Date / Project / Compiles / Errors / Runs / Saves), pluralised "{{count}} project(s)" line, deleted-project / no-project placeholder strings, no-activity empty state, load-failed. All 8 non-English locales auto-translated via the `scripts/translate-i18n.mjs` DeepSeek pipeline (one --force run, ~7 min). sameShape() validation passed on every output.
2026-05-10 02:22:16 +07:00
<TrashGlyph /> {t('editor.pinPicker.delete')}
</button>
)}
</div>
)}
</div>
</div>
);
};
const footerButtonStyle: React.CSSProperties = {
flex: 1,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
padding: '10px 12px',
background: '#3d3d3d',
color: '#fff',
border: '1px solid #555',
borderRadius: 6,
cursor: 'pointer',
fontSize: 14,
minHeight: 44,
};
const RotateGlyph: React.FC = () => (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<polyline points="23 4 23 10 17 10" />
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
</svg>
);
const TrashGlyph: React.FC = () => (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<polyline points="3 6 5 6 21 6" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
<line x1="10" y1="11" x2="10" y2="17" />
<line x1="14" y1="11" x2="14" y2="17" />
</svg>
);