From e38a0f415ea7ab15ac66a41cccf09c5153574729 Mon Sep 17 00:00:00 2001 From: KCM Date: Wed, 10 Dec 2025 22:06:44 -0600 Subject: [PATCH 1/2] feat: better ve support. --- README.md | 26 ++++ package-lock.json | 21 ++- package.json | 5 +- packages/css/package.json | 2 +- packages/css/src/css.ts | 16 ++- packages/css/src/loader.ts | 132 +++++++++++++++--- packages/css/test/loader_unit.test.ts | 25 +++- packages/playwright/package.json | 4 +- packages/playwright/playwright.config.ts | 2 +- packages/playwright/rspack.config.js | 3 +- packages/playwright/src/dialects/registry.ts | 51 +++++++ .../playwright/src/dialects/vanilla.css.ts | 90 +++++++++--- packages/playwright/src/dialects/vanilla.ts | 14 +- packages/playwright/src/index.ts | 59 +------- packages/playwright/src/lit-react/app.css | 38 +++++ .../src/lit-react/button-wrapper.ts | 19 ++- packages/playwright/src/lit-react/index.ts | 11 +- .../playwright/src/lit-react/showcase.tsx | 29 ++++ packages/playwright/test/lit-react.spec.ts | 39 ++++++ packages/playwright/test/styles.spec.ts | 17 +++ packages/playwright/tsconfig.json | 2 +- packages/types/loader-queries.d.ts | 8 +- packages/types/package.json | 4 +- 23 files changed, 484 insertions(+), 133 deletions(-) create mode 100644 packages/playwright/src/dialects/registry.ts create mode 100644 packages/playwright/src/lit-react/app.css create mode 100644 packages/playwright/src/lit-react/showcase.tsx diff --git a/README.md b/README.md index ed3cb6e..bd01ae7 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,32 @@ const { Button, knightedCss } = combined as KnightedCssCombinedModule< You can mix and match: regular `?knighted-css` imports keep strong module typings and just add the CSS string, while `?knighted-css&combined` dedupes your CSS loader pipeline when you need everything at once. +### vanilla-extract loader guidance + +vanilla-extract files (`*.css.ts`) compile down to CommonJS by default. That works out of the box for the loader—both `?knighted-css` and `?knighted-css&combined` queries emit `module.exports` artifacts plus the injected `knightedCss` string. Most bundlers happily consume that shape. When you _also_ need the compiled module to behave like a native ESM module (e.g., your bundler expects `export` statements so it can treeshake or when you import via extension aliases), enable the loader’s opt-in transform: + +```js +// rspack.config.js (excerpt) +{ + test: /\.css\.ts$/, + use: [ + { + loader: '@knighted/css/loader', + options: { + lightningcss: { minify: true }, + vanilla: { transformToEsm: true }, + }, + }, + // swc/esbuild/etc. + ], +} +``` + +The `vanilla.transformToEsm` flag runs a small post-pass that strips the CJS boilerplate emitted by `@vanilla-extract/integration` and re-exports the discovered bindings via native `export { name }` statements. That makes combined imports behave exactly like the source module, which is useful for frameworks that rely on strict ESM semantics (our Lit + React Playwright app is the canonical example in this repo). + +> [!IMPORTANT] +> Only enable `vanilla.transformToEsm` when your bundler really requires ESM output. Leaving the transform off keeps the vanilla-extract module identical to what the upstream compiler produced, which is often preferable if the rest of your toolchain expects CommonJS. The loader no longer toggles this transform automatically—combined imports stay fast, but you remain in full control of when the conversion occurs. + ### Custom resolver (enhanced-resolve example) If your project uses aliases or nonstandard resolution, plug in a custom resolver. Here’s how to use [`enhanced-resolve`](https://github.com/webpack/enhanced-resolve): diff --git a/package-lock.json b/package-lock.json index efe64bb..fc4072c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@vanilla-extract/css": "^1.15.2", "@vanilla-extract/integration": "^8.0.6", "@vanilla-extract/recipes": "^0.5.7", + "@vanilla-extract/sprinkles": "^1.6.5", "c8": "^10.1.2", "http-server": "^14.1.1", "husky": "^9.1.7", @@ -3406,6 +3407,16 @@ "@vanilla-extract/css": "^1.0.0" } }, + "node_modules/@vanilla-extract/sprinkles": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@vanilla-extract/sprinkles/-/sprinkles-1.6.5.tgz", + "integrity": "sha512-HOYidLONR/SeGk8NBAeI64I4gYdsMX9vJmniL13ZcLVwawyK0s2GUENEAcGA+GYLIoeyQB61UqmhqPodJry7zA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@vanilla-extract/css": "^1.0.0" + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.25", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz", @@ -10544,7 +10555,7 @@ }, "packages/css": { "name": "@knighted/css", - "version": "1.0.0-rc.0", + "version": "1.0.0-rc.1", "license": "MIT", "dependencies": { "dependency-tree": "^11.2.0", @@ -10571,23 +10582,23 @@ "name": "@knighted/css-playwright-fixture", "version": "0.0.0", "dependencies": { - "@knighted/css": "1.0.0-rc.0", + "@knighted/css": "1.0.0-rc.1", "@knighted/jsx": "^1.2.1", "lit": "^3.2.1", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { - "@types/knighted__css": "1.0.0-rc.0", + "@types/knighted__css": "1.0.0-rc.1", "@types/react": "^19.0.4", "@types/react-dom": "^19.0.2" } }, "packages/types": { "name": "@types/knighted__css", - "version": "1.0.0-rc.0", + "version": "1.0.0-rc.1", "dependencies": { - "@knighted/css": "1.0.0-rc.0" + "@knighted/css": "1.0.0-rc.1" } } } diff --git a/package.json b/package.json index 36e39e1..5946366 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,10 @@ "setup:jsx-wasm": "node scripts/setup-jsx-wasm.mjs" }, "devDependencies": { - "@knighted/duel": "^2.1.6", "@emnapi/core": "^1.2.0", "@emnapi/runtime": "^1.2.0", + "@knighted/duel": "^2.1.6", + "@napi-rs/wasm-runtime": "^1.0.7", "@playwright/test": "^1.48.2", "@rspack/cli": "^1.0.0", "@rspack/core": "^1.0.0", @@ -40,12 +41,12 @@ "@vanilla-extract/css": "^1.15.2", "@vanilla-extract/integration": "^8.0.6", "@vanilla-extract/recipes": "^0.5.7", + "@vanilla-extract/sprinkles": "^1.6.5", "c8": "^10.1.2", "http-server": "^14.1.1", "husky": "^9.1.7", "less": "^4.2.0", "lint-staged": "^16.2.7", - "@napi-rs/wasm-runtime": "^1.0.7", "oxlint": "^0.4.1", "prettier": "^3.7.4", "sass": "^1.80.7", diff --git a/packages/css/package.json b/packages/css/package.json index 62b4379..f447b77 100644 --- a/packages/css/package.json +++ b/packages/css/package.json @@ -1,6 +1,6 @@ { "name": "@knighted/css", - "version": "1.0.0-rc.0", + "version": "1.0.0-rc.1", "description": "A build-time utility that traverses JavaScript/TypeScript module dependency graphs to extract, compile, and optimize all imported CSS into a single, in-memory string.", "type": "module", "main": "./dist/css.js", diff --git a/packages/css/src/css.ts b/packages/css/src/css.ts index 0fd737f..38a6fe5 100644 --- a/packages/css/src/css.ts +++ b/packages/css/src/css.ts @@ -48,6 +48,11 @@ interface StyleModule { ext: string } +export interface VanillaCompileResult { + source: string + css: string +} + /** * Extract and compile all CSS-like dependencies for a given module. */ @@ -204,7 +209,7 @@ async function compileStyleModule( case '.less': return compileLess(file.path, peerResolver) case '.css.ts': - return compileVanillaExtract(file.path, cwd, peerResolver) + return (await compileVanillaModule(file.path, cwd, peerResolver)).css default: return '' } @@ -235,11 +240,11 @@ async function compileLess(filePath: string, peerResolver?: PeerLoader): Promise return result.css } -async function compileVanillaExtract( +export async function compileVanillaModule( filePath: string, cwd: string, peerResolver?: PeerLoader, -): Promise { +): Promise { const mod = await optionalPeer( '@vanilla-extract/integration', 'Vanilla Extract', @@ -290,7 +295,10 @@ async function compileVanillaExtract( } } - return imports.join('\n') + return { + source, + css: imports.join('\n'), + } } const defaultPeerLoader: PeerLoader = name => import(name) diff --git a/packages/css/src/loader.ts b/packages/css/src/loader.ts index bc4f23e..39fc0b4 100644 --- a/packages/css/src/loader.ts +++ b/packages/css/src/loader.ts @@ -4,13 +4,19 @@ import type { PitchLoaderDefinitionFunction, } from 'webpack' -import { cssWithMeta, type CssOptions } from './css.js' +import { cssWithMeta, compileVanillaModule, type CssOptions } from './css.js' export type KnightedCssCombinedModule = TModule & { knightedCss: string } -export interface KnightedCssLoaderOptions extends CssOptions {} +export interface KnightedCssVanillaOptions { + transformToEsm?: boolean +} + +export interface KnightedCssLoaderOptions extends CssOptions { + vanilla?: KnightedCssVanillaOptions +} const DEFAULT_EXPORT_NAME = 'knightedCss' const COMBINED_QUERY_FLAG = 'combined' @@ -18,12 +24,55 @@ const COMBINED_QUERY_FLAG = 'combined' const loader: LoaderDefinitionFunction = async function loader( source: string | Buffer, ) { - const cssOptions = resolveCssOptions(this) + const { cssOptions, vanillaOptions } = resolveLoaderOptions(this) const css = await extractCss(this, cssOptions) const injection = buildInjection(css) - const input = toSourceString(source) const isStyleModule = this.resourcePath.endsWith('.css.ts') - return isStyleModule ? `${injection}export default {};\n` : `${input}${injection}` + if (isStyleModule) { + const { source: compiledSource } = await compileVanillaModule( + this.resourcePath, + cssOptions.cwd ?? this.rootContext ?? process.cwd(), + cssOptions.peerResolver, + ) + const vanillaSource = maybeTransformVanillaModule(compiledSource, vanillaOptions) + return `${vanillaSource}${injection}` + } + + const input = toSourceString(source) + return `${input}${injection}` +} + +function transformVanillaModuleToEsm(source: string): string { + const exportBlock = /__export\([^,]+,\s*{([\s\S]*?)}\);/m.exec(source) + if (!exportBlock) { + return source + } + + const names = exportBlock[1] + .split(',') + .map(part => part.trim()) + .filter(Boolean) + .map(entry => entry.split(':')[0]?.trim()) + .filter(Boolean) + + let transformed = source.replace(/module\.exports\s*=\s*__toCommonJS\([^;]+;\n?/m, '') + transformed = transformed.replace(/0 && \(module\.exports = {[^}]+}\);?\n?/m, '') + + if (names.length > 0) { + transformed = `${transformed}\nexport { ${names.join(', ')} };\n` + } + + return transformed +} + +function maybeTransformVanillaModule( + source: string, + options?: KnightedCssVanillaOptions, +): string { + if (!options?.transformToEsm) { + return source + } + return transformVanillaModuleToEsm(source) } export const pitch: PitchLoaderDefinitionFunction = @@ -33,7 +82,7 @@ export const pitch: PitchLoaderDefinitionFunction = } const request = buildProxyRequest(this) - const cssOptions = resolveCssOptions(this) + const { cssOptions } = resolveLoaderOptions(this) return extractCss(this, cssOptions).then(css => createCombinedModule(request, css)) } @@ -41,13 +90,21 @@ export const pitch: PitchLoaderDefinitionFunction = export default loader -function resolveCssOptions(ctx: LoaderContext): CssOptions { +function resolveLoaderOptions(ctx: LoaderContext): { + cssOptions: CssOptions + vanillaOptions?: KnightedCssVanillaOptions +} { const rawOptions = ( typeof ctx.getOptions === 'function' ? ctx.getOptions() : {} ) as KnightedCssLoaderOptions + const { vanilla, ...rest } = rawOptions + const cssOptions: CssOptions = { + ...rest, + cwd: rest.cwd ?? ctx.rootContext ?? process.cwd(), + } return { - ...rawOptions, - cwd: rawOptions.cwd ?? ctx.rootContext ?? process.cwd(), + cssOptions, + vanillaOptions: vanilla, } } @@ -83,6 +140,11 @@ function hasCombinedQuery(query?: string | null): boolean { function buildProxyRequest(ctx: LoaderContext): string { const sanitizedQuery = buildSanitizedQuery(ctx.resourceQuery) + const rawRequest = getRawRequest(ctx) + if (rawRequest) { + const stripped = stripResourceQuery(rawRequest) + return `${stripped}${sanitizedQuery}` + } const request = `${ctx.resourcePath}${sanitizedQuery}` const context = ctx.context ?? ctx.rootContext ?? process.cwd() if (ctx.utils && typeof ctx.utils.contextify === 'function') { @@ -91,6 +153,24 @@ function buildProxyRequest(ctx: LoaderContext): string return request } +function getRawRequest(ctx: LoaderContext): string | undefined { + const mod = ( + ctx as LoaderContext & { + _module?: { rawRequest?: string } + } + )._module + const request = mod?.rawRequest + if (typeof request === 'string' && request.length > 0) { + return request + } + return undefined +} + +function stripResourceQuery(request: string): string { + const idx = request.indexOf('?') + return idx >= 0 ? request.slice(0, idx) : request +} + function buildSanitizedQuery(query?: string | null): string { if (!query) return '' const entries = splitQuery(query).filter(part => { @@ -116,15 +196,31 @@ function isQueryFlag(entry: string, flag: string): boolean { function createCombinedModule(request: string, css: string): string { const requestLiteral = JSON.stringify(request) - const defaultExport = `const __knightedDefault = -typeof __knightedModule.default !== 'undefined' - ? __knightedModule.default - : __knightedModule;` - return [ + const lines = [ `import * as __knightedModule from ${requestLiteral};`, `export * from ${requestLiteral};`, - defaultExport, - 'export default __knightedDefault;', - buildInjection(css), - ].join('\n') + ] + + if (shouldForwardDefaultExport(request)) { + lines.push( + `const __knightedDefault = +typeof __knightedModule.default !== 'undefined' + ? __knightedModule.default + : __knightedModule;`, + 'export default __knightedDefault;', + ) + } + + lines.push(buildInjection(css)) + return lines.join('\n') +} + +function shouldForwardDefaultExport(request: string): boolean { + const [pathPart] = request.split('?') + if (!pathPart) return true + const lower = pathPart.toLowerCase() + if (lower.endsWith('.css.ts') || lower.endsWith('.css.js')) { + return false + } + return true } diff --git a/packages/css/test/loader_unit.test.ts b/packages/css/test/loader_unit.test.ts index d1a8588..d74268f 100644 --- a/packages/css/test/loader_unit.test.ts +++ b/packages/css/test/loader_unit.test.ts @@ -66,11 +66,7 @@ test('loader handles style modules and buffer sources', async () => { ) assert.match(output, /export const knightedCss = /, 'should inject css variable export') - assert.match( - output, - /export default \{\};/, - 'should emit empty default export for style', - ) + assert.match(output, /module\.exports/, 'should retain vanilla cjs output by default') assert.match(output, /styles_themeClass__/, 'should compile vanilla-extract styles') assert.ok( ctx.added.has(resourcePath), @@ -78,6 +74,25 @@ test('loader handles style modules and buffer sources', async () => { ) }) +test('loader transforms vanilla modules to esm when opted in', async () => { + const resourcePath = path.resolve(__dirname, 'fixtures/dialects/vanilla/styles.css.ts') + const ctx = createMockContext({ + resourcePath, + rootContext: path.resolve(__dirname, 'fixtures'), + getOptions: () => ({ vanilla: { transformToEsm: true } }), + }) + const output = String( + await loader.call( + ctx as LoaderContext, + 'export const ignored = true', + ), + ) + + assert.match(output, /export const knightedCss = /, 'should inject css variable export') + assert.ok(!/module\.exports\s*=/.test(output), 'should remove cjs export boilerplate') + assert.match(output, /export \{ badge, themeClass, token, vars \};/) +}) + test('loader reads options from getOptions and honors cwd override', async () => { const resourcePath = path.resolve(__dirname, 'fixtures/dialects/basic/entry.js') const cwd = path.resolve(__dirname, 'fixtures') diff --git a/packages/playwright/package.json b/packages/playwright/package.json index ce6b781..2a07a39 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -12,14 +12,14 @@ "pretest": "npm run build" }, "dependencies": { - "@knighted/css": "1.0.0-rc.0", + "@knighted/css": "1.0.0-rc.1", "@knighted/jsx": "^1.2.1", "lit": "^3.2.1", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { - "@types/knighted__css": "1.0.0-rc.0", + "@types/knighted__css": "1.0.0-rc.1", "@types/react": "^19.0.4", "@types/react-dom": "^19.0.2" } diff --git a/packages/playwright/playwright.config.ts b/packages/playwright/playwright.config.ts index 4dd67dc..7279349 100644 --- a/packages/playwright/playwright.config.ts +++ b/packages/playwright/playwright.config.ts @@ -1,7 +1,6 @@ import { defineConfig, devices } from '@playwright/test' const isCI = process.env.CI === 'true' - const projects = [ { name: 'chromium', @@ -19,6 +18,7 @@ if (isCI) { export default defineConfig({ testDir: 'test', timeout: 10_000, + retries: isCI ? 2 : 0, expect: { timeout: 10_000, }, diff --git a/packages/playwright/rspack.config.js b/packages/playwright/rspack.config.js index 71a1c3e..8e40f9b 100644 --- a/packages/playwright/rspack.config.js +++ b/packages/playwright/rspack.config.js @@ -32,6 +32,7 @@ export default { loader: '@knighted/css/loader', options: { lightningcss: { minify: true }, + vanilla: { transformToEsm: true }, }, }, { @@ -75,7 +76,7 @@ export default { }, { test: /\.tsx?$/, - exclude: /node_modules/, + exclude: [/node_modules/, /\.css\.ts$/], use: [ { loader: '@knighted/jsx/loader', diff --git a/packages/playwright/src/dialects/registry.ts b/packages/playwright/src/dialects/registry.ts new file mode 100644 index 0000000..aad0f2f --- /dev/null +++ b/packages/playwright/src/dialects/registry.ts @@ -0,0 +1,51 @@ +import type { KnightedCssCombinedModule } from '@knighted/css/loader' + +import Basic, { className as basicClass } from './basic.js' +import { knightedCss as basicCss } from './basic.js?knighted-css' + +import Scss, { className as scssClass } from './scss.js' +import { knightedCss as scssCss } from './scss.js?knighted-css' + +import SassIndented, { className as sassIndentedClass } from './sass-indented.js' +import { knightedCss as sassIndentedCss } from './sass-indented.js?knighted-css' + +import LessComp, { className as lessClass } from './less.js' +import { knightedCss as lessCss } from './less.js?knighted-css' + +import * as VanillaCombined from './vanilla.js?knighted-css&combined' + +type VanillaModuleShape = KnightedCssCombinedModule + +const VanillaModule = VanillaCombined as unknown as VanillaModuleShape +const { + default: VanillaComp, + className: vanillaClass, + badgeClass, + tokenClass, + knightedCss: vanillaCss, +} = VanillaModule + +export type DialectSample = { + className: string + css: string + label: string + testId: string +} + +export const dialects: DialectSample[] = [ + { label: Basic(), className: basicClass, css: basicCss, testId: 'dialect-basic' }, + { label: Scss(), className: scssClass, css: scssCss, testId: 'dialect-scss' }, + { + label: SassIndented(), + className: sassIndentedClass, + css: sassIndentedCss, + testId: 'dialect-sass-indented', + }, + { label: LessComp(), className: lessClass, css: lessCss, testId: 'dialect-less' }, + { + label: VanillaComp(), + className: `${vanillaClass} ${badgeClass} ${tokenClass}`, + css: vanillaCss, + testId: 'dialect-vanilla', + }, +] diff --git a/packages/playwright/src/dialects/vanilla.css.ts b/packages/playwright/src/dialects/vanilla.css.ts index 737bc72..8b116c9 100644 --- a/packages/playwright/src/dialects/vanilla.css.ts +++ b/packages/playwright/src/dialects/vanilla.css.ts @@ -1,23 +1,77 @@ -import { globalStyle } from '@vanilla-extract/css' - -globalStyle('.pw-vanilla', { - display: 'inline-flex', - alignItems: 'center', - gap: '0.5rem', - padding: '0.75rem 1rem', - background: '#1e293b', - color: '#f8fafc', - borderRadius: '999px', +import { globalStyle, style, styleVariants } from '@vanilla-extract/css' +import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles' + +const layoutProperties = defineProperties({ + properties: { + display: ['inline-flex'], + alignItems: ['center'], + gap: ['0.35rem', '0.5rem', '0.75rem'], + paddingX: ['0.5rem', '0.75rem', '1rem', '1.25rem'], + paddingY: ['0.35rem', '0.5rem', '0.75rem'], + }, + shorthands: { + padding: ['paddingX', 'paddingY'], + }, }) -globalStyle('.pw-vanilla-badge', { - background: '#fbbf24', - color: '#1e293b', - padding: '0.25rem 0.75rem', - borderRadius: '999px', - fontWeight: 600, +const textProperties = defineProperties({ + properties: { + letterSpacing: ['0.04em', '0.08em', '0.12em'], + textTransform: ['uppercase'], + }, }) -globalStyle('.pw-vanilla-token', { - letterSpacing: '0.08em', +export const sprinkles = createSprinkles(layoutProperties, textProperties) + +export const vanillaCardClass = style([ + sprinkles({ + display: 'inline-flex', + alignItems: 'center', + gap: '0.75rem', + paddingX: '1.25rem', + paddingY: '0.75rem', + }), + { + background: '#1e293b', + color: '#f8fafc', + borderRadius: '999px', + fontWeight: 600, + }, +]) + +export const vanillaBadgeClass = style([ + sprinkles({ + display: 'inline-flex', + alignItems: 'center', + gap: '0.35rem', + paddingX: '0.75rem', + paddingY: '0.35rem', + textTransform: 'uppercase', + }), + { + background: '#fbbf24', + color: '#1e293b', + borderRadius: '999px', + }, +]) + +const tokenVariants = styleVariants({ + tracking: [ + sprinkles({ letterSpacing: '0.12em', textTransform: 'uppercase' }), + { + color: '#cbd5f5', + }, + ], + accent: [ + sprinkles({ letterSpacing: '0.08em', textTransform: 'uppercase' }), + { + color: '#a78bfa', + }, + ], +}) + +export const vanillaTokenClass = tokenVariants.tracking + +globalStyle(`${vanillaCardClass} strong`, { + color: '#38bdf8', }) diff --git a/packages/playwright/src/dialects/vanilla.ts b/packages/playwright/src/dialects/vanilla.ts index 20f1e02..9edfc5b 100644 --- a/packages/playwright/src/dialects/vanilla.ts +++ b/packages/playwright/src/dialects/vanilla.ts @@ -1,6 +1,14 @@ -export const className = 'pw-vanilla' -export const badgeClass = 'pw-vanilla-badge' -export const tokenClass = 'pw-vanilla-token' +import type { KnightedCssCombinedModule } from '@knighted/css/loader' +import * as VanillaStylesCombined from './vanilla.css.js?knighted-css&combined' + +type VanillaStylesModule = KnightedCssCombinedModule + +const VanillaStyles = VanillaStylesCombined as unknown as VanillaStylesModule +const { vanillaBadgeClass, vanillaCardClass, vanillaTokenClass } = VanillaStyles + +export const className = vanillaCardClass +export const badgeClass = vanillaBadgeClass +export const tokenClass = vanillaTokenClass export default function renderLabel() { return 'vanilla-extract' diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 8527c48..926d1a2 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -1,65 +1,14 @@ -import Basic, { className as basicClass } from './dialects/basic.js' - -import { knightedCss as basicCss } from './dialects/basic.js?knighted-css' - -import Scss, { className as scssClass } from './dialects/scss.js' -import { knightedCss as scssCss } from './dialects/scss.js?knighted-css' - -import SassIndented, { className as sassIndentedClass } from './dialects/sass-indented.js' -import { knightedCss as sassIndentedCss } from './dialects/sass-indented.js?knighted-css' - -import LessComp, { className as lessClass } from './dialects/less.js' -import { knightedCss as lessCss } from './dialects/less.js?knighted-css' - -import VanillaComp, { - className as vanillaClass, - badgeClass, - tokenClass, -} from './dialects/vanilla.js' -import { knightedCss as vanillaCss } from './dialects/vanilla.css.js?knighted-css' +import { dialects } from './dialects/registry.js' import { renderLitReactDemo } from './lit-react/index.js' -const dialects = [ - { label: Basic(), className: basicClass, css: basicCss, testId: 'dialect-basic' }, - { label: Scss(), className: scssClass, css: scssCss, testId: 'dialect-scss' }, - { - label: SassIndented(), - className: sassIndentedClass, - css: sassIndentedCss, - testId: 'dialect-sass-indented', - }, - { label: LessComp(), className: lessClass, css: lessCss, testId: 'dialect-less' }, - { - label: VanillaComp(), - className: `${vanillaClass} ${badgeClass} ${tokenClass}`, - css: vanillaCss, - testId: 'dialect-vanilla', - }, -] - -function injectStyles() { - const combined = dialects.map(d => d.css).join('\n') - const style = document.createElement('style') - style.textContent = combined - document.head.append(style) -} - function render() { const root = document.getElementById('app') ?? document.body - injectStyles() - for (const dialect of dialects) { - const el = document.createElement('div') - el.dataset.testid = dialect.testId - el.className = dialect.className - el.textContent = dialect.label - root.appendChild(el) - } + renderLitReactDemo(root, dialects) return root } if (typeof document !== 'undefined') { - const root = render() - renderLitReactDemo(root) + render() } -export { render } +export { dialects, render } diff --git a/packages/playwright/src/lit-react/app.css b/packages/playwright/src/lit-react/app.css new file mode 100644 index 0000000..bcfcd08 --- /dev/null +++ b/packages/playwright/src/lit-react/app.css @@ -0,0 +1,38 @@ +.readme-stage { + display: flex; + flex-direction: column; + gap: 2rem; + padding: 2rem; + background: #020617; + color: #f1f5f9; + font-family: 'IBM Plex Sans', 'Segoe UI', sans-serif; +} + +.readme-hero { + display: flex; + justify-content: center; +} + +.dialect-gallery { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1.25rem; + padding: 0; + margin: 0; +} + +.dialect-card { + position: relative; + background: linear-gradient(135deg, #020617 0%, #0f172a 100%); + border: 1px solid #1f2937; + border-radius: 18px; + padding: 1.25rem; + min-height: 140px; + display: flex; + align-items: center; + justify-content: center; +} + +.dialect-card style[data-dialect-style] { + display: none; +} diff --git a/packages/playwright/src/lit-react/button-wrapper.ts b/packages/playwright/src/lit-react/button-wrapper.ts index 2aee1f5..122f3b4 100644 --- a/packages/playwright/src/lit-react/button-wrapper.ts +++ b/packages/playwright/src/lit-react/button-wrapper.ts @@ -2,8 +2,9 @@ import { reactJsx } from '@knighted/jsx/react' import { createRoot, type Root } from 'react-dom/client' import { LitElement, html, unsafeCSS, type PropertyValues } from 'lit' -import { Button } from './button.js' -import { knightedCss as reactStyles } from './button.js?knighted-css' +import type { DialectSample } from '../dialects/registry.js' +import { Showcase } from './showcase.js' +import { knightedCss as reactStyles } from './showcase.js?knighted-css' import { BUTTON_WRAPPER_TAG } from './constants.js' export class ButtonWrapper extends LitElement { @@ -18,6 +19,7 @@ export class ButtonWrapper extends LitElement { * shadow Lit's accessors and prevent attr -> prop updates (see Lit warning). */ declare ctaLabel: string + #dialects: DialectSample[] = [] #reactRoot?: Root firstUpdated(): void { @@ -40,10 +42,19 @@ export class ButtonWrapper extends LitElement { this.#renderReactTree() } + setDialects(dialects: DialectSample[]): void { + this.#dialects = dialects + if (this.isConnected) { + this.#renderReactTree() + } + } + #renderReactTree(): void { - if (!this.#reactRoot) return + if (!this.#reactRoot || this.#dialects.length === 0) return const label = this.ctaLabel ?? 'React Button' - this.#reactRoot.render(reactJsx`<${Button} label=${label} />`) + this.#reactRoot.render( + reactJsx`<${Showcase} label=${label} dialects=${this.#dialects} />`, + ) } protected updated(changed: PropertyValues): void { diff --git a/packages/playwright/src/lit-react/index.ts b/packages/playwright/src/lit-react/index.ts index 1f59e8f..d962405 100644 --- a/packages/playwright/src/lit-react/index.ts +++ b/packages/playwright/src/lit-react/index.ts @@ -1,12 +1,15 @@ +import type { DialectSample } from '../dialects/registry.js' import { BUTTON_WRAPPER_TAG, LIT_REACT_TEST_ID } from './constants.js' -import { ensureButtonWrapperDefined } from './button-wrapper.js' +import { ButtonWrapper, ensureButtonWrapperDefined } from './button-wrapper.js' -export function renderLitReactDemo(root: HTMLElement): void { +export function renderLitReactDemo(root: HTMLElement, dialects: DialectSample[]): void { ensureButtonWrapperDefined() - const host = document.createElement(BUTTON_WRAPPER_TAG) + const mountPoint = root ?? document.body + const host = document.createElement(BUTTON_WRAPPER_TAG) as ButtonWrapper host.dataset.testid = LIT_REACT_TEST_ID host.setAttribute('cta-label', 'Launch CSS Build') - root.appendChild(host) + host.setDialects(dialects) + mountPoint.appendChild(host) } export { LIT_REACT_TEST_ID } diff --git a/packages/playwright/src/lit-react/showcase.tsx b/packages/playwright/src/lit-react/showcase.tsx new file mode 100644 index 0000000..c092463 --- /dev/null +++ b/packages/playwright/src/lit-react/showcase.tsx @@ -0,0 +1,29 @@ +import './app.css' + +import type { DialectSample } from '../dialects/registry.js' +import { Button } from './button.js' + +type ShowcaseProps = { + label: string + dialects: DialectSample[] +} + +export function Showcase({ label, dialects }: ShowcaseProps) { + return ( +
+
+
+
+ {dialects.map(dialect => ( +
+