TSX in a design

The TSX a design can hold, and what Knighn changes when it cannot.

Knighn reads the TSX of a design. It does not run it. It turns the source into a tree of layers that people and agents can edit. For that, the source must stay in a subset of TSX. The subset has what a design needs: elements, classes, text, components and props. It has no logic.

When the source goes outside the subset, Knighn gives a diagnostic with a code, a message, a line, a column and often a fix. A component with refused code is locked: Knighn keeps its source byte for byte and the file still renders it, but the editor and the agent helpers cannot change it. Fix each error.

Top level

The source can have only these statements:

  • import type { ReactNode } from 'react'. Knighn refuses other imports.
  • Props types, as type or interface.
  • Variant maps, such as const cardTone = { plain: '…', accent: '…' }.
  • Components: export function Name(…) { return (…) }.
  • One export const canvas = { pages: […] }.
  • Comments.

Knighn keeps any other statement as raw text and gives a warning. The editor cannot change a raw statement.

This source uses each part:

import type { ReactNode } from 'react'

type CardProps = {
  title?: string
  tone?: 'plain' | 'accent'
  icon?: ReactNode
  children?: ReactNode
  className?: string
}

const cardTone = {
  plain: 'bg-surface text-foreground ring-1 ring-border',
  accent: 'bg-accent text-accent-foreground'
}

// A card with an optional icon and any content.
export function Card({
  title = 'Title',
  tone = 'plain',
  icon,
  children,
  className = ''
}: CardProps) {
  return (
    <section className={`flex flex-col gap-3 rounded-xl p-6 ${cardTone[tone]} ${className}`}>
      {icon}
      <h3 className="text-lg font-semibold">{title}</h3>
      {children}
    </section>
  )
}

export const canvas = {
  pages: [{ name: 'Components', kind: 'components', items: [{ render: Card, x: 0, y: 0 }] }]
}

Knighn keeps a comment with the statement or the layer that comes after it.

Components

  • Export each component: export function Pricing(…). The name is PascalCase.
  • The parameter destructures the props. Give each optional prop a literal default: { label = 'Save', tone = 'primary', className = '' }.
  • Put the props type after the parameter: : ButtonProps, or an inline object type.
  • The body is only return (…), with one root element, one instance or one fragment.
  • A component with no other props still takes className.

The className prop

Each component takes className?: string with the default ''. It puts ${className} last in the class list of its root element. An instance uses it to add layout classes, such as <Button className="w-full" />. Knighn adds the prop when it makes a component.

className adds classes. It cannot reliably replace a class that the component sets, because the order of the CSS rules decides, not the order of the classes. To change a class that the component sets, add a variant prop. See components.md.

Props types

A prop has one of these types:

Type Example Use
string label?: string Text, links and image URLs
number count?: number Numbers that you show as text
boolean selected?: boolean Show or hide a part, or switch classes
Union of string literals tone?: 'primary' | 'secondary' A variant
ReactNode icon?: ReactNode A slot for one instance
ReactNode named children children?: ReactNode The content between the tags of an instance

Knighn refuses other types: arrays, objects, functions, unions of numbers, any and generic types.

A default is a literal: a string, a number, true or false. A ReactNode prop has no default, or one instance as its default: icon = <IconStar />.

Variant maps

A variant map gives the classes for each value of a variant prop.

  • Write it at the top level: const buttonTone = { primary: 'bg-accent text-accent-foreground', secondary: 'bg-surface text-foreground' }.
  • Give it one key for each value of the union. Each value is a string literal of classes.
  • Use it in a class list as ${buttonTone[tone]}.
  • Name it after the component and the prop: buttonTone, buttonSize.

JSX

Tags

  • Use lowercase HTML and SVG tags: section, h1, img, svg, path.
  • Use components of the same file: <Button label="Save" />.
  • Knighn refuses the tags script, iframe, object, embed, style, link, meta and base.

className

className is a string of classes:

<div className="flex items-center gap-3 rounded-lg bg-surface p-4" />

