- Rust 97.6%
- CSS 2.4%
|
|
||
|---|---|---|
| .agents/skills/qtml | ||
| .github/workflows | ||
| examples | ||
| qtml_build | ||
| qtml_core | ||
| qtml_lsp | ||
| qtml_macro | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
QTML
QTML is a QML-inspired template language for compiling server-rendered Rust templates and native CSS.
The primary entry point is the render_qtml! macro from qtml_macro.
use qtml_macro::{render_qtml, render_qtml_styles};
let page = render_qtml!("ui/dashboard.qtml");
let css = render_qtml_styles!("ui/dashboard.qtml");
Format source with qtml_core before writing it back to disk:
use qtml_core::compiler::format_document;
let formatted = format_document(source)?;
Column {
spacing: 0.75rem
horizontal_alignment: center
Text {
text: "Welcome, {user_name}"
size: 1.5rem
weight: 700
}
Button {
text: "Save"
background: #2563eb
color: white
padding: 0.75rem
border_radius: 0.375rem
}
}
Imports always name the component or token namespace they introduce. QTML classifies an imported module from its declarations:
import "components/page_card.qtml" as PageCard
import "styles/theme.qtml" as theme
Imports are local to the file that declares them. Components and token namespaces must be imported in every file that uses them; imports are not re-exported to parents or siblings.
Imported components declare their public parameters with param. $ reads a
dynamic Rust value or component parameter:
param title: "Untitled"
Box {
Text { text: $title }
}
Transitions use normal CSS values:
Button {
transition: all
duration: 100ms
}
Use parenthesized Rust boolean expressions to conditionally render QTML
content. Parentheses mark the Rust expression boundary; branches support
else if and else:
if (user.is_admin()) {
Text { text: "Admin access" }
} else if (user.is_signed_in()) {
Text { text: "Standard access" }
} else {
Text { text: "Sign in" }
}
Use // for comments. Comments may appear anywhere whitespace is allowed:
// A reusable page heading.
Header1 {
text: "Dashboard" // Rendered as escaped text.
}
For page structure, QTML provides Header, Footer, Section, Article,
Nav, Aside, Text, and Span alongside Page, Box, Grid, Column, and
Row. Use OrderedList, UnorderedList, and ListItem for lists; use Details
and Summary for disclosures. Heading components run from Header1 through
Header6.
Style modules use the same .qtml extension. They can combine reusable design
tokens, dark token overrides, and one project-level defaults block:
// ui/styles/theme.qtml
token background: #f8fafc
token foreground: #0f172a
dark {
token background: #0f172a
token foreground: #f8fafc
}
defaults {
Page {
background: self.background
color: self.foreground
}
Button {
border_radius: 0.5rem
padding_x: 1rem
padding_y: 0.625rem
}
}
Templates import style modules with the same syntax as component modules.
Qualified names are token references, while quoted dotted values remain
literals. Inside a style module's defaults, self.* references tokens declared
in that same module:
import "styles/theme.qtml" as theme
Page { background: theme.background }
Generate component rules and imported token definitions once in the application
shell with render_qtml_styles!("ui/home.qtml"). Imports make tokens available
but do not activate imported defaults. Activate a style module's defaults by
listing it directly; those defaults then apply globally throughout the generated
stylesheet:
const APP_CSS: &str = render_qtml_styles!(
"ui/styles/theme.qtml",
"ui/home.qtml",
"ui/settings.qtml",
);
QTML emits its built-in defaults first, directly listed project defaults next,
and explicit component styles last. When multiple directly listed style modules
set the same default, later macro arguments win. Imported style modules still
emit required token definitions, but their defaults remain inactive unless they
are also listed directly. Dark overrides set CSS custom properties under the
dark class. Use a :dark { ... } style scope for local dark-mode declarations.
Prefix component style scopes with :. Named scopes accept an identifier, and
selector scopes use a bracketed selector. & represents the generated component
class and _ represents a descendant space:
Article {
:hover {
background: #2563eb
}
:[&_h1] {
size: 2.25rem
}
}
Style values are static CSS values or token references. Use brackets for values that contain spaces or punctuation. A parenthesized Rust condition may select between two static values or token references:
Button {
width: [calc(100% - 2rem)]
background: [linear-gradient(to right, #06b6d4, #3b82f6)]
color: (is_active) ? white : gray
}
Unscoped styles are desktop-first. Use :tablet for 768px through 1023px,
:mobile below 768px, and :desktop for an explicit 1024px-and-up rule.
QTML emits the false branch in the component's base class and emits the true branch as a conditionally rendered modifier class. Each conditional property is evaluated independently. Dynamic Rust values remain available for content and attributes, but unrestricted dynamic values are not supported as style values.
See examples/counter, examples/task_app, and examples/theme for working applications.
CSS generation
render_qtml! assigns every built-in a stable class formed by lowercasing its
component name, such as q-button, q-column, or q-header1. These classes are
safe hooks for CSS, JavaScript, and tests. Styled components also receive their
deterministic generated classes. Grid, Column, and Row receive their built-in
layout behavior from their stable classes, so they do not need generated classes
unless they have explicit QTML styles.
render_qtml_styles! includes QTML's clean element defaults, follows the same
template and component imports, deduplicates identical rule sets, and returns the
complete CSS as a compile-time string:
const QTML_STYLES: &str = render_qtml_styles!("ui/home.qtml");
Include that string once in the application shell:
<style>{QTML_STYLES}</style>
The built-in defaults provide a responsive system-font baseline, predictable box sizing,
typography, links, form controls, tables, code blocks, accessible focus states,
and a .dark color palette. They use low-specificity :where(...) selectors, so
directly listed project defaults override them, and styles declared on components
take precedence over both. Base project defaults use zero-specificity
:where(...) selectors; scoped defaults such as :hover use normal selector
specificity so they can override a component's explicit base style, while an
explicit component :hover style still wins. The default palette can also be adjusted
by overriding the --qtml-default-* custom properties.
License
QTML is available under the MIT License.