From 6c5cc5b43e9d27f88d06974e80742d30be27db98 Mon Sep 17 00:00:00 2001 From: tiye Date: Fri, 3 Oct 2025 13:13:22 +0800 Subject: [PATCH 1/5] expose css converting function; tag 0.0.2 --- moon.mod.json | 2 +- src/react.mbt | 69 ++++++++++++++++++++++++++++++++++----------------- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/moon.mod.json b/moon.mod.json index 7226f2d..4d85740 100644 --- a/moon.mod.json +++ b/moon.mod.json @@ -1,6 +1,6 @@ { "name": "tiye/react", - "version": "0.0.2-a3", + "version": "0.0.2", "deps": { "tiye/dom-ffi": "0.1.1", "tiye/respo_css": "0.1.2" diff --git a/src/react.mbt b/src/react.mbt index ea59f2b..36a8698 100644 --- a/src/react.mbt +++ b/src/react.mbt @@ -41,13 +41,16 @@ pub fn render(vdom : VirtualNode, parent : @dom.Element) -> Unit { ///| extern "js" fn react_create_element( tag : String, - props : JsValue, + props : @dom.JsObjectObscure, children : JsValue, ) -> JsValue = #| (tag, props, children) => window.React.createElement(tag, props, ...children) ///| -extern "js" fn react_fragment(props : JsValue, children : JsValue) -> JsValue = +extern "js" fn react_fragment( + props : @dom.JsObjectObscure, + children : JsValue, +) -> JsValue = #| (props, children) => window.React.createElement(window.React.Fragment, props, ...children) ///| @@ -374,7 +377,7 @@ pub fn VirtualNode::to_js_value(self : VirtualNode) -> JsValue { for child in children { v.push(child.to_js_value()) } - react_fragment(JsObject::new().to_value(), v.to_value()) + react_fragment(@dom.new_js_object(), v.to_value()) } Text(t) => JsValue::from_string(t) JsNode(v) => v @@ -474,33 +477,55 @@ pub fn css_prop_to_camel_case(name : String) -> String { ret } +///| +/// Converts a RespoStyle to a JavaScript style object. +/// This function takes CSS properties from RespoStyle and converts them to +/// React-compatible camelCase property names with string values. +/// +/// # Parameters +/// - `style`: RespoStyle containing CSS properties +/// +/// # Returns +/// `JsObject` - JavaScript object with camelCase CSS properties +/// +/// # Example +/// ```moonbit_nocheck +/// let style = @css.respo_style(background_color=Red, font_size=16.0 |> Px) +/// let js_style = convert_style_to_js_object(style) +/// // Results in: { backgroundColor: "red", fontSize: "16px" } +/// ``` + +///| +pub fn convert_style_to_js_object( + style : @css.RespoStyle, +) -> @dom.JsObjectObscure { + let style_obj = @dom.new_js_object() + for _idx, pair in style.0 { + let (key, value) = pair + style_obj.set(css_prop_to_camel_case(key), @dom.v_to_js_obscure(value)) + } + style_obj +} + ///| fn VirtualElement::to_js_value(self : VirtualElement) -> JsValue { - let props = JsObject::new() + let props = @dom.new_js_object() for key, value in self.attrs.inner() { let react_prop_name = dom_attr_to_react_prop(key) let prop_value = convert_prop_value(react_prop_name, value) props.set(react_prop_name, prop_value) } - let style = JsObject::new() - for _idx, pair in self.style.0 { - let (key, value) = pair - style.set(css_prop_to_camel_case(key), JsValue::from_string(value)) - } - props.set("style", JsValue::from_object(style)) + let style = convert_style_to_js_object(self.style) + props.set("style", @dom.v_to_js_obscure(style)) let children = JsArray::new() for child in self.children { children.push(child.to_js_value()) } for event_type, value in self.event.inner() { let event_name = dom_event_to_react_handler(event_type) - props.set(event_name, value.to_js_func()) + props.set(event_name, @dom.v_to_js_obscure(value)) } - react_create_element( - self.name, - JsValue::from_object(props), - JsValue::from_array(children), - ) + react_create_element(self.name, props, JsValue::from_array(children)) } ///| @@ -727,16 +752,14 @@ pub fn is_boolean_prop(prop_name : String) -> Bool { /// # Inspect Examples /// inspect(convert_prop_value("checked", "true"), content="JsValue::from_bool(true)") /// inspect(convert_prop_value("className", "my-class"), content="JsValue::from_string(\"my-class\")") -pub fn convert_prop_value(prop_name : String, value : String) -> JsValue { +pub fn convert_prop_value(prop_name : String, value : String) -> @dom.JsObscure { if is_boolean_prop(prop_name) { match value { - "true" => JsValue::from_bool(true) - "false" => JsValue::from_bool(false) - _ => JsValue::from_string(value) // fallback for non-standard values + "true" => @dom.v_to_js_obscure(true) + "false" => @dom.v_to_js_obscure(false) + _ => @dom.v_to_js_obscure(value) // fallback for non-standard values } } else { - JsValue::from_string(value) + @dom.v_to_js_obscure(value) } } - -/// ElementEvents::new() From d3843e82987350615d9229e2f3054bc6f5a0d124 Mon Sep 17 00:00:00 2001 From: tiye Date: Fri, 3 Oct 2025 23:07:54 +0800 Subject: [PATCH 2/5] include js import tip --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index a4b1609..faae556 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,16 @@ This is an experimental hobby project exploring MoonBit bindings for React. The ## Quick Start +Before writing any MoonBit code, make sure to include the React bindings in your project. + +```js +import * as React from "react"; +import * as ReactDOMClient from "react-dom/client"; + +window.React = React; +window.ReactDOMClient = ReactDOMClient; +``` + Here's a simple example of how to use this library: ```moonbit From 4ab153a82185eb7734e971b3e5efa874e6bb2fb0 Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 5 Oct 2025 20:38:47 +0800 Subject: [PATCH 3/5] moving some of code to jsobscure --- src/hooks.mbt | 92 +++++++++++++++++------------------- src/js-value-trait.mbt | 64 +------------------------ src/js-value.mbt | 100 ++-------------------------------------- src/main/hooks-demo.mbt | 12 ----- src/main/main.mbt | 12 ----- src/main/todolist.mbt | 27 ++--------- src/react.mbt | 50 ++++++++++---------- 7 files changed, 78 insertions(+), 279 deletions(-) diff --git a/src/hooks.mbt b/src/hooks.mbt index 4320859..edc6d30 100644 --- a/src/hooks.mbt +++ b/src/hooks.mbt @@ -2,37 +2,43 @@ // React Hooks wrappers, aligned with existing use_state style ///| -extern "js" fn react_use_effect(effect : JsValue, deps : JsValue) -> Unit = +extern "js" fn react_use_effect( + effect : @dom.JsObscure, + deps : Array[@dom.JsObscure], +) -> Unit = #| (effect, deps) => window.React.useEffect(effect, deps) ///| extern "js" fn react_use_layout_effect( - effect : JsValue, - deps : JsValue, + effect : @dom.JsObscure, + deps : Array[@dom.JsObscure], ) -> Unit = #| (effect, deps) => window.React.useLayoutEffect(effect, deps) ///| -extern "js" fn react_use_memo(factory : JsValue, deps : JsValue) -> JsValue = +extern "js" fn react_use_memo( + factory : @dom.JsObscure, + deps : Array[@dom.JsObscure], +) -> @dom.JsObscure = #| (factory, deps) => window.React.useMemo(factory, deps) ///| extern "js" fn react_use_callback( - callback : JsValue, - deps : JsValue, + callback : @dom.JsObscure, + deps : Array[@dom.JsObscure], ) -> JsValue = #| (callback, deps) => window.React.useCallback(callback, deps) ///| -extern "js" fn react_use_ref(initial : JsValue) -> JsValue = +extern "js" fn react_use_ref(initial : @dom.JsObscure) -> @dom.JsObscure = #| (initial) => window.React.useRef(initial) ///| -extern "js" fn react_ref_get_value(r : JsValue) -> JsValue = +extern "js" fn react_ref_get_value(r : @dom.JsObscure) -> @dom.JsObscure = #| (ref) => ref.current ///| -extern "js" fn react_ref_set_value(r : JsValue, value : JsValue) -> Unit = +extern "js" fn react_ref_set_value(r : @dom.JsObscure, value : JsValue) -> Unit = #| (ref, value) => ref.current = value ///| @@ -50,88 +56,71 @@ extern "js" fn fn0_from_value(v : JsValue) -> () -> Unit = extern "js" fn fn1_from_value(v : JsValue) -> (JsValue) -> Unit = #| (v) => v -///| -/// 最多支持 8 个依赖项 -pub(all) enum Deps[A, B, C, D, E, F, G, H] { - Dep0 - Dep1(A) - Dep2(A, B) - Dep3(A, B, C) - Dep4(A, B, C, D) - Dep5(A, B, C, D, E) - Dep6(A, B, C, D, E, F) - Dep7(A, B, C, D, E, F, G) - Dep8(A, B, C, D, E, F, G, H) -} - -///| -fn deps_to_js_array(deps : Array[JsValue]) -> JsValue { - let arr = JsArray::new() - for d in deps { - arr.push(d) - } - arr.to_value() -} - ///| // useLayoutEffect pub fn use_layout_effect_deps( effect : () -> Unit, - deps : Array[JsValue], + deps : Array[@dom.JsObscure], ) -> Unit { - react_use_layout_effect(any_to_js_value(effect), deps_to_js_array(deps)) + react_use_layout_effect(@dom.v_to_js_obscure(effect), deps) } ///| // useMemo -pub fn[A] use_memo_deps(factory : () -> A, deps : Array[JsValue]) -> A { - let v = react_use_memo(any_to_js_value(factory), deps_to_js_array(deps)) - any_from_js_value(v) +pub fn[A] use_memo_deps(factory : () -> A, deps : Array[@dom.JsObscure]) -> A { + let v = react_use_memo(@dom.v_to_js_obscure(factory), deps) + @dom.js_obscure_to_v(v) } ///| // useCallback -pub fn[F] use_callback_deps(callback : F, deps : Array[JsValue]) -> F { - let v = react_use_callback(any_to_js_value(callback), deps_to_js_array(deps)) +pub fn[F] use_callback_deps(callback : F, deps : Array[@dom.JsObscure]) -> F { + let v = react_use_callback(@dom.v_to_js_obscure(callback), deps) any_from_js_value(v) } ///| // useEffect pub fn use_effect_once(effect : () -> Unit) -> Unit { - react_use_effect(any_to_js_value(effect), JsArray::new().to_value()) + react_use_effect(@dom.v_to_js_obscure(effect), []) } ///| /// a short hand to turn value into JsValue in hook deps -pub fn[T] obscure(v : T) -> JsValue = "%identity" +pub fn[T] obscure(v : T) -> @dom.JsObscure = "%identity" ///| -pub fn use_effect_deps(effect : () -> Unit, deps : Array[JsValue]) -> Unit { - react_use_effect(any_to_js_value(effect), deps_to_js_array(deps)) +pub fn use_effect_deps( + effect : () -> Unit, + deps : Array[@dom.JsObscure], +) -> Unit { + react_use_effect(@dom.v_to_js_obscure(effect), deps) } ///| // useCallback (no argument) -pub fn use_callback0_deps(f : () -> Unit, deps : Array[JsValue]) -> () -> Unit { - let raw = react_use_callback(any_to_js_value(f), deps_to_js_array(deps)) +pub fn use_callback0_deps( + f : () -> Unit, + deps : Array[@dom.JsObscure], +) -> () -> Unit { + let raw = react_use_callback(@dom.v_to_js_obscure(f), deps) fn0_from_value(raw) } ///| struct ReactRef[T] { - js_value : JsValue + js_value : @dom.JsObscure mut _v0 : T } ///| pub fn[T] ReactRef::from(v : T) -> ReactRef[T] { - ReactRef::{ js_value: react_use_ref(any_to_js_value(v)), _v0: v } + ReactRef::{ js_value: react_use_ref(@dom.v_to_js_obscure(v)), _v0: v } } ///| pub fn[T] ReactRef::get(self : ReactRef[T]) -> T { - any_from_js_value(react_ref_get_value(self.js_value)) + @dom.js_obscure_to_v(react_ref_get_value(self.js_value)) } ///| @@ -142,8 +131,11 @@ pub fn[T] ReactRef::set(self : ReactRef[T], value : T) -> Unit { ///| // useRef -pub fn[T : JsValueTrait] use_ref(initial : T) -> ReactRef[T] { - ReactRef::{ js_value: react_use_ref(initial.to_value()), _v0: initial } +pub fn[T] use_ref(initial : T) -> ReactRef[T] { + ReactRef::{ + js_value: react_use_ref(@dom.v_to_js_obscure(initial)), + _v0: initial, + } } ///| diff --git a/src/js-value-trait.mbt b/src/js-value-trait.mbt index 8d733d7..5249053 100644 --- a/src/js-value-trait.mbt +++ b/src/js-value-trait.mbt @@ -1,7 +1,7 @@ ///| pub(open) trait JsValueTrait { - to_value(Self) -> JsValue - from_value(JsValue) -> Self + to_value(Self) -> @dom.JsObscure + from_value(@dom.JsObscure) -> Self } ///| @@ -11,63 +11,3 @@ pub(open) trait CastJsValueTrait { ///| impl CastJsValueTrait with to_js_value(self) -> JsValue = "%identity" - -///| -pub impl JsValueTrait for String with to_value(self) -> JsValue { - JsValue::from_string(self) -} - -///| -pub impl JsValueTrait for String with from_value(value : JsValue) -> String { - value.to_string() -} - -///| -pub impl JsValueTrait for Float with from_value(value : JsValue) -> Float { - value.to_number() -} - -///| -pub impl JsValueTrait for Float with to_value(self) -> JsValue { - JsValue::from_number(self) -} - -///| -pub impl JsValueTrait for Int with from_value(value : JsValue) -> Int { - value.to_number().reinterpret_as_int() -} - -///| -pub impl JsValueTrait for Int with to_value(self) -> JsValue { - JsValue::from_number(self.reinterpret_as_float()) -} - -///| -pub impl JsValueTrait for Bool with to_value(self) -> JsValue { - JsValue::from_bool(self) -} - -///| -pub impl JsValueTrait for Bool with from_value(value : JsValue) -> Bool { - value.to_bool() -} - -///| -pub impl JsValueTrait for JsObject with to_value(self) -> JsValue { - JsValue::from_object(self) -} - -///| -pub impl JsValueTrait for JsObject with from_value(value : JsValue) -> JsObject { - value.to_object() -} - -///| -pub impl JsValueTrait for JsArray with to_value(self) -> JsValue { - JsValue::from_array(self) -} - -///| -pub impl JsValueTrait for JsArray with from_value(value : JsValue) -> JsArray { - value.to_array() -} diff --git a/src/js-value.mbt b/src/js-value.mbt index 5af6b23..c85768b 100644 --- a/src/js-value.mbt +++ b/src/js-value.mbt @@ -2,43 +2,7 @@ type JsValue ///| -extern "js" fn JsValue::from_string(value : String) -> JsValue = - #| (value) => value - -///| -/// unsafe -extern "js" fn JsValue::to_string(self : JsValue) -> String = - #| (self) => self - -///| -extern "js" fn JsValue::from_number(value : Float) -> JsValue = - #| (value) => value - -///| -/// unsafe -extern "js" fn JsValue::to_number(self : JsValue) -> Float = - #| (self) => self - -///| -extern "js" fn JsValue::from_bool(value : Bool) -> JsValue = - #| (value) => value - -///| -/// unsafe -extern "js" fn JsValue::to_bool(self : JsValue) -> Bool = - #| (self) => self - -///| -pub extern "js" fn JsValue::from_object(value : JsObject) -> JsValue = - #| (value) => value - -///| -/// unsafe -extern "js" fn JsValue::to_object(self : JsValue) -> JsObject = - #| (self) => self - -///| -extern "js" fn JsValue::from_array(value : JsArray) -> JsValue = +extern "js" fn JsValue::from_object(value : JsObject) -> JsValue = #| (value) => value ///| @@ -47,19 +11,10 @@ extern "js" fn JsValue::to_array(self : JsValue) -> JsArray = #| (self) => self ///| -extern "js" fn JsValue::from_fn(value : () -> JsValue) -> JsValue = - #| (value) => value +priv type JsObject ///| -/// unsafe -extern "js" fn JsValue::to_fn(self : JsValue) -> () -> JsValue = - #| (self) => self - -///| -type JsObject - -///| -pub extern "js" fn JsObject::new() -> JsObject = +extern "js" fn JsObject::new() -> JsObject = #| () => ({}) ///| @@ -70,56 +25,9 @@ extern "js" fn JsObject::set( ) -> Unit = #| (self, key, value) => self[key] = value -///| -pub extern "js" fn JsObject::get(self : JsObject, key : String) -> JsValue = - #| (self, key) => self[key] - -///| -/// Converts a JavaScript object to a JavaScript value. -/// -/// Parameters: -/// -/// * `self` : The JavaScript object to convert. -/// -/// Returns a `JsValue` representing the same object. -/// -/// Example: -/// -/// ```moonbit -/// let obj = @tiye/react.JsObject::new() -/// let _value = obj.as_value() -/// ``` -/// -pub extern "js" fn JsObject::as_value(self : JsObject) -> JsValue = - #| (self) => self - ///| pub type JsArray ///| -pub extern "js" fn JsArray::to_string(self : JsArray) -> String = - #| (self) => `${self}` - -///| -pub extern "js" fn JsArray::new() -> JsArray = - #| () => [] - -///| -pub extern "js" fn JsArray::op_get(self : JsArray, index : Int) -> JsValue = +extern "js" fn JsArray::op_get(self : JsArray, index : Int) -> JsValue = #| (self, index) => self[index] - -///| -pub extern "js" fn JsArray::op_set( - self : JsArray, - index : Int, - value : JsValue, -) -> Unit = - #| (self, index, value) => self[index] = value - -///| -extern "js" fn JsArray::push(self : JsArray, value : JsValue) -> Unit = - #| (self, value) => self.push(value) - -///| -pub extern "js" fn JsArray::as_value(self : JsArray) -> JsValue = - #| (self) => self diff --git a/src/main/hooks-demo.mbt b/src/main/hooks-demo.mbt index 1114436..8e0727a 100644 --- a/src/main/hooks-demo.mbt +++ b/src/main/hooks-demo.mbt @@ -6,18 +6,6 @@ fnalias @react.obscure /// struct HooksDemoProps {} derive(Default) -///| -impl @react.JsValueTrait for HooksDemoProps with to_value(_self) -> @react.JsValue { - @react.JsObject::new().to_value() -} - -///| -impl @react.JsValueTrait for HooksDemoProps with from_value( - _value : @react.JsValue, -) -> HooksDemoProps { - HooksDemoProps::default() -} - ///| /// 主要的 Hooks 演示组件 fn comp_hooks_demo(_props : HooksDemoProps) -> VirtualNode { diff --git a/src/main/main.mbt b/src/main/main.mbt index 36a113d..4a187f0 100644 --- a/src/main/main.mbt +++ b/src/main/main.mbt @@ -1,18 +1,6 @@ ///| struct ContainerProps {} derive(Default) -///| -impl @react.JsValueTrait for ContainerProps with to_value(_self) -> @react.JsValue { - @react.JsObject::new().to_value() -} - -///| -impl @react.JsValueTrait for ContainerProps with from_value( - _value : @react.JsValue, -) -> ContainerProps { - ContainerProps::default() -} - ///| fn comp_container(_v : ContainerProps) -> VirtualNode { let (counter, set_counter) = @react.use_state(0.0.to_float()) diff --git a/src/main/todolist.mbt b/src/main/todolist.mbt index 7ff7fde..2a72219 100644 --- a/src/main/todolist.mbt +++ b/src/main/todolist.mbt @@ -12,6 +12,10 @@ struct Todo { completed : Bool } derive(Default) +///| +// TodoList 组件的 Props +struct TodoListProps {} derive(Default) + ///| // 编辑状态结构 struct EditingState { @@ -27,22 +31,6 @@ enum Filter { Completed } derive(Eq) -///| -// TodoList 组件的 Props -struct TodoListProps {} derive(Default) - -///| -impl @react.JsValueTrait for TodoListProps with to_value(_self) -> @react.JsValue { - @react.JsObject::new().to_value() -} - -///| -impl @react.JsValueTrait for TodoListProps with from_value( - _value : @react.JsValue, -) -> TodoListProps { - TodoListProps::default() -} - ///| /// TodoAction 枚举类型,表示对 todos 数组的不同操作 enum TodoAction { @@ -193,9 +181,7 @@ fn comp_todolist(_v : TodoListProps) -> VirtualNode { } // 取消编辑的函数 - let cancel_editing = fn() { - set_editing_state(EditingState::default()) - } + let cancel_editing = fn() { set_editing_state(EditingState::default()) } // 生成 todo 项的函数 let create_todo_item = fn(todo : Todo) { @@ -203,11 +189,9 @@ fn comp_todolist(_v : TodoListProps) -> VirtualNode { Some(editing_id) => editing_id == todo.id None => false } - let checkbox_attrs = @react.ElementAttrs::new() // 修复受控组件问题:始终设置 checked 属性 checkbox_attrs.set("checked", if todo.completed { "true" } else { "false" }) - let li_class = { let classes = [] if todo.completed { @@ -218,7 +202,6 @@ fn comp_todolist(_v : TodoListProps) -> VirtualNode { } classes.join(" ") } - if is_editing { // 编辑模式 @react.li(class_name=li_class, [ diff --git a/src/react.mbt b/src/react.mbt index 36a8698..ae6351a 100644 --- a/src/react.mbt +++ b/src/react.mbt @@ -5,7 +5,7 @@ fnalias @css.respo_style typealias @css.RespoStyle ///| -extern "js" fn react_render(vdom : JsValue, parent : @dom.Node) -> Unit = +extern "js" fn react_render(vdom : @dom.JsObscure, parent : @dom.Node) -> Unit = #| (vdom, parent) => { #| const root = window.ReactDOMClient.createRoot(parent); #| root.render(vdom); @@ -35,22 +35,22 @@ extern "js" fn react_render(vdom : JsValue, parent : @dom.Node) -> Unit = /// render(custom_elem.to_node(), document.body) /// ``` pub fn render(vdom : VirtualNode, parent : @dom.Element) -> Unit { - react_render(vdom.to_js_value(), parent.reinterpret_as_node()) + react_render(vdom.to_js_obscure(), parent.reinterpret_as_node()) } ///| extern "js" fn react_create_element( tag : String, props : @dom.JsObjectObscure, - children : JsValue, -) -> JsValue = + children : Array[@dom.JsObscure], +) -> @dom.JsObscure = #| (tag, props, children) => window.React.createElement(tag, props, ...children) ///| extern "js" fn react_fragment( props : @dom.JsObjectObscure, - children : JsValue, -) -> JsValue = + children : Array[@dom.JsObscure], +) -> @dom.JsObscure = #| (props, children) => window.React.createElement(window.React.Fragment, props, ...children) ///| @@ -364,22 +364,22 @@ pub(all) enum VirtualNode { /// A text node containing plain string content Text(String) /// A pre-converted JavaScript value for advanced usage (e.g., from `component` function) - /// already converted to JsValue, hard to convert back - JsNode(JsValue) // for advanced usage, e.g. connect + /// already converted to JsObscure, hard to convert back + JsNode(@dom.JsObscure) // for advanced usage, e.g. connect } ///| -pub fn VirtualNode::to_js_value(self : VirtualNode) -> JsValue { +pub fn VirtualNode::to_js_obscure(self : VirtualNode) -> @dom.JsObscure { let ret = match self { Element(el) => el.to_js_value() Fragment(children) => { - let v = JsArray::new() + let v = [] for child in children { - v.push(child.to_js_value()) + v.push(child.to_js_obscure()) } - react_fragment(@dom.new_js_object(), v.to_value()) + react_fragment(@dom.new_js_object(), v) } - Text(t) => JsValue::from_string(t) + Text(t) => @dom.v_to_js_obscure(t) JsNode(v) => v } ret @@ -508,7 +508,7 @@ pub fn convert_style_to_js_object( } ///| -fn VirtualElement::to_js_value(self : VirtualElement) -> JsValue { +fn VirtualElement::to_js_value(self : VirtualElement) -> @dom.JsObscure { let props = @dom.new_js_object() for key, value in self.attrs.inner() { let react_prop_name = dom_attr_to_react_prop(key) @@ -517,15 +517,15 @@ fn VirtualElement::to_js_value(self : VirtualElement) -> JsValue { } let style = convert_style_to_js_object(self.style) props.set("style", @dom.v_to_js_obscure(style)) - let children = JsArray::new() + let children = [] for child in self.children { - children.push(child.to_js_value()) + children.push(child.to_js_obscure()) } for event_type, value in self.event.inner() { let event_name = dom_event_to_react_handler(event_type) props.set(event_name, @dom.v_to_js_obscure(value)) } - react_create_element(self.name, props, JsValue::from_array(children)) + react_create_element(self.name, props, children) } ///| @@ -564,10 +564,10 @@ pub fn create_element( ///| extern "js" fn create_factory( - f : (JsValue) -> JsValue, - props : JsValue, - children : Array[JsValue], -) -> JsValue = + f : (@dom.JsObscure) -> @dom.JsObscure, + props : @dom.JsObscure, + children : Array[@dom.JsObscure], +) -> @dom.JsObscure = #| (f, props, children) => { #| let h0 = window.React.createElement(f, props, ...children); #| return h0; @@ -592,7 +592,7 @@ extern "js" fn create_factory( /// /// let node = component(my_button, ButtonProps { text: "Click me", disabled: false }, []) /// ``` -pub fn[T : JsValueTrait] component( +pub fn[T] component( /// Component function that transforms props into a virtual node f : (T) -> VirtualNode, /// Props to pass to the component function @@ -600,12 +600,12 @@ pub fn[T : JsValueTrait] component( /// Child nodes to render inside the component children : Array[VirtualNode], ) -> VirtualNode { - let children_js = children.map(fn(child) { child.to_js_value() }) + let children_js = children.map(fn(child) { child.to_js_obscure() }) let r = create_factory( - fn(p) { f(T::from_value(p)).to_js_value() }, + fn(p) { f(@dom.js_obscure_to_v(p)).to_js_obscure() }, // TODO maybe better to use { value: PROPS} instead of props.to_value() // need future refactor to explore it - props.to_value(), + @dom.v_to_js_obscure(props), children_js, ) JsNode(r) From bcee953a402a8effc07015fb6a33783fd3e99b0d Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 5 Oct 2025 23:26:16 +0800 Subject: [PATCH 4/5] remove JsValue and prefer JsObscure; tag 0.0.3 --- README.md | 16 +++++++------- moon.mod.json | 4 ++-- src/hooks.mbt | 39 +++++++++++++++++--------------- src/js-value-trait.mbt | 13 ----------- src/js-value.mbt | 33 ---------------------------- src/react.mbt | 50 ++++++++++++++++++++---------------------- 6 files changed, 55 insertions(+), 100 deletions(-) delete mode 100644 src/js-value-trait.mbt delete mode 100644 src/js-value.mbt diff --git a/README.md b/README.md index faae556..b862e0f 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,13 @@ This is an experimental hobby project exploring MoonBit bindings for React. The - `use_state[T](initial: T) -> (T, (T) -> Unit)` - State management hook - `use_reducer[S: Default, A](initial?: S, reducer: (S, A) -> S) -> (S, (A) -> Unit)` - Reducer hook - `use_effect_once(effect: () -> Unit) -> Unit` - Effect hook that runs only once -- `use_effect_deps(effect: () -> Unit, deps: Array[JsValue]) -> Unit` - Effect hook with dependencies -- `use_layout_effect_deps(effect: () -> Unit, deps: Array[JsValue]) -> Unit` - Layout effect hook -- `use_memo_deps[A](factory: () -> A, deps: Array[JsValue]) -> A` - Memoization hook -- `use_callback_deps[F](callback: F, deps: Array[JsValue]) -> F` - Callback memoization hook -- `use_callback0_deps(f: () -> Unit, deps: Array[JsValue]) -> () -> Unit` - Zero-argument callback hook +- `use_effect_deps(effect: () -> Unit, deps: Array[JsObscure]) -> Unit` - Effect hook with dependencies +- `use_layout_effect_deps(effect: () -> Unit, deps: Array[JsObscure]) -> Unit` - Layout effect hook +- `use_memo_deps[A](factory: () -> A, deps: Array[JsObscure]) -> A` - Memoization hook +- `use_callback_deps[F](callback: F, deps: Array[JsObscure]) -> F` - Callback memoization hook +- `use_callback0_deps(f: () -> Unit, deps: Array[JsObscure]) -> () -> Unit` - Zero-argument callback hook - `use_ref[T: JsValueTrait](initial: T) -> ReactRef[T]` - Reference hook -- `obscure[T](v: T) -> JsValue` - Dependency conversion helper function +- `obscure[T](v: T) -> JsObscure` - Dependency conversion helper function ### HTML Element Bindings @@ -78,12 +78,12 @@ Here's a simple example of how to use this library: struct ContainerProps {} derive(Default) // Implement JsValueTrait for props -impl @react.JsValueTrait for ContainerProps with to_value(_self) -> @react.JsValue { +impl @react.JsValueTrait for ContainerProps with to_value(_self) -> @dom.JsObscure { @react.JsObject::new().to_value() } impl @react.JsValueTrait for ContainerProps with from_value( - _value : @react.JsValue, + _value : @dom.JsObscure, ) -> ContainerProps { ContainerProps::default() } diff --git a/moon.mod.json b/moon.mod.json index 4d85740..f2db2ba 100644 --- a/moon.mod.json +++ b/moon.mod.json @@ -1,8 +1,8 @@ { "name": "tiye/react", - "version": "0.0.2", + "version": "0.0.3", "deps": { - "tiye/dom-ffi": "0.1.1", + "tiye/dom-ffi": "0.1.2", "tiye/respo_css": "0.1.2" }, "readme": "README.md", diff --git a/src/hooks.mbt b/src/hooks.mbt index edc6d30..be9bb70 100644 --- a/src/hooks.mbt +++ b/src/hooks.mbt @@ -1,6 +1,8 @@ ///| // React Hooks wrappers, aligned with existing use_state style +typealias @dom.JsObscure + ///| extern "js" fn react_use_effect( effect : @dom.JsObscure, @@ -26,7 +28,7 @@ extern "js" fn react_use_memo( extern "js" fn react_use_callback( callback : @dom.JsObscure, deps : Array[@dom.JsObscure], -) -> JsValue = +) -> JsObscure = #| (callback, deps) => window.React.useCallback(callback, deps) ///| @@ -38,23 +40,24 @@ extern "js" fn react_ref_get_value(r : @dom.JsObscure) -> @dom.JsObscure = #| (ref) => ref.current ///| -extern "js" fn react_ref_set_value(r : @dom.JsObscure, value : JsValue) -> Unit = +extern "js" fn react_ref_set_value( + r : @dom.JsObscure, + value : JsObscure, +) -> Unit = #| (ref, value) => ref.current = value ///| extern "js" fn react_use_reducer( - reducer : JsValue, - initial : JsValue, -) -> JsValue = + reducer : JsObscure, + initial : JsObscure, +) -> JsObscure = #| (reducer, initial) => window.React.useReducer(reducer, initial) ///| -extern "js" fn fn0_from_value(v : JsValue) -> () -> Unit = - #| (v) => v +fn fn0_to_js_obscure(f : () -> Unit) -> @dom.JsObscure = "%identity" ///| -extern "js" fn fn1_from_value(v : JsValue) -> (JsValue) -> Unit = - #| (v) => v +fn fn1_from_js_obscure(v : JsObscure) -> (JsObscure) -> Unit = "%identity" ///| // useLayoutEffect @@ -62,7 +65,7 @@ pub fn use_layout_effect_deps( effect : () -> Unit, deps : Array[@dom.JsObscure], ) -> Unit { - react_use_layout_effect(@dom.v_to_js_obscure(effect), deps) + react_use_layout_effect(fn0_to_js_obscure(effect), deps) } ///| @@ -76,17 +79,17 @@ pub fn[A] use_memo_deps(factory : () -> A, deps : Array[@dom.JsObscure]) -> A { // useCallback pub fn[F] use_callback_deps(callback : F, deps : Array[@dom.JsObscure]) -> F { let v = react_use_callback(@dom.v_to_js_obscure(callback), deps) - any_from_js_value(v) + @dom.js_obscure_to_v(v) } ///| // useEffect pub fn use_effect_once(effect : () -> Unit) -> Unit { - react_use_effect(@dom.v_to_js_obscure(effect), []) + react_use_effect(fn0_to_js_obscure(effect), []) } ///| -/// a short hand to turn value into JsValue in hook deps +/// a short hand to turn value into JsObscure in hook deps pub fn[T] obscure(v : T) -> @dom.JsObscure = "%identity" ///| @@ -94,7 +97,7 @@ pub fn use_effect_deps( effect : () -> Unit, deps : Array[@dom.JsObscure], ) -> Unit { - react_use_effect(@dom.v_to_js_obscure(effect), deps) + react_use_effect(fn0_to_js_obscure(effect), deps) } ///| @@ -104,7 +107,7 @@ pub fn use_callback0_deps( deps : Array[@dom.JsObscure], ) -> () -> Unit { let raw = react_use_callback(@dom.v_to_js_obscure(f), deps) - fn0_from_value(raw) + @dom.js_obscure_to_v(raw) } ///| @@ -147,8 +150,8 @@ pub fn[S : Default, A] use_reducer( let pair = react_use_reducer( any_to_js_value(reducer), any_to_js_value(initial.unwrap_or_default()), - ).to_array() - let s0 = any_from_js_value(pair[0]) - let dispatch_raw = fn1_from_value(pair[1]) + ) + let s0 = any_from_js_value(pair.get("0")) + let dispatch_raw = fn1_from_js_obscure(pair.get("1")) (s0, fn(a : A) { dispatch_raw(any_to_js_value(a)) }) } diff --git a/src/js-value-trait.mbt b/src/js-value-trait.mbt deleted file mode 100644 index 5249053..0000000 --- a/src/js-value-trait.mbt +++ /dev/null @@ -1,13 +0,0 @@ -///| -pub(open) trait JsValueTrait { - to_value(Self) -> @dom.JsObscure - from_value(@dom.JsObscure) -> Self -} - -///| -pub(open) trait CastJsValueTrait { - to_js_value(Self) -> JsValue = _ -} - -///| -impl CastJsValueTrait with to_js_value(self) -> JsValue = "%identity" diff --git a/src/js-value.mbt b/src/js-value.mbt deleted file mode 100644 index c85768b..0000000 --- a/src/js-value.mbt +++ /dev/null @@ -1,33 +0,0 @@ -///| -type JsValue - -///| -extern "js" fn JsValue::from_object(value : JsObject) -> JsValue = - #| (value) => value - -///| -/// unsafe -extern "js" fn JsValue::to_array(self : JsValue) -> JsArray = - #| (self) => self - -///| -priv type JsObject - -///| -extern "js" fn JsObject::new() -> JsObject = - #| () => ({}) - -///| -extern "js" fn JsObject::set( - self : JsObject, - key : String, - value : JsValue, -) -> Unit = - #| (self, key, value) => self[key] = value - -///| -pub type JsArray - -///| -extern "js" fn JsArray::op_get(self : JsArray, index : Int) -> JsValue = - #| (self, index) => self[index] diff --git a/src/react.mbt b/src/react.mbt index ae6351a..b184150 100644 --- a/src/react.mbt +++ b/src/react.mbt @@ -54,24 +54,23 @@ extern "js" fn react_fragment( #| (props, children) => window.React.createElement(window.React.Fragment, props, ...children) ///| -extern "js" fn react_use_state(initial : JsValue) -> JsValue = +extern "js" fn react_use_state(initial : JsObscure) -> JsObscure = #| (initial) => { return window.React.useState(initial)} ///| -extern "js" fn state_updater_from_value(v : JsValue) -> (JsValue) -> Unit = - #| (v) => v +fn state_updater_from_value(v : JsObscure) -> (JsObscure) -> Unit = "%identity" ///| -fn[T] any_to_js_value(v : T) -> JsValue = "%identity" +fn[T] any_to_js_value(v : T) -> JsObscure = "%identity" ///| -fn[T] any_from_js_value(v : JsValue) -> T = "%identity" +fn[T] any_from_js_value(v : JsObscure) -> T = "%identity" ///| pub fn[T] use_state(initial : T) -> (T, (T) -> Unit) { - let pair = react_use_state(any_to_js_value(initial)).to_array() - let s0 = pair[0] - let s1 = state_updater_from_value(pair[1]) + let pair = react_use_state(any_to_js_value(initial)) + let s0 = pair.get("0") + let s1 = state_updater_from_value(pair.get("1")) (any_from_js_value(s0), fn(value : T) { s1(any_to_js_value(value)) }) } @@ -106,7 +105,7 @@ pub fn ElementAttrs::set( pub type DOMEvent ///| -pub fn DOMEvent::to_js_any_value(self : DOMEvent) -> JsValue = "%identity" +pub fn DOMEvent::to_js_obscure(self : DOMEvent) -> JsObscure = "%identity" ///| /// 获取事件目标元素的值(通常用于 input、textarea 等表单元素) @@ -164,7 +163,7 @@ pub extern "js" fn DOMEvent::meta_key(self : DOMEvent) -> Bool = #| (event) => event.metaKey || false ///| -pub extern "js" fn console_log2(msg : String, v : JsValue) -> Unit = +pub extern "js" fn console_log2(msg : String, v : JsObscure) -> Unit = #| (msg, v) => { console.log(msg, v) } ///| @@ -316,8 +315,7 @@ pub fn ElementEvents::new() -> ElementEvents { } ///| -extern "js" fn DOMEventHandler::to_js_func(self : DOMEventHandler) -> JsValue = - #| (f) => { return f } +fn DOMEventHandler::to_js_func(self : DOMEventHandler) -> JsObscure = "%identity" ///| /// 使用 DOMEventType 添加事件处理器 @@ -343,12 +341,12 @@ pub fn ElementEvents::set( } ///| -pub fn ElementEvents::to_js_value(self : ElementEvents) -> JsValue { - let obj = JsObject::new() +pub fn ElementEvents::to_js_value(self : ElementEvents) -> JsObscure { + let obj = @dom.JsObjectObscure::new() for event_type, value in self.inner() { obj.set(event_type.to_string(), value.to_js_func()) } - JsValue::from_object(obj) + obj.to_js_obscure() } ///| @@ -377,9 +375,9 @@ pub fn VirtualNode::to_js_obscure(self : VirtualNode) -> @dom.JsObscure { for child in children { v.push(child.to_js_obscure()) } - react_fragment(@dom.new_js_object(), v) + react_fragment(@dom.JsObjectObscure::new(), v) } - Text(t) => @dom.v_to_js_obscure(t) + Text(t) => JsObscure::from_string(t) JsNode(v) => v } ret @@ -499,24 +497,24 @@ pub fn css_prop_to_camel_case(name : String) -> String { pub fn convert_style_to_js_object( style : @css.RespoStyle, ) -> @dom.JsObjectObscure { - let style_obj = @dom.new_js_object() + let style_obj = @dom.JsObjectObscure::new() for _idx, pair in style.0 { let (key, value) = pair - style_obj.set(css_prop_to_camel_case(key), @dom.v_to_js_obscure(value)) + style_obj.set(css_prop_to_camel_case(key), JsObscure::from_string(value)) } style_obj } ///| fn VirtualElement::to_js_value(self : VirtualElement) -> @dom.JsObscure { - let props = @dom.new_js_object() + let props = @dom.JsObjectObscure::new() for key, value in self.attrs.inner() { let react_prop_name = dom_attr_to_react_prop(key) let prop_value = convert_prop_value(react_prop_name, value) props.set(react_prop_name, prop_value) } let style = convert_style_to_js_object(self.style) - props.set("style", @dom.v_to_js_obscure(style)) + props.set("style", style.to_js_obscure()) let children = [] for child in self.children { children.push(child.to_js_obscure()) @@ -747,16 +745,16 @@ pub fn is_boolean_prop(prop_name : String) -> Bool { /// - `value`: String value from the attribute. /// /// # Returns -/// `JsValue` - Properly typed JavaScript value for React. +/// `JsObscure` - Properly typed JavaScript value for React. /// /// # Inspect Examples -/// inspect(convert_prop_value("checked", "true"), content="JsValue::from_bool(true)") -/// inspect(convert_prop_value("className", "my-class"), content="JsValue::from_string(\"my-class\")") +/// inspect(convert_prop_value("checked", "true"), content="JsObscure::from_bool(true)") +/// inspect(convert_prop_value("className", "my-class"), content="JsObscure::from_string(\"my-class\")") pub fn convert_prop_value(prop_name : String, value : String) -> @dom.JsObscure { if is_boolean_prop(prop_name) { match value { - "true" => @dom.v_to_js_obscure(true) - "false" => @dom.v_to_js_obscure(false) + "true" => JsObscure::from_bool(true) + "false" => JsObscure::from_bool(false) _ => @dom.v_to_js_obscure(value) // fallback for non-standard values } } else { From 50526efb1784e0a438c9420b71064955383129e9 Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 12 Oct 2025 23:46:37 +0800 Subject: [PATCH 5/5] prefer todolist css from dependency; syntax changes --- .gitignore | 1 + index.html | 1 - package.json | 3 +- src/main/hooks-demo.mbt | 40 ++- src/main/main.mbt | 5 +- src/main/todolist.mbt | 21 +- src/react.mjs | 2 + src/{preact_test.mbt => react_test.mbt} | 0 styles/todomvc.css | 381 ------------------------ yarn.lock | 5 + 10 files changed, 37 insertions(+), 422 deletions(-) rename src/{preact_test.mbt => react_test.mbt} (100%) delete mode 100644 styles/todomvc.css diff --git a/.gitignore b/.gitignore index e78b0b2..74dca84 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ target/ node_modules dist + diff --git a/index.html b/index.html index 0006726..d84e92a 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,6 @@ TodoMVC - MoonBit + React -
diff --git a/package.json b/package.json index c59cce5..62b680e 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "react": "^19.1.1", - "react-dom": "^19.1.1" + "react-dom": "^19.1.1", + "todomvc-app-css": "^2.4.3" } } diff --git a/src/main/hooks-demo.mbt b/src/main/hooks-demo.mbt index 8e0727a..c3b57e5 100644 --- a/src/main/hooks-demo.mbt +++ b/src/main/hooks-demo.mbt @@ -1,5 +1,5 @@ ///| -fnalias @react.obscure +fnalias @react.(obscure, li) ///| /// Hooks Demo Component - 展示新增的 React Hooks API 功能 @@ -118,51 +118,47 @@ fn comp_hooks_demo(_props : HooksDemoProps) -> VirtualNode { set_timer_count(new_val) }, style=@css.respo_style( - padding=8.0 |> @css.Px, - background_color=@css.Hsl(120, 70, 40), - color=@css.White, + padding=8.0 |> Px, + background_color=Hsl(120, 70, 40), + color=White, border_radius=4.0, cursor=Pointer, ), - [@react.Text("启动定时器")], + [Text("启动定时器")], ), ]), // 功能说明区域 @react.div( style=@css.respo_style( - background_color=@css.Hsl(60, 30, 95), + background_color=Hsl(60, 30, 95), padding=15.0 |> Px, border_radius=6.0, - border=CssBorder(1.0, Solid, @css.Hsl(60, 30, 80)), + border=CssBorder(1.0, Solid, Hsl(60, 30, 80)), ), [ - @react.h3([@react.Text("📚 功能说明")]), + @react.h3([Text("📚 功能说明")]), @react.ul([ - @react.li([ - @react.Text( + li([ + Text( "🎯 use_effect_once: 组件挂载时执行一次,查看控制台日志", ), ]), - @react.li([ - @react.Text( + li([ + Text( "📊 use_effect_deps: 依赖计数器变化,每次点击增加都会触发", ), ]), - @react.li([ - @react.Text( + li([ + Text( "🧮 use_memo_deps: 缓存计算结果,只在计数变化时重新计算", ), ]), - @react.li([ - @react.Text( - "🎮 use_callback_deps: 缓存回调函数,优化性能", - ), + li([ + Text("🎮 use_callback_deps: 缓存回调函数,优化性能"), ]), - @react.li([ - @react.Text( - "📝 use_ref: 创建可变引用,用于定时器计数", - ), + li([ + Text("📝 use_ref: 创建可变引用,用于定时器计数"), ]), ]), ], diff --git a/src/main/main.mbt b/src/main/main.mbt index 4a187f0..ea3f844 100644 --- a/src/main/main.mbt +++ b/src/main/main.mbt @@ -15,10 +15,7 @@ fn comp_container(_v : ContainerProps) -> VirtualNode { set_counter(counter + 1.0) }, class_list=[style_counter], - [ - @react.Fragment([@react.Text("Demo: ")]), - @react.Text("Counter \{counter}"), - ], + [Fragment([Text("Demo: ")]), Text("Counter \{counter}")], ), @react.component(comp_todolist, TodoListProps::default(), []), @react.component(comp_hooks_demo, HooksDemoProps::default(), []), diff --git a/src/main/todolist.mbt b/src/main/todolist.mbt index 2a72219..e523267 100644 --- a/src/main/todolist.mbt +++ b/src/main/todolist.mbt @@ -254,7 +254,7 @@ fn comp_todolist(_v : TodoListProps) -> VirtualNode { }) events }, - [@react.Text(todo.text)], + [Text(todo.text)], ), @react.button( class_name="destroy", @@ -288,7 +288,7 @@ fn comp_todolist(_v : TodoListProps) -> VirtualNode { @react.section(class_name="todoapp", [ // 头部标题 @react.header(class_name="header", [ - @react.h1([@react.Text("todos")]), + @react.h1([Text("todos")]), @react.input( class_name="new-todo", placeholder="What needs to be done?", @@ -317,45 +317,40 @@ fn comp_todolist(_v : TodoListProps) -> VirtualNode { // 主要内容区域 @react.section(class_name="main", [ @react.input(id="toggle-all", class_name="toggle-all", type_=Checkbox), - @react.label(for_="toggle-all", [@react.Text("Mark all as complete")]), + @react.label(for_="toggle-all", [Text("Mark all as complete")]), @react.ul(class_name="todo-list", todo_items), ]), // 底部过滤器和统计 @react.footer(class_name="footer", [ - @react.span(class_name="todo-count", [ - @react.Text(active_count.to_string() + " items left"), - ]), + @react.span(class_name="todo-count", [Text("\{active_count} items left")]), @react.ul(class_name="filters", [ @react.li([ @react.a( class_name=if current_filter == All { "selected" } else { "" }, - href="#/", on_click=fn(_) { set_current_filter(All) }, - [@react.Text("All")], + [Text("All")], ), ]), @react.li([ @react.a( class_name=if current_filter == Active { "selected" } else { "" }, - href="#/active", on_click=fn(_) { set_current_filter(Active) }, - [@react.Text("Active")], + [Text("Active")], ), ]), @react.li([ @react.a( class_name=if current_filter == Completed { "selected" } else { "" }, - href="#/completed", on_click=fn(_) { set_current_filter(Completed) }, - [@react.Text("Completed")], + [Text("Completed")], ), ]), ]), @react.button( class_name="clear-completed", on_click=fn(_) { dispatch(ClearCompleted) }, - [@react.Text("Clear completed")], + [Text("Clear completed")], ), ]), ]) diff --git a/src/react.mjs b/src/react.mjs index a1dcbec..665c6c5 100644 --- a/src/react.mjs +++ b/src/react.mjs @@ -1,5 +1,7 @@ import * as React from "react"; import * as ReactDOMClient from "react-dom/client"; +// 导入官方 TodoMVC CSS +import "todomvc-app-css/index.css"; console.log(ReactDOMClient); diff --git a/src/preact_test.mbt b/src/react_test.mbt similarity index 100% rename from src/preact_test.mbt rename to src/react_test.mbt diff --git a/styles/todomvc.css b/styles/todomvc.css deleted file mode 100644 index 8f64f57..0000000 --- a/styles/todomvc.css +++ /dev/null @@ -1,381 +0,0 @@ -html, -body { - margin: 0; - padding: 0; -} - -button { - margin: 0; - padding: 0; - border: 0; - background: none; - font-size: 100%; - vertical-align: baseline; - font-family: inherit; - font-weight: inherit; - color: inherit; - -webkit-appearance: none; - appearance: none; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; - line-height: 1.4em; - background: #f5f5f5; - color: #111111; - min-width: 230px; - max-width: 550px; - margin: 0 auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - font-weight: 300; -} - -:focus { - outline: 0; -} - -.hidden { - display: none; -} - -.todoapp { - background: #fff; - margin: 130px 0 40px 0; - position: relative; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), - 0 25px 50px 0 rgba(0, 0, 0, 0.1); -} - -.todoapp input::-webkit-input-placeholder { - font-style: italic; - font-weight: 300; - color: rgba(0, 0, 0, 0.4); -} - -.todoapp input::-moz-placeholder { - font-style: italic; - font-weight: 300; - color: rgba(0, 0, 0, 0.4); -} - -.todoapp input::input-placeholder { - font-style: italic; - font-weight: 300; - color: rgba(0, 0, 0, 0.4); -} - -.todoapp h1 { - position: absolute; - top: -140px; - width: 100%; - font-size: 80px; - font-weight: 200; - text-align: center; - color: #b83f45; - -webkit-text-rendering: optimizeLegibility; - -moz-text-rendering: optimizeLegibility; - text-rendering: optimizeLegibility; -} - -.new-todo, -.edit { - position: relative; - margin: 0; - width: 100%; - font-size: 24px; - font-family: inherit; - font-weight: inherit; - line-height: 1.4em; - color: inherit; - padding: 6px; - border: 1px solid #999; - box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); - box-sizing: border-box; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.new-todo { - padding: 16px 16px 16px 60px; - border: none; - background: rgba(0, 0, 0, 0.003); - box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); -} - -.main { - position: relative; - z-index: 2; - border-top: 1px solid #e6e6e6; -} - -.toggle-all { - width: 1px; - height: 1px; - border: none; /* Mobile Safari */ - opacity: 0; - position: absolute; - right: 100%; - bottom: 100%; -} - -.toggle-all + label { - width: 60px; - height: 34px; - font-size: 0; - position: absolute; - top: -52px; - left: -13px; - -webkit-transform: rotate(90deg); - transform: rotate(90deg); -} - -.toggle-all + label:before { - content: '❯'; - font-size: 22px; - color: #e6e6e6; - padding: 10px 27px 10px 27px; -} - -.toggle-all:checked + label:before { - color: #737373; -} - -.todo-list { - margin: 0; - padding: 0; - list-style: none; -} - -.todo-list li { - position: relative; - font-size: 24px; - border-bottom: 1px solid #ededed; -} - -.todo-list li:last-child { - border-bottom: none; -} - -.todo-list li.editing { - border-bottom: none; - padding: 0; -} - -.todo-list li.editing .edit { - display: block; - width: calc(100% - 43px); - padding: 12px 16px; - margin: 0 0 0 43px; -} - -.todo-list li.editing .view { - display: none; -} - -.todo-list li .toggle { - text-align: center; - width: 40px; - /* auto, since non-WebKit browsers doesn't support input styling */ - height: auto; - position: absolute; - top: 0; - bottom: 0; - margin: auto 0; - border: none; /* Mobile Safari */ - -webkit-appearance: none; - appearance: none; -} - -.todo-list li .toggle { - opacity: 0; -} - -.todo-list li .toggle + label { - /* - Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 - IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ - */ - background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); - background-repeat: no-repeat; - background-position: center left; -} - -.todo-list li .toggle:checked + label { - background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); -} - -.todo-list li label { - word-break: break-all; - padding: 15px 15px 15px 60px; - display: block; - line-height: 1.2; - transition: color 0.4s; - font-weight: 400; - color: #4d4d4d; -} - -.todo-list li.completed label { - color: #cdcdcd; - text-decoration: line-through; -} - -.todo-list li .destroy { - display: none; - position: absolute; - top: 0; - right: 10px; - bottom: 0; - width: 40px; - height: 40px; - margin: auto 0; - font-size: 30px; - color: #cc9a9a; - margin-bottom: 11px; - transition: color 0.2s ease-out; -} - -.todo-list li .destroy:hover { - color: #af5b5e; -} - -.todo-list li .destroy:after { - content: '×'; -} - -.todo-list li:hover .destroy { - display: block; -} - -.todo-list li .edit { - display: none; -} - -.todo-list li.editing:last-child { - margin-bottom: -1px; -} - -.footer { - padding: 10px 15px; - height: 20px; - text-align: center; - font-size: 15px; - border-top: 1px solid #e6e6e6; - color: #777; -} - -.footer:before { - content: ''; - position: absolute; - right: 0; - bottom: 0; - left: 0; - height: 50px; - overflow: hidden; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), - 0 8px 0 -3px #f6f6f6, - 0 9px 1px -3px rgba(0, 0, 0, 0.2), - 0 16px 0 -6px #f6f6f6, - 0 17px 2px -6px rgba(0, 0, 0, 0.2); -} - -.todo-count { - float: left; - text-align: left; -} - -.todo-count strong { - font-weight: 300; -} - -.filters { - margin: 0; - padding: 0; - list-style: none; - position: absolute; - right: 0; - left: 0; -} - -.filters li { - display: inline; -} - -.filters li a { - color: inherit; - margin: 3px; - padding: 3px 7px; - text-decoration: none; - border: 1px solid transparent; - border-radius: 3px; -} - -.filters li a:hover { - border-color: rgba(175, 47, 47, 0.1); -} - -.filters li a.selected { - border-color: rgba(175, 47, 47, 0.2); -} - -.clear-completed, -html .clear-completed:active { - float: right; - position: relative; - line-height: 19px; - text-decoration: none; - cursor: pointer; -} - -.clear-completed:hover { - text-decoration: underline; -} - -.info { - margin: 65px auto 0; - color: #4d4d4d; - font-size: 11px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - text-align: center; -} - -.info p { - line-height: 1; -} - -.info a { - color: inherit; - text-decoration: none; - font-weight: 400; -} - -.info a:hover { - text-decoration: underline; -} - -/* - Hack to remove background from Mobile Safari. - Can't use it globally since it destroys checkboxes in Firefox -*/ -@media screen and (-webkit-min-device-pixel-ratio:0) { - .toggle-all, - .todo-list li .toggle { - background: none; - } - - .todo-list li .toggle { - height: 40px; - } -} - -@media (max-width: 430px) { - .footer { - height: 50px; - } - - .filters { - bottom: 10px; - } -} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 246fadb..a241e6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -320,6 +320,11 @@ source-map-js@^1.2.1: resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== +todomvc-app-css@^2.4.3: + version "2.4.3" + resolved "https://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.4.3.tgz" + integrity sha512-mSnWZaKBWj9aQcFRsGguY/a8O8NR8GmecD48yU1rzwNemgZa/INLpIsxxMiToFGVth+uEKBrQ7IhWkaXZxwq5Q== + vite@^5.4.8: version "5.4.14" resolved "https://registry.npmmirror.com/vite/-/vite-5.4.14.tgz"