65 lines
3.8 KiB
TypeScript
65 lines
3.8 KiB
TypeScript
import { forwardRef, useEffect, useId, useRef, useState, type HTMLAttributes, type InputHTMLAttributes, type ReactNode } from "react"
|
|
import "../../theme/sw-footer.css"
|
|
|
|
export interface SWFooterCommandLineProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSubmit"> {
|
|
/** Output lines, oldest first. Collapsed shows the last 3 (Figma 26:10048), expanded scrolls them all (26:10056). */
|
|
history: ReactNode[]
|
|
/** Input text (controlled). Omit for an uncontrolled input. */
|
|
value?: string
|
|
onValueChange?: (value: string) => void
|
|
/** Enter in the input. The host runs the command and appends to `history`; an uncontrolled input is cleared. */
|
|
onSubmit?: (command: string) => void
|
|
/** Prompt label before the input. Default `Command:`. */
|
|
prompt?: string
|
|
/** Expanded (270px) or collapsed (71px). Controlled with `onExpandedChange`. */
|
|
expanded?: boolean
|
|
defaultExpanded?: boolean
|
|
onExpandedChange?: (expanded: boolean) => void
|
|
/** Native input attributes (placeholder, autoFocus, …). */
|
|
inputProps?: Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "onChange">
|
|
/** Accessible names. */
|
|
historyLabel?: string
|
|
expandLabel?: string
|
|
collapseLabel?: string
|
|
}
|
|
|
|
/**
|
|
* Command line window (Figma command 26:10048 collapsed / 26:10056 expanded, ico_arrow 26:10023): 566px, 12px text
|
|
* with 10% tracking, history above a 23px input well. The host places it (usually above the footer, left).
|
|
*/
|
|
export const SWFooterCommandLine = forwardRef<HTMLDivElement, SWFooterCommandLineProps>(function SWFooterCommandLine({
|
|
history, value, onValueChange, onSubmit, prompt = "Command:", expanded, defaultExpanded = false, onExpandedChange,
|
|
inputProps, historyLabel = "명령 기록", expandLabel = "명령 기록 펼치기", collapseLabel = "명령 기록 접기", className, ...rest
|
|
}, ref) {
|
|
const [ownExpanded, setOwnExpanded] = useState(defaultExpanded)
|
|
const [ownValue, setOwnValue] = useState("")
|
|
const open = expanded ?? ownExpanded
|
|
const text = value ?? ownValue
|
|
const inputId = useId()
|
|
const historyRef = useRef<HTMLOListElement>(null)
|
|
// Newest line in view (collapsed: the last 3 lines; expanded: scrolled to the end).
|
|
useEffect(() => { const list = historyRef.current; if (list) list.scrollTop = list.scrollHeight }, [history.length, open])
|
|
const toggle = () => { const next = !open; if (expanded === undefined) setOwnExpanded(next); onExpandedChange?.(next) }
|
|
return (
|
|
<div ref={ref} className={`hm-sw-theme hm-sw-footer-command${className ? ` ${className}` : ""}`} data-expanded={open ? "" : undefined} {...rest}>
|
|
<ol ref={historyRef} className="hm-sw-footer-command__history" role="log" aria-label={historyLabel} aria-live="polite">
|
|
{/* Collapsed: the last 3 lines (Figma 26:10048); expanded: every line, scrolled to the newest. */}
|
|
{(open ? history : history.slice(-3)).map((line, index) => <li key={open ? index : history.length - 3 + index}>{line}</li>)}
|
|
</ol>
|
|
<button type="button" className="hm-sw-footer-command__toggle" aria-expanded={open} aria-label={open ? collapseLabel : expandLabel} title={open ? collapseLabel : expandLabel} onClick={toggle} />
|
|
<div className="hm-sw-footer-command__prompt">
|
|
<label className="hm-sw-footer-command__label" htmlFor={inputId}>{prompt}</label>
|
|
<input id={inputId} className="hm-sw-footer-command__input" autoComplete="off" spellCheck={false} {...inputProps} value={text}
|
|
onChange={event => { if (value === undefined) setOwnValue(event.target.value); onValueChange?.(event.target.value) }}
|
|
onKeyDown={event => {
|
|
inputProps?.onKeyDown?.(event)
|
|
if (event.defaultPrevented || event.key !== "Enter" || event.nativeEvent.isComposing) return
|
|
event.preventDefault()
|
|
onSubmit?.(text)
|
|
if (value === undefined) setOwnValue("")
|
|
}} />
|
|
</div>
|
|
</div>
|
|
)
|
|
})
|