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)]
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()?;
|
||||
|
|
|
|||
|
|
@ -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
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