diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3d2c69c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "heroicons"] + path = heroicons + url = git@github.com:tailwindlabs/heroicons.git diff --git a/Cargo.toml b/Cargo.toml index b1bd9c3..9c9911f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,14 +9,22 @@ leptos = { version = "0.7.1", features = ["csr"] } wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" js-sys = "0.3" -serde = { version = "1", features = ["derive"] } serde-wasm-bindgen = "0.6" console_error_panic_hook = "0.1.7" -reactive_stores = "0.1.1" -strum = "0.26.3" -strum_macros = "0.26.4" -serde_json = "1.0.133" -src-common = { path = "./src-common" } +src-common = { path = "./src-common", features = ["store"] } +reactive_stores = { workspace = true } +serde = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } +getrandom = { version = "*", features = ["js"] } [workspace] members = ["src-common", "src-tauri"] + +[workspace.dependencies] +strum = "0.26.3" +strum_macros = "0.26.4" +serde = { version = "1", features = ["derive"] } +reactive_stores = "0.1.2" +uuid = { version = "1.11.0", features = ["v4", "serde"] } +itertools = "0.13.0" diff --git a/Trunk.toml b/Trunk.toml index f476ba0..3c1f88f 100644 --- a/Trunk.toml +++ b/Trunk.toml @@ -8,3 +8,15 @@ ignore = ["./src-tauri"] port = 1420 open = false ws_protocol = "ws" + +[[hooks]] +stage = "build" +command = "bun" +command_arguments = [ + "x", + "tailwindcss", + "-i", + "tailwind.css", + "-o", + "dist/.stage/tailwind.css", +] diff --git a/bun.lockb b/bun.lockb index 996fc62..7807562 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/generate-icons.ts b/generate-icons.ts new file mode 100644 index 0000000..0696cf2 --- /dev/null +++ b/generate-icons.ts @@ -0,0 +1,60 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +const inputFile: string = 'icon-list'; +const outputFile: string = 'src/components/icons.rs'; +const svgDir: string = 'heroicons/optimized'; + +function toComponentName(filePath: string): string { + const parts = filePath.split('/'); + const size = parts[0]; + const style = parts[1]; + const fileName = parts[2].replace('.svg', ''); + + return fileName + .split('-') + .map(part => part.charAt(0).toUpperCase() + part.slice(1)) + .join('') + + size.charAt(0).toUpperCase() + size.slice(1) + + style.charAt(0).toUpperCase() + style.slice(1); +} + +function getSizeClass(size: string): string { + switch (size) { + case '16': return 'size-4'; + case '20': return 'size-5'; + case '24': return 'size-6'; + default: return ''; + } +} +function addClassToSvg(svgContent: string, sizeClass: string): string { + + // Add the size class + svgContent = svgContent.replace(' { + const svgPath = `${file}.svg`; + const parts = file.split('/'); + const size = parts[0]; + let svgContent: string = fs.readFileSync(path.join(svgDir, svgPath), 'utf8'); + const componentName: string = toComponentName(svgPath); + const sizeClass = getSizeClass(size); + + svgContent = addClassToSvg(svgContent, sizeClass); + + output += `#[component]\npub fn ${componentName}() -> impl IntoView {\n`; + output += ` view! { ${svgContent} }\n}\n\n`; + }); + + fs.writeFileSync(outputFile, output); + console.log('Components generated successfully.'); +} + +generateComponents(); \ No newline at end of file diff --git a/heroicons b/heroicons new file mode 160000 index 0000000..fa902f4 --- /dev/null +++ b/heroicons @@ -0,0 +1 @@ +Subproject commit fa902f44d071eac776758cfd6e0522cb7c37b1c3 diff --git a/icon-list b/icon-list new file mode 100644 index 0000000..9db249d --- /dev/null +++ b/icon-list @@ -0,0 +1,8 @@ +16/solid/plus +16/solid/folder-plus +16/solid/arrow-up +16/solid/arrow-down +16/solid/eye +16/solid/eye-slash +16/solid/chevron-down +16/solid/chevron-right \ No newline at end of file diff --git a/index.html b/index.html index 20d0b74..292cb99 100644 --- a/index.html +++ b/index.html @@ -1,11 +1,13 @@ - - - Tauri + Leptos App - - - - - - + + + + Tauri + Leptos App + + + + + + + \ No newline at end of file diff --git a/package.json b/package.json index 22a0e6f..b35fd0d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "devDependencies": { "@tailwindcss/typography": "^0.5.15", "@types/bun": "latest", - "daisyui": "^4.12.23" + "daisyui": "^4.12.23", + "tailwindcss": "^3.4.17" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/src-common/Cargo.toml b/src-common/Cargo.toml index 02696e7..3480d44 100644 --- a/src-common/Cargo.toml +++ b/src-common/Cargo.toml @@ -4,3 +4,10 @@ version = "0.1.0" edition = "2021" [dependencies] +uuid = { workspace = true } +serde = { workspace = true } +itertools = { workspace = true } +reactive_stores = { workspace = true } + +[features] +store = [] diff --git a/src-common/src/lib.rs b/src-common/src/lib.rs index b93cf3f..c446ac8 100644 --- a/src-common/src/lib.rs +++ b/src-common/src/lib.rs @@ -1,14 +1 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub mod models; diff --git a/src-common/src/models/base.rs b/src-common/src/models/base.rs new file mode 100644 index 0000000..f0eef52 --- /dev/null +++ b/src-common/src/models/base.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RangedNumber(u32); + +impl RangedNumber { + pub fn new(value: u32) -> Result { + if value < MIN || value > MAX { + Err(format!("Value {} is out of range ({} - {})", value, MIN, MAX)) + } else { + Ok(RangedNumber(value)) + } + } + + pub fn min() -> u32 {MIN} + pub fn max() -> u32 {MAX} + + pub fn value(&self) -> u32 {self.0} +} \ No newline at end of file diff --git a/src-common/src/models/loot_filter.rs b/src-common/src/models/loot_filter.rs new file mode 100644 index 0000000..2104f43 --- /dev/null +++ b/src-common/src/models/loot_filter.rs @@ -0,0 +1,217 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use itertools::Itertools; +use uuid::Uuid; + +use super::base::RangedNumber; + +impl Filter { + pub fn default_leaf() -> Self { + Self { + id: Uuid::new_v4(), + enabled: true, + name: "".to_string(), + lines: HashMap::new(), + remain: FilterRemain::Leaf(Default::default()), + } + } + + pub fn default_group() -> Self { + Self { + id: Uuid::new_v4(), + enabled: true, + name: "".to_string(), + lines: HashMap::new(), + remain: FilterRemain::Group(Default::default()), + } + } +} + +#[cfg_attr(feature = "store", derive(reactive_stores::Store))] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Filter { + pub id: uuid::Uuid, + pub enabled: bool, + pub name: String, + pub lines: HashMap, + pub remain: FilterRemain, +} + +#[cfg_attr(feature = "store", derive(reactive_stores::Store))] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum FilterRemain { + Leaf(FilterLeaf), + Group(FilterGroup), +} + +#[cfg_attr(feature = "store", derive(reactive_stores::Store))] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct FilterLeaf { + pub show: bool, +} +impl Default for FilterLeaf { + fn default() -> Self { + Self { show: true } + } +} + +#[cfg_attr(feature = "store", derive(reactive_stores::Store))] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct FilterGroup { + #[cfg_attr(feature = "store", store(key: String = |row| row.id.to_string()))] + pub filters: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Color { + pub r: u8, + pub g: u8, + pub b: u8, + pub a: u8, +} + +pub type Level = RangedNumber<1, 100>; + +#[cfg_attr(feature = "store", derive(reactive_stores::Store))] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum Line { + Class(Vec), + BaseType(Vec), + AreaLevel(Op, Level), + DropLevel(Op, Level), + ItemLevel(Op, Level), + Rarity(Op, ItemRarity), + Sockets(Op, u32), + Quality(Op, u32), + StackSize(Op, u32), + + // waystones + WaystoneTier(Op, RangedNumber<1, 16>), + + // effects + SetFontSize(RangedNumber<1, 45>), + SetTextColor(Color), + SetBorderColor(Color), + SetBackgroundColor(Color), + PlayAlertSound(RangedNumber<1, 16>, RangedNumber<0, 300>), + PlayEffect(GameColor), + MinimapIcon(RangedNumber<0, 2>, GameColor, MinimapIconShape), +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Settings { + base_types: Vec, + classes: Vec, + rarities: Vec, + minimap_icon_shapes: Vec, + game_colors: Vec, + ops: Vec, +} + +impl Settings { + fn format_err(value: &str, category: &str, options: &Vec) -> String { + format!( + "{} is not a valid {}, options are: {}", + value, + category, + options.iter().join(" ") + ) + } + pub fn base_type(&self, s: &String) -> Result { + if !self.base_types.contains(s) { + Err(Self::format_err(s, "BaseType", &self.base_types)) + } else { + Ok(ItemBaseType(s.to_string())) + } + } + pub fn class(&self, s: &String) -> Result { + if !self.classes.contains(s) { + Err(Self::format_err(s, "Class", &self.classes)) + } else { + Ok(ItemClass(s.to_string())) + } + } + pub fn rarity(&self, s: &String) -> Result { + if !self.rarities.contains(s) { + Err(Self::format_err(s, "Rarity", &self.rarities)) + } else { + Ok(ItemRarity(s.to_string())) + } + } + pub fn op(&self, s: &String) -> Result { + if !self.ops.contains(s) { + Err(Self::format_err(s, "Operator", &self.ops)) + } else { + Ok(Op(s.to_string())) + } + } + pub fn minimap_icon_shape(&self, s: &String) -> Result { + if !self.minimap_icon_shapes.contains(s) { + Err(Self::format_err( + s, + "MinimapIconShape", + &self.minimap_icon_shapes, + )) + } else { + Ok(MinimapIconShape(s.to_string())) + } + } + pub fn game_color(&self, s: &String) -> Result { + if !self.game_colors.contains(s) { + Err(Self::format_err(s, "GameColor", &self.game_colors)) + } else { + Ok(GameColor(s.to_string())) + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ItemBaseType(String); +impl ItemBaseType { + pub fn value(&self) -> &str { + &self.0 + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ItemClass(String); +impl ItemClass { + pub fn value(&self) -> &str { + &self.0 + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct ItemRarity(String); +impl ItemRarity { + pub fn value(&self) -> &str { + &self.0 + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct MinimapIconShape(String); +impl MinimapIconShape { + pub fn value(&self) -> &str { + &self.0 + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GameColor(String); +impl GameColor { + pub fn value(&self) -> &str { + &self.0 + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Op(String); + +impl Op { + pub fn value(&self) -> &str { + &self.0 + } +} diff --git a/src-common/src/models/mod.rs b/src-common/src/models/mod.rs new file mode 100644 index 0000000..a05d8f6 --- /dev/null +++ b/src-common/src/models/mod.rs @@ -0,0 +1,2 @@ +pub mod loot_filter; +pub mod base; \ No newline at end of file diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6768082..c107fab 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -20,9 +20,6 @@ tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = [] } tauri-plugin-opener = "2" -serde = { version = "1", features = ["derive"] } -strum_macros = "0.26.4" -strum = "0.26.3" reqwest = { version = "0.12.9", features = ["blocking"] } serde_json = "1.0.133" thiserror = "2.0.8" @@ -30,3 +27,6 @@ directories = "5.0.1" indoc = "2.0.5" open = "5.3.1" src-common = { path = "../src-common" } +serde = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } diff --git a/src/app.rs b/src/app.rs index bd4f5c0..fbabd04 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,11 +1,14 @@ -use leptos::prelude::{Read as Reads, *}; -use reactive_stores::Store; +use leptos::{logging::log, prelude::*, tachys::reactive_graph::bind::IntoSplitSignal}; +use reactive_stores::{Field, Store, StoreField}; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::from_value; +use src_common::models::loot_filter::{Filter, FilterStoreFields}; use strum_macros::EnumIter; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; +use crate::components::{self, filter_detail::FilterDetail, filter_root::FilterRoot, icons}; + #[wasm_bindgen] extern "C" { // invoke without arguments @@ -18,326 +21,108 @@ extern "C" { // They need to have different names! } -#[derive(Store, Debug, Clone, Serialize, Deserialize)] -struct POE2FilterConfig { - #[store(key: String = |row| row.kind.to_string())] - armours: Vec, - #[store(key: String = |row| row.kind.to_string())] - weapons: Vec, - #[store(key: String = |row| row.kind.to_string())] - accessories: Vec, - settings: FilterSettings, -} -#[derive(Store, Debug, Clone, Serialize, Deserialize, Default)] -struct FilterSettings { - max_level: u32, -} - -#[derive(Store, Debug, Clone, strum_macros::Display, EnumIter, Serialize, Deserialize)] -#[strum(serialize_all = "title_case")] -enum ArmourKind { - BodyArmours, - Helmets, - Boots, - Gloves, -} - -#[derive(Store, Debug, Clone, strum_macros::Display, EnumIter, Serialize, Deserialize)] -#[strum(serialize_all = "title_case")] -enum WeaponKind { - OneHandMaces, - TwoHandMaces, - Crossbows, - Bows, - Quivers, - Sceptres, - Wands, - Staves, - Quarterstaves, - Shields, - Foci, -} - -#[derive(Store, Debug, Clone, strum_macros::Display, EnumIter, Serialize, Deserialize)] -#[strum(serialize_all = "title_case")] -enum AccessoryKind {} - -#[derive(Store, Debug, Clone, Serialize, Deserialize)] -struct ArmourFilter { - kind: ArmourKind, - armour: bool, - evasion: bool, - energy_shield: bool, - armour_evasion: bool, - armour_energy_shield: bool, - evasion_energy_shield: bool, -} -#[derive(Store, Debug, Clone, Serialize, Deserialize)] -struct WeaponFilter { - kind: WeaponKind, - show: bool, -} - -#[derive(Store, Debug, Clone, Serialize, Deserialize)] -struct AccessoryFiler { - kind: AccessoryKind, -} - -#[derive(Serialize, Deserialize)] -struct UpdateArgs { - config: POE2FilterConfig, +#[derive(Store, Clone)] +struct AppStore { + root: Filter, + selected_filter: Option>, } #[component] -fn Main(config: POE2FilterConfig) -> impl IntoView { - let store = Store::new(config); - Effect::new(move |_| { - let config = store.get(); - spawn_local(async move { - let args = serde_wasm_bindgen::to_value(&UpdateArgs { config: config }).unwrap(); - // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ - invoke("update", args).await; - }); +fn Main() -> impl IntoView { + let store = Store::new(AppStore { + root: Filter::default_group(), + selected_filter: None, }); + let filter = store.root(); + // Effect::new(move |_| { + // let config = store.get(); + // spawn_local(async move { + // let args = serde_wasm_bindgen::to_value(&UpdateArgs { config: config }).unwrap(); + // // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ + // invoke("update", args).await; + // }); + // }); + + let right = move || { + let selected = store.selected_filter().get(); + match selected { + Some(selected) => { + if selected.id().get() == store.root().id().get() { + FilterRoot().into_any() + } else { + view! { }.into_any() + } + } + None => FilterRoot().into_any(), + } + }; - let max_level = store.settings().max_level(); view! { -
-
    -
  • - "Config folder: " - -
  • -
  • - "Changes on the UI will immediately write the updated filter file into POE2's config folder. Reload the filter in-game after changes on the UI." -
  • -
  • "The output filter is named " Leon.
  • -
  • - "Based on " - - NeverSink-PoE2litefilter - , will download on every app launch, saved as - " base_filter.filter in the config folder". -
  • -
  • - "In areas lower than level" - () - .unwrap_or(70); - } - /> ",hide normal or magic items, unless turned on in configurations below." -
  • -
  • - {move || { - format!( - "From area level 11 to {}, hide normal or magic items of which the drop level is 10 level lower than the area level.", - max_level.get(), - ) - }} -
  • -
