Skip to content

Commit faf23c5

Browse files
chore: merge updated Tailwind class fixes into v4 theme
Merge #705 head 031e8b5, which contains main a935315. Resolve libs/css/src/theme_tokens.rs by retaining the color/shadow token test and the Rust 1.99 typography assertion together, byte-for-byte matching #749; preserve all main integration and published versions. Refs #687 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2 parents b4790ad + 031e8b5 commit faf23c5

33 files changed

Lines changed: 840 additions & 158 deletions

File tree

‎.changepacks/changepack_log_global_css_fixes.json‎

Lines changed: 0 additions & 7 deletions
This file was deleted.

‎.changepacks/changepack_log_lint_style_positions.json‎

Lines changed: 0 additions & 7 deletions
This file was deleted.

‎.changepacks/changepack_log_theme_output.json‎

Lines changed: 0 additions & 8 deletions
This file was deleted.

‎.github/workflows/publish.yml‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,65 @@ jobs:
138138
path: playwright-report/
139139
retention-days: 1
140140

141+
# @devup-ui/vite-plugin must keep the CSS every server page needs when the
142+
# build is vinext/RSC. The fixture shares one Devup UI header between server
143+
# list pages and a client page, and the tests read the styles the browser
144+
# actually applies when each route is entered directly, so a dropped
145+
# stylesheet dependency fails here whatever the CSS files are called.
146+
vinext-rsc-css-e2e:
147+
runs-on: ubuntu-latest
148+
timeout-minutes: 30
149+
steps:
150+
- name: Checkout
151+
uses: actions/checkout@v7
152+
153+
- uses: actions-rust-lang/setup-rust-toolchain@v2
154+
155+
- name: Cache cargo registry + target
156+
uses: actions/cache@v6
157+
with:
158+
path: |
159+
~/.cargo/registry/index/
160+
~/.cargo/registry/cache/
161+
~/.cargo/git/db/
162+
target/
163+
key: cargo-vinext-rsc-css-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
164+
- uses: oven-sh/setup-bun@v2
165+
with:
166+
bun-version: "1.4.0"
167+
name: Install bun
168+
169+
- name: Cache bun dependencies
170+
uses: actions/cache@v6
171+
with:
172+
path: ~/.bun/install/cache
173+
key: bun-vinext-rsc-css-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
174+
- uses: jetli/wasm-pack-action@v0.4.0
175+
with:
176+
version: "latest"
177+
- name: Install Node.js
178+
uses: actions/setup-node@v7
179+
with:
180+
node-version: 24
181+
- run: bun install
182+
- run: bun run build
183+
- name: Cache Playwright Browsers
184+
uses: actions/cache@v6
185+
with:
186+
path: ~/.cache/ms-playwright
187+
key: playwright-vinext-rsc-css-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
188+
- name: Install Playwright Browsers
189+
run: bunx playwright install chromium --with-deps
190+
- name: Build fixture (per-file CSS)
191+
run: bun run --filter vinext-rsc-css-repro build
192+
- name: Run E2E Tests (per-file CSS)
193+
run: bun run test:e2e:rsc-css
194+
- name: Build fixture (singleCss)
195+
run: |
196+
rm -rf apps/vinext-rsc-css-repro/dist apps/vinext-rsc-css-repro/.vinext apps/vinext-rsc-css-repro/df
197+
DEVUP_SINGLE_CSS=1 bun run --filter vinext-rsc-css-repro build
198+
- name: Run E2E Tests (singleCss)
199+
run: bun run test:e2e:rsc-css
141200
publish:
142201
runs-on: ubuntu-latest
143202
permissions:
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/dist/
2+
/.vinext/
3+
/.next/
4+
/df/
5+
/node_modules/
6+
next-env.d.ts
7+
*.tsbuildinfo
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "theme": {} }
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { expect, type Page, test } from '@playwright/test'
2+
3+
const ROUTES = ['/notice/', '/admins/', '/users/', '/users/edit/'] as const
4+
const EDIT_ROUTE = '/users/edit/'
5+
6+
function permutations<T>(items: readonly T[]): T[][] {
7+
if (items.length <= 1) return [[...items]]
8+
return items.flatMap((item, index) =>
9+
permutations([...items.slice(0, index), ...items.slice(index + 1)]).map(
10+
(rest) => [item, ...rest],
11+
),
12+
)
13+
}
14+
15+
/**
16+
* The users list links to the edit page and so prefetches it, which also loads
17+
* the CSS its client bundle references. Refuse every request to the edit page
18+
* except its own document, so a page can only get the header CSS from what the
19+
* server rendered for it.
20+
*/
21+
async function blockEditPrefetch(page: Page) {
22+
await page.route('**/users/edit/**', (route) => {
23+
if (route.request().resourceType() === 'document') return route.continue()
24+
return route.abort()
25+
})
26+
}
27+
28+
async function expectHeaderStyled(page: Page, route: string) {
29+
const header = page.locator('[data-page-title]')
30+
await expect(header, route).toHaveCount(1)
31+
32+
const applied = await header.evaluate((element) => {
33+
const read = (target: Element) => {
34+
const style = getComputedStyle(target)
35+
return {
36+
display: style.display,
37+
justifyContent: style.justifyContent,
38+
alignItems: style.alignItems,
39+
marginBottom: style.marginBottom,
40+
columnGap: style.columnGap,
41+
rowGap: style.rowGap,
42+
}
43+
}
44+
const group = element.firstElementChild!
45+
return { header: read(element), group: read(group) }
46+
})
47+
expect(applied.header, `${route} header`).toMatchObject({
48+
display: 'flex',
49+
justifyContent: 'space-between',
50+
alignItems: 'center',
51+
marginBottom: '20px',
52+
})
53+
expect(applied.group, `${route} title group`).toMatchObject({
54+
display: 'flex',
55+
alignItems: 'center',
56+
columnGap: '12px',
57+
rowGap: '12px',
58+
})
59+
60+
const layout = await header.evaluate((element) => {
61+
const bounds = element.getBoundingClientRect()
62+
const group = element.firstElementChild!.getBoundingClientRect()
63+
const right = element.lastElementChild!.getBoundingClientRect()
64+
return { bounds, group, right, hasRight: element.childElementCount > 1 }
65+
})
66+
expect(layout.group.left, `${route} title group is at the start`).toBe(
67+
layout.bounds.left,
68+
)
69+
if (layout.hasRight) {
70+
expect(layout.right.right, `${route} right slot is at the end`).toBe(
71+
layout.bounds.right,
72+
)
73+
}
74+
}
75+
76+
/**
77+
* Every class the header renders must have a rule in a stylesheet the page
78+
* really loaded (a `<link>` the browser fetched), whatever the files are
79+
* called.
80+
*/
81+
async function expectHeaderCssLoaded(page: Page, route: string) {
82+
const result = await page.locator('[data-page-title]').evaluate((element) => {
83+
const classes = [
84+
...element.classList,
85+
...element.firstElementChild!.classList,
86+
]
87+
const linked = [...document.styleSheets].filter(
88+
(sheet) => sheet.href !== null,
89+
)
90+
const collect = (rules: CSSRuleList): string[] =>
91+
[...rules].flatMap((rule) =>
92+
rule instanceof CSSStyleRule
93+
? [rule.selectorText]
94+
: 'cssRules' in rule
95+
? collect(rule.cssRules as CSSRuleList)
96+
: [],
97+
)
98+
const selectors = linked.flatMap((sheet) => collect(sheet.cssRules))
99+
return {
100+
linkedSheets: linked.length,
101+
missing: classes.filter(
102+
(name) =>
103+
!selectors.some((selector) =>
104+
selector.includes(`.${CSS.escape(name)}`),
105+
),
106+
),
107+
classes: classes.length,
108+
}
109+
})
110+
expect(result.classes, `${route} header has atomic classes`).toBeGreaterThan(
111+
0,
112+
)
113+
expect(result.linkedSheets, `${route} loaded stylesheets`).toBeGreaterThan(0)
114+
expect(result.missing, `${route} classes without a loaded rule`).toEqual([])
115+
}
116+
117+
async function expectRoute(page: Page, route: string) {
118+
await expectHeaderCssLoaded(page, route)
119+
await expectHeaderStyled(page, route)
120+
}
121+
122+
test.describe('shared Devup UI header in the static vinext build', () => {
123+
for (const route of ROUTES) {
124+
test(`direct entry and reload: ${route}`, async ({ browser }) => {
125+
const context = await browser.newContext()
126+
const page = await context.newPage()
127+
await blockEditPrefetch(page)
128+
129+
await page.goto(route)
130+
await expectRoute(page, route)
131+
132+
await page.reload()
133+
await expectRoute(page, route)
134+
await context.close()
135+
})
136+
}
137+
138+
for (const order of permutations(ROUTES)) {
139+
test(`entered in order ${order.join(' -> ')}`, async ({ browser }) => {
140+
const context = await browser.newContext()
141+
const page = await context.newPage()
142+
await blockEditPrefetch(page)
143+
144+
for (const route of order) {
145+
await page.goto(route)
146+
await expectRoute(page, route)
147+
}
148+
await context.close()
149+
})
150+
}
151+
152+
test('the edit page itself renders the same header', async ({ page }) => {
153+
await page.goto(EDIT_ROUTE)
154+
await expect(page.getByRole('textbox', { name: 'name' })).toBeVisible()
155+
await expectRoute(page, EDIT_ROUTE)
156+
})
157+
})
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default {
2+
output: 'export',
3+
trailingSlash: true,
4+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "vinext-rsc-css-repro",
3+
"version": "0.0.0",
4+
"type": "module",
5+
"private": true,
6+
"scripts": {
7+
"build": "vinext build",
8+
"lint": "eslint"
9+
},
10+
"dependencies": {
11+
"@devup-ui/react": "workspace:^",
12+
"react": "^19.3",
13+
"react-dom": "^19.3",
14+
"react-server-dom-webpack": "^19.3",
15+
"vinext": "1.0.0"
16+
},
17+
"devDependencies": {
18+
"@devup-ui/vite-plugin": "workspace:^",
19+
"@types/react": "^19",
20+
"@types/react-dom": "^19",
21+
"@vitejs/plugin-react": "^6.1",
22+
"@vitejs/plugin-rsc": "0.5.35",
23+
"typescript": "^7.0.2",
24+
"vite": "8.3.1"
25+
}
26+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { PageTitle } from '../../components/PageTitle'
2+
3+
export default function AdminsPage() {
4+
return (
5+
<main>
6+
<PageTitle right={<button type="button">Create</button>}>
7+
Admins
8+
</PageTitle>
9+
</main>
10+
)
11+
}

0 commit comments

Comments
 (0)