Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9e29139
fix(extractor): let a later css() part replace an earlier one's decla…
owjs3901 Oct 1, 2026
337ddeb
test(extractor): cover known style composition branches
owjs3901 Oct 1, 2026
024a24c
chore: satisfy clippy assert_is_empty lint on Rust 1.99
owjs3901 Oct 1, 2026
71585a9
test(extractor): cover false ?? composition branch
owjs3901 Oct 1, 2026
52f4518
refactor(extractor): split coalesce check so composition lines are me…
owjs3901 Oct 1, 2026
1cedf9f
fix(extractor): compose styled extensions, merge attrs and let later …
owjs3901 Oct 1, 2026
01adab9
test(extractor): restore runtime-value composition test and cover sty…
owjs3901 Oct 1, 2026
53f740b
fix(extractor): render styled as targets and compile withComponent
owjs3901 Oct 1, 2026
b4b42cf
fix(extractor): keep read, $ and theme props off styled tags and eval…
owjs3901 Oct 1, 2026
e2cfa67
test(extractor): cover shouldForwardProp forms and element prop filte…
owjs3901 Oct 1, 2026
fd1783d
fix(react,components): support React 18 peers and drop React 19-only …
owjs3901 Oct 2, 2026
a641f0b
chore: satisfy clippy assert_is_empty lint on Rust 1.99
owjs3901 Oct 2, 2026
b6c0d89
fix(components): forward refs from the UI kit form components
owjs3901 Oct 2, 2026
ce9f390
fix(extractor): forward refs from generated styled components
owjs3901 Oct 2, 2026
dc2746b
Merge remote-tracking branch 'origin/fix/ui-kit-forward-ref' into tes…
owjs3901 Oct 3, 2026
6f85301
Merge remote-tracking branch 'origin/fix/styled-forward-ref' into tes…
owjs3901 Oct 3, 2026
d6453d8
fix(components): provide Select and Stepper context as React 18 does
owjs3901 Oct 3, 2026
ac3fda7
Merge branch 'fix/react-18-support' into test/react-18-fixture
owjs3901 Oct 3, 2026
b566e51
test: build and drive a React 18 app in CI
owjs3901 Oct 3, 2026
5644a8a
chore: merge main into React 18 fixture CI
owjs3901 Oct 6, 2026
af1177d
merge: update PR #706 with main ESLint checks
owjs3901 Oct 8, 2026
944e723
merge: update PR #706 with 9a953607
owjs3901 Oct 8, 2026
5a70d2a
merge: update PR #706 with db631c6c
owjs3901 Oct 8, 2026
19437d1
Merge main b711ed1f into fix/react-18-support
owjs3901 Oct 8, 2026
b878357
Merge main 9a953607 into fix/react-18-support
owjs3901 Oct 8, 2026
2b5cc38
Merge main db631c6c into fix/react-18-support
owjs3901 Oct 8, 2026
820e7c1
Merge main 11790bef into fix/react-18-support
owjs3901 Oct 8, 2026
141596c
Merge main b711ed1f into fix/ui-kit-forward-ref
owjs3901 Oct 8, 2026
65dd129
Merge main 9a953607 into fix/ui-kit-forward-ref
owjs3901 Oct 8, 2026
cf33ef6
Merge main db631c6c into fix/ui-kit-forward-ref
owjs3901 Oct 8, 2026
583b735
Merge main 11790bef into fix/ui-kit-forward-ref
owjs3901 Oct 8, 2026
30247ba
merge: update PR #706 with main
owjs3901 Oct 8, 2026
c7ea90a
merge: update PR #707 with main
owjs3901 Oct 8, 2026
4669b17
merge: update PR #710 with PR #707
owjs3901 Oct 8, 2026
9e4d28a
merge: update PR #711 with PR #710
owjs3901 Oct 8, 2026
ae283a7
merge: update PR #722 with PR #711
owjs3901 Oct 8, 2026
037143a
merge: update PR #731 with PR #722
owjs3901 Oct 8, 2026
2f08483
merge: update PR #731 with PR #720
owjs3901 Oct 8, 2026
49f8fc8
merge: update PR #731 with PR #721
owjs3901 Oct 8, 2026
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
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_compose_known_styles.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "css(a, b) composing classes whose styles the build knows (a const bound to css() in the file, or a css() result another module exports with a rule object every value of which is known) merges their styles: a later part's declaration replaces an earlier one's for the same property, selector, breakpoint and layer, also under conditions (css(base, cond && danger)) and for ||/?? parts, where the classes used to be joined and the stylesheet order picked the winner. vanilla-extract style([a, b]) passes each composed style as its own argument, so the later one wins, and a style composed again later (style([a, b, a])) is kept. Classes the build does not know (strings, props, CSS Modules) are kept as they are",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_compose_styled.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "styled(Base) extending a styled component the file binds to a const renders Base's tag directly with Base's styles composed under its own, so the extension's declarations replace Base's for the same property, selector, breakpoint and layer instead of losing to them by stylesheet order; Base's attrs apply before the extension's. attrs merge as styled-components merges them: className is joined with the caller's, style is merged and other props replace earlier ones. On an element, a className or style spread after the explicit prop now wins, as React merges props in the order they are written",
"date": "2026-10-01T00:00:00.000Z"
}
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_react_18_support.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"packages/react/package.json": "Patch",
"packages/components/package.json": "Patch"
},
"note": "@devup-ui/react and @devup-ui/components take React as a peer of ^18 || ^19 instead of `*` and no longer depend on React 19 themselves, so a React 18 app no longer gets a second React. Select and Stepper read their context with useContext instead of React 19's use() and provide it with Context.Provider instead of React 19's <Context value> (which crashes React 18), and Select repositions its container from an effect instead of a callback ref returning cleanup, which React 18 ignores",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_styled_forward_ref.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Styled components the build generates are wrapped in React's forwardRef, so a ref passed to them reaches the element they render on React 18 as well as React 19; the file imports forwardRef from react when it defines one",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_styled_props.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Styled components no longer pass every prop to the tag they render: `$` props, `theme` and the props their style functions or attrs read are kept away from a tag unless it takes them as attributes, and `shouldForwardProp` (Emotion options or styled-components withConfig) is evaluated at build time. A `shouldForwardProp` the build cannot evaluate is a build error naming the file, line and code, with the forms it accepts. An element using a styled component the file defines drops props the component neither reads nor passes on; spread props are passed as written",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_styled_targets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Styled components render what their `as` prop names, defaulting to the tag or component they were defined with, and pass `forwardedAs` on as `as`, as styled-components and Emotion do; `as` used to reach the rendered tag as an attribute. `Component.withComponent(target)` on a styled component the file binds to a const builds a component rendering the same styles and attrs as `target` (a tag name or a component JSX can name), where it used to call a method the generated function does not have and throw",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_ui_kit_forward_ref.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"packages/components/package.json": "Patch"
},
"note": "Button, Checkbox, Input, Radio and Textarea forward their ref to the button, input or textarea they render, so a ref reaches the form element on React 18 as well as React 19",
"date": "2026-10-01T00:00:00.000Z"
}
55 changes: 55 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,61 @@ jobs:
path: playwright-report/
retention-days: 1

