feat(sw): integrated improvement 0.2.0 (popup content, header open, tool buttons)
SW package 0.2.0 integrated improvement (Task A, steps 1-8). All new props are
optional; existing SW Stories are pixel-identical to the previous build (28/28).
- Build reproducibility: dist text normalized to LF, dist/build-info.json
(commit, dirty, tool versions, no timestamp), packages/sw/.gitattributes
(eol=lf). LF checkouts give identical dist and tarball.
- SWHeader: documentOpenPlacement ("menu" default | "header": Open right of
the logo in the existing document-button look, reusing onDocumentOpen and
documentOpenLabel, no duplicate Open in the dropdown), leading slot,
hiddenControls sub-items documentNew / documentOpen / layerFreeze /
layerLock. Open moves into More on measured overflow (with hysteresis).
- showNew / showOpen (SWHeaderDocumentTitle), showFreeze / showLock (layer
menu, field, property window list).
- SWToolButton (text / pill / document) and SWToolGroup (radio selection),
reusing the existing toggle, pill and file-name looks.
- SWPopup / SWPopupSection / SWPopupRow on the shared property window frame;
SWHeaderPropertyPanel gains children, title, autoFocus and optional
selection / properties / groups. Escape stack for several popups.
- Field dropdowns inside a popup use the shared fixed overlay
(useSWFixedDropdownPosition), so the scrolling body no longer clips them.
- AI guide, examples E/F, README, Storybook (SW/Header/Options, SW/Tool,
SW/Popup) and verification record docs/sw/sw-integrated-improvement-2026-10-19.md.
- Regression scripts rescoped for the intended changes (unused-removal 81/81,
overview 52/54 with the two pre-existing historical assertions).
Known limitation (unresolved): tools / leading content (incl. SWToolGroup)
does not move into More. With a wide tool bar the configured fields shrink
instead; in Example F the document name and layer field reach 0px at 390px.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5faa1f1ffe
commit
55179b4577
138 files changed
+17194
-162
No files matched your search
@@ -0,0 +1,117 @@
|
||||
import { createContext, forwardRef, useContext, type ButtonHTMLAttributes, type HTMLAttributes, type KeyboardEvent, type ReactNode } from "react"
|
||||
|
||||
/**
|
||||
* Existing SW button looks, reused as one public button (no new design values):
|
||||
* - `text`: the 속성Bar / 속성창 text toggle (700, text-only hover #FFC600, selected #FFC600).
|
||||
* - `pill`: the 40 x 38 print / K button of the dark pill (gradient hover ring, optional hover icon).
|
||||
* - `document`: the file-name surface (`hm-sw-header__document-button`, text-only hover #FFC600). Placed right
|
||||
* after `SWHeaderHome` it shares the slanted logo junction exactly like the file name.
|
||||
*/
|
||||
export type SWToolButtonAppearance = "text" | "pill" | "document"
|
||||
|
||||
export interface SWToolButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
|
||||
/** Default `text`. */
|
||||
appearance?: SWToolButtonAppearance
|
||||
/** Toggle state (aria-pressed, #FFC600 text). Inside `SWToolGroup selection="single"` it is the checked radio. */
|
||||
pressed?: boolean
|
||||
/** Button text. For `pill` it is the accessible name only (visually hidden: the pill shows its icon). */
|
||||
children?: ReactNode
|
||||
/** `pill` only: icon centered in the 40 x 38 button. */
|
||||
icon?: ReactNode
|
||||
/** `pill` only: icon shown instead of `icon` on hover (like print-hover.svg). */
|
||||
hoverIcon?: ReactNode
|
||||
}
|
||||
|
||||
interface GroupContext { selection: "none" | "single" }
|
||||
const SWToolGroupContext = createContext<GroupContext | null>(null)
|
||||
|
||||
export const SWToolButton = forwardRef<HTMLButtonElement, SWToolButtonProps>(function SWToolButton(
|
||||
{ appearance = "text", pressed, children, icon, hoverIcon, className, type = "button", ...rest }, ref,
|
||||
) {
|
||||
const group = useContext(SWToolGroupContext)
|
||||
const radio = group?.selection === "single"
|
||||
const state = pressed === undefined ? {} : radio
|
||||
? { role: "radio", "aria-checked": pressed, tabIndex: rest.tabIndex ?? (pressed ? 0 : -1) }
|
||||
: { "aria-pressed": pressed }
|
||||
const classes = (base: string) => `${base}${className ? ` ${className}` : ""}`
|
||||
if (appearance === "document") {
|
||||
// The wrapper is the Header flex item; [data-tool] keeps it at its content width (not the file-name clamp).
|
||||
return (
|
||||
<div className="hm-sw-header__document" data-tool="">
|
||||
<button ref={ref} type={type} className={classes("hm-sw-header__document-button")} data-selected={pressed ? "true" : undefined} {...state} {...rest}>
|
||||
<span className="hm-sw-header__document-name">{children}</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (appearance === "pill") {
|
||||
return (
|
||||
<button ref={ref} type={type} className={classes("hm-sw-header__action hm-sw-header__action--print hm-sw-tool__pill-button")} data-selected={pressed ? "true" : undefined} {...state} {...rest}>
|
||||
{icon !== undefined && <span className="hm-sw-header__icon hm-sw-tool__icon" aria-hidden="true">{icon}</span>}
|
||||
{hoverIcon !== undefined && <span className="hm-sw-header__icon-hover hm-sw-tool__icon" aria-hidden="true">{hoverIcon}</span>}
|
||||
{children !== undefined && <span className="hm-sw-tool__label">{children}</span>}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<button ref={ref} type={type} className={classes("hm-sw-header__toggle hm-sw-tool__text-button")} data-selected={pressed ? "true" : undefined} {...state} {...rest}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Existing SW group surfaces:
|
||||
* - `text`: the 상세보기 group (optional 12px caption above a 19px row; 1px dividers between buttons).
|
||||
* - `pill`: the dark rounded print / K pill (shrinks to its buttons).
|
||||
* `selection="single"` makes it a radio group (e.g. Model / Layout): give each button `pressed`, arrows move.
|
||||
*/
|
||||
export interface SWToolGroupProps extends HTMLAttributes<HTMLDivElement> {
|
||||
/** Default `text`. */
|
||||
appearance?: "text" | "pill"
|
||||
/** `text` only: the 12px caption above the row, e.g. `상세보기`. Also the group's accessible name. */
|
||||
label?: string
|
||||
/** `single`: one pressed button (radio group, arrow keys move and select by focus only). Default `none`. */
|
||||
selection?: "none" | "single"
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
function moveFocus(event: KeyboardEvent<HTMLDivElement>) {
|
||||
const keys = ["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp", "Home", "End"]
|
||||
if (!keys.includes(event.key)) return
|
||||
const items = Array.from(event.currentTarget.querySelectorAll<HTMLButtonElement>('button[role="radio"]:not(:disabled)'))
|
||||
const index = items.indexOf(document.activeElement as HTMLButtonElement)
|
||||
if (index < 0 || !items.length) return
|
||||
event.preventDefault()
|
||||
const next = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1
|
||||
: (index + (event.key === "ArrowRight" || event.key === "ArrowDown" ? 1 : -1) + items.length) % items.length
|
||||
items[next].focus()
|
||||
items[next].click()
|
||||
}
|
||||
|
||||
export const SWToolGroup = forwardRef<HTMLDivElement, SWToolGroupProps>(function SWToolGroup(
|
||||
{ appearance = "text", label, selection = "none", children, className, onKeyDown, ...rest }, ref,
|
||||
) {
|
||||
const role = selection === "single" ? "radiogroup" : "group"
|
||||
const keys = (event: KeyboardEvent<HTMLDivElement>) => { onKeyDown?.(event); if (!event.defaultPrevented && selection === "single") moveFocus(event) }
|
||||
const items = Array.isArray(children) ? children.filter(child => child !== null && child !== undefined && child !== false) : [children]
|
||||
const content = (
|
||||
<SWToolGroupContext.Provider value={{ selection }}>
|
||||
{appearance === "text" ? items.map((child, index) => (
|
||||
<span key={index} className="hm-sw-tool__item">
|
||||
{index > 0 && <span className="hm-sw-header__properties-divider" aria-hidden="true" />}
|
||||
{child}
|
||||
</span>
|
||||
)) : children}
|
||||
</SWToolGroupContext.Provider>
|
||||
)
|
||||
if (appearance === "pill") {
|
||||
return <div ref={ref} role={role} aria-label={rest["aria-label"] ?? label} className={`hm-sw-header__actions hm-sw-tool__pill${className ? ` ${className}` : ""}`} data-partial="" onKeyDown={keys} {...rest}>{content}</div>
|
||||
}
|
||||
return (
|
||||
<div ref={ref} role={role} aria-label={rest["aria-label"] ?? label} className={`hm-sw-header__properties hm-sw-tool__group${className ? ` ${className}` : ""}`} data-partial="" data-caption={label ? "" : undefined} onKeyDown={keys} {...rest}>
|
||||
{label && <span className="hm-sw-header__properties-caption" aria-hidden="true">{label}</span>}
|
||||
<div className="hm-sw-header__properties-row">{content}</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
Reference in new issue
Block a user