feat: integrate latest HMDesign design system updates
This commit is contained in:
1 parent
99faec189e
commit
1c4e0bb9a1
209 files changed
+8845
-552
No files matched your search
@@ -5,7 +5,7 @@ import { CheckIcon, MinusIcon } from "lucide-react"
|
||||
|
||||
// Radix Checkbox structure, data-state and ARIA are kept (ui-viz has no Checkbox spec; reference only); the look comes from
|
||||
// src/theme/checkbox.css. Indeterminate is `checked="indeterminate"` (Radix); the shadcn check icon is kept and a minus icon is shown for it.
|
||||
function Checkbox({ className, form, asChild, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
function Checkbox({ className, form, asChild, children, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
// Radix's reset listener uses the trigger's native form, while Root only passes `form` to its input.
|
||||
const useFormButton = !!form && !asChild
|
||||
const indicator = (
|
||||
@@ -26,7 +26,10 @@ function Checkbox({ className, form, asChild, ...props }: React.ComponentProps<t
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{useFormButton ? <button type="button" form={form}>{indicator}</button> : indicator}
|
||||
{asChild
|
||||
? React.cloneElement(React.Children.only(children) as React.ReactElement<{ children?: React.ReactNode }>, undefined,
|
||||
(children as React.ReactElement<{ children?: React.ReactNode }>).props.children, indicator)
|
||||
: useFormButton ? <button type="button" form={form}>{children}{indicator}</button> : <>{children}{indicator}</>}
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -63,6 +63,9 @@ function Combobox({
|
||||
const value = controlled ? valueProp : inner
|
||||
const selected = options.find((option) => option.value === value)
|
||||
const popupRef = useConnectedPopupRef()
|
||||
const generatedId = React.useId()
|
||||
const triggerId = props.id ?? `combobox-${generatedId}`
|
||||
const popupId = `combobox-popup-${generatedId}`
|
||||
|
||||
const select = (next: string) => {
|
||||
if (!controlled) setInner(next)
|
||||
@@ -76,7 +79,7 @@ function Combobox({
|
||||
<button
|
||||
type="button"
|
||||
role="combobox"
|
||||
aria-haspopup="listbox"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={open}
|
||||
data-slot="combobox-trigger"
|
||||
data-size={size}
|
||||
@@ -84,6 +87,8 @@ function Combobox({
|
||||
disabled={disabled}
|
||||
className={cn("flex w-full items-center justify-between text-left outline-none disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
id={triggerId}
|
||||
aria-controls={open ? popupId : undefined}
|
||||
>
|
||||
<span data-slot="combobox-value">{selected ? selected.label : placeholder}</span>
|
||||
<ChevronDownIcon data-slot="combobox-icon" aria-hidden="true" />
|
||||
@@ -91,6 +96,9 @@ function Combobox({
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
ref={popupRef}
|
||||
id={popupId}
|
||||
aria-label={props["aria-label"]}
|
||||
aria-labelledby={props["aria-labelledby"] ?? (props["aria-label"] ? undefined : triggerId)}
|
||||
data-combobox-popup=""
|
||||
data-size={size}
|
||||
align="start"
|
||||
@@ -102,9 +110,9 @@ function Combobox({
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder={searchPlaceholder} />
|
||||
<CommandList>
|
||||
<Command label={props["aria-label"] ?? placeholder}>
|
||||
<CommandInput aria-label={searchPlaceholder} placeholder={searchPlaceholder} />
|
||||
<CommandList label={props["aria-label"] ?? placeholder} aria-labelledby={props["aria-labelledby"]}>
|
||||
<CommandEmpty>{emptyText}</CommandEmpty>
|
||||
{options.map((option) => (
|
||||
<CommandItem
|
||||
|
||||
@@ -54,6 +54,9 @@ function DatePicker({
|
||||
const [inner, setInner] = React.useState<Date | undefined>(defaultValue)
|
||||
const controlled = controlledProp ?? (valueProp !== undefined)
|
||||
const value = controlled ? valueProp : inner
|
||||
const generatedId = React.useId()
|
||||
const triggerId = props.id ?? `date-picker-${generatedId}`
|
||||
const popupId = `date-picker-popup-${generatedId}`
|
||||
|
||||
const select = (date: Date | undefined) => {
|
||||
if (!controlled) setInner(date)
|
||||
@@ -72,12 +75,17 @@ function DatePicker({
|
||||
disabled={disabled}
|
||||
className={cn("flex w-full items-center justify-between text-left outline-none disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
id={triggerId}
|
||||
aria-controls={open ? popupId : undefined}
|
||||
>
|
||||
<span data-slot="date-picker-value">{value ? format(value) : placeholder}</span>
|
||||
<CalendarIcon data-slot="date-picker-icon" aria-hidden="true" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
id={popupId}
|
||||
aria-label={props["aria-label"]}
|
||||
aria-labelledby={props["aria-labelledby"] ?? (props["aria-label"] ? undefined : triggerId)}
|
||||
align="start"
|
||||
style={{ "--hm-popover-padding": "0px", "--hm-popover-width": "auto" } as React.CSSProperties}
|
||||
// Radix focuses the first tabbable (the "previous month" icon button). Land on the day the Calendar marks as its tab stop
|
||||
|
||||
@@ -73,7 +73,10 @@ function MenubarContent({
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-36 origin-(--radix-menubar-content-transform-origin) overflow-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
// Unmount the previous menu immediately on switching. Keeping its
|
||||
// dismissable layer alive for an exit animation closes the next menu
|
||||
// when that menu receives focus (Radix non-modal Menubar).
|
||||
"z-50 min-w-36 origin-(--radix-menubar-content-transform-origin) overflow-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -10,6 +10,8 @@ function Progress({
|
||||
className,
|
||||
value,
|
||||
max,
|
||||
asChild,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||
// Use the same valid range/default as Radix so the visual fill agrees with its ARIA value.
|
||||
@@ -17,19 +19,24 @@ function Progress({
|
||||
const percentage = typeof value === "number" && value >= 0 && value <= effectiveMax
|
||||
? (value / effectiveMax) * 100
|
||||
: 0
|
||||
const indicator = <ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="size-full flex-1 transition-all"
|
||||
style={{ transform: `translateX(${percentage - 100}%)` }}
|
||||
/>
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn("relative w-full overflow-hidden", className)}
|
||||
value={value}
|
||||
max={max}
|
||||
asChild={asChild}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="size-full flex-1 transition-all"
|
||||
style={{ transform: `translateX(${percentage - 100}%)` }}
|
||||
/>
|
||||
{asChild
|
||||
? React.cloneElement(React.Children.only(children) as React.ReactElement<{ children?: React.ReactNode }>, undefined,
|
||||
(children as React.ReactElement<{ children?: React.ReactNode }>).props.children, indicator)
|
||||
: <>{children}{indicator}</>}
|
||||
</ProgressPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ function ToggleGroup({
|
||||
size,
|
||||
spacing = 0,
|
||||
children,
|
||||
style,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
@@ -38,7 +39,7 @@ function ToggleGroup({
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
data-spacing={spacing}
|
||||
style={{ "--hm-toggle-group-gap": `${spacing * 4}px` } as React.CSSProperties}
|
||||
style={{ "--hm-toggle-group-gap": `${spacing * 4}px`, ...style } as React.CSSProperties}
|
||||
className={cn("group/toggle-group flex w-fit flex-row items-center data-vertical:flex-col data-vertical:items-stretch", className)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -258,7 +258,7 @@ export {
|
||||
export type { DataTableColumnDef, DataTableInstance, DataTableOptions, DataTableProps } from "./patterns/data-table"
|
||||
|
||||
export { FilterBar, FilterBarActions, FilterBarSearch } from "./patterns/filter-bar"
|
||||
export type { FilterBarSearchProps } from "./patterns/filter-bar/filter-bar"
|
||||
export type { FilterBarProps, FilterBarSearchProps } from "./patterns/filter-bar/filter-bar"
|
||||
|
||||
export {
|
||||
countDashboardFilter,
|
||||
|
||||
@@ -189,6 +189,7 @@ function DataTablePagination<TData extends RowData>({
|
||||
className?: string
|
||||
}) {
|
||||
const { pageIndex, pageSize } = table.state.pagination
|
||||
const pageSizeLabelId = React.useId()
|
||||
const pageCount = Math.max(table.getPageCount(), 1)
|
||||
const page = pageIndex + 1
|
||||
const selected = table.getFilteredSelectedRowModel().rows.length
|
||||
@@ -203,9 +204,9 @@ function DataTablePagination<TData extends RowData>({
|
||||
{selected > 0 ? `${selected}개 선택됨 · 전체 ${total}개` : `전체 ${total}개`}
|
||||
</p>
|
||||
<div data-slot="data-table-page-size">
|
||||
<span id="data-table-page-size-label">페이지당</span>
|
||||
<span id={pageSizeLabelId}>페이지당</span>
|
||||
<Select value={String(pageSize)} onValueChange={(value) => table.setPageSize(Number(value))}>
|
||||
<SelectTrigger size="medium" aria-labelledby="data-table-page-size-label">
|
||||
<SelectTrigger size="medium" aria-labelledby={pageSizeLabelId}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -14,8 +14,12 @@ import { Input, type InputSize } from "../../components/ui/input"
|
||||
// <Select>…</Select> <Combobox … /> <DatePicker … />
|
||||
// <FilterBarActions><Button onClick={reset}>초기화</Button></FilterBarActions>
|
||||
// </FilterBar>
|
||||
function FilterBar({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return <div data-slot="filter-bar" className={cn("flex flex-wrap items-center", className)} {...props} />
|
||||
type FilterBarProps = React.ComponentProps<"div"> & {
|
||||
/** Opt-in publisher list composition. Omitted preserves the general layout. */
|
||||
layout?: "default" | "list-basic" | "list-extended"
|
||||
}
|
||||
function FilterBar({ className, layout = "default", ...props }: FilterBarProps) {
|
||||
return <div data-slot="filter-bar" data-layout={layout === "default" ? undefined : layout} className={cn("flex flex-wrap items-center", className)} {...props} />
|
||||
}
|
||||
|
||||
type FilterBarSearchProps = Omit<React.ComponentProps<"input">, "size" | "value" | "defaultValue" | "onChange" | "type"> & {
|
||||
@@ -32,22 +36,30 @@ function FilterBarSearch({
|
||||
size = "medium",
|
||||
placeholder = "검색",
|
||||
"aria-label": ariaLabel,
|
||||
readOnly,
|
||||
disabled,
|
||||
ref,
|
||||
...props
|
||||
}: FilterBarSearchProps) {
|
||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||
React.useImperativeHandle(ref, () => inputRef.current!, [])
|
||||
return (
|
||||
<div data-slot="filter-bar-search" className={cn("relative", className)}>
|
||||
<SearchIcon aria-hidden="true" data-slot="filter-bar-search-icon" className="pointer-events-none absolute" />
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="search"
|
||||
size={size}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
aria-label={ariaLabel ?? placeholder}
|
||||
readOnly={readOnly}
|
||||
disabled={disabled}
|
||||
onChange={(event) => onValueChange(event.target.value)}
|
||||
{...props}
|
||||
/>
|
||||
{value ? (
|
||||
<button type="button" data-slot="filter-bar-search-clear" aria-label="검색어 지우기" className="absolute" onClick={() => onValueChange("")}>
|
||||
{value && !readOnly && !disabled ? (
|
||||
<button type="button" data-slot="filter-bar-search-clear" aria-label="검색어 지우기" className="absolute" onClick={() => { inputRef.current?.focus(); onValueChange("") }}>
|
||||
<XIcon aria-hidden="true" />
|
||||
</button>
|
||||
) : null}
|
||||
@@ -61,4 +73,4 @@ function FilterBarActions({ className, ...props }: React.ComponentProps<"div">)
|
||||
}
|
||||
|
||||
export { FilterBar, FilterBarActions, FilterBarSearch }
|
||||
export type { FilterBarSearchProps }
|
||||
export type { FilterBarProps, FilterBarSearchProps }
|
||||
@@ -115,14 +115,14 @@ type GNBUserProps = Omit<React.ComponentProps<"button">, "children"> & {
|
||||
|
||||
// Trigger button of the user menu: name / role text on the desktop bar; the Avatar (children) shows on the mobile bar only, where the
|
||||
// text is hidden (ui-viz's profile is text-only, so the desktop bar has no image). Because of that the
|
||||
// accessible name defaults to "<name> 사용자 메뉴". Use it as `<DropdownMenuTrigger asChild>`; the data-slot comes after the
|
||||
// accessible name includes the visible name and description, followed by "사용자 메뉴". Use it as `<DropdownMenuTrigger asChild>`; the data-slot comes after the
|
||||
// spread so the trigger's own slot does not replace it (same rule as Button).
|
||||
function GNBUser({ className, name, description, children, "aria-label": ariaLabel, ...props }: GNBUserProps) {
|
||||
return (
|
||||
<button type="button" aria-label={ariaLabel ?? `${name} 사용자 메뉴`} className={cn("flex items-center", className)} {...props} data-slot="gnb-user">
|
||||
<button type="button" aria-label={ariaLabel ?? `${name}${description ? ` ${description}` : ""} 사용자 메뉴`} className={cn("flex items-center", className)} {...props} data-slot="gnb-user">
|
||||
{children}
|
||||
<span data-slot="gnb-user-text">
|
||||
<span data-slot="gnb-user-name">{name}</span>
|
||||
<span data-slot="gnb-user-name">{name}{description ? " " : null}</span>
|
||||
{description ? <span data-slot="gnb-user-description">{description}</span> : null}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -99,10 +99,10 @@ type LNBProfileProps = Omit<React.ComponentProps<"button">, "children"> & {
|
||||
// it composes directly with `DropdownMenuTrigger asChild`; Radix owns Enter / Space, arrow navigation, Escape and focus return.
|
||||
function LNBProfile({ className, name, description, children, "aria-label": ariaLabel, ...props }: LNBProfileProps) {
|
||||
return (
|
||||
<button type="button" aria-label={ariaLabel ?? `${name} 사용자 메뉴`} className={cn(className)} {...props} data-slot="lnb-profile">
|
||||
<button type="button" aria-label={ariaLabel ?? `${name}${description ? ` ${description}` : ""} 사용자 메뉴`} className={cn(className)} {...props} data-slot="lnb-profile">
|
||||
{children}
|
||||
<div data-slot="lnb-profile-text">
|
||||
<strong>{name}</strong>
|
||||
<strong>{name}{description ? " " : null}</strong>
|
||||
{description ? <p>{description}</p> : null}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
[data-slot="accordion-trigger"]:hover:not(:disabled) {
|
||||
background: transparent;
|
||||
background: color-mix(in srgb, var(--hm-sem-palette-black) 5%, transparent);
|
||||
}
|
||||
[data-slot="accordion-trigger"]:disabled {
|
||||
opacity: var(--hm-accordion-disabled-opacity);
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
--hm-button-solid-orange-border: var(--hm-sem-palette-orange-default);
|
||||
--hm-button-solid-gray-background: var(--hm-sem-palette-gray);
|
||||
--hm-button-solid-gray-border: var(--hm-sem-palette-gray-dark);
|
||||
--hm-button-solid-red-background: var(--hm-sem-action-destructive);
|
||||
--hm-button-solid-red-border: var(--hm-sem-action-destructive);
|
||||
|
||||
/* ui_viz: docs/components/button.md §28 Standard Outline (ink) and D-087 Red Outline Hover/Pressed — Confirmed */
|
||||
--hm-button-outline-green-ink: var(--hm-sem-action-primary);
|
||||
@@ -79,9 +81,11 @@
|
||||
[data-slot="button"][data-variant="cancel"] {
|
||||
--hm-button-solid-background: var(--hm-button-solid-gray-background);
|
||||
--hm-button-solid-border: var(--hm-button-solid-gray-border);
|
||||
--hm-button-outline-ink: var(--hm-sem-action-cancel);
|
||||
--hm-button-outline-ink: var(--hm-button-cancel-runtime-ink, var(--hm-sem-palette-gray));
|
||||
}
|
||||
[data-slot="button"][data-variant="destructive"] {
|
||||
--hm-button-solid-background: var(--hm-button-solid-red-background);
|
||||
--hm-button-solid-border: var(--hm-button-solid-red-border);
|
||||
--hm-button-outline-ink: var(--hm-button-outline-red-ink);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
|
||||
--hm-combobox-background: var(--hm-sem-bg-surface);
|
||||
--hm-combobox-text: var(--hm-sem-fg-primary);
|
||||
--hm-combobox-placeholder: var(--hm-sem-fg-placeholder);
|
||||
/* Readable empty-value guidance on the white trigger; preserve shared tokens. */
|
||||
--hm-combobox-placeholder: var(--hm-sem-fg-secondary);
|
||||
--hm-combobox-border-color: var(--hm-sem-stroke-input-default);
|
||||
--hm-combobox-focus-border-color: var(--hm-sem-stroke-input-focus);
|
||||
--hm-combobox-seam-color: var(--hm-sem-stroke-input-default);
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
[data-slot="dashboard-layout"] [data-slot="filter-bar"] {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
[data-slot="dashboard-layout"] [data-slot="filter-bar"][data-layout] { flex-wrap: wrap; }
|
||||
[data-slot="dashboard-layout"] [data-slot="filter-bar-search"] {
|
||||
flex: 0 0 var(--hm-filter-bar-search-width);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
|
||||
--hm-date-picker-background: var(--hm-sem-bg-surface);
|
||||
--hm-date-picker-text: var(--hm-sem-fg-primary);
|
||||
--hm-date-picker-placeholder: var(--hm-sem-fg-placeholder);
|
||||
/* Readable empty-value guidance on the white trigger; preserve shared tokens. */
|
||||
--hm-date-picker-placeholder: var(--hm-sem-fg-secondary);
|
||||
--hm-date-picker-border-color: var(--hm-sem-stroke-input-default);
|
||||
--hm-date-picker-focus-border-color: var(--hm-sem-stroke-input-focus);
|
||||
--hm-date-picker-icon-size: 18px;
|
||||
|
||||
@@ -101,9 +101,15 @@
|
||||
padding: 0 var(--hm-drawer-padding);
|
||||
}
|
||||
|
||||
/* Footer: actions stack full width (touch targets on a narrow screen), the primary action first as in shadcn. */
|
||||
/* DOM order: Cancel → Execute. Mobile reverses the visual stack. */
|
||||
[data-slot="drawer-footer"] {
|
||||
flex: none;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
gap: var(--hm-drawer-footer-gap);
|
||||
padding: 16px var(--hm-drawer-padding) var(--hm-drawer-padding);
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
[data-slot="drawer-footer"] { flex-direction: column-reverse; align-items: stretch; }
|
||||
[data-slot="drawer-footer"] > * { width: 100%; }
|
||||
}
|
||||
@@ -76,6 +76,12 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Explicit publisher list composition; default consumers keep the original layout. */
|
||||
[data-slot="filter-bar"][data-layout="list-basic"] { justify-content: center; }
|
||||
[data-slot="filter-bar"][data-layout="list-extended"] { justify-content: flex-start; }
|
||||
[data-slot="filter-bar"][data-layout] { flex-wrap: wrap; }
|
||||
[data-slot="filter-bar"][data-layout] > [data-slot="filter-bar-actions"] { margin-left: 0; }
|
||||
|
||||
/* Narrow screen: search takes a full row, the controls share rows, the actions take their own row. */
|
||||
@media (max-width: 640px) {
|
||||
[data-slot="filter-bar-search"] {
|
||||
|
||||
@@ -547,12 +547,34 @@
|
||||
grid-template-columns: var(--hm-lnb-width) minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: var(--hm-sem-palette-green-light-2);
|
||||
background: var(--hm-sem-bg-content);
|
||||
border-radius: var(--hm-gnb-join) var(--hm-gnb-join) 0 0;
|
||||
}
|
||||
[data-hm-gnb-layout="sidebar"] > [data-hm-gnb-body] > [data-slot="lnb"][data-hm-gnb-sidebar] {
|
||||
--hm-lnb-background: var(--hm-sem-palette-green-light-2);
|
||||
--hm-gnb-content-menu-color: var(--hm-sem-action-primary);
|
||||
--hm-lnb-background: var(--hm-sem-bg-content);
|
||||
--hm-lnb-background-end: var(--hm-sem-bg-content);
|
||||
--hm-lnb-text: var(--hm-gnb-content-menu-color);
|
||||
--hm-lnb-text-secondary: var(--hm-gnb-content-menu-color);
|
||||
--hm-lnb-item-text-stroke-width: 0px;
|
||||
--hm-lnb-item-text-shadow: none;
|
||||
--hm-lnb-hover-background: color-mix(in srgb, var(--hm-gnb-content-menu-color) 8%, transparent);
|
||||
--hm-lnb-current-background: color-mix(in srgb, var(--hm-gnb-content-menu-color) 20%, transparent);
|
||||
--hm-lnb-current-text: var(--hm-gnb-content-menu-color);
|
||||
--hm-lnb-current-depth: none;
|
||||
--hm-lnb-group-current-text: var(--hm-gnb-content-menu-color);
|
||||
--hm-lnb-group-open-background: transparent;
|
||||
--hm-lnb-group-open-line: 0;
|
||||
--hm-lnb-focus-outline: 2px solid var(--hm-gnb-content-menu-color);
|
||||
border-right: 1px solid var(--hm-sem-stroke-default);
|
||||
}
|
||||
[data-hm-gnb-layout="sidebar"][data-hm-color="brown"] > [data-hm-gnb-body] > [data-hm-gnb-sidebar] {
|
||||
--hm-gnb-content-menu-color: var(--hm-sem-palette-brown-default);
|
||||
}
|
||||
/* Content navigation uses one straight divider, without the green LNB join. */
|
||||
[data-hm-gnb-layout="sidebar"] [data-hm-gnb-sidebar]::after,
|
||||
[data-hm-gnb-layout="sidebar"] [data-hm-gnb-sidebar] [data-slot="lnb-item"][aria-current="page"]::before,
|
||||
[data-hm-gnb-layout="sidebar"] [data-hm-gnb-sidebar] [data-slot="lnb-item"][aria-current="page"]::after { content: none; }
|
||||
[data-hm-gnb-layout="sidebar"] > [data-hm-gnb-body] > [data-hm-gnb-main] {
|
||||
position: relative;
|
||||
grid-column: 2;
|
||||
@@ -560,7 +582,7 @@
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
background: var(--hm-sem-bg-content);
|
||||
border-radius: var(--hm-lnb-join) var(--hm-gnb-join) 0 var(--hm-lnb-join);
|
||||
border-radius: 0 var(--hm-gnb-join) 0 0;
|
||||
}
|
||||
@media (max-width: 1023px) {
|
||||
[data-hm-gnb-layout="sidebar"] > [data-hm-gnb-body] {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* LNB pattern (layout visual reference: D:\컴포넌트\ui-viz Patterns/LNB, implementation: HM Collapsible + plain elements).
|
||||
A 272px green column: horizontal 5-stop surface, brand on top, menu in the middle, text-only profile at the bottom. The current
|
||||
item is a Beige face (left corners rounded) that runs to the content edge and joins it through two concave corners; hover is
|
||||
Black 10% to the same edge; an open group is a Black 20% face with a 1px green line. Spacing values are pattern-local. */
|
||||
Black 10% to the same edge; an open group is a Black 8% face with a 1px green line. Spacing values are pattern-local. */
|
||||
:root, [data-hm-theme] {
|
||||
/* Exposed for the layout: grid column width and the surface that continues behind the content's rounded corners. */
|
||||
--hm-lnb-width: 272px;
|
||||
@@ -52,7 +52,7 @@
|
||||
linear-gradient(to bottom, color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent), transparent 3px) 0 0 / 75% 100% no-repeat,
|
||||
linear-gradient(to top, color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent), transparent 3px) 0 0 / 75% 100% no-repeat;
|
||||
|
||||
--hm-lnb-group-open-background: color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent);
|
||||
--hm-lnb-group-open-background: color-mix(in srgb, var(--hm-sem-palette-black) 8%, transparent);
|
||||
--hm-lnb-group-open-line: 1px solid var(--hm-sem-palette-green-light-2);
|
||||
--hm-lnb-group-radius: 8px 0 0 8px;
|
||||
--hm-lnb-group-current-text: var(--hm-sem-palette-yellow-accent);
|
||||
|
||||
@@ -15,5 +15,9 @@
|
||||
background: var(--hm-progress-track-background);
|
||||
}
|
||||
[data-slot="progress-indicator"] {
|
||||
/* Consumer children in an asChild root must not displace the filled bar. */
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: var(--hm-progress-radius);
|
||||
background: var(--hm-progress-indicator-background);
|
||||
}
|
||||
@@ -42,8 +42,13 @@ export function isRuntimeTheme(theme: unknown): theme is import("./contract").Ru
|
||||
/** Atomic validation. Invalid documents release the previous override to host/default. */
|
||||
export function applyRuntimeTheme(root: HTMLElement, theme: unknown): boolean {
|
||||
if (!isRuntimeTheme(theme)) { resetRuntimeTheme(root); return false }
|
||||
const values = Object.entries(theme.tokens).map(([key, value]) =>
|
||||
const values: (readonly [string, string])[] = Object.entries(theme.tokens).map(([key, value]) =>
|
||||
[runtimeThemeContract[key as RuntimeThemeToken].property, value!] as const)
|
||||
// Reviewed Gray default comes from the existing palette. Retain custom cancel
|
||||
// overrides without changing the public token or the full Default preset.
|
||||
const cancel = theme.tokens["color.action.cancel"]
|
||||
if (cancel && cancel.toLowerCase() !== runtimeThemeContract["color.action.cancel"].defaultValue)
|
||||
values.push(["--hm-button-cancel-runtime-ink", cancel])
|
||||
resetRuntimeTheme(root)
|
||||
const scope = root.getAttribute("data-hm-theme")
|
||||
const properties = values.map(([property]) => ({
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
meaning only: no --hm-dialog-* token is referenced. */
|
||||
:root, [data-hm-theme] {
|
||||
--hm-sheet-background: var(--hm-sem-bg-surface);
|
||||
/* Existing control token reused provisionally; no new approved radius value. */
|
||||
--hm-sheet-radius: var(--hm-radius-control-md);
|
||||
--hm-sheet-text: var(--hm-sem-fg-primary);
|
||||
--hm-sheet-border-width: 1px;
|
||||
--hm-sheet-border-color: var(--hm-sem-stroke-input-default);
|
||||
@@ -53,6 +55,10 @@
|
||||
[data-slot="sheet-content"][data-side="left"] { border-right: var(--hm-sheet-border-width) solid var(--hm-sheet-border-color); }
|
||||
[data-slot="sheet-content"][data-side="top"] { border-bottom: var(--hm-sheet-border-width) solid var(--hm-sheet-border-color); }
|
||||
[data-slot="sheet-content"][data-side="bottom"] { border-top: var(--hm-sheet-border-width) solid var(--hm-sheet-border-color); }
|
||||
[data-slot="sheet-content"][data-side="right"] { border-radius: var(--hm-sheet-radius) 0 0 var(--hm-sheet-radius); }
|
||||
[data-slot="sheet-content"][data-side="left"] { border-radius: 0 var(--hm-sheet-radius) var(--hm-sheet-radius) 0; }
|
||||
[data-slot="sheet-content"][data-side="top"] { border-radius: 0 0 var(--hm-sheet-radius) var(--hm-sheet-radius); }
|
||||
[data-slot="sheet-content"][data-side="bottom"] { border-radius: var(--hm-sheet-radius) var(--hm-sheet-radius) 0 0; }
|
||||
|
||||
[data-slot="sheet-header"] {
|
||||
flex: none;
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
--hm-table-header-border-top: var(--hm-sem-palette-green-light-2);
|
||||
--hm-table-header-border-bottom: color-mix(in srgb, var(--hm-sem-palette-green-light-2) 20%, transparent);
|
||||
|
||||
/* Rows are a wide soft surface: hover, selected and selected + hover all use Accent 45% (not the Active State Semantic). */
|
||||
--hm-table-row-hover-background: var(--hm-sem-surface-highlight);
|
||||
--hm-table-row-selected-background: var(--hm-sem-surface-highlight);
|
||||
/* Publisher review: table-only Accent 20%; other highlight mappings stay unchanged. */
|
||||
--hm-table-row-hover-background: color-mix(in srgb, var(--hm-sem-accent) 20%, transparent);
|
||||
--hm-table-row-selected-background: var(--hm-table-row-hover-background);
|
||||
|
||||
--hm-table-footer-background: var(--hm-sem-bg-subtle);
|
||||
--hm-table-footer-font-weight: 500;
|
||||
|
||||
@@ -147,8 +147,8 @@
|
||||
}
|
||||
[data-slot="tabs-frame"][data-fill="true"][data-panel-surface="content"] {
|
||||
--hm-tab-fill-panel: var(--hm-sem-bg-content);
|
||||
/* Non-white panel: the Selected face runs from White (top) to the panel color (bottom); the panel itself gets no gradient. */
|
||||
--hm-tab-fill-face: linear-gradient(to bottom, var(--hm-sem-palette-white) 0%, var(--hm-sem-bg-content) 100%);
|
||||
/* D-02 (2026-10-09): only Primary's selected face starts at White alpha .8; the content stop, direction and geometry stay unchanged. */
|
||||
--hm-tab-fill-face: linear-gradient(to bottom, color-mix(in srgb, var(--hm-sem-palette-white) 80%, transparent) 0%, var(--hm-sem-bg-content) 100%);
|
||||
}
|
||||
[data-slot="tabs-frame"][data-fill="true"] > [data-slot="tabs-list"] {
|
||||
flex: 1 1 auto;
|
||||
|
||||
Reference in new issue
Block a user