# The packages take React ^18 || ^19 as a peer. This builds a React 18.3 app
# with the Vite plugin and the UI kit, then checks refs, context and events
# in Chromium, so React-19-only APIs cannot slip back in.
react-18:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7

- uses: actions-rust-lang/setup-rust-toolchain@v2

- name: Cache cargo registry + target
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: cargo-react-18-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.4.0"
name: Install bun

- name: Cache bun dependencies
uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-react-18-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- uses: jetli/wasm-pack-action@v0.4.0
with:
version: "latest"
- name: Install Node.js
uses: actions/setup-node@v7
with:
node-version: 24
- run: bun install
- run: bun run build
- name: Type-check and build the React 18 app
working-directory: apps/react-18
run: |
bun ../../node_modules/@typescript/native/bin/tsc
bun run build
- name: Cache Playwright Browsers
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-react-18-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- name: Install Playwright Browsers
run: bunx playwright install chromium --with-deps
- name: Run React 18 E2E Tests
working-directory: apps/react-18
run: bun run test:e2e

# @devup-ui/vite-plugin must keep the CSS every server page needs when the
# build is vinext/RSC. The fixture shares one Devup UI header between server
# list pages and a client page, and the tests read the styles the browser
Expand Down
24 changes: 24 additions & 0 deletions apps/react-18/e2e/react-18.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect, test } from '@playwright/test'

