Io/zerospace converter #1
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
sessionId: session-260928-131223-1ueu
|
||||||
|
---
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
### Overview & Goals
|
||||||
|
Resolve the compilation error `CS7036` on `PlatformImage` in `ZeroSpace.MapEditor/Services/MapBackground.cs` and resolve compiler nullability warnings in `MapEditorPage.xaml.cs`, ensuring `ZeroSpace.MapEditor` and the entire solution (`ZeroSpace.sln`) compile cleanly with zero errors.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
#### In Scope
|
||||||
|
- Update `ZeroSpace.MapEditor/Services/MapBackground.cs` to load the galaxy map background image stream via the MAUI Graphics `PlatformImage.FromStream(buffer)` factory instead of the invalid two-parameter constructor `PlatformImage(ICanvasResourceCreator, CanvasBitmap)`.
|
||||||
|
- Resolve `CS8604` nullability warnings in `ZeroSpace.MapEditor/Views/MapEditorPage.xaml.cs` for `PopulateInspector` calls.
|
||||||
|
- Verify end-to-end solution build (`dotnet build ZeroSpace\ZeroSpace.sln`) with 0 errors across all projects.
|
||||||
|
|
||||||
|
#### Out of Scope
|
||||||
|
- Redesigning MapEditor UI layouts or modifying StarSystem data structures.
|
||||||
|
- Altering web pages in `ZeroSpace.Web`.
|
||||||
|
|
||||||
|
### User Stories
|
||||||
|
- **As a developer**, I want `ZeroSpace.MapEditor` and the entire `ZeroSpace.sln` solution to build cleanly without compile errors so that the Map Editor and all companion projects can be built, debugged, and run reliably.
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
1. **PlatformImage Factory**: `MapBackground.LoadAsync()` must use the platform-compatible image stream loader (`Microsoft.Maui.Graphics.Platform.PlatformImage.FromStream(buffer)`) returning `IImage?`.
|
||||||
|
2. **Graceful Exception Fallback**: Maintain existing `try-catch` exception handling so the editor runs cleanly even if the background asset cannot be loaded or is absent.
|
||||||
|
3. **Null-Safety in MapEditorPage**: Ensure `PopulateInspector(MapNode node)` receives a guaranteed non-null `MapNode` reference across all selection and marquee interaction paths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Technical Design
|
||||||
|
|
||||||
|
### Current Implementation
|
||||||
|
- `ZeroSpace.MapEditor/Services/MapBackground.cs` attempts `new Microsoft.Maui.Graphics.Platform.PlatformImage(buffer)` which fails with `CS7036: There is no argument given that corresponds to the required parameter 'bitmap' of 'PlatformImage.PlatformImage(ICanvasResourceCreator, CanvasBitmap)'`.
|
||||||
|
- In `Microsoft.Maui.Graphics.Platform` (.NET 10 / Windows App SDK / Win2D), `PlatformImage` instances from byte streams are instantiated via `PlatformImage.FromStream(Stream stream, ImageFormat format = ImageFormat.Png)` or `PlatformImageService.Instance.FromStream(stream)`.
|
||||||
|
- `MapEditorPage.xaml.cs` has 3 occurrences of `CS8604` possible null reference arguments where `_selected` is passed to `PopulateInspector(MapNode node)` right after `SetActiveNode(_pressNode)` or in multi-node selection resolution.
|
||||||
|
|
||||||
|
### Key Decisions
|
||||||
|
1. **Use `PlatformImage.FromStream` Factory**:
|
||||||
|
- *Chosen Approach*: Replace direct `new PlatformImage(buffer)` constructor calls with `Microsoft.Maui.Graphics.Platform.PlatformImage.FromStream(buffer)`.
|
||||||
|
- *Rationale*: `PlatformImage.FromStream` is the standard cross-platform MAUI Graphics factory method for creating `IImage` instances from stream buffers on Windows and MacCatalyst.
|
||||||
|
2. **Explicit Non-Null Variable Passing in `MapEditorPage`**:
|
||||||
|
- *Chosen Approach*: Pass `_pressNode` or computed non-null `active` node directly to `PopulateInspector(node)`.
|
||||||
|
- *Rationale*: Eliminates nullable warnings cleanly without suppressing diagnostics or adding runtime overhead.
|
||||||
|
|
||||||
|
### Proposed Changes
|
||||||
|
1. `ZeroSpace.MapEditor/Services/MapBackground.cs`:
|
||||||
|
- Replace `new Microsoft.Maui.Graphics.Platform.PlatformImage(buffer)` with `Microsoft.Maui.Graphics.Platform.PlatformImage.FromStream(buffer)`.
|
||||||
|
2. `ZeroSpace.MapEditor/Views/MapEditorPage.xaml.cs`:
|
||||||
|
- Pass `_pressNode` in selection click handlers and pass local non-null `active` node in multi-select resolution before calling `PopulateInspector`.
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
- `ZeroSpace/ZeroSpace.MapEditor/Services/MapBackground.cs` (Modified)
|
||||||
|
- `ZeroSpace/ZeroSpace.MapEditor/Views/MapEditorPage.xaml.cs` (Modified)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
### Validation Approach
|
||||||
|
- Execute `dotnet build ZeroSpace\ZeroSpace.sln` to verify all projects compile with 0 errors.
|
||||||
|
- Verify `ZeroSpace.MapEditor.csproj` compiles cleanly for `net10.0-windows10.0.19041.0`.
|
||||||
|
|
||||||
|
### Key Scenarios
|
||||||
|
1. **MapBackground Image Stream Loading**: Confirm `MapBackground.LoadAsync()` builds cleanly with `PlatformImage.FromStream`.
|
||||||
|
2. **Full Solution Compilation**: Confirm `ZeroSpace.sln` completes with 0 errors and 0 blocking warnings.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
---
|
||||||
|
sessionId: session-260928-192341-jvqx
|
||||||
|
---
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
### Overview & Goals
|
||||||
|
Introduce a dedicated **Faction Mode** to `ZeroSpace.MapEditor` that allows rapid visual painting and assignment of faction ownership to star systems on the galaxy map canvas. The user can switch the active faction using hotkeys (`A`, `S`, `D`) or toolbar controls, click on any system to assign the selected faction, immediately observe the updated faction color on the map and inspector, and have changes automatically persisted to the underlying markdown files.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- **In Scope:**
|
||||||
|
- Active Faction Mode state in `MapEditorPage` supporting the three core factions:
|
||||||
|
- `A` → **The Eden Accord** (Green: `#10b981`)
|
||||||
|
- `S` → **The Keeper Council** (Blue: `#38bdf8`)
|
||||||
|
- `D` → **The Obsidian Order** (Red: `#ef4444`)
|
||||||
|
- Toolbar UI controls and indicators showing the current active faction mode and its associated hotkey.
|
||||||
|
- Keyboard shortcut listeners for `A`, `S`, and `D` that activate the corresponding faction mode (with guards against triggering when typing in text fields like search or coordinate entries).
|
||||||
|
- Applying the active faction to star system nodes upon single click / selection.
|
||||||
|
- Applying the active faction directly to currently selected node(s) when `A`, `S`, or `D` is pressed.
|
||||||
|
- Immediate visual feedback on the map canvas (`MapDrawable`) and inspector panel (`OwnerLabel`) reflecting the updated owner color.
|
||||||
|
- Auto-saving owner modifications back to the markdown frontmatter in `zs.docs/StarSystem/*.md` via `StarSystemDocStore`.
|
||||||
|
- **Out of Scope:**
|
||||||
|
- Modifying gameplay balance or data generation outside of `ZeroSpace.MapEditor` and `StarSystemDocStore`.
|
||||||
|
- Adding new factions beyond the defined three in `StarSystemOwners`.
|
||||||
|
|
||||||
|
### User Stories
|
||||||
|
- **As a Map Designer**, I want to press `A`, `S`, or `D` to switch the active faction mode so that I can rapidly tag multiple star systems without tedious manual dropdown selection.
|
||||||
|
- **As a Map Designer**, I want to click on a star system on the map canvas and have its faction immediately update with corresponding visual color changes so that I have real-time visual confirmation of galaxy ownership boundaries.
|
||||||
|
- **As a Map Designer**, I want my faction changes to automatically save to the system markdown files so that I do not lose progress or have to manually click save after every edit.
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
1. **Faction Mode State & Controls:**
|
||||||
|
- The toolbar must display the currently active faction mode with appropriate faction accent coloring.
|
||||||
|
- Interactive buttons for each faction (`[A] Eden Accord`, `[S] Keeper Council`, `[D] Obsidian Order`) allowing mouse-based activation as well as hotkey discovery.
|
||||||
|
2. **Hotkey Handling:**
|
||||||
|
- Pressing `A` (or `a`) sets active faction to `"The Eden Accord"`.
|
||||||
|
- Pressing `S` (or `s`) sets active faction to `"The Keeper Council"`.
|
||||||
|
- Pressing `D` (or `d`) sets active faction to `"The Obsidian Order"`.
|
||||||
|
- Hotkeys must not fire when focus is in an input field (`SearchControl`, `XEntry`, `YEntry`).
|
||||||
|
3. **Click-to-Assign:**
|
||||||
|
- Clicking a star system node in faction mode assigns the active faction to that node's `Doc.Owner`.
|
||||||
|
- If nodes are selected, pressing `A`, `S`, or `D` immediately updates the owner of all selected nodes.
|
||||||
|
4. **UI Color Synchronization:**
|
||||||
|
- The star system circle on the map canvas immediately re-renders with the faction's color:
|
||||||
|
- The Eden Accord: `#10b981`
|
||||||
|
- The Keeper Council: `#38bdf8`
|
||||||
|
- The Obsidian Order: `#ef4444`
|
||||||
|
- If the system is displayed in the inspector panel, the `OwnerLabel` and its styling update in real time.
|
||||||
|
5. **Auto-Saving:**
|
||||||
|
- Modifying a node's owner marks `Doc.IsDirty = true`.
|
||||||
|
- Automatically triggers `AutoSaveAsync()` to persist the `owner: <Faction>` frontmatter property in the system's markdown file.
|
||||||
|
- The Save button and status text reflect save progress and confirmation ("Saving...", "Saved", "Auto-saved X system(s)").
|
||||||
|
|
||||||
|
# Technical Design
|
||||||
|
|
||||||
|
### Current Implementation
|
||||||
|
- `StarSystemDocStore`: Parses YAML frontmatter from markdown files in `zs.docs/StarSystem/`. Currently, `SavePositions` only updates `x:` and `y:` lines in frontmatter. `owner:` is read during `LoadFile` but not rewritten during save.
|
||||||
|
- `MapDrawable`: Renders systems using `ColorForOwner(node.Doc.Owner)`. Supports node translation gizmo, marquee selection, background image, and connection lines.
|
||||||
|
- `MapEditorPage`: Handles pointer gestures (`OnPointerPressed`, `OnPointerMoved`, `OnPointerReleased`), panning, selection, inspector binding, and debounced auto-saving for position edits.
|
||||||
|
- `StarSystemOwners`: Defines constant strings for `"The Eden Accord"`, `"The Keeper Council"`, and `"The Obsidian Order"` along with standard hex colors.
|
||||||
|
|
||||||
|
### Key Decisions
|
||||||
|
1. **Faction Mode Model**:
|
||||||
|
- Introduce an explicit active faction mode state in `MapEditorPage` (`_activeFaction`: string or enum, defaulting to `"The Eden Accord"` or toggleable mode).
|
||||||
|
- Hotkeys `A`, `S`, `D` both switch the active painting faction and immediately apply it to any actively selected system(s).
|
||||||
|
2. **Keyboard Event Interception**:
|
||||||
|
- Hook window / page key down events on Windows / MAUI (`Loaded` / platform key events / MAUI `KeyboardAccelerators`).
|
||||||
|
- Check `IsFocused` on `SearchControl`, `XEntry`, and `YEntry` before handling hotkeys so normal text entry is never hindered.
|
||||||
|
3. **Markdown Frontmatter Persistence**:
|
||||||
|
- Update `StarSystemDocStore` save logic to update or insert the `owner: <Owner>` frontmatter entry alongside `x:` and `y:` while preserving all other frontmatter fields and body markdown intact.
|
||||||
|
4. **Immediate Color Feedback**:
|
||||||
|
- Mutating `node.Doc.Owner` directly triggers `MapView.Invalidate()` and `PopulateInspector(_selected)`, ensuring zero latency in UI color updates.
|
||||||
|
|
||||||
|
### Data Models & Contracts
|
||||||
|
```csharp
|
||||||
|
public enum FactionMode
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
EdenAccord, // Hotkey A - #10b981
|
||||||
|
KeeperCouncil, // Hotkey S - #38bdf8
|
||||||
|
ObsidianOrder // Hotkey D - #ef4444
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontmatter update in `StarSystemDocStore`:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
category: StarSystem
|
||||||
|
systemCategory: Side System
|
||||||
|
owner: The Keeper Council
|
||||||
|
occupationBuff: +50 Starting Hexite
|
||||||
|
systemMastery: "[[Unit Health and Shields]]"
|
||||||
|
x: 450.2
|
||||||
|
y: 310.8
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### Components & File Structure
|
||||||
|
- `ZeroSpace.MapEditor/Services/StarSystemDocStore.cs`:
|
||||||
|
- Enhance `SaveDoc` / `SaveDocs` / `SavePosition` to serialize `owner` to the frontmatter.
|
||||||
|
- `ZeroSpace.MapEditor/Views/MapEditorPage.xaml`:
|
||||||
|
- Add Faction Mode UI toolbar buttons/indicators (`Eden Accord [A]`, `Keeper Council [S]`, `Obsidian Order [D]`) with visual active highlights and color badges.
|
||||||
|
- `ZeroSpace.MapEditor/Views/MapEditorPage.xaml.cs`:
|
||||||
|
- Manage active faction mode state and toolbar button styling.
|
||||||
|
- Implement key press listeners for `A`, `S`, `D`.
|
||||||
|
- Handle click-to-assign on star system hit test.
|
||||||
|
- Refresh inspector and canvas colors.
|
||||||
|
- Integrate with `AutoSaveAsync()`.
|
||||||
|
|
||||||
|
### Architecture Diagram
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
UserKeyboard["User Press: A / S / D"] --> KeyHandler["MapEditorPage Key Handler"]
|
||||||
|
UserClick["User Click on Star System Node"] --> PointerHandler["OnPointerPressed / Click Handler"]
|
||||||
|
ToolbarButton["User Click Toolbar Faction Button"] --> FactionSelector["SetActiveFactionMode(...)"]
|
||||||
|
|
||||||
|
KeyHandler --> FactionSelector
|
||||||
|
KeyHandler --> ApplyFaction["Apply Faction to Selected Node(s)"]
|
||||||
|
FactionSelector --> UpdateToolbarUI["Update Toolbar Mode Indicators & Badges"]
|
||||||
|
PointerHandler --> ApplyFaction
|
||||||
|
|
||||||
|
ApplyFaction --> UpdateDoc["Update Doc.Owner & Set Doc.IsDirty = true"]
|
||||||
|
UpdateDoc --> RedrawMap["MapView.Invalidate() (MapDrawable.ColorForOwner)"]
|
||||||
|
UpdateDoc --> UpdateInspector["PopulateInspector(node) (OwnerLabel Color)"]
|
||||||
|
UpdateDoc --> AutoSave["AutoSaveAsync()"]
|
||||||
|
AutoSave --> DocStore["StarSystemDocStore.SaveDocs(...)"]
|
||||||
|
DocStore --> MarkdownFiles["Persist 'owner: ...' to zs.docs/StarSystem/*.md"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Risks & Mitigations
|
||||||
|
- **Risk:** Typing 'a', 's', or 'd' inside the search bar or coordinate entries could trigger faction mode changes.
|
||||||
|
- *Mitigation:* Ensure key events check focus status on all input entries (`SearchControl.IsFocused`, `XEntry.IsFocused`, `YEntry.IsFocused`) before processing hotkeys.
|
||||||
|
- **Risk:** Modifying frontmatter could accidentally reorder or corrupt markdown content.
|
||||||
|
- *Mitigation:* Use regex/line-based frontmatter replacement in `StarSystemDocStore` matching existing `SavePosition` patterns to preserve line endings, spacing, and other YAML metadata.
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
### Validation Approach
|
||||||
|
Verify faction mode switching, hotkey interactions, click-to-assign workflows, real-time UI color updating, and frontmatter persistence across unit tests and interactive workflows.
|
||||||
|
|
||||||
|
### Key Scenarios
|
||||||
|
1. **Hotkey Faction Switching:**
|
||||||
|
- Press `A`: Faction mode switches to **The Eden Accord**; toolbar highlights the Accord button with green accent (`#10b981`).
|
||||||
|
- Press `S`: Faction mode switches to **The Keeper Council**; toolbar highlights the Keeper button with blue accent (`#38bdf8`).
|
||||||
|
- Press `D`: Faction mode switches to **The Obsidian Order**; toolbar highlights the Obsidian button with red accent (`#ef4444`).
|
||||||
|
2. **Click-to-Assign System Faction:**
|
||||||
|
- Click an unassigned or Eden system while Keeper Council mode (`S`) is active.
|
||||||
|
- Verify node color on map canvas immediately turns blue (`#38bdf8`).
|
||||||
|
- Verify Inspector `OwnerLabel` updates to "The Keeper Council" with matching color.
|
||||||
|
3. **Multi-Selection Assignment:**
|
||||||
|
- Marquee-select 3 systems and press `D`.
|
||||||
|
- Verify all 3 systems immediately turn red (`#ef4444`).
|
||||||
|
4. **Auto-Saving to Markdown:**
|
||||||
|
- Change a system's faction; verify the Save button transitions to "Saving..." and then "Saved" / "Auto-saved 1 system".
|
||||||
|
- Inspect the `.md` file on disk in `zs.docs/StarSystem/` and confirm `owner: ...` has been updated with valid YAML frontmatter formatting.
|
||||||
|
5. **Text Input Protection:**
|
||||||
|
- Focus `SearchControl` and type "Ashes"; verify 'A' and 's' are entered into the search box and do NOT trigger faction changes.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
- File with missing or malformed `owner` frontmatter key is gracefully updated and written.
|
||||||
|
- Multiple rapid hotkey presses or clicks debounce auto-save properly without race conditions.
|
||||||
|
- System with read-only attributes or special characters in name handles saving cleanly.
|
||||||
|
|
||||||
|
# Delivery Steps
|
||||||
|
|
||||||
|
### ✓ Step 1: Update StarSystemDocStore to support owner serialization and dirty state tracking
|
||||||
|
StarSystemDocStore supports persisting the `owner` property to markdown frontmatter, and StarSystemDoc dirty state tracks faction edits.
|
||||||
|
|
||||||
|
- Extend `StarSystemDocStore` methods (`SaveDoc`, `SaveDocs`, or `SavePosition`) to update or add the `owner:` frontmatter key when writing changes to `.md` files.
|
||||||
|
- Preserve all existing frontmatter keys, comments, and markdown body byte-for-byte when updating `owner`, `x`, and `y`.
|
||||||
|
- Ensure `doc.IsDirty` is set to `true` whenever `doc.Owner` is modified.
|
||||||
|
- Verify unit doc save logic parses and preserves the standard faction names (`The Eden Accord`, `The Keeper Council`, `The Obsidian Order`).
|
||||||
|
|
||||||
|
### ✓ Step 2: Add Faction Mode UI toolbar controls and visual indicators
|
||||||
|
The MapEditorPage toolbar displays visual buttons and indicators for the active faction mode with distinct faction colors.
|
||||||
|
|
||||||
|
- Add faction mode selection and indicator controls to the top toolbar in `MapEditorPage.xaml` (e.g., dedicated buttons/badges for Accord `[A]`, Keeper `[S]`, and Obsidian `[D]`, plus a mode toggle or active indicator).
|
||||||
|
- Style the faction buttons/indicators with their corresponding colors: Green (`#10b981`) for The Eden Accord, Blue (`#38bdf8`) for The Keeper Council, and Red (`#ef4444`) for The Obsidian Order.
|
||||||
|
- Update `OwnerLabel` (and optional badge) in the inspector panel to display the owner name with faction color coding.
|
||||||
|
- Provide clear visual feedback indicating which faction mode is currently active.
|
||||||
|
|
||||||
|
### ✓ Step 3: Implement hotkeys, click-to-assign logic, and auto-save integration
|
||||||
|
Pressing A, S, or D switches the active faction mode, and clicking any star system node immediately applies the faction and triggers auto-save.
|
||||||
|
|
||||||
|
- Add keyboard event handling to `MapEditorPage` for hotkeys `A`, `S`, and `D` (switching active faction to Eden Accord, Keeper Council, and Obsidian Order respectively), ignoring hotkeys when text fields (`SearchControl`, `XEntry`, `YEntry`) are focused.
|
||||||
|
- If one or more nodes are currently selected when pressing `A`, `S`, or `D`, apply the faction to the selection immediately.
|
||||||
|
- Update `OnPointerPressed` / `OnPointerReleased` to apply the active faction to the clicked `MapNode` when in faction mode.
|
||||||
|
- Invalidate the `MapView` canvas so the node's fill color updates immediately via `MapDrawable.ColorForOwner`.
|
||||||
|
- Trigger `AutoSaveAsync()` (or debounced `ScheduleAutoSave()`) whenever a system's owner changes, keeping the Save button status and feedback message in sync.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
sessionId: session-260928-184444-1lki
|
||||||
|
---
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
### Overview & Goals
|
||||||
|
Fix the build error in `ZeroSpace.MapEditor/Services/MapBackground.cs` where invoking `new PlatformImage(buffer)` fails with compilation error `CS7036: There is no argument given that corresponds to the required parameter 'bitmap' of 'PlatformImage.PlatformImage(ICanvasResourceCreator, CanvasBitmap)'`.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- **In Scope:**
|
||||||
|
- Fix the image instantiation logic in `ZeroSpace.MapEditor/Services/MapBackground.cs` to use `Microsoft.Maui.Graphics.Platform.PlatformImage.FromStream(buffer)`.
|
||||||
|
- Ensure compatibility with .NET MAUI Windows (`net10.0-windows10.0.19041.0`) and MacCatalyst (`net10.0-maccatalyst`).
|
||||||
|
- Verify project build and clean diagnostic status.
|
||||||
|
- **Out of Scope:**
|
||||||
|
- Modifying the galaxy map raw asset or canvas drawing logic in `MapEditorPage.xaml.cs` and `MapEditorDrawable.cs`.
|
||||||
|
- Introducing external third-party image decoding dependencies.
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
- `MapBackground.LoadAsync()` must asynchronously open `galaxymap.png` from app package assets, copy the stream into memory, and return a valid `Microsoft.Maui.Graphics.IImage` instance on supported platforms.
|
||||||
|
- If the asset cannot be found or is loaded on an unsupported platform, `MapBackground.LoadAsync()` must catch the exception and return `null` without crashing the application.
|
||||||
|
- `MapEditorPage` must continue to display the galaxy map background when enabled.
|
||||||
|
|
||||||
|
### Non-Functional Requirements
|
||||||
|
- Maintain zero build errors and zero warnings across all supported target frameworks.
|
||||||
|
- Preserve memory stream behavior ensuring stream data remains accessible for lazy platform decoders.
|
||||||
|
|
||||||
|
# Technical Design
|
||||||
|
|
||||||
|
### Current Implementation
|
||||||
|
In `ZeroSpace.MapEditor/Services/MapBackground.cs`:
|
||||||
|
```csharp
|
||||||
|
#if WINDOWS
|
||||||
|
return new Microsoft.Maui.Graphics.Platform.PlatformImage(buffer);
|
||||||
|
#elif MACCATALYST
|
||||||
|
return new Microsoft.Maui.Graphics.Platform.PlatformImage(buffer);
|
||||||
|
#else
|
||||||
|
return null;
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
On Windows (`Microsoft.Maui.Graphics.Win2D`), `PlatformImage` has no single-parameter constructor accepting a `Stream`. Its constructor expects `(ICanvasResourceCreator, CanvasBitmap)` or `(byte[], ImageFormat)`.
|
||||||
|
|
||||||
|
### Root Cause
|
||||||
|
Directly calling the `PlatformImage` constructor with a `MemoryStream` is invalid in Microsoft.Maui.Graphics for Win2D. The supported API to construct an `IImage` from a `Stream` across all MAUI platforms is the static factory method `PlatformImage.FromStream(Stream, ImageFormat)`.
|
||||||
|
|
||||||
|
### Key Decisions
|
||||||
|
- **Decision:** Use `PlatformImage.FromStream(buffer)` instead of constructor invocation.
|
||||||
|
- **Rationale:** `PlatformImage.FromStream(Stream, ImageFormat)` is the standard Microsoft.Maui.Graphics API designed for stream-based image creation on both Windows and MacCatalyst platforms.
|
||||||
|
|
||||||
|
### Proposed Changes
|
||||||
|
- In `ZeroSpace.MapEditor/Services/MapBackground.cs`:
|
||||||
|
- Update lines 25–31 to invoke `Microsoft.Maui.Graphics.Platform.PlatformImage.FromStream(buffer)` under `#if WINDOWS || MACCATALYST`.
|
||||||
|
- Maintain the existing try-catch safety wrapper that returns `null` if loading fails.
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
- `ZeroSpace.MapEditor/Services/MapBackground.cs` (Modified: update image factory call)
|
||||||
|
- `ZeroSpace.MapEditor/Views/MapEditorPage.xaml.cs` (Referenced: consumes `MapBackground.LoadAsync()`)
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
### Validation Approach
|
||||||
|
Verify that the compilation error `CS7036` is resolved and `ZeroSpace.MapEditor` builds successfully.
|
||||||
|
|
||||||
|
### Key Scenarios
|
||||||
|
- **Compilation Check:**
|
||||||
|
- Verify `ZeroSpace.MapEditor/Services/MapBackground.cs` with Rider code analysis to confirm zero compiler errors.
|
||||||
|
- Build `ZeroSpace.MapEditor.csproj` target framework `net10.0-windows10.0.19041.0`.
|
||||||
|
- **Image Loading & Fallback Check:**
|
||||||
|
- Ensure `MapBackground.LoadAsync()` returns an `IImage` instance when `galaxymap.png` is available.
|
||||||
|
- Ensure exceptions during loading (e.g. missing asset) are handled cleanly and return `null`.
|
||||||
|
|
||||||
|
# Delivery Steps
|
||||||
|
|
||||||
|
### ✓ Step 1: Refactor image stream loading to use PlatformImage.FromStream in MapBackground
|
||||||
|
`MapBackground.cs` compiles without CS7036 errors across target platforms by using the static factory method `PlatformImage.FromStream`.
|
||||||
|
|
||||||
|
- Replace `new Microsoft.Maui.Graphics.Platform.PlatformImage(buffer)` with `Microsoft.Maui.Graphics.Platform.PlatformImage.FromStream(buffer)` in `ZeroSpace.MapEditor/Services/MapBackground.cs`.
|
||||||
|
- Consolidate platform conditional directives `#if WINDOWS || MACCATALYST` (or use platform-agnostic graphics loading) while preserving graceful `null` fallback in the `catch` block and unsupported target platforms.
|
||||||
|
- Ensure using directives for `Microsoft.Maui.Graphics.Platform` are properly configured.
|
||||||
|
|
||||||
|
### ✓ Step 2: Verify compilation and image rendering integration
|
||||||
|
`ZeroSpace.MapEditor` builds cleanly without diagnostics or runtime issues when loading raw background assets.
|
||||||
|
|
||||||
|
- Run code analysis/diagnostics on `ZeroSpace.MapEditor/Services/MapBackground.cs` to confirm zero compilation errors and warnings.
|
||||||
|
- Build the `ZeroSpace.MapEditor` project to ensure proper integration with `Microsoft.Maui.Controls` and `Microsoft.Maui.Graphics`.
|
||||||
|
- Validate that `MapEditorPage.xaml.cs` successfully receives the `IImage` instance from `MapBackground.LoadAsync()` and invalidates the map canvas for rendering.
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<add key="Telerik Blazor 13.0.0" value="C:\Program Files (x86)\Progress\Telerik UI for Blazor 13.0.0" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
@@ -69,6 +69,63 @@ if (skipped.Count > 0)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var starSystems = new List<StarSystemSource>();
|
||||||
|
var systemMasteries = new List<SystemMasterySource>();
|
||||||
|
var skippedDocs = new List<string>();
|
||||||
|
var usedIds = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var path in Directory.EnumerateFiles(docsRoot, "*.md", SearchOption.AllDirectories).Order(StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var (frontMatter, body) = ReadDocument(path);
|
||||||
|
if (frontMatter is null || !frontMatter.TryGetValue("category", out var categoryValue))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(categoryValue.Scalar, "StarSystem", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (TryReadStarSystem(path, frontMatter, body, usedIds, out var system, out var reason))
|
||||||
|
{
|
||||||
|
starSystems.Add(system);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
skippedDocs.Add($"{Path.GetFileName(path)} ({reason})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (string.Equals(categoryValue.Scalar, "SystemMastery", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (TryReadSystemMastery(path, frontMatter, body, usedIds, out var mastery, out var reason))
|
||||||
|
{
|
||||||
|
systemMasteries.Add(mastery);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
skippedDocs.Add($"{Path.GetFileName(path)} ({reason})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
starSystems.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
systemMasteries.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
var starSystemsPath = Path.Combine(modelRoot, "StarSystems.g.cs");
|
||||||
|
var systemMasteriesPath = Path.Combine(modelRoot, "SystemMasteries.g.cs");
|
||||||
|
|
||||||
|
File.WriteAllText(starSystemsPath, GenerateStarSystems(starSystems), Encoding.UTF8);
|
||||||
|
File.WriteAllText(systemMasteriesPath, GenerateSystemMasteries(systemMasteries), Encoding.UTF8);
|
||||||
|
|
||||||
|
Console.WriteLine($"Generated {starSystems.Count} star systems: {Path.GetRelativePath(solutionRoot, starSystemsPath)}");
|
||||||
|
Console.WriteLine($"Generated {systemMasteries.Count} system masteries: {Path.GetRelativePath(solutionRoot, systemMasteriesPath)}");
|
||||||
|
if (skippedDocs.Count > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Skipped {skippedDocs.Count} incomplete docs:");
|
||||||
|
foreach (var entry in skippedDocs.Order(StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"- {entry}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static string FindSolutionRoot()
|
static string FindSolutionRoot()
|
||||||
{
|
{
|
||||||
var candidates = new[] { Environment.CurrentDirectory, AppContext.BaseDirectory };
|
var candidates = new[] { Environment.CurrentDirectory, AppContext.BaseDirectory };
|
||||||
@@ -368,9 +425,361 @@ static string GenerateUnits(IReadOnlyList<UnitSource> units)
|
|||||||
|
|
||||||
static string Literal(string? value)
|
static string Literal(string? value)
|
||||||
{
|
{
|
||||||
return value is null
|
if (value is null)
|
||||||
? "null"
|
{
|
||||||
: "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
|
return "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = new StringBuilder("\"");
|
||||||
|
foreach (var character in value)
|
||||||
|
{
|
||||||
|
builder.Append(character switch
|
||||||
|
{
|
||||||
|
'\\' => "\\\\",
|
||||||
|
'"' => "\\\"",
|
||||||
|
'\n' => "\\n",
|
||||||
|
'\r' => "\\r",
|
||||||
|
'\t' => "\\t",
|
||||||
|
_ => character.ToString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append('"');
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static (Dictionary<string, FrontMatterValue>? FrontMatter, string Body) ReadDocument(string path)
|
||||||
|
{
|
||||||
|
var lines = File.ReadAllLines(path);
|
||||||
|
if (lines.Length == 0 || lines[0].Trim() != "---")
|
||||||
|
{
|
||||||
|
return (null, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = new Dictionary<string, FrontMatterValue>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
string? listKey = null;
|
||||||
|
|
||||||
|
for (var i = 1; i < lines.Length; i++)
|
||||||
|
{
|
||||||
|
var line = lines[i];
|
||||||
|
if (line.Trim() == "---")
|
||||||
|
{
|
||||||
|
var body = string.Join('\n', lines, i + 1, lines.Length - i - 1).Trim();
|
||||||
|
return (values, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listKey is not null && line.StartsWith(" - ", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
values[listKey].List.Add(CleanValue(line[4..]));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
listKey = null;
|
||||||
|
var separatorIndex = line.IndexOf(':');
|
||||||
|
if (separatorIndex < 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = line[..separatorIndex].Trim();
|
||||||
|
var rawValue = line[(separatorIndex + 1)..].Trim();
|
||||||
|
if (rawValue.Length == 0)
|
||||||
|
{
|
||||||
|
values[key] = new FrontMatterValue(null);
|
||||||
|
listKey = key;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
values[key] = new FrontMatterValue(CleanValue(rawValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsTrue(IReadOnlyDictionary<string, FrontMatterValue> frontMatter, string key)
|
||||||
|
{
|
||||||
|
return frontMatter.TryGetValue(key, out var value)
|
||||||
|
&& string.Equals(value.Scalar, "true", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
static string ToUniqueIdentifier(string name, HashSet<string> usedIds)
|
||||||
|
{
|
||||||
|
var id = ToIdentifier(name);
|
||||||
|
if (usedIds.Add(id))
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var suffix = 2;
|
||||||
|
while (!usedIds.Add($"{id}{suffix}"))
|
||||||
|
{
|
||||||
|
suffix++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{id}{suffix}";
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool TryReadStarSystem(
|
||||||
|
string path,
|
||||||
|
IReadOnlyDictionary<string, FrontMatterValue> frontMatter,
|
||||||
|
string body,
|
||||||
|
HashSet<string> usedIds,
|
||||||
|
out StarSystemSource system,
|
||||||
|
out string reason)
|
||||||
|
{
|
||||||
|
system = default!;
|
||||||
|
|
||||||
|
string category;
|
||||||
|
if (frontMatter.TryGetValue("systemCategory", out var categoryVal) && !string.IsNullOrWhiteSpace(categoryVal.Scalar))
|
||||||
|
{
|
||||||
|
var catStr = categoryVal.Scalar.Trim();
|
||||||
|
if (catStr.Equals("Homeworld System", StringComparison.OrdinalIgnoreCase) || catStr.Equals("Homeworld", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
category = "HomeworldSystem";
|
||||||
|
}
|
||||||
|
else if (catStr.Equals("Core System", StringComparison.OrdinalIgnoreCase) || catStr.Equals("Core", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
category = "CoreSystem";
|
||||||
|
}
|
||||||
|
else if (catStr.Equals("Side System", StringComparison.OrdinalIgnoreCase) || catStr.Equals("Side", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
category = "SideSystem";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reason = $"unknown systemCategory '{catStr}'";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var kindCount = 0;
|
||||||
|
category = string.Empty;
|
||||||
|
if (IsTrue(frontMatter, "isSideSystem"))
|
||||||
|
{
|
||||||
|
category = "SideSystem";
|
||||||
|
kindCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsTrue(frontMatter, "isHomeworldSystem"))
|
||||||
|
{
|
||||||
|
category = "HomeworldSystem";
|
||||||
|
kindCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsTrue(frontMatter, "isCoreSystem"))
|
||||||
|
{
|
||||||
|
category = "CoreSystem";
|
||||||
|
kindCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kindCount != 1)
|
||||||
|
{
|
||||||
|
reason = "missing or invalid systemCategory";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var owner = TryGetOptionalString(frontMatter, "owner") ?? "The Eden Accord";
|
||||||
|
|
||||||
|
var name = Path.GetFileNameWithoutExtension(path);
|
||||||
|
var connectedSystems = frontMatter.TryGetValue("connectedSystems", out var connectedValue)
|
||||||
|
? connectedValue.List.Where(static value => !string.IsNullOrWhiteSpace(value)).ToArray()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
system = new StarSystemSource(
|
||||||
|
ToUniqueIdentifier(name, usedIds),
|
||||||
|
name,
|
||||||
|
category,
|
||||||
|
owner,
|
||||||
|
IsTrue(frontMatter, "higherInfluenceYield"),
|
||||||
|
IsTrue(frontMatter, "higherCreditYield"),
|
||||||
|
IsTrue(frontMatter, "higherEnergyYield"),
|
||||||
|
IsTrue(frontMatter, "higherChanceForItems"),
|
||||||
|
TryGetOptionalString(frontMatter, "occupationBuff"),
|
||||||
|
TryGetOptionalString(frontMatter, "systemMastery"),
|
||||||
|
connectedSystems,
|
||||||
|
GetDouble(frontMatter, "x"),
|
||||||
|
GetDouble(frontMatter, "y"),
|
||||||
|
body);
|
||||||
|
reason = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool TryReadSystemMastery(
|
||||||
|
string path,
|
||||||
|
IReadOnlyDictionary<string, FrontMatterValue> frontMatter,
|
||||||
|
string body,
|
||||||
|
HashSet<string> usedIds,
|
||||||
|
out SystemMasterySource mastery,
|
||||||
|
out string reason)
|
||||||
|
{
|
||||||
|
mastery = default!;
|
||||||
|
|
||||||
|
var levels = new string[5];
|
||||||
|
for (var level = 1; level <= 5; level++)
|
||||||
|
{
|
||||||
|
if (!TryGetRequiredString(frontMatter, $"level_{level}", out var text, out reason))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
levels[level - 1] = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
var availableSystems = frontMatter.TryGetValue("availableSystems", out var availableValue)
|
||||||
|
? availableValue.List.Where(static value => !string.IsNullOrWhiteSpace(value)).ToArray()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
var stats = new List<(string Key, double Value)>();
|
||||||
|
foreach (var (key, value) in frontMatter)
|
||||||
|
{
|
||||||
|
if (IsMasteryField(key))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(value.Scalar) ||
|
||||||
|
!double.TryParse(value.Scalar, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var number))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Warning: {Path.GetFileName(path)} has non-numeric stat '{key}', skipping it.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.Add((key, number));
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = Path.GetFileNameWithoutExtension(path);
|
||||||
|
mastery = new SystemMasterySource(
|
||||||
|
ToUniqueIdentifier(name, usedIds),
|
||||||
|
name,
|
||||||
|
levels,
|
||||||
|
availableSystems,
|
||||||
|
stats,
|
||||||
|
body);
|
||||||
|
reason = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsMasteryField(string key)
|
||||||
|
{
|
||||||
|
if (string.Equals(key, "category", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(key, "availableSystems", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return key.Length == 7 &&
|
||||||
|
key.StartsWith("level_", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
key[6] is >= '1' and <= '5';
|
||||||
|
}
|
||||||
|
|
||||||
|
static string GenerateStarSystems(IReadOnlyList<StarSystemSource> systems)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("// <auto-generated />");
|
||||||
|
builder.AppendLine("namespace ZeroSpace.Model;");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine("public static class StarSystems");
|
||||||
|
builder.AppendLine("{");
|
||||||
|
|
||||||
|
foreach (var system in systems)
|
||||||
|
{
|
||||||
|
builder.AppendLine($" public static readonly StarSystemData {system.Id} = new(");
|
||||||
|
builder.AppendLine($" Id: {Literal(system.Id)},");
|
||||||
|
builder.AppendLine($" Name: {Literal(system.Name)},");
|
||||||
|
builder.AppendLine($" SystemCategory: StarSystemCategory.{system.SystemCategory},");
|
||||||
|
builder.AppendLine($" Owner: {Literal(system.Owner)},");
|
||||||
|
builder.AppendLine($" HigherInfluenceYield: {BoolLiteral(system.HigherInfluenceYield)},");
|
||||||
|
builder.AppendLine($" HigherCreditYield: {BoolLiteral(system.HigherCreditYield)},");
|
||||||
|
builder.AppendLine($" HigherEnergyYield: {BoolLiteral(system.HigherEnergyYield)},");
|
||||||
|
builder.AppendLine($" HigherChanceForItems: {BoolLiteral(system.HigherChanceForItems)},");
|
||||||
|
builder.AppendLine($" OccupationBuff: {Literal(system.OccupationBuff)},");
|
||||||
|
builder.AppendLine($" SystemMastery: {Literal(system.SystemMastery)},");
|
||||||
|
builder.AppendLine($" ConnectedSystems: [{string.Join(", ", system.ConnectedSystems.Select(Literal))}],");
|
||||||
|
builder.AppendLine($" X: {system.X.ToString("R", CultureInfo.InvariantCulture)},");
|
||||||
|
builder.AppendLine($" Y: {system.Y.ToString("R", CultureInfo.InvariantCulture)},");
|
||||||
|
builder.AppendLine($" Body: {Literal(system.Body)});");
|
||||||
|
builder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" public static IReadOnlyList<StarSystemData> All { get; } =");
|
||||||
|
builder.AppendLine(" [");
|
||||||
|
|
||||||
|
foreach (var system in systems)
|
||||||
|
{
|
||||||
|
builder.AppendLine($" {system.Id},");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" ];");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" public static IReadOnlyDictionary<string, StarSystemData> ByName { get; } =");
|
||||||
|
builder.AppendLine(" All.ToDictionary(static system => system.Name, StringComparer.OrdinalIgnoreCase);");
|
||||||
|
builder.AppendLine("}");
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static string GenerateSystemMasteries(IReadOnlyList<SystemMasterySource> masteries)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("// <auto-generated />");
|
||||||
|
builder.AppendLine("namespace ZeroSpace.Model;");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine("public static class SystemMasteries");
|
||||||
|
builder.AppendLine("{");
|
||||||
|
|
||||||
|
foreach (var mastery in masteries)
|
||||||
|
{
|
||||||
|
builder.AppendLine($" public static readonly SystemMasteryData {mastery.Id} = new(");
|
||||||
|
builder.AppendLine($" Id: {Literal(mastery.Id)},");
|
||||||
|
builder.AppendLine($" Name: {Literal(mastery.Name)},");
|
||||||
|
builder.AppendLine($" Levels: [{string.Join(", ", mastery.Levels.Select(Literal))}],");
|
||||||
|
builder.AppendLine($" AvailableSystems: [{string.Join(", ", mastery.AvailableSystems.Select(Literal))}],");
|
||||||
|
builder.AppendLine(" Stats: new Dictionary<string, double>");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
|
||||||
|
foreach (var (key, value) in mastery.Stats)
|
||||||
|
{
|
||||||
|
builder.AppendLine($" [{Literal(key)}] = {value.ToString("R", CultureInfo.InvariantCulture)},");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" },");
|
||||||
|
builder.AppendLine($" Body: {Literal(mastery.Body)});");
|
||||||
|
builder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" public static IReadOnlyList<SystemMasteryData> All { get; } =");
|
||||||
|
builder.AppendLine(" [");
|
||||||
|
|
||||||
|
foreach (var mastery in masteries)
|
||||||
|
{
|
||||||
|
builder.AppendLine($" {mastery.Id},");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" ];");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" public static IReadOnlyDictionary<string, SystemMasteryData> ByName { get; } =");
|
||||||
|
builder.AppendLine(" All.ToDictionary(static mastery => mastery.Name, StringComparer.OrdinalIgnoreCase);");
|
||||||
|
builder.AppendLine("}");
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static string BoolLiteral(bool value)
|
||||||
|
{
|
||||||
|
return value ? "true" : "false";
|
||||||
|
}
|
||||||
|
|
||||||
|
static double GetDouble(IReadOnlyDictionary<string, FrontMatterValue> frontMatter, string key)
|
||||||
|
{
|
||||||
|
if (frontMatter.TryGetValue(key, out var value) &&
|
||||||
|
double.TryParse(value.Scalar, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var number))
|
||||||
|
{
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class FrontMatterValue(string? scalar)
|
internal sealed class FrontMatterValue(string? scalar)
|
||||||
@@ -400,3 +809,27 @@ internal sealed record UnitSource(
|
|||||||
string? Hotkey,
|
string? Hotkey,
|
||||||
int BuildAtSameTime,
|
int BuildAtSameTime,
|
||||||
int? Limit);
|
int? Limit);
|
||||||
|
|
||||||
|
internal sealed record StarSystemSource(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
string SystemCategory,
|
||||||
|
string Owner,
|
||||||
|
bool HigherInfluenceYield,
|
||||||
|
bool HigherCreditYield,
|
||||||
|
bool HigherEnergyYield,
|
||||||
|
bool HigherChanceForItems,
|
||||||
|
string? OccupationBuff,
|
||||||
|
string? SystemMastery,
|
||||||
|
IReadOnlyList<string> ConnectedSystems,
|
||||||
|
double X,
|
||||||
|
double Y,
|
||||||
|
string Body);
|
||||||
|
|
||||||
|
internal sealed record SystemMasterySource(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
IReadOnlyList<string> Levels,
|
||||||
|
IReadOnlyList<string> AvailableSystems,
|
||||||
|
IReadOnlyList<(string Key, double Value)> Stats,
|
||||||
|
string Body);
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="ZeroSpace.MapEditor.App">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using ZeroSpace.MapEditor.Views;
|
||||||
|
|
||||||
|
namespace ZeroSpace.MapEditor;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
public App()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Window CreateWindow(IActivationState? activationState)
|
||||||
|
{
|
||||||
|
return new Window(new MapEditorPage());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace ZeroSpace.MapEditor;
|
||||||
|
|
||||||
|
public static class MauiProgram
|
||||||
|
{
|
||||||
|
public static MauiApp CreateMauiApp()
|
||||||
|
{
|
||||||
|
var builder = MauiApp.CreateBuilder();
|
||||||
|
builder.UseMauiApp<App>();
|
||||||
|
return builder.Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using ZeroSpace.Model;
|
||||||
|
|
||||||
|
namespace ZeroSpace.MapEditor.Models;
|
||||||
|
|
||||||
|
public enum FactionMode
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
EdenAccord, // Hotkey A - #10b981
|
||||||
|
KeeperCouncil, // Hotkey S - #38bdf8
|
||||||
|
ObsidianOrder // Hotkey D - #ef4444
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FactionModeExtensions
|
||||||
|
{
|
||||||
|
public static string ToOwnerName(this FactionMode mode) => mode switch
|
||||||
|
{
|
||||||
|
FactionMode.EdenAccord => StarSystemOwners.EdenAccord,
|
||||||
|
FactionMode.KeeperCouncil => StarSystemOwners.KeeperCouncil,
|
||||||
|
FactionMode.ObsidianOrder => StarSystemOwners.ObsidianOrder,
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string ToDisplayName(this FactionMode mode) => mode switch
|
||||||
|
{
|
||||||
|
FactionMode.EdenAccord => "Eden Accord",
|
||||||
|
FactionMode.KeeperCouncil => "Keeper Council",
|
||||||
|
FactionMode.ObsidianOrder => "Obsidian Order",
|
||||||
|
_ => "None"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string GetColorHex(this FactionMode mode) => mode switch
|
||||||
|
{
|
||||||
|
FactionMode.EdenAccord => StarSystemOwners.GetColor(StarSystemOwners.EdenAccord),
|
||||||
|
FactionMode.KeeperCouncil => StarSystemOwners.GetColor(StarSystemOwners.KeeperCouncil),
|
||||||
|
FactionMode.ObsidianOrder => StarSystemOwners.GetColor(StarSystemOwners.ObsidianOrder),
|
||||||
|
_ => "#94a3b8"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static FactionMode FromOwnerName(string? owner) => owner switch
|
||||||
|
{
|
||||||
|
StarSystemOwners.EdenAccord => FactionMode.EdenAccord,
|
||||||
|
StarSystemOwners.KeeperCouncil => FactionMode.KeeperCouncil,
|
||||||
|
StarSystemOwners.ObsidianOrder => FactionMode.ObsidianOrder,
|
||||||
|
_ => FactionMode.None
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using ZeroSpace.MapEditor.Services;
|
||||||
|
|
||||||
|
namespace ZeroSpace.MapEditor.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A star system node on the map canvas: the document plus selection state.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MapNode
|
||||||
|
{
|
||||||
|
public MapNode(StarSystemDoc doc)
|
||||||
|
{
|
||||||
|
Doc = doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StarSystemDoc Doc { get; }
|
||||||
|
|
||||||
|
public double X
|
||||||
|
{
|
||||||
|
get => Doc.X;
|
||||||
|
set => Doc.X = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Y
|
||||||
|
{
|
||||||
|
get => Doc.Y;
|
||||||
|
set => Doc.Y = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSelected { get; set; }
|
||||||
|
|
||||||
|
public bool IsHighlighted { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<maui:MauiWinUIApplication
|
||||||
|
x:Class="ZeroSpace.MapEditor.WinUI.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:maui="using:Microsoft.Maui"
|
||||||
|
xmlns:local="using:ZeroSpace.MapEditor.WinUI">
|
||||||
|
|
||||||
|
</maui:MauiWinUIApplication>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
|
||||||
|
namespace ZeroSpace.MapEditor.WinUI;
|
||||||
|
|
||||||
|
public partial class App : MauiWinUIApplication
|
||||||
|
{
|
||||||
|
public App()
|
||||||
|
{
|
||||||
|
this.InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="ZeroSpace.MapEditor.WinUI.app"/>
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||||
|
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
</assembly>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
@@ -0,0 +1,37 @@
|
|||||||
|
using IImage = Microsoft.Maui.Graphics.IImage;
|
||||||
|
|
||||||
|
namespace ZeroSpace.MapEditor.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads the in-game galaxy map screenshot as the canvas background.
|
||||||
|
/// The PNG lives in Resources/Raw and is deployed as a MauiAsset.
|
||||||
|
/// </summary>
|
||||||
|
public static class MapBackground
|
||||||
|
{
|
||||||
|
private const string AssetName = "Resources\\Raw\\galaxymap.png";
|
||||||
|
|
||||||
|
public static async Task<IImage?> LoadAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var packageStream = await FileSystem.OpenAppPackageFileAsync(AssetName);
|
||||||
|
|
||||||
|
// Copy into memory and leave the buffer undisposed: some platform
|
||||||
|
// image implementations read from the stream lazily.
|
||||||
|
var buffer = new MemoryStream();
|
||||||
|
await packageStream.CopyToAsync(buffer);
|
||||||
|
buffer.Position = 0;
|
||||||
|
#if WINDOWS || MACCATALYST
|
||||||
|
return Microsoft.Maui.Graphics.Platform.PlatformImage.FromStream(buffer);
|
||||||
|
#else
|
||||||
|
return null;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// No map image available (missing asset, unsupported platform):
|
||||||
|
// the editor works fine without the background.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace ZeroSpace.MapEditor.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One StarSystem markdown document: the editable map data plus the
|
||||||
|
/// read-only details shown in the inspector. Has no MAUI dependencies
|
||||||
|
/// so it can be tested outside the app.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StarSystemDoc
|
||||||
|
{
|
||||||
|
private string _owner = string.Empty;
|
||||||
|
|
||||||
|
public string FilePath { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string SystemCategory { get; set; } = string.Empty;
|
||||||
|
public string Owner
|
||||||
|
{
|
||||||
|
get => _owner;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (!string.Equals(_owner, value, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_owner = value;
|
||||||
|
IsDirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool HigherInfluenceYield { get; set; }
|
||||||
|
public bool HigherCreditYield { get; set; }
|
||||||
|
public bool HigherEnergyYield { get; set; }
|
||||||
|
public bool HigherChanceForItems { get; set; }
|
||||||
|
public string? OccupationBuff { get; set; }
|
||||||
|
public string? SystemMastery { get; set; }
|
||||||
|
public List<string> ConnectedSystems { get; } = new();
|
||||||
|
public double X { get; set; }
|
||||||
|
public double Y { get; set; }
|
||||||
|
public bool HasPosition { get; set; }
|
||||||
|
public bool IsDirty { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class StarSystemDocStore
|
||||||
|
{
|
||||||
|
/// <summary>World coordinate space used in the markdown files.</summary>
|
||||||
|
public const double WorldSize = 1000;
|
||||||
|
|
||||||
|
public static List<StarSystemDoc> LoadFolder(string folder)
|
||||||
|
{
|
||||||
|
var docs = new List<StarSystemDoc>();
|
||||||
|
foreach (var path in Directory.GetFiles(folder, "*.md")
|
||||||
|
.OrderBy(static p => p, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var doc = LoadFile(path);
|
||||||
|
if (doc is not null)
|
||||||
|
{
|
||||||
|
docs.Add(doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any file missing x/y gets a deterministic spread-out placeholder.
|
||||||
|
var missing = docs.Where(static d => !d.HasPosition).ToList();
|
||||||
|
for (var i = 0; i < missing.Count; i++)
|
||||||
|
{
|
||||||
|
var (x, y) = DefaultPosition(i, missing.Count);
|
||||||
|
missing[i].X = x;
|
||||||
|
missing[i].Y = y;
|
||||||
|
missing[i].IsDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StarSystemDoc? LoadFile(string path)
|
||||||
|
{
|
||||||
|
var lines = File.ReadAllLines(path);
|
||||||
|
if (lines.Length < 2 || lines[0].Trim() != "---")
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var scalars = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
string? listKey = null;
|
||||||
|
var closed = false;
|
||||||
|
|
||||||
|
for (var i = 1; i < lines.Length; i++)
|
||||||
|
{
|
||||||
|
var line = lines[i];
|
||||||
|
if (line.Trim() == "---")
|
||||||
|
{
|
||||||
|
closed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listKey is not null && line.StartsWith(" - ", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
values[listKey].Add(CleanValue(line[4..]));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
listKey = null;
|
||||||
|
var separator = line.IndexOf(':');
|
||||||
|
if (separator < 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = line[..separator].Trim();
|
||||||
|
var raw = line[(separator + 1)..].Trim();
|
||||||
|
if (raw.Length == 0)
|
||||||
|
{
|
||||||
|
values[key] = new List<string>();
|
||||||
|
listKey = key;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
scalars[key] = CleanValue(raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!closed)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scalars.TryGetValue("category", out var category) ||
|
||||||
|
!string.Equals(category, "StarSystem", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var doc = new StarSystemDoc
|
||||||
|
{
|
||||||
|
FilePath = path,
|
||||||
|
Name = Path.GetFileNameWithoutExtension(path),
|
||||||
|
SystemCategory = scalars.TryGetValue("systemCategory", out var categoryVal) ? categoryVal : string.Empty,
|
||||||
|
Owner = scalars.TryGetValue("owner", out var ownerVal) ? ownerVal : "The Eden Accord",
|
||||||
|
OccupationBuff = scalars.TryGetValue("occupationBuff", out var buff) ? buff : null,
|
||||||
|
SystemMastery = scalars.TryGetValue("systemMastery", out var mastery) ? mastery : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(doc.SystemCategory))
|
||||||
|
{
|
||||||
|
if (IsTrue(scalars, "isSideSystem")) doc.SystemCategory = "Side System";
|
||||||
|
if (IsTrue(scalars, "isHomeworldSystem")) doc.SystemCategory = "Homeworld System";
|
||||||
|
if (IsTrue(scalars, "isCoreSystem")) doc.SystemCategory = "Core System";
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.HigherInfluenceYield = IsTrue(scalars, "higherInfluenceYield");
|
||||||
|
doc.HigherCreditYield = IsTrue(scalars, "higherCreditYield");
|
||||||
|
doc.HigherEnergyYield = IsTrue(scalars, "higherEnergyYield");
|
||||||
|
doc.HigherChanceForItems = IsTrue(scalars, "higherChanceForItems");
|
||||||
|
|
||||||
|
if (values.TryGetValue("connectedSystems", out var connected))
|
||||||
|
{
|
||||||
|
doc.ConnectedSystems.AddRange(connected.Where(static c => !string.IsNullOrWhiteSpace(c)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scalars.TryGetValue("x", out var xRaw) && TryParseDouble(xRaw, out var x) &&
|
||||||
|
scalars.TryGetValue("y", out var yRaw) && TryParseDouble(yRaw, out var y))
|
||||||
|
{
|
||||||
|
doc.X = x;
|
||||||
|
doc.Y = y;
|
||||||
|
doc.HasPosition = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.IsDirty = false;
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes x/y and owner back into each dirty file's frontmatter, preserving
|
||||||
|
/// every other line byte-for-byte.
|
||||||
|
/// </summary>
|
||||||
|
public static int SaveDocs(IEnumerable<StarSystemDoc> docs)
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
var errors = new List<Exception>();
|
||||||
|
var items = docs.Where(static d => d.IsDirty)
|
||||||
|
.Select(static d => (Doc: d, TargetX: d.X, TargetY: d.Y, TargetOwner: d.Owner))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SaveDoc(item.Doc.FilePath, item.TargetX, item.TargetY, item.TargetOwner);
|
||||||
|
if (item.Doc.X == item.TargetX &&
|
||||||
|
item.Doc.Y == item.TargetY &&
|
||||||
|
string.Equals(item.Doc.Owner, item.TargetOwner, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
item.Doc.IsDirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errors.Add(new IOException($"Failed to save '{item.Doc.Name}': {ex.Message}", ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.Count == 1)
|
||||||
|
{
|
||||||
|
throw errors[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.Count > 1)
|
||||||
|
{
|
||||||
|
throw new AggregateException(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Backwards-compatible alias for <see cref="SaveDocs"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static int SavePositions(IEnumerable<StarSystemDoc> docs) => SaveDocs(docs);
|
||||||
|
|
||||||
|
public static void SaveDoc(StarSystemDoc doc)
|
||||||
|
{
|
||||||
|
SaveDoc(doc.FilePath, doc.X, doc.Y, doc.Owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SavePosition(StarSystemDoc doc) => SaveDoc(doc);
|
||||||
|
|
||||||
|
public static void SavePosition(string filePath, double x, double y)
|
||||||
|
{
|
||||||
|
SaveDoc(filePath, x, y, owner: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SaveDoc(string filePath, double x, double y, string? owner = null)
|
||||||
|
{
|
||||||
|
var text = File.ReadAllText(filePath);
|
||||||
|
var newline = text.Contains("\r\n", StringComparison.Ordinal) ? "\r\n" : "\n";
|
||||||
|
var lines = text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).ToList();
|
||||||
|
if (lines.Count < 2 || lines[0].Trim() != "---")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var closeIndex = lines.FindIndex(1, static line => line.Trim() == "---");
|
||||||
|
if (closeIndex < 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var xLine = $"x: {x.ToString("F1", CultureInfo.InvariantCulture)}";
|
||||||
|
var yLine = $"y: {y.ToString("F1", CultureInfo.InvariantCulture)}";
|
||||||
|
var ownerLine = owner is not null ? $"owner: {owner}" : null;
|
||||||
|
var foundX = false;
|
||||||
|
var foundY = false;
|
||||||
|
var foundOwner = false;
|
||||||
|
|
||||||
|
for (var i = 1; i < closeIndex; i++)
|
||||||
|
{
|
||||||
|
var key = KeyOf(lines[i]);
|
||||||
|
if (string.Equals(key, "x", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
lines[i] = xLine;
|
||||||
|
foundX = true;
|
||||||
|
}
|
||||||
|
else if (string.Equals(key, "y", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
lines[i] = yLine;
|
||||||
|
foundY = true;
|
||||||
|
}
|
||||||
|
else if (ownerLine is not null && string.Equals(key, "owner", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
lines[i] = ownerLine;
|
||||||
|
foundOwner = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ownerLine is not null && !foundOwner)
|
||||||
|
{
|
||||||
|
lines.Insert(closeIndex, ownerLine);
|
||||||
|
closeIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundX)
|
||||||
|
{
|
||||||
|
lines.Insert(closeIndex, xLine);
|
||||||
|
closeIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundY)
|
||||||
|
{
|
||||||
|
lines.Insert(closeIndex, yLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(filePath, string.Join(newline, lines));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deterministic phyllotaxis (sunflower spiral) spread across the
|
||||||
|
/// world space, so placeholder nodes never cover each other.
|
||||||
|
/// </summary>
|
||||||
|
public static (double X, double Y) DefaultPosition(int index, int count)
|
||||||
|
{
|
||||||
|
var goldenAngle = Math.PI * (3 - Math.Sqrt(5));
|
||||||
|
var radius = (WorldSize / 2 - 30) * Math.Sqrt(index / (double)Math.Max(count, 1));
|
||||||
|
var theta = index * goldenAngle;
|
||||||
|
return (WorldSize / 2 + radius * Math.Cos(theta),
|
||||||
|
WorldSize / 2 + radius * Math.Sin(theta));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? KeyOf(string line)
|
||||||
|
{
|
||||||
|
var separator = line.IndexOf(':');
|
||||||
|
return separator < 0 ? null : line[..separator].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsTrue(Dictionary<string, string> scalars, string key)
|
||||||
|
{
|
||||||
|
return scalars.TryGetValue(key, out var value) &&
|
||||||
|
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseDouble(string raw, out double value)
|
||||||
|
{
|
||||||
|
return double.TryParse(raw.Trim('"', '\'', ' '),
|
||||||
|
NumberStyles.Float | NumberStyles.AllowThousands,
|
||||||
|
CultureInfo.InvariantCulture, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string CleanValue(string value)
|
||||||
|
{
|
||||||
|
value = value.Trim();
|
||||||
|
|
||||||
|
if (value.Length >= 2 && value[0] == '"' && value[^1] == '"')
|
||||||
|
{
|
||||||
|
value = value[1..^1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.StartsWith("[[", StringComparison.Ordinal) && value.EndsWith("]]", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
value = value[2..^2];
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
using Microsoft.Maui.Graphics;
|
||||||
|
using ZeroSpace.MapEditor.Models;
|
||||||
|
using ZeroSpace.MapEditor.Services;
|
||||||
|
using IImage = Microsoft.Maui.Graphics.IImage;
|
||||||
|
|
||||||
|
namespace ZeroSpace.MapEditor.Views;
|
||||||
|
|
||||||
|
/// <summary>Which part of the translation gizmo was hit or is active.</summary>
|
||||||
|
public enum GizmoPart
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Center,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draws the star map: connection lines between connected systems and
|
||||||
|
/// one circle per system. Names are intentionally not drawn on the
|
||||||
|
/// canvas; details live in the inspector.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MapDrawable : IDrawable
|
||||||
|
{
|
||||||
|
public const double NodeRadius = 14;
|
||||||
|
|
||||||
|
/// <summary>Screen-space length of each gizmo axis arm, in pixels.</summary>
|
||||||
|
public const double GizmoArmLength = 64;
|
||||||
|
|
||||||
|
public IList<MapNode> Nodes { get; set; } = Array.Empty<MapNode>();
|
||||||
|
|
||||||
|
public Func<string, MapNode?> FindNode { get; set; } = _ => null;
|
||||||
|
|
||||||
|
/// <summary>Screen pixels per world unit.</summary>
|
||||||
|
public double Scale { get; set; } = 1;
|
||||||
|
|
||||||
|
public double OffsetX { get; set; }
|
||||||
|
|
||||||
|
public double OffsetY { get; set; }
|
||||||
|
|
||||||
|
public RectF? SelectionBox { get; set; }
|
||||||
|
|
||||||
|
/// <summary>In-game galaxy map screenshot, drawn under the nodes.</summary>
|
||||||
|
public IImage? BackgroundImage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Whether the galaxy map background is shown.</summary>
|
||||||
|
public bool ShowBackground { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>Node the translation gizmo is drawn for (the active selection).</summary>
|
||||||
|
public MapNode? GizmoNode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gizmo part currently hovered or being dragged, for highlight.</summary>
|
||||||
|
public GizmoPart ActiveGizmoPart { get; set; } = GizmoPart.None;
|
||||||
|
|
||||||
|
public void Draw(ICanvas canvas, RectF dirtyRect)
|
||||||
|
{
|
||||||
|
canvas.FillColor = Color.FromArgb("#1e2430");
|
||||||
|
canvas.FillRectangle(dirtyRect);
|
||||||
|
|
||||||
|
DrawBackground(canvas);
|
||||||
|
|
||||||
|
PointF ToScreen(double worldX, double worldY) =>
|
||||||
|
new((float)(worldX * Scale + OffsetX), (float)(worldY * Scale + OffsetY));
|
||||||
|
|
||||||
|
// Connections first, so nodes draw on top of the lines.
|
||||||
|
canvas.StrokeColor = Color.FromArgb("#5b6b82");
|
||||||
|
canvas.StrokeSize = Math.Max(1, (float)(1.5 * Scale));
|
||||||
|
foreach (var node in Nodes)
|
||||||
|
{
|
||||||
|
var from = ToScreen(node.X, node.Y);
|
||||||
|
foreach (var name in node.Doc.ConnectedSystems)
|
||||||
|
{
|
||||||
|
var target = FindNode(name);
|
||||||
|
if (target is null || ReferenceEquals(target, node))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var to = ToScreen(target.X, target.Y);
|
||||||
|
canvas.DrawLine(from, to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var node in Nodes)
|
||||||
|
{
|
||||||
|
var center = ToScreen(node.X, node.Y);
|
||||||
|
var radius = (float)(NodeRadius * Scale);
|
||||||
|
|
||||||
|
canvas.FillColor = ColorForOwner(node.Doc.Owner);
|
||||||
|
canvas.FillCircle(center, radius);
|
||||||
|
|
||||||
|
if (node.IsHighlighted)
|
||||||
|
{
|
||||||
|
canvas.StrokeColor = Color.FromArgb("#FFD700");
|
||||||
|
canvas.StrokeSize = 3;
|
||||||
|
canvas.DrawCircle(center, radius + 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.IsSelected)
|
||||||
|
{
|
||||||
|
canvas.StrokeColor = Colors.White;
|
||||||
|
canvas.StrokeSize = 3;
|
||||||
|
canvas.DrawCircle(center, node.IsHighlighted ? radius + 8 : radius + 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SelectionBox is { Width: > 0, Height: > 0 } box)
|
||||||
|
{
|
||||||
|
canvas.FillColor = Color.FromArgb("#334a90d9");
|
||||||
|
canvas.FillRectangle(box);
|
||||||
|
canvas.StrokeColor = Color.FromArgb("#4a90d9");
|
||||||
|
canvas.StrokeSize = 1.5f;
|
||||||
|
canvas.DrawRectangle(box);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GizmoNode is not null)
|
||||||
|
{
|
||||||
|
DrawGizmo(canvas, GizmoNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draws the 2D translation gizmo at the node's screen position:
|
||||||
|
/// a red +X arm, a green +Y arm, and a center square for free XY moves.
|
||||||
|
/// Screen-space, so it stays the same size at any zoom level.
|
||||||
|
/// </summary>
|
||||||
|
private void DrawGizmo(ICanvas canvas, MapNode node)
|
||||||
|
{
|
||||||
|
var cx = (float)(node.X * Scale + OffsetX);
|
||||||
|
var cy = (float)(node.Y * Scale + OffsetY);
|
||||||
|
const float arm = (float)GizmoArmLength;
|
||||||
|
|
||||||
|
DrawGizmoArm(canvas, cx, cy, cx + arm, cy, isX: true,
|
||||||
|
active: ActiveGizmoPart == GizmoPart.XAxis);
|
||||||
|
DrawGizmoArm(canvas, cx, cy, cx, cy + arm, isX: false,
|
||||||
|
active: ActiveGizmoPart == GizmoPart.YAxis);
|
||||||
|
|
||||||
|
// Center handle: free XY move.
|
||||||
|
const float size = 12f;
|
||||||
|
var centerColor = ActiveGizmoPart == GizmoPart.Center
|
||||||
|
? Color.FromArgb("#fde047")
|
||||||
|
: Color.FromArgb("#eab308");
|
||||||
|
canvas.FillColor = Color.FromArgb("#111827");
|
||||||
|
canvas.FillRectangle(cx - size / 2 - 2, cy - size / 2 - 2, size + 4, size + 4);
|
||||||
|
canvas.FillColor = centerColor;
|
||||||
|
canvas.FillRectangle(cx - size / 2, cy - size / 2, size, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawGizmoArm(ICanvas canvas, float x1, float y1, float x2, float y2,
|
||||||
|
bool isX, bool active)
|
||||||
|
{
|
||||||
|
var color = isX
|
||||||
|
? (active ? Color.FromArgb("#f87171") : Color.FromArgb("#ef4444"))
|
||||||
|
: (active ? Color.FromArgb("#4ade80") : Color.FromArgb("#22c55e"));
|
||||||
|
|
||||||
|
// Dark underlay so the arm reads against the bright galaxy map.
|
||||||
|
canvas.StrokeColor = Color.FromArgb("#111827");
|
||||||
|
canvas.StrokeSize = 5;
|
||||||
|
canvas.DrawLine(x1, y1, x2, y2);
|
||||||
|
canvas.StrokeColor = color;
|
||||||
|
canvas.StrokeSize = 3;
|
||||||
|
canvas.DrawLine(x1, y1, x2, y2);
|
||||||
|
|
||||||
|
// Arrowhead pointing along the axis.
|
||||||
|
const float headLen = 12f;
|
||||||
|
const float headHalf = 6f;
|
||||||
|
PointF tip, a, b;
|
||||||
|
if (isX)
|
||||||
|
{
|
||||||
|
tip = new PointF(x2, y2);
|
||||||
|
a = new PointF(x2 - headLen, y2 - headHalf);
|
||||||
|
b = new PointF(x2 - headLen, y2 + headHalf);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tip = new PointF(x2, y2);
|
||||||
|
a = new PointF(x2 - headHalf, y2 - headLen);
|
||||||
|
b = new PointF(x2 + headHalf, y2 - headLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
var head = new PathF();
|
||||||
|
head.MoveTo(tip);
|
||||||
|
head.LineTo(a);
|
||||||
|
head.LineTo(b);
|
||||||
|
head.Close();
|
||||||
|
canvas.FillColor = color;
|
||||||
|
canvas.FillPath(head);
|
||||||
|
canvas.StrokeColor = Color.FromArgb("#111827");
|
||||||
|
canvas.StrokeSize = 1.5f;
|
||||||
|
canvas.DrawPath(head);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draws the galaxy map screenshot fitted inside the world square,
|
||||||
|
/// preserving aspect ratio so node world coordinates line up exactly
|
||||||
|
/// with the map underneath.
|
||||||
|
/// </summary>
|
||||||
|
private void DrawBackground(ICanvas canvas)
|
||||||
|
{
|
||||||
|
if (!ShowBackground || BackgroundImage is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageWidth = BackgroundImage.Width;
|
||||||
|
var imageHeight = BackgroundImage.Height;
|
||||||
|
if (imageWidth <= 0 || imageHeight <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double world = StarSystemDocStore.WorldSize;
|
||||||
|
var fit = Math.Min(world / imageWidth, world / imageHeight);
|
||||||
|
var drawWidth = imageWidth * fit;
|
||||||
|
var drawHeight = imageHeight * fit;
|
||||||
|
var originX = (world - drawWidth) / 2;
|
||||||
|
var originY = (world - drawHeight) / 2;
|
||||||
|
|
||||||
|
canvas.DrawImage(BackgroundImage,
|
||||||
|
(float)(originX * Scale + OffsetX),
|
||||||
|
(float)(originY * Scale + OffsetY),
|
||||||
|
(float)(drawWidth * Scale),
|
||||||
|
(float)(drawHeight * Scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Color ColorForOwner(string owner) => owner switch
|
||||||
|
{
|
||||||
|
"The Eden Accord" => Color.FromArgb("#10b981"),
|
||||||
|
"The Keeper Council" => Color.FromArgb("#38bdf8"),
|
||||||
|
"The Obsidian Order" => Color.FromArgb("#ef4444"),
|
||||||
|
_ => Color.FromArgb("#9aa5b1"),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Color ColorForCategory(string category) => category switch
|
||||||
|
{
|
||||||
|
"Homeworld System" or "Homeworld" => Color.FromArgb("#43a047"),
|
||||||
|
"Core System" or "Core" => Color.FromArgb("#fb8c00"),
|
||||||
|
"Side System" or "Side" => Color.FromArgb("#4a90d9"),
|
||||||
|
_ => Color.FromArgb("#9aa5b1"),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="ZeroSpace.MapEditor.Views.MapEditorPage"
|
||||||
|
Title="ZeroSpace Map Editor">
|
||||||
|
<Grid RowDefinitions="Auto,*" ColumnDefinitions="*,340">
|
||||||
|
<ScrollView Grid.Row="0" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalScrollBarVisibility="Never">
|
||||||
|
<HorizontalStackLayout Padding="8" Spacing="8" VerticalOptions="Center">
|
||||||
|
<Button Text="Open Folder" Clicked="OnOpenFolderClicked" />
|
||||||
|
<Button x:Name="SaveButton" Text="Saved" Clicked="OnSaveClicked" IsEnabled="False" />
|
||||||
|
<Button Text="+" WidthRequest="44" Clicked="OnZoomInClicked" />
|
||||||
|
<Button Text="−" WidthRequest="44" Clicked="OnZoomOutClicked" />
|
||||||
|
<Button Text="Reset View" Clicked="OnResetViewClicked" />
|
||||||
|
<Button x:Name="PanModeButton" Text="Pan: Off" Clicked="OnPanModeClicked" />
|
||||||
|
<CheckBox x:Name="BackgroundCheckBox" IsChecked="True" CheckedChanged="OnBackgroundToggled" VerticalOptions="Center" />
|
||||||
|
<Label Text="Galaxy map" VerticalOptions="Center" />
|
||||||
|
<BoxView WidthRequest="1" HeightRequest="24" Color="#334155" VerticalOptions="Center" Margin="4,0" />
|
||||||
|
<Label Text="Faction:" VerticalOptions="Center" FontAttributes="Bold" />
|
||||||
|
<Button x:Name="EdenFactionButton" Text="[A] Eden Accord" Clicked="OnEdenFactionClicked" />
|
||||||
|
<Button x:Name="KeeperFactionButton" Text="[S] Keeper Council" Clicked="OnKeeperFactionClicked" />
|
||||||
|
<Button x:Name="ObsidianFactionButton" Text="[D] Obsidian Order" Clicked="OnObsidianFactionClicked" />
|
||||||
|
<Button x:Name="NoneFactionButton" Text="None" Clicked="OnNoneFactionClicked" />
|
||||||
|
<Border x:Name="ActiveFactionBadge"
|
||||||
|
StrokeShape="RoundRectangle 4"
|
||||||
|
StrokeThickness="1"
|
||||||
|
Padding="6,2"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
IsVisible="False">
|
||||||
|
<Label x:Name="ActiveFactionLabel"
|
||||||
|
FontSize="12"
|
||||||
|
FontAttributes="Bold" />
|
||||||
|
</Border>
|
||||||
|
<BoxView WidthRequest="1" HeightRequest="24" Color="#334155" VerticalOptions="Center" Margin="4,0" />
|
||||||
|
<SearchBar x:Name="SearchControl"
|
||||||
|
Placeholder="Search systems..."
|
||||||
|
WidthRequest="220"
|
||||||
|
TextChanged="OnSearchTextChanged"
|
||||||
|
SearchButtonPressed="OnSearchButtonPressed" />
|
||||||
|
<Label x:Name="StatusLabel"
|
||||||
|
Text="No folder loaded."
|
||||||
|
VerticalOptions="Center"
|
||||||
|
Margin="8,0,0,0" />
|
||||||
|
</HorizontalStackLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<GraphicsView Grid.Row="1" Grid.Column="0" x:Name="MapView">
|
||||||
|
<GraphicsView.GestureRecognizers>
|
||||||
|
<PointerGestureRecognizer PointerPressed="OnPointerPressed"
|
||||||
|
PointerMoved="OnPointerMoved"
|
||||||
|
PointerReleased="OnPointerReleased" />
|
||||||
|
</GraphicsView.GestureRecognizers>
|
||||||
|
</GraphicsView>
|
||||||
|
|
||||||
|
<ScrollView Grid.Row="1" Grid.Column="1" x:Name="InspectorScroll" IsVisible="False">
|
||||||
|
<VerticalStackLayout Padding="12" Spacing="6">
|
||||||
|
<Label Text="Inspector" FontSize="18" FontAttributes="Bold" />
|
||||||
|
|
||||||
|
<Label Text="Name" FontAttributes="Bold" />
|
||||||
|
<Label x:Name="NameLabel" />
|
||||||
|
|
||||||
|
<Label Text="Category" FontAttributes="Bold" />
|
||||||
|
<Label x:Name="CategoryLabel" />
|
||||||
|
|
||||||
|
<Label Text="Owner" FontAttributes="Bold" />
|
||||||
|
<Border x:Name="OwnerBadge"
|
||||||
|
StrokeShape="RoundRectangle 6"
|
||||||
|
StrokeThickness="1"
|
||||||
|
Padding="8,4"
|
||||||
|
HorizontalOptions="Start"
|
||||||
|
BackgroundColor="#1e293b"
|
||||||
|
Stroke="#334155">
|
||||||
|
<Label x:Name="OwnerLabel" Text="—" FontAttributes="Bold" TextColor="#94a3b8" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Label Text="Yields" FontAttributes="Bold" />
|
||||||
|
<Label x:Name="YieldsLabel" />
|
||||||
|
|
||||||
|
<Label Text="Occupation buff" FontAttributes="Bold" />
|
||||||
|
<Label x:Name="BuffLabel" />
|
||||||
|
|
||||||
|
<Label Text="System mastery" FontAttributes="Bold" />
|
||||||
|
<Label x:Name="MasteryLabel" />
|
||||||
|
|
||||||
|
<Label Text="X" FontAttributes="Bold" />
|
||||||
|
<Entry x:Name="XEntry" Keyboard="Numeric" TextChanged="OnPositionTextChanged" />
|
||||||
|
|
||||||
|
<Label Text="Y" FontAttributes="Bold" />
|
||||||
|
<Entry x:Name="YEntry" Keyboard="Numeric" TextChanged="OnPositionTextChanged" />
|
||||||
|
|
||||||
|
<Label Text="Connected systems" FontAttributes="Bold" />
|
||||||
|
<CollectionView x:Name="ConnectionsView"
|
||||||
|
SelectionMode="Single"
|
||||||
|
SelectionChanged="OnConnectionSelectionChanged">
|
||||||
|
<CollectionView.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Label Text="{Binding}"
|
||||||
|
Padding="4,4"
|
||||||
|
TextColor="#4a90d9"
|
||||||
|
TextDecorations="Underline" />
|
||||||
|
</DataTemplate>
|
||||||
|
</CollectionView.ItemTemplate>
|
||||||
|
</CollectionView>
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</Grid>
|
||||||
|
</ContentPage>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">net10.0-windows10.0.19041.0</TargetFrameworks>
|
||||||
|
<TargetFrameworks Condition="!$([MSBuild]::IsOSPlatform('windows'))">net10.0-maccatalyst</TargetFrameworks>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<RootNamespace>ZeroSpace.MapEditor</RootNamespace>
|
||||||
|
<UseMaui>true</UseMaui>
|
||||||
|
<SingleProject>true</SingleProject>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<ApplicationTitle>ZeroSpace Map Editor</ApplicationTitle>
|
||||||
|
<ApplicationId>ca.jonathanmccaffrey.zerospace.mapeditor</ApplicationId>
|
||||||
|
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||||
|
<ApplicationVersion>1</ApplicationVersion>
|
||||||
|
|
||||||
|
<!-- Unpackaged Windows desktop execution -->
|
||||||
|
<WindowsPackageType>None</WindowsPackageType>
|
||||||
|
|
||||||
|
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
|
||||||
|
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ZeroSpace.Model\ZeroSpace.Model.csproj" />
|
||||||
|
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.110" />
|
||||||
|
<PackageReference Include="CommunityToolkit.Maui.Core" Version="15.0.1" />
|
||||||
|
<!-- In-game galaxy map screenshot shown as the canvas background. -->
|
||||||
|
<MauiAsset Include="Resources\Raw\galaxymap.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</MauiAsset>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
namespace ZeroSpace.Model;
|
||||||
|
|
||||||
|
public enum StarSystemCategory
|
||||||
|
{
|
||||||
|
HomeworldSystem,
|
||||||
|
CoreSystem,
|
||||||
|
SideSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class StarSystemCategoryExtensions
|
||||||
|
{
|
||||||
|
public static string ToDisplayName(this StarSystemCategory category) => category switch
|
||||||
|
{
|
||||||
|
StarSystemCategory.HomeworldSystem => "Homeworld System",
|
||||||
|
StarSystemCategory.CoreSystem => "Core System",
|
||||||
|
StarSystemCategory.SideSystem => "Side System",
|
||||||
|
_ => category.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class StarSystemOwners
|
||||||
|
{
|
||||||
|
public const string EdenAccord = "The Eden Accord";
|
||||||
|
public const string KeeperCouncil = "The Keeper Council";
|
||||||
|
public const string ObsidianOrder = "The Obsidian Order";
|
||||||
|
|
||||||
|
public static readonly IReadOnlyList<string> All =
|
||||||
|
[
|
||||||
|
EdenAccord,
|
||||||
|
KeeperCouncil,
|
||||||
|
ObsidianOrder
|
||||||
|
];
|
||||||
|
|
||||||
|
public static string GetColor(string? owner) => owner switch
|
||||||
|
{
|
||||||
|
EdenAccord => "#10b981", // Green
|
||||||
|
KeeperCouncil => "#38bdf8", // Blue
|
||||||
|
ObsidianOrder => "#ef4444", // Red
|
||||||
|
_ => "#94a3b8"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string GetCssClass(string? owner) => owner switch
|
||||||
|
{
|
||||||
|
EdenAccord => "owner-eden",
|
||||||
|
KeeperCouncil => "owner-keeper",
|
||||||
|
ObsidianOrder => "owner-obsidian",
|
||||||
|
_ => "owner-neutral"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record StarSystemData(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
StarSystemCategory SystemCategory,
|
||||||
|
string Owner,
|
||||||
|
bool HigherInfluenceYield,
|
||||||
|
bool HigherCreditYield,
|
||||||
|
bool HigherEnergyYield,
|
||||||
|
bool HigherChanceForItems,
|
||||||
|
string? OccupationBuff,
|
||||||
|
string? SystemMastery,
|
||||||
|
IReadOnlyList<string> ConnectedSystems,
|
||||||
|
double X,
|
||||||
|
double Y,
|
||||||
|
string Body);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,138 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
namespace ZeroSpace.Model;
|
||||||
|
|
||||||
|
public static class SystemMasteries
|
||||||
|
{
|
||||||
|
public static readonly SystemMasteryData BuildingHealthAndShields = new(
|
||||||
|
Id: "BuildingHealthAndShields",
|
||||||
|
Name: "Building Health and Shields",
|
||||||
|
Levels: ["+2% building health and shields", "+2% building health and shields", "+2% building health and shields", "+2% building health and shields", "+5% building health and shields"],
|
||||||
|
AvailableSystems: ["Stronghold Orion", "Magenta Bloom", "Obsidian Palace", "Raider's Reef", "Nexus Prime", "Erebus Deep", "Celestial Forge Stronghold", "Aquila Rift"],
|
||||||
|
Stats: new Dictionary<string, double>
|
||||||
|
{
|
||||||
|
["buildingHealthAndShields"] = 0.13,
|
||||||
|
},
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly SystemMasteryData HeroDamage = new(
|
||||||
|
Id: "HeroDamage",
|
||||||
|
Name: "Hero Damage",
|
||||||
|
Levels: ["+3% hero damage", "+3% hero damage", "+3% hero damage", "+3% hero damage", "-5% cooldown on heroes abilities"],
|
||||||
|
AvailableSystems: ["Stronghold Atlas", "Hyperion Nebula", "Stronghold Leo", "Tartarus Expanse", "Shattered Light", "Lambda Gateway", "Devil's Trident"],
|
||||||
|
Stats: new Dictionary<string, double>
|
||||||
|
{
|
||||||
|
["heroDamage"] = 0.12,
|
||||||
|
["heroCooldown"] = 0.05,
|
||||||
|
},
|
||||||
|
Body: "I don't like micromanaging heroes, so I wouldn't put too much stock in this skill.\n\nThat said, it's hard to ignore the whole hero cooldown reduction, and stacking it.\n\n[[Vynthra]]'s cooldown on the AOE generates top bar energy, so the more you cast it the better. \n\n[[Galavax]] can convert enemies, so it's a instant kill that can give you a big unit.\n\nAnd [[Prefect Aster]] has his +40% attack speed buff to allies in his area. So all clear powerful abilities you want to cast often.");
|
||||||
|
|
||||||
|
public static readonly SystemMasteryData HeroHealthAndShields = new(
|
||||||
|
Id: "HeroHealthAndShields",
|
||||||
|
Name: "Hero Health and Shields",
|
||||||
|
Levels: ["+3% hero health and shields", "+3% hero health and shields", "+3% hero health and shields", "+3% hero health and shields", "+4 to heroes health and shields regeneration rate"],
|
||||||
|
AvailableSystems: ["Yellow Dawn", "Gelid Storm", "Occam's Abode", "Scarlett Ire", "Negative Ion", "The Crucible", "Idal's Word", "Crimson Expanse"],
|
||||||
|
Stats: new Dictionary<string, double>
|
||||||
|
{
|
||||||
|
["heroHealthAndShields"] = 0.12,
|
||||||
|
["heroHealthAndShieldRegen"] = 4,
|
||||||
|
},
|
||||||
|
Body: "Seems pointless. Heroes currently are all ranged, so while they are good tanks in terms of durability, it's a bit micro intensive to actually get them to tank. Maybe when a melee hero gets released, this would be worth stacking as a easy health and shield tanking hero.");
|
||||||
|
|
||||||
|
public static readonly SystemMasteryData MaxSupply = new(
|
||||||
|
Id: "MaxSupply",
|
||||||
|
Name: "Max Supply",
|
||||||
|
Levels: ["+2 max supply", "+2 max supply", "+2 max supply", "+2 max supply", "+5 starting and max supply"],
|
||||||
|
AvailableSystems: ["Aurora Vale", "Corundum Dream", "Draconis Reach", "Stellar Forge", "Magnetic Folly", "Mercury Forge", "Wild's End"],
|
||||||
|
Stats: new Dictionary<string, double>
|
||||||
|
{
|
||||||
|
["startingSupply"] = 5,
|
||||||
|
["maxSupply"] = 13,
|
||||||
|
},
|
||||||
|
Body: "Gives +13 max supply and +5 starting supply.\n\nNot an obvious punch up in power. \n\nMakes initial time to make a bigger army cheaper, given an extra five supply of units. \n\nAnd stacking these buffs is kind of an obvious end game bonus.\n\nBoth [[Prefect Aster]] and [[Galavax]] can hit max supply pretty easily given there powers create units that take up supply, so could be beneficial for armies that need large groups to split up to hold objectives.\n\nPerhaps an uneducated opinion, but I would skip this if focusing on [[Vynthra]] unless a good neutral power comes out that takes supply. Because your not reasonably hitting supply cap in harder difficulties in solo co-op.");
|
||||||
|
|
||||||
|
public static readonly SystemMasteryData ProductionSpeedForBuildings = new(
|
||||||
|
Id: "ProductionSpeedForBuildings",
|
||||||
|
Name: "Production Speed for Buildings",
|
||||||
|
Levels: ["+1% Production speed for buildings", "+1% Production speed for buildings", "+1% Production speed for buildings", "+1% Production speed for buildings", "+5% Production speed for buildings"],
|
||||||
|
AvailableSystems: ["Endless Wastes", "Sullen Dwarf", "Starless Sky", "Steel Depths", "Glittering Hope", "Ebony Keep", "Stronghold Cronus", "Iotan Tears"],
|
||||||
|
Stats: new Dictionary<string, double>
|
||||||
|
{
|
||||||
|
["productionSpeedForBuildings"] = 0.09,
|
||||||
|
},
|
||||||
|
Body: "9% production speed. Seems obvious for any tech up strategy.\n\nMight be beneficial towards getting a certain mercenary tier unlocked very quickly.\n\nI would be interested in using [[Vynthra]] and quickly teching up to [[Corsair Snipers]] and infusing them. Probably also better ideas in this regard.");
|
||||||
|
|
||||||
|
public static readonly SystemMasteryData StartingTopbarEnergy = new(
|
||||||
|
Id: "StartingTopbarEnergy",
|
||||||
|
Name: "Starting Topbar Energy",
|
||||||
|
Levels: ["+1 Topbar starting energy", "+1 Topbar starting energy", "+1 Topbar starting energy", "+1 Topbar starting energy", "+2 Topbar starting energy and +5 Topbar maximum energy"],
|
||||||
|
AvailableSystems: ["Stronghold Nova Centauri", "Lights Bounty", "Astral Haven", "The High Council", "Uanachian Whirlpool", "Vulcan's Breath", "Xenon Abyss", "Calm Belt", "Nucleic Redux"],
|
||||||
|
Stats: new Dictionary<string, double>
|
||||||
|
{
|
||||||
|
["startingTopbarEnergy"] = 6,
|
||||||
|
["maxTopbarEnergy"] = 5,
|
||||||
|
},
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly SystemMasteryData TurretDamage = new(
|
||||||
|
Id: "TurretDamage",
|
||||||
|
Name: "Turret Damage",
|
||||||
|
Levels: ["+5% turret damage.", "+5% turret damage.", "+5% turret damage.", "+5% turret damage.", "+10% turret attack speed."],
|
||||||
|
AvailableSystems: ["Solaris Reach", "Xenthic Fields", "Stronghold Vega", "Stronghold Tau", "Inferno's Edge Stronghold", "Titan's Fall", "Nebulon Drift"],
|
||||||
|
Stats: new Dictionary<string, double>
|
||||||
|
{
|
||||||
|
["turretDamage"] = 0.2,
|
||||||
|
["turretAttackSpeed"] = 0.1,
|
||||||
|
},
|
||||||
|
Body: "This just seems crazy good on [[Vynthra]].\n\nIf you want go go base defense crazy, I don't see how this is not prioritized.");
|
||||||
|
|
||||||
|
public static readonly SystemMasteryData UnitEnergyAndRegenerationNotHeroes = new(
|
||||||
|
Id: "UnitEnergyAndRegenerationNotHeroes",
|
||||||
|
Name: "Unit Energy and Regeneration (not heroes)",
|
||||||
|
Levels: ["+5% unit max energy and regeneration", "-3% unit ability cooldown", "+5% unit max energy and regeneration", "-3% unit ability cooldown", "+10% unit max energy and regeneration, -5% unit ability cooldown"],
|
||||||
|
AvailableSystems: ["Eden's Heart", "Crystallic Genesis", "Pleiades Cluster", "Ivory Fields", "Heigen Stream", "Gamma Swarm", "Helios Crossing", "Epsilon Arc", "Orion's Gate"],
|
||||||
|
Stats: new Dictionary<string, double>
|
||||||
|
{
|
||||||
|
},
|
||||||
|
Body: "I don't feel like there are good enough abilities that I would want to use this. I am sure this will rapidly change as more units get added to the roster. Perhaps that caster unit with [[Galavax]] is good.\nSome of the hotkeys on mercs are kind of bad, which is understandable given the sheer amount of variety. Perhaps I am also downplaying some of them, and tweaking the hotkeys will make them more friendly.\n\nI would ignore this for now, unless there is a very powerful caster unit added to the game. Like anything with summons via energy spending and cooldowns.");
|
||||||
|
|
||||||
|
public static readonly SystemMasteryData UnitHealthAndShields = new(
|
||||||
|
Id: "UnitHealthAndShields",
|
||||||
|
Name: "Unit Health and Shields",
|
||||||
|
Levels: ["+1% Unit health and shields", "+1% Unit health and shields", "+1% Unit health and shields", "+1% Unit health and shields", "+1 unit health and shields regeneration rate"],
|
||||||
|
AvailableSystems: ["Ashes of Revenge", "Andus Reach", "Forge of Souls", "Typhon's Call", "Neera's Gift", "Ivory Steel", "Void Star", "Burning Cairn"],
|
||||||
|
Stats: new Dictionary<string, double>
|
||||||
|
{
|
||||||
|
["unitHealthAndShields"] = 0.04,
|
||||||
|
["unitHealthAndShieldRegen"] = 1,
|
||||||
|
},
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static readonly SystemMasteryData UnitVisionAndAttackRange = new(
|
||||||
|
Id: "UnitVisionAndAttackRange",
|
||||||
|
Name: "Unit Vision and Attack Range",
|
||||||
|
Levels: ["+0.5 unit vision range", "+0.5 unit vision range", "+0.5 unit vision range", "+0.5 unit vision range", "+0.1 unit attack range"],
|
||||||
|
AvailableSystems: ["Quartzalian Berth", "Igneous Cascade", "Noxious Energy", "Artemis Grotto", "Leviathan's Wake", "Galactic Zenith", "Sigma Vortex", "Zeta Horizon"],
|
||||||
|
Stats: new Dictionary<string, double>
|
||||||
|
{
|
||||||
|
["unitVisionRange"] = 2,
|
||||||
|
["unitAttackRange"] = 0.1,
|
||||||
|
},
|
||||||
|
Body: "");
|
||||||
|
|
||||||
|
public static IReadOnlyList<SystemMasteryData> All { get; } =
|
||||||
|
[
|
||||||
|
BuildingHealthAndShields,
|
||||||
|
HeroDamage,
|
||||||
|
HeroHealthAndShields,
|
||||||
|
MaxSupply,
|
||||||
|
ProductionSpeedForBuildings,
|
||||||
|
StartingTopbarEnergy,
|
||||||
|
TurretDamage,
|
||||||
|
UnitEnergyAndRegenerationNotHeroes,
|
||||||
|
UnitHealthAndShields,
|
||||||
|
UnitVisionAndAttackRange,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static IReadOnlyDictionary<string, SystemMasteryData> ByName { get; } =
|
||||||
|
All.ToDictionary(static mastery => mastery.Name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ZeroSpace.Model;
|
||||||
|
|
||||||
|
public sealed record SystemMasteryData(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
IReadOnlyList<string> Levels,
|
||||||
|
IReadOnlyList<string> AvailableSystems,
|
||||||
|
IReadOnlyDictionary<string, double> Stats,
|
||||||
|
string Body);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
||||||
<Found Context="routeData">
|
<Found Context="routeData">
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
|
<ReleaseGuard RouteData="@routeData"/>
|
||||||
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
|
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
|
||||||
</Found>
|
</Found>
|
||||||
</Router>
|
</Router>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@using Markdig
|
||||||
|
@using ZeroSpace.Web.Services
|
||||||
|
@((MarkupString)Markdig.Markdown.ToHtml(ZeroSpaceLinks.ResolveWikiLinks(Markdown), Pipeline, null))
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder().Build();
|
||||||
|
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public string Markdown { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
|
@* Route guard: unreleased pages (see SiteMap) bounce to the galaxy map. *@
|
||||||
|
@if (_released)
|
||||||
|
{
|
||||||
|
<RouteView RouteData="@RouteData" DefaultLayout="@typeof(MainLayout)"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public RouteData RouteData { get; set; } = default!;
|
||||||
|
|
||||||
|
private bool _released;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
_released = SiteMap.IsReleased(RouteData.PageType);
|
||||||
|
if (!_released)
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo("/galaxy", replace: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
@using Telerik.Blazor.Components
|
@using Telerik.Blazor.Components
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="sidebar">
|
<div class="sidebar @(_isNavCollapsed ? "collapsed" : "")">
|
||||||
<NavMenu/>
|
<NavMenu IsCollapsed="_isNavCollapsed" IsCollapsedChanged="OnNavCollapsedChanged" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
@@ -13,3 +14,12 @@
|
|||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private bool _isNavCollapsed = false;
|
||||||
|
|
||||||
|
private void OnNavCollapsedChanged(bool isCollapsed)
|
||||||
|
{
|
||||||
|
_isNavCollapsed = isCollapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,21 +2,32 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #080c14;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 15% 20%, rgba(56, 189, 248, 0.04) 0%, transparent 40%),
|
||||||
|
radial-gradient(circle at 85% 80%, rgba(16, 185, 129, 0.03) 0%, transparent 40%);
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
background-image: linear-gradient(180deg, #0d1117 0%, #161b22 70%);
|
background-color: #0b0f19;
|
||||||
|
background-image: linear-gradient(180deg, #0d131f 0%, #080c14 100%);
|
||||||
|
border-right: 1px solid rgba(56, 189, 248, 0.12);
|
||||||
|
box-shadow: 2px 0 16px rgba(0, 0, 0, 0.35);
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-row {
|
.top-row {
|
||||||
background-color: #f7f7f7;
|
background-color: rgba(13, 19, 31, 0.8);
|
||||||
border-bottom: 1px solid #d6d5d5;
|
backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 1px solid rgba(56, 189, 248, 0.15);
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
height: 3.5rem;
|
height: 3.75rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -25,10 +36,12 @@ main {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin-left: 1.5rem;
|
margin-left: 1.5rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
color: #38bdf8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
color: #7dd3fc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-row ::deep a:first-child {
|
.top-row ::deep a:first-child {
|
||||||
@@ -56,6 +69,12 @@ main {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed {
|
||||||
|
width: 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-row {
|
.top-row {
|
||||||
@@ -70,8 +89,10 @@ main {
|
|||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-row, article {
|
article {
|
||||||
padding-left: 2rem !important;
|
padding-left: 2rem !important;
|
||||||
padding-right: 1.5rem !important;
|
padding-right: 2rem !important;
|
||||||
|
padding-top: 1.5rem !important;
|
||||||
|
padding-bottom: 2.5rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,110 @@
|
|||||||
<div class="top-row ps-3 navbar navbar-dark">
|
<div class="nav-wrapper d-flex flex-column h-100 @(IsCollapsed ? "is-collapsed" : "")">
|
||||||
<div class="container-fluid">
|
<div class="top-row navbar navbar-dark d-flex align-items-center @(IsCollapsed ? "justify-content-center px-1" : "justify-content-between px-3")">
|
||||||
<a class="navbar-brand" href="">ZeroSpace</a>
|
@if (!IsCollapsed)
|
||||||
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
{
|
||||||
<span class="navbar-toggler-icon"></span>
|
<a class="navbar-brand d-flex align-items-center me-0" href="galaxy" title="ZeroSpace Database & Command Hub">
|
||||||
</button>
|
<span class="brand-glow me-2">✦</span>
|
||||||
</div>
|
<span class="brand-text">ZeroSpace</span>
|
||||||
</div>
|
</a>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<button type="button" class="btn btn-sm btn-nav-toggle nav-toggle-btn"
|
||||||
|
@onclick="ToggleCollapse"
|
||||||
|
title="Collapse navigation"
|
||||||
|
aria-label="Collapse navigation">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5M10 8a.5.5 0 0 1-.5.5H4.207l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L4.207 7.5H9.5a.5.5 0 0 1 .5.5"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleMobileMenu">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="d-flex align-items-center justify-content-center gap-1 w-100">
|
||||||
|
<a class="navbar-brand d-flex align-items-center m-0 p-0" href="galaxy" title="ZeroSpace Database & Command Hub">
|
||||||
|
<span class="brand-glow">✦</span>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-sm btn-nav-toggle nav-toggle-btn"
|
||||||
|
@onclick="ToggleCollapse"
|
||||||
|
title="Expand navigation"
|
||||||
|
aria-label="Expand navigation">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M3.5 15a.5.5 0 0 0 .5-.5v-13a.5.5 0 0 0-1 0v13a.5.5 0 0 0 .5.5M6 8a.5.5 0 0 0 .5.5h5.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L12.293 7.5H6.5A.5.5 0 0 0 6 8"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
|
<div class="@NavMenuCssClass nav-scrollable flex-grow-1" @onclick="OnNavMenuClick">
|
||||||
<nav class="nav flex-column">
|
<nav class="nav flex-column">
|
||||||
<div class="nav-item px-3">
|
@foreach (var entry in SiteMap.NavEntries)
|
||||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
{
|
||||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
<div class="nav-item @(IsCollapsed ? "px-1" : "px-3")">
|
||||||
</NavLink>
|
<NavLink class="nav-link" href="@entry.Href" Match="@entry.Match" title="@entry.Title">
|
||||||
|
<span class="@entry.IconCss" aria-hidden="true"></span>
|
||||||
|
@if (!IsCollapsed)
|
||||||
|
{
|
||||||
|
<span class="nav-text">@entry.Title</span>
|
||||||
|
@if (entry.Badge is not null)
|
||||||
|
{
|
||||||
|
<span class="badge badge-nav-live ms-auto">@entry.Badge</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item px-3">
|
|
||||||
<NavLink class="nav-link" href="units">
|
<div class="sidebar-footer p-2 nav-toggle-btn">
|
||||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Units
|
<button type="button" class="btn btn-nav-toggle-footer w-100 d-flex align-items-center justify-content-center"
|
||||||
</NavLink>
|
@onclick="ToggleCollapse"
|
||||||
|
title="@(IsCollapsed ? "Expand sidebar" : "Collapse sidebar")"
|
||||||
|
aria-label="@(IsCollapsed ? "Expand sidebar" : "Collapse sidebar")">
|
||||||
|
@if (IsCollapsed)
|
||||||
|
{
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M3.5 15a.5.5 0 0 0 .5-.5v-13a.5.5 0 0 0-1 0v13a.5.5 0 0 0 .5.5M6 8a.5.5 0 0 0 .5.5h5.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L12.293 7.5H6.5A.5.5 0 0 0 6 8"/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" class="me-2">
|
||||||
|
<path fill-rule="evenodd" d="M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5M10 8a.5.5 0 0 1-.5.5H4.207l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L4.207 7.5H9.5a.5.5 0 0 1 .5.5"/>
|
||||||
|
</svg>
|
||||||
|
<span class="fs-7 text-uppercase fw-semibold">Collapse</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private bool collapseNavMenu = true;
|
[Parameter]
|
||||||
|
public bool IsCollapsed { get; set; } = false;
|
||||||
|
|
||||||
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
[Parameter]
|
||||||
|
public EventCallback<bool> IsCollapsedChanged { get; set; }
|
||||||
|
|
||||||
private void ToggleNavMenu()
|
private bool _collapseMobileNav = true;
|
||||||
|
|
||||||
|
private string? NavMenuCssClass => _collapseMobileNav ? "collapse" : null;
|
||||||
|
|
||||||
|
private async Task ToggleCollapse()
|
||||||
{
|
{
|
||||||
collapseNavMenu = !collapseNavMenu;
|
IsCollapsed = !IsCollapsed;
|
||||||
|
await IsCollapsedChanged.InvokeAsync(IsCollapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ToggleMobileMenu()
|
||||||
|
{
|
||||||
|
_collapseMobileNav = !_collapseMobileNav;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNavMenuClick()
|
||||||
|
{
|
||||||
|
_collapseMobileNav = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,44 @@
|
|||||||
.navbar-toggler {
|
.navbar-toggler {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(56, 189, 248, 0.1);
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-row {
|
.top-row {
|
||||||
min-height: 3.5rem;
|
min-height: 3.75rem;
|
||||||
background-color: rgba(0,0,0,0.4);
|
background: linear-gradient(180deg, rgba(13, 19, 31, 0.95) 0%, rgba(9, 13, 22, 0.95) 100%);
|
||||||
|
border-bottom: 1px solid rgba(56, 189, 248, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
font-size: 1.1rem;
|
font-size: 1.15rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #f0f6fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-glow {
|
||||||
|
color: #38bdf8;
|
||||||
|
text-shadow: 0 0 10px rgba(56, 189, 248, 0.8);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
background: linear-gradient(135deg, #ffffff 30%, #38bdf8 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-nav-live {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
padding: 0.2rem 0.45rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(56, 189, 248, 0.2);
|
||||||
|
color: #38bdf8;
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.4);
|
||||||
|
box-shadow: 0 0 6px rgba(56, 189, 248, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bi {
|
.bi {
|
||||||
@@ -18,24 +48,34 @@
|
|||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
margin-right: 0.75rem;
|
margin-right: 0.75rem;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
background-size: cover;
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bi-house-door-fill-nav-menu {
|
.bi-house-door-fill-nav-menu {
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%2394a3b8' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||||
}
|
}
|
||||||
|
|
||||||
.bi-plus-square-fill-nav-menu {
|
.bi-galaxy-nav-menu {
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%2338bdf8' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='2.5' fill='%2338bdf8'/%3E%3Ccircle cx='2.5' cy='4' r='1.5' fill='%2338bdf8'/%3E%3Ccircle cx='13.5' cy='5' r='1.5' fill='%2338bdf8'/%3E%3Ccircle cx='4' cy='13' r='1.5' fill='%2338bdf8'/%3E%3Ccircle cx='12' cy='12' r='1.5' fill='%2338bdf8'/%3E%3Cpath stroke='%2338bdf8' stroke-width='1.2' stroke-linecap='round' d='M2.5 4L8 8m0 0l5.5-3M8 8l-4 5m4-5l4 4' opacity='.65'/%3E%3C/svg%3E");
|
||||||
}
|
}
|
||||||
|
|
||||||
.bi-list-nested-nav-menu {
|
.bi-star-systems-nav-menu {
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23f59e0b' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3.5' fill='%23f59e0b'/%3E%3Cpath stroke='%23f59e0b' stroke-width='1.2' stroke-linecap='round' d='M8 1v2.5M8 12.5V15M1 8h2.5M12.5 8H15M3 3l1.8 1.8M11.2 11.2l1.8 1.8M3 13l1.8-1.8M11.2 4.8l1.8-1.8' opacity='.75'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-masteries-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23a78bfa' viewBox='0 0 16 16'%3E%3Cpath d='M8 0.5l2.2 4.6 5 .7-3.6 3.6.9 5-4.5-2.4-4.5 2.4.9-5-3.6-3.6 5-.7L8 0.5z' fill='%23a78bfa'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-units-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%2310b981' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6' stroke='%2310b981' stroke-width='1.3' fill='none'/%3E%3Ccircle cx='8' cy='8' r='2' fill='%2310b981'/%3E%3Cpath stroke='%2310b981' stroke-width='1.3' d='M8 0v4M8 12v4M0 8h4M12 8h4'/%3E%3C/svg%3E");
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
font-size: 0.9rem;
|
font-size: 0.92rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item:first-of-type {
|
.nav-item:first-of-type {
|
||||||
@@ -47,37 +87,125 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-item ::deep a {
|
.nav-item ::deep a {
|
||||||
color: #d7d7d7;
|
color: #94a3b8;
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
height: 3rem;
|
height: 2.75rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 3rem;
|
line-height: 2.75rem;
|
||||||
|
padding: 0 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item ::deep a.active {
|
.nav-item ::deep a.active {
|
||||||
background-color: rgba(88, 166, 255, 0.2);
|
background: linear-gradient(90deg, rgba(56, 189, 248, 0.15) 0%, rgba(56, 189, 248, 0.05) 100%);
|
||||||
color: #58a6ff;
|
color: #38bdf8;
|
||||||
|
border-color: rgba(56, 189, 248, 0.35);
|
||||||
|
box-shadow: inset 0 0 12px rgba(56, 189, 248, 0.12), 0 0 8px rgba(56, 189, 248, 0.1);
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item ::deep a:hover {
|
.nav-item ::deep a:hover:not(.active) {
|
||||||
background-color: rgba(88, 166, 255, 0.1);
|
background-color: rgba(56, 189, 248, 0.08);
|
||||||
color: #58a6ff;
|
color: #e2e8f0;
|
||||||
|
border-color: rgba(56, 189, 248, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-nav-toggle {
|
||||||
|
background-color: rgba(56, 189, 248, 0.08);
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.25);
|
||||||
|
color: #38bdf8;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.35rem 0.5rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-nav-toggle:hover {
|
||||||
|
background-color: rgba(56, 189, 248, 0.18);
|
||||||
|
border-color: rgba(56, 189, 248, 0.5);
|
||||||
|
color: #7dd3fc;
|
||||||
|
box-shadow: 0 0 10px rgba(56, 189, 248, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-nav-toggle-footer {
|
||||||
|
background-color: rgba(15, 23, 42, 0.6);
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.15);
|
||||||
|
color: #94a3b8;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-nav-toggle-footer:hover {
|
||||||
|
background-color: rgba(56, 189, 248, 0.12);
|
||||||
|
border-color: rgba(56, 189, 248, 0.35);
|
||||||
|
color: #38bdf8;
|
||||||
|
box-shadow: 0 0 8px rgba(56, 189, 248, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
border-top: 1px solid rgba(56, 189, 248, 0.12);
|
||||||
|
background: rgba(8, 12, 20, 0.5);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-collapsed .nav-item {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-collapsed .nav-item ::deep a {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-collapsed .bi {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-collapsed .sidebar-footer {
|
||||||
|
padding: 0.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 641px) {
|
@media (min-width: 641px) {
|
||||||
|
.nav-toggle-btn {
|
||||||
|
display: inline-flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar-toggler {
|
.navbar-toggler {
|
||||||
display: none;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapse {
|
.collapse {
|
||||||
/* Never collapse the sidebar for wide screens */
|
/* Never collapse the sidebar for wide screens */
|
||||||
display: block;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-scrollable {
|
.nav-scrollable {
|
||||||
/* Allow sidebar to scroll for tall menus */
|
/* Allow sidebar to scroll for tall menus */
|
||||||
height: calc(100vh - 3.5rem);
|
height: calc(100vh - 3.75rem - 3.25rem);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640.98px) {
|
||||||
|
.nav-toggle-btn {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,709 @@
|
|||||||
|
@page "/galaxy"
|
||||||
|
@using System.Globalization
|
||||||
|
@using System.Text.RegularExpressions
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
|
<PageTitle>Galaxy Map</PageTitle>
|
||||||
|
|
||||||
|
<div class="galaxy-page">
|
||||||
|
<div class="d-flex flex-wrap align-items-center justify-content-between mb-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="h2 mb-1 text-light fw-bold">Galaxy Map</h1>
|
||||||
|
<p class="text-secondary mb-0 small">Interactive tactical star chart displaying all planetary systems and hyperlane routes.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap align-items-center gap-3">
|
||||||
|
<div class="map-legend d-flex flex-wrap align-items-center gap-3 px-3 py-2 rounded">
|
||||||
|
<div class="legend-group d-flex align-items-center gap-2">
|
||||||
|
<span class="legend-header text-dim fs-8 text-uppercase">Faction:</span>
|
||||||
|
<div class="legend-item d-flex align-items-center gap-1" title="The Eden Accord (Green)">
|
||||||
|
<span class="legend-dot dot-eden"></span>
|
||||||
|
<span class="legend-text">Eden Accord</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item d-flex align-items-center gap-1" title="The Keeper Council (Blue)">
|
||||||
|
<span class="legend-dot dot-keeper"></span>
|
||||||
|
<span class="legend-text">Keeper Council</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item d-flex align-items-center gap-1" title="The Obsidian Order (Red)">
|
||||||
|
<span class="legend-dot dot-obsidian"></span>
|
||||||
|
<span class="legend-text">Obsidian Order</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="map-stats text-secondary small px-3 py-2 rounded">
|
||||||
|
<span><strong>@_matchingCount</strong> / <strong>@_systems.Count</strong> Systems</span>
|
||||||
|
<span class="mx-1">•</span>
|
||||||
|
<span><strong>@_connections.Count</strong> Hyperlanes</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Control Bar -->
|
||||||
|
<div class="filter-bar zs-glass-card p-3 mb-3 d-flex flex-wrap align-items-center gap-3">
|
||||||
|
<div class="d-flex align-items-center gap-2 filter-item">
|
||||||
|
<label class="filter-label text-dim small fw-semibold text-uppercase">System Type:</label>
|
||||||
|
<select class="form-select form-select-sm scifi-select" @bind="_selectedCategory" @bind:after="ApplyFilters">
|
||||||
|
<option value="">All Types</option>
|
||||||
|
<option value="@StarSystemCategory.HomeworldSystem">Homeworld System</option>
|
||||||
|
<option value="@StarSystemCategory.CoreSystem">Core System</option>
|
||||||
|
<option value="@StarSystemCategory.SideSystem">Side System</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-2 filter-item">
|
||||||
|
<label class="filter-label text-dim small fw-semibold text-uppercase">Mastery:</label>
|
||||||
|
<select class="form-select form-select-sm scifi-select" @bind="_selectedMastery" @bind:after="ApplyFilters">
|
||||||
|
<option value="">All Masteries</option>
|
||||||
|
<option value="__ANY__">Any Mastery</option>
|
||||||
|
@foreach (var mastery in _availableMasteries)
|
||||||
|
{
|
||||||
|
<option value="@mastery">@mastery</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-2 filter-item">
|
||||||
|
<label class="filter-label text-dim small fw-semibold text-uppercase">Special Yields:</label>
|
||||||
|
<select class="form-select form-select-sm scifi-select" @bind="_selectedYield" @bind:after="ApplyFilters">
|
||||||
|
<option value="">All Yields</option>
|
||||||
|
<option value="ANY">Any Special Yield</option>
|
||||||
|
<option value="Influence">Higher Influence</option>
|
||||||
|
<option value="Credits">Higher Credits</option>
|
||||||
|
<option value="Energy">Higher Energy</option>
|
||||||
|
<option value="Items">Higher Items</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (HasActiveFilters)
|
||||||
|
{
|
||||||
|
<button class="btn btn-sm btn-scifi-outline py-1 px-3 ms-auto" @onclick="ResetFilters">
|
||||||
|
<span>Reset Filters</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="galaxy-layout">
|
||||||
|
<div class="galaxy-map-container" @onmouseleave="OnContainerMouseLeave">
|
||||||
|
<svg viewBox="0 0 1000 1000" class="galaxy-svg" preserveAspectRatio="xMidYMid meet"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<!-- Background Grid Pattern -->
|
||||||
|
<pattern id="space-grid" width="50" height="50" patternUnits="userSpaceOnUse">
|
||||||
|
<path d="M 50 0 L 0 0 0 50" fill="none" stroke="rgba(56, 189, 248, 0.04)" stroke-width="1"/>
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<!-- Center Core Glow -->
|
||||||
|
<radialGradient id="galaxy-core-glow" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stop-color="rgba(56, 189, 248, 0.08)"/>
|
||||||
|
<stop offset="60%" stop-color="rgba(16, 185, 129, 0.03)"/>
|
||||||
|
<stop offset="100%" stop-color="rgba(13, 17, 23, 0)"/>
|
||||||
|
</radialGradient>
|
||||||
|
|
||||||
|
<!-- Glow filters for Owners / Factions -->
|
||||||
|
<filter id="glow-eden" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="blur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
<filter id="glow-keeper" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="blur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
<filter id="glow-obsidian" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="blur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Map Background & Grid -->
|
||||||
|
<rect width="1000" height="1000" class="map-bg"/>
|
||||||
|
<rect width="1000" height="1000" fill="url(#space-grid)"/>
|
||||||
|
<rect width="1000" height="1000" fill="url(#galaxy-core-glow)"/>
|
||||||
|
|
||||||
|
<!-- Connection Lines (Hyperlanes) -->
|
||||||
|
<g class="hyperlanes">
|
||||||
|
@foreach (var edge in _connections)
|
||||||
|
{
|
||||||
|
var isFromMatch = _matchingSystemIds.Contains(edge.From.Id);
|
||||||
|
var isToMatch = _matchingSystemIds.Contains(edge.To.Id);
|
||||||
|
var isFilterDimmed = HasActiveFilters && (!isFromMatch || !isToMatch);
|
||||||
|
|
||||||
|
var isHoverHighlighted = _hoveredSystem is not null &&
|
||||||
|
(string.Equals(edge.From.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(edge.To.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
var isHoverDimmed = _hoveredSystem is not null && !isHoverHighlighted;
|
||||||
|
|
||||||
|
var lineClass = isHoverHighlighted ? "hyperlane-line hyperlane-highlighted" :
|
||||||
|
(isHoverDimmed || isFilterDimmed) ? "hyperlane-line hyperlane-dimmed" : "hyperlane-line";
|
||||||
|
<line x1="@SvgNumber(edge.From.X)"
|
||||||
|
y1="@SvgNumber(edge.From.Y)"
|
||||||
|
x2="@SvgNumber(edge.To.X)"
|
||||||
|
y2="@SvgNumber(edge.To.Y)"
|
||||||
|
class="@lineClass"/>
|
||||||
|
}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Planetary Nodes -->
|
||||||
|
<g class="planetary-nodes">
|
||||||
|
@foreach (var system in _systems)
|
||||||
|
{
|
||||||
|
var ownerClass = GetOwnerCssClass(system.Owner);
|
||||||
|
var radius = GetNodeRadius(system.SystemCategory);
|
||||||
|
var isMatch = _matchingSystemIds.Contains(system.Id);
|
||||||
|
var isHovered = _hoveredSystem is not null && string.Equals(system.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isNeighbor = _hoveredConnectedIds.Contains(system.Id);
|
||||||
|
var isDimmed = (_hoveredSystem is not null && !isHovered && !isNeighbor) || (HasActiveFilters && !isMatch);
|
||||||
|
var stateClass = isHovered ? "is-hovered" :
|
||||||
|
isNeighbor ? "is-neighbor" :
|
||||||
|
isDimmed ? "is-dimmed" :
|
||||||
|
(HasActiveFilters && isMatch ? "is-filter-matched" : "");
|
||||||
|
|
||||||
|
<g class="node-group @ownerClass @stateClass"
|
||||||
|
transform="translate(@SvgNumber(system.X), @SvgNumber(system.Y))"
|
||||||
|
@onclick="() => OnNodeClick(system.Id)"
|
||||||
|
@onmouseenter="() => OnNodeMouseEnter(system)"
|
||||||
|
@onmouseleave="() => OnNodeMouseLeave(system)"
|
||||||
|
@onfocus="() => OnNodeFocus(system)"
|
||||||
|
@onblur="OnNodeBlur"
|
||||||
|
@onkeydown="e => OnNodeKeyDown(e, system.Id)"
|
||||||
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
aria-label="@system.Name (@system.SystemCategory.ToDisplayName(), @system.Owner)">
|
||||||
|
|
||||||
|
<!-- Outer aura/glow circle -->
|
||||||
|
<circle r="@SvgNumber(radius + 4)" class="node-aura"/>
|
||||||
|
|
||||||
|
<!-- Main Node Circle -->
|
||||||
|
<circle r="@SvgNumber(radius)" class="node-circle"/>
|
||||||
|
|
||||||
|
<!-- Center core dot for homeworlds and core systems -->
|
||||||
|
@if (system.SystemCategory != StarSystemCategory.SideSystem)
|
||||||
|
{
|
||||||
|
<circle r="@SvgNumber(radius * 0.4)" class="node-core-dot"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- System Name Label -->
|
||||||
|
<text y="@SvgNumber(radius + 10)" class="node-label" text-anchor="middle">@system.Name</text>
|
||||||
|
</g>
|
||||||
|
}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Floating Glassmorphism Hover Card -->
|
||||||
|
@if (_hoveredSystem is not null)
|
||||||
|
{
|
||||||
|
var ownerClass = GetOwnerCssClass(_hoveredSystem.Owner);
|
||||||
|
var higherYields = GetHigherYields(_hoveredSystem);
|
||||||
|
<div class="planet-hover-card @ownerClass"
|
||||||
|
style="@GetTooltipStyle(_hoveredSystem)"
|
||||||
|
role="tooltip"
|
||||||
|
aria-live="polite">
|
||||||
|
<div class="card-header-row d-flex align-items-center justify-content-between mb-2">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="system-owner-indicator"></span>
|
||||||
|
<span class="system-title fw-bold text-light">@_hoveredSystem.Name</span>
|
||||||
|
</div>
|
||||||
|
<span class="badge @CategoryBadgeClass(_hoveredSystem.SystemCategory)">@_hoveredSystem.SystemCategory.ToDisplayName()</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-details-grid">
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">Faction Owner</span>
|
||||||
|
<span class="badge @OwnerBadgeClass(_hoveredSystem.Owner) mt-1">@_hoveredSystem.Owner</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">Occupation Buff</span>
|
||||||
|
<span class="detail-value @(_hoveredSystem.OccupationBuff != null ? "text-buff" : "text-secondary")">
|
||||||
|
@(_hoveredSystem.OccupationBuff ?? "—")
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">System Mastery</span>
|
||||||
|
<span class="detail-value @(_hoveredSystem.SystemMastery != null ? "text-mastery" : "text-secondary")">
|
||||||
|
@(_hoveredSystem.SystemMastery ?? "—")
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">Yield Bonuses</span>
|
||||||
|
<div class="yield-badges d-flex flex-wrap gap-1 mt-1">
|
||||||
|
@if (higherYields.Count == 0)
|
||||||
|
{
|
||||||
|
<span class="yield-badge yield-standard">Standard</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@foreach (var y in higherYields)
|
||||||
|
{
|
||||||
|
<span class="yield-badge yield-@y.ToLowerInvariant()">+@y</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<span class="detail-label">Connected Systems</span>
|
||||||
|
<span class="badge bg-dark text-secondary border border-secondary border-opacity-25">@_hoveredSystem.ConnectedSystems.Count</span>
|
||||||
|
</div>
|
||||||
|
<div class="connected-systems-list text-secondary small mt-1">
|
||||||
|
@string.Join(" • ", _hoveredSystem.ConnectedSystems)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer-hint mt-2 pt-2 border-top border-secondary border-opacity-25 text-center">
|
||||||
|
<span class="hint-text small">Click system to view full details →</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="faction-drawer zs-glass-card @(_drawerCollapsed ? "is-collapsed" : "")" aria-label="Faction occupational bonuses">
|
||||||
|
<div class="drawer-header d-flex align-items-center justify-content-between gap-2">
|
||||||
|
<div>
|
||||||
|
<h2 class="drawer-title text-light fw-bold mb-0">Faction Bonuses</h2>
|
||||||
|
<p class="drawer-subtitle text-secondary mb-0">Occupation buffs by faction</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-scifi-outline drawer-toggle" @onclick="ToggleDrawer"
|
||||||
|
aria-expanded="@(!_drawerCollapsed)"
|
||||||
|
title="@(_drawerCollapsed ? "Expand bonus panel" : "Collapse bonus panel")">
|
||||||
|
@(_drawerCollapsed ? "»" : "«")
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!_drawerCollapsed)
|
||||||
|
{
|
||||||
|
<div class="drawer-body">
|
||||||
|
<div class="faction-picker" role="radiogroup" aria-label="Select faction">
|
||||||
|
@foreach (var owner in StarSystemOwners.All)
|
||||||
|
{
|
||||||
|
var isActive = string.Equals(owner, _selectedFaction, StringComparison.Ordinal);
|
||||||
|
<button role="radio" aria-checked="@isActive"
|
||||||
|
class="faction-pick @(isActive ? "is-active " + GetOwnerCssClass(owner) : "")"
|
||||||
|
@onclick="() => SelectFaction(owner)">
|
||||||
|
<span class="legend-dot @FactionDotClass(owner)"></span>
|
||||||
|
<span class="faction-pick-name">@ShortFactionName(owner)</span>
|
||||||
|
<span class="faction-pick-count">@FactionSystemCount(owner)</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-section">
|
||||||
|
<h3 class="drawer-section-title">Occupational bonuses</h3>
|
||||||
|
<p class="drawer-summary text-secondary">
|
||||||
|
<strong class="text-light">@FactionSystems.Count</strong> systems owned by
|
||||||
|
<strong class="text-light">@ShortFactionName(_selectedFaction)</strong>
|
||||||
|
</p>
|
||||||
|
@if (FactionBonuses.Count == 0)
|
||||||
|
{
|
||||||
|
<p class="text-secondary small mb-0">No occupation buffs recorded for this faction's systems.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<ul class="bonus-list list-unstyled mb-0">
|
||||||
|
@foreach (var bonus in FactionBonuses.OrderBy(b => b.Label))
|
||||||
|
{
|
||||||
|
<li class="bonus-row">
|
||||||
|
<span class="bonus-total">@FormatBonusTotal(bonus)</span>
|
||||||
|
<span class="bonus-label">@bonus.Label</span>
|
||||||
|
<span class="bonus-count">@bonus.PlanetCount @(bonus.PlanetCount == 1 ? "planet" : "planets")</span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-section">
|
||||||
|
<h3 class="drawer-section-title">Planets (@FactionSystems.Count)</h3>
|
||||||
|
<ul class="planet-list list-unstyled mb-0">
|
||||||
|
@foreach (var system in FactionSystems)
|
||||||
|
{
|
||||||
|
<li>
|
||||||
|
<button class="planet-row" @onclick="() => OnNodeClick(system.Id)" title="View @system.Name details">
|
||||||
|
<span class="planet-name">@system.Name</span>
|
||||||
|
<span class="planet-buff">@(system.OccupationBuff ?? "—")</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
public sealed record ConnectionEdge(StarSystemData From, StarSystemData To);
|
||||||
|
|
||||||
|
private IReadOnlyList<StarSystemData> _systems = [];
|
||||||
|
private IReadOnlyList<ConnectionEdge> _connections = [];
|
||||||
|
private IReadOnlyList<string> _availableMasteries = [];
|
||||||
|
private StarSystemData? _hoveredSystem;
|
||||||
|
private readonly HashSet<string> _hoveredConnectedIds = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly HashSet<string> _matchingSystemIds = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private StarSystemCategory? _selectedCategory;
|
||||||
|
private string _selectedMastery = string.Empty;
|
||||||
|
private string _selectedYield = string.Empty;
|
||||||
|
private int _matchingCount;
|
||||||
|
|
||||||
|
// Faction picker: one of the three factions is always selected.
|
||||||
|
private string _selectedFaction = StarSystemOwners.EdenAccord;
|
||||||
|
private bool _drawerCollapsed;
|
||||||
|
|
||||||
|
private bool HasActiveFilters =>
|
||||||
|
_selectedCategory.HasValue ||
|
||||||
|
!string.IsNullOrEmpty(_selectedMastery) ||
|
||||||
|
!string.IsNullOrEmpty(_selectedYield);
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
_systems = StarSystems.All;
|
||||||
|
_connections = BuildUniqueConnections(_systems);
|
||||||
|
_availableMasteries = SystemMasteries.All.Select(m => m.Name).Distinct().OrderBy(m => m).ToList();
|
||||||
|
ApplyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyFilters()
|
||||||
|
{
|
||||||
|
_matchingSystemIds.Clear();
|
||||||
|
foreach (var system in _systems)
|
||||||
|
{
|
||||||
|
if (MatchesFilters(system))
|
||||||
|
{
|
||||||
|
_matchingSystemIds.Add(system.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_matchingCount = _matchingSystemIds.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool MatchesFilters(StarSystemData system)
|
||||||
|
{
|
||||||
|
if (_selectedCategory.HasValue && system.SystemCategory != _selectedCategory.Value)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(_selectedMastery))
|
||||||
|
{
|
||||||
|
if (_selectedMastery == "__ANY__")
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(system.SystemMastery)) return false;
|
||||||
|
}
|
||||||
|
else if (!string.Equals(system.SystemMastery, _selectedMastery, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(_selectedYield))
|
||||||
|
{
|
||||||
|
if (_selectedYield == "ANY")
|
||||||
|
{
|
||||||
|
if (!system.HigherInfluenceYield && !system.HigherCreditYield && !system.HigherEnergyYield && !system.HigherChanceForItems)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_selectedYield == "Influence" && !system.HigherInfluenceYield) return false;
|
||||||
|
else if (_selectedYield == "Credits" && !system.HigherCreditYield) return false;
|
||||||
|
else if (_selectedYield == "Energy" && !system.HigherEnergyYield) return false;
|
||||||
|
else if (_selectedYield == "Items" && !system.HigherChanceForItems) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetFilters()
|
||||||
|
{
|
||||||
|
_selectedCategory = null;
|
||||||
|
_selectedMastery = string.Empty;
|
||||||
|
_selectedYield = string.Empty;
|
||||||
|
ApplyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- faction bonus drawer -----
|
||||||
|
|
||||||
|
public sealed record FactionBonus(string Label, bool IsPercent, double Total, int PlanetCount);
|
||||||
|
|
||||||
|
private void SelectFaction(string owner)
|
||||||
|
{
|
||||||
|
if (string.Equals(_selectedFaction, owner, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedFaction = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleDrawer() => _drawerCollapsed = !_drawerCollapsed;
|
||||||
|
|
||||||
|
private int FactionSystemCount(string owner) =>
|
||||||
|
_systems.Count(s => string.Equals(s.Owner, owner, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
private IReadOnlyList<StarSystemData> FactionSystems =>
|
||||||
|
_systems
|
||||||
|
.Where(s => string.Equals(s.Owner, _selectedFaction, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.OrderBy(s => s.Name)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Groups the selected faction's occupation buffs by bonus type and sums
|
||||||
|
/// the magnitudes, so e.g. two "+15% cooldown reduction" planets total +30%.
|
||||||
|
/// </summary>
|
||||||
|
private IReadOnlyList<FactionBonus> FactionBonuses
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var groups = new Dictionary<(string Label, bool IsPercent), (double Total, int Count)>();
|
||||||
|
foreach (var system in _systems)
|
||||||
|
{
|
||||||
|
if (!string.Equals(system.Owner, _selectedFaction, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(system.OccupationBuff))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (amount, isPercent, label) = ParseOccupationBuff(system.OccupationBuff);
|
||||||
|
var key = (label, isPercent);
|
||||||
|
groups.TryGetValue(key, out var current);
|
||||||
|
groups[key] = (current.Total + amount, current.Count + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
|
.Select(g => new FactionBonus(g.Key.Label, g.Key.IsPercent, g.Value.Total, g.Value.Count))
|
||||||
|
.OrderByDescending(b => b.PlanetCount)
|
||||||
|
.ThenBy(b => b.Label)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Parses buffs shaped like "+50 Starting Hexite" or "+15% cooldown reduction".</summary>
|
||||||
|
private static (double Amount, bool IsPercent, string Label) ParseOccupationBuff(string buff)
|
||||||
|
{
|
||||||
|
var match = Regex.Match(buff.Trim(), @"^\+\s*([0-9]*\.?[0-9]+)\s*(%?)\s*(.+)$");
|
||||||
|
if (match.Success &&
|
||||||
|
double.TryParse(match.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var amount))
|
||||||
|
{
|
||||||
|
return (amount, match.Groups[2].Value == "%", match.Groups[3].Value.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (0, false, buff.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatBonusTotal(FactionBonus bonus) =>
|
||||||
|
$"+{bonus.Total.ToString("0.##", CultureInfo.InvariantCulture)}{(bonus.IsPercent ? "%" : string.Empty)}";
|
||||||
|
|
||||||
|
private static string ShortFactionName(string owner) => owner switch
|
||||||
|
{
|
||||||
|
StarSystemOwners.EdenAccord => "Eden Accord",
|
||||||
|
StarSystemOwners.KeeperCouncil => "Keeper Council",
|
||||||
|
StarSystemOwners.ObsidianOrder => "Obsidian Order",
|
||||||
|
_ => owner
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string FactionDotClass(string owner) => owner switch
|
||||||
|
{
|
||||||
|
StarSystemOwners.EdenAccord => "dot-eden",
|
||||||
|
StarSystemOwners.KeeperCouncil => "dot-keeper",
|
||||||
|
StarSystemOwners.ObsidianOrder => "dot-obsidian",
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
private static IReadOnlyList<ConnectionEdge> BuildUniqueConnections(IReadOnlyList<StarSystemData> allSystems)
|
||||||
|
{
|
||||||
|
var edges = new List<ConnectionEdge>();
|
||||||
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var system in allSystems)
|
||||||
|
{
|
||||||
|
foreach (var targetName in system.ConnectedSystems)
|
||||||
|
{
|
||||||
|
if (!StarSystems.ByName.TryGetValue(targetName, out var target))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var edgeKey = string.Compare(system.Id, target.Id, StringComparison.OrdinalIgnoreCase) < 0
|
||||||
|
? $"{system.Id}::{target.Id}"
|
||||||
|
: $"{target.Id}::{system.Id}";
|
||||||
|
|
||||||
|
if (seen.Add(edgeKey))
|
||||||
|
{
|
||||||
|
edges.Add(new ConnectionEdge(system, target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetHoveredSystem(StarSystemData? system)
|
||||||
|
{
|
||||||
|
_hoveredSystem = system;
|
||||||
|
_hoveredConnectedIds.Clear();
|
||||||
|
if (system is not null)
|
||||||
|
{
|
||||||
|
foreach (var connName in system.ConnectedSystems)
|
||||||
|
{
|
||||||
|
if (StarSystems.ByName.TryGetValue(connName, out var target))
|
||||||
|
{
|
||||||
|
_hoveredConnectedIds.Add(target.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNodeMouseEnter(StarSystemData system)
|
||||||
|
{
|
||||||
|
SetHoveredSystem(system);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNodeMouseLeave(StarSystemData system)
|
||||||
|
{
|
||||||
|
if (_hoveredSystem?.Id == system.Id)
|
||||||
|
{
|
||||||
|
SetHoveredSystem(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNodeFocus(StarSystemData system)
|
||||||
|
{
|
||||||
|
SetHoveredSystem(system);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNodeBlur()
|
||||||
|
{
|
||||||
|
SetHoveredSystem(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnContainerMouseLeave()
|
||||||
|
{
|
||||||
|
SetHoveredSystem(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNodeKeyDown(KeyboardEventArgs e, string systemId)
|
||||||
|
{
|
||||||
|
if (e.Key is "Enter" or " ")
|
||||||
|
{
|
||||||
|
OnNodeClick(systemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNodeClick(string systemId)
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo($"/StarSystem/{systemId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> GetHigherYields(StarSystemData system)
|
||||||
|
{
|
||||||
|
var yields = new List<string>(4);
|
||||||
|
if (system.HigherInfluenceYield) yields.Add("Influence");
|
||||||
|
if (system.HigherCreditYield) yields.Add("Credits");
|
||||||
|
if (system.HigherEnergyYield) yields.Add("Energy");
|
||||||
|
if (system.HigherChanceForItems) yields.Add("Items");
|
||||||
|
return yields;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetTooltipStyle(StarSystemData system)
|
||||||
|
{
|
||||||
|
var posX = system.X / 10.0;
|
||||||
|
var posY = system.Y / 10.0;
|
||||||
|
|
||||||
|
string transform;
|
||||||
|
|
||||||
|
if (posX > 65.0)
|
||||||
|
{
|
||||||
|
if (posY < 20.0)
|
||||||
|
{
|
||||||
|
transform = "translate(calc(-100% - 16px), 0%)";
|
||||||
|
}
|
||||||
|
else if (posY > 80.0)
|
||||||
|
{
|
||||||
|
transform = "translate(calc(-100% - 16px), -100%)";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
transform = "translate(calc(-100% - 16px), -50%)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (posX < 35.0)
|
||||||
|
{
|
||||||
|
if (posY < 20.0)
|
||||||
|
{
|
||||||
|
transform = "translate(16px, 0%)";
|
||||||
|
}
|
||||||
|
else if (posY > 80.0)
|
||||||
|
{
|
||||||
|
transform = "translate(16px, -100%)";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
transform = "translate(16px, -50%)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (posY > 50.0)
|
||||||
|
{
|
||||||
|
transform = "translate(-50%, calc(-100% - 16px))";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
transform = "translate(-50%, 16px)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"left: {SvgNumber(posX)}%; top: {SvgNumber(posY)}%; transform: {transform};";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetOwnerCssClass(string owner) => owner switch
|
||||||
|
{
|
||||||
|
StarSystemOwners.EdenAccord => "owner-eden",
|
||||||
|
StarSystemOwners.KeeperCouncil => "owner-keeper",
|
||||||
|
StarSystemOwners.ObsidianOrder => "owner-obsidian",
|
||||||
|
_ => "owner-eden"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string CategoryBadgeClass(StarSystemCategory category) => category switch
|
||||||
|
{
|
||||||
|
StarSystemCategory.HomeworldSystem => "badge-homeworld",
|
||||||
|
StarSystemCategory.CoreSystem => "badge-core",
|
||||||
|
StarSystemCategory.SideSystem => "badge-side",
|
||||||
|
_ => "badge-side"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string OwnerBadgeClass(string owner) => owner switch
|
||||||
|
{
|
||||||
|
StarSystemOwners.EdenAccord => "badge-eden",
|
||||||
|
StarSystemOwners.KeeperCouncil => "badge-keeper",
|
||||||
|
StarSystemOwners.ObsidianOrder => "badge-obsidian",
|
||||||
|
_ => "badge-side"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static double GetNodeRadius(StarSystemCategory category) => category switch
|
||||||
|
{
|
||||||
|
StarSystemCategory.HomeworldSystem => 7.0,
|
||||||
|
StarSystemCategory.CoreSystem => 6.0,
|
||||||
|
_ => 4.5
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string SvgNumber(double value) => value.ToString(CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,581 @@
|
|||||||
|
.galaxy-page {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-legend,
|
||||||
|
.map-stats {
|
||||||
|
background: rgba(22, 27, 34, 0.85);
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.15);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-eden {
|
||||||
|
background-color: #10b981;
|
||||||
|
box-shadow: 0 0 8px rgba(16, 185, 129, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-keeper {
|
||||||
|
background-color: #38bdf8;
|
||||||
|
box-shadow: 0 0 8px rgba(56, 189, 248, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-obsidian {
|
||||||
|
background-color: #ef4444;
|
||||||
|
box-shadow: 0 0 8px rgba(239, 68, 68, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-text {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.galaxy-map-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #080b10;
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.2);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6), 0 0 20px rgba(56, 189, 248, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.galaxy-svg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-bg {
|
||||||
|
fill: #080b10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hyperlanes (Connection Lines) */
|
||||||
|
.hyperlane-line {
|
||||||
|
stroke: #3a4d6b;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
stroke-opacity: 0.65;
|
||||||
|
stroke-linecap: round;
|
||||||
|
transition: stroke 0.2s ease, stroke-opacity 0.2s ease, stroke-width 0.2s ease, filter 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hyperlane-line.hyperlane-highlighted {
|
||||||
|
stroke: #38bdf8;
|
||||||
|
stroke-width: 2.75;
|
||||||
|
stroke-opacity: 1;
|
||||||
|
filter: drop-shadow(0 0 5px rgba(56, 189, 248, 0.85));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hyperlane-line.hyperlane-dimmed {
|
||||||
|
stroke-opacity: 0.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Node Groups */
|
||||||
|
.node-group {
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-circle {
|
||||||
|
stroke-width: 1.5;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-aura {
|
||||||
|
opacity: 0.35;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
transition: opacity 0.2s ease, r 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-core-dot {
|
||||||
|
fill: #ffffff;
|
||||||
|
opacity: 0.9;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-label {
|
||||||
|
fill: #8b949e;
|
||||||
|
font-size: 8.5px;
|
||||||
|
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
pointer-events: none;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9), 0 0 2px rgba(0, 0, 0, 0.9);
|
||||||
|
transition: fill 0.2s ease, font-weight 0.2s ease, font-size 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Faction owner colors */
|
||||||
|
.owner-eden .node-circle {
|
||||||
|
fill: #10b981;
|
||||||
|
stroke: #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.owner-eden .node-aura {
|
||||||
|
fill: #10b981;
|
||||||
|
filter: url(#glow-eden);
|
||||||
|
}
|
||||||
|
|
||||||
|
.owner-keeper .node-circle {
|
||||||
|
fill: #38bdf8;
|
||||||
|
stroke: #7dd3fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.owner-keeper .node-aura {
|
||||||
|
fill: #38bdf8;
|
||||||
|
filter: url(#glow-keeper);
|
||||||
|
}
|
||||||
|
|
||||||
|
.owner-obsidian .node-circle {
|
||||||
|
fill: #ef4444;
|
||||||
|
stroke: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.owner-obsidian .node-aura {
|
||||||
|
fill: #ef4444;
|
||||||
|
filter: url(#glow-obsidian);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Node State Styles */
|
||||||
|
.node-group.is-hovered .node-circle,
|
||||||
|
.node-group:focus-visible .node-circle {
|
||||||
|
stroke: #ffffff;
|
||||||
|
stroke-width: 2.75;
|
||||||
|
filter: brightness(1.35) drop-shadow(0 0 6px rgba(255, 255, 255, 0.7));
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-group.is-hovered .node-aura,
|
||||||
|
.node-group:focus-visible .node-aura {
|
||||||
|
opacity: 0.95;
|
||||||
|
animation: pulseAura 1.4s infinite alternate ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-group.is-hovered .node-label,
|
||||||
|
.node-group:focus-visible .node-label {
|
||||||
|
fill: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 9.5px;
|
||||||
|
text-shadow: 0 0 6px rgba(56, 189, 248, 0.9), 0 2px 4px rgba(0, 0, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-group.is-neighbor .node-aura {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-group.is-neighbor .node-label {
|
||||||
|
fill: #f0f6fc;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-group.is-neighbor .node-circle {
|
||||||
|
stroke-width: 2;
|
||||||
|
filter: brightness(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-group.is-dimmed {
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulse Aura Animation */
|
||||||
|
@keyframes pulseAura {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1.35);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating Glassmorphism Hover Card */
|
||||||
|
.planet-hover-card {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
min-width: 260px;
|
||||||
|
max-width: 310px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: rgba(13, 17, 23, 0.94);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.8), 0 0 24px rgba(56, 189, 248, 0.12);
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.35);
|
||||||
|
pointer-events: none;
|
||||||
|
animation: popupFadeIn 0.15s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
transition: transform 0.12s ease-out, left 0.12s ease-out, top 0.12s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes popupFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
scale: 0.96;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
scale: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-owner-indicator {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-title {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kind-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Faction owner themes for hover card */
|
||||||
|
.planet-hover-card.owner-eden {
|
||||||
|
border-color: rgba(16, 185, 129, 0.45);
|
||||||
|
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.8), 0 0 24px rgba(16, 185, 129, 0.18);
|
||||||
|
}
|
||||||
|
.planet-hover-card.owner-eden .system-owner-indicator {
|
||||||
|
background-color: #10b981;
|
||||||
|
box-shadow: 0 0 8px rgba(16, 185, 129, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.planet-hover-card.owner-keeper {
|
||||||
|
border-color: rgba(56, 189, 248, 0.45);
|
||||||
|
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.8), 0 0 24px rgba(56, 189, 248, 0.18);
|
||||||
|
}
|
||||||
|
.planet-hover-card.owner-keeper .system-owner-indicator {
|
||||||
|
background-color: #38bdf8;
|
||||||
|
box-shadow: 0 0 8px rgba(56, 189, 248, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.planet-hover-card.owner-obsidian {
|
||||||
|
border-color: rgba(239, 68, 68, 0.45);
|
||||||
|
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.8), 0 0 24px rgba(239, 68, 68, 0.18);
|
||||||
|
}
|
||||||
|
.planet-hover-card.owner-obsidian .system-owner-indicator {
|
||||||
|
background-color: #ef4444;
|
||||||
|
box-shadow: 0 0 8px rgba(239, 68, 68, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Details Grid */
|
||||||
|
.card-details-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: #8b949e;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
display: block;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-buff {
|
||||||
|
color: #34d399;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-mastery {
|
||||||
|
color: #60a5fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Yield Badges */
|
||||||
|
.yield-badges {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yield-badge {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yield-influence {
|
||||||
|
background: rgba(168, 85, 247, 0.18);
|
||||||
|
color: #c084fc;
|
||||||
|
border: 1px solid rgba(168, 85, 247, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yield-credits {
|
||||||
|
background: rgba(234, 179, 8, 0.18);
|
||||||
|
color: #facc15;
|
||||||
|
border: 1px solid rgba(234, 179, 8, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yield-energy {
|
||||||
|
background: rgba(6, 182, 212, 0.18);
|
||||||
|
color: #22d3ee;
|
||||||
|
border: 1px solid rgba(6, 182, 212, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yield-items {
|
||||||
|
background: rgba(16, 185, 129, 0.18);
|
||||||
|
color: #34d399;
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yield-standard {
|
||||||
|
background: rgba(100, 116, 139, 0.15);
|
||||||
|
color: #94a3b8;
|
||||||
|
border: 1px solid rgba(100, 116, 139, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connected-systems-list {
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
color: #8b949e;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer-hint {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-text {
|
||||||
|
color: #38bdf8;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Galaxy layout with right-side faction drawer */
|
||||||
|
.galaxy-layout {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.galaxy-layout .galaxy-map-container {
|
||||||
|
margin: 0;
|
||||||
|
max-width: none;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faction-drawer {
|
||||||
|
width: 330px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem;
|
||||||
|
position: sticky;
|
||||||
|
top: 1rem;
|
||||||
|
max-height: calc(100vh - 2rem);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faction-drawer.is-collapsed {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-subtitle {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-body {
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Faction picker: always exactly one selected */
|
||||||
|
.faction-picker {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faction-pick {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(139, 148, 158, 0.25);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #c9d1d9;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faction-pick:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faction-pick-name {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faction-pick-count {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #8b949e;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 1px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faction-pick.is-active {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faction-pick.is-active.owner-eden {
|
||||||
|
border-color: rgba(16, 185, 129, 0.7);
|
||||||
|
box-shadow: 0 0 12px rgba(16, 185, 129, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faction-pick.is-active.owner-keeper {
|
||||||
|
border-color: rgba(56, 189, 248, 0.7);
|
||||||
|
box-shadow: 0 0 12px rgba(56, 189, 248, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faction-pick.is-active.owner-obsidian {
|
||||||
|
border-color: rgba(239, 68, 68, 0.7);
|
||||||
|
box-shadow: 0 0 12px rgba(239, 68, 68, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drawer sections */
|
||||||
|
.drawer-section-title {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: #8b949e;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-summary {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Aggregated bonus rows */
|
||||||
|
.bonus-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bonus-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: baseline;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(139, 148, 158, 0.18);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bonus-total {
|
||||||
|
color: #34d399;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bonus-label {
|
||||||
|
color: #c9d1d9;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bonus-count {
|
||||||
|
color: #8b949e;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Planet list */
|
||||||
|
.planet-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planet-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: baseline;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #c9d1d9;
|
||||||
|
padding: 5px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planet-row:hover {
|
||||||
|
background: rgba(56, 189, 248, 0.08);
|
||||||
|
border-color: rgba(56, 189, 248, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.planet-name {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planet-buff {
|
||||||
|
color: #34d399;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.galaxy-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faction-drawer {
|
||||||
|
width: 100%;
|
||||||
|
max-height: none;
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,305 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
|
@using ZeroSpace.Model
|
||||||
|
|
||||||
<PageTitle>Home</PageTitle>
|
<PageTitle>ZeroSpace Database & Command Hub</PageTitle>
|
||||||
|
|
||||||
<h1>Hello, world!</h1>
|
<div class="home-container">
|
||||||
|
<!-- Hero Banner -->
|
||||||
|
<div class="zs-hero p-4 p-md-5 mb-4 position-relative overflow-hidden">
|
||||||
|
<div class="hero-glow-orb"></div>
|
||||||
|
<div class="position-relative" style="z-index: 1;">
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-2">
|
||||||
|
<span class="badge badge-pill-scifi badge-side">
|
||||||
|
<span class="hero-status-pulse"></span> SYSTEM ONLINE
|
||||||
|
</span>
|
||||||
|
<span class="text-dim fs-7">ZEROSPACE INTEL ARCHIVE v1.0</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="display-6 fw-bold mb-3 hero-title">ZeroSpace Database & Command Hub</h1>
|
||||||
|
<p class="lead text-muted mb-4 hero-subtitle">
|
||||||
|
Comprehensive tactical star map diagnostics, planetary sector dossiers, doctrine masteries, and unit combat intelligence.
|
||||||
|
</p>
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
<a href="/galaxy" class="btn btn-scifi-primary">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<circle cx="8" cy="8" r="2.5"/>
|
||||||
|
<circle cx="2.5" cy="4" r="1.5"/>
|
||||||
|
<circle cx="13.5" cy="5" r="1.5"/>
|
||||||
|
<circle cx="4" cy="13" r="1.5"/>
|
||||||
|
<circle cx="12" cy="12" r="1.5"/>
|
||||||
|
<path stroke="currentColor" stroke-width="1.2" stroke-linecap="round" d="M2.5 4L8 8m0 0l5.5-3M8 8l-4 5m4-5l4 4" opacity=".65"/>
|
||||||
|
</svg>
|
||||||
|
<span>Launch Galaxy Map</span>
|
||||||
|
</a>
|
||||||
|
<a href="/StarSystem" class="btn btn-scifi-outline">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<circle cx="8" cy="8" r="3.5"/>
|
||||||
|
<path stroke="currentColor" stroke-width="1.2" stroke-linecap="round" d="M8 1v2.5M8 12.5V15M1 8h2.5M12.5 8H15M3 3l1.8 1.8M11.2 11.2l1.8 1.8M3 13l1.8-1.8M11.2 4.8l1.8-1.8" opacity=".75"/>
|
||||||
|
</svg>
|
||||||
|
<span>Browse Systems</span>
|
||||||
|
</a>
|
||||||
|
<a href="/units" class="btn btn-scifi-outline">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.3" fill="none"/>
|
||||||
|
<circle cx="8" cy="8" r="2"/>
|
||||||
|
<path stroke="currentColor" stroke-width="1.3" d="M8 0v4M8 12v4M0 8h4M12 8h4"/>
|
||||||
|
</svg>
|
||||||
|
<span>Unit Intel</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
Welcome to your new app.
|
<!-- Quick Stats Bar -->
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-6 col-lg-3">
|
||||||
|
<div class="zs-glass-card p-3 d-flex align-items-center gap-3 stat-card">
|
||||||
|
<div class="stat-icon-wrapper icon-cyan">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 0a8 8 0 1 0 8 8A8.01 8.01 0 0 0 8 0zm0 14.5a6.5 6.5 0 1 1 6.5-6.5 6.51 6.51 0 0 1-6.5 6.5z"/>
|
||||||
|
<circle cx="8" cy="8" r="3.5"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="fs-4 fw-bold text-white lh-1">@totalSystemsCount</div>
|
||||||
|
<div class="text-dim fs-7 text-uppercase fw-semibold">Star Systems</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-lg-3">
|
||||||
|
<div class="zs-glass-card p-3 d-flex align-items-center gap-3 stat-card">
|
||||||
|
<div class="stat-icon-wrapper icon-emerald">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" d="M2 14L8 2l6 12M4 10h8"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="fs-4 fw-bold text-white lh-1">@totalHyperlanesCount+</div>
|
||||||
|
<div class="text-dim fs-7 text-uppercase fw-semibold">Hyperlanes</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-lg-3">
|
||||||
|
<div class="zs-glass-card p-3 d-flex align-items-center gap-3 stat-card">
|
||||||
|
<div class="stat-icon-wrapper icon-purple">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 0.5l2.2 4.6 5 .7-3.6 3.6.9 5-4.5-2.4-4.5 2.4.9-5-3.6-3.6 5-.7L8 0.5z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="fs-4 fw-bold text-white lh-1">@totalMasteriesCount</div>
|
||||||
|
<div class="text-dim fs-7 text-uppercase fw-semibold">Masteries</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-lg-3">
|
||||||
|
<div class="zs-glass-card p-3 d-flex align-items-center gap-3 stat-card">
|
||||||
|
<div class="stat-icon-wrapper icon-amber">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.3" fill="none"/>
|
||||||
|
<circle cx="8" cy="8" r="2"/>
|
||||||
|
<path stroke="currentColor" stroke-width="1.3" d="M8 0v4M8 12v4M0 8h4M12 8h4"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="fs-4 fw-bold text-white lh-1">@totalUnitsCount</div>
|
||||||
|
<div class="text-dim fs-7 text-uppercase fw-semibold">Combat Units</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feature Hub Cards (2x2 Grid) -->
|
||||||
|
<div class="section-header d-flex align-items-center mb-3">
|
||||||
|
<h2 class="h4 mb-0 text-white">Database Modules</h2>
|
||||||
|
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<!-- Card 1: Galaxy Map -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="zs-glass-card p-4 h-100 d-flex flex-column feature-card">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
|
<div class="feature-icon-badge icon-cyan">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<circle cx="8" cy="8" r="2.5"/>
|
||||||
|
<circle cx="2.5" cy="4" r="1.5"/>
|
||||||
|
<circle cx="13.5" cy="5" r="1.5"/>
|
||||||
|
<circle cx="4" cy="13" r="1.5"/>
|
||||||
|
<circle cx="12" cy="12" r="1.5"/>
|
||||||
|
<path stroke="currentColor" stroke-width="1.2" stroke-linecap="round" d="M2.5 4L8 8m0 0l5.5-3M8 8l-4 5m4-5l4 4" opacity=".65"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="badge badge-pill-scifi badge-side">INTERACTIVE SVG</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="h5 fw-bold text-white mb-2">Interactive Galaxy Map</h3>
|
||||||
|
<p class="text-muted fs-6 mb-4 flex-grow-1">
|
||||||
|
Visual SVG star chart rendering all 78 star systems and hyperlane connection networks. Features instant hover intel cards, planetary buff diagnostics, and direct dossier navigation.
|
||||||
|
</p>
|
||||||
|
<div class="feature-meta mb-3 p-2 rounded">
|
||||||
|
<span class="badge badge-homeworld me-1">@homeworldsCount Homeworlds</span>
|
||||||
|
<span class="badge badge-core me-1">@coreCount Core</span>
|
||||||
|
<span class="badge badge-side">@sideCount Side</span>
|
||||||
|
</div>
|
||||||
|
<a href="/galaxy" class="btn btn-scifi-primary w-100 justify-content-center">
|
||||||
|
Launch Galaxy Map
|
||||||
|
<span class="arrow-icon">→</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 2: Star Systems Directory -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="zs-glass-card p-4 h-100 d-flex flex-column feature-card">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
|
<div class="feature-icon-badge icon-amber">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<circle cx="8" cy="8" r="3.5"/>
|
||||||
|
<path stroke="currentColor" stroke-width="1.2" stroke-linecap="round" d="M8 1v2.5M8 12.5V15M1 8h2.5M12.5 8H15M3 3l1.8 1.8M11.2 11.2l1.8 1.8M3 13l1.8-1.8M11.2 4.8l1.8-1.8" opacity=".75"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="badge badge-pill-scifi badge-core">78 DOSSIERS</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="h5 fw-bold text-white mb-2">Star Systems Directory</h3>
|
||||||
|
<p class="text-muted fs-6 mb-4 flex-grow-1">
|
||||||
|
Complete catalog of planetary systems. Review sector classifications, starting hexite buffs, system masteries, item drop chances, and resource yield bonuses.
|
||||||
|
</p>
|
||||||
|
<div class="feature-meta mb-3 p-2 rounded">
|
||||||
|
<span class="badge badge-pill-scifi badge-side me-1">@buffsCount Occupation Buffs</span>
|
||||||
|
<span class="badge badge-pill-scifi badge-mastery">@higherYieldCount Yield Modifiers</span>
|
||||||
|
</div>
|
||||||
|
<a href="/StarSystem" class="btn btn-scifi-outline w-100 justify-content-center">
|
||||||
|
Browse Star Systems
|
||||||
|
<span class="arrow-icon">→</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 3: System Masteries -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="zs-glass-card p-4 h-100 d-flex flex-column feature-card">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
|
<div class="feature-icon-badge icon-purple">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 0.5l2.2 4.6 5 .7-3.6 3.6.9 5-4.5-2.4-4.5 2.4.9-5-3.6-3.6 5-.7L8 0.5z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="badge badge-pill-scifi badge-mastery">@totalMasteriesCount DOCTRINES</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="h5 fw-bold text-white mb-2">System Masteries</h3>
|
||||||
|
<p class="text-muted fs-6 mb-4 flex-grow-1">
|
||||||
|
Strategic progression doctrines offering tiered battle bonuses, economic multipliers, and global faction enhancements with system availability matrices.
|
||||||
|
</p>
|
||||||
|
<div class="feature-meta mb-3 p-2 rounded text-dim fs-7">
|
||||||
|
<span>Includes 3-Tier Multiplier Progression per Doctrine</span>
|
||||||
|
</div>
|
||||||
|
<a href="/SystemMastery" class="btn btn-scifi-outline w-100 justify-content-center">
|
||||||
|
Explore Masteries
|
||||||
|
<span class="arrow-icon">→</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 4: Combat Units -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="zs-glass-card p-4 h-100 d-flex flex-column feature-card">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
|
<div class="feature-icon-badge icon-emerald">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.3" fill="none"/>
|
||||||
|
<circle cx="8" cy="8" r="2"/>
|
||||||
|
<path stroke="currentColor" stroke-width="1.3" d="M8 0v4M8 12v4M0 8h4M12 8h4"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="badge badge-pill-scifi badge-homeworld">@totalUnitsCount COMBAT UNITS</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="h5 fw-bold text-white mb-2">Unit Roster & Intel</h3>
|
||||||
|
<p class="text-muted fs-6 mb-4 flex-grow-1">
|
||||||
|
Tactical combat unit encyclopedia with detailed Hexite/Flux cost breakdowns, health, DPS-per-cost efficiency metrics, armor classes, and faction hotkeys.
|
||||||
|
</p>
|
||||||
|
<div class="feature-meta mb-3 p-2 rounded text-dim fs-7">
|
||||||
|
<span>Interactive Sortable & Filterable Data Grid</span>
|
||||||
|
</div>
|
||||||
|
<a href="/units" class="btn btn-scifi-outline w-100 justify-content-center">
|
||||||
|
View Unit Intel
|
||||||
|
<span class="arrow-icon">→</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Galaxy Classification Breakdown -->
|
||||||
|
<div class="zs-glass-card p-4 mb-4">
|
||||||
|
<h3 class="h5 fw-bold text-white mb-3 d-flex align-items-center gap-2">
|
||||||
|
<span class="text-cyan">◈</span>
|
||||||
|
<span>Galaxy System Classification</span>
|
||||||
|
</h3>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="classification-box p-3 rounded border-homeworld">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
|
<span class="badge badge-homeworld">Homeworlds</span>
|
||||||
|
<span class="fw-bold text-emerald">@homeworldsCount systems</span>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">Primary faction command centers with critical strategic value.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="classification-box p-3 rounded border-core">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
|
<span class="badge badge-core">Core Systems</span>
|
||||||
|
<span class="fw-bold text-amber">@coreCount systems</span>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">High-density central systems connecting major hyperlane corridors.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="classification-box p-3 rounded border-side">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
|
<span class="badge badge-side">Side Sectors</span>
|
||||||
|
<span class="fw-bold text-cyan">@sideCount systems</span>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">Outer rim border systems offering specialized tactical yields.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private int totalSystemsCount;
|
||||||
|
private int homeworldsCount;
|
||||||
|
private int coreCount;
|
||||||
|
private int sideCount;
|
||||||
|
private int totalHyperlanesCount;
|
||||||
|
private int totalMasteriesCount;
|
||||||
|
private int totalUnitsCount;
|
||||||
|
private int buffsCount;
|
||||||
|
private int higherYieldCount;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
totalSystemsCount = StarSystems.All.Count;
|
||||||
|
homeworldsCount = StarSystems.All.Count(s => s.SystemCategory == StarSystemCategory.HomeworldSystem);
|
||||||
|
coreCount = StarSystems.All.Count(s => s.SystemCategory == StarSystemCategory.CoreSystem);
|
||||||
|
sideCount = StarSystems.All.Count(s => s.SystemCategory == StarSystemCategory.SideSystem);
|
||||||
|
|
||||||
|
// Calculate unique hyperlanes
|
||||||
|
var edges = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var s in StarSystems.All)
|
||||||
|
{
|
||||||
|
foreach (var connected in s.ConnectedSystems)
|
||||||
|
{
|
||||||
|
var edgeKey = string.Compare(s.Id, connected, StringComparison.OrdinalIgnoreCase) < 0
|
||||||
|
? $"{s.Id}--{connected}"
|
||||||
|
: $"{connected}--{s.Id}";
|
||||||
|
edges.Add(edgeKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalHyperlanesCount = edges.Count;
|
||||||
|
|
||||||
|
totalMasteriesCount = SystemMasteries.All.Count;
|
||||||
|
totalUnitsCount = ZeroSpace.Model.Units.All.Count;
|
||||||
|
buffsCount = StarSystems.All.Count(s => !string.IsNullOrWhiteSpace(s.OccupationBuff));
|
||||||
|
higherYieldCount = StarSystems.All.Count(s => s.HigherCreditYield || s.HigherInfluenceYield || s.HigherEnergyYield || s.HigherChanceForItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
.home-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-glow-orb {
|
||||||
|
position: absolute;
|
||||||
|
width: 350px;
|
||||||
|
height: 350px;
|
||||||
|
top: -100px;
|
||||||
|
right: -50px;
|
||||||
|
background: radial-gradient(circle, rgba(56, 189, 248, 0.18) 0%, rgba(16, 185, 129, 0.05) 50%, transparent 70%);
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(40px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
background: linear-gradient(135deg, #ffffff 40%, #7dd3fc 80%, #38bdf8 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
max-width: 750px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-status-pulse {
|
||||||
|
display: inline-block;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
background-color: #38bdf8;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 6px #38bdf8;
|
||||||
|
animation: statusPulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes statusPulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.9);
|
||||||
|
box-shadow: 0 0 0 0 rgba(56, 189, 248, 0.7);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 0 0 6px rgba(56, 189, 248, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0.9);
|
||||||
|
box-shadow: 0 0 0 0 rgba(56, 189, 248, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon-wrapper {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-cyan {
|
||||||
|
background: rgba(56, 189, 248, 0.15);
|
||||||
|
color: #38bdf8;
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.3);
|
||||||
|
box-shadow: 0 0 10px rgba(56, 189, 248, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-emerald {
|
||||||
|
background: rgba(16, 185, 129, 0.15);
|
||||||
|
color: #10b981;
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||||
|
box-shadow: 0 0 10px rgba(16, 185, 129, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-amber {
|
||||||
|
background: rgba(245, 158, 11, 0.15);
|
||||||
|
color: #f59e0b;
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||||
|
box-shadow: 0 0 10px rgba(245, 158, 11, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-purple {
|
||||||
|
background: rgba(167, 139, 250, 0.15);
|
||||||
|
color: #a78bfa;
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.3);
|
||||||
|
box-shadow: 0 0 10px rgba(167, 139, 250, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.7), 0 0 20px rgba(56, 189, 248, 0.15);
|
||||||
|
border-color: rgba(56, 189, 248, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon-badge {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-meta {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover .arrow-icon {
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification-box {
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification-box:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-homeworld {
|
||||||
|
border-left: 3px solid #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-core {
|
||||||
|
border-left: 3px solid #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-side {
|
||||||
|
border-left: 3px solid #38bdf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-cyan {
|
||||||
|
color: #38bdf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-emerald {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-amber {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-purple {
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-dim {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fs-7 {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
@page "/StarSystem/{Id}"
|
||||||
|
|
||||||
|
<PageTitle>@(system?.Name ?? "Star System")</PageTitle>
|
||||||
|
|
||||||
|
@if (system is null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
Unknown star system. <a href="/StarSystem">Back to all star systems</a>.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="section-header d-flex align-items-center mb-4">
|
||||||
|
<h1 class="mb-0">@system.Name</h1>
|
||||||
|
<span class="badge @CategoryBadgeClass(system.SystemCategory) ms-3">@system.SystemCategory.ToDisplayName()</span>
|
||||||
|
<span class="badge @OwnerBadgeClass(system.Owner) ms-2">@system.Owner</span>
|
||||||
|
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
|
||||||
|
<a href="/galaxy" class="btn btn-sm btn-scifi-outline ms-2 d-none d-md-inline-flex">
|
||||||
|
<span>View on Map</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-3">Owner</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
<span class="badge @OwnerBadgeClass(system.Owner)">@system.Owner</span>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Category</dt>
|
||||||
|
<dd class="col-sm-9">@system.SystemCategory.ToDisplayName()</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Occupation buff</dt>
|
||||||
|
<dd class="col-sm-9">@(system.OccupationBuff ?? "—")</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">System mastery</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
@if (system.SystemMastery is null)
|
||||||
|
{
|
||||||
|
@:—
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var masteryUrl = ZeroSpaceLinks.UrlFor(system.SystemMastery);
|
||||||
|
if (masteryUrl is null)
|
||||||
|
{
|
||||||
|
@system.SystemMastery
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="@masteryUrl">@system.SystemMastery</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Yields</dt>
|
||||||
|
<dd class="col-sm-9">@YieldText(system)</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Connected systems</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
@for (var i = 0; i < system.ConnectedSystems.Count; i++)
|
||||||
|
{
|
||||||
|
var name = system.ConnectedSystems[i];
|
||||||
|
var url = ZeroSpaceLinks.UrlFor(name);
|
||||||
|
if (url is null)
|
||||||
|
{
|
||||||
|
<span>@name</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="@url">@name</a>
|
||||||
|
}
|
||||||
|
@if (i < system.ConnectedSystems.Count - 1)
|
||||||
|
{
|
||||||
|
@:,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(system.Body))
|
||||||
|
{
|
||||||
|
<h2>Notes</h2>
|
||||||
|
<MarkdownView Markdown="@system.Body" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
private StarSystemData? system;
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
system = StarSystems.All.FirstOrDefault(s => s.Id.Equals(Id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CategoryBadgeClass(StarSystemCategory category) => category switch
|
||||||
|
{
|
||||||
|
StarSystemCategory.HomeworldSystem => "badge-homeworld",
|
||||||
|
StarSystemCategory.CoreSystem => "badge-core",
|
||||||
|
StarSystemCategory.SideSystem => "badge-side",
|
||||||
|
_ => "badge-side"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string OwnerBadgeClass(string owner) => owner switch
|
||||||
|
{
|
||||||
|
StarSystemOwners.EdenAccord => "badge-eden",
|
||||||
|
StarSystemOwners.KeeperCouncil => "badge-keeper",
|
||||||
|
StarSystemOwners.ObsidianOrder => "badge-obsidian",
|
||||||
|
_ => "badge-side"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string YieldText(StarSystemData s)
|
||||||
|
{
|
||||||
|
var yields = new List<string>();
|
||||||
|
if (s.HigherInfluenceYield)
|
||||||
|
{
|
||||||
|
yields.Add("Influence");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.HigherCreditYield)
|
||||||
|
{
|
||||||
|
yields.Add("Credits");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.HigherEnergyYield)
|
||||||
|
{
|
||||||
|
yields.Add("Energy");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.HigherChanceForItems)
|
||||||
|
{
|
||||||
|
yields.Add("Items");
|
||||||
|
}
|
||||||
|
|
||||||
|
return yields.Count == 0 ? "Standard" : "Higher " + string.Join(", ", yields);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
@page "/StarSystem"
|
||||||
|
|
||||||
|
<PageTitle>Star Systems</PageTitle>
|
||||||
|
|
||||||
|
<div class="section-header d-flex align-items-center mb-4">
|
||||||
|
<h1 class="mb-0">Star Systems</h1>
|
||||||
|
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
|
||||||
|
<a href="/galaxy" class="btn btn-sm btn-scifi-primary ms-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
|
<circle cx="8" cy="8" r="2.5"/>
|
||||||
|
<circle cx="2.5" cy="4" r="1.5"/>
|
||||||
|
<circle cx="13.5" cy="5" r="1.5"/>
|
||||||
|
<path stroke="currentColor" stroke-width="1.2" stroke-linecap="round" d="M2.5 4L8 8m0 0l5.5-3" opacity=".65"/>
|
||||||
|
</svg>
|
||||||
|
<span>Galaxy Map</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach (var category in Enum.GetValues<StarSystemCategory>())
|
||||||
|
{
|
||||||
|
var systems = StarSystems.All.Where(s => s.SystemCategory == category).OrderBy(s => s.Name).ToList();
|
||||||
|
var badgeClass = category switch {
|
||||||
|
StarSystemCategory.HomeworldSystem => "badge-homeworld",
|
||||||
|
StarSystemCategory.CoreSystem => "badge-core",
|
||||||
|
StarSystemCategory.SideSystem => "badge-side",
|
||||||
|
_ => "badge-side"
|
||||||
|
};
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-3">
|
||||||
|
<h2 class="h4 mb-0 text-white">@category.ToDisplayName()</h2>
|
||||||
|
<span class="badge @badgeClass">@systems.Count</span>
|
||||||
|
</div>
|
||||||
|
<ul class="list-unstyled row g-2">
|
||||||
|
@foreach (var system in systems)
|
||||||
|
{
|
||||||
|
<li class="col-md-4 col-lg-3">
|
||||||
|
<a class="d-block p-2 rounded zs-glass-card text-decoration-none" href="/StarSystem/@system.Id">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<span class="fw-semibold text-white">@system.Name</span>
|
||||||
|
<span class="badge @OwnerBadgeClass(system.Owner) fs-8">@system.Owner</span>
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(system.OccupationBuff))
|
||||||
|
{
|
||||||
|
<div class="small text-dim text-truncate mt-1" title="@system.OccupationBuff">@system.OccupationBuff</div>
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private static string OwnerBadgeClass(string owner) => owner switch
|
||||||
|
{
|
||||||
|
StarSystemOwners.EdenAccord => "badge-eden",
|
||||||
|
StarSystemOwners.KeeperCouncil => "badge-keeper",
|
||||||
|
StarSystemOwners.ObsidianOrder => "badge-obsidian",
|
||||||
|
_ => "badge-side"
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
@page "/SystemMastery/{Id}"
|
||||||
|
|
||||||
|
<PageTitle>@(mastery?.Name ?? "System Mastery")</PageTitle>
|
||||||
|
|
||||||
|
@if (mastery is null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
Unknown system mastery. <a href="/SystemMastery">Back to all masteries</a>.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="section-header d-flex align-items-center mb-4">
|
||||||
|
<h1 class="mb-0">@mastery.Name</h1>
|
||||||
|
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Levels</h2>
|
||||||
|
<ol>
|
||||||
|
@for (var i = 0; i < mastery.Levels.Count; i++)
|
||||||
|
{
|
||||||
|
<li><strong>Level @(i + 1):</strong> @mastery.Levels[i]</li>
|
||||||
|
}
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
@if (mastery.Stats.Count > 0)
|
||||||
|
{
|
||||||
|
<h2>Totals</h2>
|
||||||
|
<table class="table table-sm w-auto">
|
||||||
|
<tbody>
|
||||||
|
@foreach (var stat in mastery.Stats)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@ZeroSpaceLinks.Humanize(stat.Key)</td>
|
||||||
|
<td class="text-end">@stat.Value</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>Available on</h2>
|
||||||
|
<ul class="list-unstyled row">
|
||||||
|
@foreach (var name in mastery.AvailableSystems.OrderBy(n => n))
|
||||||
|
{
|
||||||
|
<li class="col-md-4 col-lg-3 mb-1">
|
||||||
|
@{
|
||||||
|
var url = ZeroSpaceLinks.UrlFor(name);
|
||||||
|
}
|
||||||
|
@if (url is null)
|
||||||
|
{
|
||||||
|
@name
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="@url">@name</a>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(mastery.Body))
|
||||||
|
{
|
||||||
|
<h2>Notes</h2>
|
||||||
|
<MarkdownView Markdown="@mastery.Body" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
private SystemMasteryData? mastery;
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
mastery = SystemMasteries.All.FirstOrDefault(m => m.Id.Equals(Id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
@page "/SystemMastery"
|
||||||
|
|
||||||
|
<PageTitle>System Masteries</PageTitle>
|
||||||
|
|
||||||
|
<div class="section-header d-flex align-items-center mb-4">
|
||||||
|
<h1 class="mb-0">System Masteries</h1>
|
||||||
|
<span class="badge badge-mastery ms-3">@SystemMasteries.All.Count Doctrines</span>
|
||||||
|
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group gap-2">
|
||||||
|
@foreach (var mastery in SystemMasteries.All.OrderBy(m => m.Name))
|
||||||
|
{
|
||||||
|
<a class="list-group-item list-group-item-action rounded border p-3 zs-glass-card text-decoration-none" href="/SystemMastery/@mastery.Id">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
|
<span class="fw-bold text-white fs-5">@mastery.Name</span>
|
||||||
|
<span class="badge badge-pill-scifi badge-side">@mastery.AvailableSystems.Count systems</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small">@mastery.Levels[0]</div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using ZeroSpace.Model;
|
||||||
|
|
||||||
|
namespace ZeroSpace.Web.Services;
|
||||||
|
|
||||||
|
public static partial class ZeroSpaceLinks
|
||||||
|
{
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> SystemUrls = StarSystems.All
|
||||||
|
.ToDictionary(static system => system.Name, static system => $"/StarSystem/{system.Id}", StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> MasteryUrls = SystemMasteries.All
|
||||||
|
.ToDictionary(static mastery => mastery.Name, static mastery => $"/SystemMastery/{mastery.Id}", StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public static string? UrlFor(string name)
|
||||||
|
{
|
||||||
|
if (SystemUrls.TryGetValue(name, out var systemUrl))
|
||||||
|
{
|
||||||
|
return systemUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MasteryUrls.TryGetValue(name, out var masteryUrl) ? masteryUrl : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ResolveWikiLinks(string markdown)
|
||||||
|
{
|
||||||
|
return WikiLinkPattern().Replace(markdown, match =>
|
||||||
|
{
|
||||||
|
var name = match.Groups[1].Value.Trim();
|
||||||
|
var url = UrlFor(name);
|
||||||
|
return url is null ? name : $"[{name}]({url})";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Humanize(string camelCase)
|
||||||
|
{
|
||||||
|
var spaced = CamelCasePattern().Replace(camelCase, " ");
|
||||||
|
return spaced.Length == 0 ? spaced : char.ToUpperInvariant(spaced[0]) + spaced[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"\[\[([^\]]+)\]\]")]
|
||||||
|
private static partial Regex WikiLinkPattern();
|
||||||
|
|
||||||
|
[GeneratedRegex("(?<=[a-z0-9])(?=[A-Z])")]
|
||||||
|
private static partial Regex CamelCasePattern();
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.Routing;
|
||||||
|
|
||||||
|
namespace ZeroSpace.Web;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Single source of truth for which pages exist, which are released,
|
||||||
|
/// and which appear in navigation. New pages are hidden until explicitly
|
||||||
|
/// released here (fail-closed): anything not listed, or listed with
|
||||||
|
/// Released=false, redirects to the galaxy map via <see cref="Components.ReleaseGuard"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static class SiteMap
|
||||||
|
{
|
||||||
|
public sealed record Entry(
|
||||||
|
Type Page,
|
||||||
|
string Title,
|
||||||
|
string Href,
|
||||||
|
string IconCss,
|
||||||
|
string? Badge,
|
||||||
|
NavLinkMatch Match,
|
||||||
|
bool Released,
|
||||||
|
bool ShowInNav);
|
||||||
|
|
||||||
|
public static readonly IReadOnlyList<Entry> Entries =
|
||||||
|
[
|
||||||
|
// The releasable surface.
|
||||||
|
new(typeof(Pages.Galaxy), "Galaxy Map", "galaxy", "bi bi-galaxy-nav-menu", "MAP",
|
||||||
|
NavLinkMatch.Prefix, Released: true, ShowInNav: true),
|
||||||
|
|
||||||
|
// Detail dossiers: part of the galaxy experience (linked from the map),
|
||||||
|
// but not top-level navigation destinations.
|
||||||
|
new(typeof(Pages.StarSystemDetail), "System Dossier", "StarSystem", "bi bi-star-systems-nav-menu", null,
|
||||||
|
NavLinkMatch.Prefix, Released: true, ShowInNav: false),
|
||||||
|
new(typeof(Pages.SystemMasteryDetail), "Mastery Dossier", "SystemMastery", "bi bi-masteries-nav-menu", null,
|
||||||
|
NavLinkMatch.Prefix, Released: true, ShowInNav: false),
|
||||||
|
|
||||||
|
// In-development pages: hidden from nav and unreachable by URL for now.
|
||||||
|
new(typeof(Pages.Home), "Home", "", "bi bi-house-door-fill-nav-menu", null,
|
||||||
|
NavLinkMatch.All, Released: false, ShowInNav: false),
|
||||||
|
new(typeof(Pages.StarSystemIndex), "Star Systems", "StarSystem", "bi bi-star-systems-nav-menu", null,
|
||||||
|
NavLinkMatch.Prefix, Released: false, ShowInNav: false),
|
||||||
|
new(typeof(Pages.SystemMasteryIndex), "Masteries", "SystemMastery", "bi bi-masteries-nav-menu", null,
|
||||||
|
NavLinkMatch.Prefix, Released: false, ShowInNav: false),
|
||||||
|
new(typeof(Pages.Units), "Units", "units", "bi bi-units-nav-menu", null,
|
||||||
|
NavLinkMatch.Prefix, Released: false, ShowInNav: false),
|
||||||
|
];
|
||||||
|
|
||||||
|
public static IEnumerable<Entry> NavEntries => Entries.Where(e => e.Released && e.ShowInNav);
|
||||||
|
|
||||||
|
public static bool IsReleased(Type pageType) =>
|
||||||
|
Entries.FirstOrDefault(e => e.Page == pageType)?.Released ?? false;
|
||||||
|
}
|
||||||
@@ -1,21 +1,20 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||||
|
<PropertyGroup>
|
||||||
<PropertyGroup>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<Nullable>enable</Nullable>
|
||||||
<Nullable>enable</Nullable>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
|
||||||
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
|
</PropertyGroup>
|
||||||
</PropertyGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.9" />
|
||||||
<ItemGroup>
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.9"/>
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.12" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all"/>
|
<PackageReference Include="Markdig" Version="1.4.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.12" />
|
<PackageReference Include="Telerik.UI.for.Blazor">
|
||||||
<PackageReference Include="Telerik.UI.for.Blazor" Version="14.0.0" />
|
<Version>13.0.0</Version>
|
||||||
</ItemGroup>
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ZeroSpace.Model\ZeroSpace.Model.csproj" />
|
<ProjectReference Include="..\ZeroSpace.Model\ZeroSpace.Model.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -8,4 +8,6 @@
|
|||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using ZeroSpace.Web
|
@using ZeroSpace.Web
|
||||||
@using ZeroSpace.Web.Layout
|
@using ZeroSpace.Web.Layout
|
||||||
|
@using ZeroSpace.Web.Components
|
||||||
|
@using ZeroSpace.Web.Services
|
||||||
@using ZeroSpace.Model
|
@using ZeroSpace.Model
|
||||||
@@ -1,7 +1,25 @@
|
|||||||
|
:root {
|
||||||
|
--zs-bg-base: #080c14;
|
||||||
|
--zs-bg-surface: #0f172a;
|
||||||
|
--zs-bg-card: rgba(15, 23, 42, 0.75);
|
||||||
|
--zs-bg-card-hover: rgba(30, 41, 59, 0.85);
|
||||||
|
--zs-border-subtle: rgba(56, 189, 248, 0.15);
|
||||||
|
--zs-border-glow: rgba(56, 189, 248, 0.45);
|
||||||
|
--zs-cyan: #38bdf8;
|
||||||
|
--zs-emerald: #10b981;
|
||||||
|
--zs-amber: #f59e0b;
|
||||||
|
--zs-purple: #a78bfa;
|
||||||
|
--zs-rose: #f43f5e;
|
||||||
|
--zs-text-main: #f0f6fc;
|
||||||
|
--zs-text-muted: #94a3b8;
|
||||||
|
--zs-text-dim: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
font-family: 'Segoe UI', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
background-color: #0d1117;
|
background-color: var(--zs-bg-base);
|
||||||
color: #c9d1d9;
|
color: var(--zs-text-main);
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1:focus {
|
h1:focus {
|
||||||
@@ -9,25 +27,234 @@ h1:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
color: #e6edf3;
|
color: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
a, .btn-link {
|
a, .btn-link {
|
||||||
color: #58a6ff;
|
color: var(--zs-cyan);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s ease, text-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover, .btn-link:hover {
|
||||||
|
color: #7dd3fc;
|
||||||
|
text-shadow: 0 0 8px rgba(56, 189, 248, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
color: #8b949e;
|
color: var(--zs-text-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbars */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #080c14;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.2);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #334155;
|
||||||
|
border-color: rgba(56, 189, 248, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sci-Fi Glass Panels & Cards */
|
||||||
|
.zs-glass-card {
|
||||||
|
background: var(--zs-bg-card);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid var(--zs-border-subtle);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
border-color 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zs-glass-card:hover {
|
||||||
|
border-color: var(--zs-border-glow);
|
||||||
|
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.8), 0 0 20px rgba(56, 189, 248, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zs-hero {
|
||||||
|
background: radial-gradient(ellipse at top right, rgba(56, 189, 248, 0.12) 0%, transparent 60%),
|
||||||
|
radial-gradient(ellipse at bottom left, rgba(16, 185, 129, 0.08) 0%, transparent 60%),
|
||||||
|
rgba(15, 23, 42, 0.85);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.25);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sci-Fi Badges */
|
||||||
|
.badge-homeworld {
|
||||||
|
background: rgba(16, 185, 129, 0.18);
|
||||||
|
color: #34d399;
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.45);
|
||||||
|
box-shadow: 0 0 8px rgba(16, 185, 129, 0.25);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-core {
|
||||||
|
background: rgba(245, 158, 11, 0.18);
|
||||||
|
color: #fbbf24;
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.45);
|
||||||
|
box-shadow: 0 0 8px rgba(245, 158, 11, 0.25);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-side {
|
||||||
|
background: rgba(56, 189, 248, 0.18);
|
||||||
|
color: #7dd3fc;
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.45);
|
||||||
|
box-shadow: 0 0 8px rgba(56, 189, 248, 0.25);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-eden {
|
||||||
|
background: rgba(16, 185, 129, 0.18);
|
||||||
|
color: #34d399;
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.45);
|
||||||
|
box-shadow: 0 0 8px rgba(16, 185, 129, 0.25);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-keeper {
|
||||||
|
background: rgba(56, 189, 248, 0.18);
|
||||||
|
color: #7dd3fc;
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.45);
|
||||||
|
box-shadow: 0 0 8px rgba(56, 189, 248, 0.25);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-obsidian {
|
||||||
|
background: rgba(239, 68, 68, 0.18);
|
||||||
|
color: #f87171;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.45);
|
||||||
|
box-shadow: 0 0 8px rgba(239, 68, 68, 0.25);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-mastery {
|
||||||
|
background: rgba(167, 139, 250, 0.18);
|
||||||
|
color: #c4b5fd;
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.45);
|
||||||
|
box-shadow: 0 0 8px rgba(167, 139, 250, 0.25);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-pill-scifi {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.25rem 0.65rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sci-Fi Buttons */
|
||||||
|
.btn-scifi-primary {
|
||||||
|
background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #38bdf8;
|
||||||
|
box-shadow: 0 0 16px rgba(56, 189, 248, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 0.6rem 1.4rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-scifi-primary:hover {
|
||||||
|
background: linear-gradient(135deg, #0369a1 0%, #075985 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 0 24px rgba(56, 189, 248, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-scifi-outline {
|
||||||
|
background: rgba(15, 23, 42, 0.6);
|
||||||
|
color: var(--zs-cyan);
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.4);
|
||||||
|
box-shadow: 0 0 8px rgba(56, 189, 248, 0.15);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 0.6rem 1.4rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-scifi-outline:hover {
|
||||||
|
background: rgba(56, 189, 248, 0.15);
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #38bdf8;
|
||||||
|
box-shadow: 0 0 16px rgba(56, 189, 248, 0.35);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List group dark theme styling */
|
||||||
|
.list-group-item {
|
||||||
|
background-color: rgba(15, 23, 42, 0.7);
|
||||||
|
border-color: rgba(56, 189, 248, 0.12);
|
||||||
|
color: var(--zs-text-main);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item-action:hover, .list-group-item-action:focus {
|
||||||
|
background-color: rgba(30, 41, 59, 0.8);
|
||||||
|
border-color: rgba(56, 189, 248, 0.3);
|
||||||
|
color: #38bdf8;
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables in dark theme */
|
||||||
|
.table {
|
||||||
|
--bs-table-bg: transparent;
|
||||||
|
--bs-table-color: var(--zs-text-main);
|
||||||
|
--bs-table-border-color: rgba(56, 189, 248, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
color: var(--zs-cyan);
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 2px solid rgba(56, 189, 248, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Header Cyber Line */
|
||||||
|
.section-header h1 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header .border-bottom {
|
||||||
|
border-color: rgba(56, 189, 248, 0.2) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #1b6ec2;
|
background-color: #0284c7;
|
||||||
border-color: #1861ac;
|
border-color: #0369a1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #0284c7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -43,14 +270,16 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.validation-message {
|
.validation-message {
|
||||||
color: red;
|
color: #f43f5e;
|
||||||
}
|
}
|
||||||
|
|
||||||
#blazor-error-ui {
|
#blazor-error-ui {
|
||||||
color-scheme: light only;
|
color-scheme: dark only;
|
||||||
background: lightyellow;
|
background: #1e1b4b;
|
||||||
|
color: #f87171;
|
||||||
|
border-top: 1px solid rgba(244, 63, 94, 0.4);
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.5);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: none;
|
display: none;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -65,10 +294,11 @@ p {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0.75rem;
|
right: 0.75rem;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
|
color: #f87171;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blazor-error-boundary {
|
.blazor-error-boundary {
|
||||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #881337;
|
||||||
padding: 1rem 1rem 1rem 3.7rem;
|
padding: 1rem 1rem 1rem 3.7rem;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
@@ -88,16 +318,17 @@ p {
|
|||||||
|
|
||||||
.loading-progress circle {
|
.loading-progress circle {
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: #e0e0e0;
|
stroke: #1e293b;
|
||||||
stroke-width: 0.6rem;
|
stroke-width: 0.6rem;
|
||||||
transform-origin: 50% 50%;
|
transform-origin: 50% 50%;
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-progress circle:last-child {
|
.loading-progress circle:last-child {
|
||||||
stroke: #1b6ec2;
|
stroke: #38bdf8;
|
||||||
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
||||||
transition: stroke-dasharray 0.05s ease-in-out;
|
transition: stroke-dasharray 0.05s ease-in-out;
|
||||||
|
filter: drop-shadow(0 0 6px #38bdf8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-progress-text {
|
.loading-progress-text {
|
||||||
@@ -105,6 +336,8 @@ p {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
|
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
|
||||||
|
color: #38bdf8;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-progress-text:after {
|
.loading-progress-text:after {
|
||||||
@@ -112,7 +345,11 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
color: #c02d76;
|
color: #f43f5e;
|
||||||
|
background: rgba(244, 63, 94, 0.1);
|
||||||
|
padding: 0.15rem 0.35rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
|
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
@@ -10,7 +10,9 @@
|
|||||||
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
|
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||||
<link rel="stylesheet" href="css/app.css" />
|
<link rel="stylesheet" href="css/app.css" />
|
||||||
<link rel="icon" type="image/png" href="favicon.png" />
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
<link href="_content/Telerik.UI.for.Blazor/css/kendo-theme-default/all.css" rel="stylesheet"/>
|
<link href="_content/Telerik.UI.for.Blazor/css/kendo-theme-default/all.css" rel="stylesheet" type="text/css" />
|
||||||
|
|
||||||
|
|
||||||
<link href="ZeroSpace.Web.styles.css" rel="stylesheet" />
|
<link href="ZeroSpace.Web.styles.css" rel="stylesheet" />
|
||||||
<script type="importmap"></script>
|
<script type="importmap"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -29,7 +31,8 @@
|
|||||||
<a href="." class="reload">Reload</a>
|
<a href="." class="reload">Reload</a>
|
||||||
<span class="dismiss">🗙</span>
|
<span class="dismiss">🗙</span>
|
||||||
</div>
|
</div>
|
||||||
<script defer src="_content/Telerik.UI.for.Blazor/js/telerik-blazor.js"></script>
|
<script src="_content/Telerik.UI.for.Blazor/js/telerik-blazor.js" defer></script>
|
||||||
|
|
||||||
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
|
||||||
|
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
|
||||||
|
</Found>
|
||||||
|
</Router>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@using Markdig
|
||||||
|
@using ZeroSpace.Web.Services
|
||||||
|
@((MarkupString)Markdig.Markdown.ToHtml(ZeroSpaceLinks.ResolveWikiLinks(Markdown), Pipeline, null))
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder().Build();
|
||||||
|
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public string Markdown { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Copy project files for caching restore layer
|
||||||
|
COPY ["ZeroSpace.Web/ZeroSpace.Web.csproj", "ZeroSpace.Web/"]
|
||||||
|
COPY ["ZeroSpace.Model/ZeroSpace.Model.csproj", "ZeroSpace.Model/"]
|
||||||
|
|
||||||
|
RUN dotnet restore "ZeroSpace.Web/ZeroSpace.Web.csproj"
|
||||||
|
|
||||||
|
# Copy source code and build
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/ZeroSpace.Web"
|
||||||
|
RUN dotnet publish "ZeroSpace.Web.csproj" -c Release -o /app/publish
|
||||||
|
|
||||||
|
FROM nginx:alpine AS final
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
COPY --from=build /app/publish/wwwroot .
|
||||||
|
COPY ZeroSpace.Web/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
@using Telerik.Blazor.Components
|
||||||
|
@inherits LayoutComponentBase
|
||||||
|
<div class="page">
|
||||||
|
<div class="sidebar">
|
||||||
|
<NavMenu/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<article class="content px-4">
|
||||||
|
<TelerikRootComponent>
|
||||||
|
@Body
|
||||||
|
</TelerikRootComponent>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
.page {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background-image: linear-gradient(180deg, #0d1117 0%, #161b22 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-bottom: 1px solid #d6d5d5;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 3.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a:first-child {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640.98px) {
|
||||||
|
.top-row {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
.page {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 250px;
|
||||||
|
height: 100vh;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row.auth ::deep a:first-child {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row, article {
|
||||||
|
padding-left: 2rem !important;
|
||||||
|
padding-right: 1.5rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<div class="top-row ps-3 navbar navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="">ZeroSpace</a>
|
||||||
|
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
|
||||||
|
<nav class="nav flex-column">
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||||
|
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="units">
|
||||||
|
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Units
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="StarSystem">
|
||||||
|
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Star Systems
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="SystemMastery">
|
||||||
|
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Masteries
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private bool collapseNavMenu = true;
|
||||||
|
|
||||||
|
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
||||||
|
|
||||||
|
private void ToggleNavMenu()
|
||||||
|
{
|
||||||
|
collapseNavMenu = !collapseNavMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
.navbar-toggler {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
min-height: 3.5rem;
|
||||||
|
background-color: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
top: -1px;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-house-door-fill-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-plus-square-fill-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-list-nested-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:first-of-type {
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:last-of-type {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep a {
|
||||||
|
color: #d7d7d7;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 3rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep a.active {
|
||||||
|
background-color: rgba(88, 166, 255, 0.2);
|
||||||
|
color: #58a6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep a:hover {
|
||||||
|
background-color: rgba(88, 166, 255, 0.1);
|
||||||
|
color: #58a6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
.navbar-toggler {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse {
|
||||||
|
/* Never collapse the sidebar for wide screens */
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-scrollable {
|
||||||
|
/* Allow sidebar to scroll for tall menus */
|
||||||
|
height: calc(100vh - 3.5rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@page "/"
|
||||||
|
|
||||||
|
<PageTitle>Home</PageTitle>
|
||||||
|
|
||||||
|
<h1>Hello, world!</h1>
|
||||||
|
|
||||||
|
Welcome to your new app.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
@page "/not-found"
|
||||||
|
@layout MainLayout
|
||||||
|
|
||||||
|
<h3>Not Found</h3>
|
||||||
|
<p>Sorry, the content you are looking for does not exist.</p>
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
@page "/StarSystem/{Id}"
|
||||||
|
|
||||||
|
<PageTitle>@(system?.Name ?? "Star System")</PageTitle>
|
||||||
|
|
||||||
|
@if (system is null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
Unknown star system. <a href="/StarSystem">Back to all star systems</a>.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="section-header d-flex align-items-center mb-4">
|
||||||
|
<h1 class="mb-0">@system.Name</h1>
|
||||||
|
<span class="badge bg-primary ms-3">@system.Kind</span>
|
||||||
|
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-3">Occupation buff</dt>
|
||||||
|
<dd class="col-sm-9">@(system.OccupationBuff ?? "—")</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">System mastery</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
@if (system.SystemMastery is null)
|
||||||
|
{
|
||||||
|
@:—
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var masteryUrl = ZeroSpaceLinks.UrlFor(system.SystemMastery);
|
||||||
|
if (masteryUrl is null)
|
||||||
|
{
|
||||||
|
@system.SystemMastery
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="@masteryUrl">@system.SystemMastery</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Yields</dt>
|
||||||
|
<dd class="col-sm-9">@YieldText(system)</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Connected systems</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
@for (var i = 0; i < system.ConnectedSystems.Count; i++)
|
||||||
|
{
|
||||||
|
var name = system.ConnectedSystems[i];
|
||||||
|
var url = ZeroSpaceLinks.UrlFor(name);
|
||||||
|
if (url is null)
|
||||||
|
{
|
||||||
|
<span>@name</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="@url">@name</a>
|
||||||
|
}
|
||||||
|
@if (i < system.ConnectedSystems.Count - 1)
|
||||||
|
{
|
||||||
|
@:,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(system.Body))
|
||||||
|
{
|
||||||
|
<h2>Notes</h2>
|
||||||
|
<MarkdownView Markdown="@system.Body" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
private StarSystemData? system;
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
system = StarSystems.All.FirstOrDefault(s => s.Id.Equals(Id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string YieldText(StarSystemData s)
|
||||||
|
{
|
||||||
|
var yields = new List<string>();
|
||||||
|
if (s.HigherInfluenceYield)
|
||||||
|
{
|
||||||
|
yields.Add("Influence");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.HigherCreditYield)
|
||||||
|
{
|
||||||
|
yields.Add("Credits");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.HigherEnergyYield)
|
||||||
|
{
|
||||||
|
yields.Add("Energy");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.HigherChanceForItems)
|
||||||
|
{
|
||||||
|
yields.Add("Items");
|
||||||
|
}
|
||||||
|
|
||||||
|
return yields.Count == 0 ? "Standard" : "Higher " + string.Join(", ", yields);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
@page "/StarSystem"
|
||||||
|
|
||||||
|
<PageTitle>Star Systems</PageTitle>
|
||||||
|
|
||||||
|
<div class="section-header d-flex align-items-center mb-4">
|
||||||
|
<h1 class="mb-0">Star Systems</h1>
|
||||||
|
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach (var kind in Enum.GetValues<StarSystemKind>())
|
||||||
|
{
|
||||||
|
var systems = StarSystems.All.Where(s => s.Kind == kind).OrderBy(s => s.Name).ToList();
|
||||||
|
<h2>@kind <span class="text-muted fs-5">(@systems.Count)</span></h2>
|
||||||
|
<ul class="list-unstyled row mb-4">
|
||||||
|
@foreach (var system in systems)
|
||||||
|
{
|
||||||
|
<li class="col-md-4 col-lg-3 mb-1">
|
||||||
|
<a href="/StarSystem/@system.Id">@system.Name</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
@page "/SystemMastery/{Id}"
|
||||||
|
|
||||||
|
<PageTitle>@(mastery?.Name ?? "System Mastery")</PageTitle>
|
||||||
|
|
||||||
|
@if (mastery is null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
Unknown system mastery. <a href="/SystemMastery">Back to all masteries</a>.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="section-header d-flex align-items-center mb-4">
|
||||||
|
<h1 class="mb-0">@mastery.Name</h1>
|
||||||
|
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Levels</h2>
|
||||||
|
<ol>
|
||||||
|
@for (var i = 0; i < mastery.Levels.Count; i++)
|
||||||
|
{
|
||||||
|
<li><strong>Level @(i + 1):</strong> @mastery.Levels[i]</li>
|
||||||
|
}
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
@if (mastery.Stats.Count > 0)
|
||||||
|
{
|
||||||
|
<h2>Totals</h2>
|
||||||
|
<table class="table table-sm w-auto">
|
||||||
|
<tbody>
|
||||||
|
@foreach (var stat in mastery.Stats)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@ZeroSpaceLinks.Humanize(stat.Key)</td>
|
||||||
|
<td class="text-end">@stat.Value</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>Available on</h2>
|
||||||
|
<ul class="list-unstyled row">
|
||||||
|
@foreach (var name in mastery.AvailableSystems.OrderBy(n => n))
|
||||||
|
{
|
||||||
|
<li class="col-md-4 col-lg-3 mb-1">
|
||||||
|
@{
|
||||||
|
var url = ZeroSpaceLinks.UrlFor(name);
|
||||||
|
}
|
||||||
|
@if (url is null)
|
||||||
|
{
|
||||||
|
@name
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="@url">@name</a>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(mastery.Body))
|
||||||
|
{
|
||||||
|
<h2>Notes</h2>
|
||||||
|
<MarkdownView Markdown="@mastery.Body" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
private SystemMasteryData? mastery;
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
mastery = SystemMasteries.All.FirstOrDefault(m => m.Id.Equals(Id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
@page "/SystemMastery"
|
||||||
|
|
||||||
|
<PageTitle>System Masteries</PageTitle>
|
||||||
|
|
||||||
|
<div class="section-header d-flex align-items-center mb-4">
|
||||||
|
<h1 class="mb-0">System Masteries</h1>
|
||||||
|
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group">
|
||||||
|
@foreach (var mastery in SystemMasteries.All.OrderBy(m => m.Name))
|
||||||
|
{
|
||||||
|
<a class="list-group-item list-group-item-action" href="/SystemMastery/@mastery.Id">
|
||||||
|
<div class="fw-bold">@mastery.Name</div>
|
||||||
|
<small class="text-muted">@mastery.Levels[0] · available on @mastery.AvailableSystems.Count systems</small>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
@page "/units"
|
||||||
|
@using ZeroSpace.Model
|
||||||
|
@using ZeroSpace.Web.Services
|
||||||
|
@using Telerik.Blazor
|
||||||
|
@using Telerik.Blazor.Components
|
||||||
|
@inject IUnitsApiClient UnitsApiClient
|
||||||
|
|
||||||
|
<PageTitle>Units</PageTitle>
|
||||||
|
|
||||||
|
<div class="section-header d-flex align-items-center mb-4">
|
||||||
|
<h1 class="mb-0">Units</h1>
|
||||||
|
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (isLoading)
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-center py-5">
|
||||||
|
<div class="spinner-border text-success" role="status">
|
||||||
|
<span class="visually-hidden">Loading units...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (errorMessage != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger d-flex align-items-center justify-content-between" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>Error:</strong> @errorMessage
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-danger btn-sm" @onclick="LoadUnitsAsync">Retry</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (gearNotes == null || gearNotes.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
No units found.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="grid-container">
|
||||||
|
<TelerikGrid Data="@gearNotes" Pageable="true" PageSize="50" Sortable="true" FilterMode="@GridFilterMode.FilterRow"
|
||||||
|
Height="calc(100vh - 250px)">
|
||||||
|
<GridColumns>
|
||||||
|
<GridColumn Field="@(nameof(UnitData.Name))" Title="Name" Width="200px"/>
|
||||||
|
<GridColumn Field="@(nameof(UnitData.Hexite))" Title="Hexite" Width="90px" />
|
||||||
|
<GridColumn Field="@(nameof(UnitData.Flux))" Title="Flux" Width="90px" />
|
||||||
|
|
||||||
|
<GridColumn Field="@(nameof(UnitData.DpsPerTotalCost))" Title="DpsPerTotalCost" Width="90px" />
|
||||||
|
|
||||||
|
<GridColumn Field="@(nameof(UnitData.HealthPerTotalCost))" Title="HealthPerTotalCost" Width="90px" />
|
||||||
|
|
||||||
|
<GridColumn Field="@(nameof(UnitData.Supply))" Title="Supply" Width="90px" />
|
||||||
|
<GridColumn Field="@(nameof(UnitData.ProductionTime))" Title="Production Time" Width="90px" />
|
||||||
|
<GridColumn Field="@(nameof(UnitData.Health))" Title="Health" Width="90px" />
|
||||||
|
<GridColumn Field="@(nameof(UnitData.Shields))" Title="Shields" Width="90px" />
|
||||||
|
<GridColumn Field="@(nameof(UnitData.ArmorRating))" Title="Armor Rating" Width="90px" />
|
||||||
|
<GridColumn Field="@(nameof(UnitData.MovementSpeed))" Title="Movement Speed" Width="90px" />
|
||||||
|
<GridColumn Field="@(nameof(UnitData.DamagePerSecond))" Title="Damage Per Second" Width="90px" />
|
||||||
|
<GridColumn Field="@(nameof(UnitData.AttackRange))" Title="Attack Range" Width="90px" />
|
||||||
|
<GridColumn Field="@(nameof(UnitData.Attributes))" Title="Attributes" Width="90px" />
|
||||||
|
<GridColumn Field="@(nameof(UnitData.Tier))" Title="Tier" Width="90px" />
|
||||||
|
<GridColumn Field="@(nameof(UnitData.Faction))" Title="Faction" Width="90px" />
|
||||||
|
<GridColumn Field="@(nameof(UnitData.Hotkey))" Title="Hotkey" Width="90px" />
|
||||||
|
<GridColumn Field="@(nameof(UnitData.BuildAtSameTime))" Title="Build At Same Time" Width="90px" />
|
||||||
|
<GridColumn Field="@(nameof(UnitData.Limit))" Title="Limit" Width="90px" />
|
||||||
|
</GridColumns>
|
||||||
|
</TelerikGrid>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private List<UnitData>? gearNotes;
|
||||||
|
private bool isLoading = true;
|
||||||
|
private string? errorMessage;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadUnitsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadUnitsAsync()
|
||||||
|
{
|
||||||
|
isLoading = true;
|
||||||
|
errorMessage = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var units = await UnitsApiClient.GetUnitsAsync();
|
||||||
|
gearNotes = units.ToList();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errorMessage = $"Failed to load units: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
|
using ZeroSpace.Web;
|
||||||
|
using ZeroSpace.Web.Services;
|
||||||
|
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
builder.RootComponents.Add<App>("#app");
|
||||||
|
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||||
|
|
||||||
|
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient<IUnitsApiClient, UnitsApiClient>(client =>
|
||||||
|
{
|
||||||
|
var apiBaseUrl = builder.Configuration["services:apiservice:https:0"]
|
||||||
|
?? builder.Configuration["services:apiservice:http:0"]
|
||||||
|
?? builder.Configuration["ApiServiceUrl"]
|
||||||
|
?? builder.HostEnvironment.BaseAddress;
|
||||||
|
|
||||||
|
if (!apiBaseUrl.EndsWith('/'))
|
||||||
|
{
|
||||||
|
apiBaseUrl += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddTelerikBlazor();
|
||||||
|
|
||||||
|
await builder.Build().RunAsync();
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||||
|
"applicationUrl": "http://localhost:5114",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||||
|
"applicationUrl": "https://localhost:7205;http://localhost:5114",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using ZeroSpace.Model;
|
||||||
|
|
||||||
|
namespace ZeroSpace.Web.Services;
|
||||||
|
|
||||||
|
public interface IUnitsApiClient
|
||||||
|
{
|
||||||
|
Task<IReadOnlyList<UnitData>> GetUnitsAsync(string? faction = null, int? tier = null, CancellationToken cancellationToken = default);
|
||||||
|
Task<UnitData?> GetUnitByIdAsync(string id, CancellationToken cancellationToken = default);
|
||||||
|
Task<UnitData?> CreateUnitAsync(UnitData unit, CancellationToken cancellationToken = default);
|
||||||
|
Task<bool> DeleteUnitAsync(string id, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using ZeroSpace.Model;
|
||||||
|
|
||||||
|
namespace ZeroSpace.Web.Services;
|
||||||
|
|
||||||
|
public class UnitsApiClient : IUnitsApiClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ILogger<UnitsApiClient> _logger;
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private const string AllUnitFields = @"
|
||||||
|
id
|
||||||
|
name
|
||||||
|
hexite
|
||||||
|
flux
|
||||||
|
supply
|
||||||
|
productionTime
|
||||||
|
health
|
||||||
|
energy
|
||||||
|
shields
|
||||||
|
armorRating
|
||||||
|
movementSpeed
|
||||||
|
damagePerSecond
|
||||||
|
attackRange
|
||||||
|
attributes
|
||||||
|
tier
|
||||||
|
faction
|
||||||
|
hotkey
|
||||||
|
buildAtSameTime
|
||||||
|
limit";
|
||||||
|
|
||||||
|
public UnitsApiClient(HttpClient httpClient, ILogger<UnitsApiClient> logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<UnitData>> GetUnitsAsync(string? faction = null, int? tier = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dictionary<string, object?>? whereFilter = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(faction) || tier.HasValue)
|
||||||
|
{
|
||||||
|
whereFilter = new Dictionary<string, object?>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(faction))
|
||||||
|
{
|
||||||
|
whereFilter["faction"] = new Dictionary<string, object?> { ["eq"] = faction };
|
||||||
|
}
|
||||||
|
if (tier.HasValue)
|
||||||
|
{
|
||||||
|
whereFilter["tier"] = new Dictionary<string, object?> { ["eq"] = tier.Value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var query = $@"
|
||||||
|
query GetUnits($where: UnitDataFilterInput) {{
|
||||||
|
units(where: $where) {{
|
||||||
|
{AllUnitFields}
|
||||||
|
}}
|
||||||
|
}}";
|
||||||
|
|
||||||
|
var requestBody = new
|
||||||
|
{
|
||||||
|
query,
|
||||||
|
variables = new
|
||||||
|
{
|
||||||
|
where = whereFilter
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsJsonAsync("graphql", requestBody, cancellationToken);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<GraphQLResponse<UnitsQueryResult>>(JsonOptions, cancellationToken);
|
||||||
|
if (result?.Data?.Units is not null && result.Data.Units.Count > 0)
|
||||||
|
{
|
||||||
|
return result.Data.Units;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to load units via GraphQL API service. Falling back to static units list.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to static model data if GraphQL call fails
|
||||||
|
var fallback = Units.All.AsEnumerable();
|
||||||
|
if (!string.IsNullOrWhiteSpace(faction))
|
||||||
|
{
|
||||||
|
fallback = fallback.Where(u => u.Faction.Equals(faction, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tier.HasValue)
|
||||||
|
{
|
||||||
|
fallback = fallback.Where(u => u.Tier == tier.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UnitData?> GetUnitByIdAsync(string id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var query = $@"
|
||||||
|
query GetUnitById($id: String!) {{
|
||||||
|
unitById(id: $id) {{
|
||||||
|
{AllUnitFields}
|
||||||
|
}}
|
||||||
|
}}";
|
||||||
|
|
||||||
|
var requestBody = new
|
||||||
|
{
|
||||||
|
query,
|
||||||
|
variables = new
|
||||||
|
{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsJsonAsync("graphql", requestBody, cancellationToken);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<GraphQLResponse<UnitByIdQueryResult>>(JsonOptions, cancellationToken);
|
||||||
|
if (result?.Data?.UnitById is not null)
|
||||||
|
{
|
||||||
|
return result.Data.UnitById;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to get unit '{Id}' via GraphQL API.", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Units.All.FirstOrDefault(u => u.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UnitData?> CreateUnitAsync(UnitData unit, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var mutation = $@"
|
||||||
|
mutation CreateUnit($input: CreateUnitInput!) {{
|
||||||
|
createUnit(input: $input) {{
|
||||||
|
{AllUnitFields}
|
||||||
|
}}
|
||||||
|
}}";
|
||||||
|
|
||||||
|
var input = new
|
||||||
|
{
|
||||||
|
id = unit.Id,
|
||||||
|
name = unit.Name,
|
||||||
|
hexite = unit.Hexite,
|
||||||
|
flux = unit.Flux,
|
||||||
|
supply = unit.Supply,
|
||||||
|
productionTime = unit.ProductionTime,
|
||||||
|
health = unit.Health,
|
||||||
|
energy = unit.Energy,
|
||||||
|
shields = unit.Shields,
|
||||||
|
armorRating = unit.ArmorRating,
|
||||||
|
movementSpeed = unit.MovementSpeed,
|
||||||
|
damagePerSecond = unit.DamagePerSecond,
|
||||||
|
attackRange = unit.AttackRange,
|
||||||
|
attributes = unit.Attributes,
|
||||||
|
tier = unit.Tier,
|
||||||
|
faction = unit.Faction,
|
||||||
|
hotkey = unit.Hotkey,
|
||||||
|
buildAtSameTime = unit.BuildAtSameTime,
|
||||||
|
limit = unit.Limit
|
||||||
|
};
|
||||||
|
|
||||||
|
var requestBody = new
|
||||||
|
{
|
||||||
|
query = mutation,
|
||||||
|
variables = new
|
||||||
|
{
|
||||||
|
input
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsJsonAsync("graphql", requestBody, cancellationToken);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<GraphQLResponse<CreateUnitMutationResult>>(JsonOptions, cancellationToken);
|
||||||
|
return result?.Data?.CreateUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogError("Failed to create unit via GraphQL. Status: {StatusCode}", response.StatusCode);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error creating unit via GraphQL.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteUnitAsync(string id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var mutation = @"
|
||||||
|
mutation DeleteUnit($id: String!) {
|
||||||
|
deleteUnit(id: $id)
|
||||||
|
}";
|
||||||
|
|
||||||
|
var requestBody = new
|
||||||
|
{
|
||||||
|
query = mutation,
|
||||||
|
variables = new
|
||||||
|
{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsJsonAsync("graphql", requestBody, cancellationToken);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<GraphQLResponse<DeleteUnitMutationResult>>(JsonOptions, cancellationToken);
|
||||||
|
return result?.Data?.DeleteUnit ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting unit '{Id}' via GraphQL.", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class GraphQLResponse<T>
|
||||||
|
{
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public T? Data { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("errors")]
|
||||||
|
public List<GraphQLErrorItem>? Errors { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class GraphQLErrorItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class UnitsQueryResult
|
||||||
|
{
|
||||||
|
[JsonPropertyName("units")]
|
||||||
|
public List<UnitData>? Units { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class UnitByIdQueryResult
|
||||||
|
{
|
||||||
|
[JsonPropertyName("unitById")]
|
||||||
|
public UnitData? UnitById { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CreateUnitMutationResult
|
||||||
|
{
|
||||||
|
[JsonPropertyName("createUnit")]
|
||||||
|
public UnitData? CreateUnit { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DeleteUnitMutationResult
|
||||||
|
{
|
||||||
|
[JsonPropertyName("deleteUnit")]
|
||||||
|
public bool DeleteUnit { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using ZeroSpace.Model;
|
||||||
|
|
||||||
|
namespace ZeroSpace.Web.Services;
|
||||||
|
|
||||||
|
public static partial class ZeroSpaceLinks
|
||||||
|
{
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> SystemUrls = StarSystems.All
|
||||||
|
.ToDictionary(static system => system.Name, static system => $"/StarSystem/{system.Id}", StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> MasteryUrls = SystemMasteries.All
|
||||||
|
.ToDictionary(static mastery => mastery.Name, static mastery => $"/SystemMastery/{mastery.Id}", StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public static string? UrlFor(string name)
|
||||||
|
{
|
||||||
|
if (SystemUrls.TryGetValue(name, out var systemUrl))
|
||||||
|
{
|
||||||
|
return systemUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MasteryUrls.TryGetValue(name, out var masteryUrl) ? masteryUrl : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ResolveWikiLinks(string markdown)
|
||||||
|
{
|
||||||
|
return WikiLinkPattern().Replace(markdown, match =>
|
||||||
|
{
|
||||||
|
var name = match.Groups[1].Value.Trim();
|
||||||
|
var url = UrlFor(name);
|
||||||
|
return url is null ? name : $"[{name}]({url})";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Humanize(string camelCase)
|
||||||
|
{
|
||||||
|
var spaced = CamelCasePattern().Replace(camelCase, " ");
|
||||||
|
return spaced.Length == 0 ? spaced : char.ToUpperInvariant(spaced[0]) + spaced[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"\[\[([^\]]+)\]\]")]
|
||||||
|
private static partial Regex WikiLinkPattern();
|
||||||
|
|
||||||
|
[GeneratedRegex("(?<=[a-z0-9])(?=[A-Z])")]
|
||||||
|
private static partial Regex CamelCasePattern();
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.9"/>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all"/>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.12" />
|
||||||
|
<PackageReference Include="Markdig" Version="1.4.0" />
|
||||||
|
<PackageReference Include="Telerik.UI.for.Blazor" Version="14.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ZeroSpace.Model\ZeroSpace.Model.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using ZeroSpace.Web
|
||||||
|
@using ZeroSpace.Web.Layout
|
||||||
|
@using ZeroSpace.Web.Components
|
||||||
|
@using ZeroSpace.Web.Services
|
||||||
|
@using ZeroSpace.Model
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
events { }
|
||||||
|
http {
|
||||||
|
include mime.types;
|
||||||
|
types {
|
||||||
|
application/wasm wasm;
|
||||||
|
}
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(?:ico|css|js|gif|jpe?g|png|wasm|woff2?|eot|ttf|svg)$ {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"ApiServiceUrl": "https://localhost:7142"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"ApiServiceUrl": "https://localhost:7142"
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
html, body {
|
||||||
|
font-family: 'Segoe UI', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
background-color: #0d1117;
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: #e6edf3;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, .btn-link {
|
||||||
|
color: #58a6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #1b6ec2;
|
||||||
|
border-color: #1861ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||||
|
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding-top: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valid.modified:not([type=checkbox]) {
|
||||||
|
outline: 1px solid #26b050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid {
|
||||||
|
outline: 1px solid red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui {
|
||||||
|
color-scheme: light only;
|
||||||
|
background: lightyellow;
|
||||||
|
bottom: 0;
|
||||||
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: none;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui .dismiss {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blazor-error-boundary {
|
||||||
|
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||||
|
padding: 1rem 1rem 1rem 3.7rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blazor-error-boundary::after {
|
||||||
|
content: "An error has occurred."
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-progress {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
width: 8rem;
|
||||||
|
height: 8rem;
|
||||||
|
inset: 20vh 0 auto 0;
|
||||||
|
margin: 0 auto 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-progress circle {
|
||||||
|
fill: none;
|
||||||
|
stroke: #e0e0e0;
|
||||||
|
stroke-width: 0.6rem;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-progress circle:last-child {
|
||||||
|
stroke: #1b6ec2;
|
||||||
|
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
||||||
|
transition: stroke-dasharray 0.05s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-progress-text {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-progress-text:after {
|
||||||
|
content: var(--blazor-load-percentage-text, "Loading");
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: #c02d76;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Web</title>
|
||||||
|
<base href="/" />
|
||||||
|
<link rel="preload" id="webassembly" />
|
||||||
|
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" href="css/app.css" />
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
|
<link href="_content/Telerik.UI.for.Blazor/css/kendo-theme-default/all.css" rel="stylesheet"/>
|
||||||
|
<link href="ZeroSpace.Web.styles.css" rel="stylesheet" />
|
||||||
|
<script type="importmap"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<svg class="loading-progress">
|
||||||
|
<circle r="40%" cx="50%" cy="50%" />
|
||||||
|
<circle r="40%" cx="50%" cy="50%" />
|
||||||
|
</svg>
|
||||||
|
<div class="loading-progress-text"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="blazor-error-ui">
|
||||||
|
An unhandled error has occurred.
|
||||||
|
<a href="." class="reload">Reload</a>
|
||||||
|
<span class="dismiss">🗙</span>
|
||||||
|
</div>
|
||||||
|
<script defer src="_content/Telerik.UI.for.Blazor/js/telerik-blazor.js"></script>
|
||||||
|
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
ZeroSpace/ZeroSpace.Web_Backup_2026.09.28_11.24.30/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
Vendored
+4085
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+4084
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+597
@@ -0,0 +1,597 @@
|
|||||||
|
/*!
|
||||||
|
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||||
|
* Copyright 2011-2024 The Bootstrap Authors
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||||
|
*/
|
||||||
|
:root,
|
||||||
|
[data-bs-theme=light] {
|
||||||
|
--bs-blue: #0d6efd;
|
||||||
|
--bs-indigo: #6610f2;
|
||||||
|
--bs-purple: #6f42c1;
|
||||||
|
--bs-pink: #d63384;
|
||||||
|
--bs-red: #dc3545;
|
||||||
|
--bs-orange: #fd7e14;
|
||||||
|
--bs-yellow: #ffc107;
|
||||||
|
--bs-green: #198754;
|
||||||
|
--bs-teal: #20c997;
|
||||||
|
--bs-cyan: #0dcaf0;
|
||||||
|
--bs-black: #000;
|
||||||
|
--bs-white: #fff;
|
||||||
|
--bs-gray: #6c757d;
|
||||||
|
--bs-gray-dark: #343a40;
|
||||||
|
--bs-gray-100: #f8f9fa;
|
||||||
|
--bs-gray-200: #e9ecef;
|
||||||
|
--bs-gray-300: #dee2e6;
|
||||||
|
--bs-gray-400: #ced4da;
|
||||||
|
--bs-gray-500: #adb5bd;
|
||||||
|
--bs-gray-600: #6c757d;
|
||||||
|
--bs-gray-700: #495057;
|
||||||
|
--bs-gray-800: #343a40;
|
||||||
|
--bs-gray-900: #212529;
|
||||||
|
--bs-primary: #0d6efd;
|
||||||
|
--bs-secondary: #6c757d;
|
||||||
|
--bs-success: #198754;
|
||||||
|
--bs-info: #0dcaf0;
|
||||||
|
--bs-warning: #ffc107;
|
||||||
|
--bs-danger: #dc3545;
|
||||||
|
--bs-light: #f8f9fa;
|
||||||
|
--bs-dark: #212529;
|
||||||
|
--bs-primary-rgb: 13, 110, 253;
|
||||||
|
--bs-secondary-rgb: 108, 117, 125;
|
||||||
|
--bs-success-rgb: 25, 135, 84;
|
||||||
|
--bs-info-rgb: 13, 202, 240;
|
||||||
|
--bs-warning-rgb: 255, 193, 7;
|
||||||
|
--bs-danger-rgb: 220, 53, 69;
|
||||||
|
--bs-light-rgb: 248, 249, 250;
|
||||||
|
--bs-dark-rgb: 33, 37, 41;
|
||||||
|
--bs-primary-text-emphasis: #052c65;
|
||||||
|
--bs-secondary-text-emphasis: #2b2f32;
|
||||||
|
--bs-success-text-emphasis: #0a3622;
|
||||||
|
--bs-info-text-emphasis: #055160;
|
||||||
|
--bs-warning-text-emphasis: #664d03;
|
||||||
|
--bs-danger-text-emphasis: #58151c;
|
||||||
|
--bs-light-text-emphasis: #495057;
|
||||||
|
--bs-dark-text-emphasis: #495057;
|
||||||
|
--bs-primary-bg-subtle: #cfe2ff;
|
||||||
|
--bs-secondary-bg-subtle: #e2e3e5;
|
||||||
|
--bs-success-bg-subtle: #d1e7dd;
|
||||||
|
--bs-info-bg-subtle: #cff4fc;
|
||||||
|
--bs-warning-bg-subtle: #fff3cd;
|
||||||
|
--bs-danger-bg-subtle: #f8d7da;
|
||||||
|
--bs-light-bg-subtle: #fcfcfd;
|
||||||
|
--bs-dark-bg-subtle: #ced4da;
|
||||||
|
--bs-primary-border-subtle: #9ec5fe;
|
||||||
|
--bs-secondary-border-subtle: #c4c8cb;
|
||||||
|
--bs-success-border-subtle: #a3cfbb;
|
||||||
|
--bs-info-border-subtle: #9eeaf9;
|
||||||
|
--bs-warning-border-subtle: #ffe69c;
|
||||||
|
--bs-danger-border-subtle: #f1aeb5;
|
||||||
|
--bs-light-border-subtle: #e9ecef;
|
||||||
|
--bs-dark-border-subtle: #adb5bd;
|
||||||
|
--bs-white-rgb: 255, 255, 255;
|
||||||
|
--bs-black-rgb: 0, 0, 0;
|
||||||
|
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||||
|
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||||
|
--bs-body-font-size: 1rem;
|
||||||
|
--bs-body-font-weight: 400;
|
||||||
|
--bs-body-line-height: 1.5;
|
||||||
|
--bs-body-color: #212529;
|
||||||
|
--bs-body-color-rgb: 33, 37, 41;
|
||||||
|
--bs-body-bg: #fff;
|
||||||
|
--bs-body-bg-rgb: 255, 255, 255;
|
||||||
|
--bs-emphasis-color: #000;
|
||||||
|
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||||
|
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||||
|
--bs-secondary-color-rgb: 33, 37, 41;
|
||||||
|
--bs-secondary-bg: #e9ecef;
|
||||||
|
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||||
|
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||||
|
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||||
|
--bs-tertiary-bg: #f8f9fa;
|
||||||
|
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||||
|
--bs-heading-color: inherit;
|
||||||
|
--bs-link-color: #0d6efd;
|
||||||
|
--bs-link-color-rgb: 13, 110, 253;
|
||||||
|
--bs-link-decoration: underline;
|
||||||
|
--bs-link-hover-color: #0a58ca;
|
||||||
|
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||||
|
--bs-code-color: #d63384;
|
||||||
|
--bs-highlight-color: #212529;
|
||||||
|
--bs-highlight-bg: #fff3cd;
|
||||||
|
--bs-border-width: 1px;
|
||||||
|
--bs-border-style: solid;
|
||||||
|
--bs-border-color: #dee2e6;
|
||||||
|
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||||
|
--bs-border-radius: 0.375rem;
|
||||||
|
--bs-border-radius-sm: 0.25rem;
|
||||||
|
--bs-border-radius-lg: 0.5rem;
|
||||||
|
--bs-border-radius-xl: 1rem;
|
||||||
|
--bs-border-radius-xxl: 2rem;
|
||||||
|
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||||
|
--bs-border-radius-pill: 50rem;
|
||||||
|
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||||
|
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||||
|
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||||
|
--bs-focus-ring-width: 0.25rem;
|
||||||
|
--bs-focus-ring-opacity: 0.25;
|
||||||
|
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||||
|
--bs-form-valid-color: #198754;
|
||||||
|
--bs-form-valid-border-color: #198754;
|
||||||
|
--bs-form-invalid-color: #dc3545;
|
||||||
|
--bs-form-invalid-border-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme=dark] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bs-body-color: #dee2e6;
|
||||||
|
--bs-body-color-rgb: 222, 226, 230;
|
||||||
|
--bs-body-bg: #212529;
|
||||||
|
--bs-body-bg-rgb: 33, 37, 41;
|
||||||
|
--bs-emphasis-color: #fff;
|
||||||
|
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||||
|
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||||
|
--bs-secondary-color-rgb: 222, 226, 230;
|
||||||
|
--bs-secondary-bg: #343a40;
|
||||||
|
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||||
|
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||||
|
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||||
|
--bs-tertiary-bg: #2b3035;
|
||||||
|
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||||
|
--bs-primary-text-emphasis: #6ea8fe;
|
||||||
|
--bs-secondary-text-emphasis: #a7acb1;
|
||||||
|
--bs-success-text-emphasis: #75b798;
|
||||||
|
--bs-info-text-emphasis: #6edff6;
|
||||||
|
--bs-warning-text-emphasis: #ffda6a;
|
||||||
|
--bs-danger-text-emphasis: #ea868f;
|
||||||
|
--bs-light-text-emphasis: #f8f9fa;
|
||||||
|
--bs-dark-text-emphasis: #dee2e6;
|
||||||
|
--bs-primary-bg-subtle: #031633;
|
||||||
|
--bs-secondary-bg-subtle: #161719;
|
||||||
|
--bs-success-bg-subtle: #051b11;
|
||||||
|
--bs-info-bg-subtle: #032830;
|
||||||
|
--bs-warning-bg-subtle: #332701;
|
||||||
|
--bs-danger-bg-subtle: #2c0b0e;
|
||||||
|
--bs-light-bg-subtle: #343a40;
|
||||||
|
--bs-dark-bg-subtle: #1a1d20;
|
||||||
|
--bs-primary-border-subtle: #084298;
|
||||||
|
--bs-secondary-border-subtle: #41464b;
|
||||||
|
--bs-success-border-subtle: #0f5132;
|
||||||
|
--bs-info-border-subtle: #087990;
|
||||||
|
--bs-warning-border-subtle: #997404;
|
||||||
|
--bs-danger-border-subtle: #842029;
|
||||||
|
--bs-light-border-subtle: #495057;
|
||||||
|
--bs-dark-border-subtle: #343a40;
|
||||||
|
--bs-heading-color: inherit;
|
||||||
|
--bs-link-color: #6ea8fe;
|
||||||
|
--bs-link-hover-color: #8bb9fe;
|
||||||
|
--bs-link-color-rgb: 110, 168, 254;
|
||||||
|
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||||
|
--bs-code-color: #e685b5;
|
||||||
|
--bs-highlight-color: #dee2e6;
|
||||||
|
--bs-highlight-bg: #664d03;
|
||||||
|
--bs-border-color: #495057;
|
||||||
|
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||||
|
--bs-form-valid-color: #75b798;
|
||||||
|
--bs-form-valid-border-color: #75b798;
|
||||||
|
--bs-form-invalid-color: #ea868f;
|
||||||
|
--bs-form-invalid-border-color: #ea868f;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
:root {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--bs-body-font-family);
|
||||||
|
font-size: var(--bs-body-font-size);
|
||||||
|
font-weight: var(--bs-body-font-weight);
|
||||||
|
line-height: var(--bs-body-line-height);
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
text-align: var(--bs-body-text-align);
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: inherit;
|
||||||
|
border: 0;
|
||||||
|
border-top: var(--bs-border-width) solid;
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6, h5, h4, h3, h2, h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--bs-heading-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: calc(1.375rem + 1.5vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: calc(1.325rem + 0.9vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: calc(1.3rem + 0.6vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h3 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: calc(1.275rem + 0.3vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h4 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
cursor: help;
|
||||||
|
-webkit-text-decoration-skip-ink: none;
|
||||||
|
text-decoration-skip-ink: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
address {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
dl {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol ol,
|
||||||
|
ul ul,
|
||||||
|
ol ul,
|
||||||
|
ul ol {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
padding: 0.1875em;
|
||||||
|
color: var(--bs-highlight-color);
|
||||||
|
background-color: var(--bs-highlight-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
position: relative;
|
||||||
|
font-size: 0.75em;
|
||||||
|
line-height: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: var(--bs-font-monospace);
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: 0.875em;
|
||||||
|
color: var(--bs-code-color);
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
a > code {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
padding: 0.1875rem 0.375rem;
|
||||||
|
font-size: 0.875em;
|
||||||
|
color: var(--bs-body-bg);
|
||||||
|
background-color: var(--bs-body-color);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
kbd kbd {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
caption-side: bottom;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
caption {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: inherit;
|
||||||
|
text-align: -webkit-match-parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
tr,
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border-color: inherit;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus:not(:focus-visible) {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
select,
|
||||||
|
optgroup,
|
||||||
|
textarea {
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role=button] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
select:disabled {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type=button],
|
||||||
|
[type=reset],
|
||||||
|
[type=submit] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
button:not(:disabled),
|
||||||
|
[type=button]:not(:disabled),
|
||||||
|
[type=reset]:not(:disabled),
|
||||||
|
[type=submit]:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-focus-inner {
|
||||||
|
padding: 0;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
float: left;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: calc(1.275rem + 0.3vw);
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
legend {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
legend + * {
|
||||||
|
clear: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-datetime-edit-fields-wrapper,
|
||||||
|
::-webkit-datetime-edit-text,
|
||||||
|
::-webkit-datetime-edit-minute,
|
||||||
|
::-webkit-datetime-edit-hour-field,
|
||||||
|
::-webkit-datetime-edit-day-field,
|
||||||
|
::-webkit-datetime-edit-month-field,
|
||||||
|
::-webkit-datetime-edit-year-field {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-inner-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type=search] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* rtl:raw:
|
||||||
|
[type="tel"],
|
||||||
|
[type="url"],
|
||||||
|
[type="email"],
|
||||||
|
[type="number"] {
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-color-swatch-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
font: inherit;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
::file-selector-button {
|
||||||
|
font: inherit;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
output {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||||
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+594
@@ -0,0 +1,594 @@
|
|||||||
|
/*!
|
||||||
|
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||||
|
* Copyright 2011-2024 The Bootstrap Authors
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||||
|
*/
|
||||||
|
:root,
|
||||||
|
[data-bs-theme=light] {
|
||||||
|
--bs-blue: #0d6efd;
|
||||||
|
--bs-indigo: #6610f2;
|
||||||
|
--bs-purple: #6f42c1;
|
||||||
|
--bs-pink: #d63384;
|
||||||
|
--bs-red: #dc3545;
|
||||||
|
--bs-orange: #fd7e14;
|
||||||
|
--bs-yellow: #ffc107;
|
||||||
|
--bs-green: #198754;
|
||||||
|
--bs-teal: #20c997;
|
||||||
|
--bs-cyan: #0dcaf0;
|
||||||
|
--bs-black: #000;
|
||||||
|
--bs-white: #fff;
|
||||||
|
--bs-gray: #6c757d;
|
||||||
|
--bs-gray-dark: #343a40;
|
||||||
|
--bs-gray-100: #f8f9fa;
|
||||||
|
--bs-gray-200: #e9ecef;
|
||||||
|
--bs-gray-300: #dee2e6;
|
||||||
|
--bs-gray-400: #ced4da;
|
||||||
|
--bs-gray-500: #adb5bd;
|
||||||
|
--bs-gray-600: #6c757d;
|
||||||
|
--bs-gray-700: #495057;
|
||||||
|
--bs-gray-800: #343a40;
|
||||||
|
--bs-gray-900: #212529;
|
||||||
|
--bs-primary: #0d6efd;
|
||||||
|
--bs-secondary: #6c757d;
|
||||||
|
--bs-success: #198754;
|
||||||
|
--bs-info: #0dcaf0;
|
||||||
|
--bs-warning: #ffc107;
|
||||||
|
--bs-danger: #dc3545;
|
||||||
|
--bs-light: #f8f9fa;
|
||||||
|
--bs-dark: #212529;
|
||||||
|
--bs-primary-rgb: 13, 110, 253;
|
||||||
|
--bs-secondary-rgb: 108, 117, 125;
|
||||||
|
--bs-success-rgb: 25, 135, 84;
|
||||||
|
--bs-info-rgb: 13, 202, 240;
|
||||||
|
--bs-warning-rgb: 255, 193, 7;
|
||||||
|
--bs-danger-rgb: 220, 53, 69;
|
||||||
|
--bs-light-rgb: 248, 249, 250;
|
||||||
|
--bs-dark-rgb: 33, 37, 41;
|
||||||
|
--bs-primary-text-emphasis: #052c65;
|
||||||
|
--bs-secondary-text-emphasis: #2b2f32;
|
||||||
|
--bs-success-text-emphasis: #0a3622;
|
||||||
|
--bs-info-text-emphasis: #055160;
|
||||||
|
--bs-warning-text-emphasis: #664d03;
|
||||||
|
--bs-danger-text-emphasis: #58151c;
|
||||||
|
--bs-light-text-emphasis: #495057;
|
||||||
|
--bs-dark-text-emphasis: #495057;
|
||||||
|
--bs-primary-bg-subtle: #cfe2ff;
|
||||||
|
--bs-secondary-bg-subtle: #e2e3e5;
|
||||||
|
--bs-success-bg-subtle: #d1e7dd;
|
||||||
|
--bs-info-bg-subtle: #cff4fc;
|
||||||
|
--bs-warning-bg-subtle: #fff3cd;
|
||||||
|
--bs-danger-bg-subtle: #f8d7da;
|
||||||
|
--bs-light-bg-subtle: #fcfcfd;
|
||||||
|
--bs-dark-bg-subtle: #ced4da;
|
||||||
|
--bs-primary-border-subtle: #9ec5fe;
|
||||||
|
--bs-secondary-border-subtle: #c4c8cb;
|
||||||
|
--bs-success-border-subtle: #a3cfbb;
|
||||||
|
--bs-info-border-subtle: #9eeaf9;
|
||||||
|
--bs-warning-border-subtle: #ffe69c;
|
||||||
|
--bs-danger-border-subtle: #f1aeb5;
|
||||||
|
--bs-light-border-subtle: #e9ecef;
|
||||||
|
--bs-dark-border-subtle: #adb5bd;
|
||||||
|
--bs-white-rgb: 255, 255, 255;
|
||||||
|
--bs-black-rgb: 0, 0, 0;
|
||||||
|
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||||
|
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||||
|
--bs-body-font-size: 1rem;
|
||||||
|
--bs-body-font-weight: 400;
|
||||||
|
--bs-body-line-height: 1.5;
|
||||||
|
--bs-body-color: #212529;
|
||||||
|
--bs-body-color-rgb: 33, 37, 41;
|
||||||
|
--bs-body-bg: #fff;
|
||||||
|
--bs-body-bg-rgb: 255, 255, 255;
|
||||||
|
--bs-emphasis-color: #000;
|
||||||
|
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||||
|
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||||
|
--bs-secondary-color-rgb: 33, 37, 41;
|
||||||
|
--bs-secondary-bg: #e9ecef;
|
||||||
|
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||||
|
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||||
|
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||||
|
--bs-tertiary-bg: #f8f9fa;
|
||||||
|
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||||
|
--bs-heading-color: inherit;
|
||||||
|
--bs-link-color: #0d6efd;
|
||||||
|
--bs-link-color-rgb: 13, 110, 253;
|
||||||
|
--bs-link-decoration: underline;
|
||||||
|
--bs-link-hover-color: #0a58ca;
|
||||||
|
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||||
|
--bs-code-color: #d63384;
|
||||||
|
--bs-highlight-color: #212529;
|
||||||
|
--bs-highlight-bg: #fff3cd;
|
||||||
|
--bs-border-width: 1px;
|
||||||
|
--bs-border-style: solid;
|
||||||
|
--bs-border-color: #dee2e6;
|
||||||
|
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||||
|
--bs-border-radius: 0.375rem;
|
||||||
|
--bs-border-radius-sm: 0.25rem;
|
||||||
|
--bs-border-radius-lg: 0.5rem;
|
||||||
|
--bs-border-radius-xl: 1rem;
|
||||||
|
--bs-border-radius-xxl: 2rem;
|
||||||
|
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||||
|
--bs-border-radius-pill: 50rem;
|
||||||
|
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||||
|
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||||
|
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||||
|
--bs-focus-ring-width: 0.25rem;
|
||||||
|
--bs-focus-ring-opacity: 0.25;
|
||||||
|
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||||
|
--bs-form-valid-color: #198754;
|
||||||
|
--bs-form-valid-border-color: #198754;
|
||||||
|
--bs-form-invalid-color: #dc3545;
|
||||||
|
--bs-form-invalid-border-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme=dark] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bs-body-color: #dee2e6;
|
||||||
|
--bs-body-color-rgb: 222, 226, 230;
|
||||||
|
--bs-body-bg: #212529;
|
||||||
|
--bs-body-bg-rgb: 33, 37, 41;
|
||||||
|
--bs-emphasis-color: #fff;
|
||||||
|
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||||
|
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||||
|
--bs-secondary-color-rgb: 222, 226, 230;
|
||||||
|
--bs-secondary-bg: #343a40;
|
||||||
|
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||||
|
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||||
|
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||||
|
--bs-tertiary-bg: #2b3035;
|
||||||
|
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||||
|
--bs-primary-text-emphasis: #6ea8fe;
|
||||||
|
--bs-secondary-text-emphasis: #a7acb1;
|
||||||
|
--bs-success-text-emphasis: #75b798;
|
||||||
|
--bs-info-text-emphasis: #6edff6;
|
||||||
|
--bs-warning-text-emphasis: #ffda6a;
|
||||||
|
--bs-danger-text-emphasis: #ea868f;
|
||||||
|
--bs-light-text-emphasis: #f8f9fa;
|
||||||
|
--bs-dark-text-emphasis: #dee2e6;
|
||||||
|
--bs-primary-bg-subtle: #031633;
|
||||||
|
--bs-secondary-bg-subtle: #161719;
|
||||||
|
--bs-success-bg-subtle: #051b11;
|
||||||
|
--bs-info-bg-subtle: #032830;
|
||||||
|
--bs-warning-bg-subtle: #332701;
|
||||||
|
--bs-danger-bg-subtle: #2c0b0e;
|
||||||
|
--bs-light-bg-subtle: #343a40;
|
||||||
|
--bs-dark-bg-subtle: #1a1d20;
|
||||||
|
--bs-primary-border-subtle: #084298;
|
||||||
|
--bs-secondary-border-subtle: #41464b;
|
||||||
|
--bs-success-border-subtle: #0f5132;
|
||||||
|
--bs-info-border-subtle: #087990;
|
||||||
|
--bs-warning-border-subtle: #997404;
|
||||||
|
--bs-danger-border-subtle: #842029;
|
||||||
|
--bs-light-border-subtle: #495057;
|
||||||
|
--bs-dark-border-subtle: #343a40;
|
||||||
|
--bs-heading-color: inherit;
|
||||||
|
--bs-link-color: #6ea8fe;
|
||||||
|
--bs-link-hover-color: #8bb9fe;
|
||||||
|
--bs-link-color-rgb: 110, 168, 254;
|
||||||
|
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||||
|
--bs-code-color: #e685b5;
|
||||||
|
--bs-highlight-color: #dee2e6;
|
||||||
|
--bs-highlight-bg: #664d03;
|
||||||
|
--bs-border-color: #495057;
|
||||||
|
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||||
|
--bs-form-valid-color: #75b798;
|
||||||
|
--bs-form-valid-border-color: #75b798;
|
||||||
|
--bs-form-invalid-color: #ea868f;
|
||||||
|
--bs-form-invalid-border-color: #ea868f;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
:root {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--bs-body-font-family);
|
||||||
|
font-size: var(--bs-body-font-size);
|
||||||
|
font-weight: var(--bs-body-font-weight);
|
||||||
|
line-height: var(--bs-body-line-height);
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
text-align: var(--bs-body-text-align);
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: inherit;
|
||||||
|
border: 0;
|
||||||
|
border-top: var(--bs-border-width) solid;
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6, h5, h4, h3, h2, h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--bs-heading-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: calc(1.375rem + 1.5vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: calc(1.325rem + 0.9vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: calc(1.3rem + 0.6vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h3 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: calc(1.275rem + 0.3vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h4 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
cursor: help;
|
||||||
|
-webkit-text-decoration-skip-ink: none;
|
||||||
|
text-decoration-skip-ink: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
address {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
dl {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol ol,
|
||||||
|
ul ul,
|
||||||
|
ol ul,
|
||||||
|
ul ol {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
padding: 0.1875em;
|
||||||
|
color: var(--bs-highlight-color);
|
||||||
|
background-color: var(--bs-highlight-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
position: relative;
|
||||||
|
font-size: 0.75em;
|
||||||
|
line-height: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: var(--bs-font-monospace);
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: 0.875em;
|
||||||
|
color: var(--bs-code-color);
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
a > code {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
padding: 0.1875rem 0.375rem;
|
||||||
|
font-size: 0.875em;
|
||||||
|
color: var(--bs-body-bg);
|
||||||
|
background-color: var(--bs-body-color);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
kbd kbd {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
caption-side: bottom;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
caption {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: inherit;
|
||||||
|
text-align: -webkit-match-parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
tr,
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border-color: inherit;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus:not(:focus-visible) {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
select,
|
||||||
|
optgroup,
|
||||||
|
textarea {
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role=button] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
select:disabled {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type=button],
|
||||||
|
[type=reset],
|
||||||
|
[type=submit] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
button:not(:disabled),
|
||||||
|
[type=button]:not(:disabled),
|
||||||
|
[type=reset]:not(:disabled),
|
||||||
|
[type=submit]:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-focus-inner {
|
||||||
|
padding: 0;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
float: right;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: calc(1.275rem + 0.3vw);
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
legend {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
legend + * {
|
||||||
|
clear: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-datetime-edit-fields-wrapper,
|
||||||
|
::-webkit-datetime-edit-text,
|
||||||
|
::-webkit-datetime-edit-minute,
|
||||||
|
::-webkit-datetime-edit-hour-field,
|
||||||
|
::-webkit-datetime-edit-day-field,
|
||||||
|
::-webkit-datetime-edit-month-field,
|
||||||
|
::-webkit-datetime-edit-year-field {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-inner-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type=search] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="tel"],
|
||||||
|
[type="url"],
|
||||||
|
[type="email"],
|
||||||
|
[type="number"] {
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-color-swatch-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
font: inherit;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
::file-selector-button {
|
||||||
|
font: inherit;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
output {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
||||||
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+5402
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+5393
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+12057
File diff suppressed because it is too large
Load Diff
Vendored
+1
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user