In a component, className can also be a template literal with parts that come from props:

  • ${map[prop]}: the classes of a variant map, such as ${buttonTone[tone]}.
  • ${prop ? 'classes' : 'classes'}: classes for a boolean prop. Both branches are string literals. One branch can be ''. The test can also be !prop, prop === 'value' or prop !== 'value'.
  • ${className}: the className prop. Put it last, on the root element.

One part alone needs no template literal: className={buttonTone[tone]}, className={active ? 'font-medium' : ''} and className={className} are correct.

A nested ternary on one union prop, such as className={size === 'sm' ? 'h-8' : size === 'lg' ? 'h-12' : 'h-10'}, becomes a variant map. Knighn adds the map, uses ${map[size]} in its place and gives a note (subset/rewrite). Each value of the union that no test names gets the classes of the last branch.

type TabProps = { label?: string; active?: boolean; className?: string }

export function Tab({ label = 'Overview', active = false, className = '' }: TabProps) {
  return (
    <a
      href="#"
      className={`h-9 rounded-md px-3 text-sm ${active ? 'bg-muted font-medium text-foreground' : 'text-muted-foreground'} ${className}`}
    >
      {label}
    </a>
  )
}

Rules for class lists:

  • Put spaces between classes and parts.
  • cn(...), clsx(...), cx(...) and twMerge(...) join string literals, variant lookups and prop conditions. Knighn writes them as a template literal. Knighn refuses other calls, +, nested template literals and lookups with a literal key.
  • The order of the classes is not important. Knighn sorts them in the Tailwind order when it saves.

Attributes

  • Literals: type="email", rows={4}, disabled.
  • A prop: href={link}, src={image}, alt={caption}.
  • Use the JSX spelling: className, htmlFor, strokeWidth, aria-label, data-name.
  • Use defaultValue and defaultChecked to show values in form controls.
  • style={{ … }} with literal values stays in the file, but the editor shows it as read only. Use classes.
  • Knighn refuses key, ref, event handlers such as onClick, spreads such as {...props} and dangerouslySetInnerHTML. Give {autofix: true} to Define, AddScreen, AddScreens, Insert or Replace, and Knighn removes key, ref, event handlers and hooks, with a note for each change.

Children

A child is one of these:

  • Text.
  • {prop} for a string or number prop, {children}, or {slot} for a ReactNode prop.
  • {flag && <X />}. The condition is flag, !flag, tone === 'danger' or tone !== 'danger', or these joined with &&, || and !(…), such as {(open || pinned) && <X />}.
  • A template literal of text and props: {`Build ${build}`} is the same as Build {build}. Knighn writes it as Build {build}.
  • .map() over a literal array, written in place or as a const of literals in the file or in the component: {plans.map((plan) => <Plan name={plan.name} />)}. Knighn writes each item out as JSX, with the values of the item in place of plan.name, and removes key. It gives a note (subset/rewrite). After that, each item is a layer that you can edit.
  • A comment: {/* Pricing */}.
  • A fragment: <>…</>.

A condition can hold one element, one instance or one fragment. Knighn reads a ternary as two conditions. {dark ? <A /> : <B />} is {dark && <A />} and {!dark && <B />}. A ternary in a prop or attribute value, with literals or props in its branches, makes two copies of the element: <Btn tone={dark ? 'white' : 'brand'} /> is {dark && <Btn tone="white" />} and {!dark && <Btn tone="brand" />}. null or undefined in a branch leaves the prop out of that copy. The condition of a ternary has the same forms as the condition of &&. Knighn writes the file with the two conditions.

type StatusProps = { online?: boolean; className?: string }

export function Status({ online = true, className = '' }: StatusProps) {
  return (
    <span className={`inline-flex items-center gap-2 text-sm ${className}`}>
      {online && <span className="size-2 rounded-full bg-emerald-500" />}
      {!online && <span className="size-2 rounded-full bg-muted-foreground" />}
      {online && <>Online</>}
      {!online && <>Offline</>}
    </span>
  )
}

