diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..f6a1e11
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,304 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# All files
+[*]
+charset = utf-8
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+# Code files
+[*.{cs,csx,vb,vbx}]
+indent_size = 4
+
+# XML project files
+[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
+indent_size = 2
+
+# XML config files
+[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
+indent_size = 2
+
+# JSON files
+[*.json]
+indent_size = 2
+
+# YAML files
+[*.{yml,yaml}]
+indent_size = 2
+
+# Markdown files
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false
+
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+tab_width = 4
+
+# New line preferences
+end_of_line = lf
+insert_final_newline = true
+
+#### .NET Coding Conventions ####
+
+# Organize usings
+dotnet_separate_import_directive_groups = false
+dotnet_sort_system_directives_first = true
+file_header_template = unset
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false:warning
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_property = false:warning
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
+
+# Expression-level preferences
+dotnet_style_coalesce_expression = true:warning
+dotnet_style_collection_initializer = true:warning
+dotnet_style_explicit_tuple_names = true:warning
+dotnet_style_namespace_match_folder = true:suggestion
+dotnet_style_null_propagation = true:warning
+dotnet_style_object_initializer = true:warning
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_compound_assignment = true:warning
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
+dotnet_style_prefer_inferred_tuple_names = true:warning
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
+dotnet_style_prefer_simplified_boolean_expressions = true:warning
+dotnet_style_prefer_simplified_interpolation = true:warning
+
+# Field preferences
+dotnet_style_readonly_field = true:warning
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all:warning
+
+# Suppression preferences
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+# New line preferences
+dotnet_style_allow_multiple_blank_lines_experimental = false:warning
+dotnet_style_allow_statement_immediately_after_block_experimental = false:warning
+
+#### C# Coding Conventions ####
+
+# var preferences
+csharp_style_var_elsewhere = false:suggestion
+csharp_style_var_for_built_in_types = false:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true:suggestion
+csharp_style_expression_bodied_constructors = false:suggestion
+csharp_style_expression_bodied_indexers = true:suggestion
+csharp_style_expression_bodied_lambdas = true:suggestion
+csharp_style_expression_bodied_local_functions = false:suggestion
+csharp_style_expression_bodied_methods = false:suggestion
+csharp_style_expression_bodied_operators = false:suggestion
+csharp_style_expression_bodied_properties = true:suggestion
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true:warning
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_prefer_extended_property_pattern = true:suggestion
+csharp_style_prefer_not_pattern = true:warning
+csharp_style_prefer_pattern_matching = true:suggestion
+csharp_style_prefer_switch_expression = true:suggestion
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true:warning
+csharp_style_prefer_parameter_null_checking = true:suggestion
+
+# Modifier preferences
+csharp_prefer_static_local_function = true:warning
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning
+
+# Code-block preferences
+csharp_prefer_braces = true:warning
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_style_namespace_declarations = file_scoped:warning
+csharp_style_prefer_method_group_conversion = true:suggestion
+csharp_style_prefer_top_level_statements = false:suggestion
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true:warning
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
+csharp_style_inlined_variable_declaration = true:warning
+csharp_style_prefer_index_operator = true:warning
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_null_check_over_type_check = true:warning
+csharp_style_prefer_range_operator = true:warning
+csharp_style_prefer_tuple_swap = true:suggestion
+csharp_style_throw_expression = true:warning
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace:warning
+
+# New line preferences
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false:warning
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:warning
+csharp_style_allow_embedded_statements_on_same_line_experimental = false:warning
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = false
+csharp_indent_labels = no_change
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = false
+
+#### Naming Conventions ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = warning
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.private_or_internal_field_should_be_begins_with_underscore.severity = warning
+dotnet_naming_rule.private_or_internal_field_should_be_begins_with_underscore.symbols = private_or_internal_field
+dotnet_naming_rule.private_or_internal_field_should_be_begins_with_underscore.style = begins_with_underscore
+
+dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = warning
+dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
+dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
+dotnet_naming_symbols.private_or_internal_field.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+dotnet_naming_symbols.constant_fields.applicable_kinds = field
+dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
+dotnet_naming_symbols.constant_fields.required_modifiers = const
+
+# Naming styles
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+dotnet_naming_style.begins_with_underscore.required_prefix = _
+dotnet_naming_style.begins_with_underscore.required_suffix =
+dotnet_naming_style.begins_with_underscore.word_separator =
+dotnet_naming_style.begins_with_underscore.capitalization = camel_case
+
+#### Code Analysis Rules ####
+
+# CA1031: Do not catch general exception types
+dotnet_diagnostic.CA1031.severity = suggestion
+
+# CA1062: Validate arguments of public methods
+dotnet_diagnostic.CA1062.severity = suggestion
+
+# CA1303: Do not pass literals as localized parameters
+dotnet_diagnostic.CA1303.severity = none
+
+# CA1720: Identifiers should not contain type names
+dotnet_diagnostic.CA1720.severity = suggestion
+
+# CA2007: Do not directly await a Task
+dotnet_diagnostic.CA2007.severity = none
+
+# IDE0005: Using directive is unnecessary
+dotnet_diagnostic.IDE0005.severity = warning
+
+# IDE0058: Expression value is never used
+dotnet_diagnostic.IDE0058.severity = none
+
+# IDE0063: Use simple 'using' statement
+dotnet_diagnostic.IDE0063.severity = suggestion
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..92b6ce5
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,207 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+Hiha-Arvio (Finnish: "Sleeve Estimate") is a .NET 8 MAUI cross-platform application that generates semi-random time estimates based on physical shake input (accelerometer on mobile, mouse movement on desktop). This is a humor app for "pulling an estimate from your sleeve."
+
+**Platforms (in priority order):** iOS (primary) → Web (Blazor) → macOS
+
+## Critical Requirements
+
+### Specification Compliance
+
+- **ALWAYS read `spec.md` before implementing features** - contains RFC 2119 formal requirements (MUST, REQUIRED, SHALL)
+- **Design reference:** `docs/plans/2025-11-18-hiha-arvio-design.md` contains validated architecture decisions
+- Nullable reference types MUST be enabled
+- All compiler warnings MUST be treated as errors
+- Minimum test coverage: 95% (enforced in CI/CD)
+
+### Architecture Constraints
+
+**MVVM Pattern with Strict Separation:**
+- Models: Plain data objects only, no business logic
+- ViewModels: All presentation logic, must be 100% testable without UI dependencies
+- Views: Thin layer, data binding only, minimal presentation code
+- Services: All business logic and infrastructure concerns
+
+**Dependency Injection:**
+- All services MUST be injected via constructor (no service locator, no `new` keyword in ViewModels)
+- Register services in `MauiProgram.cs`
+- Platform-specific implementations use conditional compilation (`#if IOS`, `#elif WINDOWS || MACCATALYST`)
+
+### Testing Requirements
+
+- Test coverage MUST be ≥95% (measured with Coverlet, enforced in CI/CD)
+- Testing stack: xUnit + NSubstitute (mocking) + FluentAssertions (assertions)
+- All tests MUST use deterministic randomness (seeded RNG)
+- Mock all external dependencies (sensors, database, file system)
+- Use test data builders for complex objects
+
+## Technology Stack
+
+- **Framework:** .NET 8 MAUI (LTS)
+- **Language:** C# 12 with nullable reference types
+- **Database:** SQLite via `sqlite-net-pcl`
+- **MVVM:** CommunityToolkit.Mvvm (source generators)
+- **Testing:** xUnit, NSubstitute, FluentAssertions, Coverlet
+
+## Core Architecture
+
+### Service Layer (All Injectable)
+
+**IAccelerometerService**
+- Platform abstraction: accelerometer (iOS) or mouse movement (desktop/web)
+- Emits observable stream of sensor data
+
+**IShakeDetectionService**
+- Processes accelerometer stream
+- Detects shake start/stop, calculates normalized intensity [0.0-1.0]
+- Tracks shake duration for easter egg trigger (>15 seconds)
+
+**IEstimateService**
+- Generates estimates based on: intensity, duration, mode
+- Implements intensity → range mapping:
+ - Intensity <0.3: narrow range (first 20% of pool)
+ - Intensity 0.3-0.7: medium range (first 50% of pool)
+ - Intensity >0.7: full range (entire pool)
+- Easter egg: duration >15s forces Humorous mode
+- MUST use cryptographically secure RNG (`System.Security.Cryptography.RandomNumberGenerator`)
+
+**IStorageService**
+- Settings: Preferences API
+- History: SQLite with auto-pruning (max 10 estimates)
+- All operations MUST be async
+
+### Key Models
+
+```csharp
+EstimateResult: Id, Timestamp, EstimateText, Mode, ShakeIntensity, ShakeDuration
+EstimateMode enum: Work, Generic, Humorous
+ShakeData: Intensity, Duration, IsShaking
+AppSettings: SelectedMode, MaxHistorySize
+```
+
+### Estimate Pools (from spec.md §3.2.2)
+
+**Work Mode:**
+- Gentle: "2 hours", "4 hours", "1 day", "2 days", "3 days", "5 days", "1 week"
+- Hard: adds "15 minutes", "30 minutes", "2 weeks", "1 month", "3 months", "6 months", "1 year"
+
+**Generic Mode:**
+- Gentle: "1 minute" → "3 hours"
+- Hard: "30 seconds" → "1 month"
+
+**Humorous Mode (easter egg):**
+- "tomorrow", "eventually", "next quarter", "when hell freezes over", "3 lifetimes", "Tuesday", "never", "your retirement"
+
+## Platform-Specific Implementation
+
+### iOS (Primary Platform)
+- Accelerometer via `Microsoft.Maui.Devices.Sensors.Accelerometer`
+- Shake detection: `magnitude = sqrt(x² + y² + z²)`, threshold 2.5g
+- Must request motion permissions in Info.plist
+- Haptic feedback on shake detection (recommended)
+- Target iOS 15+
+
+### Web (Blazor WebAssembly)
+- Mouse movement simulation: track delta over 200ms window, calculate velocity
+- PWA manifest for home screen install
+- Support Device Orientation API for mobile browsers (optional)
+
+### macOS
+- Mouse movement tracking (similar to web)
+- Keyboard shortcut: Cmd+Shift+S for manual shake trigger
+- Native menu bar integration
+
+## Project Structure (When Implemented)
+
+```
+HihaArvio.sln
+├── src/HihaArvio/ # Main MAUI project
+│ ├── Models/
+│ ├── ViewModels/
+│ ├── Views/
+│ ├── Services/
+│ │ ├── Interfaces/
+│ │ └── Platform/ # Platform-specific implementations
+│ └── MauiProgram.cs
+├── tests/
+│ ├── HihaArvio.Tests/ # Unit tests
+│ ├── HihaArvio.IntegrationTests/ # Integration tests
+│ └── HihaArvio.UITests/ # UI automation (future)
+├── docs/plans/ # Design documents
+└── spec.md # Formal specification
+```
+
+## Development Commands
+
+### Build Commands
+- **Build all platforms:** `dotnet build HihaArvio.sln`
+- **Build specific framework:** `dotnet build HihaArvio.sln -f net8.0`
+- **Build iOS:** `dotnet build HihaArvio.sln -f net8.0-ios`
+- **Build macOS:** `dotnet build HihaArvio.sln -f net8.0-maccatalyst`
+
+### Test Commands
+- **Run all tests:** `dotnet test tests/HihaArvio.Tests/HihaArvio.Tests.csproj`
+- **Run specific test class:** `dotnet test tests/HihaArvio.Tests/HihaArvio.Tests.csproj --filter "FullyQualifiedName~EstimateModeTests"`
+- **Run single test:** `dotnet test --filter "FullyQualifiedName~TestClassName.TestMethodName"`
+
+### Code Coverage
+- **Generate coverage:** `dotnet test tests/HihaArvio.Tests/HihaArvio.Tests.csproj --collect:"XPlat Code Coverage"`
+- **Coverage files:** Located in `tests/HihaArvio.Tests/TestResults/{guid}/coverage.cobertura.xml`
+
+### Run Commands
+- **iOS Simulator:** `dotnet build src/HihaArvio/HihaArvio.csproj -t:Run -f net8.0-ios`
+- **macOS:** `dotnet build src/HihaArvio/HihaArvio.csproj -t:Run -f net8.0-maccatalyst`
+
+### Notes
+- All commands should be run from the repository root directory
+- Xcode must be installed and configured (`xcode-select -p` should point to Xcode.app)
+- MAUI workload must be installed (`dotnet workload list` should show `maui`)
+
+## Critical Implementation Notes
+
+### Easter Egg Behavior
+- Hidden feature: NO UI indication
+- Trigger: shake duration >15 seconds
+- Effect: temporarily force EstimateMode.Humorous
+- Do NOT expose in Settings or UI
+
+### Shake Detection Algorithm
+1. Monitor sensor stream (accelerometer or mouse)
+2. Calculate magnitude/velocity
+3. Detect start: exceeds threshold
+4. Track peak intensity during session
+5. Detect end: below threshold for 500ms continuous
+6. Normalize to [0.0, 1.0]
+
+### Performance Requirements
+- Shake response: <100ms latency
+- Estimate display: <200ms after shake stop
+- History load: <500ms
+- All database operations: async, non-blocking
+
+### Security
+- Use `System.Security.Cryptography.RandomNumberGenerator` for estimate selection
+- No external data transmission
+- All data stored locally only
+- Request minimum required permissions
+
+## Code Quality Enforcement
+
+- Enable nullable reference types across all projects
+- Treat warnings as errors
+- Follow EditorConfig rules (when defined)
+- Code analysis: StyleCop + built-in analyzers enabled
+- CI/CD must enforce 95% coverage threshold and fail builds below this
+
+## Important Implementation Order
+
+Per design document, phased development:
+1. **Phase 1:** iOS app (primary platform, accelerometer-based)
+2. **Phase 2:** Web app (Blazor, mouse simulation)
+3. **Phase 3:** macOS app (native integration)
+
+Focus on Phase 1 first to validate core shake detection and estimate generation logic.
diff --git a/HihaArvio.sln b/HihaArvio.sln
new file mode 100644
index 0000000..aebe75a
--- /dev/null
+++ b/HihaArvio.sln
@@ -0,0 +1,36 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7228B5C2-A84A-4648-BB86-FF76E090592E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HihaArvio", "src\HihaArvio\HihaArvio.csproj", "{E8438360-C957-4B6B-A7EA-9F910063D141}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{1BA313A1-7BF6-4504-B397-6EFA6B72D5AA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HihaArvio.Tests", "tests\HihaArvio.Tests\HihaArvio.Tests.csproj", "{FCA4D2E3-827F-4557-BBD4-A2F9F81CB158}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {E8438360-C957-4B6B-A7EA-9F910063D141}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E8438360-C957-4B6B-A7EA-9F910063D141}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E8438360-C957-4B6B-A7EA-9F910063D141}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E8438360-C957-4B6B-A7EA-9F910063D141}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FCA4D2E3-827F-4557-BBD4-A2F9F81CB158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FCA4D2E3-827F-4557-BBD4-A2F9F81CB158}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FCA4D2E3-827F-4557-BBD4-A2F9F81CB158}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FCA4D2E3-827F-4557-BBD4-A2F9F81CB158}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {E8438360-C957-4B6B-A7EA-9F910063D141} = {7228B5C2-A84A-4648-BB86-FF76E090592E}
+ {FCA4D2E3-827F-4557-BBD4-A2F9F81CB158} = {1BA313A1-7BF6-4504-B397-6EFA6B72D5AA}
+ EndGlobalSection
+EndGlobal
diff --git a/src/HihaArvio/App.xaml b/src/HihaArvio/App.xaml
new file mode 100644
index 0000000..22179c7
--- /dev/null
+++ b/src/HihaArvio/App.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/HihaArvio/App.xaml.cs b/src/HihaArvio/App.xaml.cs
new file mode 100644
index 0000000..d10ce75
--- /dev/null
+++ b/src/HihaArvio/App.xaml.cs
@@ -0,0 +1,11 @@
+namespace HihaArvio;
+
+public partial class App : Application
+{
+ public App()
+ {
+ InitializeComponent();
+
+ MainPage = new AppShell();
+ }
+}
diff --git a/src/HihaArvio/AppShell.xaml b/src/HihaArvio/AppShell.xaml
new file mode 100644
index 0000000..574c217
--- /dev/null
+++ b/src/HihaArvio/AppShell.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/src/HihaArvio/AppShell.xaml.cs b/src/HihaArvio/AppShell.xaml.cs
new file mode 100644
index 0000000..875c7a8
--- /dev/null
+++ b/src/HihaArvio/AppShell.xaml.cs
@@ -0,0 +1,9 @@
+namespace HihaArvio;
+
+public partial class AppShell : Shell
+{
+ public AppShell()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/HihaArvio/HihaArvio.csproj b/src/HihaArvio/HihaArvio.csproj
new file mode 100644
index 0000000..c1dfbfb
--- /dev/null
+++ b/src/HihaArvio/HihaArvio.csproj
@@ -0,0 +1,70 @@
+
+
+
+
+
+ net8.0;net8.0-ios;net8.0-maccatalyst
+
+
+
+
+ Exe
+ Library
+ HihaArvio
+ true
+ true
+ enable
+ enable
+
+
+ true
+ 12
+
+
+ HihaArvio
+
+
+ com.companyname.hihaarvio
+
+
+ 1.0
+ 1
+
+
+ 15.0
+ 15.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/HihaArvio/MainPage.xaml b/src/HihaArvio/MainPage.xaml
new file mode 100644
index 0000000..06ce07c
--- /dev/null
+++ b/src/HihaArvio/MainPage.xaml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/HihaArvio/MainPage.xaml.cs b/src/HihaArvio/MainPage.xaml.cs
new file mode 100644
index 0000000..518469e
--- /dev/null
+++ b/src/HihaArvio/MainPage.xaml.cs
@@ -0,0 +1,24 @@
+namespace HihaArvio;
+
+public partial class MainPage : ContentPage
+{
+ int count = 0;
+
+ public MainPage()
+ {
+ InitializeComponent();
+ }
+
+ private void OnCounterClicked(object sender, EventArgs e)
+ {
+ count++;
+
+ if (count == 1)
+ CounterBtn.Text = $"Clicked {count} time";
+ else
+ CounterBtn.Text = $"Clicked {count} times";
+
+ SemanticScreenReader.Announce(CounterBtn.Text);
+ }
+}
+
diff --git a/src/HihaArvio/MauiProgram.cs b/src/HihaArvio/MauiProgram.cs
new file mode 100644
index 0000000..a836421
--- /dev/null
+++ b/src/HihaArvio/MauiProgram.cs
@@ -0,0 +1,24 @@
+using Microsoft.Extensions.Logging;
+
+namespace HihaArvio;
+
+public static class MauiProgram
+{
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder
+ .UseMauiApp()
+ .ConfigureFonts(fonts =>
+ {
+ fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
+ fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
+ });
+
+#if DEBUG
+ builder.Logging.AddDebug();
+#endif
+
+ return builder.Build();
+ }
+}
diff --git a/src/HihaArvio/Models/AppSettings.cs b/src/HihaArvio/Models/AppSettings.cs
new file mode 100644
index 0000000..924a861
--- /dev/null
+++ b/src/HihaArvio/Models/AppSettings.cs
@@ -0,0 +1,37 @@
+namespace HihaArvio.Models;
+
+///
+/// Represents application settings and user preferences.
+///
+public class AppSettings
+{
+ private int _maxHistorySize = 10;
+
+ ///
+ /// Gets or sets the selected estimate mode.
+ /// Default is .
+ ///
+ public EstimateMode SelectedMode { get; set; } = EstimateMode.Work;
+
+ ///
+ /// Gets or sets the maximum number of estimate results to keep in history.
+ /// Default is 10.
+ ///
+ /// Thrown when value is less than or equal to 0.
+ public int MaxHistorySize
+ {
+ get => _maxHistorySize;
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(value),
+ value,
+ "MaxHistorySize must be greater than 0.");
+ }
+
+ _maxHistorySize = value;
+ }
+ }
+}
diff --git a/src/HihaArvio/Models/EstimateMode.cs b/src/HihaArvio/Models/EstimateMode.cs
new file mode 100644
index 0000000..19b9779
--- /dev/null
+++ b/src/HihaArvio/Models/EstimateMode.cs
@@ -0,0 +1,22 @@
+namespace HihaArvio.Models;
+
+///
+/// Represents the mode of time estimate generation.
+///
+public enum EstimateMode
+{
+ ///
+ /// Work/project-related time estimates (e.g., "2 weeks", "3 sprints").
+ ///
+ Work = 0,
+
+ ///
+ /// Generic duration estimates (e.g., "5 minutes", "2 hours").
+ ///
+ Generic = 1,
+
+ ///
+ /// Humorous/exaggerated estimates (easter egg mode, triggered by >15s shake).
+ ///
+ Humorous = 2
+}
diff --git a/src/HihaArvio/Models/EstimateResult.cs b/src/HihaArvio/Models/EstimateResult.cs
new file mode 100644
index 0000000..abe8641
--- /dev/null
+++ b/src/HihaArvio/Models/EstimateResult.cs
@@ -0,0 +1,100 @@
+namespace HihaArvio.Models;
+
+///
+/// Represents a time estimate result generated from a shake gesture.
+///
+public class EstimateResult
+{
+ private string _estimateText = string.Empty;
+ private double _shakeIntensity;
+
+ ///
+ /// Gets or sets the unique identifier for this estimate.
+ ///
+ public Guid Id { get; set; }
+
+ ///
+ /// Gets or sets the timestamp when this estimate was generated.
+ ///
+ public DateTimeOffset Timestamp { get; set; }
+
+ ///
+ /// Gets or sets the estimate text (e.g., "2 weeks", "eventually").
+ ///
+ /// Thrown when value is null.
+ /// Thrown when value is empty or whitespace.
+ public string EstimateText
+ {
+ get => _estimateText;
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value), "EstimateText cannot be null.");
+ }
+
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ throw new ArgumentException("EstimateText cannot be empty or whitespace.", nameof(value));
+ }
+
+ _estimateText = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the estimation mode (Work, Generic, or Humorous).
+ ///
+ public EstimateMode Mode { get; set; }
+
+ ///
+ /// Gets or sets the normalized shake intensity (0.0 to 1.0).
+ ///
+ /// Thrown when value is outside [0.0, 1.0] range.
+ public double ShakeIntensity
+ {
+ get => _shakeIntensity;
+ set
+ {
+ if (value < 0.0 || value > 1.0)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(value),
+ value,
+ "ShakeIntensity must be between 0.0 and 1.0.");
+ }
+
+ _shakeIntensity = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the duration of the shake gesture.
+ ///
+ public TimeSpan ShakeDuration { get; set; }
+
+ ///
+ /// Creates a new EstimateResult with the specified values and auto-generated ID and timestamp.
+ ///
+ /// The estimate text.
+ /// The estimation mode.
+ /// The shake intensity (0.0 to 1.0).
+ /// The shake duration.
+ /// A new EstimateResult instance.
+ public static EstimateResult Create(
+ string estimateText,
+ EstimateMode mode,
+ double shakeIntensity,
+ TimeSpan shakeDuration)
+ {
+ return new EstimateResult
+ {
+ Id = Guid.NewGuid(),
+ Timestamp = DateTimeOffset.UtcNow,
+ EstimateText = estimateText,
+ Mode = mode,
+ ShakeIntensity = shakeIntensity,
+ ShakeDuration = shakeDuration
+ };
+ }
+}
diff --git a/src/HihaArvio/Models/ShakeData.cs b/src/HihaArvio/Models/ShakeData.cs
new file mode 100644
index 0000000..9b6b7da
--- /dev/null
+++ b/src/HihaArvio/Models/ShakeData.cs
@@ -0,0 +1,40 @@
+namespace HihaArvio.Models;
+
+///
+/// Represents current shake gesture data.
+///
+public class ShakeData
+{
+ private double _intensity;
+
+ ///
+ /// Gets or sets the normalized shake intensity (0.0 to 1.0).
+ ///
+ /// Thrown when value is outside [0.0, 1.0] range.
+ public double Intensity
+ {
+ get => _intensity;
+ set
+ {
+ if (value < 0.0 || value > 1.0)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(value),
+ value,
+ "Intensity must be between 0.0 and 1.0.");
+ }
+
+ _intensity = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the duration of the current shake gesture.
+ ///
+ public TimeSpan Duration { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether a shake is currently in progress.
+ ///
+ public bool IsShaking { get; set; }
+}
diff --git a/src/HihaArvio/Platforms/Android/AndroidManifest.xml b/src/HihaArvio/Platforms/Android/AndroidManifest.xml
new file mode 100644
index 0000000..bdec9b5
--- /dev/null
+++ b/src/HihaArvio/Platforms/Android/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/HihaArvio/Platforms/Android/MainActivity.cs b/src/HihaArvio/Platforms/Android/MainActivity.cs
new file mode 100644
index 0000000..b04273e
--- /dev/null
+++ b/src/HihaArvio/Platforms/Android/MainActivity.cs
@@ -0,0 +1,10 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+
+namespace HihaArvio;
+
+[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
+public class MainActivity : MauiAppCompatActivity
+{
+}
diff --git a/src/HihaArvio/Platforms/Android/MainApplication.cs b/src/HihaArvio/Platforms/Android/MainApplication.cs
new file mode 100644
index 0000000..3f94033
--- /dev/null
+++ b/src/HihaArvio/Platforms/Android/MainApplication.cs
@@ -0,0 +1,15 @@
+using Android.App;
+using Android.Runtime;
+
+namespace HihaArvio;
+
+[Application]
+public class MainApplication : MauiApplication
+{
+ public MainApplication(IntPtr handle, JniHandleOwnership ownership)
+ : base(handle, ownership)
+ {
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/src/HihaArvio/Platforms/Android/Resources/values/colors.xml b/src/HihaArvio/Platforms/Android/Resources/values/colors.xml
new file mode 100644
index 0000000..5cd1604
--- /dev/null
+++ b/src/HihaArvio/Platforms/Android/Resources/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #512BD4
+ #2B0B98
+ #2B0B98
+
\ No newline at end of file
diff --git a/src/HihaArvio/Platforms/MacCatalyst/AppDelegate.cs b/src/HihaArvio/Platforms/MacCatalyst/AppDelegate.cs
new file mode 100644
index 0000000..3011238
--- /dev/null
+++ b/src/HihaArvio/Platforms/MacCatalyst/AppDelegate.cs
@@ -0,0 +1,9 @@
+using Foundation;
+
+namespace HihaArvio;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/src/HihaArvio/Platforms/MacCatalyst/Entitlements.plist b/src/HihaArvio/Platforms/MacCatalyst/Entitlements.plist
new file mode 100644
index 0000000..8e87c0c
--- /dev/null
+++ b/src/HihaArvio/Platforms/MacCatalyst/Entitlements.plist
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+ com.apple.security.network.client
+
+
+
+
diff --git a/src/HihaArvio/Platforms/MacCatalyst/Info.plist b/src/HihaArvio/Platforms/MacCatalyst/Info.plist
new file mode 100644
index 0000000..f24aacc
--- /dev/null
+++ b/src/HihaArvio/Platforms/MacCatalyst/Info.plist
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ UIDeviceFamily
+
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+
+
diff --git a/src/HihaArvio/Platforms/MacCatalyst/Program.cs b/src/HihaArvio/Platforms/MacCatalyst/Program.cs
new file mode 100644
index 0000000..9fdd3f0
--- /dev/null
+++ b/src/HihaArvio/Platforms/MacCatalyst/Program.cs
@@ -0,0 +1,15 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace HihaArvio;
+
+public class Program
+{
+ // This is the main entry point of the application.
+ static void Main(string[] args)
+ {
+ // if you want to use a different Application Delegate class from "AppDelegate"
+ // you can specify it here.
+ UIApplication.Main(args, null, typeof(AppDelegate));
+ }
+}
diff --git a/src/HihaArvio/Platforms/Tizen/Main.cs b/src/HihaArvio/Platforms/Tizen/Main.cs
new file mode 100644
index 0000000..9c20490
--- /dev/null
+++ b/src/HihaArvio/Platforms/Tizen/Main.cs
@@ -0,0 +1,16 @@
+using System;
+using Microsoft.Maui;
+using Microsoft.Maui.Hosting;
+
+namespace HihaArvio;
+
+class Program : MauiApplication
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+
+ static void Main(string[] args)
+ {
+ var app = new Program();
+ app.Run(args);
+ }
+}
diff --git a/src/HihaArvio/Platforms/Tizen/tizen-manifest.xml b/src/HihaArvio/Platforms/Tizen/tizen-manifest.xml
new file mode 100644
index 0000000..5102344
--- /dev/null
+++ b/src/HihaArvio/Platforms/Tizen/tizen-manifest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ maui-appicon-placeholder
+
+
+
+
+ http://tizen.org/privilege/internet
+
+
+
+
\ No newline at end of file
diff --git a/src/HihaArvio/Platforms/Windows/App.xaml b/src/HihaArvio/Platforms/Windows/App.xaml
new file mode 100644
index 0000000..40c29eb
--- /dev/null
+++ b/src/HihaArvio/Platforms/Windows/App.xaml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/src/HihaArvio/Platforms/Windows/App.xaml.cs b/src/HihaArvio/Platforms/Windows/App.xaml.cs
new file mode 100644
index 0000000..587aa7d
--- /dev/null
+++ b/src/HihaArvio/Platforms/Windows/App.xaml.cs
@@ -0,0 +1,24 @@
+using Microsoft.UI.Xaml;
+
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
+
+namespace HihaArvio.WinUI;
+
+///
+/// Provides application-specific behavior to supplement the default Application class.
+///
+public partial class App : MauiWinUIApplication
+{
+ ///
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ public App()
+ {
+ this.InitializeComponent();
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/src/HihaArvio/Platforms/Windows/Package.appxmanifest b/src/HihaArvio/Platforms/Windows/Package.appxmanifest
new file mode 100644
index 0000000..3bc1642
--- /dev/null
+++ b/src/HihaArvio/Platforms/Windows/Package.appxmanifest
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+ $placeholder$
+ User Name
+ $placeholder$.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/HihaArvio/Platforms/Windows/app.manifest b/src/HihaArvio/Platforms/Windows/app.manifest
new file mode 100644
index 0000000..00464f9
--- /dev/null
+++ b/src/HihaArvio/Platforms/Windows/app.manifest
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+
+
diff --git a/src/HihaArvio/Platforms/iOS/AppDelegate.cs b/src/HihaArvio/Platforms/iOS/AppDelegate.cs
new file mode 100644
index 0000000..3011238
--- /dev/null
+++ b/src/HihaArvio/Platforms/iOS/AppDelegate.cs
@@ -0,0 +1,9 @@
+using Foundation;
+
+namespace HihaArvio;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/src/HihaArvio/Platforms/iOS/Info.plist b/src/HihaArvio/Platforms/iOS/Info.plist
new file mode 100644
index 0000000..358337b
--- /dev/null
+++ b/src/HihaArvio/Platforms/iOS/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ LSRequiresIPhoneOS
+
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+
+
diff --git a/src/HihaArvio/Platforms/iOS/Program.cs b/src/HihaArvio/Platforms/iOS/Program.cs
new file mode 100644
index 0000000..9fdd3f0
--- /dev/null
+++ b/src/HihaArvio/Platforms/iOS/Program.cs
@@ -0,0 +1,15 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace HihaArvio;
+
+public class Program
+{
+ // This is the main entry point of the application.
+ static void Main(string[] args)
+ {
+ // if you want to use a different Application Delegate class from "AppDelegate"
+ // you can specify it here.
+ UIApplication.Main(args, null, typeof(AppDelegate));
+ }
+}
diff --git a/src/HihaArvio/Platforms/iOS/Resources/PrivacyInfo.xcprivacy b/src/HihaArvio/Platforms/iOS/Resources/PrivacyInfo.xcprivacy
new file mode 100644
index 0000000..1ea3a5d
--- /dev/null
+++ b/src/HihaArvio/Platforms/iOS/Resources/PrivacyInfo.xcprivacy
@@ -0,0 +1,51 @@
+
+
+
+
+
+ NSPrivacyAccessedAPITypes
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryFileTimestamp
+ NSPrivacyAccessedAPITypeReasons
+
+ C617.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategorySystemBootTime
+ NSPrivacyAccessedAPITypeReasons
+
+ 35F9.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryDiskSpace
+ NSPrivacyAccessedAPITypeReasons
+
+ E174.1
+
+
+
+
+
+
diff --git a/src/HihaArvio/Properties/launchSettings.json b/src/HihaArvio/Properties/launchSettings.json
new file mode 100644
index 0000000..c16206a
--- /dev/null
+++ b/src/HihaArvio/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Windows Machine": {
+ "commandName": "MsixPackage",
+ "nativeDebugging": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/HihaArvio/Resources/AppIcon/appicon.svg b/src/HihaArvio/Resources/AppIcon/appicon.svg
new file mode 100644
index 0000000..5f04fcf
--- /dev/null
+++ b/src/HihaArvio/Resources/AppIcon/appicon.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/src/HihaArvio/Resources/AppIcon/appiconfg.svg b/src/HihaArvio/Resources/AppIcon/appiconfg.svg
new file mode 100644
index 0000000..62d66d7
--- /dev/null
+++ b/src/HihaArvio/Resources/AppIcon/appiconfg.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/src/HihaArvio/Resources/Fonts/OpenSans-Regular.ttf b/src/HihaArvio/Resources/Fonts/OpenSans-Regular.ttf
new file mode 100644
index 0000000..ee3f28f
Binary files /dev/null and b/src/HihaArvio/Resources/Fonts/OpenSans-Regular.ttf differ
diff --git a/src/HihaArvio/Resources/Fonts/OpenSans-Semibold.ttf b/src/HihaArvio/Resources/Fonts/OpenSans-Semibold.ttf
new file mode 100644
index 0000000..bc81019
Binary files /dev/null and b/src/HihaArvio/Resources/Fonts/OpenSans-Semibold.ttf differ
diff --git a/src/HihaArvio/Resources/Images/dotnet_bot.png b/src/HihaArvio/Resources/Images/dotnet_bot.png
new file mode 100644
index 0000000..f93ce02
Binary files /dev/null and b/src/HihaArvio/Resources/Images/dotnet_bot.png differ
diff --git a/src/HihaArvio/Resources/Raw/AboutAssets.txt b/src/HihaArvio/Resources/Raw/AboutAssets.txt
new file mode 100644
index 0000000..f22d3bf
--- /dev/null
+++ b/src/HihaArvio/Resources/Raw/AboutAssets.txt
@@ -0,0 +1,15 @@
+Any raw assets you want to be deployed with your application can be placed in
+this directory (and child directories). Deployment of the asset to your application
+is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
+
+
+
+These files will be deployed with your package and will be accessible using Essentials:
+
+ async Task LoadMauiAsset()
+ {
+ using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
+ using var reader = new StreamReader(stream);
+
+ var contents = reader.ReadToEnd();
+ }
diff --git a/src/HihaArvio/Resources/Splash/splash.svg b/src/HihaArvio/Resources/Splash/splash.svg
new file mode 100644
index 0000000..62d66d7
--- /dev/null
+++ b/src/HihaArvio/Resources/Splash/splash.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/src/HihaArvio/Resources/Styles/Colors.xaml b/src/HihaArvio/Resources/Styles/Colors.xaml
new file mode 100644
index 0000000..22f0a67
--- /dev/null
+++ b/src/HihaArvio/Resources/Styles/Colors.xaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+ #512BD4
+ #ac99ea
+ #242424
+ #DFD8F7
+ #9880e5
+ #2B0B98
+
+ White
+ Black
+ #D600AA
+ #190649
+ #1f1f1f
+
+ #E1E1E1
+ #C8C8C8
+ #ACACAC
+ #919191
+ #6E6E6E
+ #404040
+ #212121
+ #141414
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/HihaArvio/Resources/Styles/Styles.xaml b/src/HihaArvio/Resources/Styles/Styles.xaml
new file mode 100644
index 0000000..d600a7f
--- /dev/null
+++ b/src/HihaArvio/Resources/Styles/Styles.xaml
@@ -0,0 +1,427 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/HihaArvio.Tests/HihaArvio.Tests.csproj b/tests/HihaArvio.Tests/HihaArvio.Tests.csproj
new file mode 100644
index 0000000..322654d
--- /dev/null
+++ b/tests/HihaArvio.Tests/HihaArvio.Tests.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+ true
+ 12
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/HihaArvio.Tests/Models/AppSettingsTests.cs b/tests/HihaArvio.Tests/Models/AppSettingsTests.cs
new file mode 100644
index 0000000..989b2b4
--- /dev/null
+++ b/tests/HihaArvio.Tests/Models/AppSettingsTests.cs
@@ -0,0 +1,90 @@
+using FluentAssertions;
+using HihaArvio.Models;
+
+namespace HihaArvio.Tests.Models;
+
+public class AppSettingsTests
+{
+ [Fact]
+ public void AppSettings_DefaultConstructor_ShouldSetWorkModeAsDefault()
+ {
+ // Act
+ var settings = new AppSettings();
+
+ // Assert
+ settings.SelectedMode.Should().Be(EstimateMode.Work);
+ }
+
+ [Fact]
+ public void AppSettings_DefaultConstructor_ShouldSetMaxHistorySizeTo10()
+ {
+ // Act
+ var settings = new AppSettings();
+
+ // Assert
+ settings.MaxHistorySize.Should().Be(10);
+ }
+
+ [Theory]
+ [InlineData(EstimateMode.Work)]
+ [InlineData(EstimateMode.Generic)]
+ [InlineData(EstimateMode.Humorous)]
+ public void AppSettings_ShouldAllowModeChanges(EstimateMode mode)
+ {
+ // Arrange
+ var settings = new AppSettings();
+
+ // Act
+ settings.SelectedMode = mode;
+
+ // Assert
+ settings.SelectedMode.Should().Be(mode);
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(5)]
+ [InlineData(10)]
+ [InlineData(50)]
+ [InlineData(100)]
+ public void AppSettings_ShouldAcceptValidMaxHistorySizeValues(int size)
+ {
+ // Arrange
+ var settings = new AppSettings();
+
+ // Act
+ settings.MaxHistorySize = size;
+
+ // Assert
+ settings.MaxHistorySize.Should().Be(size);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(-1)]
+ [InlineData(-10)]
+ public void AppSettings_ShouldThrowForInvalidMaxHistorySize(int invalidSize)
+ {
+ // Arrange
+ var settings = new AppSettings();
+
+ // Act
+ Action act = () => settings.MaxHistorySize = invalidSize;
+
+ // Assert
+ act.Should().Throw()
+ .WithMessage("*must be greater than 0*");
+ }
+
+ [Fact]
+ public void AppSettings_ShouldCreateWithDefaultValues()
+ {
+ // Act
+ var settings = new AppSettings();
+
+ // Assert
+ settings.Should().NotBeNull();
+ settings.SelectedMode.Should().Be(EstimateMode.Work);
+ settings.MaxHistorySize.Should().Be(10);
+ }
+}
diff --git a/tests/HihaArvio.Tests/Models/EstimateModeTests.cs b/tests/HihaArvio.Tests/Models/EstimateModeTests.cs
new file mode 100644
index 0000000..c6b3f5a
--- /dev/null
+++ b/tests/HihaArvio.Tests/Models/EstimateModeTests.cs
@@ -0,0 +1,51 @@
+using FluentAssertions;
+using HihaArvio.Models;
+
+namespace HihaArvio.Tests.Models;
+
+public class EstimateModeTests
+{
+ [Fact]
+ public void EstimateMode_ShouldHaveWorkValue()
+ {
+ // Act
+ var mode = EstimateMode.Work;
+
+ // Assert
+ mode.Should().Be(EstimateMode.Work);
+ ((int)mode).Should().Be(0);
+ }
+
+ [Fact]
+ public void EstimateMode_ShouldHaveGenericValue()
+ {
+ // Act
+ var mode = EstimateMode.Generic;
+
+ // Assert
+ mode.Should().Be(EstimateMode.Generic);
+ ((int)mode).Should().Be(1);
+ }
+
+ [Fact]
+ public void EstimateMode_ShouldHaveHumorousValue()
+ {
+ // Act
+ var mode = EstimateMode.Humorous;
+
+ // Assert
+ mode.Should().Be(EstimateMode.Humorous);
+ ((int)mode).Should().Be(2);
+ }
+
+ [Fact]
+ public void EstimateMode_ShouldHaveExactlyThreeValues()
+ {
+ // Act
+ var values = Enum.GetValues();
+
+ // Assert
+ values.Should().HaveCount(3);
+ values.Should().Contain(new[] { EstimateMode.Work, EstimateMode.Generic, EstimateMode.Humorous });
+ }
+}
diff --git a/tests/HihaArvio.Tests/Models/EstimateResultTests.cs b/tests/HihaArvio.Tests/Models/EstimateResultTests.cs
new file mode 100644
index 0000000..35bd6f4
--- /dev/null
+++ b/tests/HihaArvio.Tests/Models/EstimateResultTests.cs
@@ -0,0 +1,147 @@
+using FluentAssertions;
+using HihaArvio.Models;
+
+namespace HihaArvio.Tests.Models;
+
+public class EstimateResultTests
+{
+ [Fact]
+ public void EstimateResult_ShouldCreateWithAllProperties()
+ {
+ // Arrange
+ var id = Guid.NewGuid();
+ var timestamp = DateTimeOffset.UtcNow;
+ var estimateText = "2 weeks";
+ var mode = EstimateMode.Work;
+ var intensity = 0.75;
+ var duration = TimeSpan.FromSeconds(5);
+
+ // Act
+ var result = new EstimateResult
+ {
+ Id = id,
+ Timestamp = timestamp,
+ EstimateText = estimateText,
+ Mode = mode,
+ ShakeIntensity = intensity,
+ ShakeDuration = duration
+ };
+
+ // Assert
+ result.Id.Should().Be(id);
+ result.Timestamp.Should().Be(timestamp);
+ result.EstimateText.Should().Be(estimateText);
+ result.Mode.Should().Be(mode);
+ result.ShakeIntensity.Should().Be(intensity);
+ result.ShakeDuration.Should().Be(duration);
+ }
+
+ [Theory]
+ [InlineData(0.0)]
+ [InlineData(0.3)]
+ [InlineData(0.7)]
+ [InlineData(1.0)]
+ public void EstimateResult_ShouldAcceptValidIntensityValues(double intensity)
+ {
+ // Act
+ var result = new EstimateResult { ShakeIntensity = intensity };
+
+ // Assert
+ result.ShakeIntensity.Should().Be(intensity);
+ }
+
+ [Theory]
+ [InlineData(-0.1)]
+ [InlineData(1.1)]
+ [InlineData(2.0)]
+ public void EstimateResult_ShouldThrowForInvalidIntensity(double invalidIntensity)
+ {
+ // Act
+ Action act = () => _ = new EstimateResult { ShakeIntensity = invalidIntensity };
+
+ // Assert
+ act.Should().Throw()
+ .WithMessage("*must be between 0.0 and 1.0*");
+ }
+
+ [Fact]
+ public void EstimateResult_ShouldThrowForNullEstimateText()
+ {
+ // Act
+ Action act = () => _ = new EstimateResult { EstimateText = null! };
+
+ // Assert
+ act.Should().Throw()
+ .WithParameterName("value");
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void EstimateResult_ShouldThrowForEmptyOrWhitespaceEstimateText(string invalidText)
+ {
+ // Act
+ Action act = () => _ = new EstimateResult { EstimateText = invalidText };
+
+ // Assert
+ act.Should().Throw()
+ .WithMessage("*cannot be empty or whitespace*");
+ }
+
+ [Fact]
+ public void EstimateResult_ShouldAcceptZeroDuration()
+ {
+ // Act
+ var result = new EstimateResult { ShakeDuration = TimeSpan.Zero };
+
+ // Assert
+ result.ShakeDuration.Should().Be(TimeSpan.Zero);
+ }
+
+ [Fact]
+ public void EstimateResult_ShouldGenerateUniqueIds()
+ {
+ // Arrange & Act
+ var result1 = EstimateResult.Create("test1", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(1));
+ var result2 = EstimateResult.Create("test2", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(1));
+
+ // Assert
+ result1.Id.Should().NotBe(result2.Id);
+ result1.Id.Should().NotBeEmpty();
+ result2.Id.Should().NotBeEmpty();
+ }
+
+ [Fact]
+ public void EstimateResult_Create_ShouldSetTimestampAutomatically()
+ {
+ // Arrange
+ var before = DateTimeOffset.UtcNow;
+
+ // Act
+ var result = EstimateResult.Create("test", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(1));
+ var after = DateTimeOffset.UtcNow;
+
+ // Assert
+ result.Timestamp.Should().BeOnOrAfter(before);
+ result.Timestamp.Should().BeOnOrBefore(after);
+ }
+
+ [Fact]
+ public void EstimateResult_Create_ShouldSetAllProperties()
+ {
+ // Arrange
+ var estimateText = "3 months";
+ var mode = EstimateMode.Generic;
+ var intensity = 0.8;
+ var duration = TimeSpan.FromSeconds(10);
+
+ // Act
+ var result = EstimateResult.Create(estimateText, mode, intensity, duration);
+
+ // Assert
+ result.EstimateText.Should().Be(estimateText);
+ result.Mode.Should().Be(mode);
+ result.ShakeIntensity.Should().Be(intensity);
+ result.ShakeDuration.Should().Be(duration);
+ }
+}
diff --git a/tests/HihaArvio.Tests/Models/ShakeDataTests.cs b/tests/HihaArvio.Tests/Models/ShakeDataTests.cs
new file mode 100644
index 0000000..06926f0
--- /dev/null
+++ b/tests/HihaArvio.Tests/Models/ShakeDataTests.cs
@@ -0,0 +1,106 @@
+using FluentAssertions;
+using HihaArvio.Models;
+
+namespace HihaArvio.Tests.Models;
+
+public class ShakeDataTests
+{
+ [Fact]
+ public void ShakeData_ShouldCreateWithAllProperties()
+ {
+ // Arrange
+ var intensity = 0.65;
+ var duration = TimeSpan.FromSeconds(3);
+ var isShaking = true;
+
+ // Act
+ var shakeData = new ShakeData
+ {
+ Intensity = intensity,
+ Duration = duration,
+ IsShaking = isShaking
+ };
+
+ // Assert
+ shakeData.Intensity.Should().Be(intensity);
+ shakeData.Duration.Should().Be(duration);
+ shakeData.IsShaking.Should().Be(isShaking);
+ }
+
+ [Theory]
+ [InlineData(0.0)]
+ [InlineData(0.25)]
+ [InlineData(0.5)]
+ [InlineData(0.75)]
+ [InlineData(1.0)]
+ public void ShakeData_ShouldAcceptValidIntensityValues(double intensity)
+ {
+ // Act
+ var shakeData = new ShakeData { Intensity = intensity };
+
+ // Assert
+ shakeData.Intensity.Should().Be(intensity);
+ }
+
+ [Theory]
+ [InlineData(-0.1)]
+ [InlineData(-1.0)]
+ [InlineData(1.01)]
+ [InlineData(2.0)]
+ public void ShakeData_ShouldThrowForInvalidIntensity(double invalidIntensity)
+ {
+ // Act
+ Action act = () => _ = new ShakeData { Intensity = invalidIntensity };
+
+ // Assert
+ act.Should().Throw()
+ .WithMessage("*must be between 0.0 and 1.0*");
+ }
+
+ [Fact]
+ public void ShakeData_ShouldAcceptZeroDuration()
+ {
+ // Act
+ var shakeData = new ShakeData { Duration = TimeSpan.Zero };
+
+ // Assert
+ shakeData.Duration.Should().Be(TimeSpan.Zero);
+ }
+
+ [Fact]
+ public void ShakeData_ShouldAcceptPositiveDuration()
+ {
+ // Arrange
+ var duration = TimeSpan.FromSeconds(15.5);
+
+ // Act
+ var shakeData = new ShakeData { Duration = duration };
+
+ // Assert
+ shakeData.Duration.Should().Be(duration);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ShakeData_ShouldAcceptBooleanIsShakingValues(bool isShaking)
+ {
+ // Act
+ var shakeData = new ShakeData { IsShaking = isShaking };
+
+ // Assert
+ shakeData.IsShaking.Should().Be(isShaking);
+ }
+
+ [Fact]
+ public void ShakeData_DefaultConstructor_ShouldSetDefaultValues()
+ {
+ // Act
+ var shakeData = new ShakeData();
+
+ // Assert
+ shakeData.Intensity.Should().Be(0.0);
+ shakeData.Duration.Should().Be(TimeSpan.Zero);
+ shakeData.IsShaking.Should().BeFalse();
+ }
+}
diff --git a/tests/HihaArvio.Tests/TestResults/a7530e2d-8631-4bd5-9898-59b8cf8b73ed/coverage.cobertura.xml b/tests/HihaArvio.Tests/TestResults/a7530e2d-8631-4bd5-9898-59b8cf8b73ed/coverage.cobertura.xml
new file mode 100644
index 0000000..b98f483
--- /dev/null
+++ b/tests/HihaArvio.Tests/TestResults/a7530e2d-8631-4bd5-9898-59b8cf8b73ed/coverage.cobertura.xml
@@ -0,0 +1,490 @@
+
+
+
+ /Users/ivuorinen/Code/ivuorinen/hiha-arvio/src/HihaArvio/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file