Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,27 @@ browser acts as the runtime host for editing, render, lint, and typecheck flows.
- use AI chat with tab-aware proposals and apply/undo controls
- switch theme and collapse the preview panel while preserving fast feedback loops

## CSS Query Imports In Editor Tabs

`@knighted/develop` supports `@knighted/css` query syntax in workspace tabs, including:

- `./styles/button.module.css?knighted-css`
- `./button.tsx?knighted-css&combined`

For Lit + React-in-Shadow-DOM flows, a minimal pattern is:

1. In `button.tsx`, export your React component and import CSS Modules normally.
2. In `lit-host.ts`, import from `./button.tsx?knighted-css&combined` and use `knightedCss`.
3. Apply `unsafeCSS(knightedCss)` in `static styles` so styles render inside the shadow root.

Example imports:

- `import { ReactButton, knightedCss } from './button.tsx?knighted-css&combined'`
- `import { LitElement, css, html, unsafeCSS } from 'lit'`

`knightedCssModules` is optional for this flow and is not required when you only need
the compiled CSS text plus your component exports.

## Why this shape

The app started as a focused compile-and-preview loop and has grown into a
Expand Down
185 changes: 185 additions & 0 deletions playwright/rendering-modes/core.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,69 @@ test('renders in react mode with css modules', async ({ page }) => {
await expectPreviewHasRenderedContent(page)
})

test('reactJsx tag interpolation renders memo and forwardRef components', async ({
page,
}) => {
await waitForInitialRender(page)
await ensurePanelToolsVisible(page, 'component')

await page.getByRole('button', { name: 'Open tab App.tsx' }).click()
await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react')

await setComponentEditorSource(
page,
[
"import { memo, forwardRef } from 'react'",
"import { reactJsx } from '@knighted/jsx/react'",
'',
'type ButtonProps = {',
' label: string',
'}',
'',
'const MemoButton = memo(({ label }: ButtonProps) => (',
' <button type="button" data-testid="memo-button">',
' {label}',
' </button>',
'))',
'',
'const ForwardRefButton = forwardRef<HTMLButtonElement, ButtonProps>(',
' ({ label }, ref) => (',
' <button ref={ref} type="button" data-testid="forward-ref-button">',
' {label}',
' </button>',
' ),',
')',
'',
'const App = () =>',
' reactJsx`',
' <section>',
' <${MemoButton} label="Memo OK" />',
' <${ForwardRefButton} label="ForwardRef OK" />',
' </section>',
' `',
].join('\n'),
)

await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
await expect(getPreviewFrame(page).getByTestId('memo-button')).toHaveText('Memo OK')
await expect(getPreviewFrame(page).getByTestId('forward-ref-button')).toHaveText(
'ForwardRef OK',
)

await expect
.poll(async () => {
return getPreviewFrame(page)
.locator('html')
.evaluate(
() =>
Array.from(document.querySelectorAll('*')).filter(node =>
/^__kx_expr__/i.test(node.localName),
).length,
)
})
.toBe(0)
})

test('react mode keeps App.ts entry but surfaces rename guidance until compatible', async ({
page,
}) => {
Expand Down Expand Up @@ -306,6 +369,128 @@ test('css module imports expose class map for module tabs', async ({ page }) =>
.not.toContain('.item:active')
})

test('workspace modules support ?knighted-css&combined imports', async ({ page }) => {
await waitForInitialRender(page)

await ensurePanelToolsVisible(page, 'component')
await ensurePanelToolsVisible(page, 'styles')

await page.getByRole('button', { name: 'Open tab App.tsx' }).click()
await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react')

await renameWorkspaceTab(page, {
from: 'app.css',
to: 'button.module.css',
})

await page.getByRole('button', { name: 'Open tab button.module.css' }).click()
await page.getByRole('combobox', { name: 'Style mode' }).selectOption('module')

await setWorkspaceTabSource(page, {
fileName: 'button.module.css',
kind: 'styles',
source: ['.btn {', ' color: rgb(7, 89, 160);', ' font-weight: 700;', '}'].join(
'\n',
),
})

await addWorkspaceTab(page, { type: 'script' })
await renameWorkspaceTab(page, {
from: 'module.tsx',
to: 'button.tsx',
})

await setWorkspaceTabSource(page, {
fileName: 'button.tsx',
source: [
"import styles from '../styles/button.module.css'",
'',
'type ButtonProps = {',
' label: string',
'}',
'',
'export const ReactButton = ({ label }: ButtonProps) => (',
' <button type="button" className={styles.btn}>{label}</button>',
')',
].join('\n'),
})

await setComponentEditorSource(
page,
[
"import { ReactButton, knightedCss } from './button.tsx?knighted-css&combined'",
'',
'const App = () => (',
' <>',
' <style>{knightedCss}</style>',
' <ReactButton label="Combined query works" />',
' </>',
')',
].join('\n'),
)

await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
await expect(page.locator('#preview-host pre.preview-runtime-error')).toHaveCount(0)
await expect(
getPreviewFrame(page).getByRole('button', { name: 'Combined query works' }),
).toHaveCSS('color', 'rgb(7, 89, 160)')
})

test('workspace style tabs support ?knighted-css query exports', async ({ page }) => {
await waitForInitialRender(page)

await ensurePanelToolsVisible(page, 'component')
await ensurePanelToolsVisible(page, 'styles')

await page.getByRole('button', { name: 'Open tab app.css' }).click()
await renameWorkspaceTab(page, {
from: 'app.css',
to: 'button.module.css',
})
await page.getByRole('combobox', { name: 'Style mode' }).selectOption('module')

await setWorkspaceTabSource(page, {
fileName: 'button.module.css',
kind: 'styles',
source: [
'.btn {',
' color: rgb(14, 110, 173);',
' border: 1px solid rgb(14, 110, 173);',
'}',
].join('\n'),
})

await page.getByRole('button', { name: 'Open tab App.tsx' }).click()
await setComponentEditorSource(
page,
[
"import styles, { knightedCss } from '../styles/button.module.css?knighted-css'",
'',
'const App = () => (',
' <>',
' <style>{knightedCss}</style>',
' <button',
' type="button"',
' className={styles.btn}',
' data-css-length={String(knightedCss.length)}',
' >',
' Style query works',
' </button>',
' </>',
')',
].join('\n'),
)

await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
await expect(page.locator('#preview-host pre.preview-runtime-error')).toHaveCount(0)

const previewButton = getPreviewFrame(page).getByRole('button', {
name: 'Style query works',
})
await expect(previewButton).toHaveCSS('color', 'rgb(14, 110, 173)')
await expect(previewButton).toHaveAttribute('data-css-length', /[1-9]\d*/)
})

test('preview styles require explicit import from entry graph', async ({ page }) => {
await waitForInitialRender(page)

Expand Down
Loading
Loading