Create src/renderer/components/ui/icons.ts as the single re-export point
for every Phosphor icon the renderer touches, then rewrite the 95 files
that previously imported directly from "@phosphor-icons/react" to pull
from the relative-path-to-barrel instead. The TitleBar commit earlier
in this series already used ui/icons; this commit extends the pattern
to the rest of the renderer.
Benefits now wired in:
- one location where bundle tree-shaking can be reasoned about,
- typos like Briefcase vs BriefCase show up against a finite list,
- re-theming or swapping icon sets is a one-file edit,
- a lint rule that flags raw @phosphor-icons/react imports can be
added later without mass-rewriting call sites.
The barrel also re-exports the `Icon` type so call sites that type
icon props against the Phosphor component type work unchanged.
Test count stays at 363 passing; typecheck stays at 156 errors.
97 lines
2.8 KiB
TypeScript
97 lines
2.8 KiB
TypeScript
import { forwardRef, useCallback, type ReactNode, type KeyboardEvent, type ChangeEvent } from "react";
|
|
import { PaperPlaneTilt } from "../../ui/icons";
|
|
import { ActionButton, InlineNotice } from "../../ui";
|
|
|
|
export interface ComposerShellProps {
|
|
value: string;
|
|
onChange: (value: string, event: ChangeEvent<HTMLTextAreaElement>) => void;
|
|
onSend: () => void;
|
|
placeholder: string;
|
|
sending: boolean;
|
|
error?: string | null;
|
|
shortcutHint?: string;
|
|
sendLabel?: string;
|
|
sendDisabled?: boolean;
|
|
leading?: ReactNode;
|
|
belowHeader?: ReactNode;
|
|
onKeyDownExtra?: (event: KeyboardEvent<HTMLTextAreaElement>) => boolean;
|
|
textareaAriaLabel?: string;
|
|
rows?: number;
|
|
}
|
|
|
|
function SendButton({ sending, disabled, onClick, label }: { sending: boolean; disabled: boolean; onClick: () => void; label: string }) {
|
|
return (
|
|
<ActionButton
|
|
label={label}
|
|
icon={PaperPlaneTilt}
|
|
tone="accent"
|
|
iconOnly
|
|
loading={sending}
|
|
disabled={disabled}
|
|
onClick={onClick}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export const ComposerShell = forwardRef<HTMLTextAreaElement, ComposerShellProps>(function ComposerShell(
|
|
{
|
|
value,
|
|
onChange,
|
|
onSend,
|
|
placeholder,
|
|
sending,
|
|
error = null,
|
|
shortcutHint,
|
|
sendLabel = "Send message",
|
|
sendDisabled,
|
|
leading,
|
|
belowHeader,
|
|
onKeyDownExtra,
|
|
textareaAriaLabel,
|
|
rows = 2,
|
|
},
|
|
ref,
|
|
) {
|
|
const handleChange = useCallback(
|
|
(event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
onChange(event.target.value, event);
|
|
},
|
|
[onChange],
|
|
);
|
|
|
|
const handleKeyDown = useCallback(
|
|
(event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
if (onKeyDownExtra && onKeyDownExtra(event)) return;
|
|
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
|
|
event.preventDefault();
|
|
onSend();
|
|
}
|
|
},
|
|
[onKeyDownExtra, onSend],
|
|
);
|
|
|
|
const disabled = sendDisabled ?? !value.trim();
|
|
|
|
return (
|
|
<>
|
|
{error ? <div className="mb-3"><InlineNotice message={error} /></div> : null}
|
|
{belowHeader}
|
|
<div className="relative flex items-end gap-2">
|
|
{leading}
|
|
<textarea
|
|
ref={ref}
|
|
value={value}
|
|
onChange={handleChange}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder={placeholder}
|
|
aria-label={textareaAriaLabel ?? placeholder}
|
|
rows={rows}
|
|
className="min-h-[40px] min-w-0 flex-1 resize-none rounded-lg border border-border bg-muted px-3 py-2 text-[13px] text-foreground placeholder:text-muted-foreground focus:border-transparent focus:outline-none focus:ring-2 focus:ring-ring"
|
|
/>
|
|
<SendButton sending={sending} disabled={disabled} onClick={onSend} label={sendLabel} />
|
|
</div>
|
|
{shortcutHint ? <div className="mt-1 text-[11px] text-muted-foreground">{shortcutHint}</div> : null}
|
|
</>
|
|
);
|
|
});
|