From 2b3701cbe3f2358b36b470df49a303d34191936e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Chyl=C3=ADk?= Date: Tue, 19 May 2026 20:52:31 +0200 Subject: [PATCH 01/60] feat(figma-variables): Figma variables exported 2026-05-19 19:52 CET Format: CSS variables Exported themes: 2 themes (light, dark) Base branch: master --- libs/ui/src/tokens/figma/dark/variables.css | 146 +++++++++++------- libs/ui/src/tokens/figma/light/variables.css | 148 ++++++++++++------- 2 files changed, 191 insertions(+), 103 deletions(-) diff --git a/libs/ui/src/tokens/figma/dark/variables.css b/libs/ui/src/tokens/figma/dark/variables.css index ed1193751..49c31451f 100644 --- a/libs/ui/src/tokens/figma/dark/variables.css +++ b/libs/ui/src/tokens/figma/dark/variables.css @@ -1,6 +1,6 @@ /* * Design tokens exported from Figma - * Exported at: 2026-05-15T07:51:19.180Z + * Exported at: 2026-05-19T18:52:26.634Z * Format: Raw CSS variables grouped by kind */ :root { @@ -303,6 +303,7 @@ --color-header-nav-fg-active: oklch(0.827 0.109 229.198); --color-header-nav-fg-hover: oklch(1 0 0); --color-header-nav-item-bg: oklch(0 0 0 / 0); + --color-header-nav-item-bg-hover: oklch(0 0 0 / 0); --color-highlight-base: oklch(0.373 0.034 260.169); --color-icon-fg-base: oklch(1 0 0); --color-icon-fg-danger: oklch(0.702 0.189 22.228); @@ -447,30 +448,36 @@ --color-radio-card-item-bg-solid-checked: oklch(0.827 0.109 229.198); --color-radio-card-item-bg-subtle-checked: oklch(0.937 0.03 251.281); --color-radio-card-item-border-base: oklch(1 0 0 / 0.15); - --color-radio-card-item-border-checked: oklch(0.827 0.109 229.198); --color-radio-card-item-border-disabled: oklch(0.373 0.034 260.169); --color-radio-card-item-border-error: oklch(0.702 0.189 22.228); - --color-radio-card-item-description: oklch(0.494 0.037 261.595); + --color-radio-card-item-border-outline-checked: oklch(0.827 0.109 229.198); + --color-radio-card-item-description-fg: oklch(0.494 0.037 261.595); --color-radio-card-item-fg-base: oklch(1 0 0); --color-radio-card-item-fg-disabled: oklch(0.551 0.027 264.328); --color-radio-card-item-fg-solid-checked: oklch(1 0 0); --color-radio-card-item-indicator-bg: oklch(0 0 0 / 0); --color-radio-card-item-indicator-border: oklch(1 0 0 / 0.15); - --color-radio-card-item-indicator-content: oklch(0.507 0.063 255.385); + --color-radio-card-item-indicator-content-fg: oklch(0.507 0.063 255.385); --color-radio-group-item-bg-base: oklch(1 0 0 / 0.05); --color-radio-group-item-bg-disabled: oklch(0.278 0.034 257.672); + --color-radio-group-item-bg-outline-checked: oklch(1 0 0 / 0.05); --color-radio-group-item-bg-solid-checked: oklch(0.827 0.109 229.198); --color-radio-group-item-bg-subtle-checked: oklch(0.937 0.03 251.281); --color-radio-group-item-border-base: oklch(1 0 0 / 0.15); --color-radio-group-item-border-checked: oklch(0.827 0.109 229.198); --color-radio-group-item-border-disabled: oklch(0.373 0.034 260.169); --color-radio-group-item-border-error: oklch(0.702 0.189 22.228); + --color-radio-group-item-border-outline-checked: oklch(0.507 0.063 255.385); + --color-radio-group-item-border-solid-checked: oklch(0.827 0.109 229.198); + --color-radio-group-item-border-subtle-checked: oklch(0.937 0.03 251.281); --color-radio-group-item-description: oklch(0.494 0.037 261.595); + --color-radio-group-item-description-disabled: oklch(0.551 0.027 264.328); --color-radio-group-item-fg-base: oklch(1 0 0); --color-radio-group-item-fg-disabled: oklch(0.551 0.027 264.328); --color-radio-group-item-indicator-disabled: oklch(0.551 0.027 264.328); --color-radio-group-item-indicator-outline: oklch(0.507 0.063 255.385); --color-radio-group-item-indicator-solid: oklch(1 0 0); + --color-radio-group-item-indicator-subtle: oklch(0.507 0.063 255.385); --color-radio-group-ring: oklch(1 0 0); --color-rating-fg-active: oklch(0.798 0.164 82.706); --color-rating-fg-base: oklch(0.373 0.034 260.169); @@ -479,7 +486,7 @@ --color-ring-default-base: oklch(1 0 0); --color-search-form-bg-base: oklch(1 0 0 / 0.05); --color-search-form-bg-disabled: oklch(0.278 0.034 257.672); - --color-search-form-bg-focus: oklch(1 0 0 / 0.15); + --color-search-form-bg-focus: oklch(1 0 0 / 0.05); --color-search-form-border-base: oklch(1 0 0 / 0.15); --color-search-form-border-disabled: oklch(0.373 0.034 260.169); --color-search-form-border-focus: oklch(0.827 0.109 229.198); @@ -610,7 +617,7 @@ --color-tabs-ring: oklch(1 0 0); --color-tabs-trigger-bg-hover: oklch(1 0 0 / 0.1); --color-tabs-trigger-bg-outline-selected: oklch(0.937 0.03 251.281); - --color-tabs-trigger-bg-solid-selected: oklch(0.827 0.109 229.198); + --color-tabs-trigger-bg-selected: oklch(0.827 0.109 229.198); --color-tabs-trigger-fg-base: oklch(0.494 0.037 261.595); --color-tabs-trigger-fg-disabled: oklch(0.551 0.027 264.328); --color-tabs-trigger-fg-selected: oklch(0.827 0.109 229.198); @@ -642,18 +649,18 @@ --color-textarea-fg-disabled: oklch(0.551 0.027 264.328); --color-textarea-fg-placeholder: oklch(0.554 0.041 257.417); --color-textarea-ring: oklch(1 0 0); - --color-toast-bg-base: oklch(0.373 0.034 260.169); - --color-toast-bg-error: oklch(0.702 0.189 22.228); - --color-toast-bg-info: oklch(0.704 0.123 182.503); - --color-toast-bg-success: oklch(0.844 0.143 165.23); - --color-toast-bg-warning: oklch(0.798 0.164 82.706); + --color-toast-bg: oklch(0.373 0.034 260.169); --color-toast-border-base: oklch(1 0 0 / 0.15); - --color-toast-border-error: oklch(0.702 0.189 22.228); - --color-toast-border-info: oklch(0.704 0.123 182.503); - --color-toast-border-success: oklch(0.844 0.143 165.23); - --color-toast-border-warning: oklch(0.798 0.164 82.706); - --color-toast-close: oklch(1 0 0); + --color-toast-close-fg: oklch(1 0 0); + --color-toast-error-bg: oklch(0.702 0.189 22.228); + --color-toast-error-border: oklch(0.702 0.189 22.228); --color-toast-fg: oklch(1 0 0); + --color-toast-info-bg: oklch(0.704 0.123 182.503); + --color-toast-info-border: oklch(0.704 0.123 182.503); + --color-toast-success-bg: oklch(0.844 0.143 165.23); + --color-toast-success-border: oklch(0.844 0.143 165.23); + --color-toast-warning-bg: oklch(0.798 0.164 82.706); + --color-toast-warning-border: oklch(0.798 0.164 82.706); --color-tooltip-arrow: oklch(0.278 0.034 257.672); --color-tooltip-bg: oklch(0.278 0.034 257.672); --color-tooltip-border-outline: oklch(1 0 0 / 0.15); @@ -708,7 +715,7 @@ --font-weight-badge-xl: 700; --font-weight-bold: 700; --font-weight-dialog-title: 600; - --font-weight-footer-link: 500; + --font-weight-footer-link: 400; --font-weight-footer-title: 600; --font-weight-header-nav: 500; --font-weight-header-nav-active: 600; @@ -716,12 +723,13 @@ --font-weight-light: 300; --font-weight-link: 500; --font-weight-medium: 500; + --font-weight-normal: 400; --font-weight-popover-title: 600; --font-weight-product-card-name: 500; --font-weight-product-card-price: 500; --font-weight-product-card-stock: 500; --font-weight-semibold: 600; - --font-weight-table-caption: 500; + --font-weight-table-caption: 400; --font-weight-table-footer: 500; --font-weight-table-header: 600; /* Measures */ @@ -752,13 +760,17 @@ --border-width-select-validation: 0.19rem; --border-width-slider: 0.06rem; --border-width-slider-error: 0.19rem; - --border-width-steps: 0.06rem; + --border-width-steps-indicator: 0.06rem; --border-width-switch: 0.06rem; --border-width-tabs: 0.13rem; --border-width-textarea: 0.13rem; --border-width-textarea-validation: 0.19rem; --border-width-toast: 0.06rem; --border-width-tooltip: 0.06rem; + --brightness-100: 6.25rem; + --brightness-70: 4.38rem; + --brightness-gallery-trigger: 4.38rem; + --brightness-gallery-trigger-active: 6.25rem; --container-2xl: 42rem; --container-2xs: 18rem; --container-3xl: 48rem; @@ -795,12 +807,14 @@ --dimension-50: 0.19rem; --dimension-500: 2.81rem; --dimension-550: 3.13rem; + --dimension-576: 36rem; --dimension-600: 3.75rem; --dimension-64: 4rem; --dimension-650: 4.06rem; --dimension-70: 4.38rem; --dimension-700: 4.38rem; --dimension-750: 5rem; + --dimension-768: 48rem; --dimension-800: 5.63rem; --dimension-850: 6.25rem; --dimension-900: 6.88rem; @@ -847,6 +861,9 @@ --line-height-status-text-lg: 2.38rem; --line-height-status-text-md: 1.88rem; --line-height-status-text-sm: 1.5rem; + --margin-footer-divider-lg: 2.19rem; + --margin-footer-divider-md: 1.56rem; + --margin-footer-divider-sm: 0.94rem; --padding-accordion-content-x-lg: 1.88rem; --padding-accordion-content-x-md: 0.94rem; --padding-accordion-content-x-sm: 0.31rem; @@ -877,12 +894,14 @@ --padding-dialog-actions-top: 0.63rem; --padding-dialog-close-trigger: 0.63rem; --padding-dialog-content: 2.5rem; + --padding-footer-bottom: 1.56rem; --padding-footer-bottom-lg: 2.19rem; --padding-footer-bottom-md: 1.56rem; --padding-footer-bottom-sm: 0.94rem; --padding-footer-lg: 1.56rem; --padding-footer-md: 0.94rem; --padding-footer-sm: 0.31rem; + --padding-gallery-trigger: 0rem; --padding-header-hamburger-lg: 0.94rem; --padding-header-hamburger-md: 0.63rem; --padding-header-hamburger-sm: 0.31rem; @@ -906,6 +925,9 @@ --padding-popover-lg: 1.88rem; --padding-popover-md: 1.25rem; --padding-popover-sm: 0.94rem; + --padding-radio-card-item-control-lg: 1.25rem; + --padding-radio-card-item-control-md: 0.94rem; + --padding-radio-card-item-control-sm: 0.63rem; --padding-select-clear-trigger: 0.31rem; --padding-select-item-x: 1.25rem; --padding-select-item-y: 0.63rem; @@ -924,6 +946,12 @@ --padding-table-cell-md-y: 0.63rem; --padding-table-cell-sm-x: 0.63rem; --padding-table-cell-sm-y: 0.31rem; + --padding-tabs-trigger-x-lg: 1.25rem; + --padding-tabs-trigger-x-md: 0.94rem; + --padding-tabs-trigger-x-sm: 0.63rem; + --padding-tabs-trigger-y-lg: 0.94rem; + --padding-tabs-trigger-y-md: 0.63rem; + --padding-tabs-trigger-y-sm: 0.31rem; --padding-textarea-lg: 0.91rem; --padding-textarea-md: 0.59rem; --padding-textarea-sm: 0.31rem; @@ -933,6 +961,7 @@ --padding-textarea-y-lg: 0.94rem; --padding-textarea-y-md: 0.63rem; --padding-textarea-y-sm: 0.31rem; + --padding-toast-root: 1.25rem; --padding-tooltip-x-lg: 1.88rem; --padding-tooltip-x-md: 1.25rem; --padding-tooltip-x-sm: 0.94rem; @@ -956,7 +985,7 @@ --radius-dialog-content-center: 0.25rem; --radius-dialog-content-edge: 0rem; --radius-dialog-content-side: 0rem; - --radius-footer-root: 0rem; + --radius-footer: 0rem; --radius-form-control: 0.5rem; --radius-form-control-lg: 0.75rem; --radius-form-control-md: 0.5rem; @@ -970,7 +999,7 @@ --radius-lg: 0.75rem; --radius-md: 0.5rem; --radius-menu: 0.25rem; - --radius-menu-item: 0.25rem; + --radius-menu-item: 0.13rem; --radius-none: 0rem; --radius-numeric-input: 0.5rem; --radius-numeric-input-lg: 0.75rem; @@ -983,7 +1012,7 @@ --radius-radio-card-indicator: 624.94rem; --radius-radio-card-item: 0.5rem; --radius-radio-group-control: 624.94rem; - --radius-search-form-lg: 0.75rem; + --radius-search-form-lg: 0.5rem; --radius-search-form-md: 0.5rem; --radius-search-form-sm: 0.25rem; --radius-select: 0.5rem; @@ -996,6 +1025,7 @@ --radius-sm: 0.25rem; --radius-steps-content: 0.5rem; --radius-steps-indicator: 624.94rem; + --radius-steps-separator: 624.94rem; --radius-switch: 624.94rem; --radius-table: 0.25rem; --radius-tabs: 0rem; @@ -1004,17 +1034,21 @@ --radius-textarea-lg: 0.75rem; --radius-textarea-md: 0.5rem; --radius-textarea-sm: 0.25rem; - --radius-toast: 0.25rem; + --radius-toast-root: 0.25rem; --radius-tooltip: 0.5rem; --radius-tree-view: 0rem; + --radius-xs: 0.13rem; --ring-offset: 0.13rem; --ring-offset: 0.13rem; --ring-width: 0.13rem; --ring-width: 0.13rem; --size-1: 0.06rem; + --size-12: 0.75rem; --size-128: 8rem; + --size-14: 0.88rem; --size-16: 1rem; --size-160: 10rem; + --size-18: 1.13rem; --size-192: 12rem; --size-2: 0.13rem; --size-240: 15rem; @@ -1026,7 +1060,9 @@ --size-44: 2.75rem; --size-48: 3rem; --size-56: 3.5rem; + --size-6: 0.38rem; --size-64: 4rem; + --size-8: 0.5rem; --size-800: 50rem; --size-96: 6rem; --size-carousel-indicator: 0.94rem; @@ -1055,18 +1091,18 @@ --size-image-md: 10rem; --size-image-sm: 5rem; --size-menu-separator-height: 0.06rem; - --size-radio-card-indicator-lg: 1.56rem; - --size-radio-card-indicator-mark-lg: 0.31rem; - --size-radio-card-indicator-mark-md: 0.63rem; - --size-radio-card-indicator-mark-sm: 0.19rem; - --size-radio-card-indicator-md: 1.25rem; - --size-radio-card-indicator-sm: 0.94rem; - --size-radio-group-control-lg: 1.56rem; - --size-radio-group-control-md: 1.25rem; - --size-radio-group-control-sm: 0.94rem; - --size-radio-group-indicator-lg: 0.94rem; + --size-radio-card-indicator-lg: 1.25rem; + --size-radio-card-indicator-mark-lg: 0.63rem; + --size-radio-card-indicator-mark-md: 0.5rem; + --size-radio-card-indicator-mark-sm: 0.38rem; + --size-radio-card-indicator-md: 1.13rem; + --size-radio-card-indicator-sm: 1rem; + --size-radio-group-control-lg: 1.13rem; + --size-radio-group-control-md: 1rem; + --size-radio-group-control-sm: 0.88rem; + --size-radio-group-indicator-lg: 0.75rem; --size-radio-group-indicator-md: 0.63rem; - --size-radio-group-indicator-sm: 0.63rem; + --size-radio-group-indicator-sm: 0.5rem; --size-search-form-lg: 4.38rem; --size-search-form-md: 2.75rem; --size-search-form-sm: 2.06rem; @@ -1123,6 +1159,7 @@ --spacing-footer-container-md: 2.19rem; --spacing-footer-container-sm: 1.56rem; --spacing-footer-lg: 0.63rem; + --spacing-footer-list: 0.31rem; --spacing-footer-md: 0.31rem; --spacing-footer-section-lg: 0.63rem; --spacing-footer-section-md: 0.31rem; @@ -1159,12 +1196,15 @@ --spacing-product-card-col-layout: 0.31rem; --spacing-product-card-padding: 0.63rem; --spacing-product-card-row-layout: 1.25rem; - --spacing-radio-card-lg: 0.94rem; - --spacing-radio-card-md: 0.63rem; - --spacing-radio-card-padding-lg: 1.25rem; - --spacing-radio-card-padding-md: 0.94rem; - --spacing-radio-card-padding-sm: 0.63rem; - --spacing-radio-card-sm: 0.31rem; + --spacing-radio-card-item-content-lg: 0.31rem; + --spacing-radio-card-item-content-md: 0.31rem; + --spacing-radio-card-item-content-sm: 0.19rem; + --spacing-radio-card-item-control-lg: 0.94rem; + --spacing-radio-card-item-control-md: 0.63rem; + --spacing-radio-card-item-control-sm: 0.31rem; + --spacing-radio-group-item-content-lg: 0.31rem; + --spacing-radio-group-item-content-md: 0.31rem; + --spacing-radio-group-item-content-sm: 0.19rem; --spacing-radio-group-item-lg: 0.94rem; --spacing-radio-group-item-md: 0.63rem; --spacing-radio-group-item-sm: 0.31rem; @@ -1195,6 +1235,7 @@ --spacing-status-text-sm: 0.19rem; --spacing-steps: 1.56rem; --spacing-steps-item: 0.63rem; + --spacing-steps-text: 0.19rem; --spacing-steps-trigger: 0.63rem; --spacing-switch: 0.94rem; --spacing-switch-control: 0.19rem; @@ -1205,15 +1246,8 @@ --spacing-tabs-content-padding-md: 1.25rem; --spacing-tabs-content-padding-sm: 0.94rem; --spacing-tabs-indicator-height: 0.13rem; - --spacing-tabs-trigger-padding-x-lg: 1.25rem; - --spacing-tabs-trigger-padding-x-md: 0.94rem; - --spacing-tabs-trigger-padding-x-sm: 0.63rem; - --spacing-tabs-trigger-padding-y-lg: 0.94rem; - --spacing-tabs-trigger-padding-y-md: 0.63rem; - --spacing-tabs-trigger-padding-y-sm: 0.31rem; --spacing-toast-content: 0.94rem; --spacing-toast-description: 0.31rem; - --spacing-toast-padding: 1.25rem; --spacing-tooltip-arrow: 0.63rem; --spacing-tree-view-icon: 0.63rem; --spacing-tree-view-indent-per-level: 0.31rem; @@ -1269,6 +1303,12 @@ --text-footer-title-lg: 1.95rem; --text-footer-title-md: 1.56rem; --text-footer-title-sm: 1.25rem; + --text-header-hamburger-lg: 1.56rem; + --text-header-hamburger-md: 1.25rem; + --text-header-hamburger-sm: 1rem; + --text-header-item-lg: 1.56rem; + --text-header-item-md: 1.25rem; + --text-header-item-sm: 1rem; --text-header-lg: 1.56rem; --text-header-md: 1.25rem; --text-header-sm: 1rem; @@ -1313,12 +1353,15 @@ --text-radio-card-addon-lg: 1.25rem; --text-radio-card-addon-md: 1rem; --text-radio-card-addon-sm: 0.8rem; - --text-radio-card-description-lg: 1.25rem; - --text-radio-card-description-md: 1rem; - --text-radio-card-description-sm: 0.8rem; + --text-radio-card-item-description-lg: 1.25rem; + --text-radio-card-item-description-md: 1rem; + --text-radio-card-item-description-sm: 0.8rem; --text-radio-card-item-lg: 1.56rem; --text-radio-card-item-md: 1.25rem; --text-radio-card-item-sm: 1rem; + --text-radio-group-item-description-lg: 1.25rem; + --text-radio-group-item-description-md: 1rem; + --text-radio-group-item-description-sm: 1rem; --text-radio-group-item-lg: 1.56rem; --text-radio-group-item-md: 1.25rem; --text-radio-group-item-sm: 1rem; @@ -1397,7 +1440,8 @@ --text-xl: 1.95rem; --text-xs: 0.8rem; --text-xs: 0.8rem; - --width-header-max: 0rem; + --width-full: 100%; + --width-header-max: 100%; --width-numeric-input-lg: 2.75rem; --width-numeric-input-md: 2.5rem; --width-numeric-input-sm: 2.25rem; diff --git a/libs/ui/src/tokens/figma/light/variables.css b/libs/ui/src/tokens/figma/light/variables.css index 64dfdbe64..b98a713a8 100644 --- a/libs/ui/src/tokens/figma/light/variables.css +++ b/libs/ui/src/tokens/figma/light/variables.css @@ -1,6 +1,6 @@ /* * Design tokens exported from Figma - * Exported at: 2026-05-15T07:51:19.175Z + * Exported at: 2026-05-19T18:52:26.629Z * Format: Raw CSS variables grouped by kind */ :root { @@ -303,6 +303,7 @@ --color-header-nav-fg-active: oklch(0.807 0.101 250.446); --color-header-nav-fg-hover: oklch(0.13 0.043 265.132); --color-header-nav-item-bg: oklch(0 0 0 / 0); + --color-header-nav-item-bg-hover: oklch(0 0 0 / 0); --color-highlight-base: oklch(0.872 0.011 261.787); --color-icon-fg-base: oklch(0.13 0.043 265.132); --color-icon-fg-danger: oklch(0.702 0.189 22.228); @@ -447,30 +448,36 @@ --color-radio-card-item-bg-solid-checked: oklch(0.807 0.101 250.446); --color-radio-card-item-bg-subtle-checked: oklch(0.937 0.03 251.281); --color-radio-card-item-border-base: oklch(0.129 0.041 264.695 / 0.15); - --color-radio-card-item-border-checked: oklch(0.807 0.101 250.446); --color-radio-card-item-border-disabled: oklch(0.872 0.011 261.787); --color-radio-card-item-border-error: oklch(0.702 0.189 22.228); - --color-radio-card-item-description: oklch(0.494 0.037 261.595); + --color-radio-card-item-border-outline-checked: oklch(0.807 0.101 250.446); + --color-radio-card-item-description-fg: oklch(0.494 0.037 261.595); --color-radio-card-item-fg-base: oklch(0.13 0.043 265.132); --color-radio-card-item-fg-disabled: oklch(0.551 0.027 264.328); --color-radio-card-item-fg-solid-checked: oklch(1 0 0); --color-radio-card-item-indicator-bg: oklch(0 0 0 / 0); --color-radio-card-item-indicator-border: oklch(0.129 0.041 264.695 / 0.15); - --color-radio-card-item-indicator-content: oklch(0.507 0.063 255.385); + --color-radio-card-item-indicator-content-fg: oklch(0.507 0.063 255.385); --color-radio-group-item-bg-base: oklch(0.129 0.041 264.695 / 0.05); --color-radio-group-item-bg-disabled: oklch(0.967 0.003 264.542); + --color-radio-group-item-bg-outline-checked: oklch(0.129 0.041 264.695 / 0.05); --color-radio-group-item-bg-solid-checked: oklch(0.807 0.101 250.446); --color-radio-group-item-bg-subtle-checked: oklch(0.937 0.03 251.281); --color-radio-group-item-border-base: oklch(0.129 0.041 264.695 / 0.15); --color-radio-group-item-border-checked: oklch(0.807 0.101 250.446); --color-radio-group-item-border-disabled: oklch(0.872 0.011 261.787); --color-radio-group-item-border-error: oklch(0.702 0.189 22.228); + --color-radio-group-item-border-outline-checked: oklch(0.507 0.063 255.385); + --color-radio-group-item-border-solid-checked: oklch(0.807 0.101 250.446); + --color-radio-group-item-border-subtle-checked: oklch(0.937 0.03 251.281); --color-radio-group-item-description: oklch(0.494 0.037 261.595); + --color-radio-group-item-description-disabled: oklch(0.551 0.027 264.328); --color-radio-group-item-fg-base: oklch(0.13 0.043 265.132); --color-radio-group-item-fg-disabled: oklch(0.551 0.027 264.328); --color-radio-group-item-indicator-disabled: oklch(0.551 0.027 264.328); --color-radio-group-item-indicator-outline: oklch(0.507 0.063 255.385); --color-radio-group-item-indicator-solid: oklch(1 0 0); + --color-radio-group-item-indicator-subtle: oklch(0.507 0.063 255.385); --color-radio-group-ring: oklch(0 0 0); --color-rating-fg-active: oklch(0.798 0.164 82.706); --color-rating-fg-base: oklch(0.872 0.011 261.787); @@ -479,7 +486,7 @@ --color-ring-default-base: oklch(0 0 0); --color-search-form-bg-base: oklch(0.129 0.041 264.695 / 0.05); --color-search-form-bg-disabled: oklch(0.967 0.003 264.542); - --color-search-form-bg-focus: oklch(0.129 0.041 264.695 / 0.15); + --color-search-form-bg-focus: oklch(0.129 0.041 264.695 / 0.05); --color-search-form-border-base: oklch(0.129 0.041 264.695 / 0.15); --color-search-form-border-disabled: oklch(0.872 0.011 261.787); --color-search-form-border-focus: oklch(0.807 0.101 250.446); @@ -610,7 +617,7 @@ --color-tabs-ring: oklch(0 0 0); --color-tabs-trigger-bg-hover: oklch(0.129 0.041 264.695 / 0.1); --color-tabs-trigger-bg-outline-selected: oklch(0.937 0.03 251.281); - --color-tabs-trigger-bg-solid-selected: oklch(0.807 0.101 250.446); + --color-tabs-trigger-bg-selected: oklch(0.807 0.101 250.446); --color-tabs-trigger-fg-base: oklch(0.494 0.037 261.595); --color-tabs-trigger-fg-disabled: oklch(0.551 0.027 264.328); --color-tabs-trigger-fg-selected: oklch(0.807 0.101 250.446); @@ -642,18 +649,18 @@ --color-textarea-fg-disabled: oklch(0.551 0.027 264.328); --color-textarea-fg-placeholder: oklch(0.554 0.041 257.417); --color-textarea-ring: oklch(0 0 0); - --color-toast-bg-base: oklch(0.872 0.011 261.787); - --color-toast-bg-error: oklch(0.702 0.189 22.228); - --color-toast-bg-info: oklch(0.704 0.123 182.503); - --color-toast-bg-success: oklch(0.702 0.158 160.438); - --color-toast-bg-warning: oklch(0.798 0.164 82.706); + --color-toast-bg: oklch(0.872 0.011 261.787); --color-toast-border-base: oklch(0.129 0.041 264.695 / 0.15); - --color-toast-border-error: oklch(0.702 0.189 22.228); - --color-toast-border-info: oklch(0.704 0.123 182.503); - --color-toast-border-success: oklch(0.702 0.158 160.438); - --color-toast-border-warning: oklch(0.798 0.164 82.706); - --color-toast-close: oklch(1 0 0); + --color-toast-close-fg: oklch(1 0 0); + --color-toast-error-bg: oklch(0.702 0.189 22.228); + --color-toast-error-border: oklch(0.702 0.189 22.228); --color-toast-fg: oklch(1 0 0); + --color-toast-info-bg: oklch(0.704 0.123 182.503); + --color-toast-info-border: oklch(0.704 0.123 182.503); + --color-toast-success-bg: oklch(0.702 0.158 160.438); + --color-toast-success-border: oklch(0.702 0.158 160.438); + --color-toast-warning-bg: oklch(0.798 0.164 82.706); + --color-toast-warning-border: oklch(0.798 0.164 82.706); --color-tooltip-arrow: oklch(0.928 0.006 264.531); --color-tooltip-bg: oklch(0.928 0.006 264.531); --color-tooltip-border-outline: oklch(0.129 0.041 264.695 / 0.15); @@ -664,7 +671,7 @@ --color-tree-view-fg-disabled: oklch(0.551 0.027 264.328); --color-tree-view-fg-hover: oklch(0.13 0.043 265.132); --color-tree-view-fg-selected: oklch(0.807 0.101 250.446); - --color-tree-view-icon-base: oklch(0.494 0.037 261.595); + --color-tree-view-icon-base: oklch(0.13 0.043 265.132); --color-tree-view-icon-hover: oklch(0.13 0.043 265.132); --color-tree-view-indent-bg: oklch(0.129 0.041 264.695 / 0.15); --color-tree-view-label-fg: oklch(0.13 0.043 265.132); @@ -708,7 +715,7 @@ --font-weight-badge-xl: 700; --font-weight-bold: 700; --font-weight-dialog-title: 600; - --font-weight-footer-link: 500; + --font-weight-footer-link: 400; --font-weight-footer-title: 600; --font-weight-header-nav: 500; --font-weight-header-nav-active: 600; @@ -716,12 +723,13 @@ --font-weight-light: 300; --font-weight-link: 500; --font-weight-medium: 500; + --font-weight-normal: 400; --font-weight-popover-title: 600; --font-weight-product-card-name: 500; --font-weight-product-card-price: 500; --font-weight-product-card-stock: 500; --font-weight-semibold: 600; - --font-weight-table-caption: 500; + --font-weight-table-caption: 400; --font-weight-table-footer: 500; --font-weight-table-header: 600; /* Measures */ @@ -752,13 +760,17 @@ --border-width-select-validation: 0.19rem; --border-width-slider: 0.06rem; --border-width-slider-error: 0.19rem; - --border-width-steps: 0.06rem; + --border-width-steps-indicator: 0.06rem; --border-width-switch: 0.06rem; --border-width-tabs: 0.13rem; --border-width-textarea: 0.13rem; --border-width-textarea-validation: 0.19rem; --border-width-toast: 0.06rem; --border-width-tooltip: 0.06rem; + --brightness-100: 6.25rem; + --brightness-70: 4.38rem; + --brightness-gallery-trigger: 4.38rem; + --brightness-gallery-trigger-active: 6.25rem; --container-2xl: 42rem; --container-2xs: 18rem; --container-3xl: 48rem; @@ -795,12 +807,14 @@ --dimension-50: 0.19rem; --dimension-500: 2.81rem; --dimension-550: 3.13rem; + --dimension-576: 36rem; --dimension-600: 3.75rem; --dimension-64: 4rem; --dimension-650: 4.06rem; --dimension-70: 4.38rem; --dimension-700: 4.38rem; --dimension-750: 5rem; + --dimension-768: 48rem; --dimension-800: 5.63rem; --dimension-850: 6.25rem; --dimension-900: 6.88rem; @@ -847,6 +861,9 @@ --line-height-status-text-lg: 2.38rem; --line-height-status-text-md: 1.88rem; --line-height-status-text-sm: 1.5rem; + --margin-footer-divider-lg: 2.19rem; + --margin-footer-divider-md: 1.56rem; + --margin-footer-divider-sm: 0.94rem; --padding-accordion-content-x-lg: 1.88rem; --padding-accordion-content-x-md: 0.94rem; --padding-accordion-content-x-sm: 0.31rem; @@ -877,12 +894,14 @@ --padding-dialog-actions-top: 0.63rem; --padding-dialog-close-trigger: 0.63rem; --padding-dialog-content: 2.5rem; + --padding-footer-bottom: 1.56rem; --padding-footer-bottom-lg: 2.19rem; --padding-footer-bottom-md: 1.56rem; --padding-footer-bottom-sm: 0.94rem; --padding-footer-lg: 1.56rem; --padding-footer-md: 0.94rem; --padding-footer-sm: 0.31rem; + --padding-gallery-trigger: 0rem; --padding-header-hamburger-lg: 0.94rem; --padding-header-hamburger-md: 0.63rem; --padding-header-hamburger-sm: 0.31rem; @@ -906,6 +925,9 @@ --padding-popover-lg: 1.88rem; --padding-popover-md: 1.25rem; --padding-popover-sm: 0.94rem; + --padding-radio-card-item-control-lg: 1.25rem; + --padding-radio-card-item-control-md: 0.94rem; + --padding-radio-card-item-control-sm: 0.63rem; --padding-select-clear-trigger: 0.31rem; --padding-select-item-x: 1.25rem; --padding-select-item-y: 0.63rem; @@ -924,6 +946,12 @@ --padding-table-cell-md-y: 0.63rem; --padding-table-cell-sm-x: 0.63rem; --padding-table-cell-sm-y: 0.31rem; + --padding-tabs-trigger-x-lg: 1.25rem; + --padding-tabs-trigger-x-md: 0.94rem; + --padding-tabs-trigger-x-sm: 0.63rem; + --padding-tabs-trigger-y-lg: 0.94rem; + --padding-tabs-trigger-y-md: 0.63rem; + --padding-tabs-trigger-y-sm: 0.31rem; --padding-textarea-lg: 0.91rem; --padding-textarea-md: 0.59rem; --padding-textarea-sm: 0.31rem; @@ -933,6 +961,7 @@ --padding-textarea-y-lg: 0.94rem; --padding-textarea-y-md: 0.63rem; --padding-textarea-y-sm: 0.31rem; + --padding-toast-root: 1.25rem; --padding-tooltip-x-lg: 1.88rem; --padding-tooltip-x-md: 1.25rem; --padding-tooltip-x-sm: 0.94rem; @@ -956,7 +985,7 @@ --radius-dialog-content-center: 0.25rem; --radius-dialog-content-edge: 0rem; --radius-dialog-content-side: 0rem; - --radius-footer-root: 0rem; + --radius-footer: 0rem; --radius-form-control: 0.5rem; --radius-form-control-lg: 0.75rem; --radius-form-control-md: 0.5rem; @@ -970,7 +999,7 @@ --radius-lg: 0.75rem; --radius-md: 0.5rem; --radius-menu: 0.25rem; - --radius-menu-item: 0.25rem; + --radius-menu-item: 0.13rem; --radius-none: 0rem; --radius-numeric-input: 0.5rem; --radius-numeric-input-lg: 0.75rem; @@ -983,7 +1012,7 @@ --radius-radio-card-indicator: 624.94rem; --radius-radio-card-item: 0.5rem; --radius-radio-group-control: 624.94rem; - --radius-search-form-lg: 0.75rem; + --radius-search-form-lg: 0.5rem; --radius-search-form-md: 0.5rem; --radius-search-form-sm: 0.25rem; --radius-select: 0.5rem; @@ -996,6 +1025,7 @@ --radius-sm: 0.25rem; --radius-steps-content: 0.5rem; --radius-steps-indicator: 624.94rem; + --radius-steps-separator: 624.94rem; --radius-switch: 624.94rem; --radius-table: 0.25rem; --radius-tabs: 0rem; @@ -1004,17 +1034,21 @@ --radius-textarea-lg: 0.75rem; --radius-textarea-md: 0.5rem; --radius-textarea-sm: 0.25rem; - --radius-toast: 0.25rem; + --radius-toast-root: 0.25rem; --radius-tooltip: 0.5rem; --radius-tree-view: 0rem; + --radius-xs: 0.13rem; --ring-offset: 0.13rem; --ring-offset: 0.13rem; --ring-width: 0.13rem; --ring-width: 0.13rem; --size-1: 0.06rem; + --size-12: 0.75rem; --size-128: 8rem; + --size-14: 0.88rem; --size-16: 1rem; --size-160: 10rem; + --size-18: 1.13rem; --size-192: 12rem; --size-2: 0.13rem; --size-240: 15rem; @@ -1026,7 +1060,9 @@ --size-44: 2.75rem; --size-48: 3rem; --size-56: 3.5rem; + --size-6: 0.38rem; --size-64: 4rem; + --size-8: 0.5rem; --size-800: 50rem; --size-96: 6rem; --size-carousel-indicator: 0.94rem; @@ -1055,18 +1091,18 @@ --size-image-md: 10rem; --size-image-sm: 5rem; --size-menu-separator-height: 0.06rem; - --size-radio-card-indicator-lg: 1.56rem; - --size-radio-card-indicator-mark-lg: 0.31rem; - --size-radio-card-indicator-mark-md: 0.63rem; - --size-radio-card-indicator-mark-sm: 0.19rem; - --size-radio-card-indicator-md: 1.25rem; - --size-radio-card-indicator-sm: 0.94rem; - --size-radio-group-control-lg: 1.56rem; - --size-radio-group-control-md: 1.25rem; - --size-radio-group-control-sm: 0.94rem; - --size-radio-group-indicator-lg: 0.94rem; + --size-radio-card-indicator-lg: 1.25rem; + --size-radio-card-indicator-mark-lg: 0.63rem; + --size-radio-card-indicator-mark-md: 0.5rem; + --size-radio-card-indicator-mark-sm: 0.38rem; + --size-radio-card-indicator-md: 1.13rem; + --size-radio-card-indicator-sm: 1rem; + --size-radio-group-control-lg: 1.13rem; + --size-radio-group-control-md: 1rem; + --size-radio-group-control-sm: 0.88rem; + --size-radio-group-indicator-lg: 0.75rem; --size-radio-group-indicator-md: 0.63rem; - --size-radio-group-indicator-sm: 0.63rem; + --size-radio-group-indicator-sm: 0.5rem; --size-search-form-lg: 4.38rem; --size-search-form-md: 2.75rem; --size-search-form-sm: 2.06rem; @@ -1123,6 +1159,7 @@ --spacing-footer-container-md: 2.19rem; --spacing-footer-container-sm: 1.56rem; --spacing-footer-lg: 0.63rem; + --spacing-footer-list: 0.31rem; --spacing-footer-md: 0.31rem; --spacing-footer-section-lg: 0.63rem; --spacing-footer-section-md: 0.31rem; @@ -1159,12 +1196,15 @@ --spacing-product-card-col-layout: 0.31rem; --spacing-product-card-padding: 0.63rem; --spacing-product-card-row-layout: 1.25rem; - --spacing-radio-card-lg: 0.94rem; - --spacing-radio-card-md: 0.63rem; - --spacing-radio-card-padding-lg: 1.25rem; - --spacing-radio-card-padding-md: 0.94rem; - --spacing-radio-card-padding-sm: 0.63rem; - --spacing-radio-card-sm: 0.31rem; + --spacing-radio-card-item-content-lg: 0.31rem; + --spacing-radio-card-item-content-md: 0.31rem; + --spacing-radio-card-item-content-sm: 0.19rem; + --spacing-radio-card-item-control-lg: 0.94rem; + --spacing-radio-card-item-control-md: 0.63rem; + --spacing-radio-card-item-control-sm: 0.31rem; + --spacing-radio-group-item-content-lg: 0.31rem; + --spacing-radio-group-item-content-md: 0.31rem; + --spacing-radio-group-item-content-sm: 0.19rem; --spacing-radio-group-item-lg: 0.94rem; --spacing-radio-group-item-md: 0.63rem; --spacing-radio-group-item-sm: 0.31rem; @@ -1195,6 +1235,7 @@ --spacing-status-text-sm: 0.19rem; --spacing-steps: 1.56rem; --spacing-steps-item: 0.63rem; + --spacing-steps-text: 0.19rem; --spacing-steps-trigger: 0.63rem; --spacing-switch: 0.94rem; --spacing-switch-control: 0.19rem; @@ -1205,15 +1246,8 @@ --spacing-tabs-content-padding-md: 1.25rem; --spacing-tabs-content-padding-sm: 0.94rem; --spacing-tabs-indicator-height: 0.13rem; - --spacing-tabs-trigger-padding-x-lg: 1.25rem; - --spacing-tabs-trigger-padding-x-md: 0.94rem; - --spacing-tabs-trigger-padding-x-sm: 0.63rem; - --spacing-tabs-trigger-padding-y-lg: 0.94rem; - --spacing-tabs-trigger-padding-y-md: 0.63rem; - --spacing-tabs-trigger-padding-y-sm: 0.31rem; --spacing-toast-content: 0.94rem; --spacing-toast-description: 0.31rem; - --spacing-toast-padding: 1.25rem; --spacing-tooltip-arrow: 0.63rem; --spacing-tree-view-icon: 0.63rem; --spacing-tree-view-indent-per-level: 0.31rem; @@ -1269,6 +1303,12 @@ --text-footer-title-lg: 1.95rem; --text-footer-title-md: 1.56rem; --text-footer-title-sm: 1.25rem; + --text-header-hamburger-lg: 1.56rem; + --text-header-hamburger-md: 1.25rem; + --text-header-hamburger-sm: 1rem; + --text-header-item-lg: 1.56rem; + --text-header-item-md: 1.25rem; + --text-header-item-sm: 1rem; --text-header-lg: 1.56rem; --text-header-md: 1.25rem; --text-header-sm: 1rem; @@ -1313,12 +1353,15 @@ --text-radio-card-addon-lg: 1.25rem; --text-radio-card-addon-md: 1rem; --text-radio-card-addon-sm: 0.8rem; - --text-radio-card-description-lg: 1.25rem; - --text-radio-card-description-md: 1rem; - --text-radio-card-description-sm: 0.8rem; + --text-radio-card-item-description-lg: 1.25rem; + --text-radio-card-item-description-md: 1rem; + --text-radio-card-item-description-sm: 0.8rem; --text-radio-card-item-lg: 1.56rem; --text-radio-card-item-md: 1.25rem; --text-radio-card-item-sm: 1rem; + --text-radio-group-item-description-lg: 1.25rem; + --text-radio-group-item-description-md: 1rem; + --text-radio-group-item-description-sm: 1rem; --text-radio-group-item-lg: 1.56rem; --text-radio-group-item-md: 1.25rem; --text-radio-group-item-sm: 1rem; @@ -1397,7 +1440,8 @@ --text-xl: 1.95rem; --text-xs: 0.8rem; --text-xs: 0.8rem; - --width-header-max: 0rem; + --width-full: 100%; + --width-header-max: 100%; --width-numeric-input-lg: 2.75rem; --width-numeric-input-md: 2.5rem; --width-numeric-input-sm: 2.25rem; From c561a8d00eae21ef4a13495201f6ca69873d413b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Chyl=C3=ADk?= Date: Tue, 19 May 2026 22:44:01 +0200 Subject: [PATCH 02/60] feat(figma-variables): Figma variables exported 2026-05-19 21:43 CET Format: CSS variables Exported themes: 2 themes (light, dark) Base branch: master --- libs/ui/src/tokens/figma/dark/variables.css | 60 ++++++++++---------- libs/ui/src/tokens/figma/light/variables.css | 60 ++++++++++---------- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/libs/ui/src/tokens/figma/dark/variables.css b/libs/ui/src/tokens/figma/dark/variables.css index 49c31451f..1de5cb574 100644 --- a/libs/ui/src/tokens/figma/dark/variables.css +++ b/libs/ui/src/tokens/figma/dark/variables.css @@ -1,6 +1,6 @@ /* * Design tokens exported from Figma - * Exported at: 2026-05-19T18:52:26.634Z + * Exported at: 2026-05-19T20:43:56.598Z * Format: Raw CSS variables grouped by kind */ :root { @@ -108,8 +108,8 @@ --color-button-bg-danger-light-active: oklch(0.962 0.019 25.602); --color-button-bg-danger-light-base: oklch(0.9 0.048 18.09); --color-button-bg-danger-light-hover: oklch(0.953 0.023 24.126); - --color-button-bg-disabled-base: oklch(0.278 0.034 257.672); - --color-button-bg-outlined-base: oklch(0 0 0 / 0); + --color-button-bg-disabled: oklch(0.278 0.034 257.672); + --color-button-bg-outlined: oklch(0 0 0 / 0); --color-button-bg-outlined-danger-active: oklch(0.794 0.119 20.048 / 0.122); --color-button-bg-outlined-danger-hover: oklch(0.805 0.111 19.782 / 0.161); --color-button-bg-outlined-primary-active: oklch(0.813 0.111 233.957 / 0.122); @@ -144,33 +144,33 @@ --color-button-bg-warning-light-active: oklch(0.975 0.117 107.339); --color-button-bg-warning-light-base: oklch(0.928 0.073 92.942); --color-button-bg-warning-light-hover: oklch(0.975 0.117 107.339); - --color-button-border-danger-base: oklch(0.702 0.189 22.228); - --color-button-border-disabled-base: oklch(0.373 0.034 260.169); - --color-button-border-primary-base: oklch(0.827 0.109 229.198); - --color-button-border-secondary-base: oklch(0.772 0.173 65.367); - --color-button-border-tertiary-base: oklch(0.806 0.112 11.594); - --color-button-border-warning-base: oklch(0.798 0.164 82.706); - --color-button-fg-danger-base: oklch(0.13 0.043 265.132); - --color-button-fg-danger-borderless-base: oklch(0.452 0.066 4.881); - --color-button-fg-danger-light-base: oklch(0.13 0.043 265.132); - --color-button-fg-disabled-base: oklch(0.551 0.027 264.328); - --color-button-fg-outlined-danger-base: oklch(1 0 0); - --color-button-fg-outlined-primary-base: oklch(1 0 0); - --color-button-fg-outlined-secondary-base: oklch(1 0 0); - --color-button-fg-outlined-tertiary-base: oklch(1 0 0); - --color-button-fg-outlined-warning-base: oklch(1 0 0); - --color-button-fg-primary-base: oklch(0.13 0.043 265.132); - --color-button-fg-primary-borderless-base: oklch(0.507 0.063 255.385); - --color-button-fg-primary-light-base: oklch(0.13 0.043 265.132); - --color-button-fg-secondary-base: oklch(0.13 0.043 265.132); - --color-button-fg-secondary-borderless-base: oklch(0.447 0.076 49.767); - --color-button-fg-secondary-light-base: oklch(0.13 0.043 265.132); - --color-button-fg-tertiary-base: oklch(0.13 0.043 265.132); - --color-button-fg-tertiary-borderless-base: oklch(0.511 0.054 323.997); - --color-button-fg-tertiary-light-base: oklch(0.13 0.043 265.132); - --color-button-fg-warning-base: oklch(0.13 0.043 265.132); - --color-button-fg-warning-borderless-base: oklch(0.492 0.079 93.623); - --color-button-fg-warning-light-base: oklch(0.13 0.043 265.132); + --color-button-border-danger: oklch(0.702 0.189 22.228); + --color-button-border-disabled: oklch(0.373 0.034 260.169); + --color-button-border-primary: oklch(0.827 0.109 229.198); + --color-button-border-secondary: oklch(0.772 0.173 65.367); + --color-button-border-tertiary: oklch(0.806 0.112 11.594); + --color-button-border-warning: oklch(0.798 0.164 82.706); + --color-button-fg-danger: oklch(0.13 0.043 265.132); + --color-button-fg-danger-borderless: oklch(0.452 0.066 4.881); + --color-button-fg-danger-light: oklch(0.13 0.043 265.132); + --color-button-fg-disabled: oklch(0.551 0.027 264.328); + --color-button-fg-outlined-danger: oklch(1 0 0); + --color-button-fg-outlined-primary: oklch(1 0 0); + --color-button-fg-outlined-secondary: oklch(1 0 0); + --color-button-fg-outlined-tertiary: oklch(1 0 0); + --color-button-fg-outlined-warning: oklch(1 0 0); + --color-button-fg-primary: oklch(0.13 0.043 265.132); + --color-button-fg-primary-borderless: oklch(0.507 0.063 255.385); + --color-button-fg-primary-light: oklch(0.13 0.043 265.132); + --color-button-fg-secondary: oklch(0.13 0.043 265.132); + --color-button-fg-secondary-borderless: oklch(0.447 0.076 49.767); + --color-button-fg-secondary-light: oklch(0.13 0.043 265.132); + --color-button-fg-tertiary: oklch(0.13 0.043 265.132); + --color-button-fg-tertiary-borderless: oklch(0.511 0.054 323.997); + --color-button-fg-tertiary-light: oklch(0.13 0.043 265.132); + --color-button-fg-warning: oklch(0.13 0.043 265.132); + --color-button-fg-warning-borderless: oklch(0.492 0.079 93.623); + --color-button-fg-warning-light: oklch(0.13 0.043 265.132); --color-button-ring: oklch(1 0 0); --color-carousel-control-bg: oklch(0.373 0.034 260.169); --color-carousel-indicator-bg-active: oklch(0.827 0.109 229.198); diff --git a/libs/ui/src/tokens/figma/light/variables.css b/libs/ui/src/tokens/figma/light/variables.css index b98a713a8..bdc9f59e3 100644 --- a/libs/ui/src/tokens/figma/light/variables.css +++ b/libs/ui/src/tokens/figma/light/variables.css @@ -1,6 +1,6 @@ /* * Design tokens exported from Figma - * Exported at: 2026-05-19T18:52:26.629Z + * Exported at: 2026-05-19T20:43:56.594Z * Format: Raw CSS variables grouped by kind */ :root { @@ -108,8 +108,8 @@ --color-button-bg-danger-light-active: oklch(0.685 0.114 22.469); --color-button-bg-danger-light-base: oklch(0.9 0.048 18.09); --color-button-bg-danger-light-hover: oklch(0.725 0.114 22.247); - --color-button-bg-disabled-base: oklch(0.967 0.003 264.542); - --color-button-bg-outlined-base: oklch(0 0 0 / 0); + --color-button-bg-disabled: oklch(0.967 0.003 264.542); + --color-button-bg-outlined: oklch(0 0 0 / 0); --color-button-bg-outlined-danger-active: oklch(0.7 0.191 23.399 / 0.122); --color-button-bg-outlined-danger-hover: oklch(0.702 0.19 23.327 / 0.161); --color-button-bg-outlined-primary-active: oklch(0.806 0.102 249.608 / 0.122); @@ -144,33 +144,33 @@ --color-button-bg-warning-light-active: oklch(0.775 0.11 86.269); --color-button-bg-warning-light-base: oklch(0.928 0.073 92.942); --color-button-bg-warning-light-hover: oklch(0.816 0.11 86.477); - --color-button-border-danger-base: oklch(0.702 0.189 22.228); - --color-button-border-disabled-base: oklch(0.872 0.011 261.787); - --color-button-border-primary-base: oklch(0.807 0.101 250.446); - --color-button-border-secondary-base: oklch(0.7 0.202 44.441); - --color-button-border-tertiary-base: oklch(0.823 0.119 346.157); - --color-button-border-warning-base: oklch(0.798 0.164 82.706); - --color-button-fg-danger-base: oklch(0.13 0.043 265.132); - --color-button-fg-danger-borderless-base: oklch(0.452 0.066 4.881); - --color-button-fg-danger-light-base: oklch(0.13 0.043 265.132); - --color-button-fg-disabled-base: oklch(0.551 0.027 264.328); - --color-button-fg-outlined-danger-base: oklch(0.13 0.043 265.132); - --color-button-fg-outlined-primary-base: oklch(0.13 0.043 265.132); - --color-button-fg-outlined-secondary-base: oklch(0.13 0.043 265.132); - --color-button-fg-outlined-tertiary-base: oklch(0.13 0.043 265.132); - --color-button-fg-outlined-warning-base: oklch(0.13 0.043 265.132); - --color-button-fg-primary-base: oklch(0.13 0.043 265.132); - --color-button-fg-primary-borderless-base: oklch(0.507 0.063 255.385); - --color-button-fg-primary-light-base: oklch(0.13 0.043 265.132); - --color-button-fg-secondary-base: oklch(0.13 0.043 265.132); - --color-button-fg-secondary-borderless-base: oklch(0.447 0.076 49.767); - --color-button-fg-secondary-light-base: oklch(0.13 0.043 265.132); - --color-button-fg-tertiary-base: oklch(0.13 0.043 265.132); - --color-button-fg-tertiary-borderless-base: oklch(0.511 0.054 323.997); - --color-button-fg-tertiary-light-base: oklch(0.13 0.043 265.132); - --color-button-fg-warning-base: oklch(0.13 0.043 265.132); - --color-button-fg-warning-borderless-base: oklch(0.492 0.079 93.623); - --color-button-fg-warning-light-base: oklch(0.13 0.043 265.132); + --color-button-border-danger: oklch(0.702 0.189 22.228); + --color-button-border-disabled: oklch(0.872 0.011 261.787); + --color-button-border-primary: oklch(0.807 0.101 250.446); + --color-button-border-secondary: oklch(0.7 0.202 44.441); + --color-button-border-tertiary: oklch(0.823 0.119 346.157); + --color-button-border-warning: oklch(0.798 0.164 82.706); + --color-button-fg-danger: oklch(0.13 0.043 265.132); + --color-button-fg-danger-borderless: oklch(0.452 0.066 4.881); + --color-button-fg-danger-light: oklch(0.13 0.043 265.132); + --color-button-fg-disabled: oklch(0.551 0.027 264.328); + --color-button-fg-outlined-danger: oklch(0.13 0.043 265.132); + --color-button-fg-outlined-primary: oklch(0.13 0.043 265.132); + --color-button-fg-outlined-secondary: oklch(0.13 0.043 265.132); + --color-button-fg-outlined-tertiary: oklch(0.13 0.043 265.132); + --color-button-fg-outlined-warning: oklch(0.13 0.043 265.132); + --color-button-fg-primary: oklch(0.13 0.043 265.132); + --color-button-fg-primary-borderless: oklch(0.507 0.063 255.385); + --color-button-fg-primary-light: oklch(0.13 0.043 265.132); + --color-button-fg-secondary: oklch(0.13 0.043 265.132); + --color-button-fg-secondary-borderless: oklch(0.447 0.076 49.767); + --color-button-fg-secondary-light: oklch(0.13 0.043 265.132); + --color-button-fg-tertiary: oklch(0.13 0.043 265.132); + --color-button-fg-tertiary-borderless: oklch(0.511 0.054 323.997); + --color-button-fg-tertiary-light: oklch(0.13 0.043 265.132); + --color-button-fg-warning: oklch(0.13 0.043 265.132); + --color-button-fg-warning-borderless: oklch(0.492 0.079 93.623); + --color-button-fg-warning-light: oklch(0.13 0.043 265.132); --color-button-ring: oklch(0 0 0); --color-carousel-control-bg: oklch(0.872 0.011 261.787); --color-carousel-indicator-bg-active: oklch(0.807 0.101 250.446); From 59e8d228cad7f97b56ef0c7eb26a9e3ff34870c7 Mon Sep 17 00:00:00 2001 From: Lukas Chylik Date: Thu, 28 May 2026 11:11:33 +0200 Subject: [PATCH 03/60] chore(tools): add figma-token-binding skill + scripts Adds the procedure and tooling for migrating UI atoms off OKLCH derivations onto literal Figma exports: - SKILL.md documents the end-to-end workflow. - split-figma-tokens.mjs splits the full Figma export into per-component fragments. - apply-light-dark.mjs rewrites a component's @theme tokens as `light-dark(L, D)` so .light / .dark / .always-* / .reverse all flip automatically via the existing color-scheme machinery. - apply-reverse-blocks.mjs is an earlier per-selector approach kept for reference; superseded by apply-light-dark. Co-Authored-By: Claude Opus 4.7 (1M context) --- .agents/skills/component-to-figma/SKILL.md | 14 +- .agents/skills/figma-token-binding/SKILL.md | 437 ++++++++++++++++++ .../scripts/apply-light-dark.mjs | 186 ++++++++ .../scripts/apply-reverse-blocks.mjs | 156 +++++++ .../scripts/split-figma-tokens.mjs | 220 +++++++++ 5 files changed, 1010 insertions(+), 3 deletions(-) create mode 100644 .agents/skills/figma-token-binding/SKILL.md create mode 100644 .agents/skills/figma-token-binding/scripts/apply-light-dark.mjs create mode 100644 .agents/skills/figma-token-binding/scripts/apply-reverse-blocks.mjs create mode 100755 .agents/skills/figma-token-binding/scripts/split-figma-tokens.mjs diff --git a/.agents/skills/component-to-figma/SKILL.md b/.agents/skills/component-to-figma/SKILL.md index c0b312cd2..9ca84e946 100644 --- a/.agents/skills/component-to-figma/SKILL.md +++ b/.agents/skills/component-to-figma/SKILL.md @@ -289,9 +289,17 @@ When it varies by size, keep `type/component/size`. - **Semantic**: `type/role` — e.g., `size/sm`, `color/primary/base`, `radius/md` - **Component color**: `color/component/cssProperty/variant/state` — depth varies per component - **Component non-color**: `type/component/size` — flat, no redundant segments (`root`, `gap`, `x`, `y` are forbidden), no state suffix because these values don't change between states -- **State suffix rule — add a state segment only when the value varies by state:** - - Color tokens with multiple states: use **"base"** for the resting state, never "default" (e.g., `color/button/bg/primary/base`, `color/button/bg/primary/hover`) - - Tokens that are state-invariant (padding, radius, spacing, border-width, text size, font-weight): **no state suffix at all** — not "base", not "default". The name ends at the size or role segment (e.g., `radius/button/sm`, `padding/button/sm`, `spacing/button/sm`). +- **State suffix rule — add a state segment only when the value varies by state. This applies to EVERY token, color and non-color alike:** + - **Multi-state tokens** (the value differs between states like base/hover/active/focus/disabled): use **"base"** for the resting state, never "default" (e.g., `color/button/bg/primary/base`, `color/button/bg/primary/hover`, `color/button/bg/primary/active`). + - **Single-state tokens** (the value is identical across all states the component supports): **no state suffix at all** — not "base", not "default". The name ends at the variant/size/role segment. + - This rule applies to **color tokens too**. A color token that does not vary by state must NOT carry a `-base` suffix. Examples: + - `color/button/fg/primary` — solid primary text is the same color in default, hover, and active. **No `/base`.** + - `color/button/fg/outlined/primary` — outlined primary text doesn't change on hover/active. **No `/base`.** + - `color/button/border/primary` — outlined border color doesn't change on hover. **No `/base`.** + - `color/button/bg/disabled` — only one disabled bg exists; there is no `disabled/hover`. **No `/base`.** + - `color/button/bg/outlined` — transparent across every outlined variant; no hover/active sibling. **No `/base`.** + - **State-invariant non-color tokens** (padding, radius, spacing, border-width, text size, font-weight): same rule. Name ends at the size or role segment (e.g., `radius/button/sm`, `padding/button/sm`, `spacing/button/sm`). + - **How to test:** if there is no `-hover`, `-active`, `-focus`, or `-disabled` sibling for the token, the `-base` suffix is wrong — drop it. - **Modes**: light and dark only, applied to color variables. No responsive modes. - **Inheritance is strict**: component => semantic => core. No skipping tiers except when no semantic equivalent exists. diff --git a/.agents/skills/figma-token-binding/SKILL.md b/.agents/skills/figma-token-binding/SKILL.md new file mode 100644 index 000000000..b12047749 --- /dev/null +++ b/.agents/skills/figma-token-binding/SKILL.md @@ -0,0 +1,437 @@ +--- +name: figma-token-binding +description: Migrate a UI component to consume Figma-exported design tokens directly, replacing the codebase's derivation-based token chain. Use when binding a single component (button, input, dialog, …) to its Figma token export, keeping the rest of the system stable. +--- + +# figma-token-binding + +End-to-end procedure for migrating **one component** at a time from the +derivation-based token system (`light-dark()` + `oklch(from … calc(l ± …))`) +to **literal Figma-export values** scoped via `.dark` class cascade. + +Designed to be run repeatedly — once per component — until the whole library +is on Figma values and the OKLCH derivations can be deleted. + +The Button migration in `libs/ui/src/atoms/button.tsx` is the canonical +reference implementation; mirror its layout when migrating other components. + +## Prime directive + +**Load Figma tokens and do not break the visual design from the previous +static version.** Every migration MUST be visually verified against the +pre-migration build. Any pixel that changes must be either: + +1. An intentional alignment with Figma (designer's intent now reflected), + reported to the user and approved before merging, OR +2. A bug — diagnose root cause with the user, not by reverting the + migration. + +The agent does not silently accept differences. Differences are surfaced, +explained, and decided by the user. + +## What this migration actually does + +| Before | After | +|---|---| +| Component token values computed from a handful of seed colors via `light-dark()` and OKLCH math | Component token values are literal `oklch(…)` per mode, exported from Figma | +| Single set of tokens; dark mode flips by changing `--state-*` deltas | Two sets of tokens: light defaults + `.dark` overrides | +| App-level brand overrides via reference-layer tokens (`--color-button-primary`) | App-level brand overrides will need to target flat names (`--color-button-bg-primary-base`) once bridges are removed | +| Token names: `--color-{component}-{slot}-{role}` | Token names: `--color-{component}-{slot}-{role}-{state}` (Figma's `-base` for default state, etc.) | + +## Prerequisites + +- Full Figma exports present at + `libs/ui/src/tokens/figma/{light,dark}/variables.css`. + These are the canonical source. Do not edit them; re-export from Figma when + values change. +- Component-level CSS file at `libs/ui/src/tokens/components/{atoms,molecules,…}/_{name}.css`. +- Component implementation at `libs/ui/src/{atoms,molecules,…}/{name}.tsx`. +- Storybook running (`bunx nx run ui:storybook`) for visual verification. + +## Workflow + +### 1. Inventory the component's token surface + +From the component `.tsx`, list every Tailwind utility that resolves to a +component-prefixed CSS variable. For Button: + +``` +bg-button-bg-{role} → --color-button-bg-{role} +text-button-fg-{role} → --color-button-fg-{role} +border-button-border-{role} → --color-button-border-{role} +gap-button-{size} → --spacing-button-{size} +p-button-{size} → --padding-button-{size} +text-button-{size} → --text-button-{size} +rounded-button-{size} → --radius-button-{size} +border-(length:--…) → arbitrary-value border-width vars +outline-button-ring → --color-button-ring +``` + +Note any tokens from sibling component files (`_form-control.css` etc.) the +component reads — these stay untouched in this migration. + +### 2. Split the Figma export into component fragments + +Run the splitter to extract just this component's vars from the full export: + +```sh +node .agents/skills/figma-token-binding/scripts/split-figma-tokens.mjs +``` + +Output: + +``` +libs/ui/src/tokens/figma/light/.css +libs/ui/src/tokens/figma/dark/.css +``` + +The light file wraps declarations in `@theme static { … }`. The dark file +wraps in `:is(.dark, .always-dark) { … }`. Sizing tokens (identical in both +modes) appear only in the light file. + +The splitter filters by the `--*--*` segment, excluding +composite components like `product-card-button-*` that contain the name but +are scoped to another atom. + +### 3. Reconcile naming differences + +Figma uses different conventions than the legacy live system. Compare and +list every name that differs. Common patterns from Button: + +| Live (old) | Figma (new) | How to handle | +|---|---|---| +| `--color-button-bg-primary` | `--color-button-bg-primary-base` | Rename in component `.tsx` (utility class gains `-base`) | +| `--color-button-fg-primary` | `--color-button-fg-primary-base` | Same | +| `--border-button-width-sm` | `--border-width-button-sm` | Rename arbitrary-value reference in `.tsx`; matches the locked standard in `libs/ui/AGENTS.md` | +| `--padding-button-{sm,md,lg}` (shorthand) | `--padding-button-{x,y}-{sm,md,lg}` (split) | Keep `p-button-sm` in JSX; bridge in CSS: `--padding-button-sm: var(--padding-button-y-sm) var(--padding-button-x-sm)` | +| `--color-button-primary` (reference layer, used by downstream) | `--color-button-bg-primary-base` | Add a temporary alias in the component CSS (see step 5) | + +### 4. Find downstream consumers of deleted tokens + +Before rewriting the component CSS, grep for everything that depends on it: + +```sh +grep -rnE "color-button-(primary|secondary|tertiary|warning|danger)[^-]|color-button-fg[^-]" \ + --include="*.css" --include="*.ts" --include="*.tsx" +``` + +For Button, the consumers we found: + +- `libs/ui/src/tokens/components/molecules/_product-card.css` — reads `--color-button-primary`, `--color-button-fg`, etc. +- `apps/n1/src/tokens/_n1-components.css` — **overrides** `--color-button-primary` (brand customization). +- `apps/frontend-demo/src/tokens/_overrides-components.css` — same. +- `apps/frontend-demo/src/tokens/app-components/molecules/_product-card.css` — same as the lib's product-card. + +**Read-side compat** is solved by aliases (step 5). **Write-side compat** (app +overrides) is a separate problem — see "App brand overrides" below. + +### 5. Replace the component's `_.css` + +The new file should be small and contain only: + +1. `@import` of both Figma fragments (light + dark). +2. Any bridge tokens (combined shorthand from split tokens). +3. Legacy aliases for downstream consumers, clearly marked deprecated. +4. Component-local utilities (e.g. `token-icon-button-spinner`) that are + unrelated to Figma tokens. + +See `libs/ui/src/tokens/components/atoms/_button.css` for the reference layout. + +Delete every `oklch(from … calc(l + var(--state-hover)) c h)` derivation for +this component — Figma already encodes hover/active values literally. + +### 6. Update the component `.tsx` + +Apply every rename from step 3. For Button this was: + +- `bg-button-bg-{role}` → `bg-button-bg-{role}-base` +- `text-button-fg-{role}` → `text-button-fg-{role}-base` +- `text-button-fg-{role}-light` → `text-button-fg-{role}-light-base` +- `text-button-fg-{role}-borderless` → `text-button-fg-{role}-borderless-base` +- `text-button-fg-outlined-{role}` → `text-button-fg-outlined-{role}-base` +- `border-button-border-{role}` → `border-button-border-{role}-base` +- `bg-button-bg-{role}-light` → `bg-button-bg-{role}-light-base` +- `bg-button-bg-borderless` / `-outlined` / `-disabled` → same with `-base` +- `disabled:text-button-fg-disabled` → `disabled:text-button-fg-disabled-base` +- `border-(length:--border-button-width-sm)` → `border-(length:--border-width-button-sm)` + +Hover/active/light-hover/light-active utility names already match Figma and +do **not** change. + +### 7. Verify + +This step is **mandatory** for every component migration. Token diffs that +are technically correct on paper can still cause regressions in the live +build — Tailwind v4's PostCSS pipeline rewrites selectors, cascade layers +interact with overrides, and Figma's per-mode values can differ from the +derivation-based defaults in unexpected places. Always look at the rendered +page, not just the CSS source. + +#### 7a. Static checks + +1. `bunx biome check --write ` — lint and format. +2. `bunx nx run ui-kit:build` (project name `ui-kit`, not `ui`). + +#### 7b. Storybook + Chrome DevTools MCP visual diff + +Storybook is the source of truth for visual verification. Always run the +**before** and **after** comparison; never assume "the math checks out so +the UI is fine." + +1. Start Storybook from the UI library folder if it isn't already running: + ```sh + cd libs/ui && npm run storybook + ``` + Assume a dev server is already running on its current port (do not + restart unless you confirm it's down). The port is printed at startup; + recent runs have used `http://localhost:55385/`. + +2. Find the component's Variants story. URL pattern: + ``` + http://localhost:/?path=/story/atoms---variants + ``` + For Button: `http://localhost:55385/?path=/story/atoms-button--variants`. + +3. Capture **before** and **after** screenshots using Chrome DevTools MCP + (when available in the session) for each theme: + - Light (Storybook theme switcher → "light") + - Dark (Storybook theme switcher → "dark") + - Reverse (Storybook theme switcher → "reverse") + - Auto in a system set to dark — verifies the `@media + (prefers-color-scheme: dark)` fallback path + + Pseudocode (use whichever tool surface is available): + ``` + navigate to http://localhost:/?path=/story/atoms-button--variants + for theme in [light, dark, reverse]: + set Storybook globals.theme = theme + wait for repaint + screenshot iframe + ``` + +4. Diff before/after pairwise: + - **Pixel-identical** → migration is a non-event for the user. Report it + and move on. + - **Close but not identical** → likely OKLCH→sRGB rounding noise (ΔE < 1 + is invisible). Note as "no visible change." + - **Visibly different** → STOP. This is a finding. Open a section in + the report (see step 7d). + +5. Sanity-check downstream components that read the legacy aliases — for + Button that's the product-card stories. They should render identically + because the alias resolves to the same color. + +#### 7c. Fallback: inspect compiled CSS without DevTools MCP + +If Chrome DevTools MCP isn't available, you can still confirm the cascade +landed correctly by fetching Storybook's compiled stylesheet directly: + +```sh +curl -s http://localhost:/static/css/main.css | \ + grep -nE "color--.*: #" | head -40 +``` + +Check that: +- Light defaults appear inside `@layer theme { :root { … } }`. +- The class-based override appears at the **top level** as + `:is(.dark, .always-dark) { … }` (top-level beats layered at equal + specificity). +- The system-pref override appears as + `@media (prefers-color-scheme: dark) { :root:not(.light):not(.always-light) { … } }`. +- Vendor-prefixed legacy forms (`:-webkit-any(…)`, `:-moz-any(…)`) may + also appear — these are harmless side-effects of the PostCSS pipeline + and do not match in modern browsers. + +If the modern `:is(.dark, …)` block is missing, the dark fragment was +emitted with the wrong selector. Re-check the splitter output. + +#### 7d. Document every visible difference + +For each story where the rendered output changed visibly between before +and after: + +```markdown +### Difference: + +- Before: +- After: +- Root cause: +- Recommendation: +``` + +Present every difference to the user. Do not merge until each one is +either confirmed-intentional or fixed. + +### 8. Cleanup (after all components migrate) + +Defer until every component in the library has gone through this migration: + +- Delete the legacy `--color-{component}-{role}` reference-layer aliases + from each `_.css`. +- Delete OKLCH `state` knobs from `_semantic.css` that no longer drive any + component (`--state-hover`, `--state-active`, `--state-light-variant`, …) + if nothing else uses them. +- Migrate app brand overrides (see below). +- Remove the bridge `--padding-{component}-{size}` shorthands if components + switch to `px-/py-` utilities. + +## App brand overrides + +`apps/n1/src/tokens/_n1-components.css` does: + +```css +--color-button-primary: oklch(0.4495 0 0); +--color-button-tertiary: #f6f6f6; +``` + +This was effective under the old architecture because Button read tokens +through `--color-button-primary`. After migration, Button reads +`--color-button-bg-primary-base` directly, so the override **silently no +longer applies**. + +Three options to restore app brand customization: + +1. **App migrates too** — change `apps/n1/_n1-components.css` to override + the new flat names (`--color-button-bg-primary-base`, + `--color-button-bg-primary-hover`, `--color-button-bg-primary-active`, + `--color-button-border-primary-base`, …). Many more vars, but explicit. +2. **Fallback chain in fragment** — instead of literal Figma values in + `figma/light/.css`, wrap them as + `var(--color-button-primary, oklch(…figma…))`. Then a legacy app override + still wins. Adds complexity, breaks the "pure export" property. +3. **Brand override aliases** — at the app level, declare a single set of + semantic-brand vars (`--color-brand-primary` etc.) and have **the Figma + export reference them at the top of the chain**. Cleanest long-term, but + requires coordinating Figma variables with the codebase. + +Pick once for the library; document in the app's tokens README. Until then, +note in PR description which apps lose their brand override. + +## Common gotchas + +- **`@theme static` must be reachable from the Tailwind entry CSS.** The + fragments under `tokens/figma/{light,dark}/` are imported transitively + via `_button.css` ← `components.css` ← `_tokens-base.css` ← `index.css`. + If you add the fragment but skip the import, Tailwind never sees the + tokens and the utilities silently produce no styles. +- **Dark override must NOT be inside `@theme static`.** `@theme` is for + declaring tokens; selectors inside it are ignored. Put `:is(.dark, …)` + blocks at the top level of the CSS file. +- **Tailwind v4 utility generation is name-based.** A token named + `--color-button-fg-primary-borderless-base` generates the utility + `text-button-fg-primary-borderless-base` — long but consistent. Don't try + to shorten names just for ergonomics. +- **Reading vs writing token names.** When the `.tsx` says + `border-(length:--)`, the value inside the parens is **the CSS + variable name**, not a utility class. It must match the literal token + name from the fragment file. +- **Hover/active mode flip.** In the old system, `--state-hover` had + opposite signs in light vs dark — so `oklch(from base calc(l ± .08) c h)` + darkened in light, lightened in dark. Figma encodes both directions + explicitly per token. After migration, the `--state-hover` knob no longer + affects this component — that is intentional. + +## Troubleshooting + +### Dark text on a dark page when Storybook is in "auto" theme + +**Symptom.** With Storybook's theme picker set to **auto**, on a system +that prefers dark mode, the page background turns dark (correct) but +component foregrounds stay at light-mode values (text becomes +unreadable). Switching the picker to "dark" fixes it. + +**Cause.** Storybook's `withThemeByClassName` with `defaultTheme: "auto"` +adds **no class** to `` in auto mode. The dark fragment's +`:is(.dark, .always-dark) { … }` selector doesn't match, so component +tokens stay at light values. Meanwhile `tokens/_semantic.css` does +include a `@media (prefers-color-scheme: dark)` block, so page-level +colors flip — producing the dark-on-dark mismatch. + +**Fix.** The splitter now emits BOTH selectors for the dark fragment: +```css +:is(.dark, .always-dark) { /* overrides */ } +@media (prefers-color-scheme: dark) { + :root:not(.light):not(.always-light) { /* same overrides */ } +} +``` +This matches the existing convention in `tokens/_semantic.css`. If a +migration was performed before this fix, re-run the splitter for the +affected components and re-lint. + +### `:is(…)` rewritten to `:-webkit-any(…)` / `:-moz-any(…)` + +**Symptom.** Inspecting the compiled CSS shows the dark block as +`:-webkit-any(.dark, .always-dark) { … }` instead of the modern `:is()` +form. + +**Cause.** Tailwind v4's PostCSS pipeline emits both the modern selector +and legacy vendor-prefixed fallbacks. This is by design — both blocks +appear in the compiled output. Modern browsers ignore the legacy forms +and match `:is()`. No action needed. + +### Migration changed dark-mode colors compared to before + +**This is expected.** The previous derivation system computed dark +hover/active via a symmetric lightness flip (`oklch(from base calc(l + +0.08) c h)`). Figma encodes designer-chosen dark values that are usually +visibly different — typically brighter — than the symmetric flip. + +Surface these differences to the user (see step 7d). The user's +direction was "load Figma tokens, don't break design" — meaning Figma +*is* the intended new design. But the user still owns the decision on +each visible difference. The agent presents; the user accepts or asks +to investigate. + +### App brand overrides stop working + +Apps that override `--color-{component}-{role}` (the legacy reference- +layer token) keep working through the bridge aliases left in the +component CSS. Apps that override `--color-{component}-bg-{role}-base` +(the new flat name) take effect directly. There is no migration step +required for app overrides during this PR — they continue to work +through the alias. The follow-up cleanup task is to migrate app +overrides to the flat names and then delete the bridges. + +## Skill output expectations + +When you complete a component migration, produce: + +1. **Token renames** — table of old → new names. +2. **Downstream files affected** — and whether they need follow-up. +3. **Lint result** — confirmation that biome ran clean. +4. **Build result** — `bunx nx run ui-kit:build` passes. +5. **Visual diff report** — one entry per story showing + before/after pairs across light, dark, reverse, and (where relevant) + system-pref-auto. Every visible difference must include: + - A description of what changed (which token, which state). + - Root-cause hypothesis (Figma intent vs. bug). + - A recommendation (accept / investigate). +6. **User decisions captured** — for each visible difference, the + user's call (accept / fix). If the user asks to investigate, do not + merge until resolved. +7. **PR description draft** — including the visual diff summary and the + list of "accepted Figma changes" so reviewers know what to expect. + +## Reference files (Button) + +- `libs/ui/src/tokens/figma/light/button.css` — light defaults inside `@theme static`. +- `libs/ui/src/tokens/figma/dark/button.css` — dark overrides under `:is(.dark, .always-dark)`. +- `libs/ui/src/tokens/components/atoms/_button.css` — fragment imports + bridge tokens + legacy aliases. +- `libs/ui/src/atoms/button.tsx` — updated utility class names. + +## Script: `scripts/split-figma-tokens.mjs` + +The splitter reads `tokens/figma/{light,dark}/variables.css` and writes +component fragments. Usage: + +```sh +node .agents/figma-token-binding/scripts/split-figma-tokens.mjs +node .agents/figma-token-binding/scripts/split-figma-tokens.mjs --list # list available components +node .agents/figma-token-binding/scripts/split-figma-tokens.mjs --all # split every component +``` + +It identifies a token as belonging to component `X` if and only if the +token name contains `-X-` as a discrete segment after the property prefix, +and the segment before `X` is in a known property prefix set +(`color`, `padding`, `spacing`, `text`, `radius`, `border-width`). This +rule keeps `--color-product-card-button-*` from being included in `button`. diff --git a/.agents/skills/figma-token-binding/scripts/apply-light-dark.mjs b/.agents/skills/figma-token-binding/scripts/apply-light-dark.mjs new file mode 100644 index 000000000..6f45b20a5 --- /dev/null +++ b/.agents/skills/figma-token-binding/scripts/apply-light-dark.mjs @@ -0,0 +1,186 @@ +#!/usr/bin/env node +/* + * Migrate a component's `_.css` from the per-selector dark/reverse + * pattern to a single light-dark() declaration per token. + * + * For each Figma-bound token in the component's @theme static block: + * - look up its light and dark values in the Figma export + * - rewrite as `--name: light-dark(L, D);` (or just `L` if equal) + * + * Then strip every dark/reverse/media block (no longer needed — the global + * `_tokens-base.css` already flips color-scheme for .dark/.light/.reverse + * and system preference, and `light-dark()` follows that automatically). + * + * Hand-kept declarations (not in the Figma export — e.g. font-weight, + * spacing aliases, bridge tokens) are left untouched. + * + * Usage: + * node apply-light-dark.mjs [ ...] + */ + +import { existsSync, readFileSync, writeFileSync } from "node:fs" +import { dirname, join, resolve } from "node:path" +import { fileURLToPath } from "node:url" + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const REPO_ROOT = resolve(__dirname, "..", "..", "..", "..") +const LIGHT_INPUT = join(REPO_ROOT, "libs/ui/src/tokens/figma/light/variables.css") +const DARK_INPUT = join(REPO_ROOT, "libs/ui/src/tokens/figma/dark/variables.css") +const COMP_DIR_ATOMS = join(REPO_ROOT, "libs/ui/src/tokens/components/atoms") + +const PROPERTY_PREFIXES = ["color", "padding", "spacing", "text", "radius", "border-width"] +const DECL_RE = /^\s*(--[a-z0-9-]+):\s*([^;]+);/gm + +function parseDecls(css) { + const out = new Map() + DECL_RE.lastIndex = 0 + for (const m of css.matchAll(DECL_RE)) { + out.set(m[1], m[2].trim()) + } + return out +} + +function isFigmaBoundForComponent(name, component) { + if (!name.startsWith("--")) return false + const body = name.slice(2) + for (const prefix of PROPERTY_PREFIXES) { + if (body.startsWith(`${prefix}-${component}-`)) return true + if (body === `${prefix}-${component}`) return true + } + return false +} + +function findClosingBrace(text, openIdx) { + let depth = 0 + for (let i = openIdx; i < text.length; i++) { + if (text[i] === "{") depth++ + else if (text[i] === "}") { + depth-- + if (depth === 0) return i + } + } + return -1 +} + +function removeFirstBlock(text, startRegex) { + const m = text.match(startRegex) + if (!m) return { text, removed: false } + const start = m.index + const open = text.indexOf("{", start) + const close = findClosingBrace(text, open) + if (close === -1) return { text, removed: false } + let end = close + 1 + while (end < text.length && text[end] === "\n") end++ + return { text: text.slice(0, start) + text.slice(end), removed: true } +} + +function stripSelectorBlocks(css) { + const candidates = [ + /:is\(\.dark, \.always-dark\),\s*\n:is\(\.light, \.always-light\) \.reverse \{/, + /:is\(\.dark, \.always-dark\) \.reverse \{/, + /:is\(\.dark, \.always-dark\) \{/, + /@media \(prefers-color-scheme: dark\) \{/, + /@media \(prefers-color-scheme: light\) \{/, + ] + for (let pass = 0; pass < 30; pass++) { + let changed = false + for (const re of candidates) { + const { text, removed } = removeFirstBlock(css, re) + if (removed) { + css = text + changed = true + } + } + if (!changed) break + } + return css +} + +function stripLeftoverComments(css) { + // Remove comments that introduced the now-deleted selector blocks. + const phrases = [ + /\/\* Dark visuals: explicit \.dark\/\.always-dark, OR \.reverse inside a light parent\. \*\/\n/g, + /\/\* Light visuals re-asserted: \.reverse inside a dark parent\. \*\/\n/g, + /\/\* Dark mode — class-based path \*\/\n/g, + /\/\* Dark mode — system-preference fallback \(suppressed by explicit light class\) \*\/\n/g, + ] + for (const re of phrases) css = css.replace(re, "") + // Collapse 3+ blank lines. + css = css.replace(/\n{3,}/g, "\n\n") + return css +} + +function transformThemeBlock(css, valueLookup) { + // Find every @theme static { ... } block (there may be only one). + const themeOpenRe = /@theme(?:\s+static)?\s*\{/g + let match + const segments = [] + let lastIdx = 0 + while ((match = themeOpenRe.exec(css)) !== null) { + const startBrace = css.indexOf("{", match.index) + const close = findClosingBrace(css, startBrace) + if (close === -1) continue + segments.push({ start: match.index, openBrace: startBrace, close }) + themeOpenRe.lastIndex = close + 1 + } + let out = css + // Process in reverse so indices don't shift. + for (let i = segments.length - 1; i >= 0; i--) { + const { openBrace, close } = segments[i] + const inner = out.slice(openBrace + 1, close) + const replaced = inner.replace( + /^([ \t]*)(--[a-z0-9-]+):\s*([^;]+);/gm, + (full, indent, name, value) => { + const v = valueLookup(name, value.trim()) + if (v === null) return full + return `${indent}${name}: ${v};` + } + ) + out = out.slice(0, openBrace + 1) + replaced + out.slice(close) + } + return out +} + +function buildValueLookup(component, lightDecls, darkDecls) { + return (name, originalValue) => { + if (!isFigmaBoundForComponent(name, component)) return null + if (!lightDecls.has(name)) return null + const l = lightDecls.get(name) + const d = darkDecls.get(name) ?? l + if (l === d) return l + // Multi-line for readability when values are long. + if (l.length + d.length > 50) { + return `light-dark(\n ${l},\n ${d}\n )` + } + return `light-dark(${l}, ${d})` + } +} + +function processComponent(component, lightDecls, darkDecls) { + const compFile = join(COMP_DIR_ATOMS, `_${component}.css`) + if (!existsSync(compFile)) { + console.warn(`! skip ${component}: ${compFile} not found`) + return + } + let css = readFileSync(compFile, "utf8") + css = stripSelectorBlocks(css) + css = stripLeftoverComments(css) + css = transformThemeBlock(css, buildValueLookup(component, lightDecls, darkDecls)) + css = css.replace(/\n{3,}/g, "\n\n").replace(/[ \t]+\n/g, "\n") + if (!css.endsWith("\n")) css += "\n" + writeFileSync(compFile, css) + console.log(`✓ rewrote ${compFile}`) +} + +function main() { + const args = process.argv.slice(2) + if (args.length === 0) { + console.error("usage: apply-light-dark.mjs [ ...]") + process.exit(1) + } + const lightDecls = parseDecls(readFileSync(LIGHT_INPUT, "utf8")) + const darkDecls = parseDecls(readFileSync(DARK_INPUT, "utf8")) + for (const c of args) processComponent(c, lightDecls, darkDecls) +} + +main() diff --git a/.agents/skills/figma-token-binding/scripts/apply-reverse-blocks.mjs b/.agents/skills/figma-token-binding/scripts/apply-reverse-blocks.mjs new file mode 100644 index 000000000..92d7977d6 --- /dev/null +++ b/.agents/skills/figma-token-binding/scripts/apply-reverse-blocks.mjs @@ -0,0 +1,156 @@ +#!/usr/bin/env node +/* + * Splice the splitter's regenerated dark fragment (which now includes + * .reverse selectors) into each migrated component's _.css file. + * + * For each component: + * 1. Regenerate fragments by running the splitter for that component. + * 2. Find and remove the existing dark/@media blocks in _.css. + * 3. Insert the new dark fragment body (sans header) at the original spot. + * 4. Delete the scratch fragment files. + * + * Idempotent: if a file already has the new selectors, regenerate them anyway + * (values come from Figma export so output is deterministic). + * + * Usage: + * node apply-reverse-blocks.mjs [ ...] + */ + +import { execSync } from "node:child_process" +import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs" +import { dirname, join, resolve } from "node:path" +import { fileURLToPath } from "node:url" + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const REPO_ROOT = resolve(__dirname, "..", "..", "..", "..") +const COMP_DIR_ATOMS = join( + REPO_ROOT, + "libs/ui/src/tokens/components/atoms" +) +const FRAG_DIR_LIGHT = join(REPO_ROOT, "libs/ui/src/tokens/figma/light") +const FRAG_DIR_DARK = join(REPO_ROOT, "libs/ui/src/tokens/figma/dark") +const SPLITTER = join(__dirname, "split-figma-tokens.mjs") + +function findClosingBrace(text, openIdx) { + // openIdx points to '{' + let depth = 0 + for (let i = openIdx; i < text.length; i++) { + const ch = text[i] + if (ch === "{") depth++ + else if (ch === "}") { + depth-- + if (depth === 0) return i + } + } + return -1 +} + +function removeFirstBlock(text, startRegex) { + const m = text.match(startRegex) + if (!m) return { text, removed: false } + const start = m.index + const open = text.indexOf("{", start) + const close = findClosingBrace(text, open) + if (close === -1) return { text, removed: false } + // Also gobble the trailing newline(s) + let end = close + 1 + while (end < text.length && text[end] === "\n") end++ + return { text: text.slice(0, start) + text.slice(end), removed: true } +} + +function stripFragmentHeader(fragText) { + const closing = fragText.indexOf("*/") + if (closing === -1) return fragText + let i = closing + 2 + while (i < fragText.length && (fragText[i] === "\n" || fragText[i] === " ")) + i++ + return fragText.slice(i) +} + +function processComponent(component) { + const compFile = join(COMP_DIR_ATOMS, `_${component}.css`) + if (!existsSync(compFile)) { + console.warn(`! skip ${component}: ${compFile} not found`) + return + } + + // 1. regenerate fragments + execSync(`node ${SPLITTER} ${component}`, { stdio: "inherit" }) + + const darkFragPath = join(FRAG_DIR_DARK, `${component}.css`) + const lightFragPath = join(FRAG_DIR_LIGHT, `${component}.css`) + if (!existsSync(darkFragPath)) { + console.warn(`! skip ${component}: no dark fragment generated`) + return + } + + const fragText = stripFragmentHeader(readFileSync(darkFragPath, "utf8")).trim() + + let comp = readFileSync(compFile, "utf8") + + // Remove every existing class-based dark block AND any reverse-related + // blocks left over from earlier runs; we will reinsert from the fragment. + // Order matters: collect insertion point, then strip, then insert. + const darkStartRegex = /:is\(\.dark, \.always-dark\)(?:,\s*\n:is\(\.light, \.always-light\) \.reverse)? \{/ + let insertionIdx = -1 + const firstDarkMatch = comp.match(darkStartRegex) + if (firstDarkMatch) insertionIdx = firstDarkMatch.index + + // Strip in repeated passes — there are up to 5 candidate selectors to remove. + const candidateRegexes = [ + /:is\(\.dark, \.always-dark\),\s*\n:is\(\.light, \.always-light\) \.reverse \{/, + /:is\(\.dark, \.always-dark\) \.reverse \{/, + /:is\(\.dark, \.always-dark\) \{/, + /@media \(prefers-color-scheme: dark\) \{/, + /@media \(prefers-color-scheme: light\) \{/, + ] + for (let pass = 0; pass < 20; pass++) { + let changed = false + for (const re of candidateRegexes) { + const { text, removed } = removeFirstBlock(comp, re) + if (removed) { + comp = text + changed = true + } + } + if (!changed) break + } + + // Trim trailing whitespace before the insertion gap, then trim leading at gap + comp = comp.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n" + + // Find the end of the last @theme block (or top of file if none) to insert after + let inject = "\n" + fragText + "\n" + // Find the position of the original first dark block in the original text — we + // already lost it. Instead, append fragment after the last `}` of the + // @theme static block, before any @utility or @keyframes blocks. + // Heuristic: find the first @utility/@keyframes/@layer directive; insert before it. + const insertAtMatch = comp.match(/^(@utility|@keyframes|@layer)/m) + let out + if (insertAtMatch) { + const insertAt = insertAtMatch.index + out = comp.slice(0, insertAt).trimEnd() + "\n\n" + fragText + "\n\n" + comp.slice(insertAt) + } else { + out = comp.trimEnd() + "\n\n" + fragText + "\n" + } + // Collapse 3+ blank lines + out = out.replace(/\n{3,}/g, "\n\n") + + writeFileSync(compFile, out) + console.log(`✓ patched ${compFile}`) + + // Clean up scratch fragments + if (existsSync(lightFragPath)) unlinkSync(lightFragPath) + if (existsSync(darkFragPath)) unlinkSync(darkFragPath) +} + +function main() { + const args = process.argv.slice(2) + if (args.length === 0) { + console.error("usage: apply-reverse-blocks.mjs [ ...]") + process.exit(1) + } + for (const c of args) processComponent(c) +} + +main() diff --git a/.agents/skills/figma-token-binding/scripts/split-figma-tokens.mjs b/.agents/skills/figma-token-binding/scripts/split-figma-tokens.mjs new file mode 100755 index 000000000..f74544e77 --- /dev/null +++ b/.agents/skills/figma-token-binding/scripts/split-figma-tokens.mjs @@ -0,0 +1,220 @@ +#!/usr/bin/env node +/* + * Split the full Figma token export into per-component fragments. + * + * Input: + * libs/ui/src/tokens/figma/light/variables.css + * libs/ui/src/tokens/figma/dark/variables.css + * + * Output (per component, e.g. "button"): + * libs/ui/src/tokens/figma/light/button.css (wrapped in @theme static) + * libs/ui/src/tokens/figma/dark/button.css (wrapped in :is(.dark, .always-dark)) + * + * Sizing tokens identical in both modes appear only in the light file. + * + * Membership rule: a token belongs to component `X` iff its name matches + * ---(-…)?: … + * where is one of: color, padding, spacing, text, radius, + * border-width. This excludes tokens for other components that happen to + * contain the substring (e.g. `--color-product-card-button-*`). + * + * Usage: + * node split-figma-tokens.mjs + * node split-figma-tokens.mjs --list + * node split-figma-tokens.mjs --all + */ + +import { readFileSync, writeFileSync } from "node:fs" +import { dirname, join, resolve } from "node:path" +import { fileURLToPath } from "node:url" + +const __dirname = dirname(fileURLToPath(import.meta.url)) +// scripts/ → figma-token-binding/ → skills/ → .agents/ → repo root +const REPO_ROOT = resolve(__dirname, "..", "..", "..", "..") +const LIGHT_INPUT = join( + REPO_ROOT, + "libs/ui/src/tokens/figma/light/variables.css" +) +const DARK_INPUT = join( + REPO_ROOT, + "libs/ui/src/tokens/figma/dark/variables.css" +) +const OUT_DIR_LIGHT = join(REPO_ROOT, "libs/ui/src/tokens/figma/light") +const OUT_DIR_DARK = join(REPO_ROOT, "libs/ui/src/tokens/figma/dark") + +const PROPERTY_PREFIXES = [ + "color", + "padding", + "spacing", + "text", + "radius", + "border-width", +] + +const DECL_RE = /^\s*(--[a-z0-9-]+):\s*([^;]+);/gm +const LEADING_LETTER_RE = /^[a-z]/ + +function parseDecls(css) { + const out = new Map() + DECL_RE.lastIndex = 0 + for (const m of css.matchAll(DECL_RE)) { + out.set(m[1], m[2].trim()) + } + return out +} + +function tokensForComponent(decls, component) { + const result = [] + for (const [name, value] of decls) { + if (!name.startsWith("--")) { + continue + } + const body = name.slice(2) + for (const prefix of PROPERTY_PREFIXES) { + if (body.startsWith(`${prefix}-${component}-`)) { + result.push([name, value]) + break + } + if (body === `${prefix}-${component}`) { + result.push([name, value]) + break + } + } + } + result.sort((a, b) => a[0].localeCompare(b[0])) + return result +} + +function listComponents(decls) { + const components = new Set() + for (const name of decls.keys()) { + const body = name.slice(2) + for (const prefix of PROPERTY_PREFIXES) { + if (body.startsWith(`${prefix}-`)) { + const rest = body.slice(prefix.length + 1) + const first = rest.split("-")[0] + if (first && LEADING_LETTER_RE.test(first)) { + components.add(first) + } + } + } + } + return [...components].sort() +} + +function header(mode, component) { + return [ + "/*", + ` * ${component} tokens — Figma export (${mode.toUpperCase()} mode${mode === "dark" ? " override" : ""}).`, + " *", + " * Generated from tokens/figma//variables.css via", + " * .agents/skills/figma-token-binding/scripts/split-figma-tokens.mjs", + " *", + " * DO NOT EDIT BY HAND. Re-run the splitter after updating the Figma export.", + " */", + "", + ].join("\n") +} + +function emitLight(component, lightTokens) { + const body = lightTokens.map(([n, v]) => ` ${n}: ${v};`).join("\n") + return `${header("light", component)}@theme static {\n${body}\n}\n` +} + +function emitDark(component, lightTokens, darkTokens) { + const lightMap = new Map(lightTokens) + const overrides = darkTokens.filter(([n, v]) => lightMap.get(n) !== v) + if (overrides.length === 0) { + return `${header("dark", component)}/* No tokens differ between light and dark mode. */\n` + } + const darkBody = (indent) => + overrides.map(([n, v]) => `${indent}${n}: ${v};`).join("\n") + const lightBody = (indent) => + overrides.map(([n]) => `${indent}${n}: ${lightMap.get(n)};`).join("\n") + // Selector matrix: + // .dark → dark values (explicit-dark / system-dark) + // .light .reverse → dark values (flip into dark inside an explicit light parent) + // .dark .reverse → light values (flip back to light inside an explicit dark parent) + // no class + .reverse (system pref-aware) → dark or light depending on system pref + // + // The system-pref blocks mirror the existing convention in tokens/_semantic.css. + return [ + header("dark", component), + "/* Dark visuals: explicit .dark/.always-dark, OR .reverse inside a light parent. */\n", + ":is(.dark, .always-dark),\n", + ":is(.light, .always-light) .reverse {\n", + `${darkBody(" ")}\n`, + "}\n", + "\n", + "/* Light visuals re-asserted: .reverse inside a dark parent. */\n", + ":is(.dark, .always-dark) .reverse {\n", + `${lightBody(" ")}\n`, + "}\n", + "\n", + "@media (prefers-color-scheme: dark) {\n", + " /* System-preference dark, no explicit class. */\n", + " :root:not(.light):not(.always-light) {\n", + `${darkBody(" ")}\n`, + " }\n", + "\n", + " /* .reverse inside system-dark: revert to light values. */\n", + " .reverse:not(.light):not(.always-light):not(.dark):not(.always-dark) {\n", + `${lightBody(" ")}\n`, + " }\n", + "}\n", + "\n", + "@media (prefers-color-scheme: light) {\n", + " /* .reverse with system-light: use dark values. */\n", + " .reverse:not(.light):not(.always-light):not(.dark):not(.always-dark) {\n", + `${darkBody(" ")}\n`, + " }\n", + "}\n", + ].join("") +} + +function writeFragment(component, lightTokens, darkTokens) { + const lightOut = join(OUT_DIR_LIGHT, `${component}.css`) + const darkOut = join(OUT_DIR_DARK, `${component}.css`) + writeFileSync(lightOut, emitLight(component, lightTokens)) + writeFileSync(darkOut, emitDark(component, lightTokens, darkTokens)) + console.log(`✓ wrote ${lightOut}`) + console.log(`✓ wrote ${darkOut}`) + console.log( + ` ${lightTokens.length} tokens (${darkTokens.filter(([n, v]) => lightTokens.find(([ln]) => ln === n)?.[1] !== v).length} differ in dark)` + ) +} + +function main() { + const args = process.argv.slice(2) + if (args.length === 0) { + console.error("usage: split-figma-tokens.mjs | --list | --all") + process.exit(1) + } + + const lightCss = readFileSync(LIGHT_INPUT, "utf8") + const darkCss = readFileSync(DARK_INPUT, "utf8") + const lightDecls = parseDecls(lightCss) + const darkDecls = parseDecls(darkCss) + + if (args[0] === "--list") { + for (const c of listComponents(lightDecls)) { + console.log(c) + } + return + } + + const components = + args[0] === "--all" ? listComponents(lightDecls) : [args[0]] + + for (const component of components) { + const lightTokens = tokensForComponent(lightDecls, component) + const darkTokens = tokensForComponent(darkDecls, component) + if (lightTokens.length === 0) { + console.warn(`! no tokens found for component "${component}"`) + continue + } + writeFragment(component, lightTokens, darkTokens) + } +} + +main() From 40ac3b8d8cac4ea390643fd0ae1f811a7cbc6daf Mon Sep 17 00:00:00 2001 From: Lukas Chylik Date: Thu, 28 May 2026 11:11:47 +0200 Subject: [PATCH 04/60] feat(ui-kit/badge): bind Badge to Figma tokens via light-dark() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the derivation chain (--color-badge-* → --color-* with OKLCH state knobs) with literal Figma exports, encoded as light-dark(L, D) so all themes — explicit .light/.dark, .always-*, .reverse containers, and system preference — flip automatically. No visible regressions in light mode. Dark mode has Figma-intent changes for tertiary/info/success/danger and outline fg/border (the exporter currently omits dark overrides for some of those, so they fall through to the light value — to be revisited when Figma exports them). Co-Authored-By: Claude Opus 4.7 (1M context) --- libs/ui/src/atoms/badge.tsx | 10 +- .../ui/src/tokens/components/atoms/_badge.css | 143 ++++++++++-------- 2 files changed, 85 insertions(+), 68 deletions(-) diff --git a/libs/ui/src/atoms/badge.tsx b/libs/ui/src/atoms/badge.tsx index 79e03e3bf..2874d0c2f 100644 --- a/libs/ui/src/atoms/badge.tsx +++ b/libs/ui/src/atoms/badge.tsx @@ -38,10 +38,10 @@ const badgeVariants = tv({ dynamic: [], }, size: { - sm: ["text-badge-sm font-badge-sm", "p-badge-sm"], - md: ["text-badge-md font-badge-md", "p-badge-md"], - lg: ["text-badge-lg font-badge-lg", "p-badge-lg"], - xl: ["text-badge-xl font-badge-xl", "p-badge-xl"], + sm: ["font-badge-sm text-badge-sm", "p-badge-sm"], + md: ["font-badge-md text-badge-md", "p-badge-md"], + lg: ["font-badge-lg text-badge-lg", "p-badge-lg"], + xl: ["font-badge-xl text-badge-xl", "p-badge-xl"], }, }, defaultVariants: { @@ -92,7 +92,7 @@ export function Badge({ ...style, backgroundColor: bgColor, color: fgColor, - borderColor: borderColor, + borderColor, } : style diff --git a/libs/ui/src/tokens/components/atoms/_badge.css b/libs/ui/src/tokens/components/atoms/_badge.css index 76e84a2b0..faab38a4c 100644 --- a/libs/ui/src/tokens/components/atoms/_badge.css +++ b/libs/ui/src/tokens/components/atoms/_badge.css @@ -1,72 +1,89 @@ -@theme static { - /* === BASE COLOR MAPPING === */ - /* Reference layer - single source of truth for theming */ - --color-badge-primary: var(--color-primary); - --color-badge-secondary: var(--color-secondary); - --color-badge-tertiary: var(--color-tertiary); - --color-badge-info: var(--color-info); - --color-badge-success: var(--color-success); - --color-badge-warning: var(--color-warning); - --color-badge-danger: var(--color-danger); - - /* === DERIVED COLORS === */ - /* Background colors - using reference layer */ - --color-badge-bg-primary: var(--color-badge-primary); - --color-badge-bg-secondary: var(--color-badge-secondary); - --color-badge-bg-tertiary: var(--color-badge-tertiary); - --color-badge-bg-info: var(--color-badge-info); - --color-badge-bg-success: var(--color-badge-success); - --color-badge-bg-warning: var(--color-badge-warning); - --color-badge-bg-danger: var(--color-badge-danger); - --color-badge-bg-discount: var(--color-badge-danger); /* Alias for discount */ - - /* Foreground colors */ - --color-badge-fg: var(--color-fg-light); - --color-badge-fg-primary: var(--color-badge-fg); - --color-badge-fg-secondary: var(--color-badge-fg); - --color-badge-fg-tertiary: var(--color-badge-fg); - --color-badge-fg-info: var(--color-badge-fg); - --color-badge-fg-success: var(--color-badge-fg); - --color-badge-fg-warning: var(--color-badge-fg); - --color-badge-fg-danger: var(--color-badge-fg); - --color-badge-fg-discount: var( - --color-badge-fg - ); /* Keep original for discount */ +/* + * Badge — bound directly to Figma tokens (no fragment imports, no bridges). + * + * Source: tokens/figma/{light,dark}/variables.css (Figma export). + * Re-run the export and update this file when Figma values change. + * + * Conventions: + * - All Badge color tokens are single-state (no -base/-hover/-active suffixes). + * - Dark mode is overridden via class cascade (`.dark` / `.always-dark`) AND + * `@media (prefers-color-scheme: dark)` so it matches the rest of the + * token system in `_semantic.css`. + * - Font-weight tokens are NOT yet exported from Figma; they stay aliased + * to the global font-weight scale until Figma exports them. + * - `--color-badge-border` (base) is a fallback for the `border-badge-border` + * utility on the root; every variant overrides it with a per-role color. + */ - /* Border colors */ - --color-badge-border: var(--color-border-primary); - --color-badge-border-primary: transparent; - --color-badge-border-secondary: transparent; - --color-badge-border-tertiary: transparent; - --color-badge-border-info: transparent; - --color-badge-border-success: transparent; - --color-badge-border-warning: transparent; - --color-badge-border-danger: transparent; - --color-badge-border-discount: transparent; - - /* === COMPONENT VARIANTS === */ - /* Outline variant */ - --color-badge-bg-outline: transparent; - --color-badge-fg-outline: var(--color-fg-secondary); - --color-badge-border-outline: var(--color-badge-border); +@theme static { + /* === Sizing === */ + --padding-badge-sm: 0.19rem; + --padding-badge-md: 0.31rem; + --padding-badge-lg: 0.63rem; + --padding-badge-xl: 0.94rem; - /* === SPACING === */ - --spacing-badge-sm: var(--spacing-50); - --spacing-badge-md: var(--spacing-100); - --spacing-badge-lg: var(--spacing-150); - --spacing-badge-xl: var(--spacing-200); + --text-badge-sm: 0.8rem; + --text-badge-md: 0.8rem; + --text-badge-lg: 1rem; + --text-badge-xl: 1.25rem; - /* === TYPOGRAPHY === */ - --text-badge-sm: var(--text-xs); - --text-badge-md: var(--text-xs); - --text-badge-lg: var(--text-sm); - --text-badge-xl: var(--text-md); + --radius-badge: 0.25rem; + --border-width-badge: 0.06rem; + /* === Font weights (not yet in Figma export) === */ --font-weight-badge-sm: var(--font-weight-light); --font-weight-badge-md: var(--font-weight-medium); --font-weight-badge-lg: var(--font-weight-semibold); --font-weight-badge-xl: var(--font-weight-bold); - /* === BORDERS & RADIUS === */ - --radius-badge: var(--radius-sm); - --border-width-badge: var(--border-width-sm); + + /* === Base border fallback (overridden by every variant) === */ + --color-badge-border: oklch(0 0 0 / 0); + + /* === Backgrounds === */ + --color-badge-bg-primary: light-dark( + oklch(0.807 0.101 250.446), + oklch(0.827 0.109 229.198) + ); + --color-badge-bg-secondary: light-dark( + oklch(0.7 0.202 44.441), + oklch(0.772 0.173 65.367) + ); + --color-badge-bg-tertiary: light-dark( + oklch(0.823 0.119 346.157), + oklch(0.806 0.112 11.594) + ); + --color-badge-bg-info: oklch(0.704 0.123 182.503); + --color-badge-bg-success: light-dark( + oklch(0.702 0.158 160.438), + oklch(0.844 0.143 165.23) + ); + --color-badge-bg-warning: oklch(0.798 0.164 82.706); + --color-badge-bg-danger: oklch(0.702 0.189 22.228); + --color-badge-bg-discount: oklch(0.702 0.189 22.228); + --color-badge-bg-outline: oklch(0 0 0 / 0); + + /* === Foregrounds === */ + --color-badge-fg-primary: oklch(0.13 0.043 265.132); + --color-badge-fg-secondary: oklch(0.13 0.043 265.132); + --color-badge-fg-tertiary: oklch(0.13 0.043 265.132); + --color-badge-fg-info: oklch(0.13 0.043 265.132); + --color-badge-fg-success: oklch(0.13 0.043 265.132); + --color-badge-fg-warning: oklch(0.13 0.043 265.132); + --color-badge-fg-danger: oklch(0.13 0.043 265.132); + --color-badge-fg-discount: oklch(0.13 0.043 265.132); + --color-badge-fg-outline: oklch(0.494 0.037 261.595); + + /* === Borders === */ + --color-badge-border-primary: oklch(0 0 0 / 0); + --color-badge-border-secondary: oklch(0 0 0 / 0); + --color-badge-border-tertiary: oklch(0 0 0 / 0); + --color-badge-border-info: oklch(0 0 0 / 0); + --color-badge-border-success: oklch(0 0 0 / 0); + --color-badge-border-warning: oklch(0 0 0 / 0); + --color-badge-border-danger: oklch(0 0 0 / 0); + --color-badge-border-discount: oklch(0 0 0 / 0); + --color-badge-border-outline: light-dark( + oklch(0.129 0.041 264.695 / 0.15), + oklch(1 0 0 / 0.15) + ); } From 714a3ef4273a2eda321c6ab5ed4e57537c1871b4 Mon Sep 17 00:00:00 2001 From: Lukas Chylik Date: Thu, 28 May 2026 11:11:51 +0200 Subject: [PATCH 05/60] feat(ui-kit/button): bind Button to Figma tokens via light-dark() Renames utility classes to match Figma's -base/-hover/-active naming (bg-button-bg-{role}-base, etc.) and rewrites tokens as light-dark() pairs. Dark mode and reverse containers handled automatically by the existing color-scheme machinery in tokens-base.css. Co-Authored-By: Claude Opus 4.7 (1M context) --- libs/ui/src/atoms/button.tsx | 38 +- .../src/tokens/components/atoms/_button.css | 452 ++++++++++-------- 2 files changed, 283 insertions(+), 207 deletions(-) diff --git a/libs/ui/src/atoms/button.tsx b/libs/ui/src/atoms/button.tsx index 31ac39f85..5d654a968 100644 --- a/libs/ui/src/atoms/button.tsx +++ b/libs/ui/src/atoms/button.tsx @@ -26,9 +26,9 @@ export const buttonVariants = tv({ solid: "disabled:bg-button-bg-disabled", light: "disabled:bg-button-bg-disabled", borderless: - "bg-button-bg-borderless hover:bg-button-bg-borderless-hover active:bg-button-bg-borderless-active disabled:hover:bg-button-bg-borderless", + "bg-button-bg-borderless-base hover:bg-button-bg-borderless-hover active:bg-button-bg-borderless-active disabled:hover:bg-button-bg-borderless-base", outlined: - "border bg-button-bg-outlined disabled:border-button-border-disabled disabled:hover:bg-button-bg-outlined", + "border bg-button-bg-outlined disabled:border-0 disabled:bg-button-bg-disabled disabled:hover:bg-button-bg-disabled", unstyled: "", }, uppercase: { @@ -49,7 +49,7 @@ export const buttonVariants = tv({ variant: "primary", theme: "solid", className: [ - "bg-button-bg-primary", + "bg-button-bg-primary-base", "hover:bg-button-bg-primary-hover", "active:bg-button-bg-primary-active", "text-button-fg-primary", @@ -59,7 +59,7 @@ export const buttonVariants = tv({ variant: "secondary", theme: "solid", className: [ - "bg-button-bg-secondary", + "bg-button-bg-secondary-base", "hover:bg-button-bg-secondary-hover", "active:bg-button-bg-secondary-active", "text-button-fg-secondary", @@ -69,7 +69,7 @@ export const buttonVariants = tv({ variant: "tertiary", theme: "solid", className: [ - "bg-button-bg-tertiary", + "bg-button-bg-tertiary-base", "hover:bg-button-bg-tertiary-hover", "active:bg-button-bg-tertiary-active", "text-button-fg-tertiary", @@ -79,7 +79,7 @@ export const buttonVariants = tv({ variant: "warning", theme: "solid", className: [ - "bg-button-bg-warning", + "bg-button-bg-warning-base", "hover:bg-button-bg-warning-hover", "active:bg-button-bg-warning-active", "text-button-fg-warning", @@ -89,7 +89,7 @@ export const buttonVariants = tv({ variant: "danger", theme: "solid", className: [ - "bg-button-bg-danger", + "bg-button-bg-danger-base", "hover:bg-button-bg-danger-hover", "active:bg-button-bg-danger-active", "text-button-fg-danger", @@ -99,7 +99,7 @@ export const buttonVariants = tv({ variant: "primary", theme: "light", className: [ - "bg-button-bg-primary-light", + "bg-button-bg-primary-light-base", "hover:bg-button-bg-primary-light-hover", "active:bg-button-bg-primary-light-active", "text-button-fg-primary-light", @@ -109,7 +109,7 @@ export const buttonVariants = tv({ variant: "secondary", theme: "light", className: [ - "bg-button-bg-secondary-light", + "bg-button-bg-secondary-light-base", "hover:bg-button-bg-secondary-light-hover", "active:bg-button-bg-secondary-light-active", "text-button-fg-secondary-light", @@ -119,7 +119,7 @@ export const buttonVariants = tv({ variant: "tertiary", theme: "light", className: [ - "bg-button-bg-tertiary-light", + "bg-button-bg-tertiary-light-base", "hover:bg-button-bg-tertiary-light-hover", "active:bg-button-bg-tertiary-light-active", "text-button-fg-tertiary-light", @@ -129,7 +129,7 @@ export const buttonVariants = tv({ variant: "warning", theme: "light", className: [ - "bg-button-bg-warning-light", + "bg-button-bg-warning-light-base", "hover:bg-button-bg-warning-light-hover", "active:bg-button-bg-warning-light-active", "text-button-fg-warning-light", @@ -139,7 +139,7 @@ export const buttonVariants = tv({ variant: "danger", theme: "light", className: [ - "bg-button-bg-danger-light", + "bg-button-bg-danger-light-base", "hover:bg-button-bg-danger-light-hover", "active:bg-button-bg-danger-light-active", "text-button-fg-danger-light", @@ -223,17 +223,17 @@ export const buttonVariants = tv({ { theme: "outlined", size: "sm", - className: "border-(length:--border-button-width-sm)", + className: "border-(length:--border-width-button-sm)", }, { theme: "outlined", size: "md", - className: "border-(length:--border-button-width-md)", + className: "border-(length:--border-width-button-md)", }, { theme: "outlined", size: "lg", - className: "border-(length:--border-button-width-lg)", + className: "border-(length:--border-width-button-lg)", }, ], defaultVariants: { @@ -294,9 +294,13 @@ export function Button({ ) : ( <> - {icon && iconPosition === "left" && } + {icon && iconPosition === "left" && ( + + )} {children} - {icon && iconPosition === "right" && } + {icon && iconPosition === "right" && ( + + )} )} diff --git a/libs/ui/src/tokens/components/atoms/_button.css b/libs/ui/src/tokens/components/atoms/_button.css index d4e7d04d5..c1203096b 100644 --- a/libs/ui/src/tokens/components/atoms/_button.css +++ b/libs/ui/src/tokens/components/atoms/_button.css @@ -1,221 +1,293 @@ -:root { - --opacity-outlined-hover: 16%; - --opacity-outlined-active: 12%; -} +/* + * Button — bound directly to Figma tokens (no fragment imports, no bridges). + * + * Source: tokens/figma/{light,dark}/variables.css (Figma export). + * Re-run the export and update this file when Figma values change. + * + * Conventions: + * - Single-state tokens have no `-base` suffix (e.g. fg-primary, border-primary, bg-disabled). + * - Multi-state tokens keep `-base/-hover/-active` (bg-{role}, bg-{role}-light, bg-borderless). + * - Padding stays split into x/y (Figma source) and is combined into a shorthand here so the + * existing `p-button-{size}` utility keeps working without per-axis class churn. + * - Dark mode is overridden via class cascade (`.dark` / `.always-dark`) AND + * `@media (prefers-color-scheme: dark)` so it matches the rest of the + * token system in `_semantic.css`. + */ @theme static { - /* === BASE COLOR MAPPING === */ - /* Reference layer - single source of truth for theming */ - --color-button-primary: var(--color-primary); - --color-button-secondary: var(--color-secondary); - --color-button-tertiary: var(--color-tertiary); - --color-button-warning: var(--color-warning); - --color-button-danger: var(--color-danger); - --color-button-borderless: transparent; - - /* === DERIVED COLORS === */ - /* Background colors - using reference layer */ - --color-button-bg-primary: var(--color-button-primary); - --color-button-bg-secondary: var(--color-button-secondary); - --color-button-bg-tertiary: var(--color-button-tertiary); - --color-button-bg-warning: var(--color-button-warning); - --color-button-bg-danger: var(--color-button-danger); - - /* Foreground colors */ - --color-button-fg: var(--color-fg-light); - --color-button-fg-light: var(--color-fg-light); - --color-button-fg-outlined: var(--color-fg-primary); - --color-button-fg-primary: var(--color-button-fg); - --color-button-fg-secondary: var(--color-button-fg); - --color-button-fg-tertiary: var(--color-button-fg); - --color-button-fg-warning: var(--color-button-fg); - --color-button-fg-danger: var(--color-button-fg); - --color-button-fg-primary-light: var(--color-button-fg-light); - --color-button-fg-secondary-light: var(--color-button-fg-light); - --color-button-fg-tertiary-light: var(--color-button-fg-light); - --color-button-fg-warning-light: var(--color-button-fg-light); - --color-button-fg-danger-light: var(--color-button-fg-light); - --color-button-fg-outlined-primary: var(--color-button-fg-outlined); - --color-button-fg-outlined-secondary: var(--color-button-fg-outlined); - --color-button-fg-outlined-tertiary: var(--color-button-fg-outlined); - --color-button-fg-outlined-warning: var(--color-button-fg-outlined); - --color-button-fg-outlined-danger: var(--color-button-fg-outlined); - --color-button-fg-primary-borderless: var(--color-primary-accent-fg); - --color-button-fg-secondary-borderless: var(--color-secondary-accent-fg); - --color-button-fg-tertiary-borderless: var(--color-tertiary-accent-fg); - --color-button-fg-warning-borderless: var(--color-warning-fg); - --color-button-fg-danger-borderless: var(--color-danger-fg); - - /* Border colors */ - --color-button-border-primary: var(--color-button-primary); - --color-button-border-secondary: var(--color-button-secondary); - --color-button-border-tertiary: var(--color-button-tertiary); - --color-button-border-warning: var(--color-button-warning); - --color-button-border-danger: var(--color-button-danger); - - /* === STATE VARIATIONS === */ - /* Primary states */ - --color-button-bg-primary-hover: oklch( - from var(--color-button-primary) calc(l + var(--state-hover)) c h - ); - --color-button-bg-primary-active: oklch( - from var(--color-button-primary) calc(l + var(--state-active)) c h - ); - - /* Secondary states */ - --color-button-bg-secondary-hover: oklch( - from var(--color-button-secondary) calc(l + var(--state-hover)) c h - ); - --color-button-bg-secondary-active: oklch( - from var(--color-button-secondary) calc(l + var(--state-active)) c h - ); - - /* Tertiary states */ - --color-button-bg-tertiary-hover: oklch( - from var(--color-button-tertiary) calc(l + var(--state-hover)) c h - ); - --color-button-bg-tertiary-active: oklch( - from var(--color-button-tertiary) calc(l + var(--state-active)) c h - ); - - /* Warning states */ - --color-button-bg-warning-hover: oklch( - from var(--color-button-warning) calc(l + var(--state-hover)) c h - ); - --color-button-bg-warning-active: oklch( - from var(--color-button-warning) calc(l + var(--state-active)) c h - ); - - /* Danger states */ - --color-button-bg-danger-hover: oklch( - from var(--color-button-danger) calc(l + var(--state-hover)) c h - ); - --color-button-bg-danger-active: oklch( - from var(--color-button-danger) calc(l + var(--state-active)) c h - ); - - /* Borderless states */ - --color-button-bg-borderless: var(--color-button-borderless); - --color-button-bg-borderless-hover: var(--color-fill-hover); - --color-button-bg-borderless-active: var(--color-fill-active); - - /* === COMPONENT VARIANTS === */ - /* Light variants */ - --color-button-bg-primary-light: var(--color-primary-light); - --color-button-bg-secondary-light: var(--color-secondary-light); - --color-button-bg-tertiary-light: var(--color-tertiary-light); - --color-button-bg-warning-light: var(--color-warning-light); - --color-button-bg-danger-light: var(--color-danger-light); - - /* Light variant states */ - --color-button-bg-primary-light-hover: oklch( - from var(--color-button-bg-primary-light) calc(l + var(--state-hover)) c h - ); - --color-button-bg-primary-light-active: oklch( - from var(--color-button-bg-primary-light) calc(l + var(--state-active)) c h - ); - --color-button-bg-secondary-light-hover: oklch( - from var(--color-button-bg-secondary-light) calc(l + var(--state-hover)) c h - ); - --color-button-bg-secondary-light-active: oklch( - from var(--color-button-bg-secondary-light) calc(l + var(--state-active)) c - h + /* === Sizing === */ + --spacing-button-sm: 0.63rem; + --spacing-button-md: 0.94rem; + --spacing-button-lg: 1.25rem; + + --padding-button-x-sm: 0.94rem; + --padding-button-x-md: 1.25rem; + --padding-button-x-lg: 2.19rem; + --padding-button-y-sm: 0.19rem; + --padding-button-y-md: 0.31rem; + --padding-button-y-lg: 0.63rem; + + --padding-button-sm: var(--padding-button-y-sm) var(--padding-button-x-sm); + --padding-button-md: var(--padding-button-y-md) var(--padding-button-x-md); + --padding-button-lg: var(--padding-button-y-lg) var(--padding-button-x-lg); + + --text-button-sm: 1rem; + --text-button-md: 1.25rem; + --text-button-lg: 1.56rem; + + --radius-button-sm: 0.25rem; + --radius-button-md: 0.5rem; + --radius-button-lg: 0.75rem; + + --border-width-button-sm: 0.06rem; + --border-width-button-md: 0.13rem; + --border-width-button-lg: 0.19rem; + + /* === Focus ring === */ + --color-button-ring: light-dark(oklch(0 0 0), oklch(1 0 0)); + + /* === Primary === */ + --color-button-bg-primary-base: light-dark( + oklch(0.807 0.101 250.446), + oklch(0.827 0.109 229.198) + ); + --color-button-bg-primary-hover: light-dark( + oklch(0.73 0.105 251.457), + oklch(0.898 0.094 209.143) ); - --color-button-bg-tertiary-light-hover: oklch( - from var(--color-button-bg-tertiary-light) calc(l + var(--state-hover)) c h + --color-button-bg-primary-active: light-dark( + oklch(0.689 0.105 251.583), + oklch(0.932 0.092 198.233) ); - --color-button-bg-tertiary-light-active: oklch( - from var(--color-button-bg-tertiary-light) calc(l + var(--state-active)) c h + --color-button-fg-primary: oklch(0.13 0.043 265.132); + + --color-button-bg-primary-light-base: oklch(0.937 0.03 251.281); + --color-button-bg-primary-light-hover: light-dark( + oklch(0.828 0.063 252.273), + oklch(0.969 0.046 196.42) ); - --color-button-bg-warning-light-hover: oklch( - from var(--color-button-bg-warning-light) calc(l + var(--state-hover)) c h + --color-button-bg-primary-light-active: light-dark( + oklch(0.79 0.063 250.849), + oklch(0.969 0.046 196.42) ); - --color-button-bg-warning-light-active: oklch( - from var(--color-button-bg-warning-light) calc(l + var(--state-active)) c h + --color-button-fg-primary-light: oklch(0.13 0.043 265.132); + + --color-button-border-primary: light-dark( + oklch(0.807 0.101 250.446), + oklch(0.827 0.109 229.198) ); - --color-button-bg-danger-light-hover: oklch( - from var(--color-button-bg-danger-light) calc(l + var(--state-hover)) c h + --color-button-bg-outlined-primary-hover: light-dark( + oklch(0.812 0.099 249.126 / 0.161), + oklch(0.823 0.112 229.19 / 0.161) ); - --color-button-bg-danger-light-active: oklch( - from var(--color-button-bg-danger-light) calc(l + var(--state-active)) c h + --color-button-bg-outlined-primary-active: light-dark( + oklch(0.806 0.102 249.608 / 0.122), + oklch(0.813 0.111 233.957 / 0.122) ); + --color-button-fg-outlined-primary: light-dark( + oklch(0.13 0.043 265.132), + oklch(1 0 0) + ); + + --color-button-fg-primary-borderless: oklch(0.507 0.063 255.385); - /* Outlined variants */ - --color-button-bg-outlined: transparent; - --color-button-bg-outlined-primary-hover: --alpha( - var(--color-button-primary) / - var(--opacity-outlined-hover) + /* === Secondary === */ + --color-button-bg-secondary-base: light-dark( + oklch(0.7 0.202 44.441), + oklch(0.772 0.173 65.367) ); - --color-button-bg-outlined-primary-active: --alpha( - var(--color-button-primary) / - var(--opacity-outlined-active) + --color-button-bg-secondary-hover: light-dark( + oklch(0.628 0.201 39.026), + oklch(0.881 0.181 94.02) ); - --color-button-bg-outlined-secondary-hover: --alpha( - var(--color-button-secondary) / - var(--opacity-outlined-hover) + --color-button-bg-secondary-active: light-dark( + oklch(0.588 0.198 36.788), + oklch(0.907 0.182 99.038) ); - --color-button-bg-outlined-secondary-active: --alpha( - var(--color-button-secondary) / - var(--opacity-outlined-active) + --color-button-fg-secondary: oklch(0.13 0.043 265.132); + + --color-button-bg-secondary-light-base: oklch(0.893 0.063 56.696); + --color-button-bg-secondary-light-hover: light-dark( + oklch(0.726 0.128 47.749), + oklch(0.97 0.107 105.368) ); - --color-button-bg-outlined-tertiary-hover: --alpha( - var(--color-button-tertiary) / - var(--opacity-outlined-hover) + --color-button-bg-secondary-light-active: light-dark( + oklch(0.685 0.128 47.292), + oklch(0.97 0.107 105.368) ); - --color-button-bg-outlined-tertiary-active: --alpha( - var(--color-button-tertiary) / - var(--opacity-outlined-active) + --color-button-fg-secondary-light: oklch(0.13 0.043 265.132); + + --color-button-border-secondary: light-dark( + oklch(0.7 0.202 44.441), + oklch(0.772 0.173 65.367) + ); + --color-button-bg-outlined-secondary-hover: light-dark( + oklch(0.701 0.201 44.767 / 0.161), + oklch(0.833 0.172 82.058 / 0.161) ); - --color-button-bg-outlined-warning-hover: --alpha( - var(--color-button-warning) / - var(--opacity-outlined-hover) + --color-button-bg-outlined-secondary-active: light-dark( + oklch(0.702 0.2 45.096 / 0.122), + oklch(0.822 0.171 78.982 / 0.122) ); - --color-button-bg-outlined-warning-active: --alpha( - var(--color-button-warning) / - var(--opacity-outlined-active) + --color-button-fg-outlined-secondary: light-dark( + oklch(0.13 0.043 265.132), + oklch(1 0 0) ); - --color-button-bg-outlined-danger-hover: --alpha( - var(--color-button-danger) / - var(--opacity-outlined-hover) + + --color-button-fg-secondary-borderless: oklch(0.447 0.076 49.767); + + /* === Tertiary === */ + --color-button-bg-tertiary-base: light-dark( + oklch(0.823 0.119 346.157), + oklch(0.806 0.112 11.594) ); - --color-button-bg-outlined-danger-active: --alpha( - var(--color-button-danger) / - var(--opacity-outlined-active) + --color-button-bg-tertiary-hover: light-dark( + oklch(0.743 0.12 345.839), + oklch(0.747 0.158 9.988) ); + --color-button-bg-tertiary-active: light-dark( + oklch(0.704 0.119 346.312), + oklch(0.769 0.142 7.776) + ); + --color-button-fg-tertiary: oklch(0.13 0.043 265.132); - /* === DISABLED STATES === */ - --color-button-bg-disabled: var(--color-bg-disabled); - --color-button-fg-disabled: var(--color-fg-disabled); - --color-button-border-disabled: var(--color-border-disabled); + --color-button-bg-tertiary-light-base: oklch(0.941 0.034 344.058); + --color-button-bg-tertiary-light-hover: light-dark( + oklch(0.842 0.072 346.249), + oklch(0.857 0.079 9.107) + ); + --color-button-bg-tertiary-light-active: light-dark( + oklch(0.802 0.073 346.374), + oklch(0.884 0.064 6.283) + ); + --color-button-fg-tertiary-light: oklch(0.13 0.043 265.132); - /* === SPACING === */ - --spacing-button-sm: var(--spacing-150); - --spacing-button-md: var(--spacing-200); - --spacing-button-lg: var(--spacing-250); + --color-button-border-tertiary: light-dark( + oklch(0.823 0.119 346.157), + oklch(0.806 0.112 11.594) + ); + --color-button-bg-outlined-tertiary-hover: light-dark( + oklch(0.819 0.125 347.339 / 0.161), + oklch(0.706 0.189 14.04 / 0.161) + ); + --color-button-bg-outlined-tertiary-active: light-dark( + oklch(0.816 0.114 343.311 / 0.122), + oklch(0.705 0.19 14.191 / 0.122) + ); + --color-button-fg-outlined-tertiary: light-dark( + oklch(0.13 0.043 265.132), + oklch(1 0 0) + ); - --padding-button-sm: var(--spacing-50) var(--spacing-200); - --padding-button-md: var(--spacing-100) var(--spacing-250); - --padding-button-lg: var(--spacing-150) var(--spacing-400); + --color-button-fg-tertiary-borderless: oklch(0.511 0.054 323.997); - /* === TYPOGRAPHY === */ - --text-button-sm: var(--text-sm); - --text-button-md: var(--text-md); - --text-button-lg: var(--text-lg); + /* === Warning === */ + --color-button-bg-warning-base: oklch(0.798 0.164 82.706); + --color-button-bg-warning-hover: light-dark( + oklch(0.718 0.149 79.256), + oklch(0.909 0.189 99.865) + ); + --color-button-bg-warning-active: light-dark( + oklch(0.678 0.142 76.945), + oklch(0.937 0.199 105.01) + ); + --color-button-fg-warning: oklch(0.13 0.043 265.132); - /* === BORDERS & RADIUS === */ - --radius-button-sm: var(--radius-sm); - --radius-button-md: var(--radius-md); - --radius-button-lg: var(--radius-lg); + --color-button-bg-warning-light-base: oklch(0.928 0.073 92.942); + --color-button-bg-warning-light-hover: light-dark( + oklch(0.816 0.11 86.477), + oklch(0.975 0.117 107.339) + ); + --color-button-bg-warning-light-active: light-dark( + oklch(0.775 0.11 86.269), + oklch(0.975 0.117 107.339) + ); + --color-button-fg-warning-light: oklch(0.13 0.043 265.132); - --border-button-width-sm: var(--border-width-sm); - --border-button-width-md: var(--border-width-md); - --border-button-width-lg: var(--border-width-lg); + --color-button-border-warning: oklch(0.798 0.164 82.706); + --color-button-bg-outlined-warning-hover: light-dark( + oklch(0.788 0.162 82.711 / 0.161), + oklch(0.856 0.175 88.008 / 0.161) + ); + --color-button-bg-outlined-warning-active: light-dark( + oklch(0.789 0.163 80.949 / 0.122), + oklch(0.844 0.172 90.132 / 0.122) + ); + --color-button-fg-outlined-warning: light-dark( + oklch(0.13 0.043 265.132), + oklch(1 0 0) + ); - /* === FOCUS RING === */ - --color-button-ring: var(--color-ring); -} + --color-button-fg-warning-borderless: oklch(0.492 0.079 93.623); + + /* === Danger === */ + --color-button-bg-danger-base: oklch(0.702 0.189 22.228); + --color-button-bg-danger-hover: light-dark( + oklch(0.624 0.19 22.474), + oklch(0.855 0.079 18.827) + ); + --color-button-bg-danger-active: light-dark( + oklch(0.584 0.191 22.13), + oklch(0.881 0.063 18.416) + ); + --color-button-fg-danger: oklch(0.13 0.043 265.132); + + --color-button-bg-danger-light-base: oklch(0.9 0.048 18.09); + --color-button-bg-danger-light-hover: light-dark( + oklch(0.725 0.114 22.247), + oklch(0.953 0.023 24.126) + ); + --color-button-bg-danger-light-active: light-dark( + oklch(0.685 0.114 22.469), + oklch(0.962 0.019 25.602) + ); + --color-button-fg-danger-light: oklch(0.13 0.043 265.132); + + --color-button-border-danger: oklch(0.702 0.189 22.228); + --color-button-bg-outlined-danger-hover: light-dark( + oklch(0.702 0.19 23.327 / 0.161), + oklch(0.805 0.111 19.782 / 0.161) + ); + --color-button-bg-outlined-danger-active: light-dark( + oklch(0.7 0.191 23.399 / 0.122), + oklch(0.794 0.119 20.048 / 0.122) + ); + --color-button-fg-outlined-danger: light-dark( + oklch(0.13 0.043 265.132), + oklch(1 0 0) + ); + + --color-button-fg-danger-borderless: oklch(0.452 0.066 4.881); + /* === Outlined (shared transparent bg) === */ + --color-button-bg-outlined: oklch(0 0 0 / 0); + + /* === Borderless (shared neutral fill) === */ + --color-button-bg-borderless-base: light-dark( + oklch(0.129 0.041 264.695 / 0.05), + oklch(1 0 0 / 0.05) + ); + --color-button-bg-borderless-hover: light-dark( + oklch(0.129 0.041 264.695 / 0.1), + oklch(1 0 0 / 0.1) + ); + --color-button-bg-borderless-active: light-dark( + oklch(0.129 0.041 264.695 / 0.15), + oklch(1 0 0 / 0.15) + ); + + /* === Disabled (shared across all variants and themes) === */ + --color-button-bg-disabled: light-dark( + oklch(0.967 0.003 264.542), + oklch(0.278 0.034 257.672) + ); + --color-button-fg-disabled: oklch(0.551 0.027 264.328); + --color-button-border-disabled: light-dark( + oklch(0.872 0.011 261.787), + oklch(0.373 0.034 260.169) + ); +} @utility token-icon-button-spinner { @apply token-icon-spinner; -} \ No newline at end of file +} From e9dc0ab1dd08235756a749326ca540871caadeb1 Mon Sep 17 00:00:00 2001 From: Lukas Chylik Date: Thu, 28 May 2026 11:11:54 +0200 Subject: [PATCH 06/60] feat(ui-kit/checkbox): bind Checkbox to Figma tokens via light-dark() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames bg-checkbox-bg → bg-checkbox-bg-base and border-checkbox-border → border-checkbox-border-base to match Figma's naming. Manual keepers: --spacing-checkbox (not in Figma export), --spacing-checkbox-indeterminate-icon, and the form-checkbox molecule spacing tokens. Co-Authored-By: Claude Opus 4.7 (1M context) --- libs/ui/src/atoms/checkbox.tsx | 4 +- .../src/tokens/components/atoms/_checkbox.css | 111 ++++++++++++------ 2 files changed, 76 insertions(+), 39 deletions(-) diff --git a/libs/ui/src/atoms/checkbox.tsx b/libs/ui/src/atoms/checkbox.tsx index 4bc731eee..70ffc4ffc 100644 --- a/libs/ui/src/atoms/checkbox.tsx +++ b/libs/ui/src/atoms/checkbox.tsx @@ -5,8 +5,8 @@ const checkboxVariants = tv({ base: [ "relative shrink-0 cursor-pointer appearance-none", "size-checkbox", - "rounded-checkbox border border-checkbox-border", - "bg-checkbox-bg", + "rounded-checkbox border border-checkbox-border-base", + "bg-checkbox-bg-base", "after:-translate-x-1/2 after:-translate-y-1/2 after:absolute after:top-1/2 after:left-1/2", "checked:bg-checkbox-bg-checked", "checked:border-checkbox-border-checked", diff --git a/libs/ui/src/tokens/components/atoms/_checkbox.css b/libs/ui/src/tokens/components/atoms/_checkbox.css index bbb8fcac4..c63580d36 100644 --- a/libs/ui/src/tokens/components/atoms/_checkbox.css +++ b/libs/ui/src/tokens/components/atoms/_checkbox.css @@ -1,45 +1,82 @@ +/* + * Checkbox — bound directly to Figma tokens (no fragment imports, no bridges). + * + * Source: tokens/figma/{light,dark}/variables.css (Figma export). + * Re-run the export and update this file when Figma values change. + * + * Conventions: + * - Base state uses `-base` suffix (bg-base, border-base) to match Figma. + * - State tokens (checked, indeterminate, disabled) have no extra suffix. + * - Dark mode is overridden via class cascade (`.dark` / `.always-dark`) AND + * `@media (prefers-color-scheme: dark)` so it matches the rest of the + * token system in `_semantic.css`. + * - `--spacing-checkbox`, `--spacing-checkbox-indeterminate-icon`, and the + * `--spacing-form-checkbox-*` tokens are NOT yet exported from Figma; + * they stay aliased to the global spacing scale. + */ + @theme static { - /* === BASE COLOR MAPPING === */ - /* Reference layer - single source of truth for theming */ - --color-checkbox-primary: var(--color-primary); - --color-checkbox-success: var(--color-success); - --color-checkbox-danger: var(--color-danger); - - /* === DERIVED COLORS === */ - /* Background colors */ - --color-checkbox-bg: var(--color-fill-active); - --color-checkbox-bg-checked: var(--color-checkbox-success); - --color-checkbox-bg-indeterminate: var(--color-checkbox-primary); - - /* Foreground colors (icons/checkmarks) */ - --color-checkbox-fg-checked: var(--color-fg-dark); - --color-checkbox-fg-indeterminate: var(--color-fg-dark); - - /* Border colors */ - --color-checkbox-border: var(--color-border-primary); - --color-checkbox-border-checked: var(--color-checkbox-success); - --color-checkbox-border-indeterminate: var(--color-checkbox-primary); - - /* === VALIDATION STATES === */ - --color-checkbox-border-error: var(--color-checkbox-danger); - --color-checkbox-ring-error: var(--color-checkbox-danger); - - /* === DISABLED STATES === */ - --color-checkbox-bg-disabled: var(--color-bg-disabled); - --color-checkbox-fg-disabled: var(--color-fg-disabled); - --color-checkbox-border-disabled: var(--color-border-disabled); - - /* === FOCUS RINGS === */ - --color-checkbox-ring-focus: var(--color-ring); - - /* === SPACING === */ + /* === Sizing (not yet in Figma export) === */ --spacing-checkbox: var(--spacing-200); --spacing-checkbox-indeterminate-icon: 60%; - /* === BORDERS & RADIUS === */ - --radius-checkbox: var(--radius-sm); + /* === Radius === */ + --radius-checkbox: 0.25rem; + --radius-checkbox-indeterminate: 0.25rem; + + /* === Border widths === */ + --border-width-checkbox: 0.06rem; + --border-width-checkbox-error: 0.13rem; + + /* === Backgrounds === */ + --color-checkbox-bg-base: light-dark( + oklch(0.967 0.003 264.542), + oklch(0.278 0.034 257.672) + ); + --color-checkbox-bg-checked: light-dark( + oklch(0.702 0.158 160.438), + oklch(0.844 0.143 165.23) + ); + --color-checkbox-bg-indeterminate: light-dark( + oklch(0.807 0.101 250.446), + oklch(0.827 0.109 229.198) + ); + --color-checkbox-bg-disabled: light-dark( + oklch(0.967 0.003 264.542), + oklch(0.278 0.034 257.672) + ); + + /* === Foregrounds (icons / checkmark) === */ + --color-checkbox-fg-checked: oklch(1 0 0); + --color-checkbox-fg-indeterminate: oklch(1 0 0); + --color-checkbox-fg-disabled: oklch(0.551 0.027 264.328); + + /* === Borders === */ + --color-checkbox-border-base: light-dark( + oklch(0.129 0.041 264.695 / 0.15), + oklch(1 0 0 / 0.15) + ); + --color-checkbox-border-checked: light-dark( + oklch(0.702 0.158 160.438), + oklch(0.844 0.143 165.23) + ); + --color-checkbox-border-indeterminate: light-dark( + oklch(0.807 0.101 250.446), + oklch(0.827 0.109 229.198) + ); + --color-checkbox-border-disabled: light-dark( + oklch(0.872 0.011 261.787), + oklch(0.373 0.034 260.169) + ); + + /* === Validation === */ + --color-checkbox-border-error: oklch(0.702 0.189 22.228); + --color-checkbox-ring-error: oklch(0.702 0.189 22.228); + + /* === Focus ring === */ + --color-checkbox-ring-focus: light-dark(oklch(0 0 0), oklch(1 0 0)); - /* === FORM CHECKBOX MOLECULE === */ + /* === FormCheckbox molecule (not yet in Figma export) === */ --spacing-form-checkbox-gap: var(--spacing-150); --spacing-form-checkbox-text-offset: calc( var(--spacing-checkbox) + From 4ed2b87a46bf0dace04b8f7c5b7e5e337af1e621 Mon Sep 17 00:00:00 2001 From: Lukas Chylik Date: Thu, 28 May 2026 11:12:13 +0200 Subject: [PATCH 07/60] feat(ui-kit/icon): bind Icon to Figma tokens via light-dark() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Figma-exported icon foreground colors (base/primary/secondary/ success/warning/danger) and rewrites the text-icon-* size tokens inline. No TSX change — icon colors are typically inherited. Co-Authored-By: Claude Opus 4.7 (1M context) --- libs/ui/src/tokens/components/atoms/_icon.css | 95 ++++++++----------- 1 file changed, 37 insertions(+), 58 deletions(-) diff --git a/libs/ui/src/tokens/components/atoms/_icon.css b/libs/ui/src/tokens/components/atoms/_icon.css index 3b55a24fc..ebe48824b 100644 --- a/libs/ui/src/tokens/components/atoms/_icon.css +++ b/libs/ui/src/tokens/components/atoms/_icon.css @@ -1,210 +1,189 @@ +/* + * Icon — bound directly to Figma tokens. + * Source: tokens/figma/{light,dark}/variables.css. + * + * Color tokens are exported but not consumed by the TSX (icons inherit from + * surrounding text color). They're defined here so they're available when + * stories or call sites opt into colored icons. + */ + @plugin "@iconify/tailwind4"; @theme static { - --text-icon-xs: var(--text-xs); - --text-icon-sm: var(--text-sm); - --text-icon-md: var(--text-md); - --text-icon-lg: var(--text-lg); - --text-icon-xl: var(--text-xl); - --text-icon-2xl: var(--text-2xl); -} + /* === Sizes (text-icon-*) === */ + --text-icon-xs: 0.8rem; + --text-icon-sm: 1rem; + --text-icon-md: 1.25rem; + --text-icon-lg: 1.56rem; + --text-icon-xl: 1.95rem; + --text-icon-2xl: 2.44rem; + + /* === Foreground colors === */ + --color-icon-fg-base: light-dark(oklch(0.13 0.043 265.132), oklch(1 0 0)); + --color-icon-fg-primary: light-dark( + oklch(0.807 0.101 250.446), + oklch(0.827 0.109 229.198) + ); + --color-icon-fg-secondary: light-dark( + oklch(0.7 0.202 44.441), + oklch(0.772 0.173 65.367) + ); + --color-icon-fg-success: light-dark( + oklch(0.702 0.158 160.438), + oklch(0.844 0.143 165.23) + ); + --color-icon-fg-warning: oklch(0.798 0.164 82.706); + --color-icon-fg-danger: oklch(0.702 0.189 22.228); +} + +/* === Semantic glyph aliases (not part of Figma export) === */ -/* Semantic tokens for icons */ @utility token-icon-error { @apply icon-[mdi-light--alert-circle]; } - @utility token-icon-success { @apply icon-[mdi-light--check-circle]; } - @utility token-icon-warning { @apply icon-[mdi-light--alert]; } - @utility token-icon-info { @apply icon-[mdi-light--information]; } - @utility token-icon-close { @apply icon-[mdi--close]; } -/* Component tokens for icons */ @utility token-icon-input-error { @apply token-icon-error; } - @utility token-icon-input-success { @apply token-icon-success; } - @utility token-icon-input-warning { @apply token-icon-warning; } - @utility token-icon-input-info { @apply token-icon-info; } -/* Navigation icons */ @utility token-icon-chevron-down { @apply icon-[mdi--chevron-down]; } - @utility token-icon-chevron-right { @apply icon-[mdi--chevron-right]; } - @utility token-icon-chevron-left { @apply icon-[mdi--chevron-left]; } - @utility token-icon-chevron-up { @apply icon-[mdi--chevron-up]; } -/* Action icons */ @utility token-icon-spinner { @apply icon-[svg-spinners--ring-resize]; } - @utility token-icon-play { @apply icon-[mdi--play]; } - @utility token-icon-pause { @apply icon-[mdi--pause]; } - @utility token-icon-menu { @apply icon-[mdi--menu]; } - @utility token-icon-plus { @apply icon-[mdi--plus]; } - @utility token-icon-minus { @apply icon-[mdi--minus]; } - @utility token-icon-folder { @apply icon-[mdi--folder]; } - @utility token-icon-folder-open { @apply icon-[mdi--folder-open]; } - @utility token-icon-file-document { @apply icon-[mdi--file-document]; } - @utility token-icon-home { @apply icon-[mdi--home]; } - @utility token-icon-shopping-bag { @apply icon-[mdi--shopping]; } - @utility token-icon-cpu { @apply icon-[mdi--cpu-64-bit]; } - @utility token-icon-smartphone { @apply icon-[mdi--smartphone]; } - @utility token-icon-save { @apply icon-[mdi--content-save]; } - @utility token-icon-trash { @apply icon-[mdi--delete]; } - @utility token-icon-copy { @apply icon-[mdi--content-copy]; } - @utility token-icon-clipboard { @apply icon-[mdi--clipboard]; } - @utility token-icon-undo { @apply icon-[mdi--undo]; } - @utility token-icon-redo { @apply icon-[mdi--redo]; } - @utility token-icon-check { @apply icon-[mdi--check]; } - @utility token-icon-check-bold { @apply icon-[mdi--check-bold]; } - @utility token-icon-check-circle { @apply icon-[mdi--check-circle]; } - @utility token-icon-alert { @apply icon-[mdi--alert]; } - @utility token-icon-alert-circle { @apply icon-[mdi--alert-circle]; } - @utility token-icon-information { @apply icon-[mdi--information]; } - @utility token-icon-ellipsis-horizontal { @apply icon-[mdi--ellipsis-horizontal]; } - @utility token-icon-circle { @apply icon-[mdi--circle]; } - @utility token-icon-star { @apply icon-[mdi--star]; } - @utility token-icon-star-half { @apply icon-[mdi--star-half]; } -/* product card icons */ - @utility token-icon-cart { @apply icon-[mdi-light--cart]; } - @utility token-icon-eye { @apply icon-[mdi--eye]; } - @utility token-icon-heart { @apply icon-[mdi--heart]; } - @utility token-icon-search { @apply icon-[mdi--magnify]; } -/* Semantic action aliases decoupled from concrete icon glyphs. */ - @utility token-icon-increment { @apply token-icon-chevron-up; } - @utility token-icon-decrement { @apply token-icon-chevron-down; -} \ No newline at end of file +} From 944cd0be0cbbd99859db15b8cc5f52fbb8723531 Mon Sep 17 00:00:00 2001 From: Lukas Chylik Date: Thu, 28 May 2026 11:12:16 +0200 Subject: [PATCH 08/60] feat(ui-kit/input): bind Input to Figma tokens via light-dark() Renames bg-input/text-input-fg/border-input-border to the -base suffix variants Figma uses, and renames bg-input-hover/-focus to bg-input-bg-*. Validation variants pick up -{role}-base suffix on the border token. Co-Authored-By: Claude Opus 4.7 (1M context) --- libs/ui/src/atoms/input.tsx | 16 +- .../ui/src/tokens/components/atoms/_input.css | 158 +++++++++++------- 2 files changed, 109 insertions(+), 65 deletions(-) diff --git a/libs/ui/src/atoms/input.tsx b/libs/ui/src/atoms/input.tsx index e3ad0015c..4478a955b 100644 --- a/libs/ui/src/atoms/input.tsx +++ b/libs/ui/src/atoms/input.tsx @@ -6,15 +6,15 @@ const inputVariants = tv({ base: [ "block w-full", "font-normal", - "bg-input", - "text-input-fg", + "bg-input-bg-base", + "text-input-fg-base", "placeholder:font-normal", "placeholder:text-input-fg-placeholder", - "border-(length:--border-width-input) border-input-border", + "border-(length:--border-width-input) border-input-border-base", "rounded-input", "transition-all duration-200 motion-reduce:transition-none", - "hover:border-input-border-hover hover:bg-input-hover", - "focus:border-input-border-focus focus:bg-input-focus", + "hover:border-input-border-hover hover:bg-input-bg-hover", + "focus:border-input-border-focus focus:bg-input-bg-focus", "focus-visible:outline-(style:--default-ring-style) focus-visible:outline-(length:--default-ring-width)", "focus-visible:outline-input-ring", "focus-visible:outline-offset-(length:--default-ring-offset)", @@ -30,19 +30,19 @@ const inputVariants = tv({ default: "", error: [ "border-(length:--border-width-validation)", - "border-input-border-danger", + "border-input-border-danger-base", "hover:border-input-border-danger-hover", "focus:border-input-border-danger-focus", ], success: [ "border-(length:--border-width-validation)", - "border-input-border-success", + "border-input-border-success-base", "hover:border-input-border-success-hover", "focus:border-input-border-success-focus", ], warning: [ "border-(length:--border-width-validation)", - "border-input-border-warning", + "border-input-border-warning-base", "hover:border-input-border-warning-hover", "focus:border-input-border-warning-focus", ], diff --git a/libs/ui/src/tokens/components/atoms/_input.css b/libs/ui/src/tokens/components/atoms/_input.css index 0581155eb..bfac07b5c 100644 --- a/libs/ui/src/tokens/components/atoms/_input.css +++ b/libs/ui/src/tokens/components/atoms/_input.css @@ -1,77 +1,121 @@ +/* + * Input — bound directly to Figma tokens. + * Source: tokens/figma/{light,dark}/variables.css. + * + * Conventions: + * - Base state uses `-base` suffix (bg-base, border-base, fg-base). + * - Validation tokens use `-{role}-base/-hover/-focus`. + * - The shared `--border-width-validation` token from `_semantic.css` is + * still consumed by the TSX; the Figma-exported `--border-width-input-validation` + * is kept available for components that want it. + * - `--padding-input-with-button` keeps the same name (TSX consumes + * `pe-with-button` / `ps-with-button` utilities that resolve to that var). + */ + @theme static { - /* BASE TOKENS - direct mapping from semantic tokens */ - /* Base input colors */ - --color-input: var(--color-fill-base); - --color-input-border: var(--color-border-primary); - --color-input-fg: var(--color-fg-primary); - --color-input-bg-disabled: var(--color-bg-disabled); + /* === Backgrounds === */ + --color-input-bg-base: light-dark( + oklch(0.129 0.041 264.695 / 0.05), + oklch(1 0 0 / 0.05) + ); + --color-input-bg-hover: light-dark( + oklch(0.129 0.041 264.695 / 0.1), + oklch(1 0 0 / 0.1) + ); + --color-input-bg-focus: light-dark( + oklch(0.129 0.041 264.695 / 0.15), + oklch(1 0 0 / 0.15) + ); + --color-input-bg-disabled: light-dark( + oklch(0.967 0.003 264.542), + oklch(0.278 0.034 257.672) + ); - /* Validation states - primary colors */ - --color-input-danger: var(--color-danger); - --color-input-success: var(--color-success); - --color-input-warning: var(--color-warning); + /* === Foregrounds === */ + --color-input-fg-base: light-dark(oklch(0.13 0.043 265.132), oklch(1 0 0)); + --color-input-fg-disabled: oklch(0.551 0.027 264.328); + --color-input-fg-placeholder: oklch(0.554 0.041 257.417); - /* DERIVED STATES - automatically calculated from base tokens */ - /* Default state - hover/focus/disabled */ - --color-input-hover: oklch( - from var(--color-input) calc(l + var(--state-hover)) c h + /* === Borders === */ + --color-input-border-base: light-dark( + oklch(0.129 0.041 264.695 / 0.15), + oklch(1 0 0 / 0.15) ); - --color-input-focus: oklch( - from var(--color-input) calc(l + var(--state-active)) c h + --color-input-border-hover: light-dark( + oklch(0.129 0.041 264.695 / 0.15), + oklch(1 0 0 / 0.15) ); - - --color-input-border-hover: oklch( - from var(--color-input-border) calc(l + var(--state-hover)) c h + --color-input-border-focus: light-dark( + oklch(0.807 0.101 250.446), + oklch(0.827 0.109 229.198) ); - --color-input-border-focus: var(--color-primary); - --color-input-border-disabled: var(--color-border-disabled); + --color-input-border-disabled: light-dark( + oklch(0.872 0.011 261.787), + oklch(0.373 0.034 260.169) + ); + + /* === Validation (danger / success / warning) === */ + --color-input-danger: oklch(0.702 0.189 22.228); - /* Error state - hover/focus */ - --color-input-border-danger: var(--color-input-danger); - --color-input-border-danger-hover: oklch( - from var(--color-input-danger) calc(l + var(--state-hover)) c h + --color-input-border-danger-base: oklch(0.702 0.189 22.228); + --color-input-border-danger-hover: light-dark( + oklch(0.637 0.208 25.331), + oklch(0.711 0.166 22.216) + ); + --color-input-border-danger-focus: light-dark( + oklch(0.711 0.166 22.216), + oklch(0.808 0.103 19.571) ); - --color-input-border-danger-focus: var(--color-input-danger); - /* Success state - hover/focus */ - --color-input-border-success: var(--color-input-success); - --color-input-border-success-hover: oklch( - from var(--color-input-success) calc(l + var(--state-hover)) c h + --color-input-border-success-base: light-dark( + oklch(0.702 0.158 160.438), + oklch(0.844 0.143 165.23) + ); + --color-input-border-success-hover: light-dark( + oklch(0.596 0.127 163.225), + oklch(0.773 0.153 163.223) + ); + --color-input-border-success-focus: light-dark( + oklch(0.696 0.149 162.48), + oklch(0.845 0.13 164.978) ); - --color-input-border-success-focus: var(--color-input-success); - /* Warning state - hover/focus */ - --color-input-border-warning: var(--color-input-warning); - --color-input-border-warning-hover: oklch( - from var(--color-input-warning) calc(l + var(--state-hover)) c h + --color-input-border-warning-base: oklch(0.798 0.164 82.706); + --color-input-border-warning-hover: light-dark( + oklch(0.681 0.142 75.834), + oklch(0.795 0.162 86.047) + ); + --color-input-border-warning-focus: light-dark( + oklch(0.795 0.162 86.047), + oklch(0.861 0.173 91.936) ); - --color-input-border-warning-focus: var(--color-input-warning); - /* Text colors */ - --color-input-fg-disabled: var(--color-fg-disabled); - --color-input-fg-placeholder: var(--color-fg-placeholder); + /* === Focus ring === */ + --color-input-ring: light-dark(oklch(0 0 0), oklch(1 0 0)); - /* Focus ring colors - using consistent opacity */ - --color-input-ring: var(--color-ring); + /* === Border widths === */ + --border-width-input: 0.13rem; + --border-width-input-validation: 0.19rem; - /* Spacing - keeping your existing values */ - --padding-input-sm: var(--spacing-100) var(--spacing-100); - --padding-input-md: var(--spacing-150) var(--spacing-150); - --padding-input-lg: var(--spacing-200) var(--spacing-200); - --padding-with-button: var(--spacing-950); + /* === Padding === */ + --padding-input-sm: 0.31rem; + --padding-input-md: 0.59rem; + --padding-input-lg: 0.91rem; + --padding-input-with-button: 7.5rem; + --padding-input-with-button-left: 7.5rem; + --padding-input-with-button-right: 7.5rem; - /* Typography */ - --text-input-sm: var(--text-sm); - --text-input-md: var(--text-md); - --text-input-lg: var(--text-lg); + /* === Radius === */ + --radius-input: 0.5rem; + --radius-input-sm: 0.25rem; + --radius-input-md: 0.5rem; + --radius-input-lg: 0.75rem; - /* Border */ - --border-width-input: 2px; - --radius-input: var(--radius-form-control); - --radius-input-sm: var(--radius-form-control-sm); - --radius-input-md: var(--radius-form-control-md); - --radius-input-lg: var(--radius-form-control-lg); + /* === Text === */ + --text-input-sm: 1rem; + --text-input-md: 1.25rem; + --text-input-lg: 1.56rem; - /* Form field spacing */ + /* === FormField molecule (not yet in Figma export) === */ --spacing-form-field-gap: var(--spacing-50); } From 6893b41e3675544b3750ca2b82763574e2ef1a9f Mon Sep 17 00:00:00 2001 From: Lukas Chylik Date: Thu, 28 May 2026 11:12:19 +0200 Subject: [PATCH 09/60] feat(ui-kit/label): bind Label to Figma tokens via light-dark() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames text-label-fg → text-label-fg-base to match Figma. --font-label kept manually (not yet in Figma export). Co-Authored-By: Claude Opus 4.7 (1M context) --- libs/ui/src/atoms/label.tsx | 2 +- .../ui/src/tokens/components/atoms/_label.css | 27 +++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/libs/ui/src/atoms/label.tsx b/libs/ui/src/atoms/label.tsx index f05746e78..af37fce21 100644 --- a/libs/ui/src/atoms/label.tsx +++ b/libs/ui/src/atoms/label.tsx @@ -3,7 +3,7 @@ import type { VariantProps } from "tailwind-variants" import { tv } from "../utils" const labelVariants = tv({ - base: ["block", "text-label-fg", "font-label"], + base: ["block", "text-label-fg-base", "font-label"], variants: { size: { sm: "text-label-sm", diff --git a/libs/ui/src/tokens/components/atoms/_label.css b/libs/ui/src/tokens/components/atoms/_label.css index feb5e9091..ad4ceaadc 100644 --- a/libs/ui/src/tokens/components/atoms/_label.css +++ b/libs/ui/src/tokens/components/atoms/_label.css @@ -1,11 +1,22 @@ -@theme { - /* Label colors */ - --color-label-fg: var(--color-input-fg); - --color-label-fg-disabled: var(--color-input-fg-disabled); - --color-label-fg-required: var(--color-input-danger); +/* + * Label — bound directly to Figma tokens. + * Source: tokens/figma/{light,dark}/variables.css. + * + * `--font-label` is not yet exported from Figma; kept aliased to the global + * font scale. + */ + +@theme static { + --color-label-fg-base: light-dark(oklch(0.13 0.043 265.132), oklch(1 0 0)); + --color-label-fg-disabled: oklch(0.551 0.027 264.328); + --color-label-fg-required: oklch(0.702 0.189 22.228); + + --spacing-label: 0.25rem; + + --text-label-sm: 1rem; + --text-label-md: 1.25rem; + --text-label-lg: 1.56rem; + --text-label-current: 1.25rem; --font-label: var(--font-bold); - --text-label-sm: var(--text-input-sm); - --text-label-md: var(--text-input-md); - --text-label-lg: var(--text-input-lg); } From 8d9ec78f13c395894ab3f788a2579e9db3d44f97 Mon Sep 17 00:00:00 2001 From: Lukas Chylik Date: Thu, 28 May 2026 11:12:21 +0200 Subject: [PATCH 10/60] feat(ui-kit/link): add Figma-bound _link.css and register import Link TSX currently consumes no tokens (empty variants), but Figma now exports a link collection; register the tokens so future styling can use them. Also wires the new file into components.css. Co-Authored-By: Claude Opus 4.7 (1M context) --- libs/ui/src/tokens/components/atoms/_link.css | 39 +++++++++++++++++++ libs/ui/src/tokens/components/components.css | 1 + 2 files changed, 40 insertions(+) create mode 100644 libs/ui/src/tokens/components/atoms/_link.css diff --git a/libs/ui/src/tokens/components/atoms/_link.css b/libs/ui/src/tokens/components/atoms/_link.css new file mode 100644 index 000000000..7ae7db7ee --- /dev/null +++ b/libs/ui/src/tokens/components/atoms/_link.css @@ -0,0 +1,39 @@ +/* + * Link — Figma tokens (TSX currently consumes none; tokens registered for + * future styling work). + * Source: tokens/figma/{light,dark}/variables.css. + */ + +@theme static { + --color-link-base: oklch(0.507 0.063 255.385); + --color-link-hover: light-dark( + oklch(0.488 0.243 264.405), + oklch(0.504 0.128 245.46) + ); + --color-link-active: light-dark( + oklch(0.704 0.158 253.627), + oklch(0.748 0.155 233.252) + ); + --color-link-visited: light-dark( + oklch(0.547 0.246 262.866), + oklch(0.593 0.151 245.368) + ); + --color-link-disabled: oklch(0.551 0.027 264.328); + + --color-link-fg-base: oklch(0.507 0.063 255.385); + --color-link-fg-hover: light-dark( + oklch(0.488 0.243 264.405), + oklch(0.504 0.128 245.46) + ); + --color-link-fg-active: light-dark( + oklch(0.704 0.158 253.627), + oklch(0.748 0.155 233.252) + ); + --color-link-fg-visited: light-dark( + oklch(0.547 0.246 262.866), + oklch(0.593 0.151 245.368) + ); + --color-link-fg-disabled: oklch(0.551 0.027 264.328); + + --text-link: 1.25rem; +} diff --git a/libs/ui/src/tokens/components/components.css b/libs/ui/src/tokens/components/components.css index ae1a8aa57..36e667b21 100644 --- a/libs/ui/src/tokens/components/components.css +++ b/libs/ui/src/tokens/components/components.css @@ -8,6 +8,7 @@ @import "./atoms/_image.css"; @import "./atoms/_input.css"; @import "./atoms/_label.css"; +@import "./atoms/_link.css"; @import "./atoms/_numeric-input.css"; @import "./atoms/_rating.css"; @import "./atoms/_skeleton.css"; From 83a0f8ef5b3010cbf9e2827b1c8240cea604f4e8 Mon Sep 17 00:00:00 2001 From: Lukas Chylik Date: Thu, 28 May 2026 11:12:33 +0200 Subject: [PATCH 11/60] feat(ui-kit/numeric-input): bind NumericInput to Figma tokens via light-dark() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames slot/state utilities to Figma names: bg-numeric-input-invalid-bg → bg-numeric-input-bg-invalid border-numeric-input-invalid-border → border-numeric-input-border-invalid gap-numeric-input-root-{size} → gap-numeric-input-{size} Co-Authored-By: Claude Opus 4.7 (1M context) --- libs/ui/src/atoms/numeric-input.tsx | 10 +- .../components/atoms/_numeric-input.css | 144 ++++++++++++------ 2 files changed, 100 insertions(+), 54 deletions(-) diff --git a/libs/ui/src/atoms/numeric-input.tsx b/libs/ui/src/atoms/numeric-input.tsx index 79314ae69..9a153838e 100644 --- a/libs/ui/src/atoms/numeric-input.tsx +++ b/libs/ui/src/atoms/numeric-input.tsx @@ -25,9 +25,9 @@ const numericInputVariants = tv({ "data-disabled:bg-numeric-input-bg-disabled", "data-disabled:border-numeric-input-border-disabled", "data-disabled:text-numeric-input-fg-disabled", - "data-invalid:bg-numeric-input-invalid-bg", + "data-invalid:bg-numeric-input-bg-invalid", "data-invalid:border-(length:--border-width-validation)", - "data-invalid:border-numeric-input-invalid-border", + "data-invalid:border-numeric-input-border-invalid", "text-numeric-input-fg", "has-[input:not(:disabled):hover]:bg-numeric-input-input-bg-hover", "has-[input:focus]:bg-numeric-input-input-bg-focus", @@ -63,19 +63,19 @@ const numericInputVariants = tv({ variants: { size: { sm: { - root: "gap-numeric-input-root-sm text-numeric-input-sm", + root: "gap-numeric-input-sm text-numeric-input-sm", container: "h-form-control-sm rounded-numeric-input-sm", trigger: "text-numeric-input-sm", input: "pl-numeric-input-input-sm text-numeric-input-sm", }, md: { - root: "gap-numeric-input-root-md text-numeric-input-md", + root: "gap-numeric-input-md text-numeric-input-md", container: "h-form-control-md rounded-numeric-input-md", trigger: "text-numeric-input-md", input: "pl-numeric-input-input-md text-numeric-input-md", }, lg: { - root: "gap-numeric-input-root-lg text-numeric-input-lg", + root: "gap-numeric-input-lg text-numeric-input-lg", container: "h-form-control-lg rounded-numeric-input-lg", trigger: "text-numeric-input-lg", input: "pl-numeric-input-input-lg text-numeric-input-lg", diff --git a/libs/ui/src/tokens/components/atoms/_numeric-input.css b/libs/ui/src/tokens/components/atoms/_numeric-input.css index 2b17cfc2f..18e9dbe9c 100644 --- a/libs/ui/src/tokens/components/atoms/_numeric-input.css +++ b/libs/ui/src/tokens/components/atoms/_numeric-input.css @@ -1,65 +1,111 @@ +/* + * NumericInput — bound directly to Figma tokens. + * Source: tokens/figma/{light,dark}/variables.css. + * + * Conventions: + * - Token names match Figma 1:1. + * - All `-invalid` and `-disabled` state colors are flat (no `-base`). + * - `--spacing-numeric-input-{sm,md,lg}` is exported but the TSX uses + * `gap-numeric-input-{size}` utilities (renamed from `-root-{size}`). + */ + @theme static { - /* === BASE TOKEN MAPPING === */ - --color-numeric-input-bg: var(--color-form-control-bg); - --color-numeric-input-hover: oklch( - from var(--color-numeric-input-bg) calc(l + var(--state-hover)) c h + /* === Container backgrounds === */ + --color-numeric-input-bg: light-dark( + oklch(0.129 0.041 264.695 / 0.05), + oklch(1 0 0 / 0.05) ); - --color-numeric-input-focus: oklch( - from var(--color-numeric-input-bg) calc(l + var(--state-active)) c h + --color-numeric-input-bg-disabled: light-dark( + oklch(0.967 0.003 264.542), + oklch(0.278 0.034 257.672) ); - --color-numeric-input-border: var(--color-form-control-border); - --color-numeric-input-border-hover: var(--color-form-control-border-hover); - --color-numeric-input-border-focus: var(--color-form-control-border-focus); - --color-numeric-input-fg: var(--color-input-fg); + --color-numeric-input-bg-invalid: oklch(0.702 0.189 22.228 / 0.1); - --color-numeric-input-bg-disabled: var(--color-form-control-bg-disabled); - --color-numeric-input-fg-disabled: var(--color-form-control-fg-disabled); - --color-numeric-input-border-disabled: var( - --color-form-control-border-disabled + /* === Container borders === */ + --color-numeric-input-border: light-dark( + oklch(0.129 0.041 264.695 / 0.15), + oklch(1 0 0 / 0.15) ); + --color-numeric-input-border-hover: light-dark( + oklch(0.129 0.041 264.695 / 0.15), + oklch(1 0 0 / 0.15) + ); + --color-numeric-input-border-focus: light-dark( + oklch(0.807 0.101 250.446), + oklch(0.827 0.109 229.198) + ); + --color-numeric-input-border-disabled: light-dark( + oklch(0.872 0.011 261.787), + oklch(0.373 0.034 260.169) + ); + --color-numeric-input-border-invalid: oklch(0.702 0.189 22.228); - --color-numeric-input-invalid-border: var(--color-danger); - --color-numeric-input-invalid-bg: --alpha(var(--color-danger) / 10%); - - /* ==== TYPOGRAPHY ==== */ - --text-numeric-input-sm: var(--text-sm); - --text-numeric-input-md: var(--text-md); - --text-numeric-input-lg: var(--text-lg); + /* === Foreground === */ + --color-numeric-input-fg: light-dark(oklch(0.13 0.043 265.132), oklch(1 0 0)); + --color-numeric-input-fg-disabled: oklch(0.551 0.027 264.328); - /* === INPUT === */ - --color-numeric-input-input-bg: transparent; - --color-numeric-input-input-bg-hover: var(--color-numeric-input-hover); - --color-numeric-input-input-bg-focus: var(--color-numeric-input-focus); - --padding-numeric-input-input-sm: var(--spacing-100); - --padding-numeric-input-input-md: var(--spacing-150); - --padding-numeric-input-input-lg: var(--spacing-200); + /* === Inner input === */ + --color-numeric-input-input-bg: oklch(0 0 0 / 0); + --color-numeric-input-input-bg-hover: light-dark( + oklch(0.129 0.041 264.695 / 0.1), + oklch(1 0 0 / 0.1) + ); + --color-numeric-input-input-bg-focus: light-dark( + oklch(0.129 0.041 264.695 / 0.15), + oklch(1 0 0 / 0.15) + ); - /* === TRIGGER === */ - --color-numeric-input-trigger-container-bg: var(--color-form-control-border); - --color-numeric-input-trigger-bg: var(--color-numeric-input-bg); - --color-numeric-input-trigger-bg-hover: oklch( - from var(--color-numeric-input-trigger-bg) calc(l + var(--state-hover)) c h + /* === Triggers === */ + --color-numeric-input-trigger-container-bg: light-dark( + oklch(0.129 0.041 264.695 / 0.15), + oklch(1 0 0 / 0.15) + ); + --color-numeric-input-trigger-bg: light-dark( + oklch(0.129 0.041 264.695 / 0.05), + oklch(1 0 0 / 0.05) + ); + --color-numeric-input-trigger-bg-hover: light-dark( + oklch(0.129 0.041 264.695 / 0.1), + oklch(1 0 0 / 0.1) + ); + --color-numeric-input-trigger-fg: light-dark( + oklch(0.13 0.043 265.132), + oklch(1 0 0) + ); + --color-numeric-input-trigger-fg-hover: light-dark( + oklch(0.807 0.101 250.446), + oklch(0.827 0.109 229.198) ); - --color-numeric-input-trigger-fg: var(--color-numeric-input-fg); - --color-numeric-input-trigger-fg-hover: var(--color-primary); - --padding-numeric-input-trigger-x: var(--spacing-150); - --padding-numeric-input-trigger-y: 0rem; - /* === ROOT GAP === */ - --gap-numeric-input-root-sm: 0rem; - --gap-numeric-input-root-md: 0rem; - --gap-numeric-input-root-lg: 0rem; + /* === Focus ring === */ + --color-numeric-input-ring: light-dark(oklch(0 0 0), oklch(1 0 0)); + + /* === Border width === */ + --border-width-numeric-input: 0.13rem; + + /* === Padding === */ + --padding-numeric-input-input-sm: 0.31rem; + --padding-numeric-input-input-md: 0.59rem; + --padding-numeric-input-input-lg: 0.91rem; + --padding-numeric-input-trigger-x: 0.63rem; + --padding-numeric-input-trigger-y: 0rem; - /* === INCREMENT BUTTON === */ + /* === Radius === */ + --radius-numeric-input: 0.5rem; + --radius-numeric-input-sm: 0.25rem; + --radius-numeric-input-md: 0.5rem; + --radius-numeric-input-lg: 0.75rem; - --radius-numeric-input: var(--radius-form-control); - --radius-numeric-input-sm: var(--radius-form-control-sm); - --radius-numeric-input-md: var(--radius-form-control-md); - --radius-numeric-input-lg: var(--radius-form-control-lg); - --border-width-numeric-input: var(--border-width-input); + /* === Spacing / gap === */ + --spacing-numeric-input: 0.06rem; + --spacing-numeric-input-sm: 0rem; + --spacing-numeric-input-md: 0rem; + --spacing-numeric-input-lg: 0rem; - /* === FOCUS RING === */ - --color-numeric-input-ring: var(--color-ring); + /* === Text === */ + --text-numeric-input-sm: 1rem; + --text-numeric-input-md: 1.25rem; + --text-numeric-input-lg: 1.56rem; } @utility token-icon-numeric-input-increment { From a80604fdd0e0422e71147577a070120eece1d088 Mon Sep 17 00:00:00 2001 From: Lukas Chylik Date: Thu, 28 May 2026 11:12:35 +0200 Subject: [PATCH 12/60] feat(ui-kit/rating): bind Rating to Figma tokens via light-dark() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames text-rating-fg → text-rating-fg-base. Star/ring utilities and size scales unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- libs/ui/src/atoms/rating.tsx | 2 +- .../src/tokens/components/atoms/_rating.css | 36 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/libs/ui/src/atoms/rating.tsx b/libs/ui/src/atoms/rating.tsx index b97d9e5b8..8149915b2 100644 --- a/libs/ui/src/atoms/rating.tsx +++ b/libs/ui/src/atoms/rating.tsx @@ -18,7 +18,7 @@ const rating = tv({ item: [ "cursor-pointer transition-colors duration-200", "cursor-pointer transition-colors duration-200 motion-reduce:transition-none", - "text-rating-fg", + "text-rating-fg-base", "data-highlighted:text-rating-fg-active", "data-disabled:cursor-not-allowed", "data-disabled:data-highlighted:text-rating-fg-disabled", diff --git a/libs/ui/src/tokens/components/atoms/_rating.css b/libs/ui/src/tokens/components/atoms/_rating.css index 3b112b63f..3a96a1c72 100644 --- a/libs/ui/src/tokens/components/atoms/_rating.css +++ b/libs/ui/src/tokens/components/atoms/_rating.css @@ -1,30 +1,30 @@ -@theme static { - /* === BASE COLORS === */ - --color-rating-fg: var(--color-highlight); - --color-rating-fg-active: var(--color-warning); +/* + * Rating — bound directly to Figma tokens. + * Source: tokens/figma/{light,dark}/variables.css. + */ - /* === DISABLED STATES === */ - --color-rating-fg-disabled: var(--color-fg-disabled); +@theme static { + --color-rating-fg-base: light-dark( + oklch(0.872 0.011 261.787), + oklch(0.373 0.034 260.169) + ); + --color-rating-fg-active: oklch(0.798 0.164 82.706); + --color-rating-fg-disabled: oklch(0.551 0.027 264.328); - /* === SPACING === */ - --spacing-rating-sm: var(--spacing-100); - --spacing-rating-md: var(--spacing-150); - --spacing-rating-lg: var(--spacing-200); + --color-rating-ring: light-dark(oklch(0 0 0), oklch(1 0 0)); - /* === TYPOGRAPHY === */ - --text-rating-sm: var(--text-icon-sm); - --text-rating-md: var(--text-icon-md); - --text-rating-lg: var(--text-icon-lg); + --spacing-rating-sm: 0.31rem; + --spacing-rating-md: 0.63rem; + --spacing-rating-lg: 0.94rem; - /* === FOCUS RING === */ - --color-rating-ring: var(--color-ring); + --text-rating-sm: 1rem; + --text-rating-md: 1.25rem; + --text-rating-lg: 1.56rem; } -/* Semantic tokens for rating icons */ @utility token-icon-rating { @apply token-icon-star; } - @utility token-icon-rating-half { @apply token-icon-star-half; } From 3f5e4aace227f0c38ef4e2bc2ef20dcbca4c0b18 Mon Sep 17 00:00:00 2001 From: Lukas Chylik Date: Thu, 28 May 2026 11:12:38 +0200 Subject: [PATCH 13/60] feat(ui-kit/skeleton): bind Skeleton to Figma tokens via light-dark() Bridges --spacing-skeleton-text-{size} (TSX consumes via gap-skeleton-text-* utilities) to the Figma-exported --spacing-skeleton-{size}. Circle sizing, text line height, and pulse animation tokens are kept manually (not exported from Figma). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/tokens/components/atoms/_skeleton.css | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/libs/ui/src/tokens/components/atoms/_skeleton.css b/libs/ui/src/tokens/components/atoms/_skeleton.css index 8fb20b37c..23b61c5fe 100644 --- a/libs/ui/src/tokens/components/atoms/_skeleton.css +++ b/libs/ui/src/tokens/components/atoms/_skeleton.css @@ -1,34 +1,47 @@ +/* + * Skeleton — bound directly to Figma tokens. + * Source: tokens/figma/{light,dark}/variables.css. + * + * NOT in Figma export (kept manually): circle sizing, text line height, + * pulse animation tokens, and the `--spacing-skeleton-text-*` aliases that + * the TSX consumes via `gap-skeleton-text-*` utilities. + */ + @theme static { - /* === BASE COLOR MAPPING === */ - /* Background colors using reference layer */ - --color-skeleton-bg: var(--color-overlay); - --color-skeleton-bg-primary: var(--color-skeleton-bg); - --color-skeleton-bg-secondary: var(--color-surface); + --color-skeleton-bg-primary: light-dark( + oklch(0.928 0.006 264.531), + oklch(0.278 0.034 257.672) + ); + --color-skeleton-bg-secondary: light-dark( + oklch(0.967 0.003 264.542), + oklch(0.278 0.034 257.672) + ); + + --radius-skeleton-text: 0.25rem; + + --spacing-skeleton-sm: 0.31rem; + --spacing-skeleton-md: 0.63rem; + --spacing-skeleton-lg: 0.94rem; + --spacing-skeleton-xl: 1.25rem; + + /* === Not in Figma export === */ + --spacing-skeleton-text-sm: var(--spacing-skeleton-sm); + --spacing-skeleton-text-md: var(--spacing-skeleton-md); + --spacing-skeleton-text-lg: var(--spacing-skeleton-lg); + --spacing-skeleton-text-xl: var(--spacing-skeleton-xl); - /* === ANIMATION === */ - /* Use global motion tokens for consistent behavior */ --duration-skeleton-pulse-slow: 3s; --duration-skeleton-pulse-normal: 2s; --duration-skeleton-pulse-fast: 1s; - /* === SPACING === */ - --spacing-skeleton-text-sm: var(--spacing-100); - --spacing-skeleton-text-md: var(--spacing-150); - --spacing-skeleton-text-lg: var(--spacing-200); - --spacing-skeleton-text-xl: var(--spacing-250); - - /* === SIZING === */ --size-skeleton-circle-sm: 2rem; --size-skeleton-circle-md: 3rem; --size-skeleton-circle-lg: 4rem; --size-skeleton-circle-xl: 6rem; - --height-skeleton-text-line: 1rem; /* Match text line height */ - /* === BORDERS & RADIUS === */ - --radius-skeleton-text: var(--radius-sm); + --height-skeleton-text-line: 1rem; } -/* === ANIMATIONS === */ @keyframes skeleton-pulse { 0%, 100% { @@ -65,8 +78,6 @@ } } -/* Display reduced motion for storybook without having to - enable the prefers-reduced-motion feature in DevTools */ @layer utilities { .force-reduced-motion { animation: none; From 7f9780b98554545e3cce4d634adf6d41117ca1f3 Mon Sep 17 00:00:00 2001 From: Lukas Chylik Date: Thu, 28 May 2026 11:12:42 +0200 Subject: [PATCH 14/60] feat(ui-kit/status-text): bind StatusText to Figma tokens via light-dark() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multiple renames to match Figma: text-status-text-fg-default → text-status-text-fg mt-status-text-icon-long-text → mt-status-text-icon-offset-long-text mt-status-text-icon → mt-status-text-icon-offset gap-status-text-icon-gap-{size} → gap-status-text-{size} Co-Authored-By: Claude Opus 4.7 (1M context) --- libs/ui/src/atoms/status-text.tsx | 22 ++++----- .../tokens/components/atoms/_status-text.css | 45 +++++++++++-------- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/libs/ui/src/atoms/status-text.tsx b/libs/ui/src/atoms/status-text.tsx index 90efe2c61..f653954ce 100644 --- a/libs/ui/src/atoms/status-text.tsx +++ b/libs/ui/src/atoms/status-text.tsx @@ -13,25 +13,25 @@ const statusTextVariants = tv({ error: "text-status-text-fg-error", success: "text-status-text-fg-success", warning: "text-status-text-fg-warning", - default: "text-status-text-fg-default", + default: "text-status-text-fg", }, /* for long text */ align: { start: { - icon: "mt-status-text-icon-long-text self-start", + icon: "mt-status-text-icon-offset-long-text self-start", }, center: {}, }, size: { sm: { - base: "gap-status-text-icon-gap-sm text-status-text-sm", + base: "gap-status-text-sm text-status-text-sm", }, md: { - base: "gap-status-text-icon-gap-md text-status-text-md", + base: "gap-status-text-md text-status-text-md", }, lg: { - base: "items-start gap-status-text-icon-gap-lg text-status-text-lg", - icon: "mt-status-text-icon", + base: "items-start gap-status-text-lg text-status-text-lg", + icon: "mt-status-text-icon-offset", }, }, }, @@ -51,11 +51,11 @@ const ICON_MAP = { export type StatusTextProps = HTMLAttributes & VariantProps & { - ref?: Ref - icon?: IconType - showIcon?: boolean - children: ReactNode -} + ref?: Ref + icon?: IconType + showIcon?: boolean + children: ReactNode + } export function StatusText({ className, diff --git a/libs/ui/src/tokens/components/atoms/_status-text.css b/libs/ui/src/tokens/components/atoms/_status-text.css index 21bb249f6..a65c4fb88 100644 --- a/libs/ui/src/tokens/components/atoms/_status-text.css +++ b/libs/ui/src/tokens/components/atoms/_status-text.css @@ -1,32 +1,41 @@ -@theme { - /* Status colors */ - --color-status-text-fg-error: var(--color-danger); - --color-status-text-fg-success: var(--color-success); - --color-status-text-fg-warning: var(--color-warning); - --color-status-text-fg-default: var(--color-fg-secondary); +/* + * StatusText — bound directly to Figma tokens. + * Source: tokens/figma/{light,dark}/variables.css. + */ - /* Text sizes - inherit from input sizing */ - --text-status-text-sm: var(--text-input-sm); - --text-status-text-md: var(--text-input-md); - --text-status-text-lg: var(--text-input-lg); +@theme static { + --color-status-text-fg: oklch(0.494 0.037 261.595); + --color-status-text-fg-error: light-dark( + oklch(0.711 0.166 22.216), + oklch(0.808 0.103 19.571) + ); + --color-status-text-fg-success: light-dark( + oklch(0.696 0.149 162.48), + oklch(0.845 0.13 164.978) + ); + --color-status-text-fg-warning: light-dark( + oklch(0.795 0.162 86.047), + oklch(0.861 0.173 91.936) + ); - /* Icon gap - spacing between icon and text */ - --spacing-status-text-icon-gap-sm: var(--spacing-50); - --spacing-status-text-icon-gap-md: var(--spacing-100); - --spacing-status-text-icon-gap-lg: var(--spacing-200); + --spacing-status-text-sm: 0.19rem; + --spacing-status-text-md: 0.31rem; + --spacing-status-text-lg: 0.94rem; - --spacing-status-text-icon: var(--spacing-50); - --spacing-status-text-icon-long-text: var(--spacing-100); + --spacing-status-text-icon-offset: 0.19rem; + --spacing-status-text-icon-offset-long-text: 0.31rem; + + --text-status-text-sm: 1rem; + --text-status-text-md: 1.25rem; + --text-status-text-lg: 1.56rem; } @utility token-icon-status-text-success { @apply token-icon-success; } - @utility token-icon-status-text-warning { @apply token-icon-warning; } - @utility token-icon-status-text-error { @apply token-icon-error; } From c9e5c430fbcb920f75d6138c26e6fcfe92c9ede5 Mon Sep 17 00:00:00 2001 From: Lukas Chylik Date: Thu, 28 May 2026 11:12:45 +0200 Subject: [PATCH 15/60] feat(ui-kit/textarea): bind Textarea to Figma tokens via light-dark() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames: text-textarea-placeholder → text-textarea-fg-placeholder border-(length:--border-textarea-width) → border-(length:--border-width-textarea) border-textarea-border-{role} → -{role}-base Validation -focus tokens not exported by Figma; aliased back to -base. Borderless variant + placeholder-on-danger kept manually (not in Figma). Co-Authored-By: Claude Opus 4.7 (1M context) --- libs/ui/src/atoms/textarea.tsx | 10 +- .../src/tokens/components/atoms/_textarea.css | 172 +++++++++++------- 2 files changed, 111 insertions(+), 71 deletions(-) diff --git a/libs/ui/src/atoms/textarea.tsx b/libs/ui/src/atoms/textarea.tsx index 96ca404c0..40b703a41 100644 --- a/libs/ui/src/atoms/textarea.tsx +++ b/libs/ui/src/atoms/textarea.tsx @@ -9,8 +9,8 @@ const textareaVariants = tv({ "bg-textarea-bg", "text-textarea-fg", "placeholder:font-normal", - "placeholder:text-textarea-placeholder", - "border-(length:--border-textarea-width) border-textarea-border", + "placeholder:text-textarea-fg-placeholder", + "border-(length:--border-width-textarea) border-textarea-border", "rounded-textarea", "transition-all duration-200 motion-reduce:transition-none", "hover:border-textarea-border-hover hover:bg-textarea-bg-hover", @@ -25,20 +25,20 @@ const textareaVariants = tv({ default: "", error: [ "border-(length:--border-width-validation)", - "border-textarea-border-danger", + "border-textarea-border-danger-base", "hover:border-textarea-border-danger-hover", "focus:border-textarea-border-danger-focus", "placeholder:text-textarea-placeholder-danger", ], success: [ "border-(length:--border-width-validation)", - "border-textarea-border-success", + "border-textarea-border-success-base", "hover:border-textarea-border-success-hover", "focus:border-textarea-border-success-focus", ], warning: [ "border-(length:--border-width-validation)", - "border-textarea-border-warning", + "border-textarea-border-warning-base", "hover:border-textarea-border-warning-hover", "focus:border-textarea-border-warning-focus", ], diff --git a/libs/ui/src/tokens/components/atoms/_textarea.css b/libs/ui/src/tokens/components/atoms/_textarea.css index 3a0850f3e..5edba7345 100644 --- a/libs/ui/src/tokens/components/atoms/_textarea.css +++ b/libs/ui/src/tokens/components/atoms/_textarea.css @@ -1,91 +1,131 @@ +/* + * Textarea — bound directly to Figma tokens. + * Source: tokens/figma/{light,dark}/variables.css. + * + * Conventions: + * - Base bg/fg/border have NO `-base` suffix (Figma's choice for textarea). + * - Validation border tokens use `-{role}-base/-hover` (no `-focus` exported; + * `-focus` falls back semantically to `-base` to preserve TSX behavior). + * - `--color-textarea-bg-borderless(-hover/-focus)` and + * `--color-textarea-placeholder-danger` are NOT in Figma — kept aliased. + */ + @theme static { --opacity-borderless: 60%; - /* === BASE COLOR MAPPING === */ - --color-textarea-base: var(--color-form-control-bg); - --color-textarea-success: var(--color-success); - --color-textarea-warning: var(--color-warning); - --color-textarea-danger: var(--color-danger); - /* === DERIVED COLORS === */ - /* Background colors - using reference layer */ - --color-textarea-bg: var(--color-textarea-base); - --color-textarea-bg-borderless: --alpha( - var(--color-textarea-base) / - var(--opacity-borderless) + /* === Backgrounds === */ + --color-textarea-bg: light-dark( + oklch(0.129 0.041 264.695 / 0.05), + oklch(1 0 0 / 0.05) + ); + --color-textarea-bg-hover: light-dark( + oklch(0.129 0.041 264.695 / 0.1), + oklch(1 0 0 / 0.1) + ); + --color-textarea-bg-focus: light-dark( + oklch(0.129 0.041 264.695 / 0.15), + oklch(1 0 0 / 0.15) + ); + --color-textarea-bg-disabled: light-dark( + oklch(0.967 0.003 264.542), + oklch(0.278 0.034 257.672) ); - /* Foreground colors */ - --color-textarea-fg: var(--color-fg-primary); - --color-textarea-placeholder: var(--color-form-control-placeholder); - --color-textarea-fg-disabled: var(--color-form-control-fg-disabled); - - /* Border colors */ - --color-textarea-border: var(--color-form-control-border); - --color-textarea-border-success: var(--color-textarea-success); - --color-textarea-border-warning: var(--color-textarea-warning); - --color-textarea-border-danger: var(--color-textarea-danger); - --color-textarea-border-disabled: var(--color-form-control-border-disabled); + /* === Foregrounds === */ + --color-textarea-fg: light-dark(oklch(0.13 0.043 265.132), oklch(1 0 0)); + --color-textarea-fg-disabled: oklch(0.551 0.027 264.328); + --color-textarea-fg-placeholder: oklch(0.554 0.041 257.417); - /* === STATE VARIATIONS === */ - /* Default hover/focus states */ - --color-textarea-bg-hover: oklch( - from var(--color-textarea-base) calc(l + var(--state-hover)) c h + /* === Borders === */ + --color-textarea-border: light-dark( + oklch(0.129 0.041 264.695 / 0.15), + oklch(1 0 0 / 0.15) ); - --color-textarea-bg-focus: oklch( - from var(--color-textarea-base) calc(l + var(--state-active)) c h + --color-textarea-border-hover: light-dark( + oklch(0.129 0.041 264.695 / 0.15), + oklch(1 0 0 / 0.15) + ); + --color-textarea-border-focus: light-dark( + oklch(0.807 0.101 250.446), + oklch(0.827 0.109 229.198) + ); + --color-textarea-border-disabled: light-dark( + oklch(0.872 0.011 261.787), + oklch(0.373 0.034 260.169) ); - /* Border states */ - --color-textarea-border-hover: var(--color-form-control-border-hover); - --color-textarea-border-focus: var(--color-textarea-accent); - - /* === VALIDATION STATES === */ - /* Success variant */ - --color-textarea-border-success-hover: oklch( - from var(--color-textarea-success) calc(l + var(--state-hover)) c h + /* === Validation === */ + --color-textarea-border-danger-base: oklch(0.702 0.189 22.228); + --color-textarea-border-danger-hover: light-dark( + oklch(0.637 0.208 25.331), + oklch(0.711 0.166 22.216) + ); + --color-textarea-border-danger-focus: var( + --color-textarea-border-danger-base ); - --color-textarea-border-success-focus: var(--color-textarea-success); - /* Warning variant */ - --color-textarea-border-warning-hover: oklch( - from var(--color-textarea-warning) calc(l + var(--state-hover)) c h + --color-textarea-border-success-base: light-dark( + oklch(0.702 0.158 160.438), + oklch(0.844 0.143 165.23) + ); + --color-textarea-border-success-hover: light-dark( + oklch(0.596 0.127 163.225), + oklch(0.773 0.153 163.223) + ); + --color-textarea-border-success-focus: var( + --color-textarea-border-success-base ); - --color-textarea-border-warning-focus: var(--color-textarea-warning); - /* Danger variant */ - --color-textarea-border-danger-hover: oklch( - from var(--color-textarea-danger) calc(l + var(--state-hover)) c h + --color-textarea-border-warning-base: oklch(0.798 0.164 82.706); + --color-textarea-border-warning-hover: light-dark( + oklch(0.681 0.142 75.834), + oklch(0.795 0.162 86.047) + ); + --color-textarea-border-warning-focus: var( + --color-textarea-border-warning-base ); - --color-textarea-border-danger-focus: var(--color-textarea-danger); + + /* === Placeholder-on-danger (not in Figma) === */ --color-textarea-placeholder-danger: --alpha( - var(--color-textarea-danger) / + var(--color-textarea-border-danger-base) / var(--opacity-placeholder) ); - /* Borderless variant */ - --color-textarea-bg-borderless-hover: var(--color-textarea-base); + /* === Borderless variant (not in Figma) === */ + --color-textarea-bg-borderless: --alpha( + var(--color-textarea-bg) / + var(--opacity-borderless) + ); + --color-textarea-bg-borderless-hover: var(--color-textarea-bg); + --color-textarea-bg-borderless-focus: var(--color-textarea-bg); - --color-textarea-bg-borderless-focus: var(--color-textarea-base); + /* === Focus ring === */ + --color-textarea-ring: light-dark(oklch(0 0 0), oklch(1 0 0)); - /* === DISABLED STATES === */ - --color-textarea-bg-disabled: var(--color-form-control-bg-disabled); + /* === Border widths === */ + --border-width-textarea: 0.13rem; + --border-width-textarea-validation: 0.19rem; - /* === SPACING === */ - --padding-textarea-sm: var(--padding-input-sm); - --padding-textarea-md: var(--padding-input-md); - --padding-textarea-lg: var(--padding-input-lg); + /* === Padding (single value; also exposed split x/y) === */ + --padding-textarea-sm: 0.31rem; + --padding-textarea-md: 0.59rem; + --padding-textarea-lg: 0.91rem; - /* === TYPOGRAPHY === */ - --text-textarea-sm: var(--text-sm); - --text-textarea-md: var(--text-md); - --text-textarea-lg: var(--text-lg); + --padding-textarea-x-sm: 0.31rem; + --padding-textarea-x-md: 0.63rem; + --padding-textarea-x-lg: 0.94rem; + --padding-textarea-y-sm: 0.31rem; + --padding-textarea-y-md: 0.63rem; + --padding-textarea-y-lg: 0.94rem; - /* === BORDERS & RADIUS === */ - --border-textarea-width: var(--border-width-form-control); - --radius-textarea: var(--radius-form-control); - --radius-textarea-sm: var(--radius-form-control-sm); - --radius-textarea-md: var(--radius-form-control-md); + /* === Radius === */ + --radius-textarea: 0.5rem; + --radius-textarea-sm: 0.25rem; + --radius-textarea-md: 0.5rem; + --radius-textarea-lg: 0.75rem; - /* === FOCUS RING === */ - --color-textarea-ring: var(--color-form-control-ring); + /* === Text === */ + --text-textarea-sm: 1rem; + --text-textarea-md: 1.25rem; + --text-textarea-lg: 1.56rem; } From b6fb07c82788dbc129ef667cce684d41644e5701 Mon Sep 17 00:00:00 2001 From: Lukas Chylik Date: Thu, 28 May 2026 11:12:48 +0200 Subject: [PATCH 16/60] feat(ui-kit/tooltip): bind Tooltip to Figma tokens via light-dark() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames border-tooltip-border → border-tooltip-border-outline. Adds a --padding-tooltip-{size} shorthand bridge so the existing p-tooltip-{size} utility keeps working (Figma exports padding split into x/y axes). Zag.js's --tooltip-arrow-{size,background} names are bridged to the Figma tokens. Co-Authored-By: Claude Opus 4.7 (1M context) --- libs/ui/src/atoms/tooltip.tsx | 4 +- .../src/tokens/components/atoms/_tooltip.css | 59 ++++++++++++++----- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/libs/ui/src/atoms/tooltip.tsx b/libs/ui/src/atoms/tooltip.tsx index 47ece15f1..f30163f06 100644 --- a/libs/ui/src/atoms/tooltip.tsx +++ b/libs/ui/src/atoms/tooltip.tsx @@ -19,8 +19,8 @@ const tooltipVariants = tv({ variant: { default: {}, outline: { - content: "border border-tooltip-border", - arrow: "border-tooltip-border border-s border-t", + content: "border border-tooltip-border-outline", + arrow: "border-tooltip-border-outline border-s border-t", }, }, size: { diff --git a/libs/ui/src/tokens/components/atoms/_tooltip.css b/libs/ui/src/tokens/components/atoms/_tooltip.css index f3a683ecf..d39234a87 100644 --- a/libs/ui/src/tokens/components/atoms/_tooltip.css +++ b/libs/ui/src/tokens/components/atoms/_tooltip.css @@ -1,21 +1,50 @@ +/* + * Tooltip — bound directly to Figma tokens. + * Source: tokens/figma/{light,dark}/variables.css. + * + * Padding is split x/y in Figma; combined into a `--padding-tooltip-{size}` + * shorthand here so the existing `p-tooltip-{size}` utility keeps working. + * + * Zag.js requires the exact variable names `--tooltip-arrow-size` and + * `--tooltip-arrow-background` — those are bridged to the Figma tokens. + */ + @theme static { - /* === BASE TOKEN MAPPING === */ - --color-tooltip-bg: var(--color-overlay); - --color-tooltip-border: var(--color-border-primary); + --color-tooltip-bg: light-dark( + oklch(0.928 0.006 264.531), + oklch(0.278 0.034 257.672) + ); + --color-tooltip-fg: light-dark(oklch(0.13 0.043 265.132), oklch(1 0 0)); + --color-tooltip-arrow: light-dark( + oklch(0.928 0.006 264.531), + oklch(0.278 0.034 257.672) + ); + --color-tooltip-border-outline: light-dark( + oklch(0.129 0.041 264.695 / 0.15), + oklch(1 0 0 / 0.15) + ); + + --border-width-tooltip: 0.06rem; + --radius-tooltip: 0.5rem; + + --padding-tooltip-x-sm: 0.94rem; + --padding-tooltip-x-md: 1.25rem; + --padding-tooltip-x-lg: 1.88rem; + --padding-tooltip-y-sm: 0.63rem; + --padding-tooltip-y-md: 0.94rem; + --padding-tooltip-y-lg: 1.25rem; - /* === SIZING & SPACING === */ - --padding-tooltip-sm: var(--spacing-150) var(--spacing-200); - --padding-tooltip-md: var(--spacing-200) var(--spacing-250); - --padding-tooltip-lg: var(--spacing-250) var(--spacing-350); + --padding-tooltip-sm: var(--padding-tooltip-y-sm) var(--padding-tooltip-x-sm); + --padding-tooltip-md: var(--padding-tooltip-y-md) var(--padding-tooltip-x-md); + --padding-tooltip-lg: var(--padding-tooltip-y-lg) var(--padding-tooltip-x-lg); - --text-tooltip-sm: var(--text-xs); - --text-tooltip-md: var(--text-sm); - --text-tooltip-lg: var(--text-md); + --spacing-tooltip-arrow: 0.63rem; - --radius-tooltip: var(--radius-md); - --shadow-tooltip: var(--shadow-md); + --text-tooltip-sm: 0.8rem; + --text-tooltip-md: 1rem; + --text-tooltip-lg: 1.25rem; - /* Required for Zag.js */ - --tooltip-arrow-size: var(--spacing-150); - --tooltip-arrow-background: var(--color-tooltip-bg); + /* Zag.js required names — bridged to Figma tokens */ + --tooltip-arrow-size: var(--spacing-tooltip-arrow); + --tooltip-arrow-background: var(--color-tooltip-arrow); } From c323ec959453461ff83db2754b420d4b42a5cdc7 Mon Sep 17 00:00:00 2001 From: Lukas Chylik Date: Thu, 28 May 2026 11:13:02 +0200 Subject: [PATCH 17/60] chore(ui-kit/figma): biome formatting for Code Connect templates Pure whitespace / line-wrapping changes applied by biome --write while linting the migrated atoms. Co-Authored-By: Claude Opus 4.7 (1M context) --- libs/ui/src/atoms/figma/image.figma.tsx | 2 +- libs/ui/src/atoms/figma/numeric-input.figma.tsx | 2 +- libs/ui/src/atoms/figma/rating.figma.tsx | 2 +- libs/ui/src/atoms/figma/textarea.figma.tsx | 7 ++++++- libs/ui/src/atoms/figma/tooltip.figma.tsx | 2 +- libs/ui/src/atoms/link.tsx | 13 ++++++------- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/libs/ui/src/atoms/figma/image.figma.tsx b/libs/ui/src/atoms/figma/image.figma.tsx index c91cb1920..ecabd5535 100644 --- a/libs/ui/src/atoms/figma/image.figma.tsx +++ b/libs/ui/src/atoms/figma/image.figma.tsx @@ -15,7 +15,7 @@ figma.connect( }), }, example: ({ size }) => ( - Description + Description ), } ) diff --git a/libs/ui/src/atoms/figma/numeric-input.figma.tsx b/libs/ui/src/atoms/figma/numeric-input.figma.tsx index adb9ddf3a..67eade30b 100644 --- a/libs/ui/src/atoms/figma/numeric-input.figma.tsx +++ b/libs/ui/src/atoms/figma/numeric-input.figma.tsx @@ -35,9 +35,9 @@ figma.connect( }, example: ({ disabled, invalid, readOnly, size }) => ( ), - }, + } ) diff --git a/libs/ui/src/atoms/figma/textarea.figma.tsx b/libs/ui/src/atoms/figma/textarea.figma.tsx index 98d78cafa..8957d0a8f 100644 --- a/libs/ui/src/atoms/figma/textarea.figma.tsx +++ b/libs/ui/src/atoms/figma/textarea.figma.tsx @@ -31,7 +31,12 @@ figma.connect( }), }, example: ({ size, variant, disabled, readonly }) => ( -