diff --git a/instructions/ui-guide.md b/instructions/ui-guide.md index 7b4d19ee3..dad3d485e 100644 --- a/instructions/ui-guide.md +++ b/instructions/ui-guide.md @@ -122,7 +122,7 @@ type ThemeConditionPropName = | 'darkHasBgImage' // Active in dark mode with a background image | 'hasBgImageHover' | 'hasBgImageDarkHover' -``` + | 'print' // Active under print media query ##### Example Usage: @@ -132,9 +132,10 @@ type ThemeConditionPropName = base: 'bg-50', dark: 'bg-900', hover: 'bg-200', - darkHover: 'bg-800' + darkHover: 'bg-800', + print: 'transparent' }} - color={{ base: 'bg-950', dark: 'bg-50' }} + color={{ base: 'bg-950', dark: 'bg-50', print: 'black' }} /> ``` @@ -211,6 +212,7 @@ Layout props support responsive values. Breakpoints are defined as: - `lg`: `@media (min-width: 1024px)` - `xl`: `@media (min-width: 1280px)` - `2xl`: `@media (min-width: 1536px)` +- `print`: `@media print` (active under print mode) Any responsive property accepts a scalar or a responsive configuration object: @@ -220,6 +222,9 @@ Any responsive property accepts a scalar or a responsive configuration object: // Responsive width + +// Hide element during printing + ``` _How it works under the hood:_ The engine applies `.lf-w` and `.md:lf-w` classes while defining CSS variables (`--lf-w: 100%`, `--lf-w-md: 50%`) inline, keeping output stylesheet sizes extremely small. @@ -564,16 +569,18 @@ interface WithDivideProps { } ``` +`WithDivide` must wrap **each individual item**, not a parent container. It adds a divider between adjacent `WithDivide` siblings. + #### Example: List Group Dividers ```tsx - - - Item 1 - Item 2 - Item 3 - - + + {items.map(item => ( + + {item.name} + + ))} + ``` --- @@ -692,6 +699,27 @@ type ButtonProps = ButtonOwnProps & Since `Button` extends `FlexProps` (which extends `BoxProps`), **any layout prop available on `Flex` or `Box` can be passed directly** — no wrapping `Box` needed. Only reach for `Box asChild` when you need a prop that Flex/Box doesn't support (e.g. CSS properties only available via inline `style`). +> [!TIP] +> **`Card` is already a `Flex` component.** Because `CardProps` extends `FlexProps`, you can pass `align`, `gap`, `justify`, `direction`, and all other layout props **directly to `Card`** — no need to wrap its children in a `` container. The only prop `Card` adds beyond `Flex` is `isInteractive`. +> +> ```tsx +> // ❌ Redundant: Card + inner Flex wrapper +> +> +> Label +> ... +> +> +> +> // ✅ Correct: layout props on Card directly +> +> Label +> ... +> +> ``` +> +> `Card` defaults to `direction="column"`. Override it with `direction="row"` when you need a horizontal layout. + #### Notable Engineering Features: 1. **Dynamic Contrast Matching:** In `useButtonStyleProps`, when `variant="primary"` is set, the button fetches the user's active theme color (`derivedThemeColor`) and runs `getMostReadableColor()` to compute a text color with optimal contrast. @@ -1116,5 +1144,6 @@ Before submitting a pull request, verify that you have adhered to all core desig - [ ] **Correct loaders:** Form/button loading states use the pre-animated `svg-spinners:ring-resize` icon and **never** use custom `animate-spin` utilities. - [ ] **Type safety:** Typescript `any` is never used. All prop overrides and custom handlers are explicitly typed. - [ ] **Datetime manipulation:** Standard JavaScript `Date` is never used. `day.js` is imported for any date calculations. +- [ ] **List spacing:** For vertical lists (`Stack`), `mb="lg"` is the standard bottom margin — no need to define responsive sizes like `mb={{ base: '6rem', md: 'lg' }}`. The spacing token `lg` is consistent across breakpoints and is sufficient for all list containers. - [ ] **Component organization:** Components are strictly separated into individual files under their respective `components/` folders instead of being grouped together. - [ ] **Conventional functions:** All React components use standard function declarations (`export function Component()`) and avoid arrow functions. diff --git a/packages/ui/src/contract.ts b/packages/ui/src/contract.ts index bf270667a..e746a0a45 100644 --- a/packages/ui/src/contract.ts +++ b/packages/ui/src/contract.ts @@ -1678,9 +1678,15 @@ export const contract = { "output": { "OK": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, - "NOT_FOUND": true, "FORBIDDEN": true } }, diff --git a/packages/ui/src/system/colors/color-props.css.ts b/packages/ui/src/system/colors/color-props.css.ts index 3185bcff0..6ce47a5a3 100644 --- a/packages/ui/src/system/colors/color-props.css.ts +++ b/packages/ui/src/system/colors/color-props.css.ts @@ -5,17 +5,32 @@ import { COLOR_PROP_DEFS, THEME_CONDITIONS } from './constants' for (const { className, cssProperty, varPrefix } of Object.values( COLOR_PROP_DEFS )) { - for (const { suffix, varSuffix, selectorTemplate } of Object.values( + for (const { suffix, varSuffix, selectorTemplate, media } of Object.values( THEME_CONDITIONS - )) { + ) as Array<{ + suffix: string + varSuffix: string + selectorTemplate: string + media?: string + }>) { const fullClassName = `${className}${suffix}` const fullVar = `${varPrefix}${varSuffix}` const selector = selectorTemplate.replace('{cls}', fullClassName) - globalStyle(selector, { - [cssProperty]: `var(${fullVar})` - }) + if (media) { + globalStyle(selector, { + '@media': { + [media]: { + [cssProperty]: `var(${fullVar}) !important` + } + } + }) + } else { + globalStyle(selector, { + [cssProperty]: `var(${fullVar})` + }) + } } } diff --git a/packages/ui/src/system/colors/constants/theme-conditions.ts b/packages/ui/src/system/colors/constants/theme-conditions.ts index 300ec6989..70822930d 100644 --- a/packages/ui/src/system/colors/constants/theme-conditions.ts +++ b/packages/ui/src/system/colors/constants/theme-conditions.ts @@ -7,6 +7,7 @@ export type ThemeConditionPropName = | 'darkHasBgImage' | 'hasBgImageHover' | 'hasBgImageDarkHover' + | 'print' export type ThemeConditionProp = | T @@ -52,6 +53,12 @@ export const THEME_CONDITIONS = { suffix: '-has-bg-image-dark-hover', varSuffix: '-has-bg-image-dark-hover', selectorTemplate: '.dark .has-bg-image .{cls}:hover' + }, + print: { + suffix: '-print', + varSuffix: '-print', + selectorTemplate: '.{cls}', + media: 'print' } } as const satisfies Record< ThemeConditionPropName, @@ -59,5 +66,6 @@ export const THEME_CONDITIONS = { suffix: string varSuffix: string selectorTemplate: string + media?: string } > diff --git a/packages/ui/src/system/responsive/constant.ts b/packages/ui/src/system/responsive/constant.ts index 7d2d3807e..188e5565a 100644 --- a/packages/ui/src/system/responsive/constant.ts +++ b/packages/ui/src/system/responsive/constant.ts @@ -6,7 +6,8 @@ export const RESPONSIVE_CONDITIONS = { md: { '@media': '(min-width: 768px)' }, lg: { '@media': '(min-width: 1024px)' }, xl: { '@media': '(min-width: 1280px)' }, - '2xl': { '@media': '(min-width: 1536px)' } + '2xl': { '@media': '(min-width: 1536px)' }, + print: { '@media': 'print' } } as const satisfies Record export const LAYOUT_PROP_DEFS = { diff --git a/packages/ui/src/system/responsive/index.css.ts b/packages/ui/src/system/responsive/index.css.ts index fde22e0a6..0aeb7e1ef 100644 --- a/packages/ui/src/system/responsive/index.css.ts +++ b/packages/ui/src/system/responsive/index.css.ts @@ -45,7 +45,8 @@ const breakpointMediaQueries = { md: '(min-width: 768px)', lg: '(min-width: 1024px)', xl: '(min-width: 1280px)', - '2xl': '(min-width: 1536px)' + '2xl': '(min-width: 1536px)', + print: 'print' } as const // Generate base styles (no breakpoint = base/mobile) @@ -71,7 +72,7 @@ for (const { className, property, customProp } of RESPONSIVE_PROPS) { globalStyle(`.${bpClassName}`, { '@media': { [mediaQuery]: { - [property]: `var(${bpCustomProp})` + [property]: bp === 'print' ? `var(${bpCustomProp}) !important` : `var(${bpCustomProp})` } } }) diff --git a/packages/ui/src/system/responsive/types.ts b/packages/ui/src/system/responsive/types.ts index c64a73535..4b4912eb8 100644 --- a/packages/ui/src/system/responsive/types.ts +++ b/packages/ui/src/system/responsive/types.ts @@ -1,4 +1,4 @@ -export type Breakpoint = 'base' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' +export type Breakpoint = 'base' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'print' export interface PropDef { className: string