feat: port el-ui vessels — rename crates→vessels, add El source + manifests
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user