Text

  • Write {, }, < and > in text as &#123;, &#125;, &lt; and &gt;.
  • JSX removes white space that has a line break between tags. Keep inline text on one line when the spaces are important: <p>Read the <a href="#">guide</a> first.</p>.
  • Use <br /> for a hard line break.

Layer names

Give sections and important layers a name with data-name:

<section data-name="Pricing" className="flex flex-col gap-8 px-6 py-24" />

Knighn shows the name in the layers panel and in search. Agents find the layer with Find(page, '[data-name="Pricing"]'). Knighn writes data-name only for layers that have a name. The tag and the classes identify the other layers.

The canvas

export const canvas puts components on pages. It has only literals and component names.

export function Home({
  headline = 'Ship it',
  className = ''
}: {
  headline?: string
  className?: string
}) {
  return (
    <main
      className={`flex size-full flex-col items-center justify-center bg-background text-foreground ${className}`}
    >
      <h1 className="text-5xl font-semibold">{headline}</h1>
    </main>
  )
}

export const canvas = {
  pages: [
    {
      name: 'Web',
      items: [
        { render: Home, x: 0, y: 0, width: 1440, height: 900 },
        {
          render: Home,
          props: { headline: 'Ship it faster' },
          x: 1540,
          y: 0,
          width: 390,
          height: 844
        },
        { render: Home, x: 2030, y: 0, width: 390, height: 844, modes: { theme: 'dark' } }
      ]
    },
    { name: 'Components', kind: 'components', items: [] }
  ]
}
  • A page has a name and items. An older design can have a page with kind: 'components' for its mains. New mains go on the canvas page.
  • An item is a frame. render is the component. x and y are canvas pixels.
  • width sets the width of the frame. The frame is then the container for container variants such as @3xl:.
  • height sets the height of the frame. Without it, the frame hugs its content.
  • props gives literal props to the component.
  • modes puts a mode on the frame, such as { theme: 'dark' }. See tokens.md.

Refused code

Knighn refuses these, with an error:

Refused Fix
Hooks and state: useState, useEffect Make a variant prop for each state, or a frame for each state.
Event handlers: onClick, onChange Delete them. Use href for links.
.map() over data that is not a literal array, for, while, .filter() Write each item as JSX, or one instance for each item.
Calls: format(), price.toFixed(2) Write the result as a literal.
Spreads: {...props}, ...rest Write each prop.
Ternaries with a condition outside the subset: {count > 2 ? <A /> : <B />} Make a boolean or union prop, then write the ternary with it.
key, ref, dangerouslySetInnerHTML Delete them.
Value imports Define the component in the file. Use <Icon name="…" /> for icons.
The tags script, iframe, object, embed, style, link, meta, base Delete them. Put theme CSS in tokens.

With {autofix: true}, Knighn removes key, ref, event handlers and hooks in place of an error. A useState value with a literal default becomes a prop with that default.

The subset applies to the TSX only. An execute snippet is JavaScript, so it can use .map() and loops to write the TSX that it passes to Insert. See execute.md.

Diagnostics

Each diagnostic has these fields:

  • severity: error, warning or info.
  • code: what is wrong, such as subset/map.
  • message: what is wrong and how to fix it.
  • line and column: where it starts, from 1. For the source, they count in the .html file. For a fragment that you gave to Insert or Replace, they count in the fragment.
  • fix: a replacement, when Knighn has one.

Errors stop a fragment. Insert, Replace, AddScreen and Define throw on an error, and the call changes nothing. Warnings do not stop anything, but fix them too.

syntax