-

Weapons

-

- {move || { - format!( - "In areas lower than level {}, only show normal or magic weapons selected.", - max_level.get(), - ) - }} -

-
- - - {kind.clone()} - - } - } - /> -
- -

Armours

-

- {move || { - format!( - "In areas lower than level {}, only show normal or magic armours with selected base defence types.", - max_level.get(), - ) - }} -

-
- {kind.clone()} -
- - - - - - -
- } - } - /> +
+
+

POE2 Loot Filter Config

+
+
+
+ +
+
{right}
} } -async fn load_data() -> POE2FilterConfig { - from_value(invoke_without_args("get_config").await).unwrap() -} +// async fn load_data() -> POE2FilterConfig { +// from_value(invoke_without_args("get_config").await).unwrap() +// } #[component] pub fn App() -> impl IntoView { - let config = LocalResource::new(move || load_data()); - view! { - -
- - - - - Loading... -
-
- } - }> - {move || { - config.get().as_deref().map(|config| view! {
}) - }} - - } + view! {
} + // let config = LocalResource::new(move || load_data()); + // view! { + // + //
+ // + // + // + // + // Loading... + //
+ //
+ // } + // }> + // {move || { + // config.get().as_deref().map(|config| view! {
}) + // }} + // + // } } diff --git a/src/components/filter.rs b/src/components/filter.rs new file mode 100644 index 0000000..b0c3a0d --- /dev/null +++ b/src/components/filter.rs @@ -0,0 +1,231 @@ +use leptos::{either::Either, ev::MouseEvent, prelude::*}; +use reactive_stores::Field; +use src_common::models::loot_filter::{ + Filter, FilterGroup, FilterGroupStoreFields, FilterLeaf, FilterLeafStoreFields, FilterRemain, + FilterRemainStoreFields, FilterStoreFields, +}; + +use crate::components::icons; + +fn group( + filter: Field, + group: Field, + on_action: Callback<(Action,)>, + selected: Field>>, + root: bool, +) -> impl IntoView { + let header = move || { + if root { + "Root".to_string() + } else { + filter.id().get().to_string() + } + }; + let (expanded, set_expanded) = signal(false); + + let new_rule = move |ev: MouseEvent| { + ev.stop_propagation(); + group.filters().write().push(Filter::default_leaf()); + set_expanded.set(true); + }; + let new_group = move |ev: MouseEvent| { + ev.stop_propagation(); + group.filters().write().push(Filter::default_group()); + set_expanded.set(true); + }; + let nav_icon = move || { + view! { + + } + }; + let active = move || match selected.get() { + Some(f) => filter.id().get() == f.id().get(), + None => false, + }; + view! { +
  • + +
    + {nav_icon} + + +
    + {header} +
    + + + +
    +
    +
      + { + if i > 0 { + group.filters().write().swap(i, i - 1); + } + } + Action::Down => { + if i < group.filters().get().len() - 1 { + group.filters().write().swap(i, i + 1); + } + } + a @ Action::Select(_) => { + on_action.run((a,)); + } + } + } + /> + } + } + /> +
    +
  • + } +} + +fn leaf( + filter: Field, + leaf: Field, + on_action: Callback<(Action,)>, + selected: Field>>, +) -> impl IntoView { + let header = move || filter.id().get().to_string(); + + let icon = move || { + if leaf.show().get() { + Either::Left(view! { }) + } else { + Either::Right(view! { }) + } + }; + let active = move || match selected.get() { + Some(f) => filter.id().get() == f.id().get(), + None => false, + }; + view! { +
  • + +
    + {icon} + + +
    + {header} +
    + +
    +
    +
  • + } +} + +pub enum Action { + Up, + Down, + Select(Field), +} + +#[component] +pub fn Filter( + #[prop(into)] filter: Field, + #[prop(into)] on_action: Callback<(Action,)>, + #[prop(into)] selected: Field>>, + #[prop(optional)] root: bool, +) -> impl IntoView { + let remain = filter.remain(); + match remain.get_untracked() { + FilterRemain::Leaf(_) => { + leaf(filter, remain.leaf_0().unwrap().into(), on_action, selected).into_any() + } + FilterRemain::Group(_) => group( + filter, + remain.group_0().unwrap().into(), + on_action, + selected, + root, + ) + .into_any(), + } +} diff --git a/src/components/filter_detail.rs b/src/components/filter_detail.rs new file mode 100644 index 0000000..94d47b2 --- /dev/null +++ b/src/components/filter_detail.rs @@ -0,0 +1,12 @@ +use leptos::prelude::*; +use reactive_stores::Field; +use src_common::models::loot_filter::Filter; + +#[component] +pub fn FilterDetail(#[prop(into)] filter: Field) -> impl IntoView { + view! { +
    +

    Filter details:

    +
    + } +} diff --git a/src/components/filter_root.rs b/src/components/filter_root.rs new file mode 100644 index 0000000..06d4ea1 --- /dev/null +++ b/src/components/filter_root.rs @@ -0,0 +1,10 @@ +use leptos::prelude::*; + +#[component] +pub fn FilterRoot() -> impl IntoView { + view! { +
    +

    How to use:

    +
    + } +} diff --git a/src/components/icons.rs b/src/components/icons.rs new file mode 100644 index 0000000..aaec7f0 --- /dev/null +++ b/src/components/icons.rs @@ -0,0 +1,68 @@ +use leptos::prelude::*; + +#[component] +pub fn Plus16Solid() -> impl IntoView { + view! { + } +} + +#[component] +pub fn FolderPlus16Solid() -> impl IntoView { + view! { + } +} + +#[component] +pub fn ArrowUp16Solid() -> impl IntoView { + view! { + } +} + +#[component] +pub fn ArrowDown16Solid() -> impl IntoView { + view! { + } +} + +#[component] +pub fn Eye16Solid() -> impl IntoView { + view! { + } +} + +#[component] +pub fn EyeSlash16Solid() -> impl IntoView { + view! { + } +} + +#[component] +pub fn ChevronDown16Solid() -> impl IntoView { + view! { + } +} + +#[component] +pub fn ChevronRight16Solid() -> impl IntoView { + view! { + } +} + diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..a1c3870 --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,4 @@ +pub mod filter; +pub mod filter_detail; +pub mod filter_root; +pub mod icons; diff --git a/src/main.rs b/src/main.rs index 8387868..0e506c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod app; +mod components; use app::*; use leptos::prelude::*; @@ -6,8 +7,6 @@ use leptos::prelude::*; fn main() { console_error_panic_hook::set_once(); mount_to_body(|| { - view! { - - } + view! { } }) }