feat(sw): add SW Footer component and Storybook stories
This commit is contained in:
1 parent
77a809baa3
commit
6cf885412e
105 files changed
+3288
-3
No files matched your search
@@ -0,0 +1,64 @@
|
||||
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>
|
||||
)
|
||||
})
|
||||
Reference in new issue
Block a user