Initial commit: HMDesignSystem baseline

This commit is contained in:
shkim4 committed 2026-10-08 16:58:09 +09:00
commit b6a0a61103
486 files changed
+68432

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
/* Accordion mapping. ui-viz lists Accordion only as a component name (no spec), so these are current HM values built from the HM
Semantic tokens. Items are separated by a 1px divider; no outer box. Radix attributes drive the states (data-state, data-disabled);
the open / close animation and the chevron rotation stay the shadcn ones. */
:root, [data-hm-theme] {
--hm-accordion-text: var(--hm-sem-fg-primary);
--hm-accordion-content-text: var(--hm-sem-fg-primary);
--hm-accordion-divider-width: 1px;
--hm-accordion-divider-color: var(--hm-sem-stroke-input-default);
--hm-accordion-trigger-min-height: 48px;
--hm-accordion-trigger-padding-y: 12px;
--hm-accordion-trigger-gap: 16px;
--hm-accordion-trigger-font-size: 16px;
--hm-accordion-trigger-line-height: 24px;
--hm-accordion-trigger-font-weight: 500;
--hm-accordion-icon-size: 20px;
--hm-accordion-icon-color: var(--hm-sem-fg-secondary);
--hm-accordion-content-font-size: 14px;
--hm-accordion-content-line-height: 20px;
--hm-accordion-content-padding-bottom: 16px;
/* Current HM value for this component only; not a global Disabled rule. */
--hm-accordion-disabled-opacity: 0.6;
}
[data-slot="accordion-item"] {
border-bottom: var(--hm-accordion-divider-width) solid var(--hm-accordion-divider-color);
}
[data-slot="accordion-item"]:last-child {
border-bottom: 0;
}
[data-slot="accordion-trigger"] {
box-sizing: border-box;
min-height: var(--hm-accordion-trigger-min-height);
padding: var(--hm-accordion-trigger-padding-y) 0;
gap: var(--hm-accordion-trigger-gap);
align-items: center;
color: var(--hm-accordion-text);
font-size: var(--hm-accordion-trigger-font-size);
line-height: var(--hm-accordion-trigger-line-height);
font-weight: var(--hm-accordion-trigger-font-weight);
overflow-wrap: anywhere;
cursor: pointer;
}
[data-slot="accordion-trigger"]:hover:not(:disabled) {
background: transparent;
}
[data-slot="accordion-trigger"]:disabled {
opacity: var(--hm-accordion-disabled-opacity);
cursor: not-allowed;
}
[data-slot="accordion-trigger-icon"] {
width: var(--hm-accordion-icon-size);
height: var(--hm-accordion-icon-size);
color: var(--hm-accordion-icon-color);
}
[data-slot="accordion-content-inner"] {
padding-bottom: var(--hm-accordion-content-padding-bottom);
color: var(--hm-accordion-content-text);
font-size: var(--hm-accordion-content-font-size);
line-height: var(--hm-accordion-content-line-height);
font-weight: 400;
overflow-wrap: anywhere;
}
+60
View File
@@ -0,0 +1,60 @@
/* Alert Dialog mapping. ui-viz has no Alert Dialog spec (its Dialog doc lists it as a follow-up), so these are current HM values built
from the HM Semantic tokens. A compact confirmation: Header (Title + Description) and Actions, no Close (X), no Body region.
Radix AlertDialog keeps Portal, focus trap, initial focus on Cancel, Escape, no outside dismiss and focus return. Action and
Cancel are the HM Button (composed in the TSX), so no button style is set here. The Overlay keeps the shadcn default.
Values repeat Dialog's look in meaning only: no --hm-dialog-* token is referenced. */
:root, [data-hm-theme] {
--hm-alert-dialog-background: var(--hm-sem-bg-surface);
--hm-alert-dialog-text: var(--hm-sem-fg-primary);
--hm-alert-dialog-border-width: 1px;
--hm-alert-dialog-border-color: var(--hm-sem-stroke-input-default);
--hm-alert-dialog-radius: 8px;
--hm-alert-dialog-shadow: 0 0 12px color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent);
--hm-alert-dialog-width: 400px;
--hm-alert-dialog-edge: 16px;
--hm-alert-dialog-padding: 24px;
--hm-alert-dialog-header-gap: 8px;
--hm-alert-dialog-actions-gap: 8px;
--hm-alert-dialog-actions-margin-top: 24px;
--hm-alert-dialog-title-font-size: 18px;
--hm-alert-dialog-title-line-height: 28px;
--hm-alert-dialog-title-font-weight: 700;
--hm-alert-dialog-description-font-size: 14px;
--hm-alert-dialog-description-line-height: 20px;
--hm-alert-dialog-description-color: var(--hm-sem-fg-secondary);
}
[data-slot="alert-dialog-content"] {
box-sizing: border-box;
width: min(var(--hm-alert-dialog-width), calc(100vw - 2 * var(--hm-alert-dialog-edge)));
max-height: calc(100dvh - 2 * var(--hm-alert-dialog-edge));
overflow: auto;
padding: var(--hm-alert-dialog-padding);
border: var(--hm-alert-dialog-border-width) solid var(--hm-alert-dialog-border-color);
border-radius: var(--hm-alert-dialog-radius);
background: var(--hm-alert-dialog-background);
color: var(--hm-alert-dialog-text);
box-shadow: var(--hm-alert-dialog-shadow);
overflow-wrap: anywhere;
}
[data-slot="alert-dialog-header"] {
gap: var(--hm-alert-dialog-header-gap);
}
[data-slot="alert-dialog-title"] {
font-size: var(--hm-alert-dialog-title-font-size);
line-height: var(--hm-alert-dialog-title-line-height);
font-weight: var(--hm-alert-dialog-title-font-weight);
color: var(--hm-alert-dialog-text);
}
[data-slot="alert-dialog-description"] {
font-size: var(--hm-alert-dialog-description-font-size);
line-height: var(--hm-alert-dialog-description-line-height);
font-weight: 400;
color: var(--hm-alert-dialog-description-color);
}
[data-slot="alert-dialog-footer"] {
gap: var(--hm-alert-dialog-actions-gap);
margin-top: var(--hm-alert-dialog-actions-margin-top);
}
+63
View File
@@ -0,0 +1,63 @@
/* Alert mapping — current HM values from the shared Semantic tokens. ui-viz has no Alert spec (and its success / warning / information
status colors are undecided), so only a neutral default and a destructive variant exist. Variants use data-variant. */
:root, [data-hm-theme] {
--hm-alert-radius: var(--hm-radius-control-md);
--hm-alert-padding-x: 16px;
--hm-alert-padding-y: 12px;
--hm-alert-border-width: 1px;
--hm-alert-icon-size: 16px;
--hm-alert-title-font-size: 14px;
--hm-alert-title-line-height: 20px;
--hm-alert-title-font-weight: 500;
--hm-alert-description-font-size: 14px;
--hm-alert-description-line-height: 20px;
--hm-alert-description-font-weight: 400;
--hm-alert-default-background: var(--hm-sem-bg-surface);
--hm-alert-default-border-color: var(--hm-sem-stroke-default);
--hm-alert-default-title-color: var(--hm-sem-fg-primary);
--hm-alert-default-description-color: var(--hm-sem-fg-secondary);
--hm-alert-destructive-background: var(--hm-sem-bg-surface);
--hm-alert-destructive-border-color: var(--hm-sem-action-destructive);
--hm-alert-destructive-title-color: var(--hm-sem-action-destructive);
--hm-alert-destructive-description-color: var(--hm-sem-fg-secondary);
}
[data-slot="alert"] {
box-sizing: border-box;
padding: var(--hm-alert-padding-y) var(--hm-alert-padding-x);
border: var(--hm-alert-border-width) solid var(--hm-alert-default-border-color);
border-radius: var(--hm-alert-radius);
background: var(--hm-alert-default-background);
font-size: var(--hm-alert-description-font-size);
line-height: var(--hm-alert-description-line-height);
}
[data-slot="alert"][data-variant="default"] {
color: var(--hm-alert-default-title-color);
}
[data-slot="alert"][data-variant="destructive"] {
border-color: var(--hm-alert-destructive-border-color);
background: var(--hm-alert-destructive-background);
color: var(--hm-alert-destructive-title-color);
}
[data-slot="alert-title"] {
font-size: var(--hm-alert-title-font-size);
line-height: var(--hm-alert-title-line-height);
font-weight: var(--hm-alert-title-font-weight);
}
[data-slot="alert-description"] {
font-size: var(--hm-alert-description-font-size);
line-height: var(--hm-alert-description-line-height);
font-weight: var(--hm-alert-description-font-weight);
}
[data-slot="alert"][data-variant="default"] [data-slot="alert-description"] {
color: var(--hm-alert-default-description-color);
}
[data-slot="alert"][data-variant="destructive"] [data-slot="alert-description"] {
color: var(--hm-alert-destructive-description-color);
}
[data-slot="alert"] > svg {
width: var(--hm-alert-icon-size);
height: var(--hm-alert-icon-size);
}
+41
View File
@@ -0,0 +1,41 @@
/* Avatar mapping. ui-viz lists Avatar only as a component name (no spec), so these are current HM values. Sizes follow the shadcn
API (sm | default | lg). The fallback uses the green tint face (the shared accent). */
:root, [data-hm-theme] {
--hm-avatar-size-sm: 24px;
--hm-avatar-size-md: 32px;
--hm-avatar-size-lg: 40px;
--hm-avatar-radius: 9999px;
--hm-avatar-fallback-background: var(--accent);
--hm-avatar-fallback-text: var(--accent-foreground);
--hm-avatar-fallback-font-weight: 500;
--hm-avatar-fallback-font-size-sm: 10px;
--hm-avatar-fallback-font-size-md: 12px;
--hm-avatar-fallback-font-size-lg: 14px;
}
[data-slot="avatar"] {
--hm-avatar-size: var(--hm-avatar-size-md);
--hm-avatar-fallback-font-size: var(--hm-avatar-fallback-font-size-md);
width: var(--hm-avatar-size);
height: var(--hm-avatar-size);
border-radius: var(--hm-avatar-radius);
}
[data-slot="avatar"][data-size="sm"] {
--hm-avatar-size: var(--hm-avatar-size-sm);
--hm-avatar-fallback-font-size: var(--hm-avatar-fallback-font-size-sm);
}
[data-slot="avatar"][data-size="lg"] {
--hm-avatar-size: var(--hm-avatar-size-lg);
--hm-avatar-fallback-font-size: var(--hm-avatar-fallback-font-size-lg);
}
[data-slot="avatar-fallback"] {
border-radius: var(--hm-avatar-radius);
background: var(--hm-avatar-fallback-background);
color: var(--hm-avatar-fallback-text);
font-size: var(--hm-avatar-fallback-font-size);
font-weight: var(--hm-avatar-fallback-font-weight);
line-height: 1;
overflow: hidden;
white-space: nowrap;
}
+58
View File
@@ -0,0 +1,58 @@
/* Badge mapping — current HM values from the shared Semantic tokens (ui-viz has no Badge spec; it was on hold there, so it is reference only).
Variants use data-variant. Only roles that exist in the HM Theme: primary, neutral, destructive, outline. */
:root, [data-hm-theme] {
--hm-badge-radius: var(--hm-radius-control-md);
--hm-badge-padding-x: 8px;
--hm-badge-padding-y: 2px;
--hm-badge-border-width: 1px;
--hm-badge-font-size: 12px;
--hm-badge-line-height: 18px;
--hm-badge-font-weight: 500;
/* Default is the Strong Brand / Primary color (Primary Green), not the Active State Semantic. */
--hm-badge-default-background: var(--hm-sem-action-primary);
--hm-badge-default-text: var(--hm-sem-on-action-primary);
--hm-badge-default-border-color: var(--hm-sem-action-primary);
--hm-badge-secondary-background: var(--hm-sem-bg-subtle);
--hm-badge-secondary-text: var(--hm-sem-fg-secondary);
--hm-badge-secondary-border-color: var(--hm-sem-bg-subtle);
--hm-badge-destructive-background: var(--hm-sem-action-destructive);
--hm-badge-destructive-text: var(--hm-sem-on-action-primary);
--hm-badge-destructive-border-color: var(--hm-sem-action-destructive);
--hm-badge-outline-background: var(--hm-sem-bg-surface);
--hm-badge-outline-text: var(--hm-sem-fg-primary);
--hm-badge-outline-border-color: var(--hm-sem-stroke-default);
}
[data-slot="badge"] {
box-sizing: border-box;
padding: var(--hm-badge-padding-y) var(--hm-badge-padding-x);
border: var(--hm-badge-border-width) solid transparent;
border-radius: var(--hm-badge-radius);
font-size: var(--hm-badge-font-size);
line-height: var(--hm-badge-line-height);
font-weight: var(--hm-badge-font-weight);
}
[data-slot="badge"][data-variant="default"] {
background: var(--hm-badge-default-background);
color: var(--hm-badge-default-text);
border-color: var(--hm-badge-default-border-color);
}
[data-slot="badge"][data-variant="secondary"] {
background: var(--hm-badge-secondary-background);
color: var(--hm-badge-secondary-text);
border-color: var(--hm-badge-secondary-border-color);
}
[data-slot="badge"][data-variant="destructive"] {
background: var(--hm-badge-destructive-background);
color: var(--hm-badge-destructive-text);
border-color: var(--hm-badge-destructive-border-color);
}
[data-slot="badge"][data-variant="outline"] {
background: var(--hm-badge-outline-background);
color: var(--hm-badge-outline-text);
border-color: var(--hm-badge-outline-border-color);
}
+63
View File
@@ -0,0 +1,63 @@
/* Breadcrumb mapping. ui-viz lists Breadcrumb only as a component name (no spec), so these are current HM values built from the HM
Semantic tokens. Parent links are secondary text that turns primary on hover; the current page is a primary-colored label. */
:root, [data-hm-theme] {
--hm-breadcrumb-font-size: 14px;
--hm-breadcrumb-line-height: 20px;
--hm-breadcrumb-font-weight: 400;
--hm-breadcrumb-gap: 8px;
--hm-breadcrumb-link-color: var(--hm-sem-fg-secondary);
--hm-breadcrumb-link-hover-color: var(--hm-sem-fg-primary);
--hm-breadcrumb-page-color: var(--hm-sem-fg-primary);
--hm-breadcrumb-page-font-weight: 700;
--hm-breadcrumb-separator-color: var(--hm-sem-fg-secondary);
--hm-breadcrumb-separator-size: 16px;
--hm-breadcrumb-ellipsis-size: 20px;
--hm-breadcrumb-focus-radius: 2px;
}
[data-slot="breadcrumb-list"] {
gap: var(--hm-breadcrumb-gap);
color: var(--hm-breadcrumb-link-color);
font-size: var(--hm-breadcrumb-font-size);
line-height: var(--hm-breadcrumb-line-height);
font-weight: var(--hm-breadcrumb-font-weight);
}
[data-slot="breadcrumb-item"] {
gap: var(--hm-breadcrumb-gap);
}
[data-slot="breadcrumb-link"] {
color: var(--hm-breadcrumb-link-color);
border-radius: var(--hm-breadcrumb-focus-radius);
text-decoration: none;
outline: none;
}
[data-slot="breadcrumb-link"]:hover {
color: var(--hm-breadcrumb-link-hover-color);
text-decoration: underline;
text-underline-offset: 4px;
}
[data-slot="breadcrumb-link"]:focus-visible {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
[data-slot="breadcrumb-page"] {
color: var(--hm-breadcrumb-page-color);
font-weight: var(--hm-breadcrumb-page-font-weight);
cursor: default;
}
[data-slot="breadcrumb-separator"] {
color: var(--hm-breadcrumb-separator-color);
}
[data-slot="breadcrumb-separator"] > svg {
width: var(--hm-breadcrumb-separator-size);
height: var(--hm-breadcrumb-separator-size);
}
[data-slot="breadcrumb-ellipsis"] {
width: var(--hm-breadcrumb-ellipsis-size);
height: var(--hm-breadcrumb-ellipsis-size);
color: var(--hm-breadcrumb-separator-color);
}
[data-slot="breadcrumb-ellipsis"] > svg {
width: var(--hm-breadcrumb-separator-size);
height: var(--hm-breadcrumb-separator-size);
}
+192
View File
@@ -0,0 +1,192 @@
/* Button mapping: shared HM theme values are the defaults; each mapping can be overridden alone.
variant = ui_viz role, appearance = ui_viz treatment. Only [data-appearance] buttons (the HM roles) are styled here;
ghost and link carry no data-appearance and keep the shadcn defaults. */
:root, [data-hm-theme] {
/* ui_viz: docs/components/button.md §1.1 D-087 — Confirmed */
--hm-button-radius: var(--hm-radius-button);
--hm-button-font-family: var(--hm-font-family-base);
--hm-button-weight-brand: var(--hm-font-weight-bold);
--hm-button-weight-standard: var(--hm-font-weight-medium);
--hm-button-text: var(--hm-sem-on-action-primary);
--hm-button-disabled-opacity: 0.6;
/* ui_viz: docs/components/button.md §26 / §1.1 D-087 — Confirmed (size, padding-x, label size / line-height) */
--hm-button-height-sm: 28px;
--hm-button-height-md: var(--hm-control-height-md);
--hm-button-height-lg: 42px;
--hm-button-padding-x-sm: 10px;
--hm-button-padding-x-md: 14px;
--hm-button-padding-x-lg: 18px;
--hm-button-font-size-sm: 12px;
--hm-button-font-size-md: var(--hm-font-size-button-md);
--hm-button-font-size-lg: 15px;
--hm-button-line-height-sm: 18px;
--hm-button-line-height-md: 20px;
--hm-button-line-height-lg: 22px;
/* ui_viz: docs/components/button.md §1.1 D-087 Brand gradient (180deg, bg stops 0/30/65/100, border stops 0/50/100) — Confirmed */
--hm-button-brand-green-background: var(--hm-sem-surface-action-primary);
--hm-button-brand-green-border: var(--hm-sem-border-action-primary);
--hm-button-brand-brown-background: linear-gradient(180deg, var(--hm-sem-palette-brown-light-2) 0%, var(--hm-sem-palette-brown-light-1) 30%, var(--hm-sem-palette-brown-dark-1) 65%, var(--hm-sem-palette-brown-deep) 100%);
--hm-button-brand-brown-border: linear-gradient(180deg, var(--hm-sem-palette-brown-dark-1) 0%, var(--hm-sem-palette-brown-highlight) 50%, var(--hm-sem-palette-brown-deep) 100%);
--hm-button-brand-orange-background: linear-gradient(180deg, var(--hm-sem-palette-orange-light) 0%, var(--hm-sem-palette-orange-mid) 30%, var(--hm-sem-palette-orange-dark) 65%, var(--hm-sem-palette-orange-deep) 100%);
--hm-button-brand-orange-border: linear-gradient(180deg, var(--hm-sem-palette-orange-border) 0%, var(--hm-sem-palette-orange-light-4) 50%, var(--hm-sem-palette-orange-border-deep) 100%);
/* ui_viz: docs/components/button.md §1.1 D-087 Brand bevel (Green, Brown only) — Confirmed */
--hm-button-brand-bevel: inset 0 1px 1px var(--hm-color-action-shadow-light), inset 0 -1px 2px var(--hm-color-action-shadow-dark);
/* ui_viz: docs/components/button.md §28 / §1.1 D-086, D-087 Standard Solid (face / border) — Confirmed */
--hm-button-solid-green-background: var(--hm-sem-palette-green-light-1);
--hm-button-solid-green-border: var(--hm-sem-palette-green-dark-1);
--hm-button-solid-brown-background: var(--hm-sem-palette-brown-light-1);
--hm-button-solid-brown-border: var(--hm-sem-palette-brown-dark-1);
--hm-button-solid-orange-background: var(--hm-sem-palette-orange-default);
--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);
/* 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);
--hm-button-outline-brown-ink: var(--hm-sem-palette-brown-default);
--hm-button-outline-red-ink: var(--hm-sem-action-destructive);
--hm-button-outline-red-surface: var(--hm-color-action-destructive-hover);
--hm-button-outline-surface: var(--hm-sem-bg-surface);
/* ui_viz: docs/components/button.md §12 Hover and Pressed are the same Black 20% state surface — Confirmed */
--hm-button-state-overlay: var(--hm-color-action-overlay-hover);
}
/* Roles: pick the colors of each treatment. */
[data-slot="button"][data-variant="primary"] {
--hm-button-brand-background: var(--hm-button-brand-green-background);
--hm-button-brand-border: var(--hm-button-brand-green-border);
--hm-button-solid-background: var(--hm-button-solid-green-background);
--hm-button-solid-border: var(--hm-button-solid-green-border);
--hm-button-outline-ink: var(--hm-button-outline-green-ink);
}
[data-slot="button"][data-variant="secondary"] {
--hm-button-brand-background: var(--hm-button-brand-brown-background);
--hm-button-brand-border: var(--hm-button-brand-brown-border);
--hm-button-solid-background: var(--hm-button-solid-brown-background);
--hm-button-solid-border: var(--hm-button-solid-brown-border);
--hm-button-outline-ink: var(--hm-button-outline-brown-ink);
}
[data-slot="button"][data-variant="modify"] {
--hm-button-brand-background: var(--hm-button-brand-orange-background);
--hm-button-brand-border: var(--hm-button-brand-orange-border);
--hm-button-solid-background: var(--hm-button-solid-orange-background);
--hm-button-solid-border: var(--hm-button-solid-orange-border);
}
[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);
}
[data-slot="button"][data-variant="destructive"] {
--hm-button-outline-ink: var(--hm-button-outline-red-ink);
}
/* Shared. The last two shadows keep the shadcn focus and invalid ring visible on top of the bevel and state surface. */
[data-slot="button"][data-appearance] {
--hm-button-border-width: 1px;
--hm-button-bevel: 0 0 #0000;
--hm-button-state-surface: 0 0 #0000;
border: var(--hm-button-border-width) solid transparent;
border-radius: var(--hm-button-radius);
font-family: var(--hm-button-font-family);
font-weight: var(--hm-button-weight-standard);
box-shadow: var(--hm-button-state-surface), var(--hm-button-bevel), var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000);
}
/* Size: min-height with vertical padding; explicit wrapping labels can still grow. */
[data-slot="button"][data-appearance]:is([data-size="default"], [data-size="sm"], [data-size="lg"]) {
--hm-button-height: var(--hm-button-height-md);
--hm-button-padding-x: var(--hm-button-padding-x-md);
--hm-button-font-size: var(--hm-button-font-size-md);
--hm-button-line-height: var(--hm-button-line-height-md);
height: auto;
min-height: var(--hm-button-height);
max-width: 100%;
padding: calc((var(--hm-button-height) - var(--hm-button-line-height)) / 2 - var(--hm-button-border-width)) var(--hm-button-padding-x);
font-size: var(--hm-button-font-size);
line-height: var(--hm-button-line-height);
text-align: center;
word-break: keep-all;
overflow-wrap: anywhere;
}
/* Keep the existing shadcn nowrap default. Normal labels retain their intrinsic
width when a parent is narrower; consumers can opt into the existing long-label
behavior with className="whitespace-normal", without a new prop.
Utilities remain able to override these layout defaults. */
@layer components {
[data-slot="button"][data-appearance]:is([data-size="default"], [data-size="sm"], [data-size="lg"]) {
min-width: max-content;
}
[data-slot="button"][data-appearance].whitespace-normal:is([data-size="default"], [data-size="sm"], [data-size="lg"]) {
min-width: 0;
}
}
[data-slot="button"][data-appearance][data-size="sm"] {
--hm-button-height: var(--hm-button-height-sm);
--hm-button-padding-x: var(--hm-button-padding-x-sm);
--hm-button-font-size: var(--hm-button-font-size-sm);
--hm-button-line-height: var(--hm-button-line-height-sm);
}
[data-slot="button"][data-appearance][data-size="lg"] {
--hm-button-height: var(--hm-button-height-lg);
--hm-button-padding-x: var(--hm-button-padding-x-lg);
--hm-button-font-size: var(--hm-button-font-size-lg);
--hm-button-line-height: var(--hm-button-line-height-lg);
}
/* Brand: two-layer gradient (face on padding-box, border on border-box), white label, weight 700. */
[data-slot="button"][data-appearance="brand"] {
--hm-button-border-width: 1.5px;
border-color: transparent;
background: var(--hm-button-brand-background) padding-box, var(--hm-button-brand-border) border-box;
color: var(--hm-button-text);
font-weight: var(--hm-button-weight-brand);
}
[data-slot="button"][data-appearance="brand"]:is([data-variant="primary"], [data-variant="secondary"]) {
--hm-button-bevel: var(--hm-button-brand-bevel);
}
/* Solid: flat face, 1px border, white label. */
[data-slot="button"][data-appearance="solid"] {
border-color: var(--hm-button-solid-border);
background: var(--hm-button-solid-background);
color: var(--hm-button-text);
}
/* Brand and Solid: Hover and Pressed are the same Black 20% over the face. */
[data-slot="button"]:is([data-appearance="brand"], [data-appearance="solid"]):is(:hover, :active):not(:disabled) {
--hm-button-state-surface: inset 0 0 0 999px var(--hm-button-state-overlay);
}
/* Outline: white face, 1px role ink. Hover fills with the role ink; Pressed is white with Black 20%. */
[data-slot="button"][data-appearance="outline"] {
border-color: var(--hm-button-outline-ink);
background: var(--hm-button-outline-surface);
color: var(--hm-button-outline-ink);
}
[data-slot="button"][data-appearance="outline"]:hover:not(:disabled) {
background: var(--hm-button-outline-ink);
color: var(--hm-button-text);
}
[data-slot="button"][data-appearance="outline"]:active:not(:disabled) {
background: var(--hm-button-outline-surface);
color: var(--hm-button-outline-ink);
--hm-button-state-surface: inset 0 0 0 999px var(--hm-button-state-overlay);
}
/* Red Outline exception: Hover and Pressed are a Red 8% face, no Black overlay. */
[data-slot="button"][data-appearance="outline"][data-variant="destructive"]:is(:hover, :active):not(:disabled) {
background: var(--hm-button-outline-red-surface);
color: var(--hm-button-outline-ink);
--hm-button-state-surface: 0 0 #0000;
}
/* Disabled keeps the Default paint, dims the whole button, and shows the forbidden cursor. */
[data-slot="button"][data-appearance]:disabled {
opacity: var(--hm-button-disabled-opacity);
cursor: not-allowed;
}
+208
View File
@@ -0,0 +1,208 @@
/* Calendar mapping. ui-viz has no Calendar spec (inventory lists Date Picker by name only), so these are current HM values built
from the HM Semantic / Primitive tokens. react-day-picker keeps its default class names (rdp-*) and puts the state attributes
on the day cell (data-selected, data-today, data-outside, data-disabled, data-focused); the range states are its classes
(rdp-range_start / _middle / _end). Everything is scoped under [data-slot="calendar"]. The Calendar draws no surface of its own
(the container, e.g. Popover or Card, owns it). Nothing here references Button / Input / Popover tokens. */
:root, [data-hm-theme] {
--hm-calendar-padding: 12px;
--hm-calendar-text: var(--hm-sem-fg-primary);
--hm-calendar-month-gap: 16px;
--hm-calendar-caption-height: 32px;
--hm-calendar-caption-font-size: 14px;
--hm-calendar-caption-line-height: 20px;
--hm-calendar-caption-font-weight: 700;
--hm-calendar-nav-size: 28px;
--hm-calendar-nav-radius: var(--hm-radius-control-md);
--hm-calendar-nav-icon-size: 16px;
--hm-calendar-nav-hover-background: var(--hm-sem-surface-highlight);
--hm-calendar-weekday-color: var(--hm-sem-fg-secondary);
--hm-calendar-weekday-font-size: 12px;
--hm-calendar-weekday-line-height: 18px;
--hm-calendar-weekday-font-weight: 500;
--hm-calendar-day-size: 36px;
--hm-calendar-day-radius: var(--hm-radius-control-md);
--hm-calendar-day-font-size: 14px;
--hm-calendar-day-line-height: 20px;
--hm-calendar-day-hover-background: var(--hm-sem-surface-highlight);
--hm-calendar-today-background: var(--hm-sem-bg-subtle);
--hm-calendar-today-font-weight: 700;
/* Single selected and range start / end use the Active Semantic fill; the range middle keeps the soft selection surface
(--accent 45%, text --accent-foreground). */
--hm-calendar-selected-background: var(--hm-sem-state-active);
--hm-calendar-selected-text: var(--hm-sem-on-state-active);
--hm-calendar-range-edge-background: var(--hm-sem-state-active);
--hm-calendar-range-edge-text: var(--hm-sem-on-state-active);
--hm-calendar-range-middle-background: var(--hm-sem-surface-highlight);
--hm-calendar-range-middle-text: var(--accent-foreground);
--hm-calendar-outside-color: var(--hm-sem-fg-secondary);
--hm-calendar-outside-opacity: 0.6;
/* Current HM value for this component only; not a global Disabled rule. */
--hm-calendar-disabled-opacity: 0.4;
}
[data-slot="calendar"] {
position: relative;
width: fit-content;
padding: var(--hm-calendar-padding);
color: var(--hm-calendar-text);
}
[data-slot="calendar"] .rdp-months {
display: flex;
flex-wrap: wrap;
gap: var(--hm-calendar-month-gap);
}
[data-slot="calendar"] .rdp-month {
display: flex;
flex-direction: column;
gap: 8px;
}
/* Caption: centered label; the nav buttons sit on both sides of the first caption row. */
[data-slot="calendar"] .rdp-month_caption {
display: flex;
align-items: center;
justify-content: center;
height: var(--hm-calendar-caption-height);
font-size: var(--hm-calendar-caption-font-size);
line-height: var(--hm-calendar-caption-line-height);
font-weight: var(--hm-calendar-caption-font-weight);
}
[data-slot="calendar"] .rdp-nav {
position: absolute;
top: var(--hm-calendar-padding);
left: var(--hm-calendar-padding);
right: var(--hm-calendar-padding);
display: flex;
align-items: center;
justify-content: space-between;
height: var(--hm-calendar-caption-height);
pointer-events: none;
}
[data-slot="calendar"] .rdp-button_previous,
[data-slot="calendar"] .rdp-button_next {
pointer-events: auto;
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--hm-calendar-nav-size);
height: var(--hm-calendar-nav-size);
padding: 0;
border: 0;
border-radius: var(--hm-calendar-nav-radius);
background: transparent;
color: var(--hm-calendar-text);
cursor: pointer;
outline: none;
}
[data-slot="calendar"] .rdp-button_previous:hover:not(:disabled),
[data-slot="calendar"] .rdp-button_next:hover:not(:disabled) {
background: var(--hm-calendar-nav-hover-background);
}
[data-slot="calendar"] .rdp-button_previous:focus-visible,
[data-slot="calendar"] .rdp-button_next:focus-visible {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
[data-slot="calendar"] .rdp-button_previous:disabled,
[data-slot="calendar"] .rdp-button_next:disabled {
opacity: var(--hm-calendar-disabled-opacity);
cursor: not-allowed;
}
[data-slot="calendar"] .rdp-button_previous svg,
[data-slot="calendar"] .rdp-button_next svg {
width: var(--hm-calendar-nav-icon-size);
height: var(--hm-calendar-nav-icon-size);
}
/* Grid */
[data-slot="calendar"] .rdp-month_grid {
border-collapse: collapse;
}
[data-slot="calendar"] .rdp-weekday {
width: var(--hm-calendar-day-size);
padding: 4px 0;
color: var(--hm-calendar-weekday-color);
font-size: var(--hm-calendar-weekday-font-size);
line-height: var(--hm-calendar-weekday-line-height);
font-weight: var(--hm-calendar-weekday-font-weight);
text-align: center;
}
[data-slot="calendar"] .rdp-day {
width: var(--hm-calendar-day-size);
height: var(--hm-calendar-day-size);
padding: 0;
text-align: center;
}
[data-slot="calendar"] .rdp-day_button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 0;
border: 0;
border-radius: var(--hm-calendar-day-radius);
background: transparent;
color: inherit;
font: inherit;
font-size: var(--hm-calendar-day-font-size);
line-height: var(--hm-calendar-day-line-height);
cursor: pointer;
outline: none;
}
[data-slot="calendar"] .rdp-day_button:hover:not(:disabled) {
background: var(--hm-calendar-day-hover-background);
}
[data-slot="calendar"] .rdp-day_button:focus-visible {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
position: relative;
z-index: 1;
}
/* Today / outside / disabled / selected, from the day cell attributes. */
[data-slot="calendar"] .rdp-day[data-today] .rdp-day_button {
background: var(--hm-calendar-today-background);
font-weight: var(--hm-calendar-today-font-weight);
}
[data-slot="calendar"] .rdp-day[data-outside] .rdp-day_button {
color: var(--hm-calendar-outside-color);
opacity: var(--hm-calendar-outside-opacity);
}
[data-slot="calendar"] .rdp-day[data-disabled] .rdp-day_button {
opacity: var(--hm-calendar-disabled-opacity);
cursor: not-allowed;
}
[data-slot="calendar"] .rdp-day[data-selected] .rdp-day_button,
[data-slot="calendar"] .rdp-day[data-selected] .rdp-day_button:hover:not(:disabled) {
background: var(--hm-calendar-selected-background);
color: var(--hm-calendar-selected-text);
opacity: 1;
}
/* Range: start and end keep the Active fill (inner corners square); the middle uses the soft surface with square corners. */
[data-slot="calendar"] .rdp-day.rdp-range_middle[data-selected] .rdp-day_button,
[data-slot="calendar"] .rdp-day.rdp-range_middle[data-selected] .rdp-day_button:hover:not(:disabled) {
background: var(--hm-calendar-range-middle-background);
color: var(--hm-calendar-range-middle-text);
border-radius: 0;
}
[data-slot="calendar"] .rdp-day.rdp-range_start[data-selected] .rdp-day_button,
[data-slot="calendar"] .rdp-day.rdp-range_start[data-selected] .rdp-day_button:hover:not(:disabled),
[data-slot="calendar"] .rdp-day.rdp-range_end[data-selected] .rdp-day_button,
[data-slot="calendar"] .rdp-day.rdp-range_end[data-selected] .rdp-day_button:hover:not(:disabled) {
background: var(--hm-calendar-range-edge-background);
color: var(--hm-calendar-range-edge-text);
}
[data-slot="calendar"] .rdp-range_start .rdp-day_button {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
[data-slot="calendar"] .rdp-range_end .rdp-day_button {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
+61
View File
@@ -0,0 +1,61 @@
/* Card mapping: ui_viz Content Card (Type B). HM spec: docs/components/card.md.
Source refs below (content-container.md §7, §10 and content-header.md §7, §9) are external ui_viz docs/patterns/ files, not in this repo.
ui_viz defines no Hover / Focus / Selected / Disabled, no shadow and no footer: those stay shadcn defaults or are absent. */
:root, [data-hm-theme] {
/* ui_viz: content-container.md §7.2 — container-radius 4px = radius.container.default — Confirmed. Not aliased to Input or Button. */
--hm-card-radius: var(--hm-radius-container);
/* ui_viz: content-container.md §7.2 — border 1px, body-padding 24px all sides — SOURCE_ADOPTED (Canonical pattern, Q6) */
--hm-card-border-width: 1px;
--hm-card-padding: 24px;
/* ui_viz: content-container.md §7.2, §10.2 — border colors were LOCAL_EXCEPTION component values (#DEDBD2 / #ECD4A2).
HM maps them to shared Semantic tokens: color.stroke.input.default and color.palette.brown-light-4 (see docs/components/card.md §6.1). */
--hm-card-border-color: var(--hm-sem-stroke-input-default);
--hm-card-border-color-on-content: var(--hm-sem-palette-brown-light-4);
/* ui_viz: content-container.md §7.2 — surface: color.bg.surface (White) or color.bg.content (Beige) — Confirmed */
--hm-card-surface: var(--hm-sem-bg-surface);
--hm-card-surface-content: var(--hm-sem-bg-content);
--hm-card-text: var(--hm-sem-fg-primary);
/* ui_viz: content-header.md §7.1, §9 — content profile: title <-> meta gap 20px, title heading-md 20/28/600 (CANONICAL_OVERRIDE of Source 19px) */
--hm-card-header-gap: 20px;
--hm-card-title-font-size: 20px;
--hm-card-title-line-height: 28px;
--hm-card-title-font-weight: 600;
--hm-card-title-color: var(--hm-sem-fg-primary);
}
/* Default size. size="sm" is a shadcn size that ui_viz does not define, so it keeps the shadcn spacing (12px) from the TSX classes. */
:where([data-slot="card"]:not([data-size="sm"])) {
--card-spacing: var(--hm-card-padding);
}
[data-slot="card"] {
border: var(--hm-card-border-width) solid var(--hm-card-border-color);
border-radius: var(--hm-card-radius);
background: var(--hm-card-surface);
color: var(--hm-card-text);
box-shadow: none;
}
[data-slot="card"][data-surface="content"] {
border-color: var(--hm-card-border-color-on-content);
background: var(--hm-card-surface-content);
}
/* Header: title group at the start, Action (Meta) at the end, centered on the cross axis. */
[data-slot="card-header"] {
column-gap: var(--hm-card-header-gap);
}
[data-slot="card-action"] {
align-self: center;
}
:where([data-slot="card"]:not([data-size="sm"])) [data-slot="card-title"] {
font-size: var(--hm-card-title-font-size);
line-height: var(--hm-card-title-line-height);
font-weight: var(--hm-card-title-font-weight);
color: var(--hm-card-title-color);
}
/* Footer is not defined by ui_viz (shadcn default). Its bottom corners follow the container radius inside the border. */
[data-slot="card-footer"] {
border-bottom-left-radius: calc(var(--hm-card-radius) - var(--hm-card-border-width));
border-bottom-right-radius: calc(var(--hm-card-radius) - var(--hm-card-border-width));
}
+88
View File
@@ -0,0 +1,88 @@
/* Carousel mapping. ui-viz has no Carousel spec (it is not in the earlier inventory), so these are current HM values built from the HM
Semantic / Primitive tokens. Embla owns the scrolling; here only the slide gap and the previous / next controls are styled.
The controls are Carousel's own buttons (they do not use the Button component or its tokens) and sit inside the carousel edge
(8px inset) so they are never clipped by a narrow parent. The slide width is the consumer's (`basis-*` on CarouselItem).
Everything is in `@layer components`, so a consumer's utility classes (gap, control position, size) still win.
Nothing here references Button / Scroll Area tokens. */
:root, [data-hm-theme] {
--hm-carousel-gap: 16px;
--hm-carousel-control-size: 32px;
--hm-carousel-control-inset: 8px;
--hm-carousel-control-icon-size: 16px;
--hm-carousel-control-background: var(--hm-sem-bg-surface);
--hm-carousel-control-text: var(--hm-sem-fg-primary);
--hm-carousel-control-border-width: 1px;
--hm-carousel-control-border-color: var(--hm-sem-stroke-input-default);
--hm-carousel-control-hover-background: var(--accent);
/* Current HM value for this component only; not a global Disabled rule. */
--hm-carousel-disabled-opacity: 0.6;
}
@layer components {
/* Gap: the track pulls back by one gap and every slide pads it (the shadcn approach), so slides keep equal widths. */
[data-slot="carousel"][data-orientation="horizontal"] [data-slot="carousel-track"] {
margin-left: calc(-1 * var(--hm-carousel-gap));
}
[data-slot="carousel"][data-orientation="horizontal"] [data-slot="carousel-item"] {
padding-left: var(--hm-carousel-gap);
}
[data-slot="carousel"][data-orientation="vertical"] [data-slot="carousel-track"] {
margin-top: calc(-1 * var(--hm-carousel-gap));
}
[data-slot="carousel"][data-orientation="vertical"] [data-slot="carousel-item"] {
padding-top: var(--hm-carousel-gap);
}
[data-slot="carousel-content"] {
cursor: grab;
}
[data-slot="carousel-content"]:active {
cursor: grabbing;
}
[data-slot="carousel-previous"],
[data-slot="carousel-next"] {
box-sizing: border-box;
width: var(--hm-carousel-control-size);
height: var(--hm-carousel-control-size);
padding: 0;
border: var(--hm-carousel-control-border-width) solid var(--hm-carousel-control-border-color);
border-radius: 9999px;
background: var(--hm-carousel-control-background);
color: var(--hm-carousel-control-text);
cursor: pointer;
}
[data-slot="carousel-previous"] svg,
[data-slot="carousel-next"] svg {
width: var(--hm-carousel-control-icon-size);
height: var(--hm-carousel-control-icon-size);
}
[data-slot="carousel-previous"]:hover:not(:disabled),
[data-slot="carousel-next"]:hover:not(:disabled) {
background: var(--hm-carousel-control-hover-background);
}
[data-slot="carousel-previous"]:focus-visible,
[data-slot="carousel-next"]:focus-visible {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
[data-slot="carousel-previous"]:disabled,
[data-slot="carousel-next"]:disabled {
opacity: var(--hm-carousel-disabled-opacity);
cursor: not-allowed;
}
/* Position: inside the edge, centered on the cross axis (the vertical carousel turns the arrows 90 degrees). */
[data-slot="carousel"][data-orientation="horizontal"] [data-slot="carousel-previous"] {
left: var(--hm-carousel-control-inset);
}
[data-slot="carousel"][data-orientation="horizontal"] [data-slot="carousel-next"] {
right: var(--hm-carousel-control-inset);
}
[data-slot="carousel"][data-orientation="vertical"] [data-slot="carousel-previous"] {
top: var(--hm-carousel-control-inset);
}
[data-slot="carousel"][data-orientation="vertical"] [data-slot="carousel-next"] {
bottom: var(--hm-carousel-control-inset);
}
}
+54
View File
@@ -0,0 +1,54 @@
/* Checkbox mapping: current HM values built from the shared Semantic tokens (ui-viz has no Checkbox spec; reference only).
States use Radix attributes: [data-state="checked"|"indeterminate"], :hover, :focus-visible, :disabled, [aria-invalid="true"]. */
:root, [data-hm-theme] {
--hm-checkbox-size: 18px;
--hm-checkbox-radius: var(--hm-radius-control-md);
--hm-checkbox-border-width: 1px;
--hm-checkbox-icon-size: 14px;
--hm-checkbox-background: var(--hm-sem-bg-surface);
--hm-checkbox-border-color: var(--hm-sem-stroke-default);
--hm-checkbox-hover-border-color: var(--hm-sem-action-primary);
--hm-checkbox-checked-background: var(--hm-sem-state-active);
--hm-checkbox-checked-border-color: var(--hm-sem-state-active);
--hm-checkbox-checked-hover-overlay: var(--hm-color-action-overlay-hover);
--hm-checkbox-indicator-color: var(--hm-sem-on-state-active);
--hm-checkbox-error-border-color: var(--hm-sem-action-destructive);
}
[data-slot="checkbox"] {
box-sizing: border-box;
width: var(--hm-checkbox-size);
height: var(--hm-checkbox-size);
padding: 0;
border: var(--hm-checkbox-border-width) solid var(--hm-checkbox-border-color);
border-radius: var(--hm-checkbox-radius);
background-color: var(--hm-checkbox-background);
background-image: none;
color: var(--hm-checkbox-indicator-color);
box-shadow: none;
cursor: pointer;
}
[data-slot="checkbox"] svg { width: var(--hm-checkbox-icon-size); height: var(--hm-checkbox-icon-size); }
[data-slot="checkbox"]:hover:not(:disabled):not([data-state="checked"]):not([data-state="indeterminate"]) {
border-color: var(--hm-checkbox-hover-border-color);
}
[data-slot="checkbox"][data-state="checked"],
[data-slot="checkbox"][data-state="indeterminate"] {
border-color: var(--hm-checkbox-checked-border-color);
background-color: var(--hm-checkbox-checked-background);
}
[data-slot="checkbox"][data-state="checked"]:hover:not(:disabled),
[data-slot="checkbox"][data-state="indeterminate"]:hover:not(:disabled) {
background-image: linear-gradient(var(--hm-checkbox-checked-hover-overlay), var(--hm-checkbox-checked-hover-overlay));
}
/* Focus level is the shadcn ring (ui_viz global focus is TBD); keep the ring variables because the theme sets box-shadow: none. */
[data-slot="checkbox"]:focus-visible {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000);
}
[data-slot="checkbox"][aria-invalid="true"] {
border-color: var(--hm-checkbox-error-border-color);
}
[data-slot="checkbox"]:disabled {
cursor: not-allowed;
}
+66
View File
@@ -0,0 +1,66 @@
/* Collapsible mapping. ui-viz has no Collapsible spec, so these are current HM values built from the HM Semantic tokens.
Collapsible is one independent disclosure: a trigger and its content, with no divider system. A plain trigger (no asChild) gets
a text-button look; with asChild the child (for example an HM Button) keeps its own style. Radix attributes drive the states.
No --hm-accordion-* token is referenced. */
:root, [data-hm-theme] {
--hm-collapsible-trigger-text: var(--hm-sem-fg-primary);
--hm-collapsible-trigger-gap: 8px;
--hm-collapsible-trigger-font-size: 14px;
--hm-collapsible-trigger-line-height: 20px;
--hm-collapsible-trigger-font-weight: 500;
--hm-collapsible-icon-size: 16px;
--hm-collapsible-icon-color: var(--hm-sem-fg-secondary);
--hm-collapsible-content-gap: 8px;
--hm-collapsible-content-text: var(--hm-sem-fg-primary);
--hm-collapsible-content-font-size: 14px;
--hm-collapsible-content-line-height: 20px;
/* Current HM value for this component only; not a global Disabled rule. */
--hm-collapsible-disabled-opacity: 0.6;
}
[data-slot="collapsible-trigger"] {
display: inline-flex;
align-items: center;
gap: var(--hm-collapsible-trigger-gap);
padding: 0;
border: 0;
background: transparent;
color: var(--hm-collapsible-trigger-text);
font-size: var(--hm-collapsible-trigger-font-size);
line-height: var(--hm-collapsible-trigger-line-height);
font-weight: var(--hm-collapsible-trigger-font-weight);
text-align: left;
cursor: pointer;
outline: none;
}
[data-slot="collapsible-trigger"]:hover:not(:disabled) {
text-decoration: underline;
text-underline-offset: 4px;
}
[data-slot="collapsible-trigger"]:focus-visible {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
[data-slot="collapsible-trigger"]:disabled {
opacity: var(--hm-collapsible-disabled-opacity);
cursor: not-allowed;
}
/* An icon placed directly in a plain trigger follows the open state. */
[data-slot="collapsible-trigger"] > svg {
width: var(--hm-collapsible-icon-size);
height: var(--hm-collapsible-icon-size);
color: var(--hm-collapsible-icon-color);
flex-shrink: 0;
}
[data-slot="collapsible-trigger"][data-state="open"] > svg {
transform: rotate(180deg);
}
[data-slot="collapsible-content"] {
padding-top: var(--hm-collapsible-content-gap);
color: var(--hm-collapsible-content-text);
font-size: var(--hm-collapsible-content-font-size);
line-height: var(--hm-collapsible-content-line-height);
overflow-wrap: anywhere;
}
+144
View File
@@ -0,0 +1,144 @@
/* Combobox mapping. ui-viz has no Combobox spec (it has a Select and a Search Field, both different components), so these are current
HM values built from the HM Semantic / Primitive tokens. Combobox is a composition (Popover + Command): the popup surface comes
from Popover, the search input and list from Command. Scoped popup overrides align the selection language with Select without
changing either shared component. The popup is as wide as the trigger (an inline style sets its width / padding).
The trigger look is in `@layer components` so a consumer's utility classes (width, height) still win. Nothing here references
Button / Input / Select / Popover / Command tokens. */
:root, [data-hm-theme] {
/* Inner heights; the 1px border is added around them (outer 34 / 44), like the Select trigger. */
--hm-combobox-height-md: 32px;
--hm-combobox-height-lg: 42px;
--hm-combobox-border-width: 1px;
--hm-combobox-padding-x: 12px;
--hm-combobox-gap: 12px;
--hm-combobox-radius: var(--hm-radius-control-md);
--hm-combobox-font-size-md: 14px;
--hm-combobox-line-height-md: 20px;
--hm-combobox-font-size-lg: 16px;
--hm-combobox-line-height-lg: 24px;
--hm-combobox-background: var(--hm-sem-bg-surface);
--hm-combobox-text: var(--hm-sem-fg-primary);
--hm-combobox-placeholder: var(--hm-sem-fg-placeholder);
--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);
--hm-combobox-icon-size: 18px;
--hm-combobox-icon-color: var(--hm-sem-fg-secondary);
--hm-combobox-check-size: 16px;
--hm-combobox-popup-radius: var(--hm-radius-input);
--hm-combobox-popup-shadow: 0 2px 24px color-mix(in srgb, var(--hm-sem-palette-black) 12%, transparent);
--hm-combobox-option-state-background: var(--hm-sem-surface-highlight);
--hm-combobox-option-state-text: var(--accent-foreground);
/* Current HM value for this component only; not a global Disabled rule. */
--hm-combobox-disabled-opacity: 0.6;
}
/* Share the connecting border via sideOffset=-1; only the seam uses the neutral stroke. */
[data-slot="popover-content"][data-combobox-popup] {
border-color: var(--hm-combobox-focus-border-color);
border-radius: var(--hm-combobox-popup-radius);
box-shadow: var(--hm-combobox-popup-shadow);
--hm-combobox-option-font-size: var(--hm-combobox-font-size-lg);
--hm-combobox-option-line-height: var(--hm-combobox-line-height-lg);
}
[data-slot="popover-content"][data-combobox-popup][data-side="bottom"] {
border-radius: 0 0 var(--hm-combobox-popup-radius) var(--hm-combobox-popup-radius);
border-top-color: var(--hm-combobox-seam-color);
}
[data-slot="popover-content"][data-combobox-popup][data-side="top"] {
border-radius: var(--hm-combobox-popup-radius) var(--hm-combobox-popup-radius) 0 0;
border-bottom-color: var(--hm-combobox-seam-color);
}
[data-combobox-popup][data-size="medium"] {
--hm-combobox-option-font-size: var(--hm-combobox-font-size-md);
--hm-combobox-option-line-height: var(--hm-combobox-line-height-md);
}
[data-combobox-popup] [data-slot="command"] {
border-radius: inherit;
}
[data-combobox-popup] [data-slot="command-item"] {
/* Retain Command's 36px height, 12px horizontal padding and search layout. */
border-radius: 0;
font-size: var(--hm-combobox-option-font-size);
line-height: var(--hm-combobox-option-line-height);
font-weight: 400;
}
[data-combobox-popup] [data-slot="command-item"][data-selected="true"],
[data-combobox-popup] [data-slot="command-item"][data-checked] {
background: var(--hm-combobox-option-state-background);
color: var(--hm-combobox-option-state-text);
}
@layer components {
[data-slot="combobox-trigger"] {
--hm-combobox-height: var(--hm-combobox-height-lg);
--hm-combobox-font-size: var(--hm-combobox-font-size-lg);
--hm-combobox-line-height: var(--hm-combobox-line-height-lg);
box-sizing: border-box;
min-width: 0; /* lets a long value truncate instead of widening a grid / flex parent */
max-width: 100%;
min-height: calc(var(--hm-combobox-height) + 2 * var(--hm-combobox-border-width));
gap: var(--hm-combobox-gap);
padding: 0 var(--hm-combobox-padding-x);
border: var(--hm-combobox-border-width) solid var(--hm-combobox-border-color);
border-radius: var(--hm-combobox-radius);
background: var(--hm-combobox-background);
color: var(--hm-combobox-text);
font-size: var(--hm-combobox-font-size);
line-height: var(--hm-combobox-line-height);
font-weight: 400;
cursor: pointer;
}
[data-slot="combobox-trigger"][data-size="medium"] {
--hm-combobox-height: var(--hm-combobox-height-md);
--hm-combobox-font-size: var(--hm-combobox-font-size-md);
--hm-combobox-line-height: var(--hm-combobox-line-height-md);
}
[data-slot="combobox-trigger"][data-empty] [data-slot="combobox-value"] {
color: var(--hm-combobox-placeholder);
}
[data-slot="combobox-value"] {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-slot="combobox-icon"] {
flex-shrink: 0;
width: var(--hm-combobox-icon-size);
height: var(--hm-combobox-icon-size);
color: var(--hm-combobox-icon-color);
}
[data-slot="combobox-trigger"]:focus-visible,
[data-slot="combobox-trigger"][data-state="open"] {
border-color: var(--hm-combobox-focus-border-color);
}
[data-slot="combobox-trigger"][data-state="open"][data-side="bottom"] {
border-radius: var(--hm-combobox-radius) var(--hm-combobox-radius) 0 0;
border-bottom-color: var(--hm-combobox-seam-color);
}
[data-slot="combobox-trigger"][data-state="open"][data-side="top"] {
border-radius: 0 0 var(--hm-combobox-radius) var(--hm-combobox-radius);
border-top-color: var(--hm-combobox-seam-color);
}
[data-slot="combobox-trigger"]:disabled {
opacity: var(--hm-combobox-disabled-opacity);
cursor: not-allowed;
}
/* Items inside the popup: a long label is truncated and the check stays at the end. */
[data-slot="combobox-item-label"] {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-slot="combobox-item-check"] {
margin-left: auto;
width: var(--hm-combobox-check-size);
height: var(--hm-combobox-check-size);
}
}
+127
View File
@@ -0,0 +1,127 @@
/* Command mapping. ui-viz has no Command spec (it has a Search Field / candidate list, a different component), so these are current
HM values built from the HM Semantic / Primitive tokens. cmdk keeps filtering, keyboard and ARIA; the highlighted item is
[data-selected="true"], a disabled one [data-disabled="true"]. The Command draws its own surface (white, radius) but no border or
shadow: it is meant to sit inside a container (Popover, Dialog) or a bordered box. Nothing references Input / Popover /
Dropdown Menu tokens. */
:root, [data-hm-theme] {
--hm-command-background: var(--hm-sem-bg-surface);
--hm-command-text: var(--hm-sem-fg-primary);
--hm-command-radius: var(--hm-radius-container);
--hm-command-input-height: 40px;
--hm-command-input-padding-x: 12px;
--hm-command-input-gap: 8px;
--hm-command-input-font-size: 14px;
--hm-command-input-line-height: 20px;
--hm-command-input-placeholder: var(--hm-sem-fg-placeholder);
--hm-command-input-icon-size: 16px;
--hm-command-input-icon-color: var(--hm-sem-fg-secondary);
--hm-command-divider-color: var(--hm-sem-stroke-input-default);
--hm-command-list-max-height: 280px;
--hm-command-list-padding: 4px;
--hm-command-item-height: 36px;
--hm-command-item-padding-x: 12px;
--hm-command-item-gap: 8px;
--hm-command-item-radius: 2px;
--hm-command-item-font-size: 14px;
--hm-command-item-line-height: 20px;
--hm-command-highlight-background: var(--hm-sem-surface-highlight);
--hm-command-highlight-text: var(--accent-foreground);
--hm-command-group-label-color: var(--hm-sem-fg-secondary);
--hm-command-group-label-font-size: 12px;
--hm-command-group-label-line-height: 18px;
--hm-command-group-label-font-weight: 500;
--hm-command-shortcut-color: var(--hm-sem-fg-secondary);
--hm-command-shortcut-font-size: 12px;
--hm-command-empty-padding-y: 24px;
--hm-command-empty-color: var(--hm-sem-fg-secondary);
/* Current HM value for this component only; not a global Disabled rule. */
--hm-command-disabled-opacity: 0.6;
}
[data-slot="command"] {
box-sizing: border-box;
border-radius: var(--hm-command-radius);
background: var(--hm-command-background);
color: var(--hm-command-text);
}
[data-slot="command-input-wrapper"] {
box-sizing: border-box;
height: var(--hm-command-input-height);
gap: var(--hm-command-input-gap);
padding: 0 var(--hm-command-input-padding-x);
border-bottom: 1px solid var(--hm-command-divider-color);
}
[data-slot="command-input-wrapper"] > svg {
width: var(--hm-command-input-icon-size);
height: var(--hm-command-input-icon-size);
color: var(--hm-command-input-icon-color);
}
[data-slot="command-input"] {
height: 100%;
border: 0;
background: transparent;
color: inherit;
font-size: var(--hm-command-input-font-size);
line-height: var(--hm-command-input-line-height);
}
[data-slot="command-input"]::placeholder {
color: var(--hm-command-input-placeholder);
}
[data-slot="command-list"] {
max-height: var(--hm-command-list-max-height);
padding: var(--hm-command-list-padding);
}
[data-slot="command-empty"] {
padding: var(--hm-command-empty-padding-y) 12px;
color: var(--hm-command-empty-color);
font-size: var(--hm-command-item-font-size);
line-height: var(--hm-command-item-line-height);
text-align: center;
}
[data-slot="command-separator"] {
margin: var(--hm-command-list-padding) calc(-1 * var(--hm-command-list-padding));
background: var(--hm-command-divider-color);
}
[data-slot="command-group"] [cmdk-group-heading] {
padding: 6px var(--hm-command-item-padding-x);
color: var(--hm-command-group-label-color);
font-size: var(--hm-command-group-label-font-size);
line-height: var(--hm-command-group-label-line-height);
font-weight: var(--hm-command-group-label-font-weight);
}
[data-slot="command-item"] {
box-sizing: border-box;
min-height: var(--hm-command-item-height);
gap: var(--hm-command-item-gap);
padding: 0 var(--hm-command-item-padding-x);
border-radius: var(--hm-command-item-radius);
font-size: var(--hm-command-item-font-size);
line-height: var(--hm-command-item-line-height);
}
/* Highlighted: pointer hover and keyboard focus share cmdk's data-selected. */
[data-slot="command-item"][data-selected="true"] {
background: var(--hm-command-highlight-background);
color: var(--hm-command-highlight-text);
}
[data-slot="command-item"][data-disabled="true"] {
opacity: var(--hm-command-disabled-opacity);
cursor: not-allowed;
}
[data-slot="command-shortcut"] {
color: var(--hm-command-shortcut-color);
font-size: var(--hm-command-shortcut-font-size);
letter-spacing: 0.05em;
}
[data-slot="command-item"][data-selected="true"] [data-slot="command-shortcut"] {
color: inherit;
}
+121
View File
@@ -0,0 +1,121 @@
/* Context Menu mapping. Same anchored floating menu family as Dropdown Menu, opened by pointer context (right click / context key / long press).
Surface values come from the Floating Semantic (--hm-sem-floating-*); item and text values are current HM values. Radix state attributes
(data-highlighted, data-disabled, data-state) drive the states. Tokens are this component's own and never reference --hm-dropdown-menu-*. */
:root, [data-hm-theme] {
--hm-context-menu-background: var(--hm-sem-floating-background);
--hm-context-menu-text: var(--hm-sem-fg-primary);
--hm-context-menu-border-width: var(--hm-sem-floating-border-width);
--hm-context-menu-border-color: var(--hm-sem-floating-border-color);
--hm-context-menu-radius: var(--hm-sem-floating-radius);
--hm-context-menu-shadow: var(--hm-sem-floating-shadow);
--hm-context-menu-padding: 4px;
--hm-context-menu-item-height: 36px;
--hm-context-menu-item-padding-x: 12px;
--hm-context-menu-item-gap: 8px;
--hm-context-menu-item-radius: 2px;
--hm-context-menu-font-size: 14px;
--hm-context-menu-line-height: 20px;
--hm-context-menu-font-weight: 400;
/* Highlighted (hover and keyboard) uses the same face as the Select option: the shadcn accent. */
--hm-context-menu-highlight-background: var(--hm-sem-surface-highlight);
--hm-context-menu-highlight-text: var(--accent-foreground);
--hm-context-menu-destructive-text: var(--hm-sem-action-destructive);
--hm-context-menu-destructive-highlight-background: var(--hm-color-action-destructive-hover);
--hm-context-menu-label-color: var(--hm-sem-fg-secondary);
--hm-context-menu-label-font-size: 12px;
--hm-context-menu-label-line-height: 18px;
--hm-context-menu-label-font-weight: 500;
--hm-context-menu-shortcut-color: var(--hm-sem-fg-secondary);
--hm-context-menu-shortcut-font-size: 12px;
--hm-context-menu-separator-color: var(--hm-sem-stroke-input-default);
/* Check / radio indicator column at the start of the item. */
--hm-context-menu-indicator-size: 16px;
--hm-context-menu-radio-dot-size: 8px;
}
[data-slot="context-menu-content"],
[data-slot="context-menu-sub-content"] {
box-sizing: border-box;
padding: var(--hm-context-menu-padding);
border: var(--hm-context-menu-border-width) solid var(--hm-context-menu-border-color);
border-radius: var(--hm-context-menu-radius);
background: var(--hm-context-menu-background);
color: var(--hm-context-menu-text);
box-shadow: var(--hm-context-menu-shadow);
font-size: var(--hm-context-menu-font-size);
line-height: var(--hm-context-menu-line-height);
font-weight: var(--hm-context-menu-font-weight);
}
[data-slot="context-menu-item"],
[data-slot="context-menu-checkbox-item"],
[data-slot="context-menu-radio-item"],
[data-slot="context-menu-sub-trigger"] {
box-sizing: border-box;
min-height: var(--hm-context-menu-item-height);
gap: var(--hm-context-menu-item-gap);
padding: 0 var(--hm-context-menu-item-padding-x);
border-radius: var(--hm-context-menu-item-radius);
}
/* Checkbox / radio items reserve the indicator column; `inset` aligns plain items to the same text start. */
[data-slot="context-menu-checkbox-item"],
[data-slot="context-menu-radio-item"],
[data-slot="context-menu-item"][data-inset],
[data-slot="context-menu-sub-trigger"][data-inset] {
padding-left: calc(var(--hm-context-menu-item-padding-x) + var(--hm-context-menu-indicator-size) + var(--hm-context-menu-item-gap));
}
[data-slot="context-menu-item-indicator"] {
left: var(--hm-context-menu-item-padding-x);
width: var(--hm-context-menu-indicator-size);
height: var(--hm-context-menu-indicator-size);
}
[data-slot="context-menu-radio-dot"] {
width: var(--hm-context-menu-radio-dot-size);
height: var(--hm-context-menu-radio-dot-size);
}
/* Highlighted: Radix sets data-highlighted for pointer hover and keyboard focus alike. An open sub menu keeps its trigger highlighted. */
[data-slot="context-menu-item"][data-highlighted],
[data-slot="context-menu-checkbox-item"][data-highlighted],
[data-slot="context-menu-radio-item"][data-highlighted],
[data-slot="context-menu-sub-trigger"][data-highlighted],
[data-slot="context-menu-sub-trigger"][data-state="open"] {
background: var(--hm-context-menu-highlight-background);
color: var(--hm-context-menu-highlight-text);
}
[data-slot="context-menu-item"][data-variant="destructive"] {
color: var(--hm-context-menu-destructive-text);
}
[data-slot="context-menu-item"][data-variant="destructive"][data-highlighted] {
background: var(--hm-context-menu-destructive-highlight-background);
color: var(--hm-context-menu-destructive-text);
}
[data-slot="context-menu-label"] {
padding: 6px var(--hm-context-menu-item-padding-x);
color: var(--hm-context-menu-label-color);
font-size: var(--hm-context-menu-label-font-size);
line-height: var(--hm-context-menu-label-line-height);
font-weight: var(--hm-context-menu-label-font-weight);
}
[data-slot="context-menu-label"][data-inset] {
padding-left: calc(var(--hm-context-menu-item-padding-x) + var(--hm-context-menu-indicator-size) + var(--hm-context-menu-item-gap));
}
[data-slot="context-menu-separator"] {
margin: var(--hm-context-menu-padding) calc(-1 * var(--hm-context-menu-padding));
background: var(--hm-context-menu-separator-color);
}
[data-slot="context-menu-shortcut"] {
color: var(--hm-context-menu-shortcut-color);
font-size: var(--hm-context-menu-shortcut-font-size);
letter-spacing: 0.05em;
}
[data-slot="context-menu-item"][data-highlighted] [data-slot="context-menu-shortcut"],
[data-slot="context-menu-checkbox-item"][data-highlighted] [data-slot="context-menu-shortcut"],
[data-slot="context-menu-radio-item"][data-highlighted] [data-slot="context-menu-shortcut"] {
color: inherit;
}
+106
View File
@@ -0,0 +1,106 @@
import { runtimeThemeDefaults } from "./default-values.generated"
/** Public Runtime Theme contract v1. Component mappings and derived CSS are private. */
export const runtimeThemeContract = {
"color.bg.content": { property: "--hm-sem-bg-content", defaultValue: runtimeThemeDefaults["--hm-sem-bg-content"], type: "color" },
"color.bg.surface": { property: "--hm-sem-bg-surface", defaultValue: runtimeThemeDefaults["--hm-sem-bg-surface"], type: "color" },
"color.fg.primary": { property: "--hm-sem-fg-primary", defaultValue: runtimeThemeDefaults["--hm-sem-fg-primary"], type: "color" },
"color.fg.secondary": { property: "--hm-sem-fg-secondary", defaultValue: runtimeThemeDefaults["--hm-sem-fg-secondary"], type: "color" },
"color.fg.placeholder": { property: "--hm-sem-fg-placeholder", defaultValue: runtimeThemeDefaults["--hm-sem-fg-placeholder"], type: "color" },
"color.action.primary": { property: "--hm-sem-action-primary", defaultValue: runtimeThemeDefaults["--hm-sem-action-primary"], type: "color" },
"color.action.cancel": { property: "--hm-sem-action-cancel", defaultValue: runtimeThemeDefaults["--hm-sem-action-cancel"], type: "color" },
"color.action.destructive": { property: "--hm-sem-action-destructive", defaultValue: runtimeThemeDefaults["--hm-sem-action-destructive"], type: "color" },
"color.stroke.default": { property: "--hm-sem-stroke-default", defaultValue: runtimeThemeDefaults["--hm-sem-stroke-default"], type: "color" },
"color.stroke.input.default": { property: "--hm-sem-stroke-input-default", defaultValue: runtimeThemeDefaults["--hm-sem-stroke-input-default"], type: "color" },
"color.stroke.input.focus": { property: "--hm-sem-stroke-input-focus", defaultValue: runtimeThemeDefaults["--hm-sem-stroke-input-focus"], type: "color" },
"color.bg.brand.stop.0": { property: "--hm-sem-brand-bg-stop-0", defaultValue: runtimeThemeDefaults["--hm-sem-brand-bg-stop-0"], type: "color" },
"color.bg.brand.stop.12": { property: "--hm-sem-brand-bg-stop-12", defaultValue: runtimeThemeDefaults["--hm-sem-brand-bg-stop-12"], type: "color" },
"color.bg.brand.stop.42": { property: "--hm-sem-brand-bg-stop-42", defaultValue: runtimeThemeDefaults["--hm-sem-brand-bg-stop-42"], type: "color" },
"color.bg.brand.stop.78": { property: "--hm-sem-brand-bg-stop-78", defaultValue: runtimeThemeDefaults["--hm-sem-brand-bg-stop-78"], type: "color" },
"color.bg.brand.stop.100": { property: "--hm-sem-brand-bg-stop-100", defaultValue: runtimeThemeDefaults["--hm-sem-brand-bg-stop-100"], type: "color" },
"surface.action.primary.stop.0": { property: "--hm-sem-action-surface-stop-0", defaultValue: runtimeThemeDefaults["--hm-sem-action-surface-stop-0"], type: "color" },
"surface.action.primary.stop.30": { property: "--hm-sem-action-surface-stop-30", defaultValue: runtimeThemeDefaults["--hm-sem-action-surface-stop-30"], type: "color" },
"surface.action.primary.stop.65": { property: "--hm-sem-action-surface-stop-65", defaultValue: runtimeThemeDefaults["--hm-sem-action-surface-stop-65"], type: "color" },
"surface.action.primary.stop.100": { property: "--hm-sem-action-surface-stop-100", defaultValue: runtimeThemeDefaults["--hm-sem-action-surface-stop-100"], type: "color" },
"surface.action.primary.border.stop.0": { property: "--hm-sem-action-border-stop-0", defaultValue: runtimeThemeDefaults["--hm-sem-action-border-stop-0"], type: "color" },
"surface.action.primary.border.stop.50": { property: "--hm-sem-action-border-stop-50", defaultValue: runtimeThemeDefaults["--hm-sem-action-border-stop-50"], type: "color" },
"surface.action.primary.border.stop.100": { property: "--hm-sem-action-border-stop-100", defaultValue: runtimeThemeDefaults["--hm-sem-action-border-stop-100"], type: "color" },
"radius.input.default": { property: "--hm-radius-input", defaultValue: runtimeThemeDefaults["--hm-radius-input"], type: "radius" },
"radius.container.default": { property: "--hm-radius-container", defaultValue: runtimeThemeDefaults["--hm-radius-container"], type: "radius" },
"color.state.active": { property: "--hm-sem-state-active", defaultValue: runtimeThemeDefaults["--hm-sem-state-active"], type: "color" },
"color.on.state.active": { property: "--hm-sem-on-state-active", defaultValue: runtimeThemeDefaults["--hm-sem-on-state-active"], type: "color" },
"color.fg.inverse": { property: "--hm-sem-on-action-primary", defaultValue: runtimeThemeDefaults["--hm-sem-on-action-primary"], type: "color" },
"color.bg.subtle": { property: "--hm-sem-bg-subtle", defaultValue: runtimeThemeDefaults["--hm-sem-bg-subtle"], type: "color" },
"color.accent": { property: "--hm-sem-accent", defaultValue: runtimeThemeDefaults["--hm-sem-accent"], type: "color" },
"color.on.accent": { property: "--hm-sem-on-accent", defaultValue: runtimeThemeDefaults["--hm-sem-on-accent"], type: "color" },
"color.palette.green-light-4": { property: "--hm-sem-palette-green-light-4", defaultValue: runtimeThemeDefaults["--hm-sem-palette-green-light-4"], type: "color" },
"color.palette.green-light-3": { property: "--hm-sem-palette-green-light-3", defaultValue: runtimeThemeDefaults["--hm-sem-palette-green-light-3"], type: "color" },
"color.palette.green-light-2": { property: "--hm-sem-palette-green-light-2", defaultValue: runtimeThemeDefaults["--hm-sem-palette-green-light-2"], type: "color" },
"color.palette.green-light-1": { property: "--hm-sem-palette-green-light-1", defaultValue: runtimeThemeDefaults["--hm-sem-palette-green-light-1"], type: "color" },
"color.palette.green-default": { property: "--hm-sem-palette-green-default", defaultValue: runtimeThemeDefaults["--hm-sem-palette-green-default"], type: "color" },
"color.palette.green-dark-1": { property: "--hm-sem-palette-green-dark-1", defaultValue: runtimeThemeDefaults["--hm-sem-palette-green-dark-1"], type: "color" },
"color.palette.green-dark-2": { property: "--hm-sem-palette-green-dark-2", defaultValue: runtimeThemeDefaults["--hm-sem-palette-green-dark-2"], type: "color" },
"color.palette.brown-light-2": { property: "--hm-sem-palette-brown-light-2", defaultValue: runtimeThemeDefaults["--hm-sem-palette-brown-light-2"], type: "color" },
"color.palette.brown-light-1": { property: "--hm-sem-palette-brown-light-1", defaultValue: runtimeThemeDefaults["--hm-sem-palette-brown-light-1"], type: "color" },
"color.palette.brown-default": { property: "--hm-sem-palette-brown-default", defaultValue: runtimeThemeDefaults["--hm-sem-palette-brown-default"], type: "color" },
"color.palette.brown-dark-1": { property: "--hm-sem-palette-brown-dark-1", defaultValue: runtimeThemeDefaults["--hm-sem-palette-brown-dark-1"], type: "color" },
"color.palette.brown-light-4": { property: "--hm-sem-palette-brown-light-4", defaultValue: runtimeThemeDefaults["--hm-sem-palette-brown-light-4"], type: "color" },
"color.palette.orange-light-4": { property: "--hm-sem-palette-orange-light-4", defaultValue: runtimeThemeDefaults["--hm-sem-palette-orange-light-4"], type: "color" },
"color.palette.orange-default": { property: "--hm-sem-palette-orange-default", defaultValue: runtimeThemeDefaults["--hm-sem-palette-orange-default"], type: "color" },
"color.palette.warm-neutral-light": { property: "--hm-sem-palette-warm-neutral-light", defaultValue: runtimeThemeDefaults["--hm-sem-palette-warm-neutral-light"], type: "color" },
"color.palette.warm-neutral-mid": { property: "--hm-sem-palette-warm-neutral-mid", defaultValue: runtimeThemeDefaults["--hm-sem-palette-warm-neutral-mid"], type: "color" },
"color.palette.warm-neutral-dark": { property: "--hm-sem-palette-warm-neutral-dark", defaultValue: runtimeThemeDefaults["--hm-sem-palette-warm-neutral-dark"], type: "color" },
"color.palette.beige-light": { property: "--hm-sem-palette-beige-light", defaultValue: runtimeThemeDefaults["--hm-sem-palette-beige-light"], type: "color" },
"color.palette.red-danger": { property: "--hm-sem-palette-red-danger", defaultValue: runtimeThemeDefaults["--hm-sem-palette-red-danger"], type: "color" },
"color.palette.white": { property: "--hm-sem-palette-white", defaultValue: runtimeThemeDefaults["--hm-sem-palette-white"], type: "color" },
"color.palette.black": { property: "--hm-sem-palette-black", defaultValue: runtimeThemeDefaults["--hm-sem-palette-black"], type: "color" },
"color.palette.neutral-gray-500": { property: "--hm-sem-palette-neutral-gray-500", defaultValue: runtimeThemeDefaults["--hm-sem-palette-neutral-gray-500"], type: "color" },
"color.palette.green-tone-1": { property: "--hm-sem-palette-green-tone-1", defaultValue: runtimeThemeDefaults["--hm-sem-palette-green-tone-1"], type: "color" },
"color.palette.green-tone-2": { property: "--hm-sem-palette-green-tone-2", defaultValue: runtimeThemeDefaults["--hm-sem-palette-green-tone-2"], type: "color" },
"color.palette.green-tone-3": { property: "--hm-sem-palette-green-tone-3", defaultValue: runtimeThemeDefaults["--hm-sem-palette-green-tone-3"], type: "color" },
"color.palette.green-tone-4": { property: "--hm-sem-palette-green-tone-4", defaultValue: runtimeThemeDefaults["--hm-sem-palette-green-tone-4"], type: "color" },
"color.palette.warm-neutral-pale": { property: "--hm-sem-palette-warm-neutral-pale", defaultValue: runtimeThemeDefaults["--hm-sem-palette-warm-neutral-pale"], type: "color" },
"color.palette.warm-neutral-secondary": { property: "--hm-sem-palette-warm-neutral-secondary", defaultValue: runtimeThemeDefaults["--hm-sem-palette-warm-neutral-secondary"], type: "color" },
"color.palette.warm-neutral-muted": { property: "--hm-sem-palette-warm-neutral-muted", defaultValue: runtimeThemeDefaults["--hm-sem-palette-warm-neutral-muted"], type: "color" },
"color.palette.green-tint": { property: "--hm-sem-palette-green-tint", defaultValue: runtimeThemeDefaults["--hm-sem-palette-green-tint"], type: "color" },
"color.palette.yellow-accent": { property: "--hm-sem-palette-yellow-accent", defaultValue: runtimeThemeDefaults["--hm-sem-palette-yellow-accent"], type: "color" },
"color.palette.brown-deep": { property: "--hm-sem-palette-brown-deep", defaultValue: runtimeThemeDefaults["--hm-sem-palette-brown-deep"], type: "color" },
"color.palette.brown-highlight": { property: "--hm-sem-palette-brown-highlight", defaultValue: runtimeThemeDefaults["--hm-sem-palette-brown-highlight"], type: "color" },
"color.palette.orange-light": { property: "--hm-sem-palette-orange-light", defaultValue: runtimeThemeDefaults["--hm-sem-palette-orange-light"], type: "color" },
"color.palette.orange-mid": { property: "--hm-sem-palette-orange-mid", defaultValue: runtimeThemeDefaults["--hm-sem-palette-orange-mid"], type: "color" },
"color.palette.orange-dark": { property: "--hm-sem-palette-orange-dark", defaultValue: runtimeThemeDefaults["--hm-sem-palette-orange-dark"], type: "color" },
"color.palette.orange-deep": { property: "--hm-sem-palette-orange-deep", defaultValue: runtimeThemeDefaults["--hm-sem-palette-orange-deep"], type: "color" },
"color.palette.orange-border": { property: "--hm-sem-palette-orange-border", defaultValue: runtimeThemeDefaults["--hm-sem-palette-orange-border"], type: "color" },
"color.palette.orange-border-deep": { property: "--hm-sem-palette-orange-border-deep", defaultValue: runtimeThemeDefaults["--hm-sem-palette-orange-border-deep"], type: "color" },
"color.palette.gray": { property: "--hm-sem-palette-gray", defaultValue: runtimeThemeDefaults["--hm-sem-palette-gray"], type: "color" },
"color.palette.gray-dark": { property: "--hm-sem-palette-gray-dark", defaultValue: runtimeThemeDefaults["--hm-sem-palette-gray-dark"], type: "color" },
"color.palette.indicator": { property: "--hm-sem-palette-indicator", defaultValue: runtimeThemeDefaults["--hm-sem-palette-indicator"], type: "color" },
} as const
export type RuntimeThemeToken = keyof typeof runtimeThemeContract
export type RuntimeTheme = { schemaVersion: 1; tokens: Partial<Record<RuntimeThemeToken, string>> }
export const defaultRuntimeTheme: RuntimeTheme = {
schemaVersion: 1,
tokens: Object.fromEntries(Object.entries(runtimeThemeContract).map(([key, value]) => [key, value.defaultValue])),
}
export const runtimeGradientGroups = [
[
"color.bg.brand.stop.0",
"color.bg.brand.stop.12",
"color.bg.brand.stop.42",
"color.bg.brand.stop.78",
"color.bg.brand.stop.100"
],
[
"surface.action.primary.stop.0",
"surface.action.primary.stop.30",
"surface.action.primary.stop.65",
"surface.action.primary.stop.100"
],
[
"surface.action.primary.border.stop.0",
"surface.action.primary.border.stop.50",
"surface.action.primary.border.stop.100"
]
] as const
+149
View File
@@ -0,0 +1,149 @@
/* Dashboard Mode owns its internal navigation and scroll frame. No GNB/LNB CSS variable or responsive dependency. */
:root, [data-hm-theme] {
--hm-dashboard-height: 100dvh;
--hm-dashboard-background: var(--hm-sem-palette-green-light-2);
--hm-dashboard-content-background: var(--hm-sem-bg-content);
--hm-dashboard-content-top-radius: 12px;
--hm-dashboard-content-side-radius: 18px;
--hm-dashboard-sidebar-width: 272px;
--hm-dashboard-min-width: 960px;
--hm-dashboard-header-background: linear-gradient(180deg, var(--hm-sem-action-surface-stop-0) 0%, var(--hm-sem-action-surface-stop-30) 30%, var(--hm-sem-action-surface-stop-65) 65%, var(--hm-sem-action-surface-stop-100) 100%);
--hm-dashboard-nav-text: var(--hm-sem-on-action-primary);
--hm-dashboard-nav-text-secondary: var(--hm-sem-palette-green-tint);
--hm-dashboard-nav-text-stroke-width: 1px;
--hm-dashboard-nav-text-stroke-color: color-mix(in srgb, var(--hm-sem-palette-black) 30%, transparent);
--hm-dashboard-nav-focus-outline: 2px solid var(--hm-sem-on-action-primary);
--hm-dashboard-padding: 34px;
--hm-dashboard-gap: 24px;
--hm-dashboard-content-shadow-top: 0 -8px 18px color-mix(in srgb, var(--hm-sem-palette-green-dark-2) 18%, transparent);
--hm-dashboard-content-shadow-side: -8px 0 18px color-mix(in srgb, var(--hm-sem-palette-green-dark-2) 18%, transparent);
}
[data-slot="dashboard-scroll"] {
box-sizing: border-box;
height: var(--hm-dashboard-height);
min-width: 0;
max-width: 100%;
overflow: auto;
}
[data-slot="dashboard-layout"] {
box-sizing: border-box;
height: 100%;
/* 4 × 200px KPI cards + 3 × 24px gaps + 2 × 34px padding + 20px scrollbar allowance.
Also fits the measured 549px compact status bar, its wings and the 125px brand without overlap. */
min-width: var(--hm-dashboard-min-width, 960px);
}
[data-slot="dashboard-layout"][data-sidebar] {
min-width: calc(var(--hm-dashboard-min-width) + var(--hm-dashboard-sidebar-width));
}
[data-slot="dashboard-body"] {
flex: 1 1 0;
min-height: 0;
min-width: 0;
background: var(--hm-dashboard-background);
}
[data-slot="dashboard-header"] {
display: flex;
flex: none;
align-items: center;
justify-content: space-between;
gap: 24px;
height: 48px;
padding: 0 20px;
background: var(--hm-dashboard-header-background);
color: var(--hm-dashboard-nav-text);
}
[data-slot="dashboard-body"] > [data-slot="dashboard-sidebar"] {
flex: none;
width: var(--hm-dashboard-sidebar-width);
min-height: 0;
padding: 28px 20px 22px;
gap: 22px;
background: linear-gradient(90deg, var(--hm-sem-action-primary) 0%, var(--hm-sem-palette-green-light-1) 30%, var(--hm-sem-palette-green-light-2) 50%, var(--hm-sem-palette-green-light-3) 90%, var(--hm-sem-palette-green-light-2) 100%);
color: var(--hm-dashboard-nav-text);
}
[data-slot="dashboard-sidebar"] > nav {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
}
[data-slot="dashboard-sidebar"] [data-slot="button"] {
justify-content: flex-start;
width: 100%;
min-height: 44px;
height: auto;
white-space: normal;
color: var(--hm-dashboard-nav-text);
}
[data-slot="dashboard-sidebar"] [data-slot="button"]:hover {
background: color-mix(in srgb, var(--hm-sem-palette-black) 10%, transparent);
}
[data-slot="dashboard-sidebar"] [data-slot="button"][aria-expanded="true"] {
background: color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent);
}
[data-slot="dashboard-sidebar"] [data-slot="collapsible"][data-current] > [data-slot="button"] {
color: var(--hm-sem-palette-yellow-accent);
}
[data-slot="dashboard-sidebar"] [data-slot="button"][aria-expanded="true"] > svg {
transform: rotate(90deg);
}
[data-slot="dashboard-sidebar"] [aria-current="page"],
[data-slot="dashboard-sidebar"] [aria-current="page"]:hover {
background: var(--hm-dashboard-content-background);
color: var(--hm-sem-action-primary);
}
[data-slot="dashboard-sidebar"] [data-slot="button"]:focus-visible {
outline: var(--hm-dashboard-nav-focus-outline);
}
[data-slot="dashboard-sidebar"] [aria-current="page"]:focus-visible {
outline-color: var(--hm-sem-action-primary);
}
/* Content surface and vertical scroll container, positioned after Dashboard's internal navigation. */
[data-slot="dashboard-main"] {
position: relative;
box-sizing: border-box;
flex: 1 1 0;
min-width: 0;
min-height: 0;
padding: var(--hm-dashboard-padding);
overflow-x: auto;
overflow-y: auto;
/* Let horizontal wheel/touch gestures chain to the outer Dashboard frame. */
overscroll-behavior-y: contain;
background: var(--hm-dashboard-content-background);
}
/* Dashboard header and sidebar use Dashboard-owned radii. */
[data-slot="dashboard-layout"][data-header] [data-slot="dashboard-main"] {
border-radius: var(--hm-dashboard-content-top-radius) var(--hm-dashboard-content-top-radius) 0 0;
box-shadow: var(--hm-dashboard-content-shadow-top);
}
[data-slot="dashboard-content"] {
gap: var(--hm-dashboard-gap);
min-width: 0;
}
[data-slot="dashboard-content"] > * {
min-width: 0;
}
/* Preserve the desktop filter row inside the minimum-width frame, independent of the viewport's mobile Filter Bar rules. */
[data-slot="dashboard-layout"] [data-slot="filter-bar"] {
flex-wrap: nowrap;
}
[data-slot="dashboard-layout"] [data-slot="filter-bar-search"] {
flex: 0 0 var(--hm-filter-bar-search-width);
}
[data-slot="dashboard-layout"] [data-slot="filter-bar"] > :is([data-slot="select-trigger"], [data-slot="combobox-trigger"], [data-slot="date-picker-trigger"], [data-slot="input"]) {
flex: 0 0 var(--hm-filter-bar-control-width);
width: var(--hm-filter-bar-control-width);
}
[data-slot="dashboard-layout"] [data-slot="filter-bar-actions"] {
flex: 0 0 auto;
}
/* The sidebar remains inside the Dashboard minimum-width frame at every viewport width. */
[data-slot="dashboard-layout"][data-sidebar] [data-slot="dashboard-main"] {
border-radius: var(--hm-dashboard-content-side-radius) 0 0 var(--hm-dashboard-content-side-radius);
box-shadow: var(--hm-dashboard-content-shadow-side);
}
+76
View File
@@ -0,0 +1,76 @@
/* Data Table pattern: wrappers around the Table primitive. Header / cell / row looks are src/theme/table.css; Checkbox, Pagination, Select,
Dropdown Menu, Button and Skeleton keep their own tokens. Only the footer layout, summary text, sort button, empty and loading cells are here. */
:root, [data-hm-theme] {
--hm-data-table-footer-gap: 12px;
--hm-data-table-footer-padding-top: 12px;
--hm-data-table-summary-color: var(--hm-sem-fg-secondary);
--hm-data-table-summary-font-size: 13px;
--hm-data-table-sort-gap: 4px;
--hm-data-table-sort-icon-size: 14px;
--hm-data-table-sort-active-color: var(--hm-sem-palette-green-light-3);
--hm-data-table-empty-padding-y: 48px;
--hm-data-table-empty-color: var(--hm-sem-fg-secondary);
--hm-data-table-skeleton-height: 16px;
}
/* min-width 0 lets the Table container scroll sideways inside a grid / flex parent instead of widening the page. */
[data-slot="data-table"] {
min-width: 0;
}
[data-slot="data-table-footer"] {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--hm-data-table-footer-gap);
padding-top: var(--hm-data-table-footer-padding-top);
}
[data-slot="data-table-summary"] {
margin: 0;
margin-right: auto;
color: var(--hm-data-table-summary-color);
font-size: var(--hm-data-table-summary-font-size);
}
[data-slot="data-table-page-size"] {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--hm-data-table-summary-color);
font-size: var(--hm-data-table-summary-font-size);
}
[data-slot="data-table-footer"] [data-slot="data-table-pagination"] {
width: auto;
margin: 0;
}
[data-slot="data-table-sort"] {
gap: var(--hm-data-table-sort-gap);
font: inherit;
color: inherit;
cursor: pointer;
}
[data-slot="data-table-sort"] svg {
width: var(--hm-data-table-sort-icon-size);
height: var(--hm-data-table-sort-icon-size);
opacity: 0.6;
}
[data-slot="data-table-sort"][data-sorted] svg {
opacity: 1;
color: var(--hm-data-table-sort-active-color);
}
[data-slot="data-table-sort"]:focus-visible {
outline: 2px solid var(--hm-sem-stroke-input-focus);
outline-offset: 2px;
border-radius: 2px;
}
[data-slot="data-table-empty"] {
padding-top: var(--hm-data-table-empty-padding-y);
padding-bottom: var(--hm-data-table-empty-padding-y);
text-align: center;
color: var(--hm-data-table-empty-color);
}
[data-slot="data-table-skeleton"] {
height: var(--hm-data-table-skeleton-height);
width: 100%;
}
+89
View File
@@ -0,0 +1,89 @@
/* Date Picker mapping. ui-viz lists Date Picker by name only (no spec), so these are current HM values built from the HM Semantic /
Primitive tokens. Date Picker is a composition (Popover + Calendar): the popup surface comes from Popover and the calendar from
Calendar, so only the trigger (a field, in the two Select sizes) is styled here. The Popover padding / width are overridden per
instance through an inline style in the component (the popup hugs the Calendar, which has its own padding).
The trigger look is in `@layer components` so a consumer's utility classes (width, height) still win. Nothing here references
Button / Input / Select / Popover tokens. */
:root, [data-hm-theme] {
/* Inner heights; the 1px border is added around them (outer 34 / 44), like the Select trigger. */
--hm-date-picker-height-md: 32px;
--hm-date-picker-height-lg: 42px;
--hm-date-picker-border-width: 1px;
--hm-date-picker-padding-x: 12px;
--hm-date-picker-gap: 12px;
--hm-date-picker-radius: var(--hm-radius-control-md);
--hm-date-picker-font-size-md: 14px;
--hm-date-picker-line-height-md: 20px;
--hm-date-picker-font-size-lg: 16px;
--hm-date-picker-line-height-lg: 24px;
--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);
--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;
--hm-date-picker-icon-color: var(--hm-sem-fg-secondary);
/* Current HM value for this component only; not a global Disabled rule. */
--hm-date-picker-disabled-opacity: 0.6;
}
@layer components {
[data-slot="date-picker-trigger"] {
--hm-date-picker-height: var(--hm-date-picker-height-lg);
--hm-date-picker-font-size: var(--hm-date-picker-font-size-lg);
--hm-date-picker-line-height: var(--hm-date-picker-line-height-lg);
box-sizing: border-box;
min-width: 0; /* lets a long value truncate instead of widening a grid / flex parent */
max-width: 100%;
min-height: calc(var(--hm-date-picker-height) + 2 * var(--hm-date-picker-border-width));
gap: var(--hm-date-picker-gap);
padding: 0 var(--hm-date-picker-padding-x);
border: var(--hm-date-picker-border-width) solid var(--hm-date-picker-border-color);
border-radius: var(--hm-date-picker-radius);
background: var(--hm-date-picker-background);
color: var(--hm-date-picker-text);
font-size: var(--hm-date-picker-font-size);
line-height: var(--hm-date-picker-line-height);
font-weight: 400;
cursor: pointer;
}
[data-slot="date-picker-trigger"][data-size="medium"] {
--hm-date-picker-height: var(--hm-date-picker-height-md);
--hm-date-picker-font-size: var(--hm-date-picker-font-size-md);
--hm-date-picker-line-height: var(--hm-date-picker-line-height-md);
}
[data-slot="date-picker-trigger"][data-empty] [data-slot="date-picker-value"] {
color: var(--hm-date-picker-placeholder);
}
[data-slot="date-picker-value"] {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-slot="date-picker-icon"] {
flex-shrink: 0;
width: var(--hm-date-picker-icon-size);
height: var(--hm-date-picker-icon-size);
color: var(--hm-date-picker-icon-color);
}
/* Focus and open: the whole outline turns to the focus color (no ring, like the Input / Select focus). */
[data-slot="date-picker-trigger"]:focus-visible,
[data-slot="date-picker-trigger"][data-state="open"] {
border-color: var(--hm-date-picker-focus-border-color);
}
/* Existing native aria-invalid is the source; Calendar selection/focus remains independent. */
[data-slot="date-picker-trigger"][aria-invalid="true"] {
border-color: var(--hm-sem-action-destructive);
}
[data-slot="date-picker-trigger"][aria-invalid="true"]:focus-visible {
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
[data-slot="date-picker-trigger"]:disabled {
opacity: var(--hm-date-picker-disabled-opacity);
cursor: not-allowed;
}
}
@@ -0,0 +1,77 @@
// Generated by npm run generate:theme-defaults. Do not edit.
// Colors: themes/default.json; geometry/aliases: scripts/theme-templates.
export const runtimeThemeDefaults = {
"--hm-sem-bg-content": "#f5f0e5",
"--hm-sem-bg-surface": "#ffffff",
"--hm-sem-fg-primary": "#0a221d",
"--hm-sem-fg-secondary": "#6e6759",
"--hm-sem-fg-placeholder": "#999999",
"--hm-sem-action-primary": "#123328",
"--hm-sem-action-cancel": "#6e6759",
"--hm-sem-action-destructive": "#d32f2f",
"--hm-sem-stroke-default": "#9a9284",
"--hm-sem-stroke-input-default": "#dedbd2",
"--hm-sem-stroke-input-focus": "#266651",
"--hm-sem-brand-bg-stop-0": "#0a221d",
"--hm-sem-brand-bg-stop-12": "#071e18",
"--hm-sem-brand-bg-stop-42": "#123328",
"--hm-sem-brand-bg-stop-78": "#266651",
"--hm-sem-brand-bg-stop-100": "#205241",
"--hm-sem-action-surface-stop-0": "#1f8265",
"--hm-sem-action-surface-stop-30": "#165c47",
"--hm-sem-action-surface-stop-65": "#114838",
"--hm-sem-action-surface-stop-100": "#08241c",
"--hm-sem-action-border-stop-0": "#0a221d",
"--hm-sem-action-border-stop-50": "#34daaa",
"--hm-sem-action-border-stop-100": "#08241c",
"--hm-radius-input": "4px",
"--hm-radius-container": "4px",
"--hm-sem-state-active": "#266651",
"--hm-sem-on-state-active": "#ffffff",
"--hm-sem-on-action-primary": "#ffffff",
"--hm-sem-bg-subtle": "#f4f1ea",
"--hm-sem-accent": "#bddfd4",
"--hm-sem-on-accent": "#123328",
"--hm-sem-palette-green-light-4": "#34daaa",
"--hm-sem-palette-green-light-3": "#249473",
"--hm-sem-palette-green-light-2": "#266651",
"--hm-sem-palette-green-light-1": "#205241",
"--hm-sem-palette-green-default": "#123328",
"--hm-sem-palette-green-dark-1": "#0a221d",
"--hm-sem-palette-green-dark-2": "#071e18",
"--hm-sem-palette-brown-light-2": "#897445",
"--hm-sem-palette-brown-light-1": "#604f32",
"--hm-sem-palette-brown-default": "#3e3523",
"--hm-sem-palette-brown-dark-1": "#352d1d",
"--hm-sem-palette-brown-light-4": "#ecd4a2",
"--hm-sem-palette-orange-light-4": "#feba88",
"--hm-sem-palette-orange-default": "#cc5200",
"--hm-sem-palette-warm-neutral-light": "#e0dace",
"--hm-sem-palette-warm-neutral-mid": "#9a9284",
"--hm-sem-palette-warm-neutral-dark": "#6e6759",
"--hm-sem-palette-beige-light": "#f5f0e5",
"--hm-sem-palette-red-danger": "#d32f2f",
"--hm-sem-palette-white": "#ffffff",
"--hm-sem-palette-black": "#000000",
"--hm-sem-palette-neutral-gray-500": "#999999",
"--hm-sem-palette-green-tone-1": "#1f8265",
"--hm-sem-palette-green-tone-2": "#165c47",
"--hm-sem-palette-green-tone-3": "#114838",
"--hm-sem-palette-green-tone-4": "#08241c",
"--hm-sem-palette-warm-neutral-pale": "#dedbd2",
"--hm-sem-palette-warm-neutral-secondary": "#ede8db",
"--hm-sem-palette-warm-neutral-muted": "#f4f1ea",
"--hm-sem-palette-green-tint": "#bddfd4",
"--hm-sem-palette-yellow-accent": "#ffc600",
"--hm-sem-palette-brown-deep": "#221d13",
"--hm-sem-palette-brown-highlight": "#a98d53",
"--hm-sem-palette-orange-light": "#d35a00",
"--hm-sem-palette-orange-mid": "#ce5600",
"--hm-sem-palette-orange-dark": "#b44b00",
"--hm-sem-palette-orange-deep": "#783402",
"--hm-sem-palette-orange-border": "#bc4c00",
"--hm-sem-palette-orange-border-deep": "#5e2902",
"--hm-sem-palette-gray": "#767676",
"--hm-sem-palette-gray-dark": "#5a5a5a",
"--hm-sem-palette-indicator": "#2d2d2d"
} as const
+203
View File
@@ -0,0 +1,203 @@
/* Generated from themes/default.json + scripts/theme-templates/default.css. Do not edit. */
@import "./primitives.css";
/* Derived aliases and geometry. Values are generated from themes/default.json. */
:root, [data-hm-theme] {
--hm-color-primary: var(--hm-sem-palette-green-default);
--hm-color-primary-hover: color-mix(in srgb, var(--hm-sem-action-primary) 85%, var(--hm-sem-palette-black));
--hm-color-primary-active: color-mix(in srgb, var(--hm-sem-action-primary) 70%, var(--hm-sem-palette-black));
--hm-color-surface-default: var(--hm-sem-palette-white);
--hm-color-surface-content: var(--hm-sem-palette-beige-light);
--hm-color-surface-subtle: var(--hm-sem-palette-warm-neutral-muted);
--hm-color-text-primary: var(--hm-sem-palette-green-dark-1);
--hm-color-text-secondary: var(--hm-sem-palette-warm-neutral-dark);
--hm-color-text-inverse: var(--hm-sem-palette-white);
--hm-color-text-placeholder: var(--hm-sem-palette-neutral-gray-500);
--hm-color-border-default: var(--hm-sem-palette-warm-neutral-mid);
--hm-color-border-control: var(--hm-sem-palette-warm-neutral-pale);
/* Common Focus color. Trial value: Light 2 (#266651), pending user confirmation. Was --hm-color-primary. */
--hm-color-border-focus: var(--hm-sem-palette-green-light-2);
--hm-color-action-cancel: var(--hm-sem-palette-warm-neutral-dark);
--hm-color-action-destructive: var(--hm-sem-palette-red-danger);
--hm-color-action-destructive-hover: color-mix(in srgb, var(--hm-sem-action-destructive) 8%, var(--hm-sem-bg-surface));
--hm-color-action-overlay-hover: color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent);
--hm-color-action-overlay-active: color-mix(in srgb, var(--hm-sem-palette-black) 30%, transparent);
--hm-color-action-shadow-light: color-mix(in srgb, var(--hm-sem-palette-white) 10%, transparent);
--hm-color-action-shadow-dark: color-mix(in srgb, var(--hm-sem-palette-black) 25%, transparent);
--hm-radius-control-md: 4px;
--hm-control-height-md: 34px;
--hm-font-family-base: "Noto Sans KR", "Malgun Gothic", sans-serif;
--hm-font-size-button-md: 13px;
--hm-font-weight-medium: 500;
--hm-font-weight-bold: 700;
/* Existing semantic names remain the shadcn and preview adapter. */
--hm-sem-bg-content: var(--hm-color-surface-content);
--hm-sem-bg-surface: var(--hm-color-surface-default);
--hm-sem-fg-primary: var(--hm-color-text-primary);
--hm-sem-fg-secondary: var(--hm-color-text-secondary);
--hm-sem-fg-placeholder: var(--hm-color-text-placeholder);
--hm-sem-action-primary: var(--hm-color-primary);
--hm-sem-action-cancel: var(--hm-color-action-cancel);
--hm-sem-action-destructive: var(--hm-color-action-destructive);
--hm-sem-stroke-default: var(--hm-color-border-default);
--hm-sem-stroke-input-default: var(--hm-color-border-control);
--hm-sem-stroke-input-focus: var(--hm-color-border-focus);
--hm-sem-on-action-primary: var(--hm-color-text-inverse);
/* Active state fill: checked / selected / on (Checkbox, Radio, Switch, Slider, Progress, Calendar).
Sibling of the Focus chain (--hm-color-border-focus): same Light 2 today, but neither references the other. */
--hm-sem-state-active: var(--hm-sem-palette-green-light-2);
--hm-sem-on-state-active: var(--hm-color-text-inverse);
/* Public gradient stop groups; geometry remains internal. */
--hm-sem-brand-bg-stop-0: var(--hm-sem-palette-green-dark-1);
--hm-sem-brand-bg-stop-12: var(--hm-sem-palette-green-dark-2);
--hm-sem-brand-bg-stop-42: var(--hm-sem-palette-green-default);
--hm-sem-brand-bg-stop-78: var(--hm-sem-palette-green-light-2);
--hm-sem-brand-bg-stop-100: var(--hm-sem-palette-green-light-1);
--hm-sem-bg-brand: linear-gradient(to bottom,
var(--hm-sem-brand-bg-stop-0) 0%, var(--hm-sem-brand-bg-stop-12) 12%,
var(--hm-sem-brand-bg-stop-42) 42%, var(--hm-sem-brand-bg-stop-78) 78%,
var(--hm-sem-brand-bg-stop-100) 100%);
--hm-sem-action-surface-stop-0: var(--hm-sem-palette-green-tone-1);
--hm-sem-action-surface-stop-30: var(--hm-sem-palette-green-tone-2);
--hm-sem-action-surface-stop-65: var(--hm-sem-palette-green-tone-3);
--hm-sem-action-surface-stop-100: var(--hm-sem-palette-green-tone-4);
--hm-sem-surface-action-primary: linear-gradient(180deg,
var(--hm-sem-action-surface-stop-0) 0%, var(--hm-sem-action-surface-stop-30) 30%,
var(--hm-sem-action-surface-stop-65) 65%, var(--hm-sem-action-surface-stop-100) 100%);
--hm-sem-action-border-stop-0: var(--hm-sem-palette-green-dark-1);
--hm-sem-action-border-stop-50: var(--hm-sem-palette-green-light-4);
--hm-sem-action-border-stop-100: var(--hm-sem-palette-green-tone-4);
--hm-sem-border-action-primary: linear-gradient(180deg,
var(--hm-sem-action-border-stop-0) 0%, var(--hm-sem-action-border-stop-50) 50%,
var(--hm-sem-action-border-stop-100) 100%);
--hm-radius-button: var(--hm-radius-control-md);
--hm-radius-input: var(--hm-radius-control-md);
--hm-radius-select: var(--hm-radius-control-md);
--hm-radius-container: var(--hm-radius-control-md);
/* Floating surface: anchored panels (Dropdown Menu, Context Menu, Popover, Hover Card, Menubar content, Navigation Menu panel).
Not Tooltip (inverse face), Select, Card or the modal surfaces. Component tokens keep their own names and point here. */
--hm-sem-floating-background: var(--hm-sem-bg-surface);
--hm-sem-floating-border-width: 1px;
--hm-sem-floating-border-color: var(--hm-sem-stroke-input-default);
--hm-sem-floating-radius: var(--hm-radius-container);
--hm-sem-floating-shadow: 0 0 12px color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent);
--hm-sem-bg-subtle: var(--hm-color-surface-subtle);
--hm-sem-accent: var(--hm-sem-palette-green-tint);
--hm-sem-on-accent: #123328;
--hm-sem-palette-green-light-4: var(--hm-green-light-4);
--hm-sem-palette-green-light-3: var(--hm-green-light-3);
--hm-sem-palette-green-light-2: var(--hm-green-light-2);
--hm-sem-palette-green-light-1: var(--hm-green-light-1);
--hm-sem-palette-green-default: var(--hm-green-default);
--hm-sem-palette-green-dark-1: var(--hm-green-dark-1);
--hm-sem-palette-green-dark-2: var(--hm-green-dark-2);
--hm-sem-palette-brown-light-2: var(--hm-brown-light-2);
--hm-sem-palette-brown-light-1: var(--hm-brown-light-1);
--hm-sem-palette-brown-default: var(--hm-brown-default);
--hm-sem-palette-brown-dark-1: var(--hm-brown-dark-1);
--hm-sem-palette-brown-light-4: var(--hm-brown-light-4);
--hm-sem-palette-orange-light-4: var(--hm-orange-light-4);
--hm-sem-palette-orange-default: var(--hm-orange-default);
--hm-sem-palette-warm-neutral-light: var(--hm-warm-neutral-light);
--hm-sem-palette-warm-neutral-mid: var(--hm-warm-neutral-mid);
--hm-sem-palette-warm-neutral-dark: var(--hm-warm-neutral-dark);
--hm-sem-palette-beige-light: var(--hm-beige-light);
--hm-sem-palette-red-danger: var(--hm-red-danger);
--hm-sem-palette-white: var(--hm-white);
--hm-sem-palette-black: var(--hm-black);
--hm-sem-palette-neutral-gray-500: var(--hm-neutral-gray-500);
--hm-sem-palette-green-tone-1: var(--hm-green-tone-1);
--hm-sem-palette-green-tone-2: var(--hm-green-tone-2);
--hm-sem-palette-green-tone-3: var(--hm-green-tone-3);
--hm-sem-palette-green-tone-4: var(--hm-green-tone-4);
--hm-sem-palette-warm-neutral-pale: var(--hm-warm-neutral-pale);
--hm-sem-palette-warm-neutral-secondary: var(--hm-warm-neutral-secondary);
--hm-sem-palette-warm-neutral-muted: var(--hm-warm-neutral-muted);
--hm-sem-palette-green-tint: var(--hm-green-tint);
--hm-sem-palette-yellow-accent: var(--hm-yellow-accent);
--hm-sem-palette-brown-deep: #221d13;
--hm-sem-palette-brown-highlight: #a98d53;
--hm-sem-palette-orange-light: #d35a00;
--hm-sem-palette-orange-mid: #ce5600;
--hm-sem-palette-orange-dark: #b44b00;
--hm-sem-palette-orange-deep: #783402;
--hm-sem-palette-orange-border: #bc4c00;
--hm-sem-palette-orange-border-deep: #5e2902;
--hm-sem-palette-gray: #767676;
--hm-sem-palette-gray-dark: #5a5a5a;
--hm-sem-palette-indicator: #2d2d2d;
--hm-sem-overlay-background: color-mix(in srgb, var(--hm-sem-palette-black) 10%, transparent);
--hm-sem-surface-highlight: color-mix(in srgb, var(--hm-sem-accent) 45%, transparent);
/* Adapter for shadcn/Tailwind. The public contract.ts allowlist limits runtime overrides. */
--background: var(--hm-sem-bg-content);
--foreground: var(--hm-sem-fg-primary);
--card: var(--hm-sem-bg-surface);
--card-foreground: var(--hm-sem-fg-primary);
--popover: var(--hm-sem-bg-surface);
--popover-foreground: var(--hm-sem-fg-primary);
--primary: var(--hm-sem-action-primary);
--primary-foreground: var(--hm-sem-on-action-primary);
--secondary: var(--hm-sem-palette-warm-neutral-secondary);
--secondary-foreground: var(--hm-sem-palette-brown-default);
--muted: var(--hm-sem-bg-subtle);
--muted-foreground: var(--hm-sem-fg-secondary);
--accent: var(--hm-sem-accent);
--accent-foreground: var(--hm-sem-on-accent);
--destructive: var(--hm-sem-action-destructive);
--border: var(--hm-sem-stroke-default);
--input: var(--hm-sem-stroke-input-default);
--ring: var(--hm-sem-stroke-input-focus);
--radius: var(--hm-radius-container);
}
+104
View File
@@ -0,0 +1,104 @@
/* Dialog mapping. ui_viz: docs/components/dialog.md (Dialog / Modal), §1.1, §2, §4, §9. Radix keeps Portal, focus trap, ARIA, Escape, focus return and scroll lock.
Only the look is set here: container, regions (Header / Body / Footer) and text. The Overlay, motion and Close button keep the shadcn defaults. */
:root, [data-hm-theme] {
/* ui_viz: dialog.md §2, §8.1 — Radius 8px (Dialog-local, USER_DECISION) and shadow 0 0 12px rgba(0,0,0,.2) (D-093 D-088) — Confirmed. Dialog-local, not Foundation tokens. */
--hm-dialog-radius: 8px;
--hm-dialog-shadow: 0 0 12px color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent);
/* ui_viz: dialog.md §2 — Surface color.bg.surface / foreground color.fg.primary — Confirmed reuse */
--hm-dialog-background: var(--hm-sem-bg-surface);
--hm-dialog-text: var(--hm-sem-fg-primary);
/* ui_viz: dialog.md §2 — outer 1px; color #DEDBD2 is Temporary (Token relation TBD, DIALOG-10 FOLLOW_UP). Same value as the Header divider. */
--hm-dialog-border-width: 1px;
--hm-dialog-border-color: var(--hm-sem-stroke-input-default);
--hm-dialog-divider-color: var(--hm-sem-stroke-input-default);
/* ui_viz: dialog.md §9 — Header 24/24/16, Title<->Description 8, Body 24 all sides, Actions 0/24/24, Action gap 8 — Confirmed (D-093, COMPONENT_SCOPED) */
--hm-dialog-padding: 24px;
--hm-dialog-header-padding-bottom: 16px;
--hm-dialog-header-gap: 8px;
--hm-dialog-actions-gap: 8px;
--hm-dialog-footer-padding-top: 16px;
/* ui_viz: dialog.md §1.1 — Title heading-md 20/28/700 (Dialog-local), Description body-sm 14/20/400, Body body-md 16/24/400 */
--hm-dialog-title-font-size: 20px;
--hm-dialog-title-line-height: 28px;
--hm-dialog-title-font-weight: 700;
--hm-dialog-description-font-size: 14px;
--hm-dialog-description-line-height: 20px;
--hm-dialog-body-font-size: 16px;
--hm-dialog-body-line-height: 24px;
/* ui_viz: dialog.md §1.2 — Description foreground color.fg.secondary is REFERENCE_ONLY / PARTIAL (current style kept, not a Confirmed value) */
--hm-dialog-description-color: var(--hm-sem-fg-secondary);
/* ui_viz: dialog.md §2, §9 — size is fit-content; max width = viewport - 2 x edge; max height also subtracts the app common header / footer.
edge 16px is REVIEW (existing reference value, not a global spacing token). The two app heights are Web Mapping names the consuming screen may set (default 0). */
--hm-dialog-edge: 16px;
--hm-dialog-app-header-height: 0px;
--hm-dialog-app-footer-height: 0px;
}
[data-slot="dialog-content"] {
display: flex;
flex-direction: column;
box-sizing: border-box;
/* Content size up to the max width. Not fit-content: with the shadcn `left: 50%` placement, fit-content only gets half the viewport
as available width, so a text Dialog stopped at 50vw (195px at 390px) instead of 100vw - 2 x edge. max-content + max-width gives the same min(content, max). */
width: max-content;
max-width: calc(100vw - 2 * var(--hm-dialog-edge));
max-height: calc(100dvh - var(--hm-dialog-app-header-height) - var(--hm-dialog-app-footer-height) - 2 * var(--hm-dialog-edge));
/* Centered in the area left by the app header / footer. */
top: calc(var(--hm-dialog-app-header-height) + (100dvh - var(--hm-dialog-app-header-height) - var(--hm-dialog-app-footer-height)) / 2);
padding: 0;
border: var(--hm-dialog-border-width) solid var(--hm-dialog-border-color);
border-radius: var(--hm-dialog-radius);
background: var(--hm-dialog-background);
color: var(--hm-dialog-text);
box-shadow: var(--hm-dialog-shadow);
overflow-wrap: anywhere;
}
/* Header: Title (and optional Description) with the inset divider below it (24px each side, not edge to edge).
The divider is the last pixel of the 16px bottom inset (ui_viz draws it inside the padding), so the border width comes out of the padding. */
[data-slot="dialog-header"] {
flex: none;
gap: var(--hm-dialog-header-gap);
margin-inline: var(--hm-dialog-padding);
padding: var(--hm-dialog-padding) 0 calc(var(--hm-dialog-header-padding-bottom) - var(--hm-dialog-border-width));
border-bottom: var(--hm-dialog-border-width) solid var(--hm-dialog-divider-color);
}
[data-slot="dialog-title"] {
font-size: var(--hm-dialog-title-font-size);
line-height: var(--hm-dialog-title-line-height);
font-weight: var(--hm-dialog-title-font-weight);
color: var(--hm-dialog-text);
}
[data-slot="dialog-description"] {
font-size: var(--hm-dialog-description-font-size);
line-height: var(--hm-dialog-description-line-height);
font-weight: 400;
color: var(--hm-dialog-description-color);
}
/* Body: the only region that scrolls. Wide content (tables) scrolls sideways inside it, never the page. */
[data-slot="dialog-body"] {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
padding: var(--hm-dialog-padding);
font-size: var(--hm-dialog-body-font-size);
line-height: var(--hm-dialog-body-line-height);
font-weight: 400;
}
[data-slot="dialog-body"] > :first-child { margin-top: 0; }
[data-slot="dialog-body"] > :last-child { margin-bottom: 0; }
/* Footer (Actions): trailing edge, cancel first and the action last, no divider above. Stacking on a narrow screen is TBD in ui_viz (shadcn stacking kept). */
[data-slot="dialog-footer"] {
flex: none;
gap: var(--hm-dialog-actions-gap);
margin: 0;
padding: var(--hm-dialog-footer-padding-top) var(--hm-dialog-padding) var(--hm-dialog-padding);
border: 0;
border-radius: 0;
background: transparent;
}
+109
View File
@@ -0,0 +1,109 @@
/* Drawer mapping. ui-viz has no Drawer spec, so these are current HM values built from the HM Semantic tokens.
vaul keeps drag-to-dismiss, snap points, scroll / drag arbitration and body scroll lock; Radix keeps Portal, focus trap,
Escape and ARIA. The handle is vaul's drag affordance (bottom direction); only its look is set here. The Overlay keeps the
shadcn default. No --hm-sheet-* or --hm-dialog-* token is referenced. */
:root, [data-hm-theme] {
--hm-drawer-background: var(--hm-sem-bg-surface);
--hm-drawer-text: var(--hm-sem-fg-primary);
--hm-drawer-border-width: 1px;
--hm-drawer-border-color: var(--hm-sem-stroke-input-default);
--hm-drawer-radius: 8px;
--hm-drawer-shadow: 0 0 12px color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent);
/* Bottom / top: full width, height by content up to max-height. Left / right: fixed width. */
--hm-drawer-max-height: 85dvh;
--hm-drawer-width: 400px;
--hm-drawer-edge-gap: 48px;
--hm-drawer-padding: 24px;
--hm-drawer-header-gap: 8px;
--hm-drawer-footer-gap: 8px;
--hm-drawer-handle-width: 48px;
--hm-drawer-handle-height: 4px;
--hm-drawer-handle-color: var(--hm-sem-stroke-default);
--hm-drawer-handle-margin-top: 12px;
--hm-drawer-title-font-size: 18px;
--hm-drawer-title-line-height: 28px;
--hm-drawer-title-font-weight: 700;
--hm-drawer-description-font-size: 14px;
--hm-drawer-description-line-height: 20px;
--hm-drawer-description-color: var(--hm-sem-fg-secondary);
--hm-drawer-body-font-size: 14px;
--hm-drawer-body-line-height: 20px;
}
[data-slot="drawer-content"] {
box-sizing: border-box;
background: var(--hm-drawer-background);
color: var(--hm-drawer-text);
box-shadow: var(--hm-drawer-shadow);
font-size: var(--hm-drawer-body-font-size);
line-height: var(--hm-drawer-body-line-height);
overflow-wrap: anywhere;
}
[data-slot="drawer-content"][data-vaul-drawer-direction="bottom"] {
max-height: var(--hm-drawer-max-height);
border-top: var(--hm-drawer-border-width) solid var(--hm-drawer-border-color);
border-radius: var(--hm-drawer-radius) var(--hm-drawer-radius) 0 0;
}
[data-slot="drawer-content"][data-vaul-drawer-direction="top"] {
max-height: var(--hm-drawer-max-height);
border-bottom: var(--hm-drawer-border-width) solid var(--hm-drawer-border-color);
border-radius: 0 0 var(--hm-drawer-radius) var(--hm-drawer-radius);
}
[data-slot="drawer-content"][data-vaul-drawer-direction="left"],
[data-slot="drawer-content"][data-vaul-drawer-direction="right"] {
height: 100%;
width: min(var(--hm-drawer-width), calc(100vw - var(--hm-drawer-edge-gap)));
}
[data-slot="drawer-content"][data-vaul-drawer-direction="left"] {
border-right: var(--hm-drawer-border-width) solid var(--hm-drawer-border-color);
border-radius: 0 var(--hm-drawer-radius) var(--hm-drawer-radius) 0;
}
[data-slot="drawer-content"][data-vaul-drawer-direction="right"] {
border-left: var(--hm-drawer-border-width) solid var(--hm-drawer-border-color);
border-radius: var(--hm-drawer-radius) 0 0 var(--hm-drawer-radius);
}
[data-slot="drawer-handle"] {
width: var(--hm-drawer-handle-width);
height: var(--hm-drawer-handle-height);
margin-top: var(--hm-drawer-handle-margin-top);
border-radius: 9999px;
background: var(--hm-drawer-handle-color);
}
[data-slot="drawer-header"] {
flex: none;
gap: var(--hm-drawer-header-gap);
padding: var(--hm-drawer-padding) var(--hm-drawer-padding) 16px;
}
[data-slot="drawer-title"] {
font-size: var(--hm-drawer-title-font-size);
line-height: var(--hm-drawer-title-line-height);
font-weight: var(--hm-drawer-title-font-weight);
color: var(--hm-drawer-text);
}
[data-slot="drawer-description"] {
font-size: var(--hm-drawer-description-font-size);
line-height: var(--hm-drawer-description-line-height);
font-weight: 400;
color: var(--hm-drawer-description-color);
}
/* Body: the only region that scrolls; Header and Footer stay in view. */
[data-slot="drawer-body"] {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
padding: 0 var(--hm-drawer-padding);
}
/* Footer: actions stack full width (touch targets on a narrow screen), the primary action first as in shadcn. */
[data-slot="drawer-footer"] {
flex: none;
gap: var(--hm-drawer-footer-gap);
padding: 16px var(--hm-drawer-padding) var(--hm-drawer-padding);
}
+121
View File
@@ -0,0 +1,121 @@
/* Dropdown Menu mapping. ui-viz has no Dropdown Menu spec (inventory: design pending), so these are current HM values built from
the HM Semantic tokens. Radix state attributes (data-highlighted, data-disabled, data-state) drive the states; no HM state attributes.
Floating surface values repeat in Popover / Tooltip / Hover Card, but each component keeps its own tokens. */
:root, [data-hm-theme] {
--hm-dropdown-menu-background: var(--hm-sem-floating-background);
--hm-dropdown-menu-text: var(--hm-sem-fg-primary);
--hm-dropdown-menu-border-width: var(--hm-sem-floating-border-width);
--hm-dropdown-menu-border-color: var(--hm-sem-floating-border-color);
--hm-dropdown-menu-radius: var(--hm-sem-floating-radius);
--hm-dropdown-menu-shadow: var(--hm-sem-floating-shadow);
--hm-dropdown-menu-padding: 4px;
--hm-dropdown-menu-item-height: 36px;
--hm-dropdown-menu-item-padding-x: 12px;
--hm-dropdown-menu-item-gap: 8px;
--hm-dropdown-menu-item-radius: 2px;
--hm-dropdown-menu-font-size: 14px;
--hm-dropdown-menu-line-height: 20px;
--hm-dropdown-menu-font-weight: 400;
/* Highlighted (hover and keyboard) uses the same face as the Select option: the shadcn accent. */
--hm-dropdown-menu-highlight-background: var(--hm-sem-surface-highlight);
--hm-dropdown-menu-highlight-text: var(--accent-foreground);
--hm-dropdown-menu-destructive-text: var(--hm-sem-action-destructive);
--hm-dropdown-menu-destructive-highlight-background: var(--hm-color-action-destructive-hover);
--hm-dropdown-menu-label-color: var(--hm-sem-fg-secondary);
--hm-dropdown-menu-label-font-size: 12px;
--hm-dropdown-menu-label-line-height: 18px;
--hm-dropdown-menu-label-font-weight: 500;
--hm-dropdown-menu-shortcut-color: var(--hm-sem-fg-secondary);
--hm-dropdown-menu-shortcut-font-size: 12px;
--hm-dropdown-menu-separator-color: var(--hm-sem-stroke-input-default);
/* Check / radio indicator column at the start of the item. */
--hm-dropdown-menu-indicator-size: 16px;
--hm-dropdown-menu-radio-dot-size: 8px;
}
[data-slot="dropdown-menu-content"],
[data-slot="dropdown-menu-sub-content"] {
box-sizing: border-box;
padding: var(--hm-dropdown-menu-padding);
border: var(--hm-dropdown-menu-border-width) solid var(--hm-dropdown-menu-border-color);
border-radius: var(--hm-dropdown-menu-radius);
background: var(--hm-dropdown-menu-background);
color: var(--hm-dropdown-menu-text);
box-shadow: var(--hm-dropdown-menu-shadow);
font-size: var(--hm-dropdown-menu-font-size);
line-height: var(--hm-dropdown-menu-line-height);
font-weight: var(--hm-dropdown-menu-font-weight);
}
[data-slot="dropdown-menu-item"],
[data-slot="dropdown-menu-checkbox-item"],
[data-slot="dropdown-menu-radio-item"],
[data-slot="dropdown-menu-sub-trigger"] {
box-sizing: border-box;
min-height: var(--hm-dropdown-menu-item-height);
gap: var(--hm-dropdown-menu-item-gap);
padding: 0 var(--hm-dropdown-menu-item-padding-x);
border-radius: var(--hm-dropdown-menu-item-radius);
}
/* Checkbox / radio items reserve the indicator column; `inset` aligns plain items to the same text start. */
[data-slot="dropdown-menu-checkbox-item"],
[data-slot="dropdown-menu-radio-item"],
[data-slot="dropdown-menu-item"][data-inset],
[data-slot="dropdown-menu-sub-trigger"][data-inset] {
padding-left: calc(var(--hm-dropdown-menu-item-padding-x) + var(--hm-dropdown-menu-indicator-size) + var(--hm-dropdown-menu-item-gap));
}
[data-slot="dropdown-menu-item-indicator"] {
left: var(--hm-dropdown-menu-item-padding-x);
width: var(--hm-dropdown-menu-indicator-size);
height: var(--hm-dropdown-menu-indicator-size);
}
[data-slot="dropdown-menu-radio-dot"] {
width: var(--hm-dropdown-menu-radio-dot-size);
height: var(--hm-dropdown-menu-radio-dot-size);
}
/* Highlighted: Radix sets data-highlighted for pointer hover and keyboard focus alike. An open sub menu keeps its trigger highlighted. */
[data-slot="dropdown-menu-item"][data-highlighted],
[data-slot="dropdown-menu-checkbox-item"][data-highlighted],
[data-slot="dropdown-menu-radio-item"][data-highlighted],
[data-slot="dropdown-menu-sub-trigger"][data-highlighted],
[data-slot="dropdown-menu-sub-trigger"][data-state="open"] {
background: var(--hm-dropdown-menu-highlight-background);
color: var(--hm-dropdown-menu-highlight-text);
}
[data-slot="dropdown-menu-item"][data-variant="destructive"] {
color: var(--hm-dropdown-menu-destructive-text);
}
[data-slot="dropdown-menu-item"][data-variant="destructive"][data-highlighted] {
background: var(--hm-dropdown-menu-destructive-highlight-background);
color: var(--hm-dropdown-menu-destructive-text);
}
[data-slot="dropdown-menu-label"] {
padding: 6px var(--hm-dropdown-menu-item-padding-x);
color: var(--hm-dropdown-menu-label-color);
font-size: var(--hm-dropdown-menu-label-font-size);
line-height: var(--hm-dropdown-menu-label-line-height);
font-weight: var(--hm-dropdown-menu-label-font-weight);
}
[data-slot="dropdown-menu-label"][data-inset] {
padding-left: calc(var(--hm-dropdown-menu-item-padding-x) + var(--hm-dropdown-menu-indicator-size) + var(--hm-dropdown-menu-item-gap));
}
[data-slot="dropdown-menu-separator"] {
margin: var(--hm-dropdown-menu-padding) calc(-1 * var(--hm-dropdown-menu-padding));
background: var(--hm-dropdown-menu-separator-color);
}
[data-slot="dropdown-menu-shortcut"] {
color: var(--hm-dropdown-menu-shortcut-color);
font-size: var(--hm-dropdown-menu-shortcut-font-size);
letter-spacing: 0.05em;
}
[data-slot="dropdown-menu-item"][data-highlighted] [data-slot="dropdown-menu-shortcut"],
[data-slot="dropdown-menu-checkbox-item"][data-highlighted] [data-slot="dropdown-menu-shortcut"],
[data-slot="dropdown-menu-radio-item"][data-highlighted] [data-slot="dropdown-menu-shortcut"] {
color: inherit;
}
+93
View File
@@ -0,0 +1,93 @@
/* Filter Bar pattern: layout only. The controls inside (Input, Select, Combobox, Date Picker, Button) keep their own component tokens.
Spacing values are pattern-local (no global spacing scale exists yet). */
:root, [data-hm-theme] {
--hm-filter-bar-gap: 8px;
--hm-filter-bar-control-width: 180px;
--hm-filter-bar-search-width: 280px;
--hm-filter-bar-search-icon-size: 16px;
--hm-filter-bar-search-icon-inset: 12px;
--hm-filter-bar-search-icon-gap: 8px;
--hm-filter-bar-search-text-end: 32px; /* clear button 24px + its 6px right inset + 2px */
--hm-filter-bar-search-icon-color: var(--hm-sem-fg-placeholder);
--hm-filter-bar-search-clear-color: var(--hm-sem-fg-secondary);
--hm-filter-bar-narrow-breakpoint: 640px;
}
[data-slot="filter-bar"] {
gap: var(--hm-filter-bar-gap);
}
/* Select / Combobox / Date Picker triggers and a bare Input are w-fit or w-full by themselves; in the bar they get one control width. */
[data-slot="filter-bar"] > :is([data-slot="select-trigger"], [data-slot="combobox-trigger"], [data-slot="date-picker-trigger"], [data-slot="input"]) {
box-sizing: border-box;
width: var(--hm-filter-bar-control-width);
max-width: 100%;
}
[data-slot="filter-bar-search"] {
flex: 0 1 var(--hm-filter-bar-search-width);
min-width: 0;
}
[data-slot="filter-bar-search-icon"] {
top: 50%;
left: var(--hm-filter-bar-search-icon-inset);
width: var(--hm-filter-bar-search-icon-size);
height: var(--hm-filter-bar-search-icon-size);
transform: translateY(-50%);
color: var(--hm-filter-bar-search-icon-color);
}
/* The text starts after the icon (inset + size + gap) and ends before the clear button. Input's Focus / Error / Readonly rules reset
padding-inline at the same specificity and load later, so the search re-applies its insets for those states: Focus / Error give
back the extra 0.5px border exactly as input.css does (the text does not move), and Readonly (no border) keeps the icon clear. */
[data-slot="filter-bar-search"] {
--hm-filter-bar-search-text-start: calc(var(--hm-filter-bar-search-icon-inset) + var(--hm-filter-bar-search-icon-size) + var(--hm-filter-bar-search-icon-gap));
}
[data-slot="filter-bar-search"] [data-slot="input"] {
padding-left: var(--hm-filter-bar-search-text-start);
padding-right: var(--hm-filter-bar-search-text-end);
}
[data-slot="filter-bar-search"] [data-slot="input"]:is(:focus-visible, [aria-invalid="true"]) {
padding-left: calc(var(--hm-filter-bar-search-text-start) - (var(--hm-input-state-border-width) - var(--hm-input-border-width)));
padding-right: calc(var(--hm-filter-bar-search-text-end) - (var(--hm-input-state-border-width) - var(--hm-input-border-width)));
}
[data-slot="filter-bar-search"] [data-slot="input"][readonly],
[data-slot="filter-bar-search"] [data-slot="input"][readonly]:focus-visible {
padding-left: var(--hm-filter-bar-search-text-start);
padding-right: var(--hm-filter-bar-search-text-end);
}
[data-slot="filter-bar-search"] [data-slot="input"]::-webkit-search-cancel-button {
display: none;
}
[data-slot="filter-bar-search-clear"] {
top: 50%;
right: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
transform: translateY(-50%);
color: var(--hm-filter-bar-search-clear-color);
}
[data-slot="filter-bar-search-clear"] svg {
width: 14px;
height: 14px;
}
[data-slot="filter-bar-actions"] {
gap: var(--hm-filter-bar-gap);
margin-left: auto;
}
/* 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"] {
flex: 1 1 100%;
}
[data-slot="filter-bar"] > :is([data-slot="select-trigger"], [data-slot="combobox-trigger"], [data-slot="date-picker-trigger"], [data-slot="input"]) {
flex: 1 1 calc(50% - var(--hm-filter-bar-gap));
width: auto;
min-width: 0;
}
[data-slot="filter-bar-actions"] {
flex: 1 1 100%;
justify-content: flex-end;
}
}
+573
View File
@@ -0,0 +1,573 @@
/* GNB pattern (layout visual reference: D:\컴포넌트\ui-viz Patterns/GNB, implementation: current @hm-design/ui primitives).
The bar is the ui-viz green top header: 70px, vertical brand gradient, brand left, menu centered on the whole bar, text-only profile
right, Beige "current" item joined to the content by two concave corners, White 2-depth panel. The Navigation Menu, Dropdown Menu,
Sheet, Button and Avatar keep their own markup, Radix behavior and tokens; here they are only re-colored INSIDE the GNB scope
(`[data-slot="gnb"] …`), so their own Stories do not change. Spacing values are pattern-local (no global spacing scale exists).
Navigation must use `<NavigationMenu viewport={false}>` so each panel opens under its own trigger at the trigger's width. */
:root, [data-hm-theme] {
/* Exposed for GNB Mode content layouts: the bar height and the content join below it. Mobile uses a shorter bar. */
--hm-gnb-height: 70px;
--hm-gnb-join: 12px;
/* Same stops as the shared Semantic action surface gradient family (tone 1-4); painted over bar + join as one gradient. */
--hm-gnb-background: linear-gradient(180deg, var(--hm-sem-action-surface-stop-0) 0%, var(--hm-sem-action-surface-stop-30) 30%, var(--hm-sem-action-surface-stop-65) 65%, var(--hm-sem-action-surface-stop-100) 100%);
--hm-gnb-padding-x: 28px;
--hm-gnb-column-gap: 24px;
/* Below the z-50 of Popover / Dropdown / Sheet / Dialog (portaled). Applies when `sticky`. */
--hm-gnb-z-index: 40;
--hm-gnb-text: var(--hm-sem-on-action-primary);
--hm-gnb-text-secondary: var(--hm-sem-palette-green-tint);
--hm-gnb-text-stroke-color: color-mix(in srgb, var(--hm-sem-palette-black) 30%, transparent);
--hm-gnb-text-stroke-width: 1px;
--hm-gnb-hover-background: color-mix(in srgb, var(--hm-sem-palette-black) 10%, transparent);
--hm-gnb-focus-outline: 2px solid var(--hm-sem-on-action-primary);
--hm-gnb-brand-gap: 12px;
--hm-gnb-brand-padding-end: 28px;
--hm-gnb-brand-eyebrow-size: 11px;
--hm-gnb-brand-eyebrow-spacing: 0.12em;
--hm-gnb-brand-name-size: 16px;
--hm-gnb-brand-text-gap: 5px;
--hm-gnb-logo-max-height: 100%;
/* Primary menu: 4px above the item, item face 66px = bar 70; first/last item keeps room for the current join curves. */
--hm-gnb-menu-inset-top: 4px;
--hm-gnb-menu-join: 12px;
--hm-gnb-menu-min-width: 112px;
--hm-gnb-menu-padding-x: 16px;
--hm-gnb-menu-gap: 12px;
--hm-gnb-menu-radius: 4px 4px 0 0;
--hm-gnb-menu-font-size: 16px;
--hm-gnb-menu-font-weight: 700;
--hm-gnb-current-background: var(--hm-sem-bg-content);
--hm-gnb-current-text: var(--hm-sem-action-primary);
--hm-gnb-open-background: var(--hm-sem-bg-surface);
--hm-gnb-open-text: var(--hm-sem-action-primary);
/* 2-depth panel: parent width, White, items centered. Radius and shadow are review values in ui-viz. */
--hm-gnb-panel-background: var(--hm-sem-bg-surface);
--hm-gnb-panel-radius: 0 0 8px 8px;
--hm-gnb-panel-shadow: 0 6px 16px color-mix(in srgb, var(--hm-sem-palette-black) 10%, transparent);
--hm-gnb-panel-item-min-height: 56px;
--hm-gnb-panel-item-padding: 16px;
--hm-gnb-panel-item-font-size: 16px;
--hm-gnb-panel-item-line-height: 24px;
--hm-gnb-panel-item-text: var(--hm-sem-fg-primary);
--hm-gnb-panel-item-hover-background: var(--hm-sem-bg-content);
--hm-gnb-panel-item-hover-text: var(--hm-sem-action-primary);
/* Profile (Avatar default size + name and role; the text is hidden on the mobile bar, the Avatar stays). */
--hm-gnb-user-gap: 3px;
--hm-gnb-user-radius: var(--hm-radius-control-md);
--hm-gnb-user-name-size: 16px;
--hm-gnb-user-description-size: 12px;
--hm-gnb-user-text-max-width: 220px;
/* Mobile menu content (inside the HM Sheet, white face). */
--hm-gnb-mobile-link-height: 44px;
--hm-gnb-mobile-link-padding-x: 12px;
--hm-gnb-mobile-link-radius: var(--hm-radius-control-md);
--hm-gnb-mobile-link-font-size: 14px;
--hm-gnb-mobile-link-line-height: 20px;
--hm-gnb-mobile-link-font-weight: 500;
--hm-gnb-mobile-link-text: var(--hm-sem-fg-primary);
--hm-gnb-mobile-link-hover-background: var(--accent);
--hm-gnb-mobile-link-hover-text: var(--accent-foreground);
--hm-gnb-mobile-link-current-background: var(--hm-sem-bg-content);
--hm-gnb-mobile-link-current-text: var(--hm-sem-action-primary);
--hm-gnb-mobile-link-current-font-weight: 700;
--hm-gnb-mobile-group-label-color: var(--hm-sem-fg-secondary);
--hm-gnb-mobile-group-gap: 16px;
--hm-gnb-mobile-nested-gap: 8px;
--hm-gnb-mobile-nested-indent: var(--hm-gnb-mobile-link-padding-x);
--hm-gnb-mobile-focus-ring: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
/* ── Bar ─────────────────────────────────────────────────────────────────────────────────────── */
[data-slot="gnb"] {
position: relative;
box-sizing: border-box;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, auto) minmax(0, 1fr);
column-gap: var(--hm-gnb-column-gap);
align-items: start;
width: 100%;
height: var(--hm-gnb-height);
padding: 0 var(--hm-gnb-padding-x);
background: var(--hm-gnb-background) left top / 100% calc(var(--hm-gnb-height) + var(--hm-gnb-join)) no-repeat;
color: var(--hm-gnb-text);
font-size: 16px;
}
/* Continue the gradient only outside the content's rounded top corners. A full strip would cover the content when the sticky
header creates a higher stacking context. Static and sticky share this geometry (Dashboard Layout contract). */
[data-slot="gnb"]::after {
content: "";
position: absolute;
top: 100%;
left: 0;
right: 0;
height: var(--hm-gnb-join);
background: var(--hm-gnb-background) 0 calc(-1 * var(--hm-gnb-height)) / 100% calc(var(--hm-gnb-height) + var(--hm-gnb-join)) no-repeat;
mask:
radial-gradient(circle at bottom right, transparent var(--hm-gnb-join), #000 var(--hm-gnb-join)) left top / var(--hm-gnb-join) 100% no-repeat,
radial-gradient(circle at bottom left, transparent var(--hm-gnb-join), #000 var(--hm-gnb-join)) right top / var(--hm-gnb-join) 100% no-repeat;
pointer-events: none;
}
[data-slot="gnb"][data-sticky] {
position: sticky;
top: 0;
z-index: var(--hm-gnb-z-index);
}
/* ── Brand: horizontal; logo (img / svg) only when supplied, then the name. Eyebrow + name are optional. ───────── */
[data-slot="gnb-brand"] {
grid-column: 1;
justify-self: start;
box-sizing: border-box;
min-width: 0;
max-width: 100%;
height: calc(var(--hm-gnb-height) - 2px);
padding-right: var(--hm-gnb-brand-padding-end);
gap: var(--hm-gnb-brand-gap);
color: var(--hm-gnb-text);
text-decoration: none;
outline: none;
}
[data-slot="gnb-brand"]:focus-visible {
outline: var(--hm-gnb-focus-outline);
outline-offset: -2px;
}
[data-slot="gnb-brand"] > :is(img, svg) {
flex: 0 1 auto;
min-width: 0;
width: auto;
height: auto;
max-width: 100%;
max-height: var(--hm-gnb-logo-max-height);
}
[data-slot="gnb-brand"] > span {
display: grid;
gap: var(--hm-gnb-brand-text-gap);
flex: 0 1 auto;
min-width: 0;
-webkit-text-stroke: var(--hm-gnb-text-stroke-width) var(--hm-gnb-text-stroke-color);
paint-order: stroke fill;
}
[data-slot="gnb-brand"] :is(small, strong) {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-slot="gnb-brand"] small {
color: var(--hm-gnb-text-secondary);
font-size: var(--hm-gnb-brand-eyebrow-size);
font-weight: 700;
letter-spacing: var(--hm-gnb-brand-eyebrow-spacing);
line-height: 1.2;
}
[data-slot="gnb-brand"] strong {
font-size: var(--hm-gnb-brand-name-size);
font-weight: 700;
}
/* ── Primary navigation: centered on the whole bar by the equal side columns ────────────────────────────────── */
[data-slot="gnb"] [data-slot="gnb-nav"] {
grid-column: 2;
align-items: flex-start;
min-width: 0;
height: var(--hm-gnb-height);
/* `clip` (unlike `hidden`) leaves the vertical axis visible so the open panel below the bar is not cut; too-long labels are cut here. */
overflow-x: clip;
}
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu"] {
flex: 0 1 auto;
align-items: flex-start;
height: 100%;
justify-content: center;
}
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-list"] {
align-items: flex-start;
justify-content: center;
gap: 0;
height: 100%;
padding-inline: var(--hm-gnb-menu-join);
}
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-item"] {
height: 100%;
}
[data-slot="gnb"] [data-slot="gnb-nav"] :is([data-slot="navigation-menu-trigger"], [data-slot="navigation-menu-link"]) {
position: relative;
justify-content: center;
gap: var(--hm-gnb-menu-gap);
min-width: var(--hm-gnb-menu-min-width);
height: calc(var(--hm-gnb-height) - var(--hm-gnb-menu-inset-top));
margin-top: var(--hm-gnb-menu-inset-top);
padding: 0 var(--hm-gnb-menu-padding-x);
border-radius: var(--hm-gnb-menu-radius);
background: transparent;
color: var(--hm-gnb-text);
font-size: var(--hm-gnb-menu-font-size);
font-weight: var(--hm-gnb-menu-font-weight);
white-space: nowrap;
box-shadow: none;
}
/* Arrow optical correction: (12px SVG box - 7px painted width) / 4 = 1.25px.
Keep the total padding, intrinsic trigger width, gap and icon margin unchanged. */
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-trigger"]:has(> [data-slot="navigation-menu-trigger-icon"]) {
padding-left: calc(var(--hm-gnb-menu-padding-x) + 1.25px);
padding-right: calc(var(--hm-gnb-menu-padding-x) - 1.25px);
}
/* Hover / focus (not the current item, not an open parent): Black 10% over the green. */
[data-slot="gnb"] [data-slot="gnb-nav"] :is([data-slot="navigation-menu-trigger"], [data-slot="navigation-menu-link"]):is(:hover, :focus-visible):not([data-active], [aria-current="page"], [data-state="open"]) {
background: var(--hm-gnb-hover-background);
color: var(--hm-gnb-text);
}
[data-slot="gnb"] [data-slot="gnb-nav"] :is([data-slot="navigation-menu-trigger"], [data-slot="navigation-menu-link"]):focus-visible {
outline: var(--hm-gnb-focus-outline);
outline-offset: -2px;
}
/* Current location: Beige face joined to the content below by two concave corners. Hover does not repaint it. Radix `active` on a
Link (`data-active`, `aria-current="page"`); a Trigger carries `aria-current="page"` from the consumer. */
[data-slot="gnb"] [data-slot="gnb-nav"] :is([data-slot="navigation-menu-trigger"], [data-slot="navigation-menu-link"]):is([data-active], [aria-current="page"]) {
z-index: 1;
background: var(--hm-gnb-current-background);
color: var(--hm-gnb-current-text);
}
[data-slot="gnb"] [data-slot="gnb-nav"] :is([data-slot="navigation-menu-trigger"], [data-slot="navigation-menu-link"]):is([data-active], [aria-current="page"]):focus-visible {
outline-color: var(--hm-gnb-current-text);
}
[data-slot="gnb"] [data-slot="gnb-nav"] :is([data-slot="navigation-menu-trigger"], [data-slot="navigation-menu-link"]):is([data-active], [aria-current="page"])::before,
[data-slot="gnb"] [data-slot="gnb-nav"] :is([data-slot="navigation-menu-trigger"], [data-slot="navigation-menu-link"]):is([data-active], [aria-current="page"])::after {
content: "";
position: absolute;
bottom: 0;
width: var(--hm-gnb-menu-join);
height: var(--hm-gnb-menu-join);
pointer-events: none;
}
[data-slot="gnb"] [data-slot="gnb-nav"] :is([data-slot="navigation-menu-trigger"], [data-slot="navigation-menu-link"]):is([data-active], [aria-current="page"])::before {
right: 100%;
background: radial-gradient(circle at top left, transparent calc(var(--hm-gnb-menu-join) - 0.5px), var(--hm-gnb-current-background) var(--hm-gnb-menu-join));
}
[data-slot="gnb"] [data-slot="gnb-nav"] :is([data-slot="navigation-menu-trigger"], [data-slot="navigation-menu-link"]):is([data-active], [aria-current="page"])::after {
left: 100%;
background: radial-gradient(circle at top right, transparent calc(var(--hm-gnb-menu-join) - 0.5px), var(--hm-gnb-current-background) var(--hm-gnb-menu-join));
}
/* Open parent: reuse the current concave joins with the panel's surface.
alignPanels supplies each side independently after viewport clamping. */
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-trigger"][data-state="open"] {
z-index: 3;
background: var(--hm-gnb-open-background);
color: var(--hm-gnb-open-text);
}
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-trigger"][data-state="open"]::before,
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-trigger"][data-state="open"]::after {
content: "";
display: block;
position: absolute;
bottom: 0;
height: var(--hm-gnb-menu-join);
pointer-events: none;
}
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-trigger"][data-state="open"]::before {
right: 100%;
width: min(var(--hm-gnb-panel-left-extension, 0px), var(--hm-gnb-menu-join));
background: radial-gradient(circle at top left, transparent calc(var(--hm-gnb-menu-join) - 0.5px), var(--hm-gnb-panel-background) var(--hm-gnb-menu-join)) right bottom / var(--hm-gnb-menu-join) var(--hm-gnb-menu-join) no-repeat;
}
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-trigger"][data-state="open"]::after {
left: 100%;
width: min(var(--hm-gnb-panel-right-extension, 0px), var(--hm-gnb-menu-join));
background: radial-gradient(circle at top right, transparent calc(var(--hm-gnb-menu-join) - 0.5px), var(--hm-gnb-panel-background) var(--hm-gnb-menu-join)) left bottom / var(--hm-gnb-menu-join) var(--hm-gnb-menu-join) no-repeat;
}
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-trigger"][data-state="open"]:focus-visible {
outline-color: var(--hm-gnb-open-text);
}
/* ── 2-depth panel (viewport={false}): under its own trigger, same width, White, centered items ─────────────── */
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-content"] {
position: absolute;
z-index: 2;
top: 100%;
left: 0;
box-sizing: border-box;
width: 100%;
min-width: 0;
margin: 0;
padding: 0;
border: 0;
border-radius: var(--hm-gnb-panel-radius);
background: var(--hm-gnb-panel-background);
color: var(--hm-gnb-panel-item-text);
box-shadow: var(--hm-gnb-panel-shadow);
}
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-content"] > ul {
display: grid;
width: auto;
margin: 0;
padding: 0;
gap: 0;
list-style: none;
}
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-content"] [data-slot="navigation-menu-link"] {
flex-direction: row;
justify-content: center;
align-items: center;
box-sizing: border-box;
width: 100%;
height: auto;
min-height: var(--hm-gnb-panel-item-min-height);
margin: 0;
padding: var(--hm-gnb-panel-item-padding);
border-radius: 0;
background: transparent;
color: var(--hm-gnb-panel-item-text);
font-size: var(--hm-gnb-panel-item-font-size);
line-height: var(--hm-gnb-panel-item-line-height);
font-weight: 700;
text-align: center;
white-space: normal;
word-break: keep-all;
overflow-wrap: anywhere;
}
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-content"] [data-slot="navigation-menu-link"]:is(:hover, :focus-visible, [data-active]) {
background: var(--hm-gnb-panel-item-hover-background);
color: var(--hm-gnb-panel-item-hover-text);
outline: none;
}
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-content"] [data-slot="navigation-menu-link"]:focus-visible {
outline: 2px solid var(--hm-gnb-panel-item-hover-text);
outline-offset: -2px;
}
/* ── Utilities and profile ──────────────────────────────────────────────────────────────────────────────── */
[data-slot="gnb-utilities"] {
grid-column: 3;
justify-self: end;
min-width: 0;
height: calc(var(--hm-gnb-height) - 2px);
gap: 4px;
}
/* Optional icon actions (search, notification...) are HM ghost Buttons; on the green bar they are white with the Black 10% hover. */
[data-slot="gnb-utilities"] [data-slot="button"][data-variant="ghost"] {
color: var(--hm-gnb-text);
}
[data-slot="gnb-utilities"] [data-slot="button"][data-variant="ghost"]:is(:hover, [aria-expanded="true"]) {
background: var(--hm-gnb-hover-background);
color: var(--hm-gnb-text);
}
[data-slot="gnb-utilities"] [data-slot="button"][data-variant="ghost"]:focus-visible {
border-color: transparent;
outline: var(--hm-gnb-focus-outline);
outline-offset: -2px;
box-shadow: none;
}
[data-slot="gnb-user"] {
box-sizing: border-box;
min-width: 0;
max-width: 100%;
padding: 0 8px;
gap: 10px;
border: 0;
border-radius: var(--hm-gnb-user-radius);
background: transparent;
color: var(--hm-gnb-text);
text-align: left;
cursor: pointer;
outline: none;
}
[data-slot="gnb-user"]:is(:hover, [data-state="open"]) {
background: var(--hm-gnb-hover-background);
}
[data-slot="gnb-user"]:focus-visible {
outline: var(--hm-gnb-focus-outline);
outline-offset: -2px;
}
[data-slot="gnb-user-text"] {
display: grid;
gap: var(--hm-gnb-user-gap);
min-width: 0;
max-width: var(--hm-gnb-user-text-max-width);
padding-block: 4px;
}
[data-slot="gnb-user-name"],
[data-slot="gnb-user-description"] {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
-webkit-text-stroke: var(--hm-gnb-text-stroke-width) var(--hm-gnb-text-stroke-color);
paint-order: stroke fill;
}
[data-slot="gnb-user-name"] {
font-size: var(--hm-gnb-user-name-size);
font-weight: 700;
}
[data-slot="gnb-user-description"] {
color: var(--hm-gnb-text-secondary);
font-size: var(--hm-gnb-user-description-size);
}
/* ── Mobile menu content (inside the HM Sheet): destinations as a plain link list ─────────────────────────── */
[data-slot="gnb-mobile-nav"] {
display: grid;
gap: var(--hm-gnb-mobile-group-gap);
padding-bottom: 16px;
}
[data-slot="gnb-mobile-group-label"] {
margin: 0;
padding: 0 var(--hm-gnb-mobile-link-padding-x) 4px;
color: var(--hm-gnb-mobile-group-label-color);
font-size: 12px;
line-height: 18px;
font-weight: 500;
}
/* A group inside a group (a deeper local level, e.g. the LNB 3-depth): one link padding further in, with room above its title. */
[data-slot="gnb-mobile-group"] [data-slot="gnb-mobile-group"] {
margin-top: var(--hm-gnb-mobile-nested-gap);
padding-left: var(--hm-gnb-mobile-nested-indent);
}
[data-slot="gnb-mobile-link"] {
box-sizing: border-box;
min-height: var(--hm-gnb-mobile-link-height);
padding: 0 var(--hm-gnb-mobile-link-padding-x);
border-radius: var(--hm-gnb-mobile-link-radius);
color: var(--hm-gnb-mobile-link-text);
font-size: var(--hm-gnb-mobile-link-font-size);
line-height: var(--hm-gnb-mobile-link-line-height);
font-weight: var(--hm-gnb-mobile-link-font-weight);
text-decoration: none;
overflow-wrap: anywhere;
outline: none;
}
[data-slot="gnb-mobile-link"]:is(:hover, :focus-visible) {
background: var(--hm-gnb-mobile-link-hover-background);
color: var(--hm-gnb-mobile-link-hover-text);
}
[data-slot="gnb-mobile-link"]:focus-visible {
box-shadow: var(--hm-gnb-mobile-focus-ring);
}
[data-slot="gnb-mobile-link"][aria-current="page"] {
background: var(--hm-gnb-mobile-link-current-background);
color: var(--hm-gnb-mobile-link-current-text);
font-weight: var(--hm-gnb-mobile-link-current-font-weight);
}
/* Desktop panels grow to their longest label. The GNB region measures only the
viewport-edge correction; extreme labels scroll within the bounded panel. */
@media (min-width: 1024px) {
[data-slot="gnb"] [data-slot="gnb-nav"] {
overflow-x: visible;
}
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-content"] {
left: var(--hm-gnb-panel-offset-x, 0px);
width: max-content;
min-width: 100%;
max-width: calc(100vw - 2 * var(--hm-gnb-padding-x));
overflow-x: auto;
}
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-content"] > ul {
min-width: max-content;
}
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-content"] [data-slot="navigation-menu-link"] {
white-space: nowrap;
overflow-wrap: normal;
}
/* The concave current-page joins belong to 1Depth only, not panel rows. */
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-content"] [data-slot="navigation-menu-link"]::before,
[data-slot="gnb"] [data-slot="gnb-nav"] [data-slot="navigation-menu-content"] [data-slot="navigation-menu-link"]::after {
display: none;
}
}
/* ── Mobile bar: below 1024px (Tailwind `lg`). Menu → hamburger + Sheet; profile → Avatar only. Keep this block last. ──────── */
@media (max-width: 1023px) {
:root, [data-hm-theme] {
--hm-gnb-height: 56px;
--hm-gnb-padding-x: 16px;
}
[data-slot="gnb"] {
display: flex;
align-items: center;
gap: 8px;
}
[data-slot="gnb"] [data-slot="gnb-nav"] {
display: none;
}
[data-slot="gnb-brand"] {
flex: 0 1 auto;
height: auto;
padding-right: 0;
}
[data-slot="gnb-brand"] small {
display: none;
}
[data-slot="gnb-utilities"] {
flex: none;
height: auto;
margin-left: auto;
}
[data-slot="gnb-user"] {
padding: 0 4px;
}
[data-slot="gnb-user-text"] {
display: none;
}
/* The hamburger is a ghost Button: white on the green bar. */
[data-slot="gnb"] > [data-slot="button"][data-variant="ghost"] {
color: var(--hm-gnb-text);
}
[data-slot="gnb"] > [data-slot="button"][data-variant="ghost"]:is(:hover, [aria-expanded="true"]) {
background: var(--hm-gnb-hover-background);
color: var(--hm-gnb-text);
}
[data-slot="gnb"] > [data-slot="button"][data-variant="ghost"]:focus-visible {
border-color: transparent;
outline: var(--hm-gnb-focus-outline);
outline-offset: -2px;
box-shadow: none;
}
}
/* Opt-in GNB ONLY composition. Reuse LNB's DOM, tokens and responsive menu unchanged.
These layout hooks ship with @hm-design/ui/theme.css; no Story-only stylesheet is required.
The consumer provides the frame height and content padding. */
[data-hm-gnb-layout="sidebar"] {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
min-width: 0;
min-height: 0;
}
[data-hm-gnb-layout="sidebar"] > [data-slot="gnb"] {
z-index: var(--hm-gnb-z-index);
}
[data-hm-gnb-layout="sidebar"] > [data-hm-gnb-body] {
position: relative;
display: grid;
grid-template-columns: var(--hm-lnb-width) minmax(0, 1fr);
min-width: 0;
min-height: 0;
background: var(--hm-sem-palette-green-light-2);
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);
}
[data-hm-gnb-layout="sidebar"] > [data-hm-gnb-body] > [data-hm-gnb-main] {
position: relative;
grid-column: 2;
min-width: 0;
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);
}
@media (max-width: 1023px) {
[data-hm-gnb-layout="sidebar"] > [data-hm-gnb-body] {
grid-template-columns: minmax(0, 1fr);
}
[data-hm-gnb-layout="sidebar"] > [data-hm-gnb-body] > [data-hm-gnb-main] {
grid-column: 1;
border-radius: var(--hm-gnb-join) var(--hm-gnb-join) 0 0;
}
}
+31
View File
@@ -0,0 +1,31 @@
/* Hover Card mapping. ui-viz has no Hover Card spec, so these are current HM values from the HM Semantic tokens.
A richer preview than Tooltip (title / description / metadata); it is wider than Popover so a preview reads in one block. */
:root, [data-hm-theme] {
--hm-hover-card-background: var(--hm-sem-floating-background);
--hm-hover-card-text: var(--hm-sem-fg-primary);
--hm-hover-card-border-width: var(--hm-sem-floating-border-width);
--hm-hover-card-border-color: var(--hm-sem-floating-border-color);
--hm-hover-card-radius: var(--hm-sem-floating-radius);
--hm-hover-card-shadow: var(--hm-sem-floating-shadow);
--hm-hover-card-padding: 16px;
--hm-hover-card-width: 320px;
--hm-hover-card-font-size: 14px;
--hm-hover-card-line-height: 20px;
--hm-hover-card-font-weight: 400;
}
[data-slot="hover-card-content"] {
box-sizing: border-box;
width: var(--hm-hover-card-width);
max-width: calc(100vw - 16px);
padding: var(--hm-hover-card-padding);
border: var(--hm-hover-card-border-width) solid var(--hm-hover-card-border-color);
border-radius: var(--hm-hover-card-radius);
background: var(--hm-hover-card-background);
color: var(--hm-hover-card-text);
box-shadow: var(--hm-hover-card-shadow);
font-size: var(--hm-hover-card-font-size);
line-height: var(--hm-hover-card-line-height);
font-weight: var(--hm-hover-card-font-weight);
overflow-wrap: anywhere;
}
+5
View File
@@ -0,0 +1,5 @@
export { runtimeThemeContract, defaultRuntimeTheme } from "./contract"
export type { RuntimeTheme, RuntimeThemeToken } from "./contract"
export { applyRuntimeTheme, resetRuntimeTheme, isRuntimeTheme } from "./runtime"
export { fetchRuntimeTheme, resolveRuntimeTheme, loadRuntimeTheme } from "./loader"
export type { RuntimeThemeLoadOptions } from "./loader"
+107
View File
@@ -0,0 +1,107 @@
/* Input mapping: the control only. Label, Required star and Helper/Error message are consumer composition, not part of Input.
States use native selectors (:focus-visible, [aria-invalid="true"], :disabled, [readonly]); size uses data-size. */
:root, [data-hm-theme] {
/* ui_viz: docs/components/input.md §24, §16 — Confirmed (size, padding-x). Not aliased to Button or the global control height. */
--hm-input-height-sm: 28px;
--hm-input-height-md: 34px;
--hm-input-height-lg: 42px;
--hm-input-padding-x-sm: 10px;
--hm-input-padding-x-md: 12px;
--hm-input-padding-x-lg: 12px;
/* ui_viz: docs/components/input.md §9, §21 INPUT_VALUE_ROLE_FOLLOWS_SIZE — owner decision (value and placeholder per size) */
--hm-input-font-size-sm: 12px;
--hm-input-line-height-sm: 18px;
--hm-input-font-size-md: 14px;
--hm-input-line-height-md: 20px;
--hm-input-font-size-lg: 16px;
--hm-input-line-height-lg: 24px;
--hm-input-font-weight: 400;
/* ui_viz: docs/components/input.md §10, §11, §1.1 D-005 — Confirmed */
--hm-input-radius: var(--hm-radius-input);
--hm-input-border-width: 1px;
--hm-input-state-border-width: 1.5px;
--hm-input-background: var(--hm-sem-bg-surface);
--hm-input-text: var(--hm-sem-fg-primary);
--hm-input-placeholder: var(--hm-sem-fg-placeholder);
--hm-input-border-color: var(--hm-sem-stroke-input-default);
--hm-input-focus-border-color: var(--hm-sem-stroke-input-focus);
--hm-input-error-border-color: var(--hm-sem-action-destructive);
}
/* Large is the default when size is omitted. Height is a min-height, so the control keeps one line of text centered. */
[data-slot="input"] {
--hm-input-height: var(--hm-input-height-lg);
--hm-input-padding-x: var(--hm-input-padding-x-lg);
--hm-input-font-size: var(--hm-input-font-size-lg);
--hm-input-line-height: var(--hm-input-line-height-lg);
box-sizing: border-box;
height: auto;
min-height: var(--hm-input-height);
padding: 0 var(--hm-input-padding-x);
border: var(--hm-input-border-width) solid var(--hm-input-border-color);
border-radius: var(--hm-input-radius);
background: var(--hm-input-background);
color: var(--hm-input-text);
font-size: var(--hm-input-font-size);
line-height: var(--hm-input-line-height);
font-weight: var(--hm-input-font-weight);
box-shadow: none;
}
[data-slot="input"][data-size="medium"] {
--hm-input-height: var(--hm-input-height-md);
--hm-input-padding-x: var(--hm-input-padding-x-md);
--hm-input-font-size: var(--hm-input-font-size-md);
--hm-input-line-height: var(--hm-input-line-height-md);
}
[data-slot="input"][data-size="small"] {
--hm-input-height: var(--hm-input-height-sm);
--hm-input-padding-x: var(--hm-input-padding-x-sm);
--hm-input-font-size: var(--hm-input-font-size-sm);
--hm-input-line-height: var(--hm-input-line-height-sm);
}
[data-slot="input"]::placeholder {
color: var(--hm-input-placeholder);
opacity: 1;
}
/* Focus: the border alone marks it (1.5px, no shadow or glow). Padding gives back the extra 0.5px so the value does not move. */
[data-slot="input"]:focus-visible {
outline: none;
border-width: var(--hm-input-state-border-width);
border-color: var(--hm-input-focus-border-color);
padding-inline: calc(var(--hm-input-padding-x) - (var(--hm-input-state-border-width) - var(--hm-input-border-width)));
}
/* Invalid owns the border; keyboard focus adds the HM 3px / 50% semantic ring independently. */
[data-slot="input"][aria-invalid="true"] {
border-width: var(--hm-input-state-border-width);
border-color: var(--hm-input-error-border-color);
padding-inline: calc(var(--hm-input-padding-x) - (var(--hm-input-state-border-width) - var(--hm-input-border-width)));
}
[data-slot="input"][aria-invalid="true"]:focus-visible {
border-color: var(--hm-input-error-border-color);
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
/* Disabled keeps the white face. Dimming (opacity 0.6) is set in ui/input.tsx: 2026-10-02 user decision, Input and Select only, not a global Disabled token. */
[data-slot="input"]:disabled {
background: var(--hm-input-background);
}
/* Readonly: no box, text only, not dimmed. Always Large metrics, whatever the size. */
[data-slot="input"][readonly] {
--hm-input-height: var(--hm-input-height-lg);
--hm-input-font-size: var(--hm-input-font-size-lg);
--hm-input-line-height: var(--hm-input-line-height-lg);
border-width: 0;
padding-inline: 0;
background: transparent;
}
[data-slot="input"][readonly]:focus-visible {
border-width: 0;
padding-inline: 0;
outline: revert;
}
+17
View File
@@ -0,0 +1,17 @@
/* Label mapping: typography and color only. Required star, Helper and Error message are consumer composition, not part of Label.
ui_viz has no standalone Label spec; the Field Label role is owned by Input / Textarea (docs/components/input.md §9, §10). */
:root, [data-hm-theme] {
/* ui_viz: docs/components/input.md §9 Field Label → label-md 14/20/500 (CANONICAL_OVERRIDE of the 13/700 Source); textarea.md §2 */
--hm-label-font-size: 14px;
--hm-label-line-height: 20px;
--hm-label-font-weight: 500;
/* ui_viz: docs/components/input.md §10, §16 — color.fg.secondary, Confirmed */
--hm-label-text: var(--hm-sem-fg-secondary);
}
[data-slot="label"] {
color: var(--hm-label-text);
font-size: var(--hm-label-font-size);
line-height: var(--hm-label-line-height);
font-weight: var(--hm-label-font-weight);
}
+531
View File
@@ -0,0 +1,531 @@
/* 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. */
: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;
--hm-lnb-join: 18px;
--hm-lnb-background: linear-gradient(90deg, var(--hm-sem-action-primary) 0%, var(--hm-sem-palette-green-light-1) 30%, var(--hm-sem-palette-green-light-2) 50%, var(--hm-sem-palette-green-light-3) 90%, var(--hm-sem-palette-green-light-2) 100%);
--hm-lnb-background-end: var(--hm-sem-palette-green-light-2);
--hm-lnb-padding-top: 28px;
--hm-lnb-padding-bottom: 22px;
--hm-lnb-inset-start: 20px;
--hm-lnb-inset-end: 20px;
/* Menu top (ui-viz 20 under the brand). Padding inside the scroll region, so the scroll starts right under the brand; keep it at
least `--hm-lnb-join` so the curve above a current first item is not clipped (the bottom padding is the join for the same reason). */
--hm-lnb-nav-padding-top: 20px;
--hm-lnb-text: var(--hm-sem-on-action-primary);
--hm-lnb-text-secondary: var(--hm-sem-palette-green-tint);
--hm-lnb-text-stroke-color: color-mix(in srgb, var(--hm-sem-palette-black) 30%, transparent);
--hm-lnb-focus-outline: 2px solid var(--hm-sem-on-action-primary);
--hm-lnb-brand-padding-x: 30px;
--hm-lnb-brand-padding-bottom: 22px;
--hm-lnb-brand-gap: 12px;
--hm-lnb-brand-eyebrow-size: 11px;
--hm-lnb-brand-eyebrow-spacing: 0.12em;
--hm-lnb-brand-name-size: 17px;
--hm-lnb-brand-text-gap: 5px;
--hm-lnb-item-min-height: 56px;
--hm-lnb-item-padding-start: 18px;
--hm-lnb-item-gap: 12px;
--hm-lnb-item-font-size: 18px;
--hm-lnb-item-line-height: 28px;
--hm-lnb-item-font-weight: 700;
--hm-lnb-item-radius: 4px 0 0 4px;
--hm-lnb-item-icon-size: 20px;
--hm-lnb-item-icon-opacity: 0.5;
--hm-lnb-item-icon-stroke: 1.7;
--hm-lnb-item-text-stroke-width: 1.5px;
--hm-lnb-item-text-shadow: 1px 2px 1px var(--hm-lnb-text-stroke-color);
--hm-lnb-hover-background: color-mix(in srgb, var(--hm-sem-palette-black) 10%, transparent);
--hm-lnb-current-background: var(--hm-sem-bg-content);
--hm-lnb-current-text: var(--hm-sem-action-primary);
--hm-lnb-current-radius: 8px 0 0 8px;
/* Inset depth of the current face: left edge, top, bottom; the last quarter fades so the content side stays open. */
--hm-lnb-current-depth:
linear-gradient(to right, color-mix(in srgb, var(--hm-sem-palette-black) 30%, transparent), transparent 6px) 0 0 / 75% 100% no-repeat,
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-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);
--hm-lnb-group-arrow-size: 18px;
--hm-lnb-subitem-padding-start: 68px;
/* Align the 2-depth link surface with the 1-depth group text; retain its existing text inset. */
--hm-lnb-subitem-surface-inset: calc(var(--hm-lnb-item-padding-start) + var(--hm-lnb-group-arrow-size) + var(--hm-lnb-item-gap));
--hm-lnb-group-padding-bottom: 8px;
/* 3-depth: the items of an `LNBGroup` nested in a group. Same 18px; the hierarchy is the extra 20px indent and the connector
(a vertical line at the 2-depth text start, a 12px branch to each row's middle, 8px to the text; it ends at the last row). */
--hm-lnb-third-min-height: 48px;
--hm-lnb-third-padding-block: 12px;
--hm-lnb-third-padding-start: 88px;
--hm-lnb-third-line-height: 24px;
--hm-lnb-third-font-weight: 500;
--hm-lnb-third-current-font-weight: 700;
--hm-lnb-third-connector-x: var(--hm-lnb-subitem-padding-start);
--hm-lnb-third-connector-length: 12px;
--hm-lnb-third-connector-width: 1px;
--hm-lnb-third-connector-color: color-mix(in srgb, var(--hm-sem-palette-green-tint) 40%, transparent);
--hm-lnb-third-connector-current-color: color-mix(in srgb, var(--hm-sem-palette-green-light-2) 40%, transparent);
--hm-lnb-profile-padding-x: 30px;
--hm-lnb-profile-padding-top: 22px;
--hm-lnb-profile-gap: 12px;
--hm-lnb-profile-name-size: 16px;
--hm-lnb-profile-description-size: 12px;
}
[data-slot="lnb"] {
position: relative;
box-sizing: border-box;
width: var(--hm-lnb-width);
min-height: 100%;
padding: var(--hm-lnb-padding-top) 0 var(--hm-lnb-padding-bottom);
background: var(--hm-lnb-background);
color: var(--hm-lnb-text);
}
/* The surface keeps going for `--hm-lnb-join` behind the rounded left corners of the content next to the column. */
[data-slot="lnb"]::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 100%;
width: var(--hm-lnb-join);
background: var(--hm-lnb-background-end);
pointer-events: none;
}
/* Brand (vertical) and profile: text with the dark text border. */
[data-slot="lnb-brand"] {
flex: none;
gap: var(--hm-lnb-brand-gap);
align-items: flex-start;
padding: 0 var(--hm-lnb-brand-padding-x) var(--hm-lnb-brand-padding-bottom);
}
[data-slot="lnb-brand"] > :is(img, svg) {
flex: 0 0 auto;
max-width: 100%;
height: auto;
}
[data-slot="lnb-brand"] > span {
display: grid;
gap: var(--hm-lnb-brand-text-gap);
min-width: 0;
max-width: 100%;
}
[data-slot="lnb-brand"] :is(small, strong),
[data-slot="lnb-profile"] :is(strong, p) {
-webkit-text-stroke: 1px var(--hm-lnb-text-stroke-color);
paint-order: stroke fill;
}
[data-slot="lnb-brand"] small {
color: var(--hm-lnb-text-secondary);
font-size: var(--hm-lnb-brand-eyebrow-size);
font-weight: 700;
letter-spacing: var(--hm-lnb-brand-eyebrow-spacing);
line-height: 1.2;
}
[data-slot="lnb-brand"] strong {
font-size: var(--hm-lnb-brand-name-size);
font-weight: 700;
overflow-wrap: anywhere;
}
[data-slot="lnb"] > [data-slot="lnb-profile"] {
display: flex;
flex: none;
align-items: center;
gap: var(--hm-lnb-profile-gap);
align-self: flex-start;
width: fit-content;
/* GNB profile's 8px inner hit padding; the existing 30px inset still places the Avatar. */
max-width: calc(100% - 2 * (var(--hm-lnb-profile-padding-x) - 8px));
margin-top: auto;
margin-inline: calc(var(--hm-lnb-profile-padding-x) - 8px);
min-height: var(--hm-lnb-third-min-height);
padding: 0 8px;
border: 0;
background: transparent;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
[data-slot="lnb"] > [data-slot="lnb-profile"]:is(:hover, [data-state="open"]) {
background: var(--hm-lnb-hover-background);
}
[data-slot="lnb"] > [data-slot="lnb-profile"]:focus-visible {
outline: var(--hm-lnb-focus-outline);
outline-offset: -2px;
}
[data-slot="lnb-profile-text"] {
min-width: 0;
}
[data-slot="lnb-profile"] strong {
display: block;
font-size: var(--hm-lnb-profile-name-size);
overflow-wrap: anywhere;
}
[data-slot="lnb-profile"] p {
margin: 4px 0 0;
color: var(--hm-lnb-text-secondary);
font-size: var(--hm-lnb-profile-description-size);
overflow-wrap: anywhere;
}
/* Menu region and the only scroll container of the column (brand and profile are `flex: none`; the column itself never scrolls). In a
column of fixed height it fills the space between them (`min-height: 0` lets it shrink below the menu height), so it starts at the
brand bottom and ends at the profile top. It is `inset-end + 1px` wider than the column's inner area so a current item can overlap
the content by 1px; vertical padding keeps the join curves of the first / last item from being clipped. `direction: rtl` only moves
the native scrollbar to the left edge, so it never takes width from the right side where the current item joins the content; the
menu itself is LTR again (direct children, see "Menu rows" below). */
[data-slot="lnb-nav"] {
flex: 1 1 auto;
min-height: 0;
direction: rtl;
margin: 0 -1px 0 0;
padding: var(--hm-lnb-nav-padding-top) 0 var(--hm-lnb-join) var(--hm-lnb-inset-start);
overflow-x: clip;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--hm-sem-palette-white) 35%, transparent) transparent;
}
/* Layout owns the old top spacing only when this column owns a Profile. */
[data-slot="lnb"]:has(> [data-slot="lnb-profile"]) > [data-slot="lnb-nav"] {
margin-bottom: var(--hm-lnb-profile-padding-top);
}
/* ── Item ───────────────────────────────────────────────────────────────────────────────────────────── */
[data-slot="lnb-item"],
[data-slot="lnb-group-trigger"] {
position: relative;
box-sizing: border-box;
gap: var(--hm-lnb-item-gap);
width: calc(100% - 1px);
min-height: var(--hm-lnb-item-min-height);
padding: 14px calc(var(--hm-lnb-inset-end) + 12px) 14px var(--hm-lnb-item-padding-start);
border: 0;
border-radius: var(--hm-lnb-item-radius);
background: transparent;
color: var(--hm-lnb-text);
font-family: inherit;
font-size: var(--hm-lnb-item-font-size);
line-height: var(--hm-lnb-item-line-height);
font-weight: var(--hm-lnb-item-font-weight);
text-align: left;
text-decoration: none;
word-break: keep-all;
overflow-wrap: anywhere;
cursor: pointer;
outline: none;
-webkit-text-stroke: var(--hm-lnb-item-text-stroke-width) var(--hm-lnb-text-stroke-color);
paint-order: stroke fill;
text-shadow: var(--hm-lnb-item-text-shadow);
}
[data-slot="lnb-item"] > svg:not([data-slot]) {
flex: 0 0 auto;
width: var(--hm-lnb-item-icon-size);
height: var(--hm-lnb-item-icon-size);
opacity: var(--hm-lnb-item-icon-opacity);
stroke-width: var(--hm-lnb-item-icon-stroke);
}
[data-slot="lnb-item"]:not([aria-current="page"]):hover,
[data-slot="lnb-group-trigger"]:hover {
background: var(--hm-lnb-hover-background);
}
[data-slot="lnb-item"]:focus-visible,
[data-slot="lnb-group-trigger"]:focus-visible {
outline: var(--hm-lnb-focus-outline);
outline-offset: -2px;
}
/* Current: Beige face, left corners 8, runs 1px into the content, concave corners above and below, inset depth. No text effects. */
[data-slot="lnb-item"][aria-current="page"] {
z-index: 1;
width: 100%;
background: var(--hm-lnb-current-depth), var(--hm-lnb-current-background);
border-radius: var(--hm-lnb-current-radius);
color: var(--hm-lnb-current-text);
-webkit-text-stroke: 0;
text-shadow: none;
}
[data-slot="lnb-item"][aria-current="page"]:focus-visible {
outline-color: var(--hm-lnb-current-text);
}
[data-slot="lnb-item"][aria-current="page"]::before,
[data-slot="lnb-item"][aria-current="page"]::after {
content: "";
position: absolute;
right: 1px;
width: var(--hm-lnb-join);
height: var(--hm-lnb-join);
pointer-events: none;
}
[data-slot="lnb-item"][aria-current="page"]::before {
bottom: 100%;
background: radial-gradient(circle at top left, transparent calc(var(--hm-lnb-join) - 0.5px), var(--hm-lnb-current-background) var(--hm-lnb-join));
}
[data-slot="lnb-item"][aria-current="page"]::after {
top: 100%;
background: radial-gradient(circle at bottom left, transparent calc(var(--hm-lnb-join) - 0.5px), var(--hm-lnb-current-background) var(--hm-lnb-join));
}
/* ── Group (HM Collapsible) ─────────────────────────────────────────────────────────────────────────── */
[data-slot="lnb-group"] {
position: relative;
width: calc(100% - 1px);
border-radius: var(--hm-lnb-group-radius);
}
[data-slot="lnb-group"][data-state="open"] {
background: var(--hm-lnb-group-open-background);
}
[data-slot="lnb-group"][data-state="open"]::before {
content: "";
position: absolute;
inset: 0;
border: var(--hm-lnb-group-open-line);
border-right: 0;
border-radius: inherit;
pointer-events: none;
}
[data-slot="lnb-group-trigger"] {
width: 100%;
border-radius: var(--hm-lnb-group-radius);
}
[data-slot="lnb-group"][data-state="open"] > [data-slot="lnb-group-trigger"]:hover {
border-radius: 8px 0 0 0;
}
[data-slot="lnb-group"][data-current] > [data-slot="lnb-group-trigger"] {
color: var(--hm-lnb-group-current-text);
}
[data-slot="lnb-group-arrow"] {
flex: 0 0 auto;
width: var(--hm-lnb-group-arrow-size);
height: var(--hm-lnb-group-arrow-size);
stroke-width: 2;
filter: drop-shadow(var(--hm-lnb-item-text-shadow));
transition: transform 0.15s ease;
}
[data-slot="lnb-group-trigger"][data-state="open"] > [data-slot="lnb-group-arrow"] {
transform: rotate(90deg);
}
[data-slot="lnb-group-content"][data-state="open"] {
display: grid;
padding-bottom: var(--hm-lnb-group-padding-bottom);
}
[data-slot="lnb-group-content"] [data-slot="lnb-item"] {
width: 100%;
padding-left: var(--hm-lnb-subitem-padding-start);
border-radius: 0;
}
[data-slot="lnb-group-content"] [data-slot="lnb-item"]:not([aria-current="page"]):hover {
border-radius: var(--hm-lnb-item-radius);
}
[data-slot="lnb-group-content"] [data-slot="lnb-item"][aria-current="page"] {
width: calc(100% + 1px);
margin-right: -1px;
border-radius: var(--hm-lnb-current-radius);
}
/* Only 2-depth destination links: the group keeps its full layout width, while hover, current, focus and click belong to the
inset link. Subtract the same inset from text padding and width to preserve text wrapping and the right content join. */
[data-slot="lnb-nav"] > [data-slot="lnb-group"] > [data-slot="lnb-group-content"] > [data-slot="lnb-item"] {
width: calc(100% - var(--hm-lnb-subitem-surface-inset));
margin-left: var(--hm-lnb-subitem-surface-inset);
padding-left: calc(var(--hm-lnb-subitem-padding-start) - var(--hm-lnb-subitem-surface-inset));
}
[data-slot="lnb-nav"] > [data-slot="lnb-group"] > [data-slot="lnb-group-content"] > [data-slot="lnb-item"][aria-current="page"] {
width: calc(100% + 1px - var(--hm-lnb-subitem-surface-inset));
}
/* ── Menu rows (direct children of the menu region) ─────────────────────────────────────────────────── */
/* Back to LTR. A left scrollbar narrows the content box from the left, so each row starts that much (the scrollbar width, 0 without
one) to the left again: `100%` is the content box, the region is `--hm-lnb-width + 1px` wide with `--hm-lnb-inset-start` padding. The
scrollbar sits in that padding and the rows keep x 20 → 272 (current 273) with or without it. */
[data-slot="lnb-nav"] > * {
direction: ltr;
margin-left: calc(100% + var(--hm-lnb-inset-start) - var(--hm-lnb-width) - 1px);
}
[data-slot="lnb-nav"] > :is([data-slot="lnb-item"], [data-slot="lnb-group"]) {
width: auto;
margin-right: 1px;
}
[data-slot="lnb-nav"] > [data-slot="lnb-item"][aria-current="page"] {
margin-right: 0;
}
/* ── Nested group (2-depth group inside a group) ────────────────────────────────────────────────────── */
/* The outer group already draws the open face and line, so a nested group adds no face, line, padding or width loss; its name
lines up with the 2-depth item text. */
[data-slot="lnb-group-content"] [data-slot="lnb-group"] {
width: 100%;
border-radius: 0;
}
[data-slot="lnb-group-content"] [data-slot="lnb-group"][data-state="open"] {
background: none;
}
[data-slot="lnb-group-content"] [data-slot="lnb-group"][data-state="open"]::before {
content: none;
}
[data-slot="lnb-group-content"] [data-slot="lnb-group-trigger"] {
padding-left: calc(var(--hm-lnb-subitem-padding-start) - var(--hm-lnb-group-arrow-size) - var(--hm-lnb-item-gap));
border-radius: 0;
}
[data-slot="lnb-group-content"] [data-slot="lnb-group"] > [data-slot="lnb-group-trigger"]:hover {
border-radius: var(--hm-lnb-item-radius);
}
[data-slot="lnb-group-content"] [data-slot="lnb-group-content"][data-state="open"] {
padding-bottom: 0;
}
/* 3-depth only: the row owns the existing connector gradients; the link begins at the branch end.
Decoration follows the row's real middle when text wraps and never owns pointer interaction. */
[data-slot="lnb-third-row"] {
--hm-lnb-third-line: var(--hm-lnb-third-connector-color);
--hm-lnb-third-line-end: 100%;
--hm-lnb-third-connector:
linear-gradient(var(--hm-lnb-third-line), var(--hm-lnb-third-line)) var(--hm-lnb-third-connector-x) 0 / var(--hm-lnb-third-connector-width) var(--hm-lnb-third-line-end) no-repeat,
linear-gradient(var(--hm-lnb-third-line), var(--hm-lnb-third-line)) var(--hm-lnb-third-connector-x) calc(50% - var(--hm-lnb-third-connector-width) / 2) / var(--hm-lnb-third-connector-length) var(--hm-lnb-third-connector-width) no-repeat;
position: relative;
padding-left: calc(var(--hm-lnb-third-connector-x) + var(--hm-lnb-third-connector-length));
}
[data-slot="lnb-third-row"]::before {
content: "";
position: absolute;
inset: 0;
background: var(--hm-lnb-third-connector);
pointer-events: none;
}
[data-slot="lnb-group-content"] [data-slot="lnb-third-row"] > [data-slot="lnb-item"] {
min-height: var(--hm-lnb-third-min-height);
padding-top: var(--hm-lnb-third-padding-block);
padding-bottom: var(--hm-lnb-third-padding-block);
padding-left: calc(var(--hm-lnb-third-padding-start) - var(--hm-lnb-third-connector-x) - var(--hm-lnb-third-connector-length));
line-height: var(--hm-lnb-third-line-height);
font-weight: var(--hm-lnb-third-font-weight);
-webkit-text-stroke: 0;
text-shadow: none;
}
/* The vertical line ends at the middle of the last row. */
[data-slot="lnb-third-row"]:last-child {
--hm-lnb-third-line-end: 50%;
}
[data-slot="lnb-group-content"] [data-slot="lnb-third-row"] > [data-slot="lnb-item"][aria-current="page"] {
font-weight: var(--hm-lnb-third-current-font-weight);
}
/* Desktop only: below 1024px the column is gone and the layout column collapses with it. */
@media (max-width: 1023px) {
:root, [data-hm-theme] {
--hm-lnb-width: 0px;
}
[data-slot="lnb"] {
display: none;
}
}
/* LNB Mobile Header is the mobile representation of LNB Mode, independent of GNB/Dashboard. */
:root, [data-hm-theme] {
--hm-lnb-mobile-height: 56px;
--hm-lnb-mobile-join: 12px;
--hm-lnb-mobile-background: linear-gradient(180deg, var(--hm-sem-action-surface-stop-0) 0%, var(--hm-sem-action-surface-stop-30) 30%, var(--hm-sem-action-surface-stop-65) 65%, var(--hm-sem-action-surface-stop-100) 100%);
/* Mobile menu content (inside the HM Sheet, white face). */
--hm-lnb-mobile-link-height: 44px;
--hm-lnb-mobile-link-padding-x: 12px;
--hm-lnb-mobile-link-radius: var(--hm-radius-control-md);
--hm-lnb-mobile-link-font-size: 14px;
--hm-lnb-mobile-link-line-height: 20px;
--hm-lnb-mobile-link-font-weight: 500;
--hm-lnb-mobile-link-text: var(--hm-sem-fg-primary);
--hm-lnb-mobile-link-hover-background: var(--accent);
--hm-lnb-mobile-link-hover-text: var(--accent-foreground);
--hm-lnb-mobile-link-current-background: var(--hm-sem-bg-content);
--hm-lnb-mobile-link-current-text: var(--hm-sem-action-primary);
--hm-lnb-mobile-link-current-font-weight: 700;
--hm-lnb-mobile-group-label-color: var(--hm-sem-fg-secondary);
--hm-lnb-mobile-group-gap: 16px;
--hm-lnb-mobile-nested-gap: 8px;
--hm-lnb-mobile-nested-indent: var(--hm-lnb-mobile-link-padding-x);
--hm-lnb-mobile-focus-ring: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
[data-slot="lnb-mobile-header"] { display: none; }
[data-slot="lnb-mobile-nav"] {
display: grid;
gap: var(--hm-lnb-mobile-group-gap);
padding-bottom: 16px;
}
[data-slot="lnb-mobile-group-label"] {
margin: 0;
padding: 0 var(--hm-lnb-mobile-link-padding-x) 4px;
color: var(--hm-lnb-mobile-group-label-color);
font-size: 12px;
line-height: 18px;
font-weight: 500;
}
/* A group inside a group (a deeper local level, e.g. the LNB 3-depth): one link padding further in, with room above its title. */
[data-slot="lnb-mobile-group"] [data-slot="lnb-mobile-group"] {
margin-top: var(--hm-lnb-mobile-nested-gap);
padding-left: var(--hm-lnb-mobile-nested-indent);
}
[data-slot="lnb-mobile-link"] {
box-sizing: border-box;
min-height: var(--hm-lnb-mobile-link-height);
padding: 0 var(--hm-lnb-mobile-link-padding-x);
border-radius: var(--hm-lnb-mobile-link-radius);
color: var(--hm-lnb-mobile-link-text);
font-size: var(--hm-lnb-mobile-link-font-size);
line-height: var(--hm-lnb-mobile-link-line-height);
font-weight: var(--hm-lnb-mobile-link-font-weight);
text-decoration: none;
overflow-wrap: anywhere;
outline: none;
}
[data-slot="lnb-mobile-link"]:is(:hover, :focus-visible) {
background: var(--hm-lnb-mobile-link-hover-background);
color: var(--hm-lnb-mobile-link-hover-text);
}
[data-slot="lnb-mobile-link"]:focus-visible {
box-shadow: var(--hm-lnb-mobile-focus-ring);
}
[data-slot="lnb-mobile-link"][aria-current="page"] {
background: var(--hm-lnb-mobile-link-current-background);
color: var(--hm-lnb-mobile-link-current-text);
font-weight: var(--hm-lnb-mobile-link-current-font-weight);
}
[data-slot="lnb-mobile-nav"] [data-slot="lnb-profile"] {
display: flex; align-items: center; gap: 12px; width: 100%; padding: 8px;
border-radius: var(--hm-radius-control-md); color: var(--hm-sem-fg-primary); text-align: left; cursor: pointer;
}
[data-slot="lnb-mobile-nav"] [data-slot="lnb-profile"]:hover { background: var(--accent); }
[data-slot="lnb-mobile-nav"] [data-slot="lnb-profile"]:focus-visible { outline: 2px solid var(--ring); }
[data-slot="lnb-mobile-nav"] [data-slot="lnb-profile"] p { color: var(--hm-sem-fg-secondary); }
@media (max-width: 1023px) {
[data-slot="lnb-mobile-header"] {
position: relative; display: flex; flex: none; align-items: center; gap: 8px;
height: var(--hm-lnb-mobile-height); padding: 0 16px;
background: var(--hm-lnb-mobile-background) left top / 100% calc(var(--hm-lnb-mobile-height) + var(--hm-lnb-mobile-join)) no-repeat;
color: var(--hm-sem-on-action-primary);
}
[data-slot="lnb-mobile-header"]::after {
content: ""; position: absolute; inset: 100% 0 auto; height: var(--hm-lnb-mobile-join);
background: var(--hm-lnb-mobile-background) left bottom / 100% calc(var(--hm-lnb-mobile-height) + var(--hm-lnb-mobile-join)) no-repeat;
pointer-events: none;
}
[data-slot="lnb-mobile-brand"] { min-width: 0; gap: 12px; text-decoration: none; }
[data-slot="lnb-mobile-brand"] :is(span, strong) { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
[data-slot="lnb-mobile-brand"] strong { font-size: 16px; font-weight: 700; }
[data-slot="lnb-mobile-brand"] small { display: none; }
[data-slot="lnb-mobile-brand"] > :is(img, svg) { max-height: 32px; width: auto; }
[data-slot="lnb-mobile-header"] > [data-slot="button"] { color: var(--hm-sem-on-action-primary); }
[data-slot="lnb-mobile-header"] > [data-slot="button"]:hover { background: var(--hm-lnb-hover-background); }
[data-slot="lnb-mobile-header"] > [data-slot="button"]:focus-visible { outline: var(--hm-lnb-focus-outline); outline-offset: -2px; box-shadow: none; }
}
+36
View File
@@ -0,0 +1,36 @@
import { defaultRuntimeTheme, type RuntimeTheme } from "./contract"
import { applyRuntimeTheme, isRuntimeTheme, resetRuntimeTheme } from "./runtime"
import { beginThemeRequest, isCurrentThemeRequest } from "./requests"
/** Fetch/validate only. No DOM access, fixed URL, storage or framework dependency. */
export async function fetchRuntimeTheme(url: string | URL, request?: RequestInit): Promise<RuntimeTheme> {
const response = await fetch(url, request)
if (!response.ok) throw new Error(`Theme request failed: HTTP ${response.status}`)
const theme: unknown = await response.json()
if (!isRuntimeTheme(theme)) throw new Error("Invalid Runtime Theme v1 document")
return { schemaVersion: 1, tokens: { ...theme.tokens } }
}
/** Resolve a supplied default + partial override using the existing v1 contract. */
export function resolveRuntimeTheme(theme: unknown, defaults: unknown = defaultRuntimeTheme): RuntimeTheme {
if (!isRuntimeTheme(defaults) || !isRuntimeTheme(theme)) throw new Error("Invalid Runtime Theme v1 document")
return { schemaVersion: 1, tokens: { ...defaultRuntimeTheme.tokens, ...defaults.tokens, ...theme.tokens } }
}
export type RuntimeThemeLoadOptions = { request?: RequestInit; defaults?: RuntimeTheme }
/** Convenience path: CDN/settings URL -> validate -> optional merge -> apply.
* Failure releases this API's override to the host/HM CSS fallback.
* Superseded requests never apply or reset the newer owner's theme.
*/
export async function loadRuntimeTheme(root: HTMLElement, url: string | URL, options: RuntimeThemeLoadOptions = {}): Promise<boolean> {
const request = beginThemeRequest(root)
try {
const theme = await fetchRuntimeTheme(url, options.request)
if (!isCurrentThemeRequest(root, request)) return false
return applyRuntimeTheme(root, options.defaults ? resolveRuntimeTheme(theme, options.defaults) : theme)
} catch {
if (isCurrentThemeRequest(root, request)) resetRuntimeTheme(root)
return false
}
}
+97
View File
@@ -0,0 +1,97 @@
/* Loading Screen pattern (visual reference: ui-viz Dashboard LoadingScreen, dashboard-composition.md §18). Pattern-local values.
The surface uses the HM brand Green (ui-viz local #0a3322 is not an HM Primitive). The spinner never rotates: each of the 24 bars fades
1 → .08 over 1.2s linear, started 0.05s apart clockwise. */
:root, [data-hm-theme] {
--hm-loading-screen-background: var(--hm-sem-action-primary);
--hm-loading-screen-foreground: var(--hm-sem-on-action-primary);
--hm-loading-screen-inline-padding-block: 36px;
--hm-loading-screen-inline-padding-inline: 24px;
--hm-loading-screen-inline-radius: 6px;
--hm-loading-screen-inline-shadow: inset 0 0 24px color-mix(in srgb, var(--hm-sem-palette-black) 45%, transparent);
--hm-loading-screen-gap: 12px;
--hm-loading-screen-z-index: 9999;
--hm-loading-spinner-size: 44px;
--hm-loading-spinner-bar-width: 2.5px;
--hm-loading-spinner-bar-height: 10px;
--hm-loading-spinner-bar-count: 24;
--hm-loading-spinner-bar-color: var(--hm-loading-screen-foreground);
--hm-loading-spinner-bar-min-opacity: 0.08;
--hm-loading-spinner-duration: 1.2s;
--hm-loading-spinner-stagger: 0.05s;
--hm-loading-text-font-size: 14px;
--hm-loading-text-font-weight: 500;
--hm-loading-text-letter-spacing: -0.02em;
--hm-loading-text-color: color-mix(in srgb, var(--hm-loading-screen-foreground) 60%, transparent);
}
[data-slot="loading-screen"] {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
background: var(--hm-loading-screen-background);
color: var(--hm-loading-screen-foreground);
text-align: center;
}
[data-slot="loading-screen"][data-variant="inline"] {
padding: var(--hm-loading-screen-inline-padding-block) var(--hm-loading-screen-inline-padding-inline);
border-radius: var(--hm-loading-screen-inline-radius);
box-shadow: var(--hm-loading-screen-inline-shadow);
overflow: hidden;
}
/* Fullscreen: the spinner + text group is centered in the viewport, same column / gap as inline (spinner and text share one center axis;
ui-viz's block assembly put the spinner at the group's leading edge, which read as misaligned). */
[data-slot="loading-screen"][data-variant="fullscreen"] {
position: fixed;
inset: 0;
z-index: var(--hm-loading-screen-z-index);
}
[data-slot="loading-screen-content"] {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--hm-loading-screen-gap);
}
[data-slot="loading-spinner"] {
position: relative;
width: var(--hm-loading-spinner-size);
height: var(--hm-loading-spinner-size);
}
[data-slot="loading-spinner"] > span {
position: absolute;
top: 0;
left: calc(50% - var(--hm-loading-spinner-bar-width) / 2);
width: var(--hm-loading-spinner-bar-width);
height: var(--hm-loading-spinner-bar-height);
border-radius: 9999px;
background: var(--hm-loading-spinner-bar-color);
transform-origin: center calc(var(--hm-loading-spinner-size) / 2);
transform: rotate(calc(var(--hm-loading-index) * 360deg / var(--hm-loading-spinner-bar-count)));
opacity: var(--hm-loading-spinner-bar-min-opacity);
animation: hm-loading-fade var(--hm-loading-spinner-duration) linear infinite;
animation-delay: calc(var(--hm-loading-index) * var(--hm-loading-spinner-stagger));
}
@keyframes hm-loading-fade {
from { opacity: 1; }
to { opacity: var(--hm-loading-spinner-bar-min-opacity); }
}
/* Reduced motion: no animation; the bars stay as a static comet (bright start fading clockwise). */
@media (prefers-reduced-motion: reduce) {
[data-slot="loading-spinner"] > span {
animation: none;
opacity: calc(1 - var(--hm-loading-index) / (var(--hm-loading-spinner-bar-count) - 1) * (1 - var(--hm-loading-spinner-bar-min-opacity)));
}
}
[data-slot="loading-text"] {
margin: 0;
font-family: var(--hm-font-family-base);
font-size: var(--hm-loading-text-font-size);
font-weight: var(--hm-loading-text-font-weight);
letter-spacing: var(--hm-loading-text-letter-spacing);
color: var(--hm-loading-text-color);
}
[data-slot="loading-text"]::after {
content: "...";
}
+169
View File
@@ -0,0 +1,169 @@
/* Menubar mapping. ui-viz has no Menubar spec, so these are current HM values built from the HM Semantic tokens. A desktop menu bar:
a bordered bar of text triggers whose open / focused trigger is highlighted, and menu content with the same item states as
other Radix menus. Radix state attributes (data-highlighted, data-disabled, data-state) drive the states; no HM state attributes.
No --hm-dropdown-menu-* token is referenced. */
:root, [data-hm-theme] {
/* Bar */
--hm-menubar-background: var(--hm-sem-bg-surface);
--hm-menubar-text: var(--hm-sem-fg-primary);
--hm-menubar-border-width: 1px;
--hm-menubar-border-color: var(--hm-sem-stroke-input-default);
--hm-menubar-radius: var(--hm-radius-container);
--hm-menubar-height: 38px;
--hm-menubar-padding: 2px;
--hm-menubar-gap: 2px;
/* Top-level trigger */
--hm-menubar-trigger-padding-x: 12px;
--hm-menubar-trigger-radius: 2px;
--hm-menubar-trigger-font-size: 14px;
--hm-menubar-trigger-line-height: 20px;
--hm-menubar-trigger-font-weight: 500;
/* Content */
--hm-menubar-content-background: var(--hm-sem-floating-background);
--hm-menubar-content-border-width: var(--hm-sem-floating-border-width);
--hm-menubar-content-border-color: var(--hm-sem-floating-border-color);
--hm-menubar-content-radius: var(--hm-sem-floating-radius);
--hm-menubar-content-shadow: var(--hm-sem-floating-shadow);
--hm-menubar-content-padding: 4px;
--hm-menubar-item-height: 36px;
--hm-menubar-item-padding-x: 12px;
--hm-menubar-item-gap: 8px;
--hm-menubar-item-radius: 2px;
--hm-menubar-font-size: 14px;
--hm-menubar-line-height: 20px;
--hm-menubar-highlight-background: var(--hm-sem-surface-highlight);
/* Top-level trigger highlight/open keeps the full accent; keyboard focus adds a separate ring. */
--hm-menubar-trigger-highlight-background: var(--accent);
--hm-menubar-highlight-text: var(--accent-foreground);
--hm-menubar-destructive-text: var(--hm-sem-action-destructive);
--hm-menubar-destructive-highlight-background: var(--hm-color-action-destructive-hover);
--hm-menubar-label-color: var(--hm-sem-fg-secondary);
--hm-menubar-label-font-size: 12px;
--hm-menubar-label-line-height: 18px;
--hm-menubar-label-font-weight: 500;
--hm-menubar-shortcut-color: var(--hm-sem-fg-secondary);
--hm-menubar-shortcut-font-size: 12px;
--hm-menubar-separator-color: var(--hm-sem-stroke-input-default);
--hm-menubar-indicator-size: 16px;
--hm-menubar-radio-dot-size: 8px;
}
[data-slot="menubar"] {
box-sizing: border-box;
width: fit-content;
height: var(--hm-menubar-height);
gap: var(--hm-menubar-gap);
padding: var(--hm-menubar-padding);
border: var(--hm-menubar-border-width) solid var(--hm-menubar-border-color);
border-radius: var(--hm-menubar-radius);
background: var(--hm-menubar-background);
color: var(--hm-menubar-text);
}
[data-slot="menubar-trigger"] {
box-sizing: border-box;
height: 100%;
padding: 0 var(--hm-menubar-trigger-padding-x);
border-radius: var(--hm-menubar-trigger-radius);
font-size: var(--hm-menubar-trigger-font-size);
line-height: var(--hm-menubar-trigger-line-height);
font-weight: var(--hm-menubar-trigger-font-weight);
cursor: default;
}
/* Radix sets data-highlighted on the focused top-level trigger and data-state="open" on the open one. */
[data-slot="menubar-trigger"][data-highlighted],
[data-slot="menubar-trigger"][data-state="open"] {
background: var(--hm-menubar-trigger-highlight-background);
color: var(--hm-menubar-highlight-text);
}
[data-slot="menubar-trigger"]:focus-visible:not([data-disabled]) {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
[data-slot="menubar-trigger"][data-disabled] {
opacity: 0.6;
}
[data-slot="menubar-content"],
[data-slot="menubar-sub-content"] {
box-sizing: border-box;
padding: var(--hm-menubar-content-padding);
border: var(--hm-menubar-content-border-width) solid var(--hm-menubar-content-border-color);
border-radius: var(--hm-menubar-content-radius);
background: var(--hm-menubar-content-background);
color: var(--hm-menubar-text);
box-shadow: var(--hm-menubar-content-shadow);
font-size: var(--hm-menubar-font-size);
line-height: var(--hm-menubar-line-height);
font-weight: 400;
}
[data-slot="menubar-item"],
[data-slot="menubar-checkbox-item"],
[data-slot="menubar-radio-item"],
[data-slot="menubar-sub-trigger"] {
box-sizing: border-box;
min-height: var(--hm-menubar-item-height);
gap: var(--hm-menubar-item-gap);
padding: 0 var(--hm-menubar-item-padding-x);
border-radius: var(--hm-menubar-item-radius);
}
[data-slot="menubar-checkbox-item"],
[data-slot="menubar-radio-item"],
[data-slot="menubar-item"][data-inset],
[data-slot="menubar-sub-trigger"][data-inset] {
padding-left: calc(var(--hm-menubar-item-padding-x) + var(--hm-menubar-indicator-size) + var(--hm-menubar-item-gap));
}
[data-slot="menubar-item-indicator"] {
left: var(--hm-menubar-item-padding-x);
width: var(--hm-menubar-indicator-size);
height: var(--hm-menubar-indicator-size);
}
[data-slot="menubar-radio-dot"] {
width: var(--hm-menubar-radio-dot-size);
height: var(--hm-menubar-radio-dot-size);
}
[data-slot="menubar-item"][data-highlighted],
[data-slot="menubar-checkbox-item"][data-highlighted],
[data-slot="menubar-radio-item"][data-highlighted],
[data-slot="menubar-sub-trigger"][data-highlighted],
[data-slot="menubar-sub-trigger"][data-state="open"] {
background: var(--hm-menubar-highlight-background);
color: var(--hm-menubar-highlight-text);
}
[data-slot="menubar-item"][data-variant="destructive"] {
color: var(--hm-menubar-destructive-text);
}
[data-slot="menubar-item"][data-variant="destructive"][data-highlighted] {
background: var(--hm-menubar-destructive-highlight-background);
color: var(--hm-menubar-destructive-text);
}
[data-slot="menubar-label"] {
padding: 6px var(--hm-menubar-item-padding-x);
color: var(--hm-menubar-label-color);
font-size: var(--hm-menubar-label-font-size);
line-height: var(--hm-menubar-label-line-height);
font-weight: var(--hm-menubar-label-font-weight);
}
[data-slot="menubar-label"][data-inset] {
padding-left: calc(var(--hm-menubar-item-padding-x) + var(--hm-menubar-indicator-size) + var(--hm-menubar-item-gap));
}
[data-slot="menubar-separator"] {
margin: var(--hm-menubar-content-padding) calc(-1 * var(--hm-menubar-content-padding));
background: var(--hm-menubar-separator-color);
}
[data-slot="menubar-shortcut"] {
color: var(--hm-menubar-shortcut-color);
font-size: var(--hm-menubar-shortcut-font-size);
letter-spacing: 0.05em;
}
[data-slot="menubar-item"][data-highlighted] [data-slot="menubar-shortcut"],
[data-slot="menubar-checkbox-item"][data-highlighted] [data-slot="menubar-shortcut"],
[data-slot="menubar-radio-item"][data-highlighted] [data-slot="menubar-shortcut"] {
color: inherit;
}
+130
View File
@@ -0,0 +1,130 @@
/* Navigation Menu mapping. ui-viz has no Navigation Menu spec, so these are current HM values built from the HM Semantic tokens.
It is for navigation destinations: top-level triggers and links share one text-button look, the current location (Radix
`data-active`) is bold and tinted, and the content / viewport is a floating panel that can hold links with descriptions.
Radix attributes drive the states (data-state, data-active, data-motion). No --hm-dropdown-menu-* token is referenced. */
:root, [data-hm-theme] {
--hm-navigation-menu-height: 34px;
--hm-navigation-menu-padding-x: 12px;
--hm-navigation-menu-gap: 4px;
--hm-navigation-menu-radius: var(--hm-radius-control-md);
--hm-navigation-menu-font-size: 14px;
--hm-navigation-menu-line-height: 20px;
--hm-navigation-menu-font-weight: 500;
--hm-navigation-menu-text: var(--hm-sem-fg-primary);
--hm-navigation-menu-icon-size: 12px;
--hm-navigation-menu-hover-background: var(--accent);
--hm-navigation-menu-hover-text: var(--accent-foreground);
/* Current location: same family as the Tabs / Select highlight (green tint), softer than hover. */
--hm-navigation-menu-active-background: var(--hm-sem-surface-highlight);
--hm-navigation-menu-active-text: var(--hm-sem-action-primary);
--hm-navigation-menu-active-font-weight: 700;
/* Content / viewport surface */
--hm-navigation-menu-background: var(--hm-sem-floating-background);
--hm-navigation-menu-border-width: var(--hm-sem-floating-border-width);
--hm-navigation-menu-border-color: var(--hm-sem-floating-border-color);
--hm-navigation-menu-surface-radius: var(--hm-sem-floating-radius);
--hm-navigation-menu-shadow: var(--hm-sem-floating-shadow);
--hm-navigation-menu-content-padding: 8px;
--hm-navigation-menu-content-min-width: 240px;
/* Link inside the content (destination with an optional description) */
--hm-navigation-menu-item-padding: 8px 12px;
--hm-navigation-menu-item-gap: 2px;
--hm-navigation-menu-item-radius: 2px;
--hm-navigation-menu-description-color: var(--hm-sem-fg-secondary);
--hm-navigation-menu-description-font-size: 12px;
--hm-navigation-menu-description-line-height: 18px;
/* Current HM value for this component only; not a global Disabled rule. */
--hm-navigation-menu-disabled-opacity: 0.6;
}
[data-slot="navigation-menu-list"] {
gap: var(--hm-navigation-menu-gap);
}
/* Top-level trigger and link: the same text-button look. */
[data-slot="navigation-menu-trigger"],
[data-slot="navigation-menu-link"] {
box-sizing: border-box;
align-items: center;
height: var(--hm-navigation-menu-height);
padding: 0 var(--hm-navigation-menu-padding-x);
border-radius: var(--hm-navigation-menu-radius);
background: transparent;
color: var(--hm-navigation-menu-text);
font-size: var(--hm-navigation-menu-font-size);
line-height: var(--hm-navigation-menu-line-height);
font-weight: var(--hm-navigation-menu-font-weight);
text-decoration: none;
cursor: pointer;
}
[data-slot="navigation-menu-trigger"]:hover,
[data-slot="navigation-menu-trigger"]:focus-visible,
[data-slot="navigation-menu-trigger"][data-state="open"],
[data-slot="navigation-menu-link"]:hover,
[data-slot="navigation-menu-link"]:focus-visible {
background: var(--hm-navigation-menu-hover-background);
color: var(--hm-navigation-menu-hover-text);
}
[data-slot="navigation-menu-trigger"]:focus-visible,
[data-slot="navigation-menu-link"]:focus-visible {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
[data-slot="navigation-menu-link"][data-active] {
background: var(--hm-navigation-menu-active-background);
color: var(--hm-navigation-menu-active-text);
font-weight: var(--hm-navigation-menu-active-font-weight);
}
[data-slot="navigation-menu-link"][data-active]:hover,
[data-slot="navigation-menu-link"][data-active]:focus-visible {
background: var(--hm-navigation-menu-hover-background);
}
[data-slot="navigation-menu-trigger"]:disabled {
opacity: var(--hm-navigation-menu-disabled-opacity);
cursor: not-allowed;
}
[data-slot="navigation-menu-trigger-icon"] {
width: var(--hm-navigation-menu-icon-size);
height: var(--hm-navigation-menu-icon-size);
}
/* Floating panel: the viewport carries the surface (viewport mode) or the content does (viewport={false}). */
[data-slot="navigation-menu-viewport"],
[data-viewport="false"] [data-slot="navigation-menu-content"] {
border: var(--hm-navigation-menu-border-width) solid var(--hm-navigation-menu-border-color);
border-radius: var(--hm-navigation-menu-surface-radius);
background: var(--hm-navigation-menu-background);
color: var(--hm-navigation-menu-text);
box-shadow: var(--hm-navigation-menu-shadow);
}
[data-slot="navigation-menu-content"] {
box-sizing: border-box;
min-width: var(--hm-navigation-menu-content-min-width);
padding: var(--hm-navigation-menu-content-padding);
}
[data-slot="navigation-menu-indicator-arrow"] {
background: var(--hm-navigation-menu-border-color);
}
/* Links inside the content are destinations: block items, optional title + description children. */
[data-slot="navigation-menu-content"] [data-slot="navigation-menu-link"] {
height: auto;
flex-direction: column;
align-items: stretch;
gap: var(--hm-navigation-menu-item-gap);
padding: var(--hm-navigation-menu-item-padding);
border-radius: var(--hm-navigation-menu-item-radius);
}
[data-slot="navigation-menu-content"] [data-slot="navigation-menu-link"] [data-description] {
color: var(--hm-navigation-menu-description-color);
font-size: var(--hm-navigation-menu-description-font-size);
line-height: var(--hm-navigation-menu-description-line-height);
font-weight: 400;
}
[data-slot="navigation-menu-content"] [data-slot="navigation-menu-link"]:hover [data-description],
[data-slot="navigation-menu-content"] [data-slot="navigation-menu-link"]:focus-visible [data-description] {
color: inherit;
}
+5
View File
@@ -0,0 +1,5 @@
/* The existing shadcn 10% overlay, now theme-derived. */
[data-slot="dialog-overlay"], [data-slot="alert-dialog-overlay"],
[data-slot="sheet-overlay"], [data-slot="drawer-overlay"] {
background-color: var(--hm-sem-overlay-background);
}
+52
View File
@@ -0,0 +1,52 @@
/* Page Header pattern (layout visual reference: ui-viz page heading, dashboard-composition.md §8): space-between row, aligned to the
bottom, 24px gap, 20px to the first card. The Breadcrumb is the HM Breadcrumb (its own 14px look, ui-viz had static 12px text).
Spacing values are pattern-local. */
:root, [data-hm-theme] {
--hm-page-header-gap: 24px;
--hm-page-header-margin-bottom: 20px;
--hm-page-header-breadcrumb-gap: 10px;
--hm-page-header-title-font-size: 28px;
--hm-page-header-title-line-height: 36px;
--hm-page-header-title-font-weight: 700;
--hm-page-header-title-color: var(--hm-sem-fg-primary);
--hm-page-header-description-gap: 8px;
--hm-page-header-description-font-size: 14px;
--hm-page-header-description-line-height: 20px;
--hm-page-header-description-color: var(--hm-sem-fg-secondary);
--hm-page-header-actions-gap: 8px;
}
[data-slot="page-header"] {
align-items: flex-end;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--hm-page-header-gap);
min-width: 0;
margin-bottom: var(--hm-page-header-margin-bottom);
}
[data-slot="page-header-content"] {
flex: 1 1 320px;
min-width: 0;
}
[data-slot="page-header-content"] > [data-slot="breadcrumb"] {
margin-bottom: var(--hm-page-header-breadcrumb-gap);
}
[data-slot="page-header-title"] {
margin: 0;
color: var(--hm-page-header-title-color);
font-size: var(--hm-page-header-title-font-size);
line-height: var(--hm-page-header-title-line-height);
font-weight: var(--hm-page-header-title-font-weight);
overflow-wrap: anywhere;
}
[data-slot="page-header-description"] {
margin: var(--hm-page-header-description-gap) 0 0;
color: var(--hm-page-header-description-color);
font-size: var(--hm-page-header-description-font-size);
line-height: var(--hm-page-header-description-line-height);
overflow-wrap: anywhere;
}
[data-slot="page-header-actions"] {
gap: var(--hm-page-header-actions-gap);
margin-left: auto;
}
+80
View File
@@ -0,0 +1,80 @@
/* Pagination mapping. ui-viz lists Pagination only as a component name (no spec), so these are current HM values built from the HM
Semantic tokens. Links are independent anchor styles (not the Button tokens): a square page link, a padded Previous / Next link,
a filled current page, and a dimmed aria-disabled link. */
:root, [data-hm-theme] {
--hm-pagination-gap: 4px;
--hm-pagination-height: 34px;
--hm-pagination-min-width: 34px;
--hm-pagination-padding-x: 12px;
--hm-pagination-radius: var(--hm-radius-control-md);
--hm-pagination-font-size: 14px;
--hm-pagination-line-height: 20px;
--hm-pagination-font-weight: 500;
--hm-pagination-icon-size: 16px;
--hm-pagination-text: var(--hm-sem-fg-primary);
--hm-pagination-hover-background: var(--hm-sem-surface-highlight);
--hm-pagination-hover-text: var(--accent-foreground);
--hm-pagination-active-background: var(--hm-sem-action-primary);
--hm-pagination-active-text: var(--hm-sem-on-action-primary);
--hm-pagination-active-font-weight: 700;
--hm-pagination-ellipsis-color: var(--hm-sem-fg-secondary);
/* Current HM value for this component only; not a global Disabled rule. */
--hm-pagination-disabled-opacity: 0.6;
}
[data-slot="pagination-content"] {
gap: var(--hm-pagination-gap);
}
[data-slot="pagination-link"] {
box-sizing: border-box;
height: var(--hm-pagination-height);
min-width: var(--hm-pagination-min-width);
padding: 0;
border-radius: var(--hm-pagination-radius);
color: var(--hm-pagination-text);
font-size: var(--hm-pagination-font-size);
line-height: var(--hm-pagination-line-height);
font-weight: var(--hm-pagination-font-weight);
text-decoration: none;
outline: none;
}
[data-slot="pagination-link"][data-size="default"] {
padding: 0 var(--hm-pagination-padding-x);
}
[data-slot="pagination-link"] > svg {
width: var(--hm-pagination-icon-size);
height: var(--hm-pagination-icon-size);
flex-shrink: 0;
}
[data-slot="pagination-link"]:hover {
background: var(--hm-pagination-hover-background);
color: var(--hm-pagination-hover-text);
}
[data-slot="pagination-link"]:focus-visible {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
/* Current page: filled, stays filled on hover. */
[data-slot="pagination-link"][aria-current="page"] {
background: var(--hm-pagination-active-background);
color: var(--hm-pagination-active-text);
font-weight: var(--hm-pagination-active-font-weight);
}
[data-slot="pagination-link"][aria-disabled="true"] {
opacity: var(--hm-pagination-disabled-opacity);
cursor: not-allowed;
}
[data-slot="pagination-link"][aria-disabled="true"]:hover {
background: transparent;
color: var(--hm-pagination-text);
}
[data-slot="pagination-ellipsis"] {
width: var(--hm-pagination-min-width);
height: var(--hm-pagination-height);
color: var(--hm-pagination-ellipsis-color);
}
[data-slot="pagination-ellipsis"] > svg {
width: var(--hm-pagination-icon-size);
height: var(--hm-pagination-icon-size);
}
+30
View File
@@ -0,0 +1,30 @@
/* Popover mapping. ui-viz has no Popover spec (inventory: design pending), so these are current HM values from the HM Semantic tokens.
Popover is a non-modal floating panel: no overlay, no focus trap. Radix owns Escape / outside close and focus return. */
:root, [data-hm-theme] {
--hm-popover-background: var(--hm-sem-floating-background);
--hm-popover-text: var(--hm-sem-fg-primary);
--hm-popover-border-width: var(--hm-sem-floating-border-width);
--hm-popover-border-color: var(--hm-sem-floating-border-color);
--hm-popover-radius: var(--hm-sem-floating-radius);
--hm-popover-shadow: var(--hm-sem-floating-shadow);
--hm-popover-padding: 16px;
--hm-popover-width: 288px;
--hm-popover-font-size: 14px;
--hm-popover-line-height: 20px;
--hm-popover-font-weight: 400;
}
[data-slot="popover-content"] {
box-sizing: border-box;
width: var(--hm-popover-width);
max-width: calc(100vw - 16px);
padding: var(--hm-popover-padding);
border: var(--hm-popover-border-width) solid var(--hm-popover-border-color);
border-radius: var(--hm-popover-radius);
background: var(--hm-popover-background);
color: var(--hm-popover-text);
box-shadow: var(--hm-popover-shadow);
font-size: var(--hm-popover-font-size);
line-height: var(--hm-popover-line-height);
font-weight: var(--hm-popover-font-weight);
}
+46
View File
@@ -0,0 +1,46 @@
/* Generated from themes/default.json + scripts/theme-templates/primitives.css. Do not edit. */
/* HM color palette. Primitive names describe colors, not UI roles. */
:root {
--hm-green-light-4: #34daaa;
--hm-green-light-3: #249473;
--hm-green-light-2: #266651;
--hm-green-light-1: #205241;
--hm-green-default: #123328;
--hm-green-dark-1: #0a221d;
--hm-green-dark-2: #071e18;
--hm-brown-light-2: #897445;
--hm-brown-light-1: #604f32;
--hm-brown-default: #3e3523;
--hm-brown-dark-1: #352d1d;
--hm-brown-light-4: #ecd4a2;
--hm-orange-light-4: #feba88;
--hm-orange-default: #cc5200;
--hm-warm-neutral-light: #e0dace;
--hm-warm-neutral-mid: #9a9284;
--hm-warm-neutral-dark: #6e6759;
--hm-beige-light: #f5f0e5;
--hm-red-danger: #d32f2f;
--hm-white: #ffffff;
--hm-black: #000000;
--hm-neutral-gray-500: #999999;
/* Existing preview values without an approved palette alias. */
--hm-green-tone-1: #1f8265;
--hm-green-tone-2: #165c47;
--hm-green-tone-3: #114838;
--hm-green-tone-4: #08241c;
--hm-warm-neutral-pale: #dedbd2;
--hm-warm-neutral-secondary: #ede8db;
--hm-warm-neutral-muted: #f4f1ea;
--hm-green-tint: #bddfd4;
}
/* Accent for the current parent group on the green LNB surface (ui-viz color.action.accent-on-green). */
:root {
--hm-yellow-accent: #ffc600;
}
+19
View File
@@ -0,0 +1,19 @@
/* Progress mapping. ui-viz has no Progress spec (inventory name only), so these are current HM values from the HM Semantic /
Primitive tokens. One default look: a pale track and a primary indicator. Status colors (success / warning / error) are not defined
and are not created here. */
:root, [data-hm-theme] {
--hm-progress-height: 8px;
--hm-progress-radius: 9999px;
--hm-progress-track-background: var(--hm-sem-palette-warm-neutral-light);
/* Shares the Active Semantic while the Progress and Active color policies are the same; split later if they diverge. */
--hm-progress-indicator-background: var(--hm-sem-state-active);
}
[data-slot="progress"] {
height: var(--hm-progress-height);
border-radius: var(--hm-progress-radius);
background: var(--hm-progress-track-background);
}
[data-slot="progress-indicator"] {
background: var(--hm-progress-indicator-background);
}
+57
View File
@@ -0,0 +1,57 @@
/* Radio Group mapping: current HM values built from the shared Semantic tokens (ui-viz has no Radio spec; reference only).
States use Radix attributes: [data-state="checked"|"unchecked"], :hover, :focus-visible, :disabled, [aria-invalid="true"]; layout uses data-orientation. */
:root, [data-hm-theme] {
--hm-radio-size: 18px;
--hm-radio-border-width: 1px;
--hm-radio-dot-size: 8px;
--hm-radio-background: var(--hm-sem-bg-surface);
--hm-radio-border-color: var(--hm-sem-stroke-default);
--hm-radio-hover-border-color: var(--hm-sem-action-primary);
--hm-radio-checked-border-color: var(--hm-sem-state-active);
--hm-radio-dot-color: var(--hm-sem-state-active);
--hm-radio-error-border-color: var(--hm-sem-action-destructive);
--hm-radio-group-gap: 12px;
--hm-radio-group-gap-horizontal: 16px;
}
[data-slot="radio-group"] {
gap: var(--hm-radio-group-gap);
}
[data-slot="radio-group"][data-orientation="horizontal"] {
display: flex;
flex-wrap: wrap;
gap: var(--hm-radio-group-gap-horizontal);
}
[data-slot="radio-group-item"] {
box-sizing: border-box;
width: var(--hm-radio-size);
height: var(--hm-radio-size);
padding: 0;
border: var(--hm-radio-border-width) solid var(--hm-radio-border-color);
border-radius: 50%;
background: var(--hm-radio-background);
color: var(--hm-radio-dot-color);
box-shadow: none;
cursor: pointer;
}
[data-slot="radio-group-item"] svg {
width: var(--hm-radio-dot-size);
height: var(--hm-radio-dot-size);
}
[data-slot="radio-group-item"]:hover:not(:disabled) {
border-color: var(--hm-radio-hover-border-color);
}
[data-slot="radio-group-item"][data-state="checked"] {
border-color: var(--hm-radio-checked-border-color);
}
/* Focus level is the shadcn ring (ui_viz global focus is TBD); keep the ring variables because the theme sets box-shadow: none. */
[data-slot="radio-group-item"]:focus-visible {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000);
}
[data-slot="radio-group-item"][aria-invalid="true"] {
border-color: var(--hm-radio-error-border-color);
}
[data-slot="radio-group-item"]:disabled {
cursor: not-allowed;
}
+5
View File
@@ -0,0 +1,5 @@
// One owner per root. A newer load/apply/reset supersedes an in-flight load.
const requests = new WeakMap<HTMLElement, object>()
export function beginThemeRequest(root: HTMLElement) { const request = {}; requests.set(root, request); return request }
export function isCurrentThemeRequest(root: HTMLElement, request: object) { return requests.get(root) === request }
export function invalidateThemeRequest(root: HTMLElement) { requests.delete(root) }
+74
View File
@@ -0,0 +1,74 @@
/* Resizable mapping. ui-viz has no Resizable spec, so these are current HM values built from the HM Semantic / Primitive tokens.
react-resizable-panels v4 owns the drag, constraints, keyboard and the pointer target (it expands the target around the thin
separator); here only the divider and the optional grip are styled. The separator state is the library's `data-separator`
("inactive" | "hover" | "active") and `:focus-visible`. Panels and Group keep the library's inline sizing.
The divider look is in `@layer components` so a consumer's utility classes still win. Nothing here references Separator tokens. */
:root, [data-hm-theme] {
--hm-resizable-divider-width: 1px;
--hm-resizable-divider-color: var(--hm-sem-stroke-input-default);
/* Hover: a stronger neutral; dragging and keyboard focus: the primary color. The divider widens by a halo, not by layout. */
--hm-resizable-hover-color: var(--hm-sem-stroke-default);
--hm-resizable-active-color: var(--hm-sem-action-primary);
--hm-resizable-focus-color: var(--hm-sem-stroke-input-focus);
--hm-resizable-halo-width: 1px;
--hm-resizable-grip-width: 12px;
--hm-resizable-grip-height: 16px;
--hm-resizable-grip-radius: 2px;
--hm-resizable-grip-background: var(--hm-sem-bg-surface);
--hm-resizable-grip-border-color: var(--hm-sem-stroke-input-default);
--hm-resizable-grip-icon-size: 10px;
--hm-resizable-grip-icon-color: var(--hm-sem-fg-secondary);
/* Current HM value for this component only; not a global Disabled rule. */
--hm-resizable-disabled-opacity: 0.6;
}
@layer components {
[data-slot="resizable-handle"] {
flex-shrink: 0;
background: var(--hm-resizable-divider-color);
}
/* A separator between side-by-side panels is vertical; between stacked panels it is horizontal. */
[data-slot="resizable-handle"][aria-orientation="vertical"] {
width: var(--hm-resizable-divider-width);
}
[data-slot="resizable-handle"][aria-orientation="horizontal"] {
height: var(--hm-resizable-divider-width);
}
[data-slot="resizable-handle"][data-separator="hover"] {
background: var(--hm-resizable-hover-color);
box-shadow: 0 0 0 var(--hm-resizable-halo-width) var(--hm-resizable-hover-color);
}
[data-slot="resizable-handle"][data-separator="active"] {
background: var(--hm-resizable-active-color);
box-shadow: 0 0 0 var(--hm-resizable-halo-width) var(--hm-resizable-active-color);
}
[data-slot="resizable-handle"]:focus-visible {
background: var(--hm-resizable-focus-color);
box-shadow: 0 0 0 var(--hm-resizable-halo-width) var(--hm-resizable-focus-color);
}
[data-slot="resizable-handle"][data-disabled],
[data-slot="resizable-handle"][aria-disabled="true"] {
opacity: var(--hm-resizable-disabled-opacity);
}
[data-slot="resizable-handle-grip"] {
box-sizing: border-box;
width: var(--hm-resizable-grip-width);
height: var(--hm-resizable-grip-height);
border: 1px solid var(--hm-resizable-grip-border-color);
border-radius: var(--hm-resizable-grip-radius);
background: var(--hm-resizable-grip-background);
color: var(--hm-resizable-grip-icon-color);
}
[data-slot="resizable-handle-grip"] > svg {
width: var(--hm-resizable-grip-icon-size);
height: var(--hm-resizable-grip-icon-size);
}
/* Between stacked panels the grip lies down. */
[data-slot="resizable-handle"][aria-orientation="horizontal"] [data-slot="resizable-handle-grip"] {
transform: rotate(90deg);
}
}
+56
View File
@@ -0,0 +1,56 @@
import { runtimeThemeContract, runtimeGradientGroups, type RuntimeThemeToken } from "./contract"
import { invalidateThemeRequest } from "./requests"
type SavedProperty = { property: string; value: string; priority: string }
const applied = new WeakMap<HTMLElement, { properties: SavedProperty[]; scope: string | null }>()
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value)
/** Restore the host's previous inline values and scope. */
export function resetRuntimeTheme(root: HTMLElement) {
invalidateThemeRequest(root)
const previous = applied.get(root)
if (!previous) return
for (const { property, value, priority } of previous.properties) {
if (value) root.style.setProperty(property, value, priority)
else root.style.removeProperty(property)
}
if (previous.scope === null) root.removeAttribute("data-hm-theme")
else root.setAttribute("data-hm-theme", previous.scope)
applied.delete(root)
}
/** Validate without touching the DOM (editors/loaders can preserve their last valid value). */
export function isRuntimeTheme(theme: unknown): theme is import("./contract").RuntimeTheme {
if (!isRecord(theme) || theme.schemaVersion !== 1 || !isRecord(theme.tokens)) return false
const tokens = theme.tokens
for (const group of runtimeGradientGroups) {
const count = group.filter(key => Object.hasOwn(tokens, key)).length
if (count !== 0 && count !== group.length) return false
}
for (const [key, value] of Object.entries(tokens)) {
if (!Object.hasOwn(runtimeThemeContract, key) || typeof value !== "string") return false
const definition = runtimeThemeContract[key as RuntimeThemeToken]
const valid = definition.type === "radius"
? /^(?:[2-9]|1[0-2])px$/.test(value)
: /^#[0-9a-fA-F]{6}$/.test(value)
if (!valid) return false
}
return true
}
/** 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]) =>
[runtimeThemeContract[key as RuntimeThemeToken].property, value!] as const)
resetRuntimeTheme(root)
const scope = root.getAttribute("data-hm-theme")
const properties = values.map(([property]) => ({
property, value: root.style.getPropertyValue(property), priority: root.style.getPropertyPriority(property),
}))
root.setAttribute("data-hm-theme", "runtime")
for (const [property, value] of values) root.style.setProperty(property, value)
applied.set(root, { properties, scope })
return true
}
+23
View File
@@ -0,0 +1,23 @@
/* Scroll Area mapping. ui-viz has no Scroll Area spec, so these are current HM values. Radix keeps the scroll behavior and the
default scrollbar `type="hover"` (the bar appears while hovering or scrolling); only the scrollbar chrome is styled. */
:root, [data-hm-theme] {
--hm-scroll-area-scrollbar-size: 10px;
--hm-scroll-area-thumb-background: var(--hm-sem-stroke-default);
--hm-scroll-area-thumb-hover-background: var(--hm-sem-fg-secondary);
--hm-scroll-area-thumb-radius: 9999px;
}
[data-slot="scroll-area-scrollbar"][data-orientation="vertical"] {
width: var(--hm-scroll-area-scrollbar-size);
}
[data-slot="scroll-area-scrollbar"][data-orientation="horizontal"] {
height: var(--hm-scroll-area-scrollbar-size);
}
[data-slot="scroll-area-thumb"] {
border-radius: var(--hm-scroll-area-thumb-radius);
background: var(--hm-scroll-area-thumb-background);
}
[data-slot="scroll-area-thumb"]:hover,
[data-slot="scroll-area-scrollbar"][data-state="visible"]:active [data-slot="scroll-area-thumb"] {
background: var(--hm-scroll-area-thumb-hover-background);
}
+203
View File
@@ -0,0 +1,203 @@
/* Select mapping: the trigger and its connected list. Label and Helper/Error text are consumer composition.
States use the Radix attributes (data-state, data-placeholder, data-highlighted, data-side) and :disabled; size uses data-size. */
:root, [data-hm-theme] {
/* ui_viz: docs/components/select.md §1.1 D-004, §8 — Confirmed. Control height is the inner height: the 1px border is added
around it (outer 34 / 44). Not aliased to Input or Button. */
--hm-select-control-height-md: 32px;
--hm-select-control-height-lg: 42px;
--hm-select-border-width: 1px;
/* ui_viz: docs/components/select.md §8, §17 — BASELINE_VALUE (current applied contract) */
--hm-select-padding-x: 12px;
--hm-select-gap: 12px;
/* ui_viz select.md §8: Option min-height 42 for both sizes (no medium value defined). Kept as one token per size so a
later Medium decision changes only this line. */
--hm-select-option-height-md: 42px;
--hm-select-option-height-lg: 42px;
--hm-select-radius: var(--hm-radius-input);
/* ui_viz: docs/components/select.md §9 — large body-md 16/24/400, medium trigger body-sm 14/20/400, option body-md in both sizes */
--hm-select-font-size-md: 14px;
--hm-select-line-height-md: 20px;
--hm-select-font-size-lg: 16px;
--hm-select-line-height-lg: 24px;
/* Option typography follows the Select size (Trigger and list share one size). ui_viz left the Medium Option Role open
(TBD, medium Trigger body-sm vs Option body-md); the Medium Option takes the Medium Trigger value 14/20/400. */
--hm-select-option-font-size-md: var(--hm-select-font-size-md);
--hm-select-option-line-height-md: var(--hm-select-line-height-md);
--hm-select-option-font-size-lg: var(--hm-select-font-size-lg);
--hm-select-option-line-height-lg: var(--hm-select-line-height-lg);
--hm-select-font-weight: 400;
/* ui_viz: docs/components/select.md §10, §1.3 — Confirmed (D-005, M06) */
--hm-select-background: var(--hm-sem-bg-surface);
--hm-select-text: var(--hm-sem-fg-primary);
--hm-select-placeholder: var(--hm-sem-fg-placeholder);
--hm-select-border-color: var(--hm-sem-stroke-input-default);
--hm-select-focus-border-color: var(--hm-sem-stroke-input-focus);
--hm-select-seam-color: var(--hm-sem-stroke-input-default);
/* Open list face measured from the target Open screen: #e1f1ec on the white list = 45% of --accent (#bddfd4).
Same mix the Toggle/Calendar hover faces use. The list shadow is the target's soft, downward-offset one (not the floating-surface shadow). */
--hm-select-option-state-background: var(--hm-sem-surface-highlight);
--hm-select-option-state-text: var(--accent-foreground);
--hm-select-content-shadow: 0 2px 24px color-mix(in srgb, var(--hm-sem-palette-black) 12%, transparent);
/* ui_viz: docs/components/select.md §1.2 — user-confirmed Disabled: opacity .6, same white face and #DEDBD2 border */
--hm-select-disabled-opacity: 0.6;
/* ui_viz: docs/components/select.md §7 — indicator glyph stroke (user-provided Source) */
--hm-select-indicator-color: var(--hm-sem-palette-indicator);
}
/* Trigger. Large is the default when size is omitted. */
[data-slot="select-trigger"] {
--hm-select-control-height: var(--hm-select-control-height-lg);
--hm-select-font-size: var(--hm-select-font-size-lg);
--hm-select-line-height: var(--hm-select-line-height-lg);
box-sizing: border-box;
min-width: 0;
height: auto;
min-height: calc(var(--hm-select-control-height) + 2 * var(--hm-select-border-width));
gap: var(--hm-select-gap);
padding: 0 var(--hm-select-padding-x);
border: var(--hm-select-border-width) solid var(--hm-select-border-color);
border-radius: var(--hm-select-radius);
background: var(--hm-select-background);
color: var(--hm-select-text);
font-size: var(--hm-select-font-size);
line-height: var(--hm-select-line-height);
font-weight: var(--hm-select-font-weight);
}
[data-slot="select-trigger"] > [data-slot="select-value"] {
min-width: 0;
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-slot="select-trigger"][data-size="medium"] {
--hm-select-control-height: var(--hm-select-control-height-md);
--hm-select-font-size: var(--hm-select-font-size-md);
--hm-select-line-height: var(--hm-select-line-height-md);
}
[data-slot="select-trigger"][data-placeholder] {
color: var(--hm-select-placeholder);
}
[data-slot="select-icon"] {
width: 18px;
height: 18px;
color: var(--hm-select-indicator-color);
}
/* Focus and Open retain the focus-colored outer outline. The connected edge follows Content's actual side. */
[data-slot="select-trigger"]:focus {
border-color: var(--hm-select-focus-border-color);
}
[data-slot="select-trigger"][data-state="open"] {
border-color: var(--hm-select-focus-border-color);
}
/* Direction is mirrored from the actual Radix Content through its ARIA relationship. */
[data-slot="select-trigger"][data-state="open"][data-side="bottom"] {
border-radius: var(--hm-select-radius) var(--hm-select-radius) 0 0;
border-bottom-color: var(--hm-select-seam-color);
}
[data-slot="select-trigger"][data-state="open"][data-side="top"] {
border-radius: 0 0 var(--hm-select-radius) var(--hm-select-radius);
border-top-color: var(--hm-select-seam-color);
}
/* Disabled keeps the Default paint and dims the whole control. */
[data-slot="select-trigger"]:disabled {
border-color: var(--hm-select-border-color);
background: var(--hm-select-background);
opacity: var(--hm-select-disabled-opacity);
cursor: not-allowed;
}
/* Invalid belongs to the trigger border, never the popup surface or its neutral seam. */
[data-slot="select-trigger"][aria-invalid="true"] {
border-color: var(--hm-sem-action-destructive);
}
[data-slot="select-trigger"][aria-invalid="true"]:focus-visible {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
[data-slot="select-trigger"][aria-invalid="true"][data-state="open"][data-side="bottom"] {
border-bottom-color: var(--hm-select-seam-color);
}
[data-slot="select-trigger"][aria-invalid="true"][data-state="open"][data-side="top"] {
border-top-color: var(--hm-select-seam-color);
}
/* List: same outer width as the trigger, border in the focus color, rounded only at the bottom (connected outline). */
[data-slot="select-content"] {
box-sizing: border-box;
border: var(--hm-select-border-width) solid var(--hm-select-focus-border-color);
border-radius: var(--hm-select-radius);
background: var(--hm-select-background);
color: var(--hm-select-text);
box-shadow: var(--hm-select-content-shadow);
}
[data-slot="select-content"]:not([data-align-trigger="true"]) {
width: var(--radix-select-trigger-width);
min-width: var(--radix-select-trigger-width);
}
[data-slot="select-content"][data-side="bottom"] {
border-radius: 0 0 var(--hm-select-radius) var(--hm-select-radius);
/* sideOffset=-1 puts this top border on the trigger's bottom border, so it is the one seam line between the two.
Only the seam is the quiet control border; the outer outline stays the focus color. */
border-top-color: var(--hm-select-seam-color);
}
[data-slot="select-content"][data-side="top"] {
border-radius: var(--hm-select-radius) var(--hm-select-radius) 0 0;
border-bottom-color: var(--hm-select-seam-color);
}
/* Content uses the trigger's outer width. Its Viewport lives inside that bordered box, so it must fill the
available inner width instead of applying the trigger's outer width as another minimum. */
[data-slot="select-content"] [data-position="popper"] {
box-sizing: border-box;
width: 100%;
min-width: 0;
}
/* Option profile follows the Select size: SelectContent carries data-size (from the Trigger, through context), the Portal
keeps it out of the Trigger subtree. Width is not size-dependent (Content = Trigger outer width).
Hover, Selected and Keyboard Active use the same face (set by the shadcn state classes). */
[data-slot="select-content"] {
--hm-select-option-height: var(--hm-select-option-height-lg);
--hm-select-option-font-size: var(--hm-select-option-font-size-lg);
--hm-select-option-line-height: var(--hm-select-option-line-height-lg);
}
[data-slot="select-content"][data-size="medium"] {
--hm-select-option-height: var(--hm-select-option-height-md);
--hm-select-option-font-size: var(--hm-select-option-font-size-md);
--hm-select-option-line-height: var(--hm-select-option-line-height-md);
}
[data-slot="select-item"] {
box-sizing: border-box;
min-height: var(--hm-select-option-height);
padding: 0 var(--hm-select-padding-x);
border-radius: 0;
font-size: var(--hm-select-option-font-size);
line-height: var(--hm-select-option-line-height);
font-weight: var(--hm-select-font-weight);
}
/* ItemText: a flex item defaults to min-width:auto, so an unbreakable label kept its full width and pushed the row past the
list (Item scrollWidth > Viewport clientWidth). Let it shrink to the row and truncate, like the trigger value. */
[data-slot="select-item"] > span {
display: block;
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Radix keeps selection and the current pointer/keyboard highlight as separate states. ui_viz gives both
the same option face, so use the actual Radix attributes rather than DOM position or focus alone. */
[data-slot="select-item"][data-state="checked"],
[data-slot="select-item"][data-highlighted] {
background: var(--hm-select-option-state-background);
color: var(--hm-select-option-state-text);
}
+17
View File
@@ -0,0 +1,17 @@
/* Separator mapping — current HM values. ui-viz has no standalone Separator (only per-component divider values), so this is a plain 1px line.
The color is a divider tone (the same pale warm neutral the HM input / Dialog outlines use) referenced from the Primitive; no Semantic exists for it yet. */
:root, [data-hm-theme] {
--hm-separator-thickness: 1px;
--hm-separator-color: var(--hm-sem-stroke-input-default);
}
[data-slot="separator"] {
background: var(--hm-separator-color);
}
[data-slot="separator"][data-orientation="horizontal"] {
height: var(--hm-separator-thickness);
}
[data-slot="separator"][data-orientation="vertical"] {
width: var(--hm-separator-thickness);
min-height: 1em;
}
+87
View File
@@ -0,0 +1,87 @@
/* Sheet mapping. ui-viz has no Sheet spec (the Dialog doc lists "side sheet" as out of scope), so these are current HM values built
from the HM Semantic tokens. Modal panel attached to a viewport edge; Radix keeps Portal, focus trap, Escape and focus return.
Position and slide animation stay the shadcn classes; the Overlay keeps the shadcn default. Values repeat Dialog's look in
meaning only: no --hm-dialog-* token is referenced. */
:root, [data-hm-theme] {
--hm-sheet-background: var(--hm-sem-bg-surface);
--hm-sheet-text: var(--hm-sem-fg-primary);
--hm-sheet-border-width: 1px;
--hm-sheet-border-color: var(--hm-sem-stroke-input-default);
--hm-sheet-shadow: 0 0 12px color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent);
/* Left / right: fixed width, never wider than the viewport minus a strip of Overlay. Top / bottom: full width, height by content. */
--hm-sheet-width: 400px;
--hm-sheet-edge-gap: 48px;
--hm-sheet-padding: 24px;
--hm-sheet-header-gap: 8px;
--hm-sheet-footer-gap: 8px;
/* Space reserved at the Header end for the Close button (28px button + 8px gap). */
--hm-sheet-close-clearance: 36px;
--hm-sheet-title-font-size: 20px;
--hm-sheet-title-line-height: 28px;
--hm-sheet-title-font-weight: 700;
--hm-sheet-description-font-size: 14px;
--hm-sheet-description-line-height: 20px;
--hm-sheet-description-color: var(--hm-sem-fg-secondary);
--hm-sheet-body-font-size: 14px;
--hm-sheet-body-line-height: 20px;
}
[data-slot="sheet-content"] {
box-sizing: border-box;
background: var(--hm-sheet-background);
color: var(--hm-sheet-text);
box-shadow: var(--hm-sheet-shadow);
font-size: var(--hm-sheet-body-font-size);
line-height: var(--hm-sheet-body-line-height);
overflow-wrap: anywhere;
}
[data-slot="sheet-content"][data-side="right"],
[data-slot="sheet-content"][data-side="left"] {
height: 100%;
width: min(var(--hm-sheet-width), calc(100vw - var(--hm-sheet-edge-gap)));
}
[data-slot="sheet-content"][data-side="top"],
[data-slot="sheet-content"][data-side="bottom"] {
width: 100%;
max-height: calc(100dvh - var(--hm-sheet-edge-gap));
}
/* Border only on the edge that faces the page. */
[data-slot="sheet-content"][data-side="right"] { border-left: var(--hm-sheet-border-width) solid var(--hm-sheet-border-color); }
[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-header"] {
flex: none;
gap: var(--hm-sheet-header-gap);
padding: var(--hm-sheet-padding) calc(var(--hm-sheet-padding) + var(--hm-sheet-close-clearance)) 16px var(--hm-sheet-padding);
}
[data-slot="sheet-title"] {
font-size: var(--hm-sheet-title-font-size);
line-height: var(--hm-sheet-title-line-height);
font-weight: var(--hm-sheet-title-font-weight);
color: var(--hm-sheet-text);
}
[data-slot="sheet-description"] {
font-size: var(--hm-sheet-description-font-size);
line-height: var(--hm-sheet-description-line-height);
font-weight: 400;
color: var(--hm-sheet-description-color);
}
/* Body: the only region that scrolls; Header and Footer stay in view. */
[data-slot="sheet-body"] {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
padding: 0 var(--hm-sheet-padding);
}
[data-slot="sheet-footer"] {
flex: none;
gap: var(--hm-sheet-footer-gap);
padding: 16px var(--hm-sheet-padding) var(--hm-sheet-padding);
}
+20
View File
@@ -0,0 +1,20 @@
/* Skeleton mapping. ui-viz has no Skeleton spec (inventory name only), so these are current HM values. The pulse is the shadcn
`animate-pulse` (no HM Motion rule exists); reduced-motion users get a still placeholder.
The default look lives in `@layer components` so a consumer's utility classes (`rounded-full`, `bg-*`) still override it:
Skeleton shape is meant to be set through `className`. */
:root, [data-hm-theme] {
--hm-skeleton-background: var(--hm-sem-palette-warm-neutral-light);
--hm-skeleton-radius: var(--hm-radius-control-md);
}
@layer components {
[data-slot="skeleton"] {
background: var(--hm-skeleton-background);
border-radius: var(--hm-skeleton-radius);
}
}
@media (prefers-reduced-motion: reduce) {
[data-slot="skeleton"] {
animation: none;
}
}
+75
View File
@@ -0,0 +1,75 @@
/* Slider mapping. ui-viz has no Slider spec, so these are current HM values built from the HM Semantic / Primitive tokens.
Track, Range and Thumb are internal parts, so they are styled here; the Slider's own width / height stay `className` territory.
States: Radix attributes ([data-disabled], [data-orientation]) and :hover / :active / :focus-visible on the thumb. */
:root, [data-hm-theme] {
--hm-slider-track-size: 6px;
--hm-slider-track-background: var(--hm-sem-palette-warm-neutral-light);
--hm-slider-range-background: var(--hm-sem-state-active);
--hm-slider-thumb-size: 18px;
--hm-slider-thumb-background: var(--hm-sem-bg-surface);
--hm-slider-thumb-border-width: 2px;
--hm-slider-thumb-border-color: var(--hm-sem-state-active);
/* Interaction halo: hover shows it, dragging widens it. Keyboard focus has its own semantic color. */
--hm-slider-thumb-halo-color: color-mix(in srgb, var(--hm-sem-action-primary) 20%, transparent);
--hm-slider-thumb-focus-halo-color: color-mix(in srgb, var(--hm-sem-stroke-input-focus) 20%, transparent);
--hm-slider-thumb-halo-size: 4px;
--hm-slider-thumb-halo-size-active: 6px;
/* Current HM value for this component only; not a global Disabled rule. */
--hm-slider-disabled-opacity: 0.6;
}
[data-slot="slider-track"] {
border-radius: 9999px;
background: var(--hm-slider-track-background);
}
[data-slot="slider"][data-orientation="horizontal"] [data-slot="slider-track"] {
height: var(--hm-slider-track-size);
width: 100%;
}
[data-slot="slider"][data-orientation="vertical"] [data-slot="slider-track"] {
width: var(--hm-slider-track-size);
height: 100%;
}
[data-slot="slider-range"] {
background: var(--hm-slider-range-background);
}
[data-slot="slider"][data-orientation="horizontal"] [data-slot="slider-range"] {
height: 100%;
}
[data-slot="slider"][data-orientation="vertical"] [data-slot="slider-range"] {
width: 100%;
}
[data-slot="slider-thumb"] {
box-sizing: border-box;
width: var(--hm-slider-thumb-size);
height: var(--hm-slider-thumb-size);
border: var(--hm-slider-thumb-border-width) solid var(--hm-slider-thumb-border-color);
border-radius: 9999px;
background: var(--hm-slider-thumb-background);
cursor: grab;
}
[data-slot="slider-thumb"]:hover {
box-shadow: 0 0 0 var(--hm-slider-thumb-halo-size) var(--hm-slider-thumb-halo-color);
}
[data-slot="slider-thumb"]:active {
cursor: grabbing;
box-shadow: 0 0 0 var(--hm-slider-thumb-halo-size-active) var(--hm-slider-thumb-halo-color);
}
/* Focus wins the color when states overlap; drag keeps its 6px size and grabbing cursor. */
[data-slot="slider-thumb"]:focus-visible {
box-shadow: 0 0 0 var(--hm-slider-thumb-halo-size) var(--hm-slider-thumb-focus-halo-color);
}
[data-slot="slider-thumb"]:focus-visible:active {
box-shadow: 0 0 0 var(--hm-slider-thumb-halo-size-active) var(--hm-slider-thumb-focus-halo-color);
}
[data-slot="slider"][data-disabled] {
opacity: var(--hm-slider-disabled-opacity);
}
[data-slot="slider"][data-disabled] [data-slot="slider-thumb"] {
cursor: not-allowed;
box-shadow: none;
}
+107
View File
@@ -0,0 +1,107 @@
/* Toast (Sonner) mapping. Notification surface, not an anchored floating panel, so it does NOT use --hm-sem-floating-*; its values are
its own (the visual values currently equal the floating surface and are a Notification Semantic candidate, see docs/components/sonner.md).
ui-viz has no Toast spec and its success / warning / information status colors are TBD, so no status palette is invented:
Default, Success, Info, Warning and Loading share the neutral surface (the icon tells the type) and only Error uses the destructive color,
the same stance as Alert. Sonner's own data attributes ([data-sonner-toast], [data-type], [data-button] ...) are the state hooks. */
:root, [data-hm-theme] {
--hm-toast-width: 356px;
--hm-toast-background: var(--hm-sem-bg-surface);
--hm-toast-text: var(--hm-sem-fg-primary);
--hm-toast-border-width: 1px;
--hm-toast-border-color: var(--hm-sem-stroke-input-default);
--hm-toast-radius: var(--hm-radius-container);
--hm-toast-shadow: 0 0 12px color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent);
--hm-toast-padding: 16px;
--hm-toast-gap: 12px;
--hm-toast-font-family: var(--hm-font-family-base);
--hm-toast-title-font-size: 14px;
--hm-toast-title-line-height: 20px;
--hm-toast-title-font-weight: 500;
--hm-toast-description-color: var(--hm-sem-fg-secondary);
--hm-toast-description-font-size: 13px;
--hm-toast-description-line-height: 18px;
--hm-toast-description-font-weight: 400;
--hm-toast-icon-size: 16px;
--hm-toast-icon-color: var(--hm-sem-fg-primary);
--hm-toast-error-border-color: var(--hm-sem-action-destructive);
--hm-toast-error-text: var(--hm-sem-action-destructive);
--hm-toast-action-height: 28px;
--hm-toast-action-padding-x: 12px;
--hm-toast-action-radius: var(--hm-radius-control-md);
--hm-toast-action-font-size: 12px;
--hm-toast-action-font-weight: 500;
--hm-toast-action-background: var(--hm-sem-action-primary);
--hm-toast-action-text: var(--hm-sem-on-action-primary);
--hm-toast-cancel-background: var(--hm-sem-bg-subtle);
--hm-toast-cancel-text: var(--hm-sem-fg-secondary);
--hm-toast-close-background: var(--hm-toast-background);
--hm-toast-close-text: var(--hm-sem-fg-secondary);
--hm-toast-close-border-color: var(--hm-toast-border-color);
}
/* Sonner's own rules use up to three attributes, so the Toaster root repeats [dir] (always set) to win regardless of injection order. */
[data-sonner-toaster][data-sonner-theme][dir] {
--width: var(--hm-toast-width);
--border-radius: var(--hm-toast-radius);
--normal-bg: var(--hm-toast-background);
--normal-border: var(--hm-toast-border-color);
--normal-text: var(--hm-toast-text);
font-family: var(--hm-toast-font-family);
}
[data-sonner-toaster] [data-sonner-toast][data-styled="true"] {
box-sizing: border-box;
gap: var(--hm-toast-gap);
padding: var(--hm-toast-padding);
border-width: var(--hm-toast-border-width);
box-shadow: var(--hm-toast-shadow);
font-size: var(--hm-toast-description-font-size);
line-height: var(--hm-toast-description-line-height);
}
[data-sonner-toaster] [data-sonner-toast][data-styled="true"] [data-title] {
font-size: var(--hm-toast-title-font-size);
line-height: var(--hm-toast-title-line-height);
font-weight: var(--hm-toast-title-font-weight);
}
[data-sonner-toaster] [data-sonner-toast][data-styled="true"] [data-description] {
color: var(--hm-toast-description-color);
font-size: var(--hm-toast-description-font-size);
line-height: var(--hm-toast-description-line-height);
font-weight: var(--hm-toast-description-font-weight);
}
[data-sonner-toaster] [data-sonner-toast][data-styled="true"] [data-icon] {
width: var(--hm-toast-icon-size);
height: var(--hm-toast-icon-size);
color: var(--hm-toast-icon-color);
}
/* Error is the only status with its own color (destructive); other types stay neutral. */
[data-sonner-toaster] [data-sonner-toast][data-type="error"] {
border-color: var(--hm-toast-error-border-color);
}
[data-sonner-toaster] [data-sonner-toast][data-type="error"] [data-icon],
[data-sonner-toaster] [data-sonner-toast][data-type="error"] [data-title] {
color: var(--hm-toast-error-text);
}
[data-sonner-toaster] [data-sonner-toast][data-styled="true"] [data-button] {
height: var(--hm-toast-action-height);
padding: 0 var(--hm-toast-action-padding-x);
border-radius: var(--hm-toast-action-radius);
background: var(--hm-toast-action-background);
color: var(--hm-toast-action-text);
font-size: var(--hm-toast-action-font-size);
font-weight: var(--hm-toast-action-font-weight);
}
[data-sonner-toaster] [data-sonner-toast][data-styled="true"] [data-cancel] {
background: var(--hm-toast-cancel-background);
color: var(--hm-toast-cancel-text);
}
[data-sonner-toaster] [data-sonner-toast][data-styled="true"] [data-close-button] {
background: var(--hm-toast-close-background);
color: var(--hm-toast-close-text);
border-color: var(--hm-toast-close-border-color);
}
+47
View File
@@ -0,0 +1,47 @@
/* Switch mapping — current HM values built from the shared Semantic tokens (ui-viz has no Switch spec; it was only a candidate there).
States use Radix attributes: [data-state="checked"|"unchecked"], :hover, :focus-visible, :disabled. Motion is the shadcn default. */
:root, [data-hm-theme] {
--hm-switch-width: 36px;
--hm-switch-height: 20px;
--hm-switch-thumb-size: 16px;
--hm-switch-thumb-offset: 2px;
--hm-switch-off-background: var(--hm-sem-stroke-default);
--hm-switch-on-background: var(--hm-sem-state-active);
--hm-switch-thumb-color: var(--hm-sem-bg-surface);
--hm-switch-hover-overlay: var(--hm-color-action-overlay-hover);
}
[data-slot="switch"] {
box-sizing: border-box;
width: var(--hm-switch-width);
height: var(--hm-switch-height);
padding: 0;
border: 0;
border-radius: 9999px;
background-color: var(--hm-switch-off-background);
background-image: none;
box-shadow: none;
cursor: pointer;
}
[data-slot="switch"][data-state="checked"] {
background-color: var(--hm-switch-on-background);
}
[data-slot="switch"]:hover:not(:disabled) {
background-image: linear-gradient(var(--hm-switch-hover-overlay), var(--hm-switch-hover-overlay));
}
[data-slot="switch"]:focus-visible {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000);
}
[data-slot="switch"]:disabled {
cursor: not-allowed;
}
[data-slot="switch-thumb"] {
width: var(--hm-switch-thumb-size);
height: var(--hm-switch-thumb-size);
border-radius: 9999px;
background: var(--hm-switch-thumb-color);
transform: translateX(var(--hm-switch-thumb-offset));
}
[data-slot="switch-thumb"][data-state="checked"] {
transform: translateX(calc(var(--hm-switch-width) - var(--hm-switch-thumb-size) - var(--hm-switch-thumb-offset)));
}
+93
View File
@@ -0,0 +1,93 @@
/* Table mapping. Reference: ui-viz docs/components/table.md (an earlier test spec: green header strip, 1px row divider, 14px/12px cell
padding, 520px default minimum width, no radius, hover face only). Current HM values built from the HM Semantic / Primitive tokens;
the selected row and the footer are not defined there, so they are new here. Static display only: no sorting / filtering / paging. */
:root, [data-hm-theme] {
--hm-table-min-width: 520px;
--hm-table-background: var(--hm-sem-bg-surface);
--hm-table-text: var(--hm-sem-fg-primary);
--hm-table-font-size: 14px;
--hm-table-line-height: 20px;
--hm-table-font-weight: 400;
--hm-table-cell-padding-y: 14px;
--hm-table-cell-padding-x: 12px;
--hm-table-divider-width: 1px;
--hm-table-divider-color: var(--hm-sem-stroke-input-default);
/* Header strip: a pale green face with a green top edge and a soft bottom edge. */
--hm-table-header-background: color-mix(in srgb, var(--hm-sem-palette-green-tint) 30%, var(--hm-sem-palette-white));
--hm-table-header-text: var(--hm-sem-palette-green-light-1);
--hm-table-header-font-size: 13px;
--hm-table-header-font-weight: 700;
--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);
--hm-table-footer-background: var(--hm-sem-bg-subtle);
--hm-table-footer-font-weight: 500;
--hm-table-caption-color: var(--hm-sem-fg-secondary);
--hm-table-caption-font-size: 12px;
--hm-table-caption-line-height: 18px;
--hm-table-caption-gap: 12px;
}
[data-slot="table"] {
min-width: var(--hm-table-min-width);
border-collapse: collapse;
background: var(--hm-table-background);
color: var(--hm-table-text);
font-size: var(--hm-table-font-size);
line-height: var(--hm-table-line-height);
font-weight: var(--hm-table-font-weight);
}
[data-slot="table-head"],
[data-slot="table-cell"] {
padding: var(--hm-table-cell-padding-y) var(--hm-table-cell-padding-x);
overflow-wrap: anywhere;
font-variant-numeric: tabular-nums;
}
[data-slot="table-head"] {
background: var(--hm-table-header-background);
color: var(--hm-table-header-text);
font-size: var(--hm-table-header-font-size);
font-weight: var(--hm-table-header-font-weight);
border-top: var(--hm-table-divider-width) solid var(--hm-table-header-border-top);
border-bottom: var(--hm-table-divider-width) solid var(--hm-table-header-border-bottom);
}
[data-slot="table-cell"] {
border-top: var(--hm-table-divider-width) solid var(--hm-table-divider-color);
}
/* The header edge replaces the divider of the first body row. */
[data-slot="table-header"] + [data-slot="table-body"] > [data-slot="table-row"]:first-child > [data-slot="table-cell"] {
border-top: 0;
}
/* Rows: hover colors the face only (body rows, not the header). */
[data-slot="table-body"] > [data-slot="table-row"]:hover {
background: var(--hm-table-row-hover-background);
}
[data-slot="table-row"][data-state="selected"],
[data-slot="table-body"] > [data-slot="table-row"][data-state="selected"]:hover {
background: var(--hm-table-row-selected-background);
}
[data-slot="table-footer"] {
background: var(--hm-table-footer-background);
font-weight: var(--hm-table-footer-font-weight);
}
[data-slot="table-footer"] [data-slot="table-cell"] {
border-top: var(--hm-table-divider-width) solid var(--hm-table-divider-color);
}
[data-slot="table-caption"] {
margin-top: var(--hm-table-caption-gap);
text-align: left;
color: var(--hm-table-caption-color);
font-size: var(--hm-table-caption-font-size);
line-height: var(--hm-table-caption-line-height);
}
+200
View File
@@ -0,0 +1,200 @@
/* Tabs mapping: default Primary Fill (content), Secondary Fill (surface), Legacy non-fill and overflow move buttons (D-090). Horizontal, text-only, no size/variant.
States use Radix attributes: [data-state="active"|"inactive"], :hover, :focus-visible. */
:root, [data-hm-theme] {
/* ui_viz: docs/components/tabs.md §8, §16 — SOURCE_ADOPTED / COMPONENT_SCOPED (owner 2026-09-18). Not aliased to Button or radius/border tokens. */
--hm-tab-height: 34px;
--hm-tab-padding-x: 16px;
--hm-tab-radius-top: 4px;
--hm-tab-border-width: 1px;
/* ui_viz: docs/components/tabs.md §9 — Navigation Label 16/24/700, same weight for Selected */
--hm-tab-font-size: 16px;
--hm-tab-line-height: 24px;
--hm-tab-font-weight: 700;
/* ui_viz: docs/components/tabs.md §10 — SOURCE_ADOPTED. Default face and border are also marked TEMPORARY there (alpha composites of
Green tint / Green Light 2). Selected border is #DEDBD2 (color.stroke.tab.selected); no Semantic exists in HM, so the Primitive is used here. */
--hm-tab-background: color-mix(in srgb, var(--hm-sem-palette-green-tint) 45%, transparent);
--hm-tab-border-color: color-mix(in srgb, var(--hm-sem-palette-green-light-2) 35%, transparent);
--hm-tab-text: var(--hm-sem-palette-green-light-2);
--hm-tab-selected-background: var(--hm-sem-bg-surface);
--hm-tab-selected-border-color: var(--hm-sem-stroke-input-default);
--hm-tab-selected-text: var(--hm-sem-action-primary);
/* Hover (inactive only): Black 20% overlay — value Confirmed (tab.md 6, 8); foreground SOURCE_ADOPTED */
--hm-tab-hover-overlay: color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent);
--hm-tab-hover-text: var(--hm-sem-action-primary);
--hm-tab-transition-duration: 0.15s;
}
/* Frame holds the Bar (and the move buttons when the tabs overflow). The whole Bar overlaps the content boundary by 1px; Selected paints its bottom stroke in the surface color. */
[data-slot="tabs-frame"] {
display: flex;
align-items: flex-end;
width: 100%;
margin-bottom: -1px;
}
/* Bar: no surface of its own, gap 0, scrolls horizontally with the native scrollbar hidden. */
[data-slot="tabs-list"] {
display: flex;
align-items: flex-end;
flex: 0 1 auto;
gap: 0;
min-width: 0;
max-width: 100%;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
}
[data-slot="tabs-trigger"] {
box-sizing: border-box;
height: var(--hm-tab-height);
min-height: var(--hm-tab-height);
padding: 0 var(--hm-tab-padding-x);
border: var(--hm-tab-border-width) solid var(--hm-tab-border-color);
border-bottom-color: var(--hm-tab-selected-border-color);
border-radius: var(--hm-tab-radius-top) var(--hm-tab-radius-top) 0 0;
background-color: var(--hm-tab-background);
background-image: none;
color: var(--hm-tab-text);
font-size: var(--hm-tab-font-size);
line-height: var(--hm-tab-line-height);
font-weight: var(--hm-tab-font-weight);
box-shadow: none;
transition: color var(--hm-tab-transition-duration) ease, background-color var(--hm-tab-transition-duration) ease;
}
/* One shared border between neighbours. Must come before the Selected rule. */
[data-slot="tabs-trigger"] + [data-slot="tabs-trigger"] {
border-left-width: 0;
}
[data-slot="tabs-trigger"][data-state="active"] {
z-index: 2;
border-color: var(--hm-tab-selected-border-color);
border-bottom-color: var(--hm-tab-selected-background);
background-color: var(--hm-tab-selected-background);
color: var(--hm-tab-selected-text);
}
[data-slot="tabs-trigger"][data-state="inactive"]:hover:not(:disabled) {
background-image: linear-gradient(var(--hm-tab-hover-overlay), var(--hm-tab-hover-overlay));
color: var(--hm-tab-hover-text);
}
/* Focus is a global TBD in ui_viz: the shadcn focus ring from ui/tabs.tsx stays. The theme box-shadow is none, so keep the ring variables. */
[data-slot="tabs-trigger"]:focus-visible {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000);
}
/* ---- Overflow move buttons (ui_viz tabs.md §24 D-090, CURRENT 2026-10-01). Outside the tablist, shown only when the tabs overflow. ---- */
:root, [data-hm-theme] {
--hm-tab-arrow-size: 34px;
--hm-tab-arrow-icon-size: 18px;
--hm-tab-arrow-background: var(--hm-sem-palette-green-light-1);
--hm-tab-arrow-border-color: var(--hm-sem-palette-green-dark-1);
--hm-tab-arrow-icon-color: var(--hm-sem-on-action-primary);
--hm-tab-arrow-disabled-opacity: 0.6;
}
[data-slot="tabs-frame"][data-overflow="true"] > [data-slot="tabs-list"] {
flex: 1 1 0;
}
[data-slot="tabs-arrow"] {
display: inline-flex;
flex: none;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: var(--hm-tab-arrow-size);
height: var(--hm-tab-arrow-size);
padding: 0;
border: var(--hm-tab-border-width) solid var(--hm-tab-arrow-border-color);
background-color: var(--hm-tab-arrow-background);
background-image: none;
color: var(--hm-tab-arrow-icon-color);
cursor: pointer;
outline: none;
}
[data-slot="tabs-arrow"][data-direction="previous"] { border-radius: var(--hm-tab-radius-top) 0 0 0; }
[data-slot="tabs-arrow"][data-direction="next"] { border-radius: 0 var(--hm-tab-radius-top) 0 0; }
[data-slot="tabs-arrow"] svg { width: var(--hm-tab-arrow-icon-size); height: var(--hm-tab-arrow-icon-size); }
[data-slot="tabs-arrow"]:hover:not(:disabled) {
background-image: linear-gradient(var(--hm-tab-hover-overlay), var(--hm-tab-hover-overlay));
}
[data-slot="tabs-arrow"]:disabled {
opacity: var(--hm-tab-arrow-disabled-opacity);
cursor: default;
pointer-events: none;
}
/* Focus value is global TBD: shadcn ring level kept (ring variables come from the tailwind focus-visible utilities). */
[data-slot="tabs-arrow"]:focus-visible {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
/* In the overflow state every item keeps its own label width (no equal split). */
[data-slot="tabs-frame"][data-overflow="true"] [data-slot="tabs-trigger"] {
flex: 0 0 auto;
}
/* ---- Fill profile (ui_viz tabs.md §24 D-071~075): default Primary content; explicit surface is Secondary. ---- */
:root, [data-hm-theme] {
--hm-tab-fill-line-color: var(--hm-sem-palette-green-light-3);
/* REVIEW VALUE (ui_viz tabs.md §24: curve radius 8px, BORDER FOLLOW-UP). The curve itself is Confirmed, the radius is not. */
--hm-tab-fill-curve-radius: 8px;
}
[data-slot="tabs-frame"][data-fill="true"] {
--hm-tab-fill-panel: var(--hm-sem-bg-surface);
--hm-tab-fill-face: linear-gradient(var(--hm-sem-bg-surface), var(--hm-sem-bg-surface));
}
[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%);
}
[data-slot="tabs-frame"][data-fill="true"] > [data-slot="tabs-list"] {
flex: 1 1 auto;
width: 100%;
}
[data-slot="tabs-frame"][data-fill="true"][data-overflow="false"] [data-slot="tabs-trigger"] {
flex: 1 1 0;
min-width: auto;
}
/* Inactive: only the bottom line is Green Light 3; face and top / vertical borders keep the basic profile values. */
[data-slot="tabs-frame"][data-fill="true"] [data-slot="tabs-trigger"] {
border-bottom-color: var(--hm-tab-fill-line-color);
}
/* Every fill neighbour restores its left border and overlaps it by 1px, matching the tested ui-viz geometry.
The active tab's z-index keeps its Green edge above the inactive border without producing a double-width line. */
[data-slot="tabs-frame"][data-fill="true"] [data-slot="tabs-trigger"] + [data-slot="tabs-trigger"] {
margin-left: calc(-1 * var(--hm-tab-border-width));
border-left-width: var(--hm-tab-border-width);
}
/* Selected: Green Light 3 top / left / right, bottom open in the panel color. */
[data-slot="tabs-frame"][data-fill="true"] [data-slot="tabs-trigger"][data-state="active"] {
border-color: var(--hm-tab-fill-line-color);
border-bottom-color: var(--hm-tab-fill-panel);
background-color: transparent;
background-image: var(--hm-tab-fill-face);
}
/* Curves: a quarter arc of the vertical Green line toward each neighbour, 1px Green. Inside the arc is transparent (the neighbour shows),
outside is the panel color. Decoration only: no pointer input. */
[data-slot="tabs-frame"][data-fill="true"] [data-slot="tabs-trigger"][data-state="active"]:not(:last-child)::after,
[data-slot="tabs-frame"][data-fill="true"] [data-slot="tabs-trigger"][data-state="active"]:not(:first-child)::before {
content: "";
position: absolute;
bottom: calc(-1 * var(--hm-tab-border-width));
width: var(--hm-tab-fill-curve-radius);
height: var(--hm-tab-fill-curve-radius);
pointer-events: none;
}
[data-slot="tabs-frame"][data-fill="true"] [data-slot="tabs-trigger"][data-state="active"]:not(:last-child)::after {
right: calc(-1 * var(--hm-tab-fill-curve-radius));
background: radial-gradient(circle at 100% 0, transparent calc(var(--hm-tab-fill-curve-radius) - var(--hm-tab-border-width)),
var(--hm-tab-fill-line-color) calc(var(--hm-tab-fill-curve-radius) - var(--hm-tab-border-width)),
var(--hm-tab-fill-line-color) var(--hm-tab-fill-curve-radius), var(--hm-tab-fill-panel) var(--hm-tab-fill-curve-radius));
}
[data-slot="tabs-frame"][data-fill="true"] [data-slot="tabs-trigger"][data-state="active"]:not(:first-child)::before {
left: calc(-1 * var(--hm-tab-fill-curve-radius));
background: radial-gradient(circle at 0 0, transparent calc(var(--hm-tab-fill-curve-radius) - var(--hm-tab-border-width)),
var(--hm-tab-fill-line-color) calc(var(--hm-tab-fill-curve-radius) - var(--hm-tab-border-width)),
var(--hm-tab-fill-line-color) var(--hm-tab-fill-curve-radius), var(--hm-tab-fill-panel) var(--hm-tab-fill-curve-radius));
}
+82
View File
@@ -0,0 +1,82 @@
/* Textarea mapping: the control only. Label, Required star and Helper/Error message are consumer composition, not part of Textarea.
States use native selectors (:focus-visible, [aria-invalid="true"], :disabled, [readonly]); resize uses data-resize.
Input and Textarea values are separate Tokens even where the numbers match (ui_viz keeps them as separate component owners). */
:root, [data-hm-theme] {
/* ui_viz: docs/components/textarea.md §8.1 TEXTAREA-01/02 — SCOPED_DESIGN_APPROVAL (2026-09-17 owner, Textarea component-local).
Not D-093 Confirmed; Canonical min-height and inset stay TBD in ui_viz. Not promoted to size.* or space.* scale. */
--hm-textarea-min-height: 112px;
--hm-textarea-padding-block: 8px;
--hm-textarea-padding-inline: 12px;
/* ui_viz: docs/components/textarea.md §2 Typography — Value 16/24/400 (existing role) */
--hm-textarea-font-size: 16px;
--hm-textarea-line-height: 24px;
--hm-textarea-font-weight: 400;
/* ui_viz: docs/components/textarea.md §2, §2.2, §3.3 — Confirmed (border 1px, radius 4px, surface, colors, Focus/Error 1.5px) */
--hm-textarea-radius: var(--hm-radius-input);
--hm-textarea-border-width: 1px;
--hm-textarea-state-border-width: 1.5px;
--hm-textarea-background: var(--hm-sem-bg-surface);
--hm-textarea-text: var(--hm-sem-fg-primary);
--hm-textarea-placeholder: var(--hm-sem-fg-placeholder);
--hm-textarea-border-color: var(--hm-sem-stroke-input-default);
--hm-textarea-focus-border-color: var(--hm-sem-stroke-input-focus);
--hm-textarea-error-border-color: var(--hm-sem-action-destructive);
}
[data-slot="textarea"] {
box-sizing: border-box;
min-height: var(--hm-textarea-min-height);
padding: var(--hm-textarea-padding-block) var(--hm-textarea-padding-inline);
border: var(--hm-textarea-border-width) solid var(--hm-textarea-border-color);
border-radius: var(--hm-textarea-radius);
background: var(--hm-textarea-background);
color: var(--hm-textarea-text);
font-size: var(--hm-textarea-font-size);
line-height: var(--hm-textarea-line-height);
font-weight: var(--hm-textarea-font-weight);
box-shadow: none;
resize: vertical;
}
[data-slot="textarea"][data-resize="none"] { resize: none; }
[data-slot="textarea"][data-resize="both"] { resize: both; }
[data-slot="textarea"]::placeholder {
color: var(--hm-textarea-placeholder);
opacity: 1;
}
/* Hover is identical to Default (textarea.md §3.3, TEXTAREA-05 user decision): no hover rule. */
/* Focus: the border alone marks it (1.5px, no shadow or glow). Padding gives back the extra 0.5px so the value does not move. */
[data-slot="textarea"]:focus-visible {
outline: none;
border-width: var(--hm-textarea-state-border-width);
border-color: var(--hm-textarea-focus-border-color);
padding: calc(var(--hm-textarea-padding-block) - (var(--hm-textarea-state-border-width) - var(--hm-textarea-border-width)))
calc(var(--hm-textarea-padding-inline) - (var(--hm-textarea-state-border-width) - var(--hm-textarea-border-width)));
}
/* Invalid owns the border; keyboard focus adds the same HM 3px / 50% semantic ring as Input. */
[data-slot="textarea"][aria-invalid="true"] {
border-width: var(--hm-textarea-state-border-width);
border-color: var(--hm-textarea-error-border-color);
padding: calc(var(--hm-textarea-padding-block) - (var(--hm-textarea-state-border-width) - var(--hm-textarea-border-width)))
calc(var(--hm-textarea-padding-inline) - (var(--hm-textarea-state-border-width) - var(--hm-textarea-border-width)));
}
[data-slot="textarea"][aria-invalid="true"]:focus-visible {
border-color: var(--hm-textarea-error-border-color);
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
/* Disabled keeps the surface face. Opacity 0.6 is set in ui/textarea.tsx: ui_viz `.6` is Temporary; 2026-10-02 user decision extends it to Textarea. */
[data-slot="textarea"]:disabled {
background: var(--hm-textarea-background);
}
/* Readonly (textarea.md §3.1): keeps the Default surface, border and typography, not dimmed, no resize affordance whatever `resize` says. */
[data-slot="textarea"][readonly] {
resize: none;
opacity: 1;
}
+111
View File
@@ -0,0 +1,111 @@
/* Toggle Group mapping. ui-viz has no Toggle Group spec, so these are current HM values built from the HM Semantic / Primitive
tokens. The items share the same selection language as Toggle (full Accent face when on) but use their own --hm-toggle-group-* tokens
and do not depend on Toggle's CSS. spacing={0} (default) connects the items into a segmented bar; spacing > 0 separates them
with a gap. Radix sets data-orientation only when `orientation` is given, so "horizontal" is selected as :not([data-orientation="vertical"]).
States use Radix attributes ([data-state="on"|"off"], :disabled) and :hover.
The look is in `@layer components` so a consumer's utility classes still win. */
:root, [data-hm-theme] {
--hm-toggle-group-height-sm: 28px;
--hm-toggle-group-height-md: 34px;
--hm-toggle-group-height-lg: 42px;
--hm-toggle-group-padding-x-sm: 8px;
--hm-toggle-group-padding-x-md: 12px;
--hm-toggle-group-padding-x-lg: 16px;
--hm-toggle-group-radius: var(--hm-radius-control-md);
--hm-toggle-group-gap: 8px;
--hm-toggle-group-font-size: 14px;
--hm-toggle-group-line-height: 20px;
--hm-toggle-group-font-weight: 500;
--hm-toggle-group-off-background: transparent;
--hm-toggle-group-off-text: var(--hm-sem-fg-primary);
--hm-toggle-group-hover-background: var(--hm-sem-surface-highlight);
--hm-toggle-group-on-background: var(--accent);
--hm-toggle-group-on-text: var(--accent-foreground);
--hm-toggle-group-border-width: 1px;
--hm-toggle-group-border-color: var(--hm-sem-stroke-input-default);
/* Current HM value for this component only; not a global Disabled rule. */
--hm-toggle-group-disabled-opacity: 0.6;
}
@layer components {
[data-slot="toggle-group-item"] {
--hm-toggle-group-item-height: var(--hm-toggle-group-height-md);
--hm-toggle-group-item-padding-x: var(--hm-toggle-group-padding-x-md);
box-sizing: border-box;
gap: 8px;
height: var(--hm-toggle-group-item-height);
min-width: var(--hm-toggle-group-item-height);
padding: 0 var(--hm-toggle-group-item-padding-x);
border: var(--hm-toggle-group-border-width) solid transparent;
border-radius: var(--hm-toggle-group-radius);
background: var(--hm-toggle-group-off-background);
color: var(--hm-toggle-group-off-text);
font-size: var(--hm-toggle-group-font-size);
line-height: var(--hm-toggle-group-line-height);
font-weight: var(--hm-toggle-group-font-weight);
cursor: pointer;
}
[data-slot="toggle-group-item"][data-size="sm"] {
--hm-toggle-group-item-height: var(--hm-toggle-group-height-sm);
--hm-toggle-group-item-padding-x: var(--hm-toggle-group-padding-x-sm);
}
[data-slot="toggle-group-item"][data-size="lg"] {
--hm-toggle-group-item-height: var(--hm-toggle-group-height-lg);
--hm-toggle-group-item-padding-x: var(--hm-toggle-group-padding-x-lg);
}
[data-slot="toggle-group-item"][data-icon-only]:has(> svg:only-child) {
padding: 0;
}
[data-slot="toggle-group-item"][data-variant="outline"] {
border-color: var(--hm-toggle-group-border-color);
}
[data-slot="toggle-group-item"]:hover:not(:disabled) {
background: var(--hm-toggle-group-hover-background);
}
[data-slot="toggle-group-item"][data-state="on"],
[data-slot="toggle-group-item"][data-state="on"]:hover:not(:disabled) {
background: var(--hm-toggle-group-on-background);
color: var(--hm-toggle-group-on-text);
position: relative;
z-index: 1;
}
[data-slot="toggle-group-item"]:disabled {
opacity: var(--hm-toggle-group-disabled-opacity);
cursor: not-allowed;
}
/* Gap between separated items (spacing > 0). */
[data-slot="toggle-group"] {
gap: var(--hm-toggle-group-gap);
}
/* Connected (spacing 0): square inner corners, round outer corners, outline borders overlap by one border width. */
[data-slot="toggle-group"][data-spacing="0"] [data-slot="toggle-group-item"] {
border-radius: 0;
}
[data-slot="toggle-group"][data-spacing="0"]:not([data-orientation="vertical"]) [data-slot="toggle-group-item"]:first-child {
border-top-left-radius: var(--hm-toggle-group-radius);
border-bottom-left-radius: var(--hm-toggle-group-radius);
}
[data-slot="toggle-group"][data-spacing="0"]:not([data-orientation="vertical"]) [data-slot="toggle-group-item"]:last-child {
border-top-right-radius: var(--hm-toggle-group-radius);
border-bottom-right-radius: var(--hm-toggle-group-radius);
}
[data-slot="toggle-group"][data-spacing="0"][data-orientation="vertical"] [data-slot="toggle-group-item"]:first-child {
border-top-left-radius: var(--hm-toggle-group-radius);
border-top-right-radius: var(--hm-toggle-group-radius);
}
[data-slot="toggle-group"][data-spacing="0"][data-orientation="vertical"] [data-slot="toggle-group-item"]:last-child {
border-bottom-left-radius: var(--hm-toggle-group-radius);
border-bottom-right-radius: var(--hm-toggle-group-radius);
}
[data-slot="toggle-group"][data-spacing="0"]:not([data-orientation="vertical"]) [data-slot="toggle-group-item"][data-variant="outline"]:not(:first-child) {
margin-left: calc(-1 * var(--hm-toggle-group-border-width));
}
[data-slot="toggle-group"][data-spacing="0"][data-orientation="vertical"] [data-slot="toggle-group-item"][data-variant="outline"]:not(:first-child) {
margin-top: calc(-1 * var(--hm-toggle-group-border-width));
}
}
+77
View File
@@ -0,0 +1,77 @@
/* Toggle mapping. ui-viz has no Toggle spec, so these are current HM values built from the HM Semantic / Primitive tokens.
Toggle is a persistent pressed-state control, not an action button: it has its own look and does not reference any
--hm-button-* token or the Button roles. States use the Radix attributes ([data-state="on"|"off"], :disabled) and :hover.
The default look is in `@layer components` so a consumer's utility classes (width, height, rounded, layout) still win. */
:root, [data-hm-theme] {
--hm-toggle-height-sm: 28px;
--hm-toggle-height-md: 34px;
--hm-toggle-height-lg: 42px;
--hm-toggle-padding-x-sm: 8px;
--hm-toggle-padding-x-md: 12px;
--hm-toggle-padding-x-lg: 16px;
--hm-toggle-radius: var(--hm-radius-control-md);
--hm-toggle-gap: 8px;
--hm-toggle-font-size: 14px;
--hm-toggle-line-height: 20px;
--hm-toggle-font-weight: 500;
--hm-toggle-off-background: transparent;
--hm-toggle-off-text: var(--hm-sem-fg-primary);
--hm-toggle-hover-background: var(--hm-sem-surface-highlight);
/* On = the persistent selected surface (full Accent), independent of Active and Focus. */
--hm-toggle-on-background: var(--accent);
--hm-toggle-on-text: var(--accent-foreground);
--hm-toggle-outline-border-width: 1px;
--hm-toggle-outline-border-color: var(--hm-sem-stroke-input-default);
/* Current HM value for this component only; not a global Disabled rule. */
--hm-toggle-disabled-opacity: 0.6;
}
@layer components {
[data-slot="toggle"] {
--hm-toggle-height: var(--hm-toggle-height-md);
--hm-toggle-padding-x: var(--hm-toggle-padding-x-md);
box-sizing: border-box;
gap: var(--hm-toggle-gap);
height: var(--hm-toggle-height);
min-width: var(--hm-toggle-height);
padding: 0 var(--hm-toggle-padding-x);
border: var(--hm-toggle-outline-border-width) solid transparent;
border-radius: var(--hm-toggle-radius);
background: var(--hm-toggle-off-background);
color: var(--hm-toggle-off-text);
font-size: var(--hm-toggle-font-size);
line-height: var(--hm-toggle-line-height);
font-weight: var(--hm-toggle-font-weight);
cursor: pointer;
}
[data-slot="toggle"][data-size="sm"] {
--hm-toggle-height: var(--hm-toggle-height-sm);
--hm-toggle-padding-x: var(--hm-toggle-padding-x-sm);
}
[data-slot="toggle"][data-size="lg"] {
--hm-toggle-height: var(--hm-toggle-height-lg);
--hm-toggle-padding-x: var(--hm-toggle-padding-x-lg);
}
/* Icon-only: a square of the control height. */
[data-slot="toggle"][data-icon-only]:has(> svg:only-child) {
padding: 0;
}
[data-slot="toggle"][data-variant="outline"] {
border-color: var(--hm-toggle-outline-border-color);
}
[data-slot="toggle"]:hover:not(:disabled) {
background: var(--hm-toggle-hover-background);
}
[data-slot="toggle"][data-state="on"],
[data-slot="toggle"][data-state="on"]:hover:not(:disabled) {
background: var(--hm-toggle-on-background);
color: var(--hm-toggle-on-text);
}
[data-slot="toggle"]:disabled {
opacity: var(--hm-toggle-disabled-opacity);
cursor: not-allowed;
}
}
+30
View File
@@ -0,0 +1,30 @@
/* Tooltip mapping. ui-viz has no Tooltip spec (inventory: behavior and design pending), so these are current HM values from the HM
Semantic tokens. Short, non-interactive text on the inverse (dark) face; the arrow shares the face color. */
:root, [data-hm-theme] {
--hm-tooltip-background: var(--hm-sem-fg-primary);
--hm-tooltip-text: var(--hm-sem-on-action-primary);
--hm-tooltip-radius: var(--hm-radius-container);
--hm-tooltip-padding-x: 10px;
--hm-tooltip-padding-y: 6px;
--hm-tooltip-max-width: 240px;
--hm-tooltip-font-size: 12px;
--hm-tooltip-line-height: 18px;
--hm-tooltip-font-weight: 400;
}
[data-slot="tooltip-content"] {
box-sizing: border-box;
max-width: var(--hm-tooltip-max-width);
padding: var(--hm-tooltip-padding-y) var(--hm-tooltip-padding-x);
border-radius: var(--hm-tooltip-radius);
background: var(--hm-tooltip-background);
color: var(--hm-tooltip-text);
font-size: var(--hm-tooltip-font-size);
line-height: var(--hm-tooltip-line-height);
font-weight: var(--hm-tooltip-font-weight);
overflow-wrap: anywhere;
}
[data-slot="tooltip-arrow"] {
background: var(--hm-tooltip-background);
fill: var(--hm-tooltip-background);
}