The source is not valid TSX. Look at the line and the column.

  • Close each tag: <p>…</p>, or <img … />.
  • Write { and } in text as &#123; and &#125;.
  • Close each (, { and template literal.
export function Note({ className = '' }: { className?: string }) {
  return <p className={`text-sm ${className}`}>Saved
}

subset/import

The source imports a value. Only import type { … } from 'react' is allowed.

  • Define other components in the file.
  • For icons, use InsertIcon, or define an icon component with an inline <svg>. See assets.md.
import { ArrowRight } from 'lucide-react'

export function Next({ className = '' }: { className?: string }) {
  return (
    <a href="#" className={`inline-flex items-center gap-2 ${className}`}>
      Next <ArrowRight />
    </a>
  )
}
export function Next({ className = '' }: { className?: string }) {
  return (
    <a href="#" className={`inline-flex items-center gap-2 ${className}`}>
      Next
      <svg
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        strokeWidth={2}
        strokeLinecap="round"
        strokeLinejoin="round"
        aria-hidden="true"
        className="size-4"
      >
        <path d="M5 12h14" />
        <path d="m12 5 7 7-7 7" />
      </svg>
    </a>
  )
}

subset/statement

A top-level statement is outside the subset: let, var, a function that is not exported, a class, export default, a call, or a const that is not a variant map. This is a warning. Knighn keeps the statement as raw text, and the editor cannot change it.

  • Make data into literals in the JSX.
  • Make a helper function into a component.
  • Make a lookup table into a variant map.

subset/hook

A component calls a hook, such as useState. A design shows one state at a time.

  • Make a variant prop for each state: state?: 'idle' | 'loading' | 'done'.
  • Or make one frame for each state, with props on each canvas item.
  • Or give {autofix: true}. Knighn makes const [count] = useState(0) the prop count = 0 and removes the other hooks.
export function Counter({ className = '' }: { className?: string }) {
  const [count] = useState(0)
  return <p className={`text-2xl ${className}`}>{count}</p>
}
type CounterProps = { count?: number; className?: string }

export function Counter({ count = 0, className = '' }: CounterProps) {
  return <p className={`text-2xl ${className}`}>{count}</p>
}

subset/handler

An element has an event handler, such as onClick. A design does not run code.

  • Delete the handler, or give {autofix: true} and Knighn deletes it.
  • Use href for links.
  • Show hover, open and selected states as variant props.
export function Save({ className = '' }: { className?: string }) {
  return (
    <button type="button" onClick={() => {}} className={`h-10 rounded-lg px-4 ${className}`}>
      Save
    </button>
  )
}

subset/map

The source uses a loop, an array method other than .map(), or .map() over data that is not a literal array. Knighn writes out .map() over a literal array (see Children), but it cannot edit items that other code makes.

  • Write each item as JSX, or make the array a literal.
  • For repeated items with the same shape, make a component and write one instance for each item.
  • In an execute snippet, build the TSX string with .map() in JavaScript, then pass the string.
export function Plans({ className = '' }: { className?: string }) {
  return (
    <ul className={`flex flex-col gap-2 ${className}`}>
      {['Starter', 'Pro', 'Team'].filter(Boolean).map((plan) => (
        <li>{plan}</li>
      ))}
    </ul>
  )
}
export function Plans({ className = '' }: { className?: string }) {
  return (
    <ul className={`flex flex-col gap-2 ${className}`}>
      <li>Starter</li>
      <li>Pro</li>
      <li>Team</li>
    </ul>
  )
}

subset/expression

A JSX value is an expression outside the subset: a call, arithmetic, a ternary whose branches are not literals, props or JSX, a template literal with a part that is not a prop, props.label, key, ref or dangerouslySetInnerHTML.

  • Write the result as a literal: <p>$288</p>, not <p>{price * 12}</p>.
  • Destructure the prop: {label}, not {props.label}.
  • Give a ternary literals, props or JSX in its branches.
  • Delete key and ref.

subset/spread

The source uses a spread: {...props} in JSX, or ...rest in the props parameter.

  • Write each prop and each attribute.
type FieldProps = { className?: string; placeholder?: string }

export function Field({ className = '', ...rest }: FieldProps) {
  return <input {...rest} className={`h-10 rounded-md px-3 ${className}`} />
}
type FieldProps = { placeholder?: string; className?: string }

export function Field({ placeholder = 'you@example.com', className = '' }: FieldProps) {
  return (
    <input type="email" placeholder={placeholder} className={`h-10 rounded-md px-3 ${className}`} />
  )
}

subset/class-expression

Knighn cannot read a className expression: a call other than cn(), clsx(), cx() or twMerge(), strings joined with +, a part glued to other text, or a nested ternary that is not on one union prop.

  • Use a template literal with variant lookups and prop conditions, or cn().
  • Do not join strings with +. Write whole class names.
  • For a nested ternary, use a union prop and test it with === in each branch, or write a variant map.
type TagProps = { active?: boolean; className?: string }

export function Tag({ active = false, className = '' }: TagProps) {
  return <span className={'rounded-full px-2 text-xs ' + className}>New</span>
}
type TagProps = { active?: boolean; className?: string }

export function Tag({ active = false, className = '' }: TagProps) {
  return (
    <span
      className={`rounded-full px-2 text-xs ${active ? 'bg-accent text-accent-foreground' : ''} ${className}`}
    >
      New
    </span>
  )
}

subset/condition

A condition in {… && …} is not a prop test. The allowed tests are flag, !flag, prop === 'value' and prop !== 'value'. Join them with &&, || and !(…).

  • Add a boolean prop for the condition.
  • Or compare a variant prop with one of its values.
type CartProps = { count?: number; className?: string }

export function Cart({ count = 0, className = '' }: CartProps) {
  return (
    <div className={`flex items-center gap-2 ${className}`}>
      Cart
      {count > 0 && (
        <span className="rounded-full bg-accent px-1.5 text-xs text-accent-foreground">
          {count}
        </span>
      )}
    </div>
  )
}
type CartProps = { count?: number; showCount?: boolean; className?: string }

export function Cart({ count = 3, showCount = true, className = '' }: CartProps) {
  return (
    <div className={`flex items-center gap-2 ${className}`}>
      Cart
      {showCount && (
        <span className="rounded-full bg-accent px-1.5 text-xs text-accent-foreground">
          {count}
        </span>
      )}
    </div>
  )
}

subset/component

A component or a tag is outside the subset. Causes:

  • The body has statements before return.
  • The function is not exported, or its name is not PascalCase.
  • The parameter is not a destructured object, for example (props).
  • An instance names a component that the file does not define.
  • The tag is one of script, iframe, object, embed, style, link, meta or base.

Move values into props with defaults, export the function, define the component in the file, and delete refused tags.

subset/prop-type

A prop has a type outside the subset: an array, an object, a function, a union of numbers, any or a generic type.

  • Split an object into one prop for each field.
  • Make a list into separate instances, one for each item.
  • Make a union of numbers into a union of strings, such as '1' | '2' | '3'.
type MenuProps = { items?: string[]; className?: string }

export function Menu({ className = '' }: MenuProps) {
  return <nav className={`flex gap-4 ${className}`} />
}

subset/default

A default is not a literal.

  • Use a string, a number, true or false.
  • Give a ReactNode prop no default, or one instance.
type PriceProps = { amount?: number; className?: string }

export function Price({ amount = 20 * 12, className = '' }: PriceProps) {
  return <p className={`text-2xl font-semibold ${className}`}>{amount}</p>
}

subset/rewrite

A note, not an error. Knighn changed the source to a form of the subset, and the call worked. The result notes of execute show each one. Causes:

  • Knighn wrote out the items of a .map() over a literal array.
  • Knighn made a variant map from a nested ternary in className.
  • {autofix: true} removed key, ref, an event handler or a hook, or made a useState value a prop.

Read the note. Then read the design again with Tree or Get, because the source that Knighn keeps is the changed source.

canvas/missing

The source has no export const canvas. Knighn needs it to show frames.

  • Add export const canvas = { pages: [{ name: 'Web', items: [] }] }, then place frames on the page.

canvas/item

An item of the canvas is wrong. Causes:

  • render names no exported component.
  • x or y is missing, or is not a number.
  • props has a prop that the component does not have, or a value that is not a literal.
  • modes names a mode that no mode block defines.

Name an exported component, use number literals, use props from the props type, and add the mode block with SetTokens.

lint/viewport-variant

A class uses a viewport variant, such as md:. All frames share one document, so a viewport variant follows the editor window, not the frame. Use the container variant:

Viewport variant Container variant Starts at
sm: @min-[40rem]: 640 px
md: @3xl: 768 px
lg: @5xl: 1024 px
xl: @7xl: 1280 px
2xl: @min-[96rem]: 1536 px
max-md: @max-3xl: below 768 px
max-lg: @max-5xl: below 1024 px
export function Hero({ className = '' }: { className?: string }) {
  return <section className={`px-6 py-16 md:px-16 lg:py-24 ${className}`} />
}
export function Hero({ className = '' }: { className?: string }) {
  return <section className={`px-6 py-16 @3xl:px-16 @5xl:py-24 ${className}`} />
}

lint/screen-unit

A class uses the viewport size: h-screen, min-h-screen, w-screen, h-dvh, or vh, vw, dvh or svh in an arbitrary value. In the editor, the viewport is the editor window.

  • Fill the frame with size-full or min-h-full on the root.
  • Give the frame a height on the canvas, such as 900 for a desktop app screen.
  • Use a fixed size: h-160 is 640 px.
  • Use cqw for a width that follows the frame: w-[50cqw].
export function Hero({ className = '' }: { className?: string }) {
  return <section className={`flex min-h-screen items-center ${className}`} />
}

lint/fixed

A class uses fixed. A fixed element sticks to the editor window, not to the frame.

  • Use absolute in a root with relative, for an overlay or a floating button.
  • For a header or a tab bar, keep it in the flow at the top or the bottom of the screen.

lint/img-alt

An img has no alt.

  • Describe what the image shows: alt="Two people at a desk with a laptop".
  • Use alt="" for an image that is only decoration.

lint/status-bar

A row looks like a phone status bar: it starts with a time such as 9:41 and has only shapes after it, such as signal, Wi-Fi or battery icons. The device draws the status bar, so a design must not draw one.

  • Remove the row.
  • Do not keep an empty band for it: start the content at the top of the screen.
export function StatusBar({ className = '' }: { className?: string }) {
  return (
    <div className={`flex h-[59px] items-center justify-between px-8 ${className}`}>
      <span>9:41</span>
      <span className="h-3 w-6 rounded-sm bg-foreground" />
    </div>
  )
}

lint/top-band

The root of a screen starts with an empty band of 40 to 64 px: pt-[59px] on the root, or an empty spacer such as h-[54px] as its first child. It is space for a status bar, and it shows as blank padding over the design.

  • Remove the padding or the spacer.
  • Start the content at the top: the navigation bar or the large title first, with pt-3 or pt-4.
export function Home({ className = '' }: { className?: string }) {
  return (
    <div className={`flex size-full flex-col pt-[59px] ${className}`}>
      <h1 className="px-4 text-[34px] font-bold">Home</h1>
    </div>
  )
}

lint/unknown-class

Tailwind does not know the class, or one of its variants. The class stays in the file, but it does nothing on the canvas. The severity is info, because the class can be for your own CSS.

  • Correct the spelling. The message often gives the class that you meant, and fix has it.
  • Use the Tailwind v4 name: bg-linear-to-r, not bg-gradient-to-r; shrink-0, not flex-shrink-0; grow, not flex-grow; bg-black/50, not bg-opacity-50; text-ellipsis, not overflow-ellipsis.
  • Use an arbitrary value for a value that has no class: tracking-[-0.03em].

lint/unknown-token

A class names a token that the theme does not define, such as bg-primary when there is no --color-primary. A var(--name) in an arbitrary value that the theme does not define gives the same warning.

  • Use a token that the theme defines. GetTokens() lists them.
  • Or add the token with SetTokens({ '--color-primary': '#2563eb' }).

lint/theme

The theme block does not compile, so Knighn checks the classes against the default Tailwind theme. This is an error.

  • Write each token as --name: value; in the @theme block.
  • Close each { of the theme block and of each mode block.
  • Read the message: it gives the error of the Tailwind compiler.