helixui prompt DSL
JSX is verbose. When a model is emitting helixui UI inside a chat reply, every angle bracket eats tokens that could go to actual reasoning. The helixui prompt DSL exists to make UI cheap to describe in prose while staying losslessly convertible to JSX.
It is not a templating language and not a runtime. It is a parser + emitter validated against the same components-manifest.json that powers /components.md. If a component, prop, or modifier doesn’t exist in helixui, the parser errors out with a line:column.
A side-by-side
DSL:
Stack gap=4 Text.lg.semibold "Welcome back" Text.muted "Sign in to continue." Form TextInput name=email label=Email TextInput name=password label=Password type=password Button.brand.lg type=submit "Sign in"JSX (emitted):
<Stack gap={4}> <Text size="lg" weight="semibold">Welcome back</Text> <Text tone="muted">Sign in to continue.</Text> <Form> <TextInput name="email" label="Email" /> <TextInput name="password" label="Password" type="password" /> <Button tone="brand" size="lg" type="submit">Sign in</Button> </Form></Stack>How modifiers work
.brand.lg means “set whichever prop’s enum contains brand, then whichever contains lg”. The DSL reads each component’s prop types from the manifest and decides — tone="brand", size="lg". If a value is ambiguous (multiple enums claim it), the parser errors and asks you to spell the prop out.
Children, slots, and booleans
- Indentation = nesting (2 spaces per level).
- Dotted sub-components (
Sidebar.Item,Tabs.Panel) are written verbatim. - A bareword that matches a known boolean prop becomes
prop={true}. Common ones (active,disabled,isOpen) work everywhere. - A trailing
"..."becomes the text child. A trailing{js}becomes a JSX expression child.
CLI
# DSL → JSXpnpm --filter @helixui/prompt run parse <file.dsl># ornode packages/prompt/bin/cli.mjs parse <file.dsl>
# JSX → DSL (lossy, for compact display)node packages/prompt/bin/cli.mjs reverse <file.tsx>Pipe a snippet through stdin: echo 'Button.brand "Save"' | helixui-prompt parse -.
Programmatic API
import { dslToJsx, jsxToDsl, parse, emit } from '@helixui/prompt';
const jsx = await dslToJsx('Button.brand "Save"');// → <Button tone="brand">Save</Button>
const dsl = await jsxToDsl('<Stack gap={4}><Text>Hi</Text></Stack>');// → Stack gap={4}// Text "Hi"
// Parse + emit separately if you want to walk / transform the AST.const ast = await parse(source);const code = emit(ast, { includeImport: true });When not to use it
- For production code, write JSX. The DSL is for prompts and inline conversation.
- For anything you’ll read tomorrow, write JSX. The terseness costs readability after the fact.
- For components that aren’t in helixui, write JSX — the DSL only validates against helixui’s manifest.
Errors you’ll see
| Code | Cause |
|---|---|
UnknownComponent | Not in components-manifest.json. |
UnknownProp | The component (without ...rest) doesn’t declare that prop. |
AmbiguousModifier | A .foo modifier matches multiple enums on the component. Spell it out. |
BadValue | A value doesn’t match the declared type. |
BadIndent | Indent jumped more than one level, or wasn’t a multiple of 2. |
Roundtrip examples
The reference roundtrip set is in packages/prompt/test/parse.test.mjs. Every component’s prompt_examples block in spec.md is also a roundtrip-compatible JSX seed.