-
Notifications
You must be signed in to change notification settings - Fork 0
GPT gen: add enhanced CSS properties and builders #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds enhanced CSS properties and builders to the respo_css library, introducing support for advanced CSS features like gradients, shadows, keyframe animations, and clip paths.
Key changes:
- New enhanced CSS property types (border radius, box shadow, gradients, transitions, keyframes, clip paths, text shadows, pointer events, letter spacing)
- Integration of enhanced properties into the main
respo_stylefunction - Test coverage for core enhanced features
- Version bump from 0.1.3 to 0.1.4
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| respo_css_enhanced.mbt | New file defining enhanced CSS property types and builders including CssBorderRadius, CssBoxShadow, CssGradient, CssKeyframes, CssClipPath, CssTextShadow, CssPointerEvents, CssLetterSpacing, CssTransitionProperty, and CssBackgroundImage |
| respo_css_enhanced_test.mbt | New test file with basic tests for border radius, box shadow, linear gradient, and keyframes functionality |
| respo_css.mbt | Integration of enhanced properties as optional parameters in the respo_style function |
| moon.mod.json | Version bump from 0.1.3 to 0.1.4 |
| Agents.md | Added comprehensive MoonBit project documentation (appears to be generated) |
| .github/workflows/release.yml | New GitHub Actions workflow for automated package publishing |
| .gitattributes | Git configuration to mark certain files as linguist-generated |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| background_image? : CssBackgroundImage, | ||
| clip_path? : CssClipPath, | ||
| text_shadow? : CssTextShadow, | ||
| letter_spacing_type? : CssLetterSpacing, |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameter name "letter_spacing_type" is inconsistent with the naming pattern used for other similar parameters. Other parameters are named after the property they represent (e.g., "border_radius_obj", "box_shadow_obj", "background_image", "clip_path", "text_shadow", "pointer_events"). The "_type" suffix doesn't add value here and breaks the naming consistency. Consider renaming to "letter_spacing" to match the pattern.
| letter_spacing_type? : CssLetterSpacing, | |
| letter_spacing? : CssLetterSpacing, |
| if text_shadow is Some(value) { | ||
| style.push(("text-shadow", value.to_string())) | ||
| } | ||
| if letter_spacing_type is Some(value) { |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable name "letter_spacing_type" should be consistent with the parameter name. If the parameter is renamed to "letter_spacing" (as suggested in another comment), this should also be updated to "letter_spacing" for consistency.
| if letter_spacing_type is Some(value) { | |
| if letter_spacing is Some(value) { |
| ///| | ||
| /// Enhanced CSS Properties and Builders for respo_css | ||
| ///| | ||
| /// Border Radius Properties | ||
| pub(all) struct CssBorderRadius { | ||
| top_left : CssSize? | ||
| top_right : CssSize? | ||
| bottom_left : CssSize? | ||
| bottom_right : CssSize? | ||
| } | ||
|
|
||
| ///| | ||
| pub fn CssBorderRadius::new( | ||
| top_left? : CssSize, | ||
| top_right? : CssSize, | ||
| bottom_left? : CssSize, | ||
| bottom_right? : CssSize, | ||
| ) -> CssBorderRadius { | ||
| CssBorderRadius::{ top_left, top_right, bottom_left, bottom_right } | ||
| } | ||
|
|
||
| ///| | ||
| pub fn CssBorderRadius::to_string(self : CssBorderRadius) -> String { | ||
| let parts = [] | ||
| if self.top_left is Some(v) { | ||
| parts.push("border-top-left-radius: " + v.to_string()) | ||
| } | ||
| if self.top_right is Some(v) { | ||
| parts.push("border-top-right-radius: " + v.to_string()) | ||
| } | ||
| if self.bottom_left is Some(v) { | ||
| parts.push("border-bottom-left-radius: " + v.to_string()) | ||
| } | ||
| if self.bottom_right is Some(v) { | ||
| parts.push("border-bottom-right-radius: " + v.to_string()) | ||
| } | ||
| parts.join(";") | ||
| } | ||
|
|
||
| ///| | ||
| pub fn CssBorderRadius::to_styles( | ||
| self : CssBorderRadius, | ||
| ) -> Array[(String, String)] { | ||
| let styles = [] | ||
| if self.top_left is Some(v) { | ||
| styles.push(("border-top-left-radius", v.to_string())) | ||
| } | ||
| if self.top_right is Some(v) { | ||
| styles.push(("border-top-right-radius", v.to_string())) | ||
| } | ||
| if self.bottom_left is Some(v) { | ||
| styles.push(("border-bottom-left-radius", v.to_string())) | ||
| } | ||
| if self.bottom_right is Some(v) { | ||
| styles.push(("border-bottom-right-radius", v.to_string())) | ||
| } | ||
| styles | ||
| } | ||
|
|
||
| ///| | ||
| /// Box Shadow Builder | ||
| pub(all) enum CssBoxShadowType { | ||
| Outer | ||
| Inset | ||
| } derive(Eq) | ||
|
|
||
| ///| | ||
| pub(all) struct CssBoxShadow { | ||
| x : CssSize | ||
| y : CssSize | ||
| blur : CssSize | ||
| spread : CssSize? | ||
| color : CssColor | ||
| shadow_type : CssBoxShadowType | ||
| } | ||
|
|
||
| ///| | ||
| pub fn CssBoxShadow::new( | ||
| x? : CssSize = Px(0.0), | ||
| y? : CssSize = Px(0.0), | ||
| blur? : CssSize = Px(0.0), | ||
| spread? : CssSize, | ||
| color? : CssColor = Black, | ||
| shadow_type? : CssBoxShadowType = Outer, | ||
| ) -> CssBoxShadow { | ||
| CssBoxShadow::{ x, y, blur, spread, color, shadow_type } | ||
| } | ||
|
|
||
| ///| | ||
| pub impl Show for CssBoxShadow with output(self, logger) { | ||
| let mut ret = "" | ||
| if self.shadow_type == Inset { | ||
| ret = ret + "inset " | ||
| } | ||
| ret = ret + | ||
| self.x.to_string() + | ||
| " " + | ||
| self.y.to_string() + | ||
| " " + | ||
| self.blur.to_string() + | ||
| " " | ||
| if self.spread is Some(v) { | ||
| ret = ret + v.to_string() + " " | ||
| } | ||
| ret = ret + self.color.to_string() | ||
| logger.write_string(ret) | ||
| } | ||
|
|
||
| ///| | ||
| /// Linear Gradient Builder | ||
| pub(all) enum CssGradientAngle { | ||
| Deg(Int) | ||
| Direction(String) | ||
| } derive(Eq) | ||
|
|
||
| ///| | ||
| pub(all) struct CssGradientStop { | ||
| color : CssColor | ||
| position : CssSize? | ||
| } derive(Eq) | ||
|
|
||
| ///| | ||
| pub fn CssGradientStop::new( | ||
| color : CssColor, | ||
| position? : CssSize, | ||
| ) -> CssGradientStop { | ||
| CssGradientStop::{ color, position } | ||
| } | ||
|
|
||
| ///| | ||
| pub(all) enum CssGradient { | ||
| Linear(CssGradientAngle?, Array[CssGradientStop]) | ||
| Radial(Array[CssGradientStop]) | ||
| Conic(CssGradientAngle?, Array[CssGradientStop]) | ||
| } derive(Eq) | ||
|
|
||
| ///| | ||
| pub impl Show for CssGradient with output(self, logger) { | ||
| let ret = match self { | ||
| Linear(angle, stops) => { | ||
| let mut result = "linear-gradient(" | ||
| match angle { | ||
| Some(Deg(d)) => result = result + d.to_string() + "deg, " | ||
| Some(Direction(dir)) => result = result + dir + ", " | ||
| None => () | ||
| } | ||
| let stop_strs = stops.map(fn(stop) { | ||
| let color_str = stop.color.to_string() | ||
| match stop.position { | ||
| Some(pos) => color_str + " " + pos.to_string() | ||
| None => color_str | ||
| } | ||
| }) | ||
| result = result + stop_strs.join(", ") | ||
| result + ")" | ||
| } | ||
| Radial(stops) => { | ||
| let mut result = "radial-gradient(" | ||
| let stop_strs = stops.map(fn(stop) { | ||
| let color_str = stop.color.to_string() | ||
| match stop.position { | ||
| Some(pos) => color_str + " " + pos.to_string() | ||
| None => color_str | ||
| } | ||
| }) | ||
| result = result + stop_strs.join(", ") | ||
| result + ")" | ||
| } | ||
| Conic(angle, stops) => { | ||
| let mut result = "conic-gradient(" | ||
| match angle { | ||
| Some(Deg(d)) => result = result + "from " + d.to_string() + "deg, " | ||
| Some(Direction(dir)) => result = result + "from " + dir + ", " | ||
| None => () | ||
| } | ||
| let stop_strs = stops.map(fn(stop) { | ||
| let color_str = stop.color.to_string() | ||
| match stop.position { | ||
| Some(pos) => color_str + " " + pos.to_string() | ||
| None => color_str | ||
| } | ||
| }) | ||
| result = result + stop_strs.join(", ") | ||
| result + ")" | ||
| } | ||
| } | ||
| logger.write_string(ret) | ||
| } | ||
|
|
||
| ///| | ||
| /// Transition Builder | ||
| pub(all) struct CssTransitionProperty { | ||
| property : String | ||
| duration : CssDuration | ||
| timing_function : CssTimingFunction? | ||
| delay : CssDuration? | ||
| } | ||
|
|
||
| ///| | ||
| pub fn CssTransitionProperty::new( | ||
| property : String, | ||
| duration : CssDuration, | ||
| timing_function? : CssTimingFunction, | ||
| delay? : CssDuration, | ||
| ) -> CssTransitionProperty { | ||
| CssTransitionProperty::{ property, duration, timing_function, delay } | ||
| } | ||
|
|
||
| ///| | ||
| pub impl Show for CssTransitionProperty with output(self, logger) { | ||
| let mut ret = self.property + " " + self.duration.to_string() | ||
| if self.timing_function is Some(tf) { | ||
| ret = ret + " " + tf.to_string() | ||
| } | ||
| if self.delay is Some(d) { | ||
| ret = ret + " " + d.to_string() | ||
| } | ||
| logger.write_string(ret) | ||
| } | ||
|
|
||
| ///| | ||
| /// Keyframes Definition | ||
| pub(all) struct CssKeyframe { | ||
| position : Int | ||
| styles : Array[(String, String)] | ||
| } | ||
|
|
||
| ///| | ||
| pub fn CssKeyframe::new( | ||
| position : Int, | ||
| styles : Array[(String, String)], | ||
| ) -> CssKeyframe { | ||
| CssKeyframe::{ position, styles } | ||
| } | ||
|
|
||
| ///| | ||
| pub(all) struct CssKeyframes { | ||
| name : String | ||
| keyframes : Array[CssKeyframe] | ||
| } | ||
|
|
||
| ///| | ||
| pub fn CssKeyframes::new( | ||
| name : String, | ||
| keyframes : Array[CssKeyframe], | ||
| ) -> CssKeyframes { | ||
| CssKeyframes::{ name, keyframes } | ||
| } | ||
|
|
||
| ///| | ||
| pub fn CssKeyframes::to_css_string(self : CssKeyframes) -> String { | ||
| let mut css = "@keyframes " + self.name + " {\n" | ||
| for keyframe in self.keyframes { | ||
| css = css + " " + keyframe.position.to_string() + "% {\n" | ||
| for pair in keyframe.styles { | ||
| let (prop, value) = pair | ||
| css = css + " " + prop + ": " + value + ";\n" | ||
| } | ||
| css = css + " }\n" | ||
| } | ||
| css = css + "}\n" | ||
| css | ||
| } | ||
|
|
||
| ///| | ||
| /// Pointer Events | ||
| pub(all) enum CssPointerEvents { | ||
| Auto | ||
| None | ||
| Visible | ||
| Painted | ||
| Fill | ||
| Stroke | ||
| All | ||
| Inherit | ||
| } derive(Eq) | ||
|
|
||
| ///| | ||
| pub impl Show for CssPointerEvents with output(self, logger) { | ||
| let ret = match self { | ||
| Auto => "auto" | ||
| None => "none" | ||
| Visible => "visible" | ||
| Painted => "painted" | ||
| Fill => "fill" | ||
| Stroke => "stroke" | ||
| All => "all" | ||
| Inherit => "inherit" | ||
| } | ||
| logger.write_string(ret) | ||
| } | ||
|
|
||
| ///| | ||
| /// Letter Spacing | ||
| pub(all) enum CssLetterSpacing { | ||
| Normal | ||
| Value(CssSize) | ||
| Custom(String) | ||
| } derive(Eq) | ||
|
|
||
| ///| | ||
| pub impl Show for CssLetterSpacing with output(self, logger) { | ||
| let ret = match self { | ||
| Normal => "normal" | ||
| Value(size) => size.to_string() | ||
| Custom(value) => value | ||
| } | ||
| logger.write_string(ret) | ||
| } | ||
|
|
||
| ///| | ||
| /// Text Shadow | ||
| pub(all) struct CssTextShadow { | ||
| x : CssSize | ||
| y : CssSize | ||
| blur : CssSize | ||
| color : CssColor | ||
| } | ||
|
|
||
| ///| | ||
| pub fn CssTextShadow::new( | ||
| x? : CssSize = Px(0.0), | ||
| y? : CssSize = Px(0.0), | ||
| blur? : CssSize = Px(0.0), | ||
| color? : CssColor = Black, | ||
| ) -> CssTextShadow { | ||
| CssTextShadow::{ x, y, blur, color } | ||
| } | ||
|
|
||
| ///| | ||
| pub impl Show for CssTextShadow with output(self, logger) { | ||
| let ret = self.x.to_string() + | ||
| " " + | ||
| self.y.to_string() + | ||
| " " + | ||
| self.blur.to_string() + | ||
| " " + | ||
| self.color.to_string() | ||
| logger.write_string(ret) | ||
| } | ||
|
|
||
| ///| | ||
| /// Clip Path | ||
| pub(all) enum CssClipPath { | ||
| Circle(CssSize, CssSize, CssSize) | ||
| Ellipse(CssSize, CssSize, CssSize, CssSize) | ||
| Inset(CssSize, CssSize, CssSize, CssSize) | ||
| Polygon(Array[String]) | ||
| Url(String) | ||
| Custom(String) | ||
| } derive(Eq) | ||
|
|
||
| ///| | ||
| pub impl Show for CssClipPath with output(self, logger) { | ||
| let ret = match self { | ||
| Circle(radius, x, y) => { | ||
| let r = radius.to_string() | ||
| let px = x.to_string() | ||
| let py = y.to_string() | ||
| "circle(" + r + " at " + px + " " + py + ")" | ||
| } | ||
| Ellipse(rx, ry, x, y) => { | ||
| let rx_str = rx.to_string() | ||
| let ry_str = ry.to_string() | ||
| let px = x.to_string() | ||
| let py = y.to_string() | ||
| "ellipse(" + rx_str + " " + ry_str + " at " + px + " " + py + ")" | ||
| } | ||
| Inset(top, right, bottom, left) => { | ||
| let t = top.to_string() | ||
| let r = right.to_string() | ||
| let b = bottom.to_string() | ||
| let l = left.to_string() | ||
| "inset(" + t + " " + r + " " + b + " " + l + ")" | ||
| } | ||
| Polygon(points) => "polygon(" + points.join(", ") + ")" | ||
| Url(url) => "url(" + url + ")" | ||
| Custom(value) => value | ||
| } | ||
| logger.write_string(ret) | ||
| } | ||
|
|
||
| ///| | ||
| /// Background Image | ||
| pub(all) enum CssBackgroundImage { | ||
| LinearGradient(CssGradient) | ||
| RadialGradient(CssGradient) | ||
| ConicGradient(CssGradient) | ||
| Url(String) | ||
| Multiple(Array[CssBackgroundImage]) | ||
| None | ||
| Custom(String) | ||
| } derive(Eq) | ||
|
|
||
| ///| | ||
| pub impl Show for CssBackgroundImage with output(self, logger) { | ||
| let ret = match self { | ||
| LinearGradient(gradient) => gradient.to_string() | ||
| RadialGradient(gradient) => gradient.to_string() | ||
| ConicGradient(gradient) => gradient.to_string() | ||
| Url(url) => "url(" + url + ")" | ||
| Multiple(images) => images.map(fn(img) { img.to_string() }).join(", ") | ||
| None => "none" | ||
| Custom(value) => value | ||
| } | ||
| logger.write_string(ret) | ||
| } |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Several public types lack documentation comments explaining their purpose and usage:
- CssBorderRadius (struct and fields)
- CssBoxShadowType (enum and variants)
- CssBoxShadow (struct and fields)
- CssGradientAngle (enum and variants)
- CssGradientStop (struct and fields)
- CssGradient (enum and variants)
- CssTransitionProperty (struct and fields)
- CssKeyframe (struct and fields)
- CssKeyframes (struct and fields)
- CssPointerEvents (only basic comment, no variant documentation)
- CssLetterSpacing (only basic comment, no variant documentation)
- CssTextShadow (only basic comment, no field documentation)
- CssClipPath (only basic comment, no variant documentation)
- CssBackgroundImage (only basic comment, no variant documentation)
Consider adding comprehensive documentation with usage examples, similar to the documentation style used in respo_css.mbt. This will help users understand how to use these new features correctly.
| ///| | ||
| test "enhanced css properties" { | ||
| let style = @respo_css.respo_style( | ||
| border_radius_obj=@respo_css.CssBorderRadius::new( | ||
| top_left=@respo_css.CssSize::Px(10.0), | ||
| bottom_right=@respo_css.CssSize::Px(5.0), | ||
| ), | ||
| box_shadow_obj=@respo_css.CssBoxShadow::new( | ||
| x=@respo_css.CssSize::Px(2.0), | ||
| y=@respo_css.CssSize::Px(2.0), | ||
| blur=@respo_css.CssSize::Px(4.0), | ||
| color=@respo_css.CssColor::Black, | ||
| ), | ||
| background_image=@respo_css.CssBackgroundImage::LinearGradient( | ||
| @respo_css.CssGradient::Linear( | ||
| Some(@respo_css.CssGradientAngle::Deg(45)), | ||
| [ | ||
| @respo_css.CssGradientStop::new(@respo_css.CssColor::Red), | ||
| @respo_css.CssGradientStop::new(@respo_css.CssColor::Blue), | ||
| ], | ||
| ), | ||
| ), | ||
| ) | ||
| inspect( | ||
| style, | ||
| content="border-top-left-radius:10px; border-bottom-right-radius:5px; box-shadow:2px 2px 4px black; background-image:linear-gradient(45deg, red, blue); ", | ||
| ) | ||
| } | ||
|
|
||
| ///| | ||
| test "keyframes" { | ||
| let kf = @respo_css.CssKeyframes::new("fade-in", [ | ||
| @respo_css.CssKeyframe::new(0, [("opacity", "0")]), | ||
| @respo_css.CssKeyframe::new(100, [("opacity", "1")]), | ||
| ]) | ||
| inspect( | ||
| kf.to_css_string(), | ||
| content=( | ||
| #|@keyframes fade-in { | ||
| #| 0% { | ||
| #| opacity: 0; | ||
| #| } | ||
| #| 100% { | ||
| #| opacity: 1; | ||
| #| } | ||
| #|} | ||
| #| | ||
| ), | ||
| ) | ||
| } |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test file only covers CssBorderRadius, CssBoxShadow, CssBackgroundImage with LinearGradient, and CssKeyframes. Several new types introduced in respo_css_enhanced.mbt lack test coverage:
- CssTextShadow
- CssClipPath (with Circle, Ellipse, Inset, Polygon variants)
- CssPointerEvents (all enum variants)
- CssLetterSpacing (all enum variants)
- CssTransitionProperty
- CssGradient Radial and Conic variants
- CssBackgroundImage with RadialGradient, ConicGradient, Url, Multiple, None variants
Consider adding test cases for these missing types to ensure they work correctly and generate expected CSS output.
| if self.bottom_right is Some(v) { | ||
| parts.push("border-bottom-right-radius: " + v.to_string()) | ||
| } | ||
| parts.join(";") |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CssBorderRadius::to_string method joins parts with a semicolon but doesn't add a trailing semicolon or space. This is inconsistent with how CSS properties are typically formatted in the respo_style function output (which includes semicolons and spaces after each property). While to_styles is correctly used in the integration, having to_string return a different format could lead to confusion. Consider updating to_string to match the formatting pattern, or document why it differs.
| parts.join(";") | |
| if parts.length == 0 { | |
| "" | |
| } else { | |
| parts.join("; ") + "; " | |
| } |
| } | ||
|
|
||
| ///| | ||
| /// Linear Gradient Builder |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation comment says "Linear Gradient Builder" but the enum CssGradientAngle is actually used for both Linear and Conic gradients (as seen in the Show implementation). The comment should be updated to reflect that this is a more general gradient angle type, not specific to linear gradients. Consider changing to "Gradient Angle" or "Gradient Direction/Angle".
| /// Linear Gradient Builder | |
| /// Gradient Angle/Direction for Linear and Conic Gradients |
| pub impl Show for CssGradient with output(self, logger) { | ||
| let ret = match self { | ||
| Linear(angle, stops) => { | ||
| let mut result = "linear-gradient(" | ||
| match angle { | ||
| Some(Deg(d)) => result = result + d.to_string() + "deg, " | ||
| Some(Direction(dir)) => result = result + dir + ", " | ||
| None => () | ||
| } | ||
| let stop_strs = stops.map(fn(stop) { | ||
| let color_str = stop.color.to_string() | ||
| match stop.position { | ||
| Some(pos) => color_str + " " + pos.to_string() | ||
| None => color_str | ||
| } | ||
| }) | ||
| result = result + stop_strs.join(", ") | ||
| result + ")" | ||
| } | ||
| Radial(stops) => { | ||
| let mut result = "radial-gradient(" | ||
| let stop_strs = stops.map(fn(stop) { | ||
| let color_str = stop.color.to_string() | ||
| match stop.position { | ||
| Some(pos) => color_str + " " + pos.to_string() | ||
| None => color_str | ||
| } | ||
| }) | ||
| result = result + stop_strs.join(", ") | ||
| result + ")" | ||
| } | ||
| Conic(angle, stops) => { | ||
| let mut result = "conic-gradient(" | ||
| match angle { | ||
| Some(Deg(d)) => result = result + "from " + d.to_string() + "deg, " | ||
| Some(Direction(dir)) => result = result + "from " + dir + ", " | ||
| None => () | ||
| } | ||
| let stop_strs = stops.map(fn(stop) { | ||
| let color_str = stop.color.to_string() | ||
| match stop.position { | ||
| Some(pos) => color_str + " " + pos.to_string() | ||
| None => color_str | ||
| } | ||
| }) | ||
| result = result + stop_strs.join(", ") | ||
| result + ")" | ||
| } |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for mapping gradient stops to strings is duplicated three times (lines 148-154, 160-165, and 177-182). This code could be extracted into a helper function to reduce duplication and improve maintainability. For example:
fn format_gradient_stops(stops : Array[CssGradientStop]) -> String {
let stop_strs = stops.map(fn(stop) {
let color_str = stop.color.to_string()
match stop.position {
Some(pos) => color_str + " " + pos.to_string()
None => color_str
}
})
stop_strs.join(", ")
}
Then use it in all three gradient variants.
No description provided.