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:
parent
a290bc1891
commit
008f164ae7
7 changed files with 168 additions and 1 deletions
|
|
@ -86,6 +86,11 @@ enum Commands {
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
output: Option<PathBuf>,
|
output: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
|
/// Initialize a new DreamStack project
|
||||||
|
Init {
|
||||||
|
/// Project name (creates directory; default: current dir)
|
||||||
|
name: Option<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
@ -99,6 +104,7 @@ fn main() {
|
||||||
Commands::Playground { file, port } => cmd_playground(file.as_deref(), port),
|
Commands::Playground { file, port } => cmd_playground(file.as_deref(), port),
|
||||||
Commands::Add { name, list, all } => cmd_add(name, list, all),
|
Commands::Add { name, list, all } => cmd_add(name, list, all),
|
||||||
Commands::Convert { name, shadcn, output } => cmd_convert(&name, shadcn, output.as_deref()),
|
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 ──
|
// ── 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>) {
|
fn cmd_convert(name: &str, shadcn: bool, output: Option<&Path>) {
|
||||||
let tsx_source = if shadcn {
|
let tsx_source = if shadcn {
|
||||||
// Fetch from shadcn/ui GitHub
|
// Fetch from shadcn/ui GitHub
|
||||||
|
|
|
||||||
|
|
@ -771,6 +771,38 @@ impl JsEmitter {
|
||||||
anchor_var
|
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`
|
// ForIn reactive list: `for item in items -> body`
|
||||||
Expr::ForIn { item, index, iter, body } => {
|
Expr::ForIn { item, index, iter, body } => {
|
||||||
let container_var = self.next_node_id();
|
let container_var = self.next_node_id();
|
||||||
|
|
|
||||||
|
|
@ -263,6 +263,9 @@ pub enum Expr {
|
||||||
Container(Container),
|
Container(Container),
|
||||||
/// When conditional: `when count > 10 -> ...`
|
/// When conditional: `when count > 10 -> ...`
|
||||||
When(Box<Expr>, Box<Expr>),
|
When(Box<Expr>, Box<Expr>),
|
||||||
|
|
||||||
|
/// Each loop: `each item in list => template`
|
||||||
|
Each(String, Box<Expr>, Box<Expr>),
|
||||||
/// Match expression
|
/// Match expression
|
||||||
Match(Box<Expr>, Vec<MatchArm>),
|
Match(Box<Expr>, Vec<MatchArm>),
|
||||||
/// Pipe: `expr | operator`
|
/// Pipe: `expr | operator`
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ pub enum TokenKind {
|
||||||
Effect,
|
Effect,
|
||||||
On,
|
On,
|
||||||
When,
|
When,
|
||||||
|
Each,
|
||||||
|
InKw,
|
||||||
Match,
|
Match,
|
||||||
If,
|
If,
|
||||||
Then,
|
Then,
|
||||||
|
|
@ -306,6 +308,8 @@ impl Lexer {
|
||||||
"effect" => TokenKind::Effect,
|
"effect" => TokenKind::Effect,
|
||||||
"on" => TokenKind::On,
|
"on" => TokenKind::On,
|
||||||
"when" => TokenKind::When,
|
"when" => TokenKind::When,
|
||||||
|
"each" => TokenKind::Each,
|
||||||
|
"in" => TokenKind::InKw,
|
||||||
"match" => TokenKind::Match,
|
"match" => TokenKind::Match,
|
||||||
"if" => TokenKind::If,
|
"if" => TokenKind::If,
|
||||||
"then" => TokenKind::Then,
|
"then" => TokenKind::Then,
|
||||||
|
|
|
||||||
|
|
@ -894,7 +894,17 @@ impl Parser {
|
||||||
Ok(Expr::When(Box::new(cond), Box::new(body)))
|
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 => {
|
TokenKind::Match => {
|
||||||
self.advance();
|
self.advance();
|
||||||
let scrutinee = self.parse_primary()?;
|
let scrutinee = self.parse_primary()?;
|
||||||
|
|
|
||||||
|
|
@ -670,6 +670,11 @@ impl TypeChecker {
|
||||||
self.infer_expr(body)
|
self.infer_expr(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Expr::Each(_, list, body) => {
|
||||||
|
let _ = self.infer_expr(list);
|
||||||
|
self.infer_expr(body)
|
||||||
|
}
|
||||||
|
|
||||||
Expr::ComponentUse { props, children, .. } => {
|
Expr::ComponentUse { props, children, .. } => {
|
||||||
for (_, val) in props {
|
for (_, val) in props {
|
||||||
self.infer_expr(val);
|
self.infer_expr(val);
|
||||||
|
|
|
||||||
17
examples/each-demo.ds
Normal file
17
examples/each-demo.ds
Normal 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" }
|
||||||
|
]
|
||||||
Loading…
Add table
Reference in a new issue