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