feat: port el-ui vessels — rename crates→vessels, add El source + manifests

This commit is contained in:
Will Anderson
2026-05-05 04:19:22 -05:00
parent b580a63540
commit faee6fdb25
145 changed files with 4050 additions and 12 deletions
+318
View File
@@ -0,0 +1,318 @@
// el-layout Responsive layout engine for el-ui.
//
// Primitives:
// VStack, HStack, ZStack stack layouts (wrap by default)
// GridLayout responsive grid
// ScrollView scrollable container
// Responsive<T> value that changes by breakpoint
// Breakpoints
let BP_XS: String = "xs" // < 640px
let BP_SM: String = "sm" // >= 640px
let BP_MD: String = "md" // >= 768px
let BP_LG: String = "lg" // >= 1024px
let BP_XL: String = "xl" // >= 1280px
let BP_XXL: String = "xxl" // >= 1536px
let BP_SM_PX: Int = 640
let BP_MD_PX: Int = 768
let BP_LG_PX: Int = 1024
let BP_XL_PX: Int = 1280
let BP_XXL_PX: Int = 1536
fn breakpoint_for_width(width_px: Int) -> String {
if width_px >= BP_XXL_PX { return BP_XXL }
if width_px >= BP_XL_PX { return BP_XL }
if width_px >= BP_LG_PX { return BP_LG }
if width_px >= BP_MD_PX { return BP_MD }
if width_px >= BP_SM_PX { return BP_SM }
BP_XS
}
// Cascade index used by Responsive<T> to find the most-specific value <= bp.
fn breakpoint_index(bp: String) -> Int {
if str_eq(bp, "xs") { return 0 }
if str_eq(bp, "sm") { return 1 }
if str_eq(bp, "md") { return 2 }
if str_eq(bp, "lg") { return 3 }
if str_eq(bp, "xl") { return 4 }
if str_eq(bp, "xxl") { return 5 }
0
}
// Responsive<T>
//
// Stored as a JSON object: { "xs": v0, "md": v1, "lg": v2 }
// resolve(bp) walks down from bp until it finds a defined value.
fn responsive_fixed(value: String) -> String {
"{\"xs\":" + value + "}"
}
fn responsive_set(rv: String, bp: String, value: String) -> String {
json_set(rv, bp, value)
}
fn responsive_resolve(rv: String, bp: String) -> String {
let order: String = "xxl,xl,lg,md,sm,xs"
let target_idx: Int = breakpoint_index(bp)
// Try each breakpoint <= target, most-specific first.
let probe: String = bp
while !str_eq(probe, "") {
let v: String = json_get(rv, probe)
if !str_eq(v, "") { return v }
let idx: Int = breakpoint_index(probe)
if idx == 0 { return "" }
let probe = breakpoint_step_down(probe)
}
""
}
fn breakpoint_step_down(bp: String) -> String {
if str_eq(bp, "xxl") { return "xl" }
if str_eq(bp, "xl") { return "lg" }
if str_eq(bp, "lg") { return "md" }
if str_eq(bp, "md") { return "sm" }
if str_eq(bp, "sm") { return "xs" }
""
}
// Constraints / Size
type Size {
width: Int
height: Int
}
type LayoutConstraints {
min_width: Int
max_width: Int
min_height: Int
max_height: Int
}
fn constraints_unbounded() -> LayoutConstraints {
{ "min_width": 0, "max_width": 999999, "min_height": 0, "max_height": 999999 }
}
fn constraints_tight(w: Int, h: Int) -> LayoutConstraints {
{ "min_width": w, "max_width": w, "min_height": h, "max_height": h }
}
// Flex axes
let FLEX_ROW: String = "row"
let FLEX_COLUMN: String = "column"
let MAIN_START: String = "start"
let MAIN_END: String = "end"
let MAIN_CENTER: String = "center"
let MAIN_BETWEEN: String = "space-between"
let MAIN_AROUND: String = "space-around"
let MAIN_EVENLY: String = "space-evenly"
let CROSS_START: String = "start"
let CROSS_END: String = "end"
let CROSS_CENTER: String = "center"
let CROSS_STRETCH: String = "stretch"
let CROSS_BASELINE: String = "baseline"
type FlexLayout {
direction: String
main_alignment: String
cross_alignment: String
gap_px: Int
wrap: Bool
}
fn flex_default() -> FlexLayout {
{ "direction": "row", "main_alignment": "start",
"cross_alignment": "stretch", "gap_px": 8, "wrap": true }
}
// Stacks
//
// VStack / HStack / ZStack are component classes in the JS runtime; the
// El side here exposes their layout descriptor the bag of values the
// el-ui-compiler emits into JSX/HTML attributes.
type StackLayout {
direction: String // row | column | depth
main_alignment: String
cross_alignment: String
gap_px: Int
wrap: Bool
spacing_token: String // semantic spacing token (md, lg, ...)
}
fn vstack(spacing_token: String) -> StackLayout {
{ "direction": "column", "main_alignment": "start",
"cross_alignment": "stretch", "gap_px": 0, "wrap": true,
"spacing_token": spacing_token }
}
fn hstack(spacing_token: String) -> StackLayout {
{ "direction": "row", "main_alignment": "start",
"cross_alignment": "center", "gap_px": 0, "wrap": true,
"spacing_token": spacing_token }
}
fn zstack() -> StackLayout {
{ "direction": "depth", "main_alignment": "center",
"cross_alignment": "center", "gap_px": 0, "wrap": false,
"spacing_token": "" }
}
// Grid
type GridLayout {
columns: String // "auto" or "1fr 1fr 1fr" etc
rows: String
gap_px: Int
auto_fit_min: Int // for `repeat(auto-fit, minmax(<min>, 1fr))`
}
fn grid_auto(min_col_px: Int, gap: Int) -> GridLayout {
{ "columns": "auto-fit", "rows": "auto", "gap_px": gap, "auto_fit_min": min_col_px }
}
fn grid_fixed(num_cols: Int, gap: Int) -> GridLayout {
let cols: String = repeat_str("1fr ", num_cols)
{ "columns": cols, "rows": "auto", "gap_px": gap, "auto_fit_min": 0 }
}
fn grid_to_css(g: GridLayout) -> String {
let cols: String = g.columns
if str_eq(cols, "auto-fit") {
let cols = "repeat(auto-fit, minmax(" + int_to_str(g.auto_fit_min) + "px, 1fr))"
}
"display: grid; grid-template-columns: " + cols + "; gap: " + int_to_str(g.gap_px) + "px;"
}
// ScrollView
let SCROLL_X: String = "x"
let SCROLL_Y: String = "y"
let SCROLL_BOTH: String = "both"
type ScrollView {
axis: String
show_indicator: Bool
bounce: Bool // iOS-style overscroll
}
fn scroll_view_y() -> ScrollView {
{ "axis": "y", "show_indicator": true, "bounce": true }
}
// Platform sizing
let PLATFORM_PHONE: String = "phone"
let PLATFORM_TABLET: String = "tablet"
let PLATFORM_DESKTOP: String = "desktop"
type SafeAreaInsets {
top: Int
right: Int
bottom: Int
left: Int
}
fn safe_area_zero() -> SafeAreaInsets {
{ "top": 0, "right": 0, "bottom": 0, "left": 0 }
}
fn platform_for_width(width: Int) -> String {
if width < 768 { return "phone" }
if width < 1024 { return "tablet" }
"desktop"
}
// HTML emit
//
// Server-side render: layout descriptor HTML string.
// Children is a pre-rendered HTML string passed in by the caller.
// class_extra is an optional additional CSS class string (pass "" for none).
fn stack_direction_to_css(direction: String) -> String {
if str_eq(direction, "column") { return "column" }
if str_eq(direction, "depth") { return "unset" }
"row"
}
fn stack_align_to_css(align: String) -> String {
if str_eq(align, "center") { return "center" }
if str_eq(align, "end") { return "flex-end" }
if str_eq(align, "baseline") { return "baseline" }
if str_eq(align, "stretch") { return "stretch" }
"flex-start"
}
fn stack_justify_to_css(align: String) -> String {
if str_eq(align, "center") { return "center" }
if str_eq(align, "end") { return "flex-end" }
if str_eq(align, "space-between") { return "space-between" }
if str_eq(align, "space-around") { return "space-around" }
if str_eq(align, "space-evenly") { return "space-evenly" }
"flex-start"
}
fn stack_to_html(layout: StackLayout, children: String, class_extra: String) -> String {
let direction: String = stack_direction_to_css(layout.direction)
let justify: String = stack_justify_to_css(layout.main_alignment)
let align: String = stack_align_to_css(layout.cross_alignment)
let gap: String = int_to_str(layout.gap_px) + "px"
let wrap_val: String = if layout.wrap { "wrap" } else { "nowrap" }
let style: String = "display:flex;flex-direction:" + direction
+ ";justify-content:" + justify
+ ";align-items:" + align
+ ";gap:" + gap
+ ";flex-wrap:" + wrap_val
let base_class: String = "el-stack el-stack--" + layout.direction
let cls: String = if str_eq(class_extra, "") { base_class } else { base_class + " " + class_extra }
"<div class=\"" + cls + "\" style=\"" + style + "\">" + children + "</div>"
}
fn vstack_to_html(spacing_px: Int, children: String, class_extra: String) -> String {
let layout: StackLayout = { "direction": "column", "main_alignment": "start",
"cross_alignment": "stretch", "gap_px": spacing_px, "wrap": true, "spacing_token": "" }
stack_to_html(layout, children, class_extra)
}
fn hstack_to_html(spacing_px: Int, children: String, class_extra: String) -> String {
let layout: StackLayout = { "direction": "row", "main_alignment": "start",
"cross_alignment": "center", "gap_px": spacing_px, "wrap": true, "spacing_token": "" }
stack_to_html(layout, children, class_extra)
}
fn zstack_to_html(children: String, class_extra: String) -> String {
let base_class: String = "el-stack el-stack--depth"
let cls: String = if str_eq(class_extra, "") { base_class } else { base_class + " " + class_extra }
"<div class=\"" + cls + "\" style=\"display:grid;place-items:center\">" + children + "</div>"
}
fn grid_to_html(layout: GridLayout, children: String, class_extra: String) -> String {
let css: String = grid_to_css(layout)
let base_class: String = "el-grid"
let cls: String = if str_eq(class_extra, "") { base_class } else { base_class + " " + class_extra }
"<div class=\"" + cls + "\" style=\"" + css + "\">" + children + "</div>"
}
fn scroll_to_html(layout: ScrollView, children: String, class_extra: String) -> String {
let overflow: String = if str_eq(layout.axis, "x") { "overflow-x:auto;overflow-y:hidden" }
else if str_eq(layout.axis, "both") { "overflow:auto" }
else { "overflow-x:hidden;overflow-y:auto" }
let base_class: String = "el-scroll"
let cls: String = if str_eq(class_extra, "") { base_class } else { base_class + " " + class_extra }
"<div class=\"" + cls + "\" style=\"" + overflow + "\">" + children + "</div>"
}
// Entry smoke test
let r: String = responsive_fixed("1")
let r = responsive_set(r, "md", "2")
let r = responsive_set(r, "lg", "3")
println("[el-layout] cols at lg = " + responsive_resolve(r, "lg"))
let v: String = vstack_to_html(16, "<p>hello</p>", "")
println("[el-layout] vstack = " + v)