Skip to content

Commit d67ec29

Browse files
test: add css modules. (#17)
1 parent f0b5670 commit d67ec29

9 files changed

Lines changed: 97 additions & 15 deletions

File tree

README.md

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Typical customizations:
8282
- **filter**Skip certain paths (e.g., storybook-only styles) before compilation.
8383
- **resolver**Resolve virtual specifiers the way your bundler does (the repo ships test fixtures for webpack, Vite, and Rspack).
8484
- **lightningcss**Pass `true` for defaults or a config object for minification/autoprefixing.
85-
- **specificityBoost**Provide a Lightning CSS visitor to bump specificity on selected selectors (e.g., duplicate a class for matching selectors). We compose this with your `lightningcss.visitor`, if provided.
85+
- **specificityBoost**Provide a Lightning CSS visitor to bump specificity on selected selectors (e.g., duplicate a class for matching selectors).
8686

8787
## Examples
8888

@@ -150,11 +150,13 @@ module.exports = {
150150
import { reactJsx } from '@knighted/jsx/react'
151151
import { createRoot, type Root } from 'react-dom/client'
152152
import { LitElement, html, unsafeCSS } from 'lit'
153-
import { Button } from './button.tsx'
154-
import { knightedCss as reactStyles } from './button.tsx?knighted-css'
153+
import { customElement } from 'lit/decorators.js'
154+
import { Showcase } from './showcase.tsx'
155+
import { knightedCss as showcaseCss } from './showcase.tsx?knighted-css'
155156

156-
export class ButtonWrapper extends LitElement {
157-
static styles = [unsafeCSS(reactStyles)]
157+
@customElement('lit-host')
158+
export class LitHost extends LitElement {
159+
static styles = [unsafeCSS(showcaseCss)]
158160
#reactRoot?: Root
159161

160162
firstUpdated(): void {
@@ -166,28 +168,36 @@ export class ButtonWrapper extends LitElement {
166168
super.disconnectedCallback()
167169
}
168170

169-
#mountReact() {
170-
const outlet = this.renderRoot.querySelector(
171-
'[data-react-root]',
172-
) as HTMLDivElement | null
173-
if (!outlet) return
174-
this.#reactRoot = createRoot(outlet)
175-
this.#reactRoot.render(reactJsx`<${Button} />`)
171+
#mountReact(): void {
172+
if (!this.#reactRoot) {
173+
const outlet = this.renderRoot.querySelector(
174+
'[data-react-root]',
175+
) as HTMLDivElement | null
176+
if (!outlet) return
177+
this.#reactRoot = createRoot(outlet)
178+
}
179+
this.#reactRoot.render(reactJsx`<${Showcase} label="Launch CSS Build" />`)
176180
}
177181

178182
render() {
179183
return html`<div data-react-root></div>`
180184
}
181185
}
182-
183-
customElements.define('button-wrapper', ButtonWrapper)
184186
```
185187
186188
The loader appends `export const knightedCss = "/* compiled css */"` to the module when imported with `?knighted-css`. Keep your main module import separate to preserve its typing; use the query import only for the CSS string.
187189
188190
> [!TIP]
189191
> The Playwright Rspack demo shows how a Lit host can import specific dialects with `?knighted-css` and pipe them straight into `LitElement.styles`. See [packages/playwright/src/lit-react/lit-host.ts](packages/playwright/src/lit-react/lit-host.ts) for the shadow-root wiring.
190192
193+
#### CSS Modules and stable selectors
194+
195+
CSS Modules hash class names after the loader extracts selectors, so the stylesheet captured by `?knighted-css` never sees those hashed tokens. Provide a second, stable selector (class or data attribute) alongside the module-generated one so both the DOM and the Lit host share a common hook. A minimal example:
196+
197+
```tsx
198+
<div className={`${styles['css-modules-badge']} css-modules-badge`}>
199+
```
200+
191201
#### TypeScript support for loader queries
192202
193203
Loader query types ship directly with `@knighted/css`. Reference them once in your project—either by adding `"types": ["@knighted/css/loader-queries"]` to `tsconfig.json` or dropping `/// <reference types="@knighted/css/loader-queries" />` into a global `.d.ts`—and the following ambient modules become available everywhere:

docs/specificity-boost-visitor.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ const styles = await css('./src/entry.ts', {
3131
})
3232
```
3333

34-
You can plug any Lightning CSS visitor here; we compose it with your `lightningcss.visitor` if provided. Keep the match as narrow as possible to avoid unexpected cascade shifts.
34+
You can plug any Lightning CSS visitor into `specificityBoost.visitor`; it runs after the built-in `strategy` helpers. Keep the match as narrow as possible to avoid unexpected cascade shifts.

packages/playwright/rspack.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ export default {
2323
'.js': ['.js', '.ts', '.tsx'],
2424
},
2525
},
26+
experiments: {
27+
css: true,
28+
},
2629
module: {
2730
rules: [
2831
{
@@ -49,6 +52,10 @@ export default {
4952
},
5053
],
5154
},
55+
{
56+
test: /\.module\.css$/,
57+
type: 'css/module',
58+
},
5259
{
5360
test: /\.[jt]sx?$/,
5461
resourceQuery: /knighted-css/,
@@ -97,6 +104,7 @@ export default {
97104
},
98105
{
99106
test: /\.css$/,
107+
exclude: /\.module\.css$/,
100108
type: 'asset/source',
101109
},
102110
{
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module '*.module.css' {
2+
const classes: Record<string, string>
3+
export = classes
4+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
.css-modules-badge {
2+
display: inline-flex;
3+
flex-direction: column;
4+
gap: 0.45rem;
5+
padding: 1rem 1.25rem;
6+
border-radius: 18px;
7+
background: linear-gradient(135deg, #312e81, #4c1d95);
8+
box-shadow:
9+
0 12px 38px rgba(76, 29, 149, 0.45),
10+
0 0 0 1px rgba(129, 140, 248, 0.45);
11+
border: 1px solid rgba(191, 219, 254, 0.35);
12+
color: #e0e7ff;
13+
}
14+
15+
.css-modules-token {
16+
display: inline-flex;
17+
align-items: center;
18+
gap: 0.4rem;
19+
padding: 0.35rem 0.75rem;
20+
border-radius: 999px;
21+
background: rgba(224, 231, 255, 0.15);
22+
color: #c7d2fe;
23+
font-size: 0.8rem;
24+
letter-spacing: 0.08em;
25+
text-transform: uppercase;
26+
}
27+
28+
.css-modules-label {
29+
font-size: 0.95rem;
30+
line-height: 1.4;
31+
font-weight: 600;
32+
letter-spacing: 0.02em;
33+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as styles from './css-modules-card.module.css'
2+
3+
export const CSS_MODULES_TEST_ID = 'dialect-css-modules'
4+
5+
export function CssModulesCard() {
6+
return (
7+
<div
8+
className={`${styles['css-modules-badge']} css-modules-badge`}
9+
data-testid={CSS_MODULES_TEST_ID}
10+
>
11+
<span className={`${styles['css-modules-token']} css-modules-token`}>
12+
css modules
13+
</span>
14+
<span className={`${styles['css-modules-label']} css-modules-label`}>
15+
Stable selectors keep Lit-hosted styles in sync with hashed class names.
16+
</span>
17+
</div>
18+
)
19+
}

packages/playwright/src/lit-react/showcase.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import type { JSX } from 'react'
44

55
import { BasicCard, BASIC_TEST_ID } from './cards/basic-card/basic-card.js'
66
import { LessCard, LESS_TEST_ID } from './cards/less-card/less-card.js'
7+
import {
8+
CssModulesCard,
9+
CSS_MODULES_TEST_ID,
10+
} from './cards/css-modules-card/css-modules-card.js'
11+
import { knightedCss as cssModulesCss } from './cards/css-modules-card/css-modules-card.js?knighted-css'
712
import { SassCard, SASS_TEST_ID } from './cards/sass-card/sass-card.js'
813
import { knightedCss as sassCss } from './cards/sass-card/sass-card.js?knighted-css'
914
import { ScssCard, SCSS_TEST_ID } from './cards/scss-card/scss-card.js'
@@ -27,6 +32,7 @@ type HostManagedDialect = {
2732
const cards: DialectCard[] = [
2833
{ id: SCSS_TEST_ID, css: scssCss, Component: ScssCard },
2934
{ id: SASS_TEST_ID, css: sassCss, Component: SassCard },
35+
{ id: CSS_MODULES_TEST_ID, css: cssModulesCss, Component: CssModulesCard },
3036
{ id: VANILLA_TEST_ID, css: vanillaCss, Component: VanillaCard },
3137
]
3238

packages/playwright/test/lit-react.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const dialectCases = [
77
{ id: 'dialect-scss', property: 'color' },
88
{ id: 'dialect-sass-indented', property: 'color' },
99
{ id: 'dialect-less', property: 'color' },
10+
{ id: 'dialect-css-modules', property: 'color' },
1011
{ id: 'dialect-vanilla', property: 'color' },
1112
]
1213

packages/playwright/test/styles.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const cases = [
55
{ id: 'dialect-scss', property: 'color' },
66
{ id: 'dialect-sass-indented', property: 'color' },
77
{ id: 'dialect-less', property: 'color' },
8+
{ id: 'dialect-css-modules', property: 'color' },
89
{ id: 'dialect-vanilla', property: 'color' },
910
]
1011

0 commit comments

Comments
 (0)