Skip to content

Commit 6f83b8d

Browse files
feat: knighted css imports. (#139)
1 parent e79dda9 commit 6f83b8d

4 files changed

Lines changed: 469 additions & 9 deletions

File tree

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,27 @@ browser acts as the runtime host for editing, render, lint, and typecheck flows.
3636
- use AI chat with tab-aware proposals and apply/undo controls
3737
- switch theme and collapse the preview panel while preserving fast feedback loops
3838

39+
## CSS Query Imports In Editor Tabs
40+
41+
`@knighted/develop` supports `@knighted/css` query syntax in workspace tabs, including:
42+
43+
- `./styles/button.module.css?knighted-css`
44+
- `./button.tsx?knighted-css&combined`
45+
46+
For Lit + React-in-Shadow-DOM flows, a minimal pattern is:
47+
48+
1. In `button.tsx`, export your React component and import CSS Modules normally.
49+
2. In `lit-host.ts`, import from `./button.tsx?knighted-css&combined` and use `knightedCss`.
50+
3. Apply `unsafeCSS(knightedCss)` in `static styles` so styles render inside the shadow root.
51+
52+
Example imports:
53+
54+
- `import { ReactButton, knightedCss } from './button.tsx?knighted-css&combined'`
55+
- `import { LitElement, css, html, unsafeCSS } from 'lit'`
56+
57+
`knightedCssModules` is optional for this flow and is not required when you only need
58+
the compiled CSS text plus your component exports.
59+
3960
## Why this shape
4061

4162
The app started as a focused compile-and-preview loop and has grown into a

playwright/rendering-modes/core.spec.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,69 @@ test('renders in react mode with css modules', async ({ page }) => {
165165
await expectPreviewHasRenderedContent(page)
166166
})
167167

168+
test('reactJsx tag interpolation renders memo and forwardRef components', async ({
169+
page,
170+
}) => {
171+
await waitForInitialRender(page)
172+
await ensurePanelToolsVisible(page, 'component')
173+
174+
await page.getByRole('button', { name: 'Open tab App.tsx' }).click()
175+
await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react')
176+
177+
await setComponentEditorSource(
178+
page,
179+
[
180+
"import { memo, forwardRef } from 'react'",
181+
"import { reactJsx } from '@knighted/jsx/react'",
182+
'',
183+
'type ButtonProps = {',
184+
' label: string',
185+
'}',
186+
'',
187+
'const MemoButton = memo(({ label }: ButtonProps) => (',
188+
' <button type="button" data-testid="memo-button">',
189+
' {label}',
190+
' </button>',
191+
'))',
192+
'',
193+
'const ForwardRefButton = forwardRef<HTMLButtonElement, ButtonProps>(',
194+
' ({ label }, ref) => (',
195+
' <button ref={ref} type="button" data-testid="forward-ref-button">',
196+
' {label}',
197+
' </button>',
198+
' ),',
199+
')',
200+
'',
201+
'const App = () =>',
202+
' reactJsx`',
203+
' <section>',
204+
' <${MemoButton} label="Memo OK" />',
205+
' <${ForwardRefButton} label="ForwardRef OK" />',
206+
' </section>',
207+
' `',
208+
].join('\n'),
209+
)
210+
211+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
212+
await expect(getPreviewFrame(page).getByTestId('memo-button')).toHaveText('Memo OK')
213+
await expect(getPreviewFrame(page).getByTestId('forward-ref-button')).toHaveText(
214+
'ForwardRef OK',
215+
)
216+
217+
await expect
218+
.poll(async () => {
219+
return getPreviewFrame(page)
220+
.locator('html')
221+
.evaluate(
222+
() =>
223+
Array.from(document.querySelectorAll('*')).filter(node =>
224+
/^__kx_expr__/i.test(node.localName),
225+
).length,
226+
)
227+
})
228+
.toBe(0)
229+
})
230+
168231
test('react mode keeps App.ts entry but surfaces rename guidance until compatible', async ({
169232
page,
170233
}) => {
@@ -306,6 +369,128 @@ test('css module imports expose class map for module tabs', async ({ page }) =>
306369
.not.toContain('.item:active')
307370
})
308371

372+
test('workspace modules support ?knighted-css&combined imports', async ({ page }) => {
373+
await waitForInitialRender(page)
374+
375+
await ensurePanelToolsVisible(page, 'component')
376+
await ensurePanelToolsVisible(page, 'styles')
377+
378+
await page.getByRole('button', { name: 'Open tab App.tsx' }).click()
379+
await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react')
380+
381+
await renameWorkspaceTab(page, {
382+
from: 'app.css',
383+
to: 'button.module.css',
384+
})
385+
386+
await page.getByRole('button', { name: 'Open tab button.module.css' }).click()
387+
await page.getByRole('combobox', { name: 'Style mode' }).selectOption('module')
388+
389+
await setWorkspaceTabSource(page, {
390+
fileName: 'button.module.css',
391+
kind: 'styles',
392+
source: ['.btn {', ' color: rgb(7, 89, 160);', ' font-weight: 700;', '}'].join(
393+
'\n',
394+
),
395+
})
396+
397+
await addWorkspaceTab(page, { type: 'script' })
398+
await renameWorkspaceTab(page, {
399+
from: 'module.tsx',
400+
to: 'button.tsx',
401+
})
402+
403+
await setWorkspaceTabSource(page, {
404+
fileName: 'button.tsx',
405+
source: [
406+
"import styles from '../styles/button.module.css'",
407+
'',
408+
'type ButtonProps = {',
409+
' label: string',
410+
'}',
411+
'',
412+
'export const ReactButton = ({ label }: ButtonProps) => (',
413+
' <button type="button" className={styles.btn}>{label}</button>',
414+
')',
415+
].join('\n'),
416+
})
417+
418+
await setComponentEditorSource(
419+
page,
420+
[
421+
"import { ReactButton, knightedCss } from './button.tsx?knighted-css&combined'",
422+
'',
423+
'const App = () => (',
424+
' <>',
425+
' <style>{knightedCss}</style>',
426+
' <ReactButton label="Combined query works" />',
427+
' </>',
428+
')',
429+
].join('\n'),
430+
)
431+
432+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
433+
await expect(page.locator('#preview-host pre.preview-runtime-error')).toHaveCount(0)
434+
await expect(
435+
getPreviewFrame(page).getByRole('button', { name: 'Combined query works' }),
436+
).toHaveCSS('color', 'rgb(7, 89, 160)')
437+
})
438+
439+
test('workspace style tabs support ?knighted-css query exports', async ({ page }) => {
440+
await waitForInitialRender(page)
441+
442+
await ensurePanelToolsVisible(page, 'component')
443+
await ensurePanelToolsVisible(page, 'styles')
444+
445+
await page.getByRole('button', { name: 'Open tab app.css' }).click()
446+
await renameWorkspaceTab(page, {
447+
from: 'app.css',
448+
to: 'button.module.css',
449+
})
450+
await page.getByRole('combobox', { name: 'Style mode' }).selectOption('module')
451+
452+
await setWorkspaceTabSource(page, {
453+
fileName: 'button.module.css',
454+
kind: 'styles',
455+
source: [
456+
'.btn {',
457+
' color: rgb(14, 110, 173);',
458+
' border: 1px solid rgb(14, 110, 173);',
459+
'}',
460+
].join('\n'),
461+
})
462+
463+
await page.getByRole('button', { name: 'Open tab App.tsx' }).click()
464+
await setComponentEditorSource(
465+
page,
466+
[
467+
"import styles, { knightedCss } from '../styles/button.module.css?knighted-css'",
468+
'',
469+
'const App = () => (',
470+
' <>',
471+
' <style>{knightedCss}</style>',
472+
' <button',
473+
' type="button"',
474+
' className={styles.btn}',
475+
' data-css-length={String(knightedCss.length)}',
476+
' >',
477+
' Style query works',
478+
' </button>',
479+
' </>',
480+
')',
481+
].join('\n'),
482+
)
483+
484+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
485+
await expect(page.locator('#preview-host pre.preview-runtime-error')).toHaveCount(0)
486+
487+
const previewButton = getPreviewFrame(page).getByRole('button', {
488+
name: 'Style query works',
489+
})
490+
await expect(previewButton).toHaveCSS('color', 'rgb(14, 110, 173)')
491+
await expect(previewButton).toHaveAttribute('data-css-length', /[1-9]\d*/)
492+
})
493+
309494
test('preview styles require explicit import from entry graph', async ({ page }) => {
310495
await waitForInitialRender(page)
311496

0 commit comments

Comments
 (0)