From fadfcab1c161ee903361281390ef135495396d64 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Fri, 23 Jun 2023 22:04:16 +0100 Subject: [PATCH] added use_cycle_list, use_color_mode and maybe_rw_signal --- .idea/leptos-use.iml | 2 + Cargo.toml | 6 +- docs/book/src/SUMMARY.md | 2 + docs/book/src/browser/use_color_mode.md | 3 + docs/book/src/utilities/use_cycle_list.md | 3 + examples/Cargo.toml | 2 + examples/use_color_mode/Cargo.toml | 16 + examples/use_color_mode/README.md | 23 + examples/use_color_mode/Trunk.toml | 2 + examples/use_color_mode/index.html | 7 + examples/use_color_mode/input.css | 3 + examples/use_color_mode/rust-toolchain.toml | 2 + examples/use_color_mode/src/main.rs | 36 ++ examples/use_color_mode/style/output.css | 289 +++++++++++ examples/use_color_mode/tailwind.config.js | 15 + examples/use_cycle_list/Cargo.toml | 16 + examples/use_cycle_list/README.md | 23 + examples/use_cycle_list/Trunk.toml | 2 + examples/use_cycle_list/index.html | 7 + examples/use_cycle_list/input.css | 3 + examples/use_cycle_list/rust-toolchain.toml | 2 + examples/use_cycle_list/src/main.rs | 32 ++ examples/use_cycle_list/style/output.css | 298 ++++++++++++ examples/use_cycle_list/tailwind.config.js | 15 + src/core/element_maybe_signal.rs | 18 + src/core/maybe_rw_signal.rs | 258 ++++++++++ src/core/mod.rs | 4 + src/core/storage.rs | 22 + src/lib.rs | 4 + src/storage/use_storage.rs | 21 +- src/use_color_mode.rs | 456 ++++++++++++++++++ src/use_cycle_list.rs | 231 +++++++++ template/.ffizer.yaml | 5 + .../src/main.ffizer.hbs.rs | 2 +- .../{{ function_name }}.ffizer.hbs.rs | 20 +- 35 files changed, 1818 insertions(+), 32 deletions(-) create mode 100644 docs/book/src/browser/use_color_mode.md create mode 100644 docs/book/src/utilities/use_cycle_list.md create mode 100644 examples/use_color_mode/Cargo.toml create mode 100644 examples/use_color_mode/README.md create mode 100644 examples/use_color_mode/Trunk.toml create mode 100644 examples/use_color_mode/index.html create mode 100644 examples/use_color_mode/input.css create mode 100644 examples/use_color_mode/rust-toolchain.toml create mode 100644 examples/use_color_mode/src/main.rs create mode 100644 examples/use_color_mode/style/output.css create mode 100644 examples/use_color_mode/tailwind.config.js create mode 100644 examples/use_cycle_list/Cargo.toml create mode 100644 examples/use_cycle_list/README.md create mode 100644 examples/use_cycle_list/Trunk.toml create mode 100644 examples/use_cycle_list/index.html create mode 100644 examples/use_cycle_list/input.css create mode 100644 examples/use_cycle_list/rust-toolchain.toml create mode 100644 examples/use_cycle_list/src/main.rs create mode 100644 examples/use_cycle_list/style/output.css create mode 100644 examples/use_cycle_list/tailwind.config.js create mode 100644 src/core/maybe_rw_signal.rs create mode 100644 src/core/storage.rs create mode 100644 src/use_color_mode.rs create mode 100644 src/use_cycle_list.rs diff --git a/.idea/leptos-use.iml b/.idea/leptos-use.iml index ec492ad..0efa622 100644 --- a/.idea/leptos-use.iml +++ b/.idea/leptos-use.iml @@ -37,6 +37,8 @@ + + diff --git a/Cargo.toml b/Cargo.toml index 911076f..d6c667d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ homepage = "https://leptos-use.rs" leptos = { version = "0.3", default-features = false } wasm-bindgen = "0.2" js-sys = "0.3" -default-struct-builder = "0.2" +default-struct-builder = { path = "../default-struct-builder" } num = { version = "0.4", optional = true } serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } @@ -33,6 +33,7 @@ features = [ "Element", "HtmlElement", "HtmlLinkElement", + "HtmlStyleElement", "IntersectionObserver", "IntersectionObserverInit", "IntersectionObserverEntry", @@ -50,6 +51,7 @@ features = [ "ResizeObserverSize", "ScrollBehavior", "ScrollToOptions", + "Storage", "Touch", "TouchEvent", "TouchList", @@ -59,7 +61,7 @@ features = [ [features] docs = [] math = ["num"] -storage = ["serde", "serde_json", "web-sys/Storage", "web-sys/StorageEvent"] +storage = ["serde", "serde_json", "web-sys/StorageEvent"] stable = ["leptos/stable"] [package.metadata."docs.rs"] diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index dcc4a0d..a369350 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -23,6 +23,7 @@ # Browser - [use_breakpoints](browser/use_breakpoints.md) +- [use_color_mode](browser/use_color_mode.md) - [use_css_var](browser/use_css_var.md) - [use_event_listener](browser/use_event_listener.md) - [use_favicon](browser/use_favicon.md) @@ -52,6 +53,7 @@ # Utilities +- [use_cycle_list](utilities/use_cycle_list.md) - [use_debounce_fn](utilities/use_debounce_fn.md) - [use_supported](utilities/use_supported.md) - [use_throttle_fn](utilities/use_throttle_fn.md) diff --git a/docs/book/src/browser/use_color_mode.md b/docs/book/src/browser/use_color_mode.md new file mode 100644 index 0000000..7d2775d --- /dev/null +++ b/docs/book/src/browser/use_color_mode.md @@ -0,0 +1,3 @@ +# use_color_mode + + diff --git a/docs/book/src/utilities/use_cycle_list.md b/docs/book/src/utilities/use_cycle_list.md new file mode 100644 index 0000000..005c361 --- /dev/null +++ b/docs/book/src/utilities/use_cycle_list.md @@ -0,0 +1,3 @@ +# use_cycle_list + + diff --git a/examples/Cargo.toml b/examples/Cargo.toml index fa729bc..d70b09d 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -7,7 +7,9 @@ members = [ "use_active_element", "use_breakpoints", "use_ceil", + "use_color_mode", "use_css_var", + "use_cycle_list", "use_debounce_fn", "use_element_hover", "use_element_size", diff --git a/examples/use_color_mode/Cargo.toml b/examples/use_color_mode/Cargo.toml new file mode 100644 index 0000000..91d22ea --- /dev/null +++ b/examples/use_color_mode/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "use_color_mode" +version = "0.1.0" +edition = "2021" + +[dependencies] +leptos = "0.3" +console_error_panic_hook = "0.1" +console_log = "1" +log = "0.4" +leptos-use = { path = "../..", features = ["docs", "storage"] } +web-sys = "0.3" + +[dev-dependencies] +wasm-bindgen = "0.2" +wasm-bindgen-test = "0.3.0" diff --git a/examples/use_color_mode/README.md b/examples/use_color_mode/README.md new file mode 100644 index 0000000..f9a5e1d --- /dev/null +++ b/examples/use_color_mode/README.md @@ -0,0 +1,23 @@ +A simple example for `use_color_mode`. + +If you don't have it installed already, install [Trunk](https://trunkrs.dev/) and [Tailwind](https://tailwindcss.com/docs/installation) +as well as the nightly toolchain for Rust and the wasm32-unknown-unknown target: + +```bash +cargo install trunk +npm install -D tailwindcss @tailwindcss/forms +rustup toolchain install nightly +rustup target add wasm32-unknown-unknown +``` + +Then, open two terminals. In the first one, run: + +``` +npx tailwindcss -i ./input.css -o ./style/output.css --watch +``` + +In the second one, run: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/use_color_mode/Trunk.toml b/examples/use_color_mode/Trunk.toml new file mode 100644 index 0000000..3e4be08 --- /dev/null +++ b/examples/use_color_mode/Trunk.toml @@ -0,0 +1,2 @@ +[build] +public_url = "/demo/" \ No newline at end of file diff --git a/examples/use_color_mode/index.html b/examples/use_color_mode/index.html new file mode 100644 index 0000000..ae249a6 --- /dev/null +++ b/examples/use_color_mode/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/use_color_mode/input.css b/examples/use_color_mode/input.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/examples/use_color_mode/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/examples/use_color_mode/rust-toolchain.toml b/examples/use_color_mode/rust-toolchain.toml new file mode 100644 index 0000000..271800c --- /dev/null +++ b/examples/use_color_mode/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file diff --git a/examples/use_color_mode/src/main.rs b/examples/use_color_mode/src/main.rs new file mode 100644 index 0000000..a51b0c8 --- /dev/null +++ b/examples/use_color_mode/src/main.rs @@ -0,0 +1,36 @@ +use leptos::html::Col; +use leptos::*; +use leptos_use::docs::demo_or_body; +use leptos_use::{ + use_color_mode_with_options, use_cycle_list, ColorMode, UseColorModeOptions, + UseColorModeReturn, UseCycleListReturn, +}; + +#[component] +fn Demo(cx: Scope) -> impl IntoView { + let UseColorModeReturn { mode, set_mode, .. } = + use_color_mode_with_options(cx, UseColorModeOptions::default()); + + let UseCycleListReturn { state, next, .. } = use_cycle_list( + cx, + vec![ + ColorMode::Light, + ColorMode::Custom("rust".into()), + ColorMode::Custom("coal".into()), + ColorMode::Custom("navy".into()), + ColorMode::Custom("ayu".into()), + ], + ); + + view! { cx, + } +} + +fn main() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + + mount_to(demo_or_body(), |cx| { + view! { cx, } + }) +} diff --git a/examples/use_color_mode/style/output.css b/examples/use_color_mode/style/output.css new file mode 100644 index 0000000..ab5191f --- /dev/null +++ b/examples/use_color_mode/style/output.css @@ -0,0 +1,289 @@ +[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + border-radius: 0px; + padding-top: 0.5rem; + padding-right: 0.75rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + font-size: 1rem; + line-height: 1.5rem; + --tw-shadow: 0 0 #0000; +} + +[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + border-color: #2563eb; +} + +input::-moz-placeholder, textarea::-moz-placeholder { + color: #6b7280; + opacity: 1; +} + +input::placeholder,textarea::placeholder { + color: #6b7280; + opacity: 1; +} + +::-webkit-datetime-edit-fields-wrapper { + padding: 0; +} + +::-webkit-date-and-time-value { + min-height: 1.5em; +} + +::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field { + padding-top: 0; + padding-bottom: 0; +} + +select { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 0.5rem center; + background-repeat: no-repeat; + background-size: 1.5em 1.5em; + padding-right: 2.5rem; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; +} + +[multiple] { + background-image: initial; + background-position: initial; + background-repeat: unset; + background-size: initial; + padding-right: 0.75rem; + -webkit-print-color-adjust: unset; + print-color-adjust: unset; +} + +[type='checkbox'],[type='radio'] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding: 0; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + display: inline-block; + vertical-align: middle; + background-origin: border-box; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + flex-shrink: 0; + height: 1rem; + width: 1rem; + color: #2563eb; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + --tw-shadow: 0 0 #0000; +} + +[type='checkbox'] { + border-radius: 0px; +} + +[type='radio'] { + border-radius: 100%; +} + +[type='checkbox']:focus,[type='radio']:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 2px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); +} + +[type='checkbox']:checked,[type='radio']:checked { + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +[type='checkbox']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); +} + +[type='radio']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); +} + +[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus { + border-color: transparent; + background-color: currentColor; +} + +[type='checkbox']:indeterminate { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e"); + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus { + border-color: transparent; + background-color: currentColor; +} + +[type='file'] { + background: unset; + border-color: inherit; + border-width: 0; + border-radius: 0; + padding: 0; + font-size: unset; + line-height: inherit; +} + +[type='file']:focus { + outline: 1px solid ButtonText; + outline: 1px auto -webkit-focus-ring-color; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.block { + display: block; +} + +.text-\[--brand-color\] { + color: var(--brand-color); +} + +.text-green-600 { + --tw-text-opacity: 1; + color: rgb(22 163 74 / var(--tw-text-opacity)); +} + +.opacity-75 { + opacity: 0.75; +} + +@media (prefers-color-scheme: dark) { + .dark\:text-green-500 { + --tw-text-opacity: 1; + color: rgb(34 197 94 / var(--tw-text-opacity)); + } +} \ No newline at end of file diff --git a/examples/use_color_mode/tailwind.config.js b/examples/use_color_mode/tailwind.config.js new file mode 100644 index 0000000..bc09f5e --- /dev/null +++ b/examples/use_color_mode/tailwind.config.js @@ -0,0 +1,15 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: { + files: ["*.html", "./src/**/*.rs", "../../src/docs/**/*.rs"], + }, + theme: { + extend: {}, + }, + corePlugins: { + preflight: false, + }, + plugins: [ + require('@tailwindcss/forms'), + ], +} \ No newline at end of file diff --git a/examples/use_cycle_list/Cargo.toml b/examples/use_cycle_list/Cargo.toml new file mode 100644 index 0000000..c878e46 --- /dev/null +++ b/examples/use_cycle_list/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "use_cycle_list" +version = "0.1.0" +edition = "2021" + +[dependencies] +leptos = "0.3" +console_error_panic_hook = "0.1" +console_log = "1" +log = "0.4" +leptos-use = { path = "../..", features = ["docs"] } +web-sys = "0.3" + +[dev-dependencies] +wasm-bindgen = "0.2" +wasm-bindgen-test = "0.3.0" diff --git a/examples/use_cycle_list/README.md b/examples/use_cycle_list/README.md new file mode 100644 index 0000000..e672963 --- /dev/null +++ b/examples/use_cycle_list/README.md @@ -0,0 +1,23 @@ +A simple example for `use_cycle_list`. + +If you don't have it installed already, install [Trunk](https://trunkrs.dev/) and [Tailwind](https://tailwindcss.com/docs/installation) +as well as the nightly toolchain for Rust and the wasm32-unknown-unknown target: + +```bash +cargo install trunk +npm install -D tailwindcss @tailwindcss/forms +rustup toolchain install nightly +rustup target add wasm32-unknown-unknown +``` + +Then, open two terminals. In the first one, run: + +``` +npx tailwindcss -i ./input.css -o ./style/output.css --watch +``` + +In the second one, run: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/use_cycle_list/Trunk.toml b/examples/use_cycle_list/Trunk.toml new file mode 100644 index 0000000..3e4be08 --- /dev/null +++ b/examples/use_cycle_list/Trunk.toml @@ -0,0 +1,2 @@ +[build] +public_url = "/demo/" \ No newline at end of file diff --git a/examples/use_cycle_list/index.html b/examples/use_cycle_list/index.html new file mode 100644 index 0000000..ae249a6 --- /dev/null +++ b/examples/use_cycle_list/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/use_cycle_list/input.css b/examples/use_cycle_list/input.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/examples/use_cycle_list/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/examples/use_cycle_list/rust-toolchain.toml b/examples/use_cycle_list/rust-toolchain.toml new file mode 100644 index 0000000..271800c --- /dev/null +++ b/examples/use_cycle_list/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file diff --git a/examples/use_cycle_list/src/main.rs b/examples/use_cycle_list/src/main.rs new file mode 100644 index 0000000..dc8dabd --- /dev/null +++ b/examples/use_cycle_list/src/main.rs @@ -0,0 +1,32 @@ +use leptos::*; +use leptos_use::docs::demo_or_body; +use leptos_use::{use_cycle_list, UseCycleListReturn}; + +#[component] +fn Demo(cx: Scope) -> impl IntoView { + let UseCycleListReturn { + state, next, prev, .. + } = use_cycle_list( + cx, + vec![ + "Dog", "Cat", "Lizard", "Shark", "Whale", "Dolphin", "Octopus", "Seal", + ], + ); + + view! { cx, +
+
{state}
+ + +
+ } +} + +fn main() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + + mount_to(demo_or_body(), |cx| { + view! { cx, } + }) +} diff --git a/examples/use_cycle_list/style/output.css b/examples/use_cycle_list/style/output.css new file mode 100644 index 0000000..b648a86 --- /dev/null +++ b/examples/use_cycle_list/style/output.css @@ -0,0 +1,298 @@ +[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + border-radius: 0px; + padding-top: 0.5rem; + padding-right: 0.75rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + font-size: 1rem; + line-height: 1.5rem; + --tw-shadow: 0 0 #0000; +} + +[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + border-color: #2563eb; +} + +input::-moz-placeholder, textarea::-moz-placeholder { + color: #6b7280; + opacity: 1; +} + +input::placeholder,textarea::placeholder { + color: #6b7280; + opacity: 1; +} + +::-webkit-datetime-edit-fields-wrapper { + padding: 0; +} + +::-webkit-date-and-time-value { + min-height: 1.5em; +} + +::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field { + padding-top: 0; + padding-bottom: 0; +} + +select { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 0.5rem center; + background-repeat: no-repeat; + background-size: 1.5em 1.5em; + padding-right: 2.5rem; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; +} + +[multiple] { + background-image: initial; + background-position: initial; + background-repeat: unset; + background-size: initial; + padding-right: 0.75rem; + -webkit-print-color-adjust: unset; + print-color-adjust: unset; +} + +[type='checkbox'],[type='radio'] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding: 0; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + display: inline-block; + vertical-align: middle; + background-origin: border-box; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + flex-shrink: 0; + height: 1rem; + width: 1rem; + color: #2563eb; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + --tw-shadow: 0 0 #0000; +} + +[type='checkbox'] { + border-radius: 0px; +} + +[type='radio'] { + border-radius: 100%; +} + +[type='checkbox']:focus,[type='radio']:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 2px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); +} + +[type='checkbox']:checked,[type='radio']:checked { + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +[type='checkbox']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); +} + +[type='radio']:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); +} + +[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus { + border-color: transparent; + background-color: currentColor; +} + +[type='checkbox']:indeterminate { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e"); + border-color: transparent; + background-color: currentColor; + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; +} + +[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus { + border-color: transparent; + background-color: currentColor; +} + +[type='file'] { + background: unset; + border-color: inherit; + border-width: 0; + border-radius: 0; + padding: 0; + font-size: unset; + line-height: inherit; +} + +[type='file']:focus { + outline: 1px solid ButtonText; + outline: 1px auto -webkit-focus-ring-color; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.static { + position: static; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.font-bold { + font-weight: 700; +} + +.text-\[--brand-color\] { + color: var(--brand-color); +} + +.text-green-600 { + --tw-text-opacity: 1; + color: rgb(22 163 74 / var(--tw-text-opacity)); +} + +.opacity-75 { + opacity: 0.75; +} + +@media (prefers-color-scheme: dark) { + .dark\:text-green-500 { + --tw-text-opacity: 1; + color: rgb(34 197 94 / var(--tw-text-opacity)); + } +} \ No newline at end of file diff --git a/examples/use_cycle_list/tailwind.config.js b/examples/use_cycle_list/tailwind.config.js new file mode 100644 index 0000000..bc09f5e --- /dev/null +++ b/examples/use_cycle_list/tailwind.config.js @@ -0,0 +1,15 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: { + files: ["*.html", "./src/**/*.rs", "../../src/docs/**/*.rs"], + }, + theme: { + extend: {}, + }, + corePlugins: { + preflight: false, + }, + plugins: [ + require('@tailwindcss/forms'), + ], +} \ No newline at end of file diff --git a/src/core/element_maybe_signal.rs b/src/core/element_maybe_signal.rs index dbf582c..4759c30 100644 --- a/src/core/element_maybe_signal.rs +++ b/src/core/element_maybe_signal.rs @@ -156,6 +156,24 @@ where } } +impl<'a, E> From<&'a str> for ElementMaybeSignal +where + E: From + 'static, +{ + fn from(target: &'a str) -> Self { + Self::Static(document().query_selector(target).unwrap_or_default()) + } +} + +impl From for ElementMaybeSignal +where + E: From + 'static, +{ + fn from(target: String) -> Self { + Self::Static(document().query_selector(&target).unwrap_or_default()) + } +} + impl From<(Scope, Signal)> for ElementMaybeSignal where E: From + 'static, diff --git a/src/core/maybe_rw_signal.rs b/src/core/maybe_rw_signal.rs new file mode 100644 index 0000000..877c0cb --- /dev/null +++ b/src/core/maybe_rw_signal.rs @@ -0,0 +1,258 @@ +use leptos::*; +use std::pin::Pin; + +pub enum MaybeRwSignal +where + T: 'static, +{ + Static(T), + DynamicRw(ReadSignal, WriteSignal), + DynamicRead(Signal), +} + +impl Clone for MaybeRwSignal { + fn clone(&self) -> Self { + match self { + Self::Static(t) => Self::Static(t.clone()), + Self::DynamicRw(r, w) => Self::DynamicRw(r, w), + Self::DynamicRead(s) => Self::DynamicRead(s), + } + } +} + +impl Copy for MaybeRwSignal {} + +impl From for MaybeRwSignal { + fn from(t: T) -> Self { + Self::Static(t) + } +} + +impl Default for MaybeRwSignal { + fn default() -> Self { + Self::Static(T::default()) + } +} + +impl From> for MaybeRwSignal { + fn from(s: Signal) -> Self { + Self::DynamicRead(s) + } +} + +impl From> for MaybeRwSignal { + fn from(s: ReadSignal) -> Self { + Self::DynamicRead(s.into()) + } +} + +impl From> for MaybeRwSignal { + fn from(s: Memo) -> Self { + Self::DynamicRead(s.into()) + } +} + +impl From> for MaybeRwSignal { + fn from(s: RwSignal) -> Self { + let (r, w) = s.split(); + Self::DynamicRw(r, w) + } +} + +impl From<(ReadSignal, WriteSignal)> for MaybeRwSignal { + fn from(s: (ReadSignal, WriteSignal)) -> Self { + Self::DynamicRw(s.0, s.1) + } +} + +impl From<&str> for MaybeRwSignal { + fn from(s: &str) -> Self { + Self::Static(s.to_string()) + } +} + +impl SignalGet for MaybeRwSignal { + fn get(&self) -> T { + match self { + Self::Static(t) => t.clone(), + Self::DynamicRw(r, _) => r.get(), + Self::DynamicRead(s) => s.get(), + } + } + + fn try_get(&self) -> Option { + match self { + Self::Static(t) => Some(t.clone()), + Self::DynamicRw(r, _) => r.try_get(), + Self::DynamicRead(s) => s.try_get(), + } + } +} + +impl SignalWith for MaybeRwSignal { + fn with(&self, f: impl FnOnce(&T) -> O) -> O { + match self { + Self::Static(t) => f(t), + Self::DynamicRw(r, w) => r.with(f), + Self::DynamicRead(s) => s.with(f), + } + } + + fn try_with(&self, f: impl FnOnce(&T) -> O) -> Option { + match self { + Self::Static(t) => Some(f(t)), + Self::DynamicRw(r, _) => r.try_with(f), + Self::DynamicRead(s) => s.try_with(f), + } + } +} + +impl SignalWithUntracked for MaybeRwSignal { + fn with_untracked(&self, f: impl FnOnce(&T) -> O) -> O { + match self { + Self::Static(t) => f(t), + Self::DynamicRw(r, _) => r.with_untracked(f), + Self::DynamicRead(s) => s.with_untracked(f), + } + } + + fn try_with_untracked(&self, f: impl FnOnce(&T) -> O) -> Option { + match self { + Self::Static(t) => Some(f(t)), + Self::DynamicRw(r, _) => r.try_with_untracked(f), + Self::DynamicRead(s) => s.try_with_untracked(f), + } + } +} + +impl SignalGetUntracked for MaybeRwSignal { + fn get_untracked(&self) -> T { + match self { + Self::Static(t) => t.clone(), + Self::DynamicRw(r, _) => r.get_untracked(), + Self::DynamicRead(s) => s.get_untracked(), + } + } + + fn try_get_untracked(&self) -> Option { + match self { + Self::Static(t) => Some(t.clone()), + Self::DynamicRw(r, _) => r.try_get_untracked(), + Self::DynamicRead(s) => s.try_get_untracked(), + } + } +} + +impl SignalStream for MaybeRwSignal { + fn to_stream(&self, cx: Scope) -> Pin>> { + match self { + Self::Static(t) => { + let t = t.clone(); + + let stream = futures::stream::once(async move { t }); + + Box::bin(stream) + } + Self::DynamicRw(r, _) => r.to_stream(cx), + Self::DynamicRead(s) => s.to_stream(cx), + } + } +} + +impl MaybeRwSignal { + pub fn derive(cx: Scope, derived_signal: impl Fn() -> T + 'static) -> Self { + Self::DynamicRead(Signal::derive(cx, derived_signal)) + } +} + +impl SignalSetUntracked for MaybeRwSignal { + fn set_untracked(&self, new_value: T) { + match self { + Self::DynamicRw(_, w) => w.set_untracked(new_value), + _ => { + // do nothing + } + } + } + + fn try_set_untracked(&self, new_value: T) -> Option { + match self { + Self::DynamicRw(_, w) => w.try_set_untracked(new_value), + _ => Some(new_value), + } + } +} + +impl SignalUpdateUntracked for MaybeRwSignal { + #[inline(always)] + fn update_untracked(&self, f: impl FnOnce(&mut T)) { + match self { + Self::DynamicRw(_, w) => w.update_untracked(f), + _ => { + // do nothing + } + } + } + + #[inline(always)] + fn try_update_untracked(&self, f: impl FnOnce(&mut T) -> O) -> Option { + match self { + Self::DynamicRw(_, w) => w.try_update_untracked(f), + _ => Some(f()), + } + } +} + +impl SignalUpdate for MaybeRwSignal { + #[inline(always)] + fn update(&self, f: impl FnOnce(&mut T)) { + match self { + Self::DynamicRw(_, w) => w.update(f), + _ => { + // do nothing + } + } + } + + #[inline(always)] + fn try_update(&self, f: impl FnOnce(&mut T) -> O) -> Option { + match self { + Self::DynamicRw(_, w) => w.try_update(f), + _ => Some(f()), + } + } +} + +impl SignalSet for MaybeRwSignal { + #[inline(always)] + fn set(&self, new_value: T) { + match self { + Self::DynamicRw(_, w) => w.set(new_value), + _ => { + // do nothing + } + } + } + + fn try_set(&self, new_value: T) -> Option { + match self { + Self::DynamicRw(_, w) => w.try_set(new_value), + _ => Some(new_value), + } + } +} + +impl SignalDispose for MaybeRwSignal { + fn dispose(self) { + match self { + Self::DynamicRw(r, w) => { + r.dispose(); + w.dispose(); + } + Self::DynamicRead(s) => s.dispose(), + _ => { + // do nothing + } + } + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs index 0128503..b937068 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,9 +1,13 @@ mod element_maybe_signal; mod elements_maybe_signal; +mod maybe_rw_signal; mod position; mod size; +mod storage; pub use element_maybe_signal::*; pub use elements_maybe_signal::*; +pub use maybe_rw_signal::*; pub use position::*; pub use size::*; +pub use storage::*; diff --git a/src/core/storage.rs b/src/core/storage.rs new file mode 100644 index 0000000..95fe29d --- /dev/null +++ b/src/core/storage.rs @@ -0,0 +1,22 @@ +use leptos::window; +use wasm_bindgen::JsValue; + +/// Local or session storage or a custom store that is a `web_sys::Storage`. +#[doc(cfg(feature = "storage"))] +#[derive(Default)] +pub enum StorageType { + #[default] + Local, + Session, + Custom(web_sys::Storage), +} + +impl StorageType { + pub fn into_storage(self) -> Result, JsValue> { + match self { + StorageType::Local => window().local_storage(), + StorageType::Session => window().session_storage(), + StorageType::Custom(storage) => Ok(Some(storage)), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 99cf7d3..9b755fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,8 @@ pub use use_element_size::*; pub use use_resize_observer::*; mod on_click_outside; +mod use_cycle_list; +mod use_color_mode; mod use_active_element; mod use_breakpoints; mod use_css_var; @@ -47,6 +49,8 @@ mod watch_throttled; mod whenever; pub use on_click_outside::*; +pub use use_cycle_list::*; +pub use use_color_mode::*; pub use use_active_element::*; pub use use_breakpoints::*; pub use use_css_var::*; diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 73bbe30..3806bb2 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -11,6 +11,8 @@ use serde_json::Error; use std::time::Duration; use wasm_bindgen::{JsCast, JsValue}; +pub use crate::core::StorageType; + const CUSTOM_STORAGE_EVENT_NAME: &str = "leptos-use-storage"; /// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) / [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). @@ -478,22 +480,3 @@ impl UseStorageOptions { filter ); } - -/// Local or session storage or a custom store that is a `web_sys::Storage`. -#[derive(Default)] -pub enum StorageType { - #[default] - Local, - Session, - Custom(web_sys::Storage), -} - -impl StorageType { - pub fn into_storage(self) -> Result, JsValue> { - match self { - StorageType::Local => window().local_storage(), - StorageType::Session => window().session_storage(), - StorageType::Custom(storage) => Ok(Some(storage)), - } - } -} diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs new file mode 100644 index 0000000..5049de6 --- /dev/null +++ b/src/use_color_mode.rs @@ -0,0 +1,456 @@ +use crate::core::ElementMaybeSignal; +#[cfg(feature = "storage")] +use crate::storage::{use_storage_with_options, UseStorageOptions}; +#[cfg(feature = "storage")] +use serde::{Deserialize, Serialize}; + +use crate::core::StorageType; +use crate::use_preferred_dark; +use crate::utils::CloneableFnWithArg; +use default_struct_builder::DefaultBuilder; +use leptos::*; +use std::marker::PhantomData; +use wasm_bindgen::JsCast; + +/// Reactive color mode (dark / light / customs) with auto data persistence. +/// +/// > Data persistence is only enabled when the crate feature **`storage`** is enabled. You +/// can use the function without it but the mode won't be persisted. +/// +/// ## Demo +/// +/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_color_mode) +/// +/// ## Usage +/// +/// ``` +/// # use leptos::*; +/// use leptos_use::{use_color_mode, UseColorModeReturn}; +/// # +/// # #[component] +/// # fn Demo(cx: Scope) -> impl IntoView { +/// let UseColorModeReturn { +/// mode, // Signal +/// set_mode, +/// .. +/// } = use_color_mode(cx); +/// # +/// # view! { cx, } +/// # } +/// ``` +/// +/// By default, it will match with users' browser preference using [`use_preferred_dark`] (a.k.a. `ColorMode::Auto`). +/// When reading the signal, it will by default return the current color mode (`ColorMode::Dark`, `ColorMode::Light` or +/// your custom modes `ColorMode::Custom("some-custom")`). The `ColorMode::Auto` variant can +/// be included in the returned modes by enabling the `emit_auto` option and using [`use_color_mode_with_options`]. +/// When writing to the signal (`set_mode`), it will trigger DOM updates and persist the color mode to local +/// storage (or your custom storage). You can pass `ColorMode::Auto` to set back to auto mode. +/// +/// ``` +/// # use leptos::*; +/// use leptos_use::{ColorMode, use_color_mode, UseColorModeReturn}; +/// # +/// # #[component] +/// # fn Demo(cx: Scope) -> impl IntoView { +/// # let UseColorModeReturn { mode, set_mode, .. } = use_color_mode(cx); +/// # +/// mode(); // ColorMode::Dark or ColorMode::Light +/// +/// set_mode(ColorMode::Dark); // change to dark mode and persist (with feature `storage`) +/// +/// set_mode(ColorMode::Auto); // change to auto mode +/// # +/// # view! { cx, } +/// # } +/// ``` +/// +/// ## Options +/// +/// ``` +/// # use leptos::*; +/// use leptos_use::{use_color_mode_with_options, UseColorModeOptions, UseColorModeReturn}; +/// # +/// # #[component] +/// # fn Demo(cx: Scope) -> impl IntoView { +/// let UseColorModeReturn { mode, set_mode, .. } = use_color_mode_with_options( +/// cx, +/// UseColorModeOptions::default() +/// .attribute("theme") // instead of writing to `class` +/// .custom_modes(vec![ +/// // custom colors in addition to light/dark +/// "dim".to_string(), +/// "cafe".to_string(), +/// ]), +/// ); // Signal +/// # +/// # view! { cx, } +/// # } +/// ``` +/// +/// ## See also +/// +/// * [`use_dark`] +/// * [`use_preferred_dark`] +/// * [`use_storage`] +pub fn use_color_mode(cx: Scope) -> UseColorModeReturn { + use_color_mode_with_options(cx, UseColorModeOptions::default()) +} + +/// Version of [`use_color_mode`] that takes a `UseColorModeOptions`. See [`use_color_mode`] for how to use. +pub fn use_color_mode_with_options( + cx: Scope, + options: UseColorModeOptions, +) -> UseColorModeReturn +where + El: Clone, + (Scope, El): Into>, + T: Into + Clone + 'static, +{ + let UseColorModeOptions { + target, + attribute, + initial_value, + on_changed, + storage_signal, + custom_modes, + storage_key, + storage, + storage_enabled, + emit_auto, + transition_enabled, + listen_to_storage_changes, + _marker, + } = options; + + let modes: Vec = custom_modes + .into_iter() + .chain(vec![ColorMode::Dark.to_string(), ColorMode::Light.to_string()].into_iter()) + .collect(); + + let preferred_dark = use_preferred_dark(cx); + + let system = Signal::derive(cx, move || { + if preferred_dark.get() { + ColorMode::Dark + } else { + ColorMode::Light + } + }); + + let (store, set_store) = get_store_signal( + cx, + initial_value, + storage_signal, + &storage_key, + storage_enabled, + storage, + listen_to_storage_changes, + ); + + let state = Signal::derive(cx, move || { + let value = store.get(); + if value == ColorMode::Auto { + system.get() + } else { + value + } + }); + + let target = (cx, target).into(); + + let update_html_attrs = { + move |target: ElementMaybeSignal, + attribute: String, + value: ColorMode| { + let el = target.get_untracked(); + + if let Some(el) = el { + let el = el.into(); + + let mut style: Option = None; + if !transition_enabled { + if let Ok(styl) = document().create_element("style") { + if let Some(head) = document().head() { + let styl: web_sys::HtmlStyleElement = styl.unchecked_into(); + let style_string = "*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}"; + styl.set_text_content(Some(style_string)); + let _ = head.append_child(&styl); + style = Some(styl); + } + } + } + + if attribute == "class" { + for mode in &modes { + if value == ColorMode::from_string(mode) { + let _ = el.class_list().add_1(mode); + } else { + let _ = el.class_list().remove_1(mode); + } + } + } else { + let _ = el.set_attribute(&attribute, &value.to_string()); + } + + if !transition_enabled { + if let Some(style) = style { + if let Some(head) = document().head() { + // Calling getComputedStyle forces the browser to redraw + if let Ok(Some(style)) = window().get_computed_style(&style) { + let _ = style.get_property_value("opacity"); + } + + let _ = head.remove_child(&style); + } + } + } + } + } + }; + + let default_on_changed = move |mode: ColorMode| { + update_html_attrs(target.clone(), attribute.clone(), mode); + }; + + let on_changed = move |mode: ColorMode| { + on_changed(UseColorModeOnChangeArgs { + mode, + default_handler: Box::new(default_on_changed.clone()), + }); + }; + + create_effect(cx, { + let on_changed = on_changed.clone(); + + move |_| { + on_changed.clone()(state.get()); + } + }); + + on_cleanup(cx, move || { + on_changed(state.get()); + }); + + let mode = Signal::derive(cx, move || if emit_auto { store.get() } else { state() }); + + UseColorModeReturn { + mode, + set_mode: set_store, + store: store.into(), + set_store, + system, + state, + } +} + +#[cfg(not(feature = "storage"))] +/// Color modes +#[derive(Clone, Default, PartialEq, Eq, Hash)] +pub enum ColorMode { + #[default] + Auto, + Light, + Dark, + Custom(String), +} + +#[cfg(not(feature = "storage"))] +fn get_store_signal( + cx: Scope, + initial_value: MaybeSignal, + storage_signal: Option>, + storage_key: &String, + storage_enabled: bool, + storage: StorageType, + listen_to_storage_changes: bool, +) -> (ReadSignal, WriteSignal) { + if let Some(storage_signal) = storage_signal { + storage_signal.split() + } else { + create_signal(cx, initial_value.get_untracked()) + } +} + +#[cfg(feature = "storage")] +/// Color modes +#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum ColorMode { + #[default] + Auto, + Light, + Dark, + Custom(String), +} + +#[cfg(feature = "storage")] +fn get_store_signal( + cx: Scope, + initial_value: MaybeSignal, + storage_signal: Option>, + storage_key: &str, + storage_enabled: bool, + storage: StorageType, + listen_to_storage_changes: bool, +) -> (ReadSignal, WriteSignal) { + if let Some(storage_signal) = storage_signal { + storage_signal.split() + } else if storage_enabled { + let (store, set_store, _) = use_storage_with_options( + cx, + storage_key, + initial_value.get_untracked(), + UseStorageOptions::default() + .listen_to_storage_changes(listen_to_storage_changes) + .storage_type(storage), + ); + + (store, set_store) + } else { + create_signal(cx, initial_value.get_untracked()) + } +} + +impl ToString for ColorMode { + fn to_string(&self) -> String { + use ColorMode::*; + + match self { + Auto => "".to_string(), + Light => "light".to_string(), + Dark => "dark".to_string(), + Custom(v) => v.clone(), + } + } +} + +impl ColorMode { + pub fn from_string(s: &str) -> ColorMode { + match s { + "auto" => ColorMode::Auto, + "" => ColorMode::Auto, + "light" => ColorMode::Light, + "dark" => ColorMode::Dark, + _ => ColorMode::Custom(s.to_string()), + } + } +} + +/// Arguments to [`UseColorModeOptions::on_changed`] +#[derive(Clone)] +pub struct UseColorModeOnChangeArgs { + /// The color mode to change to. + pub mode: ColorMode, + + /// The default handler that would have been called if the `on_changed` handler had not been specified. + pub default_handler: Box>, +} + +#[derive(DefaultBuilder)] +pub struct UseColorModeOptions +where + El: Clone, + (Scope, El): Into>, + T: Into + Clone + 'static, +{ + /// Element that the color mode will be applied to. Defaults to `"html"`. + target: El, + + /// HTML attribute applied to the target element. Defaults to `"class"`. + #[builder(into)] + attribute: String, + + /// Initial value of the color mode. Defaults to `"Auto"`. + #[builder(into)] + initial_value: MaybeSignal, + + /// Custom modes that you plan to use as `ColorMode::Custom(x)`. Defaults to `vec![]`. + custom_modes: Vec, + + /// Custom handler that is called on updates. + /// If specified this will override the default behavior. + /// To get the default behaviour back you can call the provided `default_handler` function. + #[builder(into)] + on_changed: Box>, + + /// When provided, `useStorage` will be skipped. + /// Storage requires the *create feature* **`storage`** to be enabled. + /// Defaults to `None`. + #[builder(into)] + storage_signal: Option>, + + /// Key to persist the data into localStorage/sessionStorage. + /// Storage requires the *create feature* **`storage`** to be enabled. + /// Defaults to `"leptos-use-color-scheme"`. + #[builder(into)] + storage_key: String, + + /// Storage type, can be `Local` or `Session` or custom. + /// Storage requires the *create feature* **`storage`** to be enabled. + /// Defaults to `Local`. + storage: StorageType, + + /// If the color mode should be persisted. If `true` this required the + /// *create feature* **`storage`** to be enabled. + /// Defaults to `true` and is forced to `false` if the feature **`storage`** is not enabled. + storage_enabled: bool, + + /// Emit `auto` mode from state + /// + /// When set to `true`, preferred mode won't be translated into `light` or `dark`. + /// This is useful when the fact that `auto` mode was selected needs to be known. + /// + /// Defaults to `false`. + emit_auto: bool, + + /// If transitions on color mode change are enabled. Defaults to `false`. + transition_enabled: bool, + + /// Listen to changes to this storage key from somewhere else. + /// Storage requires the *create feature* **`storage`** to be enabled. + /// Defaults to true. + listen_to_storage_changes: bool, + + #[builder(skip)] + _marker: PhantomData, +} + +impl Default for UseColorModeOptions<&'static str, web_sys::Element> { + fn default() -> Self { + Self { + target: "html", + attribute: "class".into(), + initial_value: ColorMode::Auto.into(), + custom_modes: vec![], + on_changed: Box::new(move |args: UseColorModeOnChangeArgs| { + (args.default_handler)(args.mode) + }), + storage_signal: None, + storage_key: "leptos-use-color-scheme".into(), + storage: StorageType::default(), + storage_enabled: true, + emit_auto: false, + transition_enabled: false, + listen_to_storage_changes: true, + _marker: PhantomData, + } + } +} + +/// Return type of [`use_color_mode`] +pub struct UseColorModeReturn { + /// Main value signal of the color mode + pub mode: Signal, + /// Main value setter signal of the color mode + pub set_mode: WriteSignal, + + /// Direct access to the returned signal of [`use_storage`] if enabled or [`UseColorModeOptions::storage_signal`] if provided + pub store: Signal, + /// Direct write access to the returned signal of [`use_storage`] if enabled or [`UseColorModeOptions::storage_signal`] if provided + pub set_store: WriteSignal, + + /// Signal of the system's preferred color mode that you would get from a media query + pub system: Signal, + + /// When [`UseColorModeOptions::emit_auto`] is `false` this is the same as `mode`. This will never report `ColorMode::Auto` but always on of the other modes. + pub state: Signal, +} diff --git a/src/use_cycle_list.rs b/src/use_cycle_list.rs new file mode 100644 index 0000000..4d770ee --- /dev/null +++ b/src/use_cycle_list.rs @@ -0,0 +1,231 @@ +use crate::watch; +use default_struct_builder::DefaultBuilder; +use leptos::*; + +/// Cycle through a list of items. +/// +/// ## Demo +/// +/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_cycle_list) +/// +/// ## Usage +/// +/// ``` +/// # use leptos::*; +/// use leptos_use::{use_cycle_list, UseCycleListReturn}; +/// # +/// # #[component] +/// # fn Demo(cx: Scope) -> impl IntoView { +/// let UseCycleListReturn { state, next, prev, .. } = use_cycle_list( +/// cx, +/// vec!["Dog", "Cat", "Lizard", "Shark", "Whale", "Dolphin", "Octopus", "Seal"] +/// ); +/// +/// log!("{}", state()); // "Dog" +/// +/// prev(); +/// +/// log!("{}", state()); // "Seal" +/// # +/// # view! { cx, } +/// # } +/// ``` + +pub fn use_cycle_list( + cx: Scope, + list: L, +) -> UseCycleListReturn< + T, + impl Fn(usize) -> T + Clone, + impl Fn() + Clone, + impl Fn() + Clone, + impl Fn(i64) -> T + Clone, +> +where + T: Clone + PartialEq + 'static, + L: Into>>, +{ + use_cycle_list_with_options(cx, list, UseCycleListOptions::default()) +} + +pub fn use_cycle_list_with_options( + cx: Scope, + list: L, + options: UseCycleListOptions, +) -> UseCycleListReturn< + T, + impl Fn(usize) -> T + Clone, + impl Fn() + Clone, + impl Fn() + Clone, + impl Fn(i64) -> T + Clone, +> +where + T: Clone + PartialEq + 'static, + L: Into>>, +{ + let UseCycleListOptions { + initial_value, + fallback_index, + get_position, + } = options; + + let list = list.into(); + + let get_initial_value = { + let list = list.get_untracked(); + let first = list.first().cloned(); + + move || { + if let Some(initial_value) = initial_value { + initial_value.get() + } else { + first.expect("The provided list shouldn't be empty") + } + } + }; + + let (state, set_state) = create_signal(cx, get_initial_value()); + + let index = { + let list = list.clone(); + + create_memo(cx, move |_| { + list.with(|list| { + let index = get_position(&state.get(), list); + + if let Some(index) = index { + index + } else { + fallback_index + } + }) + }) + }; + + let set = { + let list = list.clone(); + + move |i: usize| { + list.with(|list| { + let length = list.len(); + + let index = i % length; + let value = list[index].clone(); + + set_state.update({ + let value = value.clone(); + + move |v| *v = value + }); + + value + }) + } + }; + + let shift = { + let list = list.clone(); + let set = set.clone(); + + move |delta: i64| { + let index = list.with(|list| { + let length = list.len() as i64; + + let i = index.get_untracked() as i64 + delta; + (i % length) + length + }); + + set(index as usize) + } + }; + + let next = { + let shift = shift.clone(); + + move || { + shift(1); + } + }; + + let prev = { + let shift = shift.clone(); + + move || { + shift(-1); + } + }; + + let _ = { + let set = set.clone(); + + watch(cx, move || list.get(), move |_, _, _| set(index.get())) + }; + + UseCycleListReturn { + state: state.into(), + set_state, + index: index.into(), + set_index: set, + next, + prev, + shift, + } +} + +/// Options for [`use_cycle_list_with_options`]. +#[derive(DefaultBuilder)] +pub struct UseCycleListOptions +where + T: Clone + PartialEq + 'static, +{ + /// The initial value of the state. Can be a Signal. If none is provided the first entry + /// of the list will be used. + #[builder(keep_type)] + initial_value: Option>, + + /// The default index when the current value is not found in the list. + /// For example when `get_index_of` returns `None`. + fallback_index: usize, + + /// Custom function to get the index of the current value. Defaults to `Iterator::position()` + #[builder(keep_type)] + get_position: fn(&T, &Vec) -> Option, +} + +impl Default for UseCycleListOptions +where + T: Clone + PartialEq + 'static, +{ + fn default() -> Self { + Self { + initial_value: None, + fallback_index: 0, + get_position: |value: &T, list: &Vec| list.iter().position(|v| v == value), + } + } +} + +/// Return type of [`use_cycle_list`]. +pub struct UseCycleListReturn +where + T: Clone + PartialEq + 'static, + SetFn: Fn(usize) -> T + Clone, + NextFn: Fn() + Clone, + PrevFn: Fn() + Clone, + ShiftFn: Fn(i64) -> T + Clone, +{ + /// Current value + pub state: Signal, + /// Set current value + pub set_state: WriteSignal, + /// Current index of current value in list + pub index: Signal, + /// Set current index of current value in list + pub set_index: SetFn, + /// Go to next value (cyclic) + pub next: NextFn, + /// Go to previous value (cyclic) + pub prev: PrevFn, + /// Move by the specified amount from the current value (cyclic) + pub shift: ShiftFn, +} diff --git a/template/.ffizer.yaml b/template/.ffizer.yaml index 58c6216..2990f0e 100644 --- a/template/.ffizer.yaml +++ b/template/.ffizer.yaml @@ -1,6 +1,11 @@ variables: - name: function_name ask: Name of the function + - name: scope + ask: "Is the first parameter `cx: Scope`?" + select_in_values: + - "cx" + - "" - name: category ask: Documentation category (lower case) - name: module diff --git a/template/examples/{{ function_name }}/src/main.ffizer.hbs.rs b/template/examples/{{ function_name }}/src/main.ffizer.hbs.rs index a15670e..b1669e0 100644 --- a/template/examples/{{ function_name }}/src/main.ffizer.hbs.rs +++ b/template/examples/{{ function_name }}/src/main.ffizer.hbs.rs @@ -5,7 +5,7 @@ use leptos_use{{#if module}}::{{ module }}{{/if}}::{{ function_name }}; #[component] fn Demo(cx: Scope) -> impl IntoView { - {{ function_name }}(); + {{ function_name }}({{#if scope }}cx{{/if}}); view! { cx, } diff --git a/template/src/{{ module }}/{{ function_name }}.ffizer.hbs.rs b/template/src/{{ module }}/{{ function_name }}.ffizer.hbs.rs index 1093ceb..cfbfc57 100644 --- a/template/src/{{ module }}/{{ function_name }}.ffizer.hbs.rs +++ b/template/src/{{ module }}/{{ function_name }}.ffizer.hbs.rs @@ -15,24 +15,24 @@ use leptos::*; /// # /// # #[component] /// # fn Demo(cx: Scope) -> impl IntoView { -/// {{ function_name }}(); +/// {{ function_name }}({{#if scope }}cx{{/if}}); /// # /// # view! { cx, } /// # } -/// ``` -{{#if feature}}#[doc(cfg(feature = "{{feature}}"))]{{/if}} -pub fn {{ function_name }}() -> {{ to_pascal_case function_name }}Return { - {{ function_name }}_with_options({{ to_pascal_case function_name }}Options::default()) +/// ```{{#if feature}} +#[doc(cfg(feature = "{{feature}}"))]{{/if}} +pub fn {{ function_name }}({{#if scope }}cx: Scope{{/if}}) -> {{ to_pascal_case function_name }}Return { + {{ function_name }}_with_options({{#if scope }}cx, {{/if}}{{ to_pascal_case function_name }}Options::default()) } -/// Version of [`{{ function_name }}`] that takes a `{{ to_pascal_case function_name }}Options`. See [`{{ function_name }}`] for how to use. -{{#if feature}}#[doc(cfg(feature = "{{feature}}"))]{{/if}} -pub fn {{ function_name }}_with_options(options: {{ to_pascal_case function_name }}Options) -> {{ to_pascal_case function_name }}Return { +/// Version of [`{{ function_name }}`] that takes a `{{ to_pascal_case function_name }}Options`. See [`{{ function_name }}`] for how to use.{{#if feature}} +#[doc(cfg(feature = "{{feature}}"))]{{/if}} +pub fn {{ function_name }}_with_options({{#if scope }}cx: Scope, {{/if}}options: {{ to_pascal_case function_name }}Options) -> {{ to_pascal_case function_name }}Return { {{ to_pascal_case function_name }}Return {} } -/// Options for [`{{ function_name }}_with_options`]. -{{#if feature}}#[doc(cfg(feature = "{{feature}}"))]{{/if}} +/// Options for [`{{ function_name }}_with_options`].{{#if feature}} +#[doc(cfg(feature = "{{feature}}"))]{{/if}} #[derive(DefaultBuilder)] pub struct {{ to_pascal_case function_name }}Options {}