Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_at_rule_composition.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/react/package.json": "Patch"
},
"note": "Fix at-rule composition and cascade: `_print`/`_media`/`@media …` nest correctly with selectors in both directions and inside `globalCss`, nested media queries merge (`print and (…)`), breakpoints put media types first, distribute query lists, and nest `not …` queries, and at-rules are emitted after all breakpoint values while pseudo selectors keep `SELECTOR_ORDER` across breakpoints. Add `_motionReduce`, `_motionSafe`, `_portrait`, `_landscape`, `_contrastMore`, `_contrastLess` and `_forcedColors`, remove the never-matching `_speech`, normalize query whitespace, support Emotion-style `'@media …'`/`'&:hover'` object keys, fix template literals losing declarations before `@media` or selectors around nested at-rules, apply `typography` conditionally under selectors, and drop stale global at-rules on re-extraction",
"date": "2026-09-26T00:00:00.000Z"
}
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_css_output_fixes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/eslint-plugin/package.json": "Patch"
},
"note": "Fix the full WASM build panicking on every vanilla-extract `.css.ts` file (Boa read the clock without its `js` feature), resolve vanilla-extract references to other styles, keyframes, fonts and themes in selectors, values and `globalStyle`, keep `globalCss` pseudo selectors in `SELECTOR_ORDER` across breakpoints, merge adjacent rules with the same selector, and let declarations written next to a conditional `typography` preset win over it. Add the `prefer-media-shorthand` and `no-typography-token-prefix` ESLint rules",
"date": "2026-09-27T00:00:00.000Z"
}
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ const example = (
```tsx
// Type-safe theme tokens
const textExample = <Text color="$primary" />
const boxExample = <Box typography="$heading" />
const boxExample = <Box typography="heading" />
```

**Custom shorthands:**
Expand Down
2 changes: 1 addition & 1 deletion README_ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ const generated = (
```tsx
// 타입 세이프 테마 토큰
const textExample = <Text color="$primary" />
const boxExample = <Box typography="$heading" />
const boxExample = <Box typography="heading" />
```

**사용자 정의 shorthand:**
Expand Down
17 changes: 16 additions & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,23 @@ Available: `_groupHover`, `_groupFocus`, `_groupActive`, `_groupDisabled`.

// @ prefix syntax (equivalent)
<Box {...{ "@media": { "(min-width: 768px)": { w: "50%" } } }} />

// Query in the key (Emotion style), also inside `selectors` and css()/styled()
<Box selectors={{ "@media print": { display: "none" } }} />
```

Media shorthands wrap styles in a fixed query and nest in either direction with selectors:
`_print`, `_screen`, `_all`, `_portrait`, `_landscape`, `_motionReduce` (`prefers-reduced-motion: reduce`), `_motionSafe` (`no-preference`), `_contrastMore`, `_contrastLess`, `_forcedColors`.

```tsx
<Box transition={["opacity .2s", null, "all .3s"]} _motionReduce={{ transition: "none" }} />
<Box _motionSafe={{ _hover: { transform: "scale(1.05)" } }} />
globalCss({ _motionReduce: { "*, *::before, *::after": { transition: "none" } } })
```

- Conditions beat breakpoint values: at-rule styles are emitted after every responsive rule.
- Nested `@media` rules merge into one query (`print and (prefers-reduced-motion:reduce)`); ones that can never match together (`_print` inside `_screen`) are dropped.

### Custom Selectors

```tsx
Expand Down Expand Up @@ -365,7 +380,7 @@ const spin = keyframes({ from: { transform: "rotate(0)" }, to: { transform: "rot
```

- **Colors**: Use with `$` prefix in JSX props: `<Box color="$primary" />`
- **Typography**: Use with `$` prefix: `<Text typography="$heading" />`
- **Typography**: Use the preset name without `$`: `<Text typography="heading" />`. Under selectors or at-rules (`_hover={{ typography: "heading" }}`) the preset applies only under that condition.
- **Length**: Responsive length tokens: `<Box px="$containerX" />`, `<Flex gap="$gutter" />`
- **Shadow**: Responsive shadow tokens: `<Box boxShadow="$card" />`
- **extends**: Inherit from base config files (deep merge, last wins)
Expand Down
126 changes: 126 additions & 0 deletions apps/landing/src/app/(detail)/docs/api/selector/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,129 @@ The attributes defined in Pseudo-Class correspond to those defined in Selector,
</tr>
</tbody>
</table>

## Media Shorthands

These props wrap their styles in a fixed media query. They accept responsive arrays and nest in either direction with selectors and other at-rules.

<br />

<table>
<thead>
<tr>
<th width="300px">Selector</th>
<th width="300px">Media Query</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>_print</code>
</td>
<td>
<code>print</code>
</td>
</tr>
<tr>
<td>
<code>_screen</code>
</td>
<td>
<code>screen</code>
</td>
</tr>
<tr>
<td>
<code>_all</code>
</td>
<td>
<code>all</code>
</td>
</tr>
<tr>
<td>
<code>_portrait</code>
</td>
<td>
<code>(orientation:portrait)</code>
</td>
</tr>
<tr>
<td>
<code>_landscape</code>
</td>
<td>
<code>(orientation:landscape)</code>
</td>
</tr>
<tr>
<td>
<code>_motionReduce</code>
</td>
<td>
<code>(prefers-reduced-motion:reduce)</code>
</td>
</tr>
<tr>
<td>
<code>_motionSafe</code>
</td>
<td>
<code>(prefers-reduced-motion:no-preference)</code>
</td>
</tr>
<tr>
<td>
<code>_contrastMore</code>
</td>
<td>
<code>(prefers-contrast:more)</code>
</td>
</tr>
<tr>
<td>
<code>_contrastLess</code>
</td>
<td>
<code>(prefers-contrast:less)</code>
</td>
</tr>
<tr>
<td>
<code>_forcedColors</code>
</td>
<td>
<code>(forced-colors:active)</code>
</td>
</tr>
</tbody>
</table>

```tsx
const example = (
<Box
_motionReduce={{ transition: 'none' }}
_motionSafe={{ _hover: { transform: 'scale(1.05)' } }}
transition={['opacity .2s', null, 'all .3s']}
/>
)
```

## At-Rules

`_media`, `_supports` and `_container` take a record of queries to styles. A query can also sit in the key itself, as in Emotion, and `globalCss` accepts at-rules around whole selector maps.

```tsx
const example = (
<Box
_media={{ '(min-width: 500px)': { p: 2 } }}
selectors={{ '@media print': { display: 'none' } }}
/>
)

globalCss({
_motionReduce: { '*, *::before, *::after': { transition: 'none' } },
})
```

Conditions always win over breakpoint values: at-rule styles come after every responsive rule. Among themselves they are ordered by condition, with width queries mobile-first (`min-width` ascending, then `max-width` descending), then media types, other features, and user preferences such as `prefers-*` last.
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ Or use responsive typography from `devup.json`:
```

```tsx
<Text typography="$h1">Responsive Heading</Text>
<Text typography="h1">Responsive Heading</Text>
```

## Responsive with Pseudo Selectors
Expand Down
4 changes: 2 additions & 2 deletions apps/landing/src/app/(detail)/docs/devup/devup-json/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,8 @@ See [Shadow](/docs/devup/shadow) for more details.
### Typography

```tsx
const headingExample = <Text typography="$h1">Heading</Text>
const bodyExample = <Text typography="$body">Body text</Text>
const headingExample = <Text typography="h1">Heading</Text>
const bodyExample = <Text typography="body">Body text</Text>
```

### Length
Expand Down
24 changes: 12 additions & 12 deletions apps/landing/src/app/(detail)/docs/devup/typography/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ Define typography styles in `devup.json`:
Use typography with the `typography` prop:

```tsx
const h1Example = <Text typography="$h1">Heading 1</Text>
const h2Example = <Text typography="$h2">Heading 2</Text>
const bodyExample = <Text typography="$body">Body text</Text>
const captionExample = <Text typography="$caption">Caption text</Text>
const h1Example = <Text typography="h1">Heading 1</Text>
const h2Example = <Text typography="h2">Heading 2</Text>
const bodyExample = <Text typography="body">Body text</Text>
const captionExample = <Text typography="caption">Caption text</Text>
```

## Typography Properties
Expand Down Expand Up @@ -298,17 +298,17 @@ Here's a recommended typography scale:

```tsx
// With Text component
const titleExample = <Text typography="$h1">Main Title</Text>
const bodyExample = <Text typography="$body">Paragraph text goes here.</Text>
const titleExample = <Text typography="h1">Main Title</Text>
const bodyExample = <Text typography="body">Paragraph text goes here.</Text>

// With Box component
const boxExample = <Box typography="$caption">Caption in a box</Box>
const boxExample = <Box typography="caption">Caption in a box</Box>
```

```tsx
// With styled components
const Title = styled('h1', {
typography: '$h1',
typography: 'h1',
color: '$text',
})
```
Expand All @@ -319,7 +319,7 @@ Typography can be combined with other style props:

```tsx
const example = (
<Text color="$primary" mb={4} textAlign="center" typography="$h1">
<Text color="$primary" mb={4} textAlign="center" typography="h1">
Centered Title
</Text>
)
Expand All @@ -330,9 +330,9 @@ const example = (
Typography tokens are fully type-safe:

```tsx
// TypeScript autocomplete shows: $h1, $h2, $body, etc.
const example = <Text typography="$" />
// TypeScript autocomplete shows: h1, h2, body, etc.
const example = <Text typography="" />

// Invalid tokens show TypeScript errors
const invalidExample = <Text typography="$invalid" /> // Error!
const invalidExample = <Text typography="invalid" /> // Error!
```
2 changes: 1 addition & 1 deletion apps/landing/src/app/(detail)/docs/features/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ Define themes in `devup.json` with full TypeScript support.
```tsx
// Type-safe theme tokens with autocomplete
const textExample = <Text color="$primary" />
const boxExample = <Box typography="$heading" />
const boxExample = <Box typography="heading" />
```

Theme switching happens via CSS variables — zero JavaScript execution required.
Expand Down
10 changes: 9 additions & 1 deletion bindings/devup-ui-wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ crate-type = ["cdylib"]

[features]
default = ["vanilla-extract"]
vanilla-extract = ["extractor/vanilla-extract", "sheet/vanilla-extract"]
vanilla-extract = [
"extractor/vanilla-extract",
"sheet/vanilla-extract",
"dep:boa_engine",
]

[dependencies]
wasm-bindgen = "0.2.128"
Expand All @@ -39,6 +43,10 @@ serde-wasm-bindgen = "0.6.5"
# `getrandom_backend="wasm_js"` cfg requires on wasm32 targets.
# Keep this major version in sync with boa_engine's transitive getrandom.
getrandom = { version = "0.4", features = ["wasm_js"] }
# Not used directly. The extractor evaluates `.css.ts` modules with Boa, whose
# `Context::default()` reads the clock. Without the `js` feature that clock is
# `std::time::Instant`, which panics on wasm32-unknown-unknown.
boa_engine = { version = "0.22", optional = true, features = ["js"] }

[dev-dependencies]
wasm-bindgen-test = "0.3.78"
Expand Down
13 changes: 13 additions & 0 deletions bindings/devup-ui-wasm/tests/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,16 @@ fn test_object_to_typography() {
assert_eq!(typography.line_height.unwrap(), "1.5");
assert_eq!(typography.letter_spacing.unwrap(), "1px");
}

#[allow(dead_code)]
#[wasm_bindgen_test]
fn test_extract_vanilla_extract_file() {
let output = extractor::extract(
"styles.css.ts",
"import { style } from '@devup-ui/react'\nexport const box = style({ color: 'red' })\n",
extractor::ExtractOption::default(),
)
.unwrap();
assert_eq!(output.styles.len(), 1);
assert!(!output.code.contains("style("));
}
Loading
Loading