test('Devup UI runs on React 18', async ({ page }) => {
const errors: string[] = []
page.on('pageerror', (error) => errors.push(error.message))
page.on('console', (message) => {
if (message.type() === 'error') errors.push(message.text())
})
await page.goto('/')

await expect(page.getByTestId('version')).toHaveText(/^18\./)
await expect(page.getByTestId('refs')).toHaveText(
'button,input,input,input,textarea,input',
)

await page.getByText('select', { exact: true }).click()
await page.getByText('second', { exact: true }).click()
await expect(page.getByTestId('selected')).toHaveText('second')

await page.getByTestId('increase').click()
await expect(page.getByTestId('stepper')).toHaveValue('1')

expect(errors).toEqual([])
})
12 changes: 12 additions & 0 deletions apps/react-18/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Devup UI on React 18</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
28 changes: 28 additions & 0 deletions apps/react-18/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "react-18-example",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview --port 3098 --strictPort",
"test:e2e": "bunx playwright test -c playwright.config.ts",
"lint": "bun ../../node_modules/@typescript/native/bin/tsc && eslint"
},
"dependencies": {
"@devup-ui/components": "workspace:^",
"@devup-ui/react": "workspace:^",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@devup-ui/vite-plugin": "workspace:^",
"@playwright/test": "^1.63.0",
"@types/react": "^18.3",
"@types/react-dom": "^18.3",
"@vitejs/plugin-react": "^6.1",
"typescript": "^7.0.2",
"vite": "^8.3"
}
}
18 changes: 18 additions & 0 deletions apps/react-18/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
testDir: './e2e',
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: 'list',
use: {
baseURL: 'http://localhost:3098',
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
webServer: {
command: 'bun run preview',
url: 'http://localhost:3098',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
})
64 changes: 64 additions & 0 deletions apps/react-18/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
Button,
Checkbox,
Input,
Radio,
Select,
SelectContainer,
SelectOption,
SelectTrigger,
Stepper,
StepperDecreaseButton,
StepperIncreaseButton,
StepperInput,
Textarea,
} from '@devup-ui/components'
import { Box, styled } from '@devup-ui/react'
import { useEffect, useRef, useState, version } from 'react'

const Field = styled('input', { color: 'red', p: 1 })

export function App() {
const buttonRef = useRef<HTMLButtonElement>(null)
const checkboxRef = useRef<HTMLInputElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
const radioRef = useRef<HTMLInputElement>(null)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const fieldRef = useRef<HTMLInputElement>(null)
const [refs, setRefs] = useState('')
const [selected, setSelected] = useState('')

useEffect(() => {
setRefs(
[buttonRef, checkboxRef, inputRef, radioRef, textareaRef, fieldRef]
.map((ref) => ref.current?.tagName.toLowerCase() ?? 'null')
.join(','),
)
}, [])

return (
<Box p={4}>
<output data-testid="version">{version}</output>
<Button ref={buttonRef}>button</Button>
<Checkbox ref={checkboxRef}>checkbox</Checkbox>
<Input ref={inputRef} />
<Radio ref={radioRef}>radio</Radio>
<Textarea ref={textareaRef} />
<Field ref={fieldRef} />
<output data-testid="refs">{refs}</output>
<Select onChange={(value) => setSelected(String(value))}>
<SelectTrigger>select</SelectTrigger>
<SelectContainer>
<SelectOption value="first">first</SelectOption>
<SelectOption value="second">second</SelectOption>
</SelectContainer>
</Select>
<output data-testid="selected">{selected}</output>
<Stepper>
<StepperDecreaseButton />
<StepperInput data-testid="stepper" />
<StepperIncreaseButton data-testid="increase" />
</Stepper>
</Box>
)
}
10 changes: 10 additions & 0 deletions apps/react-18/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'

import { App } from './App'

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
23 changes: 23 additions & 0 deletions apps/react-18/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"paths": {
"react": ["./node_modules/@types/react"],
"react/*": ["./node_modules/@types/react/*"],
"react-dom": ["./node_modules/@types/react-dom"],
"react-dom/*": ["./node_modules/@types/react-dom/*"]
}
},
"include": ["**/*.ts", "**/*.tsx", "df/*.d.ts"],
"exclude": ["node_modules", "dist"]
}
16 changes: 16 additions & 0 deletions apps/react-18/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DevupUI } from '@devup-ui/vite-plugin'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
// The workspace packages keep React 19 for their own tests; a consumer
// install resolves their React peer to the app's single copy, as this does
resolve: { dedupe: ['react', 'react-dom'] },
plugins: [
react(),
DevupUI({
include: ['@devup-ui/components'],
singleCss: true,
}),
],
})
Loading
Loading