diff --git a/AGENTS.md b/AGENTS.md index 0f3190b..e047dc3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,265 +1,136 @@ -You are an expert [0.7 Dioxus](https://dioxuslabs.com/learn/0.7) assistant. Dioxus 0.7 changes every api in dioxus. Only use this up to date documentation. `cx`, `Scope`, and `use_state` are gone +# MCALC Agent Guidelines -Provide concise code examples with detailed descriptions +## Project Overview -# Dioxus Dependency +A simple Dioxus calculator application. Uses Dioxus 0.7 for UI with static CSS styling. -You can add Dioxus to your `Cargo.toml` like this: +## Build Commands -```toml -[dependencies] -dioxus = { version = "0.7.1" } +```bash +# Development +cargo run # Run desktop app (default feature) +cargo run --features web # Run web version -[features] -default = ["web", "webview", "server"] -web = ["dioxus/web"] -webview = ["dioxus/desktop"] -server = ["dioxus/server"] +# Build +cargo build # Debug build +cargo build --release # Release build + +# Testing +cargo test # Run all tests +cargo test # Run specific test + +# Linting & Formatting +cargo fmt # Format code +cargo fmt -- --check # Check formatting without changes +cargo clippy # Run linter +cargo clippy -- -D warnings # Lint with deny warnings ``` -# Launching your application +## Code Style -You need to create a main function that sets up the Dioxus runtime and mounts your root component. +### Formatting +- Use `cargo fmt` with default rustfmt settings +- 4-space indentation +- Maximum line length: 100 characters -```rust -use dioxus::prelude::*; +### Naming Conventions +- Components: PascalCase (e.g., `Calculator`, `DisplayArea`) +- Functions/Methods: snake_case (e.g., `calculate_result`, `input_digit`) +- Signals: snake_case with `_` suffix convention (e.g., `first_num`, `operator`) +- Types/Enums: PascalCase -fn main() { - dioxus::launch(App); -} +### Imports +- Group 1: `use dioxus::prelude::*` (Dioxus core) +- Group 2: `use crate::` (local modules) +- Standard library imports last -#[component] -fn App() -> Element { - rsx! { "Hello, Dioxus!" } -} -``` - -Then serve with `dx serve`: - -```sh -curl -sSL http://dioxus.dev/install.sh | sh -dx serve -``` - -# UI with RSX - -```rust -rsx! { - div { - class: "container", // Attribute - color: "red", // Inline styles - width: if condition { "100%" }, // Conditional attributes - "Hello, Dioxus!" - } - // Prefer loops over iterators - for i in 0..5 { - div { "{i}" } // use elements or components directly in loops - } - if condition { - div { "Condition is true!" } // use elements or components directly in conditionals - } - - {children} // Expressions are wrapped in brace - {(0..5).map(|i| rsx! { span { "Item {i}" } })} // Iterators must be wrapped in braces -} -``` - -# Assets - -The asset macro can be used to link to local files to use in your project. All links start with `/` and are relative to the root of your project. - -```rust -rsx! { - img { - src: asset!("/assets/image.png"), - alt: "An image", - } -} -``` - -## Styles - -The `document::Stylesheet` component will inject the stylesheet into the `` of the document - -```rust -rsx! { - document::Stylesheet { - href: asset!("/assets/styles.css"), - } -} -``` - -# Components - -Components are the building blocks of apps - -* Component are functions annotated with the `#[component]` macro. -* The function name must start with a capital letter or contain an underscore. -* A component re-renders only under two conditions: - 1. Its props change (as determined by `PartialEq`). - 2. An internal reactive state it depends on is updated. +### Components (Dioxus 0.7) +- Prefix with `#[allow(non_snake_case)]` if needed +- Props must be owned values (use `String` not `&str`) +- Use `Signal` for reactive state +- Components return `Element` ```rust #[component] -fn Input(mut value: Signal) -> Element { - rsx! { - input { - value, - oninput: move |e| { - *value.write() = e.value(); - }, - onkeydown: move |e| { - if e.key() == Key::Enter { - value.write().clear(); - } - }, - } - } +fn MyComponent(mut value: Signal) -> Element { + rsx! { div { "{value}" } } } ``` -Each component accepts function arguments (props) +### State Management +- Use `use_signal(|| initial_value)` for local state +- Use `.write()` for mutable access, `.read()` for references +- Prefer `*signal.write() = value` over `signal.set(value)` for direct mutation -* Props must be owned values, not references. Use `String` and `Vec` instead of `&str` or `&[T]`. -* Props must implement `PartialEq` and `Clone`. -* To make props reactive and copy, you can wrap the type in `ReadOnlySignal`. Any reactive state like memos and resources that read `ReadOnlySignal` props will automatically re-run when the prop changes. +### Error Handling +- Prefer early returns with `return` or `?` operator +- Use `Result` for fallible operations +- Silent failures acceptable for user input parsing (graceful degradation) -# State +### CSS Guidelines +- CSS files in `/assets/main.css` +- Use CSS custom properties (variables) for theming +- Follow BYTEMALTE design system colors when applicable +- Class naming: kebab-case (e.g., `btn-number`, `display-area`) -A signal is a wrapper around a value that automatically tracks where it's read and written. Changing a signal's value causes code that relies on the signal to rerun. +## File Structure -## Local State - -The `use_signal` hook creates state that is local to a single component. You can call the signal like a function (e.g. `my_signal()`) to clone the value, or use `.read()` to get a reference. `.write()` gets a mutable reference to the value. - -Use `use_memo` to create a memoized value that recalculates when its dependencies change. Memos are useful for expensive calculations that you don't want to repeat unnecessarily. - -```rust -#[component] -fn Counter() -> Element { - let mut count = use_signal(|| 0); - let mut doubled = use_memo(move || count() * 2); // doubled will re-run when count changes because it reads the signal - - rsx! { - h1 { "Count: {count}" } // Counter will re-render when count changes because it reads the signal - h2 { "Doubled: {doubled}" } - button { - onclick: move |_| *count.write() += 1, // Writing to the signal rerenders Counter - "Increment" - } - button { - onclick: move |_| count.with_mut(|count| *count += 1), // use with_mut to mutate the signal - "Increment with with_mut" - } - } -} +``` +src/ + main.rs # App entry point + ui.rs # UI components + logic.rs # Business logic +assets/ + main.css # Global styles +public/ # Static assets served at root ``` -## Context API +## Design System (BYTEMALTE) -The Context API allows you to share state down the component tree. A parent provides the state using `use_context_provider`, and any child can access it with `use_context` +When modifying CSS, follow these conventions: +| Element | Border Radius | +|---------|---------------| +| Buttons | 8px | +| Cards | 16px | +| Inputs | 8px | + +Colors: +- Primary: `#8888FF` +- Secondary: `#3DDC84` +- Background: `#0F172A` +- Surface: `#1E293B` +- Error: `#EF4444` + +--- + +## Dioxus 0.7 Reference + +### Components ```rust #[component] fn App() -> Element { - let mut theme = use_signal(|| "light".to_string()); - use_context_provider(|| theme); // Provide a type to children - rsx! { Child {} } -} - -#[component] -fn Child() -> Element { - let theme = use_context::>(); // Consume the same type - rsx! { - div { - "Current theme: {theme}" - } - } + let mut count = use_signal(|| 0); + rsx! { + button { onclick: move |_| *count.write() += 1, "{count}" } + } } ``` -# Async - -For state that depends on an asynchronous operation (like a network request), Dioxus provides a hook called `use_resource`. This hook manages the lifecycle of the async task and provides the result to your component. - -* The `use_resource` hook takes an `async` closure. It re-runs this closure whenever any signals it depends on (reads) are updated -* The `Resource` object returned can be in several states when read: -1. `None` if the resource is still loading -2. `Some(value)` if the resource has successfully loaded - +### Signals ```rust -let mut dog = use_resource(move || async move { - // api request -}); - -match dog() { - Some(dog_info) => rsx! { Dog { dog_info } }, - None => rsx! { "Loading..." }, -} +let mut value = use_signal(|| String::new()); +value(); // clone value +value.read(); // &T reference +value.write(); // &mut T ``` -# Routing - -All possible routes are defined in a single Rust `enum` that derives `Routable`. Each variant represents a route and is annotated with `#[route("/path")]`. Dynamic Segments can capture parts of the URL path as parameters by using `:name` in the route string. These become fields in the enum variant. - -The `Router {}` component is the entry point that manages rendering the correct component for the current URL. - -You can use the `#[layout(NavBar)]` to create a layout shared between pages and place an `Outlet {}` inside your layout component. The child routes will be rendered in the outlet. - +### RSX Patterns ```rust -#[derive(Routable, Clone, PartialEq)] -enum Route { - #[layout(NavBar)] // This will use NavBar as the layout for all routes - #[route("/")] - Home {}, - #[route("/blog/:id")] // Dynamic segment - BlogPost { id: i32 }, -} - -#[component] -fn NavBar() -> Element { - rsx! { - a { href: "/", "Home" } - Outlet {} // Renders Home or BlogPost - } -} - -#[component] -fn App() -> Element { - rsx! { Router:: {} } +rsx! { + div { + for item in items { span { "{item}" } } + if condition { p { "Shown" } } + } } ``` - -```toml -dioxus = { version = "0.7.1", features = ["router"] } -``` - -# Fullstack - -Fullstack enables server rendering and ipc calls. It uses Cargo features (`server` and a client feature like `web`) to split the code into a server and client binaries. - -```toml -dioxus = { version = "0.7.1", features = ["fullstack"] } -``` - -## Server Functions - -Use the `#[post]` / `#[get]` macros to define an `async` function that will only run on the server. On the server, this macro generates an API endpoint. On the client, it generates a function that makes an HTTP request to that endpoint. - -```rust -#[post("/api/double/:path/&query")] -async fn double_server(number: i32, path: String, query: i32) -> Result { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - Ok(number * 2) -} -``` - -## Hydration - -Hydration is the process of making a server-rendered HTML page interactive on the client. The server sends the initial HTML, and then the client-side runs, attaches event listeners, and takes control of future rendering. - -### Errors -The initial UI rendered by the component on the client must be identical to the UI rendered on the server. - -* Use the `use_server_future` hook instead of `use_resource`. It runs the future on the server, serializes the result, and sends it to the client, ensuring the client has the data immediately for its first render. -* Any code that relies on browser-specific APIs (like accessing `localStorage`) must be run *after* hydration. Place this code inside a `use_effect` hook. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..c75dc88 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,64 @@ +# Architecture + +## Overview + +mcalc is a simple calculator application built with Dioxus 0.7. The architecture follows a clean separation between UI and business logic. + +## Components + +### `src/main.rs` +- Application entry point +- Loads global CSS stylesheet +- Renders the root `Calculator` component + +### `src/ui.rs` +- Contains the `Calculator` component +- Manages reactive state via Dioxus Signals +- Renders the display and keypad grid +- Delegates operations to `logic.rs` + +### `src/logic.rs` +- Pure business logic functions +- No Dioxus dependencies +- Handles digit input, operator selection, and calculation + +## State Management + +The calculator uses Dioxus `Signal` for reactive state: + +| Signal | Purpose | +|--------|---------| +| `first_num` | First operand | +| `second_num` | Second operand | +| `operator` | Selected operator (+, -, *, /) | + +State flows from `ui.rs` to `logic.rs` via function parameters. The UI component owns the state; logic functions mutate it through write handles. + +## Data Flow + +``` +User Input → ui.rs (onclick handlers) → logic.rs (pure functions) → State Update → Re-render +``` + +1. User clicks a button +2. Event handler calls logic function (e.g., `input_digit`) +3. Logic function mutates the signal +4. Dioxus detects the change and re-renders + +## Design System + +The UI follows the [BYTEMALTE design system](BYTEMALTE.md): + +- Dark theme with `#0F172A` background +- Primary accent: `#8888FF` +- Secondary accent: `#3DDC84` +- Error/clear: `#EF4444` + +## File Responsibilities + +| File | Responsibility | +|------|----------------| +| `main.rs` | App initialization, asset loading | +| `ui.rs` | Component rendering, state ownership | +| `logic.rs` | Calculator operations, validation | +| `assets/main.css` | Styling, theming, animations | diff --git a/BYTEMALTE.md b/BYTEMALTE.md new file mode 100644 index 0000000..dbac6b2 --- /dev/null +++ b/BYTEMALTE.md @@ -0,0 +1,57 @@ +# 🎨 BYTEMALTE Design System v1.1 + +Dieses Dokument definiert die visuelle Identität für alle Projekte von **ByteMalte**. Konsistenz vor Komplexität. + +--- + +## 🌈 Farbpalette (Hex) + +| Rolle | Hex-Code | Kontrast (auf `#0F172A`) | Einsatzbereich | +| :--- | :--- | :--- | :--- | +| **Primary** | `#8888FF` | 5.7:1 | Buttons, Links, Brand-Elemente | +| **Secondary** | `#3DDC84` | 7.0:1 | Success-Meldungen, Akzente, Tags | +| **Background** | `#0F172A` | — | Haupt-Hintergrund (Dark Mode bevorzugt) | +| **Surface** | `#1E293B` | — | Karten, Sektionen, Modals | +| **Text (High)** | `#F8FAFC` | 15.4:1 | Überschriften, Fließtext | +| **Text (Muted)** | `#B0BDD0` | 7.2:1 | Beschreibungen, Footer, sekundärer Text | +| **Accent/Error** | `#EF4444` | 4.5:1 | Fehler, Löschen-Buttons | + +> Alle Farben erfüllen WCAG AA (mindestens 4.5:1 für normalen Text, 3:1 für großen Text). + +--- + +## 📐 Layout & Abrundungen (Borders) + +Wir nutzen ein weiches, modernes Design mit großzügigen Radien. + +* **Buttons:** `8px` (Medium Round) +* **Cards / Container:** `16px` (Large Round) +* **Inputs / Formulare:** `8px` +* **Profilbilder:** `50%` (Circle) + +--- + +## 🖋️ Typografie + +* **Font Family:** `Inter, system-ui, sans-serif` (Clean & Programmierer-Vibe) +* **Headline Scale:** + * **h1:** `2.5rem` | Bold (700) + * **h2:** `1.8rem` | Semi-Bold (600) + * **h3:** `1.2rem` | Medium (500) +* **Body:** `1rem` | Regular (400) | Line-height: `1.6` + +--- + +## ⚡ UI-Komponenten Spezifikationen + +### Buttons +* **Default:** Primary Background, White Text, keine Border. +* **Hover:** Helligkeit +10%, leichter Box-Shadow (`0 4px 6px -1px rgb(0 0 0 / 0.1)`). +* **Active:** Skalierung auf `0.98` (Click-Effekt). + +### Karten (Cards) +* **Background:** `Surface` (`#1E293B`) +* **Border:** `1px solid #334155` +* **Padding:** `24px` + +--- diff --git a/README.md b/README.md index fbd3f81..49e0f16 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,56 @@ # mcalc - Simple Dioxus Calculator -A simple, modern, and beautiful calculator application built with Rust and Dioxus. - -## Description - -**mcalc** is a lightweight desktop calculator that demonstrates how to build a clean UI with state management in Dioxus. It features a modern dark mode design with glassmorphism effects and responsiveness. +A simple calculator application built with Rust and Dioxus, featuring the BYTEMALTE design system. ## Features -- **Basic Arithmetic**: Addition, Subtraction, Multiplication, Division. -- **Modern UI**: Clean typography, glassmorphism card, and neon accents. -- **Interactive**: Hover effects and smooth animations. -- **Responsive**: Adapts to window resizing. +- Basic Arithmetic: Addition, Subtraction, Multiplication, Division +- Clean UI with BYTEMALTE design system (dark theme) +- Desktop and Web support ## Tech Stack -- **Rust**: Core logic and safety. -- **Dioxus**: UI framework (React-like for Rust). -- **CSS3**: Custom styling with variables and flexbox/grid. +- **Rust**: Core logic and safety +- **Dioxus 0.7**: UI framework (React-like for Rust) +- **CSS3**: Custom styling with variables and flexbox/grid ## Getting Started ### Prerequisites -- [Rust](https://www.rust-lang.org/tools/install) installed. -- [Dioxus CLI](https://dioxuslabs.com/learn/0.6/getting_started) (optional but recommended for development). - -```bash -cargo install dioxus-cli -``` +- [Rust](https://www.rust-lang.org/tools/install) installed ### Running the App -1. **Clone the repository**: - ```bash - git clone - cd mcalc - ``` +```bash +# Desktop +cargo run -2. **Run with Cargo**: - ```bash - cargo run - ``` +# Web +cargo run --features web +``` - **Or with Dioxus CLI (for hot reloading):** - ```bash - dx serve - ``` +### Building + +```bash +# Debug build +cargo build + +# Release build +cargo build --release +``` ## Project Structure -- `src/main.rs`: Entry point. -- `src/ui.rs`: Main calculator UI and logic calls. -- `assets/main.css`: Styling and themes. -- `src/logic.rs`: Calculator logic. +``` +src/ + main.rs # App entry point + ui.rs # Calculator UI components + logic.rs # Calculator business logic +assets/ + main.css # Global styles (BYTEMALTE design system) +``` ## License -GPL-3 License \ No newline at end of file +GPL-3 License diff --git a/assets/main.css b/assets/main.css index 6f471fe..289a31c 100644 --- a/assets/main.css +++ b/assets/main.css @@ -1,187 +1,147 @@ /* Main Project Styles - Simple, clean, and modern calculator design. + BYTEMALTE Design System v1.1 */ -/* --- Variables --- */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); + :root { - /* Colors */ - --bg-main: #121212; - --bg-card: rgba(30, 30, 35, 0.8); - --bg-display: #1a1a1a; + --color-primary: #8888FF; + --color-secondary: #3DDC84; + --color-background: #0F172A; + --color-surface: #1E293B; + --color-text-high: #F8FAFC; + --color-text-muted: #B0BDD0; + --color-error: #EF4444; + --color-border: #334155; - --text-main: #ffffff; - --text-muted: #a0a0a0; - --text-black: #000000; + --radius-btn: 8px; + --radius-card: 16px; + --radius-input: 8px; - /* Button Colors */ - --btn-num-bg: #2d2d2d; - --btn-num-hover: #3d3d3d; - - --btn-op-bg: #ff9f0a; - --btn-op-hover: #ffb03a; - - --btn-clear-bg: #ff453a; - --btn-clear-hover: #ff6961; - - /* Layout & Effects */ - --radius-l: 24px; - --radius-m: 16px; - --shadow-heavy: 0 20px 50px rgba(0, 0, 0, 0.5); - --shadow-light: 0 4px 6px rgba(0, 0, 0, 0.2); + --font-family: 'Inter', system-ui, sans-serif; } -/* --- Global Setup --- */ -body { - background-color: var(--bg-main); - background-image: radial-gradient(circle at 50% 0%, #2a2a35 0%, #121212 100%); - color: var(--text-main); - font-family: -apple-system, sans-serif; +* { + box-sizing: border-box; margin: 0; + padding: 0; +} + +body { + background-color: var(--color-background); + color: var(--color-text-high); + font-family: var(--font-family); + font-weight: 400; + line-height: 1.6; min-height: 100vh; display: flex; justify-content: center; align-items: center; - overflow: hidden; } -/* --- App Container --- */ .app-container { width: 100%; height: 100vh; display: flex; justify-content: center; align-items: center; - animation: fade-in 0.8s ease-out; + padding: 16px; } -/* --- Calculator Wrapper (The Card) --- */ .calculator-wrap { - background-color: var(--bg-card); - backdrop-filter: blur(20px); - width: 360px; + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-card); padding: 24px; - border-radius: var(--radius-l); - box-shadow: var(--shadow-heavy); - border: 1px solid rgba(255, 255, 255, 0.1); + width: 100%; + max-width: 360px; display: flex; flex-direction: column; gap: 20px; } -/* --- Display Area --- */ .display-area { - background-color: var(--bg-display); - border-radius: var(--radius-m); + background-color: var(--color-background); + border-radius: var(--radius-input); padding: 24px; text-align: right; - margin-bottom: 10px; - min-height: 48px; + min-height: 80px; display: flex; align-items: center; justify-content: flex-end; overflow: hidden; - /* Hide if numbers get too long */ } .display-text { - font-size: 3rem; - font-weight: 300; + font-size: 2.5rem; + font-weight: 700; + color: var(--color-text-high); white-space: nowrap; - background: linear-gradient(180deg, #fff 0%, #e0e0e0 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; - /* Fallback */ } .operator-symbol { - color: var(--btn-op-bg); - /* Use operator color for visibility */ - font-weight: 500; + color: var(--color-primary); + font-weight: 600; margin: 0 4px; - /* Reset gradient text effect for symbol to make it solid color */ - -webkit-text-fill-color: var(--btn-op-bg); } -/* --- Keypad Grid --- */ .keypad-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; } -/* --- Buttons --- */ .calc-btn { border: none; outline: none; - font-size: 1.5rem; + font-size: 1.25rem; font-weight: 500; - padding: 20px 0; - border-radius: var(--radius-m); + padding: 18px 0; + border-radius: var(--radius-btn); cursor: pointer; - transition: transform 0.1s, filter 0.2s; - box-shadow: var(--shadow-light); - color: var(--text-main); - position: relative; - /* For overflow/effects */ - overflow: hidden; -} - -/* Button Interactions */ -.calc-btn:active { - transform: scale(0.95); + transition: filter 0.2s, box-shadow 0.2s, transform 0.1s; + color: var(--color-text-high); } .calc-btn:hover { filter: brightness(1.1); + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); +} + +.calc-btn:active { + transform: scale(0.98); } -/* Button Variants */ .btn-number { - background-color: var(--btn-num-bg); + background-color: var(--color-surface); + border: 1px solid var(--color-border); } .btn-operator { - background-color: var(--btn-op-bg); - color: var(--text-black); - font-weight: 600; + background-color: var(--color-primary); + color: var(--color-text-high); +} + +.btn-clear { + background-color: var(--color-error); + color: var(--color-text-high); } .btn-equals { grid-column: span 4; - /* Full width */ - background-color: var(--btn-op-bg); - color: var(--text-black); - margin-top: 12px; -} - -.btn-clear { - background-color: var(--btn-clear-bg); - color: white; + background-color: var(--color-secondary); + color: var(--color-background); + font-weight: 600; + margin-top: 8px; } .btn-zero { grid-column: span 2; - /* Double width */ text-align: left; - padding-left: 32px; + padding-left: 24px; } -/* --- Animations --- */ -@keyframes fade-in { - from { - opacity: 0; - transform: translateY(20px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -/* --- Responsiveness --- */ @media (max-height: 700px) { .calculator-wrap { padding: 16px; @@ -189,7 +149,11 @@ body { } .calc-btn { - padding: 16px 0; - font-size: 1.25rem; + padding: 14px 0; + font-size: 1.1rem; } -} \ No newline at end of file + + .display-text { + font-size: 2rem; + } +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..7a9cf26 --- /dev/null +++ b/justfile @@ -0,0 +1,41 @@ +# Justfile for mcalc + +# Default recipe +default: run + +# Development +run: + cargo run + +run-web: + cargo run --features web + +# Build +build: + cargo build + +build-release: + cargo build --release + +# Testing +test: + cargo test + +test-name name: + cargo test {{name}} + +# Linting & Formatting +fmt: + cargo fmt + +fmt-check: + cargo fmt -- --check + +lint: + cargo clippy + +lint-strict: + cargo clippy -- -D warnings + +# All checks +check: fmt-check lint test