feat: each loop, dreamstack init, expanded registry

Language:
- each item in list -> template (reactive list rendering)
- each/in tokens in lexer, Expr::Each in AST
- Reactive forEach codegen with scope push/pop
- Container trailing props: column [...] { variant: card }

CLI:
- dreamstack init [name] - scaffold new project
- Generates app.ds, components/, dreamstack.json
- 4 starter components (button, card, badge, input)

Registry expanded to 11 components:
- NEW: progress, alert, separator, toggle, avatar
- All embedded via include_str!

CSS: progress bar, avatar, separator, alert variants,
toggle switch, stat values (230+ lines design system)

Examples:
- each-demo.ds: list rendering demo
- dashboard.ds: glassmorphism cards with container variant
This commit is contained in:
enzotar 2026-02-26 14:42:00 -08:00
parent a290bc1891
commit 008f164ae7
7 changed files with 168 additions and 1 deletions

View file

@ -86,6 +86,11 @@ enum Commands {
#[arg(short, long)]
output: Option<PathBuf>,
},
/// Initialize a new DreamStack project
Init {
/// Project name (creates directory; default: current dir)
name: Option<String>,
},
}
fn main() {
@ -99,6 +104,7 @@ fn main() {
Commands::Playground { file, port } => cmd_playground(file.as_deref(), port),
Commands::Add { name, list, all } => cmd_add(name, list, all),
Commands::Convert { name, shadcn, output } => cmd_convert(&name, shadcn, output.as_deref()),
Commands::Init { name } => cmd_init(name),
}
}
@ -1310,6 +1316,96 @@ fn add_component(name: &str, dest: &Path, added: &mut std::collections::HashSet<
// ── Converter: dreamstack convert ──
fn cmd_init(name: Option<String>) {
let project_dir = match &name {
Some(n) => PathBuf::from(n),
None => std::env::current_dir().expect("Failed to get current directory"),
};
if name.is_some() {
fs::create_dir_all(&project_dir).expect("Failed to create project directory");
}
let components_dir = project_dir.join("components");
fs::create_dir_all(&components_dir).expect("Failed to create components/ directory");
// Write starter app.ds
let app_source = r#"-- My DreamStack App
let count = 0
let name = ""
view main = column [
-- Header
text "🚀 My App" { variant: "title" }
text "Built with DreamStack" { variant: "subtitle" }
-- Stats
row [
column [
text "Users" { variant: "subtitle" }
text "1,247" { variant: "title" }
] { variant: "card" }
column [
text "Revenue" { variant: "subtitle" }
text "$8,420" { variant: "title" }
] { variant: "card" }
]
-- Input
text "Your name" { variant: "label" }
input { bind: name, placeholder: "Type here..." }
text "Hello, {name}!"
-- Counter
row [
button "Count: {count}" { click: count += 1, variant: "primary" }
button "Reset" { click: count = 0, variant: "ghost" }
]
]
"#;
fs::write(project_dir.join("app.ds"), app_source).expect("Failed to write app.ds");
// Write dreamstack.json
let project_name = name.as_deref().unwrap_or("my-dreamstack-app");
let config = format!(r#"{{
"name": "{}",
"version": "0.1.0",
"entry": "app.ds"
}}
"#, project_name);
fs::write(project_dir.join("dreamstack.json"), config).expect("Failed to write dreamstack.json");
// Add starter components from registry
let starter_components = ["button", "card", "badge", "input"];
for comp_name in &starter_components {
if let Some(item) = REGISTRY.iter().find(|r| r.name == *comp_name) {
let comp_path = components_dir.join(format!("{}.ds", comp_name));
fs::write(&comp_path, item.source).expect("Failed to write component");
}
}
let display_name = name.as_deref().unwrap_or(".");
println!("🚀 DreamStack project initialized in {}/\n", display_name);
println!(" Created:");
println!(" app.ds — your main application");
println!(" dreamstack.json — project config");
println!(" components/button.ds — button component");
println!(" components/card.ds — card component");
println!(" components/badge.ds — badge component");
println!(" components/input.ds — input component\n");
println!(" Next steps:");
if name.is_some() {
println!(" cd {}", display_name);
}
println!(" dreamstack build app.ds -o dist");
println!(" dreamstack dev app.ds");
println!(" dreamstack add --list # see all 11 components");
println!(" dreamstack add dialog # add with deps\n");
}
fn cmd_convert(name: &str, shadcn: bool, output: Option<&Path>) {
let tsx_source = if shadcn {
// Fetch from shadcn/ui GitHub

View file

@ -771,6 +771,38 @@ impl JsEmitter {
anchor_var
}
// Each loop: `each item in list -> template`
Expr::Each(item_name, list_expr, body) => {
let container_var = self.next_node_id();
let iter_js = self.emit_expr(list_expr);
let iter_var = self.next_node_id();
self.emit_line(&format!("const {} = document.createElement('div');", container_var));
self.emit_line(&format!("{}.className = 'ds-each-list';", container_var));
self.emit_line("DS.effect(() => {");
self.indent += 1;
self.emit_line(&format!("const {} = {};", iter_var, iter_js));
self.emit_line(&format!("{}.innerHTML = '';", container_var));
self.emit_line(&format!(
"const __list = ({0} && {0}.value !== undefined) ? {0}.value : (Array.isArray({0}) ? {0} : []);",
iter_var
));
self.emit_line(&format!("__list.forEach(({item_name}, _idx) => {{"));
self.indent += 1;
self.push_scope(&[item_name.as_str()]);
let child_var = self.emit_view_expr(body, graph);
self.emit_line(&format!("{}.appendChild({});", container_var, child_var));
self.pop_scope();
self.indent -= 1;
self.emit_line("});");
self.indent -= 1;
self.emit_line("});");
container_var
}
// ForIn reactive list: `for item in items -> body`
Expr::ForIn { item, index, iter, body } => {
let container_var = self.next_node_id();

View file

@ -263,6 +263,9 @@ pub enum Expr {
Container(Container),
/// When conditional: `when count > 10 -> ...`
When(Box<Expr>, Box<Expr>),
/// Each loop: `each item in list => template`
Each(String, Box<Expr>, Box<Expr>),
/// Match expression
Match(Box<Expr>, Vec<MatchArm>),
/// Pipe: `expr | operator`

View file

@ -27,6 +27,8 @@ pub enum TokenKind {
Effect,
On,
When,
Each,
InKw,
Match,
If,
Then,
@ -306,6 +308,8 @@ impl Lexer {
"effect" => TokenKind::Effect,
"on" => TokenKind::On,
"when" => TokenKind::When,
"each" => TokenKind::Each,
"in" => TokenKind::InKw,
"match" => TokenKind::Match,
"if" => TokenKind::If,
"then" => TokenKind::Then,

View file

@ -894,7 +894,17 @@ impl Parser {
Ok(Expr::When(Box::new(cond), Box::new(body)))
}
// Match
// Each loop: `each item in list => template`
TokenKind::Each => {
self.advance();
let item_name = self.expect_ident()?;
self.expect(&TokenKind::InKw)?;
let list_expr = self.parse_primary()?;
self.expect(&TokenKind::Arrow)?;
self.skip_newlines();
let body = self.parse_expr()?;
Ok(Expr::Each(item_name, Box::new(list_expr), Box::new(body)))
}
TokenKind::Match => {
self.advance();
let scrutinee = self.parse_primary()?;

View file

@ -670,6 +670,11 @@ impl TypeChecker {
self.infer_expr(body)
}
Expr::Each(_, list, body) => {
let _ = self.infer_expr(list);
self.infer_expr(body)
}
Expr::ComponentUse { props, children, .. } => {
for (_, val) in props {
self.infer_expr(val);

17
examples/each-demo.ds Normal file
View file

@ -0,0 +1,17 @@
-- Each Loop Demo
-- Demonstrates list rendering with each ... in ... -> syntax
let todos = ["Buy groceries", "Write DreamStack docs", "Ship v1.0", "Deploy to prod"]
view main = column [
text "📋 Todo List" { variant: "title" }
text "4 items" { variant: "subtitle" }
each todo in todos ->
row [
text "•"
text todo
]
button "Done" { variant: "primary" }
]